diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 6addfd7e96cf..ffc0150ebac5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,34 +1,18 @@ -#------------------------------------------------------------------------------------------------------------- -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. -#------------------------------------------------------------------------------------------------------------- +FROM mcr.microsoft.com/devcontainers/typescript-node:22-bookworm -FROM node:8 +RUN apt-get install -y wget bzip2 -# Configure apt -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get -y install --no-install-recommends apt-utils 2>&1 +# 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 -# Verify git and needed tools are installed -RUN apt-get install -y git procps +ENV PATH="/opt/conda/bin:$PATH" -# Remove outdated yarn from /opt and install via package -# so it can be easily updated via apt-get upgrade yarn -RUN rm -rf /opt/yarn-* \ - && rm -f /usr/local/bin/yarn \ - && rm -f /usr/local/bin/yarnpkg \ - && apt-get install -y curl apt-transport-https lsb-release \ - && curl -sS https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/pubkey.gpg | apt-key add - 2>/dev/null \ - && echo "deb https://dl.yarnpkg.com/$(lsb_release -is | tr '[:upper:]' '[:lower:]')/ stable main" | tee /etc/apt/sources.list.d/yarn.list \ - && apt-get update \ - && apt-get -y install --no-install-recommends yarn +# Sudo apt update needs to run in order for installation of fish to work . +RUN sudo apt update && \ + sudo apt install fish -y -# Install tslint and typescript -RUN npm install -g tslint typescript -# Clean up -RUN apt-get autoremove -y \ - && apt-get clean -y \ - && rm -rf /var/lib/apt/lists/* -ENV DEBIAN_FRONTEND=dialog diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 1b2f0fa72a59..67a8833d30cf 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,30 @@ -// See https://aka.ms/vscode-remote/devcontainer.json for format details. +// For format details, see https://aka.ms/devcontainer.json. { - "name": "Node.js 8 & TypeScript", - "dockerFile": "Dockerfile", - "extensions": [ - "ms-vscode.vscode-typescript-tslint-plugin" - ] -} \ No newline at end of file + "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/.env b/.env deleted file mode 100644 index 71d3e4b59cf3..000000000000 --- a/.env +++ /dev/null @@ -1,2 +0,0 @@ -# Added for Language Server -PYTHONPATH=./uitests 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/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/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 16d732007cc3..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -labels: classify, type-bug ---- - - - -## Environment data - -- VS Code version: XXX -- Extension version (available under the Extensions sidebar): XXX -- OS and version: XXX -- Python version (& distribution if applicable, e.g. Anaconda): XXX -- Type of virtual environment used (N/A | venv | virtualenv | conda | ...): XXX -- Relevant/affected Python packages and their versions: XXX -- Jedi or Language Server? (i.e. what is `"python.jediEnabled"` set to; more info #3977): 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/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/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 588fc8130e81..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -labels: classify, type-enhancement ---- - - - - diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md deleted file mode 100644 index 966c6ab477e8..000000000000 --- a/.github/ISSUE_TEMPLATE/question.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -name: Question -about: Please search existing issues or Stack Overflow (https://stackoverflow.com/questions/tagged/visual-studio-code+python) to avoid creating duplicates -labels: classify ---- - - diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index b8bc43886065..000000000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,16 +0,0 @@ -For # - - -- [ ] Pull request represents a single change (i.e. not fixing disparate/unrelated things in a single PR) -- [ ] Title summarizes what is changing -- [ ] Has a [news entry](https://github.com/Microsoft/vscode-python/tree/master/news) file (remember to thank yourself!) -- [ ] Appropriate comments and documentation strings in the code -- [ ] Has sufficient logging. -- [ ] Has telemetry for enhancements. -- [ ] Unit tests & system/integration tests are added/updated -- [ ] [Test plan](https://github.com/Microsoft/vscode-python/blob/master/.github/test_plan.md) is updated as appropriate -- [ ] [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/package-lock.json) has been regenerated by running `npm install` (if dependencies have changed) -- [ ] The wiki is updated with any design decisions/details. diff --git a/.github/actions/build-vsix/action.yml b/.github/actions/build-vsix/action.yml new file mode 100644 index 000000000000..912ff2c34a74 --- /dev/null +++ b/.github/actions/build-vsix/action.yml @@ -0,0 +1,101 @@ +name: 'Build VSIX' +description: "Build the extension's VSIX" + +inputs: + node_version: + description: 'Version of Node to install' + required: true + vsix_name: + description: 'Name to give the final VSIX' + required: true + artifact_name: + description: 'Name to give the artifact containing the VSIX' + required: true + cargo_target: + description: 'Cargo build target for the native build' + required: true + vsix_target: + description: 'vsix build target for the native build' + required: true + +runs: + using: 'composite' + steps: + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + # Jedi LS depends on dataclasses which is not in the stdlib in Python 3.7. + - name: Use Python 3.10 for JediLSP + uses: actions/setup-python@v6 + with: + python-version: '3.10' + cache: 'pip' + cache-dependency-path: | + requirements.txt + python_files/jedilsp_requirements/requirements.txt + + - name: Upgrade Pip + run: python -m pip install -U pip + shell: bash + + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox + shell: bash + + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs + shell: bash + + - name: Add Rustup target + run: rustup target add "${CARGO_TARGET}" + shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + env: + CARGO_TARGET: ${{ inputs.cargo_target }} + + - name: Run npm ci + run: npm ci --prefer-offline + shell: bash + + - name: Update optional extension dependencies + run: npm run addExtensionPackDependencies + shell: bash + + - name: Build Webpack + run: | + npx gulp clean + npx gulp prePublishBundle + shell: bash + + - name: Build VSIX + run: npx vsce package --target "${VSIX_TARGET}" --out ms-python-insiders.vsix --pre-release + shell: bash + env: + VSIX_TARGET: ${{ inputs.vsix_target }} + + - name: Rename VSIX + # Move to a temp name in case the specified name happens to match the default name. + run: mv ms-python-insiders.vsix ms-python-temp.vsix && mv ms-python-temp.vsix "${VSIX_NAME}" + shell: bash + env: + VSIX_NAME: ${{ inputs.vsix_name }} + + - name: Upload VSIX + uses: actions/upload-artifact@v7 + with: + name: ${{ inputs.artifact_name }} + path: ${{ inputs.vsix_name }} + if-no-files-found: error + retention-days: 7 diff --git a/.github/actions/lint/action.yml b/.github/actions/lint/action.yml new file mode 100644 index 000000000000..0bd5a2d8e1e2 --- /dev/null +++ b/.github/actions/lint/action.yml @@ -0,0 +1,50 @@ +name: 'Lint' +description: 'Lint TypeScript and Python code' + +inputs: + node_version: + description: 'Version of Node to install' + required: true + +runs: + using: 'composite' + steps: + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + + - name: Install Node dependencies + run: npm ci --prefer-offline + shell: bash + + - name: Run `gulp prePublishNonBundle` + run: npx gulp prePublishNonBundle + shell: bash + + - name: Check dependencies + run: npm run checkDependencies + shell: bash + + - name: Lint TypeScript code + run: npm run lint + shell: bash + + - name: Check TypeScript format + run: npm run format-check + shell: bash + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: '3.x' + cache: 'pip' + + - name: Run Ruff + run: | + python -m pip install -U "ruff" + python -m ruff check . + python -m ruff format --check + working-directory: python_files + shell: bash diff --git a/.github/actions/smoke-tests/action.yml b/.github/actions/smoke-tests/action.yml new file mode 100644 index 000000000000..0531ef5d42a3 --- /dev/null +++ b/.github/actions/smoke-tests/action.yml @@ -0,0 +1,66 @@ +name: 'Smoke tests' +description: 'Run smoke tests' + +inputs: + node_version: + description: 'Version of Node to install' + required: true + artifact_name: + description: 'Name of the artifact containing the VSIX' + required: true + +runs: + using: 'composite' + steps: + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node_version }} + cache: 'npm' + + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.x' + cache: 'pip' + cache-dependency-path: | + build/test-requirements.txt + requirements.txt + + - name: Install dependencies (npm ci) + run: npm ci --prefer-offline + shell: bash + + - name: Install Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + options: '-t ./python_files/lib/python --implementation py' + + - name: pip install system test requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + shell: bash + + # Bits from the VSIX are reused by smokeTest.ts to speed things up. + - name: Download VSIX + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.artifact_name }} + + - name: Prepare for smoke tests + run: npx tsc -p ./ + shell: bash + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "CI_PYTHON_PATH=python" >> $GITHUB_ENV + echo "CI_DISABLE_AUTO_SELECTION=1" >> $GITHUB_ENV + shell: bash + + - name: Run smoke tests + env: + DISPLAY: 10 + INSTALL_JUPYTER_EXTENSION: true + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: node --no-force-async-hooks-checks ./out/test/smokeTest.js diff --git a/.github/commands.json b/.github/commands.json new file mode 100644 index 000000000000..2fb6684a7ee6 --- /dev/null +++ b/.github/commands.json @@ -0,0 +1,157 @@ +[ + { + "type": "label", + "name": "*question", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because it is a question about using the Python extension for VS Code rather than an issue or feature request. We recommend browsing resources such as our [Python documentation](https://code.visualstudio.com/docs/languages/python) and our [Discussions page](https://github.com/microsoft/vscode-python/discussions). You may also find help on [StackOverflow](https://stackoverflow.com/questions/tagged/vscode-python), where the community has already answered thousands of similar questions. \n\nHappy Coding!" + }, + { + "type": "label", + "name": "*dev-question", + "action": "close", + "reason": "not_planned", + "comment": "We have a great extension developer community over on [GitHub discussions](https://github.com/microsoft/vscode-discussions/discussions) and [Slack](https://vscode-dev-community.slack.com/) where extension authors help each other. This is a great place for you to ask questions and find support.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*extension-candidate", + "action": "close", + "reason": "not_planned", + "comment": "We try to keep the Python extension lean and we think the functionality you're asking for is great for a VS Code extension. You might be able to find one that suits you in the [VS Code Marketplace](https://aka.ms/vscodemarketplace) already. If not, in a few simple steps you can get started [writing your own extension](https://aka.ms/vscodewritingextensions) or leverage our [tool extension template](https://github.com/microsoft/vscode-python-tools-extension-template) to get started. In addition, check out the [vscode-python-environments](https://github.com/microsoft/vscode-python-environments) as this may be the right spot for your request. \n\nHappy Coding!" + }, + { + "type": "label", + "name": "*not-reproducible", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we are unable to reproduce the problem with the steps you describe. Chances are we've already fixed your problem in a recent version of the Python extension, so we recommend updating to the latest version and trying again. If you continue to experience this issue, please ask us to reopen the issue and provide us with more detail.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*out-of-scope", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode-python/wiki/Issue-Management#criteria-for-closing-out-of-scope-feature-requests) in the foreseeable future. If you disagree and feel that this issue is crucial: we are happy to listen and to reconsider.\n\nIf you wonder what we are up to, please see our [roadmap](https://aka.ms/pythonvscoderoadmap) and [issue reporting guidelines]( https://github.com/microsoft/vscode-python/wiki/Issue-Management).\n\nThanks for your understanding, and happy coding!" + }, + { + "type": "label", + "name": "wont-fix", + "action": "close", + "reason": "not_planned", + "comment": "We closed this issue because we [don't plan to address it](https://github.com/microsoft/vscode/wiki/Issue-Grooming#wont-fix-bugs).\n\nThanks for your understanding, and happy coding!" + }, + { + "type": "label", + "name": "*caused-by-extension", + "action": "close", + "reason": "not_planned", + "comment": "This issue is caused by an extension, please file it with the repository (or contact) the extension has linked in its overview in VS Code or the [marketplace](https://aka.ms/vscodemarketplace) for VS Code. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). If you don't know which extension is causing the problem, you can run `Help: Start extension bisect` from the command palette (F1) to help identify the problem extension.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "*as-designed", + "action": "close", + "reason": "not_planned", + "comment": "The described behavior is how it is expected to work. If you disagree, please explain what is expected and what is not in more detail. See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "label", + "name": "L10N", + "assign": [ + "csigs", + "TylerLeonhardt" + ] + }, + { + "type": "label", + "name": "*duplicate", + "action": "close", + "reason": "not_planned", + "comment": "Thanks for creating this issue! We figured it's covering the same as another one we already have. Thus, we closed this one as a duplicate. You can search for [similar existing issues](${duplicateQuery}). See also our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "verified", + "allowUsers": [ + "@author" + ], + "action": "updateLabels", + "addLabel": "verified", + "removeLabel": "author-verification-requested", + "requireLabel": "author-verification-requested", + "disallowLabel": "unreleased" + }, + { + "type": "comment", + "name": "confirm", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "updateLabels", + "addLabel": "confirmed", + "removeLabel": "confirmation-pending" + }, + { + "type": "label", + "name": "*off-topic", + "action": "close", + "reason": "not_planned", + "comment": "Thanks for creating this issue. We think this issue is unactionable or unrelated to the goals of this project. Please follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting).\n\nHappy Coding!" + }, + { + "type": "comment", + "name": "gifPlease", + "allowUsers": [ + "cleidigh", + "usernamehw", + "gjsjohnmurray", + "IllusionMH" + ], + "action": "comment", + "addLabel": "info-needed", + "comment": "Thanks for reporting this issue! Unfortunately, it's hard for us to understand what issue you're seeing. Please help us out by providing a screen recording showing exactly what isn't working as expected. While we can work with most standard formats, `.gif` files are preferred as they are displayed inline on GitHub. You may find https://gifcap.dev helpful as a browser-based gif recording tool.\n\nIf the issue depends on keyboard input, you can help us by enabling screencast mode for the recording (`Developer: Toggle Screencast Mode` in the command palette). Lastly, please attach this file via the GitHub web interface as emailed responses will strip files out from the issue.\n\nHappy coding!" + }, + { + "type": "label", + "name": "*workspace-trust-docs", + "action": "close", + "reason": "not_planned", + "comment": "This issue appears to be the result of the new workspace trust feature shipped in June 2021. This security-focused feature has major impact on the functionality of VS Code. Due to the volume of issues, we ask that you take some time to review our [comprehensive documentation](https://aka.ms/vscode-workspace-trust) on the feature. If your issue is still not resolved, please let us know." + }, + { + "type": "label", + "name": "~verification-steps-needed", + "action": "updateLabels", + "addLabel": "verification-steps-needed", + "removeLabel": "~verification-steps-needed", + "comment": "Friendly ping! Looks like this issue requires some further steps to be verified. Please provide us with the steps necessary to verify this issue." + }, + { + "type": "label", + "name": "~info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/pvsc-bug). Please take the time to review these and update the issue or even open a new one with the Report Issue command in VS Code (**Help > Report Issue**) to have all the right information collected for you.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~version-info-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~version-info-needed", + "comment": "Thanks for creating this issue! We figured it's missing some basic information, such as a version number, or in some other way doesn't follow our issue reporting guidelines. Please take the time to review these and update the issue or even open a new one with the Report Issue command in VS Code (**Help > Report Issue**) to have all the right information collected for you.\n\nHappy Coding!" + }, + { + "type": "label", + "name": "~confirmation-needed", + "action": "updateLabels", + "addLabel": "info-needed", + "removeLabel": "~confirmation-needed", + "comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!" + } +] diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000000..14c8e18d475d --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,49 @@ +version: 2 +updates: + - package-ecosystem: 'github-actions' + directory: / + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/build-vsix + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/lint + schedule: + interval: daily + labels: + - 'no-changelog' + + - package-ecosystem: 'github-actions' + directory: .github/actions/smoke-test + schedule: + interval: daily + labels: + - 'no-changelog' + + # Not skipping the news for some Python dependencies in case it's actually useful to communicate to users. + - package-ecosystem: 'pip' + directory: / + schedule: + interval: daily + ignore: + - dependency-name: prospector # Due to Python 2.7 and #14477. + - dependency-name: pytest # Due to Python 2.7 and #13776. + - dependency-name: py # Due to Python 2.7. + - dependency-name: jedi-language-server + labels: + - 'no-changelog' + # Activate when we feel ready to keep up with frequency. + # - package-ecosystem: 'npm' + # directory: / + # schedule: + # interval: daily + # default_labels: + # - "no-changelog" diff --git a/.github/instructions/learning.instructions.md b/.github/instructions/learning.instructions.md new file mode 100644 index 000000000000..28b085f486ce --- /dev/null +++ b/.github/instructions/learning.instructions.md @@ -0,0 +1,34 @@ +--- +applyTo: '**' +description: This document describes how to deal with learnings that you make. (meta instruction) +--- + +This document describes how to deal with learnings that you make. +It is a meta-instruction file. + +Structure of learnings: + +- Each instruction file has a "Learnings" section. +- Each learning has a counter that indicates how often that learning was useful (initially 1). +- Each learning has a 1 sentence description of the learning that is clear and concise. + +Example: + +```markdown +## Learnings + +- Prefer `const` over `let` whenever possible (1) +- Avoid `any` type (3) +``` + +When the user tells you "learn!", you should: + +- extract a learning from the recent conversation + _ identify the problem that you created + _ identify why it was a problem + _ identify how you were told to fix it/how the user fixed it + _ generate only one learning (1 sentence) that helps to summarize the insight gained +- then, add the reflected learning to the "Learnings" section of the most appropriate instruction file + +Important: Whenever a learning was really useful, increase the counter!! +When a learning was not useful and just caused more problems, decrease the counter. diff --git a/.github/instructions/pytest-json-test-builder.instructions.md b/.github/instructions/pytest-json-test-builder.instructions.md new file mode 100644 index 000000000000..436bce0c9cd8 --- /dev/null +++ b/.github/instructions/pytest-json-test-builder.instructions.md @@ -0,0 +1,126 @@ +--- +applyTo: 'python_files/tests/pytestadapter/test_discovery.py' +description: 'A guide for adding new tests for pytest discovery and JSON formatting in the test_pytest_collect suite.' +--- + +# How to Add New Pytest Discovery Tests + +This guide explains how to add new tests for pytest discovery and JSON formatting in the `test_pytest_collect` suite. Follow these steps to ensure your tests are consistent and correct. + +--- + +## 1. Add Your Test File + +- Place your new test file/files in the appropriate subfolder under: + ``` + python_files/tests/pytestadapter/.data/ + ``` +- Organize folders and files to match the structure you want to test. For example, to test nested folders, create the corresponding directory structure. +- In your test file, mark each test function with a comment: + ```python + def test_function(): # test_marker--test_function + ... + ``` + +**Root Node Matching:** + +- The root node in your expected output must match the folder or file you pass to pytest discovery. For example, if you run discovery on a subfolder, the root `"name"`, `"path"`, and `"id_"` in your expected output should be that subfolder, not the parent `.data` folder. +- Only use `.data` as the root if you are running discovery on the entire `.data` folder. + +**Example:** +If you run: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +{ + "name": "myfolder", + "path": os.fspath(TEST_DATA_PATH / "myfolder"), + "type_": "folder", + ... +} +``` + +--- + +## 2. Update `expected_discovery_test_output.py` + +- Open `expected_discovery_test_output.py` in the same test suite. +- Add a new expected output dictionary for your test file, following the format of existing entries. +- Use the helper functions and path conventions: + - Use `os.fspath()` for all paths. + - Use `find_test_line_number("function_name", file_path)` for the `lineno` field. + - Use `get_absolute_test_id("relative_path::function_name", file_path)` for `id_` and `runID`. + - Always use current path concatenation (e.g., `TEST_DATA_PATH / "your_folder" / "your_file.py"`). + - Create new constants as needed to keep the code clean and maintainable. + +**Important:** + +- Do **not** read the entire `expected_discovery_test_output.py` file if you only need to add or reference a single constant. This file is very large; prefer searching for the relevant section or appending to the end. + +**Example:** +If you run discovery on a subfolder: + +```python +helpers.runner([os.fspath(TEST_DATA_PATH / "myfolder"), "--collect-only"]) +``` + +then your expected output root should be: + +```python +myfolder_path = TEST_DATA_PATH / "myfolder" +my_expected_output = { + "name": "myfolder", + "path": os.fspath(myfolder_path), + "type_": "folder", + ... +} +``` + +- Add a comment above your dictionary describing the structure, as in the existing examples. + +--- + +## 3. Add Your Test to `test_discovery.py` + +- In `test_discovery.py`, add your new test as a parameterized case to the main `test_pytest_collect` function. Do **not** create a standalone test function for new discovery cases. +- Reference your new expected output constant from `expected_discovery_test_output.py`. + +**Example:** + +```python +@pytest.mark.parametrize( + ("file", "expected_const"), + [ + ("myfolder", my_expected_output), + # ... other cases ... + ], +) +def test_pytest_collect(file, expected_const): + ... +``` + +--- + +## 4. Run and Verify + +- Run the test suite to ensure your new test is discovered and passes. +- If the test fails, check your expected output dictionary for path or structure mismatches. + +--- + +## 5. Tips + +- Always use the helper functions for line numbers and IDs. +- Match the folder/file structure in `.data` to the expected JSON structure. +- Use comments to document the expected output structure for clarity. +- Ensure all `"path"` and `"id_"` fields in your expected output match exactly what pytest returns, including absolute paths and root node structure. + +--- + +**Reference:** +See `expected_discovery_test_output.py` for more examples and formatting. Use search or jump to the end of the file to avoid reading the entire file when possible. diff --git a/.github/instructions/python-quality-checks.instructions.md b/.github/instructions/python-quality-checks.instructions.md new file mode 100644 index 000000000000..48f37529dfbc --- /dev/null +++ b/.github/instructions/python-quality-checks.instructions.md @@ -0,0 +1,97 @@ +--- +applyTo: 'python_files/**' +description: Guide for running and fixing Python quality checks (Ruff and Pyright) that run in CI +--- + +# Python Quality Checks — Ruff and Pyright + +Run the same Python quality checks that run in CI. All checks target `python_files/` and use config from `python_files/pyproject.toml`. + +## Commands + +```bash +npm run check-python # Run both Ruff and Pyright +npm run check-python:ruff # Linting and formatting only +npm run check-python:pyright # Type checking only +``` + +## Fixing Ruff Errors + +**Auto-fix most issues:** + +```bash +cd python_files +python -m ruff check . --fix +python -m ruff format +npm run check-python:ruff # Verify +``` + +**Manual fixes:** + +- Ruff shows file, line number, rule code (e.g., `F841`), and description +- Open the file, read the error, fix the code +- Common: line length (100 char max), import sorting, unused variables + +## Fixing Pyright Errors + +**Common patterns and fixes:** + +- **Undefined variable/import**: Add the missing import +- **Type mismatch**: Correct the type or add type annotations +- **Missing return type**: Add `-> ReturnType` to function signatures + ```python + def my_function() -> str: # Add return type + return "result" + ``` + +**Verify:** + +```bash +npm run check-python:pyright +``` + +## Configuration + +- **Ruff**: Line length 100, Python 3.9+, 40+ rule families (flake8, isort, pyupgrade, etc.) +- **Pyright**: Version 1.1.308 (or whatever is found in the environment), ignores `lib/` and 15+ legacy files +- Config: `python_files/pyproject.toml` sections `[tool.ruff]` and `[tool.pyright]` + +## Troubleshooting + +**"Module not found" in Pyright**: Install dependencies + +```bash +python -m pip install --upgrade -r build/test-requirements.txt +nox --session install_python_libs +``` + +**Import order errors**: Auto-fix with `ruff check . --fix` + +**Type errors in ignored files**: Legacy files in `pyproject.toml` ignore list—fix if working on them + +## When Writing Tests + +**Always format your test files before committing:** + +```bash +cd python_files +ruff format tests/ # Format all test files +# or format specific files: +ruff format tests/unittestadapter/test_utils.py +``` + +**Best practice workflow:** + +1. Write your test code +2. Run `ruff format` on the test files +3. Run the tests to verify they pass +4. Run `npm run check-python` to catch any remaining issues + +This ensures your tests pass both functional checks and quality checks in CI. + +## Learnings + +- Always run `npm run check-python` before pushing to catch CI failures early (1) +- Use `ruff check . --fix` to auto-fix most linting issues before manual review (1) +- Pyright version must match CI (1.1.308) to avoid inconsistent results between local and CI runs (1) +- Always run `ruff format` on test files after writing them to avoid formatting CI failures (1) diff --git a/.github/instructions/testing-workflow.instructions.md b/.github/instructions/testing-workflow.instructions.md new file mode 100644 index 000000000000..844946404328 --- /dev/null +++ b/.github/instructions/testing-workflow.instructions.md @@ -0,0 +1,581 @@ +--- +applyTo: '**/test/**' +--- + +# AI Testing Workflow Guide: Write, Run, and Fix Tests + +This guide provides comprehensive instructions for AI agents on the complete testing workflow: writing tests, running them, diagnosing failures, and fixing issues. Use this guide whenever working with test files or when users request testing tasks. + +## Complete Testing Workflow + +This guide covers the full testing lifecycle: + +1. **📝 Writing Tests** - Create comprehensive test suites +2. **▶️ Running Tests** - Execute tests using VS Code tools +3. **🔍 Diagnosing Issues** - Analyze failures and errors +4. **🛠️ Fixing Problems** - Resolve compilation and runtime issues +5. **✅ Validation** - Ensure coverage and resilience + +### When to Use This Guide + +**User Requests Testing:** + +- "Write tests for this function" +- "Run the tests" +- "Fix the failing tests" +- "Test this code" +- "Add test coverage" + +**File Context Triggers:** + +- Working in `**/test/**` directories +- Files ending in `.test.ts` or `.unit.test.ts` +- Test failures or compilation errors +- Coverage reports or test output analysis + +## Test Types + +When implementing tests as an AI agent, choose between two main types: + +### Unit Tests (`*.unit.test.ts`) + +- **Fast isolated testing** - Mock all external dependencies +- **Use for**: Pure functions, business logic, data transformations +- **Execute with**: `runTests` tool with specific file patterns +- **Mock everything** - VS Code APIs automatically mocked via `/src/test/unittests.ts` + +### Extension Tests (`*.test.ts`) + +- **Full VS Code integration** - Real environment with actual APIs +- **Use for**: Command registration, UI interactions, extension lifecycle +- **Execute with**: VS Code launch configurations or `runTests` tool +- **Slower but comprehensive** - Tests complete user workflows + +## 🤖 Agent Tool Usage for Test Execution + +### Primary Tool: `runTests` + +Use the `runTests` tool to execute tests programmatically rather than terminal commands for better integration and result parsing: + +```typescript +// Run specific test files +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'run', +}); + +// Run tests with coverage +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + mode: 'coverage', + coverageFiles: ['/absolute/path/to/source.ts'], +}); + +// Run specific test names +await runTests({ + files: ['/absolute/path/to/test.unit.test.ts'], + testNames: ['should handle edge case', 'should validate input'], +}); +``` + +### Compilation Requirements + +Before running tests, ensure compilation. Always start compilation with `npm run watch-tests` before test execution to ensure TypeScript files are built. Recompile after making import/export changes before running tests, as stubs won't work if they're applied to old compiled JavaScript that doesn't have the updated imports: + +```typescript +// Start watch mode for auto-compilation +await run_in_terminal({ + command: 'npm run watch-tests', + isBackground: true, + explanation: 'Start test compilation in watch mode', +}); + +// Or compile manually +await run_in_terminal({ + command: 'npm run compile-tests', + isBackground: false, + explanation: 'Compile TypeScript test files', +}); +``` + +### Alternative: Terminal Execution + +For targeted test runs when `runTests` tool is unavailable. Note: When a targeted test run yields 0 tests, first verify the compiled JS exists under `out/test` (rootDir is `src`); absence almost always means the test file sits outside `src` or compilation hasn't run yet: + +```typescript +// Run specific test suite +await run_in_terminal({ + command: 'npm run unittest -- --grep "Suite Name"', + isBackground: false, + explanation: 'Run targeted unit tests', +}); +``` + +## 🔍 Diagnosing Test Failures + +### Common Failure Patterns + +**Compilation Errors:** + +```typescript +// Missing imports +if (error.includes('Cannot find module')) { + await addMissingImports(testFile); +} + +// Type mismatches +if (error.includes("Type '" && error.includes("' is not assignable"))) { + await fixTypeIssues(testFile); +} +``` + +**Runtime Errors:** + +```typescript +// Mock setup issues +if (error.includes('stub') || error.includes('mock')) { + await fixMockConfiguration(testFile); +} + +// Assertion failures +if (error.includes('AssertionError')) { + await analyzeAssertionFailure(error); +} +``` + +### Systematic Failure Analysis + +Fix test issues iteratively - run tests, analyze failures, apply fixes, repeat until passing. When unit tests fail with VS Code API errors like `TypeError: X is not a constructor` or `Cannot read properties of undefined (reading 'Y')`, check if VS Code APIs are properly mocked in `/src/test/unittests.ts` - add missing APIs following the existing pattern. + +```typescript +interface TestFailureAnalysis { + type: 'compilation' | 'runtime' | 'assertion' | 'timeout'; + message: string; + location: { file: string; line: number; col: number }; + suggestedFix: string; +} + +function analyzeFailure(failure: TestFailure): TestFailureAnalysis { + if (failure.message.includes('Cannot find module')) { + return { + type: 'compilation', + message: failure.message, + location: failure.location, + suggestedFix: 'Add missing import statement', + }; + } + // ... other failure patterns +} +``` + +### Agent Decision Logic for Test Type Selection + +**Choose Unit Tests (`*.unit.test.ts`) when analyzing:** + +- Functions with clear inputs/outputs and no VS Code API dependencies +- Data transformation, parsing, or utility functions +- Business logic that can be isolated with mocks +- Error handling scenarios with predictable inputs + +**Choose Extension Tests (`*.test.ts`) when analyzing:** + +- Functions that register VS Code commands or use `vscode.*` APIs +- UI components, tree views, or command palette interactions +- File system operations requiring workspace context +- Extension lifecycle events (activation, deactivation) + +**Agent Implementation Pattern:** + +```typescript +function determineTestType(functionCode: string): 'unit' | 'extension' { + if ( + functionCode.includes('vscode.') || + functionCode.includes('commands.register') || + functionCode.includes('window.') || + functionCode.includes('workspace.') + ) { + return 'extension'; + } + return 'unit'; +} +``` + +## 🎯 Step 1: Automated Function Analysis + +As an AI agent, analyze the target function systematically: + +### Code Analysis Checklist + +```typescript +interface FunctionAnalysis { + name: string; + inputs: string[]; // Parameter types and names + outputs: string; // Return type + dependencies: string[]; // External modules/APIs used + sideEffects: string[]; // Logging, file system, network calls + errorPaths: string[]; // Exception scenarios + testType: 'unit' | 'extension'; +} +``` + +### Analysis Implementation + +1. **Read function source** using `read_file` tool +2. **Identify imports** - look for `vscode.*`, `child_process`, `fs`, etc. +3. **Map data flow** - trace inputs through transformations to outputs +4. **Catalog dependencies** - external calls that need mocking +5. **Document side effects** - logging, file operations, state changes + +### Test Setup Differences + +#### Unit Test Setup (\*.unit.test.ts) + +```typescript +// Mock VS Code APIs - handled automatically by unittests.ts +import * as sinon from 'sinon'; +import * as workspaceApis from '../../common/workspace.apis'; // Wrapper functions + +// Stub wrapper functions, not VS Code APIs directly +// Always mock wrapper functions (e.g., workspaceApis.getConfiguration()) instead of +// VS Code APIs directly to avoid stubbing issues +const mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); +``` + +#### Extension Test Setup (\*.test.ts) + +```typescript +// Use real VS Code APIs +import * as vscode from 'vscode'; + +// Real VS Code APIs available - no mocking needed +const config = vscode.workspace.getConfiguration('python'); +``` + +## 🎯 Step 2: Generate Test Coverage Matrix + +Based on function analysis, automatically generate comprehensive test scenarios: + +### Coverage Matrix Generation + +```typescript +interface TestScenario { + category: 'happy-path' | 'edge-case' | 'error-handling' | 'side-effects'; + description: string; + inputs: Record; + expectedOutput?: any; + expectedSideEffects?: string[]; + shouldThrow?: boolean; +} +``` + +### Automated Scenario Creation + +1. **Happy Path**: Normal execution with typical inputs +2. **Edge Cases**: Boundary conditions, empty/null inputs, unusual but valid data +3. **Error Scenarios**: Invalid inputs, dependency failures, exception paths +4. **Side Effects**: Verify logging calls, file operations, state changes + +### Agent Pattern for Scenario Generation + +```typescript +function generateTestScenarios(analysis: FunctionAnalysis): TestScenario[] { + const scenarios: TestScenario[] = []; + + // Generate happy path for each input combination + scenarios.push(...generateHappyPathScenarios(analysis)); + + // Generate edge cases for boundary conditions + scenarios.push(...generateEdgeCaseScenarios(analysis)); + + // Generate error scenarios for each dependency + scenarios.push(...generateErrorScenarios(analysis)); + + return scenarios; +} +``` + +## 🗺️ Step 3: Plan Your Test Coverage + +### Create a Test Coverage Matrix + +#### Main Flows + +- ✅ **Happy path scenarios** - normal expected usage +- ✅ **Alternative paths** - different configuration combinations +- ✅ **Integration scenarios** - multiple features working together + +#### Edge Cases + +- 🔸 **Boundary conditions** - empty inputs, missing data +- 🔸 **Error scenarios** - network failures, permission errors +- 🔸 **Data validation** - invalid inputs, type mismatches + +#### Real-World Scenarios + +- ✅ **Fresh install** - clean slate +- ✅ **Existing user** - migration scenarios +- ✅ **Power user** - complex configurations +- 🔸 **Error recovery** - graceful degradation + +### Example Test Plan Structure + +```markdown +## Test Categories + +### 1. Configuration Migration Tests + +- No legacy settings exist +- Legacy settings already migrated +- Fresh migration needed +- Partial migration required +- Migration failures + +### 2. Configuration Source Tests + +- Global search paths +- Workspace search paths +- Settings precedence +- Configuration errors + +### 3. Path Resolution Tests + +- Absolute vs relative paths +- Workspace folder resolution +- Path validation and filtering + +### 4. Integration Scenarios + +- Combined configurations +- Deduplication logic +- Error handling flows +``` + +## 🔧 Step 4: Set Up Your Test Infrastructure + +### Test File Structure + +```typescript +// 1. Imports - group logically +import assert from 'node:assert'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as logging from '../../../common/logging'; +import * as pathUtils from '../../../common/utils/pathUtils'; +import * as workspaceApis from '../../../common/workspace.apis'; + +// 2. Function under test +import { getAllExtraSearchPaths } from '../../../managers/common/nativePythonFinder'; + +// 3. Mock interfaces +interface MockWorkspaceConfig { + get: sinon.SinonStub; + inspect: sinon.SinonStub; + update: sinon.SinonStub; +} +``` + +### Mock Setup Strategy + +Create minimal mock objects with only required methods and use TypeScript type assertions (e.g., `mockApi as PythonEnvironmentApi`) to satisfy interface requirements instead of implementing all interface methods when only specific methods are needed for the test. Simplify mock setup by only mocking methods actually used in tests and use `as unknown as Type` for TypeScript compatibility. + +```typescript +suite('Function Integration Tests', () => { + // 1. Declare all mocks + let mockGetConfiguration: sinon.SinonStub; + let mockGetWorkspaceFolders: sinon.SinonStub; + let mockTraceLog: sinon.SinonStub; + let mockTraceError: sinon.SinonStub; + let mockTraceWarn: sinon.SinonStub; + + // 2. Mock complex objects + let pythonConfig: MockWorkspaceConfig; + let envConfig: MockWorkspaceConfig; + + setup(() => { + // 3. Initialize all mocks + mockGetConfiguration = sinon.stub(workspaceApis, 'getConfiguration'); + mockGetWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + mockTraceLog = sinon.stub(logging, 'traceLog'); + mockTraceError = sinon.stub(logging, 'traceError'); + mockTraceWarn = sinon.stub(logging, 'traceWarn'); + + // 4. Set up default behaviors + mockGetWorkspaceFolders.returns(undefined); + + // 5. Create mock configuration objects + // When fixing mock environment creation, use null to truly omit + // properties rather than undefined + pythonConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + + envConfig = { + get: sinon.stub(), + inspect: sinon.stub(), + update: sinon.stub(), + }; + }); + + teardown(() => { + sinon.restore(); // Always clean up! + }); +}); +``` + +## Step 4: Write Tests Using Mock → Run → Assert Pattern + +### The Three-Phase Pattern + +#### Phase 1: Mock (Set up the scenario) + +```typescript +test('Description of what this tests', async () => { + // Mock → Clear description of the scenario + pythonConfig.inspect.withArgs('venvPath').returns({ globalValue: '/path' }); + envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); + mockGetWorkspaceFolders.returns([{ uri: Uri.file('/workspace') }]); +``` + +#### Phase 2: Run (Execute the function) + +```typescript +// Run +const result = await getAllExtraSearchPaths(); +``` + +#### Phase 3: Assert (Verify the behavior) + +```typescript + // Assert - Use set-based comparison for order-agnostic testing + const expected = new Set(['/expected', '/paths']); + const actual = new Set(result); + assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); + assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); + + // Verify side effects + // Use sinon.match() patterns for resilient assertions that don't break on minor output changes + assert(mockTraceLog.calledWith(sinon.match(/completion/i)), 'Should log completion'); +}); +``` + +## Step 6: Make Tests Resilient + +### Use Order-Agnostic Comparisons + +```typescript +// ❌ Brittle - depends on order +assert.deepStrictEqual(result, ['/path1', '/path2', '/path3']); + +// ✅ Resilient - order doesn't matter +const expected = new Set(['/path1', '/path2', '/path3']); +const actual = new Set(result); +assert.strictEqual(actual.size, expected.size, 'Should have correct number of paths'); +assert.deepStrictEqual(actual, expected, 'Should contain exactly the expected paths'); +``` + +### Use Flexible Error Message Testing + +```typescript +// ❌ Brittle - exact text matching +assert(mockTraceError.calledWith('Error during legacy python settings migration:')); + +// ✅ Resilient - pattern matching +assert(mockTraceError.calledWith(sinon.match.string, sinon.match.instanceOf(Error)), 'Should log migration error'); + +// ✅ Resilient - key terms with regex +assert(mockTraceError.calledWith(sinon.match(/migration.*error/i)), 'Should log migration error'); +``` + +### Handle Complex Mock Scenarios + +```typescript +// For functions that call the same mock multiple times +envConfig.inspect.withArgs('globalSearchPaths').returns({ globalValue: [] }); +envConfig.inspect + .withArgs('globalSearchPaths') + .onSecondCall() + .returns({ + globalValue: ['/migrated/paths'], + }); + +// Testing async functions with child processes: +// Call the function first to get a promise, then use setTimeout to emit mock events, +// then await the promise - this ensures proper timing of mock setup versus function execution + +// Cannot stub internal function calls within the same module after import - stub external +// dependencies instead (e.g., stub childProcessApis.spawnProcess rather than trying to stub +// helpers.isUvInstalled when testing helpers.shouldUseUv) because intra-module calls use +// direct references, not module exports +``` + +## 🧪 Step 7: Test Categories and Patterns + +### Configuration Tests + +- Test different setting combinations +- Test setting precedence (workspace > user > default) +- Test configuration errors and recovery +- Always use dynamic path construction with Node.js `path` module when testing functions that resolve paths against workspace folders to ensure cross-platform compatibility + +### Data Flow Tests + +- Test how data moves through the system +- Test transformations (path resolution, filtering) +- Test state changes (migrations, updates) + +### Error Handling Tests + +- Test graceful degradation +- Test error logging +- Test fallback behaviors + +### Integration Tests + +- Test multiple features together +- Test real-world scenarios +- Test edge case combinations + +## 📊 Step 8: Review and Refine + +### Test Quality Checklist + +- [ ] **Clear naming** - test names describe the scenario and expected outcome +- [ ] **Good coverage** - main flows, edge cases, error scenarios +- [ ] **Resilient assertions** - won't break due to minor changes +- [ ] **Readable structure** - follows Mock → Run → Assert pattern +- [ ] **Isolated tests** - each test is independent +- [ ] **Fast execution** - tests run quickly with proper mocking + +### Common Anti-Patterns to Avoid + +- ❌ Testing implementation details instead of behavior +- ❌ Brittle assertions that break on cosmetic changes +- ❌ Order-dependent tests that fail due to processing changes +- ❌ Tests that don't clean up mocks properly +- ❌ Overly complex test setup that's hard to understand + +## 🔄 Reviewing and Improving Existing Tests + +### Quick Review Process + +1. **Read test files** - Check structure and mock setup +2. **Run tests** - Establish baseline functionality +3. **Apply improvements** - Use patterns below. When reviewing existing tests, focus on behavior rather than implementation details in test names and assertions +4. **Verify** - Ensure tests still pass + +### Common Fixes + +- Over-complex mocks → Minimal mocks with only needed methods +- Brittle assertions → Behavior-focused with error messages +- Vague test names → Clear scenario descriptions (transform "should return X when Y" into "should [expected behavior] when [scenario context]") +- Missing structure → Mock → Run → Assert pattern +- Untestable Node.js APIs → Create proxy abstraction functions (use function overloads to preserve intelligent typing while making functions mockable) + +## 🧠 Agent Learnings + +- When mocking `testController.createTestItem()` in unit tests, use `typemoq.It.isAny()` for parameters when testing handler behavior (not ID/label generation logic), but consider using specific matchers (e.g., `It.is((id: string) => id.startsWith('_error_'))`) when the actual values being passed are important for correctness - this balances test precision with maintainability (2) +- Remove unused variables from test code immediately - leftover tracking variables like `validationCallCount` that aren't referenced indicate dead code that should be simplified (1) +- Use `Uri.file(path).fsPath` for both sides of path comparisons in tests to ensure cross-platform compatibility - Windows converts forward slashes to backslashes automatically (1) +- When tests fail with "Cannot stub non-existent property", the method likely moved to a different class during refactoring - find the class that owns the method and test that class directly instead of stubbing on the original class (1) diff --git a/.github/instructions/testing_feature_area.instructions.md b/.github/instructions/testing_feature_area.instructions.md new file mode 100644 index 000000000000..a4e11523d7c8 --- /dev/null +++ b/.github/instructions/testing_feature_area.instructions.md @@ -0,0 +1,263 @@ +--- +applyTo: 'src/client/testing/**' +--- + +# Testing feature area — Discovery, Run, Debug, and Results + +This document maps the testing support in the extension: discovery, execution (run), debugging, result reporting and how those pieces connect to the codebase. It's written for contributors and agents who need to navigate, modify, or extend test support (both `unittest` and `pytest`). + +## Overview + +- Purpose: expose Python tests in the VS Code Test Explorer (TestController), support discovery, run, debug, and surface rich results and outputs. +- Scope: provider-agnostic orchestration + provider-specific adapters, TestController mapping, IPC with Python-side scripts, debug launch integration, and configuration management. + +## High-level architecture + +- Controller / UI bridge: orchestrates TestController requests and routes them to workspace adapters. +- Workspace adapter: provider-agnostic coordinator that translates TestController requests to provider adapters and maps payloads back into TestItems/TestRuns. +- Provider adapters: implement discovery/run/debug for `unittest` and `pytest` by launching Python scripts and wiring named-pipe IPC. +- Result resolver: translates Python-side JSON/IPCPayloads into TestController updates (start/pass/fail/output/attachments). +- Debug launcher: prepares debug sessions and coordinates the debugger attach flow with the Python runner. + +## Key components (files and responsibilities) + +- Entrypoints + - `src/client/testing/testController/controller.ts` — `PythonTestController` (main orchestrator). + - `src/client/testing/serviceRegistry.ts` — DI/wiring for testing services. +- Workspace orchestration + - `src/client/testing/testController/workspaceTestAdapter.ts` — `WorkspaceTestAdapter` (provider-agnostic entry used by controller). +- **Project-based testing (multi-project workspaces)** + - `src/client/testing/testController/common/testProjectRegistry.ts` — `TestProjectRegistry` (manages project lifecycle, discovery, and nested project handling). + - `src/client/testing/testController/common/projectAdapter.ts` — `ProjectAdapter` interface (represents a single Python project with its own test infrastructure). + - `src/client/testing/testController/common/projectUtils.ts` — utilities for project ID generation, display names, and shared adapter creation. +- Provider adapters + - Unittest + - `src/client/testing/testController/unittest/testDiscoveryAdapter.ts` + - `src/client/testing/testController/unittest/testExecutionAdapter.ts` + - Pytest + - `src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts` + - `src/client/testing/testController/pytest/pytestExecutionAdapter.ts` +- Result resolution and helpers + - `src/client/testing/testController/common/resultResolver.ts` — `PythonResultResolver` (maps payload -> TestController updates). + - `src/client/testing/testController/common/testItemUtilities.ts` — helpers for TestItem lifecycle. + - `src/client/testing/testController/common/types.ts` — `ITestDiscoveryAdapter`, `ITestExecutionAdapter`, `ITestResultResolver`, `ITestDebugLauncher`. + - `src/client/testing/testController/common/debugLauncher.ts` — debug session creation helper. + - `src/client/testing/testController/common/utils.ts` — named-pipe helpers and command builders (`startDiscoveryNamedPipe`, etc.). +- Configuration + - `src/client/testing/common/testConfigurationManager.ts` — per-workspace test settings. + - `src/client/testing/configurationFactory.ts` — configuration service factory. +- Utilities & glue + - `src/client/testing/utils.ts` — assorted helpers used by adapters. + - Python-side scripts: `python_files/unittestadapter/*`, `python_files/pytestadapter/*` — discovery/run code executed by adapters. + +## Python subprocess runners (what runs inside Python) + +The adapters in the extension don't implement test discovery/run logic themselves — they spawn a Python subprocess that runs small helper scripts located under `python_files/` and stream structured events back to the extension over the named-pipe IPC. This is a central part of the feature area; changes here usually require coordinated edits in both the TypeScript adapters and the Python scripts. + +- Unittest helpers (folder: `python_files/unittestadapter`) + + - `discovery.py` — performs `unittest` discovery and emits discovery payloads (test suites, cases, locations) on the IPC channel. + - `execution.py` / `django_test_runner.py` — run tests for `unittest` and, where applicable, Django test runners; emit run events (start, stdout/stderr, pass, fail, skip, teardown) and attachment info. + - `pvsc_utils.py`, `django_handler.py` — utility helpers used by the runners for environment handling and Django-specific wiring. + - The adapter TypeScript files (`testDiscoveryAdapter.ts`, `testExecutionAdapter.ts`) construct the command line, start a named-pipe listener, and spawn these Python scripts using the extension's ExecutionFactory (activated interpreter) so the scripts execute inside the user's selected environment. + +- Pytest helpers (folder: `python_files/vscode_pytest`) + + - `_common.py` — shared helpers for pytest runner scripts. + - `run_pytest_script.py` — the primary pytest runner used for discovery and execution; emits the same structured IPC payloads the extension expects (discovery events and run events). + - The `pytest` execution adapter (`pytestExecutionAdapter.ts`) and discovery adapter build the CLI to run `run_pytest_script.py`, start the pipe, and translate incoming payloads via `PythonResultResolver`. + +- IPC contract and expectations + + - Adapters rely on a stable JSON payload contract emitted by the Python scripts: identifiers for tests, event types (discovered, collected, started, passed, failed, skipped), timings, error traces, and optional attachments (logs, captured stdout/stderr, file links). + - The extension maps these payloads to `TestItem`/`TestRun` updates via `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`). If you change payload shape, update the resolver and tests concurrently. + +- How the subprocess is started + - Execution adapters use the extension's `ExecutionFactory` (preferred) to get an activated interpreter and then spawn a child process that runs the helper script. The adapter will set up environment variables and command-line args (including the pipe name / run-id) so the Python runner knows where to send events and how to behave (discovery vs run vs debug). + - For debug sessions a debug-specific entry argument/port is passed and `common/debugLauncher.ts` coordinates starting a VS Code debug session that will attach to the Python process. + +## Core functionality (what to change where) + +- Discovery + - Entry: `WorkspaceTestAdapter.discoverTests` → provider discovery adapter. Adapter starts a named-pipe listener, spawns the discovery script in an activated interpreter, forwards discovery events to `PythonResultResolver` which creates/updates TestItems. + - Files: `workspaceTestAdapter.ts`, `*DiscoveryAdapter.ts`, `resultResolver.ts`, `testItemUtilities.ts`. +- Run / Execution + - Entry: `WorkspaceTestAdapter.executeTests` → provider execution adapter. Adapter spawns runner in an activated env, runner streams run events to the pipe, `PythonResultResolver` updates a `TestRun` with start/pass/fail and attachments. + - Files: `workspaceTestAdapter.ts`, `*ExecutionAdapter.ts`, `resultResolver.ts`. +- Debugging + - Flow: debug request flows like a run but goes through `debugLauncher.ts` to create a VS Code debug session with prepared ports/pipes. The Python runner coordinates attach/continue with the debugger. + - Files: `*ExecutionAdapter.ts`, `common/debugLauncher.ts`, `common/types.ts`. +- Result reporting + - `resultResolver.ts` is the canonical place to change how JSON payloads map to TestController constructs (messages, durations, error traces, attachments). + +## Typical workflows (short) + +- Full discovery + + 1. `PythonTestController` triggers discovery -> `WorkspaceTestAdapter.discoverTests`. + 2. Provider discovery adapter starts pipe and launches Python discovery script. + 3. Discovery events -> `PythonResultResolver` -> TestController tree updated. + +- Run tests + + 1. Controller collects TestItems -> creates `TestRun`. + 2. `WorkspaceTestAdapter.executeTests` delegates to execution adapter which launches the runner. + 3. Runner events arrive via pipe -> `PythonResultResolver` updates `TestRun`. + 4. On process exit the run is finalized. + +- Debug a test + 1. Debug request flows to execution adapter. + 2. Adapter prepares ports and calls `debugLauncher` to start a VS Code debug session with the run ID. + 3. Runner coordinates with the debugger; `PythonResultResolver` still receives and applies run events. + +## Tests and examples to inspect + +- Unit/integration tests for adapters and orchestration under `src/test/` (examples): + - `src/test/testing/common/testingAdapter.test.ts` + - `src/test/testing/testController/workspaceTestAdapter.unit.test.ts` + - `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` + - Adapter tests demonstrate expected telemetry, debug-launch payloads and result resolution. + +## History & evolution (brief) + +- Migration to TestController API: the code organizes around VS Code TestController, mapping legacy adapter behaviour into TestItems/TestRuns. +- Named-pipe IPC: discovery/run use named-pipe IPC to stream events from Python runner scripts (`python_files/*`) which enables richer, incremental updates and debug coordination. +- Environment activation: adapters prefer the extension ExecutionFactory (activated interpreter) to run discovery and test scripts. + +## Pointers for contributors (practical) + +- To extend discovery output: update the Python discovery script in `python_files/*` and `resultResolver.ts` to parse new payload fields. +- To change run behaviour (args/env/timouts): update the provider execution adapter (`*ExecutionAdapter.ts`) and add/update tests under `src/test/`. +- To change debug flow: edit `common/debugLauncher.ts` and adapters' debug paths; update tests that assert launch argument shapes. + +## Django support (how it works) + +- The extension supports Django projects by delegating discovery and execution to Django-aware Python helpers under `python_files/unittestadapter`. + - `python_files/unittestadapter/django_handler.py` contains helpers that invoke `manage.py` for discovery or execute Django test runners inside the project context. + - `python_files/unittestadapter/django_test_runner.py` provides `CustomDiscoveryTestRunner` and `CustomExecutionTestRunner` which integrate with the extension by using the same IPC contract (they use `UnittestTestResult` and `send_post_request` to emit discovery/run payloads). +- How adapters pass Django configuration: + - Execution adapters set environment variables (e.g. `MANAGE_PY_PATH`) and modify `PYTHONPATH` so Django code and the custom test runner are importable inside the spawned subprocess. + - For discovery the adapter may run the discovery helper which calls `manage.py test` with a custom test runner that emits discovery payloads instead of executing tests. +- Practical notes for contributors: + - Changes to Django discovery/execution often require edits in both `django_test_runner.py`/`django_handler.py` and the TypeScript adapters (`testDiscoveryAdapter.ts` / `testExecutionAdapter.ts`). + - The Django test runner expects `TEST_RUN_PIPE` environment variable to be present to send IPC events (see `django_test_runner.py`). + +## Settings referenced by this feature area + +- The extension exposes several `python.testing.*` settings used by adapters and configuration code (declared in `package.json`): + - `python.testing.pytestEnabled`, `python.testing.unittestEnabled` — enable/disable frameworks. + - `python.testing.pytestPath`, `python.testing.pytestArgs`, `python.testing.unittestArgs` — command path and CLI arguments used when spawning helper scripts. + - `python.testing.cwd` — optional working directory used when running discovery/runs. + - `python.testing.autoTestDiscoverOnSaveEnabled`, `python.testing.autoTestDiscoverOnSavePattern` — control automatic discovery on save. + - `python.testing.debugPort` — default port used for debug runs. + - `python.testing.promptToConfigure` — whether to prompt users to configure tests when potential test folders are found. +- Where to look in the code: + - Settings are consumed by `src/client/testing/common/testConfigurationManager.ts`, `src/client/testing/configurationFactory.ts`, and adapters under `src/client/testing/testController/*` which read settings to build CLI args and env for subprocesses. + - The setting definitions and descriptions are in `package.json` and localized strings in `package.nls.json`. + +## Project-based testing (multi-project workspaces) + +Project-based testing enables multi-project workspace support where each Python project gets its own test tree root with its own Python environment. + +### Architecture + +- **TestProjectRegistry** (`testProjectRegistry.ts`): Central registry that: + + - Discovers Python projects via the Python Environments API + - Creates and manages `ProjectAdapter` instances per workspace + - Computes nested project relationships and configures ignore lists + - Falls back to "legacy" single-adapter mode when API unavailable + +- **ProjectAdapter** (`projectAdapter.ts`): Interface representing a single project with: + - Project identity (ID, name, URI from Python Environments API) + - Python environment with execution details + - Test framework adapters (discovery/execution) + - Nested project ignore paths (for parent projects) + +### How it works + +1. **Activation**: When the extension activates, `PythonTestController` checks if the Python Environments API is available. +2. **Project discovery**: `TestProjectRegistry.discoverAndRegisterProjects()` queries the API for all Python projects in each workspace. +3. **Nested handling**: `configureNestedProjectIgnores()` identifies child projects and adds their paths to parent projects' ignore lists. +4. **Test discovery**: For each project, the controller calls `project.discoveryAdapter.discoverTests()` with the project's URI. The adapter sets `PROJECT_ROOT_PATH` environment variable for the Python runner. +5. **Python side**: + - For pytest: `get_test_root_path()` in `vscode_pytest/__init__.py` returns `PROJECT_ROOT_PATH` (if set) or falls back to `cwd`. + - For unittest: `discovery.py` uses `PROJECT_ROOT_PATH` as `top_level_dir` and `project_root_path` to root the test tree at the project directory. +6. **Test tree**: Each project gets its own root node in the Test Explorer, with test IDs scoped by project ID using the `@@vsc@@` separator (defined in `projectUtils.ts`). + +### Nested project handling: pytest vs unittest + +**pytest** supports the `--ignore` flag to exclude paths during test collection. When nested projects are detected, parent projects automatically receive `--ignore` flags for child project paths. This ensures each test appears under exactly one project in the test tree. + +**unittest** does not support path exclusion during `discover()`. Therefore, tests in nested project directories may appear under multiple project roots (both the parent and the child project). This is **expected behavior** for unittest: + +- Each project discovers and displays all tests it finds within its directory structure +- There is no deduplication or collision detection +- Users may see the same test file under multiple project roots if their project structure has nesting + +This approach was chosen because: + +1. unittest's `TestLoader.discover()` has no built-in path exclusion mechanism +2. Implementing custom exclusion would add significant complexity with minimal benefit +3. The existing approach is transparent and predictable - each project shows what it finds + +### Empty projects and root nodes + +If a project discovers zero tests, its root node will still appear in the Test Explorer as an empty folder. This ensures consistent behavior and makes it clear which projects were discovered, even if they have no tests yet. + +### Logging prefix + +All project-based testing logs use the `[test-by-project]` prefix for easy filtering in the output channel. + +### Key files + +- Python side: + - `python_files/vscode_pytest/__init__.py` — `get_test_root_path()` function and `PROJECT_ROOT_PATH` environment variable for pytest. + - `python_files/unittestadapter/discovery.py` — `discover_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest discovery. + - `python_files/unittestadapter/execution.py` — `run_tests()` with `project_root_path` parameter and `PROJECT_ROOT_PATH` handling for unittest execution. +- TypeScript: `testProjectRegistry.ts`, `projectAdapter.ts`, `projectUtils.ts`, and the discovery/execution adapters. + +### Tests + +- `src/test/testing/testController/common/testProjectRegistry.unit.test.ts` — TestProjectRegistry tests +- `src/test/testing/testController/common/projectUtils.unit.test.ts` — Project utility function tests +- `python_files/tests/pytestadapter/test_discovery.py` — pytest PROJECT_ROOT_PATH tests (see `test_project_root_path_env_var()` and `test_symlink_with_project_root_path()`) +- `python_files/tests/unittestadapter/test_discovery.py` — unittest `project_root_path` / PROJECT_ROOT_PATH discovery tests +- `python_files/tests/unittestadapter/test_execution.py` — unittest `project_root_path` / PROJECT_ROOT_PATH execution tests +- `src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts` — unittest discovery adapter PROJECT_ROOT_PATH tests +- `src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts` — unittest execution adapter PROJECT_ROOT_PATH tests + +## Coverage support (how it works) + +- Coverage is supported by running the Python helper scripts with coverage enabled and then collecting a coverage payload from the runner. + - Pytest-side coverage logic lives in `python_files/vscode_pytest/__init__.py` (checks `COVERAGE_ENABLED`, imports `coverage`, computes per-file metrics and emits a `CoveragePayloadDict`). + - Unittest adapters enable coverage by setting environment variable(s) (e.g. `COVERAGE_ENABLED`) when launching the subprocess; adapters and `resultResolver.ts` handle the coverage profile kind (`TestRunProfileKind.Coverage`). +- Flow summary: + 1. User starts a Coverage run via Test Explorer (profile kind `Coverage`). + 2. Controller/adapters set `COVERAGE_ENABLED` (or equivalent) in the subprocess env and invoke the runner script. + 3. The Python runner collects coverage (using `coverage` or `pytest-cov`), builds a file-level coverage map, and sends a coverage payload back over the IPC. + 4. `PythonResultResolver` (`src/client/testing/testController/common/resultResolver.ts`) receives the coverage payload and stores `detailedCoverageMap` used by the TestController profile to show file-level coverage details. +- Tests that exercise coverage flows are under `src/test/testing/*` and `python_files/tests/*` (see `testingAdapter.test.ts` and adapter unit tests that assert `COVERAGE_ENABLED` is set appropriately). + +## Interaction with the VS Code API + +- TestController API + - The feature area is built on VS Code's TestController/TestItem/TestRun APIs (`vscode.tests.createTestController` / `tests.createTestController` in the code). The controller creates a `TestController` in `src/client/testing/testController/controller.ts` and synchronizes `TestItem` trees with discovery payloads. + - `PythonResultResolver` maps incoming JSON events to VS Code API calls: `testRun.appendOutput`, `testRun.passed/failed/skipped`, `testRun.end`, and `TestItem` updates (labels, locations, children). +- Debug API + - Debug runs use the Debug API to start an attach/launch session. The debug launcher implementation is in `src/client/testing/testController/common/debugLauncher.ts` which constructs a debug configuration and calls the VS Code debug API to start a session (e.g. `vscode.debug.startDebugging`). + - Debug adapter/resolver code in the extension's debugger modules may also be used when attaching to Django or test subprocesses. +- Commands and configuration + - The Test Controller wires commands that appear in the Test Explorer and editor context menus (see `package.json` contributes `commands`) and listens to configuration changes filtered by `python.testing` in `src/client/testing/main.ts`. +- The "Copy Test ID" command (`python.copyTestId`) can be accessed from both the Test Explorer context menu (`testing/item/context`) and the editor gutter icon context menu (`testing/item/gutter`). This command copies test identifiers to the clipboard in the appropriate format for the active test framework (pytest path format or unittest module.class.method format). +- Execution factory & activated environments + - Adapters use the extension `ExecutionFactory` to spawn subprocesses in an activated interpreter (so the user's venv/conda is used). This involves the extension's internal environment execution APIs and sometimes `envExt` helpers when the external environment extension is present. + +## Learnings + +- Never await `showErrorMessage()` calls in test execution adapters as it blocks the test UI thread and freezes the Test Explorer (1) +- VS Code test-related context menus are contributed to using both `testing/item/context` and `testing/item/gutter` menu locations in package.json for full coverage (1) + +``` + +``` diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 74cba62d1d23..000000000000 --- a/.github/lock.yml +++ /dev/null @@ -1,3 +0,0 @@ -daysUntilLock: 7 -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 a4331c1baf78..091ed559825b 100644 --- a/.github/release_plan.md +++ b/.github/release_plan.md @@ -1,97 +1,138 @@ -# Prerequisites - -* Python 3.7 and higher -* run `python3 -m pip install --user -r news/requirements.txt` -* run `python3 -m pip install --user -r tpn/requirements.txt` - - -# Release candidate (Tuesday, XXX XX) - -- [ ] Ensure all new features are tracked via telemetry -- [ ] Announce the code freeze (not just to team but also to ptvsd and language server) -- [ ] Update master for the release - - [ ] Create a branch against `master` for a pull request - - [ ] Change the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) from a `-dev` suffix to `-rc` - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) is up-to-date - - [ ] Update `requirements.txt` to point to latest release version of [ptvsd](https://github.com/microsoft/ptvsd). - - [ ] Update `languageServerVersion` in `package.json` to point to the latest version (???) of [the Language Server](https://github.com/Microsoft/python-language-server). - - [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) - - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/master/news) (typically `python news --final --update CHANGELOG.md | code-insiders -`) - - [ ] Copy over the "Thanks" section from the previous release - - [ ] Make sure the "Thanks" section is up-to-date (e.g. compare to versions in requirements.json) - - [ ] Touch up news entries (e.g. add missing periods) - - [ ] Add any relevant news entries for ptvsd and the language server if they were updated - - [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Distribution.txt) by running [`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`) - * for each failure: - 1. go to the repo (from link on NPM page) and look for the license there - 1. copy the text from the failure into `tpn/distribution.toml` - 1. fill in the license found in the package's repo - * if there is no license in a package's repo then do one of the following: - + check the NPM metadata and fill in the corresponding license from the OSI site - + ask the package maintainer (e.g. via github) - + ask CELA - - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Repository.txt) as appropriate - - [ ] Create a pull request against `master` - - [ ] Merge pull request into `master` -- [ ] Update the [`release` branch](https://github.com/microsoft/vscode-python/branches) - - [ ] (if necessary) Request from a repo admin that the branch be un-"protected" - - [ ] Delete the `release` branch in the repo - - [ ] Create a new `release` branch from `master` - - (alternately, force-push the master branch to the GitHub "release" branch) - - [ ] (if necessary) Request that the branch be set anew as "protected" -- [ ] Update master post-release - - [ ] Bump the version number to the next monthly ("YYYY.M.0-dev") release in the `master` branch - - [ ] `package.json` - - [ ] `package-lock.json` - - [ ] Create a pull request against `master` - - [ ] Merge pull request into `master` -- [ ] Announce the code freeze is over -- [ ] Update [Component Governance](https://dev.azure.com/ms/vscode-python/_componentGovernance) (Click on "microsoft/vscode-python" on that page) - - [ ] Provide details for any automatically detected npm dependencies - - [ ] Manually add any repository dependencies -- [ ] GDPR bookkeeping (@brettcannon) -- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython) - + new features - + settings changes - + etc. (ask the team) -- [ ] Begin drafting a [blog](http://aka.ms/pythonblog) post -- [ ] Ask CTI to test the release candidate - - -# 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 -- [ ] final updates to the `release` branch - - [ ] Create a branch against `release` for a pull request - - [ ] Update the version in [`package.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) - - [ ] Run `npm install` to make sure [`package-lock.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) is up-to-date (the only update should be the version number if `package-lock.json` has been kept up-to-date) - - [ ] Update [`CHANGELOG.md`](https://github.com/Microsoft/vscode-python/blob/master/CHANGELOG.md) - - [ ] Update version and date for the release section - - [ ] Run [`news`](https://github.com/Microsoft/vscode-python/tree/master/news) and copy-and-paste new entries (typically `python news --final | code-insiders -`; quite possibly nothing new to add) - - [ ] Update [`ThirdPartyNotices-Distribution.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Distribution.txt) by running [`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`; quite possible there will be no change) - - [ ] Update [`ThirdPartyNotices-Repository.txt`](https://github.com/Microsoft/vscode-python/blob/master/ThirdPartyNotices-Repository.txt) manually if necessary - - [ ] Create pull request against `release` - - [ ] Merge pull request into `release` -- [ ] Make sure component governance is happy - -## Release - -- [ ] Publish the release via Azure DevOps - - [ ] Make sure [CI](https://github.com/Microsoft/vscode-python/blob/master/CONTRIBUTING.md) is passing - - [ ] Make sure the "Upload" stage on the release page succeeded - - [ ] Make sure no extraneous files are being included in the `.vsix` file (make sure to check for hidden files) - - [ ] Deploy the "Publish" stage -- [ ] Publish [documentation changes](https://github.com/Microsoft/vscode-docs/pulls?q=is%3Apr+is%3Aopen+label%3Apython) -- [ ] Publish the [blog](http://aka.ms/pythonblog) post -- [ ] Determine if a hotfix is needed -- [ ] Merge `release` back into `master` - -## Clean up after _this_ release -- [ ] Go through [`info needed` issues](https://github.com/Microsoft/vscode-python/issues?utf8=%E2%9C%93&q=is%3Aopen+label%3A%22info+needed%22+sort%3Acreated-asc+-label%3A%22data+science%22) and close any that have no activity for over a month -- [ ] GDPR bookkeeping +### General Notes +All dates should align with VS Code's [iteration](https://github.com/microsoft/vscode/labels/iteration-plan) and [endgame](https://github.com/microsoft/vscode/labels/endgame-plan) plans. + +Feature freeze is Monday @ 17:00 America/Vancouver, XXX XX. At that point, commits to `main` should only be in response to bugs found during endgame testing until the release candidate is ready. + +
+ Release Primary and Secondary Assignments for the 2025 Calendar Year + +| Month and version number | Primary | Secondary | +|------------|----------|-----------| +| January v2025.0.0 | Eleanor | Karthik | +| February v2025.2.0 | Anthony | Eleanor | +| March v2025.4.0 | Karthik | Anthony | +| April v2025.6.0 | Eleanor | Karthik | +| May v2025.8.0 | Anthony | Eleanor | +| June v2025.10.0 | Karthik | Anthony | +| July v2025.12.0 | Eleanor | Karthik | +| August v2025.14.0 | Anthony | Eleanor | +| September v2025.16.0 | Karthik | Anthony | +| October v2025.18.0 | Eleanor | Karthik | +| November v2025.20.0 | Anthony | Eleanor | +| December v2025.22.0 | Karthik | Anthony | + +
+ + +# Release candidate (Thursday, XXX XX) +NOTE: This Thursday occurs during TESTING week. Branching should be done during this week to freeze the release with only the correct changes. Any last minute fixes go in as candidates into the release branch and will require team approval. + +Other: +NOTE: Third Party Notices are automatically added by our build pipelines using https://tools.opensource.microsoft.com/notice. +NOTE: the number of this release is in the issue title and can be substituted in wherever you see [YYYY.minor]. + + +### Step 1: +##### Bump the version of `main` to be a release candidate (also updating third party notices, and package-lock.json).❄️ (steps with ❄️ will dictate this step happens while main is frozen 🥶) + +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] Create a new branch called **`bump-release-[YYYY.minor]`**. +- [ ] Update `pet`: + - [ ] Go to the [pet](https://github.com/microsoft/python-environment-tools) repo and check `main` and latest `release/*` branch. If there are new changes in `main` then create a branch called `release/YYYY.minor` (matching python extension release `major.minor`). + - [ ] Update `build\azure-pipeline.stable.yml` to point to the latest `release/YYYY.minor` for `python-environment-tools`. +- [ ] Change the version in `package.json` to the next **even** number. (🤖) +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` at this point which update the version number **only**)_. (🤖) +- [ ] Update `ThirdPartyNotices-Repository.txt` as appropriate. You can check by looking at the [commit history](https://github.com/microsoft/vscode-python/commits/main) and scrolling through to see if there's anything listed there which might have pulled in some code directly into the repository from somewhere else. If you are still unsure you can check with the team. +- [ ] Create a PR from your branch **`bump-release-[YYYY.minor]`** to `main`. Add the `"no change-log"` tag to the PR so it does not show up on the release notes before merging it. + +NOTE: this PR will fail the test in our internal release pipeline called `VS Code (pre-release)` because the version specified in `main` is (temporarily) an invalid pre-release version. This is expected as this will be resolved below. + + +### Step 2: Creating your release branch ❄️ +- [ ] Create a release branch by creating a new branch called **`release/YYYY.minor`** branch from `main`. This branch is now the candidate for our release which will be the base from which we will release. + +NOTE: If there are release branches that are two versions old you can delete them at this time. + +### Step 3 Create a draft GitHub release for the release notes (🤖) ❄️ + +- [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). +- [ ] Specify a new tag called `YYYY.minor.0`. +- [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. +- [ ] Create the release notes by specifying the previous tag for the last stable release and click `Generate release notes`. Quickly check that it only contain notes from what is new in this release. +- [ ] Click `Save draft`. + +### Step 4: Return `main` to dev and unfreeze (❄️ ➡ 💧) +NOTE: The purpose of this step is ensuring that main always is on a dev version number for every night's 🌃 pre-release. Therefore it is imperative that you do this directly after the previous steps to reset the version in main to a dev version **before** a pre-release goes out. +- [ ] Create a branch called **`bump-dev-version-YYYY.[minor+1]`**. +- [ ] Bump the minor version number in the `package.json` to the next `YYYY.[minor+1]` which will be an odd number, and add `-dev`.(🤖) +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) +- [ ] Create a PR from this branch against `main` and merge it. + +NOTE: this PR should make all CI relating to `main` be passing again (such as the failures stemming from step 1). + +### Step 5: Notifications and Checks on External Release Factors +- [ ] Check [Component Governance](https://dev.azure.com/monacotools/Monaco/_componentGovernance/192726?_a=alerts&typeId=11825783&alerts-view-option=active) to make sure there are no active alerts. +- [ ] Manually add/fix any 3rd-party licenses as appropriate based on what the internal build pipeline detects. +- [ ] Open appropriate [documentation issues](https://github.com/microsoft/vscode-docs/issues?q=is%3Aissue+is%3Aopen+label%3Apython). +- [ ] Contact the PM team to begin drafting a blog post. +- [ ] Announce to the development team that `main` is open again. + + +# Release (Wednesday, XXX XX) + +### Step 6: Take the release branch from a candidate to the finalized release +- [ ] Make sure the [appropriate pull requests](https://github.com/microsoft/vscode-docs/pulls) for the [documentation](https://code.visualstudio.com/docs/python/python-tutorial) -- including the [WOW](https://code.visualstudio.com/docs/languages/python) page -- are ready. +- [ ] Check to make sure any final updates to the **`release/YYYY.minor`** branch have been merged. + +### Step 7: Execute the Release +- [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). +- [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - NOTE: Please opt to release the python extension close to when VS Code is released to align when release notes go out. When we bump the VS Code engine number, our extension will not go out to stable until the VS Code stable release but this only occurs when we bump the engine number. +- [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299). +- [ ] Click "approve" in the publish step of [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) to publish the release to the marketplace. 🎉 +- [ ] Take the Github release out of draft. +- [ ] Publish documentation changes. +- [ ] Contact the PM team to publish the blog post. +- [ ] Determine if a hotfix is needed. +- [ ] Merge the release branch **`release/YYYY.minor`** back into `main`. (This step is only required if changes were merged into the release branch. If the only change made on the release branch is the version, this is not necessary. Overall you need to ensure you DO NOT overwrite the version on the `main` branch.) + + +## Steps for Point Release (if necessary) +- [ ] checkout to `main` on your local machine and run `git fetch` to ensure your local is up to date with the remote repo. +- [ ] checkout to the `release/YYY.minor` and check to make sure all necessary changes for the point release have been cherry-picked into the release branch. If not, contact the owner of the changes to do so. +- [ ] Create a branch against **`release/YYYY.minor`** called **`release-[YYYY.minor.point]`**. +- [ ] Bump the point version number in the `package.json` to the next `YYYY.minor.point` +- [ ] Run `npm install` to make sure `package-lock.json` is up-to-date _(you should now see changes to the `package.json` and `package-lock.json` only relating to the new version number)_ . (🤖) +- [ ] If Point Release is due to an issue in `pet`. Update `build\azure-pipeline.stable.yml` to point to the branch `release/YYYY.minor` for `python-environment-tools` with the fix or decided by the team. +- [ ] Create a PR from this branch against `release/YYYY.minor` +- [ ] **Rebase** and merge this PR into the release branch +- [ ] Create a draft GitHub release for the release notes (🤖) ❄️ + - [ ] Create a new [GitHub release](https://github.com/microsoft/vscode-python/releases/new). + - [ ] Specify a new tag called `vYYYY.minor.point`. + - [ ] Have the `target` for the github release be your release branch called **`release/YYYY.minor`**. + - [ ] Create the release notes by specifying the previous tag as the previous version of stable, so the minor release **`vYYYY.minor`** for the last stable release and click `Generate release notes`. + - [ ] Check the generated notes to ensure that all PRs for the point release are included so users know these new changes. + - [ ] Click `Save draft`. +- [ ] Publish the point release + - [ ] Make sure CI is passing for **`release/YYYY.minor`** release branch (🤖). + - [ ] Run the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) pipeline on the **`release/YYYY.minor`** branch. + - [ ] Click `run pipeline`. + - [ ] for `branch/tag` select the release branch which is **`release/YYYY.minor`**. + - [ ] 🧍🧍 Get approval on the release on the [CD](https://dev.azure.com/monacotools/Monaco/_build?definitionId=299) and publish the release to the marketplace. 🎉 + - [ ] Take the Github release out of draft. + +## Steps for contributing to a point release +- [ ] Work with team to decide if point release is necessary +- [ ] Work with team or users to verify the fix is correct and solves the problem without creating any new ones +- [ ] Create PR/PRs and merge then each into main as usual +- [ ] Make sure to still mark if the change is "bug" or "no-changelog" +- [ ] Cherry-pick all PRs to the release branch and check that the changes are in before the package is bumped +- [ ] Notify the release champ that your changes are in so they can trigger a point-release + ## Prep for the _next_ release -- [ ] Create a new [release plan](https://github.com/Microsoft/vscode-python/edit/master/.github/release_plan.md) -- [ ] [(Un-)pin](https://help.github.com/en/articles/pinning-an-issue-to-your-repository) [release plan issues](https://github.com/Microsoft/vscode-python/labels/release%20plan) + +- [ ] Create a new [release plan](https://raw.githubusercontent.com/microsoft/vscode-python/main/.github/release_plan.md). (🤖) +- [ ] [(Un-)pin](https://help.github.com/en/articles/pinning-an-issue-to-your-repository) [release plan issues](https://github.com/Microsoft/vscode-python/labels/release-plan) (🤖) diff --git a/.github/test_plan.md b/.github/test_plan.md deleted file mode 100644 index d64ef246832b..000000000000 --- a/.github/test_plan.md +++ /dev/null @@ -1,496 +0,0 @@ -# Test plan - -## Environment - -- OS: XXX (Windows, macOS, latest Ubuntu LTS) - - Shell: XXX (Command Prompt, PowerShell, bash, fish) -- Python - - Distribution: XXX (CPython, miniconda) - - Version: XXX (2.7, latest 3.x) -- VS Code: XXX (Insiders) - -## Tests - -**ALWAYS**: -- Check the `Output` window under `Python` for logged errors -- Have `Developer Tools` open to detect any errors -- Consider running the tests in a multi-folder workspace -- Focus on in-development features (i.e. experimental debugger and language server) - -
- Scenarios - -### [Environment](https://code.visualstudio.com/docs/python/environments) -#### Interpreters - -- [ ] Interpreter is [shown in the status bar](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) -- [ ] An interpreter can be manually specified using the [`Select Interpreter` command](https://code.visualstudio.com/docs/python/environments#_choosing-an-environment) -- [ ] Detected system-installed interpreters -- [ ] Detected an Anaconda installation -- [ ] (Linux/macOS) Detected all interpreters installed w/ [pyenv](https://github.com/pyenv/pyenv) detected -- [ ] [`"python.pythonPath"`](https://code.visualstudio.com/docs/python/environments#_manually-specifying-an-interpreter) triggers an update in the status bar -- [ ] `Run Python File in Terminal` -- [ ] `Run Selection/Line in Python Terminal` - - [ ] Right-click - - [ ] Command - - [ ] `Shift+Enter` - -#### Terminal -Sample file: -```python -import requests -request = requests.get("https://drive.google.com/uc?export=download&id=1_9On2-nsBQIw3JiY43sWbrF8EjrqrR4U") -with open("survey2017.zip", "wb") as file: - file.write(request.content) -import zipfile -with zipfile.ZipFile('survey2017.zip') as zip: - zip.extractall('survey2017') -import shutil, os -shutil.move('survey2017/survey_results_public.csv','survey2017.csv') -shutil.rmtree('survey2017') -os.remove('survey2017.zip') -``` -- [ ] *Shift+Enter* to send selected code in sample file to terminal works - -#### Virtual environments - -**ALWAYS**: -- Use the latest version of Anaconda -- Realize that `conda` is slow -- Create an environment with a space in their path somewhere as well as upper and lowercase characters -- Make sure that you do not have `python.pythonPath` specified in your `settings.json` when testing automatic detection -- Do note that the `Select Interpreter` drop-down window scrolls - -- [ ] Detected a single virtual environment at the top-level of the workspace folder on Mac when when `python` command points to default Mac Python installation or `python` command fails in the terminal. - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) -- [ ] Detected a single virtual environment at the top-level of the workspace folder on Windows when `python` fails in the terminal. - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) -- [ ] Detected a single virtual environment at the top-level of the workspace folder - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) - - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works - - [ ] Steals focus - - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal - - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart -- [ ] Detect multiple virtual environments contained in the directory specified in `"python.venvPath"` -- [ ] Detected all [conda environments created with an interpreter](https://code.visualstudio.com/docs/python/environments#_conda-environments) - - [ ] Appropriate suffix label specified in status bar (e.g. `(condaenv)`) - - [ ] Prompted to install Pylint - - [ ] Asked whether to install using conda or pip - - [ ] Installs into environment - - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works - - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal - - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart -- [ ] (Linux/macOS until [`-m` is supported](https://github.com/Microsoft/vscode-python/issues/978)) Detected the virtual environment created by [pipenv](https://docs.pipenv.org/) - - [ ] Appropriate suffix label specified in status bar (e.g. `(pipenv)`) - - [ ] Prompt to install Pylint uses `pipenv install --dev` - - [ ] [`Create Terminal`](https://code.visualstudio.com/docs/python/environments#_activating-an-environment-in-the-terminal) works - - [ ] `"python.terminal.activateEnvironment": false` deactivates automatically running the activation script in the terminal - - [ ] After the language server downloads it is able to complete its analysis of the environment w/o requiring a restart -- [ ] (Linux/macOS) Virtual environments created under `{workspaceFolder}/.direnv/python-{python_version}` are detected (for [direnv](https://direnv.net/) and its [`layout python3`](https://github.com/direnv/direnv/blob/master/stdlib.sh) support) - - [ ] Appropriate suffix label specified in status bar (e.g. `(venv)`) - -#### [Environment files](https://code.visualstudio.com/docs/python/environments#_environment-variable-definitions-file) -Sample files: -```python -# example.py -import os -print('Hello,', os.environ.get('WHO'), '!') -``` -``` -# .env -WHO=world -PYTHONPATH=some/path/somewhere -SPAM='hello ${WHO}' -```` - -**ALWAYS**: -- Make sure to use `Reload Window` between tests to reset your environment -- Note that environment files only apply under the debugger and Jedi - -- [ ] Environment variables in a `.env` file are exposed when running under the debugger -- [ ] `"python.envFile"` allows for specifying an environment file manually (e.g. Jedi picks up `PYTHONPATH` changes) -- [ ] `envFile` in a `launch.json` configuration works -- [ ] simple variable substitution works - -#### [Debugging](https://code.visualstudio.com/docs/python/environments#_python-interpreter-for-debugging) - -- [ ] `pythonPath` setting in your `launch.json` overrides your `python.pythonPath` default setting - -### [Linting](https://code.visualstudio.com/docs/python/linting) - -**ALWAYS**: -- Check under the `Problems` tab to see e.g. if a linter is raising errors - -#### Language server - -- [ ] LS is downloaded using HTTP (no SSL) when the "http.proxyStrictSSL" setting is false -- [ ] Installing [`requests`](https://pypi.org/project/requests/) in virtual environment is detected - - [ ] Import of `requests` without package installed is flagged as unresolved - - [ ] Create a virtual environment - - [ ] Install `requests` into the virtual environment - -#### Pylint/default linting -[Prompting to install Pylint is covered under `Environments` above] - -For testing the disablement of the default linting rules for Pylint: -```ini -# pylintrc -[MESSAGES CONTROL] -enable=bad-names -``` -```python3 -# example.py -foo = 42 # Marked as a blacklisted name. -``` -- [ ] Installation via the prompt installs Pylint as appropriate - - [ ] Uses `--user` for system-install of Python - - [ ] Installs into a virtual environment environment directly -- [ ] Pylint works -- [ ] `"python.linting.pylintUseMinimalCheckers": false` turns off the default rules w/ no `pylintrc` file present -- [ ] The existence of a `pylintrc` file turns off the default rules - -#### Other linters - -**Note**: -- You can use the `Run Linting` command to run a newly installed linter -- When the extension installs a new linter, it turns off all other linters - -- [ ] flake8 works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] mypy works - - [ ] `Select linter` lists the linter and installs it if necessary -- [ ] 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 `()` -- [ ] Auto-completions works - -#### [Formatting](https://code.visualstudio.com/docs/python/editing#_formatting) -Sample file: -```python -# There should be _some_ change after running `Format Document`. -import os,sys; -def foo():pass -``` - -- [ ] Prompted to install a formatter if none installed and `Format Document` is run - - [ ] Installing `autopep8` works - - [ ] Installing `black` works - - [ ] Installing `yapf` works -- [ ] Formatters work with default settings (i.e. `"python.formatting.provider"` is specified but not matching `*Path`or `*Args` settings) - - [ ] autopep8 - - [ ] black - - [ ] yapf -- [ ] Formatters work when appropriate `*Path` and `*Args` settings are specified (use absolute paths; use `~` if possible) - - [ ] autopep8 - - [ ] black - - [ ] yapf -- [ ] `"editor.formatOnType": true` works and has expected results - -#### [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring) - -- [ ] [`Extract Variable`](https://code.visualstudio.com/docs/python/editing#_extract-variable) works - - [ ] You are prompted to install `rope` if it is not already available -- [ ] [`Extract method`](https://code.visualstudio.com/docs/python/editing#_extract-method) works - - [ ] You are prompted to install `rope` if it is not already available -- [ ] [`Sort Imports`](https://code.visualstudio.com/docs/python/editing#_sort-imports) works - -### [Debugging](https://code.visualstudio.com/docs/python/debugging) - -- [ ] [Configurations](https://code.visualstudio.com/docs/python/debugging#_debugging-specific-app-types) work (see [`package.json`](https://github.com/Microsoft/vscode-python/blob/master/package.json) and the `"configurationSnippets"` section for all of the possible configurations) -- [ ] Running code from start to finish w/ no special debugging options (e.g. no breakpoints) -- [ ] Breakpoint-like things - - [ ] Breakpoint - - [ ] Set - - [ ] Hit - - [ ] Conditional breakpoint - - [ ] Expression - - [ ] Set - - [ ] Hit - - [ ] Hit count - - [ ] Set - - [ ] Hit - - [ ] Logpoint - - [ ] Set - - [ ] Hit -- [ ] Stepping - - [ ] Over - - [ ] Into - - [ ] Out -- [ ] Can inspect variables - - [ ] Through hovering over variable in code - - [ ] `Variables` section of debugger sidebar -- [ ] [Remote debugging](https://code.visualstudio.com/docs/python/debugging#_remote-debugging) works - - [ ] ... over SSH - - [ ] ... on other branches -- [ ] [App Engine](https://code.visualstudio.com/docs/python/debugging#_google-app-engine-debugging) - -### [Unit testing](https://code.visualstudio.com/docs/python/unit-testing) - -#### [`unittest`](https://code.visualstudio.com/docs/python/unit-testing#_unittest-configuration-settings) -```python -import unittest - -MODULE_SETUP = False - - -def setUpModule(): - global MODULE_SETUP - MODULE_SETUP = True - - -class PassingSetupTests(unittest.TestCase): - CLASS_SETUP = False - METHOD_SETUP = False - - @classmethod - def setUpClass(cls): - cls.CLASS_SETUP = True - - def setUp(self): - self.METHOD_SETUP = True - - def test_setup(self): - self.assertTrue(MODULE_SETUP) - self.assertTrue(self.CLASS_SETUP) - self.assertTrue(self.METHOD_SETUP) - - -class PassingTests(unittest.TestCase): - - def test_passing(self): - self.assertEqual(42, 42) - - def test_passing_still(self): - self.assertEqual("silly walk", "silly walk") - - -class FailingTests(unittest.TestCase): - - def test_failure(self): - self.assertEqual(42, -13) - - def test_failure_still(self): - self.assertEqual("I'm right!", "no, I am!") -``` -- [ ] `Run All Unit Tests` triggers the prompt to configure the test runner -- [ ] Tests are discovered (as shown by code lenses on each test) - - [ ] Code lens for a class runs all tests for that class - - [ ] Code lens for a method runs just that test - - [ ] `Run Test` works - - [ ] `Debug Test` works - - [ ] Module/suite setup methods are also run (run the `test_setup` method to verify) -- [ ] while debugging tests, an uncaught exception in a test does not - cause ptvsd to raise SystemExit - -#### [`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 -- [ ] `Configure Unit Tests` works - - [ ] quick pick for framework (and its settings) - - [ ] selected framework enabled in workspace settings - - [ ] framework's config added (and old config removed) - - [ ] other frameworks disabled in workspace settings -- [ ] `Configure Unit Tests` does not close if it loses focus -- [ ] Cancelling configuration does not leave incomplete settings -- [ ] The first `"request": "test"` entry in launch.json is used for running unit tests - -### [Data Science](https://code.visualstudio.com/docs/python/jupyter-support) -#### P0 Test Scenarios -- [ ] Start and connect to local Jupyter server - 1. Open the file src/test/datascience/manualTestFiles/manualTestFile.py in VSCode - 1. At the top of the file it will list the things that you need installed in your Python environment - 1. On the first cell click `Run Below` - 1. Interactive Window should open, show connection information, and execute cells - 1. The first thing in the window should have a line like this: `Jupyter Server URI: http://localhost:[port number]/?token=[token value]` -- [ ] Verify basic outputs - 1. Run all the cells in manualTestFile.py - 1. Check to make sure that no outputs have errors - 1. Verify that graphs and progress bars are shown -- [ ] Verify export / import - 1. With the results from `Start and connect to local server` open click the `Export as Jupyter Notebook` button in the Interactive Window - 1. Choose a file location and save the generated .ipynb file - 1. When the prompt comes up in the lower right choose to open the file in the browser - 1. The file should open in the web browser and contain the output from the Interactive Window - 1. In VSCode open up the exported .ipynb file in the editor, when the prompt for `Do you want to import the Jupyter Notebook into Python code?` appears click import - 1. The imported file should match the original python file -- [ ] Verify text entry - 1. In the Interactive Window type in some new code `print('testing')` and submit it to the Interactive Windows - 1. Verify the output from what you added -- [ ] Verify dark and light main themes - 1. Repeat the `Start and connect to local server` and `Verify basic outputs` steps using `Default Dark+` and `Default Light+` themes -- [ ] Verify Variable Explorer - 1. After manualTestFile.py has been run drop down the Variables section at the top of the Interactive Window - 1. In the Variables list there should be an entry for all variables created. These variables might change as more is added to manualTestFile.py. - 1. Check that variables have expected values. They will be truncated for longer items - 1. Sort the list ascending and descending by Type. Also sort the list ascending and descenting by Count. Values like (X, Y) use the first X value for Count sort ordering - 1. Check that list, Series, ndarray, and DataFrame types have a button to "Show variable in data viewer" on the right - 1. In the Interactive Window input box add a new variable. Verify that it is added into the Variable Explorer -- [ ] Verify Data Explorer - 1. From the listed types in the Variable explorer open up the Data Viewer by clicking the button or double clicking the row - 1. Inspect the data in the Data Viewer for the expected values - [ ] Verify Sorting and Filtering - 1. Open up the myDataFrame item - 1. Sort the name column ascending and descending - 1. Sort one of the numerical columns ascending and descending - 1. Click the Filter Rows button - 1. In the name filter box input 'a' to filter to just name with an a in them - 1. In one of the numerical columns input a number 1 - 9 to filter to just that column - 1. Open the myList variable in the explorer - 1. Make sure that you can scroll all the way to the end of the entries - -#### P1 Test Scenarios -- [ ] Connect to a `remote` server - 1. Open up a valid python command prompt that can run `jupyter notebook` (a default Anaconda prompt works well) - 1. Run `jupyter notebook` to start up a local Jupyter server - 1. In the command window that launched Jupyter look for the server / token name like so: http://localhost:8888/?token=bf9eae43641cd75015df9104f814b8763ef0e23ffc73720d - 1. Run the command `Python: Select Jupyter server URI` then `Type in the URI to connect to a running jupyter server` - 1. Input the server / token name here - 1. Now run the cells in the manualTestFile.py - 1. Verify that you see the server name in the initial connection message - 1. Verify the outputs of the cells -- [ ] Interactive Window commands - - [ ] Verify per-cell commands - 1. Expand and collapse the input area of a cell - 1. Use the `X` button to remove a cell - 1. Use the `Goto Code` button to jump to the part of the .py file that submitted the code - - [ ] Verify top menu commands - 1. Use `X` to delete all cells - 1. Undo the delete action with `Undo` - 1. Redo the delete action with `Redo` - 1. In manualTestFile.py modify the trange command in the progress bar from 100 to 2000. Run the Cell. As the cell is running hit the `Interrupt iPython Kernel` button - 1. The progress bar should be interrupted and you should see a KeyboardInterrupt error message in the output - 1. Test the `Restart iPython kernel` command. Kernel should be restarted and you should see a status output message for the kernel restart - 1. Use the expand all input and collapse all input commands to collapse all cell inputs -- [ ] Verify theming works - 1. Start Python Interactive window - 1. Add a cell with some comments - 1. Switch VS Code theme to something else - 1. Check that the cell you just added updates the comment color - 1. Switch back and forth between a 'light' and a 'dark' theme - 1. Check that the cell switches colors - 1. Check that the buttons on the top change to their appropriate 'light' or 'dark' versions - 1. Enable the 'ignoreVscodeTheme' setting - 1. Close the Python Interactive window and reopen it. The theme in just the 'Python Interactive' window should be light - 1. Switch to a dark theme. Make sure the interactive window remains in the light theme. -- [ ] Verify code lenses - 1. Check that `Run Cell` `Run Above` and `Run Below` all do the correct thing -- [ ] Verify context menu navigation commands - 1. Check the `Run Current Cell` and `Run Current Cell And Advance` context menu commands - 1. If run on the last cell of the file `Run Current Cell And Advance` should create a new empty cell and advance to it -- [ ] Verify command palette commands - 1. Close the Interactive Window then pick `Python: Show Interactive Window` - 1. Restart the kernel and pick `Python: Run Current File In Python Interactive Window` it should run the whole file again -- [ ] Verify shift-enter - 1. Move to the top cell in the .py file - 1. Shift-enter should run each cell and advance to the next - 1. Shift-enter on the final cell should create a new cell and move to it -- [ ] Verify file without cells - 1. Open the manualTestFileNoCells.py file - 1. Select a chunk of code, shift-enter should send it to the terminal - 1. Open VSCode settings, change `Send Selection To Interactive Window` to true - 1. Select a chunk of code, shift-enter should send that selection to the Interactive Windows - 1. Move your cursor to a line, but don't select anything. Shift-enter should send that line to the Interactive Window -- [ ] Multiple installs - 1. Close and re-open VSCode to make sure that all jupyter servers are closed - 1. Also make sure you are set to locally launch Jupyter and not to connect to an existing URI - 1. In addition to your main testing environment install a new python or miniconda install (conda won't work as it has Jupyter by default) - 1. In VS code change the python interpreter to the new install - 1. Try `Run Cell` - 1. You should get a message that Jupyter was not found and that it is defaulting back to launch on the python instance that has Jupyter -- [ ] LiveShare Support - 1. Install the LiveShare VSCode Extension - 1. Open manualTestFile.py in VSCode - 1. Run the first cell in the file - 1. Switch to the `Live Share` tab in VS Code and start a session - - [ ] Verify server start - 1. Jupyter server instance should appear in the live share tab - 1. Open another window of VSCode - 1. Connect the second instance of VSCode as a Guest to the first Live Share session - 1. After the workspace opens, open the manualTestFile.py on the Guest instance - 1. On the Guest instance run a cell from the file, both via the codelens and via the command palette `Run Cell` command - - [ ] Verify results - 1. Output should show up on the Guest Interactive Window - 1. Same output should show up in the Host Interactive Window - 1. On the Host instance run a cell from the file, both via the codelens and via the command palette - - [ ] Verify results - 1. Output should show up on the Guest Interactive Window - 1. Same output should show up in the Host Interactive Window - -#### P2 Test Scenarios -- [ ] Directory change - - [ ] Verify directory change in export - 1. Follow the previous steps for export, but export the ipynb to a directory outside of the current workspace - 1. Open the file in the browser, you should get an initial cell added to change directory back to your workspace directory - - [ ] Verify directory change in import - 1. Follow the previous steps for import, but import an ipynb that is located outside of your current workspace - 1. Open the file in the editor. There should be python code at the start to change directory to the previous location of the .ipynb file -- [ ] Interactive Window input history history - 1. Start up an Interactive Window session - 1. Input several lines into the Interactive Window terminal - 1. Press up to verify that those previously entered lines show in the Interactive Window terminal history -- [ ] Extra themes - 1. Try several of the themes that come with VSCode that are not the default Dark+ and Light+ -
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000000..09d019dec4a7 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,449 @@ +name: Build + +on: + push: + branches: + - 'main' + - 'release' + - 'release/*' + - 'release-*' + +permissions: {} + +env: + NODE_VERSION: 22.21.1 + PYTHON_VERSION: '3.10' # YML treats 3.10 the number as 3.1, so quotes around 3.10 + # Force a path with spaces and to test extension works in these scenarios + # Unicode characters are causing 2.7 failures so skip that for now. + special-working-directory: './path with spaces' + special-working-directory-relative: 'path with spaces' + # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). + # Also enables a reporter which exits the process running the tests if it haven't already. + MOCHA_REPORTER_JUNIT: true + +jobs: + setup: + name: Set up + if: github.repository == 'microsoft/vscode-python' + runs-on: ubuntu-latest + defaults: + run: + shell: python + outputs: + vsix_basename: ${{ steps.vsix_names.outputs.vsix_basename }} + vsix_name: ${{ steps.vsix_names.outputs.vsix_name }} + vsix_artifact_name: ${{ steps.vsix_names.outputs.vsix_artifact_name }} + steps: + - name: VSIX names + id: vsix_names + run: | + import os + if os.environ["GITHUB_REF"].endswith("/main"): + vsix_type = "insiders" + else: + vsix_type = "release" + print(f"::set-output name=vsix_name::ms-python-{vsix_type}.vsix") + print(f"::set-output name=vsix_basename::ms-python-{vsix_type}") + print(f"::set-output name=vsix_artifact_name::ms-python-{vsix_type}-vsix") + + build-vsix: + name: Create VSIX + if: github.repository == 'microsoft/vscode-python' + needs: setup + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + vsix-target: win32-x64 + - os: windows-latest + target: aarch64-pc-windows-msvc + vsix-target: win32-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: linux-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-gnu + # vsix-target: linux-arm64 + # - os: ubuntu-latest + # target: arm-unknown-linux-gnueabihf + # vsix-target: linux-armhf + # - os: macos-latest + # target: x86_64-apple-darwin + # vsix-target: darwin-x64 + # - os: macos-14 + # target: aarch64-apple-darwin + # vsix-target: darwin-arm64 + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + vsix-target: alpine-x64 + # - os: ubuntu-latest + # target: aarch64-unknown-linux-musl + # vsix-target: alpine-arm64 + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: 'python-env-tools' + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Build VSIX + uses: ./.github/actions/build-vsix + with: + node_version: ${{ env.NODE_VERSION}} + vsix_name: ${{ needs.setup.outputs.vsix_basename }}-${{ matrix.vsix-target }}.vsix + artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }}-${{ matrix.vsix-target }} + cargo_target: ${{ matrix.target }} + vsix_target: ${{ matrix.vsix-target }} + + lint: + name: Lint + if: github.repository == 'microsoft/vscode-python' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Lint + uses: ./.github/actions/lint + with: + node_version: ${{ env.NODE_VERSION }} + + check-types: + name: Check Python types + if: github.repository == 'microsoft/vscode-python' + runs-on: ubuntu-latest + steps: + - name: Use Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Install core Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + options: '-t ./python_files/lib/python --no-cache-dir --implementation py' + + - name: Install Jedi requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: './python_files/jedilsp_requirements/requirements.txt' + options: '-t ./python_files/lib/jedilsp --no-cache-dir --implementation py' + + - name: Install other Python requirements + run: | + python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Pyright + uses: jakebailey/pyright-action@8ec14b5cfe41f26e5f41686a31eb6012758217ef # v3.0.2 + with: + version: 1.1.308 + working-directory: 'python_files' + + python-tests: + name: Python Tests + # The value of runs-on is the OS of the current job (specified in the strategy matrix below) instead of being hardcoded. + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the Unix case. + os: [ubuntu-latest, windows-latest] + # Run the tests on the oldest and most recent versions of Python. + python: ['3.10', '3.x', '3.13'] + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Use Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Install base Python requirements + uses: brettcannon/pip-secure-install@92f400e3191171c1858cc0e0d9ac6320173fdb0c # v1.0.0 + with: + requirements-file: '"${{ env.special-working-directory-relative }}/requirements.txt"' + options: '-t "${{ env.special-working-directory-relative }}/python_files/lib/python" --no-cache-dir --implementation py' + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Run Python unit tests + run: python python_files/tests/run_all.py + + tests: + name: Tests + if: github.repository == 'microsoft/vscode-python' + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: ${{ env.special-working-directory }} + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix + # entry to lower the number of runners used, macOS runners are expensive, + # and we assume that Ubuntu is enough to cover the UNIX case. + os: [ubuntu-latest, windows-latest] + python: ['3.x'] + test-suite: [ts-unit, venv, single-workspace, multi-workspace, debugger, functional] + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + path: ${{ env.special-working-directory-relative }} + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Install Node + uses: actions/setup-node@v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: ${{ env.special-working-directory-relative }}/package-lock.json + + - name: Install dependencies (npm ci) + run: npm ci + + - name: Compile + run: npx gulp prePublishNonBundle + + - name: Localization + run: npx @vscode/l10n-dev@latest export ./src + + - name: Install Python ${{ matrix.python }} + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python }} + + - name: Upgrade Pip + run: python -m pip install -U pip + + # For faster/better builds of sdists. + - name: Install build pre-requisite + run: python -m pip install wheel nox + + - name: Install Python Extension dependencies (jedi, etc.) + run: nox --session install_python_libs + + - name: Install test requirements + run: python -m pip install --upgrade -r build/test-requirements.txt + + - name: Rust Tool Chain setup + uses: dtolnay/rust-toolchain@stable + + - name: Build Native Binaries + run: nox --session native_build + shell: bash + + - name: Install functional test requirements + run: python -m pip install --upgrade -r ./build/functional-test-requirements.txt + if: matrix.test-suite == 'functional' + + - name: Prepare pipenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install pipenv + python -m pipenv run python ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} pipenvPath + + - name: Prepare poetry for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install poetry + Move-Item -Path ".\build\ci\pyproject.toml" -Destination . + poetry env use python + + - name: Prepare virtualenv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + python -m pip install virtualenv + python -m virtualenv .virtualenv/ + if ('${{ matrix.os }}' -match 'windows-latest') { + & ".virtualenv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} virtualEnvPath + } else { + & ".virtualenv/bin/python" ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} virtualEnvPath + } + + - name: Prepare venv for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' && startsWith(matrix.python, 3.) + run: | + python -m venv .venv + if ('${{ matrix.os }}' -match 'windows-latest') { + & ".venv/Scripts/python.exe" ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} venvPath + } else { + & ".venv/bin/python" ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} venvPath + } + + - name: Prepare conda for venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' + shell: pwsh + if: matrix.test-suite == 'venv' + run: | + # 1. For `*.testvirtualenvs.test.ts` + if ('${{ matrix.os }}' -match 'windows-latest') { + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python.exe + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath Scripts | Join-Path -ChildPath conda + } else{ + $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath python + $condaExecPath = Join-Path -Path $Env:CONDA -ChildPath bin | Join-Path -ChildPath conda + } + & $condaPythonPath ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} condaExecPath $condaExecPath + & $condaPythonPath ./build/ci/addEnvPath.py ${{ env.PYTHON_VIRTUAL_ENVS_LOCATION }} condaPath + & $condaExecPath init --all + + - name: Set CI_PYTHON_PATH and CI_DISABLE_AUTO_SELECTION + run: | + echo "CI_PYTHON_PATH=python" >> $GITHUB_ENV + echo "CI_DISABLE_AUTO_SELECTION=1" >> $GITHUB_ENV + shell: bash + if: matrix.test-suite != 'ts-unit' + + # Run TypeScript unit tests only for Python 3.X. + - name: Run TypeScript unit tests + run: npm run test:unittests + if: matrix.test-suite == 'ts-unit' && startsWith(matrix.python, '3.') + + # The virtual environment based tests use the `testSingleWorkspace` set of tests + # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, + # which is set in the "Prepare environment for venv tests" step. + # We also use a third-party GitHub Action to install xvfb on Linux, + # run tests and then clean up the process once the tests ran. + # See https://github.com/GabrielBB/xvfb-action + - name: Run venv tests + env: + TEST_FILES_SUFFIX: testvirtualenvs + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'venv' && matrix.os == 'ubuntu-latest' + + - name: Run single-workspace tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testSingleWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'single-workspace' + + - name: Run multi-workspace tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testMultiWorkspace + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'multi-workspace' + + - name: Run debugger tests + env: + CI_PYTHON_VERSION: ${{ matrix.python }} + uses: GabrielBB/xvfb-action@b706e4e27b14669b486812790492dc50ca16b465 # v1.7 + with: + run: npm run testDebugger + working-directory: ${{ env.special-working-directory }} + if: matrix.test-suite == 'debugger' + + # Run TypeScript functional tests + - name: Run TypeScript functional tests + run: npm run test:functional + if: matrix.test-suite == 'functional' + + smoke-tests: + name: Smoke tests + if: github.repository == 'microsoft/vscode-python' + runs-on: ${{ matrix.os }} + needs: [setup, build-vsix] + strategy: + fail-fast: false + matrix: + # We're not running CI on macOS for now because it's one less matrix entry to lower the number of runners used, + # macOS runners are expensive, and we assume that Ubuntu is enough to cover the UNIX case. + include: + - os: windows-latest + vsix-target: win32-x64 + - os: ubuntu-latest + vsix-target: linux-x64 + + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Checkout Python Environment Tools + uses: actions/checkout@v6 + with: + repository: 'microsoft/python-environment-tools' + path: ${{ env.special-working-directory-relative }}/python-env-tools + persist-credentials: false + sparse-checkout: | + crates + Cargo.toml + Cargo.lock + sparse-checkout-cone-mode: false + + - name: Smoke tests + uses: ./.github/actions/smoke-tests + with: + node_version: ${{ env.NODE_VERSION }} + artifact_name: ${{ needs.setup.outputs.vsix_artifact_name }}-${{ matrix.vsix-target }} diff --git a/.github/workflows/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 f3ef8df8b783..2fa056f84fa6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,11 @@ .DS_Store .huskyrc.json out -node_modules +log.log +**/node_modules *.pyc *.vsix +envVars.txt **/.vscode/.ropeproject/** **/testFiles/**/.cache/** *.noseids @@ -14,14 +16,15 @@ npm-debug.log **/.mypy_cache/** !yarn.lock coverage/ -.vscode-test/** -.vscode test/** -.vscode-smoke/** +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.*/** @@ -31,5 +34,25 @@ obj/** tmp/** .python-version .vs/ -test-results.xml +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 index 7cc70f119660..c6a66a6e6a68 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v10.5.0 +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/.vscode/extensions.json b/.vscode/extensions.json index b0d7a2ff1e6d..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": [ - "ms-vscode.vscode-typescript-tslint-plugin", - "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 2f57a7f4ebb3..1e983413c8d4 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,92 +7,98 @@ "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": "Extension inside container", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}", - "${workspaceFolder}/data" - ], - "stopOnEntry": false, + "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"], "smartStep": true, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile" }, { - "name": "Python: Current File with iPython", - "type": "python", + "name": "Tests (Debugger, VS Code, *.test.ts)", + "type": "extensionHost", "request": "launch", - "module": "IPython", - "console": "integratedTerminal", + "runtimeExecutable": "${execPath}", "args": [ - "${file}" - ] // Additional args should be prefixed with a '--' first. - }, - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${file}", - "console": "integratedTerminal" + "${workspaceFolder}/src/testMultiRootWkspc/multi.code-workspace", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" + ], + "sourceMaps": true, + "smartStep": true, + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "env": { + "IS_CI_SERVER_TEST_DEBUGGER": "1" + }, + "skipFiles": ["/**"] }, { - "name": "Debugger as debugServer", - "type": "node", + // 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", - "program": "${workspaceFolder}/out/client/debugger/debugAdapter/main.js", - "stopOnEntry": false, - "smartStep": true, + "runtimeExecutable": "${execPath}", "args": [ - "--server=4711" + "${workspaceFolder}/src/testMultiRootWkspc/smokeTests", + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test" ], + "env": { + "VSC_PYTHON_CI_TEST_GREP": "Smoke Test", + "VSC_PYTHON_SMOKE_TEST": "1", + "TEST_FILES_SUFFIX": "smoke.test" + }, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/client/**/*.js" - ], - "cwd": "${workspaceFolder}", - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { - "name": "Tests (Debugger, 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/**/*" - ], + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], "preLaunchTask": "Compile", - "env": { - "IS_CI_SERVER_TEST_DEBUGGER": "1" - } + "skipFiles": ["/**"] }, { - "name": "Tests (Single Workspace, VS Code, *.test.ts)", + "name": "Jedi LSP tests", "type": "extensionHost", "request": "launch", "runtimeExecutable": "${execPath}", @@ -103,14 +109,12 @@ "--extensionTestsPath=${workspaceFolder}/out/test" ], "env": { - "VSC_PYTHON_CI_TEST_GREP": "" // Modify this to run a subset of the single workspace tests + "VSC_PYTHON_CI_TEST_GREP": "Language Server:" }, - "stopOnEntry": false, "sourceMaps": true, - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*.js", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "preTestJediLSP", + "skipFiles": ["/**"] }, { "name": "Tests (Multiroot, VS Code, *.test.ts)", @@ -123,13 +127,14 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], - "stopOnEntry": false, + "env": { + "VSC_PYTHON_CI_TEST_GREP": "" // Modify this to run a subset of the single workspace tests + }, "sourceMaps": true, "smartStep": true, - "outFiles": [ - "${workspaceFolder}/out/**/*" - ], - "preLaunchTask": "Compile" + "outFiles": ["${workspaceFolder}/out/**/*", "!${workspaceFolder}/**/node_modules**/*"], + "preLaunchTask": "Compile", + "skipFiles": ["/**"] }, { "name": "Unit Tests (without VS Code, *.unit.test.ts)", @@ -147,10 +152,30 @@ //"--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)", @@ -165,48 +190,73 @@ "--ui=tdd", "--recursive", "--colors", - //"--grep", "", - "--timeout=300000" + // "--grep", "", + "--timeout=300000", + "--exit" ], - "outFiles": [ - "${workspaceFolder}/out/**/*.js" - ], - "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": "Behave Smoke Tests", - "type": "python", + "name": "Node: Current File", + "program": "${file}", "request": "launch", - "program": "${workspaceFolder}/uitests/__main__.py", - "args": [ - "test", - "--", - "--format", - "progress3", - // Change the tag `@wip` to what ever you want to run. - // Default is assumed to be somethign that's a work in progress (wip). - "--tags=@wip" - ], - "justMyCode": false, - "console": "internalConsole" + "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"] } ] -} \ No newline at end of file +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 54e8de97f27a..01de0d907706 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,47 +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, - "**/.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, ".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.testing.promptToConfigure": false, - "python.workspaceSymbols.enabled": false, - "python.formatting.provider": "black", "typescript.preferences.quoteStyle": "single", "javascript.preferences.quoteStyle": "single", - "typescriptHero.imports.stringQuoteStyle": "'", - "prettier.tslintIntegration": true, - "prettier.printWidth": 180, + "prettier.printWidth": 120, "prettier.singleQuote": true, - "python.jediEnabled": false, - "python.analysis.logLevel": "Trace", - "python.analysis.downloadChannel": "beta", - "python.unitTest.promptToConfigure": false, - "python.linting.enabled": true, - "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "cucumberautocomplete.skipDocStringsFormat": true, - "python.linting.flake8Args": ["--max-line-length=120"] + "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 8758e33a5abf..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", @@ -43,6 +26,37 @@ "kind": "test", "isDefault": true } + }, + { + "type": "npm", + "script": "preTestJediLSP", + "problemMatcher": [], + "label": "preTestJediLSP" + }, + { + "type": "npm", + "script": "check-python", + "problemMatcher": ["$python"], + "label": "npm: check-python", + "detail": "npm run check-python:ruff && npm run check-python:pyright" + }, + { + "label": "npm: check-python (venv)", + "type": "shell", + "command": "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 b14ebdc34454..d636ab48f361 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -1,50 +1,47 @@ -!out/client/**/*.map **/*.map +**/*.analyzer.html +**/.env *.vsix -.appveyor.yml .editorconfig +.env .eslintrc +.eslintignore .gitattributes .gitignore .gitmodules -.huskyrc.json +.git* .npmrc .nvmrc .nycrc -.travis.yml CODE_OF_CONDUCT.md CODING_STANDARDS.md CONTRIBUTING.md -CONTRIBUTING - LANGUAGE SERVER.md -coverconfig.json 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 -travis*.log tsconfig*.json tsfmt.json -tslint.json -typings.json -vsc-extension-quickstart.md vscode-python-signing.* -webpack.config.js -webpack.datascience-*.config.js +noxfile.py -.devcontainer/** +.config/** .github/** .mocha-reporter/** .nvm/** +.nox/** .nyc_output +.prettierrc.js +.sonarcloud.properties .venv/** .vscode/** .vscode-test/** .vscode test/** languageServer/** languageServer.*/** +nodeLanguageServer/** +nodeLanguageServer.*/** bin/** build/** BuildOutput/** @@ -53,27 +50,40 @@ 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/** -pythonFiles/tests/** -requirements.txt +python_files/**/*.pyc +python_files/lib/**/*.egg-info/** +python_files/lib/jedilsp/bin/** +python_files/lib/python/bin/** +python_files/tests/** scripts/** src/** test/** tmp/** -tpn/** typings/** types/** -uitests/** +**/__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 11fa70bba36a..56c1f7697ad7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,61 +1,7601 @@ # Changelog +**Please see https://github.com/microsoft/vscode-python/releases for the latest release notes. The notes below have been kept for historical purposes.** -## master +## 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) + +- [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) + +- 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) @@ -221,48 +7761,50 @@ part of! 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) + +- [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) + +- 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 @@ -284,7 +7826,6 @@ part of! 1. Fixes to detection of the shell. ([#5916](https://github.com/microsoft/vscode-python/issues/5916)) - ## 2019.5.18875 (6 June 2019) ### Fixes @@ -305,7 +7846,7 @@ part of! ### Fixes -1. Changes to identificaction of `shell` for the activation of environments in the terminal. +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) @@ -315,7 +7856,6 @@ part of! 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 @@ -415,11 +7955,11 @@ part of! 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. + - 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 @@ -443,53 +7983,54 @@ part of! (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) + +- [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) + +- 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 @@ -632,10 +8173,10 @@ part of! 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. + - Path mapping bug fixes. + - Fix for hang when using debug console. + - Fix for set next statement. + - Fix for multi-threading. ### Code Health @@ -714,60 +8255,61 @@ 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](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 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 templagtes. + - Fix issues related to debugging Django templates. 1. Update the Python language server to 0.2.47. ### Code Health @@ -775,26 +8317,24 @@ part of! 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 Lanaguage Server. +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. + - 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 @@ -890,7 +8430,7 @@ part of! ([#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 intepreter. +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. @@ -903,11 +8443,11 @@ part of! ([#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. + - 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 @@ -956,48 +8496,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](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 @@ -1018,7 +8560,6 @@ part of! ### 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)) @@ -1029,48 +8570,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](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 @@ -1123,8 +8666,8 @@ part of! ([#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 brekpoint targets. - * Properly exclude certain files from showing up in the debugger. + - Validate breakpoint targets. + - Properly exclude certain files from showing up in the debugger. ### Fixes @@ -1153,8 +8696,8 @@ part of! ([#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. + - `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. @@ -1176,56 +8719,56 @@ part of! 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) + +- [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 @@ -1436,7 +8979,6 @@ part of! ### Fixes - 1. Lowering threshold for Language Server support on a platform. ([#3693](https://github.com/Microsoft/vscode-python/issues/3693)) 1. Fix bug affecting multiple linters used in a workspace. @@ -1449,48 +8991,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](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 @@ -1541,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 @@ -1568,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 @@ -1624,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 @@ -1634,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 @@ -1653,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)) @@ -1676,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 @@ -1696,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 @@ -1764,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). @@ -1800,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 @@ -1842,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 @@ -1858,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 @@ -1875,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 @@ -1979,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. @@ -2014,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 @@ -2159,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 @@ -2175,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 @@ -2274,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 @@ -2336,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)) @@ -2362,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. @@ -2446,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 @@ -2505,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)) @@ -2552,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)) @@ -2562,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)! @@ -2587,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. @@ -2612,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 @@ -2638,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)) @@ -2685,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) @@ -2704,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) @@ -2836,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 @@ -2874,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 @@ -2899,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 @@ -2922,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)) @@ -2937,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 ```" - ], -``` -...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 by using the `./requirements.txt` and `build/test-requirements.txt` files -1. Run the tests via `npm run` or the Debugger launch options (you can "Start Without Debugging"). -1. **Note** you will be running tests under the default Python interpreter for the system. - -*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 appropriate configuration you want to run to easily change the interpreter used during testing: - -```js - "env":{ - "CI_PYTHON_PATH": "/absolute/path/to/interpreter/of/choice/python" - } -``` - -You can also run the tests from the command-line (after compiling): - -```shell -npm run testSingleWorkspace # will launch the VSC UI -npm run testMultiWorkspace # will launch the VSC UI -``` -...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). - -### Testing Python Scripts - -The extension has a number of scripts in ./pythonFiles. Tests for these -scripts are found in ./pythonFiles/tests. To run those tests: - -* `python2.7 pythonFiles/tests/run_all.py` -* `python3 -m pythonFiles.tests` - -By default, functional tests are included. To exclude them: - -`python3 -m pythonFiles.tests --no-functional` - -To run only the functional tests: - -`python3 -m pythonFiles.tests --functional` - -### Standard Debugging - -Clone the repo into any directory, open that directory in VSCode, and use the `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 and we try to close before the next release. All other issues are -considered best-effort for that sprint. - -The extension aims to do a new release 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. This allows us to have a -[development build](#development-build) which is expected to be stable at -all times. Once we reach a release candidate, it becomes -our [release branch](https://github.com/microsoft/vscode-python/branches). -At that point only what is in the release branch will make it into the next -release. - -### Issue triaging - -#### Classifying issues - -To help actively track what stage -[issues](https://github.com/Microsoft/vscode-python/issues) -are at, various labels are used. The following label types are expected to -be set on all open issues (otherwise the issue is not considered triaged): - -1. `needs`/`triage`/`classify` -1. `feature` -1. `type` - -These labels cover what is blocking the issue from closing, what is affected by -the issue, and what kind of issue it is. (The `feature` label should be `feature-*` if the issue doesn't fit into any other `feature` label appropriately.) - -It is also very important to make the title accurate. People often write very brief, quick titles or ones that describe what they think the problem is. By updating the title to be appropriately descriptive for what _you_ think the issue is, you not only make finding older issues easier, but you also help make sure that you and the original reporter agree on what the issue is. - -#### Post-classification - -Once an issue has been appropriately classified, there are two keys ways to help out. One is to go through open issues that -have a merged fix and verify that the fix did in fact work. The other is to try to fix issues marked as `needs PR`. - -### Pull requests - -Key details that all pull requests are expected to handle should be -in the [pull request template](https://github.com/Microsoft/vscode-python/blob/master/.github/PULL_REQUEST_TEMPLATE.md). We do expect CI to be passing for a pull request before we will consider merging it. - -### Versioning - -Starting in 2018, the extension switched to -[calendar versioning](http://calver.org/) since the extension -auto-updates and thus there is no need to track its version -number for backwards-compatibility. As such, the major version -is the current year, the minor version is the month when feature -freeze was reached, and the build number is a number that increments for every build. -For example the release made when we reach feature freeze in July 2018 -would be `2018.7.`, and if there is a second release in that month -it would be `2018.7.`. - -## 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). +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 494d033929b0..e9dd52a538cd 100644 --- a/README.md +++ b/README.md @@ -1,84 +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.5), including features such as IntelliSense, linting, debugging, code navigation, code formatting, Jupyter notebook support, refactoring, variable explorer, test explorer, snippets, and more! +A [Visual Studio Code](https://code.visualstudio.com/) [extension](https://marketplace.visualstudio.com/VSCode) with rich support for the [Python language](https://www.python.org/) (for all [actively supported Python versions](https://devguide.python.org/versions/#supported-versions)), providing access points for extensions to seamlessly integrate and offer support for IntelliSense (Pylance), debugging (Python Debugger), formatting, linting, code navigation, refactoring, variable explorer, test explorer, environment management (**NEW** Python Environments Extension). + +## Support for [vscode.dev](https://vscode.dev/) + +The Python extension does offer [some support](https://github.com/microsoft/vscode-python/wiki/Partial-mode) when running on [vscode.dev](https://vscode.dev/) (which includes [github.dev](http://github.dev/)). This includes partial IntelliSense for open files in the editor. + + +## Installed extensions + +The Python extension will automatically install the following extensions by default to provide the best Python development experience in VS Code: + +- [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) – performant Python language support +- [Python Debugger](https://marketplace.visualstudio.com/items?itemName=ms-python.debugpy) – seamless debug experience with debugpy +- **(NEW)** [Python Environments](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-python-envs) – dedicated environment management (see below) + +These extensions are optional dependencies, meaning the Python extension will remain fully functional if they fail to be installed. Any or all of these extensions can be [disabled](https://code.visualstudio.com/docs/editor/extension-marketplace#_disable-an-extension) or [uninstalled](https://code.visualstudio.com/docs/editor/extension-marketplace#_uninstall-an-extension) at the expense of some features. Extensions installed through the marketplace are subject to the [Marketplace Terms of Use](https://cdn.vsassets.io/v/M146_20190123.39/_content/Microsoft-Visual-Studio-Marketplace-Terms-of-Use.pdf). + +### About the Python Environments Extension + +You may now see that the **Python Environments Extension** is installed for you, but it may or may not be "enabled" in your VS Code experience. Enablement is controlled by the setting `"python.useEnvironmentsExtension": true` (or `false`). + +- If you set this setting to `true`, you will manually opt in to using the Python Environments Extension for environment management. +- If you do not have this setting specified, you may be randomly assigned to have it turned on as we roll it out until it becomes the default experience for all users. + +The Python Environments Extension is still under active development and experimentation. Its goal is to provide a dedicated view and improved workflows for creating, deleting, and switching between Python environments, as well as managing packages. If you have feedback, please let us know via [issues](https://github.com/microsoft/vscode-python/issues). + +## Extensibility + +The Python extension provides pluggable access points for extensions that extend various feature areas to further improve your Python development experience. These extensions are all optional and depend on your project configuration and preferences. + +- [Python formatters](https://code.visualstudio.com/docs/python/formatting#_choose-a-formatter) +- [Python linters](https://code.visualstudio.com/docs/python/linting#_choose-a-linter) + +If you encounter issues with any of the listed extensions, please file an issue in its corresponding repo. ## Quick start -* **Step 1.** [Install a supported version of Python on your system](https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites) (note: that the system install of Python on macOS is not supported). -* **Step 2.** Install the Python extension for Visual Studio Code. -* **Step 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 -## Set up your environment -* Select your Python interpreter by clicking on the status bar - - -* Configure the debugger through the Debug Activity Bar +- Select your Python interpreter by clicking on the status bar - + -* Configure tests by running the ``Configure Tests`` command +- Configure the debugger through the Debug Activity Bar - + +- Configure tests by running the `Configure Tests` command + +## Jupyter Notebook quick start -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. +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. -## 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: +- Install the [Jupyter extension](https://marketplace.visualstudio.com/items?itemName=ms-toolsai.jupyter). -Command | Description ---- | --- -```Python: Select Interpreter``` | Switch between Python interpreters, versions, and environments. -```Python: Start REPL``` | Start an interactive Python REPL using the selected interpreter in the VS Code terminal. -```Python: Run Python File in Terminal``` | Runs the active Python file in the VS Code terminal. You can also run a Python file by right-clicking on the file and selecting ```Run Python File in Terminal```. -```Python: Select Linter``` | Switch from Pylint to Flake8 or other supported linters. -```Format Document``` |Formats code using the provided [formatter](https://code.visualstudio.com/docs/python/editing#_formatting) in the ``settings.json`` file. | -```Python: Configure Tests``` | Select a test framework and configure it to display the Test Explorer.| +- Open or create a Jupyter Notebook file (.ipynb) and start coding in our Notebook Editor! + -To see all available Python commands, open the Command Palette and type ```Python```. +For more information you can: -## Feature details +- [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. -Learn more about the rich features of the Python extension: +## Useful commands -* [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more -* [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more -* [Code formatting](https://code.visualstudio.com/docs/python/editing#_formatting): Format your code with black, autopep or yapf +Open the Command Palette (Command+Shift+P on macOS and Ctrl+Shift+P on Windows/Linux) and type in one of the following commands: -* [Debugging](https://code.visualstudio.com/docs/python/debugging): Debug your Python scripts, web apps, remote or multi-threaded processes +| 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. | -* [Testing](https://code.visualstudio.com/docs/python/unit-testing): Run and debug tests through the Test Explorer with unittest, pytest or nose +To see all available Python commands, open the Command Palette and type `Python`. For Jupyter extension commands, just type `Jupyter`. -* [Jupyter Notebooks](https://code.visualstudio.com/docs/python/jupyter-support): Define and run code cells, render plots, visualize variables through the variable explorer and more +## Feature details -* [Environments](https://code.visualstudio.com/docs/python/environments): Automatically activate and switch between virtualenv, venv, pipenv, conda and pyenv environments +Learn more about the rich features of the Python extension: + +- [IntelliSense](https://code.visualstudio.com/docs/python/editing#_autocomplete-and-intellisense): Edit your code with auto-completion, code navigation, syntax checking and more. +- [Linting](https://code.visualstudio.com/docs/python/linting): Get additional code analysis with Pylint, Flake8 and more. +- [Code formatting](https://code.visualstudio.com/docs/python/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). -* [Refactoring](https://code.visualstudio.com/docs/python/editing#_refactoring): Restructure your Python code with variable extraction, method extraction and import sorting ## Supported locales -The extension is available in multiple languages 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` +The extension is available in multiple languages: `de`, `en`, `es`, `fa`, `fr`, `it`, `ja`, `ko-kr`, `nl`, `pl`, `pt-br`, `ru`, `tr`, `zh-cn`, `zh-tw` ## Questions, issues, feature requests, and contributions -* If you have a question about how to accomplish something with the extension, please [ask on Stack Overflow](https://stackoverflow.com/questions/tagged/visual-studio-code+python) -* If you come across a problem with the extension, please [file an issue](https://github.com/microsoft/vscode-python) -* Contributions are always welcome! Please see our [contributing guide](https://github.com/Microsoft/vscode-python/blob/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) - +- 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 @@ -86,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 9f43d06cb58e..000000000000 --- a/ThirdPartyNotices-Distribution.txt +++ /dev/null @@ -1,13309 +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 7.4.4 (https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.4.tgz) -2. @babel/runtime-corejs2 7.1.2 (https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.1.2.tgz) -3. @emotion/hash 0.6.6 (https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz) -4. @emotion/memoize 0.6.6 (https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz) -5. @emotion/stylis 0.7.1 (https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz) -6. @emotion/unitless 0.6.7 (https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz) -7. @jupyterlab/coreutils 2.2.1 (https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.2.1.tgz) -8. @jupyterlab/observables 2.1.1 (https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.1.1.tgz) -9. @jupyterlab/services 3.2.1 (https://registry.npmjs.org/@jupyterlab/services/-/services-3.2.1.tgz) -10. @mapbox/polylabel 1.0.2 (https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz) -11. @nteract/markdown 2.1.4 (https://registry.npmjs.org/@nteract/markdown/-/markdown-2.1.4.tgz) -12. @nteract/mathjax 2.1.4 (https://registry.npmjs.org/@nteract/mathjax/-/mathjax-2.1.4.tgz) -13. @nteract/octicons 0.4.3 (https://registry.npmjs.org/@nteract/octicons/-/octicons-0.4.3.tgz) -14. @nteract/plotly 1.48.3 (https://registry.npmjs.org/@nteract/plotly/-/plotly-1.48.3.tgz) -15. @nteract/transform-dataresource 4.3.5 (https://registry.npmjs.org/@nteract/transform-dataresource/-/transform-dataresource-4.3.5.tgz) -16. @nteract/transform-geojson 3.2.3 (https://registry.npmjs.org/@nteract/transform-geojson/-/transform-geojson-3.2.3.tgz) -17. @nteract/transform-model-debug 3.2.3 (https://registry.npmjs.org/@nteract/transform-model-debug/-/transform-model-debug-3.2.3.tgz) -18. @nteract/transform-plotly 5.0.0 (https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-5.0.0.tgz) -19. @nteract/transform-vdom 2.2.3 (https://registry.npmjs.org/@nteract/transform-vdom/-/transform-vdom-2.2.3.tgz) -20. @nteract/transforms 4.4.4 (https://registry.npmjs.org/@nteract/transforms/-/transforms-4.4.4.tgz) -21. @phosphor/algorithm 1.1.2 (https://registry.npmjs.org/@phosphor/algorithm/-/algorithm-1.1.2.tgz) -22. @phosphor/collections 1.1.2 (https://registry.npmjs.org/@phosphor/collections/-/collections-1.1.2.tgz) -23. @phosphor/coreutils 1.3.0 (https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.0.tgz) -24. @phosphor/disposable 1.1.2 (https://registry.npmjs.org/@phosphor/disposable/-/disposable-1.1.2.tgz) -25. @phosphor/messaging 1.2.2 (https://registry.npmjs.org/@phosphor/messaging/-/messaging-1.2.2.tgz) -26. @phosphor/signaling 1.2.2 (https://registry.npmjs.org/@phosphor/signaling/-/signaling-1.2.2.tgz) -27. _pydev_calltip_util.py (for PyDev.Debugger) (https://github.com/fabioz/PyDev.Debugger/blob/master/_pydev_bundle/_pydev_calltip_util.py) -28. acorn 5.5.3 (https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz) -29. ajv 5.5.2 (https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz) -30. amdefine 1.0.1 (https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz) -31. angular.io (for RxJS 5.5) (https://angular.io/) -32. anser 1.4.7 (https://registry.npmjs.org/anser/-/anser-1.4.7.tgz) -33. ansi-regex 2.1.1 (https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz) -34. ansi-styles 2.2.1 (https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz) -35. ansi-to-html 0.6.7 (https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.6.7.tgz) -36. ansi-to-react 3.3.3 (https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-3.3.3.tgz) -37. applicationinsights 1.0.6 (https://registry.npmjs.org/applicationinsights/-/applicationinsights-1.0.6.tgz) -38. arch 2.1.0 (https://registry.npmjs.org/arch/-/arch-2.1.0.tgz) -39. argparse 1.0.10 (https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz) -40. asn1 0.2.3 (https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz) -41. assert-plus 1.0.0 (https://github.com/joyent/node-assert-plus/tree/v1.0.0) -42. ast-transform 0.0.0 (https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz) -43. ast-types 0.7.8 (https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz) -44. async-limiter 1.0.0 (https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz) -45. asynckit 0.4.0 (https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz) -46. aws-sign2 0.7.0 (https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz) -47. aws4 1.7.0 (https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz) -48. azure-storage 2.10.1 (https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.1.tgz) -49. babel-code-frame 6.26.0 (https://github.com/babel/babel/tree/v6.26.0/packages/babel-code-frame) -50. babel-polyfill 6.26.0 (https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz) -51. babel-runtime 6.26.0 (https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz) -52. bail 1.0.3 (https://registry.npmjs.org/bail/-/bail-1.0.3.tgz) -53. balanced-match 1.0.0 (https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz) -54. base16 1.0.0 (https://registry.npmjs.org/base16/-/base16-1.0.0.tgz) -55. base64-js 0.0.8 (https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz) -56. bcrypt-pbkdf 1.0.1 (https://www.npmjs.com/package/bcrypt-pbkdf) -57. bintrees 1.0.2 (https://registry.npmjs.org/bintrees/-/bintrees-1.0.2.tgz) -58. bootstrap-less 3.3.8 (https://github.com/distros/bootstrap-less) -59. brace-expansion 1.1.11 (https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz) -60. brotli 1.3.2 (https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz) -61. browser-resolve 1.11.3 (https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz) -62. browserify-mime 1.2.9 (https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz) -63. browserify-optional 1.0.1 (https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz) -64. buffer-from 1.1.1 (https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz) -65. builtin-modules 1.1.1 (https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz) -66. caseless 0.12.0 (https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz) -67. chalk 1.1.3 (https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz) -68. character-entities-legacy 1.1.2 (https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.2.tgz) -69. character-reference-invalid 1.1.2 (https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.2.tgz) -70. charenc 0.0.2 (https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz) -71. classnames 2.2.6 (https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz) -72. clsx 1.0.4 (https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz) -73. co 4.6.0 (https://registry.npmjs.org/co/-/co-4.6.0.tgz) -74. collapse-white-space 1.0.4 (https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.4.tgz) -75. color 3.0.0 (https://registry.npmjs.org/color/-/color-3.0.0.tgz) -76. color-convert 1.9.1 (https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz) -77. color-name 1.1.3 (https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz) -78. color-string 1.5.3 (https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz) -79. colornames 1.1.1 (https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz) -80. colors 1.3.0 (https://registry.npmjs.org/colors/-/colors-1.3.0.tgz) -81. colorspace 1.1.2 (https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz) -82. combined-stream 1.0.6 (https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz) -83. commander 2.15.1 (https://registry.npmjs.org/commander/-/commander-2.15.1.tgz) -84. comment-json 1.1.3 (https://registry.npmjs.org/comment-json/-/comment-json-1.1.3.tgz) -85. concat-map 0.0.1 (https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz) -86. concat-stream 1.6.2 (https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz) -87. convert-source-map 1.5.1 (https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz) -88. core-js 2.5.7 (https://registry.npmjs.org/core-js/-/core-js-2.5.7.tgz) -89. core-util-is 1.0.2 (https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz) -90. create-emotion 9.2.12 (https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz) -91. crypt 0.0.2 (https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz) -92. crypto-js 3.1.9-1 (https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz) -93. css-loader 1.0.1 (https://registry.npmjs.org/css-loader/-/css-loader-1.0.1.tgz) -94. d3-array 1.2.4 (https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz) -95. d3-bboxCollide 1.0.4 (https://registry.npmjs.org/d3-bboxCollide/-/d3-bboxCollide-1.0.4.tgz) -96. d3-brush 1.0.6 (https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz) -97. d3-chord 1.0.6 (https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz) -98. d3-collection 1.0.7 (https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz) -99. d3-color 1.2.3 (https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz) -100. d3-contour 1.3.2 (https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz) -101. d3-dispatch 1.0.5 (https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz) -102. d3-drag 1.2.3 (https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz) -103. d3-ease 1.0.5 (https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz) -104. d3-force 1.1.2 (https://registry.npmjs.org/d3-force/-/d3-force-1.1.2.tgz) -105. d3-format 1.3.2 (https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz) -106. d3-glyphedge 1.2.0 (https://registry.npmjs.org/d3-glyphedge/-/d3-glyphedge-1.2.0.tgz) -107. d3-hexbin 0.2.2 (https://registry.npmjs.org/d3-hexbin/-/d3-hexbin-0.2.2.tgz) -108. d3-hierarchy 1.1.8 (https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz) -109. d3-interpolate 1.3.2 (https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz) -110. d3-path 1.0.7 (https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz) -111. d3-polygon 1.0.5 (https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz) -112. d3-quadtree 1.0.1 (https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz) -113. d3-sankey-circular 0.25.0 (https://registry.npmjs.org/d3-sankey-circular/-/d3-sankey-circular-0.25.0.tgz) -114. d3-scale 1.0.7 (https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz) -115. d3-selection 1.3.2 (https://registry.npmjs.org/d3-selection/-/d3-selection-1.3.2.tgz) -116. d3-shape 1.2.2 (https://registry.npmjs.org/d3-shape/-/d3-shape-1.2.2.tgz) -117. d3-time 1.0.10 (https://registry.npmjs.org/d3-time/-/d3-time-1.0.10.tgz) -118. d3-time-format 2.1.3 (https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz) -119. d3-timer 1.0.9 (https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz) -120. d3-transition 1.1.3 (https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.3.tgz) -121. d3-voronoi 1.1.4 (https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz) -122. dashdash 1.14.1 (https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz) -123. deep-equal 1.0.1 (https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz) -124. deep-is 0.1.3 (https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz) -125. delayed-stream 1.0.0 (https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz) -126. dfa 1.2.0 (https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz) -127. diagnostic-channel 0.2.0 (https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz) -128. diagnostic-channel-publishers 0.2.1 (https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz) -129. diagnostics 1.1.1 (https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz) -130. diff 3.5.0 (https://registry.npmjs.org/diff/-/diff-3.5.0.tgz) -131. diff-match-patch 1.0.0 (https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.0.tgz) -132. dom-helpers 3.4.0 (https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz) -133. duplexer2 0.1.4 (https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz) -134. ecc-jsbn 0.1.1 (https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz) -135. emotion 9.2.12 (https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz) -136. enabled 1.0.2 (https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz) -137. encoding 0.1.12 (https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz) -138. entities 1.1.1 (https://registry.npmjs.org/entities/-/entities-1.1.1.tgz) -139. env-variable 0.0.5 (https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz) -140. escape-carriage 1.2.0 (https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.2.0.tgz) -141. escape-string-regexp 1.0.5 (https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz) -142. escodegen 1.8.1 (https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz) -143. esprima 2.7.3 (https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz) -144. estraverse 1.9.3 (https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz) -145. esutils 2.0.2 (https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz) -146. extend 3.0.1 (https://registry.npmjs.org/extend/-/extend-3.0.1.tgz) -147. extsprintf 1.3.0 (https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz) -148. falafel 2.1.0 (https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz) -149. fast-deep-equal 1.1.0 (https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz) -150. fast-json-stable-stringify 2.0.0 (https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz) -151. fast-levenshtein 2.0.6 (https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz) -152. fast-plist 0.1.2 (https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz) -153. fast-safe-stringify 2.0.6 (https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz) -154. fbjs 0.8.17 (https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz) -155. fecha 2.3.3 (https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz) -156. flat 4.0.0 (https://registry.npmjs.org/flat/-/flat-4.0.0.tgz) -157. fontkit 1.8.0 (https://registry.npmjs.org/fontkit/-/fontkit-1.8.0.tgz) -158. foreach 2.0.5 (https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz) -159. forever-agent 0.6.1 (https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz) -160. form-data 2.3.2 (https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz) -161. fs-extra 4.0.3 (https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz) -162. fs.realpath 1.0.0 (https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz) -163. function-bind 1.1.1 (https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz) -164. fuzzy 0.1.3 (https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz) -165. get-port 3.2.0 (https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz) -166. getpass 0.1.7 (https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz) -167. glob 7.1.2 (https://registry.npmjs.org/glob/-/glob-7.1.2.tgz) -168. graceful-fs 4.1.11 (https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz) -169. har-schema 2.0.0 (https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz) -170. har-validator 5.0.3 (https://registry.npmjs.org/har-validator/-/har-validator-5.0.3.tgz) -171. has 1.0.3 (https://registry.npmjs.org/has/-/has-1.0.3.tgz) -172. has-ansi 2.0.0 (https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz) -173. hash-base 3.0.4 (https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz) -174. hash.js 1.1.7 (https://github.com/indutny/hash.js/tree/v1.1.7) -175. http-signature 1.2.0 (https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz) -176. iconv-lite 0.4.21 (https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz) -177. inflight 1.0.6 (https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz) -178. inherits 2.0.3 (https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz) -179. inversify 4.11.1 (https://registry.npmjs.org/inversify/-/inversify-4.11.1.tgz) -180. IPython (for PyDev.Debugger) (https://ipython.org/) -181. is-alphabetical 1.0.2 (https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.2.tgz) -182. is-alphanumerical 1.0.2 (https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.2.tgz) -183. is-buffer 1.1.6 (https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz) -184. is-decimal 1.0.2 (https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz) -185. is-hexadecimal 1.0.2 (https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.2.tgz) -186. is-plain-obj 1.1.0 (https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz) -187. is-stream 1.1.0 (https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz) -188. is-typedarray 1.0.0 (https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz) -189. is-whitespace-character 1.0.2 (https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.2.tgz) -190. is-word-character 1.0.2 (https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz) -191. isarray 1.0.0 (https://github.com/juliangruber/isarray/blob/v1.0.0) -192. isort 4.3.4 (https://github.com/timothycrosley/isort/tree/4.3.4) -193. isstream 0.1.2 (https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz) -194. Jedi 0.13.3 (https://github.com/davidhalter/jedi/tree/v0.13.3) -195. jquery 3.4.1 (https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz) -196. jquery-ui 1.12.1 (https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz) -197. js-tokens 3.0.2 (https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz) -198. js-yaml 3.13.1 (https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz) -199. jsbn 0.1.1 (https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz) -200. json-edm-parser 0.1.2 (https://registry.npmjs.org/json-edm-parser/-/json-edm-parser-0.1.2.tgz) -201. json-parser 1.1.5 (https://registry.npmjs.org/json-parser/-/json-parser-1.1.5.tgz) -202. json-schema 0.2.3 (https://www.npmjs.com/package/json-schema) -203. json-schema-traverse 0.3.1 (https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz) -204. json-stable-stringify 1.0.1 (https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz) -205. json-stringify-safe 5.0.1 (https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz) -206. json2csv 3.11.5 (https://registry.npmjs.org/json2csv/-/json2csv-3.11.5.tgz) -207. jsonc-parser 2.0.3 (https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.0.3.tgz) -208. jsonfile 4.0.0 (https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz) -209. jsonify 0.0.0 (https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz) -210. jsonparse 1.2.0 (https://registry.npmjs.org/jsonparse/-/jsonparse-1.2.0.tgz) -211. jsprim 1.4.1 (https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz) -212. kuler 1.0.1 (https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz) -213. labella 1.1.4 (https://registry.npmjs.org/labella/-/labella-1.1.4.tgz) -214. leaflet 1.3.4 (https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz) -215. less-plugin-inline-urls 1.2.0 (https://registry.npmjs.org/less-plugin-inline-urls/-/less-plugin-inline-urls-1.2.0.tgz) -216. levn 0.3.0 (https://registry.npmjs.org/levn/-/levn-0.3.0.tgz) -217. line-by-line 0.1.6 (https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz) -218. linear-layout-vector 0.0.1 (https://registry.npmjs.org/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz) -219. linebreak 0.3.0 (https://registry.npmjs.org/linebreak/-/linebreak-0.3.0.tgz) -220. lodash 4.17.11 (https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz) -221. lodash.clonedeep 4.5.0 (https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz) -222. lodash.curry 4.1.1 (https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz) -223. lodash.flatten 4.4.0 (https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz) -224. lodash.flow 3.5.0 (https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz) -225. lodash.get 4.4.2 (https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz) -226. lodash.set 4.3.2 (https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz) -227. lodash.uniq 4.5.0 (https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz) -228. logform 2.1.2 (https://registry.npmjs.org/logform/-/logform-2.1.2.tgz) -229. loose-envify 1.4.0 (https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz) -230. lru-cache 4.1.3 (https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz) -231. magic-string 0.22.5 (https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz) -232. markdown-escapes 1.0.2 (https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz) -233. martinez-polygon-clipping 0.1.5 (https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz) -234. material-colors 1.2.6 (https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz) -235. md5 2.2.1 (https://registry.npmjs.org/md5/-/md5-2.2.1.tgz) -236. md5.js 1.3.4 (https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz) -237. mdast-add-list-metadata 1.0.1 (https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz) -238. memoize-one 4.0.0 (https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz) -239. mime-db 1.33.0 (https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz) -240. mime-types 2.1.18 (https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz) -241. minimalistic-assert 1.0.1 (https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz) -242. minimatch 3.0.4 (https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz) -243. minimist 1.2.0 (https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz) -244. mkdirp 0.5.1 (https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz) -245. moment 2.21.0 (http://registry.npmjs.org/moment/-/moment-2.21.0.tgz) -246. monaco-editor 0.16.2 (https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.16.2.tgz) -247. monaco-editor-textmate 2.1.1 (https://registry.npmjs.org/monaco-editor-textmate/-/monaco-editor-textmate-2.1.1.tgz) -248. monaco-textmate 3.0.0 (https://registry.npmjs.org/monaco-textmate/-/monaco-textmate-3.0.0.tgz) -249. named-js-regexp 1.3.3 (https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz) -250. node-fetch 1.7.3 (https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz) -251. node-stream-zip 1.6.0 (https://github.com/antelle/node-stream-zip/tree/1.6.0) -252. numeral 2.0.6 (https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz) -253. oauth-sign 0.8.2 (https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz) -254. object-assign 4.1.1 (https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz) -255. object-keys 1.0.11 (https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz) -256. once 1.4.0 (https://registry.npmjs.org/once/-/once-1.4.0.tgz) -257. one-time 0.0.4 (https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz) -258. onigasm 2.2.2 (https://registry.npmjs.org/onigasm/-/onigasm-2.2.2.tgz) -259. optionator 0.8.2 (https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz) -260. os-browserify 0.3.0 (https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz) -261. os-tmpdir 1.0.2 (https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz) -262. parse-entities 1.2.0 (https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.0.tgz) -263. parso 0.5.0 (https://github.com/davidhalter/parso/tree/v0.5.0) -264. path-browserify 0.0.0 (https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz) -265. path-is-absolute 1.0.1 (https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz) -266. path-parse 1.0.5 (https://github.com/jbgutierrez/path-parse) -267. path-posix 1.0.0 (https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz) -268. pdfkit 0.10.0 (https://registry.npmjs.org/pdfkit/-/pdfkit-0.10.0.tgz) -269. performance-now 2.1.0 (https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz) -270. pidusage 1.2.0 (https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz) -271. png-js 0.1.1 (https://registry.npmjs.org/png-js/-/png-js-0.1.1.tgz) -272. polygon-offset 0.3.1 (https://registry.npmjs.org/polygon-offset/-/polygon-offset-0.3.1.tgz) -273. prelude-ls 1.1.2 (https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz) -274. process 0.11.10 (https://registry.npmjs.org/process/-/process-0.11.10.tgz) -275. process-nextick-args 1.0.7 (https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz) -276. prop-types 15.6.2 (https://registry.npmjs.org/prop-types/-/prop-types-15.6.2.tgz) -277. pseudomap 1.0.2 (https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz) -278. psl 1.1.29 (https://github.com/wrangr/psl/tree/v1.1.29) -279. ptvsd 4.2.4 (https://github.com/Microsoft/ptvsd/tree/v4.2.4) -280. punycode 1.4.1 (https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz) -281. pure-color 1.3.0 (https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz) -282. py2app (for PyDev.Debugger) (https://bitbucket.org/ronaldoussoren/py2app) -283. PyDev.Debugger (for ptvsd 4) (https://pypi.org/project/pydevd/) -284. qs 6.5.2 (https://registry.npmjs.org/qs/-/qs-6.5.2.tgz) -285. querystringify 2.0.0 (https://registry.npmjs.org/querystringify/-/querystringify-2.0.0.tgz) -286. quote-stream 1.0.2 (https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz) -287. raf 3.4.0 (https://registry.npmjs.org/raf/-/raf-3.4.0.tgz) -288. react 16.5.2 (https://registry.npmjs.org/react/-/react-16.5.2.tgz) -289. react-annotation 1.3.1 (https://registry.npmjs.org/react-annotation/-/react-annotation-1.3.1.tgz) -290. react-base16-styling 0.5.3 (https://registry.npmjs.org/react-base16-styling/-/react-base16-styling-0.5.3.tgz) -291. react-color 2.14.1 (https://registry.npmjs.org/react-color/-/react-color-2.14.1.tgz) -292. react-data-grid 6.0.2-0 (https://registry.npmjs.org/react-data-grid/-/react-data-grid-6.0.2-0.tgz) -293. react-dom 16.5.2 (https://registry.npmjs.org/react-dom/-/react-dom-16.5.2.tgz) -294. react-hot-loader 4.3.11 (https://registry.npmjs.org/react-hot-loader/-/react-hot-loader-4.3.11.tgz) -295. react-json-tree 0.11.0 (https://registry.npmjs.org/react-json-tree/-/react-json-tree-0.11.0.tgz) -296. react-lifecycles-compat 3.0.4 (https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz) -297. react-markdown 3.6.0 (https://registry.npmjs.org/react-markdown/-/react-markdown-3.6.0.tgz) -298. react-motion 0.5.2 (https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz) -299. react-move 2.9.1 (https://registry.npmjs.org/react-move/-/react-move-2.9.1.tgz) -300. react-svg-pan-zoom 3.1.0 (https://registry.npmjs.org/react-svg-pan-zoom/-/react-svg-pan-zoom-3.1.0.tgz) -301. react-svgmt 1.1.8 (https://registry.npmjs.org/react-svgmt/-/react-svgmt-1.1.8.tgz) -302. react-table 6.8.6 (https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz) -303. 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) -304. react-virtualized 9.21.1 (https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.1.tgz) -305. reactcss 1.2.3 (https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz) -306. readable-stream 2.0.6 (https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz) -307. reflect-metadata 0.1.12 (https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.12.tgz) -308. remark-parse 5.0.0 (https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz) -309. repeat-string 1.6.1 (https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz) -310. request 2.87.0 (https://registry.npmjs.org/request/-/request-2.87.0.tgz) -311. request-progress 3.0.0 (https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz) -312. requires-port 1.0.0 (https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz) -313. resolve 1.7.1 (https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz) -314. restructure 0.5.4 (https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz) -315. roughjs-es5 0.1.0 (https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz) -316. rxjs 5.5.9 (https://registry.npmjs.org/rxjs/-/rxjs-5.5.9.tgz) -317. safe-buffer 5.1.2 (https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz) -318. safer-buffer 2.1.2 (https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz) -319. sax 1.2.4 (https://registry.npmjs.org/sax/-/sax-1.2.4.tgz) -320. schedule 0.5.0 (https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz) -321. semiotic 1.15.1 (https://registry.npmjs.org/semiotic/-/semiotic-1.15.1.tgz) -322. semiotic-mark 0.3.0 (https://registry.npmjs.org/semiotic-mark/-/semiotic-mark-0.3.0.tgz) -323. semver 5.5.0 (https://registry.npmjs.org/semver/-/semver-5.5.0.tgz) -324. setImmediate (for RxJS 5.5) (https://github.com/YuzuJS/setImmediate) -325. setimmediate 1.0.5 (https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz) -326. shallow-copy 0.0.1 (https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz) -327. simple-swizzle 0.2.2 (https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz) -328. sizzle (for lodash 4.17) (https://sizzlejs.com/) -329. slickgrid 2.4.7 (https://registry.npmjs.org/slickgrid/-/slickgrid-2.4.7.tgz) -330. sprintf-js 1.0.3 (https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz) -331. sshpk 1.14.1 (https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz) -332. stack-trace 0.0.10 (https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz) -333. state-toggle 1.0.1 (https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz) -334. static-eval 2.0.2 (https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz) -335. string-hash 1.1.3 (https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz) -336. string_decoder 0.10.31 (https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz) -337. strip-ansi 5.2.0 (https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz) -338. strip-json-comments 2.0.1 (https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz) -339. style-loader 0.23.1 (https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz) -340. styled-jsx 3.1.0 (https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.1.0.tgz) -341. stylis-rule-sheet 0.0.10 (https://registry.npmjs.org/stylis-rule-sheet/-/stylis-rule-sheet-0.0.10.tgz) -342. sudo-prompt 8.2.0 (https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz) -343. supports-color 2.0.0 (https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz) -344. svg-inline-react 3.1.0 (https://registry.npmjs.org/svg-inline-react/-/svg-inline-react-3.1.0.tgz) -345. svg-path-bounding-box 1.0.4 (https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz) -346. svg-to-pdfkit 0.1.7 (https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.7.tgz) -347. svgpath 2.2.1 (https://registry.npmjs.org/svgpath/-/svgpath-2.2.1.tgz) -348. symbol-observable 1.0.1 (https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz) -349. text-hex 1.0.0 (https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz) -350. throttleit 1.0.0 (https://github.com/component/throttle/tree/1.0.0) -351. through 2.3.8 (https://registry.npmjs.org/through/-/through-2.3.8.tgz) -352. through2 2.0.3 (https://registry.npmjs.org/through2/-/through2-2.0.3.tgz) -353. timers-browserify 2.0.10 (https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz) -354. tiny-inflate 1.0.2 (https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz) -355. tinycolor2 1.4.1 (https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz) -356. tinyqueue 1.2.3 (https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz) -357. tmp 0.0.29 (https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz) -358. tough-cookie 2.3.4 (https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz) -359. transformation-matrix 2.0.3 (https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.0.3.tgz) -360. tree-kill 1.2.0 (https://github.com/pkrumins/node-tree-kill) -361. trim 0.0.1 (https://registry.npmjs.org/trim/-/trim-0.0.1.tgz) -362. trim-trailing-lines 1.1.1 (https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz) -363. triple-beam 1.3.0 (https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz) -364. trough 1.0.3 (https://registry.npmjs.org/trough/-/trough-1.0.3.tgz) -365. tslib 1.9.1 (https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz) -366. tslint 5.14.0 (https://registry.npmjs.org/tslint/-/tslint-5.14.0.tgz) -367. tunnel-agent 0.6.0 (https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz) -368. tweetnacl 0.14.5 (https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz) -369. type-check 0.3.2 (https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz) -370. typedarray 0.0.6 (https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz) -371. typescript-char 0.0.0 (https://github.com/mason-lang/typescript-char) -372. uint64be 1.0.1 (https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz) -373. underscore 1.8.3 (https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz) -374. unherit 1.1.1 (https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz) -375. unicode 10.0.0 (https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz) -376. unicode-properties 1.1.0 (https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.1.0.tgz) -377. unicode-trie 0.3.1 (https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz) -378. unified 6.2.0 (https://registry.npmjs.org/unified/-/unified-6.2.0.tgz) -379. uniqid 5.0.3 (https://registry.npmjs.org/uniqid/-/uniqid-5.0.3.tgz) -380. unist-util-is 2.1.2 (https://registry.npmjs.org/unist-util-is/-/unist-util-is-2.1.2.tgz) -381. unist-util-remove-position 1.1.2 (https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.2.tgz) -382. unist-util-stringify-position 1.1.2 (https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz) -383. unist-util-visit 1.4.0 (https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.0.tgz) -384. unist-util-visit-parents 1.1.2 (https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz) -385. universalify 0.1.1 (https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz) -386. untangle (for ptvsd 4) (https://pypi.org/project/untangle/) -387. untildify 3.0.2 (https://registry.npmjs.org/untildify/-/untildify-3.0.2.tgz) -388. url-parse 1.4.3 (https://registry.npmjs.org/url-parse/-/url-parse-1.4.3.tgz) -389. util 0.10.4 (https://registry.npmjs.org/util/-/util-0.10.4.tgz) -390. util-deprecate 1.0.2 (https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz) -391. uuid 3.3.2 (https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz) -392. validator 9.4.1 (https://registry.npmjs.org/validator/-/validator-9.4.1.tgz) -393. verror 1.10.0 (https://registry.npmjs.org/verror/-/verror-1.10.0.tgz) -394. vfile 2.3.0 (https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz) -395. vfile-location 2.0.3 (https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.3.tgz) -396. vfile-message 1.0.1 (https://registry.npmjs.org/vfile-message/-/vfile-message-1.0.1.tgz) -397. viz-annotation 0.0.1-3 (https://registry.npmjs.org/viz-annotation/-/viz-annotation-0.0.1-3.tgz) -398. vlq 0.2.3 (https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz) -399. vscode-debugadapter 1.28.0 (https://registry.npmjs.org/vscode-debugadapter/-/vscode-debugadapter-1.28.0.tgz) -400. vscode-debugprotocol 1.28.0 (https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.28.0.tgz) -401. vscode-extension-telemetry 0.1.0 (https://registry.npmjs.org/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.0.tgz) -402. vscode-jsonrpc 4.0.0 (https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz) -403. vscode-languageclient 5.2.1 (https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-5.2.1.tgz) -404. vscode-languageserver 5.2.1 (https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-5.2.1.tgz) -405. vscode-languageserver-protocol 3.14.1 (https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.1.tgz) -406. vscode-languageserver-types 3.14.0 (https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz) -407. vscode-uri 1.0.1 (https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.1.tgz) -408. vsls 0.3.1291 (https://registry.npmjs.org/vsls/-/vsls-0.3.1291.tgz) -409. webpack (for lodash 4) (https://webpack.js.org/) -410. winreg 1.2.4 (https://github.com/fresc81/node-winreg/tree/v1.2.4) -411. winston 3.2.1 (https://registry.npmjs.org/winston/-/winston-3.2.1.tgz) -412. winston-transport 4.3.0 (https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz) -413. wordwrap 1.0.0 (https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz) -414. wrappy 1.0.2 (https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz) -415. ws 6.2.1 (https://registry.npmjs.org/ws/-/ws-6.2.1.tgz) -416. x-is-string 0.1.0 (https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz) -417. xml2js 0.4.19 (https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz) -418. xmlbuilder 9.0.7 (https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz) -419. xtend 4.0.1 (https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz) -420. yallist 2.1.2 (https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz) -421. zone.js 0.7.6 (https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz) - - -%% @babel/runtime 7.4.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.4.tgz) -========================================= -MIT License - -Copyright (c) 2014-present Sebastian McKenzie and other contributors - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF @babel/runtime NOTICES AND INFORMATION - -%% @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.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.2.1.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.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.1.1.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.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@jupyterlab/services/-/services-3.2.1.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.48.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/plotly/-/plotly-1.48.3.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 5.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-5.0.0.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 - -%% acorn 5.5.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz) -========================================= -Copyright (C) 2012-2018 by various contributors (see AUTHORS) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF acorn 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 - -%% amdefine 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz) -========================================= -amdefine is released under two licenses: new BSD, and MIT. You may pick the -license that best suits your development needs. The text of both licenses are -provided below. - - -The "New" BSD License: ----------------------- - -Copyright (c) 2011-2016, The Dojo Foundation -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - * Neither the name of the Dojo Foundation nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - - - -MIT License ------------ - -Copyright (c) 2011-2016, The Dojo Foundation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF amdefine 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-regex 2.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.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 ansi-regex NOTICES AND INFORMATION - -%% ansi-styles 2.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.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 ansi-styles 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 - -%% argparse 1.0.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz) -========================================= -(The MIT License) - -Copyright (C) 2012 by Vitaly Puzrin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF argparse 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 - -%% ast-transform 0.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz) -========================================= -## The MIT License (MIT) ## - -Copyright (c) 2014 Hugh Kennedy - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF ast-transform NOTICES AND INFORMATION - -%% ast-types 0.7.8 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz) -========================================= -Copyright (c) 2013 Ben Newman - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF ast-types NOTICES AND INFORMATION - -%% async-limiter 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz) -========================================= -The MIT License (MIT) -Copyright (c) 2017 Samuel Reed - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF async-limiter 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 - -%% 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-code-frame 6.26.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/babel/babel/tree/v6.26.0/packages/babel-code-frame) -========================================= -MIT License - -Copyright (c) 2014-2017 Sebastian McKenzie - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -========================================= -END OF babel-code-frame 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 - -%% 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 - -%% base64-js 0.0.8 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014 - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF base64-js 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 - -%% bootstrap-less 3.3.8 NOTICES AND INFORMATION BEGIN HERE (https://github.com/distros/bootstrap-less) -========================================= -The MIT License (MIT) - -Copyright (c) 2011-2019 Twitter, Inc. -Copyright (c) 2011-2019 The Bootstrap Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF bootstrap-less 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 - -%% brotli 1.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz) -========================================= -Copyright 2019 brotli 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 brotli NOTICES AND INFORMATION - -%% browser-resolve 1.11.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2013-2015 Roman Shtylman - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF browser-resolve 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 - -%% browserify-optional 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz) -========================================= -Copyright 2019 browserify-optional 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 browserify-optional NOTICES AND INFORMATION - -%% buffer-from 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz) -========================================= -MIT License - -Copyright (c) 2016, 2018 Linus Unnebäck - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF buffer-from NOTICES AND INFORMATION - -%% builtin-modules 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.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 builtin-modules 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 - -%% chalk 1.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/chalk/-/chalk-1.1.3.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 chalk 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 - -%% 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 - -%% clsx 1.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz) -========================================= -MIT License - -Copyright (c) Luke Edwards (lukeed.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF clsx 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 - -%% color 3.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/color/-/color-3.0.0.tgz) -========================================= -Copyright (c) 2012 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -========================================= -END OF color NOTICES AND INFORMATION - -%% color-convert 1.9.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz) -========================================= -Copyright (c) 2011-2016 Heather Arthur - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -========================================= -END OF color-convert NOTICES AND INFORMATION - -%% color-name 1.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz) -========================================= -The MIT License (MIT) -Copyright (c) 2015 Dmitry Ivanov - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -========================================= -END OF color-name NOTICES AND INFORMATION - -%% color-string 1.5.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz) -========================================= -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 color-string NOTICES AND INFORMATION - -%% colornames 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Tim Oxley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF colornames NOTICES AND INFORMATION - -%% colors 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/colors/-/colors-1.3.0.tgz) -========================================= -MIT License - -Original Library - - Copyright (c) Marak Squires - -Additional Functionality - - Copyright (c) Sindre Sorhus (sindresorhus.com) - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF colors NOTICES AND INFORMATION - -%% colorspace 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF colorspace 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 - -%% commander 2.15.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/commander/-/commander-2.15.1.tgz) -========================================= -(The MIT License) - -Copyright (c) 2011 TJ Holowaychuk - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF commander 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 - -%% 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 - -%% concat-stream 1.6.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz) -========================================= -The MIT License - -Copyright (c) 2013 Max Ogden - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -========================================= -END OF concat-stream NOTICES AND INFORMATION - -%% convert-source-map 1.5.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.5.1.tgz) -========================================= -Copyright 2013 Thorsten Lorenz. -All rights reserved. - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF convert-source-map 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 - -%% crypto-js 3.1.9-1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz) -========================================= -# License - -[The MIT License (MIT)](http://opensource.org/licenses/MIT) - -Copyright (c) 2009-2013 Jeff Mott -Copyright (c) 2013-2016 Evan Vosberg - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF crypto-js 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 - -%% deep-equal 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/deep-equal/-/deep-equal-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 deep-equal NOTICES AND INFORMATION - -%% deep-is 0.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz) -========================================= -Copyright (c) 2012, 2013 Thorsten Lorenz -Copyright (c) 2012 James Halliday -Copyright (c) 2009 Thomas Robinson <280north.com> - -This software is released under the MIT license: - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF deep-is 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 - -%% dfa 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz) -========================================= -Copyright 2019 dfa 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 dfa 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 - -%% diagnostics 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF diagnostics NOTICES AND INFORMATION - -%% diff 3.5.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/diff/-/diff-3.5.0.tgz) -========================================= -Software License Agreement (BSD License) - -Copyright (c) 2009-2015, Kevin Decker - -All rights reserved. - -Redistribution and use of this software in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above - copyright notice, this list of conditions and the - following disclaimer. - -* Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the - following disclaimer in the documentation and/or other - materials provided with the distribution. - -* Neither the name of Kevin Decker nor the names of its - contributors may be used to endorse or promote products - derived from this software without specific prior - written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR -CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER -IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT -OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -========================================= -END OF diff 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 - -%% dom-helpers 3.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Jason Quense - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -========================================= -END OF dom-helpers NOTICES AND INFORMATION - -%% duplexer2 0.1.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz) -========================================= -Copyright (c) 2013, Deoxxa Development -====================================== -All rights reserved. --------------------- - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -1. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -3. Neither the name of Deoxxa Development nor the names of its contributors - may be used to endorse or promote products derived from this software - without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY DEOXXA DEVELOPMENT ''AS IS'' AND ANY -EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL DEOXXA DEVELOPMENT BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF duplexer2 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 - -%% enabled 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Arnout Kazemier, Martijn Swaagman, the Contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF enabled 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 - -%% env-variable 0.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz) -========================================= -Copyright 2014 Arnout Kazemier - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF env-variable 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 - -%% escape-string-regexp 1.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.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 escape-string-regexp NOTICES AND INFORMATION - -%% escodegen 1.8.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz) -========================================= -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 escodegen 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 - -%% estraverse 1.9.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz) -========================================= -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 estraverse NOTICES AND INFORMATION - -%% esutils 2.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz) -========================================= -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 esutils 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 - -%% 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 - -%% falafel 2.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz) -========================================= -Copyright 2019 falafel 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 falafel 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 - -%% fast-levenshtein 2.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz) -========================================= -(MIT License) - -Copyright (c) 2013 [Ramesh Nair](http://www.hiddentao.com/) - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - - -========================================= -END OF fast-levenshtein NOTICES AND INFORMATION - -%% fast-plist 0.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz) -========================================= -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 fast-plist NOTICES AND INFORMATION - -%% fast-safe-stringify 2.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 David Mark Clements -Copyright (c) 2017 David Mark Clements & Matteo Collina -Copyright (c) 2018 David Mark Clements, Matteo Collina & Ruben Bridgewater - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF fast-safe-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 - -%% fecha 2.3.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Taylor Hakes - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -========================================= -END OF fecha 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 - -%% fontkit 1.8.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/fontkit/-/fontkit-1.8.0.tgz) -========================================= -Copyright 2019 fontkit 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 fontkit NOTICES AND INFORMATION - -%% foreach 2.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz) -========================================= -The MIT License - -Copyright (c) 2013 Manuel Stofer - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF foreach 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 - -%% 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 - -%% function-bind 1.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz) -========================================= -Copyright (c) 2013 Raynos. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - - -========================================= -END OF function-bind 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 - -%% 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 1.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/has/-/has-1.0.3.tgz) -========================================= -Copyright (c) 2013 Thiago de Arruda - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF has NOTICES AND INFORMATION - -%% has-ansi 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.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 has-ansi 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 - -%% hash.js 1.1.7 NOTICES AND INFORMATION BEGIN HERE (https://github.com/indutny/hash.js/tree/v1.1.7) -========================================= -This software is licensed under the MIT License. - -Copyright Fedor Indutny, 2014. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. - - -========================================= -END OF hash.js 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-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-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-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-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-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-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 - -%% 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 - -%% 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.13.3 NOTICES AND INFORMATION BEGIN HERE (https://github.com/davidhalter/jedi/tree/v0.13.3) -========================================= -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 - -%% jquery 3.4.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz) -========================================= -Copyright JS Foundation and other contributors, https://js.foundation/ - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF jquery NOTICES AND INFORMATION - -%% jquery-ui 1.12.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz) -========================================= -Copyright jQuery Foundation and other contributors, https://jquery.org/ - -This software consists of voluntary contributions made by many -individuals. For exact contribution history, see the revision history -available at https://github.com/jquery/jquery-ui - -The following license applies to all parts of this software except as -documented below: - -==== - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -==== - -Copyright and related rights for sample code are waived via CC0. Sample -code is defined as all source code contained within the demos directory. - -CC0: http://creativecommons.org/publicdomain/zero/1.0/ - -==== - -All files located in the node_modules and external directories are -externally maintained libraries used by this software which have their -own licenses; we recommend you read them, as their terms may differ from -the terms above. - -========================================= -END OF jquery-ui NOTICES AND INFORMATION - -%% js-tokens 3.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.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 js-tokens NOTICES AND INFORMATION - -%% js-yaml 3.13.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz) -========================================= -(The MIT License) - -Copyright (C) 2011-2015 by Vitaly Puzrin - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF js-yaml 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 - -%% jsonc-parser 2.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.0.3.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 jsonc-parser 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 - -%% kuler 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz) -========================================= -Copyright 2014 Arnout Kazemier - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF kuler 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 - -%% less-plugin-inline-urls 1.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/less-plugin-inline-urls/-/less-plugin-inline-urls-1.2.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: - - (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 AND INFORMATION - -%% levn 0.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/levn/-/levn-0.3.0.tgz) -========================================= -Copyright (c) George Zahariev - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF levn 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 - -%% linear-layout-vector 0.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz) -========================================= -Copyright 2019 linear-layout-vector 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 linear-layout-vector NOTICES AND INFORMATION - -%% linebreak 0.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/linebreak/-/linebreak-0.3.0.tgz) -========================================= -Copyright 2019 linebreak 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 linebreak 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 - -%% logform 2.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/logform/-/logform-2.1.2.tgz) -========================================= -MIT License - -Copyright (c) 2017 Charlie Robbins & the Contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF logform NOTICES AND INFORMATION - -%% loose-envify 1.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Andres Suarez - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF loose-envify NOTICES AND INFORMATION - -%% lru-cache 4.1.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.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 lru-cache NOTICES AND INFORMATION - -%% magic-string 0.22.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz) -========================================= -Copyright 2018 Rich Harris - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - - -========================================= -END OF magic-string 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 - -%% 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 - -%% minimalistic-assert 1.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz) -========================================= -Copyright 2015 Calvin Metcalf - -Permission to use, copy, modify, and/or distribute this software for any purpose -with or without fee is hereby granted, provided that the above copyright notice -and this permission notice appear in all copies. - -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH -REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, -INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM -LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE -OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR -PERFORMANCE OF THIS SOFTWARE. -========================================= -END OF minimalistic-assert 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 - -%% mkdirp 0.5.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz) -========================================= -Copyright 2010 James Halliday (mail@substack.net) - -This project is free software released under the MIT/X11 license: - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF mkdirp 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 - -%% monaco-editor 0.16.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.16.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2016 - present Microsoft Corporation - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF monaco-editor NOTICES AND INFORMATION - -%% monaco-editor-textmate 2.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/monaco-editor-textmate/-/monaco-editor-textmate-2.1.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2018 Neek Sandhu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF monaco-editor-textmate NOTICES AND INFORMATION - -%% monaco-textmate 3.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/monaco-textmate/-/monaco-textmate-3.0.0.tgz) -========================================= -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 monaco-textmate 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 - -%% 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-keys 1.0.11 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz) -========================================= -The MIT License (MIT) - -Copyright (C) 2013 Jordan Harband - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF object-keys 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 - -%% one-time 0.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/one-time/-/one-time-0.0.4.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 one-time NOTICES AND INFORMATION - -%% onigasm 2.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/onigasm/-/onigasm-2.2.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2018 Neek Sandhu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF onigasm NOTICES AND INFORMATION - -%% optionator 0.8.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz) -========================================= -Copyright (c) George Zahariev - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF optionator 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.5.0 NOTICES AND INFORMATION BEGIN HERE (https://github.com/davidhalter/parso/tree/v0.5.0) -========================================= -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 - -%% 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-parse 1.0.5 NOTICES AND INFORMATION BEGIN HERE (https://github.com/jbgutierrez/path-parse) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Javier Blanco - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -========================================= -END OF path-parse 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 - -%% pdfkit 0.10.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/pdfkit/-/pdfkit-0.10.0.tgz) -========================================= -MIT LICENSE -Copyright (c) 2014 Devon Govett - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -========================================= -END OF pdfkit 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 - -%% png-js 0.1.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/png-js/-/png-js-0.1.1.tgz) -========================================= -MIT License - -Copyright (c) 2017 Devon Govett - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF png-js 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 - -%% prelude-ls 1.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz) -========================================= -Copyright (c) George Zahariev - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF prelude-ls 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 - -%% pseudomap 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/pseudomap/-/pseudomap-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 pseudomap 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.4 NOTICES AND INFORMATION BEGIN HERE (https://github.com/Microsoft/ptvsd/tree/v4.2.4) -========================================= - 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 - -%% quote-stream 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.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 quote-stream NOTICES AND INFORMATION - -%% raf 3.4.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/raf/-/raf-3.4.0.tgz) -========================================= -Copyright 2013 Chris Dickinson - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF raf 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-data-grid 6.0.2-0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-data-grid/-/react-data-grid-6.0.2-0.tgz) -========================================= -The MIT License (MIT) - -Original work Copyright (c) 2014 Prometheus Research -Modified work Copyright 2015 Adazzle - -For the original source code please see https://github.com/prometheusresearch/react-grid - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 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-data-grid 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-lifecycles-compat 3.0.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.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 react-lifecycles-compat 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-motion 0.5.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 React Motion authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -========================================= -END OF react-motion NOTICES AND INFORMATION - -%% react-move 2.9.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-move/-/react-move-2.9.1.tgz) -========================================= - -MIT License - -Copyright (c) 2017 Steven Hall and Tanner Linsley - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-move NOTICES AND INFORMATION - -%% react-svg-pan-zoom 3.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-svg-pan-zoom/-/react-svg-pan-zoom-3.1.0.tgz) -========================================= -MIT License - -Copyright (c) 2016 https://github.com/chrvadala - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF react-svg-pan-zoom NOTICES AND INFORMATION - -%% react-svgmt 1.1.8 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-svgmt/-/react-svgmt-1.1.8.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2017 Hugo Zapata - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF react-svgmt 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 - -%% react-virtualized 9.21.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.1.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Brian Vaughn - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -========================================= -END OF react-virtualized 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 - -%% 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-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 1.7.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/resolve/-/resolve-1.7.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 resolve NOTICES AND INFORMATION - -%% restructure 0.5.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz) -========================================= -Copyright 2019 restructure 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 restructure 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 - -%% 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 - -%% 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 - -%% setimmediate 1.0.5 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz) -========================================= -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 - -%% shallow-copy 0.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/shallow-copy/-/shallow-copy-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 shallow-copy NOTICES AND INFORMATION - -%% simple-swizzle 0.2.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Josh Junon - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -========================================= -END OF simple-swizzle 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 - -%% slickgrid 2.4.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/slickgrid/-/slickgrid-2.4.7.tgz) -========================================= -Copyright (c) 2009-2019 Michael Leibman and Ben McIntyre, http://github.com/6pac/slickgrid - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF slickgrid NOTICES AND INFORMATION - -%% sprintf-js 1.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz) -========================================= -Copyright (c) 2007-2014, Alexandru Marasteanu -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: -* Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. -* Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. -* Neither the name of this software nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -========================================= -END OF sprintf-js 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 - -%% stack-trace 0.0.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz) -========================================= -Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - -========================================= -END OF stack-trace 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-eval 2.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.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 static-eval 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 - -%% strip-ansi 5.2.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.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 strip-ansi NOTICES AND INFORMATION - -%% strip-json-comments 2.0.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.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 strip-json-comments 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 - -%% supports-color 2.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/supports-color/-/supports-color-2.0.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 supports-color NOTICES AND INFORMATION - -%% svg-inline-react 3.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/svg-inline-react/-/svg-inline-react-3.1.0.tgz) -========================================= -Copyright 2015-2017 Jaeho Lee - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS -FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR -COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER -IN AN ACTION OF CONTRACT, 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-inline-react 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 - -%% svg-to-pdfkit 0.1.7 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.7.tgz) -========================================= -Copyright 2019 svg-to-pdfkit 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 svg-to-pdfkit 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 - -%% text-hex 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2014-2015 Arnout Kazemier - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF text-hex 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 - -%% through 2.3.8 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/through/-/through-2.3.8.tgz) -========================================= -The MIT License - -Copyright (c) 2011 Dominic Tarr - -Permission is hereby granted, free of charge, -to any person obtaining a copy of this software and -associated documentation files (the "Software"), to -deal in the Software without restriction, including -without limitation the rights to use, copy, modify, -merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom -the Software is furnished to do so, -subject to the following conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF through NOTICES AND INFORMATION - -%% through2 2.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/through2/-/through2-2.0.3.tgz) -========================================= -# The MIT License (MIT) - -**Copyright (c) 2016 Rod Vagg (the "Original Author") and additional contributors** - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -========================================= -END OF through2 NOTICES AND INFORMATION - -%% timers-browserify 2.0.10 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz) -========================================= -# timers-browserify - -This project uses the [MIT](http://jryans.mit-license.org/) license: - - Copyright © 2012 J. Ryan Stinnett - - Permission is hereby granted, free of charge, to any person obtaining a - copy of this software and associated documentation files (the “Software”), - to deal in the Software without restriction, including without limitation - the rights to use, copy, modify, merge, publish, distribute, sublicense, - and/or sell copies of the Software, and to permit persons to whom the - Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER - DEALINGS IN THE SOFTWARE. - -# lib/node - -The `lib/node` directory borrows files from joyent/node which uses the following license: - - Copyright Joyent, Inc. and other Node contributors. All rights reserved. - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to - deal in the Software without restriction, including without limitation the - rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - sell copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - IN THE SOFTWARE. - -========================================= -END OF timers-browserify NOTICES AND INFORMATION - -%% tiny-inflate 1.0.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz) -========================================= -Copyright 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF tiny-inflate 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 - -%% 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 - -%% transformation-matrix 2.0.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.0.3.tgz) -========================================= -MIT License - -Copyright (c) 2017 https://github.com/chrvadala - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF transformation-matrix 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 - -%% triple-beam 1.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz) -========================================= -MIT License - -Copyright (c) 2017 winstonjs - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -========================================= -END OF triple-beam 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 - -%% tslib 1.9.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tslib/-/tslib-1.9.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 tslib NOTICES AND INFORMATION - -%% tslint 5.14.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/tslint/-/tslint-5.14.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: - - (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 tslint 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 - -%% type-check 0.3.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz) -========================================= -Copyright (c) George Zahariev - -Permission is hereby granted, free of charge, to any person -obtaining a copy of this software and associated documentation -files (the "Software"), to deal in the Software without -restriction, including without limitation the rights to use, -copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the -Software is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF type-check NOTICES AND INFORMATION - -%% typedarray 0.0.6 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz) -========================================= -/* - Copyright (c) 2010, Linden Research, Inc. - Copyright (c) 2012, Joshua Bell - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - THE SOFTWARE. - $/LicenseInfo$ - */ - -// Original can be found at: -// https://bitbucket.org/lindenlab/llsd -// Modifications by Joshua Bell inexorabletash@gmail.com -// https://github.com/inexorabletash/polyfill - -// ES3/ES5 implementation of the Krhonos Typed Array Specification -// Ref: http://www.khronos.org/registry/typedarray/specs/latest/ -// Date: 2011-02-01 -// -// Variations: -// * Allows typed_array.get/set() as alias for subscripts (typed_array[]) - -========================================= -END OF typedarray 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 - -%% 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 - -%% unicode-properties 1.1.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.1.0.tgz) -========================================= -Copyright 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unicode-properties NOTICES AND INFORMATION - -%% unicode-trie 0.3.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz) -========================================= -Copyright 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF unicode-trie 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 - -%% 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 - -%% 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 - -%% 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 - -%% util 0.10.4 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/util/-/util-0.10.4.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 util 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 - -%% vlq 0.2.3 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz) -========================================= -Copyright (c) 2017 [these people](https://github.com/Rich-Harris/vlq/graphs/contributors) - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -========================================= -END OF vlq 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 4.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.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-jsonrpc NOTICES AND INFORMATION - -%% vscode-languageclient 5.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-5.2.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-languageclient NOTICES AND INFORMATION - -%% vscode-languageserver 5.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-5.2.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 NOTICES AND INFORMATION - -%% vscode-languageserver-protocol 3.14.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.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-protocol NOTICES AND INFORMATION - -%% vscode-languageserver-types 3.14.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.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-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 - -%% vsls 0.3.1291 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/vsls/-/vsls-0.3.1291.tgz) -========================================= -MICROSOFT PRE-RELEASE SOFTWARE LICENSE TERMS - -MICROSOFT VISUAL STUDIO LIVE SHARE SOFTWARE - -These license terms are an agreement between Microsoft Corporation (or based on where you live, one of its affiliates) and you. They apply to the pre-release software named above. The terms also apply to any Microsoft services or updates for the software, except to the extent those have additional terms. - -IF YOU COMPLY WITH THESE LICENSE TERMS, YOU HAVE THE RIGHTS BELOW. - -1. INSTALLATION AND USE RIGHTS. You may install and use any number of copies of the software to evaluate it as you develop and test your software applications. You may use the software only with Microsoft Visual Studio or Visual Studio Code. The software works in tandem with an associated preview release service, as described below. - -2. PRE-RELEASE SOFTWARE. The software is a pre-release version. It may not work the way a final version of the software will. Microsoft may change it for the final, commercial version. We also may not release a commercial version. Microsoft is not obligated to provide maintenance, technical support or updates to you for the software. - -3. ASSOCIATED ONLINE SERVICES. - - a. Microsoft Azure Services. Some features of the software provide access to, or rely on, Azure online services, including an associated Azure online service to the software, Visual Studio Live Share (the “corresponding service”). The use of those services (but not the software) is governed by the separate terms and privacy policies in the agreement under which you obtained the Azure services at https://go.microsoft.com/fwLink/p/?LinkID=233178 (and, with respect to the corresponding service, the additional terms below). Please read them. The services may not be available in all regions. - - b. Limited Availability. The corresponding service is currently in “Preview,” and therefore, we may change or discontinue the corresponding service at any time without notice. Any changes or updates to the corresponding service may cause the software to stop working and may result in the deletion of any data stored on the corresponding service. You may not receive notice prior to these updates. - -4. Licenses for other components. The software may include third party components with separate legal notices or governed by other agreements, as described in the ThirdPartyNotices file accompanying the software. Even if such components are governed by other agreements, the disclaimers and the limitations on and exclusions of damages below also apply. - -5. DATA. - - a. Data Collection. The software may collect information about you and your use of the software, and send that to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may opt out of many of these scenarios, but not all, as described in the product documentation. In using the software, you must comply with applicable law. You can learn more about data collection and use in the help documentation and the privacy statement at http://go.microsoft.com/fwlink/?LinkId=398505. Your use of the software operates as your consent to these practices. - - b. Processing of Personal Data. To the extent Microsoft is a processor or subprocessor of personal data in connection with the software, Microsoft makes the commitments in the European Union General Data Protection Regulation Terms of the Online Services Terms to all customers effective May 25, 2018, at http://go.microsoft.com/?linkid=9840733. - -6. FEEDBACK. If you give feedback about the software to Microsoft, you give to Microsoft, without charge, the right to use, share and commercialize your feedback in any way and for any purpose. You will not give feedback that is subject to a license that requires Microsoft to license its software or documentation to third parties because we include your feedback in them. These rights survive this agreement. - -7. SCOPE OF LICENSE. The software is licensed, not sold. This agreement only gives you some rights to use the software. Microsoft reserves all other rights. Unless applicable law gives you more rights despite this limitation, you may use the software only as expressly permitted in this agreement. In doing so, you must comply with any technical limitations in the software that only allow you to use it in certain ways. For example, if Microsoft technically limits or disables extensibility for the software, you may not extend the software by, among other things, loading or injecting into the software any non-Microsoft add-ins, macros, or packages; modifying the software registry settings; or adding features or functionality equivalent to that found in other Visual Studio products. You may not: - - * work around any technical limitations in the software; - - * reverse engineer, decompile or disassemble the software, or attempt to do so, except and only to the extent required by third party licensing terms governing use of certain open source components that may be included with the software; - - * remove, minimize, block or modify any notices of Microsoft or its suppliers in the software; - - * use the software in any way that is against the law; or - - * share, publish, rent or lease the software, or provide the software as a stand-alone offering for others to use. - -8. UPDATES. The software may periodically check for updates and download and install them for you. You may obtain updates only from Microsoft or authorized sources. Microsoft may need to update your system to provide you with updates. You agree to receive these automatic updates without any additional notice. Updates may not include or support all existing software features, services, or peripheral devices. - -9. EXPORT RESTRICTIONS. You must comply with all domestic and international export laws and regulations that apply to the software, which include restrictions on destinations, end users and end use. For further information on export restrictions, visit (aka.ms/exporting). - -10. SUPPORT SERVICES. Because the software is “as is,” we may not provide support services for it. - -11. ENTIRE AGREEMENT. This agreement, and the terms for supplements, updates, Internet-based services and support services that you use, are the entire agreement for the software and support services. - -12. APPLICABLE LAW. If you acquired the software in the United States, Washington State law applies to interpretation of and claims for breach of this agreement, and the laws of the state where you live apply to all other claims. If you acquired the software in any other country, its laws apply. - -13. CONSUMER RIGHTS; REGIONAL VARIATIONS. This agreement describes certain legal rights. You may have other rights, including consumer rights, under the laws of your state or country. Separate and apart from your relationship with Microsoft, you may also have rights with respect to the party from which you acquired the software. This agreement does not change those other rights if the laws of your state or country do not permit it to do so. For example, if you acquired the software in one of the below regions, or mandatory country law applies, then the following provisions apply to you: - - a. Australia. You have statutory guarantees under the Australian Consumer Law and nothing in this agreement is intended to affect those rights. - - b. Canada. If you acquired the software in Canada, you may stop receiving updates by turning off the automatic update feature, disconnecting your device from the Internet (if and when you re-connect to the Internet, however, the software will resume checking for and installing updates), or uninstalling the software. The product documentation, if any, may also specify how to turn off updates for your specific device or software. - - c. Germany and Austria. - - (i) Warranty. The properly licensed software will perform substantially as described in any Microsoft materials that accompany the software. However, Microsoft gives no contractual guarantee in relation to the licensed software. - - (ii) Limitation of Liability. In case of intentional conduct, gross negligence, claims based on the Product Liability Act, as well as, in case of death or personal or physical injury, Microsoft is liable according to the statutory law. - - Subject to the foregoing clause (ii), Microsoft will only be liable for slight negligence if Microsoft is in breach of such material contractual obligations, the fulfillment of which facilitate the due performance of this agreement, the breach of which would endanger the purpose of this agreement and the compliance with which a party may constantly trust in (so-called "cardinal obligations"). In other cases of slight negligence, Microsoft will not be liable for slight negligence. - -14. LEGAL EFFECT. This agreement describes certain legal rights. You may have other rights under the laws of your country. You may also have rights with respect to the party from whom you acquired the software. This agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so. - -15. DISCLAIMER OF WARRANTY. THE SOFTWARE IS LICENSED “AS-IS.” YOU BEAR THE RISK OF USING IT. MICROSOFT GIVES NO EXPRESS WARRANTIES, GUARANTEES OR CONDITIONS. TO THE EXTENT PERMITTED UNDER YOUR LOCAL LAWS, MICROSOFT EXCLUDES THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. - -16. LIMITATION ON AND EXCLUSION OF DAMAGES. YOU CAN RECOVER FROM MICROSOFT AND ITS SUPPLIERS ONLY DIRECT DAMAGES UP TO U.S. $5.00. YOU CANNOT RECOVER ANY OTHER DAMAGES, INCLUDING CONSEQUENTIAL, LOST PROFITS, SPECIAL, INDIRECT OR INCIDENTAL DAMAGES. - - This limitation applies to (a) anything related to the software, services, content (including code) on third party Internet sites, or third party programs; and (b) claims for breach of contract, breach of warranty, guarantee or condition, strict liability, negligence, or other tort to the extent permitted by applicable law. - - It also applies even if Microsoft knew or should have known about the possibility of the damages. The above limitation or exclusion may not apply to you because your country may not allow the exclusion or limitation of incidental, consequential or other damages. - -Please note: As the software is distributed in Quebec, Canada, some of the clauses in this agreement are provided below in French. - -Remarque : Ce logiciel étant distribué au Québec, Canada, certaines des clauses dans ce contrat sont fournies ci-dessous en français. - -EXONÉRATION DE GARANTIE. Le logiciel visé par une licence est offert « tel quel ». Toute utilisation de ce logiciel est à votre seule risque et péril. Microsoft n’accorde aucune autre garantie expresse. Vous pouvez bénéficier de droits additionnels en vertu du droit local sur la protection des consommateurs, que ce contrat ne peut modifier. La ou elles sont permises par le droit locale, les garanties implicites de qualité marchande, d’adéquation à un usage particulier et d’absence de contrefaçon sont exclues. - -LIMITATION DES DOMMAGES-INTÉRÊTS ET EXCLUSION DE RESPONSABILITÉ POUR LES DOMMAGES. Vous pouvez obtenir de Microsoft et de ses fournisseurs une indemnisation en cas de dommages directs uniquement à hauteur de 5,00 $ US. Vous ne pouvez prétendre à aucune indemnisation pour les autres dommages, y compris les dommages spéciaux, indirects ou accessoires et pertes de bénéfices. - -Cette limitation concerne : - -* tout ce qui est relié au logiciel, aux services ou au contenu (y compris le code) figurant sur des sites Internet tiers ou dans des programmes tiers ; et - -* les réclamations au titre de violation de contrat ou de garantie, ou au titre de responsabilité stricte, de négligence ou d’une autre faute dans la limite autorisée par la loi en vigueur. - -Elle s’applique également, même si Microsoft connaissait ou devrait connaître l’éventualité d’un tel dommage. Si votre pays n’autorise pas l’exclusion ou la limitation de responsabilité pour les dommages indirects, accessoires ou de quelque nature que ce soit, il se peut que la limitation ou l’exclusion ci-dessus ne s’appliquera pas à votre égard. - -EFFET JURIDIQUE. Le présent contrat décrit certains droits juridiques. Vous pourriez avoir d’autres droits prévus par les lois de votre pays. Le présent contrat ne modifie pas les droits que vous confèrent les lois de votre pays si celles-ci ne le permettent pas. - -========================================= -END OF vsls 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 - -%% winston 3.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/winston/-/winston-3.2.1.tgz) -========================================= -Copyright (c) 2010 Charlie Robbins - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -========================================= -END OF winston NOTICES AND INFORMATION - -%% winston-transport 4.3.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz) -========================================= -The MIT License (MIT) - -Copyright (c) 2015 Charlie Robbins & the contributors. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -========================================= -END OF winston-transport NOTICES AND INFORMATION - -%% wordwrap 1.0.0 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/wordwrap/-/wordwrap-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 wordwrap 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 6.2.1 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/ws/-/ws-6.2.1.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 - -%% yallist 2.1.2 NOTICES AND INFORMATION BEGIN HERE (https://registry.npmjs.org/yallist/-/yallist-2.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 yallist 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 e5766366bd53..9e7e822af1bb 100644 --- a/ThirdPartyNotices-Repository.txt +++ b/ThirdPartyNotices-Repository.txt @@ -6,16 +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) -14. less-plugin-inline-urls (https://github.com/less/less-plugin-inline-urls/) - +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 @@ -242,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 @@ -961,3 +943,92 @@ Apache 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 index 539aa1c15b60..abe46f117f5b 100644 --- a/build/.mocha-multi-reporters.config +++ b/build/.mocha-multi-reporters.config @@ -1,3 +1,3 @@ { - "reporterEnabled": "spec,mocha-junit-reporter" + "reporterEnabled": "./build/ci/scripts/spec_with_pid,mocha-junit-reporter" } diff --git a/build/.mocha.functional.json b/build/.mocha.functional.json 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 c21f226b38fa..000000000000 --- a/build/.mocha.functional.opts +++ /dev/null @@ -1,10 +0,0 @@ -./out/test/**/*.functional.test.js ---require=out/test/unittests.js ---exclude=out/**/*.jsx ---ui=tdd ---recursive ---colors ---exit ---timeout=120000 ---reporter mocha-multi-reporters ---reporter-options configFile=build/.mocha-multi-reporters.config 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.js.opts b/build/.mocha.unittests.js.opts deleted file mode 100644 index 314f85a1d3a9..000000000000 --- a/build/.mocha.unittests.js.opts +++ /dev/null @@ -1,8 +0,0 @@ ---require source-map-support/register ---require out/test/unittests.js ---reporter mocha-multi-reporters ---reporter-options configFile=build/.mocha-multi-reporters.config ---ui tdd ---recursive ---colors -./out/test/**/*.unit.test.js \ No newline at end of file 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 1583fcaaa727..000000000000 --- a/build/.mocha.unittests.opts +++ /dev/null @@ -1,7 +0,0 @@ -./out/test/**/*.unit.test.js ---require out/test/unittests.js ---reporter mocha-multi-reporters ---reporter-options configFile=build/.mocha-multi-reporters.config ---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/.mocha.unittests.ts.opts b/build/.mocha.unittests.ts.opts deleted file mode 100644 index f6672aed1db6..000000000000 --- a/build/.mocha.unittests.ts.opts +++ /dev/null @@ -1,8 +0,0 @@ ---require ts-node/register ---require out/test/unittests.js ---reporter mocha-multi-reporters ---reporter-options configFile=build/.mocha-multi-reporters.config ---ui tdd ---recursive ---colors -./src/test/**/*.unit.test.ts \ No newline at end of file diff --git a/build/.nycrc b/build/.nycrc index e9540ef130f2..b92a4f36785d 100644 --- a/build/.nycrc +++ b/build/.nycrc @@ -2,8 +2,8 @@ "extends": "@istanbuljs/nyc-config-typescript", "all": true, "include": [ - "src/client/**/*.ts", "src/test/**/*.js", - "src/datascience-ui/**/*.ts", "src/datascience-ui/**/*.js" + "src/client/**/*.ts", "out/client/**/*.js" ], - "exclude": ["src/test/**/*.ts", "src/test/**/*.js"] -} \ No newline at end of file + "exclude": ["src/test/**/*.ts", "out/test/**/*.js"], + "exclude-node-modules": true +} diff --git a/build/azure-pipeline.pre-release.yml b/build/azure-pipeline.pre-release.yml new file mode 100644 index 000000000000..e7159618d3ae --- /dev/null +++ b/build/azure-pipeline.pre-release.yml @@ -0,0 +1,158 @@ +# Run on a schedule +trigger: none +pr: none + +schedules: + - cron: '0 10 * * 1-5' # 10AM UTC (2AM PDT) MON-FRI (VS Code Pre-release builds at 9PM PDT) + displayName: Nightly Pre-Release Schedule + always: false # only run if there are source code changes + branches: + include: + - main + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/extension/pre-release.yml@templates + parameters: + publishExtension: ${{ parameters.publishExtension }} + ghCreateTag: false + standardizedVersioning: true + l10nSourcePaths: ./src/client + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + + buildSteps: + - task: NodeTool@0 + inputs: + versionSpec: '22.17.0' + displayName: Select Node version + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: python -m pip install -U pip + displayName: Upgrade pip + + - script: python -m pip install wheel nox + displayName: Install wheel and nox + + - script: npm ci + displayName: Install NPM dependencies + + - script: nox --session install_python_libs + displayName: Install Jedi, get-pip, etc + + - script: python ./build/update_package_file.py + displayName: Update telemetry in package.json + + - script: npm run addExtensionPackDependencies + displayName: Update optional extension dependencies + + - script: npx gulp prePublishBundle + displayName: Build + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 591 + buildVersionToDownload: 'latest' + branchName: 'refs/heads/main' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" + displayName: Clean up Nox + + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true diff --git a/build/azure-pipeline.stable.yml b/build/azure-pipeline.stable.yml new file mode 100644 index 000000000000..cd66613eec8d --- /dev/null +++ b/build/azure-pipeline.stable.yml @@ -0,0 +1,153 @@ +trigger: none +# branches: +# include: +# - release* +# tags: +# include: ['*'] +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: publishExtension + displayName: 🚀 Publish Extension + type: boolean + default: false + +extends: + template: azure-pipelines/extension/stable.yml@templates + parameters: + publishExtension: ${{ parameters.publishExtension }} + l10nSourcePaths: ./src/client + + buildPlatforms: + - name: Linux + vsceTarget: 'web' + - name: Linux + packageArch: arm64 + vsceTarget: linux-arm64 + - name: Linux + packageArch: arm + vsceTarget: linux-armhf + - name: Linux + packageArch: x64 + vsceTarget: linux-x64 + - name: Linux + packageArch: arm64 + vsceTarget: alpine-arm64 + - name: Linux + packageArch: x64 + vsceTarget: alpine-x64 + - name: MacOS + packageArch: arm64 + vsceTarget: darwin-arm64 + - name: MacOS + packageArch: x64 + vsceTarget: darwin-x64 + - name: Windows + packageArch: arm + vsceTarget: win32-arm64 + - name: Windows + packageArch: x64 + vsceTarget: win32-x64 + + buildSteps: + - task: NodeTool@0 + inputs: + versionSpec: '22.17.0' + displayName: Select Node version + + - task: UsePythonVersion@0 + inputs: + versionSpec: '3.9' + addToPath: true + architecture: 'x64' + displayName: Select Python version + + - script: python -m pip install -U pip + displayName: Upgrade pip + + - script: python -m pip install wheel nox + displayName: Install wheel and nox + + - script: npm ci + displayName: Install NPM dependencies + + - script: nox --session install_python_libs + displayName: Install Jedi, get-pip, etc + + - script: python ./build/update_package_file.py + displayName: Update telemetry in package.json + + - script: npm run addExtensionPackDependencies + displayName: Update optional extension dependencies + + - script: npx gulp prePublishBundle + displayName: Build + + - bash: | + mkdir -p $(Build.SourcesDirectory)/python-env-tools/bin + chmod +x $(Build.SourcesDirectory)/python-env-tools/bin + displayName: Make Directory for python-env-tool binary + + - bash: | + if [ "$(vsceTarget)" == "win32-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "win32-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-pc-windows-msvc" + elif [ "$(vsceTarget)" == "linux-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "linux-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "linux-armhf" ]; then + echo "##vso[task.setvariable variable=buildTarget]armv7-unknown-linux-gnueabihf" + elif [ "$(vsceTarget)" == "darwin-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-apple-darwin" + elif [ "$(vsceTarget)" == "darwin-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-apple-darwin" + elif [ "$(vsceTarget)" == "alpine-x64" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + elif [ "$(vsceTarget)" == "alpine-arm64" ]; then + echo "##vso[task.setvariable variable=buildTarget]aarch64-unknown-linux-gnu" + elif [ "$(vsceTarget)" == "web" ]; then + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + else + echo "##vso[task.setvariable variable=buildTarget]x86_64-unknown-linux-musl" + fi + displayName: Set buildTarget variable + + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'specific' + project: 'Monaco' + definition: 593 + buildVersionToDownload: 'latestFromBranch' + branchName: 'refs/heads/release/2026.4' + targetPath: '$(Build.SourcesDirectory)/python-env-tools/bin' + artifactName: 'bin-$(buildTarget)' + itemPattern: | + pet.exe + pet + ThirdPartyNotices.txt + + - bash: | + ls -lf ./python-env-tools/bin + chmod +x ./python-env-tools/bin/pet* + ls -lf ./python-env-tools/bin + displayName: Set chmod for pet binary + + - script: python -c "import shutil; shutil.rmtree('.nox', ignore_errors=True)" + displayName: Clean up Nox + tsa: + config: + areaPath: 'Visual Studio Code Python Extensions' + serviceTreeID: '6e6194bc-7baa-4486-86d0-9f5419626d46' + enabled: true + apiScanDependentPipelineId: '593' # python-environment-tools + apiScanSoftwareVersion: '2024' diff --git a/build/azure-pipelines/pipeline.yml b/build/azure-pipelines/pipeline.yml new file mode 100644 index 000000000000..0796e38ca598 --- /dev/null +++ b/build/azure-pipelines/pipeline.yml @@ -0,0 +1,58 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +name: $(Date:yyyyMMdd)$(Rev:.r) + +trigger: none + +pr: none + +resources: + repositories: + - repository: templates + type: github + name: microsoft/vscode-engineering + ref: main + endpoint: Monaco + +parameters: + - name: quality + displayName: Quality + type: string + default: latest + values: + - latest + - next + - name: publishPythonApi + displayName: 🚀 Publish pythonExtensionApi + type: boolean + default: false + +extends: + template: azure-pipelines/npm-package/pipeline.yml@templates + parameters: + npmPackages: + - name: pythonExtensionApi + testPlatforms: + - name: Linux + nodeVersions: + - 22.21.1 + - name: MacOS + nodeVersions: + - 22.21.1 + - name: Windows + nodeVersions: + - 22.21.1 + testSteps: + - template: /build/azure-pipelines/templates/test-steps.yml@self + parameters: + package: pythonExtensionApi + buildSteps: + - template: /build/azure-pipelines/templates/pack-steps.yml@self + parameters: + package: pythonExtensionApi + ghTagPrefix: release/pythonExtensionApi/ + tag: ${{ parameters.quality }} + publishPackage: ${{ parameters.publishPythonApi }} + workingDirectory: $(Build.SourcesDirectory)/pythonExtensionApi diff --git a/build/azure-pipelines/templates/pack-steps.yml b/build/azure-pipelines/templates/pack-steps.yml new file mode 100644 index 000000000000..97037efb59ba --- /dev/null +++ b/build/azure-pipelines/templates/pack-steps.yml @@ -0,0 +1,14 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - script: npm install + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Install package dependencies diff --git a/build/azure-pipelines/templates/test-steps.yml b/build/azure-pipelines/templates/test-steps.yml new file mode 100644 index 000000000000..15eb3db6384d --- /dev/null +++ b/build/azure-pipelines/templates/test-steps.yml @@ -0,0 +1,23 @@ +############################################################################################### +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +############################################################################################### +parameters: +- name: package + type: string +- name: script + type: string + default: 'all:publish' + +steps: + - script: npm install --root-only + workingDirectory: $(Build.SourcesDirectory) + displayName: Install root dependencies + - bash: | + /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & + echo ">>> Started xvfb" + displayName: Start xvfb + condition: eq(variables['Agent.OS'], 'Linux') + - script: npm run ${{ parameters.script }} + workingDirectory: $(Build.SourcesDirectory)/${{ parameters.package }} + displayName: Verify package diff --git a/build/build-install-requirements.txt b/build/build-install-requirements.txt new file mode 100644 index 000000000000..8baaa59ded67 --- /dev/null +++ b/build/build-install-requirements.txt @@ -0,0 +1,2 @@ +# Requirements needed to run install_debugpy.py and download_get_pip.py +packaging diff --git a/build/ci/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..4f9ceefd27fb --- /dev/null +++ b/build/ci/conda_env_1.yml @@ -0,0 +1,4 @@ +name: conda_env_1 +dependencies: + - python=3.9 + - pip diff --git a/build/ci/conda_env_2.yml b/build/ci/conda_env_2.yml new file mode 100644 index 000000000000..af9d7a46ba3e --- /dev/null +++ b/build/ci/conda_env_2.yml @@ -0,0 +1,4 @@ +name: conda_env_2 +dependencies: + - python=3.9 + - pip diff --git a/build/ci/postInstall.js b/build/ci/postInstall.js deleted file mode 100644 index 2660453a1305..000000000000 --- a/build/ci/postInstall.js +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const colors = require("colors/safe"); -const fs = require("fs"); -const path = require("path"); -const constants_1 = require("../constants"); -/** - * In order to compile the extension in strict mode, one of the dependencies (@jupyterlab) has some files that - * just won't compile in strict mode. - * Unfortunately we cannot fix it by overriding their type definitions - * Note: that has been done for a few of the JupyterLabl files (see typings/index.d.ts). - * The solution is to modify the type definition file after `npm install`. - */ -function fixJupyterLabDTSFiles() { - const relativePath = path.join('node_modules', '@jupyterlab', 'coreutils', 'lib', 'settingregistry.d.ts'); - const filePath = path.join(constants_1.ExtensionRootDir, relativePath); - if (!fs.existsSync(filePath)) { - throw new Error(`Type Definition file from JupyterLab not found '${filePath}' (pvsc post install script)`); - } - const fileContents = fs.readFileSync(filePath, { encoding: 'utf8' }); - if (fileContents.indexOf('[key: string]: ISchema | undefined;') > 0) { - // tslint:disable-next-line:no-console - console.log(colors.blue(`${relativePath} file already updated (by Python VSC)`)); - return; - } - const replacedText = fileContents.replace('[key: string]: ISchema;', '[key: string]: ISchema | undefined;'); - if (fileContents === replacedText) { - throw new Error('Fix for JupyterLabl file \'settingregistry.d.ts\' failed (pvsc post install script)'); - } - fs.writeFileSync(filePath, replacedText); - // tslint:disable-next-line:no-console - console.log(colors.green(`${relativePath} file updated (by Python VSC)`)); -} -fixJupyterLabDTSFiles(); diff --git a/build/ci/postInstall.ts b/build/ci/postInstall.ts deleted file mode 100644 index 6d0fbac62791..000000000000 --- a/build/ci/postInstall.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as colors from 'colors/safe'; -import * as fs from 'fs'; -import * as path from 'path'; -import { ExtensionRootDir } from '../constants'; - -/** - * In order to compile the extension in strict mode, one of the dependencies (@jupyterlab) has some files that - * just won't compile in strict mode. - * Unfortunately we cannot fix it by overriding their type definitions - * Note: that has been done for a few of the JupyterLabl files (see typings/index.d.ts). - * The solution is to modify the type definition file after `npm install`. - */ -function fixJupyterLabDTSFiles() { - const relativePath = path.join( - 'node_modules', - '@jupyterlab', - 'coreutils', - 'lib', - 'settingregistry.d.ts' - ); - const filePath = path.join( - ExtensionRootDir, relativePath - ); - if (!fs.existsSync(filePath)) { - throw new Error(`Type Definition file from JupyterLab not found '${filePath}' (pvsc post install script)`); - } - - const fileContents = fs.readFileSync(filePath, { encoding: 'utf8' }); - if (fileContents.indexOf('[key: string]: ISchema | undefined;') > 0) { - // tslint:disable-next-line:no-console - console.log(colors.blue(`${relativePath} file already updated (by Python VSC)`)); - return; - } - const replacedText = fileContents.replace('[key: string]: ISchema;', '[key: string]: ISchema | undefined;'); - if (fileContents === replacedText) { - throw new Error('Fix for JupyterLabl file \'settingregistry.d.ts\' failed (pvsc post install script)'); - } - fs.writeFileSync(filePath, replacedText); - // tslint:disable-next-line:no-console - console.log(colors.green(`${relativePath} file updated (by Python VSC)`)); -} - -fixJupyterLabDTSFiles(); 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/build_compile_jobs.yml b/build/ci/templates/build_compile_jobs.yml deleted file mode 100644 index 77c7133efdf5..000000000000 --- a/build/ci/templates/build_compile_jobs.yml +++ /dev/null @@ -1,19 +0,0 @@ -# Overview: -# Generic jobs template to compile and build extension - -jobs: -- job: Compile - variables: - build: false - pool: - vmImage: "macos-latest" - steps: - - template: build_compile_steps.yml - -- job: Build - variables: - build: true - pool: - vmImage: "macos-latest" - steps: - - template: build_compile_steps.yml diff --git a/build/ci/templates/build_compile_steps.yml b/build/ci/templates/build_compile_steps.yml deleted file mode 100644 index c0ac5b5d2d01..000000000000 --- a/build/ci/templates/build_compile_steps.yml +++ /dev/null @@ -1,103 +0,0 @@ -# ----------------------------------------------------------------------------------------------------------------------------- -# Overview: -# ----------------------------------------------------------------------------------------------------------------------------- -# Set of steps used to compile and build the extension -# -# ----------------------------------------------------------------------------------------------------------------------------- -# Variables -# ----------------------------------------------------------------------------------------------------------------------------- -# 1. build -# Mandatory -# Possible values, `true` or `false`. -# If `true`, means we need to build the VSIX, else just compile. - -steps: - - bash: | - printenv - displayName: "Show all env vars" - condition: eq(variables['system.debug'], 'true') - - - task: NodeTool@0 - displayName: "Use Node $(NodeVersion)" - inputs: - versionSpec: $(NodeVersion) - - - task: UsePythonVersion@0 - displayName: "Use Python $(PythonVersion)" - inputs: - versionSpec: $(PythonVersion) - - - task: Npm@1 - displayName: "Use NPM $(NpmVersion)" - 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` - echo Gulp Version = `gulp --version` - condition: and(succeeded(), eq(variables['system.debug'], 'true')) - displayName: Show Dependency Versions - - - task: Gulp@0 - displayName: "Compile and check for errors" - inputs: - targets: "prePublishNonBundle" - condition: and(succeeded(), eq(variables['build'], 'false')) - - - bash: npx tslint ./src/**/*.ts{,x} - displayName: "code hygiene" - condition: and(succeeded(), eq(variables['build'], 'false')) - - - bash: | - python -m pip install -U pip - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt - failOnStderr: true - displayName: "pip install requirements" - condition: and(succeeded(), eq(variables['build'], 'true')) - - - bash: | - npm install -g vsce - npm run clean - displayName: "Install vsce & Clean" - condition: and(succeeded(), eq(variables['build'], 'true')) - - - bash: | - npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID - displayName: "Update dev Version" - condition: and(succeeded(), eq(variables['build'], 'true'), eq(variables['Build.SourceBranchName'], 'master')) - - - bash: | - npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID --updateChangelog - displayName: "Update release Version" - condition: and(succeeded(), eq(variables['build'], 'true'), eq(variables['Build.SourceBranchName'], 'release')) - - - bash: | - npm run package - displayName: "Build VSIX" - condition: and(succeeded(), eq(variables['build'], 'true')) - - - task: CopyFiles@2 - inputs: - contents: "*.vsix" - targetFolder: $(Build.ArtifactStagingDirectory) - displayName: "Copy VSIX" - condition: and(succeeded(), eq(variables['build'], 'true')) - - - task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: $(Build.ArtifactStagingDirectory) - artifactName: VSIX - displayName: "Publish VSIX to Arifacts" - condition: and(succeeded(), eq(variables['build'], 'true')) diff --git a/build/ci/templates/generate_upload_coverage.yml b/build/ci/templates/generate_upload_coverage.yml deleted file mode 100644 index 927e3206cda8..000000000000 --- a/build/ci/templates/generate_upload_coverage.yml +++ /dev/null @@ -1,39 +0,0 @@ -steps: - # Generate the coverage reports. - - bash: npm run test:cover:report - displayName: 'run test:cover:report' - condition: contains(variables['TestsToRun'], 'testUnitTests') - failOnStderr: false - - - # Publish Code Coverage Results - - task: PublishCodeCoverageResults@1 - displayName: 'Publish test:unittests coverage results' - condition: contains(variables['TestsToRun'], 'testUnitTests') - inputs: - codeCoverageTool: 'cobertura' - summaryFileLocation: "$(System.DefaultWorkingDirectory)/coverage/cobertura-coverage.xml" - reportDirectory: "$(System.DefaultWorkingDirectory)/coverage" - - - bash: cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js - displayName: 'Upload coverage to coveralls' - continueOnError: true - condition: contains(variables['TestsToRun'], 'testUnitTests') - failOnStderr: false - # Set necessary env variables for coveralls, as they don't support Azure Devops. - # Set variables based on documentation and the coveralls (npm package) source code. 😊. - env: - COVERALLS_SERVICE_JOB_ID: $(Build.BuildId) - COVERALLS_REPO_TOKEN: $(COVERALLS_REPO_TOKEN) - COVERALLS_SERVICE_NAME: $(COVERALLS_SERVICE_NAME) - COVERALLS_GIT_COMMIT: $(Build.SourceVersion) - COVERALLS_GIT_BRANCH: $(Build.SourceBranchName) - CI_PULL_REQUEST: $(System.PullRequest.PullRequestNumber) - - - bash: cat ./coverage/lcov.info | ./node_modules/.bin/codecov --pipe - displayName: 'Upload coverage to codecov' - continueOnError: true - condition: contains(variables['TestsToRun'], 'testUnitTests') - failOnStderr: false - env: - CODECOV_TOKEN: $(CODECOV_TOKEN) diff --git a/build/ci/templates/merge_upload_coverage.yml b/build/ci/templates/merge_upload_coverage.yml deleted file mode 100644 index 539cc3b561d2..000000000000 --- a/build/ci/templates/merge_upload_coverage.yml +++ /dev/null @@ -1,51 +0,0 @@ -steps: - - bash: | - printenv - displayName: "Show all env vars" - condition: eq(variables['system.debug'], 'true') - - - task: NodeTool@0 - displayName: "Use Node $(NodeVersion)" - inputs: - versionSpec: $(NodeVersion) - - - task: Npm@1 - displayName: "Use NPM $(NpmVersion)" - inputs: - command: custom - verbose: true - customCommand: "install -g npm@$(NpmVersion)" - - - task: Npm@1 - displayName: "npm ci" - inputs: - command: custom - verbose: true - customCommand: ci - - - task: DownloadBuildArtifacts@0 - inputs: - buildType: "current" - allowPartiallySucceededBuilds: true - downloadType: "Specific" - itemPattern: "**/.nyc_output/**" - downloadPath: "$(Build.SourcesDirectory)" - displayName: "Restore Coverage Info" - condition: always() - - # Now that we have downloaded artificats from `coverage-output-`, copy them - # into the root directory (they'll go under `.nyc_output/...`) - # These are the coverage output files that can be merged and then we can generate a report from them. - # This step results in downloading all individual `./nyc_output` results in coverage - # from all different test outputs. - # Running the process of generating reports, will result in generation of - # reports from all coverage data, i.e. we're basically combining all to generate a single merged report. - - bash: | - cp -r coverage-output-*/ ./ - cd ./.nyc_output - ls -dlU .*/ */ - ls - displayName: "Copy ./.nyc_output" - condition: always() - - - template: generate_upload_coverage.yml diff --git a/build/ci/templates/test_phases.yml b/build/ci/templates/test_phases.yml deleted file mode 100644 index f21a9bf0d488..000000000000 --- a/build/ci/templates/test_phases.yml +++ /dev/null @@ -1,562 +0,0 @@ -# To use this step template from a job, use the following code: -# ```yaml -# steps: -# template: path/to/this/dir/test_phases.yml -# ``` -# -# Your job using this template *must* supply these values: -# - VMImageName: '[name]' - the VM image to run the tests on. -# - TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. -# -# Your job using this template *may* supply these values: -# - NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. -# - NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. -# - PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion if not set. -# - NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion if not set. -# - SkipXvfb: [true|false] - skip initialization of xvfb prior to running system tests on Linux. False if not set -# - UploadBinary: [true|false] - upload test binaries to Azure if true. False if not set. - -## Supported `TestsToRun` values, multiples are allowed separated by commas or spaces: -# -# 'testUnitTests' -# 'pythonUnitTests' -# 'pythonInternalTools' -# 'testSingleWorkspace' -# 'testMultiWorkspace' -# 'testDebugger' -# 'testFunctional' -# 'perfomanceTests' -# 'testSmoke' -# 'venvTests' - -steps: - - # Show the complete set of environment variabes if we are in verbose mode. - - bash: | - printenv - displayName: 'Show all env vars' - condition: eq(variables['system.debug'], 'true') - - # Ensure the required node version is made available on PATH for subsequent tasks. - # This would be like using nvm to specify a version on your local machine. - - task: NodeTool@0 - displayName: 'Use Node $(NodeVersion)' - inputs: - versionSpec: $(NodeVersion) - - # Ensure the required Python version is made available on PATH for subsequent tasks. - # - # `PythonVersion` is set in the `variables` section above. - # - # You can reproduce this on your local machine via virtual envs. - # - # See the available versions on each Hosted agent here: - # https://docs.microsoft.com/en-us/azure/DevOps/pipelines/agents/hosted?view=azure-DevOps&tabs=yaml#software - # - # Example command line (windows pwsh): - # > py -m venv .venv - # > .venv\Scripts\Activate.ps1 - - task: UsePythonVersion@0 - displayName: 'Use Python $(PythonVersion)' - inputs: - versionSpec: $(PythonVersion) - - # Install the a version of python that works with sqlite3 until this bug is addressed - # https://mseng.visualstudio.com/AzureDevOps/_workitems/edit/1535830 - # - # This task will only run if variable `NeedsPythonFunctionalReqs` is true. - - bash: | - sudo apt-get install libsqlite3-dev - version=$(python -V 2>&1 | grep -Po '(?<=Python )(.+)') - wget https://www.python.org/ftp/python/$version/Python-$version.tar.xz - tar xvf Python-$version.tar.xz - cd Python-$version - ./configure --enable-loadable-sqlite-extensions --with-ensurepip=install --prefix=$HOME/py-$version - make - sudo make install - sudo chmod -R 777 $HOME/py-$version - export PATH=$HOME/py-$version/bin:$PATH - sudo ln -s $HOME/py-$version/bin/python3 $HOME/py-$version/bin/python - echo '##vso[task.prependpath]'$HOME/py-$version/bin - displayName: 'Setup python to run with sqlite on 3.*' - condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Linux'), not(eq(variables['PythonVersion'], '2.7'))) - - # Ensure that npm is upgraded to the necessary version specified in `variables` above. - # Example command line (windows pwsh): - # > npm install -g npm@latest - - task: Npm@1 - displayName: 'Use NPM $(NpmVersion)' - inputs: - command: custom - verbose: true - customCommand: 'install -g npm@$(NpmVersion)' - - # On Mac, the command `node` doesn't always point to the current node version. - - script: | - export NODE_PATH=`which node` - echo $NODE_PATH - echo '##vso[task.setvariable variable=NODE_PATH]'$NODE_PATH - displayName: "Setup NODE_PATH for extension" - condition: and(succeeded(), eq(variables['agent.os'], 'Darwin')) - - # Install node_modules. - # Example command line (windows pwsh): - # > npm ci - - task: Npm@1 - displayName: 'npm ci' - inputs: - command: custom - verbose: true - customCommand: ci - - # Show all versions installed/available on PATH if in verbose mode. - # Example command line (windows pwsh): - # > Write-Host Node ver: $(& node -v) NPM Ver: $(& npm -v) Python ver: $(& python --version)" - - bash: | - echo AVAILABLE DEPENDENCY VERSIONS - echo Node Version = `node -v` - echo NPM Version = `npm -v` - echo Python Version = `python --version` - echo Gulp Version = `gulp --version` - condition: and(succeeded(), eq(variables['system.debug'], 'true')) - displayName: Show Dependency Versions - - # Run the `prePublishNonBundle` gulp task to build the binaries we will be testing. - # This produces the .js files required into the out/ folder. - # Example command line (windows pwsh): - # > gulp prePublishNonBundle - - task: Gulp@0 - displayName: 'gulp prePublishNonBundle' - inputs: - targets: 'prePublishNonBundle' - - # Run the typescript unit tests. - # - # This will only run if the string 'testUnitTests' exists in variable `TestsToRun` - # - # Example command line (windows pwsh): - # > npm run test:unittests:cover - - bash: | - npm run test:unittests:cover - displayName: 'run test:unittests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testUnitTests')) - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish test:unittests results' - condition: contains(variables['TestsToRun'], 'testUnitTests') - inputs: - testResultsFiles: '$(MOCHA_FILE)' - testRunTitle: 'unittests-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'UnitTests' - - - task: CopyFiles@2 - inputs: - sourceFolder: "$(Build.SourcesDirectory)/.nyc_output" - targetFolder: "$(Build.ArtifactStagingDirectory)/nyc/.nyc_output" - displayName: "Copy nyc_output to publish as artificat" - condition: contains(variables['TestsToRun'], 'testUnitTests') - - # Upload Code Coverage Results (to be merged later). - - task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: "$(Build.ArtifactStagingDirectory)/nyc" - artifactName: 'coverage-output-$(Agent.Os)' - condition: contains(variables['TestsToRun'], 'testUnitTests') - - - template: generate_upload_coverage.yml - - # Install the requirements for the Python or the system tests. This includes the supporting libs that - # we ship in our extension such as PTVSD and Jedi. - # - # This task will only run if variable `NeedsPythonTestReqs` is true. - # - # Example command line (windows pwsh): - # > python -m pip install -m -U pip - # > python -m pip install --upgrade -r build/test-requirements.txt - # > python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt - - bash: | - python -m pip install -U pip - python -m pip install --upgrade -r build/test-requirements.txt - python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt - displayName: 'pip install system test requirements' - condition: and(succeeded(), eq(variables['NeedsPythonTestReqs'], 'true')) - - # Install the additional sqlite requirements - # - # This task will only run if variable `NeedsPythonFunctionalReqs` is true. - - bash: | - sudo apt-get install libsqlite3-dev - python -m pip install pysqlite - displayName: 'Setup python to run with sqlite on 2.7' - condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true'), eq(variables['Agent.Os'], 'Linux'), eq(variables['PythonVersion'], '2.7')) - - # Install the requirements for functional tests. - # - # This task will only run if variable `NeedsPythonFunctionalReqs` is true. - # - # Example command line (windows pwsh): - # > python -m pip install numpy - # > python -m pip install --upgrade -r build/functional-test-requirements.txt - # > python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt - - bash: | - python -m pip install -U pip - python -m pip install numpy - python -m pip install --upgrade -r ./build/functional-test-requirements.txt - displayName: 'pip install functional requirements' - condition: and(succeeded(), eq(variables['NeedsPythonFunctionalReqs'], 'true')) - - # Install the requirements for ipython tests. - # - # This task will only run if variable `NeedsIPythonReqs` is true. - # - # Example command line (windows pwsh): - # > python -m pip install numpy - # > python -m pip install --upgrade -r build/ipython-test-requirements.txt - # > python -m pip --disable-pip-version-check install -t ./pythonFiles/lib/python --no-cache-dir --implementation py --no-deps --upgrade -r requirements.txt - - bash: | - python -m pip install -U pip - python -m pip install numpy - python -m pip install --upgrade -r ./build/ipython-test-requirements.txt - displayName: 'pip install ipython requirements' - condition: and(succeeded(), eq(variables['NeedsIPythonReqs'], 'true')) - - # Run the Python unit tests in our codebase. Produces a JUnit-style log file that - # will be uploaded after all tests are complete. - # - # This task only runs if the string 'pythonUnitTests' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > python -m pip install -m -U pip - # > python -m pip install -U -r build/test-requirements.txt - # > python pythonFiles/tests/run_all.py --color=yes --junit-xml=python-tests-junit.xml - - bash: | - python pythonFiles/tests/run_all.py --color=yes --junit-xml=$COMMON_TESTRESULTSDIRECTORY/python-tests-junit.xml - displayName: 'Python unittests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonUnitTests')) - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish Python unittests results' - condition: contains(variables['TestsToRun'], 'pythonUnitTests') - inputs: - testResultsFiles: 'python-tests-junit.xml' - searchFolder: '$(Common.TestResultsDirectory)' - testRunTitle: 'pythonUnitTests-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'UnitTests' - - # Run the Python IPython tests in our codebase. Produces a JUnit-style log file that - # will be uploaded after all tests are complete. - # - # This task only runs if the string 'pythonIPythonTests' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > python -m pip install -m -U pip - # > python -m pip install -U -r build/test-requirements.txt - # > python pythonFiles/tests/run_all.py --color=yes --junit-xml=python-tests-junit.xml - - bash: | - python -m IPython pythonFiles/tests/run_all.py -- --color=yes --junit-xml=$COMMON_TESTRESULTSDIRECTORY/ipython-tests-junit.xml - displayName: 'Python ipython tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonIPythonTests')) - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish IPython test results' - condition: contains(variables['TestsToRun'], 'pythonIPythonTests') - inputs: - testResultsFiles: 'ipython-tests-junit.xml' - searchFolder: '$(Common.TestResultsDirectory)' - testRunTitle: 'pythonIPythonTests-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'UnitTests' - - # Run the News tool tests. - # - # This task only runs if the string 'pythonInternalTools' exists in variable `TestsToRun` - # - # Example command line (windows pwsh): - # > python -m pip install -U -r news/requirements.txt - # > python -m pytest tpn --color=yes --junit-xml=python-news-junit.xml - - script: | - python -m pip install --upgrade -r news/requirements.txt - python -m pytest news --color=yes --junit-xml=$COMMON_TESTRESULTSDIRECTORY/python-news-junit.xml - displayName: 'Run Python tests for news' - condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonInternalTools')) - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish Python tests for news results' - condition: contains(variables['TestsToRun'], 'pythonInternalTools') - inputs: - testResultsFiles: 'python-news-junit.xml' - searchFolder: '$(Common.TestResultsDirectory)' - testRunTitle: 'news-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'UnitTests' - - # Run the TPN tool tests. - # - # This task only runs if the string 'pythonUnitTests' exists in variable `TestsToRun` - # - # Example command line (windows pwsh): - # > python -m pip install -U -r tpn/requirements.txt - # > python -m pytest tpn --color=yes --junit-xml=python-tpn-junit.xml - - script: | - python -m pip install --upgrade -r tpn/requirements.txt - python -m pytest tpn --color=yes --junit-xml=$COMMON_TESTRESULTSDIRECTORY/python-tpn-junit.xml - displayName: 'Run Python tests for TPN tool' - condition: and(succeeded(), contains(variables['TestsToRun'], 'pythonInternalTools')) - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish Python tests for TPN tool results' - condition: contains(variables['TestsToRun'], 'pythonInternalTools') - inputs: - testResultsFiles: 'python-tpn-junit.xml' - searchFolder: '$(Common.TestResultsDirectory)' - testRunTitle: 'tpn-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'UnitTests' - - # Start the X virtual frame buffer (X-windows in memory only) on Linux. Linux VMs do not - # provide a desktop so VS Code cannot properly launch there. To get around this we use the - # xvfb service to emulate a desktop instead. See - # https://code.visualstudio.com/api/working-with-extensions/continuous-integration#azure-pipelines - # - # This task will only run if we are running on Linux and variable SkipXvfb is false. - # - # Example command line (windows pwsh): N/A - - bash: | - set -e - /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & - disown -ar - displayName: 'Start xvfb' - condition: and(succeeded(), eq(variables['Agent.Os'], 'Linux'), not(variables['SkipXvfb'])) - - # Venv tests: Prepare the various virtual environments and record their details into the - # JSON file that venvTests require to run. - # - # This task only runs if the string 'venvTests' exists in variable 'TestsToRun' - # - # This task has a bunch of steps, all of which are to fill the `EnvPath` struct found in - # the file: - # `src/test/common/terminals/environmentActionProviders/terminalActivation.testvirtualenvs.ts` - # - # Example command line (windows pwsh): - # // This is done in powershell. Copy/paste the code below. - - pwsh: | - # venv/bin or venv\\Scripts (windows)? - $environmentExecutableFolder = 'bin' - if ($Env:AGENT_OS -match '.*Windows.*') { - $environmentExecutableFolder = 'Scripts' - } - - # pipenv - python -m pip install pipenv - python -m pipenv run python build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) pipenvPath - - # venv - # what happens when running under Python 2.7? - python -m venv .venv - & ".venv/$environmentExecutableFolder/python" ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) venvPath - - # virtualenv - python -m pip install virtualenv - python -m virtualenv .virtualenv - & ".virtualenv/$environmentExecutableFolder/python" ./build/ci/addEnvPath.py $(PYTHON_VIRTUAL_ENVS_LOCATION) virtualEnvPath - - # conda - if( '$(Agent.Os)' -match '.*Windows.*' ){ - $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath python - } else{ - $condaPythonPath = Join-Path -Path $Env:CONDA -ChildPath $environmentExecutableFolder | Join-Path -ChildPath python - $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 - - # Set the TEST_FILES_SUFFIX - Write-Host '##vso[task.setvariable variable=TEST_FILES_SUFFIX;]testvirtualenvs' - - displayName: 'Prepare Venv-Test Environment' - condition: and(succeeded(), contains(variables['TestsToRun'], 'venvTests')) - - # Run the virtual environment based tests. - # This set of tests is simply using the `testSingleWorkspace` set of tests, but - # with the environment variable `TEST_FILES_SUFFIX` set to `testvirtualenvs`, which - # got set in the Prepare Venv-Test Environmant task above. - # **Note**: Azure DevOps tasks set environment variables via a specially formatted - # string sent to stdout. - # - # This task only runs if the string 'venvTests' exists in variable 'TestsToRun' - # - # Example command line (windows pwsh): - # > $Env:TEST_FILES_SUFFIX=testvirtualenvs - # > npm run testSingleWorkspace - - script: | - cat $PYTHON_VIRTUAL_ENVS_LOCATION - - npm run testSingleWorkspace - - displayName: 'Run Venv-Tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'venvTests')) - env: - DISPLAY: :10 - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish Venv-Tests results' - condition: contains(variables['TestsToRun'], 'venvTests') - inputs: - testResultsFiles: '$(MOCHA_FILE)' - testRunTitle: 'venvTest-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'SystemTests' - - # Set the CI_PYTHON_PATH variable that forces VS Code system tests to use - # the specified Python interpreter. - # - # This is how to set an environment variable in the Azure DevOps pipeline, write - # a specially formatted string to stdout. For details, please see - # https://docs.microsoft.com/en-us/azure/devops/pipelines/process/variables?view=azure-devops&tabs=yaml%2Cbatch#set-in-script - # - # Example command line (windows pwsd): - # > $Env:CI_PYTHON_PATH=(& python -c 'import sys;print(sys.executable)') - - script: | - python -c "from __future__ import print_function;import sys;print('##vso[task.setvariable variable=CI_PYTHON_PATH;]{}'.format(sys.executable))" - displayName: 'Set CI_PYTHON_PATH' - - # Run the functional tests. - # - # This task only runs if the string 'testFunctional' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run test:functional - - script: | - npm run test:functional - displayName: 'Run functional tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testFunctional')) - env: - DISPLAY: :10 - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish functional tests results' - condition: contains(variables['TestsToRun'], 'testFunctional') - inputs: - testResultsFiles: '$(MOCHA_FILE)' - testRunTitle: 'functional-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'FunctionalTests' - - # Run the single workspace tests. - # - # This task only runs if the string 'testSingleWorkspace' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run testSingleWorkspace - - script: | - npm run testSingleWorkspace - displayName: 'Run single workspace tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testSingleWorkspace')) - env: - DISPLAY: :10 - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish single workspace tests results' - condition: contains(variables['TestsToRun'], 'testSingleWorkspace') - inputs: - testResultsFiles: '$(MOCHA_FILE)' - testRunTitle: 'singleWorkspace-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'SystemTests' - - # Run the multi-workspace tests. - # - # This task only runs if the string 'testMultiWorkspace' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run testMultiWorkspace - - script: | - npm run testMultiWorkspace - displayName: 'Run multi-workspace tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testMultiWorkspace')) - env: - DISPLAY: :10 - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish multi-workspace tests results' - condition: contains(variables['TestsToRun'], 'testMultiWorkspace') - inputs: - testResultsFiles: '$(MOCHA_FILE)' - testRunTitle: 'multiWorkspace-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'SystemTests' - - # Run the debugger integration tests. - # - # This task only runs if the string 'testDebugger' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run testDebugger - - script: | - npm run testDebugger - displayName: 'Run debugger tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testDebugger')) - env: - DISPLAY: :10 - - # Upload the test results to Azure DevOps to facilitate test reporting in their UX. - - task: PublishTestResults@2 - displayName: 'Publish debugger tests results' - condition: contains(variables['TestsToRun'], 'testDebugger') - inputs: - testResultsFiles: '$(MOCHA_FILE)' - testRunTitle: 'debugger-$(Agent.Os)-Py$(pythonVersion)' - buildPlatform: '$(Agent.Os)-Py$(pythonVersion)' - buildConfiguration: 'SystemTests' - - # Run the performance tests. - # - # This task only runs if the string 'testPerformance' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run testPerformance - - script: | - npm run testPerformance - displayName: 'Run Performance Tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testPerformance')) - env: - DISPLAY: :10 - - # Run the smoke tests. - # - # This task only runs if the string 'testSmoke' exists in variable `TestsToRun`. - # - # Example command line (windows pwsh): - # > npm run clean - # > npm run updateBuildNumber -- --buildNumber 0.0.0-local - # > npm run package - # > npx gulp clean:cleanExceptTests - # > npm run testSmoke - - bash: | - npm install -g vsce - npm run clean - npm run updateBuildNumber -- --buildNumber $BUILD_BUILDID - npm run package - npx gulp clean:cleanExceptTests - npm run testSmoke - displayName: 'Run Smoke Tests' - condition: and(succeeded(), contains(variables['TestsToRun'], 'testSmoke')) - env: - DISPLAY: :10 - - - task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: $(Build.ArtifactStagingDirectory) - artifactName: $(Agent.JobName) - condition: always() diff --git a/build/ci/templates/uitest_jobs.yml b/build/ci/templates/uitest_jobs.yml deleted file mode 100644 index c6b98e2e9c22..000000000000 --- a/build/ci/templates/uitest_jobs.yml +++ /dev/null @@ -1,157 +0,0 @@ -# ----------------------------------------------------------------------------------------------------------------------------- -# Overview: -# ----------------------------------------------------------------------------------------------------------------------------- -# This template builds a dynamic list of Jobs for UI Tests. -# Basically, given a set of environments (OS, Python Version, VSC Version), we generate a -# list of jobs for each permutation. -# I.e. we generate permutations of Jobs for each of the following: -# - OS: Windows, Linux, Mac -# - Python Version: 2.7, 3.5, 3.6, 3.7 -# - VSC Version: Stable, Insiders -# Using this approach, we can add tests for PipEnv, PyEnv, Conda, etc and ensure we test in all possible combinations. -# When using this template we can also, ignore testing against specific permutations by -# excluding OS, Python Version or VSC Version using the parameters ignorePythonVersions, ignoreOperatingSystems & vscodeChannels -# -# ----------------------------------------------------------------------------------------------------------------------------- -# Parameters -# ----------------------------------------------------------------------------------------------------------------------------- -# 1. jobs -# Mandatory. -# Contains a list of all tests that needs to be run. -# -# Sample: -# ``` -# - template: templates/uitest_jobs.yml -# parameters: -# jobs: -# - test: "Smoke" -# tags: "--tags=@smoke" -# - test: "Test" -# tags: "--tags=@test" -# - test: "Terminal" -# tags: "--tags=@terminal" -# ``` -# Based on this sample, we're running 3 tests with the names `Smoke`, `Test`, and `Terminal`. -# The tags inside each test contains the arguments that needs to be passd into behave. -# I.e. we're only testing BDD tests that contain the tag `@smoke`, `@test` & `@terminal` (as separate jobs). -# Please pass in just the `tags` arguments. -# Multiple tag values can be passed in as follows: -# tags: "--tags=@debug --tags=@remote" -# More information on --tags argument for behave can be found here: -# * https://behave.readthedocs.io/en/latest/tutorial.html#controlling-things-with-tags -# * https://behave.readthedocs.io/en/latest/tag_expressions.html -# 2. ignorePythonVersions -# Comma delimited list of Python versions not to be tested against. -# E.g. = 3.7,3.6 -# Possible values 3.7, 3.6, 3.5, 2.7 -# Any OS provided in this string will not be tested against. -# -# Sample: -# ``` -# parameters: -# vscodeChannels: ['stable'] -# jobs: -# - test: "Smoke" -# tags: "--tags=@smoke" -# ignorePythonVersions: "3.6,3.5" -# ``` -# Based on this sample, we're running 1 test with the name `Smoke`. -# We're only test BDD tests that contain the tag `@smoke`. -# And we're ignoring Python Versions 3.6 and 3.5. -# 3. ignoreOperatingSystems -# Comma delimited list of OS not to be tested against. -# E.g. = win, linux -# Possible values = mac, win linux -# 4. vscodeChannels -# Comma delimited list of VSC Versions. -# Defaults include = `stable`,`insider` -# If changed to `stable`, then only `stable` is tested. -# -# Sample: -# ``` -# parameters: -# vscodeChannels: ['stable'] -# jobs: -# - test: "Smoke" -# tags: "--tags=@smoke" -# ignorePythonVersions: "3.6,3.5" -# ``` -# Based on this sample, we're running 1 test with the name `Smoke`. -# We're only testing against the `stable` version of VSC. -# 5. pythonVersions -# Do not pass (these are internal variables). -# Defines the versions of Pythons versions we run tests against. -# We use this to build a list of python versions as a list instead of having to hardcode them. -# This way we just use a simple for loop and run jobs for each OS and each Python version. -# Note: The display name MUST not contain spaces, hyphens and periods or other similar funky characters. -# Use [A-Za-z_], else Azure starts playing up (recommended by Azure Devops via support). - -parameters: - jobs: [] - ignorePythonVersions: "" - ignoreOperatingSystems: "" - vscodeChannels: ['stable', 'insider'] - pythonVersions: [ - { - "version": "3.7", - "displayName": "37", - "excludeTags": "--tags=~@python3.6 --tags=~@python3.5 --tags=~@python2" - }, - { - "version": "3.6", - "displayName": "36", - "excludeTags": "--tags=~@python3.7 --tags=~@python3.5 --tags=~@python2" - }, - { - "version": "3.5", - "displayName": "35", - "excludeTags": "--tags=~@python3.7 --tags=~@python3.6 --tags=~@python2" - }, - { - "version": "2.7", - "displayName": "27", - "excludeTags": "--tags=~@python3.7 --tags=~@python3.5 --tags=~@python3" - } - ] - - -jobs: -- job: UITest - dependsOn: - - Compile - - Build - # Remember, some tests can take easily an hour (the `tests` features take just around 1 hour). - timeoutInMinutes: 90 - # Build our matrix (permutations of all environments & tests). - strategy: - matrix: - ${{ each channel in parameters.vscodeChannels }}: - ${{ each job in parameters.jobs }}: - ${{ each py in parameters.pythonVersions }}: - ${{ if not(contains(coalesce(job.ignorePythonVersions, ''), py.version)) }}: - ${{ if not(contains(coalesce(job.ignoreOperatingSystems, ''), 'mac')) }}: - ${{ format('Mac{2}{0}{1}', py.displayName, job.test, channel) }}: - PythonVersion: ${{ py.version }} - VMImageName: "macos-latest" - VSCodeChannel: ${{ channel }} - Tags: ${{ format('{0} {1} --tags=~@win --tags=~@linux', job.tags, py.excludeTags) }} - - ${{ if not(contains(coalesce(job.ignoreOperatingSystems, ''), 'win')) }}: - ${{ format('Win{2}{0}{1}', py.displayName, job.test, channel) }}: - PythonVersion: ${{ py.version }} - VSCodeChannel: ${{ channel }} - VMImageName: "vs2017-win2016" - Tags: ${{ format('{0} {1} --tags=~@mac --tags=~@linux', job.tags, py.excludeTags) }} - - ${{ if not(contains(coalesce(job.ignoreOperatingSystems, ''), 'linux')) }}: - ${{ format('Linux{2}{0}{1}', py.displayName, job.test, channel) }}: - PythonVersion: ${{ py.version }} - VSCodeChannel: ${{ channel }} - VMImageName: "ubuntu-latest" - Tags: ${{ format('{0} {1} --tags=~@mac --tags=~@win', job.tags, py.excludeTags) }} - - pool: - vmImage: $(VMImageName) - - steps: - - template: uitest_phases.yml diff --git a/build/ci/templates/uitest_phases.yml b/build/ci/templates/uitest_phases.yml deleted file mode 100644 index 6099c94da19a..000000000000 --- a/build/ci/templates/uitest_phases.yml +++ /dev/null @@ -1,245 +0,0 @@ -# ----------------------------------------------------------------------------------------------------------------------------- -# Overview: -# ----------------------------------------------------------------------------------------------------------------------------- -# Steps to be executed as part of the UI tests. -# 1. Show all env vars - Logging (display environment variabels). -# 2. Use Node - Specify and use node version. -# 3. Setup Python ? for extension - Version of Python to be used for testing in Extension. -# 4. Setup CI_PYTHON_PATH for extension - Export Env variable that'll be used by a seprate step. -# Note: This env variable is NOT used by any Code (used purely in the Azdo step further below). -# 5. Use Python 2.7 - Ensure minimum version of Python (2.7) is available on CI for Python Extension. -# 6. Use Python 3.7 - Ensure latest version of Python (3.7) is available on CI for Python Extension. -# 7. npm ci - Install npm packages for some JS scripts used in testing (NOT for extension). -# 8. Show Dependency Versions - Logging. -# 9. Start xvfb - Start in-memory display server (for launching VSC). -# 10. Restore VSIX - VSIX has been built in another Job, download that from artificats. -# 11. Copy VSIX - Copy the VSIX into root directory (test suite expects it to be in root - default setup). -# 12. Setup pyperclicp dependency - We use pyperclip to copy text into clipboard buffer (see where this is used in code for info). -# 13. Download & install UI Test dependencies - Download & Install everything requried for the UI tests. -# 14. Run Tests - Launch the UI tests in Python -# 15. Copy Reports -# 16. Copy Screenshots -# 17. Copy Extension Logs -# 18. Copy VSC Logs -# 19. Upload Reports - Upload as artifacts to Azure Devops -# 20. Test Results - Upload test results to Azure Devops -# ----------------------------------------------------------------------------------------------------------------------------- -# Variables: -# ----------------------------------------------------------------------------------------------------------------------------- -# 1. VSCodeChannel -# Mandatory. -# VS Code channel to be tested against. `stable` or `insider`. -# 2. Tags -# Mandatory. -# Contain the `--tags=....` arguments to be passed into behave to exclude certain tags. -# Multiple tags can be passed as `--tags=@smoke --tags=~@ignore1 --tags=~@another --tags=~@andMore` -# More information on --tags argument for behave can be found here: -# * https://behave.readthedocs.io/en/latest/tutorial.html#controlling-things-with-tags -# * https://behave.readthedocs.io/en/latest/tag_expressions.html -# 3. PythonVersion -# Python version to be used. -# 4. VMImageName -# VM Image to be used (standard Azure Devops variable). - - -steps: - - bash: | - printenv - displayName: "Show all env vars" - condition: eq(variables['system.debug'], 'true') - - - task: NodeTool@0 - displayName: "Use Node $(NodeVersion)" - inputs: - versionSpec: $(NodeVersion) - - - task: UsePythonVersion@0 - displayName: "Setup Python $(PythonVersion) for extension" - inputs: - versionSpec: $(PythonVersion) - - # Conda - - bash: echo "##vso[task.prependpath]$CONDA/bin" - displayName: Add conda to PATH - condition: and(succeeded(), not(eq(variables['agent.os'], 'Windows_NT'))) - - - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" - displayName: Add conda to PATH - condition: and(succeeded(), eq(variables['agent.os'], 'Windows_NT')) - - # On Hosted macOS, the agent user doesn't have ownership of Miniconda's installation directory/ - # We need to take ownership if we want to update conda or install packages globally - - bash: sudo chown -R $USER $CONDA - displayName: Take ownership of conda installation - condition: and(succeeded(), eq(variables['agent.os'], 'Darwin')) - - - script: | - export CI_PYTHON_PATH=`which python` - echo '##vso[task.setvariable variable=CI_PYTHON_PATH]'$CI_PYTHON_PATH - displayName: "Setup CI_PYTHON_PATH for extension" - condition: and(succeeded(), not(eq(variables['agent.os'], 'Windows_NT'))) - - - powershell: | - $CI_PYTHON_PATH = (get-command python).path - Write-Host "##vso[task.setvariable variable=CI_PYTHON_PATH]$CI_PYTHON_PATH" - Write-Host $CI_PYTHON_PATH - displayName: "Setup CI_PYTHON_PATH for extension" - condition: and(succeeded(), eq(variables['agent.os'], 'Windows_NT')) - - # Some tests need to have both 2.7 & 3.7 available. - # Also, use Python 3.7 to run the scripts that drive the ui tests. - # Order matters, currently active python version will be used to drive tests. - # Hence ensure 3.7 is setup last. - - task: UsePythonVersion@0 - displayName: "Use Python 2.7" - inputs: - versionSpec: 2.7 - - - task: UsePythonVersion@0 - displayName: "Use Python 3.7 (to drive tests)" - inputs: - versionSpec: 3.7 - - - task: Npm@1 - displayName: "Use NPM $(NpmVersion)" - 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` - echo Gulp Version = `gulp --version` - condition: and(succeeded(), eq(variables['system.debug'], 'true')) - displayName: Show Dependency Versions - - # https://code.visualstudio.com/api/working-with-extensions/continuous-integration#azure-pipelines - - bash: | - set -e - /usr/bin/Xvfb :10 -ac >> /tmp/Xvfb.out 2>&1 & - disown -ar - displayName: "Start xvfb" - condition: and(succeeded(), eq(variables['Agent.Os'], 'Linux'), not(variables['SkipXvfb'])) - - - task: DownloadBuildArtifacts@0 - inputs: - buildType: "current" - artifactName: "VSIX" - downloadPath: "$(Build.SourcesDirectory)" - displayName: "Restore VSIX" - condition: succeeded() - - - task: CopyFiles@2 - inputs: - sourceFolder: "$(Build.SourcesDirectory)/VSIX" - targetFolder: $(Build.SourcesDirectory) - displayName: "Copy VSIX" - condition: succeeded() - - # pyperclip needs more dependencies installed on Linux - # See https://github.com/asweigart/pyperclip/blob/master/docs/introduction.rst - - bash: sudo apt-get install xsel - displayName: "Setup pyperclip dependency" - condition: and(succeeded(), eq(variables['Agent.Os'], 'Linux')) - - # Run the UI Tests. - - bash: | - python -m pip install -U pip - python -m pip install --upgrade -r ./uitests/requirements.txt - python uitests download --channel=$(VSCodeChannel) - npm install -g vsce - python uitests install --channel=$(VSCodeChannel) - env: - DISPLAY: :10 - AgentJobName: $(Agent.JobName) - displayName: "Download & Install UI Test Dependencies" - condition: succeeded() - - # Skip @skip tagged tests - # Always dump to a text file (easier than scrolling and downloading the logs seprately). - # This way all logs are part of the artificat for each test. - - script: python uitests test --channel=$(VSCodeChannel) -- --format=pretty $(Tags) --tags=~@skip --logging-level=INFO --no-logcapture --no-capture -D python_path=$(CI_PYTHON_PATH) | tee '.vscode test/reports/behave_log.log' - env: - DISPLAY: :10 - AgentJobName: $(Agent.JobName) - AZURE_COGNITIVE_ENDPOINT: $(AZURE_COGNITIVE_ENDPOINT) - AZURE_COGNITIVE_KEY: $(AZURE_COGNITIVE_KEY) - VSCODE_CHANNEL: $(VSCodeChannel) - CI_PYTHON_PATH: $(CI_PYTHON_PATH) - PYTHON_VERSION: $(PythonVersion) - failOnStderr: false - displayName: "Run Tests" - condition: succeeded() - - # Write exit code to a text file, so we can read it and fail CI in a separate task (fail if file exists). - # CI doesn't seem to fail based on exit codes. - # We can't fail on writing to stderr either as python logs stuff there & other errors that can be ignored are written there. - - bash: | - FILE=uitests/uitests/uitest_failed.txt - if [[ -f "$FILE" ]]; - then - echo "UI Tests failed" - exit 1 - fi - displayName: "Check if UI Tests Passed" - condition: succeeded() - - # Generate and publis results even if there are failures in previous steps. - - script: python uitests report - env: - AgentJobName: $(Agent.JobName) - displayName: "Generate Reports" - condition: always() - - - task: CopyFiles@2 - inputs: - contents: ".vscode test/reports/**" - targetFolder: $(Build.ArtifactStagingDirectory) - displayName: "Copy Reports" - condition: always() - - - task: CopyFiles@2 - inputs: - contents: ".vscode test/screenshots/**" - targetFolder: $(Build.ArtifactStagingDirectory) - displayName: "Copy Screenshots" - condition: always() - - - task: CopyFiles@2 - inputs: - contents: ".vscode test/logs/**" - targetFolder: $(Build.ArtifactStagingDirectory) - displayName: "Copy Extension Logs" - condition: always() - - - task: CopyFiles@2 - inputs: - contents: ".vscode test/user/logs/**" - targetFolder: $(Build.ArtifactStagingDirectory) - displayName: "Copy VSC Logs" - condition: always() - - - task: PublishBuildArtifacts@1 - inputs: - pathtoPublish: $(Build.ArtifactStagingDirectory) - artifactName: $(Agent.JobName) - displayName: "Upload Reports" - condition: always() - - - task: PublishTestResults@2 - displayName: "TestResults" - inputs: - testRunTitle: $(Agent.JobName) - testRunner: JUnit - testResultsFiles: "$(Build.SourcesDirectory)/.vscode test/reports/*.xml" - condition: always() diff --git a/build/ci/vscode-python-ci.yaml b/build/ci/vscode-python-ci.yaml deleted file mode 100644 index b55d72c4522f..000000000000 --- a/build/ci/vscode-python-ci.yaml +++ /dev/null @@ -1,285 +0,0 @@ -name: '$(Year:yyyy).$(Month).0.$(BuildID)-ci' -# CI build. -# Notes: Only trigger a commit for master and release, and skip build/rebuild -# on changes in the news and .vscode folders. -trigger: - branches: - include: ["master", "release"] - paths: - exclude: ["/news/1 Enhancements", "/news/2 Fixes", "/news/3 Code Health", "/.vscode"] - -# Not the PR build for merges to master and release. -pr: none - -# Variables that are available for the entire pipeline. -variables: - PythonVersion: '3.7' - NodeVersion: '10.5.0' - NpmVersion: 'latest' - MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. - MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). - VSC_PYTHON_FORCE_LOGGING: true # Enable this to turn on console output for the logger - VSC_PYTHON_LOG_FILE: '$(Build.ArtifactStagingDirectory)/pvsc.log' - -jobs: - -- template: templates/build_compile_jobs.yml - -- template: templates/uitest_jobs.yml - parameters: - # Test only against stable version of VSC. - vscodeChannels: ['stable'] - # Run only smoke tests against 3.7 and 2.7 (exclude others). - jobs: - - test: "Smoke" - tags: "--tags=@smoke" - ignorePythonVersions: "3.6,3.5" - -- job: 'CI' - - strategy: - matrix: - # Each member of this list must contain these values: - # VMImageName: '[name]' - the VM image to run the tests on. - # TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. - # Each member of this list may contain these values: - # NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. - # NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. - # NeedsIPythonReqs: [true|false] - install the ipython-test-requirements prior to running tests. False if not set. - # PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion if not set. - # NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion if not set. - # SkipXvfb: [true|false] - skip initialization of xvfb prior to running system tests on Linux. False if not set - # UploadBinary: [true|false] - upload test binaries to Azure if true. False if not set. - - ## Virtual Environment Tests: - - 'Win-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Linux-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Mac-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Win-Py3.6 Unit': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'pythonUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Linux-Py3.6 Unit': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'pythonUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - 'Mac-Py3.6 Unit': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'pythonUnitTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - - 'Win-Py3.7 Venv': - VMImageName: 'vs2017-win2016' - PythonVersion: '3.7' - TestsToRun: 'venvTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - # This is for the venvTests to use, not needed if you don't run venv tests... - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Linux-Py3.7 Venv': - VMImageName: 'ubuntu-16.04' - PythonVersion: '3.7' - TestsToRun: 'venvTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Mac-Py3.7 Venv': - VMImageName: 'macos-10.13' - PythonVersion: '3.7' - TestsToRun: 'venvTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Win-Py3.6 Venv': - VMImageName: 'vs2017-win2016' - PythonVersion: '3.6' - TestsToRun: 'venvTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Linux-Py3.6 Venv': - VMImageName: 'ubuntu-16.04' - PythonVersion: '3.6' - TestsToRun: 'venvTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Mac-Py3.6 Venv': - VMImageName: 'macos-10.13' - PythonVersion: '3.6' - TestsToRun: 'venvTests, pythonIPythonTests' - NeedsPythonTestReqs: true - NeedsIPythonReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - - # SingleWorkspace Tests - 'Win-Py3.7 Single': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Single': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Single': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Win-Py3.6 Single': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Single': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Single': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - - # MultiWorkspace Tests - 'Win-Py3.7 Multi': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Multi': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Multi': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Win-Py3.6 Multi': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Multi': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Multi': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - - # Debugger integration Tests - 'Win-Py3.7 Debugger': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Debugger': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Debugger': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Win-Py3.6 Debugger': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Debugger': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Debugger': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - - # Functional tests (not mocked Jupyter) - 'Windows-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Linux-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Mac-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Windows-Py3.6 Functional': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Linux-Py3.6 Functional': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - 'Mac-Py3.6 Functional': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - - pool: - vmImage: $(VMImageName) - - steps: - - template: templates/test_phases.yml - -- job: Coverage - dependsOn: - - CI - pool: - vmImage: "macos-latest" - variables: - TestsToRun: 'testUnitTests' - steps: - - template: templates/merge_upload_coverage.yml diff --git a/build/ci/vscode-python-nightly-ci.yaml b/build/ci/vscode-python-nightly-ci.yaml deleted file mode 100644 index d008ad826f55..000000000000 --- a/build/ci/vscode-python-nightly-ci.yaml +++ /dev/null @@ -1,458 +0,0 @@ -# Nightly build -# Notes: Scheduled builds don't have a trigger in YAML (as of this writing). -# Trigger is set through the Azure DevOps UI `Nightly Build->Edit->...->Triggers`. - -name: '$(Year:yyyy).$(Month).0.$(BuildID)-alpha' - -# Not the CI build, see `vscode-python-ci.yaml`. -trigger: none - -# Not the PR build for merges to master and release. -pr: none - -# Variables that are available for the entire pipeline. -variables: - PythonVersion: '3.7' - NodeVersion: '10.5.0' - NpmVersion: 'latest' - MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. - MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). - VSC_PYTHON_LOG_FILE: '$(Build.ArtifactStagingDirectory)/pvsc.log' - -jobs: - -- template: templates/build_compile_jobs.yml - -- template: templates/uitest_jobs.yml - parameters: - # In PRs, test only against stable version of VSC. - vscodeChannels: ['stable'] - # In PRs, run smoke tests against 3.7 and 2.7 (excluding others). - jobs: - - test: "Smoke" - tags: "--tags=@smoke" - ignorePythonVersions: "3.6,3.5" - -- job: 'Nightly' - - strategy: - matrix: - # Each member of this list must contain these values: - # VMImageName: '[name]' - the VM image to run the tests on. - # TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. - # Each member of this list may contain these values: - # NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. - # NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. - # PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion if not set. - # NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion if not set. - # SkipXvfb: [true|false] - skip initialization of xvfb prior to running system tests on Linux. False if not set - # UploadBinary: [true|false] - upload test binaries to Azure if true. False if not set. - - ## Virtual Environment Tests: - - 'Win-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testUnitTests, pythonUnitTests' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testUnitTests, pythonUnitTests' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testUnitTests, pythonUnitTests' - NeedsPythonTestReqs: true - 'Win-Py3.6 Unit': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Unit': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Unit': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Win-Py3.5 Unit': - PythonVersion: '3.5' - VMImageName: 'vs2017-win2016' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Linux-Py3.5 Unit': - PythonVersion: '3.5' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Mac-Py3.5 Unit': - PythonVersion: '3.5' - VMImageName: 'macos-10.13' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Win-Py2.7 Unit': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Unit': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Unit': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - - 'Win-Py3.7 Venv': - VMImageName: 'vs2017-win2016' - PythonVersion: '3.7' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - # This is for the venvTests to use, not needed if you don't run venv tests... - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Linux-Py3.7 Venv': - VMImageName: 'ubuntu-16.04' - PythonVersion: '3.7' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Mac-Py3.7 Venv': - VMImageName: 'macos-10.13' - PythonVersion: '3.7' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Win-Py3.6 Venv': - VMImageName: 'vs2017-win2016' - PythonVersion: '3.6' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Linux-Py3.6 Venv': - VMImageName: 'ubuntu-16.04' - PythonVersion: '3.6' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Mac-Py3.6 Venv': - VMImageName: 'macos-10.13' - PythonVersion: '3.6' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Win-Py3.5 Venv': - VMImageName: 'vs2017-win2016' - PythonVersion: '3.5' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Linux-Py3.5 Venv': - VMImageName: 'ubuntu-16.04' - PythonVersion: '3.5' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - 'Mac-Py3.5 Venv': - VMImageName: 'macos-10.13' - PythonVersion: '3.5' - TestsToRun: 'venvTests' - NeedsPythonTestReqs: true - PYTHON_VIRTUAL_ENVS_LOCATION: './src/tmp/envPaths.json' - # Note: Virtual env tests use `venv` and won't currently work with Python 2.7 - - # SingleWorkspace Tests - 'Win-Py3.7 Single': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Single': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Single': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Win-Py3.6 Single': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Single': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Single': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Win-Py3.5 Single': - PythonVersion: '3.5' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.5 Single': - PythonVersion: '3.5' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.5 Single': - PythonVersion: '3.5' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Win-Py2.7 Single': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Single': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Single': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - - # MultiWorkspace Tests - 'Win-Py3.7 Multi': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Multi': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Multi': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Win-Py3.6 Multi': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Multi': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Multi': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Win-Py3.5 Multi': - PythonVersion: '3.5' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.5 Multi': - PythonVersion: '3.5' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.5 Multi': - PythonVersion: '3.5' - VMImageName: 'macos-10.13' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Win-Py2.7 Multi': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Multi': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Multi': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testMultiWorkspace' - NeedsPythonTestReqs: true - - # Debugger integration Tests - 'Win-Py3.7 Debugger': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Debugger': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Debugger': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Win-Py3.6 Debugger': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Linux-Py3.6 Debugger': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Mac-Py3.6 Debugger': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Win-Py3.5 Debugger': - PythonVersion: '3.5' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Linux-Py3.5 Debugger': - PythonVersion: '3.5' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Mac-Py3.5 Debugger': - PythonVersion: '3.5' - VMImageName: 'macos-10.13' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Win-Py2.7 Debugger': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Debugger': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Debugger': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testDebugger' - NeedsPythonTestReqs: true - - # Functional tests (not mocked Jupyter) - 'Windows-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - # This tells the functional tests to not mock out Jupyter... - VSCODE_PYTHON_ROLLING: true - 'Linux-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Mac-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Windows-Py3.6 Functional': - PythonVersion: '3.6' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Linux-Py3.6 Functional': - PythonVersion: '3.6' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Mac-Py3.6 Functional': - PythonVersion: '3.6' - VMImageName: 'macos-10.13' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Windows-Py3.5 Functional': - PythonVersion: '3.5' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Linux-Py3.5 Functional': - PythonVersion: '3.5' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Mac-Py3.5 Functional': - PythonVersion: '3.5' - VMImageName: 'macos-10.13' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Windows-Py2.7 Functional': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Linux-Py2.7 Functional': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - 'Mac-Py2.7 Functional': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - VSCODE_PYTHON_ROLLING: true - - pool: - vmImage: $(VMImageName) - - steps: - - template: templates/test_phases.yml - -- job: Coverage - dependsOn: - - Nightly - pool: - vmImage: "macos-latest" - variables: - TestsToRun: 'testUnitTests' - steps: - - template: templates/merge_upload_coverage.yml diff --git a/build/ci/vscode-python-nightly-uitest.yaml b/build/ci/vscode-python-nightly-uitest.yaml deleted file mode 100644 index 570a842e9586..000000000000 --- a/build/ci/vscode-python-nightly-uitest.yaml +++ /dev/null @@ -1,55 +0,0 @@ -name: '$(Year:yyyy).$(Month).0.$(BuildID)-nightly-uitest' - -trigger: none -pr: none -schedules: -- cron: "0 0 * * *" - # Daily midnight build, runs at midnight every day, but only if the code has changed since the last run, for master and all releases/* - displayName: Daily midnight build - branches: - include: - - master - - releases/* - -# Variables that are available for the entire pipeline. -variables: - PythonVersion: '3.7' - NodeVersion: '10.5.0' - NpmVersion: 'latest' - MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. - MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). - VSC_PYTHON_FORCE_LOGGING: true # Enable this to turn on console output for the logger - VSC_PYTHON_LOG_FILE: '$(Build.ArtifactStagingDirectory)/pvsc.log' - -jobs: -- template: templates/build_compile_jobs.yml - -- template: templates/uitest_jobs.yml - parameters: - jobs: - - test: "Smoke" - tags: "--tags=@smoke" - # Smoke tests are cheap, so run them against all Python Versions. - - test: "Test" - tags: "--tags=@testing" - # We have python code that is involved in running/discovering tests. - # Hence test against all versions, until we have CI running for the Python code. - # I.e. when all test dicovery/running is done purely in Python. - - test: "Terminal" - tags: "--tags=@terminal --tags=~@terminal.pipenv" - # No need to run tests against all versions. - # This is faster/cheaper, besides activation of terminals is generic enough - # not to warrant testing against all versions. - ignorePythonVersions: "3.6,3.5" - - test: "Debugging" - tags: "--tags=@debugging" - # No need to run tests against all versions. - # This is faster/cheaper, and these are external packages. - # We expect them to work (or 3rd party packages to test against all PY versions). - ignorePythonVersions: "3.6,3.5" - - test: "Jedi_Language_Server" - tags: "--tags=@ls" - # No need to run tests against all versions. - # This is faster/cheaper, and these are external packages. - # We expect them to work (or 3rd party packages to test against all PY versions). - ignorePythonVersions: "3.6,3.5" diff --git a/build/ci/vscode-python-pr-validation.yaml b/build/ci/vscode-python-pr-validation.yaml deleted file mode 100644 index 1241ff34a8df..000000000000 --- a/build/ci/vscode-python-pr-validation.yaml +++ /dev/null @@ -1,122 +0,0 @@ -name: '$(Year:yyyy).$(Month).0.$(BuildID)-pr' - -# PR Validation build. -# Notes: Only trigger a PR build for master and release, and skip build/rebuild -# on changes in the news and .vscode folders. -pr: - autoCancel: true - branches: - include: ["master", "release"] - paths: - exclude: ["/news/1 Enhancements", "/news/2 Fixes", "/news/3 Code Health", "/.vscode"] - -# Not the CI build for merges to master and release. -trigger: none - -# Variables that are available for the entire pipeline. -variables: - PythonVersion: '3.7' - NodeVersion: '10.5.0' - NpmVersion: 'latest' - MOCHA_FILE: '$(Build.ArtifactStagingDirectory)/test-junit.xml' # All test files will write their JUnit xml output to this file, clobbering the last time it was written. - MOCHA_REPORTER_JUNIT: true # Use the mocha-multi-reporters and send output to both console (spec) and JUnit (mocha-junit-reporter). - VSC_PYTHON_LOG_FILE: '$(Build.ArtifactStagingDirectory)/pvsc.log' - -jobs: -- template: templates/build_compile_jobs.yml - -- template: templates/uitest_jobs.yml - parameters: - # In PRs, test only against stable version of VSC. - vscodeChannels: ['stable'] - # In PRs, run smoke tests against 3.7 and 2.7 (excluding others). - jobs: - - test: "Smoke" - tags: "--tags=@smoke" - ignorePythonVersions: "3.6,3.5" - -- job: 'PR' - dependsOn: [] - strategy: - matrix: - # Each member of this list must contain these values: - # VMImageName: '[name]' - the VM image to run the tests on. - # TestsToRun: 'testA, testB, ..., testN' - the list of tests to execute, see the list above. - # Each member of this list may contain these values: - # NeedsPythonTestReqs: [true|false] - install the test-requirements prior to running tests. False if not set. - # NeedsPythonFunctionalReqs: [true|false] - install the functional-requirements prior to running tests. False if not set. - # PythonVersion: 'M.m' - the Python version to run. DefaultPythonVersion if not set. - # NodeVersion: 'x.y.z' - Node version to use. DefaultNodeVersion if not set. - # SkipXvfb: [true|false] - skip initialization of xvfb prior to running system tests on Linux. False if not set - # UploadBinary: [true|false] - upload test binaries to Azure if true. False if not set. - 'Win-Py3.7 Unit': - VMImageName: 'vs2017-win2016' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' - NeedsPythonTestReqs: true - 'Win-Py2.7 Unit': - PythonVersion: '2.7' - VMImageName: 'vs2017-win2016' - TestsToRun: 'pythonUnitTests' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Single Workspace': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Mac-Py3.7 Smoke': - PythonVersion: '3.7' - VMImageName: 'macos-10.13' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Mac-Py2.7 Unit+Single': - PythonVersion: '2.7' - VMImageName: 'macos-10.13' - TestsToRun: 'pythonUnitTests, testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Unit': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testUnitTests, pythonUnitTests, pythonInternalTools' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Single Workspace': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Smoke': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testSmoke' - NeedsPythonTestReqs: true - 'Linux-Py2.7 Unit+Single': - PythonVersion: '2.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'pythonUnitTests, testSingleWorkspace' - NeedsPythonTestReqs: true - 'Linux-Py3.7 Functional': - PythonVersion: '3.7' - VMImageName: 'ubuntu-16.04' - TestsToRun: 'testfunctional' - NeedsPythonTestReqs: true - NeedsPythonFunctionalReqs: true - - pool: - vmImage: $(VMImageName) - - steps: - - template: templates/test_phases.yml - -- job: Coverage - dependsOn: - - PR - pool: - vmImage: "macos-latest" - variables: - TestsToRun: 'testUnitTests' - steps: - - template: templates/merge_upload_coverage.yml diff --git a/build/constants.js b/build/constants.js index 54c7349a1a72..73815ebea45f 100644 --- a/build/constants.js +++ b/build/constants.js @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const util = require("./util"); + +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 5b9387ecb0e8..000000000000 --- a/build/constants.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as util from './util'; - -export const ExtensionRootDir = util.ExtensionRootDir; - -// This is a list of files that existed before MS got the extension. -export const existingFiles: string[] = util.getListOfFiles('existingFiles.json'); -export const contributedFiles: string[] = util.getListOfFiles('contributedFiles.json'); - -export const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; diff --git a/build/contributedFiles.json b/build/contributedFiles.json index 0d4f101c7a37..fe51488c7066 100644 --- a/build/contributedFiles.json +++ b/build/contributedFiles.json @@ -1,2 +1 @@ -[ -] +[] diff --git a/build/existingFiles.json b/build/existingFiles.json index 3fd7bc753de0..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", @@ -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", 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 dce60486ae64..5c3a9e3116ed 100644 --- a/build/functional-test-requirements.txt +++ b/build/functional-test-requirements.txt @@ -1,7 +1,5 @@ -# List of requirements for functional tests -versioneer -jupyter -numpy -matplotlib -pandas -livelossplot +# List of requirements for functional tests +versioneer +numpy +pytest +pytest-cov diff --git a/build/ipython-test-requirements.txt b/build/ipython-test-requirements.txt deleted file mode 100644 index 688e039a4461..000000000000 --- a/build/ipython-test-requirements.txt +++ /dev/null @@ -1,4 +0,0 @@ -# List of requirements for ipython tests -numpy -pandas -ipython diff --git a/build/license-header.txt b/build/license-header.txt new file mode 100644 index 000000000000..2970b03d7a1c --- /dev/null +++ b/build/license-header.txt @@ -0,0 +1,9 @@ +PLEASE NOTE: This is the license for the Python extension for Visual Studio Code. The Python extension automatically installs other extensions as optional dependencies, which can be uninstalled at any time. These extensions have separate licenses: + + - The Python Debugger extension is released under an MIT License: + https://marketplace.visualstudio.com/items/ms-python.debugpy/license + + - The Pylance extension is only available in binary form and is released under a Microsoft proprietary license, the terms of which are available here: + https://marketplace.visualstudio.com/items/ms-python.vscode-pylance/license + +------------------------------------------------------------------------------ diff --git a/build/test-requirements.txt b/build/test-requirements.txt index f91307d3f63e..ff9afdfc8a2e 100644 --- a/build/test-requirements.txt +++ b/build/test-requirements.txt @@ -1,17 +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 -isort +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 45ed1ed7a6ef..000000000000 --- a/build/tsconfig.json +++ /dev/null @@ -1,29 +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", - ".vscode test", - "src" - ] -} diff --git a/build/tslint-rules/baseRuleWalker.js b/build/tslint-rules/baseRuleWalker.js deleted file mode 100644 index 3e563c8d8f44..000000000000 --- a/build/tslint-rules/baseRuleWalker.js +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const path = require("path"); -const Lint = require("tslint"); -const util = require("../util"); -class BaseRuleWalker extends Lint.RuleWalker { - shouldIgnoreCurrentFile(node, filesToIgnore) { - const sourceFile = node.getSourceFile(); - if (sourceFile && sourceFile.fileName) { - const filename = path.resolve(util.ExtensionRootDir, sourceFile.fileName); - if (filesToIgnore.indexOf(filename.replace(/\//g, path.sep)) >= 0) { - return true; - } - } - return false; - } -} -exports.BaseRuleWalker = BaseRuleWalker; diff --git a/build/tslint-rules/baseRuleWalker.ts b/build/tslint-rules/baseRuleWalker.ts deleted file mode 100644 index e1e7dff0e44b..000000000000 --- a/build/tslint-rules/baseRuleWalker.ts +++ /dev/null @@ -1,22 +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 * as util from '../util'; - -export class BaseRuleWalker extends Lint.RuleWalker { - protected shouldIgnoreCurrentFile(node: ts.Node, filesToIgnore: string[]): boolean { - const sourceFile = node.getSourceFile(); - if (sourceFile && sourceFile.fileName) { - const filename = path.resolve(util.ExtensionRootDir, sourceFile.fileName); - if (filesToIgnore.indexOf(filename.replace(/\//g, path.sep)) >= 0) { - return true; - } - } - return false; - } -} diff --git a/build/tslint-rules/messagesMustBeLocalizedRule.js b/build/tslint-rules/messagesMustBeLocalizedRule.js deleted file mode 100644 index fcd38774ad37..000000000000 --- a/build/tslint-rules/messagesMustBeLocalizedRule.js +++ /dev/null @@ -1,68 +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 ts = require("typescript"); -const util = require("../util"); -const baseRuleWalker_1 = require("./baseRuleWalker"); -const methodNames = [ - // From IApplicationShell (vscode.window): - 'showErrorMessage', 'showInformationMessage', - 'showWarningMessage', 'setStatusBarMessage', - // From IOutputChannel (vscode.OutputChannel): - 'appendLine', 'appendLine' -]; -// tslint:ignore-next-line:no-suspicious-comments -// TODO: Ideally we would not ignore any files. -const ignoredFiles = util.getListOfFiles('unlocalizedFiles.json'); -const ignoredPrefix = path.normalize('src/test'); -const failureMessage = 'Messages must be localized in the Python Extension (use src/client/common/utils/localize.ts)'; -class NoStringLiteralsInMessages extends baseRuleWalker_1.BaseRuleWalker { - visitCallExpression(node) { - if (!this.shouldIgnoreNode(node)) { - node.arguments - .filter(arg => ts.isStringLiteral(arg) || ts.isTemplateLiteral(arg)) - .forEach(arg => { - this.addFailureAtNode(arg, failureMessage); - }); - } - super.visitCallExpression(node); - } - shouldIgnoreCurrentFile(node) { - //console.log(''); - //console.log(node.getSourceFile().fileName); - //console.log(ignoredFiles); - if (super.shouldIgnoreCurrentFile(node, ignoredFiles)) { - return true; - } - const sourceFile = node.getSourceFile(); - if (sourceFile && sourceFile.fileName) { - if (sourceFile.fileName.startsWith(ignoredPrefix)) { - return true; - } - } - return false; - } - shouldIgnoreNode(node) { - if (this.shouldIgnoreCurrentFile(node)) { - return true; - } - if (!ts.isPropertyAccessExpression(node.expression)) { - return true; - } - const prop = node.expression; - if (methodNames.indexOf(prop.name.text) < 0) { - return true; - } - return false; - } -} -class Rule extends Lint.Rules.AbstractRule { - apply(sourceFile) { - return this.applyWithWalker(new NoStringLiteralsInMessages(sourceFile, this.getOptions())); - } -} -Rule.FAILURE_STRING = failureMessage; -exports.Rule = Rule; diff --git a/build/tslint-rules/messagesMustBeLocalizedRule.ts b/build/tslint-rules/messagesMustBeLocalizedRule.ts deleted file mode 100644 index cdd94678faa0..000000000000 --- a/build/tslint-rules/messagesMustBeLocalizedRule.ts +++ /dev/null @@ -1,72 +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 * as util from '../util'; -import { BaseRuleWalker } from './baseRuleWalker'; - -const methodNames = [ - // From IApplicationShell (vscode.window): - 'showErrorMessage', 'showInformationMessage', - 'showWarningMessage', 'setStatusBarMessage', - // From IOutputChannel (vscode.OutputChannel): - 'appendLine', 'appendLine' -]; -// tslint:ignore-next-line:no-suspicious-comments -// TODO: Ideally we would not ignore any files. -const ignoredFiles = util.getListOfFiles('unlocalizedFiles.json'); -const ignoredPrefix = path.normalize('src/test'); - -const failureMessage = 'Messages must be localized in the Python Extension (use src/client/common/utils/localize.ts)'; - -class NoStringLiteralsInMessages extends BaseRuleWalker { - protected visitCallExpression(node: ts.CallExpression): void { - if (!this.shouldIgnoreNode(node)) { - node.arguments - .filter(arg => ts.isStringLiteral(arg) || ts.isTemplateLiteral(arg)) - .forEach(arg => { - this.addFailureAtNode(arg, failureMessage); - }); - } - super.visitCallExpression(node); - } - protected shouldIgnoreCurrentFile(node: ts.Node) { - //console.log(''); - //console.log(node.getSourceFile().fileName); - //console.log(ignoredFiles); - if (super.shouldIgnoreCurrentFile(node, ignoredFiles)) { - return true; - } - const sourceFile = node.getSourceFile(); - if (sourceFile && sourceFile.fileName) { - if (sourceFile.fileName.startsWith(ignoredPrefix)) { - return true; - } - } - return false; - } - private shouldIgnoreNode(node: ts.CallExpression) { - if (this.shouldIgnoreCurrentFile(node)) { - return true; - } - if (!ts.isPropertyAccessExpression(node.expression)) { - return true; - } - const prop = node.expression as ts.PropertyAccessExpression; - if (methodNames.indexOf(prop.name.text) < 0) { - return true; - } - return false; - } -} - -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 index e30399df3034..4da3d450af23 100644 --- a/build/unlocalizedFiles.json +++ b/build/unlocalizedFiles.json @@ -7,7 +7,6 @@ "src/client/formatters/baseFormatter.ts", "src/client/formatters/blackFormatter.ts", "src/client/interpreter/configuration/pythonPathUpdaterService.ts", - "src/client/interpreter/locators/services/pipEnvService.ts", "src/client/linters/errorHandlers/notInstalled.ts", "src/client/linters/errorHandlers/standard.ts", "src/client/linters/linterCommands.ts", @@ -15,7 +14,7 @@ "src/client/providers/importSortProvider.ts", "src/client/providers/objectDefinitionProvider.ts", "src/client/providers/simpleRefactorProvider.ts", - "src/client/providers/updateSparkLibraryProvider.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", diff --git a/build/update_ext_version.py b/build/update_ext_version.py new file mode 100644 index 000000000000..6d709ae05f7f --- /dev/null +++ b/build/update_ext_version.py @@ -0,0 +1,126 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import argparse +import datetime +import json +import pathlib +import sys +from typing import Sequence, Tuple, Union + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + + +def build_arg_parse() -> argparse.ArgumentParser: + """Builds the arguments parser.""" + parser = argparse.ArgumentParser( + description="This script updates the python extension micro version based on the release or pre-release channel." + ) + parser.add_argument( + "--release", + action="store_true", + help="Treats the current build as a release build.", + ) + parser.add_argument( + "--build-id", + action="store", + type=int, + default=None, + help="If present, will be used as a micro version.", + required=False, + ) + parser.add_argument( + "--for-publishing", + action="store_true", + help="Removes `-dev` or `-rc` suffix.", + ) + return parser + + +def is_even(v: Union[int, str]) -> bool: + """Returns True if `v` is even.""" + return not int(v) % 2 + + +def micro_build_number() -> str: + """Generates the micro build number. + The format is `1`. + """ + return f"1{datetime.datetime.now(tz=datetime.timezone.utc).strftime('%j%H%M')}" + + +def parse_version(version: str) -> Tuple[str, str, str, str]: + """Parse a version string into a tuple of version parts.""" + major, minor, parts = version.split(".", maxsplit=2) + try: + micro, suffix = parts.split("-", maxsplit=1) + except ValueError: + micro = parts + suffix = "" + return major, minor, micro, suffix + + +def main(package_json: pathlib.Path, argv: Sequence[str]) -> None: + parser = build_arg_parse() + args = parser.parse_args(argv) + + package = json.loads(package_json.read_text(encoding="utf-8")) + + major, minor, micro, suffix = parse_version(package["version"]) + + current_year = datetime.datetime.now().year + current_month = datetime.datetime.now().month + int_major = int(major) + valid_major = ( + int_major + == current_year # Between JAN-DEC major version should be current year + or ( + int_major == current_year - 1 and current_month == 1 + ) # After new years the check is relaxed for JAN to allow releases of previous year DEC + or ( + int_major == current_year + 1 and current_month == 12 + ) # Before new years the check is relaxed for DEC to allow pre-releases of next year JAN + ) + if not valid_major: + raise ValueError( + f"Major version [{major}] must be the current year [{current_year}].", + f"If changing major version after new year's, change to {current_year}.1.0", + "Minor version must be updated based on release or pre-release channel.", + ) + + if args.release and not is_even(minor): + raise ValueError( + f"Release version should have EVEN numbered minor version: {package['version']}" + ) + elif not args.release and is_even(minor): + raise ValueError( + f"Pre-Release version should have ODD numbered minor version: {package['version']}" + ) + + print(f"Updating build FROM: {package['version']}") + if args.build_id: + # If build id is provided it should fall within the 0-INT32 max range + # that the max allowed value for publishing to the Marketplace. + if args.build_id < 0 or (args.for_publishing and args.build_id > ((2**32) - 1)): + raise ValueError(f"Build ID must be within [0, {(2**32) - 1}]") + + package["version"] = ".".join((major, minor, str(args.build_id))) + elif args.release: + package["version"] = ".".join((major, minor, micro)) + else: + # micro version only updated for pre-release. + package["version"] = ".".join((major, minor, micro_build_number())) + + if not args.for_publishing and not args.release and len(suffix): + package["version"] += "-" + suffix + print(f"Updating build TO: {package['version']}") + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH, sys.argv[1:]) diff --git a/build/update_package_file.py b/build/update_package_file.py new file mode 100644 index 000000000000..f82587ced846 --- /dev/null +++ b/build/update_package_file.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import json +import pathlib + +EXT_ROOT = pathlib.Path(__file__).parent.parent +PACKAGE_JSON_PATH = EXT_ROOT / "package.json" + + +def main(package_json: pathlib.Path) -> None: + package = json.loads(package_json.read_text(encoding="utf-8")) + package["enableTelemetry"] = True + + # Overwrite package.json with new data add a new-line at the end of the file. + package_json.write_text( + json.dumps(package, indent=4, ensure_ascii=False) + "\n", encoding="utf-8" + ) + + +if __name__ == "__main__": + main(PACKAGE_JSON_PATH) diff --git a/build/util.js b/build/util.js index c6a55d6c2035..c54e204ae7d7 100644 --- a/build/util.js +++ b/build/util.js @@ -1,9 +1,11 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -const fs = require("fs"); -const path = require("path"); + +const fs = require('fs'); +const path = require('path'); + exports.ExtensionRootDir = path.dirname(__dirname); function getListOfFiles(filename) { filename = path.normalize(filename); @@ -12,9 +14,6 @@ function getListOfFiles(filename) { } const data = fs.readFileSync(filename).toString(); const files = JSON.parse(data); - return files - .map(file => { - return path.join(exports.ExtensionRootDir, file.replace(/\//g, path.sep)); - }); + return files.map((file) => path.join(exports.ExtensionRootDir, file.replace(/\//g, path.sep))); } exports.getListOfFiles = getListOfFiles; diff --git a/build/util.ts b/build/util.ts deleted file mode 100644 index 30178318c7e5..000000000000 --- a/build/util.ts +++ /dev/null @@ -1,23 +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.dirname(__dirname); - -export function getListOfFiles(filename: string): string[] { - filename = path.normalize(filename); - if (!path.isAbsolute(filename)) { - filename = path.join(__dirname, filename); - } - - const data = fs.readFileSync(filename).toString(); - const files = JSON.parse(data) as string[]; - return files - .map(file => { - return path.join(ExtensionRootDir, file.replace(/\//g, path.sep)); - }); -} diff --git a/build/webpack/common.js b/build/webpack/common.js index d6e620e73e4c..c7f7460adf86 100644 --- a/build/webpack/common.js +++ b/build/webpack/common.js @@ -1,55 +1,51 @@ -// 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"); -exports.nodeModulesToExternalize = [ - 'unicode/category/Lu', - 'unicode/category/Ll', - 'unicode/category/Lt', - 'unicode/category/Lo', - 'unicode/category/Lm', - 'unicode/category/Nl', - 'unicode/category/Mn', - 'unicode/category/Mc', - 'unicode/category/Nd', - 'unicode/category/Pc', - '@jupyterlab/services', - 'azure-storage', - 'request', - 'request-progress', - 'source-map-support', - 'diff-match-patch', - 'sudo-prompt', - 'node-stream-zip', - 'xml2js', - 'vsls/vscode', - 'pdfkit', - 'crypto-js', - 'fontkit', - 'linebreak', - 'png-js' -]; -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` - })); - } - return plugins; -} -exports.getDefaultPlugins = getDefaultPlugins; -function getListOfExistingModulesInOutDir() { - const outDir = path.join(constants_1.ExtensionRootDir, 'out', 'client'); - const files = glob.sync('**/*.js', { sync: true, cwd: outDir }); - return files.map(filePath => `./${filePath.slice(0, -3)}`); -} -exports.getListOfExistingModulesInOutDir = getListOfExistingModulesInOutDir; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const glob = require('glob'); +const path = require('path'); +// eslint-disable-next-line camelcase +const webpack_bundle_analyzer = require('webpack-bundle-analyzer'); +const constants = require('../constants'); + +exports.nodeModulesToExternalize = [ + 'unicode/category/Lu', + 'unicode/category/Ll', + 'unicode/category/Lt', + 'unicode/category/Lo', + 'unicode/category/Lm', + 'unicode/category/Nl', + 'unicode/category/Mn', + 'unicode/category/Mc', + 'unicode/category/Nd', + 'unicode/category/Pc', + 'source-map-support', + 'sudo-prompt', + 'node-stream-zip', + 'xml2js', +]; +exports.nodeModulesToReplacePaths = [...exports.nodeModulesToExternalize]; +function getDefaultPlugins(name) { + const plugins = []; + // Only run the analyzer on a local machine or if required + if (!constants.isCI || process.env.VSC_PYTHON_FORCE_ANALYZER) { + plugins.push( + new webpack_bundle_analyzer.BundleAnalyzerPlugin({ + analyzerMode: 'static', + reportFilename: `${name}.analyzer.html`, + generateStatsFile: true, + statsFilename: `${name}.stats.json`, + openAnalyzer: false, // Open file manually if you want to see it :) + }), + ); + } + return plugins; +} +exports.getDefaultPlugins = getDefaultPlugins; +function getListOfExistingModulesInOutDir() { + const outDir = path.join(constants.ExtensionRootDir, 'out', 'client'); + const files = glob.sync('**/*.js', { sync: true, cwd: outDir }); + return files.map((filePath) => `./${filePath.slice(0, -3)}`); +} +exports.getListOfExistingModulesInOutDir = getListOfExistingModulesInOutDir; diff --git a/build/webpack/common.ts b/build/webpack/common.ts deleted file mode 100644 index de762dc824e6..000000000000 --- a/build/webpack/common.ts +++ /dev/null @@ -1,60 +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', - 'diff-match-patch', - 'sudo-prompt', - 'node-stream-zip', - 'xml2js', - 'vsls/vscode', - 'pdfkit', - 'crypto-js', - 'fontkit', - 'linebreak', - 'png-js' -]; - -export const nodeModulesToReplacePaths = [ - ...nodeModulesToExternalize -]; - -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 7c3ccc8ef5d1..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}`; - return contents.replace(new RegExp(stringToSearch, 'gm'), stringToReplaceWith); -} -// tslint:disable:no-default-export no-invalid-this -function default_1(source) { - common_1.nodeModulesToReplacePaths.forEach(moduleName => { - if (source.indexOf(moduleName) > 0) { - source = replaceModule(source, moduleName, '"'); - source = replaceModule(source, moduleName, '\''); - } - }); - return source; -} -exports.default = default_1; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const common = require('../common'); + +function replaceModule(prefixRegex, prefix, contents, moduleName, quotes) { + const stringToSearch = `${prefixRegex}${quotes}${moduleName}${quotes}`; + const stringToReplaceWith = `${prefix}${quotes}./node_modules/${moduleName}${quotes}`; + return contents.replace(new RegExp(stringToSearch, 'gm'), stringToReplaceWith); +} + +// eslint-disable-next-line camelcase +function default_1(source) { + common.nodeModulesToReplacePaths.forEach((moduleName) => { + if (source.indexOf(moduleName) > 0) { + source = replaceModule('import\\(', 'import(', source, moduleName, '"'); + source = replaceModule('import\\(', 'import(', source, moduleName, "'"); + source = replaceModule('require\\(', 'require(', source, moduleName, '"'); + source = replaceModule('require\\(', 'require(', source, moduleName, "'"); + source = replaceModule('from ', 'from ', source, moduleName, '"'); + source = replaceModule('from ', 'from ', source, moduleName, "'"); + } + }); + return source; +} +// 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 403088403ec2..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 { nodeModulesToReplacePaths } 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) { - nodeModulesToReplacePaths.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/pdfkit.js b/build/webpack/pdfkit.js deleted file mode 100644 index 5c31590a3924..000000000000 --- a/build/webpack/pdfkit.js +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -/* -This file is only used when using webpack for bundling. -We have a dummy file so that webpack doesn't fall over when trying to bundle pdfkit. -Just point it to a dummy file (this file). -Once webpack is done, we override the pdfkit.js file in the externalized node modules directory -with the actual source of pdfkit that needs to be used by nodejs (our extension code). -*/ - -class PDFDocument {} -module.exports = PDFDocument; diff --git a/build/webpack/plugins/less-plugin-base64.js b/build/webpack/plugins/less-plugin-base64.js deleted file mode 100644 index c05d9a2b5b71..000000000000 --- a/build/webpack/plugins/less-plugin-base64.js +++ /dev/null @@ -1,62 +0,0 @@ -// Most of this was based on https://github.com/less/less-plugin-inline-urls -// License for this was included in the ThirdPartyNotices-Repository.txt -const less = require('less'); - -class Base64MimeTypeNode { - constructor() { - this.value = "image/svg+xml;base64"; - this.type = "Base64MimeTypeNode"; - } - - eval(context) { - return this; - } -} - -class Base64Visitor { - - constructor() { - this.visitor = new less.visitors.Visitor(this); - - // Set to a preEval visitor to make sure this runs before - // any evals - this.isPreEvalVisitor = true; - - // Make sure this is a replacing visitor so we remove the old data. - this.isReplacing = true; - } - - run(root) { - return this.visitor.visit(root); - } - - visitUrl(URLNode, visitArgs) { - // Return two new nodes in the call. One that has the mime type and other with the node. The data-uri - // evaluator will transform this into a base64 string - return new less.tree.Call("data-uri", [new Base64MimeTypeNode(), URLNode.value], URLNode.index || 0, URLNode.currentFileInfo); - } - -} -/* -* This was originally used to perform less on uris and turn them into base64 encoded so they can be loaded into -* a webpack html. There's one caveat though. Less and webpack don't play well together. It runs the less at the root dir. -* This means in order to use this in a less file, you need to qualify the urls as if they come from the root dir. -* Example: -* url("./foo.svg") -* becomes -* url("./src/datascience-ui/history-react/images/foo.svg") -*/ -class Base64Plugin { - constructor() { - } - - install(less, pluginManager) { - pluginManager.addVisitor(new Base64Visitor()); - } - - printUsage() { - console.log('Base64 Plugin. Add to your webpack.config.js as a plugin to convert URLs to base64 inline') - } -} - -module.exports = Base64Plugin; diff --git a/build/webpack/webpack.debugadapter.config.js b/build/webpack/webpack.debugadapter.config.js deleted file mode 100644 index aba65b8d30e6..000000000000 --- a/build/webpack/webpack.debugadapter.config.js +++ /dev/null @@ -1,64 +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'); -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 23625e8c0c3d..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 } 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 b19caa9f6a7d..082ce52a4d32 100644 --- a/build/webpack/webpack.extension.config.js +++ b/build/webpack/webpack.extension.config.js @@ -1,98 +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 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(); -// tslint:disable-next-line:no-var-requires no-require-imports -const FileManagerPlugin = require('filemanager-webpack-plugin'); + +const path = require('path'); +// eslint-disable-next-line camelcase +const tsconfig_paths_webpack_plugin = require('tsconfig-paths-webpack-plugin'); +const constants = require('../constants'); +const common = require('./common'); + +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', + }, + ], }, - { enforce: 'post', test: /unicode-properties[\/\\]index.js$/, loader: 'transform-loader?brfs' }, - { enforce: 'post', test: /fontkit[\/\\]index.js$/, loader: 'transform-loader?brfs' }, - { enforce: 'post', test: /linebreak[\/\\]src[\/\\]linebreaker.js/, loader: 'transform-loader?brfs' } - ] + { + test: /\.worker\.js$/, + use: { loader: 'worker-loader' }, + }, + ], }, externals: [ 'vscode', 'commonjs', - ...existingModulesInOutDir - ], - plugins: [ - ...common_1.getDefaultPlugins('extension'), - // Copy pdfkit bits after extension builds. webpack can't handle pdfkit. - new FileManagerPlugin({ - onEnd: [ - { - copy: [ - { source: './node_modules/pdfkit/js/data/*.*', destination: './out/client/node_modules/data' }, - { source: './node_modules/pdfkit/js/pdfkit.js', destination: './out/client/node_modules/' } - ] - } - ] - }) + ...existingModulesInOutDir, + // These dependencies are ignored because we don't use them, and App Insights has try-catch protecting their loading if they don't exist + // See: https://github.com/microsoft/vscode-extension-telemetry/issues/41#issuecomment-598852991 + 'applicationinsights-native-metrics', + '@opentelemetry/tracing', + '@azure/opentelemetry-instrumentation-azure-sdk', + '@opentelemetry/instrumentation', + '@azure/functions-core', ], + plugins: [...common.getDefaultPlugins('extension')], resolve: { - alias:{ - // Pointing pdfkit to a dummy js file so webpack doesn't fall over. - // Since pdfkit has been externalized (it gets updated with the valid code by copying the pdfkit files - // into the right destination). - 'pdfkit':path.resolve(__dirname, 'pdfkit.js') - }, extensions: ['.ts', '.js'], - plugins: [ - new tsconfig_paths_webpack_plugin_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 9b565d99e369..000000000000 --- a/build/webpack/webpack.extension.config.ts +++ /dev/null @@ -1,100 +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 } 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(); - -// tslint:disable-next-line:no-var-requires no-require-imports -const FileManagerPlugin = require('filemanager-webpack-plugin'); - -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' - } - ] - }, - {enforce: 'post', test: /unicode-properties[\/\\]index.js$/, loader: 'transform-loader?brfs'}, - {enforce: 'post', test: /fontkit[\/\\]index.js$/, loader: 'transform-loader?brfs'}, - {enforce: 'post', test: /pdfkit[\\\/]js[\\\/].*js$/, loader: 'transform-loader?brfs'}, - {enforce: 'post', test: /linebreak[\/\\]src[\/\\]linebreaker.js/, loader: 'transform-loader?brfs'} - ] - }, - externals: [ - 'vscode', - 'commonjs', - ...existingModulesInOutDir - ], - plugins: [ - ...getDefaultPlugins('extension'), - // Copy pdfkit bits after extension builds. webpack can't handle pdfkit. - new FileManagerPlugin({ - onEnd: [ - { - copy: [ - { source: './node_modules/pdfkit/js/data/*.*', destination: './out/client/node_modules/data' }, - { source: './node_modules/pdfkit/js/pdfkit.js', destination: './out/client/node_modules/' } - ] - } - ] - }) - ], - 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 7186a206a71a..a90e9135a605 100644 --- a/build/webpack/webpack.extension.dependencies.config.js +++ b/build/webpack/webpack.extension.dependencies.config.js @@ -1,71 +1,44 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -Object.defineProperty(exports, "__esModule", { value: true }); -// tslint:disable-next-line: no-require-imports -const copyWebpackPlugin = require("copy-webpack-plugin"); -const path = require("path"); -const constants_1 = require("../constants"); -const common_1 = require("./common"); -const entryItems = {}; -common_1.nodeModulesToExternalize.forEach(moduleName => { - entryItems[`node_modules/${moduleName}`] = `./node_modules/${moduleName}`; -}); -const config = { - 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') - } - ] - }, - { enforce: 'post', test: /unicode-properties[\/\\]index.js$/, loader: 'transform-loader?brfs' }, - { enforce: 'post', test: /fontkit[\/\\]index.js$/, loader: 'transform-loader?brfs' }, - { enforce: 'post', test: /linebreak[\/\\]src[\/\\]linebreaker.js/, loader: 'transform-loader?brfs' } - ] - }, - externals: [ - 'vscode', - 'commonjs' - ], - plugins: [ - ...common_1.getDefaultPlugins('dependencies'), - // vsls requires our package.json to be next to node_modules. It's how they - // 'find' the calling extension. - new copyWebpackPlugin([ - { from: './package.json', to: '.' } - ]), - // onigasm requires our onigasm.wasm to be in node_modules - new copyWebpackPlugin([ - { from: './node_modules/onigasm/lib/onigasm.wasm', to: './node_modules/onigasm/lib/onigasm.wasm' } - ]) - ], - resolve: { - alias:{ - // Pointing pdfkit to a dummy js file so webpack doesn't fall over. - // Since pdfkit has been externalized (it gets updated with the valid code by copying the pdfkit files - // into the right destination). - 'pdfkit':path.resolve(__dirname, 'pdfkit.js') - }, - extensions: ['.js'] - }, - output: { - filename: '[name].js', - path: path.resolve(constants_1.ExtensionRootDir, 'out', 'client'), - libraryTarget: 'commonjs2', - devtoolModuleFilenameTemplate: '../../[resource-path]' - } -}; -// tslint:disable-next-line:no-default-export -exports.default = config; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +const copyWebpackPlugin = require('copy-webpack-plugin'); +const path = require('path'); +const constants = require('../constants'); +const common = require('./common'); + +const entryItems = {}; +common.nodeModulesToExternalize.forEach((moduleName) => { + entryItems[`node_modules/${moduleName}`] = `./node_modules/${moduleName}`; +}); +const config = { + mode: 'production', + target: 'node', + context: constants.ExtensionRootDir, + entry: entryItems, + devtool: 'source-map', + node: { + __dirname: false, + }, + module: {}, + externals: ['vscode', 'commonjs'], + plugins: [ + ...common.getDefaultPlugins('dependencies'), + // vsls requires our package.json to be next to node_modules. It's how they + // 'find' the calling extension. + // eslint-disable-next-line new-cap + new copyWebpackPlugin({ patterns: [{ from: './package.json', to: '.' }] }), + ], + resolve: { + extensions: ['.js'], + }, + output: { + filename: '[name].js', + path: path.resolve(constants.ExtensionRootDir, 'out', 'client'), + libraryTarget: 'commonjs2', + devtoolModuleFilenameTemplate: '../../[resource-path]', + }, +}; + +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 f380ac8a77e6..000000000000 --- a/build/webpack/webpack.extension.dependencies.config.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable-next-line: no-require-imports -import copyWebpackPlugin = require('copy-webpack-plugin'); -import * as path from 'path'; -import * as webpack from 'webpack'; -import { ExtensionRootDir } from '../constants'; -import { getDefaultPlugins, nodeModulesToExternalize } from './common'; - -const entryItems: Record = {}; -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') - } - ] - }, - {enforce: 'post', test: /unicode-properties[\/\\]index.js$/, loader: 'transform-loader?brfs'}, - {enforce: 'post', test: /fontkit[\/\\]index.js$/, loader: 'transform-loader?brfs'}, - {enforce: 'post', test: /pdfkit[\\\/]js[\\\/].*js$/, loader: 'transform-loader?brfs'}, - {enforce: 'post', test: /linebreak[\/\\]src[\/\\]linebreaker.js/, loader: 'transform-loader?brfs'} - ] - }, - externals: [ - 'vscode', - 'commonjs' - ], - plugins: [ - ...getDefaultPlugins('dependencies'), - // vsls requires our package.json to be next to node_modules. It's how they - // 'find' the calling extension. - new copyWebpackPlugin([ - { from: './package.json', to: '.' } - ]), - // onigasm requires our onigasm.wasm to be in node_modules - new copyWebpackPlugin([ - { from: './node_modules/onigasm/lib/onigasm.wasm', to: './node_modules/onigasm/lib/onigasm.wasm' } - ]) - ], - resolve: { - 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/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/data/.vscode/settings.json b/data/.vscode/settings.json deleted file mode 100644 index 615aafb035a1..000000000000 --- a/data/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "python.pythonPath": "/usr/bin/python3" -} \ No newline at end of file diff --git a/data/get-pip.py b/data/get-pip.py deleted file mode 100644 index 90d5644ff02e..000000000000 --- a/data/get-pip.py +++ /dev/null @@ -1,21492 +0,0 @@ -#!/usr/bin/env python -# -# Hi There! -# You may be wondering what this giant blob of binary data here is, you might -# even be worried that we're up to something nefarious (good for you for being -# paranoid!). This is a base85 encoding of a zip file, this zip file contains -# an entire copy of pip (version 19.1). -# -# Pip is a thing that installs packages, pip itself is a package that someone -# might want to install, especially if they're looking to run this get-pip.py -# script. Pip has a lot of code to deal with the security of installing -# packages, various edge cases on various platforms, and other such sort of -# "tribal knowledge" that has been encoded in its code base. Because of this -# we basically include an entire copy of pip inside this blob. We do this -# because the alternatives are attempt to implement a "minipip" that probably -# doesn't do things correctly and has weird edge cases, or compress pip itself -# down into a single file. -# -# If you're wondering how this is created, it is using an invoke task located -# in tasks/generate.py called "installer". It can be invoked by using -# ``invoke generate.installer``. - -import os.path -import pkgutil -import shutil -import sys -import struct -import tempfile - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -if PY3: - iterbytes = iter -else: - def iterbytes(buf): - return (ord(byte) for byte in buf) - -try: - from base64 import b85decode -except ImportError: - _b85alphabet = (b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" - b"abcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~") - - def b85decode(b): - _b85dec = [None] * 256 - for i, c in enumerate(iterbytes(_b85alphabet)): - _b85dec[c] = i - - padding = (-len(b)) % 5 - b = b + b'~' * padding - out = [] - packI = struct.Struct('!I').pack - for i in range(0, len(b), 5): - chunk = b[i:i + 5] - acc = 0 - try: - for c in iterbytes(chunk): - acc = acc * 85 + _b85dec[c] - except TypeError: - for j, c in enumerate(iterbytes(chunk)): - if _b85dec[c] is None: - raise ValueError( - 'bad base85 character at position %d' % (i + j) - ) - raise - try: - out.append(packI(acc)) - except struct.error: - raise ValueError('base85 overflow in hunk starting at byte %d' - % i) - - result = b''.join(out) - if padding: - result = result[:-padding] - return result - - -def bootstrap(tmpdir=None): - # Import pip so we can use it to install pip and maybe setuptools too - import pip._internal - from pip._internal.commands.install import InstallCommand - from pip._internal.req.constructors import install_req_from_line - - # Wrapper to provide default certificate with the lowest priority - class CertInstallCommand(InstallCommand): - def parse_args(self, args): - # If cert isn't specified in config or environment, we provide our - # own certificate through defaults. - # This allows user to specify custom cert anywhere one likes: - # config, environment variable or argv. - if not self.parser.get_default_values().cert: - self.parser.defaults["cert"] = cert_path # calculated below - return super(CertInstallCommand, self).parse_args(args) - - pip._internal.commands_dict["install"] = CertInstallCommand - - implicit_pip = True - implicit_setuptools = True - implicit_wheel = True - - # Check if the user has requested us not to install setuptools - if "--no-setuptools" in sys.argv or os.environ.get("PIP_NO_SETUPTOOLS"): - args = [x for x in sys.argv[1:] if x != "--no-setuptools"] - implicit_setuptools = False - else: - args = sys.argv[1:] - - # Check if the user has requested us not to install wheel - if "--no-wheel" in args or os.environ.get("PIP_NO_WHEEL"): - args = [x for x in args if x != "--no-wheel"] - implicit_wheel = False - - # We only want to implicitly install setuptools and wheel if they don't - # already exist on the target platform. - if implicit_setuptools: - try: - import setuptools # noqa - implicit_setuptools = False - except ImportError: - pass - if implicit_wheel: - try: - import wheel # noqa - implicit_wheel = False - except ImportError: - pass - - # We want to support people passing things like 'pip<8' to get-pip.py which - # will let them install a specific version. However because of the dreaded - # DoubleRequirement error if any of the args look like they might be a - # specific for one of our packages, then we'll turn off the implicit - # install of them. - for arg in args: - try: - req = install_req_from_line(arg) - except Exception: - continue - - if implicit_pip and req.name == "pip": - implicit_pip = False - elif implicit_setuptools and req.name == "setuptools": - implicit_setuptools = False - elif implicit_wheel and req.name == "wheel": - implicit_wheel = False - - # Add any implicit installations to the end of our args - if implicit_pip: - args += ["pip"] - if implicit_setuptools: - args += ["setuptools"] - if implicit_wheel: - args += ["wheel"] - - # Add our default arguments - args = ["install", "--upgrade", "--force-reinstall"] + args - - delete_tmpdir = False - try: - # Create a temporary directory to act as a working directory if we were - # not given one. - if tmpdir is None: - tmpdir = tempfile.mkdtemp() - delete_tmpdir = True - - # We need to extract the SSL certificates from requests so that they - # can be passed to --cert - cert_path = os.path.join(tmpdir, "cacert.pem") - with open(cert_path, "wb") as cert: - cert.write(pkgutil.get_data("pip._vendor.certifi", "cacert.pem")) - - # Execute the included pip and use it to install the latest pip and - # setuptools from PyPI - sys.exit(pip._internal.main(args)) - finally: - # Remove our temporary directory - if delete_tmpdir and tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -def main(): - tmpdir = None - try: - # Create a temporary working directory - tmpdir = tempfile.mkdtemp() - - # Unpack the zipfile into the temporary directory - pip_zip = os.path.join(tmpdir, "pip.zip") - with open(pip_zip, "wb") as fp: - fp.write(b85decode(DATA.replace(b"\n", b""))) - - # Add the zipfile to sys.path so that we can import it - sys.path.insert(0, pip_zip) - - # Run the bootstrap - bootstrap(tmpdir=tmpdir) - finally: - # Clean up our temporary working directory - if tmpdir: - shutil.rmtree(tmpdir, ignore_errors=True) - - -DATA = b""" -P)h>@6aWAK2mmD%m`<4gN^}MtBUtcb8d5e!POD!tS%+HIDSFlx3GPKk -)RN?{vP)h>@6aWAK2mmD%m`-N@Pvb`c003_S000jF003}la4%n9ZDDC{Utcb8d0kO4Zo@DP-1Q0q8SE -6P(>Xwfj$MoHf@(`KQCU(&8g71HO0ki&o<#cYNZz>|C(zo>JZGyl;FMx!FrO6t%vRrOrPh9=?L}8oY6 -ou)77Hd@$a4r7F5rryfn~JTAHWO)@Mv!(a4fto86JiEF(QHSJ}y)-GntEpbmcJyNSL0Vx@Gi7c>xAuL -EgIxovfWq|0NvR`+SC`IVq5DSMEVyuc5y>N3AD=LF+DESFFQK3yL&bIL-tGT-b6~%(tWCE -QA8qFLP)h ->@6aWAK2mmD%m`<{zTe%hm001@%000>P003}la4%nJZggdGZeeUMUtei%X>?y-E^v93R&8(FHW2=X;*U%xw2l%3W6Py<0o>h8I>=Z-x4>3Qeu^QF|!Q -E#E$`?b;8%9;(7<*M_Y#j*ssX^r(Dmd>coV;T2Z)}Jd=35ADU(@5Q2r#EE2I)jML=NjONY)o=yYXtEu$-H-Gm-+BOZ^V00~A@_@kKt#R;YX;F~+>#O^V-@pfx`4TJ9IRvvJI8Va8$ENdb=f@y)O)kTy&U -t8+keW`k*)triwXqw@TIWQ=kz7M~CAHT>NbjTlEYY)AN#)U|Z9b9>#fqZ|RKjCp?0)`@@)+QZGV(<*pWow<6RAI8<%7GiZm__ -Ldg`4O+Qpu7fWi`gXwe$yB+-oXAT^C-o>eO(u)G`>R`0FaN!ISX|%Tmbd@B{p;!heuwGfY&xCN-zpmA-{5oleV1B&e-FWh`ubGg0izE% -KZO)wlw#YA$T!=C?FodnQsl@e#K*pC0%ZPUoT5Xu16`yZo4?Mm2gR8?r;Ukv%TPaPRYIpgHc4htJxYu -31A&PNe&h4w2e)^%_UDMfHx;k#%rzESxFX0 -R}T+&50~)Fjo=x}j=wK!TtU+k9kx$}@KO4FHcr{QMdYUqkGc4oVwSXF^3yn{;cse6L;km*aWLjY?u`B -1>(cZ-(H2Eg3N2sONIU#CQ@u5ZKCbfq;O15N=grLo&d;ADssDON)B))X1`r}2Jv~}VIMtf&o5~8eW>) -o|`KIcquJn2mN`9rQZ=1QY-O00;mj6qrs-OWq+R2><}#8~^|s0001RX>c!JX ->N37a&BR4FJg6RY-C?$Zgwtkd7T+uZ`(NX-M@mHK1jk!!gU{w^8(#8-4@qNf;PQHVHkoU(GEAVs3R#m -z9RqqW=K&YB{}V`Uu;qH#rdA`#l^*MEvrg$RUeq(^`6#>w33!&%LQSQSGi)mCS@yFy(6+@QjvSafXBt -f#l>R5_6-+`RD8F?v+j{g`%9kspNc-IqsW`ZR`5M3cvaB?$xG4!+=!A2TE1n4GBC)mRjiUJkSTb*Wjh -PIqbh4o>Vel;#qJIGJW#G4{cL^jb5Gf{qaP -bJ2{7ltA&thiTmQ^&%NG|a>t9YSx=P+iqqN2{L)Ld!LWMQ$3ys9$U-Zz4SH1a%>qWdN*nXrg=@cb4eE -)*B17tl@(8n8q9_t)F6+2#AI%YS8`zFc2_xc>b-X3H!VZ)lxKpi@%;cHhpBSuO~CG%cUwUE5SNCZM^P -d;t3SJwDzvsG+=y$wx5sfa}Y_>XJTLLbrMGnD6L2JOnAw?WiCt>whU&{G&b#v#iedV326BSQJ$*CCP3 -D-Lj|ULUStV7L3Mfrm`%QwA#i5T_rMiz|SkZ1YMF)DiM+7S8~m3+P_7V6fuB1e)%sXRc!-r-sP1X;oP -%YTW~eFXV^pFC#Y3GK)+n3cm=XrnIj6MNYHS}p0Y+?C5S!5LoVd%TX3IC8Z5O?F~CC%J15UQMzjuOAy -}hiuyO8u%@y1k>4ReX01TYqQXC~G>VZ0F5QHKZC(T0=$;s{M_5>*e;#{D6Rwp9c25m^ow8v{&K>^e1q -L1egr3PoC1>3S>rL`CnbLo{f(?9|se}KfDXwOjqRrhkNEWM?tS#3BtMZ-y8weAoG#i4P23@5fMHoa%+ -B<$qikl+6aI_rE9^6aIcxq>v6*CpBKHADdZ?lLr_YwF~0Fb}Jxf{bEPHF$a>Euk~MKj-ylTt4el(8|v -~A>_t#b)k7CAkam01~D!4ZNg|V0x;I0s4S-xgno2{7!$D#<`9Aet(pGGSsjk}mi0tgmf4mhCOyv@QPfvQt>_eA0?703f|_rP6D1N!Ocn9Kw`v-2!0BW5R)9t= -|MJlMex_Wz>O{XXrL2(aS-b`elYMQJ3GmYgjKpOBRsqLIjJnN;Ne!vxk76@28w7{%c5>WJWlH -x}Qgmfr52$^nE7|5IXR3R?St9NT0w1Z$$f5 -N*$*im8@KaxYz1&RTA8UTBP02<`s={ajqr(_UfzN$&o%Sd3yqrMe2j8Z7S>iX^$5`CqwQ_z>8FoBesb -<+sV#Ke4=kCz!qe90RD=eDP(v;?$p4YRS^-Gui1a%h8bW1bW(@|n>(JN^}VR{97X^hQ@05cBwnL=+=# -ssX0oe=a*rYJHx<8`l0lEn77Pm?-JcHO_*AlZ-SRW>i5^(g2mD8vokeQd@b_Q0tX#}OK(TP&PVog6P; -xeqb1qOSZxW2>rZL@2iU(@vyIJo$kF#9@;2v3@Vn>|OJNy?@snVAt|e!M}ZT_a4jr?bo~aH`muUUp`- -b`uySUYBf;$1bPqT5KUw*;Gu~)GTS^b|$7G*~s!U_GS -Qcl+`vR_F%qBjetusFfk=S`$Iee9qVpMg4e+;&^fBT&<^cq-!q<^@r*td88%Pm*8%57eRS{m4B8pSAm -aw8I^8Pi0Pt*)f^zoILHT;`4AS99?MKrHbF|p-ChXZwy>2=b`1tD%cY}GxG9K<AD>ZFVp6F_Tx}_y>SOQVcg1L8{1nA{@yu{@9|!|3}S5cRw*8w-`_S3dBzsLcgunx@kv&<&BR(Nw)2i!uo5_6uh8)BCFt#eS)UW6pRI@#4)htTEHT3k*4F| -FAouPcT&|vl+C_90U#K$I#uV;AAR8Sa;3mOKQ)_Cwzqz7@(jhT8i7+M+Os^5 -ZTjVUa6j@+;Z-F9N@&2ZL=O3rAo6LB3R45XM~Kt|e3(=i~4JnJ^j^$gbQ@CDB+3Ub?mB0|&% --IbhlOHq8$`FDuqEi^x=132U060enqnG11okVU%nptw#zcRuu_3HIScjn77`g0~kCb0!wb>G9^4A=ao -nJje22g7Rj4W!S6H+p%I=vF`nk6z`XRmPYW@B5qF2Bi6cF>3<$S9@-sz?r7i2b!(QhNCUQew8)3I<|G -vAuMtxDgJBmVGr#fg46O!2ri&i`#(@i0|XQR000O8B@~!Wn_6G(90>pb=p6t6761SMaA|NaUukZ1WpZ -v|Y%gPBV`ybAaCya8ZI9bF68^rwf^a^pba0ejkjn=HF3_f%Hn?Pa$fgL2#X_JZ+EyZyI#OE44f@~jnW -0`N*-p|nhx)@}MRDF|o|(~XHhU(strD-KmK(KGy1tmrW)~O5t}?b3%35u@hplwmvRHMGrP*u>z3Co}y -SP|evlCTOCF#A=xv@zlv-gs2B~4wFPPBJ3sf{i&Sr)&kRLh+TNef*ot7VzwX0J;l^Ny;L^){_d+Su~c -^_|I8>5{V0?>mZ@`2W;ZRwl{J4lKL7>O&=U-Z*8?Q`Bl#rFmgHnm2g`=i49nq^_|MP4 -5?5C?||4v{3a((^c`o~k0`>j%CNAg17!&muRI(uG}id;4q{D@GkPq18E2>h7uSo+UZT^IELbM>Jkes1he*3XQtt$}390AX&>Lr5x5YjV~@PvQj!HIL{)pdIxXCUOi;mcU -s7mtF6qC6@2fZsI0j!a^;XFl$v?#-~uOWV?{1&={2QLBkPzK{T7w9a_7k?ObhV_^wt<;=UUYGpI;?Yf -;F3!X5Ov^+#$T4L^iUy)v-2^y({XT5&c$$maODTk-x1%=K27f6)elr>{Xt61>8dHw?(#X^>HM9jcB`p -gK%5Zl(}&5N5GDL(IqQzxD-fZap_EBGld7MnB3^{(8&|XrbfBA3xfAxuGS(&;EOsNnYz3U#@ -;x(`vD2`5~`SRMz4R^hPAC*rpE~EO9E7OqS#sxJ8wY+~3;ahTU0s~U=04}E;-@%zkNxns0l)+IHPG~? -AWhM*lUx#lNqayExCbFu~Vx!q|#?Fs!$IRygx4SqVDNTM}irIsfuxA;EF}l(j+9^i?7dSQ?T(^~vA3v -dZT$ZKqE-zf@Z-^8Q;8;o<@N+6_Y*8Nw+)?K>;Ut1h?Edw1E>`d_^OuBh7QF{=Ygs~py285%{s~ie+c -*JWbe^w@6Y&!&=bp&8W31Q8DxE=y593b0hmX;x2+{*a!%c22WGs1mPx6BEy&66qI;RM6E{?# -SV+&IN7QXd9hKhj^Z;}uko@~bZBkPASLj!vamyQ#!?%RIMdv|Ocih|aj6{9?@3=uT%Yi^(d0a$bj@mX -6Z}Y6!0q|XU}&Gy-s0QEmK(GP>?P-@- -}FGCAq(8BmK)ENY%C>{@kup=535S7)WhZuWwBh=>9d(yc-jKdNiNgvm4>y+ETPyHP1B`uE!<7niVo^W -U`Ct4YeVdkf{o@?ah_7C}SWH7MC##ScJ2L7a9WpkNjV9b^Y_z^_#iqpWgiN;`Q%i(y>LOQ&~7QB>1siU)Xw)& -SA5B?S;v{Y7dN+H1qkMm=8^iV2h!C)NMx!qL0)sc2&vioyyrq9X7RUf2CRss-ic;5$@7X1`}kcK82*9 -y$d0AbKjizq^@o0a1fvp=A`_uTFOD;==3l&vb;Y%Z#JwwHujGsH@(B%aB|{o4y7L{hr2ugEg(4#n4T7G4-3+x`A?N`r4sYW6%Bb`^1;659gMKsrE8tRv)CWQ1q|14)$28C`)1kBhXH6Sca6>ujIgg=u&;l-TP1 -#QSgPwbOR~qx4Mw4b3NIaWuf%Y)y`nKIVkspc|2M`fvLF};57QC1r{D5ASFs@jYN???8fR~<^ZgwjiA -+pBnG)(9Sp4=g9wF=nsFxg6M@7p15sJCIc`fsBWag}TmkvwWaFI*RL^PAGV_;eXgBK@Qo?Ed7lRa0Oq -mz~-~^G)(ik|gvJCPG`fQuF0D#?l9lXX#lRb_m1OGLM2E*22!mpG(ZLbuLRSrzelcSF -zls^7D2!n9@9fb3P4e?(bc!=K@80lO0aY~&=+HOE(v^%|Vdc+GV;)avEO;n0ayM8fn9xseTzaKDiJhf -q!_S~>k|07&q{%;L!7+h&M4kjD#Md;FQb$aBPe%k${DqM}zsfT_(@Js)#FB-(eqnqvgbLunUF-t0#LN -{vkq8<2HKmt^y&wVP7;$!j$;(N|yhMRtY!pwZY=Bpn`n{6MdI{PxQgMYe(HX5mr^5@4CWma)E74;=RW{-kU^pzFuXY%3m -@Y%ll`DXgLKFW^hKW{I`U$!H_eZHJ2GWj6~;UQ}{i_b7fylvqE>^t?uYoBh^6P^SGQ&Ug1=|4USO_gs -M48cA>7`?T!I!Xpc`J=RZAvhk)jax -JZZM0lj_`L7FC#_9G?Z`u~WBo#Mu`aBu|F$XaYs_!mH}G(N8%Nn=NcVBzXzo34+k_80zvnYWgDLITog -qi)6&Q4O_ZW%yntxp-0&jeg5?{YPLmf1~ek_@IV>W -aCyyIdvDt~693NQke{e28$?n5 -b8h=QVP{l+0I|V7sD9St?XgNg%UgyS1o*^|dgp**(IFr^Fm9xvW@%OE0TpGYW!WG?JULsA`rK3C~3PZ -M6cD_G_U=Bb>oXSw``l$kU>Vl>8KJiaVj08KqY(d8I_Ip -lCCHW2R=t3&^ekpeE_a%INODf^nguUOEnV`Aj5&e0`cUliuq*ONZbR2bmW{=qQ8W>pfJf-=sWuC$$vs -4)1_W(p%SFixu7|}Gw7Og~mLGQviMzfD&7&UstF5kXCo3b~2emc6-%ayZIuWX -iu_JiMNUXkRD=p+ONjmwnHl{7Bi+F+$5AE9j3**IiBBf%T=^-=n3T>+&uFu}h0#I%R#LV-EeL$nSE=3}jGNEy=F5Z6s>|z#Zch+OU==PwQ4f -|21+Ip|g-=EC_bX=F({G+VuEzcJ0%+Zxc{_OJOEC5Zm({Lhr*wy(}{QT|Z%kx+9%kzsfJ3RjQ>>W)3g -eBh1aCeE12;CrRH9{#R8HnD8x3IYR72w`z01V~_a3C1(MyIYoBkiGARdEN~R*Yjz0vBI`J)tk~BmoOW -Pykw*5PtC^gNa>4>HS(NrkR65cLGcg?5U;x?CKnJ$ZJ-ETPPOT0EN*7%^?8S*&FzPcZ$|kbx{e@y$+A -PuqbPI#e$Z|Hcvt6Fo6i!aA;QxTxNL&ex%qE+>Hb0!V>`nLx|G2^i2UP;Rj0&S+h&nrO`CefxzbOfIE -q!nBZ!U($tE;r8dkE9grl^GqhRCYp(YX#E@A903{PJe~SwAf&38yf~?~hf_}D~z~~}F2|<8@NEf4L&n -}BxH~`Ri=}l1pa3ijgv*NI3klL1jGM=RY_QDe5s9DhfJw7pLNTzYKCOGJvJT*e^oM^|6vl(B7e=;{u*lnpmJ|VLppb@LF-?C1~ha&A}}_j9Cr1xas?q-#DJR=kuey|17Jm=kAp -teA}a%Lli!PkWANW$a2F-3E1uk%>#wPk7F_DHw?~s%PuYAy#$>*u*$&C5nfs;H4Q -5!R7oA+qDUrgUIq9ZQi^ZbDmMJ3+a`pMkAHWrTBz_Qj270w}fItSk*@;K -tW9ARToS#f)3y-VXKp9xpX>PA_sD_{c!986vC3{qdp)xG@RuC6e?vA6L$**mjYl3LL305(zy_P=b91 -y8n*_yL(sZoU3C#$r-fQ>C>Cu=C>S!XeK?-~t=+UP1lc*n`cH)PgVe}W8eu~=w^7F>Y)c&1SbckUr0r -M5ie(bA{S^Ep(kqP_h5|?ja@;&AC!@l%A$GquJh32cG=OSvSMV)~m@z2;FgLVIo0G$)-1S}w$f9gGq; -jyEpG1S~9am$^?;P0aDetw|52@}@C=V_RNJx@3ZVjTe6O?hUjwn=L6cfF-0}>$}4`Rz3GQCDLLy6n4V -6*^C0l(&VqRj-{jA@CnYr;xf93M0PkqbX&hU1o9{7H1SW(vJt!GJvp>I>sVM8^%AP1(N -Uvm|krEULd>@$Xs8O+nPH+VEK4)hs%`?1>W<^-C{xer|nvON%i?XI~qHoUrxfoB`PLQf%%O>Of5t2*X -jT}%3a1pYij0yUspm}L2wIQ~d)`~NfHcLE!VBwS?N$k}uh-Jwm!qKb{E*9g!v_7Z9=(KYX{GHv8QM+e -9%v0>r#n%E8s)ruHQvBLw~?#&-$%W$Bl#+sP#5gj=I*L^}m%#vs=OS-#AYfYL$cf--Db`-@oWxekaF# -}_WNT`1N6B>qXz}3R)*FL@IVIOc3Y)*+!jpGyfJ_Qp=uIr+t(2YZnk!%x!j=1n=u*AL<+Q-*H54Hxx@ -(_IS_5!xoU;E*eEk40ZC|xp0i*$FG2^`AiGMznLMC>XPT%o?H#|y^D+0Awp1wB&Eeo~R$kFHMa1%&## -^!=I}e@(VkC8$0*-GG#lVk6Z9s0JOoC5tb5-CB-hzAVDv$4cr9u3(Q96qm~ZmwUv{h#5! -#D=-RVUwneHM@7Dpo0fi9sHo)iI)cyCQ`S^FWGRs^N|9U333kC4Rku7CR`Gxds2M_!Oi%Wzl6N3YRu$ -VCD|_=}+d&$Z<-=7emjg9J-=B^I94I5Ga4S{-Amqxxo9S@d=amMsSHN1Jxf(}U|PD|oMbq^-Mhmc~+nonTd)iCI6&_pJNji&3Mb-<8T@`l&+jF^&t=*b6b$G28ro4%>AWt?O$ -~FoBV!&7M(;itmdZmS0MckW*cQ5?ubyCWcm2ZtTS>rpE$1IMpKQV2@6jis}mMBjlIWeY@^Y{Io|I-{l -p0$&FrMI=AcUMQ${1aV;2XU-fW8!+C_C0t8ZeYMSfD#VIEM -dgjSw}H`|&e@Vyqzp`Iqyv_bYezMmkPu8XWaor|2IFA5#mVNpqjzT9S51m*dl -Mza=1I9P`IAzXPoz>?p`LvE-9P^Rkn(2es?IP%0{5!OX8{5kmv~{voa=r5!jd%y0D}0ej-TD(qi-(Z? -Gb(bp@M`chb@DiLTU*NIHk}UUr(ouec86;phh?fo?0X -E6O)D6KBniOx7Hj~{nn3zj7DNI3KX{ePo7b-Cw2r$@&<=8}pl2;#eb|I2dxM8foUKHT9WL0oz77gGh$ -B9b00*q}zV2_ch9Oj`m5#o{oP;M%zXO -I@sXZb?@d=*Mr-eD>Y>^sXmj`-~jIELc*b5!}Xi9i;F3HW83IW4*@7_Yn{-;Qp;^ll-wH@gILYhw!w! -QaE_yg>T57C$0)&_!jn#QdZI529V)XAA14@VXfK+~ooUY5WUmX`pXgmH;kt0V;reO`?H2Myb6@zt$(c -*j-i^t?IKqKj#$$K2QdFmS$+2S~*eY)Wp>_?5$~d;YAkw?EK(>Z&eYbW6iX%E%rt`BK4IPafV8IuxzI -UXrP`h{GQHQ3h1bG;{(I=9**tvrasz_fhnm^!Xwx`CW%mGpN>`q_t1QYD}U;#$t!@+yDpX%-n -s={#FFJvucRY3!PmbK4af=Xpa|r -`Wy@UvY6sk*dNdKevjoVF7O>Z;`rvFM;x9B+zCUm(7ZYWsxZfX;ylq;<)ua$UhmnCnKQ**iL_6OVy~cQ2EEPSl7#CKKZ)SJf5c0g`Ac}QHi{ -MN`letwJfLCGk9&9zJl+kbzSxhU}g}fjDiLKU8hybS&OURh5P6nXm<-ntDA&9&>;Ii8JU_sJzK< -33+f-~EDBwoL{I6V}9`Qg>Ow3-B!MFQQza5L)M -Lq>0DS*a*w~LLqg_W8YWi3UskklKyHH+M!(#06~4JdZw=J|28tgC44MOkg-C9?pPeUf|N7?b>Fe|JS5_E6C$D#2023DJGL|2wvTAZ*2-Evzsun-wYLzz -A#aO_iH|Y{~IW%cAzXbw2B@JNQ);WkH;6w@kt -Qz%qcny8?hW1^8*y28fwPHB*PJV1a;1(4OO$#*M8*QH#(2meEbGv}=&+}SNo3h^Q=(ef6#rol|Z~qGN -==9`I$-DQjU!T4HetXY#ZhAe36qPAeI#Tf0JkZi*Pfwd7#up{2U1ESme0^U>ek%QLD+{?VUwrT`EZ)?ueUce5N!F~kQyQPOWS=0{F#{lw5(EP{@>hQ^vdEPA6lNfqFsWw&m1a{$*Tq#h>Pk!^w?|!#;a+aLF{_*VHo -7X>}!S&)5zJakH>cOG-G(gng!vNNBFct%CQos|^JI2IFlFv?d4yp~%joN5;Fy<$wbu{uCgHBpX6b(d0 -9Mc!V!XVHF6?XF>YD_-wvC5m#6ffK3TwOBcD{*uL{Qs7q4i7DqZJi7Opx^fC5fs?g_Zk}n)y5kbIsv- -^$ZBJ$i);q&6be@O=SD86BXD85^m^i7ryHQz3-0pp>nqz6Y!dl}a2`akK|F|mD)S-=#<~QajXsWOCw# -6&G`Y_V|vAwFGBuebiNbx<$YW2Uc+-I=qJKXj^gqPU?v~s2u?C!czw;-yb0cf7}$HB-UvMnY9en -(S#i%nl?Z4_Vty19wKiUK1vT-s&2nq;Z?cql$zKD`ReEAew)MVd06V1bW$Hn -{{g0Sm>1fB3uB1j|w1RoV#sf0&THLxOaTw5YQ#!kx8;!zoVkH~UV(EI>L<4RD;6HsHx&6@v(FLyM-~*uR9^n8MOm_s -B-2zOq_6V1QX|)c4*p?QE_LbT_%L=SZwM_hd3*TeyHYGovzQZ^GgMpc>s|HLgW!gjQUe_LGYTFJ*mNU -BjJ)f5>dC3{v4pV1dukLPFakNAp?{9-eiDR^fF~-YRXKW3 -P!M<7HhqBN1#H8pas$=zlDi;CE^;c>&Jlbh$>`9n2mD>koa7m#Q372oT>HFXAR$u0ynoQiI`Ffb}W`i -xq|f=E$*CNl3CEgp(mtRDe$o$g;B*?|B@O6Y(x;0G4-5X;QS>sm4BJ&uI(HnS|&?MuWF!Zf5-aA&;HX^+2w>{r79ekp;*6_Q!#Qk{n`AO%w=7g)y;u9Vad>+JX|3<;6NBEDEm)P< -M|B18ps!|cX1^%j1Sc$S$N6}!6a^`R_LMOIvl54q9M@U00boq%53s!X^KzBug$;j4CM*v7m6r -lcBS|0Nnd&jmjZhiB))dY~cE$a-m;iu0IEXIh?z8Twa%$ -1Mhlwu_s$6Ef~LyTg?8LpWZGsgHXeTbkje$j%5XL61qJZ92bW@!Awrqn?#c*xPd*mls-KO7(-#xRq>- -QOE2j2NkyzlCE<9vOfcx8H=Vo6^2P1y!ywu)pzfv~cvSdS;E07o0a^aC!LjO8itY17kybAC_ -VTGe9;4~noPZ-+LAR-niNDeeU>1=>OS_nuO+zr4&9(95xr~PV(86o?OIXKoqq`fq9HIZ4Sk2f@lU}GD -c3KX)8xNkTZlyKXJTYf9Toh9}$`yQ4KvC^zUhcg-t)#M;+*()cM0L#@bT-fF?1NS264u7;(a}OL>-#H -@>3ZUVABN6Mb!d@@Fu-pb(h#s(VV-AgMYkyWj>yQ&JnF~C6CdN5l`@y)?g)Sq-181Beg&cQ$g?!g|>z%&|+dHUQxf -&Sh&TD3rTC+}QCEAdn{7GHgJeVf*E6-ebj<7wRzqy$DQ(+LWV0KqZtYM>JcWm<7y*4wloVe`}4qZ*zF -B1M3vb-A7|h~DY6S*J@2%x4gMhSqemRtcUUiC-Pzz=y46(34ZuUWhYdODC6;rPM)8P!}!&0|t^pZ*F5 -?u%PVXfj~m#a}N~m)EO|hw$gap!jL0ug}l9k`|aB!;m$4dqFRIP^4r3PWDu4ElrqI(HsgHdz%jyS`QjNtkYZL`QV|94HEDG$602E-A-9gwfj=y3*-{w!+=H)kvAYLi -li)B0!NEegMJ%Bc$Od_K+{l1Qyd(}B10+ZNuo}-7$hNDl -GxBq4j4SsVPJNQ=s7G?~i4~C$L`pP7nM$XAV{zgcCVVQw!|^^2&&Jw+{#t4YqYd^~CD^zx&UKzkUQ0`+c6}<}V>Sm$kmUrqHg~;T&%S@VaW6uW`=F*0bn5%o#!n{!$(^t0hfTPbvq`sU!9ZWE9K@Cv%`hybt3ET -841O}uO|?eBpLCnG``hD%nsI%DMA`KnQ9?O5M=pOXo@1<&9=rKo9$b-xz+8kU)e;=NL3aWRn -P!+)b<~%M6n*Nj>fnS?*uQ|W28RJgUBLU3!8p!S%BVJV0_5Z -0DR$kG8lE+PM4*UMuMRzrw2VomoI{-4Q;Mq7bs=-=y^JmHt{X8pIS#u$AYroz*a>+3LDtGD^Ni*X|%4 -qkp&-QQ{ELwIoaqVF%Y0)m2LPcbLhW9>Dens)w%*f6U>Pf$0)Wi+Pw -p%O#vR54Di&NObs3RadSZS0mek*6ZH)f!@l-l;i2kAsra@2%{#+^VCxY&co!|BLT7s&uQd|X%zvRqLnfrCYUEfIlF= -I3BRY8B&#)JAPP&5G7Dwv!m`)K}e|cM&Yi5EK>iQ4X@SL!S0r2VH115xqQoVyZnfo}G>QX%b|yc+1vY -4=it?WWCBM{;3Ec&6}ensfQr3Ko6jBDab)o>#Q7$O}WPH#EuU>8Y;2>Qp}d=JRWqVR7mLXeUES25N|v -3!HysgAO6?Vr^#`)?DlbN77@O}jf~NfyY?`#=dJW352-_C%>3xqMJ>wpG9z&UOT1rJ0oS$g=?zM-BO?s(WM2?Y+ -`6kKC)2Pz)w>;!kDv}UzZkwmvoxXT98bi+msR`WWk#uXvG@FEV}wPU3B&Ti@tr)IpO!;a^3aR{(tFPO -Rp_Tw8%k^O0UEr@;>{nAG&V|J9L^;&^4!13FGLokLS?~Tg226utqOncyjvgB6%`Tz(Z-WraZwdO8@31II$_5AmKi>cSn;tw{6P^7*@jVp%XzloLr -mH6r&%GrjHqVj}1eszn0+B(lDlm;vFo>M$qXe?m~8U$O7g79VQEwU@oerT!CBY0b9UZI>e#Cx?bA2hX -aD4qSmf6M`}&kq)VN3i&mrQPS#_Jb}o>FvT)5Fh8^F6g;F)#uLSi-!y32Ezhm*6gRfcU@Tm}{(^6{ul -tj}?riF5@(F^c@;pzS##2X4}w&jNlZ_dF2r)>3lUFpIeI-0*t!T$9}g%`jKvQC#RU?2+}9#|NGiT_v@ -gxSCFja(}Np-NbVA*|QD6e;o@3cVJznisi=dkAnq*0o8M?tljjk5}o>FsbveyS<+spd<2|XWxqbJHeb -R%XRhwRDc3%14)g(->(qM)>&8w^!Yz%a9gol_I0AN^lC1X)u!+{)Z2;xkVRdmV$Q4Y+z -h;7Oku^-dt6UE^ENCRO_t7oYy3H(35CkXAgL=Sc}CLEb -~k@Q!M@BfVqk}S2*xMb7j+ht%AHsmw84s_aJiuxs{j>x(ANO8IzK`di5-l6%b*8xz$x8y?`Zr`MGw!-a$`(ioVien2MKf?*LrD#29!0OF5bw -IlNQKAImf+m4`Isx;gcug{kvD-zAF#ft;8*+D8IlI1_;!#CbeLAgKJ)2bI~GI4dSR+OGA6#2L{7=%U* -N{)@M@fCLAe15&+S_phssYKIGBX=G?lCNw6@dWC&7R5T_2>O!b1(4FFv0IAF9*Cg2*Nd0*_CMMaZU6N -6T!*f`n6s@WHxkH99AE7C8FzoRYorbWt&mf6&E_}3!er_WDp$`f`OZ6y$zgc?6>5pp$wHf(o!L&s!Yf -M>~*7CNiZ8l)xV`M0bcZSVO>kh;A&Hq&1Nia*a)g@v8rsd)Qr&vS;lW}=zo>p;g?2qyM2)8)QWd2c -=@T72H~-|urfE?wl{LZP+m-j#VRuETTvVvX=~q~ycV(i=-^)h3P5NM#>Y5O<-{a0lbxrOiKH-2oaWIO -2vB^B7ErL&CQ+=UCWGY|B1~QhF=+gwMZ7Om+mx*}|4lx66g%mrqT!*5W&Zy9OfH$$<(oqUJu%!jup{8 -|SnbIh&YA{(`1${M9d89LA)M5!&Ud5TNr;Hs1!!i-_td&V64PELw&0)9V$qMbi2~iVk{|(r?XfB_vZ+IWF17Mb1*k2aKH>8ii@K`l=_sZ$v -M9*tj!avsu~^C>nE{UW9V-(5+-pASk(!ON$=<|EbP-=)Yib^(u!XTz>_iq0UwR{KXxG=X3Z3TUzd&S$Adw7>kRYTiWa;ySS+!Op^7 -@>;FDr(vcraYBwn2T<%iQluwIaOjD$)_VI;Sh|x^rw(Wo>@2pBW{oU8vSSn%#voNde0Ojzv(~bzSrIvo=}L8PJ1d@1VT}Zay%${j5!7of -FF4x#+$mJpX9cjLdiY-v?(=_;mGBRQKf(nx;x&OA> -Y7_NIPAu=X6E4gOlLDYQyyiZnxpj#e;+I7(|ifMI|{eO!+)7tUpvBx9R{P4_wAgQO>WkE!eW>629Mi* -|zkgyNeN0Gg&jb=mQB3fh%i_foujaUy~d7BqsK -O1%f`Z|>V{`HBTUjGd+s6z!xOV&0*LY3j=_&hUN96BXji~e9Mkf3Ll_O3c$Rdb(hzMa7dE#hs<>bk<8-ca;%f=+2 -B?LARs-r!mhEYf-zH^UfJ0D#hWnhQ_5KcS!MK=sBNx9MnT_{@@}Ets8v^zV -b{m6S#-bcLg>)A@k?K5%)FA5ui$Ug!acqOqX^tc59)fR8*bm=%HG1Hjk}>1>84MmO>m-ggj<# -2@S>J7=$7yzw0SMR{u(00wg=Dj6#tCim_W%;^sz`sP4hYsr`-)+5Fgm_`_soM=8Lg -jb_Gq0=!mO{LGDQZP=}6Tw!oplvBsLpc#&?pmq7K5TwwqN83 -P@vn>!T1}^skd4cm7T>Y?GueJa%)AgHLvrQLP8f;`Y45jCRmI~jhG=asrPUkvxlYEtOI$hV=BisUj=P -%MMLyO3yL!X+9_E?JOu+tV08JYTYjJKfrX+u@MV5%%;`FssVRySpC -neniMp$a)=BR>C^$^}t>Y7IVeCe0|w&8FuU??yN*o(aD!EwWPut@KkK!@ac1C&kcjFxPlqD$rEC(z*`Iv_G&MN#W@$7bE)l{d6?Sf_{0DTgNgf` -sJX&RJTuiNyy%n}i-K9@jsa3)COK?G0~|*%#BDrwk;-yEAszdPV=O`_u-1NbS3r58ZUfUmZSdAXwXgm -NBp}JPV@F{&v^Q_?F0Fa8Db)ve=`bXJf0Uinw=*+b;2pd?R>g+1ZZ2Rr>MBx4?qqj(*b&|Fi*$H|<0N -gCOqS$4W$x)o7p(IK5j}qhOd_Nf0}LGhFW`sb0G^0Pvp9&@{>05d5LJR159$u8ovrr}s -{5P`RiZUmU}hJa;lZKEAL5Za8+YVBTA-AYI4Ht@o8?%#Z-eJw`|{vLA?3MmBD7)(2p=_97TiMOw@b|I -%_wc%itJ3e;j!k#aW4Sf&5`ktn>7iXhrSqLRI;O%5hW4=K+>c9(o#>7=Zip?Dks@-Hs#!3;VMb36dJ#eXLFPdW_GC>_d20@}7kFVWIDa&{vH- -EN1yJJOcJKF73f-WtR4T{wqg9f)5`tb*@pxdWp~3!_XbRvm-79Y>k1bVY>D2A --n$m*jt&O((YCxW{No=2i2ErucJtpww6sMuJdptAY@+?Ab~7MH`2IAFiVwYQxr3^{&>WtFft}rxCxVE -rxjQ)887^|O4Ui$+fxMymhLT+7%IW^SKfVLnl=0OPR^emt<$f=?Q#S%m1+Obc@8(BsxGE+(3ml1$ -s;nGX8cL39C52D<2ztp*_`R#$N)5*)F`8XUj~&=~CoSPjE|zUW0sB;kkv0=hi!nw@2V-qZew|pW^7vs -zOHS$8y|8z%7U6=3lrbn#q3w1jFdD`$>T>`qNSEF9+aDm>@}!wZ3Sa@-bQGOWue^gvMU^IR?8t -Fqvo2YB*}lmu&FWR+Q1rgXtT{>1R7QkR^+0?m0Bxwepd--ktnEzG1*OnHiZKa^&}#(a59;CXMoAn_Qr -{2HiY#pN-@jsSf*Kf46bguJ*magDtc!=%mu^u23=BBcp$fV)PW<4n23L5*rK#@0l6-7aTCj!*U*z>a4 -Xam|0-1aG~P(_DLL{5v@4fh08)*k%1vc=b&`T@70&uynG*sm!RjDixRc=_qE-zZ+@CXV -QI}+eBketNTBb6r%3VGMjIc|It{(6|F8=t6^kF(s$pjQy}q2m8aR!)sFaf&x1v$dt*4mgqJU_MZg81w -F#f9hV_OVhSzHMq1vx1jY@aXqpcH#i(iY)IxI^mQ#}!TQsE5{KgbuiJZq$erTT+ -SFOrwE>8OT)Ew6i6U0d1dUuSLoGc2vZ%DRt*bzkc$w2k~;cHQ(T@Po}Sz1qJ-wG}PA% -Wo*Pf%$$AtG+?+;Mzt`_<)^gCx4Seb}vaZ&KQXR&?mZUoY?TA1MnZoAAUuH6R_-dHbg@)Vmo|MLTDIq -!gA*JVHSV_*pdUX8)aC2MITv8<(Ynz04{xCPgT;nz~*Xq5Z5^(sY+QLBC`v3ao`6rJhRZmHd2o2tF7a -0s(G$EMoZUXYed%4|aw@{>|^DPYi#e6%{S+%>7~g7Q0W;cdLz4mueH?tZRML;vcnA^7EVu@@}R@N*~p -t0R&Dec}pG=CpFUTRNEj14N@n=}7-?@0aUdS7tD|D?y&n)bt0e^`{mnPJQNkB!O~?0Umjy+;;Abuy+z -*wcqyp-n4Bqt#dj!adPJISX&=&XSK88i5s#o$?I$A~lNTyidze7S64{t(juAtsRE&qnlc+bk)Uh+7uPXI`>X%qOtkav -RnN9v(QF|MwA=LKZI$qfD!y($g~D9ZHJ8PbezjeYoTk0pePxVR6*UUvG=+3d|lwkTx4zt9MrNzbk2@E -0Enx0Le4E!!BFCVTIYD@Fa=r>m-mIufFF^!Kv!CrJ6+swa$yd=%=l71Otf`1zWKH^Tl~8tVcFP6w89o -5#re0S)_OpL*S;YZvXS1Wfff9Ssy6)*eM@#%h}x4BAnLu_W4s7SZ_MBPb-e+Lc<^+)<=-3X3uYO<3Z# -$!#kO(>hyEt=s=BuPY-=}Dyc^ft8P~B!oPFgeN)1d8@}_>&(ZWsCm)#_DQ@apPCb=-!HPP#;Y!;q%Ht_o4_?|*PB;2Cn=qBKa~U=R(S_q2mm)}%V -Bj*&Y+i?-TiA1RNJ_u*2)-VP(sZM=S4H<_cG3ZJKO3-p4a-Bb0E2A57A$d(?7wc7UkAv}tiK0gY@E5y -5O-(!^=9+kiR$(|wg+eLxh%cTsQ1d+>(9>X!gASq51LIo?~X1^=gX0}UJrkk$ZQV#1JZw%!@516MOo> -(IhTG%w=ymATIbt|mt`eq>*dnKoH{p;Feu8Opt~D550)>K!oUUoR8nCG=d%n&0o&WFKNpm^Yg4X=<|h -c0`^C%_1g~efB`9s#Pj|*UdS_@JD&D^>Kt`hEGqc=$n+=xe>Lf8_oNEj)FY)3V{K?ifg~7q&}=u3R*=oF@*oXjD -?P9Q=G6>Ml@4b?7!_r|qg@7@9FtG9&V0#roLDz3(r_>01tKM0y^V66M%6zcF@Xufm%05lLxX4L_1J~3 -*KX(~b##Dubqy4NBLg2J57T+BeQt&y^>lU=wL0{}hqC_gp}+PZyq=~I1JV!!{_!vml&vGML?x`eJ7^s -78DE~ec|4@w)i&QWGY0jBHuuY1WuTAe;GH7~_i2Y+;VIsh4z{b$I>Bj5042cp*LZDw(;CqpY|)MPzdE -va2DDKzM?`iS_7_R~8yW74067A!dQ{4jbJk;+BIj-?uw60ZOy5=DXYYF9)Sx1aE11}<4U4>tSsI>Vvx -u?9;XPRaHfwOp0Ya}=(^m{g2JBc{!(`ibLn2_H>g&yeC;;wQyyOjkXM7o?tZ01%WHY}8fe2XQ(5a05o -d|8z%;+b%ZGD~Ml41ADcN&o}qSLpG-I(?LQy#riYNpiLtS8bwW=)Mq(&-g>5~ -+$(yuPCU{hk?8vR>>Y#eF&ho3$)*W;mRA<{5G{8eM*gRbFaqoT!{iM7b$!wcgmO6uFG=1-3^{{v&wV8=EBir -?)<86ic|nk`~!j4j1di4{o^r4RY?%u5_y2$-*hWyk{G+u2)TigpK2>aLB5{(aJlTo -mfGKNVQQ>+lrUUm7f$QSaY5hN+!FYTqOs*+}2)hl*p_rMU|hH_B4^D5)WRIuq$W}*)6ma(u-y>x|ZLy -LnaqC%cOZq?Sn4LN~X#@2rOA57472YOUa9kmNwZ*Gg0z* -VpmUUjcdN~W$8?-$toe^B?9hQ>?h+-vz0S^LdUh~)pz#Y*ZF^n>6VNTwO -RYZQ}fNkJ;^joq5lwY7=ZK(un+$a(xAfxxt@b8${(vJ}n-EstvzCbkO!v)m!WS1TPO9d>}s)}mqy{W~ -#9?N${iD_#nR%nV`~dqJiOkyz!-7?sBoqJUDUl2kULyQtKI=KWKDp{nOT+<0l7!GW?B`g$ -Q(L7{*%_DyuF2-XJxe&9)<9!NocK6bUs*=wjDO`m@S5P2|Om%#ZiW*iLb|zRzANCK#O9U~|)s&6k921 -V7cc^fh_y;JHa&e)(r+Amt-W}fgwCgR>($X^rtC88$yO*7=&vy8#h@0DRu<+Esip2;C~PH=N^!^ -fIf;AonYB3=8}QFo`Q;%;321Ohh)!Amy&o}%W25#%BS*H0fGsD5kaUCa9_ExUK}Z*pu!I>@GY9b=!ew -&M>|g^qYFt=Le=qwg{8oiPIZZVjVX=pQ75)X!IUjB7huh6t$BG^7^Ae)pq7C2`Dof|i2~#NSfu|qp8V -(E@gx!8`PW|`_9%wz`rgbfm2#?j!8bjZkz3xNcEZ3XTx68mJE)z4Z~=E6OGlVDSl>6D^^LFnl4Q|@1+ -isudK#Wi!U5tZYqcB -{@MB@kT%3O;47LskuvO;}WooBnjl2Nr~S}Au7HD -|Mi_E4O=#&Ny@{fhF}Ikq61ZFGeDy+>b^fWyM{jYq!I6u?44*jo*wCGW@2qi5S|5%>!N$VF17~x`p8i -ipm>60cY4X32^}VNo6~v+kar!+ey_08`;9uNli|3f5n{4%GHg_f1Dm- -*fCWLJscKfp3nVN``BSB1qbx5h|-(C&wjW5ko#V<|7esfr?N!IyRdr!CHZpv{=K8QE5EmD46bB*Dh~E -&QnmGy>RouylOuIq6aKvcFcmorxTmcpVm*SZjJdEdbq_|odH{f%T=sb6e&~(xCjC9LNf@j%X&o -2qC(XkmY{FDCFtg8xuHYrPAq1GCtT8f4d~%u(CBKLxNw}EQQt*apI9<@J|2`?AZ@%kpR4-(M+g4h|;$jMC;HP^om}1v#cx_7l|uAa4Qs-tS#L&2+mfjsE2#Uf=&oAXpdVN@h -WN`>;5`Z&cTdck67>1cinyZaDDmyQoK9A`0@Pp<$Lk!t+;*rLA*KtaB(g0PEdT}_p7_NZ}|D{^4}kC? -k?Y4-hO!B->IvcpDrU2YA0`KmD8DT((z8cpEH4Gfxy8J>I>UbgGjt3Ox$AOGdM$L)FiYe5`*MOiosx3 -6^iS>`qF9u%H%GD8dv)|oFFZW9x1YjS-YiX9l7ToFSB7N9abbdW=bT}HSn_BWE?FBWa5j=RxQ&e=Lhk -;j@cXa1wz&eLk?7P_gozz{d#4*;ZbWk6X%PljS4KuxAU%YIdK7Vyu=5q5lVggYp8O*^t;DveiwD;-9G -m~&-^rG`kA!!MQk%TO*?D4Epq?mutRRXH?8u1ID4IaDClxQeF02TthaEa)hAyfZV~VE`AOgM%X*^Q$Z -x*?0USS0)N}tl>IPuJKfn3*yYHX-=b*6Hh>q|fg{$ZOIo!UJ-`VYmHM6kAgdb>6r+wCS1@lo|eZip6O -jPV^7e_N^@eil*2C&Q_&!?VS#XJbbz9&smugkekhoopa`S{QQLg>b;q$@JlB@iJb`QCW)Yh&kc_0{oY -?0r10V~Rcp&iU*1lxNSHIN|fp=7M?B&ZeF#(4egJ7YIG#xZ~&b-bDH|K?X__Qp;e_#LJ$-tm*W4EbWi -`YvT_o4fhD+&eT@;(D4;CA6L^rM$6j~FwjHbq-SB{mU^%M73li@A=-mIVZmy*=Ihbyh^hA(ohKO>jj+j -xffh8i5Jcm+zcHD{xktP$bn+BbZonOM=@oDp*l*HQeveN$GUx=x;4n?JprMvd}toz{s6ZwfZrf9bwtO -Yn~W@4oVE=*%1VtK`k@&C6L*l* -{~6-i$`02bXoVPUiE=ZM&`W`8+Aso2qV;?4qew+cuxe-wz(>^TkzRz7}m>w^g-j%!5^RbybvC=FfGp& -fDE4x4&2R+oo!YPn)d0HV^9Dd~0@%%xO`rR{5eWs-mu&)9Ov%O;$H~k~Q*WuD|vHm}RH=r$xRA@b}HTcW+MWx~gZ%+q|uJ`u+6u6@Tl)BE+ -TZ>SNxd+j>RF>di$S&-w5Bch@O;0Q8B!twl-nRc5PnS>2YaDqGrBJU^Ykdv!`w+{~%_ECC_S+e-eRfA -F1&D*~*Kl*xmHKL16de=jI~zN+(VxqDeQZAK)xoF!iosno^A7X4mV?bp?|q^B>-kF=4?AFlI!^>wkzv -7a*r_P%Vk8vxJc*V%(42IU>Bt#|dtrXu1K_f%*I7GXRApOND+A?ge)>Wl?&;0E -z;pt8*wc1ZlsBg7EBYR5y2&^HA%erV -5;l@9I`{J9I-=EA+{`b3+*QYPPdrgFO`u@$E@7}&U`6_t&!#5`^>x0?7A-w!n%k -;g&M^}v%s2_ita_|vah)%2_)rvtg863GUI$0L)1SDk*ZJGOzWM9?#WyD}{xX02{`KpZum2qDc?-fcU6 -@LV$6rP|AMv^vLI>fm=+C@%KZny0^y2dl9DA00$LB7iGy1&T&4_-Qc1A~qp3RaMp!W;3I?Y@BL+qej= -yAg5B~4oze7kM&<=bq7(YV3X&Ea0GbpBsilj~`ZcQs7Z^mv;$8{!#xr}ZA9f5T7SWmiL8Xo${%e=GaF -CMq84)a8Y5ip?pV5v+r|oaPG>B)=tZ%^jmy?y!Tm#?%cNn<(&~ZQCYhY(`4K4_?dIt4@S7xK?^ra$oCtBTYB% -p&&V(`Ua=pWuIoe~A5jboBJ_>9g)nmn4JCaIdQ}*B>4xO><4MGRaoA*{;!rq}qvo3#Z824z=xwPv{4q -qJY~KA9}*6GJ}-3yD);uoK>3nd`iuh`DHSvty^5rKQ5YiLmX|LPlyptk91Zt#)JrHnjCx?k|sAH?etY -u-6SO5lJ8%fCIW*FSeXz}Chyf2m)X{|CwR(3lB(-E%s{cgM?&lk)Qnv%D`}aUk%kT&P2QZf?-$ -LfNHJ+lU}Xf%$o-h=JdZ5zqH$^AB(81=Nz;_hdfhg+bh0MWbea%7qsa$SBFwWhKpj6m8ap^5U$9mk_< -{EXD>Qf)#b~}NZt{6nFRqJ^dr^NuO1!~?;4&fUzz1IuO`Bca;4Ivb)>kG9vx7WUfrvDq8t{Ord1x_qk -uB%>CkX3^ul8kA&V7>;$%*0+c)pNeKk2T`kz8!e9?9n?R-9I^3~-m -fgS}#0$8$E#C%D1fj6>`MCO8wN!|q0gj60J3LTjm5%g(l2ir4@13_I+$kNUBul*oE#{_Ax71t ->dE~4h*);fPU5X8x~*YEeTXH1!oBjP?HjKS!Y*JridY@$ttU9RS+$yfy9Iyq2quG$wmidT;wD$NHbU$CH -w+P!95}_T0^#SV9?@Onmk{$TOx;DLUI8%yCPCJgw6v~NIWM687)NHWJ;1AM2Cq;q7it%O_Mtr@tXA6g -pc!Wo?Pj4bSp7hWcC%r{%z58gnMzPR?#EcC|w>K`V^WFWinmn7u&1J_|JI@%Rwmb5p??@YnsQ6pFA*p -J0Ui_%^xZfj(-Z&2PXSXJ32}{K0gbZJWPI|eXq)Ko7`q4v&@SXSsS#Iaw16ITx6_vl2MX@*>(CKHl#z -Y^1~TX&?4KyceW*gP&dg{UXo!8qR2mO;O?aLx7S(Q0npwO#U*4+!S?(2e}|GvY>KT$I-~T@<+fyBDUG -lt>bvarzs`5#kGu*RJLsN4)`RCIo3DNeJEUOLxBI#RTAf=!GtYOkR~C|Cj%6~pV?{!k4~eRaif9@OOu!)JFSf(~Ym*MLEjd}9eQvphUg2(@$10I$>yR$cPGTtRH!LijY>ysKXk0|&&N0>RX7tvtEGUS-$1Q* -E&pO^I1HbP_vfm1_!wnow^yTLGvT$}U@eB*$HZ9}!#8rgte8vJoue@x*8vM6JtJ&I$);dJZ`xEEewIn -eQ(sZxl$@fL!b^?Bx?blvQ}e$j1VY**dq4ttY@Z1wW)|DlUz)vj<(xhJFk6T*4cG0%dsSq!T|q4rGp< -0&IlsYRUV3nYW8;R#F}{NAzEEkTf?1dmG7kkTuH+`^p#)G?6Ao;;}2HGwD~{!Yga?T?fFz!$?xN(miT0#Zmg`7GJ2Ifm!E4xhdcA -aI5oeduNSCk!Pv@E_iNr6;4<0Su}R30530;H~SQx&2ui0)t2g^E=((A*d-bNPS7P~k7ZGA9Nd$H^FDj -#;0paEs7hr$s}9O~P9>z}c;uBl)E}A)CAG=$9b)ECm*v$BXIlOG}Fwfo65STxWsfY50n#@U8G0u -9kPq9&>ffPK8$Ze(VZhRj2e640R%vO${5_`SPnRO+Y3=}8Uarg<}k$Tn?ESD=ff~{J&x?&I&adN(~f| -VuPQL~;-=ym;HiU1|)${sAnRKp!h9$h%Sx!;B<6!eYuV?M(HSv4F*MdN{xCcb!nn$oc)T4ggt5b8Szo -}~(0pwWCj7MTPTm>r$7tez8(i|nj}^xKqXwXMlCH3pu)l8}JGLmNa2@PgP2B9qAK+~7s5Ljmbwlq4R} -2K*VBx-4bdxVTtf;0qiY#3;$OFG_WfThqpUI9#PJz>=#CXrihc@xJh!mc=E_mvtvZnlW`W9T4nY<=w! -&ncaa~Hbt&8nmt@ixj#$doY^W$ss`ERb`9M;--qlA+`i)(wPcDVlfWJub2aZ;GtQNTAS{D=J@HjMdk9 -n9YU8}5hDC&m-b1!i#a4-hPX&!7LgiR!X%ZR3+z?KKZmM$L$y1Ixn_+CIZt&Km*N!nyijMIQB({UjSv -)iJ^M-g2+$U-Bvh^f+0EiYgX3w&aZ3=-YlfR5%1)tu6BRsypyx2BA4O(sLf}R~rZtr_~dlQkKj=U5_p -{;5@Y|ZhdUCLi4_4xlx{jD1o -^vuCxy@P#9IN4DQdWW%RNltnj;fb--asfMq8K8P15W3L?bYs#Jr5$2QpjSrXU%zVb;bi1jW{zAeNrw(CS4e8wne7WJ^+iGwrYDNZcgC-oG -(diaPnk&&hn$Jx3GS7(C6ypF_&{+Bg^@IPqyiT5;Vu30P+KC;Dv#s#k=R5aoV^R5QrvZlTgk_h2yreX)njZV)i?W_8U ->^P>*j~QaGj;0_%wEg>3f>DE}#k@yYyMuu#bdaVr6)waU4U -Hw6b%cMtLxtYhxA!3q-#PrT!-$6^W;?sO^xvLn-OJ{}TLK*d4UF9Do$zjh56{+nv_DI93A_BU5>@~OQr7)0!#G)~EnfaBDcRM`7%w!^Y$s -(k&)NMSD!84+s&#n;9hb0~L%5pZ$~$1xPQ#M9bpqZgul*XU0juBDF!#7EO3M+Xm1L=JLNFw{4xZp)du -Oj_170$(p?5+wxkn5I2Q2rZc`4042g7IGhIwUJq`mv=0V{UZ`Ibehx8U2#qaI}qoN|Jf7dX-vpeLQpa -5NwG5{3Xq1F-^B}vr*qRbH?o%nq?=A$dC{3;E|V^|?9J5*od32dd#svOWCy#Ag~=@exs*_hZ!Z<-0LDtO -i?MDTqBLRoCrnL@KfN>uFS7tOmjrVd)bOA&y|CJ%ghob<>Ce5s;#)!GW6DQ-YbVL8Wb!(Sq)sLQDT&= -Nc>{b{-w@-CP|=WEZsg;<_wI#}waLRo#F}Oz4aOIfHhEbS!h<&@kijnggfAs_Mcx*ifAyA=mHV}o6 -T{0q-Vt$vNcegQtrVH|92QRw0DqPQP$4y=MH!zcBAeeQO#qt(52U&KVSi&sFcf5=vR#(LWuEFbrGa7eV|C7PIv7K#w@4t{-HkVWKhxPV(_x@`^`uB7jGmV8r-r96}8r)p^VPbtjgK -NW-O`rJJ3J<_l6T5%0nFQg0q~%3@pj|6t_s*#3W-MCdTJUI#$sUvTSQt(M}|XSe4p>D-v%!~XpWfmm5 -m=C|e?;_;v#YcP-ppS{663@nt*_l)Q1JD^9A5FdP|4?Nb#_W_=WlKa!BVbby>jCJw=n}8xg^29)h0LRAw*H)olAzXP%3LBZ$I&z2~r(h8<_s4@^ -8*`Y@a41yFFa({<{`tx*lk|PsLXveH@37=R%IoU(C>%|GwENNIt$z&$*BP$;&N_4KWZNHiWI7mFAk!x -dcP8M2gY{Ll=ByeUu*@Om`I_%0>yo7b$bh9bhf(nvN7Tz{I48;JoSWoSlkjRjk~Rw6QnEaQA29-c0-; -R;J(R;~g9o_zAi8w`?#LGP*UBqYi-0xcgjHm)+5@8`|0!HnRFS6DMW1(wBvF;u^ShXg2orwN#i6vfsR -UhsEC3P?i-1PPv5lJw;Uxt)8w@wS|MP7+7{cBqQ4m`Zim6~%tj=sb8=<;l6mat;{2AIa{TDuj$UP3_Z -a1#VBEzmAaMnhGV!}z=#U1;hFQK{}{N+!;L$^4&B;|q+^}AQ6NnXPCS?Znt56|De27wayB^)erbsMT> -=)ofTC~{7uAs~()>s^u-6FKS~3B!ABm78>NotZmO-w^Duw+Mse@Wtg|aOWTkmVT*=_tcc~0TwKwUCKG -)x7PTKIQgTIz_Y7ut^S?FT&pWTQaxhe)|vqk-VRSsUxn!#2w+=R%k7HOqSZ^Q*T8#VS(ix~Z`4N+J{Q -F5Z$cN3aAUA6yP*9ZACkKEmlc;wf@ltPwNNy%>$LOiH`EEmQ&P$F!Nap~pBT0getk{smAW)Qa#Ee?DE -aBe?)c_%3`^|5T#e&C%jZ5w6tGjFO3obCLo8 -BW%dX&$M*j5aI;|UofsX!E(Y7X1s&ZVfpuvkX5Vt5Qyc?wry<6j7ccs8+cLGp#r)nsL<8sNi`sx@y$9 -VN0|2`ypR1tP12~V&DxS-=@m)nhGTy<-kPk#MXUSu;mH|?zuSbpai1k(xrRP%_`Laqk)r|9f*MKzE{4KUQ1;A$>945UAuc&|!UUoBa~x4&CsS^#ze -Jrvn41ux4zH~{oAD;z&;~5YfR&@W!Cu%x66R~{VTDpwO?$_5%*Ro-8IpOBq;3G@G(IXR9+&&~$9(n@s -{=Q*7x>&){Npov0}PITdh+uaqPe&tqcC5_90WlxNo&qLW8JcJ#$x}P&wTm|V%#{w0E+f)MTfb32A$qp -EqYJqaQvtVF`}8=S_UZ-rnpCqDY?#phoD)QgFK8BLo7yU_gC{U`3`PLB*j4aWcc0v32 -5Y{f9e0gm*iMO0ap2R3YYAo9_$FjC)dKv#NQz0maxB5&G@?hdX>txiLS^$!GT5vf-2^G&w!Kk**%0}v8=D6H18kaS7hhc!rO*=iedzGo~H3W+ELCE05n -V7Rt0cai2|ojQ21uCsc%sBnQ0FC;dToJR!MHJleu$J-=xSEYAv^3XTf_-do*T+v1@K)R;VeUC&Uz9kwsJf0KnqT*`_@nh;H -C9JT>Bi$^Nz=*&SKdPy_gHUDRD9Vg&Ma8Cnh(PrY=dtKJ^Kr=0LR@`rLzmi#4fN99x@Yio`JP8RL}NCfmUX76Oenz_hVQ@2O8OA)q0JZV!LX~8m6?= -S~)WpnamAvhut6pYte($N=&by4D*)zEWVuHajY+Fv+==!F7<#HNETDtsVvpi*zfma-~Q2Jmw009kvJ! -B#}keVnF<;ZmunrUY02hJ|5n0GfczalyecR+6R9N -wfu^h#|<3r+nG2DiLpA2U2(g*_eO}+~lYz+R2?An>k_BXb03qPI9=Y>QG4P%^vOHzM3O$oAK}Cp!f(r -da~%p-f8;~p`Sf>ENAXu@>K`!t=$drFFBHT1Sto{6}Dj~fh*ceqV>F4xMfR*nw;LwHJBIHVVggv!E6+ -{@CAqyn;FN3?!Y8g95ZD>osxRO;O82jH}?S7sKuD!?Hah{zb+QndhLPHL^9^ffC*R%;YB#+b*v%MgKR -U{oemiG?sD-_+jTC4<>ePTP@SaB#a2-d<)hP^QDF-G?Q>BKl+AIc1)}fUxM*woe9p&xZXr85AZW6wLCzFg@!cPJD4WmmbV{lTP~ -}^c*FpNCB_-)zL(>^O!RL0XC3|p=rJa>37CFxp)KL%SGmjjF;#<_kcI}OYo9irlT_XP=)wx+jR3YDEx -x6_s}$DC+}md+(GVbS8EO8hjfPDKDx!PEBXg<`PfP*|fwY)&E@bg{6GA8n`jE96zwOY8mhyi}A*LoZN -T|uZV%Jf0_ohC6Wut^ub%+!V~Zu+KMo11qhNZwNe5a7Bm>FwIvlkC#6G%;7jn~el)(qr8%SrVPgV9E -f<0XtY(K*Rgf=k)Ay~*_FhBLp=fbqsyqaryM(SGj2OwF6p>>hIiobgGXUZu(Txy$kO)?(Ljnz!fYQ*S -250?v~2PLlQc9f^^UxVA7tH1yy7e4GqS#;a;#%>Qr}=}CU -(g?D)=X^8yx_V699H!B{LHXc8_yjdF3QO60_@ -or=u&d~A3lh?h-45m6i$; -OM@~HHt}@gG0MVB&2Tg7BK3eHtj2$bR1l)clOf*T&}5eLi`{VHq1brA8x<#AST?;lbRwxL=cFhWI){k -v$bS_Db-u~j9U6?LdHA@^kYs6-6@*JqzV__EQrC+sFD7*g*t4Vg|K>GD3_Gl95c8COZZQE@I-mpXmlq -N+@trO(G!hbz&rkcR4KVV5?kfwZ^%EeyV-(?16`?z2?)eFE@7N<1W!a2@4xvZ6xoSHL68O;CBMOgKc3 -L=#&69^sY{;;&uZl1%!Pbu-;Mh5%X?E48M|%>;$jg-7f0gECtuXG>v|{$y -pzb{^;!Y?o>&L}A)2Pyayg0kGVx)@)p76j6x!G?V(;=FdoE&evBk8TU|zv5J8TN~)VG;!c7_T`wUm;d -xY?R+cKKt$2-_k#QcyjQ+&i*w0w{frLv-rJ~ygHe71H|NR%zEZr&)CYvf6z#hO?e}MT~fD -_gPE_aPY~DCv&25|=mM|eLD^D7iRY_gU6dI!(&d&9nqP*^p96tvFv=WX*vUEb9i5*=Z+hP_oGoqwhvI -bRNNOl6ku%O|qI2_ly?E#mmJ+6V_gW0b>?Y3bi7XNkoVATUDf*v#C -+a8Ety-}Roi!Oo89zp0QnfiQcXWv2XFMo)V`Lc?={BDQ}&nYl?j>+`PkbH`Q6(E5_xlGw3=Y=9bEhNo -M@3+o@4BFsy*`UWbp5E>)TgWFoZ|VAN2|-c$bggy$9Vp6W$`Dc=lJS*dAP|P}480JT`cXu5LD;#e~k4 -E>icLXZ(WKMVR;f_$|YRpIY1Ill!TE$_>Ps--QG6>F-I8GO^$ELyT21t#=7c5I>e!Gk!r%pY`}Ea%VW -qNlEPV{hK%6y?uA`RrD3bcT6M7*mQ1NS7O{=tBJ05q&6?j;x0A+j&+PA!G7e<3r7N)n@WDwE6{(O{P4 -}m$t$mqb^vTGGKF%xi7dcLUEH;re(fveZ;35^{^U -3HwxQ%8#;1gEA=QYLa-Q%Od;IWeW1Nn4&mt6=PQ_Wr>MG&wL35tmR6}nV)=Y>c#F_VIA_gNE;w#7$Ra -$8p@9H>`Ie5*gffq+5PC&RlJ7+WE~$86x@B8|K%rTdbUf59@{3ujHd;3_w7mq3~jC%D{W|9TCa*pOW_ -cE2aj*tzYBb8k1MmYX8Cx-l>9;1IHz*7G&5OMLeGCr^T^93$~O-^I9obI0MjFD4zsvL5fxyr`0d%hL>_92ndclV%0*Qp!secGr6(xND<*Kl39StOSk9Qa6* -bRAd~_aq@3Ro;;axYYAhEa3dvt44iVnW3>rBHPM11I#XLF#dCyfzh_8v8(f#@%TFe~XqjUJx)Fdm|=^ -+?lwE8{d-w>sUlE`P;LJbiUULvvw6&^_dk2PdDxyTxmIBR#6ezfD -Ta-cyT=Vl(~$r=H17vdJby5hGXn@W|P@D%|$v&jnQ;SwCEdrUxzE1P$Zx!Yi3&Wl_xQ=MA16vf5{u9i -88v4)gyt(GZok}|KEqLX#ia?&*(nMKh|7zi@Ai9B%$kRhh#NUNND`Q?|sDLB5Ys`>mPs}G*0PsX-IFM -ty54yc)H^0_|md5gF -1uO2)t3~L5I;!+7p0mw7rte)fqVPl}|XX#VFTL*pe+VcKEHH8sWw17}7rZ4hURdR*F@C7n~__qg#-`{ -Zj*=H(wtS|oBlSJH=9Zr|BR1-|&$iAaGs*HBQnfD+XdqSvSX(}(w!LB*EW?3z^5~OACc5DrLSEvE_r#x3Zn`O`nPdA)mR-1O%uzx33mSs5t -O`P@{^&)1uF$HXIZdnxPZ)O0C_&h@&vnw)~*Kz%ON&&j8$?xtR5-SIB_hh#xWO$w<61krbkL$ED5Z&p -I(F{q-~7VvExDrgHhf(&J0sx9m(?yI%lIWm-Qu4JuTMAcK3P*N-TfmjBJz@*TMejsV7TABehy=r*rS9 -y!PG0qh8ps4#xYyayE0Q(gwVV~eT!yBzU2$(IJdoVcaMtJ$6A|=4{sTqJjaP4#`w2E8v+H?<$sgpxZc -?E(VOu%{BTWkv$uDxtE8$eqB{B97CgcIDg*+U&w -j93a&A&YC`J?HHyC5)9g&}i!H}yCG_zgdF`XH6AZ?sZb*gCL0l&qJfbi8u?@Yjc-3d2t?$)x9ePby)> -cKA9MW{MZe5$gsGq)~)AwYmM$~WGYpGKY#d2^UId0o5XvUx;bul+PQ&>6}c8oXR9T;cU!Di^f!tcH<^ -JbB4=*XLgQ*q{+CR}wXdM_pMNiJQ{Abpl}{AtGyHgS_vlbngoU&@QK$MO&fX~yT}`E>Nwf>wf;1Z5it -aeiTZ)`*@gCaCz#9y~a5R)qm38ZjqETli`@rha>Oe-@gZ*31<5lGj7$ChhI@isY$!udvp!l2iBoRfHl -jPNmL?TZ=1QY-O00;mj6qrsQRMS^(3IG5U8vpc!JX>N -37a&BR4FKlmPVRUJ4ZgVbhdA(R|ZyUK0{;pra${$urrB&0SS3reZ7>RQ=F0q3+K`wBO#cG$5cwQ~HB- -gSUGppsZ+YNV!xF}ioFzt);D6UjmUJlRvSALsM_xpwsyI=llcr3Ic%Va -CRkcsUaIvDB -f2lkzYVpGutlOuSZ^n~Ar;W+pBw@`tn-jYdb}q^b(#Jp_}C%$zn#+R>-W(+|n36YP|{KKme!8_Q#|A< -5EgD-*m=#;{~Og{ar3Z%;p-Chtx@{D773PA@M{zCXpfD2o0$`nXkAtW_Ze{=*$InG5#D)q$W8PQwLr( -nAWjmBQ-UWbjoSbXB`iZ)!XmU6dKkB1vg>lWth3t+Py8yRM7k(6}=;;b}oyrgMl?@Pw5d<+6|t6DD<8 -V4XPVS{S+0w?Q;1JB>%se)RtAeKNG}&Dq=2^OJXM9(0;}VRKqI&jW~u*xk7Zz~pi*_69DW_-V;5H8;S -Jc9^uYr~Vilf1Frnregj#ajr|rUnn5y|7Q}#AQ5n0r0^mG!X?mT8#+}65@;5MC$tZN8vmqK+37M9Pn$ -et5qpK@x{~Fj5jGRiKAKWwHs)2{BKoyMF#|Dq)K{hfD^r&xyf~@L9BXcsadleA@^*3`BF1N>buqB+gG -9C}#Ya;E3Lq~d_)d^Xj-lWLsp!Pc#GACRQfNSG_R30lSYIAwY*U*$XC}xrFw@AiNGc=O>MojgHkxi<9 -+L%DVnytyir*cJjdY!y@hY`4$QjRiGoO$->J6~Q0j|!Hq}0i#(5tlgbx^62fPo-^R3%DHIyt%HblrrS -9lFwyceI@{MjOjsS?G<*NLHs9kzvrhwpoSe*>KGGfG9%#wnh5Q--gRuPn#;aQ|oB#>xj@oAZ8lTffShEI8EzBU= -xHiI@gcy3LNno3kI^oi4?Vlobh~6U_EGpIfQ-Vxkr4xH?Zpf9|q(xDR#4byKMs9&4Fd;qWomRj=-YGX}N7$P`L -a?`l>p+T(A@5{Xv$Il~PCtMp|@~)Q61b?my4Au7jcZ)=h`8>YfCdJFL7GW5zbTsb`_yx1uFZ+<&RL9X4%8 -8Q00vsJ$gWG2(=&CCiV@HYiY{}=bvRT+_7tLx*&m6Yqjt`E(T#U8gY0vuh^|+Y~%O5F+1piV+u4_j+TM}nBNti5Iy>~_pTM7b7*n3S;-v$qeLHdwOOGO1$2F3;ZfIyZ^M=_pFbQ6N9SYYohuU|`2m6(B|J>SPKizI -08tO;ndAi@L3uwW2)InO8u(xnlJ!Y;r6I(EdV`?5}!f5UM@7!ddiFc1R8EOAMr@3un3x>Vv@Ct{Xn(K_$c;-TaI_jgeq#O5Qn>CJa+*Hsmv3$C -i^kzmJ#VYCNBV^AmjYY`qy@e|vUF9%ja -&O;|%5rSh2V~Nl;7{y~ZnTguX1u%IqKY%g0Z$}2si=E0*?jIe(070Fd%#;v>3+W)t26 -m7H#7b|5vxl_TolkO9)B{xcJeUin+O$R8Ln9p*LRvM$l9pC(aokO)x#R_o+3wMRW>cv2pCd1`TFaFB* -jx0fZ2epWi>@LVJi`xF)2DDWkd%H>?H*Wh(TC3&mRo%)Fg#~{roMUZ}KLnw&^4a3c9UNZtmLc<*_B_p{6ac&I^eI~{n`Cq>^q5JjQIQF!ZCpRG-de -=CV%mx%c&;$@YcV4gJwnu30L%Nahz#{x}n8D~Bdl306J%w+Sjqr{C@i%%3-<|-F0KbuRt=GT2?e&_B; -}xc$zO=V23MD!mQxc&S!{-n@N6qmOjTW|&nOZA~kCGgw1|wLbQQGRd$lF}?Lf%Sq;GXP7D}*oF=x%bN -Vi^;ucIk~|rk%-LQZ(pWavP136FM9^xgdZFCq|~R;6`o+1p11>sb8eUK0VkMq?bt7PBsqt5UIb?&Au+ -tP3}`PXHbo{Hgw&^Dsm$rMSIjo>};Ueh02uUCiGtje@YCnW6R2KG`aN(lwNpKLsc66W;q60MLnA%Is= -~&ykQB|lP)1lmk+M&hfdIBOaX*gDO>u(d3g?CSiA9UVMlXXz -^ArJTuFmK*kgFA9G&8tu&Ubj5;TUX@iYf`G|YmgXhnvm%YFQUvOHXGh84*hi{GSDw5l$|OZ*F~9o%zWMK?4Wu?4v)2 --d|>(w-E)H=;VaNP;0n^>F5r2%3{X6$pfwVejD?#!uK#o#xW7crH{5UlY9|tN5~5OOaLaQS{)`pFyU4 -9LVJ;9dW|MDgZmx+kTvFznlUfG305eP)+wcc4(_(QKimYT8m*;t&=8P8%c7g3KilP#1ce?-ls77iG%f -}z?=(n#UAFY#@t{8jlU6qt_C4N$i#$8h0rirk)i0XkQM!ZDk9xKI;vmJpLPO3TsMh&xHk1`uq -hqh8`4q}#A(?czB8=Ts){if1l4n$1+LF?T#3+hQ;a1_WWj3`s=!HH0FM`M!VCkq)ZO#xaTI7r{z|VW3 -J@6@tlxMIy{eh!}p#rC=+Wq)O5~f%dGuFOFUGx(vjzEX%Ao+1p!ysZ}!!8hc>8d=J9so0nCcW0;6QcgTQT*&xbXDboOsfZ -@QW<`Qkwrpud%-3sM_~dQ@wOgpyEW1th<$zyZKPBR{U@j!d9 -Geou14|xU23S%q%9Eki)Qo9E0_1G*;;~SmNuJOVH^DJ-F17aV47!E#y_40aSwi4wsjYt5IfYc1CLVJF -H`SQC9hJ+Sqy(p(dhzO#tt}@8XdCov)RZ@r&UA{^nE=r=THvL;3_rpk?cK|gm03?_gtk^Io37oX-OXX -cZ60>UIj?M@j(jaj|<~%8!ni`K?ZGf+YAs1j3Y{fTh)U($5Gn%1GhiRUJ5=I`Y3AEI0MCP`F5b<|ftF -M*YscKWf4%!IFI -5kKh=0pMj?@;DdsFohb$84p8~+A=*MBW2z -be`-K7=o(!ImSGZWW9|Yfh+IG+84 -B0&(a{2D+}xSyI|J!dVW65DFLP5vOC6>HSp<2NV8jm&4`x8qV6dvwhn1eeQVRKRGLwVp(etAhhsQ_Ht ->|a!-p7`tX_eQFE|v1kS%3AuO^ -!h0nwbSZSBFV8ODe}WKn_2%;DAJ5-@2tK@g_xk(;f~=P^T$(Ly4P|ET6fCSDDJ3NUhfA3V-%3kFug3z9l*V9ci>WF!w?95*SK4BSSWW#J#e3SgrhO9o^-kYYk?w}1Pp{&!4KR8)st25O>AB#}tMjdp;dim{u&*Q_4xtsoME5m>X${1JlLWrqh0?iq8-@^q2&6>~KN+9PMjN3`Y*; -vS#i42BiUw2C9@6f9&ifj}0=v@c*V$o0%3V)ay~z?2VG3eV7jEL~{XXh!5Q>feqVYK0CGz}ku@8(KRJ -LOo84#RDN|mtA6qL;QF|EOk5}wF$7g$8cA>=dzrgI#j4u+Jg34^nUI2BHwcesm4ammI7*KES5812FyZ -WL}(act2C-&(VxWV@l><2yA3kvHjn*5*|df=?ginbiH*lzLa8HXRVnDYWhURFTFYggR*NMoQ??XU4(1 -C@PDt#`M4YahsRJ*HHoG(ctb1B^2GQv9J$r@$s?ZM){AYe^La}X*=r8%Lus90nt_!8=v+;mo9PCf`|8 -E;xZKETe|0VM{ZkxyS;LqB}^uTVRRi>x;swiANoS-p76WBYpLgOC#GSgF>2oE)2oW(RsfeNF_P>2xz) -J}*cjH?L4>jET}>HA~UyV5LV?uEP+3A2eJV>DR|L1YQFbCr{ca9kUL%vvel)H!co{`7%@^LcJfEw<1<6TxSCT#sX!qg$&Zst;B)BY*UZ4MB=aI7nzA -jazD)%dw8{X`9Aox`;Qu3i5m-^7k1eN)gg|RW0YSEWEPC<6}%gVa~87}&~)G5XA^djlsR}_2ttJ7!Eo -ZY7asK%9v^|QpoCF@L93~n8!NH1W&57{vwdzGhI(NcJqL0I^F+rmLjye5aBnqdvqRZ8LZOOmc>&?B5&nDT?nP -YDT}rXviCa1Rl!s)A$}0wW^7g$K9lxDH}e1sw#K(n>CtwB{aM1TqsCd2j{`ByE*rBqO3d4WTTzu;HOc -Oxo}V1@Ja4!2>^_Q_Zlz#AX!>RNNhmqU2vdKI%9N){I{EJ#W-AiZG3e(^DgPVYyAVc=1CzSVe+hVkeQ_V-{Kpa>LBsi$b;LH}E{7hL1zO?#QfbZ --J#bTY^Qxj34>ry5fGL(;MinceFez#Yvwj#*vRBzmf?8(SkecmmZyOlz8Aj8?ao0U~;4FH@~$Z{6X+5 -W3FhH`Xx)QmsyZt%G1ZFw&X$Avv}S5&WoR?qh;WBDdXv4k)lS5I5yRqh4BlqdLXktIswiYQ@e@|A|v2 -5>q{o&4|EEk!YDc?~dbuHZ*5HKuZyT%|`W6`4=Ly-5}0&Jm`8x5QYNoNkNrQ=H -;i=Wq1^LMOHC3TkW#C4>851YUb$OFKFxW$O9Eib$>4 -AkldiEGTET%iMj`xty7BAZSN}d;)*$mdwgi!seKn<>978~7(2#uRXlCHvkAIp1uVet8izj8Gz@poD~j -v=ll>dS3KtEQL)-{zM-4cqvJ_tpqhvPt%9uUr#8E%pZk@IGfY|9~HQdYRQREr`GvD^u0Uowky!*5%tW -?n!V}eWY7^9n2CbB1>Pm==mNvMjo?d)!i;EmLpF;i=0fc7KrRKFe+A~U~~WKQYI2PY{dx}jT7jg9Q_u -`Lia1n-Juw+=$;`2^d%2~}4(A#QH|b)qVx7BuuI#8c~SHlOags%EfPQO7$j`n(#l7!IPpM@>{t3aQ675`;2lv<$LVqp#gVQ;s@clx(f|I67SeYi8 -BZ@Cl%g4iM0|(xPN7Se=gw6;L6Hc2KF|C1xE+#TuD9gV84%2{hgXh>2drefE3`Ih>cGc#tA}=%J?tlD -d)(X=-JcIWxOjuh+gh-KEJ@ByRUIkrntg*#|=QcJbwqHP54B651~ZPmt9~L(xR-Gf<5iWq6jrr+8{?B -1HX@LPSSrb=c`8pY{$nU6*~tz`c*Dj*D&6*h*|KP&{=0^O!B~5zcskdxM}IxcdIobq#B@ai&jWbQNeA)+&Ae#^6*YaK)b)#D%XK>MGMHlFdjAFj5$vL8!(*E -xOnLffqF{|0l!K}xX;a{WODZ;6Oqi##X=kpdCpTR`qJ8j4T$(W}mh7e(JM<5_24dm;%{iRnx#{7)^R+ -LCSuXgOzW8d{9zxAev1OH&8!SUi{x*9LCWeQ^Kdgcgs$kSp!FKWgfG-Kigy&K~{}H921SHMWvF0_ltB -a21+(X@fVPqR^7?w#Wt59`Kn=+hNlKl3V3P-aF|JA|2DhU-ZS#!HXIN8~`Z#E~yn_ -MTSS{&)XtD?K+a)-um5EaSxm3%^!nghyKZkWdqn3*o}>96aE8GO9KQH0000803{TdPC*9bFG&ml03<2 -^02%-Q0B~t=FJEbHbY*gGVQepOd2n)XYGq?|E^v9(T5XTpxDo#DU%@&bYy-BC9$GXo;47||v==nZEiN -ezK@cdkOs|=hMOCDBBLw;Hof*D}l;qvLHvLfJaIqzFI2_JA^UQFMjXJYDKencAM4mHwu2o|g-)mL2M& -$PQZ04Rz<-aNIzv)iTW=E7$%R0+%MRib(7gTpfoy#@T@^d^$Rz@@xFEjDE5Veu2(tgIvN*i9593Q@D8 -r4iqYmF>*X1ZEd*M8zZ-d(-RfBfmikAKg9`RD7`FJJ!+)ILr;-p&|&NNOtKhpJn%=dv(s_DX8_|Iefv -2mRl+btz^v@k-Qx{QaMvbHmxrK7X%GW2fM^4IQQhLRXcqs6W$%>&SG=Jq(J@X -rp*InJHPogk+&CO}4L6sUF<_Fmf^#;+TH~dR4Z~|CE=?uSRpOmbU`D?Za(lhjk#cJjxUUlZAs_)PVWr -;4RPpT~stWt*U1#2r=sDq%vaGJQUt9Rd_0b;xd^WUYr*fnnsU|8Ew*;rnu7$xR;VYPho`boTE -2o(|37q7N)ChivjD*{ug)bS$d(V4xDeEKFJ#h4VKBs?w0ildDCv>4%YA1#oW>%wZ%^Q9ujkfIVN!WcE -#SDJGJk9RJPMPmpSsrq`=Q+FOWh@wC{t#X%ar2mcZXDa%IuE3f!jtnpB_s@xI!%{W -W@QCl(!eq-uL-{DOw8}u5Jr87|4){VjM!{Oky;MNE_V9-Kn4OZuu?7Y#Y8yflybNICYoAe;DAfRN@LM -ICU^1J5imib+?Gx$8Nj6tQ_8qp5cMMzlmLZ;UCuAnn;9}wVvPVFL+#D{L|gzZJaTP@g^uUS0&jS%eW# -T5fsC7rCvN^#4lHv5&G;5hh -H1Y30uN?6Az6v*k=yq7^i?N;2fFV@Ci=RS2%KRC-jLt4!q6fad3}bRx~LvIvhY$XzC?^VO|rToLUcu*;PD{Ps!;0IJ`qz -He@APmJutlj5Vy$v(}#OzV9jSh^>%KPwH+OQGmYyWEseF+VeQza75c{I1h11sZ0{v6a^3S6aF}MvStt -_h`{L95vO4SFikXsdes8M`xbFf1X%=($_}6i$I0|CnZhSkO~^pv8e0f`K`Y0~T!iM4AabGmgwTMj?#9 -!5CrjtU^M=AhXk-IE1G|h4VU;nP!@OzEf*OIG);+pnFnk~`48SE5hgsGi$+8nVM~x3wm~QbJYHH7WC1r?BJ#!5e7zY4cHdRcEg$7Cy2ZOF8^rz6_33xt)361l$zRUZVA_xE1B*;Hx+uQz -sq_)ytXPxqfN1w?F&)Z{-0--%1WDIuVg!U}z&Lu3&da9ObnGGlF%tJb -)lqE#k@Cz#%y)fd58eLRqd{+C+o!ijezSG{j70ZP6X%whbkHhNbi>^W-7HJF+6?>K?rMp2jh@zRT&s^ -!Ou!tdt)-hW3$vJeL}5f!i44TEsiIQ{>HWAuwZy_#R>hEG^zwK)VUGrwY;0;(az>$b0gGvs6%NaY3cX -&O7$DaiAi~VGV=gTwU!#dw!XG|G%7{=M88>iz6Pk0G_^lotio-!RC`J)_+nyo!aliXvXR{`;BcO#C4t -j-0vM!xbe}6V$HRf5LI2|UYGoew7cll(`7k+?(xSdpZUfSHQ`D~Kc+BzxN(P+FOQhY5D$?0+^OaU5E- -knK&(`pcvvT(D8Q)j3B$>OqZ@K3!!dOry%k5u##I3t-y!Q{S`A6;Dddpp^IwK+F(O;IY}lm(?}7ct9g -RtJpFWwX{#5X^Z_%d;!XD^XLBeu*)1S4$4%U<)zo`e>R(&_%A!RHHXKy5&;(c5l%{U=!qM?fu0q>_G+ -H>X!K~o{UW}RvwbULWTwH|U6bYLJ1Q^L^A0XQ$2JroTSQ->YCgD_l{nyDe&4o`6qhQq$!6d>K#_VXhX -7)m_b2l@}jx=XEq15qH}R8aN$Ft-)PwIa7lLf%FcSXMOVLYnmbiO`n$t&PZCzyBDQP^DJ;MqDYbdC?I -^W40l0h~-;FPB`q?Hu%iRu&fxj1DJzD1GBr7(*12!Uno%#GkaW{^a*<*(_(Zop;0aa*N~bMSK=TYnMiZamRzsE6U{V5c9{aXf%>}?56elkRA?#- -7lwNHjBQ!*udFPjP=#cBLp~hF#X33JK^%Eoq66(aIz@^wzE`)t7J34XZd}pGM>qn(h4Dmi;?jEW{Do-&yE3m>I@d3N+re()MqBmF5j@;ndd1aHJpl_F-rfhR}9GC!+dgv?pWL#7)FX)!oU9Nx}I$=n=Wee=*5q@qP^62QLv@zDJs2UqGk(gy@;8#K%dLM9eR< -4^bo22~#OhBa|fXo$is>n~2Gr7ARg0N4UC!CsDLSvDy0Cpld-lt(lzGE`#mDrJVeFy)6T!TxOin2LU* -j@un0c5EzKp3@D$l`-@fxrODhW@s;n&UuXTnZb&68+e%R;cqCEWZhPohHPmbFh%??wXk3P1zG0zL%vm -9jfUaDY1`H36LZ9i2#Nc^(z6J!tR9zR@}f!cziB-qY&wCYAGJ#jXE$CXTaiHLD!(zV3F4nf!FIx5OAW -Kd@awiQdy& -c=++pAV|7RoCl*)On-&HDrpLjE`fb=xJnEa@*K+@J+Hp@lgZBGG3F*^C??<7s+O -S{$oA&$os6gT?WeJXN5#ReqEJ2Jv(0c_Ww;3_tum`@OF#ONj|Iuf76JSr%UUKQ}-~u2IHvqGs4ZYnyq -REe)-M#PeyZN!qZRqOvopRfFo+9$fcldjEON(u98Sh{|vIpIz`y2H4wB{2zlWqDiVwLpk(=I~)7h_W6 -K`&DmTZJj)4&&k8mHLV!y>q+7Iw${@6aWAK2mmD%m`*6173nh(000;}000;O003}la4%nJZggdGZeeUMa%F -RGY<6WXaCx0uZI9!&5&rI9!TONeMs1}5S`=7dTU_sQ&7sNWkX)JsmxDq}w5_Zx>O^VxjH3U&Gec5*5w -*J)Be<+3ayXpVXNL50xqK;F(UhVoF6=v@WIgniY!-{RC&5bhsvBRis$p%_vP!X5bO+g;Maj5g2U*whU -R^B~@Qn4c-PPhw)G+hIP<7%gnjT&<`0$L$roO;Gf~i&%jN?q80`yqh$=^iLZ(-h%cV#VppD~yUT-=GSt4d&H*zbZJJH9j8Q!t< -s%5G+Ps5;ey0EVXAviqu0OSZlTwgj#K441GkSbX0JUiK$O!#9iNa=BPk=T>$-tL5=nHOIxFlV{j#U4s -aKWyQ?tR5*(iWT`HHKjmDhViYdy72fBALY8~6Wdb(SiM*`3c?vBrZn?i~aT3LCPQ;Au -sJtan%oFJ^tw-UTpWpnPzxe9Q7yr(``{Cy1_07M)Kn{t=+K-58Na0&jG_OH{+G1W-=u2M%Q(N}E=-s@ -Kn^N94wdAFx^QLOQ2hYPuFf#)ch`KQfvlER8ednBKjsR-u?PX^>pY_Td}zkM@tq0O>=z=2K?yAs{2yMA! -hy&I#ya#Ar*6@x>_8g^F%a|SV%N?rYYX}#H(?yzmG|NZ5fb+N-=%<|-Ezy881#45VTT-x5})*Pr(kV$ -xa{!d@_(8VL_3y)V5>gQoraXC58LsZQ6xkn_6(gsT+M1sPbO>jMn@)2N+t92$4tx;KliBHoYd`&wXXN -hss8;q1wspjmt4n}anc?__&BIKHWH(bmtr0nTDgGz;J7Fr&5Z^dDR8hMJ4+M{5fnVT=+GWJUJ7dm7jG -Oq3S`oX_OD<3?fM3xz*Le*Rm;%00e4y)C?0N+KA1 ->B<}Pk-W&gm}Y_)t&H&8KpHC0kos@J-InRC#Eo8fm4^-suhFXHVQY>^z|hRdG-t+`1Zc}yc5^(A4Ta# -14oY^B4q3_KLLoKDDj{HgmWhvyPha9QbTCfsE_bTINVw{jI)161Ey|@W2aA((_CD>o_KL`Lgc${S`0E -mDfs~hMdW{<4%L2W#_2DS*b&$ih?T)5qOJcjj>L}#Ro68b*T(79!zAxOR~nbRcQN1C%9{bu15c`s`BL%XOot0-bc -G>vXmBo;Ofb>JjS?-=W^dvzBR7hY9k*2sGR*cD_Uo^RK&I&C*IzTYpKqH&us4@CuNin71Yj6i_>~=Oi -P_nAbe4TlUKJ#0> -5=9$;C(3aI&^QRhCO%ndBYeLKOw+;o^Q$ubW>+n<6d=#uMrUQp}a@fx7vdgE+(^GNi5n5k&E?1I -rCSY#)Iw=^P`rdCClfEScQtN?tLJmCU%hlF@xy5=@%7yP7_lEjUqtjCViYFU_2aP6mS1n0Rs}x1x00|VxhCtUVt#b{RBY^a>o*^91)B1sO`DJTB2b{iNdq9 -UoUeQD}MLh1#!#)@1|v=!_H(Cf?5qQ)%6`bjd->~eUSBNG23|cO{lTHyMzxC3}@nF4-soU8HXR898Ko -RAuz|zMTu-PnHxUQs&t#fh8jGZvIY;)p+FGu@8qo~s*JZ7X4f#mGe9t1Z%jy@dSm`;uNbRTZb6p8#V$ -#w67y7_l$^sd;xGs3uBVk@#H#3ru3@iu?U}TwAEc^$C{9{@j54$_S%?A?buBgW|*;lu -%r2Kqyt^l~Id)ur2=Po=Wy&g`cAeB1-%Gtn0rd)_!pq0p3b3YZCHqoB&gCAn^x^?<8X4UK2Fj8A*Tcxu21bB7wWTU+Lg^#h$Ys -axe_!3RXiw+@CH~rEp~pdy58$noh?vn;YVqJDCt}f{HLSnN0h2b;!j>(7ResBi1GkY7qtBR-drGWj$( -Jgfle`Nv4x1+B|8&F%+j#|7`g3xqV8eoZ=h9&x-34Dd*N(XVr%CDz%a6#FY=fb(o|BN~~U~q^;_zzv$ -MsC7m($3dSIo-4U`4akHzf?uCv%fxC1<_)b6`;OyUz^)UKQACs|Xut5nXy~0AUDbbPcp`<}NlN=eD6S -DaPRO=Tx_&>i&j8wX{g1AO#l!U4eoE}b>#;%}fXuo@yH*VTbGuflIFtj^>g(X*4+B>6$SHwfu!Pq=h57^~Y{k-nvWbNExXF8r(Pk;q0Uv7@T^f@Lt%I^ZB(P5e&kl#WDi4BwKOmY{^nQp-~$67E;21$CIMyQ^ -l=EBTH7<6o;-SMtMSRr+`SNuGa4}4G`btnu_Md@s#-o`Q-YM>CsfV~in?Rwh-h|IP$H*V#y97F6W%Bs -i4TQV1fohtK3Zl?rJobaFz+te1f37Eb01H?8U6?8BHpar5L2qq8`QL7IgTGW={T)|k?QZ(f#Z5k+)?HoH@fI156wbvF}(Pk -57FYiMb1nCb8vo_YM!cysvoiNZ%zw8^XhMlPNH3mjXiSxoQa99`NW3?1STtB;DR)m7iaaRH15o4>8$0 -9yeg$TqZJ+ht|>)Fm|A4WC3H<=&ZgDW05-fhYJ&6+z5qvKH1opGv -m^w9wA^M~)g_9nHrM=XC@!R;-R5q%=FV~M_#%`UFWp0V|_Fa9z?q))=aAD)DDpYBdtc==hDSZ~CASH@=D`Z3Z&&|~;O^~`e_+8QfzHB`TQS&r)yTe}fF%$fHejGk?5 -$U0e1dn#20Y8|FB@Uki~smZ`2F2~o_}0lR<#516$l(6B(PQknXP_J}BGIv -myKKFVoZ%V!QQ*RHiZ&<86{NfHs-u#&2OZJDlV?EFx|xDqU+lABCdIb2M$-D5o2(M!Kh4~yi)<$rPVJ0`z?&ztxqEI?xlGf$_KWFA{FumIiv -5rXfA+I9VHwp+)WYA8u+O^XbR+h&+OD$SBwhhXv6YCcS_<)#b44h~xi_Y5%6D_u04A1XR%APRNom+Td -c*PSClDHwqRtP;4TpcXdjPYg6kBiq?^SXF^HUU!zUdZ;m4d)?{06z}t#^eLzAaIn? -)LxCXwFx2%WLB`!yTt>{B$d*^T^lbg>5SBs2WQ!106?NH67Wf|~9C;~^o~*E!YX4`ZG3n9Ch2k78 -^CiO1yS%1*@D{@u!)evsvz#6SIV=~F7i7f@{X!f*KN>yfl-aQdDapbw;537%YG`LzqP4gGXwa$VSDRC -kDM&di%*hB=si&%ufdlq*01yInwMUxC$j7uG_o?475h>Cq*xMw-L!>hPL-v$YioVgip?a7CFq%yctV@UE^jjsUz{ -2|BuS(l$D>Wb`^%H-wMeMK%ma-UVl{lgDcc>?M^$O?OzPXQz{1x4YWruRV8inj+^4?kCOimqF+;ZA$4 -*Z0mk2F^5rcbpodeaBHOi6E@TJJ%9I{+`A^78=)8nPG4b-sx!N!M`Cg&MyoIW4^T@31QY-O00;mj6qr -th=FjKEF8~1Lpa1|C0001RX>c!JX>N37a&BR4FL!8VWo#~RdBuJCbK6GJ=uHzqb8 -ID1t*^4|#9OjHEJ%V9Rv^IOkj!NLzrVib!a>W~-FmFDMFKNDJv}`=-90@$qtWQrNweJ+Wv$jlsnWcv< -19<_a~0<+wP@08g+Jy|uU7Fm`o>$J!#`)IRGvjlK3PpT{Z^&+k=vUH(;X2tnAAg6y8Rk)4o3;nZ9^tbAwsZ#?FI#`? -UJJaxWT;>3*;yx_mD*5Isjek>SRlH83#W!Ci`Lb9gTqxPZX$EbURZ{Arujtn;F0f6vVRV(`tHQkBUY< -u~QWZ_POsY<)aFtdy^f0WJWxA~^UF(E?K94T}bf*pv1mGTGH%*;ooELezjI;FjB+BDW(ydvgA505%2@ -sB7x|pYVos@Z;8QhcYR}a3fpLcvBd9vRg(|PWJ_$ID+jj}e2$qLV -|;Z~mPI**M_16J)vw5kKLVp*$ENCSUNI9V{APM<($`K_EaRSLULC)Ro*q3rdUF&#fBgDyN3WwFkDndA -c>Mfms&GJvtWmYRNT98L$QxjatO{Y`Tx!_Q;Co+Df))zz>cVNhO7c2_r^8h?X|?nbfA;dH=;`q<$4`% -5Phq^XEUKEtwk&|PDtxfTX}F52dQ~*_R3$(qn3k)woKkh3xe-jc4NUeZF3+J>nZ&DTdC}yT)j(evn<} -ZBZCw;V8Ilr)k&fQHef8$$%V#Ii$hIB$pO2pWEq -eX-#f#$?KMmJxQeCUqCV5l=oE)cWy6ypU$P_|Yev3?b{5(yUK-6czAXD{{*f$1FKS_RTKu7>nK99Fpa -jM=l+YEjmzl5*J2PpkG-@#8nE?#5_!JF9T`logYV1YOr;1a{VukEu|b<=aGhW{dw>$;{)tQ!Dm&+1p{ -_5`>KIy+PYS=oq{ABn}D6nR}1K%7^+X8ed{${_$SIweoja9buIUXw2E9n4>GnX;FT2Iw+|>8s(8jCg` -ihKgHJXF5(#-o)p(T+x)Qo}B#hy0}(z9_7=j1|b!O;n}TQw|+T#eRBNrMfBw5^H-1G9RKj_2#N*wr|R -AW-l7D6bz`oVvG6>ppV6}*B6%G}SZtN7RSr}{aT-+Ra;mlyX3i33V43S>4~98&%fRn#7B7=Pzps+*RE -_VBHPAA_8cEtcP~P&A^n?#tAq-SaiPoak74M -5s_8Zu&&LxPg$7n~D03HHsO_%4DDnp?t*Qoe2!|B{227(82>mB^5YuyDq(p|FWFQbK0{trBRTLMo-f> -5%MFS7|0lo!^37S)Qm6Rw5VAiaQ(|c#?uj-*CXPyB?{`hD?p}1t80r7Wc%7HeCq+5*X2`r*X8^$KgPh}-pk|zDY4iz%qjS_Y--dCLXjKz6qW|;Bz&!L&4cG4N`uK{uo -s96iXjdB4w4Js!788ukK$^XrXwFn$)e1mf**mUCQweo^Ek!9eRGx -dX-&pj#%P7>+>+EcL&^qgQ{$euISiUVr~Syz^)h%D`KkvyVn_*l31E5hs4q;*c;pNGZ -5gkPV$W+l5v};(tM9|QT1^srf%)p7|&G*)XK>-57w~Uv-`64<%ifGti+7=Dy-sc6b&4m&Sd=Wyr#Jb2 -<0AWtQjBYTRL5D_>@BldgfWfc@VizzR(lTgu1V5nDEpbV9haseg0Rpg8g)VSe^d``^@VqPlVR}%pRKAS#b@jm|2YbIR*O1b>?b>Y% -oi!nGC|As$mdE{SDz~fWuirFlWKMk3hvOcU$Oq4f=~%yh8$;6*IdqqINb_{_is_Kv9rs!X_VJBs=gOt -p4~og@3oC**6y{D8`jq!E6N4yo{SFQM;l+rvt1=RRs%LMIHr|-PD7KyP&R1FpEHOfMH0N7XojS#9)-y -7jcaozXML$DHt*QgII#_C#zo_zc{8fqzVCrM8!24k+r%=&_)0Ng}|dB10612X-^hRp_JTO&}^MlTU@} -9%}X#u5XVh?Nz3)dwj{8)y_W822t}Ib){+aBs2Dg%yHgytaaquS_QYb!06Vb^=^O2!HbQ)F}M#;9Uip4*ZC0g((tr^vd*$VX1CQA<2O->Hc7FAa;g#+97 -D>kdHke8%TNXlcSnTAQbry3M!RgpdlyeXCtF$5e^f!Q9#i=wIZ$V4zRBhZw=wEa1dXKlHpogpZF&U}^ -9H5{KNV0Le4ZmeNAc1pWH(e@`Xl`Cw9hKjvbs7SNNHoWC5oRxj9u_j#+K%+bX^JfS)yTl3@uktztX@- -i1zmAH?l=>YB$(jjw|0i7IOiPO$u>l#uB;Kw`UTF^keB&X*O35l(?8q%PcirP)Bd+5nt7$;1q@MJdLG -b*{EaWTDrbyj}vF@OZ4wJl=X7m~Wzfc$ix~3H{Tt5;uRhLH}`9KA~E2p-(voKz*0tcR%Fux|anvBsA5 -Ay_O2M#0mM4W8>_9$q>$9M3)<{d{VkqulXWBSaOh3Ot3egPeslp&1BJyk1 -e|kM11Ri0+-FKWafDf`T4zs`H5KBxEByn+7_g+tJ;y?NziHp=BjnRP?|IN;`kifo!FrbOPN^J_u5k@3 -%}WG(Mvm3kZPyVlsLHGcZL3 -5`-GgJirJ97p+vdR|VsUIx#3BYbaxp7~$IAY*9hM*$v3bk~LMRN6%(?F~cV_rqfJsTS-4An$wX<44vB -i(IKGM4^4v+HLoS?ZX^@kXN?bT5SwvVEZ2pfUiD~s -zYkiTu^LvOI0wtV%hh|siazv(l@5#{p~1ggSDPzcA`QDxRw!iS9sQAdtqU_+>YtQf0@vl=ZkC4Lijkd -yzJrpB@;ffu$#zCs%Y$^!`j7_>#O{Ptk^Pt>cUSL)%n-%xG*{?&tTW!102&H)uwp?D+Q5o_@jRuwzbX -wbE$Ic2pOkRj`BfeJC|rwQ&(#FhL)^@;UWQIr@snqND4;cH6=AZuZqvusBVpyI{8iXzW;*b3@oNFXo< -6_bT*bDX}k+SRf<1J)c+1NzC8x)$-0wZr490xa8Dy@B~b+v!XM)HHx3npDU)D9>0=SBrRgiE -DPgLIpFK&7{~xU2m(yyLZn)0cjTKe!IKfZR5M!bbFU~ChmUy-8bL4Gg_8FnXXfHne66SyjiSbWQ0RSa -|C@E!3S2$KG6P6aXpzh{*YyL6=8kayf6y@OXVV4nfFsG3XCK$YppmzbnR)cI<41aaaKEpK+|TGJ;`y% -y?6e4o}~-c8f@LVyww&}VHn;}C>_E|6>lu&hw8BcQ=IoO<8pZcl1hsUHaH4hm|2tGIoca&TE3a1K@$~ -84Qh3+7eO2^l6Sc`3U(Ed1q+8$eedoR5t(S?Gn93(t7D9%OSD*k_+(QKtN~=;5(J_a35T!I26v6S?PD1k^0^2KQv-jiGF~Ex`VeDpON=;x ->aDYI9TDrJ{j|vW!`?;R{N?$oSz=8(zn&(Jp -KJif_2d(c1ZFdQJ=L>oGF5#$pDirxocUA}=JDzJY^ug%{O3DK{EFI50`^)&-sOm#hRnvi8{kcU2K1Zr -v>l0IecYXjz^CP$oW#ycvyX5AYvCNN9Oq@%=Yp>4PnX*6WK(61KI*>KI3{Wkl0?9Ce9u*pAvX;70#)Y -p!>2JM`%AnDT1zHmTQG;@Et!M(285qTHN8V?^_Vv=pT0cAz%yX5^c)c9(8w~0FM0KBwxL(_Sy5cFJ^~ -!UVk8xpUknTWDqUjl)iOgzImu1!tYmMs;|EZl@D|$>CWmm?HS@Xpy^CNK){x@Zo!T+Yr7x ->@Q4wC*d^`OWI@gMtYVlceXrWk%?4%4P1VtYE4D&w;rTcoEnV8^9p`C1oJqO))rzCp*eaTf6-c695Vc -63cxKaq}~neaZU{%MUH($+=yMhk(=925a06INzGt-U&z_ptn=@RI}0P#>i&KGM+>^5LayS->5q4nN2a%{kSIz_p$)Pfn-j-;mV7K~_At7Orf2iya6XOhc5m^eEsY->82bX6{qnp -5LKrl6U0oob2gZ@hqz?^8Uh0nL+NhVJGH=s%(%xq)unvJn`rLe(y95!DJocZENdRz$06D^bSt6 -?9r#FSjAxGAkB~#E!{|R1gq)=o+75lnn@3Uc`9V>4!r;8<9)kCv3=VHU;~Hk-5iU5oXOQ857~GgG@Z* -Fs+&eO0ua;0p7d6j~yXSmw}xDziUo|*4<{YWGM? -sddb0U4{LVIM4p#d`AWyV^~QKuqL-=dUw+K@|p&;2L}2S*jKw`m1vP<==P|~CP&@BYHN2SVd@@89G7D -QHz@prt7E_@jwX6NLKBg*mI#wgQT4$i-zSVu!!AYkdc>fIozwO*sYbwY=wZRA{{-EI)kRz;T}g2u!3- -)$!t+pq=yw<$Q&kyyWs2tfLg5Z5A7%hpO$kca;$a_8SjQ(fr-4J4Hk)`mM)$9Y_kjUllBw1MsM>871q -I2aHHVNA;o3b7)!Q6T>uBW`hpPt~w1Q4BBs-QY)*jhK>83&&kQ~3!u}E#k;Ms>gXxPV%+7B^Im)o4ZP ->IhnI!OQIhSCl7s;#W}Sp1q$Y|}CrDs$0FXNX76Vab30#PCB_su@mxlKHU*u3tpJ?>+}T+r>U -$xH&_w?{UlxTA&fd1mgDCm*FAX=mwY;fi(dou9#94XHoUAGG^5y -ch_$*y$43rB-Q})M-8(Q(G(chu)23Dh8N#wDaj!Dlv0DMRQ+hy&*2tm)WXy(DmkoaFLAPc(j%On^uDx -my2nLvhC8%ywHEIypXb&&qdq+73GXz?Q2pcp9@dqI^>7~Xouzyp^CR?T4@ClmI6tagUTtPZ8kDajUfo -T>maoXB}f=BP7mwy}R-OUJwpDk|h(1EGS+dS?y!3$Gs&K -zDfo4LBp>AlEYl)=uw6tqbAE#2L^u97hEd>WLE4pPN{q0!^@N8(wCYg>7IeBO0aECP%lcJ~fg -HBz_HFKeQ>B%}~bJy=y&UMwZMrnXU^4y2m|g9bE4FTAn-P>;Qc$*Dx@wIh86v}0_7s&5NK;(Q5O(Ygo -f=+zYdVek%3_@h@6MG%5$iJkB_>u_-e@qyb(sEn~|_N1q4FjE}4j*)f@3~0D*^HAKk1drjL)5FM*uBuORZ0#*p8HGmv;zwOb#7~YBNh}A-*ZC&uv+7HlULOpq=NyD8de8`@_mWr) -P6(OFAduaLNI0#=*@G$x3KHMbnvWB0uhbAn0BiZX0sFEefw_yBm+lNmy@Bh`s=@XOnu0NC$f7f>+$Qb -NBZ`yHC1)4(nm&5`LsozvoBxGuZD#{~N~-|B>;-l?%J!9P1C*O{AULnoMUVg64>|AJi7z2v*bg4w`%Mv&B~-!UBzV@=MwH`f?Y+$s>L9QGh>Pzfby($EEu3ovM5$ -NQpBo0+!txRXqJ~r9Tw#|qc7hIJ>q -jQe&AvZB7zrupXMj&W((qL=SJzXW&eTIKu|!{z>e=y8 -EZ9J-llcu?W{W*|5XNyMU&#BT7J8(4(+w*4SYfIh+~3xF};BwoZ14p3p=|7on-+EN+MDw%&aEjlR#+W -3nnKa(C1PWf9V>6sjj{Lk4?PpU(giHIn>p4fl`iT{b|8{}=7!OdiP8fEl?)7WptwNL -TbPn~wBfg*mT1+ok#6M)wXty(ohyJ=*C^V=X&yzgykZo0T%KQXEJ ->LFtrtN#4urwp1FK#?`qz8Z*(Q(vXUw4vPNj)~l6-){ydB`cP3XV~?%!x(uuYa}oN?41dXMNpjZuGaX -w0%9N5*l#Qg#m+h!NHaA8V)6TzZ*6p9&wF2`X%B&D)sMp2jBV*$`lO$d$GP@GYvp=r$q|uSN!sk$*yJ -hAX;2E{|Fnnm=Dfl&2Nxcl!4-JXqs(s=LICueUv7+sNfo1o3jfoFE}8&jtWeE?L -BeOAsMD{8U=EmmKy()Bt)r@)t*!04&c2hZE5z{n}K0I+2Nf(L1C2v(v=L<&HqCY4hcIRr%uWua^O;1`b1ld8_(~skexgkqC8%{fa!%4sD*o6`yO~kuMs=-wZ=$ap*}m?c$~fwd$~q7BdA#xDrMgXh;-Da1nF -55Mug@k6qz&L}BV36_r#|TEjBLt_{XCz!^pX;WUbK^^boXy*PRM`Uo>ky?piN_~na}fBZwqDI*i~b-) -@mauvrx6CCZHYtm#!HY02kf2hqVBNURj189a`PBMMNv2C93Y?Y#Hi6Ryn#iU;s~fD@7P4`3=Hsi)am?4AD^;E+*${K -V0p#lDag)q+#Pjbdj-u9fo4$S7ANaI_&=@Du1A;stKN~iE@pJTPJJ;~BRdGALF8_um3O-uqu%ReqB%X -he>Qbcp8RQ?Z_?JofwpeYjxGuumSl;T(`1l30u%?K<8su@k>CKGvVggCy+#;%r*kosh=1^Y6}%7MuO^ -RJ$Ge&U!>j}R=@w}-YESk56vw)tg>`#c`?vYQo2`2}qo?)%Jd1d}?&TE^4Q -3jT2%qF27eFw3zhYKmMlz6kII7j~Ue+@HcB>N@Y*0r2^!C_SC&UckbALv$&}@c5O;R0?8r(bV(miS=B ->=kEA&52-b>ce|PRGuGUtV!ny(9Q?3e-|#&xjAA;!DoH2^i&TJUU$FcP5%Vx_mdw}8EDc^JN5p+bQ0r3awQTlKuFKD@8{$$g2d_=y5g(G1g2_CieWUIX2YxR3Ll(F&yw3%?Y*u3gKnK40+6vRvp-s+vpM* -{{Gzupjoaicza>{WpH8^P0CF9L&^%2mdykjt!7>wy>HX(a*h?zgF+^mO#O!7i!Qw+zGNykwxWmxm26xHfTXR02^EI|ylyhxUpm{u8Rmo8FhU>69isp?DLHONOcQJi~HiM -a~*Kq@|4T%mgkSK|tKVrgB3d|+5(lC>C6xa$=xc5*hmOxCn_XN}po#j|L0kI^->iT{baehEfu5t#S_q -!IEoZSSR6%4wLe!AL>!>wT~8hxaFcA?1!Tpx~KpcY&pFPwUJ?7sV#c3#0o=YXtCq_&@+khosCQKzJYr -@BI^2tv&_Mu`g5g99v*6z)?En{G5eI2t{c?IOjbOi1(mO=5j@f7q@>{^4PSqk{|NX^u1$(iWg-Y@CI8 ->eCdu^-XkR1?X+@b+^*Pnj14!osG{@VygeqF{croR490HLhRwY*AapmYF+Bfllz=;0gK)c=Xsxr&H{| -2dBk%8pjSBLA&<+?MF@w1*X(M2+)4?O{Qk_g%U85)w4Y#jn&bhNa2b9OhmTCDMHb#lU(%9|c*k4fh;O -d>JyTxY(nUowED>4K){egRM^vi&HA6{Q%|8mlLPAIRZ -BhPhy&s#!${6m}9po^t-6a`h1t@*r7C;Y0FkkN~VanC$y8PRv2fyfCR^2zZ(m6@#MQ+6!gHfSCMigCj -ecG)`ghD9uUgS~5QQtl*Ey?@iai*L`qJIfsMsVYarnEud~wf-@nV-o6y0P}8fDj+W{)_#IfldEqW;r` -}q?0QzNlQBgXR}H`zMI*^WIhu{sii5NnYXF6Fn}c(l`oT@YZ0>)3uhplvD&t}e@Ae{10B9OursrM|^t -N4v=7^3$?be~!%9#x`T}W_mrH{$dIfpFia!N!Q`{%7th -Ty|g?WXE?C#gt_CLJ4OX9`iz@zL+L9`?#q`Dg`%ta%Y#fe>vlBNj;t#HxMtudDpgQCD-#z*)=00C{`X -JKY`g0%IL3zD(o;?~YL7LuVIx%#@4Tlr_-eTiv4M)td+^?L$#MAK*>cobD@{?ubC95 -LS-w`G;)K<@p^Wa)puld?sPBa)tD^zOc2DJRi_>9v-G10hB*a!)C6^Y`pQCG*m)_tD1KIl@$dobA-Ar -{zWzG{v<>+^?WF}i0X0dFEsyX!*e%PVZK{@2ZAczw>4J6Pp^wh~(=WrRuN&matWfCq3y^eK0G9s;`D( -ZbvNRuay$B}O -9kg8kBHi&5~f?F^+Ja=Jl=5TVUuIX8L3@uTY_qvGaB4JKr^`wKoNk%=O52y`dQEgsgzJp#;`o!9-I4y -R~ZMXi+TqDi{_?@jmQPf-0COdaQr<@R!%-j44W -uVIz2FHX0gJJ!pur3j9wE+d9h!FaY$I`t~kt7d~J+>VH}8>CXm#s&n=j#N9#2U^=BBb7U1rx?&#B_AKw0S1K6jWTNX^7MZpUVC<@nSefKW)qVF8bdf&O>#xF|k -L!nP1^*;aOqm!?%%^%MFhC*23NrdO5gv^rjc)8IRJT@ -+ja?gg#H4~G^QLj5*TX#AWSpO{r^n2Z5B&Z(rAO;B>;iaUbadF%Z -s9*MS?8niO8X0dhu>k^8< -D@h5x;IhcNA8;}`!h3oH%;P)MGY;ePL0;>lK5fC#8Il%U|Pdgc3S+LXT<`nw?9!cFQR3Z#5t$z>M_gu -yak=Sh?H9>v%pCWJxrzD*;?0nM7n+$GyBznlnfPST`7No{=-DZXu0^T-P|{ogR@PS5-bK3Os -9!hg_HMbne=j`3HA{iMY_5uXgL9XxfI|LwJcZC8!q_97P<9@_3%E79Ey5N8@TTI;9*ZRw@<-HI}847t -P(^pd7mh-9mGhRoVwwZs6kjDLOqyVhlJC~3IwNq*Iie&M9gG_Hm|7|xQe5-c9HiPW(K_kSFc+OLzoL2 -(=X?D3&~dY2OimoNwBaveHF0BJerQ$Vagbi{0v?br9e?Z&PwSpMma8V6;r;Z^7!L*EaA)grHgRgUyDp -`aZghVDYhIf(~|+v=VJZg0RKmm9i2#zN`OGo|tMmKJyLEch+}FY3kpxY0otgQ;z?KZ}NI?~YGu%4>vY -@HKPbS<da680JcjJ~ -_~AdmRsrAAQ}v7HB+cXv1&WiW6vEs?KRj_zL!DYiIbGy2H82dltEV!N^j^6^bYaRySU+di<@{jloOCx -3a!SVk~EI)0cv1BSs7u+~J^PJiwh9>(F)Bje)D2E4l$1X9Un{I+Qe%w3f=`(IMNvGsE#EH%$7!zPJJb(4Gj))}Y+NOD -=uTTTa{ReTE`bdjf@a?44XdeYQ747(`ldY*OHHrCJQKs(FfekRrMtCnplCMI?=;>x2FY2p?Uy(^Bx_9%U` -|&tXV_-_{U#I=a|0Aj6YSA~9oXw0+&)6Yb+ox-<#Al`Oza;TT41M(fLFAJPg359;rybPGyWZ+D802kzoSma&Snd#5-SbiOug^0_MM(Z@7A!GyNQy|h|JZgFSuO -(I`f<8)zHO}Jf~>psA?9%OMz`=s+QNQxvL0$rWGmUGMB$Ti9fA>OEq+jq}4?*qDemL3Znmj48TvH9Ke -ru(>j^E202CysZaXMJMic1Meki;>Ac9mN(}^C$q|Fru<5fsaj6;0n69;3CcsLotzPY(;!nI87sMjxfTK!QK9oKcgs&#QMDZn&g5T -ctQURDH;S>%=Sc-t5&~mR$c`#b+<9_cz_1@Iz}dxWO8K#%JnYjn#`r%dPEt`8e*R$uS7~5EGyBy$+(0 -vFS-CcCNM8k9^`19N&@$bc2WI8^pss~Or2e%s1`#hPCFP@gM!T#8swC=g)k$w9#!h06g}LiQ^`j5 -;3}bI{=)ZJm{OiplZ#vO77Q6O_{#?hF`7UQV&*FQlL$2y4`j)I=bUB^Fmi|<6LUpjSpNPrwXUA{T7P; -o_a4fI0UI12Cfcl-Ew|?&vrFdlD=|*oZHflmAmG|CgJ}}p6QO6vBtt1h~E`~b2uH;ikaX3)N6&;D^^^ --*pLgu_1f(o4r?^^dz>4LS{vS&JjmLKbWoFVn0IzE@b1SB$=orM99Bb|w`YLF&oP9IO -D;Uof!SRD@3xFryC$#BotGzk{EP3jY9y5nXH(-Xfd_ye$&W-HyW;CWqLi$luv2d^JgB8`bBwjzc#g?0 -cz>>99oVmFu3RK4|1ILf`d1J;r=3>fgOfw -2qF=9+jPYQj#n6lxgn``$-4<^(HRBjDtYBZumE_HDMV_hnJce~{ru2cv%e=VG_oE=mGK=!kXMKRWha -FqIFn7WfbPk>U#P_0Fq-LM|(w}|Qc)MNcoJJIntYd!nqc5S$&u}H9b*(Oo(Aro!jH54r6bMzw -+AeiI%$L(NT_e2+|zM3SqZcK{4c0XJ9COdn)vX-C&q*f_PMs^KVvY4SF7PA(a~-}j^NIL$o#uf+BGM< -Md7oKI&|FLU454d_N%;Ng?i1OO@>T5gQ)c2a}X6@k2yo~r?Hf_hp{}+@vSkRHZ0bjmCffG*FHLqn}|< -IxlP^re^5&U1QY-O00;mj6qruVcj`1~0001l0000T0001RX>c!JX>N37a&BR4FJo+JFJE72ZfSI1UoL -QYC5$l+10f6qdtR|3MIMxVzz61Hov{)4iu^uEX=g^0bAH{&pmNd4C~iSwVkbxy-Y{f5*XF&GRj3$4EO -<65jLAyDb?eG0=bX|5?~g1r(SK(lZhQ4bbzU$JNFP1}>pr^ITY04)P)h>@6aWAK2mmD%m`?EJz?)hJ0 -06@m001KZ003}la4%nJZggdGZeeUMV{B0e;tvUD+nb7B&GEe6TCK-<0YtSMXevp0~Y}2!kf2@ -P)YXp&DT$N(JGZq#Z;Uu-_BRP1PFlN@tM#^l!{h~o(cH$YnH7L6rftzC6`9e6?tpa(ql;*MLySSdb}2 -bi!sqCn&`JZsA&4K+^bI(Dx6FJ4pQqOFd_NMXduncdDSmkCi^ESD-Yz1{jDqcd>ojk(9@{<}}x#I_2KW%M?d(?Kkt=1W;=qne1ibE>~#zJ(K5o(QQB9IVI(7;|`(NR@YDWbn1^ghjOq4tE8QH);5M234luJC1PD3O -&@`_6KZYpd83LzBRzkR4J}$f@zwqz>3RAG!+TDOMQyY0>Sp>D{ -;6+ppJmciHdPUv9@SfVmmuhUR}zkyX6jW^gCTYx4qfRw(^u938h+*~rFL;Y>@{h1kl(^YIn#MzF&Y-a -=C$=o90ugu>P}zhShaC!)9H;;CR&@leq-l0jKxC)CN&ZavW=N`OqYW5*yd`z@reDUaUcFzG4E^YW9>n360J`v -i&cKsT!~<<%X7^#je$K%_H7L@BV>Z;0T+pE_Vn%*W{Q9=T$*}fKQ|*S&%*r5~v?2>T!SxM245kPP)Ir -aWk{oud+(9e%Z_6~bZsiDjpuV?ykL$HB=kHdycTcabrsvh)8}xdCO9m4RX?9bLr?iq`gMnMO?tg6$<1 -EI#3CxCku4zf7gf4C3myxa^=I5A3kdaQ68`}!rHZmysj;qcyH5o1vbEDWr&9od`?;0xD+b=W%A#7m^j -4&IPTl%^A7chDK=)8UJJ?+%Z@K`5XnGBJSTB5Lqs6X!WHpCLZrJ#&Mz3<2Uf!Fo4`Tq^c9gmWUX*@YG -!wcE4V9z1}~`w5OgI;eTxAMh-L`bu7+L!b9T9l8X_J&2yA5z<$=qvfNSZ;qk!Gv_rZ&qKh%>5e_7A$U -*T;SAl#hiA=qtIL(_9f~g&v|q@J=M{3vYsKo8hMR)UJ0*}x6W8Xv8S;vAD6~K5iO`5qzCq{F)IC)Y27 -!1K^(h|Z7`${P#1Z0)2PA|Z{{i^uyg&9`Il3m(uH<<$as=st_GP>eEzd6Pu9QAsY9xruAk#6J)R~#1C -$gNN+qME|bbq3?4SN)wE3DoUOcz|m7?bC}5RNCB1C!M3YFeo$YRKY}cp6r&d2MP&2O7%cfp%Yn*1_Zf -TaFe_Y#fJz(N1gsv4j#97TyP7ubPTNVM=a7KoE>K`ZyIj<*LoSKh5-f`~8B;6qmF~;MmiAH!=@8uHMY -zLE?Yh>2n`92A{=rI@qHmwBiL00!T%PorJ~6SG3h7_kIMbpz1K)>y_b4F41wD879jo^>K -MZ%P~j&;;vWaP5Qh1s8$t73fJq&6o3_};$<-!S-8C1YzvWUP1LWougJO8&(1>J3Oo6%d<^ribWDMO7`6w9J_Fxtv{Io?p($#pT)i#re%u^7>}+_B1(NTqgF( -)y>Jt+0_;BUjDp1|7B^{HS+t#<@L?0w`a@u$@2U&2eN<66mH2ah?qcLC -F*EF+MGjry1CPWFESro9x91aVR?9zo`cf73S>ze1;DHmp0)43`506r`?FEaM25gHDgZrB@K6d~xmOsO -Zfs0(1VNovGM?zm7j%~^35LKf^_XqHlmI#s3UZQTl-091m=3Cp;mtDMb-k8<#x1N%WH8jk18G0@hB*f -key#|jm+Q}D=K#u=*wGn;hiZi!`w(X87!l-HvC$cihKfuFxMkL;mIrs^tjPJ58vj?2{7&xWa_66Iz_3 -Ur8^|C=QhRofW^m5iqF5TeX|DKev?TI2^bWEg;8nGQuxN4!ezm_UO~(k%n0KZvUKRcEX3e!KWBIr-`A -jPxG3|_IWQPP}qV4{bkEoeu68 -9z@5#&**K)j)>y8X8jX?!4KN{d@?l~fb8S$>PU)>)i;|?EJBa7g(P*SIfOF`mT>Z{cH8u -J`Ai!Ui1ta7cYK9#*8ud5BSfE)7k}5nG>=V)Pd?rQzdxE){7kg)lVgGmV*O+01E5{Q^T~5bp34b&P9kUR(R60eP -*2ky+S+9Xza;G0+jpOLi{;uzG@OS -Rn7zXG5X?*M$6Mt=U}X5Ng}spoo58LM{GtWsw8U5rTtAdPLF# -kjF($z+vwO~qcCguZaB^m~Yh;}x_z651YL*lrAw#9~#j3^yX;UWnMhzy|Aapm&ggWk-v{ru%8J(~9#x)zz0Mo0hx6r0 -=FfW1-e9f`m3G&iV$zhiGq|2~{5Ae12C_|j=Gg1l%7G~(+=45DD?irA6z8*SH3*ehc=zw%whBMBfB`Z -I17L*;SDPwi9q1c)Yj?#h_nU@ -tmvuP=+>`ci3yjtzbLKi@zii#Kmq-ZKFQure!(cWhg>{Nx)H8r38VA#C%P`PL)2Sl|GzIo9g|R2^RZ_ -sYPsk&a5%=qFI4zkaZ3uXLl@5ih8%wm4?@o_)gT2pZnZ=Q-dQ<%e)lxjt%Gq9}KMw+34o1frW)&L -Xd%YRml<;C*emFk5C3A4}uV>_&sqbBFmo^Lo0`l_v_$9gLd5* -eA=NJ#^K|K!!58h;{>25~FH66v@#p46w;RghC!>hQ`C^#xJMUswQ(G-OppBQ@Mkkcr~q;*xDX_b@bH}tpu4KAa&SMbmYLkJ`Uhc$(L@+hdjqVo(=7 -&u^U}FcEi{8`RVyFxrUuXs{wr*>IZg*;|O#M9E`OkOlmrnA3FHvbI)Pd5WfYJ)}MeHRD#gJasldqU)w -Uz4Don?evK^-v~VRTVh!E3Y&tEX%VVj>uMksBhZK8Zrk}ZdOKFApFu7P62~glgLRujVD9HAFNS-fhPbblW9lfQqMz -+l(|~AU@XHBQ0#Yb(*1L3;UmKY2dXM?Ag)C6B{8Gw@UR)f6+HJz_0JWGqzHg|{hEc!*s9({lQTCDCzx -k&YJ+YwnIUzZdgj-n?uMO?yoW~n{T)Ps5qfA&-WkGA|0gyW+)ej(;7L+Wo=d~-vx<3W5Z;eKulJZQ2x -dY%Q`NQ)!ZWmM;DNag(}GP=daxxGgc;EzwSrs*l_oyKgi@41u|SIVrT8dg4_e=;!M0KKW1L$sOBONju -|m}Hg93EPieba>9F!tnN&UBWAbfiE`sT->uQhjTvANa*bwvXfPuB9!Y`-e0$QC%!7Imc_F@^Jr3De-E -7#H#!)8t;)=abKC9#980L?02lg?k!9?>?^qfGmPp%;yCw<-LFX5iAO!k1o;ju-LHi9Quem(~j=*$Pp7nQ%Gc7;+0x5xb4AC-z$SaBHt -4FsqMs72ls3uuu0W58|VS6E~)yZKE0znN8WwM3Fv<4F70g5jM@E>XRvZ`8V){m^S-abkKLGb2Q}EYv; -Kr=SWXOAu$*;L54VG#!4X6jyy4d~>u2O@TPs~oYc<;&$6cA#J*~RfBQ1P19n50vt#@-b)w%C-*HkVC9 -vkF;U|qn|9fLYOYezU`RpWtoZ&&diE$aQAwWE{X)mlh5-UsWdnYZF7H8YsUi8TH;Akw)>eDJf@m*QPP -90mW@{82*9F%Jn1hw2AE`aa5Aaa2GdkE7q?rD$UANqzKY*5BTnK0&{s5m_9^!)ro(-DdB$Ea-Tuu6eGvp+=p4Ne>w3qYsrFHJRQEh}rSHpIPr^ -|Jrfhw6#*231tzU7#&RqJQj|h0ptjYv0=m2H-VnR5UIX2;F)iTuCaAr+)?kv^{Hh(Z8TC1El*4eR;dF -OkuZ*Y5xxx_mcH~@&$C`$;|(=}eGb=IPm*3{QDJV1@1h4>P5G&&EzDKgY4Jf4xI?WXK?w>v(!kU(G!C ->&~}q+->#FTZOr+NB}*0WCg|S)Z!S|5lUDY{9-}^Ow}>kix1REdKu~ITRSWe*SqP!y|!U;PgQv2ecao -e(>ZzW9)D^dbaiAg#~+ILppYL)TA7YDP77^t8_*bbVVI1e$hhcN?#>*`m9Hi$oxP;@@9FuRM$DMQ;j$ -N*n64rs!S1@7+-HC)VG+;uVERiKL={zVWsLp2g+4Sz421_#edv5V$U&q0)gwnUvKBWYxzHWLI}2IB3bc*ihXw@?P~Y6fd2qcO9KQH0000803{TdPU -1+jZM_))00mtD03QGV0B~t=FJEbHbY*gGVQepBY-ulJZDen7bZKvHb1ras-8^e=%Cf?!ZDWu<1K -D4Vp%Ya#QQSX4z@)`P+AQi-xk^CT@ZC6=mEBCSPJh?$zFxw3tVJW*pYZ5#2IylDO+(!6eD-lVcoqFJi -dV38G5nPuxpe1LA1l68@bsY>$&yqApy^q4kFArt&@8p^U%`79KP%;A4ktpz}B)L^QbN2PL6%Bog%B+9 -hZ-^D`Zs*;(7*F2^P;G8$A%5nTLO|M=SZ8popW>Cr+v1h=MxGexFkwDM1ZdBet^DJ|agCFxE%Zf+98V -E5{b-Ku-!5{!K&Z}Z2;&|RRZKdK^q^q*18X>23k+qGA`S)O;pEv5Md8}kMu!w)wX*YWw;`P+;5_2u_GZEcyrOIa_K#r5X)`osG-@!PYTw--0NJ2mSPWmk84_w&`yF--6LKjP~j-oL -+m|7Lg3HZ{GzJ-d!?f4+iY2I+hYjFW+Ye+0-M?kop6J_pH^(+t$^b(%DWkHCD}sV@NB;tC&CWAUTRT2 -*(geO!|)ji__gaXp`>3m3*x+h -7vKz5oj59JgXKpgA%&I34(F)($*(Yk#5IhIPErC0D>oIsPy=#n~{evG!UmE{Pj0(aUy>GEz}}9bZ6-_ -HVPii!E6je5k|ihX+DIPQC((fGaQY$xi?P&!h~JPdrMa`$V2)Z>kgWtFHktti5Q|7+<}D{qkMcEZ(-O --Ouq5--Q$P?yOnn|q86f<{-TnG8qnZEOeO+sX1fB9(U7&A!MJR`|jLJ2*MaMsZ8yE0KYsv`_O -Rmhz6M??mQxoeW3(=M{^nY$YZw}ns=|FGChz|IBQMV;XG)M#xNVZvnO?m{ceSJ#j{s0(O;IunF`L9n6 -7@iw4Vxb!3|1ckQoiy9vm28@7=)o8Z&{5(AHOIaP_by-_>Q=#61k6tNiD9>S^wCU0=Ih}~f@U%@V+K2 -NP%;k8&Yx1^*)}Ffi15qMKE{6`O9~oJl!+E}tC$!9j67|@XeO={7^gIGl+9{NBP$@_>31@#cWMfTJsaBb9Y -#Q?@~HhY1B2%{m0$7I5dP{cXn1s2SFb -?6f)1~mqWLFu~NP^5C*ETPGR0+$JXXY;+D@#Y&jO*bEcf6vqkf?W#Q_&wNE`b#3Xj*C2713F&tH3Srg -qp|okJOOH+guk(r)-;uw!pL30xkCN=;VZVcXw^8V%28xlN;?SW*qC@=B?Vr+MmftEU!olBNe9fqQ -86pwjU$XTsp3&c`51h2&k#DxY)iWT6XG^Dy{t3*-zDuo4(T_WL=NnNTWou`S`GgvOvsAOvrG61H5ELs -p52(7Ho#1I))BREzGNP>Znj78Zh3bbh1Nm3kd2dm*rSRar|Np&WZyqLiBG%Kd$%QOU(h6zi?DT$kH(}QJ*MZma2$5{p`h -5*tT0$I|7a+0Gf8byI?&oqBLy|?f8G0v4-?gs|3I`M`)#5h9h90Df<)>7!ac1&9Z%gj_*X*8y;%`*k- -+h7xvHsr|Ghg6Et7^%>2i)Q>q_iymRf_~vWA1qZ?#-QPP1=lZO%h6kTU<%M#NQ6b43`(Gxw$q-)2lx6kQ`BLWty?u8MBj@sdVYJ2I8B -Pan@Yg*KMr6ZhZ#C2N_XaAW&H!0e@>0$c0E6H!t2L*fw_fWJ=**1|~k^-k^v$TQtoTS36*OcNl;D3o$ ->MPNt5M4&$5!1^HaH<&?hQ0GcD@o0~shPgdWkqlU>wyX_4}rnlBF!~8MnbhW*^l>+mM_@0&ll&}PTU> -4Vn#%wtEaR%&c=YmQjE3-%YQPK)Y8MzeR8bibk>Arh%EavWT*@DLl!47Oo|G)RTlY-^09^Z>FgR4P-A -g%ef{B@L7iV;-d>)ceGjqAVgc=3I28WZqbNMy5S3N2KoNrU*MQP%Vm)!hSU6`KF -z>C4{gpZ!n_2m_I-kfR&e?>zaihcrU7vMWX85hkQ%)48pwPXiobT<{#$2#HpUfy72!uQ)B~T0&-Ay^C -(sZ$alpAj3tYa^m&rWMl=%hWC%<%@UiW(i>t}F4rvU_~%fQv_D|moYmcrOiPST^`K!0#WN13KH2eIAn -d0qo6AbzUvX>CUx9HT|Q`$o%s_^1015P+xkVaa`3_eIt|VOfQEv4>e0dbU&xo)kPHgb=ulr?~G91{lWy3suieSKlTYcYU5UJ1O>wn67iM@RlO?V>a>W -aJ-l7q*Y1FE0+z>V}N@I0~?HGjX$0!Z9E%pl^M#J2S4Y(erd*~@iSgXXNi{FgtGKa-ypE6&#qzP|uB? -64Ci{xJMln~9$$-;18snOaVGu`?=En^*DVPILqkA-^LuX|^L8?kL8?*U&ClyE^AV&RL4`(i@fJ)-Qu< -5N_@-@rOz?v#N7k6y!QY!8P0?2?lbrx!Yb$WJ=+|H63K$0hAM;wEKQDLGp+{LbJ{COqJT2MoJ>3)8_v -$9cQOap6DXyzaspWiiE%NBo%LhYUZu@R??J&a>W5YWKnl?*<~QsIRrK@oS#8+%oa{q`$fhhV|$%ZIb0 -!4AT)MYT(=BC@t~tbR?5|+`|~k(UeTo*ko=7iK-5%B{uQI%6ZV2Av;CH+B2!viDk6g)xg60OibH+mZ{ -@xNRp)>!tpT(Q_C%QcMuM!0H!;SozTtA_ktk{gp9cWm>tA#0uwkHcnt^vDA6oAO^OD`HLYcpJ|I2r6( -!&3y#ooI&(*~{!CAZGXhl1*aa(2kDV4IRrPdQ@%|FX^nes1(b{1lv(LY>&-y&=n=J#1+VHFLeHoPQr>r+DvDSwj{#Xao}HDUrQfqXW}()_+=3Su@8Z2FNQ1xXEphzZ1VeQG$Fvyyk%Y=}}f%5 -$Z~N}MmLEJT-c&@ePjplYRY3b|M|tBg$L80A3M8PPI?X|_d_WR)yo)m=w|V6xT{Na%&2PF}vGk+9k+1 -cB;Qx}Ogh0S$@_qcII=6Zt!~030yz)D=Hu4M<9jxyxk%3*Kik2gF;0nIM>;$C$t!qYgHB6EJUrO+Ex~ -Kiu5DKYMo(9HZs$WOdJ_RyYvtuJFKPp&k^{qi7dPkv3vh0EI9)zLXD2Jje{pWS_1`%zQ20-rio_^oiz -dcRXL96Bg(W)`mbk!~AZEMzQLPba-wCFljBbdxN60NRepn&JMzeoXy{myqd0~aI;4%K!b_^O$yQ_o*U -OcpL&&-B}QNyk9xpUH16)qQG4!Zj>n+qhrF>Yk~`}e~KU97>nu)pYu#QZE@Q=1?N-r#nr=oJKT{aIw -{BzpBd{%{d;$!J%+l(?RYA*SMOU|XgKe`6|>qu#r!=}dVnwWN=YN;+GJLMvgkv8P*!qU-y5w4t6}MMb -1hUbV>ISUn|K3sdSeUL$OVi|pK-XBh06r=Gbn1aTVtKO~7e{cVKuNYcs1Arst_${nRJ^k=lsW(Q7PLN -L~`ln)#pn;;5nM5YkSJE4+!bh;tmW6mH1z?dzel|RNtwx_Ul1-fQJFCieFJ!gMrOZ#94xs<+afDA6(b5CAOaF5=eA2J@m$cOEs_?=84z5aMN`o1)fl-HsGz?H4>Kr()v8X ->IESj=SXWTpM4`r)`G)T@;bl96=N -sk6yIuD)IlK>^fyQe~k2c>iUPsuddhnzbd~y^!|MLy)pudO?r7Ff~GafFL2|NF585*C!~F7yM)%3Djn -D`)6M)nGyh>1l=d6<6Q&%Z1|Km0fwG~CVY{L`rrRq6=o>G=CrlFD3P4wHHK*TIn^P9GUc-_UrhC+;?Z -7YJJVX$$#Lg#+2WNvg+ulB#L^?t0QO7M7G?RIyzkEY^5GoDlnlxOoH!MHC5e-ZP%0%=^5Lanlbg;Ea1 -ib^3`b$qQ0@G@!q6LJ&Uxp*I60;Z}8^MSqOVUKIH;+`Qhnjf{=AOsUU?YK}dgyrl^{8L{O%E%xy70z6 -Ryh1p$pgaSH}-Sn+g@4W*wOKi6b^qLul(QNKx{??D@$B<{j$c-1iyPg(D;RE8SWJ(BR@N3vxkZmBk== -F3G^gHxd63J)V?W9}qmd8eQvRhy69ffm>+I)xN%+WLLFWwd{si$1)JZEnLg1>ezs5cl^X~Yy{QIsd -}Sq$*FD>p1K0z>;+h<2(AdCD6$!R_CjY-0I`7ZkMP+KngZ*$6NrQqCOr-m8c{n;n|KvPws)^mws#=Z6 -ZW8Mt_xsr+N|mbPRwK@F%$9%a~4OHxR}MBMJG0asJjB*zWOzphpVbcu-r>>D@@irq%$?6)CwZUwUmYo -M##8qobrQMoYx(Ei$0iiA!Bj~j*H6+I-$aN$ZFA2!7%9&xE!<$eC))71EXV|5Fm{8-Y}4clyJ#8?!Um -)L1Pl8wGQzxtxk1V6zkg2Ag&f~i$?__WNJnli<0)c5ZF!(G7dyMKVGGag~GZ}GCNb;Yt~Si=J%?E6$lppsk;Ng~v|9!`1PmOG@>cuGxHUtnOd&1=d&@k0U+F`vw>ykc&bfF$kla{a+<&kB74m@nw` -hG5kMCo9T#)RB&kQV`lG!)l&#=sUmB7bHYRwZd21S93ZM|uMQiv{{j+FPw9uWvG}>Z(Q&Dn<)yphpYJ -ktMC{HfM@_{RK~LfDCcoi07o)T8P`K)prmKL6!5d=D)itUcNjufk03d(%@BI -okmj6#!%*bN(TNq-(Lp(pc^)THL-QG|X+8 -=UPC^~jRPxDX%+8Pr(AUA?yFB~?G-vqkP4{=Q}f`RUi5EtpAo+K3l20D;~Wl{`s1ej8+8ypkt^4%jq% -QM~TQQAq)i>gn8Zp8>Jp8_gty=>M=ZJgBxHe07)0cl1D-Z+8aBa2*ts@YB%7|fP+=9rQ0V9hNC2Zp!9 -I%8Ten`4`jw=TXn;)nk}EjH&XTbRWkkZO<6d%PiR4kLC;n;!!@n$iV<-ot%1wHw>BrCtAHgQQ-vDMYr -hefvAflr^7Q56ioDZipGGK4EP7EmI4btUdnV1^Re-v-_|XtwN_Ox@$9yl-U6`~&zK0=EkoAgEQrgrlI&xC`j4P{R3}whHu -dStwys`Mty35VBbU`_d`V*ksGUg#MvSZWN-Acu`(RrY{1u2%?jG0E2G -8M<>wl{iv+)BZ}ZRUU=cz3dRa^QpconoMCg&ex=lLb!(XqR5iO(bdLM9ap}>q82#k2t)H&3@-%kB4f@ -&2ayy>|$9RI7E(V-)EzEnN!INrl2A42TW_OD$o^k>;rYBXC%H|8%SQpJixmLs0cG5$y7DqJX74}|CN^e1?@jMWx -yRK)WP3bav&YGBz)e9yLhFlg^KZB#Xqsa3@ZkyG}o+IVEIMssz7Fo;$)UC6xOZl^U1n$9-nbmG&866= -;#g`HOgMlb#_xuR3ap*u!x*{%P{8l!!*7@vWvmDy#>zw`WvvG-}clwW@RuP1Vv%oOlF6rXK}q?tW36k{?LRKM! -oueoy;8WF-7dsm=TtC}?KGfu*MR_`8vXr&Dmf)C((Gb}G@Lj_0gYpDsO6e&I?HkChO2ixD5LPA%ueGO!j -UnqqqqFV9!bRO9$eGI8uAJLh*gk-lb3w*z%^oy~y)dCOlo*c_Knx6QDsFBHN(!M07Xb(d|-Qs=~X -Sse8P6w^8>_-&Wy0@NdtfJ6XD)S+VGTn7bvsY{}Ew9hz=2=pN|f``v>b=ij>q-obDOkaN(=DWp`D`>Q -)l?ABW9jT>(o^wg;y*cEm8o$wYPe)4hfTPGU+jkD&c?+$kRhii{ux;;SZ(#Shm+GEhnC1-bX@G#>415 -ir?1QY-O00;mj6qrtQDQyFr1ONcK3jhEh0001RX>c!JX>N37a&BR4FJo+JFKuCIZeMU=a&u*JE^v8`R -&8(FHW2=p- -W&q5oiTE<3M!DMTNZh2iCY8f;SO{qGIn&uhBVu&8}(+Kc)soJBg{&a@#ojivRUCHSXzlkI7Yir(!x_h -m3!!0_?qwK7I)iii!v-BQzSt$=The}vxWFu>?SrGZ%9#>il^Nf4(a1g`cLV(M`{*~5BpR?$#tWD8Ka| -jj`r_-u>UfVu))Gn|8$H(^{+2xy8mv7l$AFi*ju7Ag-9nWlU+*zI1rH7lot3?j)q_I&HF~;kfF}Q$je -9(eh!3Y#=XiJYNbreXa_$u=GH47+GUXGs}5N{qoWghWxq{=ZYt7TI`ez(5I6#>xyJ#An()+FO*YOqRe3rdTsR$> -+)DL_fjdcJt%T$BR7Buz3zLm1u?LH3X_5Deh5E4dyYCP|@%^;aklrt@eV_hOnDS+z=w;Axn!z*bP>ywViYNR+}Hv~+pQ8EqmgbiE+3bU4M#js`wS -cJM7eR_h;rHrN%{6q&=P%tfI$z%-wD-z~tm@QGuW3_zOg`{E2k9VK1 -=0fB-O&hn92IJ}Kr$-47BUT9-@L&Rgd({>8|>kFZDk`=Z!dmIa!4fJ`j{WRuGc-Dmfo(euW -pp8O|lEt0r5Vur6csTn}=bP3#t4c=O>=?$pf~?LuY8dtF`NV=+b)aN8YXRb~|wv7-2IK2+@SGC1zdRY -iUKg`(<7)fe4lZzB$RKe| -uSGkFGZKEEO(zT`JB@NaKLXthQ$*kcQ8Xo^1@>T`NfZ(u<&^m}`opKvluXqNGjhzyr&Ahy_+;qUn9A` -@-x3jyZXsTom`Ep>>_!V}It7_xdOrp4I)PM?H2iTd;8FF^2;SP!mouBi)?8xk=O1Qi0&DOrg&wo%I>C -Xa%DLgamv~q8_N0_!6`jsd#qDOd*e@VzUrviMH95M1iE%>jwGXJHv-WO4`R=jUch8;c(vb8#>G7@^4fzB3jKwLs}{olSDDywaevlv6G@E -dA{p(Ct02oUbjMOLbpm(oo2axUM$>tCH8yX?END!H4Oc;Iw}8EvwMA%w6!OcT(j5SYTpZ4Qw_XQZ}IFyQ!+Tbj9#xc3j!Z->}%5EZqDS(iUF@XCbt5@-~i-g=jriV@5Bt_!R#;D4 -q}2gT<3YJShZV|AE?|b_~(b)BYtxS;pfcqY=z>+igl+n*)l>azHeAbj$9vlZkag=PE;BAsgT@6WzWkp -zfNj;pCi}Yb4_l*W6NVWqXsw*v#LI0 -EE4Rxo237=Tb+RG&Hp}OC+Q&6X$X{NlpA+~3e}eNK*cKkRCiNYayINoNOwbpsy)?WAaWEA8_s>=|N+7 -yxdJfbSk^wwhK~fX3^QFZdNN7`%nrR9rscg}++2FR?5pM{A0#q^Fo~nbZC2L5FW{=-iYr?YqhNQ{)4n -;dZx5|OOBYsYyjX%q*DFD%K#8T9Kd0#S93!6P+zmz!)p -b)ljLjj1(|Qi3k9lx!LF*H+%#H>iU -{akAwAIo>=CLRyo?=)UZx7_wlv4u?jxa|)RZ^LD2DM<(JiacDsQmXHKj@-5>_ep$fmc09Pz$0qD0L|D -T=;I9#Bv0B%D^>-OxuIU6bgDL8$&^mVt7B-e1 -Vfl2#h2b)eyD7iqg@Uy7@thj}#347A=5v5#>hPf^jDwuD98jH$Z~O49(6*%0sJ82~r`uP*Ps+%?R!Fq -++Pw%wh1jrU*x~G3sC1?tAzn0Jcw=^Fs;#)T(RVYh1k10@($?sn;{a~Iv?UABfNqL7u{&9fX2!A>}9Q4CtS~>xs)&dJg*nJB*fz=(})@#7A(1LuO#U=C0Y^&73Jp~o4RUb!8i5gldoS?d1j{^kXFdc5!TCYCvh`e(kI)!mF9$?fZgujkRao#czc?w@Phj20# -O*z4-4R-Qm{P)3P%sEa#2X%}?xpBj$DNn=k4+n3%@1ZT6fR=9|AM;-Kt4+L9Hz&96v@JFXU_1ohL&Y~ -_B@L3Z5-07tQBF&n;~#$%BW(Nrs%~i+tt6eQnWyS>aF9rVHo8Qe#SDf4Sr#$t_R>i_=c4Ld{0f^ -74xl^fLaRuV`4^LjJnGNu(rCR07fB}YNLXhd;5WH!}Z;u!CPXOgPvG@d4@A75r154ni?zj{Uj*-wH0ef<~g?E2!DmK=_fga3jUz)H4lPf!4a&_1mYba{`rSi&yDwO)# -}fY&sm*;14t78jAJ78i(dqIvG)qGxilT#BU6QghgJBc}2!59U6@W_&4b%yP8amuh;h>F|2tt;CwaYx> -lXJ~#HO|+w*iEKNDYTNpyn;H`HI5qr%-Mh&$Q3@DZZlwa1PhXv%pQ%sdAYB|mR1lXy?+55Z6J_0%V07 -+BcB9-4Qx-YO2E0$#7e%~!Ku*QZ9WGvRt(y}LEn(`2Buw84NO#HRf**J9#ss_!8@}rAl6)&wbKZIpAo -)qObf1CeIh0tR73%d5j!EgnL**^8Vy?Aw%ezZxA<9pK=u>oqbCqDCKyLtC(53TZPy5rE!VT`0v#vsSC -bWle7xz{B91T1!{m%9#63%gtd?*(xmb;@;0UK~nA#@nh6_G++wT;ul8!r3fh -&gV~=}!rTq;Xu#2{={cWMuL?gLIrjKI^^*#EtVyfcz|4=zE2^sI);x%ndKnxJui1iJ_vwOV=?n@>bwm -=*L=D48|!|;8b@2`U=AhH7=kGQ?w%9Vnfpo3$aPb6q}`54Y#n?6R)9e)g6{~ynkyecJC!7c;(HqAZsD -NocxzVwKR3o!NDmGM8-K@k(ubz0j@a%w9L>uI;tGTN)}m+5_IZUyvuy5R!(X_a(Ln&}bn!L;Q84xt*w)Vn{PLHctL$ -2-H8w4Prhs1ybgnFcn?q>+dK+poe0DQv-ULfEYF)Q3fL^dt~PEQbRIvC(*zTgbEd9+W~Gyza0S*~Tr@ -UqoD?GSh1VgNTywuXJgAb#e4@F-8PK9Ww{L{}{O63kHwBsAvfS!1Awhf^Fn1c}W*O`=BaO -whGke?%*!AuEoYo<6?~G6Zi^pc*bL^YByF_1!pKv-FX>6SIS$_I5>z0;(IClXIL~ZplUVXJ~H+ddEd4GIpF@BVt05_`BE9x)cbU3~BFM -3xg5F9FMATAFELcS6MgD>2$Q4i$%rcVg;gBWAawh%b#~nC=%ZUzrlfC}_6I?6r7mdeH!Q|6j4EA~#*l -B;eL66dGk2ZG23${{c`-0|XQR000O8B@~!W+pKr?eEbYEXCaCwbYO>f&U488kT2)<4ArI+p|WI1GSY0=e;@VbuLYQW -a3qTFnS3Ob#bS{>(&CNoffQ00$^;X8LOCZ5YtU5JRFpbdpkJkAP4c{IO{*Z!39B2a456=D^437^_lfs -Ibe1z=KncpT$YU1`2g9U@@J(mgc&Gp1MaA~5qGO7IaSlrp2)Ew6RSYVE2AI2ib}V_Bk5npp -OTD4*An=BI+EhHH_=boEker0rd6d@g?8f-E2PR-<}^H?jJrS39HVSuM>igV;wWtDz3=Wse#^O5-w-iE -7g#PxCgW4V~zRbQk9^{nrt1_SjLsJO=iIxr&>Fo9Qb~nxCdgRSKS`35P?0^DT(@|l80e)tf^z+ -&T1b6`@hW{`z)MXu)(A?nj8(H=SVdamNlqy-EvBP0dj&fJ?4CgD3GGwVB{NWK3^lj3u@m%il*wumbm| -CzYdA|a!e-98Onnemg;BhD!Kv0xmq5@{o@~BkdB#7BSq{}>HRDwEDOg-D-e#Z{p{ZS>xa=)Vu|mxr!(r*ZwCAhi}!2^?-sHBuuD5PcrTza@6aWAK2mmD%m`;7SnRnv>005Q+0018 -V003}la4%nJZggdGZeeUMV{dJ3VQyq|FJowBV{0yOd972yYTPgoz3VH6g|IdllU@rwzL1L$Q4v;hgBx`bbo$}{J_0z4xj4aqB7n#rPTYYlA -q=3$Tw#v)FGMM&H=7wzI_EL?H`RL!s-vL{LN+AuR8-|MZV|J{`}k7_v@6aWAK2mmD%m`?f@3If#w0040d001Na003}la4%nJZggdGZeeUMV{dJ3VQyq| -FJo_QaBO9CX>V>WaCxm(+iu%95PjEI47?5`W-Vd!YQPU^>IH&r*M^hDqG$p_ktl~--AYnP?ZE%OLsGJ -CZhArWB8oF}X6DRr#6%PsP?}V#5{y#dSy>1L;e#yFN-=7`9jD%xJJ~E1`=XvjSUOIkF-l$rl;?_xJWP -Wq<-tQJ8I6i83-h?ay|SODOe)1w8Dv~Wjq;W$T8d)pIBza*mh@`%e*JEFd%gM_7BHDioZmD8oO$w*r7 -4kszRHv-UgRKEC=^N}!AnKtE1C?W(O^ysPj9X^*X!FGbo1f>J}##6=+2E(s2_@mw8inrQSx$J=|{tEm -g{#P79LDJ^GW|*ZQkB2CexSp>*M+TVFGt|J=?)!w6C=KV%o01VYI$p)60KuR}Fp3#RW_+fIu7Y;kc^v -Vc-`Kvm~ri1^%DIT#-m!79x!uk}|U_?MEafRUR3OAwrjP*b7xMEMWSjLj`#CkxY56>r9 -}7J2J5y=AB|73)Q0r%T>s82|fqkAAlaM9OOg6CV1Wy%#If^p2Au!%a}umUZ*KI -Gj3*d9+pn0O}`Bopkc6V*64juWM!lzNQSMXLvzSHH=;QNIM7H6;_!Oh8q@1A|Jk7?PshZtEX370$zq; -p}l+k!$v4g(hOBMI-draIH1p<^_&;!9G_U-%l)mLZn+!F~`X9ykk4@oGVH_$&{FRHw5d#jACs~fn}_;^0oIC-#l)gv24QY2=3%kgA^$)Y>4&4zCJ@i -im(#CEcCe=w?b#MU9eO+czeB01_l7q?^ybfO9KQH0000803{TdP8q- -EpY8|%08}3U04D$d0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WV{dL|X=inEVRUJ4ZZ2?ny%}9^qe$}IzoL -)P$gsj_viEe>a?)dN0T4wiZ!IDcD -uaXO>+6S;`)iV>zY*=<$N;Pvg$zUypEE*Y$z+!BFc(9TBn>QS#>z1<%TGQt3PJLEjm@H^ThV2|Bof)=BU#M*QbEZvHCev*{ -wfhI{XnM|@G<=j@1vW>6q!F)pC@;vYNv`lxDpzH-fWA2J-4Ud-Iqlo~=YuT=q+4;PGTjac%lUw*D&7P -7pYw{ztX%N3+M97N}s5HWBn&sP^Zi0>w-Q+-M0lxywrXY~=64(@QU`34vdYF?>R3aVIq6Ho}ud*DqOr -G*)Pf$G_JV*GYg2$CuAa(0)DIo6gc8O+(bh9O8RZ>!I$!ykg%0jp+b)sfxGjto0GD>QoL^mNxiSnvm0 -VTYt$^$JMr}UfvWeB+~!L8u0(lu_N726a01{UEF`3x)UDd!Y+EDCa{HlTLx?EAE?X$j;6ep87XvM+aB -KX>FdbepzC6KUU(qN5VPC8v_xsd(3NbgEbYY3%pt!({afQk_*IEdTx>Jm&()YBrPRCcnu0tWKLfEa1P -%1!V9H+w^WQuE2=x2X0{D9X-`f6TF4MV9zM`+rmoWfnG@h86an+4H;6h?Ln0Hy|BLM)wZ`NR@TeikVUvK^JW4m>0Ra6D0IwA2pLs(MvnP!~RaTc6kdg;EeGl2Ypo~Kjez92busnKmNgAIMQJ=W2LJx -DEhN_^LLjg!+?i~O^HPmp}RbmMnK=2-dD+syg08Z3E4JrO4R+)Y6Rrij3q|n?R@{%e~>6TP_MRj0y#{ -25SgLnr9(`uPpTgxUmqS(g!-X9dYA!rCCVdB5M23Q54Ms6#1h`b1IX)|^(FC<>{XO)9(pvk5iU+)$K! -3lT3i_xZKr5s#(5!LRomcT9i(>~Akj*#V|Cj_q7*fFF~XAE?>AQC9_+t{K#?J4$hsySBU7ndE~Gfo5@ -X2YI`dP~7I!P|jNVYOtVITJBGR#E4!B`8^XLF_A0!8|MBPNDRfP`gB^-0|U*7UfXZO=8eg6MDwNG*UW -!iyhvPTaXeWy`)dJqR+QJRy$=YBIi|sNhns@i1dpDlnxDbr`EW8GV;giT_Xhd1Cw{D58;PkO4XP{gH9 -RjqhdmjS5-OF4tvJt8e}C=9(*eUt*+(|{~x=kw|dU+yWW8d4$@@rcSf>&ht@h$Jiws4G1>f0{ZE@SOoy&Y9TCq> -3!4KqTSH}TXbOsOaNfN_L#X176?gSWp9xe60{2i2`VeR~;`7ojAGg}I>8H_r7ZNK*Gq88mq!A!uY?2}e=q}M6a=a@a#Szi_ -`v-ja8`)C+*ZE#p2zPQY|@i7M9}T$1^zKM&V7RP3bTT~}=(`;1LL5!E%TYKEuEGKqQyQ=^8i|1=Y2dzw -8aSy~kSN)3CGmvK*LU|j<|-F%V%PBnU$O1P&^-Ig6)JZkQa#qpWocOLTcI3WcC4|RcUv0-%2^ZrSTnu_DVL;6TTa7L|JX;;a^L6-0KP!Ze18lbB!Qf;xx;3hHC%2-2$kCDH`q -_ov_;5%>}#~H>a*sU$;x0+_C9LIJjiCTSmja;V}z7lm#yq8xs)1Y31!J&m#YZ-PQjQpTeRB+C|z<*%3 -6Q>KL9&q*RaX~f!4LdqwTbx##ef_`DU>Um8wk@ENGMwxC*;X4dBBI`6mk&hIoCb}-cPqUx4kSlj1#12 -%b{6^@;+a`!93b~4o*#ES+8dEC1x>*K(3j))y=IS}aj=tGw>wu451b@emg1{(2NQ!_bE7^e%Bix;Dub -!Zl}rtr7_*YfsK6@bFGsGEX1BrZKFpKn@E&#+&={)T-c6=kUTLg{0Wo=$|GyGbSZ5vh4OyF-pB+%S{{ ->J>0|XQR000O8B@~!W>aZw&{RRL4-x&Y^A^-pYaA|NaUukZ1WpZv|Y%gPPZEaz0WOFZMZ+C8NZ((FEa -Cx0r-H+Qg41dpGA*3kc6p6Pz3JUzYv#Bju0Cv7DY4;4ZO6QWQUa -B$aD*WSr-ck>;$F9OjTB@(o7J ->J7IvQ>Ub1o)^Ys>-)pJi2&H}Btc18#1zI@SaiyUW8Up;En-v`rpy7Lf;|tgcV*2A&i}VR}fbqUr2{W -TTpFREDW017AvM_Nj3ovJ6;n-X90O=F2ggtO|DsS-~rAa$3C=EF_lX}BSrH`VJ|ALb;&5xT_#L>yP$L -9}t#?&f(UMwgE@cc;^4DlZ9C-*Mmyc9{P+7WtkUWmo`lu9=-+=*sVMrDtUaoP -D*~la{!v+AM%6k6CUkm`&qU_v(3VUTMln-YWH`gJ_AsQHGxyR{5Kl%pgpx+6ycwkQQ2fAw2t*;tYYlEi6_Z=ByuW|c5duWsOk-ZI?@*R6<{jz -nI-}dxz98XBv>VdH4*#Ae++vn8y{(E>q-*)Q%_9RaSW!$P}OHL0thU)|ga5Y~7<>7JE+^V8W#{9Mt==0(1;#5&{dxA-Ct~A|bwU&cWN4RrT|-azia{o=*cH@tfGJlNy -CS4KGqM?~hdMEg1NN2rp)Q8MZrHBIX5|2Er%q59PoB!%`(X>tQk6m1&*8eTRLrq*+H*Y8xzmW8`0(g? -H)r8cyBm0FxaNd}*&cf~JOTVj$e*f&LH6`T*tPPRG?pVOS}ju-rtPv9B3>PE4S9kSS4`~*#<8LA=!#- -JV)a1;`G+$=6r%F>zf2E5qC81^XhO4vY=dy`ABqXX+IK^{WD*Z>#sow7O2Vi%WLpoO+Rh3Ju++v!rLwGdQCHb%U>UVy>!BboLsEfp>B -5A@E0TCUaz-Q4_1)-f&3G7}gf4OH8M}@?%X`fCzWA}#YqKI{C8975>!tM;rE$8D0vTI|#qG`xL9pIAW --Q^&AxKY}y-o+fgk{*b;H(7Ylo-rNLXj+PPVQtP;@~{pq_i75$q)@>AtX%-YPFBq$DHASn&M=QdJrNu -nNnwJ!_KNk(4;A^oD@ZrE5o*bu)GlacsULx`ivYiCRk>~hp%%YFecYMncxs3KML^i{Fg2|k{gyM! -Z0i5{JykSq6^U1QqnW0(SroDCV7NbqPNd+Ou;)#Z3B^-~##-}v?u4>NmNnD`m_5*M{MlZ}g-pJBgrzD&9ww?Zy?LF7%>X8JTnchGdh+ -BB>Y+H)fjM}8OBl}!*QGZ4KJgUxII4v~(BR(G_|H0_<`#N!Mp=6@OE*j2@-}*`bb`AKdM -Hd5w>;snZk#C$Dkj;1}T|d5>f5vDvvWj5`7Nf0J{tT03ZMW0B~t=FJEbHbY*gGVQ -epBZ*6U1Ze(*WW^!d^dSxzfd8JoPkJ~m7z3W#H1_H@jS?Sq;PMd5|v_OisX%A^Y(9%d|O_3}~t=BE`- -#a8FTe1}-yAfc^9L@*do8gddjOqxbP49XGloH-)WgKBUt3>aB#&MDia(l8#(s&!qbwRmwU?dYo#i~7w -#((Ys#J!g+stR80m<3v?u4A$ut?uJ+$)r;?2wODDbj+chbViBM?jcC;qj1SuX)w%jCDDTc`1icdI9f9 -49mjaMK+W8!ZCx$+j_N~;6uf!+2mR%zAATbx`7cG=G$*MA*K4Pguzpl?>kQu^hkkVQl%$`MB&h_mmOO -;e1LSOYueS-oXSG^|_q*Qd9>YuPm=I7C&8h=y!J>#5_^0OvI*`tiNKBCgv$Ga6Km@J=Ezy9H3Q6CfH; -|S){s_b(ESyxP22;#JOV$BGOCv*Rko6Y5qHxq9#Nn$=g30LJTETOOZWbokcI%3 -ZC5d^=quX0+Rs7ta5TITSxdcG+57Eaz|-qK{PqJW2UwEx;q>PisLe|F0xr7a^elv#o2ypnOrPUsi?Y| -B63@>I{ZKiva^PN(v9(%-)ts0(|o25+!Ro-uzGOa<@H`Wd7g)2XNoXb0K?2(x=ZMc9dn>`3 -!uHIW94)<%wyibK6v{(|?NkIoBY*8GnW{$$M@>@7)Vtq@}f($!NwnT-*7v%RnwN^tpsLk?@AgdLy_zT+|AQk -P0kPQ7a;q8_H%8eKZiK&22Mz2_+jWIkU*c!o;^z@lJ}@oRg!<^l~_*hGR4*{>+)2Q&+d1a(>}KW0$SHe|%kN4fh -QiZ)N4=LY56t;)RXY+~ijQCaLjBFN5dF-RCmTo49B7^ls%#Z9C)CyD55IGY6LfxNtX^9gZG0hO2qy=E -_zZx>SfuetboU!3DzPh89@6aWAK2mmD%m`?eE!dct`005B&0015U003}la4%nJZggdGZeeU -MV{dJ3VQyq|FKA(NXfAMheN;_voG=i*^DC@KD!$(RHaR&s*yHPFKuOcVE{MArv6CUf8Vh&yGz -oP1K{zS@w_+V>5Lu;W1V;20AoaqlQxdkLJ#Y)G(c{QfyPWcVUcTmH9e3WMQww%+x!&(@_y2P6PDo2bekbsUtFO2FPxsgC=IZXIB6s)u+bTI=2qxcoDqgUb=eS$t%bJa;kG|j=7DFi -GqJF<*9tet5mtvITZ<&IIErL -h|(@vcdO*iPn`^2Q5KuAz!`&sp#@{g72W_&FqP)Rap$B)y`(^owj-+32AkB)m*_<%^R-9noi6f?-F;W -{m8j@a}6MIC3zekSae!ndNwPMpU`@TAMNj#ENUpi->d9P* -NIyWGbU+Zd-_hCEg9RGbc8sTD_Jkn3zf7Aao2mM=u3ermacA_C_~sa|n+fZucWMVb8tP)h>@6aWAK2m -mD%m`;IRL>biq002S+0015U003}la4%nJZggdGZeeUMV{dJ3VQyq|FKA_Ka4v9pbyH1m+b|5h`&SSd0 -y{uv>^v;H1O>Wtwo_0DicQ;EZAl}grvCSnKjg+is*6F9@A2u8uE=yCMCF|)6arLd4LOh(Hm!FkrZ`Ee -q0TCs3#A>BmTg|ND!)iZQJAiix{TQ46n+L_C!Oa2Dv=}P^yhedI-MRt@V!9ml+k?0|3qXcNs^+K%=T{8pk)d;+=uM18sS3A;8l7O(sa8^)5+@6vqy -B?@IA0(Q^y4d1xGL9xHlqko3{3#HSi+^BGz1aK36Wb#j6rg?TX7X#DQ0H}hV!?Vf!{S4HNq7^V<3#CE -%0gN@JpBYK2CY9k}&M5UUp{5sp_MtHV%|t+p%V;J%+V8JhUi_JiBCt>9`JvbL#c0HrIt7He24LuwB}} -tWZJM*lgW@=F6qE7c!JX>N37a&BR4FJo_QZDD -R?b1!Lbb97;BY%Xwl%{={&+cvVl?_a^^Ah6upYIAsp2Lx4tbhAYR^zIhf&Gm=vLML0at&1!#pq@o~EqW?dzsx*;3SH*YZ^UzPeJ+dDB$2{!!MOO -;K(1&w9_BtgRdKTj+0M+qFe`wZ^gPeY?*Z!SzUYA8v2o-`w4$zuf$KdnNnqi+!9v@hYzy-EIGIlQvw` -U9;lC?p9PSZ>p?}SJ`T7u>Q8?yu8LI-mYb#JFa#)P*l{F2y~2>nc(TF-tDp~*HG{Ir>wovRF4w -}I|Jie7JjSoS?KY1YTcspmBfK36q1lse-O94E`|I@a~Rv&reL3S@&{J^kIo-CT_r?)GyI9?Ye7YEVuo -y)9!)^5}KxjF{BE5yoG3|q6>53jaM9u-2YM -Kx5?SK=K|((iGgoZ%{SBTQ>(E*fXv{XF-yB!VIs_asaCnZ9-xo{#SYyrD>Jzz!J@_uC8*vW`#%zQ9rk -6_L^ojnM~;Kdk(g%V$iGI0#f*g$)Wckwrm5!5zGL`4rtpPBrg0YN+m3N!K(UG_L}|tpWnZdyr#v;-cc -`FU_EFfN%%P{1;4top4>MbR})#{i=a3RikSj0PZ=B_IHEku)HhM{#GYTu#J<6(XhiJvN?qKF}{t7P-07cDSXYMEfqd|H+O3IDX)rtUUd*48WoV~s1$f>i7y;k4UY@Ypm4_<b)T63u4kf`L0Hz*j0Rg91Mi_M%H5z#YWi=XU3L(Pqn%pBllja??dSox{J27Ff -*TyVD9Tdz(yU6M8;_<{fXdr>se)<0g=Q)|GAY*Sg|v%a)h>WTZE+a$yDk0Em8!+0+4$bpCYEIKjxnHv -3!vH14(F8J8ItA^sV>c(O2PPQF{$X!#MZ7o8gE1B79VMYrEHS&G -(hWP1B2Ba+f$Y%ly(NY)L~0xZy^$TrML58o#U^7JPBi;tc7NDIXi%01_CMcIv{-&O$6fE`o7>yBZ|>i -`{QY+|4*cgs^_z?)Viw(TZqt?5tIlwp$ibP%AMS46r$4>9yYXVfZtD&w0_zO}k=f*;6~3tIuOWgp`u0 -8MSS=t8=_X+k;fYJ_0jrXg6uNy-PpTbD0>Z-1|YT|1#*q+wTf1GwF0MG{Satm5VH@Fn$QCMk(O@` -X!vqx@43uS56~TBVjAou)pA-_j -2KDm((W>^*M8qzt`Z*@lL@qR+#%Brchp^YfAnxiH${ov!SZhB}=&- -*<$C1lB8~nRaBji3XJz}Dl1il@B9#v7Wnq)tKnhWpmpZHH+{onDWh1(9XU|n-+tu$2hLi^k -HsJYMGg&NF^==PA&%~<%lU@TR9zB})fwdLKJeSY8xRy}buT_a43cV;5b7(o9_*hskoD=x0=S+*7(hKG -#sedcY+Uoyc63Wn531rmbb(V_Pc9E0e;TyoBLm!sej4L&C% -DtGiOR7ry*3A%vq+RmreZ<=5Fuj=gW9tn~ExH4i_wS++Z(P(#lu**Jjg(uxKb+>o5$g!-5f%*)S^yNCH)AYtGK&u_iGjCdV!0+c4b+b#rG&5#>z -O(5nmFiS<&l%<|tz@U2M08GwHBg@{MQ~n&7CM$s&5p -pS`D4}iYa-i>)VKSh4$&c#K#-n)Fw=Cd_F{*&25?b3pf4{J%FU1sRU%(Q_YQD-|b!xpM0eNVBFAsM9P -#$vOB#=&8ud+%BJa@L_+

|XoD4)na*Q5bHcgDw5pJP44n!fBV;Ns3)-9hD1^ORgYn|4PM(<-d$shs -yq@EU8{1`A#Zd)OVb7^xi(lBf%r^04teyB7i~qz9=>-}&YWN{~$0d&unFy;oaz5AzgToo!#u_wS5sAG -#=1gv($~3xMp)n0}J$p+}CRnr<=HnKA`nAyw?9B3ZAN%<(SBNU-;kt$|A%z|7hCZ2$9<{!C;YWsE^}Y -kc$J4<~{S))BL`Gg#ASO+=sUh@F(~OVxLUY>+dApBQ4L_=gw{fMS3wXmbF#3}8%L;AM5mIH;U?i!2hX -Ha})n%Q@QO3uH_D9bB0suSu?{_cjVHk9f-%Z>VWS)l+Sj#i9muf(2fz=@kmq~2$YgkrV#)K`gd<4#=mHz1m0efwM+5U2SR?0F(T5if0UW?bKpe?n)EKrP+u}zP=rr- -qAV49+HNcbtU3XPyjn2&N%`Na}F)EEk-yfDQFupu^(Rqr5F;(>F=gzpwI^0mW95U1dhrI?|Ca9VScj$ZSlA -TPT9+r+0TjBJyiY`!aykDV;5pZ}QhdJV5W`MopU)W9y23UZz)Dt+uU3YYsb~8Um(b0ThC8}j){&s7uG ->Dy*X6X~{*;RKrD2c7d~|T&IZHYMb5KrVS5NI{=?1(u*e=65f -#Y5pjeUWLF4clB5$TsiGk%lzTsLU$RZ`2j@gDys+c3T5a6cH&f1-;)l(35Wn2deoheTi0kF&N#Znm(S -O>x7V&H}a=*nPG)f5bK;9@<$y(f%B^7@_o-K{UMQtdwx{{It`ciJ9B%Ppt1pF@z%qB6B^a^!UUG3c2) -c!lny68X6%8$0+iG@{N9pz==pG6aMX3lVHLZ{1MO#Ye$%}sr0=FHn^u7kT7-XY@w*ubu_)}R-pa@FM! -wu!!N~j-v1nrrg_m{ktEL+Gt1n)x@~{J;drL>8DD~{g&q=l6TtXo4YujPeoWY_z>3=emGVNRJOoVf_? -^Z|Ra00OxqOR*JN(>V%W#Q7QJf_7k)mRbyh)L{i924gOutcL*%wbPZ>F*&m0>*&~OOe=}gzqTT>>lRG=i$3&O*6o(kZs$wBW}=0v;(xq^Vhf|d23aNpL2>X?9cRmSlskdn5#RI -(wN<;w@P@f@(j^ASH#t22fq$LyA -BNmSh%zJg^3VV}I5t?%lefemqqIoLLy2~KY>!BTb6mnp6h|5mka!o4J8Ud`UkE+fV)L-|lZ-<2~kvUNa`Z$6|T6F -V(3q9n&!VZZt`qfkqP5D{1JRXSlhkvE7OGISz}f4>suI!}qSYg}ANj!98XdELhmLY6`M5fyzEGZ7+q6 -6Y#r!ebML*V1ZILn~J%f;M^kl;g#nw(pok?=2`OkS`1VK;x|L2N_APIeNCorGhqi{#9OS0KGYq)5p0` -*>6SeW#{)%$MTNO#(8FcPFO&liFb_=MClq?WT`&o3y}ro05Oh3P8Nv{V3Qr$KLCJg^dps(tg2|Xb&V^ -2C`)o`9RMV3bAdr(o};V{!AHT7LfrJwa`)nTtgTabNd(4ydpo+=u}SE;P+TgXhkZ`6H{*B -)JBD=dA%{sO}l>IDgsEPNLqXZm)xuUGba-JM>uMrQ5Sn-7@_DAI>Qj^#>oT)n2ktNv6a#49`cZYSc#QGf^FTjN@=)FjW2UV`^FPYwqu0rx&W|0gtW<3`kAdbMC>*V)pV$gQ5Y6TS< -ue(y+*)(suKoxndda~%1e)U{8z5xRiMSq&%;*!Nb!-`|gK_Wh;H|%1dMPK5ZH`muUcRdqGGu3T8q#Q5 -9nW7&_0;2ziyae+PmH6o`gf(st@2i8JIN2(SQQW05-0x91{Bw)Kc^DaqA=z^_LB4pb(vZNote>$E+3o -pgISJ#NX||#a@dZ?G5`*O3HyKV;F#+|P#dsBOAI;deQc3<7P)h>@6aWAK2mmD%m`;UvZu{B_006cp00 -15U003}la4%nJZggdGZeeUMV{dJ3VQyq|FKlUZbS`jt&07C&+cp;e-G2q)ppevalyBvD6FN!(%o`@P365^2d!(rv&QZETZ#kMH~Ykn&cFh7`rN*S%y#L3q;%sR`XEQTLh^ -_WNSt#y>03`p>o4?RdLeEVjs_<6U0dv9=P@Gg16uGMB?(-iSM<{Hia!D{9g1n9?CD-fAXWTIXfW{nE0 -jM5noE)scnyhALK+qG@Pbd8)Ve>l_JtD4At=u=oclg`A_{ZN(n^;-6@FOLy!Y4$iLbHLq3Pa8-JebV1 --%ait5c3RZDVH#J+49fN^ZXab7EwbahJ0k+P!EX>Ul8Xg8iAl2d-pXPAgBeSUqNNS^JX -rbERK74I&Pa+-$w&?KK*w7`NeR9>2VSgIT -=P3d6vz*gLV`8(Xj>E%bV!gqF(bVKo-pKB5b~mvVn?oAfL!|t)q@^#kJLwm&l4+X#kP%CFdep!QXSy>xzQ -4l2tTMOQcyRN()G5Eqivb0FU?nyIO2$4ZD;~K_pj#v_hNoB}v4~iE -7u^`}4W{%Es+S^`jIxVT1EtF*(sq>2)$Q@%#Puv90$Q|6Q*K4T7Qp2E%R7p=2?1=5xGx-pM~a&eG+3Q -+YAO*tujigQ`lf`&I0Y)uL-ytH~Xc3aesH{;~HgS7bTk$R#uxBG3j~=xT~68@~gCW#r3eX}=Y+q56~o -X1$CtSIp9S+bm1KDov}F@pO43I7BZ5x|_#j+DgX0nA}Gw6GrA>(&fLwilv5JU_KD+_JlToQ!a^PM^Yz -1wj!48tVx#uh;XmvR~rs84?O$dp3ASP_h>`lP#oc4RW{XX2zOHjuw7%$SF8i%#@dp@jM&bqIFs~WuP% -Tx4Ctqo<}K#Mtj-pZu;K_Co9#c?y@En*S3aU5 -{8s?439CmN}J+ofLf+D;})09LySvkpQnsD4%no5lrWZ3Z;(sc(n=y%AP~!U6lnmZ`1R-Dx8sutwHb(^ -f}@EcD<|>3a_vL1}Jbuo`k%W43P*Ot!@|++Ck&*B*#fDAg{QxJ=7gh@>y<@!C|b8PZzO?k$q9~_ExRE -smGz%$)ghMK%Y1c>ss6w@V9^-;#$ba7{=n|5U2>^iyNWzxGWlt37qZL?tM}i&>^yK56zDlI)tF%}PHw-_q2;W4yFV(9!VpNqlJ1aZg(!C;Ehp~2Ge)a;lA_MM6H$GQ8JbDW1DsRP#YxiZJvuRJNVS#5jFP*SnX -@pgBV4wF8T-DdxE`Q>3^Zz1HR;YJ8BwDsY73X#d|jt=G>{DhlDp6@!E>ZrR=jlTJs0LcKhLI1W82gsHJ&kj^=b8xCQL10$n{8&heP9oqNT?sj -MB@IBZVO#?%QEf#oW<#zvVRR4tKA2-CyBLzo6KJGHeHx^6gkkC4!KEPck5!WJS_h|IsO -Yb|p@ax;#R@}GK3k$?L&^s!Q@rAltK`e-hCsPwz5D?jf3wQ-%CICuPN$_<>er9;(O#*l|yKo}b)$q+I -K7PY{n`rSYzv;_~JkaGX>2t0E@OQnKuw!?$?kOBpMq>?3ieYj>w!usp#a@qmOm~T=(^#chgC8&iJpC` -x9WL! -@FJ8pP0gAdN;Lhu1Z2HWf8uT)O6GZ)VN25b)+bl%v_}%Af=XH&WO`hwCMCXgJc+sHQ78s#9AFEiv*a9 -Pwi<`~<`rScID#0e**~yX&yauc)yBR|t=d0F3?pn!KKmW7nUsXQs;+YQ27;Ncuu$%hl8it4C{@A#pM3 -82CJ#7LDV{H6zrlJkfqnG_1K@obAzOyC48?<&5{U$I@hQyG@tK`QyfC#9oS+D&XSJoX{@GJtHlHgASa -}uuYm3ihp5PGc0fO+$};OHET_nWdMj@O+(z>z~H5e55qBMZ+8VAhFg3xhWlXl!a=jg`eB)d$4q~vf(O -S6{zC=;PBoq5i2?;1dbwV&$dW8A2x@4Tx^nPvNs{w~yd)1!vMdW^!E}JGWwK#pdEB?K79Xji=LYp)%- -63o^3FWW-8KTwmITV|Ke_&~-}u=Ztvhvjaj^r))o=1rG#A~zqZb|TF1S)XQy1U9`R?_Z`Hc5S0D;fHd -Hv>Z-+uGmo4*<+R2i3}ZOq1K8>^4E`_-#!P`9BEDK2`*jFJziq9Is#y(Pqj8w{R*dL~E@&O{bqm9zx2 -87denkt}$$pBkbSGRz#jfd16i+5*j!`$773HN{7`gY&NJetG@5N8TBRLWzwK(G4O3llCN;dsa2Ro+4{ -_lt6iN+Tl$_>V(R?F6NkFm$)EuF)&x_Z+DZn<}U!>iuTUutG`Xe00$W%QSlZ--ypQ_72eSplFj~+5)n -U4mwnsf=B_R7~)A7Ctwr(_{MG-h-*5eTZy$LDji|}o>q<{AmMT*%uk~?>`<` -w*GPi|otw-6;uwl0&Rwt)f5oNQgD%h`X@rkPn%|yhM-=TU@lHst$vTF1Gm8gXo7J)Pk#^RT^AYNHkK) -pBol*5v^hSJq%m&KD6SJ{_EV3{*(EEASH=S}g>3>j50|XQR000O8B@~!W4*|qV32Fx`- -~|FB(KZ*EQb{VdSMsLG<&ENy;ICU(3i -*K01;?x#8^_o~@m?S>)yrpj7oOpTW3sj8APPiOR6E%<(4{s8HFi=KtMgnE<0y9=CK!QOhL&#|*%Qab+4eJjM#R<4o0LR -*8Y(nFlT}cDo#2_5S&o!5Lp2Ra~Eo2RGiSs#ILDt}c>og4&D^@W%qjIg_>+ES~-EWqxfVLh9Y@4Uxj7n&3Ucjr+OW+esc@V?r)PzhB_a-6Q^(;6xvt^n-Q#lLnKVFZNR%WJDfuT`lMU|jd}ye|E?jCyf^V1S}1aAU!owI^!+p0N$c39`DCd0w#MbC6eIau92hGFo5D}^V)Zg!4p^%hf7*hCvKQ4}RXyE|qCA_7lk(zuHc@XZBnV1*m3aPY)$%D^CC|bQ& -=cN2ZrU(jt%&8i5{@W*+2A4$4))!sUsht*{gWToJlCuce30cx&VHuK7@@HL~?=kCBU>8TsTSso8a8YR -~RZ~5FOhCgo7lG><$9Z#GX&!=6eA(zTirerktQNQ0OJ2MR6dsnSwN6+YXhGzZlt3I6;2Sz_iI9_dz?* -jwVsMl%^voxL^q^4C_EqfO+s9Gz`wr6roG#J{UG785K|$Skg1!uRH5@VPV4yW`A%Qo1lKc+L8l<%ZBH -NPF8n?#LPO{TLIUeSpoYq=$tQ1QMdr+DOlRqpC@)u!^7()m#FaopkI2^__#>GuXsG$28Mk*BIEV|gqH -UGp)dB;|AH=seb2^ai}sMvs)7zMv?HCghR+Ey(e#aGgMqO11`3y>ForOz3YsxbI`(}k!xpiN!&fcoGY -8@t;qcqm4X8fmir-$$>&<)PgFQ+Zj(&yYnHDvJEsX5xI+jRH69spKdMj)$#e_aeusCS#vlXo$q0Rb!e -h7+;B1Au=`yw;4rTcK%_mM;UfQ8PryzozVc3!xlVvakg{Yr#1}DtV8RTCYDxn3?kWE;gb`I;Z -N}!?p2QE^k?Q9w~b6UFBq-0mhCn(^M%=N?nex$KG4FWG#z=cU^GLEqCu+cs9gF0-lV*OphM?+@;720j -mKy7+tC#U4s;eqIZydkg7$FY7w@X~6|RC5LJL_khmaqU+gX|{y9t8aoIp-V8m)uZpIhr!<>fJg}7?~H -_F&t8(BqbT{OatPK5Z0Jv?X9_qtRRK9Y0y55b{U`puDtK9kLj37U;n8K -Obaa3@UQK{$@Xe7q$&Hv!gexDb#+Bp8R{0_D5ybV|#npUqf#U|kTH4kzb44fjg5y62u7vr-Yr(L%Dc39w&%HbDmT6lO -FsZtkm_SXR_5LvvP5uK=O9KQH0000803{TdPN9$nMRNxL0D%|)03HAU0B~t=FJEbHbY*gGVQepBZ*6U -1Ze(*Wb7*gOE^v9xSl@5sHWGfC6_z&f4`X_DUp -`!?7m$dVvCw@I6poPXS{8yBTLh5*LICaQznmf)wGO1sjBE&k?Q9lFzZFN+sSfgA1f7XQGqyeStK=Ylx -VDqTOsl=$8!+X^0rd|%0<87&Her7`#F2~{rdXm;UNex3gGCL9qKU=XTf)d+YCb`^!lnU0B2NkG`gJqUGe4e}D -~E!q9;^#VMu3(3otVP4o?@Epw5`V((!G#2~_6_^mxN~b_OELjQ0-qlUDW9zC$+th~r!cP3oWJIY-NE&>n!>Lh$Z?w>);W77%kVlKA8P+7`0O(P|W?VSC{o1KD>9}h1kmfD6IGT_8}&Q -Lr~dbEohU?7uZ$2)k#3g_NS;7Yp+r{3@kRjoqRqK_>&HG*z=SG22JU -MS(e+KZxI>2S7?=51FLtdI(rv2~(JyJB*g;Mv{=Hwojw_fVhC1c0Jxjf?57sk%CPKlg(W4}ovG(!|{4 -f6g$5PSoX=l~qftOV50LN|G1elyF9(kWSH(eJKQ-N(Q8QmbqZuTCBMUZs4av`(0&?fC5$Oli0ubs}n> -y+$@Zz`TZ*KN2o#^1UpG(%QUmof@93OU{8I|~$^;~#r#X8T%~PdBzigi4<=cLk#lF -4-82Jmv`{mTNp)?YM-B!z@~P8Cu`lSSw@C5U!~>HRtbmtQMn@NtjfK1^4Mb;geLYwJ=5Dm{hg}aby8tuI -YA?STys)PRLg3da1cyu7%yY)CEuqP^m9(k%g)9%ORL=`v%;q1&*17Ows67{y9MWNhqj)o@`Mdvy97(C -lt=K2H0{a(7;CIeje!31}l8Pc!W+K8Z<|i!p0Bd(Zst*p?PLAad?EdEZ^Zf_K;3n*foIOE>;tiGB1a% -TCdmEGo1`EiuJy)rfh(fI}JlP2U3Rx0~2n4M|4@&9H039J~IO;^dB2jR|w^29`$j>ROW9J+pV64Z@POqzhdkQ|IdhWAitnM4a_S?_6d1+MMfN}ecj{@PKV!9gk -#Gfdcn!%qRCra9twa6!mJCl7wh60k?9ZlqQqT9K5Ui%_;r}FsU;g#$;_lP!=kHJ9fDD4mdx)}yLRmuy -+R4DX?=QuEtjAMhs_fd63-MZT7v3`V?$g8L)yI!F_or6Uu?0i=;y7jES+v;@#S*M+bu=O1d10B>kP9w -w68F>yei)qlf&J1}d9}oW1#9HE%Jx0Qo<9rI4QS+7^b7&W)2~Q_eU<+_RDBp0Q!_uVoYB!S%n?cOdt~ -D@h*QI>b$PBVO4&_ImOh0FwW+^ -4Ct837BZ$8Zft5=8JKb|Zg>vv+lXQY8<<2@np$*uUn27wkp0duYaLL5-Il`-VbB$ar?`F=pvRMTU3cKv{5mu8<+dF%)Rnp41RGmYvW!|`+N^Ij<-G4VMcEy -x8TSo$(lk~^S{UjDrnGIIlb3S>zmK(;DkR9Gy{}N8Ug0c6HEeWy|;lb!9ATMfoaDQ -lD7orE(*%6xwK63MKzSu0?}$sMH><7K)WhvZHXwOVcz$S0nmM|F{LgKTB-AGC4%q=YmME4 -S^Q(vZ-pg~B|M_BJXU4mn)UvOGt701Cwg;fQBS8?=E2y{Lp2A=V0s#) -haRgz!*HMUJjW0ZiW55Y{iHm92-X6>W=l9?tQ$5ln>_RHi)~*n-5+qfkIHk(5Mn=O{&zk7@u9RK^oLo -1UN-_%!hF85m>l=sa5ZG;g?=PXvs1T}PF*w-CWbIg`Ew71ne{rIw!a)S;?#_#o`b(e1K@Yn=k(Tddc(I=^<1vyBZZDe6mptXo0ROo8lBKEcdt+z3pCbhX`489z|^z3P@%%uvTUF2me -I$lX&Gsv^+WjlWKGwmAM<5`*}YqBr-w!z&O#2M=Vq-MM@EG6V<%O`9Xc1N82}lD4TJ1yks1}wac}rN` -~v>|XQ2|ZjR%`59{fJ7${Y4Vu|3q&buU0aCvuvqh87N1#SkSM1!1ORzCvaziL8Tf7R;@q+Dz|xwNum4 -LiR#Q(8DVE(7Pi6W2p0nU%L*dxGeR?qTHq=!Ujj{)W>UZKQi{d;!x~jxM$n+{3n0m`fd)%i#C33XQPp -Xbhv&O*=8*H-E=fKXbFcI1hn927t)iVeK+GfEW(F02Iqjz5pK<=U~V5aeK_iq`aVnGftGdYqo9Zv?6Z-eB*uAek&Zi2-TrWH#%a{;jQpz+Vy=c*(BetgXqZqFb=1qOj -9<83)-V`18@C<^f&knAHVT%uOiy+(v=zuufo!&+3sIZO9KQH0000803{TdPFx5_P=^Nq0RI~R03QGV0 -B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WcW7m0Y%Xwlof+GX+cx&yU%_=zSkB6t^kK2VAj(6#4NzcF1iJzH -5Cj4(9og*AqDoSB{P#V)NTw{x9>>6VB=X#EJcs^)Y~E~0DJs_No=KAOYu;E(?j&_gdfq#`!$dr7NaAcxWKX;5*|6$TD#|ViQB(;RHRzgc9L}AWv}i=S14HFNb!zcMU4v9aVYcL`@o@EfKVfIJAtTa*neSwLY1 -Es5)sD0gZhotWlbBo{^6z;X#;f&=k#`5(Qw$TWZdfl_=FS*=yaZ=cgyThHj)4LYb|Mi6>~DIMtx^lhY -+I7osZAU8{xQ>O=lEd*?K9bR75!J?Iuzq*LJZ+g42J!ijL9K;WB(Mq#WI>w5!fVJxBEWLj7oVm>w%JF -=>y+(GrmHg2;I#_JdmE63d~b;^*)2iFqvV=KjueDQ_gD|sLIlc*L-Rx^+vNS0+Y7#lf4!q8wGZXtZx0 -P;DOir~hd811B!{5}E~C92w^v{dhJ;k@^eYG^+Vz=Lg6hmnrag647I?M&zlKZ)yX<*+KKv39eD>7qy! -@H!=LsN7*_y?OI;@@eD9W7$f=G&9l1>vcAuhlq;-5o>aRIw%y(z-RsY6e6RQeL!`SVbez3=Cn%jdt+7 -FV0~GoBY+BnId@S}Om`2MEk`QzRSdDF9oM-~S=mY4A8+r0w|TN0*%M2bAny_MLu!_!O~9UzsR-tEIU? -geOXMI;44#C1G1*=9g)v*MFMQWG?Q%KcvPKkJ)}YIi^{a72!_SXn;W34U?3H*|p4-^e-nh-E+bn>`R& -%b4Z`0DO<}2Eo0=d*trr3aq=9V+4%ws+GSuZ6Fgt1#n;Z_BY7K4uv -#PczCYZNfs!?i!%4`yVM+5Uu&YH6`yVz|$Ev5;)-8;CsO%aM9Xk0=qM19ZP;b_axJJ?KeG>DZbcBG?g -OCrqPAK_yBkyYD<*tkwEt;QtYYPgBk@P3I^Z$qdGu2&=>HOik%c)J9a>rhL(bb8Qatz{5a1jkWoE@eI -$b4dd%=`8c4T(1g$s6`qJ*muJN6Ympe7bg9>_`uukrwd?|bf?u)xify9GMEQsxyvEY}nRdDqy`Df1(r -%6nv;(~CYQIo*E7T|egOE(r}6>Pg*vii+bKGRHCNWlL6M`e1Ybd~)nSd&~h1Tiv|_3I)gz)<7H5=^#{ -0ya>lCCzZ^H~*6fOkk#<#^1sA5CQIq>Amy2cLok1CWQuX5Q^F5*2Yqs#IxgMi0wy^!fhr)KzIsclX+d -R7-%zbBsj*AaIBQ;C5pmX15ID5Tlu!D+@fgwG=$C$Y7P}NS|111k-;y9>|kPrJ6 -{d5~pt-DxVVUU#om+^%$2s%W-IIpBEDjK|g@L)2RRW4r3})faS|p9-uZ>hO7Ot3xwllMFaV -2J4s)#Hc5_c+TSG2t1vh_zf2@14R6Tm@-n~5(|LqSoEk2u< -MO*`U>?*@aUnv_W*G41vBMT?zzY}WQLj4pe4Goz`aFRk(eg0q9`T{SadSR&kMoY}9iI0O^Rl@x%zZT8 -2tpgB`Q2y@LNpJa=Ml`5f`775468|`?-~KOJ4q`j%!s04cg~kuY!sSb)#r_Aqn0-`6|yw>>(A_ato-N -@Uhxf{kL=wgbTxxq)4JNeOU`QnRsI>f*c;$BJP~Z*=NF2DsP4GM4UYrnWoagGW2=4K_E*NqRb8A3_aF -StpSxR$f}FPf8R>6^=kuYjwL?0PSB2*20^%y|`#A?s^)FxNNsLFYfyQ|G9+AF<$gg{fxe79`OidZ@&= -qL9)7Gi->LYA4vS&xiJ2NPxud5Alp|Gdq&?IBo*5n^Kc^f~Btyq-d%oYB~GOvR9EI%S)H-oyM!efT?H -|8$DVx!rd-0t_D_|H0Vp(m|iPE69q^TjAP*}(wDWS?nJnEl~k!sUhmul#DkyS6a5+33;N)b<_+q5lR@ -O9KQH0000803{TdPJ78H{yP8w06zc#03ZMW0B~t=FJEbHbY*gGVQepLZ)9a`b1z?CX>MtBUtcb8c~eq -Sa#SctOwLYBPgN+%NGwrE&d)1J%*-oR$jwhl%_)Wm7Nr&xr52~=l_&slW=Uphu^yL_k`fmHP)h>@6aW -AK2mmD%m`>FkRK%14000#P001EX003}la4%nJZggdGZeeUMZEs{{Y;!MTVQyq;WMOn=E^v8`QqfAoFc -f{yS6u5$3o#!soS;KgbVCFMpF(JFM_ihOBol}D@1|YTtyp1sXfH|5x#ygl+L;Edw2XhEu8d<=lpC>@d -d)u3d1*Ar#u^vE3g;1o-xs_;C8Qo0qz>q`P;48d@~klxDxctwxVsS^tA%MI0WRg2)c<3{&PE9dyL^9s -=ZpK>#Up=tU9BEgcZA4#VtYm)uKHw8yp{CUI~hX_`gpFwRL$Rlc&u%s(0tbs@UGH{-IR5bbaHQOE^v9RRo`pdFc5zCUvan(ahPY=YanA#)^&_!V`Gd`icsxKqfL=KNp6~ -k{O{8bN0F1Xg)uz@S$Fs8?$dXdxspq0SzD2N6pd6>OY7hCj)}XfGmL9s%T_7_H5F2@nsWAtM9?K>**u -0aVNeOmLr7)Tt&QJBe!lv8Mb0nZpMN6Xe#~YcXCJbR%}*RpGO){|4iUbzhDi%w!gn+!Tqya7!fvxHt2 -xygE~Hvgb1sEZk{9y!7uIGR;PO1bMBbu;xm2*E!q?_LY=jQCtro0a0EJLGx?15DfeCFW=h##`q|9#`o -CC2nGebzBkn^plbUHe9*u_T&S7epq$NkV{2$FoI#NnSvn=}hl2{#H&r$oRaq+zvX7^c(U7f(_!fiHmxX?B4 -A=HQlMC~Q@aavYv-w#8;!!e%?ptDu@DIz{+g#ja>VY;TO0 -lY`GHVZc;s0NP(wX}zJ7McN^?W@tm;n;$m%uW^UR*+IWc7w=?f4bwwCcnu;i{vHbT6U_&e49? -W;U$jW!{!=t8GvJ@qkx$x4EfWjv7MjC3_gdLNuT_W6c6G4HMf4~&HWR&KYchDi-}8 -8a_|%>lT!TFOa>~QGdBoLzl8&>#P)h>@6aWAK2mmD%m`+CGD;LTE001Ne0012T003}la4%nJZggdGZe -eUMZEs{{Y;!MZZe(S6E^v8;Q%!H9Fc7`-D@N%7kWzAwl-_$;MXTPC14D6ZY|AsG;oo-*35DIXwGf!`n ->QbC+yvKw*S;vvXloY=(wCxhXQZMJ#^@91&bz?iRUiaKFqzBoKS`3xC?Ytj>Rr9!v#s%xxzig~obMBm -wb^X`q7O)DnLyo;FhW7u8hmhPT_XW1(ilKh6$&Lgkc_<3;&)?8#!Zb4l%=+s%Q7Q0%{C^rg+?1JiCqx ->vUZ))_L@Xo55R-RJ!HgT3-Ze0@d^Goi!+3Jk0D#uB0%BKi#c5W1(t)@BRIt9YqLc*u9{BfW<@Z0qdA -jJe#NZ)NSZA*Osu0N?#spnq2M-4CqVj77Jl&h2d8)}E{jgQw3wDIa>mIx29I6y -HHxVr2|P=y5ncFPQw)EP{_#SFK3nMKo|0@WL3ZGO-2XXw3i?qp&eXZ&{m>3Myq6bW=R)g=%Kz7CR|RM -si*C9l50ecdU!4Sv6)Fow)sGOvVY3?|c}L`(Xc5^-YE}yUoLuQLe^V-(h_>Myj-B_O##aqB4aGue%b%du$p1dIO?*Uj)#jjB!>hb$fpHNE!1QY-O00;mj6qru*Rsgq=1polA5&!@k0001 -RX>c!JX>N37a&BR4FKusRWo&aVY-w(5E^v9hSZ#0HHW2>qUvWs0O4<}HNta$z&;b0vXFZ{}W9^EF -yuWzwpT0aheeGXf&t|V?zfUG?IWYXp1Y)dqExgSX<7nQ6t2~RT3)d;WH_}}UEDh;hrTX&f&G`jcA@!3 -WMk?T(rMLD_I8NbyZ*T9CW`atZDhZ@WWC$^;70w7;kl+?g!Yl2_k|5DnOyT4M=u;0cu`AJZ96sZQ0$oS{oU+o*`AX0(K(KaVsW^cmPv3AZNW#*lDh#;Pdi^VU4h%pEv*4%%F8Or -xuw5l!Qz900xq6;Z?_c`gEqeV~~QL2Xp@elr1+9(18rx*a^E7mU3 -qaVIdat(9Bd2Troe@UwsUk=;K5DBUE=!$*3#0P^5~@f@6oDn9aj08>1`MCW9$2_IyMQN8PdvDan1qCE -U_l#cSZpx=+R#A->!5(uu|pKXTI7mqiyDxQyognim8+M9kjemM&}8ZKyv&~CasFyL6?2jPQ{tz -xUw5Zg^Se{W~>xOlWVaKbxsT0sG-AwHYwc~c?um??D_@?`(+E_jBwA&QRuI$nlU5IliqAs5z!w4;~dl -m6)Upf}-4;rZTb<{)Fta}i~8ca}?ixjMV3AV`#y4{!Bu|2i}jB&N)(}RFZR_1IAq5w4xK#2xKCk+DE- -uro|g8WJ64Tpq??Osd%mdf7hN}nsq6O;xTf;S*X9K?0 -*WPqnRi_4Tfg6m0VBUmY)-v1r&T<^R$`!u)+FfVq$DAu_>$%1ca*2@|1s7}!Hyzj)fCPkjfU8N$>)7k -!>)Iv!lF%=V_&#ti$;7S_$9$8!{!+Bg3^o`2(5Y%W(`Z^Yp5=}{l9~%Q4Y1kFQ=@8y_KPHcG^rF-|NNZv^R|2-@Su>-4Ko*6oQQ -Sx;}0_FoM$j*b;A!`DWd+9$79N>CLqo__-Pwtqdl8yn)djz!sdxeLTiFuKO^Z_J45L;ta1NY>5ZK2l# -8>QeRNK*e5KdQfEr(dzrbDN_{28TM=5$Neq=2u&KB5@S=p*paqFzbKGLfCflm3zDE{FJu2>L+jxVF*) -25}e-&v}0)L#o1KbmcPLwwYao|^oPeJ|T2|iBR#a3*gX1~$5Kb-aur(cZ7!7BWX#IkiWDo3wGPAC5YP -)h>@6aWAK2mmD%m`(ry0006200000001Na003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeRUukY>bYEXC -aCrj&P)h>@6aWAK2mmD%m`;CD^JYs1008w9001EX003}la4%nJZggdGZeeUMZ*XODVRUJ4ZgVeVXk}w --E^v9ZSpRO@HW2>bPeHgCA{DA1AVUTWX}c9|)1vJL6j>n9(#hs5i7H7o^NPOv?nqIjBs*OP%s`NsL@-a -u*ku7;I6(}`}Ae@0-bHVzTRH@y+{C{yrOwxU2|{)U_YPDmbhxdJVFRlq3s4nFllK?ALM%aw*_N=X%~6 -|Di)@_HjXQ1+gB(<$h<0*}$~1LSC#HA?Yq4HMm!Ewx>E>*$3tcwIueMIU -DDVpx9)hNI6e?(E^$LTj?<%E7y%XpP&wmTgrd!s0|Y7Nd}|iB7WhcG$dIK%PDx9q=N1C=TyH_q7?Vh% -{z%Of+hGSCk6rlgu5FvD=H!pfvg#|GrQzZs;I%@T!E06$-e4d`R0|BSR^efu_mh$A^2ab~?QrLV|IIO -NQ&{qm(`QX|OtkCSzMEYj-~^19$^8!Dz-=jZUytBc?t>(n;J502?nlXnYD4CkNrN-bR!d=_?HzO=Vb; -%pRCNY6FdxMbbO#+0_;M^7V2grC()V*0sbclFPO}t}oehUTY-cS|3D*Uz7B!V?pE~68h>El|)K61Y_k -~E=XnXhju-b@N>V?hXRRMi_(^mBo-n0Cl*e-l8!P)9RtA3(px%uhlo>FIJkipnKgFfnUSOc8vZJvJ5w -ADs(}Mzmv5?#VTt9tb$H$J^F*b#GiF$eU!1t0kc4SP` -!(T0@JHDc1Jdz^W>-Gi@BdnPV2qw>oOO+Rw}ro%8elyE=%lAGZyC(%Ua^%Sxjp3urX$p50156OhOy9J;EMUVL92#-kh6G+`Ql=21{~0Oavk -tq#?!_+p)@zB9WaBh{e@@HOkICn#4xU+=hBkx`!Z5e!%3IUH>-5xEqss3U;=-P_Lg9mv9Ya@M@OiBVm -QP(B65IGq`7UwbGmoQ|x@K+)>`nkjS`(s{;-=NIim_((fc#5Zb_JUiaAIlJpe1lQ@s&1H9p7nIx|EAb -7e2TZU@pF6>MSE-2AFdnPA`a*W0=3b-%T)3D4vj-H`w>kbN03A{O3#IgG%W2MJ2RsrU?27N{TPL~&-G -dsTch~qpEGdlLw6m-~`GL-TK=Q^A&V!@0Z;CO|1OJnG{MT-payXREQ0U|YVqZlm7V#rCK7#ex*_jO{V -Uz|4vJMArp<-0TUQq&j5vFSNxx>an4YRjKc&yH-Tc(-TwzUGH!!FYu;Jp03yZ)XBQ>;yiGCXIDAWxY2 -X=XN*BqpqS?ir^5yz&kx7Y6)XiuZPNV^~FthHH*%$HPgJ{+H~|q8Z*Yow^_mLE$OTbiKh|Oa${)E&8v)z}<+o!J;si492_!=omf04 --*JG^4tTBDn2@KDp-=)X63k$ -gTzaw8~Zg>z=z)iSjo~;{Q8fRf2h0OS+@SW7e`F$;M##w-<=~fN_nO{Od%iSoL3d*_87~hvhN)mMRo3 -6Q1(C0X4L0S&Sh>M`?fVFy<>wn87~(yTa4Rb650jI>K{-`0|XQR000O8B@~!WlCbOpBnkikKO_JEA^- -pYaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?GFJ^LOWqM^UaCya9>u=k-760zPf=v)e!fXNCM+3eXmYZ(H+T -0>-cX8(#0xi)AZzNGAsU&XE|9$6>dXTbYJG&nyw2mH!hv)G-50X163X&wdTGx^#3E@R0q$YH$L|$u_* -yq`-d7p|rXQ}3*RPC!=+~4!^zI_zk2gzo$9kQ=@6(>)u%!F+DEB5*vQDo`$RG&YMHWb+#UN`bIA%;>W@?^b|a;bu1LJpOqMi{*-OeQqf|TgZ#26WMM2AKPTrMD(>$k -0V0nzNknC%m0s}4UR14WL3TFf^ZdrhgUlSahu#9WE&Ds3$Q_f2^pG8xX7&@M_c7p3)K76>j{hZw0jEy -jcj0>((U!aUDop9yIanIm|m4#&qewTb(8>37}V|nX+1s-d;fB5a=Z^`vPZm$2C+qX+A#KwS!rd@cy%iPZiHN-8`!cl_1c-o7=xD?Li16bJ0celvWY13{p1ML$P(9 -z$#ZGdBd@>@_v~eo7^Sd*2WgCImaX2>T)Cvk&QLs~(-VV(Np?LjELX@Ed7ZH?dZxLftgA5UzjV;GGDb -EI+OJ_8#}Qfl)M3BZq;~+g+j)STx_0bRP@O&i1*&2Z2jSOSY}ujJqm~SkS5ZM)347WoR*cH@!Lr%GL^ -vZPENA+j-=4EPBf(-vu2ev-46U2{sLih{^8H7zF9xo~`dcZ!vC>6+H!J2Mob^bC3LUWZxARW8bFRi!u -N&0kw!4dZn`pUTdh8(Zzyb%l#wPKjMj`%0-56V##w(f6*=Bwb;ACn$ZImvv79s2fHJfoh7=#_fyc2^KDJ);JJ_*ciBe?Xd0Y(G3NBL0T70P1g@ -plT4I>Mhp??bV`;1do4huw!9DXwPm9ojM>nyWXlLExQ1*3&nbjEgSvns;9LqsYbJ!GgK#bCTyse64mU -?uMX-pgIZ=W4en{7+Cd?gup*~4><-!<2q18;b`-!2b -SfPv?f{sk#_SG)YU%s*x81%aY!9T2|``X8ucC7pn-GP-18@d0-zMDwfIMO#-J1UYZI7vlXvQVP8^xj4?vG&U$_nnRCZJ0)m*o=v;fxN4%WbuX`U&%OHjS|Gj78c=T`!;wpNf4}- -@_Py9GwvbwwcJj;Jd%UGjf@dcz0rb#uHBD4BO*{qvCS7&DZnL-7eoq77k}$|{Op?wc-5PC{*9@*bZ8j%zXdusUCq -Rpr58RXFCo_?cfusEXNKKmZQjEQRnGIU${~=-9h5)n*{pF)SU39va$yiQ~v6vkAN)?@z(XW6)LbsyU{ -z|b~M@h_@W=Wk5mP7=UCDdIQ0+@4lxZX4YS;ESGw*z*i9$Aq_{rU>mDk%EAS?1xYj=#iQGYS6-)v2Ne -y$z-mXgKUA@)P;;Fax7D(c?nTchGncIYDE$lyO7Ou}fmgp4q?}>}t9@<7RtvunTUeFD`7-ycR`O>vp#YH* -1Ks071OXNq=OKio*CKGKR&bq1oX$w!zlmyPd769>enxR}H)xK|;Gj+-&!*Hw}t#%hC9AMTCkmuH#b3B -BhFj?F+h9_}4Iw+W{>MM@}Z5((Z@>Zt*an5SrZz{Z`avX3wE;=u?`Ym0@Zi!~Oqc3xx#|jd>k*GtFFe -n?bIA*y0-5sB)d^WXL9DGq$YtjcDrGZw_4D2}3^1+C`hSq_?+g&DU7fI6=wOGh@ -%~U|ZD+(QWk}xcPNnI?GPqe;k-fnu&c29pu?o!}f4##D{;tq>Tysv)APid!v0?0Ljk8FoV!3aB=w+G9J+cTipiDLaH$sh!en9MaewRNQ#SadeaVrtNoo9v%I|P<~C4v)+7 -OWs}!`oYE0SShx+AApGreF1Y)r%d7fP*oOAcwl_Y-nL|F-M^WI{ICi-Is6sTGV-X4q~lg68gXxQ#uCy -2}bW}$tt0^7INPwVPTG-=G}`~FQpqCSmWQkA!l+8HdeaPLg3(dmgZEc!wM9NEe3p3Qm}d^kn6cl5@<{ -E-Mr~pCMw*@tM6T1H5=Iz2gdDraeEwY_PR~+HAjOjFPz*i`&Pv3{LKfOaPYXrd))zI)ye7;ICgGtt@- -|H%u_J^AhOO9eQ{26WfSC>wg+9rfsx%rIc7!>XS!z@0q+=ar!(*{)V?GFK}{YaA9&~E^v9xTYGQgHWL5epMvmWVHt3g+XBTM -0_3jkcDD`M-3@lr_KKoVX^W28$dXQ!;&ng$%?uwRCCW+KUU5bru|y7s^Z3n!^n5 -IT`Q}SZL5aKQnkD&I6j!o=kwW2?(3>)SyA2I%JSBJt<-F`ZK^%1Wu0dCqRer!eplb!W%~0@9pF-@WHF{UN%*^7iAlplvjtcsCaJYBr^sdsj^%)S=SUR{Cg`4;lA8&l> -WObYreTNe>X+eR$0y4-D_sNk;c}mMn8n -Oa@PMg1%2vXpgnG=9?d{f6QKf{p#g6+57L_zP)<;6*jML#H98f+E+&5Rq^xkica!IDAqWR?>IP&Oa!v}-Umek)$%!)9v6(3r1f{Xu|Q!2cd#vTc -CAaBk;uccb6TW~ezBacSK*y%p`Z^eoA;lJ7;9EoZY?F1BpX??lGe3aPdk0z(3eEVVqJp)kb-OU9B|va -^4JL#Xz1GuLts=<%A9m$0@X@{E_vu0YVks`annNa@V_#HK29Ne*|xUdeU`yu%y;&-l6!2qo+Qj{&(fy -r9Rg7(kB#2UA-q@@##C??jKY!%m2T6|%geK^%;)JUdOz@N&_Jwrk1@_BjKdHt5)DgkjL>d&Bvfk|iXsXxkL`~9&>$g-9>Ndf_`WErgEBifV+X}4s4CyzQq^R-(~U?G^i;pL(Qg*6Dz#gXkmtc9M5o9OD>hUYj;iDi0X(OXf9hJ(sE7!R5$XsRpETD -cd%P%RJuHxeMzERR-%Vl!LB-NLpR&$Ti0g4G%dl;!kLbwzHc@Ay3}E40P0QHit`*m7PWwN{{yb8saIf -(OoWyO!etMO_d=wDMlS<{`e|NCXeX7I}rDfhkSEW>*#fHkJlLUZ6nI(><4@wFL+Prxc!sEDKOMJSZU?dzzbDbU09++JCIGtW=#H0_LfMm0%QnnLV*PVA+8|#0c(t^czME -102Z{7eCelA+OsekSBiAEbNk@y3N*bJEzfz&djOef$!%E%C@{!8^=n8>Gf+Oh+lExWfjxE~25K!z4#Kjid3CXC+ge?mpWlKLcWcaP&+B8&&jGj2AwPxM -;rzvm7oWI!HOSbLwT^X9l(i9^lLHul-s#fwMbdNz;eT652#iJw_B*(ChPV?x0bLD5N=4Ssqu;@{t5}M5XLxlkeRUh -CZaR7U;3t8qb;NNtPI|#`5))$lI0{%&Vg_xI&M7QjD_q1d-pRF6L(kI&k8hBH1%*$0 -TykT#Mo$i#%#CK@F^DHhe4TOh*)EWFdW9j4%B?`Y4VMiNJ-15?oEN`vLLr -#Xidu14um}d>UV^lZVW`z?BWw8S?zU`%BlMf6QgfUzwSw1l55k`(5Yj`Kb2*wXta#Qf*!20nMc)$pNL -%0c9p1_XCP9gv@8N*V47fzV6Hb9D0My-R{RqMEW?_S%a|?QNJJ2qVm%j+maWTOA`D-^yo4?9HVz$4x)Jxflc -}Tzu1k|kM#4?417K#Wdk-<)7}%m{+RRj4d)@+Cs0_dk+r3HOth=%s`KxNkz997asDEiS7*us$cf#Zq& -8T7Nonj69^6{2RppM=xHUHE{b{vDC+Y^xN361Pamc=S7)K#gJs6i_BYV7{A+LQu)Q--}#AqE^79Z3{foXVXUnPP -)|fg;+_CdlATO#0|oLHGU6b3Xctw@t6VJMv06c`VxQp?&g*EHJFR)j*!%eY7I;7dD%(~G)Ee{=YFFL) -U>JgYgwY9*g$-crrB;EzC~vM*ie{PW$~7`!oRXQyd((7bdK4b%*^kL_H{ir*Wy#_t`9Ec$61xEQ6gj -t76Bgs-@llOF`e7IrZg@qGDtaQq*UjNddNiM-|@fiU;Lyb4c7AuaK$im8ZBt;JIZ9{ -n^rXZHpV4&U)(^PGOn=u{s+=4>%D{&FG;BO95yhcO;>D)h!q*-2F{8-irWKO)s<95zx^>y{G)(VYT&+ -8pgPS~vTm5&W*J!Mm`+*<()Hux(e;`pWmP_(`^c1a~jkOk+=^X5+ -EgK6#(0+m2yv2;7ZopXY!<%S75SPG|Bn@!g!-L$1sOaO(tWxT~hV%>q&sfT~^Xy%%hiS^r2i&xOu7+P -o69}Oj8Mz$Q#grtKrYI|VKs}XR;WV=wn#{m%sIKm`>-B#2TWTxO5(7_{+Qo(FfU84L*Q#r#17?m1WyK -6||vWV=@zrL0Z-s#gOeYkg^({Otqz>2aFz%$-A!9Cz*w~&C*tdXuhX89#NMf>HCP8r7%`;}S+;nWpdP -}d=?0qtWUhVVXm)ocRuO>+ZeQ=^9g!}f5YZ{;A?v%Dj<@`)x1qV|~;hfq?NYmC(2xizN!`SY@pogoS`ui}VYnX^tzBlkt_ -#Sf&=erL>bxVQ1K0*^mEvGAOT*H(>aa`bcU0b1?Q49BT#?`9t08V9!Vn%J;H$1^aV&}h4{gPuE2UdS< -0y?E(#~c7Zm`hX>fmlSxZ^$u54(AT7ZmU+#Th2+p&(l8#aa%MR$S~h3CP0?6P4Th2o5k5-DJ* -0VQCj?6;q?fk6pkHDb$kJKK#Q)FvF~4gVBfv}7ADa}Bw7IHnGSS1O3*{ch&4#jHz-r0P=+# -7XgiH+*(a>OmuRdiLZH7|Oj5$a29~WwEA_RYtw%-2GVHxZ2-)jlIGe6sL#A^E?zHU+Enntt6Kn_Bf#n -3ZY413CcdYP^K>_fj&v|w8mV#bcMaUo#j05+u-$>6C&+6RG>I6+I5`yqErP*W*d~_+&yS%N*>QgRvz5 -C&{PTbFqEGoBzBj`?5BX4ENi_E36_6;nruXM-Pea7sS?=S)63OhjhR#8)Rbh`twi6f{?P%==1=+Mq$uOVl$!t3a1!8Oi^mb|v=#+@ogA<>f8kU<@_)4mI*qzY|=yx})H_i%>ziccx -=P$qU-079RJ(l(}C)L|Lg0_SOiRm<}j(hXk$5wJFD{a!Lq(gfLVUWK?5O4mvfXo#sUn2qF}J*HD?KxFGcU)@8~k|1u)E2vBSO3{IRz+$6Z-mZ-`V>Gql0U}&?Bn&_yiHa4#;q@K -}~ntV}oMSj5JCgv(5NcVC2L5)$IPD_pArgNj+?ry3D4e?RR`D;+Urig?DUYpC@P(Kk>HQfOG%;v1XMv -fT=J!h*{?ZnL)SwaZHahSN!z(NW9py+8dT5@e}Lt)GETq)HyzN(=J`BO}qW2{$G5K-<`f3wXYv2mQ3y -7F5Z28b8}4PT-=Q_vWIHi;D<+G(i6W%3g(!f5`;&ADgEF3>C)sXX?Us~+)NrCY7b+-zNHg8%ZLU3H} -u;lmi`7P4pOG3opUz?2wp&WmLJUz3gWr8O++o=@}`v#`P+#+7;hWsW295Z7pBAyTB9UTVR-B2%&zQ-ieXPIc)2XjeV_4ccfge(T&T-0hqd^^Z`2wQJLleu;+gWWLY)94Kg2zoQ2J@9R`Q -q6U5%uHc2Lm{904xZZ7WC5O_P32f2RZB2sKU_prrRNd)em?aj1>IzH%CTJmr6tB+vSETJ{+*mu&@irF -|#&fzl$0nKGeL~j)CJ|wf(f+?9-5-IvN1~1$sxMl~ZDv~CfwOy@JPxItsRc0Qen>TJj>7)$WS!}se?9 -J3R{HAFD!i)m1Pn1Sf7Uz26Trl8gaV)-WSHuT8gpymH%qC#MBRcBEpOL;2X!^L|2)s7&KmHyQ8>r`P! -9jD%?hJ7aM<>qV}lu{gR?$w=BE1{Zp8mhhyBaPynx=~eE>7~6B9a1801;r4tvc9%cjE+|Z!`2*}2M|5rTv2eQ3n}9oe!esB9l27Tg3`J;~vYU!&l!LG{L4|rSf-Wy%%aUtU -_wLuPT=qyu!F&?R@eC4wd)l-eFYq??c--mCufO{k#uJP7~ -*9RL6TaA|NaUukZ1WpZv|Y%g+UaW7wAX>MtBUtcb8d97AaZ<{a>e)q4iQcZ=5@dF}d(o}U@wXT}F?V+ -kF%YlP<1)JI?ZT|hv7zhNCRoeVQeE#n4yYJ4BOskSImesb_z!()}rL?7dZB$WPV17?XYcEuu3z-v=Il -oxLGZWHSUKE|@wcp;sb1gKKAngEaz;?I+;0FvbnJAzGRa_fjdJi=oScxje#1^#VMO<4^n7F0ZxeA#go -}u-)i``WNk8k(i@7d*-tIMzK?$_<@_3dXYJDV{t3B}b!9Y^>mcM-i2#zyo9at4_IA&j9MsO*Y9G=7V5 -<%@_erkX7xkDUtO@U7%57h90-6s_cu5GMg>%+zWT=U{IdTfms)B{n!DgrtzsX^5c7XjRwHh|)7xA1s@ -a9;PH88;>rMs;p4b9J(2+JuWZQn)k3(5Dt)?T6^>b*ZD9OO6%;Rx^><%b?Fj?6cURGy5LLt&YVYMhlrlXzm<@|19mue6F@ViDQ=n)`{a+|-ZAv#U -WDBYE>RHf3AQ0RHOHZrBL-OF02wDrW`#(sVJ`d_db0GsqED(ub>S6z;v3Z@wh=8JQS*C?f0J{?3BNmv{;fH%!Wz8~E;nn2NLs+6IVd&$`mE6{< -G-k(oroI0v=9dFbNKBbXA@T$u4lL1Eyi&WS2ZVgIMf|Elm_7tihOs(Ac?CPO;-P+MRY+{c{j9W$!d2? -Xs6Q3L)(8ouesA5cpJ1QY-O00;mj6qrt??u(Tt4FCX#EC2u@0001RX>c!JX>N37a&BR4FLGsZFJo_Rb -98cbV{~tFb1ras?HX%un$tyO(piCZh2eEnr)VK6n|;^`Ru+P@oltP4S-ayc{1I9ps=X)}XM6qwV9j@j`jN@aGJgY>7W!{=dv1g3Dd=|H)la-C3y-)3^KQP3e*Fje9b& -?Qxrdh~+_{R4lxA&^xZQ_);mU7OcVR%%|3uLs^3&YhI;=e6B0W3x_S9G5D1_<0{{kPa@ltl~~||gQ^f -~yUSW>Zr~4IF<@$1<=y5(TLO9eQs-_TNY!HE>q=xrIVlaZuRS(4(vY2g_w -Kvw_Ny;%|C+u3_U+rdw||-1`IL8Zhs!Ala6i$p@lU(~kJ?P2zahII3?U`PVDR_4J}%gw%N&gV@1%4NH -u8;t+Yg5Zjp*AN9xezz>CuG*|)Eoy}-pX -1SXWp>=O{XRD;96!K+&osk`+2nvcC(jaq3FfS@XmT6_r(=}vTe-$Jmg7w3nazCGyFwwkoURUH>VRf66%~30-BXL%Xy8@*S0dLDv(Yz##M -TThAHWW{p;URigg@sJG%K=apbPc~0!LlP;ogC$oVc#0`mt)*K7Q^`WeaG#R4_SyjA*l5JNYXxS@#;(1f?TMKSWVK2{byH*Y6GRl)KPmo8eG{?EA%=l#m^ccsSC<;0;{iC47h2>RZ0W -HxgXk)v2m^!xWRLum-3F-OT&8#OP(l9$Gz;v?z*8tFK<~t%fXR8qTzw1j;Orl_-w@TI<3wu#l}?QgKt -UeU(1LBt7XI$G@CQ4-pA-;#8SD!(NUuV=e9AM-*&rVjv2LI{jDg(+dzIY(YUO0!Ndlz+y1`;ATZ4w7& -511vPv`=rknM%mU;!xlH$d6-wWDJR5|K3o4pfFu99Z8&$T1`blQQn@X({>D6{rOAdSsehvtrFAwYPee -jL~7H?BkYgvPBWSsL_BUD~=PMKH=Y1aQ=o*G@|(}84zMAM2mJZEP6;3U~5=gRsy20EeVbwYDkqKHZmV -7SB}t|WD*(cOt88u -3tyLc}HZm0=lcXiB^7tvZGwjT8uNOpSDalc*@cjJRpWMZtwVb5knaGVH3z`9^Rx1m}1>gormyiis6cK -{DE73ywL%i5OSg1DPY(6o#{>66}D@M_$-jVSK59>ZTEJs2wwLG(VE<>1`8&!h)Cz0h+~$GiQ=5EpVJg -6>)T}Ac$0ai|5-a@!W3QV{Ly}bAz3hzRJxgWqR5ZYRR;+X(85!Z9Km<=4+IO4vmTpSPqFp0d!KsLOzG -)d?S}1)Im&jft}8%hNc5TM@l2Qa&!7`{?adR^CweeVYfaXN1o!gDRo86FWpeyu8g`63px~Yxzld)oAf5*bJ ->4hhpWq;Y1+FikfzT7{?f$6d8s4}^BogSCdsyEGylamsvdiV -rJRxz%iJLW^h#i=`V$aS$CtlD@(>U%}RjdDpk}i}$9dv77CGjb -N=GSGGbmDtDhqn`yOGJ_0k0jm2nD95l+qIVj`^}T#XZ-6o_8tgQFXM3hW&I@58htYcu={u~;4Jr0@Qw -FArgRvURJY2D@QjY*TronmPuFdjTB<#zh~Y4eG-+_rOCS%04G$yq=WW4aq1bpTY&VN3pU!3FG%dy1dcv%25BpU7S+$E9~l%HG< -+NFNOb4Qq$0#wIr$VMccF>r(Y-W7xL%CgM*3o5UY#84L4YsgC3@m~*=s?S!;!egDiA#=H~LSwFIZm`G -O)b!8|0CLG~(mim@yBol3MG@kz9@4u9k%_Jp$#NZ}a=EXA>Fc`oA()lVP>ZNkQz5?r}mpW)VPr~%y5b -$+itgtE7klQf!EDdWB`Go>=7pR(8MGxsi`UT_;g2rm-<|GRvZhf9!3{`Xhg$2J4zA$;3I2aKR21Kia& -YdRZt0FMTYf5bf}S-BJzuZv)qtqf>D|^y6}Zx_dUjRg+5B$T?QE@0dIW -i-VnSzCik-WU|>E$PKP({+O3en&uxYRET7yJd9y8>X99fVc*ct&vjeZYHjGq+WgNTRj;rN~+29oUMu0 -$F94(^}Sr`f?A{)w&*T-0n8KDWOdER{hFnr-SU~lPE!kK9h-9~)PSkRygKa#=#^@sU@@S$@JP>0|@JE -5L#RUc~W`T^4mcjnZq%9H)LpVOO8G^9VX# -r&LhanR+SY;r%bZw%tqbgT-xkC%hg=Z$Urg9ue2_n>N>fj{GyNf54!?%O2%;b&@;i(#8o_;$nW=xxJ& -%yt@T?Zt+?sFShLLQrp4Os;3Jc@b1Ph5@S$+8ty3)MFms>@t2?D=EDEpZ;P00(<;0rT -ZqqWm#Tv)E@7X=Jizp%Y5~e??qhRJx^+;?CHju)`yKfetJwB2d0;U%w&wS!Q?+sO9KQH0000803{TdPPfS(c6$&20FpHT0384T0B~t=FJ -EbHbY*gGVQepQWpOWZWpQ6-X>4UKaCy}m?|0j_@w@&Cyf}FhTbYiNc3HGKeQD}!-WuDB-L#$U(l7~Go -GFq8NZVSP|9y7{fCNa&NxXiU-`*kuxWnOo-vNE_;KAsv_^nQ*Sc<$-Y@TLjH3T -DWW1%rxD3|B20s0UL+m*7AL-11k76N)z$vUnI*kep(2Ty~2@o%hNm&w|4m_m2mlCcplEs)8>!!Y>|rz%TmaggY9Ob03jT*M|g4aa`Ru0 -H=jpmUk0Zi0_4}xkB5f`Z;yAsj}8ux_YU@tP;w;n*1Z^;<{)S_?annu+?{ac-kZ#eh}5(qn`zHpsrz(*|ivk?Q8g@hru0T7dqcg@f&qXPPI)QYeaAYoP$SUTc`Qc45P?cf=A6q_ituPgCQqEkV{=cV1%};=o)KMMi6GFp5MJSk~fgBaOye -120Qae!cySKn5TX{n;qcLDtSrKotVE`JxRQ{m7>`fHdws$|U$wm -o+Y%u?Ie)u_0jGjt5#DqbW04H+ZscBHohIx+TY_v_GefCounxoGmb6R*}f-C2Rq~sgwnngPNF4rdE&4 -a`c}F5&^)4faDfG(4V$=X0EiO6pWWJ6I#m8CzPZ+RC@@u1x-B`4gI<2v$4h=bn2?o$5A;Bs{J{2{bBn -vXt@roptL-14m$I;(q0GVY*e{}yB*i33xl=po!p|ETw9esDzmhjnp$#7zVF|Km5%zJZtl`4gbO2g=I&mkbGJ*38t9X%4%mjX!+7RvWE{(8foZuJm{}WFLOw!!H -kyLYVTaEuF?{W{8HRH>l~}-pX#;*HpAC-WXpj57Dfo|aHArQnKZbaAd8Ceu~ft}r)D;_b>jbjqVfkMA -wwWK3(+U`Goel*^gkskTEbv|7$>rhL80jWq6@v4qhC>VhFJ=WrQldWZjn>L-j#8%ZK*^#N}13yGS#`s -Flgy(f^{(4=5_aj)|dHhtzh&Kny2Sw}flQ9wOm8#qBjJxE<(KY&Qb<64 -rJpaN+Ln&LXt3D2rUQC~u4T1z-&Og%F^2^|MLyJD&?u`MH#KuZKvl9rPNCOD*psn6I`nSs0y(!S%hY2 -QFb*8~L3B+AmI&+QvQrzOo5Ua%K$NJnkBloDan;;CkhUe18=aiq~}6Rk<4hMi^GY(v~3dr5c&a|1 -Dd?OiQf$XMENDJDCd#|LKznMA -Bg0vO5wY9zd?5hUIKZWxZiX2IqjO%Lt=dlx?wJwkDwIkpMi;P-f>iinn)@HiI8=WRh9D+c!wS-!wjh$`418T;P6odbf$lymXW{;ev?|3sFqZpjxM_|AhZ_4qy6xU6^H%yjj(l -Oa->H=n~Rjn{nRb(7VZ-f%sp=gPx0Vr!?L=45S7QC7D(^a=`w-E7FWE(x&{B&E#a-DQs1&)G&?P-7N8 -aFS|Cv{s*76{X3yL^Ye|KO$8*sodJsG;tTPd*&Wf*@&O6eVd~MG<*EZ{!5mo%zrma`Y0-;4WYTrzJOd -1>u*9tz+n@*P0r`uULkw6aVZ+{5$qXi*GcXrnQ3>Q=_d%gKW>Om*y&N_zeL-<;btVCC572912ob%Rd@*>Bv?R3 -P1lDiwfL*}QT$JD~{CZ*pY -+Y$ow-W^v~eAK_TpZn`QI-`kIEp3cM;u%~q!uk3LP$nYEjsjAH -GukHP_H=Nx6|VUSOMD#uE^-TU}cfGH@U-Vszi*>jC62lRJC``$%8K*Oc`AWGx(`5F7dO;Z@sMo(?48S -Y7VrQsbi%dVo+*I4>4^0{T<=~??l>bTKuSiIpwl8rKbE%-^GTE{bYJ6HynaFEY^>p=rE=ols6Z1;XVO)VQYkm&OYUpK&2&U1CJNSeeSu>mO9bm7vN34`7PMF&hU>vEF){whGZeCo? -@xwIYrn+x3*^FKH;GMzz8!eKOvJde{Q_6ENdz1PSYoY>K#Hl$P-MM%270D9XuWudNJ+)HaA1AdENPwr -kz5k+t^p`Yh}8rgK5B`#K);-`Z;i)5Sqf+NP1?#&o}Hxa~8z$%H4M^CByag6jsYNv`!!H0St=EG0ATw --Ub`E-4#%dq`Hs-aNG3kld6x7HHB+$}}^OYQ4q?p(zKxTiM=4b6{%ffMdjjKX14u@D38p1MJM2*p1|+ -l@ONONNWuzxPijWKFJGh1I&;TA>xS=*?h{D>e6cx88OpKHf*b)sO`HoXBiv@BHJAF66rz-Hj{u6dnS8 -1c-hn|Zf5L}I?W#`>YwOa4cxwM2GC{e`genqX7vW!KRAZCjBO;Aq>AeoQ5)^(0` -xSk$UpglV^@1k|3I{6mf63#bk_6uQL`kd?nSfD)&}v?akib0h6+ABExxnhj!XNJi`{(bz0Q=8i7r;3S -fqtn`dM8-oRdc1>kIt0T9UqH%ed?zY?T(h*9TmO%@}SHdx=!mNEqgy^5S3m)l*<#SKl7x9vRpX@kyZT -`ZeIiAVun7!P}Ts(o+!<}Y00n?Bc-IT$`=nYFDHp9Lxi+^XRng&_Svj1fFC5E6aaLtqXMUKo6jXD#+D -hC}_m?TifrZQt1a(^bcS7{K)#&u-(=&o}_;3d|5aj(-J&8v@Gw%@_8`#hG=tb{Gy`$95h2H6_YvgsCFn?%?ct2BK6ta -V_&sKm@XjDT4?N!=<;hB2jrz@Eqc)mD2@r$uT8+a1k{ZnM-Om|5_7>tk2Jlvt85vrZ4L;Sj7u--jT%c6&w(J^w^L5JEIaHmO0xGWSb92Xe -Y;S-miNNL@-RqIH3}bg*Sr1%iH@(X9?gR02OiOeSVbu-Zg+nxkRFW`khh)<*;9t)J$q&yl(&Xcc@i7Q$@dvKQx(q2L{*kMV*_$f$*+H}~GFdixSyZ| -=W@Ck(VBY*^M8Dl8yUT*{&@!$Mwefxtn8II;EoPtTt}-+un=`SUH^;|SUhA8}m -YqlE{$u_7yI!MtXfW72krDkj1hBY$vQJG75dBcb()56=rcW%6UJC|;GA-{%WYoVjuT5q<c!JX>N37a&BR4FLGsZFLGsZUukZ -0bYX04E^vA6J!_NO#*yFkD{xe)0G3F|a>eo0CbUo1BUVMPlC1c0iV6maAs1N?VDQ*o#^wKh-97IIK(6 -DvFL$WYY8RO4nd#~2>F3P0ZT3-?ZHInnS(ZiR{?N32l&`y{8hV!Tzwg}>@2h6FE9;&7v+3k--R01imH -O-08NY6tdRwYG|11yNvSRnP*m6;JJyz^e{3%=CT;}zTWo^?y%T(@A9@6ZZ)kV|FLWiqe*0Qb{+70U#Q -CHr2MbksRxyrG_Y0LgGlr7t{y6+@}_xjybRqCJWT3$DGxyh^YpDe5MJ)5d|&DySPYFYO${LeeoIOJ`| -qP*k3WY?nr*x{?kU#B;1emFoGS^XWo{=R9hp5|4>S`(RN-LtmN70z!i8LI-{)AdkRMaJrDS@KQu_V69 -Pd|qFdZBsk=Qx)0mh8=q7ujK3u2S6l1;M*=(1i7h`MH3*9Q^FYh32tB~!Tkp8gsyE -!MXzUIIevK7+U8RrY1Kv8O5{d~muc%60(+d__pf*e$GgSgdOx_~F1m!dj}bZde2MU?F!40s(yCP~}Ie -wID75&1PK*9EmR$T=Meu(BZo1*&csq1c}%6E?Qx ->o@nH1z^@ -M3t&n{>R379&<;n)_VqHZp-z6LmjE6_L%O#Hs!W7_K6Sj -4f387wfA{@WmJ7lf8M)y4><>Gqm>j9QdkzR=v|W0Mww-ED7L9!(aDX#*npE}jl6TtCw(@X(f{w+0ftI -@cGtifyF@DJFM$K@^JBCIEX&0m!L9<$LG48p-A8I?7m -98_8Cd$P%ECs#I$~U*Dn#F(fFu?HRDae`I29pRS-oj$76Ju8ba_^kJub4<3&4z7d<~3EmBNj66dvxh8 -sKmMuE|zEw8JPgL2#o0IRzRZK~UoI&}bEeuPc-w>Ov=q=|MGIlo^GIT!>>Fn392F&gY0HjMmjm!kl23 -xEAQHpY9jCg;L!rnd|S873#SY%L*S^C`r?F9xcC#uv6+QEKb(aIsmlLrwAqPj))ytXF@b;5YrrY(|=e -5jWQ1K0q?aYS@^2|a0|&{Qz7|>o~*4wwAjbG>8YQ3gbJ2Xa0RMLio!%%153bP68QpD9r|0Bp3gOQ0rR -IITL`4ja+$zK`RhHqL}ri$lrsJ@N6IBsF%QU(5VPX{6yz1VXZNFb&)-E~eev(n$M>&HR%DeTm@5)^69 -Hy!_f3?b-c!~erGrP#iFMuNc0Wm4bjHkty?rfGrbto -L2Gq<x+wC&kwMgz2q;-lX-;aK{gnnH%RngH-WaG+| -3?jbC;)r^z6QYs(ILN$eSza8Y{5mfyB;O0WqaAFj9S96Xwm!Nm!069!(jBxORKQ%j=%xgufc_0oTVv- -h43HNdH{o?Ns_XZZp%jz1AWM);E_F|5T1fO(`V4@vWbfn(q_x!ydcUMHv$@g2#}0Z3!bS%r8R27BHEU -423t3$XB=E^?sJm>2sxY9DShHrVjy5nsuxF*wQ;;h*O^d)pO*x1N1*WD*oxzj^gY|5V=1eMsJ(^DyQ( -R5-5G66blNZhe@VC$g|HS|Yq9)>npypeY-MV3e3gC9AeNAo_9_#|@(~vaaVWMc8DN)~!K-*3t3B25aL --7S#;bA0i1biV8pe&-+cA?gGRKf{D`$x{k?s@>&=axXOX0xE>2S#RiQk_(t|2N;<|@F^)tk2^>-ZWk9 -2zJ4;bk&Gc}m7o){vVe$1q1{=Y;ECXl46*y^;O3_T^(5^ODiJIMbnRY$9JUz@aQct2d3lV_V#15CoY%Xh_fz4kFpJ -9L=QBj4*`DjBWFw>SqRF68Gt`6jJ}~0d~)_=dDBkOz`dqpq*|wpiuz2Z@>eFD!7=eNXXe%`OerrdcL2 -bts6xT8oIMwv|@+*iTcCTuYpiNy~ffZjfd17G?vOQs7we#bi_dv0{b?RU+30WSq+6@yB@Fma23Vegz} -(^qX(nr#zaA}rEn6FM_SH1fiy=dyok1B)?9!vxNCvSEhLA` -xRW9@5X&ICAkzo`>Dw-5v)OZFy9V6nT-(Ks99Yb@8Guzv?z|ACdL(N4lxoiYrs)L~tr4SHvFmYT16ag -am)OQfJ{E*RT$uQAZ|L!+P0>)Tl;mhM`*Q1G1S@dcSkcsRA4$f|#SpR=05;er#}D+U7GS6h3^T88@?% -HaVNVK9RUH>xlPJpV7Q`#)m}m}$TvG$wE-yh9%}vesC1H+7`!<=aJJwzUXXj%c5J219L|Z}x!i@H1C! -{6K&0h>Hs&ISQOctWoqPG!XTv&X057DCsTgSevK@I>}2W_z=X%xmDh_+nlZ?tb;`lky#cVgKrY9@DF>Y?e-f~;|wzlnO(*@=rxC?cfA*gC5lVeV@|4_ -e_Ks$Y23D-7HmE2lK3M@JLbI6GDCh6FEkjz3&l9lCUl%pdE;oveOnGj@;NVrlB-t~ekaQXg#E;w}7EQOiCE`g{$PFlMtp+>5nii?vr -3St9=U^?q+)fV%g>|LDte5|&7H-4FVz6 -JzbFR*9iz!h|UJ5Vu`sLQ7@^`5HnUQGKwy1fP9kfHi|`$_4D%oZIHQEEp#ZG{B;<3swebE)3O%yysh% -w0PRJ(JY5fIT{kzI*uHUEs{B%09r`mc;Rzh!%!O*R0Xo3nZzRt6GAFGpZpwC>+uEe#V>QOo@IP?yE-L -?i_VZjH$q{sJJ1f133JiEw3-pQS2b+r%U#nHa??kiEY|@kV4^8waf8B*lFbG(La^mP<0kRQsj_5sP8($s|tgS=t6F=3R7aKi}jO4M3IxSoMbG -OWF_Mt)!h|P6h!^0Ggu8K8^OpSA@pyGXL_+-w|0t83hdZVspcBLS0+{S(lqjX+x-oV#i=)4QxPw@SD6 -XBBJuV+BF^80u4wc5x;#;0*IQU9gPTB={2i0UT9v73A;e+>)s);3ykw1e85!GCru1davw&vB6sVhO=z -KcOP{ -R6@sL~`%Euv4K=PAZ9)`k8wcew*!H;Dn#UMFye2Kt7F4%Y4l_?$!9_xV)AKGz -LU6l&MQ{&1Y*wm9ITp&fB#%h0v0%1(T95-29-K=$VEOl_i}BYubXQYj#t2KD?~z%EW3rR -%A=C*_!L>XOT+ABz8K{vqnu=@G2!Ee*gzz?gZ9RT*Elf`slpNG;n5p<-bjE{u@>%#EtRGuHIaO58y*1 -wq83Mq$*=?$LU6quE}~^`+5EJ<@tnXlJ~(&dF&V$N@0evK*Sjc~g!zeMqYP`u9zwAJ!^>apYQVj!+nC -`ft24SdzJZFk3RSXZHy-G~^6`3XaSVrBm@plN(dH${zg_~1Z$yZnD4Lh1eh!^G$Nm5ekAYeM6OCc;xV -iN_#JH>n7JJ|vSDMvBLw+~enFS*Twm{BW(WeKU^h{#A08Yg`5zF)TorT@LIid5#MBg+dBkq85#8Mo$1 -!!SS=KL&TFl#s!!U0Zpp}U2Kj$nN}X3vZfc*=RZ9W0_a#)2gFUeJhd&+p079ue2NLC9nA!-JSQhC8DhR8XwE#U6u@Fb5TScrE3EGE+A=93~24Hp@O7%cbXFWes5lftv5+)m -6VmYTm3JqZ}tV?7)a2Z{6+^prG`=3*rff>HJhThnH$i8v4Pw%EaDkD43JlGri -P?~Qj$j|B%ZK75L(eVvBFO1YJNb|a9e_iT%8azPFWi4NWk7@DHKDOnFEFeA_T9lsl=Lq(Uceq1jStEo -dYeek2m>|4;{3cmak{zMP$*rEbG562*>NL)@?P8$JJN-!+itLW9ck4CTBOnQeN`J1MHfuSB}tcxHlk-_~`;LiplM^2M?dUn -G@SK65Q#hZGIwfU0_CEVQD#OhtZ?5BF*5rbxlN3ggsD^H0ad8!n1K;6?7KuOk5rX!r!n0b4wBE10n0rq6eb@>_Mh77MEMV?QdaGOg{G8uub+Z^ -XmB|RRD*M+u*@E^fefGK;Q4+sT8)GkqDIF@-&THR9>i+3}KfEPEP2hsSl4WtFBNwU?6x1&kx8BN3uBHHSE*U83JO7Rv` -S3%B!c>S~skf29|Bxd`I2}7(pD52S17apWk=Wu4Sq?vp0>@Qc$Tt%n`4pl4c4EZ_w3MIXnZKizuc))5 -9^Io0|K(V}IGyU*hpNKpa`7T+P6%FypB%H%(#tf6CkR-Cw^XbXjgV-WvD6_3XAM@0+u;86B3trm&?z6 -A5lKAVAQ{BHsqR2l7egMvoOw#LibhWE%Eho|Umt -8U3iaH6W=Whelz6+qI*-=Mo%4#g1o3!HzFdKMej(XPF?RP&SB>#|NWML5iU`nQ}8!dAqRe=SI?lYFY* -~4$e$r=)+#tlJ}c3K3Gf-IPXs -xtroV;YvOfwTI>M!=JN9384lbV`J|cz7q*T_u9lXMZIQWlP)B7#q_QBakd6Na6 -Zyoz-;2nsINY!ssRM*&$HY$$5$8M)))n_MF-0`9hw~7g)v30V!{_nw`;YS=K46470XP(W^e2aUu27xP -3qB})=kbbyWxSmSFz>F5FtYBchnW4%*OQ_yAqiZd@?*b=&w*9+qT+SW1w31`G;TCkiHJ%X5c -W+CD1Rp`z0XU73plr$vBGj(Kx*%!GA(00-gjR~Zz^+;a?+$Ak0>&*dQl*SR)CXIqq2*&k&P8=cw$n^4 -iP4xx~N&~}b!=`k>2r$xPYGzwfy;vfYv8k<=bWS87FGR#fv>!(oQuV8b=9A;*V?m@@0MOtQwh1El2HC -Wr#`Ab0Vr#_n+kL$oEe$TD$dfQL1!xs;_xr(TAm{ovx*b0=vMn>4b&(nY7Udqh*^SVkYjw4;=HaM6{= -a5Zdn^2%s1q|5FKtRSrDA4A@ZrNtwT4@w7H|P&1kbY*NG!ME8oq+cc0M3@nx>=%GbGg~>X2z*{GJ=G+ -tSd#IV)-pLPqO)uOaQP#4+D=YNu|6ef)r#-D%wMpSEEMZj^xufo<~`7M4hxQFXqrxNXawa -zSXW)&aE&S@zRnZVVE3DJsqy8C$>PRvv=SYeuo@Igc<-?i3aEc -69sbW!nNp&MLg$Rs-s2g4vJ5*Q#)`|17On4muSmI;dKr*#YsbPKV-OS0+TUJf5Dk@8Th#9K1*xz7)y1o5Y#a{?kvJVIOQPd{P(pMFAxxVAz_E11+~*WmPH^NPi|1io=4wAk0@bfW1}o -|?j)aZ%sX@}ikR70Y1 -gsmoyNW>Th_|D_JE5|GQ=(Hls2ntm08u9?6-HkJw9fd{=^}(AqQ+IZ1z;BJ;~jc_HWE+cc{uf8G&Pyh -Jix6*LW=HYl+aH(8`-XC(z$+lEUnS!PV+9o`WJsGY1N^YQY3}eMHQ@gu=lI8Q0XLm~N4B+EVDQwLF6u -H+n5ZSf#A}8q{h`hM`PiOTIuLc27L~-46?zC{jGy9}mYzW!DX`Ej)hw_3zF?ydr5g-O62T@G%L~uJzU -oI$WtyFM=pt$zQxcCS1kbh%53eGI8M7d1vB`8iNBWoxB~%Np|@Ziw|+|#3y}X5V3U$ghzq~N(GHuqh5 -mey*iZU0p4sg^k$H>Y;4q=<-e7;)cjWFw+}`_2OTr2`=pN0P`M0dkV>%6frA}&;>VeT36EXH#bNsCx< -Si{3@Xy3-_aDR&~t5&;C385El2l9>}hB;uKQDo|E8V7@$lx(l>->F<0O*u18>cgc=K3ss<(A>WP>>lC -0X9-s2EJP_YWS?qq(4Sa4Fjj?`ko=h&_)P{kBCe?$xS5?CaW%s-e)zGp?C -))rbu?md5yg?+c-P=L7inFqCv^&7A=LLbU2i_W~lkrs(sQVjL%uIJmC-ld>qfkXgTs|jD9`WXJ -S5|N7Sn@RfLBX3$jE53=vJi4qZG6*`3D)2ENQzQBo8i -0Q3<_B5}tvbc1iWuR=A{8WUVQqPSyD(f51F!CV{HElt&F(2GWz_^nhI)0Xcx)vw&F|eDMN4BONNS7#` -uc8>$=h$7JANaZWbR&bXkb#;hZ>Tt@trB{(h#qUyY9Blr^-+NqZxR8_I^sZlvpWG5jX+RY-D=!FE0)OFXI2KRl$RM#2*xMulQE&NTRc -U69EuM?%-a`>fZ;p-SOY*tMky)!q5-RIDq4zBLZb1U&ad@qKZwkebR~jy4o7oXavR%|jV(~9yFmmu$O -jD3#hY;3y-do1?yQ?m=I8`?Rv?vUjFot2W1E-3S@)h;N1dtPRVi@+hoNq6h888RN}UI&BjnBd?i8qZZ -L>f0oxCrgm8s<*X3!DMRG<5?h8liqJK|42nP~dyCwcz00sKAVr#4aCd&PRD!3vno&|{bj_cBq69&Pgg -ZK0FhWBncLOe+TjY*NM77#8&buW05!5)T`a>Kx{L}-5?cwdS -w-?dN`WhV|J34JZT-l^=KDoj)bKpDvZ5#p!e?Blg(T<-aOLRsQM@jjAdHMm`l?RFHElA+^F2^6<#Tv) -W9dZ_>P^>B&lZ&^FJm{7Wx)iV)t3B0-PSx?g*D5620~Pe57`IFtEl!>CWP)=~d1BoGHgW8*b7yrxuJ} -RRlR)=qP@xoLi8+~MVTs<%OO)%9^_(udhTrn}8n4Wa18L!wSLZ28h_-me;9=BIpDesIB ->n$vh%6&5mz%BfNlbSoQ))YMn~JwMiMV32I9NS8s$Z{3ZCfGI?$&A))ysd3Tc+#Ej|G@Fp!|i7=H)Qa -B#yx9GMlgic%lOr|@)nHGn|jK#;a^ke1ofLL=f77V`iWx!nLXj^$%({n(^q*MkKOIESBOnM@$+ZVAO9FV{ylDvUo>BT_0?GY^rkH_u%S&jumxqmy9FrIJ=2~Q&+*~BfQ9V7G0s<|!}LG--s*6auw{kz9aN(v -Vs~IMbr{N|;a0nVc%qGCOH@vI`k3-VV6248F;gI~z^xc`$3`zQk`n1R4!WciPNol?QN_%|D)c!q4m!p -TmZ5&${gfB^*u-g{YjgStnm|z5s5c@ofd(xF2;=# -iA29D=wLup5_j#@g!5d%)(@jxb`tMD!Bo -2D&hbRGho9EyPu4()H~Qq&QNKvqhHDk9ysyjt4d_Kp-Mv6;6!b+L+ZR~Nna8IQ|*on9>>lLr}`U~2vE -yB%6mJU_l%y~2-g+gBWp>m4-#K&KCvIN=Vrik)(7%jfLFD!d7J=xuY8b#dzU)FlO8_HaYV -iK%RRJFP2w!B^4$fdnDvbvd*?mgE{TMmXA#5W_Zsf5N8swkKr>JIb|RNVaPBa^Z}U2DLYWd#~RQ>@$b -{XC+t8pYHcp_ddK)6#4;lTPSKr@rC51LR -#}T5`Szy>z3tLcR=ttCNq5Er|O_Iyf@XzPt_bulePNnEvtY1X)_2=)m-{4ki8lPY|ZD8Dhzl2VReoqh6+HMV;VX9{h(Z(^C~JuZAu!`5J}l -o>Om8x9Zg*8qeI7kAECJ?*V0!{ECcVBwVg?1f3FQMJ-~mVo6m3GKQXW2Qt}8M<-s`EaJb_v2hMRjts> -_d_A`_Ey6O3y>?v5K;&z8e1Tw2mDhS2#UyjkKz7+ttBuA{ -Sbj8Brdc(j~gq@JuWGUSImt`hV4E4CCm_fi@~tK$;Xs(iWk;FNNd#V;_ -|u8Kk{&zzT}8(OH~O*fyt3>9+#_x>ADO9KQH0000803{TdPT5?&l|Tsq0JI(e -02}}S0B~t=FJEbHbY*gGVQepQWpOWZWpQ6~WpplZd9_(zkK?uxfA^6n@x)v*tI2b{?GhoNbQV1iag)7O=}>}g*u+JX@uNbU9}D5?s_ -)s?pJ!hSM`3jBeFtQ6%>uqwe9KNm=X+>->L#>c+p9nvRu^-m|9jDd@kS=P~6nEkyRz><||{giIKI9s% -+K??)K6w+Yz5G6Z`_t|1&F$Y}l#d61swerGF3|j4gm^s@_+bvA95O!@H -^@(JD@er;%Hs7KHe6Pzc;B8X7>Gmh20qar-eAX!{PY$+waUN|EuYP1@W>ft(rVfEL6Kx7`Hw@NP7Ug`A$|AZ7B{=e9UcoBCVjz*;{t=-n6i -C4x!Hr@(8R&veg<5rvEGP#aH51*Kk=hpU+H5w#CXG1mo|e -`6lU$2efBxg2qU<}XQ9yVjTpV`>62%|PJpa6wCr%l?6S7j$u2GPzIA~Yx&3Pqj_3OW5w&K70kh-mjW0 -{Y&_}R2RqlNtAhQ_jqBAoUlqD21nIVpnUiuwN} -FVOj`wN|xu0d3m1oIGBnGYLahP=|#bn=T%t-RgwfLn_jLuSf-`v;PMDM_XH?%#f4iaB>oTx!nHWIRin@gqC-ZXeaOxL&Oi@9% -99$FSQ1Gp9sy^cwkn0j2j)>BZ>URwNcO0j17_0+Xf`R%0l!MsAFZ?Cgay$E-8NR0z}(;Z_-wHQdVWEj -Z4+8rVY4XDXjyOct!}{a=1jq%p;FLEBS(=lB)P)`jHpB5!oUluc)+zaMr;QLTdP2Z**I$B1YI*lmyHx -Tx4;DeJqUy>l68bY1E?RA(KSc+D9uMO))VHyU@b7iluOMZUc}iJvZkP;p=cv03JLWSwF~5oGYQucpi8 -p9aAO)m(w&#a#A2fEh{ZsT_Ll-@1n~ibg@M6TbD;@Fnf^ED=f*D@hEjh(xN+Iv`9zeO^*zd_IHW@DYi -+CN^*7Yl20F&k&zhoRh4>mYid5X+12Xrz)+4&8Lr|D&Zb4#z`a$%RKX1(mR6ldKUKfd#H!pYTBs -3mg?m##)J;0|$zfW`CV)M?!a??QZ;!Vvc*)hcEx&`u-s2;UvJpm}a{Zk0RCX0cNhe(|8zVpyf+dWdB$ -=Ay?B&C!p-o}f^>Gm)2Shvh`;yCLzyixl?wefj1cc|=H$n?(*4gsG33K@e}>i-a|tk>i`0vThg+!|fc -laoe3_LPWACRAC%Y=2KHJi6{T_Wp;U-j53WQ%5j-&1*e(E*|)Hl>K%gHnZW+Urzd2CGsZESi$#=+)2c -UYvSj6F)qCPtW5tNA;-mwG&wfxXxyCXRLsZJHTq-1AtWDY0-Jw|v`2vijJGvDkb6m4T9*@OePj&Sf?o~Ehwx+At=+ -@yDf#%yT|JBBbG3Ie-ch}Kku)+dSGHjwac3>}!5l`Pt6QQBfgH8+Ge3?wet#IlPh-xZqVO3f -EwqW7_{K@c9eOZkhGpf>N5uDV<^p(LkAit!;P@fM#$SN@BJ>SbvdhS -;jIwSr*o5t`yZa<1{W(Zgm@$6{83Yf#obG>NIz8c|(9_SxqxrYG-G<04_UZv@+MkG|pVP2V7B=Nn-Ym ->&KH-D=mSAJyo9*brbPP)MXPRqkeIW|8;hQKMD{xY%v4bb_otLFQ!t4s*q^i9` -Q)VUq*b8vVTrv*x=jvl8PHRcvCIlgqS53X2Nd5SUSz(pwmtIfY+y;PkM}_R^qY1ljIMfwB|*DOJC6Yd -Rkw7)*zKEm{~d)~zofj1*5;{#Uq?;55A%31sMKI{WS3hEqurfzPtX{vVpGioho*%YC>o`UG0(W8ATmw -J`N@@;G{BfAY>QWge-ifFZRpv+^pZP0&%aI)F(~4~PxJa(OtEOLJoeoQHMs>zsP9tJXlm~<(u)k2ol- -pDWQ=TZ3Ujmm6&Ld4j>(JjMU8WFrvYJ^^z=@7)^-DP7a)&=J1go=mT;r!n=z#LwU)IJe83%F*S9Ru&; -d$f-5@&(XOl!|R^ng$Xxi$${26|B+pZF}?S3>0zJsQ5Tdsa@-Dv7Eqg_?lIU*XRk-U8&-nyFgT^qCt@ -~Z4M;r)roFn;=5l@3%~uNeH^gMzP76d>pF@LW6UU&GK)n}yc4;&;q554 -TkA^3*nRJF-iP3Wz_rCod{C^`ngXbB<`x!r(-|F56?)T+s!E&r=Tre6Et!sL8#`5&Er$YPwTJhy5hIrv>K=3<)XNvXU6djBcSIk&ns1z_3!2T -)4`1QY-O00;mj6qrts?Le0Q0{{Ry3;+Ni0001RX>c!JX>N37a&BR4FLGsZFLGsZUvzR|V{2t{E^v9ZR -!wi?HW0n*SFl(Fk^zN>MNa`-bk}v;U{fb`)5F3r1lk(gT+5`Eq!RnzcZRk;7%7`>feJ8SQk*w4Z{7^G -G5Wx=Y*V?)K$bCiD7A4+tgJ372ibWYMVI}xRt}D?kgIwJj8VE??1bHQ-W7Vgm1iGo*77M43WlvyrK9l;~6e~|{(PMgy=%uXeGsEz*k{O~!O{63w0%I1I1X1BB7F!Ig7_D#g_@u-LZw() -Aj8dhSxf5hG3OvlSDMj$Y(l--w3Y78c2e}PL`XcUHP;4A%b!#YtE4c2)%hp!bTdjREL132g$FyTU~eQ -kV)|D%Xl2E)cR-Fyq~&hI5zro;iuI*OubT?lJg#}Vba$3uGc1z8^5MBZ}_8`OhT(q&mHL@u)Gw;&d#f^`Nc87P*nfI8;W0 -q=19*ucEM!vb|LKI7XhY-8s9CuRa)G8xRhXp)IcNaqlJbEButz -V?Akehs>$uJcKW!b=jk=%~U3?T4rpGn-&E+yMe4=jd+l3~ftJRR7?jSjzAIhI|{z@;c3lOR*QJZbvRE -bkY8uNsvjE2DwEDZ*aYhBAF4QJHexSrD7?!^1|KPCjiD^q08((QERvW*d2Dh4WljYB6Hjrb0Zx!i(vo -nE=r20Mfz;$O5>Nvopt;;$3G$n#6wD8r$!`(o;oClIXUs4HDmGrPU_-YBUTLk4FvaDTnCAI9fEC -+pgL>(=Z{*kO{8oIWqZT~`wtx32xI&Eg3;!56O>_6YvD{ley#zXzej_Hri@|2~}mq`LaUbwO+`L!I>+ -N0LVQg$JaCyaCZI9cwmi|7!LUj<>N^MQk0vEek%x;Uk><0HXEs}P##dM&^7Clx+mQ+z1$D5n~zUP -IcNQ#>Av<2255|2b4l84WE<)OE2y^qp#+jo5{(ljdeP2F};ww86-cOvD#&(75Ijl9)gw|%wg>bjKrL0 -Mm47uB`?vo#_Meuo*-H2C334>{7K7S#&}_ktr^`Tt?#hIxUOpCWWQ -!mXUcu#Hza8XJzrfLcMK8mF462Cu`)|i(=C)qt`Gv|4rXuZp-K&z_ -?}f1ECB#_fEi{KlKf?B4Y6{mIdL&!LA#%E_|x!6Xj+ -_Xj0(Nnp|X-d{3+Q93+hN1s+ooalepVjs`M0<~KFpwuGhE(ZC<$D)*Cs!_u;7as+&5T4dDLA)ow)kU>XjU6`gR%08Pb~SA8zP` -ndmzv0xp0Cmh!`NP@Bx@SL$-<2OWi$t6Cg$-&dnEg;Dz-v)R#%t+<6#*8M2ah3ZPFYM7Co1Z4<@B3wY -1}YR}JvKDt_<9Vq5tb)-p<<)qh+jNiqoOB?$8s^Zr|7i;_c|H4+KZ3mim^@g!@nL76>ya&xdCWCkS70 -RPZ{&Jd=p6oBUo_^(^ULr>5QZZpI`fOQW#J=#_i8ak6s@OQRYO(<~A=G -nEb8c@R?$SWh6<QHpMy6;v1mdj0 -jb!HLdo?UGyv;HY`-z0hkR0WvW2zs*$F)zUy<~b3=p74GXWmS$}YHWlLCBik=mMGz&jT7-Bj1JpMY>% -T3j4Y_WtToCo)#e@iB{liP;R~n#Vl-W#O -I=Wp0TbsgwZL>wq*PB+VpT)_+lH4QeZXrEfbdO-N2zwqBzKQ8o($*!L?_t4E>n=&7gYiNQ&jc)HQ4u> -*~~Vr65tNVtUcPD5Za7$4N8Dq^m-eeXXp6*98pW%^@Fa4Z;17z`v_A3K(1?;%+P%ql_CSq+TH{Nuk9 -F+Gzogxq8@4%vnR7SHr%kgUBW=&_WC=lERL_D3tUa(^v4yLuC$C{@us0;UQtn7YjH)!>P?DckQJqrZS -n&q!CRCmOeJM^`;X#NJ60J%Q$Y+}U_^$}raOpVDgi1OK)4plzt5HO`)OB5>buX5z>H1V=YxhUKmi_hf -^i0ag5L^AmjWzHwGl4XRo^@kx1v?58FW4Xa-yTrC69!3raQYO1(mh4%MM~oz6TcceDs4USE9sz -ZLp&bZviMX@7A!3UPeHQ%$)MLt4|87O3+sb|ogPziw9g>zqDAjE_co@KzLss4}H{8Wp<0O3p23#zL-N -g3Gfp9){)&jiDN%1hrcr+Hx>E4mH_l$(&Y{?r%aeWRg5(KUA`Ga -llms{`4Yi^7UaxP -DvafN{62BdG1vt&dzXrA3lo|OY6hA9lfOr4{Y8w-2n~?(=K{O({C3sNei6WF;fSa~GTcR8kEa9edK7o=fS0O6ab5z-vIW&eym87dB&c -mtOaMmk|YP`}TP-Lghl4if>xv?(({;kz!AJCXZYkVU=z4OFz^2`IYnY(GFn5mDV>hKP<z=AN0Y -)T2Am@KTUI-OlcKfI$u%E|HFac_z$znB@BnZ+j#=Hc{m9AV>vuFw*yHUoFGv#W`7F3rIUsjt5TDx^b) -v0tO7&vJ`?AUR@S{%;E~f=e>}sJ>7D^oi6cY#6+kDue_DOs#YXQ*M^g<&kN%gFqIhQ06Iqv)Vh(%+gg -(*2dCv^MaHk(Tu*hFjQSsj9}_f^fwtPjPn|6VAPHCR3Y*2;uwG;Gek<~;k|3e$FO$%Zvg!07zzB{A}8 -CsEie?Ujt~JC9Y#5{=c6&FxcmU&nH%AmpDH}`HpJTraVb3jsV56o^TZn4rPdwL)7Rg>7u^3u8t;iFbjLnk4u>*n&Z$9_|DyiZ!?@Y*Qx$7UpFG$2vW -meEJK!8lDKJZ)^%bj|mOL!5)4fna}>x%}czJr*zWYEx?vCQZa(=#f8>-8E^F0iebO<;@3LA*AR;GQbA ->GdYtiIx{c1XL^s*&jJ85psXH7dXSe^*~;$V8d5%y?|-ASUMtWJ*wAvtwM7YdY3Tpsogs)varmR5FYz -|tm_v8%ppwM7u^HANj~w}BXXT{&1ik(c~d$}`7Ru<+E_+w1fiEvNp!mFXp#ieMRE~`9e`w0imd9Jktr -WMyE}c+VdZUImg{VD^RTu53~r?eEIqXF2+5CD-wbP}IlJrBr7xH-Sj@g>s(`=8^4E&}_s4R!jGi=>93 -oY{v&-S=X=;d|)#TcnBp2)cdNF@1nljtq(2u1ab-wfl35K9v1Uezi`U9=|3d^nnkk%hjj#S4zRJkha4 -wCmt3k~adqfv&rh)K2yY_piX%3#f;E?Q7EHK;1utm%n3T%Wzv#e2`VpZMr2TP3aP@wedkL2%g}2Wif# -^fX*!*^{pw@Y%wGO>6?-2S&aohq^XtM&oR{br6^pRe|{LJ*d+?1QyfrE?4b^)-C%^R|r;YfGjUAKE#br|1yy{QD`g4PyLYPmUdo=QW+R`AhawzT78y;EEl2M4yQ;#7I`Q`-xa>!E -Co^$VvN51jQegWBeoH~xHI=fxHb^s5XFZ4GqVLm};u#vWB6QI}uZ9tSQDGef7X%h`YS&O4CKiMkDo4m -uLL+>=&KZZ?Z_4W=7LZZR4k96Nd3qY49?4){ao-JTVuIAL6{cm)94upt8#bpJWf@3Iy=u -p!b2ReQL#sXERWg=#p7*jlJMG1BeGr;#DYl6S=J$Iu*r7U(nxoOj@lpRwO*i=3a`W1Z -)HhMK#RIgtVx`De?00k@yE!6NxnWC-Ufv7x>ZlA=+N*QmAwDXETt1~j2`UI8pVcm94lD*2F)D!8*?b$ -$Y15e~quY_Q4T>Ms#Bl=Ml%EMj@#2DD#bM&a(nMdr -nWz+=b{z8>i!#pkT=>Xb_D+x-QprDb$0+I1}j{~6*u>Pf3u*mn!xa+G0;mOgwKMBu}R2oD1yH6$Cp1^ -7L}N+NvtU{4FW0B%wD#%^Lxm)aQOWA73C3347s`!jo6y*)qJAuAZ<{B&N7UO*#jS+nr%~mt`cX+J`oQ(&z*)jjw&Z|F`&*sbVBrKgnTQ=N1 -@Z%9I6tOl8BS{7#3{W4yXu~_0_Xc9pg#uT=TV>biVr=aLXF{eBN*%8GcCm6g -eOZyl8p^s!#3$^tGpP{QYx4Umz6q4p4iAX9AR;doAiMBzMD(ZXUcua}WsH8GkUnUZk{g`?Q;qcl!H+2 -F?)YoF&Z55JV;PO&l=2xFC3G=T(rTn}Dq5o)uZc{2)~{rN)X6{Oh0rU`}8xvwidC`eJx*N$lK-QprH( -*X5*U8{J1l!?;iBvW%&p-O0CrscT60zU>c^Y;WoRU0v1+T`QO=Eztzg$19=1B&-q)n_PXbUGg!VmO1- -NCCqsR#dsGGL9)xPj(NynL~75^STA*y(|wda;nOK#6!5c#d3H|TcXi?W) -Q?F>Z%5oRCrGg#7rOdI@=%EcQGQxH6RKV&*B+ku@Q5m^&G1I@-< -|ASZ)i*XIRR$B3>tk4yRw<&oE!w)mToWbQMnq_WH{uw6!zs`)(vJ{1lBT*J-4QUEm*&{0foA}|PtA}n -{$wr((;XS&;!JjVS*&OAcq+k9hy*CdPLugOO$Nij&wtS-WO(X@(T%>F(SfIvwRr@SK;VbMjR)_?sXQm -~aIP*6jm!{OjafITz99^aoXf)ZG^a1fCg?Z+^f`e)sTgpcY*8vY@4|P&qEE7avD -eF5)(7BeaNOVJx&9=@cCUMHbquaWxu(gMwgGH!C|)RSoJng8+)|?v#je{9x7Eu3gC3kaUOH718|Z&Mw -igw5~2pIG|FqRc2psq8PL$=sCZZPkg7=5qDB|l)I^tf6`2kQrJHplVttDlJ+P`dcvu^BzWtCAYwLU_B -U2qN+V{rHE{Ch{w4Mv}oy3pt_CC@Y<3U95q|_s$3fi}LiA(W!gn4AR3shHfDDg6ZDaAl~yUV1x;kO7T ->?Q&N(=^pL8b>z<$9hPA!UJ)NkFOa{1()N(7ZUl&k;XR=H9wZ#62d`r_-TmQ<3RjT^y>BheE;$pAB1P -%OUO=D|A4xsW3)9G)f=xOreK(Nu9AnBe24t`8)qZS>^Oi~*W8;5tnAuh)SMzjZdttqH@gzx1fhLqi(d>_QV&r -Ap0)1+XtIMblhyTe+pP7;aGRaMF+R#{6#h>p%c-M*Jkuco -^$H7(_JusLjqk)yv0h&KYa59tgIY_L(b{|0xa_Y=Q9v@qF#`3Ee@K!9|CXd~n%A^$d`$8_Mi^xMqG -}a9Bm*6{-(+NQ%6WJJevO0 -+&xR&r1;DS2cV_*LiR|v`Yt84bUv#oR6Tj|cf2O_F;IBT}pAf|F`YEV*>_>%u@##+uLC)7Y>*nL_R=> -2B`g^P&xW7y_*fy`99UV~30p4F;T(@=KT+F?;b2nS8Zw7j;Fk>}}EXT7-FvhC#Md&BZSUGV~LhLX6x- -UkHww~R&AgkTPVyqT_*+QtG!7E&)oX(ioJwLC7bztM;8LwHOd~qm3aVi)Cm4<tHm-swRn>=+#osA8c}Q+lZfIkj364=Zy1#U^!;XXv7TDe#i+*{zQq -gqBxM4V2>eT~$wGc2nz+bK(^YJ&Jy)qW2&#^+As7;!tL9!BLUw`h3^w3;z}YJbaNQ-RVfw&%t0-S>0k -q17Av1{Nh`Y0@1t{=9fY(Qd}Tpoo%A3U+K&PR}kvFB-KhvoC}U|aDNO!&&P(JQn_TkY$%eKpSQ)WX3p -hTUU09ON&kayht=O=G5_^fJk~s)$3b-1$qmXVn!hV`1BpCF+y<_-WGix;mP=h7^&Oysx)C0eQ15_-%z -EG3D!!NU&S`8nmADW>n_Ec{G>WGeMDsjLL_7r*U8dCJv9E!AZ@UmW;vJTgAz`d}EphY9=w;tnn742B% -zJ5eTAvFSXZKh0-fG%60+bc`5Ra|3;#S6A9-w|#p!%iO1l**{h5hGVj)TU4Z4;M -S}5Vdg{+eZu=U9>_^=j{IgP)h>@6aWAK2mmD%m`(ry00062000000018V003}la4%nJZggdGZeeUMb# -!TLb1z?CX>MtBUtcb8c>@4YO9KQH0000803{TdPUCiy>L?2U0JbCm03HAU0B~t=FJEbHbY*gGVQepTb -ZKmJFJW+SWNC79E^vA6T5WIIN*4Z}UolHo0xDwKq_0tv6-&|j*eV+5o43$b{@6%Xn<|$vWDc8*Kdl9l>tR`%1Or(#S&5<FGHN(=4mRX`1iblqWMl32JdlaolrhVg+`L1Kh!Kkx0O-z% -NH({sINXGbz}+y@c*Dt5G-Wg@P>95$E{Gvk_hB#L8YXhaogJJq_)iaFj48gk5fD(wSP}#byc;t}ff)-Dd!QrXL1H{c@)^iyLlwhwNI579q{od^p|eM# -@Ks6mR~QUIIrxF8;hN!}z$1?bq$yV*JfmVMKz=Na7h)&eyXqa=_y25O$U)5Gd9#g^bn|U1in^fYbTRx -kl(Td+40lL)K8)3VIeX6_c0378eKF}yLS;-f&1)a`Z^10xe~Z?k52t;Ye#Q;&UnsD;zmV1^E38XbVJ3 -xqS79)RdLE60M!%U1>FdkhS(o|B(>6v>QUQu&S{MU4)@-CmS~~oDyVp6pa4)XftqZqxeSOyY)cU>%)! -bpU0`bomp`no49FmYwg$J?X6D%Ic0rSBHN@NXXr$JybRe|u?V9vf%Md3IssPA=$fvcK?8JjZjDoRY2G -Zd)RL`=sb7VJq9`XYWpmXBbuholT2IjjexNRF8CNfZd3C)jbE{-yed3 -By)YzZz_p;|w0K5q>jZGg!FNumVg!?M%^eBjPG#s0RRDpjRn{0#sn_=kO76xG)kI@PujghA&H#Yf43Z -jHI~o#cU=gXjAOm?15DocK@NGew0()boAb1?B5Ao87iH2+m0eK7HK^`@#T7-a!im`eOM|4|AgP;JK5~ -NG8Pa7V3qJ}mV5VoENtPmKA0ls&Qa>;m_S8}}N`_vh*jtN&#QM-g3b4;W(o5KF9l3{933RI1(0xS@S0 -DOQDI8ne2I67e`C#>T07|g5uP^R;?P^&CMR^@TGe$a)?axH$e6ruJ+_)x0=9=h!ov>x}v)#Z7mv1Ggj -(4{K;<5t^7l-F&sp=Iz>Tcjxkv6FWk99`4i8f>6c@JE{50vi7hYv-3J?zlm&E9K=v94G6R43433^Hc-XfO-Drtl_2A$4Cda=@KXoyvc@F3C6AY -Eae_v+=#=da(??t0F%r*((xXm(nXhIXRo+Do)XRD;w>trMi0tIVVnrbmfM@s+RmR$degdKA*PHH>A`3 -(yGYu$TeggIMAkCqkpbp&V@?K4o(p;uuXm2J;J4h*1lOfop6v<LYjs`Nw45BKV%C54VOF@YPRG#F31tz52C}T -xfw`=LrqfKZl10&7#`bvAt`Plo -`#|$mV>eS}M`%3Y{s`B#@e|wmimDtVipa3ijjOhzcx(9rxj+?th1*PzZ_~g;)#vh9H;{m68?NcIBsu% -35jvCn}qWil%*(F0<}k_{2sIf}QEf&YFI*0kyBX?>g^yYeCDF?$CWwu-UQ|$$IZkG>Em5ABVjnxE0)A -g*WRjZr4_Y{K#f(KgAePEWucecxlplvG48feXs%t7O<_D_LB%C;*173@mEQ -_e$x8uSczceuiR&*5Sz^;x=Vk`o0=Trqv*^es8V%o@#GKrykbcaT)vxSl%(7299Dr!C90J&c -@RB1z?SBQ%~|1u~s7(Lz11+i7$JVYWhIJ06P-HrZLna9|R&jKZ8MQMV75v_GJfr+4VHAeRX+x)h!+e7 -6nncpRliwEbnidA*yO2s(QR#X7#aJt*&Y-A4nM>5P?$nbd3>iF1MLKD#6Mo@WV->Wino_MO2>pZSkobtz{#>6;IxHFy(Viu|$mo0=D*pIX2j --2!fSlI`+W3M&YWi;(cIml=j)d!Gbkq9XC7v)$q`A-=#Wr@rufY)&N4{dCk+(58Pd@p|UlSU14xG%sdj_5{$V`0>>Z63mJTyWe=^*^}RE%)~A3mOcaoe{yH|O0??rrb2aXpD4rD0_CEmNc@N5Y9^z7;3a~qU&82Il8fkTlyI&^v;-V0 -;Xa19Iou4Z?81t%Nn2Jk7i!C{IK`|M0jLuhh7?c4Mimjd-Ks(M(4WBwqZ8IQ&TX_yBjOXnM-{l%7Okc -_m$=?4HBe*i#q5Ts4P{*mL2Kk+IqEr_}g7dU>q$jv+J%m26GRI6j42k`kS7)c}@ -WqQVm@W&$jcIAX0AhNd_3P5;To9-`+&v4ata38$1#F%%8<2}0h7wbOq~Wdx0vsY}6#c^nBT;2HY#HF= -5`ZtJ?$HkkN^}Ddu@EDRWY(s9<(F3ruEWji$M*J_CnCc-B+z|OtRi -&FFK(hX{^Jp@U9*DH0{o_YtfXOUn;0NOIDW?&_Cd?gD!($%E?d)!c>b -WIukl;~2qXxY$QjDB_p!*BZ*9RWIFi+nUiRY?ys>j=Ph7lZrMZQTm`+#NF1xIw=P!>=(_nOlOkgO{DB -D0=y>_QV;xGT&?m-6zZHo(K`W-_oM>2#KlbTN9D7Q%3Vq{{$ms*viB0oc+)j}PY3bIhdB$Q-`S|%!AN -omE^QWI=7P~|n)1u&r`q7N&V%UZAOZ2tvNO9KQH0000803{TdPKS?;>_H3w0B0cp0384T0B~t=FJEbH -bY*gGVQepTbZKmJFJo_QaA9;VaCzNX>u=n+5&yn_1tBAl1gvGpNiL}i7f2K5>Nd9yVh6sf&rrA$wac? -s6iCVI?c@IT&J0OCc5Pp-=%?-nTjFD0ocYZR6$HUW(-sA5mb_uPEDE6wla+fERl`1Qnx(4P9ZX178OE -!e)g^BVrB{<&nnufc+u&3Z1i_@xYQ@sDXq#4xG-YyCE8Q?YH>zwKk=pkuFu0RQa58Z#GL?(W__tDJyc -GT0Vj-)Ae^uQwvjo;<_qH(;D?lylB)t<=u5@DLy_cd9_f6VtYB5|Ut48RGmkE+G2}}bkzvG9WKK+!w` -^)LOzonnQ%w}h^??K?=f#Kr`gC9{HQ@CvFQcT$;u%1jBy|D}3q^xe|~ -jAGkhfqSrTR31)j5#;5(_2{}4lRaN43m|4t2rm%JB1X>tS7qbmyy!aJW%VD(2Di_)|L@@X2Ai-9vIF6 -h8T3&s9)!lD;b_A<7oH&hhJUFuf!E@+Pxlpv?}r%Y>8T#y>o>P$>TX~fteSM_Aq?{+rgVXf+9cl-(ed -)rv35L+&|$8seMt5kzJ0d_d0()3CGGgXPnLzdWGEG>1r<0;#|a<-lG?ADarEVTfr%R~y$1n>mJgYBtc -V8cCj)&V7Zk-Q|VRAnyobcZBZ1+de&ePEW}3Z|{;EYKQ&vW%FctGLBi0*zma6~Z%U$ke)Gp6n8Mmf&E -xu~|ybz{sZMh|*JLltufs08>>A0|X=iTIB{=dWpQeo-%Pmst|o$Hn1Y)qIk??X;DQ&>$pE|&DZIC(+I ->2oJ!RE7m+pB@`k8O%s!e|3qg~n;g~J(5fCqqdkEP*#p}Zxc5=dwdR|r)m%)zMg5L>Hk!4G+dDdX6+e -iI@2;>IEs&*x`%ovF`A~F(M={%apB<n3~Yw1D(iu{H>jUzDt(Hu(AH`)Bt7d&V-_n2k4f8WIJY3 -KK0BcrZE@7%)*se8&f!tsVY6;lOYjc1`?y-Q2T)_`%a2qiU4%hQ~ezZ2U)7F}D9FoBBxbAoy$z!x`2M -)dIYc7uL&F+te)_wSt3f=e1TDW)M4?*gyxz=f=PxfddPMOfj^HLp=F`kDu -?6HZm*=7w-@iu&ZcPd^-^X_(9(2=^5ycw{u_(E(9&nRM8T6q(U>UScvHzyC*-Wi5n_+;vBkjCrADM&g3NFj(mJ9I8Tg~mlGa7gwcr}HojV7CSb)OrnHk{2&|>{){CgCcIv+iG1S(> -0{~!1*gaL+#%Q!Ve{xDP^Id1y%lLW>6?6&J}9|HnLThTP10|4~kKBfE%l&axDA`QOavKpjQ%(PJSfkL -6_In71JaYi;UO7T(3TMTBht=kR$EH&gWG%|3r{*)8U6tcp>QIwinS&#B^BWlPf*6%2eYRu9$65Y{4*s -V1lYmSC8Y9)XGbVCkqDVJM@;Ks36>k^$@wP+FuJ`Tkfp#la0FRc{`B9t+5E=$>Ld|43ct_Tn)HUw}9y -d}Z5uOlG-p+BS?oTk=xP?MtFfmXSOQ#T8Pq!< -2^5zrPy>1)JiI9ZHtrT7Mgj2AD_jPZV0PQv3P%vIYN4QhqGLx2FdVg&E4xGojWt -DKsLdQ8oI$bpnLa-2vnyd+T>OeCYbP4}cRrc6tCjn-C>bY{ZzYiWPZK_i?EhF$cgm~Nx-Sx<0rf^Q>O -!1JKR2>fc=?EXTYeUe0G=V(ORXF(HRfo6*;uvZiOmKNatp=DrI$RdNG$xQ=8+1g9TtnJ5MsT -I;b}_KY;K)sN1qU_`{1=d$}}OYska&s~3l_4-a2h`k&B(IpH!Z+A1S}L$FY*8t=tgGNiu~gS5+F{_WY -1U6JrtYB8o3K^<0!JIGFeB9Jf^it#nyIBkwKmw;^S{KBSub_FSll*U8kUOOs-=+q#QYr*qmWGgtmB{u -{*3eGM(PiN;J&Od%UzX}J(S0FGblR%|g(NRqC?L7Vb{(ScFC-yJ<=6CidikBRCe0FyMKGH-*9z#Edqd -f+hBrBB>IA;qRbaOmuXB}smfxlk8{Y>{|tdUU40oW5pv%9vegyuFASv((<+>k)n`am%+=+KAJFF+1dE -|CT;H}tulF*V}-JtlCrvbk(csp+d%_UmZ#*cxOEVzF(GKFE8vblCo^sqn73#%xO|cw~OY=ju+3LvOfy -jOOGF&&ZK*wC89N--3eF$kc}nwxB5ch*Ug;X0PniS0(-$YU{db#WSc)1eDwELh1C!%hTD#+4(H -J`10Yy*^j3elMV$Rz9@@^mjza&2Z;2h_7ZrGNY&H&FL+(sIlN3DyU`nh!H{kg-#Dxj_cgdVb_`DWS0Q#ZQJT)knt)+#I8T;K`gZ7A2+l#yH>@aF9Te4iXSQF?J8p@tOCU%vkC&4`~BQ# -cQ%SlO1`qY6#Zy^BE?jly3K!kFarL`Jg*2SdCW@bQn=&yR10{^iHPfP26!yX6bfb@BG<)#B)iZ){Xjj -w(xAcS7~cv1+gw;jk7R9!l~(dz%Gu4ibcJo5;h2S!@RmkRThG5h`c26~HwY_8u1tY$y+*%&dSEe%nZ} -eI>S0y*YY){1w6hSQL`(Z7IotFI%?7z;~f~nf){z{4gnAiVt;Gkexudcy@y#TLwh@Fx|TkypmK^`Sow?g2>(2v5- -1%#5L{E{BMi$U@A99tx|2C|8oZ>hJ7_d}&E&k_Gr&xKWess)FA9$y%&>_~o}u3t?jpxF>9Op6dhGV$> -gR)q#HI%O6VEQ;bA$gap~2-R4jz5$#-n(rP;-n=EFlZ@URC=^4K&^Pz7Y;;@tw)&p}N&x|KSI)Ybf>b -+NinduQ7wZwXlUB!d!!^S#&>Tn-~jY4V_@sV6%nru$DP!bsziAo-Ku3ESqRExEQX6_q(>4WL{L~^T~r -kIOC86&J4)|dd52+ImWg9|D$P8FR8MoOuq*ZxcEEPd*ec9bPv`sv>Ca`s-}EwlK3n0*tAWFJF^*nLbG -#8TVWQVFFv~!68^&FGJLoJC&?1h3{;wWVUo+U^LJKL2!!WHFJ8X-?)9+Y@xSqiyVX0Te}JdE25~M%U! -q3J7NijX>*16g#^1itjb}aHl4V9`bx1d=qoL=!Lr0@;E0|C-{S8MvYA~=JAQR?MJZdy{Ew?MCZy|=io -&Mt6O1$$tP) -O9KQH0000803{TdP8gjcxV!`a00;~K03rYY0B~t=FJEbHbY*gGVQepTbZKmJFJxtKa%E#-bZKvHE^v9 -BR%>e;M-=_8UvW`BR3hpc`q5MYbXjNDEC0UCVc1Yn4v=_F -w>S3>H`iB>x1a9vkAJ+&G5YQHFIv)cuyRRLY%R2Kj_y_S_9*k`q|mJWohQ2uHsU~Ttb#;B=Wb*6t(bt -mGqogo#mwX-Gs50i^tp@WuRp(`4=E6I3pFYmO-)LDjJYo@?Ki5}&|Yax8&Q{#2Ybk@QIc{1GbpvrBS&jvV-olKTFhlvSXm|ghd4Kzo@V>7bf@zd0 -NIO(cPh9+1b#sM|E>HS3Ch{0YUiOw-lFG;-K^EOOX%60kincE?etCRSSDB+1gB5o2GR8KlS}=K4wL_F6k{kNxa#jJ}*7BH6y2#s*i!d#M|wC@z+s4au*{d1k#1b&(D$f5?)0iRn?JNBP -wU2%hGNy{s7y^}>~A+q-GMIbaG3pJ1ZKT#vMpvL0xXwdQ(3zPITD641qLI&T*@snD^^yxJXW_>>1u9Z -(lFA5Df^@dri$++B9)2Yk7e230@e703i#5~>~iw`Pa-qUvK?co53rptROl$LR>HpZZI&Hv-rSs(KC&d -J&c9QO+7J -}9M2-1l9AYm`!X?HONeJDPQ!}&PHClg|E#RM88j+K?LWqjDOfC%o8GC0h;u -*Q;!d0MBsiTpkH4-U{uc7>L8&UFbka4#h)KTu~c9-wo2vusz|bAv~Njkd=hekV6_-}(8^_s_HCnL7-Z -YVoY)PU__^^T~fuO9KQH0000803{TdPK4(bCouv50KxtfEww4oxXkR6D6`e`AU`!TAwkHf|HBmoDSV-#z@K9F@=?DKf#;$H}?qLdXb0us=N3@wfK3rc8AgEn2ybSo@*!lUz9 -yq_~CDN;T2Msgq=N2D0)yIuTduTlBCEhcRNBtRzTYTW!j0wQ0w+ttZRdM4~ob4Ri)~_UCY`&b`ekYgH -yEz$WFv|6|k_yAj1eAm9jwjRP*?tM*;qzOReB}XWjIFT%Q9GTI3Q`iTg%iB()FEfsWa2RV);7nVot&J -^HkiMHfaVmeGrpsRHnc^>fYUT<#$fK$4%X=C0o(=w%T^6*;#>=*GC^?&Z-+5@L01_pknXMza7dg*4ob -cv=nBh_gDzpUa~oZbNlZIhlswbhL6-w$>9yRq_4q}D=_`z^6UNy$={6zV;O~H623>8+28=Bv@gxKrtm{C2CQQqqN4@LUEwo{W-DYCRdBP0kaTP9Y0U0 -}$mJJ~TVBC``TyJ@`)@oek683(0RRBr0{{Ra0001RX>c!JX>N37a&BR4FLiWjY;!MWX>4V4d2@7SZ7y( -mZBtEe+b|5h`&V%7p$U*0I~T|<#fA+7)8q+^I>KOwdS|^Ld*kXT)`hyEqh>(KKcMUqU>9v$RaJ`}G(9%|<{*t8d6l;7rwVl4gG>WH!72!^;q -eRXiCj7P(ZPt&l7A5B%^QM&(M%vTGlKL8*mc;5)kCcZ)M?NL7?>IiVUtpPVH@$mIrtlcfC(WUQ4dwNR -!~uytF^3L#8aIiwgXS8axfyiBBi_2?obQe74>N`N@NeeIbxNA1LK>e7+?#($YYL;y>$qwZ}_x5XIx0M -Rx!GE_-=!QYm-jv9-edKnw_b%(~+&1y>jm-Xd*Luff$hM8{kY|#IM${-NNTv>Y4s^z{VPw&n{jb1@c!Q}LS?4}RM8WT_j_Q;swA$=jlmSD8fQxtgo4xKZHdVv<6min@}Wwg}X>q(p4S|I -Sf!9n2V^(!;+$BgCvz8M#Akx&jVRn@qgBvjOrx4_hM)1*Pn@;2%J&+g^tMduQI8ja0-!&^8b^Kj^hdU -M39cCY0p*UN05AI@pG^CSKbP)h>@6aWAK2mmD%m`+u-GhA*3007bq000~S003}la4%nJZggdGZeeUMb -#!TLb1!FXX<}n8aCxm(ZExE)5dQ98aZpxR!fds1SFFvJ4qKXa#j>nOTNFiC2()~b*hr*CQg!`hzkLrW -$!~En44V-+7IpXB@wwNf(KTh+vURQDEF)Pr+Bgz(tE<*=);*6#{dw-z4cp<#@bGITjFPIbqtVg>HnK@ -GsT><6sj%w;S*5z5q@u+)PSGR>$a(QG#V{OrUjnUFSgtrZj8RN7H%y)!{HjYhqJj- -T}6+0|9-g%cWBsMeI@+tQ-a4XgP2&DB*hA)yu|EV+{V7d=odR3i!|E7o>G(TgQ5WyMO=uy4sLPrQ-}n -kM+o3n~Fu1&JlR#j6dPRMZ$<7*R(ly)GfK5^Gv9oKjS=3pkZjw`^#k!_!KeTXGtP3*-jr*7azsQsyNE -j&y50ZqC+vt0H%8xN0CL4f6f#%=>N|ku#m&r71`{v+e@rKZR$Tr93I!Pkxq?48(!FIMljFGO!JcA}+dw%x~f3*m5=#eEQcyY=6C-?|9ID7}=fq!LDt?FMyt-y^|ud(c -VkCdFqDrn3j9-=wFfQIaC|Ohq|;_~`NDj;0!kWH_z8C$Wj*A6|YR0`~O%gXxnGUnhU0;mBWVIEp5_DV -6M|alOseVOpjBM;u*#ZaDWr+VA@-=}MY~!azYQ2d2FBG#H#j8?^mTM1^)|bk9a~kM^?erd3YX{I)Zm9 -CnSLMP+%nHe%#kT&IQ6Z4*bqc9bM^etwX@KJWIN+1K;ldcCLT2YKF4%%k-c|Em%?UL6N*=;&bHg4`X) -h}$lNAA5+y*+263AIm@Vp{5cJnO~;^iZ9W&5(cwv=-8e!Oisp;LS?YB5);19{*z6`q;%;E1qoHJgAPU -!j1CsUgg+adK!fP7uh5HKsnsm~mZmw4`(0-c{tolcNB53ERK -I_yS2fY7L_7*Z&584=r6-G_J{r?e#Uc&edGDni=QqC^*@`Qo+jZ(Pz`$t{#peuT&pk$CY%0;R#>x8w( -D>Gy27PsF~Nlsp5O!KJINK+tAZ7GV`aYf%PBohPt#dw*o>I^DdNpquKXh7>j^uPv=y4M-(%>NUxopBw -%ek+1#sYO5?Z<6hy1kbOb_c2(EKuG(@=0*a*wU0VVQkD4A_1#jiT3P9Xdbq%^*)X5U|&;y19C@Stpza ->PJcFx-q0T?J?=s4>$w{!&KqAaqB_4t3VW!u$`?+>)W=WrSQA_;8nMw9IR-^D}5;Ged#Y1bsV9SM^UH -6DDWc^0SRJzh{@=0P)h>@6aWAK2mmD%m`=L!_;@%40074g0012T003}la4%nJZggdGZeeUMb#!TLb1! -INb7*CAE^v9hS8Z?GHW2=AVzEv(4KHE&1H>ZmA1kMZRh1P;@omy+`BpMHi -uDsGN$(73*(N>COnr*6L>z=;g_a&-nuDrc^f0dq`ou2%WUVNO*&S!6slhwfXlwq^fzJy>8J+D>fC-A3 -o`1?0i2)jRjH-VYCkm!U04MQBl8_O5Q_wt^g`~B5RK7(*@Ic?SOo!Vi|*_0UM6H+_*jkEL)!qu0e=ow -sYB$AhzJVz9A3E_lZ8!#*I_%S#5#V2sG5ye{KmU6J$$U@#q%LmY9UgBvKMy~?(WPH~Bf*}GsQ;aamD% -@U(Y`YV738=0_Cohy%exF3YAAfpxaejH4o*jQUgCiKZjd=0urx633iCj7Y)7V1bFc>$v!=*Mm1ijd^> -cc}uZ#e2e6ib-Tkzu|$pQEgk%33r=?EA3UOCBqN -!Op!^pE8ECy>j-)dVl5Qr4GY^AW>*}pjafpInY9l7yOb-e460B*O=BnZYElb7njr(@``~#-kuIkYQ(# -{^Z$og?>gHw)hra?{XpcAY>6wYwyA#_Gl}Oo{hXiAsA2I4Q -eDqlC+71Blz-_5~pSjqm`g?a@2cK%LUVz)&$n&qvj>N3{leL^7Ufxr5k4BgaiZoq48c&5+aQ}>_di95 -fuwf2~_K(RBA*mg%?TGzFr80R&)y}Sz3^N{MI?zC1`DsT;k=vI2g;oQgQ{TqOIz2kIatw^mx1PZ#2TM -TUj@#r%u$Qkh{43Fz#Ipy#eA+6r`cj!zI$o%kg6+mJNT@9^%o*f%hhk-C0JJdKt~XMCNINYVU%q&0P87(&Ew7RKl?}0(z)p^E -X2^pOj5014bW))1&y}*0#-(7xx#vj-hi(@}qs&)Ahb#4T)w#oso%0E!xH}hz;jWpcaRxCv9zs2JP7;F -XZ1Rmx|0I16Q|dL(Yi6~o$GiDFcyB%j8s64DFFu3!#Hpz)ti&y~ci9$D1h>X9(b9A%-~j7@m!VJ}B=8 -pnGK%j=l6mR(JPAyAZd6)Bm@?~s6`VneKvLlkUxnU@y$hTr_l2**Nh+gnx`0YQ-`>l%v3@<_|-kpF$} --4~DK$@bb}r~^b4@7=ri{dHQ^d_$6C)i!NSlZ3EM#p{NoOTmk_p^5rFndtW!FB|&M6l`hU(7Gu7@)n!`5ZNmx?XM9 -toje-5NxOjj0>3TnSPOF+`X~TGF_Pn2eyqdo|y;)pdBpv4yzT+!?Pjw}EI3I;x0jF?q}f$;M%Z1+YdUex8Oo7Ze -HOeWad_{7$TG5)8QnkDmBuh(x+faq1Su-&utjvm59(uhK4c$me38)zmIkd-ypSk=1;VN>> -vE+nY@DeFS(Ilct8w&3%OL4)ovxmj0D4}G5ayf;QgRUK0~27BCHc5(*1* -6q$_&Lx(mZEKT+6 -wJwzPR|;={c!WU={g=)%nN8$GJhYZkkHWj*gb?oro(*cFgO$BPK*k-~8>bZ;qb>`1YH>y!lpH@(MjyV -aLCmu1LZ}qDV}T#6((N2)2_*-A+MnS=4ROGQXx&>L=$J=mw1uHRj00Nck;U(t>YeqbKuMQ;2#c=?)@H -ki}(Z%*68tz_AQ^sE|-T#Y%=#qQ9Z|qDbzhg`gebkEz9+yM!GL7VT3CL1G>J=@@{g{1iS -;AI+7vS~N=+pe%rs%yZcdH|>EhHBKSXcBOpvX~mK6oazZJ?_U^{_qQ9;6JI2eXT3yEeWHNTU{5NZPz0 -pJmYZdii=z%$EXL(|VHdvf2a69QyP2%F~dZ^vk%9B9R$I4PW^J(ylAOcN=EJK7}9VyLB{B@U};)6`*y -X-WKW?C6?>rl*tKQN8fjoPNHF -7deh4HPwI@hGB-rxYvxmHc86xDG%hq*U}eDvIVLy^!EXS)T%=@GHKXK3#?*yWSwH%RIJ2=e8P-U9yVl~LXFKZObjp;oPPl(zfckw -(%oicMq>E$X5~INV@b@ft}d_qadFeBniENvXus|wc0=o;SCtrOWkM?=eWL9$5=HgLwf-YTBx@I& -9?wNwf$;*-Rue)U(K)QH_7Swxu!^?(D-fodxyK&uQb1wo#T&c3Sv`W?=29dOTPCqI4oNRX$3npf)t@s -D56mLTN6Uo={@awcx@Wyd&lCt%k#^tYf#YbC}u(3R8pD?TdY9ko_x -XL!VcMH@pJqj8NS7mQC`#fm14Y}oDH&JHv>W5-tGw$RP -!4o@9*A8ptTw#J<)V|>;kBxAI>^3_c(bxB?OeXnBC*yG?zI$s7!#(8a*br_HKVA93|+}5a0?Vo95F3c -I=@?`lBU_AkaTsdoPag1G@vSyorJ>^BqfF~cR?iW$XSZjLKu0)glS?x(3UFIpnY9jF9>A#%@Y)H5!3R -X)x5;S9tJn_s}Dd4wC4e$2%dX1^mx)zloY!&t^VC}$BKTsyxNF6P`^Je`F8eVj?$`Y_5+#9q>O=nYN* -woq#|k`SR?J%I^^#s3d0z-BVU~j&yg2s2~2$%c{G5VG);M#|EBy-n5v1Pd8jhqOafp&+*ow@R -gJwT?`lX;p~v>axh?)E5zpj4&JI)>*m-RMBROa(=J_$C?^#m^xjcFwh2eMle9(QVb`Tc|#I#P*9G>Ej -OBk~UQ%>Hy$GG=+1IqiM>}O{gMsuJ(eWz>zI4i{0aAJD3JkIHJO2qO8~>5ZlWtLObh~B@ZB|5-pnWp4Lk)ShMR2; -3-snw3ZV-KtQ`O=sRAn*d17=acPhfb|}4`UEcr79jdZay?;%AX`y3KgP*RKnBoN#iW{#|Q3UsC(b9p) -p$?;%zOhl1hnOy8*=2xK*NhN&EJv7z-Z*ug -}j(1m!o5j1+bF<0qW-QR+;_On(MZQbQcC)0|VVKdZ7Kw3BH(v%d=S^VDO_wE5E@W~>NdgGvf)_N<$Gv -RJf(r3cUO*I(Jg`&L=wuUkCns~#T;w-27p(_*KTH#2NkCuEsIW>z6PCBXT5-cY9rxyL6v{!1V1Tgm4` -4Cz>)I8lA7yZq)BB^cEsDU8(1~hbmQa)~3#>7B))1EzXZV6>lOalkwj7o8Xz?0nQOF>e11@z-pzi3=S -=h2pYew~DiFq$c9$BXy`C(4KRhBX}hWJ@kqedY5(Cam?@y0h-m)#Jkpf<5L6uR~naL9kUzE5TEU`7J^ -(9DjGy0$8q_MjkhkD&0Kg8C}|*Eve%b~-uo}@d5$>D -r#~+}AS5D7%8yNeA1&{SBech@8Q+u%?Hj{xEYJ#j$F*$Uk27yL^VFSZ&92w-wRulOH1T4Z-5D)++dyl -@j8nx14l?ys`Cfl?wu@fFaNT@J+O4j{4BbbhHNHYdB!>LdY@@xNV0C}mYK_p4TU9mqxq`f)XO9)n206 -F>zPmBwXlVhayJW?K$mnXyjakPGQHE)LD9_6d)?;|XDM(SfobpkSc5bEJ~B!b5Usju(wF2=d{`acjN0 -sFUYGuiMzOPD5Bj74bzhnEFw`m#P6uggdY>x0O7Yog_6pJnLE4z}4MZwzI_trPlhO}O`=?E=NE&)TP+ -h~Y(J)P&m5X7;#648rZBa(Kuct1yAUiZkRrqSMNWK7$IoRcs3knUfKzz%F9D& -R=x79y=*lwoYwM{%_$Rdd=V)tB+WBKF-PQ_0Zr5=U20jYhhfS_xTzg`&CzUW^CiMbCdr9P)h>@6aWAK2mmD%m`;^ti* -HFL006Rn000{R003}la4%nJZggdGZeeUMb#!TLb1!Xab7L-WdF?%GciT3W-~B7F_GU>XG99NglU+~MC -QY1<&!%yX?M$Y1vkXN-62}y&@?lx+_P_6a03bk8vOAAG`(b_BSSEpsi;Iio6pU3RTi!aoX)fx`N>&T_40A7X@$BjG^TVgl509UZ6-`qF# -6RoB2C13O^P-eUPwI``nAU_u(8Dfh5zthFzlAuU?k5UfzZ=k6_jv#Pv%&YlvbdFoI%&RY>M<-uChx+Z -nxY;C%AJCy%z*6->7ufdNCbWy(%<)Y54fKK(&RL%1x7uwmn;R2FY_o1?94HRsC$bE0)3R=&Oy;*hofLNADvEESxva*xHOdg5jYZok%!_cI71J -mi6SQ@Sof4Tz__d768|Z_4I^G)%;emj&It`oVA4W2|7KIm>g*hs>as2Y&e{2%%qlt9* -t9^1{ei4LE>1jY{5hwE26~FCW(9;3XE2p$il5%56)^30Q5`SF;zJJ2#^PnZ!AC0u23 -1)?)bb6L`xQHz7kuZ~2}XuXCJ%XHcxN}XtQVQL_stK_EaJd(-wR;Hg6Yr -CaLPeF{TW97%MXK%?tbPr`gf*&!z<_GG6EPsoW1V>0ZxHt9ppxPS1&IH$D1pla|0aU_Lm+z(2913d?G -rteIhy|JrT}KM|(pY1mJssT{wGtU?Q+wRT2fri6%o{{1nmSLAXyQ9R&AS$%-=qLRyreGV=t9oHldcL* -UA?)JD>6l#=j3t}(bAi1R$#2#_}7{ia@k5%DxXcF)pc-X-wVO -ON%JraRvX+Rhp{*6U?xGP=3@U*B@Rgq?u$og(t|xGO_$UF -LMVX}(ApRfR;v;!#xw)zEG*Ak@e2K=UMCMpS9_kv7uzp9HBO#2Q^yl6A|;_1_;VE -#v-_GamwOeVj7`gBAq!Ft<=1s^N3J(P%8HF*=kRJ-d#rroqCGMC-hR|>WRbN+Hvi)Or>-c1+;H(Y=Ln -aNUbgMnB6tjXiCK#o9*GX@OQ4Tvle!9w3L~Y}j9mn-Xl08oDBMfzbEVHh}$tt}Ab5_!y!YDE^K7+2ArfyEuLMy5+$G92@ -woMy5hoN8~8j;vrrD1fMCcfqc9;yE+|NP{8abf)`~9Iy}k*t`kv<;I1T4hu*KiW;RAE{$N8(^0fP5uf -O|jiDFxwgHn?0@N42Vq>U4V#%#l&$)v_cbJdU&lxfgdGE5P{ ->4>ECC7Nga>$a_2C+W2Fm#ez<_}1U|Fty}*5|Nh9yTb*peTKn&B|qE?Qk#c`{DT!_#KCgTgkb>H&#f{@`8QK@Tr$L~?bYy;;mPL-M=zH(U7z7*OGJN^!iu?)Tc_4t$_ -3}rK^@?FHZQpG?uquJ{1JnM#Xv!9X{|L4_5&%JgL4g|^m&eej!=Y)w>q;Xq)QodseZo0 -SnER?=gqjP=v#dXjb4CfT1E*8!)r}3T=YASY}^qAbJsqrzDcAa`P0hjU@=7}S}lStK2N^B1-0z1#TG5!7pnlwNn+eEQ@p!3B*978;; -|*yF~=@kF!&bF!>RZL`&xqI<G*( -%U&^;mFl?$LKlxzUFe9Vl0R%qy}ahb%pQ`nwQ0~w4tzEUsLu_{_3U_n&Pv{fANEW)rsya0R8So?9YT! -M$TD&;J_gXK3t5wwt5|1ixgn}!?X{;z5bcMr2;m(jV`pq)XRUFxUKKO! -qyqQ?(PYS(oJ}fmEFp)+AQ&0s+wSj-7zQ2Jg48URKOx)T7woy>q^8?UgT+v$8+duADYpVvB_KzN*V`y -4nVbw+w)6nG1y*pQ11D)TCIWz%37~}V25WreL0|Udx*N4A5`NXcPd5h2W>yyLZIgt -T_j8R>55a$;eEORc`JA(pttXxw<0~j$CILcTtTuLX2OK|c;0!9I-IV&7@wgd_j^NWP=57_C#ZY8Rp1u2!ftJI_wBtEJdW8UoJ -S`O3rM3?eIzAr8zl}MGSrFNSEq%*EB$=Z_Qz-V3=|BQwR7;Cp$|3SF{L4udB-yQO2_+HEbTg$#?IvSV -G^}JWkc{aMX+nnIKH(nhyJAJj@NH=v{{05`fCL@cvA53qcEZNOOn_lRLtOl;OI$mV;6YU8(1+bk+jyi -z)rqm75mi*0^sQbN6}q-4J5tntnXRKuWd_>15*_iah~>;O)HyXgXO|p#h(aC{X8vguaYzB^CF+cdacmf+{=~N5K&Z>51WOmE|dFb8F{b -5hKw?#mD(ZSz8lNxx8q=wOyac8Y9$PiFn3N6GOn_SN4A*J*T`2N&wOj8Q9Z%+a5C&CM$;~{qR95b^M6 -VT9y4@`GO1l)v?#F}~K)5Z+(l#1-Vq2oaJr@Bd36DNu6z8+~FXH6o_GFA^9$+TzmXh!$5nQ!;?DOZA=$}6$^AT(M>W~NQ9XgWK!qduA^|aek^?Je&pDh=*v~3dN`7oteW|K0W@UP}owe$6^O-mc4c7$?`giq{z -LRB0-pzteeAM`lHV8i{+8Q(Ot9Q0r=g{VTOj-jr9Y7I>a;#(l(_>&hoY9$d=Jfm;1*&5K|dPc -o+|x%uO8SY*I=_U2awzbeNIO7+r#`)lBV7a$Ck4hWIeZGi?$s%!$mDJ|IdyzCVt9FR$SLFxgX{wNLIK -&x>TiQ$q1WQ9JNhiyv!b}cj%omH5Gqme2IzfoOfKxf7X*&{HoRamg&mS46e_ClnuO%r5JY3N()0oW>} -N(Fk`y-!0up~I!F)}?vjNZWuT53!J;SL-WyOb*vjTiS*UrO9RhUNo$wZkR;3h(|p#*=wBNYgEfTtvR( -lL5lE3$Wf`H)@;3FB9|5PDEr|6@S1ZHKc9#(kI?E1GnxVyVipo`;NQzCF>in@6tUyxVTxZZ@lzwp689 -l;wc9uFQQRryCOeR&sI+PUBD$!>}x$iUYgaMtK08z%(^JI$!_cHs(gkSjTUa#aWL6z($?}Q@M7S{=)T>_PMZd6@$U$GNl`@ -3GW{LSG?OB*!0BtDKdwu&Vzs2}59Zg{92~AmO4k90%HLwyvm}eUkS)`7{ojis*K{tQnKTaS3A1LhFO9 -$4;U`yl~*rBz|w};%f0Oi_*`tz@}OtBP#uNEsXIxCErdtxQISaVafRUl=5`mvT9kDmfY;Zv^N3s^fJFvd>qU -=4H{W#+WYv_D%*i1)VVMQ&yySa<`Ai(OHVO?`GiOfMUUGhs4M{tpEsFOGX3*(^}`JT4RfN5#_jLn+iE -ZCtX^TJ1kZeV#me==dCA2T)Aq+${TN0A-=#%i7RBQfm37N{&r9omwBQuG6p}St2f9}1cRcW#TuUd;dj -hPt!JXn+9tgdKj<0Yfat^eBeKdgGn(E=8q9e_QG@OAGq-m|Dupv9)HKi&aT?4pQLgtCZBWLjud}E}|3 -r=y!1eK`K6FQQSo={wT>@$P*mAz6Wt$mk`foI4PsYJ|LWsL;BNilXPEz-Z9!qS3gWD&q~Gs{Ka{JjxEs? -s@nT+xY2)Inuf*{m2K!KIICaE*<;XgGV2c@Byv?@c5>PI8c|>CGh(zAkgYE1=bz1o4#p$;`KIJ{GGS8 -nD$H&Lp$$NnL)vPcWYZ9;;WxDdvL1+gkU(4kY3r$ofO5&3Ev_$7tC?@9}xx{~3Jb(UN9ClT|_-~$;@s>)yIc`a<2*wLArYwPQ5Gv13`DUm6p0xw~k8mn%zWU?-hr(5?Lb;<>+lJ -ks&FqkM7UR6oD}#dTfwtcv85L~_xOaQ!n5Hc!0|Ki(kqn+fUNwC;8mk -8(<=Uu!F=gVS=8>K9p71cCRTA`94{y~qd$|7lENpXTL5d3N-<8+qN!9S=FunSbm)UdI$YY*E$-Wf+9G -*Yl>)g=w$^+y#SG~Y&9n$R7G?dNyaPHlOgG}LgPVZ`-yy$2Z8TbYCmSzKhb#$**RQvE^o4b%UW8+D=7 -y=yx9|F!+6t}Xsp0;B8FS-1G?pVr+_DX|Z6IDV_MvE83I5Fb`aRI4gK=@KAB%N5yDvnC^L5wr1v>M_M -5WnzmVNPnk5Lq2a1h2JQ`4m2Nd@u=;d%maLuR-)hUmi&WUl7qj^2kG3o{aZZ{Bzo8XFoj2a6NBSjVpc -+jmRsf8d!>By5T9DF`{!U3knO+6A~c`&y8ccO1g@*2ksHdeHFqVnMeW;2;iXxRd;y-r5mwmf=FV@oMB -Ta;y`*~`7fY*9eQeRO%mgbKJ2#()o9jkJdpi!r*t(Uk8RTe@_h -+R`ofiu;AIC -Rpd-d4v^Bi3~FkZ=*M$NfUJCbwZGv+dIVN}j5AOGo!JIebSx`Ir<-|%n>ipiv6U8l+w4CYIE`U0y}K@ -Z0t@%8PJGoKM3eWK-7pJIB%%fu4w232}jEsC`&My0al+Lk6OGF~SW6y!VALy?D!H68{|JX#@dK;Mvs9 -2PfL>uM`>bCC#kn!2T>*8;l5_hZGQshiqco2$ZZe8u8rzEMLpc_r(6J3Cj<6;-*|8g&`y(XGX{U45D^ -bbHESk(aRSS}a(rctV#eFB4``P%0xRlx3e2guRC%r)4`&L?pu(eLlRHXRSdcLmtIRP -%%K6+vmyD)x4LAU-Z4R&$DgX?1wWKoqD$s*Y0tbMH!W`7)RXNZ-Rl -(8^P=u1&*rge?uurKGwG6alA1rZrO$YvA&3#sWk-9~9H^09i~V0A8nvG5c|GaaPet%}) -XwF#^X$w7m)G`qS>Au}3K>h$8`{6bNRbJG?yH4$YF -wd)ULS!D@mf+4v1lME2V++3SdXD&Tgsl8WTGqIe=Hch*q@p3XUp$L-)E-C^zSPu+17S(D5?uq+4?0-t7sI37>WFj{-frAVCLynlTQw1Z4_bfs;Kj!rT1wM8!2T{&yrZ%Jr8Q@|O -?e^Pl}F%w3>Do(oYe)7MwBD2*7!F~m`df{KVx1jwOSt+67^1Zk{V&W)6cgqZ1xF^MMHV4yRn+f`gAsv -R1D@Vq604YmlF#;jSAHl)CIBhUUx1usS$@)L_y({C{s3@!7#!AJJfI|Hb8p{Buw?-Tp@*%&J-8hR#9I -+IBzF|RxKEbD&NjsQh)zKU9!{W>!WIa>_Aj*N`A|}y)KJ`K`*~e*0K#TOY`NoK|_Hp-)W(uAO+V2r%P -9k9(Mq&ij~hE1WtS4#0KZ$aDmd-j=Ml+GRx12)$;P{_4$V@dXwh%i}z>mPhC$&$*67{vY+AqF(Ny`>l -OnZ4fY6V3?~G5Z`i-j*~9MLKG%K(kB&<a8ugOlrOTyEQsB#MvrXC&B)e>$ -6`^%@xSET#(K{)!sxkidx3F)3)gS#W!9lKO!(`w=GQSdFV*>N?RwE{8kPXDFC -#G$mnwP?f@Wd7Hwo5^j`NjzU+rU^pE$dz#HzhF}&3G8R(slrE!LLz5W?GN4P=@td#&`M5iQVDe@j57I;KhKdSl_#vYN6)6%HBCrHqbY%U$-F -FZx-uvecDTaVh@IRH-&;@)4s`!RR>+&c|E{D4_s -`c5zRV)XzksZd#nheF}B0VkG@0nH~Bi?ItawC`BMZTa>EyWQSioB=V!8-PIxw|u9wO)~#jMl0(kDDIF --m8elS@QE{9y@+xGF(Lr3=+^an{H_Z;Pg||poO7C72y(CvL%)*QY|Ox|ST{Zm!nwp)fzOAB8IH~E_6! -&^ICil5_s+ow561QVKP)U_M(m_!uzB+ijJJW35*85CKOk_uk`$M9v7$g6l}6kffZ%z1HhG|}B2T|^gB -i@M;w6c1T(Xrcef{kd(385pphkRs$c#bN^jYltafVmH%5<(CSVO|Az%VelK$`0)L7`le7a$5?PW8qhZYsv^AdZEj83Bm(qW$)uKz#(E6M;Ze(P{PV2fPz~ -1`t|h(o8`p*cl3dP!RY#gQgqN`3{96GMbO9zy}2qt=kyd_g7a|Hd-5t#})#QRd~gGwTQkM20Jcib@?` -(9#zc>k9Lmq2E*cm$_ss^3ee+aVK)w_s6~<030|$o9AGT)F%~un*nQpdcI@(CV6&G6e>7k|a{aX3!}d -(d?ReOunPUfMNvx|hulS7}^C`GAvbI(Ah(!J#AoUGUU8|N0ih2u~Wkq{tN~{Sx5!m_krv^MAwFhA8q+ -h58GWBU~|9`~#>fPC4Z%=*6#}Ji(S1l^h?@KBB^MCL_0MKi-P8`Jxc(3<%Px-tQ`W5dMjI!obx_k)$D -OotCv<$xUj(4UUh4->XNkdIxWhpa0$vahrm~vgJMf7$)E~gw~GbYW1FaVV%`edwh$*75HV3gOxP -&Ft7|`6yokJPg_-Qw;HXs~pMKJBJ^kt{&0o91*v*1veymc`H~>V>*lNzov>r9y=OojdWw^leSAS0J6d)x-~2MYhYhARTa^i(jJz% -J*6rL)^J?w5@={(zgo!nhObPjqEKQ^-+#AlG>rchwW|09uM!1l-4amOJgL2^tq5qbCSCkAi5bjBSex(lKn2EcoO459p;w|>!OB=h_fJjR9lvDYE|dA>>4?4oYO7KWk$&K$S+FUrHh+6>6_@O!NGf66#j0o -EG=1M}B*^lMK+05^w@$h}Wg7o48iRK{~_-#djq-dLcXrS49ZL+eYXa#?3Z?DMaV8YG{F3jFg0DE;jl0 -=v^)Dhys5R>a+aP?W|{%B1LR=H!(?ltiEu$d#xiXAU;eyv;v18Gs4 -+8rozfHyI^Xn#c^X8fpf`U=WaVsxnDyI5p1MO?-71{y~+{YiXcehv6?BKMKEPcRV?;JTPqeRc0wg)v5PI*I;6^D>59%`HnP$Qa0Sd7l0i3N*K#kS -Lh7R@Gy=}X67*a^siQ`LoYPqx*~?RFSj=QQ@a8f$vC;T_5pfvK8GuPmb6FRk?NIPvFB7oiJHyhX8Mhg -aJ+y1-_F7Z8TFU#~u6H;L(j%|4fkD!xbjLI&XeWBA!0J5^AEwtk1ai;DMawX@Z!S7I2DtC@8uxkaoA7#Df4SL9_*Ypu|Ww}wGXJs^B>U+t`{T@cY$;|}R$WD -|IdudXk`8%}1bkm)TdHwX0*{c07W+eLEvt18_6+ppnMj)xro72;(@I-S5XgyWzRi~fmCPyTvfB<-Efr -+sAH=CkXqAPxLRx@8(jVJzrxvM-at^_i^h`G -{+=|vR$;!DOPGNu^{ZOwYDr~-C&|_cBZoHl>+?hqxN;Wf!#Qyi(Oz-fR|xKpokzd?N)NSk8~7HiQLjU -%&*NUFlV|E?dvz^~?sFLuQnzrKRM5Ha)wCe<(pHACw)`Di^eB&Xg*uy0#Vx;iCX@}Lh4L-*wZ8Z!&nX$j_x|2(NRV -X?j{vbayiPeLUkLcQZfptWzY=V;?^Iv{-kI?k$?u&gsld&daotlmI{s&M?0|XQR000O8B@~!WzcL9LD -**ri5(EGM9RL6TaA|NaUukZ1WpZv|Y%g_mX>4;ZZEs{{Y;!Jfd8LxUPJ}QJhVOlf2`4ro`v4|h^kj_2 -i|3{b1B^lk+tIjhZ_6%h0dMp~=s(~8rI{?t-i106LJu`NsIVqWwt=22?7YgdOeyLIb^<)0Gr~?ORd&X -Ec*gUC@mM`_KNyEE^h-A9(+8GvxKj{L7=Ls8Lsh_+V~RYnh0+Y>H1$er1jxfFvLM_V_{8%xuf~ty1zH -C+(K`3o)kVw{v#%bFuC4LJ%u!!DQHLzt&(z!fay&$qKho~j`1abJ)HsxHXCYqZo618v-`&hvT69~;Vm -=>3#r){U3t@@G-AG&^o86Yp+OpYg*{m(`YcDbq7D&6>(ylD0WCAUqM`^iA;8cU@6aWAK2mmD%m`;%}xKsEC008V30018V003}la4%nJZggdG -ZeeUMb#!TLb1!dobYx+4Wn?aJd8JrwZ{xTT{_bDFDhMLCj!+au3k2r^!6xYim(3lLTo>DIAkY#Wb0do -?N!c-q{`WpZO0pkmJr55L~u-`@Wn30+StU(6Uj -Z2~J>aczWKbCyD_m*ru>u4HD98$wrdoV8Fq@1iZMM`3(sW{=l*H{SNu<MfWSoC5Dv4qyPN(xSeA01tJRy&GW>50)|5Y1f9yB#V37WRx -uYZyAe#hNn-G2QCSw>{(P;qg8TpAMJ<}=pTxmE&Bs8qQdIqjD#mL9Y$FTG>)Z(TS;EIh5|p(m6;GMTd#>TrW^wG -oInM2dpFsGd5$5vWmK=ED<$n}H79DV4%7jvq5E;BXHZM`HiJ1NIMdY0+0ILMYp7iBcjwlGU~ox)?f8c -2cf#+a8H>%!G;~JI`jL^*+nco{@BfK4McB$LJy`OPoZn -=^-AsulZuEs9)ev71VpMd*J80@dJ79qu&am3tV1rD<8PUaW5XxjMGX4%Q$C%$5JdB#?N~yyA5=D_C3B&dq;n)XKq>gp!s0yLZN9Bpi(N|>dXad;JrKt -*ja7A2KFPi6qZ9(ps=16Nv(w>yrnl~d#puHK;{LlzlUg94L-qhKKVw;!1e#veG#!jBJe!KrccZ4GrC< -^9Qnom;tLCaN5UB-o_6Y&hiV%m{x5XG&q9DL5>v)lW}r^~y$>xZ*BG0Z46XE@dw9m>g)nN~I}WGPIb! -CP#4XXxNn&$)YLZ&Y387efbcIEt%DHte>@^_2l+8sE2+Z=}PtAE&)VH%cb1Z=0}ZwQyi-bXQ&?oz&ah -H<~9s2JYlmT9@s$7XPkAnH`u~d$KbG2t`f20+#kdye7;Ie*^#n^djRzI-tqYXApXT3W;v8liJq25ao{ -1eZ)5?;{G$keto=tNIzVn&eN;g2ZoY?Cbjm>-!q6)rw<9K&_IviHKjY6|{^sml~X(w%@e@_D#xl+Jw*rZxKDhyPuz2GubG|nLHBUXJi9jcE@<1D%t3u&z -jv;5NT@7A8#A6lNoBxN;EkK4zpP@Za^;%^F)(V`F>WV>{9yck|;glmVMs)u^4M)*xWJ4CQLHs0 -|1vgP-8u$ZBkWbPTkN7YBDTQ?1vxlXl0Ug8m$k2)svPM?P3`b#6o>bO&sbi6h|5Z%1`7Xfq&G?^_R$$ -cXk&x(n=t-W(koAsD@tE~7A0`6-IM*D#4Xrv#Og(~PZRWEpPpYnegx-d0B^aPR>VAXB@-Etrv{ye2?a -sYG_%J5ExYfHY9s9o@h-9X*Bdsh_Eg!KuCQG3_J`A*3k2Q`=x2J(I)hBtKSp&LAn2I5U5Lp(Ka=O$PV -C(pJyy=4lW_>TKTUv?!y*0{oG*-iRTo!zV3*5g-QfW4 --E^-EpjJR*&7=U@1444m^foz;dkyx10Iwe^5&U1QY-O00;mj6qrtKz<#^E0{{TG3I -G5g0001RX>c!JX>N37a&BR4FLiWjY;!MgVPk7yXK8L{E^v93Rl#oCI1s)2D+n(FNr1!4-U6gRkS49s) -HM>fi=rqPMxtyc5~(FAxBmMMNztMt2S|OeE%D7G=grJxsbq}^DO=MjMhN0{Bb7n)Nz1A=jQHo-tb1R{ -?Usvec+>|yD=h=7DX(xtm1Zh%+%i_I-J6*QHN3%O&qN`QnC4|m6w|U*Inxsq5aut`ZW!>oVL2~3Q+k2 -+@Xe)&$+<9030eg>`<=7K07#zz;nwg<UstQ;>I0 -}%o_KvRL-28|LxwNzh_ve0y__}Enr_)~BmdZ=21><3hKToE8^xbm<7ArI;z4;XLlyCsnLGC`B}C8~>Y -o9uU?s|TEPnyQ|62~Mq&b+K6zS+W^BMhP(_t4V4SD>4U4Lfi{2h9d(q}cF&)x5}^W^?1hjG -jAS-2@N1!K1@u7qIImE|Y9+;VQ$gJy-{}_^@I&@WdhK8-VaNoI-4%2NC!$gpp8Cp<7Q*Z+_?(uk}T#Lt32{lQ%=0Y1Pa^~U}LyN;Ueh -H@?@=#G6%V0k?67Q()&ysw;*<8K9+FT`B))zP|gIq3TaQrC51+WUvcY=BS>BISQb^9fGGjSi$5$mYwy -vyKUR0;5ybiR^3TPa&n*gBl)`C{O-1THv#?eWV#J5uk7Ypg`n#<^#Nwe`Xw*S*WG3bpLl~)p4 -Z0LRVJQD+qI>MIKwjBU-1IL(l&Z!{+;2_Wnnk@UC -N>7_o~@jG1cmdQ%T5=j+a3K`fZ8*tkc9MQ|mkgT*cJUsWVexpV0(@x9WC}54M6eef)D%wP)h>@6aWAK2mmD%m`)a=@ -=Ch^000&N001Wd003}la4%nJZggdGZeeUMb#!TLb1!pcbailaZ*OdKUt)D>Y-BEQc};+_(f`Xzjy&IHLGc9gQ>EiFx+aeQl^FeaXml@O-*8>wgg7GjS-+bv`;D$R{!;S`cIKF%jOw^5 -8y*wQEhuv;d>w2}`LIpB|IJxUR1^z>e*n7pny>X#+>pIlBnukUH#oOwPY?y@6aWAK2mmD%m` -)MdwU*}w007$*0018V003}la4%nJZggdGZeeUMb#!TLb1!sdZE#;?X>u-bdBs?3Z`(E${qA4EIv8wYj -<)W-U>+8a?$}UHlb+rAP?{3@ZLk`))Z{H%qw46v0O4) -l*W3-SI(%)i)DBojpBP@tv2<8^ul^$l&fDTvtCQRt{;s{O71uHqZdV)OC?6592YHRnJlFS49!(id8u4 -dNSD?o*uq-@gAr6%wy4pQweZjB-E_Isya20bqtPf;+&OjyYdn@#q~6&5)U4jZ-Rx|{;2Mv|PlYN0Dh% -}-4klh}$GFbmo#1d=m5h_2#&Wx7Sq+tp2;*uaU7VhB%~pa{PGpP&21^b2vh%FqnxnzS(tFKbzGPOERy -d(O_tI!)a>fbo?v>8Hw{is;aKU=KD448sJvV6p^ak7!EVY7ru;&OYn)AYF(ag_;5?=J+UT*}TSb}ONx -{aw+hQ5X=8N1#H&BRW6%yk!FN?!&ci4mCv&whhNOxAHLSUsKfpdQgncRU-EF$Z@rKzc`kVN#RfgCOxb -l1aNFCc7#RmV8X^IYPN}QBg0~z+&(zvZENoBN3=cF~7qfWL3g3=+LaPC~B|#Dd! -A9-+DPGw87v5yx!EXvMF(k%` -tWwCQp>YxbRAO%sdUks?-CDQx}wx)<5)|N+Nve@svI^lMc2feZ=^}C;ZdR4HlM|n@-rRfNHMU_@|p&c -JsjA{ya`{V+-zSAcUSu*m9ZF1$5q;HtWI@3y1hLc(85bs`#;ovFp%B?9w@V#})F_uR4&B`c4q~X%5RZ -d$z%j791D{>Y3~c+U2bJr!p0-BV;eYP;2FEQ?dL0`rHj> -n3W0B4ov_ye-f>0NZjkOPk#ZGGkk!(*gf2?3ql4u3g_3%UJ;gxe4E2Y$SH+I#h0Y -v~fI5@oI;%>mtP7J`S$YRFGq?qzcN#5IuH@gMDcWqcXWoKM)?w)?4WN3-)zBJ8P9;aTRn#0w?SDTHwF -F6uZHD(#Xv?|WEyf^5PH%2c`*Fw3HfuxO-t`h4*><$+-jT~Lha;_rBz`3d6B2Zk#&sD-I-|DmKx*Ep8 -vlmAG`Wr#>{I7m$I3prgB&|X$Zq&UBaC9~JbTMiNMc`<^`7q2X6gMBAc7MyiSE$EZCSPAm6!zaA1 -AiDL34meV|TSGdZAek-m24dqp_vi#y3OQVtNEBanL!VPK=28ctkyom35d|{pxuf#19{9frxSClf6uor -AEln6-lM8(j=;V1H?RX^n~iP^h@N=6k5a(8-{Rz?$!gmDFvmK}y5DHPw29@1V9+P!&|1&tVF)Z>gZ+~h^dGW{ -v2%4hUbfB$;3L?DCKnW#hW*inBHG#3l^l(Ta(v93k%60!QiH;f?|%h{hIY8j>0fIpgY|3njB3|T0w*a -;%W`%_)USQB*oLH!0VuQ_!SjCf;0$}Ac$9FABScal7nSFbh-}296D1N1SPP+2op-4Zh_^&Mm9xOtf1e -q-}ev+O^JstfziNNL;ptKI+cKC&yu4B7{MgkZ@FHJ>BBj=r>7LZgEEUFp*?0YL*~Pb=8&DFrdUDJXz3 -;Ty^H}XfEGi&ffuAH=@Ao-5A2jZJXjPjEv_lIz>dlL3HzK)CdpeP^|Y~O1}|~MpYrUl!E|Ub4Z?#3jT -^3k*Io)JM)la13bmFm_!gBJq!YI@ha(eeb_uFG=oJ%2aA?9BkJ&vo9K;!eJP5&mAMuZh_NR^X-Llyl< -HMHjs3-?Vp(tGHk}>NAyBD}{xHza32Soy}1F1%Shvi%E`?3v`)WAiU4k}LC=};cSDMOT=y}Z1j0uQZB -?1lg*>=Cp#sVh;Rsmc4GgP<%yn6mw`@QYnSzk^};=_a|j_~Y5-)$ue?-|3sD+>w;st73;f;@6aWAK2mmD%m`)o59uH -mu003nK0012T003}la4%nJZggdGZeeUMb#!TLb1!sxaA|I5E^v8mlTUBkFbu`-`V>SiyCgQ&0y_lQVJ -OzF7*-TTcj&2MEG80Wi-!Ir^S2)9v^>vS}YdV93c?qegZ#?Jq;AH4|_-%v+p2Jqdxfnq -aPb%Hp4Vd8(2Yh^lA?Q2JENg6J+KdjszueH$zt1V?G3p-3s^PeHDLZj_(VqTXHt^_5T1pdM=~>#M5brQlVfsB%muq!a6l0_$BpdU{!YBR$j4XKT>19O=LXnU2uw -JHK`JEKDeaqbb!%*CDKp0r^yvdhed4Iz?gKlc!JX>N37a&BR4FLiWjY; -!MlX)bVi)mv?E+s2muu3s@w5Lj$v+D>opi?J?l>bk9Qdz~Uq5EMy3jL4BSHAV7rhL+Xc{qOsnc_D`sC -2!g;E>=Kni<~)g=Da@V%w$=YYnCL-MmM!c5+>JGS!>1@s>~ZL68nA5GWj5tEQ`s+jY!Kvi$|Tyg?}Qo -sC8N9%0I}nhRwfT^DIB)}q -in$M*xYR3(c-wDfN6*KkkRvZ%4oxY06Kaayh`uKoD;*Eiq3et)}vxD&Y_e-+T`{S&lqDpD=t3)jow+#TH`35?q%g(ASfRuD8MP5 -rwB^Ue*WCi{EaQ7V9e-%3x+7bZtnRC-Fo) -rlQj9%KU=-EIWQ#cEN7SLQEzkbm;WL8+;70|7f1glBD2k_*FESOkmV3Q6d+qPP{=RY0j0JRkZ-ywaDg -d$@6@{)BDISCW^E)tX_~Zxz=?zI9|$J%-PhBna&MESc_(2FH5!&EGvs6&F+hG!!|3JQcID9!H5LIZ&} -RoKopp9l;`Z1Mj;3&)xb|JF->!&205XKplT|kgr<<7CrEs(QG$rDSGSkf*9?9Wo5`?*qU;v78>3Gx5O -s=n%bxGcGAAK1JSLqI#i~?FF2HKK;x$iUCyMsA;n1`$&B7b0S9YwIk0aOe5zM_{A6hPUZO1;z}sc`tT`gmHM{c(vCA>q)z+oiR7pjkOrw2Zfsst -__9yvn^%IP=EQp^;aSX@lU!QM*p&6X3T?uggEa)mo;=Gb+!0&yB9(9iT9M4b>D|sIB{R}8ac|U=b7@j -Yr=Zs3=`#>u{7Wf|S-00HHd_%hzoW?di*?y|)+(>e5aZ4JtD8RbUr_wPR;KhL|#Fhke4VF(z6vkR$s#N3#nGKh%AtT92 -_QNWx?*nc2V*gviEDm)^OgEN-?b5D7f&Z$V;9nBE1mZSkNFZDjVc(!5@sx2nvWCMyFt)R@1Xw-V3&p8 -4?Rin_3~zMj*68stK2~yTu<$u4T^aR`Wh513VDvBL%g=2~0$mcMy*eiXpTzGMKYPL-C00^#RI9EM{l% -%ej?^9mC))hWxUmZ4xmo1PQOzxn=4sMusuMQUpd~EUIBx=G{y4E<+lG^eml@Cxc3u10s}8s$6K1Z|&4 -C%~TSYz-}oIZHjiVEXdGPP$LiHOGuiL;}oILtv1`RvRTP=1x>(VRv?PO;<18xU}Pq8zQu2WG?!v2Y5f -;|25pSJK( -Y=dH~4a77YiUV|lH|yBCA6Y+Nj+5(H+c>aYb@{Zcn9$ZI-OhN59k>rQgew+$o}Ds___;jejv6mt}JO~-I>@Sy^h -o!oe{g3Ym8^yV&6EK9Wq+$YnB#a#m-QS(wwBMo}h#~Y&WM^iO9EEK@+0x$57|0j-pU4xq@JPoHO- -z~yLe6jV7@u{59KGuYL$OAk1bZ&xVgZq8)(9Yg)MAfZF9|#J^WH)%Nz#ai5Rr+}F1hTH`%una9Zue50 -Y5RR(1mN5VJ^lcEjt+liU2w75oXBm;fONp3!w!Hx8R}B01`6-6X%^En<_)x8=7K;IqlqU@(%U>i- -?_(2BNj1x*e1zFgpUbDLrJ0|c&oIzYUZN_oOY<3f^-#);{VM9RZRWTlIgWSZD1Si7dBINO(|+W1yiGA@dWhjOcG40eiA -+z_bRXw1GmUBs70{4t&kbIUvw;`vxtg-)?YKl|L%u>JLQN`XG*Zu5ad8$bM}XpyVt~~B<+f5FNIdT55 -Fj%>E22G4@2&bhGzZ}QyN%gqS4R!dyk46~M~T~HcVClJ*aAEkCx~tvhtS!po7-zf<&X;K8B}U2XW!L` -*=^}cwy^3Ta*3ma@cA7_!Gx$8kaBGGv4FJ22`gZdrrfwIeds18t#?Ld4BWBYSt}q$$x#q0GMF_sqF5m -EbOD;`PAl+D4Lw^FKP75T2Gf9?EuU@9XeDV>Pl=Lj1MJA4gF=g&!dR8PeT$-O?p6jkAs_%nn-!;4szr -%~P(rGzEKJj7N-CKSJkGWC44|`EvXj6Fa0zvBVO2311iaAVJ_GMQW0xy|65Z~bLrZm26vQ`zg||BgJ6 -9f_xBaknnKiix!KIg$Ij~v~Q~_U -j=i5w)is*zy^vLsW^huY)Fk@bGZ2)4-vCY-Zsmethlu*<6Q%KJj;5*=P;)e?4&PDTV-fe4P$MN9P^nwDQ_dw~ -v?i6CWeE_p<}S_b$J4!t@7mfX@!;LwvL~#Q#R4pUnzBVUco(5xN -@)b++RltJ2LKiWPVLxw8jYks_YN$yN3L#C;o3fl(5Xqa)kr|IpjDTe+`QD#9Q;&lPp>f%N-h3@4-092cXr?7QySr(s9Xz{|Xt5z~}?zIpW -~dHeHESGU-~f7BHHlSrn6BnF -*ukLi?tkewqqhBpvmCvj%5z -(S;)&hn%_QqW4L9Ng5x{7u$m&(c*|bDh$WD=!R!ED7ZhxRA9)nzEpS%92hIp4qc!6a!QV -#=l=KlfQQtP-6Gzniq%!wXq+2I_0XQnW+b?9(|IUUB8al@z?F0gAZgepWKi?q|BZ>Vn!1WlJ`sOzLwg -9a#sO$HfLH7j#8^!f9{>;jn=LsjQmsqpSxnd&~=%IN5iHRJF5LzErl@GHRbN$mn!?6g@ykD*{ndGrOLV=eiWarkC+O0|l{wXt58_A2g-%PpZm(2a-+cFaOelQ^b= -3i!Zx&qi=`vC8fVoe|PA|yt`$Gbn!F{2w=y-$7`J-H$#!~MXccirgLrQF}wfAWrc%CQ$ZDsv7k2BFFB -A_-_YA%}ea!_)B7Shmxw*xxZgmyzQ@3dRqtlnV+bL6+%R&=ZyE*F3z!TG}m^h~dYegt?R6S=0_Gc@9O -my2PzwZp%@czWLq;WQ*|Vy@IxMPAtWF -UC%6o)enT`|`2TbAOG(LOzRd^6!ji-gQzB}*V%Vm2PYWKWwyb|chy7%;J@TMl_avL5nA`BLXg24`bbG -Vm{wEN6^D`_eL#)^Vg4MQB81hLJYX^b6d^dngzmE$NS+M*4(sh$wFe~OzVlN!UC}8x4F6+4#D`$(G7xzrjRR@!=v&?i+tw8s$=>)z3MfEj(V|>6hnS0F+2sO%4@I$A0Uy)bXwHOAV_R$}P1lsF8WVivW@VakI$orR&Sqs{Z$ -F_F`SU6B2nF8jE?Y#l6UkF^!HctdA*y4Z)5^r1GFNozuGqZ*^ZY@o4a(V3VFIPmY0 -M{9Ein)Da#&V2S_vZQ%%9me75xcYp#r-jv?@%`S^XVSjnX|(nZ*RWZlNk2<>u=9Kt~)e^8PvcH)4)}d -pmX$_Li~Za;)l5UKo-k#Hhr$n>A-j1mruL&|D#~kNtHavm0EPH{%?;&p0n1tO#NpUW=GG}5uLa9BGo* -4I^sN0u$L9eb=RDjIkgT;cE7{6W*>T~uipcQ_U#dNN9B7aYryr~T%qpJ9N@9LScSl`5R~(4JZ^80>s3 -u>yap@y|6yU{lD(KJHH9x1qonEOBEO=9WmLBMK7foxY&$IGwvoF|B(k5u*VHC*wq1!%R9)rH4LILhy8p+;p4#1WeKA&Wc*S>0AF6ny> -hCCy$8t~9Oz5=$%+~v@qHe3vNUOlFWx23-&3A4k?r`RBgiKS8WSE-}+Ik>WW+A!3|27#TW3KIt(-;#> -J1;|Iz_kAXP)h>@6aWAK2mmD%m`*DfpvguQ000F`0012T003}la4%nJZggdGZeeUMc4KodUtei%X>?y --E^v9hJZW>=MwZ|8D|+S9f*e!OI+?1eQbitTWM`t<#7V_Ywx%>H2n3pBk3axH1C%)`{r7$E^@)>`<6V -B>0rc_is~>oLeEfZ!rx}+l&NBAff__%qg7RGSro0Bx~X^+F|oxvEM -CeYYib^;-$zIKIDwN_=x -Gi>EhE0`aVri$vmEP^Hy30cbpTgJ&<7SM!G4TOoU>p*5ryJx!9Ma&aN} -cB;;4`NlG8eKx6KgnjWFAOfc0`-UE%#zoB&G*gr?qH<*R~SsFknHe%G?DZrt|t^XBL%ijbZW48IO`i4 -5S<5egfx;F6ZQaLwx<=-Mob@^}lvJBMK@U$Jd`$0L{rg7teND!%1;9f6?CDqa@&1vtWY3kKZ!7f))My -#S?H0xvJ#7df9RvZ8+A=Pw+TBWLx7vtNJtfvrG#W6~^d{ZM0Ct>w8Uz1i)YX2P(d<}{5UxEJ+?#rdA) -oTogc8)d~;;uD+ka2>MawWwbn&qKYM53E{o*5q6!ammxUxy$fw*aDuf*O5OLDA>}uur8vN$T;4c`2!W -t0Y#V*JUqU6czgkCT|7JnAy8l&*IjaUcTx+0*ec4NTZzOqU&5mQscyjdqT2fltLyV44FORfCINesES3 -@&g0y^J)ZZ0Np1!N9qME%kdajvQ!a#4rPRW>@6-xvV2DPa4E(b!&CW~t{oXCt*S@J}zM1sf(vY&OqL@ -sNP%>@|O0+h`%5hf^X)qPf|-LzC(;nf_7&59MUPDrM}lAEH*Qnus_>;r6_qD2FOiS4$jQBfT$=?e;X1 -j;AsC~^uedA3^EU%Mot#TSgdK$`%SDSq;Z> -GjtY);hdemX1IjCYq-$WARnJ|WJ?PNlqbQM;qjcL;Nd^2$Jb5={XI -f$A~iT#Gv9!=m%OU?c3S`*Hr{o2wcIDx4~rMt34`q*B{k@6xM0(`$?8aNeAGqMEa@l%<_<|b4Wbp~LA -HDTpk*i$6;&V=8uSSxQe%a|?b_c@F`d9SxOtS)M9PbLfKasl%Zm=Bz+FbmSaMuSQGggZILVX5EOw$uk^T9{B4QB?K90a|M12zNgwVM3!IB$!p6H6Nc_=D(3`0_sdt*_O -rmBF$%9?;>hj6f|wL1Whevdnb4u`H$o3>IIV9-riN4cwEK^oKu!W?<5E!Rx>W@o%R9G+Vz`CB4CsKmf -IN!$ijTZ4R7dl1NnM@K%4DN$$_gAPWCvP?D*9+9{QQf`6;3m~k)kIfq2m;bKtL$2zQK6iO=GvMncg{$ -RCUH~%@S_l=lMa3-u#2ScIRN{aX-o=$D8p%4@k+%{gE;ZjenecOsu#sGXgw-A(4$_4Hn7qY+R>x#15+x1>?y{1T;x0e(3Q_RQ0(HY;c_+~1p*>Z)XHaj2)*aEMk3Vf -kgVxzU@jC<45Bxqjui7r#hT~B;i=rDhna$_ElPStBR2-Yp6D31tLsioDy@;)7$=z=LT8Ahef`$nV-ri -#3s?)(0qGoc)y4QdIOAs??b -SreAxh`#(?RhP?P)IL93E0OWr2$6Ih4nIP#)n94x_Y95$#+oveJA6G(~!WJr0|B?Xbe^U-7bz+Y!|+CLVG5s=*XKJ?6*)#i6a&(A^7vRDz_Jm3b}G6vU59IO32fF&JYnljehM>?zGI~p6$ -}uSw;W=|)C!lX6h(2GQehK_F3@ocy2R2>;2{vv_eJnD~pCiB_^xYG=Y#XX#Wst -;B-@lIvzfsCrD%koCe6ka){_v;$zf -Cq+M=*3R-p{i&=r#Fe(G2#eJR?aY|#ce(fSEottNSbbxr9oS-MU%!yPfut09QT(ogc=jKzW -p2CdF1{ab;v^Rb(LTfY$kuw<}{04o*~w$2Sj#54!&D1NJz=M^%?sk*4OQ(J_ykW;_*?^pvU6n-gd~3s -)bvOlHk_cQFec==Mvm>~FMtVaLq7%D?fj6&Z9Qxzck;l^xk!a7(Nz!!a;a>W{sdd=lQ -JW4t@OQ5gvOtTgBm6&M4F^8{bazkH97k(%_KME%-p%h@KD?LXcURSl4`#U*$$;kf3yTN!8>cv?+LEb0 -T|?)j%c;*Pqs4=ze#UgH59Sc5He&r^A@P2FwFN(pz;Jc6G-366#Wrp7%#ljx%w#eP>l}r;vy`WlC^cb -YZ=5Y1{!#LIGd}yIM9Z{y4reIUB2ud?IZE|NsEi?toUL@uW-8)d7G8nRhYw!3(kI*aKv95X6o{zeVIf -1j7D`^WR2*#h*IzN&=04@sa27>l7p#s2Dy!Y6%tTGM$mI0OEO^lmlsx$6n7 -JaZGn(%uuKW`P^P}mi57s*xYXctbJGD8i=n35=NfKVhU>f%CY5hE1%nTo|70{r^R4-opA`FP$_Ije>xdv!}kD|Rjaz!;$L(Vb9HwU*M- -q7rMMbJhVykMY%1A$2|sxw>gd-%bTuv5cTI&221@edbW&L$kDnhfF6+1zf2_iYJ7Hia!xWCj$;u$g6T -R!{9J?0+WlKh-ul=Qy`Yg7LlZX-e7zW&O4+;Iih!>I27}(@u|xh|AUztJ!|sco1&)n -)ml3gFhTe`&i8g5KuKhHQG#RKOp0_{vuSls;DbS^D^bpZvtY{2D)0zvY2E89djD#K1PgXB%eYjlviiP -IHAJ9>%6K6b%4<=BbrEZzIZ+}{=qdz!saUOe#j%5M5^GgK>9XwAoLwlx+q?DEWFd9M>tsV+G(-W6^A| -no88KDg;aoqVq~_t+3nCUKra~1~QDdz`PPl3pS_$Bo0!8;Zw?dJL*cstfF|e)npjA;zbpo2Y=rj=;^v -;KK>Om$<=Oq7XUR%-dU#soUG>tpEQ8xu#6xntu**ztT*M# -N(`j*nWRXP4=h-gse_9RUP;C)?4uZAJaii5i-JRm8c_;M*R0C&U}?QT=4eKWklL-GE+-4T333M=oFvF -<$t*J?MA8^cWmnHu8I6Jq(FOXCk0a;W3&tS%JflJTCC7m@KjJ&G}{&_*GDzUUA!g)D=rBfbSG#>>{6Y -k;e%DNX|f?4_ZyLHyb76vayr21dhxMXD{NQ3b<2yX@2KZbW*ySvi{?6`J)(l`d`7jnv|?wpJ`>xO`2j*tAwbntg%@ln8`WCF$*>ma}0DL;hEjfMTwuwIXzL -wMuln?U+uhy6_aV8>UVqHnELKarMmz8TOx_OX)pZ~!urr!PhMHr-w@tmnw{v$Hj@xmlw1J1e -P|_-wl`_h&-NhRd^8um1AqQG!pA**RoNI`}BrI~2F$rU)EhK?`Ikr-0u1N_4Cz()X=B*&U%?OU(TASe -Br|Sdi*Gqd5!yhq8)-rNv(v?!!(rajyUCW?{a*x@kLGx8MZ};-KSW_t*z3>e=b5iCOo-q$CD!dB(LJ4 -Hg5+)^XDG_~?LJ`~nWY3;D7GIj$M0<8$}G>i{SRb?t938!-mSJ0XD>L49GjoYD>TgfTrDtkE{JzBq+m -u(t(Tfej_)=1~nzGLY?KaU714Qw|Hlu$v83l4t#^qSCl&|AY2>Fvu8nu>*9~+3XlmU1FGHoc@d++0nG -%GUH%SK!i+cWL6_<-8x~!@X{F>yTS|yn&U<}hYuif@6@vMkY6YC9%)k5YHIJ=n?x*~7rDFR*<>dircC -m{S*=%6MZ~G;D|Y1Z4q7mS8+?M+$+pXS3NCfK{f<-d250$s4K^RI#JZ_?`sK3eR`c02vG3%}E}5ni=X -OV!S@p|7UNw27+g6-swXcAseLKi58PwIDTOJVERljaXf=Fr6_t=5uJ|Hm_6G4k5_^`!Z&9`X -z1N$bj!)-WQkKfNr7{sw{l^g-G57*fSHYhS4Ua0%bN|LNWPkN8|cq52@uKw>4!R&P4fu5#U3fu~bola -284PdrioRseE{Pc-&L5nuSMd`MekVC2KlPqUTjb^p^cQn}GxeJg=#p%vw%VyED@*uH3#*9u0eK9$Kx& -{L{M;9yQQHvZ>tKYssL%pbfiRj(V}A3Iz+P&fe{D}JiJ+kd7(+QjydhyG*-+gK9vW$AR;d9dt8J`*FB -&+3Ki)TCpt_(`qt)YLFtQudc?Keuy<=1U!KVT6lkU+{tDH88$onZG)IM{?^YO8n>GEP)l+omkEM{8s_ -NS8tuk%4V0`bX=ueb!wUjg~@GP;iGIRuYo -6iNW109CeDT2Dx%vzEuf9so(p@lK!xb}Y49`osGQBItBvcvEB+})I2;cV4{Wc!JX>N37a&BR4FLq;dFJfVOV -PSGEaCwbZOOM<(48HqU2q_l!WM>_uKv7`P1=pTx+XBs76Y0 -Kt_ikGM5oDRK<-pZII8ffY3lseT21H=dvXtPJ2?i(2t9B_7RPaG-lq4&Sc#(*^o9-)w89Zjnv%)$j3$ -HJ6E-d6!wFn=k7P6I)0wkfN=hI|54ICpNZA&LqGXxG*F7*!{9R<{=NzEJ#JFYXluuUT{eT!RnY43bGL -mjGO&+g+Zb8J;1jhpR49cp{x;7i;2r$2>8j!nlSKSC$DqDLitQF9|tS)s9TC@tHYFc)+=@}U7@@K=5f -2iY8Q-{W#+D!Ys}{Zz~11-l+YhuIz60p;`#((QE#EH|S2mj`iI&M1sc48XW5nlhk=zVF5+h{gEuHGLI -k+}{O2$mo8ygs(!J%h7Vw;ozhY+@XBlMD~UqyRKuywpD&-s4l62Z#Eu#PQHHuK3oq{Q3_X7MfTyCwN8R5PD_#Q9Au -%A->*5VTA2#`_cZdkTMuj8kpIm(kRj>||*{#ZHB8E7n)tBfF~f -`Iq8@hi}|0cy;fPkm24bBj**d9{1KxZc;EPx%;?Zl|{3JAy>c&8luGbhGn#T<%%f1(KOj>yy8<3CV^{ -y&>Zk4A&!csz*p%o3an7#>d8S#^F0Jv>!ni={bdWL>>fVu|0fE>mmI>mO_f}J?Szh>9~x4ZQ^s%lBrZ -5{0d!422^{-D7;r2zsmEae8)ZB;49&L76p$`B6pf -Uge7ytkOaA|NaUukZ1WpZv|Y%g|Wb1!FUbS`jtts84^+&1#Ne+6r!kha;C5ufP2l930 -*)bYCGIZIT5&^CyV(o+-*09}Nz`L)_u574O4M*Toc9dbg;r~pri;o}TBIqH>y6Tu@wrh&Wku@VkBHYE3wfIyCV*>QAdj3y5f3DVRUgl%)m6e4_GPMT&)H4iUi-j!2Vfe -K)Sq+^_vnlvKMFvlS+MH(=UX0l(ou`N*1-cL8vSPhSbE#{5@8SK0)FQJ=@5iQXvu1DCVy&$BMcJ3CD) -URNm7cO+oq(RJ(rQ&q8NeSM0SQ2+wCtiIhNK=xfT}zM;=ddn9id_g4Io~_lBKXe(^#CQB`_{cCjfT!? -A=v*efjG0HvrXw7z`YZqZ>Y3Je$3II{V@F%lA)zj}SN)3qYstOj^i0k*>H|#b(7P=SK{FT7b%zY_3!h -w`h|!x0wS&6j=cbXRoAFNC{`UcG*wzIc6&l;Y!uH`o74-(O!|MvhPr6t -4=Ka=ip2+#;Kr% -%BIZOHFzAop925+l0-r_LPin8!=!@&usS3Ocs7F)iX#&Gq`MNq0=pzHQUeOSA|1o^Zq8?gC}Vs~3S+h -0#L;OqncNQMmtu!d9o@VS|C3Ruu46@u{}!>LEf5it(OC%?Ou;j->W?2$Wq`tv+QFRR2rT3{=^acQ_qp -%)HxR&Nh>S15EYeL;As4*>7P8Fa=t(qTPuSUkLE-~6nGur3tLDHS;%*6ql|P3hFXxr4%sjl83zX*OohQ6D52Ux(1r}du+Pa -p`ETOK5)d_T2JlFqUx6Pq!StWSl4pA#)Yxt%vlZjTj_(aKYhDx(i|qA4@GH$p^ynx -}!&G~n?lfG7T61{p-t!@-*N3<=gH2Umxu6YTOIV(U_!4^33Dv;Ol@p1Ef?Uq9(`q|9Qo-(Bw1`bpO3^ -BmCGJ+fs{_!9Uk{JAd0#sNI2ALD|BpF@q1VuG7`}JHEGMi~2Tzr<+xF8B?m)$d`khttIVmd~N@dkT*xBq}wr!g7t -=$c0YZe*i{p=B5(E`~1!joVyk-SP(A0%*rg6tV;z`Ro(^rO@6Q3$SHVVc-MFcC5N%K+Z*p&sOtP-K9> -Ua1-VtKAW=1h}}Ue@RR$Y$tk|K2FRf#oXTtg(o%wXX02hr{(0z1m?K$bIDT6`vT9?+okv1w)guktUyww(~`1WX}tRbJ>P+65G?rYTnTap)B=l~ -P|TfPo3Xh0H?+rD7As2ffLR -DA{u9UI}AQvOJC_D&Ui9u5)=2U69l;!D{7BkvhGZQqvGY#I&0Ju${SDg9{3xj=$xLvlBTOt&22R$oV -&}e*!NviL4B@Uzav`q#v!%g-{?P13QF`?imJ6!FvHpN>+c&-yrsn? -viMjxQ+V$@0*_lsYdor4OAEQa5fmsVI%I|S90gdOd@L9gT-uViT?h;N04Q*VkQqZf2mE`?0nyK)$XJSQl=f*Pm~&l>qSeASh=ibCah|JhLaq5+L@zJjKfg*T=MTf?<4mR -R00(>@^?zV9PB&ATr{Yt^i_in2echjXL5AhaueN>+l${Yu<7@P-sFRw%AYVd6=EP0(J_>Q -bk*!vYw#J5UP%d;v=kzuI@OPXrF(F)(ddq_4KI5+}1L -^8UV8Q#zK4DGH0h>U4$|i<9%Pre7xp8i6P}4ob^CAvKA$Z}n$}1?eI-aB27_4JeCnB>?JkWUE-Bdvg6M?U8jtW9 -(_xslj|cf{ez1M`{zZkjUAmOKO|54iQVb)E~{2BzF(xN8m`LLW--91(vs0(CTxh;iFrah?CQ3K*EHQ?4;1eONi~=>np -1QKs<+jxYXw{d1jtyE7u+wi9IrwKK3=g!S_^HeAfu&5gDV*e0J4KFLv2S;G6;kDX-aEh-(R7TtE;2f< -1q=wtvhDt1P7Op6YoHJQEtQM)b>}K*Ir*cN2M!QI|2La~RAyQ%p$ -NmY&^yGBN%GA{a)-Uv@LzuHKrJdX44T(z47MGreov4(CSI2)IVLQfb4S+=e2oq8k#3;t$&F}8t3?Ycp -=kwK)=PA;P{vB}qgyTaLJAwZl`y)60n}Uze%DdPi_XmYm=Tt^IlB$mO4BRM6YCpoXQnvYs5fX2jjWy)My`kY -rI|I{9IK<&|9pLk@IUTfqiXQ4C)00^P*tKuG<6@OpWG*0k7uJlDU -oP56z!vogCne;=M$|I7K@%I3eyN-xB`IgH9FPI+(avs}(KXo|TX4qm~N^B;e^e0FE15ir?1QY-O00;mj6qr -s;J=Nh;1ONaV4FCWh0001RX>c!JX>N37a&BR4FLq;dFKuOVV|8+AVQemNd9_wcZ{s!)zUx;I0RqWbTj --&u00y>5x9FiSG(|6lK`_)vVwWPdBxNTk`rkVwC5y71P15tn^Vy&8o?uXRf7A -)4|AV>Vp%~mYuMCGN^>@C3~jc@MQ}JT~8}vCad4@`&(gvJ8hn?nH~74hTp+hq1C-s&S<$N_$$ji8eqt -Ff~9Q0{SagYrHXY3=qk%HE}6CDM=*Rag2`g(Yju|qe7rh(%G)Ln-v&4X6oLMIV_3YT!VbX>5TnW7;t-B#$CHlW6+ -C(13JbDcBS0J-Yn52t+}wN!D#W!wzHdTxR`^~J7KPY<;&gbez&fFVkCQX>aPoE(u3a{?_$XAhy@_eut -O&DYBEXzPKo2u42h}tFv6wVU&!iEQg`OD{<+|&bs*1&5|Lk&Rcq57e6Y?&#jfw2S5(=r|t8k3^@$0ULi5tCM=9bSYG=m-XfM$T#F -TX&Z8DZ>?|4Es|=XBJVkh}Ham&IU_dAif(}j|ARqlpgwux%l-dA5rkW_KAnC3Y5j#clk -npPu`uw5xD(w^vPFjUr2hUyF~`_cBK0ox`LA&_abJdZQwxphZd@cwt;Np4hZ9Fc%Ec7S*EMCd|)-)2f -;5vkd)%g^OE$@ur$D`B1vX-P04~Qa-U$>Ay_OMq>Fej^}`VA9#rZQO^$gOzxYjSA>r;~cE(soBODZo< -{SZ@IP{JbKR`RoJP%ZcgxWpiIx=cmY}bw>0_nW7HSsj$>5Njh_>@H2FGzC$tHqVEE{8LzG6h(!8L@lBvX_;*!lH}{JHmGJ8$9w3>M+;A^0xmjig)awh;jiFKy -~}@%v(gp(A3%q!J_9sPiBD8GDk_SAGnw9W{CT(p`5Sp;&xo3}173_h#D{plwxrT3ccf#u$y12NoI?J1 -J;$p&o`a3Bh<0&W_^a%1P)h>@6aWAK2mmD%m`>-7%6(%B008G70018V003}la4%nJZggdGZeeUMc4Ko -db9G{NWpZA*3QABuF7D5qvCyTUFEHdwti9Jt?&4%Y2_gYBzB-muKMp`8=!-(gKiJ -YckrFbesi{*&(JYl(t;Pg03#%1i_{Rx4;gv&Ce4^c_gc1lF11Y=VIXIw?JQrie=MLAPx5l<(*T4T7-TdY4&0nKWzkK}o{^Os=Fn6(Z2>)b4@*=+}awUp%O5oFE63Ja29kMj4L>dW3u0bJTTx2Cr -neV~j^@)G`pOe*DaN-39TCm*!Qh4x`aiv%?gYSU!C267Xd|hZRF;i^ -Pb*)DG*xtoR3gMQM&!S?MvQ$*@xJMdP7TrH2kYpA8&w+EWx&8#T0t$A -?AT|TX)T!f2x_=@;qad~lmb$%IiK;h5EX+qwyJ97S-oX;0mQ?j^Np3j%Bt_YAgTz(sUdoiOWu$(1S?0 -LTUE}Vz+$GCnME-sMk>x=WntNC%RAE+Sbm&o;UO6Hf#i>u}0Rf}sG&r+UOhuJ2tY9f}64Fwy!4R@~(2 -oUI@p{VjGHhTBLuV?nNx0+(0?IWoY2T-M -ro;~d1H3^3uun_1xGlird>qu=W9OC1x2GJo3-k|ffPG)5W1cb@O@GM2%$OA1hE+#(t+~GVts?z46*Fb6J%pHdYCnBLweWSfni7}_DiZC -#o-ITWS9_H>H*1}*r4y2-;$`C;vlmN&SbR4+q-1`TfuYR+`WYeL1&@8pnqe$Wv>)8lt+ufyZ8V8 -@OEkE3$n+1iTsD7N|^MoR;St&%m=cgpb@$u4urNu>N^CJzOZ;ouCHrf8j5EiEE@`OmUPFo&x!& -r4BxuCOsys?9yn0*)^6LhSpTOt=oUx0F+!rNVU4qUgL_^QuKJ1Cc{xO!?_L`v2Cab5!RbJrhW)(cuBpvtm1HL9Ko)pK@O>c?;+ -iO&YZy&XX%?CM^6BUCAqPxpWJgc&aFp&E>=dMD&)^wT>~lc)3PoS{X1h~j2Xzz#TY-YyGggi(f#*Y!T -jr8g$%NlDSvq)waLhzK>kPW!@x7Bpd#Vn&{~+$z4eOcB<3>%(moMQf -baid-cC~PdIu;R{0Q=?R>KI6we%rZq~3j^p|i9S7$%g02Q0)u27uMYxU9(LUmoyw4%7B$VU*mI;%QWl` -ZIz;%hHwji1@VXD(5O^bXZAM_Fm0=DTBH3;U7j2V9`Rx$ZISjphG+D> -S}2#m2RURAP;AHx`b|igcPb+$A4d`&1LkMf;|Jk`GfEHcXCs&!nDb1V`FF6%#bk1_}Hl=y5~F@Ck3?@ -`k5LF`kLTmIttn``w%1;oIJLT7;S25YvFR{crXp{j)l|64+3WKVrr(@fWJ2@L5-_dF^#OQ(LvzaHtUsG=YkOf^tBTC*;LQG2Olw^>ZQxyIe0-cM-H*}9Ie7Fm1nWQ4 -xwb+%AG9(St9r5E&fyAHhrclAuT#e9$ntSGog)7v$vgjvv~JZHEr&@{}(?o0mVFpt0FK&0!H2}?B~_! -s=6g20ss^k^=I;U*)6(4oBQHm~4%x<-L>`N{3oDlqYrVL+VC3ns~SoMOFHX -0MD&fn{^ztK}Qvr)u={&8LF@KLtCgbCPc5f-cWn8*A5I;v__;sBn)6cp?2D8?+8Xy;jM?=t6tX3&`zz -@*m&+7EW)0XKaGCJeg||Ghm)Yz-$?#7Y95v7aozWRv22Wh90&BeVPSXMgS@6aWAK2mmD%m`?Rsje!{k001r$000*N003}la4%nWWo~3|axY(BX ->MtBUtcb8d97DXkJ~m7z3W#H%At1ZXp&fWLv4TBzdDWqNvuXl7?XWlU;3kU5Yh{!UO|LrBF@Nd%4ymB*G3Rrqw&xC4E;)4=OV%m4iOQ -Nr^qupTWRoQ+t{0Z_yy|^#DbEqJGu8{ac1HJ}^7x!2!}>;>_4wPktdqVTAKlI+$s=)TwrxB>AV-CtYT -bKdo!Hi@s6{Pr3AnBMw$w<^^!6CV;TlpqgJ?JaK7h)JTd8~E>i`6Ad&r7Szkk1ccXcUVi8siH_DTG`M7JOoa8zFa1ON>nM_M+(OzD@WU$ZaIzNQ&)Zlnh;yp!iEG4V}; -ueZAxgg^Pvu6aH=WEGkg{9P=Wvc>od5~W`~T?S|RTu|r5+u(g=t+9uokHmYZ4p{Q3cj$5ysP&K-RK#p -YE>`Fr=tn@klg^=577hBg0@){lHUK)*1-N!bK_QhF4M|9ouca5)*F(IC^r3w~K}gv3^&}D>hzbVXGw8 -kY;Cryo#Gk4y&~5Ysnka6E9^!sE#Oga#mUH1o7<=u&uIT4hktU1{$KVUbHa-Lj0P%IW#W#c&1^qU?mU -hG`xFbVqIm0j)9?Qb_0CV?v5(#Y@!5pQwrASDEf=w9Cf~KHEQCP-EQ^eIREPKa-M7Wt9QfSCIx#7-^* -=~dA25mMt#X5L8^W)xmEgiU8lPqC;5H;sFqz!BXg9GG0f_|Bcr+maxyUtR7D12FasvD3Gw@G#i&P&WE*I)0W(6H%)Vjr@Py&n!;Be3{799 -wT3Z>43Px=SYKCu~A8cGjPOz{t>&i0DD-JMDg6Xh}bBq;n)TqLg<7AYe=@Wz3ja{dTKn5t5*#^3m_l{ -<6Z(zgIPsK)v?-|bXYs^-n-3x0RizuJlY3W&-T$Vsn_j585;gR@t;NdtR#}}HzC_t`|_HsHK8O$&bYi -b7Z9>T@$91z_Dl8l7orw=cgVXSi*|Je_gPY{{K{@9gBAM=Unaw&e?`zCj@j%`2SdsM~gxXX^Ddyy8hs -0pBXYjD8bO`|bf8+o$9RRA~6u3K}mT>C~pKW7hJzhN9tHuw$a%o^GP!f^ji)?0nZr>XeP6!vZ)a2vfI39R$C1ZjSGqa_*hLTwT -s{(9{{{XSmGg&|$&Y@NZ;Zs^TF>X6K~kd)%DP8~(>EuW?r%$p252EZx5tgip#Z!xNeaCz-q>vr2XlK!u!z|h-Al8;RJl5{$r -I5TcvIzFA)-pJ{sPn4rj6eKaGNG(BHQtz30kA0(ktgR{lBtcOZJ8pM(*7}jq1W+i{^+SQ=#^0KWh?-q -5X!78Ojc4&N3@RIJYjcaaVb2Q&`z(q3TR-3(xPKImXOTA;#;oSn+3xoC)2-d@oo#mL#@-EY#ZJswE9} -P;C*tfZaP5*YJA3S;*PF3R9!K8){!bWHDwbuvi#^|qJuX;3j98DyJQ{fcFu@h!=!7w;i#EorwPNc^Gi+F@qEFYpvMy747iPR1Cz)6hMk8o7yGz+_NH}w& -cc9Ah8z|WhfKsFTmUWBb7F_}JzsWZOwBHm@i>fPd*r!MC_n_nNRRiO#E)%9jHmw)uGc%b?*5$zTZ#Dl --p=;!^Cuf0TCcBO?d?2&va`Fhv%9wg^1%!EcUyAexa^|EKJ4p>di;j_;h0A@3Wm#RLN^)lASNL-VaP@ -6L-QyMTt1Ey$8Y|4c?t}-W+LXJrWf@1)E>qopC9;o6~eA5rdeVs3+`x#Cea4>o^?SVKi -uV9l=4Uqz>bcEgwWKVoANy>D=&0081S&=!cySJ7Be)27A(Ad-Yr+-mltK`%CBrwUINfi8yMo;^2C{Qm -J?&6fzStp)W&Gsa*cL2PEL8ogeaX`x84jV0$pC&!EeG1+>MIzyrhP*guY_3CE3rZ&?4xhBZgAn1CtOs -$ZNNr&^c8)NHyR-3vu0a7IWf9gahg?^^xi-X~x{j_vEN2Ko1(h;Slc2DZU0^%whaX*IIHp1~hH1~+2r -CrF1Vu!$G!?N+IQ`wON3OHkys87-yPo`|wPI1dBfz$oD)5J)5Sw= -zogR}?xB*aiou4$P#iD}o^Omd8mHuzuqEZ0y8Clp(GVXUfGrj6N_W34}zQ3y%;G^};O05{Vzlkfnmqj -P5M=JL@ya;Fko%i$jzEU^b9#7|oDWL|pQoZOkN>Yq1(>7C7!<@KhSE7g>O7W9F0q+CtAmC4rToEpkxv?VzBSm3XV9n`*6?t) -P^-8%UXD49c7};}tt#qkM?sE*!YCev39L|XWdnz_4TTUD~BEi{_ad7e(w9@?%WEBs>zn(`Jq(bT$QN9 -8SU2rI594@0IV3+`q3E;?|*{FEbSc4`p$V|e-2UfMDk&g9ZztF43&ew -*;E1Q%Wu1@g$6{arR_P12p|{6cPlGLL)lTH76dpZ`V|;m<4W?Ji-qL9@s9R7NdhkFfn(BFlPZn5L{^b -UN>^0S<`i&egFNFAD-9Vx9pwmIw6af1~m$mXsm7`$OOR}JRuS;RD4jbljB;83E>xn-~E(chCa;SzniZ -WKZlu9vX;a^%@=U<9F(5%^)B$HMH-Th?T5fiQw(9=Y#_F6U^wss_8)*yI>(1shn-g!Z%!KM24Ex}1fi -5XC=U`Rt6`o|DyrjjNBiw#1&t);Q%Op+U*O`=FkiOwD>w#ew2_NL3P>;8^Gnfl<>z7R&h;7>o3-3w>S -wHUneWh&3#%fEG~=3dOO~{d?pxV+0c9e6N`TUvHrRxNRr-E7A;(P-CrU}-=7N@GjL2LTaKkJO4b~#7Q -NaiiJ^nAT9p&%?W)WVpGSl|@xx@oLjLC0*L0&r0VJvNTA>_tzKMgpr6ziyUcKo_?czKEIu+V(ft=AVv -hgm~PgmS|HAUB(0Kg81!Uer1R9(RD>4hXFWV3H;S{A;p~{Fn?EmxOjgan$TqGBrVOyvm`MhMuHujOMq -emk}YPnkhwt&izFq0bB=?MeD0om&csyYVoYiV}60zAW#bIk!w}MP^*nHUJDj#t)aU$nahTg$*JtWP<} -Zws;eX;VI1{VfL3@hzZ;;HPy{I564Ih%dXIqi5zu}^fHw7oIq{912u8(#xWbgAs~52eS<+;5=($5&7n -%oGh!w2>rUUC`z9+Z9+Is{OLt0w_dZyOReoXUcg9!Cu5ZD&_Lq7^fERAZ%XK!1qAU92>fd*Zrl3{5At -~QGKs7RJ_65weQiDK}%1rq%V@VS_GTpG0&?iYZcB(xap9YU;=py`(2?iWz0qu8vSI-dVGKo9%v%P1Ux -1T?nNv)h6hPe9`%^+(vlL&M}b}9oPduA8Wo%Chg1i|NeSqOo~PjN5!ST5k>Tqp%NSl(}sG+*6F5BbJz{#ep?iqhmK^ZoBeQ)rlBgYgl7HNEFd3UoS&Zkw7i78Swg*>r90L)b4;wPa2A)eu(%0~e^gkv!7Pc%tWR?(sr1C -s0E|X5S-%#RTh!uem|V?YrVw_=MEqC^drZTRX;=#Hp-W*|&bgmrmIUnY?y#jo -3BOr4U9l6`r#YmPOhH^Dj1K6;;W9ndAY10uh1e85ItP`aNj(kRWN%Nf1jg4cRAsr;(5s+76qm$Cby3R -P=n@6zL$TskVttL2F?M|4$kT&tQ=W&~efPsvfGnus#w>SI0R537ePs#!p+;I~Ff;n)lCFWi6KzG8t>J -|d+@r1WhdCFKiQ!6L0t%n2QJDrJXi4g1El))D%JrF{mU%g&xy*WQxPTE^(y-oDFWD_gp+*N(Ruq-0NP -5LaTwxg2g0v0n~|)N<*@JkVNw3iq{-T(ybmQmAsrhpiOFinNdP>1T3MG#8N!23!zZutK~|?;!L-S4d| -D)Jq2i2q0-GY!GU6hTT7xJZkjWtW`-!Z(kKIPlON@pda2^9Z<{%H?0&F9y?iL=Zcr%D&MA*HP0J|Ak6Ve(-gV>>!LdUdj-a#z)1x%w9<+)+Fd_|De@AE42k -k8t!6j(!_(^zr8)7vfv?;-S?lWX>SV2BvRj{wfliKj*8}4LO^rhVt0f@ -|kn+U4_UsXY)%+ti+r`CC_>nm -Ccnid3Ia=N#!?Y4IC~zk68<*K@B`DI$1}MJXahg{k$5QZ@YO#y&f%Vjhh-zS0qG=Jy>EZP?|u>`Ydo&J;$)?Qa*`{JO#~JIcYSdk~8)~i8M3t-=!X;aK`dvYeuf6kq84_)fR`y01V(fe1~)w -Lc;JE$MQFj&MhEJ{9XSjjKd++=4-r|X11pU?otof&KZSwoV6w8UpfvME&r9JCr?Im`)nffpH|GnS^4j -c@=}ujl&3fmUzQ=1eXQ$Q&8LKk7{1dk@Jc?(jgiOQ7@ww$FJ$;g=e3!d1wt%h5#uoe}q{1zeE`^9V>G$OxyKvOg!3Ws^W-iMi(}q@ -0YU5tO6n+>(f6noFO+QVB~=kZnB&i4(!Li*p2X1cux&LVO5`QA|{hg87()Xq!X^8548xDE-J -+`n@h#AIykVs*b#zg9fUjZl=3p$bDbSMP;D>&bC(OXCC&MrGU28DE2A51PEP^!qEuw(ZUhtN!vam8id -zo|9^eV)T15-y5=)rQmw+i0Cf5@pJn(@H@y1!abi0A@$cba{(R65T!UlsL+mh=Ib_I3~dx4R2 -t;z~c`nfqm5nb*&xM3;G0{ba06bbsYi;56xsQPG`-1EnDZjRM$)k+(2x!Y1C2r$apXf(^>j$Ec1R)fF -lSxIYy9fy-(5IH^hvBM#K>ke6y1%^HwIyamRy}_{S#mpT#QQ--ajkI3gO$Net=mU%td8u3u6ek^MvE7 -0953hId!_)2O&tGQcilHMqNWFuk=a4o=02dbtT+f{|hfxo|EBTV`K7BfWgj*zxg~7Q_-H1Ead0cOdGK -m^8O^2k>W^dOSdvonl_PW^y?YB_GsC?XpOKs!_@F{+}k)09UlPC(K`AM;YR>^gKCskYMo%UMKsa*6~G -4E4qg|b;1j_jj9gtH_*?tZ^IFz!8F847oQSQ!1fNgr5{%-Jqz6B53%bg;U9AGW$X+q+Msj8!)*BY?+~ -Tkq`_J^y`8Ze?2}xGf}&+A;~LNkM{N{ -6F+iekusMd%>bJ#d6Q3vF0uPBfwR!)ohVYpYsxH?RIt`7TVdo}n+-S9PevNiA+^_z@P6 -YuDAG*mES5i_fWzc8mt*gWHqm!)5Ty~T8tP`9b?q -77j6q20mnD2nJPA@xGzh0hbJfYkIr<3sxg>demN!xMQqAW&gj6<2yYW2E)NZV>}TYmwMGV;%Zc+_g`< -4^icY63rNH!^HM%_aMm<{%=Sqi-(hYm}N#K8wqUMsAhjrq#K`u2Wv- -(nj-|v*(Ac%Id308k@9qn}?e}hTmPrZ+3H%aEDwMKdlTh?sP^D(C8>JXJs|(%?x%HmcE--)8PJ*Jl(U -T{&0sY@bk@3+SuS?!tU8jV_2d25cbx?UG-k!u#nkPfx*-LdZk+uE|&cex -KxcNX`qQ3*dinyjiJ)w-2NLX?}X$u224A6Pr~wi<;22SrYAeW2)bcuw;0o!AFXkTapaB_{v?^QZs-6 -#xJLaA|NaUv_0~WN&gWWNCABa&InhdF_4uciT3y=(q*P`;y4&`7>vrAPZGD>9IkD5ddy}k8 -i?Ga?BDDl%TbtYe{mlmef*>f#j+4B5&(%3?OcEFjW(I?q!2oz5o)xo2nNBV$@$~VNKM&!5-{Akg6^Cg -FA1`FS@nGYDc%F`Bu4E$Sc_K?uT}ZJzi^uSf{xTH5%TlF9E}lk@MIQ@n>Q9@4zW{_qF&EQ#A@ZUUb0q -;ps>FGkNg;2>a#o2n7vo|&%hEU>OL3i67u1Xf5&?+6YY@d*6+`J5%Fp2G+$|^KiV(i2s@e9VN7vWaQA -~(NMLBttaUu2S`N6aO!{hxeK$eQX&NHc$DCIxrX$jptTZniDxQyd7z%YxiMNx`)QcCz%6^QF~nO12&8 -46XLSJ!bV0ZNjps!Y%3RfA9lGw82d07w<*VsrOc92{?opLUNAj)wr_ZwDv89=$#hf7^ZaYWMKuVE1VNf_;>N!!QtLeNFWn5BX4FU;txosNJ5z)y^f`9AaGtVLTV<*>3KSae&v&SJdt8 -jT*)$r?ueNzr>R0lDnKs*C|Np9tGJ>!txiQ78=IS(8z=DQ^ptv6oSq6G_$7kvJMUBl;1uETJk94fV$a -Y5AP>Z>EUwZ-s*N;I&#Hk|EN|bZJsmOr`^YMj<0b}4G2oUgWV+u@9bGcQ@I3Xr7Cg -K371~^=4X?2=|z8qA@C2X*iAdFK9{Y~KSSr(5aJXM7M%SClj5GeRtPc55 -aW@H@u|Qf_}5Ut8|;ATwU}ZDrUgimDHCUDwTQ$nP$8>;Ie_AoB<>O-OrSG4j;B)Oatz#$%LTo_(Zwik -d>#E+Bobs}9HYqbn6pR#Lv%s$b@Uw!xh#XeP77?O!e)z!MDdD%E(&m!3AFagE%_kob! -Y2LqMj;MR>kjlLlqsNpzM1)#4HNN|YNxJ4Vs6%-mGYf2d7@ZU7f7h)X4V5ZS{k)i~Q5kDNDN5`J9)NI -}W8sspJVT{lSIn~X8)X$}O^7YfF;(OE{3e?Bhe4?TmeJA}vWw6K(ry~@F4P;qUhHyA+0Go=k5`{8<0b -})FYGGf#SBQ;%eUZr<^N#U;eg=YrUESE&7`;9`_zzGIKkps956G+(X0LE`@6^cBhWnihr2KKp>nTKFkUhVRPAkSJlGZ|Wt^*wWv!x7gz}$bZ?p7Y -s4qy-AM9;{-qo571nwLL9&xaShtN>lt093v83i4SwYB{4@Ql*rR7^qY3e;fRSnHwqO)j>@?{Nm~0jYA -pWA(ZTf)661ab``O71yA#P)>#jOz^~V2Ac0uc|}RNsWEU=)ooL=+u~4n8A@K^n`q$8nNZbM|NFX3a8;H9kQWlpme!o$*;IvW|Xpd -y1|_R)lQn(1yMKz*h=vEWls|M~Gx4f0D%<@&F68A1R+&s_jk_S;7}`%}{(R{ -#F+W!%(t>8KJ88rFaSB3#MJ7)40fymmS0u{ZgViArnxnxCnx) -_4gsA~MriK>fIcBs_WD0=zxl`D-NUzoe)Mqgwm)_-Z6YVhxj$4`y6XZwGc|iC7 -NjyLUtY04t@dd))*cGYK%}Xk!RpKj5HFxA-@GBWv4X#s*qFfRcJcYg<-m`obnscGq7jQMccgVpU<~61VK$Cz8ZkRO`|CVFf^IFmzd7pr-OBZ1(Ew%&-u5gKXNX{&};mKu+0d~_!WpOYo -^OgX2j>GsOHwxF(zWm8nr=V0HnC+S$o*mV?o;1H@|>ylctp9pYU(_hVJXvLw>$7sqaX~~(QnH`saAfT -ii9Fw?;$>AZvQPz=`H}Sa2(ClZ&j-8_zL-?-jKJf=@a)NzczENh -V{%C?kZHv8@O>(3%QfV~u!R77D$btV(B@?vKGFtq79!h5EITpMu^;uK~Ryw+SVpC>m%2?LuSd1cS2y2 -DR2Cj4Od>hYmPgai@aQ9YNwL&(5_{c~dPSfb(zRIGUp6Xwoc1@DP<)Fzu&3dgM=N8QBjgKgq8^@1VK7IR>s2%9efxk4MM+>0BmYb= -yxr(<6UUV1zm?9KSx^*Fheq6>S$&g<=LG0s2f=!bhycALD8KucGeCE#T*=l@g>G$xtU}0$X{L*jkH6)zaz2SeS=mqLrIgOMpW)GbyZgwc>R^*$%={w0lBi>Kh@UZ1snjHwaV6F-5fwIDWMTOc=J#e#;04=amZ)5+JGQ5b>F7b9^tgw=0hU4!Whmqoa6sEXY3}$gY#u2MPD_2u3>CX1FEskCEsZ0$4a(SV>;Zt{N -ezACyd9I(3r}vb<>JcDmNm7Q0IZ03JYVArTx6J)@+vLn3cR3NCN(rW@wKq{gy+XsN0e -J3pf{+-+dHqo~7`XY*QI%%Q8+ux97XHLlSq=Qe#OoZ4^)4N?o^2WC}^VpiyJWg1vE -1Tiq=-`VGtjq+kpfNiUeiYt>B{$GN42(rNYZs`zI%VAKeErq#Bp$3pv-N4Z^q6Kn-&@&pax4(0E5j+?@~SGC?}(@G!_k&g@GYetx{iBgM3;IW|!XsO -3dmS(kzVVx6$LxK^SVSux!N?%@D-8u#omXC|6^x{xDYGpgb -u@J@bQisfN0X*fbiZr)@v--PC|Xi@~vf1QE6pXY&26*2`s@$b9p_aR?o7IX&+qWza56g8n5x}n8}*c7Ef5-lr}=Q -RrT+0DhsTuP2=}PITSE_@t?DKnk6HqHNa*`W!DG=p=&GOm`S-Sl*z~S}dm;x3gi`VBs+hVy)+Mjk33t$w?@q4Npsn*d>^#2}OBpp(DQK%Hg2KjXAkhx?s{{g^YqI2##Fiq&>3t+KkgE;xCT@tQ+Vjf5FP -&$EUi%l<@yshg>q!2xO5JlGpOKlrU-Yq<*!4sbAJfM9yb_W)?|;OpK*=Mh`KSg -IT~yQT2VAvSB26qbT7rz*xnz~zdZx8^#8|Hw!_4@49M_(VwaN@Htvjd}m`szH{n0(L8Qx)0$vP%&?Z& -1*Z5_>0f*Z4Nnuw5}1W3!^&@SiNWv9)X@4gv*^QX;-#m6p7IcVnzrX$S97d&!IDmdnaQ&cd{1hh}%;Q -d_H$ul$^=&n_?Af`Clk}p;Z^TQ^8&=}N_NaG8w;Ru=J8JqNyELr_|rrY}XbssMnR>!5*2n{X~?pmHr0 -t$@vTZW#-vZOsdt#lJ#Z>V6VTtHRjv~tN?Dar-SZt9}A&Q0LB_LH>`Y7;(+wBwB3+TIqN{coa*LT8bh -UoTDSOw#5TlZXhPzl|Pud}XT_$~3`J5i-cAx>uRMqLqMOebDsPoxZS8uTkU=4;fTXSc>yCd{hm8u>=L -gYG8jjDQcG1IJo9xU@HgRFD2*zS~g&HK*Ns|4NqOP@d`wbmh9N>X!9Ja+fcsdAg$MB3uK|2PsY{6rXPR*0wfGAzTb_6 -0Z8{Ic0uxJ(#=to{5PW5l5VYdV1O$3RU{d+S6lZK#krjySw!pTj#eO@&8jf{{` -lOuq1dI?_Px=w3X|BrQpjsji>To61JjLySMV2PC&cRaSoGhY18=};mYTcvNN2naj(N7C#f?`4Ip%m_?Gc*$B4CMm#+ocsI1-I2^gQ~F}7x1=^nKZ$W6wmIm{ELdqg*XY?M@CdDXHL0;R7^M)G0(h -ygfcjA#iEmj6_@fYR&0w?@&Wa*~+5Q<11R57ez6PuV004oY^ZEdVMw9uVQcON2A4c-ht+6yKG%Wt`%=>Edh3dPUc@DBJz%_f< -GSIGi#i4u*K|Ow%~zv;c%`CS0nvQm>6&_975=()jv-V -NMnJ{5ya_2Amar}&G{Ix1n}g8r{>%AU2D5r#BH8{NstD|vIVBr}#U3F$fSOqjEIo=48F%HbdIGZ(W2{Xj9B2H}TiI3CV8)VC{(q$XdtHE#yUUR*tq={K`9JFwJ72 -~O24WY-AV&u(u`}E*wzbuRLbelJn96UPGTcFv7Ztb5G_H3jMZ4?EYsgDVap5YGuJLJ+ -EE21^dNfuLem6LWr$sPA}5uKvgd;<)4+py~k6=7GR_eGlb-6I%>!Na5m%?1Oi0vZjyg@$`QJfUnpV@Da&0?Q^ -eXV3u@=P6ulu45>TXV-hey&&3qYWgX!OqgO;nq4k9DuRU7A&n;0Hrr8+O}~imdVL77=-l=RXsk^)idv_n&v<_DrdF$M|rvJL5vcJk}YWwdyss|Q34TQj_4j}bXqN!WIfhvPd6Dn(Y#8 -`TVP_}mthlH*t%zm*aJhC;o&USe1_(A)n2b2P&Cw7JqAvt@Ws93bc*{KWID -?()UGx(!h%d0r32rrD%Taej*3AES=yA#OD^bgld0Qe<j5}-!m|GI)d0$6y<#z-(Yv^c0XgIPN@yGu -D>ei{hRiT+LEk#?{Y6R1GfdroLL(x9h8(6EsSU*v2P&ivysC_-iq-t;4|u-c3}>^x3*UdvGp5wM}{S; -E%wf5C?2HWbCn3nVN>`YaJaz+Q0fy413V9Cbm+aGhqXSR?2WK1=R@Mvyz`@>A2# -hP!E<3u1@J3>Awi_`gzwMudrG?@$@$g_0bBAAJ9TrysI9IP3exAGs&mILRT>C1Qs$M_18?Sz~JK*;%R -ViYYx;ZV06xb`J2(^K>T3Jx-XqwpgYKuumY;Nc4fayI{4=fIEjC|T|@5gtd#L(d-93B|h^3~vN^dtTIqy591UV%H5rL)rn7Sa2@({jr+&^6mK*L{ouw4!T8ld_o4RKGoM -Ye8^<@Z9Tx;`!LC)gxUxY|PZn{pGippyu}a@Sf&Cy2~2E;_s&kA)-6xN{M@7!JEhLoSrr|aNnV)jScM -g+EZ_*&s}=@#OY~cm-OvBx@>#;-8$&)Ho8myp1yN~QkOCix(M}U>E3Pv6jFS*2?1Sp`B`p{0s(^&ZSN -cSmb2CV>-EoWc@1mu^*iEh%?dSHl?&fi`jxl()?G9Ed$WbUP>Mk-ZDr`=H8IY$5N-sH!BHX&p0MdIlE -E`=gw6}w(~$ojjM2WSw@B8;puW}aH@Tn{jbJpO!%Vf%mN}3?3w~8im;bZ}MeS~@~kR$+AZOaCK84iI8CLU8Aj?@mv0~X(Jt?5H?9^qroye@!9Yd8 -_4q)7=2s&;JDzb^mvTb7{w}@MO5`KW#t@;;h;fex=j!9cZ|h|aFRFG9O!8qs|Y)4FHt5UDO5)MPY?@|5WBiRq-(M{I_)!0u -mVA^LYvSrihpU8i_hSc`o^N(I!Eg{5=k?o)vLUfo4oEhSsXQ2R8L>)L-snA{`D0zJB;hFVHEC+zxZ=+ -)3lpHjxSC%2_nt*ko9^jB|Vn5CURu$QU*Uu4yh$8Ra+q)Yq9b9vPF;!wLCbBW0v3T$fh(M!*5wpFUI& -3-5&$P0r~>INPv6)voAP_QrB!LtS@c=5pjViq16Kz%NL(5K`4m2L(d^G=SML!ChJ5L(s2tCv`b)g+b* --?y-Oj|5fT765pzWUXHE?9evS0TTi&GN(e(v1LWlm1)GSMJ^B)$gJMe(-OF?LQ$C< -vrSWzy}>2*;in9Qh7>3AYy1~0MzG+<7qh7PQ0e5p?@OC+6Vj(3tWqfDBk(N+3VYm?61;N1f}TH3pV`) -Twor@}L?PCZPHGHi3w$sdSbs*0`ezWek4-Fnh9cLvIEm(u#$W(xE-*#E%rUFYD&oW&`0Lg=2fX`y;|L -5aDw`UX*V&FO(T>e$@ZO6L_CVdVXj`dS}*_}6)QgZaWmcBS`2Iner16q%da^;I&sZGa9}$E|iGDN*DM -RnS#xp=?-@f~}0 -cgPltHk+VQVNNuDcCg+|>iHGxn8ldfv9vqxw>2PtKkc|5Z|2Z#c=P8ioEFN -E~ELImns&PESG&J03HkST6r&FEolH4N2qwk2poxD?!Hok>A%@DO*Ueh4<;Gn?GW>`{I<7l9Fph-h7UM -js7GR37&pC}J&xrQdwNlsnc#J52Pp`${LIqWY_Ygzuo)l?O^aBL%;m_k5p`P&fDFoM=98V{AX2kY;4h?Y4e=81%imDP^2-*?Ot|Z{l_;SS5>I1MLmXc+;qfux=(F3OeZ7Sk(Q&fTvS3B>F8k&vg-9;WF-={&%p3GZ?DAo;Rq27VU -fW>T?o4-X{z|H=^#-{eO&Gh(Iw3Ky6I{KjX~K6**SEjhn^OHj%~sHk=n-0h1kkp -iSi---JtXgR4!jwKp<>noYGtSB0%j6^SEmu)X#n4H&Gl9R6hoUyS~hm -!huqJ<;URKmMdAU*kBqDMEX{Ch1@4H5G3LxCrg1a?15gxWoMmm3^nsCHdyW~2?_UI6n8~+e4#iO!kMT -&eeY|;x_Bo)+3VOy?r=5aKgDjeu)G5XHi)pSPqEW@A&T*MUh;T)B&)GGPs -V2UVH}4wUJq^h04POgK-#3ghmGYU1++o)%iJ;!bShJFECP@S0c2L=4pffTECdM=wNfY}CNfWaoGo0al`eo0;5 -_{;YQJ%i+!*QHLOxEX;7*;Qf&BZJHosL+Xfx -Qg{nkR;RA^)jmh}DrCO9#47X%@0Cj_9I48?3DFTBe)e>}hdBGh(5_1gv`5&Uz -mU@?YERyQU`oek;`3RA_6|aKLmAqN>T5Glu@jK^$EQ+QUe2{5<^erWd;mwTMBDkRRz+M8` -&b)ab@DO>tv`}q7dNcuE%+GMk;iEh)AFD^)vqyAlmwNQ%o2Sc!eQDgNgGNZ46>?8OQ}X|BGX~5%4o7c -elt(U2aE!!@j`(s*1Ull)pD(=JTz1C}c4U2Er*&AlYu&xxFY3>sFt45LAD+N7UP!pV=gp<9mQ2*{&Gz -=z6K5f^Iq^AxtFAq#D6kWGGXv_P7ocdp7Y|eK3S>ADt;TlL{?{5A-#2cSdb-N1b+-1m+jzQG8&6iX(O -8-E>v}Da7fo`#21Q)f_(42(8r#^=gB0qtk5{X&(gw3f_a!SP6H0Z%KTz@~vU*PM`sP&qfm1M2@L68Tt -Z&Ni?f>-pmqw8|NqCc9X$u`!pzo&UEk?gN9zM_kUeYg!MO;qk?`T)&#E=Fl`_5qvJDVo;wxF>F+O)_v -2M!#>U}TbNY8mfsZM|18ISidQ@13U$nau#WK6%w{Wvd-t9@m?7jd;um(Fxk4s+>!W2Q-eOQKQ4!6W^J -DHeN6aj}hWi^Iy}CBCPl&O6Jp<>ht|fc=kGmpm+W>q|n6(Z!Y9KL~gSgFmYk^4l#E?{2$dD@?k;l87< -xzH>ZF(=MyYD*rn0T@A_yLZYcz=qp?8hK)<66a{+3o5mOiCJjKHcS$-)LKN_Lbk48Eg&qHA2{{c`-0| -XQR000O8B@~!WE{%&KemejF>^A`b7ytkOaA|NaUv_0~WN&gWX>eg=WO8M5b1ras?R|T98@aLP|N0ad> -3T@o6h%@pEi;z0ipQSOITL&C*hzNR>+m$mrqnUn&0#n7$Y$?n->Sm<(aokF9#8Ht_j+s+C=?2XLZMLj -VUNAamN%>9{Gwp}Zuc4cKFiMMF*{2qot-_#eoQ8Dn#WUCWbCK+Z`eAW#w!*EnzJp(-adDNcKIfdTot@b#Td>h+wl3DIcr;?kVwtT9=uvTK)eq#qD_9|N}p>yK|sNJyHI-Ouo^ -PSOzei$WbF)D7BaSrvG@Q-$Dr&!$#cNlz1%dmNtp0{@5>q)#U*xP6kzh14f6|c37^4zBZoI-8UwUU~4 -S@@H_1#-}9HohrW$Eajb^HW&ds2aKnkOq9&QD5d#fTs -o5dQ*xfw>~N47XRC3aXL++6tvS|UdUSggu-o%lk;gV{!M+k6rDlBnwUW_$&)lMqI42B$Cqtiv0 -7Fo-bNGu^Wp_NZm}oOgeze96()w+SurR%swi**S{kS4#f9HfpD9_Rgt2O|fHLxy@arSpfB2r$WiMElf -t6$Ok4ff@vj+Zm5fuz))Nad_kDw#9?+mn$ -x6ez)bBpcXpg!Nd{_$NH|Ih#2j&msARxG?^gCq5?Z&~+x=6q!DGEG7K8*4MrpGnM$3+TyJmNxLu)n}Xv{mwIXbrG*(GL$U7PVypPj1;YA;v0 -~}Xbbo(u|{DMC}*5Kz=~QjI+JXj7R{DDbU4;1Rhn{WNUdKhryC7v6DH8ApeY9ODB|Tw&z@xX9eXJZMn -YyCD$u{KE)rPT6VNiKtsA~c;`x*%S{>Pw6CwlcPv$_DjXq>WG=H~(r9Vl}|D0SWX)_!DBc4b*JR0Tmt -jK9G#4+Y$_(MEjf=1GKVbx3Mb8!)~4E|U#>eVXAVFk&c7&!zvL*pr`k4EzXDB?ms8pXeiM&kRw#Voqa -k||$YNz?`MH_8NqY#dJ_a2sGO*ffiC)RYS#Gmo`$8JY>4tX9yM`3<{?3)Z|SiskU&V7xxhJ4^1BWUKR -oB+u7zuiHK9x3pRiU(Gz8&)U3Ots1jn$EcsirS;=q)-dN_DB#zQXo;6GEe9_Jev;jgV=%Z**+tw5kiw -KBY9@a;1b6A*yu5#NGVHQ@)R5mjD&+MJjU=**zXp3+Au~||tD20Smv{6IjBYawzs7hsiEWT3k!@K5d6 -1F2C7*n>cFunK>(P&|-+uq$2WbAVkN+O1f5S4VEY>OC2mCdfueqz%)PEHnp`8kS%jj!p4mT9E*FY;Jw -h|rMIS8_E<6;rzpYK98JznavC=reGV!sZ57X|QbvuqcvA6vBbuxFyC#AL$EY#?YJU -@bNrdog1@6v@v9-f~4>`(0gbT3201qp0U>nS!>#6Wh-O!?EMz(>u$jAvV6T<&Xaih2T3yLiU5lMEYpu -4ANJW*GM^iC#>jJ;Ct$m|l*VFHH@-nljMno)^y6phveA{(luC-h#(p4E>YpSRSr?6!QB{xI5S10X{aA -G7G3}^;mxNZ1m2jg~>iUVcr(@Rsu+ -$3p#cOT!?9vAtF3uy-p-M92fTKI|gP3kF+>9F|bY$0pnHHmns7Y(pNl)nCV!C=LG2*%HC|7PbWE+G-Y -Q>Hhi#TX{=&4j`Cy@&0#wn;U;~Yq&!#;S}+DTpE@Jx>X8Svk>*wKQPZxSc^dk#7VV+hyTi0rleF%{zC7Xq(` -yLjqP?w{i&d=b-xmbDqIJV%Vz5T*^Rnp~HfA}JfDcq*qMNLMtb^hEa1nC>O>4AofDk -$egkiQW2qR!zY(J_Th%uq#dDbKv%p=5>)}3?{{Stj5Q-MS*@)X|)#~R}0%wn@6#2l{4eGZT9XGbYY%d -xtiwb*RGt~;jYZMb{*|1r5W5-X?lY%sWpQ;YYp&QpV8ZmWN+~0hjO5Xa+JeDSd6kIN-mm@L`=aSQ5k{ -F7}YQX5PYQ%dset)rExrsr&2mdnVpyihN`g+k#05)ge>?m2mRb)FIj)HAGiB1g~cXIV+KSRFuX_#jF` -aiJzDLW4wNxA_?;BTEJ>#_h9bh`%6rhlcmt(iNz3D3)-j$VfX3z73LAosuAvhXiqn9k_f9SK3+ghM8X -z`La&nK1O2RRamH!t;*G5QL7xX$=tQST -@)CQ&{AQM$~sy=Vh&*nAUVV+4!_OX#XKu^Shm-zQ0Nt^gxJ9KpQUxL!NOh}gPvtFwu%}c&sETUEN&)A -t$#*pbjcrOdXY>>v5A3mOgAxk$Yx9H{T>j(qDx?3+_zNE?3EMvxur!{)d`;-Y4NJ4($Z)a7*jH@HzD+F4h@ti)xCmr-<(ee^UEb -vGqnz4%q(@4snK4-CSlA&X^JCgNn;-CLXgZX;a}CGJsXkj{OvD+6C@dtXDjt`#7z0h-=m-|Y5%}=4PW*)RFx@IuOSh>Af|ni+ -Df(omKmIs+_xAOB+nkI+p+0N(X+DpZxmqaWoQMBlhY)gyf6THa2G3U6`uxH?@x=KkXrXrsj_PE+TH&@ -3Ix?PTlh59&ZKzz&)>Lbq8#Ig);$^?A(hce>p%Oo-y@p){1{M6dCH90l6CX&ZS?i__Q^0 -FnmW_8#)EpbNWZ^)J|V%XsF{dYE^f1rUtzt7ps^g?+_I~7Bgr7VXuw&$j({!v!_#2eKu%}o;D -(2q8SJ@#jk2*$AE$(jNKf~r++8qfXKtP)Bb7>7)!PIvZjpCWxS*o9nDbS7WVi8Hw@w^{x2#)21!8HX(+x; -5aiufkQ_Ob*oo$)G*rW0HjWQ~`ckoSY1#D}~4Bwg!S;I9N~TQ2v%fr%b4+pk0^EBN5}$lh)$WJ9VpBc -*N;e##9oRCP_H_o-ns%Z-}7aoBjoTN*zM>_DW5^aiAamJ?>y8xVBQ#p#Y7n?-Vs7F~Mm7AZ5hhL-K)C -*fxq{}4v+<0qL^$pd*-e8{}&;6!({@T?$@k%+x?e0e`zb;(ed>%1e;hp%&mEVahj7akU(2>+ -$Z)rW^rV$bmWExQi{O@WSfyuWd9lCs-dj=Zph!BVR7f)AHyg@{IJVT3a~^v_BqbJF`jv@Ma^%R7)+s} -cO6-lfg44XK`8Sy+Kt&DL87j60RAA4zPa&Sr_%^CQRxVLn#^V~ZGQL(IZPKkwn8P-4t`lI5V)S;{R5<}tqx&!8`V0y$=l3xfnnq5SoXUS4(8tE)>o^yd> -4QS;my6ND8h5y8(9;`UU9wy-E!IwmA5~XaWMogg`Xb#%$exudCtlbh9@inE^tQKa -7tT(M{kRafyGVIi(fU~Qf~j6a5MUi=RfSiCB3xPHaB%~;N}DH*8w%;<=J4(=c=a@@-|9zpkVO1?}pr8)UBo+Ps*o?@0>6PG4 -lyKUSSDZR=Dgw6XBlZ$xrnKCS-METu`$g><^W7N*ny#U6!-F-A=P57-P_rv;3995grBp#{dZ`YzwcF2 -yDp_4k1lcXyo(Q1XbdnpQqV~+-_cv123<44nn=ht%9tGB7n$L+5>ay)$`Ja}z1ps+r{@6c?^+nbJPJE -77ZEQeUjlS+ZVYtjmd)OTDlm8^-iL5M)0uAtovS0Z)yELS2E$aHPpYFrKBY&D*y6n$;xHiFj5MGPI9> -fz>DvHX@cSEc3zuV*W@axA=Dc=+$~3NMhzPWyj^k{~lbYZBY^G0(R38R;Nl@oVqV6nY{#>3K{@z0A{5Ly8M!JZSif -8y%XobPcWr$xh{AH=K|b~Im{$Hkb?hwiy2LMaD)n&9CIi=5=Bp{a$mS4wwfFILt}0kO+8DENJyX@+jr -U2kxQ@&_%-&APQ&Q13KXiW0_>z@;U=AVX>Rb0%e0jiZLoE+s&npzCaaY5g9CPK6`#fhFqRt$Y#O?NOI -J~}()#2__z)ry7))n@dtC-r88@}gh&KXe(6fJnBkxwKK2EKek*gT7NPzFlXr4?}_8-kk@elNE15VINt -{V}fy0SR>KSoX0`|)l)YTswG%@BVV{=aQ%9X2a1NQC)MusfOv&%Tze#Ri6yh9%PPd4}&KOJS{`?U<}` -@5aKDnrkzHBN4P0dyKAT%;(t^?rg*yVNVYBaoN9$=X3mTzJ^xT{4nn7A_08{pC)b_RPqc@=i(Gf3K#` -`4T0j{unRROBJxIC-@hJpM&G@Db@u1eA4lJveSh}heFv|?FVQP6nq@Uxnq3UyXf9raEY98gz0LG@7G!f`@qY#!m`Li^0p2|$D<&p{aAYja-moyMIpxv(+Gm&Iv} -Bt76s;)S73kVLQP-tZa(b!OiX~C#is^BR(GmmY>aXW6*Hxl-De<|LZ}Zwa-rb-td>Z!G|N}OzLbr2Q_{hMN2)~s@KP4QgCH{>o@jJ02Qz>u55x;IO69%7@(!gxpX)xC>$8I>28-j -CAr$cm$%S64>E2W)eBlV#3AF8pFI(bn6m@A^0l$KA7B$N)Rh`B(9hEP@eVHTMg|y4iB*^8W^a80c+ -VywB`3tDtI3!(c~iLiI-oAwgLHrM^jDR$wm?3U^l#pbkT{+1Lt_(UT*XxzhBTD$cU9Z3jLB|$qGNoUrZ@B(oUaE7B$dj(bpg{pm -w=Bi#42;RO@MYuypot~w9`BfD(H4x+a8+JdN;e=}uFbJX!J84p9-eEf;nr8{9Q%1KS{V1>9Gjo7&rz< -=otx{em~?(9cc#50@-Tr=%M>LrTf{FaTcR2->Ng(x&1UVl|;8bjiWi82P6f!hA -D4K`7lOf+nJj!8gu7~zYy$sWsEi|2IqyTJGd=?Z>rVKUq_A6M28QPT1!R}Bk6e1A_fM>3-OfZH_}1pf -EApZvTP>r3W}(J9-wXR!u(cLz)yHKCcN=*6a -4ji8ZBewZbwkqqvZPU=f|(^PV1ieA&?$bcU;Ur*!mrS0c)_(5Vhy<&DcQa#LXtBp^gLPLPI#5{0}=|C??d@#Ta-V7@`EkPA4B= -@&DBn)Q&dt=4`=}z`i_iivt1TjnBwgpit*cI>uy3!Dn~3 -&Mxb7>u*TAnp(OG!FPiJz=4rZ1k$Ep1cPrz_P+-6lLeZ#7WvATI&#FmGR$Gm9l!l6+SI@z$vt&UIJ*9 -Aj6K5?(n9b~aYyvUb%GGsK#g}^<7Q8sgz65Wu6VJgSM*y0HHM(`~n_eNuU6i>K28rnFT2^fFITW^$)b -`>{hX{T3V1S+2D+)Qe-tSK6iL#vL+4F4;*8`>(5mQ&Cks3Fw62VL(=pIaas7&sCfo+9t5eLFu=?W=!# -@mg6bUst7xQO+K9Wm$Il*4?yH!cgDT;*MN0Qt#SKb1TA$PXM!L` -FTWWEQfZjHOc_-A8qf~;Egel(%z4!#{7)|cLormD3k@to;$@4z_N7(d7Gfli1UtU4AUfXl{lxU=qCVxUpyW(fXXAv_2qjyqJZbS9VV~qUPYImF$iJ!RsMZQTpu}U{;}G#Q&*e3IZki**5BsCFwmjhJ+g% -O@_cr<6Y#h(CD_W$6UWaNPw*}83_In%2z1)Ia&+Ss}bFU2@44v|A(IKk-vPN}XTYS-V(xoiugFtYR|G -ZC&BqDN1=m+208;^wW`y`l=gjcGCPRl5{ULTF4L3t05FO&*jf^fMM2*@rlu&Z$zRHsw_}vQSk1z0PQ -%z`X6ND9rkB6h*wYkfit^I8Soxm=KF()bP0&#Gacu_4W9Wn$7c!Kfx3(!*97v<2*sP48C`+s0a^Fpu0 -v+TOxES5Vo870?{2;f<#LL{VEJV{)@W@D(#vhQ@cZ3MVS=jvhOf&x(53}4VcLXzFk25wH`9?cX#i8u2 -2$z7^Mab7c6>!2D?w-g7PPaE;k@T+(P_H+H!^oqHj*u^RA-c!b-GCqfdeP#J{Ql8PgeNm7x;K= -rWZzjFV^=d*O0amPcjAd<;F8s*T7n@yhYPd<_I=q*tc}*qZP-T~R&cPJ>R_6SJrL;X{xxpC0Un&!T++ -m1;?0ykuR2G0g+Nr0A-^6Q}W#5yhe}H!F+Bs3+|Z=a2BcDJJd)NnlPo(viWupucBvry_ki1cnbw*Gg) -PVN@&A8R>XY(~S|hy6GRxb3kSjxqypQ)|?5efMorAPxs|)_tvH`4C-B58B%OYl5JZePM!dt$AXIKF$j -#=JGcx2wAOht#6V?sQj+g)`GwWNQ4KTLp;zN+Y_7)U@nBoL1L3ZYuthbULUjdIO;T8w4%_4jith8YB9 -BZ@V?yM@YAU#4{FE(#i_y;@594wG;WjgGv6L)w#24UG8<|F;!k8}5xIUi@CJo8Li?X{9t$ -4XD7bg8t9iM7#n(B!gvi>1;v%KC8oj?3W7ds+qwd+aM*2VGh2o4!%?N>#4EPU8uW2*y+Ev3ehi_8Z9G0iOe9Hm}R3?B4Z_Ykda#soZ(a-8g^3eh@ws9p4LCeK?A;U^ -4BM&WH9#>&&0*HI89pT(pdA?my-V)+c^}8@=Qpod|M1x%>vx_J69UVm&aT^$Z^s -ltFl#vMFIe@Yd~*!QPdv1ze56miqBa{>Wl8U`)7xe1k`lqJwc>K)2Y|KZT?k%kXpTd`=TYtC>~R?IJg -T~VQYSy#?de}qit7{^!#9&eq(k~P6ywnrS$G%oec5fLWLh_lxp-YDyIcUy$s9%ehI5?j;Jd0_?V!AO}I!$FmA -X^0fb?d*`ORwdSXl71e+b}O3S(#|7+eIjso!79S|G9()FqyF>WEoG)Vwx!f`#VWZiSIK2m#FawRL1rE -RL!yNco2c*JfxRc;+15Wwqy1!gd9+WUqS1VR6`vLtm*AV|GMe?6KFstYO~G1+aYoDJZM{f1KNgeesa$jP)8~ -lcKRrfpnieehi%b_2|dfZ@>TWgT|S3ufd1gxoKhjc%`hc=UAe?D)An{omuT3luoTS3V26kRWa -eqzYIp@9-lykk@QIS7&U3F^2bKG8xphqpwZGiD$cge8qi>+A<(2s~hvDYfy3`a?n?vn=FvYtiklcYeoh$RNRbrgCA36p_27` -ICC`ko()&bR|wlpGfhL=B_48Uwps-1d*HX_kfo?b9u2Ycee1yxZo#dvBa-uJhCtr?zpNwjr51r}3yjZ -gJamq3%FtQE^(s58C!*I`$?S>s%87?-$f-5Wu$M{2SE!HBl6Vr&oQv`$QT&)Nd4An0TM$58tj5RGGyd -!+8yQniX&gMA9=Zv=nm{s71+BJB4&y&;a!x*&g$1&^kNF)sqVNGE`J+KZh=aXKNK9JNPd#rbaGyf-CJ -@%~nrk*rE0y1w0uPm%EM&v~758mx|@mOwVq>wg!C5sq8b)x99drNrc5n+aM*twt!iVH% -xbela#~%<6)|NaR`zLV5|bjDn@Hifwc#m>(+p=0QHif3nmy`R -LT&`9y0vTFQ+kAbS;hb0HW}x#L=83vM)&R|d&X}D$o_roe!{ciMz;|jD+NN_BrXm_yDdhq%<{o(+V`r#Qy{hiPDBTjYHieF+PZeuDZcU395=rCQv(l+K(m6Lviq?&J7e)yMCdMp|G`}iG* -wy_E!EgeRq`IDIFdeu2|SeKHV#FJXKMt@47V1=to9ui`hwb;v~JxqJ?!4rO9 -$V$HOBPzttPTxa?`k23`&XQ#2Y~#nETz(rDB$Em9ejY;Y8PlOpfqD4RkDIAV$N!dz!R*_Q)p-%GI5_- -1R{+_du`op_fU1D#IA;M1)=eYZ~jcG&Ot`u(TRkDs>O_(}!~AGx4}>x&#zw1G=>S(~lk6}+<2TL%KpX -2z?qfmnb)Z|$3EuV=Gll2GJRhJ*3_VcbBe-I*-y1PJA=noU7M0Y2qDrRXJ|=)#DnUjZOzVhy~KY@;3t -d>j7kpGTmCf4=gU^Xxn#sOlZroR2R}(?vw(!q1}&oBCT3pa9yzH>`YdoWjO%A>T_eAMzc?#w5fSiNy^ -s`{uJjiSHD|voE=PQikux3QE$Dud%91*h30R*(TiUI1EJNpdp{KuOEln{Yxre2$TeP*u-nW5Uc#F7d( -`0mm@UMz?VD_J5JJQbrZZeuFuj#i9DA_e5Pb0gFZnWe&HORu>V#+i^omhvGRA0?EIZ-2cNFJDbI`yQ3 -NhUM|E3XZv1yoE2$O}%-W_D6{d@cJ0C&GiSDRdq1&g2$b)UqT*>n!zfH;$kxhCB-hmX1-%uK@A{S1cJ -?uT})lZx)Re04(4UPZJC8<1pM%#eRl2JG849OVHDVXMR*?e``CNeMY@bIvEOWlyPYN*Cnm7Az(X(>GK -KfA3IfEp3gVlcK(XA)}C^vC6nrPlKpB~D_K9J-LR>v)bvyPcB8d{>|r+sLk-+>9x`&9gF4o}sA(?^ -oEIzaSp%i8E4OMRTqaB%oD2TRC44c(e&W8w?#_p~hvAJ$afyhFJp1n>f41ZHTw;K -6o38J$^97&y3fp573L44N=iR01keTg)aa&B#P*BoC|1%VinQRqS(|<=$-CPNC@8h3>EK&Nb$}IPR1Ew -ubw%=R?T{2S@OaW{OnJl0mCLk!NbpwS%o0_KQzZhtT*s+$=fEJBXUEpn`6mYEp>WjIbk2wDqrd^kRZ -?^a+wB=bF3awCC|%(gxQW7{dhe9&cJP0Q<*kc)N%ST3EyQgFxMi2MQqwIoJiYD@S^t^Ng2(^vu~#H%u -vqIn7Ae;FDzX8xEVJ$T#Fy5SkC3%uY}&9W&kXlm&WX$!-zz0_m52b2s!7@pZ!SuK>i&)z4CWFD>NH}2dsj$ulFIdE<*RFnio3o@8zHixmg6~Vn -2jH?9!3I5vy>hjXEOp~;koZL3Zw_Y&EC5`lzSp*51~cS-)9o}goNCt%*I)?o -)=;SDUg@j!F>AM>BQ8B@AT0~jT2LcLahT+9iEX#ruv(Dho<534|i;8((&&F$f={jYLJpbWO5BaT0z{! -p&|2*15Hsi4yL1AZ|f?htC{QNa&Cur&G?$>DwGDOROveBtH$E6F^cAe&*brI=={}#=(LvU%4izDeNY} -DGHw->#FeZllHJH2n2qa2Jd^@BkmEww2LV{_+sEH(1=H}3k2)^@*s~Mh%D~(IhT!}hrCQvIO3O&-Ha0 -~X>kpe3HoV2idDmKmUe-Y`6T%fWz74!$_|1v<)t6o(=V$n0Q1HbCd|gQ6gXeL9TLgap&8;6EvYPUhJ* -wDQRDJ?)BU*j?bu$jcMX*rf!8UQy8gi^m8$j)~ALEd-lvd2X<4&vjY0fNvlsC_@+fEXb(~#!E*H!{k6 -?T%ZATU*GYir;F;ccf&jX|vF$2Rk$?7dJaQFk0p(f5taN2gj< -8mrCGP>i3UCB?XJ`rf|5;*y53OoKzFc!d61F>UTAh?Qn@+jUr}W%gQ)nEGza5b`Jwys$X78#2&iz=G% -rsy=PtLmAQ(?94ZR+2FDw@e}3HQiBeB1OFiv%aOf%ykp$>@BZ@k1 -C%^g-)=z8IQ`SB?_R%&#uHeBL_9QvS0NgdC>tJN#dw`gE)2Me!54(9_#I&4zYV~|U2q^QXoP_@E&p!N -2s{)uP#^+ZTm~wqB0TWI#hwPC}GsGX)(x9N$X9Nz~6FB=^AK3KX -rJ~h-l^r?{TMJZQ^(Kb{DHGG4^jk+>ylT|jAcDDA)Gj$9B~26rFglC>Qu0TP{Mw#20aW{_=HL<)o3y&$qfl{ -l)<9nAWin`pU=(Q83U@@LHV2J9*E(x_UA4gfPk -^w@<42tKG_Fa$2v-6qO+Ne)}%Xf>6RgTbWxm@oln3Al%i;jqDvqd=S&;zG5+&9#Sx-i4SZ6FS}}z<`L -Y)^tXNQjPgE93$Rt{y71)nd#?aW!cam^C7H=>z0h9TiB9gVh%J(Vsw$nM0x?Dl8H;e{PoA>jW}!>XP5E8IG$zVJw>aJlG -r?}MU0*@JJ2V)7Ok4YAfG0kWotTRwwf8d?z3LG# -&rW4r&`@VVg7@-@-{XU2y39wrXPjTg7uy6Z?rbS>_QSa45ViPbP@TfP%4dXgkt~u5FEr`)(Mn@zgNsL -Z&kF!Sl#7$R@vnb(l~#iJ`evx_jx${kAPwb`xRUd_E}Sn^b^&=z!{K9=tby+O>|$uP(^yV;L!0)omo) -T`Ra~0LtW?v9EQ+?!SCdKzqX3^iiuYwEA4-As3wvLJ!b3XQ>u9ilNXi#=$FUgZAqo=m~BqT*CiefuSe%OuSqPq-16AM7MAcPi -(06o(9>Mi+Pg#bp_kc)vI#_L@;01?HD~$!O9B@8v#L2Db^j%?);<8t5k480Dy%v$*r5oM~aK&3T)&b<9HF!Pd%@;<}XjlDNEzwZLbVJBhZWZhWb@iXDm;pKGJTCFTa2AySW0wrH^G&^~va{Z -IcEmV%5>=%zRe}YbGp&BGJ69{fz7%e#?G -2j@gdmpU5ZM~9nj2|r-sVyY@@&&C0Zm4NhyrI -;#|6WDR$fJu#d23pLPP|s5hQ_ -CTFs%(T!ol_DYNkZ(BSm0t558cW`&L>T6EEc)xO$|MA^80Ztfp^q`kV%!F)QB -Zo|iSD?$Fe3#Ka)+7rN4VrsB(aQJPono}JZDW(E4{>TiYWR>{9ez)j)%ys)Um-RO6_z2S6xJRCO4E)J@~NByTq-DiWrQ4bdV{?q6E!$CBUpFP*J>@T3I#v9ULoQsm{fZ}L^(+3?~r^zpC475%RR5;^iipF -#m4IlNMA3nfv9zN~yr*;Ro98?ry-9h~g!bNZURU}sbrep2Hx_PVy -J;iEyd6)hvK_D8Z9sjKcoakz=l9zc)_}TKbv`x9NuA%Qd{!ux)z~UMPn4l)-F$Z#8UaDcp>4iPuY9?a -zP15W$GRHhcFh@6$QGcP$u63(HygohS427SjpnWIG$qsl^2D!Ha}M;>!Ns@WJF-AKba?la|Hx&dqm=~ -)y*O@&0@aQ2H{34vAc>S7jnFsvKT)TLPyY|Lfih~0f@Dp{GShm{IOyeKDEcd!g{c14vt#(VE71yX5Ie -^k6`H#=E-%D&9e1|h$gA(pvaE&h}N7ZAI#eOZf(l;6_Gc4SVKXrz_4QCM}*tdjoeiYT(8&HBiFyx%(3 -VlcU$fZGJ=v8B)SYTeQ8K^+3qbN(dDVgeM6#e=wS&W`i9PY?TF|#MXjXp8wW(M8ZB$6ZZC^%2q(KR6t^I$+xvVE-Ks-9kI7Wa!s|LoXTs%2-S&s6jK#( -o37hErK4Qph2)1Nx<^gZE*L&9l_mYdLKm&oS0iBqWi!+hsN#YK@VY}HUPSZ4zaOvDXP1!FdWV{1G<;c -pOt~S>+&=B3^)GKEdhInzB`~y_X77W+1s=kbK%}4i{&{S^62ORayYaiF`i)L%v#L(1TEy!I}oGc!NTk -D1I0S_fYjB`6xb;Hqq?dG22ckgen8h^EgW`-ibiGM7KhzsN29VsKkgOGTUn7>xiPZv1LnraCS-0*kiY -WWm|$^RU;@`vhlvSH9nTEfEKc+FN*uRc#T+_sApkj$2D-+WGO_9c}T-o3R`fVVM*XhSL6XaJz8ppi{{Tw)%`=0zd@Mg~n -{4Y#0#C3vgPc)v#ycXqd%GKTSWaV8>yzb6qn=&Q_8&i467Cg0nPhR!@&)FI;HNe!iGYo^?fsWvBHqL6 -3X_oeOp1QnBtFs}{WRp)2tw+;*;*G)$JvYR~Z*2#g*Hs~n-!ss@t_-xGR&}g_+5&~%u4qHa54MGp*E^ -+IeB8y&b$1gYyisM=#VW_Oq1|;=zKwAa+0EZK{7exT*wr6AcM;EZsUER6aFNHI-Qn<{e^_0j4`q(ZnK -42Whb}CupyRrG5%A(o$&7to_r4Ut|cdxF9b$*QuGf-(F1BQE38ddKwp(@=Z(L;2Vo@dAfl&4K=*w -8?PI(@iyBmDPN`iQ6~bl77B9-XNeiuc^RucR;=DRDZ%w^n~59&dXf7ytkOaA|NaUv_0~WN&gWaCvZHa&u{JXD)Dg?0x-X+cvV`@BS-L<+`F0O0k@_+q!Y -yI*HTzn%KG6-R?fuE71~VvzADer0i(h?%#g%0e}DqO0tu_xA(5vZ7h+%02mAggTZ_}=sxU*e^h|yJ)U&f&*GZCdrF^}^CDr6UBl%&uyz=@!X^Cgxou -K-YyP0HISkD>TjL`9K}k_aG*aW-1a;9@?C2o`QDqEBy -kBc%-MhGQ@LZj(o%>7UWrpYXktsnrxM}c4$1&jmx8X^Ld3I2^~aP!5*G%2n^fHF=H{KcY##|1tf#VOW -<(LBy_QN+_J!bqU^G~%jMRpU@pm^TXtb`tOPjEKo)g;ST#Z?4DxInx!_Mmydb!3by$6gdAP(lI|G0$>p)E -#e+JMry-I5~cG`osRa195mF-o88j>*33Tm!h?Q0>4|K_~G#M+vD#~1(bNVe{}kHar{c`AN^f?cX;#?F -@Es3x9<*4PQ>v$arox#>%)VWp*TEx@%sChhezLt=TP(L_*A?;d~_0zzeR%r!P`o-kJwm9j0PMbayZ`R=@WuD9_uq-P-@ki%d~yI?cnN@y4v${FgLV$y92}j(h@fS7Dh~b -%KgG$n`>$V9bNk=J7~WAIUmU;v`@6$$zC9J+9>0Eh01uxZK=1aSzdqo`j{c4-@%8@Un^3&mf3yG10o6 -MOIPa*S?BNgJ9?&CbaUcHw;`H$N2yye`_~`T<{6yTG9=|))m47%qIS9r6yTcPi%&T|D&;TM9>KqdosC -jh2a1iO(bAX{|2MXa>zCSsr`t$N&|2056!RqEz;^b}(2C!%c1Fx+_yL$y)y+^ -WvjwdDv<)jaWuJEDJ2uuq0RkpCDwu(HvNya!)z#lL2C?| -$JlCxJ{MJ06w`;LeKIAy*-HUtBbEdp!Ifiq2)$S&S4VXo8O*4DQ%^Rft=z>-)W8;?cnTVR&J(tgPD>G -+eD2%yKfhg`zeLWq_O1%@}<8V*}ei)gxF&iyPDPowb|#}voUK4}eyurpjNl4&UxbLb`3-lg8}4gvgl7 -;dSbuM?Q;XgXwete)09h@e!%a0g8{TX7(5$_4ESeMW`J#?mDqt-VLjYp -mJ9q9wvY4@5Q1CaqRhI0%$qnbd3N1inlL)MyIWfVK9d|ai+M8d4Q}FeoaMcWu7Nax9V1F%kB+AEtEk{ -Q57e4dvjxbFG^(ix{eZ=cl>@HZ5sySGY{9?o!%tca_X_6_Dmjx$@8&R45H+s&O{@D1xIHz}BLX-fOz` -VYN3KvwB(PD>7q~5(JDeK7)s*Ny6K8FD)DA^E#DC}we)*(*&TyF?VbLKrwUi+VOrZqG^ydX29*RZd=w -b?6Ht(!3F-YjJxlg(#qm;!uo$kHMx590%6~N>&1SM1gb#vP%j5g9S=r5iD+EMp3F-L(zzaG2V(wDc-1V -)_CR>3|K@3HpnRh)|3JfW3c#_vn!0(n7`+66)f_DM_&<UF5ipBEY8M=nY3#_S>w!Vo=>h(z((K_vJb=}X_Uy~vscBq-AchZIy*RiJ_L^|MR1mhwqVw?NUvZ53jyvwT@*-|Bv6rMx#NG -v_^eZfFrb5_g-cZm3Xg!>lBX;;}TG>4e)8q9pkOoz7(YtJB#Q{zpzEdtin^B2%*fM4kMvU_$TcwmS|P -(s`SOX*(b8E|Z~E#}@P2PO`VEzxfThj`*KD8>sb8#ozl;dNjN(0|5xcAUS-@|eJxQ*oTe@{QSTT+8M@ -0Brpk>>T>hukELA5LvI1yeO?V|7brw&f$sMPWw;orw1SoSeplJsj~}Y&r-NB#{f(;LohSw9hQUl??sk -!5A_L(*$^*f*s610USm8DA$c(-%;~b3NfDQl5RU-P5JAIsG3HecEP6hH6JpgXI3~b8(;Xp?o+j57mc} -uiuj27*U^+D{L$8k~6>u66;22@!dIPxjz%4S+bU)y-1f51rOoE96STh-IkWE -S(A?LUkCc`oP>-2ZZS=S%VZ=?g3gJ2MQB3(?_No{YuQc{v+h{$+ujt5*P?M=>m}%ea@u;s -%W!XOYb*y$j3|cn2JYC5Iq33o_zG0(;^TcdtouxyVUs1n$6Zi90xuf{yXJR~?X%En)NfuwP|G(cPdO+ -O#X_YXaNcB3I%7i9Z*yAfP#HyHO$aG-9&Aa%vUj<>;ZVon~(|iFkFHi7Q2CGmv -%HpMnQzR{F2p9|WPqxAmhCP^Zd=pQzIqohDk_2RsNKqk44|uY>iY5mjE)HOa9hBL1Ob6A;G$P@fn+1l -zXWhf1Sj6JN7f%=$@ECd+!yKn^wkQON6zWg|A3ALFCMfqv6h&b``=ox*TN+?Q3e32PCRF4&N0GGJO2nuP~sFY)oKEQgJfpnsE{W-qeoVn(38K|;VUM*FV;_Ccz@l1R@# -WPi5WZ}WH*j}`Q4?(x@AM!)%Mxt~%IEj#j$>GC)o@%K4vgXnusuujgE*>PN4lGMXACWkXV;)5T6mUsQ*vq(FS|f9Ooa$4dPDE*>pl=5OL}_>^q5@-DuN-(#th>tudP2XL -+_QOrLyl1~N=)n9?#=c{u-T>%W3?APk{6wBg#OzGdhW3xkXaK<(DXpetY`n^+5_pkR?A`;62HHo=5z9K6hb`F -QzuGxA4&k>`zx_DN~-N2e2rPC-g15O-DdVgg_c%%nL-cuj2QNfUWjbIhz{reu{1)wQ!G&`g{3xrO_t$k9npUFWO;~BuF3J}k?wqTnpFj1x|ns&cAuPYz5V;(2^6vGoNb?ry*-fPlZn6rEcOlF?Eh_Wcy!9ed$Z_$k^Gb8XwhWcn -(?jgoRSpl}U;O}jY7`kP;G6s{#dy>}v$_q14je3q1W|s;s_7(!?U*DMDNQY;qK?(!kvzE}D6YDnUp|) -=aZ@f9vrtUqG=xKWPM5H-gkn78hf>8y$O=(JL3QL3HNewQ%o3;qBL?)waF%L92%Zy3nndD+$M5M24{Z -!*RvK@RUHsIkq;!yDUgp`nn~-W17XgWTD5R;vJYv9aTu8C#dq$;kd`RMSJYDwSd;`K9?6k7D0s${3Qq -+r*DeE|N?MT5pNaL!ZJCrx-Alh6Pa>&&r3E1rT!U1GnK>JmM ->^4VwQ$>)F9GEyrx^B9FU+V5vmR50f8MABosV*#z+18kCBg1r5w2S1Js+BkrT$2*F^=vYV{>U3Q4( -;K0O^x*6Zb@6mHxQtU49Zr{XWPGSs2!bUIl`b=h5eEGZUQ!V!gee@v#VCSuW01V%IAEzC&_nU3P&^IA -vrx3~Mv5*R_-ros8JI7u*CS;mPj@s!YY@67hb7FXMc= -u$TP%xH-93qvUQ~>dWB@0GI7gWVmX-Aa4}A1u3#CBWFsg9-J5N6Q{EI(-`BiI6ioN`XzC9AH{pT-U9= -wV!Mlf})Eqyq^BC;EgsO;9@qCnxBYs0=De6`crssu>V;`DyXk~A2xx&tvJ`>2Z{lOLfijq$R&$pQjm1 -yubU80c>mIngyxte#8V6*i5FNgeugIBB5XR+lT(o;|pY@1f4Mgt*1vii -T!w6bwbg+N+9dP;*4uDy9|i(VCOQZitf!i0?IVw$0$yhZm($z)j>vmm;W#nG-|X&7vUZj|zMV^>5sSu -!pJN)S+A%`JewOi4XRio_|LOABCQ8Cu9JF7zdur@N9Bz?g!0++(D`7s3nt1=P@A3$GnR5FN-pprWscb -|f1+1+Ws{ma02#aj_&FAYM^hv)>hb$b~PaFo+M4Ib6iKy6Uc$BVJ$TX*TZvHcOE6cOKpi -Dpaw09qD94lVP -jK)qeB#kLi2cp=A$f4_zz6j@<5)l6|U@;5@CB5`nsZTZ5zhOZK5#XtF1jy&(3y@2F}CUl)4|Yl~6-IG -l0N(pyN)TD%V4V&I5De#|h3m&1{_zCAUI`Lu*CJaUjb;y?{hms%lFdm9$Y8Kq|3={vK2u<|@x_Q@%Cf -BZPTL&(?`-GD0nV%_obExbFwC-4Efn_hqyaHDpPn%+ZMC-SZ3C%b{4=!Q&&|4+bsVrZ3~X)v@f~P(W? -f&Mx+Jk&xwZG!i*s1MPU8N|wAUe)+A{BMA^o;`-=Fc1Pt3V+aTDVL_I3Sw+397~Qt!bA4uW{i;dTPJc -@i4+}K~st`wxR719Nu*hJHAv~CuS9|?08f2O`T7)U1Ycv%$rh;w~@$5WB!iwsaut8M10Es% -=tPt?7$1lc43vnGVek1@NsN?j+-0HTN)8FukQRWs@JYXdCgrvN~53;{~x`)pS6Hy?9^%*pV~uA)O+4D -Vft;9r!D$U)qV0nZNEPA3M#1f5+@1&4CD{CIu%09#)HIidVR>Y#EQzd=-BXddV6Y5Be4M%s$cqVUbd! -eDV4UsSX?(vUIw1_ZZX$l0^q?Nl2k;n+v6Bxk9}>cPr4v6)?1FuHH! -=5W(4dS|(lM1G36gD!+)xOo6meE+6(7C7zt+jLP)F9IHV-M_C!~vQ{yPF4GL!8%g_D?yz*jP(8A3Sc{ -6uX%eoulG2&7aWRoW5B)-lxCTbyhHSEw!583OnSBo>8zVbK)xag%+a-9zkW3Of{xTAvF|t9kjN6a#IB -}b>b>fm6Tqs*bra)ykrQjp$f`bnnpka5JMN919C|}MIMbHMC^z=-vBeRzdo)VE@j9qAvj)A6?iBE%mR -@xz@D4UcOT)+Y^6z}1aU$Gh;4kkI@mqPP+Zx>iQex+TLU=4LJ@x;bm*#M#xVNihtqMDLX6=;jLcqphG -ZnX1C(Z`G`&F^U$jFagifuVJ*NyMv>At|E3ziHMQZj@MkrT`q@Yb}_YlMmGPa;!N7;*LbQQN70M -aZdEoW1UJWSkV0KxZ!gH?Occr1t8sbiB%O(%oW(6Fk~zQPV&_JAybet+4ih*N_DB4V=_prExvy8mOsl -7xOwNODZ|LE!;cW4oYft#guEf9{E@kBp=`db|U4Pu%JwuT3lBZX!mD@QLVea~v7nJU`|BKii1fH$jv} -lG$v>L%hu3x_ic<(&#kKB=Z<=qjZh7W%64*zO{c$+kD*Q8z1wCW}EV?Jg1=%a+gkLwDrc`6B|emu=}D -cl}eWj=Ci35DACh2gZ=Sq*nP8kVJVlB*(@HTdHHmyc1kvtG-^q8|9mzC3hGvVHy{iG)IcB#udY#`VT;xa8QnRF*>Of%r%HL&#gRII7?%+&<*QMfBaF>j_W4I6 -#72V>M9Hm^M)g>AwP*KL9|vTABNhr7D6)1uHKPdy9~Wnlacj=gsbVFud|VK@Wl<@<=r|%Fk|;Ev93{4G7#hE>B -o#x$1|tZoGF%hOs0X#ntYP%mSx$u-vOvx`WQQ5ktG;(*o7K{zeso2c{(^nur}wCMd?TeTi((8Yc~N-V -f(H?#Rl;oUW1Roly}*Yczq9b(8uiRQ0puyslgJ>lpE6pzCd)R}Dh<4yMYz8FekNtyD^xE_+3i&LS8Nr!nro1iv#JcB)c9)l>#; -=iXyUSqcZDdUV!59}X*D!m6fLcN!IYPxH#R>e+B8J=iw##OgHa9F?MO&rmk%m$>+(YCDhx9mqjz8Ao} -G&V-NKjY8EU(Nui~V#L)4)E`I7mLW+#5b@881Q>_3Ae$LX&m4{fXLrA9#_5;&f*Ra1pc-metq6`R3YB -JH#@fKgZ0o>^drOtu|LUu+9`|=1Kk1td;hI5X{Q&5AlJ4Qc>iBfIc)iqpCbUcGLSFqso3I>ySkZmGYDT@f$5=SCml=EN1hd(+Y93ZBj`KbByfgjR?l$z+?QUz -oWlC>9^0FNkT(VsKZsnfbt?E&#!L9k1AlPQ)OizUE;qUy@BjY1x$J(XUA#8r$DIzIFnz`eOQHeWA>NCKY^{NWwlDBdm@6yhr2Ecu>?&_V}B|lI70~M(Bk%ri -NG>p;#GZ0a%3|GQo)D^D!9XKv@FyzC)-I@3*b?Wjqlm%P--_u%N68~e|*CXeACLBZ&L7vhI@ddqmGQ@ -C22GjR5EP0!?W{_XM}#*slYZUw2Ud&-_L!(d66|Ll|5~n0?rC`8e&jSZ__cx66a+D6~O}jw*6|$W_{r -YHI3(#u%XEc=nA@6-M88`e`BYs{`iN_xBS6(@sd3jkb&^GLp*iR*G=a}?XKPvq1=#?=ItBrPw1r?cyVrS4#mx8`? -7(|o3MdgJKp_T_2&9q*)Zv6blY?t(vH!=)6Ch8!}J#yQ0d0Rkh@b|hsMM<_JAgodT`@wV8uk=XB17#` -3BalcYqC;hPSIY8N4>d849-%823}s$SR(0yiSr8$eb;wbU}$a1$Wc7xe>JJSxZ(C5T%&P4Spvj9`>MknDk`XfLiTz+`DV -4)RS%`f!pXLdB2#e=13omK8HqFE2}&%d=fk|U_E(`9-&2@5A<;Qi@<|Btp5`qvog*-eZj{oJENSLHmrNLi+*1w1T1wOFN?Zr8J6W$R -FE5!jlxj0RIS#!ZCFt+x3SXp+32-h-{gh(ny)k(b>UPSNt39)qJXLwEn;}z1nhD;fzS$56dfou-SN)dANLjPM1|60WZBtuPWh;n$ifi`aw^K7off`{nV27ex2-{Q9hJ -MMQRJ#hqRHDJV?Qmt;u%IIA2A+= -L+ov5IK%(~8V(|nEUwX;nnKRYV5sUoO+60TxL;JB+jH7C`2_uOqNa%0(hKrYY$BhVv2OUxoR+Cnn)950eIw6|vPJvK~hb}A^v|jH&KX^?yq26wKhJ*DT@09)LR -Tt&0RmVNAV#ryVSHHatkKg9h0Z8QqUTOREbndyD{Y?onG45Mg2?0#Zn8(lK$-8qyo4 -oPd24MAvUe@pIOOYAeaKE2s~<0t()xt`oQf&tJ&pZQM!6W>nPa=X@e2l^bsi{v$TPyhdNow&W`9`m89 -oY5YsJ7dM++nXo;W)~5z7Cd^A50Gjc|%F*eXqrz1jto>=x#;f=1jyZDkia_@S)h!~+YmKW9o^)wb#3A --qm6RaWxyQv4?TdS=lq-%%b=w&gJ9mhG+W4-zI$fzKpn2itD0|TN!ZAx$lES634?;wTs?cAt5Z#3~$_ -p$bSj<8vGN5qn$xWOxP?Xu|A~%;DquK@+BMxaU1LK%}ehdJ%Bcn?oJdnhIJsS?0;?14QE{c1dl3k3A> -?D~bn9~7Rp4{vZ2^(#8D~)*wrN4s(4A0@_7A!?80B;DqG~}Ku*dXh0h9v^>`y# -s;YH)?Qk+69JSaQH7sXPXw?OWP56Jc2_LvcoR?amjLQ%I6esAS?kD;~s#1{ksmwwlqRLoSvH@uU>lZ5 -l{OBU+v?`q7#mO$qlTmmXI@FXzF$qC?aU-m6s6U(81Up^;>lMjDO_hn5IoQhz)E9vD`}28`Z3F@&3rdx@9X~TXwz -09M7tOu3?xY*8(hIZIc{xCP#xB8|{7QEt#znPh`^F+jFgH9;ggPh;&RCOPLr@uWs%6oQVo0q>c*V+Qi --5)mC5359Bpbw3OLa}jwS*5m0?RACy%qdH<%#~Et{oJ@iOoCWM0RwDq>e(_l>C{c0EK=mPP0c54N|4xml9v!bK5rYHi3%12(1cd;s3M;4wcBRbFm149YfkX-U{PzLw -u4|@Bp-n4IRY)T{0Z+nZe7br6PmNf?B0v2p2Awf!aaL%o2X8W?nsMll8WoA3JMzMt)jwSlbrF+P`_E8 -eYHe|D3ey|K9ci7nSI#ElPM+D4;GxwqFNJH*UcBx@%2w)THwF*-BE@;5HYG0F~RiZm6Wgv7;EjTSg>7 -keaBl1y2iuAsH1X$zU?S$YlmcWnms};*{4gtDCLnTW!9(cWm$#W?^&5Z@Tf!spu7%+{K8Lahs^IO4;N -F9k!lY!=2EBapjs6Q2hwm|s^=7l)dLwwdvWuZJtTazW&l3YlO%fvfOS`##RacIFE`;`Hh1+Ryx|%P05 -jEWz5rOY-MkDcUCsZqdgC^!j31K{c+JGCT|@rbl#YWl$5T%G&taSAq4=c@y>9P@IX4)Xxe_so;Is#n~W%|aq=T)u=osyrh2aZ0%e!wfK^|yr@y&t*5pHuD{G+) -D6ko3MC>>nQkF*wB8}0$=;~gjo4$%LDK=HH>5gM%{Z!aN-{jcwFNjze_^O&79sdNN$&R -vzA?;%{$WE2(&#jmqfKaA+Ol92J=DX^)&u`Kl`|_gb*RIJOdOW;NGg6QjXP3_?g5hB~W7MCeFQV45jP -F3p0-m=%X?bcG@|PJs-ANo>n+^5*SHidHqPAJghdP3Eu0tfrIB@!@r+eoXgI^xEV8s*io#uVAI|`|>r -$={zLt(>70E9*Gu=(F}KhX$GS?s(lxwELBhxIG!vS2Z$ -8B*>yk0FoQ&X&9T#}np|=fU!IsNz^@TuQ078$aUn~2HHOvFXk>p5CYrr}xhJi|(}On#qU1U63{a@n9c -*h)TcN50zn-;(A<}c8L@CNvS9Hz+dDRwFlad?!jgr(rf`1p$-Dja3*-TaCEZHF3w))uUCN@x!3NfqAL -D}TGc}2Y}I(28y=AG1ogFE*3-+1W$VLzaI>6>JXheu-EAqB!QZFnNqxwD4Fvw8{-e^8%I$xlPEh|UKN -S2~w&bg*8x;MHek49(T<(7L~p4Y=>$&!*3_@lwVDGt(2X76+Da}&|H_5iU7b;NuuriV$NFOVsT+`HYtB9=Z8&gBW^;>V54?xFQY{7R-pHrm2&pAI|5 -FGt$KbbL(4msgL#wY!IXrgs{lnlL<^BBR&x(EKd8~BP>TiL*BU`O#>r$7lZ0wUP3O4doCY8&`S!U?Mh -i4VssibKur{M8(g8LrboRE^k7N8_SO0_mbJTNi94_Jk$2C{gRW90~{h*B+58YrWbdD$+3WnPp$W&JW< -rWTVfIDP}rk{Dth}Gv;!64L#peB6Xpn}8^l#edsR>#vv%#dm6Jax~TbU3=2t@hm65&eF@yR*}M0w3{U -XQ%(%>7l6!XK;0T=2JWAq!qzpuD;zi8&*(kxh<=3&CVHyDt5)rj!O*i(CA^~q*LAg^QNPIqc~{a=UN9 -Qa<#WkXm(Y2<{sgKnt~smH%f@`u4i$kjf|fSE@AaFQ1_?OA~|vleuQvtSV=$CSQfX7z~fNb)9zdy%!S -w|yXIg!?Dk!cdD+uOK+=(IgDY*DC6eKMWqJwc(RN!Uo7fI1=>VNc8;+zes~G&!2@5G)WdX<*r3yk?(& -;Iy{E8Y>3O%tuLY)`6d1W#oxEKz(bLhaN%+HNvoSZWid9w`qFEcsDK+NN@O6#E2#l?IYky?W<_b3nF| -5VMf6+UeP|AsNM*h*-Y_l7o>8A3^;a2ptd{j1@JtXlp7~f0$^b(*4=xni7@~A(dbr -#;SYl~blZOXuH*%Zv5=;_u-Adtb)13ccMK?AYWb4Ni$>r?R@%b))m7z2(stK5EI$;uep2O62pwA6+`h -m0X(K;xp;-+JAz?Em5Idjjuc<#OhfaS7F6=M#zW}Dtu;dDc3-eP9H8FG{2=~Y?Ii`~bM$Jwap&B?#Im -*tm_pFEx>7kQK~A9KmxRXLkJ8x91A`mYqVdIKGg6S;pX`)~M)Y^n0(qE0Er@5J!ClHKXut5*-K%Gk#Z -_E)L%Gz<eHvH>LXd{SV|}XMn;+ -eqaV{Qgw52$^~il3LD>g~Ng9bLyauHP;uFGtcO% -2Ti9GEbbTHF|>+r5MGs9IcuENNfnRmxZgmRh(-=Qx9FuTjxhyN+5_ij9AT8<5Q$dPqOqSmS1FOe$d7* -t*emst&*ij9*E*@9U+}w~hwiuNgTvMMP~hTr``rWIH@FuDs!>n^nTk4NS%DsIE47r7_WFBY`>OWnxAD -V2am2IjXDC%&J_Lfs?QjSWy*WL!Sb--Yh7;_8Ztr(HbO=FEPMHZbgBV`Pnhs6}o-_#pEiyrV%uRy^cz -iD-!cm7|Gc&4Hah8hTRM>g9)~1OW~Ywm`ee}bnq?WDSD=%Prm|%bI1CIKJO1kZZ{b_L0NcR#F$Pw%Vp -+~`2C!sibfYIgPfIN17Tgn6HLuU2^^FwtbzuN0}VNPLY&031N%eE8$5c*=Bfa3ixvPY@+o0!s=-PtJ2 -NNdaQO6e|2f96mwrUW;_{Ntp$LbM%mw?8Bvc6yYb2n#N#a|_ElHtWKSDrCWf;nfUCDnWp4IMOsL88=q -xn&6nod4I3R?!%^(n95Q)uTY={?xSw>X0b=#sL>M>VzsLsf296ulgz3KHU4p?`>Z># -nW<}H5CC>uz^K0&H{8DA_erMDOQ#Hx7Bh)~k^ON6rQ9|Wy8bWzcQ8Q`N7Y2e9Zg59o-S8}FW!KdR;Ci -o7_6|b7>P;y-Rk)W;1>h6L}?ixBwON@J+j&<+Jm$Goiz(WAC^9`c=j2`1dtXk=koZe`2ymbhq%#Usl4 -xH4syJlBLs;zV}otoeu8b?l`yY|bh3jzidOk0Ak#NkkQ -jhR4=s9u=gHcKg=BXhv_oJv$h1*5@7(`kX(y4vG-lyPV<^pnk#S(WV-GfYZ;gQSG(Gs=^4shAARBnVg -SEtTg9YY(VIfG{vq?R&(;-jgcT7peDBx;JFENY5-B9XD5QxQ(Xa+j(G(lQ9j(D6+NW`_ -rqmu#Qnawv>sA(sI#P1SwM -(Z1Gb~Ph)su-r*`AskJl|AJ`&B&HFjBt{B*S~;OC5;0;7o&ns{e8#kcjC90KH`?6hdKyuInNxkMgW2y -6E%`I~JPCehhkDO{^*<)n=5smP{3mszaMK&u2uV=jzN{rSw+TqIeZssEkoE>)M!u_&w=;PAeURi)r%D -m{V9|4k=6!EQ0~6ffUOEElVYy&vtjtan}l-gr6JBK>CD3AIbscaSo{Od)tm`@&QLIfyDyd8B=;{ly)= -&`nMcKFETUq#nK@Z?ag1sV}{?!{Ai5tJ99@FR-T0DJpo)uVqqD@VqltRPey^wt(NESxlBt5&|eXc&SbTg(x86&FZ^w| -J{}tCHVw_kdtw+8bIEk-Tn18j;z}$lWk+7q)wU8&_4ElG)JR;VlUMS=vUM6*zk^fO(uo(-Je3$+~Fq& -)s}|cyus$dHCuT4CV{j%;77sfAn|p;`rr(c=7H2(KiPtp*Z;K!IAji-=Cbq^EYn}j!sX+@jLPS_5RU! -1O$paKREs207{*HdmxSu{&q^w0n|$g7oWe_fBjmV9*bAsAH5KVM<<8S#t-lI-@ZL~7aE;;gAF}D5O0p -*=i$N2PvApPkTDKOnBDWP%AJm;hIur=(NpOGO$M58XaE$=tNPVHf25l?lu=Eaqj%Hu?s)#~4qEr^coi -E&r|*UxuVK5}V6THZT_;LfA#L!MVw{Xg0R`KN3=xTm+zBQTmV#vC-uQlP2#rdCz1UwC*tFI* -gYrSAtHB0XSX6QF&PmQwuTvy*&@XtiG)C4+}qVGD6S3=IR(jL2(MFvx+>#Q!S17;pl>1F#|u1hKJm9x -h%;ug7$RM#ll`GiJtbaB2Fpcm6Xs~2Inmsswl^T)>D%yJ!6U7nntsK!I#&*fr>6h*wl~dnHeMfH#iMI=2%pB2vMUpf8*a<;qA{MI;w6ZiI+Km!E>mF12k+<96W_G6T;#1>; -Jz`Nl0}NQ0F0wb`X)!JMgdD1jYem9eyq~3Te=04I`HhrloX9`C{r0rWE3RPZWZ?PS#fDQ<#BiL_EtHK -*Lnko=xf`f5_dw*=ZR)!XwrZ=Gw3dXECfc2*q%^xFJp}?Pc9QOuOfg_IK7`KF^WBA5I(5{#OsJ%LG -cmb8=C%ie4xIowi;=z;s*7z;7M2y``bI8;nt&_J6hkhyzt-LAHJuNH}1yfM!D3Z`nBqS{^|&KpMNL%+ -uP!c&$ho4JD-2?rnP~U^XL7Y?GUSl;)~BefAR(V-TC6n&z^k#+2>yvQtg4AU>(VCIK=B6i35&oO|mXO -$43p5ZD8=jk>^_{qd6`w>{!oSROWkUJ9c+yGawuE};MgXWT@@HWs&?x#0N=EvXp+J`Pl%{U3IN?f%Y_Ua! -{?0p@!EOo{DIh}%IL@Am29bl1r-Xmb+6NlVgO>yGU!_OwykwMenPOtH=es#|Kwz-m3Kw3F^FZ0SjGle -{rp^|r_0jmBKkv4gk)p59e5o=HNSx_Ey&QVf{T@Q1gKO45!Nxq_h#Wh%PSj_$Hw!7pQ}b-d9> -N$(mbOr$szF<>n23?vR2mtATXM@me8sz$(wdCRur^v!L46YOR@P!$xJEJ{s1lTSv%y)TfhXEhutqZ+^ -Q?qx#Sjt^#R=7m06a_wq+ml}_ZnI2{*AKtb!F%NPtMbhhj2xt+Cb9h79t&f(8uPpXI`_oKSphU{`5S< -zxZ&*7|d&1~)hzrLE1<9glW?hSj_QS|>C;3Y)%@|tu$zL47%+R0r}L@V6eq_Q-zv>MR`dF7F2i7l~uq -;VP!bq8RLzDH^Y;ZqiWz3B55nPR@$kyyHB+Lshnoq#|v0IlQbyMN-;$79d{XX1Da53jp`LYs(xbXWIjb;ye -@ilivd22y#zKO_;=6V+@%iqBGv;PqY*!a;Ev((G`sB>5YSMMA?So5W<+}qxO-t(vT`-DrrR@`Z#V)i*uG5=(jFoZF+Z3qR*J*bvE%QzKvg{vbm^EwjO1&Zi!juH$ -gnoMkChT=-wNGwui=!cH*(N!FcIo4M+MRf+&R+rM1H2D#Y?&))P`)A{LV%gaX&{{Ee=R!pFrQo|xrO1DB0iCKn0|3u5MX5FaosUZ7M{5%j<;zd;f*vNFuH?V1TOM?kH$Q -je*JrRu0Bu@J~^2?LH|Kg_Ub_LVP80Lt4kPRkwVi~VuRwf^$l(nAaL{F;xT47(>4tTf_CE4@S1m?HiauyFm0RRT_DR<}eYY(Et&x=1Oc&? -GTw>xVZ9DDcU1!M{D51)yDFO6G3nc|WHh@fXtDu^#^$Y*t2)(nPp6qTstA^7Pb**ypULz=T5de_8L^%mdOTutHZBXSG$fx0nn#09aG{|D|2lW -^Q>X>Ppb;JkX3Q?Hm3-Ax1NYuSdVPFS0fWKc?d#}7n`nL*^_UHN}U&emXYryn7X;uC -&@t1j?O%j>LfmS8;_$Yf3jjo!SSgQ;dmS>i_-`-P^PS`}KBOY)}WMx=n6M^ATn!DVk=EZ0^-L^u$#${ -LG7Htgobq{uR+(FGA3WdZbeYR7srowkjCS)6yHeZKzF|DM0F+5{qwN*=a6^rq^c4$aQ$L0&xUy?sJyy -aqc0Uvv8FLPq_orE!s9Dxj?kFGP%dqhHJV#(T5?u{6GLdcM_aaoj`wEi|J4bF -piJNG=2|;@iCaie+#47Xa{fDdfhW=JXWXD?z2{?(OgA-+Tfts-|(Q>Z(i&A)M>9CuhKymsB{moWH(a{ -)z-EZtkS3cGPMfQ`q>N>YO-Nled)BbPDoVjK{3ev7F!4ptYAJA;WQ?z-q#ZoT|qPTm@;`Tar#aU -x41c>f4pjc>u_W&(SpdqWlJ+P_z85F_D9aN|eid-&y$j|_KT*T82dOgtTJQ@dwC4566#9mT361+JodT -jBCp+Ew7tO&MNrSMlNR#c%e_$twh#%co`9qnNCifZuIx&n4KIL^m$9*;4hsXvCUB-dBTBx|5lg<*?Yp -`qlTzInbkf$BNN%R-~uzH*xovoYtJ6SM-pAyMl-+l1{aCbiz0`ouHB;0GM~cvSOjj(H~n=cdv9NcQ(# -yj-DbP#=yK=ssP$LaR=BSE+`vZl&U9&1%KZJyxu)cDGfltKEI&>Z*5Mz4lh~Z@EmJy$L_C4hEBi5^Z5 -#)mR!2jpLsead37n`}03+MXN!tU(|Zd>N@xXJ@7FuK5}ON64s@qsRAPCqpZ-Cjs3BVYgO;Oj2~2)ta# -brd0oaXz+1LFo^#e_cCaT8hYK0I=$g%{b4pt;BQ)HYwhVmxiZ=OqR1T_TDIQdwet;r2IJ#I$AD{7Ct` -Ky-*2T!!LU{!O^tfa;(;+)}jGTbi4fW;*AY@};z<00VucRlt07lBebM4u7D7LGxmDsNIxxi!&8V}Q@^ -0$x{?iMo0Jwto(YqJ#5m0)jU@*ZGJV1Plh5Zz#42J8$F8BcGdaZ3sKMdO!jf)U`9!G+3a7Lh4cg>rpj -;p5atyRZHB@H7-}4o@)95IU{v7o6%2Z*jeim_UstEAl0j+1}Un8T6k$R@Ab}vn$4)!IinwwbL~hgqi1 -(Cfly_k5zppqB=u#HAHx8l%7;yjU%zTb>Ga<>yEn0dBbk-Kw{Ypd0`kHg*0y}v7Qcuxq_n?8tox8+S3 -u>f`{;ntb#YyGt!tE;}6zb3kataGLpab&!zQeQKjYdKQ(P+T2CHp&H+ -UAW6xezVcQcGFBp^IFC-sj^nZWyzSw@nP>Z1@{Zv&&>;nK`~w`>NCuv&mqgV#m_{p<=5klf!_Ux*{RV -vTVvGWlTEu8M=RAZ0TwIUR>r8M$auewCNZodfHcvSId}b#8QSLO~%n!?mF>%*xgGba{FVbk&L5~qtcA -gu<(r@JlYA8ABB}&?ji$5Pt~eSWa(I=NV68x^PY(YZ~+2BVVj3{g*$8T$aGnQIUEi<@Q~8wX`5=zloF86<9H0)GMajlmoDH}WBVov}Q?7>;KQ`fAp-55yrn=lVddd@WugVVO7$=<}Aorq9s4cH^{M0%_(p1Fyur_@JY%sxb;7JNpO7PRW`$q@MjwhSl^)8x%@}cGj3%!@w7iJ%tc -d9a45B}r(E7i{m~GT0-lN!E>gPq0$5rGPXfw%e?_L0^L0OLWY_ZQQm=2Z0p~9RIMS> -vleU&dzh@dl4d&8{Mu1pmpKEI72beXC2g!)WtwIk^x`R6v)XW(xIVfD0a1GUrVj#Wls13`-xC#e^lnD -(v5gD|{VQ5W-FrpOIX0U>3Hq}P@DvT*Hx(8$jl!%0={N*WI2>Z-ar=4YGV#nx|izkEQG)Eh2M{PUUS3 -APb1^m}Gf*#|?N$0(`y1@`BzRHRKVz9bZ-YqZBv^leRym2>1R_0X!K|S}o)mvu=Qt@N0IIY(9xS^{_> -~F*Dq6&xVSOUtugq$BG^EbIt#SL7gglP|HsU*z1^X6ojzi$)2KX-Dp;u -Zn>sEFdCaq>Ji*McexwdMxl}goyIsuSyvnM$o`NFh?>uk)#e4Ir`LiW@6#NBg%agpXZ(E;rX5kEkwEF -#bris^o5Ye?^j<}-8ob&6+u-o%6hs3+&NP~@7FAAg6R2@GS_FG2+T?1Y2CI(1}BM#b0S% -)wjj1*ktOU#w3A*nMX3k23_??}glptCBC+8rx*MOB6`GN`Wzv(SFE?e@9T$dhrl#BH$+!J(IUl%QQ6C79-{(+x52(L!*AOEP -ARv+1*_nix2O(E{_nnHI2sXvT_Rma==?&9~`pYOLpa~?+RFX7*(*SazCF1u||6uZN`O5M%3Dq_EP!r* -NnDk*klwp$Y@4ByH<+qqIze`@S3cW_(K&N$UjH*=)LG^Vdj?-EaK3x4lQl1O}SP7Vz!Ok&nQ;kKPbqqmp)SHGuB;Dn@hG)bk<2l -w1a%TeS<8lkJPCr#WGX!tQ$Q8VP)BJ*^vRqCAeW9tAafbFWpqm>%m{TAM1?eeKUJIy9xv+b39Kr4he& -s~V-;^m%7V_@l>vxkbf8|Dl+!qHgBi2*2O!)Lv?zQV_x_fa0qQMKM>9?eRNkRxY!;1UaXHQMBwN72VK -JMr%?w?`6bp)>xy_3fu4Ld$+0PlVCJ=AC#kt=b+Y^97zA^n@LeukQg?|` -M8tihVc0Xy@rd;skOS2yAJvKc9C1t%gil*#s!l>h5(aG5-MvhfN>Lus<&fH1Gs@4m32N2 -CNOSNAS@cSpFs;4V#?;(X$Amh1qpbQc1;OPAO0H@7HlOHh+JU!O7&1BPt%Cxpt1%YraGQq|92(f(~Cf -CMjJusQ`#L)iH=M+Vn)`DdaThmQ$mO4_zM(g8v;yv_4_bSsUAFpNHbCjqUI^Y=OTaZ26Y|7!9<~ZLTI -^g@Pf$UxEXF2?{>Pe=G>zCEDcFXE}+$|p0}ei%cb -*V1C#wGPGc&j3}!I88$!ed;$A>9pEOkU6luLsEmh*+3cktwlA3V2Wo3HBcikl7VZoxx5~Hp&4REyqn- -|rdK<2={ICZw_xhe43$Gs}9xokv-Q)L|Kyg2Ka_Z-tS?_q@D;|ZnI3tGMK>)prQPyVCjD2tijP#LV%< -B4yf?9F-4|09%we~i*PX5WP$7ZIALV}){owuPT;#8lb{6_+|e#4q}X22W2sEl}x((9kq#mc)-znDt;K -NASuEifkNllQmeDxCb(`%MWTqQeW6o6=wPv8$GWUsjopJ8rxtHjmO%@iA9)(#fLF$h(YNS%_D&(AG99 -3`edtQp8a$@>|vzp#0Wl5Tbae}G#?hVUda$PNpX3giM=Ps5jaOg=5<2^l)v3NeS4`|uO{PUb -2GF$Ax3zQr#g7N3qk@!Z>p;EGF-S3I|A*#pq)^`vdJ!!`%>2&p#2f9MT<`J{n}?UN2qWJn3Y{l_?->6 -+13J)v!&h{9|NuL(TOS-{*DZ=)PDKDKV#0v(i{bQ1U_esWSggFN6_Xgz1eFAtVqaAA5TI#il-+ZZe|5)n(&Q!o -d?$(z2Y%x_cez%qW&{*ja>M{ztInjv|p}-S6qoSdip#qPD@n?uRnhz>3Rq~RcBy`S(1hzoc;#g&=A;AH;*qDSk7>Z#mgTAQt4ZC52TC=AQ7{8ia{U-Ep+9O9t -LSh4%v83lN%3ZKH$pI*_mhRFBMJ!AO9KWnM`+#&pMVVEPjF<-GHDM6gW3VK{yqPc8h8bhC?&PRn<$%2 -AK!h -ilf=rGAU5urW*A&8>f8f65*jLnJs3dCf%D?H#&m;+RRfp=vd^VRmE>j?^o|8yMv74;|n@Ylv7cp;eUc2iAO}V+aWkb$gFich5VO7P4wD`{ -y3&!OuDhs?VfRrn_g>-#|ymD;z^FkR*i3H)zk6+rv_{t=+lJL0;RR7H~F&e>zeRUBC&r+O^SAFf@Ml% -}h5Q!`}P!0bBPr-m$IP40MR6E(vnWe?@1@hNf+C2lm=DzGTx>a?xyAB9l228?;r#m|TW;YU%Mw{(%?h -?h;eD2{h{ac@a&KowGNhQQB_Y94IUgi*jmf?;Zo+#?An@*zUA!3&-)uHjBQFoA|q1{{bkfE6xz|`9q^GojbMDz#0htW>CD5&)4 -Io!pp|}X&A37-NPU29A`-NwFA5r-%-l{T*d^E<@Y@Z{<=z4N;>0LxU~N`^!@&az6#sxbbKFWy8k%$|N -D^u5Bb;Hx&QtBXZ#vnF_$9%fvjJPWQi7E$O4I+)tPOY1MtiBC1uhM`%2xi7<_3@g6DW8XSwS -vb-EE*wD<#0R65?GB6BPU=Osd*`H38U5PXcvICV2hu9?Rf7$GbZ7k7XE0Iwo+7H`M02Ad#BQmxmZKw+ -$^Nz9`uZA!4M@JaL0WHcK8ppP?SyGm$Y2~Prg$`ojWDVEV8?8r68FDgix*otBB;%-z921Y8jmDsbeQR -};%?GAte$?u=>g9@TRK%k+bMjv_G}8#z3&?fxMC5p5h#RC<$gr$tC$Zv6C7MxHW{uV)iYb)wR%i8+Ig -zELM^3gVQGcIFrMjOFevL?)EJ!~^t_;fRN*Nex=%eWZjd#gRZ(h0ka4UZI3^}9|r}x#krg3x=S9vy95 -AP*M`r^T1f==>ZhvD6704l&pWm<;;gw(1jiVu)E-qB0%BYHgR4s6NVZ7vqe=v(~FlZ~r&T2}l2C4?BZ=^7s6Gyrk4#| -aRI4szZHNrbXNq=hE-S`uR*{akNOS@HB&kgiMVoe1^dFkm$OR>_^+d%ivSO6>NsLAuli%a%S55 -xvyo6Oj4W}eQ-%9TKjMG*Hcdzk)oCiR}|ygkl1LIkZpdt~t9Yqf|Z`l_(WWZf&>8l@^cR=`pJZQsBoD -krdZ7u>+U7D@t=s{1K(Yw6UAIuV90rqNI0@k8CEhmXayxc+I}?&x?$SfqVkZ&bzv2u3I6avHy203WTv -P_+5^CMJqC$+$r<)|}te#@4|p(N`6BrCxP$$RhJ5$+F!N%2iADLlvv``HG68>Ce!G$7PS0#nv@d?FNiG~p#>?NAT|;5o=5DC1* -3a-9ixoO5;Q$Pk0VEfR`G22DpA>}1wTD?U_3LZ0ImJ1>eo&PbU;iEw -W5oW=jild4?CgBNSfH{qnEsQEM$`WsoBrZMn*O}j^cMz -|oK~l&;wrmM*=s(cFqz}Uh;8;v8JHQ0if7psBarU0ne&((1uIUZf^tc(CqtEBZWzO!c$-VfQ-<$0oro -;w(&6xS47{R}wL;lThXX-DP7ElXVp%+yK2ykjnm!AjB4(b2PXRH{IxWMT3Vn4DAw5Jeom}W8nt+dTF@J6~9LR5j_FQFq>i&>O*(Z-M1G9BKfu -dab5-6Tgo#~bs)z#7Fm(;d}ZJdTQlgQ#L$;2dQcSVY9glw=c{RH9pI?`~ -NKrLX(%JwLf7lK7pTBt7>-BK5{JT0#749UyU{tPv(zv;=+OAh_zhbS#rFP^E&T)eQh@3v{p{YPY6kBT -Mc)qF*$HOXOD;c8^XRB7v4f5qZ%)vx>o29rjzxIrsR>LnkDDM@^S8Mb00BM(7IoJhen(b661nRO$;k()p~(h -?VZN7D`bn^Mm6j}WR_hDT;?D?&z&q*}0|97p=G6-ZG8M&^P@g5X?a6*noX;L=n8b*ylhKox6F?%K#OM -$@dQ6aXr?x_3zRARvev8A*lG6+5`@5Dd$-}{WPPZ`NPw+|%4hfXb*|hZYf;l3Ze$lVm;rVV#0dvM>P>S{EaaC!7(IBj -GP)6g<-d@e+CykqsBY~k7fsGLNq)D!I8w3ZVt9mwCSr=ah$(Y~TH<`_oV&YgeX;uBOE-Pd*!z=;&CvH -@K3S#YNl$^Vbc>2$a1nDs53?9VU2m(lJm#A;V>r{NJw3*_YOxC2ZC>8N0>68?3$tcgT0y$F@iwkRm;Z -rb%(HP51g(-#rlX1>-<2d`)NY0`uqic*5e`MS>)l<6jFjzkJGGbJjFcgNItS)?2hG|_;YC_<+-I|WgR --9VW)8-3~(n|wgdln~0A@fviy~SI*yre>Ut+e{v+YR?p{$$W5TV)BS!dS;3jyjJuD9;jW8hNjG(Xue7 -$rSK&il{N@v~WmN>w)*$5856T5Qe&cxtPsmVLHvrJ2v?U^YekELJhHi0L8L`gQ=~$)?FtrVDO1NVYeV --4PY2(IGo_T$B{u=SR|}~Rmq6hSRpqX!aidhPFheP+cPN8VA5-WbU%cWpeC>1<_GC%+bVEttWeo&vO^>mB8 -v4ijGM-?m5y8u@a-1zJQ0B68K&enD3imjOi8K*l(j!9wrL93f2>@0L@s0ge>)2_5T^Ku6pcXtTW1c|X -nsojaxp*W(zgCw=QQ@F)I7x|94y@!j16i*#l=2=Mpy=2&e61C6Anr7*-gNC=lxCHj2FDoua`7kh+N7)fij5sEw*&mt(iTf!y -r0=sUnQlcha=ZJ9f$5BCdW*;XKWdJAc{+a76rk)qm1Uddm2Jgr~{^;FXyK~2@V{(6&X7RflR#RyXu!O -?Jpq8eFG)GB<@+un(Ho_GK$CHuyp`f<0vQ+owA` -;h;io@%qC;N*pHV8p-Ul^rYAStk?hiD<9Gvseu;AN5G^1u3-(8x}QO*0Zh;#vO>)N4RJ7@X^kb8sIw-bDfaS=#c|eV%}|P6~+0e&5rW_YX9)HKaWp!QSijQC-|% -u5%5m*Tc_QXeflMjVLOw)(@h8!zY4S1ufis;->T7U*KR%P@eP|6QR4!-J5qg@SgWU-va=XcrgpPr!JM -NjBE+7Z`AwY(^&XAeXEBd37=ttp(QVLn7fQXnI_OT5>zIy`zT=x>h(X>`3Bl9ML~7Kb;$VbnB*svCxRC+leWxju=P^76fd -Mc!Ry?mUTN(e$64 -_&|Gw?o);`x5`AdGe180f@^xVd?pmM_A&y=f6Q5)6J3Wx2&G_ds%sZOz1yrD;tf78hz|t9?IH^h02V3 -3~?H?DDcrvL=1&FSr*R@FLWpBy^4%Iy#$yzw7X<5p(S`|4>iVzr74^L?U9u@c0dC}8sMbqk=qBv80sZ -~S7>w{CH;j+d^Q|c4^%;y7@I9`LqG4m*zvuj`%RMoXT@L&m5R|-HwGIgn$vM&QRium37W&1aIWZD(K^ -nZhYc7AL1IQn~_$JrYU@B}^>$WR4w$9UrQYULfp+|#c~RKgnC6RqIY{^|bfPRn7T^H^%}Utz&2bGwv2 -TfSMxbwa$(j|Kw;bjLZ5dz^@`0mDXlOf3bxWvc3woo>IwnW^Ak8p~b$;2p>ebR9Tz23hF9*pNgiJ{s{ -ao|OwnFY|fqo7~m+PQEU75B~Pz;O*(*@ln-TsQ8h4tFMaGhd6Jg>G4KEP;6)Y1hwnEmHEc>u0wI^?LJ -hyw*qAF4Wq2S?vZ+ts@~St7H8W&#Te`MRgjb>K;qfIz%#+9Y%uM|I|zCak|@9z)@b2TX0wFT=`Do>4ZsiBY1`DV@}SIOQZ&G6s?gH1%0>NcoWcdZOuVv@E!bckdRp<95rjj -HHWhU_(^axwxN2OV(Y{ORvQP6gk9Py>zjx%{kOSk1{d}str}9j3M!t#+k+-YTS32vXu=7DocN?p;|@a -Jf9V4)zbPtrNNN8m9bwG9!6j;>$3z&F>g6h2?Ymq28@qJs~%9QuA)*r6 -h;5W+^d!?3tiE1zzh!f=0O&_90Z0A|}xSMModI4KsMQ+>)yWxzcMk#e~`NWG($8f!yU_$stas0hU%it -<~n}8+*&j2sJcYgMmsn?Bz`D)~7|QEj~4D(-@JRCMHu6v5bGqR&dT#IE>B1wi=i?zJDOz#yLsGls|ld -cIvk?Wm^O3WimtBa!S}l8feIZRriQ8 -6vHp*ej_fE(4myuEwrtq^shQ@mQ&|PF;9NM?mI=~xc%fqY$ua8#g0{Yr$_tg_&A*|5 -Aj{Ef~s(1L4{NE*EllzL3g{gb}ZQ39Df(@_xf#2_(vgJ;X$z61K!kD@kIAWIPrxjZ!_Z-HA~s@6Yu?H -k_)h9eAO48sA#HJ7D7l>>tePt|Y;0?vJIR0x$>wY+^dXb8n -%=FD$$~c(woi>(jw^2Y>(J_}$CFi*NVeoxn0yI4&Nw2LHZu?E3A0-m;X{J#VVqR3|-0PSCf9!GtWG!R~}MOiOHHtt@=?b%ig7C; -UX@B>Vazre_#2-8bs}yBQWF>N_1Qk6vVmex|Urmlyqvc3w;90o$RCw@7j;${Zq=;i>09AvMY|Xy -)rH>euZQRfW_l<2$s0NIegYtk<=#S|7upBU$em_l)kOSm6L4QO2-YRn8?-)hti4C1dadt7(o#tm>)m6 -Mk%N<3+P@Tdi$CI&Xu412ltnU!gejwR_~LW;!*YCQh_?Qi94mXP!*57&-O+F;)qqEEZxYx;j<$fC6y< -_M3o}j!$$|%7f$v(X4`rHW>OLty8i#d}K(r%IrM}fI9vz?7%5o?Tv`A={;41fGcigQPZ(g=Kj!-t5jR -iaxFJ|NZc1s6VfJYcs!T2N1T7nl@TIQJJfOE%i2dscnHQ}uDVX7Qw95wB62qcW2`rq2 -ffN&iPjktxa`RaIZEkZF%o!72@huFJIW9rXYaiG)=01JbOQGd^1gh`UZCpuApceil>Vwx0(eFtGe|t%uqPqxy7FROZ -@!G}Nvo1yOS`)AjgJ&L^2swn$6X_wde7#ZL|6W%9I(>Zabbb12JZb8A8NPf)0)8>XpJKa$1>pCE(LJC -cPQd#IK9H2HUA1^dI&1liHYOsqX0>>IvKwKzsq$-hxa<|V;d+j@@K -h<|BUqiFAnv$LNkpzQew!W9XHmEt#-()?mEPYy>r%2hzQ~_115_ -#`ELO*dx*RXf%}VG70!PVZBByksBU2&d=-Pk+sh%7Iwatj;wkHOT?NUZe3MTCl~FNW|(Jvn8nEtjTe5 -J5J?zFf5^rnXI@~M3XGt|7y))uchl92u`}m859I@IFv#Z$;MRbF=melHY*0I)J3W9wG8 -@6@gNUJ^%2H3^p)|dvyS2{OmbQorQOp+tKtlYG9UU>r9&~{+-N#}Zw!Szbyrh{HCxNK=K4<5Mg^C)fSY9-3-b5pK+ccBuW;GJ7JC*uj&ef%MJ*R6e+isyy>V&`8;!&N`%`RIkISV+(xPnVP$cD%Z8_0fU$vE{PbGy~kxPj&uk4bRl_>e_?;JP= -SS(2?PSf;Pwz0@vU@#Z}gTY{qhV5gwcyIh}s2Mx$sE*l7cj898TrY~hl;Y6s^2t)|`Hh=g5a0roZC`0G2!1>pLYC}nxyyb3pFUoU -TruR%3-uRyLx(*gavqL?-q(7zpm+SUUW4d$h4$^2eWRJCS)FOA~pH&X6SWh0_XC9TF=bCXC)4LHVa_! -(kRzV&WksuHBKAcGcwT0&L=D}Z2Q8Iw?y7F)fJZ6FtHs)EGo2I$C@v-lsEkRFWLJ_x?}ngT>WKs$J&s --z#5(b<&kyx99k=iuex9d}`&=&_^~E3StT=l4NXl?;~YyuY!L&ob4J;X7z|roTIVt-=2sy8^kO)&rB0 -vL7Rx+iBBxWOX|mD#R-U&2V5r;m{%w1@FbhkKJM|D~!D`4x)-oVEG;SFxje8XP`8c0- -aR8nriiX5#a9PSo~#XZF0xKah;ev&+EF~XsSmjr7!pGAFk$Xjo$lHWE++?#i8^^y49`>E%ene59uoyW -&#@w)SLot`;0gKoX55(3FuD1Mr-tG0olUu#9!Hy0+wy!{ATTuqa_6d6)_VM7n-{AIOKCB>j}rssMUK| -gP4Mx;Ni4^&RwsHqHw)Dyb3)_FdePZL+2?Ane6*;{C@yFKb5ML8)lcZ4Q$spcGKcGqm_a;jG=*Ttq(E -(B~kQNJ3tdD|cdkR;8!u0)yt9P%l#tXrm<>7jW#yT~V(*00y!laTX5ouf- -Anx25)7P7HXVo?1>JCe%mYJ8n_q;BP^-ymiV}HUarM}5`KkvNw((KU|jU1ETR3MX!i-JMDtL>$PmSG+~>EI@EAIn;L_hOa(!2adKLf{@Z`waRe?7G|@j$|dLXBy` -sK#OD$8G<1C=yAFH&cU#HbqH$N@=mIT9r;jJ7XkvuUw)|9)@MEm~75(3<4u_aEGc8hM&u-3O9f%tpi -A(+*FU17Vhn$YdXE&N{ZFBDtraFrL&IzSuZDO}=`)P5DSLo5ksAUE8~5+M4Su<2XY3droW0;GD>w)h0 -+7Cv7~(NSHgp7ONpzS6Q0x9`}BJ_)EQszemuc3wUtc`-&g*(kt~-1j -$%~M*CZlrXl&#Ox|aCe>l0sd368BImX)t$@l)|r=LCe;*0wilgnmr(!83?n=~Ps=t%7xoU*nW`ZX#shBQl`jpRUdOAqP)Z#)_0TkpP- -IOMrNC?34z%taNqG79ysQ9)rrTZ0f|M3YK@qmHf~MshfJCE%ZPutF3erwVhJNNK=G}2Lun+9xOd#toEA3lvPh^|ZP -`u>^+;->S^jEeWBbB8U{$y~rxQg5XVNGz6G;&XF3HX$5r8i}{%vd<=am$5@Je%-DImRr -}ExvqYxk-DXqb?>YCR$M29@_75Wy?ea5eINdKIcPa1p#fA1@fu&Ww?^a@G?)Pbu@{7KO=rG{w=deew&3=C -_-X0y#aLdv2YS+*x{jVx>dZ5cifR0~yYetZYkYCQhLvSFUf5imWt@6>uIp`{A|JyP%xguQ6tZ7Qq>M|__bBp`2k*BF~I{cdmhFs -D<(U)Z^C_FFsK&~GRW~>{+_{zQ$AwhD~P_DCF*^Mwlt2qkQOo0!Y!XAokZP~m`nC}_wrEr;p55dqFTI -@wDS<97fAygWUvua2rs*VK&8CX->7j%5<0%ZrT6UzseFq8J0!3-~%tzDfrZ -$n4{^Hy;xyn^kB*qd|vEMVSj72}%L%1=16_V>0gj?IX-Dov`2{bEle(I~E{#w6lv@w=y@CkeTihO%+D -{J7WO2Ph(uzS8CLIku?&O!+m;MJDyQ2FZ?#9Z)BS)VzGy7hUizNDE$#n%nmoH#X_mcyX%idtH{z^yG1 -9pVevz+AH|0%%AX-(92LXf5B-|hWbvdt%szAi4gLIct>FykPVJXpN{w3pK_q_uSz-}08o&HfMx^+07( -yaO9Mm%3sm0~abG;P_z)UiZ^)ilQwc-S2?ikokvWcUkS_xe3gj?K5+Q?+LPu9yWIk%9bwjgSW -jt!grEEzx=h5n1mo=z-ea<+m*kBb)#6Rt@tS^Ucf$9_0@T1B5jMqlQwzuT`&GcZyg=Ta)8Dki_9P{v9 -U8XiRr`^1E?03$Jk(3Tqrocjp$%0g}(-|wA+2@+sP26w=pxeto4ucGyW1JGAmkUH3uZsLxqK%-BEVoq -6>9p}ND7BPYBxMB$yrPH=1{YxZtcAqLPeJO+xNm%@uBK^qL5qQW>gyJjXtbkOZs|3PH9KZ4y9K<>eaB -nUZsE$EZl`jEyQZ?5e}v-bDV;7z%`d)vet7Ww;N?E0L25mHb-4HZc}Q2Nb;)_{ZjpSob@ct+;%;$L3t -9>$k!SB{hc$FqS#+H&hXW=0Q2j&?jGuU>0$Rdu5UN%k -Y;@+N=k^;u7OsVpQ2W$Z|n?!wzc0YBLm-N*6HllAl%!9pOr}oz^k*d$PallqIQ=(gu>)$wi_FbX1h)) -KI-KxI<8xX;v7rpnG^l)7WEow$uyyK?Ya-Myi-(yY?WbBHv)G<>eU<0F7(>q7K_@{dJpcHCUYP`5qpG)-@m&&U<&j18J<X2Krx|Z?|`2y17~jwZP=|&_bZ0emI$N4sYWp1uF&*KJBaXkrqn2O8m5;jD#X%1)^0N?-nm7vxD)i!kP$mU5vNaTv;K<*9(9*gpo)pnxX?hpzkb$@>9nkjYe*OhOTS6aLL~#Kn5hxRgM65mkDFq7; -**_J1ZZNewZ -CIw$t;1XW^%kDQCC0qEYcYtV<=V#ISPXF4>zH;?60QF0d;Af^e^<6oTof>Z`AU)OKEiRJ}`N#<78!pHM5hnFf@ -HWLR$gtTxDgjf`i(#se}#Jbe^ZM<>y691*YN#ur$kYJp~A6e=!+3VjY_o&vO+KrvUgEb}JpvTH59!GK -tHK^|27oQ1}VPmvQ4Yf2k!T0z`?8;^_Ezc6^*4ACF@U-4*Q(i;0R1Euy!PK-Q4bRHGzLsccac8BDwQQ -1W$k+3toJqaUuL8T9~!mcls?W4H%6i*CPty(>9TexKI1^^qE_Be!Q`< -w?S`SKK2@&AGbgKoZr3r`kTEMowu*|p6tV}_~x&Exz9KDsoaZKFTZ~E{9x~8=kKq++kf79yZ?Ic&EDa -wH(rI!jSU98|FZM9Z}*;-U$^=7i;CBqx;Eide*ZvMuXz8E>G{o@{r%?a{Ws0(l8=~dufBb=q~NDi@a@ -4rmX!O90Umt)?VJ6oGM`hK*WbQ;a`^2Y=2NaJ_=W1nL1l?OD)G(U!JDcQkEz5z_ustge0T8l@SBQqo6 -mTf1m?-+#v^|D;zjA*13lCSPxoJa{buj=HwRD3E&NiJr&-FlEOubk&S1@s;|h_w9GMB1)+#wYsUWPsO -Yde6&W%>k<|_zTx20%ERF>8;%iJ<1lGlinYvNaaJIvDenQf;cZgQdpQ%_Po -+Z}_y+x$!!#IK^?VzMdRTo=k@G(b&T&&0_jMQtOkUs~A(lmpQ6Q-{%h14CLo -xT%BBcp{I-Ub8Ou-KmA6ciaT#VI0N?L1VjL_9SXj*HpoQa?SfY{b20%lL71Uq9>f^h*Yrl$J@^mY#Tb?aY4I|J7oXb -q_L@rzV*6F{_`&7;BLb>QwSC%ljfTYITCgz(181Y9EYBVp5EHf;J-;?{PNY(J$GJPQ>_Q$=-J!-XOpb -Xue1z?Wn&H}j2NCAG!)b)WwgqucO=Oz5^n}frB&Pr%5J!3rY#Btr%(gsFfb@XG7J;uFqcD0iFtYq4O2 -v>RlVVXENw_%wT6lA|qIOYhFoTqa^CtTFT=sl|XwKg=<~p4AMJEJTtQNmKq5RxMHQtdMAN-3TUb{cUs8bPu)R0F4V$~TdX;21f}%%**MdVB_z&a#X2{Q#4g^aF9gD2IqHZ>eFzF=vQ^;bIISf-&b;Vb&5H1|K)SD;MxfG|ZT(E-JRpx -f6oR_+#;PKa;o$!|8Qml9%{!waU%*^qQ@KKoRR1xT)$-kL)kU?6olbD!nc@Fv%Wj#DN(=JvRz<~?5pNeS)mto -7T-|@rQdMhJ!P7H1aSmi6KHBQR!K!HTltnbJBmUIY0NkOr=Ha@b&UrS*B#4vz5F;IW<3NRQ@}R*T{iuw!rvX} -2WICCjp;>dAJ@;sn#B*l4iBxxr6b%t7y7~88p -D;aTN$(9=-tvduxgk4m%CuxEsTwxB0%}BwjqpnnTqu=Spvb(W_gy5P^7I=$%H8(>ecVuWbuV7uA$O@m -`;K9j5G4D*cpf{gMp2y@}(nBvn3;)W8W4s!jH -lIBe~393X#08NI2ciANhirD8@d5z-;(~0XDx*68o3}T7!kLCf|*@-rN?!aqgM{X<|CqDm3Lj1;^8ByJ -ch3*HqPC_-K(FqgZM5A^RtIK^cpfGeRF!rv -wmdo!oA3B*Blo^`gk?IFf033VQ*+!-R#w&4 -YbgFs`50@fki@*GUQ^a*3giB7`OP>jz>+Jq0ngl4%=;C!akgurotwwIXt8M@lLlHl^Jsr7>*KYW2Wk`{k5&yf$9f-(-M$g!tpraO9!QOW$x -bgvPqF(fX&qtuL+J?N!ZQV0Y{e$?JNDO-pLomeAs*KxMX;# -@yE4?k~T(A|Xx0m-23pM!Ea?n2LBa9w-5f}hUL93MXupht6t}5~XnDw^4BkK4p$6K7v{*v~3W+z-xja -98w*C(D8wCTR)#|gOXYCxkkI>sM4$~ISm_2gSFNMoRopj;ac>pz3?Pq3a!!{L{fr_Y4ZG}TLd -V5KEw9kNHzgt0v1pr~rq%(C^X(-7ltk=@Q$!}Ch{fy_MI8werd{D}Q;y+4o@h|ae@jhOa4j=Y7U`_%Y -M;jws;0}oA*~$~apy0zmD>~p?-^=TT2fK-`CV -Fv^^+PiSla}3eZD=0RX0|Eec}(nJ4r|-$zgAR!|A}{BS+0wa_xzKJuemFeuktIc{%0h#s`s58IzcpMC -ztq5u|0H7Hv0nLydQ+tE)Zz1&>4uRtK!kWQjQ(b&itI*8k%kIvB_2I2oriK?nu+MrHnZ!egSfuv -}42DlF4P+$wkUWj$8ynLr{oZO)6r!vBg$4S)+d)MO;`R#jj}ypQlZD`Pv^^Ha@d-6)l;u=uxe$KsYU)=Jz%3oLAbuSZ#Mh!r+HMFvylO!8n -e3Kc&)%s!2OL4F9t=N;n!B#N&VNcmyVMlUXROeNsRhy|uqb(2qH!vBY_+~Xrvo<@H|^Nt45NjX -diFb>MC`=)&p(lfzW(=Q;)*)AJB09M>#f8!afd1&-%N`8@A2cd#}C02$(raKWJ>n`L#>|ViYg3PLkWU -L#cji&X*E+1YO@7?f1bl1!A#5Rd1blQr_U1E7C#H*+b}Y=(nphX5u{?we+DU6$Vq=TY4`fS(yo?v_tW -mh3UcmEO;8$Sb*SQApvx3nq1?ySgrm$szs~kQQ36a5K0>aPrcxEs70hnoszt063i#V^alxPz}oYnizAE4`f|ZYt*ET -$rVVH2~`o`h;RlT{UU($E#U`}FoT~2!tjF%n})0I#)fk+N_bfHP)0DaD(@ieo(OXQa|*rr#&$* -tNC{DbtWdEJWaOIj{OvW=1fcW@fCIhMaVLFxW5^}kE-dl?BB=V8$i!5(w5iL-h6XY;u7TXj=i@_Q)kT -7VRUBqT;~S}Y#H3cUkj61;Z)`N8_U1;YmYQ}%^61fqtqv2KYd_j(eC}4|GCcFC;^u||XJ#)|-P~wAbZ -h#kSKf}?yiZ3pu7janxEg-rn)r<>;}@!5qx0Lw20m!uKLAQ5C~P`yipSh~pswgw6BU+Fz_69~kmy()zdYLT$?@&JON~diL!VdC;@kZIDZGk4As&_J@Ogj?jIw;LI)2Av##@n?c#)ZT -kvsJwckUgDo>O&)Hk;G|OFj+CrO&4A=EE?32y%cxUQM4snK|kj=Nq< -_)0V_ -u{vQWJdD9=>^U~Mw2DljEt>4W0M$_jycAtC*$BCXkh-=`|TLOvz@{ulL#!Qx>L-Q+AG -}E29dGRjGrZ04ib&soXz(!fZQ&o+o`^unSqq(lJSr5*l^<{MGr_f~2y|<_5dKh(^H@NIHbPeC(tJf=D -bcUN?wDoB}WRe(Eh2Y1IjIJ&jR_i>Q5wk)*71li=TTqn2A*jA7RRvm{cR7S5V=4f`NG^m2?<>G5-5e9 -9u0s{+(F>R=dJ|E&Rx5D9{*4A4wgApejcWCTsv1hqT&V(tjVFGgpv#Q)X3;B)=_1mOmKqo5q?Y!2bnc -pub#5rlXbRltqAvPP!>Rl#xoe;Y=8N$p3Mz^!fo!4FK9yFBm|11c -GGoEf5%uT_NcJ4Fc6Bo^ka1y26VGiziD+5%)OBk>p1&o2BB^ne! -seM$Coavx0Rr7O0X5gDNV7DA9moT$lf{z!EcJl6Bf6Ahp0R@)%Xeo`K|GKmWJz5QuH$wi1YDPh#Xdge -mshSyX&ylBsnRQ7^*-r)SZ7^gzYld7tHj{y6$|EHs8WW(@k6qgG(WUz7@G`wB6bYejG@cKKEcTNnDcYXWAzq(x#}w)ZZ$sq)UB!yQzAZO*mV&E8Mt;kOMaVc=qp@X7Smc!%m>4vg7r8?Gj)MKwAQOD -75o#kA}tR$h9E3V6Y8JIwmC7^<=b=^}?U0w^w6$xM{s+8b7ShT!p$ZD3VP((-0&=^eV7S}AYZM-aX{4 -IS^r7pyBm1oS;Um=6F(>ZM)(W1C#&wF=X{3d=gnfRWskXW0fc&!SY{%Q=>b?tM#+C2%1ynq$F~!z?kA -YQE*5%slIkrD1??DRUx>&?`9?as_Q%H?jR;dP$ze3`BD(#gbq+00yQn)vKe)Q@2@mh{gN{%$vf!lP5} -;0;#niY?8}W20)rlBxu}8PfzRk6jo^6WQA8o&krI;Ir%i$4N}u%s_CzV3$2iDMX&JqfYId|rZf$~nM%1~T7 -=%?Xk%LR1@zN|qliKg#$7PTxO6isg!VE?G4``qu$TS)akH3R4Kt84^8rP~<-L`NM3!Pe^8Vn1$c6#q- -htAnMY2pHUL)HqlkgYK%15wn20cu`0i70_BG@j<_5WvS{b~j-)qjfcJ2+9xCnwXO%^h9M&HaOZQeT#W -^Va$HymoOu`melO1=e-W^0G#p<{e-6l(m9R0}gh6sAO&-@z++pvO%SKg9#_|Mc%4W4rg!IY+YTQa0`^ -M^Anrva|uEpek(e`IQ`}3Hdbsz-6QL2>YFzOOnf^(X9gKk;;lq^zE&u&0^&zN`oR3SuKW?y{Xu_98YI -rQBD-}vqjS-7Z_?WArD%5~8gh!QA|w&0MarTzGoqGVTQ)6N>m~d%xx`HxO+Q`5EP5t)eD;B&xXIh&>6 -jFLonknkkuyQ?Im@Uli&3OB>|4^aPe~g3nEC-M_~ySH;OMMERVXPlJqv_Qsy(h<#K$bQ9mF8(frv>Eeo|K$Kjv9QS-U$7<=%InUlk -Ob70`H(pt*0P;j{P^5UEvC=y4^h@P+Zt2GQg^w+*i`Jc_AOV4WZ&*wkKIR03>m$32ChzHlnD&Si){I} -)bJY_{uq>%47Tf&Seyy!w@4xr*X(M{|rV+h-br^AQ|0gj0hd=-$+Qv`_%3zVWRZNEiI5ayJ53}?gV8O -wXDOSP=@WW^C7(_wq-wIYjvcqT4?ym{9agD89V>@;20qQF#&L$I}-M9lRa0K;?ExD>g@FZ$L?5n7o&w -H7!DrNd61$KZ#J0<~!(TB=#Fas(3ob9z_5ZpCIO{{kToj`}vD^4#n7=yeDgC|Zi0#(Hy9L(1vYjc*Ez -GDw$F^9;?H$b~m=b5ADiCZx}gWYlt%k3)a4=3r&5ur=&6VTePDDfmv*~ZLdez2;tf~V|aMa7EQ4~Hc142P!T$@Fg>6qryZ|m3Dn5{ -Osy{hvV7IMuvcWP0UwQ*=raT+}H!ZFeM*TyVzhRB~{onQ_BlBq*wJ7z>p@^pd@CD|Rg$AP`0hL#efJ^tt4U%Qezn;bw9ski>1Gy|$I^#|;DH9567@=5L@;dhf^2AZna}yG)sJSKh*}xpCntJV4DYFGg)}f-nH$bO%kl* -!!7X(V^^kPhrRi;K=FuI(~hp2XjQ$?C%K=qLcHj^822ynLfdga;R-{-b)rTall=xg17^(~-(@#kC%+B -{QBFBcKIy)#4(%#m_1Vz%-#tf8&|ai-;m&tble5i9O0ho1576d4~cm<~z0Jb#QoFXxZZXp+b8`^lh32 -;iLIHp&sYaS9+(A7><1zqh?DW!KzDJ;5v3&~t4cN`PIbG>!zW8(^Jlp~Y6H}M}{Hsh -17MpPil+|I%wp7(b$*^l>>Elmv20p4<)F&%1>dwpga7edG$>i*ZtUGfKy{LRrx0~f8miBV619J2j7a_ -&<)YVEf3xh$H+0p)iFDJ7X%1LTR;#Swfp4RUz)HfS86gCL?y_t^o;7UxO40uTHTq@FwvHVgDM3?i-3p -j%|Mm}6{eg5rRa#~3-d1eo_u46h=fUQ-K{W5QCr?8rhd6=Hv8%z%;i#K7~xJj%=HQ1DPVZ7?Zh-k2T1FbzgGyuJEy>qzXa&u@Eh$l*R9F<20Iw^2d+-tNaDg)r? -TS`7i(Z+DIsr=08nz!CEh>|k0;%qZ#w^en|ADy%JN&#qGO$S}N5&UsJ!7ZUs`QzOLpJ@DsuI+Ut_YBT -2W;#80)F(z{lATP#w9t5>O1kn4rtq`1#C;~2lS5wd0tcjoXrum4kOFy!#iz!Gb7=0Sh#Q6kY55{zQMlH%LDz1uIHj27=T3m!M) -t^tj?tk%+LRuHFya!#R#55ZYk%E*n4ULtbPtIY?liF}{em$<$13=pkJ)oa`0dHRn#+cj{3%+IK3baDG -yZ|l*A}YCo&nDcTlj~h(Otox$NA#J`Ix -vD{3oNPZe%58nNnB*_Z&m^6u(XE3st3y&1hi}%OZ_%dd-jeD=tOti(C0jl{hJj$*McEpM8P&6Q?2-_o{q~BI|ut)d|76Oea=_9PZ3#U-+FNoR^*&l`pv3y!k+5Ryc| -T4J9qmkMa>xF@}Z`qQx1~_nY?ajnSZ$eynl?^ouOBmRH@OV-s1<($59w*F|Ra-2Vl390X3uRI6BXaM7 -jl{tZO_;$m-&pI&NLLzb_Q2mL;lPh=)tzVfE}!)zQ?C3*5~o}Cvf+L15`I?~=s*Wp)=DV8~cDH4XwVV -nl+#O!Fp5Z!=%Wo$-lj6*x07uek3X9grM(T=Sp)T`|sL(8C+AV`FT!3WmZh?#_MrbO8*w@RfDYE5YM8 -RrV41CjI6hZ>^ul0ImQ;wc*BD6vYMPD(W@O;BhUs*Kd>r5>XyRLcYi!m2b?uWmX(vLLF$gv7^1u3g2) -%gU-?Vmx186;&#%@Uw$pCJvK--3>88Ml{GU=*Y%yK?KWSL`b`U~c!X5up=3W=RwsF63%YKkc;ng -k)eX}2ob1vk^O?P!a9YNd;Oc&EXU!?Z_p|w6*uztVQt5DLIa7dVA(PGWDa45xpK%4zJlS;#!IG?*^L2 -e1M!l@Al6z)ASvoTAQwsyhHI_{|f!jJ-v(HNEEhe8SW&ZA=v4lk}?IKV4MKw-iVY;TH -iHH$&`U0kd6j-&|C)@F?t$6X^yT1s7*T0&R`03*{U3Q7d6Ccpzlz2^D@x_gL^lgTpI6P=ngO!)}e6le -}%V-ePH}9g|EQ&cF9@bC7i#uglRvCjSQOwM+sOP{-eeZJ(ko7gPh241*0970$(WjZ`tjFbW;sFuU?S) -3-v#_tP3aFF=1%VxAM@^zwFTlxxEc_ucI^gPo?;yQ>(FuhcU@;9Z&*=)H(Mg5=Pwxf?{TNc16LQu-j* -(#_LA7p4rF-&R7q$AGAu3zeZQH1O%fR@8TS9BSKfId9Oo{0WU<0-IF?=bDCE=%1 -+!y30)gyo&0sXUuycpF(+h{QyUjIVATph;6tpT1Mwx;sWG04~7iC)pI+nun;{h}EKT71c8;9yrF|PG= -QRRB!n<4$yKLaa3b~dgUe{A&-UN;FyrGs~=SaD8K&$@l3&{enX-RdE%j!ymsDV^4$Jz~}lIX(5#LAaw$XHwr}-7Rd%Ad2SP<cw6Lqa?%#e*zZ;Xo^@$Jm0k`K9>4Lk}@@g|#FPjYSo;@Q&5ACPLO>tVYii>SmEG1Dl)|05TA>l3 -b#-RO<&|o&nS1u@5y!?!GJ*)1|JGCBmWAtaMC&m&fGYH`BvJ{MNlEgehAq@K21s*a+w?va|oCP#8EkKLz2qmMTUSUjt$5+~(rD5;PnObNkjS0fgN=+L! -m412=RcRBUbyPCp -mSdCR2vumBAm9n^^O?d4As=r&5h=_8x3J%4REK&ls_2Uev;F#sI%dFHQRJR%}Yu4}Ptuphj(%)98pIS -%4r!qRE&=opfA?0YTMlwZl<6c@_lC>fGZY$YnZ$5y{U{y0Q^Z?)%cH$V-IBE`slkh0ARjf5+8!aE>AEpcVmHDOZduS;aV@uI6=^);M?7biv;4CLoe1VKmtxo$w~BUf2UND -F0hyv?Sg8qm#o)f6`9{c?4$(`1?~umH&&Q7rV))My>z(4=PdmCrj+176JMCPk!C?5QssE3Xmw-|K3Om -fc00$Sde35)pOtnj1wa^T>Ai|IHAwy@Di`|mA#UY&Y^!2l69Ry>=|833mVF)Mnyubfbl8ZXR~oR{uxW -;h8NrpWv78kJ=i{z(n36b6Cy`GCbx}NNr*1v-DNS#v%S6LYUj0J-=646{uEPw-DHlZ#$(4#TyG{iZy+dI5Ey!niN$by@ -~al-=G2CJC|YTGEYJF|JAOupsBVh!U7M{D*3X5jx*&H2(_`04T6x}?XaxfXC -9AW5UHz2R_4q0XUi+DzRJ5KJ)RFoE+bQ2sRprw3+*EZ=Qv70PBK}g7k+ -?EVM?TH>9*p2RCRv#WjXtuOP&EBLd*cgrUUb#Ke3pYFq!>4Q#YijNd|ezA8KB -fionE=OU)Lzfw+bZybqd4SYxI&%YnucnYF?~yon{*Wz_!frL-6>cyY2s{)x(8{u9hY?EIHhD+s+cwJD -G*T{%eSI-(CMBwRJZZNxeMIit&fy=; -GAUe=DL#Z|qxAsXx!VCo~Tp0@muY^fRm5JQzz4CB^dg^EstvM$a4-(ab?aS2lhe#0Bl&OER6Sa|ZCZg -x&t=oAbl6hy(EsG+CtV9{xUZ6`$9DQ!q0FVURSade4IE6{cvl=yhjM9ZoEJY(xESFSiCF22FOOTo`(Q -T{Jo0c411F~H)ndhW&h*J=08&S8>vvSX6rNnpH>mcfw`1Z2rhOetNP{VcE^S4f2#o6Jxd3PV5^k--_u -Fp6!#GN`@_U5@(57W&Cy!iojoq&_~oZq_oCB02>&!fYK-shad<7q&P_nVGi!dYG`=*kyfj*iC!@)=& -eLeAniq61#vcJ?seRt6&&7B)%vx`aZ3Dod!WDyE!2Z3oo|3{ -q^opKUUpQ0>hvw}&rudqrc|m{nZUG60`jn5>r)r-b2@RgngX(mk%x|8NFO^EhN*hR|ab1rqy!7k -jf(=I}tL?_g=X^2y{$r*B8|Y1W;vxe1PEFFTu`pPRcLJ+g{l8S^H`{eaGr}uy*UQwowUV*qtDoo#AYqITdB=;$qUK6jP?~52rFLm37laEa^K7yU1YIVu>FMBw}?aA=6eAW*mQD(J;mj!0{uJ=O`6~i9>2mkbd7r{S2^y6yYySr$+p5{wNogzG*7o+c9zWiC@?`7j)2;n|+ot>6 -W7fUlOE$<-8Ng9Fx*kVI+S!Vl#wYrLD#4MT6c<)VmBemsn7}$G=P1K{f@iFbif9u?R1(IBS%!P2RZxT -A%+W{zt}g<8hwY>07*~5Q&(kZ-24GF0nUG`e8(#XVc4*6KpHR^kb%j|r971!H?tlb1RaG-17W`TYv(mk)PV9xCQ>2LQQ|RJVAAL0rL<@bP6j(wBeP -9iWw2EQq)Cc4{P+U+vI|?Ud(jxf*9!LAI_M3D0)r}N7_9HAA>h>78#Q>-Ls^T}E6~MFZH~`oHFiWyMF#X_LJBf-8(rII*GiJ? -Fdd!s(g?O@lvrn|aEdmC=vPS`yA<5kJfgKXmQYQy8tR2|l64IP&2oH(>TlTr{}7A*m@}zLv_R1tVNAO -pU-s;cd&v<_qLX`=RS8Zu>Ou*P%j+ePM1|Ku%`x}99N|X4keB?G6xRf=hd7yqq`paW5&cMWt(f}w9KG -_L_i4`|Rgi$}EccYuScmx)ZyQolN(?URf|izL))El0>ru_b@zEiCYQU6gij{WyLu6~?}+4I>k0Mn;qay|5P>XWospX#0b%YsW1jDX;eeofOJ+<%GPrAR)i}_Uots9I&>8uG)E~i5b5xM -9;60Uw1ugRnyU;GZH_557l9EjPiz=?dK$O4+A&IjD0`yHi5_jDDkF*>XWf*&f(9(Hn>4z%e?y&!&01A -jOG?FPwOWJQLi+NL4QhrGD&i=yiE}S2FperEO`(cfL+Ma}wx?F~bfU86Aetm1f?WMB=X{8TZko;cdal -wBhO2_NO|MARemHqUBbwzU&W0fx!?$8|ClV}eXOc(j<( -m-II&xV(b!IaT%`#&JIYL>yTnPd>`zT^%LB3WZ-Xg9x6|4(qREc+aoM9T^3^)`1%N8st{wSVscIBGV5 -=fa6OG;j*5w%GQhVZ3l1HNFEBiV*MU3#ozb+(ahD!*hY -8SsjM=bJZTEkvtB9Y=8C>ZYXtYlZx!;ha!pT(1OEO=$go6|MpN%&GFpj>Ig$-$M5VaopH27Y1U*OO6r -jL5OEWe>B(WWJNcfW^drg@Zk+6CuGk?tsY(b6_BG6OgM_HRcuAyH?3qv5eoA77@o!A#ssf%TF}lAt;D -;H&RGFF-o?5K?clzSkH>K%$A80bXY_U!JAF_tQAz>?Ir&vTK?M#Gx}ZdEI$p!P;2t`5_zq9~aX#q2Gd -vU3LK&T^g>$n`eO&N59i{J-*%#l+O)y#4Z~`POW=&>F7@|*_C*~RFQU(^)AEIT9>drZ=Iz-ovp#Th$J -Z{N)U#7#;vI9-9XV?;ZBA-EX5%dR-^eNp3Qia!nB-ThK_@mQ0W5euKAQg(bw-hhF3&;1YtK8l6D687K -sD`e|Ulmyr4)B!q6McbnU6GDJSJ&Y5=y~8+)rZhhI*wyp*U{jolusSVsWrg!Y)s}q&fRA`$X!CO&kS66g31E@k&11SXJBE!JpXbBssQ+3)n+ltZ_n -dK!U7N40PQamZh7sIV)O13DvNP*jPVqnSoQ{ik#F7xPOrO;DMD0HN+LYL570X>PVeTL>5^YgI4hr;@`Fexx8`t5gJ_%1V5) -k(DUM;?ImJBSH%9||2E)zZhX#8aIp&Y1?0S3aq-+A2=sA?AXb;dr-H-EtA958ouzpx?mo4i;X@02*nt -cgC*6vidY(TwkanO)t(D0nITq?PNCBjIS4~PSftt7|M>}@Cm=$*;d0)dIG+Ihmp&E2>W#n;M}DmromE -+uPSjri0GUntlUckCpRQ9J`)Xg;;2Ld?COMq{$g~d+q87H -4cdQmDMJ)Rg|NR_cp0&E+|3tb~YyT!Lsh>F6G58-sr!*SA;Y) -l4LY5oo`Ytc?`KE_B(EOmk;ibnwatIhP(5kcQbHA;b%D6Ry?mco!wz&Q{FwDeaPy*Kgx_065ciCsXHp -_>hZTq2WW3s(lfS7&VdUwxDHXLL8xE&VnZA}XzYzv!>~N*kwtMAYSdpGcBL9u;&bq!`*5TMCa+bmp@r -yv7SjEp3Z`Lm+%(7o^b3We!C2magX1xyk1BQ*KN^m3L^5G6@4%?t%0l?IX0R%aHE}bOSGkt^@!^AY~n}- --qbbSDI8Tj>Cx3?${ovHRWBPD+K>a$pw7^tJBNy70wYp72;ybRCr*kY1rrFcofMe^R~ca}aA(QUur$` -UjHqjjw$FAW5bS!<_Fxxjw0(cD8*Sf*r(@h7hD<%XdX^v3qiHq -qlJuecd>dN>iq2OW5;|L4{cXrNpz|e4iRNq`6#QYJIt1yBN*U_Az+KHsH`;PmFcnc_*@(NygaAB&BB*}4N)7KG=qQ9|fxX~!3cfB~xxS -*o)=!+sS7kAO>Zi0b*uPDfaXUPkWG;US>w=t;{k~Hz24;@ -#9B*kgM5KjRzhqHUL>1qM`5&3$@XIf}Pum2q)wgP;y|uq&aZ$)rVfea=2j3Kd;&+sGvGSD -NoSI9>v&3mo?S^xg4r2cCZ>pZKQ(c+;+Hh0tpPN(62!>Uzig7u03`FP{2qYwW$IofD`kv98#&1Wa;@n -Wd+@t}K=C)$d#HDss!ymq|tp;m9SKg9!(Zt+!jR&Eg*`|PI89JRMj8lMwrmWmZvhjiDG-&6ZUV{#pl> -h|Sib}&YyPimT+ZGer8i1fQAG>vRds9@qIEdcQrYUkNwU}NtxZ9=Neui~TF$(tL0&z*l`-N}=9oSg&f -p@Q79P?AIfv1{3=HllYRVOp)&By%J;+#IVK>xDd|zEmnp9my3d@WMpBE3ihKm8>PxZYOlud_2kX$tCQ -7q}1iZM~-7XjcP|HH3yzhS<$(Bsdx&Zn7V(`Pe;b= -UW{+JiD`TNTcS3iYo{!a(%F0T1Momg+tUIGU?)?fhnF-huVF9y9WjH!~96hw}SL7|UdDFuMH>Dcgugx -W0~cu#qB9Yko*p-q6&HM9YQTe&) -6;qY>_zsW8uaOD?f65@LP`M6hs`z~wl6ORxD%r_2$cGyCWV7K72No+wq~p9Y@4V^CO1>I_yF9|WJ?)^ -%?4+K;b3+ZLUhul%-M@j-SAM|c(@!@I|el8Slx1^4O!B&$$N@|$odm<`c0!A5gS9SHh%(W=JF()bz1` -UE9KQ=Vk{eoTsa#|=2x;lL)>fr6P8xzfAEa}silUFnU&_qGu?`&`g%{?I8VKBAC!DZJN7EJ-K`4~C|Y -#5r%h4rZtyqQm)6@#b?V&WnWXUg=1t*+S3Am#6~7^5m*KjR73wWygW8O&>aI&VO_GB_l#- --B$JCG~Cb#?XPxUKUwL!SLOALPROP3e15?O)T?vDlnr0jsU#s8D$HEFbi*5FcO$l>TIl0elMAL>b^E^ -a=~r(S*;t(hEEJ8;G&vcf@&5O~kmnJ-iGZJCdS7i%F-9(ohn|oP(IyU=j7u50cbRCEBSJnB0n@nlgqm -7h(HEMpBuncpzj-MiK3d5>lTR;P5*x@E(At#ake~lX=JjYW6`~$7fH}^cD)8s -g(h9fn`CwlbJKmBi*q0_ZUwS(`3Dd3zpPzM(WCiY}zhS!G$%Mg65Bp7Z^)9S%lLpEwu#y(W`+Oj!=y> -qJuBD0LT^sBv`T3R`~7WW_+K!KsxKYpKAC ->;kUmlE-tZ8e`v>Q)S4+KBF@HZnoj>(iNCXQ)Z%7|(qH=acD-B9q#xG%Ah1E13n{~1lsXn^VKb)?v(AN^QRz4 -peX2q;X{IuK~8&SI*ZEi+s8a;a!ZKTmgFKVYw8K@&}n(3l25dL&w#$l=?5VL(V;Of;#j;sP^Lj@Q(Pe2QR;V`sxLKefH`NnbG1u-@JPB -c3-+8|1RV>B5r(8&~a4p*H``MmBU}77YO!i4fZXAeSF1g=C=SAs{9{-^}`KNuGZ4Ug!aKjPKGmRteG+ -2k--BJVO7^1K_07gR8pDqY(}|ln4Q;j1d&?UKumv1wN!|hTHVATStj61BnkzJ>r7K8s);UlM{Y~g^v< -366ipkAGmTkA)`MHcsH_{4}7%OS -dGVr!gIACYR%tk*6$szX^HH(|0@MT~rnN=!b(BtNKzE_KrsMVKnq-jkY4|JU`sA8oKScURm&KS#US5T -iN13=gYV-LWWUz(XFq(7g~*bxy)Si35_EcLoOq&ao^Qx7=V5@f5;y&pI9$z$h;uB=b$=5k`Gl74 -5WcE489hM2OULG=j#zd(IB9g|2=zJtr-4;vaEd6!KDp;mpwa4j0``f8*7FB)vp@- -o47$ezw~U~Ts`-pCHtqj*;))lpe+?}!1j7f;)Fi(ORaC~m;bVAAUzUt(_qP<*lLTHOp>T@491q^?nh0 -Kn(jwu!ImXDS^58dSj+aS59P8h95DPF2_Q(F}Ix0#h$!qfQ6!%J5okJvKJ*gt>Kt+=?4<1e7P47AHKr -%hVf@PWDnSQB!|~aU1t;0b5+Dnf&vDb)|&LXFk;W%ggn7`wJT>+K ->_!JLSbNZEdGgJ7R${hjz?Hk%e(_wS!WEAum$CZqdCCK2B*2k!>)uKB%vKi!S+87zkLZ2LYmVUIy;4% -v;#VkmBsHSrgO7l6bNr~pr!&Dk6}YRaUIR2gtMV;%1aRy!Pw-)STA)LNNQ#X4Pazw02ih?me)5sm-{3 -<{7@gofjqLy%pxt7Bf1gQ`^9+k3k-Ns{5!IInSdUgD -F)}`oj6DP{J73e;ksU`e7-pgI;U(=fjsWr!(OWtdz(lcka(n^Oi7dYs%-<^)u(5Hz* -1C}BfOZ6(1*#_Yi{=j?{}RiZ(%%1$S9SIW>?c}CqB;yy?D1BJ&ZP@cC@j5zXZZ5oK7#GbmKv^3IDYl? -TYfdq!zrut#CqhL*@99c_uppZyNAlP-uYXv73KkY;vR$s=(1M*eNfiw#87WMSJ!(xV -0xpXxN9t|rBlP@+f+kDrC|P|z=rl6Q^wtow#96eSGboRn1PepvjzV()llY@|kGqXCqh*mYT*=zeySP) -wA%K_>)e^Ao*XF&3DHT?zta@%m&Zs+dY_;6~USX+S4f-c-H`k`^%x)=un=c?rjE*JgJ33r^TKQTIfNA -$JMghDFvrPEXT2Ro&iEZkmaZW7)x+cDOM$WOl%vTFr{GZ_%QKg6iUyS17A3xiea!jiehZ4w9q9nEc4! -N1P<3$rnexLBBufqDRG<3eslxX5*XY$UZoYSSI>?cR|8p(L&Bh>Vv-cK^Btgv?1AzcNV_)vjw~r8Mg8ZbYe -2yD~bs^2M{yKUTWCDj?}x>^ZjRsl1=I7Zw|ix=CH)S24gr$yi`_23E6t=lY{bP`ka#64&jV#FUe|24W -#X{bQ<{yRq -dA7!U_iMJ>`fltO?*IyF3e9l=?*A%T}!1kHztT8Pp{tiQ)L!uP;)}c(`CGlkEYVh=QcjB8t(4o9G$mG -&_iB5K}i7<4`J{?x;#~>A2QpD?J~3Tq{vLxNi?VMlbBbTab_D4?bOnR@djO -$#$)=|f@vP38}4Lw1(w%YR-MsdjYvlGvWY+rj}n9yA0}sUiXP|8nA9o@>0|g#}=Fg0h&`Bm96&VGG-H --MH?%itL9i(PIXWwAZ7UXc@CDj0H9+(7}Vc1?-N22Cq^CpcdpJquUNiQ!GXGL{C&G7{P)XA_=$|s-}) -nY<%P_jMGzxv!jLs#25P%_W_-pjBpq8#mtGa{#2tRO{nSPGHadA#UY}YScwnb0^(60Wi%v;Ur$DZ8Cs -cqsU1cAJa24nX@Qu@lUog?va&21Cbl^0UEv^+S@%)xgQH03$vF4=xRh*xB+TR7TGsIe_ez%;8Lv``1B -&~P;P<*68MV{dX@yTsz8Ey`Xe-06Gwdqye@z9?G20AM7r1lN8nWgvH-H@jPi5z4l?+LMcn@VLXQG<`+ -*+}gjDIl7Q`XaVuBHY6QU)uf8n96UpNAt=E@BZOYFheq#T(uBwT9^$1|2!Sl3Ej8Zjob&W#-uuC2YZ8 -yBFO@!D!Q##^qO0cs*(^FMLuDb*b9&^3nQ(d!M?^!K1qOthw22w%g7l00jJYj)9X$Q^#naEH1l^934G -sM74FOegurYb`QRMdU7JnAAJmtY>fvewuY_Xp$Fw@5H_(ee&L_(_KD}~yasD~N_uQ@vVNvgDLz)Z<=! -My*#;(_!wJf$P=B7gF-8h2Dsl5p^ib4ka(H}~kNkco!uZXK8PIWKon{ -vWHusXpi=kjo)lcPbl@;)_E(ltgjos$15R#|7B!sR(1I5P65bSMx(F$nFi8#6dI(N8=zY002Z^ZhE?hJBrnOm~Y?G0RVM@tvLMK}mmRr47m$Mf>hpiCE+v -wOOU6BDD}@u}gybrkG_Mk|%boT%*N^!2aY3Ii4{T*Kcb;7+v1$DQ;?hb{=DXqG?)}gz==6ogIhal$DK -|j)7bjGU^Jh1vA0cBTY@?uq@sQ-(^K0KaC-UJvA){hQk>}T)uA3pBo{Gu?p0)Rq?eoN~kmXRr$uNkXc -_916ytp#U9*ZtlF|y>t@Ee#5rE5W-TMWBIr_Qf?-enB^VnSQ!g+&;YUduHc~#{N6BWL1CycQF8+Q{7i -)~DR=pr-bHCAwSVJ>+dx8J_jZRmHawmq`p&QfCboLx+6+bnGg@LfN_)+t5#~L82g|~G4xm-&{L(*(SO -g}T#4U73>*JuF6C2Q0o{!!NGO>n4e`s1uq170pBiQMhi=|a$d+&V3#AzIRiI(=unx&vqH!A$Cw2Pm(V -R=RX-PaAN2#ofC>dv_>KbY+mOd((6k!d2Fn)y+^~N}0s1<8JyO+XeWm5CSzQBQPRJ{a)&#DHEHf?`x~ -t@{Up^?+}nk(7}ifYT{PQsdU=VilaNdd@`Nt{7U_~61|5Rnw_xIhwC~Z44+j5nOdBDwRQA;d^|qRkB? -8T;a42j@z3!wz4>r*{XxdFsn_fGYW4M6Tr0)japGw7CUZQsl9?$AX~p>xOyU}~R}+}jYRfQ*>+5kXUW -|zf+hH*I@%JLgbrlQws6BMhXJwb_A>Q_QRL1}klU`QpZ0C7Wn^WxIfRBS{Ox5>Nu*B-tgB89M-iw1T? -!-x~tf%37ow7`mA01_yg%h2LhPM-Hca``M+$M1pH*kw@`>Sm{MbhI0&?EI8pcZMTTcz2lS1ZU>V9BOw -!p1jYy}ZCFq%eY3PfzP#%G;1d8DKF6FbBeHnoiub#rlm~RQ`@7-5A+zkW8jD@p1q{ZR1D_jhh|v0Bn3 -mm-1!XTn$}=D54CZ4MKU7=kzk -x6yIxsGY;a9M2-5x38~hQdrLcstUK&TIGTUk -XTp<;^CQD%BuofMwd;BzuGc!XMr2(P;b$OuQ%OneWov@|o2YIcn(N%E1^WE;-kZi7_=n6wrp9Be5yeS -e+DhVHqg{2x=2H6^-NJvVIU7jg%(m;O`*wCU%%1RMlLjN*j?uU8L>{h5jQ@rm>Y_;ZB#FuMw7L{_P>z -I~mkYn>&@r>*c%z~Zp@5=yAGEu-tBhi -B@%~)SPu_R+J`_7#xPDVhq6DKDb`DqYSe=c4I14d>z=tYy_|8s1b%l6X>1L_QM)wDpkY?jn|M9DUw7y -Ukp+6$Kfg~e!aew7$NX0v169w}b^FMKyT9q|`SZ+|015x&2(qH$uEbG|hnP_rY97ntt|p^N0YK9=veqjhnRSH&WdSl(cEg)ESWOOfAi7u>0yAM^*MR9?!Hv -C<=~Q@9}WbeV3f!zi(%7yuu;kdkQY`7SpxqAK@%EAuWi1^5ps)(~cTb;s5o?j5B}CIXQ_wLQ+BWw4AB -V?6fi(l%{NL^f@l>s^^knK+(IL~h7v6fEJib)j}jJ-Z_7B=&S2rQ` -HGb4WwU@r=^KoIMRv7$WjOj$A223u#(e-=^UxdqjPjE-vsinQE_7fo&f-j_Z# -+19En7-tR}*U=M7DjX%9!V9vc#5?UsC*IAOO2dQv_cEHtDnN6k2j>2x9aDS&w<7_t;lhqNJ4!DYl7G> -(DIV^wgaY#-X<2aO@pRaM|o7KRLqLo;0$dRK6P<;>p+WWN%ZJcYZuDKOQ@+Jbs*{jk8AAY3lJ~*%lYu -OFU8W>@02j4>tV=w%I4<+fxStSiYm=L*vT75~0K<=jVk-1@%Eqgmd%|GQU_8JJFFyWLnHdE?ak117!8 -(MJckn0pUsjM~}Zg?6N`G9L+PDi~Sx*g0_(lYK_-EVDsJ4y`nYe~Rs5eC;*g8_#PbHw6L2$vniMK$UUIW -}deaxEb_?Fx#FCcXL4s9&s%h2TUnveG)tGo+oU;zT;d)m<;wXifQ;yC}yV%q=I59fkxHKaXk&o=bnif -9kapr{tDf@~F1gri^<|Ht&rMIL^|;ug$}~O@2mS==AWWWEvN9_~+MGqtBndV;(u)RzzV&($j3FxVq2v*`a+>H{(~g|Dk&z=-qnX{S* -9OsshHHl-pQCmD7+E+rYYTbyk7EI3=cdy#eDz0R_ncl=VYMpO%+j*2oM*SBO0ZT!HV`(%PVf?|a?1g} -Kh1;6~n#l6`+DQO;5$=$^T@l);hIRKI8JRNF9xs1Wo5xS`}-k^LBn#L7Y{3zobv%Wh -h2Wtvv6m=9S=VKKtr2}+fgR}@EGW`nk9cRQsy2ybBx~WUq1J>32VO!`{=+i&o8g&xE=`|29rWdP(c_j3M5`|_e00 -y8=(IivEbIZJtXk6bTD+)9BCsuYeZk?&wvFGaTqG>OmQ*ab{rG>yh*1;?|G(&?m#bws^JZFl=GQG8rOK -ztHKJQ$kEA65HH^406@j2`aRfY!BZm3YK$yY()+LRJv1$Mr9zEs8qhQr=qz1LP3-D{PxdMGAvlxJ{`_ -Q3>imNAH2N?jVm;x(aa}?FY?ru|Lmd33^n{C~3`|wP-M_6_}`r>`u=!ikDMl=AxRtZ09+)n&TC1lS0$ -ia`KC~Qut9CbaL~9V%cy=gN+48QTiSh0y^#7aW3ZqHc$7D?~&!ty_0MF_Q`#+_`#95cV8MMXgpVx_AHdG`@2O>0lezjc$L^Y#tpK$8S&WebsF4$^j*sFKq+?P%}`Q+QG!gull%BMK -dkvNjZs~Z3`=rP6Ddov^g8T#U)?H_x^n{kvbkswzHr!7NJm0an=3dI#xZ#C+CkiZfQjZI?{()+{1#0KVTzgt+yV$@N;*Fekna6j%OSVc53V*F -*Q)7GW(8@HP0_$eP~FQ}P#H+K)~Kzf+PAt00vJcf6rypZO9A~ccM5SMR(A|)Q;O=GG!BhtM%cT=V~^< -p^e<*6oW9j5s>n5Z$J34oR0Gy=n5s_F%!WvgW0ki~wCc1Jm&1_1&NJ;4D-^acLT#LLBaHNENKf%j(7o -%9n>)r!xy8t}?%X+K+kJV@(7Ry(bAr_|;5(@0J#F0~o7Od8yl5WF@lZKokyO(*mHiNfU~6IsHsyRqR< --a7wwO!Kf(wJjS&xhpF4<_ASQB@+qK}@~$#Mon7^!^=+HH~|HfW=zf-6!oS3YC>Fdt2uCf=di6?+I^G -98g%s`bX?Og8A!>ME>Pt@6h#w$+#2(}tle_ORH-e|K#PGySsY4p1Hd-%?6{)DT)tlhjh_zzdd-mJ)aMHPb|V7kyuuc>tjl=q-gkT1d~pBaO!TH~p@nTjlG7kH975oI8Ra(#zg?*K}X5FR19RuOsz -96!Dka5j|Mnd?3oW%kPMOp4_zaF2C@RZY}O_YhjODe?*@{yu(3k^N~zcC0Gc&dE|g9uS2=&jy77dxU< -}jnj5XpTwqm8TSRGS%YNRrC3oE)wS7FC1$U#xc7$s^sjX6{R=3wV^u-rpWb&kewiMS&94QXzz-L(7NX -I=L{e(h&sT1iy`CXLTB&kTYNOS(iZK_!%nzU=jH0mW6G?@kYWTxJh)0+77G02Hi6dfQ4mx9&*9Op$hs -h4%3G7lP*mgGES%y8LMV&|j>G}!?cV25p{y{tPJrNgE8x#4Q8=Am}9-aPs0xVP>&8ajRcD1_7va|CX6 -0yi0}vB65z^>U^JK2A3R*AdQiB=ymKRJq=;o}h(pJvN=xPtu`xs2APr4cH3`b7YSLgTu30uL~nIRcDt -u+BmsC!|N@z4Li~fQpQl}+8=9iqhP+&nl*X-gytKneyH!HtRU8^U^dHCao#%$s+xY-G>z066%(OEDTh -w3i02Wjn2+i@#!Na4%<$vZaqnKuQH`4Idchoj=J2t~#Yl(}Bb%T!pger=E;2Cayd;$lAb@IelGq^Qit+y`m#3n?zaV@O$;p8AshOzww21Sc{Ba&nSol5~%QJ@v%4Q7SVXnh50v}Ox|htMdM7B7>Zs;> -W{k=Tn(b`FbZ&5=VRNoQ;@wFb9g?TjA@ptor#>943{!!l;_=7WAm4Ka@<;8sQ&YVUt}jy{iPLWx|{M@l$@S|kd6NX1_sh=OZ<0wT31MSNJ$23WhQ5U^)iRXA3E -&{fk8Ff!MJ7}scgeJEdZ}aNo@;vGyAN5?=v}(06>H)JFi0>a)c~_HoL*HHj<6Tr)+JvwdI)QtyWP=7U -Yq+8j8r^pWuq~_CjFvNt<0nIm|po$?(s_7LArlQj^SJV~@6@&sB?RH}Qx9UF^e$`YC6k=mc+dflhd^^ -kqvD7}@J!`_~~lxRX`1S^2`u;!o`?MlaX~d&D;EevTWop29*+Trr0?e@TwETFsNX_wcK&ZS>GmF+}xA2QS7?O2=5jCx91w^6|ny6F!;Fhr6smZ26Be#q3IS8TQ<9YTe0Nm5a|29Tv&QW&zW4%8xy=V{ClY;G;e?+fhr|iiZN -N#p5j|+vPlDr1)8hTC(;t^i)9~Y@fz}^o=};Pm1m*Dtj$NJG_ng`|wX=05!X5ej>PaS|VS^Gszn9t06 -)xO`mFGTT>&*YdyWecxeX(`-ZR>8|TMM^Z0+zt4xh2a1zx@77!s+Eo%VW0E;-IEO3uJbSO~!PcWM;QuXnbmP@R&B_(ih&CTIqDNowfAC^sW8+`DivE!0Bga(=nqR%%zRJ>k_qYeUSRHn^CO(4 -En_a-O?q7oVfU=mZ~JOy)U1*`o4;@qC -u4KF|}+!A4IKI%Z-|>R;ij*-yLwyv73yzTEnXDpht8kqvc}7pUy);l;()XtV|5U6K6rdcC=^u?3S2NP -zZVFrY)y2N7B@lB2R#h_?CA&fX$Jn$A8~9z+p!Ma{Z8p{W{n)BdgeM37LVe^f?dG5PgaYfl;H_hwxg3J -^FYkB&1Qj=%dwwNM$u( -$nMjMn07}RBa_n$9HYTAA#xo=Uf&IWX>1+v<&eqS5^E$W%ln83$R2i}aIlfLaRXSifQ?~_ccU<6}tx@ -CrRU2Aix!e+Mf%(_^l6>!IrdHY=sy@88f -y!FW3Mgj0lhQF-4`Y%CFwMN_W@?3q#?zaOkPjU=xLgF?OSew02!y<3;~f+ln_wHX`NyE(-nQiD{QE8m -2hq%#Z8grb~=OcVAkoV!0P?6;ItGuy^nVo{jvM7)9FpRoesUxKl~DCDrbxEuREgM^UO92Xkl=`!galY -2jvhiG?9l$QHs(8HGQueOjL%sof0V4h-##<%Ez@jP9fNm@Nr8$q(BGRj7q}}mF53`Z&&x*MiIo{`&XO -{5;k=#Uu?%QQY9QysG2?iX{Dx7>YROdjY;evJAp>Lw56h=;)RDcLaKPEQYfXB3Tj2#^6?);=?h8UECn -Kj;D2CeXZGXn);V7qkw}TXo}Kya?Cy9!Zf3Z)v`RcU6W>Fqk9im$;8AaPm+JKy`;D-NjxOPt^@)lK1c -E5l0x4IiMr(v5qojlyzkz~gM5S599GV(kO`7V+F+*V;nUz=$4i;fvtK5g2P1Q3L08%|`k6C3Foz8_7W -|&7Xk!-6e!%g@U_-$?u4^tth&fFXvsNySmrVDQ|*NU>9BGXD$hlmf5q9K86D;YzNyJpDL=n<%R=I?Qr -VQ|RjO;D8_DB#oSD+q$QM3RbHkGjlTAg{ -X+v0(Wph@&ptKt!WgiWZOoJ`l|8MB_3wN5%8I{0dm8C0hejw>a6#zoI+ts=;8a;~988lSkJ$zGjxC{; -=aCocvJnNiy@z6)=LW=_=Okth7o`Zv0LYOt0p1WJK*m#466Hgg2WxSz|hg$KA2lG{#~Z{*m&5qtKi9o -==!0FHTPMR5Le@VzC-#gvB0!KgABp{J;&mFiL>IHaQ#czSo1IwLAzq#j0*4_%0U0cayDo0Zqd9S5_9^ -W^4fQXJd+b8cDGwXwT0V*kA#beyFo*!+_Q&rWkt$QoMC?WNIz;V`P0uF2dMTYVn}T+ZlllsgEs_>co1 -8ZsclK7&Ys>jC4YDce!YDJQtG!&?xkwen{6P_?U>j^ -3P;rK5G~)?iIQ57vZF^0|!F@ldoC6ta4N)~2qH1D9b{^&1WuyFwWb!AH%O6Miqs6fNce3q=2S|?gv=YrF5aP>OrEISMA -9W6$Ut*~apOE}JI07fHDULjs}2K5~$^}XHLSA7bbXkQX|qRv4O7T5?wJQX7zq2XleqFy-|Acp|?k@1C -#>|sY2QYRhpP7^!GEjz+O(6@(e%Q;{d5TiX#B~E3GmpGL&mAK^w@fl|347;m{G!iEW+u0au^BIClNHY -PMcvbKXiw5iTyfbK`M34Y&V)N$ZZEoJYf_I^$F@%&4~r406Jsn@P<|;oMc(p0g2;PLcj$goH4@{fkRIfqcg8)O2IKg36tnOIhTY&_VzdCkaz>_|M1t*2WX&o6ClY&~PY8_z%6dT8cHE1!6Vn>RL}vEQFIf7yJ -p`E>KXksV(G$Mf30{rjWsFSqZqzh7x=f*3wIWtCHVKi~fI!P>suaNH -;0xqIdN-7CMr-%qc{amK-#g!$zkcdvZDd-;pq%Vt$ZW>>sc|GjbR@5c}Rx%tS*kiPd~x%8i0?Qg90H* -Oobz&FwmJl4PI-?`Rbf81Zc+h6~+zrNOAf7ris$;eU}O9%nqfjJ1f{Mb)6=P=tNIrHWWXpbb)n}bJ@LgSc -)dg{GX0--i2=nk*r@{3K?a7`uL1w#yrqd`<*QShbHUb2M8C&ey?19-R-<*A3-4bOFXvYfVzIt(cu=>j -2$Sox0X7r(DS&SQA=1~KA&04z1#B}jjc5c%PjeqF_yY`u>6{8Q6YxwF&f&Q0sCaZMWPm?6$u>R73%O@T!AucWmH$Q$u=J?Pn4^}~p7Q9us6IJ}N1=VVHVj^3sQ -A1XHId{cb47eJKs|DyL%VrFzp3PI?DU@)f&6nHDR;`zgz$Rbr+Dj32fTq?m=m2&LWhzDBR1Yj|mlj+4Bxw@N@zg^ -s2-OUl?{nh;sH$UH#_ZPRf7uWY!mv`jmmb|^Wes^_$b#o0*-;;~$U&-IEuHVfG18V^v_EZYg0L0{INt -S4{-7)5fkQPQHS+ba?JO&qz`*ng2Ygmg>EfF=-+XoY=d(9}o3Ir8ti&!jPN;a0b8>u~eBRE^Cj>sz=~VxH$Kpa -z1;VFQ9;4=kclDSIjUr&is?^3^6_8ioY1PrM_19oVs3eax;|UlrSM*6Q<0;8l9%v0_WC=FZYhs#Zm=b2zusS0TjXAnXi9}_u^PAG8{@tmtD3M9+Y`9M%2RZ%XliySt9E#=C5^^--A -l@V2nZAum8)n>)SLGpxUbT>wY45O4kv1BiWsYlU<=a!)DD1F+y7>bQxm~CL52$EhX8^oDyANB%5iH2F -M9a*%gGR3h>3mep-7#C0HZB*ou**>AN9HfN30XU9|htc1GeNQ3e$9#k3aKBp)5q#nh38Gcd&La%9Fd7 -LNDCA#bT^gH)?G^EK!j2J*anjMIcZ!9>!i@Wo^;{3I3rqv1(X2rSsT=p9bioTy9ga0T1nVaEA=0P5aM -sK?XuUJ^K!<7Ki0hdiau^aFg(W -(^NvyU8wfkI4PayBk6iXcE@YqU(0VWD;31=+Jl}q($vabb>V{MuIdSKDOig(;g2ND5ATac32_qg?XZP -vQQ7x6cwnEtR2*w`+pQ(19-r3(H`6e1uW2#cOWdQVH&@Tug>UZl~7V^3GIRw=o}ikU62O7;h8=gqaCX(K-0Z?ZC%ep%4o;|T3x~HW%Gy_W>S-GHs2F*umu8A)Te_1#(4I8+$nVD)4i0LCK8o>lXK1ltt -ajwG#ZbGFNuLEwpSw@}n&LUS*QB-LIeUbf{-t`i7`>5x&+E3}7@W>PcPtxjUv*fNfjwPP2h9a)u;(w+o-a!$4ZG}&IsaI* -uz^Cgz&K`x$(V^8dFHALiIsM)hcrT_wK@mc;3^r6KL99(EpB9TwBy0P7L8}{i3^<{iqJljM?{LxlE2g -%LK@p?z+DPdWv470!B(^usmlxTfkICHc3Gk|{*9drYF_U`7=ebGa!P#%_Kfuj8y@z}B%W23!{tQ~=F{r)98)@#25!YjYZ}%KwM=UeG;AIVL@87j;4T)u}da1qu>sC4~ -&w-`)qOw-Xbx~!ByRtcS+Dma7bTE7^t!vtFO=Z^$^bMdF|7SVcaW%bY_(-#geK`HtMeeYD8HFFUdvSQKfm%7j;hh38=m0-t-%r4>_n62Kr9I0VakDlNQ138B(a#kSVIvyYc6#KI@p{xD#;0Q0uv?@g -^KF0$(561=z~;^+OXg%%Df69ts-$_rujAzJTG4zD+j;<7t*ewiD5UhrNq`Xx8> -ct+|PqGF>%$BW_xX;sjFU+H7`VqRkpCDajG@3VTvi4N0n(F^%rjd_Jdrg?eRP%r$VJ}F6PCx -cgzBl^8B<*oF-RzZbO0~+-Z0~@92iC_&}yW-5F6;W>Xg-m*E*{-Yw`C9xphQN7+#y`t2XFQ*LD9@#Bn -9?2M$Rka1J5Z6N@Wc6SfMp_1k?3XqXR+l9Nn!QAh6?ftO~jJ2fca`(D;KOn}yT?j&wX+1&e9YoxusHM -T`-IA+$rFX64H>$m^0zR;?()v|fm;$5XZ?s(sz1lrEUlg*1-gyxCnNNus|WNuDxD~4V(wI%eb(xl#=0 ->}H(dG?2XF+t2{RTBOb3Z|C_o%=s5KF+OZfF8toJUh^mbSFO(L{@kTR*E`2E#E%F2sP@oc7;4Azb$^B -6Rqv}zw+Nsf1idaW?sEB_G5lJ4L=n;589us)YOq$1+HqdXDHVf;}={E-w#RAahkdB^o`dpdr8?ACp)y -9@`Nb>C~6q;=9gl196sU$mHD{x9pnaV1<#Li4@rN -&d^*vdq=ex2-0W-jaEQY2(?OpzKsY-_jnzh6H95`0RQeRZx>L;~H7Mt7spco29cj-%B&_HP%du-t*zf -BWw38~E?9;-d`zaF%{3E@dj?Bno@4davLmUiwMmN1^Z&v5>Kxt;KEZgsJp~VjfE=qPcJvPJAneB8`L- -uEmP*MKnttKlH;};RqK>1#$qJE`U%H&C>@bmH_SvCrKjLcYvbsA~##gFm+Naaqb5)5mvg8qJO0@`U5J -)lTHBifL+Yhnc~4u7g3gqSSD%gyGUgSKyHwE$cN4j{H3qTKy(sE0_1>jmOwp_+o4!So4x)Sg+&*sqY$comq4*sYs;rJ -R~XDf$s!j6~$C&wYDQ>;`3NsU3|L!dNdu2$(6XAUi>;a8J~#$(G|S+hvMtx`t!w?YXK0`qx0+E#lxszbU$58#gVu?nqE(iznmUT#pRdj<;B$)SU3T~=achKQz&PAHa@=|LAj77#=p -XgxcYo_dWwYswWBXkhbeL?jxR2MpH6=Hd@Vj-oSuv!@#7e{JNkG!=E9(A$EQb=v!OUSIy?GhOjs8{2W -yCc%%k}Fd5noz<`MimzMfp1V`Yvn&abENI)u7TuXE(DldJJi98D)zD3(vti!-by3KnoKhzww!kC_e%y -{sDm!S^p$Y<}8 -FELPovI-}iw8a5mB;{cs+AdwU}e55+!YAGBv5VD|gQ{15v#;-^Dl9Sp_$0f=la0Q5soz#lrR@YW^?AZ -{8DDcuJjek~6s3$AXGgzXP5x)Bm|Z%LiT_X8lR+XIx -tckNM!E<7G6*wvAbU6Ya7pDn!99I7i=iccL -n1B3xCH{-8+6f+u&;vI8X2!)AG(mn9X5xC@8AkoJ!9B|+ -$zi4ie2*iIJ>i5gFo#3OL4vHd4I^s7gqHY2>&U5h3lXGZxfdp%|ptG7AHPU~vgfbaifgta$;IPyx)q}4+j-mo(#lq(11x6j_27Bd06OSh8 -Tk=iGY_n@~S}z#yISYusx$ALr_c@*+_+41pI2^?0{?tCbB?Up|g|)^$9yC&KoMJqyNNClv9N>2j4^qp -wtv7r+J~aCT=s`o}oHwbwnZghM70O51WO3>ilzkA`>^R*azDML~N-tjG-{t#Xn3OVH#8se@whvS)c)3 -XpJia69Td|2a9po{`X;&p$5naX6$WD*frDd -x$Cj)rakLN$zvYD22{F(tT4(Z#eeg -;+JTmwxb(Qa#Ix?kzuAFOwDG*E;x(?27Q4i06mU<3QT=&V>Ob<>rJ_#<&E`;Z4J$U+8aM$3YJ5ykJIz -l=Ej&8ymNcRk~`a^t=e99yF{oa>a3kM@&Kgk+zzDqAgBgL@UQL-8ZdQH^=fF+3U+HJ(^Tut70Mynq*~ ->7qbtk(PpdQ2kg64EKeRO3`G|c{?o+P($1?!CbkYUKAhH?^=osTjECCR@rF5YNXc&z|da#!Gk0WeE36 -1L@?!|cl6?cr75_QE~E+7RGLn-W-FUOY$Z}-LA4?Q4>=t1PbBMh-q-DGDtaj}HLF%%|h7>VbD$1vDzIGPX($Ba~DPrp~^-#@~?jzrD1?+sEwwUhT=6090R137zLu+ -)9L8?a+n5NEtUDLVD&GeKkCE(8W#qSSfarY25TjTG-$mKeOgIH8K~FhGKOSLRnG^_%dB_3*s=f&?y4L -k;c8^Msj{^Yvc2|2T{Wx<>;5L!4FO&3_=yzU$|&3FQ;T3@hj<>$C5%#mh3Lj^iqr^J5asDLp;=NF#@E -;)9HK^6~t3*kX(YU$Vu|FUeC^Zz{BP~RL8bPS4## -xN#!yz!*8RT#YAe_*n2?}rI=JJwixQO4YiG+$e2`{61}dB~x7;}M9-XzV=LIUGNK;LrMMIP{)+1j-=dgx^lx%iJci`I18$X0iz6qy1+VrG&onVSG-)!E}7!tQKsu -vsW5dP_JXiy3Of7(gdIEya1{v95 -t0%^XQwAJKHi4U`;O|Whh&i&gJt`(7!Jf`b -tI(?u*DatGD-GC&iFMVhXle~j*Y`G*rTgZP525neERRAg_UkgzcDv4Whmpe_R&m)BB-iwX3;hTtSm8R -a0YL<(JezCJs>%-;xo)vg$3KJcLg&2g#boYRCUmyRFUOCJITT>e57N*bL_&c+{0DF`$opbdneL%FR`n -v|1>6oYM`wambPq2_6s-dR1FXcpib=!Q|)LK#!l1IW_qRGwUK -IaCAdz4WETZ@Y$tX8N^o5SQ=fvDYyQ#bnA(Yz-VwO^(OB@>g#|q!?ITK9k)>Bnqe$%3aOg1&^^j{f>@ -*+g6XNE;qfNxl~!BskS@m#o= -N)h!=s?Hiu%ZxI8Dy>%`5IqWNsp$XsAxj6RzHP9*2040^chg;$*nKI#jrIJ;F%>-Bh<=de7ZRkOPOpx -4Y7X5n*6&cdC`&gYSGuf9E+y$Z7A>9|?AW1$N1dBxgkWYeC} -4hr~gFj+-xSA-04(*EhZk@{d16t8#0P4mX`RLrc@^F9fcHVy5dhy*+?Ht(hCyq|3GNFbDH5V<>}1BA2 -O$+Z!>sjH`B$GCgXzu(y0Z_3?#XD3qb^E-REhA5!ur8_a3`|VD?dM$nPhCX;hSUjkQ;3^Prj-ky|E#H -md0ENdd77lQ*T?z*%Y5Q>C%k*?yj^Ot@gLwlLCvbMfJ%+&-wAAY;x?1R!&Bj)rQ)#x_=Gty&w}iWs-4 -bS}-8S>}RJ&Ei(q^}15pS~Fe4y4uVx!&065LagzL@1IdS%;HiQ8K*;6*DLHXEQWqIDe<#wI~vBLUfB! -y8E8+KBfY0=O8C%H0R>M)Ti*4^aD>#xD4`GF`>dqu&y_Y`~tX>|JVpK|(w6aSKSB2iZUZ{m+S -|MA%gyV4^ztFV(8eLdO@Y{w)Bw#r -BaJ;r#74;Q2PW79T}H~T6I&~{`TZA*vf@j{Sc&;xeKB@6yL>t@tvSr4+kg@^Q&YVKVY(PGd-T3W7Csu -_l-O>GS0&%%;QXg#E>Y9CFiS`9GxNXiyh8}r*fXhGbah>>$ft@Z4k|zK!K5M%TMdhz)wd=#v1hS2Z& -a&Odeb(2%H&x(X1ak%GV(Hb#lF6CtO>obexX1`xqMBVZW4HIekrg$DbaDbUYBzf&670@mE@=o$AlovT -zn>a8@iO%R1`@+J*DtKKQW2i%D+#so!hFOe0DHO=Tn_;Ru*J7m1y -2fDxe~s#>R<(eo|B3@72G0;eOCfx$0%9m-kk+-F8cssO!pUkIjzBVsvh>*t2&=opUaKYNX9+cF!055S ->;EECKD|%n3Ot#RKG9WYKdrT5auC2mo29Ot1dx+PLr|xm(YO8pLf<3iZfjs-DQDG=!L>vTU4Y@l8qrM -0R>frB)`1296&%Cmwc}}^3U1*E+ffxvlLtVjbtl(}g`eoZzI$l$cR_rfA7l&td=P|wi&c=KrI0w{LMKmC31gcYd6 -GgH-O$IA)WO(>HnvrUhIo=fa+oJ5B?m#A+{bBw!(ef$J7kf^77!Qmxn)!ba%>(S$?C-G5$+Q;=`c$hD -5<(_qD7pV&bEuD1?=gQm|&df$D8Ssrk+Os8?%1XQAFJn&8&w0FB$R1Hmi^Omclph%t2e$h(QT>_Td5p -2Kb*6^a(C$oVk30o*$my?79QDHCF&+XHr^nEfrsF>|C%_Q7n#Pf|+Av-^TJznF`0-G@Gqz -nb?vzd#n95uESP(Q4SN$4B1(u-BoYY-N70H)RvDmVpt;*?F{AsgkQNkI3tQERWn1nrS(*zlO!jHk -xRoX5zHT?B!>=PGqm>y9~yYW(rlc*wN!EVY;)+?z#X1wmIE{kvB|=XG@dIih%{HOWc^e>QeI>IetNm;y$GLXo7{q -ds?2)56?B_vr;j9vKF<&hQ1J?-q!28h(`!DtWwOY-S;gY3 -k&6eZP4uL~1kx`ernK=t1&md>5YXgibO$OAt#E{3rBe-HcRhQ>P*3<%NhI(4!jiKH4S0fmOfRyQ|pu7 -uTKqIGySa>YQ=;eaCiq1J^AL94NolEjmGKjAkRGPxOGrYsju1}cX0p(J2q&>CL-Na4qUD%j@!^HBA?# -V}@LPi%Ih;Ilis(Rq+1i)ycnX#h_SvH)d<>Z3A4LQD<2_IZbs1oupUmN{LjV%s5a2;e4&{U$Abs>>t> -&7tYrLU`{7Ma1$DJFCRP|5td7CJZh8+faIk#Z1N5>CxrocxoSAT}^&D#~X~Us{1Cgm4|n4H6VStJUO} -^Um0@Oh2kOQ)kU*;X^NzUwV^I<0QhBe7(yy=+RUS#8^Rt@JMmYsWWpH+s0)Dr_OuN4sv%qr?g4 -^u@hi=WqVHrw0p%dpy|KGwd^xc*ODR*U9VGdaIjp2bMZ}`jat))gpK!{-L6ue~33fsnH!BNBs{d< -sStUz99rBSLq#ms6(2xXqw%i#_vSW$kGj_Vxhz0p0AE2v*s}sh2mvmhG5#3Ny@QbEuhAB1->7TM#cMs -@RqT??~*;-CCA%J27|IvaiqPQMw8dMF8+VCSRqg-4!vQm5JQ92ksmnST#aQV)aHMlC~8$8-o8f{Rctg -Jx+-rAyTJ?l>J*#phiS4#iI~(C?8ENl}I -jY+|o8~xv#d%_-ub(@#AY3OzqBBW+`e2jwnYz+nA6Wk`0R-2{5~-yc?rB|VsaS&%SVT81T@QVGKsCM4 -`C*<$#A}eOh(i%L%bDlUjR_z0y$LxLU`cs4`*ue8WkI!S$qL_{b!H^5;W9+@{|)IRf2LoME-a5=sdc$pBI89u-N{vBZuG9l$365z -!Sd?$uD_~8ff^HkE2g5D90P_bq>0#|EoASjw%0J%al$FscaF|*DaD7Kj;-fwLXgtk0~I`IgM_OqajpcG&b+}8s&dT6iIe2bo8fVS!`qxs{V_H(=YV$jTO|PgtAdS#14L`_T2Ayr$bR|i@ad{;F28X -Yc!Jc4cm4Z*nhVVPj}zV{dMBa&K%eUtei%X>?y-E^v8mk -3kB;Fc3xeImM6z1QlEgN^v3R8Oji+Hnh!zNow)-rZxo^-TwFf@JDN1p>3&RpM-mGLtsg8^NFNEOGc>& -b_6Z~Vf;YiRcV9=uyfaz?&MO-V}hRj3p@@KL%tA=(psy!#xY~5YdC?vinDM>iR@=Y1G;`8l*!q#7q*> -YdCmAu5_7Z&JF`ESqgLuSz)XT6kgTizpD6}DF{EwdvY;ZCKl*Jq$Lb4EO9KQH0000803{TdPD-13rvd -^101pKK03ZMW0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gD9ZDcNRd4*F!Zrd;ryz3RLdZ7SAxf{?SO^ -W7_8b#xr3Bp>-L?x0TsRZroyIhj8+$4=IGIwWZhcmL8tZ{*jW*C$ns_N*>#H_Ndcm~}Y$5aA1c$&cn{ -~FOAP^x3HpKsfIfIhSRcu{Bt+FJz!&scAsg+Zhs4q5Aw2|R5)+{@9qw#Tw`n;aqSVtp9+tqvg+# -Y}4vtU?ozZs^IOst#r{!Bx!ygkdAq|!WA$j%ZSo>9?R>e}FosFNw5*?0L1m@MW;H3*}Ew?=!&V!TBHv -Jl&}w#CaVEc>X{=;i;MwqAVL1sX4lt$;-q&;z>k`w8Vjk;CJUHmlvRbdoO%`_8=3hI|hdsIHFxBkx7# -yCBbwQkG7|^yp*nKjna6^zaI@Ga+55XIoy!sBpCqT23GY>pE5o5rY$=PP3F^Q~d={O9KQH0000803{T -dPIwlb7KjA^0RIvI03!eZ0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gJCVQ_S1axQRrwOCPa+cpq>_pc -xv43i5(NYFhM@I#lT-GFT`ZK_kDLfTAUiCEwh&f%t -H8&eOs4SDH#U7NwHfj_FA1-BbvN;g1S;V*YH;h#+sa0ys@AS>*RmgZa=^GW8Jd6RJGMgO3=N~Jui=NA -BKB58tp|sh|)T-ZgMXqoHcKnu@)(JynOtKQ(yhmVzDSCHzwMlGbbCFwtBZ<_=x(6OHpwv>O*k?dsLNS -8}|3|^Ox;%&6Xb?etXzHEdN6gWa0>|7RHJ)wG-b-=$)C&zv22|MxSpU&+elOhugEoFlLGHt(~Wk@8#4P%ePfa7wx4DWxU8peASIs0OOSwyBAz% -g69$%Unq0XTIuiEh%k%9(3y$UT#W|X>e?H*YlR$SvzQ*k#F|n0EPPp_;xkGQdTtm_l#dJvz#KarWW1^ -{9eL&I`krBF)v7_F+Sukk`kiRrHdsm_n*~wTl^}9)S#+Wyk|T4-WP;$e0_2h+`0LBhViF?KHKfrbR&n -VbGAkLgk6aqKmUYwOIcE3Iw5iLw4)ooHo%kc3G(##9$y-B1A+Pemm+es)b{0}H%HOs@0Vzq?xO~Px@2 -ywaC=0d4Vj?ebhE>6Y|0@e?Wk(_FEc?#HykYl&fV{p8r+~qjaLZ2OaJ0-btLNlI!47aSP>Q=qsy>jD^ -okFhRCdK>Ac*G3e|gFJ`B?_JX=gk*)fUa!zzZ!r+)qJp}@Q0N~UWfcC8}<9@#Xr19 -o?N`vaC_4XZ1-YLLLYega{sp>*`8zT6Q4ha0?4%%KfwK;C5)l&V4)5m@B8uez(3*0^0szRnV$jR-RVZ -<@S>fIRby88tI+Z|{PJIDJ80OW?aX3zE%5;>aEfOT1NzS!gicbmEqau120cob^{EpVD-81h*2pMu9pX -!FPAYA|NMBdE@H^I$+upR4*V{?CIc6T*t1Z;jnI<&Yk`inm1h2nU+@cdXN*Rds5SCZpWN`W9Z9cTR%d -TPJP$H6n$kP$wESXK!7$Q=M(dDBGcpUoe62zXsyyFyl6%?_if~(k7y^pw?yyT>zlZZXa$0q@6aWAK2mmD%m`=uGyiGCz001Qe001EX003}la4%nWWo~3|axY_HV`yb# -Z*FvQZ)`7PVPj}zE^v9BQOj5JFiQBRt56k7-Lj`Y6^y03Y54d!#&~tygfwo(FI_L7IrL^R)f(iuySOAM -#=%$2oZgpVD^tR}GC83IFdwSszFGjM;MRYy2l}Y>a;`V%|Vi8E2@fE*6H5RPtu$-8@YimzH~l%}_6!S -d6qJ$)hVD_@Yne8=6Myf}1qT1RVMt4BXuIm!ZRCa{9k8SavPHFh8-xdXzZdb{W6Pn>v9x`m|^XSbAuE -n*tW%8&FFF1QY-O00;mj6qrthmShtW0RRBE0ssIa0001RX>c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eV{dJ -6VRSBVd7Y85P6IIvhW9*$CH7m`kQkB5f~uDfY96bN;Vr|I$|_#0xEyc9UBdsi6P6wwok1c!fCzX9d;ELAP+w?@;rD -I(wBZc#u|(8Q~FX3Xc4zMK5;1nTUC25jG!CO9KQH0000803{TdPTIDN+J+AR0D?6D044wc0B~t=FJE? -LZe(wAFJob2Xk}w>Zgg^QY%gPPZgg^QY;0w6E^v9ZTI+M$xDo%ZzXGKvW2%v8#ZKL;XMES3lgxOUTyB -zF?}O{pP$VSb%vTA}wyx9vy}Ni3;6qk&SIs2m1K7nru>0EugCLlE-cqs^qRQCnl*HMlteM#6oK$5kXi -C;)O&U(g13V=){dYsT;L#+2nJmXKBr7)?R&2~|P0dvjXS7J;+T074)5*Fnb3*f&Wl4rX%4M<)PR*+X!1sY -?I3m{x+q0tg^?Gi_vMlmOe~k7)fK1NdZy-G{!kWFUoWYMH~jK*-M|y; -MztQoMJyV=Ov;pU`ACw}i#1fWYhEgXxF+kKT6O+N#nOydcmA+;9Wctd0n)8w57LfT8C|-oMgs0FkO)r -~iy1jxoXyA?UY#voOqJ&TE=`nEaHh0XWm9KGS?6Jr@fm5|SnK=;c%`-;dhkyl-=Wcxn!@%6|K{AT%;uUE{J|DSrOhi -mb;yzCGdfr5AFoU)YG;0o|E9>*t58J -{*kdWJxkK%)C?ASt%4$^Kz1(vD@$7gclQIV}MOgw*M#VZR>&?klk(@_F}iA28B3lTRI~d7NY&-z?r_e04#wGIzyG!lo+L}{Uf@Ie3c*SkNR7<3C -k8YBrA#&REBi7Crc-a%z)I069ZndIftz(&gc~jQhy#QK!n0uNMRo>gVpq*%Faa_tkm&B!8+&={&80FwkbltKo4PLRK4FC^qm-_ -iO&DBnAckN_Z3@YOc=l?AxWGIzYwxEADoyJhq%qme$kbn%vbQ4`h;5;ZLlB4!=xa!cV^o1csBH4W+N3 -X$R^@e((#H{H>J^R4WPYC3M@Qi=c+a@R)v)S@Du4HE5|E%{mc8`s*zsYbqW=?@ut)U6qzbMJ8F5A>YP -Sl=_6(99Mo7s2`-M9|FtBrE3Z>yu(_WHR{?aB?71EbFuj^c@?|eXMuRkhEu>t##x2h6~Lv$4;K)DQxN -DN#SSI$TY*YyJ+yzY8rXpRQ?Sg0VuwngcP9rOYtxNfcMnd|snS>DN=U!l?iZ#~DemUufqNerVQlwnvJ -oZ&*h@1fKtWh3PFmGz=3iWt<(s0+YuGP@Hxiei0L{_^BFc(daeW!P -o?Ezd1VibRg8k6M4K8A2|NfuwaESSBNMB!%fRZBY}hOWf>9ILH+G^gwUv#?ap9a2I@UpfND!i2?Y^??|bkFtoee&guU9*USDQ*+oL`hs=gn-J>)Rn2Tb9 -iN>Xp2y&U*eP-H=Q}_@6}^SU{IKcxGq3z8o6j$+BK*eW(}gzbf(`j%&>q750mp++Grc!(>8oSnL9HRK -ysGnG-UwavRy=e1{IXWv2aKf6y`uFE6_R0rf$OzMqU|b3<@KwlSGo_&U}D+HSWs<9BW4cTb)a1m4nHT -6l%5t5YPx8#7@;f1Xo&ZC!3^>E}`K4ZX0+8fmgg4I;T3UqRnQdT>7SV869_M@bc9Wt_YsP`F -U7aVAm0Ae+~0i+o+N=(3*My^w!3M18y!2J#u@!ccm8*sRO3OrV+0J&*n1~8YrFYn>fYBAddt&xs4ZLa7@tp`u9dt!pKK8p=9- -uJ$M=2Zr3^v3hBDDbIao((rbxxv1ZzJ9Y6*pE%Uaja<=UclwSgdR;sgM+40Cl$*258>KfL~MK|VtWqZo(4%8)UeTsFkO5n`ctKW{PtTRzL -sw6PbJ@CPc-*y^nH7uCy+fHF|cJ`LC%xNWnTnL>ahEhlcBgDGEL7o -@r;M5~ySJA+<4C6nRV)$2vawgn97O$53Vb~D-O2OOeoE21|tb+13;M*t5jIAkJAE+<-xfd0yK@W%=Z} -QkfjKqL>#eWn+X$k!8J-7xv(t%ogcF9{qfmjL>2|{LV?_tLC%EsL}#)oMnFq~VvYc%LDH>jhGz*67%3iNTZ5S$7>&B^K^}Xcv-T7Gn7&Mz^pqE -Oa=$Ny?VJwD0dJQIP3h&^(Y^qXF4JEd4JZ>gdhg^Tn5Wj|&2qED^UIdmo{juUvVpMYGErOEEbWOLp{s -9=zSkPEDj{i6Hk+-hp8-RD>yqAA*?o3g2YCcaa|WyrB<2XbEppeueU0 -TR$K^GVWc_R^2lF?WeRRFF$-+3CC^%c_Vd4OJ2gI{5AKrITlRSHUVRI(Q(c^h0xE#$c+144k`w*cufC -nEQukZkCbMamE`#F=Ruk9Kvww8L^(i>9)@yi@BKRWy;nJD&a3;^b9LHoQZt)6ZKP_w+44vu-;ht1{h<9j1@ -;{a%?5S8R_HLqPkziXA9nrbMptTM9D*LYI$62e}-Pw-^r1=Q0=MwcOGQg`F2&E9Ojmw<#s|EPK -$ibs)KQAeuulP{+l_meAba3x^!j?>W@(da+Epx~|q?W_aSBsMAlUpA_cdT*91BPe92DX_hX6N|wX{mLX-Oq`20BoEVJ>%w@jF&w7e{%cc3HQ+0UEwN -<(eG8>%EjG_=%P_lhWE6!!=27Qg^^jm&$F#cnVoXgwx(+R=&0nzNe!uS&;ioE9m98?T{zA_*95GPg;evmj;DLP1@#r{y-&$k-ZH!-%p|4290>k9NeX8m-r@O -I~&p`r}>>YnH% -zE%JZSu;2haC*!2N3*uJLpxay8U+N#KjI9)Lb79sM=S3Oa(%zGrvy8z{#(%4`@E4@S7UlOg+>AA68c- -TikJhM=I*PcE8rXTuIaY%CP5+-sa_i1ale7MP$1^4jGwnRA*FgJ12J@W0FHH%L6nLudpR8I~(qK+EKf -Rjy82u{7DBqG9eXN3)}LvO}KB#q6f5}Jgoe-dsaDaJ3JebD$wWENX+5Azr#Ja1@QL=ZUg*v${Ewcvz? -Q~_UmStJ#rcs9PQ|SjSDY2M08sSN6VJE-@xnj`S#mU+olg0Y^U#lk1IT@l9>9S~mZ26b2io(ox)1+e&pN)R7?qm6@u+P_5?B6 -(DA}PSdV}pZF1Tq=aXqKb8HzI#u$W6D;3Rh%k<=j9Dd~M3tEb7AB$(Xo1{gXDo|H&YaUaUW++Co8zbz46#g${43s^sd(9h@NTc%<~5JientnIYBnjru++BHuS6iXvGxsZtnsnyRG0>3H -KMDddNruF3Cez;C%dIg*@nw%N)vEIgme@RT^w=*6Y6tMHuPIYo1G|6`IhDHy-4Z2NW2s27?FB~n)Pqf -d$@aW8O$cQbO|k0;nn!8dNGwbYrQIS>1jzx`KE4^T@31QY-O00;mj6qru)(f%6x0{{T!2><{m0001RX>c!Jc4cm4 -Z*nhVVPj}zV{dMBa&K%eW@&6?cXDBHaAk5XaCyB}!EWO=5WV{=2F_t^AeDTOX4BZ6Zk?~}@-iJg#923W%Ulxmnwkk$?Qr1>V@1Kq3&q4 -J)hn26svQF!V1>v<^F7y9S9U|#4hL8$=a5X7vdK$y&&>G#+!5}Iz6Tfmerq;L}1y(f~mprKqRSiwU;h -q!_4lO%MtxD8@0hvhJ`pAe+BNo509ugy!K2gQ@sl^%2VA{LHE=BUqh%=%Wi~RHv2r8vuDquv=X6l1~G -e`hBz#x>t!5)QYW-wNBI{rtS35ifc#QBc9ibEV3D60KX*%L(69^k98T$4q;g4YLb%o|129W$KsNzy8< -O%2Yd=SZRBFfQkhPH1cnAHIi({KEWK5i)h(yXB)G;C=6@)S%y{I`+$Qq -SpwvTkM59U=Ro`ZnfpWpulAnOm2toM6C82TJPVSuh3IO7CmauVfcFpuZJpF;3qv?^>_dyeiyf-_A7!| -aimynve>%4;xjGZf<*=^dlj0agw0)3#Xb8mAPpZSrUbLITxu~l`~fiCL+%zXcjJ&w<-1UVY8{dj -zy0-^sew>xB0k%CxPxXZ}<%cjS+o`MVmT%r;_i!%g!{7fukqP?3fda>n5z0ris@$JqCO6FVc+k{zHm8`poooh+-mXq*fY -Dho{hcSImd$<;Rx?daP?~kB;Hs-A-&H#wd#7R@rA|BhS>#Ja1Jr&F!RLGrf7Zx-63doy()LTCZ`;!=qYZXOc&0;b6qpV()B&d>PDua6!l_go=89lm(5g+Qo4W@>o3mmMtvjIKZ{|i%M~;Sg --f<^>NNR&eCb2fm`csUdf6k{|jFccf6zW?KK0!q#?L0YqnT566NcHK54O9avhzVM+GFvJ^?dQUbX#uM(@s`h+RDRO*K3v3Wq9G?9FD+$Tnp0ZL%^b1$(i*# -zPXyw^?U9ajW;vN$)JKb^Ji2Im)E?%MR^u~MI6lVnYLT{n3Wz6ZmC*+318gun&2G`!CF(De^A7+iTSs -Q{1otU?9tQjzdYlL$nV+JHyHUx1{&B7ZeP5&EH0J)pVIyfP)h>@6aWAK2mmD%m`;=>ZC001 -Tc003}la4%nWWo~3|axY_HV`yb#Z*FvQZ)`7UWp#3Cb98BAb1rasl~-GD+cp$__pjho7?nL+vSX*kjO -C$Q)1^SuZmol2C<=j=sF)2!Y9tlME%M)YNRhIoBu?W8S(kJD4$t{WT9iU+lwwABhGmb|&zcsb*UP0S5 -GgQaQKczU$bHR)EU?b7CL>g0sYvHG(chi2c`~-Vrhgt;4;f(^_j=2>=cmb=^Cdb#K_8D$KSTW$>i>)S -XN&&%qW=l~b-oOGyZa`Es{0CnQ^3@X{WVAAoS2hM&+^qQG2JWl96yQ7=Y1~VWrR?SdlYQNvbqWL$UfsQe(A_7~F2K-aeN=~RK2!t@t5njJd9T+5>YT1gV+3d4Dy0j;wTgPp+a -<_(?}#c?N^wmyk|8Yw!iO1$wjvVOkl)c9twkl_iDl8f7`a+KpH3%ejDnAQ7CvdHTJU@aepT9q -TVtmV*5Ja5S~#1aoF3Xxy+)XCA)AeVxd-k056ja90Bj;S`*88@HQ>3yTWBnvvMM7;3Fwv2?LunN=+{3 -*FFDP(5CPWhFg94#CC<=lTNj*BDDMbyp*vPU09+%HTe+slP0$Ar{c*bk4wog{K8#n5YvzH4t~8=i2ku -632Nlh`x{Fo(+ft>K1V!`S#nzvad<=`<0=R04*}ZXVC%p;TV4euEfFOUaPlzI5 -^0?sb-pd;N&&l{%MLGwkC@M-}WSF~=s?ZcB-xijlk+!;d1-?nt*n4uf?5ggUjKeZ;J -_Cy$<#(WJgXso(Loi`i{7Yb72VdJo^NF3bS)-wz5b{+g(}80UKlj6-x%;ws@paI)*EsXhe|0R=VGH;9 -0cG2u<5X}h1+ylCvRc8cyVGY*rK$O8ZTFSU;_;#nhcK;&L$LN=MU+;CwSY08fRvkYZg{yQl8Wq}5+@1 -eR40im?r?au*(gl?S4q>7dPO -p&Q*7(?4w^l0U_Xcb42bry#{rChnpGQa0kyQfj258cXnWn{%Fjj)&6(jYQ#L?j!2sRft5T_C-6A-xJK -D{>4*jGimlTj22YanE)zB^YL0nts2_ -ZW3Wk9+XU7ku9!|>#pp`ee)!R>o1@8O1eck)%+2M+2+hp9*a5n{G5CYQE3_umwqHk!cG!G7Qn|#bh#QA#jh0-%s}A -el+_Xfw|-4U6Y3x%)+L$%q?{OI9#8Rqszj+^^`U^NL+h;?EIMO_5KG?O9KQH0000803{TdPPkDG57`I -+04f^*03`qb0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%giZKk;ucko~oj#YSkFnGR?oeTn -YCtp}MC}TFLJDw^D3Zt6ifis70M^4qO(h0spnq_chBOX5X`h|F`AZ=uDfY%}v{I-R*ger@oh(OD&8z@ -HZki{(vl3Rn1Jd@L$*0KmUj8TH)dp(esBA{~G?;Y|N?VtJSLDJJ@W$z1$Q$R|Vf}PloGwn_R5`e+|cT -rPngUWo}ix&A7A=5!1O4QIae`q1>?^1IBbT7OWI`{SJm?(1gHS-~3(>m@tJ-LlLtOIAIC*jZo@8VGxUrl0+??oUDYu_E@W=1L -ka&H5cHH4gyk_X0lrbp7@UzW@I3pwY=-_K1)F6$k*dJ%jgW6)0JrpycczO7P34FJsphY3kiX`ox1HqyivMA9G*ZXD3@0Jb(y(hF>G)(FoSOn<1#t2>1a -gk(P~lH>6BP338W>mi3{z+0_`SLS;Wv3cmj-CLx2c>_YO|MECe`Nr!cgJO$$pxHasZ2wNs|a)Jn>56ufYrF2rt!`(%y*z|E1P1fgt}vFy{4&#-K&p^3ChvkuA#W(G7oq)K70j;LMJ8)b%7D -6T1eAEY`?1k2!^YH}Z@Xh%G%6Y(g}PusaW) -XORF&@v|T)i9@Yf>_J}|Y+GH#cy;)j2t}5Z4@t5)5KUTw25F@W~qirxiwGA9ta{_PCQ83(~9q?OpOHn -aEVM+8KLZ5Ucc3dHGncziGey7X(GcsM5!o*P;jonH9cWddiG2Ye8P=4h)@PvDnSliYMvEsgO4y8x -rT~hQ!M$|oe62>SYJ1I{%ta}UZrBito5ReL9g`!ehG^&ndQjO4n%~=g7<>BoeF+co0DZswX+amT#?=A --PmqUl`w$caKoEQNF+ewdY_tvNaUvblIXaOcP)A|*{Xb1}n#3=;-4i4I@li@vP2ttxBmOz -(wSkZ1gSSnini&iNSOwcaR9KTmh}qScEFYi20Lmj1E^ZLjuz}tz##PviI9 -U8Fb6oRve4uReRFkj3xD(SMPtdm?xet1V|P8N*FSwL*9k7$YKv=CYdR<4mJh2j4*tKl@7mh;=H;u~ep -c{f$0!bj#>pqQg^oa-$!xen#j(;BmOto|3qUW=;|}*}gIUiE%`!IRqm4~~&EfYyq6`5mW@1E2l6ag9t -7$y^fEWThr=^&9G4xu!nT~wi#M@(G*UH?`XFNIutthX`asu|L#eSdMOKlppu?#AqUF+hNB1U>Nu_(>! -hS%7f_5J5G<860Z1UVl?(ML?G-Dy@xu)!z4(CK+!ZJ2Eyp>0YWk1u+ZaANgMViq0|QcTTr%wd#-`XF@ -c+4&h}9iHph6!k{!E6$`QW9Cc}rF5AT02Muh2p^ADx4u4+Uo_Gf!_LyjkkEZ`2ooqew{_6%x8=f7bc! -`Id5*~5$|3nlijScQyU?AA=1hwg4}iR6{hOg|rrtD<%Gh;LV_8wNXG4_lyK`g)J@-U-X@`!LLDO0V0>Wz`_kTMAW5Pjq9}p&8lrsP$_HLdXt3LE1)XmXq -<&JQzFO?u(KMoV^ej20Hkmf2NWW -(~v|DxG3pH5*$$+R)I-5fn(kF3)%&7H_dp66sx+x2)9T%YkjJ-tvb-~GXdK%QQ9g#vBAQ)JeUj4@T@a -Cbh+`ft?lKRy$5J%Hllf)H#?T|Kk1zQdd%HvC9_s6q&d55ZHFdeUE=xijZq5c=krhsTC%Pref?}(MkB -In#J!MI8u>lTPc9f-N8@6p8$Y`l+YOKbijXcus1DIZMNjH9)}l8lK#*}Ir<;cRNzQ9aCqJjeoCKf{#WJ6#j`~ArnPrped+1SFY2R -lOX}_?>0(lAL46x8wl`j+5A|6A7XOS*A0IloaPBb{62FF -Gi2mCzaU4AiLbYuNMd1-H=HF3nt@1p+`3H}dIO9KQH0000803{TdP86=8UFQG*0G|Q?03 -!eZ0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%h0mVQ_F|axQRrZIQjMgD?z*_dG?^2_#hZN(@)MbEoboH -<%JB5M-OBPk(9nNeE@Z_W4ds4lTM~UR9$;29%~B9kRSD-ND@rjL!Znl4w1sKgx#v8qFRRLNw4GY?+Yr -LqJsc${VYP8zAayVD+NviPJhIK%=aA14d{E6B=TpWBanj0@m6pqDWfra+>>%>d!*m19z_3wEQN5F01w -JFkCOo&j-)ov7Zg!VvCHaWr=8CT^!pro2U*z6jLaJ90J>`=2t487S@920O9KQH0000803{TdP8q+rFgE}I09F7104o3h0B~t=FJE?LZe(wAFJob2Xk -}w>Zgg^QY%gPBV`yb_FJE72ZfSI1UoLQYODoFHRnSYz%t?(;PE5{7RmjXO$S*2Ua07}sg9Q|n74q^66 -S>mBDvMH6GK*1_27!c7)dB!eO9KQH0000803{TdPV&hS!4U=k08tPC04)Fj0B~t=FJE?LZe(wAFJob2 -Xk}w>Zgg^QY%gPBV`yb_FJ@_MWnW`qV`ybAaCwziZExE)5dQ98!MPtMcZL#lTL%OPup~`@0I3b6+0vp -Y1X?;ptR+$)sW=|`<9A0=Z&uQ7{$hzo-pg~(9Z#aFmA0(lrYOZ`Cj6^1vs|l+S$MKL&1)9cGsr;NnHR -=!%F_Z`**Q0GK?|K!Nvl;^f^N4s@kO^hn_0a-nKArbi4s=IUa3ZAms%@5gQpZ~%T~Msx8V1^V)gz_{P -FoS?36{ksZ>F`LaY$XQ|_Z8RjR2AS-%un^^VUZVOX%PW<^qP1`jOv!T3c3fI!6?41k!4|?WI -<;g*G1A0@WkQA>?ht8mdh(J#81zV)c%9teu2(moX2F14y12bOyJO${%jL(58@3Z=$#O1ACUUd|&J^RN -2A=I1JPBisv%K@Rf;NB7dGlJr?zKB+;yxHtLn`vUcYgyZZw!!*Ha?IBkwG!H&MbvM@|qjN@CdF2Wlo} -c=i))6-%ekMtBv}dVp3U_W9rq8nSE6Xxi#cKt>F=Ag`riTB&sI%Z?_aQZe! -V)o{q(XuFK?}#L$0*Tf(uDCIuLJ=w+u=^+Ho1kL2x-&TqcDHsxVpFC$OtF`k_$`m3r*!VBut;?xuy*A -AQB{T6R{Au`aF7gO;os$~zbwm0UWJZO$_4;xcFg*I%5i4WM3OAd4|eT`@IQl;R)M!b+8HLmE(-DO6Kt!+P~ -zkee;vaT_uPs@kOx0m3()ZZYssV43eDgxj!d5r5j|HBGPv+h|GW^30#ikchj@d@&m&dYIKS8RAqa){} -;eQSYzGDay#zJL+}ZCthv7LTZO>N=s#sOxdFMfMCM@o0il`s9n1YAnI-k9IO~dwe`n(XRh7T*nk9C`%}Frz^}zFQTQy`9Bb}Zihl29;aMVI -nZXus9wQP)T7?v#Qx@GoI8hXO$+g=vxW(Ha#wf5tPs3ahjfC`@&*!7H)%x!8gk2-6b%tXFmI^^cb=wPBSi+xhH?^s-L>Ky_;1v1Efj79+?P@iRwj4UUcqYjMC?X&IAa#H=9$P(N*P%;8ejer;c;Rj$ivfEJo2)C@qYe^IYMMBEDe=){RL!-YN -Dsbq(g?;H#P--^X9kcDGpl~i@1QNOgwtVT)#iLv?k8cbUeRkKC9OrtBkOOatcV?XR60iVNuKaFB(d`iq`^ -HP_Zbh?jTd_kTY5{mX*=!an=gFVEa3HMd3lRS6G6PlqLY?Gz5M!u}Mn+QNPS6}i!RMtG9+^J2>2;AdXMG>b&T`tt2)^4)OIjs1Blk3J4FjAO|jsaO9foIyU{^DWNdenf%3X2MYaa^M -Iy&cT2OOwp5UDDM?P_dwl8um&FgA}XX#n~U_|@a?K1>^3CRQaBe?oXfxUh6MreQ>)1 -9-RGdR}%PhW;amj<5|5RIZDV@UPpO*rVh6P^Q4o9o>u^&U^0t)T(#?2oU0R@R{yE^Aq%oeDfiI0-)8{;09XS604@Lk0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPBV`yb -_FLGsMX>(s=VPj}zE^v8ulEH4nAPk1@JcZX&B4s?lq+YhmPCHE688L7=Gzt+WZTI#wNlH?w<&@a|ets -MGDIP(oe#~P+rNAABm@-&*a^zwVqAvwjXUy)1HNE`L*})A`oiNy#q|xRFO$<3jKmS~rS55d<0zwGmH4 -%KGbu@*VnrvT$06rFbw&jA$N;QPO?|@YUr(roIC`%0XQ)=Z7m>1+1|29NIZZfQ(EQO_HMQMw6aMFIn4 -)8L#gtY&N0XKTChY<5MyT)(aNt!QN#M)Ygp754ggt(vU*K?h;9J2}W+_aBx!OMJ)3%OYYWR6*TMdd{- -HEmm6)Q&BBM(@sRRbPs3XJtIWX4XQvwL0)_i+@l{0|X -QR000O8B@~!W5m;ebHUIzsGynhq9{>OVaA|NaUv_0~WN&gWV`Xx5X=Z6JUtei%X>?y-E^v8EE6UGR&` -ZuQN>#|rEyyn_Q7F$yElTC$ijOZ#Eh^5;&x?;&uvJhpGBC8%GceXOuvFp#08mQ<1QY-O00;mj6qrs4? --cJd0000r0000V0001RX>c!Jc4cm4Z*nhVWpZ?BW@#^9Uu|J&ZeL$6aCu8B%Fk6O$Slx{FH6l!$uH7N -PAw|QOv_Zr%q_?-Dp4rUNG(d`Dk#d#E71V6H8eH308mQ<1QY-O00;mj6qru^7Y3&2RssNKR|Eha0001 -RX>c!Jc4cm4Z*nhVWpZ?BW@#^DVPj=-bS`jZZOpw{vzy4iC;HB(aCE<%4&AfGJSgHkFbhc_ftX)zFb@ -I^%<$=NqUGhXUG}bB``nK13Ol?iCsGLVH~jyZ3;Lg5l(y}YX7`2H{!6YNVNhL*?Eb}gJi0GD`{n$7;e -GheHu!S3{_ATk^gq8``>;%+?%(Zx3c{q?efb}M?)pF0!IP#4Ks8Qd1O;O#MdJiR!4yd{FifB@4%(;!@ -kJ``S<*b4;tnQ)B|Dth5$Fyfb}+LekR5~W5@Z)rJ0jj;)Q(AaA+n>X9m?$JV7<(qMSdIu)Q8cIA$N4R -i}78G@0fT;qB}I$1@w*yb~Lg}KdykRmkRJ567Ohq$3!3Fp#=Hkh{<~(1m5BBF2Q$UxC8Tys2z^(B4(F -h>xeP7Ly~peb+}-M)4Pz|k@SvuPbHyt5vav>5onq2$Y>o916@EZ1O4avA1BI+_JtP>Q;`&&HN|%+q$- -LKn>^1}XvU5T$7afjW8eRAlGOzxp^t*@9gYuQ$%jZ|by4DUcC;7qq_Q^|Vi8N^a+TL_)+C#(%VnC)u@ -6%e<2FPOc_?X5AH}oq?sX9=PNKk`Kiae=ha3hd#Pm#u#YtXR|D!@R#oLdat(qBPo)dJFO{5Vf&H_uQf -;g!qJ3^|!1)y*Sg`gmN7p`Th6ixiX9zF*3O!jt=RO2>EqbJz9eiSEeAIeMtdCp`8PTVUpfu39~R}FR) -vUj7CIL6lyWne|rIMrxJS$~&@E^@4Hu`I4|!pN@mNA>} -3wk`xhANWuiktGgqj_EK=1+G@X=Bezr;;TK?wy0kjTD}-~9oNgW#LLtv#|^0!9vL)q&HBOcgr*t3&SV ->zdv^aY;iIa;c4=*KEWWB1XGIhu^b=2E+jDiLpAv)1n5~)o*}Z8CecL}F#A(X*aU<>WhKHj^7jhLIZ< -zE%u6U^2)p~oQ=iX(C)Vf{{2fXUSD3(dnkoQIjeuCP&bR@Rii7jl(w -%@T=VG#X+G=|OFp*?KbD8zEfSOa)J}TIH_5y7QybPZ%sN=Jm1NF3fddH7>io@>?M@Bg{eIJC<1X%!8MNMedh-Dl#anpj+znDu& -XOj=~X1FH?Kx$7ay$#G;tSeLalpDXjS8d4c8<$<0`k`B2@Aq)>W@71B*JH0C|msT=;-ctcTm6h(5nJ= -|q}fbk{LPmv^B6_JS$Mr&!MS@vug++8)|*wJkvWo-34%jK5VsCgzlI7DB=k`D5nIpqh%KQvjD@-;Qr7 -l;i-yVJ+`nG(;Fe7hwaxjd(7@_OnxACgu=(BnAd`@F_qljKcR%-svmYBtVXyH*fG!tuouhswZQ9zzk2 -@e<>O+l#{tID_5xDn}Bf)i9>D)f+NOnQlmU7k96^QHEKliqKxQ(x<`=(p5NM*{tw6=J=IcY{l|$y!ua -^{jdM}SkgpI`167W{SLK$fExfbzd)q_{9>TL$E|;TfZrq6yTxzei=+ -#*J;CEa20E{u0L048`Br7If5Fev;bd{M+Mfpihp&*eq -AKVDYMJCO;j1qJe7%l;f-eF0Y9fPw316llg4F^4fG>VhT?3s4ua;U1t?psLeOvPt9?q;%nZ$)PKB0~O -4xJzXPv2FlKU9Lcai2$;DtG{uL_`%bfK2S@tYxM6E`z=mmYk|y*>V4gJX8D)VSWOb>~!U>ptGrh+w$= -da`pJbNBCN~)&W%fsSez)tMcu>KwobR2X2f2py|R2pn?A2!Ot9f3@-!uJho8E)>IHRXl@x;64*N{y4}};WOYKTsv -s9l5Gv)D9)aG+%w+@Eo555*3>D{cv`{^Jb4dt}N5YM>&a=E__;dh5ehcT -m{7x<`U?*USdr^l1YHsJGT2i&WIy#YtUV>qGO9P-2+`uemj>(h3si7Muqd3_H~t}>r$&`4m@_f@Z|C= -0BUf!74jV%ML>WRxE=gjF?M7g&mMwR|dn1jp~OvGyc0pQcBJ2L^TKj=iX#Ubx&s2YBm@VHW9Fu?UBTC -X0~{YcVvf_R09Pw7-U2f35{qK1ki`rrfCdslu&SwrpO!e -=-pk+-}f*w7nhe5>ZzL&jv<_qyL}FZEA;3#SiuNv#@!+#V!QHO;-|oX?b}`ALi-}cL~rafA}8}rztsr -&AXALIiN};zHE)U`HXVd8ybp$q5Jo9&_K$wgHF#PQP|9nyI(LYu|{5GK3^lkS=OFHl}-M0S`{ -jVhYgnq(}25_!xId;xB!he0kif`6meezj>aYNWXSz|IImmPX|z#Bx#&vNR(o53dIl@C2*9Y -XbMJXm?23}h9kcu2T{E0L4fK3wTj0GUnSuO+5`QG18@b35}@o`^TNz8$pIh)XuN~tw@Su#c(@~(9Rmi -7cOigyVi#dM3`kORM-V#-2vB&(gsTRP$lq&QKq~+#0b~UzXCP65LQaFVGn($ua4kgPRoDU<8-=@oSpO -HuSU?@9c&#OXs3vwK@un2fS`MfK749e$e4M``2co=!-Kg-Q&WCB{A07))^s(FD#C$p?jQg`pr6O^2;2?Q)zhydV&#_$9furT%-vRI+E_RJah#j6#yuYnrMD>px+1< -Q%U9*SXeEoOrZ=mLMQCOfeBQwI{z3np8~DdZ_&0ChdjFvR&JBEj3;&%PxZXeL;|BgnyZ$NbQhV|)o#= -$4gMCehkcOR^jaoGor;O&7MUAbi-}DBN=%Vr>(a}YWDr#9LTrV39QBJ`s6uv@4wB+-qKs)GIOa4=iYE -uJj}lERcN;}}w)10z -C*St&H7RM){``t*S#fwWJwWcbj97a>m)8~xh7+j=L7`6OfpDKCOb~^}IFj$m|S0g(T`F+lin9RHF -0QMBbR*Z+5I`lYx>kLLN0FX8;9Nz#9O`Hu=b6kYqJDuVDI|LYG5@cnqFwMi2H#7h1zoZcrH`M&njBz4 -i(U*5xClc;Z!m23=2^OKLx;3P~kC=UOMTO`S9h+}W(8$(z70ibw7?h-&ah+RspPI`#%F#Ij5 -E+_T4GBP40DB0cb=vXU<)E+~4X@q<9IehfU=?lvq(5c{P=GwrKic@i$4klZE!sMR+1115g*?qh}AQ7hbVQm9!{pbu}aRO*ReL~^!_H#R}itfe}^6R -SfT;my{c&58H9LUTZxCWu{*{ljWel)*PhFLB3Nn5N@Go=r+-efW8T_}i%Y90V*kYON?Mlt*9&X1k+!{ --+yRu%A%8#w9f9m1ef{o~Y5u%=rG{l2;)15SZ5bHwAB2S7Ti?fO)(4*=dBnB}5?dj)xp(jQH+QhD|3$ -ZB+y)u$qeVA{9rr*A1UKRQ>wATEFfeV8xoq`rP`_*azRC@7;QWY6_M;*>vWW$`N*(d=5!XU8`%H5PJu -8ArM8U!}rEwp8mc=m-zSFm^}Nqq=M#R6Z5sy2>qjn?FXaGmi|C^!vcm{PRhEBDAWC_U*PhaC!o -@p8PCb6bGyOiQS64pv}$GVsGr8fvck&^L)J&?11E065A1W8!^fa>T8o5SGlr#7g8&3xI_i%XG^&~q4+ -hD(89X;+lYS*US0b0R=!iyBs)BQN^b4!bEtpsE&RpcNNt;tYc9_Hbh;e%3vnV1z3mKT$*kG-8m6rcRp -_rChq+aFDkfF*WbL&5t^vVJKZMw -CX9&q1G|MK_Ye!;kqKQI?Fv^Wz>bgZ^S^KPK=#gIoOcdp(_r(UX*HD=Lh!Y^e+1=BT{&?J)sYw5(z@} -9GkT}kx3A16%+a3F{8MAdsMMW_VA747bdB5ZGNDeI=WwGbBj-Pih+l9dO#K(uvvcgHVwey3TEht=$YI -yDn(JLa$`4>+z3oGU^y+6NYw7znyT}sSS77zl3ofeF5-&(Cgj*E$WL~UaxjU|Ay;ofhNq`D<6&gL -1lXey?e;T2Yu(FmP!7Y3UMp?*u36q@<-bTkjhD4ZM`Jj^62zm~)qQsec??$3cBvdAC+5q10gIKC2&4- -c24-BBAT^z;+2bel41+?wCZ^d_Q{E0f$cNtHY-r+5Wh4o`t6(kMIdT8k!O2xp39hnHb1Hc%>&LEKmVO -||K4u?*i9|8>H_1yxtn4*-z-945_&Iq>%F&)?Mn`2M5%Cr)tRU0T^#OEdL1| -l);Ix${|2QIaJjen5CD`XSK%MN;|~b>whsUS2;SyEuwFQQ$I(MR6`v_{WDDb6cXo9Mb -!slsX*u{?1%aVZ)e4O&IALw2y~~F=~z|x?!o%Ni;q}?%HghHu9DhX!veS_c%v#kTu3k#ed~BdvKwOgd=en^8gufSfQrOu^9&w~UALlyowN36OD7f=>ht65Q)W^abNVw1*S8^#ENbxaNzqXu -p@0FNQqmoX@-YbwGyWgWtX6VP%Skz}xUa_hi``Xfu4@*@a(~DkCf?vkPX13K;oVUEp*kg -3zFC%w4a%~^r@f|F*PYfB#woj#pAs!TwIkDsG+Cy$FY`T{xZBJ&*AvYe8ZeA^4kf(FWZtTRkp&}>4St -9$vVo4&&1tQ%}ZWn|L;1T`|#Sw#9H*A@Gw(U)1OA$xhoUO -%6corfv-&c$apZ|%Llvx5X?mRdv^@_w8z#g23F@KK|;@j%ou^FNDmFA%sHxZ_RKJ-$(-V7#7SM3HbZc -{)@WifD8%G9Q=u}QA1d#u8F_SqMbCL$i#)aY29pMXqmllm9z0am#cjJBwar=eFwxp}n3GLVd0VdSD8W -=iAZ`{b@|_vClON?ON9189ZQaerH{?>FS-!jES8ch8NN}5yyx2bY28P7v9ecolhAdOU_-N8aMUwKA@; -nfLQyZr+MaIx+1CK&(M83h6L749|Ny$_?TS~q_xAs)vQ=rwQ!oP6b!;+gWCd5Vydc9fjvD!Dfx}bMo- -y&OuOuUxa8~n(*VHHzG((ly?^{)tzq2e1l=IXD3 -2zeF?`UHxW&7(fI9B*d|GJd$3)2aE@BiDcH1fD#2LMucnC8WaZ&=-=s1ARp5;5eukZ48S3}4wR<56kb -In09hp6(eZkv_`@ZCN7u<@_0WMrO@dV`BLKp}9SzzrtJDQ>3*;;c20{N)cWQ6lNq!<4_e(EJ;D(>j%m -iR&LEno|-7#*TtGjL~)1hQ<-=v}?-TZH^tBuF9J%<1uAVo4Pi|5Y1YhVB;nop?vigcVW8=uG~C3~ZaY -VdBZxm(u{q=UYLx>ZB1GH5vir~^p%bJ-X!KO&t4TN5M+?DZSe!QW3);?=7+9@x(aI`YfeKwvtSEwPyi -Bx4LDVzB%KgWug3!_8p;m%ZnV@6h{-g3#ZB!BdLK<)x-GRy>*a%&dkE+EY_2&?vGg&gjySp=h4nCts< -le@+MMMc+2CAou6Xvavf$YMg4WE$6|HSGNgIPnUsu^|jTOtlO7BF9pbGx9Mmu+lTrj;JGby`+S$(k-_ -i&lfAHH>jlq5QZ`9WN-s-WT&TX00~em_$iGHWsOcWt>_q~`Io#u4=lT&`J@0nx)K;X+*YdpNTA41-Z5 -}uq3V}~96u{;1A*YDGh0rm9=TP&V^U@M$JE0 -L0QHaN#{0>HzCN0qUW&EK>z^bo{@oL3ONRb^3v@w_Q-X$5q@}Tp&x|ki4Sqbs>a -SM@oeL8&tt6nMTB|=k;S*fne@G~_*F>>UQG3^?o4~gQ#7N!+~!M?(oEr*6V5u%kO{U*dP(;6LGhcyWT -RtVIyBu8#-tscj+r)QPmEh*Bp`AedCSndvVk33-KWOgy&sNP%ZNuf_q9=!6Y{Qm$Tcs`Bi5*vbAif>g(5uL+Ic&j -xdM*#O|V4S`ToFrc_XW}ayjjn{bY!X%1@tOh7pev5A3?%)XSPrqTW^qWpbGNZI^P0xjoV -OUr+30O3<6qeY_yG_BoLMDv%KU~}Q129Oz!?B}z|%DWE+SVJ5yWds9AGDKveqWlnz9GvHx;c+BYD#dA -hbc>)s-jLG(jNO(Qwu3fO1ga6D2!5`b%nYTT{WY`kPoboKClYV`;a-kJt>^wmL~AYkgv~V109EzeRXg --}hV4cK@Tz4gKMBt6AWJzYb9XiZOz}IathY->hyROMOrWq;2{&S$ADE9q7ZlU1@~(opn2ASJ3!Wik-^ -0pCEtxqyAgAUj?nc_U~-%ue=3SO>x#1aFJ4l)#}Y#zLi0m`7Ru@ko^e(Y*RJ7QDQ$40Qtk-{u<<0fxL -&b@hf>*I4s7Z-v7~99(05=6}eiz+1p@t-s(v6T#;Nkp!c0Ma&3c3>&JL{@%H{_RbbKYtp9J{%$geZwl -Y^JIsDP!Tk<`?AUJURr-w@5U#16i>ob%NC*~i72>NSA6xiKO<2S9}9cxCx2tz&Fkwmo`AzIP{(t51jJ -|A33ium!eV4~>PryHEejTo41Xp9F(Y1R8y_1cAHdMv}*jiML{6&{@9F~uSLRI!nnN;4dttN97w{%Cu- -8yd_Vf%##Z-G)PBQi-YcdM2@3-g}Ac8Q@R}drLYn(6Oomg|OU{=p=iT5E2Jg-9x##9#ctyL4V$1>cPM^D%sTv>sdkfcj;x>mRTWqH_5TtuG~3GL4GZHqScAXHWqJ;~(0(J>c}Gp*)&_!6LM`;8DmEqFznzn4?ke5 -Av(;z3{wC}ZjO!}8TSKtJbT>D=2}LgE&Qp$BX>? -z;i==4kxj-jDz~c^SxGx{&4Kj$00N@I++y6uoh0pQ^owL4sM$7yfk5pB?E~~cJ?X>{%0w{?;k9ijRM7 -9*wIE;AbG~07#?d4Gh8e^0)!yeu-(t)by<@L7fyYB6i)d+H9jp-SevY(Ot?_)1`tU8jy?S-C8MaW;lh -XAS>)D0ByW-0_RkT!_(^XFdt&=Yo);g1S8;9Iq_fje|;AIf_MB&9lj;n|Ecj0L;%Ne>X#%SrPd6&AY7x{K#c=Ek0&dbQ`9@T -KHb5@4tpo01NcmdUpwZ>3d8`N1AOfPusCE^As+xJCw4Fp_{jP*0RDhlli(S0wYX`(F3{hQ05A$otnM} -%?@(f;AW^*f?lHFd@dWv%0`S{F2Q-CVv+KY|2;K%dpdl2qy8jfqc2Dp%q8$>eX^y>91F_&Q9dmnKtIP -S51PE+tIKRx36O}(<9P|^&?R|&&6yP5q_tPe-@9IByW4(p;*Ti{c)J$bk1^v6k`Jc*4j{n~2fW9W1IV -z`rIvtvU+QX-w`bq{umAieCf$R76&}Xvzd(YS32+yMJ=y{94Knxd^a^-IvhNeTIyhUM*-{LF%h;wyw% -!MwyJk6`P=an*Wc&0vIYld@+)7>l|To}Kg^e*3s*2tcXFl6WJ;mB^1LmYP5_S`+6`+coeyewGOJpj_| -sq`k@OgYf9($jWNwlF}&6=jnu>FAzF14d!X@Mdi|-PYneYrHfnY71kaEBi=edbI$}RGTVBlY;E-k0y} -x5aRYvIYMl#EnB$4acg5oCO&rCiY>Kas)@EgKf$uio)ViaYl9!D&6%f+z3v3e4cVuc1} -*DR;lJWHj+azgqI+*2J_U&topDrVy-vllY|Re2owj5Ym-~yEUZGBRY_F+n>~UkXdyB_# -;WkGvD`&Ev^@VSrmBgy)S+vDM>G_ImJp0O4H?krw@gZ#kJc~i(w}Z -yWsmmP#=-ArJ?>sZT^(I+!p|MLweCRSbZJV8t5>U!7=#ShZ^c*8ks0%m7}qyP0s^>r7&+Ck-%$U{;Bm2CEdtqt%KA+BSWsWv!kx -vc_F;bVW1lZNsN;(>;9aV<1xTckVX8J>)OlYbjXK1>yh7y_SA*uir~L^cQP-qH^Qsnr?qzZ#%>@UW1t -t`gL9Z5Oc69sG}wT61%1|f0)q3Pi3QF{vmXsRq6g&=*Crud>U_GQ<+OP`ZVUG@9H5%viraV8ttxq_~N -T{Dt_lBD<8@!Ta^>IS4Yo=?0_$)o=wXGoLgGT4Mi7r!8Lir)$E*LR)*)T7u~GAljZM6swgOJeyil_s+4}f~T=Xd#~6MIQK`A8sa%{&$&DyC0uQ{~$vSq|5 -&fl{3wgfPm7o?M|(57VQM50S1?k6l$J6DSAz5VqpbO=#^|g+m4)$2(J8UAJBy9;HK}i@ -QeXCX4rapIKdkc7bzVq3k?~V2!g4o) -NI=x<_a4M8TCBd!?3otEsCDs61M}IZsbq}lu0sBuI|6<>F{vD)4KVW($vZ`Cw6XmA;!C+^1FTMn~HFA -v4Q}y8}!N_mtb6eIILvk8lF(N1k?s{Ej^rp8`EwM!$=ZH!!ZasZX0jGf%lamWXZ>qo@m}TSV7Z@R0dY -ZcFHIS(bDPx(}5+yiUj=q9mi02LFoei76iWDl9(u{bw-{7-k{Oq$#C+bZ)RX_ZvqJx4vz?8Buw(Wvq7$Pm|gf+8cp;L5+wW(vW1%-RVF=Lwii;gp7aKBl -OaT>MU1e9LQ8T0xn0FSvn>jFI}mu&;&ldQ?; -=0-dyZ*#4bq9vAXG5&YS68dk^rC;^>r-bPX`j?am!`FPXkbZAqCs)n@~mTuJ$0^;nZriuW1DTgfgLTKOPI$2tR?YzB^!UB)a -+nfNaso>d*u31cqKS&f|COI{`gt9i({l7z*-rL~6QY -QDGQl`~d{}AOscrPn;vc41Lw;#*OKW6j&4zRxx<-eQ_^!;?cpADcyzdL7z|2StwSWi@)6Zag=N2rbj@2@n+m3vpaAmesc^=|dytq30z#2EtUuqO3eL05EQS%#H4PFkD6e -q6rm!rZn&(0srxXBy4%bLa`+sYY^H`KYn8#%pn0v|kYONpBRbs!}R^=bp!M!tx(^YqF%XvG!Ld&>)t`?klnb?I(%+dNX1Av9b|OLeDQvquD -gDobxIcotZ9OSc(#@UE{{H2c+a9Go-9;2}wI%#?EbL!_FfKuL6m3&`!ppK3Unap=?`e$&;tMlqHaPm6 -lns2m4AdRqaU&7)be_1P(y*Xe=spRNb0j^0<-1N!H-hk??Fn{Qq^y4munD<3AxTwr6#^yBj3dO+V<^# -82!pW_Ce_owjA*USO1BFP~)6_*7~*gmYbkA)=1mbbOzHlv(F#~y?@n90_;rke$d-P`iGGF`+)@GT|o> -0{g08#~EfmA#+05h96uaUff%#9J-$%dIx*CN?dh}B~c4C5xLlsO`^>zJ5v%ykMNRpIcHsiaf;}&I)SOVY%q3JT1; -%|SC0+aC<51T=qHJ8_M4l0=n4Wz0qzwng6DPsa;_|HN8bZb!ea=Tic=@fiO*%t~+M}9!v2t8snK{~2i -yW^>R&28TiN`ms!c>$C+KE!``r%+hXTH!Zhl(XRHp9IX9Ug`j+b=Y&Ip<}e-G*qBYO88!e01%78eEJc -hI7=wvq8J@oIOMar>MbjPkdY$JCC_jXFan!>NCO~je`5-#ESQ)?hvo$pqNE7+zIV!`4(;6&u9BKx{$8 -2hvWHr3U0ZHZWhh9-Dw)i=cAa^XD8M!Mh?zx3HD?-@A+xdWQqeLL`e -%65dWwj@ndB-zuqVKH@kiksIPTv?{7Vte{r{VYP;?J)>cr%RF*<&G2{|Pt$Dn%_IdBY#$z1zSYRK)H1w#9qd#soXaJFFm*d*cumHbs!06f7R7a!bU5T?nnNp(K-@g0$3$jz3ggV+-PI}|xUJpvm)1Ix8|>9 -Cj)yQysXe4R$nclix{2T$P;;&mW}lH|L|;$~tQzdxoCx~N#uQi`s{?(-(q?aHGEpD~?N-NlPu#e2?>* -;DMn^T2EadSJr7Y%SwdQ2HazHPrMqNkz8VNS_IMKIn83d!ibHh)edg#H4M`a-$T2EX2ML#M0Sfva*K2 -Q2@b`boSRtbIDJ<0g>RHD5;MkR|sgdo!tZ~p02qVnT?VM_11{tOS$`f~iZyVCySA)1LYs>e}#vG7AU% -Bq~h@t!45GqaYj2{RqEKB`|cjW{sak58oi$4mD0$fG~46VPY;hnGT{9r%#EY;K#*r>5JLU2a=(N*{1;+?O>~VJ(|68S+4M@Gj7`ne9uW;VY`y(6MB2!xp2(rreuI0~Zb}r6`YaRbZIa|u%)fP~N3iwSTVB~lVEDD=nPMJMwib -v3SdW@T7*mMs93Fl@wmB%F`c~3?H+;xWn>n_s^gWCF1Fpkn@o$+EtNvF+PxD)PEdj?+kcaAaA`-dIGM -f+D7ir?CO1=~LkoBef{pK$zt<7GZE|EEj*xILdHV3Z^o=2w^xuyj4Ik6d@@QRte$0K==%0l&jTDZYkl -kYr6x1iS$K&Bnjvy|Lnbk{|)U_t{}9O8}h!$fx9*(FoufrdHPjM%J-uW=%Vce(RdxYZApeZn&Ol3+BK -?Ypg0k*4Wp&XFuIV@cIKuuLJ{Ja*dL`(=Et!&3attOs)G1!By7-0^#+%L-L&q0o34Mxh60G4DM9?u|2 -=>^eI7DR(O0?X)2q-e*BX{fhel=W -o+lP#1O4-n;5Q%A$7}v=*v+`sXunwV%$wHDZTxJjZxOGp7a)@?0}5)vGl^%4aI#G=0mE{HF<$SA0Bu? -gW`_1Nv^{e{F!Dk}aTrmTfVhGE&)R(%vp4T}P*L-lU?(-Kx+u1{EQft@nhg0>9ZJdplpEAde@{RQuZj -KDzK>BNXjr-R%;xN88xnSusAJI7E0g2nkc%lu6})3IK7RHELvGpfrd*y}W*KySPMXG$yT}>`oHBw@V7 -C&ce%v1;o~FRGQe)bj&HwaqI)+@L&Ocb!qzCK9L*NX4v&Pp^HZq)iJ+5Wa&~_M0W{%Jvn(xbVV4U9YH -hQx|USbXOOg!CO^VszuaSSCi?Rp7MxRHUk$Vti|{dJ6N;a;dfq(aXA$;9G^K-++}Ue%ezZ>unnH=J3} -o8h9?v2JH{#}UPp(SBCDcapICfhiI-d{M;gXB`CeZuwtL=a*yajkO#NP1nqI=45^wzweJ-GWKc+v)yh -h=`vs9hm>VY$4J9`W87qh*C}fVddjN8s^>;`j5d4Sww}>v!#cVEJ#-5CP3I9v>kd_6mL$M+k5}?fu=a -)XP9X#_~Zd{Lt@|;5pF1D=gd|8?ypXb(H938kSn3A3WDgr -91_v;PH2EX8t2(?;8{_{H$?GHKRVa5k7;Xej=UsaNj5b<+=O$h$^V@TYv&LjbErIL9H^zG}*L`SN>5u -PtsNeoEIt*P` -*&6*=yPfMcULCpb7}g!D^r%~WLNR4kox68(!b3SDmvZjQVOo0$GaR~Z%}$yyppLNsifR6CmzmjBfct} -!K-34UEa&({X^bpPno688#vYAaK}w&b{xeQ+wUoMXVpuBRSmK=pxy -XZos$Up=1Vvf%T0brMgU03o)Q*pSycgO}6^czq5mK+qtkXi&EW!xHr01oR84;uA`g3bC}f6)ao6)`kAWzb -H?}EKEI=HKZUzc`j^~|cpra6h3gKKlwOZPSmR0P>WKu=%C$fV9KH9z!aFqlC3gc12@6(hB}&(^5&S)y9y` -XsjzRmh@#%VNda3~BA}%Kv1Z0b`0Cft{PZbbp&+o26Sr{8{gj`R1aIUm1;0G^;9ehtA7E94*E#1F -r1p!%+j^vACx$kg(KYH9DYnsq>{6zucNL(uKB>`lK^(fYQYL4S*C{g@E@ZUgC_K%}L#8Hf3IT!G0h%doCm0!vtu@{2*fmP|H1%+uZ&{1w%%3$ -08l -*+?hmpup%Y)wUb0HAMOv#aPHeO%w6KLn2?6BjA}^_x5_+e{#9*rBdlf7ceHfxD9DZ`vKoOBaSdhupV- -AsU;FNpU{Ri7P`N0+(V% -mi5L!&ek|H`(ksq7{{leT&OenqPE4pbmi92^Bxy7V*2$!qX5XP3OpW6bmqLH-5Ng*MD8iY0$n=!u{~abeJB|1RkJYIK*I##P*t9l#y9sGjqKw&k -LSlk@EAfQZaANXE#V0+kDfDVyPR`@dbE<^zAEZ+eKgL7e$383e_i&@eDlnN$nWX`dOG()W3rc_X2~&! -!{^;4tUdIYBZJC4J)tJ)dYflXJA$vxWdvz|w&aJRrnP})^Tn3{om*ZfMPT~UiI{U2Zc@8fpxLIVMczl -nqG2$s&zfEp+(B``hffZ0N}cC@xjDD7^Nn`OBVZNXr*j1Tyd#(YV2yw5-2EW}rU$uvq_5(*9CXv7Ist -`PTKA1kwt|KG##Q`#cCjqvPfvTy0YEt-$I -kcppF6EFK!Yoj^|JIpy|4zZj5dDL!_|5yuY7O^_dwv>1fuW<2&Y7W -6O-pS;>DvmhTAoV>t2OmJI{5MTS)-?)k+Ky}FCys&+`!wum!=E188Hzi}eN)vKE40ZWZxGDDQ0jqL)8 -6!Nbc~?TSadY>sC)kG`xoF7_Ip=xzxZE;KeiWGN`^jBRhXbl8_*3*- -Ra)~wpVZ>k2GWl-uJf}E+(6Y$;K -S9fupJ>^eifKR6GLuC$!T8@!%hbO?%LEm&Va2fzOI>Rh(@dxh6(y;@R7XF&Jfuekqa)(qT=Q~kGEqOm -{B@C=K|zf`%Q7KVrZhkI@L9)_30m3o$PXg+zaNX#rDVL;S*@m9q+?+m~?{d3kvd>CpGYq{ESKD-|%IUC_Y%Eey;(G*G7_al -hnpA<67Fd0F$irIb2Pg~3EjE^|@yB30}e!z|xD%RVl+5pZbURpm=3v{M$r7eDBfn|Z -^*$E`oT6tz$0Ky1CwwJrX_gQs;ai!p9*y-&+ov-9%Br2C0kJVyif7Nd01!2K>l$9@7ai_YgCE2A0 -jNY3v{Yg+T+u=3G|Jj2=yL@Ry%Mzs1I&i<+Pwv%ACQ0%%A1W{0p+If(xJ<(-*SylM(FN&qvXjH+W|88 --6t4m}3%T&$3Zgl)KE&1ab;M1GWq}Oe)JH38a4a<1i!r|=9ulRhCCkpoMeo;Yyct#yOXyN!^IXLF7Tf -j+>Fx%)5l--u3sE?i`6^~}ZnFaIbGPnz`QfwO-y&8p?)IbciC`25Fd|IEbr<2SO8ZkUvLi0MANH=tI; -w(>{Z*W*DUhw18LZP2;Tfk(VGwmD$hqDaN#~J1ERz*s=Q($7f=ucXvCa5c~S_5^qbDeC?mZjJBb0LcZ -{Yu*9#@)9_>~W1Gi6k`ZE`#$-~1bV2@i!i&sWh4d`o$(Q7%}R{(x($Z`~l)fDuF -4N2Ah<*52vE9mDeq^fLCgEK)XgX5)ko8$577ar=%e+wwNW;4$oUtj}V)#$N#EG5j15mbAv{6SYYNlb* -PUSH<=*qaoRZ3l#UwQ#@vm*gP&eWrk(gFl>0g462V2KuL<_;*`N9Vjfl@k3>IuRuo>Z5&ytT(##>^VZ -`}B`56o1oM(HjP#bm8im9Q>_yDY;`^=8c?A@D7cuEUPQw(;-Acc7b(-91=M^TB719681G%z#gni)yNw -viMU7c>O#fU`j0RX+RPtJES2>Lx)JC!GNm;`~TMj1dTzHU=k`hB37UzAZ=8dycnWIS)qhN;X`OtE@WB -_Z4cJ3PR6YdS_`blnAWw(fe2~?yQe?%TAi>A(_h(&Q6FyUWwH*)jD+*05$DgC1hVX$WLgE-7$kZer*3 -`nNk1$OrF)%4_BG*Qtxlc^}RF9wsrk%zPEq(Sd^uqA4A*!+3DY-+`n1)ODFm3LHM_{`xfHj!5$xOVPu -Hy6s`2FyP6RD@Z!$zigy`1^lM{O1b>^kgnQ7NQadM}O7`O6?&$WWb|K_{z;qY1!QO1Loh2Xb1y_^ZY#}ET_Z(T>o9tlJ5>gE>lBGf((Nxpm4#M?qCX8Dc=z5BZ+vy;R|Aa<}H -5NL-lgS%Sa2jkUQx71nXuDjSd_QApb`555bzrMA9duPD!ZtZt>_N(tH@O$G^b{&q`5rp9v@$yL}Iw2zrwDiWa5<5PKZ9UgX}&niJ(lY?IP-RA1R903FXN ->Qa7tiL;8W!;on3R2hqFpR@_-0F! -G?+viG{xqnBZ?&Qkg_8OU*|(fmtmE`fXlWP>}C)?i{smp!*!6*mFb -0QcYtTZax+hG*lb_5JFtyJ}tkYj5ti_PAAd;1!_1}S=h*C%8hSN(jSsSV@2X~o0m;?l+Gj8IcOpXvW* -Al_`7h_hU5q5BRnX+x`> -Zj(^?NLWsR^g2%g_8A$ByDhk{sRW^`8lD&boVbU(V2E7eD(0w{=p9>2%-1?om+P3$T`%)0u%QDehzI8 -hpd`rKO@%}OhwgU^X-_>4d*$q*lowWqMwaO7>=PQHQ-j&RtRHNRP)&te&rFOE+RHaFTrnQ;ny$6HvIUxa -ATC^i+lg#-{q6mVyZ3qs_n)5A#K=Fg6sD?`fin_7T+e@|FzMmOuOdYiI<7isv+hNxrZG2f|}d|eYS1{ ->Kt0Y$Lk~gdTDooTnx%)0_AEkDi3jT&tH$PNK6m%aL8Oig$?)kl+isPB#+tkSKqI(VFZl~-@X-h?%wB5fNZ=aED~;RE-S*^+flys@y&QJ0-Z&6A~vahXl43-@!ArN+Z-mDq!sg=NSxB -%DLx#M$%6wW9 -qZLIgTnoj6z8AIs2hz~SDFPy^T5u0?)g^(MDnK$b9kRs_1z3Yz!k{Tn8bv_>MkWXxu*7GP -wthzruk3xOoq{uWi-?RZ0ry*f6 -RC;5%XhvW+mhkiivKri>t6um|L!Y&hoGNdh5oB>I7mPsNr5B@A~*(NAca#fMS>8F!U#fae`W}!u&)g> -pndB{KgNrL9SLG`-{`mef{J(Oir*Q_?w1+-Cx*V(0DU7GiT~$gXlAFlzLiBcSo$!&M8G|xN|8Me!rtW -EkS=4R<>e_wAd#>xrh{&@bNoJNFAAAEsIGj$~xCBNqI2vx>ksQRB@A0ps1Vi+uZdY=9 -a3v%I*v@5--Fl)BzzV<4D&5BV<*^3y5y4%y>NbXp_iIx2H#wOnGdOh$O&mbTMD5H3 -*6Fb$319Eo`6(GtWN2jWy68Q=U`hHglo1a`5hpSx4~rU_d>=6$uE$;g#fJrfsw2+N$2g~3M>ncLq?B5 -$Q&d%7X-hvwZ`~keF!GT*x2*?zie;;kJi-of1-RGtO~m5Vf=>adTUZ%Pjc7edTs?xp0)nY7OP;alBz) -pm;gFeqeH(Zr!0-}G8VLn@UQ{pg)ku>`>e>a#he=V_$-h|1`GB;CyjYuviWD1vmG!H6P&CQ2Qa{kI$o -DTs0LZ4IY3#C4br$nAVih@hg9r1{J*9gd+c+0OJ*qd`ZzCbD`PlS@I~tyvA5~FKHM;;V(_?v=h;@vO> -U&a_`Mad}JoED>laL{`A8S}FG&o*M`*{}`c-8FI3>( -{#n_q&+qlyz_>9RXwSiE%v^x4ZgJq~-kLal6sK2@|af5SU3S$XNghlAWX62X4 -Wt;m(?Fud=3D~ptRbpV>BY0=Ea7hlxc(1l~+qZxq7+EiW$11aCtt)gUC!_(9#zvTss&Kkn6=5^N3CTgxA3M>De|g^F$QW>J7)M8)Eap=x2^ViMXw>nde2x<|If;kKZN!VE{Nmh58=^1yFazMKaYRU^Zvz1%>S_~eoBV?<&=LeTT;+IUjpL$SZWaNAep} -54kC8WWEj5XGl{p}=B~c|C4j@kF3B0b{Vb7mH~!f0e8W^?I}hK*&4Ray*7lzSxR(yeck4Y#K1IR*3gF -25<-$E9Mc)c0)H}Yw(Y>U)g#lo)S3$Rg7TO*}dyR9uE}HHT9Yj04$8SAxhL}0s -Ng-J&S)TTi(Br3(0}+bEq3YdG_8!vF~#8$&&=+L!B+3N7ufO2ap=PeE{^RzV=3&2d7zCZ(^((g0^obeI>&|nfJpNa&=PYkU{|p#oi|;?9FkuU$*K2!Sq6DywXyX;|~289F-H@sjYvS@MY1dB$|9gd;8+5<$E{evgiyjHPE2RS;>%A-ByK$9%&!R1e34fVc(9S$CXo>;xvk`?_ -zN~P59N{(?t-C4-+ULxIcbhX$q4yJhT@QRep>>kdx4!(Z9AGC&I~K+@N -DvsdmO~NxANg&zs@u&;+`&e0J?DQ$`7?x_el_RzzF?OKn%_kVS>k#|3zLo`y2RygFH}DTprdp*Fqjbh -=hOjIDVRgpv;`p(N@3<<4@K_6msg;t*)0y=W>gaO9aSPx~O@W-dw?zHohZfVtI=pX_;kSVaIw8?b{%U -p*H73$1wri229Se)S@U*wT5RjKD}TI+ycm3zM&B>1yc&|6mxd>z#xh9oy?#ZAAcF<0^IU-0^g4W_&$;vP5keEi087D~5u?;5Tq68^Gs(KEOmKPSIRbGa&<8YpoiPk-=l?+;tqIk!IA~I#-qz9x<&Cs4`Urp| -?7wJ2<YNFTD8`HmlEqj(%2$ex!3_jlYUD~!f;Y -$}hp@o136AtTdR@3_Vrsiiy1^GXT -UvO~8PBz-cv2;i{j~*obcxhhM=;GNBea7xf?gcU^|gepEK`0+NZc7oP&Swrvv2ExFHgWvLnpLz!2LBS -{L}TkwZxMKZb&L`UmAPxS?0E-w{2KcLkP#6@2Z~`DC(V_zgKb;d*^w5p2GqEHlF<*JOjVAf2N)If)+6 -tm4O`yLy%rpC+uvGb2%=!1Xx0sCK9f8UQ8(FJ?XBw9F`R^Pqj!^w?l -JQFSi#BOTrXmI0qz;XYo4Tz!OUN{EY*T?A%+1ee7U4J{PV<(?cfYPElsd!bEnD}I8|W0Tgj>#X1- -upr?}DDZq5g>2Nx4i}cD;@*d9jH2j5)5`VN_bid7_S0NI;RlyM=&hozcqn{p$eRls%fg^yo|G6nhj|4 -OXvCS26v)l7fyI&JZ8r!{$X)1iSMp5v(Lfm8WjyyaZ!3%qXM?7=3f(-%R=K{*X6SJ^W|shiE76K%a6?R}q7JXN;yf^ZCxBr6AD~>MNciM<0P-0PSY4{H(I*ce$g+f -GK$QYx -`^xO+KY?-5y5{|+wh&Qa!yW>rfTpu~&HS%ion+1jl&zH!d&wbN|dLX;T4%t31X5^5Fm9_C4GfzuFhm? -KI*nEsGkcD_c4+|Pfjky)}+s@fu%cwGi*QhMOlFxeLiO%RU1xxc!t0Z;E5T-sjR`uf;tx`P7PfND(Gf -_>AidvL)7hY=GjTTFf18_Qy&7vyf5W(~)r%}Am%_mh4;Z~mTsh*E6p{5sH4Nt!(#6ww ->*L$GN+`S<`+icCWhj*y9VxrjOdm%s5kVfIQ%woP71<)JQw68<#j12tQ@2D -t5k7oAXhIjl3DfeR_Ia{0^(_W6uu-kZ`PSy&i|oN3mVTQG1itx!sH=Wd6^AW#LNLesimSD9d -9XKIUpIjM@Duq{RFf+OMu>W5rg`9-)CEg#(c`u+oVE|0qw{v(EJP-=8)XYP={>~6rdi4_$I8rLkXn+N -I}@taE5OaU{eHraXK8b4>m|YxRf8`1w!JtP=Q*J2nkWVyGm#Hi&&nLbCSx0;=Bx{Oa?cO;dMEFRd~vfS1^RU|kBH+gxOMmE`w@$1XS`)hnlu%7G#7rW|-SnA~i)yHR0|fB>IXBD;z0]lq6VFL#+CYWJEp;zA!* -INc)3c@E9^90=H1sb-ettneU)rN4b!fQCC1>L4pGkM~rAar)Mp&ezBB$xsORI8=_p6qpvDOV`l~kM+_ -TIp07mc^reKLLC=)#+}VP%90pUL1(aSvyq4@IeCxEIn6aYtxZz1?Wxwa;dw#MohiBJ=Tcht7mC_pvjW -3#ooiFm-BgxZLsAh&xw1ZUE3YbP99(;G+-w9#ir`agn1{o|C$2F~>o@@!kBCcvv&wN>UNa%|vpJMu>S -@oYtEy{kYF&is%ORava0}O>2))uzw7E;x@xk9T9$Gl{NmoGUc|&@+{(I$2uE>f#Ec%1qjy;n24~hF6# -MA$L_6IINyd8_SV_y&s{?`}#9)|tRg?^$I5Dn(A*S3veFQG29 ->?#AQ43Cnic+?!Caod806a7LzY4qmk1+lMtd74MPOcIn+?6xs_VJJk)_Uznu!H7T^eBu?#g>}?qm@0F -b`^u+#|PyJSP=sr~)uAH7`n2~zMgkLa+BZVV9>t8E#R=J|Y6w*m-1ac+Lrwe#ZrquFF$ -nN>)7%$1??vw+9M!z;;yB+3bR!=)arZ7Uuneb^2lz0$bTqreiuPs`Ja#7nd&QNv -j6I~f4pnpe{$R3?|SC}{Dr#p8zR8S<*ttLI_;hXktE{m;PPe{5D_46yHY<{I%U$Ba>_IAQqXW;+La~e -m$UOyBvaW^o|MfN?u7K0QdtxXsVtWhcxCb~K+Q>TJgf;~Ep7Xx`okhFw^~e_g>%J&@`6*;^EJtIpd;o -m88q>!)>L_?eOgb@qqQliM~?t~+oJj+$Ck4gV5 -i7^iD<+3E*WRX{p9V;We9&y3J=R6g}__Bv6z@J_yU%He0qrDlxFvbbTm5Gwi=j_gpH -}BbWix87AB5~_qtT_L&7WiTDZ`s^!hLgzQJd(P$L`YXw=OYr;p{G6#7Kk0?nIEPFikrlK=;^AWH|1Pv -70g6@xy6Z^N@ZYma^wgKjJ`Dvo%3qy2S4Ub=2!X -i^wOQWy-PAVQ!N29XF%;m8J^D25^^{uO8v)LwrNsJDgoTY3ly_TKykdt3gGrh8W&ee>uxB!a)LW$ZXe -?xRK9srXxS=tFiWe4}TK?|gC;{ML+s)16L7z15YF-_h~*S0r~5z^+n_?ypXeJM9j}-;H~0=Z)`3`hGT -iD`9W9y507!-4yT3;x`Y0NcT0!+b49pW<=}|j6?qjnvXYVezp*&3u+>SB#5=z>}km7Eh<9zmMTH@t(K*NkUrmHo+jOW?$hv+mQUUO(iFMY}<}}ohQYY%;xDeCBV!`vRCt^ -k&TNToeD3HYtaksPMXiTkX7Dc4BFGfRuc_=mNmkPocG -c|A0Ic&^lggzKUK3Xl5(V0~S)9N18<=c9we;uWuh#y)fUiCy4euoWB`Jpy;v1eahl`ayG7C!FQfim0c -jSi$umS$~j9u|KS&RtX&rbo4lE!Lkb_2%Hy+;b0ueU|2eO)bGuIOhnnmWBZ!WSE(;|u_C{n*%pOva9m -LKpB}U+@c_hNH0Ma&_o{4#wP5X~VJ(F}uUIN_sdELrVv6R3!NXT$<`u7J;{gd{WP)6RG)_$Arm4&-B& -72ZPxQKuv(&ZU^O2!Z(t{Z*+*9pLgB{s{!y#-h-~@m~iu;x6GqEJg1Xkb+FVhPb(kL?lcq=RX*LQvR9 -547W0nAYlna_IfmB~V7EUL!NL?vP^SrEu|B;#YMM76Q6as$40KUYxu{_W`HmgN=>GsH#Ujf;61$*@l0 -SI+{{UwGFR%F*`26klene6P!w?h%K>|l1a7#ofh$3K=KtKXP5rTxar^qjP?J@c`cSv>>7%bXHVR!gL- -sE&_*W`<^9Z@%Y+_F;g>uFx>-TF@6>^cP5p%0x7QGn -kd%Khj8p}u}OHiz6VDrRp7Q;W7f9ZdNjF|m4j>kq0BFZH^?DgvDA_oDm0=o_E?p|YQ{5XDcQ!T$I&8~ -xDb01Vsv3=Do94*T2LFf~*$_fi7~$|Nuo9MwKdF^@kk5m=d_|5ApFU2#U!Zi(G+qPnhRU$1kr+$q-Ul -!|ezhlR+N16;Hb)D=Tnd9cc+Sm|ul)OBLr(#==tV)wCs)-I`+`6a)2m2fzy?f|caiOylg%Df7^*qpJ` ->Fl(noi<{tg_IU$B{kh!^mZB%)p%+H8I^|LHxB`%Pp6h2iPpj)Ua2?80HhMDJTGCcHd`n5A|rXF*wpQ -bRbX^mUWZx*111m3K)hVH55#hn(+h+;(vHW(*LeiUSH@xVn=!K6wmF?J-75+@Kan!Lr5)!OC5Kk&x#G -dr7CXUyTzMALnztl}z~Dv?8Nd%O<_V+cn^mADIeZ%q)9zdJ^@WAQU)krtm-O>9f0*HtJDe$zj!jR_TZ -w*Bcb}cOA00b)!%pCD7yov+jE;mXa1kwufM#7JKl3De5ku*gqDr0g(<@uzlD^ucppeMJ=N(7a(mpxz4 -%Cd5s@|SxndIDA!dD|GnmVXmQmn$+qYL?bodx%#Ag@cQ)EaNQ7->$;3C>=ae-QjGAWwwCGDlKYWZ#^oAV -(#N=#J47meVPUN+;V<N)&^p8vG=)9dKlp+ -Ek5!q3&%?SCK)k|2sv8!)0Ej6x_*5;%yGTY5=g6b8O)pMNdKlKW&7`7wY+?luERwD-qTVy~ux$X@sil -DAC~yc;rp1&G8)Hyqtt;RL*6>6SrK@Q2s-+slaDEt4Q{PY5IUt$zXUxEY4O0mOZ@jN0#HH$_O_Rhm6SD*klt)eZSy|Psa@JNFQLS -j!tZ$Xok7KZU-;xRIhB}pJ^T}?a+=b3**%`S16(nh|D^ -QKJdEi`=6?1&7(3wphjOC;qma{4Dtqg^j!J6oPjc;!`glNb_P$xsE`Jf-jiEJXj0k8l9deG -T{t{(utqAFN@0A^_me;q6IflIp~l7lpYhf`UTAMh?EYp-W^>ZLlas)U>L<1*avXJMp=CxN*x{5eHxluj`N&l#UD#);-hC5t5|_76qY21!;LhC!!nfl9627{_XSmrNPV;qRdoXaQHZ -yg6Ph%OksT%M0s|n2yW0sXS|})!?yYpmU*74Q0PF3jhL -ywY6Z?*wVA<9=Xiu9b$;ZC;xIrdj~e1`?}<2TuA28%T2xev@UELlQ!@xq*<1w6ze)=#VI6w1ZjlEWVK ->A9%xew-RK%gUKaAVI&lpYXGmUeRZ<*W}RDYO7)&%48k$i|T{lT`sIwJp}(6|DYYfHelOwuMJT)NW;J#Dxf#N8GKt5Z@7ZL<=G?bYxAFA_bL7u -Lc-o9%-!%OeJh_LZ?-eE3oXI;E_$&~G6nCEsOUe??^=!E8^hqY-|~iy0kN~bw#PBG&n~BL4UH{*q<)x -D-l<*jzHEER^sN)Mt7?F62g0{J)rYG5uF*mLBet14w%tFao34g(m@_T7%S11$TrHfZoDRNB_I3u?pY1 -i}dwVSwPJ?vPwgd>vW6@r=bnF&2_PXaX;ex(SD{X)3J#(dD**}yNrBn9Zvv14E%2nm0Ek~1Hy8T^-V? -W04ets{_{$POpG%EZ~tz2WTQ(D{IJU!Rg(yH5c*{19YlTX!%=F>Nn1^5}`Z_uCAu&c><>OWZQ{fKYdm -UNhKKfbb(=8o#C@YCR$Jy&syHy^6ng@o2{X%y45k==>|*Q$swD^K+(-D>N>qdM?dF=>I_=TTF04t7mt -ESEdFRuq!VeDj2=h?QHs#))kQO|n@~TfIoz_NJ?O!?YhCkej1(i>^ps(w#j|9B%a -!YoGF2D2min^n}w|`F=;;b56vPMuotu=#_rKsYpRi?5nLx(O6#Y*!QdDs8VypoXkp~>v_?FCvobdGDj -)@tgEl_a9nPu86X&cfy{8wPdDsJapcj+qw7U*amop56pWu^TRo%DLrC7H@BIU=iPxndAeSzpixzM|kX -*0ztL(?3a?!($s0FBL@x|C#?v>Z?$OC)I2-5DifV|XOkb)ixf!A2ajauFa_=D*$j*sTF+k>oQ`= -|kZc2{E(3K%(mi1{GhmE@@56&drW&RnOsV|%l7P#CU2gKdwWg}t#3cVK4L(=w*Ric{239?>`=V|+l2m -YR=p%Quu(ZQ@{-LC&$qG>LMVTX!8gmxdZq5t@Q{t$M1kw2fHgy+F@E4BoLLcT^AaxXe==s~_kn~)EP3g0z6bkALO -jxo0P$&fczQSYK6Oza)9uMuYF!^C?@Frju~?iAZaIyTTllDE$+xf58hXvZ1kO_kYTYZt{1_DP?8oQHf -*7NI+e?T?{-T6g!B#dgjP^^aQEr{|6^_$R{1^m(L3{sOt>Vg4%c`$CCYlYVJ#SutF#5wxQWx<#nZvj8vssU5)A&IL=j}jj(+vmedf9Xkn>!4-t4jK!*TWA?iKMvM2ZHJP}V9OlX@bS;)!Kf*^6An@~bW=K}%TdQixJAd$6hi2~v83`SFy(`3%s*3SwN6UG( -qm--VilacvZo3uku>%l>JtK-6$gng7-!v`1^gIkn139BjJdcm*#ZOUFq~|d^V0nf(MYkN{t|(>2Uh+c -(;Gs$?&J;YYq#AuFAzxZ=F@Cs*Jr5)`O>W_0svNc)CF7 -4_iBvZ!Jdet8G878Uyyn3F*l)-o*NEudQ7PF#i^;t^1}^+tn*xf|&63*I9Z8>@-B_}`F5t0`ZPA$Mm -DD^J6aa>dAa*?wn9i6>`zWQq=9Qu-5=_{Jl;Uq~qbmu$g-tqKN!lG=%rB=NZKp%f1esE=Lsrnf%u`d7IAH~mRxvA5i%H>#V9qk4Lw6Twe#?$MAs`j`IXzWA%Pz`oeXzN -v3>}5L;Ec#sBv9)XWp3a3@)>>n{@4BY|1S2MUr!jnNz8151 -1M&YWI)hJp4M1=w<+74v!h9@HhN0HLmE55bkTDsonA}AWgKjdg)p<&L9encW7dJ2_YiN~>51ODgTPXq -6MB>{@edT$d8|;xdxK;|rubf%CDb{46wLNLQl(e97LFPO7@$Myab8TzuEwr>uBSO1P+GqXC%i@xH703 -KPt`KPZ-oSkmW;D@(4_1Q?k(sH4=f9Wb5HO?A(~5M91L`PVd}9N$5gII6E<;$G%Uf8K)Vl<;}qky@Wh -|zbd=&(dUzg|2@o!~{+7~ME+=*t%bdI-KVGUE-pa48ZjS*#5X -yX!4`Rqj58tKj+no<&m6}vjyNc-Ra$hc7%%AS!$!Sj-ctB65>w4rv_2lX0V|xpETyV>>{d!p#m1FzXJ -~9FCVY6e+Z1w`k%(Z^|J3Uz_ux)kH>v8NyC0JmPm+~S;01EE2YdvpaFyY2Fy_CZ_RaUw%wQcLSvdRst -&h?K%CAwE51?LLXFQqalFVtT>zUzQxoc6?%^wj-GdsCNsR3*C&r^MhGqfcA?OdgP;5RROr)Vf+qSvv---whkP7?YeX -_bQlpC#ZW~m%>b(JyWm!68J%gu|+4Z$iKb8Ln;hI@gQ}Xz41GaxU>#rf(&u9D?v|$7YA|wK02m}!rMP -c}t74O{$1piL}?Rh;B?@9LdAdU7^I|BC#Gl9LSRpd@ng1(N~w&Xkr_J-|;_HvT!gy1OL*-JYOIoYGN- -6uHR)B6un;6_fxV=uEzNum9 -UZw!wgXBrP-^3O^ZZYK+zs3I?vrT)27ytAxk*wErJ3}UM28XYGOnw-|0^UX@VD@w0^z>&;wvQqkKPW= -bFV*`oiURiXK`8oFRQwPreRgr_MYCvBT_sSf-d)^ZkIjJb%UsxhdpE#OLETq(^AB;`{vop8ho9K&cEHB+jFtxik#ERR7 -CZHRD?o$9ts*%AO-S1ncMiNOBw7x`J~~;%B`vs}?p@J7F*gt+#IF=_^_wSnFEheJa4^FkGu7N*kqF?l -r4VrY$|;K;r}`GTW8c6or>wL=%%y<9^^#4R?hLoDySb -EYcsOdn+Nd9PgM|g^VJBZGd3KfowPmsayU@zR^H>Zz+;7do^WE_f+rs%(o9!0L$^AmyxwYmVFOi{M>n -%OaK1)i6&=3L9Vg^HRI}LTrKnTcey&sabe1Z@+i^nnX&Oz5rZF5b9P!P`xBaYR={h>RM>UW}vZV#yh| -`}8^?F013BZYsi%fft9rAW=|B=LzYdocxU2fIV>g-^6Xe3FovqF_`h*g*rK&nh=cU?vCeG_LsQrtF^% -Q#eWmGaeo)L0ej%~zlqxbTD%BZLN{OmUQqcqqg}_O5IwjMRd_)y1a;|gq9H>S%;V+B6a>n9*o-3y4`D -L%4sc{KDh(d$tu~&RzVc+k6A|iEI*p$`Ml>3Dfm9ksXUW69R*6V=Vi$vIii9|Hxv*oP0jsD+p?qc -vq_aCz2y_3R)h54Va;kvg6c*V#Tr2rH#IL{D6e+{HsJjhFr$qN -&i5k+8Tw7;L7270Y*W+ftlC)=)zGE(KA#L1UWHabWbSd`d}RzTZgWIS?u52%Lz&Q(S`XW6#Q1C_H*ej -obyW{_rJKzr?}m}yu?rRKbW9M7{)OS+5#yGM-UiAAcO>Q6u~JH+MeA0{Z;U{MM&^Fc={Of0?EDJwg*+ -n9zw<7w}@~%lHmUdzc#N!-W&-C+S^A^_-0CwZ_yk0Z4mNiHtf>r@!po%UXXgf!WIQmza!FbPsiB%9DJ -_@Lhze)5$*x#u8armU7_U7(xBe19mrenHYN7~x`f*ABY6w3C*d9%Lezdk@!JVxdq@Am%_H608@2h_5) -zaJ3MQx<^BeBL%LGpkzWsXo%=?P}=cvr}d$~+AV0-%BjQLe(ZVSCy;Kv0&k`De}XYNn#JfPmApUCrX* ->=lY)b)mGRu!nH>@Vbb)$ryM{ZU>7_!$y?hx*>N`c;sO+Xcyfc>cQG!RWa%_FdZm_^pE9FNFEu@#TS? -Fh4yqJ2@V2sR%U>Pb(8LXKxFVQtefe1o8i8?!C6$RM)P-bFN|?b`SOUiyZbCeLx0DfXIR~au7iQ3B=V -mFXbwSRbICD-u-ubILMNSCB5?r^O@6Mg0pKU0#9NF-G<&~ZpHC|EfLPRXCT&(_f+ZYF)5DS0WHgrbd9 -I(S1dctO#O&10gq;|P#*^*&hqO+a(&KjnMRH?VAF|PtbjZ-p;FhpgHoMhaMy%tE)K29wf7q4j|PD~MT3zk~rq3wL3(k}^z(KvD*Hk*)6h -ZH$eHG%oine*MJsKTNq5}R1;jveJ!-ATG%H$F@rvUXz!&kifeKpuv6OCuu{;etI1)&(OrnhLcB6KpOo -ahRj$IBf3l(=@q`n{@LFVIEdTQ~1dc8T-XD><9b$?*f~HzI+~^~tYXjZ+ -i#qiafO;_fWro!VjeiUUd<+N%QKuTDQs9Q;xVW##R;a_q$SnS --6jvm%zyYrZWKqgL?Y5V|2_wPD5G)6K0xLxNB!-Eud;C8!_S8#yHGu=`|`~8|49DNzfc$^|N3sj|M^S -mZ+}H^U^QpyUypnCSKl?S9{}_J!_#;N+|N(pTX-cI0%ec|Mi~^wz#oDnQ82THQ8Yy%D1*Zc{3*OH2$e -)D-694aQ%MGzPw{G01sH?~=v*zp8PTA;f_y&Q%^)kmVXdlQ8K_{qiRRHbKslTSZ8e5jC3IFF6*2)O4l -4c^{j+`;Fn2}4NZazh1?7|FG}h_~#eiDLf~^<{$}Y@00vDrTkIdRLV<_;3cvY-jPJTIRj<0<(9CYgF7 -_bSMU()Nv)!bzKDjTnhl;^z!kIJSU!*30px!>ZS$;Mxuze{+1SJ_akJ~*XtXn*L%^%@_%^V4(a6J0oG -fmU>-+rKz5I)#@6U7oK+{EHek*zmcRwTmjKBT$_MrjcceS8 -B~3REF=Zl`7EnLtbbr0qn})dYKbL`Ufk`Ec3W-Wl7n_gPMMSWb`TA#`EIh;NSBc8b2oUtxl>w`_;-R? -gi8&Dz;+ZSJagGei(($@+eGNy;5-W(CTma`24ms=x=&wTyep5w!AXw+}K$4F1(2$9{<@b2(WOuq5x>G -equ{An{$jMUI`onvq76t_o*JV&os|Jti&rFMpSSynP0}S;+)so1 -JSP*KoYZ7WxQ?6;%q!d_jeeEJ-DB%w@Qu%HtqaAxK`-`;!FP~;wJ=n9D6Z&u#}>at`JJ%sAmbyS&0uy -QH$_(_-6qJ94bi<$O6sTY(m7_$vCz--=Knq&Aj;p)rE{D``S9ro-Dv8NJ>ltGUarGamq{d5-Or>izl@ -q^mcrVUVGYlDB>mil1fP(e&8a^#2ZNor16ui`;^haWzM#E0-(4l{Lq*jt`)+!uPao-*Bz`_L=CY%dX= -fMwb4QMXYA@0v_77&wy#*TA_QM+yAN%Y0h?l(mmwE61{3R0$y1#G!72iL0_?Gz3e{zoBRRJ&zl;seVjsND1yCi`D{kEmdfV_z5LAJ#P54YIu6)zseR -~7{z@X>3TRyV;Q;$Gh%WBt0dN5!r$qyrKSubk85jK1CKzL(ShpOxCwx{Wq6jSBF70F+(m-BwEw-w>V* -#r&nW8U`6hC}i@1s~(ERlOi#3L$a=>{%Bhs;WoNbV#_{`<^ZB9`3E49yz)7%X6a6ke!x7u!I~ovp#h&o3$BEs!_I0Harb_{Y=_vS?pSMyI;YOD_mPvD6ve8 -^#p_(JX%nw1^&j`GQbT#v<2jP&s>wOsX#Ohc?^gq&a-`hpdd%r0D)&t%g#lYe6-a`w2=0Z(MM$Za%V9 -qH-T_AhiYW$K5%&Y=&g?keUjh744tv4q}?xTAh5Vw)uu+d;p~l}cIp~57+SX3oR&|@%$E&a$XA}bDA= -VVl6;s;msYzU=JNzkz9<4)V7bTW*LmO{KPmZc;q~#7}v5lnpWuC8+A=$!-QToGie;3oJr+oeT`|K0oi -9#liy=ORoG^CA=eVoY=y@Lu?D3N*2`lfyq -VO6p7mOC_#dSO0E81j*v@!!j3qQ@S^Jk42wh+iFsovJqQ3rBZl`x7rF@`M0i|F^VY1yz%E6=#ww8t#GPUu -CEYlspeH&QOx<)Cyzwc)bI9!dK6*|(1a&2D2DId31`p0;>9!7*cINal2a)wYWo_)%~+M4z9!i%kkYN0 -QJ+7k-!cx|DO_Gh^`$GJ0C(;5^<-UaLHA_Ydww=kQ!xv(5rvXTVAj*+9i4R$YGnxX6kcYdSx;iT^~a2 -l|mzVb*(8diLTrCsG@`3EMo?Ob&@mk{)$--9gR)1G{-Ouq&Wl@GUUvub3ykxYv2`+Xv{)z|KXiPYvv# -uMT#9^K<&z+z^Dpfp4WRT|M^XA9j-IrkU6YVQmC -nE4T4z<|u3W-2rJ?@=hI^Ergve&r_-;%yFYGc*b8%Z+acF`|eRZ26eh_F(r8%l)Q~6TJ_8uAyw4_NiM -ewdFiKf3kk&e!5@U@*6)t3)$i)e=(^W)r`Hy>JLtHxb59pGXq8)dcIz+?ideJ60NY(Ross{NPD-&oQ!0P}F4&Jw<VIbb0B4Dx0^DpeBPMf|L0)%RX=V)HR -%gh+20tfepj~#J@+%L)hfQ`xb+z{=7HeO8ClXyV2HE-(=&p8Jf+_`qd%#dKXsc5Zt~;;{Pa?vPXWbkhw@c!%4LFN;{bG`j;{9+rHwf=|b;#x -BBFS66t1im?AJTh~9+)!8#5r|r1>TnQ+Q6YOKYm&&c3@9kL=%y+GVLF4Zuw2#=X=Y3g4zqfTwzI@`+w -@=(#vWV!)@AjYVlFcJ4b4LV2mFoj!RFG>00yUNN%P^i%Q#q2% -Tld~cDv{h6K~K_~h_5`D(<$S&+nb(nQ4#-q5l?60wCPR;q`BfFKyn5ryU;Ii4hp5jX=Tl1V -d@h0hb;bO=ZOE@IXOJWy1bp49uuFoZ2_Cmo@AGwo&?A -Vr6hps{7s53|5t>8D)j>C*_zoR2&iX+Trmb5Hfk+k=d0)*LjNmgz*h8X77r63?L^jAJ1A+BzpP;^AR` -(-#u!Ye7ni%gYjF0ygj6`fp5x~DQ-}5+g?nK28+n}N)pPm^eF+XvYT@`mxvgvx%xaQaG~f9{A~$J@i% -R#fWnQ5mMjBVV}$zQz -f#~-nfn2K6|)v&jeEOXp3`g#+cm(U2h2U_Kv*x5mn=c{UYfVYkx-+MwxRW-V>R@C)@-o{r`I=;K&ts84(_s$Kt-ACf0jhFBl3CwnI}SBt!}$X=eW_GtqN&Zbl1o -3q@D!$rT|4TJGq-jqT&a&(a%>Ijn&UJT)$+H<}%ahR?UK_7}YSacG?fV#b>-VXPcc^8|-Wn=je9lmdB -n+M#|Q3YcwkQ3E+68?fc9*M)mT -I!q2RqI!k2!_FaL#C1Ngbe#QULW0(BCnzu1hv*bEM}{iu;`!v= -6<&4V}J53~bL@vW2WE)%v?CH$~^8TYP&ILk>gg$}4EvpvB}opF%_0->G|jKh&Jj2tK^ym_sIa5nZS_H -19n7yyccyyG>L%7pZ4^@K6OlcOi4cZQYCWo``0T&fUcNQ0}%vHbvKr*9SY4qXtRH5FaKrt%>$t3{TmR?zQfE!mFw99Dc*z3$b^?qoCfUzyV+ -jD&OHi=n#mjz|G?t3i^Gtx~1#obufCV11y3$3fe(e%6P^(ToLxCJ0UJn+d;LL!vT?F2+oC?7EUsKfyp -e=DHKLFmN?{iG=66p~tQkmChLF)0(%fr=Y8S!^6)lTcd>5_6rFV2b430bLOomMzJp&icJ0~lV7hj)GV -SNhRgRi$MuQoMEDNq)fXB|rV}VTHbW$OgbubNa3=yrj%)#{AJ1=Bgjh82Zyw{_%{VKRx9y*0H~vQVZv -&LfV>)$*79ocxnlt9&FbPRJO@`8=l7*N+?Y{P}5W!em+#Y;@B|hTy)!_7~{5)Ou347Sn8E0t4qX!9{B -4{I9a)=@6>|qjz`GgM|oE?Wb({OS2*C~<>*9_TF+;BnhnQO=Gdz08(z!DN|KrjWnxZ_KKloIY#j8~K& -`A_Y}Vf#p7cCC+LP~hcsR9>Gb;NQcXE;&yM4+Rh7!wdofx-kC*EAN;oR9q?uSl-xZ)<>#S>zB$fI&S> -ge3tdWk)W^kC(&Y9YrV(5ih@XkD^3=Sz;@LPO&e%h}2fV{zyj5RdySMO#TJxJ)ar)6FYwmEOmGB;9@X -dbFu%=8p$B;QO1XsSm;Vno=hqr8d9MhIw&(+du=wNq;4aUnYfwkb-*YX!@4gGa(gLJz=Vvz{`7I~=VOV06(!X)U-c1 -Q3p&>H2!tj(K*%E2iwu|D*^>nCf)JO6F*$l!*P;kt{U6WvxLW(KGb>(l?a;@3vlAXo_ -&(+!J=zH%Qj&+?^$O`%FLWDC!NUjJy{F{Jw|7i`bs#@v)P(2cO;E_QvZehVhIk!xI|!nMyZ0c1OAFcEx+NcJJ|QN$@K!vBwlUukXo*f2fB`tEfAi@;GC9xA8bzj3Uv7jWYxu2cQOUep -4V1o6Nz&jR?b?~2=_I+=Zazh-P$;UBH7GyrSk)+=}$B2H+6x3Bcldgun)~a^=n2gnXvYoChx_l0bSj< -toog}wLIyF__xHZ@;8bG(66rXSCKcD_iO`SNnAWVx70H($m&yUUO<|g>TIdzKz_^9opJ~$aQgEW -u%(ftDuy4VoLynDl;O+#+Dw`8-_U9u9bDXh6wLQFR#<@r{humPX6x7lc-a$!j?HRr_ha; -&fGQN#@`pJ7ueDUrFjp(XzMt2*(zf;t~;U4YExBk)E3rV762~JwN3$d%|=6R!8YD#oY_8a5s&^KhI?? -0-e-UA~&wia1x)Z^^>!ZP;q8t^*bQl2ca=l$j?v`}u=^0Lx1^1hs(cDxVRS2|zXd`8BjX2=^!P_Asvy -39_a?I@YE3nyEYUgwq(JGb-+$wJ1uTPjrz7HR9)nnsx*hB>9Tbb1ha-(y`h#cSO;}ilP!z^HB+1fzhf9QC6`^oK&9npea2Od23?aNP;JH6F)9)ecL9xfEu_+`K_;55DlZgR{+ -HY^SzRudg&aT@4WIxdbtFjhfTb1%#Ax$hs=o*{Sm1~DV}Umvx2&6IG*T&;Tj}vt@kxsS{^@>PH&)P -$^01aS#qlUeFJa#H*|+4v;StlrI@R%`CADU(m}b@5huN7XI-$~rikZ{=Lp@AKneB71}2aO{95^47Hj3 ->h|V{))k(8Wlo_+r?h>LUtny{Ox7pW%Du`yQqsj&X^hV)rj<%<}md(jA<~>6Bz3K}dGp_{o&YE3}yAO -vk=KcLAXnD}ss7L(Ww=3m7^X~ewoC++;zX_jsH)1-5++H!^ju&+3+5CGm!!u@HDu!QN|R?sk;1yx(bEwY{U~sO&g49_GuCqtM7;`g3m|h -Oj9gtX%bsr6bE~dX6xBV9P1oGb3$HxBM9kDFL4o?0FW{xkR0~Y=gT5wdQlM9C7!$t1P-rU;Qq+Ho>xL --!n8gX?og58pb`~fZXRfp*h-7E_ci`+`cBS+Q|iki?nj*%O&$J;s&;jD#Z#3rMmrNb2Q9;D%C?^a@Ow -9U6wQ?-{7dDjb1a^ud%!!0|Vh4W*vGt9+|RdDAbq9X}nv3%QHjqumKJBOF0!0M`!*!6$ttreU6FJ2R~kB!)-1mtL*Lb6>g683}+PJe8 -#Uc^Hf_+JDaZ(RX3O!D!3;-WA5(eXB5hZC*lYyX}U~o(c7MXq3Fc5X{ve3R1a5y99?vt;e>cyxRhow# -lknw0tuuuFHR*5J&%Ka=efb8hC)*=orcU(n=1IhY3yYC(5{!HIVp#b=g6B=&voI*IZMXWc*HN2bc&!- -_e_?Gt8#C&`n9)=LMdhO>05F&AM?gZReQEBlH3$LRxGE7XPSu*kBWFzM>Y&ANUCubpI(jLi$Q;$?DWo -BJxaC9U8?;Xj=HqFNrR0X*-hY5e)2#!`AKsFN{63#Snr_ify(&;{(T@{vznrdfM9xAwXc4t{$?8T`CG -pA3IQ*FSAL{HXAwpj3P)iEBTZnscLB%C?iPy11el(QKmqlasCWhJn+yL{$M#4i_$VGB?p`#v!rKotKasGAMVjPS3{CkKXKd_DIwC2ED;j>U -)?%+vG(-Rmcj!?*cQQVT?(Z|_;c(Ys@dlUWkV+=mU(7*dJE{Zd|9p)Nh&F#)U{%lHClzE?*ZB~{yBKU -Bv1xl8i|K>CLwOv=5T6XRf)VyxX9+fk4dt`;}zCrY>M-0&>5&|Y~@!U~wPuQeODE7$4ZI6cdte^Yg@+ -4=#=sYN|`My!xXB61P7Ex3NbA(bkPplk_M`Zap7Bfe+QkOt^5)lI5XuV@pB$|+6;5g0WFr;U1tSR@lg -d#O^ZXQR@My3mmD2-r_0dq}6lMn8X2c-BVcOfq)QZlX_qsq+Sx?J(FSj@f0${qv+1A!Ip%A&!nc -;s7bmYJ(n|(I!V|fX#N~WA=u)V^=>tz3M~mXeG*XdpsQSEqp{3lT$p=HYi(T%WGuzhaC1c2hP}Um&m| ->DYO`h@bj!ZGXg!O`R3Flyzled>jREYkBt)^{>Xqidkpua%>Ht+COh}-hnxiv^8DM2tPFBP=YPQe?JR -kB0m-@{yZz`P^55L?hr`HEclzB>Dg9|#aBW-R5fFJ_a1N$d`skW(;S`W}&tV|Bj;!TEnErfJg8+aCHi -ZHI6j`S-h!o_a%btMl)G8RY04N3nWTX>762Bbwv3J`sO&vLvL2jP|Rvu3r_I{rZ|JN)&iMh6B)`w#zRZuhy@^7KR2wCqYuAdTzaOI(g6dq}G2 -btEr2o9x{~e|X0Bs?wQ%DFJ~j%J+E04gIMBJW)uMKBgv`xLfOK^|JiDCxOvVcKYEw0!*&HwbcUi8>{E -?H?~^DTcvM#uKxRFK;Sz5lgog>b^Irn0j(v$V66ME; --S~tdm8}w%ubo6AAPKlgX1S45{t*ogqoz}RVP4c6v9sgV&7EWXUV(-wLgVvg`>j&M4?LM^#Ln^K={d- -h7uBgBBy`;0UyoFq*(3CciI?3${SwDj(L!R(NxoGx6$f;+BX;{O)*kA?}VGoYGu@qd7$eZCg -+qzv!k1qG=38kZgRrU!Y-El6Plt$@zS;Fg&V@C#}pU{&L<7L81?(m` -RriQs>L5*?OcVZXO+a(d^g5=-Fd^eDD`U-%;?*=gV7j4!5B)>IKhxKN>db0;~4pQ=L-d50Sm;@6zCa|%xW~e5;(F5O#CfR=Ii -L-f_2&F(}+Y4gbOkPU}U^HpHMWwu_e_5gj5PtHSyJ(F(CoipfO;nV)*jkpJ^hHU=A_O0o`QDD>G&-)U -F!cfNIJB(kSpci2$%Ju$D#u(-wG*Q!ps9oIJJScD6b{q8Jz^S(06h0R+|P*AUl(!f5|d7(FcCZ}5u~a -Q}L6i%+GojadvdF8C-49!;#N0KMXE>T3Mrg8ZFnh>-a^5I28;g0~VL^j^dJI(4W{`n#Bb^u-|S`{zE6 -zP8T(lcR%!L+jE1fQY%#D*9_A#w5}5n#UEe8bW`g7G>M(j%298k#A`VtD@td=%DouHcmqWYB=-* -@=A_b06+!8`{xvwVk89mHk45DS6-8F%q)&Jee{hJZprnHY7{oSC~*688sS5G*1Rv9b(KgTJ! -}ehoKn7@WAHY?$!}IegDS*Bn|9EfZL0G61M=zBD>QC_+0+5L4_#h2{WyKE+h -M`^8uBF5C;-6LNczMGv`2Mw7z1%H%P%4+3!KtDPiONT(sMueR -AW8tWwuQk;1ijvad#GO8?tw3LfD|~9~>t3>qCd_vmwTqSn$n8hAVK~~kQdwPktuGNLp?kT6Xzph#f^LNg>mz!*#{s~xdCfdb=X>eD$zWGzL)tJV -(ia3og>WeVDKdA6222~g++O~v?M@#p=+1*aC+f>-H5DgzXdSeHP;1mmn3n)Sz={qCOJ?hT6Vv{aR -OW`e*%+BN*R%0GcYuXfs!dGfv!@t#Tc1riNtN%r>OWP0|I2T)mw5|z$O?9 -d3@T}Z2zUPsHwhZ+VJ_q;qg~f6wo?GA&4)~e0*bIrf)64AD+`B{0AXn>!jJ*2&v|m${2SMBE@@zXo&? -jw`O@@Sp<{)hiX~r6zhDb#jD1D;dhK<8G76SvcD##Kb6M$G8FNt4F&YAf7rVX1#Y`Xs>k9=(2>^+qdLEq601P=D~x$pzpEi}ed(^i_}rzY=TUu_Ik8QzYAs_2?)1WsI2)ah+Y&v=gL -UIB@HD6$TS8T>yc*2j@s`>uJ8F+w`>Ju~i1f29zvs?46Y>LmdNx}JWzr0>OPBW*H1Nftcxu=Q62jimk -fRmk4T+>Fekb~w8F_gw5xo(r=hc{S_r&i{951-Gf{6|cmkrg}8hR=M6@eVCyN7vrn@qo?&WVHEv~ow7 -CO@3d26f$7oGxp^=5*BS6N26z#$4BX#y$zRbG*RW4$9}`x;drzj5vc2M`@3<#@Rh`NHgTp;hJB1_-w& -k_{3EFUWa#my9r?rHhlAXi}IX>?u8{~jmh3_gu~oU9iA6-KP&b%$4j?$q)VHRshP+PXLDW>bOb9OC0e ->BX|}Pq4;S(kWFtDpWIod>=4yW)oBr4_ccvtd!4+)+=Dxd3bY-5!EGF9hama}y%#m5lH*9#Yq0WI-DR -NELMrW;y`o=`(>~f>HiNi&ITh??&3NJTG`CFsxO0G@mvfNZ2*Y^86dQqUXJ)y>Za}3HP%*MW-t(#p;c -sadZ+vk%hqeI%s)POqyW`vv5c??KQMdPXqxh2o5E|6Y5_DifoblI#`HwE7XP2%5zXU#D~X4{?H)IveK%)VKGUNyP+p5n^kOU4>@^h36HMI()pOB?{d_IzEUq4CSa=r_}%h8c+ -At5;ao1d8M1!CY1+0epSmrG;EpV-uP4lO@JkqI%Af?%F|t_$H*OX26BriIAlucPZyU{Mq!#@FJo8hrDND__wC&_6-(}~)73XJ?$BFI6UdWxJ#}r|jr}zwg9bW|;l -=BYLe(xY$*bi&%#>f3`MNg>JbVQ=I`xqw>N^`DOzR%iO`cce?DpKzEHen_BeXjWy{lMW$4+7H^^6&sj -a$@(^%_0R>b&QedeiN04`B|Ua6@`@3WxeHPCNyB{>E>i;*w)zjggzjI9%<@HIO)k8xe?IFg)jLDWby7 -5XCoLOY2rV>ZeD1tD6gt?h2DWd6UhLDyVH2Ogth)6m5;6`pGF51!l}G!&D>qm;hI~TBSRE} -{d%$dNvfh#q@o4sB!|w%Dc9e%Y$eB7ef^VNp66HubEL280OVZ(R#?mr7*nFezCE`ZQw^6B|R+?AI$9iM^!D%0Zy2*goqvw5?4tkL$}mozQDij2q6H3N -6U=a|x?wDOqBuURLCT88jk+5rI!ucx@do7jzb^4LU>e1*vX5y5t9ThIwamB(W4fW3`r^6w0Vy_akX5W -w4i@b4YKCZn(D_CARM2j@vSZBSJotV7IesV38Tc*h}>^*N-0(8SxZu+-UtGymkgwbxvyt951RNj=hR= -XZl-on$3*>mAbi6)e~md@U1(-SH5U;vR8019DlQ@Vf^385>VNytb>s3~e{dn4e9)0>=gh>?Og$tPZ}8>qI&5WXM)x8S*A$L9C^a6imy|>FO%J&i -Me-*wyM4bh&ZBNL%)XXP|G614po5Rw@h-f>j^QYFT9iN$Obs^G5akhYnw}J8G8rMweU-SIZhiO)lIhP -A!HSm%LZ6%>cBFC4FbXc;TLzqqjs@kohr;3YymDq|ZPj`He`mN<)0}+>b{G2Z#QXqXQWv=y&bA*+Viv -PL#THuf~u*;63-adW6tJuzhgC!_7HwYWJpwr72$u4=vqNMCqt8c`o1PEzw_Z4Xi9*CSk8^pPSc{%yLD -!<*W(0FWaopcz0E++YPzatkG^B=lJ1cI(6VOzVL|Zuow9-Hkir2v-#bj*;zjc8CGrXL<(7HfX|MP-tU -UpsnY)XuwSe#yS<$uF+!eA$Y7~W4Nq|k$5%7=M_zFRSS{wEn@ -_2}O-fx7gw7>;+hg~}1%HkB{ZO}Qtqu>V9G}pxS55q(Hx1j1qeNjR?}>}X!F+U=!-l~zb^&JpRoC!kn -veBfY4^&`wYMkiI?i{P{=n{E+IN4)(@nk#@S(=N-{RaD4-d>3VsY1IHDR(a|85~jJgb0=|Jku-I6nX+HG&;mQ$ -k9I&J4>>H6TBDx`@TZ|^OubM=n4c-;|pV${_Fc!epmna|BK`Qsgu!q?C+})G|V6{L6HoD(gX>-j2Ie+ -X##~Ah9YR1Aec}67GeT`6NUpAOBgV9iLZTWoCYmiAS$>Lf>Rs}fWV)NKr9%?WUFH&y?TaIU~Cfu@$nl3Qz!LrC&nf7R-n~qOaA6XnHhU`^wybW2)>jPQ_7~e+=jEZ*1oF6VX1YX59}RUN ->!G$2#lO9|IIQemwFo}a`b!h?9Fm}J5u?N%${Z2>B47hLz{8*VBSC9j< -IT5n{?aS@m*HOsBN;sRw>STeZNw5n -z|i|#%i-I*Epaa(6`OSDUybk-&~ld*ZT`DF;!nc@eXdUJE>Il(WB?(@&Et6}@mGh4x0LJaYpCK=yLa( -3M+WnYsP|h=ev0<1iWGLwPP>Gcy$*HQ%SE%<)@`Bk9yL`ikO*!U$(}jNmt@PG9q*M5FHynHnq5Ui9o_H>%dKZCY+q(%udQZ8aM|ZWKwT=w>S)CfKIW5QMtJ@v+IcQs$ -GuNaqC07cDe_w**J~2$+OM25$kN2uryI&>4aBJyFKvK<7SznGz4{#dPOX;aB8VsM`sgzfP{AcNE?5__ -iS@KpE-9%gPp0PJE>0x{jnpvt{H9`b)+uZUX-@nlGN5q@WK>snr9&ol@Fsq+=TzP!U8iGq3`m4%_3es -SvrN5D*ALoXDMz3e5)z+@MBoOk8fcCw8RC&WF3!Q&rw-dF&&BD+bn;g!^S-o&E>L5ACdZEk{h91P4Xn -(%02Bnj*-%ht~(qwyCg@mv^eqhtnXs^a_vV9Ez5+-iMsc9)JSBFUGDc)b`(|ou>>q4d!ZshNgLs8>d^ -U#G50AzH>Id6!Dw)JbKQF}mOPzQ3MWkW!_#%r9aN<_Q#+l+DU6bPwq$&YnB8OnV*Qe-UAb(yn%j9ZO& ->{t?NeGXvE!jA6Z_2S$H$8cvfzx;w?e33m81uGff1W$1o6S9l`cCSt~EWYZVr;U-(+@SnObeerz$(fjBN22=l4i>3eC -vHsad>2I#o6tVy~hQcI;(kOu=FuXh_5pv0L7kpp9e#vxU@+YLTHxXbU%z&?XWeU3H3o2vT+BknJ -x_~)eT1zN$ZXR>zNF)$>WtW-8YfTnL!X&ljh -tGNI(eVXq6(i0?EkZo(%<}$pf~TInUq7EoY5a-{Y3}{T_Xb+n^-_tDTohSe6@r2l`@v6&?%{R0WM^u=smf>zaao-A7rPhSL= -P|8VG%B8~7{Nz;-BDjTO2G+eorO9)>%)hlFEc;qmh0DgyP(wTmD+i%fP53w2_349BHZ@yWd9?*_vJ{J -xo8UB=I}r-m4`*RJ4lvLOrb`;luBb=tCx!#}s%pm(Tn9+#bHw%#1)#AOaicPe~ka%UCMZ5-(*=oDi6+ -YNU1uBq>kM^B(tB0kgL_#$3xlhrjTx_?9m{`rE4{rd)Y^nV)!{`Z{AuR-Fw3=l!7B~C;L3MNRP3dAt$ -FzW&iKuMlq7Tmxo>eCc(Sy_=7@Q9c7m0k^El2z$FA^$~@|H@JDJRSp@@{-wMpYyhI1gz8`+e^XDG6ob -r!RQTWIj^l~4D_cF3art(EDKO1O`*V^ftjKb>>>ynS=>u7DhN(4sPN;PKI`s%7Z1hj)l@b_ -CQdO7?8Bk@}|ItDo+7-e1c=J8+31c`f)0#5r6ln7Y)G{8|u-fXxX!oy*knVX-bfVa=QZ2mV}?iK$w#C -!+|{uac{ZxFMf39PNy_GY6CeN4a#--!r5eg|T}SwkNX^L1eLAWnamX;UT8@dp3*W>vHpicQ}_6ZG$3(>KsGLw`biyWi;UaD2w7^h`759(P^ZLPc`lJeAjjCWEf6?K%>q$q@94IOr3$TYLp$0@Ikw__t(fD4+l|634KdLK{!_lrB;n439_^mhmtwPY%lrzLDkPAneR{5j7QIEpNed$!^uh -8o6VWuaCbL?4>=Sg8#2Q8Rp&iB`vzz1=@8qP(;FDG%`I9S_$bB?R&)~+#*6FIoe%9jD^8{y2dHf`5L4 -X(ikEh$-4o3o&DL|gO|xL=nb5iyv8TxE#E!cyzfpzbDYqg_BjEBHY+`nBUN_W+=JG5jk3c&K(plx#m^S8N!S>Vjo!2JtQLC2)o00cF&OUo9?|ogJI|$hj0Gt+Q@uDWzKd}7Zm;@zf1ZSE8yguCKcIoG4bW(t&<|2=f1$0!kvWPxC2e-w1{|W^gJ(Jh%$az~bT -W2bl2+|=zPtC3wX#YGm#=ebv2v@zdQBs2MBW3E{r%A5PfDxkKa_Q;{|#m3|JL#UowD+GnI{bc&NsQ_o -)|_WFbOn~kR^(MX^KFo|A)FaYjPB8q6FXh742ZQ(6JBvQZu101#lu-dQ$(A2i95qoP~0Ec=QbYZCt285rFJ0)- -7UL1%fZCN3()5Ac?7ZiSBz?CE3vc3cY=-@yem;~kPEyhQ`BDsM}ir!~{&#!T?I9)tO=9FtMF;xBZ>0^O^YNZ6{%dVvz5|$|{9Rjk+oxfNeX#gMb(*l@aDfuSpQ -UBC%(MA4^ZW-TWl&LuzLu1+e^XKh6=mqZDJg@BGL(L#Y+p*s*xLi={6k53Lo?zQ;r>`s{(U6+UzU_X< -cGGj^k0{hzrmt^TT%va2{K9!1)A{O`jj05XTSt2=ZqZ3vNsl0S&s7;@&pT#d#bzRsF;g%qnLwW)*54b -u?{&8gz|)*jB46rd(LWmy4Er5;OM2|%0_&+iE5@jP?Vi^uoFqXAkGK=^166vIqG2z4Vpeg{F;#rBHTi -icHo<>Y%6NR-Vb<^QU9XM3Vl{*{gGznWu}+dUe?$B6Bfd3=C+)ACwdppU>`EX^IzUO(CKB-m^EaJH)O -~6!|1^$q&`kU^xjskz%dGmQc+oX -)(oEAECC-Qma|`qAc^~`L#IRKXpc>|7!==| -1&P_52meu@3MY~L!gaGP&kF*FpU#5!fa89#3%wKaU8}F3MJ{!$=3ASMHN`6ARqz66Ci@LWs7wDE?2$< -m=GMWu|buQ`P>wS1mR%ISTO>yYPMiOp9mKu}G*Q4vK6<;*yvawO#Ub=9XTQY{z{#~ -qz>%=QBZ^@b{2;EePMJFkLAX|UUZKuzy)BnHlY2DJcf9ccudmgyuqRbB_r(V?5==SsLJ7uT8NB90Y;r -&Iv%Y2gWULb>WKN8-f*LZ8_y{#rc($Vis-utuk-3|Si^@UWCs1qgGE~#IRC5wNH^v-v^W9g5-iH~3h{ -aLgFgFD~)r^xrO?v3GP`}uDEK3euS2R9B#u2haEBTb0_WhuUg{qxMf?oAPQ>C9qjCBD+5_`)Z{lJ7eO(sc@DvV_M+e_x;n?J{#ZMqd&_@kF -B%N#!n$VchG98>n#HIS1lmz4jwv$wO62i3#mF!%l`ARYBm` -}<~&G)BmR9-l$LR6m4P^U2-Yh)-dKi$#W$6M4}3RxihHMWP-?9<)ghYz+ce;@u@w@>BtVUPp;EgyOrm -U+IH0b`k)MT6Tu$QM#d9#TI;{+YWNlI!z+Jp_JI>uAzmOBQHM&)ghHhZz^x;vq`vzJ`~l+^Og>q)(?@ -?p%ky?z}@gFa@`l6~7cVw$;&?#<&vG46o^Ut>l -7Y%BY!1L9024)yG*(dC39G#d-(Wjvp1M#!;v)hlG&J{Q6MQSF*lD8)26qNi6g)u_?>c7I9fPJ8a4KD8 -D#4!PR9hC-+Ch%PsN_*w#b#LpaZt;B6t<${NTJV)^~a;}gYk?~qi@d*ib#}|aJilzzoc5i!)Jz0^MaA -r)Jj6GEh6&6`AoR`zS%o8c@WDVs8!+M~tb7?O^*Kz0$;ud8_k|`mA*NmJGbH2M?@W*aHJ>}>0f|DV!a -B@iJi_o6Ja(9~L>y|)wEgD)rqbfF3+tx9QQO`8*PKj}^{4&N_hXz4XNCk1TmUNU2BS5CTu%8iM&EaX{ -uBseo2fWda7(^K;A7mmR02u1O##a9aUgsZx*1x#M?@S{LFajn -?lE4szp$P&;HWWtUFiaqyp)M>0b6^{^O5X){@=`zCvT-Ovy6PNq>HzV#{U+8#;XQ)J0onHWYv;a~ -%7^xGDQNdU%*GtjU=6JQ4rGeD(_1U>Cvpe~XDjq2n*jSgVbTd^#Di>M$N=zqj-eKOFph{0>)Z#LV8s{ -mz<0cDK%Et|YON)e#)fW8O#wu=CYDSvInSAf3r{QQQxbX?e}_7(cJY7PEnYW#sLHUhZK!H+2(8 -2ud;?uQ5y=QBkNj92$yq~oc|#5gfydY%`TG8?4H%!H1Q6 -v_;Vv$b^FaorhafI#n?d2gP+1fR1sR{9#Rq@omxhUqc-ZOb^ixg9AY&x-o1(VM6F|^E0J`C_Evj5D9M -}NfCZ)_Ld*WdoHFTqHD)XD$&P5NIWj1b5W-v8Hmuriu#5=Mg?kU-gE`bPYc9aroLmbE^ker`F^{r -k*)%ab(KJO8eI$pP)lyH^Dlu8uM5`}V#efaF|UrU3182ff02DPoWzxE~YSqbEz_Otr6>MBt=97SX-#L -SL$8%Is9W__W0hee}2wK)O>fJ`^~6VBm9>nKE2CE^eeTb=Zf6E=7zPxs5XYm9KKU_iZTo!>fLCDp-tC -++wLa9|d*!VkHEQ0l}EB$F?{MwXYvbKaS*nC*2rtTd9UzWof=%OxUnp;Mn?A?r{O3(tsj#dw=54Y#H- -nzF6dA-d?ZmBI=K1Odp3tiG+tw5FuI@SL65!SMYYeRMw%-`4AP*uX%k4Nb29+ee0rux0xSAvkxxj3sS -{oie#%*#Yn0ye|*({Byzp*b{bQJ9@%H%k)6w$;6|X5sg$qr%G4FkJ-2Q8F|UtPsTxYj2;CW~B|6NcVV -GXrso*p8zA)Nkgjc>5R$=e$E!@HaMAp-%H}91;cm-rc#ikso_IydY7sOuKaxdcZ^FkFGdb(4-ML+aXG -tkpcYK48TmkdH~=tHvwo{Bs_3{!7lD+S|-j_C==W05*ZD$lxFT!&CS^T3upiB$U=A0o)B+dqShm!0y4 -UA@l6se{ap#d>)>pLDh~whXCyGH0(>>X@VNQZbS2tPt;Gzfi)9zwRj6_NATNNX)glKQk3>$p&rn6CL3 -J<0%UYaoW)BTrOdSv!b^(a!-AgC0ldHo*e5Qu`=)sJlymLyuqLs>wMp_2NGw{SB*3A@37}yr}kd$d2} -@SdgiE?yGISkWp)BW1ixMH_)2_wmGbgHCm|ls7=5CT*K+z-<@&P$?8Od4v(be|@lJWGzLD -eb`RGsmaMUhE7IG0_XwYSH42!rA1UgL8?G$;@4c)E^twa|KLb?u#@8%MD+%&?@o6LU!FHXQ>v>!rTBB -9dc1Bit(uRhW@0o*nEL4@KL>`W|Zeukj(Ah0}x~``08VIfZFSZ)_K5@wIp}FmO&eakM@qC|PV|ih|t|bcGHfN-o!j4OOh74WZZ1nKOUNiGCqR^$3@xwHv%w5T -BK1`QmU{;YeCRd+z{Z=;);T$~WGku+cS8tcenYr53Ke44B!f^O4NxLmE-oDo1JxT9B6S6-&>1Uhz|7M -|Yw)KCx$dAsv;J-Hu`ZJTDkO1uh;9wVl{xy}nXM15lYuj>6;+-$!D2S8v=k4(=1Ok3E3YrrP0;q04!y -11Jn^C~lA%0IEZ#gaqkP!&o$vbOCekGL#nj7)k{sDh0eZK95sJ9Z#7GSq@ev7voWWe6Gd?^VobqoE;+ -k$9A91-@W{$X#&z6f+IfE_Rbcz6`DW&MAt0keYkIQ5NkC$iejFaIE#0NM~-_)}H=2%QBnHMsdh^Tl&9 -y9h2;ewDoy6wATCTjDfs|Bi;!&xfz#Wdg6Ax4>8cPI2{i*x6FCEwuE3PgGYQ|C!Xa{yq<7tmZpv=8K6 -ufZR4{25bkcx -A}!I;jvWVe|fRG|;QLN~cTa-XgS<#NwJ -u$Ys6yVbi{N`)G)`5(HwQKSJ$Nr1x;Yg%(-xMLcmR-^e20W9GsQ5~C}!#%fDN52V6mc(^{$&X(aScDJ -0dq(8JOe}>2HU7L{HT&@TF7Bq025J%M^%Wdz6_Q2&eH{ -wYa^skDdXVv39&ZbtB(Uegw%83~C)q(hHNH9I$sVFO_!EB#v#YqG9wIdL98W(N@NLf}Tta9{7-grK5m -_JCynJ49d&hsty5x120m$DI1>go#Q!7B20>1=;51=fRgFOxNw+1@!**Ew_}DzRO8UpWq};IH;VGZds= -o25;%TQFQ4HcLZ -lhbXKswZ4D~F0n_nB(Z0Iu}$vn#ZbvA~8>&&CTIbnU9fYgt1A)%Lucf2UAV%w8Znzxw>ShUCj2iQq5B?jh8Ach4brPPZeY$L -+1>1@8K=$mm!H=ybT2>e7yZ8F$pgohvQ+^_&79Mi>O@7#%}0&+d9|Sz_6XfLjlEY8JRnz_%oj7%k%2F -FXmNQi<3Xq%Okq=R<%3R-l{!3_2hX$8x7Ibw5Fr;Ob^%CiTlDh-@Px?U0%nmOfx-<@vc!y4}`pl=#E*uNE@3s1(lKads@xnk;X?h`gMdye7mL>a>r1VbcHbLr0R4KCINo~slx( -0Z3am?mO-D;y&)Z+Opo>yX@v}e9;R?Nx-Z)F3V7%T&y4C*W+R%tkbMvLb{su<{g_uyg3`Ahk<(w^hZy -WWQt7E`pvVv7-Y2;HPc>GSs*oA&86U?I_3qbMP;MK=2adJ?XEL1FrVEM|^OYriyyTpw>gIN>) -SWoy`P7t8a>Ius)G}FOvOsFF7bf9hpQ>S^*^H3v-YALQ8s+vY^8F>P2C5j96Gw$a!rOlJ~^vAv(qm+< -uz!&UB`u$S9NXp5cx{5(mdwbMWFb++8M-EqQZ?EU;B}}c`K31DGJnywoIyXz4#GRWKgQArz>U8iU&?q -FXIp}dSxI`$t+q@(6CcUYwg`w@N!%JM?>h6-B?WquRe*2q^h|(7CgfufMmlooQKO|ZYiN$#X>}W4)d| -F66MQhi|zU~e*-S+4G!#)u-)aPZT;hkI@~H+3^_ES=foKVXf^CfK!GAnK>?mgK?i?}Lde^~7z7*UO{ISi^MQazfhd@~`SL -IgL|X87Ai*;bDTxG>5kSEr1ThjJ!EpFrMU)6s>+5f1d5-;D{WbWh5^!(@v4cL+X1*|PP9x31mdOA4Ox -?FZytg3-^p)p+1_m3+MXSGde&39@EyV_X=PzS#XTSo?XH#%dQ+SKMuHYE-{Xs*@f7C0hTI;(S#6euOL -vY~=(A@nf{X?IPxBcFno@e=+hogDE`X&-L-dB^0q?#@wm%6g2-1+=w;y@#xsI`1#DbWb?n$7hv>3T7PFSw7B_*(?uFVpv^1cHXU -j?#a8ptBAp24Xu0$QFt!NyNe3Z+Mm|LdRq(WeDokA=~#KMk3rkW!E3k2uEsMb3v{%NY}m32>*1f&F&^ -dV;PH_v4X%AmNHL$n`+Ck>267J=!dZxPhOf)s$#?F)7f~L&Ty5ka@4M5HVwa}$##QZQc;;=owbZM-E+ -!kW>EnKbXd@Qgtn;r=jRk!7w -H>gOQsDrsr6GB!)SxInO;kD=58vZ1_4grrX4RnaYD08;F$~|^x9`B-iJ9kTP#AmJF!FnTWh=fWTTo9& -&*eS4{_pvE0y)SlDsTLM=A>4S{p`F@!b&vbxJ>TJ7>X92618>wZ -!F4zeQ6jhMN9r3j8caHF(p4f9RPZyKMy$&bm?uH*$qR=RN3`;OZWbqDX`EjzhnyJJQ`?WtXB+&&B8#W -IS*PU^~y*nLMyJGN9jA*YX0b7GA94K@iS6)KoS>O9pC+%YP-#)K><(X1nt5;s#kCTpP%A<6F5zaOHET -iiYRH2y99zn#|>9v;8DN5L?&g_&?-aM)3b8gobGm^K(Js9GO--~5$;}G~AS}-u~MZK*Uleb}3iU9up7Eb8 -&7r_eKQl1QO`?nPVBR>R`cr*r=vBk0Q9XZn&6wKqd8fW^pCE2c!CP5AszKuoF_naAUVou&XwfD3bh@^ -krTINBubvpcR^k`^1YqpQ}i@yG%z07|EF6e9I`t$bk?@7s)#2{^eX;Tc$pFZTvwHI4|bU>BryAx!0aI -qHH=}q6yZoe7z=P$NW(07-ov36jEKG8%lSL+v(1TiIAiAVB##6yj9umJZ+<6OKW9&`lu$1nEV!SCPS- -+6n`pS{1o^Y)-Wdw+lD?Lo=;A836Y?|8X4%Px7!=poCkX*xkwLN?lTySBbLch@cQl_s0{s2@GWqkW_b -kw9ImK7`Z7mlA$`2nBi`iW^pL-@o=l48_qsq)yZ$d+aNHLD#3^E2y3p#CbY3ZY#s8am`}B5R27|+zv* -Kv_7AWu{hUka=~3FQ`rsd2#V#W_(SCi?{MwU#Xi4VxI)GaY3#DyEV&PzJ+SR^NAAPox%2Uo&WXAw?i6 -I=k7|+bhRbnf`Fyr=$BXhKA}-XpxH4J6R%aj=X}7JmMSt%c-L!07rv_1$q#Gh0+A^7%vy?04Z4t>aiT -$ZlHTCGxl=pNk#Y&i{6`u+rKTcF$j6|tt=8y!h)MmfjS|ugbRI ->NMCQQhm4>JvQ&w{mkrV_)%XCan<+X>(byBA+K;@DUDW;2xKnu2r*Z`)$(ZA{<@V7dK7>}l6bJHAST6n&kyRA-!;Y1bI^z`SzV7sWMLF -K>qMqmMj%PhZ5zw%T_G`P--kraTnwrWlhf|(tEcb?ZI>LOA1SvO}+3}IBhrJ{fJD2H9Me%Tl2}vr3&{ -KoTs5bL27+qa`HHL%?(#Szy*oN=!TEn~|Up!x;*KNwM^86s-FxjqlK40l_AqGe>OvHZT2p2}^MSu&cj -^;H`Z~CQmpO_$Ij#lS)-v9dp;BSYkcVc$@;RpBopMG!V+Stz%-2Y~QZzI^BpYcntEs9`plmz0PpDS3$ -1c;!~I~H$|3ntza=C?C35TFi0q87hZzJM6@=LD~W1QX3)rqDpv2!pxiEh=tn;{+&e#t5iPqHhZiU@k( -u!#nw(@K>^(arCZTqVG9s3e*+Rw{ib(rd682iIb(P_xeHDRF+hr4BGn$eHW -PRjEtD%@vpa<%q`UU|!0_r`pKlpA#Je@ -_8{a}+SSP5Y6CCs9B2&ny!?~TWRkI~4r)zuMNBYX&Y~qR@0x!lo#d0y_oF7MG)mJj!F+4TxU4}hwL1T -C04%P$S365DwVd=aeY6}Wl*{_aU-Kk7WXXPKOUK=)ig&tmSV#>>WV3ym8dRqQ|CM`k$ikVG_DDjg%KZ%3DI8#V9^x-2>7qx9Tdy+ -BL`U#!xrR9_e4{Te23XTjs>Nhi5d! -qsbws6EVfgyhDjTnvE7JOM|fE*Mp -K@dt>^xWs;}9(HMi1NWGegfdwn;mRE%MoDr$XvvO7V^JwCms=Zd$qZp=q;1HU`AR110yVLkr8!mV)@| -U6x!=Fm&a?1Sb=fvn+PpXsRp>W_4%A7oJn^g&*pYc@UCqjmCXIW@SZMONUYw#5j>mPCQ==~Q#UZI-@F -Hd>Qn{<0Blv+T`VmGo>uOV7)uw`xG_N6U704Hu>~DuBb{rnvEeVs~lJxglV+?AI(yeR4wtHkLkP-{)- -*{E}jU`bM!(o#CDN6k}>wE;Of4#;J(?c*$p%jiX6bcl`Fq-~!K%2teB9!>sH7W*Orx5!O^A-+^-+q!n -$eDgy5d(kX&m}6~^rI{SH0ABw^lcp(;~<~I-yA3qUf!mWTjmGD@6bd6T2l6_Q6U%ybSWByPOuh|fp4~ -4a*J7X1{T`BNodehW8TWyKtlutYLr_v-JRkuL;s2z6$%K+6upZjj-ApzF`dGGSEWkweBV{_eVA_{iL*Fj0favt2R7d$`%K6&H4N^d{qD|Mvdy8m+v1kjfWm -ObWqYPfAz@kUtTPBFS(=5T*0-E>5szDG}XbVW+4jPj9Q|;3`8?HKA>{tGkPy>++Fo#?PJ|H)_`494E0 -s1c0%Hc&Dwsdig&};pcKHGxW7MC^q#&Kq;qQ=Xb%`%_!T)jcRurlVqSQ)Fp%6JQG!l7=)BRQ269jXYf -Fa|mbf;%J(G!%l$*5u1tJMoM@*R@!ZgVy4r-L}4$PJ_eogxa5oVasUpqoeO#n~?R3aTjsavJ5B=d&a% -sCyrrC&U>{JVNbKu@Uwmn-ALe0`Z$k)Rsl8Ar8S|6`xKA?~(iwH|1BB!&(hohAf0kiDonE -A*8iu*-wW>_^2rdpp>=3ws!}}(KPfcXQdz*bB#0@f8X68yfm!p9_^^8-6B!t<$!ym`H>tR -%bH5N^R#?Av5-23_B(v})`N=t9!@^yJVCi)xB{%E8!eWyNcL1d4@B7Ph+^F`A%9idT42J)(3z{@% -S&_WM>K*Oy0I8bG_a*&gcpvhbTyUp%VsA$@{;QjR$;n>cNM)Sr^8@K_e-Td;w?>zoOspv6}s-NKxz$g -FJ=LJx%q~@BMAYHpW{~tl)Mk`LyQK -KRgH)1|O;v`wzUH)+oiUv!2_nsky9rfTqr#+ -4&`bG-sOIwwVjol;8Oc&DlJ_|eP6mB~<*-r&^lk%4Nk0YC(;VBu8)SGJyfnDXNP36PubB~l)mpg8n7<2mbaDTS#v5^T2!`a&Q>RhPjkAJ^KrXBQ -lH(3umTxn40)*bsEX9EZJe@*lfvb-282{G6xxZQ5(7Kkb&lTX5hnDM9X)n5kc -C6R=*=gtntIG9(M$tSRggU%7ErHjWN?%JMNS;anE<_j@>+TU@)zrPl53uAN;wDJp;PN(ws -**z88(9p8~-Y)3rPGH?Hqf%uW#xUzd^KMx;+HJ`bC2U6zw*ZmL*SJGwELULPJ^-TAS`6x;?YBDyeHJh -YGj|arB3cUQJRcym)5BzpKkH#K-b>FVCgO)&ae1s^|pCStG0KKPvx#@?18VtBeW!WQy-P9a}-yHcCoFZ_uYq#$Z-n`HA!I~J8Vz0UV-Z}OZFw% -o>wc;sxuuc@~LXyrY6z6jBAZ)v(`-JQcc;jz=7`cki|<$OHHC1o1iE@d@aM9ixp6oU6u)NUPQ^0b4r& -h=UBf0A6BvbG} -C_^UGd3L>qTOe?EjVZ;$VI}LyKDv<=?~CG@9boyHP2y@oTxo`n$EB_WKM(5JKTksWm>z=MS$!OtfxZO -Ijg%gG!Fy>XwK?8S~QtwY7F+`--bi8If7hLcttg=h;B*V_)EVl(EE5mjV9?>XK!lAW#IicU -Tfoh#5CnItL)S<=%?qn-YOr_-~DxH@%)v-;RcgyQzTUgx9FatBMu9_Svu?`{H-oCMP_cT8pz7i|YAMu -F{-F%+nDDcIbxeUK_reKROS2xMen>&k1NfLf3|2fkI<+=jyjIPUvvendizjF*9J8QrXV+Li}srCwj#FDarp*r@bMWUozcoC_2dOox-V94mKU`C`2|?yCtw -WgIIBy~-jON!gMyq`t~Eye+l{K2b+2CxHcnEwj7&Vs4zR?F!g=TX2C4p7X;mXXS~t}|IncQhN_B9m3x47LsLW!GJIm^` -MHnC`wsbILfTsvkjhXx&ve;hp=U9w$%Abi*iX3GX!bjP((dMR{0>Yggqbg>qHymG6T-d)}&1%A=4Vp> -S@V%k4gfVtqGMwn|IUKYGKCw&xM^XRXWBWe&9C7)`{~(a++%XRU)l`q^@IUh@Oe%n_C@{wM@DQoo&4FE -mV$VZx9Ya&bdQmZ9<=-YX-&y0)1Ukq^42LWtP>HQGyQ%qo?LX6jLS`mO(y1Vb!XUGlC9ZS -njurn{(A1XFO0FQiI7TK*G%4J4S1q&b-X!i9%?ikH%MwDyk+I+ym@_F{W-gJR)A1Cotp3bSD3A791i7 -U@Ho|ilOaH8rapssXVz;+dl*_6;D?GpbJ<5S-tsaN$k8LRRxQ*agn74D6Tpt#EThZO{(vGQ}AxDZ-f5DLh -49>zQ&!)U(vM;TZAFYeAy>aUURSy9A!`jd(>HT3sD$JF5{y-M^-lW{dkBG^h&bZm15Xspsd+_vKHNSO -|9~3F7hw~nUH{u7`;C7zP1?ojmmbvr{YA?0_&-_xH$L1R$Ut%LGYI%^*Z9DPfA#zycqqtQbr{8IoQ5~ -VNFn&A5(h;3W=W<%AeadM1E&Dn!EcL!4V7i@ZW!`*tH*#m@24mcILf_?k74}oYeiq{bQ>nzj&E>pL!B -F-+g>E3|3jsKT*8F^iBq7l_>00J30`gkn%P@qcmsP$1`G$V_a(vaLgof*aTH)poCH^!(17a+_?;=JCg -Le*i*0CiJGi|hpoAu%AiDh+!+(VmmGg_|xpVqE59M&$b}6#zLn{2#$Mriw{%;t~J4=EgerXwTi$R`!s -RHFWl`mdb-^~NeTjRzX6P_!svb|m}8{Cs67*dt@$W$5lNF6s{s)=8f+rFK6IExO@zkM%m8=7?nXwiBZ -WsX6H-Ous|DRR*`YvqLR7yCeohckQsXtSxJ`Pk060TD+?m6>KcFh%btne|7jO#dp0`1|nYTPA2V$_n{ -ic^D}6|9)_bxA@PcOV4nP+4^#Yiuh{Hz_+)EY2^pStgjANLdh2WHqZ)hH>A2MS`Abfz|iKGVu)EpzPD -bRdX0SXYYpVvd97iqGbDZ~j7s8Ma~rhAOu<->8{C#(?DeDXC{~iNz0L0z%iWdW^ipiM&>DYq`*I5{wv -)Ei??xV>!S*vVfF}j0`*lo#9lkgc<=@aX^w;p3pIBp<&2?n`>On@z&CNn{FBf~%One4H>E5&zb*FU)% -kA3oxE4V~`mgBfG&F-$f^Cewj<;@)t}Jj^hAGs7gz<2F*rO~A&yeqWJ@8oBo#ZsbqJEwCmXc$q=FBpRVN0T -P>yhE~Kz5hsVu?z^Ty2QR&HWbk>a$-UW7^R73ctpgyE?=&1YD)>=ru`_b+7g4slsw1y_l(HQHRTMz{E -V$p{png?PN*7X-G%gPKGwya -lKi4PXfb`dW;Tv5?<)5BakUN_kQn$HrYcv1Z|gV*$&bWq#e#YDvJz|yUVYaREBp9Vae$$QCs9nUu3JD -rg^pCxz5n^crfFIfcy%+n#2KH40-7GHlb#7^OmmX+6WpcKbLZ+q?KMR*1{zXEdQn1 -aNOBi0M{td&rgAx%>w(C|>2$xxMO(sno((Emip*hm)R=@vrVEw3hUMHMqil)|W^Rj((T>KAr9j1R5|} -C%fd-48&tm>P+{`z)4T`8LCkQ`TKkOW*7pMcWQ||1p;Z>PFU(ePdm6mGG)3+E)qRtM7zIYB**>C4Ve| -$ny# -5F3i=~`_C-Cmdaeh`&`;<2HokBrD3DmMOR%uy0lN0%Py@V*Y;cac^nWZufzM3=)!Rby?d8eW2VaFJX?HM6Gct@+) -8RA_NJD^@DOx&{UTL%8u5JAlYQ%WnWl4jU{6 -bR9UK2ZY1!a4<{`Yr#6!@tD -+&Kcl-`8)pk-Gp~{FLO@EU*P`!|A)B$I~Mp^`JmVFoB)OLhEbaB^1dhS@$eTGxbV%P?CmaMl*VHI?tz -IJFL8lB4t{@Q{>goTKD#mh0;^>BvUXs#N+Uw7 -qOkuQMKLucxj{RehFGm#cy_V7;wSCOKtMn)kYq<#ZxAH)igLbolxkt6sJH}OW261S0m|7u;&GnWBay@ -g|Ik2sOr!)eBQa96Hsk4`DMO~@bIb1lu}7Jlq4r^pL1TXCN9aRmT4n~L86zv7LSeU>~*6lCq@poKf)o -{mxo3z0*SZ}wspK@V- -v>{KEHCb8-Q3@(kHgPTl5ecbN-kL3I&fy=9{9ia6bhe3$RUc6=y< -g`OQ|VIJ^w(Ca3rM>aYmF{>cc@F;qQ%g=&u7;}k`A_ -FVKRn?nbN$odx-B{AgdYVEY|nj{)W2#do{tq$Y-qiOt~~pQQLk2I6N&Fy9o?;b%LTTb$9u&qS8SWjhW -O*p@a_c8gT94c!;+I?UkHbfood$J8d9fxaX1PGnX0hvj6BHWR7%4%5OiUO@(gnMe+m&XgbAUj` -|Z~E+}r$+0U`S{YA#OJGrg}@Px9;NGQc!Q#ck4r0?HaS1I?y~Qb>jneu{Q_mnVHs1q%FxVL*A5t!wR49Sy(=NwYrm(28f$bhyX5Eg%Ie+=3Q;3xN=f2{>c}YE&QiGW#BiPUtl^WBPi%uAaq -DWOTa-&5lpb+7U&;$tX4QdN*8~cR7usE3Nn48P&?!pxaGTxJeh%9a!&Ii~m9Ezt!*E%4AK)>lg$s+GA -MJcaS646+$a&Ub+gAd$!(f6e##?7!isM+F7-4)qpNfy39@S3ko~DDx_o9<`FhNLLHcLJg#AS{Ud!nvk -OVfr$KiFJin&a)>AGGRzzvIqH^`7yoS;cd=rDIq6J}js5wKRuJk~wuxbo~P|V7;}imX5N6oH~0cFKUJ -lYPzp&^md@qIXfA+Pen!&p1ZcmsdZbExM}|DF^YJeLKDq9wO~c~nqs$wn5=1kC(mi*a`5n&DxT_xz!% -zOlFRbh%ag+A*<+Z?>v_kKPx1=EOg_IB!#pn+t16fPy*}f -jN^6)MIaZUIzj7%i-u6)ucxhme -9Dy_A+C(i -nZdi`1`(aMP+B&E)emO;>0mHPSzgQXvJ7K!)B%#S94m;tnWw*R4TE`fpZV9Y -rWn7`8EsUn^{oIJf8R(zmE$R)N3NhW(i34`;zeQX<#}P@VV!+rNoKEP(g;W?Qq1@vu=k|9T%j^^m$ce ->4I#zRuqKT$=iP_C{xxATO6)UBS#UY4YKO&Ov}gGLlr$;EwotAs(J~U$ZCku%h#mhf$0VmY2%) -mb>F%YGh9BNk^(#-F6JwyLR}4RiOQb)slcE+{;w&&OVxmWLpO6nb3p%(>e;f=V5m%yl2MX`b-5^iM<@ -i{`lZDi?Z+Y3JICPUT+)h_UplX%(kp&=e}6-K`2S~@jAi`B6AFWIE_P+83nzg*v=&#CunM0v%CV5D~B -ujwY7+##K@VVT`L3R7P#Gny<~W4GqFDAIkriy7y{wQ(d=4@A -(z$p7U*TT2x(L~^LsHTuH#Q^)7p+0hi@egS+x=C7Fun&Wj!Y_yPN;uKs{m?6XXZB$Q<;Uj;bPKi7%JzpHq+@nBNL534rcS$+`OrNC -!-8+Irs^|WA0o!;`r(=85*d{+Uj|OR<1_8E&!E_z&UK$4r6oDab&c(lsP7CC++1_)x@6SApD!jx=21{ -M!`k~%4N+H{jQv;6K*%Z)UD|J|%W -_iF3ZV|eNzD+NAky^_IuAYaClk<4?AuW>tV=V~AuHhyKV7c#pe%7^9+YFK!7uAGrA%-6-Az|`0YjhY0 -N=0YdTw4afDelBt{03f7UZ9(47XXie$;*g>;5>_N#mk<)Q`OUnVQi5+-3iyK>T}`{3VrrnXTk_^3*`s=YoU{4We`d+|+w -cnfh_8RZEB#yW3jBz#e}Pxq9p&)@UZ+iYc`H`c(b3AiqgMgpk$7*;DeLq6p?0(hE3+TrME%`KKf*2WE -4=*`+yYp9S9XC+zWr}st) -N?sEA{hYBwQq&-Y`VXgziaSx-OI(O=FFEz?h4z)_nX^kb@EEN?0Lcs>LbL}PD!jOk563LO#9q@%WiBN -vRNETS^(Xt*os64--R;qwj+X{1BG1sTFUSC-v9E^Zm4MJhuu4%RI(^S?Tt!pL92Pg@9=9&vTyz5(v@# -ZhbpgLLLuPeLP~a<&duVOB2H%c*~ys>`^gGg-GC0jNo=xdOHWGEpT4~~Dbd7o-6?kfpRga$fHXm1&Rjs -#o#t*hQ!HmdA$<6Q7x;Q1Q${sX&!rt1!KZ}Q@XY(v!GG0S~yZ*dZ4g8{>To9uUVKnDd2_+=R-630U-p -g(&S02p5Z{Ys`Za-A?{oPk_Aikbp|-lw)`XUA)u7FAKHkme@eTa2c%z5u?uWDfGekc{; -uU3ADoJM>A>LGvl5Ko7g?Fc%-N1(ox02bW%s}sqlimg+HVxbCCrC_?+qKq!V2uF6eFT@LHr^WIo6i4= -$14Ui@D?J(+4eO;Hus-x!UEoIhG1J@u_7_bY=9r{W_jt}9G9#t{?|xMZBh<>`b#9f^Z#EWvAM9s2NJJ -r_F=eq@Cg<$F{{y%|W>=Um^cf6Rz~PmRKGb+f -Zxpi7lZgazD+~)cSpBa$Ao?IPS+DTK6_vetMKyfaV>=>Aj<=MZY%U~Qx -lDAA;XBdg3#I~|8!JQ^K*!7P3GZ+g7Kk&DjFQTA}ss09#cadxL#`O$P(&)JC%1CdJ+6@(mewUB;B)W0 -pa=D1R2hX`{+35bDM)4mqVo{kg)&O4so5Z8Kl=7o3di%M|LU82&PL0BNg+hWhi8Lf3&5ieligY$E9Pmy_J6aT&ejX1Rg&wTt)vIlsXD -&*owQG;X<@L^zS8WqrqCh7}bLPo9Rz7{> -GS$<7rKgN3X@LB7^O3JOMp_~Pnmm7pox<3n1W>HeFPtf=e%wQ0*=q;ih`jCp;SAhUcOiwbBStpiwbgZ -n=gk3fL|!4UO0lRmhd|J8RVCkZcZbJ`D%|Mc|ghOtTj(mE#B;-Z+I8Jdzci;r-ifYg8_7QN&dbXMYeT -we6ghc+OWL>*w*av#}9rKp7^~}zG+N+diZxPcO*<>1i?@gL*mG$AMt4)IQ>O4H{4n^(p}z<4mV^AcNs -ro7ptZ5uPq$W=Y8PoAuE@Sc9C?P*dUXMH>#P&o3QOBEC~NQiMN(*GTrcXGulCS@ptNHec;$fL-{Ua7= -Kv?yu&%ZoxFlN{KZEn-P^o(>?d|r3XI(FIoz?I*>l{Rup+!GA!XZYLh6_B>1F%S!%<96M9=r(lfR$N7SArKU;aamF!21``ZwP|wHY`Y2=21*ux3QiByI$xTk|`X;7wtK46aBc?-pV8 -P+$kz8gncl7494Pwg2lVDt~BsXe`0b$Y^q#pF;n#9v(u1OmKX6Z?wuDWtwCWbov?vTbkcl`HRd%99Fj -{4nCh-!8&HD{*V4C9f~kMB1DSLvV}UM=8_da1IAl3YMcHJ->Drz(sk2>oyLaGT(Cbn!l&Uo-?X6MiCs -yZ1ggXf6NM3ij`?X#1E0pIW<#Wyi3Vxx}mijV*&{5C1aXnG`JpUuf)}gE8kqH|u_V1TOCBSd=&zQ}R7 -?LFW}kg8F>5auB;+Lh3L%DB?E%Z8($oqOMPIaWmv-QsjEE@3*0<1;!S -CKsu(MVBW}p@9n$mf072jpx(ZI2t$|kg?ty@|&Dk~Q%dzuHOQo;x$)sC%d9}(L9$%$vo=_9ahqJClK~ -+9=B#>zzj&vM0DxS#!gTS2mj)0qBdxI%xISpC-Mr^|sMfQ1pOPU(S@U7_FS~|Naycz5Wac$hYDd?H&0 -ch%{8AQI@7Vk`rSf7q)w#j2sL~VrA+-k)rwRVO}X41bum-J@<`09GTZlJ#nF@JN)XAtwp;om_FLr@rv -(kO*)W9k$|e5z5j*?JILPIQHYb<<{cDIt6>)Y0+3aby?aOZI8*mB?kE52Q2OzK-sx%*{G{r%v0T60wo -AP2LXOY)+Fs>$(!FFEz-`!{cWR4Gy>T?FLcdhJY*kqISu>^|bZ;Z3GG1u!BgpDW>&u>upw$+vJppjVZ -1Rj`l<;ynC6$={AeBo|3`4@5isj_I`wzOrQnxytsE8{@nTFe;Q)Gxo?L)(fk!+^7)JF$Dbi)f9-!BV! -mI=KY^HDc65D;hmbgTAu8GrbEp>V?mDLL=1QY^{#gjFOcSiTHI9s!on7B|+!mw~n}MqWk4`rWMJjj8M -5a&`wmKFgXCp`I;=ve0LGpd&ok{T(Uf%(vb!6MrA(AqPLjVqo=2_ -?xUqHz2r(V&ac*1$-5;Ck$=(N<@xT}XAQ6fdpa^%F+MIVRxh9P|UMu?|jZa0dkp+G>_!(g|Xm%yj$Ke75VioRgKXj^QZvf9&Fx2#kSSU50zuwb6|o9#$7t$O>$ -f@3PTx1<%~BL87Fbh-nuTfC@lUB#BdX26T}jZcd`U5%^uU5wOq*&W)1#tLCiPJbylt~T@jEkU1N~`9^ -Dt`&}h~y3TX2*pzi%jK~OkFHzzZJo|gw#bVI`>Pzw`siZC#z@J8*C@#7ikF?~Q3###dg(RP=o05>Oj; -yN+#p5^Vi41jfoClrifvc46FC6Wkg;{o3obBewxx8rR!&F|Kc#g2YuyolO&i&K7k%Rweyq|^%lEq_3f -QJtC8Rr=$!Qn5A{T~tbZ6!VoJ#C*(VRUW>>B{(kWZK$F=lT)#t9^RwJ2jEc;OgmM?P2`POY;Xl#2^gK -`-11~#drBhiMh30NK!Nzq+aq{TPLU{N)1%6ybAZ#pgr)C5EgcLBo;j}M^ljxVCK9ruElS)Kz2#F3zQX ->^bpIK|oZ9qW4)@>2m!BQ~8Mu5s_Rq*d(F{gym;zH6juGgJ>YvKE;@kcV@6zdOxKZ${JTA+0{PWoPIchHy6taYjTdtBUM!8R6&Qbu-+PTs3G`uzEWji%;D>i5^Vs0Rh6E_*d -wx1pN(3^bJ$lVIGtrvRHBr8)gHI7dTGz-xmqEu44L}`HjoP5=+U#csgYeM_QUZ{4Yf>36dyBd)5_90q^*up_Lbu{Uaqr3Tqz&}kPnG7A -rj;lOw7j*aYH?S%*RX^FDA)wtLOHRo#uSRKgBmC -cha=#DCW$;eJfWUEc`G2MFF_fF(>EiakyylZO5Tp16*AVzPKKzcG??xfL$*Op1|`r2UQ%#u1f@Q`taP -+u-oIV@LCKb$@n_GL-a|j>tddcp;x;-3JuEmm!g(2pcb2+0lnoFAjFZF2N!28Kx7+#)7%^5MWxnGU+- -L6OMUiwKw+O4_=iqr@kBeAKL@~*2mf>;;9FRuv9R=&CDG$9X0Y%m^ofkjgqu}jUeZeu{(^gMvTO1f!# -Sqc7QUULeZ$lG} -@DSE0=p0Vf?`XpVe+cM)pK;{bZ>E}y2gBC&hLeQ4yasN5>*Y!A;5u}J2?bOcAzpQI6qyU+zC-M6=JHv -iuZ1CNxsHj#8)iMrD+*YOd)rP=x^taR{n(TYUO)7efAs{-qc^8iJ;ZqC^0SMwR;Mc82(eR7k~G-CmQj -{@=HaIq@}AieJo19Af~k>g6D2> -D>xppMWiHkPfU`;*#(<9Ex=_bR`wUM?}7$2lJ#pJw*tNz;q-3K~9lo0Rw(N-oP@NEt-5{FZ_{l`1#$^ -#thZv!&xv07tm0AJPz~?&+i`B``$bVw|eMl=BbOo03r)%W9P(aF2w9as6b>E4yG_?5pTpBZKd92BdOc -#$`aR6LfIo#bQSavb9s77!1dwR3wML_Be=9&TQv|z*(&C-N!|3XdcJ{=K4|@UFfHb!Y43G+f01Jfy%v -A{SU50PL1YT3=>Z+s`f)suSL>f>fP5C%2#3Qz4 -u(EE=Wha{?fCELHVz{wiZd`plgRoPN`4v?ZJsUgCZR?UyZI!t1sDvoQP-7#BiNQ!T_c_~a7tGio_yY# -L~rh!Yvi&s=q(Go#Zcs?wYD<&O*$^#WbZ?K3vf2;muRbxZXHEG>-60w^N_6_INQBDSGv6!vt?V$@XFi -CUBEcm_2yPqzOH`_xhQ56zt8AhaxUFy^`>wfZpme0*DayxEqq#oC=~luP{cu7P;~YG$ml9@U3M{)*4B -8)ukscYEfu`(pZa)qzAuUbzYInGP@7*vk>tC8Xp2OEk5EJwJl -s*7dNCIfUG+!42|JPtuM@bI8vmEbUMm?zH|6omGhwWUGBPSq -+9h`NR=fgL_A|d6Uc=q(?0VU31`3=W5V@lvhXG*SORW8rJKKqJuqtUQDcVjE|a>z`n6uGda*EgtTqzm -MAK=_FA>5#w#UpUCIQj*G)hY9&fYXkus3gG1G|5A%t8>i! -NjSJQ2PR4#DYpC9YkHqbAs2B_kSDJ^d%fzgq*vx?}A7N1A-(>o%VZ^Z&X{xupH$yUvV#(0KFS_sna-Qmw0Axbe6{^@k{y3?h5TP6-~c{+st+LMWs8@;2M&2e$vao}v5My5XZ^;ntO -$^%!9w|+%;we$7VmCYf{wiT`7YqvMYFQ1|`(isJXJ=^)c!6m#M~KYX*6lAA29q2@d<|RI~Jho<_~-fNs -m9{H>$E_M&@D2AA`D<(c&rErtl4-oftW{+DStO_&HC*!mU{AO6fg^3OdC(@N3uk7kggyZ3r*yVO@ccl -2(uK-7tpyyJVfd3Ssj(#+S5RGzmUM`O{kuZAG|kn+cCfM0Ro4@1e8!waQ;U_Yk!0?#L&>D_~)`7iA%I -q+4vqtOpv#Q~@Ff9Li%(zd04*Ou~(P&~IT9vp63fR{6|y?=oG(SAAA{Kxz8Wk){O!JiVKn^DSV2~gl? -rjt0-GY?E9$2YHta)yMXXyQJ>a@4a|5Cme7uJ4dm0$dMlpS*g%UQ;y6!6RI;EAgx@p_dj%zD~mXnV51 -hX!&untAd7e$F}F{A&WG{k(v%Tms3QO#A!kz&wr(UnYGIcty~!o0)?g(g_^slQF)2yW(+)(WTF*``KIo7scrOt*r`0rzf-sXyTlxx8^OcKb7#zwE7J4y9?O)4JGt&)at;^RWRppQE -q(kMj>7YaXhV&8;JOvN#yQ>a%)0>2hujWH+az;Gj2hN$>I3z!`6BifQ?TbRtUX_z}X=3=Sy*K!UUJ}x -l&!_LOc;i^W?xfhYewWp8!35QM36GRlbt~x8$V5G!CL~Om{J;Xt3U&mV@gqAnoW0QAbDboO -%*`RRpHBfx=78bl(y2w2~;9mX%B3uksTM%zg$lXT7LT+m-Z!QTdzZtx_-qSk`D9l-^qK9!VCTyAPo21 -=udTht4Wbwv{UXFHHlNJK^3wfEE=%El{74%rqz7@`=Usu3;7)xy0$73%~R`htkOW5(7;Oz{BnOivIk9 -zswR4}OXv)y(4q**VuM12ut2~V430LHFOvr;_n6)6b6DADjsLSjp!n(x}e8_Pe@=@^wQSo5W)ZBFg|D&^Cq`OW>kNVjv -rvRt3~U4JR7{e0AFOI*{^8lBBtXKE7*Oy+|O1E{FCK=wp!q4%Uw6IpGi$~U$pRqO$%=Ww!@e;D*j|mm -w2rkm{XqZLSzdvp?%+SQ_4LH1_r*F3p*fDS=blYxX33^TqE{W6xK~$g(Y2|tRj#KM=z6BR-+NB6^}a= -*ARRY`5+wQ^j!#Ik^9ypHdZF(Rwr|D4@wfCVizq_z@bl$iM%A$lbH^W616$5x5mqWpNhBQ<+i3FYSf0 -cPsS3o{(UW$1Da>)1vqD8;4NJnfoF+PY7r{12~^UmwC@P57B|n3Wv&!JWrk3axFxI6tn ->C1+}la5~+!fJP0$i&~tu?G*W-B*sC&zb!>?VJiTQIX=l(ql}m0=c}qh%cx~2SKY;m@9b|VckMI&dPg -jyN$1GmLtY8(B -kP)>uXK3hJE~z`Yq)_O0G{xaSh|loxasj-(yo`Eajesx7c3COU~kd7v% -|hHqWhygHL=-ueeYP*U@p{fD`vKJ@NSi*=Q~t{)x{mu0 -6gYRjKsJ~i>@f*H{9;4T?uv=Aa1hezDZ*8(o{Wxh<=EdZ`7WeRNV0x!#vxRSF=Ps-AcBh+-JPVf|!$n -Nb-OYLtbugw2z3kOsG!l(Xa&nv0AcmFz=|MmZ94!<)qJB^1OgSIZhq4@xo4Qmc8`e!8jAGp@HF@V24@ -Ru$BWW1+@k}dzVg2GCvRx%c$8%ayZ4MDcj8oKpuBrDJQT*HdkJ*Mb*!@w0}Hr9phNhf+sG{t-KBC=1C -$Ls66B-IKyF|{(bpRqh-pQMlXEY*I+m9K4f$;6h}qIc&6B-{jTltHEEi{By|<;Gnv^Py_lw|DZy#3wyi+e-IQacd>foU;WDPcxT2tM?4EZvLV1ly-i -wi??&H63@_gBCvezNWhHHYnwxXy-2jVc7t?!O0N#;*d~v+|kchKu_Pap?Ni;+YW{j1K&cK$4lSPBrI9 -0T=HVdYQm4UkF@9d8|N2-l_lbqVf*;lPwWF@r~@WI~phWiZwe{|cm>39W#&#w7BsM{{^X7A`zJ9q`x-W(;RB$2vDiqL>%3GuGhxYk3P|gp=MY2qP?by04lX;;Idw&&2K -7{&4RQF~3F)sVuj#=*LKWbLhI-w68A7YoVi%`Q*5%sare%Toh^e~n4%$n(p -si{@{WN{h(v&YMXXxX)**ay&|Toe9_mZ@f4IhVHIzXa12+qP!Ol -`cga&aDKBVm6+Yg}WI~y!FiswJ4FJL9DL+d8@xv@=+YNdGp}1qS4 -k}zm0`82E3vY0_>pK=wE2o;fH*p%Z{h~V{R;w1ApoQpLky9bw}K!ihbk>tp6yY`!?R~ErBo2S2W+GS5 -_FW;bez7V)?@5&b+m2y9?9TW}Q843K*5J82k;*Ty_>e~?~IY~8g+>uX&K5SISarTx~*1 -fvGx#u~mG@pD90=kX8aWXwK#U3A=njP-q51C3yfP`2-{K5<(gZM^{M74hA8Y{Q0YOt3~K>zn8^!^b?~ -X8){L>!qB}VIKGt=hfO_O?`O2LleC7S1oWs@G!nSfii~cxv0hBR4u4R2&V-|u6M&t9o}G6wG^sJx6?Rt0{BzcDyiR$ -gE(l}E{A(E&vpnplbS<1CA7Mc!-@rQ<>#RGIU3cEDZgK9ki -BFP=@a2hPz9nW97Q&Vm|_4HzCo%IjwOf+%Dig~UN!Nnc9^%JAry<6h2$2_+NAi|VpJ&^l$p!IQ}@W+9 -cHGDbluVoeB>+F0+VfFfdtRGI4suxvquy1&5Or#OCE8xfVNyzD9WX(%adi^EQOEoOIM~-f7zVLDVSd$ -aI@b~Hry+&%~mP-K+PP|ws_@15ZeCUZ~1tbF0|ytfRI1tnSJG~m2Ar-@{;F^9TQ1i!zb2b>*6Td -g}LUhtVfl>WpXo*6Q-AgaotKL~iBjEk|n*GLCQZf2{`;LAaUa%Yw*ze2^nWIvU#gfN2iX7h{4e&uAb5?@X##~AlKQc -~q(6z9doi!S-nH-N-hR=+Du7C%h -{i-bU8RZ9W2}_jZeLC(GDglT7z|Xtc4|l@xELek(HnOx}5A=nT5CU3%}~V92d>O77UbO$2^ShO8Hbqs -=RZ+<%%y+p?p5UN8*z-Fmf+t7n@y+%_)%OKL3c1yJ%^npgJ5$GL_z0jED7=L?q~I$~|lS|fT^| -2qAyb1w3XRs`2=eU!fn4D;NRyJFike7$VK!tsINw2~eHebEtfWwtthLyhGR-pr0;x*`{R^{3|wN_+lv -b0+waHUoP$_5+>y+pKQ_YSjwJA2Wd8zxLho_x_dt&hod_1OJ`nZ>tCXNe}Lmj)bRuziB2^A9a6pRpSk -~&X67IRvB+Oe(2=*!4=Rg@}V4oMS8nDw_l5U=a*(q^gkJ+;n62=vJ4PF>PRk6H$!DTN7b@d3`iZFOJ!HIKIR{Fstmy -BZsBx5g$%7<{FPwJ56;xjT^OnJ^S1muT!@kbIX^C{t$Sf0_vPH%B}<}c!+k3-eNFDgoyAV;#RCzRg#U -7d$hbxK&|`fMYhogCRGCA~bq*WYSHKa?)Z!$gusr+qAUn|p!*OhFELW2}a2!1@uVr{e{EO!@k*T`hoE -BaRpUL=^$#?5(x(9xfheVGAyY5Mn^G|F>q5% -IB(ND7T#xh0{7=lXK*!VjNP92MPvt`^4K^&`u7lJflTRt2ir`4{jJVL?`;J0XVHlYI^xmyv3(Y{%qwq -S}&l7$=NTu$2g)86|YU)TPj6rYUBJd{lV?&q7l3E!u#Yg_~@)FTCLatq7guVqnir>@Ncq)Rc@`t870k@d44TxtBOkIdzx_+VYPFJ-eU -v-BACK$#hoDIMI|k`G4De%G-3vwq#L35_+7Wi=qhRB1dO?zTJ2zIjc`Q|typc}K-uK-C$QRvP#2n1T@%vBFa4ua^uffq}R^*)$<+7IlRV+L9LzJL@su%i=$M;LXj{NwAk7|r`*V0(ZElDUMw4HCJ_G~E61uu1$A*{hV^6>`Wm^qzC#k=qc9lM3IFf9(K=z;#gYPLAMEApuyEU7HP066b-%=y$*NfrQ(` -KodFfUFi*6Shg@hoe}bi`Sn4zzdOEIYmeW~H0sDN=q|=FY6}%o&R>rE}4lQX7N4*OKhec5+PrEfl4Ka -F;N45Euz?)?sE$for%{A0)1~Y??!JA9xSJIGBIE6V$T_i|RTtJ()UCeMPLjAV>kxmYv{(%G(!;tiBSkl5ZGrC -9JVVJqaEg}2F5lKNTu?iX)ljNX-Xu|59VqP6g3vI2tv+h`5#Tn!;?Yz!rkFi{leRTH6bP+61M}Fm&U$aF#@};3Ty -y&@eto!hCt^;0(0iAn%)LNWBE%)e(e+UFwKS^Y4c?{q$i45JdQUa%pgwG~avHP{Y_n@zOkI-q3g%+~5ek1h5sp4&8d6d`dEiPPwsTlvAE*CT_3kfJ -4DdwB{CJ~M8Q!SXG2dGWo&56&SVF}eYtM?#qdi#dk&k3Oi&GEz=v4G~;T`H;yc-yzb>-30mZy<8A;B0 -LS<15x&2e!y&qQ5*>!JXq2k>GLyhdH@X<-5Q?9w+_yuoP*%2Qmja-B-(P`5|2vGJvUVA -{P!4MW2(w#fUTb|ZDNQm-o=E^b!M0cPtIIHZxZep4SeDS4$jKjFD&PK$edI+q4L2f`ix2nC#V4c`sK0 -)Lu_&Gw{)tqa@lKj&-`Y=kWjKcSb5bsM&4YznzFxIB9p-=~HQA4U --cTkHR|tX;U=0W3qX4GeVW6wW=tA?Hqqig)L6FlzS|ppZ>_a-}Vgt^USy!*hiSwr7 -g{lY08sF;VjY@V0(7wzF4ZoCumcpD!}KyZVJ_>OGfEWN}#)osHID}>z%2M|u4L5t$MWSk4Z>LR*7^5F -AsSk`utaBjr?=?!_4)QpTB@bYMoyOOT9+JoS -q{fowFCnQ(tiC=8<)E6<>e~;en%NM%+Ijv{&lFgujrrB1s^|Q?f=8j7(!wg`eVxBAD{ezRX;iLyO!!t -g&WAd>lpp_m;C<9$I0C+c4g=Uv#TsbTfH2|w{OtTO9MEwp#!xm^d)=q*!oX=cWWi+O}%GDrg$HcT`?{ -ucMDo{>uYA1pFj~4Zt08l2$a}z5Y+B66*1dD3xn?>d3%?2oNQ)NTQ!2-?iZ0;1NPP=5O1Kjb%^c#11l -!M={CHA><@_3zf30h+cpay|0oUoh1TwMcJl|+F`Y$7)_uJD=)!WX#v|gRxVe#V?hF$quTLv|SoA8MWS -&jGHbd(otRU%yD>kP>z(S|}8@>Dyf`m=*ZZ;|w*Oz?Mk&iLk?_L`Lr}`d(oEtq!4jX;Uwur(Xce6abH -=TUNw(FuVGPcdR0Dl^EIV6{Qyl!qyg3;}N^YZs_RQ@!t{)0b~kx7?##&OIU17W>jP7V^L&YNE(NRS-ST7WF3LUcL=%J0ItteP&GsX`CTF0UthSx>)AcwCL%C3?2^*0w5}SwBx~R5$hNk`BA`;2m* -!s(Q8;mdvWn)MNN@=D9Sk-H2qB5HFpgSZk}}%iKR6ls9DQtAh>v*y23Ecw=h5#Ry4T{(vsECoYZ0bt#e^3r7aqrP-~Uq400C0p3(?aogL>Ct4ShahCDDQT94V;k`>>Nu}5U -oPE1*EbFb+t?V^4_Wr;@Z(b54|N)xQoJD56T)**Qi3B~J;#^CvYv<|kDO7)X3FK|EIyF9ZUMK$g!2Q6 -`|Nb}Fg+y$i{LcK!Ky0l#Zwoa=Q8W;6T$Uko2ez3y)p5AzJ~#$xS>`8;<;$J+Y>_k8F3xzA?N3^0K+@cM~1{7D_{6Y!@^%Tmpx}Dur5Lu0294Nvz7@>QOAho6 -^h1lYK-RkNj;y^8A^*4Um+g_CpS8g!+n&LM9{>EYNr!Uq8-4Z@DSVNdos=bLBYBZrZ5D*=5$&k7Bab5 -k;b-ftMijU669);J>jYA&%Ags$Jd1QfLRY?Z4d$u3-Y6rIw^@5hl>0&!@38$z0a*jB^bd;2+2=WsGC7 -Jo`{gMd?sJ%`2zs6u8fnK=@#Vprfi`l*qFy@JA6TJsvyWVSJbkI+vH7lo_xD$0uA-8+BlE-aSJjEP~( -8Q98h9LL$oK0NT`ouZ`I(nDMsj3zpmOf;hUQd#qCRxxp^dFh;Gu>nq_qY2Nd9C`=H5OYh54mXbyGAfP -v5rD&mprTPo#Bo};#YAx%y|pxNiOOd;P)%T#Z&o?*8GB-)l;lEC=>9wwc?h9YP+^p{S5QS9rrVDM%Ak -TI5o-zrRgkmFNfUdw}=yE_j*X3YX*aB%(R`NS*`vc$9tk=jB-Gr)ZctSpgGX5GV{YLc#RwLc`9&2!<= -ysA+eo^V5@0HSB;s5~Ah`b?&XmNl3Z_|mD9}7PKu8?M;}KS#jHldQvvUiNA)~3c(*j%(B=k-BjZfI^l|1jSqP5-XXIdm_vR!Tst+qqat)CDLx5NORY+tEpY -k9=?p|dsi+B^%;U5yyu?!9t&Jl+(D*MElTF2xw_?wsfrWkq}U#{W-(rN8j?KLtxwj+o*P!P1x4{?~&g -{XYzr)Ju4Hy1r|g9$k}3QVB=Bc}4V@LkIQ{_MVI`_F3u_`U(Wv95V0LOOw*N-o{d6aQu`+`OCb;n$hD -zdN)H+@Zv68bUeE~L|D6++=ZJTBNJl+U|{jXIhSXmg*^9-OxlG72|5zW;WVo38%~~OQR0c$RSyT!Vo+ -I9Qy$ERGq>-n)-gc8G(4Y+(%pk?hv|8+P75l#C+|X(l)7{7YM+RY*Jw=BrExD%sa(-F^;S8oe;4TK0> -HfTLTaSrPsb&ynlfC3!L-;$G;n-0#~^rIZBjvIu`q6Kmp+YYVe*Ra3ocI>m52cdV;$BUCdM&Hn+ZIS^ -$i=nk4&ULjCyn@QCu9q(`d(%&?zPmW_+aR8)+bY7+v}Ts0F_)dix+DqK!t7q!&J|eX5spl`(wLilERTd2i0=a-5=U$9Ejia>Qz7A;x`skY<%LcNV)aB -14{0j2$T!|LU?Q${9$ha$74Ud8@8^nE_}ZE;Ep1$zM?>NK17&KBs~?zOUaSMBV91eHTqR~^4&wm~pF~ -}ZjxcJs!TZEsp6gCdJMR&j+H!dk8D%KTf|X?e=DIM5LomCFbU$VO@I3qZ2~Zh+LW^bHCo9$Vm3}t3>e -?eYkCd;9Jkng~-5(ttC9Nt1ny8ze#_C!-Rzi2^v=IB}!IF^TO9s^uNa5Wuz5yqv2qik0r{kOdr@<0?U -@O3KT;ZlG?DfvS@ygSY1?=$3wgy#vC+~+ZzRt2SFVCB<&8JhYHcZJ8Q6HhD+pXa10kLh(OqXkPJH0@~ -?7BQVTnH#y^iqAb#47?tMbO6y!4K3xSJy{Q+&u7AGO_y9&lekzk8KAXWKPw=;~=x?D<02JXPL#;keY0 -<^MfSJ3@63Hv6vrva5zjoT_ac{KU{&aP(V_&k+7~9rbhRhvaTMP+9MnuEm+A;xgfd7ISDQpRyJmFug{ -@0*SvVh4uu6GrPPIhN!Xa=ADTW`8r82a{&R5Bn?4@L)8*O~?DN&qfq7ODLUh~ItjbL>M5f~3TbI~Aa -4~=SP|71k*as&DB6)Nja;-QyVu#7&(5dbp<{Spni8#r24M6FFsz=hKIc -+4{MF3LFREJYwgVg1I5&-p&rwWQOxPZ^Cvz{%|)hfh<@zJgPK*G-?u$AT${^Kte^zPZdg2Cuv5Wu{{n -C*rv_#+5Y(C7h`PUCeMmaez_ruzo{aMFgrp&e|G}){|8defwbS@tUp&LVge_lV7<2vc$H0cr^m}yBrG -I!Z9Pg2|mpr~5^;KCq-46b7obiA4PT%S)e{+{#4m-x-p38x^OclO+Co$;OzJlxy=XAOa^2hkzE4s#Lp -DT`T7EI)3H;(PiubTi{yvvkg+eG8$G7fL)CnovYzk(qDPO&X6OOT($Vp*~kKGqm7!FH=9d~4Fl_QDFb -i*LjG4K{TYauZ8|cQ?d@*~RP9t+qm?Tis{9F}2H+ZpD{m_dj3byIk5X_!X{r8Ogdfs{^V~m(_8CEaFPEs-=XW0X!Y -LF>jKZHx1QFo*16M1F!vEg{YeU7>*r)=({a%+!;$(|ot3SLWuK1x{P}+8@qoX3zTbH~;P0OA?;h`Cni -BZcI3;Txm?Suj*HU>aDwU^A*kutGD29*s&|(0zpyec1%3e)I_u5JvYgU>5bc#=ByH84oGmT@>h@4%%N -41q$WC_#6<_kYUz*(D}lYeZT4mRFzHdm0`JWI52e55r(TwUYITlTu;C0Q4Sm^pzu`8?Rm>@pW# -6l@$QUKg~SI|6;_i)S=sT(p>6xN)h7ZH7kmh`!kF;4By(h~)_mGR&8x37n4Ob5X>B0D_e5JiW+a^m;j -glJ4=waD{4A-FWbFvLEBl}LCOt^ -JL<=#(Ii(EX2fp9{A0O4a^@E#$IFu#64X( -}r_v#aXP9TPKAS@B5-b0nd&*4}6DwSHNP@uzOe2L#aP7{0^8CCG0tf!BI^6|DD}xUat>Fgw4oOXR)B{ -uJEaSHO|+sk*_Qgrz4s6lN>Cpk|_nS62_yft}o@dPxBlK{*nDZ7Y#j)S=fvL^8u?&%6&n!h?Tc&0Es- -WVM2>rtr&;Y8|M680e_UawUuZwkpMzU&KkrFdsmB9v -G#Oh(W02A*2#eY!R^kWfjX`GK%$*G9cSud0IsVWelYhVG2aq{YgdVv?h3Pd1G1NoZn$hPbG+&Z9^ev; -)jeV>@2AoPVMP#&JKj;2+45p}?E_^EB! -!IcD$QyyzT$RR665XV{*j2)*5K2l=D$CfzS5%71(4dkN&76!*c4+ -HLD5G8VJ8HK6c2V(DnK6qrm*8O`+0VXa8yQw#{=Ro%St)=0MzL_pz8LO#h#)zYBQs(&(=c`v3dN*Px@ -1o0R3E)z#HZdf;8Fe>$dXo0s4pnxW?u?_Iea2MDSSe|3g5uaZlyqeXGSxjXfIug^Iujd7{SpA=U7u`r -$_~k!Zt{B4O-wQSIf%u9eHxW4EK{V+T)G`Tp0*GfgS!?Wk-36f;_t&B9c(4ixd{h7g?B?;*CHS|yd+IDlGS<;177|HqM3KtVn(0x2;hIpABmAyrzBo>zMg?i4+b*j>UEFPAm-sK -+^0r4-r?<}u(NQv48rltV`gRNe_79DM`=XF?kBz8X75O|St30vKkmr=xZ{QXvg4Qis{g370RAYnc*B@ -NtFOEkM-XzKXMCIp{@$lT@1GDZkbpQPqe!rN!AlSg0V#QnMVHyHHhlQc9PS5oO^N5!+ugB -bR6D?M2H0~hAdMefnyS4^q;eglGikvwJVB+1g|VTj3w -f-5Ce;z)27AN>0$&HituVLi-${V6s*IwJqI10S_R>%PM$?mXhrpK$q~9{UZ6|NC)&?&Uym@R!AZhg`s -?umS%lfTP6GGf91v>yVEx#j!T<5xqg;@75MS2d?DD|0q0EG?7oWAA%g=F!?!p2RA4@!bX@n;?VTtd5E -Ql@>~l324~R^W(Vk}?=m_pF7~h)hd;#}AI&-p{RkIepPml>hq&IS!} -PFgg8m)-jb_XPXP56d%U(Qf`&E6j!ht_wkii3rVa?q9O)Ycbtg~5^V0n0y*{^eAx#e&xk?q0dx|a2+Q9H5(BP_>(YGq3 --d`&agLSbNhh&OkuX@;lv0A($gV(N5;@YSd3Jhb~mEDhm$F`14R?vK2GTP)SNMtwFSO{6`_0M>{0j9x -hS<~=E0WLcDj_syqotL-W*bq9qB5>>mq5&p6<7g2xppu!Ory%zqk2!jID{L8M@PH*1gxqFO_5}4Bc#- -^fiLN5rQ!Vy(U{cF@iQ0)l7E5VhS|Q6+w#`9dORu7#3ABi(U@$B*tO8^1i2o*c4Lsa)DeVILpLN9 --{A^j2GH?9<;(aqD9~@^W7Q}?3^i8R5X7Z*yWzCy|=0ostMe0^h+-c_@%rsWv-&ET*a$$73imkXX(|) -L&3-+_W71!ql+551Y&_&lLuDG?w7kwZm>GOP^?)(#~t{vZPk*@>-^;=vsUG64&vCRtUKG;H!zA;d!fKYmYw({vI}agTIE25F(`M-+M4xq>VV>+s(3~8MMH-h^3Wd4Y*2nlu0c;u_!{n**^IL$(&D~GzZc_UBEj+L?YW}mm>j$7dx$wqbk)1K64<64%_Rcd>S1 -=FT<8-E%0zu-$DJg*udz~Y$??=b(tfLis)T3L$DqYK3$9fYgmS*1H(kKn#C0j^Iy$W -SAV6*$G369OpgMiO?f}J{ktl1MT-Hd4DPXAUXNhh^-%pz5WTY!CxXaPychozLoUz8#Z38zk%3>?fnL| -tyyLLpHSPHfn(#ZhW2+y2K;VlzaQB@@$Qw{C1;I!Do%)UyWHRI%xwf#yrq`Py7ZA`T?^D@CQ&(auOHI -ZcssB>kd{ejKU!}{*=6*bovHrD4=2jUBx@Q;WvSq2cDjX`c8L`bKuittMls)vwj=UfGx=gdbHZWy!FI -+BpA_Bm8KP8(8&tUQot4jiEevK2laD><@qw3aDtxkJ0UM4NaYCr@WD<_IWauVD=0h~|^wp!2Q5uV*2W -X)}C_?6l4?aLuhHC@p2(0OzPyE&+qR@*uuU$^}9L6?iT7owY4wjca-$Qao*ms5HR4Lvaa&X_ddl_$!k -Oag`KZ!TiW6AN6!4gS4N3-@XosG*cPj|hOg>cfjoB9O7AV@?4{0HQI==0Guk^QZeYV~dv)lzWl)0cZqv#?I*U$)rD=?-9G$c0u -J2&z2Pwp67Cpiw}(~S$$)b$Q6{vLracqu5%&4n>i}e87fA7{OVTnMWQ8Nk`di+gm|AG~KpzQ0lHV)+l -&qbqls=-~^9q4(3m3&(GJqJ+4!5xsxty%KzNbjiteyApG;03ZU@$MtnNOU}VJRyQcu{v~_lFA@Ru{I!3p*rf$fM-@Ri8cV1E)l@1%wn8 -0o_n_NIrJ_~F|A$#=yGk{i=dYGOALvn_6lER++wvBb1+kQjEwj%7r5E;`~BpZ$=l9&3uR14sL+O(@=L -Mbx3ZBY`#n=DO0VLpUBX4?AJE@dpQr!D7DJJJ0aF7$Q1|mZ_#ean_*p~#FZTVe1pm{1f68Qo7y%;~h= -VXfAqYak&@b14Vf;u*@3HRzpq~UYM10IAu_I(XoWVZ1WALNg2mV?pa1U&8=*UVZA4R@6IbvJ*(=&&E1 -pN-3BkTxf(NAUG0sO?VYD@fX9XNrH=p79{USsj62oL-yE@2<^hdz?J-~soCARu)#0>(Rc2#;*_5nX?L -k8$K!m<~QUYbyRYB$J2Y1NfI#7<_Pm4fMSxPokyUKKo8{G4SQDLHha~34t%V@T#mB%FKO9UyIz_U805 -u_o-kH7ySH(*KYf3+`hf@S0sOILZl9c@<$j0-n%1U#snOeGkUw -vnt)G^2EpS-4jW)iD9hjF&T93&p+UJ+4^IZ>{orz{ -2%+;0pc$txV0;rL+*e=eT@KLbtER%9IioHh6VYlFn%u!1fF5PCQ0 -OUB_5~z2k4ErSc_SHx42C4{X&LhN~B3Onw)QHzR -k_rQI1r~U1`dC>yVtZl<-$3IYx)KKCN0r`ZDDRO09W>Q^j^(Mkld43W(%RKP^x*~}6DELeW+482lVFl -@yugjRiw_GD-&csYP7wYOn1OSeQdlDsAD4U9$dPd)QpYA2vdz;ypkLinUZIs-drHE=32oe(8y+#fgOO -eibGdqc&3jbeJzjQ3swSr|o2XB#(XobJ_IE%RaJx^>LVbg#8hzYT=advrT75XshPKl~9JFmtwO-EbH* -CB$74f)LHUf-E+LeM}K%{6tp0a@nHK%1wuO62?$2cD2^s7RO!KCLkbi-FQL;9&KaQ5zm?KG+W2>TlqN -PwPtT=jf*HwWtP`PRlRc!_jko9_v=E^XX8mvwIZZ0tFX7}_T9o4~IwPizs#%(7UZIQy-Wlnj##6cU|i -x}s`b%p(J0ug%FzGxuE?w>-@KzCh~UUd|P62)>N5l9k?#0cd6At{F)&l==f|@%6xP#8N-h#EikiP$wpUOdMfS>B`{v^dT`YqZM7mG -@i49Z^PR$)%J2lM&_{s+_jYA?OdNEdGpCE3om-q=XOO^d%H6v)yaJ3D{Z -X{gxKDLRE4w$WUlR$%6d@|Qx7*Xu=G{ZjUUyg&o(%$@TD44xWlHocYQ0wq2+z*!#)5!N+a3&&f9dx_3 -pX_oSuVEY$vta*{50x?K@FJvxU5OGyr&Fhsv;LA`2~*qkHXG^GfLmH)@??z5<{AJpSN;pM3Ea2^3KO}9qgwc04qVvOwQSV~y;W)v*>wa>si-42WU5JH;yI;Zf-ogk9!0RhXjXph8(XRpjx%folv9L{m@)RXhex;;pkF6xtR? -axEA4e7E)sP5v5#tAu#$bA4d>G`BwsaT;-66@k39o?G^ -EY^u!02y5eu1DVbr6g}ok`B45`Gv_@OWTP>WfLcV97J;T&&U=sBV@d^`jMwyr$+MMSD?CFY=ge~hcM* -v_$Y?cZr9`C&k9rjb_$`#TT4N$}_cO+ -M}f@rUAs$-%84>gZkAsn*VWV&Y>O{^`1a;e+Sx1PJ;J8~)+~^wF1!p+oiRXnNR(@|W~T1h>nD3BTtqu -I_M0x=KBFclwvc+nhvtDk$o?O4{K$l|u`i*FNtltm|nHrmRF>YGgjA%OAX3V=wryBfwA~U6Io*p2Mg$5 -e|$#EI%YD_s`OyFL01j03Eo$F;vIVO5O<}@DFOhHHxK1(O{1+!22VR+6H5a8meB_(JGEA@>W%N -06TsoO(g%WKv@aWiQJZe1=qSlTVgnQ`UjOi{0DM+3E}^;?Fy8hvN(9e8`%F&hMn8@i32+#P6S-`<;B) -!R*IpLl}P6SgQGXfWSj}Yl -B14A7&{9^?<`m%0u`PL#q+t -i8YN_O259G48G?U%~(Xt;1a#i5vj1mBP<7x&h`qit!^IT||e5UKh-H2 -(C0~wABMOg-rU2x?L-{mR7RQ?RzZLdK#9MY6K&>)V1$=yTF>l2u|s*|_ml!fpKx;-CQ#UW{ljn`3sf> -=?c7g5q)tUd_hX|%@MqYHq@?W$nhL0VAMeL|Bc4@|3TweshD(~0uoba%mv!#vjeSK|0^!BGo7>ot^hL -DFe8Fj<4E)2C(ZTL`Jn5h~w@`?5zLSL3F`xPMjXF4BA2|HgRs+j%`wmqRqCx_1S>FZt^n7n#gk;eMzk -*yvv*JowT>yvcW!x8KwQ;5&R3I`t%jhtz4_#SOK|KQ4+m#yeW6)pKgxbJQ{}xyeH$P+`c=g@CVDV)9) -5ex(prx9Ec99<0K0V{d=1FEk%HVV1}iwH4&TfIYn!%nX&1LCo~JMY&8s(YylbhtCMI -B~zJou-S{+tJEKk&7KNWphb;_9iad60_JKjDA@CB0@w)mhw7F7=W$!Q9}A2k)%%kcuw;1}AUX#9LXn%i~w4l>hnyF~PI{vc9Um(gD@Uuh4bhMz*p1w29{W&=^6Mdm+E87pegLIP7*>M72KmN}9e -*0gJqXB*1&%}HC8R_S-!JnC$7Es!oj}vk~UeP-KddGFMMxoeL<-6DEOYZO2v4;EQR^~X4-5IXFd=Auw@4vv`73u7-E=3F7o!hgew -vQe4uuCHn*8KFxi>Ah%>V@&G24M)s*o=9g_oZ!hvWq)_B|y%gniEV|9o)VB{?+(UtdNSNLHql9p6Ng} -H1o!HHmOicQQ#SJx}@Ia0fq#bS%9=&I-fJo^5{daW>LNUW_(zx7t(>`=tL#AlE(j(b6M1tJC}$=-lxif9+5jOnUB*V)k7QWw6q8ka?|s)t(yTDTYPLGeZFQIBer~Xepl$F^E -^bqTKz?nnS4$<5mwn4ZS|xW;oL!nix%JT(u4~~1c)TJ4GzBUwBVFW9olQr-L9%TR4O&}G<3b0GcE^TP ->7Rcdto|}$=;a*H5kenGL{2ZeCxW$L*cxO<-BgiWVMeu-i+=9F5F{Hgdm(y+s)^Dud$w4@kCOjH#e~# -mOu;}4dl>5!^3K8kejJdg61*~x)FqDGrtngv~d@B8a-E##&a|KeJ5s;*siam4<+1A3lGG|$&;|>>OT5 -1zY}~Tc<;KP_g_)TN>nnRQo>IoL~l`2=K0)c*ONSN*@#Sq20dXB=rr5Wm%x{`M`iaqI0+oGMv|8?Q&! -_5>3B%TrJ=!sqpfn@t3W;CpZS)3X`A_>n)vTJmHpiV|Kv^f=Y#&Fga+fk?3YeII`Kqu*jpxtdFG+aLL -gsTlqvAw@`rh6dUUr?zgakqjBnUgiw}FxC_dJ5N#Y1G20y-*w|DgYiBK}Yb9DNfOJNjk-n~#rt?AJb&`G=C ->uYpKO!%7*=YI#MKP5m(tSya=)fKMeb&;8^gYV7mXb>_aAh|aaO2PLjxmJ)3)w4W1zF|un -VoZjA3SdoWXD(?V|&v+SHYuqc1;bj(_4DU0fm`gPpN455Sh+K1(Wxip`&Mf9X(wFYlWZ?hV<;YA^X>h -@7S{d3n8L)D(H0kye|UVHHXB09GLeh3^vpy1mP9An;-@<@<=LsG&kqzh!B@Xe7GPK7ZUsZkzQ`$KJ6D -_1fclsSvk*i?s&f7x4Mz@r86vlbr*<>mO19gu~}fl&>-h_V^0(6zgDe_E>IaQ)d&OYZ75(@(Yn+K=x2 -2{F|^1gyoh<_rP)dw7|>9b*TJXh#)CQ7e5t~?QPi-)L+D=$pbq5wM!hcB9D#x#cGE?8-CVr6%lQ3fJ# -)XU(t*4~7bi`iQ5TK3@`5L?P+?ZiLQeyp^toFl`}~F+gK>LJGh8~PP-!PmEVSw2#$9SsE<&Gjyvm%~T -r6zqWds*3>tlImYz(`l6Z5#^>ZvQ8rL9UOp5^pGNb>caHD~!!@!>4>?Q|mF>x*>h>YSH)?M$+d*-j>wE<9*H&RoM*!gR8vMTe -{{B>pYk1BKDs;GhU_WOQlcY84LbaKSlt;e{!c&P5o0iH#?&{E(cs!=8b(3(+&EBdceW$;`N!^_USgyk -Z*cpBJ4rjMrr9D6R6Vz~_2=YF40ol|j*{(5w;p#|i;Q?u~IK_Z>>R(d*HuUI*;>l7LM<-*7&)}WQ-9fpe^czB6-7O -&Iz1NtL!?64i+YM_Oaz@#2H^_wYH$ml_V=j9Ce8n5!fgtWxPkalLHW@2L&rH*H$n3m(#$E)S1Giq$ji -|RHLl2F?kmh9N>3Y>Q{}XvKzUF=0*=<-sCs{z8NLA-%E8WjKt|2Wqf|ctqH{&XTZ~y9z*wPq|Gq7-O -v`CRwwH&Gy0V!_XkgG{(KaF{nkljJS>v9lD?ij{eNX!Ht6BFzJk%NGA5?&wNnHewXPflFcPjHfQmH?@ -@H6}Qdxw1IJ-^)jPs`L41)>mz;{*zU6bOL?g`@i>m?U6`AV{19Aq@Maj{o8O{mEZKpX?@vAIsJ-{*}Q -*@S)Z6wZ$3xH~MQe85B7J9gyXB=wi<=zgRW*2 -_0`WYCgx;$JKxSK*Ru%_VxjE;F3>4b&UN{lkzCE$ZdS5LTA4G@p>^RdvGsH$hajYh&b(G=Ja`?pQFPANi44)Ntla-MI52UE+` -#7E&)C)0$zmmnsDM&^7YxbFJ)M*SRED#^wn>4 -+9nNRY=@?_}C(sxb=^)AX=Q+;kx5l}nf1u5UsW)1vpGw0kD3t$ynd(>9v=UFqHBYtNYW>jrCcE};sgW -eqB^m&2-Ve`==}f67emhh|LRPj!z -1P@K9X8s4rtq~jUP#c$srP}N-8{>;|CBtIz}%C<&=%n~s5E{hsjt@PCC!t=Fp-s8fw%?mv8bM{+Fr0E -f5=lDrC^y+me8^Tnuo(pa~IPuHbTG&-)b2Zd@U?8e8BE{afSO!i=BoiM)7e3{-cf3RyEE?-yRzSgW!< -L(VI?-qPgA5-F-dTL>-o1ALk^Qkh|b*Gq_wGz+vZc0Phv#Qq=@*b=xCjxle?>=^s?sCNiWnHj%uDi}_ -$pN|OITC2#dL#4!r=oetq@vfG4VuNm7&b44Uq<2pBT1^&d%6p^#(TsZ!nub5o;u|Yga{g9);K8z2BM0`|S$l&lT+&LN& -95jufcEb1TmOkiMSU(&P$%7N_^a)RnL=*L?)5oHZigA1t6Yf0fXw3UG&|^E3`<;mLF_QG4gz%?NF#c3 -5?ql1}xNpbeW05`jbQuO80vCaw1_$)WWbNFPL=HcKB=|HYQXe}h?9>rLyL6e -Odxmv_+A!_kj^75)0kcNXUq>(~qII0vZa*yT`%=a}0_4+oukI)7e -Yfj=Zfcip0|NZdaQ@UB_3oe(@3qq8eHsVxo&E;15JEb3yWG -04Y9BnHQ+K+530DWlH@qffzUvS}DRQbdp4*6P^*Z4WNMBe%cW2o#w?texIJ~oB`v!gP3A~_F#?{w3TA -F)#+L;h8vlH0Qcuv}{J6dlyLpU)*K@iGaC!T{Om%PkxmZ;^iFC#2aLFlvF$d>fb1YA3v2~-=7-tFDi= -tja7Te!(m&myS@k(d%BekIvim9|4&sQx<=f%Jmzu?ih&)W}MTbIz+68;~lZpdRE-%Wz`Pg=3s-qPm`@ -0DdY@TdKkE^wQ-b7DEeUZNg%^;?NhepT|Xjn-e?sCOcNKEErAew8m?xR-*586pX6@+jFmrJiCYZyK -r&?;*Qx81zVStvlVM#Yx@P@=UYj;rsg7+MKWR*LWrt#5&1a3-o*!jXNg0ynDApy@>2FyqsALxiPtae{ -~R?-8?+aSo4(i)cpmv#M=tk#(Io)m0K=gkrU!)SYv7>7F(;1Ez-Pz*6&TFE<_0vk_1YNwR3hHkkDkSydaf^JOaC$m -hn3A8%yL)?ifH1om&H1-I0@Q4T=DQP%j -1v61JfT1KoyaM}zOr?AAEc360Ruf;(7T}g0E -^48tWlxtw!;|Lu`DEUb&{YeHJ?fDQ_)|+pUl1GkCvBOvw!6M7pk=X0i(^wGONSv_xLuLBvI -*!!MC>!=Blz#!gak1`!t`@vY4LyqeyTK7p_%%_c^!-041dP+G%d#{w&=gJe;1c!Pkq>C6O37@QEC$j$ -9xk&^mu#oUNR>Iz3Hhq%NB+wHZ3r~MlkI-N`qeuVCCHDe1$p$*9(q#5A?~z4J(ic_ -qo@@7xS5h4QN<`e8h}vjSgH-9j{(U?`x-<)$=8p@1-!Fc`2W8Gi`Sn877u4D@c%Oci`Rc7u=sMRXXu{ -ttxDX$)gA>_r8NoXwntkrK))Va1GSs@e7)5hLb8$UKbUq3yt8!G=KPgKJ`pleFSw$60xzjJlCtr3roN -%)ytn}T)5QIrzAm+1W{IgzR&}N-YEr@Md8g0(dy1ct24XdcJQl1ltXt0+vQnmIE#kIR08oB2-9B3C>d -K#xpcP*lG{3hl8SLTZ@)|yj=v%nF7mC;+Xga0ky$-tvOR^?@4c;%HY;T!{n~B1|-+2YSYI|gjTc-2IR -?~LMH=5WmMPa-OmLDOdv8?-gSp?g;i!_UbJ}_S)GE3DplCbwVf8Al5gqB`J< -!m}pP0zA`Gb$2*d@kZpY3)h>P9sp`>nr!21n@I>!azR>6kFuj-jdh)hrWAI?}iv$O`D{6RtB8NA$ZzW -qnyjo4)>XY$`rs#@z!OCZm4*Xm6?HRao&m+P7Ixh~5g}s}Mc^JDhpA@y2R+pz@sK{o~#K`kvQlJJ3Mh -4PB4S0Rqwh-b#2Dz_Pxpsx|?m|MlqnX345Au{;?IIlNbawMH=1cm)WJ+VUeW%R9=+;4LI1$X~DnPYQ( -I%rJo2^lkhFU5?Z@_CdRm$rNerW9Ler}m5R@8R@GS&1dx8=0mcM@l+y-{lb^GNO-&QJ5-Hg7!I9>(Wa -%u3=9Ccz-|X@S_c|J)(*-*^4qBJr!;{?smkqX2Q?NSOL05 -m?QpkaQy3oAOZ#K&*A+`gy5*BBK|aY6Fgp4bj^@pea^hjDKpZP`D1Kbw*a80-Cr1Bx -qmkK=<+%V>xkW5bXG;;2ur1c`rj?%v^!{76+GpE9On8SkScd|)+ss2->20pr1r?dV^EUVMOFefpzag4 -JZNSkhtpk9Dd~CP!nrL}BLd?Gm=`9wpFUCvw5AzN~m1o$1{2bDKmgP}`5fi2yfC`=GwJNg&6XoXT3Lv -N1naC;cH)$j*yc($RNqCEnt}5K*$Pri!)ywXjGj51{+0J>nSY&)(WQxcyRE@HgmT=jP;w>ywbnx7NLt -4p8?Wr*NB3udzkVfIbOTg1x0aCHG)VtI}uEg%|0HLe#BN&R3ZVBYyRYgu2(?z6UJr{Q3;@E|ai<96LP-;r>JPiu+2V^My+O8OPSw-fO2`Fsb*6Pqb73Vvn?eYIH|wS)IoW`h -7NK2%+twRHSUl*lNoi!qNye$dY%qI#7Mpb|0I0f$^)H<6*O!9ZY8lT8;SrUIsYNqFgE~_(Q6kOcBOmBAZLv_d%Ci^1J -bftA;$F1YOmp`oov@pxM!tM9|FK|ovQ#=j)Bx}&bn;F(zA>1kd=WmbCA4)3?Pt*}TI*0Ow?ts9x9ZvRnA-TyJ<7;#d~}O(60Ym7uV(x1qY@S>=Juy=4)EZk -%V-9#a>B$$-M4S4;9<@iLd%Ry_M9!$Ld2EKoN&9(jU<>hB!SxiFwhCGd%%t%Ss2uT3)D#7oOj_{j)5R -SuU35s-#CPpPUIlKcj4edKAc$cOcj8qAo{`wQ+d$a>ByW6&;Qf+z9ch9Z4>+Cf|l-^)9oe+h@g{rBx( --GYCDrGIePH;DS>A%E`ZBS7@19>HN8$0-;C2?U1;2t)`R#4!xTVH6}t0tP{3pDKR260y(c;m0fyNsj^ -!rg`@eu)>;QhET;=i -jYIpQ$DAlPTVi3sP}G#c9lF19KAZQ!H4GiuTT -9T#@f0qs6BSR-HxzjR%4srP=E32|W)vzMQk}B@isgHujYPOD6$(wVy4rZZ!2V(3u$Q@0fVelF;LNgr4 -)w5d(zEaGed^c_TOc6!{7k-S_GDrSkjF1;9x+on}0muds-s8JasOt_?|#GWUov3U9+SXz*{~`J$N{<# -H0(W^K8Km)kxCKAt&%YBTn#wIFqOEYE85&4WCnobt!>B*)LI`b^8mvKWYi(J+Gwp_YZXRb%B6c=Y+@c -7T<5AWFFPu&RCbEY;(s&Jsi7UpvjDB)aZINjzv%FU^xzfGE2?x!aZgfrUUkE$N|0&ZHG -_Je694MV*@mDiih%l+ed)LZ5GuNIuDS8|06&3xL#OY+NC9RbMTV%22_ZO&d0U+_cmm1-vJr91(@bN~6 -|@#(T^q?4Uq*VY@?fYZgqG>yw$FQl^o+@{c_{MY6%=v%o~R{?qyqo0)oog<=T$KK943V%tT)yrrmGJj -Q-bGa3i_sEvjeT+b!hW-)?GMHGpQbB34(zwP|se;-*8^7g?YqNX6O#}!}k?NT^l02-FM8?mp@}J6tUv0YgS%@id2XRTQ?L`+!#dSPuE~z{gj)@`m_L_v=g$&C! -x~%MyKzEkHH}CgRf88&DVg}bOX^Dk?IFIYVEGWoq(6BMx{++gZY@QHi>>H4EM8!i0S8fxf&ga(^&imY -sVIX1^^E~ldSEz`fJ%jRIP=3^E+S!zNYU>wbqnQgj8U@w)gA$f!V$eqIR?8H8DJ&Iuxx+=gwfY?*<@w -6tnq%f~JQ~d&QWrJrmg4GxiZevn?V{mN{JBTH^(Mv_L1Sa%fdSqn8y&dJ_*dOzg=$dAXu_-zCZ`5zt# -@BAp?0!Cw5Y1NJRu9dv|zkE7lDIXTbSQK2)h`(A;UFU5=wi>Ys1vG$}hfVV%_V`D4VxM>7`0|>fU=wj -+BN}Cb{2B$QsvA7$9>E>`%0`z}zd;(yR?|BCnp$a3!WuY}Ja@*Z~P~DrJUN7xYty>r}U_8<+<3a;^aN -!I)%i?mSpvyG$-5b3ea%9o_b>TQzi96qI_LL`?AX7z!kPvs^ -I@QR`vVCe#@%hpI8+_fe?m+#9@3uVLPiLP!#{90H~d2?b~;{1;RK9qXdloMnoa_`1X*Wl8o?64m&;)1 -3QP>V|)ny*fs{ogM(Q8Ix7HDNAnZ@X&xfO<6j&dnQ9C>VsZjI#0x<9tJrb^9qH&4`*%eC8^M3as?v`H -b9nHv!*wz^3ObGw5#rEG06%Wq;m6zPFr*}oO!t26{k;2ycaj*#NBJ(89EBz5r(f6Tui&@q6AG4~wFPxP&%E(5uvoXD~{v` -KTL@b(-&;5c=f!B*{t;C*$CA4H0(g$8e-fo6_cd6RLLo)B)j_}0C^FrDw<=A2_%UJ&47)4CgUU7gH?a -MWuyq!#W$`U<|_u8&0`xsSQy||KeN^c^Hw?P{4R6tyDDX2%QdJHESn@-f)01+nWUf{9(g{RL%Zt*WfdvU{I)vSphFNrS-@is -%v1qetQPwQ~Vk6^Bn#v!h3o&uGFX`-$RgK2Gg3Eex=ypp`GV8$hOpFs<1q@XjlDsM -eXo(UJkEJk6_m3g4L}6t*4%dpd#)i$J4wr(hU$?aLvtw_Gcs@ry$&17^Ss;K1sWZ|YQ0DPovdc9rmq( -uW>IFaXBYaPLRCbXyp!g6_Vq=+lVOt((<*eAXyL#=KvlAs;7K&@AgV$(@UA=i*3(6ht~J9DO8I-KZ=X^yo -pMioJN>PuwI^BE;>yeU=6y;HC79d7UGN~;EV34!}L2rhn=x -_*v0#)NwGBYl8ZYQbQMBe^xDrP`JCcj>3n>O5&N9gL{Lifa`Tnt^se^M+cDn~ELEEkd)1Ok>Kl*!%oI -D+KCMW0Y^n{b`HLb@%_+TlCJdxZbYC<&udHnFmhpY4RJVZX3lXT(Nt2xTnATx44L*gVJLW;Dcszkax(ojuIKIB9 -dMO$m@hl6E&zL9kQ|dW+f9@^4UGi6icpl&vE7e%}`I!Y_>bK2yFENSX}heJ3U3O}lG3ps?3!4DQ_K-& -@2YZy20d(J;OXZ-cDv)-5(czb$?TZ)b9vHJNVSlAK1R%T-Y7rz#-2`g#OSOjU{3TZN9IqRFX?IB$a!` -LcC{lO%D>i+sr3bA!D?-vRe3GWJZC?IT2iyEVY(1_mXIJc{&^5|Sf@gD`4poe+FJ=twmMqot?!mP5Le -`p;1n^3PD!k6-pfRE2zls&>ExQXqjt5CQIR>PSuD2({xY42CcQfe9FeA%p-)3Pvc5{4T-ZfIZ1kZV8j -4SdV|HOh5-5B0rNOK;$QiIK(?Y?A7o0KamIO`a){m;Z;a}YBwMHY)brufS)NCpEOqb=^c-=Puo(upZQ -0qiaf?N9y1=(L!onj>VEbE5Mjs9`(`ro$fgnS!O1<6WZ7|gDEkO%f8;#D&s?fQr;|F$J_&i`%y8;xfk -OXu;MDI>)%AUk=(6l!WH>{i-V+1JJyg;Pi{Bf<{|l&U{T-@0h6KRhqN-mG{x72{^>3r9ZJkjgK#8Xyr -=40p6t@b_A-{w&IF@O*ycp@-N+D?+c~)~O -neNNBM7j?-I0^N5hNy*@4{287-NN${qrn}8tFmufj>Cn@PigLuzq9pI-&>}LxeNka_f>a{CFoSzWK<= -R1hsvF+j8Ms6=4&0+-niAha5Jw@#33SieK`s@evqKoHSMqD)R>Zx$lH6T|voIU2ZlX;d2Qva|N{pJf8 -A#XHvflvx3^L`_o}@6Ds~cYp>D*w?I}$!Q*56QOrO~pzgW)Ug8j_{eA_#E=HP;3ht!JqwSj*fpI?;;- -Idm7}^)466wFq4A@|=dmnE0E?KW^ewOef_Qfaiwb7S)%S9&oojrQMh2rj>}0oSLjEXn8J4^|^_!NJ=> -TOQ0P#J8>sTA0ceHjM9jCw}g<@3upi|>S<(lQiZ`|#tN6B3v;gdlS`fbX)t(t#sS?nqhu0jlE60(L`)ebsBEC5pfOBYdX_0Ciy+767TSGGbW2wg!?7T9-%*}9hZ_vfo1zs< -XV}>N{RSS3_oUCLUBY$^lA?u6Hen^aVS7!B;{!!W;39UkGbCFYEZS~S+~R%Bg4#=#i!bEB>&*DOm5kY -QdXpNboB5H6>{q}gM$*Dsccr)&7x(R3HgEkCna(;^4MKblEblfx!W#h6)%hj*PBamXd|HR}>e{c!KjS -QifzR@^KcW=q%9H1Ad0fg!DsV>LKX!~PEHmwA0HSdA%MCje%cl(069k=yj#G5Cw`ba(OQol{xg@xjt3 -_RWlU~lS`yH{$UCR}>r~yb-F!_?F}mrA}6ah`2Fa; -CFHYxFVK7u=GIjVv#h7-KyOtiHJ{@%IgJpph{S+C3q%xWT^EOW>>f?Y)?D8ozzjH2HcuQuU%`ZQ1K;5 -%%Bd0dt!-9f)QBy67nw5Cx)rOOrO#krEIH|`qK`UZZ&)vgy{^d`|WzabN>BucPqy>DTF%7JLDK19K?V -teCwCRm22KQL;fkKz`s78q|D2s73e?oer!mS|47^y*mL=!TmJK-QWqbb%XyyvPqH72_hbJjeze~XEBU -vJ{W7}b#|!<2xrrephM**bgCq*>kc&b{97R9`!QmZzp%jRr5cNIubpWQ%sLlh=AV(^QgpVTJ(E>yqB54|Jc#U6h;TgJZ6FK7lQpM)1^6t -?mGOJai1)v#`@%A!D$aMh%+}}1899ua)V=>Z6fccdYsq4!Z6*2mwA -5k&Y+bKTmr3Fg2;xA;a|*2TgosP&6Rb7h)IvjG0$Q2PRd_c;HzDBjOAs -$tRE+yZFO3LTqEju2&>F0|BF-%X>Z)4H;8!Ap|90O4)yXyV!y%W^~3#=rm_cIb$^@bX1GoFxjzU6BR7 -Xm*=Wr9rfm9aAAWJXsW*wW=Op?Wo(`MX%0e+?LHdRJ -UVd!$>LH3~WN~8SDT$lJEWyJ-j%xXl&>uo^KEi&ufy89usYwXLXI~zDSHh+(ydHD@P6JQuypTQ^Ay59 -QIru>aJr}qvbnl0?0KVWFCPuH#Vn*&RV=nTy69KZ7?cTwS0vK#9UsKJ*&9>RGm~aCh -bFo>yp9m=2UNWJ`3|DXJDkS?&k8;luRI5iDwD)bkaqJt=HR}CV^pj%I-4n6T{3@eJpIEMmbCYZ2^a+2 ->JatE^Or4R_lVMW=L{ikBMlX}+2Ot&N7O7-Q`Hvp^J0q}v?0%5{NvHT17yEOki2T+mqS&EIPLU7@k{E -^%2t>goLZTG5<4z2Paf15p3~b`V_uRc80(~kzUy>2YQQSe&W6pH?3H!t3)13Q#`b>r$4F%z67}zm~gg -p9sp4>$iw08P6`1Jag-l8#V-|{PtgdC4`J-x3xZ!>h93naDEgVvxx3crsNEcyl -zsK1CWU`gV6dZN6+xf=4d}Q=^rPvG9BGR`bBe5aIK>$LuT#W@@Khk#hTJe{Dc4GxHUf- -TNAu!M+iu(Mx0g1$X>YQiAY|7!dnS!UK0$sXXF$-Q8J5hE>DAXw4Q#fDHP5*2u~?VV%>XynL$B;YDeL -a?R((#Vlgv%}6MHAh3?dA`wCLO2^9^W^9Z42RwYNMfcKer|1n@mCu{((?c!hg3$OR`9G!0j{G{!a28t -USk8w#Ljt=nN%O;>c&OM*bRqh%p3Xp?z`BHWsI*EEy5dBcR&53rP%<-i?UZdtfa*Bc*@tAK)111zeVR -mB;0&h(7k5;SjQ0P(gy9PA*#0`hE6!j4>zz_C4fq!0vA0v(l`$YL>JwFjzZg(@8y -4&to6nFXl#8{9Mu%hHZ6srg|E;Kkp2c6RQSxhR^sO?ec;s}yBz!j-{V?_`dN-C|$oF3is`JY+_42JUs -B8ab+9N+NzdoBBwd$|Z)1%FcAD!y@c+N*Xn#C$0g26@N==7KLG-&n#+;GRxI*Cul2$qY8{hAn#*F%C_ -`n?d)&$0o=M>c@@$Oi0?^R;D~rY%HBwreP}lht`=b9fYNUTT!tIBWKj0QjuG$m%KA_JU*2@p5V3w}C| --US8McwJ+O_xIggXgbF4k(f850rm?^DqpEG8xU>rZEkt@wvV6rXPQ!^&1T`J;#t|IAS8tu`XH-u5WCi -`zZ}4Cm=sgPZZ`4h4YM+8r19-yxrIXI<>(zpIAgv+cZSJW^E+njq;mc)YS0*=SYF?TgB@lw1C)pyb(K -XV(!tNU|u{vR2Ac4Gv{n$J6bc0mapF69GolH`a&C@M#33jWys*ZNKiFtbxaIux*fyz&g0lX!>VMnu5Z -owun24!{;S1gt+*`_wkl)x#0{D_`Jr|ys(Rr}(bv?cIY^OnUoX-m759f>Jj0DEaKEI;F?p|%La_o}V_ -Zr<`ULi56Oifq2Qx~T9?Oz<}Lf!!NYH}!tCx^w@Y#zCM?vxjJiQc1S%3!U*WDzbJvEnTJexx9@RUey% -qCGqR-wBIP8r2-5mr`8zWqDhyRYzshH7{Z7Kox6Rf$hu3W;w+CdBq|jgT!|@u5uPLIm%xLx3m_!Zqdj -w*syQb$RkT7Ts&X2_ra~becQf+V%#O&2hADU#hoCDQy)3e03o9APy8{Er;i9*p@a74Di8EYM#^T1aj0 ->Gr$4tH4yxGqJbJlt$P7WC1R~b2%tAbfnEt{d80{wN5nKUrUTqUcThAX8@-Ib9W;Yh;8K9Z&2{x^F!$ -qt%{YSk72y5I3@5VZKF*;xaq!{5*(`o6dMH&MhtS?G^_-*2LaPtg)ZcfSWB;O+-u3L@}5jzEqfgd~dK -D1?4DcsS&7k|WuLeH0tB=rax{JsP!kEDk1zucGiHyN2xn!S@@ciBC`R?t)0_a58()vHLze`EbJUL%MP -IRLE!MPC^|64G;b6?}CToe=B(S>wfR3a{ -~Xy?;UOr_&0v0Z>q$r&)>fbDBrOEO?<$ls%8vY!xl?il5@Mx_Rc4) -bt@;`WFex9!#p6!4iFOhlC4|_2|@{aNVD0kD!y8ickEtW_MVfpN%_642fO~LJg-f$QdWp;NfLOp19QG -!Z0~E9SEqoI?rnSI7z3!Tf;hh0q|>QFjAA$-`ZkH%9mD)Du35{zidg1^k+8rrJt{zKi-#+rsu$r7Jyb -@l5FQQUk~Qh?l?AYz4&ZN+GCYu;@y@KWE5diodR@4+R*0CqBoz8pQ1z9OirDM#+bQ&4Sj~U5((laXH>>={Zl -V~3fgpyF6hR;aNDu^t>~`~*P6k3a4xupe{iF-|>GO$?NvDT~e|DtnBlJk}A6-XZgdLC%X#@L_dRmek_ -VyTSo*cRV-8Ahl-Q$EKUF$4A~9}pj!ZuS{b7G{U7#N)#f6CZPVAU2kow?(AmxAUW-vxBhU=1jEp;QMdL)57?%SA -QK4OOQe=BC-zsL)HY-jOnYxYZH@2`?B#@|W0l>b{vm%qN{W5<7oNOHz{W(#*Hj|q@+Du#O`hNX0Mm&j=ak1fgNQt_yc%;vl$;B!7M=KOJ#DWgwa3yvu|m;q_Q1qJN -(f{4*5C3*!lTAB+358x(PfgQ6s-%TnJbh!z_BmH#^V`wqu1I^35p}K4H1o&#>+}QZEsrj3gyD7zxamC -U^7yQ(;)KG{o(7hY5-MIIT~)f7!Q^xUpn!!Fh8L?YuHy0p3O~Zag+Au!qzNuC`K-EAvBn3Ty?Ba1=%y -eYDs8?Hj6#mJfPrOCf35YSa&<6c&nf>2a?|gWa@v3B-g2cJ-CmqsJF;`G~<_&mXIpAbo)XAx^n+D-rV};SX`3M7+3-SJmojTd~gJP^W8^Z8AG(e4wg4MADQWFrbfEfk+2Fjl^k=Fuu&YngXBkG)>d(tGj}E -U-#?ueMOijU+R>iyh!)Sd@$H*-X1Sys+GCN&{k6zi|8z=gC!7dyD6cTyB=-HtX0RiN*}RXiQ^RK{6eN -)eBdhu*FL1hX64eHeeb6diN1j2f~O7t`7%$o?YHvXSh9GakRdp5~PhxOLeYCT_$HfM`aPFR%G=`Q8tv -60_Y1;N@9-APNHxL)dd~C-a*mOPd2EZjr|?!-g9HnPRJ_C*y*Xdx%eaIFV~wsfUN`!XWeR}aKl>u20k -1bgXj=rUT$Z$#Vc5smf&gebv8JVVk5Oy-a5ves($I?LX)2k1f2aQc}s37+E?qrP7V?D-kiK~UL}JGos -S( -_aQ{tgh;u{vXq#|3K^Z=O?lB56HUzc#*GA?avqZO{oloD3IDg8io)U34;`Z9JCP#!4V8b$)j+FK%X(L -zk_X1bil0WQ&8Gr5Asn+Ib^l)gX(ciF!)GpWyw$Jew_S1fh0RJvB^i(BqR@@bj*H*k7`(!9cfr9`!ub -S2N5`m4s>^np^bNV_#@Z`e;VDPJo-$3I>gAxBSgnO^JQV;Gb; -f(U?&}a|oKm)tBK^e0#tgO*wYjt>vzil0H0X@HbwgXZl+p8XZ~_p5Rja0HoBt -=(*g+6@C^IUt`KznbBnZz?65hC}ASpN+3=mXMWz=7w!!FlK*9QIW=?9VO{__J&Lvr7d2>>B^< -5`phs=L;iG+d)3gr?(Yu@Tb7l|s2$2x+(joW-Jo_}*EzX3_)t4XH- -jEQmL0He?1C6{?&nEItLdRuhLnxV*dWstXTE#*-(t)LJ&44FvBl8;%ex3s8)CIzh{j -boiak8C^uPQBy7-MqOVPgbrtu|yCL>r!1N%3HY;iMt5j;*`*)N_8(S@bzr&x0rfA-;{j65;O3;oxzua -^xFA_zi!4=e|HZ9l+rRO`E|K1aP%(9JBwCqaUU2X>FA-0)Ax;=K0NU;<&U)}vi-K1apiesFHe(19~~VC$#7 -yXYzjgl4z3W_hG$bPI)28(C>uoQhs)pSYKf -gTBYuJ}hARZRan&fL2jmG)Ps7C5dq79{r49DB`v^mMMGf#K^n`Zc5pdN5wy%$cM=nBF6H%}H4J`q*v6 -I#8h(~lHn|62|Qep3(r4lw-SLV8#rC*&s_Ghgt!+79o~vZi+Gy8&jwzCGBxPQN>9R3weESpUYT(;$T9 -s0AlJxc1lKnO^&T9V0L>FH50?N;omT6}nFqK$G~?GsELL-@u{e6P_8Ly)@xVh6?>_cS&P)>Yn&bAs$q -lRen4-zS-h!ce6d2(p>mUrCj$IL`qAYm}0xphow>vLIr2J(WVmc%{xC8HT@YOSdkn7Hh)=`RCA$J!tND75}Dth0;sKc -DBCj%EKxOQ-oU)ZgI4f4kOKnE030`EAD+itX@_z)^yPK#U|v0;3?3fcE1!juGVkY4LJhb>TPK{0gGj8f%i3Q36li0<^ -Z%)}$80eMS)cx6w-B7uN|dPVTyz;54SSe|IE)UrbEiaXT*8iXMN#iL&e3rYnC{XZ>4P@k^iBpJ2s5x= -i4&T<0HMCh%9T^N%hQ_@nFmD|iw388Fg(>v&pDaPVAipt5+J7fqX$*|q{1IV_gXlF?WTs@Y`I%iyfu! -fvF-GXo$u(H@h4(AL~kz%10`kkpG7^ze|%0=V))1y0^dudgrjq|0wmpYM`P%eU9{k*Ku~OtR?stsP~! -dLjMX4|}@2UAGf|W<^1K!x(eYA>A~WLg>uAFjv%f+#P(WXY3To+Zhmg(0{uZT?@l57>hNa#v&L*r$us -Zzgcly-Dc#vruGegGl36@P!dk(vwpH+S8jLl66KSojwdKOQ^fvh#OF$1LHfP9ZbCOQ%s2j}J6YN*FzcMy -vGnS6qZQ5)$K5JHyG(%xCz#cO~ -r<-CUJW(8ZN_UK{IB_ik5cLUyr%44`nI$u9zfTyYO|DP||v`XXO3Vo2zR*F46^ZQ??F>hMYZqta*12) -#U!=m5+hg-^&g;BLHoejv%``zUc^G>0Y8TT9v|v-ZS;XrkTo(lul%2njz^6cgx^&YExK3Kuc3U3)Kja -H&Zy5%7(8e9h@4$H$0|+PmiQiZY4Uy13_v`j4f3fw$Vb3eJ7MlQW-fSspLI2fznuG9$Z#UJCTqlX3tJ -c(5M+JZnnI;7rx!tn)VRyeDYHJ?wigYQ4X)!n|8ce`&$ZF8Ifq-r4+2x5ml-{*N}*Ct&=Sw!6qsE9(gRT6tS{g414y3nhVNE57XucRHToRM)0#wz)g$3MQG>p2L(c-^5rB*8HAK*ovW&g+U;y+v -K_jvIS*7;3l5k^1=Az_4oL5QMI654ShhEw~mD2h-Nfe|o-px-6Zga?29kSL-)LXp{lWhCL1*YoXBx1m -Yi{nH^irK5cUle9%hK&!ny|I+n=iQS?JmjgkXo?yuUPgNH|9Ie`x)=kUmC{wa~BI*2sIe~C1n^a>XD= -`4x7xD%n3KBvmRBhq|9QNX`JiwBAV{ta3@P!#YDj1mk`{p*><9W4H7X7SHJG4OBD;&GY4KR}DWxlG_k -*ZGy_{{xzTI0$C=%$f%voe~_f%(p`Z;SM&2WxB&)8fF>k<};YI^ -)7~>3wdFY|8=V47fzv7=G|^kfBlQ|>)#Ih`Bhfs|ND=Leq*rz@giR>^&dRzH})AtZ~{U}m_jKSrf?J` -K@y`t7=}JNcoad96oC@owa_qlSWcKY>~!*FmJ9e3m3#tl{6i89kIpIVOQ4*7*Fx`>c>j;#A6XUf;Pvn -3bvNiCbV#@C@ivtm<)kP)NQ!BCsMCefVK<}bM*|Pj!$qK{yl!d&Ot;!@HdZr89BJ__ -VZvier&2LJUoE2)ep^4x9Lr_jah>ppRN7XodG}E+F#unpbGWTC-7_f;@3aa9&W^(k<J0889q~4p|OgG;B_Q;aPc$4XDuxr$*`k9Oi%q;8Y)GRl=a$Yg6ilN4;`+EW -WSw8zWqt<>st4*-x6o&-hrOR}79+l|*I;mt$K2Y0L>Bog-pBGC-sgErLb6VvcRx-G_eqM4Ok8U)qeg8nhgz~&B@LRd2uUX1vycFc^9=T&!k#%NGFdET0?LFJH6#_9m_4Wv)^Be6@Bsr9!Q2fKBj*f@I@CRlK`jok&Pw{-; -50V^dO)&eE#nZzaBIsfKcDQ>;VPyvuF~dIi6FRz{lFz8t5IeG$8Gf)Pk9s@!>0hPxz5Xe%6pvJ7X#HZ -ge4&w1)_=Vq4`aJjiL*w5FY~wL+J9uWp#OKx*6+6V*Ui>omE9*h{ -!2)G-b6eXEl7e{z%FlV4zlF+v!}q$Ses9U|Cqq68lYg{GOQhiAW^_>fxi5h4v94*|?fJ2J!KA;u@ -`gc!3_YFU5bZ!~O(8PSZdb;-U)`zA0Sv+tJAZMVFu_Vl%W^3^{al*kM1d-3d+H})zbWsJB#?+z*eFXU -wS-k3tS-M6r_+IDUdbL7dNPX%)EcfQ>%9d%jr?UhXiZ@_{U=F>J^bCjHer{f-LDMMr`P1WPx{6d{;P$ -)+QUCzArDDF3_0v*3LUkgkA -%x%9JAzTQ`vFcZWfQu`0OAZ?PmDM6ny&N;XmhBCkIgnTEAL7o|el}|9%f<_zgK(2kc)BIAZhJbr7@gJ -tGe;?s`Oq$M@(Wy!P)e)f#8zXVby4vYlLn0@w{t{b9JY?*3gK{X13%cl5XB@@VMzIv;wsdfKN2B?r~! -WA!BTiz$@0pH1O>|7IqiUH^Iz(nA$o4fO8=za&x;`4?2E?~<^n`ARRI7Hiwh1E)y+KJ{`S5M!_aexP6iH<{KaE>@L+zom^R1@)RXRVQjSUtx5mrEvufcuQreJU<2 -D}b^b<^wVO;hbsr7EJ>6%1jn5|I%~V2zoN7e8ggQ9L)6%sW>TpjHd_#XM|pnapS4eYTy@6}`7X-I}2$ -bPRcOON|wd^BUUPy*^Bec{_zIctXvh;Raw9e%hW!G+QnI7`!XXYOt|NwYbDQbiw20y_^-;4+&{#ppnw -3yDCqMgF)SE#7`Ot;0{41&Q&7Z(&gxYee@fGiR^|T^7~#TbeWVwuVo8MVcaD#l{>qtW!0u{)DlV4Cjc -$qdXTV{h-nxnZwoPEo^SN}js?C@r&&lI+m(XtnugeSB@5k?^j4y&d}JUHY!h!lq}D_ni*9*!o43k6ge -4aF!HvP^nAe2e<_~SwMo)X7@Km1GYIbxHt8b-t#ocoACV<(x@X0`Uw}mM`&}v{RhH&Qsxn_723i96Mo -Uqy`BboJkox%i`6p3>V&SScYi9G?H({9iEa19=Y4Y_NMXw>TvONc#Ba^cgPLz3w0_ -+k24ou9kGwx$OS}r}12ZsEbkdqy=TqT=3rt-og}u``xh0 -M2)s8mcP|zHqI7bKNA#Bs=c7z-#H#4O6M@&R_h;a+-$#LQWB>C}Bvdg7tzJpD|_t3?HV^61Fr;JWB=* -l0a5MJ(-YD9;ci~UqSR#+NEpI*6D_=a5b+EyXtKX;LuhO4P;|PCyD$z!TC(+>$S@oSqmQFRcp7#?p(3 -IB73abTu%-a*}Y`bRgAH@rCc#ebGIjeGQo*orYE!!UiY4-B)T$!<{Y9$Q8bUIlqEH!(lud7Bn;KV+@O -fr(X`#Ucsk#k0OYo1>DhKips7V=mncdl`9kG&Xt9Mm-Hm87Ol5lV1Yur<65%V&AFHH7m>bkE;2AIk5w -gz?PQ;aflTs?0p_`7$$>=V2RFCeoaoKYZ4GO0m#ru%x_pV0027QmHaImle3S|iC9(Q{%|JP&C@1q^w{ -RQy+8|3wumic4c_1`7FMO{1SLcUwXh>uD|_-P?a$)g!8gAS32qp^)VVmso~>b8TXJ(!Dr&<%_qd=w%* -kk1iQMn}B2|Gd9w$2r77mpRf`QF;I`nEI@p98ddUiVXb>eW0KN$D#2-%%QMPH3UDR#zW%b1EF?AwPUX -xa-|;@%pO_7AB8{okODl8P#@w={9(HsP0z`(E$mN=7()C(UFEM)*S-2a>hk|3>Z)gX{))PoKSo`q>;D -&deVsIUOqD#QA=^BrR?X4$y8>$U%fVT#icl54RX4`eUzZLz#{Hc6>xqGH&b%l#-F=-?DGC3DXTL*dd( -i5Zj0Jp^K0X3>>32G6KMP#sAAt+@BXF@h50p$!WmebLHdw$OEJN2zHzIUfqtN9cTNC!8a|)F&cp-V0B -D583tX~dp%Q8by?mbvMYZLrS&QDaw0#50prDET8kZtD?Ja0j1kC5Tq<-mnp?o>SC+DE9kIEOk`rdF_| -*6utOaA)Z;lIfz^^!Y-ZDk_(KGQ@?|JW9+lVI31@+Mv-!M_@zEzfpVtlyP3`HbhVe2?AhO3u+UCw`|j`SWPbeG%@Ktjo(CFnuJ#-AY-5N!A+JMP -kvD2}A8s$90r|RH1q>pJb%E34?;LB`s6T5JU7R1aoJ8GP4D^D4bLrJ4jm{UBCk0)Y(pbNXTm?(D>2WV=pdV|u{Tf}Lc%y^U#WIHCo-(Xz=vj~uRLGuf(|k2b-a -gC}O)d2f!C+4r$y!usaL#8s754#Gq9%nrAfj~v!!f%#@Hmy9-*C~PjY5<#%0#?m4_W{vZ%O=3Q2Ejm* -{H4sUMT)F%D&MM)pW5#lcWMu`|r>45R{0#Uf{>Y_Ma~04m}lzU3iB@tt-xxmOTIMBtFCkElSPDr!Vj0NC$VC7J!w7J)}Vkbv{_7vaXk(Mus$@?G7 -Wt;o@EMlANea7sc0J8dema!_2RoLrW11cENg;}NEAPb4qLtK@OFDhA#iDM32!*`o+_MK)>jlXV>GzKV -ZesM<>U))BdE$y)83!#I3v~4=XrT5{5torBEC}VT1 -xP_(*S&hoS&ZA`}XLS4SX1j(#)ZQ?uK#QTi!LCdf}IazH4QIvTq6uNd{|B>O?0EBojprUy|2{fMlfpQ -0j${Y0Wi=RW?K{dGtlr=QUe@lgmx!k=L5M-6GoQH9*kOGU@HuH?fIA)^Cg9Ys>`lk!iG;sfy+ABKU)e -V89?82DJ18(=`gi9^7;G-EIlcdLoNDDQ==hQTo**m7DBsf&u=YszYqOPX -?GPnu%r)^eP_=ef{xwCuVDf>&fG;Drjog6u0OszlKHU6Du;eps^kVZstYZRxNd9%cvB<9c$_|lYZup#BAPJ+ds?pj3T&sD!6vY{F1n8MqsAaCdqaMp6X -Xux@!!+R898-1xUorz55w^15wozuHY?8to$uLANWRDwh<~fjVZZ@O}wh~jMa%p3|E{X)|boVGbh?WeD -dD!CpW3{v;#XjUoOc=(m9tlcRB+Dy?JJ!SP=y^yn%*T?e^Wqe9xfigB67&S -r778B9_bRCA?zV!fkvMTx`%M=!Ri->7OMBP#7+*Rs8chG0RAZ -P)}M?ZDgD>#lkk6XlHwRCM#f(|E0jSv@V}Yo36s`Bn+#8IE|L=fybbq|V ->mRA~x4PNHguP*BcFx&fk?Lpo493lKbEekl -X!Cq@!coqUhi3ko40St*dew9RxVdzH#c@AgBaUNqV>~wmyFn?{;WR3eBswZC$_G8rP>`2r}67`GW1}& -5_`<7MgVR6wIA38qit@wOI4IQ71`^(>%H7^*5_lWpBHR~bFZ4_Q*i;%G6+o3Nn!6UJ27Y!GF15L|IgfeExT>5+k*Fa3SV -{?dvD~ls`>(%APA5Ek?)WL5r|Ci=?}oEDND9w%dWHgbk*|mG=%UIQ~ba2g)zn$AN;5=J;K8$4Wz4y>< -3i2wPNpP^UF#^NI6u86OsBrZB>_p;xNc+U5}Dh6vO@HBLgMKSE-MQ_GJW@GG&e$Dy396Q+n#M#bIGI7 -&_q=!2l%KJzYMN9>%C(@q(SrSJXWZx-{BKb0LjLda2}WxCgc%a)~(p3t)Af!m5ZiWD|{T{|~X%uU-3R -c=eO({vKh$G`WFWg2GT5MG2bv5yC>rcTEf5+ji-DynuddWAAgZ;oeV#-%j&UywAw~q^7l2Xg4^#Y3WV4vGHCeC;>Y&^t{C1cZQD -T@+UsT%@vdM6Z?DzpKjeFegT{BBS&Y3~kkmewf&8tytLX3Z6AMszM_74$OyC|QQSgZIw!m -^Hb5lU4li(2KO#vB!Yg|Z@8eW~k?vVa|CeRr^AwSTfI;77~))m80C>t~h^e?(e{{4lA9P|So7_uQ7p$ -CJ5Uv6cjqdpoGNPAx<3vAo2-JH|-P1xPQ89IuzXZ5L!R7nJX!Ybzn=PDmeK<0V3ZZ3~lDz+gj&7#$%Q*#b)3^-7C%IzTHSy!9*%2!fE|$IDF{5C16S^&$(9x -Usfe-XOgBx!yS~XFmCD-JW!s;*M>J8ehEA3t0-$G}wSwC+LnH_hSiVU|#nfmM3h3J`+|q8P`wTtLvBw -8&xgoHq3_Qx$Ucs|gBh&!Uk~j){m`{O0kO->}!>l~2Fw=-budynAN;2G^ZeG&0xBB$-{nk75skChSwZ -xuE!Hd*D7V^SE#22*dsrLFNADDI^yP7F<%&$c(~_zzM{o&E2PL&C}xsk~>s(8787~&fRJL&zI -|U{5qE;!ubNkTSM;qV`eoU=1QyxgB&PrR4{fIBbN7%ACpS#SYg5u3M-Ck#mcxn1{VcbMZp)@25COBm@ -U#B{?D0C;b@$*o1Ni?52yEJ6_dK3=udUEM_aJ2|w{C)GXGPwpOlWfoOR<*PU;N3MaVuyg{(E -uoxvUJlBV4R`YVkB@o{|} -X3jsF$;i4sEgF_oP!WvBPy57OkQ*0U>j-ujSP&&gS$sMJl$F4#yA15***2kZGFTpp?nC5XQVOpp9d#- -?NFm1V9JD_N<4Cm!COJgEjI@Mi{9v%ZQO-}OP5R?TVMl#~=K6lZ;@MWI^ ->`W75|H_PDgN@hzUOjb8zQ7p9QhF{#K -OHz7}0NsFJgC3fubEiLCG7Dq5HJhK4%~7?Zy!KAu6QaS0LouBH(Rqf(AQcBZxQOD}J|7lbvF`|#~9lKgk5P`K`>aQ#Z~7jD%>RsN*cIlKw|^B)GcfStP}mW=i3*~(k?6n{XfK^CgXTe0VMv-(p -!_y}v2fBPbW>EICXK|jJL-__?(vbSx18RNY@>Z<#oEc$t&%H^eUV^)=Ca9Q~MA;6cX?9*L!h|1ZP?m~ -e?uYvohe2#@9#L7YhACi3}m;XS9%u{_B+tc@l@4ePfq*q0av4xka^jzZ+B3Pz>y_tc_9Q3%0IubAH6T&pS-Xiy)WROys#g=FW{fNuphlI;ICfT?w0mpdK!6?Y@r3)P`ke*DREv;dO2b;%x8ED??b`42rz3RwG=9G&#TqZIk3eNaXf -(nc!fzuuhqy=2~<{a|c)=#Tvj%UV6Pc97eskr{Id^DJIx@=mh`p;A&jHAYcfyg0IiXl_f -zKu;4>`7_^x@5jQrsPGAhCFeAW$iO~Yp^T(=_EdK-MgxB4mX`QbTnzz|m{d`Es -hKaT;7O0ZE^xnaRihWK25nqHpfvCOa#d&(<<#y5b|R&p?GtH9c;Kn8we20HV2(lWIMu}F@Zou`gwii& -VKj1NX??)Br!NAG%f(_uU2$(Cc!TRQ06Q;73dXQmZi;f03bfbra1N{h(QYSq=;)q@2Sh?<08TYzOO42 -a&x*!b&3e%38IwcbJk&glbl}BV#i)b(D4?z}ClHrbih5SNm?vz0VFu7J)4lQAoC3L!d$AZo9itq&9%k -thtb@xP)#g~H6e(1hjQ1W*9^1j3C%zgm^(xiJEmOYSBT6wrSkQacwwSo2*ibsP?griQ#|fsNmjrvrAv -TKiKWU}?VoVh{_}>S682O(CBLCkl?jItNZDHR+5gf*`H@}C1X%r$b8ip|#CSY_gTK%vqNW485;y2Zs+ -6~$FPT-q5j)r?s-@-ply-7Urf5Pa;@B&*6ZLke-_A_%oz%F -kFa2AeQ|_=+72B^el^UZ2=j3*Om#Qp{GY;mTiRAO^Bj^Hu6?K0l=mLKgb^ki(3hN$p{}|eT1*N>Ui$8 -;I2mDpk{b|s()aNG)2~bV>dEo_EEv=VUwc9}z{XDjzl>OL}h13r`A-qLocE-({Wdw_1^HD}Se|SPn>M -dJN*{7~KZTgOt#T!mJ*I!@m6xJKKI3v%>N5tXm?qg!`OEov}^eKq`Vp?y#XK$6Ig8kD&_KySX>Y4JE^ -2Sm=M|DN`j-G8NRkB1+;qkt+te&5=cIE!{>Aw@Fb{C4k@gE){ZvUnyS@?Q0D0veYY+f~4WUm~u>j(dF -pNM8}6T1b?0$>oCVP)N1V~z+dk;LkkPQZ5RXL4}58e!p7M~p1FG(E1&;H2%<4`FYRB+tKEPj&>@Rd3p -W$6zb+J?lxY549m0T_ur&!=-ph9@$vy;3g)>Q{z5ugYOCt7E(IV3RtO*0Q(|US@|+!^e?!dz~WbAUlpQD0qrk@V-|5zXhPWI_5xj+2{K1UDsy!X -$K#Gc&v77=tQ!Cqb}l>c=844&JyxZbs7R4l#TOxk -E|?BnDpz~#v;eOhhQ%ka~B}aize82w4ixUa{Ofb{-d_>zrD#fZ(%!cSzl?$eZ#L8(P>{FRHL=H(AbRV4P;Yipx}Qd&KN|~ -)-g0qouL$fNpLVY~d^gS8mIq;9?0$9wptlj;o>ITPuD6sFj^5fQ$*zkGy*XIhn)X>-{GC4U{HVWeT9k -VLK)t8_1 -caso~|bR=^i2P?h%;Et9xSjeCLDZ{Ryb$wikaMEdRX3zgr>jcT4=c6$1Mbe_a)rcU@=J1fEJ+yRlgdL -V-aihnkx%qJa{3XmGPad~REbYdOXu4ktUXkc;Rb>AWoAGI6C(dup#zMc>h=%{|V5DL+iB>3OmslK~Z~ -C8^?TvZZ>&6;;aDNq)jz_uM`>czlt{wq2=|{X*sW=v-Xl)ZhT -%%(-pORpv-a@(~0HyZ|#Yy`W8_`MSUgS -#%hNpDxLzF_4m+=CGhXJD0Qa7`>x;9%m9{m`wI8kemm*ftvyu*@|Y2w^1^7#sL5=o6qd~qFADffnH5k{>4Vb*d~neQepVEX`LSpi!7lZ`_v8tbT#%$ -~ADEW^%-qVx=2Agbjr|jDOD-iO(oayN}TNY?HfoxI3?|9f3fhl+3Y+NR`6gf=63zyR$Wk5_w_ph81~d -Oth2*FJuwnjIk7kT;1tbsc)w&sI?3*8&9eQg3bsicI39oM+TjMWUAv{@SMvP&b!!RskN -!>=h;6@&Qs+pA7`wADFG0t8t9e-eqzL!5(E%`=D^2y>=hU2JW$j#3R(K$j5G~iFxsBJUV8QuVtS>xSB ->eS)(fdSO1p>{3TUv_!=q`x{MqbPMSZ;90tA(ZUuNohWK6Sn5dLTd3bVAq;gR>E|t_QQ* -eq}&;d5u=htq&&b%xQ*#XG0U96=0mz)#K}^n<2U$bus5_`x1NS3!yiMY}MdQUhD=UfmhXABl3~6Iu}2Kkz*8PH!Jy6cNvs=Afzk)GiTg%Zy53>*8O*li`g}e|IKF}7YM98Vdz3Z)X8}oi<;s`pv4&FeZ!}RO$Yll -WnTNlGFi1C%n^v&TdSBD@7`3*x+nx<$7{Ui4JS9kadynb-?cMXCc3jbnXJb$Tpm-$7gJtIb`w-oR@k= -^og;w`ib?WTr51YYzT&xGObwiV#J+C~8Fx%!rH@A!v&D|x^>ha5$A{DWd|K038M@F&0v3HCAGopTlKc -iFoG>08#1r+eN0xy>N -HO{$Ym4XSVtnfL5p?pyln?eMBL@&WrS?g2p;__yM7YwAnRQzIQ_y(1pN0SMGuSSAjpifxSW8kWL0{wL -^<9E*amT{X5uA0&FBED1H7x0IJf8v}QmHdtBT5EFm?;i97#1~_K=JovBJWHYT0 -G*A4w)Yhfei+r>(yewf+I;Tr*WpZ-*;>Mu7BrOE`QDf*jsbF_Z2_>T=Bduz@*|7!a+4vIFYQ?pJ2632l7_c7f@5g_PqU)`!ITx4|L{5z*L-mOj;Iem&rfU -_Yg$NlYcp{ihQ|w5Ja)y;0V^FDV<=Q(Pp@}=5%JReVNCDau2s%>Uwgzqwn9juCtI@W0WyJ601xdNoDX -Gn!h0%2JKXcei~X+RU@@-?6%Hvk60x)W3I~xkwpejnllo;^3Ygeh8nj -)zHbv?UoStz(OIdEK}Z9DI9Is -wZQ9mUzZ(_9p~V)R5sck0;fFN*RijOgjw*aNnoO*Yzve>1)3l~>u&@Re7Xbs2V(fEuhaD5uA}el~MkgX6k!&y{JJjwTFXo?(K -@AEtSlOdsd$Y8n+w5IG*kx04b7j-zM(t6(SO8;EVKC5i8{>@R+89(>8Z$7V?u=e}>dF>B -G4XodpmH?^bF?kz5{5Ff9sn=lLsRE!R6@jX14rzfk%FO$c~5FNgg8l4|6xkH`3>mNNYGZ$Ji7aCKI1w -u@HS{oLcH)#M^pb(W-mB9PIFj9YP+cGSBYPo9XyVTOyk1dnP4sq9q(T~0;lxybHons^5dlzoc8eUiXf -t&5Y%aV2Hj7V*X+gVTYPPb)&-oJCvninBTGrU5zT*+<;EBZ@KC195NsafJF4!nUDgXQfCiO`r{O=s?o -JyY*#s39!dpAkJ{3t~%u5(Zl@>iJPLEGyEkK@Vs0dP-kw;NrTS~FHnb9_<~*BYaT1ZRS7c@Ow6 -(dWdq?DEI8X_{Gyl)7q3rmHf)xTZ-d5EC~iNR7ppE?YHPqJS-PJk{}P8)R?rZm8rf0;9;o6_LnBs@La -TCo@<(sW2mPCJLyHy?j&@ypP-dNGY2OEqT8W97Xdw*h^FGcSSqE_Cg$~kzx_?jIc)0;>hUsKsP8#27+J~_N5IkSxc90St@V;z<#3V&je-RE~% -nk?bhTRa!}W(vPK@-0!4^?UPZ_5(?M{@DhjK28?fsfj;oSwa)H{qQHRum5c~{e)?MbHnct8%~l0g>Qf -uCUA$@EsJ<(2lZjc*9gb#C1@*m$N -YF?UPFFEUjIVF?b8>QhSGE1B~09=;U4aiudYV@UD6#Kbrz3-jcidMmOK_&$}qua -Ni;0`D!LJ}qcC}%>qYYK2Z;e%A3WdrspE%Dqx8t^8Wq?uBa9A6k>aybY^StZX`n=GpocG!INIx(5 -dc)g??=|qP@wHz3PJ`FpGfc^d6Aj -V{zi4(j$6i!m4FbnCtv#PB?)P*aQ -WRLVH&>lsZIxahyl`ayfW(DC4z&H4`hw2`yKZTyWye@1&PRcH)rBx7p(%B6Viz2{@C1!{vq%4x;%wRi -b+wuYI_JInQUNj|y&EW$cSJ{p9(_(v)OF`5KJcti(hoF=up( -}$y?Ti!)+kNR}GQmZGX<{Zx`c@TP6y8{y$b7kEi@X?FY2hvPpqF7G6!t|}3PPU~VRi2)Dhyby6+b_Z9 -riMfB(HW~BO%A^TG;JwjhS2_9Mbkp#lMPhov#ly|cd0jI{LpX5f+a-kP?@kl%f+IV$z{$Y>oH9$-2*C -Ocy|JR%or7t*hzkVPQb##V!oL3mkHR9=~wU1JR{&o)32Hi%37@@Sa%%`=2(jnZdlXBs7q*9-a9Fa$Fh -vXX-|s2Mervz>>B0Bt3ng;&5i_mzR^uOVPqU%Yn!)+%&*YQK1evm;_jZB9UMCtn%RFV^ -LyIm0z2SYWik&aF4>6dlw7MQn*u&=S9YW>~^R6r{l>ce^5>gQ)zX1MhU3Cg+6gVb5$nUxWzadw1#2jQ)d=|T%`&=fvvPf3A!$W2irsC))Niv@%{di1) -;3`&Tgi~xxn|@GN)LC-bBV!xc#e+2kw9mK9Il{M)emJN4Y$4ioQXMLyxHAVKas+4*tOkZ{Ne|9Lry+5 -v2l}L@%hl2z&FSGmoKB+8k*OLC?!DvDY$T#!)qkM);xnRbeDs1iW*>u8`k-u=;5Ydd23yp6RYmklnqo -h6`)}sI|0wnHFRuTp@$&t(-|36uB!r<5ijf2fQv?lT2uva*iDDQ|ktjuw7>bf4j#CItAoLFhB4PN=f` -NBV%5E?8b_v=|E8$&&k4)agndGgbmuzA3XC0^A$}-p~DQNmd)&+U%&xi3I;I_z{qI<~Og5nnAw&1n%p -VR-uKXW|Vg+$Svu!FwU^Wk7u{KJVo5aQ%J0;9WTJsj;7lr1=;?*h%Xej3{!iNA$McM<($Cn!VMZe<=P -d)$up$+dfN8#-?4fHVtjz$2!gFiwv&Sg=Z^c(ivlbuiD6&}_C-SSUX!QM=Ww%);`{1X567-!xWKzr#g^~J -se;OD{5W2z|pq0!H(--IoNXX!H#)nfNvflv`%E$q>j*r_o|ZBJXwL$~Fc=escNcv{RpyH2#}+(qeT%N -bzZ>8t%NeEiz*-c^I@s2M9;3CrNUKN;-KD1C -p3TptR|?9?S-7HUk04plk*o;Y!>cS%Glxg3WOlHWmKDuQPlw9a=jd4l?ib1c0Uj9Tr^GvI=Cn9R3>P^@7# -r1LK4v7oVbg4TG=AZib_}GRJ{n2!(ru%|HZ4JPvVy-7=aAVwdZ=8)CbQwyZT~vi^3%$NC0&M-g$x23c -iBTZ$s#b6+!>nHO=-T*>u*)Bsm%nU!C#vMT1k(uDh&BpmQ(O*nlLyGoJH?)i1Kx1C=tT`-Sv;7px!61%zMh*?;f^*mx9~GoaRI{n2$N)Y8tDm-e<8&u)XohJBeT>@c75fT?6YCCdR -*HQCIsva|v*g!2zCPxCs-uSo#8l7w8x`7o}f*k;KzM!izr+8c?d<5F_>_4XhRpr*;s$yZ;u3^>ougL{ -FNn;ftC2`r@Oloq3x9VhKIF8^ToZ!e8c^>1(XWfFfnr|jw59+Rx5cqL=|o0&v;(^hR(B*Qxz?J3=tOM -h{A;(zct-Qj);7=!wG!P80Nxhw(mq^0ufK)BEJy_McnAhM>Fc@ -}7NRy8#QF?0MG~?b07p1@=hwR^ULBJznjF_h@H~Zh2pj?t}VUHn+viEwM!2LT3B<(LU`#BR}&Ec)J9o -?~%MX*l$FV`xxGKCGqzEONqThuw{nvI{>5a)HlWU4?-Y&ZW#u9#<@tw>dCT~kz_DL@Fa- -8!czSIi>vKx)qhb$-HbG8;TKn4c51&?Ydpy@h^FKOV{!YqV -m}bxR@5=T?f^*de>@B!&VH65i*76;#4QqN!3FC+Fs+qOo? -70-0?ah3q&B@o5e(8b$Kb-)%>5Kd|_Z;8vBX*$dgWhWFGlm!6HP9`2J@p@)R{5WJMbihj#PHE!0A9Mq -c|JM56oGQtCb2GVx!)N1*z5EXpMB>a>&ZG-vM`0nVG5$O!c%f+xr}pjyX%^Gd8~;Hvxjjpt=?He&6yOD1um!AIJ`u#Jg_Mi3LuYR&duRnwuQVy(AI)7bP& -xOJ_dzGj~`XdnxgS2dUW4-Xe9vYeJoFMEuBjpRIKN%Si5GqD25`}_#)2uF>aa@fr53WxJ%^g_s77U&& -Xpk4WH`|0&ttb6znf4LDva(%1To4`}pZ~2(^xv=X%fSrxd}M@71|zA8vYfS4e#f7cb%2_665K3!{*9A ->`GAT;o>zYGZL~f;~<{-ZN7Gdv_buo^~K3#R=xg#4M795*T$BAa{k30vMj8&Q^U)Kdx6Bb$mMQB&m+E -fjl^1X-0Dr-|WeG;jT@>kYup&GY*oA&07~7lrIi&@oh4c*}!~i>vN20#eE1miJqKGoTBUN+~_$xt8C6@6Q;r=j5h#!PlwtnpA -SW_!46Tq7Q=0e!o|LRdN>M>-&0!u)PDf}+<>?tt*ztCaLKM1q0HCbIg{{zFcpTk*W}va0sJtuC^L+a -Z>4Qho)mNNua6imj(0N!IJysGHhGRgH9%qWXmw#p_Z7ap{~DC4RKmJ4x -TJac;2C>NC9kI5s``IKr&r3b%9F-fJtoB-{6IBF=#ssMx4J?j{I0JhhN2tAF97T#SdNyk>`aM_jaV6 -7=tp;bDv=-JI47KP7=Jit@BA9K@19!>q+N?or^>D(*+i0-f>!{)V4#2;CxmXtJmt0ifvgf!i3_geQJz -u;8D!l0dOkIr5171jU-J^zSMtSp!(#{wfbSiAE^gOfhLgvns*giWY7L(-^j|*zTZjt%2z~B#igf3yZs2l5X&VNO!oB0Ldq%&z1 -QhljpbhtyMD%0VD(LMH0}*>&CLrF-5qhU@Y&Q2&ntY -P1PEdaKCw>5$j#i@E4z#1mc6fz!-2Rmx}U%hfZYyFs!)w~9v-PbA+M#8@mN(rIj?+ISJ0yxxhl*I#>* -Ppn`(;uHIRwc2}te)5ggE#ZY*B)6eLe5>_u}|bXfR3;vA)fK^}IJmIp3BE>026v9@dDN6wtGNu{^FWqn?uyHZ8%rf}v -_i}q8lri~dJ2n@9*ifvx-2exL_;UdZ1skRl6n)Nr~XXKQuPVI!~L^jvX@-I3(3Z)*CWv%F2xc!njPfn -gx(zQ>MKY(fj(u6MtXzlRjPb1;fA2kIzY<>A*nc8ZEuG&{aoQVip6_+HxMtof;Zmn -kbUhWCOXR9REYiIN&Z(>gqSRr8jXt}Z&UXS@yoA!AgcEleCmJn67&}ur$N4v-MudCtJV3TBc6I3oYXq -!){i!Jk72h1JSr$A$2i@3U -`|semb7{sH%5+DK{DWu_OQwcJ#x52l}9YSJh;(U9Xnogo>elW77sj}dl6!+&qFSj< -NBZq4}3PYI@qcVsF<|a`Q?CDVuYLq#e}BFu2hb&K!kVr&a_T8aa=n%P8~`OH4bosY6hcAJ{CIiTYZiO1eF$E6xC}S)s6Z -r`^=bROE%GGT*ZX{(G`rKD^$<5X;L^pb6z`A|+XyO<6!ny{H#@cp7U8G9idSYu7W+ci0Ja8qF -^GV2vpt?@j4P?0A8)flB(p5rDh52Fg+z`Ksw$@|;(!Kq=SJOa --Dm78npdNGY;Q1^woOk{9(r(MA$4kC% -Zz&-A#NbBwE&B>AD25Md3Q6ONh>FxnqgFZoPA9{Jy6V(3*l>M~)`#%k5|97wB-$2?wtl@8AEd}pAybU -qJ8?@brcA}^HCN1lGv2~|Le;!-1pndl;f!~LD;+v|!xi@Ef@Qh^( -(4(?Pps*-ner{(_kN<|Ut$4A7JOoNY(}o3p3j*OD(_r_=2a>o-d2OZyt1oYiuSzI5jLigGr^*n&a$>q -TUdN2L5CXw-^I0T;s_UgSI$LhD0H1r1w6y*${1^^FVai -PIhi;{9#Iq?!|*7-o;9>cqhQ@TK3RBpn?6kJh5%9Vf>!bpm*n3=s -i@4!h0D3j^APx#6DLUy$c|8{Py&r-#7jlUxp_3jX!zUJdn4L{oX4>_ptCq!8O>sY{*+$BY4kS?wvUFZ -Bq1BSA~O}483jiNwW9pqW1(#obIi^=x@stVzBSy@YnK$!N{`yBKezs(R@R?W)pJZisgsXUJGEp{;U)A -yX8)6?%VTQaH6)M4~bIyL+;^A77l#u;qe-x_VO(qSXiDX3qBq9t#&s;biOLAT8maP&~6Z<@f(KwP_X* -`pi{EfA1JIoR4)3AF#8?`ZGmC}w$wAT5W9@sccl1kqchOc9J|a@G#T?VdhkMJTKDL4n9~E)f5MYd(Dr -n^LL#5OY8>nBHH(0pB%_h0u)F-6t{2~Jk0lyMN&(Jx7tS7Iu4RbUg%k=mbL_puhQQTz3JP0Qyp6VOA>!WXXcb*7NT4LhKOQNOxPKj?n!cGQA&7$(dNM~S$m`H)fJ-G#c{wGit29DpV@?7cEH -|^c<#H`!W@bbAkU(v8Lse0jUlK8~x`00UJ9>MqjoAQYPO;moT3~T}YQ27(h7C1{PkF>-vQ~WBle?l_Y -wmUK=xou$wpDpOI67=9gi9YTKzCfF3ly|Bu9MMY5X3!|2AxGq9@t;5+8rITp4ZzJ1k(`9Z4Tzn`Uyt! -E#P=K%E>@)ku~BC#B+oi%OSy8GGP_cuD>F}_2bt6q`CB!wP4_SGIj=R`%i1Qyqq1#)yF*c{AD -u?RDY0b|SCy=c6ap_w2OZ{VFHnDZ-#9tg)?9O8VQ(dRimn&rgJXm;*}fDpZe`U58eOova`eipk{f$Jg*YK19V<$PPO -Y=uL$+8aH4qjGcUlVlF)R%;{xEK)V(wi=j7YjKc(AitTyas6%YtlbADJ#F27 ->;PPv?r?%4sDk#$3m^PWAMM|vSkH(bw2p~W%fs>niH;7G2Jn1b|LK$3uAet`8Na@Ob=hRrJj)P^)Q){ -%`GN0_q{t5OnV;P&_Anz@F7xgARSP>J-PT$^b_c`C*$wo+6~!lC<}TI0dF -{tAor{`cr%H&e0HB&LUy(a^zQo*@IC^yvy;i4ShFF)@O_hAkN%zG({K6H^zY3UvmH7i(os$wS(#ii@33{YPrPXgzRG?9F3Y0pmYP|2@dP*p`^`!3Y1-woC -z|DbOTMi4bk9CatMbhAoq+C~<(>GvJ^c>}=s$n@{&3*;lm3Z2_2ZcxA8KCXAh5F9xTrpTWH`567gZ() -y}ok~sZWl@wFvFHC-G*w6u5GEtaj`e$51N>)H0&9)3X#{+34;lEqcq#6gL~o22ppptp?*Cw=bg3FJei ->P&$+^9MZ{d&}3M<9Q~&3c{zMBFTgp9(Rj7{aMW4roSA9;R9dJpWVV@Rm3c^#)1Wgsqk5;yNfh|_Y42 ->O=8L1Q`BMZ&mUjbTOBw~9z|qJ}ChuG#c6in5z#1ob_HpU;Li@XJamy)Kpr|Z)35)HwLOh2 -EE#L)vBsV}+q!UIy?T$;G+X2ygZzjCe_Rm2EqfzIoSbTvZ_@3pMsGrhPExX504Ru4i$57~4oMAKn?lgNTHBN^^8I83C)4Pq=iD46IFwHEUc?&HBYWg< -=pCPzZk_pv+U3%=>f60Qlva!8N9zDJl@BHjI&5LmNo;Iv -6yfR%5D2RG!T+K+>maiU>SNWl8m%6)%lBWV^TZ3%GO5pGu%a}ku9Iref(WyL$Xm`JAzAeb4EqdVO?Zy -ztT+waH4amoZxL+tE1xQkURk`4eRJEasGH(epI^-n|k&9WsDndK=rk>7(+P2-eP7;UI{N+q)MS-0o1< -`=h<2)T_i&b_D>qioX6W2<{Ldf2r9`D@99MaftNO-z28gnR|gLe{PzR6mKjbXg3fz-#Fh`I#IyoFpsA -jAs=&7yoM{32ilPB^-_82Fm|n&!(vWGVCcoLJ!!9%7G_pXCD}TGBABK%>n09t<6ESTAS|c0O~f0!M5| -3~a1W$Lc;3C+)nf2WAluwNoOm0nOI)1Axcps)BG#6&xLxONkUEx_t+F$1!bGq}R0Ish3y7cPZAqrUlI -oinq?9IS5QA>O+pb>3H8^@nr5t4d)gvKOQ%8YT!Fr*MKIsh#8mBPdKFCd;C^Ur}x7C4z`ka@&B!J3Z;AT -pGbE7)b#Bo9YuTPYS%MO{saH4$3P|fuuJk@0G94zuGAY^?Fud7UW3|@Uxe945%dvfy?E`)c^$>qgY5I^LsXqah$<}N7!fUW(S{$2Mthit6s`SJ42f}*5wrS!&r~+MgME0 -c$BdxAs=bv}wtqf!Td+4>_x1JIaxsN7@W;y7@53wY-u -W0k_c4d%?*MCig#NjrFFgi5d)L$V_i+0ez!E+Hmc9e5E#{n0Sav)riRP9-6wt>^WTDg{Js8Uo`c$ -miP2rYn^@XadW9vf%Fk1jnPNyoq4uMXsVgWuf7UU8ZJp>bimgX8jS`u-C{<@1dGcozWMn5*h+AhIA>= -vcr!6cg%4;BabX);jHARLm$PD;Q^8zc|R490#FDad3AHdJD -Yv2#ksF&G^Cz{;!5EXMTw*F2r8t~344hWbvx=TbKE|AT*l1@?WNa1nQf**?xjZt{-tK}XAl6`$8dkZ0 -Fr)NB^xA^S*>J!V);5+2uZiHR{AVn0QRXfT4!%$83(%0=EAUfNi)kXg{t+_t)1{5taRQGK{1%}D}m?5eVCS=Ov~m2e9~3eid>TkdT6-p3OmgBJ=p5Pe7IyBn@a5-c -r6GysRK(pb~dy%U9BAf|9t`FN2lZ3;aqy2y#@>y6Kg)1KaJiC<~-p-^=fzKz<9;Z~7ol$ixQXj9Pk&| -T~->;^~2C(O#QOv*e)quh(_UBBFu1DHBUhnH&Thx~N*mF-a_gv!rlehz`c5bMgJh8eY^&KJ01LUhk^xr -K%Jy}l{qH9wqs)N0tNBjMTR@k>t4aYcJ8hy-ID-lxVeR2I20&+tJ}fvqII-8DinOzFW`NZEr8$rUHYz -mZw_p895}pty)C3Ck^Mp-j_3wd6iKouk#BYGuzNe%LRp_LQ7=~`kc^I{(0bhzkl -$CXw|!UlLNW{T#j;Cw}2#)ke8*KX*BBj9_Fh*W=}Uc7VX$SZNiS=6U`0Jc4AIx=Z~07>Ok3vhW%2vK% -$q~1*0Gfov27gd5p?j#fss1sm4vW4S8qE}jWIL}l?QWZYJ?CLWo9GKJF@^F`oXTUm(hkX8FN8ynl@GpM0YV)?9rLhIs -Laa{b>VSwXlPCk*nu(*&&IS9hXrw2$|@PXI=T4VJ|=DV3R}iMLJHR`m;`vC80*m`x8zGX3T3F!0&%Fr5i -M%?_LM>B^|;Rp(t=mX6l7vLBR^YnK|Z|Aac{Z-MpWQP3M~LE8gcKq^vGevWI0mZk~TJ9QfAK1vu+q~( -?4^>P%c_2^!fO+1B!kYpGQ$RO3&9iWMH{hojj0gDWMXzYGZ@?jIz*bfVFFvdB&4GP?yH -un`RP$2yUU6b?ew(ZP@eq-9-N{>E0x3%p=iQPjBz`x6J9g>2L-yRj8h7K1d8K7T;3vLxW&cJ5<9NY?! -z)B+Hl;nsMq;^Ml&?Wgb~QL -Mpo_&zg}4lgkxR3tX|TMOGZJg$_Mx>G+IZfVifD72kg3fR#F4~I80pGHdT4!pam{6LF?+HSOHUSki5c -ZuaHY)o;@w-Vm#3~4FIc -Kr;HuOpGrwLNI6)WqAhkZ*Q0E?d!Vyj}t#iw91ZW798qE6XNe -Bfp7*kl(;T$Y(fc`=LMzByb$2P+|i=$QDj-!8J&2K{mPV#wg^|rjG#Jhl)20vtf#q*nvt+zPt6X9S7~ -9_PY+TtL;Ktbo=>CO1#6IINl)(5`ER--Tq_bJ|=>r`%AyfxWLK2>OQN2>?INWS6m_Q`Imk8Iel|trO; -ap?X5eKAUlW($-RRkdB^=bH!8GGJMTp -Y30vl?~iy1{2fY*PrOnOjN{W|Iuz+otiWLWykILQggiR`+=rQ6WctJibir+5q-(ZIf|TjK2bhOz^~~c -I2>k0X2|0KjqC{Y89c%bW)mcerBYHY6mHb+(XVWi5kB-H9&^p@Y$3z8gFGCV9d2#f^D&3GUbfkGe#@AhN=lO%}EVZbCx5}z{h?fgRTF4`D-YYq{6K(%+yzDL30w;aUw9Q -5738+~5(O!1vFfkgMRXN>RB&GsF}-@@Erv^NuPaoJ{F_Fuw1IR?EWE`0kw|H^o576Oa+hA{L^aI_l+a -Bn@^nfTt!fC#y#?e^G-+8@z(9Js|@(R*kv4fhP)-arQL(`>t4eCsW37ec&A9RA99kh}5liXV*!DG(et -clyEEK3~J|VtVHEv+-E}!^Q*r|IT<^PJhwedO7z+8E+s+y&MPOoPblCfWC4gm;#^*FofrPkz$F)1&yk -Ulps&ldc$avhentHNQ;;U);2=M<5sKRO4O0sbtzlhLYRv+C)gNU1=jxiEqnb*qY_i(}B?E` -j|D`_BAY%k#OK0rnng-daU>z1ZODX3|1y9Yk{|v78<@*NL*r1+ipYUL9IQ#bl`FIJp3He -klPw%3P(ceVpE(g+-2f2SaC<DMo26NxRd^CW%iY*kkpQZ4nh{f>MzwgNwFE -E(Y^3#vL6=c5gDBv?(BB14qIa!Q) -`Ap2HQ19^HQ45omWGMaC9)2!f%(o6b|Z9brI#}XO590Bo;7ZwW@M2*U$r)a$>tdLdFzd`_~nt41e4`g -r#1Pegt&aks^Hq|pX^1hjY1ud{H_6QTN9|^)Hubh)g&)Vu+YvqOp@VYz6L+~~8$n|O;t?Tj}ywh>Mat -jnb&o$&DfuhGHJ@_x&H%W`D9ShCP*X7v;Hw6XOlMzZ!R_M^0GU;ygTYh}0`TelH`;;aouusG=^t8|2z -2=P}v1F;sI>NcPf~kA74^X&O-NA{B;b=jOl~O(W!ee*!pwF0Kw%%wlixb<|zAE#pNrE+bNU5ga=k9_> -B~}9^HoKh5UYoXh`UH^~P)nTTt|>@LSRN?a9?#8F3*FouR#~JAlN_X)cuEIa>cnt!y(ca>251rvuddc -hM+nakyie;}a;S92(3dA621)|cy_?++aNWZqXQ`Us3}{RrZ`A=H?^lD1$)|5p_q8JJDHM)s2#X2d`Tn -hV9QpgpltR~1?wyHwR{XpmvJ0`|sW@3FTml|89+hChgc)O1KQ6oLv<1!fw%62E7T=;~W*j?ncuJ!NAt -!RN3e9J|+_GwY9cmHC`TU!tY!+m57TCY2<9r#s5lx!YHr%3~qRrP&3~-+J3GH@PPV{2~H$9VaJflY}Z -QPPTG{8tyapgt5e2;lXa0P~lj4Ac@^%uW<{ek_DU-Z!a%oKkV*c?9C8Lym$i8P1sOvn~+;6`%ziNE!@ -WNrMcB*uK@{gdm3*YvWFxsodCrzY*oe5}y=yar84#0@#6Um}t4sHT+J+0Yh6)b7aXFz^SwCMo9GR-sF%DxwI^?O8>%@RXp#G!m+VY$qpCK-CIT)i#XlEI8Is^F>QeXfNhynB -NG>bTEO9NICGLcI|Uon*7`0K2q}0rw|Fmun2fO>U+!DHq$)^3qeN~A>%mZR#-!{Np!|*HDNUaE+;3v= -`%UsLx~G0$bEJ$?YPMSfK~Ve8|9>A2SM@ANyJjqBZQZhu>YFJZzRzS8(#-pXGC(0Bhfz57xm7k99d -skLudh<1wX(I&~`I$@pg>_3pBJ=TNiDRk2+M47$T=@I0*#$kkIoForQb;q&K%~}`2nz=k}Svw+SAggUSUJ_SxUlXb0j}C#P;> -D;dY`K#h3j;?O=jw(7t*lW!O5ahsJ78q*yrzP8;u$_qf*r_L(Qjn#gwnZM&(-qM=xpx$-^O*8%}5q~{ -^K0f(;ue#hiNK*oZI`3w|1JodoUD#{MQER`l)_5d{|R|FJ8$XOi>l}kW_y>lRvEFzO((6#r+Su4~DVm ->)?;`{*q~X?Z*)``~UrNkZ&u$wvbd!`g^kO$M|{%KS9X|r|^yeS6beWW&d*DH50jPI$sB-jz_hG)0K?3Ba!J`%RAv!?@hF#A`ql) -LS)6rBD-sEB32pU|hnOAGK%dfmu6fpK+fx@uSNF=48_xy}$+{2u*%gw>cMIer#~wFQ@-$;cmZ78*yI1P86tqrO{V -cZc$5n)=m<1v7Ei}8C3U4-0!xj(&k&g1JA&Qo^8A2ZYuMIz@J&vs{uPjQopZP3E@lbZ#PS|rCF!HQth -vmz1@>O@Tv-sIJpajYK&lKkVLNLGvv@35w0Y&d}Vcvmjlk7cp@DCf{sy6ck8I%z{vd#W(tL%HkbLKl#}XQZiROrndS&5y<@hYLRHWRRm;1A2M611`wT_PHi^Qc8 -D_&*~D32?2IT&=AXH{~$K;rs5hUb!Bst_GD_-vfDKIB$Xj872(7u0$%AM2BGC|m*|>CyuiWRB)14$+6 -jTl*I{M&3x%ly!M^7m6d*>^KQ4`8<>yPUXkCDPFwSht>Rr+fH~YEB^=-rnD-DfMth(pUZkDBQ3u+ -aA%BdzB9&ulYss2C5h9RtAf6#|?4fAkga6CY_593!`g{Q{nVnq&Yx~$-z`J=phLyrewp*BemfoD;EfP -OtGzarAbku+RX>B=KbZs^4i&z5Z!#0=^S2DV2c?s^q}Gh`HHGbu36-z=ZO@FregF<4U!$LN3Xg1@DYm -4V7%`e|1tRe!PkiXF`+=U&L^SoPD>YHh9{u|p5#>t8wo1M*@a)3N=lBR%X(@f566iP@+AbbH -xE1dt{7a;ENnNFGpQ__f|BYf5R0YG27tLOs$2%LiuEhciN)=6sS1=*Z8JJhUret8BW#Qsg>5LAr5TyY -5NWxO&Xok%jk=u5I!+`c{P#OMjsWrfp07!@xc?oduh|2!EWk?cuv6{@9Y@;wZ_r^1mPCm`>#8u=!~NT~e?IKHp99dnl@M1MtBobCy(4NSz~TgMuC^J2t%)}IXa6g&7X0Ky- -QP*l3-w8C)5GrPz%yjKFh_9-E6#tnE+u0!#A#ON=XHTYsru^{XRFpyHc_Kr_R#GCfhDeT*K00Gz$f)1 -jaGo@F&(ZFO#8y@Vl}$s*d|Aro8#~|pLV$_#iBZ#KB>l6CH9>vpYcUwr! -M=N$9lhrWz38Dh1CCRE*=C_i`x_Ld(LXPIA4?Tmb7l$H#dBTG^hRHlz%>B;P0OD&u0w$_9;se`h|rtm;LZ{ezRfpqoU&c`mc5`7v9TE>r9abL-nezM#@52`>SYS8K<_Myj4MA31kYE{?Sm7fchV -6kdPPW-$!+Z%UTry^**^?E9)YB6gm$S -dP}&;a~pVfvGX<(~(QBE^XD(kM(X*>E-cK2tH}AQ^FK^?rAp%6z+t+M+{)K+KeRx -GN^N}Q6Z5CzIJI44EL3exe%1;Udb>W?8v_~3K*Zq4Z(^+f8lF^f=QvM?3;6HLeq5%u+9&&)7*jNBm -5&hd4c+b#e3qgouTs76^yonE_jLflrT6p}aM@J=~=@5J^@CXRB;yaXrv49Big+Ng#?{oS{Ra@C?NPuR -m<&#FJ7Bz!7B@&{Mim277(bYjVF$)ZAu9WgLP#xsW&hpf@_|dul@rJ~>H**IB(+pM|Jo7zY5UQ6V^1C -kJS9D7iQ_i+3l4CXC=XBZdcdaA6JMZ+(gwMz{p`1haVeXc>A1h5bVZJOky{HBH4wUJtjmmW2L!;R -bGCm~Po?g~1A-WS$rY<~qtiM#oj3)2TBbQK{R6Pb?hxHUC6ZpXsZ4ciSUce|R~zQ^$R;ZY=uWmD)$8? -)i~1(=P3AZV7ZO)T%&%Iqd8QbLJo4TrCV5N|HEgf#>0BVXJBvsNl6e=K6}OTjSP1^XnaSB8>HJM4BzRR4 -f$<=5Byc4Cr0;*CKc_k+}7QIUGnBwi=dre2dCacQE#z{fY^ay8vFx8_a~F+&cGI-d&|eJ1@&eT{-R8= -UEr=DwaZxiWuiyj@!ayQUqYrYkB|_>l_qHa)A4XgxPRm+fDcUu-TJOif}RCi;HNFX&s9U8xi1`E;)x# -hn~UY4%Vj$JML`1CB_zI<2E(ti3a1c}G8?JZSa8PZ#aK$g-2Dv{Dff9&&1gH^m)y>Y!E7LEoHgby -rOuVQ<_4-{)370-9Uv-*m-VCn^YsV#m{n%*@W6zhY_=QgWv+MW#@mK^-9c$W7K2&#)}MK5*Twv{Xcbb -pfI;J)kJ$W*4wyJzQ2L`w^Yg{kGB{6plk6XaQ@*Yeql*_^Rew+EZ}dYU4W0g%M-R2C;NKm68dw7uz&#UFwP$Fh+%W`IsiW(BV{;t57(tc=iJ(xg`Zy9BbJJuXS_ALkI2jfP{1=VfFq -cBb|9<5eFEO2Uxh=<||qY#+$1>fvT)Lux45gv}oaX92(C4d9p*30HNeK9JRLeS5W4L2h`OGK*_{m7BN -vzOclX$z3J_W*vaLp?RjbR4ZO@pdup+=RMpaB> -cvQ5A-d)x!~1_+|#%)Zj{}`F4YzxQ+c1pW4G;HAaCeUz7U7A)b^n8n2+M4Kol7}wPSVqNV~x5e6xN`y -Abs#U&ZzaoDywe!{q$hHM|jZwY6v#zmyL*Cz;KBOMiRvXU_$Gn`-X%Cy9nPuPyK;M`Pb0S-5fs{3_<4 -VN>mBuy?;@uwM1C%NBi%S7%{D%ajk2@v_LG0^LBQt4niiF -;&9kxFiKBfA=t8E&!YGLt&vk(4ZJ%>>DWdUBP8Wz-Jn2vJQ;vrY&(K4swAs(Mro?LE26XS6?4uAsD5k -@^>AGxR0?d;T9((vcQb&HvEe{M|Z4h14}k9I1&8Po4qOYTjCuoKxlJg!57yd+>UyDF03w`y0CUg2wRsy7N`iP_bod>d#w|W%x)~ -AvLquNe;ve+MFs#I}v;lMWs$$hKA>^e@>PdZgUv=@EvBZ04k_d7AgK6o~MlYK0A=2U(6^)I!&d{KSer -I6Qsj#`4%wc4er?mzd7{%srtd;p<;6$b$yK>e11ZiTofVav6Jnvk>HOg_FQ4Y -jt|KS)x-v73!Wr_*1Zyk;3Z!C7WaV(@iA702$+(Y$nF(m -}^5$f;vsD2j#)^?n~*>HoERf$+rbEJb;z9YjhdJ#1}tj9ugTNiiM -OOcHmYeT_-(lvq2#gY9}t#L_OCJ1Ys`gH_+`FI9=#qk{UKeD!}zS1+t -T_A({zqBlB(&K$l(aO&tkMw7l31yuWi__XG$p(ly>h?%<0;o8^tpx;^*n~xW0yCM3X|nMrC!pG096{S -lSo@8+s7H1rLy?^5Ad-8-&rg!=rIJcA8=yH@jlobVyc}LOSHIbhEe96YE!YC!pB0s)cRNJp!1?+~6FY -40s?*%{iiUa2@%gR8P+s5<2@%pn;WYR&`CuoF0xj%WNN*3%A6N@k#d#Ko4Ys`Ib+jSO|0L+$2lR6N9Z -*x=I(dC3a_lYK~6~llW8B#I>GH%JUJ?ykafxtpl(PVjE{&J4>8(i99%jty+&PMFukj;v+mVyr@0 -Gm7Koi2IYHp%V5&oTJVE>fK+`P$eYnc~pUhGHrxyPcJmr4QNl_R|!YE9k8y+Qb7^BEfiFIINPujr9UV -a0Ey_tMNf<*M@ea81>6!mT<-x>Xhf8d|D#AD=Jn>5|0XLiCuVt*a}W?)XiJ^KW`$pm+rg77`*6Ygm%V -gs}BuUg`Rol<85o+0#=#UOr*SVh=AQAi>C-u*G!d-ylt9KU5xp~kpGa9+6n9T)XSHmC9IyL`3wA%Ru$T7mJ;^ -0W{Id}HeaB2&iwiRLTK)(jQs)4wI86lVSh=*{l3)s=~^CWWAqPg+=SrlM -`@u@g$oP>m#uRm<*Xu^u|<5Om0RdRAb~oviDu(E!uRG_Ht62MLXMQ|kbKCWkyR&QvM%48An^oq_zM#x -odMcsrGm)nqRYEWYke4$>HaSUJymo?bs95LJ$s7ca%r2D?sSK`hw{M^NOt4R*R4l%oy)Xwc@PIoDAKz -GNQNCZL^-#BNzd%%zGm8t)`RlxiCcK2FbpS)W-<4~yUC^@@yoIqLFL8PfnZ4&P*cFGlg?HUyrpPBIj2ULZ -+pPF~VpQdoz?1hn+BF@U+fnS6HNZ@(MvbkVh8oSpVTm>W} -0j}(KRy>O(FIeq(NKY%Q+!T{7gI2QY?ZbAxE8uj>uKY9cM6Qt|rQYtl!tvJ)|G5P)%100TJGCd%RQ^} -g)?X)IKbIc=!!YU18SmY@n-A^U{=NSv-~Deb^x>y}caiTsI7t#1j)E|?Gv*->2%?{M50m&_quHG!xHs ->l(B6p~y#GgEN=wn6yx9rq_W82U6HCNDf^hHT0pG4X3HHVwg4nZE(B@9TH=|v!7ns8M9|5&@6rt4K=d -+o)Ur^&=dovLp?0G6U*(D{yWUoslZ{hU_+Cc^Ro}k;sB=NmiMI`(7T~adM+ljFBEnXSyC8wRwDBZ_+q -4&rePW@$*5VA8Fk?yy|Qu2C*;1bYkcmFi8l%9TS5_*rnmbCh^B_7-zG6Uw%xBuKv`JbKCt$KpqowA#Ml;`ayf2-$|+@iANhYx -LS=%On1BdV= --8ABtXK~(cfOBA6dRb6@R$6m5h2B>S@k&eDP0^zCG=Nb*$DmVoeX5%mM=sc;ruM60pLn>&2sn2i*{4s -P?NKmm{c^-793ZklASPal6+c;pRL0bv8R*mwU=c742Dfx -EXOQq6YYig-12x0F>^gsW~L(mesk~Y>q()b?_9*$@3^jAh;s`P>2{wq2`2^bDXo|o-;10zz-88{;BoK -n%9P -IVXsp=FPEnzpwLwjZ*IFr$25*WNY}+B#&sP_(nHFzt_fk(hZV>c9AsirIUi7);nb6k{FYP2}+vlXI@M -B8aQhk^50eiggK4n6+R*Wo-shtq^ -F5kJ6mGK5BD=v4vg+i=^;wXB@32~8PJ!?500#JN1e0 -;J#AHUrHiX=Pk(guiEfpLf>Mo0XhT1z+XzGIu2ZTOtH?xl=SHF7sgHmdx^CMz33S(OE=3q17J*}KNAQ -LWTB0vn{8YKjqndg@lgb#sWFqna(h_lLKM7V=*r$w2cuCu^*cG6t!;Puq(BDlsxbIAtanzi|h1=n6V@ -|k~MVwH!Y$UO`xT^gww2gljJVQ~4REkU4%YJxQRL};{q!Fb|s9DxUBpuSvMR8ht`mCwFs5vTrc;dpet -1@~7qtjTnMN{cCohkZg?-k2mQUt*(tB)tv-(L|Rr5=UcqdgO&eKMv%2BBwO#@j|*j;n4&GY&i3Sr(X| -bUeS-urQ2z|d4J|fl|2yP%K@2|Qe9j`nv3g_9WJ`l$K~K+<-y^vNEuF}6zYn2h-~nPN3X`MUvUDuv{Fm6_6X`K -g;-qkSdbT^ZZ_c!Ff<8d3v%mN{kQwGokTAbPi|6yqMp(kqs60vcr=TK?$-8T<6sG>6hwXR#v6r>HJTV -kQLq?93C5g7|AbGv&=^amUS&Sa`tqaY}*qb}Po(Zcft+Za_K1gp?iQj$uj5Dqt>rUfc9;K0Skulc}f& -IPy9Ix=8Lc(WIH*04aiJrt%1RrL4Qib_|1gGVwPmlfMPVb``|`2sHfFLVpVyZj1Z?4N(dsV1$Gam?B^ -ZN8!(tJIPKBkf3`Nk4WDwKIvZO+aTUfW%6zpf{49xhXns1f1&7w?C6V#_BCMiEux6NB_qN&VM4g0G78 -)Kyc77{W|Zt*-5|VSzh6K@nrleUW_Y;@Qk*H)#hTYYFaV@&{PBqe9@{frUFN1pXaZxT8Yg6 -IjUWp!yRm%$CcEUVnszU$z1N4lI0+X#YE~a7Tr}Z^6Pn1+{+8A_IJ*k>Q-_;heaxP~oN`+szLsRUzN6 -)Pk2eqOyusM5c+C_ZnlEQLL(>SA0(b2p^R$QP$vSt=dr8-`f5_^20S)dGiKxL=hnap#&EpRzZe(5liM -D=ez3=>Ueyx*9>5jnmIf#B8Xo~b-RT>@)s*c?x{1j+8l_Y?EU$yx{Jt3NqPC`9R_}$IQqGwlZ^r&t*7 -RqTUME!&BMXHgOQ%&D)|1O0e@~Z{*|EOvB9Cc(Fv#PEo9x~0P+Wuw19iOj##?r!NLnhuSSy26ca(5ut -Rxaum(HnV;q$P_sFDKBwjdskY@QhlHrYU*zSG+G8$YlHz)^OBf8jC%K1KY6?8zKw8#qi2sTR>R>u@`y -XtQnXU_TSh}(dF+eH5x%l&Mqe{ZSp%{30ABn%TMg(4WdJs{YpTztu!tQUdyoHZ2eZ55lT#oop=eH-d6 -oQA&0>BgJQ{nT77B0e}eaa_z_l<;a8XY*@WB&L(RkX+U$1q{&ODMu~dC0e*4HnJGSrTd9-r+*W}*b4k?-fwP$~K)AeoS6=1XZy4x;k&p$WJW^ -um+=39_GUp_h*S!|E?ryER-o7r+d$YS;;M&O?UEl9q9`N5?-|t)=@T)jh^rty4*}l3TVmoy4Zwpufck0CSsu4rqLe)e&Y}(CFaNzhk04 -M8!magj64N6wQ|R1>Cu;8(Gmb?C?er{PG7N36r9jD3ZguyaZKiW+TNGf{nGaAb-iABOY`SLyF#!l=uZ -wsl)jI73J}fod7lRT2|araKVGv7!hqe4bCK@Qo$sV32KQTwXPQGiVv>q!zP|d!d|>c%$(T0-2>kSsht -ajlM&-qwa$9MqVt67atSvC)5L}k)g}j4S@Dhz#oeT283)E)z8@0xFTKBm5e@$iyiHjEK6Y -5POvxtHf9ha0qLZzc^UM?b&7y%qJGt#_=D2U*XO}F->9;n3h?3R8@|J>yf|WjStFwNbi6~0XZG`EB)5<&ejG+^*XF9SjJ~#5{nXFv}xH4F7bYHAgkKjtHahZ&|%pmj{|ePZ;p -P54ogg^GB&<;v;KJWDx3!KXkWanI?%e2jQuIJrGy6ebfR_qx_^#+p<&26QCyCaTw!rr2Z0U7);DZ%Ni -ptTDq4CQ{k<9Ry5@(`?p@U$d(6WPPIPT)Wutxi}y*-5IsQ8OIl9H&vdDti=vv!+BvlcvL>i5DOAOeRd -WQ(k<9Mt&~01*0EoRf;P+092|n@Z@4hTkA^+`9ro1Ug=v2$!2eg*`n%Bar!XIZb~?lD{l*Cl#V`yevC -nWLM(rpOhTr@v$a}yV+M#0v?m_YPXGrbUn+@uv8z=-n#f@mR6SZK_9$v@L-tPgwp(BLufG)!K)(hm_D -6)4@K<~C?d|w;?RS(B@J^{Q}baq5HU@c5ukR|~%@8w#`C=?9Re1Rba_2&$M -cZ_+6y|M+&2(aZG6?XKvLRMa=DmK}b2gU(}qp8&UE0%n2_4 -)?z@%I1LQ=;f<6UvIJCGW{^b)bjuZcqBYUOrgbn%g!mhA$OHD6IeM4+H<1z2je5;2T@V=g;_&C>tUO9LFJoA`lcssa;`cGs++WLO4WW2=S?AUAmjpEhtXVJ>=f)^E( -r}GswP$=XTMdH-qe3_U&`cI&dH2z~2*^;oI^B&_3k>z5C*z_Yq=jPvw%(JL!tNHSV^+ANrM9j`pPOcH -A_1r-H#fuHOvxK7q3Nh%d$++^swkf917>sXdzzy>k)Uscg?d-`pNMvDjN3IEdfLf%JC@^FN6KYeo_fG -^(yM>Qtr{9u4PDEVJq!)>|ho+tBxyaGhOL?}M7vhN~w0r%Onnb)UWX5Cx{^FKn~=0{pPCWK`9pXEFOq^|i!eC{V@CU97;3zcFI(gxHG7n3s~XwoBkXSjWy -kT*9{VV;{Zzyzigoao{iYh-6#D#p~IBHUqDA#Pu3o*sA~<`Y>d@Apy%YhSxKGnz+q)Eap8#j@@}!g+a ->uB@ARR3SP&>(riP|39gR?s289%oh++$mbe;nwyOxIp1Kw^rHDGatIcLgQVxsQS)=Yq!1{sjSAJz;YDXH&!Tw^p|o -^5krisUx;e9l=0LQ^?W3GcFIKtaepWpO{f7J7zxhf-$EXR;5*To|c!MC1wr=dWDIWr(vk8ozTsdtb#z -z~#wHH`(o{vM|=CGt*|95lW(d8z(ZawE$oH+xLL$7rQ1d)S86q*r51_>d;um3=Hxovm(xci*D*8NtyZ -5M(Usx0kyKWT3$J7hX5;dxB0_G#O|{Ijb`ec;fB#vNOs_RnE3;rJ=@*HZ5T;BlQf>r#(=7hf-eJzt~n -P%5wZ2v$Z!-bPeR#w8f6XK;3l8*`0i1Fx>BwZ*#aYmHMtzh!|S>Xdq1UO$w_Ki7Z#OMf2lr4;^y^TQZ -1ep$_@q8qaUo>zZ9u!luVoR0rlK-M0Ce{=FLy^C(kgX0AFlyiqYbD6`z1S-)J-y7E1}C+9R11Bd#fX<&W_7G7#!&k{*g4+2x_09? -j~4!}*qXX?8rX39df~7{KvqaI~3MuZ^i2<{+nhDP@m&jx}wJ~?+WD=dwW(ikSS;3XNrdo_E9Yvl9mPBmy1WK6t2Drm4i$i^XYtIgagl`)C|*SxZ^~RZ)`fftuzTHUR~CZ -A$R9O7MjCTz>|i3ZH3~*PGqZk{6?xn%MPnrbXbYRwmKM06g#K=+1Ripmmmm5AaR9a81gy-b3kG=I_%n -Q?{MszkJzF)pYqmk6+|=c4Yp~$7>tE`f{?+jcxKJm_4>h_ZZUTCyMsJw&(|K`^|#i(>Dx*C=A7L0)Zi -v!Z8ZQh+U%t+796uO29aRmR+A|2 -Qs*GB0c>~4fDddIbUZHL%P@f#D|3*gxv5N|J`zoKw^ZzZ$`#c;fLVj|dHJ>SUNUWSL>VRgEfe3ED%<0 -Z1ab_2g_KgjNyvJ=W(yaV42BqMZJ?%1Mi9N#6`VEPX#`E6t!?gjJ|{Gf1W+3tN_B|~0)GFC)Ax&k7Iz -nGh_&pi?az6(WpoBy){v+*IxgKFpMKI_yu7F$H~LG`ZAc6?VgvlH8(ele7krSX -?BjxO3)<{gW;T6uNRiG9%b&mMqZ+%30AEd}bKbb9^+g}G{5a-r$Ba-Hp?Bd0_*QwT4Eei+fO?#gi|rY -;jd&YI$dk-`@WT4E9d|986(iIefvflt+k4}63xH$9t91J|DYp+p7y9FZZwr3!i}1C8p71;0)umuMGJ* -6ppTRNbKv1#ck>O-MxG_h^tk07koZ`HokdsIZSX%(8doP81^6)q(aSLraEM~WGK^j(&ITCL{wi7>e4gC$$9Y5G9 -kZT;UnR?b$bZ{%>^{sB-}dayd}eBqk`C|)7RVLf-@Bqt0n0KXXUs19Wnt#09FNY23^QHN@}vw&zs0d0 -fRkFgFpFO5rz8!gD4;Kj+gqlytryS4a@*e5O3dnh11qY}VnKtOVAXAzgWcK7UwC1i2{r7`o$NmCq);>2TcHON`|Mk^EC4Ib#I350_Y24 -2d@!!7cTNwQ36+ec)1Wu7KiPPI}1P*P#!5f&;Bu-K=gb;gH5~4qie<@^#oiy8n-vrq~W=}t6d)0UgEo -pM^|ADa`vPk3)>hp5;?u<_FdIKr3ue8O`8~S4Lo+HHY_jS>Gehu0YF?kzN?J+X?M@s!F{te%w9u -2A)xs_Ottxcm^GGno~w=g|NR!U2p7@XQj{Fq5cHQT)$cf{kkG^Eq`-c*|TnYz@)p8&GN -%yg%ZXFx4&p>@7D+dHFH+{c;TWF7}oFlcxc`f1KYv&1cj9z|Z6VU#I_(<5|_D(aaNkAW;o;Pmk2=A%^ -NQgaa?o!gk6`zd{|z4zCU&=eyRYmkygX(0(C9)|xqs%wX(a&=JI``>B7is&cufclZ9nt5nr=&XYq^|s7%Ve`QTSBksxS?iyB#nz=Ra_p0h-!T1Pb3sZ1t6NS^tZ`{qs3Z9}` -Bv*cFmPf?F4Z%Uo8pb^-LKDxt;}bVgG$~D%!*8DXuGNkoq%kK>N`_{^?0K~#d63_Ckk%x>RYp5(p}fu -i{F}K>oxYNuITPjBA2PNPm?w?6Z4FLR~Aac>J1MzKWCV8b9BUp7QWZX*=|B=dD_jY*mr2Ve8J+rqQQ_ -m%4pZds#=^n47hd=DmYXucq!==o>8z3C2(A`;f&+8mqIT)dcilzBIZCHF-&{L7on*A2Slv*${l)OWt& -RhD7E?$YRh1__5LvJ$ho)Z!VgE5#3A$1BTHGLh;qw%z0VF_Ih;%zt3|2B~Vy_xHXtH(3sACrd^9dFdxWI=%U9)?%0Rz3On0==H5JH|7AuszEd6^-}f>;X;DLno~leCPxgVYuMr -Jmg1v4~+RH0QLC?DeJ!uCUyJoc$(0j3*k{SKF9ZF2;RD==(N85vFCdt --%&@l0>$bd5yS#PT`C`|ZdOyPEWs$}JbxSqoXd#y(s+*$gQFS~RNbq9GjD}9sMGZ}}*UweY`0R>73FQ -*S*)Fn|O{ZA}rkpVlVaO~TP95AMdwpFd;tE_&K^9-o7l{t9Z0r!YPbO}@j4(X=VQJPFRAIc9(;$Wjy( -U~QFYAsaNKQA_@iAU4;I;_OoF74o!X>hwI%&Ct2V!pO1cG^ZeaYNh9Ihg9k5|g?7v~n}eGT=L->Q(Aa -43*aPyFClL{(qz)edLj5N_QMq9d@33U&SQR= -Ii%9W6uUt)B?Z>?#hwN)my)`4k1b-z-W_90VT1k0*_Hd|aJ>P1BF_+F1~P7h%(i1Z&4W=4sIubih+K) -gF$$fH3W~r7omoQ3+TTw~8Zxned^|5N9RO|Dzop0dLWpIShgHj7@AX{JNvqZzjwQwzygC~MW94|dn^6 -erwtW_-I%~(3OO^!?2!ydtY}W^%n`AWPk449{lg_1TZ%^YbKR=5x1vgOFNb+#CSh*euwT8NAxD4cl2J -ACUoy>(7IhJ>xzB4|~&|p!%kyAR}oN=LAW9)<_i&_^am%lHA3o#FSEP~r=B+~#K(Nmxtq;ovtu(@byB -j$~Nki-B=JW0FPoFKq`b^H5`gntqm@ni?#SE7*LD!ib|tEq+zSzVMdCd ->k5YSZn#F%$zpCf%`Tt~Z1>A4?R-q!uot7c*5;KbLQ7-h>rNYv;n;o^=e{SS#Hy6hDS^a(VKYR}tzHh -vRoEu}?NY^fE4*h4uslKoH!V9|69YR4{qV@lVILZHC5vR{(lYT**coTgg4G6A-z;9d6bPXe+MMdY5(3 -F^X;lo*-v8qt*1#qwa*=Es{2W-^2x!ta<@C32$VIH)kxw&HkJ{fl6)hylL=!3+jtt1s~}aO|&L;z)r=2qAWDv>VQE)+dY+vKCnK`j=<%`j5+30r9Tn}e=9)H1icKXcL#&wqkCd#e%cyu+s!E7r=d8L;6?eg~F0*j4NrZ< -DYC@^995zf1=Cuja6=9IZ5r{6A5jc(hEu*cRhXw7#gE}$_Q(}}{s7m;BUi&Up4kPSR$t@bgiqxy_HaX -Wj?MzR(>1-Hi)RPCIC)PuxL=~>^kszdUjCJ>1os%HfUowiP>*~tYEmNQCBP5^4As*J)QN#xUs{j?OUB -cf7eC~=E<8A#E*XJQF%yqCI*I=9n17V5o_FO&10~^~;b3|z6`MQjL*4;~B#w}ld(03cfSUHkQ{duUG% -6j_VGojDiO;TQBs-#AB0;5H}LyuXoOpm;RNcx1m!2()G4Mh_5#(J!mc&J0q@DzBxEoGz?qC`rM?7V=O -57K7i7=+y?PJtd=(KE2Ibw>!OR_=a_$wYk4Ry@I{*lg=(9^~SsS@5pX65mnbaHP2S701C)A24?Q_@nE1qEuAdJTEV&$a2ZQ6&IB4 -_?oo)vfJVJ<3nq;Xj8gFJZgCaOBUbEDxhj}^Bxl3D(9m-hlMr8q-+pZM6*Q1I^87D78~+8JB4&)Sjjp74+2{jR@D*YkUGz*YQ_I3 -Y_PFBVgrDc(yELvsF-u9tpJ*Z*VEw6hf8*EB`{Eos`n2L2n;v_B2-_ehgIB_(raZ}Hckfy}Zu_i;O)8 -9=@n1;d;(XEh~#Y1A%o(uIn1|0pU%9VRL?3UdzY3OOH?T38}b&EWe<-R|dF-S`AZbc@T(WzLGj;eHVl -QMLs3D0orvx+^V6R*Y2Ib3@>Q?OW$&hW*8|ROEQXh_GZ32H<%O!ML%78CDixMl<;t-@7qFA8+4bu9~P=H= -(NITbOFf3Zd$*N86C`yxPF`gS6_wVSyWDO$E7aJ59gx`nC{(p}LD2|J?#;X~`MEv$u6Opp6xTh|#YW69`#jbuu6?#}4YG77hmK5CoTl -lgW$A})uiB^pJ$wN*Hd>XtvrJR%Uukh7w@KrG-dv^r|0yy28Pz66HoYa+=F=k+h?q$G?B6;oPu=lzaR9Eb3RGC0H{gcME01+_UIms|QuP1eKk_c?# -!o6So0{bW93ogfebTf#^7l1MU$8C!@7X1WTzdQE>s{VT34~T_eBuNwW#e471ScsRf=Gf -uVTAnD(1d!ceP?gxf_)h~jsGB!eNuqV-o>G}d%K>^Gx?WH)$V4?&_8FYc&9vm7*6fenzHBP{ -TH3W^DM5+^<$vmSnraJtOR`YIasUwWvYPxCMmuTmwxSDWxupIe32CYc1-7?8Tetiv@%TH`a06~o^8mz -sHwk5GV;erXRD9Cf7^2ncJEREzP2ey`q!@9e=`0#r@;e=(DkL<+_uAx71A6uCTp(Ey;`P1l>%3^OzOp -l>>$qW(yNrv3X7bmAd0I&<8*C7164ELx(O+=;qDAXWR?8IOS0-h5413+9zjZ})5%o%xM3lKf*@@0Ppu -svR$)YkWo-aYy1NtCd6eE5TNF$xrgRBSc|I}0YCW#*F$`;MQCzjY8O&Lm!Q8X1$jX4v6cN#b1$f=JaALnJzX8PEBe~vCqcAA$gfCw-5RT|69OpYVjx>?0F$Z5rYSaO#k7pDbs -kK!oK9Djxg^yf*wV3P-27Xf0DL-y;DPR6;~8eA6R(=r|HK-L{Tn2gOs$Mn#ZfQCMv -#`C_tt5F2+$@&5pr)7kG|e7kLgN1#UlX08XupB_YpK5@;D7Plvn -{S1HVHQE{}~S}%RD*gjD@6q_zmIUGJ}chM{gX^DmLnbiP%Zw)l1kw%6Xet;tIW};=s27D18+}G%KL!V -yb*3XSQZd{~Q8Q4<-hO9y>=oV?X8y>U}{p529w+To{td|`Nd@RumD^Fwt8b^DO+r)8S7%K?MUAByFA| -7^&f#l1_G!HmE!)K~!(S=`EL`8{Fd>&>#=$<#(E9&#mYRo0Z$g@G{?nx;;hM9WS8&lhD<_ndTz(P^&H -NaqjOfC!0U(eo%3tH4oH_+9?)NSlk^nysH{H@5O>A=c!JFh -wSuL9#^Ro1W7&$zLS9VNuvpmTF=-o)`2+qq*RahnHLfz9wE>uQ3ST&gU4*gvj^V?F@j_8A20(xZ4+iF -BVAH~9n3lJ3I2H)JGHI8$k$}-D^*0Lz!Fq#EPn%!_P=yH1=X^}JS!a*Z*gf>#j;I@JFaC8A&7F5A0f~ -UNlAm$%(qT~Eb+ayi?<%MF6!OCun*L{5Dv?w=WJlmtA`0dlOt8{FN?k8u_J|l#^<5D= -?5ALxmvKRlhkPpHBac$3`8uwY`7Kn3TQ0`zvFTgg0XnfK;;TXxlNa9P0 -=E+<$A&{1RP#jq}`TV@m6fao+81#`f=Odw=U`fbSpYcTe+AnxcB{9Cyb_-Dm~7{p4_7X!~{8g6YH|I& -jo6MtTf|-)-=My@Wv-RtCO{b6tpVED9rZ5ZfbbEDv%FuOapbj#JYvHk%HDiuW?4<;bazWLbOJz;F^zh -&l__3%p+u4oXn`F0E6zvAE;u98>ujIYKOraIfSp81#az7Ue;>&p0?vgI0xImfJLE61q@P<;Vc6S^K0QB!q9W~GCoX-G-HeC=@w2mtH$B -TwmX$*i{A^T?WkL-f|_O=&8z@oXuIW9S|?DVINV;9l&1P;WH0wbPc=L3r -{LUgvUO0-o+O&yh=Q6qb~G%yI&16$*KoR^Jdn(DBaxAr;}&pta^JHDz!~}17Bou_ -T%SI3na{V&0_mM}Q$r&d02r)aDy^vO^+dG|{kR@LnR1CbbbH&FF84VP4QQ~hP#`}{vV)T>v0SfQ7bZ( -^?aTw&kF~uY8iA3^nZUOwiO*simGF?gQmLUlP|P}itM>u_sdwC$rl`;WYHH>L|J)n+&}c?PK60gm*I|w*_4<_9cKFscx-$<=8svTV?Fd9NPH=wT0$%iBZo?`&6fz~j{2e!(V^q(13RD7e7#E5N -qO9wRmOps81As&%BgZ}^b17|edQya=mjhY4i3TfA$l2p~ -hDuV_d^=;5_2K#ADwp|Gxy -0wk?MzM>eTwU$@c(MxiWE=Txz;c{l6gHiZaA;Wj45R&{n50( -4@C40>f#&*)Q+ugo%K^wz@;Jto`K<^|{y3aaO_J?d{2PW|?*Z>^h4*OCAH;7JFhqYh*E@bwPO}*O(Ti9+duQF~z$+WYP -T7xt8Ml6`r~!WYqa=n^%k%M_V*a88!MRo!`Y;}$Rv)j&SZ6!&s|~F4CGlM_kjWn*qtqvpf9IH1qdZ=9 -HvUK%oxduGwNZ2YrhxG=k^_86OdYq2cMrVe?Sc36F6ho`7wre~X^=T;FK4B8 -igdaX3$91P|^3C9PWz1mXb{n7DsoWaOqQhK(Z@IM(@d^abzZ>fQtvem?U7M95@^vlN%}g|BpYIk|Q9S -BKp9Is6V&0^D4?{1Ax|HV@3{t#Y<|IQZ5fA7bfjtz?@UO+S^^Oig-K^Pkxliz5yKVS@fsc@f(ZGl#=M6 -BIR*^?B@~+$7y6B|3yg-rVcQ1Nkq`%f6xfW)YVAx5-DS`wD40IY7dSNeCo+EcdaCagDXJs$42&>D15?@=#45v?%^2@8c$A9tf<68sXa32#z_*i+=PxH -SXIkfn$vg+g?JhTlXWo*t95;yDr%+%w*xMq9E|IJ$Ub`2dp~?1NJO+QF&UkI*S8(y5w`QT}t=h^yzm% -e=G>%}cZq0<1UX51HQh#tY;}|& -to9Z@Z(QP}4|+X!sv<2c)1QpgxG6V@03cS>K})!kcZ#0iEk1^+HFXy8E*U~4Pq$~Qtiob~EOMz+MK-o -ziy^RSqq*Raw=oOU3=-pGg_joCxWRJe`5MI{I|B2$^$_|Vc0{j2^Ci9X7kM}s7sL=tZ@!vcgy~gM038 -w=q9Vm0aedHkp2v%EEpKZ3Iy!gy*d&x^1)$Hpki4k0B^OZXekr8ArPo0rz3Eg70(!?L=Rr3u5b5z+D?ijG1EwPw)^VtjBA -?Z9<08k^T}$$^by+TjJ?-rB1qm2rC`e0`p@O3-12iP&{Gpkl@MFQ{3cCb&f%5mB6BL}QM9qqx=tpcS* -l$&*JDnt3vk^oT92FyAiQCuvS*hJZyEpX_w-mZ|nmV1ZPi8aQ?OC}FkKpueB|OPimxdBlCBG1bQ;e}w -;X(l~Fsy1Ny7gGSL5&otR-=+~=Gs{9jw{a;w_gUo)i)DL_X#c33#2!fy}6s9*iys=^k`&9auj`t?|eM -p?#({*vQcbbLd-ds<=^>rd*&l@K1p0Dt88$K%B8{KxP^mm(o81EUm_^q=>;yarR-{yYVJE0e6yB^&35 -|#cz#lIl1``N@U_70(Y=UPbbt#i8oQL;Cg;P}o@w|i_4AHP}lcKht@M!lU3y*cl;!rm$O)}`Bsa7^vL -lDE7NLH>@w@^1;ZcKIN%s-9h{;5HAwiTcRr>A?198$QMPPFRPVb@l4QU)6b7&32Bp&gmab81dDG;j5T -0fz7(W)i=7Rp@#@b`s+o%IU87q7i%9j|HgK=9mn}|!5`#)BtZYlWKEuVOR{aG_0P)d`r|QOjDI4swF& -5!J{`Xr`yHpgj=Z-*(ak+R72S3x`D<17IM20n@v2Z#HhyZ=0g{gqGQWT;QrGdq>KjqtcehHnrb+xZWN -!Sq;0O1%nWub==k15j?iE|$(~|A^U9x50CEN3nsi2W0{7@x(ID#G9f!HCPz%(7j^9p2$frXl5XsrOl_ -;8ymsDEkKE|4FneC>V1d#uT0BU#tSp@Oek?^R-V)gd*3d_(d~fa*kO#;@kXsyw%PfG3HCV;y;^#Naih -PTe1-b{}B=kX`Qa>F`|VBU$UXsppX+L(CmWNb_}KjzeT3S51Chou&zfS9&@}%#wVj)CHv}n*q5&8|io~0MwZ6C69N|W+4!QX8tPkh{Q!!tX9rM* -4@God+5HrT`f%QaTT;Fxz9QfVH;X}EOlhj5tfjF+!QG1n0Kz)r`JKi}`i -15?^_tP=?!8iCAAwMaJnEqg>q`)pa+g}uGh`*J|*CA|m9$)oO|{wNMkgIZ+!$ez?uCJsc)1QI}G&$7Z -;<{4iHt~_?@ZOK+rFO7g@OMg8Y=*Vgqm!FTSKqM7brbX^j5c@*f&c8(qV4cS^cqj80jxLWnruvXryqB -$AUA{j!BCDOwIG2=TNTg7oLtcL^(e1`TenD5)n`fXf2x4VHCab!Cy#4J8qu*^b=>NhN_}?!1(>D0?az -AW^6iHDKhCwg{<0K4G1dZV^PJFf%Zd=K=_mRk34kddlrKJ0ofxmZ+l-kQ&5w#Eg6XYM*=W3>3x(~B(m -pI9g_of=Z+lSD-XK~w?wtW?+-hJ5A?iss}#1VV9V*E?Z6=L7`(sxG_MD1JNE~lUDlX+3P=WkGKujj$< -RMYNolI#L6Xu7vaqHn=~y@{K8?~=)$tisI(vDvAB -MYo~9BW0Dw#R%s`5%oi*5-seZ+vjFF2FO%5b -!_pi#k@U2t%-$Diqux8HwMv-i6K@0d+`eQ`Bvo?9LOmB_G&f}`@Qmn;@|%sG$cmhwL -jC%EtVzud*jP^LLtA?FP`O_@7_ch54d}O(I-RD2GK?jFxyysVe(X$Oz-}zQ|4u^O~;#*zZ9B+W_y!}4b?+W~{){_*7 -BoMp1HL+3O_c3uxPF1A)CT`K$_ys%g%Z&j_KICsH4`E&)OKPp%@d|q2u1K7j$D*Y1cJFt&d%(jIW|`+cjuMG+1CDTthMb|DtJ*uAq3g!Bma-S7#b>;(1l?TXVHT%A2zc?K(i`? -u4F~GTU{WM-sLYG)sMlleMd*C&55&^}wHKQ*;Lr-@%Ol~gG}Ia8E3YUbzNsXTTQ!dvtRQ^p%&P!C-_5O++jt>GAUag1kbzjf64zj^q=op|8RwW%kH@!*gZ}m -G)8T-p1=?s+V(C8f-xNWR8etb#5lUQnQi2EBglJMl-$+-Giq1Vir)JMOupfNADjH#t@@pl+8gNJ9#`8 -k+md9m=c>|dr_{TlK(_zdtE>2~w!NpKwvB}N#oiUYr=xbU?P$LMrrw2EYA@ODDqzXpo0q+P2*};o@I7 -Uk>;VDsX7?ff9@Km%)3%ehtDtn>)}UnX=!?+bvU_#kyP|J1PHKvZ$gK{sW4pxnzq0%AXLi3&6a71S{B -3Y(qsQ8J^jL`+eFip=Nk7D}1?1a?w_6STymzhtXz%*DhWy{{U4JTVZ+lnrZSS&Ng9m=fz47`loCCi(3 -7n8R2ys%$nS_Yq0?<`Vq-Aj(TxG7EhJs+Nd=Lkm>SMSnuOi>2UKv%mKQ<&v^Wd_f@kx7n5oVU*>sbJU -EGkUP(zfoY7+4h^rx+FaH7%;{bgR{P%Wnj&ztIW+O+3|QYt?7;;gQ4>q$g-b5-bb5lglwK?Zp^(zs>IXA2k0;U3 -;sx|NnLBhUYg7+TrVmSAGJ$zkk&S(EZ|yAB1EHn1uFzUy7nh7^QHWq7Vw+n^p*fCTSESDH26V7@-h|` -V{|Qc&FPN*^QF777w&Hbx`3hm4jyQ)?jL<%kkS+HTyifv7NH<>IB-;5y;!=Z->C>?X`mLNV@w4yq)^? -0syk3()Q&(vWffx|F+-($#&WQT?{DRll2=C)6kv~*^?-5b;bSq_hi&Y*@^hA|4-~li6%QP?lOt#j@%o -T!|$0bGTYNMyZj^Z+y38m$3HUtz(1E`uUSzoHOcTw1@YOehvP;TWB-2sl&?(DPX;dbarmxVWqT*P`73 -JTn~l)pbv@cUHoVWBbp^1+?cl;{5rVh66-)y2q2~(A`>_ytzkZHT1NsSt-me4SpYS~rZ8-R$E{o{zEQ -gF6{Pja!_S-RFKd6h;PZfY%`)Fm^ef^G8{l1%geD2@Mci&#@06%?yzRsRM@ZNZTd&v8un=8`$+}017k -NtJvhZDH((jVI+{;ths?{WdYb{wyTk=@rDb#oqM1K*4%S+~?wXPN33`EA0$P)|Bvnd0C$D(7jzVb)*B -K`?2mOS$qSJ2isFcJ{)ja);B{;aYS3^p1>M6 -65+WOqq+SyJ?B;|k8oag@+C9$8=r-bjaG1o0=F*Vx`2;sn?*X*$SOG(Z<)3l --eNt^mxoU3L!Xjs`Bk9gs>Sf#-QscQlNc~y*di+Y}aPO!(+h0<#4#kkS*iel^J0o=yms}m8Wk9Rl8B)S>W;(@29vDm(e4v=F(5k>-oIJSWnP-0 -+Ep(l-tqj@}Rey0b+Z8IMONtB0U0?>yt8qCz>8TQZ*xOS(Q`qszT>=hzox0yWaHwQ5j4<|b-c5s+Z#kW4GO4DP$0E#L;7NSqv$L<7tI*R=H?) -#IY!1>a~Pzn@ufU79)Tnzg*vf#v=R$*KDhxkpvQA6rpzG*)v2wvSzyJZ8zQ?q}ZfM2#J3z3&1WFy}s` -`ix!d?qi~!icd0ahq|?Ca(6=CNvQsS%pdrPloEsZDRg0CenjG-X@Er0zR)C7PQERjy -9Ic64pqtBa6`=PQi&3@@(E&yEm!dp|D}fzTFd9g$LuEqq>tvzI&T?88tTguM&gkW;k>QIDEc5aJZdc; -fDb4eBuh{D_V3M3IS5VC-9MSz9T1%?dC2Gk+nXe5#PVa&Zpbg=a)~z{s{_qOG2pYf_js?RqQ%Xb_uY2IQF- -aTUlJNl5(&c0VLP{k6^>580+^bvz|FyR8NvLT=;!*dQJ<`L!+%_X|YK%)p%r2BHZEk1sDTc!Jc4 -cm4Z*nhVWpZ?BW@#^DZ*pZWaCt?I%?iRW5QOjh6iYp{6`QjNzJ>P?l8sr(A4!UM@!9PU)Hw|^!+a;4& -J*fnI6iMUH#=R|1fc6W&{P^*7Wo6;LjVuQ6+<$!CoI(F5-T=G$gwu%y=t5y`lNT0ENZnriA+IkzZ`NZ -4O0aGLl)nT3Jv28aFNCP(uk~@6NXDvfd(lpdy;a5ZL&j;)hlrv>;?Njryt=5P)h>@6aWAK2mmD%m`?D -|4^!j<000*T0018V003}la4%nWWo~3|axY_OVRB?;bT40DX>MtBUtcb8d6iR5bJ{Qvz4I$}!XYr@x*? -fflF7kIOyVgu3^vVh0%5O%T3d1@IfcEHe-qYK+t8Vx01y}xU5W*<9_QS}BtI_!SE4 -+j4cDfL+xrUst6jL?O&rBhLmYI2C3NN6R6(mf6!jx;H_^L7p+yE2lXQ_x^$dqTb-Ks>23d|b8pei)Tt -od^r&R~RE)7dQwR4j){waR%yHnQrH;2=sFcxdV+%0AEw;2 -8*M9C%WkX2;0a-6fTMe;o)!;yUnUKp-Z5=G20S=tG{7^e~rFIdCPlvCwZGaZgSMgOg8#vy%b9ETWzF7 -Wm=K2VX|F)9GYN7T_kbC~e=1QYr(J`Kd!6XYFAnxE&>yQ>vI4%DBbNwg)%uK}*<^vDCZ?ptbncHx!t4L#}c!Jc -4cm4Z*nhVXkl_>WppoMX=gQNa%FKYaCw!TU5_P2lBVzTSA^7BEiEMw{vFZ1GZN6yQ(79>qR^=K1{GCF -lbWKE$toJLzkcm`ENXkDVWeFylB|>Gc=(68*=w)8?&o*E`|ba>k3aqMlP|WGeDY6sf4_V6@YCJn_VE4o_BYy$_kVHs_RWjuKi&Ob&E7w}eiQ$E{ORG*e?-IER}YW*`i -sZA=Wp*`Zr}ZCi~sRa&mU>)r(feyKfK-E`{egqInN`S_|3O}uzmFE)%KM4zS*AMy}5h*``ybN$tMr5- -#*@d_j3&My*I!AL%gzWe|i4+Z`)V*um1LU|Ks-l7Cn5sy?xle`t|J(53jHB$5g$#|L*bmZx7+jAFF$;EjL-l0@bdoqUwP^0*Dqr@^!n}Hi$K1`1AW0cdy^vZO`9qKk?d|AO0~@pV5)8ua0b=Jw)@)?jN_i`}j -N0{`=kIn|NibACz}byIQ>6Vy)kM{+9m7YW~Cre;=Lxb$b=7@%ZkOAN=MN{9~jqxBJ&N`$Noq{1OeuKz -|9d+P=HnetvWJ{m-v%qlI|y_ODN#{mYl%Jlj6{;;-Ake)RO|M_)Yq>mTF2G1KwaSifxP{>Puf1Yd43s ->kQA-~JjMj|Tqo>C;dCB|h=d$4@?g^6anl{<9~~zWDU(ueZ;>e7b$Kef81PXHP!)=JSu9ZeM-#^s6tw -{`7-w`}*$g-4rvKzctDKXNtd%CVza0nS6Qo_WAv*H#zjb#`3<24!(N1{qX$ncd^Vb?(V~)w&$@$e){$ -QYYksLy#AZxjgS0eLjSnkf4{wccze73@^~M%6V~@1RySJt$NJue(ZBfMb{i$$^!)X|y^1CHIzIl{{rA -!IXRjU}9&fjgAKtv>t$+C_KD*aa_V-KM%eH;la~_&% -b*`U&BD3J-`3u`Ro50P9`?O^T+7oSC0=rKG6Tzhd;l5asMjb_}_kb`}U_leE8unzx?vS-OHapc>BvAj -`HD;@sY<5|JnI3_1L!e-~Z=r|De}8OT1LaeD~5=$Ka(=>bsX#&zH(x`}NXswxjdGy&u<0=Nd=z0Z(4u -;XdA3H2r(ZG;ja(uCWi_EAL;w{(k$X`hmXZY4q{o$KUwe>-b=F=fn3#-^=^)+wcEr|Dfz6H`1>^g+Kh -w$36d!T~Nn=!KAWN53ggJzIn@L`tj!%KWsmQQ`3QjyLtMjr#CkrKl$_c_Ss**dh*FfpMU$OCtp8%`sC -wpo;~^Ui*KKP6#vU1j992oet7yhr-qxe?i -L&2rxZyHy+EIFZsI}WxSNs->9sgYMgsu16uwedL;*)2OALUrLrSR7|&f9h_-dyKx>!aM(Ia;>c>P?|r -@rHPM8~bgiA6t(H*ILHy*7)Z*qaQ6ESkB2~wuAoi+c<8c#hrni@z++f-G0P8%oZ>Ew-QfpC3-aN<%)J -|jklKQTRgDc*3Mu1;hUo;BPOlwXW@OF$++5`{U+wf$!iREr{^s`nd3We>vG_2#E0gL9`)$?5pUUIdoy -#f7^OD4Q+d`k566BJ!-$#L2gBW24}RlW@vi7}=`~toMoMM&cV~LBBAx!m)aHyZL6hHT_nbHJ@#xIJ9? -n?j_`!-}6er`GeefgxI(O`$xJL2l)@XdO82JbOd89qW5N=|6;~%vhJa}MiCq@wiI$JEoepc2w`mv&Kr -N=zPYn{g9ExlufGv@1z`HJ-|*=~(>D=S{!&oCWlwb1=NCbS&g@$D@8&LAFH_A^==3_KPtjIA>+%xHwc7CsiwxBF>lv2kun>~xH?N0ZS|tUva{jt+ -37T`O?Oo6dnvoPY5@r9u@COP&Gb{?*EaU6NndV)y@`RIlV3HdH-)#fa-SHIhRKhf-z-ZRhZr#J5Rk -lk^UM0RXRx1V+-O@4J1BCmYokwegw -s`)*OcPM`lUqFhJP9AwaF_e7jT-%Jyi5R&T^eqnIxRXMeW%liI~+LoPLs>G#BZa!FN`o@Jo~keW4z3- -D;^7QteCP9PY6?pPM7c*;d%!>r(Mjn5MM{kNH`>dR;*zBv+=`cUF`-AW0vD_bnB=(9~>SJ?gzeL5}66 -&3wN;&p2c1XQ)Wg^!np1#I<}5%x9@xSb^SFx>M^S^BjSyT|KqKM_=AOFNMS&je|6^UCB}mXiYbk;*7W -NgTr#6uF(3gBz7|Vf%lG>u*6ob$mxC3oE-e-pgXOPyQGo($xz5}dJ#a}h$vhCr1DLZ@F^|QPEtYIzg) -C^(hw2S19XMK7UnQI;m*JM+wqg`hHfuGN&wEzBx4k90xTvg*y5=L!wygxo$k|pOuP~R?5 -_URZs3vv;EXK{h9*?t;S0b!mKYax5YDGthbTepHSE*rSTe$M7r!vzJ>%v8mjE5MQ@23`i^l`afD?r7e -GoJbCKSlWqhlc8@8yg*OuEX34O`511D8}=V?bd<45;ZE7=3g#o-lDk4m>_dEQ=8m?qaF}R$~vlud!<` -Ts^@hF)W}xy&SGCkbw6K{11|niiyY4FmU`sb>?ty%P|JjCYLVu6YPtQuSh|?>}_VYM6E6SqlsHc-n}uRM0`#J7_LV -1CV-#&V)gj4e9Y-KlypTq)^E49=_=S6Z0|cAok1?1>*%;F1j=Q;B$Wxo0&to{ss)_8a5|1j=d3%`~zv -j-~Tqa749c&zcH#s%8L^=+a^g4kY#2>_DU*tGMY6+bn$#Rp8ztzgCp_75x68igntOn5ae*U0Q#!K!!-aeXYMV>uvmMTA*SVIT3W{_+1st -V{TTJkeS8EiX;*(wRx27h+{Zj!uLms??~7U8g@)apv-UdcL-xn$Cm_A%A#h2rCV1@j15`FTO*TNCm^5 -SW!D{Ju81n)XPZqgj*wd$0W-0UI*nBr(NFCgzDHwAkmQHL{!hy&Pt3%u_yKNXj2~eaKWi`_naDC1Gp7 -14Lu_jfq0lb;+Dggunj4{&H2DL;M7L70lqNQY)$v^RRnD5T?4<~%dOn*YWz{hl!F$3iQ&mGMEffa(cm -hPAivJso~4UVJYLc%pAemKD;7;Gc#Rwge#4kYL{$?`DL%EX@o*C04LjWG1GX*Vz|hfzK4EK*q3Z9#X) -cH&Y^cw}BXFQg@hG*- -aG|zX9gT)!#w>7QKvQlF$wN0dVP69?uy84$F1mC!<}4ix0=WHE9lVo-z#r$(MwQRr(7>dir!9eDDM4O -W;Z93t7OUH#*oPoDUp1ux`We#AM*4`kCzpE(xQHg@qX}kTC%X$p6_}tvS==0StGNo^;*+bmh2?sz`FS8G?qDEkS0c9))LyFu7opiu|3x<9Lh~0JXTo2*oy=-d@Z}Z@xX5 -28Mt2f(E*uQAUYdqNx}bCaB5p}fEnlGO%dNVa$z0VArm|$=SU`b>%Pl#`2^+$=sKyLx#q;FtY8ry -&5zKFy=;(P@s61o@2a%DonJ~sN0d!6zRB$GJRSR-%=4wq!Qor#?Z3@h!d1TN{rc_HMyfa~pLP3Rj+$__85 -!!`!NDm@zVkWIRW`IDB6mkwYK`yaTYH6_K!(aqy9e!iS(O~Rkqt~U-9|lt>~&7b8;L`yL3%`LF#&|kQZ&TwWtZ=}M#-!utir^B$Vb)#Kzr~ -HoyX~&Shvn-nkaRT$gpmcM8IbwEQ){flHzd*xaJ8Wh^9HjUClo@ujnb`vN4vV@4 -;KRiGqZshQ=%C)C=m`3+oBx-Xlu@Zbe4f+KI__@kI!liB;)aG?nCnONmpK-h2xrC#j@E~%_rhr$~rHy -!;jECHzxX|jX@Y;Zgfh`E!yH~d2}d$o!;SFT$NTv80|SZx8r4wGtJRQN9GG$KB3NDBVgsci9ADoPy+a -TIMoyOaJmrY7=O0wDZ)hhrQt;sdI>W*xGSEdkXRbS2i+;9W}<0Tw(qWG$6}TCy$@&9iR0S?yQOWhV~+ -%)pma(@IKd#cDxuZ$qb%??%ElX;SPV;k>flNqp;!0FX9LV+maVp|CQ8S&gnzzQaWtBlU44BNa-clFAP -W+>1}8ghRWy>M9H){ljYUljXG2!;Jt^lW3Lr!C4o#iH;5&mHF|J`=WtMU}9q)KxJSi$eYrVlO=VllC^ -YyxbuMCQ({dfukcis#d2KoJFvdtCqY2M?d{Y9V3H)&V6nPfRcp%Rj}ZDDcA#YkyMNW6$wX%y>hdLUL~ -xhUknbYT!RY5s8_UTsGXWSc<{WQlX$YKAMHk{(=o8xwTvEY_BQh0ClB}tT=WHB+g(GLhW4LZ%!vg6&$ -y1rkF99cmz~vu0d`V$^&0>m)qn_A`q$P%`C9$dod!psc-3aS}i8n#}DIJpgA|8CxnEL^uDQ;KrStDc? -fa!3T*Gwj9vg;x-#YtRQc=IeLGs1Dr1ANJF3t8u#T7fyBsSq1%I9v+L6jLZ{1R!(momKO5k_M11NQQR -f73M)NG6~%-R0l9XLBnvAX$;Z5IS{PdWN%7%0Nil2hZWJDb~dQv6jxk=0=AwCMS%#bR#3NM4F)Hnor9 -FDP?Nz_Vn}ln?S|0{r$%ZTc!$&!Pv3SYl=w15?&L#DnvT?hP!WH{pDnq6O~KE)@J!IgU -CSWsO9?_z+wQ@B8$?TTAGHybVx#w_m2}@70%C-?jIIj0cwo2F>?!@zp^zloojdDxIC+rV`ZJQ$3F|80 -hLjI4h7o59O^KtKIgllQ8Fib7oAb>1oz!u=fFEc*N@c1HYo(E3=SQILUh5km&Fw$M -^1}^DHK>BWJQ(^%3w%GSm3&lHAkF7<5oQMRq-}K2q$ruQ=8WP66TV+> -#v*-l(p1<#^yDQ7hWky4R}%>5RY6!@1Ub*25{Cm~}fLk$aP4Q|A0=^)46tep)v5l?8kH$xTSG29#``y^SBUItfk!%ElJ~<=wdCQO!3a1r6F}Ee72;?!2F6kG!*q -E__LXHrx^Wy6PrYJ0`VII7_v2>xMsPtOR74dcnX09m=uZ4AW)7fQajxRSGx;-viJt^251(>w3@N+?2w -bZkby%>cN97eezdDySgZ)!GzLdRIek|RN_l)4r9~_AMX=DdtHW?AOj4<#2SUq246GSCY -3sGHAl0SFt7M_!k3U|wO!>wCnY1lx+ck_V01k;HT;G~pk`6)Ff!V>^8f>|%XMJ;!`+zn -d!jE{@Z0Kwib$W%Y`I$Ymtc#?1n;qtAT-O`%`zVmzp5i8nxSpl5Do_}>AMTS|2wH>+n20VDOuK0d?dT(#=r(s(Gb1!F>%a-zI6X$>8AJR}REdyK -_VI0e52JZGSfp@FkEO@geX%NtBjk5dqNAsTR~Bm?3x-V?S0%MX4*5TL1+CKuaDRX-Tvz8Dhf3R-<;`x -@Ey#B@b?XS-ErnDC_irILqDb2UvzLsr{M -G=XsQF)sU0;=uh!jNL4`<^sU^)F{UJP%eIc$ObcnEcX1jq)+S1$X?oS)XhZ|Zl!Ne0&=mI2l*B3sw?! -s)ULLk6lYK$4#F8UIb=kb1)j9Nt%ew_00)?nCv>_#}@Hq7U75&)cG&Y?V~X4ublL-*RN6d8P&GR(A#?iCqFpYpFwQ^yGc*QRn{$$Kynw9ARauru -g3Yu+2v6;WZp#WiStz!jbTIL)@4D@2e^o48>jq -!arO{70Tx1S{8PY$QU_q!V0s?BmF*jgMC&&%}R<-a+gy}QS2jH=88o;GB3tou^AbfQL(`B~~i6T#L*A -yb*qJ=K*B>t)nq>roJN_=a2EXIN>8T#wx#{q6`YuE|wq04pbdgtZ)nBV*?=9xS#O1s!^#1{+3Xrv1XN -Jn(pQBn@U0U)WtYe)Cl1H&;}4G~OHq&L548IRGu22yeX{E$sDJ{(@n*T_+ZM(kr4xK5aWb{NL>cG%vm -SG$2rhR(QA&BK*-$_^VYs-qApZm=#BfSWas?Q%24jDQ+3^=n;O&H7MP_e%a$@p>IC=)j9L@L82eR=~B -a_#*&TwG2Bp$S{zpwdic&NEg>}VXx1wbzls3-d1T|e`3w8$SLq$v2JgHDUqA_sRI?!WfyMa -pz#iF7Tpjd13FbL^#KU7$_M$iE`Vfe1Z>yu -E3swFP?c&6*~1X`boM<(WRP=9_rMrc&JmO01aNR`(+R4UbklL)gl5h3Xw-+*&E)mH7(gifib{^RA{nV -)PXc8KJZzoQA7U_Fq@-icFS^&-Jc5EDve>aGP_$- -pU=?=5Xem4Nq=|)P5^|%Oh$Hn}>ItrP(Y<@QmFSY?1ve+y4MNZe+y7~()X|8E;q~;Bcf8t?vuJj1 -4P$l}cq6BaV2+7|}H6>q9pMSMG;3rvjRcaPfG*ja@ym?z{1Pu^!?o@Bo!;q~M@`Vb*9!*~3S~qmBBNL -U=USeWoQk0HclPuMNV*ZSQQY-Z|&&k1SbQ6R|$3$=Gf6={`Lt$>wS>a0Ho=hQkYlwtm`Zk*9B;-{9Sg -;-g#co4H*~nGBMEGEk@SNupo3aj0iCt|$dm6>bzID3P_@u_k{;-97d(0xp%Xh -<=(5de5hWtI5lh#A5{;>YiFucu@M|Txf@8j>H32DS6MT?iDt9qqedwC7d3oBu!i>FJ;hA>w3e1um{;N -SX}cs6Y!vVq;6jJi|(~FtAD2lF+q{l^DTpUV^*sp<*A;>>~`NDJ`4H;n?BtBsrO`|A`Z!MrvC##3iqc -9Nk47}eT~?QHd>q7^$%I)HY+DxzIXz3uN^BN0IjN=&`$!P`t(G~?lA-`lwjYi^1z}twkff^rFk~JLE= -7iuXVRRqy6f`o4=8CvRCMIZTgsIYY+(3hse7JWzk`s?ogq5;j=?r0tQHSOHOI1_9xg|CV^0*%2Q?ZND -zj?DJ-rIeE_Sa=zo9LU+Orx?*YfSS-&-b?;g;@7c{xWiDR5DC|S)Am+*-!cChB8eeJRbbg$b?=W5^qG -m&1i03F=b^{_`uGPM!*n+Tgt$Ak{KmSwV+{8>f!I$D}F_8`qrndHtB*p;5$Z#FF -uFm`-d2r~Ki1Xle9L>O$4c4d&htXK|=8-STFS=3%XJ}kU7P_RghtrLTXWhS|du>n;MIO&{*v)kwMm3sQT(y4MnR~R?6j7_1Wl(~4jsCO?wtG8-_vyi2?{lqdNmmt*dlUP$_@~jNbnRD=o8h# ->Q)qT5M*6~rJcbihCf?u1Snc7o$^5nRg$vDIN6dz`WS9;kWds*Svx!gmv6=>vR>HKy(+=rm}Y}3D+HN ->3cFm6k#+N|J~WaFzHJ%=0F`7xm}jht=Ti4<7hD2n8PLK*+J6HxO-c6Deyg7ZeoT9AYEI_qO1r0Uo66 -QM-m|HDJ(M13k|w&8y5$Z$9!a8Kr@NuZQ%Dr~KAt5!Xcio?_|$VXbgz{xsN)|tNh~P^5}?BI-nR7SRE -1O~LVj*&B@6y{GgmyU#vBHyBijX+urCIPZR9BpK%ES)MMGRe^)R5d%#5Yv=PfJLbU#5Qtt5{Ks~vVm& -T1IpR6zGU0|C1nYA8&vJ*ny;ICl9GKz0wPur`N3pm)=sHFU2rQp}o7x2Yemnm?E+ALwfNNwy9&vxgt1 -^JM|p1Y3>Qe)Si=ghaPOrPg7N;9{dOKu^$yNr}EwTm-GyJQPHrJ~D+RH!(rD)(zcj&B}vV)A})LGA4O -65g-{sru?2`ZIT-&^9#hFhPf-qJ)EyxH+8QJs1lwUEOASfk9>(XEdYQRfQcQ{lw*Feb7N^jWJo;gPI; -d~L-#t!k2lY7sF=;n1R&6*&h!}sMdk0%I92C^2EXqf$5SJ&93$_mXy{%Wv$dg+sl^oikkIi&Ac3UH>a -({`qOigx4;hjcj6}!m_fs|{{R6t!Dw~^u-PGbVJ%RpPnkmaJt1st6LnRe?OtOq3=RZ8xVOfcGIVY^b) -{}`aUbUzUJnWm3O!iUz5IK^^y}SA>R`u{i7s3o!ftK~yhVHdMunjd~%JU@R%%W>*>4sz8jc -DFDcdXRYBeN6&g}L-$&7Zbj*>sp<4oA@ng;K9ExmR$gffQ*}z-b`T$ce| -=Hm623%OU6xr_f0L9xmY)MS@!}NuN5!^Wu_dvH_z2!9vovCDEb>^k3Q2fncsKq1}>?R{kA -J|`Z#GFV^2qJ-`6T3Bt96%Aj|IZ*$yYaZubu$d^THiFV@;Q=iG(pxA@oUIYkpq$A}C)y5quA*fh4RbC5s3u%&vQ=wV9CZ6ZX;GyB82v5c3Q3y!}K|P7G&VYT7Pr3vm -QH+f&jLJk=&LPkc&c9seB$%k!7v{K#8+MC(mIAs)?+k|NlKV6eZe!UyxzFfU2D;~>ykUq$DbI^Ebg#` -MW?23(S_y)iOYHbOiRL_@ydQRuQUs6_hIw>dI=;T=`R=CfwRu`b`Mb6atIi>wN@`Jb-$;L(Hz*+yZeD -qF$SNHw+`O&;taQzTx>rS)how-=Bv-uqPw+i41kSRM0oUD7kw;1li!6ZvR)#t6Q{X;G%aaHO3MnL2*~eCdzPoQw*n1>N2z6uXfRw0I#-FU`W2{VXI=a`> -db;w%n=eebiGe5elV3y{OlTOe2zGfOXhaxWR{A?N0A6;e6JvbrED%QbAa9^;w+U!$}Myt{aTnuLXIes -g2yadopq)e@5)RDQeJ%cMM2xnpqdu&3RzwA-;mN=*fS8OEymmq)OTm5@GT@dQ3Rdo6JgqFXK1KfLGpRo`q -KsClkxOY|ptV`n}W}HFd9f+khvUy!pi4gBv;&yJ)`%3R$wzJ)C-Iiuep;qn1f#G~3nRz$G14vYMPpAA -89VD_)Rd*Det0YWb{gzyxFGRtb;1xd==wU;VCKa0ygfrEJx7pJ?N%hw(r(y_hKF7(iLXE3BZ-yoiPU? -%5-&pBIio_u4#52qWDP?~Seu0IZ-&7?@p~GC~l7o7w7eQ{M-E05cB7x8usQ(7kSlLeuI*Nq3XzE>#^6 -P^O=dCRA^_G(J|)iWh2FIXr9Kg6UGsA;%xYq3&H=RxMH@M&%p*&>+k -01{hf{T}8+Dwf=S)OSDulbB^4(Eh_9e^b4Wgt4-TRG(o -yLro!3fI)vt8$AI6xcy1=cW@ingdx3N2^e&*Ig2)_FAm!d(vBe4cV3YJ;M(!7~HNd!o^fe8^dq_%Mu7 --(=^F;lWv*6+Y416SeaxxjcP~L&6~yYhf$fMPI^$8^@Q6I&Vb~BX3FL2)4y1Z-9aGIrL)i1l+PUI2GG -G(}Y~AUpl<5?sd10ZMG}JfgAEb3n-%(lP#~Z;piJVQsvy)CQSz!K8C>2D&>C7FLj*ZiEp@51bvmFK#} -6AE>G2mbFIMcHMl%FYfN?(hVXFa$Xjw>bak(KSZ{6e$O@gQQdiz5cIGhv516X=@6XES9midOD5rK5^blSCFU`R9U?V=B$R((6r=*#Y0T@mHb&p_gay2 -kD-I^+7$cK3H3%-b`6|mDft-RtYzwaSXHm>v5uedqN95qT&^&@=Ywk6P(WoN8kEyz<`pYQS_tGz{&1n -dVCMKFqATmB-TPjLXVMTzkd*X1X1sh>pfWG=*(?a*Pp$tAFIogq4M72oXV4he{HibUD2&+xyT^NHao= -^R{{=@Otm#x#YR`+B0776=wXmu=5K7jb^?7c@j6BOr0;+xU23jhvr`;!B6vv0dhi#?4I=q2Fysi=hBB -xBNAmwVea!;=jcL;DGth};rX$2TIu6oNRodO6F*TK5DCx%hVT6P|(Pvy>ln*uJOQp8&+NjFvYhJ6~+G -3vXJI(au{o?dkc(DT;3yn{fUc{k@L!>?!G;Ln;?PT*m`QEl4PqMk-(v_;BIV2}nhM~96Yq@P2{7exWv -MPH(JOL>!;7G)XB;z;qIP9t8lOY4yWkV~kpIr9=?0$I_j($3Jm)}HSo8+3w8m#u-Z?Ie@sbZYfLFp40m;YsM>eL-Gn>XHyi!LU^8d;@Vv_67b`dX=L<75;AIxr6Gogw^t~ZCV%wCFCa08vf -xnP4Io1^%WIs5BGv$=>MGZ|aZb1h)Mr;z^*L| -1>~Rv!y)tNMK>^-W9EG}TvHl;-7g)W3P8G8t2dL%O5!#XO>-*YT09chrYX`1KqVyQMwZ*#g?G@s0($5 -hb@$~NI*aMtu%q->3BMIK;hndhk!MW_1WuW@e7<-+?QD1+=hPHDW$I=0SdC{uIg|rMI;Oy1o2J-1y;7 -cyc$IybT6NIx<^jG03mvxWwI38+8pX@zT}m?Zkl|HPO0~*uX!Fh(Fs_`tH)mcte$Eiz405U=g$QvtVE -MX50c>?BEO+#t5+P7lkHf*|_T^zN3mbH&WWA7cdpF(h8(3Ka#D2WU^6-K|2m}Iz2zM$=cI)UYar54pO -VU}ry>?{1o3jqlU-BWdM~%pPsYd?hVlzI|IU*=FU3a$$&`_$x^EKo$=&iC{_!3LJ0K*QcS}7VfYJJr! ->pfLYQeP!Cb#J5q-Fte(zh3y3N1uE8b#$+rWCT4LAhn^Sg3~qKGxf^ch7r_j2qZ`-Re~uRcd#^XA37D -p=6N!z?Khjk8r>FmKotP*P|GbtM&-Mz!tsc55w#o&@4P|_gp}X4fit=Aq`KGBjqb5Hm=lRn-0TqGc%2 -^JqkG}c>1CjK5Y_a|xPok#wC}qA4C?i!c`>6Wnn+Lj@-8`WNw>!DJ)D3Omj}ss^E*5aPYRs&iMoil#3 -lbgn#8)<8SjSyi+L6E#gZuDwMUuk1@wp(#O5MuH```flC0R}Ce?n4*lX3J@a0@azK*zmfoG=J$F~v-jI`rVdH$ssm@@*Sh9` -@5e#IFlN3V)mL5~imR@=5;l1AlnSQkxpBX*V|h?g`z1$nH(v1q-D^$3q*#+ojv{?Kjfd0&mPG+?IrK` -D<^%CS6sIkOdp^M{R%hOa*U`N;X*~}Mclp#)`0SYdLKAHSfa82Y)T>?aO2U!cruV&g5B=3He910)@se -7P`?)+80Ay^f=omp$lM8-P&o@TiEY6q3 -l65bIi*>pnGi;2a4Z3pg5>|q>kDoxkR%nr2-|e!01USdo%wu9(s+Z!$lsL=#4n!hJgj^`1P^Uuk&pdH&yGB@d3yVzop2p>NvfVM{eB -Fc)@_=O7lQ2Tk`PvV2aZl7_R=pmn^UIKYat+)dTD9V^4NXd*rj&A=KMtESAi!hfbM{l5z9uFLfMTS%D -nuo;mUSuM#4~80Z(v^%X(-f1;`dtZr)rYDs7vSG)QfxMWB^j+{4$Q4c6jXAsMGvDHv`1(Nr8krH}wOB -VyN8@)ZT<@aiE3GPiH4s2der&w39fMPL^kb1yW=Gt$^@Jc{(%K@i0q{6)C@AT`F&Z}MSRr3g-$t=aSv -9cNr`(fJdSw;>7Kt)yXK!Il)3Q_@SL&4DSQn7dKdVh(tYR>AtW}embz!3E}-Gv0y$}d0T(ak$~z`v(o -zd(}X-6MIA_sIKR-K54L1sYHaL;e*4!Sfo27d6RL;s&5T#&Y&GBSV`9EfPm@WnJ`F<3#1Nyk{Tpzg%q -39pGL}lD)CqTi-pNcse*w4SAl}(n<2E+(Z_*k6-|6)v?QJ`X{+-V&(ma%FJZ#u&KuR(E(!a-kv41n@Z -&5`?#iwbAW!~kNXWHmX7cm54pY5ADEK~khHYRsQjq=R6HOd`%Hhvoj8$i4xi4Hb-!?pB_`WlF1CA{bJ -!)H5S>00hpLItD_HCEbbZzXxCn)FYPv(-m8C^*`|_l$3NvigN0W~kt*y**GH94Qk+rqy%_QrZ2hQU-z -ej#`79@`jTKv2FO&|80fs<#ESdFFYRR=;$$ivvMYb)nyJB -PIeC65l~&+|DXeOKUmk&EXmPw5DoTz)edqRU!FwWg?p_M*=|czZH^g+mSj%aF6l+ -d~H`n`w>~Hg&Fcn13uXiuw)EZAus$834;+_@_{J-OYZ@QvN=U4{Mg -xE~#6Up4>fs%~Y;bDWdO)|NBx#=Cc!Jc4cm4Z*nhVXkl_>WppoMX=gQXa&KZ~ -axQRrl~i4C+c*$>_peyQFAm_kaf<-g3kpTxtV%dXknq!`a! -@Hk+3p>h+(76B-TU{v_#BZ#0d6(3RX~qe+V`)6~k1)Iw2gGA8M0Y<^3n1ip)+)G`&`SW1;-?I}f0r*V -W1we%9mM}A>tc5KPy3 -a;grnViY}D}+5zWlBngCd-+0dwLk*N$BjOvA`E5lX(?FrOr@JQ0>_k?&#suWJx1-P)bj%*`k_RxmGd- -cqCKSj!8J$g_zr(of-ro$w3GW4CrfjABZK+!`&}z9U}Cp(>>mHU~Ch5-4Z_NM>lr?cni@LDRjdgk)4Y -coGZ;p>Y8>7@9_dc6#9y>qBT?LI4@Nj49KOQ!^QXMazWALnSMs|c{Eu(KOh%Q;}redGs$9y5zGixS)q -Le;UF0I=ka$SqTX;cTs#N$gW+P*PZAnT=M>Q_nlFa&aum&Jww%wVNxwssFdr=rW?nM+WN{A4!obOly^ -zZ7OMgat9R#Y3w&I1+=9DESiUfX^nqkf+W0oTT8Y`g^!?FYu$=qb#?YsoHr;oTB -=j$Z79@ZVWDe?qGRC&4XPghI;|SQhPu=d_hwkUQF3~bUV=qD$PbCx2B*@u}-(bI`%tj)~Fx- -3qR4gR9^Nf&oNFD_Otr*fWdZ=dgDAstq|!}YCgi^`XmV_mmx?d&Z)p+{YA2k|{R0-#5ioLwnW!2afSQQwIoK`D|jk8*{9IN~g505Lcds=6vHq{B8Q~vEa6$b -|gR*?4fLd~sz34I5B>%vNea(~12uUDOTf3?L@^a$3`Z|EA7`|4ETp_?l$3VvUuWnWrNH{q(AYXnN@O( -`~4!Evb2@lE3&P)h>@6aWAK2mmD%m`>nXC_Oz1006@z001Wd003}la4%nWWo~3|axY_OVRB?;bT4CQV -RB??b98cPVs&(BZ*DGddF5GcbJ|D}{_bDV7uVInEymzDw{c#!3fth2*x=)(QogN}N=O4tA|zHbgs{JU -pB@R2kidpqYWM1>RBUE?o}TG``enw)$N3++tlu2+b -D&(|r&|<4!1SEnFHb3}RN59WvH*0aas7g{vY}FD_mJB3HajHP53C%PQ&!CBm=5jZShE_&St>8ID;{#a -9?1y0gPCbT7S8C?|A6*r6sJ(EUP>13x}LOy$WzXom~7+AtJ?R4~+YZ0t2Hf*CosPoZ$7ir`@gO}6iz1 -|il@0#^*OqGVM!NAvs1X3( -SobvviFwxjRfGy7bXKk|wZ(`fg0Bm<=KaH)Q(joarWfSF8{u+gIoAa1le#2*13QN8@e2TP&^c&D13@z --hBL1O1jy3IX763Q-=kXVfj-tcom#8+@EWqPG-lzy2uV(+prA`QRcQNqfsF&;uHLzNhZVKU=54e0z`J -iWd#!r6OO1AiYII-g^qN=w+ggY2`8SesR?cWke@03gQq6#6~|Kh;~T`X_NA)p&x5 -QHVV43uWkyBgM3&Z^bQN%eW9O4RRSWZQ@8t?MTymE)tMVHixPEM|_LBPf1j&vEz^>iw(UM+@%iSiQE@ -=+yshwL91KPKlx!IPtb_b({aGmyT%5CzgD;Z(h}It?OpD*J)n%d(C#s>eTR~xYqt9+|>S|vrRi@o~WH -AAJsk@oIS6erPSYC@=&h+c3!eq=BnlK$F&-%9Xg -MW9M4v$B4azeUZx)eLk-1ep`!o-(`WGADMYq49T)2YLI0Wl<(ePN>jbSPaILPU8wAwJf -6`s(y_KA)E-QHxRz!qMq84A{l$RQlRG?fa5zlxGkRPt@1`s?KVoqP>Y8Q>1_wih9b}a< -qf|IH^o3f{%kUc+RcRcd>nAW-iRbFsVKqRh4?6rXS-})S=)FhglkM(GAg3~El>Dqld~Ad!g3c;A^+kB -b02E`ksc?lE{7`^di$2nUZv9BG0hQpzPCU`py5FLp#eHO87wp+YOMhg!_8Tem$A>KO5rU7-y9VABgzB -?Mp`Ef0oT0BU3OAU?mHU&1vW}cXTA9o66(geR-QnrkZx*n1^p(IlhTS}VR(>!*_!5lHUEFguFQ3~+3C -vTY@v1f$qg?(3;jfjqJhmR4E7h_;0?}S=*8@GoMF#!t1DgqA9~p*nwZRZloKj|qstJXhfAl+SiLVs5H -J{Oj&?p_o2)P1aA+aZWsd$PrbT`!OWQA2Y~Q$<{!VHZinZ#DDWNvOz(8BV4`!pW#XQQk -4o%HLgj)ev;2^2-#S6Mttit_@7xI`J+Ve!hE+T+Ntbi0!IGSZc~7~ANO>x -b%iK0A9cNJ{iqb3RHw*4co%AEy)0`A(+eM$0$n&eH;|o!+d}`og;c^JF=lxMKa`>7FQ=l=>QO(M3r$4YpgN!ywB2Xnz((PZXnImO=2XsQa8;KQXeQT{Hw}x#yX>Nq#=eS0)uk6i@!q#JjGxHF#gKCYZu;_4 -P~e_8Mr7{zoN=F=bTPX@N+0qvPyt{?uM*g&5`wks76LLz(;oh7BVBtxXh!6Lzm@@CFq0(ENaa7b!7!$ -Hf0JObCM(TX%gGrtHXz&Q -BQ_l(ek$UI*!hxjz(D^=64=VqAq`s^?oz$0={{pF_mzf{K>h0Kv(R%A+)Gl6Uo~iaA_M^{M2QWLrd@8 -f|AoicY>r$*VhvcOsF_VlhWGP=XxAa01%gofO#eJh8iE%(QT*}dl9}^4PkPnN5Ew7@x;$Y*wbdM-_9u3QR94$R`?;^&2L}K3JOz~vlmo%fD|-cB_ -kFYbV}frR@?pW3|EGanv6ud%!5*Qv`|k(`M4tMbxJDinc`xJexfq&g5_>dR>p(2g3Mu#t1!AT)y{yKy -DKWusQ{OHc8>MMUj_S*oIq?CUL@4BGs#msXs_&<%UO^x}ukLy%T}R#~Wb|`qAaCX(c4yAWpXZXdDT^IYvV=`{_bBf5R -P*=N42jMn&tv^96M*~*v2m$p%k%}M)q>+U9-DNP=Ec+>c#SlRN{Uq6@kdJGtWNr%!{tBsvmghKMlt)8 -ut3*c^@7|linXt<9pS(g2e`5qWPLjCZMN61a9D2{mq47^n1IFB@dWY8Un#lS_sI;YZReF$t|bIM|?%0 -cBnx`U@I`me~i`yY=;N1TuDPe^-k&Z2LZ!W>C7HzWEMkMB`M)*%%@XYjgm~ye!dU6Q^3F5y7a)z7e -rINK^~~lb|u=CW9v-EHD}Vq%_5ERmh{nnPM4II*zqPy)}h~Pva$w&=6C2LW!EPnZ_%@0}3AT0Hr}-26 -oP7Hl@7##8BCY6>15vb0Op8=#7@jAk@pfu1i4kDWQdk=VY1X -;sJw;5vnX-vy -OmPT>O{5%fSPrCaczjCHQhU5agg?sVEWouAtsfaRPlbr1B%kMCS6SB*xb)fPOIG4(Q4&5h1fs}+~)xu -qt`&w~xq2JMbcNS)lTN5(nYX=-dV8i8QO6xJuL;$3#sJaVL&rIDoqMd16KCg1m)21V4Orj=_y;(}WDe -yk+sp6)U6uEaTjZh;HE_V!4J7zgjyB@+32J_{^0fKOS&J+zMxvm^~2{Q1{O*b0sH*D>{opG$EMj%j0` -lBQ*4(|j&qp9Q^>qigY^4)Ta`d7*o#Av622**%&4?Anv#?0&j?p<_IAI^+219y%Av^*h-}7n|(G%e%~ -%Y>THFX7g1Ce4JzIv?icH$%uz213XFd5;XT{w6T^M`4d|WhIg1uUBa$IW48YPCtD;^4nl+&=5xJDEc* -1ZIY{t=CU(^8m{Q7?%aiUripjh9ouS69JZjQBHqBwrzEV>c@P1x7{{7L4^#+sW_|YFO&T;C;D0`_ZT^ -g5OcI1Jgmcw|m@Spp&t75+k227ZDEQ6cFWpF6KspA(~ZRfX^m_u}f -1N`HntmZ*~WdtF8Fx7na#$p+^(zvs|QI_jV>Ts^dVL{!$f4>(lS^Z^eozBdjPvZwrO9KQH0000803{T -dPT-eeTE7PX0QM6A03-ka0B~t=FJE?LZe(wAFJow7a%5$6FJow7a&u*LaB^>AWpXZXd6iddbJ|K0{qA -4U6@M^nKnuBbYj??Gmk@}li?IuErLu`v7Saf2Ek@drWLx*w@97zV1cp>XRZ?cAryr-gPdDf1$3OIO_< -1~~$#^iF&WH4Iay|HiPRO30ozr41=vvE_R9-}bI1nULx{CjlQRLCBl8-{CxbfGXPDMt6$b_H8ntX^lX -Il+pB{K=JPvTKTaUwLuA!SEYLw+i<)b|pBqs=B)()ThMs}x^#5^M7Xis>fS_T1OP%S1rS!f1SH%G0bX -DPU@yQDe}gH=RxwDvpXf)Q=*Xv202+kqZ4N0vpL7R+*N|9Pu^MW)CMsSDt>L8yT&%+)$g~VMbX@H$}F -Nm64dQ3;gqqcsZ<0=uNr>`SbLaCL%>B^jRpOVg4pxM$*UQiS&g^1$ikYe3q^&C`atbymW*{G1MFQ-;$8HXWN{9oX -{JmyR}woY#~-^cp3YTbSM0<38xE+kS@J_=^TZiiZmdpjM_DlgFm1Ufj*)9XelU9MVLn|Xn-tz880rcZ -x_^`ex)z{*{nZZe0>X9qyo1{u+=0tNhF~dQE5+Q1ssQhtKn>Li5LBkmOmAxO07t`Tf>$aO}E -==cYo`2iEigOIrX98^wUq0&-vLIGR6HA#Va(;YWo%5o`o9ixor63LP(p -+SGCv+oT{uk_o&OIY;%**T9VsN`~7nidkzDz#Rd+Ip<_}yLyA|%(9;0M=jq#_Dis9d#jLm8od-%n#Dp -oz*43pj4d#-W8T_pfSCabNp(Acjz_gpJu*iELud8m_Cnjd0y&<(>vTWyZIYp43rsJ`ESc!D2l9Y*qX# -(ad;Ru~S&mxUqDoSBosy%7(Uk5DF2L;4JgHbOQ8GSuwtyP+#-DvH}oXKUSd(gz`_l>^Pm$AitLWMxgJ -5%2R{3y9Tkl%OVqQtfHc^?=-#vGA*1mUpAH(jc?zdJDv9T=f92any)Vq;sssUKo5=M>?e#%h5wc5F)w -nTVjZLh9=v+>YWMdSW{OZMW*F|(tVo36bap?j-gr^6_Ld^+kx(mU@ZCvD-@Nvp{r11_cEWou^!C4+^m -{%|@bLTd-#PTrsCv7>tP{G&OsX;8P*x-&@XojYSSz(ByEi=jrAolR2sA`#^C(+#NO8{N7-WRZf)5o45 -9}swhz1*kAIS|UE2uJ5lwPEc9U5V-eD57DxW`uG+g@RoL_@!Lsah}4z -|K(hP#>LW)Dh-;+JBd8sPEdpcgd&Xc1VM=38kU) -^GUgnuA*&V+y*E|E6R&7AE8FGh)nX-{NjxWD6f@85JH(kYLQzqN(u^rDYwz%@X*-D|*8%GQY?$!Mk8< -pLjJ<`i60K**u4qC9RdCi1)1+vP03e}Jw^ja^TGhU;4que#KQCH4g-1T7!7B(cwvFdfS?j&Mw_AiA@t -nca28(>_wYzsU6*iW!o#YXtR)t%xH`YRmsiwuM-35$Xb~Av(=RYOr{D$U~7ra -RXKgQ>3V$;7}LZEb=t+?D;yz+~nfpA(eY8R3lbRlecV*CNSwQ9~`K`=4u)Q72I_8?l@?dgGTBMLhWwv -ZcoDQD3zciQ$u5orZHbBcn2EhR9^I6qa8uuK59{&y0bzw%py -{L;C*SwwC725Z9*VxGeSHc#BTuN!Df2A&f-QX=bWL1#m-an|Ui-jy%lBYA@2_k=M+9eqo2039xOiL?aT)lu)yG&|h=x`}idUtBex1B -D)IUWeNuZ1{qj_gDqp&AEETyo)#$HwMZU*W6M2b{xx;)4(tuq!hhh-d=eM1>Bk$M`DP}B`~yia+w&2u -UM{XCgoqEDy0XVT`Qb>{oVe7voC={t^dGN2t1?cQy -#D(-se>p4LuKd_kcGqd{~LRVTdsrg{bW^2ZV`%<-4|A!DseMR#^4%kc`6|44s$j9g4xsj|;sDL{ve#o -p`SM0EwC{QJd%NWy32ud4D4S8`2 -fxXUB|&a3F_Ss=GFfJP)h>@6aWAK2mmD%m`+^#H%O!f000IK001cf003}la4%nWWo~3|axY_OVRB?;b -T4CXWNB_^b97;JWo=<&XlZU`E^v93SMP7zHW2;pzv9q*h_kv{++x6*Ai$i|X^kaLU^^&+A~0y_WOJoN -kEEP>fBhaQ$-k3zMuFDHyT`kCcaQGT(cuqWc0c#}bkn=)_J>`%yt%vjlV0#WY8=sM#&l=oROUj_l}?z -XrA+@Ll@gIQQ;K1W8ntULmrla-6E9flIUCX`xdRyR@3XV9m@`aSR^&1kPU_s^tC?#Pz5tl+wJGhfVc| -HT$uc1BUnPvknlEwH%u(~|gxViJ{tk$P?h$pAqJj6dG+@hS!AV)kmCl`!lfr4!v?ph<5Zwy%o$jTY8k -x~MLWYiAb2R)F?SI!Yn;YsbMCoQ}>L-&S|L&ibYO_6KP9$5aCGV$*H2O!9lGxfM!lT)P%M8;RM$^zC&QDC_>VF!XWMqTkwZ68U$977P)7EmJ^WE3Hk7F12;u^|pi9)A#N6>AUua(>Bq=5 -SEud)a`%zNf9|}G|(x(E=o_K0nSBqc7CFa&G3Bm!?HfH6SID9jQbE(_0zUibDJ` -{8z#B*)_ycG_ab(13|QM~i3P?*OqAa#d<{{E7UX2cQP9urfOsbIDT5wcfZF;rWdu?LErgOucxE8F$mb+95!4tEjWwD<@+J&TkO}8brqZi;fV=J1X;pdUiGO6+m -Lr=`C}`mInct`<>j>OL;1$QJJ+QB7nEtO#F(sxlb&Z1L+M|S -7*CT3Qi=$l;x1gBaa7hECKbsk7D?4anPqr_eBHQ6`dNk5KI7uFT7m8CRy$D9+8qoaq+1Q&Aoev*akpC -ce!t~gYZ@qCujjg&L#6Cv<+x|3wvx0p2LVE$=Us)gdSVfT^jQt-=Dk(W)?ojVz)ScLY2{KV$Fs?-KRjjH^zi(hJ`1~<9{(mTLxk6 -U>AyA0vPa)O5K>HU^O9KQH0000803{TdPJD*tWk~`60B!^T0384T0B~t=FJE?LZe(wAFJow7a%5$6FJ -o_QaA9;VaCwzeO>f#j5WVv&p2USnE`>zBl;&WFNvsk;*eK!@t?hBW>avS=*QEaU9rF<)C6U^_*z@t;z -M0wSc0WECNB8j@vv?BClL+J4V)7H80gAU5gJn?XN;vnx+6b9dI(*7HJ0&PQ^>MuU9~RA!dC=OcI#lugKg%Ug(N2-m>Q-RUK%|DNshO7@5ws$;JQCN-*#dQU@UsVxB4y2t_5 -DP=mtu#x-+u>VGAym7LfWtTbUh~RWTtK`XZG=MyMT?wF;PlbMX|X4~un*aK6D)xLSqtbTi^yUm1?lo{ -mg%yHk>hxhgBPZ&)}JJVvX@10%w5Jd4v!t9}}%^C(F$U91pd8LrZJvYv%2EZ3{$B8mDyLUe9%+su!e{ -MzD@DYu3@S&$b}xnt=Y9rgL4E)5+)me9rrc(Ib-jyLb8%Pg1q -28eau5AbcsNkactJc5MbQ&}?gR2gFfjE(bc>SM^*!S!%B_%gg141smRp3e@V`Q1M*RJYUNNg3>GY$x93 -2I82Xm_!gb_+z3MG>)_8IO*m_UQ&dy&@O9KQH0000803{TdPQ5~!F=zt-06zx+03rYY0B~t=FJE?LZe -(wAFJow7a%5$6FJo{yG&yi`Z(?O~E^v93R84Q&I1s)2R}A8d1322ay9G843ap~osS$rcvQgw@&=O@6r -AULM5~#m^A0^pNS{vBqNt&7WIP+#W-R}8A55uSNj3(n~I7^1~FqucsbRo9e>C$q?X>P<;C?+Y=87E<> -&_9KgEYN04DQrRK5IR=E2}C~fS1$FQ8`3$sGaQJYac5Ju=P(sTsYJ@0(8}W1?6s+XKuim58hdIub3CI ->RWts66*Hb|U*W2qqh54PgS)%GL2))bpioMReXOOJTW(%?)>IN{<&4;rPMe;+{sIfpl$js25OQloK{t -d99XU;l%I&mj>xVQ_Yz#A%S1C7~lhwI5@{W$nZ?qh8{vVVk-MX8{i9_iM;ZrQe`N|XYIHt>_ -Nk)*@`CIB2#+eiW^k3C^u5108d29mF2`N?Y+(JPD+h@B1xMFjWpG*!z7{6Jf@HqVZ0nit4SEsVihmu$uJ; -FI6p}2#XKkSQQ{nwg@%$DcT7mzl>UPDS|lhl+Ob!THm6)*qKM&%?5qFPL#frat{0G#p!XzlQrh+D%?Q -j6rti1zg5}iTK88LG`t*5#Af|puv>*X|Byv!Xq}HZS5865(It_un{WKWd-VFY{9T2S&SS}qJ&L02c66 -tn2xzPm$>5iG>K=@%p1N|!+uA=Ao@tq^Yo~SH#M2o|_P+RLTZUswsLh%DKsy&JKOdC%NDK`Ex%iZ4S4 -Ww@4r=@h_p>jNeH{otkdruf^izD^o?ytLbGF?ySkHbl)(@7<>wlO!b-g!2x3v3I_Mo#Osz|vW-dzQ<* -wfkdBte@UHsy@{b!4YtMiiM+9V2$j)o~gO_G1n6oD|Wsderb~S&yh7(Ll-}e(Ea8)#@)>veP~TP$s7K -TtT`A!gyuOnT)E(5p6@Bv%y;o8#Q%D^!@m}4u!bFswe=q~581lvC}`W%wPFSTRaC=WX%t=6#a>-vkW6 -h$w!Mgc#=BUO9KQH0000803{TdPDBK($-n{t0DT4k02}}S0B~t=FJE?LZe(wAFJow7a%5$6FJ -*3ZZF4Sgd99R9Z{jczhVS_mqn_FeDzueWNGml!pe`XnB&^g^Fv$Sf8k>!sc9Htycj7=OtM;&~<^q208 -9(#kxwyD+Tp>WMz-$maS#CA`;|6$vP-@I*Rj6{kQKhj*(hV;%G_d38GhbuNak{)T8&z~+z$Nqqm-Yx; -W?Z(G$L0Nc%X866)%MD{pi3Pm6`4dxf)pdZr>P+<;Z*mPtSWw`uK#kI9%aP@)6&AD -vU0G}3P|ExT3(W-(Ah}Wnq=d9Y$Ytxy>vTTz5;y8I2H)Y@N?z1&l(tnK*ythYJ;fd=od($Fa$Qp3!(1 -3F{u^2x+`!8E(eR2EmhX0A4@LpEo)(MwF{r^Irp4ksmi_|^yvR!;HktZt$h<)n)9>r{*={^@2P|e`;Q -IA;$8JBmQ@7f$wOgx=!werm>K^P?c$FY^#b-G-?_R}7|zlNj_aNa(msLia?qw(7b=j}kzqB?p%z{zV9K&!P3=$SMy%B_QGC6OI2~Rh<#4jSWe_&ht{0uaT-C2C<;MpvkHH0jV -{w9p@SBWx3KYvD%Zc!Jc4cm4Z*nhVXkl_> -WppoPb7OFFZ(?O~E^v9RR&8(MMiBnauNWm#1rp~%s!GkJkxGcU^Br%piB4%%S&Kb}Rn0ElU6OLYzO#P -8IGDTKwf+Lk&OAHw%rk>8FPk5@bKiMW7<)r^8n|#bo((_38Q;s+B`nqmGsRb2uoQ+eM&KGY@-t6U=18 -@Ih-^-)boB%`oILOG2~)X61u_BCfI#yQizx2HL|8^sldfA&5qI2L@>hzV+4Mqek_FabC$#3ZwXPXQXQD&RenoOqlu$<~&^~zJWJD85%GW= -6kc2qNty!N%>MSwYMxK3K6tS%rhl+1ADoHhzU>_bDhHz5eAruf0T;iH!|kQ-kNqIrgCi72GvHF9KM@A -z!)`U3hz*$qH5;5Wy&K89`gu=MqnDY)~44LQzM&5P$U8)N$?rFH-MbZ?cGnPG+E%v>6f<^h+U8h+RWJ -eshe9H*rBBNkWx2P8R;}qP*oZO7a8oDxTh}!NQFL=j9`hd)0phiE+LRFeDW6WXAcV)OdsLX!1o8!#p4 -b664P`_{mYT~W}9*Xrm7SZW>1L|z{K^3?f0OkXK;SC?g10UuO{(Kg=4n -TldQf$d=BKbgZLXaCtOvY#!Pjz1UBehpkf@utE_Jq{t2stSV7>&qw|4ltirC4S4QbIe=p&~KKljOriN8{6K|tXno)GjXl~Q%#PHJOaCj*M@UG@9DH~pV_eSn96Eax6@r}wX1 -BA2aJqT~jgXw8&HvsH3Q)l>KE2o1>?pqXdJ(vb#1th6D0ZCJGC0uSEVNnlSV`AHFiVn=gQc#WA@?br% -OJSBy8!Gf1zRA?TU{VJn%q&H=B`~B}>FbOBKdv{C>2D7W))zxr%YWpy+Y%OSb3crv3!LQS5wW5@14X% -!ArMGLpXy;gfi;IiwGaIX#gPj;n(z>wg7j(474PF4-61h}fCQ(u+MuzGJ6a}Msn$I-yNUH&v=IP!FRj -vg8%$6wv<{O#9^_o-y8vou=xV=kRO=kl~d&0;JipMC7U0NhW67Jz5=a7E(CB_88kkbYZ!?wmW>C*BLt -1!tLbUT#-i;C@|ceI_`FA3^`4eU4voMVKe@0|*V{0l9G)ZSb8`zE -2VMH6yP`|CN>ONsU6c~raGWv>FT2#>NmTjoUBSq6-+MP=Mxgk^VYlU87l-_oxD>c(9uX9)DHUFo8GpF -{6=pBcP9k%MmLnkseskntY>+>?hGi^vwwClNc{@<=-e!tP`BRb9gzmpy+O -@arw~nv*%)abV#1?Z%nT7U9G_8+okMEdIok{^7sEl(z`zistOTklASVFuf1G#TVil)@6aWAK2mmD%m`=e8V4@TT000gt000~S003}la4%nWWo~3|axY -_OVRB?;bT4IdV{>gTaCz;SZByb%5Xay1DZ17dY87x0p4Yk;$fC0HR0-CW`^Fl=L{b|PY7*SyuI{^EHz -W&@j)KVQT?ty~bpLy1dLH^$;pm8e!Fltt)rEGe(d-&cIB)kF*O1_QlsbaJSVB+P_qHn>Xm}$Dwht5UF -WYfM)>HRjVp}#9xwc$8u+j62e2|Vel?pry0wxCfcV*z4Vk)s|GMTxyDFWMbef(EVJ+=4;14G|a%XU*q -5y%k?=ZlD&T?s|8rgQ9S9KcEA6!NuN6+^J@S*SY>=;Z5zE`6yUJY#~v!7=9DE%NeRAZ1wV0MXE66>UEd9U65Y)x?i-c-g&yw)AnNa4B7>a5nwf(6LPvw{26pz69aJl-m69DpMCU5F;E2TzD9Mk=-+pHr -lE02dR8)0WF)4dJ7|_!BcP|0vbQA=9Gk|IOsVzxEd-_X4w?1W;z1fU4$KD?(q){h!g#Z+X#jdX1=s$P -8uViFMMkau6j2EsCRGSTV2=d-NEe{`eLThitD$GWKX7!je&7iO1Qxs2aW+cO}%l2iu!r0-5T7|=r3A> -ZqqQ}qNhV0`gMKKYTUHzI`nV!e$QxT0Sqal6jL&}B>zgWg&`*%W^yD0VLSeE>bJPOJ`T_s!B{*M&?qysxIyxO;j%`;O0dAsH>gxMlqi -w*SaFWWWbFnu74l9QzE>}tAI9ru$7Hi9sE+6UgaU1FKyiOyX#`mX@PUCe2(iM1Jfpi64S0r7L*A+=u< -aH&|m3Uo=bR}L_CS94=l}T6TbrsT8B3%)GOKB~#k*;Fl`o-U5tCFr-;ccX=^12%7YP_ySx*D$&q!YYO -kWTQrA?b#^Zb-TzuQN$!@;Z}rCa)WjZp7BF7--k;Za -Il!E&Od6#Gc(qng3%G}$Ax#6Ob#*gmveMys%Wa0z|!9^9pgDndq!&JCi6e%Eex?~p&*O*%t(JH~#(^V -PZQ^u9FP=<)Zl=?LGy3&hZo$Dd(Y3)9P4u3n*+B}}AJbSrOq_%y8bIL3epGZyO3k#4NN*r+!(u9%SLr ->pOo%kzv@$ZMZAogCrb&%`}B9ycgUzC8XXm1-G1EvIRX?ym~3y{igeI8cQz{tqhrBU@PEwRHTwUg23= -Sm8NaRN+}$Sm8NaRN+}$Sm8NaRN+}$Sm8NaRN+}$Sm8NaRN+}$Sm8NaRN+}$Sm8NaRN-0Ms=}`hWpbh -Cs`@VVXpb9}DfGLL!hxIEBLCYEV|x@?7X4R?yGx3m=Y+!{+up0Y1{tL%$mrm^Y{?bxI8lXMrf@(WfB7 -_`d!$g!6dv7o&jZ>zsjc0U$-ovPoDD8Yz_(hWW1QmFZ|daOkC`rMzKIbc!b4$(l$l0%6Y;hqy|4Y9Ni -V8UuSI$lhV%+d%za6(9CBWjCB0g@^g(*RY#*femZXP=@;^*^Zv}sQklt^6GBOGm&o0 -d3z%L+TV^yzly1NgrwK{n?HOktl3r%AB!6?U+3XtDQ5e4`1paO?+;1wP`(|KetgS#0JLI2bkEG~%Z7) -4+PMF>hYKE!9gD5LL|i*X&yv8h{%uPFo8K%x2?C3AyfX-FOmlk>xYoA=foo4m;SvU<{smA=0|XQR000 -O8B@~!WAUDL&Dg^)lr49f9A^-pYaA|NaUv_0~WN&gWV`yP=WMyR&UoP?pZ`X-`?HI!abh_yy#2kl8=1&|&7%p=aCQ@h3r(SYgCG#1fPi#1&mBD7=`jkk8CWR^dqiNs7Zy>%)rH0Na0!hS -(uH?&nTg-nv>b74|?2w{oI`R_`@ey!$Wv2{zaoFg3Lk^tAjTpO&=>qc|uEn}>c(o|*E#gA%koM8GhmY)vMuq5pUTER@DB>X$N!^(GP^Mpl|hyG7t2 -7<4D?2UxUjd;OmO=%T;t`NOW~(cNf5EgH8bey=_4w&>Ip9lFHWUix%h|bl*{R!LqM!9ZerL>hwnwASO -RYD1is_JFx)uU8Iz*)2%YrK?g0^UcGKMuP&Q!u9`$s4=d*$>JB?UI7BWgm4#MoYH%5&mr*r$bjDiEgj -{+SO$6sGTnWjOw+y|Wg)62Fw;Ld#R%SPdBP(vVmbbC%aXU_SGaJkza2m(5P8oM*?PMq6&wTAnK$CZ=I -{}$NciMhG?i_5iFub+~gDbWBXA?^MwSab0gMYOpAo4I~T>u8&AQ+4~-F~G~2_t4qx}2s`J)I$@5QY$1 -Zb3mHFw26VYIwBB*4<-bOttC(Rh<$v6gHPH4B(a`5TJ#?!0ct&J(qcY)dM -Z%`^tJ#bT~Ty6~e??99I#O3}&RXh5*pZSFE_%s{d=FMHP9##+(~_`ZBUesPW>VsaU?E40Suj|~4#j;F)j9~^vqHm?Q!M$M-mY2@Pb|C0PgL6jmX4&#FoUlmGx*?ihV(JTo%@6bZKce; -MJ?e~WF*X0>gJuUH4*?>zGFINFRomka<;Lgi=rM5{kE4dJJE<+CFUuh)X!)W5Sr+(l+OuBgKcgk~jpW -^q=X!$bOC%V53N>P+#7-tWP(^S&A(3HTzmUjHO=Su2ud8P)h>@6aWAK2mmD%m -`?OT19@Q>008PV001BW003}la4%nWWo~3|axY_OVRB?;bT4IfV{39|a%FKYaCw!QYmXI2l7_$cuSkh6 -MrxSKsH{t_wJQMzPiru+4QS;3f*LrOqiOoEryG0OU%&A@1@=m2q+KMJ`&4B{M#klhOXjUx-~LzbKm76 -RNqKzs;Ng?!56k_>PapiP{FAY_PHvSK?|0?tasSKy>f*9IIK17J{dM{2@ZbB(%ZvQ|;lrn^{hN!M!?C -=%-0!Y#%9|W>_vGu;v#b5hK4<>0`*nAD__#Zk!@F|x4VpRr$?oR*&Be#v|026TTwUkSQtxldcMoot+WVR$zB&AExp#S4&KY}M&Ue?ly3O0;-*|&z5UDKnDajz-tOOh=GCXGx49j-zS$i=Tz{3}k568f$Ghv~wEVcc+8uK -@&py4r+`q}mkN0nOSJ%68aa}(0?)v>7l6nLq&%ZEI9vzar(*3^d_W7N)f88Cg^G%7n_3LV1D4v$o^>- -IH@SocJ$i&;p^yhM!+BklldH0(l_+zJU%l=BT?^E{qM-t49{+4D{Uhm4M>)pFgm#0Y~$CkgJz4+_Xmo -Lh_CqI|J-#b6Q_vFRT-{;trbbd|!ZIt#OKBfu2ExD`X#nsK{WIPG{^zi(_UvtL2`)7~OUi=LAkIr5^d -HDQ!dGz$W+$+!SoxeDH@bdA!^YZNF`Ln0bAKoqH`EK`B#0c|SLjGOE?~>$)LrU`P?&e~DdA)7@=Tz@? -GI;s6yubK$muh~q+owg9i*%8XpZ{MvygXd};(l|=9|HZp?BA8E!_8^=?YK|dN$dNQc9Y5<`aMmfe{=V -=Of?5xT>axRRq#BgKia=b(vL0=hvR9vf4IJ3=uh`@?hv&Ocj`FQQeHmKH~;KkdHCdq|0Af}I!VIMFHl -zQ>fQ11pG?aR`Rnf<`FpDI`|ovT!VvB2i{4vf&d}CBUiGdn&))7X$`4;{7r!`1pygjTO4C^WQogHqz -WlhI@0amzK<+%*%?67be@qCx{KnjqlZP)K{O$bJi=UsJJ-GMy)emRS -U!0%afBE9<>62II_ws*9X}&xBKJMQA#}8jUKl_h|`94<*g!SP4#SxQ9bNSDwNAcz2_2sUdewma`lQW; -bdUXEq-(NjF|KZ_zPX6wslscZ~e?vZEIIUx!PfJ_tXnnp@n4%R$O~)xt`PI`Ul-oYd$7eJx~x^y1vkmRC$yJ?7@qL^KC1nMb -(q6;oPNf09A{xv}JH?aWx9x?L-RR$QfQdSJVF(-j{$+ByrmAXzDO_`>6 -qYJ<%W5EU^D}f=7bw8aI^H_gFlf)XGVvG4(SfTriXRMbX;#_-yObWE=SkYUjBv9U~bKw)FCBRSA2OyQ -tRL&?J%Dse>NVlxqX>=EId}U)%mMK+U=!i+TwBysUif1(D9TNr(bK50c{Ga4;>Rn69xB3EmxOE1i(@mu|CY9flZ -^*?t6EF%)pCk_$L0~L6#ENGYY;FZ*$5cvb1PE+1!6sOV?cs0({?P!8#>QIeD7 -Or{t)N@OQAeaXgd8G)wA!-iF&8eNbJ21EjC?1<*jN+m1_72mVm3sebT~E-p2Py&YiWSpI>{!WqAXVU0 -Qx3P0tpW*TVC;@b@G9APp}F8V#yggKw^Mq6%=R3u?Qg2dDjr`h#&DGx2&0>`pRR#uU0T~o#8Z%xuCiBtej*EpHSMyJ6IARr7jsvjEW1)0SUuoOVrqEhhw&}@j7;ZlCl&KG~OvS^7 -zuh>8$B_;w@Ww^Q=^xfDBV%JcaY-L5_`VdyMBEEtLj9?7lQ|9Ie3=hf86YhweW1IN_a3}>x*k~Ey-~j -^)B3vItOE?rG`JfAp;DU$^eq`1G!PIXm1+OqD7L29N;*Q47R+9fJERyk&{MRZj`*URxwG6DzP} -QM3#zIb!`^II_k!o!L>5d=?5R+?e?v7b*`%W!aSMI2_;{gM4*;Is(S&2$j-Lm=Wa%I$)l_CDU%3N-_S -G3%5k_M&$y{R)oGREFPjSjZPBCa^HAuPQ8N}Q^ZZQRK72@!jin{=NnHUk3m}fT -Gi2Gepg*FC$R6zue2-x^vZW)vk7i#)#SvN*x6D<%7I$8ka^Dndvtv;F^`4y?)^PPmV65}iJwi0|2Of| -*^a^Vo&uTo$#4$+%f2mhFAZ%Ad;Jyv}gwR`l-KXf3on;&)pe+ -M5spx7r37FDYr^26q)Tx2qHa0a5siaKFu0O5uiEl2nnb+NNyFtFe=qAYIKK!|R-*tI8Y6RH)A>}QvGk0XoPCBv{8D|wGhvd^qRiFW-7}L6HB|eRghA%Cx3F=r8UmIA* -PVzxzKsuM60~kReaDxf6+)Qgx%kafUof1u@kbj!Kn{~3=s%;9eGv+W5BP%FXnG*WDQ^*abS=3;;*|JE -6We(jEPWmFA!(kXYC1iG^TWAXpY^<96MkUS~nYL+h0)FlFMAFvhU -U88xNkU4uHxqs;Y0fj%A~QG#o} -)K66|S_|CJPe>3ScmTjDe>VB3uDNbIV{p`ACE)%ez$^1%|W^RthmbBS&mNNeEa?zJqFX0yai6ISPzAU -_f;>^r(RG(>w -AoiTH$fMnp<{(M9LTpeG&{G%#&eT9Jr$y(`0a-j16#@W|Kppn(9b`BO4eE0WdO4fnr{Z8BK)A>&3_}XtB -~L85UM$&3$eJnHNxPY=AET;#C|qy6^BFZ3iQsjgC6`V$g7|Ac)!6o~cx6)`om$dk{Zlju+VC%Kp3svYF88&ZJdX!6`JaAqQ8If6;H`0;+#+ta9miv+ij}8BP$&J3 -q<8nw|YQvTXK?*h_zU^VbgvE>OlrjN)RacsQP;XSgp*75A_F*GPAY*4>ll5$1 -A|%WSBBl>4hYd`t%aHp;or0$hY$8WW|0wsG9p22pi(q%u`L9a8G-<;WlYluz!M -?5ZsjK+ubpeZ)-x-*?Xb{cp(`_z=tfLw$!ApSWstZcIiREx+4BP;o!lsDwGP4}D_BwOwK&bJdqx69)yCPwNhU?Ki$we%NG)86sW -R$^45G12c396G~({-^XA*LAXns{g-?IPG5sX`DuqbeH3FcPE+`k~kkJ%1b^ZJb$|Cr+cDDa;D}3f5_s -i%|&IV=kpw%5atkVZ+hx$My6SV({p4KHz5Lt7+4=W$@7Yn4~>u;0EbRJ|Kwa7r<^@ERNVCLARowfb~K -`+h(VU@idieOCZA>W5*eZ*c8R4&G?(eiYeHvJyGgUE)1K^Mj63>G!n=Q{+C~ -NJs^GR*d9V|3jg6M3ahN*KqXfz9NFT?+FNg{Zs|phMs%~#%*mn>FpQ@|REG*>Y7Ly<$2Z#&eKvdC&G< -M$Ju}4{2ps+w;QHK77knE8eZpps#B_SCO7j4Pp6-N~fC<>#Rk&pg_^lG0AKC~=Suu1^q)`Vg<|AaR -qlFLQKHWg2pk-6=#g#1p2#)}GRSaDtMgbb$EOWWLAR{BsJIN=Wf@Wy^7IM_!1R!F|$iNt*HkmvA5%h+ -G9#&Wm&K6NbXmW7~vH%bi_DyZx5N8@>Xch%cv7qA>w1k32O;GvQwta;(s6;`T1OXoEv!I*`^5QT_}lag;Zn{i)Sa8+lVm+xM20;K@L6GLhC+g_UFSblYmV01l -nr0%JM)0p*%t$0HtUSW*B8Kng@IeU9faVQORkZNf|X3JyV-T3)Sp`yj$@#Ow~0lqU^VBzD8|iJx)=8k -7STqWCTmwdJg2RP4$rFxKZ$9Bq{}{0h9;XM1&0Eu^d9=1J?_JLm?$5{*}!pOh7Cs2?3&IOx91 -}CQ!{q76`IYodWqUTSbpyRF6NJXkfdSDT}=vrVylPQf85c_iU{MHwG>Jo2-*0@uZLu1o0}i<{)4i7Ew -^JhtgEFjcbi|Bg2}21N?{JFg1OdG>XPUgxsutMp3$+?PNWSe2U^RC=WuFm9W6Td?}j94>E(*X~=ESXh -N+WJdXxY@L)Mb$!PKlV2rfFfZrPS`AJkr@yZ6U=1s%q5U#8s(J1g$><*Y$>bII8>L&mdPw#ADyYOnKe -HBMLyTwHCun-yPPX^|kWgg;F_Evah;)6jD6kq&l$$;4y>sTcp3y+lt@WsB9MR?!?2se^sjn*tXk= -rUZqjEfC|G-%!GLi)1_$saRwDTxv3R08O$^7#v5pEN(GJkb=_H0Hr=ve@G1|@Y@Q@}6*>(}8XQE!dlm -~;<@>&UPx3v6c+NmhNCzQ+h2jvjBj6*U -6_VF@uRi>(f_&+H8N0-w0W5!^ONs%TjN;S@eNQ4Efr!jg-=MBze2aQGdzJ~{tO(Wv>x9-YQd1Tt|gAv -hEckD0=9y&6tPGiReJAF5lPd0;1Ir&aaanDHEp5|n0IrA1V|@j_gvfKjDmRBg_KNWpP%o!p5mp*&AqN -lY{JK)-ue#Qvcbw^w1@_m)|kAW(Nv5l>okOSK04ONCf+LC&mbV;K1ZNas{Iioq^FP3_pu-t%@b@U -SyD76Xuzr>9rR#N4-^fD3&j&G>$+w&C#~&vgNMgZAbNQ%$crX3C~FD{h84O4#al>=hyIhy+9V@~oJ1} -Kgd0pw#*!)IiU+GS#!KIt5z4~BDuL3>mg!`f1ORs1!juBsLJKpWnON%00&|qf9&h!~#v4Iipwi;uuyi -^0pnGMsQ=~BSSa|rsf(&Hz@Y~vp(>rTzj?`oFeA$+VU?)lmQ21Zrf>_`5ol#?FoM!=Cqsj!7gmX)4AwBAAkfH9i%gs&4;Lb$QPs#^W! -JH%hAtFo}h*8c=$~SDBen$J-MON=TXo^j#cdil+|bO*TiRmP -1V7CL%-;4LmgEvd*P*ThK9VS-L#*q!49pn8pp0zYhP(?1F#LJ~9+GBxz5!~BpyKWcb#xT;(5!_H6h-i -c(e2G2$Z&!#7@#}Hn;##k(EV1yV+ySmV%F00J~uZe`oq<-r-R85ExSO~_c6stK?GP{x%CP&{Jp!iQ -vLUPD24IMXWGj{9wD6#lyLmxPk}@)5@&L=b$`IuCS}}~y=Zcp6PWI+A#EPAFZvF%oy78|CSm20V -mqR8Y-(ot!CNTozz?3yyzg4Qu~q@|aLeB@VFX3LgLLH+^;;-~CgCbkSySyozR!b_tae(l!S-4B3iZtU -KkSr(piTGczm->(w3!P?fiYU8OB-BR!K=f44QF+8A9DTOJGD -5qiV*_Q68;nQ?UV|HLlPpsLZnt8QSGDGrMW~?2PaI|e~ZgW;@*bqq$L}+LLzf<>tSi;}j{mRzCLZ5=( -@wNkEmyD&3@C|=-bK4}|f^d#6oF!(Z4;CEVoJm7*$!JeGTFOm<;0RD>B0VXgpfHH0+0?#dMG)G@Qj0(mae8qi8t}W12 -7H`79X(L<#dz;zrC%BA+BK09hI+nX?yCxqay`Xs-i7mD5JNGTNN6#6*$66WP}PtB1Rqz`2Q=%F&kcByZiZ$c*_a_>L&diQs|Kks`)%LJBdh -X&Kf|F}fjtyW=8NkO^FHo`jgTWeORAN^|{(WFvd_G`}(yb1gH&U?tgc9DARJVvb)*+yzUrbRnX(s(9& -&oKosr@3op`j9YKM$hr%J4c9hR6DGq6Fuk3U!v1wl1^rigK?|X%9L ->9IN7BZ;q?5emd`D(!%f7OjZgAJ#b9{3RLJ7UD#QJLbGaj4JH?5ygO9KQH0000803{TdPR!d$)iXK(0EK%103 -ZMW0B~t=FJE?LZe(wAFJow7a%5$6FJ*OOba!TQWpOTWd6k{rjwZ#mrqAmtQbMD#r2=RCWklNJEj1XtT -N?w@G_pJ=sOc_D$xRo#x-nv|e%A9uBp%7$(hLVhSACh0krC_nUGIwTz4!k0f7^#2|MBT(+ow-I`uMXi -Ki)q4^z)DYWBV_zeedSI?fG|i+vkt>f4zV6;`R2?!>haP{_XashyQ*5`t^(W{rm5Kd~^Tu#k+^c?d9w -HyEpH)mvPMpH`lFC-`u~uk30Wx_xHQk4?o;JZV%sX@BYP@as6j^@7}(A@x$H!HSGPvo44`n;|~vy{t^ -RkUpzd{zrTFEd-3k>)%MNLj`+`ydhy6uKl~hz`tIHKtB-!YrSlvy#J{}!gYC)d*V{9$eY-uodwciz_q -$i~O+I>f^X~Efn;+vffA#j)zm30a+n-)M{%!l>{`FrU@4w&P-(rUEws#NP7eBxI?%~aP|5&Qm_uo9ec ->I}F`}XnfZhQOi?Yp01V!zvde)w^F`Qpv?c=zi5?HoQ9;@$S*&8y!$JjVUsKfJpC_GkX|?Fat$_Pc+q)F({j%QcbhlZP1 -I)&1RecOReg?7!bVzKy?Z`Ga&lZH;)l#a{pF#XII7yZHk*{yHZ8^Y%J+62&M7 -hgU5;`1**{$Sg_yt})Wn8o~8i~P$He;Y%7{}3zr>h9f(``2&htN$go_iaq@^{ef>7k|HtZGL%o9}=~_ -2o?F^=l`!geEsm|ufA{G@{a}m-FE-&_U7T;?e^2-eaKEo-#_hcjPj5Dy$zv%`N8ehQe5=n&EHj~~4I>9-^O=KHwg@i+g*vr``1_Wt|-w(TF(oR1WL%I&!R)cR=rsil1Vs -n_|ZwCB41G|nZ9}RjYe?KmPUW-|ru!y-f}K`G+u+um9EKUVMXk`DA@zC&XfKl<*)BQh8A_Me|WYM|J}?e-ev$%m -hQ{PmYl|JTRNzx3l8J=F2p*Uvxy`jclL|Ie>KfA)uupTz~gx(S<_ZqwNJ+tl{sHYVO~Q!VK>toFE#Pt -W)_Vk)>N-r&bE9JK28pFkw$FDsevDJDT5?|@m)vldK=ku0l$GgXB<o&N$u1D>A7gK0RC>2G04W^X|ExF|ji?C4O|?J$CzKWslnU5esIWtd%R~zmaoVpS_sM65#jhC<`s`n(t7+F7dM{6i~cg>zTa$Tj2Z9g0>mUjr(&bd!a|RD#f+rI -d&XwPvpcg$yg-X}&!w~Or!p7MDQ!u`7{^U4FIySwnR|?$N-WBDD6Yuy$?WzshHI>L>|Sh0VOP<{7TcJ -5VvTd+1@avD-1AKetw%`X3WdDJt+Cn;5{rTug&kugr92+7m4T$Nyog(@!ia|*@yac)8J}s~ge(@87Yk -84>fd=jYLVD?W^yp_2q{m>W#&#%&{J!YK -m68)WFWvabQzoyJOyK;I`kyyEAZRk8`YXh)TRWDul%gWyIj9TYKTt#1yekad9p!9#(kK&bsHk#O*wen -|PftY#6+_0kuFkQqvr<(4(;Lv4f#7F{ -7%5B(rnF!+;$Z{PO6ZWwI6~fQ*_Rv>f5)Y9Lyyf5`-6fU$Y+cc4`X5oBL+004s6QFUc}$x;*9tYc2KL -)*!bbVac#XR+N!KZ#r?7EiTgXZ#v?)m5SPTQ2lf@e(mvWwFMsv25 -76knMQHU@3ey@4h<-)&*y_hvUJ2BM`h#?`zz)gYP@1>??5UJUWIElr31~uyzefhW^-#S9$FG!DIRgGf%}LV7ypjV6ih;NnS7fA+!+19T>MkCZsHz|ZpA#aqFO@D~SXpEDy<-VuqZ*IKE`(_7cpGz9xzW6sUD4B=yMC(x8h~pna;lT!NV|?A<`_0&b?y0VrG3TsR+vvhMwONexrtN#iGO -wN0_)UhhB^KWVd*!SaJ0LJBfL3Dr{(T!H>p9?Q38HmLt3DjE?5wJYutux#Dic2PeWjupim|MexFS@+s -cS_#u2&sBT>YN5B$eq5?R0OhUXNLK4pj(Ne>CL?}qcZpXhe8xrr{=c0Cu6~kfY@Dy`k0v2plOj`v_I0 -~IU;m{0Im{@gAF`!_WSC>t76hxi4v!QuC79b^DB^#dz)JnZnIuo!&5DEXN5B%7xjjTJVK71?Vm*j4t3h^;-HQ9!Hymsq*pGyN3ab-A9TLT~k8U`!G%vI%R9d -x&ps?!dD-I3gUwDCn2g=09g%~jze4ZgQg~^r?YgBbtzGFF-ROAR)67gtgGm4D!$c3G0I8L107}$9I%k -W3Reh@L?AoqdO&IUFZ@8z0eiy~klq}WcaJt0ZWq$J2JCO={o0*S$`#DhFLUL^)L&SB%L+FZCKL~;!r0 -ZUfFPi9ikYcxO1#Ez|LJc5vwEn}g>be_cFxWHI)k{A-B!QTv}*5(xgmV6Kd4d&S}4w!s?q^wa+W(MPo@?L;SSq>|yQOhJJ`5>z3PdBd8i)|Fg)GGs37mIj?&R5IaShjifFJc*iU?R -D`e0KDDSFk|hNSoEZpRK9L`5nW!=VCLr;IYxVvN@?QO!FP!E4|_s?eM#wu(G}P%wmyaEK|kicxOrD;A -f}8-c3vrOm|1)tA&DVxt9V5NmcvNhj8U1v* -s{h0pGWMMu)?wUXe}le!Heu_8KKznn@}NkJEj;vIy=jf&saF2h&Wh{3(I2~O~kbsL!7&>*Gci%knvs?JgOe$H)9pgof -5XgnkYv2f2LS|7*Vu+T#maK^w;NW3HkBIxnM`S|%kQ5eQMZ$~|fF<#hb}TtQB48=g_NufRa2ExKvhy$ -ne2{xG*${B&&N~{Wu<;4o$0ge@GmeNY487h8SsX-cEq;XWP>)cnjF4oIF~qWB9YHQa8Dj3JN?0O-$AM -UlC2o;O-GtKO3W$}6gkm;WM+}a8+?6koprm}Drh74T(I#Q7i@6Sw|5&c+0Ab1OQBunr`Ditez$+oHjf -FNs*iC}EOF{nAU3}L~jskhQ*D$+#?1--n-_RDI!7z?-hz*8q7jnLE%Bbf --4A-0m`b_ij$bA_%C)9>B}tsX#i%l8b9)>A1zAHi3n@A*HVPhVDpHn$nJz>PQ5fC>)z02LQccOVqm5l -LP$)5CrC%u(++I;0-M5m#OBp0_$Vkj%mKtT=ZOdEr(?2|Cqb@B;rtaZeX>v|dr7i)Tt+B@%!oFtc9M9 -IwQnR1NJ^yPx;9f;&=8~NVkqeAk@0z**$6<{G1eM~3%$ucy2gsdkHimO0MLVIV%z+nuv+Y!sS|)NZWa -6h^XkCF`@bwj1T0-&6ybXecPby4$b^D`1eueGIc}$6?13p&BQhTdWqS$n{*~N>Ns2rVOGt8+#7hRx-& -tA0)0D)24*e~9F#YYxNO%QIF?8suTGax3lJ}?BB&7RC)bz$055fWS_@ -_`=(6`KP`#2E20)`a*xf!}ON=m(EV+F1on+5GJ=(d3emeYUrm_TfQVPsj5bV3sH -fUQnHn$aO}YGVa`^KQgO_8zwtqUX`0V*xN&=xYnj%>c`ta3}$g#=^!s;w3X -dNc;*$b-DjRz_O7ycYSWNsvxQ-^vjPO!xTDPL5(-e`-bOFM0Syx^25l#;3TeSGg7~B|1&-(cr6CWCLI -`hB$|be_aOjdOc358@=C5#$@$~WOd3w?_L7Pb0ZTQtKh+H#FK}`P{9~iLu?BE*=nH8lm3BiE>81P#RT*5=c6nyRas%snjLMCjmgYA -M!II)s4C3JRKSOQLyQiTyMfDski=3beFwAiU##cBx`|=nsjQ8RRaFSPR&O#MFdisEtW+Vc7E{>mv#S> -5y;%$uf4!4akqD4(*J2i!g!Y#`J|kd>K?n+ywpaKANjDH7bsHATHcTXu1Bpb;!qxT#I&Or}0}M4UTM< -V1z*aaMc?}BE@k)S{u`=$=U_*hJNyy17I)P&NbwC`#p-Ng$&fe>)j)hInH#yBs@e0?I=}hyCq*7#P$W -of3#q6eR<9Lh^NKVXvkA-c9V+hTw#%_SaT{{b)*DXUPVK}jKe0w%Qz@{K`)URrpcQ{#Art~#%1T0~?c -spaNhN=~=CC*B^<)n?*%%F6GMafhU2#iRByZ)L=R8GGvMQ8Iu%c>iVInDUoGFJ>dK4YlCgpxIfq?(f; -)LAwojc&#yiq$pafzPWxl`u&>ZTKro?t3=8yyUBi50j8rX-Xtj1lZriX>?_979!!?W=JXS| -9F)Uo+yS!J%BhNHfyKew>%MB?A`10?J$)vIxk>=paE&nRSh^8kA7b|1z~1EDgV&iC;Q<89$bSBuYZWd -3mAm*iPXBixk;Kbz<5dV9DZ3fufVe&6t@8l1dT@=^EI8d -jTW3b*I}a0=}f~NRI(-$6djs*BIy_|_yqK<;ERAO8`8UJ$*N`huIV79V8%s-?Q8RL1T5vWPG^h?YSb9 -BsoW&L%(GD6D5SSpjJ|=?8MpSN#l$y*V%1*Oz?6il<&wj{<1$dt38^FzYivbxWd%~$H)5xA7|~;=bgM -(a_8K?>maxKXxj`w*YZxo2zJ?>qn4glU5}O<4ckyg=fVppG?{)dSu>WixuXtM4A@GNakjc+8#gI({3f -5ANnRI~vW|F;ZIh)Nh4NfMSOO7)L3xc1BMk-a7UZ<8~lr1V!6Fb!x7A50CPplxj+JU!{I3l4`y%-^2+ -BcP|X0fi_jAhL|fpjSe)ufY}kP1tYeIn(kjME)R-;grkuWbz+0ZTpv^xQ3mBfw8cWvnMEKoQGY_aGE& -x^J^hH18a81XNgKwHXm-8GK+am1~Ek6mZZ-i`vRMS6(-a#6dBTaS+_#Kf?JO%N@~Eh5f650I;TOldvr30=3Pt1AbQ -k8d=Otkf)7n)4opd?PdP^L$zi6?H9f6X3(02%IIO~1eZVVaSrzQ+p=CY^moMuUY1++3m8gK)WZ`{s`3 -+UsPL~(JX19FI&M-)}YIKNV6WW3^W%J|x^3pYHHhD!f-`oN!LluWCCJm%Ct)Y2D-wjxx>S+$Kg~Ga%P -nw3-wG@?rr5|nz;kM?Gbpn}7RCx+scEg->YGRpUY$iB}T!mQ_Ow0+!u4v?I#snezwphinLNAr7`7 -8_#-a2O;fi4^{tH;Ko8~xh1S}o%5z|IU4`t4jo7}UK+43p`O9>IIAQDCT3dmM(mb0^@H!SnQ-6DKa^C -awm$Ga6+VZ^=5aU#ZytpslnU?9^rJ3B$F0Q4g!V*nIU_|kzX30c=_7P5KZd5Rf=+*%t}qY&3_QEt{UL -Du7vZoLJUo+z!Jx0P?AT>spADEJuMI^;x) -Bq=E?eBHGl)i1we!O+h|MzFFuj;EQ!DqK8eyiue}EFEZ?K^EJ)ITXK9*5>oKfHcg-mS9^NLQt6P?o17<6irrRtEE+RKD#xi?DJ675TmjVh!8__pP$c+7UBh%-YkYvPpr -G++h|^cqiFC7FY=De -Z{6CQKi*WF;CFx5Mksn;~MHUa0D!A5-Q|k5e1smHlc>14RFilZK`Ul2V@YyjgqF>B#YzXbY-THZZl-G -0ynMj?j19e@dq&pl2T0qD%}P7c0@@3(p7VUrQLK0R_K+ELAp&ydMy)qCFMTQK_m^NpyGEZKg1h@u|Y@ -9v&ayZsG9CqP^Fj#U3OGRf#5Bh5lBM%|ynRah36bYEI;De%!8sL0HmMI&e3uc#@(R9 -h4$b(~8$dT*!+6p)+7&nRInPhoUJx#0HkQLAtG!WfDcbL{L~@?q=caJ~=E#k)T+x9g-fI#CxVm*+)+)I)ME+gy%KL%k*#5$$PrsJ`2R%UpfYqV`*+p!8W@BHdk&8b=LN#j3^ct9(`Ac>Gzdhn7< -S3vgeOEvr-R89bTHRNKx&X~3*5<4+yLnX8gN`rrfde1T>yLvx?U-zC8E*}^{xZC^=2>+%+c~S@S)~u* -su#yjIh{JGwANI}5MRiXB|gYQQokngRf}OV3n#ts9!qT1a*&h%i=?-Xz^7p?KYkF~A^ -gY{E$tk^tJe04IvrMS>2wc345$Sb)b1$!Dk3G6&B1yi@DQvU8aX(oF6t?kAUB07|z=IF!5I3xktS`vT -BQ*Ju@38l>9<2Pb7+F#l?b42D1g6R=aIf?299!h)Ws|*K%t5?F=87RJ>%qggq&MR^yL6yIO_bDGa7i!M=FufYc -@dkB2!*kTDvjoLxKRBN3dc(i3}QrD+d8d#Z)x?>S)0J|=Su%;Eru66Q#&?Z~f+O@_JuvBSBRDKfPw?iE1z_(r2%x -QQC1PQQ{uqlN?W2IRuv=qJ0*dJiYwy@0x={6sBz{vg}ic5Na0R~?-i|XMKg*EY>r%0bjGLkl{&Q%i4Q -fWxHanpew0IwZpL&PfG0Fu9&97DQIG`eMQ)J*GDp;szbs|qL+Al(* -Hs(wszu>m!LmxMZi-3?f%8yK4>s~5%m3G-|@TSd@GTnYl|HUWjj&fqBl^(T7`JZtu{NRgh|2#(8)4VC -VS0fL8<>N0#U@V`O2?T7V%>a>wGj3otycoo;(P6l<5m`J=8n__PeDcVMpOeH)l>5>Xaw`qvsDwP<^WG --%NPBw~DeOYa=r_*Gy+Qq>vNK}h)Na^Q7bGR --xGd6qAV9gD9=_S_-;^j%jT5MOd<8PlYdl5h2VWX-PC96&znVDfF4g0}ynP@s6=OSL0uUblYt(INdyX -=h+un0`LAovG!@D-k>O6?3xmK0LEb*S1RLj3O1i2K%Nf~Y2><=FPYvFDU(rq1b22 ->uwk4yHAytu2d7r1o4yp+Hl7{WVtF+l9-KtTl^l6b(7Q&o1hLPw>mda`JEHRPXz(@#qjQQ~^;o0heY> -;mIVG)a+vAVW6Bidf_Cp-uY<07j{dbi+4)5$gU1pz1isH;7{LAvd>FgJxv1K!0Ui8hlIGUz}A^VO`YY -o0N{2(Hz-UAt|lf4P51w~5d?lL;1cS2VBNzSuQFHXK+NvTz}6)ct_G*FIw8c(!h?l2C(mTdSo+J8 -v&{^&IP13E*f65qTHZ2jq04*kKRat1yLk6biaVu2;2|!~SOH(u=@&%eUNVkQa<9--l3s|#dhwMZM&doQVfykmCAUpel -ez7w@ZkQwIp$bLUwj}ZOoEYK7Zvc@D{s?Rg?Hmd5&-fMp-GMcbDc#y0x$A&k}AYi -=>CKIdNJt9TOh2(X~3j(mY7FRm`AUbq5o&Um6#jIFO)_zEt;3Ru)-ghIo;sQVzFfI5RIC^~N+T3m2T`CRfwon7gIP*+dCe1D1@g>c>kO}}w3Q2WtC>E1 -n(`-l)FExkg*T9`T??43%!hq*@qY*{Wgo)VxFv(A{lXCM5Ibj36C|w*UffPFo6QEHw~HUGX -GcCW`lHFq{ZQ1v%ITmgis+Hob5uDE(&+LLVw#d#YrMAaRFwc*aCB2cm<@}Omvp%p{&s2x`2x_o+v394 -l^!QhA``Hzap-o&TPUWr}^=!{kPM$W+VXkq+ly@60<}b;n3A;7D{F*dCDGu-2}l%#;C<^eVuz>h91&w -A|Q`B`2(nSJ^~4t8g7UrnnA<1;Uzh4(h{Khc83#2_I??9NVggAF709)VuoU0zzVcgcRJC#P1jKJdRGx -;R`NtD4Y}F}>|QR6{(y8_SeSx)=!DkE0^TfaAui-;8{k_tM;(TDM0>FQ5=*{GLdH7PtEJ$8bX%)M8$D -__>{E`-j0wB=mry#GP_i0rOC)Zn?7C$MneOT`^v7;VNLxc14YRUSPD^0#c_x)oBT$g;;Wl$USWg>?gv -=~;bT8>^;0RbUab6d0Jglu?fQ?#!ZaRoWC*3Y~dN`7p}51UC9$xJNZD8pN^7VVOO?)`4l!U}dV?L+S)~8WIiG8dobhHwk~Bh+Pq6<6$M# -)pK(|y3J4wvghi^lEb$a$iW?6aEED8=r1k;q*MAlDa8W5;!k2G1bMY>9FT4ksd=Fg6?=pSBcBC6sgk; --XHxkslesSf$AJ?X;KLq7qMOxFvX%nUZOM}CQ4kV@JehODjb_R1gRZvM+BM}A2|2+wIYg&-^HzJE_G@ -5DLZ-=`L0IpPtnzKQ%SYR1z-~;NP2aTwz1kYo}@#wI1Tnx2mvv -fJMDi{cBo#vTME^0Fo`D$Q2Al()!m>Xs)c_fZ}J2lO%OB*um61mjurMWWKj426a^ -@y~yn>1*Ih-R%`3`Sn=@z={8#$HJd@)%aBir3p{Pd^282#<4LJw{6;|tAL#ph4NSx_$sG$aw-+Y<4V?XDF>w6-X#m1HicN0@vt#+GSe}X!3 -e`n;oXy(U_skXKI6{Qa)mp!))>-lvDjC2jmUzrBN$2sz{-}7Y{eI7^VBU2P9D>jSo{O*<^YYcV1=W24 -vP`n3?lof6@3=Ov7_cBl!oL( -rdpD%8EK9Pe%uGLs%pOWPS@|UF&Et}Uz5U=C_(bzG+X9#D!a2JpDC$k5z{jC+tXcIPJu$W@6Z{>hd598g -+X*sp7#W=vJ){8z{un^(8BTJRse6FxV=lEwjmk0$gK^OBqsp@cbmty&IS4vkXX0Zn`-U+5alW9(4vRE -Rj5Z59|^&MLx6P!|=Eh|HYO8JI!-8a7ffvd!LQJ=)k#RH%PZhow3D3d;sEE6~E3hfk5FWjJJEdne#OD -qISkR&IV&!kbhZY&tVZRu&Ylrm|xSJ35l|y3r<@zVFNq?#|9ZbopKTMAa+fz!$%HCw*}Ze3(0z901-; -|EP}~h!q`g0tMi9$RWJdLl#y!pUcJxSJV>{cy)hr+X#hT?6*LI+t^`nq1h^~{pgdhDN`3;%E)+F7;J& -mknD>BmyR~`ZAws6AXzk})==s4*CBEBwuG6>n=Fx6AxoS;ZR@GwY9guD}d|vWcJCVwHj?3vIj2skFq~ -eqo>uZy{32Ah@tH@gP1^rrM4~|2QwP{2+H5=!h1{XAoof-^3AaAx*B0tn-9Lc@*WoPARd1*+uoY2P}d -8irThoo#7=-{%$>MbweF%zcD*`7P=Y4d_3(1YWc?l16ezL1POf^mY<2ucZ&xYCQc?QHXY8hF^8isp9kqS$&nDxw=7d86sA- -uOPDT_8=~)SKnnPlc0q&P)6$#|pI+j2$YH_$=zJut$nUUUKt{gM4RKZ{0IT@7I+GKRG4V7!-3?kY)+t -j5q}x8k;|}?@CJlrSQ!422cDIPlg5fUTop61ZxC?VU!-g#m-e2!-@us?lV?jO=^MWAY)X -H$9J1MZf<>a?28LfdJ4THTl6AqEOuCt#zbPzuXqK{p4a+eC7cU?JsZZxHc9x6}(nmBe&(#;z9VcJ{S- -wlNW{0~t)ZTqnmIkZ!Ya3D`uRO-sChOn2DCeaM{EbOEzir>Fh8kkMDeE~>$_J>V*rt$=i!?TnL8HRd6 -2o+?QQKF@+9)X%P#oiZK+iA|f+c3>g|IwaeE4Gf3HaxLZbgp*62Pe7!dLF+RZS(O#x(GyLfAJ+5fo*{ -|SHxfeUdd0_(ZZ~<75;{a~?R5ZZO$M*Z+j|WKrZrx4#^ZtDuyK?LYnN6q`T3#rUQ-x)*wl08a^BFuK0SI*TGw$$Wra)h -FmExq}#&wB-J0xGVM0ccnImK?PN?1BV|b0JyTMqiPCmajIaT99ba`wx}{cDvwb4!ro?xTS8I;cn3%O{ -H;VOW9?K%m?B8(dT}8SE?!sfNZ1tUxepbvw&8lbFvRn3vfwp(4knoht5Xb6Wv-`H;K -7dTn+;J?Uh3-7!oa_y;~%r|sm>JdX$?!=cPvWTUiol*i#ox6OBwbxdVY@@LUMX$d8SRRaMJAK}eBCB_ -~^G4gEa;RkCePB|=!CWSF*X+;i9u@Aa<=z|BF5(;viMM2NO6mBHZEgo|-IVAZS7=)!s?U;+job2=yk? -;%|-klJ-h)9Y}4o{mVl3{-`1fV3~I@TF_2c+BW)CK#pvsz&7ZD%;vQ*Q(P#I|O;U~LeBvR$%uA$!-sN -?N-Hrc20*@UEoKR$7bB1_Hdr;RlX{KzJ-S9$)WvCfAPs@BQ+bgRg`JZagl)k7wcO+7N;E+uI^97cdURyx)p-Ddk -ghmE?vyFVlhq5|1ZhqD7RGq|ufQ4CheAO64|FK5~%-HH%5T>&X3QF#Z|tMe=Z#Q^_A;gm*be)VF39m` -DYesjsn8rXy5@S~F`PQiP+He;2!vgYibfrm%6NGb$LFg?rx++S7YNNeDOBO5*C!G>{S5D)zF_=lp9>D -%q(EY4WB459%3;=v{EfMuG?2z5xe8L3FrrIMuy&;H=-u?&J&ce$Xfv7J<&4hUk^JxEVpKY&`b*IH{z}ld>g*!`a&9ajn3+L%QvjfNjtodJ8TCim&rfka@HL -%(;1#A?r3n4^+iwb1HMQQFej&5B)Eu%LCkq#x;@oWwHqIHV=cYr -t?H)s5QI8h0gj7@yBF=`PyYZ7ziy#gXP30CEXpuW~YW9%BbH4^AV37m_q+3KONTa*G;-n2s}@dkKAR_rQqCrAg(fi=%l0} -~&D2{{Oc$%ElxVHfc54pg~O0?CA&Ww%q0b_h>+xTq!_(rxt&<4K#Yl8D%{(}T!yZ62A_eGYgj_55Ty1 -@(p|(C!hIyTZIqj_HtYi${3lyov`JfS2~ut{NJDNQo-t_D1bRfP=kx -R0Gp#@+i%UsB41+4WP##=>QH2zch1~pRi+#DKF6p+bN1jj6Jc=GJgr|Yd56CBxV~D&PU{S(BR}z6V^= -+<<7heO@)_l(U+4AC}*>ZTn)Svq4q0ma2eQRBWm87G%WttwlXH=x6d5~_aXq$(0%L>b;bHhA9sKIilr -!Rr4D!86WI;GsBzBL#`T#LBD8OAYV;$Dwyu@TZT1Qmo#t72V0coh8#jme~3kKTZj~lH+1YNV1~`X^{hwuOz=)fP}t!F2(NI3?d%{F&=W#Jw&v5{+2>dF1)&gIQ%UX1V`ba4J1 -cd=B}7B@K^^P%T|+!k1Lse86G4&`)1*%?~=KQ=T=7{=R-xRU_+iv_gU~}xWjnCkqn$Y5dneVSK~Q}*2 -DS&kKK0GwzxY92u(WGXu;iiPGzXNf?bM^O -gJl)GIfx>hDZTslHGqDW*~A4VC2PmVFxWK3z7vqOsyA1nJ*RY@RJ%L+8W_@TCdSNjvASD<{ZSFcCjB- -T!kK0#g$x_p3N5ut0;b0ADRt)}ZqOGVw!+qNzx;z*q|_|B@+zE-?!i$u%X^TX=QMkA1ElH%@jY`YK&R -;1FMb7WykRRmj>Cvi^*2;t-zCnQvI62{zo3AFj;i1PpbHi5la1>E9Az;==#32bKhHQqn)x8?I>{iAPx^DG>h6mBG1d_g5zt4RAMQ_d8rLVs?j}qQRbx>GzmWbe12c8)~+Wqq!bVV|$4j1|>J`= -~@a%x0I^86$e}lbjNS_EdeBunb5S@@6+owV>m3 -`vhqAI-Sb!rk3ff;4^zRIECHNnty^`n(+_UP5Q`Hn-;ZnH2v`Q$ZjoU_HdF1LwS3(~1Z-O+JDlgIhs+ -F;uu|sNw0Iq9-|5#ZEL#ETHk-tGT!QBwWvPd8*_a0=OTe}ma!6{zV&Px`-w54VSt?nlueUT0(rvbn(H -bF|Vb0h+$ -aC5X2iD>dBAE`Y?qk?N4D2Mll?Ak~pj%G_1e!14tuBh}Qx=NC2N>B9L!~r1_I%B_uzA9rpebHq*R>a45EH5UV5;-66m^Pe#24wj{KB{vgp=a}bx@N8s~3-K$EF1)~?q0Z5g|4<0o<+zQK -F^8)m!?Z%8f=L6?aJpIzqi$7TR!{nRic#5lV3`VHBTgKs@iSGGGS7r+7wn}&RfEWw6Ku6ir4{Yf^bf% -33^rRI`DTThV+fXM+sKv6K^D>)|Zi|Pyp6bRxctjP45=5bK-s(3hnHr0>yWLn9Nl9ixBHOFhMBRd=Km -yF~J)0A9-vk-8lLrs0W+=$Etaib74Xx*g>TawwtfNPPsi~RTHzTjuh6zuO!C+L!6gBp-zA@W45X^Ur2sOD=!^z;~89tPrF -;VO?^J4e2OwIAYUl=7++vLO}7CWyt`?4EQB^gKMMT5|F1%LH$w$WbNj4-aM+x!;ujAsTLI~|W_bpN^`~uCY<|-I__D0xVM`{8c>2 -9|tOv&k*qQH1`!cvW@M( -5MP+8x~L#5$kiRmh)en?GzTH05O7x?*jVeGN>&VkjwEXWm<7Wm(wwTx0i0`I-lAVJ1#TXB)L?*)O}ID -!J%9#EE=87+nT(dJMcfTRu@2&oy?&Y^|F-cnekEL)blsDf=yeesHZZZOz1F6{w?5NhRX5as%``JW1W7 -yR8oRyqaA)4f!{m*(MALZ!h -RK?MhNqCwfe63ZswMvj)HmcUY+=1;yER%ebBV^XE%gcSaj*1&w15UDxGSVJ5Ehp@#peCDvoAodODFg! -KX!)ZK49>Pqi^Q?3;*v=)_jA?5=rokaGY@Va*;iN=T*d#K};VGSY-s%rWXKRMvUR3!TkolhbMt=yP)h>@6aWAK2mmD%m`#xjbHVe`%Pte^}Q(fJ3yM6gX{lU{{O5;&9n8pL@k7v=pbfvc4YE!ahnpwG#S}2N4#v~mT=7&^D1jc -SCMJ#9?OGjFIiONTQWo1gXWODKsI8Z-j?^01RLPb$&nF=qBcKB&aV|PD5Omk!R@zk>LoYA`8G5-3Lun -kvPELxesBz2j>K#x5C49pM}Om}c1 -kZy~xSg=yF$GPr2LxzmiK-qCL1J-&btjlN-xXw6hP&MVag18V7clzf{l6AGs<^c*f0;WT-9KrNg`7W3 -OP$)Z#e#OTTj?P~}J!DO(Az5x;TN8?fQ(x@MflIb9hX*gR@Nb_)!jH2Z@T+n>En9t%tKom0{EpE)bWb -(=49F&EDlNoyCR2gl>D`U(lOKcPg{6)(8w`Qn}-t77X13r{FsE5iJ+ois7zA2rAz}|oA_3rO_f8O_qmN6pNJ~Ws<{?8-QZnbi23JQvK -x)s*3FRfXlf6TA>6HO}RWxw_uAse%w@3IFBu8Y(g=do&qNVih+5f<$-ag&+sGe-z$PJ7qv^Z_tfM#=N -zyF`d{R6CralRW2&pQ=~!WHp&R4#urks)Td<=w4vwvTj@p91E;QPOFu~%~`EFj#Yk$Hy>Bw`gGn=b!k -@-oB+4yS|ko8tRU{og|<6COZ|X-@5D+p9shv!U#`3H@o|gG=n>pSzhZO{@9R^Ohmo$dDEMQemVIS4-Q -cae@6aWAK2mmD%m`)i*($`}r008Sy001EX003}la4%nWWo~3|a -xY_OVRB?;bT4OOGBYtUW^!e5E^v93oNaGqRd%hv`&TRpT?r&Z@Ab0ZZYz-rC_1IkK&lEw+%L#fnJz1) -Ol;Gw%CEoUc~%i3HIWiN6t>SfYp<6%#~gFaxy}b4{QiIC)2DxW_Eq`v*=J9`dhxV;`sLT3{d4(m-uuD -D2j%6P>+<#e&0lWbUfq_@?p|M)n}_n}yZ^Yky}iosKmGK}+nZNck9YUw)$Pso+sE=M@A>57*N;AXd-H -gcPkw&=*X!H6pRe!B-H+w*cgD>7zq)>Wcy;yj^?w=m=I-r7e!c(s?%rQAaQWu$e*XQ{{q@!3_3QG(J4 -gKcM_t`B*3a+qs5g)0!_Ph{^*oOZ@w;#Tq&&I3Ezf!HLwSDvaDD&R>(_H7pWVHEyubP3mn`$chmZbm{ -!+@HukQb?d~f{Uw^jh_U4ECtNVBC+K>0w*X7~v$H%{AVt*{}?tUq+ -uHKgW>(@6AbNFn;W4U_!`VV*a`TS3JuWx>Q$De+A`#Q^E){ocsKRx`qhkyF&+w$f0LuOk3bp7`FKCAi -Wmmh9#Ugg7I-n_bg`*2;Z9?H-B?cvRDJM{$E@@OV}h?sR&{^%_VHaNo&o;+^!aE1lutbQ^x2osU -Vg{yzj*fYtEVqslrO%1UY?Y1o;-i~?6YsbeDb_}^X>C*zJBralTuz>U;nzrZ07H6@_%mej~Vi(yX@ra ->&L5`+lRUI?{d5knc(f~^5*KV*E!}_*Eg}KaupZ(`Q88O3~%q={>Am?Bfo9vAIr^;8I -D_jjk8QGPq$%NYHuPcF+^^QNn}|8|=rc#)5Par0w_{^IuT?*6iTdiU_iTmSqdpIut5%g41ZwUlpP+-*QgzE -&>t>ARoGoB!DMtuyeNQOn04|6^G`*-w7*hrCdiHr~H*w)Zd8o)_9Oj$dCm-oLPHc_Bm27wRX=^6LxD= -{$RV4Rri!&3MXlwz&L*kE?zC!-w_b_dkA=zxFwa=W*^#_wPUWHt%8Te<&aJeO&`6`RyZztLr{}>D|wP -#NU1D)eksATmKJMHRpEsHlF(Mh(G=G%d0o#O>j*NiK9LL{Q1SjJb~|De)r9@&z^kw{pZhKynO!b({Eo -s`}(WzpFhd}eE9j>8=rjo<-qODzyAJ<=THCT`>&sW{`7g?@!>_?_Ig?NabD -Ij`ep0exa?=kKgY7?*S_#;y{!9^zl|+#+0OjBR{vRJr+d5XHGgYuSuZa#LI!LZp{?t2S;n5B_q@BD`E -ajW%iqRwIhJj?tY<#b&TY#-W54X{v1ELHIx|W?M$3RX`pa@`=jCXbQaN|tvmcl1*8E(abgo9U$&!VC1W`+FG?G{kyrM-Qja~0TJq4k?}LwIWk>CoCDXaAc@JY{u1iL(Yvwf$p4mpm --!hr(Vc)W{nw1?#R@;s((_Aycnt8R}vL094GEA={d$BQoJ=RQV&x8Axg&k{NWttb+uWZX`2b<9{!jc* -8J^Q@m!z~Nvg(LII&h?W;^sK4$tf?<&KDKa1JxGVda#!%P|{ri^Sx!4Mxz6V9no -XwN8~==axbnQrCR^NUwhx<#4Gb(MvssQjZYD&vrH` -Zk4^qYz~IebTf2aax#M{<#;v>FHh!J+k3~%o2!P`Fl@FayOhu7NipmkN=|{(>v?=-zqXUj*zo|ob<4K -&Ge7mM#Tt5Eh(~1+2itIF`(m4UTx~c-&47*b$u42UCrjC!Toy3S>_x$d2T$^VcYezG^S+f~GS-SeEHR -+6IYUe`kdQg-Z1!>TtNxAOZXC)U>xe~SN_cwX1ecbn3u=N6v6ANYKpKC)>!lp`<42mlpNAsbYB4q#;1%XPuT*}e`q1UECUZDR`9Ie@e-0L>adi -p61v_2fKaOED^T?!cQexl+K$g{|6R6LpDq#+UHP%qEzEwE?gNi#+j}CDs;S#=&zSIQ`BH^UuJmS4^t| -rgit^vkhIytXPrb17rti!w$Dq_!^bn`W!p@nGl#1;fE5^t+u$+Xp1`Z4WHqRSJOsfM%jga4V{T86~jZ2O!=9InOfVs|-@<;4sBkuD)B4&k -(^c@J@Rt|tOBf7+kiTfZDI~>1a$s8u`A4mg9*w+QBb3$H#tDpwH4DBf3W7*?5)$@lyHNr`6IK<(31FM -yX;D3!ZjWbqSi7wR;u({-B{J=>D^8!`_;Kc6J{mL6%Sp%^Nf{79Xdp-u9Pr={dTt!gdC1y01r`O0$MX&_KTqQ1; -~zi{xJEF(Nazz1h{1U4NnG3XYE1kP6c5-D8;L_?=Kwq)GiHjNSXIwT1~vlQclBfCQ`w$IGxlM^AaJdM+WVS$;HGtx6J>du-b};1yf`y1~Pz*+0@fL#7@LD@oFEnpL;3@__|%1-3n2-po|DIK3|SU0<}=>Yfx#IZN%=%F!U*kOenT=0Eh;t6SD#X=M(G6 -GO7V+Bw_h}AbYM(BiA+_tjU2Ye^5#tyNiHI|U$*hx5801GAbb)6pUYh854N|S$J=X@-G%SQ53)|6AkY -&gQgRwW`ZLnDM)$1Y`M!Y4#~WAQP!e3pf7+8DHw@S%&sIxo6j4t5mQU8`)B!-U#aHYD>0LS<37?72Yu -Fb-0nIslfuCw6F_vI1-?INiXZc|5q|f;Y(kww!Wdz-E-#WW!L3;lAe3N0TmO!!IiTChlYRhanC2V%Ef -5H|#4Q01yy0Fq_=~As}JCV0Wg)sQ7XNLK_J|;f>)+gzc((0<|uRQ_#9M0=?nn*nlQN*h!|g5I!)I#AO -T*3KK`#G3*@h(J^O2xk!J?&32PV=1P!SSJn;FHGVH9LyKrbcG}rk*>>ds&cIjVAAxcKA#xNUd!&brjA -Atv!0Ymf#AO)eDs+I@!sRy15sBxJWm((FK`kBt|6tBJ88YB}Vr;CjLhcW?l#t)S$iP=M){$?5A{8FfP -5n+16MT&XCQg`N1r@zQnz3Mq`|i*_u9En$?ZAB_3ut_#>{8tYbB2!2aDtUC=IHEj0s;9GCc!kzCYj6( -2TU%g(cm%UXFC8+pqF#yM>xv@<6v@&N8JgBkOq12Cif#QH>2K3+Nv<15l@zYn}NxpitrdhGMTM*ZqC! -lK?q@;cofFU$wQVpn-&ko1c9ot8DQlGm3@_&2t#D84HH>CdD#T}Pv|?@3)o%okc_(%x%z~?mBa-5Juo -fF2CjYJYq0bNVUC$2U9cx>Lz+y- -|<9@{Djq_QX8%Yi)ybzx%6d%gh7sCnRc96nnJ)}+xY-J-K#~laOeE?ruB^nbF2-cMyh}|mk;|PU*Mpvp11LW`Zj3C&rX^kgK6-}q!9Wiq=(urjGWSLIN8ALCyB10OSXFSq -tGbv?6QKu6qA)aRMm%Ml3TIj${BW?^jaVd+KM}hYYYkDyJV0R -!v}tBFGe_W8M__Ak@}k8YT~-b*009Vi5YB~^4r|wH-L!e$;31-@DKGBQlzb=Tu=P9p+NQ>f0TEN$F5O -}&9S*Szg(z{IH~^c>$tz-jHazKAWvAKK6L*&q;ctb^BZf_y*GUg?je^~`Da@h%Er(*1JFo*<1OvQc?f -4dp*BGnh8*pfd)P?xbcR8d|6%+%kNa{VHfkqrQq?-VkdGtIEVz-&8A=vH%Rb1WUVvVw^-<}eJ13ctO$R4oyuHH=w8eayi -t5gKLUjXaF36ssO#G)NxnIJXLtSkIE@P2?hDpy8m;{f2mSWQ<%`mXS|I@L4TFp?=cSIC8FD!@#Gxp#e5_JkdSgI!$naN<+7nZnmp|Lbp_*DO(+CU52`WVO!!+MFMx -Sc*8VEPw?4zBc|F=Ktj+gv0->d=tISWe-KVK^AvbYN6%mJlmRvDXf$&e0{W4a4r4^;!i7ve8C_Xo@nu$-@CoJbz`t9pQvzzAyT -u3Y0KssfKbI4$we|i-z$uSV<-MN?fB@vMExtT1;t0J14siT9U^>A<0#PZHgoi+HOipfreO7DL^E2AiZ -(^`=*Z~1o=1I#o7;%q}Ex$B-}P-2@4_FIKo2mWrgt_hl8H7*|n)pZsxNHu~Y4Zyy6g~Evu%~XMztdVD ->{*WJMfoRM8Y_s025HbBs}$p2Y(`U{TjeK$}L*pcFt6avUHAGC`ZVm`vbin~{++7Ozxv%{Vln9Ip_sj -5vt+V`!xa$-4zhAZG4iwbIGa54NR{XRp?JmHQe_Z`FEM*LLv3BAQ`A{Sb~-Dely{WM`<9FFK?CLe4hT --ig!r2cd6+aa38tVnSv;Ry_|TpSaxRhciXxBg%LPtid^y@Qga}g(Q4rkf)UofdF4tlx`}H)_(M56c7S -gYcfogjAj`en`U|BXR&EGD)bi@O;n(L0WESCLJlh(47gyha^Q1HLhyWbD3g_@ITIu0pGy2NU2DRKJRP -^{C^519nFXv&gy4J{<26%LachJ`LIO6`iXr1bN9h)^o56;{!D@l2lc%tA35 -@B>ghNVY(iq7YPlnHoryfRI}T+Cd=H0sAOqT7!s#G}*?~#7#dkc(W{MODG-o0DNwnt`I55fx!ePp}t8JRiEfx;mSb{5 -h%QzFR>!N79_stBzG>eh}!bC$@neabA9VMvBTmQ?5$YfT9D6U*^iPM#F2cFV!6jLg7Nynl -;*ORmZlwfsiS1>Xc+UZXdbO^AXC)&}C;m -#VR}wZAZ|qHiZ*5vjxn?EE&7o7sdW&TLQrc<~}=PSlMwXRgt@r^Wce9!2)G0>5kzL^rr;2C|pt|Sti; -H+ZpH`uzx}2$|eKL?g+7DQU$fNAzU}OQ!$~W{FpqBMAstS0Uqv1SC&pIpjZyANL2(+X*N@!>cL6r9Vk -dP?Nq^mQ94c1r$KnS7~O(wA)X7NaMWs>2r|VWd&lgnRWpR9Cdnnc8qkN3iG?J}h||;$A$sJvo02>SN3 -;yZv*Q6(2V=K*f&DgJ50S5{ZCj9`BcyKf!_{5|9GZMNp2_EwGY-iBXJo~yYK(Slke8BW?EDSoHGpdG1 -MG`-gUS{UhUtJ&M;FTSWWMmXW^QCgPl)Mux!G-J4q{LuQMXNtE~`q8+xN-jA&HC9n5cn7j_yPwK&AU` -rJ<@pj@x2A%b;}HIJOF3c1z*giKVO6RZ1j<@@B7=N?~oQi3$3MKuIPTdgAJ0HpqLOXdiy4iA<~IFbz^ -8v3quJpSN>(^8;5~NPK2i+R*5ta61QR4?3l$b37V;J^#kPq}`MaE=KuqL -LU`Joc?Ii$UV{AsnAM5Ef~L-4vosO_eCR(UP;b8ZT8fB_7@9xkIo -@ldCwQ7xI1Iemj>*#`1v+{@^Ef%nvO;7|a9hHxWCq$BV%qB#M3)qAdW?j#k(NH-CZAexMGESpO^I}*` -BoJqOR5ANW5KUmd!xAV2q$nUEO%^o2bxe6oUDN(B^U|4CFcD9|q|?fQNjg1IHb)+o=1%i$%o0MVyV$C -CS_9c7J(NEvF;p_v0f+2Eb1+h!Hu=~|YkkX_6!3D|)u?5G*z??gWKy$ksbAWR8c{a8i)@O)5)}00%A2 -iWa1R=Ps?|FS8tP7`Jy8d)cUb$gqQBcGAmbn@pxQRf-cbSuhPT>=V);~7h@z$#NH1G8j8@2N4Bc|Aq7 -rHffw-G$9#;A7*Q75bZyweq?f(^@XmP0S3NeKC7}$#qbjDIcj6FG<#z>?B%I$$uG%o0-6d~$9D?aSC{ --3MpO2(sku|nrYu94XfzKuva3Rkq97_%4`oqaVi7iRomOuZYIK9yOgh_wpct?m8$k%IW48y+ -4#{E@6sgRj%B>owdV*n3hG|8EXfw;AQw=h7@B&FE!ay;5r*_gbaR_0RNLaL4(O{dY4L;O~T*G1|KXvp -M?%F`A5hKXQyGpns3}U%r2O?5Gm7Py}MJQ;U7P=SZ4cRu)m~|iaBo4c^PaF>JM@BSGYrA{?W(Q3(Gq> -n3D>QwBm#y}45UW?wKu_VQtu2W*(hLhUkc5VKR=WxQCb!uvs>wnjRHWvl8c8rrxr}fe3cPYChq}WwvY -0d(hskL}-wC6qR;4-)4y}{5$<-*l?3So(afeFJb}oabWodS9*mi5^w&-thZ)zfu_JeFPiC<4GYUm00da9_E -6iwWvit*HA@_T4L3X7^S8Ci@HthIckTt)z@^&Z^#x1nwxZeoK^4>aFUk@Y5@ckg750@it5_T!T~!SR* -iJ7t;MIn0Y^Ncxmea;QrJmK!R%!^sm)%9PgOs&}X-2owNWCe=~8|HwC?>3Mhisj74)`-o9 -7oct>vR6V=p`2Q+SRxLM&2pW6a$u{{E#aeESDl9>(7++cH?Fy&}71SAcU7ouNoxve9CAW}E$6=7~f{cxsh>(WF%DO&OCuu!ml00pc)`ddz|uHb= -ZL{@E-i`S!lWv;p~{9locoL02m#U%OsGVF&N&c4jMs@Zkn49EyF3X>T%b|qRA=s9j#Kop>D1dSJY-i$21M-i -==N7B{775D!#4SqnJ1P!i^oifJ+udoMwxrcKPV|4?FtFl134Gp#h~h8F|!UZ}D -y)?A6|4w1Iok~6gfDKO;^l5#{{cmTTTFu(#OUS!Z6n>uh;P{7g%TnJQ!zRSX8$iCP^rdDgmI3=rk+NG -%u1yT|wnw>sgzTHZ$4cM`%J|t0pk?**2;g7qx1?Mt0JXVzVlI2d>;`)NZ!!d -F!RkRs#0W1nIPpN||AiR?@T7r&fVn);ttTxF%^xEU^6U^CZ)(p$wM;RclovSXHP6A_55goYg! -%(+^cn=S5Gk&QCW5H?&7TSgb}*qMlW!G*R2tgj&v6@=MhUkh}uEjHF_JEsfv2AFn&6;->tpfS@GWGmj -}vOa_=jWi>@_?k!$KM6j9(GJCBy4GEoFnGyeKc#$)p8v@egf9F>NefQRGG;tFR+T=4*M=H1 -e3`E^|HXY4!=*1_qn}28VYGhh6uxtI8$@)oTO2i&99jt&Q-j25VK^N)-F*EhNmhql#2`*hM23FZKd@` -{PuXhwrZ5go4m|4VviKU_ND?o$OvKB<|b8Nruf1XNE$Yva>yX<_xpkg(ax8dvRrUZbCD5+q3m3aLs}x -g3i7y1}W(2VYd`{gqKcDxtgMkHAu*xrT10&81S^NV?#}&r?$C>)2k3($FMswMqd54W;UX$EyO93GyXHr&#lvSS5^&{J?M%aFOecVfp4X64K4Whl|o7ht#MJ_wHEMalIyIn=+XQ%5gWzp(I(~i!e;sdWzEht} -5NUGq1u8Y|9tywcq6)+%nyzEDF$GM^h|^eNm7*CbP0<$wrXfR$$U%4ED4it(u_%%vR1xnrSy4i=ATLz -Ovf@%e7&IURADavm?9B+}`n0w;`UR+br|Sorx~tkaOF-@M2%&@V*Et*es`2sfZdQ5NcrY=Jzav8=4dl -uyZcYEOwG`00=?t@Y3ES?cNX|NO4=LfD5hKU2ExDHN}+CW*SHdqjpT9?eprgh7B}%_sVlHrs$B}J%BVMEc%}|lRn%-uWQJZhhK0->&{O=%fx{ct^J@+WLsrf=g&L^sYC93>pwfs+`01@6ub2 ->ht)6eYjT03i6Ac1@vH}229{k!6vZ}a4<*poFt5;xyjFrtBC#ncosU71qtyC_o7h4-ChIVsFuV-Vr#a -o%D1ghW#1U$cp!M+2_<-5&(YG07vu9&*n!Lix*C;n1`p_2h^xNh_QFX!!5`$d#RqMN#wcO8H)8;iIVQ -4xabEVlX$2UG7M5_D60D-a_J`u!}aeVaV7dAiKS*EU$Wz3@AJVKw3Mxc@Q-a -c%3wkC@;pCWG==rI})hyP-@z|8n -$|;mgK9>qJ&+VmUHZau?mZTu-p3U&2L!kqQ}BIZP~V`adWx1XO~JzXyHI&q -&LLpWiJa3P;P4h!=`yiihYq@Q#O&K)ut~tmDozqL?E(cV3pLVR_~x1j{r&(V6HWRSK!H*G0gr}uku(d -feo8H3gA;}L#y4q5#nC^br@b>@LM}hos0Oi+fiyhi$k01nI3vk4T~gs>;$f2^d<2e>%98O{JGXt#N=5 -V8{QZPuUyn-f4_M!L3h}F?gb0)xGJtx>LS3`N=j|>iJ3H|v{Jo1yxFG34$!yZt>tbJO3o;~=D{HEm>0 -*iS5iypV(1(}o!UtOhYILcEMT7AHc)G65_GYTXzWdbX1$R_6yj^PfLC+jg<+~0F%3M;cbhI)ufea_FB -`9ly5lgV#vkC8XbipDrRO(J5S>f2$zVoyn~2y$%Dh(ocl)0dPphJpH;241&g7KG4jZBuuPB-=(m^iuR -GhFwdEQXvrY@*qr#v*X5t8dUdmkug``t2VcW4@90T#6AnXo2h*NETo0Hl#s_V*YDfV&9raCSU&i4d!s -v4ON=e#wJ1HWr}lRqa;ucxY{;*QxLGt}2P5{ZBagBGJZ0XG`a4<#FPP6=c6d#12M_5`K9N>oM}e57E0 -D*QJ}q(B>X`znj}u_Ow@9hZMnQ^_JDrm@~Y!<*m%aZ}K?gBi;ZrRl>x*`(-)MYDG5$*a6|!PH^L^L;r -$1yW`~`$HbMfmLrH#K?WfLTFT7C#hBc{ -L|Cbo=FSyozR!b_tae%h_EyB{E?sj>TbW?6X7X;tqS|9nc=z_qRM)W%aQxu(viPai>Xu{@$!DVl{^M> -BS8pE%!lk{Io+TvguKmb?Cp2+>H`A2gL}ZDmRC2ns!UL(|%Cj6U=aYm{7BVe8;hVVTL9!aqaU9o43yO -lY#4OSh+o5uSw3|1lQ$(&VzJL#Wa@+6k&X+tM99d>Aii$PP;Bfi+unGp$xiW&jUm#@aCnN81o{o3m5> -AS68qp}qnA&fPm=iF|nbiLFC~ZlliewF6_DjHQ;ZUI&3Hr;T`Gj5 -%Y8jUs`+$hQ7pGgL;ecl`o#HuR2ULE88>JzI(0!S?+zU9g)}9k&Q})wc -_JF~4k$UYsR;?7-MruC7l3gYZnbAITgn;I>cVVXwfQRnw`_AqCi%g7rR6E?FlRoFl-^-WjXgQkP_lAu -|ri62c@Lpr!0xd%U>*c{k+9}^a$>v|6+j9@9R^ahruqjDEW1-mVIS4UF{#cdW%8{y{^RiO?VRebbQtL1yD-^1Q -Y-O00;mj6qrs8Dbuo<5&!@WHUI!40001RX>c!Jc4cm4Z*nhVXkl_>WppoSWnyw=cW`oVVr6nJaCyC3? -{nL@(f;ng0<}L_o=U2nYuanB=~Z#;M02+6v7Kfzc{3h}ge2A!sUt`$>R;bycL5TVXeW2mc|FsfO#+Mk -z0WR~gM;oL_2%r|c%m-Gr)QJfGxg@;>hu@&#M*Z(ZQNv2bEn#V>ZR$b=*NzzoS4_WeP3X3< -jaY36{Mzk0#Amrcn7zl^vw9zW%vgZ16vw=1V-i&Wke1GpnM)=+eY$x?u -vllyzZ1ZM6aAMX7qHN9x7#@k@~CTs~BzG*vgW)~Xw0P4Q@AVf-}D$|9Lo5T|F4UNLKzx_D66Njfi*r5 -dOexKd@Vt~cc(&z!hWP?}5&U2I5Kv%(l<^I5qDV?U`)Ua3fDsxWb4g*@agmCoWntHBFNUn=g{cWY#Ec)ry|kMWd;6V&v8_qRw-W*QC;)lu7WO&L(5x -iFz?S@uxY8eFdBLbV>Zfcq`g?1g1ACg*6JlbJ(eZM$%X%8IxT=_xJ?_A?O;;q^3qy*37E34+>bTK929 -+Uwyb!qsb@raddMtn%sT*32UKgyoLRQQnFm72?#@~LTBX$jDx`C+0E&D>=?ZnUyScQ5%=fgyUE$@tvb -KDQ6qIdy15&lez+LjsOt|m*H^b^LwKAqUd5!$E+yA0o`K{thbH0jI!UdF{t4!_U@(o<;9jpp+Bi)rn0>3t96mUci??H)&-TuzI`}-H0-OFCs?GjhZH8b#rE@L2GZwgo)>-fC -buOmy&Pfh@r#p_4ub^&$Hw|&>`G2n^$^n$OE)bQ?Qj9m*kpSElf&T!_gwEk~-m -v-d69;v@mOmOOv`~c8_#Yn=Xlswp+`6LUg>4Arxm+u?>7j!wznhRpu*Vgn~o1Q?l6~-(C%V{PE@A20! -j2)%+{5kj}lQ*XtyU^R*qEymBxpWRAc7csw}%`nPjRpEh3_wM=Y;M4FLfu?3e%|CU~_4N{h0CaN0P -m5GuW$8enJ2+^5DmZb}hKm3&754$)-JkNDmK~QLKhaE5z5i{{$$X7$<)H2y*nWVaC$<`=>Qz$17-Dr` -@i(ah|NRq8aC{ILzxKv121qF&Td}+Qq0$Co)gP@7>EHXuA;y#j8ikPXWc+`GD#%o-GNfd^Z!=b*E%els%~>P^YszXU|52wEv;oTW{>5MYKBrYs>m^x~RxzD3W31s`9X0qQ&X;%tH} -k5B3<^@YHd18B$GmiqA5RjbodmHhiciW7nQ32QbbsrWT0%-IGDmP~WLESW4+^dP60j(jxJVB -(F3VYlM=(|Hb)ES5u-px$bYvCT2?K61xGuPs$t&aA1Ku_YKF$!=k(v?iTVoMts|8M!DY -RWOY3C2CV0J%Ftk_8Y;?NMXcbD?zEb-*pD4e1`UD9j(CT7Jn8Q9B<{-a5z+^j1JX4ogK4N?Iei4Lg}J1!Q7o?a{^ -+bW2pU6^yIvN>|E<^eTG>}=;a -%~8~rs#lopudok5bY -pCseK#C40a(?mKRH%_JauY!P0or7K&obXsfA1I4!h6+VfyEGvd`#r$P@7B$sk^oF&|8Zet%hf$~7Jhy -h1%5XF&q`*5$iqCJSv+ridF90W^F2rq$;-Z0P(`o>BAb@&H0DTCglQAHWQ -0#?3wu8=#EtP9yVVzg7vO2y#9{NZfb)AcYfe-xYba66&tlcd_EUZLNSb1iKrPRBj3mWosR0Pxj3QJ8JwtPWtXCtqfgm?!;YFNITz72T6Q4diC8QElO)?lkyQ1mFSF9}3%H8D!6+t~mx$-5d37Wcu^#P04gVF8jc-)H3aH(CPA -bO0G5u_#D&vh9FuQ)?z8{}(*A2}#tl`0C4V^pjt&Qc@fhE7rk+h&rajK((ayHW*76>3hp2%LIF?TX7h -AhbYaiwvG@r{a69*7*pDgWEd~;Oh>Zmr?Edm}HdZF6nPiSrFV7ZNd(MP~-^7=LDogHMyH>ZEvF_-DgB -qPYS~P+DA2pEhGTB1PS>ycdyebNgZr<&~TePp{6`#%`WwWh`Q2AG011mgF%L1F_i1n-Bv;fu{oDw)rx -GIz|7_=eu0eW|0){3v4?n6*&0?zh9&Z?GL^RL;?k+vDY_McRNc{q_*A{w;!}pF2xC$xEheETOekZ70b -9c+Tg~TGD;!@w$H)Y7n=^!)hy`-*eIZEfzo~gN4jVVdcmCD)h_y<~t)Yw?FCFk`%}8pM1~fGms&0v{) -3x4E`~w;QQ6CSFkIAiI@R`U0I#5f*)7uP~q-ZL(ZAML$#iX%9;AXZ2vK9`x1s3ouxq4giD{UPz-jR|0 -+fT0cClp1pZ=b^D4y4ux-G`(Z?zN1l>Bwu>MUZ^29@KYIAS!g6dh{{!3nup1-KCp -@`mDC`mMJfn+P&4`_K;cywsI$TEfkOK}GPa;AtPukG;oe?^p4GMl?zNKA(W^Z|IS1`j@F&$!X-$v76T -2s3mG_;GC!>q|UqF21G-*-MM@)IfBur2KAk5M<2K4}Q>M -OW%iL2u-n*J~FDiVkCMX?Gua2G{qoEb)2K_f8@wrTIwKAF=9xS@yw+jazuQi0|n7M0%oTdbrS> -pq?)ddf?RPAxpwc)t^MbCIJgM#7)PB<5T$~PTmp%MCgoj?ao4Mb=KY^RBaZhFag42ReG>aed4(M0Oy> -W~jTysV~3H4kAd5~4*bMYBbanRDAnig0&k%^BBxcMQJKAmnQT`T`{zQd5(eSyH-eVH=qN)3n54wYOcL1#vYqCU)^O-IV4Yp4tklR?KM -pzOjk)4KCFTT#H(x^kDOa>l>R!u^vO!Ur@M_#Z%p4K&|c@eb>0n%W)Oq*~CiFf#Pun6|!(fxV&n<4 -I@&!E2#9Ui>B*cLQ&nwfS<1)ei^7fkPsQqrSQ{%f6aaS>FX0=iz>vlKYB=(m)-S?PS(8QglD#7&`kJ8 -U8wHOZVK{Q8!klzVpv$8TE5?*+M~-SP*C0y`$QMuv(kacbPC4;5ZYwNk&`78a~UTAHHD?0PLcC(&w1- -_5pPh+bFZi{5w!b3c&xLPBe_*r>7Itmr~@Fa^uXCr65Y -;-eY?;GR6jy9Rhch!>F%0l3>Vlm)V}?7Fil#>yH^;wdBssnx%z3S$q4$0Zc})SNUk~MFUyLJ`__|q#q -$U2KG8`xL-qM`bnQw9`^`+silwi&)a%!+uenfc?ogm!wd#}m;Am^SqG^sRQ0AcMq5OU -xym&<{Fn3=*4^Cc%5%=XYRQpQaJxT2wee54!tCP;X=m{I{+YQBcOYovgg6$gmk|4dK&uJ*w@G%C6=h#eCyfFpo2LsAsD`(YBbD -mMZ>+WLDFyz?`W>Xf;npVGw7Nt?QA^?l~f=6kH!e?aq{ohe@*)Df%?Cf_Vb#*RhcIvPMnCWPd@qe|H>E7|M23Q^7V@^pMUe}dHLe&Z@>H>{a&vceTfV%1dtGiG%b)K5{pR-eDnI}7%lo^VH&;*h59Q76&Gp?=d6U2S{NiKR7k4*LH`(*|*M -Ggfy?=N8Q0{*!Prou|{{EZmr^h!}@2>x^VQ=p59`obFyZZ-y$brju_YeE~HxJiWPuFkDj~_JRKVItUf -wA6w$V>hFRDSd2r^VKqImEC2{PXhc_O`s_w~yuJ_2c!!U$5WpC;9UJ?&;y?$M^Y|zj^%hzvefk{ORi9 -U(0tlw|{xK`K5eT@(!QM(|!5w!_&|AcjxoxRNdbE_;B^`fm!?M;rhBf-v9LUx4g06l@Ist%bTma@^Jn -3=5Y_76Y*58?%w{>{X_Qu<^Ju>PapW|{oUJq9Paw*`r((yk8}8kZ@w>IUq9xZmOotIT|ea0eE0sx+nY -Dp`RkiE*LRQC+=5b`lt7|mpMZIw*1SB*Z=(O_ -pi&dZ~k2V<=M-Z&%Sy6=ilXTbEflObN%*6H^00~2)-@(R1a5oPapEebAUfRfBEGpye% -U%hz!>2U%2(gMEYHe!&tATM@#XhlKYLle`~Ky3-@bbOc`2{1uRl&Pllg0t{C}qSryTN^`<%(Q*H2e -Bw~zbL|D4PFm^XO)w)}kc*Xvy7H`h0bsB)ECyUOMBh -KGZjSQz^}S5czxn*KG|L}d-TmurF2Sqp{?*M-IrLYz_xBH%<%|2rCw}^;XW92~v*R<1!%F%7RetlI{a -2oU^ZWltqw>i`4*cqh#mcAp>EZsDa`ht%>BpZE-~aom`~r}2Z$2hQ-rT-V`*54jmloiCPI7LKoWNh+y -$5Vh*KdBlyT85v%LmQ*AFtkB-CeUJ57+py?@;>%}W|M2@4uU@}=@x}MAUwr$`4=yn6A!o@b95eeQvCo4)+v^|wEK_44`u{NdY|zkmKR|M@o;r -PSjx4X)Aj%VN{BX*4tc!B@L<^O1kec3EQDW$;~%qh7|*=Vk3jzYOc`GMZ%%the=x{9up5=Te$Z5!Mxz-SH1jf7Onv!SQc)9DGEL18xZaC!tif*wH+aKpGg*Uf^SLC>IP&@<> -6^sMx(^sMx(T&vQf(xcL&(gPS7@MPT4bKFMHM$cTRTr#>hx;MHvx;MHvx;MHvF4*bT>Bh#gPLE#qxGU -FeVOKwhB=p$75|1j(r$;TlpP#$<8xP_tHQ$=L<7i63)S7q&aX -*4i?b2UH3UJ0{f8WFs%X`vXn++vfR1bOU_^pf$fyj$aTmc%cV5FtNevMWnAWqanqMvV1@H*4Q~1iBb@ -jxu{3UAw7Ho=6I>9OcJ8e>-E#k=hma%NI14{6uo4W(*G+$MM}^VkeS%vpa)BB$|7wmO1Q~26!@L!+mr -rz#8)+w-ZZ(&^u*Vbc4QG@+Bk)_AH{+Sry=$s8mzoT&l`~?P`4YDA8_h2Y7AybM405Z^7^X?)AG8~m# -x?vgyMbBFTcI1<1lwt{aVH+&0Qn(%IyTg?p`JE0o1m#QjfR=lB+CU3#?1|uyM!6xE-_MtD`?!A>*ly= -S)#&n;^zyduZ+%JRt?EavzDFt%74iTVsNZ^J|K6l8JQbr^NyX|OY%zGQ4_#5nw~z{02HcVZ5#`mwyURo+Ph(b+W -Z)7`lDhGMqOk~eJfhDeRv+3?S*;wB8^im0+c<0R5^wwz2kbath+zX1+3;v -MVAsmvfLZ${X6u-K@ASa&I*!--dB5-yjB7UZ!~we=4F~KvV8;Ptr?R0ZZq#w3j^O~K+0bLsWA1fp@Dg -ZogFt3OPn?%S*RZ+`dYKJ9HLExcD0L$kXwcBc5;WKRuFXptyd*F=fyoIb4-(o49vUpU!7&@YzllKN0v -n6hnEl3_XG0G_5w`bRgO>!1Ex=`f%K(=FE^z_b&=aN>;4#2sfX4uj0UiT926&9qK4d(Sg!Fts>w){~dy2YOa|f|Y=&1sDl15@002NPv+5BLPMNj9}EVp$8a= -zSpfMFKL*_Z0HGA0+JSBB)~|3kpLqBMgoik7=iOQ*nGp;XG4#%*R40!4XhCF6;v}jkRk9fnor~0Ez(=11JVi#HeM%BdzT>GYZ -N31(E?I14zOevY{uC3^r*6iWMkUpjd$-TritP(`g3HwmTzvNq(RwkgPxw#+nU1fn>#I;2yJ~Cs3?BP3 -1u_)0>$c^?~LRn`GKB5vI5BpBrA}tK$3{I5!f{mJ!Ul_)0>ugxVbj^r11MIYSljmVBY8=FpeK;5K(YeK3M4C#teC9|6f01yK(PYF3KT0)tU!_A -wAGWZ4|quhk`+iop|YVTkgPzmVzw$!teCBe*{X2u3PvgzsbHjnkvh+Ho2(mHsbB>{pA9|1NChJmj8rg -E!AJ!oQ2N%uNCP7cj5ILPPQE^qm*j`WH=SnCOd5KE5mb+c644;?O}h@{;^OPq5O!O2ce5Fw($C10xNL5UpgxwZKRNBM_%-=mADr-|IHvB@L`Ju+qRv!)!G$( -!fXqBMpo+Fw($C10%>B+0X-wG%(W6_+r9K8dzyyg&-;$dV-M#Mqu;V&=ZU_Fw($C10xNLh&ssG_v2hQ -cu5B<9jtV)(y=EUjG&7(=x{^lYDhp$f(~j*Q+!T%))bG4WkFpSccGWSOB(c}$vVm~b}-VxNCzVwj1c) -{!?nOj2O}MfbTHDvNRPd43trN}N(U<)taR*22O}MfbTHDvNCzWW{cPv~MmiYjV5EZ)v@DGaFX>>VgOv -_eI`*W4kq$0qT}PdXUsV5EbQ4n{f{>0qRTkq$;W80 -lc7gOPs57Ykm}!Ab`!1SJh|E*pA+5ovI!TaD1X5j{5|-zHMo!AJ)q9gOrd9$N5{4pur?>0kvJG8?0Dn -xLsPji%EKnn^t@;SQ*%p0Y(NG8DM09kpV^q7(qSE%}5V0GQh|HBcs+mF1%!bl>t@;SQ*$8^pf1c^aLXVj0`X`z{mh -21B?tXGQh|HBcmUCTzJUfRO=41{fJ&WQ@MYO$7XhhjGno -cul_PQCoWP+6mRwh`P*pmrHCK#DuWP*_iMkW}UU<4s98+w3|2}b7TB^lu7x*-*3Lr<_Wu_ve*X;JA3M -kW}UU}S=k2}ULunP6mskqJiT<|StEk_lEOSeamDVoxR*A;!*zo?v8xkqJg77@1&Xf{_VECK#EUmzcpz -CRjm2%!Z!WlL6I9e}=m|#1OEi?i2Gega{075sV)zS;ps -cpFd5Jl^WPz0hRu))U*pmfD2pO`WCm2~^WPyb`%EF#3FtWhN0wW8IEHJXb$O0qeof;{phU(i8z>=1n>z2GEKN#OQP0&=DM$>5q&7`4cVx$wbWkV0 -6htb36v3ZGQIG*DgJ&m45Pp7BT)9LB-bb2~HoohKgoE}b(phwVS^AgK&JjV@s20eqGLC>IP&@<>+=~? -MnxmKk|rAMVlrAMX5<|UTlc#d1?+34Bm+34Bm+34Bm+34Bm*|=7tN2f=pN2f=p$L1wz&W;?n)3ejF)3 -ejF({s>s&~wmp&~tFDL61R?L61R?L66N#EW_~}chYmxbJBCtbJBCtbJBCtbJBBhtwoPTk429~k42Alu -G?bW7Cjd|!3r{DN9XEjS{*s7BWHE8XdR8SlSS)f(K?!RN2Kgz(Yl?yB;(|ZP9m+7NOP9LnYRj0Eju5I -L`#TgrrZPSH|8Y}96G7DPU@{7J(!*TE#u^iitOW%MTdg~R-}_X2^?R6wg&29AvQ2@7O=7)NOrblAUrv -GRJEOVvW$~23U0(PK2FS9kPV!;!3p3CF%t3aC7UxZow#wr&!W7kG9-$&p=(B{qH&X#6nLr=Od6!e(8` -QRF0?C2sTl!6!PA)(Cn7UrmI@(kA(Awn!BG)3Zt@btccR*=_NVUMj4Kh`5zy%i{aNUCPSojBrBREo+o%wESO9BJp>;XezHXMYbRCe-T!B(s2gN{UOXrYEZ -4W;ymOABeqLL^r(xf(ZArvMak3?~9Nw4A_B6#~Y>eHaeSu$YGRFkG7vDs~VvRI=D3AdBaB+9FIW=9re -9HWu4%~pe}_Fz}AVgk2aU)v^*d{sJMsAQQ0(1()Jr@?VETuijucCT5#F243c^pD+S9 -TT6&TvMCqclR(PDb6)jqFhNh)YAuD-o?L2^V^r4sK4nlbE`2GQ^HGHRQrV6yhX|4Ry=NMw@2Um2f+hg -VF9)o{qiBP9o^^y7OdtN91wHeUc}$s#*tB%tP6xybo{68STq-Y9g3q>;u7Uf&G^zMbni|-u5A%3f&X< -8~7#lg=peR2@^0HZq$%BttkP^;t}K+L7Yi?Et`tH< -?2U0<@V0yNSZR*DW{6g)5V*Y(JF52<^h9I9sjx8t6n59F`5P`gI{AQed;pmw#V -YW9pV73)Wt%7!HkO(&k?`)VMurCr)A-4p?a{oAjKq1+oag&!gKs4B_5e647;}jtn?#@|fCu9mlEQd7& -T+ETGYu_?Hh8EjB>p2g}OC0&DA|9E12}vzuu?=y<5xIlMb>fhaQDfa4R#u@CM!0Txn2P$Oag&!=m#xE -WAypXab0HonL}*SP(n$L{0YX86G%~(M%xtRcB7`-#euPx7+@ur(mO&IopSlBTGQROf8<^WjaehI?^#!-gKF;o$`pl!1)%r}m@khn2Yt4`w6(ZMT8mqJR!@Ek!tAdX$9klvkjV^(o3gG_x48fxyJ -rpGEGG&m+{2(|xbtp#GuOLckTzCngv>_}RbwSp}igvxmWPY*rO&b?_SLM!Ca3O1#JVX9!qi}40k#Ta` -btW4TqK52=O7=(u;`R#{0F#=_081SKSN|AuQjzfRU{SKQ|GwUm#B -sTJ<32*L6S3!Wg(|v+SXW#A`+omO$5deVL^H}0ySeXx}Eq{CDXq#M@|4z&_o^e&UT=p@2b`f28L%u -h%-_xmF%(;@0O6AW<}<%Q%HV796*();vuTkDz@1V`6iJsH3ycB#}xpp`0RjuDmECx9<*Z}dtB;XN$Lc -QA{14GiqIet6!uV(#2w)jMOHyF=?^EqDM&^I@8?}%-_CbaDShGEv+@n9c2d!t9I#j?TCqDbVoEX}IYsqZ)f$>RyRO1EcK-1I9ItoRJfGI!cdK)$UfD1_gbj>z%w_S49;BwjjxQ^mBfbm-zX642 ->YhGZR7@F>^uZH)N;a3JxI)w9q0dOF5+@Q55`@`*F^1@{*VeREQ`M!%e{s1jDJ$WHWK-L*a1K(tA>$HRs$#=jRv3`&^ -H@1W;%0#{>M8HP=!l_(y%1sPyq-b1XRl}4PY7ENU{=%ty4R&u)L?<7~HJv&`3zb@G_kz_X^Ib7zEFKB -Uvee+WhpMOMIc~>J4wZVckEcA%!8Ecp4ZgmE3@j|@>N_%mLa}EHB?nZb7=6vRzi{ea3tF!!OhPZQv7? -&`Ny5C)PzGSa6j?W?!lNL$Qjh=#!#CnhR29a!$xA#`Ud7`ozM%TUT@jC_k>D)060krm-Um40-Hob3Cl -y>o6voz-U3IUdoE&H5RPA_^XaX;o3f&GdyN(eg7$7{M3a55mA5uWsf~iGFv*&z}!ennp$Kk{SeGB(N22Jee+2c0Di`jRrw3<9D}FIz_qD^K#UZI;4nAKGMG4`NfQGl6vOI|)9z -6B>NrtTxiNwu#TM*Yb`)EJl(Z3DIRvY+VVwHz&RT}b+@^FZOas|OjSDXk352!<$sc1Hy(vF^g8GZ -_%4XbSXINT-4~$svcht2ofc8FjA(IQf$NB}p!%n(1VN&R-}Dmc@dI5Jph3P>|&-7QQGL5j@-JH+S7DP -fs#3P;guZ!9_Z-Y=@#;HYxgg846h-5tx2I6o4V`QlDzR&x_zUsMEy~5naKdom^&68B?fOC|v{{771DG -Qu*XksCqLd)S)_MZmz%gF?FxbzHk}R*p;|QOl((7stxUXZbmBDP+p|MOL8x8aH`KDEuRo4wn;Wf07n75;sW1JdM;J{Y0{i3Z#kima6yV!6jau_nW-Ls6{HUvN --glOQf>WG}s;${S?gWWC2;fAghUSVfLXZiv6nOvAy4@dv(f?3O_ZduaiRep?$A7YiRZn0c1V!bnt3N2 -XZFf5o?W%Z_zC>uJjT`6-&-YE(WXCq -$&f%V-4)x?grpkDEh0D)gCzP<{y(=0w(qxXy#$JQh{{#QAe_?HM8raPw=I@R^?;6UjZ@Hd!2>ddXFhf -u;MBdEtA3pcrOw7pELkU%2*A*3U8Ud{A4*49GPL|Ey<{<(E+a~``3`ljPOW;OPS;dsKvteg4xW!Gn^M -h0!xRup>SKZob=DQ*Ytq=Y>m0We2WU%F$Snr1>R^~jDi(9cX+ZfXS~FE^*M06xivJ6fYzFQl@N%f>f+NB*~h(Z{3J3$rB^e9-FELU5bwz#0{XxYy&a5XUb*WPCQWeYE%mv ->a$hFr@BkXj3NROj|In5mc$rW;414Z`5Nb~b80f6TSvqdUQiT+A_61lwj{4s9fjE;H*qY~0lVYM2Vj+ -)ooAsuk87y?$wy68awDi|0UL!07QvcpqH37uI?AoW!juN2~k51-4)bVyMH1#5useWX-iD1mcY|>Rx>bVj-162m0X9Q&|@1CAURIsngg{9!Yk3X~_m`L} -w`>X(z9y?zOn&*gdN}i|CSxF>*)PhpD42MAl6@Ee2l)C`g1jt6sr;g-Sf`?vV -%E@v50e(m_?ARyl&UH&((m5Vjpq^#t4N_uFHxKm@;(Kpv6RRoRA5$(=*R2M7dm -yXCWQfu97AbL>2fmP2Xcf&6GbK%P^CDkcppdKNPgLrL28IRr>;TWt4r>nBCaaHL?kqool}Ql)U7xgiX -GaGl6fVuZ3hfcL~jwL;>5$$Ibr#Wrc@n|hN5TMU>j^ajZ`RZhByn|4Mz!k+TI6<3zYj@hg0`js!lF5G -ohWzDLti}rWBm6?T3?L3D`QO$jD`S>7j$s+E`>RHE!qp+m1!yC9r>~hldtQ(RP=Klx&KP!&{$QLf%q} -ffR0{=QDnz?lrnXoK3z60nk#W4r8m{zDn?gDPfTILMh4=*XC9!t~0bZJ5Jq*y4O(MKZev?QRb4|A`XP -z(HT1CoD^A=TR;Id?S`q61BCRg`J8bqb+05Nu~S+Ze!68Rj%$H9Q9fbhP|Tr2VnjMaDMdMN=r}~I_&$ -&B)V-RrNXlh$e#bDbu8;+)jAn)7L5^ieSy-~sWhxXSh*S1vla03RFLkdbmI19WS5E5CEx;t0TIoi3Rp -Ypn6A|#3NKrf&aSNArl`5Zgj!xaH4XIY9L@=oiQ8@*I5J;sqDSahoE)XRMG-J2#Lh%NJQ?*81*^23A9Ctm3w32Cf6QM_g;|%Fxbs;u^N3O$9yFM+ -cLl^de~tacBk4S3-EnMC_v&58*-fF4cnm*43YtoUP=17h?^LsKVvdcV0Fy9=Op4T>v5zGldtCGq@}{N -(%AvEU;D_okCt9(-$Av%+2-c3a@zy|$)3c$J0m%lJRbpK95@JTv67QqDnun%x1WdKEAxZ|a5g`5n4GT -z`P01me>84ofykGJ-6zY|>a}atUrdDMvhZ0g!BJdKZKV1p=seBsbQ*jm#tY;l|jteg#g%apC$hJ62Rz -~T4PWNQ@sNjONMs>M}LML`RdduVlfcZ2Fc*uv~QW$Gyq(YTxwb}>u>Hq4goL#LRA -P@(|a6?z&uYMD}Q6}_bB7`gIJII_?oWX2_rgV8q1VGV~yjD+7pYiM#69VD@ZyUZIY4lys4DY2>)P8s? -`)s|#j^pdgPNP&xQ%Jw6-Xn8Q?vh^}Ka!@;?ZW0G+y4VH$MH=?5M~goJG&E$AG-tg2ap!-vcp#TbS?3BB$Dj~{Ra)8HXdOwsdH*}|S*>R -wG8nl~GC8b?ELlOhgk&a4pWPFG;1O{8%#e)vJ^Zo`#%spq)L<8*0(XcJN(LrS-*cF;Yi=sxa_XCPbZ% -GpR?ni9?TJm70~T*xW8#Dnal;`GhKWpKrWm8+KAqGXN^isSh78jr3Wg74P~PHB33eyMx)u5E?%AW>d5 -n?ox@EUr!x0$q5Nu8eYh$bCRI>MkI2NW^)+@Dk1l7I7g;`<$Gv@yy0S12b_29jUHj`Y`OF_bzA!p{QL -2Dq88-<0dcZ%HQGzgIc=6%DCVxqm9}coJn)ssB2rp#mptaRLwa~mYMd=t9cs=aksPyX-bPuT5;K0F;R -^0RyI}KMnU1Ok&*Le;`et1K06K|xX-wID#tHUsoewAUGAI%#8R9j-Zv#4RfGn6k_!T7Ook~GQ^gJAQe -VQ!4G*RF8_|_Ax$sop(cuvPD&I?8M-7RK`}Bpb?7eF-MSb{r7Yy%rq1t7p?ubS0tzqD%u<@U8;Ls>vWE#KM!VDs|_8NFnj|nzrAI!b@ei -Un$Mrbzf#r7lztHfS5FcEF0v|T$oj( -jWDD2MS?MJvo0m}cTG($!y|7U;RB>K@Y$;wD6*jIiKm-|~pjWvl(JG=>Qk#Ze9lm)~Q1@!|G#FSY;!z -c=UTUB^IOc(Kp82F~E8@o@Jq&UkZ5tm_{*naS+~ZRBsx$ge5Ejjj>W?XeV7$LaL~CL -(1?1QPwm$xEnvjky^G09WL>Bv3BmI}Q>^0n@F&jWR@HKbS$l~FICi`A9xN&Xm -R-VvOgEJR18t~^|Y=6A$b$F7Gw;@t0J0wx!3dK=ZyibGr&w~HhRQyag46O&GAHI_=g5VrL0C$u`(Td4 -Z!->7@F?brvgB?=C5L{PJDW0NnKu-mBq&XdW5lA3w6Zc}*D+u!QDBqC&~CuOR+axuftY}t%6rwMNyT-Q=n8fx9 -#r=#;ak*RGNg1Hs@X!X?2ziq$tQ1mF;CZg@kqP;Nx{FARg$vjexvTyI2Phl0j9Dy$`W=NTx|U=uvg_J -9A4!GdQVPMa!(~!YyekE{2Z4&&eBF7nj@ei0VtIGXWf7f;!$7~LcuATRxHWf9LLa$+&B?v?cB%Iz1mU -*pUjI0uL2vnI}9$~x+13q^s=DI8h+6CI!OJI?N;^oan5h@l9-$NYPHz2a&7W2lXnxjL<=n8P(?jzGGH -Hi<5ZQM)+?!`32&UK?$ssXiR1A`74YsVAEtDsswP6^eo{SjI)kvbh9ov^b?F2u5Ip@cb+4sqA48l(&? -y%1wkx9L*d=(#G_hAgqoMb?Buq@da-*8QGrmyW>ynIv*+M;Z1yK|eku3lU%DYRTrX-U?7{W9;=nBqFW -xGn~uAP4i)xB1|1F*>!fD8%hQ&9!G0S=^MPTMJb9Y`QZR>E540=kUSmvj3!>Rt^cHkRs91R_;-0d;tF -AZ1o=wG7q1xN7i;eY(I#fFKQrA=|Du3hG|VRB;(44SJs!L9(OKJNk~J!<41SNM*3%f=ydOmS| -;c0Ei`_iCi8V;d#~@W_=aRDqyZ)qzx*Qq7=5o}DVx8-BD+xojk%&Mo$Hom*U@Fn1Fh*qyT8F)lr}UDkQd$u`ZgnZGEwB-LVIN?uF4c|z-goW5l2y= -_Bv2C*-*pv1-K!s}qUGO|ciO2?<#eYCVlh5Viy|MbGX`S906_VcrMN|JfZY3diI<4ltALtVt_kp+(s=(FzuG-E!aYO@FjvvGj -z~!NR23oyY0IDl$yAxL{h})syw3ZDm!S6<%6!NVDSDK)r;tTQ5yb*Re#oTXsiIX3ltc^_SnRGYLFp4p -4>?DL5mddAYB0NkdZnmPC6&ITvY;SJ8RW!t)=gx2DG}q!rAn!cJubY2F)kZ)BVymdkO;?qiG9Ne -|l|x`Df(W&C%{fehc(b&1j+?y1b%;D<%~k0kpRQ7S`8=UOOdsWh15WM|mjydjRo_FNgI9zRl97+KU1}sYuY{DgNmUv_$F)?C4m>vBsnori=AIJLp!Too$d?K)NZh-Qv?+tC@>*ABR^ -uX)nv%W5$5bOlY@MJ|_iB{n7%^2>b(pf(h{i={Z(Q1!wERQJhqX*mU&$P*#S+9sv)3MXp({&GaAjpwp -QQ1P!ACm;yh1QO?jN1{J{da9J^j$DR3e -ZR$UOcpxSfP>?+-!Y)XEfXfU?@)9D0k0jH-_mtK}uOGXX;tteNe4BVJWsjTPyqL=6mzq{aI$gxTyg`KK4q>DC6n? -VRv^(g8rG}M|3Wwv5~>ayz}Ds``+v=#5`F?nI7C3SKREwm~WL5FtTs_>v1Z5^)=uh%XUMfQGwA5`glY^_)4 ->h#8I(I2fj^CgzG&T-)-obz!)P2UG2hDsU)0Pw>a4ar`3znwb4QId=d(>Wt0BXy2y*Tq-8?-fg52;i& -A`&BzjMU!!+$MHTs6q2ee5AmKvf2aai>|Z;{ww$-^j^6i5$w=sEloaWyFh)}Rj?+mzDm^RP)m72NtHG -sUDJYWW6xyqMTL70;}#sC($&7>C) -Gkp4uILIz|*yiBv_Q?aZMV*|E`~x@UMm6IMe%HsmU_LQdb2mfoq5jh@vVz%lRRL#deci*mXh3#?DPDh -*Lm*&I9!&_|!nl;q{KV1-TnVH&h@6aWAK2mmD%m`(hWqtfZ%<{Gq{jBmOvH`b4c$_`WMw -}2WL8Sz*|X_?;)mlGr)T2T>Cy4o)v@^D)%np+VoTd+JI}=RsubsKbz9ZNS{yZtQdFII-TZI0UKi^7-Q -A|H=0)GMV!p1*x)*cR=3ob1PwT3$RL|$-$8z1=m#t`)qMsO3wLdHSZeHA%Y}l%)JN31_Z(8$34JkB -3D@F98s26{2TGjuqSyanMeP~lJ)N-`cec9f1(BX@-H{w;&)j}T>ti(+zHeI>gtRppoYAb#|y?%NA=2{${y%j$nUS1xaUBCTKwN;u{XO;czNY&lFg -5W}^RkcOkKPti10I!cPk6x-Chd-RYI=z0Yr9U~nK0Cg;5+~=E;!s>1US6Lby?J$bDK6ezUYuVY9|&<( -mQb-)=9H4XikE80yGChpQT9c(?(EXvD)V+q!1Y3`ijSo-^SrDSqC}xw+dpx9_WUzR<=M^-kfHvssOv&@^HtG`uKd@gtmkSq#m!oQO|Pil+}$YsC~3=Lz7q3Nfut?(Tj -is*^3|$0zGeD7DwpcZ+opZo`Q<)2QX^{LEr0FB_hNTvH;(ixiQ?boC4XbT>Hcyr+SyH_Onubv)c?-RO -P@*2w^V($pX~3{oRjXnjB@h{@7#=(nxl#NB+bv%|CHuP#*So8S-O{fS*dK#Obhy3^zd8t__?pd3aVw# --P^7sHyX9N$&JRKFK7+AL%ZDT0^c(CTN?daJH<}N`llT=G3`mCcEsGg?NR;HYxMv9%L+=^#OK^;ZLB% -cY35!z({%21ksJKx429BTG4n@qH$xtAA0eArj7pBt-}?HaF&Yy_Nt?0PP-2jjm^DeSVY^=Gbb*!YpRf -bz-6BmI^x2<@`y8A0b_UEZy+&b+1nltpWAqQwEn$>APkIiO?l<@}jR?7tajNYh%`0;(8@pcC->to*Uk -2=um@z^gC)C>`dhrIM(l?|I+-YD?aweR^0zmkQCl(?ZJ6_@z6?@nueo+=4*3dC?NJwKKWi`qt`2F(Fguq=V-DWs^3t&%iK<|Na -S(Nl-5)Zq)ERJ!(w8$a_$o-%#TOz#OlVM5wfCyv3$OX)5BI!ViEl(KWFa{~BzsMj7$FiO>k2os;aj}O7(LR7fOXP%)bOu1lxJF4wvFAr%1lmj@q890fNIcFUE? -7hQ@g&124+-F2u2q5U-F$Mh6i4tMKU?^Q9Ge@N2L=GHsJaN?SS@aN5QiI$|EFo9EZ}F)WSMvgHTQctcDxo#NnJG6K(EN|2g%od1Okw*T-C=VQxfJ&fQCT -|Hax9W=iBoVa5sz@C{!QeCD)8BN2}PcxhPD;XImq9wk0a7#AQ)5kDYi(fQm(`_%u$Qizz~;bc{m$lx` -ScyyRJgLo4sob;n~&_laTM1D9B(iGxPzN?~ikC$ -|0gusE`DWf&a4At4$2l*&v;lntc_@g1_2!o`#*kemZ+t5j^60aKGb107_9vw1V>>^Sa4#J(4bZ~&kcH -2F4IL@TBH;_)Z)34DPDnRfez_0+pt80AA$Bj#lgt)>Zxb#%S>sUy6xHIuBIV8%4k%`tr8l?nQMq{GEo -{Fbw1P}_FTu%fkVpDDd3ag9;Q7MrFim>`Y1v8$|F5fy6=|CHtx)6v-pOn(%5i*0YN|XC>t;*7{q%+K5 -cz(l6HZ%!f%(6mzJ+LBI(Gr41y-`T^nd8h##VFCPe+c`b7yqT)gBkWT!{5lUN^kTH{BR5-5 -*0wLk&QS1%Fv-;SPejJhUct#Z_&QvnYqbwr)h8aGE%QT|>W*kvC$vl1?kA|DOaR{Y%!n6!sV4Fk#c@EG{UliKROhB8>8#V0d|p#7dayrPm#_$1w`ZLOp=e91uLu@G&AjQ)uv!jp%8dWy1(Otw -p9EN2pMN!V$YLA#y3&i-EQ{0FZ%()KOK@}$ppa0JmgXIX -X#*Q`rLb7?xTk7gluY976+OLZFHR}^&p_sgr|8jn&Q!%wLqGe4|$X*g5#628jeBmEYFt^NNJIL5fngN2PNOUX0`=a=Pvj77aNnxeL+WiT|YfN)pTzvn6w4V^{9N4HzPHGB`F&2!UAgRK79 -!zug0nddo7D!kNhPKpw+P2Q)=j^FZ{xuEH}Wq9nen<=G|-%efZY7Pw);ohjU04GX!%d-%!AlC=LOQ1E -IB_j3NWg7O$A+05kkZuD)J7%4LyA0K|FX-J6eDe+S#kC}!C=Qxk@B4cQJ4go6M%0P#33?h8EV9Z7iVP -N})#ZAi8$e9Ad=dKghN2%hBgpI06-0Ni|JurA;H`H$!tCCi^zwE{5P -Di5*oQZ1r8nni72!k>>7C|D8^^GV88+t=~HbH+TD5nTOeu&($pAJbHTA!k&&xZ -%bVxot8Cdv!LYZUxK81^)YH#EPD!*3|yyM7+ic>@$LCn2L~I$t^)up)Gu<9baM$rx>PtR5gvAs -Z)P0iD+ArM^vAlDvjf|%<+XxDs<2eE9H0TgnK5QY*5VMcRku^=s4ZWgWET78606Dyi0^OBvV@vr~~`@ -!F^9WjPYVt7;8W&Fx>bm2bCRY=Sf_UGELF_P+%Nhb|+#R?3u6~fjfjXd(te{p7@18ceJGgR1koPYe}! -Pi6rP@!-yCHA-f3crg+(>^H-ufsPWUMTA9)U1=1GmU^ -48pb|!=d^81`6N19k(oHagusA|}{;6FkPiKZ2Pev_RhL6YuoPcmBakCnhf&OucJC6|SfojTX?Raz`Q9JbrVuXq?d@sAYqN0_ -`*x1-32}Jm5w9PCGb!l++w!^yZh51MaqjDS6zl1Ve(aXR?_2@w!isawlalZXbLLmeYZv5XyzoOCKcaB -!JXypa!9aXR9#0o^!l=mWuZh#^pYeeKzFn5gq|yb)@EUUN)=miu-(r)?QRy6klr|`Fgu5W2J0$IP1}Z -?vPv71i#u>WuOSvWPjo^Wlwt!C;9(fK-Jev7&+1%5DPFGE)&ZWUbw9$q3;6U5dkPcK4vr+gf8D~%2CF -nlR@g+vOKOdAI2h5O0oF!yL{5!mtd3Y@}>)H9q?C9|N_{I6qgc -{Oo%6)7kmY>d3w{hmOzA-@JG^yF7k1yMB3jeD(7D)pK=L#<4m5`tX0I&bgDPXQ!{#RL>8u4}%ujjyb< -5mtr=n>Z+g3cDr)DbmHlM`s=~0Z~9_AYsx#Skbvb>$F3W`)W7d7B?q~XW!dbn!n{K -L<*V`sdtDml&Y_w2^CeP?+5BuFcT9!TV$u-47JQL;sJ?V%iLw&br{vk#&Z#K%TZ5P=`A1n3cRtakkcQ -iy*o2MZCvyV?`?Ops{{HHxJKn0t>S_-Wnar4-l!RMQ{)em>yY?e#?Bxa|dJ2lT{QnO~MA8l0%E#^w%h ->iA83T+tufa-_3c@Qi0zSB<(>wcX+tnH=esr8eZc9PGuZ2U>H?n^C$ei+>zQF7k5<;SYobVghz=;_iv -V7puww|bGvq*}V`c2h?B>0aef`k`q3AhQNWQ7@Dw>iK$Oo|Rq|9UJSWENivO#TH1N6zi@e_$<}(40mx=r7T0^&|&?rE&EMd?=TtkqMgZ<4Q`dml<3x2t$tX)TtsFO%x+5LN6{%t(dIBuHTN -s^g*(7AH)eMrP8BqYt|TPr#Wk(y9~zGxix3VR4)jLDvRCSTFpi*16vD$G$6^O8UY(XEaKS&`+y(l|EDOMfy(W`F8C(c -g2TN+-;OgFDk7W?Zw*ks<#^J(X8UWs&CmS2APAQTxzyXxCY{Fg}H?*;wUvG0BF@XS)3{KvL=$&TO{FtzXoXhA;LB>E9dI}@{g -r}WHjdjK6gz4vD%SIj{(BdKdMh^U#Y5A6=qz=ZKMTPK8;PVFC%4TmzQ4sczQL=Nw?&xZlou(!;NCI-H9c_$E -!hdIH!?Fv-&*r(vBjTguT9r{{l-geyu7;A7dIa#c!Jc4cm4Z*nhVXkl_>WppoWVQyz) -b!=y0a%o|1ZEs{{Y%Xwl?OI(=<46{L_pc~TYt`M7n{kzskkGpiGz&9mX+S`0)KADH1z2P3tnG9UyW0Q -0=h`XPEq{d2u+lyZmPuSy_ug~Q{ip)@&71xo@#*lF<5O{Rd~kSrc_==eoE`i_46J?Qy%AUUTAbzaU7S -XVILH=S#D(~r{VPtA2+t1>t2CZRWtNM166>@SbFA6%jOcM1moX&&uD|IdTk2e7x1#LPiS?(tEauTtGq -rJ+7I@5;SzbLru{h82dVHSisMHH_^H^!@nu>DLwS0uB`%=6+crWB^Hie4bEuum!X{@-nOdS!B%QX;jltF#P?nj|x%#-bJIxjFH)Q&XP)*5|*Sig%(!jEV!N$@>DKZ*p=$c^MqF9p -`qg^1Hi?LD~17bt|I-;q}0MvGOUZ&!&A4OlCFJHGn$?8}wdKm96x+rPNjKfU_;5o^ITW)Z(yQ~a<*3N -8e^%A>S=1aK($e0Xv2D_Y5|^Zkpf8JHms%T&i -OfEd8;WnC@*xA03tdKWQq-=0MR*GUOcvrk`lb=)xsH)hB0`BQAD -s8xM-rc)VAaRhEePeUwLYBu`N%!m3zBN%ANbnJ&mdq5nYMP{L4}mHLme(lCMsggTDy^v@T%T;*vs9PW -wY@!=72PCyuWB9$K}k=P(N&RB*4 -;#i6e{L>oJuv8pH|6T1Z*j3y2{RSR4`QeXaLZWrEo7@J>0;+Une6{RIHFe-=ZX5c-{#hr{n7Z9pZ1|o -d*K_?e0i~9dJG`mnkR-mR=47@0Cp@2vMi3;FYFu}cxdy0EjshP -qlaD(1}8_R)0Nh~KGfKo{;msk$2l)^fNbqebg)+wx0b>7SZkJwiVA_^i3A_^iQXeY}d=0b>t5DOs|(i -nId(I?i2(@ISQHCQUJRA7l1!BM$iKh)@Clg(TmB6iy3phvLBvwSvU)DeFirNDV9rQUei6J4H9X -D-wrFa3&OqP@Z6%3WFRPk=h!KhMEZt(+wIcPC)aBa)>@b{fL~P@1TB6xg)D=H+4p8ZUgVV=WXt6e|Du -myV4JLrFNHTw--rmWJ8NFU5UzK7rab}G7^(jo=dq~x0F^FYPX)ID^05|#5<6rs+&@zCQdhMI#Jb?W_3 -SUccoqUH$}80AsAJ~$t$s{LR4p6k=;scy3)&=+x(f$jpnJjA**?FTf9zl(?8hU2q|lBbyva(AAdn6?0 -lA$I7AovpDUftb+HE&!Cx?b8|OGgj-}#rv&qgZoGCkxsvEHd;Z87-Q<}~-X1OT+1UqV0OQ-q{hc=w^` -HEmC2o#JrE%PKw?`pWzEdxJ%;!+z&yr;y?6*R(X{aaSQp+FAUX^VD8k=Q<5=w7nAWzS4^UkXW$E2qg+}e~rXGS&ZmH%u6TXMkbUojk}jYt-Sbt3iorkK~Vv9)y`gzN -aXtF(SH2;?RX!XBGjaL~>H`^oWaRSSLoZk-1Pja+jtebW&XbFG6wI+lJg6YZ8!dumXGYZPkjy>y!HYu -1AGG;AzOHme#`wiX6CNI_g~E?!8VPY&s#xrXJnCu(2y|$N$=eb{FFPS2Fwd(B*!Rs3Dy=Et>5p=D; -0tc)bt=EeEHeJAW0^5zAtc7Rffk6=ZHvd#(i$|r0#O)(K5Cwkz;dO4TnT{~z#tRWN9N6=aWFGUH9NOq -#qZUlG`}SewATc`=-vD18g?1hU&7Q1$d+c<(5O?%-&j)7Lv8SZKH#@V_u%4}`6nb>0lKg@K?**tk6%PgCP%D3bOe!C|rX3Dd6Ia -%%^+sybMU276;9R_~DEL!qxf94dcwHZ$_YrKdJ4$g?18Om^APod^yraHFxi1cl2SQHeq*%ya)OKqaya -uBj4H5@30$xW;^2izVH9Z%Z6|0gtgV0*GT{kXf739YNLLD0=ace(ldNix9>2c{eJm5XwgTlfwK?bfN9 -1>H!<79!`AW*G?BGM)#agN)^Y+JJ!Dc9r)aaEWsJ!9X6k`EL*ezMFsTmfFe#Qwwgl<50s60<(j#Jm6i -~ZuLA096@hKvi-=Y44JidTd6-ievDFVncE|HP+M)?4w(H|b%gn|Vq*+}azumHA{Rps0vdCM*-3PgE?B##8 -@iVc@IEUSzi9wwQ_U!9Y)y?}2dBDq;Z>t_ok~OMJl;I}TPv?oR*y -~KW<=(5x~mK;S%p?!n1(B*X+J!nk)bbNXCVLBbpK8$~=<`dXlMRzp!wgM98QTo>R{^NPSwO(m^(*Gyk -?)|^;9~c+e_u>Nq9bkZ@6aWAK2mmD%m`=#?R+|D00010D001Ze003}la4%nWWo~3|axY_ -OVRB?;bT4dSZf9e8a%pUAX=80~WMynFaCz-qZBN`f7XF@JVO_LpE2XUMka=mfA1D`kQF#+kRsDp*On| -6EGMj|?Zm?{Wv(u&AaY5ZBffphke-nxPQbB_IGxV&UV>{gX5h~Y=P{Cx5 -3VDVs@M-*GU@9*iJT$SyHgW>_5qD7UBB#_AX5(QJLjzGE3sLWE042dTQu?nv@Af{vLmiXW2Z?S$4%rh -fc^J#bq&x=CPqR$<33n*qMSzh-~@;EBvDZ9L{H1?;8a?&-w$5b~Zd%g39iSc*@6;AO@wmqA% -Q<5#%XI@ -!amEf?q)tTK$d;kKmX(S^Eul-`pUj+pPp_Xo -qv4~SwtFlk-u6~aythErwm@@QCi-^a40z3J>B^OBep;6AMBrhCF}S0&yRM`&e-1ZDcfcz+o$LIJD(4> -Pua=m)05+~-A%^M;#h^4g6Rr!8{#Xdyv-2FXp2P_d#Uhl*{C=ez&a(8 -n@)tvNK;N_EiltfUvmbc^>;Qee>_UsquMf~qHhmTf2u10)8B%bD;d{vyRPW8QEce-mtSCwJa2sQ}6oS -7M0T+yYK10u9ZkZX!sCvh!LsT;+_cU62n3a3o$IjumOe*Fl+$z1E?Qh=pbBZO;}sQh8Qx$OarJJ;u=E) -W@SUVMi@y=bv{HNHK-qs&_Ax!47kSzuwsZy>V^t2f+Zt?b67Qk_=x(TX0+IvA)Fe+k}<@`5FbN)3`@q -Cc?|VqI5{5sl5;eHhEQ`1=fDkzT0W@MjNlX>;&OxraRG4waRG4waRG4waWO!HoPeBwoP?Z&oRpQCG1N -$iONdK~0mLMRNDPs5jv)bt1Q-%PbpY7_vH?^FkPC*Dnh0tFhzAhgg7_B1k$j2dOC(<+`4Y*ONWMh!C6 -X_Ze2L^MERmW^gjkYBT*62O3z(o=Fczw@jG~YHN7i69gTsuG3^_xEASyKzsDbZHkSHaORN`FFFf_!9A -tnql0pigh8_;i34^<8PrceX8G4c^kFu;$&F$M=184QOQmERU&3d0c(wng%|k9e`5Rue-_Kw>n6W`a0k -#0d02HVX)f5FgMreup$28jua>0$aRNbH%+kp7$e31)}ysNH2u+j0ow;RAiCRssu#L<1M3}irF5ZAFnO -*pegOP)CYB4K2+wg#_Jb%Q1b)v1{HGH6g3qz6+0C@6*_Uu7FnQZui~e%ras~<;;DydPml#&vI`o%Bpx -MVBcdacBRCWJ5eZhrH>}8#2nL(!Bn@Bq<%M5f_{HRx7ka2Uw&u~F+2Vy2zuZQXo%9J#NWzNie&ab9FD@5P0S0w{FR#+g8V8K?oo-aQsLFQ8#YJ>=hU@DEJyW4Oh+XVH -lL9Y+fkhn<59_w;Z%QAv<+2wCRZq`sA{Xi<5oqHUFxWcb-*hM@fLANRm~NTXfAP4iC2|8kuQ}!5ipfL -kuYnBnEdbXP4(#s#bk^2Ve~NyQ3R1S(F74TQ3a7U(M8yB4Y59P5UmCj%z>y#%OlpRc!L<62#J;k;*_x -A5t@n!)pL;8HhqqV?g!(t0$6Or{;pd-2{b -c-^D`gL0%R*%7=8CJ4DJS7XJ);c4?H<73E#M}Gz{3~X`6&tVbvpJ(QyshCY3^1>ZMAzL%7L&AMply1z -1>$iQD1^&i40IOMnW&ESRbw~${k*8Cn^)`PgMJN2M^B9djS5E$9Ff4eC_f89JzlEh;?T^$Hx(*EmYsi9ZR{`a}Fj!9`QM7eot -`qCpshSZ`tREQB*Xx2t)?jMI$xgXh;)Z^2Xf!E%X)g(wkR}eMCzTXS(zOR`kv -bXvm;={Ky{kCWNFRGTWl!X)NpB9zzs`Gi)2s2%!DTl{MExWBGlg4#)0rnJa{%?nejD4=E7kc1N(`$>^_M-;3WxQnHmcO4y5jn`kBO -`LT>ZijijPb?;#NMnXN2HoW8kT*CmtGtWMMcy|^6b2G`iv~Ljrh>rN(J-=LW6c~QI|{l?;VHJU*-%$M -x9n2*(=K{$4%EKe+lf9pJiGc`u;RWe#H|J0dUrv_j86BrCt6n4-fCYJGWulFX2D*l>@0Mxaj7vF$xfn -*ir$@?bL%M$B`)Ep%6FZe6vt&|QyAgJn;F<|1|9)3bCp_a-NGBVyn{uZcF>s2LU~uyMik%}iJ~+I -=-b;Xtp5N|g<58~+Xq`j~SgvOlzV?gG~(G8f_W{@Uk4#lukY`cEXQ9LBS=8wKu&m_6R=o7~pR#0>t8XOy*JJEe@0(h$-mwL5_R?2CUd`#P=_! -5T0bOOD{0aH)w;d|M@_wF7qS?hfD-!ym#6|W01wfi_j)(Luj7sWT9M3P}^qO9WznH~k8Rrn$i)KaaEA -xuDyhx+l7&m^a-%|dqx~E=MM*pkEgjc@zKhOJ8zY3_nO~2INg|7}i2ajRgH2YD!B@37MeNerj^k}yiz -0mX=yxu=Memfe4@6aWAK2mmD%m`?t?uPJH?0059N001Qb003}la4%nWWo~3|axY_OVRB?;bT4dSZf9q5Wo2t -^Z)9a`E^vA6T5WIJI1>J@UoqHX(H2m%B4x*Riu;gelWwqS5+o^#ehL(uNrWnrPm-JZ4)@>hq2Bl+$#o -hn?gfp&NG3VY%sew3(uQ8V=>DU3hwqP1>E!s}@bvPK-kqEs{7F4?UpO!5>R!-U8s3F*5Ya)h5EN$gA^ -ArbMFBoPJgnky9^^?%^C%Q?PIJuJaca@yILt#x{w}_XC|Qb>l3U6t}3Jo?);tU_tWs(*j0 -8HmeTK3OV5#(Y)H|qkittv<*)^ZJ1_c^^fcuj0Jn*yS9`VQ?!5naf+8C{4>q+i9N^yDCk^EA9!!Od6M ->pk=keF)MobRI@`Y4|{I2o~m)Cv?8f?~}OlA5=x*O&X+Y$=Yoy1ZBx>{s_a~(mGkuJcub3i!dwUK}1e -Ly!b6iA^(sp!rQg%TEz>vBdyLw`jFKezCZm)CnAHT^j^dwg*WG`n<$(^_#~W*I1?0Pw3L0>y<+M}8gf -|~LPrVU3wGa<2r({oe-&wl9^yN!)>a|9gjl}{a%n%Jxs-yhVf30JL?f*wcRDFho-Qbi3)p*bj|Tw4(M -O~e-3VG`;&v6eK)_u3bbR&4*~cr|KmAOf_Af5>Pp>||#au9rQN*uA3Lln8!3DvqG>G#x3>xnO!6|6yE~!Apw&Mkq2Rv -m9BqAcrzFnE$BY@DiG$m2$4|~V2Lc(Pl{ob#CL_i5K)A_rSO*GBzNgC4Us#@zE*UBqT=f!>E}BxjTt5 -d@s|i8xP^UbuiznC8^6R0kM7X(+z+oY#)?7{&}eKMT^&g7p7ikWe{>E=cv1p9N{4f`k({zR^YXc?n;(k{pNHO#FQ% -;41W#<2=V+61;U@w{R<6&Te;<)63X`Qw=!j>R2LYB+c6Gce3X(_fw -UI3j;J3F5obR9PuK_j%Va07yJszICzc?lq%c)p+s0>~Z`GiEF5QE8UF+N~hQS=hjoHM%myuN}Q)?>$1 -)93LRY>c-j-UdX!y_1<&As8<_1&&R~}A`oES314O~I^tSu-v6_{F1GcpVY?B2gYCTYEVm%f$09{dd%G -Jt5`!la{#53Z=8!LubjlO}b=cLX-J5#us2ZLC^D;DY~=sG>Oj=pC^YZwbWOOMi`#ynTm18zsb&aWS?N -Zj=d&)e0CyZTBTjsqRptU`6Q(lysCaJ3rk>xmqgvDfq5H^#Wp!>cw#MS3=gPuJQJg-tb4@uO!YtE1p| -pgt=T^}X60W3FD0d9?tBIv?0IuPYCf{`zz8`JjuJABaMCCMpXytc539P}W}igrL_XorBt0UPukI8qJ! -h*PgE2)T^@Z4qcUX0(GD5sQ9k013cRTyCRv^aBXa3Mbh_%A6|`mr1i`*vycB}9N2qtZ4GO*b8Y|Tm7~ -MvfnNFjM*m-*2kPX#X*KfdILb8+de2BdxHs~KiMsOp2AJ>ahhkP-L!Z)^tFH_5yrQB|de0QV02XxBgL -=LBjZWTh!Zl`w4JUJG7jhaQc77Mz-SZq*t+jJofg`$AVe=x4tp54e$uwa) -fF)GjAOtp)vL*v|e<;>_lNHFPmo&ka31=O -ph;bSshkX3>~yP~G+O%W%J?@g~FCWxdS?v-&z}q6wukfoq>TqF_^Gp96F|bcQ;1XI8&2Bs~57hcFD$@6~Y#O!dfn9yRe+`$)n=~`)Nq4$sL0>BM?HvCVUwQuL0bV=RtEI)e3H5sk|8$ -66wqEUexkr7kp}JCFDc?0-$d?|_m8ao12$-)t6w~tcGMop|^)ksq`KrWq@s2&_dONeJd<&zP^+kx~wT -SQJE9BRa$a9fm%25zy!mX{#M1CCy4*~<|5EC2u|0001RX>c!Jc4cm4Z*nhVXkl_> -WppoWVQyz=Wnyw=cWrNEWo#~RdDU85Pvb}ye)q39n$`$fax<=S5)xXYRiHyRT5bv;)K8H~3b5MP8QW< -(JDUH#-?8Jke9H~?VJTH?SDiZNJJ%{BfA~=UN5_M&XP0z-b~3oU8PM_h)yco9f%b#(fo`7_U4`zG>pL -Eu1QSJWL>Iw-T+egp&Ub&RFv86E*$E>R(?rE;90wtdL3&_h9L{_(Z{0%-GvkWpljEA3f&j_L{p%mIH2o!{2cfR#m(~^-SGmR50)AxHdzn^(X-?G&NH0`;e(2* -bhQ7kj|=m`;GX*sI^mpc{}?JoQScbQLd(xI4`wuWdWH`4iZNO%SW -_CCXgz_41C+RRp!^tMXL{2-mX*&mCj&xjR;Vq{xYA%45-UZuV0Ka|fq~{|s8V4YLS_l#}~1^;|H9R-xm^a{vc}i^1^Z8&>p>&(6NY{76>#Li=0sO2~W@0vGospL&egw;x0G31wIj$F_s(*)jBLMU!^z8gpaPwFp=O*MJN~ZIz -eCP%KQ=%`HKRmeL2Fp*~N3cHif*>^MIEZ2|Fw#0>`whgqzC#R- -QNIKb7e^Q8V5s(2Rq$*_Gy(fs8BeD|j_e;m)8G(;2DOO_8RF)D2K8ibQ%p*$iB>D*&{_pUz)UZYO}Ko -Qvf(+pBLBbCR+B?F9-GM&2MbL%{K>d3f1yW|xkMBH$6do{Wmei;l$7yWDcllBeFn49%h?L84ZVr{{uz -B3!aY;5x_o!LlcBhAfvGaEZaL!F*UJz>%zu3ZcF5?3BOM8|WdYYHLIc^v%^YkE+qn<~|^3_1jf1dDD5 -)3|mHO<_rNu;{k16y36MZ8xH6FVM6h@vsFJEE%j?ERosrGsZz9EJ*G#Eyg>zhqtKJOW1UPrqh8Wi!%< -xAZsx@T)V6!E7ird4L03IG>4WUBw_$cNC1}>-$PQ1*tROQvGz|fmf)^)?pf -$&$#H4LuA>i8Fc^-^ywiIY=7_tr3ARGu-UktR^xjYFX8xm|t!eRn5vcVLcI#3Hjq|HKs8gz!Vc@cVxi -69vUlwc;|Rtet-z?AThSQ4BHD-+ZsVkAJo1}7fJGl3Rr8^mD? -LlG=ZY#_nRpaRD$j7Wse(Lx-XBFP%Tw(50~XfQr-RlaT4;7~MspQW(u)2pl?W-r0Rcvbo3QSv~{k!pPa6BB`kIws-Uq=^LUk2 -w%`Lj&BrpemTbR7DlBSO&O0o!C2>4Qk7`I?f*q=`k7%d54RV@ji5()uPB%@7MnIcICx#1g1JQheGOfn -7vkC)()<#0Chg-4LW~_+A -P@2cfu#*JNqYB4WLsD|0^JKzlTwXDxhaCCdXUZu%^z=wgT_dd8=w$oX8ei`^hjvV#vJX2UXZ0|%nO`h -Eo+`goRdqGYhq%FK&)(5;nynsCIzKz+j31@*LKPUy)AMfuSnapwQ(S{YM^@~+jc~KHm-_;qIK3F5DTx -DqaahaAsfpGsxsFisLu(x#FL`4&HtSjiXPXkpV#BODz2=F8|K0){e@TSDJ;SAM1hQBYSIH}$pz>B52UZ~tYz;+z6sT0B>d -Bxf_SVEfIR~*V3uYJYWzwCW8dGXA{LYK1M8c7`F -5x1)8i1ZgwQMJLg%~&O|+KMk_nip1Qz#0U0qr7HxX+8=)4t7RRUy(Ak^lYdXv6UCf<5l+jd~3pS|kNxI2&qgE|q)ahCmkNLk!iXJg^ZadjPm`#zd%gi${Kaiysd3z*Ur87 -t!iefd&D$;u)$d8y+SFT>V+#BC~T3;ov=~PfK;@g^0uKVSkx-7dk7mB8_%yQzCH(g76S{^j@S1)Pmhd -(_AswwpzHds%zDk3VUH2uVs*MTgo_5U|6wzloVX9F)3rL8(_z88YB4WwBNCSm5eQbACz!4T9?Kv%}6*m0sSN%4wcI8F_`@B>T6p!Ny+LeN$^dP!1`*tmGN0*`TiW -HNPEffj}0P<_Pk7P(bj`HYjGJzR9kLgI$li4Ut&fDXUveZmsvXK@~x^wdA(RMk{gfZeA$HK~Z{*7jlS -oX`8dHN7|5$o2&HxJ5&9>kk{=>F1$N}{|^ST$UrOyi>F}AET>L!WO2}ZvkKLkxLtN^m@)} -jBBFNe5hvAxvq@PsvtZdqwH`<-wq{7w=>ITavruuVYddHtn+`B%1pY=^Wgz7c@lHyPL+aTKGZ_q|@Fb -cwn3P&%_bnl27zvkkPLH4HnMc;(KeE@kJy1%fZ^5H{58PBKg*zrcwAaePmiBZU(q=DQz=ymynjD+fkQ -qz(0pZHtNk*DHVg&;X~yhxcD!brs<-+57(`0Z8uMD4ePBKuAavwNwEd#3S^@n29&0|XQR000O8B@~!W -!@*U6)d~OrFfjlCDF6TfaA|NaUv_0~WN&gWV`yP=WMybDEbsAHj@Zfk-U=J)VtXKzK515k`nDWw_fZnur*Xm;&6E8%$X -4#=+!I#Kl*U=@#Kt7PY#dHu8!!#>G|PbX^rix;1yjzNIK8s`#6mfI?QI0;(|VBe~*(S!u8|hB8{g}nd -LN1VwsjSh0R`|r=Fy78Dr+}^1Dp3xy&iMqtfFO_GhvzrqNtlZsRO1aGB4ue02fGbdlxN{b??vQqJghx -$@XuD$13w`4US#l=SBCE%C`@3>RMWJvvAdx>U9WUCKh{-{q`|AS|iO=wewuWN95gs7m77Jj$1fwYywODzdxs3BcadGF#9zN-3AKxTxHNh?1go_FI-?{$nC9Y~*_8o|gg>>5SrZ<@N#G;{*;7=o4f`w~`ixyjvtacz -`W^J-PmL{^gnu&c4ytgUicS58I3q-rM`^hPa5(sUbb0s*GY&qSoSs~NQ|OORu -FsCHuITvuk`CzN;PU$9@XP7JC0%^Eyg0u)+9SG>Qd6wR^pV_9dTL54(qlf6b -M4G2EhN37!iOiQT%3+eF_p5j@#Sr?Q;yX&SvPVyO4DCRDo$SJk$!`y$pYHW&z+n`ne;BQv+3#C6l~(~Ai@{*ODyuA^=|hx9Q;4Uigs>~jc%0nL>km@+Pm%m-ga*?O2+u=SH%g0;zeKTLA({d425?#4&T;9qRso?A>%V(>JVx -Jf)d7|(gYDlQ_`8?f|*B|u)>f`1JH~Y#!9U~ay -=wsp#hC_U-=+|JW5D-@6FcfgcF`3{{;G5&V01^TMfpb!0%0|tBa~cDIz{OA*D?`Pz3JDH4CJk{tRL<2 -T?nl4}lO7*392|$h5#s3M7~mM<7~vS>fZYgo2$zpwH-g=$4+kR{j$k-~;RuE!7>;2$hT#~7V;GL%a16 -UK?8dMg!)^?_34?D03&k|T5Fl0vOcI!+GSgS*m?+ekA{VwH76~~VKr{mR7fKuh7 -$|{&JOa4_!V%DgP+|hnG*pw7Eyf@`0Z|1sR%yayFe>^A0Sw1#5a|Tl0;?0?(^y@Dm$6bIY(PAL*g!zK -0%{bHCAtR&V5l6a5THf@ISVE0P=$&~LS6HwN`kjR@D!&UHvjJ?|IuzD{-?XipX?&Ki3IH~qDoP95>+) -j=qAH0x`}E6yXhuj)lF0#|J%BWY82gW(yzP8pzbCLji|dxXmu0SD6l$+th$NnEF)hx>A$#}V6%%KQ;F -WH;CD_|Vn12Pzb#}smBl_p#(u^9yEw;wYQTj(HtFw;!r|DAb0+$h!C+4eMW}{ziBWs1`p5Rh<4Io)r} -7?~By4$Y3%17?oBSFyt?o2R?<=^f%duX#@xz}P4}3R;=Z>h|@zr_tQRjgf^iAg0wQ-@o+3E}uV7)*85 -ja)|_!uer?a2val6`uvS^MCb#(4=e*`%jS}0dJcGcE*j7Jx>O^PaMYXUA%bHq -fcL&}Y)7Px7mls##MkV?+;j52ls&SJN#U=#yYIZjeP6onScA>4r?GZeqL?4E(N~&rUKi>~51SZOMzN@ -%-&-5Z)3mXt@(x0a%@*eL**{uarkIbqC_H)mLEfDB@n;uk!!J9yLfuLK7qRC2Cb#?~A`mw&tEcoIV&N -IO1(yj%kD{o$96S}~xZQ{1OvQ{FuaA^d)t%XW;+*;q93u|1|->g2{#dE=|V^(F>lS%jA&|0^0wKfYce -lm4yK{U+pMuoiYMBKu#%Uzn$fnmC;1(Sc#bCsyBc9jKJ@nNs6344EL_rzW3pt@nQps#fIVND&fx(~P; -qTTdF-wwrS-dx$y?_{^$WI;cP+N{x5!k$M%7UjBTn@VJ%*9Y=K?_Ik1nnKU*^Rl>87x2Fm4y> -^n*);tv%0B?It$&NXyWv9U`s17eoYnFPh#dwHA!tku-P|NAe{BwmeS!MbU4s}yOZ4A$%d}%dwdu?9Ua -)ak9IAD-YmF}x4>5e>pQd^SW^oIYP*rK&YE3rws<pIfLYGj^Og@Zu%LSbEbbii8wx7(WM*{Gw}ePOIo&`yo}cVl< -Gu2uqHtjsy<%6csVdj;R0MSb^zHhdd_?RqVOZd6?@ZWNOHr15jG5y85j63jWXv*F&~)n71t?Spx*HfE -u%1H(yM-j;*6LH%ZD^L~PUH{l-v(QEaF>P?y5Wa`bVo5yJWdY?AG3EETB=IW*2nR*2RbILsafoa_rAX -cX3avo2kV58;M3$w@VRD@>WKY8^WOw@dQgQqI)&=y20_f7yL5_zP8>(dp7*dude-9gb#?b?5TLCa6U)CzW`860|XQR000O8B@~!W -A`j9(@(KU|8Y}<+Bme*aaA|NaUv_0~WN&gWV`yP=WMy#li>`sCfgAee`_)hoV?Z$7LBE!3P{r| -+L4`iIys_MP`m+&*b>na7WD8YSW=n`sdj;yn9ToFoyR=ksM6Popx+#Waa^T8 -b&g3_LUSG>yv`Gk??HbdoJ}F0uzv_Q=He3tbk|XrY>q6(>^sKhzC`-#czF)%3onrR@Ekc|}`Jdt{P9F1kE;3_~Fe3P22_Ol}O-?+{_&GQeZ8ei8!sqK7o7OKp+hLj<6EF -w{AUv9#NI~8QlB~om(*+ -qQ^bf=h1q_8*sehD}gdrrr)p7Kwf4$b_GEb}K@IVx+`90+BESBlCTt;<>R-#*x_qrk#oPE#oSv64|hy -W{j;x)PJAxo0%dyG-JE|y7IIT})8L>RIUdi9h7`618d@M;pzV|pwi#Gw{D@_ctdqK=GpNEwR-iMx&N6 -Mf*_p5Ba9K;5_5WsVh&e(`s)Nn#|43H_frNsJ;f%HQtXEGDec$bIF@V1#pw -Gr%*o4RP+_S^8o^_vqiJ{(YPiA90LRR$_*MC->33kLF3>3tYtnQzgVoh?+>8xS!Dda3h+%8jTnYeKA% -zFEMu8_LBMiYKG>b9NkTC(B@WOmni9{Tpiz9)ugAd4vehPVQvs0LvIkvqgdfp#KOks|bsN -FbQQ5`X~Gghok7&@n;>*-it&i5Sv}K1pbZZW;%{BV0!z1b_&HJsQzT<|xvJbBH!@AVShc+<+6bsUdiP -PzAJVNC+s(K$IBL61Qzo8p`62vRIJYn8;wERXzZ4a_OR8x5 -rJX|q=UxQ&9ADt^Xw$q4v`f}vLRKr_p+luupESb7m4c8sTvbyE4C7#m`2%h&Y71?XZP^9KG(vqb*I(O -{ZeD_EU2(WVZ3i6ljGK-*Je~sHSONQqk)56yV52}y}@%q -@cIVx;5+2@WI6w}QoyXkd3E1P@Im20BG0+mecdPWLtKImpe*^uk{-K=>E3((~E-o)*nhgJZE%I#4y<@f`P -qousvNmO*Zp=FQfM*7qWVvvAZTAHeAchCK3hi#8p26TmqD}EUN7+aSI#{Ym3>hdlE3e1mIxEiJsC>IO -TeFN~-L{aMAbp$6`L6t&9D2L(rgaT47OXhT{JK2Z%9_eSFptFGe(yGXiT*eVW -eCNhMYD6A>vrH5mJ%{6_zzs!TjtIoi;<*toEj;^~qtNu!rbMX9{lmgk&_MME!rfkL6qiigCW@*@F*Dsr6q@LqhdwE8(??-mO -Sl{xheZkdcmXF=9`rcgD;=qk|UMq86l+$mF*m5n%cEW}@;B}%&t@bt44(~4dz}AVI--=$01Ea-eCc6u -|3EybvNzUunD%*)$Auub!_||H%FMu}b^B8H~T>G?JPM+t%vvTdbvDMAXaBCD#yx#K~w)O&l0k*}SEqd -J|oWGuS-dL>Gm!qQD+N@T$*fu^_Y@&@yJAx(Ew;e2ycqOzwojsBGN(5$WRXz5ZiiDRs%IDj=?9T`5e% -SQB(EbHO`L5=m!@iBf?#po#iK^Af-_pAov6lnOdl#}|PxUYNPzQl=B$wTdjs93^-&|{*-Kxw+y~}}Vl -l7Y`m;TzA^kh%$t>@mZ<@T8^rnl8fp8Y$XmCCaL7yV3$6F388CmOnD@D|V3KC;!uYXxh!Wj?T*M}a8uEzxB%0^E7ez-u%1% -+O5lGlSucY^UMv44B(BGVBjAmZ$O{|Aer3lifzLy&tqt)b? -bgpXR{*?~!$|E}{*uJpUsEZxry_3AWhyectasV{h8i`Tx_>F -W`&>op%`(WeRKvxI+pLcFI>)b7gcE~D?X?&i_r{ekHFe#@u93;Nb?MxQ%iWSPf5XfXfSp&C`L7V$Jn? -iN`Q(>D@#)hDhP8Vn|iKEJ3&{i*fhPN$Fb1?OF&%TnhUauOwl_6@K?mv?D2*XZ~KpgsWAFI8s(h0ExX -I^Gk2_rCWZ@4rw>0|XQR000O8B@~!WwKBtG$O-@eUMm0qCjbBdaA|NaUv_0~WN&gWV`yP=WMy(|8Z)9a`E^v9>TFq|TND{vDDGHNAk_GKyH!WFmum{IuXN+-d!*(#p$)KgSO@t-|l1idk>}% -|M?0s*u54K-ZY<4w&6vdg-hM+|@tE;QN`l`C9=mqWkZHH#rJWd}DXj$IxO!SENV&EUWJ$`q3PG_e_$L -H6_^!DuH=rO!mT5eT$}Fc@605Y-rl?)dOg&BGGRDkb) -Mu4s3zbuLPi2oz$e*jSm_-Z4)W%s_;5J`md36KDbeZMr@ma2-QqAdZRcY)l73EsjVuht1OL}$mn#6QE -fr?)7T{=t>y3(=*U8zFlpVfTr$x)V;d3?8oo3D!32N)rGALXCuGEN@y_=$E17M4_Ibh#=Yv$T@<^Iv~ -F{P~}p-bTfz|Nc9L(9JUc6c>+l7Nrl%=s^LPHy^e2C#N^($Jf_%a&bk6ba{Anb9(gQ?C^>%KU`g2Tp#ZeT`OfE)?oI5+(3K}l}{O -voU1a5lVa`qM})V4fyta6qt6Oqo~ao0qX_x5SbbLvlPrCx{KXUt=o^afDa}fszT`2u1FCkS3oSOjJ~C -vs>(fX;C`vyi2*EX`pTzf2eUfBZ?$g_>D7EPOL(C1N5W$WJ13~oR`f#uWy`basU$(43FFYugqtU@S2e -X7`k5L}YK%Qbvgk`ael4M2mOcmNeQeVLvO&CqHQhhBe4SUdlP{+u+pRZK8%+qQ(JfLFrbO*h!7Rz*2E -~7O?E7gt4J5_-S%)Vs#yqYKvC_qL}wS7m%Bou|k0)?1vqPs*LcxTX-j&AF;*J3x>MUM1@9Q~bMdGO5t -k@NpW0m;y(aNnmroZ%GrI7brKV~pYX2+zlXPeR}8=eXX}*V6ZBZ-R#y+}pzh2!}$`7N2??O2!!5gM!g -`g9U^MQlFS4Fo|#qTuWFa^>ZkYSU_N9!o9#f>3b+32~)L&?4-nC1(vg^tv|@4fcCx>)EIq3RB*IuZKP5357epZ0(=_2~La$$AZAIz*r@Fo=Klu>9yB%4^fW?bx -dUScWuF1nYZ>y_#D}`-+aY@Va6Z8z3^wi$LB2`36R2GX%$t`&K82xUI5Op3J3vB374}yji+u`71(20V{d2WGO&7`tyvZPWF^kVR>iqOH -pGSOgQ>w#$MfvJ$Paa*j@d3zSPk@)7mU}}-w7pw(MRqoG(O%}P&JUM8Ap_r-4CMK4nVzULpVVpU7_C{ -WhIi%KG0~s%-=DJ~`U@N$kG07yD48uUMy~fQ(YqY7#qWLsYsIu{UBG5e=3{2!r{)>8L(YVA(#ByQNPS -|IS@LldY7KqMg(BvL(gd~IWuiBZ+T0H7ugEc3XvB8kcGcegax4n^Qg!@=2xCOih@rq!}jg1X&y#_1Nw -~yYcY%8L<=iE2>V0$FNdC=$Irqvq@M3aebJ&?`%(&yFE9A=mF3`y(*i@Po0CQhomkYf;l5}F -hEpVrqe#MMAyYqAK@LYf0Udb{*`nKq}IpGD3h%Mlvfy(AWw>M#LZT4Nkpe>u7C}iUH(<<_QEE59r-Wd -3HTjjg6snfQ_w>7e^NVX3!B}6u=B%8>ayB;RmE?H7*5g? -`VzBML!-xb2GgTag9X*kXIvdUxjDJ(CAv!*-*ecg5Co6C?Y#;(6c^A&R1}7ec=s1=&)-$<*QjXQCTaw^j@=vHdlS%|_X=z>> -VNfUnklwXvHEZ|-_A;1M?ak8sLOy8t&#?9$cE{2>-L7KChV?y>C?-6+@%ml28k+s;H@7Y8h`K-sBbuL -Xv1or>hLkvG&lN8Ai;Ss+`CkR5KezdvBf)P}JA9=S0J{JR3Kh<|UlF&Fyx0?S1XUeHnG12YA%Tj(}iXhosj;J9A9 -bY33~5BNHwg@Mb69wu7E-H_R6^fR$`!F_jq_TFf_Kkn<$bXHW|w;{eWxg)_42GUvAU3Iuevd_G>p>0o -x*LEy$1Ibs}te!D(+oNrX*KVM?#d9dI-(`VYE%>-+ymdEG_l+g+-3^$Je>Vngn>^|BUD<`Jy`62Ka{s -Qg&Ec78T^_bg?pOV63>pPC7rG?wW55qHQM9gp+=ahHK|j9R{YT=DTkzVef6G+=I>G!h;U6B+OZ|(i+c -LY&^e>xkpQ6Rf1KRY0o!dAIND?N -qtgkajVh?{oBRcM3tq=wZeRKuaa&KZ~axQRrl~`? -W+DH)o?q4w~Ur0+Rh9o4-opj*f79GJHhE`EjS&ms?731aBOCs*q-;CdR4Itszv-8X|Z)aSd!0p;QC;Q2?HL&qxg^ez6;}kzcCr&iZ^#<$P`SNVxAHbJXEb}sf -BL96-TuP_Qrg%WD>*_)E?8Q-)AcH+$BTNVzCOi=PE9O5Fh1I$mjtL!$8Pb?n&k`oW`l8Q5Q>rt61RJ5??HRj)pNR=>}?zj-!Fc&d~UQh*rDX>yDntd$T+0cWfI>YX~hEw -1%UuG48d7Fc=R9mffjhaT!ZfOv&7r4*C@nO`6L7IZmg}=> -=!;+$#fy+Uq4&V25C~O)cgeALuswIGi&pabR&ZB%)e2nesN@E(K9;~nxXGtz+Vq8x6=;i4k?La$v9-F -c*Dm!&O$Qj;C^__jPXFP=mrqI4rV=+zHF>uzrFg;Uv3j(}3>4Z-4I*Kp$buC%eWE}sJeb)EyG_%~Vdo -#mXpNly_!0huD@_|en0QkIC`H?5$^$mhtkDk?>Le6Vhqr741$7d*p~pGy#3fgle4i;5t=qWX$k)S#>* -uS+Pa=y}J*<(+;BVrc`M7T3`t_dml%4Q5h4t8cg0>0W+*ysWMh|^U!o4LOCCISnyVyjbruDGYf$mTqN -5UW}DwZ{TVF6~La>|9Q)@t}xt2NKZ-4f0`8>bVWemgd^&sQhD`uThf;%l7G*C4(|fe$

-qI{}j8i0{fg8+T27*L(Sn#&=D8*BkNKA6*D{Lwq+W--nPKM|?Nt*|;Y0HTUuzjju_3%|d+ -meyXIAT8DAB#CN;hFZg09-bJ?Zy9h^ow=rK%^>Lrw5#QYoU*=+T7xU3wL>l3U@9zBl&?3Ip`TLQ@G>OkNwF`}oGw==Ygvn;Ek54*2;c0b^=LDa>GJlP-I?4he7 -m-3ERY;cOpVsI%^$`#Ge)PjyqFq6L(#wus2$X8dFS$g1L@w7V$G?%4OtW$1TOzI#HPNHJntJjrYT5@i ->xI81itbc)!`jf)x7hUp`_H4AML#Vnr4}7`ATCIJRxEFv9C3;xgLwV}$Kf~&I!-xc{xp%(CtTrFST35 -VK%`R55Er>JE>s;PRSq3n9WOap^+mw8lyJ=Z*YWVsP1Ddm4T;02Lfe^#v0&T11B_|)4-!7te#2)d9*i -^njTBW2w6QOL;ZYoOqSl#b$r0TN+y&cqP%^cW0etG(*5&Q(_1&fZ>5v)U4Q4BMcF@G9JQq{OCXreNr& -YxqoEHm#RD@whu}`^=K|3@WIV_mbnCrx6^st=4Gc*lq9?hNUQf`<&8)@D>Mn8MMH`FQZnPUD@crNd++ --Jy6?iU+&4t>599NJ{Yu`edaIp^Dst(ZQf`3T)Bj!V_PHF6$1JBSy_+*fVaMzOJ;>ESng!6|Bw=5Cwj -588IQBS;v}ZzQI&&#=in5ls1n1>R1dV8A(u!rl|rW+D=*TD7L<{PCoOt+QH`cO$$g(#5KOL>_*N4KxXLP99*zT2FLHi$2O9KQH0000803{TdPBwKIP2B_l09 -6eD044wc0B~t=FJE?LZe(wAFJow7a%5$6FKuFDXkl`5Wpr?IZ(?O~E^v9RR_|}(MiBkZUopCa)?=|SU-R=OQ(!5 -38I3~y1CggC!`jx{Slb(`Bi$w~4^`!9`Cqz%a{zg*~FSS_HCqjmXjH2n*tdz_wW~(iJ{aD$1mGjLM$_ -%Djsc55wG)<%v98`nOu~=wdZ>{8!W~M|%<~!)z&{m}s_>we-BH1Mc`3(6o{7q^2uT?0btvyO*2oWq-! -+M<@lz$k{X~YTm(F04?$Z47`ViCZ3Bm$NRlb_JWo+Yb7-+@hJwoilx3h-G?Ziof$ZEV{UIHBuS=MZg= -*n|L7_l?aTJ+QHk4J5s#7&X)f&uR$@rXdNL!LCp-d;lPben(Hzf+mLC#Cj780E3nGrP(5J06g`b-XlD^cf--peYV*ThVHmOo6%t6P?x4%#~t?Oqpm~Kxig -*2`YlwP`5>_svr6P#;s}sy1tl>#zKD|@^=FhfL4t8eEB}>I=72G3A^B*V&Gub2jFntweBn|EdP5>2sn -BxYwLtHnXG_%uN>SfkZ#LW(1Tbh-K0B}7;E~}g>U=1(`kR|{NC;m&1X2d^iY3%| -C1Z(s#c3MCaJYwI2)}NV2|qA!Rz60#%7uw?hRR{X!^Z@y%BObGw3#C=?5zzIeV+sYC-HL*llTS#NF2L -30VE)4YQBAr4JuIWPht&a+B;s$Psy-z_#(cdctwkM9a&i7l{~+e>+x^_o((sQf#7nHExrvd!v=39pfB -*ZWL&+Z{^d2T6#WK(Dny}yCoa>)Dnvc8hOdO^45b&#x)i~AaAh?|uusd^0m(SSO&I@6aWAK2mmD%m`<(J5vXzl007(v001Tc003}la4%nWW -o~3|axY_OVRB?;bT4gUV{>P6Z*_2Ra&KZ~axQRrl~rAj+Bg(_=T}^5UO=K0=vHcX)CWj`(lQVwpwVcx -LMCy*9b-qfGo$p^?=c?(0V;JPAtm>odwlOXwz<2z{ZM}}8c%654hGYBK>f)q_(8wH-qr4CzTq@8Vl5P -t6zG(bu=JpR2`L#Zlz8FBB9m;GvD{J09Z#G#lmObQ9T~<-I04vK{=%isxgnjAyFm^7lslWSoFlZ|7D^ -<{39T$XO|H%E1H=?+vyUf+Gsja}l{?1krI_(td5NVqj_N^!y6tuc6gS~5>PbnF$6AWG<>rN_`$+<=oD -r)6^VMzRZ$yY5n0cm9$h8q$dPnHck<%2GZll!>5pPQTnAAS6-{##Zv;%}j>nztoxhC34j;cXtDOQG=( -o3Eh&dKV`y<(huDs@2#Q)GB5?7k_;=ZLBFy*8-d>QrQ<4;3nf2!2+_&DI{3A59lD;TH4Jh%0W;Q&_B| -NKiZx30IaAvy}Uo-JJ9t`bOeIBQ(^Y-+6LRTp;h)_Im;m>b5$^)eekJ2tak_eE--3xi4&>>5?QoG{?% -@5}YPYNvIv|29Dtcgc$S{dy-b13d^%XHo*WaJ&xz!W{WxXrcd_zi&u$c5B3Kvm0iw7+@&iPSdFXkyc2rrVK7b^}H!S}ENTCchvI^8O2)c3JVu4i+_^&_f -)lKk^upD#uX5vqVzV*Rm-=_7Kvn0viiZ2ouv)oO`k7I%9;h|jdEU(1P`54RRJY(~qa!2P*g)-9J=6Ng -x>mzj__%3r2h@dMI7w!qz5zaVdtuWPj4XwC`F-Ww+x;9L74EPbJW-V**f*;#KiE18pvmzgu~E>q`*-6 -%WSbeTFc=Q4Hj{{v7<0|XQR000O8B@~!WY4+g{j33(07+>%wKAk=!sne6z`54=oPMpj0gOj8F*{IJB2j@rMm}1{*rOL)PoSg^ -Gwc}YXJMyQTIU&3C|LeG}RriByWuv#ux{28PqYupJ^-Rcu+`Y~YT$c^Sz9Ad(kO%jCDr -<7&dr{y_=8+$~4?p|?DU6+3!C&mcajygCmT3%17_rD_7mMh|_mcWU)40wgu!4nXrCGo^3;kL200sNV7 -XF;smd64C<%qOm619{-s -+zUCgLUt$8!kbk~9gBjDl7g^fAF_{I_aoyD;NsZ#JP09$HR?usv?RwG^8@zYibVP0Y~Bgt2PpJ{xo|d -ulvyX6U{%v8bG$g)8)$p@12Tk)K0r5O6VB!#pUvGGF)Tn93SS_PIDEui3#!D)YZ^d~S54o?Or;~%2*$0y^n{%FLG&xh=QT^tO@Cr6iqgCV=P9A2D{`g -O)eoTp7J8k1_|WfRXJ%Ud5>a>^siaYI@4A8>j@D4;uKH`YCe(`<7GdK9zZ61iJ!bq-zMyN>G%lvRU%W -X_CvepF)*fdhR9dSBt}LN2TGTZ2w-*K4e8z)qI;mkXz01oUHP23a4wz8}=sp&v$K*V6;w>J7utHKVB; -j9rc(MfJn_XP-+BBGpP|7WlWU&b>MGWjJf%(mA#48^`0L2s%*(j(m9XrCq6r|F4b)Bld~CufRpej9T` -?7!*>6>rd|rxN5ns|G@O0kOn>9<=4=j_m(^71-1^Km~9I7OqshOayPmN*R#0b8)|+-@EctKzbW -`lHNPqNP04T6vLAuB1i#f)oR`c6}-&XTGg5Oc|JA&U)^SgrIrTDV`UBT~4eyf)K2;3BWv# -Z7h-&FH^g5Oi~dxGCf`OVBn;FjQ9lHa6o!MA2gd?NT0HGd-b6E)u!d|S=81>aWlr-DCK^QVG8Rr9&vb -2Xm}K3DT+fKsDa;(5ZO1(cEo)2r8q|q2gekzH{V{PJZlT_7pP4AW}7Ljx^9$lUG -pEoTnr>P}_;m*zXyh&9dNZsZ625lc+4f*N!b(j*B5oFYgqN(tS}hpqlJTY5#6j)?e((76kj0lj`?ST7DG#@C&|fc4ExBn^ -*~p^Tpp-@St`f9IN%?xksl|G|glIlfTE2!ddDl}{PF#0BDci`o80nw48ZVM0^k<(vQu%7MU%VR6(y2P -D!Cqwug&Rc9P=0N!G(v3YkF>8Bl?rUXUSuLEl&M -N0DFr;WL^xJL@o}sqWoB>`E1~!}Rzl(^Rzl%8Rzl(^R-!hQYCevYP&kg2kT{B!P&kg2kT{B!P&kg2kT -{B!P?i_RN-$<5_u@EKLh*5|gv3#-q)k{o94n!$AdZz#IF6N&IEs}}IF6N&xVgsc=3*rzj$$QxE&IvGN -=O{VN+=x1O0sA>7b}tD0ScDLpJv8fHc5~9Nbi6mA{3vf_ykjV5gJn{KEcBZ^KH0NnDO_xa^vrrTTrTlVu8tYPeNcQJ{FjinZc33P<$*fB#s1z!m+@R_ -;35TzVRr5={$?TGz+)LSYWb`c#ML;Py&hthQyJ;h_q(>?>Bi|Ul14=5VjDQO`1b&T2KEl{)O&SRZrqy -ZZ1H%J4JK{`jP?S?QW8@H>2c3vuC?|R@OSX87hvRU4+(Ko=|NUvv~x7dOtUOM9)6k!E1Ck?EE -(GVwrC(#RC*vBt>!)cD-iq6cYZz8p51Mx#=Z+p-fA{}DIK)L4$aOfyz0_096{XVFNE~??3ddfC#F3YwaO`DB9C;ZE$6h9j7Q9R{B0R*z+T*odtO> -%{AJU^ls3hBd^Zl9pJTbU2Tfu{fbvm%%)4^Dq=kUm{;DeX#@Q*6|=);6@KTS+){A_$+ihTddWP+(%UJT7k}Dh>1o6Z_Yf{zg5wcKO>tgP+>&tqc?OV4{rWV+Gf@9C8&|=A9o|DVm$QKS`0^Z*=3#qN} -ebqe^82|Nm0vkHX*9ov6%T@kz1q_PpJBOhQhM;&@kW1Tm5Qy=}53Zei?$wp8_B+ksC?7ssg -%@`VpT*(J57=@hgU~Oz+T}H-c)Y^Hp|a*=x!y&}&ieH(@;Q?18)`XD1+1WcnTDZW21!SiUFp4XoPKwPqJ}{x!20b@>OnP}HtqC9b7z(#eU8d4I6HJWJlBlQmj@lYTiq){M?!A6bP#V -ig8&R^iPmyjg`etFW{RgMM+*Lw2n0X1=|-gRC&ObDKvz7sy+M5?OCR)UF|WQq9b!#Bo)wp>#8BdI*zj -3U1<2ZL*7-7_4;@jf|TZY;+R?m76HzB?u=mP&f(NM-=L^?&TWNMGR6G0pCoh<@nev|N5!i%crd0?)6g -?{yQz0NCU;0FhBAa)RN4neEk%)wSXgkLE+e6ka*5tyyhH`^vx$GdIJjf7b~QwUSiRQTM8d^h}LCd= -ZIi^2S~kb?KdUv0m9t$rCB9(_Z(rrl~a_dBr}ag<87S+Vd7_nObY>e$%BlB@fxwwf3I3O54=hPe0P{) -LI=~>+E@Jbxf_Zbsy@{o05m@>RNZtTcvAi-4}5S+qImjYv!J}oM~$2w*5&Oz2c_iA$z*k+w)fGnOg6~ -`cwIqvvkec^Omzr&3bfy+WqhoU7PHA3r$RIvUA^hk@urjZC$hXx)oW~HZ^;7^Q)Sxq@!Q?rWN@czv`9 -6Z&wvJpq=GS^qMGR5}_**V>HSH8Rb2Dxe#NqikkwA=DWWu{|8V@0|XQR000O8B@~!W9(C^?p9cT{85R -HlCIA2caA|NaUv_0~WN&gWV`yP=WMy$RT#i -UYRF0EFiIy@)jF5 -dYBZWKQ7T@iw&&4E$x0eA$%0qr#*uV{9|n9LA-}|-A@`O6U(PDkR%?KZpSOYvJ<6PAx^gi+EL7Aq -&gch2-E57(}33lpoC*MUpKd&wvHU(jVQ)!(ck0cK?a~ -Xpcti{^aQ!WRVK&BEhjGzTS8oh7na@`{53b!@#3C>O4TBebeoACr`@zyY8fKj>mL27*U&s?a`#$nfBT -v8cs*U!Pu-Lxs0U{D=^DI-b1{A$+ZBIXcC)yG7kL->6LKMb7^J2GNjpI=qi_NRLExcQ#tg6ztr(UZN!RrGQ@nU%nf<}^{=piVJ!|qX -GRF^t-Yo>0jn*e3@Is^Bx}smQ>i=wMT~tJ~Z=VxuTvfE3%NAr=95iQ -HHOah%p$LO{1}aVUAak$pY)|#8Z?BmZqkP>LK1%{r9~preYbRhaNSVE??@zfr=wYL7>GR-}{&&BJt{+ -WJRhwHzK10Q$FcY6tvw{gWHV@MNkBy-AUZTyB{WK69k^Y%EWuU1C{bEOda*r#9NlHmIqzW1sIb4y^k= -+a+FrE2!B!`*_EfKg{Od)r)FWC>vL3g1TWw}HsLfcbvXZKYjl<(up_;GqWz%3RcEBktg7j)KsGRLW@1 -HAgwICxN%f+0$}OwWy(>W@N~)|V`_(CL;FH#TRoP;ogt8|T1m9EGC&mhnZiuyM| -ZFOsaUO|YY>nknZ6$RnY)ISSXp?2z{pmtQ{e7sfUgL#1Qhu{;(c=ha!>fz%RGs#O=IUdb}C*tWJAk4Mw--D6wwD^N>xiwihu)QLU?n -q5Ld!zc4h^W3vh(tVbY(9SnTb2yONJNu5D|G8**C%pv@>{i3IJ#}jIHOJ=YAGeDJ-AJ+9FN%%01QGNk -%!FXi`RjezfW^Xm0suF_}nlque|2h4( ->t+@eKYsv9*ISU%jx<$_s*!_niI1#>Gtox9@l@N=zdFZEPp%{iwu3f;2NXUQ#BeLRx@K!=Gcr@a^RU4 -$C-BV#Bzzd+-37IV3efKM(%hVA{X6x|4z)X2~2 -92~nJRRQ*>-%$X;)c;qZGH~^698*!-06`rz=zEg2)u2urq@jF>H@(8^Z)xB45)%Z`C{B%D0gNgNM9=f -eK3hjrwqf$N^K7;s;+blF;zMmRLlb7PGTC?6rDJ#z!C2x(iHD;Iep%`r8#1DDG2zMLqU=Q~ySBbaue_ -vK-mj$o(B$~P9J`}FeV+IN6H!xhiQLN1vpD$mIkq#>V22e`_1QY-O00;mj6qrtm{!gHV1ONcq4FCWp0 -001RX>c!Jc4cm4Z*nhVXkl_>WppodVqa&L8TaB^>AWpXZXd8JovZ{j!*{?4x$sb4^%IbcPvx7B_C -3N2dRL_k%wS|O7-VAa^jcHnZq{>Dit1OkOSOO?pXJdfwqcJJ=;7c|>1-2wEvt@gleL$fz*{e&~NcZEB -cEGY~XTQEU5w4_ggY3R#;80Q40f^DeMsPUFWX==cyhI&RS@X*vL>=?R&8HQ%hbVIpZQw1^rlS7U618T -HK))YmnRVbK843k3Rr&>!De;_f8q)OzTqQp=i=26Uex)o8DYaQWMO9RDL32K!}6)AGf9cXY4V{5BnOf -^*->L-!3q%exjLyWJeOW#of&?o8xMvN~MTfse`hX#ypzxzZGk0pIs%U6Qahx -Rn}U6$J}?o}F0nZH0|_zEEw&%bDTJB^Aul5X+QM6R@;aPOpfPxdx5jwf7);)u&=yO9S}btF#8zw0kQk -#8Yewl7D|Te -wUTp80hZ7XS_?HUH7`~(iZv-j&r?v=-eqFJX)`i3MP@*ph=qrz1jnr6Oi{jr72lYs*6P((s_a{38GcI -ksFL3W_+s@iv7&xN&?jD2@+D1ozIuFkNa6OQUXMRY2O%u5t{)ly66Ez95Pbamr -C|?sOTRYF3Q>uxDF_;v8G;b*Ia)RkGp(&v5!#hGx=rA8M5eijPf~bgt+&W1O(pUt`UoePWyKl*HdXY>c6h0L6?8uaR|0Zo;Y#H3z+4GCn3@|9yV1HKsDl~15_U3(H{~!pcQ-~->FM($bMPDtHJ1x~2=m#o`Q>;I54*1~mvgH_xP8uLw%&WTVLJ -SOj=@BQ^gp|I#;uIl!I!`+ZnjjQi@6Cc=SGu%v^w-}Lxo@bb;OWQd^x(jU4NGf*?krMFJiDI{smA=0| -XQR000O8B@~!W){$p2AO!#bz77BYApigXaA|NaUv_0~WN&gWV`yP=WMyAWpXZXd97A|Z -`w!@{hv=UYIG{-2p3YFbZykr5g-XYehC{@Q59LnUSO|g7wxXo(ocVHZDSiSxl}roZKX{Gty9Dtxm9H$;c864JL-lTh+V9{00f< -WX7ImVCX4Y5JjA^ExS(q~EN?{aV#73&RZhU}+XymJ}H09As@eN%PDl}vyO?PH3MaCb}M0}xqwX-TM6= -S63(tJZA~LatMI2w}(<;crqQej`J^+*wmBLeQ~Q8>TjTui^c8PD7?qDcv){6xdATMZ^OH4 -|%|ZX5?$yTAN-MO5NE)oU9PKlYmduy&=YNZgW2~rD37#R(qcP0<`m^o>B09H&;lO>g)!z-=ao=(1ZZe||O*=Dp(47xEGn&q4(}~k>5jl+QCAMN#iJVKk1ms3S$&e -YJM>?hci1um}7=^U7sAQmhib2q^@;A-*IbO_RPUj@iNVopcu{MSq(Z`k>XM-` -jBV#n%Wea1egSmw>*Dq*P71mD*U>Q3tVC*X{PzW&7P#n`rL9a_*u2xcAB>a#5=-mE2Hkvk2C{(#&j?T -!26NXD`txjtsxu8P>(A!^|`}&0?@!xL9d}QQP>znhUlM>n10TJsG7Z`w+n7b7yBd3vfOSoLFR;V#|kCu2eght%i4Or73T+<`SGm2r>PTJk3) -~tK8{jY&<4g_iPZ2)8TQdH7IK*5FH9-G%r4KEO|K{X4LeS(n2D7*gIf+f2Ojxi75l4mz;8bJTVJ5QH- -!RA#=^wCiA8Ui2kA?Jc1v-)baC?m8!=JOy-IHO*$;!A0CeMUKw83bNzQVBFZ`)-fPlqx2=eh@)LZ@8Y -%)7cnztd6s})2F*&<%m-~OTr|FR9S#w&s;1txf1*yK2S+dP3eeNqY~*_&CeOywr=Ms+p03gwI28c$qr -)IW(>;EUwzft5x7CB?vX0Ch%->g;!u`MBo|-2WxN*D3kp?}Ev_B?O6@ -XJYk2pwWb>y0dU~nm*GNz*@sip}UG1N5L-G`b9Ioq9x33PS=d%4&xMB_y?|k?VZc?xB_Pg$2eE;Eieb -uY=UrFOJA>-BB!#9;*w_m>DF|9NZ=1QY-O00;mj6qrtRfJZ%W -4FCYeFaQ830001RX>c!Jc4cm4Z*nhVXkl_>WppofZfSO9a&uv9WMy<^V{~tFE^v9xT5WS0$r1jpUoo- -j0;<3UknQuy<*U*Q$mArX5~v)xp|FNQ^jhJCHnN6CQFW^a -JLA)Y?lVbUhIYke%^+?)pm}Fn`KIgJ{tAcqkov$?5k_*09lI{OAzI09xa7e-pXi;O`d%1_afJO<<-rl&Fm`DNckEiYvp_6Z -oneI`3w?II3g^D3DEjji{s&+{0$8j%-@c@fnF&D=bB%GE}1=&s~On6G{9EXEHAP^gUfc -dd8XOZO7$gM#FXxWGE;QiI@fF1WfvJb~MH^;rf$G4!xQD77YtR;!X(iISlT?MumuCQmPBv -5({)EY;NCk9J9mWRTE~zLYAv1ouTV{Gqqo^WD0%D#FTk{SYr=?fbGCM*G?Bwvh%IMD;%rFUNu2vdv`9 -5pbz$rm_l^N_5Gm6PJ9^>>E$ta8?9!uQEwhLnvC7{@n+M*+P$+!g3tcmix)5UF8xX5a;UIjOBuhE88ab9OSO9<%TROhd%ibQ$n3h4Z9hv-c{TU{mB|mYnd -(!}`v0{Lp1%9+#2L2cdh-pqS(&ynhja;jdn1y>0FeIkxddn#6hj85*yRf}kWY2g7YnsO*x-brN7|A@d`UC4?C_ed5 -KA@22l471Ob?1%i-YtO}qd8>f#Kf4Xf2qgYUcN?}sNJ2kqfmd(b`|0Hxo-;w{HtE -QQOfuEb@g-O@bF*%{{0}P{m|__PW(@)_-_AdNNktK*C77ABBWn`^XAo0 -^=9RWRXzxB;y=iGv-PS~sTEWaKN&--#L(Eq@T$bn+{N&^#PE6-!;d9~!(9w-N(`@dG5l0wc)5$Ad068 -3vGPq@&V*0da7chX99AWFr#0jq-fTEknDwp*Vb<}*MeOeM5Va#o(uJ^Ivjd7e0_LOns;u?;e -+rr2pH#vHh2gmNRLUspt0N;B(O^lW#Qq5xKAJ#gC}8KatLGFxr-AWk!>o8-%4-NVmj}{KL;Xo-a8s4R -x=vakcBJ6^gVaQJOCU-roxCv=UEehq%8yq@}w~jA29NKt8J6Q+!)oA_8=G=W^v!5s9ov;#EX*`qK1mZz(G0U3!9s@I9v=5kyWK5zIj&T)qnU1WyQ2aIY@$l --Dj}pKD#-vmv#%BOE^a&GMneae!yLRk8& -ilFJttl=XgqfOZG=$el#MnWUWbXcjla|A42bKH{!_*$QNJ?qfsiy4wG^co+42V;1EbgZ0Rbm5FP~31a -az}GMjTDUEjc{RQV^!DN)u)wy;s&+t>@IoF&PE=&8o_-Dso@A41Y~9ppBR|kS%;AgJQ-zjAu8?d+sMkSWSb9`qn(B(c0nwa03gq0~u=Ud34_m{F_Rg8&XN~oweP -GmGr^7mJ)@aZ_rM|-!k}45~F0XTMB5cp7$^DoGyq!+QI2N;QT)Xst*!sgLe_TO@(WWZn^Vdd${;Qbvr -xcLr@3dckSwqzr$p{&Vk2)v>WK#bQ>R{Ewm(A9}leF^pI;xL)v+?@n>$;L&CmDYzEotp&O?BF4Eyc#7 -Y;C8OTFsY-->W5(@rTh;-jU(LdFR~jmddZ1uM>x4TOBgzio^m}+(y646`QaBAL=tCfw=^hqH;M#bSgPcn%9KVQ;y3jl6l0S(k!GMNOnSp+US(>J@Cps*98P+SSlr^vSMl@ZmW2klSGksE|3 -@MoSXQ$z6+83pAjift|!ce9DG9(eq_|mdIQChND`d+rko^G{l>&UmAP*3k6*6Z`DderC?WYSPO{{3dG -Qxhvi`_TkbGUdY^z>k@t^9=qc0_U(5Eh`5G)^Hjatkq^(nVREE2bHd*LL#dVEuhnR^(mr7>!}H?|k5;ElHl7Tw3U3v+v;51_Rfag!G`4-k#iUNZCp$8le#ZYL)~XjIm4fspz6U>p!?aACL+E)9-+& -gmqXI3u((x=EnMaNsxp>rrY?`^&J@9~+4?cJxqYfal2Ul$>(Z8*zb;)#30F|fcBZad?9)dsdbSwrMFIv%KX8)2-q_Xt%Nw$>OP`%cEN)jDEq6;}P53yPRV$vaduqvdB!8+D8h -vMcYLg!!O6XKu_h^N_g3d)xU86h=zBd&dTO7DzOGgOvJzw%Xt$5s6IL276KeJJLBqUeMttBT!RyF1_) -S-F7!}TlZ|4KyDNRdOE@>Ll<3;XNPMA)zz8|X|@o{QXMdz>Ui*{likvNkDg -?cEOUl%*Sr6^>5dO;)nL`+OTM{X)RhV~iyjg5u_KrKD!XoBP -;<^i^G7(qmijyXSmTlk@pCV*Xp9r!?qtmXjqvIpDqUb%hlBO*V_vNQ3CN9c$CfS;so4yFHz9Bog=hg1 -+3Xs+=d71lxTY75h3nwJbOW0zjn -=m<+l)F953#=JF0^Hy@$&RwUdXgnfbo?=Heljm**dNvY#goOeaIz@ojjTy2C%=li17FB?M~(SFsfn5r?5n5SWHc{>?a#EU8uylg -(=FLMMV5Mv)_YM=m$0K&D{Ma8SI1M#s?x$&_WeU~y!a6q;VuMym3Kh+!^OWsem`1`}9rOUBbwkt(=0T -S~Py&>nW6e|L9F6lcRN7;p|=5v##Njp`kfs*<4;MzLjXq-yKVJy`%|k$QtU<1587xCT;az(|;H&031O -ergS8OBJbY(WO)&f|jZIAYl)%l{v(b0EG$D6+Jb=KqQi%r6PPL6P9iZQ!WxJr%-KBWqQ}cXJ-LZ)T9) -iQJ|t~=J}Gdn7~sOqtFPEhD~A9>qDo;g%H0M0%J+^Wp^I{8TpIc@2E6c;Ku6jvek@TkP6z7DfH7cHwC -amqPM_l8fu5M&bHuCX#z}?v}@`;{Xv9O=m)J4ED>^zY0kUEK(X+361=#J00z!0d>we+zzJR-D3&@+UY -cLU#Ig-%L`+qwNSG}NCxV&n4POW`c$!Qn!E2#@JP91z_hIaMFo5~M3ns(Gbl|~!;muv&wg7y@U5krmo -@?^H#VJu{k~*27i5S;a>8~_iO#*oW>*yV6%rP?BD2V72*=&DkhFpqOSua5j4SfKXf{>;QABxfL(Dt27 -msk$-?b6c6Ru{hXDJT+eoJQaie9Tg!9&;&G7oMaxMd)lm*xpUQ-@ESr((3~(e6oD@fbERF^N3uwTB(v -5Sn)bi8qG$@B~?$;D?Xti8GSVK(pU$g$Z6^8fWpJX$~Uxaq%w-vOknx8qovPAtaM??D({}V^pvqRu7+ -gGSnF&Twg|?zp+5^}?#Q0D=!toxb!o0-wHqSo9?&@e*RexdG%uWc?h`yh&-&6T9SNoohK$Zl7`8R?v< -4K9>956oRC1FU^~=MYMcy0(zCKAx$j-B5l64XKG?whYUz6Wozhej`HB%`=p7uevgPr{o>_yx#NfV -@&OAcZDZ0xoisUf*7P#=s-7%A?h%=!-+yDPmHFb?IOl4C!Qj11P@sxHw)r --@#QpyNZjkP(?>w5crp=eC@c?z|p7Ngg!QQ+p;~+^_r;;ZQN!1_S>)=Hwb6;31UgVnr{s&@_$P{34Eg -Zq3mOK;f%sb@W15u1G>?t=?Qk!-xcjhoGXRGluN}?v^((oBix*#4+S)~@TW)Uw_3+3_PQEHj_kl51{3 -G`{&x!~*#%WHLacT#k-F~7mDO_ -??Ls^0SE0ld08!_k1(qWe~l{(Y*r_I~6V@6ScoasL2NO9KQH0000803{TdPDjU(*SP=y0P+9;03HAU0 -B~t=FJE?LZe(wAFJow7a%5$6FLq^eb7^mGE^v8Gk3CBSK@ddq`4z<^cb5!;MhAk37nmrhm@d7$y*rKb -vCQ7JL#i1EhkM -vtcjNA?`8>V7Kfk_g&A;8%?S<}lZg()hP)h>@6aWAK2mmD%m`;-h*D?bD00031001KZ003}la4%nWWo -~3|axY_OVRB?;bT4CUX)j-2X>MtBUtcb8dE){AP)h>@6aWAK2mmD%m`?X5w4a~^007ns001Qb003}la -4%nWWo~3|axY_OVRB?;bT4CUX)j}FVRB?;bY)|7E^v8`R!wi?HW0n*S4>`Rj)}Ztl1jf`Ey} -|iv4$=)FON>dXu6Ixy-GX%(3X42Jksm3Fx_7-1MFFxU4uCJMZh9w*@I6OZsT`WA(n+o6&?||Qh@z2B7 -~UW;<4UvYxUpjVwmUoQ(#eh$8-dXqHjp-MSKzp@2wW5=ZCR;X40Bv^{q^d!DSAhMya|cHd=IPs9Vb2v -6HydZqQ*>HUn=B+u9HS8VdtQDEArU$6{nd`43S3vUj!1SfFqm8%xVBw(krHovasY!tc0<0K-XyOivu? -$ghpCNqXEFOP)`|>$6N3YWN?E}{uP{t74k*c2aqhRal1~8OFmLQXlI08?wqiPuoO@RpYfNb2XC;Xnnw -7%(x}Hdzzu8A%cJ;v$X}9~q<*eOOhUn;8@%$vA!r=Vy<^-MzVl3{({#^jA*yGCXJFTuyAtbH_ntChq0 -zb{RxIBL>|E{{se40B0}E^j<7C-4+%!Art*HA3WDVP3i>dhK@4wiz1!fP~uU{UT7h&Z&NKUN9qmIRxDVH$~ee08E7bD5-bh~-M5D#huD7O-#Q(I~##gPTx)@#Do) -29K9dFT~;T>M3TJiI%&BT$sNCH$RH@yPpwgzgv74oED80CwxSa3hR!%N{9Yyxl{>C$0q-bS$NiKf1)@ -eVHjQ8l1}>~LgOCkb3wY1RP2#8H4>I8=3Z3C4Mo@)d>VRgT0BQ-C8}*FN?A)$4dOrUwV=J&OPWx1$MV%g8b{I#D+CN~e%ET7$c@3 -D7$=E1kui(@(pXX$}gj8|1Lx|_uQw24ob*IG1P9{)~$Y}eRVdriXz-Riy}0v-hQH~C{)%EBp+rYFHl; -#gr>IO5Vb9H4yU6OZJ`4UQzSS;i^pzFFBk{_zcfVXOR>((kt$`rr2eOrvbs*R?QrZ;-WE42NHg)#zn7 -kEfB74h$A~{4T-6W|dmNHk*3u;6P2o))W%f&ftOnBOk2qx-^=EyC&793FecS9cpCFq~*@-lJr<7{pBE -;ebv3cf|y<@%H_e$2@8dA%dSLne!YzV -N=4=MWbK$zM^#fmN@CY4kr(O9KQH0000803{TdPHjq{)3N{n0Pg?*03ZMW0B~t=FJE?LZe(wAFJo_PZ -*pO6VJ}}_X>MtBUtcb8c}D{=+|B@#>8)HEjqj`u)P$}SRaN`-!zH-+nW4vtfp_}V?cZPexfY%{!W@$qW3U|eBx -%V%(DkjO9KQH0000803{TdPCgZO+YcCG!^= -8E`Peh_SvDDolWR3(&?dn!~42hwxgewU1vW77ZKPw7qqNm&G3LWf8IGM)IrY#ER>OQCDowr#BvCdoF7 -cPWq!a1d01B*WQqLZXBU8YvJY%3=(vjO2oG#rOszX(mhv@h#&tm0fEIa7z-U^4sfc;N3m@&T@zu?Pg+ -gtykAtHv}2c96~K>O0axPHX!y_r{Di#Tl1y=vifFQqhMOBoEzL}#jrNjgenVleX^xj?OTiBB|z;2CtB -_|B9$mG{B3XTf+|6Uis4bU%x9ga6XV|AA2xm&9KSJtnKcC}LKz7F-ogWeL9Z$8_9g8H`;?n94nNEMsB -bn(sl;Az$$Z^_R`&cCM5^R!-7sXFsW5CwNY`atUspsWK;-V(C}8Viz+VDLp-7by?0Kgfy-!$_5DoF&_ -8A0ru@ZUAGfo(niel5LVz}CaPK-BF#v?dU>zv4$0_YY_;=uyD`d}9Vs;q^7EZyg{k;QB -b<|pCf^ZbaB1p`{`R(hAtqISR+115Dg_2sjWrz0JMt%K5(;cAAHlTD}Mz3SI?TfwgVo&GHkw&fEt~YQ -eWF{^<`1t8XXyBd{c3})|Trsqd#PR%T_8RJ_qQ0Lmo`#F@YuJ9p9u57Euc3}B>Zkd#Wd0bq9dh`8`oY -l6FzV$Gv%Ig*#rsS^>o_2#+GvU;o^)(L`i=n>!MN5+1Y85$ErMWH2YLqh989P4ry9^Nb36uTQl0@GhI -heiH9Q607{LELoSiY=8sO=EvI^?JOp@#nCei~AW}4!}=XoX+#2qtKdH`~?K -FQX_JmSE{RK&%MPbYWxD|1$1@TZY~=TrcmRS`CTTLI>*j*MOf6lY}+1p22r9)nT~0j~nev(gv>Zz@2a -RmFH)0p_fZ$!}_27}4naPk6y=@`Bgo1+U2qUXvHRCNFqRUhtZ{;5B){`?X$hohCE5PLms4r^ybk)8q% -&X)=WCG&#a`nk?Zubx$nSA^ndB#6lBGEfX0}9*zfF+{0|Xco@6aWAK2mmD%m`)?`Y;$P~007=7001KZ003}la4%nWWo~3|ax -Y_VY;SU5ZDB8AZgXjLZ+B^KGcqo4d97OebKAHP{@s5Cdj3IFD#@=-A5Jx{EbC$?j(uZ0eb0!(+1Pq3!AS#LSdCBC0Pf2H$hLU;R4*0tWVI**8$|5$Wl1+)^g -lDP38uR!YOTjS`#zN9KfJt=Cp0XM7{V)!t?|XtpGXPX>PJ1M>W+#0)F1TtnjK7I+3s-q>-J(i1U{$A5dEmkcsLVBZ_b4m6Zums57ChR0ld0GZCqy0!DQ4Gi -6rctaktY>2X56C(oU^6bxL=bVoroVS4YEG609ca++@J&V1)(k9icl;hpRCC3+SSAr$!h*nxkQ2Hk2Sw -{Wq(*?4@|D2K26YQ;3==ZCy1gL5f~HVam}YvxVC4h93tcFXP%&+LDrF#W3|b;^)_5@-{=&g1_6!O7$P(b41MgXf(bKR!pjr^z -eoLlJ#|6i@TAivNI6iWWXdCQYdjzqct6boUmk`OdTAg~`s#UA+V5N4;TvV@iZarD>EP_; -FM413)LYE|Pdchf*MVMJLto^D&6GLW|5poY;jQrXiB>>Bz7r}(my^68~!w5}=Nh{-D8985u<@PC$s#> -+hNIdpeI@Li1SP=^?84gJ&mL12Od(3MgF{RNYSRJFEE(q2$SR&6XP%SLBASeN$!HQL^A_oZGE$JFmNG -4QgMzH^b?lA&jDv7F?9dIp6RD)TJ=74x?FdvZddJd+OJEPwnKnjCAkZ?f3noxEGfl3b(qnD)?bE_^d4 -WpMgpE6OjKRO2mtVQ35snopaErdqRcOI8h1Xs$670X)_(n%QJZ4lHjvn?dzey*s3&dgG)6N`yjWijjDZ<3 -<5V+B9LgMJeQ0n4(R0XtF|`vluE2zI`rQ990*Pfnpx_4GWPZq7t@zOQ5VQj&P616P!@s9y_-*3ZdbO! -j}T7py~xhyCDK-2CQ|6za}x=>|&;1C36za4Yu1k+Z@CqR4g}pS1AZMq16MX?`iRjmD`Ztra&D!v?P4~miWOB7xTfJ`6|2nz6yuRNIpx?K9|7FtV{Fh0a_UF-u$<^ -IxGk_6j!x(=ZwS)lQwrSs=U);4g01^D!K6$VJYU*tomzdNlDg4Pl-$3I(u5-w8)ltm`!g4?aO3KER$HRa7JcJM9j2SI270tOJ(I-^YFD53=eW^-1MwYcemmurC;WziuPu1Q -o2=cs%W=ylhUtoQw5-vn~e7*ZmK!Ik(-ouotr8E?cAg=D%?~7XyGQMUFD{F1UGS$nY=PLY4tXZP2AL! -Hg6|iGdIyzLH -tZQELIe@zox+4JgM4?y|Q+}g8Zb4F~1>Ia+dW~p?nyisTtWn$s9@Ycq>DQ_4cZ;#d2E9-diw7pwz0K_ -I=_xoOF+368)zt?r$1O#^v48hyHEq*f~mA2n?p&szYd33iq_o$-wi1QH7gHY6b8>^hlnKM1L-=pzbog -g^d-j;(^yKRF#pRr3Fu-cPUz0oYraEe3Js{`RvM2KpWlDNDXS%YCOa6-svIcDbEt&9`lsk2WzS) -pshnkEY`l&lf}^~a;4|EU0DeDoctYMY_)P9l)^EHKW(vy3*$;b(T1rL98AG@j)%?aRC?Q0s&#FifL-` -az}HVbgH;-#X`YHjUeRz3uV8D_8%HRaZX_^0JLj{`2J2;(M9hOV<0W0x!g+V7vKIdHr2(o4NQS@eX=z6yNO38)jFWEqh_#85f2i@cVqI#Q)=`Wv -4DwvK&)!7BXjN;ne(NrWBKI0IsyUhPyFk?y6=>8cg54O9T&8+T+UyS=$ocA2ZCKNIP -*_hJ^*|+8f7ox9Gthyu}Vsecl;Z)?;0u@YL_im{Xm=8GU@c~*j3_?hx8 -V`@WuY8@g9WA;A|fLx>A?Aq#*a1pAHnSDZ^1&=bx8co1vNvrr)#n|#)hUm4j!H|?iR6=`FG9=DPrpD@OhXyz|0EJZfME;8^p^=9V}t6*G~pZ+IJx$em -p_4qqDk|xV!=k%#e7!r*gxD*J=`^@uq+E4I9-;hB{*3-{{>J>0|XQR000O8B@~!W8*fE&g8~2mdj|jj -A^-pYaA|NaUv_0~WN&gWV{dG4a$#*@FKKRRbZKF1X>(;RaCwbXO^?$s5WVMDjMT%Xs}R8!38_*fpcJt -P?PXVR093inq%}BpHFhZZ_t?&tlkL)tRMnmtkKdbl;~A{fu_nGh82GFtGY2N&6C)$AhMSkK7Ad^H{{V -{@E5Ry_AovcY#soAXe7Rk%@9x)7aDi#W%cCL#%-~;cy4_AmyElSwJBzM$6cwaONSV~!s1uiq8)&`ntm -#30bEYVYlm)iYCRde#4DOV~-h@P*DLN559xu>eAiX2XPt=gSD}$BPgTa -1Bx!pJwUYGq}~dLYTdIy#%%Wg*&66^rhV5PL=3GZ9%|S1lli183zRCX>fmwh)Zr_RvG18nFpJ=n*KpD -nage)VTd2wwWFLpP=%I2$G|~$c3?8MzdHWjC+t(TS$?*>-A9buc7J7}!Znei_{t~u5ZtlCD#6k{adU8 -4L74**Fxvb3ujbADd!e>Wz_>vPj9U{%1_enV=yj8BtOu*4wAqZDO|x5Pf7DiDhAwHv83dON&yX}c_$WP2nJcBh0QR`w&?Xg{2=;Q3#s|)%d0wd|^WLBf)zc!Jc4cm4Z*nhVZ)|UJVQpbAcWG`jGA?j=%~xA*qec{d_pcZw+8VY5(ovQQ~DBND}*yaJ_2LzvZy`fAHU+gx(b4sp6z3)$_@rrqDH>F+xEN4O^L6V@h+L58e?1wnLoleazbttUN -!HW+YL5yo5SiH&vj2WZMB9ojie=#rX_GUDNoZ^5O(Uj8!mhd7<4akys>s8>9kM3o~jq=_mb_Hj{Y&l*Ga>>zrzPLr*dvLa!Q`DiKc$_24%$ZNet}|MU$20fl%iZ -00;ZA0EbEDK=;FZtO(GkzdBu=GTpQd(XBempk!x)l)yJ+^$k|-bJDA+pjcQ~??U#8VK9%YPEc29XMVw -!d`Zp+AW+20bP%JvGnj{vy9~Dk;ATFb7Vx{f4&sa?>J#>`4zv3RT~O^iafS^2HItvOr>dFjrmBQr@z8HsS`Ys_kV -jM~&Q)P`?bq(R1A-!wHhdXVBq1>V|?gR4Sar$xTCz+}^-VS1@@8Rt+*2z5j>#k>B7%xJ=7eT~(2y`MeL~j)N>VDKIDN89u{b -OR?62gvD{@LO7faG(PHNk_HN)4r(|HoBeUT44VP9%;_?+2f1?UhtJ&`w9jKwy>t1Y>nITH!sOp)!n)V -WRIWdpvDlE#a*Gi!2qYv#n*SubWKrveLs+lZwG|Ad@bI(B?F^++it^=Q=KKgY0@H^?C;+-DT8uN!^aD -64^?^`1H?in3hfYr+jA==iT!s8ik*Ug3N!IOP>W3+V!QwgyZNw)W6vMrEyHZ2yxoIY|4mP7a;ZYNmT3 -(5pt>$P-APM1=Pi;W6>PK8AXbPc1lx{ILOV-T@d|!%=4RS(T4vL(wa~G+Vn{77GDsllfFH*BQkvMMR# -!AIx<8L(9R5G#uo~l;^VX%u!1KCiX#`7Is@l?b)B&TWU+>_I+0Kzu9iJL;KlFGU)kao$a^#lMRQ%K0P -EV4y?TI>(^-PZCgB)_f$Njlm)7$Eb@ylgTWt)Phz5tcch3{Un{Ha&X6ZRahB}Mu_vsp8BzlNp)_fmz; -eekg#2x;K#g6nU>y7dP)h>@6aWAK2mmD%m`X7001T#0018V003}la4%nWWo~3|axY_VY;SU5Z -DB8WX>N37a&0bfdF>e6ZW>4Qov%0&QVa>+f}N&OB_i3FI8Gg?z)|8Sw3oqbvdo~_*2@GClH4UI6qxfp)D -X?n{uA$S~Qf}}3Xl!%FscWJ|s(5x0bnKDjLx;~V^7CdQkfhU12c&Q9&fvBJYvkq4j7>@`C!GO%O#1}H -vrRV#fA*Rv^5nHA$SP}>r=WGdLM)J@qZl1+%%<$S5YZjr*{1ki|IW!P4piDrRupmI`#1Ch{kTY*t+)$ -?A(oDRVqX7$_qkhQ>D-tu;Rx}0njqoLpg&-7y6}&~q{c>)5rEV*+NuV+x)s0B%l{}fK6qAKoQ`^<|i51$XAmt>Jpw|al6+hOB(wA%^P^xtiZ~1Buni}S)QR|r%+J~rLQaAw)ml24pD(h(4y^x&${sUHp-0vto>A -cS>cz-W*jX)4|cRl)=Eb^&Cc$C)#)C%g}=+u_JsLypIh9%-~Clg4vd5H4Elfng4dJ>muzmtt3FXj)@n)1 -5F2mc}Ax)c8Q7TIS~W`z$Ml*o&6EjHitMX=tr7IVYjodq3%!kHZjGgBbT@f&&~ISo=8eQxe$|T8`ARe -J+0>_X}LWQs`)EZ$o?UGrY*RUg!MQJvXB@zb<8q5?c0H4(^jUg)=L22REgoxn5Z^&IuLCO7A8yy;B=e -PLA~^cOari24!H1#VfJ1Vprs2p?%Xq)`tJfS?WfyNYh+JD^rc@{pd@U3gh_u7OZyp`)O?0=~Ghx1+vz -cL=%guVdtK-z|K*)ZyxHc4H@~7amd<i{i>7MsAD3RdI= -G%QZ`keT7tc@clKftkh_>tne1B`ow$s&U=i~ix23wp9QSUEccatlJaq#O(;z;eMOr3d0x3IaHkYpyG( -DW*8zdA6gd|B)`9w0#l1%{u#ceHF;^1@B1QwN$x?m3;$A1ybspk%STavt8YCzORnM*b9T$Ee77O#;_h -up4$o5HF9s&78q0g*`xU0kDJOzv9b_7SQlKw5 -rbi0lyTgoY|JBcchqf~7?`u`>1hoM?E?vmAGdTee^<#ld9rM5>8XLBa(7PwT?oDvd|$idum3Sf_ePII -H#Cov^Y+3F?#pU~*bY__*~7xTZ>%b&1SIqmfL(Wd#m&1y@Veyp7wqRjzq?1!|`zWTql{p8l>JcvXrip -R71wwlSti&WuitlB?NO9KQH0000803{TdP8*_L`CI`207U`-03QGV0B~t=FJE?LZe(wAFJx(RbZlv2F -JE72ZfSI1UoLQYg;Bw7!Y~ZI=PN96pq)m=xC~X|GS+EG2qwyQtwQUz+1jLX%?5uzx0H$7a!Bl#=jZ2S -1e3c7q%JeI2;s>2WZofaq(-oG&6!M5GI&cLX48k+M7BrxFjk)-)}N*cgtj&*+7>fl2V5e&ow;-tVwdx -eCWIGvw3%ijm;^|mrJrRoha)@53PP;Wa;Fs|!g{rgHgOd2i9y>8mfOvKw|aWtZ+CGA3Qw83al}x$q7d -UsCCN8ZD=m@)7BHR$vtT|}Dy?+e!n0|)GKc7tV$<31{vtnAAgd9UQcUMdnv<)q(lB(|9INV8+*C*k`* -K+IHxZGe5Ju3DE@~Zubuyl~>7_0PZi>A@HyHB4LANr@q3ltw#VWrC!qidbhZldhG^`E(OG*hf}L4myPF)vB1t}#jcffVR0yAE+XkngKsP)h>@6aWAK2mmD%m`-Ne(&;ZN -006w90012T003}la4%nWWo~3|axY|Qb98KJVlQKFZE#_9E^vA6ed~JMHkROjJq4!Jo?!r26U$@CNp=)2Ig^8~w1Wion6aIS1#418`p)JUk -F_nPkPyk!YL6!RPqP-u~XcI4)P~D!aLDL~uM2PaYpWJ%E2c6W?V;v=-;l&)F)3X5VCSTGVMGno=~ksd -%++Zp%WPmy6~;s#5WyY>OmnGWZQXq?L$ZyrQYH>$WN3+f9|G%d}_^)_IzWZ%&U-UY?(X&AUcKMIw&RU -S7OD{p!ud+3R!s5!&r7s&Xmj^F`aVRXU%GY`H2cXntLndE2CO{=T;-e=lqGsY=zSdR_1BHP!lPPr#r0 -vCj9>cX7IE#3}u9QdMPjB*ea0Rng5dIub<*=)X-X>IUt_OuQ_M6vkR0%n%;cFxPojEXvErSK^Q2>2QC -V$2riVxH&yjNT}~T0)3e`>s4CAP}fnN@=sHG+@$XsdI-SfM-KEH -0WyvYinC$y1YYO~34SuC=fS5Z|X8*Ahvd|%P`ZrE^s9mRLlL6P;gZL+*;(N;NiQqR@*DSm2;?(@%OR? -vrfm1m87OrI@ad3LQp1@N^>fkNq90P#y(HZHAYHo{PQL*`y(r7`T -O}J)LFgbcCwl}ZNf$hBP*09DWRQ-m268r05yO+I;+D~b*bRKF!?eIh!I;+Lc%oat#M-KmlL7F1CUck$ -Ph7%0(Uf_e{?dZ*(A>*a3WR<0`WrckuP-iM@y9`0P@Au!I*XsR&F$B)H1kyUd==I8eOV>n-fZh{b$gv -AS(V0(`f<1`3S%-yq|=JYfvc^~JL36fjANWj;3lPRn+AAxZT|FbnOCdW`SIV{v?b~J1wV9z{rckDZ{# -A-BdK5Pah5sAOV+GGCU_xL(LHE}Roeic_2&+9)}&QambqTF87$R?7aM=t-gXh?b=q~HuPi9@77$!j<& -Dx=N?b{V@nLXV){P;OqtU2)HpYK1k5%Oas2j%j+obFf!Lkh -h|n2p7s*yG`ab!Ikzee<8rwIB^QkU8iWrgf5Lx$H#~{w6Z?^O0J4^ew!p%61hT -PQ{bn(nNHp0-@~fr9k*Csy{1tQTp(;ukGPSADjzla(ecC6|GiUZ2G$1r`&tWd{}wIIteWW9&FLzT8N} -d-mui^&s5Vr)o-a>7p;nwoQW9gI?FDYs&EH0p#Cg#u(kDX)t3gCk5rrT?}N0bOTnHJHQT0WGR&6JoOF -i8XER-09D?G7HYlwg)$E`JjZ8~t>2Cv&SD`z49Q8BH@eYbSBu$>df+RFIYW8hf-PFv~ik19K>G;ksrT -*w;%ATZv=BQstV#P5)2E89gP0FP8d9MQ+D9%9)dk})}qP*pgh9N+mzSsFT^Q5Q|fjMi66TM!7h8^oXb!W@)ns%j)J4 -vf)v+h_UOw?0LXFjUkOmG^kE5uS^ABl#NIV+r&s^{-QIT+`XdyNfD9)3dW(~o8{@ES(d0%s&pA;6zR; -k4}<|z`RVZJsspHr1PxVpuv~2cpdXGcGt@Qx(x$JC_`W!;Yw%h24?p|j^T$UbdRt}*aIDB+Dx!o{pH^ -=nUK7`AKy%U7NdIN&*wTH7z76(9;Ms-;L^i;&RB_JN!e2Ox#K|KlXDZ~-T4Unx^8+6PH5qBNM^ -OxrW#rH^z#3wNBssgE=7#_nuSe+DbZC9{_mM|j`Lo6olAC(hvdi|*1%#jC{ -2YhX>LAfl&VQ93zfFN8d!t<}s-hA_1yga)QQ3DNEL}+$?BG3wH4Xuy?32L)NhKjHd_qSPmYXHQkUJF8 -e8&y$^>I!|V3K#$yQRLBenu`)zRxCW&2W9#Ig_<9SCV7;+Xc2iZV -m)HXQCD-~L^+AY^QjrAX%fE(KpBRfS%?&I=5OG+(_gQlrjNcUp2jKC7;nO_pKX -xaym4#13r^jz%qXX+6_>vCs5lEaauhS#}2sj0h0AS#=1Ibet)*JO;fz2`)xGHElM|t))0Fjia{%A1`l -N((0w2q@y8YmJ^Ow!k>OTRM|t@{kUN)!vt`a-i@dd*XjhBu+OzrTO@A)|wTvC1O|&8zW`4>5E>c>pDB -t-~luh={D=QFkiAf`C4|DL|15OUrZ)R>|rUmzTB=M>`;holGR4l^x*#zR$*}vc^A#;}G5L03l8Ir+*) -#Jql0%{POJe$?>!ElU~FEjg8m`xi&Ejz3p!Vs4+WJZ(H{7(kff5arHOiAvSy{0+?-*VlE>=3IH&4o_J -vR>3XgEgK;=7K2=BQDVDY(Jl7r -jCz!h)&gV^q`Nih$3=IGTfWRCi4bt{E~u30O(7h_bu|DvPrMmNNjWK^zd_MO)BXcW7SYsOSWRCjDIFf -fR7;2AR9gKMO{z^8g=o>^dB$4&OupmJ9Xd$b#4V8=;M^i4O=AzhSBtLP}B$BF#v_Z-a7t^7>+a_UhvF -?B%m>5D@+F`sClnbOo~PE?rN>TOMge3=LR$$JEFMjzdepyn=4=gSgpyq-LcSGXa~p>{YE?MTcr%&KRYvEb=wz>J)gApc*I33UZsF5Q* -gFInqRc2CCK$AS8uFIqNyn4IV2u$f7ipxIbqPt=nrqY4*hrKm5@5@o~ZO(jeOp9#xDY3sE9@R6Q&^aL -X3jgNN9{nhU*c3^ucaM$+O(PqzOkuB*QR?yJA;QUDQTpju79uRrvy_MUS)(u?|mZzYbBGOdfTbdJ=D1 -zY`~?l*oQf-Y=BqsR7R8La~JfTwyAlgXs#8JWlf(f=OUKd83gtT2EGYoq@#gbgeqU{6(oAB#aiCpW0I -Jm}dA3c~d?2fccqlAURbWQ4ZU4&JRzm~#q%uy4HrX9KMvCP=^?Nd2Mvme^hO4NAhxHpu$(GjW5dA6RF -x>OgCm)XB2Vn+%ZRMO~ArkkObj)Lx*J2AG2v)Xe7ryW|fa-rYylP3`V!z{7ApPqG-AhV3c=QU|hSk6f -H>RP^FVT)`=-)yC-!!9`%`RM1u`0GpyHA`+-FOx+KjMh6SCyoSH>6 -GHh3=NSQeX@uVVLh%zKWw&DQwuY}77U+%=x1`=?Rauao7$IJx31Jq4u?A5=`d1l*5CwY*Kpjxn$j>yY -3_Ni}15Z<+_Qf%pId&i;Ne!Hr8Y3dlN(L!}DOeXwgc8ln2>4l&WE_zw<5Mdm%K&G>GC%HF>6@nv^VU{J%+a -#5Q!nReK!@_)bS~QokV5}Jz;3efypwN^t?Ws6MCt8?7CzJH1TnX_xBCJL -_hE56g`>w7t9oY1_{A7J$Dd)^ooeys58LR)Hw$!2~nr3iG?DTB0xafpoT&7Ji5<{r%y&cZ?i{t(L{GB -?(j8*6&(gwxm{!A3+VOG=_);*qfJq)ZFBP7+y4;QpZQ)FL+2$gaf4N27^{X!ht>fWms#R6u67q2#COJMdjGW+axt&}o5XA9X*A0~5K=@?1n1hl2l#lc{fM$1SVkHfz`p^Kh -dTSt`&6>Bt2I%NQD?hr|d?IXGx=SA^6X?LC$%Q37jT+(vSt^`Zl6*5@Y75EqI{LINohPHVvo)sGW$9+ -29S+wjpBEo3=Ib^!0ndfNBT-1g!Vg5Mvf1LRE-6*2{>B)=BxvIUZ%OUbmru)7v{c9&{NL;9q^AZXf4n -?(&vK;14D**lpt0OrIk=DdO2bt!(v(1dJ5Nh$ZU2ix-|%@9nuc2r2=M!4`*5h;xCwr -4rnzw{KVC%x?sEp=dBy5U^+lkhXn3?ZfH`=@(cUiP}(gW9^2_34b{#dB|QkH-y -gy!yJC{^5sgcH{iKKmt3sd(HSoGKN!IDr<6cyXsaOM(Ct( -$FJ6})?-q*T}7x@9=N6B*|H9uiSf_T+i2|X_pgM9)zcWu+K@U0Ux?9mx1|&?qlK$T$H(eHzgTu%bIs6 -)@L{MzRKx0qiNRx{t>2mcs7AsJib@1*94RjIocG&w4(3?F(sH4oV;04rkfeZ7wh7Oty1$*fsJUj7O`r -NEAMqB5dc@zldj%l>RT|}-6Vi}Mv4FV?h{n -dVxI@JFm5`#=Bde^Y1zu_uNm5}uW5?vfbjtrz(`kBS@AsloBt^ON(Fi;0_5kx+b1X?G`0LSEo=$yyv~ -wvo0lfUMu$lk3y!3#)s19oNR{#>XfF=q8R)`@{`*wLM+C87TG=6*=z~jysuFA{ZcYPBz=FMOF>K%C8} -o@T&;b$W8)#q39yT&5%@)vq6<THG5Sb%td}Hz?m;~g4+h;w{6joD5`pVN+X3^5Nl0 -F>A413-Nw5PkcVI~<@%yhzNGBa$-lawMH|@A4X#+#f73LpoN^t72*6m-NeLH`1@#1q%W3?;OTvf`QgC -|)!uTp0peZ&}C9?h_BIjFN6s;^Vqd?uYSbJXFL8T6q -{4zpZt93icxtJ6(U!}P5nZ4%UkZoi?~GdoiN{<9a-{WA{Z?By*OZz|mcU`0R`4Qrpx5tR -_11Cz&kaOlfJdD0cJ#?UYB1TL0L=zOEhR(%4MtITVOh3#;YX`SIx~`QBK)DJwc?WDhW`I93c$2=psI5 -X-rY)o1ig`9ro7r>*bhSX1=vNkYu0d{b9Rn-2)taE-s6d2I)fk8lT_E`2EltgR65Q)3R)bp1lFf7;DE}rDtAydh -f3lv;0L%*Rm1Duq*FxTmA^fm)~3XEK4ac$*uFmngWym3lrB_T{kZe!x8E$)i)zFRe1Qr@#YdIsLzkPY -y(h#ZOczmAlc=$K)no9-tf{_P@1*`iaT-ZDC38RxJ5Nh{5v3bEhB6gl8-jOmCu)pHb6Ak!=POb`L{p@=-)$0jrIu)O(xvu|7u~=4>^*UIY!I$;i-k`g{xr7P5v5Y^rC8=vO+lw{eSuN$ -lmyK?Wv8wR5;Yte_(>UPqA3kXS+=sNKo9UhnM3ff&pymY3W2NHFM^B}@izwdR6w1<~&^Z(&CFwv{OdFypn)x2tpLgry<|Gd|V%KZ2wv~NpJ{ZZAh$P#WdrK}BY`vilV)!2nja&%~H- -KN$w^Q(UY6z#>s5dPu4zETIKW(8|ekZ0!Y1ZoGXbeCT)&4!OW0-v>R_xfcE26qj4Yi4pLIaTfk6@jW9U -(xe*9A*$5>nz^?E?$t}RFmg6b7g&zNoIU~W_tkK4yw-U`AaHS#xTfZ5r3f8xHrrQa+C!y{tjIk`B76E -7suF_dDfO-Pz1}0~g7-GQ}*jv23MPST@6%|}#Ks$nOv-I9bS71ht?iR{vpH{)83=LT8inBVLc?z+4)N -mFGyv9{~oo6v+c48yzqKi!dTOK$Od|Z}JBEb;#Lp+SfSqK8!mFDvFYD@wfSaOU~%aT}v<3n1WAOdBYu -b}}}Lr1J|WFn^vf=>&+N-081eqUuxS}QWuoS;k6JZ0NNwPNK86AQY&n0x>YO=4hRJa)4TZoxN_p>I=8 -p-o1o0ReL@4cBYb)8tx6^4WFIa{hLR&xt6Dq2Ok}1@)XvwP*-=^fM!o#d<-RgAMn3=EwX_H5G^oWe(~ -lpy?pTFW>)q)wSCfQGOq-VR#UmG^(l64v}(+BNGIgKBy*rTP}Y|*X-hx9(91gO27l58-SqAl_cS!E;) -_95+Ae``aTnaatUKxSQqg^E%A47Vd8W~AAh6BQ1y2i2vOd~8lapiWg7Bf(KtWU44h2l1go^gN -lD#x8`Zb!cm`Gir5(9gt^23UecD-#Pt^!tHQbr9!xU>LK!R7CpFQ)4w -gR`yS8P+74rl^(77R!;Z|I();@ZUU#9mRVtkg6y#$G50nf2B`f*4gyb6 -zZj21OV7c06A!;7X=0g_LhEC7&@Faj^VlU)R3ppBxtT9e0wAME)6TJ!ie%cZk_Mna=44GV!!kYy*4NeyR}mKT*IVYdVTM+6q -d!5CG)4BECNRh;UbJ=J-pLcDz7j|&2CS54+|GpvPR|w#>a~l|tv`k@**t959P>VgPn^@Dd|76AUCNps -*~xxH)u9kD1ZU%2g|E|P`8GATVk1)XOAv#sGI#|GN4&O>Lmx>kOu7LATquW~n>MkmHK|barsj92*2@x -ERd1kHIPMe26dSK1l|*_7Ego7VG3k~;WJ1E!r_(}ql@;P@(jQN&z`*tYy?n*s(t7hF<70fH<~v2#_pO -XnXeDEQJ_1`Z;8c^cCfL0qAWnJVtzPC~&p3*2(>XSn%c=)F4#ZmlU(UqS@b@2d611OjgK-AO8p^H>g4 -g78A>s8Kx9D76w>NWnE;9A`nCQ4xeDbS1XEP30>v3-*&IUHT+2ORCkut=e=9?jU&VjlgyDY -t*@UE-AM*gIO8*%XUBc~;!joK8#1_By8;F1(zCl4}lNI^jse_nHdcX(ZyH -_S4Y~p0_B#6-)EfzOqZ+jp959R~uG8(zoVp7r+l1+*+>jkOtN)@uqT=-;}_?+vUUrc>?O2(4rzIj<>0 -1O;xD@M{nr3d6nXstjkxgPrjRqmnT15OvPVL{^wP=IYZW+g-Nz#%A}-931iZSLY!weMTFO!BZ3Zh9Zl -QkG#_mP*5PfVx>~Ti`3MwZxrCn*=juL{gop9y;GngZs3@qq&YB7_+?-;QH-(U^)4$?`&~}QD$NFS#)b -K4ahaKWmaf8*!$yhw-;OUmD)?Wt= -T}1m<9eKl_rLZXt@&J@yt4&u_ZVwR@}e5dC^=xXcSW{L%%?#%73+!k<81b&D3i729GvJs$MoxN3B_DIB;=!vSw|1+Sy0~%gnvLF#<<~$v@)FOAL~FN1*SdPc3s;9*Jf -C56Kk%9iod$QlP56=KPG=6x$2(f<-o_HCHiOo -~`L!vj<{6f?^_?{!v`%y(SAsjxwv%a>c}t{r0?QT{y4Yi8UcoQyh3U&MfiK^jzBp4Z)`D#a`h0*4X4q -d?PwDdT?amihrg^QHlS;A;C#3_OJj51mOO*Q~kxt)W43(~vdT(Q!&T9H4&C#CbbARH(MDB24;DT?KA> -=%5WZyyEZoy#;dlO(&LL)qJYbJBoH?W?wCo^Lqx6+~BgZG_WnnB>f0cQ?rv%-u>laep1OjqQP71G;M; -UdUpOtHzqfxW0`Z)rfW(KA{>D#oJN5*9$2c#}u8H}6@wQgICoqtu!?klI8Y9O|;$@s+n-DuwB&Zc@NQ -L-Rvx$t}P!+vZSTatZf>|w6Zw1+)$w!BOx4@tK -1h#L%|WwDH#ek_V;DkPc?0lndy&-BQKC)ntP=@q=82Y{E%=z$)1q;_@137MrAj-fbRzyrDwhmu+9-0_ -qymooPZ_2f>D(96F=-F1Sr?m@eyq={PFtUg?L9W>c)1JECfsG4AsB+&Rwo -jeJhIj3t7;ps;q%B4_Uc18q`)yVcxR53H7fn4LhFTTYxV|)WsrHkI~Q}_)_pzC(I;%xyw5F -WW$Rt}yyF!6mCk)osO$6^^9BP0+x#l)g>}yS0W(6WRS>2(!DI_e0ig_CS0G*J;;2lm33eG(cO(;hpKh ->uw^`MXseVHtLvVLFO|D31zn!WWp*wJlTyzdo)5WIssx0Vm2=7a~I#-s2>g`{^$2`r8B225%yb{pd{F -kSYw`w%Q?eOhz%X)LotzHx5!1|xs1>?@p>by{9bE@RLsqC|Dn0-K05ib@}!xhd9ffK*?EZZoc_EhN=O?gD}=ZtKX%9h}1mXo>4 -*Hl)EL9=I|G=H3^Qq>a;ooSeKfjrQ3^R&c2a-MtN(W|hTvIhGCEoFjL7CN3{K&-lvrr5%F|wwUe}Bx_ -Jfw_a8Q`Wh;DvMVYpPIxO7+hQ^fRyP-BjC^ -xal{nti8T>w+w^8_*DKxB4*n$Z+_u|sk0u5m^n55J2o*X|38FQbFM&9Cs^#|K}`uO9wRzQW6VEtfH<^_KA^0P7&j_ -j(ycrs_@)-=QSqy;9?s^m+uo_(WaCpx-TbBpy`gix_YvN$ENO#*&M5j`eYJd>xPq&2ZVsbBOqbYFAO2 -B=}xBIh?-CvR2nm%Dw(Iy~C1jHJ69g=^`mCtmC7BI+@6UlmvtCBT&~oaz8wDu0Yg$r7ay$oKnt~3zM3 -5h4*&zLA9)sSgT7c@g^=9jtf)h6?>6-3tcTYvDG{Jj3L#F(5h68t^peFqRH1B@a@Y-RHr0~rmoeY8_g -s6;%1h|?nt7zE%QX}jz@6=_OhFP*BC4;~T7j*_s=~zpl>(O7!6S%y86@PmoAbZfeMJ8vN^1ukI1>4?BNnvESeAzxn?U!~YGrGl%!X -yO|!oe>l%~Vb$M*2ixht6YU4-?xDG>yb;j=E!zg=O`Dab+ExueRBKZ$@PBLVf9zF@fA-4Fl6SL%!%aN -5+Wu$odRI367w5qa3zXmojWde7MCti|${Hm|f4@cg{|V!qhC44F=B^tqZhlYW1?#lVIj5jR9;;SKF1j -$6ZvO|rebMGR@nv7Ns=pkcz5EVezx(Rh#l^|%m(Hcv -c6+MUtm7Q;)woN?Z&nYj^o7*HG%&}K`*<+}Kr4H2ku`qx|QRZ(`iGW+%Vv0Zhz2??BipCdL(vqp=?K?V -8AQLilBeRNmZ7k_3#c<*c+jb}Y74f4j`~qi|8Tr(qlG;if}`y3JHH*MeWvpuP -K(Fnklejx>QSvuXuoIubQ{uV0ljZh`z-Q6+AVCyEyozsiHW=m!;5M6og8Dc9eQ^%KQyQBbSZuIs}mA= -W>5sYcKVpHUt4`9Mhsr8JBtJ3ck4yq*kc!k{Kp - -Z`Bl*BJjsB?=nc8;=Ot#CdkU*ZLBtdqP6@-6x5k8%)@af{j-SOW+>ssHX=caZ*Xs_6My*d|Y -R9`DMi?69bOeB|R|v(q^wwxk1K;0C?=o?GM)j_(wXtvE?uno?XYPyWE=jrBk&`Rh*p`+YvE5nTYwoui -sO(gsvf~qbOoVRjb%@PYy6$!pt{CPH;p*h@hH<5g-`^is4zCf}AN{MR-`P!a49|g+BENkSeH=K#ptp3 -nK?(k8d*#H>(n1ViG_1*z&Ojwpi{x#^fjDW)F@GNWRbA(1Ni^LsmNrcogs-N_Kv?(L;M}X*T>vfqJb; -#qn)2{<^vO?#jgpc8-8A6wyd@x>%fFN`04dtKDVLk#UeF;bHo6&pDYs -|P}B#iOlIB;v<Ln2bHL7AqzyPdv6^pa!p@pTXc9cr1dPp|G!X60|XQR000O8B@~!Wyd7{y`Y!+gRmT7T9{>O -VaA|NaUv_0~WN&gWWNCABY-wUIWMOn+VqtS-E^vA6eQR^u$hF{i{R+e`7f6Mo=be;xmA#6snd`cKt?k -LoEscwU$fkr52yg&U5|h3Eeb1xc50LUqvPtblC1VljKHYu#^m+B^(~q;G!=r58EXw-oINS8gqd(%CgU -1JtvuDkE+m=_?eKvYF&c68U^DmCzKYz$RT#M}Ow!d!b?0vKB?}}Dr-!_|iQS@a~Pod^}A+nb*o}ImZe ->Uy!`>d!J*|RsVKfHVK=kGtfdG{V)Lb-!Sj~=}}dz*dvhi|fSwXVcU)I9@%F9!$9wpnGf*>clwS}~hp -nWpWtOB{?c(O;_ -<-By45Yq{pXWM`+UkI%~v+d8|Si!}~tlC`31Htk$=vgEv30Z>)`ecwXkFW$(Hx1#N!mil@n`dK%>2K- -F2@9S=}#$X{ow%<-NE>jmPF#{Tz$zey^Z$+_~iF<_IO -|rWdCg1#g_Wm#L;Jaw)&mZt(J3cr#n9Yi+0?Ikb&U2iSw!GZnbmWsP|1(g_i@NKfgjl#A?e_CTnJhoM -x_VJBn}MqKr?)^s`A6sz5}IhCOOoQIFuO15-Dt+Bm_gUieti4p-G}!Y#ZW$9Zx;aYNRE2?a|aC`JbUx -%)#>Z!(UL%8E5NHNdhq1T>PBNDTE{O -pyx$-z&SUZrT;l$2Mzfku94x6VSiRpZaMOIJP^*5dcE|_FwL697E*wm8gO1!Rm)^-vaj&2YNX3N)+wT -Se{{ltiP5xoB+hWM9eS>a46_F50e^!6C -|O0lAn$0V-&I?!BvY~_;5GV<#N_;YPWb?nf-etoWMe1w7FkFXq}3$P7iEjCp@qAE86Sm61`@f1i|)C+ -n!)@#A1p!lkIC)OZYfV;vP!0F7IrAzcovdF)HK%oqcY}9Yp<(#M%q#5;nJRM#h6RLUg8mN2%Fz3|0Z5@RK#{TGxGSp)BtdgS -tOMCt3uA0+iG`FTrpKLU7|)#mVDK;KX27mc9imB>I#!b6)gW+gT^g4>NR2egpu!I`HE`@8l)!N(N&sr -0fc5LZE6B|LL`jBm?6ZV2T%;~8E8;wn-mq>3-;AO)%H>i;(M8C{1%clkcs{q4lNV{(g3?8yOtRae+Sn -^8-hV2^(ELwIu^NRPokW@)d!mEC?i3&$vD^~$=@yo*&wB$&!BFE8!OWljZsBQLifS=)sC=k%|B1zIEd -W;2Q&?0}06(HPNlsydd7S+qvRVK_fm1;5<~O)T?nHhopwj{-w*gww>U>sIsG0&NzZCNV_yDXp3lYGE5XeuDhdngZlFy!MkqZduj~QdaeNr~GOLm1kIfM8dW0*bI#6dBa=6jX@ru7nw}HSKY{5in}I5_rH2NrzQvu#xt+1S8KwjmgN9vM)bIVc--gdsmMGaT -^XUjs$!(042?owLks2DQlk7N!Uy{&FW#(%Oee^m2_Q{#Z*1rUwkfs;oL+^2vEh+bv0#0OCaf3ZW3U~f -QtHR#66l8-3(Z#C|0BK7j}wevqg=$ywLQUQ;wGh6<5<0akxUD``e*x2#~M{6zin073>9d_saMs3NLh6{GSJS;V6pmbbF^NO1$bI83}D@?Hx=3)+B78M)-YH-?yrj;MJxb;QO9)w=$BGr$s( -}y8Ewoug;^7gPp}7pw4M+O(1&gnMkj%KQdtgL3+||@6q#}WUUtg%AOmZXMg3a|aK|s1?bZq5hO5S_=KGSC7T=^Wowi+DS?heH!s#u28aPzX*m^BfV#~tF7l)N;sP{R06Nz&Td}T+ -xkYEh?xFAu3$q#tD}vV+da{7D;H&Y`K`HFC{A?+f*m-gy;U^NoWa8s5<;Jczt4rWAW|EKfh^fXgDvD; -*=%7IdRJ|zLh3mT^_?KHb9Q4=6$c!FDM!Nx)(4YcVAA!cah5>F?qAlk(dI%HHAV8}^txG==v6hwJZ`V -Thza#&BY{kp)7f@D|V0b>IVh(&|$Z=4ud!g3mMu!*q_f*c9jtst0V-9XV@3v{?6k` -y{uU>BF`A!NbBHF3-fQFnUGgk}2ZV^U+vM}J&SHK8nagqVq8@?rtc -KEbO~7|$8N?-Ea~_n%C%u3w?WCsHpPYZ -KXt}qjf)=U7D#AiuT+U!-D4=SqYu#SfH3y!Ov*tONKW=&{6<2NH5JEi8%i*S=;`5Y;`4hQII|4%6nv|+13swUOqjH{?xF)56uCijM -<1P4r(b3(VJ_6}-CGA;u8016VS@T5WWZYKUDpwO?kXK#%YlWUV^0 -`Uq5rmTw;|~pc=ndcRK6bNPr7XMq#NhilWgSLO0NAVb*x58Rc~oIusVb9athDdvf -7R8BH{dT~WPfgt$m>(Z*p~VlhL$}DR@mYR?V7TKyFUJ#M -%13GLg9vz)-O|1FDM8^kzR3fwXx3k00$}J<(6RYu-i-{x6z29R16&kg=Abja5zj*Adw5niWzFzorjxQ -_lh*mN-A|%Y)SE3ZMvSt5^ZYh4HNiDM3Hwy#PUzvT9;b4h(1p#MV~15|>5t1z36@G<0*hY=jF1x -be5+%}>~Xyp6u^d_Tk>&ZJ2!Q6N5Ni~f{C?YfJX>0vjC3JcS>Mz=DB-WvFr^VC?^#@-r -a0X(~a!R7W>d*u|xMaiL=>!4%b-WylkO1__i5?1KK)(zAEhvXiv7J$+o)^;#xfra$QS6BNOi21OEWtZ;<8w$q&}& -t~kP;&dG}eJ}o@N;T2*7k?m$BE^Rk7{No(n34+H$K=CxAJ-1_qLda#B8T+b+{&K5-~^oEnPK>4%392} --qswq+Z}m4Li!VD98*z_9`Rh{iusoqZ&&-o&`qm8Dzi3D?$%CRmJ6-wQXY2D*?`#4t7DMN(E2M8Sc+U -7%mmXD`suIy#}0M|d?uTh2oEYT=JYe{!w4?_{jE4VGoQ=96F~1Djt=`vyc7tY5pEztGBsevvJ?T3DS4 -7gG-frok8p$RuMSr$1nWPa*oIp<-3TZ -q{in9!)J-?B5VxP&f?d^)#AW-pvrC&Lk_sIuwzx{&_-hF$8!a~#C|^}KCKFXd4fGfI#;S`|2(?TCWb- -L<#ZhTsNFo0+x~l5KgzN&=B~ziP@2o~wFdiBGDA!bW#BLGnPsK4VO=riD(sI9JJ$wzy7CUPDYlR2XPv -c^-QH;nnJTekB?a5JkX=((XL(6OE`U^2GQ#We>Nbg|c44m(I`|-ygrA$6rH4DL3VokxAqF5wdwUTXuN -Ut&eEuS}=ilcX^nv$|lT0r-vQ)iIK#u$#&a66bBw8BXxd5;=QcK)RMi293H-YlqaGfr2|17-1l+8%~Xsz*<{KMi -R}6A94{_nU#{psY%=Y*QRt2+gq_{jp@twBu6(LmG|W&18nmzwl0g*45}7y3h;Uxo -BT}BdR7&vC*lBY-vfqyHd~0h+(r|5<_~rFp^fW@x{*%n9bP1 -9GP5s<=Pxa#9jlX3Nc-CQlrOFFOFT|f>Yk>k79M%r(R1+C(g%nbaY7316N@~Da_dCy06<|Kl8JGf7+0 -d>#4p0Y6Kh;*2b`TFB4uB54`7nG1Dc970IZ3l**Em;ZrP0~Ap;AS4)wCdwFr0U3dZ+eW!c86HM -tdWpXNs=;Vku^q-+U!%O -s6yA660x$#JnC|0_p*dpl^B6&C4(Y?7?rQ*$`SfJHus$i)v664S^*?Qmuf -2*J{ukqxN$FWBJcC8GtnI&Zq*_$+xk4il_gpsIm-L9cf3c$tHefU#&3WQec|JL3H0djSYaO)>Iu1Z9? -+isUrHV?oqzkJ?S0u^*#@b%W~{*K|@T%Oy`tDrVj}H_q%3(azYX(=0X>pA!A(e$hvSAADAL$2nn+-Y} -71qg?glbSS4(SdoVLnHZ=?B -b8J4+3*46+pB{G~geug{ZR6reA!iB(m>n83=RoTTh(Z-PH$~rQ_GPpR1?a+Q^_1d=Da4i?W*Sb&1*HMyfaRoo7sXzjooU2L7fr-pNn -@X;LZdfqqdStV{og_-1XXVt6X;vSIEjnMC(cb!#2}a^o1zMV$#x~b;-kMeV#|Z05VZkr8BpySBd_0lI6LNThtsz&ILhT3rL0!AC?B-7#4WZDvazYq51Tk|V!T9G(2ZJ -m$e0}N8D$;xWA)qQk~9Otri`*PQO+mT_70nqbmU&N&R(L~{td>Zwopt1w9iNW;DIfa-P)x}<@*m*Fr} -*~seO^2Cb2xh#R9#S+Gf2+b$pQ7sd+j%@g_4lHR|M#Y0)5(oDcsYw#M}kp)%bj%^8?hO-0W&$HqHg;lTj}XnTRE8ATI~g~-x=QAu}rj?MJf(j?Hjo66^R-<^CE=~>}8sfnXn;`uLh6 -rrZ5>GA}FmHe@H^t=7!q`X`!ifv~jT(TE{Yy4~vZQIqnl6>N&ref#@D4WTNoZTVe%{Z%VN?PBuTX;ZZ -7HdL8CsP+n)6nrPMqW9R{@+0e^mMJGGldOOENHt=tgOz-wMTQp=nt~WiKoVeI|A!;YZ(U*VtCR&@qWj -L+Ii85I8kckbX`;v8@5K~hFgPFq*k;I`6ZrMr4{yy!eTc`Q8+gn8L-48BUv-ENwJ6IRc4Ic9A1aaeO(Fe2# -<8vto`2l_@DOUL0(rMV-q~bq#Jr5Jb)TCn21iye%dg=usviVEHFP9#7OcX3Ec@0r9d``Q?f^#m3Uhxb -!k;as8+J%c@V5D2j!L3ry=QQ60@&ClB%Ir5tiGKb%VDPFk)W;GU7zUZ>OsO1xJVVY<#fLsgvap0z7KW -rQQ=2-k^rm=0}&~8j1ilrouN3(bsvH|_>WG_tNlb%K5ZtmK(+6V=UfwaRp-Cct -OSZE*ljw6w-qlxS4T@*Ygj%Rmn#FnGuB(ulkjybCj)-jWOy#di$ULqG?gV8AeM3>T< -fiIi0}7G?P~B6lI2JS}>{BO-4b!Wl$ -#nB9%Rqx9KAVhv8Q_8}>%{CBQ7< -%j}u!8A?JNk$bsbtqq^M|NrC6BUt9rpn#~1$S!nk{7{M(oHkUA0r -_ZOj&9C&?b_k}>^$V+fT=_z{3kBN0dI)CB0tF+qE?%wi{rAwMF7(?(sC=yrGT=ek|nE1KzTgD+RE;Tr -hTO~beM3tSbj6?rdn4RF)O3QXvz5jUN``{KTG=2K!+Jw%XyNGd^6q!uZzl7>dvykcW}D9JvrTyP32Mk -Q!SGhQy)T2#sVKEdUf{!%%Zo3rz{JZ;lSJ^7>n6V=jJ0AoF~=@Bc%G{w0GQo@+U2aMR7oJp)M1c`g6B -C7BylN?iG+44(@x3l0(i02&HXDf?+~aqxL6FiybNF`T9e1G&(V1{6nX1c_U5-{E9qSgTc_2Pr|SnN~2 -*;s48c7bcc-cWb)=%frl%e<`)-7GB>=C({AsT4W7YDhHTRfX|6u@Khj`;iOW#%vJ2|j2^KSQYNY|`J; -QV?ITB(TK;ayivWbVY5EsrFGCr`w$aGT&3?FTH%pNRrQZyK|JQI(XR7ro_gx!Fcm&ae~Oq4QSDEre%_ -L+aap3-djjr=dCkbhai#EDE|B}z7Ne)Pq0z}x-$WWKA_Ia -tq8O?^zZqu-#@*VgtonGhYPY$xubJGnYwB8?_Ne#wavFD~7kRkP1}ifQ@Fl@R>ecH5FPeDf(Eq -c?Rq8^M#eX7Tt#&N37t`iXT1M%j4R!6ELS?TvVP__Fkl{std%4J#~>PQuVQ&;mo@JN+5gLp4$qK=@KO -Dk-L@Ow>)wu*ZxOn)XWc*^r{xkd;aHF0tA_CLNm!h1bnI7&;R%?_m(MYBCpJ_edhx^tC}go8ndsR95~ -n0ONS2JW!Jf)g_9B0=#@P%9T+@_<(4L>74*Vc~|ku&W(37cWQvdGfA -+o5DmN-B5;ERc0m8FKGKsYV=MZ5Z9xLwGQ=&aOQ*W-XJ_1&`*v!Qu2F>e~=Y09a^RwWi+%MrtB)ilej -!yclT%2^qH|pG!-?bRd&tuu-380ru>-$=5{t}Sm3`QU1CCs{cL3#W4yZI0S#rKioN!R1K&Einh?XCNb -?lmO|n>SZ=*{SrpRvSE|f&LEN{o2Z8iTm}0%DTB8DPGbdHr!jo5j$Tgh`NmCZ~Aj|tiH<5g -mgN%~}J;bp+XOT%^Poyd8v*mM^TQo+y7pY?^zt~# -+zT;%K3K8?fd!D6C$*a9!6>X_WDaXgoa*e%&yj7cZrwq*9U7jO2-)|Q;RzEVmE{3V66e-{b|La3lSd6 -jAq$`Og!i*zb+l=bX8)<7f6iqeF}3?w|dsx;R`J^xHGRzA@H_qcjj&19@?o1!9B4Q7Lyse`*#t4fX!t -}B-wq7374_U(%wU!8$=UN0(j7i_2U5B;@VtNj~fT{@va00s*}Z{VL255V_R{g-GHA&m0;Hq?4gKGYX) -;{GNC%~3}_br|L!r@(Y1|_1fA;W4mu#vdTphEcqCyD8Cge)XC%l(D|bG -eh@9EMM!bd`-as)v5$?curG6rdNG>_aP=~SoNlOQxfhGr-OlpxAg9r5SNwTLMoGFrtFyZ8+j*$md)Y6 -%uCkLGs_+zrC?sL^BFQ;dwQD7`%IBx8rMHD2!rt)S?kLmJHjwNZgbyi6q{QvAdzxSa -j2hzbmJvO0hsjo)_sSi$w_21cAviyW_yjy4L|1sf8`IM_I!KXTVtMNJt2#nEWel1bKNIWLe+;r~Lw!U -uH)Zhlq{)PnyHwzoVl6|BH%_N8|?qBbq3VXmWO@RoC%d*CptH^m#I-)8@QS05x%r}w -^jHlP)UZ=uy2V~;QqY+TJ@!S_Zmw-R1{K0R#>~tiiwVofc6eAY8eW2zc5tX+4Fd_B#aUbGi8^f4bmNe -89hR9u(%2@Jf`t%XsM9Y_Dk;_e9Rdgx(J5JQ=AOA1npVgp<=z{Tie=ZbH(v8OE8l6!GpAN2D&1nUNxhgNbUimP_DXh;D2*!IAqMJ3 -u%*VXQGKzguz6Iu?z;d{yGk=!w-xuYlR^^Ed0P?^T}P=y`m3H0GGl51;8GvUlc!eae=}Q)67zjbt+fc --2s~G+oJ9;{+D{r8C75)l^ZrR&E9Cb8w?Huqwh{?M&vC`q2IaPU5}MQtIROvMST$wxL+j3KU}lwZpyX -Xf&Ld@^Kz$%;^3!G69Mqnu-uym{AZALNe+a7(#`x -%p`%f7PQ;R!d^fL~LIf+N{95mI!EfyJ{xnfn{mHAy;tgqv@tiDqrv+nJh*%emNT8VS>g5<}P%H34p;6 -M0$U9R~om`OWWpru_belF%%bcSLLxE`75t76UU%Dvn{C$mDH2DX8m&Z@W+71{+d%_959p#XiCInT>Z1 -`2BThB8aV;)?8C0;58QE-vUN0Hw=SgR7QQhVPbKTHOXD3MLa*@k8Tjc3O4Kgm}>k;~vCvS-O&A;vQv{886HTFh|jKO-t7q+@R~Xn@sS=z;z1})`|D1h5oN7wsL!N=HH}w}6vLX)AB%PG~(T8L-Eez3tvqMZZ?r{2nml8 -PpRz~bG-$Uz>uA+U5u~>0TekN&`X1z0JoIcJ>M&z?e#b}34p|91sCS2au5Qe$Q-k!?z2Zwf%Llw0~OV -w7A_!0%H=&_FJ#B1h*5R%XBj(?2g@OJ2bpzvdR24V%B`M~uf= -iz>mZ6<T$+J%Q6$&f;XO@@`)+&arSj774yl#HVDj!iWyZ25a{It1PZM`!`_Ix5Lew#^6DXoCli|d; -dlT0EM{SX*`5oK48!NEAN_4y8+%jJ)_~r(>9sCmaWLr#kZ)?N%D!4tQ&8-*WA1k4HWM4UkSO`L14^}pb4jWO;rpNS4z#lpG)A -W~m>YkXil`_2qBHY|t2aD3&YHT&YO1)DOZ88RKb^}B6LBd0%sVf9W5(%`%oeDfa9Z-d@^6Ze&mHnQQc -F;(@BjTyO&719+FB!JI@igd%cs;|ys|5WGEeu?{@E?tId=bs;c3@<3rXD7LWMF)K|TQrKU;D2ad -B4)u9t!c031TArwAY{t8B76SkgPMEIAjkrcI)>J*$hmX4v{?oEUxLQS5re_N0KoWTXd4MVb&gi1A(2j -3uu7+u%swLxKBmvHt_e-A-5aNfIV_LSg^umZa6sq>_Wt7Hs48y+y7}9}ZJ#R!V?o}6-j@V(v5Q)o1wQ -J?>!U@<=yWvazx`A{a?igo*}@X)66y6QV{fsOgGGFX^QLO}wDE$91=CFYw5+pXvT@LbNYXfp1esYUzZ3oX -FXxl@+Tw9*=>1hW$Bag>q=)t=--;GJxwzA4YGqhUZK6xVNma12Zr`cw7<{+bsX_EGa)kRiBv+47}+u1Qa~yk-)8^Xckb2BLKqZ2pW4puHjZtCua}3={^2*ypT#{-`vE`e%i+2GWK#km&3ly!S;KTw?f8ZO|E3u?S& -oeP9$?4PM`JFM-x<>`-2~REH#qWe6o!-ZtbR-30qFoYwMS6zuv1a*v1jboAX1+DSt9fb!0qaBAWqR=5 -DHO*dcm>)GZ(lJ*^TuXc@SHo5_zR_=n@e)3KW_8UEkdd9=~9U;!HG>#m~3k_-s9pPSgG$KcXmt07gzg -T4gVAM5m(9f6lPDoPAj$tIJOn=k{R`vDt0F3Qkr17acIAylwA=wwx&cjdSl*fYiE(#u>(s&Wz|^6P|( -kv{{TlxG)3z}jbf9ZpZg|LFIb5j+>HS5N>4=xjMt3ra-8vx5&y%J#i%?9wiU*%^A(vmc~j7tOKD|rSs -{)n{BER!2Z;`(BVX;Z$iTD9O<650BZT(zON;^qBE4gV<(%L3)E4_h4-5Ig<;ohaL+oo+dn#dj$}KN)b -BL59mxI%cUNnTuXvmY>=*}abnaKa=6SoD4AqSYMXS*P{lI42vP^`DGYLLaaob*;mw+nYxm^Q1xAP#k? -cJ_Z5S#(gtyo^gIEJ(8&R9}eOSr}`~`PF~1&w+VSw6QoWj%lN-1Bh^^@&$ThqQ`Q+uk}%}zcA7g@%5Z -vfQ}aTM|`p6QYl1!W03jg46GPhY#0R2*$=x4h&egp{&Mt%lHW05Z_jEQ_^vm2-puvGo4cYYVMcrjTG)wP6~87Rij)x#FINE* -)3-%sV?&kp!ZgQX>}!P3S~+OWtQ;d%TVq78Q^ -WL8wZFhxo$C2?iWAf>7L%r|jhB-CM$bEvansyjOmjO0=m#8F4gcO~t8{Y$Hz7@xx4T}s`ru^eKrZx#) -z6+|q-?j2QEs7D2n(o}TU#3O;TV4OL($wnMPv$9Lz(Fk}mT{m`M4|GSnvRt9kZRT|HHrAVufme-6tLY -JuXdP->>A6tYpoe<&*%(JBEk>Ga-ZB*H2?Y`Kd^rv(1ke*smx|lb7(ds@zzBV}%i3B&QYbF|0Sibqh1 -=@?y?%b9^@F+%T3bvW62Uu`PHbo8H(WfnhJcDgeEqCtTt~c1;R>Sia)JQr@Cw4h8bRt9v~38|MvPb@^LGU+Bxr0%;8O=;w^^+)R^=q)1_^Dse2YQN -Z}IBN>!wl(71bxk;A&wXBGkQ>-*nm=_(#36^3_c!Jc4cm4Z*nhWX>)XJX<{#FZe(S6E^vA6JZq2Kwzc2)SMb_3q)B -GwN7|rw+r5D6Y?=ie$5^lXfNRWXB+d-88cCO;c0Il5fA4va67`y$*v3T~L@>M>i99?!?{i4%p*a2GR3 -t^J^39p3&HD8B_~hi_$wTp?DEC@zwnl_6X5!hCr(cP`s65__H}Q|EjG)C!mB_r3sW62wTPa@e&9=zJn -__M5VlBnDMV+UyQSc0Iq!uv@l^d;AwJG4&MoYPqxj|TOq!ceNUtGL;a}k*jM#On4Ui|Rt`s(seZ?Av2 -dV>$4-HFvNYn79$si&Xs=UR?{@peRM!(~6S=3~sLH?uE(jn{$i7aBT^SqKDE#jF`t_gUN+r*0v;7 -Wel1V)mSMq;rS(&-O*RSKMx+`@0{YK9Tm%&x*tc-Of=VDc>EL{|(%n?iloX9r|voB>ev*RPR+~`T1Y- -PFtij`8E{apN0m5WqC&o@%vO8r_FAND6FNPFPzxnn}Kk>({m3m1z#-pR#cc5?E~#kbGjzPw&szWV0kZ -_qf{8dFwhj~|!&Qbi?6PE_a({SKPuR~OG;evi%h@kx@wbj9m9xrsOO5`^kQSgd}OiJ37Y9z1wJf3LTy -65LqJQp*a&tOEHfxu;OrRWP;HDwE<)nJozL>&v+($^^;|CTO&%8HH|EB;k4w7_NfT=DOZqRtxxNzLP* -OV)TMWTtJ6cX?*qf#cu+Jkjk}Kz)NKoi?EW}dXD@$e^untPRXAZUYF2T#hI!kC<#oYGI5^B=%i_I2Hc -K!2<8mvAjR9Om!eqHvkk{2wM&%>{_Q23cI|c*8@HY=>_67K~ckK8OqT!W-%be|Zohg -UUrn;uy@WSOy;H@zOC;yRWigpz>Q_M2aF-R0?d`{!Bco0`W);(V-`$Ro2Cpbs1)WobEQNs%!!3g`630 -uqiiF&llSQX4QL1vW;3F7FCqW+q}*)nCe|H6LBRfmEUxy`%t`v>EbA>Jql=CX9j2hGNM6HK^FL14@A@M6q&n?+O;ash9--3^{->`wYOVvC#cXCotjGv`4_%w5ZKFPI|&zPG8VhYE6A-qkTc-5SerFWE@ih(sFQI#qe5zg4_(=TJQ728_)CfFQl?(S-=)A&~ -)kq)NQu|sUuC9FfD(UhAaWvEc67Gu$_oaDmPNc2{E)Wm!hM^Rcjm0v^2HO&j_FeGj2K9dAcFN1w!cHc -}WTvlqXz+_K1M%fjC{gc){Z}T~eU756iPQjAplAn9f=nj@YULB)o#w)`I|X;shnKOC!>`x&+1C6-ID%^Lmfayf@zPTk;F18%v5$z;3blEN$Kp)0?1Y&}-|a1sz(q(1=$R-^$~~>kYI%EMAfLdr^yBU15+6jVoZ1FB`(w3t -vopIatXxY9ZQC)OLfSwv!MNrCyxzd=smD?nwYNs%<(jBCyj@gEiP%rVyn;Xj6HN{oGQvsDKsH2~bCD= -3U9~cr--t0E=E=WAu|lx_9YK>{&^fBai0vo&$}zwRO@38|YggSQ)1xyz43eZ~`L$3UoTsZeJW1-ZbD4 -wFU%<)`|qn6a5kuq?eC)gkQAb+@kq+IDRX4QJjLRTX#IjBG -k32i)S`?)pb8L8)NATY9-MP0ErNeqo{RSLS|Kn~+~_vTDI7iW~poGrh%&z4Qxk7z>n0ANAhG8qGJVu9 -3{4sMW}HX;&vy;}jo=-!>YHE(tC&2zj<$pR3*lL`z|=ca0dfNOhG9o-v?ggKzKRJBlDz4<-$G<(?}V)15D=P- -FrXWu=KFKATdpwKKAc43PddP$tY07b71&3enfBEh`&`b0}nw5A&g$tQ$~=B9zxf7ZpF)0MZlqeg8^2P -9K>GWjsa!?PXOv#>Y+m5@GMzbu~Fcg=!r{h87&c#2f_huIRdmV+s~ptFmBG^Y*!eZkVeF8cy*_;Oc?M -L=n3~&KTzeQN`M>AWn$WT7*MFJjROyO`m-Hi#PqZi1kFMyaS2%)$CHE_6VDXmx^TBunH1N$?%DSW<%5 -u*#2bNx-2_J2g1|7R7-k{_4bn&yHSiHwGYn>tM#E9|!19zLo<&%7Uvk|D0Iljx7(A+6>Ks}zn|TJDkx -#OsLd+ZSL|fNOItHuc903CgrZZH&O090eq$1r!qj;7C -gOMF^+o_X?gVO=uKyg0^aI&QuF+`RXA%dNzk)SbEF1TEh*Z_F6yfqEPxzYfWC|5)A== -&;o44oP+R_+=Zco4|k{=9e{5u#LI{}9Q57+#Cr$*P2N5lnLH^iq0brqXr%X4d`*UtG#;EiCAK(5Lk?Q -T-M_Y=o8yf3N<#WDJ^Php2O1r8oe%6vi{%-ren;&wZ?*Xf1QvTE?W%_}GJdgT$(K`gKI7IHj_Cg565sl@0{}~|EeOC5hP} -!W6;fpVLG`}|SS4f#4HaF-VNKES-y$}Q`VhfoO$W0RCt2-%n@gCAvvhuy+Mv_VJUtCfh)L6ahX=~G-0 -+JkIhXzoR=A3IE9Ib;K0Gj8a;|r)?#J8^e?O6o#6yC+!&wI?XqroFTEqJ({TZ;PNpAEnt$yx%c^5{-O -5)-yPW}~e-p&`Gxi&bY=CO?UhIP{;9n8%u+@a$_yL@ESc~o%u2DzNfyn -;Ep*nclB1n^&b_|bB#D5J|=cdY^2)51bVjxe-Hd*5Gn+T4y$b2 ->-!m)rkZTig%rfuOxD!nK;+PRCO?&9G$qX-KuDokLxT;Y!em_wQQ -@u{6iBgs4yCl4?gyD5#4;6fV9Ed0fV(*xu*MI^q3!AXbvdxbEMVc0g$xGU-`Jpc_v2&P18hOv5q+bQn -Q@<0|qeX#{n1e$#nJsX+iFW3qGJk|&tEk3r8B73 -_+TwT{zaE9ewTSS1w2HB6uH)SkP*bPcud<(9Rs#m8BjDpLc8;8rRl0)eIn4=AuKVz^l3^dyPPVF#(Uh -#IPbr(%+!I{Xp;C*TS+EMzuMXENu!eY0aM({9fdd;G({J>3oE^Xsyzx#UFm7m|&JB{j+grb*byOp3b5 -fB8n%e_KU0BkcwQqUH%$~J%6Z{+MgymYsHF#EQDbKh&zk_Q^KKl*9EPYlfd{oh{h-x2^(!ZO3VFhElV -dKw%m$<13P&ffPlf~2j>@m4zA`)TPQWeDX_3^pm%H7;;zzFV7IzLG4ktSxeumNH0KY@4%CB=BSdB~GbM_fK$Hb| -?sE~4my#o%iaP$zC!E}8UOv%^hdqeS>ma^?!6NgWOakp?>8%DwOk9J)q_?trugEAKBN*d}0ES}F$j1{ -(QCgcyVo^gJrKU|;Cu+Y2=KMcb8IH2rAjKifci2%w=P77}2j+8+i2$1JrL+<7e^1$MDcYt;C`#Biq`Z -!`d-6b8-*H8^_)e$-3wJRG`S>%*3i5&jf2h~*UCAkte%X?($=4Mq^R_UEBk=hGLY8L2}{(nAan6d2oe -lf#Ru?bU!EnnIrppKDAGPX+I(>J9iyUx99U{p%X^(QDAXwsx1-5sLfS)?!;LVl7BB^k=Xk$5@cR(6DG -hni?~jj$aMNFWO+}TA~JZ*B0);V%Wn+fCQ%X;#-KTD({30y5lVlF0Bk#BdmgN -vCu&NYYTzy`nNlrWekghKD<$5p4({GSAA -w8UaGn;$H${PBMCkc{+_K!A(jNO>`BxbKTQZ1Z6lgnB)N-Se*rdc3Ju!6V>=%nT1#0ju~ru>mos|`g+ ->K`VPW-o*PBtR%66k8oe8N(H^xM_C$VFc>k09scd=4D8Xoey;Tc)Hx&hg%!U1YH;<<}q?HaeJRL;kSw -o)lVjHBnwliA1tRmak%?8`St)X6Pp yuOidDe8T$9oR+Hzp+pJfQ>`wig-n`wS1jF_BG9hQz1sV@d -AW~8RP$vaaVK={dY>vFHf0b@)6CTb|#@gU`FRFsA$Q|2Ewnnle9uFPCe4_x;1zk4YM)$UC+d7=!&X#S -{b=g0{9H-zj)&-u%vwMvA>k|;_gWbww{AK~z#AnfmUPnOrPV(U_oH00GqxX;3Do6LyEyu3Q(PJF;`wS -r-XX^51^bGTswpG!#`gIv0(&5k`tg<4xsnkC)8T$Rx|9&=7rlxnNPe;mVF~&nr#$o35ZuBMSzyn)XIR -*A_%=Q5)wncXQ@Ug`~VZ3A*?}t;v5U8~}TlAQ|s4aCXg- -6%)YYWjfp_}Oiq1u){va{=V!p~i;+VYa*84m4_PV}Hr71Ie~<3rYvzHL?Ks4MoU~cpsa1l0TJIR<%Y} -UVspTjeQSofac7~rr;RmZd+v1?kfOP+2HuDgoN{dK-Br9CGz5r^zf*7dw^VE4PgxSB}coKNdv(JqJt- -n#G@3Czo1cjl+w=QY8a3|5+UMYiQ*e$o&g!5SU3j+hLs`+3cDhIZsVe0Pv4%maxk{s*a3mtqqy6U9aE -XeqbAtgsfuu;$N*3DG=arY=^T;u5gvmFBHF`1jZ<|-|5!l%%Mp=hH#@>-x-{69ah%6&GNO*n=Y$A;O> -OLyv^TpS_%y{f>ob<t4?A$n+JYtypDl4> -pPd0MKgHC;M;&~dE#w{iCx14}Fb6bVOyaRcJHLTcc=>fpc`UOZ_c -h+Tu-20nni73pItWTVK2y;JCegZ-M(BL#ppRj}b>Va+C`Pg1p=BNeEnwP)N_<@IPnczqGJ|N}2FYkF+8?} -@8RjaR}C*JXvey-^%yAE7rC)Jw{Znq7<3wafi(FnbP69yEd2Uv*sH&9Ch1QY-O00;mj6qrs-lC)GsGX -MZ|$^ZZ#0001RX>c!Jc4cm4Z*nhWX>)XJX<{#IZ)0I}Z*p@kaCz;0Yj@j5vfy|93Pc(&z=VQr<*_rG$ -hjWJnfRW>@!HPZ?2#1;BtQxy5?}yOGBeJ9-+FbUyU_qCJF_=?&f+8%33gY%s_Wg=N5S#O$HA;j^I~xt -Y?}G;pYhGX(ZNyhq+D;Sd~wwT(UVbd^5EggG5qs+@Q=Jmw!y39pZPk5V&CPntf;dzXv(0u%7Pc$=Bg} -$SLM99O{y&TrrZ>1(&X?CK4evpKzl_~<zB -{|_T%dxUcSPYQ10L$U#-ik2^PQR>w|e!u7bP_BP?Rvho2)V`r<>JkJJBG5q&DD5n -O~Rr^ngbbt9oDObpx-TQj0K!^7E`*L2p%|Xj3i!S?0w!fDh}Wsx$huDca8`IJhEN!8i5k1YVD+kDce2 -O}2rOzii3|s~1^4OV;qIhE*#TlV-cl>TxjLupnG7w%%EF!GbW%k>*L_Zm*3H+k<1I=SuzFS=JXMw!PJedZ!cgL#=$DN&L%0mminu#Nf3sr|Gj -)4>rc;$n`D`%)T&ehcuTz$vt)LarIU39^HMcioNicy$*js!!0|j;!oHf3I68Rp?ce)`ShAHRD&dG`G4r$0 -gE@TzIn_38cl>+L#^*F+HGvRcsh@ZccL<}{CfTe;Q?=eW>XabR0_;W0a -XgFi}JPzrdyzyK@N2GZA_I;0SA&5l7Q3THBe{RF+n*eLT#3+2=;04I9LK`EP>51%x+_`)vtVx;9-#i? -DVNdM0|M`SaU_2X{9r^yH4Vmx`zY08?B`^i%&wCKOc@uS17}(;jVfF= -3TMl*&cYECJD9<)s{@DnL>`Ld%>LtmYXX85Bl!4E~2SNUud2hU*5g6~Sk&fp4oi?E<0nYlV32=| -le47OwgP3Frwm}awNgA}s~>SS9}XLzRvVJP)sX{Y641EdRfH(=nb+2A}=ovU)QOap{~H7h34VJ$Tg-E -cH5)R>sOOjKUin=Ckb_~{pa`GThjw+8N>Nrp6K^67(#EQoXlBr&wdp!+YfO9bnSd|)!)08xaO(P$jFpauL@QYTGQMKW}l!zx=ZlUX -+5H|})eBV}W|WCXM3ZzfyTSuYftZ0I{zbP1G)mc;v_v|scYeAFg5pt@E=4-r4i7oWN>{-^umL{UiE9A -y=iQxrQo#E|F}mqg57a_KrahOG`P9qCBY@axRtAPzaK%S{E#0UQ>jBK#RT6oqkR?-Lg4+bR8?cF0(#)=6;<`Z95}-N)3nWOOdH@RdAQuDLk}Q(Es2d`c)J7{Z1-E%~#q@Pk6sR& -VAf}>3`a2B=C6uqYE!ZL;{%{{B1RIE6q83dNd1yQ-W*I_A+R<)+QUW1eZ*NdYPo_EiQ_ZeMGVbnx37p -ZC&_Sl|AWtky5Ts#H_Jnuw;1gT~Tqm^%$QmY-ya3U7B0Grb%ULM%Yi@jnVE8gw=mJ$zKvUCtQn&yMJuRyypTrt37SGI0h(N|vspgRv$_u&0 -+?-pp+WmEp1wGSu9Pbf{{ep$z+>XYmzUu(TO_k>czM|eEN84yJrtoHHUum^BDS?_5o2C#Up$Mw6O5}7 -Pe}KHwfxs__792+NEZz@t7aMy4P>EO%TXesUNQBg#?ALY!W&qQRXsLf-DWg$z@q@AR@O^{Dxqxv)Tjv -lxq;Q&)HpUE+gKpBJ1TX~EY&u0WyR6RX!Y6L)Xt(2vep?0M=+1KqW`-T-yZyhnakg4F+i0{~q_K1g^$>O%&|qNEH=wL%q -fSxoQVgmf?7PS*Af_P`)KlzOw?(vp<;5nmelY7bNqs#@VM(HqF M+=X@-W}s+Pdo08gEZ1F!RkG!^ -m}Vf|lXl*-97n-_;vRz8AsiJbXr*heDkgBNAh}JpHX0ddGID!OM8oa0K~G{TX4adu#t65b%nw%=YSY% -M?RsNoj-6&TxuT!BzJ8|+JJG~6i5Q^Wjw;H)Es}}Fzc}BZF*I1@H?Rg$i7Y?x5@Zb|!XVhdTCLY)K)e -C%4cVyXL%6+73s)b@ePf%R+~&(A!bYlwB8 -3K|3MjqMq;$xKK<26EAeNH=bDOhns-Pi_>LC>g7JXhe&)UI|VJZq=kRslWQYqbPoS{R~JT{p<$QyJJf -%e*F$UdIeO04*|kKzxEd5B20wLcI>TIxwe6U&$g3o&gyX0)0hS-_4glPzFTi4(i_PbNq>retyltyj1X -*ET1UZKSGTwnUv8%>#L1Hg!4#j!K+ktqTK?t8P+Ri?EO0ua6p-*)Ul+Yo(GfJTGghXJwUCZ8uPcKy=kKiNhxe+796h>_Wk^vi+o)mn)UvHdWN?pEiz6Y)tv1#XC1 -4s1BMsTDQ9*VL*CElGxX2nHUR@)FGv=}G_t@y`SWkpOrx*!r{Cdb9t3DZqu?ID0)CO)#LLXE(0pF&;G -Xiuv0?MU%3har^6RD3np&AQlR<{O9#1*BVp$Xikuo~1QH^81#iZ>f9=*lzKcp27lg$@8zjDDU0+!h*-i`iswO{Q=FK*U0HIngOeIDlyJgHtq(!1 -=JLGX(A;;wR&>>(C6>(~52$pgv(}}m#Nyq31;2uHgt{`o8>7iRoe*B0#+^x*6s5s3){ft6jy8#;rk-y -xl*zR%_z$S|`x>_1C#g8uLfL48{O>ug&Ghd7+Wad0S8d)U&HLp61Wv;>DR3x|Jg -<>4_K4h^vWWg*N#cd98EJY-SuWaP>yw1l=Zm@ABsHF$nF869-#f^9<(^+@;zg&vUaI$6~iUXWEF=FdS -A>t*4vrucijEN{{B5`#QhHNtqa#o{d7L*chKHrNSjY-WA@q00t*qj<84n@Fb2bA&1z -6&mx0l&>}gMyi*gLOvH<-F0IcFgRc_X%tk)$lP92=_u|4l)l`n9vQSiZ_7NdQ7jYx$b&W|5n5Z}Ka?w -&>i;Q1+3bfI=cBD>dKvx^d|SZuN7F#|l>dTcpIqzhNN>9&B6jR4dZfo$Qy!lT&Z?|`4gtC1Th(RG1$D -iq{wa0Sf$TC)LI8=Iye_7U5jZVR0LhV+L1SO0c^C&X3&d5jJWmzS-ltEiZi;aZ9w>S~&|4A%o%0#({r -s3>wtlG-YF_PjY%EDT4Z9i3ru`na6|@8%^hW=66ZY%&k+Fsy@YQmWJp&Yn!&3;JIw>ILS#L1B+@5`nbq{Dj6dK -PlT*P&lZa#NjY(T=rtiq0zGqL;iH{pWRvO`q;o%_NwxlsgcF}T6Hk1OL -J9Z6AW<|hJomwEhDdTZd{z%H^tnse(YQuXy#P8PIf5=Ilc8kiGaa6T)H-2-FAVNcF>Hk2 -?9v@4Y|+M|qEw-N)n~*Lt1roxV&^wBndz2%;baDGSd9^vEV^Sgln{HUI&A1iFC`8w2~LhFupo#`VRl5 -~K8DqeX`{2TKX}3>lx6Ew!akr=7?d+4BCo!EeDe7hW42>_|MllPOc$N7EbIfvWz|;+KtjH}N6tfk3p5pI?1t8$sv)J4!QE%V%BnI%Q*ZPKilLhwX|FSN-@+3-w@ogWW`e`AyJx -?D$0gWKs>%Ar|AZEZ`H(;?DoRqWNqo@g`sxfHh9@+zfWTsJ!Kd_e6JcsgV~a=4wu!Wa=3qkS8rw_2gp -A6n0XkPpHnU5n%Q2!V-TTsT|pJmhM1cySKDdngv_!)Yv`KM3w43*M>If3u|Y&afX_zq6ylOG@Df=uuo -}jYJJa?G$EBG#bT8n%Y(fnjdl7Nz3k7)%waDw;NQvq&nhH2kfKmTZIUpajZd)doE`itrdM_S*Dd6p99 -ITDEUu}KI&!-YAjJt#*Y~PZF*x3+75z5Sq9O=_`xW|Ex69At7P39s6-D}d2|*)t#R;WaDsn6!mpo>Lb -n^nY@DJY?eY>;rIJHC!YQ^4t{8@XLkmL9)*Hz!>+l_2%JA9C+z8^=?T5mE ->IMhU`)t+E-zW`4C>A7s@tx$vNPqOSL3(?YpX~raL$QD#%GI~Rd;ZYl8@Rwn#5XH60jm@Vb#3~d8M~y -9a!N6b#zFW6RXsIlCW}l5mm9b9a{&H!l-Tp(Y<;UjupT$@}?}3Z=<+9kfI=IulHFS+@f@aDz0 -QNy+*dCTJq|r*DNv-IR$}9(fS_c$Q$m2kJm0JP2iqW-DP{~^?>f>+IeAW(-s8#T=XhmzmI-N_C8Ifa7 -QC}oLA^roY#}~*I^ZrR!D`A^>k%-ce;bFuX7I4FiBiE2*7%jpo9AYh(9Ug*Lv1szhS^NBkiwDxmF|!b -l}#=TUHz1vRx~OF_O4X24+@vBNf4|_^>;*YU|W=yp4;Fac`apnSqa8*CdwO;%S}T82 -fpgQ;t{zSB)eh~-ms+#z94)Eh>}^veo?R`Wjr=ppuJX|%M`=^mgeY0t6AGPFRN-I0jp(~0VcBx69WhL -+kahqn@)QZlu6yC0RX_u6*)BSNZRWfYNpS*J-v>ar>KE7phXj`X~lNadWfBE8xI)EX6of8Ez+6_`g;`P{{ZgShwAX41R0vi%hjvDEjDs|7enom1QFCOk_E`>3fI -f-hAAfueTikNb>=T{uN{H4OMvj}O#g~D1!uh$;)8K`Up5|yd5M6nqVH7Yyn^|u#9+i#k^afpTN9R;A?9mWm0hp+u9H!@M}vFT!~BCm!>C*AB#eaYD2pg -+C$=(WrTQ{U -5qKE?Z5MyyIbb_4Y&`+>F`xCIb{X*hY&Ggy3fkAORLw*eR(^w~}NEhatyyR1PDH*A^Vf4=(R`CYjo)( -h(ZtHP`_Sv609rvMLxm#`_eAc(DU33s1w3btEfm>j{Q010*c7kMOLH!w#!JM`W>ujMAjfZl_hnCT;U* -8eb!qCa?@Yy&!STuwQ)@WOu|_ppx{Ta!6PYyN%A5QFv!N_WD+)9vJU)Kh4Ctu_!J5Tid`^cP}}~hw2xQJVDUY`uNfJV@_r8qB)M)ytt-+*ft*-1x>HDdCJJ8+azL(VR(TgJ>9Jh4rgrk|2Sl5 -|W-5Z2a%LJ>^(03r~E09OMil0X4GXuScEmqvOWQlut8>FWGrk?U{8&j&N4Je=XDer>hMuqpnbYqn7;|ubE*%#f9`eu;jx+539>+&+^u~T?23DsZA!HG_xJ-Uj|9 --SPX{2@{^2GWh#<`}TV&X={05Ry9Q83j3k$djdF@iXW^kLd^WS%1b2J~l^% -s!Uu2HJ!8KxO5$l?#VU;>xV8hG^y50Oz4Mx_av{j$10ek$`;vMK5ECY70R71h90yA3q16>7qctO`H>g -bV||pnre7MSL0NEkRb}(TAj^rC=c`qg=1G%5%WL#trdcZRO8ZytDhvoSSp|nc)(=~#oJ^x_gp*sy`7( -hAGS)JwlsPLcHz;qCXop!aV6gft+wawvV;KpskrYof3OJmEWmYTzmUu(6w>bUnVTcKiG_W#;;p|7y^* -5Ap3{#GMb$S?m_2Nr-_wdpA&xaQuj}G63^5z8IgzzSOCoevQ7muLYn*!dB;KL`QuMV)44m>4hldwmMbOyc -ql-^QF3|oQAe<1d|6HQqyx68x`LW1RDyPlU;=7!SVq2q`-(!YM8}*lgOc+Y?He)PYEsnl-?Sm^#ys#_ -~M+|U@G}NK91a1p*m>_y6b2q`$q{XCimm@s?IXV9I!SP@I^glis-#`89*;oG&AKjb0dGqf0Lg&aM2>; -<^H`Gglq+lO4Q)aYQ3Fc^bVO>s0Lrc(pL~d`e$@ -xt?S|+P$ngAUL!h84boxDX -W2I$a{+W8MgquxL$lC0S20OPiD+Wc2W{!dBHz(IspCHa*xsHUP1PCQa4j^;=?T!2;Zt;i&c -ixoCeMLgA42zLe0)>i@slSdPRP#SF%5ct%Z-ZsgUY7wu62lS@Jicn66nMEMpN-H|ISUT$T%9t -VU8N8TcYK?iouQfiK+1>!L0jY<%NT+KLAKckZJLH=6?)u-wq;8c|b9km^)BbsaG^02B*`&tv!!DcTpP -F*QYNDHA%Puxe(#ciHkp&(w81+M^i7KE7ScalW?J~w1LjmKP(-cs>YaFLaP9DF6S-p--_F!Zl`|R^m% -)6#i$W4h~GGstPIwnnxIXNwnCfEv+HX)vGfY<{uLY@R_Uf9qcYrARy1Hws%uW1L) -O5lTe!5JDcB^>#HFqmDT@-g*|M7J_%mJ1Lxlceni8j5XUy>qHKLRoUvO#nNnJS~nhj}IT77$nV -Uc}T8)Eu*2F7d#A()}Yxp^XurmbevX)1!O3%K}{(z1m{rF1(UYDCD0T%0-Afi0-j}0nGSWV)0Hf5GN8qk;2NbQ(KvN};%Pf{!-D%3z+_k6Ln6g)3c1KU1p*b>V-%@&0kv_5^EWUI1>Y$ --#2#pC8q;vK8UD$=Ym_0!t~$j&~Cmw^hT)F4A-&}$P!(=D`D@Rb4h3Gf+2V9r~H>!@L2o62gl67pP^e -OcU|gBHLUTH6eewpb)#IvB|9@y#A?Dc21vw>_$C)ex#-14-cLJp;J_Kz> -L*N%!Le#fz%p+{9Q*3QI_eabl)fKMkqyj|7WwvNLPHl49@eYyy~()2#fkTP(bmt}5w1BzQ-Y~D_y{7w -4jFW$ZV^B0pZK6{r`E4scp9*G{^AdPTzKB_4 -*pqp0mv&ptCld9#}y-)+fA_p7fO$jG36R+Y4ppvV$3LQ_euC(1~Goc2;G9DcUqJ^(Jp9kK4i`P>G{^BHO-khCqdDsCnM{LR6S2m -3N5m1)2O;T+O`=VCGtt4&Lq+<|W5(8TOm}18OEXg`W#*iaP9Rv%-WJK3SFzEiXUmH96VvapB{iYid~A -|WQ!*SnDS$RtvVGcTIWd8MKHDslj*ZpG8%iBadvsn)%<~#JC;Xpj)WnWjOXkTE;mZ}yJg}4e{gZxT$_ -~D6_V-R=B;nPzQbk!TeA*XIB}RZS4r|4I=pjLt};XbI=~~_$UVU`I01Zmt -A*Dzl$c-V5uC&KyH02i^&!mI6O>DVPqBb1cMTe+NJ8H=UM8`?Hg~3(W6^0yW#Rk)V(t2off_;JrWR0Y2Kp;CL`tsT~CYxnTz2PBo+KjfxYlfyv0TfD|9<#=T>s3$ii3<;>hq7F`vcWREl#QXRNGxqN2 -BYTx!DN7AvFnUn)1}%|s*lnD%d}tD^Nx?2%=kF$km1Gz|m&cfxs?ITPU8Snbi{n+cDnV6Yb-pE;KmGJ -m@cM_Ze<0Q4R9zV%sj>_7zO_0B83FJhJ%!N2s&7_kH3D@-gQEa)41KsQQC&eE00Ik8RY_NizWzC#V&? -3++AyqQGH~~5XhnwGSL}_|#z^tO{)CBC?=dVMsYJSKnBTa`mULChrpTKu>f-mm&TgJyfO>WRX;N>WVH -cLmBRNxaOn2iQ1BLnSzRtlDX^%LesI9h$}?;~Tn?2kJ7kb{P0| -pVYrq_6u}RE-Ph?R&MAz`&amQhE(x56JIMPvC8RQ&}JL{)};--tkNF9H<>;#-DgCTx$~Y}plisK&(!p -H`N4SF20}30w9J4y4nfMkzoZ0U5!ny)?RFgSuUyryG5;534eOJYIyZVs`T!I90Q)BPBCbK|@B-0%+0$ -iwx`#b_i>k%%8=Z2*_l!dJW4s2|i7@kr0( -Ik|DjX8Q!BYc1VJDfq+@mwb13|imV?1{5h58`x8ftVki1)n|mY-gSiBi{NOC)CO -AcK-9Di;o{g=RbdW@yY1RKhixv{^&`sRlqj&nZg~X(OtOGTu*+c9}Dt?K>tuQhs6x;1wF7mQJltPx51 -obC-4Mw8Zm}OOeav?-Ua0_zOU(~JS@5g;aK2k -aivWemjWJ}7rZZwQu91X#;n`tmWxZ*D->zrmX$0i)0W#=v;ME(#@n~7 -aem{3I#fp>*FPC_GLR8{ov?UM4X{49ebBUTq|k}XEJk~wo$(i-Lw!KKIUTE9+6{`ZCUAW$+X75PU>gb -nYNNgMLsmSnw~P1RLyR<-OW%iknH~&-Nl$Cx!=3Dh$I+t)#qAepO2A;b4sTrYS3Tsv`yl@b7SDzov(t -&b#O}&Fr2cm?TEco`}@8qc7alwKw`>%TXUB!Mcu>jS^ZB5v^P3)C5_>U?4{ -C|Y!=Z}i;6qfgX2ye)Pd_I`-P5k~C&eGBQWQ?H@on*-uKw$KT&e%jwr8aqq+=R_{r{(PZz&!zonbXNO -eaxc?l>2(N#U7T+jfy2onZZk5T7A{{0S}21nCjft?GY4-(bZ6Mefm|OdCSs|kh5;f4Z&LBd0*gu$s#X -^wI+?KQ7_e9&}K55|8COoM@%6jW)}l!o9N4u`tWU+Ea^NTI&}$=D9&`^B~-je)^yRq1sX);u7g0J;hh -8Rbp!M}#l(2x6Y%Nv{nA;ll)N={gP5iAtQc3q>21IP_LzUX>lQp}oj=nY4GEIq2Jo!N=1?u&P_$9IA3 -Px19n$%+llm8Sbp*3w86(tMiJ3<3S+0HZBQyEv}f4#2U$>D8;Anfms}mEs!g!&Gp2M8j5$-hhr*8rmv|1Pj*Ra>QR!+h+{+qj{$0vk@dV`}?;h_M`kJGCc -fqVJQSpFf}<4j?L{->C#Bid;zPcn;)wZ1(j%b&}uSqvL6d4Az|#T{9IjiGKSC;a!h0gbXsuYv*Qq?-2 -x5N_4YQe>QjZ;2Dg!6h)uOB`9bqzpe5nbA+(5{;sotc_B*sY;=n}`SGZ`bKmfKjUGH9bdAS+Ae;p6(- -?CaYO2wnqR@d)cWexSZu@1RB`Ap5E_qY0uunFd -y3j>~w%2?XpA%<`o^o;QyaN{JK@C?(5gk^c(W3}xzVI3H7)iSb2;;sZG3dGFWR*776E -y-4ITFN}7V=9oqi&35`b5#JQFOFc^!G=zt<#eGPO!JGUFM`ibz6kE~r_Ub52TpJ~Ot42u^kR&nRFO!YSYH^?MV!3}Oi==shh`*kg8$+HYw3(Zc)0eNF{qTJ -9@~L~E9TP($T0A;`b9;R8$>>e}@s306Rrn^t#FJ;3_;K_V0)Vx8bpG=QtB?MKy%L+4HW+gvEPr;??<7 -~3X*{G;W!4Qix=NoOF5{-eQFl_n$9y$_dDI*4dU7~<-_1PzdSQ!&>Jw!*v{uQk(R-hst=AZONs^|vMX -5FC^~}NJGuwC7Zx9jq?4POCP>G -^W-~Tq-d>8kw$*)SucCS5Kx}B82DoPsdo?mOGcbU`%_}s8gOULRU_~Sj~Y=I<3oI19h<@#p8iQt6~VE -wKp}f3?J96(UVLuon!ew9J`mN+f$;4r7D}IZ?0*WydxWmfoxT_0zK}I`1X=2xhSX4o$f@s);enBbNFz -iBCq)Caq>p$?_Z&XI;kYARB^|sL{4mTZj(Da2dd(_I-N^t=nW|raUe^`l)iPsCWbc5P#1AE_}A$nBWCP%Q@0D2YL3w9z)IC1App_lpRvdM4ETNC}E^RjVWV$o9nIXA_z4NQrwwYM%8QR`aK~(Ep%+uo5&Jw`kDDf2ts7 ->r1PGHfw#&h3Y6=1t>!c5W+4)T8M$*723nGP;+*r{kq#qzl(iwZ%`B+ODN_`D_tExtS3-~5;Wo&z*%j -n-8j*LzD4TXUy&y^YsU;Tc0sm61834N$B$_80RcW>3kSa2I&Ued^+nrz0pO9vEerUmWHaVlU~C2{zx` -Nb*iup(-v10nPQ=pKHMB|g>h|EB+K%;pV})FiB%M6Z68QNcqaa{p`+e~>^x*fv`~UP9%!tPMfO}cPX{$%ci&QCRK1pMUudJ)9nHK -pKF1nqS((&z$Q+XS3y75jzU7&lf@xdoHAPVfO4Yp{d4M<6UWv^nplVE7uI*Ac-7g|;wG|J2rCU*1Z!Q9a+e*YXq{aU6qU~_NGAepaG3zV8M;npt -h+zwi9J2khMqi?AR&%9Lt|xsfS~sq}Ix3YO9VZ=5fMzUrQEoXr_a+g@9EDf{$bV~YI*$Gk7l6Ezl_u%L(0@*t>9u>`J8_rf4UWVW49UKAC -dY%Hj<`zxm4i)Cx$*w{^~3Y*d`6B{$(m8&u6A+{O%XlYbZNxb&yhO#-gbB<~KPA>(y(n~YVU)FFod7g -fxoCE`%VJ($cR+!6613{tmS_cqaUeY!j(F!tu?C%I+&IGO4lQvd(s8_~Xv<5{?R$bQ3u>?l4#mAEVg# -SCec4k-09qoK7`GJnU)tp|OL9qy->t9}mo1%u6YqSN2&{HKFm>XFgZWDHo3#O`QNO%*LII{I_H9SR`- -%(^+BMs%q%}>w%lb-j=)a3b{C^^4xb-tEXh&*7v0it7G?TPWlryrkEaIr~bY)PU0L0NdHl1=oFf>SI= -6bD444ai4`1L!}=XF1+?CdYl!KH%u{qBz2ki5Q-m5i&*})9mIH19;8e8!@34_b@lMd&v#UQ|i)2pOpt -6C($WyrF)om$*ck$D{X68S0Hd2v|It~jIjDXNcKFB$rg>z-FTzkpj3QzA6OT^37>g2Iy_{0}kv>@~bIXq`^~ivOU9H-U{cbF--7!*c1Fr&}J&t_+9ke8kf*kYvI!LQpJn$Mj&^NpeNX7 -%Mh-zmjP7@7hBbla5J%+O+C2_wp9ZRKdEFJ2IbJl!(Hla!zGsVO4k36qk{0Ir%EmEu>Y{3Cif2-MGnD -b#E{s&*{DT6!(+qX>j_L4H9S1_}-D{5!tm&eR!|d6C#b-qsQT?xsjE4r>6CtfbHlRWR}x4Jy8bA`l_( -ZvB`SOTq>Q0Dzi`Kt>>lD5{Iy0?|@8!LgBZhX=6$cF&^(nQ`K{X5^~XYL&um&pc`OPDp3!0NB?Vq810 -O8S-wl6P^Sbv6K1wbd0SNWV^fCjMVY%zMfZM=fsX9V9wZC+guMnVbZrM0PI_#r{HB11_52lw1F#AV3_ -a5WKmy-d4`QDe!x?&n5Jp`}zatAPvYOp?Ul}xG_@3o4$u!X}-otkwXBHH*=$2$di$Yo -_9Bnc`}<9%suef-nTFIM)2QDc -D63&B4Za~TCBpHv-hIR;;=|W=EJHB&e?~fgTs@HL%+B$SeV7^;PmA3{OCVFUY?#`;7gb{c>46|;2;g9 -WI12vT=J{{99S{uEKOv=#FR~wluMRmEMzgf%9BZ{b{QQ0UM6>83Ts#)Xb?tH>EjAqJ!CKy*5AW=DHfb -9b1_NzLi)6CvRH~7#}zo^o!U}6V@1{7tUDTK99 -{2~uCnT7=)vK40;=P^J*VGAZx)XH>iK?3;#g%UMrDsrQud;Y&j|6clg%=dlRJ&^>;0Gn_&$wi4e4r*q -teChv_i44>`mZ+@3+5axTKRpS4Iy}EPIz^6wCioyX2@(&IY$~qCdmyEV6xztm6_ek1RCwb7gAXX)+m> -oi$|Nn2XxNe8FH?~l6QJ?6Eck*5fUgA*iVPxH;c}U-aqeB1C!&-Lv^a~AyplzjMF6*&FkXgvxF9%Vb_o>V69^~=28eu(c -qs}w18>1|zjqr%#;2A0V1GW^gda{M{&mgwOymoCLGwUlFaX97tzy^nye24cEZ_uCTovGNaK7Zr=0q?$ -C6axHTxbrO5y=bJQzY-G|JhkNNt1}ZJv-8`D+VL?0V?sg$<+5K7Z}n?KXBYOTje}tlqr4JA~e;Q6&YNv^f)n(W{4cMA=)URfZJCv361k -xWf(p>1_-qMHXOeWucvL*GC~3<)e)G;2PM*hnEl_BJgk!7;+BLC%LNtqF(pg6;O>dB3u+uUdUAf{>kz -{Es}9&Ta_x?(RB8>ARk%Laf39w$*%y2Ut3I*4|1&;leS`?Z1PW941`SX^ErqnNK63}RPY=8QcehnSDd -LxmEgp<(T-J^p@qx+_hBkG#j$hBH1xtc!K~wy$iW*omR4U3bJ309Mne|26zXjQmwN5o66dBJ -gl|T!?7JuoLdM~kk&5OPizY@;^}g%imdhkYQz-LRJ(N<&_vR(0KTC@y3>|>(m4OIPdi$qkUCs7EO8ef -gQ8rf;J^@%`a2-hbiY)6IaBMX6w@;?AV+h?6U;GD9=g`jc?c#Ott6&`_VJ^BIm84m=L>Pi`&R2&T_;o_p&U_-6Sx -{pqW0!;F3L1UcSMCGhy?13-T=f@&6Yr+$k2h;btPbO{PyJN{o%!>pJYG^;Wh4teEQ6bVmW^PJQfkC1e -FuM$Y;+hXwRi6^N8bje_kxo9oQG#Z7h2k4>@L-gHKh3aRvmFhJl-8<#MRZO_sTupvv{ETueYB;L=y%& -ZGqj_(281kOH&Tep<*=eg_t+R``rLVT9GJgG<766|QYCph$p8JO(5jsq2+os>Qgi!4=^AOgf_93EbR* -Q_OP?&H|9v5aBOo8xjj5N_Yv0?{FV52aussHVcF)e1^;=p9*A|R~{)BzSeKNI`}BSLu)y}g_&ds`lU% -|p)>~pSQb;;%D}?p^ascU#R@C0Tl`yK8CDa4jJ$&iDkdCZ(mcE)p;;h$#bBuQ#PnWCRE@VAnQ7ruXrkVH3X}>#m)w*A -%VQZ5s}0G<+@1RQ;g3BC!qDgsH7q^mWv;$)mrqe)jQoD_Gm}ewK&RqQ&n -AH4S@>6$`CIrOT)u9vM=S{;2pVWwW;1Owlqgv$h5`C~E7)qbTZ=dY8&invv5Cb~d3ko!FVd<(7SO4eT}sbqQILQj?Aw -a`IvQ=Yjr(L?oqp>lEje)a85-54>!WmqSUpERe=r=3E5IfO^x&<&W60`eGckZ&9sm(p?wJ7|AeuV=6m -s6fiGxGZ()3#uDp2d9!vW4WqYlMLHj?zh8wmgU-vSF7Y*jZ}wc`&!$S78!LjXF@#%)8}eSM<+TmZg+| -sD*KA}mb(&W8(T|Mr-#$(csSLxx?m3?eY=&5cObpU`XipMX;H#hyUg&5bPoPE -mn36{M46IlXMfBglFskElU72v^*_c$i*J4HjEnn@L~cc&N6e@?PktmJnzi`F5S<1k{U7Zxjyrr0Pz5mO(u05L -Pyibfu(k7~~u)m9Y3?1jkfb8r+XVf}R1U#_33b6++2=~M`MhnJR+lmKtnbWg= -w%#Zh>vlp|q10%5n*r5sx+YBD3|2(FrCd&(u6L>nd1oQlRRAR+*%lmOJj-@|NOP&%IAv0$;{y -p{%Fuo&7{H)%{}nZSE5Nv1CEM0AAMAj}w4C8A*sf%MK$j{2Z-LmV_Cj -^rYg7NrxYuFjKpV~Xn5W?yh)+dGdWfZjIRL}+Z98{s9*+V4(PQ@Ss|-kk^i+J>zvN+whLKSv{J_2wLnW>Ps#GAu;dsXNLTGBgu-yz0UB=~f -_~b?>7}_++K8Ftq2pwlDDMLCCF1@@BWY^1g(dX|pv`moZg!(W*7D5dXG_jt8`Sn)5-ju;%~JWWS -wa%<&RCLcg;sGK|c{4x#eCKKe+Io?|pS=UpeuGV<$UR&WR%6?t3`7aic5=vI -pW#7#k|x|mBbh`A~_O0&$4cHYLjQ3lwV4Kmq -WJKX->om=4q4}nQX^OT$`Z-CkGdJNhE*~^Tr$-_@|L -#Vc1>mnOIdiL2mPQY_|8it2fV|6p?j#JaB@A)Dq8HG#kZn_WWI9{WPr*HM*U2aI0yV2Xo3k4u5hSmc6 -6q{b{Z{!nSEQ_^O#&)IKbDv7#yGeHHg5cG7goSzozIU^Qc5efGqqxo5P13H$`-R -a9vjgMgw-Tb@}<7#YF*IQQc`c}RO0&D>5D12Hpjt72)L=OzdGz2rmu|ye5IK+A)ns{#gF6fBG`H-ZyP -VxK&<}K<#G*nBbz($a_5;;|NLG2A6qD{_GHQiYnqV}UqX1jA-TrRSq!y+wP{1;1Yr@|OgeD0X&TPjVp -T)Ehr6F4@eVT@Qm;q4zMxl58+qHq(_Sgwbh}pFxQF7k(jUv@8+p%GDGM%crYky2c`!TaD~YSB77~?r^ -xGVDldsP6JHPIMgk2T)P!nLMC>C6~b=?~2wu$Q8!c`X#u`a0ELo|PyL>tnPhGe?PbPq&#sBcAmgk6nk -i(?ZW;k`x2*VFuZ-udBBU6QoG9UXsvilJ+j3*Z -VbkM)lt$j@8y>kSH@2naiD#U1>uk+fSigld*7OKk-7cBpR9sh^lNVc?IJh-iI%-E%MvQ@6aWAK2mmD%m -`=90h-L!@001Kq0015U003}la4%nWWo~3|axY|Qb98KJVlQoBa%*LBb1rasg;#BF+cpsXu3y0>C}gdU -khUvU7%%j;QgHil3!CubioZ$4f8_W9=Hr)yjUTyk`Dlw47zf! -hLj;IT|RawP?FB5SH1n9^j;OC?V#DwYjhGBRfiEhPqu?PFb01BP6 -Ocg2!qq2!w6PEitY_P_$(D(f@r)-h>RW%UDFfM3P3Fx#4G&!I8A3iMnQkKm}WGe_b4lq5w#tEwo-l-# -CZ4W??vOnR3j;4xBgT~r(vX*H7BBq8wO3nC=2(_CmnMaf38dv-=3ED$0c0e*mIqXappm1aq_Aa76a(3 -_Wkt0nvqxGr=!%iN|H?|^55{o8-E{TO -(&$H>-#w+n=#o=$Oarww&NJ`W6ZY$5H!k$yzgM24Pc*j*}VhAKIB~&F&%*HPV9o;4`918z5hPDE*O%d -uK{LE43esk0bVhLi#`ORB8J$Q8-Tg>0R7MT7-KgC#7MJhF~)d+yvORtHX)+osKU=I#1M_5NVzcb5eva -~zqK`IvS>s(`Qoxkv40w_Y+in8*&)EzUXTwdbj?b>;K0I*kq=4%2^J-nle?2&JK;SXF>xYw0r&wWn#w -@HTjoeFlG9-nsX=_9i*&-=BcdHyM6P)w$~6tCEpc@0XtePp<`WyGR)>)+rVYF5%iUHwrA{>ZUutpJ -&Bl#wpDMNcgu3bC9|p5-?TikL&|hq#s#DRpSvEbxRwHya9>nqgtCu8qp=k;%aRUPr3n|_WWBwh!u -hT!TjJVeNzn{H)&H?pUoNc%mvsVNo>bVC>sFy%*FEM#OizYGGT&vkCU)+Pw -c*YvA|;B(JhM9(nC?1XyYI8V4O#VSTY7Z^#ZIQuOI`alXJ6Rq~LISBD$YH^tMFR(l~DIg;_t$H@_L`i -7iE2^mh8W=;yP<=ZiMH*?`-6?hk7_t^H%ew@*-^va#6m-qdpM6T)NnEnTd4Eu#^fdj!A!(W4N->^q|W!ahG`JyvvLI!Gl0TU?w3Z{5PonxjHw3!!@PW8#?*dk7!Uq4Y#qbh$ -}oCosTLkWXb|rQSHQb4w{N4YgLS&a;W(iB;3=K0?-AWjUf)H8pkWcPC>n%tcnzaSlz^cK{XRSY_49|D -0xwpxziuE3dnE}9RkgpGBrMFtK(d>UGgsEyxqalu7Myli+1T*&9Hy62u)RV0B9QIo9c&<8WEwJM&T1y -`l1d=cqN`r+@@WE%g04M-2~6kh2o6(uE14U0COE*fP;h6=p(nFr=3m^Pk2g-6&y|!9O&xO`AYa234CL --8ZPa#GPTar%dSX3@??v8_u5htU$!bGp_6N%i+zg|dG|)yr3!Ha41_(yc7KdFmj0ZcXWmzcDRwU;PZL -r2;J2+0iduSu@&Qlt^55abY9)rt2#la?+eBe|(!5;rfia?LB{Y+{TUn>r=2&{m6B8x6;PxUDc|sVy_cjY}rqe6T7lPQ_GRWic4-tu4FawcRv90n=|CD`G0Js&59*=@qnP$c1Zm?`FM*odZHnujlf~V!;uF5X2njm^Q2qxP*li=iv1+VX#tFj -1=%ZuhVsaWv5To!54WMwgiPREP|FAkng509tg=3^5iMH)PPb$D`g@b#OMS4YSA5ZY~QZf+jT7de}=qM -^<~c@Z=K{v~S?0G9+8WhKX2B(v+}lGVZM>FavDv2nayEXt~>gCx&`xzS;sz{6-~ygdwd#yj|*$&gT(j -JE^!alxvL2FwRz#9-s1D(68QUo4wt#o{=~<_I@f78#I)#d+4SD#`1O4e&T8I`&66ybJ?5;g0mB&I -#1~o4WRJhsWg&mh%galkv;4DS8yg(dSb}+$)lHtAPe0Ds0*NvN@5H+Rn=PB57og-GbG_pkW^y`d##zC)G6olHIZbX11tUBVI2XJs0R%C9@_zzl*Qg-7vUhO-#?k#K!_7*&g55vny -E2AuiC9C)@G&)1%{qSBLS@bYlZ&&8lEePJayTU(mBCj*DatbRPgAW_eQAYV}`cbq#H&RaI6|n<|6djR -1aN0r!E3KP|J0rJ4=0I+*j`xWa9~Ps`#W&t}a^$W6oMizaAF7$QjnV8+`D*uh}8jOA$!JPV6~`(P#@F -Oz)i8~II9u~~UpWdC64_nd;Oka$yE7v*iC$CQ*Wmh*E~d51YDZjwApS5r;{P4Xo}vSqw0LEQ=#%X64c -u>bmiZ%w3YnJ$5~;~4fG?3TUYG?c@J!ytV9-8b>U;qzDV^ziAcX9tJhSPx$xO`lJXj;7BfcZ6p+%(UR -rCs@|7in!@f3P7(B_6l$>)f*0E7&|0s6%wiyyI>V^fgsR0__kcIi)EhQ1-BUpiE~(=tE9MODKv*Q1;G -%M#f%}t246n;3a4LlsRB?KfVsQ9Vz5KI(MJ?O$N&QIi-W@{H8^-1@5B>$Q!(17fP56bF9QDa{{1iEz- -|ry^z{g^{p86`{QO}0;@L4YixkC2f{BpGhp>(CU*pm1JZVrOhQp1({t1tla}Wo2*yf2eS*_^xa{uT%U^gO|0i>f>3y=0tExc%qCtv-Ct;x#-&Nlc -zUkLq|>|m^#(|6599HChCujJp^tdZ^I5{c$|ox1v~_gw9&P)s6rcf{8FFJ9;|Yt}>q -^VxX0fSIw#P)q#p^>N?vUL8ID_jtJo{p>`AZ1 -Dh45QAfJSJ5r0gP)#@2+C`yBWp5>JHYoLwj#e2?_M?QmD|7noDast63q&?}ZZy>>jkG`+fdO?j)P?m6K}8+LU^pa`6zp_`;cuO%vz!xMK72hD8KmV72AGi*d@%mzNR9RBV@m@wgqNum&)}N2o -OrAv=&Xj2?mH&gUYzvnG-|>m#Y9fIxEcxkygdL~^n=lEzUeko@v{f(iOEQ~iRg3V>R20Xjr*#gY`jI-;n1>Jx{Fx~D(62O}8@ikSXqZpq1-TXN#tl0q)Ep_5bu{k#r-S -|&NF-pX%O%R@eAMdV;Lz(a7yIRKq@r`u;V@CACtgWu@0v>WQShH6jn)sl-!TK75_10KLLH^=~wf~adOr1PP`-CVcz8?Zg9_F)5S5+uqMG?j|}RBrHx_ -*Yr^sB+=X4R429W%wH@sBuyhwJ!-5$|_5uSj;$$0u-lhJU7=KNycQE)M__M`)eT)09TylSER`BZr#1x -N&-vL>F2UGYR?rP32deLuLaFyIJI*?~qC77&ugR(tD=QrJ?z`xK}v<`q7@g#&9!vTL@mDj8YgwZX|`* -UDkFs-k_KFXRax~*x-GOsNNJIGCF{@j#VYQ32Nn2%s?NuUT0M+3{Lf$1bE-xWq56az_`qNln7WsoI!u -ftac20^cGP_SFM3M92S^I{A2=fwPTV!wrj&}%>>o0ZEvEy4zng=R@{7t{+j1Fp=GSlEQ20K-x8(N&MYaIcG6Bly)ue5`UX;57VypHJQ -(2?mpV}`k%tNb~^2P+;YZy4aap&@wT~ePwlonRpCs#r*=KwHl@yaIPcT=7Yh8 -ly}EoIC8OcJF}UZJKe+`pBS%9@3HNc?YqA*Z(qz?nRQ&Z>w4{5Jk^3?I)FQT;hk*nMMybPO^+y1>*US -r7gLjD=IH1=oW7X8oF1MW%R?YCBqF|lb9nId)iV=>_Ojb<^bY?$0(p6Ge9SL_%X1t)!iUcfUQCbopSN -6q;pvb2qrWAie{7HbFh2W2d-^bHfuaxKa94gOd@z*8zz{tV6wHB-_sDyrT~bH<@bW0rd~j=S~|NW -Dm*+TIA^dj%o>=m-V4+Mj_c)l1fvY;jW%bLC(_qc5?df0g#j|oI2;TtJ>^k70#hk?6xd6x15jSD%Vc& -Z^^=J6mtJ)YfXbg=kFU+gJ@RS*@hGY~&ZFx>%$}f -tIcs7k;mgI2_Ur@#Ng820jWT&axE;wmAk(C=?vSMb+k>KK7!Lod+#!W3@Q!yE)pJ-ky;+kzylW8e{v&@|!#*y -vw-`n#x^U}^zIwlblmEbqgHh!Kn9fzu5H4-k@{+>w1eG}WEW1OVM$9t}_L21cQA0VpZ77kwL5zH_M|9 -f>Q*8Sw+Zuy<`)Int(Uj_tlC5>{(37U<~^2pIRMUe|&W!}uczkN@an70=o*4hLg&Kv&VAJy*j8pg2e# -so#JYx(c%svW~+I*c|@MUNV1$on|?#l*VYE`CkVSs|idSpxzTVtrFu>Fl@kry)t53Q|Yu%I73eMrXr -4A^lov$LPHrC2rEC4|s_CE&pEFuXUb~>7=OoT7*iE!ke^Qf)bEOgt8j0`Ee{l#KD%gdUXDv9oH>Ie^* -C|Av`sm|+5n5&b?AapRl?%AR80H{;t_#H_P)S2E;Tfq-ktf1)t@hHcDUk~UQ4s@*xbBL$`l3zp-^qga -)Vqzn4BMFGOAsS9pgw@k#Ft_)Xz>MFO5Rdb4u7bH&u!8bL5j+nua-K)GxYw@^2Bx!xta=WtQ|omGJ*7 -yqi2fee3sAPAa0n(b5mUvH@aAU@D5bs;R^o7}ZM@w>ijzqA3Xmv%ng}K#(C*MW%tg;tlYs8Y#D4Z@M|wF(J5eZi^)Vb)79@agpmgqJE%E6Kf00Y?D%fSzAoqK0r)KY -lP&Kjqn4cmI2K&Z9oFuhZs63gfhJXA`H1TxK%;@)OyWb1fxL?K)*WdG>Lmn9&Kmf`1<<=-+iI~;(@NEw-n6%=ag|uxCm)r_BwVP0rpOITRf15+GdIH%y3yy!q%>YGw$)@>)WKdtZf -N!Y|3L(mqDR`K1*16jHSjHhYtkq18po0%TVLLW^iU_CB?6hgYpCVDutuy!!D}%vOkY%`0xRPI6i#9;j -hY6`v;-X9XaRnvP6v^?8l16{P2Oq1#~-uZm4tDb#_@Wd_$nctFZz#1x^~ux1x_Rp%QTV5b5a90X7jY& -AEMt5v7!jn3fog00MG$WqW*)Ybd`iX}IzNi1z?}Isg};W`o|gqIk?miE#Ydw#A}~X`;IG@lgrZb&g4Ae?O(Ku -H;banAetLb@XWtV@n5cr<4erJ!1n2Z_XksTg;ly2Fh4-`9$p;L9xdT;6T6;lSQ=8q!_pvU`2}P0L%cJ -~1cv7ev&H-|kH7Opg!>>_hLIWNK5uWEmo_{eIxGd}TFq5i1bd##=>CG5hE+QdUmBci(Zd=RspC1a|na -z-q&lT35^S@x$_{%-#Wv;SN!n1l>pg2pPmsKRQ#DCa7Iy^Z1rZv0_doXxOx=!rFOI=^+?DWXE*Ko8lM -PMgJL7N@DhL^qWe$@c4SlR5FOJKSj<%by_B;=XtQg?7zjyMo2rlv6ocveL>lE?lS4o;=Ds7Y)zXW>rsu-U_%Rm? -eGZ(EL53=T(bx;;$6YKyCMu>R|kmrEwps9y!(&;9Z)O!%nPe;B-U0Qlc^6OAUT224Pl(6&yNdzm}#1V -YzAn7+br10Vgm*=vet(jO3GV+*9OTcnkTc@kI5!sWmE%rhC&F7x2?O6+}%*50okGxig)stAc#F35U*! -Ini%cR1UE1KPR(d?x#DeN1BlUzQuw?~&by5- -6rf-Z~|gCta%L{9pbfW7#s(VAljLyJ7)v$(nI@=jz#J5SFFD-_f}_2ZZY(=jn;e5$Z9v!u`c#Ajy7w2 -o~);I_-7ZkXD`EWnmG)c6wMe-P!0f})BO!8*Zig}yU-iH3{dSsefWDB>;+nc2*gb9?F8Ej-oAYs)K?h -HuCx@Ur>)QFCV)hy#H0`Os4Nke$%5;*OejlK+6xom4@e^E+9brMZM9cXS*4->Dtfk+>g#zyd -BN6#bBQt}=L8tjIZrN*BF7RL`sX@WOLL0aI`Ue*R;kAK9Q(6|mZ6%SRaFjApC*Gp*6DGnuZ1W;&&LS< -+b8@(FH^5JEZJJ_y3HT3LWF>eICJ&*QN4-rT9%*GVDSFUg6!Xv=)#mo4_+;Z#yz1A;w~wX!J38_@j3U -|Cs^e|70%-7B*;Aw1k0M=#ViXmA1tW?#mb{jJnsvVc#ZhLK%)loSBPOF8HrJ((J?N&Mqsa@pCMbS)1YFE@TTARlRo_SUft -t!Z1gdkE)2*p&cF^NZqvHt=omsIa=V@o1IcLUO`WkxHmb>6x~Lhwi9JaIMxraAwWPIc#OLtbJaNzzoNX;LPOP-VQDV9rwsr`sRLoW#MsN0?M< -+C|?fqsNCrED4&(7I6ZOP@c;EO$rOfrrCrB0I(Pbig!^3;*sbbb67l+6lV4u&pCcbKjG2C-)32ft5jj -1`281BwXbR=p*0NlYB(yF$GqnpufG{LZ8U&r@~#-T~fl+VI=Z4mkX3TR|JF1{VVxFFezff#-jER0s6G -KU0bZ-&8SaHgPS&HmGO{-*L_=oBAz^5y8?6Vu&gWy`^sUc6!tCyhlD+Cj!yt;G{J*o%D(`K!54r~AL88&_we>Z)1tV%-Qk1X{N8AqB?>cM)f -YcqhALTefh#rsbpx%Tkc^sO9(8*1v7N!T@kfMVBCB8w;pJv+^^o&ssi1kzqHOjpA=@k6pq5<%u?)7Mi -#`ta#HjW#c`jtRS}a6!;u?wI=Pe3|8`{?j*_7?R!SW>P^R#@3I`wErGZqLK!v?8Qx1l?9zOavI0)4o~ -*q#^3M1IG~koEwZQfN7S`{hOp@7s!Ikjx6IJwfDX=a!TP%xT-DklzAl2%;<(OBphZjz7Ry#a7|SB)+- -()w=ih&PCaTDZ_9c?W!n|HT#dJ%2SYVN!{}KN~&K~?G0gEip^U5?(C$?E5MLtC=k0F4_G|^$;3lMQ8B -U)RHL@ME`%9mXjDH66)&?Q^65ZvpoflaELP6TnhXe0%Nhnni5c@)MXf2(vkA&Db7O9-h5p$)F`r1P6eAcllW~Ht^5jo>4ARS(6F-g+>v51Bd1g)qJtW6iSN!Ntx65-h9I`Zu+#P{vb_VtiC~)VyGJM6P>Qe^h>;J(J%2Y -(G@oUs9^#TLw_<=K~jI)C4yR)aG|XrQWt-*^L9s5VT5jKGDZ_i742waX_y^txSC72ITeT6Xz?%3bU~t -fDylZqITWd`@-33wc#G=~+o1Ecap{1_S|y>{h}Pw@nlVw1D{9yAC-tjErnX+}`w)WB?~_z0b7Q6&Z%@ -uQ`I)%m-F^&Yzud90-d>vYvn{cG?_qGJjuTgourY}bE_vCAOKfs?nZjEktNt?Vjat836<&UEKL{2HFCVq6|oL{e#IVmryAApoyiZ{~SJ=FVPsCU%R+CD4AuC*IFV -;`)^lU8wQXNlZ{lv})xW|Gr|v4};Rz7C#z^@k1-k7;PIJ&dYgE3o7p{F;$F4P67T?nnwlowWPvLM#xv -skP+krMp5kO?lQkZt4t~!v43sP2}QEhug!luk+G#BK7n#`&o14zv -1)yqSzGJC*Z`Z%6)nt -5fkmTa_Y40fSq8(UoyN>GgD-=GSMAAi|%vF#2^>nN^C6mMHQ<+lOIC)r++imeyec+$h|-mX)g~EM{dhf&q{E-H -O$X?({s9l;uPimqE7DYQhxcY&c=CV@MSkmF1o3CrzQp3N>V?`YR-XdI{?_+fq?BX}{))oAag1ZM@L&{ -sY_E7iNkWO2XZ)ZK+=hKv|4WxsYPL1SrR94_XR#&@SbG9=9kx_E(7&qS}abeTRnK_4 -&M{?4_VbFtjOYaG0i=KeMkRpsA^c{NviSxbb7fO+Msb+B*BOB^(7 -w_KdHsN8%Zvcq$45j`<@u?l0xr4p+=~v~ZtmDEDW|zs_V3Lci*A#w-h96j}4zf57lhUBVo!kKc9Eq6f -k5!-Z~kIJriesYbw7`JK76`enC6rXS&cq-_AH@(|&7OSyq&ZN}DoMbFjKq-H6WO;0fOLhbUQNXas0+z -CrCxW>|0_imSk`qp8;&qC861EEkYK%7UsAIUeb`r12taZr#NoJOF+s_TR^m+yt;6Y+}=v;QSvPX*eD1#WofYy}IiK~OqXclT7 -W%=BKYddXY4wT(3uJ5exB#QPz!(uD3T2TatSHSzf!dIs!Ebo{*)X(shk6VtB2CoV=Y83YEfO!J6 -=91JkNP9c1MWzKCXM=;H9YglO*s--@#&oP$K2O`cX;PNjY*F5iW=ykyu_|x3DkR28X-%C^qQ7)yDN|IR5Ai<@!&ZR_>Ti -9dfX32}LLBq@R*%Ocgd$JUrWGpW%_;MJ5^v~)3=M3#TY~s3uKk}ohl!PP)o3Fib;|2Am%J7YPk%PQi6 -{Dea*1F$S%{P66TKsO{JxdDtJ5p(=bjBOlU(*+zviD4bMq6-Pf^nI&d*WO+s+Ru(%a6DOw!wxADX0Ac -2tu4MaEBtENjXRNhW-0Ggf0*e^a5JC;oLlT_G)qwl5gDgIh|xqsP`l(TpP6GLGG5#f{OZ*EX$0d>u2b -LZ=mCMnyEJd8f|-#Ws#DML6NtcXbYw7$<2ORS$8BDS24^_ -xGXMw$E1*(B?(z*f#Oypi`mQVEl&em=|*4Z%G&CoVFypTfV2{m1Q;`@)T -tRX_IX&O>~95z(g{GHwT!%OP}!}WA**^-J8olNV653?VpjXd6xv-;S6I|fsh3z_nSTrqG|<`@z^huC- -va1N0^&Tzi?_6D;qF*k>`Da=!CE*fJwrr*M+rCU?#UBk_mSK{yh~A8$hGxRCPPJ@M}RYRb9}q8I+9R0 -vTUZE%xbcQs4^KwMafI;YW-Dzk=Kj~pw`e>T1xG6QCC8s@Phtw2rHFC{82iK=~hv7>t&?E{{m1; -0|XQR000O8B@~!Wj@^V3s0#o94k`cuAOHXWaA|NaUv_0~WN&gWWNCABY-wUIa%FRGb#h~6b1rasty+7 -J+qe<`-=Bhwaj<;#s+&t+FuHA%G${h4$swB*XbnT5E!yU;EC~|5yUyXhduN6Zk$QN&cQt~n9y7z?Jbx -rLW*6UIuyv8jd^=|cwYm5+J{gTiWA?fzkCoi+6pLO@*$uHtL>Ng1s#cdCHZ9XK*1Z#$mi%RdRJu1>9{bA@(9yDC=4X_Ws@LHy^Iw#OgsYo~P{f# -}A)By?gcL^T$uu_z>tuo2uBeB-tF)p%O{LS&Ox@ce1coS+U*1%8&L#%d_w+1ElAQ0sK-1A^M2tZ7p~wY;-xD%{~Sj -e{+KK!nbDR9&WQ8S2_AL55OtA&fHF~r~RFXKQI4RQspi_t>yO(F9rv|Tiv;TX}RGIP41ca!PV391iZ^ -(dG#@W9-rv70K20y|n0$U3PN&xA37m8~7++2PrRgfi~1p6t|gCCC!Letk}?yRwR>oqn5{83COM-_^N|IcQ&enPf7c@ca~!vm>~9!XVRo -TuyD5HRmvd5SDDze`9))YJrs@9SQxz6GVXv`|0$qS_|q|!h?Je3Fy+f0L&_YcmalsaVGM{lb*2%0HOo -qD74ID=xuK@X^0kj#)pvLQeYm>=FbN&>h!Hbs(SR6-x$lP69T$RO0n7@99`>Z -36UnS(P6MKCTg37LdETiwaxfYad$?{(;)uw2|5flcXwKxRtr2g;!dJ1<741p2els{Or%jojSPkqPEtd -+yt;Sn*6}l3zYu_v%(pi;hCv(@7Ft-fd@kOhHPtwsHgW_GllGTFYr1m%P!a+-BOnJ= -wt;u9FFw(9M^NDo=3((0!mM_+_5vx4g~4EE&;;IJlwv64 -i;OA;8FP<+VQLfE#w_sF4yL@5&l*NyT#y*bpXN!1QWHeq0gfC`Wiv2L)9Agn{a80*WV)p*7LwP6v##( -%h+)5->m7*;P2|&*7@{7QBn9HSgp>pemD7+g28AIlqsv+2Sii#c;ll5Uvr`9&Kk%zXdyTU& -|ITu^L2WH(xA5@bO27!iz-``>;6|3G%23D{RIVr1#92u0`yaccrY7mFDFzlr-A?!wD-Q{RvJ2S-C9SB -AeF6ia-9p|s!81^9N8;mxG)ru37A!bgXwvtl*V<2c(t_K@kx)>o)ua_0O~dRJ7U1NLs2;d%M&DrdNu$ -)qDxhz1qfSM%4P5tmI|35n%?BJsvtpJ7G-of?T{U%fF@&!JFh(@W8WG53r4%3cPz9# ->y0M}3L=)x-c{^EyI;D|2Mt974O8I{))k4rNF1W;FKq*opoQx0wy1Q+#{ZXHtJ>ch9FzvZV1*6y;Bo` -m&ak)rk%I{$q!IBBOMiU2IwDAngU|n!WeY)vw`U+}=UjAo{7sgGt?O`2dlq7)E!U1loo!dc==gXAbGm -b>siA9Zo`%5H>|td9xvtoA_FD#5k)dn^jsgGucw}iI>U^RAqk(YF2+P9477#AtHMg~o8e)Y#76(`iA_ -LS%=J$7S{EpWIjXez!ACt(*v<>Psguv0lIq8Qd@u1J@RE!h2Y-*zhQ42HgWoRO_`2&uNQ_~R1Nc<)EJ -p^4R0H6mP7A;rJm%b&5Q0D$r2QXH!rPqtGNEZ5zhP`;c#NG*=0)TQil{^h0a5PmY5b-)4E5-Ux)EKmH -XHZqg6VR*SUC5=s^y~@}bc)2@A@Q54Dtd~F6HB{EZP2RUhFtm#DNe=eu#G06&uJD -!sM!`iVfH?`_eFL4G`S}*4aR(a7;U3E}oyXE<{MC`VblK#t|&#q!B8_=!V5CDCm)cXBM?H;JxEh{D(jzR8hVI4+`pPz^p_q8H@4By%*ozeRSA$V|R-bIC@ -pGPA%)iG#mcgSAEf1E*EuZnLXPqctcuGqz2yANqe3FtZ-bqr^|co=5E#W_rJ1Q*f8Oa=*b(R1Wdfw@}w-(^cFlqGfJ~p6d`ncRbDi4Y6p?dcW-kzY@?N -#@UL?FPOpKa`O#EHQyg9W`#tl<5^FzR{|f)`IL7Gi{;$J*?$w;*U@A`ccT)`;%C+miRV!(m-%>fM>Au -YS7w@(vmIeh9#!j&;5{iYEWjOBS5YmcEz>T?1}2Smzv!sJk5 -jPfPtE82X~C+Aet%3{(9qXq@xSQyGtQkpV@eEJLWe9 -~4kw^=(wv(-F_RI5>qBdEWZ}-%%@=#LgJF(~dp|7tHJNR>t-9U4^DzW(mcf!dKkDG$6~%2$AIjCtb~F -9)Pjm2VzT+eOYiYcm>>`y;{xH`pQPQ0BHstP8hBKqnMdrL)5CUJfUdQ$*t$i9}cCaVy0uwihMc)B(|3 -iybyatgPM&SPdP)h>@6aWAK2mmD%m`+dL&l9>60090#0015U003}la4%nWWo~3|axY|Qb98KJVlQ)Ja -%pgMb1rastvhRz+cuKl^($a$r%1&T<;NzMn~I#BB;#6LIkw9)&U(-GsVEYX=%Gk10ggtgtNZQO-2g~{ -q%`AfXqPRLK%>#`ZZt=1`puL@d7NaMb5_aq^m}|V7!5}3=e#(S$!05=|MQqVd-C-86#n^^{g!0mfn9} -Pk|Ka%uabyog2zneOl~=Qdyv~aV^{fF?!%I^mwA=Np-kWz{NN=EVZBV2$*Ph$+-*wEcRZ5_>xy&sYVq -^s>#NH^-bof_G5h(=>+5%mpWa`;d3S{mVccL{<~x?;EZG%#DcMg4$;IN$K;NeMW&`^%x4AICN^X7$Sy -qv(?+#+1UC6DBiXNp)8_TNeIGD*3fS8|b8WyFPzC8-hd?=CN|@#upAQdQ41at -v_#qU6?^fx7!Te0jhgF%Ki)hPtp_uNHsLTZj!xTt+9*SLX`*g^5VV119kiXfBy%?|>)FK9IB=X>(gMW -qtR+KpqTIA(L8Eq3}OH~3kAHM$%Ug9q2!!rRt<)MI|`y_k*Y&d%{0O*gVQ>GxV4->Rxd``PDAn`Xsg&p(wZJFGH2yD1;9$yki0g+eymZvPmf9JzcEVA4Dh -L=NDB^MFydp=x;so=OJ_QUK5*qOcbti~RgH8UiB0Xz>t5t70Op%^jPOmxGNwb9^2uqaGZ&}_ItVy%-> -fOaJrXZu8M;Ta48*#+5077bo5-d$ZUUoBo=F5g{(OqHC}36Szv-v5jq1>Y0|dH>_bC)587K7ZqXWS`{ -c;G6M}fAPkHtDoO3-d-=SFMoae>f)LJ0EVL@77Fk*BF*L?zsuz=1VvbS=cF=?0#8uw=~>KS4k~JT%l% -y*SExMeDvQSFwVExLYUXmOM~v%Na1S^E#6Z0fAJzY#>k&m*N&#aKW*|Var0)e@0|C~qQV9bm~&44fZ@0^Y5``rcaeQ6%_XxGP -}LtPf;mr0hOCB@}oSytKnCAvzm^5BH>Vvg%u%)oVmW*M~Z^^H5ISsw;9H|`(@F&O9EyF- -Bh5fX&Zb6-VizFJWb=e``Vg;e$e?TCQE5OpJe1()cXI9nyi!i>FrbG7&ov~~#y)1W!?a}l89B)3`QBD -wR%$a&}am+&_9P^bC=(9;)yS+?Q3$lQiy5@zHo0WaT2*W!H${^^Zd1DA?#=q%I^K+_JNea&Xe8L4@RS -Z3%T?JZ@}qiqga$aeSaZUN?vEg?nLhbcI#EOh;Wl3F`3&&g>COX9#%=!Y1Z``)HXVDH4}F|BN&Pkyc_ -!_6){2m1ng46(3-e4K$~XKI_6EohtBXSDF5q3aBC70}oSoS_QGkoY<<^V|uJ?wU2e77*)nIgk4Y*-l=y -n|h@#%{$>p5r%3S1A^MYqKQ!t8I7u~@YA=d?PDUAJRV-ms9Bd^66ii`X=SC?gpS>)mk{pfKtDkTgc70 -)0}^1YW~lB#4K(PC#9q8$?$3n68TT*j=EyQK;ePN~7T<11?!S8P2x6t5BcM_%59B^D9LZTwfAoS@6iI -FD6=@+!%yJCrtXNFwOK(fI%h-Jtt&O)r8J{84a4AW%KumO!FX9(+$*2~Tl@mhw*WujR;VzNK!cl;(SI -;NyU2vC#_J9!A}LQNxJ6$Kb1kw3TBl$Ze9q-+v@oobSakCLX~`zjcE(5DYgUX)NHXO44|#{dbSq1Dh| -GDxSCYVC&0I2m$p@!65Al*g~b5BB2PEVqm}7)C7&UJEexV-zL%4$b`&uka`$`(Tq`I71xeg1wccy1lp -u3RoOrMuE7x&YMp?+F+uNyCKG|g7IoN-+b2rdO?B&*H?Gs*tk8r23$~*cA-jXh2u_Ug1eelPGM&&+po -A-9L92#WCCYeV5`kqNcD)U=8;bV+9+n}{_H|yyo08MM80P6oR#Y-*WFJdypwsy|+e%rS&u07mK1i}@m -F|L=&;D;03MvT9&{^k|nCVT-Mk&uWa@!;Q2uJ{=?FKUB-xG)v~L*fu*D(f*KyYOz=S@hed&suh*y}zSVpFaDK(}eT=6=qYYIZoy}en9l+x0d^1-h;&0iq4 -P}4gyCC8!O_`W7|;|MnWVQ*gyjit?U7p7P-qi_bjPdi5I<#7h{aK;g^3zg77JW{eai8D{?%!US+o$I&&T)ene60l;N7 -xs{LlM8?-b}R!nLYQ@ -{B;PO9*O&DO}sM^E)9zlqRYg;Y~lBzodz^o3=cIL+pBZm8Qq1JV5RppNBq_HAr;gTB8|mVsI=aut=wa -+#)Pdl9hf^G>lbhla^~f(fK2YpiEMm_!8X%CF%N%=H+ujxeU -=b{T8N`n$dMEo=rsmtQ?y>%iWIBBl3Z2Y>&Ot{KQj3(k3cHK*CfB2>?vo!+c88tm$1wj3-}9V{83-A; -=U1*M8|2n02WAxQytn@wB*OfV4|#f0#uNW77j!<@HY_4)&1i`$%X -a#&`I@PILrwV7_-y*5c6X!HDP^sI!p@@CqS)-nGqS07V18=tVue1S@!)ui -BYZQmcf0laXEfg;9i3Nd;^{RJjF_;R6Pi{l?0!*49C`$h+CmJpsXm4im~y!gsUo9c2>9a$WD((*O|Nwm#F-;?did2fP#50G~GT}b -eQ5DS?8Cf|Kc;hFA2^3~W0Lm*h2`hDG9{`a*y1&6VI-7gWqc3n|&SNT0DmT@RU#F^H>!2T8uQ45_GSL -(e!wisl9)e|x9q&g1-C?vct)_En>F-$94bC2YdW^~So9RYffm4&FyUW-ms&}?Zmy2Wvn8YitlYWCD^- -MwU;_su)+Oi=L`41#{x8QklHhy&JR%l|){5$9TnSivecFDK6lp>Xala{4Nnv_zKJs3BU` -+mwX4(XNK1Fd<`~_O2caRYoG%I$?ad6z>s^Xxmv>;fq<9Cr~Wxof|q7p%vTjK?gCjvu{)hE+Kf{H`~z -6#J&ajLHJ~PqP2t{=50i4d3T&fbNLC@S%B6%EC42*PhJ9i@_5kB=AADu9ZdcLtwvK=TRrr(-$kOUPJN -84Db81c^tUh#FvTLkdjhfw#&`+PX@?PB}MS;8~koW^-N*yvDHKKUonMsmWU5Wj3q}9;gbU{48ehR1PN -VEKpdb!W7b9EDW1$Zu(#f`sc!a3~OaHBch?Q4Z?N2Y&^{5Zc>1ycLf*$LSk2(tB{>J7S>_>rJ^jc)v#rRt{a}(-D`G7zed=i;X{MmPG -;GGWKyuE!^|<|#C -dWG4s*=W$V#HIG_yt9V?R!%^5~r56>C~y?gpvWI3EOiI+%b@cg;7+MSS?s{9`(8hPcaTW-@mHxJ4!YaZx^wXTh&3Qq?MbOk3=gld;ds$I_6np%eN#_!m5T)!Fou)w12+|R^ -0G9MeJAlBa%@}RuCFSOSOS-3C6BO0iaq1_IIle+Bu@r3mX5z(b3NOb)VbH`&5 -3yHa9vl9(RaC6MMX#(Rt@k)m*NZBM`x+%&WqGO5K#`r`w9DAa?qpdQx?YyF@3&wzZfH7zIIXO>V=#C- -nLA!yYrDE3jIol5Cy(p0Oxsq!ekieVM*cA*0^dQK4#?$X=;;c6U_!nlpE*Mgd1#-+TFuc6DVGsmpKtC -*3!v0-WA+{@&PiRrhD2ZTwn2rv3GJ-1=Q_y5h|0PO`gBYln0+#P9LjAG6PvzgE;mL(ulvEmEA+6?*lz -LbvVJzFu>kJnE6Mb4x%pBz=cUAvIy!@Ft9EVeO{!7Cayp4Xe9i`}&d}*ou6b=_7X8{U-Ec|mTetYRXCUx%&zbJsF>q(ChES)RsiTNi@6Q -Rh@BTUk96F4q{&t|kcYkel!}bcW8U6g3JGx1fn^K#PXijK~D{T;Y#FbCGosGx( -PR≻!$nii0_X+_4bP_gCkmYWY|A$BPt@i0J~sWGI{X|h_H1?KffZ$VjqV1^&aK;YkszjXJL`VQ4^0 -U8JRTqPrcTQ66$JIzWX_O@E3NyIMRF4#J!9>yk|#`M;N(e^hZN0VB+-48!wsZV>Y7RB=-EW@1(@LZkg -x@*+!pSpuGL~Tp3&D%L;@4qPLb0IPxShKUSQVJ5@1Im0mGghst#+3DdTOIBLrG2AX*L5(_-8Fi%s+YqQmV#721i~?R;;1uf-P6{GJN8%7=TaqrfDj7D%-qJclwYgdwauC9gjc_ -V*&pY2e`wj=MoK;!LOTU^@2AP**9IJJ6t{NPD&8c+JNqQjvts;=b>^{-`uezD648Loi46uvFpVc_iqB=m3LGeT<6cr`PawkEt6@{ -XbAk0|XQR000O8B@~!WZl!l8tFiz90BQjM8vpqr05+q%M3uHr7f(r|~L`c92aotkIFauZ#B%Lf~GHkW2-mCp;uUu-cUhOT -nm0v2j%W5_#RzSrlRHIGttdnXiF)R==-{(0qy9sFT?fb|3dH;FQ%$zyrIlrFsoaa2xbDndy`mV -iO_)}Ge+0SVIoXr3JF9Cl!lU~VTUe0*us{MwA?_9Oif8U0}^$ic)+i>@f3-7u6{`()`3xBw_utB)L@V -@&CEAI3Z{`i44Yj4QT&MeZ)c=G(6H=KU*s>bBsohJ`8{u$meOIzb5`hLCfJ^Gd#UxfDuU4Ll&h{6^&K -0@Ok)!|>&->=d4lKbxQV|o9m?QR#tEHqe{SAO*VUCFpE#%RbgWHL+#eEeFA`_=XEu+c;fD;PF1jD<1K -^yHff8z>S>zcBzloy`;wVN>tai~d`0GBC4gLk!Gs+~3o{-3G>)!7z_~#xMo{!x-!zyke}nuwVZe{sw; -SW**)@XxAHreKlVWQ^+u@Z)jL^H-9(7WESgvpALUVFNcGAoi}J;%o{VYJcCwd;yZBR8yYq=+yi0SSLi -dd1m3LSyX7^kUH1S0{%4Mc16tl)-+`O=k9+w4|NDRPZ<*ZF++zj;*x532E@OXFat(-$EF-u=E}L5wtf -An*MOF2yz$5%fCvNfq%pCEJTrI)vpMJr~kO#9NF6?xZ>A9F_4H(osrpw95)dTi-2m^F{Bnmaj)vV91h -AIc#+#ySN;#|hPm?$|kM=;EfKY&qkhJzK1aw1e^kX>wXBver{pu8)`R@Gt&V$XD+e<4&Eat+v3|LS5W -Luwq58hgpTP>cPDBgQ=*@h=64#iUSG)qv5b3IoKO59|%ztHv -%0GmLtYc%jz%|Lh_=#0RU(oq=2%Du#{V%9=OjGrL)1}rt9#`n)YCpWUiF_SCtl-vtt#$o<5hk$s^UJ} -V-7|GQqL!}JUd$DGyzGQ6&#?aclSWzE2IugJ7prW^+jdHI&>^R3g5%#Z#K1o84o7{%cuAX97U%&!2)& -THRS{txx@(#-|&>HcL>BT~KvCvg~_93IaKfrjgszq8=&?g_c2P&e)w~r5>>-0Yk<-1AGC5o3&#T>P6g -_2V*+Yao8gnGvaKE28j+whfQl3cx5RNYv}?&_^sbwnE&t2b~1AjsY?2?=r)G@_;k26ckh@4tSoMmx{~ -)~n{RKp2q1S1J$s{4dk7>~q6-x=Cz|)^&=Nc^suP$DnXq$dz!v{suteSSp3XigXVzQpiO>pO>VMqBP} -b7v5J)_WnvNZf`oYXqDVvN1N-Fd)2&L$R%FQ1dIU#pa+X`t9cbjw!OiA!K&uXMu0?`V5o$A8vWti7`f -A5Iv2Z;5r78d6#$ey?9|e`6U{xRndEeOLux7NWDrj*5SLx``J{@X0yS?2R?-=&D6+W&>*2N89mw#gdD -At_Sj8U9>nI!|N+PtHv6=!V;VwdLRwVYPS>F=R%*8yChqRtD6Gkk7xY7s@F#1~DIO -J@OCpkF=dhQu|b4+tq{F0x+4Y-eFMLz&-diGK2-mN<3m`uPSV}s)h5la)CS1&m!! -3D=nskHUy$=8uvL01F{(KE|*t)=44)d34lB>$m)kUzWkg}B@5h++sD)c&L@e9$^5tg>n4ItBVEA?zyl -hv@E&BymTOtDos}=f-@zfRWdm0Kek_wX(~asK<`UE~``m+`VoEotMo$MW*3v5whewGmK@uIBPO*4iqYuz+^F -Ou379R2TjR=iUqr%j~zh%N>OONQ5k{N{VwNws+0C0q{i0|;p*WH0I5!+6_n%mWS*7w5P(s#V?liX5F$ -lll%K#4`;mIdIxiILg|NF_Tf)d%=)at>hl48M1ZG9yCB(|E1Q28B5v_Jp{rDyO>egqFG*o~MBmpy}Te;*cwq}Xsyi$y?x;h-M75@KbV1Ljev$_NXRDOT@zPv|d>98bu --GOge{2~zO%0A?iLmne70lwd+`NXUnT;3-Hrd^}<5FoiImFT~%(ddWc;yVSt?R}LbskM_grPl-sSWTN -irp-sj=H028QDJ6e@DWgk -19BIEINaqk+$mpFq$aRvrcj!AeB|kUTztfhe1!g-?)QjX@EEg`C;s8JZL17e+Y(l2Lt`LntJzsp0~IA -y=>4kBuR_3+OVb?1ZJy=Jk+YQQ^%dxw>O6l#f)BLD#NsatiWCQ3^T%Yi>nSY;md1K{nN?xy9dGd?#V|}&wmx`7=&dia@xW| -!FkE=~>!ArSUrX~R&xOgbR9p1(z2pTfu~%EK-J}M-YNWJe1&hVC1B@5t3uq4Z;o1oZjWTDykYDB+;71 -WZWI@6utFWZ%0g0u8Lb8Eg`J47iD^_+7`N6e->q=IxS|{bmReouaSppU;aCt1s@4nXBz -`r(rOB0MhhY>?Ktwg{1m0QA-EsVy)FJC0yJ8KD*q`6UAdv^UQ(i>)YvO-&SiwT#h-Hf%nmUzw;@|h+_i}n!-d?w -Uf@Q(xL`qa7DlJh=rHhnwc5s4)WaHhYrUhFk1Df}h`42h^KynFqnRV#E{i{(`?w2W7H2si=u6y<1*&c=}Fm{d>e$LKv(?vbml!6W?V#os -`Wa<1snG3e1TZlB>hJ&MpCLAy?1yN>Zu+OEUJ5oJ_BCtRcyktzwcBqe!J0Jzw-=>e}#EJqq-gnY8-Mx -g$Qv>6Q3RL&#*cnj%+HmIdo7a0XSbQp7?{d4p-Ol} -@r4RvoCC;zFqBXZs%Mr?~oeJR5AvAqzqfR%vVVQfWAa80&+z9K6pMgQ1!4l5ZaX=qWcV{Mbm`20<$XjY`L=&^&cTG&FB;6+SP~=KwsDc!Bn -g6bair>(76NCc!Mf55(1)B!68zTwDWr=9JHI7M8Hw+~l&bAJnn4L14I6ZU@nX9}kOwcpfM1d7QM7!g>j2G)OJ^!e8y-_~(Ebk0+pdUq@8~l$n -NNm*WrBJF$$NP_ug4LTq4iycU><a@jSQCM%JdPg7SgwSwV?vv;lIZ+5uQp#)*x&44(_D^cZse$P`yTHUc>l73F0iA9% -i1>`?6|&YdfBE|Ou0Gsx3zi-SvS827vkV=ic@~1p2SLREuJ|#Wo>O(sq$Y -CuXLq$3eUjeHtptGE7{W~rb7ag4qMmgbV9N->#6(W4*F;V;}aP(^%2js@yc -o3i*wS5gM#l}8{pL76M6#EIsM;kNb$T^`;jwtDJ6a&%X!%7xia2%bRjB?cB8rZY~ke=|8t~o%?Zt|D% -q#5gBWPoc7u0(O?wXOlB$VK>BNT_)K08_R@T{<<n-uvh&18K7mn_9{U^7T<{d0@93D+X%SOm?L=@Qd5Dbkf@GKVz4{S?qy)WqkK@}U-W -q3N=^TP>^Y7N+=!QjT&Z+D&18TYcn{IVhEP`@H1G6vYKsO4UJ5m#Vj%Pa$gYe%ytrFLSX1NXtiHkvC| -c8$gtDfdttz0aW*W=DErQ6lnv7kc)Mrxpj>u4V$h)lLr1(TcDw(?<>@fVSsPw0&qvyKyl<;=lJjx*TAZ}6hpLnfIY!#> -bM9b^K^Eq>9=NUB(oN92x?iW0|y8QIZ?LC+-Qfq>tP}J-*+gqCfm$C57?D -*Z$eAk5^ywN93grc`GO?x(C8KFpreIQCtP_Nrv*#9Aaf?TBVh+k5|GP?OUJqFSUCJyfMgq-kvdMQ^`y -(vRo{Qju$eZK^`LtrJ)y!j&ARYjllU;o#*FYT#@-U^3;gtTNmCjQa3-c64HjkK69VWlcq64E$L+Z~0m -AVF%H2k;@*S_!3Kx?e}TyghbCM3$0*5^gRMoZKR$8zr#yS^41Z3d~*D&%;{2f(KcAfK(ZF*RN?aT*j^ -X{~7qKg5b%0|29pe%`>3e4gcVUchb&>9NXuM1jV#ykuY&nI27#qMs%x}v+ -esTPranOd`Ml?5BjwM=o)z+1#Yqwq29|F?Soy*gW+q^r!|#eyeG$HML3rm!HEH_b$yNiJOjA~kXiWs? -sSrW?viS3(4TFB!a@VRn&;AYtdIjhs-Xxk)7k9kGq$8CsHXD^bK#Prm8dpNs1LI-1is<$$z_fgU+K^*31XzL7GfLL -lQqy+XyHdV8i)d(ybs&?|(v?F{ -$1j8IR&SG%nZg#>PF&qL5Km`YaHkkmdCT44{dvgT-in^07j(hZgN)^B~~_2Ku}(kTq8YqOj5|e=SXZPVVuNR*adhl6F@LAps&-xvCwnBVmo9Syp7mc)>rmv};^mYCF@Ot`D3k -x8!i`g|W3E$>j419w`vCE3_)JPh$R6&{n{@aO(y9NM*F-tEzGt5jlX6c(am%2buB~S`>LYAGaG>Dc|H -E#)|z|{Orsx25s|%2L%^cv!!Ky -(z1lKtXDCiU0a+}$fNaHOx(?vh=~S%ikR5Mk0IMFsmXG!g$gux1A~9n060~$?We@*0fUn9YK8%4+JI4 -$X6#4i`arnhT24ZA=%H<|pyxsG2-i|zKv^g(COq7uzeL@An@9?|mVR7)NEW}Oik~l%eExcoWb#*mQqB -c;8gM~Pa6vTP#as!*nE19W9))+-r}5WN0Ejxd;O}U>lN#`1u3iL-KSdLbT<|fv2MD!>CYDo-S7TyQJ^oQpDHK(;IEV`TvFxkKiqZhl1m5n5Kn^Ht#M#3oYH&LsFE#$(@Kv?+Bc<64bZp$tNeNDA=q1OVaaaC5i+A3`}OS7cSthB -j6sk>tDLO{Bz*LON6fJuSQ_Cz!mb-F+$LhuijR6j`HOaNG3SU(DbPri{JF0unA-`e*3qy3jC;+S -NCpW^xrEE;&Orc=OC5>NG?j!}f;8O~n#^)2A_Gut+HC_;s2c-7DIsi^Ty29G+LoRazYH*PLSvF4+HAMRCV_lyL6 -8ykPS;R5t$JXU{hbx$>Az|QB3OS9jREHA4H(Ir)O-zE{%}TGrz_;8xzr-vs<|APYSpeQwfo;gbB@}>Z -v01Ln#P_Q8T%23`JKmLEcj7##q-xUd=skrn|OZ2rkp_bau_i4JCBdVk+W2w4^B>vqXTrdh!|#TqhuRL -SMRj_iBWTzD8xm{x@6RV7G>21E%LcaO=>YfCNl$VmLayF5yD(o=uI^nUWn-{r6`I`sTDw>){peAB)ybzuFnI%OFU2 -x_a$37fWpwr!df0}hgKT+`+4Qd6I*V}K(CS52xsuhs)xRKFu>DSRy~cuJ)I-aNK^gpAi -pYf8%+Ea^1|)8p^EJ`$<>R3Ya)1r&y_zUSQS)i2!=O1w=Ixe3Aqz$75>!Js4{$!oOXE=cKk(af|oYo2 -V@IvLZ8;VC>qr04k6&Sx$K?|KkgE)70aJQJ%TUvi_)mR-O5rZc52jEOhWX$cc_6YMIB%wcH5-z1X+OTu6$KMS4Vcj-^F -580uVkVNw3FCGN$G{mxrbG?4VWsVHLCNKruJc>dyT1f$!Q7G1YZmu~EV)i2(Z-@F==#agGu+Y0@vO -iy4Z&laZ&;~q5x4%*t=!Rt6k5>NZGAM?XuT>u`P#?@!ADl+=bil)AW|`OLk-x+}fBOl0zg<<2ub_(7m --07q1UTj-H;OZmdIZu{^$c!Qd3^z9iT}(k=!vrE*9-$KMfaq2MeLHr=yyZ*4C>{xz-;`un4?(s7UVQPz157T`wH1Ji|MWU$Y|AyY+MIU^=R(^j7| -@ZA#<38$NI18uF1a2RST0+Se4!w@fXsTMSp6eO+0ip)nThhGPCrMK_vjX;^*&pP`l4k2m%Zmc!DSB(-Y8*xeI8_rr%>Tf?4&#G3)b@!#BJ*sevG`y55&m^3dw(>8f?EJTNl2dGjn3BTXoP`2h?`m>9EQ5IKmN_Zui^R_y@q(syx`+2Y_OVdQX-X8}YxB@PYRdSV`mWe= -OB4c4+{gxijDrm68ODDpjAVui2Ua#}$3Lbr8pZzar>Bk>oQ%ZmACDlV+uP|S~xlz>nj`fsN;HcD|@r2 -!_{wcOr0+}^MPDC=r#nhuF$A3zLC$84*@9tGVaMOG)*u&9g&UwSo~I%;ezOl5t={oz#w*# -Wu3PP=jHikG6J$dsgj3POcKZ%8qd;L)x*fDl>aCz#c}3>OOb(hs5lSmsQh<|PJ~?lZkz~b`XRNtOC>w -fa56}ZrwPiD@+v=!Ie8F;TNmq?Z{}KjI2462>B$d9;JoMzp5j_6A@TrfW-uk>i)Wt`r%=a9@-lTVlU! -Y>xeZX)jMM5ye`=PDkSqP=+B|U+*J(60)%iVgInE6A!~V+kpu~QO+N=dE`O3eGq0nv -M(4e0Em}eOq_UfVVv?0pTSqjkbDu;e1=YWOWrnv{NB8r5uT%F}rPHenD$%;b=^VYY8|b8i8TuBZp~io -i+y3&ZukNv|xULO7t9{LEaa>ra -O3M0)ACyD^ae1>e&)5M1d7(Np)ocxa3)M{3M0^SFB3l<5nR8pYL!Dir&tB9NJ&@@EW|7Xntu@z4MB=I -D$@^-YDpszucf1sQN6`c3W+p%d7QCF5P+$C?{uYHYyuTgHY=OY-#y_k#4LmN|#y#3|N03II#*k6qR>R -v35Y#EAw@FSc6^7gQnu>vwWjmUyV0M(vEpt4m1{789$c_{jIj22@Qr!gr}emqX^-APye@oMSpl(sAPrT|{X&-S|L&UIL2Lgh!h)zXR-q`BVK@?peZOxUou5aIt2d?I~Z@&Yk{`^>_A -NdDxm4C4%`;Yu|xF|6Pd8AcyOP3l4&|$iA9a_J4BBCIyZd^xRMW2*pjyxoqY0#V~R158Z;_Fc8K|C+x -G+_k&s?l9iEyW(n3GxPJJx0;B$e#q)|Gkivk++4S>etUPz_e94&Z^Os*_~N`R32KR}ABoUVNl?Q4 -ei^|AIfTl+eyeL1zSL)uq`_O)C4s?xsxUHe*Dj>CizKgS3_7t19Vq^*ARckvAq1XYdWgXHwVFr53eQF -<4h&LNJfj9b@1-YGuvJM@Ealv5wk+-8-|A**;RBS0b74Ij9P3~m%zv0UQ+T2+yp>G1tqHXStxTgflAu -<9F5Z0J*%L1tV+$!%2>xOE3w*f;-`vTXl4eD9it19#HDntsgIK9JnMI4(iL)VQ!HgaAoQy=NW0y9Ca6<0VXAajALFtiBLty791XAAm9NJe)F}qxf^H@E -p!RU?Es7-IIHZ}Fe(mKc)x--PzBbOsR`lSk|UYh!$dTdv6R#znH`Dr#q&n1`B^M{m{$0IFIAGng1@vK -HVmFMi?x>XwIq;48wPcFJoo^d=O54@^OytMTUU98C^YQ)h5v4s}EI$x}>!4& -p(qS4(M2(pFKk@fF0Vm1&_fTt4;dmu%rW7Ek)ct)4^MrJMtby-__bwwoP-wV@~Jt0|U0MlDZc{rDyil~dCIpGn(;Q8nZ>H;Ae -0K7%n{iW>+!>B<0QF7^8SzAX^b0s#9LLfRoWJZj{GqkRJ>r$aGDVB;~fW3b^HvHWqYXH^|P-j5-|=`~ -pc4E3ML!&o$D1Pc95WJbSMK+tE&`AnT1040=Jwx9ssf_@ykHbbz@W&3w@W)&N)AeTQ&$-EWK;H8vDo| -HSK+GD{}yk9MCK^y%^l*iwfYEMgzo#NMK2sFRr0c^H)}qOA6`U -@3ET@;O*e?J>#KNlTS$k0~$4T;1yd2hEM$Ti~@}gzV~;866R>ZnJVG=IUyq09`FQK-UmJ7X{GxQ6b%0 -vN6K7G1_b7YJiRr8Mw6yRC5p|14!p_EA=9-=uxs8$Rg==sClpbesH+cA;i-2luspN(PR-|h@^evC)zN -6qJPpnB(6D+lAF;)tL;Nm>oiTZhvjP4VSRJ=SHQ$Gonm5i!&mg&fcedrzj6+3A0Sh{%vBZCUoSbJ>A$ -CHq=ROt({Yh&S%*{8M8+2C0&bP7i%^vV20=n4GCYNG&YEu5Z5vjHL0GjfkbmnDE%8PY;3H&qA -$ycNivJWa9Y61X844)=$m1rVJN2z9;h_H|h&+VN^7;sKr(U%1oVo|Q3~@b+l5s!x+X@Yge>G(Ak^8E( -10r=E{1<-+ZTWp_-YhCbInMHx;-JdUwTIiR3tD$r*g-sWRIq?&o#kk(ZXlje5G!zpf|%xRt-VOLRG}- -49^qBw2^YNJEvOL`W1L#~&BMRwu_j5APJ=Oycms0P@NI3fazeq8*fieZabby -2O4IaMZ8sxm=f5|i2Pyb3<-tyMS5rPNI<>IEICULciX<9w0NnCAbwD!TXFcTVg -BO`OrB-5XuQI6vcCxA<8Rc!fpB~7LwyBa*TwE_1U=t8;@t@%C}uC@%c23Ge+SRo5g_P&PPa`~KiW|AC ->BrK6LX{H(4$pFkOrOB>=Hm0@GOm`SVmDbeGBG-bNm0HP)u7r7k>Kbrd-10N~DfuiGLD7br2r8e2b*D -8gGB+s?wpW^)gQo;bpntiN8Je%6N1zz~)~%CfLR3|L9>`vEe5tnCry`|DbHs#Ez*3w_hL|u3W(cz1X% -jyk+M{G5SS$@W;e7F;iQD0akV?l~%{xX9{jOS3cO;6AH~rdT=d<@CoBGo`$Wv-SCcCE0vK#1c<0K&UF -5J)CPGA-~v`7;gaC*^>@RsKAlPV*xlt3O#>8KKr>z+Y=9%R!NOrj&j@-Z(J&$qua`51-p4yaz~qUlTSWI%@!! -bghoj6UHA*Cp$0P3n6lM+boWBJqv -<6ImIA!Z=LRQd&y}V^$)z&lk_d)AYV)PAx`YNgzkmu=&3Eft*(NMV!aurL!BH=>fri0I?$JJ1FJ^4(M -z_dR2Jl^H|C^sl~!~-;d4RlwxN;XMX0CdTxZSrFsk2m)EBD>aryPjK__7l3>bkfowBC -fD#$%|6Ci1ml!I48mKp&Bqh&$O3m1D>e|$3L!U0kMSiT~tdK|E8v@&?fhj+tXD}p -Jw=65#2Ibt?u5O4w7zbzA^&xxIHABuI@<87TJ -}naCoSoyLIIpm3t;E5@X%asl>)y2HG=!s6A&sq3x-SdgNLg6(nIfvcW92l?hg1q&*z}PqcpagIHQkki@H|d8Gp!J&1h2!GJ>*QJUJzoE>e+#?f@(xxtYqC&7q7ag1DQ-F6rvpB&0k&7nQ -5l3D3&OZ6j3H0yd8U#_F;Biy5DY&wzCDcs*~m{m!M5Nr5;8&S$dE-48g#{xpz~TK!?=}isAGBLdMH72 -;5`l+tlUml3wqqZf`_|4l}{EQ!y~`=7npr?z#E -M;mQVy&GP#F59cNifI5HMd~z;jH(baE3+Ls+#Oy}Xr7rD3O|SIEXwT@!0H={Ma=-dc%#ztW0}kdJF=q -UJGHo)fAvNICMN{NluI(jv3t;?@a1dLZog3zG*Mlxne~5NgFa^pIR_70=_znDcltCL(rCl8;9&Sm=S5 -CoVhiVlL!!14fT$F~^+GVVv09Zpl**^rfLI*=9IGqas3jhw#jMCbz8#6#1rYD8dLgrh6bEH@X5V?Cem+%6Yn1@ -^ZU48A|2H=BUste^*Hf9}GSz}V{$4inqX -WPlem=xTZX?@2)>uL<+43=EBTFe8p@9BD+>*z8N~DEb2Zu8fx1Q@7Qu#4uBol#=Ou$<(ax_+QcW?r({YG$%q|zd_$WdARN6@gTj#oNKZ;rH|DDw -Sua_WPUWt9(=@`v(|}wH;)Plqe~LSemDZmMi@oFr>H2xF5-+(y3lH)|p#EP4AE2*zL$j&2LPo$h%!nG -C4e2$j^r_J1G#CAJ_D_X$JaSwJI{#LBK19gFZd`tB1SB*$M_pv*_8R6&IZ7_oO}cDi*p>nF4}?F!(a; -iEATOf>9~fLNxq75!-7xLPlb%jMWg$b3ccz#QPwUCBMOU$6X3ArSwW&Tkrv*K??f0AvR7D=^zj`z5hG$;hbJ1%$$`d3zKxIs8Qbo=*oX$&grpuh^PyKVvRdSU7N!x9 -frt_J61GQAGQ>$%Fje|^Umdu6b8*~r8_KF-!ChwNHwuL!U5!gk6Z*65|&Z*@ML9a&8aTG -K+2?}5)4eaX7028Vvi=kU?IbEHRrsfe{<=t#TR|OJ0!nOVwb);$=_sANIOE*;ag(}j_{Y(?r(rrK&h( -ROQg7#}zIWwCanS!N>H7lm3!i#ckFCM#!O0~ej`W&5{c$}fGg>8n}tn#mt%H)YG| -ZPjY)!ZeFA)lu6EiZ&)Z3*)3}=RTjJj^8#KZ3g)H&>+0$;ir{Ac1oU{uJzrCoir+zHUl(XjuyLmaR8u -`T<{p9xm4jU+8u3KEk`z|&5v)VlCYE8dqm;Hgn8R2sJtQP4sLIjv*|!r0os;H+bC}DTihWDKZ8evY{G -wl^n^#mm7mOYbO>fsr{a{k{)(HROH=2!F1^VjCX8D$+D6G->(ZR5^U+?A{JMI6ai=o!T<22(!xFqWjJ -!^DKO2b8#kIZ12`-2pRp<>c!*1@ugT7&5M)IDLvAEA8R`;L>BnT|H|1{!N6!Kj?z@50DOwB@laZ!8AW -9&y9QSOO|DGUWk3rCkI?LZS*_2U0b6x5XwaEvGFfh<1433NW2A5kx5;Vm6uBaD1}52c(wF*4|gY?z2Q -=7?pPLN+x-*#epL^d}Ng3+8s&j5?JV@xrx#L?tUIl`KnA$+UX0F_FptA&|YiUNRx2B#=@ta2^6v<@EN -7w5v0f>yUENHQ*p6ok2u!-4cpmEj~jl1lpe@pm9h*V_{D7Ip+Q}B%mCll2I=+8W;=;u(bFuP|u<53s~ -wL?Xbe#|6Yo7ZhQ|Ni{k-YHWNSQf_GtHZ4VP)IY_tFP<#p4{2i#y1#yoDZvza(Z|yfchk%AMv<%&Ne$um$5BI2swl@C4Khx{2rq3WOZslmOwF;>p?4H2@T?`mz6!7#a!BO -(kgo8D!06?;jxH^-q7QEH1b?4B+*VPn-tfQzUNW)6TvK1!rn-Um_e-unI5nFb~N%UV>*{+=O4OTVuD7yI;}ed5 -Q>*EPf>^)2{|Tr5zd3Z!r)Sd3-ST>~)}7-^#p-@`F(5AulQDgp`inNuf#*!X=b@mP?iSAmu38@~!B_I -!CI@2R#C05~FtWf^&s#5E{BN=^B)q(Sl^d4K|Q>{-&zU^;Y#OJZ+ed*B_#LLJj2UpKEpUY2rhWsYt!e -yg5rA(P~r;=rCu6rGBxSoc_Bm>I$Zew5Xbv+*oNcUU4qNs_pvul54iwT~*{t -se>vsa+bpUd2WY9wI1lU70AEW#hy%6C448ITnZv!}Q8FnFnQ9g{f0dT+Oc5TXrm+e$G^@wSKJa}UMj9*W^Th{<>yW&r{#yt)(=bl2_`jx-jBy#lO&6{cri(GNHpud}0qG#N#WBc*)(w?-j-H^ -2--T|vbnJf=&g2fg6{=v5X|xc(6;A^1!Jfru=*3=rd9g$-hYp3!Av$NjBLZlq=&g1aqttM$gwHVa{m76@Q6Mw%-ES9mwxJ--F -gLgNFo+OtYA*pyd6f!+Sxq;Po83=OrJ5zMd&fgqh>sv3D}zqf;O~!TXBSI!QNr}6-b -n6sozr(U&8HMFrfLy>lZVIf;h$92SMW&K)^eE^(A-{&SBH>^-7s$`MFSt+uZ@^ZKRvhy>yt -Z`Os!zd4$Y;`AJAec*o4Uo2Bcui&`eb+!-grQ(rPp+fY@g#2kQ05#r4X*s%5@^yz_r{DYyzf9% -W@QnakY5WkQ|2?UCXawQnaIuqmBAR+zjw-6u`W=J%~puOIm{;&|J7ap*|hY8Kh%vL-vq6NGD)_q>#_o -kkTIkp@e&V!GL1MN%)j93UG;$rl(+yJTU|`E_hMka_epN5T6ce8z`}7izx{nPbkwU_v -^ZX`zh(SHO&I)*9oxSYOq>O;z68;fN(ssrcF1*VJVCQZg6I3V|BeU4!8OBuVTfDJAEUxuR?7Esh=ZVw -To%N#kvIdEG^M0OqCPLgy9R!?!r6CFTTYV;FrpqgX*?VRf<~qAWC@hGZdIs0&JEpO251%NnLDB4S$0c -G(+JBu^wftRdZY1Yd=*(T -1Sw>^|8`M3IUD;Bahc>fkCcq!WvOo_IeydCosW_-Atw!yzlHTq=1KV51%Gk)8|!y6!&IS9mT87o!A3^CF-#p+s(lbg{TkeAUQg0Z1NU-J9Y=W=H*MX-^ -aff*Aj5cSz0~iQ)Rf{7-gQ)XDdj*wP!+!r9j1!^A*w0I{~8^xia&m(DlY3_>r-H4RP>)w#XY2c**{XW -`v-?9+M|a`o*4ayinbU{D%$_G?6N~*a;+_RN=Ubd<7o!ELCy%{$l=2&y=N^1wX0swFnGxf-r)rh&kjM -%PeI@v6Ta7}4dK&`d1T8F%p?d>8BD(;*7|~W9M_37f4jVlEk4QZGv9p0t*RZM|8&qRHm@Js$N#bOnp@a|1>?^wvqx;s -qjxkpqQYMNc1G&4R8~8{_v0@kJpEfJN>OAL8i2P%`Ru4>kHd-c3?QjqOqBD61zu8yJN_ZQGI`P48Bf5 -xz(*FGq89raX#%}U>XbmeR@Q1|DWzUwfP8qXM@?w6fu9^O`be(Ucsv3$*XwhWfB!6bJ;!Zg)Et_z6|a -yJ-c)H3WZ*eSor*z5oWjH`O{sHJH7i!tMOAu*6*)>b3~#`Zs8msf*T^sw6jos?q@8ib7gCL}?Fk -cF3h74$nR4E*STksNU2=7pRfyWR9I)EJ(K#$U#%acjSIFl0xC^%LY3FJavJcFtuCv8EkZ5NJNU)koes -q%%&Vb`uxyN7e|4`S{Q<{`iYS8spn^XgGgtxspHrNC$JLz{uHy{~Uj;bYH#=vhr`S*B2~;Rb0*Denq^SMi?|_a$r;*xo)+zXf>#U;(3+}q6IlnR3mR)D -XuAFL87!apU3jI{7q)$t!v7E#xP+aBUZTveY%vb<&P!5oALYp@}DQaaeS4!@?R#u@!nHqQF$o&jXR~v -obpGL-?*Qm+))0DPb}YK)W1Xcom2iYey77X?hdFM*92JgM2)m{jgpQXT#O%et5?uH;o8&zNDKL -hFP40v>0%*oX}Xvp{2P>HZl9+zI0d1bTuU69(Uvy5kT=iH -5Yj4@vx}6!(1c_9>In^WX$`cg2K%Y`c%@V2+@?7z`SaM?t8ue<61}V=P=hY4K^I*Hx2r{Siq4>$pWf&;YD&1y;RN~R!o`?#jvI)m#$8GzvC0_e#iPUyS{Xl=u20L -(Cd@S2hgq2>n)CYk0+dNkJl@?`ZTejouB0^PYwf0AkG|vqb!O< -{xBt}eta=4)xwk`-I3z`kXr{m0~T_V@I629(RtCO`HJ))bFx2dF5WI_H=9Qpj4ZPxsGbyl*jlCsrLPLmugcoW3Kn9`Lh-j! -_#iEK%-qQ_(>Z6wqg^kvo4!3@|?@YIMW$C!*iN9H&v@mvdboRnTn$aAafXvtWEsEfd|n>Zrd?EEyCBtfU0RdXUymH0@fHTS&zzAeL%wJ0e_9T1w>7HZ{#XXugA&f=LrQN5I4gqR#aM@z6UNnUTWB -h12dm+r6bH}Gt=eHc$oUY%R_q>xI*<6kHRG6Eq`|!RuvAW0n0^J5PWi<9aU&v=@>8z6Vyp2!SK5S$mU -TyleDxti575U*FDhNpviE9rEF8I4D5XZ(a|k5lIto*(2WA&40x$u)ptSYC -Bk&MVcuI$(LnwwJGw@TJ*Y=@iLbF3I$ot)lx5zX9kuE%#bS60ZGWz&F`!co+=6k%}j1B2Zo_`Q3{?{_ -_WZ(9e=`p}AT?*|Nen+Zb#(v}FSjw~7;fZQBb$Fs$Wf6KN>8gNXNt2J^R$1y=WOSmcX#?UuVrX4xG=C -=@l0y~7qNc*8*6K@5YcEx`R;$9c{zwzo@;p!vAjrtC4;VdA{;$5)&p|jY3S-I9KZ5u-3#A_34T&28g> -jYc)^{@KxzdTTzxp_-RE(<^8#~7dqvY6AOE6`wcG?Lj_b_!Jnn@gAQ&@=E-S1MN{%KJE>o`RZpzD3G( -I#-R>-3bEV9BVqDW(jU4Tx|`%n{)$%8|JEYeABP*W_}%d60e?wJ$S6rA#v;Ol)@z8kBrks`lfxlavZF -A6UR?z3a7vBcT<{sq{ev=wDUouV*V^IPa>^whbVRt|L;4A#7`Od?v==0d8$p#dZ!8u -Q7B08~w{Q-*2JLDU*D?`zJh-SyKE<+QhVU#0mYpljhHkkyoMe7fz=z$$>Q@y>YJN;ikKtK&lz -hN3q1=IX(w&$gAY{|EwowQP~NE&N$})92eV-)d^S#I&-)Rg{~L$6VUhvz}4QWo}{%FXJRzDJR*Snzl( -^5-=ev0n(X@9ESe-Q691?;3z2{q$hT(>OZUpIKxcx$Xwh{+KIo=H&g!8uwGCOBAIhQiit0~psMtYAo< -DfK^_j|`lq4lxoWq59tCeM&ewmtN*G1X|31m(pZPsCz|;Wm2cfu;)qmZptg(QH370!{8*UDCjt@Kex1 -`2_;eP2zgK|4rnb3cNplkT;2Qu)@7zlx|q7?3?_aH$1WsTkZw6ex74YR3R{`LM0^?1s>leFpp?yzF|1QDE^#pux6Cb8|gjZr_7x_7lEP1&hDIF_anlsMY2*j8vnWC)orCqjki7SJ*d2`8C580*Wc!I`yh1umdycm!>)?3k_sR^|e(mVm|6=ej<;gs`H>w#*HP>fL_j-Glgpmu3w&0 -5uKo9>iM*RsIL!mZIq@pXT6;rf;tS=8==zg*@jl1odd%1~B2&Et -Uy1oIkg%;q<-VZe%tv}5qSu$-f1WC=tke}w0p{v$lMrhbHH)}MepuJ{q2A3c8MkMQ6x#el5ZS617{zv -QD56wX_m!bRGR6J3bY(r#)|)c`0jIZ4f`+6@&{ybZ!EzE*Y>V&qGVc5(BgyR?QE49`g2BIU+}tCpDSrb~@o%#WxEstLQ141m?`wM=Krw68m@CSX1lI0FDJ2&|&-FGg -!%n+aaO_AY0`-Fr-si0su6%-6nK_OB>E~Nt75ET?46#xmKPP=XE>}ItWozG_sAN$!yzdQC1{Ng+4>kO -XaUkRCSbq5#^As^C}9PxuaK?*3KXdHp3h&*&5T%%#lL-L?6j@iGNug%`g6 -~6HdhMBg?thQ4zNN8kA*9p5tvLq7{i;%E3SBE9rIjS%{tPP*v3gmlvP4 -04RV3rGij&mv*^o=x`u3|r__H#*7hXjBDxj=rnNGxWWXJVD=X5~A;=BuL-ONh5u)Bm#Y}CO@X{HDoP) -`$;W*uOrLodp)V9Z=Sg5dowAg?=7TMZ^B+w+{iT;yB7_Uq<{wTx;Zj}26tdEjRw)&NiK#kh?{t%mj<8 -2;9qI*X$*c$gU@2{T^jri2H&8;=P`I7l)A#}ze1@CxOUMX-rhx?q(O8lCm|Zd>sQE6Lwb|ZR7QB3fN~ -ePj|NewC3Q52Klw~NG>BY=RM8;XcS$)75)9ru7KW20b)53xicOI2(iIH0Z?O%`{kn!E0!+3WEj7CNIRV9Gc+9pos>TVlW|Na5)D5MuRIc_&E)( -#^8rCmCNcbJA1^}$Y~A>y&NvxO-=`F$E4wksdSHMq>&cmT7uSj}}wpuuR -uuKGtGpIV?^+rbCOF?XXnnF?+Qbr^8aE$NWZ%sc={p>M_62VyYY#w;t0(V>T{ylrGhKe}9|SdtF{c6z -H~a_y+NUwB)A?op>MXMFHM$R49jlLxgH~7i*sY1{({;);Vdgq9O$d}eEY&UYC_Ur -+&A)81evyxtDNlS-dXb_u-sizwtJYO+65l5+g{kG+F@8E2Tb7Sk?Czmui}rHc+#mR}r}?`bONoTqmdIge`h1>Kmb4=Oh-Ttmq(xQg!5(n{IM-Xt#8{-eSEg -CXd~AFK*y|PnG=xiA3i@g?kFCMH}G(@v_um>f(Xo!b>pVq!#B!0-9IMt~;tO%vSDdGD)5b7m&sAWNI|&+h+ua2=)@}AlO2%iC{fw2wQsA5%&05y1VG^qWjNu|C#P(bT6a(CAwdtdpX_9>0UwiiUJ -)Ls+hJ+TdtG_2hn)ejX?r7GhncYT72c4JC*s7)S6BK{7!Wf&EEQH?63wThYBa-J8?BDczgW-IwmZ -boZgV58d^2*VA1`cU{%Gk;-Tl{jMk2LU7#hrQ^I%Fqlg{XR1Cq8LE~`&vWj2{tfCTO|wbTy;GQyJW@)NE0b#0?E0}kjg -<{w;Ji4w@F|mQJae6&e+a9zp46G%)^)*80DgM~7vGz+W-H1gmO`fa}ob-DXOTi=7D#a)w!(Ig3+0uuRd8Ku+4LFuVSMTiX~35OD9>SgPpqyn$VR6T{JAj4g}{7|7!(9boO$c#TMgQ(~n$^)BE_KOt@a!2P3Umrs`6SuIIN2<0@sbs`w&s{$-nIYV&cEi|rj5HU`0F3;1-+b5zV1-oUzobU0x}Er%2Cpn% -1$BX1}Lh_HmT&|F?QK3?qcij528e0vslVTgFyZq4jGJPiQ`qi?u%ckS9L57Zx_7`4`EH;IR&g@GtRh74Jjc@8iV#a -OHj77=IiPZuLzzNh!iD8f^tX&M23}4?#uNA$=PntTi|qT!BYtIqVkZb88oxp?4PB(^?cnog9_wLv!j; -shVhh497oQ4lmc(>Me-!Wz5J*1J?qVpm3M<-e^hx!H+n?e@5K3!(|_X_(uU9^hezoV6;>XlpE>u~uW?&*l8 -7%z$TX6X=|sqswsT(a|IM)7$4fp#8;5I*v}%XhO0icDwj5yQJAaN)sv;p@XOvHBbdn=1^2WFl91lR5x -CXCqQ&N)3M9;_^P$ -hG;)t^F^0h8WmR>AdVHmVa2ITIZ~wgY1!)S(Z)k=&j8OFxup#U_%Vk7iaLgW{WQaCK9qUK(QfXOwSjoby88NqTDoQ1N -OnVN_onw5;dS7A9hwra|9k`}$vuO=a5eVqASb5c(=?YgTM$ymPiC6<2UG^@o+{2f(s4bmsRwhp$HZh~ -EJE?K{s`C{o|>!bUV(8`mpO!^(&2@u%HofB(~xVF;&}vXk&eOhGGYC!f~-W1FHMy%rDeS0}QIdobF3a#o}G8YwVr! -ab+v<4UhJ6n71z1khjDG=9D%-Zb=I~*mr2tf_x+8YIw3J+NtF|y`2|$Gb46%9E5qqhbVN=1*=aVftn# -N+5_L#w*oJ$7DXZ=(?Fcx1%(psYk0K1CqAz*ox_JHh?G}o$e%`i?)?I>th|K&15UR85Q&DB3kd6$VBvAPlB@7DQd5&at2XVMaystsqN|EopqE-G~EV1%nj`YNKO;q -ca~wIT;n5WLy&^ULT*EiO9|ZaErFckq_SKyA=@)OD=2i;R5JqhCHm#y3@pJdS#;f!Y?(flJJ`%B0_2m -OEyErnUD0jNWBCL^!+tfU4IW-Tf_Fiz=QT1RMJJTIv|koaBKyAjsoN?2OQ0Xo9p%asgRR`L?IdSBan| -s)I-M8dT(mOanTOp@5-$bxRt=8kH7=iIKzGWiu?=^S8p=e8)A~jcZ(Ro`e-wDxUob*J{!sc2it^3wxVTAZh+{P>E%%T5)Wr!r_OXVh!H -axNgBRT3O=wR4xwA});$MdH?kv@zaH+W(Ly?^brxBm4*ZEP;HaXaH?qIHgkKf*=_2q4A`=}?QT=h{;NM*_qkJtK9 -&l`^`o7lfsK?zjHE~^Ds-|UaRa)+Fed+dgJxq_KCu*g^OVIF1jO$~giUu#Dp+}4>+9-W -f;CEVV0&5^wJrSd`o6j;aQ*0qtc$`sZ9eUS8OY7R5|M^mdFxJ++MPk(DP#;sQv&@ODvet(7M@XjgCQ( -J>RXWh#NmxanA3!si#f(S!I6rbdwUd$z*V_kjTO)h$eKv#hqs_stoSm>F?!$6E$GHfe&hQF{x1txm4F -eOKu2(lOvGICc1}20-3dz7kbK*N!asl2~U6&0^j4yDDH5E(no!mRhcXC^1!v*8Jv);Rv -O;R*W&@ojZUVC|MCkZx0z@ -93hY*cJdEV*9A_C(BojqQnRw_$tYpkjNXmCE+S+p_Hm*I%?f@n9R3?TPi*Z`bz3oSSS<^p|W;^tvtE6 -FotSRP`mDs~ewq9%nV_v|8w@qrpTGy;f1Blr^=nm@C)QlAYIy=V-M-}s(N4KA8*{Vei51u -XtmO%jTwRtYlmNH3Jh4|$Bk(D}z%5&yu*y~^CMZ@XcB!mRAY3h0Cwd9169`bd)rrO`s}pzqb*mG1=YN -`Wm#{j~_{wqnr>t(X6LRVONxKvIw{3R<{k^a|Au)#A?gWbB)^;bRR@$8ycaz--Q$&)z4^s?J{Px!kPo -N$C8x2paxL$|hi573yVt4|QUrmN5TK=x|E7c88AfKv+Cw9Db`-UfAL_!#z5Vj^1!xOLxBMeXEwonH(weR1&$?ya^B4Ky}53UKxm}3gJn+;DGqy`&! -dGV2w#NXYfM`)%X9=r|t1(#|Rv8$P)cs~jA!-5Fb?^P3fV55np5PU$;_!SdtO)!<l=zxX7`DK6(*3q3iXBXAy=Ygr1(+@$uHB*qWj2-w6(_jrwKQ4uqZs%M3CBB~!u -OfwH*Bq3$8x19?mM||9EzRCr0HWJO&>kwsqkxZxMl8eu0VP$Ju3FS7xWji3}=}IQcN)`K5Ym*q$O#>|GemDs&@qRma*+~6WN@v{Q4qrVv#M0?a4mFuxjN8e>9;q -3_dpYB4Na&`lWN!zjRQe^FyV_c*^nfPtU|-L1{vpipWRY?zJ?x~QN?;P(+N|#+ -U#oAQdvG4MKB(9#Eo=oAVgXpTZqhALSU2fAuBq)FDUg?8TO$gkJ}(<*{e1+w4cok;UDz8cmY(OqEXtx -yV%LsleuutMzH^O%pZ1-kH$1gxG?mw=*wlJFODEa+%`c?TSVr>=5>YJ1K9e=pyPdgsq09_jSRF5L1JA_W*t8A>FOO17sYAZ9AU0r@tanyXc-WgTqbY -Rn~VY@D*1Ek)XwlGWPSTPeCKfe0sD~Dq7`9y<4TW9BRKVN$G3H#8ZUn##&7th~!U)G7K+3i`s3x4u4By2fj-I(DwPChYB9gQ{t^q-sjCmqXdKy}GhVq$Mev0 -@TVTI78Vq5=C0>ZFsDqq-tvLfuw2zxe)GD^d480K)JgFN=`x5w7r_D$-hWeHEk2BCIm0pE)+O;)RnyE -e%6${AXH6w7OJLYh#_7}^6SAE&L8*L{GD2Ru}-NRAVO`>kyM%BmDO-886)&U$FE6xq3yC>NO<)XnkJC -NO1diPcBqo>B{|(jB3)4eB_1NhOVv`ud&!S_iSY6W>nMA9vejfSPqQ|+m(McfAJQu%3B|`wJ@N}qvo+ -RUm7QsfEqO_=;O|@VtozwmvQ#AOZ7=U_YmsEn@#ty0gC8@45@kxuncnCOgsacn_g&*sZ9Gu -&dpgBh*F201ryr$_)9ZY5s4Ax@d)*lEl?cQN$9_rh=Un*OXcP&N5LQXbbrV4UVPcwqF!E=JqAu3MCUF -NlK=Jtg_^GGPR%0TXuKu6ghEj^V}`eOU1g&B9u|8dgp`+$7*D1$h!*N(%0?@798yxeio&&6)*YRu(Yu -!?0Z0==Ix0UDs&%)ZLrtu^@qH1N;7yc7X7UdtfhN%UzjdqILzWjqgk#F84bWiZ7nF>lY+BHb5r;s!;$KcqCmR#zcnZ6CRaIr15Nqy2RTU@|t6NotIMzE>`tPaakCF?}Hf=Z}nix!*s>(3h^*e0ummJZ`imG!$(fF}OR -#fd$6jeWBE>fOi&48lHPbjKh6pAX*KhV4x`A(7B0#sD7Ze*zzUBOpG6V970)Kj7eg?b8{oaE3#JtaC? -?S9dz3AC^16b3Tw&qw<8-CQK8rB*^M^{q3s+!<3*@;SZO?x5adnk`P^N}*I37Ia{~Ug{W}2Nqlvilqk -%-jq~IVjW8G{CG$H#%qd7sj}Q@>Rew+OsJH!vP!9#Qd|b_&9khsYZuwb!_k7B@i7CDCVK{Pae-_zAtzBN -c+h^HwQ(rBCoNya**}ltk@Hr7vle=3)^fU>tGkArycJYA+l`@1KC;0t`bXaZoAcEB&bFER;?Aa{B~WW4LI)PQigvKedk -ql=c@L-&uE~mHaCkcxksh(r)$wk#T7UsYOc^aZ!s3T_WN(S*?T@ -HaAcv$a|Eh5_%;(xU{ht?u6TTrw}upqE(PI5_s8>FUg|&h>ebv%=jHLDC7kwmT>n -rWtD$-(`O^cS2A@fkTY52zsRp)ed@+)Hl8D{R6l)Ksh_Pwf{#}-w3Ndq82RdK3+84EN$i*T -ZzS_3}K!}eYTgZZfWickuY?1%uwxTsG@gi>1r*9@u*C4Lprg<=Qmr$xE!V@oIiHz^y28u&QOK5plJ9u -3g-%o6NW1f7nb+w2Y%gmvfkD!wZ3a)0Yf6`fNYmv6J2U1qg6@>L -cyotgH)>+qybXbx2n$Vz38ZS|wi&>-b48)ZW0BxQ;6ja1e3fNSE4_`yyH&WDN5e}3#Se#@yUpOiT=N}pZ{N+!QAxA?H;)@?6egg1g!cR1Q9>PynfdgX9P}`CGPpuC)_t1RSYADrGcY^o@5xzrhm+f -Z^)>h6vfnOK>Y!qLuS_A~I)ghbl@C@Mu9N= -Y}&R{mhgSQKS_-mD9?)^+$KP%%=!3;QqhgmC$?#!H+rzsO5g&q?;QgvP8tm+a*sOaNq)D4$U=aj~hIf~cmWVjPovggy- -W?rRKJ^j%3GZ!dq?kPky7K^F~q*|=XbJUX53AmyLW2vSNWC>z|X9fkUY(5E -%6a@5iMhB(5=HNz==T;dFGB_Cee+%-wK)DHRCVO9IyAo?k)kV8z*@ewQSOLtxY}2RKbIrz9d||z>ng5Mw*8yx!z{fUD-2-TAzt}s% -P#`|GQ#cW$Bu<^a6ts`Zp?-4Z`H$12P3UMdG0x_3)IkM(Yc;11h;jWIC2fxnx?vp~^qqB7U|~|uyJYQ4#Nu)_GgBgj@0no=zJY}${x{ -@CTWPYR|8Sw__HjmCT0FLC-WW}562oww*d0|g-|fSFW&88mz(P}vDsY`pf~PXqv%IQGy?IP5PO{a&zK -Px;PMLGP{XNM`;Vxk`%`x_8EP;&2F}i5N&wP(5dHWA>lrKHF+GPOL3f;7o7V_C)u&n47Sy!9)j}3=IKtX{N!t1vQ*}KDU03F6g2~=PWMuaPEs1TtsMm -ED9W`^2y&E3M=w_p=?V&CV9w=hao(b92<2<<<7sQ)x-U^N5orH}Q=>248FVd -Y#$3uadwNV|@44y>=gJwvhP*H^6S=-lLOQmn_8}IPzLy)i(h*+Pevf19C7dI0uj`|!O7gB_Q!78wad; -7~?QNIrHz+mO-j5N66J1EON0qF;66f~`{t>mNY#`Flp=MFz(oR)f7v_HGQR>P~(B+git -n|rko0|^-+vY9Igib0(VRT@Hj08OsQ+X;yd0Wk;UIoud!cC?Z!3Yv=GGj@&$(#(~#+zUy9@8KLWkLp -;1sNzOt3cRvDcFtQNZY#ngVqS=(W0MlDBoS`{BpbtX8IQ7_(s~!+fN&;el%|>@vt^@-s!ljV5Y7hM;B -qch+VT#5FOsQJex_HqH=;@(ZiXCV1RT|BI65J(R{*e6DT9q%*hD+kThim*S_jF*CeZXeQ< -A%-Cm54pdcmWejE2jsGaUg63KJ1Cok0PG$FS=#x8LW&dW|gGl#}_X}7or?L-omy2A@%X&BDInM8uPiRo=K4H0w>-7lkK7)L%cy^yb{_H79s`id`4aQ^ -T8RYoHM?QnxLq3Dt|0Bl;n$9bSkl%Mi-aubbIzm3(d4Lei{=^~V&0_E^*(ld{-&QtjYggnuNuT6sUfL -SpXH_6O=QZIpslxTvVI^OjM&7_(Y^C1LVqrJ3@;GvuPxqaq7vC&|#Kxij81Xzwm|2=sk670`;cm;L); -8o98|S*1tB7Gb=l)V3$?)NV$>NaE>h|=6eUBJtly8HB)+qEj^L)04y#3%dBfj(RSpV!|Z)>Oeh1T}=e -H#0IO}`&4Uj4qG<>B!|zy0)7Oi%mzow7B+8I?x^5B57{t%r43Zt=MCm9uC9L@x2Yn3n#NsVYp#t^UC0 -z%cW;4$N1qu%Oikn*hA!^93qV6sGNl`}sCF0PAqJdN|th%IQ76Eau8-c^Nm+uc=mgI;}l6fxLK!C6va -0uKx>HxlY5pJTNN|dvg5VA>3QN$NU@*aCf-Hhx>ucCag7x&gnP3OOX9P -bJ+#vAvrg#MX2x1AW1d9kZ6BH60Avj6!6TxMIM%2l4BnTkrPcVYuL4st0nFP56PZF#rc#q%^!Eu702+ -9fS*VC|e1osj2BN$9Dj=(~&gy2PjcL+WuI73iQa0lUUBnTiFNHBunAp$Ew9>E%dw+Idq{83*c%5aI7= -B9EjYHec4!6x?o3KQFM(8LHi>8~Hf&lX`4<-ab>NO?(rMz_lx`Hu -IgdGc!?_tbLn3*a4DfDX*ZX;MGn_10mB!#k)yW$c<_bhTzhoV^*n?{gL?zkf9IZe(N&lwa>$~%j~r;s -0TOrYnfutQ2^VWzY!6cTPqyab9@=_28);HcoG;G^K6Mg5tlSZoOvDNI6!c%*!gKGH>+NDJv84T?7hX&^r0A`Xj5u-Yt%2_lWbmK5#6x(5Lm2q8VbrQd6ZeEuiwk7;vu?yu!|6}tmzF))Vm9L*W46X7q}jyt5Y= --?T3Y5*rOj(Rv^q~OZ+*S`4SX6lGWa&Wqe;_dciz>!Ma#QewZ5l~v2DBdejPe?>U{5g{$09u>mJbK{= -lBSdOy&oZ@-}a0|o}0LI#C~g%2JwG$L}?@Td_ZqeqR788bFEZru0>Crq3)IsT!9DN_^8Nz;;3Qm3b-X -Jls0uw+|pGiS}7^Y9~c=grSyp_!KO*(ug&OXgHFYCoNLZ7T7Gg}B5_lqFINK>b(*pIC{f(#hQ@TB}8b -GYUEyvidS#52yANLx1Wpl5Y(4N+Z~i8hp?uM-oqlh!8dTkEZ@DfJ!@1xOAm3A%u<8cc2e3ik`(NdJeP -b(~#S6dIB|x!YxUpnJ&U66D8U7lpuOUsRu$ogFetE+D-=b0Y>^w5LC|;DO9Ef{}kbqK=D%8!*V#Zs`{ -eUN$IDE)|o;v69l$Wo<_>eCenvL`rAyAEBvzPFU=i8O0gUHLpO#DSOsrnh|-!yv>GXgEQ&i-u3stjG`dUFDm5kfq^nA_dOkgF!B;oew^>!*p_I-T -xn3-8AE|^hMX8}J{X6)gIvoLSIux9ga;u4g#A@;HCz(QtZxb~-MJ~mu;8zupPQjGs_rD?KxBSM=v;2aAVEsR^=mLlO?1stK7FJJX2F&uTJNITcndN_i`zs#aTqEiS9mN`YqG{EB -sP5r$kZS33BR6x|Qt^Wtu7Yp|j{mlf{^06cVqw3OC%`9s^YMbNl!qgv5iWvvl!pt%6^Gs(x-iT}Zo-& -6O0hZ{ptoRezl%>Og8e(uk8q?ga)mF^ECAG*Uzb2QyCWecEtlxQ0kF-~Lr1#PjS?^k+-Q07m6Mk=|glGSi}!VIQz{!(8qhi> -AJvHoKIkx6BWQ0em6E5$}y(?w~{qThjX-BisX+}$GDinODvq}ET++*!<+z?JukG&*P4($eVB3;!A_E2 -i~gULL->@bGX?T)hHtR3!y8_K6Q8f)huKIqv^j6Yc-9?-q%5oPhwCCkN`q(1JV&{^ArH?OL{*Nb~eCp| -ER;+yXxmBy5e__qq7uT)d@Y2g0UwQSlO|QT4&&_YX_4byn+qS>+?tAa=*tu(WVbPxAy&vq`f8gMUA07 -Jmlfy@jetPV)&%gNc_*W;sKKaeJ-<>*r=KHhfe)!Q<@>A*0zx;Z>?80vsFa3V`O8M14u2o#W!T)8&MD -2=)+fq#Ychmpho&UdGF;u(%|A_L(*-s7R9*VLBRnIubSdkl+s-7QLJug!|FIPSPL-qWG>KSJnE -6-b1&oC5TPfAa0zZhaxbY^iW8Ht&*Vyp?)l*|mckIgn)MovjJPqjv5BxPFCg-4teXLv%kHDszaWu`eg -Gs~7G+@yCY85Tt;CQLIA%A6g?#fEVMbX+dUStN`_N8_XwW9QsOxfF|$IF_88tk-+b&dQpVl~qL!!^*)30<0}yOXT1LYeJgQY_Vioj42 -t$v;5KJvX`;BlZY34+uHPe`xWzH~~XPc+mtO--npy(Z&fd}{{5uwI}3}ZqT1xlpw(E -L&XbW??~m=h9>NtVoX3N50u_HD855JDIYr7{^ucgBL`vWU!_LU_50+Bm~zw#-S%m}X282;JnLZMIrf- -YnaiNMt42veHtfN+A><;g@N%*6c}sR46t3Rnjt*h_RZDSrWGxZQ01WQ-`$dPF2fnn%PQ4mSQzd%}g{K -Gn14Ukoz(3mDWAF3k|1*?o-5Es=GYfQ?BsYS;Tl!ic(2adgMk+_lfm>2_-0$(SHx4{oTEat(NNYTJouOH0d~CHSlTP1RdNoZ8Ew20pS(v!U{`jh)Risdc9$8 -O`Zg);agqozE0v8JQ^(u#=8`d)a`h}0Lqq#}+<)dT5Vm#Vs -wK-LMI{eiRTNc82zwi2gf!Vk}kaoQc%Oqa)9rn4OZIm1d4t`o4*YDcRPE*1-F_WhIKv&)AD -G(*ul^Jz8_#*pQC_-Tn(Y)b0Oz=eqshzPE1wj{bG~dv>Yne@Nv&v8#z?b*<`uq+0 -yN_t%~Nq@dc<$Gv;-E$ACm)&F=k|8GN0?&n`r&(*`N39t0qqRZhC!2)lJXSqa}Y_Q%0L=rK -`o5y7Or{rEY)wjJo}wm{~WTKjhV3uFpMcVsAWJyZ@=DOziAaRsE}%vDUg6GX&D;1Z#2}n?Y}CDV`)rNwQ}fS*Vcflt}<_ut>{hvCtr?q<>Wxmylu|oN0-nK{?GV$DKw -YGt3r>9h#Yuotb71wOZ1`X;8H?b|(wT&Q6(@A*V1VvocHe9dZErp(IMUSL~a5kZV+CqAkrlm}Hg_3F$ --z+n^l3wuhY$Q>?(u|%XDV@(`NH;Ta@$g<4Xqlto$$;fkpj>#H4ZPMB%F$vs7)!a{X0W=R`^^sCl3rkaXeSy|r4bYnA; -1sRFqv!|M~5RwYsift|%P#(2=Ya_D5%u{UBrkO3#7IQW#{Ckgx3_>>{E#=|rh=*h(#$=^rgl5_(+)qR -^B*^H@6wuB-#I6|Cdvo1rYueZh68;j|$AT8AHfeSyXpIJ4>^$X0p{Wq3n-e24Xljx!xUN*~P6;dS$%q ->g8qL@PD8rHAQFyLTbi@+IiGmMlw#q)xjdAa5*^g)UewPDybMJ)Nhc{87%`3T9;t8siY?PjgyFO60s4 ->Zy6GqU~D%?Y!dxs<@T11U>@QkC96Wq@-k%p9_RB-GfQh^`po=uf_frM&X7K$W}9i@^?Gh1(Bo40f^X -=Za4>#7|d8bz!j_y_G=JDM;NnXx3~hDW6uSb_%Rg;!2?qCOv=>hGTlWt_eWHGa|Hc!vaROyF)5T!wkR2TVvo!19}aOWSUWs3f!f9tOM+z -%;rOZGsnKSslAo2@kJRpoBQ~kti4ZE8IL+WRTXr%M8)QpLB8i%@$52|T-b6=yBx9Co8EnR`Ewj=t5*a -nmtOSeMUG8y+tt!9R1WO907I7>_@<|+-B-Ki!N2PWKSkve&wQg!-jQL62B4Zm@^N{<0{wnMp;=XG%Di -7bA!(#1KbF*_5e~-TrxBp!K_b>3%h#ySvzu97UnDUN~ZWLb9%6I5`v38Fm8u2L@=IGGF!mJDQuzLAkP -rlce{TitJ?v&%=ZjjUUmEEvk;0_t!-d1++CBKKr@1gR0v>a!&>>eZEm`#vHkVr6*AetbIz(mlWAb_A1fiFQlU8S6+r*}qB{ypJE;37ChaGc-( -!8-&S37#a#CCDO}NHB(AI6)Xee}bL_T?qULS`joQ@FCC7 -mCkP|xPtciwA^vHTz_%{>@?W|9T7>+q?;3V9uKIf_!>qVKym8sY)?6VPDekhCA&kq{s*5J^oLis$OP8 -kF{m;7B=Kp8I*XIA9>tDZaweQHI+NG5*C4N8|@gV{D@mbP*v-Moi3Aaxv#xBu!6L;k-qit|52S&pUKSy -d!jnzl-jer%|9>R8DicV=fk;sh*BT>}D8RgFDiHs0MdjvsDbGc3)JB`-)oJH`e0*PA%>SYH>eai@U1^ -cZ|*DMP$1Cu>-;XXSh6MDp)Y`F_Mf@4l7qv!A8#y2|%c&uhe -YvC8|<7c}faRemr4XCyEQER~&AHDBGjbz?(@3}JC`aV#Yzh1qO2siJao=|7jPSh0e={PN4}-FM$*g@u -Le%P+rVH*VZua?{Nff$60TZ$9ktL%BZ8uz%L9!{YTYKVOmSQ!#7S+_?yFo*$|>%nw~5@BQ?yrX1!n^D6p+kqnJBCz-Lzd6;iX`DbYi^nTXNt`C$?vlc>&p>8DXBtw&q4S@hm(>H@0Z^zB!3*ET7h>;z -k>X6QJ5Sled#^ziU@z`(BXZO!xfSKcgSCQr~Em09>$^~+<;%j;j7=#+hzC*{1;X%K2&k&GSa~NjnNY$ -C_qITrBCmkI}SFzch#Z8L_!kEM}PeF1x?ONazr`aujk+2c=(~i6k-XAOJDKIvpxDq0mOUx$A{Oq+jK~ -T1(5sCA^mwdMHe~%!vB+XV-A&99L_Bi;SY0ueeCY?3gM0LRK6vv%DM0s<&Pv>k~hK&I}*nwZ;`&hP4Z -^?Me-$ivx+u_Ij6L_!NdY>RG=l$S|Epj=Q0g}mcX09K+uMR&wXWHzZIYN?Aen=MM -bfgm>Bl(!w*;C@ny@Fu~%Pxm2KIwMez6+UwpyNojd2|^DL-aHd7zEdAm1i;*qRJ__n0oENA!m-AU6kS -q@cE(r(Ukc9+HzCrr=z_4oK~^pNxXxe1(KOPY7^+}N>WcP9<+`5N8hr$^2h$@$iJKHt%4_k^+WEQhb# -y>%?odv__)&*A)+D_1r%!Nrd-Hor|9?(5x#Gp!7)4p72)%V3bo31wsw02z9Nh5dwdu>vTi!-yo<9gdpPT{kF(&tob~#Uv;LoOHt}Q5LO -wnl)_0h7F>AH*em|w -r<@j`m!B6cCcf`o7ut(oNX=R?1K+JU%#@Xr9rv+b> -l$5Yb=YL|~U*)W9eh7EQmGSsB~Bl;-5G(3nlr{B{&SfZ`9N_)!#}(ke@#_;V=!Vv4_-;{TK47g79ADgHTCd=fsQsVQfXuz~X$Hq*T7F -!euWB*bvz9gGj8_!BAq!xaA+iocEGf2NAh8nI}07q!{8Y#H;XX4{(`CeANo5sWu}kntW_jE{Ph@foWa -U;Y;3ZysR$=y$64MvC8?;t!|zQz-syiocZNuc!ELQ~X^Ne?P^iJeQxO_~$5onLB`{JSZBSBf7@@kdkqsT6-E#a~46S5o|!DE=0TzlY -)U9j^fXt_)94MOB8=M#Xq5nU-gQYF11r==%A26Aws{;hT -qq@gI~LL9f!DmLk2}gM1+M!h6V?RgvEF5+^JIszac}~D=CCUkT?BD&_h^Qy#KujFl2~P_K%DT4UY__< -ikTlf(M1fcR_#-9on^({Uf4GA(0`WVWAWscy;UI?>{iU63NJ@NaY%Qck{dAyLFRN@N3uB*nSA&Q~HsS -5y6r1t(&xHPW}|2vmBsfJjD+VLFCZL_`91lX(9Ow9R1oE3CRd|`ti-0Hfb_!fC%6(0<<+EerOncL*U= -EN$X)UYQyhC4irB=I3#LBWMp_`d`l$IqWN8S-Ra}w(`qq;j;2#q(8M{vK8ko&a;& -w$|gsQ9Rea2d-eg}Wj`dkzZiuG7>XA$y>#BEsC7nuz$m-F@qMX*7+85{5uADl(WDsnS2>fnEWD^|bms -BFHmz5b<))o}r8MJtC|!|M*Y@85z{j!`lS^&|qTMNLJ~O@*xH@HE7&uWPE5 -;a1iktW9sy!Yp8GIzEL4mju9$vq#q`{$^C)A(4dH_-V*;%`eDR~5#hc1Rr6M*Pel_E(yy8~i>!J@RrB -V&FsIy*8+?iciNe1}p3A8LYLw-2S#ULZu5LL+k>^H_9xddN`Q{;R+2iHe1KFaKaJIq{%C;;FXPcj$&m -K5HG6nT>L%-xK_KYMm%%=O(Pe09`d+s^5di83ycI{fWe*JnOE4=;o+d>|E@4fegtnhi^%j`LlH#U>3u -y^lX_Q@xo2-)E5neW({GiTV@vu9apX({{p=bzbc7tRaW;P>BuXVMRyVxjxfX(2?*m8c7y~)qf_+6%oPXl5!&FHgeuwB-L;`gTbCW;?P@h4F -H85I99iocrTzeVv6QvAws?mu$Mf8><^t#V3BxesXBvZYwMY9X|sEt_}f-n~0aA-6QPY}veJ+xy!1Hfn -Sa@mY&@ojY{s)S-K0U&Fl(>Ahv!_MJLW*v7tXy8CzcXRX@Z*SUM^`rR8-fJO})-qWgWhtAy_)bD(c>| -iu@zOTDp@6(y$+zef@g%{HcAl?QplY75P$>*7Q#K`?qS^sl9*q?*90Ii1NSB-`~&QzpcNZuN7TbLv7nuTB*x1FxDSR -IIRq|DeE<=FbI#05^W^?5ROZDNCYv~+0Wj_rEKZ&Qh}$O>wm@dSC%S(FSbxLYtyC;3Kf9Q;yTu=SC4u -YVStrNN|=(Mo~DPGg%{#z+;H{{8!%8jZ%kfB*j7Wty7H>0nC=%HhwHAL<$d^Bo;Kc5F{~lvShKjN5#|gbBgD>7wVmzx -?vc$7vqTckkZKfBf-BPU9*6;)^e!pyJKu6bBkSZAdBXYs#y@ggT#W_kPe?R!xfF8%4JpLk(mp}U+9A3n^#`syozC)J -O@9nYsuo#J1A{k3=pO$w0ABksg=mo8kmP)>YzjnZ`e@y8$hx8Hud`1$9bzx)07-+xDWT}3#e^9gEiy$ -FY;i2G;pCwU5axdHeq02&lPohShO6*`n>1*+4c0O9Ngs&hA}-2s0E|Kj4}B~*{?h$r$u!=62R1peTU6 -DLlHIyim$w0H-OXcN?i#r@c^V*=1NP#?IX-q3as=cA8468NJIsr_@*!;Kp^xcZ{^Yvf)5e`>?`{~7!* -U%t#~7!v&7r%#`b;9~&r|M=sN`TOs`&#BHi$;_e+qx?}H2M-<;0Nf88I3U`}@#DwEGiX2`0DeO|K^+0 -NZNGAEKEZkLC!F^@$a(hiET1SbJM4s2OZ}8fkT`J9v~X_a(*At&~Yc{?c -U>j=}FEjDk@g}8T_fQcLVTOK&cA_{z^MRI{?4LearcfsOQLd;9@D+GNp#yCkdUl}eo;}-9f1U^Y!6((}0N;bJt -J9&-0$v9neRpUJe|c7azRos)Kc78N(12@F3FqTw8lu1Bd^q7W^ea_+1`Tq1?uI^z`qU28C$%LS+Pow6 -t#mD@3IBHO+PV3k#(0eLH`NE)FUA9i#fj+G*I~)-=+3{KqT`?P54uPFZiF@+EU0vXmbi33N4@k<3Vk_1^m%ZZe0 -+<-*_a5ze+T0AR5+MCH%9`bN)~%(ct1d?wr(~(I-WIUD=*NgVLTsgVLVSCv|>bBy*)Y{%B_yUr5e!18 -7j7Ivq8&5rr09+jIKzx90ZaZ#*2tUztrb%pAafFZ+pq`G-V9Jkc=WN1{R1o+IV<9CA$Rllputjn7J-) -QMzlb|{1~fqCQD}i1##%)#gCE9Q$PBpN%k9gz&F{({R%>OujuSKtlYfV)Bq`hN8HnAf1+#8`!~4cD% -PefYaX!`Auz_~yAmd=t@7y*)ogebVyu5WZykAigLil-s9;@|?sl?zBC~zy0!vpg}ETbdzO_+VH3TUhq -HRvlMs(FWi9>@J4$8-b$UIk5|jb7=J+{_`i73K)#b`c!y}%GLLAWwt)5w8eWuXSSi!63^b$;;*TX04M -|~qeqy+w0T=qDekvKG*9SH8Kl%)eVUQyL8#iv`uf6sfhg_t9x=!k9C)clE=SQAT;6?Ta_>Kic1GR;>i -H3j5?HPU2TH8Rr+A@%@$O`69W|;WnX(8N6G(1W)EF>D{5e>Mi%NUc7)!hF8f8qtf|HRjMpykm=ALW}i -Z4&g{4jl?Di0eA~Hs6!?052pO-X|JFdnOvtCn@cD4fRQ@EW!MlEE9i%Xjn=#EG8QAiH{bT#lcOYPYO} -V7;l%>-2VfAh5v(sf;s{R;J0YeA~y}y>8OjA>(?vz>7r-(2TMZvZhIg8zTBSQQnlyxG7ZnkH2j13XzB -D2?nsenz$Ii18lQ&~to~7qzaO!iuNJ6t8h@9M8Z|01BO~KO;EMK$HZXtwd_jYnziw40xT8%~pJUZ@eY -y7yw~T@ITp+jSYFC}|5Aa9b4;?xb^}t_wi{y@qELE4IJZgc6K(mTCIG^k|m;DfAh^ZqThimR1+=08)F0dD#${RM=>A3h4y^- -B^$5!gXSCSX1d$6B4ezS+w(GN3%A05$BrEal6A%?_;>BvwRcidQbuB8Vkz~bq8?s+@kKsziIHAyiISgn(e+oXtScp9KI>Y#>!K-*9QXi%U!EdcP8k~aF*k5^COJMv95K1(tN#^=}Oh4SrBOyggFw2fc>{r6*3-?y!QSg~RS&&$iZ -xP19?zHs5f-yeMNL7tYDCd!@Wm4cT*1KI`X1a6?CIxWyaU>pYDV~j>yfj$EBNVq{pLfwIu8ZILL>bEG -PKdb*iWNN!>va+(id*qQvZUE1?xHti5(+Umu-FF|KHEWir2aFktZbzX5_yT`L=Zm=k?r0BaBWMrR+dI -Z=_1pvVXZ9!cZ)jWKf8wRJ3l=Q+4DB>BGLp}oJ6F^LXaN3$1`Xol#*Gv1r~}ZT0BBI=y_kzZz6Rd|Pu -#&npiRwNkf{)!`jUV6`T6zUvSrKTm_z&({K1<5@DlXRa06b@IooVD(SAciL%GRh;@!G+6ZHUp#Hmh4O -}wSF4do6#LVp0hLVt|9Q}i2z6PDXK?w5HuttB=F(GhYQ|X -zka>IA7za)$20IoJ3#*fcgUvTAIREh2WSIm6G#_*FyVfK(#dPyyt#?${l&d|_qv$0=Hq?4_#7V-f#ylsBg4c;!`0@q^GBgzLna#7~jy>fWH+!L_0vcM4f<7Pz -UIf)$$OYkuK^8^?~u6=*1U?=r=uY6L&ZMYF<_Iu$s4l8|VTLp)EZ2*khdfEHO^2FW{yC;-H^FUxW+%g -ESJtmT5yMsed5H4{F!H-5&W$#cLfj*yaZmyJH`OuuB?xcT)X;Tz+K?4p8wL -=vlM)ZF$i=h`hN5u=<6^Zsp&x3ZriqvKl|*nf}fQ(3;^AVZXR+I-oZ1<`T_NsFKe~he+73p{_1g2EyJ -i~o4V=*ux8B~5g&9RJZJ;H;4So>phdx*uGxRTnLa&Qn>xL*2j)^`NZDq0 -Z13VJ!h`P@o04Qyzb%pQ#gnwf;w$OVw2;knbV?;2rC70Pq&>`}XY<{VvJ??G^kE{Lv1GAJ_klxC{K%` -cLX7-1;BTfUy(u3;I9s0mdVYohW0>IZ^hg2SvY&dcZsSeDt;G$7oI=)-{OczZ-XfKX|t;KCg}6)qN5C -6}<(@88m{|iH~lOjNqcW!noSW~ -$o?;|)!aJkxi5${-0sjd&K?LuD7N%jq(F+b95Hjg5CKaBc(Y{SO3GsxV{n2Jx4_BZ96&pgBV&@ZGlHt -2$}rf`2T=Mz8S{EOcNE_LAmT3dN~dOkpNyBXA`%h8{a%=tU@wpRfNV~y=r`C|@1bI=T$FQh8-d(3l@FXU*Hi8AMb+@!v+UiRRR(t01(%C?l%q`Ihbov+3ZB7g -PXhw6JCP$n3^emQr7zif{cdd#J=4sgmZoX7tpt((T2k=A{c{R3)##QG4{8T-@y^Qt`A24f1%UlT~qja -Bc%K>b4o1}+$X-d~w6bYRd2Lnp7~519OuwC;)ZHo=d_rL{J!@qiz(MuoLbtP_s^NJKrLb279>+iq?*JgQr -V%tgm2gLacGg`OixnEbQ7ps@ -6YbGKa&Fuxiz+ShPjV_aUEyZs3A4!2Q#8DPr9a>s8RJz9#FoUy#=q&?c}xg0;e2bC|mfaH;!ftlg^lA -8qU5hab+M{y$Y&AHci-b32p?`W}?UCD&K{!zZG}Isx?4n`Qm_dR3diI+IggC(Ds>z$N&R@|{R<@=DeA -uguTT4k7ca*ICqScPOV@tr;EL^G~tHg0;Fl!ePF=)~LE}>i>|x)9Flj{`u!)u{H#G9C#@6YGvOb@IYC -hKY|}Ftlz%zaAljQi62$GR%8DU8GG*Bxfzc=_EdjS-Fd~wcT{?=1jy!7lz& -TEl>1`Zq;Xfm1RfR6d|=VwltGG%{2KmeaUeY(&CFI~D+=q{AG56T>Ol)tjK5#>>ROvZ&YDcq;TTXo8R -UAuN2OYL6F12NWPT?T7QBxmsn6DA0{YtuDXuN!3?SN2v?^4RxW_Xqw|{>Yo;kDsuX2)%JcM1;`40*4t -hW{7e}f1s?9C_JF(kQIKQaiStEEo~a{ayH3G_^=yu{#(td+Mukc?3X1cC-1`=4|Mcn$ByNw3#_lvxa? -jpK)EY?sO%9@?le~YLFM@frS$^M&wpXkCmXlA+9*1HqygCu?AWoF{$$=Ff8;}bjp+Yt!b6dP-+c2;zIyd)p({q;2%VF%ruY~07WpF|W&8y -%fnG%hN83}@C4mEY3~LUD+r2SMzq$e>*x*^fT!DP|uq;ZwCMULS^A#lK$kcUij9;_b&q4*RA -@uTCa*WxxuG2RldCi-0GWM^|!n3cua!o_E-MIce}<~@ucyvO>KU}kD)KT@WPfCUwrX0^-;fW*s$RrFT -M0qm40W_rcLK5jm%eHeO0(q7~Q&c>mt^yS+nc)*IyU)sf=N$XKK@5sN-Wjt*EF7de&={#wp^zOK69f8 --rippgBm)*U~v^Wx<4B -q&e{R-)RJz3G+wHUols}TqS_!N-GI3thHiJ4&1^2$oG}`5kkh -fNOQ2~E=g%%E{?eu=6;y>Vcyw?=ADpXAX5Qv^jo+vhk?BK_ER%N8tSX`nxxZ#9szSUlppE|W00~3_5D -Y~g}eoM53(L)O~|^KZ){rS5NTk}F_iE?{f!?#UaWJY4?{Ve+Pju-%N-!*laSpZw`1ObxzjxJ5RpgBcb -spea(V8#=Y%|nJtA0FLtTPjs>|X>iZ%C`b%o|r@Br --=M7DfJKDLthU*MO}^_JzA70`f1eVw|idXZ!H`m_J6LPJ&b?&-ph!;Rf)fA*DfKeDf@BIZ$suneV{*8 -OMtqiOVP{Mf2V1T=Zc{8KYS6F+3#Wf8q5{oz3Tk~MBGYrcCu(qaUuu6m?f~7T2Iiy -k8S@V)Z8ikcLZ`MB{JKLN-C2fvzc6wSy_JDRaOGdx!smbQ_gzT>ADN`+(*_lb!u2VD9`z2(jcbnOxoi -RNjBPGe4ZH+ZsvQsiM2DIzZEuh`NhV_lc{#J`ETbw|0o2dm#@hE1td8*BlVx1$ulZ(YX!$!H76QeCDG -gH#c)6Cg!PqlZrI0TLm8E&3wPBW(AcR;&@?1+q+nbXadc1BxD$W$DqFrZyhLRz-D-N63;ue~dc&a$|| -AF}1JNQATrsK>Affl@N>Z13EedG90=*$faZ*Z{FaKukrzQzKH0N-V`_QKCde8zI;fkpcojKn2SZH6n{ -*!7X4=!6>VgC1oj{FWUCB=lHLGdQR@2_nRNz%$+;WeV%#Gyys5(5B&MxgQx%S=r;X7e6{KSnzV1*^nb -<^b24)>v$6&c8{g0;h|E?Vu;X0pZZAc~=O=ghC$Qtqn*-MrNZD@Bop59Go(xtSDzC|z4E7;Xcvi -@uoo5mhvmF#V{ogHJ%c`A>1HXqHW@dx=+yqquQtNCXB7XOGJ;)l5tH;H0#Sey~bGLlcrO8Kt5AX|p*! -meQy{w#bnToJwzHdj&=t7lXn7z%g53^)sG_3L_v9%zP}31+G(wGb6VQ=t(G>s}cgbt?*=_>jJ`#oF24z -Z)`9ET#Na)EqPzAtOT_NucQt8P>rN)s^fAh0nj6f6=225-m -Ya2En>l8#wWIB1yU(7p{iD^<`_TZr32(!D@kx9RU%*#!Hy*>YcoAE^AS#wYS -Qyp`xJn3yE)78}GC@sZdk4tms_6b<5%XerzKnRb?4WgjV}kr{G;93pe%NI6F4%c=6W@^^BcoG&ZnALV -knTD~f4T*pfCFj+(dWNuN$M9 -%6hR3HiC_2<9#MR$39{Gd<^G`^DT!OZyn^|wZHdCS=*bhI)Z?VAL6VHeISWNlH5HfP;AB%1w>$V@DKgECw4tpud*%CO;(s5d=u&Ib$064fzAvNRE@_U@d -)|Rj>}83&!vT{3ZShujacv1N0Ij#gn33tnq8wL58wpm=aD>Gt_FeL0wb{&=I;rKRBi@>DH#b>1a~SER -WO0W~b?5ds<<&9b@x-F1}=|>=wJt?za1F>!?H2C91|O_p;mK4!FO@=QcCeSmU%6Ng=spIvL8Q`pntIj -_pJMq+vs+E -H&SUVme4G|Zpl6xOM}|rRB+yBZUSvb6DiQK^nN;vme4u0l$O!?w47GZZ|DhniZ*x_9mgj7XwPM3Y(6V -z8`)Q^vuDsAJk4hV@!L$rEuZc4p_G^L`FyK5Ct|Xt>?Gr!cayZy^Qp2*zAjJ8bFx|3Dr_Hi3{%5m&!n -Ycit438g=&DRQm?CO^?^F0&Z`(C!&JBj?t^(y1)Je*_z-I0D4d4#&{DV6Khjs~Yjr;j8udVZqt4UM=v -5y1!#xw`+0}Nvt+wZFr|5;Kndh)-T;uk|<6RMp#ScU!sSF;V!`OrDDYloLW>5NcKf)(^B&`&O#0jxf4 -h<)TPlTsEKQVQKxz2tN{TQ#q@o_F(8Ed@FdH`ANG5#8TgKqI!vyJYg!&x4?ht2crca|0NgFHcW6aB?) -Vud&>7RhBY$E(J@Fiy|V3-pW8;do!08fz8DdXis~r^q@|LEDNBqLWA#SBh&ychO52F-(jVv&5gpZt;z -1BX9A{u|U2izmiGeH69CN!U4;n-rx+>0T -2KMUm&uVy`}x9w+fa$nit#UZfv+gjA6h!H(c4ORj!BY>$tNn%VoO}E-$_wC%!8@ata%;#4T|(uHMD>s -PXtFkTf!gOdtiMge)O7q@E-MNdXCLkn6{qA5;dLg1R6^Q)vdxqxrPR>wbmjoH`ow{GGw_*_|ww*esS5 -{~Cb!-c%a&1p*utSjd1Z7zEjn10%fZ+2>Li__uhOZyhfdRdHPKuvZFPpu(t~ui&e0=uuE%Y@o}d -f#o%$|asHf{9JyXxpCA!|{bb{xVG?Q&|O`gg3dx`?j=5^7RQGIkIO2BrQ=yww-con8%4@~nKuFzrzX5 -k>r#vB}hxp)^A;&d#+nK%nea1NGY8UFYCN|QBNlQmhBHQE1z{R2=-0|XQR000O8B@~!Wwgsenv%&xX0 -FVIy8vp)xduG7+fZEII;tF3jZwY4OmVnQGbi-bLZN)(qnKG1-WOxWJ -`VX@!u|35xt-n;v~=bn4Ed+vS3_pTPqf*@Gn&oBg`Uf}<{!vFrS3;qUP^9Im(5)G@KV ->3%Eun6eBcpR`2&wW`k3N+aDl5*ebn{vqb}c!yIqewHh;n7tgLi*ypA^?Tzw{LKKEAQ@5$ndwnWM@whmA-mCCZwn>LBIX{m}5avy;od1CGfFQhZBd=4)hriFx&*z2 -zlX*6w1_DAZba*H{N186o*D!er&&b9IV-+&7GZ*FaPOe;9Sq|?ECIB1ZE_mL2seB6-KL#1Sh{OOZHiq -Xe$~X1UVa1|rLc3&L8Q55uwgkRWVr+PYN^m$_|nSaI9$k+mZ(V!a{{+ -99W8axCv)khBFOQ_qO&+XJMHzA^}#ZIwd}N;a_?*I~X7C66f}`t}Em#y}2a_7$Ab+hwA-^JUT`lQL*u -+vu9x#M-fAJ5D*g!tI(R&6H+Iv!#1vas=A-21zs+v3j7B(AUqWLe-JtTz}r@!SI4yIy6HNB(gQs0BJ7 -BB0k?p+s_$>Odt=8F(q#&eR&i@sW_MZY$SgPmUl^!^=BYk$dE+&j6bh|?4v73;TW*k3b#oj`=ojcz#~ -Am(JO|rr%c)@Wp#JpK*vHqI3>I$A$}Vg;X -XTOVZ*eSrt^Rx@Z#6N-eK<_7gTWG78*^T3RnXcUk+K}4*zv}#qKH~BuL?bkla5uPDuPqpo@7s^V$r@*9UPz -{hiX<)oipKGC-qb-2fKY!pMr@_|HLvl`^P$g}Vf^scDRs=qUiwh$F2R;CAzA!>BIq5_0=jm#D^Y0Cnm%3sU}(9RfW#|N{x5HYnB;Nc5rr^_bD>N~(OY@Xo=$6rge&Cuyli4fv?i -QyQ}>zvO!zOC0VAQ9r5_5(3b;^5*qp}hxbEE08@d<~^$$FQXqAfve2F0+hAZbS -0J+v1g{EQn%?j<7aq5*7nr9r(bhd}F9_-Rxn4G9dYf -i!L^<{KJ$DtC;cqL?n9$>#2Ob799jKIRW2r*(^D?MXpk(pSVSZA()0epeWzC;esVFU-r$zs4j(t^wss -q0*z2e5)mVYU~-WG?}xy4=NCDlT+0Tj=Vjq{Ohqj41i?KA@rF0LB#s`HD3%Do9t-WusB{v{rtjn}MWZ1tDDEmL4__^kpdRtG$BJw4BpH(nR*Ijh+<*AK4CzWn!GXeX_St$~e`HoR -(q2L)~nu=dUP{L1ricq?+$8Q4n{@UOH7|ph#!)4|u533hmbMd<0*1b4lfLs6ZdcK{r@T%rv5N8UpT1C -~+At@p-(&iIaULItN}{0;vI@#P(o#zS~Pv&m>^>QFk0>6g&QplPJX~ZZCsJH>4lw=J0qPQt}!CTAKl4 -U4Wd8l%!M~e+yQz+_NJXkua8p&q7!>+Jfdr9ur_W`o};p5Gk{z!Hm#xDU1TNBsvk3#9wZNw&?_DE(n0 -m4+2%n4MwuCpFw14yI>GVsyH{MFEReQoqB;jY(;toe2xw|%LB=O6&2#x8YVBxo~)lcO@cZ|Zfvwy^fV -fVn(|$f+}LK1N+8YklQ|Ql5F}*awSLmcC0u{rQHgAlLv7+O8vwAw={IA6{tLrU*m??H^BSU{L<%g=PL -@ac2m&zESMkt?)H0gWL%^XDTryeoHCBfcIpYVAbFw;){yJGL04OsqPq^r47pPOK^BVNAAXfWfL37z~h -Z8fz5T?ymd~HD*P>ORo4!dy_gH72G@N}TlE1Io(;+%S!)DzGa8_$QH##VFivX7sjA9 -Z4U_P9oqbz^r-@U7m@)&XRX3pQ5Y;^Sy(ff;kKVQn>sDkOrs0J635!u+Lcq@Q+dn!B~O0#~wST_(!ii>^+Pz1up0%BbUJ&aI|vo|c2j-{+vYAettv92kn(U%X -B5RfOBrCjY#2$Qp1R0WO|M_IB0WFC-QrLrczrt<({cLvG+0_!TE`sy-(f%0U0{Gn4`0BF(l;Wet -Ix2C#|=0*$OWHhuUP(-wWve_LU|!Q!c9Kfxt2bJ6e4$yczh`g76|)&p{@T`EXh6Z^xJ}#O|vLtU0-Wg -#&S6_vSIOo~6G|pB#llMyfy`OTNmaSszSyK`wf4G7@D>)To(y&DClk`_@gONoeA0f>`rQOv1ZExg3>N -j|MT_>S+>#O>%kbI^^qNs{^u7;U+|CYxGv8fvN6D3E=mZaCU44Bp$wzC2ox;rqz!`YW6sj^Y4My_vAkx1v5K -=t*D{lcH(1uN8=yOX8ieny%dQmH{Rm3sZ0^^mp6CS~iq5?m*I{EW -RQCc@hs|b*>nfchv>ly_iP)eq;VIr0uBXiWK?=-GBP;>+M4+dGDXfi6TfMWGkccWOsD;1}=fIzuFNar -pgW&}rV_I*)uH|h4{Saiq?qZ}>$K%{(7~SUV<1Rmd_vm9V7xXW;ATw^ROa(zHPd^YKeu2nCVcpG?K*# -`AzYaAjA98w>0bF!26bP1g0&d+(pTsJ!;#EHCL{=09x+|a{jKnL85wA2PUKPkkm|UNyb^>n($rKQ4Yr -u12=@4w0UV~MkfE%3yXcTIA`XSUWr*_Ic$7C&*Qdv432&i4I?nQIJvTNmtB+8z~W$AK#m%;wQ{B?Fw3 -l1lz6Xm^JkR+8WThg{}g#|E#?mdk%RScPv*dl!VF-}X1T{oj-6f#sheRFtXu|}^!Oa@5AK>z@WLt#G3 -J1YPRnT3o93=SfP--~47r&G`bpl8wr!;?UfBQv_H3$QKKN>_qDfs#`(Owl%a8pUD)Ge}?t3CtjY8H_B -jqX}jXx4GHk=7?UjNO1PiPTz%Eko&s02u8mG8LMgqXtb9iFjGx^P&M37m8!$l -h|M`^2qFXmpXudu}Gyzzhw9dDdEuK+lPwr}gnGW$m1y*g^lw+le+W_buGm1{wd-FJo-kF9YPGfcVy-o -@vfZW*pI#R3Dk))>EamXo&YgcjtkYE7p`kay*x`4w;g2f3SR+%^&JkO2k@J`2(JBeB#uTe)3_^(HTox34AT9iG2o$<3;h5V7YxJm_2Zz -&Zt#=iQqM6R2<|2yIT`5b^5q=Y)A7~wwO^ce9I8i*ehg=I1T^W7HwXF{$YQ$OXH3&#hO!8#at@6?ZXg -O#uGrL>_0?zYdU==tv0l$xKT?W?JqHhUdAs)E*$m7#%tlB%dO(%tK_k53vGgM;#|4A4w0A4S751P -1yG3Lz5Wv?;)^quffIjDX0~z8Eyv2hs7Y%qeXNpz2huj(~#pHGzF0sJ%yv37dfqr@=HmO%m;CX3E#~A -2D%KarDC+=ZIHFi2K5?DkjPKUekCvl*%l0YVH~pSZd?t>!_ef`4b>-f4kvwbNV2bK@Cw)&J;a%PQs)R -p=DX+7%dr35)0|#0^kac*N;k@U#;($0=adO@c{}48z1CpFy8Q-|pnW!0UbM3`U>@2E?C_Ccz@Z1h%lu -(zx(8Ly`Wj^Fo|^xRIst1LiM2RDPyk^+&aIJC2h5ob-BNy)GK79OIN8_x;8pw3fCYOMv!%#Lxr)hFW( -i{kuI)4rFc5?vvJYS@20+=t$m2-DnvrAUI!0l+0$^!lJhdLTJ;k~M6`ocVl@otGl1vCK1bh$(zO -j|(*vTfiHWF})+yrb+!bfhPI^_}km~FlpYBB_oqihxp0Xj|U1`zEZJ@S+cJ7NzHPP -*N<2;2LmhU8YJ9{?RAiJ8Lv(!eKqhsNncmP$N$ukM$+nh!A?hR&T3bI}3O;V(5Kt$>G*=$vSK%gh-=~ -emZYKpN*23gKyt~>^KGKy@9f;cSH>|B0~*4PuhjNS`Nr+b4*ticUp?4g>LcQpEE2;@9vFD6Hd$qDmQw -m{=KtE7D~b~>?i6)!qkDs}s;(AtW`C{pME7=@=<9nVy_?XtNw(TNt;iN?xfJw;C3NY#sQgJdt-CidLL -C}Qnipg&An2CguuyU1x%GB7iU2_&YN$y08S*;Za>5^jgqS<4m$>doni_-;7QLWf2>pckc#Sus`>=ZAK -rm$rm%(T@N)=o4ac!cs*xVfsjV{YfNNux`?zXPi2rxtrSp;?n7m~P+ -_6}6k(wj#$Cd9GGgQvf4M2*H{zOVOq)@W?;A5`nQT@@`-|0?iBb+%e??^`>kOp+z(-g(tl+n`*AD0f*5f*^uw#9 -Y7aOCy^B70EWV;|FcK^JD~Qtf_FK{McG!+-eeu0$JGXwh-7pkO-a$QIF1I?`*=E&Fh?C8Y)%oZ`qaI$ -Hfrj(~*{OEq9n|Vkyc_uVfPxod0vddqph_Cy171v*nQnGuAd1s50~$$(rNec-P7pfD2{Gr(=AsYsbz-Std9rdX5HKm*yc*>5Hy4M+@;Lo -F%!*PxJA6SUJ$q~7}_im6iIIJx$~K^8uTq0lXCIRi*FNwsnibU^?*zoM11$58~Gpi^&tVVsb5!ECKRFesNAuG5It|zLrV!)e13m6lqWSkK{mjdTVjG2*liG&rPCMjpW-5@ -Goo0Bt@~Hr%QbHRme(;&fvTDdnQm=1hM89_y)4q2&A!~;ReW_fVstJN8TTXw5OHol|uQ_M>^GsvR1g^ -YP2woff28TH>%vrH@wzy;B7M;e}-r>-6FC5c2?JR`m_ySVQR_UmSA}?1L;JJm>4kc3nEe67T;-d1Dxn -HkR4Pit;vofsY5}iz`%W(=s)4Tm`JX?qyD_3#W^jnx)e{|&Vu2O-ylb3*oXvUYp6^kCU`uaX@L6CX!i -_X>eRmgRm9vtf-QESi>YLBOQvr2kto7VCZBk2S~^QMn&igA7TJ7=6oCTVRawBHw)9$m=%`}#d2*C%@x -`;~j51O42EbDn9$f1AW?thX}&*mAP52Sp`YyalTSc0zB#$6>ruMJIx~m87JER+)nb*(crCU}8BQmqCW-JmE0gDo -QqXb43`;3XBIl%xF5ULQ02AsiDOjIG+WD5NSknZtpmNomK&n~Ss`cLe%oEBFrJU_z%?@lTnxRZH+LT+ -gs8hK@i>50hv|fw)oFB@B{wAQV-L7r7M_<9>>%L8kBKCQ^TR>ey4AZuIqpMi14t)hgr>Ku>U!BvwJfm -$-5!Zc_rZ0ht)CZvwDZ|jqwdhP`h87*DOo1k^!nTKNy{TeoB;ZI-ivSBzy%Aa&s2BKQwsZWXRb1CSP| -v|oHVb_-P|uF%%!HhxE{f~E%3Nxd$X?w75&gEC#gGN6^`oIOMobNM*-Ll(m-Wx|36au`;fVgG1fSOv{pg@!i -4!w!EFxv>Gn*(E5QEN3#wCGa~gTju5+j7&GtzfC}4%s^wGr|^&YpuI+pVE@Z|fb2dx5$)Dfx&);{2Cb -so>(ta)Jzl3!1v3_mOgpQ^?91$t8Fsqd&CNFdWMg;SfCb6W;~?o)Mp0PH9zPK+?3VAa4m9DlL()D-(y -cOLo6!<|0tUIZ6qm(M*#2BA^CPo3YAWfuk6etiVIrPM;n}jtQe2oYU(Hd#rbkL>jt|? -XzJ4&W}zh|@`l@vw%F<;U2KcgT8kcG=cwBU{{s6Zg1^%@|&OaN(5o9|vCASNiFFV;VgEe^bjldUC -k#BY})#dhxIgUs7-Ji$it3Tnb^w>`iN8}z_G`Of4`aR(rR8}Sh|5{A+J5}nb|LlOcLvBqK)+JkNvF!S -F+Nqw|TLN@GgIK*pCgkEJc%R(^3C4exB9QIckj{R$p0h5^FXjvn$0mz2qoi%KB>>)$V)J_{9AZ?*urg -TJb@N(W(`e`Rxcz^Fk=m1#;KD0C!Npjg-Zoity$>+M^z^$c-Y@)x%jTgtC{}Ycx3;{t%HYVJ<=90ceY -%`ri0YQE>&;;<_A3%}zILJq9P$S%uuxW2Ya9tKektP_P3EZ#!LUozVvrN$Yj=8ViZ!pUiTQHLKW()~@fb8%^yf#m|l4CRk%u -=hcu!%LSLvuHCUt?~HhU1^_FcGpblY3}hX*hlbF;h~Pj)W_OQS}Cj2Y8upC+H)^+<6IKtIpv(a!N?z$FwdhqvV%Fz -Qn%Tp45%7lU*$_btPg}>iej%C;F4DPY=>*B5mv&|X*sPXP@+Jf|tC-@EAF)0v$p;cfCXa4= -}qzY87O<+t?JAF=_&|>|Sjcb^Oq -Zarb-%`B0|CXZe*G`{R2e()kCv(n5&bV7UeMTLMT)@?0Es3WQuSI`1xi1O*)m~ziFgF|TeGHb)_&y`w -<+N5GkFOTQCUTqxP^CIZV(6KLwmH)o!-x(`L5e&n8{KHP$V3m2P11YVU_riEb2)DDnYRYaM}U6kYQ5I -VXthDlf=pjyhNNmE%HDgJlCa%y{Ov8i{=O8rsNf7Z$}cr36U8<8;u_iH>0Rc?*$#EylnouKO1A{ey8}5})sxV%lzgYUOOD(&X6-qspvqND&;$sYx93#Qmz)) -3=-E!CQtja5wO6o%a&G|CG&2{X2RHF^Hxv$<417&BY%XtJ)kw%ip?Jda*n9)0^F*<#i3zOLE-z>0y@# -RSxSbnT^;!@AKJToXz%i&y^BK&=IjMzVpJK|Cf1UB&=)P&_K&*^2AG1C6z;3tfh}`^U^_O -$-645xT7zFi$3KTv?A40Dy4PiI%Y1`|)L8Fk+qgu3?}or=7B{bjanK#X8?~ -dxaFPkrxI`Koa%O$UaQ3K82{UatLHuGKD8@S3>4wK8buc{e=JQH(NftqBr=%2u -1UVH+3ZNnjD%{!48(vvtb;p`bK(F}c-hQnOvXfZ>*9N>`Fg&99m(4C3i0eR_&ZWJ?6`qQm>0K9L}pYy --knL#Gf3SYq!bL!Bws2|b+D5_Y0Hj)!2=9DzCsmn0Qq}p!cf}NRB(`wiGLFf6w-2TCcR4Ub{vX$gOtA -kun(xqg`#E$!qkOPFnxND!u@;q<#+MkWyX7lS<-p;vJ%~k$MODcPV~=bs-4FWweeyfyXuK7Wj{9hgdEBG9Ue*YG%AnCL`!u3 -0WDup%Va`$@7t7NT7jSrqD%MS;v!fBJzR#>5a;{Rli0hK82L18Y$_z?V?=cni7Oq -z^UDzR0iy;v+v&;OEUv`FgJ1F>RznZ)_fn?dS?iG@0V^J{FERW6!h#=u93sAz& -6v#fat}LI5q%*Hz6QtH=}QL#QF~4X3*0PYwu=uGOmg3P;teD*GoTU6WNiFW&Q=kNO2}*nSu0umi__;^ -_;bAzi8D^6lfx7Q%0)>%DHql^F-<386F_!gn4tfS>!z??N!H^hQN1&iEW?ol`m~lc -A0n7@XY|*D`*hei>YZXPg4X?wC<;}-gsR~KcA-TY&5i(7<1p~8xmtloQFC<IxBz#l}k-cLzrB -qaS|Jk{0B712k|mX4!_&L6&rOR(6x=Y*25YfI2%8G8)W{~yhP>T3)Gc<{WcsWe+4?7wdw?JOdF2t{w5 -@xr)ALM*R>+Qn2?o-RPspHTx^WA8;*f2?-qz12C`ch)At8soHpHNgw1*yb?s!r638#d|C*Hk;@ybP^N -)_+%tH%#=vO?ngokQ*XgLo($wGv!L ->BQdk9di%ptJ_Q;H>mci2opqGvDmNY?cU@o>4r8qN9;t4u>+kqWy&CRO*C+tDz9JCuZ@JL9BRU7L^>- -Im75J5Lk0B;v2GUVMjuq5X2jgBt5yPS0^U{J6_@nIx()ggv98#@j=S(|UOItki* -(>e%($B>*7@yvA?AS9gTz%_W3lO1lj%c2YjjC$oak-_TA<&~HOzxUm{<)36$|`c+^EdX*J&F|=JcHtV -6MOqyoRcV=Veaj@kraPuQ3_RaEysZTF4fo{em+FWIwIL-P$dNWA2|B4Po1s(GNuo@b?V^@jX6|3z&mw -D@+X?n967TW*wup&>zp?;m6}o)PIp)V~f#`@5S>f0HjG55h=osYNTM_nryP?Q037J&sFp6D7&%(P|I- -K_m4Xuq!>FAC8OU*S7fblJ?`g`{Xn%NS^tK2`t+^*#2gO(&lqvT{Y(~^jPQzN8GnJfe44r0s`o;8S0V -cBU8RjS!!3gy=U`CO@Ek%X!}Zp`Ub;13 -uErx0NTuoUGOg@tCkltU)QX#ro(ePSSm~6UtBF@nA8un^EAbckH;a``14)shw}pqA>m!p88XkTFX*n@ -vYi!c4`UTB$%S9eQRSLy|U+AF6B@*UKNiQzP37t}n3)jXM}K-kT~WB!EP -_B+)Y^ibu!Ao&i5JyGV*++>^azUaS&DS7wLY)XFrXY~1ETON*j9`$#OdKsct`$L~AB8QG0C=vwK%Ui5 --dO1*dLJvD=znwO*BW1t`e)uAyB`NFM_3-xF^nzVt-qz922Q;KJNwPt3a}D7gqH(VBm5S(@wR)?m4WP7fL)6Xt2~y6CS2JJDZU0)H4xp5 -}s3)-WzI0HO$(II|O8#M0V50f!rHW3=K6ggTrKYI2I2vR)lZ3@)X-SHXO76h@)QN{tDW;3Ti-m2tTV| -#n1jZ`YhmVY;+0*@Gw8y$!Ns_sKll`J`E%OJehSO1FDeLvK_lf(ElFD-hrG=@{X9C6O+k4di|VB*ZcW -bP%jj3cniC>8-_uv%h>p(Al_WSVjkl$!0;Tww{Sdm7j6M#cpqorhA8csF+s%}t(`Bn_+`!! -hNLSow1d6;RcGr-d8r5Lx(U$)el%FF5oI_J)^T`bYlUYH0fvCWh{!<~!Of`gbU%1i?Z`TOv%~P+%&@^9 -dWf7h$#sb=gQaN&a53dH#-fkjUK1itW6)5fp#hUAxz>f|lxW3S+7A$wYE7)*S7FvqUUlcgSpwqKOjQo -U+pzsPP%zBTw^k52y$3o)x)`m_mwXB(Nnt4yy@iE4D8kb6Pq=a|4>cMNO&g*n;dcO{*UEJ9=;`lYzqP -5sOyH?cdsU-U+Hr&QK7drU#ihwr7Z72)zaZY(a>h=pJLvq*ZiVnovCHky4NTXj!IJ>Hlws!eM=2_#+gfj~3ix7ZWPmQ682QD$<^Y}Tsc5!b* -vxZ0z|Qq(l8(yOOyF|%sLchLHS9(#NfIP_cI%?&6<`aGF>7RxTTSUl(0MqE>=WNehHPon`?d`?C|Da) -INJnXaIBH6|@h4=|;$^~l**40yN6Ef)+`#onQU8K+od@1iJ9+A|x;)N%)tTm~4OauYkjb1pvhgL`--u -2*%{DGoj&~r#4x+G3JB6*Iet^M{|$O^Rd^eHs|y?wl_l* -nt;$2ePLlkte626K5pP8MWbm>BRT^InpKgN5psRw)U -9rWUa_sd%fAC!bY)t?i2~}%>|&0eb~R&J_etiz<>LXHCNsqSkpfT;$Z#wCPTl_95!KcJ!CP8!6D16V5nacJJdMp -*D1sK_%jP?Ce8KBv-8HFzrGgc95>nCw~haFpN3Beg^IKbc|}~V=jH;=;JL6`aTlP>xL<~!$dzyVSdGu -w%y>fu-$>~EU1MC%MauEPpe6|U7OM=C<|{VLp!^t!c5!`t!{5-Zm-s7Fr8#}vMI`ww6i94Fuf5s!96X -?c%vDbck844p`+?B$dUrHID#iiqR(JA_7I1M)iLxls5S=G8coq9=-0v~$%FegP^;)W$LFeq-JFByJ}> -UAgH(rQj;3=TgA2#a4yr{aynGmlPj}wI06C*QV>79j%jM0SCN|5F%qmdzE77PD$!wLwAllFQB_pht8% -=l^P&@5j(Z=_&`z&$L32%I@$>mKbw`%T-k=P;fv_dqzH!i%bt;+#qUAAZ>YV)ddA41?a-SGHTv3WUqf3VJFn`GBEbmhu@Ex*rPZ+NCDteBW -{y9Ec+?eTtAc2(;x8+H6mrV~VaVA`ha4-(jya5A`ibfK}`l7r~0%aTYhI;Hjg|Ao|N32Qi?-7b%*EN9 -B6u@SJv9W97C%2YvM8#8w48{Sn(~(;qJe#m$O`3xN0*%R%DzuEhh0LC>cWyD~zY)(BO$xA^H)X53Oc; -jcaH?~sf=C_#5Cd7*~oml?$!%WX38x7Rv#JKltp*G3naxn;>hTJ(Vhb2_HhmR)X$e`tynokP#463)1n -CK-3)Ua#-rS&%0h!1G>s_f;_(0}(m!pp^ru)CfG#7TV7yIQj)Q--$JC>}97|)4(>Z+8-~b3qL}6{5BK -XZOZUa0TU#H+<$f=cNGGT$V@`DxFH!kr!J<~A;9v-i6ImysT)FJr~Wh@^%3)!cb1_;*ESz<#Vc#NL}e -(~H2cOH0@gSZ4>hVzdf*_dSIwjC@a7A<^eaORPfTo1W4W^Fx`Q}pc+OGJpl=>bPT7ZOB1? -c0HaD1A`y`AWb=aN27oW}cc{0D-hP0|j>q;A0yBs-KL67kxK%S$Xh9YOOBxx6(ven+6Z35cVe9e -nqA=w52EiKFpR|668Qi-cf)u}Ag5Il>x7D_VT&oCHKbG^PeM1CC|=lMukb_JDlE$Yw*i)X%Y{CafpQR -^JIE<=%pYodIs+9MSnoHX35NvvHCppv9+{00y`EfTpO3Vn_(4{4>U%!BW;P0f$10V}1fag5PSxG$RkG-9` -;$2LS)S3|Xi`RmpR#o~+QphM<2*%n?bCB|*c_hj1o;Bew9ap}))bI1{*Gsu)oWW7xfiz)2lfpN6r08rPTFKwrw-pr^rF-FOhjDcM}g@VuSmLw2^Wu^gu(W{5T4vMs -3T_EkQ2Zmmfl_CCW_JOTO>lH8){0~(3;&rl1b1{GTPFMkV+M%2mo*nSl3FYBI;jat+Vrqx}eB9qpY#IcAPy&8<5qRmqA65vS -6o0z#C#JS{Rb{$`=*&yWKL2N4)q&s-Azj?CH%aQ4ybTEYH%9K`?CO&XI8vbc477af;{#g!9mv4+w4#W -`4SXuW){u(F&SOmMhOaSm5Pf4nEj;kG}N;Bd?PbGRi*rgT@F!!4mDd*U1pordVtJ; ->prD1p-(_b?`>J=CAcE$`3d`s%|gR)BObo_nr8mpieWbGhs3*Sk5Fy9&76m5}io&gORQ?#JeucPHom? -%hc?*Z{P}I$|PP^7%A27|SJ!BK&(7oW^A7FTPwqEtfme4X<~2Gu6@m2?o-DZ~6{k`R-I&UjgR;Z7 -YVo!CQaw6}`BRQc?Pn!%{Wf3iBlCE>3k=BEPNJF3Ae7^K>Pi87M5*^tZ!0+HD!e`H>duhy44z+ZDzc`%UyDky*olsiKQmEzj;WI6Kz+1 -LO@Vpya=ssg=ua=$V+W0`CyGor5yFVG7`vCPehX$eM(Kk}hLCG%Rg8wGOEPq+oI7ClqTXMF@jsCEUOX ->XD}?cz^Xvjg8Rpt7y0vP4uI+T7d}Dxo -QcS)Hn45ttQ+DWX6e{BH;l4W>J*SKmYmgF3H*Zw3n3HtC*c9E$xUHYcMr+VRmktj^{3blBpNe-%>Q4s -=jpAyBk;M`t1DalnLyMoeGd9J?__rVP8$S)l4I%bcQLyqOB?AN?f68^tNf -+4zH0~Qo`B~QH3O4XYqL3B&jEyy;)3vIgT8jJAYdZYdJoj9h`8dd!RPT_$?-6zpyv@PKAQ>4Kp)$`Ni -T<2iar2g!G`Z6hy6KUR;`?85<@#u*hZXKgB$y@v8CHw1tOQx6n$HW`&R3scFOY1aOhp-V8~!>5Z9Qe= -D#N_kxYW}xws}{s<^(!gduj)d-8U~db~GC+hRmjsMTojY=An;H^}A=ea`vd@IX_wfY;iolof+knG-O< -Sl_h;+9|VoQS8lFQ%;9I1mM$iAP%AOoizLdz$*XeLGnn6l=I0B>LXi%^<&T`3Vi_0bcy -%FdTT?9!T`s4}pU~cQ-?2o+dJ{pNh5R?njfJ3%lT!^F|HoA&}SsjWOuZdmd??|+TsJhC(zhAH*QMPtw?#+4-v -Dlt0uUad@L-dr);WC3hxn4X!4d;I5pciRG9&l8_TL0TVoNxaVFAmt$B -s0|Z{Xi)4qsn`Ob3v7N9ruXzYN%Vb0n)DwSjz~4~SS!%$XSOCd5q+M$;u*oHbQT4kM^p3`=uJGbBeIw}d-+t+e=uVx@`9JUobpFo8@x+TdQ@g! -v4LFA2pfHzNJ#W%bX$*1BM{3EH>vi=RB2maBg+Ad=R%dprn7En<5)N_|mH!vOL?!<9Pd*Y5(ZjhuOk2VOb!q+L+0k|F35jN5I?vt*J|3v47sT+^uA#Pw}d` -<5v}s%3y#Pcf30v3}p4kJRmTdf9Jiw?L;tJDFMr -Mp=w;?#9Y249s_wcpT=v_I?jolb#7XHn!w^HS2;8;n#WTzY=gelv<7rVQqDC=3Sl$2`ozLN|fUT0&IT -kPKDqhr3T5PUqL&B=#4Q7z#Bryz_g8XOHE3xz#EIoqV?X>|f^kUW$ywvOYMn;aO=2t0K)9d@X-PnBTZ -cC&kYEDC^fSq{H#6n!3E&f$N3y=E%ZeIM6C9*RZp7l?E4M-(TOb?h-FTIOeOu%SB*ewioi?x4G<(tr* -Q%aW&4v6bF0@ZuY){tTnv`cV$IM+{JopS1DkxcKwac>LD -*b5;DQq_SsdJhluCYO(p^&z3Ti)T1fLPb9Ghowl`Dkyx{bX?lL{cc8_li8a~mn0mOKo_<>T>Y|Xn5Th8A}KMrUlRk7k18u9kFOPJCQQeJu`ZQ-M -gOs%2tjAkzA;555!45n{sVuNv)d!o#8XS2^;k}E~-JYMT_->J`#n713IdHoqW$o}LO1SLIjR~%q~3g`jXy~(A32z8U562 -2(`!6;s(j{uU`4i%Z{4%Xw@c3+69X^0=0O9vEl^wB4?3Bp>2!mn3JD;7VvEv65Lse*@{#ibUP!@I>#Z -i}&S7OcuAY=NE*9F7fZO=q=Mn*F39WIU0^KZ24WX -?V+}3thms}q6x1d086sxSdcVCrsis~f~Ql=&8U=Y(RZ`0z(@#d04D3pX##m4Z~X9wbg^c*iS2L>sPg2 -BHSE|Cs3MTqY|f3IhVH0#sp`x_Y7t@y>s~>;Ygkn^dWs59UtZlZ2TPW3r?sJ%gqHp?`XnXOe1TbAji3VO7^%4YLlw0z_dqq7=+@mx&>~V -UNw6naV>Uz(4;}0~uDikcEY&^@7%uKZ`NK;?;34ar -CK(px}Vce|0bNbAB!)-qjmX!03O=EZ|2#$%o&C%9ZfD7|kGOmZ>i?0<6CvRpl>M(}6W&I8(0$%3#-@g -M#hZku07<<9pFl&$;&VAH -OMC)56H&E8){0mwiEFkpc0}Kwf!pB#wjA6lTf*EYrBz$$`|QIIASn4(jgyTHOlD&IemK*9`)Be-30`UT|mj)^1fY)`&F3FW;iML4hzc|5)QI!8TDcG7s*!W-`-;lb0zlAWlV`3MSsp?>BD^)yWj1n#U` -)RoO^%MrS5Pj!R?vk-b{mgYBTd{saHSLas!n%3)|u?=s+WoE7DW12b{Ua+1 -%2e*L@&Mhpc{!bGueSFSdMWaK3as5RKq!z%&+VA_B;GC4^^s#jFaFon7K{-evh^w+2Je58U^E3mveZS -lP%HJ>*Da}`_e)Z9>JWQ5)6Cr#lud&c-YA=F2skuDKYH$RVo`NMtyAiwo$CJ%gifar+Pw$^c^wTXg%M -LpI{KS`o$+YI*6E$%VlBilfr;omVE`54pVA=7f1Y-d+i(qVgXmEx6qc$zdw%uoPx^RK -|M3)v1o%H*4ynFo2^PWl1!`d>bV9av$Fsz2o_yo~To-dTfO5Wh%;_bkCXJ@i|^^-cQT|MLA0Sf$yYH} -Vf~ku#`rW;1mXO?FzW5O0-5DPuqTL|Iu;;(JhsBhyUm^9^!&EA{Q+H&%zOPquJB>Gz=4pF*wp$t%2YV -GDf$clk6=VPuI(nJkxo!(f3vV`+QX0Te97HVDZcWU7u&n19oq=n*ZKH_MUHMeOrg??A;jG;)2$^jrLn -&xFex(VBZji}m4;g~Vy-@Aq-(^ZUoI6}f0t9lPi|tDzaD=B?kb8sCBUXg5FeaVdW|!*SqIw8g~_1OzJ -z#wvpu`V=_Tp2W*HIW6!ems1?&pLQXLa9cb7livZOOlRCg#Q;)|A)pSpfxE6m$9s;Nf`t6yf8a{?CizGiG7Qei` -k|BDR}gfZk>hp#Y4F5*w|~uO-3O6X7(Mp%+0vpIJMa~!X|)1nh8_#bR9R?rJQ$AirG&Rv_H<3@PZB}o -m|IBY-(MS;#YHuzrd4?u31axpvxZlPG-i<39BGV`wA$&Q5qnY6F~9v8O6T|+MWs{S&gyXzI>qg84%UN -?kd&?(!Z`rGQ8~uB&?s!E<_zg-(15x5K3mCgV*%iKxVcvr0;PXm*muBvR@eoFE+G|1?=?6Uo+!a&uMm -sF>FS&9DbcAeolhjv2Lo)XO}tVXv>`zbpr9a3h^o=#Zh<}J_Z(!!$(@7;QftBO{Jdkg0MAys6T$z1J7 -WxlPEJ5CC~d`^4=ie^}Y1ojVP10C5{t4#{{X>FR}UGTu`9c<;b%QY%{6Rj^_`KB*LE7r4~px2bg!2Sa -TO1lpvCgeHO@XLMepL?2dN@D9WQ&%{oMz71iPM*atXnI0v3po!wfFb-RJnV9+$d%=@NIEX@zfg2<%fE-RKfUvafj$x5$xDk3x{Y1Bp|TWcoF{OfOPqHmx -$(G^k(oC(=hXsVSOR-jY2B=mjsvl$VTLkX1WDA6 -+xUqYZks3d*sAD#xvC31XbYquqtl6w)hq@Ken2StR`Qub}yJ7}{F4PUEYS#idM}GC(N;fZQamZ>7)O%Y1)f6b;J2Rk%US#)bvs!_+gFnZ#(qI0gfwDf$D0xnW^DLO -gqPsDT$ae6Cmnn%fOIQ~$S%=^3Bk$0a%H|7#CN{nP7E`Y7Dmk~pu3q-|fbbv??NhGXVK+|IcN$xdPF2 -h3!NkQG!jC=BeTW2VUS!+i;@|9w0NSB|T1E&tk%F;?~*TUsWU2hl07*I0U|92t5{_Ux#XaTny{Ey<7V --i2{X@+=&a^yd^1}NUR1{9E-8fhJkF__gT -tsxhG}@9)fyiHEtf9VDifc3$Sa=dIk%{t1@qa9r;E -+}wuajUO%auR4?$3uN{RTh0=V<};r3dJ=b@yM{o%E=A8IG|F;=6x_BXa@svR$pGD`aiZU|rSdaufi(8J3lim!AZZLnvOfC}PJA2vAwS$Rt~jSLz`y2L%LqY(A`-8=k&f -+Y*E&t{lRXoY`V{Wl@b4|mI^S8W`Uu)c^9<=fRT4P{s&k8<;+HFZk%vs-l^x#Nm8EicWtYEZn_>&Ej7 -fENJ4g6th45EcuY1MMfdHK^osC;Y*crpV6jB@ -Rv_tcmucewG4wk`bOV2@~PMPR~&xtMyexMZ^W+lH_iD-mp;PV-E32A(4+uO9+hy@-m9Daq>B{K^M%z0 ->3t@rNA1ca{27lwBk)JmdcA6m-_u3*iO+A)2T5dyj-M9R(@-zc%~&?vV_o(83 -$5bcc0RfPiHvNom={rQxu3(K;(a%u`({Voc)F8(p%0PXvvQmLd+6(iz58@64klc@RSI<}!&X -^Wtrdjs_Hq7&uU18*YSGsUNiZ_6GE7_-9VvLHnl7aP>PS=ygXkW6$qI2mhU1!km8v((<@l}OjBU&*yPuCZ=o`&w -^EziZZkx-zhn{*3&;Q^+@}s}A#FeWb&Wp=RteTBm|4TK-{a6iXY1h4mP$=#+9G1E4>+yf5hyISBLeC- -n_mXFmnkrRo_v!=ixU2u( -oB8rz>T#PBO~V}q_tid9s#}oU@Rcmqyumh9S^y_k -HvB*Kz6CDIqHBDX>vCNXP!!ZfF-cTVuuwEv+0|WK5*5=*1B69Lgmu|fEHCInFSzoCU9GIwO3Q9b`qUUjLG$i$~$FzKb;AYHSEc4`_Xb -;1kEG64GE-m}2J%bs@3d$!+OKEhUEeJq1%*?4jIeJ#9%uuj}`xMUt(l1Wut#zO6r(8@;{v|MplAN>3P -tylb|lh&3s>gzGIQHqLRnI}!%IEHJgZ)id5AwUeQwd!qmT7oWjKcv;xWO_DeV)qOCqWLcB4JdfIH*1n -luQ_4&z%T;2&C*+&fwWJ3Et*s}L%%g#$JJFKnt@U6*CN+nle)lUwoCNR{O9g-R{GXJ@l9qZh%p$ZST>$ -n?I!zmvEvT74#602#Mq2RfYz;wYds@2^`ucJjl-J;29m8$6h*d+7v>In(c%(p+;IEw%jg?4lPbf7PF( -6q_Wl1`wcaC>Dmb(a}4DX{8Fp7L7yGd!NOH^o_RyXeK<%$><%?^pZsJuO8`Bj#k9+o?;%97(>IpgW$N -70mx1@0!XjV+JQgiTEg%Scnl=-Dm#N&gAUi>X^{K|zQ9TFhy?TV&_My&JuM*!&NlKz;!SDixR@}%9y& -St&Y)IZ$Gf%eC&30UEGnCod(PWOd)+STivOU@##TPi+Z%YUkm>Xn}u*XhXG)< -C4+Jy4ANaQ`oxpbM#`;8PjX@qZ}8Lj*u=Ub;r;8cq6Il5DW5rzWNl+NvH}lY7oSBkum9i;2VZzzmEDTyL>cg}|ov0Ct8(Q$U*oq!hz$nMW@+te -Nh-nf2RsxMp^_s`c19de5OPU5;0^W~`;=(_rZe%xMGW#A{l)FuUNgeU?TvKXH_P;@8>mi=rU6LT(k&y -C~Ofj;6{(3|8aiPG}Gc0#OAlky~8SFa70w0pb7_nIj5e?mWQ;{Be2{T|O4li -l~k?p&~a!n7iqPTIrscn@c|d?Iw1^2&BO1li;L1A5zM+SsFtb&oNnv)q)^R_|@T4v(Aqt@rSS*_ax)Z ->x7REAnmNVHMoCRpqvN4|4$X@}NE*cs#Pc#nUpAA@1f-c$fHUX&^l!*@~wpar&3TTb7=WgfeIWIPL$6 -p84!2)z9Vpq>_-*DVU+&mg$Qd$&}h^RkI$CP1@AHt?1i9&rzyc^>~KzBPeWe3tF>u&97Fw_5yCiHNCb -=iEqwu4_`&N+BZeJwjQT@s0J*VTQJ716iOd|R-Rt5&qt33G4(dRZxDKggHsoTe&Oh+M86n3p^5&@=r5 -o@tC-k;zO8zCO7mIWS^-PYt?i(P#H?6Jc5-ufC#`O`_QO-Fb8qP0L0a*U)JL>~wAL0kmrkM`1b!ZlPL -et(!`VUB7CF+C5>DpqAaB+|#HEO<>>ssn2e!Bsa>fn{*W*!F@CCw!vSL(neRZ(qW7<8XVz>6v{(+Ido -b?a%MY(G?vHq!d=^s5W59!;1^le2y0sXKObOQP{qu+7#Yl3>w9>VUM8HE0{W~_s-n}e^^KX1^pvG86z -1)`+W#+Obm99YQ?EemfmBwX0R5T)=OLt=y{7!of$L=it>BlAcRsu-db)-WVPSjLcf!V-pL3waDF5N>5 -ik}!)QMnT7rG9hK9!*Z&S$Z))!O^9JQ<o2!*QMwf>t^Vs|p?r$7x9DxRc`ex)Dw@9N$YqGsE%SBz( -khoOOf-hNEsxc#GlqjYW8g;V2mhTN#e~HH1wJZ$*3q!|^6Vp`78Uoe<0n$8(}W0mG?r=lKjrQ_w;N!_ -jcGpuW>-^f~EHr?uvh49Bg*f|B7Wh=<3iX822p*D@SeRYC>BamSrd#&F!hCKNHe3Gr-(Z$Uhh;l~lzGQ1h_i3}GIk7sx* -;?WH6Ks=n`sOA#<87@bhFgytH-&Qa>BQ7wU8ms?;;W3CeF&uA{5%x1Y3GrPFNAr8a^92PG5jUOJs7@=+Uz -f*%N2HpqVkYA-#ONOw8;Si7$o -(#JOE^Sa$s*>kq)PBHF`pUCCkA{9n9n5U6AwPOF`roGlLS7Q%%?x|NdccU=HtzLwBVD%e9kRr(+&9C$ -b7zKK1T3SGoPc(rwn{1FrPik9TO0Ej$)qAE_cjC;HhMu_b+!$N#Gg6Jj<6m>CWw60rdm$<05`Aw?wL| -WJ2r%&UNz6`<(_!^tMCku??#AB(=&zrW5NbxIL0HXpbbYDp>gCe(4C(y9ssn_jMHk{584&l2w3^*;sy9szhijw=B7W2i?(o5hA-5qRPr>| -1EtKhr*g6$Oc{c6|^mdLdobc5irp?~*FyY=G7ll#UmH)yehl?`hkQ=@O3WhJI)UnJS|L(#?dTr}1ix2 -Rx4BQCK~O`a+dJDMu8G#)DKW -Dk>sOCr1TMz2>TOCNOmha9godFSgUBzDh|QIg_Yi`yp5H%FeY>M0k(B8V>f6BaNJznV|OF)T$;plUw3 -1)qb`j0F0GqYkUJS}SupY&upMW&db_{Yg=?;!*QziX|4t=*{y2`4TM_~;(Kb2(m -gc^T2ujfqH%HOcdCF@=&TM{&A=K4%Ae);l-UB7ppQ7La~j&)cAo9*dX?yB^t#3Dgf`nz>yK{yyvdJl= -7soQI{r7$`lGvfw)IC3^JwdjUgo~yd**S%xs8t9km~@or|4^joqGm{Ou$b7T2Pe;UcWFeI{^DiaGzlA -57AvDWNwi@>$2@RrY_rn^tqnbFC=9A%^rBmQ+*KXcIulwwC;OxN089PS?Y<=+!7AyYjWi2wvo|xv`aU -il{V)YywQ}RWLq{ryt%4poq0X19wvMBDsoioWNEdjO%=>DD~D)D2xr|o?J$ -tTT$SY;omvZ69v#9f25zR4#Ql|FR$JFR8QrB0@TbnJlba!Dt!lWIHOmBJx=htM56HEH3(nddMu+HM!a -Hfkt`KFn^xv+bZ;94wwSso{1nJdWAjF0Z+(U0%8$=4au^>apmLZiWsYR3EQsC%4-ZZ6Oz?^mp -Qgz`v-F7~3nrUb_8CAnf9+A%{*n{$#etW}r%Zs{C)PalkHtzt@(*1bua+++&IIIlg22bhub22kBs%}p -Q%d}7jJ*h9~;Ps9_o^`g7BCbENmB72cu80fj{RV?JN81^^nhVbcfG3*`|crOk73Ud?`bbPyB(1{o4xN -aw5^MD<@0BxAG9~T=WyT>kj2o^m6-0Mr5>nDa?%S*)_1mY2ky^E2{l-YA!cCVwZ>503DbiJ2WWrKAb9 -!nKkAEAvOKd_X3(C<)Gvg@e=lCzM`U#cnn=x2|6=}DKcK3V)jVU(JG54l6eeI87uF`CiXJ!&vL2R((E -bfog|S1&&ph-?mq4Pt{Hrie>?4v1+;VRpp@A1}B@e>3isNP>i@1?+#9!XTqsj8WI4)2 -^2Kf>)sZR+S0nZ---#+fiXbay}$E$xUoQ4Cu>h43pOo496eFhr2^2q9wF0v2PBkL}A-kaT8ENqcFUP3V9ih@BFv!`y(54NgJFZ-+r4@5?)&1=Pl}Z59dVW@dAd!pQNik`-Yun62YT3L*zurW)&{Ob8 -LJ5@C)8TMMNg+gjli$&zN|0)laDXr(-RtH$myIcC&JTuTrW|YlS3$Qw1%Dq&J`ssSd+c9swWNabk^81 -`laMMn{xP9wcsD}M^B#*anU@_qHwbl#G%Ts|H7H^U+3#BJKkJDXGdc0_2yh<9YmXh(FzoXnww@My*f^ -K|?)mndVUiLOUhX+2w4eOmYL%W}W5u(;p-LDuHIhlULJ5(*=JvSc20Q+V4vYuEA_u~Inzck*bcVT-7h(Q0k(XBw_r|BjhlvJ4hRL7k(L1g~m -Ghu&MoY`4{W;bif<-uA7w#`B)%@CLt&o+|4PgVAU&?oZ1|Uo6g3tv4%@P)f*7acfIy>P#u`bffz&Wfm -Wm?EtmU^suT;t&l6t0SU)!?^El2i$mx=4~b#yv(tO@zE00kP)M51qZy)mqs5_D>AL?xI+xdEM$ZRnl6 -~gzbbk)zrckVj&gQRB+_z5*8$6k1whFBieQx$isc2JFpejHNy9iPzP+*?U_9Y(n*Hl7Cx*KjuX;V~I0 -N<_mOQTv7vsKnS2*QeG+jVGE;i+)?MIccJ --50x0=MIu~hfML;7i3&R&ywPm=_AAWC-Yb6(b;|WNdBc9I0Rz~eo};uGPY4JiEo#KNpU`Id(37h^5}_ -zgk$&>mENWL@C1q%Sf(zM64!FEs95DmMBz;MU1`v;jy&jcvaeHg*ytB7WYp{jQ3=S(oi(0&K<4C -Nd*%OJ&0cTK{nd~1hqJ|5IjDel`(?a2Vw=~3PU;1Yd;VkZ*quCaKn5Ja~uvL1B9jSo@W%8#uvACsmCx -sE7MBo<7Yf?s#vRV5!Ny{6^IOQN_QWS+3cK^_zjLSw#BRjCzvV9Re*2i4vg^h416Xno6h%2~%I;8Bg1 -({9|?&Nw-DJE69%DdPhtcjAS$@S6CJavW|kraU;%9R$tU0ct$9s;zlViN@Csscidsb%v|fdqnHR{S$p -HepP){xWV}Vy*s$R$_vr8=S3Jtro-B(`FPB^Qb}#A9Z<*C;Q_G>kas?(SA6-`)5D7>+0zce?$K&%?#K -9I*yLo^EqJKZ9ZkWrjI^<>+FX#Tw8;k)8~kzCk!}AQl$x}1igi;Ve#@^w59|X)<#DM|sJWvNdu!HMJU -@n8tKG1JEsFf=zAET2JY&qh>({&GS9?+C+Vh>*!hOBB@cdJM+` -AiTzGaGx2EROrAa;W7bj%&Q&4JNK}*=Dq-ot+5>h0v+GR2t@4)GrHLPKfNCoDgA5Z=|vooja -4LDDBl$kFHwQ`s)E2E#m{O72vwbH{}i(zsGoRASLE&(630?rE7Wyt~eEe&@tGYKL;yd0g4Qd_6tObO? -Jz3v;lNRzX8$Juz%w66={)FMOJjqGBvW5vJ3UBe -CS{b@VV%%OvoR1%AMv$D>*mqk&d2Iq&c_m#zi@nQ59ed&ZoTY$?EJ00p -N|cVy6k+cj-HPV>EP#MO^#B8zpQmRAGa-ykl^cV&7-t6iYh#}lRT;N -Nodh3{4#E9m(G7Z>1Yu@|HWE_W~-(s9p}HWMX})Z=3u^fS%1BBxNXZg>2Mn`PnhsRIW<&uSGnVS7;WQ -YO4`N{VTmI^xUivXwwTK9&jiq|WAUV>0nz2JeJ0Mc8BTv37VFqN%AgL6sKoniU*ur0cd9^14M1x^^cQrtC@QMbAzO<5F4GWT)A2dL8u)6`GY0Lo8G#3Lz)2IQWY5)HiAhO;sUcmqn_yqjC7PmYd>}r -6hB98ov28do+ivsFInomy#h}P5aNZ0Vo3=my+%3+h`ziohMO$W+Uy%-?+`-@jHK&0;ABHz^;AX*jq4; -Ubdf3b@JqBb>12E7{~a(nSA3=nN>cM!UZ0iyHsx)>m0rgTu;c32=vm?(L?B`oEz`B9+AjiSeO7yCva -dbZw)=|5wpWJB)8_f2XLP9Xu->EUv2GS(gf4iB^~3m#Nkh>`H}mRvdeEatFjVLjhz2@q~{)WJRlNHRJ -(py_@f$u-o}X4n_>S}@lWGT0-EchqvZwK>J!M^HGMu-U|~0FQB;122kJ^2K5bEP1Y9?D*6ckEUj|whM -}#KPxIL>4kuhr8m!V3_316#j=b>$C;V1Vh!u{7QCrb4%hQ|FTQK7I`O(VNH{g!>XmN(T3|{XFv;i;E+PoF7 -%jrcX-u6JnJLv@PL(@d26D^wiKk$2o -Z;EVRqQ_0`!*>5dpp!YO74n%+Qo%p8IuUZ6T@h-Dbv{gJP(j$)r=vQeripr@Ho`&!0sXU8k$(aP4CS* -T)K(5xAs8)_Mf%N#Go>(CH)b&{cjt|nXgYLZz}enIiN0$wT2LNjq1ea16hYFjm)Mp0SM%T~v$l7C(qi -)KKe0hOOC?yp0OTRu!Mmp1gDjb;f!yy&CNT}v&$C%2leQ70{unUyx`ft65HTu09}IqsNm!FQmrzeKXM -e_2g&x$Z_+n){i?p49=we>v2HA0}z0)v#$j#4tlNJ4B5e8SGEdd#Vn#l*1H7=aLk>zaF?v@S4dKWWaZ -?f3CGv;WpJNik%)e4@2?4d}VYJ-|QbRZT5!_cHU(#<&$tscq`+d%c*y}(|v_qzvI1sBI?`gEuL0C(Z$ -g8AL(oqJGstAu@iMR_lsAlv-ziOSEn-D)#Fu)oi4_!2hz``@4BJ&>M*ok9lComej=yg^o&|%+nUC!Fk -s!2K1q3z>XVx331!a{21%dP?o3yGutVXZgFAK`M%gXO*xL;_hC&4a`Q2b?SjVk9?G_;qRH1(o>5dp`BJ@1o%OI-bK}kG -n%koAo4LMPQ;rvkbokJV72eHP#qNX7Umm8d3HW9b^HaR8mp5OgqN&%qH)v}--^Pztir4*Rs`?fWJf2t -pqgSq|IExwD4ry?=YZ}+er28<~!QI<9tmRO{;c%v{sm$P&+MS20&3eV%ecVvHG0vT9Y8oX?%}g2B)Vv -{SYF=iV8bw7t)f9j*d#4-I*36*Vn$427W(Yk!R}`(@GYY2xScerUDi@%7sBM3Wq9V?dssv8S71lNnJc -hk8dcI=`))MCFFf*;MM*e|57%!=M;%SbU*srv&Vh>T*lzj9M9^g}>!bffEuy0f=_9TH=NCjBD)pB5*i -79>heZp2JwvLu8oP^rcpAJXanu>1~6|v-^uj^3~&mQ0r9Z2J6;VZR|cR?cJ8=K)jb!N~f7hiK&xtGI$4@rl~1J1 -Gg(y=3DD#;%#2h^HIHoWg}SuPp@mXZ6kMW95?ecdM% -CKg)i#lt!d_IuOkp$PDBiim)KPU}*ttlmG^u5}sPrmWy3s?cHR&Ml9MIZ6RMXSGO)A?(u8NA#a6QwA6 -4W#8pc%}mrFteh0%!{)Wz?l&jC8kaBswppBqdXg)M-g0we=`vCY+Z%*<6UTV#|*gP$9HmW>)NqaAv1H=Co&G_mdK*mg)EDWhx+RHyWb@KifhM(uJaqvpFPqrPmX#mLP -O@V|#8j7HiWPhaOWbpe{7t~_IbH=MXSM{E0Cg*Pp0YT3l2kIv>35y;UX8vODbn^f^Y_v&G&kSN1V2y9 -!1a0(XPI`>OBp4!&DL91;aMS=A_5nrSBp^l2u;+=}9aoiF%M5;VimW7oy-^?njbXNAwHk6WazDzi#dQ --`UUJ6%A!PHGZQZ)$KQ<;v9E2rRfjnP!aSJUjs}X}wRY&t8P<*hbL#FQI&+!Bch93!+;T-_u -p66z{93%x%#mumoQ63pRiE~6`H70$jX^En;b(KdZ(H!Z5uLR1Ro0~IEPVl#xiPEP9@M|(bs6SmO-kT_ -V;zCNDOw(az+L#thSK15y&j!~4kw!T*j3_i;_S2wXWts_x6CI?hO@4cIcCXNr4<$UtwBw{)M3XlO%jN -jlCoHcK1VyDLu*7f^*R;WOWF_Dsdf6_Di8Nyr6RA{K*@0ApwVoG9ZQhni> -ZCbT!c5;HDK+0UX&cLuY!=$ -EN$+2ZOy17^dne1S}(2d^h67$u;e*MwAP8Bd_Kky(a81Nw@G%zok_OaI-!;R?)u_c;WfyW8k -lg@fUk52mb+^iHUQrOu%ohZg0l9QL8!aYN%3)+w~AP|u~w0TEcwLVD5osNuApzf-Fsq9^LHptMVbsjM -_m-Ea|leA3@;#_A4tscu9M)lE1ihV^9?z64G6cNb}m`#EbovZoqLdk(ptDNOKUv4pR(9r~tpsQQ@XfN -U#N{aS7AQIPVB=!ZeqkHolXqfH$IHHTN1N02>Icq^Obbe|(9Au5||&*&b~%XJJ)F9_7p+rGt3i29mwZ -dMm9U*h-l4QpMQ;7=@xH8XsK$?Nc719cC8$vYv4-xI$tj1Z6njt#vasuTR#mGU8L;Vmwj71M40-$UH~ -L1Ng%FtkqF?PhpXR%n@SkM<*N@g3`WYB6LJ6r>f0T|}K>FXsVL*iiajlW?ZyHZV#zE}vVk#%nPixrQF%RR5VG*G$YgqGBcL=yaw1pO7q{#;$NL!ttuk -pNPEg{omp`yEF2VP4Si0g%20uQyO9f -3K*-yVk^mEqJ2f4V-&w(i+%VR%+w|t)pslKZR0H!R)B3_Gq$t;xT=t&gS2Q$G|CJ>PrV4NI_+4{DBl7 -Uu(8M42!yXGza@9^*?x`hy=nIH+I9V+~{l`bqCaPxy*J%xb+hDlR^z5<_{Qow;vMdgVj{O(@nueRch= -s1m%N#T-D+(UZyq6*5R(;Uzp0w!c=CcEpv+sPN-{HPc=n{%Nt!D49+UY6 -V}{CWe^o~f*Nh7v_3Zm(4A)FkZ|3Dv_aOT(@Ieb5+80NrAZ`;q%qilX4PK+;|bzeA?wt2&5{(Am4yVJ -TvBbUFPzus@sbmx7$)@#5!eg+q{{CTj>(XzqOc?69gsQDNw&Bbx*6Dle+fJLrz16^l?*gwbdYO<9X4s -(K7D@XDO-p{>zSo0my!qny4*lO%0;Wv1HAsJ3hF!;HaNSH(b!6R7pW&&`ilyW1ZX!xDl~bh$%O8A1Io -YkNcmwB2_;-vkd0c;Hsnctyq5&aO4PkD4Spoy4};38N$nMZMZ}*X82Pc(5Y3eT~=E2Wru}P=ioK?Y(VvkcRV>Q}Ng|?pB9fx^9U4I3u>s$ -wgcLclYp`e-xXKWYrwx4ZZvU~e3-Qsxft_QT`0u?%=($R<-n#_Fzt*hZ7qEpGpY66uv(Nc>(xtVE5^2 -ZmrX>H?L3s7pmq*B~>P@6pNM{rq6&}!^YFtE_>t^F1)$UkD8c6^}aoLJ{DlU%k8>C+({o2$|K2w#@au -AnDHBDAq*QM9eZtC*|3sqJH%M;yi#63%F=K=payMn?*^N;6i^ -E=@^gn7>l%z8qoa-Idp^8=9M0MT5Idn9leB?q#Dz6&tO!|J4pI;v|ZnX9gy%CyxtE%;r&t5KV^Jkfui -DZr2mxh{sqh^5~B{u3kSnn!<*2-i%&`Cc^V>&F+JBj -$ndMmq;t$;5;73~M!tKjHxzXc1u2m5i*S1>Yux^&U#}fYbT7WtO)41q(ZKcBkD0HTf{#Q4?y^LtmYDj --*$Qw$In#)>7QT0gHOK_LDW!+>#F}Hkdon+FG5n6n%1?!iwK)8Q;KA*0TaYxW|#1dqiIy-2mikkVCdW -@SJ?q$Q4DGnJq}X^^c0;JM;rvjlUNxsNBqq>!5~nF#L81Psi!}h_)uwOYmOBZ$n@gr=#aIn9E0K-9Lc -}>lB)g#jrhr>}pr*`=YROt~8zEu>LHG5z`NIZUhHZ$+oZMiA%1g-{^g~g6SqvY{Y2WShSXhFG=5T9cN -Hd@Bj+638Yln&Z}&#!o?(}zmGxvy{clR`#{TJV5Kms(|6V94?VyylkucqPkPeqyBbGEqiwdMs@?QPZm -8y=7BubFi@ju+Qn>K_V}gh_kEh9OKMQNWX43?IWy=6RH(1*q78ZYvvnO~>(!#_OdmQB$ysiSIknMh`m -BWvrHFJUE8Z@?pV%U@T>ZX&8WsGpkO^!>wXWWGLN_#bHokicgZ=gmB3jK=e5t`MueY)h6THD7OMfyST -ezK8$1N7H!Vl`PEVgFnRgPg5No)$mBK1Z_oskI&v@eZ5A!o9Srm#DfN#$yAD+3n!6&sniRgWCI#A<|v -VtdZ=_7VvoCH+r5_4BPCFVyHGk)V810;^r1nvZyc}fB_otNSk)*ZYCP^}Td^i7f)Zfh>4lbE|jXg*T_s4KqQn{w`bBj0C7TOi6_ -7f3x!$=(=o>&R&P&r?J;A^R$ke}&zE&V!M6zP?)c3+$4-Xfrn%<rl2|H(sA~i)Uq!;q@Z&+S`p>4LKJnL+i)d{te81PmO3L)qLLjGAf9zSoSui=)lm`!6 -o?rEUHf-Ru=R%9GlMEMIks+I{j%zQ*>J$@gv4`U()KeK=e8h&7Al`j=-K2z*e(*PG46W -@wSn~==RlsMn!?n4sZF|>2E8=H6c2!RgSF$hBxFxl1Z;ClU=tD#4fyU+pv2Cx++`sxxdE4F~MFkZ-ao -fQ+o!V;m>e=D8)Nly1HlMXNxK*cyKSM9djT7#^UX(7^H3tf7nTuYm>jykK=Ompq@z|$u?kiDjPYsvT{ -jvDcvBhd_vx8z62We}jxIrPd)M%>!dCM!$tnRP2y^D7Ko9OK!bFJ?wp$aV%Q8w%p%|or7o#sJ!mezJa -TNBo9X2vL`9S-jPd+pw^G`o8aG*l&5n)~6DLyw(p_);1)r>S(88U&NL(jNIXdlO_vL#9wPJuIXZemsK -3Ctp%jzJs4I6g&4jCK{Lh=UwOKcq+>uGV6ty2jS(!^3}>fnh`=09Iu$s+M1hW^dg4_;rV5hNS<2TL3{ -XR_b)8W0Df{@)?lU!(D*EFV?oOVV|w)wvpb9DJ%-?1eF%3rq^Q|>k?*`wR`R^*W-7&Kr8`jqG&eIw#4 -oq!JI!9OuJameQU>^&KXCc{wl83~*hU~_#7Q(ir- -GX>_@q+!jZrQOApxsfMT-f+<523s1WI1bTPeJtkaD9(KG*zTaP-BQYyr(#H -i6Xq+Q;;?{U5N`zTi7}?4zSSo1l4ujs?nt;G%Hh&S=j>qJ{u>AP4IHQEEK!v*0Y!8>`K;zLu->f38_= -uS8s#A?)_+86;I3wu&^{*S0xg(|LV(^#IU}uSn9ChaDVxMKc)6;UEr2bExJpgTsX!F5_?=hxc*#1cy5~Y~=7G4o`8|%AwmZ1|vDV -jzc|%3prfL;RX($;P5RD-{bHEhXRKk9QyKp8^qyg4pTV1g~KWirC;cfuI}D`6P0N69glM`Xnc;lD;ez -moA)F0kBsU$-1Ya;;*V}Yh_BXkcv^#Hq}83JYEG5W^yO*>bK$QP;2e@krjk*3w2}TK_ZV^=(G!g$?>X -c~q9dtf22oRAE&NR&OW%o5p#5O~iMPY#s+UwBsUEDn6^?pHy@@^}iVepq{_UnbNXW0iJ`Hy85D -qb=_mk_oY0{nDZS#V#>s(|WqPb7;F*xl(yj{!)HYK2mz%EA50OOD2>$7%C5bBbDj!QzEhgevJOMC?UEDKr%_ZoZZ7`ap0F`Btr4%ZgW+|firCEw;$E%E{LhOU|98<2NpA8 -F5mK>9VU}-tU)CuKcS(rcOjd@~zm=~m9fq7v*m>$!R**Rv5DKCfSGu2ecF`!F<#Z>BW0lf@)#ReCTOb -c_RmlRXa*%l6S%$N_R$21rZ<6t;VCv$W6@bvPQ`}q3#2LvjDg8PJohV|_i-hV(ub|9Tz`lY(nC=>#k24KOuSIq#GuusHUW!5Ev?ODK-R8Nhog8mp%`vta&iC(EKqR=iw4sDux;1ugH;tUXt|Q+S488pmgUfrB|43wZ9`h(oQiXTlElEfAQxOFaxJAEazd`7POkLK>5)>! -w2sI@>>XgDbuo2Gsit+p*|U(gvwMG!b@X>Jq%c$7D5lg}1Z`D92}Vep%lnt*UIdV}R_ZAhvc!d=uDs` -3-k3k&ugm-0;6nFQ$S0HckI50jC|yFShhytM!5`h}7Vl_>R40jkJ+&aO7kZ2ZIYi3GLVI{2r|Olcfwa -JSy5=e6cl|%vpUY`WuRQL0D(|{~qCDitWf0#)r2x#Mr;#L$O=pf$eu@7?{j+GU22 -w_A9>+P0xv=M?Uc9{iW61a`_IC-CX5cku3zJ$yY%1H -*B4_j9>qRgLfz&b>l^RVe^;MBO2#PLOF5VJ(4$iR&iOu{_7NLx?2TmvcOlI|N#AZp>SLmAsWvQ4-eoj -$<1mG=^_HBwn?<&c^WpAt?v_%n<^OICp1ocYmp3+&zFhYcDc)gp? -n5M@r@Y&fJ5zyNbI9OXXc)ZlzQncSmt|b_a8pN%Y`uNuE`5_Z?FGxcj2yY-etfyN`2sIvx8aDwm+Vfq -iikeaA_&UMh)&KPB{ZSBX*&7_QWu>oi}Nej82yV4Y6aStY;+7;_1iH&_ms-(t!E%i;eUXu9sDXdI(Fi -sqluCASF*M3+W_SZ3m6XNg -*#+6~1A-gN;3tO|#GARS+^Uv)n|C}7~um1f}`|&Rn{#;$IVOQN`-tMmMt8K5d-Q9iYW$T -V#`Py~XipsnGQdPa)wxQ;)ch_#b=idA7f8fDQ4?XpOSte&fxz-hO9KUH#sD``ByM_(;>yV;_F>@h6`i|Lnx)Uwrx1$>y)W`S#Rz-~S-A{ -AfRY=BKl*KmYRUZ|B9O%m0rn$a)X|UqOHT-s$Hu9tMKj!^QnT -H+K!VU1f>Cc5~m|&0X8gy|J77o^I}YySaC`b#=ei&5fV82=JI?XT%S{FR^^%tml-ljj25HApZT%uNIK-4v&b-|q>QDULuP}_H;ZbJMiV -tinR?>-^C~aDt9)%a2z2gumEX0lJk-+@B)ZDVN+5iEe0fHB6-Z|>!;>=RmV>+oLS04%-twZ1DX%JrR0 -PDjf`S6M+`G(ZTy8X8CePuQnH%B3?>Eulo|V7$U2L -!Q!HqAW2Qij{^kL$1Y~v#?0|@-e3v9T=Vu4OiwAD|3txF%Qz9RvZX2x?4PxAtz6nZz@>=DOff`qww2~ -3TmbW8qEVpNHWhqtz;q8&(RIar51x}MPczGWj?LgW#Od;v)Lt>l$!IPd3hFNQDH8NA%(CXgqK*%y+?p ->AX4w)&UWQO!^{Syk#UvMQi|0WKCGy8cvm_uGMIskg=S@LNuEJjk}suT`HiMJ1Lt?8II2N50bEGensI -z}-2JC1GXlf15?D__{!5nR00zB*RRJ;#9}WFC9I?^@!@?Z!EGjHEK%WcNQ0C;D4J -PIAI7_K1Zed|@oS}Hxa4B&&K5+60mIe_EO(n%k48>+CzOx*IX-Q#m4(&AP3P>?(sig!634Pivn_epoz -f6~GSo-CK<^t^6Qge>U#rrC=b@mr+50BFWeWd!capaoUW%SH5&@Ok4!`Wm`=G0M1J=1^q(BaA)-U{TF -ZY5kEKWaF%v8bqIIpx2hmvwh9{oFfEdf_kAA`5nCsWQf}3}%MHe5GND(Y)f?KQ3<}uo7pM9H?k64jYM -CDN&XfmXz>MXhX;_So@X&RB}w&gCDG~XRBww82#{lwlR9BO&gZ4`Z-P!*F(2 -l9oY+g}mliHD78$and2fDRVX1k(`MSi>#=M1>4Xz%3ROb>RCn0(MBqY&~gv?Sj1=RT&zZlDOJI5jjIR -{Vek1YNLzyl+l?*5|!#tq)DAQ9$3(l0Ln^6)2$1YeU^vqzH~Ea1)AFz;hIz8=Ik(U(AoQRXKjeo#AEs -4odkRFKfIL9Kz!{!PAha-(;)S4MDuU($cSAL(xfnCC$?6UA88c1o9#ghaq|QYt6 -Zi-hLMq0Zh|XGgqoqXkP>yoe$X@=Elg`CX|!NMjwmTAJb6&(r?|xO<$_-BaDZh43?R&N6){eSL -`UEH6i#T|kSuV+c9_BZD%3H{ve~CNiZzQRW2`WhShT5(A0S6xlkUIlQTFU8qqO=}#i_6eKb;kVHaXM$ -QUv?b{sM)Tb`U7);yc19g!T#VluCKDnNdHW>EL+c{5+2HNKB2m0}z>5~{h`rPA}A)|c3c`VYG^&9kOq{-Q5Qr&0FAY=j1vPz=UC_fUBHY4?qUh -ZS&)0YROcWs5@hxU{s%bLzfR`kMtsJ4#`ll+BZCw3$Y9wmr0Yz}`nmxKOKAh;08N_N8^AYPDyM!m@cw -F-azbGa%nKx;nE@~t`jgOmpbbcmOkbdf52Z(Nuonr2HU^LFn%AopgnSCm*)Dm(13VN8^~QOX`N6vZcq -kO;8#;^U4?LlOJSn{?p9BD(0Iy_nUNHgx(eWjx6OGOQI9|ljvUtw1zkLZ -3?aHV+_uQK1P0-1^nXj&XV$201L!&cuc3IaTK5#Z0-a76b$_xL_$&n@9}MwH+waC)Jg9@t>c{#4D}Bp -ier98`rhRH)erb9Bq7k$JzcybodVJY62&WiM#@LNZrHwfS)dyU9P1m;`8!_Xv)*8LdVH{*kfYlfjxe~ -NFYqqB$62J@Dqv0yBz;WuO_%NHI~>1UHi`h*C<4X`%p;7yOm2K!J_X})bC2V)$%FTQW_MWv=neB&|2)UM+ -bq@ZhR+Gff=7PFwT}WJE{qi@AI3wLQ43qmU38Oq~(0KV-@kT=gr^C`@ez_Vem1R4Xf{WI9OgYgFA -vwx=J9m?i?hzE08YoF$zCPoS0j9z4}R+PD9o^Hf5QAT7wGU9`CY9FMfFVGU;Se65wSJz47fRBF@jGvK -iWPq%ijT4|#|6wrKniGL{6G%T+Xyl0#9v_K!!3Ci8 -Lo>hYiN04B-veW$XRB%%-_gbVS+x$Twz4&Z0OeSDstFE(o -{Jq`;Et(;sRRXJWW}y!HBs5iOsnA_*HCqufcBax>B`ap=Hq`gDJygD8;V5#$%Sb=%zxG*^*N8L_+RilL!7MTK{CZ?dX5Z?>_pIJLwA -<^BZJLj6`oNg+~=ypMaHUEH5j%68>*LY_sM-DEJ8QrE{c4&x6($|&_RFg__pT$KuMryDTE3{5)X{?Pu -Lpk{fLQ*#y>$Sty|MWvW!PLahxZbjE@$#pZjXg8N5RbBY&eU~wnS%4a99Z+o^#%HWR^lto%fhsT0M0; -(Jb38C^gZ@tRoC_49I@-xt(!K4s?_en>d#o1Bw+H%$G4%oyY7VgTYAR-1VvHKt -jr4P})BCSx{427%^jMh)U!pi)7eTjZ^i?(!mt+H67nQ1d#rhk8v2CN>6F7Ld1`wtq#b*{~$D5Xvc~lmU#~&1VSom`had`jQ+NJqt}arWH`f6O>108Cb_` -VIz#q48ReuxVWJqK$!*5i@6{6B|F3Djbc{B1 -EMKZ^U0<8Bpqr*d}&Pcw`A&*pG0&;Mp#b`keqLU899p=bSYkFAN<)r-e3=XGAo;W{2}< ->3`PAGG&|J4PSl?#Fq3w(|0x=Kec*{SNc+PkDH_H=~6_v;XWbsMjA0SN8h*k5}!#^6P!`6$|fubI|{6 -z5kuR|KE9_+wcE4OJt1W@|K$ioj$h01f9KZ1zx~Pe@~rc@bPU7uKimx-O``3x}3u85WoJ(6Hh98$$xD -pgDD)oc9ro}9KH_mk34bei2>xhCxj<%?LGh1k5g=JSM%S%Ma1KK`4B}AEA>pv2jX{CK;~X|}SjXX89PZ+92Zt|l_&kT(Io!(O77jOaxQWC2II -QKcio-Gvi#W{dmj8SXGda|An8INahcO(6a~Q;-KZitW_c`8f4t+ej%I8hozk$O#4tH_*5{KJ4+`?fkh -vgg^Ih@B~3WxC=hI1Iip`1g)VaIO_wsI(N*v#Q^4x2b^;INLvT^w%bu$IGe4vpRN&*m_L!xRopR5#9g`>lnaO0FDd?8KSR@!hi^%8~6tS91y|c#{--JPj(*}$ -pAPC7V&w2#{;y&6FopiwgUVbo>0I~0US1v`Nse}#PLRe-wcBK_lGzDR}N19Ac2WH{sl&u@Tl2ly^Lwf!Itz_u936Y9_ -b@V0ANTe1NT9>MCM1o%lT&=KMs2N*w+kOIIFegjW)IJ67kZKDX;0RGtkBSy1!MFJdz`#1qt0(=;rrZ5 -@V3=ntgc!bJ`2f#bWK)(RK65wCQuyShwqIj+cf849_St9f`;sAGF5B&xBTL44GL%D!Q0$el!$_2at;6 -oEwKAQphB}3bxp8fzwB(t`g0p6F);vn3?afBC=8J&YB5|RkdamX_T;5>Mm0nY}Q1J5bIjR2RzBLHp&_ -})a8Pb0wmNvypE0DqgrXoc|9WI|3ue1wmsfNTtLwg8M*LElBlDBK2%j(G6j4)87w&;jr&fG=rS{>@Vf -xmnBTJRjhzT2|*B0I$^npF#X2fD3diem20cG*}OSe>lQ4-WLGt(pXxAzfK390sr3sTJ+4nOpkV$U?{? -PK^sif>CArvz$ZBV7C@2XJN`NnBK)*vA0bt-vmUbe*hh_t~;(=!n-Z`K3<9z_{SOEP3{wo32Enw|J_|$EX7x-@n*qR0W7Ad1JFq`pS5Wv!GRu=B3*_h4w5 -TMBYNe=WKJeweXEx@nf*$#L!z$LlR7Qo8^-kt|-0lWy{T?STe6~K@4VJ!*xaeyNVcpCxEC;|Qh{|tbO -jf7mpv;glh@;U&FSjxvQz%xs6u7vsn9Bv|HBH%Fqi%qPaMu48BjJ9%sJ66Fw3~>;qt!DY?0miO@c?0} -M0?aIju?ILpp`4ZLu@>e`c(#LoBS4vz(ai&3wUyC)1KzDt!RU<8uM*Z*IIaPvR?T -iLvgaKtv|KN4WgHb#>z05hI~dg9y;Q2sQm*D*f8AK*zre%nsSYw&0R-v#h8zoI?P9`aFw&9AMxJj8;Ja!(Rm2K^%mSzR2qau;eAqO8{SbiKRtoeVOr31wh| -d814_SXgBZ;#78*&4VXV+&eQ`O`X;ms@MwS*c(MU6132vf<4G;RFAuQ#Gy~js5bu8jdIId%!0HeVaCr -l(LmA@lK_5Y!T7aI7guDfK5WqQ&Y_6UM@NXR70`SvDmd|m3*M7k2FcRQ_57-!K0663`m}9Y?0FQpgc= -98Ft4}~*1J9QO{Ots*1Hy+shkghD{Qw)kg#H2?VeLs+*8`4l>q*8}2-BL`*wF*@J_TJ1xE!GM6qAcK0 -gV41<}C0>*a^=UfRi5xiTi=eKLCv!NB9iKcL59*U=D%!p#Yz3fp;0;TLJd}kJjro{=Xo52coqS1gkw35FrDKF3pkEv1gbcW@Oh3SJjii`XE~00=5fC};t1n -8j!?&O+$(M3I6~a}i}ghK8TUtcmg5Lzpa(yH^M -6RIn5=WH(mE^0e@Vd>;j1CngC)rux&X#h66(l7b`IQ%JJJ_ysUdZGJ1kj+k-#V)%D2Cf?Ms2K0ZtY1Kxy7YC=yReZXlDs)>3Tx_^55vuqsg>s)5zSpb4g)gA+cC2tfR`y -;a@q~v}qH0`st_1?%lgdU0ogd^wUpCXJ;qjD!XzT862(X@+WsUmirUM!R5=3Q0Ecx?8S2bi_4dh(Kd=T7naGZs-;9kBQ-NKQ^#zyL<%VRnGcUHWZPs5k5Y?Yse#Nq+&UVcR0f$8(}FEaNUjNf -=9KmW)hb9q7JPIW}0|cm!f>Kyt}1K -J)OHBo=|XJ3ctFb;$FLG%h~ie_AzB?11F-n>Ez`@ROO19T$(3*U|V#MDfy`JslTmFvbV^wruVYX)vWf -W+AX(j8BcAA7{Zde_A&dOypJk$AZbl!F6kz-ODEf3k-I7+~JXx6E@NG^2m5R*L0J6dDg%qmJ=B~?(lf -Sqkw0yhzpjIMtpM5YjZHa-G9aW*NEbo_V$+u<>EU*))_ol4Duc<5_kTPD?CyhZ~iD4o -sdQ;lulRP*)VL_FcKRZOXB0>$)rh>h)Sg*I-QQvZsyFH>lT0ra<3{ -q}gAbC29(ssu-n^M?*|LRf+qRAN?@J&kzxLW|bS``At+&XB`(7gJeiq4Vts;5%-FL~MLx;!*AACTL9z -9Av`sgEa;=~E^<(FTQv)>&jAO0eeufP79@mmJS -NdI?8UkB-rLHbiJ>92wGDUkjaJevvW{|f1!f%JPJ{ZUAN9MXRU>A!{aEs*{!q;GRcA9oF`e^i97ma=z(l3PcWsv@_kbWzqe-+Zd1 -L+S!dMI<_7m)rtNZ;y6KN5174mlJ;4u63hwm}a2A&0Laht^<`{4z!)zs(d$+ftFVZxqS-mqqgXQPGh; -0@9Cy^phd|EJ&XV>6bzJDoFnjq<;$1?|}4sA^j0Z{{^JCyQG(M3WP!W2uL3V>92wGBO(0_kbWMdUkd3 -rK>DX3{T@hv!X(WzHX{uCpDx}AHjUE{rdqbA9l4 -12^goN~G;k1s?LzGZSo -g@FOu+Wf@>62)LSQ;TpiRn|-7^=eX(2#-CdDZkIumnh-HCdH@V_KRfEo%T~&_BFizrOzd{*iht0mIYj -5Aq+-B|;XG0mD-@>AJMEbWQr;E)gJn_z0T+lyr5PCS9FAb4KBRo~%sYnWp*QB -Y_fyz?{(?$%O%GqP`1eI#6yTUVma#ng)x=zDu>A3komzp>wHO@okdm|4(TItj(M|jrwxF8=dnJicfHN -+~Wr%eV%a)zs}9~YnC<1X)`gTT}&z{`n=sj1HJtgI=j8Oed(p4S1InuWRR)Xws=QZdSmA||l}A<^ra5Gfr^{| -2RfQU|$&^ZVtkaN}9$rnZZvdGB<~i-BBDv`sCNq=)eBglx$Rm$DLLPneQS$iXkCUxiw^CW*)mLAo^57 -e9yg_A!kL#W$kAS@K637bs_wOf%4scdlW+&OaT(j~I7T_ -i7q`{?g3^K4ucyy)=b42;WKR8p^i7v0_PqI-%2i#x~|@f|W#Y#>X;56MRH3-Yq~9lXC=UDCq~F#}fgM -tIrQj)e5rLHZO(p9bk~h4f1y{a+ycqmcd;NPh^@JKuAE$|--!DgRgH6vCa2Mh=w5H(_OP+%Z_Q|&(_X4tUd!{UO26xRlTdqC9C;lm+raM0km*tl2{Ib=jk+(6&BV2BVH5HKh -*YFJF1pKr_{?x9r1jEIxV{bL}_wS58u288z;5$7kD`}%l#`d_P14Dj$8Fk+ye+!sCKf(D0q%e;rg;cu -XiC+6Qb+${uLenWkId}3jIMGcE^kAzT2G7#KQer#mu@S(ABaj|GP4ay%88yg)P8x>327YRSy^BWaKon -(!Ikcp^-bLUTQs!^Fs9E^_}`v)`q5ZWBbrcsnvoM*4%CmliXa(O&?ssD}NKcg7N`r?-Vu)%`|BT?}|6 -M@(G`1rugLa(_*bKYnI9A*l7Y(S#;zw77D^Y#CK?VWpY)K!+p8yOdCs&1bDX0w0nsiLyz|b -clO|2-dH?4Kk+%m`Ksz$Go>MibE`|g## -5|SHlg26FxUxC?1X-c!#=h%6pNiobJ4Xy3klyU~^X`}dp2V88|-Z|Dg;g0@wsMG_B)#C#(1z=tA -Dwuv;{EYf(B$P+t6@{fyLxOlNxb>(+Aq})aSh7B90*Q-~rna1!rYA@l3!2rGNIr|D8cmeG^hWCdK9Ws -;0>^b`ly~qJrpqu@N$KJn-#O)P{-6PU^mq?3`M4E0_3|mF+RSfmt6^VF9B&qte+i -7`W=gsH|dVsuiKP2+devuBJ>HdR3dC+{P$o+~Tb)U$^D3{fO5JUSSw -N-V6fBTD32%Q;kbPHvg}?vPZo~qEVBp4$ZLaQ4G}%R5qa_}k;jjSJOYN#9eq}riMIOuVNjp3N%c1b^| -{gdpiZmBE$_bt8~T;cQyzHWfe5wd+0c)igyKN%k?T+#77MbD93B2>g}gZ;T71LW$Q$Wx4F<+D--Iwj@pG3xgTc|~#@HmasduSO`X9wmXHC$y>KI!d{SgrnW%92+9zXvJexSej1Ir8af#((j^sC*I -O|SP6|Hx=rG@^}o6~jEmz(`R0`Ltr_p>+J|SP+BNXE0cO#wOkOf$sZzBKLqn_dk5C>i>3gnNP~2U+uk --f7P`d;vsa-;;>l2fIp~6wxAz7xpHhfDSWZDDBF}xN7)nIC9}ns?HmUQ$Yx)cZtIu -Gt`ixC#u+CiO$58swGyIFjS!ELpc8208Pe&{k#_BOGW%;O9Qusn^S(2d`hPROmXHUw$OF<0JDu!Ms6o -X5jJ3IQ^t~h9uqCO7#XKR!0Rt$C5m1~y_{Tkm-Y1XV+gvKb@(2rf@h|GY2n8#w_HGHj&%XkleOU%G{C -#$8b%4{ji6hnbxSgaV<&l)7>&wN`YWAm-zOi`XhS$%e5jJr3KPe1m;%73d?t?I)IJJDC@hBj!oSg`%r -d-59WCcX;a#@H|}O4cfdm6@$%>8REcPz<5^{5Q2p(^J~Xl!5JKVp0bg->-v==@TcDhxL?0`-%((cZ|{ -4i7_goU+uk-f2Ah}x}l3BG(k7|0Ns`+*m!q*jQ<5A^8en%w(_oGSfdzLj8+V)3+OW#ybgw04u)x97|> -oO^j8dh<0P}sg9ZZwo7Bn`V>H`VzWie|@L|Le%*Bfri{I}T;vzfUJgH1ie*XDqDS9J8Hji&1>&Gev)r -Gee!&{C%W0UfRwUv28+RDtd7@3h8D^ru($z;Xwl42OA7)C1wMre%j=i>715A>@n82MMZ&IZd%FTEszK -)~Rs3J!~fb4zx=EnBi%$R@?GPB9pLRt(rAtIxS=ljaVIk=N5=aapJ@>452tG!}9abSLEpC*|Kd)2iZ72O4d30yxgVF3mgn{91MR|Im#K>P9`M -5uFOnvC13!wicGf80h -mWjy{KsO5-2shxZ*jc7zYIWXTc}dlAb_nlwp<4CdO(4j+R%9JTaufP2AOJjG4h04PM-S`G<6|oTUDER;becrWbm|VG}`G#+Jn -LgVXW0s@O(^MC3MF0Br>wlxM&Yvv(jT$v--nVbx)INRse5ZEQ@WJc#%43f`Cf&MqlUH7OMP7UDHJLGE -hM|Z29(uumFThV&9ON)yz@8$9_(Im$Iyp}Eyb1jmWygZy*J6y}!59O5#wN|qcm(oqn14k_M^Ad>kw?Z -NKPv0fHSce>a^=c^>agg0l97=isuRNVhaY~}#0cBAZ8P!!?a&I3tXx@sfDhRt&mpdZ7w7{zZS-0B;MV -6QA^QB$>iNWhbE+adEnBvnF>BT=QwL#fN0%ilD@)?z7-Qg{{i8d`>R0>8!JVt*;)M&v%I_-IA7;*+DcRZC=cZ4eF5||HyU?>|Pf1QrHv6u5r -I95tpcmkTHgJSup$35;M(**^=nC}+@DQ77BpANA{)vf<q`HB@QrjkS44E@L^6Ir -6(%xlm^&3V|cVMc#Dbm$=-shZ99OcQD)eU<@j<5&F74{h3+4_dkM7yo3{R>0C;-1pHdG -iR}!))~V-FM$L7}y``hN>S1LqbA=bm`K?$ku`d3k?12HG9ru=td8)KfF$Ciu@33qX*~!I>B|Z!Akp8y --s#mSXiv`-Me+`))G&!=Toby^s}xz{;~VYBwvIc<*jIJ!Cl2hAC%vs@@8ZJAH^Qqb8I~kJIbD7|CcXc -E`^1K#s|Lj)?4O0_FDI;L~YwM>LW)f?OXJ<37%lzqpJNI6TcDvVEFAW^Qh-IUihn9&FHyY+*aWVrkd-bkR-9`!DE+7S%5!|LVW9ZT?jm7F+YQ{X2NWYw*G -LM4)90#PWG6blf8!zw!RA=c#h4-)?&vrr!ajD#eA!2H}oUBmC1QU^6s`pyl?9k>@ -ygVb(N#58Y7e_ukfq?b&F|d@bj7f`F}iFgZmzC$BSu{D)RL<)BSswYcw){5kNW5qA8R_t8 -qU#ki;guyuJ2km%Q*dV=d?S8|PTZImS50G0stf#~K{3|K3ix=OzaC_tbLc?Vd_v -Me2PzKh$|KBwyq?9hJ~}U`2zq!Zh}cRG;5DF){IRjrZf!=5rf1x1AAl|A)sVr(FBXQjy`uL^|#b_SmQ -e(^I(rJ&~tB5!w5Lp`|h!!1}w}Z@;~T=5~Wsr%SPC8gpJyd;6{0rN66PI-v2nS7X}&8UynkhWm6zXuN -Tz-`BxD8MRRQe65{Y?x*KOc6~1rv(>DBx{~#OuQuGLYnHnE>Goa|TR(SV{;N9Asm(1`yZH_mre{jOkv -8$tVm~`CQuITWdcAe9)8odCOPDcZMm#hSJCQ%&AF=EBB5XCWEO83&@tD!8SOp5VVu -9X}v%j*mBW%p9i%`1~o6XHN$ErrnMO`@Z)60k=HTAEM6~t;f%*Ju9uWU%i^zs<Oi5nH@ -|mk$Nt5MECmiUg;lq50S@Mr$d0eApU$kWc~ZixS+nBZbg(vZ&XGaCjhZU`1!}^pUQfGW&+z0Wks(S;o87@)8#+P1)4e`%# -mJE(Q^$-MGr-+@g$C*->;;4RI=!X>r{71PiFzpVNRNr$#(1Z{G^%%SzBVq&&|u`T=vwbPbp6m?aA*%0 -{%@WTWBOh6il}K)8@KXEU7Y?3y$RFfa@HT+=Z|Kshg|a5=A-NRPfRvx(ximBbLYmRi{$&n=ir7G_JHF -aUy|t?(yyYf>UV0}^PTZT-hEl+%}rtty})ltruQ+Ayw`F0NWoRFCe#LPq01g#rcxYWyh -;sO`m}J^irq3zQCmu^qD3*eX=nQ4GbfXy6)3DzxeTb{kQoUdPvOg?z6aickI)RdPc=t-ZDKFdUe@KL# -ESfbd6uwKh~c-d2+%VZ@dvtZ-{stI&5BT?;C^;_5ypvdkp%wg)dywiSp#pC2Qr{e`4%Wqei7pm@pxQe -V96Rs0RaQ*PqInul8P($D!BcaZct_EMKnw*X!SA*N=~nKN=GgW9qK-rjwGAU2u1V?6O=Ah@FfBu8W$VeGDaGD8;3!Cg_UIg~fnkK21I_43^JT= -^ILb^lpgeR{9U!h^};K63!u?MzCVr9VAkgYtZpSY5ooZL@kIbCBUzSvF9f1{kL4tu40K -dpcN{_oS{p+^7YlTQk~pueVmxvXEnzFRr8_lVe|`l?I1pD*gQ=4*b=e^+d9gSE~2w#Lsji0#;KYPrZD -wRHL#=!d(H01vPWoChuJ1$G;o37s1^Zk+0=T93l$|CA|H67ur$;%&|ipP`{&zkbrNVMBTJ(MJt!w(e* -9LwpXovG#)d5|=Mup63bX3RM}#{$YQ{j~_pP>rs=(=VNo|Ej;tgGp5IkUhr84dtht4#LM(lh}#vz+-j -^{wti?0%UzSe9}O8Y#MlPzRkd|R_{Cm>8=G8QT)e27Yn%01M{SL<|K-tPW8h`WmdU(%^GvN6+eppH_7 -rb&ZL>b>SpSPGf!D_1=$`FMLIW~JkAwQt>eZ|5eS$Z;b{YPv)IhN_*gp8Ybm>y$@09MvBu}t@a4ggy{&mr!Mc3J#Kp=2NuQ4b;Ki|BrpZS%~=j)W4o4cW)puq6c`Y`ycI=$C@KK-=Kn>SNuy{y+bqVjh -hJtQ|qUJEq`>2@%9A5@IS*K>}UORcu&^I2q?7|zBsR;RFK&wd-snTSvKXjt>W2iL{N9(*usp=&lLa>s -%Rr-C`~>K}gLhA-$`g>6EQpw)iAqiFpCnLa2O8}@c`C-O(~S8@e%l}OE%W+`3tTFJ?w9r|D=f;My4@b7<$*Qb2CZ8mBCvGQiAa@#__>ftn+hLK#x-WC) -%rWsG_lVF}gO|ulXe?f|d8vu}NB1s^xdFS9{ZqeS>=r#a?jNwel(8(ct(-hkZ1$5J>Mu!V{qvUp$R4& -HIfa)`Jn@9tQ|vUnJha6t%f~%r?*E*Z@v!W8=S|LEY0uxVVS|a)?EN^{ZDKC?fjxE8!ee~7TmJdH<-f -friEH5}Ss#Byp1`%S%j)~bxX;H<^zdJFc*bw7{mK&=8MIFP(%S9LC&SI}to@7g$ -#C;4YqdPl3F+yHDbFX549`eOPEBtUF>FX`tMuOe6H^k>8>J-m9x^C>P~V}AdJjrzm5`p&c=&x0;VB8J -NqrO3?SJ)b6LDYT$cVQ8`s?uU=%GV~rJMi%S;eb04W6fGrYH6uHY922Nawk}G9+>EFkL0FPuC$y!;_K -|`z5BAz3G1Y!3;f>|D)L@ad={Kcrt(6L?onlN*z9EVB(O7@L@^qdh^d4+eGwDNKQ|TXdB(6f|IW;-lX -EyqnlhuYjl$`(E2>OiQUh(UE6hS7Z=y*(T95Ua31{g`K^1-{}boSy=%OiyxYA+-V*OwZ=^5EhisF;EY -4k%yFGV*Zb@!w?j3nyc@6TS@;c`A$m^e%kvB1KX5QkwHF?|f_UD!4mFDpSkDsEnxwUh%mQ2f^nV*}#I -DcvWn*2@q+w+U^_vatUFUdcfUz+dn-{G(A5A%on8~7vrQT|weM}Jp;5C3!i{{A$7hCjU)1T{K ->|g3%`aOX=0<{BSf$%_sKx7~)5F6+i=o;t|crMUCkQT@YWCbP$rUhmOas!J4O9N{ -Hn*!SdMS=Z+BY~2@*+6N)Q*cK??Sim^@PY;fkp)o&u>~Cq))ee7C@lypj4I41oK`rqFt>1V;nKoQg^D -G%hNp`j(!3eoEbm0`OmD7tv3IH0?$>_r5#6&=ug7+ehRW%#mu6MfTsGk -q0BG5;L@9RGjge*sWS0|XQR000O8B@~!Ww@z`2JU;*cA?W}B8vp_nn1 -jsW*-+>SGH`khqlNs)syCqt6mq?rL2?FkF*JzcAC%+Sv;@gx1XA-l;4Z$`Z_PK<wcvKu`ATx~g}BU^AH4>Gdq#4ay2Sa+lRq4ax_j;IJw?z>CXyT -}`q^q0v05uWediY3Q`sY;OL3Xz%NEzTx22-qcw-#a=!IJ2?o8+3YbiVGo&ui_hHc7EZV#n26cL$4zdw(+E?y4Kn-}3=N>1^hqy<@NB>-+mqc0l37 -~c!hrHW2~V2=&+=!_p3%R@STINfE)#GW62W!WF6t6qO>>g4Y5hp@6qu5V$ST>xyaw4r!kj>&#MkQF=S -3ka2DjN`6FjTt&xXOX;-+~zHfuGGF%b~AzKw=@k$}{eyiJ&Sz119NaPdrQ8$&ChAmCK>wT|+PcB^N~t)o1Z_T`lH -KG#Kw(SVaUJ;zaR%Jd3k(8Vz>4vRMJ(kw~hL)l`}Td!0T41s_OiMd6}^!GuK{=n2q*>M9^n9S$_0O=g -1mXrN)?3K)DO9AJ3D^YB@?>xe-Q8hJ@FSmUhS;CT}U&jSw)@lzoZ1V65`d67=C=;JU5!@(Mad$nt#4A -Zl1pv3s2`TYI{=6fG4@uPS|ap>e`QQi`tR3GV|kI3v|61ABF0jQ-@G8m15k8V5oxr7D#)_FrCZbPVgK -u|y9bUsJEsXh+8h1Me(w{0_q{NQBJ1!(_tuxNpNGxNhF++4=V@E_zgF#uwEuHYUoMU46P6ymrOv7bE1 -XJHtlDicxbZbRN#s#s_$dMbNp0Wf;>(Vh)L(kwxsclhH1+nq-P;QHn^pF>LxNaLa+6@&!wMhAozD{gP -t!P&5dFHU!_^wqgqtbZnthf(MgqeG(uV#W3MV_=Idqlr(TiNSiZ%NQIWDJ&N^S(7&_cyKkPmxy3kt$k -g!Rx=u0ioiu6(x -~Cs5p{x3j(OJCm+BAmMQ+Ea{VD_=c;HeF>wrNhjy=SGF=1oi -Gd@^?(Zp$9pI&B1Z(JY4VcUj$#i*FPx>Rfui1mY_e?Xk9^=6b{NRSxZj7r>OS3}Yz|J`Z-*uwt|&n6O -^5_%_ypn_pi+yN%M2*0!Sx1CH*+ -yzl!}YYMNj00Lij#5HGuAy_VobfGArKYSJ%s4vRlnLPdG;%-ADcnmhA5amoE-dKg$_T+6Ur0ydsmSqr -Zk|pLIdUL+pmtbG{xCT8A0KJMF%u}LG;4^dDARF^r8Ti+Y31VKb*XiPRe1>fE|2;&Y!GlZGs{>FLN*_ ->cupRX%FIZUTwYJdHv^CL;m{dXFH$868N;rnliM(q{V&u*zmd&bYyskYeHQEe-)3x7K~od+*FHV%EYr -^WYXE)kPEDvX9&kPC{Y_XlN(q~sK;$wW^GYT25Sw~L5HIpDu>J~{&R*Kw2OIx?zk(U#UnC?YyQGCM_<}>NO;nGjMr)&r%UH?7W2jIhf_yes?@~SKd -v**Q*-Rv9Qa{5e^xJgd?5H)!`#stO!T2PelIb9)_(3!66NbN|FO6$|<$kCqe`CWfLg?C^oyc&ug#aDIqpXP(Y&k}XGqwbT`V)@w&-jD%@GCwWA9Fgk -;?IVkw${{Smuh3RF6*<2E>_Jjc4J+PscZj%`YCv%f0BmBHaT?;Q4ePdjAyeX=Oc$Z5b8I$9+R_k}CdY -b3q&Nw9yx;C*@@K5qovNERIi=&Ir!i*fgUf`SC*JN}z@qW?mu17bx`&Y}oGjXIcY5{~$vp~gndNKIfr -KQf_5+F*c@Gy=tas1Nz5Zlog&ML?Z#L`#!Q!T!OOL;kL-3f;yF(40Hx*tf7c1XvP9y0S)8r2wOb8dQ^ -&(~}!1l6l&K9Z4|ptFfb9f++*6`7?@56;nXF?!;>IY`V3*)nvd=VjXF3y3qlU$fEF%+HtL#7|X;^1x9 -FF+kC~x#F_ARb!l|?VZ0W-?ObWtmOfEvK(Wxc$Vz_OO%nnrsG*T<6#T;eOr+ms+F)GL;_51Y7*aXp93 -p*3rg@#gYp5vkCC0PI3qf_bA@KCFN$@Fxq^PxsAz7;g7NY<+s{vVE;%5pm*#Oa1H6UU6tWAM$f}A3jp -^iPQ&Pd4Kb&tu^klsb1X29RoUB-bRWm`Xzw$cXouo9y~z&mO5F^T8ZJTmYb67Z+2jj}6t&pImh_es{L -j*SSlejbf$jlEX&Y;5%Ef+V_V=OsdezY*Eq)? -?KIsTvT_Yc2M-o5#7xc7GdBuQYkDJL1JYjf88ibsrdEUFTN=E^LaqCX*R0L0`bg@+Vjg#6jGi_4%$7v -%(_G-g?f+X5+tWX4_V%5hYo_y;I@ZCG87PJ_S0QyvHET#>fu79#)PG_CLRG9-x_g&zDvuDkLB(;C)-n7{}}W_@F~B5!h7KyjcVxf7~FL5=+o0b2%1iqidz -Lcz4z6ohFto3}%$vL`UoHLxDVe6d64{V-gHe)@bjWCqDd)o89QWoBKi&%BC?=1e( -7st&)|e)Z -;2pJ203zOJ`kIkmH3q2%K?Mq0aopDEQRSs(?L3Oe3gL$n^n(>3EBN0wx?SkI}u@YKGc%d$skS;XpRL! -646IRc%qir4$zV-={~1!{7=y0hRB|baIPgsmQL=$sB3LmtAtxL4@jc<|dsJN}ZYI0UY@~iOrU5j5p3CV7wonf -#u=+u1TR(>B%=EJ~XSi!VXRlxTlRX}wlv}iS%$LgIk%C6=mieJrD*$sY7Y~6*##~fNu8~akE87$V*1D -zxWy!Uy78%U>ovg9qz8$S17(+h*qGEIsKUY4gSVo(z>3ZY~DuC9M-EJJsdqyl<1;xJ6)EPzrnN&Z&=B -oMYy@6uwC31H0cP;cO_GzPnCRV}aNnjF6tw{Ozx80~44I9YM!w1pD^kum3jZ;ijNp?69Ki+?TN{#OcP -YQIMquf^bofMWDr0R>|H=$j)@Q8SR=R2r*^of90cPU(j6PN%?j%s|ntX=#d)DTBM1>)B!uH2@kOHYJt -y=9QIPlJ#|S4EqCEpEtXL+w|H)qg6+n;g?rMdhJnTBpZ*PHx~rB(cL~;Rcp70rdf+Jp35-zH*)M^8rn -`|ZG6=oLW{-MSz4xGNDwFDiL7Z}OM@w-o`J?2?4h2qQ&cwnN?-ttMPxSjzTS=3GkEA -g1zAQn@B#^ip=>NxN!eD@&$FJ&DQDL+Q4IDlJ&EPPrn!9!vdXve^pRXJ@&uRfCq!h{=%Qi!BbePwwMz -M^d7lohbo>P0UQzz2IZDL|I-sj?|5wsVkck5P5`6U-}tIrc6AX_B`|5}_CaJqyf5Z9?J07@wil0{?XL -nVd4P(Bf1?%a0FgA=f6!teP$g^=n#9;Hx1E2(qxDsagCwRCYDCB%hHXqv-%if+Ad^3*8w8lcLe>9Wmq -Jm4EIFA#8|kUTj8Pf00n#Ffi5PBoSCc`_!^39!ml^i_az$gtvGKDM`j|1zaFjv@ZAeuoSNuz$LI2wCw -i5KXlATIZ#EkLMY#ayir8qaO47@gHZQXO{z3n`{;nV%hGhuSOJ|mX#9b#$ohC&!I -;odkZTM+Tu@Xzf}JdCbXoA=44vQ^}FEX2`wTGR5Tl4F9- -Z=gx4J(3!_@OF4^sYqJL2XEAmb%RMC9j_%mh)Z=?ziY+5!BAywH8^I_-TenAyGsf=f3k3rvkvGbHv_Y>s;7}IV)f@((r%Pr0Z>B8S}bsaB&d0SY3y$oD -AgnrP)b@2GtJ0D;7O{02r^h3I;awNxL9U^g9vJr5qrc{LuH13Ck8H0T*GVxQKmHmQ|~MTVVQIIG`}aq -0BfT(6>^FI#NBq~iP&FiYqVlu>T?O0_zz$agVbzNDNX1 -4(V!SP@f(4YF6d-y=J5 -1s8+U_PuC`Zlrd}PxgIcY5yZ98$j7K#p=5{fK%Xp6%mqF|+IC4Bgi%urtpzEH0OO!capD3fabC_E)JS --||HeJfBl`3qTfBW&25Ihcq8R$lQ%SqPr?CJnL4{(6lqNZ*@p~n8FUBC+aZLtwb> -{PNaR97zjyq!^*~=fR5_EOg6?Ix8m?CJC`k3*jL$=}MgG#pjQ+uJG9y9Bg6x1Rf?|1oy1pH3 -DFjrwB8UM2afm{iQWLeou)GLHL?%K%qus`Nafn7|lKG0NPVz(8KoT;>R(b9W)*P>DtW1{=3T@Gfe1G5 -{G(7ZdhA=TEY^fqC9Oht;uhxNvfktvvuPFNUNsNVVr4<(PX@G;amx)lI2JC6U$P#m{PPe>ZMc14>rYI -8h5o~s+2{luGS)`mgtO-Ozllubu$1R*#u#*66yY)1`4)vSaU?=BmLrvRSSIjE0JLk{2PtM@4|*;VTCvi>8U?Hg*<)mI#Q*mSYyOrBm&bSon200(FK^h0h --x@cXq1JKKpDK>{Ne-4-rA}mo}5VMv92iv9&yN3JTj;<_$6IHhTr{pX7Qq&Dzn6OaXZf8#a*LVz!K$n -y<|@3RQB}uR>vydrxmIXFtGbc8g7U>-U;@3R&Ht^jUhBk03o{3L{0 -LQb>JjJLiF->kYf+rc<=50-go=oByZoGzBO9V>Pa5f#&4*|GbGb*7`%94;xiy9n?e((K$leux5Z{MRF -7i!Y~jah#kAK;vs!c+R+16}9_xiHs2-*j(`cGqF0P~S1d$?JQ+b_n`zc7kbr$whwXn{ey$EOw|>Do;)yyGjYYpn2tvS -6*YHqEZ^K}Z50>A&39DU~!&5g9>B*xr`ppf*$gYabUf2>ZEi)?6=nSnDzTtxEaeWO%NIj;7Mn -uZa?Y>le_a&?ZpGOYSUG8w>3|2DBNtDTt0j8ERDtt^FM)LB}~?LIKA@>=ki34sdZwm$A0M$TXjWdrQ?bZAOmY -OLTfp$m>10_xy5kBTj*Oa+%}Q3BK`z=&u?k+A1T+SERFfd&CW=uP>C12`k=#)F}HIh7iQ7|*LTFPQVn -EgICNoGs!|o2mdl5s?EN$TF>f1E+~5!_djtl_2T-2(|+~(kKlNM8LOrHP&j0>@LRA#TAl-)A}Zfj)&R -+)X;G~XI&!8YU*h~W?9r=$PLOk9Jo+qBm_VkinMlkaAZgG4I!7!#evY)VlxMh3`?K;W6gP2a-V{?_9- -lG)utqmkPLm2r;0d8$#UL^EIg&TT>~%FVIwR4ie8H7t&xCjzNB_ABnAIvDfpEY>7o#)unAi(b_5`{)6 -f~}%^PtibvCa>{|EhPr#0x|=Nd%=5vI6Hiq01t<|WB9)Ctma@@~mT!+59>$iP1P*agjCX`3)K9c4jW7&>xkbgYDw;qwZ!igwFFzqN`i=8SDd9cNzj~3@Tjv25#lE> -srC=gPW~r3aS|;&h{ixK>fRVJ`dqQ;2TUJj!1?sMaA)ATwaB&TCaZl -lh-8_Ool(IQ-U7(bB(*!CJBXi*f>(X}eg9PYuA=eM4#{O!r -;1!YfT;9UqbR9e_}05O_J=s{|ClFh?t`?_*-61t9!6Xq1d~ly*KaP93Ox4=Ijk31n^|ycRq -YfCn52${SY6N440d4n#GL69qAxIhaGP<}+%-?p<0pQTQhceFYO3 -H%>#}3WaHFkb45!(&S|BVA6>pU)72l!VQLXPYe)*%pt016iD_?YgySX!lijcuAAUad`;#m3CZL*`=fiw>e<%q82nd#QI*R0dp(92f*M@4k)BAhKr8uPL1c -{fNLS`x&P{<00lD=SD*$rjFiG;lEP106kby2$v=Bl#Cq2d8XfQwjWR=|K#njB>TfGfa)Q(mOJ# -AtFTEv_qIfSVZ>!AMV$m2fjQ1U;q-{P9OnPJaARilk|ck@&zwA}xcGFX7hKRw%^_z(${wOjw6poYTt5&;oT?$dFD<8+*otVdUM^?wlp2(mPd2CZMmwfV#qE>4dqSf`YS8sP}U -3DR>D*Uj`Cqc^aGdpeR;8(oSKM(-V^~iM+(yp1i~j=cUkFX^ia6FgNJ>6JYho$ZUyXPAKe{ulb&Xq4o -5t;a4P}xt8Vz650?yN2dsVkxJE9P6lA0mW&JVZO7_}m_8I&wUcFsI?i0H(~(vCf*@`siM*z;i7UU71x -9no(_{Jy!7p~I*RL74dbA|}f!HMxr{^2IN)0^|jmo0!=JYV~G2`saK?v}%X;{%Pl31&*F^cB+=s&M-G ---N0@YC&I2-j@xoF270czkm7-}`%K$>E##`^lTbZxWI2%ybuo(Ky}ud1vdtl8cwoINrLzpUH)K_yX+q -Eq9a6|LK6mbd?nw^aq2Dpbod@kJ0(`JpS^0`ek>o6Y)$U59~zp3nfoLR|q*zeX9FDXHby -GeECc(RO9TnMw+}-9S91dvI^93d*Rs!loATP8zu -pzk3-dzVnK6l$d-jWAkyZ&hJ*Y%-kou@L?q0v{K}v+DTo*sp_&Z72slonD>R^-g{qkukF!j^9B0iWoo -7ysIy}D&hk@A_w?FtPsXvtR&OdLu?vxiD$Z^TZ6vfcV{&lLnsWub6dnS+Q%{s{1S5LtR4Au8f2Rbs=% -mp2pvNpoHH{p|NGz3zSg@ssTrUIH*{Tu%0W0jzYo+^b<1Ztj8i0B -eSr5D;~&nO0ahIRDIDml8XY?Xc9Dr0qq#Ld-vk)kkhhP)EQ2XIWeo;2AUye^n9Z7};-Uhcm*3*EEWVE -6#W7pC%@#U6*cmTM?cyoGSuo#)7S&<@lu~*G;Z;=y(W`i8z`fq(A=o9|%E5t7H&Xs(Q+c5*<0hpAgUo -w9j(!JCcu*8uA{jO|zb%`V_Ys|k&ztj*V&E9G-_<9z8R_PeUKW`IXNa!=c6gz?S20T;-XF2Z8rphH@4 -z&oEJW5)*d4Y~rNz_kHwebD*zQwsM4|4tRtU9_7a?aU5ioRBN<5Mqz2OUYJL}g`*OFSB>mGFQlzzg7l8!n#6hUGRMwZi=nP3w -7nIj2TmKWWuYKZMr@Sc;@oLU}T$<4@1~Mb1+#`67nJ!*LQG!=;(ms@6Xw)i;1j|K>y0F<;MC}=d@us{ -DHJH)WzS28@F&zuMbGHNuRcCCYnF%a6b -PQL~z4bs>V9EqcJ1Uq<*P)H-^_6f8^Zgbi4cA0jUemlSE5Vv!C_)-!8#Nta|^rp7vcU -@%XYy@}f}lItjmj%$Y2A3>t56Zqc(e|5{AWLZwDgXDVN2qM8a4};8W!~@@;mvx9{)o -fe6#BDNJ=#0Ji6k^L$(AU=3KOx|KzG8BH4PyuSeWyxE5TM4_12Kqkb&Bx?(>?rDAL)FQ4Zt`?4ZaHak -Y%<0)PiTkWLLY|I@W*zcMU-$NcOK&}IR&=C;@;iG($oBO|#zpL*$H9z<=yWA6lFvZ-Kxe#=6clpTs+a -i<(^9W*#h|2vQ2r1ED|O~R3Sz?HS%Q`#wCGao@ -aQLC^mhM{%b5YJ?{>zIbv~Ef&rF!5AtLzE{k&#MU4W|2&CnZlbmrga9LfMUUw6Dx-s78Q-BkJ`=5>At -1Ptvoz7zpLqu?G`(UW1)1FEXUm#`lR{j1tp7&`;78IDPw)`qS>WA>HFhkd?QF;#?@)dx^ZYGJu~#mNG -N>!bkdd}*jC7Fg9DkWz-eo13b%Us{Uo85!WGh-)YJEA5y(RTAuDYX{eR9MX;DhHA8JV;=gw@`ZBZM+? -%l+QW}=zL>eY7m^tjZ$s)vv`-6IBmYWu64t9qrI|MTf%C?l~^6zQ38$z2B-1XTUBQm@7o-v1S@qS6X4KYzLGpX -pM;N;wftJ5t`Q+aDv+hxVsFqVVV1Z8bb*qXba|w(=_*cH~8BPCkw)PPLGgVVV2U@*1;()`x+*BPA8Io%21wF)SRPn2kK&(?%aH&$VLUgw&nz_oSruy<;>_ -BTIa-vs#j!Jtx;F;tPlb{Y}>=BC!7xb|B6|qwD?yIv6H+YJghV<&;+PaJ0&m!qh&F$Qnq?Yj(VpCO{a -Y?z4;z84ENylL5kURX=DyT{|IpR$#CZVFB8Vnm6ozbI>aUORd7!7f2)*t2RVSb#ac5f41ze0DYz8JL}>h;>O>X77Ow;nxqhYN6!WuBX1nEM038w}W=Kx1pQ&`E5SWrg<7y_4PLX+CEF`pVIRA>pxRC9DphwB`~}ir<%<{Wd*AsQ9-BOEH0< -nLqZ3GXsb@YuCi${ce63=is~L+h?ZVQK+H$_ELIP%>vVo&t`yzNjt~O%$Wrl$4mjh*p^iDFU%-}`-^q -PF&D8x=GxSb@>4!1Gw0R3f&B={V##jgaGX^E>TRUaH*DGzm3|@|cSC-fT(n^NZbk(k(d%y)bZl9_p!< ->5l)OV^Z9_>YHGr~_am<%vuSNG~xF>;E?!d{K5D=VBRl9YZq0XB-NaFg -!?=!QpOq1Szbow!=d`AV0q5KllR*s2aarC-`EYKN_R=jX{J$@Aqv%&yBS(`bQhSM>VcZ-ftu<;bh@|q -`}@u_L+`l7od;LJk64rg!}*L12Gcw5fuP -G(LAkoe%7U|X7C4e4!gD6bKAMzBZ^jwJng#8AS+b0OsTKkp`?GzHBf_Q_;aM$OR~cWA)>b{r?E$fJL! -h?Gt%yQIIWyaE!}z&PIF#I{u=xe+TN(i#8QWUk3n>w_On1@X*UO(@%kT6rKFm*{DC5605qem>vqyP1)gr^_(c8;_(Pg|YX>Iirfg-Ji*It_N<(!n9pGAA>+B=~8Mw1hdM_B78A -_EvOOtEvnM)nrk-#V=Eri6#`Tc+lN9JHEzBTLu4Is;lXjkv(aF(#t#VLhlbXOOr1Fwv*FG|-*LEUM9S#g_#BavgrIn9+=RwxFb-kq3pJ-9Mvr=Ra%t&M9`2?uWxj4leusbF9vzsPhBkuYy@Z8dSZ9Mww248+@*0tH`4oX1-@lNm}=mvXB&7MK99VqJR&wCnt -{^>M${v4IvyJcPuqzda!$rLZG!Z;LWQu)&)QY0Va%-rE**S_i7&C!uUwU0Jmez{Bkb>!J3+ICIp$A34 -?%3E8z^xrbtb~!yzGBj;{MbaUB{&x5I``zcKumI4TYq~&6b#dU?Ia%f(+CO`Rl -mTOtp@aeFpRC -X>Y5+l82SG>#&+A?27Vt@fo&tLG?8S>`!3@)t$s<~60iweQ(DdD+#`mpovlf;*9e*E=v% -I0B@)Td6p>kc{f}tJ$LYE?izlBdnPae0khf9p2^IV(?&L=mCdI;Dmmeg*rp?4~I4Q@Mu?Pu1+j2N#RW -zkm-gu$mnt6f#E6GN?*X|kssHAVJz<05z5zjoQ26(i+(S4jsg*DOjjv&+J38>EDXZud -|BEh}kVMtRL*d&_dq%`lIp39ZB -$Jqsru78K%wE>K+pQGAOFPVS(tAYQNZ`MFhGG6%ULJe~;WoSh<1YTQ1h4)6J|#puqhiD5Uq35WOHKI(?WRh{lcT(hKTuZJZ_ -GMN%iUXjc#W-i67S9y1@`Ga~GnvC655#x01*%2s%x#&Qp3S5%X%_&mpb-aIUX;o~nLfJIh -hgK(AL06XSLK;JlGKq#S`BXFI$~>g*%!OE*Qlw|{a5O2q&BaB#B!jZ> -rlMjbDl!=uCf6O5oSR|qn8Y!Z3!bmgh29aj{E)nFDJtao$_qsEo`IZBR24=V`BnB;#3GCh)TZyC~#$tUzWBMtkw^h^1b#f} -QoGbw#0uJqEc)dAhP&CF9$%V~B3tp$?{joFBILjgjiu -Q>%Z(o!Zfu^jGM86_Cq}9jPa#tw(0`htwzN7Ym|j9Iu>D6Z@uORF_@Qhgm~e#8fOnaK)1X`TyTl%#cG -MV<`xrb{i_mnm(LqI{8V>{RmR1SwkPkU;a!dKxF*SNRX%_^II_Si!rwoA;l2e{V-f@l5{Gpl&DKt*y(uc(7E$t7DkAVrn+isS-ty+GaAt~yyW@h!4^A3N%0%54bEY=Z#p1^+iS@k|K|gV3t(%1m%+y3!aDF -&>M*`DINu^OO)NBR+OpO5e_U!#T8EL}#pTu#FhsU1-jH2aZJs>zb -VIuQ?L6N~wzyZ+-;){|lb7H6p(~KfSk!wzg5t_ItEcx6SWh2$85gQadz(YXwZ9_-m%NrPI%9=rTIo6V -9_!i2t%SB#HiG?$ig(QtJ9f^P?F~MP`7W0-kU29NVWRHMWa-lV?d#zn-#YM_2pM%WIa@rUf?Fc22#4a -G4Kh>$8!oW=y^$MO2yQONk<7MF{|NHxQTPMeRA|Di+w&X8BC6>aYf{`0Bwfp*loFZDGQ4qZz2LB=Y)) -CEcFD%zaaCVI_00a3|gvs`-@90ZIrlyS!8p)ktmZiY%$Zhm$hi|CXuT;V3u3L%VxaZ`>u^*6yb4~g&9 -TVzb#2x|!pdpI>8iAtn8CiT}x?HTT{V~G)lZms8K!21-Opvo05W~08!>p+16C{gz)_-B2U{$5dZl01WavNotP{ -B+Kqd7tnelwNU|P*=YcQP2bVloIFy51xPh&vk625oCNYjB`_ -|a$_-8S_8sT*qzbm|g`3*n3=-!iDguNd>jqtP9wBmxkYgJE4r9MV=Q=d)U(|)17sB`ba<)1n5jZ4x=H -^p2`rLJ3>d_fIfYsE7@Yj{gxQq4rj?Yu}IIlkIv$W3$#sH?AZaV(MO4{rmL3?rBEwoKaf!|3G*x@b~d -pUYj3C@Zr53cBSv%uh|wo*yaLo{hhDQXmf+;$)3kA*%VSsOc=<(uAgwfohRo8k#c8AQX+RgoRK%i?GH -&jx;p5hSk=+)9=mTj##;P)RlHBOE$&V^Zjj;=1$~;Mbji~H>3$5gZJa$ZFP?h5%QME{I=69Em%bs#po -IqOf!43zjt)<4ew&cDlIrP!#e*}$)_V6w#U(3bq;|X&@W-|#YJl7P|8s%Gd$YJZr)Ifm6K-+KJ9LQ1B -1K-?3J}d*QY_3zpXG|sfeG~An>sTSq11o{#lW3_X;ab1FED7IoQb;jGoqlyy~ilN?o?%)9=yu=p=eVo -t2g%)nqu$ihPE7+`?UQB4A*`Q1*oq?zMPhdMy(NcWHBJobkNbU8mxPbX^J_uaMxLY2e_2a!>(Jy*WOB -g)Dv$CMMd|9UvSY9s$g7C!w8};UL(Q48>FCH$7xL!}IuVA}eo@E|h-Irpya@=b0^J8_a1w(~dDHBA;- -sb3yKY8SW9^=+a@pH-MNTIa;dr&?4H -nXRlE}dvd_3l9$r}Izo=4|d%n37KOzkpQk}hVg71y1u=DlVq@?XWyP!}9)XP}UG{0nAoI_LMk}4T;)7 -?|$Twtg{wvGjHT+piHKn!r4Q*sN;b=!9a$a~8*_LOe9#xL3&_*+d`AXm--lvjOfjA#6@czAD*IUQM%W -6nR;9dj;S-Ak8Jb#KLjt}|G5;LVmH2M>UHhwI5hMF7Pp(Q43hF#_Qv4~b)G%NV3H9JP;zv7!f3f)xZx -$$gbyFGN;RI}lWixmJd@&h!WJTsEahO!$mk%PK!zfn%4C?Scbu^>LV&6J8GCi+s_4SZG2`aC&q7{xKr -L0&fq{k>IV(L8n7;b$?!T`iFCCgG8L%Pa}=VCITwpPFIB(=vel$=tNYxZaR%$s$mcnbSW{Vi|~bAbs_ -nvOjmxW&t!(5>) -5_dU&J6Sv-0HQ3AYEmXY(r>crI=co0HMRBK1VJiOKMbJ1n5yV?=vn0gpXa-JaYl4LFoHr^_qBOw&>e3 -3r4JYJ3rz39#r$r>&2Oik!A&+);+w6USPw8;!`6rx7J)bURez!0qB-47#!8huAFPoeN{h;c2wHeDq -aG-vs4Xfa29S4!DHDXxC$oL!M>R(4C1#-dxC9Jg!!*|TTzPG2R@Rl;hhzPr7Rly9p^RmA!xvvaLRdf7 -xabo|Jyy?u8mSm}DnBo#U>sT%a2G|Zv0$wqRy#U0rzO9^nV8M^8vlym?sF2xn8ORgxOSA6VvQI;aBy{ -MLPpwJ|pw+opFD6cX3$6UB(dM_=Y8*(nec;PI#Lv@hXSs9ULqAYYzd^2CGL%<^iMo&(vwD4gdRWcDE) -tk*c#Vc;m&X(5*sy>IJd(sp^0%SQE(lS&b?XZv#rGO2h{Hk+PQGylHdAf7VsOI4rB039Fkp|eb(1D($ -=UUedQm6tQ>2ykVjMtN`s!?_l84*Z~$m?SEc$eKnKrH7(2mAbRdUx=`TRBE+tdBCqE_bfS=i5<|51GS -a^%Nbzso&VPiVIDvb5$&w8;yjv0gum!T^A)L-I}GP%>6r=O}$nMCjII7GCn4`L4~17+D_759vttl@JB -PAP9@K()xN?N_Xha_@76q{Zke0X+16!O0(4%wHt}2>R4OUfE&rMCQq}Kb1Br1cU -!r(ohoZR7Ox#-<6lrk0e;jJZpE`wvylVD-gRmBx8w@WT$DganjtZ+nYVD>l3@(yDRJJQY0j^p-7gbWGi*^+jn -LFkN`=^b|1Gr>!vn|$6zoR%nJkVv)+>)^OGQq7bCVR=Djblq_f}IXK#~qlZA_`g4u5!HtY|EJ^1yUU0 -iW?wkfWXn4Kr{;@ZnN`zl$*fmeh{>_X3T&e=Eb-kzMEpSZnHi-(P(9{u~RTU1x7^u -k&u1MtsTRf_k%L&OA1c_)R#AI9u~9hf+X}<^B~kV`-MG!+;7_X_{mOn{!5(@A_~t-#VNGgmVzU1a5LT4MtS_{wTlPit%FmXd1 -@0M450C?HEYJ=0wby+)0i5f;BL{FVw~TpbAZLRTSyy>C+(bb2lZu<0jeSX~yR~>HUT|@F1z@ca>Y`v4{^Cf7%6 -eP)}gb?P_Gy^h_IB3AxES0)F -C}Kzzq3D+!mNTG^Q=FMsBpJf{xtxRb(i*YITh0QHt-H+cvUJ4OuxtEEi&?K)+q^Q`ygY7g1QNqXyc!_ -yYHmV5-rn43b)RPYY_`G0^;S{QWttgt{EEFgb}Mk79){zi_U4bu!GBRpSSW=o7lk;#@5-Y=0TIu)-jnBrh{DCrphXtq}&*J%KdP@0SZVcOe -t2?^eW@9 -@4rBbspwwP4UN%!n{C3cA_Jnfr$(T5Fi{ExjBjb(k>2W|AD_;u+XQ_6l^8OXo)x^DXb)lq8$*`3cEZG -@@o(=w&gytoMt20FiDBH0My1^+NFK4rFlEqFJXX7i=coY1G)u-thE(Hd#=Pj)b+ -WS|^pRT+u0f*;u-X+hvowEN35b>6(!-(Pmsy8e>0bt(3Lo!NLA_-dQt`T|{6^L^oMuFTTBH8F)(!cvx -OJ991;avB=s_>oP}`WusUk*;tu}+ohU?qNK;ul?oZ&Ku6)o0tozeyh+4csHK{&9-Uhm_Z-v3?( -6GTUQb6x|G&&VGpI;1?E;^bg8Y$cVOw*L}J8dPwliWW_zkXzQ>^=*Muvi@sU)JX0&338Dos}zx0y5dn -{6R(z)-2gJwUOS}N)+m@nOAt10o~l%w -<~RePQ$U?%_wDlv^ZRfouyT?JMk{!#IX!lTjWLP#P9RK?FnFD8<8*H!7DKN<{0d6f8IR#!EL6NM~+2i -=I`zDq)1V|OvOq^gOrsDj^sXqWKD=fR|0Y=Plc$O8X%(4r3iRKW2?gH99O23$~~6}G5?-z8*x)&L7iH -JoQfz;=YCp{jX_$ZlAS97laMZmWBYU_*ivx&nfd{{Rfdv(Q(1CP%aW6~hxEKUrj63NBEWdFvz$YH+00 -4t6ET?IjDq^Y)8q(`B<&0VDVzmSIEDt`Lrt95!#j$*?Cl5a5l~{G^aV8igC?LAZeVw3sC{ZJ^XZt{lA -9%%gUPM)m7=>Cx;$Bvy`tS*?~vYfiJ7U_EnT_#$k!j9pF6~P3)p&Fb`t@CQE4dd-H;4_0R ->eDAoe^)R(gf*_?^kQJb>g@oxOe~8XCk>5I$O6ry5p4Abinr*D+90syMj@dL(X%T^;v$QJG40*%?TdMb}G^M(lK$ol|6)(xjF3DUnjJt$U7 -om3rBY@tT#^w3qRD^+aEmj748%LA&4~82FZr2;oQ5fW_LK4y~{5na3YHkB}sI5Q7yg+MVkQvrw4=UaM -p5E5&>u=t`s5|tA=KHKx@7Lblqw@XYyZ`)d1S|||Pi|(jKk$>$%^X)$vT5+8rW4 -W)uxE}nn!GjfsZS1jUq+W*=3z3u^19M-KPvm9XeJI`3dhL@4<_@+{#LQbYEx@4t -PFYOZjrgX8H7d28h?<{TpeH97nb_kU2uz#q3?K4@#V&;FiV11-QS1k;BBdnq58>>k7h0Ru9{i1Y#YUF -Ztxfp7a0I8pAA`^@b-Wh5d9=VVS7A_lFsfmT&mV7JXxt}m`rzVdxQrR=)RaKay9+e*q!1-*WSF(p>C; -l-W3KY4%h&Gh2U*XQH_7WSAVp*6!_KK{z+H|%I@;{3W}ATg{LZ|p*PqckafY>IT%f*IsCmWD48aw=BT -zUi`!9D!n9MP}cKd2ZzErqnq%$Sj)%I%N=1O2->8pm9_R>28|nS8hGVZqO>-7^3iI9Fs)~BySOE>J&@ -<-Fl2w6M=uwnHC+~Sdyfbr)%lQRa%Rvuxf{0s%Bx-ywt&(;9luf3!~~-ui`&hjq@s1=Vh}H1WUHD>W5 -0IMXUh-qIr&9@FVQGDQP1!K6yu6M_A)fW~zJgv!=?*u?{R1->-m?=2jX?22^@lIHhSrfSCf`{T2`m%+ -6Zt2B(y9%G|9reK*&^1}KgZVW}&`Jdg*#z6LVXI+p^t3b7)D*aCtHUUYX5M}%k8*cv77A`l+1RFG=)2 -f$@l;qxnLcL0)V;DC%VI_!+N<_+=K!W)wbZs@7fez!{jsL!K<(bPtj>EF&uNl9zA -$PcI^0vuS(4JF&*@L2vSz<$z0wzuJBY%ya6n;{yBCh_GRE8sLblSmxDUce -deBGEUFKGJj{LvTd|21!0AnPB2{4p^a*w&b!pq~6X(+TOrX_U>z-2aHZw2*0aIlb*pNHLFh-hv*)i7ES6(Po_vsIji?L~9%)=RHE=W<3cy$D` -PxER>V^OQ%J!vq!WKrli|)wl3O4+2`Jj+q7Zt~o*r_UI4=UKWIMQ`W|_XT0#_DqHw`uVuzc~6n4zP4(7ZsXIkU#lRd{CUtUgj@OlYt6<>RlQsG~DSsS!Zqk}1v+hz*~5VSP%e9pos;t -O;Yw?O+n*~#54U?{wN*cd-X)3ODpeETs?4}@5G_(#CUs)A7^cYqrm$iu!{`%6%8DzxAEcvDrD>?6UX?^i)#8K;^eZkH -Y662jTf(}~*v(fn8{2nfv2jo%iduStpoae}6`VR%hZu;0IxQ0lrYjR7Fk`FWnDCdZ3Wm3qWM2^#&I>W -s{kxJiF~(Y3X>{!CyPtF0Z&4N%|(T@kg%+Auqs+qvDtB7kFPdcjxj8&D`inrDe>xLTN*=PzZ^ -CWm7qeMu=NddN$&_IQ>|xhO5-JFs@l$ISNN&yv)!-|1C9?zlL@AQYz1ymLUtM*PFj}Yj-fQ$r9@9W7^ -w~zDxy;DQ1L!FzaWjgJogqNxFrFXwawksBJ;P<(A-#}*fFvZ>IheRl4jn*g|LN?WIYQ(P%vKHn4lrCD -F!09!Gq<({s*$Xj!BrK>;rM6HqfrjvzjwuiqNKX(}n1HDLAWkT55X&69DmwE3pXIymGx|d{0pC-JETb -m5N-NO~e^dh>^+akkAfY?zTkL5ts~PvMBt3g2}!Fuv|57l5!8IUDu#6<>^AYztXHUr~c1o!c>>MZBky1ck5N6(qR%H(+!FbVn;P=B -^Ld*`yR37E%%)iQ-H1gN+|@|WS=6_N97^A=u){r-zCI~soR9N}XcgILiX8y*gZ%zk$0zBqg=kIGtt(x -}c*;)Br;FO+9QIu2*%t%?MCg0_%Mq}Ha#4v(H6G5d>w+dnG7U*iI;aaza#L9snReo|;{a7fI7lFQ5m>#!ro=+Xfg7Kv -#kRC8J?SNv>4lld2UEs@)U=e&L5m2aqbU$)EWHmLM5DZ8LFvX>xTxfwZCk=5$3sA}kfDH#WjZ9JccZ> -xtIHqTs`V1*ioOIX^AO1B+YX`!0!{VW(MmeO5*(DcjglTX2Xy3h$B1~OAPibcvM8UK2K;`wYprxCi;I -=zt~9gliu&csp@vJ4!;$mx)`{3bn8ATs3bQV*VKW5*?CXBT0bu`<7TSXrpX~umeCXqY*lE(e!Gb2cDTdN9XccKCdNkp*~n;T_&&gT6Zu4W7Rck%s}+KY!6O -upAEaJ;mee?6o$y+Z_EZ@L@b+Lm1R^54-d*8+2im7jFNPOeU!f;(nUE7E^RC-Pdwjtzg%Egt;EbvmLv??bnYk184iZnfG+|)EB!VPRheUc7nH+_JA1P9xfz3_4 -izMUVfwjB-~tO*z8lXira$f@aP${pS#aK--Rt=D%MCc-s0upRc#|7CtJi?r>CbDE4+ocnuJ>!UfO8jz -P72uE&RT8Gh_gr!LYQ(VLp)rhw3Sn32Q`mkuXy&;8C~?+vm^|3DC~Y{o^sA^*~FatPtBjz$BzSy4?hZ -lTh~o<{KuAkdzr$_1)uM_GtIqQh#ky{X52d*g(KQVl1bWa(4_u!dgsCD2j4OA(Vo)QK-|cXisz5GY7z -nN>*l(PfV=PBOb`{d|#yUsmv`bm7N%fh9hs_W`n-mw!q4=4EqfFTEiFa;bCv^d^j9d4Y5qL#PWLMOts -$HxXF;&ZSS_%U`6cJD(z^_n4SPqg@QihzO2yq8r9G_XhR2@o2bJ+#&k`#K`1jD>T-j98a(TRlsnof<-`uZ1x#-NjeBfxNmLhYA!cj-6nP!OMgrdjxYRMo?za$qh -)~K5L@}qKKhRtB{3=7S*#Ia75Y{2T7Icx+^V=!%X*ghZ-~D{I+y@E(%v%Zfwr2<=9|Y_0Rc&MW(bJ0$ -DClywm)`8Di<4nyZmXr2eNf`QW+OE?*-$h)&3R={-Pcmj@;0X$jQ4#yhY}yxDLoJ{m3*F)*JB4E`XjS -d!L_doInb|DrW~HVd4JIee&T(S$-ym5@fPGDh(QZYO!uAB(NYyDu2JTRVT2tst9+SJWy#er%6j-lQm? -0qz^d0<;8utCFV#D|kRK)9-17Ao6;6m~_Qv%VEXOYaQJRq5nxb0I^rwi8P7)cfE4`vxo;(Iu>ZG8nl4 -+OKE=proSnG{&FqSZ%IsUYm}a&TuIcXE!?0+!b|Lttb|IMPc~f6^EN#&8MqB&Vd$SV5)_eW -4jC&upajQY)`{mdgD%=63)|fuEmz3ks<1vMQM-m#+S2JDot=_fXyUdoRkXr -10FEAgk@TjY`*0w|D%kv>1e*uFegV)Kc>==TTiZRVM}NkhQMdF$Osm@-y#(<(l)jFxh(B=X1)0D$%LC -D}}23lpe-A+RpFrXP^ByeWSyw?v>-$C&V!(iN9O~)9Le*hxApC`6^i85kd{nY(oKs9_o3zd?HUqDVIO -Tj(NCTMf4?*+9kurmp;mg3M!2&^Fe6e^7@}R3rZ`MO|x#M0qN=%n(i0bY7P;^!K`yT( -5$?fsU6Qh9%6bU2x)XvS4ShePNt-wFwUCQzQpkGrTN54!gzZ%Rf&HBw}BwhE&F@u_%B4stl@B1os(Qu -hq&o>+#0w_E1mbhS}RbyWKV=C4ruwXO17uHEP#br{gsARZm#yc^iwPkq+rR{tL6Ieo43&aYy^w7T+s6 -@J7+9OUrqxOQ<^`*IZC6f}xuZ(3SiNLhIK= ->gk;2Y$SFMoy}hc|4N5X@$*70^Uy!O^L;Kqg{Aq(ctC;2fgXPOg3Uu71$k>{eS0+MX9cU}azh`bVD{t -OksmoI5tbaafe7`#UJA2I#yqxrZ3~j5AYFS)BKVcVa=rTe^5&U1QY-O00;mj6qrseh+@Ljt -^fc4Tmb+Z0001RX>c!Jc4cm4Z*nhWX>)XJX<{#TGcqn^cxCLpeSA|@wlIA1)t;8L36Mg80u_n_78MO3 -H55{5N>z%1KthEUe1uLW;tZB^07nX~r(t?}jN*v*j&q%RWzhTL=*&1XD)=#&G}yF)S`e`gsMYD!T~E~ -_wGFiNJZtT9k`~nG-sg|^_x|(pquF0;f3LmvT5GSpPuZP&83V&GM)*?{hB?fr|D4Q!|1S=InNwfQWL` -{r`>MmbWp7`#%Kx*ixm&7#@zd(N?#;dXuAl$>7hLZD+LT+(|2+3+KhG_`!;^dOFE(zvE+Zp7Pb=fT@s ->^Vw%s*9@%Nt#F3g_`?sf88^KH~s=HE=+WAm4QyD;dr)52cy@G(xo%Zs}T5iHE!Z{@MDF)Hf>bUuV)w&qoe5wmw7=)2eJPZ9V9#JC?LWn-K0bR -Z9dO13u%!$=4tm2H1H`MV@_k3y(btZ=l?JVZC_t8SJ2K?x*vzXj@z_@1NR??wFY5djhDmZGR%hSsyE) -n-Ni7o^R>RuhQA}1!$G~y>r^nNla91bE%VFo!G*7@-dcS(_-S9E&&*12FaAJz)tff|0stS*RL6jpw@h -~>+$}%e!~g%^|INSEQn%MX`%5RIWRK5o8$0i<}9tYSzypeAq={f9>}On~uQVY ->axFe}dn7^O{414t`C+QXFpisjy{M?Ys>Q;DjC^$HvD1HTqj~@}xO?DMCj -2i1En-oP6j%EjRN*067DjT%FO?0pxTCrfMjP==-P|2X%a>NwQEMnX%$aFvy;iwrJIO!FPlF}S3ub1MX -Qk~>t^CO|+IfL@MQfxhKn$SJl=idlz54i^Iir+s8$`ES?#hM=Chg$5dj+kX%#J?FJR*)N@F#)|#;Rnf -s*+3@2mBus76QYZl28XbrdE)AB(G_>ZY7Amc+~Wn7{y9G#7_H|>xglarJj -JPJXCY){4-Jwn;$c{;!j9DP-Yj<5nOdvqnG>=t}jvBe~qWLd9k8iVx$%ILPc*u8> -Jq5*wJr!JnV0PK8bv{o7{=fuI_wSZ@>gKmILq;Y8$X>@(Ro{v_?2OJD=~&=R5Mx->E>ckiTEGITv49wm!j;JP`TWP}T=u$QKHcQ}c(v9KnK -HvMY)wMH(gD^KHjWkIT0Y9!s|*PpNfCD6vph|qAsbl05l*gmo6Qrn%)_&>psgQR?ktfKChj@abf|v?2 -6AP7U$(CSqrd|_E2%2)g9OZ&JK4V&7)+^QAfrs?98)n;Z0Detha~#opfgI9#oXNhw>V5_0Pf4r8zk@r -x|kkjWWVH04`8m>4n6(J~w%+j1Hs7<;|4lEd;PYpK{s_i5p2TAX3bjmq)W1AsdwWoto$;AE=2=n)t;4 -P3%<@H7dn-vLcw^g!gedFKSwPWh!G9W#GXGziz%WFfoQ8reU8I~tblQs3YVR5GOry4AP-Ek@&V2-whfj)9#nz(_aH-tRKW_(tTfQ|Hcn{;8!-FZuuL*L9Z63x45Y{7?~ -#%AymV1fLJwqH4y6J)#0th@*)FYYE-|+r|J-hD`ksrp?94(;4>=G#6T!zNn({@Ju$Ynhxe1m-DVT7{q -9s6s^i>5Z{1;k6r`u^APKW@OXsGVM)iB61Xz1sSTaW0K1(&H?IPxwci -^dh!e-w;m+PSw@)4Dq+Di&KSs*O@V1b*jT>8O^z566BP?~Kp#6`naFveEqZx0R`+4%4^$_FJ^G&+KcI2YLTO*`WsmhIGApltp>A|dqTn9OISq;li%<41z3OAC -0tW;DRHug?Pnb4G0#q#=hnD7P4zrxDlZScgl<*A@6bu&Olw90H7L$Z;lH3WIo*E{3V}Iy6CJHPpkJ@f -*KZOT(hdDMevT+lXqKz|?TIY!^kWtk-v0yep7y5t`C5rdsLlHr2iq3bt)?Nm`w=vQMA(7zx -NdoRRFGzYAsP^lg^Btxvq@gCooA2|`1dL&xG_Y%b5*+I-mekf6Dkto81+>nqB3Bfav&~zehXfcE^pD) -DSza^$({+wW -3Z4jgNQUIFffeV3an98frHN_T(Y>pP{`He_G4qnzfPh1q`V*YKC9P5_Kd@@RdjX6T*x3PB#mxfoumo! -N0AMRXTc;#C57&xJPV|8EEx}pfpL}M9P-*E0B@G_{Iv}3q3?uX-m3)Cv3~Z>h7}y) -Dq}67bt#Z7@jS<4IR@tgxE8kO9VC8_?Xkh-=Paw0qLER#={Le$st#Y_XPm!*-X`tB77O1rxBh{TCz>; -mb*mCgfo2qufOKzTy9FUg`OatbGa1-#igZhur-*7JeKAVf9l3h96Yv_dqsEJj|47PH5<%G~|bnxct?n -IY_!n5;>7>1kRN0-#jf(mh6(Dqs|9w0 -dgN0Mn8DlVvi_b0SEw;Fmf%x3lk-kNInun1xFMgG+Qo{t;2F-Z%xi;ah=_|dr^Jf0Vokg7jS>8gf$H1 -IM^`aSK`QqZ1QsbbVK(v>M2V@&dun+Yl_;X1;w*0}aJ6%pSWZQo=xV2>N)>JLb1_%v4j6;Fn$CJ~wvG -mblo)LwT%AVw^O&onfdX_qqycmc19VUT6`f%wp<7mlpB1CMM)?$m<3U)2h%Dsq$o)b($3pC8jE#~Qpo -KcEFsLX3kUvw|!CaAyr2@heiCE+&0}K(>Fa{xl{89_jJY;AVl_tninB{W6g3J8R(7@v=`TU8_oG^ -O%V+K3RU1!OixPKV&1$s=O)40uU+}Qxb@`0_W6=yfmylEdQ%n2#xYpAD>;|8sx^3pFtKxMzR4*DjO78 -$`mKoqE|ZCTw=z`?pEikgq2D+iI@-%=XO>I=UNW+z|Px)%o#+F!srwl9R -~hJDYJ6LRlx5pu5$EnQF#HL%CQDY;bHBFF~$xpd5F!kjIZL-4;jKT=yW$W(HvQ -dwNMYs;neYg-dUA{VBX@$&b>u7^EEnNcOF6E9`U+qwHqnRpLQ~zdB&9EHLFkB$weK|2~=y4Da%pO47mC~v_)_$gOU7oqW2^FB+xW;s$Nq6Hhu38{E?nX71|56~v>M=fPAo5Sv@CiCr!zc2j^} -suFnTZ@u$iaWRHj1$SC8gVsy`c;Z?J`^t&MEeHg(hR&r;JWl$8|`z4FdG^8)a@nq{|9kGkEoK%lgnQ% -Gzk?mZA0ddx`!I!f#?NL3_stgsoff(MQlEz?jJ>WF^+>ijizC@PTK1j`Ohdx7rfBDXJX0X6q=W@RC+L -vR4ilTb-&h)$arEEHRSP28`A$sTo*0ZW8RB!UaTt3y1)b+$s^U1Gv3x{=e+uuFqg(Jf48!eFG^U2!|z -5rqlwPn^+ci90R;tX2W5>2IscOhP!mvpTJ*G#%>)=D<%2Zx@lb%fRK)3yOsnx>?haspA+Kx>YGBvX>1 -`1<=e-SLM-^GBg*B(nB%a7<T}!ODZa{S_l84bKZkDmQj*&t6Ib_2%T -3()9q^$|kt}Tw&l#vua*4qrF-W{6)>67?+qWMkoC`!B>?c4QI)ZrT3z7_^O?j -;+b>~c4`+mTTJJs`vtZllf>&u^dQ8kFv5wlv&OuYf$GT;#g*1hAkgZC7W#opKVh4Y?)@;m!9OYli30 -aX1tT)p)El{4&%vBRe_7;fw69l9>!IT!L6_rxnKuAngNX6A!Cs|8CimWF63g}sG8lPaubHDQ2l{_W#; -J-4d+)#E-R|iNRc(!FVrev1;G27sUoC|1st9%u@sZDhXG%sfL#wWxKWPw;huAy<f1JWb|5fO+O4bc4iRYCzHmJME0aXP?13c-JBVWdMjG`LTGJWu!m8@p1Y-yyT%-UQCz!l=!FpqjL0oSP=saF>zq$m0 -WILjkhX!!ge6SI*kcnsWmm0fffKN)5?qH?%{13&7-o7RS^yPHqqDitn{?rWMsz09^`8=hs_(o$b@JS~ -eZCkBUPwma=yl%-Yj?>lOK_0|*ev%aJLYBP#qRBGu>nmzwcdNp -@lmN)xs()E4ahCKxVx!C^dA1DxA#mF_V2MOcpQkKiW28KkAzv|_B)$YGJNCBo8{T)hU@U?nm_u;O}f# -ZAdXyn#|cpoN*uB1bcZ`JW6rG4f1Y04b@O;DUi`klz1*u$q;r39GlKUM8$cCFX+yZ>1%wg=4ut6*|5F -o?j=HnE6aEc^}rM&Se%hS($P#ses->w(|P${jAV22r%-vgF0@^L6*yW7Q4x(#!9kr2-IcjB@H)Gy5@G -EN)YvCamDg@BvhmTX9eB0_CR;9&$yzy@0*j|-CVK)%XHC0Gx#9FI<5wRFmpJYF|>bC$Y3V*FKDOAKofYmx&!RvYO5gj`V{{v$t -2r#-mQav9$Fgm)DfWE=T9Jvq&ff_GF*F77Ka3)})P=`Fx%iIYd1sB%~`4-eMfSWM*$y42oPex{;IBK3F?jmVfBDKNy3aLaLkxdbILv{Yx$QlBYrzYILA_x_EbAnv -CQ`Iz13AQlJs~ce7GWi~(1@;ah(&|s%efTO*}x=H*@J4MEDq02kD^B9&SF>(He3&nFVUb_vH+n7ECw| -qjr;^70y;AE6|%U^9I*N}3M0XE_PLJgxD?oATVz8Zy?lk+>&L#3PFYbzS4RQg$)~u3MsS{d#Iyh$$;BL)O*KO2B|rx@XBLj^>(Fsrdk=1dML-HI`6*;OVRBIr@} -<(JRiwH1hss38uirHI=|SzJle-ru`T#n)M)Tp~QG$ufB-c`DI|J5+HH`!Y4IF;wH%$#En?GD10$B@f -Sz4dMCdna=|1-tSd+z(5AZOW~LT;Ag2q>xv^+Uy45VhI -cU#;JD6HXE6lFjU{-c3N`3@7N(;I%on}|_?i-P}jKv}^*lNaVd1xaIKLQS7)c(1XKtw+2LCzDHE^&3Q -xH>Mb?vV|6h#@S>Q6&-=>FZWqPa@2)l#3MLuNKC#66Zr9z%u0Zh_=l>idv=AfvdIrQ8yMWOLUFB%QlTAVy -1YzYaN~xFv|XtC%Z}Xf7PHc16J*^-0(3$iYROmf#<0ypw9MVy+$p>w1DF>Mg;&baxpP{y7AEhj2_kDh -B=Us-+6dIKy+va&zUM3uF~{r$90}S1F7GG%8CVDVE>_0O*f0VOSxqQU8#aqO`_4>NX83Zy?#|#r*OOG -cK?JR4vcwx?_k3vgI%pBHKBmy;ataYEQE7mf%?`bM-KmdQ=k!a@VW#Qm}}N2Mk{SSyXK=xJ}#t -EkQiH7MfFrfnWv;c|MuNlt}kAk}X?iOK(-Or9h -(XJWFB8G$c$1uGjv?lL5i#E6KIwbOw~+ebiVhiHD9Gm4qAmv+)Y4~h*!iMR$t -59f-99E%0!ykdg{sXg_4^XJl({c>$xZaso`xmk3w=&tCd_Uxd6-6*$d95Dy;HtBuz(z+oLUB@R>r?Lq -TF)-Qx>f#$f_>B-%!Dj#_?mJ0sfKCxIRa? -!5zeL5YM^kL2v$jX6I7+Y=WtGm_+1({Rd`Gl40b?J44uW_$J_Ducc<|8r8fNi`*A!oedzHxbwb_L`CSWjf=8+IOqe=x -!89|`tc!d=%GDl>{9JZX*5`f$e+g9OuV9TY>I=mAU#VY{R6r=3|T^KfXDtc~hnnV~}IxKbrSPao`IH~u3rR -n3r`8GEb4_%?2A+}I-F!nM`sLZx2`7e;T-3}mc<2{XREPmeQaDn^5=%yLHy2;C7O%vMB6W*TL>QVz!r -unR+6lnk(1)Sw!ror!qKXA0HJtq@uAAZ6Zr$m`&Wb6~gfyKizb8lOxvxb>l1Va>0Vmh -G{HiuYJWR}Tsu@n9=wlRh9=6;!GVhIcr3FO^(zsU2$N->I!p7%oprxx5L_e5y9VOPg>rc^t#8UbS~oQ -~=V0U>MfAg1fig+riHg3ZJ?fd5T;=y5yfIZyrT`pha2@jMPdc3*&{OETApKkbOGJ6%?#7b7TDKpu8uw -kbhC5rLs8E!Yf*3xuR8oZ+IXhm9o&mYak~Y5Ge&7v!(tH@My&zBSnzZH33l(&30DhSz)Se(z3kl)hpd -AR`|CRByk{NUsasPZWL!|STRRh!vged`vhr=Sr{7P98ktw;o=ZybbPz*y3pKhp>=<=N~55~!UINzKkn -CIFP5<6GhnY?!8yi2F9hTlHa>y^4{CK`Fr!ecvA@U&1dBP0aGV|LW1&O#7IGEzZnfmkR<@XxEmqW7FV -ghI6b8B>>JQ_oM07P2!%DN1`*M`~vX%R0k}M6d$;hns`aIH?Xxm{wY45Wu%89j9Nd8hfCq;l*E-pq+2 -2xugT~R*ALmICyz^wF-yBRHxjy#Mf!E4bBZF3&Gas}E~k(abX$tIQoa^bW0z2OE*AT0dOXhflHa>)1{ -13qy9-3*jDLM%J63vlKVVJL6sd-kw%ePvY0qkw3La86P@(&VQB$a@6ieA1$z!i^Pn8FgHSPkGNr<^z2 -at_C9A%+?L%nt-Q0OCi^z^v|UsByVsl?+vGaUO9YfqRf30ffKACNVYHFj(TxGQS8c6G?h40Fd5I7_-y -jBnVv{7qY@(Lz$du#0b3lJOB-A;g)>Sq`QAV_X}yW+DjWm6g=~bv630ujJCIJ+FzOCnFon;jW8q7T13 -%tJHhqS|-1W&!VnwGsN?atT`p9jVbfxa)F@TIhsk4+MtByyjA+=}1BG_HsWI85Nfpj$a{8N-W=O>wS; -C0%t(easxH(^z?A -l?%TO6DH_n1=SR3{@W;X*`1vKIUyr6W00>*04b`q_`+fwQxF3Jy)ynpD(t8AtnaV?3SSdwbAw>mZ;cf -)ZmbpElfRUdh-LXL|J0&jN0P*}H>5h$JSzB!B#)ptzK+J3@<1h>-2 -=G$I>)qgF_G -Ry~?~i6Xts%VMaSinb(pmN$N=gyokZ(%IJPv|0oQPb1U=Ol8{Q9kbx=6{yZ%`6Q=Dj3jG-RU)i>qe1qD)k<&-^L35%|EwlrQ&qK3RAY)|GfJzO}rV-6Il -zG@gDNc@J);2m^%l$T38bIzh*y#a8d9aVW*{eb-^Nx=gAGEFvfVdMMqE@e2M{#VH8x=7{PV{P7_AxFan0_or~IRNW1)aW(r+sQ3=sDIDpFlXni39GF*mGc -$|7rRg||MR~9W|Kdy$D2{S&l2&fIkjsxlv!UcIiTRE2c2({tDJZ|MQ3Xv}$mR@X!e$T_~(!3|BWtR%0 -HCDwot+DFT)EZ0cAkPDm!&Kfa6(T7U{-Z@Epo^v@qe?W1065{gg!7FhY?W9x -52e|i#E7xmBlpe2Yi{?uhlBs57XqYyroR)eV=PeIXYnq|_fzVQ;5I0uy9`Y)zW~m**<&O5PrH2_1L%^ -e#qK2p=T5WOTErI!SG@Igap(R#`T|{GZwOH=@h@y9g<{s8ISXw#HN`-XzK0teGqgVOh0BwwmMFVg3R3 -9WYV<4A2bUl`XL~uAi1%?G_3S_SVHtTA$Z`zA~xr)Aac|RG~m4 -iWV9obqlB({~P;BO_5)kZQ0i(HB}^Jv7|hPj7jN2LyP55k8$NUlRHxq}gl9TVR}(tymlda$7Z~!_p2M|?vgCf`?%hH+J1juPL4Zt3iy~#O(5I^Fuc53u3QDV>UDw -j%2sOn`Q_$MyX33csAyfO?gbrOoFF!)&RE09C-nXBu!$XcR?fD2+&K~}O4A^!|U>)W+JN^OWGTkY}$5 -nq#ht&9H=RcgodJ`mnml+oEtj|l(L_><(&yMn{5fY~5N#mQywMH|*hiksOse?sdZHhV-jAf)iWsk_>cE(BMVg8IYXf>J`A_wCpK&rC -FWH#L$C(qVho5|-z&7QX1_KMNTDa{RFDZdQ$mrUKgsLU~oq*xQ1%D?LkpH2_P4*UQIy(OE{<9Ey1e@j -c5kunCk$*;c3A+q&-z1K%`A7ZMTpi=z3mJT5vuc7OS9?>4{5U_3IGHhox9!k3iEi}3cKJ~si`-Wmo}%HslgA?mP#}^b$#oZO1&XQ~XE|Kvtrk-K^{~WK5w-le% -YNCgQnal6r$B0!{vC%7vHJE%1LS-!lfjIrwpmg3YDeAbQl4m%e7C#3~Bz>CEr(j9h`7rr?qbDV=E$ekQIXI -t>vPXN_X;urFkt|k|IQnjvC*b$usZMH`$KysBkD0Uj)l18e1C!%)ph5XE?XMDP>@_iecLvpl64BYOs( -Q@dmN-)YpAEU1f7mpb#G(vYXEHyVnl9zn41&&-8T?ndAY^%vcb`^eO!LT=??A7!1O@wN-{Z{;#A`joe -PBxHj6+)y`yx~XY!_af94dmtWSfsODblYNlML;i!}aJahbKtQYKp)A2MOF^60*Zp3B0hT#SN(8Xii)0h3c3o}?b^>l^Y)br@s8PAKL-kSMjzqJHUd{0-G- -VzQwvbZ4$GFkyRo?Xt|t*3gPvIYYQOVY?ZFxuF#u4OZFE47lugx4Jo@x$Jv~;=bAc6mD-n>4q(A(|1U -k_ET%&rCYek!tTusH(uKAr(9|-Id+nAsbq0C%G4R;Pdy3#aJygRJ00_=$?<&H5+-^_u!=(}tLP=qV*d -RIBrf$!6dJO5FK6`W+4jQ{-w73P-z0^0q1)<#nL(q$vrvpU*$C@F7#iZI#7yW#R#VA33k5Bx801`<^` -N{^7`nud841!3Ao3YuXpkR+dN8MPI8i=|c~d1 -oi?>>I5CLhYJqZrlCRqqgb=6QwYx~>;Z4}7j+7`gfyH3$U3Eq`u+5DD+aPv2OT*}y%*2FTm*2BE((mg -35h-~w9t|qubVhQ$g!Wi1vTpr7{#sTg4M*b2iHt(&DT)md#EbqziG^@Z?)7n6SX;+rGq0uFd+|MGW5t6Q{ -U3gk~1Rc_Wli-6h4KZLuY2n8lEvp{ST5E-?dB7ZU5CpgXIi8>+-PLPRfw^|$for1O-=?0OMxL6;dO8= -a2GwmvL=m%>?F%NZ{;sYON`Wz6iO96tZWdsvKq}&N!0^Ls^tHtU>?r1CAbO|aV<)66 -Zq;FT?jUR7#G5nE=VowP{^8ZadyR;vjmRQ)dZ&gpyVU$^R;`1eX{O*l^=n!XC(`uK7UDM7(kWJgN{k)mW>fWlOA8qS?hlv{NQDr4W&zCZlm#c1} -aXmQ+mWQWJ1R)BwTCMher=u!F_4&lS3Alg%DWS97xAE$?8%Cu5vP`5>{P^|-X=Kx3bgaXHH~KpNq5>j -u?hP$J1jVN(Z#RurcpE-DnZqoou-CUA*(apR4dFhWOn2F*ksq!1h_v?l~fPx=)~Rd~GO# -=zl`ffq*x!oz{kdUwplqEfl;fj9?a%%w>NElqe}5Ewle;Y2uURmxZ%3OEJurWdIq>ojF_X>+g{Xz43J -0yeFM3P}GSH47M5ys@7cCyBit?bBq^V9O~VIxn60Gqo14U6Bm#GE_D=(yyR;<^<2c1<0eS;9k2;*rKa(*N-C04o -q7k-o&W5FmD2eY>V(t{xMT}(Zo6Ou&LMAO^p?%qF6=s{?!vEh;x23^GrJNxvw5gn5t=u9Nx7nuo6gJD -;1PhniePTOQ|$#cTNb))XsT?f{}shod|9Hbn`}FV8mMk*xy^pYag5J`j)qF#s1hzr*lw&+FSN+FQ#uOMsm+D!rkc&S=6y7HC#0}e^vYMrLFl``u%yUF{PE*P{cL00P(U2{VhCFT$8Kp;bNA-$Ha@k}H? -D-X)4eJ&umfwX1qOTmjV^m{fpgr3@OSWf%+MbFia1>U`Q3TJRp($@I;LZFP;3NKkbf6DL1yPN+l7eJr -YI{s(;V~_rqaGkT2DV)zEm-35`oDVw@agaLxG5XYE0+=mt6DIPFDHGU!UncLc1zhl4RD_d*j&6wVGw$ -PP-9j2@#OfYYMvRS3G-yjIl=J#lbTZ~kd^%LQ)~+w@)P%`v?If70~&P2rHl(|B^4tqF^4;rMK(YQsyV -3&6Zv&bUjc&Xu2FcKmP}+QNu -mSgPPR$O>KwLjLK9%~wlN^Bp#eLW<^*;Ggth8(7184>Ld){l{1)L;iy<*f-MNyeroV)GzYR -f-`7dPy2u#=6XCvwZMD7|;hgaKunegts0LVXue9bI{3UkPRJ)!Ib|AT<$>~H%_cH9<-`UQd-Um9YbQK -Sb~M2jdY71`~V?V3_gqZNch?0sdgCQaIT|=Gn0MP?{xI=8F;H^@g1r>3&clW4&MF}Gcsd7OMNX>Ewj? -1P^nJLl$K}b$8Qyv8yy3?Cj01xoG;1}M)q=<(2@(a;tIN7Dl-cg6mF7mVTcIJ>gchqocHx*f^t3Tg(qG;N$@v{s*ig7 -^BhmH#bauziwTVTV3e3B&7`5r!Nf4B6zXkCR|~H88a2(P#h=09gNZu)LJT+tOmLPI~JvC1hL-bR$VH0 -L9%SX@nPO#4Si8(kYDqJ~%-pIw|{yJ^uk35=$mh8e*k1q)IH_HS{Ay`2|@+=%SV)mtfEAO0{VwgG{yHd#AUmNMv{@Bmg^_vfsZH|nF3^FGQQr -BsS=WnA0-J%>PJ@)6761l7FnEZH2Lf-2nQD_;+hW%miliPTtt@O7wGD_Q7W3!3mZZUUL{ahNE+`99KhX`Fd?BG&+rW4=#L=PemKhT;N1@(Yp$;uR=F2zrtXJzGW)Qjh2^nMPjCGL(Q?!V -HLvGG+2LCMzk4E-f!>Dkyg`*4-BmlUESj9PFVJn$W<^(LM}s6&L)WtV0aA(*B*SaVkhgER|}1;o=W=9 -;IxpB&LjXy?HP4FEH~j30?;)d-d|CBC#6O{Pib5>$JiIq@iy|Rn)#_@X4zkddX?SVmx~p>j`u9}DNsm -X1f-Ws(7%ETGdHiXcut-^+!k+e4OxzaO9RGoZbB)D&rSVpNce=$hQ!rY5VFOJ9{HDQEI)y`)tcs?Ng~ -3B|44m$>`=4bQjcdpuy3ZP~&4zTDhw+#1Rb18pRa)R3H|6qH^XckoEBIh^2#k)D+6P -T7ySvmrxDZ7fq7mZg~F8IFn`K7}Sc1@ok+_wKqD(VG}F+6eGVph0+N`@s@GJ2^{ARr~J5PU ->Y-qYfG`Me|)fI04W}OR$YmRf~86eS71m`qmvOOS<+@Y0_zVxmC6ZapUf>P%1#9*+M&mhmXBR=63UHO+qjuzf%XqTOmrLXt~=+cZO4XbW2332_-w8pWLrD0X(j3q+cz$Zdi -FUfD0$Mm;95zwu~2d{`l?V052%&+Jy(2_|l6|kj`faVI!ml(&CgTGy{poTBXum?|C3u)`fVour)4okknUt^jVE@p~;nd>>BO)Xj9Q8=^*~`gx%QnNNv>Z;Og*!MbxfwTdLp@91Di8k<$Lc#t4~T%AkzsFeO|rTD5YAC6wfgV9XhUAz4+_ -0P6|6=H#KU-biJQcxg7G4Z@k)lBOLBIMjfZ|?;i5&3yGU;@_6#%`^marf1j3vmz)xErkPPsL32*y`DX ->|AvgI(S{BnQQ1>P4CARl9{zYETOa1f)m=p9xH?lP+%bdR`{=rE4 -e-p;1>vHx`iA}wg-;EfIsZgMsu0r&T{^x`YKe5Ph6p-VaNZI=Geo-dT*~?9px|^B?n{J&3f4#A*O=JB -mw7|37qy#M0cRk$(o4Rg&yB%qjqogDf(58|XfJME2k0$Qx_LXAw$2yACZTjdsfsxjCS5EwfM+U`-t4> -z0``W|>!o@0rfb+4au3us(}VWY)o*)zRRv|ATk13E!X}Q0rQU$vY`R(p4bdYAiO(Z`yi+fITtM-!=AJ -j61_xV6$`Y@qtq@F@mK$oIIZ|Qm-=MWg*kE94H%zS+HrN@peej!`4vo7Wc3NnX;T>p{<3#mIA!byo*! -g=oMWQ~t5zHQ2~Bl -o+WgVW`BMw5_+)qgKt+;IR~^oQB(g5xng8+QiL<_Q>f77E8eewL};&SRV4DAVTL -{$?B$W98yw(vQb}>QTCFfD^(U1n4rv)BM6CH=7Tu;gaXvmn`kM@z(~kY6NLxAZZdQN_Y=r4iSm8gRVk -I6gz(04?j`Mxqt%~_M6Bn;2qN~!6DnEx^@)*0Y~PhcY>x(ZPZEq|p7@^-F%Ma`^2bP2-_QsWHF~(@2` -7G(MBRTPL8A7pOp+*%b~{;Wsr}SoKT~@_&pV$*X_+ekxxptb%8v(?U9){|FPVilWNvbFQQ~T<1@hI0@ -3#mYm+*FSDivRkDeEC4a_wSDr4+qFvH__CZK%uT>%C;BYJ|$s%iL2zNXKhWoXtzZcr;4xUxlK3d-#CHr|j%lEkL=#gm263Z?)xk7y2FZ -1neDyBsGId0Fj~eLd=w<2=Cx0N^!mowk*>5P7>x<;O%-uRRQ8bBOmb{8 -hkNyxw-Y_Drz{#iT=dBij40r(+ic{YZzn3Zrzipkk2gLlZN@i-3BvEGf_cyngSq(&vlBtJ~#Q`%fz+0TZO1GGG3_LJEamx&w43ta?@)qi>sem8?Pa1C#Ic -+)PdAEq~~+`oW&sT2lT|e3?*?XP7KTF!%8tUbLmHIjaz+ic33Eo;z0e-Ea`SlH*wh>Ro)D{0t5T68Rp -@bwwLMECk|lC@*}w{JT_v>y7-p57k%>9$Ste>$}Q_X4eW3djI4d@f3{^kar+fp7Hn4UYo>#;g;Wh!`~ -zFm>9-PF)VbTg|9%EH(661(vCNXsj=$)f$gdJ7`pC>tOg}J83C -U9AWSD}+xI1QcDLdN+7J|K|#E|!FMVf;^Sc52tX6Q`|S5k5CJ2sdJQO5sa-EgZsdX5ot%PK7Xvy2`eV0aiI#F7Db0oB(ui1qLcNtfdOh3e9k{fS -zr`fRD#>k9kKm1Jol^JDf~o%EH|3Ad7I<0Yc|3oW`fwTZ4*aUqrfKc{%JRvsb7uOzKKX)1yO%g1LoL#; -HAug79gvyMH_``zo~<3o>a){Brr9G@(t~oS}g(wSiVeXFsz9Z*rFu)h=4cUBK2}jf&N&^p!1hs6a|fdgKheZwB3fP -Cy?{bip1z=2lv{e%&q5p;rDJ_625;TTR=@LKALZn8qy)p5Y2DN5XCZo=)P+)lOzKra<@9&MB$GJ(V-a -`x=M=5+G@{xunYrGzqe&HtQK2G0b4?%A_w{u$5~O7Ko#w8}YD{{fyUFFj>QG$H#cD5Ul}cPDDf0hRL% -hp(Mkca5DQ5<;8gn-~;U3snE9t*}OsI2c^}6s2TEjLN&c4oxZal2b?Oz6Pc~Ip`z)6vh@}z9{EBaGFl -vw>iKR`t)qtRj`Q3sExTdJ=fwhAFC_{nJg?&G -fnd+?bWr{7tsDp*c7LT=JCC;{B!4Pui^J*V6)5!M!B^9wgdQPhrG#){Z_JUC-Cykpq-B^Syu4ddsJI~ -bj>tOAKlAh5zf_XclFqN9Q}OikvpJd*&000o2BNA(MuU7(qSFdBhpK_{inrwSPMOo5@zf~&4{)q`0Rb -i;obH}w3?wJ8F7x#{TDJMXxAJD{0a)m6$ -<6vkP2i?tfTX{6_9ddzhS32#G%5^|k&lZ7hrumnF&!4DzT0w3kF@?-j4R>qmi -;Jd9dfTc3EBpf9*bq -5*lzW5wNo~Tt9vDvEnvlKjyJ*IHSjkX{;UFV^3N2k?xpmzTb>egb>kU}oCO|g8)F6!wE;DV6~HlBUxk -91=}oWTv7C>ZBRe9y?c7YDQhBiE#uwu09k;3wKmEjC8*`}^Hmkscp8}F^2M?{_mO{n^fO88O7!k@tqB!tlRi3o+WaIw3?oSxm=o>&dQ?>TDep;U?p$h7JEz)Q69RAeE9}tbj2y;ZG4!7zFkh5z$-Cb -8&rZX73Q39(nGVJm}*kXlaT8;(6(+MZ@5B$0(L-YTYyNT9a(sKi=&0lhbhx>GwL?=qU1@lLoUes{fsw -B^M6eJG?X~`ScS(NWF~DH5ZtaRBQ~TXp>^Yn|JyR~weie1=+opKnv~JU|B<|Z1pAGbKIErVm0g*VRN0 -j&EJDvG9=yPmBDDz}Q-|LZ#1r50MnLXXUBfcH@q6iz94gl5)#lbVlwGRbbg8tVOyPI;MQSbe2leEvwS6%ONbKNJhnAxj$=6;dj3pCe)k-mmGcW0pNXM}1ISzb#XU8@J^Pag!yu77eM7xQ6UXsil4j1WGNzrD#R(>Qsh6gwS -`5L8~>r&j&;C#58&+G^&X^?xPV6(=_@TQM;ucm27GYQq;)lO;A%1Wr?m-%KvoEKx@6iA -^71Z0^Gy&U9-H`H4-B7@vimIu|n)5Kgk!+1I))=$u>6u4@8H55v}E@nj-%eh6P(5FszlL`7w8>xRCa? -TYYkm+ClsZvKr!!Pj-IjBCzpIp -TILS6guUE=xDJ0p5QDTrvy1U9m3^+iRYSCokURS-x5Hvd7~&_8=s)5PMFf7#miA^5SR?$rd9pYbYHu$ -2St2dSin)xIE6P?N*Kg0s!oJ)~X~baHQrHG?Bf=nw~`X0k4>y;s{Y@|v>*bTbBAIP53~UZoeFq5guJP -HtvFO@}(N_zW4H2(3TEJ^mL*Qnf&z&mZ|74Y^7gX}Cup>{qW3qC~`^(l5($NQYmW$*6MZc^1w1oAmPM -5NSD-o*$`*9#>z-SdFB|zo3SG>x?{kJHu2Y?$llRp2v;!sgcq_dkb0dBz@1rD!FH_l-zkMiH#oVvEO^ -n10N|V&a+=d`hcmP&c|qH5nAg0J6_WRYyAOhU^Wet~e;oWxKLO%)YGaA6VX3aML=VL -_EY%Cm1|dcZKb|Z+bn7rIh0@{Ij$(R*`p+cPUKK`YMi@L;h17d3$+Y4?lxN&gf;0}U27&4-+hcWaR;)&5vu+!P_mj{0fFb0I;^{vsp*u -s&1wf9;KwDGbLf#FENhPWFBLpuuwwK>Qeo`Z}_=bssbdY`Prdiy_wkzQ6u`0YJZLTD3u6nm2p0j?fDg+QdDLCASyeJ7XqEHJ;YknvWR!s+F=Lq!VKk{ -^kNiiVQDOG)3L=F_CTyy4qz_<20oTqKv->k~mb{QYaH1&dI)0AyKUwFzJ2pWTHj;e%95f|s7GNFodAT -MM~e_+_^b@N}KpXwv5l+{$Uc6%!6%A<73B9wZoEm<)H&urm=pP&$cz*U##2A+$t{D~7oJ_#>4O_1OVn -NS795AS}1ojgJqYSBm5JY=R0mJ9vhhNsoXggv!pY2$(}3-RXObI69p+oY5*+Y*fzkKjL%lE3kCRl^?za}l;@ft -6$eOlrp=`=pB&ZWS%dI#SNz4{{m1a7&aF&*DqN9|j}7QP8>;cb5%ws84_s{db&A3(5qRny>;ytQC8H4 -UbAwy4)r8#9)C_?JXuYW_m`l#kRy*D6sjw~c6(UOpu?2?M|=NZ;Z)AvPvcYs^oeG4eF&T>iI~L*^9iq -ZfE9`3}x-J1^sT7#jcL{ixvm^p-sqWR=Ry0e6*HeB02PF1%L>m5|&1lI%%WK+zu2r5CDqOc5^VcbSC? -2Hv(GA3d&!Qb){C1H|%|?c>Nt??B539Rr*{mWnMGVyryTO^UwPc=hNG0 -2eDfesHyCL@l`9Jf4DzT;AyM$SDe+OHI<_J`eUR3l*)<10T8ew^U&7C3(N43f=P1rq1LSM)H1r`4>i5 -Jj4Ih%n*JlH7`-_FoWwR|D}Fr^|HhD#0MBgeisH2go6ANAQk`$pdxw -qu7!0<4_Zgh9Xa>^_V)MZUjLi#4IhwN?$wZSu{*$c$kGof=ZE8iJwXh}p~E;DFS&TA@Uw*)jK)2r|2K -)j8vYzh|K1-MtOp;*!OEk9b;U5r^yoh4hla_kJydkJ0gt_mq-h`CtY^uq)HRd8QrAlUKwTU8J$19mZ_ -%Ak9;3lrB2hPw2-KZP9-!`QvX#1X$UW4ZOYWv_4p~dx1;j($h2-{q*g~hW%}Jazs+cUGZYh~V-DPAZb -=@SFx~oVwb=MFpb=MJ=x*G^X-Hjyv7`lGaL*31UP%gAAqS26ZwJZvO? -re3@kOrEA*e4mUwLA`i{Ngk$Nw3Q+P_5KFEJE-?b^xjLoPoejJQSZ~}^-=FZ^e%fWdDpV&vE)t3`P7T -oEJ;4~qSZ6WrCz+7N3s%4#`7tXN)u4lCWF65FDf#~IqJo4>5|W>7pV+6L%n!rOWLTHp!ZGc?WX;wUV0 -Dn&(zyP`%k^SwExr_r~Rj1`pkwvy{K9wJE#|rj>x^#iQ)*7AM0mzHF8m`b&nm>Oeqn3ic -VFR3xv4wGApIiSYmI83Xwm`Bx^T!(3m7PDK8$#a<2X)*VzF$)}~En3W4HD;m1#Az|NsxeN7X@?eblNw -X(FzwW0W~ecx4pXfbGnU3|TjrQouk~K1^*$+2d$sdt_$RG4qr9L`$JaBPbe89gdGa|JAYd3E9=Q3&6q -?kmvm7wyZT(t`2uE|1AN5WVj!cF!xYZvHvLt08yNzr!iz9nYo|7P!7;G! -(L|7W>-T};h;u8Nl=1qG3tun4H2D2jsSg-{j+xw-5rmI}I1t}CWpwen3%%dW3hXk{v>p_X~C%*{Xg`P>Sf&m-MTA>7!~6x!FQwtT)h)Vhd^y{uT?msqOK*%Db}c2%l#@#TDndP`$Bmo_C$uq;H+ -Syc44-LlsB2+&-(N&Hrd-~Hm(Dt-@&-womyi(FfOE25uaui^eBx-YCUdRhm+X0vNgZR9+E$A=VycNGe -;zy#hE#LFE?&BSMGTH~~MtLX^w=3{(;28UrMyvuyQWxwfsS4(B-ZRgVSE{@HM#TT0nmd~ofJo1b$B3% -chYopAQitjp$v>y5D!a1F0Em -rOh5Q){x&De!x=(*sD^VI5{yVYCs*U>1^bWSaXPelMSyQGPFYk^w{Jx_BhwK`2R7vgbHnY!Q#=H^`|1 -(m9w&=4H%QP|`qSYoCkFqF8q+>WFB?RZ>i<8k?qP2einZL(?%BTQCDgRf_q!DGY-LmRqL7apM4CdEiz -z7zWh7va*W{5{*Xs{A#&UM#NB<(EHii*hKp&~^UJzQ%`4`<$f8x8&8BcK{O_{Fu{gAzl}Yo0mU~HwOi -#vrC$)cYanB;dVsAygU?%H-au!?jytPRTr*RhTBHK6)Y%s6A-QSAmYCKXE#8JGEB(7Im-}{e>2_CKL6 -%Cb>Ww83PE_Gzq|~|g3^umhszIXv@F$O`CD!Z0q*55+paa`Pt)}>$6!C>1No1se2wi*dmQAv`L?`Slq -T3mq!=ir*oYL^heA#fgA~c?f=HZG)dfRv&Qcc)!r7=U=!bKzx}Yb{6V(M>fWbz{2Axp1tf&x;FIK&Ec -dRwBt?)}PD}*pkGIX~Tp-C1AF|@vTiv-wF7R>@0L_LcIU+h!N$AUE5T_stmPF6p;4KAjF30TDdn^C}l -Z5U;;h8f&RcpyM-BSxqTH#rmYc5C(tNfd5!(HP3(DBpDF86)T`+RH#&= -*{zw9(gzotGqE;5LyuP~N7RGtp*lhgr9;m;hpDMsxEjRJ~P4}u56 -2d+CV`DZ}qm}HPW(I@*e3$g=qb;*6rT%Zb^o&`$YQ_j`h~dhSw}Dl$Pf|DL<4lO1PEJM{%w;siy3~mJ -v>c2MmLxvcUoME@m!5YD1ef*eBeM6km`cRegCMx?aD|Ms}?AiOW>)v&OofPXvWSxxk!3qu>gjNfki_s#o|fXR29}|bv{k4M#%FdbKy -FvzTD-V9g0zs^zIVLqaV*O&_n(8i>8%8SuTsDW`&j3fi}bhSu=9C5LHkk(l&GJ -3?a1GLhBbn%Z+yoN3^)y6h5xHvv&l$>_br2HMgbEdI9*0`=sFrqd=P(NcUxk6Q#zl}KunDsB*^UnUYbjV&w0mKWYj3%?Y(&^5uPDsOdL -RJ1!3L%Z9e=~b0#4DBU`RF#q~q~0~Wrosu$Z%svO;v*h9uAxrzSZ$(Cg~#&REpRF<$!T@{r0E#DeyWx -{xjE6m({j^t(&*sBW$r1$Ata%rN$BM3(zb$iq{6bN**>F7H-U7K@v79taG{(EOY&S0ZRlqags&MFn-F -L9Cvr@s-sZKEU|4RNZUiiHaTGjV+H!Qoj-tq`o@K*i#2~VTk|80Q`V8^j@S*$wZ^J|`TE9j7mZ`k872 -2p2lRug5rV@w31IAxHg#;e0uIM9F-@1CLufjDdpHe$(#TN&Xw2GokJJ1e^=Wx_X%?)fEL{)*Wf0{_u) -vux>zCV-iJS&>xGniJ=okpre@y3qmwLJx3Puc2NEg&{G_Ow)2bg}PUHfzRM+y4d(aK{Z!g*@*pFGeoQ -4p@GcwIZ=7gmkRvkvjgS=oPm~lMMZ(OD6)p1Lah7kU*0K>pjN0{aRh%-&^#Y^jBD)I<4b41aA#U@QMzE;u0BsP8F6&8fsImnEWugoZ%0%74wF_74am};?iijCnb76a0{g+UJjpErPi|F2$IW848Kmj({5RJvBYT -1$UWWj%7J@_jVoR#qHogDOLd>Mmpk~$6TDD@9mX%QG{FIi>qF|u#F@?(%Cy$4)a*UJb{ -!S&=kM!7Y+1ry=K|<-2M_6B_kmCbi;lC=d{Yiu~|u^ca7&(Qznx&=AQpHQrn(;02)tnKNt;;48vp(t1kFD#YvCloXs7&X -AjbY5jBi?zmgtoI2;=ek??N*@)OBTt(roU7kNoVH9jXPry&Vq?yyI@;cW`0!#!Os -r8-d#{+ZC*DvY=UL%}+}N$uN9Pm$FZ&IKYBozhfsQleO^w_O@627*b7H0Pq2Yh%+?MMX$6mh={n158xI^Ug&@kph;`C3!{B&R*t8;x1eqk4H2EYBUFpUeZiDrSR;7yD{~ -mPKA`Q3voAkr63X>gAg;^Iiz}TtF|!yMnCowrCNH9rV7=>~KPzM=L!-HupCdQgYy!B@f`^23E9{vIkU_;eJw -4`5o-VPD-PJB7ozD8(Ogfr`8yypWbI3!F~==QT0jiR*6)zy&wZ*qg&-6aj-cP5826lB%UXwrnb)Gy@^ -3aZHk0ruV=c79mbz<)ExyfLGp7I2QYmHetOURNNOdTlG+d40C{CnP3Dkqi)XRhLY>%f<2timtA{|5So -;K7S)P-NvMS29r8ymq7!d=1~B8y_D0r_zCw$_ek91ltOn+X4os$$@u)?#vsxxGa^b}UmB1BCROVs>{d -Tf768w?Wy5f0nXT)t!_jsp?TS9F&a|DW$glkV2ACSWCE-39}>XhD=tFD -VQfT@TpQ`xySrb~&NU^B0f*NE|5x{zuiosv3r8G8PUL8fW%#)apdR}ftf_5UKEVig%5HdH!07G-fVk8 -VYllOT=bE$)rM4O&Wu?RvvWupBRc3?l_icrD@Ik7koFGdRP>u&?wR^q8?V}uZ5k+&{^&b7#@m(o+y3 -#^_dXUMf7~{7kGH-?*Y-<~JxBMoOOI7JSoSQ_dU)goJE#lboQt;_KbF57Awa2ifTq?16w?SGmAc?%0q -7w+KpHzhZR!DXx&u&^)+ygGRlL!9EzKF5Jj7a;J)le&xr!A@E2jF8T~)crn~!$Nz0X3{(U5CP-7GET7 -13G}3x`~bkF^%PtE`;2e|XTs$^}PKu-AviG|cICZVO9Gd|z%>nOqzyT40?~%8>tnSiaV_{{I|EzVvO? -eia!(&f`0UF??cadpF8mR0-BhGqequ>TYoNe*g=Iy9MnrNH4PZl;E2UdBuRRxnjkDfQfC44@ikz)LUC -pD*J`Sv60H>pj*AwCDsbxtXN8AA(k3g`nGtDPqnBgsn-P>pJM@2-PF(S9!LBd8=uyMXFEv?K^2xzYP> -e%9wf5pHU867hEA4z+)!caf)ucT)My^p2Wgz -Y}*|5CEPr`o+rfY+?7gN;~|Khn(-Q)w~7>8#%76xI{-R}H;FAg$MfIZbR*ez49FkJo{ll^FY2Mu}Yro -BJHQ{)^=#-Tc -QGLrGN;l6nBa8nB_CeamzO-XWDLeX`#<5t?*3cx-mmXv4oe -W*fi%Oh;T|!snOZg60ZZgO#MTLRJh`h8bnf~-Ug2pbzN4xyJCuTb1;SQNk%oCQtoM?^c+s9Ny002q#> -_Vu2Vg)R2$hVg%_kTl`jw-;?6^n)p2}em9HXBiI2iF^7IOTZC8Ymh6>UZ> -in?&4M9w$t`)v{K>6auhh>?Nnch4-8LLnZ);~RwhA0Bi{HKCcN2b}Jx9MOL0_p0HZ+ArKgscidB?h2u -$YlUy*yX)8Ks!2;WgS;-)kgbNnRswd#@25!)r7~e1+kvexilsGxFRj`HVm=W@+r`J!!{*Y8wa2&%tN3 -t)9=wvsCsOZ52KvBro44JTbU*Du2s%ttx+6_>6EZd`2sgL)?}ZHo^K5H~ShrX%8>%hGx!)+O(98mFm|}1ywG%GE5{r1W9nkdUvpFNcQ1dzcC9I2AppCYs{M=|QcQUczQ+4Z`<&#A#G -~>IB(C<6Vh2jG8xaeKQTZ4Vo2)JvCN5-W5hm{oe8q)PUEnD$a@7Uh#l=K*K?mV5f>;o}XbR(sgoN!Aw -JbI(Xou!pUg;{6oveOxfbbjaQv5!6sRWylhZ)>S03Me1Ra6(Ar-82texKDcBuT0*g2fWi+M*pbx0LDm -9hIUw&=v|0sLd-Ju;T~PVv@f6jPMpAljJQrz-l?SgW1O!UFGMN9ik+%B%OB&KhYkiDv?SfRlQ|$jd?8 -!RTtcVljvL9Oer3kHeME7DaK79SFdR0jBncE8FO`pB@XqW-U`pVD>izQ7a`z+fPfwNU2vrQwBlJhOZd -a9^ephUx~dDV-G&9PF8tUDjDtBQ1V ->}7;lgetV`9GAT#S}n;hJI=aZ(D2NJ;gS`(b=u+a|=^g77){NNlzFoRG(G9}PQd<)i!;jEy%Q5l!qzN -1Ml~QlQZmE1{Pkag;qymxRag3zh6~+NpS)e#9hAb*do~9w!gsae75~oP>Tb7ejCi)8)1RUsJpdS*k^= -R&4#IF31&rCc#4CXM!ZBm>uC~5{lMwpHON7?dwj80BQFX0)A7Q6iNQ2HSjN;v4mG!;%dsj#Ok^Ck@lE -rOpv5fcn3xX?O)_3DF(~_#W#e9XmHUM$v0G9iXCbIdXl+t(=Ej}RLgfuHGD(JEPO+bvTtY)L0pOG@EU -XBGqsh@i_wyVZ|Is$Jm~3Vkpwvs?0rLPK?;0B-z&Z$p$6gl5#SQ+eM2dx9Y7S$p>OR|*g1zng>y*ohs -YZ)wbz954!1$hHK8-mf4YJ%9>PKNSd9?zf|nEr(II?)C{jtTAyK;$_a)cR0=&)OJ&`2!AQ&K$no7pf{ -h?c8uXIQ&W+ml{68<6F*S2j#XX!#K@dtjQaN#EkuUS*kPL3niD!w9-N`Bm+zeu52F0zilNN^PXBAY~X -Gzpcg_>1ry{vshEvcCvK6cyhR-Wbu{6iHoBt;+}ukz7X3RYV(HM$2#2aT$RzlFO(NfP~jbX&jOcH4DX -Mm?peNW%>TzhBz+TuSH0pq^HW5cSW#Jd>d?yR*J7};1x~A2;Y*6V`(8bUiB2wN2{e$Ogqq^s1i1bc@d -znNo(+YL%55a()v}j+@zwjXNAiq2Sb}Q&7>4_(Ln+Jv<%*|10A)eP?(-Ogq)Yd%SMJI?T8l42z)e9eymvrf5>}3(9RCG-9mS9`EGW3-Rx`Sm2*Xpy(v;*2mIEZjzZ8r2y8M23H_d8m)`z`5X4{ -@Rp|B)(k~FE1GgkVTwo)v@ExAe5r9Zfsf?IOqAbE>Aym@APz+`0xcf5OIY#o<&t->uX?Y9cYxU?$X_9 -4uUxtcq>_>@epxZhftXPc#{&QoTGY>Qo3V%((9B0gU*IjDueA`VQ3A?vzvpeGwssRI#5p2t`_9SG)SuW^3D&D -t0LS8=PxWyZesHlx2sxaeUP%@TS+V+DwYzhT}4#hsM_b-fN5^VL1|5`ADN_;I&k0Njs-l8j^?Qs`7k$ -SX3Cpk)$@ynE&{gX>yh1UMG^kUw^A(pa?$$JB7C^}Eule$Jhp_}?N;94_sYmCZ_D0(Rv%C?V-dElx+p -$DKAqOg8nZ;&C{U~*i#c6-c58{WI;45wNfXzqi-MrMe0KmWyc+oG-fTQ)aS~6Q#d@)oYSVM*vxYgTjF -U}^Xwd3U7`_KUKHr8DC&aT(ce29SJoR*iBNkZI)i7%xRNTVT%-F7ITJt!GZoo8jijxR2m1!$XmW3xR# -&ZOJ{$g-IWcZ!4*?7@}{afY_FMo)bEmo#||0CN*TZW{f<|2n#C9hIzX|;H_ -plNqbyE3g4Gr93Ht#eZ>&t)pnQl^UUN`G`VFT5y8!eP_-uBGRd`YMCqBv|3u2Unf!Cm@x@RC+GNVk*t -7+w*~aiuy{)*f{Kup{5LZmffZcd8JlY!(jQ#z7%zyib9w>el+;;#*Y?1vG{oeKiNfQm@y-aM+%P_23v -O1c-Ekd8mO?%uukN -cF2vm7^FK%6Qp+IX|H0(l6M(@10W!{rme%BH-nH?`JR@rkfc2_}bQA!2NSZF0D^_07lZCxzG7ofN*$Y -OzII`_pB`sYWM*UBqPYp*U;0vi-NT;?@=LC&Ez@s$cxw}>>?9;v3IZzcntdXqS?*b -rWUIPW{3+OQ)Y-?uCX;SX~pS9EwGZb_3|3AQB|cSva`H{Ond7|@eKPKl2qM~NAY(1$oLax757^XdG0q -lr#VclXy#Kw!?-miMG;I@`wqQWma)PkK}-~dzpX6R;cjtJZ*$7UqLL5b`?(HcaY~B#%2MX(j69{)4%b -ftPVId~r8J?GV5#;gu^cYshEV~oh9;&`C#@-f8L!7{)0|?ho3MOoXHCAN-gVr`m)>2Ivl*4FcOto -4Vc*zO($It_+M|7_}oHK=O8alP!0&Il{XUJEKCK72UeHqkSAzU0$}XvSIECy*WpI%CkK4T-aEo+{RWn -iO=~Fd^uyVXRL{O29k^vGPv!dQcGLTtmOcrdB_ynpee>GGp#~EcX^*Y;N$O9@on{@g0g23tsBqhAA;W -!Fu5>F_IW<^|s-GE(1#;2N>VK3Z){CbqePTcs$rMe+aea~B;?= -s#F7g&M@fBFT8G^hLh=%zS? -4eFULKx*3QK?o;*wuh`aHo6qjKA@i|!<)Py@>8H?w9{~NOF}=u#duZP*hXy{ji&CDATJquU$KHl! -62n#%020dz#0kjg$~!x77cd@+NWM#NDzleFdh|~r#i_n4t*f4H&Ui9NJ6O-YKoeFXl`Ym;{^5$kYIj; -O@MJ&_sk_AgzuBn6nuS3$~|w(-+I=5m{vYb%qBOt#9Ui8p-sVfRTr{$u&n!ib>^3QM7qn1Sv&faXd9e -pb`n#~Y0UZpuQPFt&$*jn#fznn5F@AVk=GDzf!6xVRP=Qv6)_1F-eRBL_&T3>Yhl)4>-lK`&e)~bppC -ZrD6yDwWkq+Ra7h>{4;TI;PHZZWPZg`zk(@4K%E<7xeku+I -@xHbJW(3cF-%r)c=fek591t5-`=x&WmZI^W1nyQhE{P*)jy6GOa@;+y#-}mIv!rd;iTcArWs_}l` -t9T5vDcCcA2_<1#Xbnuq;Ihkrd6zzW1essKA1_^=t-3HEhYtee1^@QZg4{w7Nc0`?pfLCrg|xMAy5t?F`a5jFx2M%ea+FD1gVkUUXBmZA_@47yu_K7}lmWXJIUAcBEX0LAUbeETCd@X2;dKL!wV{uIzH9E{H%ogJ0k`sSle%prw7_N31bNR`%F!o^TDHtvqO*W&>|cG1 -S&QRl2dVhw+-}HbKK0=W$}SEGvMX{l>niZ^RB0qG?GBnakgzi1&?(+DM&lQ`xjVv@FcSp(H`Qb-t5&X -NmL*?a}JRx5SF$v2>;fhM{!EQct~bc3_4FaCTzGA^A~G`Ie?dqXu{^Yn6XtasFqjrL8J@*L}Jq|1*aQ -YkrbbUPX)AnEY<}g)s4ru3~Mtse8bTo~(qCsi|eG*76tIL`3Jk2+&2=xzbB{J&F`ZW%JZR)p?qFzPx! ->HbXs%OHo>P?ZDn7t*G&I2qAj5`78Lv;AmN<3K$CwE6JmUG;Sj%oD!Tn3yi3FwAIv_?EuTCn*TL|@>l -nK<#g031{1>Kt4LUKPknA?*wcucx+Bzsq={=B)Zt(WI0pPsFpBR)MN#n2wQcHtz&hp8^ax2Y+X$TlBg -4}+AT(z$*?R}E?c-z*_6-n#oG(ZHLAT~%r5B(@l|2GGSQE1C_Xrfw%7>`rT>=BMZMy`X-=s3M!=?h*BM{4;) -Y~Hvn}lJZ&@>0rxhs>K?Xhix_KTZxrYtZxrbHxp}lx -Ay;+^-0_|Kwnk0GXn6~^A)*%k#7=>2BVKPhTcs}3_VO`D$_ny;E#Hj!0+cdIQ$T}kFNI6Ra -xLUV>9d_bJXWR;5kDRxWT#J;rch0(#f#J#e0d4^goMgK|8DSPgs2gE1SIvx!a>ipP@P4!ElwAOs{pKi ->0zgdAK**kJYA(O|bRM_4B*kS={yWvKu|wqm838v#WQyWF*Aa~W9QSh6Tw|;$jSV_e=u07j!Z-@a -6mlu7pztPzG76_D{7Ru^6=NP0{3(o}Fp)wUg-0noMPUPlrqtcuyvf)}y1q=IhJxF5#@bNmMZurKXbO) -|Fi==Y;av*5DV(D4D}|2LgcAin3Zp4ZqmV`+m%`%|R#MnR;X?|CDEvU-SA39vgR$lm?x)b5f-i*#3b7 -QXQ^=;Ugu+V{wo*7q;R1ykT87zm*U|CGhb -oAGsC9`z;V@yNi&RhyJ=#P@I32ZzY#`-aTyXyhWkBwrHqPz)gG>c;4Yz&JKPe#)3IC>UIDSX&Kf*e6t -qeR+a={dvnIvvh&IzxO?x=~yYx4RBYPtTfxw0dS_nasda2~!=@P|(vK@YM)dEm%`=9#62xusHg&Pb0; -~5oV*=@CIV2ekfNA3lk|CipMdI==3Hke8kg!l#Yft^rs`7vgxd$kVU^n;uLU<5haeID{wYhJWCO9Gep -{1L`@D|CDCt=fHIwUKA)Z>6AV4YX0n-dmq{r=`CI{^78c@DMNAUqOJ$GB>9Xi~Q{sdIAL)|mcZP`9$z -^J&G^2ozcny`CB}zqHHvOgBa!3&SQ%sbgQi7U6PjkdQQqCs067HZ2*y%+{MtX`gSwy);z)KhDCCZXS$ -qJwP3EIHRR7#^GoHeWu^P;Ee1noh=JxIX}8&3B8Olpu*nF2>h&kRvJdV!0+PRXwu&_?;v}SK!vRL -+~_9$U`5YM`wvX2Q6J^xHr_n&)cqk?j3Ft{m!Hm3dEc{rDky;HV$q8IlH7E+$%I=oP9sQgn}v2Su8I%9Ntv6Rl3w(zEND?C4`W!T(pDVBFSWwLIdVeFy3zUEF)hg-W1rA@KAqRm`B6 -F-&>4$HEub@|D5lUkMzjsR=%>;Dg0^^}`xUJa&OGT)L!7H?U5%ov>68=Po-b%`D)XVg_#>N^_y))`MX -C(B@0D@`)@;GedGs41*G=6LB5f_An}8i#2U?(@c?A6h(jD$|g!M^d3Hk3KVK+#B8rzfk@*fk=y~J}b` -3c$V!UBeGUOGpJRxH_FXrVYp6BcvpJsTt24$6}_I%muMv+^uVNUc%maU9V?C(O59LvKiBXA8?NUEm1* -&R`hL4x{+#!cM8>d9>)gYoAE?DmW^5DflQjC}k_hI7y<%&`1zWq%(d^gwN%IDwn}eUhL>Do-g#fvvz(;3)aAI!9neMqrK*j$}F>(!|iA!Nq(!SWJ -vpaVcGgrQHM$T8K -VAl)AhzAy_6;?Q(RIxfDgEU30Qyw7?f{5U?4y8A`gp8G8pyANdm?&eX6(`Kg+1kk)I{#k~4Mk&3L196 -_b@I9>f{tbCLmhkq_yS4rvh2R1S_#&MvNQO`0}q-lC=2z14lK+qAvEUHc9lJ9X~zKvzw-?mawu_Uhf| -!M>gk_3Q8DJz$`Z?;yXy{sDnO!9zkqwP8cUBO-?l9}yKja#YOdF|lLE#f_g3pD=OK!;_~>otF4W()1b -0x|Er-QqyLqXJlq&&(Y@?jC1GZ&VTf=1q&DDvH#x>%^iPzY=3vg-T60ydYHTY6*TZyH^pMRKh-_{+2> -zk_vdhre@b4>|1|sm7aVui>8$^52~gGk-Rn_D0#vns4)^$1D*^x2zdxcMe{JZ`{?0z`o=KMJu6uv4^w -!c{|B2X}kL533Vk#(n{E4OJWtQbdE1q1r>ZzxndG@*ISFd^D#h2Fpk3Qc1$&Q`7N=kR{+52hPzWoP2`~2V+hYlY(dhGa#FTeWwlH*M6(IUVY={t(xC&^Z#;3(72+%r;gx%n*RUk^8f7}apUp-EBNnB!=PreM(je -@PV)70cGuW~)J?u#VR!wc-StYl>s5BwPuX2RZFh}rNtNqjyKAft+C+jUUa}ajQ|84JQZtjY=EWJ345? -&8(env8I{ldGX}TGPsLYfseTIlgkn)U5$}xn^Fr?1a#b#w2v&A#%UIN2n2x8Jq-O#Mu1U7*zSj>#qpA -+vREPjl{AC&Ghjp^yqCH75ofj8KlIl3?x2Y08)$Vg`#gS=^MgTxyD*om%gY_PX?LaZxwHyS~r+at3cp -@Xs9>|8qBC}T&9mb5)1+K%AQqy(G8bg*QD_fVT(dFu97!66&(_x%X$ixNWQi$=Iq)bgxHYG -}?^sv^cLi*dK(d&|uH7WY63`#AsvmV_rRtzIdM^GJ2q%&hdGLNFOrV~E4s@Kdh>h$wdGiPd21VVSj=j -aRuyJ(hUNG5WUjoIm`Go%zsjEE!R8;+znonGH?TrDj#h!}%TlPyWF#+ZYmd-qJw>0Os+Gj#@HS*k%ZB -P&^_$x2ajV0@4HEorRkCC0OQI!_mKCSLNaj&eliW>Z6uAWEDjco`W{^_qD_~<BgJXPxaDb(knW67D4lbVs8u1idpb>5WZ)EvW;T5J3me7(?|b8XLOv_^hK|}g@g?=*AMT>PYyOix?;hXn!Mn%z?tAz67d-DCKmVb+@k{OEPxsT -Z%6@g@kJQV*%lGc^Uk$1of6^|VQ=DV)-T8NUNaOi&ekD|EyWV1VT|ZrNSZ!Rk-F1C@+e9>8-u6f>I}~ -|$dMAvm8-K8VeT}(${HPig>2y%pzfSXnifsRIs -p}aDb;uNQY@fn+Bn3cfh(4GAVf~-bqRKcdJ@Jh_6)aiQ6Aa7#f!lF}#BUGM%Y}D(idsIrh;gpb+ -Y8aNKkE8B5T_;1DNhvdRddeN1m6?;3t_wHl(<7-nH4v9rSWZsr%uE@=_^et4*jbbSV#3LYu)QDD#(|# -3WF;HZb;HP78J(0tM6r#IV`2X`NXbmc7-PC2b?AJ9ZhTfkYO*eTR+64QD^p}kN49=4V8BeNy##o2tq&b(~e9DlAc*vglsVDWZ>giczRZjayK$^+~~+r$~Yua6= -%@Xe-u#`H)2#mSZtJtXkSaal#k*+!|Xm7l$DxEW*Ix|FhZA*cu9gv?n2spQDq8M5f@{yM_DZ}K6|I1MA!<&NXvXvcPAn>m&BAq9I1S4OqaMdZ#^Aas@tm+sO%`p7#= -NYbvLvBBB5Q{~5?oRor$no6AcD2Nvt?6@Y -UQjn+3{JncFbOv+Iy@{F9bDnhJ+Xu{WPGAPn|Ja#i^67t7XQ{? -|=DH<8ax<+x^careu4aaPN4-Q{O^|K~nA;CVOs`5^f|Oui47?_=dWE6XKY4KZ@K~Z{_Y(fShkK)k6t8LY_%|c=-*hzYq?Fy2;O9J1d>nZ(v!P#8~P6a_7XU<&>ed?|QSc!)wD3LX?R6gpFAPoWJ3cM5J4YR+icH42x{Na-)q`80(S6!udnq -3{8PO%ygzSV!R*3i%Wa6q-2I_DShMpQ)o`1`a3Fz!U+l`6h5S|nZhOt>nNM4oI=fMEwfTMNntOA%@j6KSVv(wg=7k`6ma~z`cgyFu)p< -Q#qPuv`@$-R<~-4OiR$`C;uYmR*DQ>Ae6RgWxc_fD{^)%shkA#jd?@+Bm+r&|*eLl;?^e>i9UAASJJ{ -dXAHVgNgE!U9s7`+z^5nyp(#LjcJfN;sGHiP}7++59i-K|}^Xax@q_fL^o$CJ_(mOnkV!YLdh9fPR7^ -~!C%(YY+p4g>x6?L?Zoh6UI4beh!iWZ*1O6ioJjao1Cm{CEm -p1ZzbtBI=HS+w!MxO6)``l%e-l_Kf+zOTWWL6D%~zqW?m-W&pYf^jr -=JoHPU?^(jPpWl5%*Td|xBQV>_H0+!Op7ipN1|@+A1uefo8g{@}sGWzvJ|0{&T)FWpo5oLdgVDT!ymu -jcTLvvgO5c!B?tnq>!T4pspM?r)Eq5=9AW(g{A@JAZPZ&4Vu;JWM2{fIn_0-&x%HiZmvL3(f8K!hx+u -9Xw1amV;exHE+Byz+Xxr?yJ8zyrKKs2Sr*0dVV=9m{(JFF$X~SA9{8C!RngB`6VL#Va|V_u&cU8L?b= -%xBR7QE}{kh0m3RpBfVIs;iMET;0xTOXy&$5KBQCz>eoM -&UYWi#o}&D&g26OUy-#`IxP#qy|%Qg(5kx2qzNBw=>mzkW;HN*8(mxHOsbTPX_tO7L#%*s&}=KAugQG>J`}I+ZO-9>!+QoXOJ -C(pW}D2Gi^HLVqk=xR9+khO)OF9m3vU6vRw3BiZuV;p~~LF!rKe%eF5LWbfohu@@FkXWLe1v$7{gvLm -m~W_@;Y_Rwz529$9Yx|g#-pK%s^h_fkQa29@ovyrDb8+(?s910UIa5niz&SsqBZ2D!+7B61R%w{uNwQ -3c6{`u$Gnl)?KOE0~|)~#E|Hg4P~>UZB=0ZZ_wvw@? -Bsa~dvA3iKtl{^uA$$Ov%*V3%d=6X9SFvKgg`MC>6@ALuQ2y?e-<$Feq5PvL|0K#klk(^CR%{vNe~I$ -HOZiJE|1rvc(Jp^4%CDvT4^w^vd@lelk8{sVQfZv4Q<4Hj^EEnDkF288KXQALAjEe+uP)l=44M`L|O36L$GoOBT!8QJd|?R -x(d&wtnm|Nq!}ZV!YMEj1S0WeC*?l&v}XQRqrvrWk2Ib&)VhJP<}tkKZ^2Cr~J8;e+A{=K>6RN{5vWC -KFUvJt~yQmFH!zVTmFX#!bpOUN)VnP2pb8)UV?C*AXK*E?AHODT^q|;)f~>Quj1^+7S3)R<+l8tD1Sf -7A4>VhQT`c}e=g-;O8M7N{?{n~2b6y|((8lc!8rwcMTyK -WrLsCwoU8SBZr6to+3dv4f2OaAT|u~ZCZC3DWf*3FG`^NiJ@UJqoboEqZ2yo)=7{EHXuGcGA1fIIwmrvYn=oX-@C7XKQtyHIx;3AX4nHA+sX+L|6refqnHFgToj>jEio7gWc~ -^G1A=0R#W9i5F@gwDg2I2oKQUr}7AQm$jE=*GwQk*-64>Gs{ag~YUXfJ5Xo`Pe*f4=YXIp%@+F9-2(m -OIbB8Ip;tg9fR%P?6yLIcCX20E&py+adY5@VtwWh`S9>52;X9UAWCq-r`^jsUNsB5X4YQHcRw?oC`&s -#YTiL!cNF9ZG^!8y_}!khf10N4NW;C^CE~$+E9+czA7mV&c%SF+nX|o&D%CJQ2{NB5KP|3`deNK`k6y -wTKT7C2@^rwejE&37EE7tCnLD!(&2&NY)s$hc6u?+*<|2gb^R3?4kibLPXQ^!9L+ZQFWsw`62ibB#2R -wg97VC+rcN+M1=*`i)PVvkC=MVoR{QPtMfz8ks(p?_t_Q)dLaGU -M%dTt0?ni|Pg>%-XxOCs6k7Z$O>`^lytJvZWO&f+gfHbX9*pL^~(_Tq~#vbAg1vi0lNvkeC-sqN0LbzI>Ve`pXqz8~pa$Z|wKqe -`l+%bGC)C|oTa#4KmW_C3A -4ys-9XM@`3$?K^hs+ts~g%Lho#I&|;TvuE#~Uaj2K54NEDj@^3n?oDZ1xp(#Q^zvk#yZ7zm)upLdD@x -F^MT-YIck9{5t69@N56BNRnm&EK+}xV?p*#=X*Rn;&_U-z5HFI-o+Qiwp`Gab8M<1IseI4QZF=|c^z!n=heA|-Ur$dDPtR_ig -1*jlz~|Sko8(!_Q!^HfNjOJ2H0Bsk*`+~xyeQFCnjgY;5RZr?n!D=RdpML=I$Z36vs~(b$MIL(BET1~ -P_*sZwJVs4fY0K3HEGg>G>cfzsZ_XzRVy!`UZ3DoawzjEcu5C8h -tzkZ~?=fclF|9pwgXRlqmc7?{J7f+u)y=C9NeHN8U?PCGUWS8LQox5l!#~tDI_CR&_3G7w&fr -zcd(7K@^5n^(ess`vr?0>M`bio`^If}k@gINukyC%lzxwJcOi=N&XU}pPe{;kW4V?Pf@2pm9InjFl*s -){(C@n3`8#;8T50mN%ui&&}(kn8+1@+og4!Blc3Bs#4U63{-Bg3bI}pDQQycUJjKPBk_ -X2)|m%#tC&pzWHee@A0{P>3-ekkz1bmd~XeAmZVQpMLs@mz0# -)_AVgAiG-v~UZegy8gK6maM|MuH&#XV?J0@*y`Ofq-vmtTIVCb_#sFs(Ol-sHdj`s>v%zx?u}@4x -^4ER}Tw>4?s!sl5#%99AIjpT(c-DU@Xkz+VZVK?$f6B>;bg4&_=2^=VN8;cN?3=eA(~9O19vzh}>$RXeIyis+ytoIBXcN?i#rgR0<07DKpgwR$y`k+O&*z_iF7QVkQv2tqhugPrb -NhqtZ_)D_#8Vp{_-F91s;c7D4GH=8_xJAwIYt2fUwrWe-@bi2r#k0kGmAD1{-Zt)95^5XaNoawzi2Bb -Po5OlpaD7n@`iSTIs$H6|HZlP6z8FbIQKokx!0%I$By&BE -mh-~;3q%Tn)f(GCVo`bKT2Xz75agDk^bLNZ)xCYO`Z{Q0#04=~9{6`(Xa+&jpuQ=Bp<2>jv=YtP&?z5 -k0*voleqM_Fg&bxoi`HIt=*VNR!^k?uVU2hA(UkOTGDB-WP6SM=!OTrn>ho9s;{7d3LXrMaq`<(NEL_ -_A+oLALwo^q6P@0~W+#697E{rYvt@^T*^pY9|J1)zcI;SO4W3*-nmHbe*b4xXcqAUBYI=v^AK353QF{ -v*GYXc&5e^AN%{kZ2f0a+7^h!awym=N_L(ZQ@?>r#^!#`XBO7{Sil~Unh#3gRiIy$Q5Kmp#yCkat66a -yHL((7dYb1b3U4Az!na}zLsbha)k4MFC-egsVzLToAW-q{yhGqi&ypR*>e!p!wR%B=$3})fUGwrU*JF -D(Jvq$^o;!xf05cKX!wrvD54>fXxRKSzn9||3NJK<|BV|rIQ27i0%%ZzLPtY&(hwcDZr$QXKU&K-E{N -tY%n9YsWoZQx5C6dVgzq^YdyexM(D04io=IkcmG*o?3Nx!)&JnN4RU+#51m9hw -I}JMZbU=Z4<+47$Kr```<6X9BK%E()U9CsjaPmJ%l!=(4o)*8qgm -!CR@NCda`(N7=QP%ApR!Nu#sq3Z;@i%gbhPgxdZ9EmhVh5KP~<-_Ft8WuLJ4TEz#F&$cZC+{e&~CQYoIsL -SD|miv2%$(|A=TPUKGeTF9_mq6Aks-^RuLrR%L|o<+F$KrK#cEG&7v%B}Z_J@nL@E>mz~&dmE#_Y-2Q -rKk0iR|Afy9;0?TR22Q{m?E!czbpjo4Zy%%o1&xsZJxfFQ4x-^hqTz#uL<6-2v}e%picG^AnTD01A#E -ssViwVm62TWGM+zEnKqm#-*%*U9ZCL)HGth^@jzHM7X%m0zt+zPrA|=?@$=%w?@4x@fkNhKvmzoCiPZ -kpm)E3?+8vZG_XXvE$#t^<%AHr8>hw^7KwfxESFm53l9w!=>5Dg271|0QmjA_Rk)<3|XWI@P3$#nr}d -HnIm`P*;5E$F!yIuu%v*Lw7QzPn&BFCiMX6Ahw06AjQwN_$>MI_V{SD1Sa%%bzA1RuB!#h=xLvqs2P0 -EtJqnVRkmg`xOoAf8ejke^5|RFW>ccbO^-)s2!(iixr%ftCDlRw`sx99il+VcjPh8JZ -To+3F~F*}T#QzaU32pfa?=TQ{a{wVt2&)JtVESq%9hSzkrt{z1jX#cxv^<`lKp$ie;~dy^s0*|Qv}w_vsUGax^BeWr^TBN|!w!7$UW6wgAmEubYu1Q)5R@&qWuBj(j~yI&j~+evgA -YE)$+i}_01vbc`v4l0P@fhA$dm#by7h~-)A=WbTI!!A8w36GTMNVawx?(EZ$ID4tA6|KIMw$(&p)hQy -_y#k6kJ`kY879yWXW$2Km0IHPfr(or*Wl_CD4F&0Xl&j=%`N%<{;1yL+;T>qpe^*0^>+LgN=l`11$|4 -ME;F4;L)F*|3PMIyX&&Ev(G;E*kiYWXF@`P2x!v^4SoCe<@4sv6ZL>TLz&xA=m5UJUzzj8*Z^m=2ec8 -ihx+XueYXAB1LJ4*C+FYLwjlo`OY0XeUVH-WG&(w(FIccZ)B|V${zHcjfz#q&TQu`1zBqb&BQ -KLo)+1jvSgTNoW2G4N~ywMJzfAAc(DdY#XHrfH&0NMoLLI)G>w+T)``}Xa%RPV3s-MiPyB%hD#z2c8@ -?d>0huLu|~0w1bdPPPT!Di-a5>YJ0i2^l~i1s*GTl=(#HQScP{|GoF#0Fe>zgL`Fu2(5=+gMgIm}1Nm0u5bXf%5_J -MOK^;IR+uK9923*t;>I3~b(TgwQpf{cG5qBH@_Ofa(!}hWb+&~v(2yNksC!XM>vqV2_e*iZnAP@8mbP -*2755`0Yz#VkG{PN3~V_fIp;P7X0x8bkY|HSVC(4gqWhUfslp@XRJ7UKlS6#Ci*=JW;rs2jzGfL{l44 -_N}w(S}yzW&S_U_d597%Rh8P{XVunEjVM2eae(6{Q2jf7xHb-cO@VlWC^m4d-MUoUGa~QUAy+bfV;ro -e*8;)&kD#T`XJDu%=bfoK-Zx^vZn*Q-MV!vf8m7}ggh&47y)!EbMvs9a1WVL{0F2nUw3qL{IB3{!{5G -Nw6|gGZJWE*3BtN{>qLIgh4i2e_(HazJ3)(rJK3kN{Y81d1ApK`?Mui%_1^```0H-8D07~Q{|WB<5q -E*V{ro5C3ETV+Xh7cy`vv+Ba)AB_eJ6N~F(-JBdQj$fQ4hF>&WEmr9-}dZ@M{pwe>d&|f5`6L9+mp@fsGv~t3BDi-qhzu>vX!YWba3i&c|!mcVZMquzjBL)|NZ*!|Ub;+r=@i-LuaIq2=mu>#r6t0raJx+S_V -*rNF6#ho|x!_;HoH*uUzr4ozkzeo0*V`3(ddZR{NzXj^hMCsuw`MVAP(0!CR=O$ -h4Asf--$VG_;)ZT4FG<8wuxKd9 -ZMEUmbKGc8b0X#wf_4B1u{B={jn8#cp&jC*Vne)V-B)@6g1sb4yYM4niJ})cU^bmtWr(T;B(b`Y(MVRQS8#E5e*6=EfCy#Jo8CEAUMSAD3MI!sKD1tl@U@IOJ -g6`iD(sHk*@Pdg-Njv_*{fVV{F;-~t}teC*X!;Wvc83iGOO$#dH;%l-wl3HV3gE6mqL*m!`$UPr@sYc -Kz3TaP~aXeR0ZG{rxFaRJ75;0bgOcyZ184gc)vSm7tY{Pbpdetm;on}DCmBKyhmWE^k^d8BfuP&j?PZ -u?ipXK078`R)BI_P#st>5qIy$9Mlz_*mepD`Lbop#P|+4DY`(>m8c)^=Bo7+KeT=}|Hu?3-UU$P@s;_*_#p`+z -kdBDP`el7K=k$S%fPopb{3yJd9t9pvAO2@b))p-%Da^W9`8Nh{R4mEKguTi<0tqMF>f3d6(#0hfy10R -a|GX^4-_AXA_K}CvLX-EPt>HRr_Ur=&LJBKA9iER|3_o0uHY5%e&wuLv&!J(!5saB2@^Q#0{%7Xmu>z -6@LiEZt!0B|IDOMHvZ`7s?6~N25dX<8*{mkLCmGYuYvYq??*r#KrbL4Z~-r%x1l -qE=dNA5o@DptqY&+X`SRsS>({T3SH|3^XW%e%=1lJC>B&cr9xZTF=KU1^5Yj<)cjKACt+H26&j0N1qR!1K+~bsZ)iI8SMgTaexQP953u;_*7uG6Adr@iL!0w*Pp|(pOZj;q}S_(Z -os>$%Df`#3%my1(8JPg%7Dw_J{_@K&fAGpHuT+ta`q##d8=rdZwb$ -zCowwh9`wGFxdh^XU#dAvI^y;gxMy*@7Zs$Aiyd&yU>BCUZ)TY0(&kuiEX=y3uS#J@Hb0mM)&<-&+hP -=K@W01ISrT3sh$-aK^f&jHm$&a*LIgmwg6mG3)_e6oSB%93ou8S>Z(<425NF;>7>#hb=TYX~p+S}`UE?vQ_!`^KUuVdGq- -G1!aOBp4WrW9)^oAI5zccly(~6E+NND&P&hg#%+4*o*H!J4e8~7#MSmAUsfilO|0Pes1V6@ZsFv^?Ymo5HX&F?GC#g;|7eK7V3tJGUCp1zKQ -tq;)^c|dl2u4z^{h7guK+Z#gCM37Iy!_88(|6#1?~p)Gr9V1)m(=A5i*I99tHJ@Sn~d7yQH+sxVcQZ` -&0HvIkucIYnKL8#hky6nYwUd1m)3{JkZ^#rvOYb4T*eK7JkfiihHT^FvPqZ@>XB;41=6=(91OguEDMqwn6kcW<4%I2O6S9p+Af -Im&_EB`9fJ<)FrM6?0`8jk{Rw2`jFtu!63*!+L8D4C~@z2B0#2IkC|rOQal>6ei@L!XtCm15{OBP$~?DJP@<+yUJ+8A+ -L`DY_g(yiT8!nw2@E`+)x5-9uV5)o6kZdSi~*S>zs}`AGRFXO3=$QJ-py>5Qm>Yrt -4nGUsN^*6F)zjHzKWusOnz?kP#>IlAs4!CsAIt}WcF@zR66> -fjpeWh0vIgS`}fhQx-&hDAg~jUGO6qI~h6`@ioVoiLP*vv)J^`@Fk)_wx4i)_TW!&+vZ5d$;$O-ZkDX -10ERg>ws|sXAgX9;P!$02mU(n_P~}t5k8ZAbUurGEI#XfcKUqjbJ<7j+s4<=cckwu-$LJKd|&b1>ATP -OJKu}G|MI=*+hkD7L7GAR2l)(oV$chN4h-_~3-L?zEA%_#ciFG?;E2I7gU1g}9b7p0#lgo0R}FUYAL+ -ly|A7Ble|^BS0q+Ow3D5@q8dx2u3UUkT8}xF}>p>p}eH!#x(CMHbf+~VsgIfjn4jvdhDmWoHF*qyO82 -otfuHXZ~SAwg8-G{UrGIU7!kSjw}A&-TaLRN>o8nQPO=Ef$XIm7!UqH>G(rvnlO_4FI!S3LMZD)qwv| -3Gcv*ubfQ#=ygYt%EJWn+e{&;1j{;f-eXE8tgQrIYFH^WX6z;Av=e7hxmmAhYSrF5i&YtLdcYm86jyQ -b3$@M@KEbW}6%w%$SBncfS%ANPLK`x|eo_r`%=53Cy4)+g8JMV}o$`+aWvwDk4&jq;86o$8 -z6o9A2Ld&2hz-?BkR27NcElV5i~FTX*4dcPHZTm1I={o>bh@btmK{t^CD{O9=}^8d|W70^5|fM|{moD -!H5xH#~Mz&8W81b!BHCh%h5&w(t+BPcoO!=RHv7lJwjcMbL+UJs^WdRXw-;C$lsir_DUPX)Ib(qYK|Y -wzlRsw~6saiXDOp=se~gjlGk&&T_|&-*@~I1$lkqSIs((;4c~K%l9yi3=Fv7dkPfXs9Q5By=p%%*c?P -8Jap>T8_-&M}}!-Y0lD!W%t1!`wtrTFK6e6_dL&a-Pd*9=bU$Y-dHtKB`U0p3aEYRfMPlYI9Z_mz{Mu -LLkED1L;CYhE?RV}Zqq*l8y&irdDaXvBg`aVqR52INplbIcbM=^ta!lRAB_e@oq=IZC0TLo7$y -L&gM$%|nMBk;G=uR4_^U}Q#zrg#57!fCs@We#%wkQ$hVy`$Lj)RJa$r*AX@OWPewO^f9T2Iw; -b*^5ow*YSkfv+F+UHymtM|U^lJ#cnQ0!F@SzTy;WG%zLgnZ>oGmpTm$T0~>kMHf?i?@2YxM^41pX#p$%}XeuZDFW@HjC~ED^93);DS&MeSx2^#J!C&QM4HGga+kD|!E^*2P -Z!Z#x{4kKPn?FmK4T4muWhre*P(-3tZmj_D<9OK$#U5y7GNK<6VRoRZaW_;W{E;kEo#Iu(0C7dNG7TZ -wNnk$P)?9H{l;f;au^d2o#B;&|BcxDzqB~Q7x) -N=Rm!qFv1pgvBXJ$DHW&Tt)S!p4&quILxz)PKy#lwB9XK&9Zp}OiPWKzrqDF{8l6X%0=fcPLN|irZ_o -}}1WMd!AGF);_nbG_73OodyQOlgtd&RQ+0LYTP9-R=(jcw!REeroHL5|KQQ>BXpIqGyc6J=nBn@Zb^> -~l9-5$+y*mic5?eu!dzL2W}h^-Lg)ekUz*5)MRy8dEnj-eJ9+jz!UuqgnVknM@Sj{l%9h$8 -f%@gI;?nmkKNN5J+nIJP -|wj8G{m87PqO!dC{P#sanRgP}f-|0v**8CmD6!Q6GCqI2rGRlNqy$T6)9*-dzq=w|uGCOGhX?KTQKFlb?nM%Yg86$x#eSuoG=$TefRUJIPMAQ|&anz$@T|;NMcdmY4I*ypmUelLI`+Yk3{7=ZC@7 -jXcDg`6+&upXV3(CEm)f!^F4^IPU?{M}XBQB4B1miD*C^1DInW{Vd@E>LkFO3dqv|dj_DN5BUASKpt> -V04x*%4{L#m&7x9Nfj0vpC~8HWs27J}7Bz~HXcni$S#e%mgvsQ}RGA_DvOrdY`s-j~AC?WWQHE5ra$! -Cv>13GDX*wMeCj+LmUuWw)y&UFrp)S&;dM(VRp#IMv3|-Y#UDZ`x)m8t~`WH}30|XQR000O8B@~!Wrh -HB4;JyF=0E7Vm8vpnYkpvME0-1zY0`dr;0*bPS -0~kP)@XGnt+WX98c-VWt@86FfnRE7g@3q%nuf6s@We+SD%z_|T;7`{Dp+VsPJi`C}uN(dbjafBF_#l1 -Dl?|rAmMia_^V0>+`BhInUiHY6&WcBV_OquH=T9DURt10N{OQk}-n*tdpM2`k$F9%L&TtuZbUrS>{Br -75(fD7Qye9ezJb(4ONc2PYd@*{MJsYBbfaf2c{V@7#miOvt8;h?q@~<|YAF<~>KdqR9^LS{;aAZ&-g|M0jaKY$-Q2Zc#6-ctm@ia)}@lSKmX>?=+1=BbGnia#z5U+^ltnr9Lw; -mdN9@YiYZtTqX&Gm?R95*~#PI8O<}=>Nl<43rauf2DkX^7YDNixqg*ZZkTBW4BzG$0-Q2udjOa5#sxQem`THU9|L=dHb}_ULo&oZyKRn%D7B0;#3r}@UzhAC7Dg!KKVm&yCR?}94+SO!&unu6EKp?%67a>TLW94 -2kj9TvY-aoO*e)uT>gjs5^?C1+%EqHte;v!`s`9<#@zuxT6hs@sM -b6TfN6jzZPT6EukcVH{8#} -U4V{!)^_>+2@#SW^xL?<00(l}{S4!b_I$<;dr>uYu9mQV)72Q%~%{j;x(j`$o=PPU?yQyh3jsc -4;ahW8tTWYWXIQ(P_-8Y;)WztD0tGf#aItKc20NcpHgmFuu?h4(Xe_l49jXe)={0Invg>0gs!Ms&y5| --525R~s8XqAlOt$Im|pX>&x(I2J?NFQ9dqd?GaAMGJr=dOvxv$WIo)>oc;Sd?`IFJtWPNX3lEa%yDbReTY~Efb8T%iE;Au-=-)+FAg>TEtv!DIm%NXE=_}!NpWgE7+ -W4kU)@h(G)<{(7xb3EUdYB^#O8e^%AQ-II<9N*EJi!B0|;lzF`bD!B)-68fEp+wk&*`>6+HNUmn#YkJ -|vaUGVTjIMj>Qkw+$bWpY|~n4o@H9VfsVEI`h(#v~XWX$z1|GT99!Ca90PoUkH&ke75pt692Z1iY(#s -iOCDTukMBElq>=Jh+zBqZ2~y!678O%5MT{q}U3#>yE$e14)E+R$V`##aM4kT)8vk$UIjykS3(?Kv!-b -WsoM{eCT4~_QH1E@!CF|P~QQmak;?h4?ttus&p3E#K@HHcx)fSN*P?U#5E1G1=AQQ(NX}U5y$>MfZG> -mbiJmmTPR4bE?$l9*teIh?;Vhpw9yYCbK!QJ!TCTA=g$(?aaSmc?Il#jk&Uh -hx}m-QnMBAe*`oI=#tAC`VkMraM02bxz_P4?=~I2=Q|vegFsO8HDzJq_IfUWpV^cUEhb5ozNYh?Zc&Z -Ncz}j%r#7^SFE3Es{~ZauT)QuRmMwXCyfnZYhwhIJ=u#5Pqo2%+!rqs+>DGV({KJvCjGGvfpu*UWRx1 -RSjAiE?#wu`iS=S#0n&=JdvzZ|{MlTwUc@E`V`EF8F@{c1v6+`S-;1DYr4h`oMpKOJS(v?y_l?SAf2F -thYGs62pNa7|@%Ri`ZApQIalykz!f2MD*ebo@ahNER4m$H}obqn$W$PVUlSv&=xgfUj`bP -m{y57{LK@G7m72M3I>yb$uA<0j%J1nC&?*+4F&^E_QO3iVNM$7W%B=2^nO*8BsE@8))cQfN^DUkz$S9 -)vu+A^&PU>WD)DP;lfB`MEKAeh8WO`^gv{y`8!zRV*`;nDcTUS3%k3`&bm9uuWm?@#Co5A9X=F@+_d8 -slkhR}n!;`Oj{bv2t8tD6yWu -EC;BAumG#gz-2xOi0DvwE097goM6$7;0c2^Pz#x!Rac)fS%|yKIpdKJ>n~|OYpQA(1^S~wlDl5e?wM- -tD-Pu3SF$uxJa&xo2vbR~+gDIz4>x1mB-t}g5({Jiozq7Sa7ou!cuXQ2WRUA$X#xNbr+1;nK;b53xFOkwdP9oYx1((y57R0Z>_nT;ZgfA -$s}p!X|ADh}S+?*jz>oF{!2xVLOPuMv)ejoibQfn*r@ih<4vVM9?CW=9a>y7NbpQERtQ -a3yI6$KVkE(AiEf~3tMT^9S<0hAR_CXM$MZOWobQdW|Rxw-p=?}BhsDN^)3wX_p#&fo#&@zSF>6H$~2 -X|ZlbSU!Jo&k;?K{GXO)W!F4$tshZV~1jSD6+26;bciDwW}gbhCh`_^KU-TSMKU97Y4?8x0&0jOm-z+ -$LOG6Bp`a$CG)EV<<7q_VPFGWi}LTx5>|x?lFq@J^q&j&ma%{8bq7NH!Gf2 -C4MOwV0N71ItoU01s`@zV(2c93Kt^%?4W2r -Lr7GJvq9Yv+kRC=60)%3?NLIhGF^y4n_gBrQ{_&8tI4zi>eT@+h%S6;2KV;>*=yjMFPSb&`WH(L2nn! -LkwCU^>ESv%1fcsR?8gNr(b%310VNa0xvmnu?(HHGCn3Cb2uqo*S6s7e{C3EEPDC;MvlR?ySU+9n`a_ -G!=0t*ct3SJep9vsJk!V~UyA_3{3EsR`u=#G^gh?Xz~^KnHlM%g~DJi&b6WNnXC_9O9wcbGO)th^ru= -E8Q6mQAs#ENL?nSs{w7ixp!`Vr}gP6oZv8|5~P3HR?#epb|QimqYBmndJ?TW`IxijerrLMRa#C0ctpX -J&%D7_+kf>Hm5)ssKteA7-V0MfSQwsnkOa%!%@W$p*ZNwJihuf={@iUq|Otzp;_R}@LGw`1EFlb{SE)omy#T7LtoOwdN>7hyWEe(UoiLAWR`wJD=yQfeo4EL- -+G%$81{zJb+k!=xToLrO45WqDFsOLI*i4cSvpiO-pm`*>M7i!F-6Lw>*)#y=#oz6`N?Ba@rjgCpr9qZ -16|IMD6}Sj*4F-e$?$m4m4B0KMKu&%k5^00o@B;)vKO7*N%l>xsGEV5LpEl%Y7Y9EX0V -^K{`6bEE|i{hyS8Qfjz?owK{ABy#ZP&9DT-8TcHuNPTBtnZ@Hn^7y|Vk(x(z*1H$wHag#vA!k0*;@n} -4Tu>qO9kqm5hmxk$_bLE!E(qF&|MHqXSQ(OTnG$jdw}dIwmxwKKzI?rK>39+{!s(tA8CxA>!mpUK>pr -Ru|8`k=o)NPr`f2QoghsPsZ`IfxHBwnozt(bOY!S%*ynca^G0R8SierJZ_jT7#KkH>DWfxxnZ@WV38bN>IsA&1HHgnm5HMM2GHqYvG%w(SS)tA<}{jc7TqXEBvE -!ZFUpV`x^?zXl+^>>^i}AcoPPjibl@<$a!oXCGt8MblpaB%4HO+&U{v_*Gh7N$yJkjdHlzpb^uyfv#K -dlZ`VbNZ2ml}k6dpwx6}S+6xrj~pkN`?!yATD26`|Qi%c4)_CV(PG?&_{7#Y@vQSU -(YC3;-V!@vu7Dw^LOka&NwTET_5LUaG2nsVa2+4-0q`k+#`G-*&LUf -lH$fKlu3V>_{~5rav;g>H6L_3mJK`tN*hrKl68v#K@W;iuf$(f6*RcHL1fxS|0^}ef%WOoJ2N5f9c7k -JN^^C4$1jxF=?Q|3HCwjdjfk7YTz{v>b?VU>(?Im!TnTA5CXY1WmabU8x6<7fM8(M0fFuAy3qUHd&dD`FV -!K#xk+rLs!72bN)WbGhyrfxotZhYk3mfm`Zx-t}jcbkz6v#Jyh5O1#Cn#%WOwzh3Co%AKLz%Mq -YeP^tgarI1Ke6F+qP7!6hDu-Y2qo8tI+@qq1WX9nqgSj?sq{fzM}f?aL|Z{ZY1;z&nwEcYIn(9J1z9< -G8xic#&`vf2!+@FoaE4#>wlMa3d3nfSPfmm7!|^^#(~NW?kUZ+DVdlV*dFOq%;XuD9I^fx#PmC$b=I;46u)SGZ^V1k1y({ -d90LBAGvdoguR-7Kxc=k%?;cn$af640zB@QEVYd!u6Q+%%LIy5ASYv6>GrrnF5XJ2fhU9Xw&6#N*Qm# -b$m;|yy1qOR>qH?VF3H87?g~!5>N9{9#SpF8QWpW-!DOCTK53Oq>p+7-#Je~pPEIcnQoonhm&FZfjcEw0d9bdQ7w&`Mhk7_Dhtg@{fDroWAi) -2Ha`+15?q)eWhP0H04slD`E+p@}nOa0qWV;LP(&7{(v$IXBQn>U;pFM%Lt5g)nbLe-25`y;;cqM=^z$ -yG>2Nqp?J%iFP)>I&THSgg@J2Qwm7-s?ffoa>IM*NYnn}5SblI%Kahi=fj99lIffhamukr~x)?~!s<< -rdmCnIS3Zqd&WYqwB^JrceLQ&*sqz)o7sPPMv@gl|divMxInLK~O7ljg?k#5#A=PVJMu0usQTdcA_7P -vT>zqS3y5KN~-!uuAp_(>r;&teUlzv(eFc)wkv5x8||Po!=IR?5`;_L(YF?)sE^7XV8*AjThS(7oOcS -IiluO$cB5Qj9oqptH%DuV&|N5aq%jkX`^9o@aDU-GwE@MKK?~E1U+ojn2;CC?z3X4tO=UEjpRsDmmLV -p=1mf})5YvcmJM@DQafNK7rt8!(ES~i*M%GiHWj3NC4h<)=zK&UUfi$|D+DjOSGa5%>!WYMJA8^82wv -@uR!czL+F81z&+UURTH1aZI%}U5i|1i~v!u?D721GG_Pp>?RdWw<;llT(mI}H=t<8{q5iS-~DOO5EI; -}v}y1EdKhrw;nV7y=tDO0cZ4*cdGK1NhNKl0J1cgM<*YLCRo=+)S^A-{m;@zjra)N6mVi32s+JoA@_( -@m=27Jqyd^P#~n%#QfBk5>^*M_0=HGGfZ~E8$x*MXh#AvC$B?< -94(&SlHUSt<*I-@`$YB9o6JE3mKy|QaUU(aKh-6ahVjTcFH3a=PjG)>C1jBkGMiJ{We1xrM?HHm(@0r{q8GLZM4{Jn=DuK+9URNY8+PS~ -6@flO%Y)C+@<8d8=_#8k%A@>Z?9x5XQ~|fSFIvgEoJ7ZmvJPz={4n7uZ6}$kVnmawCx}e)4otS$=OBI -T#>EWD;9ZDTa<>?ED+`x)g8J{iSyXF~#G3*BGox9gv@hW97N1`MSxMC~oq!P&{N~*OB%E7}9WgJ!iT(<*gBqc>*l{HFC@dB0xPKHwtSuvwv+$U&@K{ --XbcIuK7j7>ezVIece{Dp9u{Bkt5feNXX6YbuH{0FA7vySRf%t81B7vwKXlE*!7tPYlUJ^sN$>ekQ%? -oD8dW+orrA0RHC#9hDbypR0s4ckB7doa`z3zPFMtpJ4J*P}i-9m5#;^N}S?WT8467*g60%Xg~QAzKNc -fIPv1}>~KuayS@uyf(LAlYSwudj%;o-U$#O-e*?_u+^X!=T3{O0C4NpvK7>T<=3z^?U$7xKzfo%6|MDSLM+rz1cht4n^EjaX -!*KfROL*HgMf{<MapnGF*U)wS6P`JSek-PPiDZ -CY>D(sncbRgZ8{WHx29ly66xgb0%C0o#D=zuwcjFlt#4CLPk-)dCEvCa=Y|hpVSk#n4E=#onmU1oZev(8?gK*as9K-$&ZSRXu~Wd<^BW( -D6Z9o{=*Pa*CQLt~#8xz$%elngt?~TqVSi1*-L-!DkJwD$4+*0cG18(I3Uk=;0^y%X^uo75X(Zf3&0~ -XRcJ;C1Pk1Pf1kZSk5Z23S4$ejj=sBoXrgA#t`jJ$g-4Wu^Cyuq=_tc$TC>Vhb&brOPZ0TiDeN(8N9J -Ukj;KUSG>iAV(2K_Yzm-RX)~xdK)unq2%Wg-;pu!FdmxK$%@%>k1B(m?bHl#T-f++s2nV5|4e+qT-vn --S0W~PG(^POF&AUJ+DJa|*nQEecM^Uj*N28~^J4iHrnIKRf98nJ- -I?P%vc~Y<-FQwUu2KmWv7d++3w*txU-I;FQ1}bwUR$xJd}AGl=%5b)dhC{sOcxVNiSr&pJ&Ig219eYR*W~pa&kfJxy9X_lff`gX}H -`9rqqow=zE`$!SYLHD96d;?P`7pQRP2l8n)o_sDq=;;yzegtL`0aZ}9AXIYFQDI3-@mM19CH96F+h;L -nI?eu9zUu=Cv}JR9$FGM#clgr5L{OVP|OX4??vC~i_I)-o@Iibqs*22A)U$N>d_*ZIdvwfmWu|Aje$t -^e|?7p-cPo9BOaSOL&?{h{N_%zZ(`NSh;&Tt8@wpBAsZ#qnr1H)Y+%Nd(}N`O_NEs3Rf%|;EgTkfT$< -sl1zhm$9!>z8?0hc_)6Gfgzlx{lu=K=U@&43-JaZFyKvQo`MLcs%?=jps0+DHUm@qeVNeZVVHc2n`$E -i8m1#?+e*i?r8k^qPR%9!+t*O{b^X+|k1;zcAv{RJSLi@pbTf3(?Y{mlr?X5?8ukeSziA+5Et(-8l%8{&U7~t`A)K0Ph(?#9!^t()Cz6u0RHELJSx|XzaAKC%i2U)_c -H_}Xy`C3471q!krCQsXUIosD-7t^8gCO|(3kji`+1l3XMGd2?%B|#(~{w|Zv=`#lqACQ)`*kyfda0e7 -R`z@wTWh5;V)PP{B?l}A}!N}7Q{POY!Ex7^&N$W~1Y;FgN)&{8mDi*s4M%)>+V$T}xC0s(7YBZd_lO+ -g|JM*YFBd$b0@G08Y&&p<4h!bZ)#eQ|kc@PpUq*Z&maRO$!dU9gFU!+yQh~j&jS~=5zX{Eqz4ZLV@qC -uY1{>2>(kP$B04|?#A2Fmtm+5Yg%^MaO2XF}3)^h@{_3vK)-AfI -|>Jf`_y?P{78K!SkZlcG|A|RRk_zQT|Jg^)jY>#B$0*}~>O#c~wE04Mv9@-KJU&X^?Fg%a$NJ(sT+t6 -%Z#jKOdnI(v93{+S@Y7>OwA6t~6sLwGq(Kp@UJzVh#cV0)S^o&A%-6XzT%Rj!thGPGgKf#=32CHW?6<{G2B`pBhFhRR6K-M5H3 -NZJ6^>6qtK+fP$FIkD=NA#EYi%`(4Ui4_d#A1^DSrnG)Hsp20x)a{bZSHMLuazq{veooUHgxsTHk+WY -LoPtF-+@fpC-h^~slyf*^ehc}wh)AtwI~I;_Xo4dZ?P@XA?Z6cE7=4O_kLv%b~zooEQWG%TQ)tP -h(jn*#vtGu|v(qQcPaN3k#ljk(QzVVa6c;`R^fZ#aBn=HfSG>cCwjEvXJJDd(V7TiWD&S@y&ieT0-r+ -nFaGPYmcM{o*u4L}Ac%)KDvVV61L!E%9Ebk_W9agxVb~CwSA;zgwZF<{x`*eI3@8iYev#8{wX|%+SEtQ-(4a{XE)beYl)AzwrxlI6Z1ahpkCuTK=x# -eu4o;`ea;|6O3t~+-EH#yYHjI*=RCdQ^`|C?x|+4BwGCYRata%H9XVqZLb2s$8a37-QNFWybG;H|u6OGP;kMP~9uo2m{P8wN- -cIv#^<;7`cKcUd<9e#~SIF9US8?+-SLQzY_VehNQ5!YgAKjm)>UZ-~sv*$B%9j`n`(;LUP;y}zE^U0u -K04(sg})w3TYB(4(MEGZkEJ)PmWKlFHgNA5)g8Ipp#xf?#>Fhw^wBl$!1;`fO!)oy&3rN1fP3T)fdV^ckw -9x7gjgJTfz*gc=Fu^9`5VcnE|cX9KRN=OK^~bm;L+%gE)c1=J4subC5!(&&1(qhvgqx@HK!j~lnd=?W -|I(^W($z0?s)hwTt}a%mgr_Q*pEQ&XzgLT%_cNey%#lOo!VO%NacY`_Kz>tSLMi$N6RF(#>C|OOZvIge1toqv9rWlWtJH)}wDJtapXc~A*+m|SKlt~24GEPW# -+Tuy1_z61#$q8V~z#bRz0tL8?r*AK*vV)L0+c7pgV#xckr{sj+So48?dl8Jd6;wjQP6Ck19>~5+0Zkg -A0U#r(O--!;P_i5LNEAKaX#g$E7_qs*Hr2I~aGyyra;K5VVdro=>9^Y1@Da+Don}VN^+q0O745xZN?u -3%tEnc#T?}Uct1ELsS_uT07=2T{*^9YcQ+?T~p5JY_Q@k9aJTMMuguB4@~9lZ_c^Zv_Z56y=fFDxOk- -6o?@8S?GOjQIhcQ_hDK_y_EX42pXozg6lno0RnGbo(-I#+TY42&O3Z>U5{H%xn9yt_FdnrM1@ZZB@P5 -kLMQkO~E$pHpI<4^8gO7OA(J#?HjSk2Gv9+JGCqcLxZ3eK;Db$(AOcLKdFk!Br@Ny+XXlBrCZJIcDb- -@VG%q7OZdxKEd$~#{q9@lychS*->LyJgErZKf}wzWU$3HssmpH`ZO&IrU>bp#8&z{(QqqbvmT2|5NtO5Vc9$C35z17Uosldj&H5KR*P5KOl7pss`V(C -yR+J&HvDO_cUKa%JmODM!Em!lYXU_+RM6II}ITu1sZHIdHTrdYMh|Z{0Fw%&}q -J5O`?S=lTe`1v|9l`S;gJrJatIIuwtv~f*`dLDVJneo{ed+%i%OrYLG|H#+h*8(>)J5lF(&4}Esd%rEGGaUgXH{fI4r^gAP?^Sp4Q4{`}3s@1S -ETMY+LGuyLt>}_Jzv}O*v^XwX3s{L-uoo{^f!ND`52kQT -@mlKc0Xy8igJ}3i+*fUt`k=d7FS9pcjuxJu)u$iCoMBe<{gsFgt2Aj&F;>HN73c7Xhg8XuQu7r%%A%6 -j|M*ixYc}vL&Kgbw=V{w@GFbk;sKvgF!tSEaIp6Sm^ -d-x2Ia)Tlbc6d6F)B3#A)Y{dlttC;gNPuz7o`7F%E{$J#@q;ApaX!Ob=-PC>#pMPxNYU7%7c<%jxyVS -x(xke|71e6~aD%6?G_Y=l_e#_Tso7BLvd{_+gI -$D6#GuEXJxniSO@11I|l)|<0oO?hCTc>?Nc4KuOEt1^^ij;&xh%j;nm>Fu9^* -S?vhqnV7@rh8$7tb6Cu)zrtSIa7fwz`YJx8J2v8w@?K|&X6Uo^h^}k-Adm&G0yx}%5Ho(NpxXhx0`k( -&4#(|*nzays_YIz3{kOhtAQP!2kr8MW&S^x&m!<>WN3DxSaKdD$1KqkwCN*{wR|U=E-NW(_8KTxQYLMSSH}JS=CLUbLgIEoHi@%pMMa*hD#7#&e)kXvZ_RCbt`clsNg_$GIl&|4 -{wr=?n0X?N#86p#rc|zh?taJwEQ=jWY34z>YOG(0X{&_oC~T{`ry(Kg0Sf;_{-uao4F>sphHgK^!O9afmZuTGlA&MQ>9+r-+p7%a;U&^K72S -qIc-a)jX3V*Lz|=hjxDG{=tARWAj2!g?#~Hl!1=ew{X7tS_^#;zu1eE*Q@wT>AA^re36q^**~+f;j+b -5Fx~s7Mpe*nVJKVu2W+6G(2VklB>_orQ*3}CJI17#R9*VHp*?>>iTK3F0-G6DKL~*Mq|I{JSpUf`AIjwH>eDW8hTsc0TBnf7>~e<2SR%o4UT=q&9-7~JA2tK);2N5* -SWBa(t{|3-);h`V)COiGzyS&0^Fx>0^h18yZIE=p-|J;ADl$uM>F*A*t8p4+ -G&XMW5~opF%VjEaLuIYrF}i>?j>N-7!Cd+PuQymoryax{4ySfasOjkmt!XS*9=+ut&KaIJ(lY79gNX@ -Tb%0Ia_nc}dz^z!^V~=mVPvE=l^!5J49y?#3J{t_zEpZU(P9N&-v=DU|ZfoA}w=E@b_Jm -Y~i9-AjVUl{9JxVrR^?3sSz9`gtnoY`x@H4iaX1iD$X5_ilt{&`{!{+eRTvD0~Y3hM*)V7pxY3`eNt} -u85-Lw;Nt_k$cE}-`x^U!Pv1v5cLoAm&*NHL-8{Er2pw=@^MXT;hF%ANT#nQqUY3nXQ-2^t}#F1;;WY -6~5Ec7&H~fZ|8WagR#7Uu}0n4`R0ghT*BbacY0Ras|6Tt`+ag1IvABR|#aSG3G$6|f7B&-Dm>*f#P|m{e7gulC*Qw_akTb#` -8<|i1`JqEX%PuK4?xE&fH=*qpw?P4h5(zo_9hIjwWWQMmmnc)r8hpyE?#^*iUpUm;*@VbLPqz~=o9Pe -7-c-KJ28#v3GvNwt4-Lf|^_t8BGmY00#Lh?O7m*^3v`BgvP?n%^A1Fx~6cwSPwao;toQ$Q>*)!0(Cld+g%7m8Xap}k_rnqEszn!$vzBFJ{Q*80OTNnv>4Ylbhxz7h5# -sVj8Tp*f+fv;GgAB1cObfHHz>5wBgeSzD%6B;w>gac6!p7o;!0+A4vSh2?i;EH>}r!eeM+wSxJ@v -id*Qtc$o%X~oSvR^_?<1_a=D^eB3)760o+Iq&pLz?ID-BSR$ZYUE}~1j;*2G>0o0g69gwu{ukhx!9>* -Kt)K^p4W@~`-1tQte|I&4Tvu;(t9y67^c-r}pNs#CElrQt%Xj@?tYv)1UnzOi`7Y?VTP%|_zx+^}fv4 ->L;SclQe`PNjNN-^{!=Crq6+2&m~@5Umj0gX<+lw(roc}ZNe#as8}Y`tB{0h*afT6#NIn3NUjm;C{9Q -k!jrU)`oj?k-83+y%_3oL!8{{C-B!H+IK2C};zFWtTA>08txAOyPERUUz{X%`e&AucBvd*Hlu=h6F6W -z!MMn$u6k@w@Z7x1wdPCabtQ}pdekk1(cPSP+@}M{1SEdx|Sr3w;7GMhxRFDbs33r=Do7M4vO?)kz%R(yeW8nQkh$@OdgaOF;IpVI1NRy%q{V02}H_1^`Z$l>qd -6xgYMA6+^kuLmhOrfp8^r8L#JQJAc^nLZ8u;9&KK<#~44Gy|mti1_cdaKW;h_x@KppU+iEm -hx|5lpKN6xb}9wffNl+gX3bdO!KRj2!oyTY)T7b|`2R8e~_aeM(y96DExmYmpL4x2#vP*`|wM-w@l(x -UG-MdRxFv7Yz4Dd?q|^oQikzY0m}-jp1sAk@dw#1~k1Khi04!(l;1mObhdy6LrTQUSMyP#Y;a`9)WRX -!?=l8u2M5wj?x4N!~xZC6ARdg*MF7}}P?E?p99(YZs`H}#mSLFCd~V($ -vZUX3QIXDrVRhu&2UfeiXOaiw{3(Q09~WD=A^;>yg);+k3$hS+J!;I@ -#Q%&AJdAZfL3w@LD~SvSjcwa~viZ>%Fm9J!1}D5_=2Ql;5Qd1^D#tH6T>6o#x>wc;BG_d2*VR|M@oRC -7a4fD>9$L?d;CN0Kd2PU8D(f1{|96^2JE%W}~Ytp3$Xv@Vd=qzOH -y%h^otcdy<01@v?RI7Tlr*5R2`J@>qmYAvH-R;Ov{SL?fT8Y9i -f2Nv&H98kk-at7O%57NI3{OYQqE~8kD)eQ*3_F1@8C&ruS^WBziw3E!vND$9F37SS!%$Q`_R4h}yQ}E -1p-0N#{}Ec`|4r`?U`NKe8bycHMcj_G=^Q{%wrMGz=8=ZQ~M%y1sni&XxyfCEwl6&K@UU5hEdcJ0e${ -R(0f!%eLXR6W%kFs5XYDatik$Z~Ozixl6WpBX9gQLxIBq(uwx -tF>F(L=IQU4G4i&#KkgcFIA(ka1GG~%QCi4kqQrOQDy`g72xc$jYq|wQK0Rys|9}gXX{Cft3Uv|sLeY -}!uItNSwA9kQgK%0ar&k))DNmVwxG;fiC>+v{3PlTJ<0~zeRe?>jdjG$0&CcrflJTuC&}akK -4vvX*->2F-_#x1&jVuPd7H=+ZDb2wlY`CxJ!nFRI|a{8VNMb`=rU&&G68s7!r5nzqs6y#flpS~rOWCa -y?SMIvMywJ!&PQ@yFtikoqQvPrg-bBN@Zf*i3QYk<#ObXUS1745pN*b|AK@{Z?WR~iCFtXMoI7hFfw4 -=qzqIzKSF^d5Se2sGk;Z_D+Y4io5aWqYy>4Q95;H8elRIH+`c`BI3C2-B6X~paGH5%*x_d#ZeAeCk!i -fc)jzqR`hrduWF&Ps?b6P+6Nj0NB5HGgvZPK+C3&Zg;Srwx(7tDv_|-`+U73TAe&rFV$0wMS2k>DF4) -T*Mm+tsW4fFo-$razAz79p1m%L0is9zcS{4vnyuWR-biwke}LiUHJbLxCH-LblwizPorn&ivUqrlDm> -Ux{|?2=j^`5Gv=OiG?QhW(r5*6}jWN$!3nqdMTWRmVa|$0J^s-s*MRYI?gG@aFAN?P~LP$Vt6zx+C%& -_JlH*pE87^%$)s>vsM+ixDr&z>)dKz3KB85&7)}?=ZT#4tJEi?kp{vWtXECv>D< -FxN=EfFV~!9mXQ}_yqX~UougRv7uBc&qEi9lCs#A)Ce+O2yG@eyqKZBI4Amxj`-9xv5wz2e%?UFQtjv -47q=Q|+!iF@i8f_hz51{(TXStI!j4>Uk=58pbZDtz`Epo)+gnk((Z)M};ZS|uz+xhra4HxPbGr4XN*D -MT*=_Own6f>8WL-nc?=uDz}v0tHN+`f2$yX5bhAS{?Gll=kmjlcOkkjb0=1W}+d$)GU-sXGotGM5oaA -CR%l4{!o_yvLhs&_9XnfP{VQS`uv--MDFh{%v9na{_qFgNy)q!1-`#u9n7(bX%Ee3eVi -(job**aOQ>Xl>`3nKsPhEQTB(}-N(6WqZMb_iTCGtpc4+Xzuk@Zu(i==LAgiWa9}g%a(=hWis~h?!k$ -7sa;J&Dx<%KQ7w(hP4jmnG+<7;j17wY$)gVyuB~Y_8K#V)oNe2XF&K)G@K+oh{Re12iUzJg%X~07(#!92dUgqWt`p$&AYTwzRiSl^_OS`CeQZ`**|l3u`OVrCFWDL -((N!ipJ90RI-Qq%@eUQnBSJSWB;=S|~&2xd?c`w^t^SV5SA5E+j?Wj%tESX=Q=SkX>Vn<~Yr!S^G+`@ -eY+XG~qw>bN`PX!^kn8V|lL`&4xX24_MmdgY@0{PoPyt53sbl4e}4m)w%r7`Rc@nIWB`qoc~d70_9S* -*9qY|O1KxOk@-ON8?K+fS-X&y=?t5955`%3UuEL`t<*k>MQmd5YY|1vVg#2obHiZ)6!xl?P -{I&3qS5mrO3lQ%QTV;nkfZKAKDg->;tMCO~6>*b2$3>GL>yv>COEX9n2WG7xVl>If^EWI100lA`8j*K -p4pCI@EDvoVp?v*sO0?+-7-@6GlxvQgAYo8GJtoZ)Dt^t2tyz$*h`_{9I_p)a~Ewqr=I1S!oJ^ce`bT -0Dq)E##|&D{FABX6Lxcnk=Yh(5)+!QS}G@%$*f$rThw>B|s|qhQM50K0k*M>z&Rje?RzhweDFSQt3sa ->+GY7o5vG4P2&SI_Jj9kAWWUJ|qZ@2vAiw1=BsX4d|;A%y6F$=Jj}Ho0Q=+Jl!~{fpnRs -ph|PTL;=J3CD$v<4m;P-%XOYjYPjKuS&aoeJ=Y97jaO_!lbdsEsnPM~kI7F$3fn)3CAjN$P9J`2d?3< -zOkuZ|w7~A750*;*tQBii+nl^}Dk*FVd4kY|`I=j$m6diAL;CxGhM=xthG9I(Ds>q)?k3@z8d{P+;FE -(YgO#F7*|KGY^Q<2+ymbrd`))_9f)X%cbNzIC&7g8&}Ds)K~_6WAJ_;&Y+-x+7{H4c96T&8R_t=*`6gyaYPO^7eKa0+ -w=J+hy4lZmS;bnsY&}3E8@rO(XF@{)Xvx`uu-_&{Jm=W$tev;nDfaOD_)wp=*u+o|nvM! -s>niJny3QU52U7avnlsq!lEXE{F9|{u`3O(#1;wa}PrSU1Io{V4gT&etk46NEy~Lt_jnP>=8n?@|U(^ -);CMgc(a;>taIFb~HUz*bHsVV+dQXJY%wOeb7|05|5@95C3tttL6WgrgiBigW<;twDWcqVp2^sWoDH1 -Zq#WmjHFdS{r8M9V-x5x)guIvY)3zL5xkrq_rM+bdU44;r`V0R@3RA1d9G8;x96P@64-sR=e#7 -iO2CiPUcTD3a9%6NSu^l5nrHw7@27{u@(*|Gl-})&LF$vJg|Y(hc{qNgaA|21xbP_9&x3yv@6f -<9D@PK?LTJ6tynvPkv*29qa)fO#p+jGF36eszg_CdcoaWk#ZiCzHyrf~ccJ7{a%h`ioN+?3y1Ui73qM -$?I|4sPe9H@=`6gU)RSR352w5R$24$1o0LN~%pBpL)L@eC{^)gYysP*yl{Iggd=*rx6NQgESHhIH&55l`<9@r68xH$Cizwx2+= -#HPxXJR35R;+!KOAjE1IhCBmH=%kGZ%0v}Dw6ZN@L3>lJIkbCZ)B>eu$^7{%>*vpF?K!@1qvg%8y9%K -DIpn2tOiybi?QqCK`tKntkB`7*s{+=SL+x4h{x}~WwKNKQ&>ldC -xwLkdF{)zvEUk?E_GE43Snr)r;4|cyMzqs**rwl8g?ogg$JFfo1eY&IRX9Mr|KE-W+MezvtQ@H+T!ni -8Q{r&+1Xv#~yfR}m^p%uc0a?9~KTvdpckh>oCW%vyO75euMEVU# -B%l{p@?LD!9c!P&Ui{JMMLO%nDd@$wlKWe9g#GuR_}*{C;RR-WGC6{d`~keu5cuOxJn{R@rJZ?sYrHc -*xS%sjw#Ga2o#g&5_)#+HYDpl5@%^%k?wryicbRD6YF%F*dDzS?EIW?~!qUg&3Sj)!H}?w0g;P*hRyQ --E*s1mwcT7puqo9ep)U!EDZiLKB*hloTy7|RUf18V;?8kZJ37Yadb{EwBdr=Am5d~Puueb^2gH<4UM= -y1LWBN2+VZ-kWo^he0K+RD!0u}1ght(1}A(OpPAH}aec_WXT!b`h6bxX_T@X~Hy?H0urUfL(s+qE=r_ -_03kGW@(}7k$>a%c45UT3=rw4xTLhFWHvmU#ed?CM=bZa9#J*_sy22BdkS<4~*5~dfyTzB+X@e!QO~cRgX=xJKp{0=Bq_t5%$B`N9$2Cjr9(Cn*$=#$Bm!Lm~a%~ -BIM_aj~1bsP_>q>CnN|{h1q@wS%yIIMLxzMsrzS4$Dt8q_D%am55p9A*pU?UDDT((&Xbt}V`S+80x2t -A!+eREb+N2D6j+XhK6GLJG$TooHBiK~v=)r}VD3dG~6#8qEsEl8CzjeH<}K)z902E5yqGWEhggXkG^*$Qz$y5sGi4pbVvMXtc_?LD=VIpihz2ZFxYiZ<(h-SOV -zZ0GpYhq_Mx2YyIV>~|JJxlaA5A+NG(Cj7f<=CW!)OMmM__Ep{kx}*AW_662A>HQxfsBqU<`@Q7e5KN -V-J3LxCZWUPI)h3e}2S_IM;B`W*r8PU^Eg9S5(4H@&51Yegy>&}$AU|hbwlhW5(z2*auJd(@AymbDwY -nJoZ$LGv_B$m=GJRwNx*=>j3=`>VLnw`)yFLJ2xCnDAWzdl+-{^V_og+!69GS6$_n|VpbO+tGkJ)#IS -E@^Qj11c4iWzN?|Nb)W_Ou>qM{$Y%>@X)v@AIOp3uE!jD6Z&6G)IR{2D6E;Yuvh^CG&)qlGzQDz7Ej6 -v;?Jn<(d-we7Z8W1izDx%fSPWD@*V*-AvuK2*EsWov&OQ-iUWY8W@w|t$VUuvmm+QD_N|42RH0{qJWc -2o5D-m#L(*)L81)P#x<89hKaR!`)3KGCiC6RV(1x@;Y)!x|M=WrEwoGOIm^O{@M{%Avv7-MOY1 -=m+trddV-iW6kht1CAiS@p=l70|Mf+JxLesc`)_xO7Wx<}5&cHpS6Kfy7qZT$G=j<1Xs9R28U@?<$v$ ->UvH$y2ow{F>Kd7`gb%A!vbz(Tgwiv)=MSJ?&zH5^GQKNLKn$m$B1yYYe{<2Ef2xOCOtA3;v4rP*#rI -Yi;x8uct#N8Xj~nqVQ`z;By~+Zm(v+d5XJ&WO|o9R&FeV>O;?6g6Ccv{K^$WWwK8F!dM2|Avvp+K!(9 -Z&oxtf(}6UbcW{xXXlZBA$%y>O%P*XO&E_=7PZW_!484MUqP81)$Ruc7(f?Dy=vkZ=CCCsErQR_}i|~ -3%9Iixm>kj8*20U&8eWQX=naI1qcq>NvNeeT{K?l$lhaH$){b;VOA13n1t$;vPcn%y#wvmnObbMr6<* -+4#L}L?AGec403IqHs&sYCM#W(To93~s1K(_?HccSy_(R1mhe+{t4`~QWkk!`lAV$v<@${uxzYq0?8Y -H#y%?28PDZZBiXTIMc~chot(WFz}-+HNLg)4jd8!3GIGgu(^D%JAYqxk3b3GeAciw1fL-qklHBtYd_e -h_v}O?gdwcI})NEdqUR}Ry@zc{7hbvM4H+6W_dCk3z7=`q}$Hf0P{KvPB2b3MnF|&>Q}t>-@Hb$9{ -dmmpKRgzXN2CoWD(c{*T@NWKM20aeaWtPZX!n@I19EWTJ%wqoF&<1M&hk6PlEBU{8|C0R0or17nGFBKM_h#5++<^&82Lk9OP -P`f27~9AE#-;86`i$>@#>T|(2B+lumdiC~&=a-l!v-f}ORgnD%=Kn?tcT8`?*zN%HN`K2=L$L*aO&@cLRk4%YQ@ydDb@!t -2!{2hhx$WW!lJ)lZ9I*+K)mTc>B(VvCI7?!Z;uAD&V1m?`4PnXvXBwEGG(JH>QWO8j`SE7-;fO(^kKq -L62GF%1VgYbDycx+B2oDbKWYe=izi7N{e!6vi5uYEJi|XWQ;`7s-G(Q2OJ%n!`!nYgaD8}I=_$kI6z_ ->1q>x6!>8N%ti+m7+9XMBQing{>8`B}x5F83oEL9cozv8nj}+nCVCuPou;Y3y2y_l_9%o3=d6znhBUc -`W6n3qO>5!Fde1caQug-aia0Zuv61tT)weR~}xG2Jmmt``N!|{T~VZq(+TQ>}7a~YLhKs-YEBO_ZM#S -kNZS{H8HIZ%H3VtqfC*TkJ#b87~XF%-mirB;runDwkOyqN4zGz1%Lb5?|Jrnj{Tlxzh~I*Y4+RK;+Z} -p$x{LpJ?m?~7vk3%$PxkgpA3)~^}dTAH!%Sj%!GK+H#H?-0%69FtWxMU -JUDaw8FL^01XgAYJDy^4Bi`oJ#^ZkFHbIv=A2-WUy_jmdHckuOj&h0tRdCvXz95w1C=|RsG4T|Shm+l -}TJR)5p%q>o>RFo69$kiF5|@j!u2U^)+3V>q -M93JcobDjk>>V){f3?POSC(*?Y!>Kh{OZ^g3I`f5%-$c!(#?dhiQz~cF9zen(bLnT%3AZCaJ-t%KM5{2g{5V7yMR)S6uLSb$18Op|!#R?!auo>hO#|^CuI -Zc-q?pB|NPi3XR>9I?LziXEwb>nQE=R8K28)tzJ&`^@|NmzC)>Z-@G7E&2oQjako^@!Wd`8FthuuboV -Zk`bKJUGQU4aO5+>pz&}QWal6$6DSK+>=$84pPng}dGIujQqZ?#(>&)&20nLw6i!Dw=_ommj=;T4gC* -o;fb+;N-H-`@5DDuck6OBB)4@6eguDC_0U`lWPjz20nbxKX|`9RNsi8Ww;d^vHBI02WbqcphK06MgS|BNPjB;atSdp{pDFT0UO{*B~w -AkEaa5qFlV7U#9@EIaLN(y{84uMPJEh~w1-G#vGYH&N@umzP+36KD*5v}X7|RJS|*mrv&{&XN?LZ{NZY0Sp&Z~`^Q_p@h=p+zlW&soJq~*Fx -qMknfqbiTfjn%)HMpm{=SHNvOQX}>pK@J8zIDv|-m7mN`_*r?I>y|tf<)EgZpflDh2K{-xjLb`xa|SW -H`^h8)R5lCc?m>Cp*r&HEbRqXsw4DR@rp&>%wL(AQorOEKDHXS*IPsqDBGX&7LVnty~V@eQ#|;#5oLO{I^Rj|4s!FWdisw}uSNJcsAV9M1MRPUd}dVF91dQ9V9``BSUD-g -LcETq!Q&DmH;cEAs?mRAvYyR>=@Zf@0!Gs4_-aq$&vlu_{9alCAU;$Q&g~APbZ*ffOkI0!dcBuMqlSm -9GU-rYIF&t+2`m0;gAzlsbV^s+GM0C%GuwD!fW#m1hM`VpBE=oWJjNzrd*;DXRrebxBz!aFUW@7kC|? -dkXvn;d2B|Et!%ja5{^qqzb%|@FaoLt71y5z-i@G=_7DG*U=y)Y<3D8v#?nJo0o*mC~s`MZK2Zd)3!vzqgi+i7D0` -$87pj>SBka;HVMM!tguOd%@ASpmas{NO&?*iOW35sri-xotFW=cMlWn06*dL1=`L*C!p07pFk!P)*p$ -J>PuMJ4>1}{u`Q2mOa{5YdO9V?rSdL%mZIWPlN?69OY?Zq_9}ek<(->XPN9N|$I%WUH){*&)jlLuEgI -Sm*tYK-ZF-O4sSk4g6(}2J8>PThXMs0rZI)?vm{FJ_nxMP{_H)u(v_Zr>AHQj>8?snZ^Rd<2pY-{r_n -ufbRTi-g}&Zy}YoWJT5)sVmH`xe8JH_?e_xVxdK>~`%;e?^;~XZmB>->C9suf*NyRePNKJqCIrzT$+s -C$W|mQPyMTJrtAhL2^Rgnhp=I*zuU^oS -O|x3=W>C9NoVD)@70+UE|7v>Far4F@q`5wUleKg6_4WKwG;P+I$o>_S42W3y?xbv^PdL(8*|+ho)+pN -x8)^H!ngTQlsANXjo{o6N3Vo-nJA)PH$it)^h!`DoL`=+t!g33}%F&Ik;EOe*sXRx|Wv;a0cbYQ0tUG -rYXZqc`k0sJ!(G-_mde1p9mZRDZ2*R2^TD+1@$KboYgHccc4c>Y4`e1WxLj!|Iz#6|GHAP@Mr)r&fOc -lV^R~Iagq6N{vqCr|Lo7>>iZvR`?>0Gc3KLjI*zc^judlh=Hjj)0!+MP9CU7l+OI+y4Ml2t4l@7Fju5 --d2WL8Ej{SbwDK6e{O3Rg@BM}5s2xyDpQ9VV?zyf5BKubvrGV6D -k2PsjA%eF4L~JhNM&r!@Inp0o`>X9DKoo2iVvOI4Mk4YVUaaEX -#U%7lF)LD^`QN{bUrC*qT<*MDdGbLo+ao(*+vMKY5EDda -t&Wo}p#o}S_nX%YG`YVqLvE?`pVa~us%LGnRGz_*m+L~fd7J4~{%o_ln~S%q$*}s>PU0e3{kS+3(cN$gZQ ->&4iv4J-&Sa>h5nx42nxT@`8Y@~X;xb}I%Va}kUy -jUhIxAY{7%Jc8rkk8SDq3>H?rKF#uA%Z-T84=Wpj&J~>2w5?R(q`OIeMBG4KeIQ#IKwUE$#Bqev~Jg5 -5?_1*3!_#)qc*5nb~m8pC8(bctjM7*e_JI;T_nSnqN=Q(?kg2&xfYF`cq*$AxV(to7&0)fTNdUeQu0^d3~S6pKgXDq2c}!pD -jhyEC_y147$|5>MIc0lq9qorE980FBnTH?7Amc#Psia^KpAxBxsM>H%|dB% -RaT1z+F9y@@8k+He;=z)0GuNF)aSfFHF3CjRKKrSkmJc>&Y*nPajOOf}QKS#6G)=9y~Z$EnFJ)e`FQq -@=qikSLWGx=4W;Dj&H>mGX6?GcdpUT904#wVDJQ?3AGpka$EG6)TgU2r^eGt{^HOVWq)YqWE}5@C+g< --SB^^Gbqcjb8?HP2eJwvg4=b<$^{-^aZhgH=5X|UOZDl^Cn!03q8h@fI{Yo}+A4Kv5Py=#6KbkB+iXh -t!DN{F0~*vkS{18#l-%eshJ%CNfNU}`8$VigaAhEMru|8;5$fkbYxCEc?%JA(2^B44R`~hvBTWr&dfw -(eMNJh^s9qLhj-$ifw4`sT@)#<6==e-OxI87nP|0<>s&A@r+~=Rr@B?+b5vKBsi(F|`N;rFCBx^)!l} -h?8Rt#CSpH3!NbB`+byodI(+OoyUPu;ptxSKtC3QGP^^{npj@U^rp3E>%bT(ssk!SgmXs*&_Sp|Ll+k -MbocOybKj-TP=&3aHuYev8(o;A5LwXLA3z>T?>TI{PKojsa~~caS~e=K7yGomu64p_?<_t4ihWPjUCv -zV78I!(Dow7j)nqwR-wz7?3*Y;O%k#jP012G4l*nH}P{E$iI=4Tl&%T;GGx;6S=Zwnd&HMn%eP>%HVdVy>QQUOK0jso2w59k@$Olh(Rsx3xJkj1WT -1wJ4aEX6HrRly@#9U^xNBV -9%J0-AxjNIHKeuY29&%hFw;$!T|IW}bg6#9qpAkfJ+m9d`+3p01`4XCk^1x2v;Xx;!+D}KH)LYYvwg!<(!HcR -A6j~76FY;6aF8Bs#kt7&N&eO4|jIO{|SbwCax>?u)g8EKmr=h2t?O#iVz4rsNtBf@@qKoqmWt|{Wp*P -n?wI)THIe~sSzFh@8MAyu+BRK`%8_grY5^KG~CtXd&Wgxb0={oqve>_N1nHdRL<6D_rhrJ*fSdRPY?|-ivJe~>N)7W?4I~%*6w9@x=fpVU(OCdtaZ$J0kSDe -RT!TfI(LEBD;nzwd5rGnc44tE$@)z>L!U&tg&B9e8HBHlr~1qLaZEt|VmuwI{jnTjcWlS)snm)nA!~7 -zZRPBi4Ge{8hKm1GtNx-*o?w>HgGO6Zq7hNW1*SKvTu%{plh(-8sn4x=o#9TH`b%)2nw2QvI4z>n^31 -U-IL3X>_h&jMKTUuZK-dW`o}73`fKGCf5Y{0*V>R&1*qoYw8XDmPDZGPjtgPpdQ|XNPE=_w4P=D4c~9 -CLAkZMSBtHXz#f01cL2K)C+Y4mgW>)?YE;?;L2Nl`x;o_yPOWS*RMPUB8fAnNBrqr4{UJ@7EExG$X8F -+(*Cs<%C0c?tp^3HS7eWm8*V4p_-n0B8zwBObs#w+|n>pzziyoP)Gh=ducA(XK0dEglKW^RimyMFN8>2fbJhm -aXIc`cT$< -*CCAUz&#=|sl6WS+q9r7sE`FN~_wVJpiBRe?>HS0iAMaxgTS;iI3?cQ=@a|evvEPplQVx(Bquht`DQK -$lpw|ncq=T=DmH*OGZ-UchKbCE7=)pS`!eHuI?Qd; -tE~e-9(qx_VIUZcCGG2=&B29SH>JitOMrJ=&;Jj_4MsC$JJ6BnQg5J|LSR;)4R;nH_=O#JVPjZtQZ`T -f7F{4kuCA)Vg2dah9G2(YqaKK7-&t|XIL9XV+Hr#SGY7ebB4RAM{oE$lBesw2qTy^0RrEpKjDGdJwDa;uwQkZqA;}j0lU7139X}BR$C(~u;yW- -(;PtKlryfsDfm!~KUrA? -%2(jvPx;XyLOts{c(w7@{iX9-VqAPs+KEEL1v)xG)fm%2^g;qNaQL&OUD3ae^IU(S*zuh5rcf3jnJIUBoPp)W^_A-c -9meK`+TdPVxzN*{eWA>E|DoQH(1a(k=COBEgI%Mt6hYT&$kNR;n$<)zALdX=F%ErO;X;T2P&_50Ie$i -dSUh5&0#nh_Sf*CJ1(keXhiuhtfG`I{XVF1f;$FDo<_JQE>~5h>vXnDdqn;jcqS6+F@Q{sxWc3_Hwve -MNn$r;GAeZ*XN?g?!pF)_cdaD8?uiA*5n}JJ -;)cX%8n+#Ko@?Qy`QA}wC;7vmRBD9X@;vRmdo{vl`Schl?v?*2X)pl;dKx2)9`!Rc?(F>dyQd(&~3-HGaYw!r6r=y=H!8{!TeQi -G|dCVUDpM)Dp9~s>C-5nOuPm`Pgs!lpbvAYsk^z}GA6g1YY>0b&hRo4;%`p2weZ#GM!6a--emo=;bjA -+r1c6N7@#KYK_>1lnBCQMa>`V3LAN5ol=AhmI8twb1XP_e{I-@f2?dLVJ?-#G#V8RFL-j7!+*MqgWlE -!Ch&g$Y&S`XW3u3}hc@00W?|svvf#%RkUb&_{^a@UCRPV=Es`rYKqu{|*LubJE%Y#q#oDH`%K#jUEQd -|zC2ckVsaXtJ88_Lm%UiK!L^87;4P@~+vdUeLey -Rwa5au%~aNGgsMK9Q9rjAW!NhBq6}MUFY34d_PwYyu@^N4;|ZV5sJ{G&+sBrOr}Lb@xq+VQZ|(eca;y-r|0P*2ZXyC*M+a-cU -J!D+#)}w&I9dbN!9xnp+0Y`P$5COF1b?ZVBefVqecU#qB}nFNwlHV#O3;XLvbNUw_3G&n~M!hFXC)!V -^khn9j7TA%L#k6rlF%4V7OJLH9umopi{MyE<9H0=7u=A0%8ap+&->Vz;L%TQaph4~b3LWw@t{AMK8e_ -Ln<7hqRrZX*#*n^NP09^PJe}F;wp2I}LLTm3R4x-JYp@w`Zfa+cS`#^el=o?;K9ez?hjaMTV-IX?v+@ -Zz=}*fqYBhjNVYu6hPMtm&MHUF5xyr@dp$;rC03JXI4}BK^~0Jwu%xsMG72R+TE~|nQKbkeuxg6nrXu -bV^Yt(X2Z^8i~9?{0OqN%y8E6g7n??ZKPpx?-7O9CsS#@4%Qt-dEuo)|L0^~d?zc_s8?|`jwp4yOP5EAT;QoR{Y_l5#P -FD>M2p#(gjs$bZ9AG*&y2BU=9W{-n~6$5J#8L6bfA=8kg+WdrLaYwfc$DpowerXI*a?qhUN5rnPoL6Z -}InRU&QDRHBF@|4$(GLox73N6lvqhw3F7SX%b^?cvRbnqE$j3C}r1*{o4ygf#JIpd_!t4EVh?O+sEam -R1V*i`V)n8NheZtyuC~qs=fp98q;4rR)|!joTNHoB}HCDYtDN-Vd~L*^Oy+QwW{^2Qfb#}kCo?c!=Er -c*0!Y1i7hFy9Yu}74oaG(rqF1qJhOCJenssxdZ8d@9&JyNcyICCrtMFiSvrmTHiv%Z$xL@&=gn~?xJ6 -3TP+1OQ6g($^_6(?x=Tf*ggSM|=|EK!<0W2oGQIU318^m_6bsZ -Q|yt4S@MK4@zIqPShBtlUX_|7uc8t4H5WtaEyh=bKn(+HPV6n<~yUW5zV~l9{$X#2(hsj`y&P?e1auo -3u@=w=#KCUQ=~c>|$B@E*5gcT=f~s|3$8KGT59PXQ=e2_FR1vCEvU{uHidW2UldiIiicam$*rkNLJ%FzsY5M6}#5J4anh`?V%yJ*#ds6QzPl?T}ZQjkSc|Mz2A2#!7^v>S+Kfo2mJ#A0m-8m0kKs!iP=UwRG) -D<~a_YWp_Gc`h@;mO1tUNl4)M`LL4&9|aarOO8^2GfQ{87+p1^aTKNiR+cTVliZ|#n -$yCoEFQYvA){U&uaw7|#s*abosI7^c -v~f`nD3Uky@v1)cV#~KNU9fb=2w_g<-l*Fm0%$vkGQ@(uXomEa_sXsckkZG2ih_XgET5C`}bV4s&57# -2ZVVU|#d6{!%YOkBz%8!N$rTSmFCpTVI#^Qr3nOd>xeUOIb-DBop+mHi^d3ho0P`q9Y@aU_kQ;z9U8I -)4e-YpsVdvl`}D(F{SKrURTjF$T@0zoXD_2+lLFJ8!z4|yF@0<22s6d8jJyPNF{p^3qJ%QU9}}ZtA}374L_R94Y9Xm%uGa~pwjkGdbMh=c!pz#FZHn%;x@%~7OH< -GB*M(JW-9zU3#)Km{TL)C+>p4-2r8Ccl%?+65LZ{m6l=MEt{6tcQcP+~2nI)x7JD -#{9nSTORwI`b3r6wWI6ygeoSg?qtgp*0sXvf+fI`Qf+a)j(RqLTo=Wv0P;VyU{9geEe;>#O_cZc6+zO -F1=R&$HAQ8ACc5Bf0c8*^>#1Ehi-3&V@;Q}TxW_>p1NJ?zw`$5WP$aODd8>a!TMF!++!H!6Mp$2Gg>@ -sGDm=ObX@@vo<7PvNhzt7gF^~P&^?+xD|w>~r7pVDSnX3gkz>wQ-El0M@-Y{jy~Fy>0CnHsE|M(J -5E%)QWfCu&;BR}SK8ppOswUNt9%@!^Z|0Q5LdNJn#1nqFt9`V&d#E(l|sNOG-%#6uPXtTj_K;Vc{|l$M!m3UvG; -o5x>GlR{c|yXYPB$iEsjbf6%OIX^e|hZ#d#q=-CC*as(Tbz){s;AiU?n-T{`w>{w=@rv@edxW*wlj;5 -h*%*+HoZh8O$n>vw@84A)X!ft?T5UX}vuzI1qWw>VZ|Kie$bV+2qW2@z{S6*8%i^tqi~~xy7irK$7qU -q6qBWP^Y_=m*dUZfuMc|FVjOp&MI=br31+%NP*S=I*OikFA8XQuwAPm)_b`GcDd3V?6fiy(IU`#4;kl -Gv4-2t`x%zC}feN>tIBjuC9j3MUd~a(FR+ -{Nky_UcIw?t-Ifk{27X_N{!b+>9x2$eX||EZJJB3U0_D){+OPl>};i*pa+!AHGEu4J3w4U3ow8ZPgNd -O*1`>38IhBHXjzP3x7z1^$K*aFFI9etp^B$R+zGBdrp9{7VBN(p!lNFTcm0GmWw{1J?|%8N!;liP^X~ -66-Cw&u6mM}nTIB6_h5>E(o)5YPWRY?lQb+j;Y0V)OX>}Pi1XK8Ui*D6rrqqiSg8VTBepdJB7jLIg>y -IYG{rjycqyGYvtAsQ%eu;ubIwd_fS<(Niwbn76UEis-UKONt&zHP&FI@AZ6O%WqZpw#I!$L -%Weu8#Ns&bqBUqm@f18fN9_AE?pN?7RxsRiJJBY}8(n|NlF-Zl6%Ew6l6UT%Vhzq -PjI;4};h1PuZ0E#8<61r9Y=F@0*F??H1s%|pJEXMHgZk_=c^Jjj?l;fGy-l@*~%rQmZF=t=2=-+{qPT -kM%9aK~IukPcHE(MK_rZADTW7Zie?&Cra-%>(bmpt7|NjONwT^sXnP;IbJ -~{!3Nv&V=>6*!YDfMNdWLIa}AF4PvosajV}5yA{5%4INsCHJ0&$A9!y>BHZ+Qo0nV>LKzrO?Yc~_($#hllT5+Dm_%d?{6 -AS?h7V&qq2F5Sig>?^=nh*GXK7={wPXOd`;VTJ$ukT`N&5g-#ut=W!qQrC@JG{A2V&|uk3-%gAES#^7 -lWr;ii0+F@!e4inB2Mx|NRk1SxCKN_gg3E55H^l7<3dScVyj+yNlf$Xs)1CMMERgZc3!Ugt~31a^f#p ->DWmF|U+y)`}nM@AwRUK#$XY`Wf}0{I?4lg0I=cBglWBCT_ge++QnaPlzU%cJQ)c=SMHzDo0PyXcAWAt!UKaPS6->9W~5q6sc -%5?WB#?n%St$2Q2RWYSa(WBzbDcT_cs><$E7N{7pXT`0G#|uXtJG6vt@Mto -^Xw`M%g+46|+!oUG{WIi=i~PtS!0xc*8$!@ -fR~ROwA -{I*A8a2j0F-Cl%aY#x1lQAL-}K#x68j4D1SoB6XFj1W+~*O^W78I=hLnPody)Uo(s78)NjO=r%P;kQd -RI95DBTR;r0BW)4w8EmCu6Q5v7dF^LhU}myfdYtnOL#fN#BWdXm`vG*#TyjTx%wn3iFyC3(Bb_&(Yl8 -Pr2|A;lBnyy>8NR`Dye2cP9?4X$zuX!-IM=4O2M-RN!Dy#4qBe(nO466i7FtCVLDqGtX;Fm!D$9Xnm# -VyLvW+2foA^Xg7~KX$)AZ#xl%p)!hl^X_yu1kzTWEtZ+wQv$3t7OoFDSsb{@tg$3(2j|2SB2F67z1Ce -Q3a?lmixekv71^hpkDJ;!D4pB8%D6uFivgv(en4o+kdp& -V7ubc@=b4PjnFxslW7t8^ssWDzll-H%or*EFO|_W-3o -^z(7jSwd&AkDP7aN!=Vw@nj3Unq5lEJSk8qMXm#)RL|>tY|AzVEF^nrz2~%5R6_tQLzaw;CBH0J{g@} -W68YR^%&X|4JmB#gfBe*8Ty;kjAj_|%VQ4ndD{lX<@CpD^T7w#o?k2<&vs5MYq|lH -NvT_Z)hV3WB(KPpeDC->5-AGa-iNf6Qs)CqaZ>pj67x3BIA>lAzq$mM(?aPWw~J+?;j;LB7Y_dZpo!b -{G;GnFqGzLj(-J^#-bFFGezUWlEMWU`{>q;Ijqok>&l~n?6p5E|U;flBN8qy`W0r_%@-mpKp>953WiM -WUSV3|1LNF+K+!qyR7l36WVj4YN>%PZ*XqFj2Y;(cV+e -i^NVr+TmnE!|@FNKuB@7rKU@r-;k#M?%1rn~1@L>tJNVr$Rw1_>A=VX}mo66Q;Ir-Z8|+$7;P3F{?nlu#cdV6=ooButXF{@B#am=m~f7gs(_gC*cLkp?AH1_!Y$lt;hO=ZDmu4Qxu3? -$X;^ko$yPl=9G&Yr)xvhh_STS?5Ld0leMtlzZQM^X(Kb(bdI1^N+vKjdE^`p5m*r$W?|)iO(z^VSO!9|CD;^vPcy{?Fy^HW{Z8vAH;XayKI05*{nL+3{E8 -4IwCwfJ-`0rJ%YPwx!%mK5qkE!8@gkxO4EBT(BiSB}loE(<|csMapt`%$l5r^R$sH3*pBGo{cP)#lyA -;)UM*vqmq#8<;ho`IqgcSnDa;_D3|AsgY)9JEToz9l!dT`Je_Uxdz3f7ixI-f%SJJm)*__Uh#2e$n=A -8IP%i=$snv2yxGeUOsBhWxjc?@7l&>rE-R2|rG|-tT^T*+J5t1(9(xbZi?%CfFO{|{%91br%gYmS -MHF*6hj~6_r=xzNExl`hhTdY?3KsI3=-|}zylup79mb>4|7Xh2R2@~)7PpM=JXVjQNI=Gq^GIarz(NV66)LYgC_dCT|094XD&(%eOhNSeD!sy4mmo(d@xwl47n)_(+Hw$w=X)ciF{#txZ!fe#yljZ@^ocgUWmud2l=A~ME(tM}J-`|D#q -BNh7W>uQENplvj`wl)Xq45Ug#SOfV(^|bW7KcA0a{BTSWhStN2uY^1mY1HHw^Cx3*}O$4<|8a$mznVGYnR2_z*ZkKIG?`e9G)%_uNI$1U!$kd(ZZu#S;sD3Jhs$Zpz`p$+L(o(k -&vbM3HfKm=jFuIP3LinXbi|9~z3d3gPI9-ms98PDFqflayOM%N#>NP= -Lw!C7SkHt)vFlCh#bITbn33Hr8hr&}B@<)Eio%#NBe*OW0or3hiA)#U6oedF@UAlIQ>fR%|XRqFU`u6 -K@958TD%-|tIW3L((clGcQ@d+bGB_@r&=Gx>jV^hYBzivXRX=0k$GHJ3kJ!48{*7Z}fr%j(RbJh*B=i -GSH+<72`TwG$m-BIdv-La%>>792iTfSl?`~UrCyy7p``*)?=m483-p#Z}_`?{``L7pBC2mKP~?M1-~ovwBWzH1n64-e(CSB5}<4S^ZSK=ttH^U`uB6`r@wCa -^ELUp{o*9cbYE`%rRi-=cUv3&=(YD(zUuCZ%Bo-edUf?0_u86u_pHD7zTe#c+Xo)p@X*7LY<%>0zu)x -O<4-*Kho_$2{LCN!^z5Ji^4BfTJ^#XsFTK2V+xAypeeG{McGm9Ny=U+1`}Q9=_{N)u4j(yMckK9EZ@= -^IdnevMdFq1?KRR9i@h6|2Is4h?O2ZePbLYSOs`2Y@{{HQEP0cMAzW?FkkC)W{a);1yga3;HeKqC%=dQi^UR1ztIV -pf00LHa`v1cJ-ap`VRm*9x+^13u6{wm0{k!`wwK|jhYuFS^m;ul5$IWvCX-I(<-aOlRgMV!e^vggyUV -);1|o*4sw!l3LPB|V`D*x%9h#gyyBy&WNp?1AUpK~YtU&aKF^Ie?6%NctHi -3&EGSSQu}2*&Is?ix(O5Ib&D2m)e|8A7@tT%tNyBT=t^ET;W4=5kI&?7#(|nyUpR~*u6Dfxk#APX0(e -^ZgiCr8$$;dl@7gJj*Dzgh_TRV%q_{Y8B6lD5Q5$az6RMkzmDf~aR=Z+zIGKaSKHeDEM<07AQ6Me3D9 -4>I0qPVgIIwWhK@k~4JE9!z_u_4mPLidwy{Pq*&&@J1%*yqsXZsxX3WWV+8oBA@vc%w{KCTGcw6xuL$ -$zd%0X*6q1fQD&{0yn*jDV+{I|wqb1W__&f%GctU!q2x4TLpNaSf7G95CF-%KAeBK(p7aZ$7P%-xOO3I%JJ2!|<{NE`?arlF{d{~2QIuq1$pNFeRBg1v%7wDnwzxztB@ASP(N>o -N8YO0I$Bwb-Gt4udDgAEtH6wnL{`|sQpPzfx`chH_ls0xA^)IFKZa^k2orgY%I-1gXrG<;_MYj1xvb~ -#^S6J$tx8$0n5%#==S5&TcetZqLtDbeu3u9fALRr`8hPv?DQ2PgoVy$>oAJhqaTRsy0ZvtNIW7c)@W1 -U9%CFl$R%#hTH8AgTLBm0D~K6wV#C#f^**OKk+FN%8sjN1R5+-q2`-__2^tof3Ku4~a`oe$TQ9bK>)J4@s@-zn -VG-~0`U(HxEKK*GKzMGzP%fmM_2>f&+e(aImIte?3C9@Vb5?zXMgilxl~Izda!U^H`WPb6V2b#BN@va#n|}M0_wv2SeP!7>5O5_m>0>6Gc -o5wJ{ui<8hh18*LAP$X4mxz6ZtBj|pBfjHWn63e+7eTD+D1$f5#Yv1^HMuoR3UJ3{pD$p4-H_Bhnnclc-d3cgt7r-8Rl!ai{VIqlK6oC-e{kDkLuAVa?jwE+s4I=9l=+irJ+wK>*I_@{p*4 -H*PZpzbzve5Xi<3w9h7rfIztv{bRKmucGS^IN0gf|q!VccpGae)R&IFT;6&ZPw@UiIfDY^)n4i=5;B~ -Y!>S#FXXc+5uAJRzme`W~ke=x8Ak&&HPBxpyDx}2A(cVf(n@3xP9+s06Y3*q5J9)yE8@HbQP=ZLBmeS -@BtE8z$EgSNLP+T)(6Ybg7*9koHXRH4pPwYEXJm=G3|)RV=0)2lJMzI$D_+Aj9U1rgbiKK+jt_5voZe -K1))q@|s;!k+yQ&wZiw!F55kf%bp}{@H#BRF19WHay(Y-`NHE8;Nof!Mdh(zAvOvU*D-NpjPYitlsoT -g69ZkI38-}eWfg$Jy15gCU$nT4VT~zXF#~l%^&1gS^uO -w)?arMyS!%=aUAf|`eV_K{Z#p?evYxfJ=a>kdR`OC1}EL_KUkOV-yUsa@1HKOhqp7PZug-#K+n3&G@$ --=W?dY-$5`N%1Jxlpw&;zqMQ@Zx)aPv8E;#%WL^_{BIybhP&bk08%bBhHvuJ}*zR-X4%=4D7dX2y54> -R@_zNn9V>r-d=MY%=%D6jRt_379V##->+g(Y{qrJ_cLvt -Kb*zav!kN*z1Y?vtFh+4Uhzm+NS`olQ?HljVlnP-KxXys4uCs%j*@*`yJCs&D0d3g@b&2|2nB{_LgN}!NJ^7KijpyXiZm{~5 -+*(CAlEUl=dR5MMnOuycesjY`vbTga|{7aur&vs_cHf5)ChkmVT7vbRkCY@j)0hJUMI}y`+epXwKo$? -NS6vs`pgu(10Z4Ttj%%ye;xwcK&FtJ;OpB#tNWhYV2W=8_Fb*t7K_#|LfaRHA$&r+6avy-%u6%;vpfz -schv!8!@soA#BwP=yekqrS-5f~CM-C1-)A)?GFnpM1{us9E8nCvo3N^W!6C%KAqiGrorNeeE~hRCs4wM^ka$40~a5T8RsM -ehIq1ay|~Z7ulRw5H$Ig2uS%R-K3&Yi1mh?BA1QLCDRPebR(H~R+W?7d`jAzKDBFR0j)@FfB&Cc$TE -eJWxW*>+=Kq*2WBx;su1t81keav7VKJF)Z__Q=PBWGQu1w?A1D?cBZMkn%6Iqf+Pn^)pC3gsi28*&_l -v`{&lo#e4kw~4}$BmCt}%@?^d&98NLi~KQ`C`ymyX*T=M@4x!f&fkuD{PggL;xLD~*?2tvZB;u)%2F4 -I`$}!imz^qzU_bk#cyvGO>vI2oKkMs1zyJLH^ZThE)j3kjq17jU3F(Xjoqeah9J>v{TpT6MbdsTqmylnpJApm_z -uZyC+WXj^1MpIyQO=Dbgz_jNcSF{SoobZKPLHkQpWeRwBIWEJ0jiRlkU+$LKd3LeC6IYAAaihulE0sh -W~fd|3}0B|N0{}Xujt4_lbKP|0?+J{Y7;66YjA3hZB9UqSSpXK{s`TPI947BAH5}uK;UcwI~JR#xR64pt0NWudW?v=1s!q+6+CgBzdH%WNEglpT-ua -t0^giZ+yBwQe2wuGq?CQFzkVXTA^5;6&c11=vEv@l`cD4`-@y@V$utdsD7gxe(CEa3(T%Oxz3aE^qj5 -++NSAYrV8MhT-OjF3>zw0W_BEvo2W8zodEte3D(!fg_6mT-g0E~CF%n#&|CkT6@qR0(4xjF2!u!i$$= -{1VnmxLLygu5nk)_{-M~{1awFqwoFMCM;7`Z@f93ac_f`Db{QTul3$Mx -R6Dbd==~pekQgy(8>8Gs~jgX&~e{~WbnDMWHE9LKPN%zoQKsgI6z16+x38SrwzC-JR^AA%2HVx0hd17Kw|+BDzF_yOEFNbpZ^dW?{j9gv>S=mR?Q -0H4NZ2EGOGIKFJ)Zv%#2h5Q8`2KWL#JMb3)kKvma1wVjehY5bh0sb&d#B~wy`K!@KlYhXl;poSS4&V- -a^}uTZ$D%-eOJM<i9&jm!LK}cD16(&s<`Lk@MD!uBr?VaJk48 -NJer7b+nzKICLD!9nzZsc-MIJWx$sK{&~EhvjwpKb&xsije -yr(C*-plaPxJ-55WTxC)jy{h>PB4H{&aS|7?Qz?7;1Sx8rjHF9%$OuMBu4pgtAl892dvQiVJ>1FkcHK -J3>6zHO3a39uR8Yp`ztjG8ETumav`#`p~O_XF-X3x3`PykM632>6@@`PUu!2$(!c&?Go-lJHOPp2_Iv -;b%Qyq*d5=1Dq}KIe-h(krw#L1H3C;&|e1lV!EJ7@SMa64$Khv7QnCZJ&CZ!DU2=8LS6zd2i%h-=Gm-zHOkk^3a(-F3}j>GR}GIllWTL9m^0eK4i1RyQ!5&eWYj6H -LsD67u``rjn$KHyS(SQo`ZA&Hv)#x`Q2#LHNYXcBHma)S036-*p~smVH5lx0{p57IP4n%LyI9J3 -JW-@Sn!Yp_-e86{~F-B+ZoG)pY?!49Kt>pFsKx1hrJ$fRjIJA1gt9+{1bf534Y+`0AQmF`AzWxR^B1v -+5~7@0{+o<6#zb3CS70Jls0wMu%5PxLPYzr_)Upz{dd&+l0IZel_5RO``oL`1oVO{z<^V$7TCQ5 -Z`Lp*8@KN1mXo=3+Q?h;i+v0-0`IFUkezCx%o!ecLRLp4?=Fw0^YR+^FP=x1B`r5TfbLfW{k4FeS7iH7_-l|e`~>_B^>+u_Sm2d_2X}}z;SgZgodWL$IAy1xlL`3fPQm9 -Uz_GQ0&vAecyoq$d{{}$IAyM9}fZyPI4ffvx7918l5DY(paTf6jm|BN95paS%j-l+LzDEP@I40^sE#O -`6pbo-*8DR4}Vr&!kF3SJAqTba5=D#QK0$TWgAM<1QA$SDeX5e*z>idERb`rEtLAJ0bc;b{Odjv;*fc -_NrNq~DkL?1?e0LOeJ+M;oQOYofoUIzH^N3u+QB<`n_foFnWN}M3wnYbFf5u`g7WKVFsv?n-M+7rA(; -so!PIKk&7PVlhA>CFCli4*j%7dV~0A0=^uSrR8$AaR1LBu?-diPL%PLlP%Q=TV7If^^oD>X -H%zF604j(z}ru)ft?`#aR9z*ckYlW-s0YOXs7b407T>?HgLw3_Qb(sA8Un%R0<$8Pe84|&dvjMl@5OW -zytSN=wf7{Mk_p3G*?p3Mpi3z^I15*bxqj(_EB!-fs)>8GD&+qZ9LwY9bEz4zW@mo8mma%-!cdxoKrO -kwPvL*-%2uz$&tquh8@{pwU#W*K9#s!D!+AfRs{tq2Gx=&a -%bNZ(#eql!Zs_H&^h**Oi@ptj)h0`!Kkvr%A?u%;W3LEh=~B@+T6Ca3=p;@A(Pg%;|I9gfr8xmOtUlF7~foS?^yy0Y#v{& -*zU%SI$I3)4`{c{(Qqt=-^w4Pc3IUeE#@?@EP#+SE-yw$g=)(e)-+}mw~MO-PyUS`a}NmgJ);Wnzb`OGwDx&^KZ+zeX6RyG+$j&J#^>XS@YRS^@*J?%_4f+*Ae}ds(Q -Y;c?;vE7~}F4{rjsCLH$+cFF*DZpXMh>eg+{Yl2Iy`lfGji;Wv2jU=|k_#}X0}*!c0|naN~g>FMcQcG -IR!V>jGz1H19Y8`;e_-^^CzO=61{En>IcdMjJJcrkN09K8KlzI-`*%r$}i>CW-&g%v5RVv&Wdy)BL1U -t(g9I8xcGtH!cFm#4FbS1n{)H`v*}d#13uCvIc0J5+Y{Zk3JPr?LrqRW|w!m5n>1vU!J9miD&FrkqsS -w9_gp#W(8{mCgNJWx4e#TX;@ot5&UI)z#JP-h1z54?g%Hd+4Eu*v5?;*``gK*dPAz2cExMwrpW9z4Q` -q%U*l!HTKq?E$r^ERrXS&%3go{b$0OJL3a4?VRr1;G4}S`Z?lspPqGg`{E&Th_5^$D8D2eR@G)HAm;^^1F%+V~t( -n|#7|L->IRpMdb!A$%6XgH~f9!Y@VmH3{xo6`qZ+# -~^*V%~hwyhI{DTPpGQz*@6P|Tu*{lcZ>;Sf&#i80>!;YflH?nl5M&88Kk#?p|`xR4f-^kQ^pJ(cG2bg -;7v`=^=!e4{%nFzlS;mZ(y9l}3}@Gl_z4us#2@QCx?4-oz=!Z&)uUkwUVK%o#6ehmtL0ENAv@G&SfMy -l+akt+Lkn#!7PS6TDDD!cHU%6>Sedc*fd_~8gY0pX`3d@jP@f$*yl{vm{a3gKTw_}vJ96yZNWc#ltby -_7%{!uLk_0SG??;jc#c>kxhp!rzYYYZ3k_gx`tqCw;KpPKPu8r2{dW=f_<|9wtvVO81PcG)u+=khi3nCQLNVznVM@9z1Y ->bWhJpHD#F6%xMTue2us|F7CSdtxRTQWoW-iy`y`~A2C8uh#5G*IA}732mOqU^a&aB`*!Ua4R?5ml^% -x7NB9XQ3Y?ZPzjxQJJ%u~xF=n6*BKK}x`%aOpWe%el5PtpyQ` -Yqv8J3Lsy@){1=pH?~hlPdp$)p&_J&XT9zt?3R=93u6JvPwLMT(RY$89uvlzOr!h_fe91lXU)$_w@5C%8eP-Vk|w6 -b2k1hsmkuPWbhF9pK7UMnL~tjaF477PiN&mp2`EUd?xt%;CnN^@>${}GF>NBsa#B)STC4l~`4df3Q#u -C)UIR33K2c9Mx5hs|jl4`v3HJ+1CHJ%mD6Sc-)t%&n0+t#Y*?H>xw5$m!C~J)Q(icB#MC6z(6Xclg<4 -p9;+!^-QCZ?sNU+yf*59pgw5YsKA$F_0yp%2ldo5r?rW*L|Nvf4PSYVFDvL-~Yy3?yp*HRHLJ?R@Qy< -;vNFHsiUU9;ayIx#`oV^YO?E+hp(9N*y=(H+u%rJFWzloTOMA)u04P;1=>05dn&u(6ES8e1AO3t -2iPNzJi;D*^ilTMV~?>XpL~*!6<&DZ1wI~p<&{_XSmB-8r`aPIZ*0L>Vej6(?8uQLd~9&$lhf>zPd;I -1&YWQ$kB6N*caDAY^;djs@ZERcu^)f@k=@&@vgcqv_JdD58$W>YjTJlU<@#K7bW73E-Gh$qDHf@|$VR -F=*fjM3yIp;Y-K&1Uo>R}F|8DdNj}9>#GkQBZ+x1r?{51%litrf-KNsO|NBCbO{G$l}Ji;GD_||^zKj -W1Dj8p!vj#D%}?!9{T8bGioSM=U1dT@MvJiRa3%h;<|bgu!!`bTu`+z;if=fK#(gNF`|kBl%}6&_Fbp -$82e3cryN{o~`}<5-`8!(!w6hQvq0L+9}DetiZEj*SluiR~vXjKl>;MA&dBk{!u}?pn>u9>l++M^t(s!GN;&}S{;74aF2Mpjww$i}ZI9i1BmtRNAr#c@$ -$iv?~ame7-;#>wm}ZpIJO6e(A`6RR8|{NvQOZO28q(!NF)*_%oKI?w_6rKuf`|4QLYoSN;4Y-LPNipX -5s2BNcz+?|=XM+30s({pzc)KKt^^FF!}$^U3-1=g$J3{`T8%zrwuq%m*KQ@ZA3W``tR7E^geoaq%)u( -UJ~bp&&W@Bl4rXrjEJGkRd|`0g|jb|H81*bLY;Ta1DOId%ySId-q@-t?t~pQ~mt&&sFrN>bvj0OAA!$ ->C>lG%)eD~M;cW0vmYvo(txyneEj(F-|gD9Yvsg=6BC)pC%Q7ygNZ90#0%wXi2MlGuCUNmk-p@2@#4j -aEiEnIGinBZIsZK&@1v-LNkrq?Yp)$b{DCjd{~K?-p>E&4T?K#Ywr$%u-)GOBRX_XeGqtX+PCa()823 -kEFh(HzGYIqh=bn3R`PQvlmt)9gsi>%!YPZ|X>(;Hy#<-ezqw6ejJ$m#=tgf!UY1F7uB)e#{*&IuB|5 -5(`{`bGD7|Yx{Xwab1km0&7zW73|t*!OS`RLK3>ih4%&-q0DaNY^8udi26ojS$MlqT)Nn1@4@xo^Mz` -s)^yyB|PP`TqOw)o;G}=I`&k^Un59KmGJH;<`Zokj~$rzKsSC>nQ9$%0I?a6qolS{CkZPYnvAB1JB+MdG7su?dkhx_}{Z<&syZ;K$M9}O2h8myE*?M)0Iz4aF -VsBTbx5Ttygx5_KpO#EQ}0rnTk9d&rrKf-@gQmMl0W6g!Gj0+N4y_6aDdm96DLk^oYFvT0F^hYCzMCT+sj|7s_mqzPB@~fNe5Lm{& -iK2+oh@_YE^am8>;%pv#R=CQ`1|hD}8>Jb07Y9?AUQ{OiavZjN#Xzy`=o$X&`<{<|J2259I~%PB`WLM -<0E}Kf+1oByZxE$^oT?_$K*N9zT9gRn70JYU*)SO*yKn*B(;U!~;mfUR522Gz|Hhst$ZbRo8u>suwR_ --1v|1kG9_X5&zn!<%RbBGxdb(0hO28AF1l(6RMi_4&+a1KptH4rmBuY8j9aj)uxN8ntx1H6LxsFCVmn -B&CShJme(dGCJsbdsH8L?AFfCX@j~T@cx*2nBzKZIoDyl6__q50+B?_isH! -ZF7t#!_S!TBz8LSv?QVb18r6c1joy*& -z|GV$L6ZJE#CK&9rI3n>R5{@5!_(9%$c9pCx9wN)9Cdd<05)Ba}{#)ekzlhw~Byt-V-gEU?WhU0@^B) -|2#wK01%hBgvI~|=?iyP^8fDQf1=kn{WyDnPoc^UL0C*e4dd*nJChsA=dBS#;W4mKpb%PAzas?Q4|D&E&{XbwX -^F}24)!rNVS6!TV^Lo@PPsHlww$`7;2_D^yeQF8U2M~XcoEWuZ})rlZJe -7TA#sS^%)FSpRq|jcbLokEu4Pz4F95WR?7r~o#8kl>4?R`*fy)5Y%1ACY&zcQA -}n4EKJf7(Dts#MS4dgN{w=`?}+wtxdXIF8R-W{=`s~ISm+y*9KlZ}PfB*jdFM}6$qOZ^mZ -P0G9VEeK6$<7$(ZWG6}aT|W)q?LIIvMe`I9#aezis50!@Sw`k?2JK%P -;63?C&qZ@i^%pL`mOvA7%<>6Xn?-?^XIo<2*=SLmY;t5Q9jwbT=tg_l-+at$_`hbH+l4Vjf-K0i{Vj~ -ql(E%vLMsJz%Vg}`sblKSAAyu@2kFFj^Dy_^}kE*yz|Z>IXOA6Kr8x)4wRIX7z|$dIulPgqEq2HR;2O -nzFT}p=H~xWsc%Ph{3?F2}h7Bh6B9>XOV1X1C7Peq$4@Y}EVPKOsmvlg%6+;9&)7JPc`?P -rR;=O7+xUN8c{9 -f}7e_@M0+ZdzD)#t^k3umH#=gyrMX{_@*OMkCky{;cWetgcjapS&FJ8Jk)TU#r4+;N8t8#YWHdE^m!; -)y3@>C&Z!9`bwW1p~eSKVfl@!+-&MiX7q#S!3VALGo4|^nWX_tSLMlV-z|u2KtOmT3&P;R*6<1s#8e1D$pab3TPB7RRj)j -R#*=u84Utcvwc0QP>{@IB!@Xs4(4wP+=O^^>>-74RnJb6(0-QoH}Wo4z5m6d(7WXTelGiT1p5hF%OR# -ukTcg-t}EP(;N04KD8BOD7g2>dW|kB>%Is7H`T@*FV|yaP*wq42-^h&?*n`VS|o?pEjK=6?LZ0}uQRo -%h^xkD2JS#c<`7SIYG1(+wZ+8Md}#aX>Hh+nO)A0Y~%!9YG($^&Ow>%{|DUeP>yJL${EBm8F{5vuD4J -o(>r@M2d@x4IjV&{Wsotqok#!ndjjF80-Xt&3nnkh_8`*=;VkDfz2yh#8kXrZONmTTyn|vn>TM>L=JH --^dp;0WQlq+&p{V8=YoO)qrU?O4wS^iMCskTx8Vb?b51ypNU~*h!yb_%>;ZCxJ%)F-zM(YHZtH0O!qB -g{%dfxw`e@z5GW7ZR=btwi*dOYKsvibJdV0DH9Xiy=)|xeI4E^jid(QvRjUHfsc%Il4`61Ru56}U0g6 -m>~mG+ntC&&prz$Sa+A^zvO@Cbh3&lNA-4ci-*geaI5B&SQK4wA -=oO#d4%!X<-uzd6PX%&`4+4j+_hUb>b@(GM4)%8I)~&L9`En!AR)?A3wzYZUCLSX*wtt{Dv#xXJ& -VLW>E%bZ+qBn-|#y0Kogjro(ZO#W5?*|+7B3sx_uvpqPK3#W?^8O3@p+)t}$iMpUGMj(3hsD-BZT}A5 -@Em-wJvn%WEuxn|4+<>Mu4|l&ooSbTZ~e#SQtk1C_@4NO$Mofx$QH+!UV6#cUG@NdMc$zwJy3aEa~^3 -o^n2@{YA0IiKVZOj62D;okOTY?zLPyB=Vb5UgRSqv2OeYdv9;JS%_&S@LouIk+7124ZhLaxmb`my5wF -|21^Wy}WL@RxXN?h>l~?%He?8B%Gx++6icb<-&!mjUGx70oEIUn@Ta*yI1JGsZs?y;+TyvRNFaE}S@ah7`&=h%s3>(A{(`OaZ*e@|C;c -JZ|nD^l;%`3IffhUJSqrlS&G4{U3&R(FkkW7Ovl$;imKQ{(+XYV)}bo7>Kax&PVckyEby;VF^A!y?J= -I6XFM!SodV>qU`~Z-~6}wV|aw8o>H*UAlC+L36vQs?(>iXBu;!RD1im+NJ+dyY#-s=d~K!PSO~d&oEr -0Gg{+~V-r4g`ef8X>GKV6Yq@WKBl5?uL=yIy^}lRq{eM>*?$dHd=1UK3kCcVqsab$+8Z_n_L%=eR -IEQ~Hhcx#+J@6Q>sYrxPNLUw5e2+XFj2XU?4TrAwElLIbfA`2+qDyN)lyRujt-r|=s8Gwy9Z-7loaL% -)-nHvJuH^7Powv_3h2=Ab#6FHExeJ$Wwc5=XNqHs>L3@(%iCBR+Hbee}vUACFYJXf=+7$%9$nd+$T|J -rC>&{_E&p-zfPa?X)Kzm1wI{RL{mTbJdYv1fSXdyzb)CGJh9*M?5e@A -R%uTrp$DjGS4sW=-<;UZH`y346hyzD}#ji-Cx;p>O@g -2Q{j@PF^T1k>-LS42&d+PIZR>f-cQ=uMa&m%IMVaW|W_Zt=)t+-t4*PfWI8!GiRaD_5qXi{$&n=ir7G -_JHGC{!G(1q+dl{b)#F`UhVc5&?UT)NX%Yy<&_iN=Z=c26yJMfu)H6ExLdf)3=+% -`e4JB@`(KCKw|5$(F!iDKiKKW!Sy&>Xp=&*UUy>AdY*bD3tuQBM~2Jb(u6OrW6BWn@uKQVT3adFPPdG -oT_heeAP8M}ww#rI+Vkp=bxS!8W*&*{scC|K0f~1#KgoI;3z36nKEX~n3rQ>Vr25<$)*lmQBh% -P7dH1{&pERH_TEPJBitu5xTe1EEtRcyn?ldqnI}ebx86pAYD@R%?EK)aQI;<4mK6t?_dWVmtPmS -}rn3EuFpw`r+*(zys_8=Rpg5f!)StLg((?yBGO7)}t``UtV6GUQ<((YIAP*3=I<|OpvRtx=L=p{dPl} -t^3*j5br~7ti9mA#7&zv)%cuTp(DfCKkU!kxpOCRJ!c$uCGal2w -zc@}H8tRG&(^428qM|pX9#x`)Ts;w)+FZLST*yMu;53W1QwaxmhqqfG_|44M$82IU@pO#gtR+(Bcwvn -2X?J1t)+Gc&$vHllX0HO*JxUXrI<@-jE%QvD+4X*Qx8*#aGsC}sJNLg^_t4#z}2f)Z?3Ja{ -Z?(%muuIqeRSQrb*=19eSQ5gy~dQMo_fkWukZPd-|rt>U0uB^5C|B4S|0|VRj1$ao=-n*@7}%CS%1)L -G^zZZKo7}{k=LN+Aj3Xz?t@Cu_!(aj>es9WXTjKHXQ5bPUMf|ujC5kDlwWXRViKcTFJ?w9r*J5e6Sy{ZS$+R3@A=qi=;k`?1-&9L;j^hHaV~i_zI)%keXY)Alyt36>Za4z*`2<2rhQ -LWcV5KvoqSz=QBmE@w>|NVYn}Lo67z`fTf0DCbG|u%Z(Hl^<5x;}#CNUz#upRgSSP+|t&984DDypQ|K -Pqe%6!XOS6^&;ett&wn5-F5McG+7`Ek(&c{%;_$4<=1PS5X^ojEpdO8%7b(|V1alHET&Kf8C~HPKPo= -{cF>GxF_kUB*RU(>o?Q{-6FaDk^qbUO~S3Rm=`v?ONwNJu^RJY(ZY;v>EPWeI_qsYJskjF)k%9voJF& -V?suL%ZuKZZz|GL`3cUU8HE{HQCa+ri%!oUoKrYuaz2Ul`~f=o#o6NDib0CI*TE^8=NE^?~hy1A#*UzSyx*uMt%pQ(axRzOJEed)=P819gpchw -7T@n(LzKWAw~KJvT+q9J>`$8jJNR)q0%fQDw^tvk?p_m9)3+wECb=f -1Caq?4?L@Oz&9%qPeqHGA=I`#0^7r)j^(Xq1{VD!5|7f#|i~W`UwxX9`$FJl6fBY{{O9KQH0000803{ -TdPQKvO&$A@}0LYsF02}}S0B~t=FJE?LZe(wAFJx(RbZlv2FL!8VWo#~RdF?&@bKAJFzx%J?otq(*NW -3JMUOS%p`kchM^J`*z>?GILaT!X2Y{nAFAt@_L+yDLT2LJ*jNXfptzP@?o+{F?FEP%ygv0qs1dZULUZ -<0-u^lHyrRy@XznO_emPBz4Q1}l7~?2butlYDW+bPdDXS> -PS@3SmU`#etXjo|@LpxhbQ)I)yn_#-@M35$t%_v4tTOm^RS2;VX@#)Ph45YnrDYY*=VFR&(u~*bEkqS#c~$m}{66&F9KJnx -dGPiidUf>r@c7`(;n4f($HT+dQR@c@3X!A01lZS;9OfUWYlx%(F~~{{BqFK!o)1rvm8&KE{) -4G2HRm&jf#&MtP9|z-m48u0$2l%Ls=#gei)rk@$=)mqVJ3X;KViw|GVhW33hNKx+RT -03n4n03#=W9(7Njl5?!5-Gmi}-30?|Eqkc!S>wB}Z>gqtmm)S4V$_mi#>Tcf=eZ0;DjX$JGo*6~Zh70 -#-?N9r&N(+jx6;pVkg|-ah;?nJ50XP!j4@DfS${P2jhk_lIZE`MX#AO4D{o(@7tr+qejP11)Jb%_@I@ -$V;=zs{!nJot?5;@gI~AO5b}xBQueGvG??H=kza7m=L=5y+=mP9q2v(WDLV9uyb(s;z#)1W>hp5g}`7 -C76QIZMBtBnnDNLr-h}cEKze@Ei;wdYD8Wd;qR5WnuQlD1ocR3+rkrrVAhDEgPZt@|Yz&O4nu?hh!Fo -(?MHG*dKmx%h$2I`e1=blF2)NWmm~1-we!7lc9zK8fgKxmpaKkvyMLG@qDRgHma6cWr{V_WI%bSDOuT -Ng|!(40&6T0~B_~^yS%fsmW=&y(V015wDALyk4x)8AYrJnDHpRyzksNleu0}HPb+jH~n&W_@v(b*wXC -JhvSK7{;X@K8b+ -C-{^65*PS|3t@>5;p%$+n;mEzS!;Oo!|}=4;fsUwL+>B>9cbYB$@w7=E_E8do*$hY|8kw*rseY=51$_ -#|Ili5T=*Z#hr3^0{Ov>WA-#ME6Qzp2gl+5SwX4S80;&^yeet)iFCTsVj}PaM27?dfqab`Z_>BSK^e= -CJJUNEyV}JK6UQK@E$DgcUS3a@*tPD$$6FdFJXS%Aghy)GXTjIrd8hhEEml;daT|lp920284fQ_BWJT -6P`4aj(~l?K`PlbBQkV{HS+OR6XeN->`qiw3`jurH$c2748>4$b}jpZ4z{q{!DAUOb%=sG)}&I&OqR4 -`dM#K!LlKy$xKb8kYdGuG4NZT;?bf1_s=^1BF-syKnTtXt%C}jJ4H?FwgQpSJSnCXL~n8@77CF1A@&6 -U^;K`ciqpumc)YVTmO7={_1)7vv<*=0 -||KcC(uFkW#qpLmrgDsY;boQ_=aiMBn_H@~70QdiX#20Y_LhljDc_hIBr%vPXT%b~fxUXlG=9&cqJuY -{bfp9`X#&iv(l1WfcSv|>mEG{5n6d?dnfQrYC)6CZP-HW}}G?}-Bms?9u}+@ -OMljZ%e+$Cq8R9$jbI4U|(Hq~<9<;exR_%GY0iO@Cid0YL3|narmomL#lccqDmKRn$yG=>?~Ur{1%tz -Z--a1bv|OsH8pUK=4G58TGj624@ZUW_cYy{q1*tO`jG^BmGAp@@NIY%p_Oo{wvTu;WGeFW^4Xt(5@N< -K1Tp50i^(_Cr)_~=|p(T5@dc*l!IINZ9346giatt4cIVFGW==$aGfpZ(}+6*CHR9~hVXbx3I}YR)=&A -G-(Qf#dTIX%LIg=@DU983{#Hz`>Bl%-H-7^qiB{G=?oc2VO;*#ul9|+iy`&_jVN)BykO2JYa1qeo@Npq`;p|Z;ESiInc2$L%p|kbeDOV1e|PBff*dvhzq+0RrxPvI5OlgMr>0aHre@ -ey{XB)GOj6~wl`elpg$(rvh+}y0REVQj_3r2`lpFIn{Z6NKSDc%Rknu@x;G4kb-)1Yr1f0XIAlEAMc| -!LfbrTW0Wa$!yPjlG-pE5Ugco^n33%GX`f6G;Nv~}yY3bMx$`P7#M(RJ*Q*o^Zn$m{~_)sIqX;O4Qgr -gziX6W%yI|xX82JTZf$p@FWvEXa8Y2Ilu-PZV2#iYa{q@Z;6po*_r?W0V~ldt4t`exG%7Q)F_Mc{Lrj -Zq;a09^{cxVhar5wK30n3LUZSZ;OimDSmW!3Nvh9*bmR7*=jn(c#HQT7Xefa~PT%tk1S!rC4$7Y7=v>|cT}Gn*BzZlSPU=E5i -8dnC=-CR1$b6o^Pe0244?XqQcjL#eQGuUA -SUq=iqtNzavK@ZUnFHoj@daV$pV#JX*s}cif0qZW6)yfO_PdaDYNqW#bkm13NCgh`t)gCHTl}z2$P7h -lVi5XQ~GvM#BF+c1CuBvqDcUjB?lXJ9vs&%y6fDL%Ui~}P29bMkYG>~7;RKui*cM@Nh-qH#-LE=s;%T -w<)SjCRN7Fn1X)`aA$mFNDUoe)Gf_ONM4{S8Cm8NIdr<3=3tYg*J)6t!dZ#cBQQV@9Weq~Y)e_?kKwd -;xBuyu1d&*(ZqG&Gsv9B!ZB$9B$;XGTRv8v4rWPyk)8JLh(`Ykq<4Io4eIK?Yvz#T>>)j(8dg~)dB`e{9JAe{j!X(k3LzR>gR%diXlQNoS+hjyw=e@>{@^`E4 -IVyy4|N*iyHDLh(QKo;okm69h1-TR=@D08Ot%;tzO(iZ -G>RJQeOOyb9lnFwg6g#2nFdzkM*4*=){=0r2<=VK846{P(Kbd$>iLx0{6WP*C&IDWciD1`kPL$C#q+D -I09LzD_8CWenzwQqe6-Ky31=KH;-;BSM>Vpi7!44TyC<~zr5D*l|uLEGEi}*%R1XsQ$A+-cdCC1v!QIMAaz7g_ -x58s@>$E*`)Gx2k2r%F~@h5!A66|MyTU?%vNY4BqTU=(IQ(ROP4^~Yl`cI{3lxl@c+P#+EU^cxA-BTY -)s+{YNP0B$BvXv{>PQ}Th5_$J7kt}L4{a75hLkRS}?q$ujM1gRRW{@LM+le3q`@}cfoD@3o -2T+O0km2y&cQ({$(*w!qKcWV&&z#u2G8AV@U%{SfKU1ZcH{;gou$S}b0uScgK7QA|W@b>VfQz+b)MDX -F5G~h<#yf|;`Z(k&`1Npl>bYs{UNEcs~(xGPg>6%-O!)(eG*igzIKo -r|@F-c|#uyok~T344P3xxHL*ldko456E%NVrH+bjP8f$QJ$?eWr#wt*!{mz5CHGbyx$THW}ZDwPLp%Q -W+T07dQsl^T5Y&0;qt@5#zn^<6tl_eXQuLl@RyqLJbFZ86hBCurqW}Hbe$7i(w~;wD*sr(k`8aq!;ku -`BBI3(i4brYrQ`Ax=U5D+|;9nKhn{mrd!h0C+VHEnSyEqXA0R81_r_#z_`_m1}ouK>pJk=s9~A7P5*& -W0bZCjYCEu`5#AMu5H5Ivjh^3AeJ45FP~YLJQ*+-+&3AJ>-s$vjyWXp$Ansi)XRwi8Cur_1b(jV4h>; -8-s8=IJ$N)h@fxxFu0{LH)Hece$asu*FRW<|jFlYv9NzfgP4a3osbZJ}Qq)DBTO_*-Lt!?Ij6*if>&D -lkkRk&pp<*?>RP^P}*Wqe-~HVcTsJ)+=Q|IjT>hae$3U)!~qs_U>a&InfK@!N)Ignu -)--@~`*f#8nq8${mOO#aJBp?x0R4&a{&SjlI$7ywnbh*Ie1_B-IWe2@YQ*)6{J1p;LE=WxZuGwr49UB -;Stp2XHs3zbCJAIL~{pjj(8`9&l6!Nf)l&|Z)B1kc*vSJO2I0#puN)Ksq&hRLZzheR0$mMp?$BhO%FI -!e#Dn3_UxlAewt(yy+R@kp6ntWU)7>#$}gf(^ny-<-%>G>KE+<{@|?to8A+0fj!RkY|;ixszR*K_m7! -c-m@MX;jvT!h|1SprGYiL9~kWw5h%3ipjtT7R;Y%Fg*hT&HTc4U{$n -7(O~ae|zxy_2C(QIsftC?2ukd1AiOw-CJ5san5aRD>cnsbF-p`Ps>?)VL`IppL^beY4~7Dl9(NEh^7< -LKh3uTJ7U29L8N?+X^$os7_0REnCH3dDrZ20qoZ -95mONV_4K>R9)O4rMr{4XXo1fa~&?w6Q?OqPaJs+)0baM{ccg1-lDMxV!*DvBZ -8Ytgs$6X;kU_q-tpHI3V|@7VW`$mb&Olp^4_5X8d04e!7zn*1kS!68_wghAf#vt{BP^bJD60Gr#RCql -MIIY_6M&8p-$N-fHB^Ve5tqF*GV~jxN{Ud>`gv}&~4(KKseAsx7Z-ug)8*mI90tH4WfDs -nGx1iMzNg$&hNJGXq(m)~i9~h^hbjtIiLp^k<^8vB3=+Ev5`neWBGW#W -YITc~3`{&3?l;~0_e0BzjpwyyzFJo@OjzkNCYJk(^RT@HpPQqPbVypV^gpN7ACG -zfQ$853%?++z3vPxc-K(C>Y8p1%EPM46|6@0zwhe$3=dx@L_Gd`Q?6Jl|V68vghZ<@=8xZIf872Ndh5 -SfUtAD>jthYerAY4Swcz%q9_jX>13oYmIX1H3$_PZeP#R#5J#t>U|_ogZyQ<;lGugMh8)Bip6L}G8sF -xwbyQh)Y{>tzh+}SZNRTv-XbP$qrJQU>8g)(g$WkS$y>tg?!;JU!7`qjQmP$h -oLi_?JsdLOi_4Xs`ccA72gVK$^#pSD0)*Gs*{ztbzp{a -4zzL;de{o3_k(8L3l1zi{`vy+Pvrkwj>_LfdRAi2{Uz$FDd{$X;!z)RpBncNcQ~cbpjRGD-~On?C+P*j6Ti_sUZxY6QIb97;X0T$(<;gJq?q(tmNx|SB@1F@1#8Eu#KK6KEsKee`=%U=qZoHv7H -b9VI-_zCm0w6Gg_R4O0zZS%4{~PxlE5w$TEOcvGq|Sr&yzD-a6#`!qw9Pe~I4IiI -{R6UGN<2Cm2Nlza+N{KaweOX$`F#By8#(b=uR`p|3C^|mUOgG->sfmVl5KB9BW$4+#HIC8C+Avo%!v1 -b`O_v1n^Ck=*Ica*zy5>x4R1_1T*MpJcB~W|&P)i0~#&Ue@I1vSYkDMwwuE__um2V&>e0o<{Qbuxz+V -=uyw`=L5-@itW@*lbw0{aoWG3abvW2zfb5kmWWgKhFLzsux~ZgTLbz692(Tn)ciKMX?sbb2N}u&YN4Sfy-r3(E(}FsJx5*c%ao*)h{9FGIzTX< -4b><%&bc;0Z>*rjM39F-;6%HPxc>&BLGebhk(BT-#DwNv7ByL9zTOmR(RpUge9)7-uT}@DJCV -wkq$ucTjMQP=+jpA>Hxo#6AwtLdE6I>_O%I!KflC*~*(>x8Epx&@U}VG?VKe3|PUhuioa}nzC1svOuL -aH^sIu2_UXrPX*Z}B9?O=%JBX@S`2?+e>uI`Abf9FX8Xizf;&T_-Xh3wE}r|q~2vwzr;Qd{ov-=Gf{a!8=)M<6;)mK8e2FsGBy -sW-|k_BhI!G&QTV>KO8?8_Qk7C_67u8bvj}LrX>Dxl7T%17m%LePQPXM~oJe2tBTm?+j -T6qQ2Mjy%(sWy3*790tk2$}Slow@+>^@*onQ@B$HDh3YhF2P_Z^Vs9?4;>nsV`{+l`Fd30t_QXo2CTD -Vsfx1H-2*jshq~?5Slux5!=vv(Wjg_tFvJl}@pat5EaP)FqhsJIPh -^Hvbf<}N&fKFHV~31Mph(~H|s$d-{4}!Ea*h -edBuv;+Ub}o+hl!-&K779_vChnmz`#Sh*!zdi`bv=g78?MX)+n9+Ebp6Fv`;az^!X^a&%54b2ZeHUZ! -ajrCuKH%de-jC8O>p?gm?hlfNiNxAkL%|B-o&-h;`VsP=jY&>I?;#kp&H!wuU2 -7#J&mpHzw_z^^O0hJs7mDW+kZpiQqhryY)mmRxK3XmPLAK%#$7K^G7LfMdvW6u%o4r???P| -BcYZlJ4qHjd|f1&Mx%J2YMT;XGN)>TOElAm~S?sN&3vZ?j~|k-V^>V-)GxqI9#;IWzlmfz5Kxl$~hpAY)BAEO1`0k&;*b!k|rKVSqeegLS8JkVt~=sSFB^9 -y+?DM6gVW4|eZoatR%2_91P5PsdF3pI8PsNdIv?VC?ow;$^;I9W~&Zs={IU3}zq0_v<4lQK=0U1wuTM -=PCUM`F9AjQIYuWIXRp&iqm)1R=Y;I!u&s9%imBkNV&NiDgO#bJ+1r1L>k;C6_KnZ# -f}{+LSp)LMYcBx>U!Uab>mI38yXrW!H>wZVOyL8%7T!L8S$CKkk*BnH{#utHIQC_grhMyt+R7(L;j-j -Euu$3HEsGM<(Xn>{)7D_E_S6B+STqW8%hu=6Eb4SS`2l`suj)D6&?#ts5f?W+VU&(u)%@B0(P)>KrnF -Dd+YU46O+4oVf---GZ(Mt$D?0B_;hA>`#%nd@-A!lFDCSS$!@1U~$=XBYBPM7@)PM7U^Z%J#N -D?<>!V}9`6VUY1l~El*&Cb6ka^@Nj~~y;;rI!NNZL#CXWPaU{>x9nc;wwR&m;l38Ez~>a_8Dq&44&wV -kO#rZO2Z2a>KGx|k43a|{t@dl{G6d|5H;V8Z~HHU*xdF*^PN9HY#$bV^qP%A_&aTq1PV4oa{^1FmH-H -E@gqECfYENdH+AgDQDQ=}}-CB>)-X0nA#u6f4~%Ur>c|t$KwQT>!m^I=C|AoFX9FTxV0tlToVz((Z(f -Lj9y@_eE!FiRn;jR^U7+|JjgcO@ph9Io8QSQcA}`sYcjU7*OnUR+gacuO&4ai^-U7XqCh3`Diicdc|< -cj~^Aaa&`o6)STl*#M)?Sa2NYXvC@YRtZ{Bu~JqfSLjBnYoK3C( -MwAu_=+K~txEigfnb|Kr$s1_b&+Q3$jC}+e4Uh<99Vz$y3@M -&lL#ICfNVco*%pMDq}9ltuETv@Ui7sy_4av-WeJ8P&tZhh+wR@f)2uTsn!vM}daQ8^T_q>Xr^XLhMFgnnJPq!SZZ;wZvq-33h -@@^^DW8yvXp6r=-_I3ID@dDe?lS>S#e6mG{RoJI;aVB^1lh>Etfrw?QQhf2Q)ADWHSRm)8!w_zpYhG| -P8+RVoN=hd=<_Vtkqa;Poo;@CH+WUP)&JQpm5VO8=|&pgXy+?`&dG71`VlTRS>H`K|^IlW-VIwz -sV)oFNZ(hkpMjCqqZajjH?RwB9od!uEBPX -`-2) -&odNIQQ={gWs3PRh0JU#?|Z1l^A-SUWr-|-=#F@hy=;IY4I^m2ghkUte8+0v^$w;}46c^juYGYXj1Fx -~cwak@yW^yNL+*19w>_F&?7h)#g@B-n{|Wu;DeK| -Re29B6w~YemHlwX1cdBG(+TXOVHWb|TwWNk{%$i&1wbmbC!X327y+Hdfnu70A#}Jkm>E7u`VH4Jnm7# -RPYDP@k(K5+b^7SrUyhf}L@lc7|n@RJJdo!GfeSjDn){{_re1fA{Lu(VtlnTEw3+l>h4Y%D;i)(;`VT -y6w3@{uCHNopg}<0`#Sk_hcylF)!!Ud1qDXXsPjBP=g#(O3(+FD(4D}(6}y(R5@ZKLo}8~*o$P5lbw{ -`+voN03UqpHrf2c-x|XKB9NSUdQdMdf*wX=wYM?u0-a-aV96Uclvn$^@Q-h_Fb+7`IG9V}nr1cSGGg6 -Za!o+g&w)*-U91;4>=WzwdSkS@OX7 -l6^s!Q7It0HYtfT#m6FstNT+CO8Tcu)wZb+w90K@PItx4-|6E;`C5F6V{>q!Brbn{s|L^x*CLZD*qIB1VGJsnnFXE|JFfJaWXzcqTH&c5cSJ>@+iQmzlF-DS22fANYgJ3J{tmiPGH2$A?`HXv~V*f8rLX4G8#I^ -*~Awf}33+=AONX0A{2L@6qD|LD%n&Wn>brw4D3p1(ef-X8pL&PPgZsRV_UdGqxSi!gQ+rgTH-YyBirb -fPV7E|CQ(5XHRfkaUxMe$RylJXtYab>bzJkN!H#Fr&9pa(t);-S_gzJNX)e)|1@69mBpDBcWI>eF>tx -K#mqVuWf#fU)1V1-oiK6gt`LW>&tlTK<57eP)h>@6aWAK2mmD%m`)?_XZ*te000sJ001cf003}la4%n -WWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mUtei%X>?y-E^v7zkHJm@F$_fS`HE4_5CpyP2R$J12Pm6(yN0 -Y&#SYc}Jubb?j69>~7~@B;vyr0oD^tZf`i{@Twk|k9ElbU#qVY2FVA3b7eSpPiNN$))=+cf9!dWW+a- -E)*mQ{Vz%UGur<1X$7d3k!qg8G)emRx?WeOTIAO9IZmDwS%OnlA)aSVwNRd?WOm6nf@}gG#1N7yaiYac~8Wy_KJIwl^N+HG={s2%*0|XQR000O8B@~!WuZk~a&H(@b -%L4!aB>(^baA|NaUv_0~WN&gWWNCABY-wUIUt(cnYjAIJbT4gbb7L-Wd3{sCZrd;nz3VH81cjX~F}9w -D0y(Tf1_T(=p-E4}AjmSEXj3FZqPO_(C)rW!rr2~5P4YcGKFL#9yjy^@l`@ArXsKR&iWxR#Q&`!ibLw -y;$X7GC{qXS?_D6(o9UZNKovrDQa0p*)Ybrs?nj(O9i12l@T5osjg3biORIqy3?jJYzPy2_*T}*~PHk -nNBh5Xf6M;>bHK$%MYR#huBNG-fa-(&06$jI%BfM86WA_A;Wk?%ZXoC~cvhb4T^q>xACwX-MgZR;dva -|lVTMXCF^_GrSunEhakk?ZalgcSi0IN`$khKAu;VneX$>ucw%8;}*&Z@crRD5i?eu<_bJ1OggvXgd>&xe9~%-gPm#4Rf3IX88W#oe~#2%q$sb -9Ct+k?pFiVgugtty9AsHvq_3pxGm%qeOL%2s>dWJ-CdeP1uIlm0sbLOfN9ad=Bc-Y7#c%yD7EkW~9K} -Uhcx}Z2DW*e6cq*A&L?|O+c8j?+FqTviP56kr4{`aeAKW2uK -)(LQkKG-CuySl2n-d#PX=J7X=&8k}G<@TBB+Rfwtz+X;IPfpE?YJaHn?R9IC7c+DAS!e37l!+X> -FUTt5+5mxiPR%1;5s2XWI<0&kT_C=WGSw0JD>me7CRa)>MstY7YKmo3$sKy4u0xbf2}?Mjm9#2EXdKO -}We3mHWD?io!C2`jf5evSLuF0%@}vi9I=K>%+4X1Ap>h`Q?nhE`I;lv&H+5Z{NOo_u=LD=G=U!J9}b3 -uk5}xuW9Vdx~}SH_3@3Nx(T5QTT%;@a+3Lox5de-J^`-_(^oHRx`XQhu-6XCnu{SYZ^oJCojuSd0mw|Tk?+1cuV&%LI?JFd29}7+`BAq?AqL3+YG{# -#DP-lbJDzO}D8UiWyAGSD=Fp!73|I&~A@{2Jm2TW*SC=Y>3Rw3`iY3-{*TfBZU{zbbB9&Y6e*Bc4(GX -RadNO^`$iyz;v*F0QsHG*1rmJDb~-LRaOez8h}JFaX0IOz$GOE{v1QKmf-;>Z*#C-XdRuUrKv~|3j}V -2%*(+5Z?e2-2m`P6Kf?Z0w+$hon*IZDH_-ENh|~48-IzeflTA~v=4P{Q+PNugxoxk{pMLwz(O$Wqr?11eW1;#}rtsxYQAzrJI?{?)ea> -QY@&I$YO{mOXm&KG>li6mb%a=<}iwXSpx37EMO;%&e~M>V_OPu4HgcR4JxHpeP9`7?Ry8YpnZ0JS8GG -o0;?T2#0%TAvLpEEJ1z#!e+mSg+)Xo5QO}W{UI_?eZ<^Nt{yd4q!|1G0u0m}BA(omd1O3{8S@U=vnfq -pnhSk&dB+wuEr=lyi|#Dhln>jnQ-k)^>?)Xz5?=t#6{spoto;m%8;vIO?7+e%_km$B_2YGh!A#Y)vhS -e{&m8g!b_{t4y9heS>ZUS@qh)TuQS2$r?A;|Z6)8TRW!j#d)5&UmowE$2`W6f{QIo(M8lw+#sMdnVciX8DmpG;PJ5($)tS;`8EY1!H~ZC-M`vC9q^$7D -W>lYxPbsI|`<7?i{od94ygBmP$9qSN}z65Iva3U(|fp-%T4;ZU1B)o1sT-BTAN{qesV^|3i%>U%MN@{ -NA-jCdtkHn=~aA7WD^slzZ6MO<0oWE6R?p-trzq=rka;Nh6DAJ+32+uP)`@b`KkTf+H##pvY!G{FeRa9 -a|JN-x-OwnW7{NxGYbQ!J|SK-iMhIIU``IZA}?>6a|+nzg4#I-*mD}c(4xpW3HCfzcv)thm0JL_UBuv -mJOa|0x`A8-f(r`4vtAkIh`{?nyAEU*4OmuSh<)JDgrmjR8?k^eB?0GBtzfk|WoM{ALb~?mc19(hmanu7Ig)x_xSjTN}TW==q{^pi% -V^IJ8q>r^SD{8c9|xvC{ZQpi_MIo2cA68{`qfpcK7( -Dtm?FjKox()wRbz~Jl!sGooCTzSvzzEzQG=Ybz3$ey#KuKUraEtQBOM -WW^0Ov(HSY+9D*A_MxOC%^gj@vzO%{7*0TybdW^lD_sBa?Y#;~A -M?Gu+Mv*`@WaNT(WK`02Xu59n5MZzDzpWN#M~@9EVr{PZ6Yr%;@q{$SAG+bytV@ ->8t1$tJhc8;$Vm=N8KBb>CwAmTs1+hO<@!SrFE27tZTufOQn)}5Lk0|E!d7*eOM^p$lAM%u&PHlv6>g -afBwZ6IVOhRY&YR^Y=raVCyxu$W?asURK#kP?c0mxBQWDo2x>r9iE02SOWEH@ -!%3$#f01s9K#IGqY#(WO*n3y5C#-!u5SbiocE1%&>swbv_2WV0RJizai6^-XKYYU2Rk7YHlH!)mfE_k -E4x*QDE#gBaFZONQWdFc&>&f*L~0s=7K*RVeYSCeV5WI5{eO@gY#i5n9MqOica1D<^0K{q}pgX%ooelQ -sEZ<$Qf*u!nibWSTu>?2K5r_b|Co%HM%G1PFmRx$E!aa4A;yCiahk>_<5jgE)Bv`s)!#RwV_mj(Ou?{ -u|bN_fC*f7TXXvK+i(AKxV##MWb%Sf3sJsAH{=vF%ws2GMkT0@3bir*bCJJzXJdQLEf->tEhI;Q7MSa -*y3u&PR}hIpOo`wkZjOYX#LkcocxI0h;#A`}5qE@)o31FvLONezx+A8O0yrYUM>_&+rZw=sml+ZgfO! -&1m^)?p{`I?L<_+uIpDymFkx66{6>@2n0rD@6|z{F#{-t0c5E3CcT;) -dcxsZrrtdDC>Z;i6r>gUA6x5MFv)R80fVkY7DV4_q%6dz9birI;!w*&dc6 -L?sURkE;Tr0jF}a1v@5loqpd_oS%La|Zo-}#t|gNEgiSN5Gt?wkuTro=4Q3mWe+c>!0Pb(S1j54*0Jb -~b0{}4?0U#c0O^4xG-7<)PQtWZu=IbP=gpg~JO0a=vbFRJPm%8fq6oREeO5j>cC7d@dw(E!Yf(5j9dG+oFSF#YuyTex}mpT`!0_y5TjY -QqINT0+W`plw`m6iFW`;KP^-pfu-`?U#H%nHz8}%c`;GPe6_8!=mm=_&6{tpog~*Jj!t@$(ejEs_oYH -D+(__m#K=7?7@fSOFAs&h3$HVjolndQI!kRGk+Y|m21mYf~jIX74J7y7*h^bz)3L`CSkWt#{sjsnJnS -gvk4{qh?veN8m8par$oavI{bbY52fu+2ut`r(w8*`?~rB!Pi>OfFw -roa0x0w3r|qOvzV^kcdh4^4&xTEsoUSF`7Llgg>gW#WB9)4v}*te+5YX!!TO63_PP0airX`N2HgHKowG=r>GT&Gu0t1qLo^offFY_Cao2v(Ulgx)F -{M49UIHlics6wd!*!xjDGif6g!+)=)`celn6|Amh$iVM%W`yv*$5rJxszmnKBibJkU{m)J-}Zfxel@G -m-zmY6`eK!F0(ZxTkxH5^qN2$7NX33>;!%?qQ>$!@#X7h=I1BBOj4egC@AgQ)CPRX_?^sw$W2k=Q1(I -^QZR!K$Q(9%7J&h64gxv&MzK=3$jt5h$k|5#CXQ{{3XR)N{Nv{-CAgCnS|`-kM|fc%gc*V)$IcdU(8% -V_+@i@MN)9|_z-)|99)M?vRbN=g9frNGKpKTc?vxQz*IVo_=o_GGE -ruvybAm~{!~Q8n>T^oJp?w>)*XS}Y2gV;?mI^r6v@wE1CgEQdb22{R@hMbuqXC^F*<%UFfjW0@{aNr% -t^ELI`RINo1o4^&inb`Yd|(eV5}hZvwFrWp~0xVNb9@lJ$;^4nO=I=a+B7!w2d)MZL6tEh-MfJV@?k=Z -{&4D2Qw$v%~Wf-;5&&kDvV<3-9pKr>~OV2a;KZ@{%i20?MubA9`Q|1l~LKc*}%bRO(AnmP%Fm*ze1gs -i^SgeETj`34;kXkpIkUK#F=rqEvCqB=XB+%X9nSkCA~$$!PIWE4jR)2YF` -(-gSb^vN^Wd-^@T&&|8j*}T^R>1r -~Tr#fOstoZEYL3@jef3A#4|C;Zy_alixPHyj5-Twsv#JL0b`t9QTm#_Zx9P*g}Bm9$74L;`(t!U;h-= -dydki6ThxN(~?QUnZ7@7&z+o*=4odQHTOW>h1KYpseB>E^HnN*L;km}JIB&UkxVmOFwB+n-+@0e_$P^ -#3Ap3WR4ewiaSD7InqGYVrKti$A>n%ge>9H}C%Z{KI?9Te-Bb{esERGm}I$n7O$~!d#7W+}Y)v3l~!? -I%M@@@)5JSR6QOBi4$1@#UICDF(h*jEb^Y0bo1F)XWS2!9H_#?@CpNAJhG3Mk -CrTJdX8{-Lw8Y4H;ld;08w$}!IUObw#%fF6+F9^Hu5=Ol2)eHDgL|Q`JRga)wB$2)-+pOw*?_1T>z3S -rUz!M&-cbG5EE@w{T{mRk_Pmrh?HLrY -V-P>CI8Z$3yd((7mtvV@hadq-!MRN(Ea-Py^{Sv>Apk4m)VNi??lK|%?3>?ES>&w6i~Z_(xoKvd#t`9 -BXKV&`pOI>jBK6D>W2t|(~*osL{2{ZTa=b?fBY~d`S`=z=P&;F4`n0^dI67ePzw1;NW)n06T}8M9WRk -&^q#iHr8pA7m%>{R2BrN}T3s>Ff&Hkl5H9y^bSyRLdqCVgK(}@UFEIst60q>-q7nMVscpo)W9*8o5Ux -P?`FJ;a*G`7(UBE}wOBTogh%^S|L6<-r`S!*PAoqHIxl(?&hC-s7~PwGDmu3kd~XVH!77(dOue<5$DcsQizWpX9SCl1uj?9xWPBSNpC*i)y7^*w@4QX=$az@&#wdU3T%HQ{!Ye -(=_2(tT>{S~aSm0}KSz|E)ztW*sD3@2k@T*9o{*eaj65DiD1%Bw+S6R>Z(D!P{M- -!ycKvILo@%b95^Irgp}V^f1*(9}Hr9le*!Ow)9#*UHAOcFXkMV^7hvA<*&MEIMHejfFI0Jl0>af8(WYaj$WsYNe&+&{LUtUmGXG;CL@Dcupe6F`sa^!dP1k;^5GO*(i9(djeVD -NJQ=GfUIO}yjivFCTglgA702R?%9y&1;w1IGn6MD>D*o}c*oM9=46qv;-2ZnRh1*(Nt_?D`Pr*W?p|@ -BWHG{BrDZos~j7)7mSIPrF#w{^X{@@VN -u9$(6FpJh=o5MDR0M2)MbWjJhG)BOI>B3xOZ&ASY6GS_*fF-B`h7JcsZ#L`YYKKv&tE -dWOafLVowf%4_ZIb88oHZ=fI%j=EU9^%yO@(@_kE|#QhTK#|}#`*q=Gn+DIiOr}11Ca0ctqW(`D>d^r -7L9Ga*A{-`Z^?MYTepeu%?ze!PRwEF0Psd6!d1;y<}`E*bXsZ8lFIshN5#CX#mD|P|rLaqvoMm0Gp_Q -peB6MkX*#d4KbhPwl`YPbp+`x6}6ufa7zk&jW;+~8o%8E#7UC9+n=u90E5H5@@%`<(2xtiu;@EW;t^< -or#Qh4BNRIc))nubU|XMh~A#6F-{_xFy`hHo>EfizIb9Kk<3qUUzit*sS-!jj7_rneGAMp&y~0$+Uhh@4JL|NmpB+3X0XM&!(!b}w -L=YLbxERXU6bGzRLTS0><+YsxHy$e5bY`{NyqQ`a)sB!Y|)i(WMb6j@x$Be>KLT|Xa4p?$>ZF-A~8?l -QHu3{GgIiRMY9J|=PD%XwtAI*0}llT&*PX76SlY8D|?glF#8Mn`-9j5Hj{ufY70|XQR000O8B@~!W@~ -Phu=^g+8RA~SJDgXcgaA|NaUv_0~WN&gWWNCABY-wUIUt(cnYjAIJbT4yxb7OCAW@%?GaCzN5{de0ol -E3?}z)Hy@l}L2rxW1fQx6iSi*4HHVv)nX$wpXDj$YM;9T7vY)_IiK&%?tn%AVJ&NcJGJPdu=2V7z}{< -o&nrvqh}))WwSWF+-7AlAHBvWYyGu8+sjsWd3?Djn7=n*8!ujNu+s%+$9KgdOW8P^7dK(f+1sp4XJHX -%X#kzZoU?a_dj}uJ2SIULurQsmy`vAOCx>r-IXyZV<3nh-*6a0lqlgQ^icEp@1R#>me67guw$~VZD$s}kG!r~u1wp|Ja|txYCjl1wIShn-O`?~)f2*=Dks%;SWo;gWm9*30WKcRv;8<7Ifo5 -r`8K&aL@-f{jG;sr?`c3tV#R8STyFn&$%c!F(g41z&O-EX}@}B|N==m6-84n}~4ECwkp|{NF&XX0-+Q -$$3%cDYM(rTeTTSV`C$X9<-Z4A#>dOfZy_{EW&BRWy8tw$zd42Zcf-Qg|rktlNT7>B(pAjc0-id{RRi*?3SOBJx$NU<;1B)RdY?W -u1dAMYVpmP!pq+w35E78r&2ZV7U-~sk!J3FkG7Cl0sw~D4^oXmQ_TsFY7(d+GtAjxic?ho9n)*g@+@k -kK_LcUUs5%e!hy%&#zr5y?pVf*zz8d#Xi@ZrO#Mo-C`@{Tnp@;NY4lj2swCmy$fy -@3$fF3P``>zn=bd^kMSmm&14aumr6MK$B>GiHkCW*Qysu3Z9BG=ha(1*UKcE0$Wti*VKH{tf(K>D3bU -Of}D%2%t6s;sXdR=8OUedY#K&a_#A{`bQNB5+$|DmlSz<^RT3Az7kESVasW(S^_oa1&{%%)n_vcYQ$P -(D8Uf430siBAAV;K}44{QqcL)XOkD}@~WOhpsgs{cJtJ>ehpx<=sAQTf(z)CFr0qP`S@7ZL$_tU}qgK ->asHDCE+U{Z#)XLd&mgyznI$=o6<=QqDMb|4rl(ko)iJh -w9njkuXqWnQ%a8e&JCu4F&&wJpeG)K4@wqngs$>AmNFryMvSP#?32Eh~j -(_W_{*c&lYm!x*si6NMG+x_pG&uHRbXM85~IS>a$)0KkSlQc^;L%~sfJ$gabrB$`rv?XLE+$UeRraj0 -N8=&d!PdMKw@G$AM~qlvBT2620FrdcVo6S|%W7efIY7)B6K91G^W!i{y^MW}*G3tYNm` -IY^i5ECm#<^#)n5AV7rV -Cfz(c0N*;VV*n6NYTF# -TI19*mS4}ZjMLLvMiOZnR5m+`@gHk{~*b|A{BD98;WuV5&7jvgqI03Y#C@*Ai?IW>ybYPl7bKC_!hijAb>P;fdG7YsO)>f1 -B`#=8Uz~qwMTL9gPdz{ZH=kZc&lGGc$Oyy5ZFkbUdMTsI)T)$8$ddl-gw!`qSEuLG-|Wie!kM_&h8V9eUA3YCErLZ)yztBy&_(ZN -(!AP(vgu)0%IhZkzfh-fBz(Kip(Ay)|MW7AE*xZm<#(7^U!g($zp&kD?wNOeAeZo(K-zQKFe=z)8Z`s -e14xby=RQ>{eN8Hw#eC;WR4?=4E1oFNt{b7#u6%u^$}Eh%tbA)Q(x~9v|=Tp6>PrsJMIo+cU{UPu@u? -`lTUoJ6cu?fz81sm*zuH%}m9+bl{E}Y%;qA%p`-5MPPJZrL6km00S>^7 -@-8Vt^BwZ7Qb%c4oi>G;*-(l!MV*yW}&Vr}29(&pX()=_ntF!Hmi+au4=)2Ii(RHJ#LoAvLWB^nLXMG -)Xi*@@bt!09VJvln#1E_qvPQy`07y$1To -VHhGCT$u$U6yzG?+L*pJDK_M7tGjS{`~Dg91{fFWUjp=OaNg{{;q3ogNB7?i#(j)Z2iJqwztXIzAJ7vrTxC8a$W}Z -x2#%*Rt+V4%S_kAE$H=+Z>4_4)T!sbbA_`Z$N0Fj=jY0yhE8;MxFcJOo4a)%^kZp-CEE55r1OjZILLg -$k*Y`*=s^@vU@&}}%9*Phw(J3)@IUoh9uo-G`t-gnB!}e$K?X||ldedj0pFHu%e*<(2o& -(Q(#s{dpuWT_^ELRecQq{f(T|u3g;UT*05L0Y00|9*v-aFxhpwNUU(3()7iUrWx<$~2gTGf5L;!z9>8HLI%l=86 -1m<$0*DAjz)kfERgsyZ;<`87`!sF=Jk&$p>ZXR1Hr=_xLumRC_hYDYrk#s+fq8eT97Vw1Vas#Fl=_n&==x=FF`T098q+3( -a6hGO82;3-h4eYuwh(CHH;jx&E{c}P^LsuFLg#U;l5QcqXHLe_u$i_(~iZRGanmT`UQDwIz7#3XAF-YIvG=C~YL~DyP^!qQ8vbJ7THlA|?mcrv;~(*Lg9zsZCBKLV&z7* -!n5DaLw$=PE+ioxA{`mt8GZF&NCAh9lR~nSsbgzviU8-PsiVME&a_0&56X!td(BGQRc5VO8@Y%A^q5G*j9B+RfC~p9sp;415=ofmR$1Un)1; -Ss>-tp7)9Dfdf%_SSAfD?VkqPo9#}DH^NZv>Ins|SLJvYp$H3*-(W0)*%eOXc@kb)>0K3@nZ@};5xOT -Ijh>39!edXF-%8{RDcYf-xe~ygP@4h0dNR5|%=bhAKH$_F44YtJ3VMoO6#$9T7&>Ci&~K(@;sWg9_#b -k=msDDUo@cmP0j&dh7BVb(d^A4%gi}IF3+li^l7%zsO2n2VU`*@b;RhY{`0MyKq@y&pZ{-VOiax -GOI;1Nl2MA=x{rMN^L8mGW_ab4jhQ6%SO+bHuwADulFJ=jM*lF3M#J}}USWgIwOufa@mK>Yu6)?@xtF -+gafEU0}=qG1d^jl$Z84ukH6T;1tByBthc@YxZ=#l1^Ojur(Wbn|9l0Y7$ypg;fDFSH)(|9_#6Qnq{e -X{b_z=m*|_J$y+rhYDYD6OuT+;#ge+arGa -mDBPz8Bi9s+tCaFzAZ>FV6`79dbxS|Wbbtpr{%QB*VBeE!2OqtupIHJS&C)+66h55RfA6 -*i$4YA{{0`sjAB;~4y|y?r8ooLF;BJd?bw~Td4}0%^**`e`YomE=RWHf5<0ES11*^N6XFNanbf8}FTW --EkqMcrLUb=z6`}OlG{UZZ47_P9Rw-~%#Rnc4DPGe>TwXucW6dGUEvKC8? -^c?@J;H%TO>WExE!goRIjaSztjXpvgZ2uBGW=*t7pdT|lORb`+mLE}8L$ -*tUhtW3)RpSeyMZ};eRSf{gG(K458Y?Cx3?gtIBp$yb2nj%0b7A3Vi85MO$;lfqyQ9+g4-s3x?p3+St -MWx@1ZJL+(D7yn9dT^F3!T3P*_qvnKgS+%#GP -6{>FuG+I2fECay)inf26RT@|f^&p|CyHWoFV%9M)ZOf=61(>z`8kwV;1gHnt*1x3jEgIJ_?IjOWfhiL -hq>8{8MYz=ov$pT}tp@jNoX)ZvK?gtHhY>p(1F?FT5L}kgf{7*e!=l|np+J*%%^2Ak=Sn7ykcl1{-;j -lbNtRvVc9ttic41V*sY-^eqj2?{It&+mI>M-^tW=0uW!W-p8a0^8z>y;B16Ea`aXw?JmO_xGl9?}DC! -?H`oQVZIyhAcwgxCCA0~E|^9%99wbYsGtt3pla5atGave%ZGpbe*U8);RUDM8^8m$HZxv7!8hz&V*SP -f1H@NK;j6M?+&4vb9mj+6DWrIuh3~)F13H5erZ~Zu|b>&S^HwTAiI$yKYce-SUVfL2j#?K4LxUgBKcl -!(hNq%LKH6Q~|LIRllqs)HaH=3wObHkHN!r*@k_qC#bYL(m>N%9$-!lq>vUUB6uuHo>xbLn-!61ftl1 -yY&7d3n+~uEf8YY=s^O~v-3J7Z-xdW*7EhPrQAq<8jZArx-AK`bvzMyG;!se{^D>o35-8C}5~rlno#p -{mXm0eCS|=_>x_@9!%&eII9wxUDY&EB5t$Bqz5t3C`Xsy2|PYroGyl+Ce55DTwO{Q&2xEp}&W4qfKXy -dW!^co8-}0V@hi4?Bju`NlbT!XuRRk -s0vK?hT+HQUPd=F=>zGbu`VyOb*9+vO@|Q7$CUZX3gaXtY+l#aJCk8FnZNAd0FE4VzoHFeTXBtUq~cW -MPWls*#hove#y2B3b9 -qzMJG`{QNCJHzc4WY200}0A%M6(z$eWmg`4p36hgse>Q*+}j% -V4UZM%9uda5cz%BK{9NFG4E|?i3?BgCRpi9Z{>ILWi)W93(Lxmila@h;pRR=ZZlWy~Gyo{81C)`+GwD -0ve83%#3M{6R*b)u7$UQa7ma0v5QS^tS(}QhE*Rr@SMB!NE;Aup#!_zyqyIN6wB5>g+r0H**BEFAoLC -y?x5vX{>MCD!Rz-X9Wmf< -+je=U&nH0kAIr&Od#^#=hz3!qySK7mu^mT(9WfRgi-03E%4F0 -@Dm7dHQRyWaLJG3nC19qp%uOl7IA_MvZf&b|s9s|Et`mQ$%}bo0%-~W-TuHTvwdD;`Ma32!kVp^TWn~ -o#{Dfx&9zYxp}QEfb{+I@$&)nlK|R%AFkWQkCu-=c6Ti8GI?bl9C>EoQU~44GbhFei;yk_`eG|Mge{XG$wt@|vm62&2%raIP=M)*OLgFS)M|7PR^S3sqU}xw) -Fju+{40iP@WVf%kCR^ZJAEpIQ@XNvbt)5zRD;YDaC?>&U#t-`J)2C17>G!7m_BgE|+>_LMG7{nJ9+Z;~i>IG_1S>Gfbz) -(xG)jh~wC_zY&2^p)GIS!+RNX5XAsj_4Y9kAAwi1%&@?my&3PXAB{milHAY3RG@#PisPA&ywE#F(6~e -x#up1!O(W+(C{prW5mj|lEbu%=Yyvf7u2;9gU80137BeuV}pm;io*q%uhy9HiHBj|h?iT! -mV005(ad%Bolm1mfIfXR+V27vh0>#YV6v}jF|5*r`yo=AfEp-C{T_f(8zm9DQhULl|>GTO5NZIB6LHt -}4O38q26hZf)TwhfN!vvHXMIede?$5O4Z4T~wO4eKej(`)c7fyeiF0fEF9BfRyv0}$q~DtbNJ*uF5+a -&&-BpA-c$tJ26e#a7frc*zqQ%>OLN&BtFAC`tokcCR%m5p -EI>g6R>iB`K({2&=>nJzJNo -PUgAb>Z)7_IF4@im&UKoIiERdEp=4+Jwonf{lp2kUB+yNO>^o-)^2hW-U-Zyqgh+%g -GiDZ)R`ci`Bd{CHbB&;x@R18$&!|6uFh*I7iA8+Mgb!9QKZ@qY;PQZ8$?LcY>e|GcacuOPIxFQ!*HKh -RCq*p7|gPW%8B@_0SIQSoI)2V`dFbzey8d$U6gs�T5_$T7GC6d9)BCC?vG?K?-Eh$lq6^)U^+uC94 -m-vja+^ZYsnWuxAGRCA;AA7c?u6Ry_byLEhhE}psXWw1X0OnTwp`@ypx2H&l#Ul@f6WT~X%_&d-j_# -qwHryxo)!5ybj9~Zu}NZboNnGLSPd1iW)?&@g*=*#8p+`D?GEWSsqBu8yGSxyR*8ZoP%tIxeWTB|O2n -Tgw8E5qyi#v(k&MHo;$490u|&zVJ%=>y8>xZ^TbR8}9Zbk-DLth^y6DZ?p`fQnbYB#(!SsD2H2K>F3g&;cqy`%1W`JO4K$WsT?io0DAwm`{4OQZ+o#)Zuk -e0jJ*c_^RRw><&eNRieflwM5-82LLKn9aC)`oYf+;e26OU|ycKwC5d`?gvuCe19^5vyzxON_C7>vxaJ)yU9Q-<|f(tIv -=(FP(G+`($cUmi5H-CJ**Ac6$JL6w#k^94<=4E|DwXB|1f+Gd8W -7YkhSeSV)DsV>T|Z0&*4>q4Ad%@b-Q%9{u|2)B)}pPr*{o@k#O -1P<&U;95$O9KQH0000803{TdPDDZfmJ(TOIWJ9p>)zuePU@$QoyT@I>880VCDAfBGO0sSaopY -g?swh*;*FG?wCCRQ({5vl1O@|OFqjz(W;VCK{j;$dRdI2d&Xd97c4PC8f%rUM+!pEVs*1YLd(rOB&Te -!Z7iIKnSypi}PUg`MbNKyVGxd8gO@4%W2lIJE^~$J7%B1+5Oa~jA@ZwEUT&HE3=2;Y_WfWZ{MKZpPW< -{J;$+RCumqn69QGOXsuHs^rz>_MEqBy&a7D-X&8NkTLRh(vNHjCmYngE7aJgTk$UYTE3H*t|*jcF8@W -j;w`031#8$?`hMs<=uC296Un->t5oZ0B66b$ZfXG)>|;V3;9XC~>agMmK47l`kuR0X?gVbb=83SavdB -P7y0rB$}t!DYqzKWJs`uWtsE=2BP1OuJdVniGLI7@?trjr{z^Ynx@#ocv->25+6>I4C-O${+bt2nat- -5BgM|}C>YrGc^I&{1&(_a$sm^8%bTnG8fyTIx`Q|?m&+mp1Yq2#+!X#RbASdo{3DrE{FIuy%;)p`26| -0+WYZLTTkg?ZUqFF)oPSQJmrO=cmRHaV#tUbRF=L95cjZ+Ky-qPLE$4;`3*RfbhYy<3n -znI{f_j;OKQfdU5dj;MF15Jq2ih3KnIg0mi$Z4*4lGe*hR9JijKRvlPgWr7^g|mwq!n>pML+ -J0p+0i+m4=`SyokB}E>`)B{_5|ym95N`J5_{^P2>yP1j$d&+34<4h2gd;K9IKnfaqce*An?)Xa#<~lW -HgG>YowHDQKVS~PcvG(8ylTY=PZe*5fw+uyeZNuK_Ul^3M5`!$5j*;ldJS|QVy{A#>Qy$8I}@M2eA85 -=ReMp&nf==4|peUBK*L+ox%5moetE9m%tYasDMv3AN_T?vy(hRUfT6|X%DMSfrDWss`ei*;MX3o;^A< -6XLoz|aWwpH@6qnw&MP>7KD&L{FbSe)_}$`j~qloPa`kC+k>3Cx%ccVutW9&jIid+F&4 -)5)OU8q=2bTMQ_>U)uFY*fYV-Ug1XJew5@V8A!ijpWNDSyY12VNv7-m -xBV(V<-Xtp5z&TfE}qEl(*OOH2W06w7*NUX?{~8nE{^yDta>5iHbNafpMMW6+nc6OmM#=up3f!#4RG> ->_^>$N;p52>4z%zYUDRnqx2%`4j&F5^lLDAJpaUE?y2|HM+65TUdGfqSfX2> -mug5v;ILCn6nDAEJlWM2|+}Wje1?T)=S|$&Yda$TqPG>Ngvse3 -9Vfs3!yZO@jveR1$6B?=9Rx1!pR-@eRTl3w5}&(q3173#gN~$B299*PTuj`fL()WJ}y-VU@;{Dk(Oe9 -iKk``TXebhfwwLa5qqCo@BG?3U}T`kxvrX$w34f&-2Nra^vjq`RUmUsa}J+51huUT_lsdn3fx_PTr1S -?>Tx-$Rb}UqR7B`6FAZ=okX*2Nn?W&5{MtLNglm-EB^Xr2RaYj>e240|Y|QE)UA#Pcj{qJ&URfRIX;CDX(2+}Ev(pV2=vNoNyg8(vho% ->W)!A~6yBXlUabT7Y(1TO__~d6@dKiRGtgj18BAq5`HTO2oe|fFz?FQ?W!ej`*#`B-fboqzD@|rNCa* -<3>^qa!IJ4t{}YZM;^s}f1N)m|LcEj|ubo2EdRRbJd~ygYh&stZ5y3!_qTnY%5ZG^A^N7pet|yGm!vd -|C2n;|jnUqvO+)S3;HJ&T+@60wsuq=Xo|GDl)4GC5}5kH&vm*R#nbH+&t$t&N~)js!F*4u_O`X-v9JM -cjrrR#AqcBhT9;F45Q>r1^YZ~K35=M0Y7>F>R4Bwt*$iT -u--@cX5Hv5NcCm_Egtm<|Fazu3n60!ncE*B)l?QNXDee(u%fWsFfoUe0~YlQRkqXYd-*h_!IAE!uI2q -+}HQu~owhQZ=$we=(6CH+V^L$J8J()y9RkXozp(&=pk#zKi+70FU)iU6i>GSva{Y}E1EhQ@Ea?%*0op -gs?4w^DnCO{4=NYu);t$0CRT>_(;NQPA`kS$~X94kB5B8$=w5AQ#0-${p~MIXXOkLD+N_adm}~9r7ym -y-a^6Y3~}v`;I$0on<=3w=@2;M8-(J>B}1U;c&&5m%#2}!p67J)wICtb&jU4G2Xpm?MBl`}L=yg6#xa0lkb39)rG-UatisyrGY3h7*^zxYY#hrY(i&A0DifthTOT -zl&k~?-uODe*Gz+9-{&J(*J3mYN=U_oGCT=0=yuof?{6fi9=od?a_-Uq -7sjGOsRAeNjJi`psyuglF5>+AQ$JabcNkOg;#!szg@hS3PY!YGWr4@Z_f@7UT8cbyHrdIEx*ZNkG^>O -?(FE|P$6KIWP^-R^zi-RbKSI6lo3q+S|(_*G3vak5rJ6CoKt$@C8i?# -X3Xcdf04Ql7va=gTL=`m<*%_`tgzJ3U(oM4zZLjI?Vp9KlSfH@yNjF}i);~)sGVR}%6`D#WIa=hqy>IO6c@DS%o4Q=iBV{)HCgEvgnp^@O&$__yS+DCy3f -n%Kvht#o-gi2k=XnNOC+ri#`q8Eolcv61q4hkAdB>{IGENryfvU=88XTMf!B4tgGgPEVs%4Qy<%c~+= -VSCB>Wap>jVN}@UJn%alefo=JV=j-{5nXtA -t)2{~Nke}H|FqkHEpEiT2QhYw+v*BWC+LNZBpq^~*nm6qO^()uG97~yrdlmg*m9uufuAaCTM9JL13__ -~-wad-OEgzOA17Bumsnx1?yI)=sSnztU?=X%e01V+lBOgq(b-g|hi4_79eDDzrz?s{?r*rk8a6rgA$w -tTl)(m^PJbdng-UfhDL)hGMdqYlj0&M0{R?}a;<7H{Y?gGHkbt6wJ+~V_{IDNAb)G%g2kfWa@1 -qw+rf~_2$v9Kx21lLBNPsU<=St==Lxd8A2r2F*#{Vv1U9`<_M!+{pQ?DUW1D;Qp0BjE$R0s$*5wCG+cu8RN|BEzGN=H@B|(F~@+(DTwu1thJao0!a -lb9hB_TEb8#C9K&An$1NhFqQlgrXNb^xh_$LsqoOTd%n!*nDZogf6=26;C>253_qP^&=#VvK`@fav@u -OVS1w@b%E6LsgQPMmbq)p;$`C{a1Rr0eGmt}}SEyTqbQRqs1{%1Ls^Nw$ZJ7x!fU@okJ+WD|DkrlU@n -U>{^U`%FcKGP&GoVqx?!z8@|A_0|De~~~6IEpR_{rYm;qJ#C)6ZR68Lw_-9BxIZ)ag{;Ze2(?U)-Y53 -ldegEGDoSmX#7K_wk6*-2E1}>L7vnGTB0;b_BJNjMPtnqZj|trK*>fbne+_*BhKppnVoYdXg1W#ThoU(%SOcv_qbf$h8+Lhm-R+9Wr_T^zQ| -bvL>xzj-z1gRM`!BI)PD2)C;et(zd3q&s@^O*J$NP9v0X%7MIDf(`%(|zk>0QB-PrF?( -f=2jcTmc&Ta!l$;<@pL>gcwWNBad2h7v55@AT|y`U(g-@`}EE10l ->U)ZExFDGIsDe!#Udui*n-{S|?B;ov@wC@5hhCe=E#Ks5lVgakMi@KnN_lgE -hfCv|ueWjEL`86s#=#{cA?D%t>&I$OWt_;E_Xs**>8|NrLUat#YPU?UrbC{D@d?+)dX -@IglPd_D!RD-RVwgJC2d-kYFk(QeW{_uKH-I`#H?@j$ku#SKZ0tNXYLlt&UyVGE+jg*YYpc!SbK$+tB_a$gH?G?EnqvQPMIQg8 -+kv_X@Ppnagm)moE%L6TTQRqA$PLru!bp}NXZGeQ&QH7$YoJ*Az8s*7r$2|X02230n#vZ+i1VB7Nqe? -9a(-+)dMkBPFk4D`xnO}k?8($~n{+$@|AMt5*lBYhfBkrM@{B(5o;`HSB7h8viJyG7iJHxlpzxda4*i -pJ8&5TIGgb4KC3&GH36&Zrwvv! -k;{O(yIq!GSAC|M#5+UD%Q^dmQkMMO&&NrgY3E51lHAMpsEzB0k8-u5bQSVS6or=Pa|fj9(_Hhw`4`5 -v#yd1exO)S;{}@9#z}RPB$?u$ruG_J3|3Jl`OEt+fFI9qRn06(retTlia#erkkDH)@iP8B*&BF?mO_V -#ZkcqA8xXt)B#R(b&wfuA2w)6#T#ZQ{si7BauR%W~^ePtjMRuhd;q!yUR}(T|mOKvdyl9MnljYBql}ScPpE^V@qFgIH!64^?MT@Y}k-<(d%>GZtV#Aw_2|wCR|HFiomtQ -tygaiBsKU-VOcX(R)+mUv9hd*zQY>rmBVqdg^J{Fo;sG>Bta2`!ut3(&0=5F)J>DU+tGhbLy+Z$prTr -AN#!!Ohi7x^woPGN$X=5Ggw;c-HwA6vcD<4ac0sx{c)zYGyA>c3-w+tU7H=8f1jsg?|qZ|yZd1^-V<# -*2o#5WiK+`bnrmUUOJM1z|za -4Y^i$D_KyRuXr7LOkqd~+k3CUe<`jQz!sxp;@_FvL!ec5saWs -1F}=kIXcRfRm|NsPrH7=^DjJt#}Whlr`zlG?9m#H#CqBJb?4!CzwQnn{<<^V!T&nZ0|WE}^2guV{q^B -@-*qg5s^0JhAO@8)(c;IeY!pvqgVd|aVUlBNdFX?Ps@9i;Et@D-I; -qQ;mAOhG1mt~ccWi -#0pf?RhG*=PKtdm`$duG)ecJnQcN;C$s8r1GLg{!}1pwi!zF|k6n*z!IZ -r{AFFw)vJ&<3s{4`BW2`VK1I6H{8A7{(1+Gc8UcOZ_p9fa$85@Ud`Dlx8S^^&=la=tpCUr;oiwSS4n=6y#lL -%+9k2ULvQFDz2!9H{BhrP5^V8KvFr9N@y(t_$4k$1S -+QoOHo*sB)nx*%Bfiqle8D`|?8sO}W7s)JAk%gX_NO)YPvhh{X9=dClutGpY&BF&IcVmWru$S#HHpY& -is}e*&}y1tj3dE`c3yUczcfIY5ZLwg@bqP~#3>ESgzxi0HrUDx06;r-a8}(&*lRQUeZ*(ctRJEElwK~JoIf;IVe0|Ftq*e -2IH%Gak#nNaFwUJtw@n>3DFHajRm!5E;PMQtzY8qE@C%{#vq88!&nlC3oVla@NvL1^1XHd$j3yIRHu? --Q_S5cHu6LJlGDlYD`c%dE1$Wk`ws#u)yRZ+fgxE5tv1obuaq-5Jal&Van_Rh1{G=_9HK?!f);HR|GK}`TYa>t&eQhnA*fH^nhvmn)2d^& -tabXXck~df;8tv{7xReEw~wpx1B}Q}vE|b?qq;Uwdl!#DK7FQ)Zt4HMm$ -3h1?yuZ5N@FZsAj6lj4Iz3=q@vI7O?cu9-eyugD|nITQ>b`;$lXT8=WPhN4`)>$Lsj^ -iI6iRQ(YKmyTw3-!?ydH_hnputXtRc#xC+TZYvA^Lsu4tHl!*w$!m -K8$dF&>#a-Eo5yyqfxij%oX~2qrzsMh0s9lHTILoF+dD5Q+l54f&mjHO=LT-4;b1TiBZ~D#(HlE1C49 -ZL~Ri*K9v}!lwF%iurRiS<|+amdGb*fbfi4M`+>E2U3P>RLqVh^ogJI(8n-9aG1#^~r&IKd(_v`vav! -Z7c9z4-rBGxkJD95IUSeO#*7M8D5`D>V0rYuqz_@>|R=*CrXHaPKqc8SL*>HSMA7w%!=4{f2#wDz!*R -Mfa$BT-@m_C{&UDE;WI$p3Pm&Xw1;rFD-WixcnXQAu}tt(%A4Qa-ac?<*B%`UlsV0+$PYE0 -1XyHS|8zawMT;$?ycb|S6u{H)ATb0GRm&A_`PQ&~I)IcUJG3dr$;c2#wme)+E^P`6gyo$qvXf;&I+9p -N?_h{cIe4!*NcG;88W%|f&N&p2fDUT0$}Lrn#}#VQUE6-M>(~)vwpi5qd2@rUCOyS{G$-12216AL2xD -CJI?cLFV~s#O0mPK@Sm>f^Ke~qIHtQmGA1{f+E`WdXgzwB&g5%*KYkDuzE#Nwn~U9DHT@v|H!9nYz~{_x|gPBBeBLU+a?# -W;jyao_!zM(p%e9R*9xaUvN)t7Gq4|O~v$9+dv};8XsvucK8vk)w|Rq3cXVW3MlFb-l9#bG8ER-Dj<3 -|Xt?%seU{J8($+f}XcJS%6TlMOtT$qOHICR7zYp!j<}PXJEG>l%@9ws3e%;M>&8;Y|qU*e@f=3!eJI= -wCIkOV11y6E01^`>3=X2iRmRXg~@w`er%#o3@`!!qoCm+lSp*1fg{XV -ee#R*O)~x6O>ug6eAaVc=#3)AMa*Pa1jmszbJ5B@r2vIl*8MsTrF#U?$mT+SW$hbB^_iP^7>mBdGahGBfO7tkiSHKo>LDX-UDE!qjLoLE=#dGUO8B-RqlsHYQIGvZ6XKsmOsW7+o^=Jm)GY+$fQCd{cJ`^+b@T! ->fj0Q)n3@P`sq++PuUf-x|zbm_ZgU@&&N~pX5=&JIvq=WZpBBB2pZ?um_^TU~X)+(EsV!p>Xa@7 -rVLBUBC{hh@<7LItr`P@N$5Apq0l^L6}>yuecUJgo&kH1qdK=Qk!g|K2=2N)Gj#-m89t8UQw~2*rClYl@-{*8EMTfyveyOF@h{^5R{kAs-wd -w?ih7seX1akb`80)uTmIUW=0BU -Of3QOpNNnNfh3DYHd(bkh7xf~-<_Qp|ogmHM2%qb^K82Q~Wuc7l)E403w1sX}Q~V1DD8Z4E9!FR(ZsE -EOodR<~W%W?us{`5C-ac(x&>vx}LCSrz*_1DT7Uv0yAPs=AA;dfvAhv6wIhdG_TW>_ -#BcgKu^z++)W);_aPVu -Q``-W852%Jr<$0ga=k@T;0&BWjme{d4;@OfiAy9h5^`FEfTlq%s~N*bj2&R)(OfkGdW%BQJkROwwqxX -}Tk%@JfEJ-e$r&{}AN%$qRkvs;aVY2>b5JPol?^^@IUC80ShH@@e~8fB=`?8bn4!<$nXaUjS^lMT -+9Q=OZ2#gW^c=h!@M8X5h@nKehx1Qc8b -di*m+WamhSs>_Hl8J<`UVND()K|$Ev@kGhs2$H+`*EWe&_Wa-v6ANA$oE`gglwjg{CwB#BUuT%;zV~l+RBM1L5^g6refF -UqwKiZGO=3&AO%IQ;gCb>(i=JJlsw2bsNk(dA|XWjqm%!|NS|a?lDmsNf%1>c~k@Lg(d+gJ -b{j}Qs4%+-5X;%pVMa2z`L!CQRe2_IP&IIWMiM2VQBVrqV_AJdOwl`&+AFZfjB5@ZmzZ@Rv5X=9 -7ug}$4<)qYa*}EVdNPL&7`<6*)5Vdta$T<_o=G>M#q~#)^KbVllj2j$R_+P7K_eEP=3)^{ScYp3#UV* -7hRM-%`X4^+C6U^kEjs$(ns_B7+%L&(&sMSzd$Mi4IXirL^qvyI>5_a3Cgl+}fBmpMqzrnXT|4d&3bK -^jgMu~L7`C|&$ac9<&v0~TuP@743c$6N81AlBLv5C=RmS@40!nTgDb8@e_cUY%EN6vs`r-hzXTy|2 -*PfBO2PE0|O}W0cwWZnA!`+oF*|lePtZk3BaT2=zq}E5Kt(9y%#Ymw>^zq|1Q`JXTO}8Sh-J6zZY}Z% -)`?!Znf)-qB#x($aNatqD`>?I6?|r0%e|Cd(Z*}|OlSh_>ID1` -rwRph}Xi*BM2r*CN_9SvyFwVey><=dZbexONfT8C}y&&?cbNh49cCyLDr3W69&`8mj;KZ^w+FZGD}=U -k88vuVd)-F>o)}+Mw+J_Bk`ZBOd)xJUD67zl#}dQv^y+-hROSbgl0J(eExsMgaS(ufSMxBrhWF$ZA>% -$eper>{3kwwGh7_47Yc8cAnOw0hD0N$02$lJrvb2SOBH+9R)GtQ*wI)YqZarE8vtC6!;=>Ray8b35B^ -sgDe6%67H3Bv$d9FM3L4A=(86y2`CDwlq_?6Yk;Gmon>@2El7S&s(ntZLaCxbG%xZA4so}~d3Zp*4ja -hEDam8*T^Ow741%pI57H_Dj_&A`L8A#(*L<6_h(Ewwq7nB3snc8A-?>OI=V|HNof(Sjr2b~fk@)J-Rs -I0{e4NbEEHje0@u1iS0M#b?oOgDI{#5;%84BoE4!Bf-j>$KBoIN+bP=C44Vje3Jvc<5zS)|a(H)kB8LTE%@JqXltMT{WaUNK+r8<&;HYP(*bMDr6sh+ECTl{DZleRK)%ZzRzxFfBvO=Kq&;o -hAo;2jVa$0&YWQFuAMhwX;QJEh#`KP|?VutU7gJ7c&b#EB_zNcu>xd};Er4`-FwHIdppiq12r)~TfH< -W(B7J?0XXb`IX}x{A#^7r=Y8kQSy*hY|scmG2E%AYj{x@#b0V~wno3-vt^L~w2p3t9wHfQ*?58dOsUDMhctc%dF~R(gMV{wFxH;V -8>7LXEGzLP-Rt0derdI)4k#2Cm$^oq!jy?G-WEc(Fi0B?2O~7*6mFs`1UYRg5e*I&hkpIu2{v}*#pJ> -W1F6dmBe3F_h$#IJ^g4GVh`224D@;U#3?hg%M73F4R;?oWyC2}7utQaH+<*-(?xXeIaFtVo1!eYjq1>wBS~~T1pFG~%{eE@5nOpDs@Ae+VglUjYp4-g0+EG(16!_j~l4O;F_l(bp7o+@-`BKpcX^eq6i!enNh0yZz`u`3a-ot*E=rL(7l -SEy9l6xzZi2uB%GQPAhWi>fK>E>z`!UC{QS+q*}22g -_}1XVr%xP96gk{SUR+@(I`pH!(NtreEA6eUs10uxQXrmaxckwUl<}# -Obsys`s^UtUeOqba-&MIC}DlC9nl&MLJLQ820Z(SwE*h2JrD^MR;&}y?=o*424d!C@h$u|1n;bMKlqi -I;SMKq=$A)hy5A(bECaS;5g-nar9MgnUWoa7P;3s3T+J!W`FWKs!1?P#6&QNcUHmP9hBI`KWcXst%~d -2b&miI0RmRlVlY`bl8Z&}-9el`gRMqU~BF3Jm6e8IjzTQo|*#`z_gh@AzSO#F#I0jQ>tfAVs3GWV5PK -yZ*y$;tD2ppJ$4w3G@33D7nc)AYo^M?TDiGmI_$89828RJc_3vhyj% -Y5ip?pI=(?Uhz`7m(QD8R1P*4kVb7?_?5Vu1(|7O5li?u33S1(-&I3h*$*|yP;h+w1>g@>G8qY(fLT;tn3BaHv=ts4$)r4@iLuPm^Hv+G4;%=_bpTZhG!Z!At@|O -q2QEMpR)W$9?_CF5ki8+jp*0Oj;ElE%I$I|Dg@WKal>-2yl7BJGO;HHa+WeLdC+J -FoLc~6mSh***mlXZML-D8k0pw|Ow6NN7f7e6409O -kr4g5L%RBUT-j8#p?YbbZjqo5!jk}z1l|t&G{CLuUQWW%3Tk#b*#m64P{y;V=-S+aQi~JA2lAty~BiN -oXm0pEDNR=I0dQTo+^#(Pf5duUfilEY0pJl!P1qd$vj-ZK=C=_6+nmx(LM2XSuLL})X0UrAJ&#&3X2fbgj4juG -a2TKe<^3wScBh7mRof;;chf>u61-j@CdzADdblMIArQ|iDQcd52vpI;QcPEbrO;Hqt8pQjU?K9}+JO-m1YL--;gs&_Y}Q8!;)VgNe?M&Z6PYlSfY?nARO0Xo?fa@N -eiKou1ZM7__=?d2^6&rZH01Xq&b6b>HkE*L1z_$zLr?(x}BVV2i)FpM(QwptVVkt1%;RDYtiEH22J5x -;KsPmHYQfjG%Zgl3>q$$Frq3(3jO%x3RX0O|?>Wpi5~NR@Qt;$djne5ZEs#t;oOwGv_Q0iKIBt-V3R(4^@LKV{{wx1&{u -?s5m}zavnkGsc#1jDR8f@aB47d2x#avt=;NfqV=f#v%x^3fy7JQinT!J{`4S_Ykrj;&8h_Z2hJ)RQNS -tEEQEVAk8Ez6~eYw>ErkHV3vtnW6ihp;PxTq@Fc3{Keix9-r96+A8n5@a(z$y3+(JC;0Y@rw!4p+^`Y -OM@MlXh0L#8MEno5X>3NjXMi*n-8>jlqnzN_$4EF}LxlNseVA=JsSyp_SUzoIpDQ^539L-K4qVC)Hk> -$bvDb0xHO(mq&-kFV6jCqNun5EqZO9MSD7J5W!VQw!5FTGk3e -o`9prX=mJq~}z|I2fVdYafg_O+Jt|#YjNk!{N$T9(}Z!P^}d!mvO`Ew#BZBB9ObZxNbkpHcVRz -BkOql&Wwn_;#Rz26>C+S)%qK=;iH=zt|D7M-vreSF_jD}x_j{?w&kt(zvN;LJ#bcYv -Cy1%CjF&CDWy|8(9hfTBl?Grm}IDJ$Yf56^-;cF#d9KP_u-90}J=yrIw)9>^?t{MV<@?mdp_hVn$`!K -{`Z9_3^A@ns45!M-#VH2^7A&6sF21d%g+qkaHo#FQD8<8@KIl$3fjt)3t$U0rAht6C^T;E+jcQS2%6SX;8E|Z!?WUH<;()Opu-HBP`ntjioJJMTTC5qL;}FDU_l|6{V5QxDGJHh8{;5YKF}bC|ALjTkS{~41fIwFk9mv--u|Q -O-g4#M}7PH+2L7Z1^hIgn&C&Ojvg$n-E{*auW?`RFwAVok6Po5G0k@vj(yF_^yqX;+Ed_I3*Cf4)~Q0 -%X#%ZYPqk+Fp0bZt4>Wp=LXQHg`USzosFzT7uEHIuCLYQbUaF89yc$zxfdnvOOlLBNPi -B1U8AGM@YM%e{fI8MIJ}M0`Yspf1BKUls0f&xY+~?p)6i=*uF -Rz`7^X$_)6^~DTb}CM;)bb#9AbQtb;iAdn+CWymm+WoMiL;u0kd^q!3imCmB{Qeuync=%&Q@|VwM^qSL*(a9< -@vz@4$<v#CIQN;Om56MpqSC^dOUoUOgrN2+qezYz$$T$?2} -QIkAMKqfDuV&SzaU~48u{nm>7tZpVCDc@%=z_V3EMMvgpm@qh>@@)V9*__7yCMh0c@D30@l>x)lM79p -twt_A#^N<4F{EV?>5$XAKmtF`C*8Fz8|qSDMlvVNQD#ck@SVh2gIO3Qxfnm~UmO{I1FAom?t-qrp>Z> -7NQ>9NF=t1r$5fQwactr#hGph{IIaNm12s)rXNP4z+>Vp?Zsg?9=Tr9=WKf2=yslJ?=^o^SXFw9#`<) -Dc(A9Dn-c?K$390>Tq%oV2IYgc{fuNo!RL(s&xZ)LvmfLJdBkeV`*a3)Q>K)K(+Xh71 -UJeQc?^uwY6>75ia)~-A;7m+;Dfx>-gO##C#{YbY3G^#-AMH0w2dJ38!5=wVTc@B#NDYIvCwCFb+f~` -UH8}@$-88o}fL|erc44;W3J+@vT(v(k@Ht^HaERFp>qX2Z@touz%j_(Frx4zQ*Bl46!DFtPx$ffsDcW-0;9)s -$+OdOT+-y8G5)b%jO5KfI~dcyQ;hMjEvByz==%+RzZrM_{Q*n*Vf$n_JpI{db{`??nE!b>Ky_(Av{81 -QK#A(3V+CYDI#^QW^&@{&p;m8lybc5U;rAn5U~HGD6GV^aRW`~O5|^lN)N>3r^gQ`e)m7~{GX4Rm4!z -2mXK8G%;*!)qm4jN6WCX@WT#uEh$Mt+C=NS(eh3N|bvqnRKwCJRvk5#p_Zp>?*;Ly?YIJs0ouh^N2RYWIwiTbp~ZZf*$CK -6@336n^x(YNNnCPTKoYseVU2*6xG>U2e*v>J--5H-yRenp4z6TWm|v~M&{`e-u-EFz*GKTC)h#>(=*$ -B#umC~A6=rtVD5DC*R#AyBPO5!A;zmbsd@#z5dML@{WG2hbXGl>ZYuf3{b)_C)Yu!A}(0O`u7Zw^mJuMaPNl56y;`*_I$TA-ClPV*)AGOt4xs3B5btuX1TA! -r``Ue(UT?%C916K!-e2WVDLpxGjsp&mpzB2f8>@$}_$oX&Az1m2i7E)w;03<2pKE_S0=R3R9%gXU95t -zBVffhB}zh5KiR&ri=@a6shy_dngl#jFfjH7qNJp}_aG`i@h!kaz(qbb+~6Vp9K#d{Lx0={7-HObT(f -mMR)YP(LpA?r%{yEfWH}qM`3H5DW?@2C>*s_O|p74+^l6IZ)PT^lLWlHx7i^?^{g}2#?wC>7oUJ@$b7 -^ZPmw6eYdf`mR}x%J`Q<859ShCW0_{WA+?q&hW(ccr?IsU^rtGJA<+gkefBTWsJX5f8CBWLs+{TAr -Kui&53O4`pR3Q6)TWpBIiq2z*!LsCwra}sk+1`JHTaJgUe?0GDTS&FKE!tdtWwB+};ePhJC;{<{Gp`a -4dm+SEpoZ9CN2iV-K{Dg6tKYc;q+2l#TxZr^`V#(BfkH$LMv<(_vI83aTe2?x$zkL -i&tAj=B}23`_c1ysnU~Pix7R6j?$=F(!T0>Sp3v^j;x+K5_^Z3Enf@bq;W(BNZiW_vfs<)e16;UeB$@ -WIoaFq2tj<>uW5yiz{(|Y&-Xu5bz}nRD-c_!uZbq0SoDDI7ENW5*y_$5rW6bCB;9UpqB_QDyJ -G*{7qhKF98ho`9H7%!q-=L=3L^!d>VXR$`mvS!+QlKaF90aCVYagCoVws&gB2=YbXC-{{xdROUgdn2x -*+F&o}C$4O_Efw!a^Z&gq*sf(k=pEJbRy`yS*+6~}|~fO4x%yeOng*0=^0L_G#JPOS$ER7YaxIaJZC- -W!3HVY-a-jn(w#&(RVj#&QB$V~WZdpE0zk7OFubCq|QE(V|3HqKBzZ@ZI&`so?+j=WzA7uQlGm@tgg= -?(nG*;xp#&TgTIt#qK<$@HtT0gW5UeM)}u{``w2~?ezlHZ|t!393Ko`!9OoeyR(uej;NN%69?) -TilvDVmj13lBMb;H=lX<5Ywp%=Zm#u!DAL@sx|-D&gVZpRC!Xk9l%dJSVTBR}b4>=}4A{|Wz*H(yx6w -nCz1I@A+U#n!$QG{evsF*-q8Z_ZfZ#kw)Blma*m%0Ezy9H}l&XbefZFtDunbT6`d`g62J0{l7)g)NYs -Ro19{?EZuz_{-keV&n&Ru -W6=KXHmC{Rra#lj1J~=t+yz{Ia<>VgDl?3WU|%z1Rhn^gp88$9{nN1L7xD-f=pd}_B)iuKe{yPL;gNp -*MNz{24uZ=xJGE1Qk8%InE{2xvTvU;C_Alk*xfVIu+wZhuEZEwcCG@;O65Uxlo2OMwwa`=n;fP#OG~o -KaQ-=QV=XL>&&JK#zf=2Q8qJx@&p56LNzeY3=w^94a7K0V_Bgt4=AXMC?e+raMfGWwQ`S+GFENv7ggdr(bhJW#Zn%C;7@#6Wtl)*vamPgSk}i_iZV>(G=j4`8e)0SV8Qhd0#R@v6Pbf-CZ -DsbyB+v8zWJ(R{NZOR1cz)-OqBp -^7l7p=c@l~86&~s9*4A{@j{b|4Oa1P@zV0=vJ5a9BmTT{1F_vDI6Sd(v~j#^=oHk%oyuKFY@QNvrc{? -IT90bNo|=|;$0JHZ)9r=Ho)78w@cAabxk;iMOhM@3kt1Ds#4B8%;|UAO>qq9WNX6R4xyxtdc{#Ym@(zGiccX?l5U<#iDRw@SmAQ@I`80tCDwlU95%Z|&aGDLMG?-41aC|4qhcD=HVsB)v@e5|DCYuoMx8 -q4AOC9hV>O>{dXXcj)kK$mxQY1pAN#q*l60?NyE!ah)8k>&W~e$6o{fvt>Hhq2Z{kjJs?WO+AL1@dQQ -ksm@_Si67`MdTK#HTeC`|Xl&dirw5J -8M)0LMsv#d+Yw}H%17iyQuq90keasA@+a2A%6{HD*6S3_7EqpW$sU8r@U2-7*-3;Vg*QEG|Rh}WAGG% -CQ_3_+j#+Mcpc&!6&2#0lx~7tNdaHE5WZz$QVMBj>*zxF!WGKfg#%sMvvxm -Fl!xqnP;@dLX@ykjVB(a+J8XLm)KkNhs6hD5)Y93`!wn=6$j?O}4=HzJ$*Tp-!EtRe7#C~mpnYfL(sft&2<~{sgP0L;Gi#C_5+ly8}rVIxSdFGijCM0caei?FquDivx(7?w<~3~9R648=8SfmM_{@sM9q>^}Euj|TGb?5WKC`d -J$D8}m9ezJvju<*7&x3Xm6#LD=!xpI@GfPy1QjFhAp=Vh{wW+6 -9#7N#~sZ&p0_&M(!?e$MrMi_J~*WyNM60Yf)N$Y3z{bS#!@JA{M-<4S-{xhsJ> -S|!i=L&)Evv%EfVX -4~to#t7G1b0k=gC}wVTTpBHS)P|`6$Mfi!l{r#JfPl>|40pH(oqN;%y=kXG+HyZem(?ohd!*fKLp?0q -*qD*c%;dgnOv!Zn-p(%n6X8F5`k&^$-J=lpgLysyjU2vpEymvau=nJn(ej4hthN6=h*1Bs=Z2DK@_o` -yr`H6mjZ!Ue7%ZlJUblbj^u4WRT}r}`GVS0+PF}wau4e=*0=d;NBe@o&B0*a*2-*A0KFCvjYS@>mqzI -Uo4yIGje&n2@sK?hFIwj6-b#_SC#MIT}JnQcE=@1&}XixQhD$c3Cc?Cc2iKk>cL}e-l{`5%eNT@(9GF7Te -}yp?k*-5!Uc^ojUsM;vAT9Xq}RqJPS)W9ekGZ*dl6^Ti3>9~qnXmz<2cr?O`+%P{0EX??qC^o^pV?4mQ!iHq7R_YJL7APgS=sLF%#| -3w_g@R+(4hr^OI0f$$$|NP_Vmydf~fAblTP3sY?;XiCI!7Wr>d)t7gi*hD6BBDGld4GzD}E<-99Q_5Q -{iyTd~2p>6FQ?ZtuvuC-)YA_l*H_B-iy-TwxUQ<=}eo(_}+*#~UR?GUj3E8XMC@@>am}5nu@Drs)k#h -bnNlB$8F6{H2ckr5v;Dv9#^=(fresm0JHvQz7l+uVU(zgSdsGm;o|5dlfn{J95yyZwTdvBGRnQ+#*zK -TthdQ7zaY!5pug=#-O6H}X}*7nk|RlSA?)!*W!Q+p=aPC71xP`uH=Xg9?k)kT{^IOye12~a0hJfG`O) -z#I0X8?+o)D5tJL#|}SKXIi}9UM@Cm)zvVC+DPEYc~@2ri${L3}b)OX?Qx340WkoJzQWYGI9_toz&=4 -Q#H8+hKQ%HCCK^M8SBU!ieA#0PgS>W$^E$+5%`Kh*tmBb_KbYM;yGU^7r)q;Gs!4F3KP~>cu1HXp>Xi -#s?e@5A&H<4c4HrKI>sp}=GmuOH&Ohc?9bz3;>eczsEjX@hWKJe>6LLrTDe+7VP(?a$-mpNBTsKeFLF -8Tt43&4Elp(l=*NQIKd5L;b4hWvEXV@{wt&oEc0w4nfWqk@HTZo_9%c=s#Rtz4bOr;Kk4lg^FroP$U@ -1&~wLg!q$J029_o8@Ui*si2f`Qw)G0zjceqCTGy|(mvs_(32VQd`QG0NEV0WWYU3>O2EZ+Li2%hVcI$ -k@g33XrYGS>T6)fJ5P>X)s$=cyeX1`G&$OlnqaJTPzu+q8XvAevF0<_jJ8`71UOv5wby-VRLWdb8unb -yucM3@0jH28|F11S57Z$GVz*sT7oN!SLE}woQum&bfjFM%p<(CRA^&-juP*+E(=O;XTpOQiyRpKc%Eo -hT-(iR^A5!=adfhflNIj+x8Yqtu;(pf5Rv<};ds^Mdl18Yd(5`qAIKs~Csb=d{Q(G*C6Miy*5Ah>kv+2(t(@V-55QXfzVkL)64c2 -6)YuZjbyC%t@>c-5PA)z_aI%TsxCpm%*+(YIkL-hGr~_>5lx9{A`K+DZb?JF^k`=U}>mGtJ-9_E|M7q -YQk!LJ0U1O+UBbZte;a%B$h=3koY(uuB(}_Y6v>Wl4*v2l87&YIm*`tcxUOy6tnZ*2F;tz^V!6)%f9FG45EPItL&h#c1D4x;j!{6Ci<1I#c@FN -jKULHAVD3ELztCBz#Z2GGe53ey2-h8h}2T|l!(JFw;^+ookb#x=H$q^o0_+0a5Ei+oK@vZqp6xkkbXi -!~}1A_h})thr5Ig9kN$Evr5f1WJv}LF{({JPj&v+4Nw9nfXi!Lo1MNYsxZFWjR}^;~&t;Ax`#W-uW3%NRyIN_p*dMUsD(-@Y%V=?RTc(qE?(Qp -`@L_7S7Zz>JRKS7j7WkN>)>y7)>{&fV_k1b>ti4Jz)*IBhqq;a?GWt!|?H)U!?6|cfd5KbOjIkQ6lPK -v3+e=liLJ6oE%Zi^o6ehV7+6f+x(dEMh8W}<=H@$*4r6C!^PwaEIZp^yeR99&T8m|j@@wEge)1pJ{3B -`T{gs~vqn?shfK7&!oDGURhP0Zw+_9;Oe3~U;9uJ!n|Ey;o4x&340xI!_Zc*;bpXS)g2wp^^e&9dDUx -@(_ig=TNf^D7yTy6zoFD(5&@;GH!ID?rERY39@ox{sr#PDnRJTsKcxxsz&Cx -t%sX*j6`uENf!aTB0go=>e5?y*HL~h@uU}$iIR-M76pIT}{c9I%{8~fKOblCaCr0gGPTQZk+HGhbDO6 -ZMrveq(x_G>8sZ3j~%8sTW@K37a=}jZTG?hF&ibZtg#ET9%~vFF&@IDoCRt}u<2*GcAbQLh2m=XA0*kCAQM8H_BSc1SAn-!Yo*NQHV%dJiytMyOmh2>6i`Rr7!~Ec^yUK&t -z%eSLN8<+J0TTjt6u{`{x28WEcLjAa(hDJhnXPv}a;s3p(Vdja;8lR$a5Iqxb@>>eME_%kgRP{f7;QU -*IEna;sot)9-)Yw-{2Q6vf2;!*Ip76Jo?s`M~@XLahji(7^T&U6_fwt)12_cECdL;(~Esol-_ZW8F$b -l2GnExS?xMTM9zO6%#V{q>t9(I!LEd6LvJoo^rld>jopboA8V!AE|629{=(cA0tAX-U9#(_*e#<77&G -f^3ncFB$WZ&db^2_|Y(PSU+^SAFxi`>0GTw-Z|?t--gH3K*_D%!fTq{4gQ>|X{72^7Yjpde3_|)9ui% -ExehG(bgFGL=1#^6u%dbOVX&|s%2LXRU`%^L}(gn6=ULIxVlSSmGzNnCg)66LIfH+P7DairKd2>LF3wa -!~yGg=Egw7GbjG9v_D0o`&z*k(Nv$?oS^^Gom$)A{&tIFp9q4=Qj+aD>ZKsI_^*wcFG)YWMNdb9ZZj^ -eLd3{WxB2TH8zlS$JZX7&Yxt7!^xv;daa?wu!Ih+7YHwQ;oN*fTb$YHi#q+UIMN$oa8)uRIWv>vIktm -%5&U#q*mYd2V%yLnW}P~lV56z!x;6so(pCWvR3L$&oasBkdbh>BXAQ8b(Xv-(E;n=6b}c2Z9sQ= -g_(iGkrd#Ox8R86dy9)edLabb=EbZC{9O@D7vpM|S-j?*aVx5}5qz_SXT+xh?~imlb(+S7q&NI`wSGU -m1%0@yDP19H&(UjOr?xV}d>Mx3xDe$|Z&O->nq3XaVC}@M!KG=T(JH -ZRr27uoR9XYxL#X8oM$rOE#=hn9K^ZCxU#k02R4v@`^aj%4H#*K;aa^JQsC+`*w#FAJg1Oj?s!_I;xZ -jc9(CWV94Wu)U*y=)y>Pd4||Hr@B-5pO=NDydzlX#niGx?OVxLn?VBrgEJnHBFcl9u*O#HWwHp6XQ^W -i^a6=)i;MmZN%3drDF0yad2=D@t2n=sNqIe268xp`sSSa%4>`G!-S7~7lqRhmM}9Aa3;B>k%C=k4EqfNnnDdd6FK;qtemFDXc1N&=S79p -*-u-8Gqg6}@MSw#`dn2mjpO-dtq#P>_=qPMI?ZCf{W=4jFT-Oo2Lr#Y0HFj#X8fnDY1$&%K;w2sCYjeAEXBq(g11}%O^-+KwFwzbW+1T^7`ebeU__0 -&28{4vF&ZkY8V3@lyc_LaQt~IXJAJSD_>#x-MsS58mM|V<acxP -l84{Fu;Bd)5LNFYc4dvcHA_l%k_0tf$=4+1VlgLOmv?yDNP@9%1)ATJo#jR)?xP=!-2Rt3_=U@AdB<~ -iHh|<9iTOuuKAJ)1_d>Nr533JCB^%3XtI&RJG%Id!Kk+L%Wb{jCF2HOT;Sx>-eKCGmqdLtvXX9ZZ0Mg -vbxiRxx~ksE{@~tE?==%$#PnE;t*zx@#h)BsyPjS(D0UA#tANoBf -)kRUJWd1yeYy*YQe*&_K*6BDKNRMjvS8T5cfLq6Z6Dq77MOLebR2ssnBTM%BG64nu0r9sX{TRkQq -NyTo5SJG&ZFJ$(aQwm8x-Vdmll)dHQ$LSocZmQoGwl(`CVTLq!lVL!xua8@hDBP2Y*=zmTWV>!Wz}tpd~u}u+`@v^G{;fHN(HM4I(Vvd -gl}3;7{kaJ>%@va5wZgi>F#SfXBYhF%`MPt7VQqcY-|KA1;B@rFXDlsTq(6;ae -TrEZbt&GEJ9x=S5p3#ju(PZUM4lOxqD;Kq1?j-n$MtBUtcb8d3{tnZ`&{s-uWvI%#sv}+zeTSra+4}&CmiJ40tF4nUYQt5sDP}u!eIkOgYOa6tCvs9)i+2;H)1VYSqm?XW*>U*M^`KsQ!o6~ -<;Hf4-Vf^Iy3AooXCpCMkPaGjveU%s4O9rV8Ajr$6v8BZ&u}Z9j~0?jYl3b-&ia#74T!iNMuh`v?_ff -u?i*}nWJGI?1&g)WAer~%g^{8XwnMeMmV&`FXmvF7=FLb{6;#$tBy3&YTdSXj6IV^Z8WJ#)H -P-liV~6YK-MhY<2LHMRG-!;Wu{m=Ee3RC`5hE(&~>r?lhhyQF3j}n|Ni=HMHgKsRIP+_*Ra^1pXu|0D -MqORbt+b|L%*c;=Xw1PZW8M447N(k3ly~yafh+86iQLhC-}ur{^CR ->YsZg+$GCB6hG)l#qlaHOi0Gpje`V}u9<`n(b-t;>rzwk9Md1cjM&=NDGtt|JF&}qxja)j&ZVLE9SJ} -!c#J>ojD;e7WIt&%Hlj7`mQY8M6zt0~y`wLJ@0|XQR000O8B@~!WbsBC3B@_SvK|ufjApigXaA|NaUv -_0~WN&gWXmo9CHEd~OFJEbBVRU79ZEP-ZdF@+mbL6&>{;pra%*N#zucRf1uh3;L;cHc@Y$xZJiYt<=1 -4(c-o6(HIp>}m=`@bLXGy%+;v}-$;e2J>I2WT`J-47Z-li+s!`p&tz=^loMUfkR`i@W=EKRCSJtd|di -xS{%~(=OgEHjDM@)OlDfnsqB~mWx63e7QL}8T<4?m)`S!wOGB8O`TGI-mF)ffv<**R$uUef4<&q{z8H -$CpF)Q{Fe95YiBZr2oZJPchzzGlz^N36^i(6K(lVZlUY_q6jvo-GG#n -b;3lgoz3sP`mE0d6(+&7%O0H<;dry|W&X!KOjGxuUGGY_F>o+gHqW#HtnP?r288oT=m-0_ptH05J2KP -l0a8o&J12PZG}=zf8*d_oMRR2OnxD91wSYCYu~y((Yp30&Y4xVauHa4~ui7@UKUWI`(wl1$hy&^x= -W<(S=QDAySF>x|8-BFej#{^PDVI0|MifHtx@JCalBGkfVh^Eq*%3N@m&qiV97o#2)0t6PBkYEW3C%NT -R;4l-kj3I1TYJ#7ou$g799wM7-;VhyigEK2~aEiioFKCF;nn7T)%5YNhB6U;CL9f(vwVu?2Vd>n&Mmm -RKsc(!={W)=b%OTCa*%MMfA&#**4H-BTt0cmL} -qQyXoyVDX_^&dL9I&Y$q2E`uEC5D%RHkwBg8U`SVo9tzCkr3#4?Lm=10WQqi4Q$FbgrcS!gCV3u#arV -;SL-C8_0Ld1f}oGDd?kvoU6j5l8QA8Ox3E*xl?c8SQLYY9=L16XMuGQ|gtOJt50t;+T2Q3Bw#1o)8DU -a*JW^!PMMhm=lJ%MJ%Tn%Yhi_i4ftrg(W9ga=Y|$f+e@GuN?)HBB}G)}oAaU!(7g=IT$BlQPv*LN5}2l#niA -6_52+IrbR3op2W!d9zFJcJlr~7pl!i$4E?TDHs3(n$OiD7D?Nt!R_>~#IXq}dr3+Ty=J+z0H_8_as@?gHUO|N@R>)$#jr-B~4suvJ2S@oXRs(S$PR@^j7IrbCLj4%MNV%t5T?pa_sIH2WEt`%mbwkHS9Rc?iI5;nP(S5ZJ#g>N||Q)I@kl4MHTt0K*n)m% -}pYBXc+&BvTpmed-QKbdJ7H?%qB*z!jwT|PY*_%72szwf5Rsr1h8)4K4zxwa?GF*`*pw*vKmak6N|`c -2R8Zx;sj*3hHQX^c6e{>}QXUZ`gm_0+-}`6#sY=;G4@hHp1JpLS2*?w&z{11#9eI -2q-jsoA?jI^;roT!r*t5N3r=VY5m%<0w5M@>hdzh>DO6Ke1ocTC`SfG6+7WAMgZT)Fr9u?9cCXUSlX;M)Rqm&(va0li)*3&WwzME -;EDn|K_=sx_yYf+ZX0eqOk`;5p+fsIe&;^G6ws(OAeMeUu{g#?1J1mil_@k~31!nO+)BmD8DCl08c?j -AyJAn4G^|FILk_{nqf***eazXD25woWCrF+x5fX47cKB#qY#kPg18|YswHfmF$TmXNufNa(K9(o;Zhp -8Y4NP8lqbCo8gIZq*`THuU~TZ;#>}LzgUWCKY8&uy}W9FOwX$8AA-~1r`cpSN`A#xZ^XX%)#X8lvjfi -6wNxTQTy#z&6@5MQ(w%SCpD#AUR53QQ3%JziQ8sdz%f%q>&Nou_emZlyb?+#KvsgiFbH2G>E`}-mn_Z -)O|4wzI^1eK&y)eji>xWhQ1s|GQ*{9;mjo51HrH^mKQoIr+YwuIqv -T@atk#3`<$5J94yLkych2wcp!jr(zW9f6rI26*AMrb>Fj!Tphgb16su|f?()pLo;j~yJ -Ko_~G@g=B)>Gj#XMP~J1J*bFgE)|=d%^!^xN>mipzXFhG~aAQKfpfnOj&obBeI3Ux^GDgw$1EJuh#t? -UoL3XA(r}gW3w0@`jwO#gYHnW84Fv1r>auxX59~>wE{<@Nwrv?Mt_vRr9|*QUsKUNjLL;9x+uNBmFq{ -=e+L-$%u#=D9ReQETrwsCm9ekar>>11F0*}0gW-0{bo@rFm*v5+faQprELDSDCKQ0 -^9+joeLWeeVOggMDW`k^cWi&cwwLd@tS8Z8;@+=6Kgzad2uvL98C`M9&9dXXQdIx1)M>cu1^IcQN($M -%lJ>oCRs^78pnM?{~pCTb-_tuMN2@hHutiVe66`^K%G3zY3()W7w4xwgCajoj^+XMH@Q?I*NXh8$W)OTw>E0AVfJtIhlh(Q;?{BK@+0VM4*f+9JHkh#6*LNfEw~X0F*(;RVDP*FkuMQVVHnYON160G-Rm2uMil -8wNSJHzwtp~Qiy>=QKKSI>3mg*=1Ms7(GvNpB?_qpct#Nhjv`cQ%uZA@P_UpmY6C?D_(DJkqeSEp1Bl -F3Qp_iouV7~Zgc*QkVMrB)1&aVbi_}E2*hM9QSOPUkODyFwV+bh56c&stGg$3`!WULUOHCu!7`ayPGs -1^q${0-Qf~Et*N5=a1Um3PnpT*%1rK~iw{{p28ERz_;6oY$ -pg|DM4*f+928b^1PMoA@(@dq12`VTKMvqHfa6I63WER`2Vfk4@ftFO6bDiq;BcVA2^EfY!Ueel2zZAn -sxi|we6QV>SZb)6e6Qhq4c}|{Ui$#D_Cruk6*X`jPz&6y$?Y0$*W`8`qEDDm1${ycM!P@_3~CCVI>Bh -bpiXLHDZpxm28M-Lvjh~n7@9eC$2zNdNR=3rv~Y;ku#$kQ5nOeHX{#Gp(ZFTGuSQhW6jilAh45>DUjz -IaY&wmn%xi#HgE(mjqz19k1R*Gz36>^=6zv}kmT^OXGytR_KpFtj0FVYqG(e(3I5Y@^hWu&ZPXk99IM -R?K4HkYw`)Pwcv_Wh%f(ArvG~`>0<x-AC -xD}vt9C)Vo51cSaI^^8PX*S6NPN(cSP>`#rH)uJBEk5AK^S2oIJ)xy6#Y2t5`a)}^y3&18?>lN2ewWD -I7JhKQUi8V(ZaaSi$TYVYU$=wK^Sy?1e$=34GO$20E>3+E><>nI1jr7Cr^jds$=L5M>>w-yBe}JSeSW -FZf%M-5h@KTU5tu4d_7OisiXmD0?I*KMLo3wB&|Wg^3g5u&=8;@Ktq6rkQzcCG@=UhV#?%?P&Ar@V%R -7Gg-}F7Q3Kkc62rt8vN3677?DxCTFjC%s=#{QPz7SOi%N}Q)Y&H~>~6?Afi8l&2zb$v7aeTE8R)gZOE -8EDz$&Okpr!L*8fINmK^3$F)Doa2pcbIj0huo3pbe;kr;AA_hLAe^ByB*m1kXVPF9ojp^nj=B|E@osInE1bwf;Ll*-D%7e((9*c|ULd*QLu -k500auiSnyW)rP*%mfMf|6SdwwJ>;pkoe8X)A#E)j8@;rb#N{naMPNU2=IOUyL -EE^TnT?DMGY9%npBjD%gOI3EyFk_fJXaJ}x2KNwq#IuR6?_K9nA9XFR(}jt3fSr#jI;JIR3!HQ9iW*w -1(hk@u4*1mn9R2X#`T}y#zw13BMvic;S4#xVvA9-WhK3pB`k!@aKnm(czLyEmYV-NJ;mCttS -`(R3L4FE>xh-S^_1G_+Vh)JswD&27HEQ6H&A2OflMCfeyAkN+(PogAyKG}9$#U5;13iev|On4GTMAs8-$KBXR*4nV&a@ ->ZTH@6DqAAYi-+pI<32nF7vei#Hru%oWxiEi#?$Jgr4ceZn9^3)uvYdh8Nd^7NV*t}m1x6{d^lDU}d- -9PL9;dHJF(}}TR<1~DwE@Er(X0dXV3I`e{r~CiuY4-woe2K4&-MWpD#!in0k2G~Jh>gSQDrj;$4EGnW -UcG<+{`|dvzV6?=n!B$13Z}gJ1Z-K6#37sST=?fKQ;U0AQgCX0+x~IPCcy{e>e=?*ZYui-&e7F-_`)AtK4jA*i^_ZY5*#vK=C{031F!44n8azqGnl<8(=?fGZOLaie_W|=C -HI%0pse_mXV5*#7HXm6IM1p&%4$8;>4&o+tBNS*$JbFA#jvkx$f7Dvl6Sl|zI`qVUdB2CUvpNJd_)^4w;X)X1OOh^^*hfhUiUBI{Fh_GF7NjUKfM6~#0Ln`0dY -0hGI9DX5Y6!loIaFq2B;wbNLq9CCR?QZ2-bbWYXliY#($YJO?$6)}6U(U`>>EE+2pAiauSAU7NeciUN+ -aBw-$GYu_ZhNBJp6a%zy6u^6d#2l->$c~*?S*c8q1#^SwwJo?jc$9R+m3YGk#0NIZO6LpM7N#jwo~19 -s@u+V+nH+H+f{A!*V$E%v2B*+zeR9lbIG3Lswt4Ys!5t>ygj!{u-Z2J3=9KL7>KqqVpYuxWU3^aB_)r -M>sge|SX^>E>~=U?CH1ug!GS!+z@Ig6AFwMhUs72RR(X@oprtPJSrc=B$dWYWizt;46kkg*3=3jK8r4 -AD%L;Z>Gr0?dUN!MG!_COC?V85a5>>$QHCsotm!rKab_kRUD8t>Y>kD$R$?86I9pI2Xn!_DXA|I64cU -dGU@2{Qw0B3(ha31;~X8Sw6FJGFI7rVeS@LxFS-p+tM!mStgDIoP=mQ>L+<%igN1Ojlj5p3Mi7EddF@ -Q1eqLL$qv$#hvvR5$EX#C==t^UrC@4=<%Tv`tOurepXKKtd -1?9PFd9GSsY`(e|YRpRo<)vzQqgviVkCy#>`YXt+fnbSVuO}X(*g>!rb*ezXMKzn{B$ -=+GTh6As8^}(uJ(7~;t89y)TwJT?EFs$}_ifez18gT9I|> -5Bs6q%^|zIvL6(AmC%~u(P$6I;R>Y`?Tls3a>~mgY;_Lepj}SQ)0CECECJvEh+X|%94SIUL#$oG<$*Q -rq6_q^AAXo)(Xt9I9L{xiAjDelGCH&_{@oZA`gg-Ij|gd -iNrWde@w%2NQ;caIXat+Ss2YHLRzxhEJHhU$JW_{OCv{n$D144fMwCe+}T)odbgs9Zs6mihyH+(R&sE*j7$;x9oMrU^h{BZ<+LpJN<$U&Gu@JzbNhfq(Lw?W8VVxzMI199P9lzbVH$WzV` -b!Z>o+9F77*BK${0AKi_T-#BY%thz8Ft$1jnU%hm$w)LK(77E#V_yPW< -^sg*^@VA%!ll!pKexBxlpXT&HkE~H!Lc_ahkF7`}!KrV%jX&_CC7T2STIl4p^M#6|czNiSsNI&f-|Yz ->wL&&p*SsYFrSEab7NdPQWnY1ZPVkpr3%*gAt&Z>lGRCw((ysF3vyls>rj74_IF6jc&6yX_bRv!Q&ee -yU}cxWOD$KvQ-I)Lu1Pvtw)BY6j418v!d@9fK4Nu(NbC7!126Tvs_baC1Aq}FZqz2MX6j4>CWq@it4) -5Yw00|dmUr_CCKv_w(-q@gl)3uS{T+?buxNH3q;2+kvZ=O>vyQ>1OefoyD`vfh0%q6Z=$;Q3|56c8j3 -^iQBrYs0c)UOV!wABWx_tGG31~|QRo6JZxwV%kfevy>&LRker&<}T!AbgH?@`%0{Al^98vIiJpkpPbj -;{FD(7W>o6LA+>`U74qIInn1S;qM}iV -)!Cs32|Ox@P}mDsN%mhFcKFB!y;t5LM=DW(`G4ROlgvFR%4tJ;K(&;&t(DvC~DIB7|r-kuxcQnz1{sM -yWNfrq10dkfb|wSBoVk=QtUVqZQP!Bm#n)~C;)4G^qRfT%O$}39lSY-Zi;n_-j@G@f$8Zzu=+B6ih`n -@N^}NnXXwarAWpz^#O7drjKnd6>dNCNrKT)ea>RHfT|fg6dYU;ojnG#gRRM>$;LJbvuEC% -Z0$W-)*88v*&*jw;?3LYo(gwKXzjKNC6{luu#&>;0iRL)XfQGg!Z)uRu;;G)wA*D>QjWhKP~!0g9xUQ -|*1n!!3F!}cae*aGd1tLuY4v8~ElZV=AOGSO?2Ei -~;7F7l3(-fPB{h`6qYXiJ80(r1GNO~CC&7Y4yJz3-5d<(q&_Efgn -I~mYe0U2|^%ZeBqC?SrUR{0Vnc=(QEZ;EyoEd@>9Or3eo2A?Ago6koJ$w9-y*nfMZX -^$?B29ouZn=upm=Ma3xkh|ELn6#VBR^H52-%7k1sh4p3vnSGT&?is{^)~1j!79>-gx -n|rB_TO;=7j0zD)-eH2Ry|b~)hButf)GKzR+B0hN@7&Rgp7qK;%~`XKgtxnQM1gR?0m^~hHO_uGQ)5* -5f0MMxJkEw)57kh>))QaxHRyNnE(RH_Fn9N45_iZFi@BsXAtV)k~`D9HmWX5{ -5#?y)s6i6PsyAzy***l;|<=r&4HOmvNz*sO*9(+_8;ax(e@6fg`29XuO%FeA+!k$MuutY;>7K+TUv1_ -!xOfxT|hlPAw#A3EaSrVTSvid;=*Y=7VfKqM|<6I(X7*#Rq~$>nCG6InFwoGa$Ci -d*=yJEMVX2b;Jg%3Yl`x_OeZ_RN!{(9pfuUN!%R9wN$Jrmd~|YjcILBUHzw3{-VljydX8!SE$_gBmi4 -9HNjCnmc;xeqrXZD9B@AHp9VBO;O!;1s|GGB63X+=U_Br23unS%!-l -it!nH%9zN-Y^V=wm7^=3M3#^Ra2vm!-%0Mn=NIM3=NYxtJkC@~;;i)I}!%DgH1 -1DnA_o>p|I>Zq*y14j~^!(rF=DAqXQ0{*S8sTc?iYgloVWf&!(sBXougS&F+-zU{4B^uA8L~QdC=0Bi -BF5ABf=+agYMTXyA!0;b@RKO8W0IRSc9Z@jmyU33fS^`sh(2{uz5YDT+IL2Qzel{ggZ=9eU;w-aWBv{ -*neSN6pAk}v1b|DPU&dvgPR(;+EPlIW@7r^_sEI*tM3bMl2PGhmLtO`+Lz>L9RE2S^BW -HF{)wNoi0z*MlFe5=jwkjZt(?&*5?+u$52xO-Pwu4#<%)hbSC}VdTSVYVlfgMzekemvD(K>dLYzKpB* -ZE8oxM5ixnf~N+Cv~eH+DU9v*^p3JA_((Qbxn@;--6#26iP;^Llc(#w2``R>xo=#9|d2G^%i^kFWkJ{g7V8MJ$Bjq^5yQw-Tgb -v9LQmT*?iLkjVHCqkRKqY`OhCKgiPQ62o~Mas~@YYM;frHoo}5Ol*oglOmWS;Gj;kYC^!!ENt%FS&Ly -8G_HvhMsblJ8Smr~|vi5Mb47(3v8E*-f*P=A4MyGEmiJq;ZD%(N6i6e}^uQ*x_EN|M1E5E*N8@Gfpk- -P}WboBCA`|LFf6X~luP$j?_63}9Qgd6kP?6)~*^!2g7}DfLCin0b1tkhb+B!LNdU4fRB77@*9}z$rgECqiYI -mgawBq#2G3m1JIOm`c<>UI>!YZ0kq7~7ew7RZ%T@(#)xPDN=vfC1`$n+i1OJ!#D#}m)r~w+1dX6@E?5 -hwsT!p~$h(nqsxq1JNF;FrkjRr=VPyl^v6$b6Q;cpYs60(X&I2FI=-Rxbv}c{(2Y|`d&_ -2lq(?Wb4==~`lvR}~v0J)0fM)379v_}ONL2gF;LK7p-I!rd@(b1|Ng@U_&cXIQ*N|_F8&ONk|I}SgxT -51hPQH`I7Nf}9UKdBy0o&W#InRh0|hhzqp08Cd{Q0#7qpt1_>Naukse5+{5CwZAHl57H61gOo0l_od9 -WDf$85x=%yZv_H7Usa>M{iplAAqr)TgUon+h*royGy4C>GWzOo(09>_$&Bqtpjfh{Kw2eI>$E~~4~~= -v=7B}+H^!X-%NklTfSyCs@u -BT|FY->E)s_Rt)U`3%i85myU1-P^)MB%MW89Ql+5mf4_EMlA|QQy}SOb}Bc-vT+Rq`ex0O>os+-Xz&h;+27=0Fb` -II2d}kpo@u$74TG&+Yj(=D!eaY-@8o^HlJ(t)Y%T4tkqjaqW7&GVi7x)r -uOPhzPvL8V29;@3Mn)QiE|o_5**?`u@-H*J9IMEv4v6BM%|hb3#z(X8Bi)(c-7>)LWp==I(=? -bWdfzW)LJYlHSYQIr&RL#xilopa`19r6S`e!RD#$f08}=JCB9Q7WjheXq%9_25K4uww}wZwWC!g=uQg -pz!S?(h!w>ZA^Xist)}H1tPH`EKU^=veS4L3{bZdD{j=r$ynd5bnW4AU{t-@puK$|0IREW!9gWZ#Y*ak#cBq*E{oqLj&$V=0aJJLRrwS-V12?QV^bNN`rEy -)xBJlxmsK!yZ+jNXD(f4-E?z;CJ0CSj&vTDUF2(XKGrH}w5!mB>mD0m+d(Uk9Y -u(b^Wj)?^xS7`W#A;l-z73GEkI2{#E!CLPy2td1*Rr$)9d=bZ?|Sa&Q>-F<_gs_BhAm$ifH2dQ)GCV< -V0QBm!W#?;PyEL%O$q5{ERK=RdZC;mKjnHs}2`17ZvBAMpVii1`<5d^) -p2?BKHZ}T4!KxqtpuI`IJFDJi<`#L%`N?0OS~JBg(#M4LG?UcV;iN*<_e)8%5=!u^7O<%)s#8k~ImSN -lp@YtqinoS*WP0ASc!cb&Pjz0rRTYP}8CtMcZk>j6Yd^G)%i^HGWf*TI -!tJ;#*kw7LH{2aN3tGIpnT5l%X|u$eNo4m=-YPZuU4T>uV>+ -`=R2$-2*<^@Nfq7!cfR$h)qdtC%e(+da^E-hHZ&1Gu(p3JYI9J)u3=qK^rB|5-*aXhaohrPR$@$J%#Q -`tMt-9@48082R^Okn2Aj+t^JR5vfT^Q3%jkvLA5mYY7D>TvG76C5N9w}7h9&}Rh4I!UhX!IfT}$=mQ; -dp`R+VMAfqGw`T)i$TUSd5c0yHDJ> -zbDU>USdTUGG11AtFCPs84Sma!B<7y2CV*8s2m!w-6Jb++XMTxSr!-h28X39XZRCni;^xF1RWprU4t3ebzpNf)vR(gVC+5-i%`KQzM0a>Xy3V>q6wrtP4eUA*(uH#0RrCv3$I -9PAI%%UHfvQOgeporsb@h$T?KkKuy@!#A#v?lc_Nh)65P`EBK4~ppLDqBc14}NneWSpe5)B8;mT{&J= -!k9M5}hQVh>_Icq%1V|r@dD{j7B|KK?Xk2Eak`flIrM&UROkv-Iv^9zvIt#FCdL70axTSn_WbUK>r`? -{v_}M-Tq~>ZsEy4q42`#TeRxip86bYD!k#mKq?bR_b$HS@EZ=?DDOf6ARFI4XDJpWacbx$028Ru$4@W -|8eX87Jdp8HpYAN2OoLhsx9POu`*YoM%3zxTB#=vW?jINiwOyo!^ly=EAVfqqrg$BU=wsQHCTuzusez -`w(de~#!LOzGF5iNNn?|#=5JP6LE94U_ixKxpiO*>4JePMPrdWAJVDMG}RqG@E*dkAyJ2)ZvYFSYxX1IPtZMQ5KJtPh+FMwy -->X+SJ?OD0UF0Poed&QJ${b&VrBc+d=-jMcUFh)HO}%{!OQL-f20t5G3&Lno21BJ6 -iCbcY5Yqn5ol%PaZtTCiK5xt+p4xhBfmi6Mcp-BapI!xL$bm#e`!;1`>Qnp}ne%Ef;|MG>R)T5k8W4ByF^g)|d}8ar6Sl2pIr@o#aX -gp@jpUXK55*5ksad<+CbNaCka#mE#8~9Bz&mWLv+@Pfy1Dy!aQK^VuidONe%yD$tNjgdM6Uy6;0Nu(t -cj3Rb!ptDQ$tW6a0@J25aU9+-ka2YZ4qJksnv+cCDm(dUD@}!O}9S?TCitcr!O`{u$--}ilYv|IF-nO -0gwSsgljeUb)-Gqq#e97u!TV(}&ciBn|JRHsUD|Xb2CH-GDGTvoq)OwIZ?U-R+_;#Be)0MGAcqwE&ib -AZ4$+(Hr->s tTWB6@3X*F$*POByRRP`UG^m`DPi!%{9gm=q -K`kk8{-O`SpYrd^$IT_>I@>lQsts9gCuHpq#9siQX)l`~!mi!}o5fF_&YTRSJu0Nx -Jo?eg>&QpAuF0h_>w!O~AJXeq9$zO6w%O;57CRFqaDN;fov9X^k_^Y$Ep7Qu7~42Gvn}5x=)Ix}NjZ$ -A2UqC5A#cq(tP%|RRj00UVV$lu7A$^4r*4O7LVlc9wIxJ=6}Tu`>6e>tB -@vOFt&0rG9()%J>!bbrRi(C2G*H1M`QpT@=dK1DsH=z^y~Cl;y!&WZtSI2=iCn$Ko(THQQoYI+*all4 -wZi=ZEO&e*;iU0|XQR000O8B@~!W#71fszZ?JnBr5>`A^-pYaA|NaUv_0~WN&gWXmo9CHEd~OFJE+TY -h`X}dS!AhaCz-rdvn`1w*Nn$0%c}uxwTZMyW2-vXD5m6X=eM#HtyZ*bv?c`2}!IeQYA<`YMbwV&jAP$ -B*B+Ny<&1YjVu8NzpSY%elTLrJ4pyjPXl5y0{_HCE#Gyi6#_LTi`n$|h{ivUEchWby)gNe%}tBGoSK -1YL7S|s81+IYbZF`CCbJypdm18*Bk|WS^EnMZ|K7i+L0K)^nK&VopVD`y@yD~D&dW-N{w?(`7IO;4LU -VGF{8?5ZpmP^SH-pYABaBO5JKj7BSr|}rCM$e*@x%A8@as4H>!fHHI8>IofX|N}Nq*p8_FrPJJ8yx&dBzchHb -M8jZ-?w{LUAIO_zp*S>@DfJ!;m62Xj-N@p*io3YSiRS<%=VDurivt(370%FM8v_J&LJILto5wCSPN(6M65 -gOQ5EVNj|Gg=P+-A_TGgesX6$0Ab`0K#LVwEzF1#P$g=heRwgT~OOtVQ~oIH1MOzloQ}g0xk7yr0S7- -3e@f>sBNfN1wp+FKzso&v2dtsD@JlZ1^5JkM+`_Q4MkwaE-&ZNcVUhqj3YoiWg!}oGR!P%d8L0xG(st -H03KXk9jaOtnt&x~t0VFs90;JFXu`Cn)K)d(-f>+yn5bXX}m^rI>UO#)e$jJpP@vN=xFZG2gir+%X*OVv# -j}CF(l~Hai}UO`iE#ne~e}~G&n=BUc?~duu9n -1HRsM*vRcV^hnS0c=2N@N=g86P!~(8Q7nREsV%3aH1~r!6$-s2Y|uGHwJOWBrcT-?$wxheki{$44hk) -J*XAO`$N<;3FH}7PW}KJ1ov&D4s-XM{1BKn#33i-$#H5KW5LF(fKm#HkI(#`5c(o*75NGFVciONR22Q -CkRVdca-Ip7hwjq>X_?-H#_BUOZ_%h3gkS&y3N73B4ni_@pcc2glm(WA-&jSlmBC@oa4c*{jt&jZLUy -8HfL4|V$#|q}ti$Z@`u4KpzM~icA@K(!&n?uS+Ll1_6qdm1b-M9#^^Wj$%CX%hetvN+4 -i6@6UL`BGNDO41)vLNKzuX>rpiBU427TzzKV>2erZcyCd95Xb(U541bqtlp?eGfO*%2%A%^2~3}S|3K -Nxzz=D@LrJknbjN6lzqyLZqP=-r=2GA#Hj`TYKUsu9!$P7Vgg?@qruJ|^FuC|rd<;A?=MLjaRp+k>>& -Gbr`s_#Dd}Y8h_)8X6yMt#-w33^an(2G#p&={=PWEs^{ -lt@xSNz?-Tw-BLU(gD)5@0OH!Z9Z~oSn*572!ze`$wm*4!OEvnr)q*K -KKiEo(lPw4TdvUbLn4Le_jKX}y%+oVKNPDr=reT4(Z`^R~3k6Ky({!~~*U;SFW(`|C -G9tdI())lQS2|w5QD{M^qocLAxv+mjll~ezr1`i{G7aAzziQ|jFIEo>_DWKz+dfs^zRQ~c({_?SajKfU -ub1DLGKBjNf2%P+c5zoED=nyib1T(>(|UGcz*`uSHgY4-N~997D=0jGV|vCm8TQ-G) -ofYUL0`>PO*_(V1iUuJ0K+%~E642qNte1KM)D+8 ->iwA^`&ekVz?^ZALBx#8HS60UHwu0nZm1%LNAIGc&~5bV4XD3=IF+D4BH>!|^LsTxs}xd3=SQF_%Tfx -n91=0OkcCm~V|EY*+7}6_&`^1agMYFVx>oPBP^4n5YquS~?vwk2NzL1 -oBq||8b6zOxP%i1g^vWY>tfx6c=!A5*FP6&oEnqOCVF4qZI=>j)2L2fiDP^gl$Rz8~}K(31E~!f0pg1 -$l(lfwv^;=G{)qJted*-Gn$(0`j#%(HM-1e2ouK6J57c~Lk*uBP>N0D4)%Qj`A@$tTYZFkI7t)bJOo)r6=;n~xxCXvAA^jY!qic)Xh%Y%VrtUwdus|&c7Vw_iKNta+uz<1GDMc4pM -#2qS5VTue(Sd86&lPzsD5Mic%S=APDPChCti()wMl|{ce1`=`SD=%SZaT2Oo|BLrL#0wNGo&rHwgg#2 -dKq^h1!{F=nM#b82b}p>Wf|bwt$uLe}lu-e)hA$QAYu4S>0XVp$V?sR)TOl~`Y!?C}90Sq`MT|gkPYp -}gAsJPc>XJow^x%ToFzc~!?VQI82e`b?|B*?hG0qR;^>}EKL2+dnowr-If*!^j63TYF@Q}y0$ori02DsS{yho<(_=0!ndkR86{f1$R!>ecICbXP7v{vc()#*PH -R_Iid^_JR=Rc-YMmBUjfJJ)9as#b{}XDkijOI3)PIf;aZjn1808W;gQsl(>G -&)C4%=2C7jJ@CvmIwxj7P6%IW;vXR^GY{C#-R#wrney9el#3n&T%+=24e-U3SD7}QspB%E8fRg!j}QS -kIZ_zwN#^HgxySdF_p_C;<+jK#djCBH+7}R -BzHug1PM~vu>m7@9EPPv!-m&yfpmzej6KL!N%0Y5Pf1m(;jYlx=L;5c4z$G-mZ_!pm0Pn%J25h@@a;< -G`ugr{Dfin+FW38_BJZ-L5_2&KPRlV$Kqv~_l1QB+E=zRpeK~2wV;(~7yp-kNnXpsb%5;Qk1nj&GncVkO`yZ$Djpx@=Tc&51u12HCROb -L-ag?_Fw7s4YpnuVEzU0j}^?N#j{z+uz%_Z^lxeroHCfq(uJYC0;Y}0{Cw@2#aEH|J25j|~c85JFu6+BfWD{*=q9`k$e?DO -O@HcJ&QHF>y_4|>r(K>eD_>+je^k?|mS)T31{dM@3`z~-~9lF^KKO4?!3y8kFSLz1%QNL?Ok+DKl8Sh{j`nI*_5+tf)!ui;zM$XYD+f&{_D_d|5&_#B~O7c`@h|wOSH7AYF$ -f${hSAL{aLnU?71%4;(eCqK3t2mZhjL!VRe)HZ&5QFy!x@xJU7tW7^$S1N!E99No}1Fav1m?>{ph9EV -!E?q?5zLlq&Oo{N=pu$A$*Lo96#V6 -qoZr4LN{z+_bp^?}J|0+Yr-T(RA0H-eM2gXQr_?Iwg3-Q!2~IY_0O{ODzF^7|7<*$oSJOf$eV6~(@Ebx1a8|nnjPh~{b6yr -%9*rzdn6{=zeLi!A8lVEY3A0Jdu3#}Pw$PCEAj_pmc422m}xd-as^{Mvgg&668~9sWXC*~sx5cGB!KM!YSo-yu%+rHj4|>}Gy^X9Ntz^~ud{k6>Z=lzA+Sl(Hb@T -ls5`;r5$$9VKHvXdWc$8X$8ue#)y6j{D^M}tsYsACe_vGV69$5&p1tHopNtSPndHb;Kj&XkI)>Jb3oc -f1WUh&;v{MGMMNl85R^I~C3Glk!Y{-F-HcQnVne9*r-4(x^M$|I}iGLx;|7f6-de^1?BU__WZ~oQLnu -xwqlP<^zBnPAtv6E`3`&?TJ$x4aVP{H(QEps;|fw%46j7EEe)3I|CmW2>6EAy8*vpG!r@^7kJg%l=GG -)xR;~G5CHgzp`7O^t>k()pWLDMVf} -c*f7VsrC7~O5s_}oBbaI5j%=gvFMHS@;5qlGj2opI_POY#jBQ#gHOr+1CJJCp7{2wd^x>0O)I_uKR|swBH{}8;+ZQA%%>0ltY$t9;*WJ&547J~&x%9q#CjN4tDU!=gVl^D> -}Zu`-(aFs9RFXQI -zqYiEKUzWinGbF(V)31)4@vpgG4kbfzi|C;Ug;l`AIw`yOybddVu^zB1_j*1HF54JaYAA;u|t8Ft}FI -*CGJEa74a)l@#nDet>#Q2FeYpe2W<4i`V#*8HN7L3>^z7D;J -1F=-dS#IFO$CC<&0#@+E4M5sQA)yx^K<$GkF{Gc(Ncjo3~A+JpN$#{5oL_>oI1k;47dv8H^RhyyIpl9Ldm*R7XpD*BZHRufv7Fh7VqS72czZRbB?Ptr$!e1-XZTPJq*>*PuE_++qBdF2~s>n^HFPgJG=chU&J#1G_ZkY~8zTsp#hZT -oschn#;#!)jqi3Pu6fcK&b<(J62>rlVvqovMp|_aJ(3&hp`kIzJOmC>PqMAZ9Pl4g~2z@BsN?`x0W*S -bshnt>z?KX^bf?mhrNCG_Pwlqg8{|ob+5t7H)-4t(tmINU8r5x?|2EU#d>_n-9c5VuBu<}&boGIm3-K -FB+5)f>6HS$DcIy-B>}6mN(`bEpImjk(k?b_T({QGG>-zqxH2TsO0u@38N^Z>flAe;;sxM*o%JdE+ -+<9n$h+{as#cNfUsT-{mELC$D(+|_jo*c}duZII+fBsPz}uFJs>JXWdVU_+?b#74&A2{AOaOn+q -5O>|z9+b*h-24sw9Y-Y**Q=P+4G!(4Vm3#)d_t)F?VNpzqB^ziQmt;SOHE?lO`FO#=e(KYJm$E{_B#L -GB|a6q4)H1O`ZXe?^^1WUb&VVR{6c(o@$>ga-L7AJ28M%$Ux|tqetv1_cKhN}ulu@TH`?KwWkC6+MSO -}I7vttiH!tE-?ag2FO}PJ}i+exN90VjE-JV!{a%Z+MK=^gB&1V4#>SY?K4N`(j5NreE7eBY0Sr3ivy@VF@qa(LirJ%$ -?GC9e)0EZ@c7UIDcfO|8Q9eRhWMVl;8oEtvru^(Wrj0gJaqKn*+Kl)PL2%H$IJ%9vqhmGNAECqEa@)6 -yPThiOpG67XZb4h$CU4&v4+(D%)IH-Nq|+{AAG8AzHc6x6p?CcS78?{CQy-Ot1`c};IWxw0$aM{eN1mESh}+>JoQ-kK(Vk&RqL~{O&$M6&P?o~q=%9oN;bl+H9 -$$N32!!o65dnlWxDg+v){yLKFEHt4rVo7ul^nw2irFL_91U_m`0oFW?mXAO@AK(gPP38d;IICl}k-4T -S4Z7S+Fk!9<0`2gedn$=bhGLhMQ~zU88ph~12-SpYqAJf9E<|omZHJjKdp-#$D2C$!INX_mgI^8NdJ{5_$tg8ma -_@vw;zh1YrZk`=F!^0R;neixYh2>qiKHHued$MD7>|2o;UCG>MHz!n%RVE=*@5DVg4ftYV1k7Wl9HO2 -`qmo+c!Jc4cm4Z*nhabZu-kY-wUIUv+e8Y;!Jfd6iaeZ`(Ey{_bCKP*GIM6gA0;VKbHiTUul7(5* -mI?1LK!v~;%F$fQ70vE8EoeRrg6OMYoHA&4yA-NWO(Jjbcg3&`_n;~Il`4suaz<3LQTt{R7V_)eje57 -J7lQfQPcbcuN-9U4*D!C=Z5Ze63@n7`F!Q{gQwwbEMFS^j{k)F!iXwb669a=G(r&a8(6pr;kB7FCAMA -ZEp@3Kyu{Eq#EnP~ucHmCLU^p@`L5D#w-!ADxjXoHpzBbzokz`!=t%!h8#6qdRi-3B0R>n`*PLptOT -lS*jVZI_%UT{@A?y`QqZ7UZc?{0Xx@CRl)=bq!7()?qIS8H%D5k^pX-{T?Vfe%usQ5NhB>STO$x^4Ry -OWAvBiv{BS!uzx;_~DxC#XR2{smr<+m%>kO^flsqfuq)4W3))Y~d!7by{-vr4WiyJUP!fNUT!eR+pc_ -&$ea2bX$cIQmVvywFovW?QPz+x_xv{aZHrj214pe;m=8}smGvt8-sD@s2;h$?;-eGHL{o+SWJpk7y$y -kRM=m{fSpI_Km}8A@&H(?I>9tP`rhIA5i^+D?Ys)INcK5fozC##OpGG*PrYJ)LdtTs?gLeA3Kp_9+BO -o7r<|ZG%@AKmM}C>Nwl{A8+{))}r}_$8A>)kAO{t!Sf*YaHlGjoEs54P=nIH;rCQ;O~XxCROA`#cUxS -_!nIM1qUa@kh0tArt+9|)rnS*Eok*e`4!qt!3Z#-w3g=1d9@my)E{NHvL@H?=S0hrrY6GsREAoORg;o -!Q!wJ$JXF)t5NgNQY22m7fecf|2aJx~`xHgFnsUKx1RBhx;Dp7e1-tN4T_q06ugyg(IVq0as=7P`2aF -isDf#0;GkQ_weOB_wLW&)ucM9Gj|pQEoyYccl1JGj0EF9=A#Z+I@i!*VXE0}G+Jo9X=Ig3&|+-j$82a -MZ68V*hLt@&HD18WkMYJde3Q({SkPIO)~+2OplCv-V066wd{!GZZDqs+5LeP_4NLpV|)b*7nz};3tK%m3 -|(>8#VUw`Idm#&!sI=vM1V@`V-iYo=0f}c@Q#C_Ry7J=U=W-?bvx;vg1X1#R1TD# -_+oW=K@a=@anH5$*69S!=+_I@I4HKb=gN8OWLsiws5X0XzQWxR+I!LV9V?xNsVS{`=HQKZ5GlNdd!?5 -Nnf=MLpNYijI_OQ18@;6dlZ9YPrwblvqIXYrbd4#90YW^E-N@o;c(>sQDmQ|tm?HA!>#kaR#xdVl!Vy -w96NYa+XJ_|I{?)1AK4I*x%hYPzhk2g*3w0bt`&7YVav;i|+Pw8Jf~TI{W}hendd|r13T^QrE0HfJ8>NI4tHY)xEpuTcl6ud?*GDd=W4%s2#V+AHh7-G`us-&mkE9Ph~(@;Q -v{e`@;qy5DqoBlE7_<0i1&=b{jYbpL)JYwd8ng(Kz8OBV6b@vYzXLa@Nm}C@WJ-coAk4GxaDKbNbf|g -3$B=wY_7xc!-o(obFkbCEMzjkFn*tx@v2anMpEB$xJE_9iE;4eStjieS -ri?xw`kg?boj?Mg)LBAP@)y0?0*{u6w`zwkV4-3xE5q7p*sGR`k4ip2lSn{#O5f-V36eD38+Qd9O?&K -Mlg);;0BSFU}u5YRcqA7A4C^kIvq|>iwzrE=@x9xBBZpANAD#9w%iSANVUT^L$nC@wXMU!n{+Mp275@ -ME!qLRuzWc`gt!4H?il3LGLyyR=v;v`s&rI(-)7Q*VRVJjTcA3f#zwGMoGanpV!EqLF&ruYN-N0&9cx -h;`@WTodebPC@6ET=~E5rJ^kick4p3!&Q!I9x9rw=;AaO(cpFDacn~H^v}NUzM>Dw=Y=q+HLHn4fZa)!HJ=!o&}snRa|HQT(ULO_&u2s!*6kzN -4LYHKyTPiMot~@3EA9%7HL`*2YI+w^C3=?9Sj6fC6O>`M&ilriK#Zc+b9K4N@vwSS9LGb;vh^HJ8EuA -zv#VA>ju(AZxO|z(#X6By)>hIyfk`oY@Et{zhRzoYiv(>IrIFM2^5=6X133gzXdS` -Ri^>W6vNTU{tCece|6;@MWl9>7ALuES!b>kQ-EXnCi`cBpjC50_;-INFqV#MwJvCaGTGdQblCn?E~r* -43spd)+O4hK|q-epmzAR((|qFvCy&H5wf;NN&=l}{e|h=ShqEUy)Q+>T7Bb?0?x}^dX1&oP-HvA8PuJ^EZX2l!|J1O4aG-f_X}k638?{&aBMnpqcQrzL -$!Z>a2Jw15lkXI#~KOn9rYS?RGRT@x@_XAj`HolhogU)PbdDP>c7v4j -y#RFtq>FVE6r8RB$3so#KZO^UEUf5Cc&g6WT3TlJ*L6K`>qCtH8LX2{X&Tz%iaHznTF= -GZL_L(;z{3Xc0kwTqMcq!C-X_Ba|v~KPoOH-k;8raiQDA$p&d06e17EzY#Ag8DH%1$@CR -iyn&dbUeRNyb-6dg~tYEK8FxE%WUvWOJhjV4MG<1yL)H8K!&(V_Vy(gtNoJFv9CS{g42`+BI5M>3m1=+*RHww^|+ZJ;Ne;RsDF^NRPsVO;y|l_2;{XaG?)ysx -9<3`y&`g)tVP2UffpQyR3`%yu7En9p2Um(`2BJ3w*B$Ozv%=mE4QoX{sZkr4CCL|Fm0-wT^y8-L_VnO -4I6ODG+nsQ*(1A&)D3PMiCpSJl?5YZE1Ua+NO&%JqW!lwvM1WrY#RcwFOMmUPVlHr{cJ6aeMW(rTe;` -xoq2m`{Np`2lUGxFE2@?M32)b4r<=MA~b&oRdas&sFoT6TuEwzYRKPlNlvvr%2e_Ll3NNVDYCZQi75kfvYZ_U -iUXVd`e}@nCqT{fO##zOJ;lP6HiL6vn~h -6l*c>2Ur1@-vF_a8BHM^LO&$hjIKM?QJSo&X@yPW;4W+O{)|4yr+YS=F3uW0Y#?d;suC@bz)hN1pz+vKW{q)<&$4ZLbcXgF4thGf^ -8(AuV*EVOA|J2=>5OZN5AR9&Q3%Iq1zI-BC&?%#s$ZG8J^%2szYRVz%hD!cl<&py|B(=O}V>?!eNZ24 -B{OP)Hy_&SdE%4k17)uLQSS9TbYZA)?Gp;h6P-Nq8K-`mtyrF%yD^)+qF5YhuIb<0k${B$VX%N1FWF -*zs31*Jj9VU-#V6b=o#`&w%%O7v1`rRdY*)4^Z50l}1Omap`B#rodlc$8S|+E5B+os0HI7_LATMsWHr -SU9zp159DsrC@}7e-WVSgnHR-6H15drzie>52B-AD=y`FR-s%`-VjP)PXGw_1us|Ma#KNO&KX&%ym!_ -qyxp&x(z@t{nCa1kYXNU|)o3~GbwRjA(6LWke-(!bJo64bm{>1z}EOlc9_g@Lx#|MlpR7HY*&j%zga@ -alhy&9~otb9;OH;&${R&6eK`rqk&+cNM-p0`iL04(j7?-YSCXKW{JEGWpHYU0~Sn_rKBDhVV{JldD~v -RQQ&78yi2qy{K6+eWfBvzJrwt|IK49r`%6BYR)#=sD-u2(l246tk~1PRkb~ZDF0TA@Ym_`li7iZ3)-A@e5_>c2 -l^O#h{aPp6#Y^>jBVfwWRDJDYEElPe&C^n(Pr*v8Su$<7d@?=W11my!h)!I|#Bc-_zfRm?bX@_h^oCr -z3x#sqpe>pMK2MPGvvEel3|VbDdGRhbT_{t2+03H=PcO?55bGb83@N>@({Rvi-hNfN)P^e!5406~Qt~ -%gsKj_1fDf!eAdhER>78M+JjuPmhA=W)H!h9tV5WC0x(LJv~`OVI1W9X38R3mf0S4TBPaTG1NAG4~5= -2J%D5{%d5d2{BRF`vF%{>jbW7?JdKea>1KyXT7;A3lo -c0)?J@_Jg&U-sJh_jB~2ym4$vfm>5WObscvQIvW4egJy+YuUR0Rdk=i*3v*(X@@v1r6)1CF6)vG#R*+ -Y^?YAbTLn<(D1qF0{%9*Qcz?;qe3D_NKOD2hExVdCvER!RB*bCaa??A=GzY%>3DPk-N?e`$|Zljdo_Pb$)UR(aqevI&Y?Wx&~>>$}=PRl*& -cDv7`x?8(7a?9qc@}qU(bESgItlGeR(C;xk)T#deWeZq!q-N$)sl-2^JO7_;zpD;;8Ft`s%kf(6HIjWoTk!%R|Y^-n=IAQz?SAi^Ro^8d9klEaWq>0{Z{ -|o;^JttQT;5FsHkF08t_2@K39O2st)nh)~)5a0e-50%y&zz&67>6VN4rDi$$p{Vwxy-L5GA5=w%CPD8 -pbGw!P4Qb1&53gjwRnYDbxtnHsz{EvF)GkRMy*#}?U?RDO~TxfLl_ -eb@2i_5$qQXMZcFUc#%`C9-l4bY-Ct+fEr-j(W37%T0}qB~7g|9)Hl7Bsq^I82txt{OUB)kO_6wD;BX -FL_OL|DpZ?Y`)R&@wDhq%`f1KXl<~U)!z-SZo8f^8ejiDseiXL(K^x|l2U=t(>IPQ!Um|6AUvsn4Yhu -sBg|HzzPzOg>)&tc--5=-u>L6|Sc7h(pcYL1^M~rEp~$Zqou!KMtCuy*sx7|~L;{(np2n_5EZ*Zb -yIgV(bUW2Aq{(b-fUb7C{ofL;7FD4}uzT9~0#9$lc{ -m`Q`0PXB&e|88mYQFXdlR41#=&-e9DLlFewO;kAnHA!%4ejA#o@K=RevGSTk|GcbzwiIQy&}*_vNA)q&Wzo?n#J&F6BofDb6&EZJ1Jf;^yPx7mlbnXhyTESIbx4=7o?*rI3+dPv#Y3FD%b3%0Z5I{vl_cvt20Noo@S -w@PUTnak)2$YrYEd&<>@HQ=(5L!w#pmtsP5}jzMZRs%YM+`SMkubbdM=d7c7+ZiUu|KYK27r -Hy*O3o=-Kn$wXW!yPQ{|ZxS$+PZIHoki?%wM0FUFSqVr= -d&WxwBl+5YMoaQ3*`<5X7hTvd`4+Rxsxb8A8jtr@I>3GI(+!{7c`(d%SI(~OtTAKgdd!uArzX;cim%eG=T(nm$h01++w5n-nNT1e&vKq9*vl^PeM&_?W -^Vit?HF^9#LweM?R&L5r2Rf65^Y!fRILrhY}P?WS*RijK%+;&sy1JH8xVM9Q2 -Fdk;*~|ZiM{*B&+F)bT0+TM-$X(9btT%2CQ&5NkGWQUL4Ke3Aa0NTB(>^QLNsa=msu28MC&pxqD{5%J -`SQ>TR##-tKYIrefLb1(0y2i{#E^_;?q<0qMlV|RiOr0ZKP>ns^fsJfbw##HaZ|PM}wANh!5@8r%8N| -LiN4PhZHfPY8HYjUZ3H9O`FXz^!6gUcZ<|7Y0T>>{4h36NJvyAhrv?YHVySb^@}=t)PINllb5~T+2@b%d!NolGO-mBgZ=a+AKZ{ENEYt!U#Xqr51nmp_EPTzn2Zda4X!%^Rq8`tHAlb)*h0VSzY(h>6 -ZTeZ1+#J@oHNeOf~KAQ2j|OtLdQ8Lp8KoNUgw}YB)GD#o8I8MEvpm^_$)0hDHabqo&-;rnyGp+{Ff_S -UX!z+vawbL+{$zqS*cJvr@NKD+j2$nIHqj+1Wd#`JdjjG-k3+n;9}tY@EFX&Cv)dJ8kC1sp?s61uoAo -PHP1@K0HQU^(+`@mM%Y@oSs#j^E4Qt&UP9Moc6^r3C56gI|*RZD#<-)7EHlHI}1h@3p?h(bjKVZfwgn -s%-Oll3>Y0vdZz5FrtI;S`SVj<`t4Tf&tS>meLKtg97@H|udni^d82Ys`fI`N~-Z>S}3)` -i};6&w!w2D-><=x4Qi9|YsR*_53uGqs{Owe%%))&cA2dRzTy3dYx-pMh`CCqV-pdSkOAJV}>n0&1|5Z -We;e(Xl~oHu3$dGN}?E+A8}KFrc@#4Yy+foOPhNFJy5z^?}NxrKx^624_VS2d$7s5<2UE%Q{nE!44TV -vw`2Ky<#4F`O367m;jgE7}14C1ilT8Oh2;FDwz65V_>o*E#o-KTjXHq16j~08`b*GRwC5 -a8M&!Ju}TjEGos&_VUSIi>rHrV7(O&R>z8k+heIQfm+wFPRLj+yGHliX(v;I$HvwW}nhS;mjS4S5jTY -5MJ&H*EZP+~=gG_C5?@5Lxknqy?GS+e*h%ks+1v!$?Sr=^PxUS?F@HB~%0t0KN+{+*>P{4#QUh3-OW> -oLysHjT4J#EX}&C&ECb9HMK8O`uOtGJ9qQ;T9~I+eJ$QBvl>P9*k2%*e4sRtLktqEVgbd>d5<{zY15P -1Z;Zp$v&sk(F6|e_Fj_`yVQ_>4zzG2;HH3gbO=PU%*v3)3Rdf-9K_**5Y8SAF&)f| -Q^SLWLV>e1D1B6lHK00n472@WGnVXSmgYwC9_)!G`@)Z@CPOa;rTO5E*pdBXGVVV{C@@)SBWU5M+!&9zw?bBZ9KA1+{H#Q6f@{t#Vm>FFq -kRVt(i#z?4+Bcj_e2^WlVT<1h#ugs+W5@V_0{ORR*t0-s3MDV%_zPfR*3t1%3xoh4-sRBNrgV}g8RFb(^A38{g_n|Gu&l{_PfsJ -BD+!4Ny(-OZ5l~1m66oA6ghGPfrx|woR+ -f3Iw+GCABMKm9hSvy?bq-heplMF5lF%8Z%OXW*r#swx9LFhFCpkCVVz7fSA~8Qe4W?yy@pvlk -09wa$2ZXF+EjaDDWIRSFof!5a(#-xT+s^yR`&%E)GBl*fy?AW`$uN#=n}YzhZxxw8%!(|)$eBY0zK@i -bFWSbXnD!g;MyzQ~Y}Q5?;)s9ZzW(HQSn;BKGg(HadI{XF9}-Dva~c7}6?%{oB+xHEj-KI(09t@Oo7E -Sjc)SFAKuY|-6__c5WjbvV5 -YS^Fgb0&X#>opC@NWcSwfZy<1#$pD>0KcZ4nnDbRK=58|fSTAFM8iCjhejf=?XfcQeZ7QZZ3gglN9f? -QdVDUE&d+4*LSKY%^M}FQB6(HDu~o~FMbi9ZCW1G#2t*4Dc6vx8;`*Rp;@L0d4>TsCPIVj*0)Y_rU8I?8H!?4$) -`t+BFtGy1a;IKa#bqGYPr9VLpVs%Cs;@q}Bhmdv3YoM`M=d8GJ&dv+oK@NtHQhCMtGy&YKr3;45Y~Ww -SbR8W6PKsmL`-^RVjZXfh(^WJ4f9cquF;HQ~{F{FLMfS_E^;Z$+H1+&dta?T#{_1TuBxR#`f0c{WXaW -TF8sv7b%>e(k(`%y>f9>?zv~nT1HNw59*64r3&zbx!L$rt?UwG?zK;4`Jk(gES2)SApUiIB)b}QhrUT -}~HfvXGYv%`70utbT*S(Je4-M7AY@?HIbAh35VNaQxO-@(ylYJgq7K|40wIF-^=FT?5)t`PspkzC4dCa|1R^xC$k5I2~_S=dF~l@fZmj)XIVs@$=@v5j|y?~a(rMKE -E44to?)Z1LTJq*$URauL7`be0)RRGs4NFqu<9;Ib3oQTQAfC7iqt=Fm#%LSvP`pzVa|*;^cIOmXn=mX -`)ISKg|Hq+E-gKp)|@w_&Udw%7_A8N9Xh34T*`t|pGpu*H1a5vrM4Z^hmkB}y!^hV6G^F*iMbCs%6>* -gLtBB0BL-uB15ddB@^16YO2M^z_9$m)Nl5X{jz(k$?37qGTV9j&$s%%Zo5@Z+vN=S(DiYhBW70c#FNA -xnA+k9%-X~@6zv7V^@YFSon8oQlZpxPfXu=L_nKN?BL$vz{CrEf;!GDhm4k|d6R%}JHqf2BI`sNr;L= -gyl5-ND|~iHxXy?zx=6{u(V$D=iivdC*s3GjC}osn+9hxXO19NzElxb_TcQD-np|Ol4w2 -Ipf-`PWNN;FoWwK?Vp>j@z7RDOCzJ>1i1IHGDHDeu2Zt)GAl-4+~GVZb^p4U}uw^31EONa797Ak8?c@ -xKd2Uha$b%&!R?DlpTa)lqm8y$e?Oluyq`oO89DD!P3tSLdi!UCOlMU$b -dYUye<|Ok%b=vRsvI$)#oJJu_h8tekX0Ouwo2dE8T!Bu99TKra|g=}p=-EZ6d?k_%{6%?4UN?XH}71|&`2i^g0hr1h~w;coHs~da=wfLzdz$BEB00IDV&O9(f1rqB{0I -N*j&A*a4JK|6*G4AeK3zb1krF}{oZz0z#s)x0}DFqfJOZD&<9*OWkxEH>-%-&XMFN!7V-BTpVzn@?>A -wxJqOJ1kXZyqt>42$BSZ9kM+rQj;_92}RhN$?bk+g0#*8I&<^WT;)R`+pyRA -Dl5fk#0HN3o+3q81wa!PBWdt1w4?DV?o-&=0Fp+RqS~Tpw8f%&2~_)y$~&2hK+{-TFX4qpNzCL}`e3b -2IbwyESUl5^j{)qeB|-EBt4pg&&{@^ErUl!h7V|D0|hl@eb>IuD&{2B`BBwzx`mL`fRM5u^nOPuvxQ%Z8hZqSX+5hOUVdVAkJBs&Cv2p%*_PW_7g#?;)U~-p ->3I9=qp638_3SpnYMGQ8Krhb&0WklUR%FXhaO3?WL8i3xylx_3M<~D&V^T8(iym63Cv3>f;XKP?#l|l ->5gsY=9&M5g*(#VL;u!|IN@^Kg6jT|8dv7+h($|VO|BIQRGkteS5TO@YSI&|WNsHJMyqt$m{MR6$*Z%aPpM`6UEB ->HV{>ri-#&Fe>3s2n3CLYxGq5GSTz8HcA~?A_xk$#h}5{^^apYcU0a!NyX3djIN@5oQ=ZS+>)d>rbsK -sTNL9OG}8b&nL?ghCuMi?wD#KZcL{?*^7!%|4%Em!tKfptq69V+EoNQ4OT^A=Z@k6)u(7l=}9=Hs#31 -%YYlqsbb -ndVY95B+6ZgIN|jlg@uRtovUlP(-sJ)YHj%yiCj9Dh<+&F8PguH#2rYl&OB)Qe_wF{zlE4w+h2Tt`mv -7#GeAjG3LIIq0p!y=MnJP%LyA);!UAq+1-VDa2*Nkid2sYX{@V&Im4KFXA^&g~MqvJt-eSimhuosgFQN1X|}*CnXj#(9Q55dBbF|6y>H9!bG8J&F+Q^pro<h807UFx`06{37sb^;R_nnc3Y;L5CO+!BjpB{&0Kj@dcA!Nh& -4>n_{sl%92!5=z|nX&)TuFvSok5GSP4gT0^#8}85JB^sJ{D~iFz&*pCIF}jsJ%195g=zXHUi|_Iopr! -ctP{YgYSY!9Y$2HW^^@IS90mN;>8}~QpUhO%jT@!>nH@RIgP-NL5$E7POBW3eF<a9 -ME5>5mVLP0%#M`cV10{aBc<8*^d!dM0r3K!WCkD*ZZ5Fup+ -HFZp}QIau0LB7t3|A9n!9_l)_P;;QB7Rm?8vrvd#VV!SK#J6vu7C;3SY6(&xFP{4}^vf@_p&8+P$dixjphU-VqY26Z!q9AtE%C4s@irc9mOMoNy$!h)7^zJJjlf -2*dWA`Yp8&GS(mN*`8o@8ZpmMzVgTTK>Z~hRpV~$~J8tMKI0PfdLfoM20g0Z7BTO@XPd}d2vY=5|vN5?1r4>IU~fwM -q`P9C1TV(GwG1D_tfA*pwOXhV&DY};ta0K)N~!o&AfV52|XW6gzLGbd8lte8{0d@o@XBN#)@QLJ@Q>Y3-tFirX;6J{!@BxxJj4xjc`SKG_5uQfUPI$l*8?MJR}f-Mm7i0cj;}Flx+(w)q|wP>qt -Q4SnVzzoxc4TnW=C3hz!xQ=(}c`DhP6WH+pb_&*upxd8^bRXcqxd`Nuv|{4Grl5(^SV&sOXr85;F%W$Tg3z217$X9Wwk7@BzU`1g -CmZ-_$=wHr%|~AOA^DS}Xwyd|gI2m+xHo*WrcZINrf)?r8iMTBy(giS_-kn~Uj^N=84 -gU!EA;V+9Ck)R3CqD1q0NiJ=$-o&;xSBVg0iQBF0G!!*Qv%NHf2M8)45w~+45x1S45w}}Ki>m9U@*En -;cC7II}?`9_h4tj;`x3B_?+Qj6Q=F)QH -vYhS=er-C!6-EWR7CITY*=o1ow7hi!RZah}~M*Qs_Yk?B!GXB}|IKgKl9!*yiZMR;qz#reyLO?Le9wC -V0I48qP2dm9a3wgoG;$Pg!I4+To&eqp_cDH1g8EHnwm-iHgf^PEO5NC>04en;wk=$HYGH=`lAD8Y9JA -|g6r=QI&s)x&9 -+zz%pOX_wG-364Dwun>K6Ye!C=_1VbkCRHknv;HNwt59Z2LIJ%NJtS8mkD3})koyN6b=1m`4v$AhO%> -(^4jcApUUjm}LOHgf@UO1y(wWSnn+;DpalsvZ%2X9b1~X)W~dfPe-g#*33+1;?s@lqC%&atOL0c@9ZO -%Ru!2Da*LlQ)8gL@J0MG+|VWckl;BcTtIEaLP`^fz|Cs-7aV24J39)h)=yq0iCejTP8h-KCs)QIIp(% -;xayHomb|Wd*1B3sO*oPWqz1q@^!4qN@OqPlzEhYLHZOg*NEKFuBV<7Vu9KdmeML7 -%}0OCa?#uwz6EU?@w7Uh54k*9!$WylF-ehe!16ZeVwJKp5&49;yt~WXZ9nczE3{2%GRcqMZsf>%#a2W -JIe1+wTG^iztUyO_BK+*xnS1tUMSqdZR|eUHSMyU6(UQG(H&uT5TtS3OuzpIn7wj -iQaazYR#0|s`U1{29pNGcmEj7S#%UqK6^I#VjTbT4p+^VWgZ7=C|W| -^+8vABU6+;tAla3l*P1z}SYoXT`@l#jC~WJ?^Ywe;Y1@Ooo74rX3jMT3|01a!SatgwDEP#1xS5qoBOi -KvFi|@%QiwQK%p92{UB6BA5{(Xf?p5JdCN)F+oM6s6@WNjB4~EY2<|?uL{md_!jEMiO4sDky-n3#8n{ -~%h7FrSD^%#DG=h=kW7ra+ -utZ=EXF96`miiVChTe=Lj4rIR%>=GQ++d33^ljrbG6LW+gJT-nEA0*>!;c*xfauWu{?qvcG@S_qyE;* -~-Q`9$HJU(@~*AQ4HLX2abk^b3zKRneQYA -)fSh*~5jWp9Nb=C!6fzf-SC@unT1_P~8ME;Kd@sgA$85m|QFboC?GOfhKH&jH+Z4X3!VExA4X91{Qnd -F9!gND$v`wNQ~x>u_IU{X}jk4>DJw%V?rR;H1nKqk}HIXp$_pdh6W>d4nF5%qaQP%_#b@QMdtZ~VXaP -c3p#UxjqM#^3RQ^1hTNCXnG-xlEn|V005KJaV<6^?7!pH=0zqv|7%^%&4JCBu1XC?X0znLkz8zADV@8 -arZ=OI(%-I2t>4SA# -rKFWpC{5QXI43D)N#825410S#RbZ#gh;*K7YvwLOO`iAOq?Vi%6_qRXS8#fKw27|LbrL2wwD -_;*0Ta6+05-V#dEQAZ_Ay-)WnD*#Q7{{*LF(I}wkcHQ=`%@AXP9W -L*6_=<$pA8@+Kc7zvC;<~4ZO1j -0+)f0Ys0wI7R!ryzJQaD4hDlP5U0FHgeh}I9a7``cpfBA;V$TOXV*dVpvAGiuzD$cb7IeTvgnh(M1hP -o`UsE4n^q`o;D`wv8=vW+n}DjAbSN4|h-TiQ$*;?3X>GDZZ;F*#K&)&Ns)mAJu$>$`B*r-gK-}7~Lux -*ELpcg_OmL1JBGo9iV}}C^gV+t;O;afDnq>kMl4FNlD!1b_V@8U2%!D!FHJu|0VQL&Bc~w(LE_NiU6a -g71v6Yeq-CS(>x>sy_7cihpSJO+1TSC@4*5aBm3SPQqiiX2z<6cj^n#XjT5c(xN7qCbl{y-gKA*9(?W -kVrHgYuzb(Sa!fz;VpkE%=gBEWZYYgS~j2mI(5V<`%;9SD_2ZK{6=4h~-WhEVYnO6c9Zk==B!xwX<0f -%f-;A!qf2y&ha4(e#&bm*W>(@)!4?nzEpL@L~C!8W9lC*d`7xT6AS64n>b5OMT?iYTOPGQsSi^!107e -I#38aDhJ>)*kB8Q$w|s$OK)t`6+wa81=@+A@$W*iLFiQ*`;lMF_+;;3KLl9t{l0g(H)wD)s_C$lZ&S; -V6B`cE?5W+!fcAxeJ8BNO}_HJ?Ru}ebkW@jG*+vWWHSiHr)$}@+6;j}X$hOXz9CzuPT=~%$YX~H;JA3 -_5!RSe?a@N;JU=9P3pU_Q@)go-W8SLGwP+yl#RpqkhKPigJ2Pe7=yOdOg}5i-BXuGC<9zT%MKNIFESW -n5-~bT2(Z$;?Bv-96kmwp;~qxM3mJoNPG6=2OQVE9f2Gut6+R06BGJNMJmGjx=};4)N%Pz|hWYLQpcE -9fWX!aT-=5rPVx?NOFyFgsE@D!61xrg>3Efw~#$}A8X!;7`wZc+52cZ^zt$b1M5{JGs)MsmxJM0K?n# -bRUbB~2Lw^r3OMUR>9wU1K|4>{C>YN@1cVeqO}?h$BMF^#!JH?J07V7Ib4R%ET_Nbzv15rSA$$lW_UZ -D{9AI2=r<164vT=X;aGeH}UO6N+>MWB`h>feFj`>En^P+vmsOlXm!t=s#JlCZ*TXZWJ_q*1m8=k!89D -rZMrgCZT}iV55t{CLQOM$vodQ -7QfH1Sr@qL>wt-US>8g6OLrpzi*JJxz5F)l1PE~x^hm(vRZyD)zTJo6Wb+Qqm*yS(Qm_Yt2}PtCEQNf -QG{p&&Mo@%Q6mY+`n}jkE*6Ny-yNk|+`kWmcPzjkqYu6_;R}Uq^^(D>3Ue}DRPDJ9H)ez;1AsIN~v@i -+2-c?m6J*-2K??4Ja8?$3cMr=XQ%mo&CBxEH;q6ZnK6>Z);+}}#Dr9c)*64B9`Pf%DEgar~(2dY8@FF --6#O33_+lMhQ?GRRML@yZM_k~4(Y0~1G>)DuWoc1B1P*X~+H4A5r08w%`Da56Va(#x~B7@P3Rv -kc~g6{_0t*dwn|j3@qSv2s2I$n}leh-F%AKM+`c5F -!emK+{#LeEe6oBiGYk`k&0Ij~Pjel#{CF_k_8yJkyg4&qHpphSsj1*MhU8xe7B! -jKF9^0qT2~+Ux_K_qGtdfLgm@UckBs+?ZCb%Nzu!0^D3W~f2*PG%p6#pj4Psf*qkt2fX>X`%Zr$W%V)R+;N@JFJib -@OZ*Zll-GnG4JsJDf14xJ5D<9GT6!Ta8}fL>vKZc1%8%DwCiwks|?|b)b~`XrO$vc_`p?HbV>~XU@hsA&dF -l$`UFt|vah;4!@GqUl>hS_vtF%Swp-2xwfQzky|1P$ -Wawyvh}YxhO4>&{#<61kQGi}nAy!>*a)*QHm4nf|sVJ$D)~R!nIs*JiH#y8!WHAPZocDDY#3%p@q2_b -29nGk6HF%yTya%H;hC!$c-7MHt)49}*po>TuwIkm6I}V;}-tTBHwIXPuiABcM22aLQo17cpcwz4A3?I -Nz6`ipEF-{D+wFp+HuR!Z#G$wcAGu@D?!*8P6a%3A*}ReXoaPe;8MJ2I%P12;2BQuoE|fr(+#G -`OHqbKrW~V?*$xfhD&&?^W~^U67s4<-phH-sSm!=%X$}G99nCR;d0E8vRi5O+Ld%0(f{I0S0R-Rt9#b -{E@tP2r*`$ujjn@~#2vxwTLuK9_HM(@p2=n?au-+bdtmPqIu2L0|}`lPfS1s -2!oreARw_A|TML1B;gQJRH&#D)<73p+MX?5qyqEP?2Hj5rR?{Mhw1o!|9FQkiqmBrar;x8Lfc9bO&%j -a7xoe=BR?p1?rG6@^LH3Var|ie65CRmsYO5d<9E^eOSJ>AEgBsTXbU`VcR)kINd_`2udL}pTTt5+?O{ -*@^n$K%A#0v$nXsT=o4Ls4CZJ*#-igcpv=Y#z|#bWQ8hw5Ld7`O@j-sgAK1dhunJBm93!jW{Nd0ce2o -fD#SZOaA{%BQ;LtqWO05tg8)RXlXr9P&#Ghb*vQFUHR|sy%Dq}T7wmsyS{Zk=x@E^)DDq0`3+bBUH}eVDl)H1OR+zL+@auOn1HeER)+3S35^*Nl+&jO%yt$DFfyTP! -eA~ymcN|qnVPu|{L~!!c#714a<)U!>!79ODjk|VtiVs@g?rQ{n -O(_sf@pqF*qQ29sE&a~n#4#f~hZ6x~MOqpoI)@XUz>33(i9l^}NEK>}-&q1WAT_~uazYD!xf#?dKA$p -d<^Z-Y9uFBda{xyTY{amc1Bl^gC@1L=Sj?o55n|+NusEUbh{0lx9TH|mr}0PrRA9Ifu1(%eB}LlE8Os -vl8S68gIOdwfjP*&etlUJ2;Q4iAqpsajw&&%%gS&RKF6JcG$9V_;>MO&CFok|-c{Hi -SvU_5`!xOB^+UMSOXy0?urxT6C5W{Rrz?;UVJ@x+T9`53sc@9s?mZx(Ln)|IVlsU$ySz5D?H1ZjE`U`u{9&EA -H9V#>aa6RFb_{o9r~EtscSJr23*(A}cg#d?U7DwJ43M$AS|8^M6!1wTxIr@M-56Ml){V~lX^py+G^ri -3E91&tCR76c*-`9uYt^(t;7goB1SR4H!LRp9;4r-IQcdTQgYt0C!w=0m{%!OJ -r{37h6LxQ{qcTDNg2s=0i1QABy(^rzxOu=Tx2BUzGf-U5y7H$x<&|Vbuk)Ja?!WqPMDVjRj1r$kL*w%OCSyuVkq(Uao=lphN2KPVWWdBDvpB*oJC@bVopNJoItFax_!b? -;dE@%f9ErEZTc7xZoJH2~F~K==XUt&pnCb}mC#AJ7=Mqid=$H_avc6;&NCb~2qr_-}Wj-(*AS7IhGaF -LLj)lYgmS3}j_Q}_7aSUIIW1%sF>G;PiHl;8iBo8F=`V5}i;kf=f>srL48>#5J>+~FLJAozsg+tS)@gw5uv_z)j{(2dSq7NPqgqa0BW8 -IWWm4u3ePx*N6aM7(3`P)XlC;J!F7kj;k<3VA^cd;A+t!A3!O*$QwvSA~6 -ufeyE8;+JUo#WQntipy2E;?Ne^a<^f?+a8?q%8y(lnYx0LDolZhC|iGk$I))>K{j^wwlg3oGV-D~5=mUo1jz2&+##`rieB^;m%bomss!^IdyTeA>cl_AJ5ggV#cC+ZA#5?-3qM2HEv8 -d!2a8U^wrKgeToy9xxIEGQ*JHGwqLACBWV>Kld?|AUBy1P_tY;axVE)!iqP4e5XX5qN&3?+mu4x~JsR -os=5t3x2fuwgjG*VNGo>vQi$BKN!{2_$sp0MiD+Xx6=)mPddSwHcdx^L6<#fo2_;t$#))_x$k@q($C$ -#@1-&eMj;}5AVgEhrq^3S}<&81HZ`B7+hNTA|rL!?C>{(k$w4vA9Kb1g)d=P;IL|dMed6Tpc&bGv89F -vc;gIVTAwN&;Da1rI`T9__@yIH$m7<0DXfo#+b_2Xfpzcx>(L|S%&GljxDJ!zr7|6!ufLkx`%~{ft1| -t&entQ4)vME{8U+gK4)ptg9rv4hPhJ8xXz0u%017s2=msZ%jT(uYqe%rjZ0XJb8#gc$MOLs$3+n@R)W -WFU+y0VV&RT6E*sGTAm4QwB4Qz^br-N1o$AB66ftTs1g -+caHBR{k|HSz<`(^h^aGfKf)`I(zo3f9W+l-iiK6N2h9@`LQBMt&IcsgWPG+sbcBJRA8z52mkL*m14H -MS2w`m-m|xiu?F9^$P`2>fUeEAL^}ug9i9%qm*A@hApO0j`F3a4;DrZ=qw3pz+nr}mmX@+xNRj}>$#* -LlLqoZ`SjtL+8FESK94GXz2zmdyvX$Hyvpl;`ZsjtZ{Xn9=RJluZ2Vf+zvX|TK+EQ@zdn2P=+Pz%7tv -kSauE5d(Wf`hdsokTi!|$9J@4K0qNJC2>oC`{$zMc8xXzzGQ$^c?e|x+tip{s*d~L2SbvOHEz`mvYiy;HC7F48>zL2U)qKJe-1Jj|NEpM6^k>+$1{UX+JH?{*a?J -+;f}`DKl1*9k0@Z}7q^fMA+B9wd_OZ(R3D55QaHB6PJT-qc -ZCB*~7f?$B1QY-O00;mj6qrt5)^6wxL;wJ@&;bA=0001RX>c!Jc4cm4Z*nhabZu-kY-wUIXmo9CHE>~ -ab7gWaaCz;0YjfK;vgr5x3XWWRBcHXDWHPh6;}hqeB$Legp2?$P@1x4cm8p~G~ak^NRMVYg3ZoVqBYnH}8v)rr%rP<{&P5`QCbgVuaUxo#fW#5MRbrZtHvW(@J -#}nvk8Kz~`-{FG*KE)!Ouvb@M9!>y7FzT?F#PQ!23zmQVW42=Xfhfi%ahSYL*qi~3gW!sVQJl`|2a_l -)!}@2g|0W<0P#l=@0O2hG+Ukm>1Q@_6Rjorm -#L@sK$9B_<`cxICaOZPQLl>@psYX@eEi*gTq~{xQGIJJBoIWd -vgjgWlMME@EUzV#;dGgj=pmK^Jq*6*1z9viF-5$>4+6WmNZIP*Vu&yfcm?eHL4ZO;A)cV*qSV9B6zC? -Do7_99QT+tct?P|Jz?v6BqdI=#Da>n3vd4rCtJ()#ni{_mV>#73t$ykjj{-UL8qofOMy0A(XIkISp?> -v1t0X21do=b(l{(KA_F?w;d?*COc}2~HDj`ZmjZMG%jH@1xu!!@gBX)QWy>Lv`KmN283eP&U_VK4tNUHm8*jQMC7C3 -csH2t9N=Evs5!#~=+*8t~bLh}(vzb_sKJ)v4Uw{#!B5LLzb*#BqhOH4b0E1WP#&e3{AJGnd=Zv{M$}U3GHw!-1)J -4-BomFaU@R+Slez@cfJ=gwYE5a1Njk9s>j_C(G9GiA%n82KAh5*RX1<$7!xgGOeytuI1rHYqAP1{F+_ -cFGg00blHNox$98#FTN{rUXnQ`d+sjakEH|)s9&jQ4vCZQ1Dm0&m%J{w#zV6p74un!N;9aR8-z&A=PB -PPqXc4;9VQ%P9$NUJJyXrQG*y0k`d@QXD_a^+zZ7bt8T!M?|)K+VzUCd^}eG(rK-b9gA8#vzW)Smj~C -_X?7H59^AFNt@Se*v!$aY4S6W3{Zn0sTGxbX)mS -SKjMNd%CTX=*%8QMsJKO3*(d97wV7r0HgX7>!Qua6$N!h40)zpyhtgcc0Q?;B8myTiqsZ@21&hM?QLP -bs=EI5r9~%0UP*n&|#B>s5}nfvp1#f!Kpq!@4@Z-12QRu{&qEPdB*~-^%RVIvbJ(a?VBJPl8`8-U3JwybhVe47s?1qcDk{7FQ(WX+Wt!r;VBLlUyE+cqgQ#)!jRMlx3ftG -m=NPEX6xLP0u&Yw#9IvYJXujH-9TUuq!p%}p5)oe+9tgtI+K<$h-R0}{itOhW$vM2~T8wj$nS?GA-Ba ->9@_Bg(QS0#r^6DaZw&JXHVQFk5EV~`FC;BCMBy1#FQs#1s*J9g1c3}BeBbZDQ(eH_?hb=&R0d$q~b@ -PGw{%cQK|wm{?EXyu+tgZs|yhXWhRsnI!4YbD`nJsTj&49;mS!7`ra+0U?WVM1WfoF(hU749{-sAuUs -%&%bw-ESM1pecYfG}z~Xm8tHxgB!ZgZrj`22i#aVo}z_5uzpC^QH(7XH?u+ehy`U>z7o@8SkSgX>js -#9T~KFsleR_uUX3ck$_7pH+PxZ7MV?KjmsjuAq$)UV(4?rYHfh>!+zeZhT+Rl#I$kANLAr!kQ6we5 -e`6R>;;E}W{DcJ>Ra7H~*l>AgY9*#2LL16VQ|PW#a=q1c%#z3u%L(LACCWPBNVlf{2ilbzK^6F^11pS ->A`v&SVo5UMK1U@o06)@W1`VJSun~9xBNI^+!ulzlfz%EFzholhKwnU&P$VNeb(qw~7AOhShW3~FvM9 -+7D8X|ZN*ba1%yc6gHYY)hgW!0|quQo1iw=0bssZ7UBxBBjK{~TAj}oMvYzpgk9lU)17KewHfE=wpNy -xW#*zT8;D~uqC(OHQ8WWyB6mFKaEMd?a7L>xn}(u_Z<+<^ph8vH+ysZaPDG(2&4X^^D~Ja;$;e(t~nX -elw?CXNa}JBpyzW)P^f|nc|i-0v5H -b!fo+WYl1mfP#WFUZGIR-<~4UU*gXOXc -k6YWu{qHc9fZfB;1I8iXUc~EScrmav=(3$#S0lKo86Yu@$93^L^cCTUY8)+ulk)J1_A@{xVxY?kZHV{i951bXCaH9m=mps7VxdL~s4vNp9vL=&t;xsd?RvjmR>qGYYR#-+>2(W~ -z2Maqn_LrSKtPUSv1g%BT3ltzB;tWkSF}Tm<>rvy_kxLO)fI!MexOxZuB!uuxDEt;9Ee%8>^F97G+Qb -;M$jIX>VC4X+B`1J22odt(Rua0zw -7N5H5Kv%tszwdW8KRk2(ig2&%p~eevkyi+`NH{^u!4U#RAU$qYDpc~wh7Vu}@boM1#vqz -(>iuy6F9rj&;%$q*&ee2Sh;1u!~PBwR~m(q>_2jxeWQfphx607J7F)Xxu1&xw8hGz!2Tq9^3RfAOQ5< -2jpW&>|fGF{5P_Js`-c2m%Xt~i0HDiBJz7qlW}`0wy0 -Oi%^7W@{ep3ttWbde|QBTZ_mt%vYIg9K|X}eiaE?b=yA#amWGV0S)Nj#RX1QUKVJ?9uCC72IM*;`Iw- -9to*pSBBf~`ivVYDzBglPt8S#z4CN562^=cuI^%-!Lwxe^{QTEObsm0e2TNK{<4vV^2%yB+)l;+XKGt -qY2?BpVwD{aq(GbW=SMtPjV|i2)(CsL&wZcv97Z-Fk1ZHtVXxjj2kdl8@#I7ZPL1;hK=_ET%8#{*0iP -{s8C@z*#y(Iwecv~G810h^J)?R(Rz*h7KJ*}uUz^vt1ngO_l>ni81c%Qvg8Gf3Gc;Zk$t3TAnOYHIhx -uoXo20r%^c_0Uatn7*<3p_u=t&rR}h(<3F{S#Jf6Vx8TVAKeI?YM~Lv=t|Ke(Gx9vh7qBv4w6es|qMb -I`oDVN0p(g5q8Ol@-8cnGXTeUfVMpQMT5faSO^nr9+zlbiwgrJnM-&m+<1&`1-UlxRMQPScXf4NU -l{TvZY_Hmwz;+gxL8p-rJV|{66f3!_t#OAPTZp0LA`XYH2VP{E05!0C4SV&sIUiZ@;%^~A3oqa3jJ>! -o9?{|>0rlZT-KZONvHg#9FGUz>q|)=#PTwUC&y7;NIo%-psWiZI<=sDViXkHLtRchFr&aQe1^LBm0a2 -AJgfSngay08?5ZxQWs!4KFA#KinJvr1gw4Xqdc;q9*-RSly#YgSQrpHLMxl1u5yf01SrsE}lpv?n5pY -yN!jV~6q0*B4s+?VD98s{$NLE%eC?ixV9<0ukbw;W-6?M%iZVv~4eDtV@lX;lO$)iV68IiXUi95J?oI -iOIJo)asKRN6B*)j)%=XCRwmB2w4NtS~;j2xl -YFuWn|2oyQeJotPku%|m8>C@ef^l6WgLd9YdE->lUd_G25VKMGQSnm2_CHR5K9_B1gk~PVDAc$v^7{k -2!ZW-NRXt$3*+k3KOcsWbLPrh(*_I_GYfXB34g*g)tvl8uMTxR;y`0?Q(MnpdT)8i+Hhqai3N-eehO` -oPLEqo(d_%q04kGE)Hv7Vt$W{3t7GadkbGOAX+>?fTS%QWDNgA|5BqvovqS9lY8lA{l#Ui7_sI#Kt06 -otPXJ$^d+`kUT$K{|=@aO%NxniW08nnuM9suY{`5-2W^Aqkt*CG9Bo-O=>=Xf@1Z)X#zRV21wm_|H#z -4_yNS`p!3Bd&nRah}BbXg37cSevWo*|2k(eyrzE|=wKmX#w+%2Et034SyznCIQh;9R4t9$ui{9 -VL<;mEp{Xfhvaz&GUpi9cZUJq4zLBKD~E?wEKno4=tN+Mrd`aCGKc>SuGX2uVqiV;(2sIp$)2;-O4pK -#b*97%gP}-^J7*WY90<`ew`|0f8R1Z|D}%+k;>BF3o^{d*>^^|vUfFbYQVlT*U~QBVBRdz6DwP>wEAR -6KZmmEy==tx?q&p?ysFWE;o==u!MYT-ErWLPcizv>Rj~%awyX%1De);#r4%5ZV@07>xWGEK?Iu+vn|u7ba|it=P1}DNNr;bc;uR -Z6~cO`^$0q@3Oa)afzqK^wOvJ&{iBn?YLU6!mml_D-`rmi4Cy++x12!fqT3cTR3*P%0@^w#V7f@&?JC~My+UzLHW- -hgbLtfU0aUQx;|ji)nKW%*V!JIglCo4O@;CJe;!M57lixf;m>GGae1;~W1;6M`U?+fr(`Nu;p7_cdnCS>tTA7NI8NNiPhZ1CJ{evDI)P^ef8*C{iu5`M=oq*q6|GO5`h*l!WSp6j*myha=~zFgH@ -Ja7h@|3O80-HUDP?2=zF=w6b>4#$Rw6y-sEtwET^Ntm~yTBR0n-D@C_^su@iO(*vO^CgQwMx>0Tp#_Ypj)j%*QaRvD5;;wV|vJn`<^b#eUI*>~Pny -Maz^tIpokZG!9Sl3KaNbmq5lG}OGpJ}Pgi{qLeNqyJUL25u@Bdi`r*C8kS(gIQJve<%jQ9~_xDe33|6yUjpiDg?KP%uOv -1P0Kl^K5sQA$cDX~*~(Pgtu6Z&BZXYE{|3g2p-Yt~{T;KObM4#1Na3N&*A}kIUSUE}@o#{*XIi^ya^b -LA4KAd-D}_Nv?;hf<8R%_>z-&5-Q_?I)^%U(^jqmirwyM~i&zYI6Z5sA2f=f`8-Vs7t9I)Q5j2@UVfo -^f&c-JNme-lKX@%kEDJfu;N%}R;c@!YO@i8bww$Eik-UESCXvFhyYjsz!#$JWAAmCAKj0+ijoBw6$qJ -6HEmCff^Mg-Iea4)gpQ)U>!GmI6k;a_xZ?@ij!bnl)?U+zTlg{~JhhGAKZo$>5o_aFvMAs321kZsh)H -l^Ml4wwPjsJPC!k=d9Cf^d`^dE)U6Fd4L+Vy+z64&>p|xFmrl+?EarSl_r1p>=?~q6;JM~UB9Vkb*+l -_HvzS8c_O8vwd;}#?A|QZ-lr-dsp!$X9@am-A$=Sy=}#en(4rqhsdke-z+ -&SenehS!+6yv(akt#^X!HV5scmRZshIUfj{)_MDuMdj{nYZ*c-tbFCK|I>PF~Dd$hOUiDE~1H!COvI7$Ve45&!fBmK_}%h$mq4}V_kSGC`+* -t!>-;AJH|8y9s>3JF(SXs{sFYI3ON#`^YMmVU_DJ+OWD2xqE9TdNRTDXM+N*$*5+-1hvrMEfv`a?A^h -mn!FFJmDh`$lY#?}HX41m4=wzsM -OUvjW|o4XZdTl^^*l`;=$-yfNMI%-&WK({vDBM@n)G39fSk#vXsD;RDIbPF5)RHov2N-+Gt-pv@cD=JuiH@EK8y(eWlV<$ -Ff^2622IK1bAMys{+_biUuu@CfycT~|NXUlxTxhm`+*Uc4z1y87ub)*zfVvh6}AKI-5jYp4;1@U7*nn?EyOrAjVo14OGS+b7n+nhCKpV-k_k>IH9NZ7IW -hGd2@AqTzqs?$*L^|zVS`Nto+_JK)hDDQ5rAUu@|SCjm^>AIA~kh`zCy^Qm>Ja6F6|Hrs9f~1G>KDK; -D#cKsUA=$jepi74znpK(0zaLc8CQ(yAaN?DH-$NRb>Ud}364Qp|QhG+DnO=PhPWp!MLbL-(7ou5D=qy$Ycafl-%s(i`rRXhaaA~f78-j|DkzIToA`J;J^7Kmt(eM1N&yo=-DfYX8Ac}jv -C7o3#YA`q5GpRl9v^K9{st&%uxk1s#T=B$kI;-ewY?VnA9XCO#CKM0-)x#KxM;P6ap -F3$s!pkP_7%MrbteuhCDJaC_<+13k}x$KW9%48>f%(c^i+yd$CfGQmh+~rxFFj|g~UzJ`0((O!$) -<>(Y{thD-dju2oaf|XT)No!SIVIwT=qBSK+2)Be*`b|TNlu8DAn-HGO_EXX#S}xaU(MNaipMXW0zaEy -6C+UMaaGKd?yF+japt@(#!4f;Q5&1kN##`9j8^W5E^JpLRit3K@WVX4X{Riyg;=`}&A``Q_W)J3gs}h -WDo!E?2G2~fA3rOoz&fRluiQDh+?}%o=IVuad_=A*;)griJWdW$=Y-IGoSsZzP~5b16Xr1pnMcxY=i0 -1<^{E+@u&_oUcwE$~?;FyA_eG7xHK{%UYnzDTiW1`Baj>)dVi08|5nQ!VDmMowM9ZdEO1520=LS>Sb6 -WM{3eG&uN$TuKJM89Rk9t{9Vd-A$x8Z_rlTBinfiwJb-mdne2?9%UOGFfPmye9{zuJ3(NKtxaT%G2$E -9C43Pu9D*y!|q|+O~iqhHTZtpg<8ndTvDL=uLX68pRVARM&M_-)^+y_h1Par^`jcdk;%04rNxtwyX|8 -)1;#LXN2zrslLsYcsqQxFM=(LRYU9c+lq|3_)Y98zGv2}Ae0mRqH!~h^xo~ZZb?$L6&W6@-R)g%5zG3 -tb%=zUkm`$;sR*a+#cPWbqnZ_`T0aN}4HPTTw!@vsgje)SU5%e#1_b1tkQvC82texJF6^7mza~+IHh! -#++O3jq62`3n?$@x(FVWSBF5if=Lt9&ftS-7XqE;sA77bOsyM+O2x749lu1Rk2&GIJie$h#vyt3}-k! -$Jc`?=+aA&#wCSGf$iWJOu<>(R?Rp3N}rj(P$3PHpX*Q3j7i+;ms;6p!v|hU-WiWsb$e4QE-7up@7_` -l2hT&~Dr)JuEI=_V$)|(mOJ@d!^>3&R{WE(hx5|7~Un_IV -wv&!Ef2T$#}vq_l5Khu5Zfw|UklAxjaqV$<0Ua~nY -0)jp*c6F=*F`&pQO6y#(Fo)gnl3Wx(qT}E-`Mvn_;F_&hSsoQY!%i$I(QAz+f8qPMFuR_@cc8Jl0<7$ -SkK|DF&R8MtG~ysdFV;=Fj7s)xaVHT=wB*@V>NaMm4?OF%s0HLDVl{Kvgyo6@eY;v&_1D|sxULbpv=Y -9|Zh4iv8Yiyh+9u?|!_SwrIG;o<35&OkJ8*T*TLhw*mK|pPF%D98Sble%YunMfTCT+crl(eE>$!yQ?) -YFJjsAUby%$U~gnz~p);nRqX{=blppR;1xrx*iTEm&Wk5|`ZQ0{qLzzIy0rgVKDx)iH))8H1k9E|#Fi7qJg4iGP(+->t+L0Mc-!2!1@{?9^a}#1EouMb26_@mvK+W{N%BS|r6}@?6P|+vTRUH8U&vA>7k8C?Axvp|??AvW11MXCqK -WyCaK-bA#w?sdewv3Xe05X04a}C@yH0}D>Yx1wCI(rskjsh~(pVuL$Ae%3zzeSp5)fUR$@glKU;H)QR -`#KAD0`aVt`I#Nzr!pPt%LtSw`vBL;pCe9QkW%$BxK;mrI2_j4K9vaOUf%otBELN-^08QE*80?WEC8t -_Q-jTYpq)i$B_WAwavkgb8CI^k0h1rvCOg&3-e^{d$OTzfIHflJBH-jl7(bD&O0`sDxP$QhW1l9;GGa$9yjdV%d#2hla3=xUIIgp>_ZhQ$&vCmC4x -%`O*+IO{>a|Hg2&64;xgX!cPg88XKYQ|>Lr1CfM1{ZQHfniqbd^*lMK2v{&=xo=&+0jLTrX6~zcQs5f1K$+9o8>u2;y#qQ5nywE5p)6L(0c8mb7}BU$alsC(q4C&T^fwSVT#2Z6@) -WN{?VuPp>P<~MZqGx-*45d)O%IV#G~0GqDTd9GPLq{8UqY*F)M$bD*}lsTvlkrGY~>{Vr&&+{F9~>lR -d@_+nwyLPfHG5u1fr0h4R;3&B)YD^uW7TDd|C48_3pp8=QM`BD{hGR?E -OvheInr!iSgB$Z8ft+pa5T(*mQ~gbRHxiMbWkyo?3YY=?mOBz -rc$7p!8c+GN^5AhMu|6t`A#&mZt~nZQUfDJ=o5#5!QK78omdR|4sXQB3}o+^<^)^(c+hYDr6G8`Ah$X>NL=$OKCT6;3XTEf!h9h9|R?T&XX?k1jHN2F^K4ia3SXk= -JW}eyZ-mrFvY&f=(7>~fcV6}mUCRYjv8fSGhMkMcQq18NnMwQR)aD|7ip{DiAw}l@ccaf{^B!fklE}J -}?qmPr&_)o1vIO@eA?FlEf=9-H{G*^?ZL9{K+ZLquuWx*evL?ycU7whXuf(}={w#M`Wd+gx(d%;IWfv -X&oqzq7<4R?sa -+_0SEzMP7H_9-=HH=tqj(v1Di4AjAo@7(y!iw(R!;wrtT8c`_kUsYoF>X_>~_|9dqJjKG -%N9@!CApy5s0K6lAwE3~Z?SYT9<6$dC=&s|!yOKu*h<5eq%N>^zg=6_d1s#78_Us*?5SvNFwClOOH)t -V}bSw>w)!kMNzY<3BsP?eEAxb|&$)40625Twn!1tbnQHYl8GkK4*O7npISciXMpTi*!YsJ+dF;QFQ -Y9MTEDuzq*f75BNS&}4g^H_Nr(x9Z%eB?Tg#xvvZL+#&?&cJLSH=HPQ%K+DM$xj_-(YBj}y4)^KZE^a -UG4x?Nju*sa}G1nXg(nXLGh86vRjKUvZegES0#UqjTg-&=3f_HFac#pH*v -{B)j$CeB&sia&wD7`7T5_bDCkY^DIh}H2c$OyX;EHq@z|+)#6J?Vn$zbSeX2B}UuZvCg=3AmDg~y6yA -a*ueMM17sD>VudH;e*fAxAtIlKaj^ILt?TaXKO;{(drR3&*6Rcn1a~cxJs5zCLZPl^1Di>{aUH=z1OT -+*Qd^-d1$zG?nZd06u1`A6yL%=sI6}$e%DG?mvU9MOMbZ5=DqchpoKbE>9>-fTB -d1F5mkE1D%G_3>@Iu3KV*!C7AFDQ;xioU^B^uCQh-i%4=IIasZ%Kg3_2!ina*~h&+`Pl<8n`@$e2xQ? -E0Kw^1gVr|7G(nLV&h++s-)mk_3#@W+;tY)A*`s_RJo*MW|+7$8c>4Li%VGzX_yt;6u@u+=!s?suiPL -MlxFt2E2)z(m_-O&W93hikvDPDd)B)vUvmG_dIVAXN>c37Eiuv)#%>j*_`m!l{39YGe3^+#)EHjEm=o -}NY*7?JNJ>D-@?*AJ!3nmR;dyl;}^UB&JnQ7A*gv8ox|uFL1V-Xi>fLCx$LU$TnVQ9e4jC@%RYII{gBE{gy$7L3Hd-NPG@MuF1?`nc;mZ8qhEGLrYQd_2V< -#az?_2ZcDyeo`^2)9oX70lkFdme|E00LGbV-I4D8H49AAaf+gvX3dcJk09)JQj%$aj)px+9f_2f}a=E -=0NRvv9{6_e855dg~qCz3{z}eRU&#Md%ydWLcDDzMhlMAf}nDz_&4rkfvBOHcbmG8FiHpP!Qo=CJN?F -ZkzgqJQjTupD07HrNFbjAa1k4U;NdPS -?gB#U$5+LE7FSLhC!Y?x83uA9WZrhx>X&#l&>AW^g6O99CTuqT$H_4}%W$nz+8K%jyxH8N!M)y!W!^B -2|@xNdL9-d*Uk4m&tbDG$iY$jd{(=~@t1@jxJ;8c*lG7Y -B7BJn}pGv&dPGl^MkIKOuwD78xVnW7EZ33sV6;p#bJw&8Y*D#uapPxAI))A{`vbQw*+CU+pU{jc=~Q{ -V{zMrZr{^B>~LhZWoEZXnpOvHx68LiJ(w*3#VACS+C3G;x^ApJ4|n-z!d4x4E9%XvN&3Ta)4Uy7AFWHx+BSx5gU}wB4Th#_Mc}wWhzb%vij%$9Lc9?VEOutkXyB^!ok-4V3`q;wx9lNDGA^%=tn7JHtRvp9 -7caKnxMSM_I(ewXRqO3f9x*DGdpx{2s4^*A+P^j%=~koMku>cQEz~38#z$5W8wi%~f7f2u*760;1Zy& -mcIU2rJfBSjQww$7W6;wJ=_icZvAKcZlV&!7hxU@q1f4I48Z}oO%3!x}!0EAu!r#mZC%W29H;?8WCx) -!{RJ^ZxJHxJdnyITccJR+}Hm -JXS$a{~wMM+(ufl{2y-P+tPefORAc7I=NtNL%Wx9d|decJZ5_;&{Xw63~42yq9d=n8JHHlFS5L=%8et -*w#&EuUVIO~%%0i4$*4|Kb@^-zCd&XUvP!;6=K|e6e8>WNC1fts*q6T4k-NH=y8`u4_~`M+bX(?d=Bw9D6g$E;E2vd%=@YpPLkEhY4;kbNK2tYxNo{c -rv`clE8ruO$vD-7TNDQ^LZWkd7i_Q!3x&p`d%KoCw_T>s3^YcfzUXB}xjXgLTS~fz+9_^oI}UjSh`&+ -&^o|cj-FuzWqOmKy#;vdSPzJ58Zo8`Ld3lWv5EC-prv7NCfmu;Yy1nvk*sP-M?6#7sn_M?Cxh|cTZZp -g7vh^!${WessFX1>>rDs?TXVHod7l`ca_)Yb7~rc%zDqkdv~e=(prl?ehBu?yG0jS@Zfvm>DK%2-wh>C7($ctu~MDv6_{83UB`0OW#xPFc&zyNs59}!B}m?7n&MsZc->IY -db{F%w-;#6Mo-t4SKy?6J>Wgowr>psYS8N~DAk*KuQ<`(jsMDQtB(h3yLHbaCIm~j$$HAM+8#&xRKDa -XfZuZLnTcXwr_nCa+hDT2k$YRgK)q(FeYSRBfo?24w+;GDma}-4QVd_}utk%1CtFH&9P@@x^Xz8^JA6 -4j8(boTx{PP^C&?z)KP|J8n49Enm;PA|2NQ-jukhH3h@eEo0i*ba{!my&$&v@_PTYSxL*wqyy;sr=jglGKzA#}u?DRrg{2rP}T%9OpNubnjrJ1AMe%| -s29<}F-ECu4=V`aI7As(N~$9|9sm}dn7{q(4r-ek!Q(+9zy`Hmxsr_&{@f_QdCoQQyY&maCMA9_G3s? -{ae(KZqwU@kwWWSfMjPf5-H-v&Nj|vc_Bb>Q@7;3rekbw99wa%<{z*EY8_lRIPt^*3G6< -5L9@*_xyWjodsS1r%)kX28&Ssq3)FcqxHpZp-7bkxUUVWQUOPkifRe1P1W@H_ -2Izv`xf+JVZI2|N>_3KLCLLN9)#l|Iv6Ixg(ly4GneCS1|nLCO!aAUAV&Ib!%_%d!lbY55G*4qeH6nTnUQP(FDxe3}M1+wo=T_}4VrxE)EP!eb2n*CrDJZaSH#n>xvTk9Ru-4W>io+ -q!UU;MO%k$d;tFFc$d8m06SagjbO<=Ie_kb_ISmIoOdn-@XKaQ?!4ZG|JPwt6Uc>+xXZjcrlujjA0Nx -V6@`g_XD5?558|+X_Jn!3lAXL-mO^fLLp#Y;{3%XS>T&e(VAL_3{}V!3wdycQ2EwcJu0NmG0yY-Le(i -(Sf2xoGzOKa-Ty`anYQ?&Z}JA*pf**Bz)EA{$dLB-3w02Y(et -?8<_TL0VJ^DovVAnDQBe=0n4J`l1;)TuphcN7b9LXcogsq9Xv!9x$QGV#`8c3;|4^!DGkO>LMBwesA; -<$j(-AZVpg0 -mebr`-8)~PdNtd0as?2&{iR2HIhaIkRRywP=b6BK@C*fgZ=u39mXns36oy=sfBT8x49jp$>=w*` -Pxq3$l;k4Br@HVtTn8~(~P%b)_5nL-yf;K13?^Cg`Uo?--hgPt7nU<0e`;JMZVj(-dpPyC?{(3TbkTp -@8ISqYvXd=e5sOzI&C{Y=HM~m+ub428H4PB3psoh6`KeQo`YbL*1Tju(MqH@F2kh*0zNH!1 ->s(c+22it$;*l8sCnyI1SGM1pO%&(D2E&=}>;chC+aMSo58&UotXF{7~6-)*w}*jqQh*uu`~ZCi}8$r -9h(Y65CQpx$x*X{Wwckv}MIm&9Tj|IIe#+3%(NPy0Mt}ZYh){%X#_(Pm_TK>)(#1tD))lj%x)mZ{?wKDVN?PUhr -|KCPmj?pNJd~dL}|kkz)l{+LLAl{WvMru$(Y8r&)|tSHg6-Qtdl5eSsz{rDRj6FblshTX%NBqVOKNM3 -=f64}hb(;(yKK`0IlQ59&jS7oid4d7vEw5UdD8KZ*-pgAAod!A;Co6wb|g$3qMUd3cVY-PIcn5W&HaD -+5%`>oh>n!uvd8IRgP`QVum(;~6W5*9XD%{y}i#j+j$F7~c;yZtD1c(asnTI$}(^z>0aA8d)N+<@JIc -4LGL;;i<;i5eBo_c`dSS5lnhS2Ll8y0S9$`v`-_58Vkz1qu`fcH7+~Aen@tG3g3$}d~%M*b~z*G-R0r --hYBpci&_lrq1sa<4lW^osCSyzpC*haXyn}-3Hn!jWD^Pk8QC -nG#-KLw$mUnU!_iks>O{aElvlDx0h=e566v+~#Y;|Y<`#lE$Nq~eX*}F5l@=%t@gY(9D0MI;7S0o7LW -l`oV2nb(gXc$$nzneZ@;SP=7qI!5)jC*ktVB -K;OyXqa1-qLP6@=3oZa*!16rJN91u8hfA90Y@V|$rXf=ggdRK&(?k?BDQX1klt-5_TQRsdBC|A&8BN| -6Mb2j+2iy`FV0i~PX$W-0HavM4(SkOZ;GGv75dce<`imZ1OPMqFK;wHrJ0d|@@JcH%WGv_-Uh-}=fQg -WzPJ-Xdv|u%tR~ZYDLqVp*^TxlWJn>s>56RcWyYU9o^NTyrH6DiM48&SxJZ8T4#e)G`$pEffZ`eljfh -}mbc^aYvvis%TJidAb*9lywaD4;Uw{X3N>pQsq4A+~R;wJI-#WsU0yvgC6!8N~mJOTVCfd2&Wp8)<7z -<=_3UwkLP#R+h6@&>N&;i7N>jgt_r2ri&8qRagL#^h`w>|ao{$*AfnQC&F~pjc@Q1^R))=ldoW=zVsRhxT%MNM5hyr${HkB(P -1n~h-M2~u%9W~Lh|>p_`$HSsA;}=Tg5F-n#TiN0` -02y-nG|2}6_05iZwT7tC0%7_Bl2=lz&k6#v4y8g_RIPfIK2m7h}fJgi&dq=4)qBXP*^ -{)qUWRGzt%$S+k^(D$x$nur**CvTeu${EJaFSmxpv}RlPr2|dL50JoEeG_HnBNC!ASyHYbzZH_5aXR~ -rg~b>A2u$Y11V2HxyKU(d1VPM`&ZKeDbGlWHDv8)54B-%WYr1V>+MT$#G9AifS)!S&Ad`I2JdZT$ugU -4{uq}Y4vpxK^L&?CsO$pa|OVqBXx7k3i4{d~vWydtjKrX+&GqfxkW@yg_+@{1($c_h(7p(BZY}nSfgH -3R`iUEiOU7?NVtosyj;_PK;81cZ1rSF}24x6+oowX>rRcP^c`1nGcdE^E0m+TRR`*J$D9S)7T12tem{ -HpSR!NU>JPNTqVDtf@SQcccphyiwK=X;%a;ElG%h@p-lg5{jX{14{yBrBmF#gc34G5qIYkp}kT$zuAq -G+`t1r$5#3d5~s>&x@Vk1R*&k`a48C{VQ0?|S~6j0vqIcJek;2lUr6Php7*olO+5EN; -{t^tlXG<1y(VM}2`Rgx&k>RT7CCEx_2_J+VGDA5H%XH>}lfTgzAB->2F7#>XSP~Wd(J{vc9#(%y3=|i -oa{0@fFg1|fwBm}t%7HRUz?4psSVJ0F}5Q>TbrWN?*BP5R)yheVzxVRv)GOihrA2O&RK^wX{mfyC3H~ -d!6jjpQ^(i(xb##Ev62j__6;(V4yn~U@8;;;Ox^W*H|{8-&MmQM%68cE}ECb|(xSG)+6qG=oybRniPs -H>3`l_jGQB9;I8Ngh3Y;fb;v!^mOcRy@BP -K-}XG=O`1rI?lme>t)o}k5p6)UJzxnPB<7K{N|@UVo+T`Is<2~B{ZKbo7$96EeWAqRCl1fsmx*cZP!% -}oq_zn463;#merRLCN;Jbl3YHDgP<<{*fFcUOh8yE_P<4@;J~kVP -2ZWT^q2Pek)bj>Nh{jRRCPexMs6@2ijaq*oOUWw$y|ofGm&FtXrlC|i^onl>71=1%zTr+9Y}uEaxB#k -nCYq^y)&!v##o`Np5+erd;b^J7B`F?o7d_>^fgULuCN10bA}W^=(mt{;04piDDD8 -=ZMk{PnI8xa_XYNZ~A+*q=bc2Cn5qNXtA_W#yP$!;Q~BeZX}Jh($aBj5Qv)Xn`vd7Baa~L0+P -Jz(1O$;tZVx9FWfE5LBue$3Z8o*y-5rsM3y|Ed{L7HI^?cn#k}RZy<_6WJSLfwh0;(W8z;UQ@Gp;Dir -A3Fq4e*38-o1{qm0O7LOszipu3OiycJ|eGRQF98c -Ysa>FHdxWf2z?0zowOs!!g94LW0>FkoyKhKr{g*_HDr!wPvJQoZxL46kPM@u^Vq?tVB_Nh~5S-gC1A3eBC}PQQOfJWZF-ELqetcmGu(z>$3z!M{89Q;=&`B5M -E11(9Ize!0HO8Wd(xXx^IVzQ49F;<-o*F)D5=4_wGFVuoJVXjaeoO<>MdZooAV;TfR7vqP8= -BSD%fYJW=mx`0b`^&bcU@dKV#C$#vmfWo+xJ3{^F}l*kdMRb%3QNv;(01KOpL`!q?R&IWq2HWjfZqgS -9Y@3-K*vxT62cUMB;mg6Vd>$<%7H@6{#mkSw4$fGF1>A@^+a00M%R7bQ+tGI${dxTppog^ydJ9g;(fs -XCk-MwyE?A>I5Rs{dHo5|VbP10iN1Ci$=2SNp9Uzl&iCitowbs$`0Vc$-u$wt-9@Mg` -MyVEsjK%>b49h`h&BDI|88y4cw(1lU)dE?oo>t2j62aJ@Tf;+t(_MJ%+ibB=wq?4K693morVQ{Lahhc -=B}=oa)LKgcow -J$f(-{C{2cnt`gi=^@-0{ohB|QKs~8Pyx!y<@rolxlrxHU@KWe*eqb^#kF&U`{Lm@@74kUSrp8ac<<9 -tLUi}lBJ~7UtE6A6d;NBcPjr;#TdZ^kZTF2XDwUpF82jyTg*_ruS{0QE2X7y1y*6S)=1q}ZYssjDUw?Den;75;c&_0y8e;%lrnd9%VBm*J$|6Nra-Dde^y -J|n2#)P)Ti0p{2h|(kMBQD$XlrH5SOYBM48D9iDjd6*gdGErtgGUR}hk`{Txo4cR&V?=+QUg-aR?=aT -f{Sx4#Msz1ccP{rl>~Tj!XsY-mvf5DUvJX8v%9KDIq$>_Fvt4Wcdf>eGTZ?MWO}+2q&^pOR{`2Q|?gD -BO|z8cf^fNA;5Pq=&lPf_efm$H8EjV>Az!$T^hEsAX&;pUsip)OtQo>QwWSx$4W?_)^P9|A&m~SkmQx -h0Ea3f2f{rLiRdS+q}Gia)!Z(4A3wGxiL6U8X5F>7|!_hSTAW>!buMYe#ST) -9uJ$pku8<5b!8^SJD{&mFY5f%SU?}J?I;>@;g3_!C7wg@lJA7}e?97*(21utSiX8k4M7LF>hKwe>9I% -^A%i05J?Za=r?=!a&Z`XY4Vh3#?cDbjw1O2u*Y!<;!8gCx8{&y4Ht&6?bF^VhmH!#hrm~$sP=lLf6$5bTnHlWAyXOB*IeonU(O?ns#U5>OKShpQ&88*Yn3O#6?rl_o(q*x{}Bpt -0YQZtM+y25ufM|?dYhtLvw0CGtl((h~x>9esy&8LL41k7^^o`tJdp1xRLN&@409A`zi_gHSx}z%Pk0_ -1Al$gVt=hD{>Teep- -`0mK%vRVD>?7e05G|TL-x1mSS@%dHX2GneNspLUZWu!`(Ahy011JrCSFTEjgy7WYFs91b0Ok81X*>4lST>V5wFnOy$z% -g0~HPnTC$Z~ym_{O$7c{pEYl+J;0ZJR`;@6aWAK2mmD%m`?B0uMegG001EY001Tc003} -la4%nWWo~3|axZ9fZEQ7cX<{#5baH8BFJE72ZfSI1UoLQYeU7mXf@S -O?43EZY-GiJ^89uJTvPeH4W#5Er47sv1@GhC)9r1K-AJXHqf$@J>+!-IIitym8 -1#ZUZbdd|?}6b@jSo;u0|XQR000O8B@~!WXpG-gfdK#jq5}W`BLDyZaA|NaUv_0~WN&gWXmo9CHEd~O -FJE+WX=N{8VqtS-E^v8;Qax|uFbv)GD~NQlaZZ7*0Wx&BTZ(q-RuqC_Qz>zZWRUV*g2VmyN|w_&O^R} -p`1n33Sq?kU+75AG)Ea2l2aaGi65J3`mvaLh{UM>?8yGybp+oH`A{!@4+04-f=dg_lB1e)()A!`JN~z -Wv5qM_8YPzgf3Q{c=i~Eg4Hf@BB5rl%YfgvWeq`n$~9a{K}2dM;|ZJ=kgv|m33j}ud_8|V*?d|Y)TX5 -(;ecsuOSM_G1cE%FC4Q^6W!=#i`JtAR{h-;Nr!`T&xhw15P%+(|7LW7?Zfv$4vPv%#tJZ$E}f=`lHKle!E=B{X-!)B(RdBBS#ztUUd$)yW^ixR)_ -H(OtEcy)aRqRvuLo?w$is=Edp`lD<30e6=POY>#5q}S_G7jh$ZmeNrAeh+x~$sG77_Elb;KL75k&~A# -nE^qQ_Q%B}crmAVGe8iXO^jfQTP)h>@6aWAK2mmD%m`)^BovEe)005c<001Na003}la4%nWWo~3|axZ -9fZEQ7cX<{#5baH8BFJxhKa%p8QaCx0l%Z}SH47~d*h@51=@CPighxBpiWlwu63PG{yM6E0tBAw0t{f -br+JMkh#%M&Hej5rj#7<$mU8+nYVHQ0U#k-_XJxRFuka|NwETC%}cFnZgB7PYgC(KsrKPBdD>(c*>wD -!C!}wv{X+20PUHh`tT6CVRR-8BeVFeZaF&t#^i2LfbE2C#9ljoFSU-)%mh~Dj-FRU2ndr= -oU0Pe!9;>*AY@fuZ@UoT559x-6TVJ4sRc{IcHjd;8i6M^zCo2x)!h3XTp}8qY`mu8_ZUM|o2_&54naQ ->!w!Mc#QdH%pEz#Y6I#C$Bb}`ggE?f>OCMUBS_~5wG6UpY|*Bv+m -DC`IW>cQn8qN6Fi$Vo9pBgaG?@0t(U({dDdG>IP_=snwV;8W|d2tY~d%oY4|UJfAoXH9zA25zu|_wM; -6DAyS}qO^QT40#35p5PtPyG<0FB*TN%7)`b}|G(mye -0kvmP)h>@6aWAK2mmD%m`+PiZq6M7008d>001BW003}la4%nWWo~3|axZ9fZEQ7cX<{#5baH8BFK~G- -aCyB|%Wm5+5WMRv7Cup^Ahg#2I;2H=?8&zvAZTe7FG`aFNhQwTcd3U($xiw}QJyT$?r>%~gj;7%AZ6Q -!-l3F0r_Qfo*J1IqEcfQFvQlQjkOhHBfO)D+Mu?Xxygbkvpd8BJqd>FGT41Uv^BUOUXjvIBXn!;F -q!Qezt?yp#v!amM$K5R`}e&tKxtEX!)GydU<(w3**!z^lQQZIGcLrSPa*maKxB(731tW7(P3f}I#2l& -Mjii$!A&t#!cK&Cpx^z*jHwgd#4dSTyXp{Ht@;iQT={5G&!-d$xq&gCq0gcX@VcjDkuTNue6JH^|Umf -oId$i1hGHVXf*T@&R1D+ug!COD04v@v#7nW+2NaDdUi8Yl7kdTn5EQB_ -m#)>xF)?rUMewc0Ckzaq-dDWzJ}~~c^beyP8#{+BovZKG;5r&30OTQJz&V(9syONR9mBKS)+Q9Qf*_W -X;IU8$NztlIKb4Z(*BK5d_6@}@@S_uthZqyS1raAhK^=}gOM@eEY^t*xi#{WEcbpyYOO!gB{O9KQH0000803{TdP5=M^00IC2000000 -4M+e0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;MtBUtcb8c>@4YO9KQH0000803{TdPJplk -5W)cf0G9&*05$*s0B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;mkojAIOAw8#X4gFeKZ7Pvi%$ec)0-TG6J>5uDHf-_zS5Ihc)Mr;ZAj -0~;tj|61-Oc4%;Z^6^y09qK5aQ&%)-;`?!bR&c?j2M+I-*XUhi&M($c_aDG_Prd;Gw?i8sznbxlhFKy -BIiQ4E*w-o4I|IjX3*Q1xGC1N=YL!b$hPc0>}9CF&1PG}n)`WRKDzOYM`HU -&48a#~)HiZ8i0rg>%~xXdZjGrDx&B&4L-8D*$bFHlPZ1QY-O00;mj6qrsTfcwOi0000U0RR9b0001RX>c!Jc4cm4Z*nhabZu-kY-wUIW -@&76WpZ;bVqtS-E^v8ejzJ2;Fbo9meua(+gg&6qYri2JM-D2ERV=yhuR}~RbZ^?#jueX=fEcUQLWsb} -TZ(~SRZ=y=YhGaE4=YPvpk@xT5hgY&KB;pqB(DljPR*HQeNnLa?x1O-*yx@6aWAK2mmD%m`?M+KNl7Q004mt001 -)p003}la4%nWWo~3|axZ9fZEQ7cX<{#CX>4?5a&s?fZfa#?bYE>{bYWj(Xkl`5WpplZd8Jmta+@#^z4 -H|sIvK-^U@1NHw6Q)v@sJJdbqVcyl38aR;kOHG9(2%E7rd3_VFl8V -OPa#(>qKkS_0Y}h`J8oY4an7ribDlHI!0j2T2arsoaV&PMHz6AP&6Dw9Mu^u9+wME5m<9ElHS*iOzy_5*)GSV}Hyc_?rC~^IP_>|lO@xQPN% -AA{ia(&L81!j!LSo3^a+9B7B>nKAkBY3REQxq$S<^)78qgUn(!k{13DIk`*y73n#a50Aa5{TGFsa)Tp}G%t*6X)AN+8yh2B-GT{NoI1mPck&9K -P7x3|HGp0JZoRdLU}IqTy?#7d?P-{KuQ*FtKj8pGzU0-7H7#v1fk3&<1p(#*hlXV{r^SUPjZVLB$XN# -wkr*4Lu~w$rT>&`z$S0=~4RYbxZ2^L}|=k7OZ}A<@Im$DPe~){KqzChSKp7TXiQ@4?0&AtCnO9KQH0000803{TdPLk1NplkyG06Pu<03-ka0B~t=FJE?LZe(w -AFKBdaY&C3YVlQTCY;$TyfLbdO=#keM -2n(5zsR1sx&DoqbKjt}TE)3E>+~0pP2DK%Bt{me2J_=e%{y)Wxf+D1_aU2@VvpE=5=>+GX@nKx8$BI74=0xwg0bkBNm0-9#p -LbEg`3v8Q2hE$X+LH? -)#{{J_F}w;#@yg(9J`#EvS=UWH9lN9(=rED5%H2f)3BckZaLfcsVkX17cACGe=r-nMm311zYHfpz>%724e@Lc>t?T7z!1hr$$VL?-)#Hc%clTT8;G%vDO%eNxO -*7hQwEB@$hC}1lvPwSh(A5cpJ1QY-O00;mj6qrsIb($*W1^@tDDF6U00001RX>c!Jc4cm4Z*nhabZu- -kY-wUIW@&76WpZ;bZ*X*JZ*F01bYW+6E^vA6Sxb-GHW0q=uV9^n)_~w5Ns~j66exn^*h7;;fHW{@kzN -atM3jXW03&=oiG=H@0VxH0TwUI5iCzFWBa`I{RC$80wHOP1(so?x=IM;^#X1m1DNu>-_;E5FlZOm(FyeBF-diq1Gopo>LA4GktK<*cTFjY -$-ksU*x3UrX>kOY}-$J80ns;pW$#QK!^SfE!qCj?AGZEbK**B(ikEl7&2qaT(mJNuO2AMZJDqeyhsiz -X^GN1P+Zq3(soGg6lKU88+_NYX%5}`aK#Z)28AOaor!MS~NpiC*o7YfibN6WT$BRQN`f^Hgjf+xDsgh -p{35gf_JmPOK)@EQA~V)!1Um!+(1Vi*a816j%UY$cg0b8Dp_b`bXKTpRel$*a47S6&%_9ufIGn{eRFY -RNj| -QEOf`Pm5sCmoH!5um{N?Ju8}u#IDf^BB7?NN|AFbi#^7D1`j -Yx!#)(tpR9w9hmA5Y19Ar2ZMXs3!8%%7Z!rv-V^KBe`;P$JW7s*eg0$cliX2lY^__7S0j&Y#J$i*fcm -QPuPnvXzunRrW0TBq52v#VGhUIVy3SlawXIC7F*{BVys$i=IpscUtYXk -qBx&T|0_Gz7BQpwt0~fl53XryO5m=zCOrnSHQ;&xVASH!8ru~ba0Q&h?7AHX0jN54LBnl4{1H(pGK93 -4c5A3S#8w{zDbNJmj)K%pkRk^zQq1R -etzbYL^50%1VKyWQ1tb8@QX|ZF+k!bQZJ|tE1@HIij5`tFZN{NIB{vO;ev_l -a_FqAT?LlPuZ(wuHk@noPpA6%fkk0JCEtCMq)-+39W@Ns())c4n`>Tg|HpVoD5T9;Y#({+9Wmw4Ry#H -Ukwu)02VnNM~oa?~EI?v}}vyxO+|EAr7sXi57CV^IA%IeTM4f5%YESmI%&(zr-emL{s)ngg+0*?18`- -fR>dtxPjuxS~9zQaEK;Gk5hoQ!_7fpH!(Oo$eohfw;DVMrQSaY*eFqd_oF$Y_vd831qsdqaQSaN>w3Z -fgI*!CQcGA@L -c#y}aUZJrgd`f@BBLr<;>m3{wbrz{X)XY)?y0=w$GP_RRThiMc{v!zBLY&T?Psk3kS3grjy?BH`ZeFaRe0m*ZEN_~LV_D7V~9- -v$FL6Y{^3>``9AHsX>A-X3YP2mOJbOZ0{hsvkMr*uz#oWc=3PDR*ry1t<6OS=9>*H?6XJ%y*k35}yGV -g-KzA5`fSQ9BV_nFH_1HAit=X@urLC_1?xj%q?&6kG9{N -y6Ek%^ca^ZtTK)VW}L@$SB}~he$o8s5TJ=Q1)1JPcPV;&)uGR-I&c`ss<)3<|2aj*FKqg}mF@8rxfEi -pr=KFXOly%IcS$o~IKfQa0|%T2cEmqPYo7j>q8=#z;S5;oYl{8QsFgaF>UWW-<2SA=`HSz=Yc$ebL+Q -{*8fcZ&J+{%iP@lTrD>>C}3%iMt9dXbzJt2-yeYrT-FE2?Ye~Qy89qG1?P-%Dt4tP!1q=}dI4=UGY4q -CBZ$P6a>TG~+ob6H;g>h)BqmHI7`E>86FJ@!wwdvwA^!u5YJ{YPMG(eWgOqjhO}6V2HZ681T*AsmI0Z -XUI4^!SfhcYaPaE-e~cFFyjPI+^BQP)h>@6aWAK2mmD%m`*^!0Z{T5004+)001cf003}la4%nWWo~3| -axZ9fZEQ7cX<{#CX>4?5a&s?pVQy)3X?kUHE^v9RJpFeYH;%viub5Sul@wb_+S}WE=fufNQ@5LUX_FV -H?e;83rDiCJ^?Zfo$X3qxzaPN)xnuTFw78&6LTp;s&Ep$KJ`?h_AD6iKd> -he&dtG*XK&<{ly?A|FW+iR>`R%7Ydg4?&!T*Pn_^dqnZ6cU5SerH5%kmhPBGxnR0lco~7|aF0Nt@C^z -5%tsM>2{tk-avxQaurxYM6fw;Vj2!4-(eTh@nb#54S?ZH0lO7#xIg5P#Y`^y4zwzMz&4d4&2mkLL{N -Fuz&-jD~op>x=qxe^SJ-6#K$>$ -~-Y9<+s`wnth#Qh4vaimg{@0C)qushlfS(i!8a!HvUxT(_yrywPJ}!OvOY0P!%MTp+~VQfy~oGk`+Fa -zt19I03NFZfNxA%ifdm_8Zuf>&{2gaK~z3t(`@C-Wc4bG_8t`ATlo>5kmuf9p=p~|8l`e&e=_-^W*K~mC68p#JR}_ -@Y{=wx?J964?EV3=s&KbQnT67S6?>R&{Wb!uAjhDv2{8XQ$zdNP(15v}G8i-CknxPnFllf_eLzzY8pG -1sSej>lkz=CrR%Q95=vB5M;>EWr}=D;ROgG~i6^Kd9>US;uszxN1#%m!mjkMe_LsT!&*hU;_p<2eX}nB_>kwd3P-PIlQ!ud4AltL-LR~H5a+4+vNu?3AYbc&XhRLcI^?NgS-P?E(gvP6q9H7VI8QKvxa}Ggf|X{Hpu -ZtNgz+ZqmH=VQ)XsD!5Gx!nhYW9N)9gmNvyA$3VGuX9ACvh|ra1itxDnlgmQ#IAe~+U}a9+yalt%%-&qCG2qKt8ar`Lx<)K{&Y7MOQ(a@+*#b;V -_@m^X3L)K)Df!L;VNe0nrx+h$QbU2uNXNv6Ylp0thP&YZ -#riwhbcJ3=Utj!{gxHEGg!^X`&G>2@Hxygo|y4S!gAIPz&@K~g!eyMd4vU(!U7OqR}$kZ92g~M~lq@e -oRWpUvKj2*dp>}29N!;OgC$YuqQZryU*oW8YMdJy~wMdpF3#1svb4$0AnIH2@jj>7tt$`AoHls?x;hh -qe_lcO`po^mqJGGNm7A$YL3>W0KoabeAg0tdHL$Sx3ZL*_GN21Tbns5`wC;dM%QWK5bGZs-k{X^17$c -tjXWcbEKws9P$2K^UjNnclh!6KI+=o<9L6Ev<~^L$RNV^Q2nK-Sn96+*Pd%M?`t9c+Mo#uCkSuT!&l7 -Wa*G#0j-z-1HP^SFGaA3hJR5RLU0a-wbMFfA0@mlPNXq0au=hebl;Xr* -W5zeSOgsG6b2H6Pw6X9bG@&geP@si>@D7%I0%Bxl8EPE@yadVZGGovAdnIDOIAY}P~`Uc8zk)PRJaG1eSVS2V^iEtBGgQ -DVCi70(|05`B8%m(79!40hznOlQ9T3ZSj#iTXO7) -daT8&0$i0P}v>GglxZLrVDu=cHJ*YM7len1;U;mSU{pP~3_I*X|wv?#v+>lt8zvs6Q%JJ&IoA*={-vf -g}5_u6P4N+#ASh=NJu)Bq0OqIe=2s$c8pi`ye$GUe2t*WddF`IXT36484gB?mSS+0t5;+Oz%!89RYrlqca~$scLrlN4V+#eS}8)lo!~d$y4fVYzOxcB -F@eJ+fzzeK^>!e|ymeH4OJA;TO9K|_V(4;5hf0Pr8#tQ%qGIJ9T<%lP44xCyH4nMrf_1?`0xLbB2*`T -UndrSUDSOBMKWAC;GL7DqMXNcxhLUCp7Pu5@Sx7;>4^<~y#6C8$r%0U;(udDO#|iZQ^YE6$&`}$C#6_ -PSSR3SDYIMDMMy;ydX4z=gTn|u7Xn#PvhYPe>P8XJ8Ar*1zM%OlkQhWDa?ww~I$WF*wpae4Kf@2ipmY>dtx@yy&85{{rq)8tcUar4BTRBvrOtdXW+t4XNjdONmCB&u -F&RUi*>IhRBG8?w;<+a%sz{xfF4Uu9bc^yvKnq~Qq0N=6C?^?F~e(Z4%i{Z15FZPj%!Fvn;};4@|HsV -U}4Ist)3zws~x%qQ0_fck38l(*2S;mjOpe9-+=0TueS@jAhS(i0n8Ra7gCr0U07A}SD>x+6mvA5S-p8 -#Lov6p39&L-<2h^foU@{x0rN0pYqVVpwBq0rEt$EM)qSWEp$a5LxHZXC^$cYYyS5%?)k|I$EG-gz4cP -|fMQXB%j=llGO_#dq5%*Wx{>r@5?E7{h5f3wIaWY^$sA@G`XLc(Qi5j*;XMj*;^W0@3yL}6n -No!Trv3utxpm$P*uT&At3S#7XgSY4dDP*arJ;I`MU_#isoGK@*ddGt+V%4^;9v(@(Z#@~ -euTLXcWxAB&g-!w#F)W2+4%clRKJoyhTcY?>bWeDqS7qw8=Ru%PNHFkL-kc@8ndMD*v6pCRnIm)GRQyJua2pI^?{?DNane?OW1 -=ltsUvHdu`zijC~!0VMz{%{$lFTbT3ygQgYO+dKn_I0|5$hY{cqFh3%_7NaMy2 -iu!sBbd=R`;3KqCPZ+U=wf`L#0+JLv^$^4O*<|{7+nA3@m2+Z%7^Y`Dry*Nh$H3wut`J*5cRK&deK0P -&jMx)UMB>ox_G8JeDS80H$1WST!9sCn7aUYkrCXUSkC(J0og}J2=n53<9=n1?G&j`wyjMt7X+=46K?mB|DgPM__O!3_moo}@psl@(ozdU}?VEMv%z+$|xEnyvNGR3X-aa|&J -yX+6Zw;Icz=t^Wisds3SQ#{azz|=74dbf`tTO$h26@Wl#<8+f6R^Ovo1eI)jO)W~2-293DH`(%BcQh -~r?-}-Zxd0?omF0du21=e-^buMa1_40-vRe*6#%FuEW;2Jlbw@aQ`+uCFT(R1RrxcVTvf%Tr13#1!4n -^-hrxN)Z(r@(YWrzE2hbmMM8!Z{Izplu`XNKOz^YEpIa)CrKk^rI&n0~@G<@#!tqJD65>h)18lty;|NI -o^39vKzn*`$Jb!ck19`thkk-9$RLGHmy6B-)sN!!-w~;zy0~c`NcfA=w+c5J9J_5^Lml#1VS -37>r&uNa|{sRwK)&PTEOFK-*AStYWeQ{+jr;hKfFG_=!n-P_^aBAKt!t`{n`})U3+>=jCGs1Ou!m8z*KmepX(|d>%yEG -G&muFO@FV>&Y~D_P9cgr%mJF?0N9lSU(+Xgvh$xV3*89)w9tbY{yDkh|NWK4WV=TboBfc)UMaT79zW` -?XSLm^5ofL))(U$8zO8xrf1itb -;pOp`nBYYZNH6*q7lvFTnlhFkpD}7*_(Md2mPow%zlgL6}KPChzpPH={h?LMz6p;K) -jL7Ifn|m)2!cd)oCX*qD|U;+a}qWq7mkHBh(WScQKHtcBT#Kl|!*K>Nhc7PfU|JEYoU&`%_)o2f=%j( -x4Bm%)5a7s9Ar?m1bFHK*o+c{mh2a6Jkmu9^1lSHf*mP;mgl_|EOEHJ7tQ~4;#B!7fydWuRgd~O8RWhjy>EK -X8A+Nj)gRmEyDy8L}{`{c=Nar@1a#qEEr;R}1RSg5OGh`aaLJgYx7>(Zev@$c1iI!D`r#funLHH_;0J -ENy|qRJ_5#ceU(aaqsviv47?fMs|c;aW0srO|Tl8m<|Y$$0A@9-h}ZX#ASb-4Fuct -S-Qi%8VgC0O96x4-OXlV^)XBIj(0EEJ&EA>#A{hpQ?4dHs?Lw^W$Vv;*>A3ZLIRY5u=<{gm(hEZ|1>93s9H&kjuliJf}r7hJqK^v{9p^QD+x?ZtKxyE?mI$`o;`l3o -{xfu10_V8^VW@e31^wy94um*(uGV_FRz?PFiBNa4x9wLCey72mT6XrD`e@vxf@=r$B=w^^fCUrs}HczH-*xbb5HQN6#LrN|CnjVh%CKGyc+srrmO)Ul&kJ56n$HGxH6 -k@guaHS8;~FuhlOS`+V&L`*im8Rd5`P9zP!)9rc=&MuwuiY7&qRRVQ2T$wr&Iid_gyT -#3HadS7mT&Ts()$#alRDB!WEhd+X=mb;8#dP}UD`4S{ucMsKLBHEQhT_TP3>m|>)60*iPp=+Lp2GLLu -MBe^>jsRM_)^=^(8+Lj^`tC#(8ZwZwMZk-Jak@ljCwpZa+uzy1ASJ)8&uFQLQlrC@$~Y^Rab_~?ltO@=BKjC+q;|Qp3awi#`P~RT)Cs -bX{h$cv3DbRB^(jmD!tVW$aJi`508uc;-I{Jlbbxa1QTbo^~>4!SI2j9vjW$Ojvu}_SwxTE?+f$KV*2 -845&h4TlW+caF@I5gn}o?7hyQNxM0%GbcX@c1>%08+?&00Z-2!h)pv6|QO;6DW)9lmmC5V;e?Ak3W$U -PkV{QnGM$P9a@g@4)%`(Y8HQn@A=2GgVe15ir?1QY-O00;mj6qrt6a{mwY0RRBL1ONak0001RX>c!Jc -4cm4Z*nhabZu-kY-wUIW@&76WpZ;bcW7yJWpi+0V`VOId6kq+Z`&{ohVS|ngo>gz;u+g@Ko3LU?OH57 -49EgQvFTNvEE$sAkNx^lw%kTeJ778(kw`v#Nr~yK>7lN>(T^7E8e~5h>p>iyQKQFt-WSly2kE5I1&mr -Yro~!Gk5(whSd6jASQl{RLOBYD@u+N!c3x=j7Nc!i;YFpeM>;LQ4WhyAov@-Iuw$z|Tm$Fjy^%UwS#r -j9H($2ZuKr$qtA3E9MM~EXQenpTm+r9PaJlAF6l;`l4!%l72U!qX&cE}|3}|uAw?-*3ICSuMNA*q&z^ -4WW4^qQmu>g-xURV?b2;G8ftRy4Gx`;8crpOzM4($UjMUzzO&L7kceYWRB;|1`A#}BXpC7sXGR^=o;A -Y1I{=E5!1b>>iYQFd5E%tRSJz8rSOf;YcW2k9r}mhVx*^3Ow{OmTZ2cKnby$S(TO1?@<^*pYj_h!q@ERKi9JvAK1) -=wF*JVR0iQ+F@QGD0|XQR000O8B@~!Wo;h(^X8`~Jrvd-~EdT%jaA -|NaUv_0~WN&gWXmo9CHEd~OFLZKcWnpAtaCBvIb1z?CX>MtBUtcb8d2Lb6Y63A3zUL{X>A@9jJoO^#p -&(djp$DOdQbKlj)L_h{Nmlgjn{3ot@U)rv{$^)5=PQp0s#Pwc4`49Di8^S=9?(Zvd)fn8gqUd|`mTwc -PbGn}6u0OZTO8Hi8JsXCL=v|6R2yn`lL@#}+>&Jspcpyrggl^a$%EcIN&(xrH_t}b_c-DLikyC(@vPj -S4O`utx75H42Gh)+PityV^QC^6j{Dbi+_wjrn0@L_n-+Z%TkpvjJ)vpQc+8l6_GOkl-BT%@6aWAK2mmD%m`+lR5h+vx006TF001ih003}la4%nWWo~3| -axZ9fZEQ7cX<{#Qa%E*2$U2)D*4{VHg)z5TuoAzCj(*hUX_ySQJiqP5*D?-$$0uaW0@8O -y=Qq*3>O(c+n@VCI2cgph?4M}|RzQH)uj-8E2m;W^xlsy#HCGcX6{M9mpBYV8#9;w;6Bw6)DmYeq54l -+ZKuVDmRpfy_I1!V;ssE-v>s?6-kA>|%GyC3<^i0=Vd+aRLWDRm?)2d`Iv^;eg(=}*apkEkF7e=UWr?Z+GZf|0)_}iW|z3u -OO*Xn*R4g_Ep_}_hsfot0w29P!9vk~0HmdYb7Y8H%Q=q>CT#J5_gNxjn^7-uxYh%P@tW -%f3sE)y?2(BgPy+V!2YTK4XB16Eq>hl-j|6k=R47%AgVDF;)FWym?^Fo#-6NX%SROcZ#DCB!BFJnC_4ujkr-G+=~e>3k(ek-aqzz6tp_ppj)@Z&%Yj?+wjZnQ@8=KZtgA5cpJ -1QY-O00;mj6qrtcX{nb)0ssK;1^@sk0001RX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFLPm -dE^v9ZRZWZAFc7`#R}4DY;Q9vy_OPV8&_hEZEfkhuwDverBTGh-vsp_2dq=jTwevyuu+?F;o{#t5%t% -UYO#?+ycfPY&6d;?{SP$aa8P$0#hVKk2d5}&Tok6E%X(}v~^k{{0jMV|-*r?ozV@|t8QK3CS-Ft84Gh -w;a`b*QQF+R^rqn#JpyGUC7>YRTx7UfpIr3K0xF2-7mwLBJ$Xj`eba0kEtu-aG{>=`Ii3bhgqW}xY>Y -ek75^)0UaAXJAgNf!)g$$A6==|CF~E2D96g*7LC8a_89B2qENDy+eqf_js@2(?F>!A|H(p&dy$=S!g! -tnYt*QgV+#jq#}cDGZ*4!wQTBZxII!7&g`eEQH;Th6VT?PUF0R&SBVsA$o&>Xbwnimwo~>axQ|JC}03 -#C$;@P`f?hf^`A}IH6%Gnd*5^bv#!q@meUDpVy`bizxBaihuGu-FJ+O-n?34)KQ4R=a&s?VUukY>bYEXCaCx0p+iu%N5Pj!Y3={;B44FW0F9ICEb&R$^;{u7Bm!gnX4EDS1)g;622OpnTI*95^8dYBNx3f{EDmi&pIFX6`3@=FE)w<$1(`w9a>-Gm4|fmk7E -8`1AnA%r)ZtqPbAstWm?D{-@7u(>j^a?QS?rpggu=~;kE9k~~M8-w0k6&sJjo=(x8Qf!F6DWdbjEFjW -xqmz)o)gveq9Bi?#0!xNd0hD!glowKvTH*K`^@g6(6ubwK5jKfQ -yv}MD(>*7dfP*em)YeyT^9$Ye{^Wtq(0kfyJ)+A2qFeO=r>uQpcvTQb+Wtr%8z1}svyX!S?it|+EHaJ -5{T&M3#+VrCiB>5#H6hvlibYv?FEk>TNo2X6b5K7#s#>6$IF2`Q}H>w~aSMc{gS(Z^4&=?2XG3U^G+2 -!%tUfq!xmT+eR%rgK!99-bTl^q=2x&=?+(&&$aTV_g_NhaSf21%fI12lD1+NiB -bn(s5l3GDQvatuYLY9#zJ)jzDroUxYVFSayH%II3R)eSh%z&ZL=_@sfoPR37GFu!R$--m{7t;4}+4Ba -|*t>${t%w;jU!$hA8zKqiQN{=jE8wn5deg1S$&Tv%e(`_D-PkXe2YfEg)QgaD(^}0ivGZ81E0iBFt45rd;xd2A8y}4A%0D-IJ7eSSwc!Jc4cm4Z*nhabZu-kY-wUIbaG{7Vs&Y3WMy)5F -JfVHWiD`e-5dRH8@Kg${S~Z&pwcE^;uiaeFm-6tq%$xFL2PUVT+g6Sl*HzARF1qTYS;gL?>&-tlcOC3V1wYhTj+BqST@?mw>2!QSzr1|a=f92^e -DLj@O#sm^0@yT@j_9T{_7V<6OlP!wd|w-*%JH!+?B5KZ-b`BA-i$Y9Ta0gK4S?zGv%G#U*<3K&H1UCZU5T;S{YR;=QEuW -KV8g9Qaph@MDlTAvqA*5xr)1$}Fqr_$|`nHzR(}zHyj$T!QmMF -2EO65{wpbICA>33f6#M`7J_%R*gIJjVzXq*aTl0uq`#)*05TZc~JwayeI-t-qA5Y1Y6*r00q{(lEr~3 -fLB=oVPpXR7ZFIvUlHyFySYsgxRn~+H7|v#f%Sr~LB<&l>q90&+ZIQ|ACPv|~L1FVO6jtC-?>1hou-qe#(-w=&K9Ns|27Dx17sQsv;wEom*`2G!Ehj#c#f*z9T6I3S%_m!@dx$pplY^<#ID_D;|sFY -k{x>_{Gd0a7&-t%LN>aBH0~pTzn8>CmWf1vdfoydLzK$F* -OaW=3%TeWGK!(&&rWOa^?9$D5tS>~14s`ui!A-|zD79C~L4OPJAgX}qG#p3=9(rwg#?kMr}i~VbQ9y4 -)?EI<_eDDg;H1EY;h6jJfrg;;{2Yh?o(re`j;Emip?luL#z39X827o$mhp~gkNL;-F{9`^jum7Av`)| -hR}iPYX*G#nW5Z!43~29Gn2kcG1+@;q-Xi5|QDWKn*JdL|LVIpNa3=$Z!%i!;Fg(!eP -8!wCA(}JI3vi>9A$Ot@sUZa>4lDroY7dgz9g-&~GsuCs{k=&`b(nqHKph*p=X$xExyDSWD; -Zn%D~;CX3=xiZd6HwO)y0yV8jWoB$ej+ipEJ>Y#PMXm(-HaGIUc-d6Pz9^iaCv@bsQ9kV##Jido&s9x -X_tP-JA@aXiFk3apDuT-7l25NrH}m<~`E9(+LGmjXX<+_y4;9bb)P9?};L*K6Y9|g}kC*Th@b&uzJ5Q -1ou)bhlr{NH8>)|}!blw?Lk0+4z<=!e&z)t)A-2V4fz!M#WwTE-c-mYuV1|*nPwvUpjBL)0X8dzANcx -dgjQ^2{Wn3*&>c@2*AyBhReRK4o>JFaaZd~X+EYs!k0W8P%}|E?(zpA*0{>lf&E@S3%5E6ICh4Agf@J -AVN_gg3h#Fn4B0Vg2p;^6$(WkK5-8$D{jvaoN(1h1St6ZjBy!nb7}_*zQnB2oOuDvE#9TQ##ARJK~p- -Tk^fHn+d2@q)1Y}uVt5CCn?6J0+zPCO%NjqJp@L?ek8AlKxri67-~frLwQ;8b%*_xd(>Y(&|#ql51w| -@%1g9dKsJFACmJt3D0A$BQniiMrlPF6S!lJ(SL*4Yj&_*@92p_GBH)bIg?ZJ0Utn|1?B-9N?DzX@|8= -IS^^@~w&z?Q`Kz*zu_2m%2s4LZMVbVs;$T$qrQUyM*=}tzS1zT1R#4K1k7|y1x)O`8_(G){J2w -hV$wGn*z!iNRy7rQSpa$NOVN$gf9*~E>V_wfRowZDYq`I>syi>^zwBK4NKh`*DbhEjA|aZz4jgWV}A) -R}8xuwhY2DA9aH8;Iay#rD{b(%NT75QF!a{h542No4t}YUR+^zxa$@Z*$8%JZ{mx|d09;7wUq_F#sJ%snTZ^1g -30ypjyZrhF2QgTAa3yMk$8{;F@pZ0ifrHKhEL4dex$?LNo#@6t;D7r#!z{TmoCjp)~Is6MG9zz6XL(x -nVrHwoU=(uk%4p-xgCXb)6QecTJLf35=YOq)YvNjk(!T&8)*0sW;?lzU)^+@=NR0}zS>=@mN?SVC=7`U_MvMpm*ib)jK*?B+; -T9hm42WgptKW`Iscv2m_=N#H{BX2`?-5y_FU>LTKe5o5+=1Hvov3anE;!QYkO*KS@bIQ3 -vUqC-zJPf_uw4z*OLr567A)945{Q9}YY(dMjePfQWXC6{G!K$&B-;n)C)ni6eQlqBz2$B&9I_S=lXku -~{sjFE0UXX7h0rE9((KjeOvf-dYP`t5DPLw8dUjX=WVm9qwJ3_Hpyy4i&?ZC6WNpBrlS8a*XBOS56Kc@^0x4DaWdlNsHhvCYu(1P5oy{U -Pll)q3j>a8e44Ue4}743dfXkr%+SAV|$)oYtYMN!mbZkBdP{NDBj2B>s%3H12J`pDM%@9O(P<`>I$fq -xP#(g(z3LMFhy$7%vJOb3 -BBSdUY2!lg|o1^GjRfo=*eN;i0rS&D)vA4G_!Cho|*khU-wn>hja~U)+XhjCWd=kXGS-Rgb -9a8qNFXoQ79NZModqg7Y+F%sjs8I!kUc`-sfy;b5R?%rf1b}oJke`=`j^PT@_}u34tGdjo)bWFN?ihk -2^7nl?cw{9VL0g2!buBHU -V0Sbf)7)f+Pp5q^^vFq5W9)%B{7f=`k!z>rR_Q-|&2bQs4CsoMm -NPMGA4;`_h0oixi`eQs3oIOzUV#Sg-cNUfB{I~k{4jnzuNjrjv7t;Z#O60H)2XjFu8mIRJhwQF1ki&{ -1eYjO-p*UIgRFsN_07DaJy0TK06)pb~p6fwXw*X^(RD(4yyjCBpq2lV90VQK1>?S)Nj8%;*~y%_i3-F -3Ae=79~wW7q#1$pOj0agl*?;PAl#gwyZUii;6n;vJ$}1-fF$*+*x -UU&{-Q>I?(WvPaC<$G!+t3nitFq1*9CZK5+xb=GlpyyPr`xG9ZTzRhAH_y6JD2h&Dq!A#gtdatd%i8W -MYRS1GyN0@%V~i`D`92@|Jp(Rj(^NI3!akoBfm{qA0V=3lhDLAhhDL_KJ9Q~{t(jAGBr4*_#aS90|XQR000O8B@~!W2IIcL;8~uWYsBlTx24|!PS+$%N{eD^JZ9(4Sj -MvSR`hYX#WQ}eKKdsAmEiH1=#PHt_<)$DdsW=wroFB~)Jg|-yXtX<%n^}PwL&uyqzf#6yTO*2qcm%uE -3Y5WDtC*2uogmT?Gl0Y+_ENP4Y}Nl}E+MduBIMvV1T0-40T7$CQVcTE8?ZS`zmX>v&*v^tMmV6Ay?{c -q3A)EcOB^P2jlf%^Auso$QW113`(%hQ9o3qIjyr+e#;tyIXT -v6ld{acbW(W`pe8>@AOT%n34+&vC6-);6{MOp0_=xu;3!|{olQ{^&J{9ehwR!@9`nUC753OkZaq%&A9 -;s5$E4-DgSdytN%4O<1c)+y0yEBtAgaTah?c2XcuAD$kyAf-TEX7M=q_ywPJJwf%}rBpNW-7v0+%;Am -bgdg4l{8;%kL5t^@v|SeTe1=SNTX$)?;bRL`HQ3TB?ZSw-Ap>m7m;!>a@vi%}MEnJ~p`N4ky5Zhfhs{ -5>M;dxz`l~9W2QsS8Q|4aiW&rk#*gWg%sD8gag7ZaZeByR*VJN`N)3_E@Q{Dh?}6X;BLz}oX(`+cj%y -+1X-_o6x+cr30&><#n?b$kxdiy*nX)!R<>knNLk;LEv^FEHCZZpf+yN1Aq}*4!D;RSsVev{#fcv?7BX=rVZe|){q^iC!m` -a3w^=8WKcq$vwN|2GwN8T~)ja3OAYt~k93%XqFN$F!`MSthqWZ($EtJl|n3l7Dl8-?M;^H+-tx~3!-n -E(_VYYN|^=qQ4}^7{|hV{7WiA0noBD3U=O%dbqUTzu}tdvl%GM3d=M5_r0@*NZxRsGxxHq#S0k-2)CD -VJ1||uM^uf9kJKzGtGH!D>?+P!m>(guhc-_Ih18K#Cc4Q*TeXbjhxfRl;=8 -4`TvY@og0xR>Qrukku&L;IFVF2(HlC|`LYEh@&})K*zjL2$RU4nei65Uj*1q%(METQ3=XYLzBb8ftby -r_*UCCN7vID@Ty(wt+As>MBiUY`=7(`AJP_INra**=XB?To+YQ;g2#{dL{>ga4K&e+{zEnL#e<4IlGa -8{l(@1kNjAJuhoAhFP+6;DgiA=(wAPThcE8%bTx!GCn)-@Cl8LRIO+x`?7|S4_wnw}o{7EmyUYB~ve& -(&)k#OEE7=_$>jvLWUgquOwY}dfQll1rr&g# -Zx?`UMnTLNS5S|M9w~{H;da5Hh7=E&HK&L{|mK-zkYrySw5XN=mZ#qItM_ukjW7n18z6&GvMeKkN_ad -pn{#EC|WA7fxILUmM`?80AK0f*72;rirAG&U{l0`ITU7-(zPz`3iBYlFA5F<)-ZMXDt)_bs$L{Q!x}zFT~(pW(DH`%^EM~*%KZWH2bB>ugKzt3AhrdTJg7m -cC4!)If%5=eL|+QMOwxTtWN{)Hy=QDjXYp@0~kZKfX1vD$ -d!LEBqnN96aWpbZb>ZOyLn!@O-#qsi^jmqwU~wzy;7Nc!x+?;e)KEI;eBM7F6*fpUNAY{n<#P&NvB<} -V%ObZGcD>27k&rQ&-Urwv$SJqU3ph*PQ{$AO+TJ#Dyti)AmiYC)_ew6J^)wVXA6-#4+d|JJ9kFkqc?N -=mAKQFE3gH6W33DEy4g7h)=$>@DVXo|fKw<*U|l8+Ov44$YeQ42&56ENQTk_1tGKyWyZSA#FtZ%S&Gy -g|>V2H?78j@_7+jFoqK`sY{fgq%tozBtx%>u(_5HXawtiMIpZyC^O9KQH0000803{TdPN995@G}Yk0N -gPE04e|g0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@X>4R=a&s?bbaG{7E^vA68{2N%$n{-cG0VURm -8IEi9)p#EjW30Pu47~!Acf)zV{s(2rAUD@a_nyN?>%!PXNHFq-lSqb(i7UF5kCKGd~W(Fg*Fn5$QbE~63?UaysMFEI`ydKtM(DeIg#qW?eJ4_Nw}LGxXe|N&!c;>DWcoNF@%TDAQDDU49wU -|PTk(pS{qS{i&%Mi51}f3&jDtBi$JUjx;78}=x~jXMo17G;|$0IkX%5dViAN}$VG{MKjL4Hctnm!?3h -S?^xauAD1T*(8z{$_xB?tUv1f$33E@va!kZMs~OqQCvwLKg&zWzaq#3D6Uj?b0IwKRB?SN; -Pp93C%vgacKL?Ckj88%yXt86GU8_DWf!0JV@el$EU&!i+fLezKwE=MEZ=e**-#^_>yEO% -^G!8L9#7lLfGPqVRN?JE3h#ap45!l8;62t})gQrdH-W!8$neWD5K*oyeQ|owi{uWy-hL#iQYP+a|pB0 -y$=6icE6Frf&tikuBaSb=W*#d<&G(sF)NE5dutSZkvGy9-0UITr>dz1(-$2M1=`t#4;@KYS!DX9O -i5Lknw2lI7xIzO*y!0W#3{^GSQwJ5?k258H(9Pw3DD`)RFUX~WQ;DbuLLREmG%rY{6BITzLMNxKGouu -qAOCclN;)HyZeT|!-ooO_V+%}I&pT}y`JHdsGGFB^Q=!wrZORgwXkAm7q_k)z6J37{VXq8<6Y?6j8)| -Beb4&&;-nC?ah8urB8s>+7U-T(4kRTlq29l%)*2?IWf@J|&Aic_mKi;AEj+!?h1%Osibr^BRVR&ei)IfC -N^3wBLbuc7>^KOfCLECNXZtjlS4fA>p3QLn_*G9*KO{s$xfoo{API4Bom;s_4&w672H9;r-`a5U%ivV -I=PjBhL6yquq|N@{b5x~oPg4oT6RD+x=0Moo~#Z^y#Mv6*emBZBYp<>b9uDZO#O{;4b*ZtRxuUz4f?dosFPKQ~;TD -Q*{9@TGb4TpDV4afhUHLN&^Cmfm-V-jLy*hA2N{Pq0mX7Smc2U#y_u_i-;g`S450j$LLaf@N+{01*k-DW9fukqauY|CXE(t<~Vt|M&A)0TE8%<2kgbjJPO$a3Zt7qlC| -vv^9{xknJvJ>x1T(*{WPwUljO_Anhlt{?1hAc|Ub$q^@!oSVrXC-8(E^>)Adirqqsmh(d#9{kt=scBD+&qi=BXXVf~;`Qug{rQ2iogsj0FM?4{BcCEH~4plc^;9GYe#5Ytd@ -8t%uInPejEsx3s^SQ&5gS-P&P-Im8)P2{z$*(z6q3wy?&>RWRE$!~`Qgq*WB -)F{}&puBszuN0L{ZX5Pk3mcf!cs&oP5UN?L$bG?Bv(p#AGAO)AYJlMykTC~lKnTm!i?6kB2%) -K|T2j9;2x}RVSswby-2V|_?GdT6h%3~KhMn)#K5GD2nb0k8XS-W*XGx#1+3GJ~Y$U%2S`HWtTVOFFawAS7e2d}7>w2=Gb$;7@I`>Xo91c*264;13y -4-Tf2{{m1;0|XQR000O8B@~!W8c*uCUkv~NDmMTCF8}}laA|NaUv_0~WN&gWXmo9CHEd~OFLZKcWny( -{Y-D9}b1!9da%E*-YL4s+CzcUk=Ds0TSD}21wH&ajsXmK4ML-EY=jM&8SYDL@P)}Z0rUCnuMRk9_yE80!nwSb0<)(yQbfd7VOHyjXd7|A-`) -?30#wq;d2tLT;ipABuvRm12Fxnk`dV-;xtf^x>CRC#?@aS)9N5kRs+A>9E*T2*xm2)l-Hq)E%tNJLOW -{#U|xEGyPU2JiMliFI9;^&JXHLD!oK7@)ctMTK`w4Y*tWd^S60pIZXv%jzu(%u!vW449lKgETep*BYJ -H*?tE)p(RLF6Vz}v`*j04(Q2pEKv`(Em}g!DQ)Rnkp5TNQ~)uHqG&c-H(;|gU3YEQurw -vb7T1!{D_)mf%ToFGlH|p0!Hc?DlCCPUI%jEFw5*{epB<3h9;9xc=LM&r-3pX!S=O@rh-Mk%d^S^5?< -8lg|8AK6#`j#wl4udGV3E|LB&#dlQZ$l!>Fc84^lJm}(Ufiph8ADatF9<>)^Hy^Ex-d9__2}Pa%zn|1 -A)7;JjZ7%a^7@o*3#>=Vb|>Q4xTg&#NO_}OXKLz_~M^mF27xTiIZ;^(PB1Ji=MWyq5vxhCMWV?98sP} -iv*|vkF2De6IlBW-<<<|TwnbG=3P9W5%}b64eM7_MVqEEXXSb+9Aim#9r#lACSRg)Tef|ZD-8S~V1jV -KT=g1~8pY+A71R*>&9-F+e-^7r)Zh^vS4a>+*Dc>j`<12JnOF^!=o=KjX_Yzp;@bBu_(zjR>OpF`KY -1=kEXap`BS#Sz=2&ZiC!{XcnkgtP?`r{+Y;7Gfs3}~u9d7G!a#Z`=c{O0tRlk?x+pOHiUA|i(af@rnW30RjsXAj~NIh;s -z$RqK;7w`tS#*b3c$h3SEnlb9*&@7V%@htcF&ml*}K%Y68lW$2ZU737Go-LNz9E%|tOa?j!U~*Oxn1n -GvxZ7YYVxu82#$Hi(JYHZ?fsTbqExbM+%W+Z<;C(;0L9pBCI^uZefJ -IQ~-PIn_VK768(y4&5N7R9$IP!sDCxSdX$rC&&xrN;ys+tsC>*Dy5tXAaNRK@|9`aQz`EVq*8>Ng!<^ --xSgmQ#{HJjoqtWdqp`>u}cxDlN}fa{;l4dYvKDqp|Ed#%1Ih?Z~W9}bez1246*;^8s&in4isdo}A*Z;9C-@%>}qop1cas% -N4l08(SadGSgT&ViWV1;}c|17RTm5_V`O&IU%KKmnLUX=Q`wCtD_@`*%EphMfjo-wlJ7^fU~AyN&e{) -f6@&qTH(zPE*TGJ_-G$89_=9)2F{wEBZK!-4=i_Z3z{L=_LpTgf=v>AK4${1ml%PtYzLTSV3R``(71f -QxWwXIUQiBuCt%50|;9qgGfS&c})=i%G!i?fm}eB<(Vq+A0*BzlEa_L=!f-DGoM7fueW9|Kp~l^*wU+ -?YE}!R_$1sUiG8V?^TriQ5RL*+?o!vmQLgKmQeSbz7tf?Y=t&O%a#2q(Z#kk6kd`Fj*z5EUQ0eETT+c=&f%r07rI8jYY0u*O6F>$G#dljuIh0U5Jz-=tVk_~gP3tF}X~Yc@OdmM(qY7Eq$ASt?XaP`u -{oG*`m2>LxELobKwernhcYKk-!C*klBmecaM-`Jj3LiQ^&>6(biAh$|}^?UZ(ZUGU(|_;N_hRfmK$64 -DM!HO!n$}&)y>G}2QHH8qda;|Dn -h7Aj(movnW$3L-nYthkPdPnsKm10KvwWgQ7cdBXjKu0y+9z5^;P%i#UQx3&>k+t6T6R@c9SrP6lT(v+ -U1x~Kg0iP(8CCG~W`6Uo{*4Vn=o(6CzbmEC9Qx_&D9_v&6BI`uXVSV=AH1Ww559^Gs0nX3i6I -!_iqbUn_iUWO6FII{vPnTdm{fD?kWqUY?{1+$|eRP@8K@n}8l_ -(;x4=dlnvbrMq)#_fANqEjS{MzXYQ~`O6nxE05fgOt$a$yM#l)W$Bh(l9Mmg@Ic@1 -uJ$}VtZyDV*5pu-gPByfm8T<{~G(g=SR}T;)ApGz^S(dv)S$%$En0`*uNBtUK{e6E<5^6urW{TCc!4# -CAh$K?bC-Dl#|41k=bKq&uN>B?2n~~(cFtaIoq@<{hzn=&@P1dLbOE2Rz0frRo4eZ -3^Q;D7C(arxazX!jUYyw+Ji-ezijgAIsUP_9-oc$6}INpC%T1JXbj=A(&+sk!gu47CM_YN2!&WCZrEM -U36?P-{JMiJ~WmYpXA*cvgVgN%t^b(`osUZUdeBBoot+dvUF!Wm`zS@V=1RVO$ILO@d$qj$G-rzc{`$ -{A^Xn#|N54cz`f~f~N>o7QAn`7eP*gZhn-L=;=KZn&Vz$Ch2#l(s7h$wDk?EEPJV4Ts*#r@UN&hn|0S -o0q7?dkagcl0qG~vr8CmN=@l|?b=3FX68sWJc$sW)xsb5!uHEY?LbUX+xcT$HVYrMtpv<}XJ86eu3x0 -1o2b$S5zF7s7e6XU}e&UM>Ro&cd)U>r0*)q1I3f_2|GMXOkdcNf5FsE#gO|xIOs6%1-Qg`}z!JvQQgu -ahBo)e#_l -@Bh<@~Hsg9c=Qkwb#ieW8rRMv2CbhqkFv53>rNncXe|E^>fjJ`!Qb4arxO+ -jmP`D#kEC^iYXZeY_pZ)K5^f<6}OkAGt)JK;1h9gZbQ*9tU}>A%M**(Io`m=vKKD~S4>XkJ1)s#<$*I -HiPwm0%MGI~Xwsns?yi17r9p~0IMHb62sI8ju;^Nw -C{rZJ6(e(yk-UBNo#9=rVKAex=RLJXD0M$&jr-hX=a_RWdBg*PXMqf;yR#G}dkeRoylBT7IniX#7(M8 -M=m`jIw=|R)`5HsD#XeDlEzT)A8}~v%kzouSkS+p17B3d||H7*ad+n%r&h5m>=?aG)ijX_|hUV -kH*65MdZiNAi%Ina7wNxmJ2$^iJv`Ck{9bW6FZb(0x8PR{Zp%KXNdEk-h50y{`AYIx2Ng5v%jBxNMF8 -xfA;c2D&z6(Z{J&53I_V3KF*HRF}fsZRV)YOrPz*zSSn?j?aHd*`(J*f!b<&o-P_*%QqF0+><%+_=v_ -p(e?{JLBdUS--OYv4O!xh6nk7=R=onNQFuMD!SU(|De@u=?Ee}-h^;CLT!GjT^K -ba0A>Rw+gW`JAx<7E3Yz9o{tRawT9GSk;5>239(939$TR#s4ZkFDN=4=t2 -7A9HM2tQ!0xpwc6&&~cFQ%F77jut1{DIUIiJFg%UV_)xHKRV6caH^aRK#};gD+tk9h)jM_l1vQvA(z; -H}PsV5K0|oLLSG2pJw&df?i*!$9%}u*aN}qu%aO6}XC!Xe3dA$nunDF9gfDR~)Kz9gSSibS!r4Z1&nsOF%2Bw5=5~s{~7Tz;0RAJn~o9g7@LD)Uq6(Y4WA;Af7E>Gxgp{<;hmRVT?71cu8b#E4Ftfw3~9_#aX>YR#MrW!Lv)bpK~jM{D1GpWyYb?b?`aDqZmko -IA*E=XkF9K=+q8~ezsar0uriXi*NskF1H&_WfjSI+b|2}4ILlf`){T^M;@d*A;agGr`1pL+I93~;x8p -#UI&nqUg{Bsk9)aa~D)2a6hvL;~)Od{;PlUgToaUi>oap5te1yR+Sp*v0l6mCJl6+rzh<=znsCzd`J}Kk^uRIen(OL?a0rajdF|Z;Cr#!yPP7X)iLzi{plJ@vD= -Ix-PY@ihjaMkY2V4uD*RH<=upLVT>b%#J}r3(=5i;xN`SzQuU@Yp8jVKJrDdK6ZxLo%pPx%O7;kDvwicHz1cU{6+6(q-|oS}O -<1wNDf1SHaTTBnn-`WQ4gzXlUG0m+8(EAaHw~WNOTappIDyl+xrFbB6g@-vPWX?b?>o!1U9lZy{U*!p -kv{iW`xM9Hog23LR4fh9mLcYcYnQo^^H9Rz0d2)+KEi)`N#f{G_;!f -89_SD2u7f&+7|6QO6x;okfh!Zj6-1Iof=01j=BG`TFde;)kY`ZUM$p`kSLTJY7ea_rnrGD10ZqCVOR{ -~pgdcjx&2Z`zclelMAToYRCMr@gg|VcB0}_t5a@R#9y;gpFH|ySX2ou$Ln?kDa`OWz@^@BK_us0bwv^ -hQvWhR9S9A`b7htv@$F-Hf$K6QH;IZ98(8Bwt}JJs4U8R}l3cdas-J$j16vUxI_VZvcH8>^n~^*lLc>y?WA -eRif#p6_*eahHntmt;(8#P85~{_4&3>d&_~VDhY+l_+FHq&6MA${*z7b!ou&5{at -hP^8r5B{lqX@9J5Xja$xqVz_EDG?m6u6d0vZzrcIWn;7pXyDPh}>eg-uYSP<{2Pv2;*bQJzBtKW-gl( -gc!Jc4cm4Z -*nhabZu-kY-wUIbaG{7cVTR6WpZ;bVqtS-E^v9(8QX5#HuPOz!Kn|9SJ&7c_983+io|V$wNBBv>$)JY -X^FPk$dVyZNwXsVzH@kyNZstD1%~Oxyqx=uhx95-w`4wF6{5)4d`|dwmu7;{WuC@`U~@Gek%-^&oTtf -%6bTR0h|OazSVrUA^HxaX>oy)o@T&<-0Yq(#$6=b}f+iw2n+e^rd`Cl;kH~Gxqe;xR4E9GvuAXjaMnj -+uwC8gg#~{}+x$@60PCvezOlSUx;CS}={RD@z$zO75@^)f&PcPn?A^e=rKI7=)^pEMqr>Q^kNcH)%jI -mENzF`@{Ow;583yUo0x6InP_B_KJBthNAWRyuBFoJs|UEx@glcdL<;V*w9wu0E -@2^aJEQO@GkNXnz7)R3ncqk^HVl{#Jt5YTKbkpjgP)e$MV12E5~QjU?$Sb`l(XwQ}e%%>%>WXLGb88} -LuZ+9xtSv1MAG^nq*yEse8#lS)iQixKcPTUW7nr$sisf`yn1%h*bCnl_^iwV5I^I7>9gWpqMTSp%{yp5;bH??SR~E -7~o_C9Fe2~LsBLgU>aLf0p(b^YRN)c!1BfnoAZTEtN@5GwCN&7v&=x{PW2UWrk;-r%6q(Lk^ -!o#_aJG)n7h2<7<(m<}9i$%k^6uN?C@SV|(9mnx6^KjBO%%?)^^(*CdtMmUl(u`d`qBad8a^#6I_RoU -O6Pk-KPB){Ru{$MyX8tUOtRgxs?bX{MykZAwC%c_iP6{y$d3grh`+%ljRJoX`p>82Iz!AKyVjY`vC?SOp8Iy;)92SSkPsNTnN}EN&zt!0eL> -X`0(!K+w;FC^G|QiXOqkKFHa}ka_kYq4eScB7yxe<_b{~mdb=r(h`LtQ3I0b~P_BDt -BhEsvs@>D6(ZL)qUodwByLvz5@WS3%5@Bbnsn1;Mc*HLQFL=Pe?%9kf>UcX?TDAL2gLg;z{E#LxDKpY -ipkl;09LaAa1C{Zh9ItPq`kLvnno0VJ|jT*GvWN5BE~m6B5h+j>e>(Wfc4#a;)2-cv>6SSsL;r2*zMz -*s4Mn+p6Djxazy!>Jf-Vm)vRE42q8cfy8!|OtSR~x$1${rVkoD;5m+Nx%>es;STC6K1V9 -2Khw=aaUFDu6JG0`u2K_iE)>!zuRKWm0Vml}&LdidtoB(^FabGVLk*-4cIn;4Y{wVZ8m)Q^cx60KKVb -!8mYY^3FAkN`GE=uYdJlvxwtUcK>N`PgOq*F)L#$cqsuYrp%pYuU6-`F_-=K`X7E&r-O<+;x-hp;Crc -9frYTpF^dN<+DVOj(u^apNV_p)^sCP&h$}Kugkpz^}%%*J@Yu;r05!PmKCf{rpuyr@BX&qwUCQ2&0O4&adn2$ZyM_J)6} -oG0lao=<>Yp6_{smA=0|XQR000O8B@~!WWr}a9_W=L^g#`crCjbBdaA|NaUv_0~WN&gWXmo9CHEd~OF -LZKcWp`n0Yh`kCFJy0RE^v93RLO4JFc7`#D|nOuLLX4T2SZj0qYV`}a?_$H1X>y;M5qNMWyC1@?=u1hk(cx3)Yc?UPQ?emVm#y0I`##AXG(F*X!_NTu<<1+zoa>|Le2fwrgYdYE* -UGe~zaB~MehJZ&^xA52`wLJ@0|XQR000O8B@~!WzQyt_>I47)#t{GjDF6TfaA|NaUv_0~WN&gWXmo9CHE -d~OFLZKcWp`n0Yh`kCFJ*LcWo0gKdEHoTZ`(Ey{;pras$Zl|5$blIvU)(5I7NUZDKZb+U}OR#ooyo23 -rNb1G4#Lhj-nLVmhw_yz&0U(W%BO1=N{gXjF+m+S&}SkQ!7XkCi1FOhVffn%GyBEe2-WrHbRTCh*({S -w9Fup!hqsZd)|^T(o#xD4Vvf{laK=+ps;&F*Ji}Lsf)(6XY>I*7w9G3ZA@KhS7Jq)dc@usO;y0lmdY+fTlC8kZ<_h4aT1e -y_%p~Mc7#cdytL6)*{K8|8;nSN({88(D+mkZ4ubAPkTkK{6pXzxjxQ -4V=T5RB9E<9%JHLJE#6y`V+n%_#8)GcSq^?pe9CxXuVENTj;~VkwNKgm--3$Lu54A5ZVdlNTvytfSUlKNx;bhD*s;%r|gv{0Kj -}kUm3eT$Lt3k%^QWq|%UR8B_WU733Wn$%ZI)<_89-1}N)dM-h^++g$b%XTc=wuV(k|>iH5ePk{h*pg} -Gp8^zUc`yfN?>;f~#lmjw|nzd;En6NCeRIBnWwP -p?jXy_j9jlk2OiIc41qoLztXL@z@Uxb&OGZ#|;DSRnh+rc>sBc!jp}KR(ZX^qQZ5j;-Lfpo9v&fPE$YwAuBf=gAq~~!NJMxR5Cm -`;vQD4yDT^QbASW9~lXI$Q11X4G$3Os_g)10%PXRhRTmV3NIZKQ}vD6Fa>og!Vk^qSJ05@$kbbF#);& -o>`)rg=Nqd7pp8IJplBf0!CVJ9bvkM^CJgG#W+&Nh3Bbn{yj! -00$HEs=^C&XZ#Evi?=A&+a8g@ULUXHzEB634NU15fhR(-`WMyFAY;d*L7KYN7+mQ8%3?h2(XU#%QSu^ -d1b8)djqz=TOrFDqjF#+!RnVC)jwUxDJ7SYZBlV0I1o0i4+}N=6_Ro~=XNyU<<^NCR;zhQN{G5e-xso -GEKC>JfdsL(CpQ@||ZVN9+#Qi__{5;&H(B3}!6&j~yU9>|X$vk;FZZv>n{|#+%_9gOq5VB0UqzjwZ{s -?hlf6<^D-M~*9!QM|5q4zgXO9KQH0000803{TdPL>UvM`#8B0HqiJ04@Lk0B~t=FJE?LZe(wAFKBdaY -&C3YVlQ-ZWo36^Y-?q5b1!9da%E*-Y;edmz6kP>CLTVICxA(6#%J|2GON^eB5Cvm)~R3%s(6TUACp$J{eBC8aO&G#8e`GHG -bV?@o$n#u+@YG1~DM`71}HknLPwjnH+crvL(Aj6vpfe+shNw_4BMb69{{0IgVamLqr*+3>gciR3@BHXJiW2@I*~RurbM~l;nL;sGwN?!4g$tqwR6bbFShzkSyEGf -G}%cd=;^b?OCogUAM6At|mL+Q?MMCo>!B0O^H(nQ}e-8lokokH$}vA@M})9`{lhWHo%Q&S%Sxd`yxS7 -jFkhGvmeVlmI(sbh;zDU;WvRRt98bcn5W+cPM)M<`?wNW7=|q%J0`4DiWT@i797pj+395wWHp?NqU?Vq%G37v}&B| -_p@~l~~5(aQMdZ)>mPweDLR@=U35LtB)jUgyA)a-AUvAo`DC_8VnyGf8s32x#Z=$H|V{uyZ_cSh={gC -OqJfL00EXrEmX1Vf_XpS#fF!)p8NinRNJNc;S+oOGUH_e`Ca=Bt*@2SK-3_H@z2KjVxx>V4Ae(%8U#5 -E!Y#)(pv}5ayBayhU4ikN;0kY-D?9YmB)RGMidHM#^Rc#LvZtBkhWTCJ4aa_BFJH9hFomLkUXE$V)uz -%g~eWH4ApXt+n)$Rm+?vc{YefOaPAkCK&R)Ccx5V6Gm$an$U8tJJZ08wUlM3mb(}Ql0)9*TiHa=F!nn --(j!XM3ec%jb~JD{V<`0#fyxv#k|DV!bN9SWw$&2PsPsLVug*|3QUbrc;sPnXVjIWnqR2)oC@!{qx=f -Z7i22egEnDY#EVEn|+Dgrw`_+o)dV=;!r&K5X66UuLH2Vbkjx%ip_P=LIB_uzvZf(2>;QVLpbw$b(cv -an`T+(&MZUqA+p0o7&Kn2HF*Lo(mgQgpF5p6~?E|o9Gni6u?RZz*4R&u+0J(N1d+Lao~!jT=k=pNKF3 -`RcQ+%mY~8H~Sx+}F=Hy#=mE;6NiKXaX9Zb9m$9R@7w -7a!v;iZE4rG|8lc|;7`eSQSL5ie*rNgC*mrN=X#NK9rr}<~U*u4^Pvh_a{d)Ov1pRq_b3I>S4C7_CB; -WlwIg{8B={v43-4MRjo~1+;WX*t62+fq|RGdhCnRWE`R3$seg6>CvNWTgOHqmY9RtPw<79Czqm_4j5t21!F_MalOs; -AkvpJ*p8rnHIy9nf**LC`S#4ufyH?+hq7gV{~-)lu${JMfZ_Lug^UiVhc7>hQ-l`3E=&$tQDJq02!CF -B+Gmfj8leCDKOyL?XS%)X20(r$udwL$v$1X)4_rvo-~3&puJKrHM!JVv=Xdmgck=dvGM)6HI?HUU00l -c3mNZx0a%C&*ujEY?#4dEMwb$|}{`|_lZ$EAAfRquIQb21FoEAsU8bVEKflF%GoTuCT|WU -8lK<0I2*G*h@MM)Z^<1u!701Ue8C!@RhHdYVwgYBcwJyHkW=0>CJP759OHUo@S{@wvD5)~9JB$DedPz -xJ2Mv$g+wtD}L(L9Pdy)^@(%jQUZj-PuJk-tQ3Kt6xXsJAJdjwD>;+qs3>hVH-beXqK&M@(H77nipbE -vneS>kuV7)Y=F`tU${(qoLk@V!pYxIO9KQH0000803{TdP7_~k5Xb@m00jvE04e|g0B~t=FJE? -LZe(wAFKBdaY&C3YVlQ-ZWo36^Y-?q5b1!FQZgXg9E^v93Rl#oCFbuutE7R1(uwy820ZIZ6}s%drft*C6SMOO5#;-p=Qz516E1#)4A$EfgWJuwXuH^~Z>|^1HG|dFGW>Pze!}%__ -;>Fi-3@3?R@%q0&XOjy7E$~q9`yZ(`KEnRRKkacPLObr%MdwiQO#=m=(YhN=alnF)^Wtd2G9yV7_TD3cnXv+w -0zxlg>Rth9P>Dew+$OX=KK_Y5k9Hdd#3$I8k4fQ7{*YGeKR+pAl~;6P<-jQS;VWZa!l9 -hn#dXI>rn5h3(~-UV5kAA_u=G`)8T9eOAz&44~fu&JvKw1IjleXmY)?^xemKH6Mf-UD|ns*3IC`**-O -#{=`deGd69@W|0G>!-;E*^CSlIB5-BMEc%+yoZZ9U2|^bO+e)Qsd9E2dE>bqZbEHc(RXSn?0NL3ZC_A -&C&N+tJfwR%4`&F!G8E-N(NYuQEXnLE~1AP>zqOc#AOVF9>-U2JJ;(PBpBMtBUtcb8c}pwG&sESXNKDR7OiztZN -i0cJ$jmLsFDg-pk1tCtD$dN$i;w3@1FK5TFG_{UYH@6aWAK2mmD%m`?LZ)ae=n0083*000>P -003}la4%nWWo~3|axZCQZecHDZ)9a-E^v9}S4(fxKoGwBS1j(qwrZ>tBo2{$C~Xe`!GWsc&{WaZ9w&= -7yVgEb3gW-Buf&hIMI2gD;X|D4d^7XSc<1w`5qyh^iVHww+j5~0u!`4^qZ<4jQ+x|FM>^aW=buk6gy1 -3y4i;9)-7f)Aw9<^&)m6=vT$#|NSvE~^WDi!l0{KPv6Se{MooR}!u -`nRQH0D@Lla1*5@=(XVuf=AmXj5SW;G>jj -h6Ax*{@|B7Y!F%tWqC4%LWMm>e~PVVceV-NvP^aXkk&iqeeLc+v{lQldfwcW7nUS|%1 -`d~}lZGy7P>yn&E6UB5oL8JZ`8CPi`pse$%F49q}73fKF?=tt-yK#N+g%wr{((tD(t!4kPK4R%4zf+7 -^D4Il>JjSLyo-%xm@{6}i+ZCM-E6;Lo_HWj*kmRjrNC_qtWLeurtUM##!tjXKji6h9+ -&7Onp(BfBeL$< -vCA(At@b;@9%4nE7uIAxjT=Y$0`dZ^M^!x(2e!$b42A)>@)?ln_vw6gQ8jOKr|IoZ(4NvzqFua8_AvM -EE-VL=o8I}IPofYByzc+hv-J8STf^NV%f%cxFJUow0!hNIxJV<;8m#zbMu6Jd>kzc!Jc4cm4Z*nhbWNu+EV{dJ6VRSBVd0mV<4#F@H1 -^b+0mC8_{hzij~K;b40aDWiMNS17`wb|UCM2f{{%h`D|3P(B&EFd2ZSh%P)h>@6aWAK2mmD%m`)X!*;e)n007l3000;O003}la4%nWWo~3|axZCQZecHDZ*pZWaCy -~QZExE+68`RA!FoT)Ms3516Yn*s4`|bL+r4ahXwzbGX<*P2m9UW|9Vz8yi~a9Ae3M8@a_rvjUC%%qiy -~*9dF2eHm}P4;L3EvGtUxqMafA!p)7LA?Nm!VxGNoY_5qkn7<`;k9y;u^XX~A%MMR-Nk3@pPeEig@cz -2z-sc#UCTy`m{iua;qgIY$fhthdAoUXf&(GZNE#7+MAQ>3AIUmKwk^$_l<@1a2`h0TMY)$e;`^K9|$! -KKWeE;NRymj;BO?&-X5aVXsGHfpKz67^hjfq-mU8OfS)5fo4ZN1RqTusPckUBi9TD;JepT2;aUsdvU^ -8#s(kNe0eccB>Me6dWktf<*}cxM -eCXLMdQq2qDT*$+u;D430wXx8h!06PJsSQc?rQ(|08?L_0x5Bu2h34#HeBy#)`je1B3+94H47Gly~}_ -}H!RsHs}0hXf#deTvt_URqBUABG|5&?lUf%C!dlkLgXC-KF4#nBlM>ObBKX>?`Js6eUa1trmy!B@Y?R -3*a$A>ZfLZwJDSgGJ!_;H3@H)B6JM^HIfu;A=eoUKOm6|5GxbNt^7JG69~67c|a?IaGob3s702OG9 -hhKj^3Z2)~>@PCBaOo7F@f=CUVt)M)!aq9||WUX}Km00@)zjvqY;>jTatH5)$rY$AYzJfr7ww{U6W=g -*~A+1muE114-qsf>BED!1HzbMp=w>{%r{!H*W?eek@qDco6a_6?~okj&OnUj8lp1>t5}SjO0vPT|~wG -utORkb`iV}Mksi3D!|hA&4acD;(S;X&Cb -~@MqVaB_hMR+h(QLl<~sOqP)d1}Q0*OU;13=FUs9WAaAO%ieiZc!`HP2Pddhg}5`RM-W@i7v|tYgB(uNe;RBDq|1z&Zc|2z!tm4?OJRUhmK^K-83sz32Xo8K!Jj8_EA*VnA?4oqAlZV#v)lI8~|S9k-^YqrKK -MSVtZ6W<2^I>C~vvl%5Jg1(7*!;u&Y*(M)HU%(m32`uvCOdLVtr!;q3J>xVo;a!B^$4?I1!$0Qxx7w6 -OH{*Kmc71GkOPE#Mcke}mc#urOcVie?M^U$j3;S8)GwSOKr6Kx=ouKb?k0&R!0)p(U6e`4Qb{DIErAj -P@>vLqW9TIHVUU^LF7P5;xvHDTmE_#3|o?+l)jMy#b?C`zHtmCTsph)`fa;I+fAoWL7#P3_gI&#uWm{)CY!_aUKR+WJ`-i}u-#avhTdnbXx8wj2&oZ?_8G~(a=2jr-m&bx1L+Ln2C@R{lTd=9R?s -CDstAqSsruL_tROB(vTQjwU><7b`F+*ID!Mh&#M4O+Y#w0Rq}`8Pqc3M$F2QBqM3m1}|1usWF-tR4yi -c}e`kjNu1+Uoyu}^8X|v>ZrCX;_=_C0dzYVt7Gd{N3GwhPh(`RZ9k2 -%6(T1MjL`QF{vE7r{S2y*PdQD!B9D0lV8t&Zm=oXL+6>nrsCFLVp* -``VN39HCX_FmTS48l9^TogFu~XT`iQjikCPwXGM;ACY@GKrfUxBS*L~Su}1qW15@Q?VRE}sgzn>; -X}qBVv!Z@p*ml!>-v3lf)jWpUsK3W`XP;MS7SaLA76;ILcidhZsd_ut*}`@kblmC^y(V`NIBeh^6HVm -#4i^VI1J?{)MZ<-t4*aQ?RvWbiS{rFqgczl?AU9L2N6H&Gv{|^|2lFK(Yn%_FYz;Yigm@9nmhw^RLey -IFsc)*14o<3GP7v((`|rsZ)~c5Yswb)D^#c986^9|z9O4&jJ0H(K9L&Yr9UZWf81%eMhoq*_~i%kX%^nMEg-+JxBY8b7}78( -SUqmXrlft#o_p%A_&1#6B(iYzxvS4iw4S}0v=hE+yDVg7v2O&?Fnq};;j~Y*8ZibUCodzUuWu}{8^SZ -`l5khwLtw(eb7v61z^v5)vun;+TJqUX6>wATf3edvIU}CLiNv*ET?$7kal;=CQ)~IWCSBYO0uwl{IZp -(t#G#zR4d6*&_;1A;7zGC;U_@&ZNIzFiO9W5`;9Eqe(2mkdwtv=X%Itu2d!x}_7=ZvanSU4Evovhg{L -lPsyI<4jX3+zMU4*ep($VVrou-u4~2zFT_Y73=E6#LdZabmwRDyIku6U|DmNLrFOzt`+G8XJcdng^xe -d&PG}LL&dbwudyhOvxL=0VA8eU3zZ`V@jc%D!wGADZ9aQCTdp8s~CUdAhFs*;IU&s3zxUj)v3675a0G -Mo35hF8yItmB*rcE0+qRa-c^sE%4mDpL<{xtjolbC`lQc47H}$naM*BIh_iB{=4vWoZ -73ld~zUMg!#8f_P!QxysOoT@RiiRUrL*|)^sdsH{LCl{^xO{Q0yEqDz$e6&E{a}h#(VbWff| -FcGT3wL~Q(ZG1J-1eGf5H2Cny(jj;IxP)h>@6aWAK2mmD%m`=006_E000~S003}la4%nWWo~ -3|axZCQZecHJWNu+(VRT_GaCwzoThC>=QGK6Z(FzYkBnaDgI}#5F5LXc%BE;LMeA%JN&=buxkO<|!<8 -!>u*S*%HAM#0`ESLB3URADLE_+vb|H->w{>S?t-reuNUGH{j|NZg1|FVzohkg6)@811teOTYy$2H&e- -@W_z-FM&q@E7mr2mAABAMLBJzWL^V*7rZW|L(&#-@NeL-)q?P!Bzj_(R0gG -CTDj&m(s=8J4>p`fZuiKWk5@>)oA)bcHPs}Dfd-G$lA=m`Sv@gd(#UR}F>gsL8 -R{wteuMAphPuX2*kW9}5v&oArL0V_b5-aH4-Shgphr;wT0=t|-U!dPiKSQ%iFZ2Mq+&AV!PnYoe{(Jk -CeK4I4t_W3WY@Dw)58e2Q?2m_H|99v3iFq?+{&IZlf7+AQum8}k4Y$5|0=VvTYsbbrz`6!6bU#67V-U -{FaD~9ea@&q0qr0$H-^CqzTgvj2vj^*Moc;c*|NQR#hxZ?T_07lM{bqfdbg$p;&VT;yH3L4Xj;o%lfv -b_LiK~^XovMD{H`h;9zOR4W*MF)8u12mVu4eAJaBJn(#;wM!ovVYZldFrXH}xDYt~#zVzadapA#ycwm -H7>YtITg`TxEU(^Bb7o!2Aa0H{4W>kMS*^s*YPdSDD|){6^+CGQW}ejg=%Bncv9#W@aHX3x&t1@E8@= -$->VQQ$G7qLHXh%`_8TFAPYN?g&oK;xaYw=v#BqGduCH#2KUUSzObn;lO;J>b0%xfWJykzNAUt(~ibtCOpXt29QHt#@VXUDX4gKMDUy_)o%r68^LBpN0P{{Ab}m3;$X8&%%Ee{5!|5^CY!haV2i|}8B|04Vs -;lBv~Mffkme-ZwR@Lw;s&(%p??0*eXk5W&c?`-UPmb&1nibuwy%6L>6k1FF)Wjv~kN0sqtG9FFFqse$ -Q8ILC8(PTWDj7JmxoABR+|0eu5;lBz0P55uZe-r*q_&4F-gntwMP53wA--Le?{!RGjePDlG<8%Gm_4& -Df;jib$%V+KEt?qh}dX;+nylyf-naocn^OMQ^WHLXQ%ugoslga#KGC!HjPbTw|$^2w8Kbg!=Ci9cY{A -4menaocn^OMQ^WHLXQ%ugoslga#KGC!HjPbTw|$^2w8Kbg!=Ci9cY{A4menaocn^OMQ^WHLXQ%ugosl -ga#KGC!HjPbTw|$^2w8Kbg!=Ci9cY{A4menaof2oS#bmtj_bnWrGdMCO9Q{B?KizCD11KgOK&MMX#uO -Mbj%x30;3OlrTT%{y|vU$1Q|;DZwehD?#NkRUW*1^&n&=6hX>+SPw!|f+?XZVJKlL^rctaLRhLFOXVR -;&P`AqTTy?aNS7jAQwfT6$;kC8NPBg9h1V;BUJ;d$l#sQLqJ*l1rhS-R(UmatCsVIjdWFiPsXUs>!xW -gA61ox;m?K%+%E -PHVoXW$gJeK%+%EPHVoXW$gJe$-a|znzYlTo${W#+xCKXY&T&rVYQ2S*#xHquY{n4 -sDz}1tc0S3s)VKlQ$kn5P{LHgQo=0+r}A(r52x~QDi5dfa4HX{@^C5-r}A(r52x~QDi5dfa4HX{@^C5 --uk!FJ53lm@Di5#n@G1|l^6)ATuk!FJ53lm@Di5#n@G1|l^6)B;pz;VRkD&4hDvzM@2r7@D@(3!Apz; -VRkD&4hDvzM@2r7@D@(3!AsPc#^kErr^*}~s{HNKncH{ZSg@bR1Rt^M%BXK~nH|Lec~)j$8k-)Ke#qW -C?hGa)}AP}V{|c>q_T==6ykjho~8ksl-JMwbJbvx=?&GAGAeNbb2LN2P#VncP6;jGwE6%n3Hv1ertIu -fxr@>qK_I?HIQMZpXMCa7)gU18&E-9dJ9wjVOEP7`Fp%$(uqdfb)#o1Gi_~9=JW@_Q35Kw+C*|xIJ)l -N{#ct?HM-|&9S06WcX}B5;*{OVB7%!0|P_~p9_u21~R9rg#q$p@&cK&I3lt*VqkIvnbWXNvdJbFCgW* -{3@}1ekwq0DDoJ&V5LIGPC5S4qs1k-a@en6SFtG#^Bq&*Q2}3MNbP2d6hYlG*PEzO+aA(F1YvLrqFav -jH+!?qfT`dE5X51OLGvm&{of&rqZqD6u8Mrwq#ueZ$jJp6gXOTK&k-EaT3vhFCsY7z9V+-pFG`6tD7H -Di?jXk<{xypHZ6#y#(tN>UUAW~6XWsOBDs;exjhKHo_L(=?6Skhd`sgF@IB(w6Cj*=S$Qg)k -LR6DQH6f}gdB0GI$)bW&;1)kS3uDQ-$!=jR7c;*wmW#)7;n`XI>?{a&@iV{h%rBPrg1i^wM)IP&S+H+ -e#*&nbTi%jmj9cEC(hIU=5}#X24xfjoBB!F%{Vo_!yW(*bA7JeRTl)ZOAK2OlSo^@%KET=sw)O$mK5#OBxB-yQA_!R0hPyEtx4cQ&BGf9f*BjyWM)r -Cmyxz!OZ-mzy+3St)dLw(i5ngX(uQ$T$jqLSCc)gLm-UzQZvLZ()a%4r0P~^yp9O3mw_Ie}qJF+y%J#X^1$0CGr6*u>yBx+;E0tWh521tbEIgTe8<1D{k5NmN+gUHoj%UEgOppK5}FqIW~x@F -+jM^k&n-a4Y=8(jt#gueLptf=7S6(9PP-Cc7&rH+0l-0v?Dv(5$9J#W84PZ#<&f*+5e94za#tK(SV!h -hS-6-Gwu%DopE>ImV-SyaCgRyc_((p-GQ5(_y{LHcE;U-n~%nc!-E`{96?S@&L9^iSCAW%4dl+`0W#Y -vtTOhLOymi{^6VNX1j{Gc#0kOjNi7lcPULwfV%~{7??lWyk>{O=c_;F`6EW{Zo_8YVoyhY}#Jm%E-ie -rZ;*yhIfSc!?hL@eb76VMJ_W6Krf^8=J=K=%T> -n2r|oLKqi0WK}Xd0>m^Bcbalp&PnlO4`5ZQMWp)#DJ{CC_nglXDrOc)`!}R7knS21|MH0;G%ng~XPre -Vi@WWkR?|dS$Q?Pt1Jn3Si@+xeU3hYT?dxDM1tFrpw%)G1e_A<`QyDFbNR}Ey|eyiAitE?v#lUL=rp< --@my#L*B%f@bbJI5tmjeX<>SvB5aZP;OLe0*Pn_t<#(*5D>K-l=TZscgLW*t!QfFc|>ZZE5`sPh;1jL -G2s+5N&jjX(8D#p*Nn;+v{XBo?Y7l5}woA4J5p1YWD(}ZfTfcjAs(VOk!7O@)4{#5|zQMns^^urX+?vBnxS)>vZ=8f)y98{BeZjWuYjv7t6-tPQ>z8f$}b!+sm@mK!wISYr(uYiu434pe9J*rA -%8RkK4i`!$(-B-mLsJ5;l?YIdk*XVvUb%^vucP|eOd*r9`REMAAQbyl_xW$UbL9dmSN^Vsobud{jVP` -1wIvBNxe-a^3fg^$=)CW9S53$^3W)6Q12;{{abRdmO4xi{{lK_-ny2R5wpy0~Kj+>IrOS4!QuZ>UvgM -eZGBc0@W>b)8Lb$MUVSB4c@ZbynmKi`>~FcPMgaMaB$$4OY!@9e{&XbGRee!Kyi+nuC>XgoALfvJLE+ -4pz1i(FrTtz$@yJ3X?0yByR}t8mxl@IyhJdv0AL&CXCW#g}}MR*JOp5c;h~K+i=1tP1chMJ(+wo=xpeOo#BZcsL8uT6OZv^&lE3Gug -M!c6C*!)Cua_D$?tS0-sw(Oh>3T)lQ&8xMt<_hPaIr2dBb60s$1+5Eu59RILc;VIqxs^EsFbQ{YAp3==~e?#r>&N -As>r}HhL$Tur8b|)T@qZ0M`3LTZBi{HXN`G{qQBbJ>8tU^7Vm(V?bCM75DuRn!QvR>?Z1zq|{=tqL)J -c4t)1Yr4!>m>r~Ef`aRll)n4?NVYoJMeUlEb@rw@k9C<=x0bj1N}%8kv~(83q14hx(8iCIPpDzkR(_W -=kf=M9>j{?m>v`$m@Wi^jgnr|Cemjia;YSdm#O@u>$Q8#ySp^| -na{Rl)v+s(;a~9}H5xz=B|ApmW&JA`c{Xm|j3mxAxDDvHcauyxClnBP-$Lx~z^DS0U!m}{(?^5WHLoE -`3_=-#9WtYR7TUTY=5hUT79M1k0u_$56*v#0a(2sBGngogBJ?$=eyc%#e2`$8%TGx2%!r@izE}4$qO= -l7YZoqFzq9vgZCb6P+-j6%0(m(8mHK89DW|us^cw@WEFT_SlEHhR=yX5}+iAr!?^sqy2ot;ky_r?vL` -5kuhcWFXDc4NqQJH{){Qeq^>36gX}V)@=8Ek#eJ$M;SoaD9djx?F$U2q%k}$9Gf?T_Spdf}0{%QFG*QUWi}*4QILl<5xmCG%o -A;Fr{k<$z!2m*s$8k}sA6ez8Bx0lz$V%K^XSUn~dwl7O*1@Jj~9^1v@C7|R2{REOCH98J}=Sd6@XtdF;)QnAov6DONzz{z%S453cw!(Kc1r{&(R9N9|V5@{wVk(@JGQPfj -kN>p%sBY3jPTEVt-ZyezAuOc4>*g{Hj>m#Ow -!~ju_)gLiKW$~;mo$%+fnV}GRtA1a^jI1ACDUVN;Fna71-rT=*JEYimw9}_4_uP%u?p}@y2mQOFZ0(b -z%L0Os{p^)`2{<_ir_E6Uj%;vewo)6=JzFjEOu6QDflb!OFGD^z%ThA*mK#X;IF_he#NT5Uj=^!ep$b -)3jE^FuLk^0@HgOZg1-U3_%rM5`R`Kh+i+j`st-1NoP1Pi*KuFTJHbwnlbs-ktso~`LAwsQncy_YP5e -e|675p(8}N6*-+{ji{to=&=dTX@vQAtb_`Bfmz~2Rb2Y$(KSsnPrZ(R6p+>-CctpWT)@DJdZM42^!U) -HB<0RIsD1Ng-cUjz7u;2*$01pfg3DflPwPr*NdU)ED=0{;~JSZ6I+XRQhRvR+sd_~muZs3>ol6B&z?=^~iu)lFHxqd -+oqW_=1*T~=hb7!5nOZkrDc@?rt{w&}?p3mTY06&i3wM)&INuU>@SJ0c#4fHPb0XpZ{jDq_Xx$p6pb} -9IeKJ1dygGNO^Rr;yuhy50hSGc>(LNB0Kp*PS?=pA&$GeKWMzYm@LmR$$_CiolhH^JY4zX|>Z{7vvT; -Fk|+4tz*+G{N71zX|>Z{3iGf`1PZb2K*-Y4fsv)8}OUpH{dtHZ@_PY-+t>5L+}saAA)}X{}B8G_=n&hz&`~40RAcXC-6& -Z-k88Y1^)#8DflPwPr*NdUp}llCh$+eKY@P={t5g`@GszBf`0-35<9kF4<+YtoTNiPZt3SnKk~ub*Ks -UUGE3*#|Jh{~`UIW%LY%9gS*}0pL@yx>9_ih?RC)!S=NBYu>{71pxW3EvNXeA&hF!u7BlrN*XoxwY=|@51sexb{+U7E;lFOPl7)Ie-iu&_><=UHFuQur$1tIG8id{yDAf-eby&J}!B;j4nLDtuM&RfVq#zN+w5!I#8X=L- -By@HgO>;OpFgzX|>Z{7vvT;BSJz0e=(x4fvbjKOrENm-LSq@yzp`mGl0^3cnxO^#FPldICKQ{nbwudI -Q~r-a#KipP(}L`CSztel*v|s{S;T%8*v}&N -v%r29v7ZI@vxxmHu%AWjXMz1JVm}M)XA%2ZU_Xo4&jR~d#C{go&yody27a-tr2s$g!|b|&-i1CupF&6 -fVuuUta1lFPV26v?;Q~8c#10qO;Zg->1s?HF%8BK+B6hXFt`@PY1$MQF{VcGbMeJvR{VZZX3+!hR`&k -;srOCK7$jt<&0jCL015OjU8RTYy-+yA=}~qzrgzExBSC7HN^hVuzun<_C)dBy#=O!i=+Y0yrE@%? -Av1E9tS7HTv%ci|v##GI%K`buyOb2PGhS1k$F0pz!QIC3ExYbOmr&Xh-?Gaj^!w1g^yATwmwr6@@zT% -nqjt$5)7>4Mo#W;1g=k_~N8fl4e(QViBZlYT4{eyI!D}S%b0~Ky=XG9Z?fod1Hr%HnYD2=!lQvW|X!! -5h@OM8to$Y{n99YXC&<+~~yOcxto_i3_h9wQJLvDk_2yMc-d5HRp=OI*&Qlgs?Wpf32Epi3G4I1|x05 -`~s(@i*H5Ap`{iWrZmT_N;YPjhjPLq1M}y!?6##+K6t+=VEXD@hp0h1XD538E!Mx+8RSuQigrlNW;a0${@*?c27{My%U|tn^RXHQ;L5OIOd%O-rZcZCK -8YJrKYPflm*ZrHYPJ_IszYq_t$=MG=LPODpBP(yj*{LX@MFr9qdvLMt0XTFV8*B^p=L_0^;$(g)R-RL=8UH!U6B2l!LTiXLmb^sdta8f@ -gzhi95$TKT3g%=u)T0{JIl}|0wkr6~7pI^P@hyoYDXW^{7>Mf`1>S`~oon&g=2y=h)3%N>somEHvEG0 -Jy#uZK!By+F)o{+HgkDH*IKWFzu@2=8{~2Q^Fsm4DIXu^&r+~6A~JxHk_MlVu}lpRq%o;_`_fb`io~K -{-e~J2i!a;+~+ROF6G$pO(y3|VDWa1ROMa0n{dkBJNvqGxw -$+OW_d3KyYpQ3AI%p`bzdKm@Imv%~hYiLR2wux-N8-6Tm(E=@E@RFs4J;j(ySn-I|;B0=1$%gT8Msw` -8gI}sF*QhxFJOh9oW-Qvy&%4Vj+;p2}|x`OZX`0%nc;O2GdFYfbvG)W}Di=|zv$uQ`yNoX|A;@+jim^ -!OR;WX)F6DAsDJTM76lk_U&g*?h1HJqn*hy(e^AUO>>w1^4I0j%UA2S#lx;6|nENwVLJ9VHCd$>!zc}9aoV{bx1L(>MJs-h)`SKOu7hJXg`%J2(WX1Mc8 --92LSsNpSFGl*XK_)5N%Xn!2ix~(g3AAI_}LlzGp`{1S|pzHQ$C>hyPpp0m(0J8OrY*%m5ME(U2?43Cg2xC77ZQAue)s0(9j^7*iRhRqm(?b*YWzY(f -1(q(;&0+v}dZpqs-Fq+$4RJa^Sx;bR}Zn1a}%ViX1}RWzmLu8eEfxr{o{`W}0yJ+=+UWNgFa6SnLZ!y -Q;X(pK=H!>G<*GtY7?qT#OzhhmB&Fwek=`{J0Z>6seNq-{qz)H -|Mf5a<(Gf^@4x&<{U+0OmUp{cfA9ytkRRai+KWAmfK!+Q>qkPUaAMO0z2{eqSgA@xh{*P~g6*YhkCj$?Lns(B5KD=j+D_S(`r|ue8$!@_bAb5cJ-_#6oY`!a{F -HxCfq+(82l@z7IXt1UIdq~%7Oy1FE{ratPx`KYq-hD2l9r*M{{fd#1w~`g=g@sb*(k;SC~vB9&j( -MxLY+tvC2>20Sf0gkNc^Pw9i#on=Kbqbw5H_1ZYtOS0gSq(+~I5$c4tIvU+o%#!j?l|3au58+-emOh9 -PNE0Q7q*5J_R{~oAN5|FDTdbB>&s7Ma;uP3+4t6*+WJtJ)E6*~XWf1U999;WOyR=1$W5fkw39B_tuRO -iv;K6FjLl%iAd-3uDl{)AeYqnseI<;d#qe@t<;Ois&WiFDawBx;Q=v%bKC7nQPDOaq0e6WtAn4@10sn -F79>&LC2rL&RLGiz9sevY+DE}-9>wy4q@+AJ2tCqf07mt^vO&$uOSyII`GEv(nmnR&Tglv5Bt)Z|X80 -j~>(!5C_Q$}58@$!KL>J~87J6-*bR?Pbi%kWIh0v;Wt&7ML+l3KU{okc8jj0C -Ur$f-<@7BVBZ|P=I%lEK5nkgB+}xoIJU=rc=hrXP=H-QA;U(W+EzVO&mC{U=$5_Vu^&K6WPAcSr?Rd` -)j`Rt#%%DLlynIGt7g3As!t1W!4rCr5K}E?-jfWGLA;n&E2Zq~UguklAql?MqktA$>fHP7f3~dd2Qr2 -}?OW}~No@h|iEJ*|a+{SysO4mRPmu*yhEjiuTmn`KOmSgL&4vI#e8ooex@q2k2!cbRax3_>%%p6V#@M -$~8lD(SP%@6c#`LDA*Hd582uaMr=R@5D?-K2`6gX$CYA820(?VoFo;~%26nEV4!O9KQH0000803{TdP -AHI~LKgr402KfL03ZMW0B~t=FJE?LZe(wAFKJ|MVJ~oDV{2h&WnW}rbYU)Vd5e!POD!tS%+HIDSFlx3 -H`23E=i&kYP)h>@6aWAK2mmD%m`(x%+*C4@005K*0{|TW003}la4%nWWo~3|axZCQZecHVbaON|WMOn -+E^v8#y;qYR*Olk{u1|qh&rDO(uF@Q*8BI^-$;^m;nEP`3-sp(#z8Z>Nv?3`xcWwY>;SB*0wm^cggfA -3)Re=$r0~iuP7>We>f&y5MQb*_`+(bdb{{7dg0|i2i+P};^nQQH}_S$Q^zg+fD&p!9svZtSW;i+Zye; -@zp>o33f`0LL-@%Rfb{BqedPrdZitB=3_)RW78^vh+hzx?tGul@aVPrmf{-#+>H>yLluFPHu7xfj2`? -6s#}U%dONm!5d}$>&~r<_AB0{pr7LFZ=#?zVqGhe)k`X|07%WKOTSOl^QJjr^kO(|H#v?zWm~{|LdRr -&$Rh>^#$Mg&Ub$J!=F9%>TA!v{L&A9xaL%n$n>``-7zv#kE#t53c -D(^p?w_AiTnJl6M%zQ0-aSIJ*}?{EJ6cVzyK&EJXnJ2ihd%->D(cgy_UHh*`_-yU*w(^Kwjj-~Ftvq5YkJ!p1w( -^LrJYp-4*vcce@`$ZGVk?jMzC8YK7L>NY$|LDBS0s@wh;2b)3sPIqumw$9(6R+>ThOruRvw9!M`GoX* -!q#!`jJ?9q_%#f_vP_F{_1a*{q(OMXL0G9u_~r;`CwhlPqpLEPjf`&4u$r=-G;5ZO==-Pt97L>N2G -6fwgkB*f`$I7E)<j+IBp%A;fD(XsOASb21;JUUh$*#i%A-f9C){_M<%tk!3J|(k0CA%-9e`JC6DOrV=7zD_zZ^^Bc^3bNmEDmj&^bAt -`&i6Gad64z{`&b?Vi1+uiJjnX}11t|A#QO(X9%6|19}1YwY}~7v?S?Sp{lk1>P|f@AvOMVK{Ua<7k= -^@8Ssrxs{xOyZ1-*Zq@;4icMzTk@op)X_K()yj!`km5x=hAxT(t798dgszQm(t3yv@uO- -y>e+|n$kLw()#1l#(1Um$E9@@m6bc~n* -&QD3mk4^{}Uf{FTqQGPHWz)C3U3-tZUBgl2D04LW7a@{7t$u)vpcL;EDjUd-u0-RhU$aPeJlWPRIz9 -qoPHHKXG3UG3bA=iBZoLpnb^?(2;*BEj=B*4ivhFsqj;N%)Zu15ukTp27Ry@G`(hUAaS!<^(}NdBY%C -;22u{<+_XrMfI(lmyW~_hSJN^jQH;&rJC1w80--7GT|Kl!UnF -gU!a$Pa!f?zG|R9+HC}iaTvMvqxs|Lvg1KR~wNQ{7~F!!_`LQ1V0pa+Hhu%gy4tbP8&|H4ai -m8X~W620lA7hZ8*6$Ay;vyE?XEiAy;vyO()kT3$?ZYuib;9i;n_1g&i+-FA@f)hz;oxBW=GZQFU< -c98DXHwA>sdsSRbI|%uswE~=w+o7s{EZ(9WM0!=6Qzz8etKy0}L9`#eA;7g@ozQ|^eVB-0%#!wuSvo= -ZSH-(@obWpkzW9Mo5dPJ}0zwV{NI%+nj!tj~SH;FVq1s+OEx=XV4rDB5ngtnuB;MBLOrs3-R^;K5r%{ -G_E8f;+QKKw~?vv#LP;W(anX9)Mq6D$1%z1?@2=gQHt(o(!S*Wm|h~Z>Gm>)596uFdYl!u!7$!Y;Xn4 -d`ag6!%?X9T#A3yIZ_&IxcKS004C)J}O;rN1t;m8lZq#)GtkJk%uw -H&#=k2ew&PBBRHeJlbgoC`sQ?_=?-h0_R<|FAaRPdh?LsgKFn7m8)MHM78A;3wff`r67xD; -SiK|=awRzfjyfmc=tur)76F7V2L09*57fR;StG#8HAXJ*%E -Qhp1DHmy048?X!Wh}YD;oqj$s=2MWs?9Wd1MQ(Y!=`ok8I(Utpc3nkuAKkO@Na;vV~W62yl`|w(!a>0 -Z#JB7G4<@;N*&I;gz=pIJqKQ_|YB#POiunUfCzW$rahcD+dHPxguM5<&Xd;S7ZyXyeq)T71_coM+G>! -A{Th&xBw?t40Zy(+1zs5!;N+Tw&$>G%f<@Nu%A^3>(#FWzeY8_RiKibF;=T`AyQ}> -IY`_vDYxmJ|0il+BA|49KyHC~%K*vVhU+O9g=J{RKhr7yx+}%}ie=c_yBX@Uood8!^kh{Ao&dufS;yh -HAXQdoM7JVRG|FM(@QNJqX=sR8%5F~wdLIC+2sWkL9hX5lFGzXN&1+H~bn^=-hZRU09Ck^1}XxBxe+8mYfex90_j8(~<}>=o7|Ej!sP -X`!;Jyjs?wB!8M@c>mh}BITj)cuhXs%{ffky=FQk?FNX^DH3#_9up7@Tn1{AcCheIj|&JL!cR{K2#)p -BlLG8)yQCc)>$SB4?6kWCnY(N21VCo&nkOAs1(CA*RH|SnRJc!13kY`esVD?_xlhjsfYqE80MVZl5Jd -l}h#u*=PcI07=%uE0Ao@!JLZI;Jgn*y{nHQHJulMPcfS`y^FAE5Y_>?`W1nIqN;=r_v4X4iHIa7$k>$L!iJ0nYzodfBy60XCpZ -kfFNvmH->jB?ua>?G@l!ID`w=_6cyhMWk@;fB@IRAwzZTkN_uFWT>vaE5OMW8LDeX1vt4PLv>9W3zwl -vkfFLJt%S=^B^4^Mc(tmR|EM5dF&#H)EkTw`4Wa7g$lWZq1X-?Y`V6;VtLpT6Ge}uHw99fORdCI})k} -j+e=E+s3jX1@bml2iTh})V2%^2dNk9stf_9bDfkAk_5hZwd&ixV -}w5sPNaf3kVvyzC%FJ$n~89f)BsGOF*dp*LMpD8oNF!AZYCR9sxmP*Xedrq{KcuB_L?*voQfdW1pQC5 -cKicxPYLK&&~)4`uOaufS`}h&It(m`0TuZppVZk2nhQ4?4p36kIya%2>SSJLO{^RXOjYgK0XtBKo0G* -%L0NvK4Wl`B9(T1xd2$u3IVX7egUwL0Rga&K>@IjApx+DVF9p@5dpA|l>%TNs|3J4RttcAtPueFSStY -bu}%Q&W4!>_2lFhcTdSL5F3&FGyW;w-Np6e$C*k(CJhyJ=a9M_>w(V@RCp2l_PQ*`LBi;Hm``V<}d>*8D7T%Htjd9 -I67acc-`#1vC(ZirWNQ*2VKM!X?j&8Dq2viUox?CD!TM*)J*965f;`r)x7jDP|Sjl<3CIDoT;axhameaMcnSoSWik3s)_Xy16Niw -s6%FIh&i}XkE@GMb74?I9ivpNs+U;DUR0VY*OTGZi=IIIhzzYo15ZBUCt&&&gQ1LQJ1qxk+ZoeZq(&$ -QsiuIiW_w~n-n>lo8m@Y&L&09=BBt&m$ONcv$-j5)a7hatZUk!rcMRe+N#axAyD32<^na^={b4he8_MUv#!y8@hCksY~pRDhE!(jvEx3vhBp -KIGO(0Zy)o4Y(Rf2)I5MNnsA|S+MpNle*Ao|ab2?(Lt=b{r#m-_sKfRK^;TvUVk8=s#N0DX)JfIdzOfIh|rKp$rWKp$rXKp* -D>Kp*D?Kpz(bKpz(cKp&R`KpztVppQub(8rVj=;N{g=!1=F18XLKw_HHb$M2+V#nQ>&^$Q64_}zejpp -V}T3JCi6owTD^Ncp>A0d}9GhFdS$!0e6B59lkp1y&6=4Wr>wKn=I2Q^O^68eKaDrr}mNG)nhdE>vt(c -B`jmpW7QPvimYMBk3Z^rffa;Ic>Ir^uIiGn}*UvGV%6GgMxIw9x*7`#O>7vx!u|dJE^7VCZ4tW?DkTv -$ZfxtTA{YwJj7ORXHc}d?m3lvjtfoOvD^O3CD_}E-2tnex@#J4qp^nl_IATPr|F*4bUT=3?wZ`~rRLT -mwR87fu6Nun-8OEnr`N%v<~#eh-q}AXumkv}g&E*B3vVLx~`2CJ6-qAT^spzx^6E$mr -dyuZd*HKLhc;gd*`4i3Ogj-Irxr+Zu7bVNXc6DZjYl*%T!*Tu?WtB5SNl_yKwbKH);8G_+Ehfm)*JSot9klVQXf-{C$ffhq8Yv7;Wv#K=oJrdvk+eK@NPh9{(El9nl+rX8ctetk%y1k7jRY -EZR47?oFd)0UFLje=6q-7Hr~vf@66oBnwj&Rne&~Q^PQRVotg8US?=E1ja_8UcV^CaW?j3HXXboo)^* -QuzBB7O`MB^iE8KTEmzFt~mN}P}RZh+>HJJC=kU4j5FL!P)cWy85vsE&8ZZCIkFL!P)cWy6tZZCIkFL -!P)cb+YGo-KEdDR+)3Pi>o-JExO7r;|IUlRKxAH{2ec4clhsjG2z@y>m?V0A>UgJ?>T)mos67c$ -lZ#8yzOM|{6g;hLhib6dD}Yp+{IM6+ms@A8|vn696WdX7UjF&qfl)K(qx6l4o*ZGcapZ)f(^Bvtt>V~w -Xy?0)9?xpM8OV_!DZelx_UFR0MZh2AHxrJ`(e#^OquG_4t>ryG*hOJ`Vh7+^v`gEPScb&O+Teiw~-Tv -iW*FEYwQ|>xb?mAQMx+cEsn)t43zPqmZ?mE-yy5_sz -eqkYvQ}kyt3RmX~S -LX}2O095BT;ZCy!ZmS)+uFV8xOa95uENc_DRP^1D%=#7!X<(VmnbW`?tNT+F1prl6kQu86ou25+mfYl -b-r+Wu@t3~v#ZaAtIvho!MAXk*V2WoC40RW*WI~rTA;mOC?{Z2SF -{ZS^nPZbM}kQI#$sUb@~`*^&2=R4K%wP3bE$Yel7-pjT!#z$@L%yfSm|<64d~bLw -&v>`E*_zcaD!&LycOZqumJH8iE0hE_VmES+JNt}!ZIV^lihE#364(lth$~7$&<~7`zTqDHoaE+0|J5&94E}s|ZcIr-TJ*eD<2^EIm?o6$t|L -O#d*+P?kXB)8Q-~l)L!l?T26UyG!-rPd)K>)<~plY*InbLhgOAc!lFLsKcYU@vP6BD1AF((Aw^bIby0 -zsQS1Vgx=5ZSkwx!~LSq-9M6ufeA&On5H;P@VH+rxSjJe8-A$RJDT@zgw`CoJsyz3(Wi>{fDViz1ov -EQjMab2gn$o*mxxnE2o|16gdsEb@hOs6SbI=$hPq7KlQq73M?q8w;k -5iE4W8ASzE^4`X4pFO -NBlD=`t-j>~jJn9bSIbr8mOE;%F7m(VD__gyf9fK)A85-B5JxSSPN|DrMQ*tRWa=V!RoiQ*-RG)%+n; -dKUK|VlM=f%%+ICZA>LULVUbXF(t4!@B_R@CQ=cw&B+iiRM?fB2`M9z11{Cjm`+x3b%Zu(7Kbec}89e4g?UE~a=*#7Zi -lx>qQ0FM_hrc?ggfxzpNl=L3UoPNzlS83c_rzR|Q}W`s)gUE`F*Y=;G%Jk{-1NbI`Bq#^;!XeszT`#Z5D4q(4h8r8)J@Eh2h+Ofa$t2))uT>f61r9$OhVTqg$g9pw4z`Zx -}LgGU>f@LnKuE5?hMMCRsJ<^!&=K=T~|MhWw*!rc)E -QmwSPY2(Rjq2iQpA2?aP|WUCUK`uVV1ECE-~4Ls|caTJcTO0!*KsP!tsOiP|9cFT8qO36clrh7<)Aeqy8mpE;%kIy|KW8zyU&#tA1^wMt`;hO -270+&lTK5@_gxlAxhav&KCps$`ofSRf7S;UF;8C@ -QRI*Nz&hsX3#%0Q6F;z&dHTW{MN7y8joyWIiu}1BSkOFuVS^%nq)J42Z&Kvt9Z}w!6?u6_l=oIeUfvP -qy-ksqcSL#bP~_zuQQo^0d3i^a_oyN-?}+k#OOcm%OnK`eHvXK}GN!!s92mtfqkG1jVvX(LBttZ>~E@+uh-eZcqyc5cMT#=V|LV2H6=OEf-%*v1NnuRu}J&j4j!Zyr?eT5AKx>%3EE$KRC8*Qr_y~{n&N -cq`cL|`{QHFCgrUz-k%^_HYsm)@%|9mvPpTXi}&ZqmQBiAUA*rhmQBiAUA#X{wro<~>f-%@vSo|%Ru} -Kjlr3A7x4L+LtZdn$yw%0~lV!^m<*hE>A1+(AC~tM~zS~^3C~tM~Zd=N_i1Jn!?{=oFizsh(@qWOgMB -aV+lKO8ymRlm@K7C33w?AmMM6P}MlKO9d)@+IV`}8IC-)c)tkC!d6Ddyv!Tvmkn1jgyP1w(aF5Ws7% -De>Q*@%#1B{&d(f)QJJy6J@ls%CH33~w7z?Zb0S$K)#bXqF{3h -Ll~kAO51B19R!McaZimdM%&8J}xo(rpx`--Km+NNiM`ccxsLOThRq7(DL|v}iEwe77O4Q~0gJ#Q|@>Z -AYCM(xPl()KEe==>EQ{L)w-OicOgL%(?x_UxgZr97Z3wckd%XJ5b)J2f@gu2|WmvZAYcH69rC~tMSZcCB6i1JpK>()QjM -U=OO%x)23T|{|n$n19BjLL%Y){xn4z8P_@`}Bl{%x?e9s4OUN4VjlX*tev-)x|Gywr@#!tBYUac;Axp -Ru{j-3BM)ftuB6vLw-xjTSMk0&iO4VZ}s0x9Q9jL-s-=XI_;P8R{!k}{4FcWTm83N^Hvv8-s->I2Ay> -g<*okP9~)d&l(+hCw@qhVM0um8lgo&D58M$80*N*yQ=(jy^3kl$+s2pQ}-(3r@o_z-?2{-zhl25e#ZetRP;ebRJ8cM%Fj8hn4z3JrHG$8r -ih<Wa<{8Vu}m7h3SrM^xx891dDjY%leQ)_lb@)z$Gp~*+Fp~NslCwBlgs7s%!n)B>c-eYgh7-27AAlL79 -u2?9JCPAuqKBrM0hkgY#}CmOpaLCe$;n%9`&8sqrNkL)OU6t^_|6|zO#JPcUF)1PG-j@-?otY+-o8A` -HqFu=ROOm&;1rsp9d_YJ`Y+*eIBx~{iyHkJnB2MM}24hsPF7P>N|@^eP{Wo@2np2oy_)4+3&>ktJ;rS -2z^f3@5Cgm+Ls5&crxy}ZESr%R6b=zf!SKM%eN3JpR%IhoUJJ*iuR*K(Rq|8vPX#`Us@CoVE0kqSv=} -H%SU}@^@#8E>lZ4f45v=n>(QW%S%$+rk6Gr{tWo+WM})pW-E9**q!^t@$I(+SJGn%tfabrIC`o^3Ihcb -w8*2F~#@(Gb&*Q`*VEi9RMOLKf10q-g0k+Dqt`0=1ve6;&Cb`_PGpk-TR -O|P&H^BVil=iO*2uvfx!gmS^%4v&{Q&9?3ITe)C79%IWY_TdKC$+=Uhr&$my{G+&l0wn`1e1JImlX93 -YMJYMPx}ldg|pHKrz{1kaLOQ@|0%$23{)4mnMm^|ws;OP -S-_)ccs6e|4NdNhdqz+Wbl~hkfb`4(qU`K{OU9G^bZmo;%ztDrj-?iNKY2rSYhO3K0q;z-rMn$%ZSQo -YM72}F*PcH6@txu_oY_7I0vYCatsOs_gF1=J2CHR2T^*^Plq3!~kYOaf#xaVyBPif0gTp&SW&&`GFXL -wmHX09H5@?ZRg5}Q=6i&&Y46xpP5+?NKW{lh;fX$PV|x|o~#)9aXfY$rVKYqY$Ot(@K77e}}dgi!q@0qtHQN7f`#5L%U3K``z_K`)K8dugQIOC#-G8fo{^NV}It+PyT=?yZq -_FO9T&X{6mtBW>T1AnWnCf~8m1)?n$igEa`c_=N(jPTHX)DC1cLK^f002+DXtK~Tnv3OYTiA<`MIC#z`E^mS|6eI^u~me{B-d6c -@q)o1sd=#2lSExTx1p}@3gUcT9~Z>={sASyBm77~$Z)CA#7k%(R6=pJ5q3Awik`R-Btn?03PFUcH4WP -ue6dkUP{J3Rlmtut;teH17hh~v5~{CSQsN@T2t&6|KmH3v=vOUQf(6^-N~&J>VC~4=mHLft?MPh|MD? -VCRu9!-SXVz9gStOf;H7~D7t!4ngDr)Tz>dOLVAQ8H5t=9r^(FSOA$K+uHswwOgQOA8y*bTrutO;UJi^wokb2V3tl -p7E<_6zf{9K@iYENzIIz5FbO$kk*qK(3Cm)k{0Ap4w^k)=sOZc3Qo()9R_6R&VXJ9-*C9PwljNX{Xgw -JFVW@Y4y@htCx0Ky|mNnr5(3ZR9$2@_O6S342I5FpYvyK2k*Mb=9S~fZGIWYX!!50JX-%#S{Ujsa8W> -vDB$kO6ZJo#NuQza0@rFi*djF1q9v{>-BoU)NSt8O*}df}6=guH6xrF4aT?pL598ELl&XvD9&K?7=NT -yo=J_x=AR%ARJ=E)ag8?V{&|to)f?&N31;Kio3WD*r6a>?4D+qSmQGlkfPf1WhWW<9;Fjf-kaAdwd)Z -fT_elXm~d_NjK6#yJ-q!NHa%n?zv5`yLjVGiXNceHRx&sOS$RG"G7wyMoZVJfk2qF3&0mZOd~CLenCB<`m1>o>veWmK -PL+cI8C{p;>uJL67`N+6@iexe}g<5dMgAFnA0`gmPI(8o^|EWP7h4T3&?t{~{+7Yc$teyJenL --KBE(PK0rEutPZvFMR^PB9ZjYa*7tX-zB|J!&GBzG+P?T0LrFu|)PpLdu?-=hyf4NbsaZkMv@SIWAgR -u~JSeD^7&b%8J!;T3K;8vsPBDnA6HyMm;JkR?TT;#iSUmtXMgxm9<=YcfuMpdsNo4)uXavl8shYOtI0 -*iU~FvN4hn1an@(Md%jPXxh=Hf3=!$~zyAY8!Jq%YkS2&hE(e~Wj@&&u`kM<1-7Dp8i}$+7&ZUX{iJo -|e=(HkC@;EiF2s1oRol(>PI;#kCJWiccgxL(I&MRsIT~ -O2kx~M1vx}*r>!lxz_LGh<16(Rj_YD!TFbXgG+5U1V~gu$@8W6KpmUt=p2L0@D2ilDEt0Y%W)*q|clY -ivjn^fflD2>KcuQ3QRBtyBbkjjd7yeT}VF1bvOIQ3QRBtyKhljjdAzeT}VG1bvNd5EN5i8x>Jsn-o!B -Zz!U^HY=jOwkV>$wko2&-c&?=ZBs;jZC6Bn?NCI0?Nmg4?NUU2?N&s6jVhwP_9*gx&`($Ex)TNBZj;* -FtH|Z^x=3kVJ*ost7tX3%KvwIjA#RMe+oXaHD{_rQw@JkuQREtnZj%Z*rpPYXs*A7}tkz}Zsjg}(f;` -n#H4Wsct}eMwIPQ7qLVdH>M2q&Mjgs3TsxG3jXp7?;=q^(3KUNS#@-qcNB3IPABjLSWpop8s?yF+=a^L1zP)kT!II^B+! -cZc#;XWQ}e?oi%uDf05pC~x(wZbzuPi1Oa2$jdvUybmby^3EvlLyEk-Gs^p2MPA++<*iME+wQ3@qP&k -Q^776o?~{tWymQK1-Ll*6sV<_t)qlF}p5ks!d22B5wtK3JC~pno-F8oP5#_DHyW8$5?&g%Y2JLRUr?{ -I_-rDxM?Vjo)%3IyI+wLjuB0qL_T>ZD(?x`+ZD(?x`+OUvE>m-LvBd$2aL!zBlv -W#1Op-@%P=cqL9MRJ1E?_g>{ira?x><3JO2}`oXyt<|@};d2m*QUgg#m#}7_mFjx6!xzp2Pf1p8K65GqS5!%)R7hrnJ&wLBh;?1B!x5XIH9qVlTPbm1@&ituVVvzYEJ1X1APF6alFWa -pcA9nS%EU#_Trr_gEw`yG`pMmPyR+I;AM+Yj(HF5WC;a?$%ZvyWhkTRxGePQLs?8z;Pps$l^uvoP*YrGDYY|NfA4}`vSfV0AKjky)-xv^MdZsie -0Eg-F;Sg0|#azarDNX;CJH0D-mJ;V}?xm6lZVS&cnDkG?%uesI76a{_FtshiW0ZCS&!Vt^cW^KALy?A -c(K}A7db6d76N`SOE#aPSS7OlOQLp--d%>lEB=ib!HhVhuWH{VbMeeFsy!2C+By%=bkU#Vt}btm( -y)EqG2G`~vYC~O2Uze?){2A<}3tWyMi?NC3D0jT-C8vkND#QDAY$ry;5->Z_x#sKpN9vYE^t36Mnh`C -qd{6RG)>>n_HNNX0xuI3NxM_>zqd7X{JJrd`2P6&5MoY$EN+#hj%Of`V51m<-pihCo@pVgoV+X>8{Ri -}=zt@(3WRk17L{3TT>#<=Du)ZCF~nZK-66=PlVm-RKU8{+(B?T};4YoTAg4t7FZ7*fN<*w?~{N*-GrE -{tfU!x-4Yi21FcuZ69uWsHR_ys32qTN^HH*H#^4VhcOf(PCS}g`MiuF*dfaOPg+NX}BN43!|Dcz|ph|Z>bSs%xvMUa0rMd4NdPJtaiO1P!jJ&Ed)K6j>Q(#I@|))ZQOSV&)9rmh=LWR25zw%!HYN2i@Bk8Mn!}z@cW-^iooAvy&7TLkvBue~M$kI&@E?0yt7zbA -`FX*OcC}Z9zL!J+c6FfD8gopgAXf-gpfT?pT4lANcM-)M&gIc+GhV9^9MNsMBDn(G~;A%xw=^8~;=~ -_k9*E&Ve*Wh|Z(AVGwMbOvaMn%xq;3h@T*Weq9psyhn6wkaJdPfoTHMCC=^fk0!5%eX)a|w4|nI1Z*2 ->Kd2qzL*NI;;r#8hTd|^fjcOfX8;oC}G0gR;Gv46Y%(sA@u}2BX{V8BIs-Aq$22RNNt(Nc?_v7^UT~K -wdE%DrMAqobBENHIca)GZMkWOq7!bgGCjCO5$$TLBIs#E&6{VDjHr3@ERqp5Ii5u_qISizNJcg*f}Td -UDT1Cxb||7rWWKd8 -pTGkFKqcv|#>YCJrfcTkO|ML(!!z%zOW&nco^X;8t_PKLCK@~o2~t%p46WJC>>=bVh_8}gKsA+3iz<7 -8;NBKkqCqCDSZcu*1aHLSq|&o&uW)8ok|!`jU7T$5pKW_YT}usR2xX)>%8na?^P!HNr%f3mPFHM7k?G-u=I*})de2*c@?cO&@%3T$szxY_)Pxzj~h_K(lm~gac;(=N~SfF%bIv_W>im2> -Sf@wWcOS(C5EzwX%gi|FF-B5BmJW_Wgf&+kR>({NHz3?p$>oLzlH<*6U;FvUbe|#=O(mNeo@q?%BYUk -2Q*pq05<};|4bQo5ml+E#7(5K6Bm4}sd+IJxiDg^ -8s`o?HnaOJg$eXIyX}C1(C6&iM+}VlLPnnn^f|ZxvVqX&%*J&FLZ5T1E*c1Z&Y#|EAoMwRz^E*NK4(@ -N*(T8EHxv5|gg$3B84F9G&za)~4TL`DcG<^5pWjTHI)tMT9-ALG4NgLRt~2J4_E8AN98%inq^Tw;^f^ -Cbl$|Do;|5}`+|~_>5OrOgRD`)#GS90Y2MGv -kI5Y%kkX5KRy`c%*n -CT5o#l}o;gk-A3ygkn;sqbdbdRQOKp0(CV$%L#@)HA~vH+yCnV`f4$jTkc%qG`mKnGj7Q#>|9h8Zl-j -MAL{dG9h(TrzmK&j_Cv;6H-TXih@e(curAJX&uce3M&1*OcktA&{rMFDGK_k<2Xe@Uv(6xDCnz>;S>d -Z)e)ScpszZ9Qxx=7M{kOPzUtUbQ5Uaq;oI@Il?Ij7aorzA&AmZuf7p3SkzHw&#K?n~&UB1Ci0MqnZFa -%6#v>16I@2-oAa$&!2#tYeTjMsn;A-s1gXsE?8lek*aa0k9^j0Y1L|mM@qiisVQh -T23Qs0j4wB4h*qqc+GRNF!BtLXg_4b&?bgy5Vdu&B;uU~yBHsHM1e^OB!XiO3IqVHEKe(2nU-i+#Aqvx`e(4_9%kB1~h=Is4w -kTRMeM7ZWZ;VR#{PBYLylBrB+!{Uuu;V^`+riMSZEkR@9doY(;%(FR`M&)RrsiOKrKLzBCl9s4q41iu -yXO$Ys-$KJkP11~%Orm^Ai~^hJ8j(Yj*Y>))%_#Cpy6y@8#^u_k?qUNdPVnDo(6?yn8({&V_I=`{_#X -56@gq>p~{;JyBL>udA^de6hP=c;|^;>)bk%i3~TeL;Wy;gfm|UFwm=x4ozrWO~7*F@~g%?)B*67ah?H -x^lrl{W$|pL4{s(yncv#Rl40{i`N*jRC>X`~KRHn8_W&9`+jYPl;mwPnd)?Dz{7e@?X!vBI3ZFMP_6HC~BoV -W{AO);B_o3YA$DVq(a*_bWIYSY-uK|urSm#APoE@)uGVwD)w3)qk>x^XK33H*DW? -o&UhYoBC)imlJa#S4x(VXzt6a^K(Aj@Fx3TH)ZwMzeJ3dR_+3QLW4Er;Dg9iFJKKh^1$!9hs1i#yGmI@j^6iCCSYl#dcowP12I3r8gk6Gjg6+#` -*w7%|FruSR+6QH6b$S3%^8E|aiNZdN)bDT!Cl9YtaA)Ug*q1W-gGReyk!}NdE1T!MRy#-scB9y80w9j -WH6E&IniJ&H*&JUXl~?$gYn$RNe3gkkrNM(y7TfuV$KT)i8(K!pvl-vD5x^_5(>JEy@Y}?V<#bu@WxI -;7~_5RmB$qYZ9Zv8Q03!4RTA|0qsL!W6twvA&y)lezVd{UpuZ|4G -G$N-jJZY=YOOmDDOo>g7Tg*1m}M}{(_>QyyqPY+I!_`B|&>H8PbIu48pnHlg}s$I(yNOps(j266p~1J -e#SGCW9iM{gpfxgTc>Rgd@W&3YvVzqM*vxEo$JqRl*nye$fzEq9H+<&l?hS`LtRu27+HQB*)7cgyX0T -!8yN%1Vuh?2u}Jn4g`b1PgoT6WJ!!)G6)7|91_NWpRg#X$&x01%Ala87w@jz`kKPFLi3hETgJ;^82EX -Mg1TO`2nX<46!i7#<4-CoiTo#27VAe~Jopv86T`tT8A4ZS5Hx2b#p;$Z;ziMn1ruW -sM%kz5xc0@3CTgSyZBQb|zEvuY9;&VBY3B|$OIy`&`Q<@x88^mKa|%6_c$o39MY -`B`1l%u59QJnmr0$rL)BEGape!ZJ>#u#A%_EaPMf%Q%_BGESzjjFTxW<4g(5WTu3LGE>4rnJHl*4<%XlJ&V~W$9u^My -^WTI@~OEO<0z?4g9m_;+Tb8)kJpq+Z`(u*)4JEq8L{XD6$*>)I}+-9#UjCqN$6TxOzm9-6p6mYUAovi -tHvmby0?^*C?{v1Jy-cT)j?_-L8pz -UPwwoR*xC~x%;?u3`RD9HQzY9*mbzrI$9J@+VUR?=mV(5dI4Qd!G4$}P;X_;&X`MNaB1%%}Kv_W?yt> -Mcy4_;&XpMNaB1Oqlp~_q&Rm)LWPz@$K%Tik#G2nBws5?&FGFEpK5i!?(LnDsr{Fl~97?io669O7N^A -FTsQoJg>-0Frfr5D)JIcD8UIuUV;fFIHkx-FrftBQ{*Mc$Zd3mBIhSsjNC>C6nS|wavL2|sWmk=y87ioCoTxsC2s#l`r|Y5)uD+zmp1fTb<+yrMkv+_?F2XYCv -+9vs{3LaeZb5CqjhpsJ*cz(NQts*Zj40T@By4-dl -P>a#!9`#}xT}pJ+^NgtaE=o-uoew};3@P`h>+&IGiy`G6bzMHBY%!$Vqpr(`lr4mm-|kV@<+kRni%{? -OsOxe^KWD9sLRQ!1j()C-C_!~y?&#;bi1kj@?T&t~izsh(UGC`Tx`^^t*X53Wu8V@auO3tq>hIOvN`l -m{8o{6zA5#)!fAxqGFM5m-ezHeN$i04|7KkyzPqZdri16FvTDf!TK|@k^By?RwJ!nYkj)bm@?9fJC<3x_kb(VBzpZi}yzNtmZFeMVUF2S=o%*Xv$oaE&;eXMcHkx%Bc -0;?m$km-r%cWX7{=GU~dpK=f}72S>lqrnU`voy7$T&{;Gz1udLyklx%VIRj+bK -D`#2d-Ya)o#n(kkT;(cr-sdEdNB&une^%_iF^`wH%I-&07x`cGH9BX6tG)3YlQX|Pc3u%&)=?#4AmQ8 -NZwPAoUvD}a$ho=g`nMHfm!9=|6=9d2_3tRcE-Q_d7Ch?@D8d#z>klfz7Ch@!n%vxW{b5 -Def@l4^im(OG`iG5!#4bG#465(OESp5mkCo5mkCg5%o2pi29mTM14&uqP{LGqQ0c3kt2ql-mqK|^ -tEAyBIs*Fzar>s!+;{_Yr~)-=xf7}BIs+wup;Pd!-yj2OV0hR5%jfTl_KbC!)is)*M>EUpsx*U6+vGc -)+vI%Hmp|!eQl6rPM%R;8x>Jsn-o!B>Zvp8YqKKiYl|Z4YpWva>rF+}*EU7e*LFqJ*A7M0*G@&$*Dgi -W*KS4B*Qg@uYmXx8OL|m!PJL+;ms4Nb#O2hNHgP%irA=H;eQ6VyQ(xM|<h_7a(|VZ0Z|v(kh -N%4wi{D*u(*GIY^5UX;<2jVzgIk~Dv+!gAyA{Pyy5q4iQo&f -9BXLoN?gxw9#sw%NYeQwR9BJ7qYYvXH#-PbM;E5dGh!{-%Y_q8>u6s#$q-K-S=3(9AYZdQb2kXNcIv4 -VVV*C9pFmmC*dBb>pyUB3(Cyz@KM`C)1J{P~lLaOU7{b+lL!zHr>!1^YU$UI(kd=iXVX2)pGSP&bB^- -*e~QR)pR1HfR;aQtySesuYX|>*0(wg3CPmrXuWaxKArKR&~!zn%}~))noedSjIiS;<6(6!&UDnfc*P1sJ!5{XUyRduch*}C3RnLrQ?Zr6r++lMU^mR_PjPd6A4cZc6`SijLZEi5`Jbz-jqM)zYH7e -c)^`&(K*}3_n<_&|szPWHw5&Ypv^-&m)o?Whz$Fk|U^%^u`TzYQnDMhfawc0vhe0px*gd)U=m(|QMPC -av3LoKY8{^o+Vl^Cy{9oGtgb<(q2HJ-w__1qaXBCL^~k!(hd5O3_&<_7Dd=XProfN|{kP3jM^HhTVuH -ls+C&Yn<@jYO#)S6w5-8>cnMMWS?ejn*h6O6S*TgpNe%?0R*TNR-ZPHFrT@mrf``yfLI&Mxu0P2oj~U@2R$tD4pA{l@5v0g*P?+MWS@!U2_*gl1u6dkSJZ4(1;I-(wWs7h9glryGw&6ZjgOkn;j%d=cm --Bktm(Hs6AmMN@s^P;^PL{E7hrUgX}l7Ipqe~llv4Qgd9>&fJEus8wNpNhcviAqIB-Oeiss@^DDGsBT -+g(c~DW%*ZgKRTqH^tmYeUwvDNF$8^UF-)Vje9vR9i2LSJuaClZO$xdHWXNR-a+Q~N@q)H+&jkUgQ2N -1}A*J+);dN@v$;6Ng0U>d#7KBsL95~Z`tHE2Sj -bZ))&Jdh}z+oVoCW0piM1&Px66>8>4lrF5)ij73+!Y)nLAW^z7s&xa2(uKD*pw1bRXyA!N>D)n8B@(3 -z!{!a)4|i(Zfkf%7?2B3>oY{BDNCWyBFfxRFoiuL<`#Pne780eiZ)$1>iBcPrVs-S~h&pquj-K0L?m| -{;v*rV^N_t_1K|B^-BR;H?p53Wh#zN_Z?J5myls&Ur%^a(xXGhhRv0QrgEp3;(Oo3>;fd$jEm(+-`Vt -Q_)c@5atR#ghtOwaArG*XvcG;P1IYI?zht5`Ozhh)?U?bk|`A=XVV?9hshh0`<3^$oFddghFN7gkQso ->#YwmDBT+1|ftr28$)svqReH#}ewMLn}}$ulw2p63Hq9jvsT*{C5T)>+St -X<&_2*0bBShmS?pv!iOASYti=&S6CeA;-;cAtc$TpN<9AbK5l3!usmDDQ%Ikyn240elk{9&+k*eh{e? -lriYKE)eF1KYrti$Rp*Cg)w7qh^~0j-nT?~0V1grRXIM}@_lCADSWZ2^O@9lEsTX!?_=Ba?3u9VUv5< -PLYVHd9nmeZ5Ld?sXJFVR&tc#voqX9KmMbGuClfatj`Qt{s$Q -nK6WhMrj|O35&qHnVa>5nSe_gNk5ZYc?xF$Upw3BDl;GLy9(_y1EET*r9@n>^$VUht9#I5~ -*tJ>_EMotpB3Q%$^INcp0|ykrA`Wg)ghpvpD;37Q=1yN$1P^w3N|BubT4gbZFyBz*g^zKr`J)#UdEsL -aYhk&$8WG%pzNA||SQnuI*mqJ9`lS0*9~_3-e^^lv-@-YqL>Puz7*H)@7;0gsBqcHoLw$4kgd#YUfkT -SWCmm9o#4yy14gh8thMJM%@@j=L(S|^8Dbb}X0IAKhM{KOF?VrT!@N8k%9 -xQMhBf-PC_*1+$ov*_J`yTr7>1f1Ijsl*)>bu03`5OoJd|M=YIdhe9>Y+x?`Q*nVW`=&TBCTP>4k@0Q -Nqri2maJ|2bIoFnm2_98&WZ2q-t*EQAO}z8_febvS>aX-Jt#IrZ7@9cR(vRMylp?3P;9~ssj?9WgMwG -pxzQAReFkCjWF_YP)!gcRdWZm`e3AL?p>|>7^#{&vO*DBoRj7*v^bjK%P>+kcR>Y(k*c|i=C`1)DXri -bshVG|^${ag^CQO -IhFdQ|1`J5u;eoXs?;iv^UV6H|8wBCA85!$i0HFCpn)a;70ih@%9xPOgWEXJeeS8i8iC%07@2AjUQps -_6mn`Vaf_gieG)Wc%1>6^=1qcPYtGo-Z)gH1ET<}S2SYcw#&VAISR4d*%7WZE_kHf`Oi2(6T6$1-lkY -aD%xgM%8aaueQn#uXtAa9~gox+w>=ZR1GNyZRa&Njj!Vk&z?~>3G!Ygnlwcl4hpfRRn!qRY2Nwde*M`9$&3^!*ONivDf3?oUi6XrFbuLPmvD2yb{ZGTGz1yGqQwA&+4aRu*n8e7;KuG) -L;~YO*SOLVAK3MHFpd)&2P|}g~6uzQ8hsfHq9SUJHuep{6Vc*7;Li95(b;*k7&Sw!6w^Z$6(XEX8ke@ -Hreni?>S#7!(h|=QEeqK*kpq?3^vW5(E5nMCL6SIuxV1iO9q>?Dd1q!u(o~}Y+6{QQ6LAKc4&qHgH1L -H#9))n-EI;KSHmt;8)50;Wy%=mVlkzhRHvRFPy{Z+AHq}g9O$;~vao+}wws_ -v^3H5&%Z~A7r1`!x<`ewP-9E>;p@xUtW7-7Kaj|axJ-+>XQKOQnUO$<3LObsZ4)-G#c;#N^tITFlIKd -mIl=;`N_1PML;f|3MJGdHC`=B^;1*Irj*SD{onRvvtQ=JRvPQnCZe8e~V61ucAj**>$y`;IHKEAQ)~P ->;XZ@w+q1TuM2|1phBi|4tGsIWnGKyrU$j=8KI=f@;3ltR%?oi!Dlm)V|oIB*^TG-AaPQz8F;!KEJXgvYfT+qOvY)`g_U>s@<@X5; -KfeXB-idskg_J1iQLDp(M!i_GKmEtG`;VB)s2k>5k^e0p8a4M#k>8N(uS3ul6emzw?zOzPRVaSA$Bz? -|e0&1iv$^1ivHcD{h4F)jB1iR)4it3Ep76l2HG?TB!u@wp~f6LtkxDg145`54QvO>ZB69+qe?Efz}=L -(!Ww|qbL5A)++Ryzt*~gzVX-E5}_yimDWUbD!-O+n9I4p)+T0>aGtX?2>DOUqOZ_hTeVC;3wb*L6k8kOPX1*PrsxOb(--G^++xLY2j@$d7zZ3SpjLf_X^moEO6Z#vs&n(EyGfOhnExL7I -j+@^@x9-cc=9%c&eR>f52>^Y+nD@deAE;*0h -(q2i17KB)MTy$>qBWbX?qzP-ZUSCSdAR%9wxbOmp(Fu#YcV9nIj&=stini{%-xBJbzpeuO0-~L{M%=} -)H%=})^-|YcyAiL-j)=XuFK4HzwduC)tW;xjHL1kSs^L+)G`M#3Od>`~TXubix#G2_hpqE%P{RT|Cxj -ks!6Wzs{>3gEPSTlW3%)GfhWZ$z%X5O|{c=oMgY2|2hUi8cn3+? -vVP$PH)l^eLd(~8vfvSDaoXor@v}glkOwXyAdC!u}yk|wG@7ZFYYTgsGcxtBaiA0ABj4_X=X8N9($m4 -=$%;a%FGtwt@&}`nbMP}X;`m+HwX7kj{yk|yc-ZLjN?+GtzBZn5F7<(VQsEq(Hr^iJAnAGDU0LOz9@knUz2sVJ%N9X6Ea8#-(#?KjZHng+H*7i${W4qdEi -U^~nX*>A|nMwHRT+IOLgweLa~Yu`m&tiB7fku}qI$;8F#yJU2+=3O$nSo1EKxLETp;$qFa(8b#Opg)^ -K!2BSWL%;+fmqS2a)#VT}<^t?}(4Wl{WReRo_en0md;@a1Enq=nppg)^Kz+@oi5FRk-&n5wKcJ%FgLVq?1kh7z2?}Pqq5+Ik3zPV32`uYuBcJz&0y3*0t-@ -~*Z*O~9Kqi^qH6KUQPGlN{m8&iW^$GgjpzG=$4?C9HfK@({^(3l?NI?$LO(WXutAApqtGxy~-eS6pWovxHn{7t@4X+mC5NHPdg$sEzB;Vw#Za&|*xdX4>`_>?qo2 -=49skx@6}2pg$WIU_7X1=KJ73ZChM&EW_M~_2;hL#8{GRH%sYanERytGxx!N+BU9~_Rri0|7qI*j9=B -v+z0=8#@?5anfo4*Xe%F)Xe-733|l%;BO!6mLyzj)tKQWcgKDnYR8jA0g8{eO%Uucq%hqdZa|jratEo -*QVDb8w=S`r>;kxrCP{s1~y5TbSwaLtVQ1m5xUq)u`%UOBMeH`?#_Z6%>=Dw25+y_HFuJ;ujbUd#26& -!RtX77v1%zZG_6ZXE8%-je4owfHh$;^Ec=$QN3Wad5^syfqxgN|p^nHC&$JfqIE;Gm;1{(^&!XSCy1^ -huy<81#46Fb+DNvn=Xq4+RGujXe|`bi8QqOMBWw!9mAM_P%CMdnlq-PkSh$c29dKqE1hHD59*VJro>t -wD!P3M{5rpbhP%sK}Ty39CWnyz(GfA4?5^*?u&cdgA6*_`_kU_z(GfA4;*x~_P`NEYY!YzwDu5pdfNj ->6shEB9F_ckM?`n#C8d7m0Nb2R8|{ -m$me5+}HOrKVf`HZ>jyl9X_{=Q&&T1EkThYHVs~V1FFqJm;Dv4(FV6Rj(xty%ly#-HE+7RjSp8!!sA1 -F>!$AqJMH*seVyQf@gPcrqsMuuGBo4!!ce~GU(!DCBrWEt=c;Mz|u&mzB3132AzI6{4!XZni_-*vbZn -Op%nW?GGO9NoI@r<{)=*@=8L+d);(}kWXRQ=w&aCQ*te3*kp13{_xT+5xc@3@cpaG`V7*O8<_K7obMR -x>35dfV!%nFf0J)!xviEkBntM5xe5r+2g-RhcxVyIl<9IvYBaH^>=k(!#-JuHf$W6X>!NjtH -T@iUQTxO_j0P>$LSb&s0a|t#bWcL%WZ>w|L}Rm0F+8TQS$;^xKuRx}!QU -VkuLz?svP%qrSJxtbzO9uH=2(z1nlJziWJ~p>7>$!LBS*kgO{zF~#}b@>Pmh7uPDn-U!RrDN2CWD@uVj -C~COMkp@Nli-Mqte^U^ak1zYpkfNeT1!*i{tPenMOKf;cZ1gg?MyP;lqK#e#*9hOOCW^d_HH}86M>TI -`J*s&l?@`Scb2Lj;^G4CLnm5Xx)f}^X|55;E_WmaYVOH;dRuE?N{ix|akW}_tKna%iL2Gu)Hkc$+dLZ@%2C1pU@`lpvtnrv#zee -kBOP4k+QoMkUC!9#X}Mq$+Y-WF{c$DuofAs%J13QJHG>ko!I%=rK&G^ooQgND -gkxDs800P$4mNt99aeI#nruf?cA0}!f>*hqgiGp_a88^M&VJHXkn@!Km2l~o5~dP_l1~PRzZkn{!T%NW&U0OW< -~TV2z4q_5CZ5}L2$E)f)G5X3PSB{C%K_CBBK?u=*pdd8B| -4l*AhlJ2o93rIut{~{+aRosiKT;6%@q~h)k0%ubeLSTg=;LVxK_Aa32>N(dLD0u@3POkT#|nbKd0s*2 -Y`&l%*vE?sf_=QCAo$jo1z^^~D++=>exe}g<5dMgAFnA0`gmPI(8o^|1bzHWLD0v~6$E|!LP5~SFBJr -R{7ONvkAG1R>_dAMRT2#3-xLG``F|7y1Cd^Dl?DU(p9*>zNZQLl(q0CV_A-#Pmw}|c3?%JkAZafHNqZ -Sc+RH%FUIvo(GLW>Ffuu_qh(vt7kKd}&UN+R|WkZc#Hq_{4LycZG)aYeHjb1j?=w(BVUN+R|WkZc#Hq -_{4L%nhs6>=E=uYzDg|64&Yq5n@o(8t|_5|~yvD&Veeep_L}iG~nOCeGF>V&ZI_A|}pOC<>anH!z{73 -3OT!lDhqS6(OnHzfTboo&5(CA<@}?ND&g9{qHJ5qI1Cf7P3tP=C_b-8Zf_wMCX8XJu0k1zBe$b2&umQ -cN9Ti{reR`U;PIaL0|oc6+vJ9M-)L{{YMp1U&j?uUndn&Ut@}>uW?1x*I7l-SO0lM&{zLOMbKBjR#7B -S?)A&5N)_@a_xj&c1bq#tIbhn$y@5eR(AR*PD^?ue)3bZ4HtlP*BHGs)MbOuPnmJY*-y2XfN5babzOYzS -Ku`s4q3x4)vu5+o8UWDWbmAmOIp!+H!~bQiJW#zIG_0eaYD$Rp!$NNWD!DE>{!-smWm~&GfJe2=i#B2 -m2K@aF+@QGiauVRX~_NGd-yO9dl=<2i5YC9-JQ3HXO5NriV`{%7N5-V!q7uu(qs7D^3rqo-k8pT2E%K -kaCq!kqe2kCszk4|8J@g!Vp+tHSJ=X+7?w%BfN{SmZ#b2UjVAN(a@8=2WSA(VQw(FPc- -O>P0b8W_nP)C}zn_52_c%6q)J4jf$Ys!A**w(n0mTx!ZKO!jzWjA(b8GvrG@Ev%_ST=^+i0km{WtQa^ -}kEYm|8KVc5b^pLtpKY^vf65i<{jkd6YcUsR`s<41}dPp5Aa?R62>MyW-cX~)2Dpv1K52?Q>yeBItX> -}n5C4E8>C9UqaprqAL6qL03iGq?=KT&u|7nHQRj)Ia_*HKW?>N*OSN3O6?_Sag?kbwWS)-o)T{k2NI# -5PL5QJ;$yvcJ|khJ607wPIp*>|N7SDBbXDg*CBvw`k*nfq}cjYRp&>dv{ok84F_X4y!R^J?!1#&5A;G -zWb&IH&_jOcbmGl(t3xeudmNekhx!Ghkx&v*@;PYS#0HVzpU-ATTDBd>lQOk_BuP$>3*3#jI}OHm$+Wd -8K$C`3ow#4toh5y~ST;jUQe@C)}oRiqgFNuBqOH!}5WQlr^w3fJTiSJE1UM@+;*T1B@#QSx9y-yaMv| -g8DE%F~XZai#YMEKAdNd(pNn5g^r$GrcDd3Zv2)WDSRn1Kz#;|4YfPZ-!DJZWH?@RWfa!Z8Cg!qW!kg -yRNw3C|c<5S}%#Bs^zeMR;CeBr3mPps4(!fuhe#28uo>3>1A%8YudlGEnq+*+9|fdj?XUoBsd4zN9;D -B)xX;U*VcemZPyX@+&YPa1hjTALf6g7*IC~*>1nyGoJ&0D;cv{iX&GLy+ -&{se{(J^M?q;c+MXt5aT(2gg}hv{80iip7X~D#CXon5Qy=dKkk -At#&dp_K#b@72?8;m^Ct -JT-LUN%Pdui6_ldLnodzPYs=T(mXYE;z{$=(1|C_Q$r`7G*8t|JZYY)op{nbRXg#dd8&5eN%K_gFQw8 -tFzy7FjwY~lG%@dRyK70{KI{KoWBsL`$#|O|4Q$}=X5PE2x@&3RKI1P5GaIqj@O|#q#aWEXcSh}fEbu1-yT06euh!l*KNNYO>vH4os`A3$Rpn*izRC}69@w?yt-tHbzx ->FF?+di=%g)lhuZVmq7m@$GMdDAPMW9~_uRKI|EwH;ODxXqC<;SD&l&!sM{BoaYP!9YQDhK?FioI)oY -Lo;2@0L;E`=Y@2dG9^kyH@!31*lvmerl8fb)pRPbD8>lE(5#G(gf}cqFlM;66`<+PmiSs%-uH -s=$y@1^Q+GU3zq1Am1wE_g|Iy`B7zo`wIR6%iguX-wmh^RT;Q1kiWyg^9}=X!|Kx0bDIYV#Hnuc5P>+ -=Z5}2Nr@GA}1meene@dV~ -I}HP~>@cv(+pllw-d6`U78jDKJ2KX1UhiK2vmKe%_z`uC;YDk_TnRbCWjf&2VbcN7 -i%ehzptQPlYT9DQ*|>Au!~jyUkVvG>FMy=(l7%kG-@n&R$S;5p*lf8IFvR(ahuKW=gE*RiwZLX87&0lg9-fz02s0@q?W$KS_0Rc2BbAQS#bAQS#OTT>q -0W>OGe~Azf`l6xlaYRGk-_^9?hqG{nyXGtVq4YKz!p91 -^!mFZ7fpL+X+lIcGG#9vhVC;p<^*UF>)3a@-$;P3j!F`_!KJ8FEdD{A~yiv#LGoD|;o#er39oLcXB+kk$U`b?4-^;Y6Z|~aDUqah}_tRbT`mej@sU^1b)fZd(O -Kn>=UJBXL-_>^4JU-iiM`SC1=(CldYTY&eIVyjbVk_(Mxx41^+3x#BbYJEzd2Qtnr8c0h*{1M@ARAEE -Z0m0sY#Vsq)>rFn>#G*l|D98J&CfTxf6CptFAMNF&sy)kJiya#$>&X}Rp9%oDDs}8iu}B+0w;E>z|>p -qzxkK$3w+=Hn_}rcKhLY7uOd}Lf1Ip_{y0?a`xSKGmw5aQUGv70I?z}3z|WaFpfJ{ve;U5K=1tpm1a< -4CH;!HI{$HgVTD;+ke*^pm{C{rf{(nd}MWLG>(M@sarpI(s61wRL-IRuIdP+BWf>Is%_HlR38$#*`m) -tjIUi-a*%kCR9Zvr>e-Z%K&hD-2Yz9n!|8~P^dTRa}ue-q#2NxXIBOW!p~yw3VXzN?}8m_Lr!fs^@l- -1@l?P&?~=C5`U$$K7s)=syCUeE)`!o~nOGNYBq-6Vj9NH-z*|{4F8<9`o-B={K1FKuEv4{2d|v*76?- ->GzfYL`c7>{AWUXdjFn~p4ZErW9Li+f8Pe>o19|-B=^Cv?3`1~6 -oeSH2*NFSenC!~+he-P5g=P!hmilcA}oj3|d(21jP{+u`phtG+laPpiu3dhcgqj2V&I0^^OiKB4ZoHz -GGv)uxD}P;QDi3iYOlqfl^)I0 -_Y~h@(()iZ}{2r--9abc#3%Ri}ufPnm7uDr-`FbcbYf~Wv7XwP<5I(3 -Pq=hqfmc}I0^-*h@((}iZ}`-sEDIbgNir`MW~3QP=$&(3T3E>qfm#6I0}WRh@()6iZ}|TsEDIbi;6f3 -#i)p*P-==e3bm$)qfl&$I11IKh@(($iZ}}Orii1)+XZ<YRk=n<92XwJBmJl$#=ULcJ+sC%oN^*a>e -o`^rvdX((&{?|ryd1cnU?5g2wRL}1vG5P@MILIj4*2N4){97JH)W)Oj)CKVAFN>UMlp&}I#7z$DmfuS -B15g5u*5rLr^6%iPUQ4xWm78MZ~N>LGkp%N7hvQUVMXbg3zh{jNcif9Z~sEEc;goTjP=bmES* -Soo9EAc@#8Id}MI43lQ^ZlIK1Cdb;#0&?s69m-h0;^RQK&pc9EAhP#8EhnOdN%y$iz`NhfExWL&(HYI -Dt$Yh2zJ>Q8;@{9EF3EhPOdN$H$HY-MZ%iD8!^Xr>IB85Ag=5CVQ8;5v9EAhM#8Eh1OdN%y#l%rK -S4{u6J&`X} -Ci?@zn|)1P<)jz94R?0y>C!0RXWfYDFvsXPI>#vj_!s8)G$c#S`kXPKP1M1Xh@8MAJo!#2E -V4I@eF=S8>|Uy7fDrNUWDg|n6lXDt=ZS}L5iR5)v?aMn`ctfj(PONFzM3TGn~ -&PFPnjZ`=rsc<$@;cTSB*+_-6kqT!c70yN~oQ+gC8>w(MQsHc*!r4fLvy}>GD;3UGDx9rUI9sW3wo>7 -2rNY@tg|n3kXDb!XRw|sWR5)9yaJEw6Y^B24DuvSwl)`BSO5rpErEr>oQaH^(DV%1Y6i(y+5Wc&nv`s -Tm+NK#OZPN^twrK{Q``ukr+NBvN?a~aCc4;D|U7AQ~mnKr$rHPbwX(FXvn#kA2Or%sx6DifwL`tDs-;PkYH1RsTAD!jLqp?b3G*)Sh#wv}`Sfw!QaDYe6i!nqh0|0@;WU*}I8CJ#PE#p`(^N{~G? -h{~%}^|R -5)v?aMn`ctfj(PONFzR3TG`9&RU(}G_}+@YpHYAQs=Ct&RMH7oTgT1I87~e&|2!CwbVfyse?9B2W_Md -+DIL=kveFj&TyJWs-cZkLmR1vHc}03q#D{tHMEgxXrs<>nnr4(jnqV2sfo5y6Kw;UsI!#RZ#?KeEM?P -H%BHQ9OwtnrJIE(N=1rt<*$Ysfo5q6SV`SiQ0kEMD0LnqIRG(Q9DqYs2wOx)DD -y;Y6nUawF9Mz+JVwU?LeL6v;(D#+JRC=?LaA`cA%6|J5VP%ZKU*38!3I%MoJ&Gk-vebvTFU$wD1$!TMyvf5awtTt9EtBsY)YGb9c+ -E}TqHdZRDjg`u3W2Lg%SgEWwRw}EFmC9;krLx*ssjN0pDyvPD%4!p(vf4zctTs_9t4);3Y7?ch+C-_W -Hc=|8O_a)N6Q#1+M5(McQ7WrVl*(!orLx*YsjN0tDyvPE%4$=kvf5OstTt6Dt4)>4YEz}M+El5mHdQL -CO_j=OQ>C)nRH>{sRVu4ZmC9;UrLx*osjSv0mDL)hvRb25R%?{XYK>A^tx+ngH35~?SxV~n6?7j~Laa -4Ph_yxuvDPRd)*2cjBy1wnlrKJo+gf%y2!g}^WR_?n>PUn|i%OH2KlhVIjSNIxB;FYZJ7X@;Oaq@Rux)Q9xbtP6p~^ -yvgaeMtXnmc9{OCT2L;Ow4fbnV8{VG%>@$X<~+h)x-=3uZbBx)G-COi5U)d6EhtACT2JoPRwv{oS5NY -IWfb*b7F>r>BI~N*NGVpwiDAB+@()P39^!-;4d*p!C+#Jg2Tid1&fI}3Levc%^oI9kBne5F$2J7;xL2 -J#195e6ZdbUqtfUsYIGJgI*S^eMUBp)MrTo@v#8No)aWc~bQU!_iyECpjn1M*XHl!OsMYambrylM#Jv -I55+_)z6Rgz<*6IXnb%M1z!CIYQEhZR$<51EUOeUr;xJ*o6u$hRy8Td>@-wccQaWn&ai -8z{pzeF6(z+fVdX5cXKB#=dN*vr6VVg`WA#9;=T$$PL;_HU%463HyeBAG>5B(o@sbQZ-ri{elgIZIak -GLY`mI?-aCXt7STSSMPn6D`(>7VAWdb)v;O(PACaSSMO6R~{MIQ=Egb&cQ_IV4`y{(K(pt987c$CYXc -#jaD)oj4Eb0I91GWu&S8h;8iih!K~tW0^BO5Z=wSReihRf3@fHDI95zwu&kKA;8`(!!L(xff@{U}1>1 -`03%(W87mO?BDEL#%Q81{Oqu@|6N5P_Ej)F(U90ik#ISMY7H%}NnGJ;XX3;?H!!wgmxKNxsb+`mRg#p -oXux1*?kb3tkn|H`4(Fw~FZtb`{eX{3@m|7*T`CQE;ewjyfw={jQeo)48NE*jU~?k?WBWoGc~ -@SXs=3Tq?R5m|0#3bl?h|XoXI+LMK|G6RpsRR_H`4bfOhH(F&btg-*0WCt9Hst4wgCxOPzzI&cQO2gTFvJ!AeXp{>D<$7hN!-FS=kvUv$BUzUYDxebEIY`l1U)^sRKj&; -=v0XS0B2Vh81AAlc4eE@b8^#Qn -1)CXWjQ6GR8MSTEP6!if(QQQZm?1M`7!BF-=E&HJH`@mU={(s)OlYvb|-2^TbbrYCW)J@<~Q8$4_Mco -7r6?GFBRMbu2Pf<64Jw@FF?i5i4%qi}JQuaY5`(P;hpq72m$UbOA9|Yd1R+qUyyq1Okf~qo5h`22M*E -^M!cV82D?^t)uD=gGq^IqH83?lDcOWifEea#^D-XYgr^WMSNUGvHxb=SPtw|3XOs&`GG|C)U0e_uZI- -l5s#wg0>Yn1s`tD|ZNt5L_lOMsS6|1i@7TQv}xtGzhK}Xc4?iV20oZfjNTr2rLl%k-!qc`vg`9J|J+2 -;6nmy1RoLDAo!TT7QrVj7&-BLN}vGJ|obH=W_y`c)lReiRViKop`-Hh#&h*Bff&!#BLsRcK5mNCA5jYbEu7souVh_!&3k)lQ&@ -kb9cJE$+zcy!glb^q`Mge0|6HH%64bNw^L>JPj(&bbkXLEDyVeu)=UIY!T7G^?P*2Lw4+!ch`S~S5Jt -2QSNl;J6pN|mKlkw-H1oc$>`4~Yx5r3W`sHfrQO@ew7es)u>fg$DRLj*yfo)gpu>gU4*L7<)x)DI<}z -YampvQWQqr~7mt($Dwkiy@>x5(FWANDzedF+qJuKffTT59#Mu1oa{P{DvS1DL11J>3{O4`jGzTZG!rc -{!IB0`+3{MI_YAabg}okhwfS*#Gj876c`9%ol>#)w&L!Z&ZSuAQmk_+*0~hxT#9ur#X6S~olA+%r9|g -a66pUVltjNkIztkjA&JhABoLuQr-ok(wY#QsBJp+xO_ORerh(2#HM{)R-FMeCyV5}0Q%$Qh)OO9MRP! -m-d@_MVHkwFA6UhYn&S)a}Z*uFdX_}ZoR~t=}!1|*xnkFXD)kcq*#(S50cTEqLfkjSZ^A^D7S&#*?Ak$fp> -2*Y=lOfaV2><2q-8H>{$h_jwO_S?Yd#-aL*Ex}gR_(ar%k(RBx=-uu$@Tg^*X#RS=U1-tE7$pz2Zpj- -&luJbF``BmurDs+AoI=>3NurG9)6*|odoo0njvqG=h3!QC+&bC5lTM?LgN}UL$PJ~i#U(3M -uwbb-3HN8ts?^4sd)buU`^e#2MOHJ=m)4SC4E;YR?P47z6yVCToG`%ZL?@H6V()6x0y(>-cO4Ga2^sY -3${r9moP3sFrP3uYDZ9sBw2eJ2k?H~!P0%`w4R!eN -nBFFWor!-vq^9p@^a6D-<)7euWZ-%vUI7$bN+kL)llzGL(OXGKPw;P|i^K6)G6=43h4exBh6OFY40%R -~-1}SA_LFdf@LE>$~*8uNmw6^uTWz>pS(pZyD=*^}ydV)_3cHe_*Wd*8{&}tnb(Z|HxS1vj_f(vA$~$ -{4--!kNuvpVUK;wSlwg)!dTN||H@d~W8V=LL;U{07{u?7j6wXqXAI)^17i@sKYhiV1*3kAPWN#L@xSp -`Lx}&(7=-xW8G{i22V)T8zcAJh$y*06e7|+@BH{hG{A)t`Y4neT^t0%%e?t(|+WQMZt)HwQReeKt(;=cpPKSsZIUOQut(;=cpPKSsZIUOQuDB^ev=v}xkgye^vU`%VHtd -GgRmU_-U?wo-^W*$2?ld1kY)-%x!`AoqJ7KAHVUt3xxIY8=t&LSRcRf8OHkfZNEB5SP9q(!ut5_9y&=F#P -8)AVKsc`1HvGFbCi?dQ;knkKEUihc~1Qa^Z)q70$~upwL^sU@!Q#WLm0&G+$>>z{KjvnKVc;>UZnnnw -ZM3hzcUA(QNO@?V07XsVSW5YmtGJC@jLc4SDdA(erZqlaesV|VUq8bh~vfkyhw~;vhVKbIOX3FJ9oS} -?cb5$9ixYV??~~Eox`vFJB-uP6AS(w*7=U>m;F03ryZwm_;=*aU%43g4)3sD8^bSEM;~ay0Jm&3_lPi -G)rhS>y>Gt^4&B%IQ?m6{mv+AhiF;ob_`CZ*?xXwiz^$w`UrO)TM+OM4Ox9&AB9&wdN-rueA* -t>7Iugd?ed(G>=xTzYBZ~RMs{Lke3p0Eh8SA_MW^7iXPgu!XcrK5KUgq7<1CBphac>BXc!Yp8K34@jD -!NY|0L+M_9ko**brdFgRqnbcGuOOPyn92x|em;9{`V7#&+73}%O8FA0M -Kvr88i34|&2{A^bmMjcIo0N0wIJmmkEOq&hQh$V)gB5!XSh*_X&d#&O9d!4$5vEzeO -PIHjeWLA%wF>2^6wou-h1&_-ZUUt1|iio4t?2hSFIK?qLaF#17byY+blW7zFv$al$NMCk -TTWpQ1RzhGcY#G6OawqtnL-gBYKlAq--CnmC0Uk~7>+2=SSD!XU(Ft`P=VdS=tbU_&xGL*p!LNJeLVA -`A|vFWn^)!_H#o&||_N5OXI9gFws?7hz{HU3#!gAVgyBN5UWyb596^NSvc6f#5je0TU$0(K(udKy)0< -&k_can7`{{uqPRvr^yL+Bcp{Egh3`{*#6l1 -pN0(`$1jTW*c!@BG(G`jxXpW;r$}$igM^`D6Ky(~kedc1gk!34<8j;=czmTB2 -zS3iOO_(yRp)dPYk$y25e7(GukY)aV)Ap`?a$h11&$G)zL7p3yS@K?vY74d+m(XS6~AgrkP8KDdWL09 -Pnk;LPD@l`tsQGrfI{G7wH5PH$hoM<9f7ow^1p_KfacCk#S(msTZEvuCt^g)j)=#u{O;p}$U4v%Hzxy -9TLupN1q5%}4h?5C#j5+w1&62+t#0RKUT+(Ib8^2+t$x5ICARdPHMHhS9lCUkaxaM~`T#g7Q40$HXyj -3%_>_qVkyd0|ykhZqoJwPAHC^&<+A>^NgNQ#NepnX!9Up5S^zq0Kj3z>Ft-4kZ@da^qist6?(c461U* -s;^;ZI6hil$hIcr@IC}n$Fc=zN-X;tp_mX-W4l!;mJt7S9>Lo2);Edwv6(u6?OUT+Wv64` -BTCk%~uX1?~evly0d@Y(x3IM%nh?_l(NLu)BG)i`=X=>o?ZM{j7!0OuMlEK1ORMI63Kmt^|x99<00>{>~v_w1j~}lw;oM!zs$~3=L9njBTGS_!Pw*j#G}$a9bb)@7$*#!HLT8X&Q -y$NagqxjfQZha(sqHc6eFE_$*C%P~~SlM*)N~Khryp_~{{n=V+*aNoTt$js{M@5Q*@!+&v>2|;85>pYlD`xQ1EAbk>o_E_%pu9pM#%*FCV1&1zuh;zD#2%yuD(4o -d$GxeZ}}H2_sN6XzM<0Nug}e_!@2Xpm5L@uZy8{(AJN{7$_bznxQEY$_I^RNT7lOLgPhR=|Ksh@iiKk -p@`7r@B_m70B+CGvqK@FF>isPl+bvQh6*SqG+w0j8k7?n-=HB43JQ&HklX+zg{I4Ml%7ykXzKw57wQU -)Z_#)Tm4(JPDFdOl(D*h5391W?@9^iqC0L%P-i8uG<2y8gK#`&G5~){EW@x-ZgD4al+ImQ-3#Ep}t0a -s-v7zxAWfGJd8n4q%777lH@6i|vC5NWV*Z4~z1%D*UK;5CO$FwR>I`37MS!{@IE -gBZRzMi`hu%g=5S2+N&k&j^D|ef5Yi$kf*l34^`gvrWPvQ{SE<39e>h7Z#PFvX!r<`IL23;cdOHXC4}!&T@H%0zX?#IE1V3-*&`HAJ@Y138gcX1hNAd6=P0v -o{o23h@1oko9IYvap!Z#}?mI>@*xHGdz7@w->u7Q!aGkcdXI5s%@oG>_ccH$giFi);rnI#ZHck&=%5W -15W34`=oS>)Eh7`n1_fj|h}nKy(%@Xk^m;jabW+$XGp&r$5asM~r&0}PmPJM)yAV8-ptQ|Q2q+nIkt7 -=-COaRpBYE)c2Vbl}!IN)#~hb}p89={;a!Ut(IPcTYP&l3nyy7-tdM@73uAmrE;Vj{SB)0Gdm -2^=CwfdU_Ix_am+fv}|c=^bHx6t@o&_rb{9xlXeL705fmo1^cBS#SME!r;jKQChfw=eM&#!#G%eI~ -zm~u>5v5xGj*7tE-e(2IG8hoxs4%28Q1D>|w$%*&fnkgP*tah?od|-p->FguywnS!$rg5I&{;0z+?Sl -ZHhw^tMkf5e8|vNpm+Cdpn!NQZV**o^e}X#9aNrPY?&!O-k<^J*AucN!+HNT_6w@(X+Jb1BZ{ju>u^v$tLoXpW$2yDU0 -9FZ4o!8P6{lsJ~sfq?>i!Rgv2jR{~3ZeQRJLI@X#PhbpA7Kl$^3{Ec4$_$Lb>AKf95W|bKO#)|dy7rF -tk6;aMUnD|-HMo6|_AFoxPS-xrBZ4G0l0?Sm#OXG8cuH0gbuFZi=?S7|l?-*EdHtpva~+`dk0MDPtK_oz$3H=O)P13vhMlZP~rfNwZ?LL(mdhST+@#6K_& -Cr^ogU>r_1i8E*%J|po5jKc}5?qD2F*Wb~m5S+v5(h@C7z&f0~qy+?6hm#jHFN1YBc}Xivv<{b;ZD1W -v-jXH=*5TwGNk3p6PVZi(VHCW>4x|W!dALibWC}13caPA(4c_7IQCfF_cR0PfNc;x#aQ7%h5zNEgS$@ -Pmio2&Mfx$f7J#~e!K8Cv|C{AD=?#|ID1m@xHIifdshts={DD%NQ+`UAk0rPNsjlL9|!QD$l;TA*4{0 -3)mdz}Uha0Yj;)0P9A!O;uas(>@NyGY|KID^v-R{GHz+?b_(99V^8%?kyU=&=}k}M -`IKigWLD`5g~{h=V=KA&fxBn8^jXazulz`!XQu^mtGJExwd?Vus%@R_csZHTwCEk2!UE#Ck#?;{V-vW -YU>{egD^chOBjro8}}a*2ywc9o-oL@CkF|GT-$t47)*Ja+!&bhHXjhyr`qmw>KJtY?j4~o1@~`z^A%y -B(%d_Gj6j(5?#=SI!q(y*ZB$F}|90QJB@BL&`TP=L5X85y34`&HPPdle|Lwlxr+^r~nS8GF?` -3*0I7EG3I*T$0d6yhzNFaS1XslJl~3KrTIaXM-N*j)!}qEE%{M=M^Vk39uYdT{cfbGbfiK9HZyFr<)t -7(#ul;{U?muJqpNad=)cvP%|7qQSX6`?8_n!qdgPTR|il|``wJf5hMbx&48W&OPB5Gbl?Te{>G4~s_F -Q)dz)V`S77gPIUYF|w4i>ZAHwJ)LeB^+34UqbCmsC@~wFQN7&)V_q;ms0yuYF|q2OPP7pzLeURQu|VB -UrOyuseOjpXQ+LK+GnVJhEs#uXQ+LK+GnVJhT3PTeU{p1seP8(XQ_RbbC=p@seP8(XQ_P|wJ)RgWz@c -m+LuxLGHPGOgA289Z+!f#U;Wj815ir?1QY-O00;mj6qru)nInLL3IG6`Bme*)0001RX>c!Jc4cm4Z*n -heZ)0m_X>4ULUtei%X>?y-E^v9pT5XTpI1c{qUm>KJi!+zF-NhcT7;K8|URz**-J;hQ?1ye)D7G?D$F -@>Qo=pDzk(6Z1d7Jb)hc$|su|!cM^^hXzK4G)V87ovNo0}`v+U4wL97&SVXp~f{xLwLhWa}-Pv9}esO -Qq|XY)Y{f@T0Memv_=Ay=6$paDmB^I?^#@Fj=3xhvprCctP-(eMO -&!l0cY6OD?oUMhP*_9T&WD9O!N;bygn_BwWwN?dkq-ognqv1E=DoLI{e@=q1IYZ8`L9ldeR4q`%@r35 -B-d-hHmN9&gIO7G0oK9$1+!tbP*^3GV*diNI_6ycpg6cibDI0@;6;Uzn)@0+D5edS9_B5A*H-G&#wrn -9hx4@+`VsGJ%vP3mb!e6ACnVs}YXPQilN^pbPo@K5hJYRE=iwHK-u4ulK@*y!!vOG(uC+ -ylHX4Ea;GOHZD-N-_)rB*ewE5Qt}1#>V0xsHf>004f24=dH)tXQMKL)yj)jr-lgflirG1Z6leI4{69Z -eF*Qm22<=D{El_o70-UB7jKM7=SJc^Ga)_!QBAXf)y19goD>H1?p%IC!{7?e9%5e*6^yLebKoNti)%4 -aNzKwaT|Hp117=EJg(VFRiz>0669iLJ<5R-zObcxLt*grycWR7sHR)`k*s$@2WQ@gwQ!7RcSH=^a06# -&gD(x1yixWKQadL0%NMPsb&DJ35>G9jep`LeaG7J1HOy;d5+;=-t^wIX-#N#e(sZBt -mKDia1&_m{!FZFqBgfy!ZE!*v6%CP|*KKb5|9IDY%;?az!iC5YS1=;cmmgZ9BV*)d_YDqD!-iCmImD{ -i>8I_(IiY~;1f-PmYyMds=#vf(^9=HAo?8aTQ~=H_9m8)5SXJac~&N0J4C!7lyW8<;3Tn -81S(wDX4W@&Q~?wH99oSRYSgIE;JfB4va~dF{+uL<^8*qj?IMM@8sZOs|8_c1WG<|9w;sN@z^A!{mBhRo_4ie`S4_W#Ee1Q&KoGLCd`dr03n|F -ixt0deNlpY{5%t;{4COu+cTwgc9lE{u^uWPy)4>BK?RfT@yu-~(wpoeB-8KaP3avzO&X7w=OkP?8`AG -?vRRiRZt4Kw83tkK7sNzM~Ri=~Ze1!&(Kcv$8J}M8x8}k_ro(~=a<$({#KG^LMr0<>bkm&A$!O%T8hG -W;wIfi$eJ%Z#>06E(o%yED4>Dt-;5N-dR9?^xqG~#*KBX|>JM(otQ6ezk+jbO43xAQqr>h|50KN91cv -~V%|d$n+QLu_H8f@yh|Ts)|cli*SG$=|QI7A2x6Rf{j8uwEI6IgpLWM%p}2ji{DWSZG&%79SkUa18EE -{FQX;LIUiTSr>!mZ$Qy#c@PJm7l)%XK9}rz8CXz`UP~})$x;A2ahyUzBQ1ho++pm|TM9HGi(iT8c8u_ -{$NM>ZHpSJ4ZVhrRyW$p(vDt#=g=$Iz*1cXv+&gL?r^Wr6N>|LPLf}IcjFmo}7LdcxsYYC)uzgI_U*; -(5fk+8SV=YR_t$?anF5_LJ9YX%So6N>1(qZkohBBmvoD3XyB*P2T(9E<8!q8~apM0nW@#K%3^}E9n2% -qbL#8JXM-Sy>16*|w6QhS^q5&Pc?XzmFB{04Al+BIg=hXogxBKHWQeNse -m^bN2bA@~jefZ>SL{MRAF&HYG+CaL$?_cj!dyDfr@iw}26rsQktnA0k#XmqT89>pS&Kq0W#Q8xB!Gf9 -C~_nUO?`?X$~m?H&WbTRjR2A~ZUmf17wt@C_vEncTL=MZSnjMf$vQ=x-3AARJLUxZIaG3F*$hfQ{O!o -uKiP<=c20PujP(VMtNXC534`g*E^Apj{y+nNN7HiIDb|}0AA6`i^QBv~9v|#-xLThs5^S{fZM&Z+3<| -C*3CJy#uk7x?)3N=x!wBm=jIU6iLjIyvOCgloVdYILK|avN6^Kv~(%5K9lc9uUgkY}G?Bbq4?% -4c;J-c7}r(fmzWyg8+rvWvZ9}MT`N7CT|Px^TG1hN77wH1sOVv+#o29cXDFv`ab1D{Gysi9D}#zs5iy3;X8UB%uu6@rItMLotd#LeusPKIZVs~*1xg9El%nwNU5^N$0>Qemab^+}yEUTdYspdaA -N5Z>UQtO4Yg`{@z?bcl%(vAUe4TLx9if>kkGVquccCKB_14Dtk2xE^jDMu0RjAi3y9;jNF7V4GaQKjR`Uodq%RU14l+fZk0ZQe6 -M(*=Mwaj&Sk>8KLB;3E7F?DH&=p4PhM?cGSoO!VU5I;p%hPsuB(HpRoCy{op&~mf>0VJK@`>QAnUhpM -ej3o{G>4cu9vy@tBIcz3Cw=fhWBNv(cx~U~$L??jUV(4XOnkKH#jxE4&7{%7v_?-TK1nrAxJ1gMed=* -x)x>Jn(A=ziIf=fP#ciWZic!ViC}pRY1}~HGBz(y`7e2fn!V_+ahwKm?VoOfK=|_wxeU7d(gOp*7@IR -U6SMh^VUf74fY+-A2INNCL2f*n|MI>Z8jc$jrd!Q>_mBFE1Rz%ig>`xL!xXbNNngK;kQ}Zr#-#`-Y3ano{)eaIe^2(79B2>W)3to#$HNUJ+!JO%?CX|&4zy$64BBDr6^)@yH -Urqf4H!PZDuXzNP#iwC9uM}I2;^1enxmaN&S`8A<&(a?J`vi8U44KZj15Ctl7w@J{=Y}^Urc!Jc4cm4Z*nheZ)0m_X>4ULY-w(5Y;R+0W@&6?E^v93R?BY -NFc7@!D;6q(NT{nOIR>zc7HEI~c^n#`M}f9hHW5m6NUDzizDrV)t(TqXVw&O(XJ=#5g=q -@B*h*!9EwvPt!PNf`f_mQY0-LeaL6F<Mb=x?@~mHiIW=?B9C$xVL}&iWh>Dm6-+5eS -QjsEZh62I0%B3U|PciFJB!n;kJ9lw=OyiLpuX(wL)5IBrwQn9aNf^YhXqed<9!2x2Vh>WDbJRqMJAgT ->N6O0AT^<-EA(lEYJd7#vfca1GBO8F`M5pK?ECqexnR?BKcNvg^@;)&zz{F!u&~=O*Zy1YJb}ihNzb= -B&kaNR$*GQf}{?2%P>w>LTZfSHUQsOxHWM9{97rhxk02^o8d34!2I8!G@ -{K8N^D$P10FED2ywPPMo2J^>H)SGO4vk+~w=5^!#U}N}ifEndil23BFDVz$4o)5q5a_4qJ8pKi!n(#T?4EfdaZ4jj5YhE(pRojY+deBZ&#qMieGY3NC|Kg>>39Pl0& -WCH6jIK~fCsjFkWnDH!@0p0GowdAWmxM^ ->jCT-H}(XyC!#-g!jn+LC-j=-Qu$-yli@DoEi;?xRHv)gJU+qBy%2>L+7^cSN$~#vJioIHU9q)5+(zd -Pr0;NjMXdl=$#t*MM>FiVx31{+!pwEJYm$@gP`5&Rt0)vm7oxM+(LG{&n7FzmxlX-IQ9O1fv^RC5tqp -8-9n%R#4NnfYZxVdx+UTLpC${rn1I>1#{!CbslooWkhXN;d{zyyp)*Kzet!M{Dda`+SNAnuRNm=2LsM -jW-qJ9aib;d4|lR{1fojKaw*?YuyNuGOkOh){2I_ch5$)v{r0Z>Z=1QY-O00;mj6qrsSCcJ)g0{{RR3 -;+Nn0001RX>c!Jc4cm4Z*nheZ)0m_X>4ULZEIv{a%^v7Yi4O|WiD`erB>f-+b|G*_g`_igxV~4?Q0>0 -ZL~00w?X?b7{e&BylAndRFc=`zu%o?D{*XR3)4#^=kB{--+^gFg(pyR_R@aLQc5gCFVs)Ve>pOye -j9gWp+IWyC%!LSUl+l!?51L#b+fuJFTs-zrDSMiVQD~{JS)&=H$&~dXDgc5BIFX@J6k6!DlC<2m$OtE -?}P}VMz&u>E#IEYsFR^K{zy0JU84+OID+bwCVs$*esj~EAtAulU*QnGIA*UrG#`2W)yr*aD$ppu6Sm$ -nVFdwX5c~A%2=}S?-;%1F#a20D~c4UNoQgcTRk_J!nO<(ylFpUX1bjy_ge!}Ss)eQSoqdY|8IccvdHO -Mi{buH_T`N@5?CDoMXz{}{+&4p206GY=LEKm4-0yKf&KpN -6QkF1iusx}6u!O6%mmFmrR|il7o(()=0$F}=4 -DZc_J( -etuwo4b4^DRGe=Us=`ocI_?Yjpqi?Xm)76DJcR4&Kvx_n{G^eL%&2iP?pfjA4@q3QU*bZG>hFS+oj>Z -M+HyH)u{p#q%Q($el`@BuH2CBDCJcJbD|Fru`h)gF;#1=|bYXH|p5&;1eQe=9)YX&}Lp{gp!>bnlym1 -lC`8QW*u4*4+OKda(b%SHVG8$}TCFEvk>rh75W3xEqY@d*$toh`QhBb_0 -Lm8t03!eZ0B~t=FJE?LZe(wAFKlmPYi4O|WiN1PWNdF^Yi4O|WiD`erC953FqmMFA(sK?;U2Hj-=?DQLQ#>Ef{lYzV@aqG -Xs8p7ZC#7ge*%1!be-I*m)*Rw`Za+%1&6XpGBkaI4vp+r`YO^PK4SO^pmrS%R5L#4->mu#(GCki -Us%I|<+669FLfQmX{KfdRw_aK^zo!@6B?Fi!GwB{lG4J=0!>v$HcN!)Zr1`$iQ48QLFz&Ngxnd`U$H9 -s+#YI35|1XA_2EPvCeot4L3;c#dYa&P1oTstks}hmrydS;4SWmHU>NYUy0ZgD|xenj&NZ@y&R?;%kG> -o`@C_orG4T@C~F~KSuSY;0jSM&s|o>H5KlOs#5-3i5QIy%AqBsYSe(|#UKqqwnRjK`;)LP^wInn3AXK -0QOBoIx8Yq22)(j~j*%8$kZM%(Fm9NTCeA5j`nZ@1W0}mF!@-r4N^5Zce9aajPrb8*XWH6`p9692#h7 -m>ebO%R3`x~&VDyNi46z-#{rz&pydX&+06tX|;?Reo7c0a@i>G_y!LZqEsw**@1(Xd6jIx5;WFg3(YV -xuw=8)HoRTyNl?C2vJydX7++1O`{l?YYwC@gx-n4NmJsvW7N4%?la5=~kYCk&H7u_ds2cQ-E|Jyi?gDMr5^5nvr&WEi**Wz&5lhSN9xh3>kgH&28xjP9^@s{ -8x&^&NmzO_XKTNlGl|wQpS$_1Ip$ -^&CM;QM{(bwBpyQOT*=i0W>(D@x2ja*SU&F|v0AKB&1c5%%G)a|}faPL0#WjuWDMWLQHvmWrKH^{oVu -gjv(gS~YkRl+HBq4KS+tHozKTjBQ2a?sU*(`wxS2Y*4r+E@BC+5vCZ&|Ubhs}A_NZ?~a=b=iH*3bErT -O^S`Fh{Fjlwk30Lg!jZtp9SCIS1LFj>yzyW;!0abk?SYGkg!LuP(UFrp*7 -3X=gKV+{5OYbuAL5)24lJ1H{JA;baCXj8Wf0(B`TTS|(i}Y%ot!=KToSL7z|?Gpsbn^5@g4Muz5OX=8aKqW%coZqe23<~?C5R6_O%~0e8IfFqk`F@h9S>w=% -kN5=zmZpzG$E%SATwB$I&<7!3kAG_^W4K)Yst?*N3_)TOchs*8>xyMFlM<=*56zY>r+s1!c)hLT4z;4 -*B`R#r5@pC=-QtN<|`JZk#|t%5$}HVaD?tt=W&a{HgxUn}20&I3^aG? -a4>N3__tKKP@idia7MP`R&1`qnH7RN6H24j5$+$EcM|hL7Z1sa%ZJP5#S=td&G#kBl{gvmH*l@6pleP -RcN^)gHXw7{Hz&D>1dYi!=IpIg#TTA;|E(jxJi2wF&z{d~&0*a3x{$1Y*Y*eWl1(ON!hu(-90Xck#vj<@uETgVB%6pDu2~`;Yf`w>N(qH*^Jr0gxT3-+ue;przJ0Lkj^-P -cmo@oHQWgHnk(o(hhM332lG1?=AJknPG46g&E-svH`nNy2cRC951aVn5q-4(4ctB=+E`@>G5wSHUEdS -D?%r&Lzp7m-!ai$N$WUs!t6=`SMo{FeQDsxJA;d+*5ukYMMU0({lwo(Y#d;@`@N1IUqt(>?anfa+j|1 -VcLG~C5JZ4B=dI=slOJ{lc-7SD9oVw6hE*X8)hMYjbf;!4w6k;twGtse`fBOYSSX)#PUs9<=?2c|iKt -G#0=+t=bal|KhP%ec9b@C|-R14{h0M0yfm3%l_#aS90|XQR000O8B@~!WyM^$*2?YQEf)xM&CIA2caA -|NaUv_0~WN&gWY;R+0W@&6?FLQBhX>?_5Z)0m_X>4UKaCz-mOK;pZ5WeeI5DE-St*YA1E$G_lE2qRon -*c#k6arV8^%ADUHA#8B|Gq$tb}$hR1j#ndlwKKb;WW~KmgA-B@kAAY6^5^^WB`2mH -Ue%zTKiCzDUenpKD{lq2EV%LZTL6CE*6#4YW2d2S%e9t&Ut;M+U9{viYsL(UjU_`%L;#xsCO0nub4Ff -BsLFrN4!nok9+5G7VIon?^%zDmf3>UBc&+C%}x;Es)Af2=?q&MiS}bO>%c=ZH#$QrX{W@wJ|ow3TC{%O08$-ur`+p7?bu+$=rxfz!{rHlqAZynl~3C*H9RvtSZO -|`67Ct+os7D2O6IF0?mG?5A8RJT5p+*aai2|5xfwF3n<`QXZFyreYm#xRea=;pnQzX9UH9_aV~@hec^_DV|&sBQuZHuS$<6`Wt)i|(?PNcQ*J-NQ}XksaGh%24wc5RB-wE0mdT3KkU0 -j4r;&!Z@QH!0UQ<|rf0dP>mJj%d*|cdV0%coRWSNLDGpN@JB%+RMjoAeo|$eu>Yky=3xrpD3^4G%vRa -xsAi*nCR%lNG2ym98XN_W#J6IzpMelM0^y0*_SOAtHd6*1|llHYp`AmUVuD~H#$a_8k@ILOh`Dp9A|oFoN3W;F*Z_7XHh`u032zczr?|%=)g}pC_!F;7PY7 -NuEdW!{=77VK5%A?WP!M0X<-wc)scYr>uT63XC -3@s#N8Ma@nW&Rikpov0NLg$Pu`ATXy+r{Ey3We)}rP`b8=9!WTtCZ_hY<|9FOiaMf$a8;xvBG#N;IH< -GK^16quoj$2?Q!Q=$yX>%EYo&`nxBQ&a9{aS|hmDFs_?D?B1ER7fsPRI_DZ-WBK3YS4z(RCQZwF?Bq^ -3m4w#pwj6f90rfWD7;W+i)i!F9l@B6ALytSz5wS55f60NFr9O9&#)C9tO&d9iUVvSgSmI3lgLASds%{ -bSA&!84oybgi@43E>Mj$bOlFN|w9MQycdz~Cncdpw?)|;Q3Nm(6!hbGQe5@9xouG*$+pb0CGPR -+24dVD+Y$*AX6X*QicCe=%zMRg>^i%j$2s}?@A;$I_ct~#_ojyq`Z$s|ivPa)r%%5x>e -o*Eq$BM$(>-=aPax}UxMO*II(T_Oro52xgTLQ1+@cii7orVBKLZ*y`Wuuef{ih75@TIO -9KQH0000803{TdPLU%C**yaQ05}Q&04D$d0B~t=FJE?LZe(wAFKlmPYi4O|WiNAiZER_7Yiw_0Yi4O| -WiD`el~!AC(=Zf%&#yRS8YH5GjmJp^fy4w7FeE1NfKcS5C#zLAF1AC-zsJW;+~(3wv#Qo{&gDDbxww^ -*TgdaOF^$4J2fp1&Wq=i0ipF5>pQETdm%85>zQri2NImPdk}tB{d|(EpwYa30PVGtH)&=iBd4;$%0 -|pvrMm&xN^Ug?b0%65H58LmlJLdF1kQ -1S^bVzhpmQM8Xyt?g>~iZbZ>(RtXDambvE4lqaaS$WA!SWzlE!tLDdJ7F9WV92<+EX##D+!r3-u3I=f -x$jB-*)!Aqdb5G&kd1D!#J`enp6*laiW57<#9hyGh(_V9&Ur8A&tKPlfobsp*P+{4^F^13ppRC732xdQ$Op -Ci>+A99$kre5r|K<`2Qw*wgVniqIua3&dU0C*(C;U!9#TSK0Nbn`T2C_mFN%Dr471KkMImzZz0#`M@?jL5;)&zI`=${yUY_D?(?2pQVhh-5rtx4{!AYS5B_gL??H6`9|}t&*(qAs;t6 -MVmNoTZlpPJOz`-HF5euHIaEo&Nh@Im|=1nD!*czvchJaO}YMqmZ<#Og~3xEo%w{eJO@_Qc1jlX=kHX -5Ekch|=N+-X|>15ir?1QY-O00;mj6qruu>Q|wG0ssJw1^@sb0001RX>c!Jc4cm4Z*nhfb7yd2V{0#8U -ukY>bYEXCaCx0nOK;RL5Wf3YjJAi(Rv|51Kr4DcPys@~st+Vi8z-6Fx^?Wx4v -=JPk7XM6y)ZiHG+p|^_{Z`i`<7CO-t^IxbMZHktb9j|Y^ZAfSZqV2S?kj(uFcvoX*T?h>+hb%_dT1u> -UT#C2Y*uIm<7^@{Gg3~*DvFb3}xUzBjH_l<~Xd^V-^6_LcW#FFCXq5b5uro-gECJqGu>PM=Fi>-mA)~ -X%5p!#ids8 -$x~DO?}A#tj+wk&of+NhrvFU%q<%5abUo`vm%x9Npr?a0IpT2(^r?U69)PhS{xy8^mGq*;QL$AEM%Hc9B&@hyQ5zLy^rtFJE?La&u{KZZ2?nD@!dZ&dkqKuvO47&`~heQ83fw0sv4;0|XQR000O8B@~!W;;RoyV*vmFAOrv -aApigXaA|NaUv_0~WN&gWZF6UEVPk7AWq4y{aCB*JZgVbhd9{+ePQx%1hW9+hQ5MoFM51g+EU0B|m$E -QKu5*%F7{~IZEpN}sy)FnUB)r)2|DVfuY%ZC#uvMDzx7CTGcA{3vc$z&#Kr4>pC2E75IV>`Otwk6;Ae -42;3UD=$f$xDy2{_7yH$qk5+ZuGrC|eXoysiA?%M#7Amom9%qA#t>6`td12Dy%9z?*# --uQ5KX6W_0W<(uot$B3Eq%#ZUB-sG#n%iD>ihU*WrtunFI4^ZQGM5g;rWCM;1M_o%>qXoR|?j)KG -~u%lbyZY&&{6piN9sAk@6aWAK2mmD%m`+?QZwEgc0029V0018V003}la4%nWWo~3|axZOjXK-O-Yc -FPDY;0m-V{0yOdF@?obK5wQ{+?ffRVrsGBda9YNgQ32tvGQuuFH$pac*WRo0BOKvbmN>ElJtdUEP1b? -gj_~1V~Y~a+8@|RV9|lMg#qz(dY)iW~0$~>&<3kZ*s*J%b2s1ReBkPESN85e9ptvO9S{BP1!uTSO8U{ -(RlCx?nZISl2xL$v?F}9<_?-N@$A;`IUUK>wgy|? -7-WB4~untn@EfuESC%Q$3yN8D;X;IoA5EdU -e}y9rJB(PpzKyl54gvJ~Gotvk-@CulCc#7on-2@ti}&SJG#jdY}TB$J11@;QwG(Bn@mNLVt9ZWwgR$? -9a)X8N6}mp~OA#@vY{C2_@DLW?mqd1P@y1Z>(Up^my{LAR`*(v-v{Br#M&C#i@T$7oXBnq%* -H2yoEq*_N|KaK$TARUdG37<{dEE$e>b~F-F5YTD!3Vx72Ya&!?G_E=0VKZs9DhqYI#e#=E&>3X8qSmg -hWk_^`I+uxKixMP3n514f0h%_O^LZ4nt^X_>9s -do3YiML%#+<|c7GTu!Gve!HB_+VuO=GW|4t_0_!(uTHc(cyI5) -gR@^xjz-5HJ{-L{esy+aR!H}VW3e$kaSu4!4eg72r>}mt@9l|uhsPi7d;8+v(JyEAy(bSIyg7RN>dX5 -x5asok)6Ykv_s1WO&+t~S2e^?vnOyS86-_83n7&|^K)cgsFs3^xvFq-gIGi{-fraR -V*LIl7KNZ^OCriQh!>3~(%Fc?N7A5>w4iZB|J<`LDvHu7i?K&;$45TS0rQB?}o`yujRX~dQZXET1`O; -$`)!xnLrMw4iUV)i@F+AK;hd3+N9Va#`9FOUOKA`wO@PSfU4x7(!2U>cRoAL-ziW@8Cv>VicxKWh$*U -l=Ohgi~v2kg-R}zq5vAxAIRTy#zF8-1C#X6!PVWLPE2|ESTUGpaTee=7oMW%bQ>}V`Ic%9$iBL=vI$S -mnk}C(lKbTM|pp?E0~=w!!(%lpM~f-9`<~a!wqdlAcd0zW{mv_MwO;cNXT+i(`-z<5c@_#MiynhoTb4 -o+IA);*oLTE>LT)&8YoINVN`H$-e!*H_~YW8uE{u_&>I=004U0_* -WQxP#%e=fcadMI`2FnqEqaegqgLBNi<$+Cs#u&4n+>GGo=2_ZVYIxs>`46!rdll* -Q92?ZuxFaBB434K6Pnl7%WBFUWjSSC)0*0+~u!#`F`5jf{7;MpkPl9QHdZv0XCgYVrP~y#CwEIGzh4Yh=wHhF&!8v -sy@Rh(W6K_1@9iyS>0wKQqm5Q`=-eFA`4IY?{0Xqk1n!Trwd)@v%IHTUJoR=hN%c -yc3!2|+^b%ajXKo=H*QJ%?}4SGhdD4J>+;$o>BqEFcbq{`)#H2dn!#E8Ptme{*l-wYtqSkpLEoLlwU1 -NMeB$#jWmlwv`;({BMsFne$rR6;Sk3DQgUK(PwJ`7| -5psfoqz~f*Rq${a<_<5WO^jMlJuzSJe(zN~zX45zhe1Mu}Z9!=&Fit3_)^b#hE~Drw@6Dglm}+APTAo -}+OLQT)Dw?C1jeok82*98VJX$7DW1V?Z@H;fuA=8n$x5#-l(Le*X5kwX^+5J6k{AU>>9ig`U_sWXEs3Ak`)sAI4x8W(E&@fMf-p&K#Vj -&K$%C8hot9Bg{{K55Ez3~0VFVnL8J5Hzrt9T3YNeWPs5G`;ixB6JXe-{-T|jI1`7cgCebp -UI3^jy01z6@cxcHF_rN0$F92jf0jPAN-GBUeueaMPK{W_#a0IH_9BI0#!IS7}bA{nlgC`C;Cr9a3c{u -naMA9A6hJxscX~E4H405CE@&VNZ-$qRz(qc*;b&xxW@l00K3_&0sobV)RMy*Ynm_)#7ZNnU`fhhu*<1 -B7A1I8A*^S~Col*x4y-H^wR`(`jH<58fsm$k;w)+^(x=>|g~li3l)RtKM3S&v+ma*$|-Vc75OKifapd%FLu -%^s5e_jh~lB68&GMcjmHK(8m2qjWtXie(bEG;Ib=Q|_8{y%8}}MM~o2?9DU8gFIcnRW-vORwp_g{&4FCvlbrQiR -qI(U7Wy?E;-4v4SVT!6+#2ZAEC3SWu>T~ZAC#ssk%nygsjU1^{{WXEDix22orqbr=uqQxSZfxp4dBCtT -9o(V24-f*e*xwpr>w9u$Y6w}h%Jfek)HkP{du4&N}<2Hy8(a!~ay^L-|RVz{xLmM8lx62qcWFEx;3VD -L2$U&G%$(re7(g`M$O*rrJIvu8P`~y%^74CE184dAp#J#7h9oOF;QDye5kbr+KP)b& -IYxaT)VMYc{GlrXol`s@vfSphHAbmP*@X(d7(%xJsqTzUkenOoD6OUKM@3Y)q|zsBxYVTF5k7d5}esn+ULQwjPl;rm^-Ch)Yw`pHnxb|KZ+k)! -6B>h~pxzWPl` -ONUbz7AaFzLj`#AoK3fS(CfKnkiYz`qNPEM`m3;lAc!aXbA+}>@Pe`SRDB1;Gp|JKHkEM;i|kMxOUW5 -Sze!M0e>16B&!yzL@1qwCV{!NRggnJ9+TDnf?4XOv*VkvgKpJ%55DD`$b*nae#BTRVRfJ@91~UV1 -sm2I(cl7TM;JH$!+<`iL~@dpRr)MIp@tdkjnCRTfT-4Cuw5gGxAH&4TCNHdwR@S^z}Z2?k@yLTu3*?43)XibI`+P&rF#M+8-Iaa7QC5KuGJin<_#hS07UT -8h$Kpt48Z3m8K`8GT==`CaVPES_Ai^2&0JF5y49e$fVhpHG%4AL<9^G*gkT7PzKm9Dq|dLsGHq7kE(c -8y&C`qu0w39T(O>3^V<$P(E`q@JO8o;F$dz>yCR}@xe3P1%(t18f!~4d8uA6FQtCDitqNo{58kyzgYK -nI&IZp)^#yMs020K>fy&6J2}j!uYR_a+iYF9iOOmm96MSYBB40AuuwPaNxQDkE2aJL>ZDp$YoBdHNo9GLobr|>B?@*sagGUNj2q;A7k -Pe=*`nWb(HB?dpAcEf&><*A?8Bi1x}O5SX3JUj(>Pwog_- -vOSzM&Ee(uF*D7rM2$1fne)vw;JC5?-$;ucZ7GpCcMX6fOr4*3-7Z5x;qg1v4b+4kA69;ug34&Wl--N -j(65Wy_p)nH*l8Al6S^gmRC5xZ`;h*;rzY?=L3av@9Euu-m4At%Xjxidhhp(^mB#uUjJ@LKdF!O@x76 -L^7}>FbH!05_E$1>S>WkbEwItTaa9WsYPZneLJJSvEu2)fuvgwO_wcZc`u(c<`?V3;u9IT4(7Eluxt4 -8ro;O-J-Hw+1^}A`A-`!lx94#Dgp@D8S^n2S;!^`ichJJrLY8ZSsHT3tkqlW+ZermW+Q}Z;qB|oU{&=v$m^Kf@XhfX!bWnb8izg`}an3&yD6&9nJlFqPf2{H -1{_}^T{S??%x~DCvG&K>1fuDEKATl*czG#o1*!A6EqL*jplPVng@p6tQ|j=8P2EMvz$*i1+@2UBis4( --jMd5xiNiSx~^2*Dfo@nJb%Fb2!Wh;N962T^B%#(eU>|aSY*d{M7FrrS&D3(Am~0Toj)wHlTu{u9kCx -&Yk#wSIf0;mX9Ry3?fi`qtX#Mdg@Tl46Q5yZQK#Q -h-nIh@6eh(DB5b)ly!xc -f}JD>Y|_# -~+={9Foa%&w6S5V6WEv+>4@g!ZNpm`&kK{n4TrFh>cupvUL4{Ajq<3V%BPKdAhc>%*e_4oJNadoT_l9 -60)IG`1q6*FBdqu3Kr7Aqy)F(2smYw(8RB{*1=RSTQ|i*<6J9ro@JdfvMY2b@fkbsq)l-bpYis#p+;#3OYH=1T!z@g|D>vW%Q6*$5B -pk`{b}(sChjoQtN??1Ml_-!=y20dJx!QQl4VunEdd`6{haeZ#h=-O -d?NK9_b`!)$W-fR%7UrMF!L-WJ4{3lndIC`rDg2<>dUCv~>n0ST3Od93cpP4tAEMr{I%D4$9Q -lS8yu#nkYQF=SOqn&lq$m^&Ex6HYU*wYL_iIPZPGY-0XW3aM|!Ah~ZvSgC9x(CK6^f6BhrT9_FFQpRc ->}CF}mSJignc-v!nQ^T~RyI4lEm|9qIjo(V2bow-A6}BiW~QsX%+8l%P{?~`Prh2h7#R)bF^CmJ{=pK -YsrgsoC$=@z&tmYC$o`VqaP%%LkgA>pQuW=zF6&*%G5cAd+;&zUIok~L7q}Q+o0h5A?XeY;gk&jCYh- -}ScvmET6K%{0hgoy92we-j=nW7dPtp;t!>2>%Yb1(_Mpl2AKR?}O)~h9k_S+`R*V|x|MwGas_>`bpw{ -cHMssT{Ys>7h7b>&Pijnqr_&@^Z%!;gFwMviM)s!r@p1t5D-d=xqF#*@OniQ4R7qNvzt<9=ZeMdZVAk -5Xc8XM~V0=eY5=+ez1EN3e;Pf8fmT&^u9Q>B?eNeS3xaY82^LHc`C=%iYc{dqInGi2Ut=1N`a+$Zc2} -5{<(KjR?n5GgP$Oz3uCj-K(Z?RZS>|s%S`0pWja`lnWo^sFve-$W9>!HvsL&JWOf8& -QuDD>uhG_vaxNYhyz^fKj|U -%1I!XUSn=BH!rfp5^S_vx=?1g45_E!cXJ1hxvDjD_8MkLQd_VBKutBZ1 -=z{RyO?{~BdWgclN7q-r>rfY+Jtio#w_1_A|yRl`69lJ_*#Q!#0HWv03|BVRBYyBL| -W$)xcP>Ezp7v3u3)T=kF4k44+h#oSZ)v9nqs8*-m*$!y}%>{s}CFI7;avU2pxd)!fWJne=bm)F1Zw_A -UI$7V&l_Qb42d&`(TsbuDO)~Slw(@JKJM}4Z8J*#BqdeEhcTk*7k#;r0^Sg*q}MtIg{?;S(ItmE#rND -uv>tQOa~i+Y@;AN<_SxlTMrvu~}U?s4EgYNZYSe34;OZ$~q_#@?>O?AAS}mb1hvzXsp*bs)COdUp`eH -ZAxdynwiM$;(M#ty;airsghs|j(2URyWnu!FZ^(N+SY2{Yw=Rc{ADwbVtS^Hz;0=WDa;kU -s#7;+w`-39(1{rrtz)&59`%dj2_D$h7iN_Z>WaSSEr*bLX|l0sT3Gk3*${0-4|q6Urd*GYaVXBU3tdo -Jfi!)$j`%qATM%Xi{*c%>PMn11K1!Ki>hecjke1oKKN5=8X_75tsPX@UB8X@6D4ZAg;aA}X)0Az385s -_6PKqA03t!DJXT)M3hF(2_X?O8*$Ib>%>rAQ$eDC$2xhl{DGDV)h8?n0!Qo{8YiBl%EnD~F)b&v+7YcahY>>EqRQ7=C!pqaZ=ZKT&d@y2 -9hM=#;m(IYX;-km2YV4*`M$~CJ=9;(yTS^qNCl|ZPjvSO`rvy4&A%_u(>g%E!8iD>$60q-L+OOB-}5i -`Yi$PtL(b+^n_c6Z>qP~nLB~~fn5j`-+mr?AwAI)uE$Xc|XAbr8it3|1QDPiDq4(p|Hpr%hI}~;K&)a -rYrMg;MNxf>Wci}Acb6n)?exA3Wu{)=pxxBn%b*Z+b?C#Pib@V~SV5?ZRH}SZWrO7&QvcE|OZn~fSWY -d1!bo2P>-MaHI2ur&&KO$cJmKHtKP=&=2+Ct&-0OS7vP)h>@6aWAK2mmD%m`;BT0T+k?008I$001HY0 -03}la4%nWWo~3|axZXUV{2h&X>MmPUteKjZ*_EEUoLQYbyC5K+%OEi=PN{(y``DirI#%f+V1Y5u$01- -UP>{ZM45;>wvpw9{C#aF)B3VG*yu%1dV1Ck{Fo`gLZJ)95=`11Af`P=DvZ)gj~&dF9gDPZ(2kT_mby~!~QZt(^MVMNhnLf= -g67Eo%65*d{OTD)TceGbk3^?gkOENmPMn -Nn2e^jXA*4~N5rWRB-P8GG=n*N3V>Q%#jAcMtuspx41N_~fiMBb+2ola%Kso4qc}GdL?u=a^owbl>OC -ys_{;g~IE}mBRn*$QgNM(hzSfSYEGOva)M)VGrT{c=!HYIv(IQv~UamWjB8TP)h>@6aWAK2mmD%m`-I -I5kwRL001%q001EX003}la4%nWWo~3|axZXUV{2h&X>MmPUtei%X>?y-E^v8GkWFjDFbsz8`4ytQY!J -fOeTRMYFetlp>@tRN6QvQ@R*>aF|NYz~jq8heAIYyiw(vbrgq|Eij$N`29JLrCc924c0y-clijTnkBw -k0`=CGg3YWP8k#G%^4bMsV#4c%1scKEo}G;lzi;QsS{_jTAU4a3BQS!eG&f#ArIl;x8BdKPHej}8@~L -$%~$Q}x0gjOmjmL1TdY$f976kzGChx6W3Q$samrp`PoaX~x!DChB{w`a?3;a&`&H8y$-@s~L(BxR;>RHfjtwNS93Rw3 -j=x+H8|EhmbO9KQH0000803{TdPG*YQH!u!lCyp%6-8OQDn^Cib{1Doc(u-qe)-_l_iINlL4Wq?z~Ld~ -V>=$wU~8C6K$H(SkJ{xkx?CuBQNeB47$*1V7}@n+ZF)?h34d&lIBZPHkY@GU5`IQXjwU;E>+!xfG&}8~oR+Z*VxkPTbf3cVDZNrNtlgdJoP&plA;ul)VBuktOh -V&C5hlL~8jrTSYd~zy0?Zsn`UjR8Ipu8b0Y!O7ASjSf^J5uQG63M5?OF_e~&_HX$S@WO!za<-%`YFg9 -iQfgWZPBm<$EJPG15VOVswR1MVI27dkCsy|Rm0|XQR000O8B@~!WYmXe2asdDUhy?%uBme*aaA|NaUv_0~ -WN&gWaA9L>VP|P>XD?rKbaHiLbairNb1ras-IG0UgD?<=cYehwSwNzw>R72mKT;`DlqFGD%YievGB&l -3Ncs0Q5ZVNkqAVTa#rS;B`Q`k24%B~#Q6GpKcj7N|fVw50telG91qlO%o;;oZLh#$Wt=2q55Ar-7E)7I3JD=Y;!?RJ5Z08yn5 -e0?$Qa17GM0evoKZC)>(()3axWWUF;rG+@_RyLsEOe;EG -2RS|{!9(Co}2|INWook5otEVrf9bxoq~#vSPaHD#E$8At{k2rcr}7wQ;6@DcEojxVVe;{lb$fvwu`Yd -(RxyW_Sbq<$$YX8TU(#~Q$8tTNNCJ2-q9L!PI8a^?Rum1pYZhSmgvVoL*RNN-Tk+dhW(f}(vY|XAB`T -zw2?;0Ursu4(9k2^Nh8}t!8cG#0|XQR000O8B@~!WBJ^Gp^a%g}Dj)y=AOHXWaA|NaUv_0~WN&gWaA9 -L>VP|P>XD@AGa%*LBb1rasr5W39+&1=ozJgI6EWxVav|FGUWCBdG(}htdcJ0(y>|h|s6314e5ml11<8 -{(+-*b48NJ(QmTR(Ut^4xiNZhA>R-jsrD%Zd{CE!wOiRhiRT(1NseK{=5(lt{{Vg0QW6SvFY?KPMV2C -YO|plGU@9Yz)k>P`_m~vrZHa=#q~T>P0in(%3OX|wtf4#uR^7X>Pfe9cV7{~ -O;$F-&-==Jm^p@5I<4JShWT1L|ZACbPSGxL=(?%-3>iwLGwvr+yLV_6A7cxWP^vkL)o!Ze=!`rD;}Ga6${Ro)P#949xqCf2KT$_2M7kX4|r+#XIEBOTp^V)~dcOIjeWHmLIYmw -fjCHfX%?vDeZw_YXgGhRVD=SxQCZn)zTH`j7M{g9|Y=mS%YUPNU~F)r+DYSRbqkTl6MKFeYSDQ%X~%Nt(hFnk!A?j!oon0Ea2zO# -e$qEV5EiFoZnD2#02ZBf`_@jC2VelU-FbL~A)_+E3EgHgYI|+TTm&AOPiq16S*w;%W1Dq`F+5A1~jYt -l%6&1$Pn#r8R?cdu@;(>&VD9wc!ru2-|PlvWT`nB`@#^`n#+Z?YeU#+JGh -wB5SGsWjsC5>jS1LM)htU@D1}wFyDBmQwM!KfIq&0a5r)0X)%nHo>4)^)`!n})FB -S_px3BuqKE3IlUUyF~x~IQ-xDE{58+g25p8vKwPfwA9%TK{zN-ArMflodBQwl;*(~5LFE|({NV3cy}t -S~rGFJ5m2V{l{+KSJ3;L3823fs{6mALFWsN;{T_W-5}#n8b#B)!=Vd(`vD -MSMzVxMo!^^vT)seDy&K9R^W?RS2g-gwgEW*^2xv%Ww7XGt|eFvSBC3^5ZEg;HvZ0sACv*;wut|L^F# -#!mnQwm)@aMMTf@N9+^%qVC_`uBVK*sKAWimB4?E6G{r)jLc+jeKh!_M`XslHWV)E0I)xrk=jk$nGk5yAj&-2e?&qQE%*v{u?s#|*iT2+r4T0 -^g9UzFl$S5)Any6Xbz%A!DVvbMh#=?RIGf8R>sNQwJUjw9NmQ>QNF1%%GmKWYK{nISJ@Rn-}{l;6q?v -}k(iB*D$A{1IE&^%Nkvqp(+>@4}c)2uZ#2fK9hfH1I@XN_xmMPOrGnyTT+XqM1ZZ6G@BKN{qjdl$X*czTw;q_(_yzZ)z5og+m9^B!x40i7&cVL*qAV^Gl(tW_t7+{WmbFWc`0|wtR+8(2^0YQg -q^{~SgtFw|4e~)#{5TN%=AVf{X5+)Xa=^VPo5Z72w;Jsb1Hyce-g#bP9>EeLsb^SYO(b)3o;MnL95%SQAU-jLU^{^UF+(u5ZZ_?Y0Ac4aLuBqCa|`A{jT6O?X*G#Aq& -_Vg??rpgnV2}JiFRhxuhC=4%{Wu&CvP=3@QcO9&4|qvy!bh{vFBc`!K7j5Njt>KB_9JH -h4C8m~`(BMIha7nGGF!2}}n!89Lif3_=+`m38-&bM~#fw+dIGelBYaR=ALukxE5xX=P)K+J6CrA&O*` -qoCz^xKJ=q$><7P(Kf+5^5TK{7NIqo7zl@bO@5P!OyW%H9vnIt&L5nD=vF00jC@qkD{= -}7-QnjL~1kagQMUHk~m#;O!3fxPfo+;Q&i%;IQ`TE<#FKbC?9@I7&Ikxj%aUvG>hpuFJ2OC^iTOaSs|iRf}~YY -7|LrrZk-sO)8d?Inlp|3L;go5BVY8~?-YZc%d_YAe6=v_l?~hv7ZcwnY;9o<*@=}4tbiTr -K@hAAFtqGis-(}8=bAY-*;kCgZLNv=Y-sDd`daN>LjbYm70Ir#QkPw4t@6aWAK2mmD%m`)W^p8Vwo003MP00 -1Qb003}la4%nWWo~3|axZXUV{2h&X>MmPa%FLKX>w(4Wo~qHE^v93SL>6THW2^LU!iax0%O8lI_XU2( -(8rzNb8ATwS9Ti<1jiS`;=ltC2@}?`QN*e01KNmslOPsyT5&EcR4|e8!FI-W(2{1TH*|4bWL(WQdH(C -;Yi*PBnjUM#5VdRD)1WqhK4ObZwVKa<<1HEDLf4j&Qs0yYJOSsglJBPJ{%7E@pv9r3UhtN8IuMZEa0S -If=b380B^%MTC-h|5lK)=B&L}NoegI@lq8!{mYgIBqPv1|iSV^xSt&_k-UF1siIoQP66a}v7^hpB<7`3hl->*rbb$%cDPr -})Rj~IuLzGECPCt4I|0h*C5xerjE?XGD~egJGpA!U$DEYJ|46O^;R@v)eo_u-D+5)qa>%jk72t-y$qg -=76(MJC)~{ugAs<{L$`H}Ncq-%n=&8fjX^Cun=gF-{Op>GxB#y -IoXk1dT()7l9+SG{WAx_&vjk>ALUR?njO;E@S{6$VZ%z5pHe7W#0@Jm#j{a`&CoQSqMuKnhYgtHM8oO -t(fDeF&UF?%lj9JJHKhp@qEtA&D)nIsS(n0hoZ+ik)PEB%Ky2u_OVBmAsZuxxR^DHe__wJ4p#eI$V?PYn;7!IV*av2QL~V5 -q_b3{Fuu`fok&VK5 -(swQ-OKeeQ$LHo-r@3I&n6VMlsGcp2VZrx=LbpKpo$1bM_fI7c -I;yrjW3f%s1;!ArWpo(pW6cJf_L!g9&(w?;ysE0(f;_Il8{;=6VOX`^woL0%#k~S_=T;SYdC! -E5Kf3jcbuVmazG7f>R{$2G*RCYeG&Xg@N`DmU@K|4BY{dAu18UTf(ykI3xtns1PMVCr?kG*+zwWaGSg -3x%bPJs@ZC^+B211eq8v-jQhj8xT!DIkd%p -6wB9B}WbI&6D?QGuD?V53rT!|qfrOh)7lmKW$!zvcu`APQO;-U6zSddW>x5E58(TWET>)@DGM?ntH=H -g$MwcIpfC4!~ixWu<|s7O;yh2Xlyxsu;6rD}DR;>emYsmy~Q^eO3jQB%UDICa~R2Of0K{&_?oy{fqhm -w;DO$sI?BPMw;k-_+U3yK9d6tsusF7z!sPKJvLQI)KZ8bRPN`~NBpaMhCV<2`Z!eOf;*%tFAnTnk}zX -yXmsYkhn}wjc;1Qa5rFTs-r}uShokaZ7j(^eGZ>_-m%T9My=DFNC>{K#_Vz46ht?|0m -Y!SlXUFV4><&-bo$k;OR3I9+drLklfH^!U-GcWoPCRV%yQB$9n%ffL?K35)gG}JQXxIRuAz!gp`+!>Z --X?gR!w8;+e!mA!~-GIS}G5!mr7anrj==vtv&)*r)ncsfb{?XsAY23Cd&D@35bz?0%U$#o5mhg`*tQF_uVr)Yqx5JDs(YDx&V@r& -X?G-1o@6aWAK2mmD%m`?JDM$>N?004q)001 -KZ003}la4%nWWo~3|axZXUV{2h&X>MmPb8uy2X=Z6;&vesH(`o0%@oFgwmRM7ymY^)Far(F4`v5=y1ZCMxueaT5Cb2~V4-XIT3-1BXhtYS7Tt-)UAtLxWT -c<@-L{z0t3os)nu#XO3z-~T)zvbZOs>|=x)zg3lrL9R-9+h -BR>isz6aGGmX8BEyh>fCEotF)CD6<9;Jup2o{VTnzszT}qIZWAkP>TZ|Ihi0zX`{!L4M3kyn(a!+QMA -dM#bgPnW<@Hc>T$yq(mfQxM$uPdo@QI~b(Pi>bk1q(}M^;gT -a7)z6QwLIFh<`v`JxED_Fu&w3aYJBp21Xn87@*a8ax1n>XJ?fBy4HVnFGEKToA-M|CGO1eztUKmX!y@ -L&{G(;r3F#0s6y@Exo|Q>R%2Jr>oBp3lS;EEhbUOombvSE{)^1N?j|U_DDoECG#?6KWwv*`&B=hCvKw -z^B(jp^y+88&GPhQ{e!)5t@Tr7kliCPY0aC=n@GXBmy74<8RK -vChl9F@lXKj0c0Alv$=twg51sfG4$>i(6phw9Xbo>>itju&&6F)V}SGpE}K@=o-qwxo-_mqo*HEK0X~ -Jpn(An4-g-;oJ%<1Q$T1)BPH{?TCau!J>x-~prv08<5A=^P{zG|Z^1o`-a%K<)mDxTQ06MlGO=oGl6m --9ZvaiZowW|ACA+2x8sp3y>Uqoa;p9pEv851>XuXb|uCOFslweLcIb3F^u_$1XEHSX?QYazzExS1q5FXdphng$v`u3M -@!kHWhRF7cH}sZvCG^|y>%bphq@V|(S%`W?h=Vx;wE1>x**l8-d4>ydfH1cuwrIfi9(5A0Cxum5Bo4w -pOm)Tjd^nX)u!nVhv5&J?xg9HKAO%&V^p~&?W)V^&@62l-D1ESAJ}t&6nP?Q?7~I0x{Pg+$SP51VxqL -9_D1-dyOJJ7ZuGWj=xvkKFR@1*y~!H$K1%nG{Yk0k#)!t@FkLR#fvmW49fKGeHvE -Y}icfXnUAll1qz2$#qsc7TqPNHvf_qQrLufoi*U`f_puyXyd@zEth};tkTRT8zI=)QdqET|GRbUd_Hs -}x1)m-{Vg7{IM73*#(u#jWSfg(268c0M9|MEKIBRJ#bryQ>kYd31FON{)Mb^9U}+DKy}s}2wfN}J9Q4 -r=OeheTFmGTFbb~vUqM$V(T_bg5(Cm|NjbW^`+`^X00!LdV^2SFzNqUl46RcalYWH29^uOAmX4@Of*js2TNH5T+#{aDPEVq}#XW#vNA3FU1El12_82)yS1VD@x=uhl%QrdseJt9{ -xb6x7KCQY$r42e$lmSS*ar4zEg?^EXr(&L$CA{tu4=Ej0Y&ag@JsID<9N(S6|959`%c1Y(k< -&+EbLJp>@%PbxznAgl5r!B*poQV%G#V6y@1T|1HYua07UCyd9o{>U)dmOQ$F;;PNwvmh)=aZey20Y4U -T|u|gbT4*03XU#nxSq3L0{&_SyfaeyS|I_qb*8oMKP~lNN6XR -EGZiPWvXE1%~PbIW*aGBAY8GQ58IV8AwyntTyT+~7m*iYSP%XVD>;as*(~u4SOT8@X)wa3-@p0x^RM5 -%iSF>r`Cs3D{q37CUcY-2hhlB*pyM$Z;Q>Xk2Q8%rPv|j7$fWb?CA}&HY$?$#>oz!}2J>2=eduZP(t+ -Xh_B*SWA3y0X&~dAKw|-8mAx4SxAiMX03Q6AH(%cn}urWh1!t4=;HM{#9s|iLig5HB4DAGfj^?KZJPa -Zg&#{-WW^yLEw?ef**#yxxNWFI-r$ItSS^V>P#0gYAz{qowp)T0ltv`BnrGTF(dPFE7&7-aFuO3d~^u -Pca7>#!I)8!hkQRX4&}(pquR>1zceqKJVM?;uMyVc)`0I;1a{kZ@`4UQC{_JeYId=F<7c4d0p3x>rG#fEdHl*fZ!v`s)DxWcf|8f -AB`?tbNNPfi?`Ad;kD``8iKVFt@xv-@0BSsG$Q<4^6AhkFsM+z4jx -{8vv^(YN}iwsg4yTH5!r-@3lzvQn6d#(7*tH7YA7bnO6b@}W`{K_6wtST$%sz2Nm`n*s0*Z7E9tDQv% -y8kFryY}8bG|utleRA2wmSCoL)4S}h&hFA;wSX_8N$&t4{=3P=rn~384J9GaiQ{`WL9i>R=A5VnPN(!PN0>0W2SD -x!*fPBq{J2`-$)q%(eM2&;o~!u*Lhejkt~(@b@1f%RXBX-5Pnr!Glz9>(xWgX##?M{w>HFmU5h+1KJh -?5K^cD?JaxY&zPG|91x||L}Xal009FXlMLLxMDv}cyj^G1gT1JHE9Ns2jU#=?5#XIH62J%%KJ)o-R*o -;kCNhCk?F3+yJ*{A2h87ATDZ((>=2cl^))ksb=bykEF?NcY19=ruTZoV{)hEjb!&@zY4IrxZ8_{@9@B -8J3(rzhPD-+W*cdQ3Bn!I_c9Fh;~$L*M%rS!Z_ceu~Y%StNbIY1k5gK=VH3FNvabFkeT+^i~}Y7Z0EP9%RgEq^GFPp3S7>-gl)6ijR_s2wW&mx) -~}YzcN+PxGcu>#ZBt{H!k+(~S+a#MmSBau|KldLJg0qVg-a&B<@l0vsH4Ql@!nPRE~($vqv9aX+5gdG -=g#+~Mc(i{u4%!$)UUbMTz$7|@oQ8_CN^| -VW6dg2)d=#l^#)!G8BywL7pbj~6|M+d4aADGSeN=Dcb4DQC9jEN2=+eA-R>uHf^v4_JuWGB8LgvT2(F -F8erv{9*PK&lax>14sH=~7!t7q0-Eb@+(k%`n7`DZ^$XL<#<~w}({016zE9^HB%K18fgW{o<0smEVeG -b;Fn7z?kF(5)C%hy)(3Fq$9t3OxT0xPX7@T_TcTYoIVwr+Fsc>?o1g^hl#Rx(!7J9MNgN&R-&9^G)5X -e@KLPv;RnhGd*`@>bv;6#xj{x|8|Hxuu7P)^t4jdu+r=s3%L?$)pZGUu8rNh2DZnA~nUEZ7`bhu`GZW -e@O~J}>kkin@gU6KVa^}cipjA3^vP({bR_3~gQ_wXjp(o=^T(1%pd1)Vm3<<~g-TYY#_S|2ZSta;Ljt -X@+%ETOS>KJH-h6$9^+y@FRVp_;5JjOo!u99PckyYj+W}JSs4NqUX? -g7|*s-$y(gC2mzC(J_sGDIGdmJSGGXGbR4glGP=A>LH=HR%(_v5vmko -muP`f#ToJe;Eswm$)yBkp4?yLywE{mvPG1Pv-8T(d}B|i4S>!nR7BzytZTF`(G$XZaX^C35i&2!55Mk -rVE8+86jB9~aI)lOC`~1 -&{%#-Nle2}cW_xosE0>jWJA6W$T2qmcU53B8ItJp^hiKj-bKuUtdwataN#Y{jS;ZANw9_F -l*7Ea@HMq_>ayl9MOZ4FHR -s*R?{;MdVv;PN52=@AWY9R|osMkZ(kOhhr(pjsJ95r;3;80;b^n%~*P(<*0mnLdo|8}bAO{o$IF2PkE -$4RRA=Yb4)oyBztJ>uKr6bcmCqErbTTTVut(s0h*(EW~f=8T4Q%j0*Vx`XQCA==x5MVr)zX>UhAn`ra -??y0}UPVg37JtSJwMQQA%A3oy3q28aFaEiE9@dW3vzi3Q>6XyyFs!t0RYpM0)is_X!0SaiLfFQqIe_bi2ex!ti*%#RoqKvF-C?dlw9WmlGqeI0p=_0DFl{HUL65AH%5+Jzjepxy4p~(ML=Vxi~umT6_gtBO*`HoZ~QQWw(T_sw5H;UBI0Q+J -6Bw6gkfmJmuRQW@2-d=ep+g%-zc2cjic03xbb_nKzY;ku$7GG(u2~{MP)zi3MidvxmS#L`LhC=9TkDjtmxI|euKnbXapN>!a90uPe$%N19)ii(`O}J7Ib=JPr*My9eL>7 -0DK9UJj31NHH#Iu#E+np!!GW--)JE&PVH&K;a&?j`x-C(YXpx&*V8y!H2qzM2b#kIm>7H4Th?%{YOc6 -epSAcFF1pmwml~-pQ9kCi}lLJ00y|M|6y!Ydji|tJ}Scwk|%XF9zJZ1ddNx;D^)Za#N>47wwJc=@C_f -AH#6d^2hOEBzRXM~y7OALXM%9w=n%@77QD0w_Jy+0Deq=e|9+vB`(+f+82}+Pk7-60VXc|%QSO9+CLk -%p_Hu=R5I?fwi3QcaE@{KIH?qj}EVMLB=imU*<*KfJ!b^UFx8X9qQ3r3$U1n?bjk|=)&pnZ93_MVhiA -J`JC0FR<177GEp?eDJ$XBMldH@zMJl@}_+tf)C{qE~GIl6{0`S9E+etYfaEOcBk -=>l;{x08=|pz#0{x?9Y9Fc^o>{se<{oLH)V*VcvYp79xc!M7s#te9|^LFJlpS3-2+(54l&A9TTC34t2 -()QS7Ds}tH7pCw$Musqt{vekv!bCd91=%{^ogkM>FJB6a8S02r=7@x2{+P@i5pY#(I9QI!OK}R}z%m+ -H>y&ZF1x5w@I@L-(2g@*-6RKe45ST0&tGv%$e5ji^=l}f1(nZT}ju1};}4D^XZa_6dY6rO=MMMGNO&l -J9~TcE+uz(phVbQp8&uP(9uMcPx -RQ}ouhtGrcCsvXZ6Bxe5)%as*orYJJwT(9Qt!l`0m4w)?qS%a`khylvvV%E9nb~7`pdbQbZ3VzEKS8D -?^(4_m2%^F;(fK-p4L=DJfrBcE#*#Gwx_CMlxk^*uuH23ISkq4*}6z;RZ%R$oRAZ_$2}0<8^W{GUYf( -!!*pKzTB^IzPNmznfKs4H+^-9zl%_8`2agsW>$0K(#@ -1qE|Ju&j9TEh5|33*&JR!X$u=hen42<7FfSW5Csj>+?5c091=WaCP8wTjvFChv<9W0(nbxUYP8#(N7S)DW1d!P1;(`D|x0ynl(bK2n~lZk7_LfTI}Vj+3Fj5tqJImoXR+)#U|ZKSJ9iQ -PnPuu?>wI)`)9d(z(Om3z(%@i)K!#r*kG4ocw8~UKeXE8w3_Ai`7V=p9F~fxXp6=L6un#1hnhHC-=6C -!V4xH5Q%uDym#M=#beAA4qStS~jM8OQ&YhGlb!~UFUGFjAFX$fG#8aGEYl?I^{lX-^*{@U!buA1 -_6X>Q~bqUej=GUjH6=TBxBArLbJE4F?nT1cU4EzAJq7HX{VrDBsnJD#m{w4+8UyYZRrS309;zsBAsoS -k8uBn9Bp5rP*rh)l`}(A->byv&eDzw5Sb9g_u)qP>8&UFP%hC!x&57{)>$SdS6L~y(keORDh@6aWAK2mmD%m`-6mt(}Jg008g>001 -5U003}la4%nWWo~3|axZXUV{2h&X>MmPb#!TLb1rasjZ{r<+b|5h`&Y2efZ9kZ1RHiK(8IdsP#`FZb= -ajV48@`oZMI}ca+++|e=j9lNk397h8@`y$w$6NWdz?>(n2L`1X@MMYp7+3%A$Zy2{hnV2o6nSL0949O -Up}I=DphlexR{Zt7HTp*>nP2iQsm5cNKjY+#v?z{QB$c=596ss$er(yFNEnV6fFzI&HR;uhIre-Ly42 -1c8pr+OniFx`CpoI@cL23Xn~!jRU^0y6zkn{r3cfTuWcr1X?4NqX1PpUuZH!7)<(PY~8mIQ(dbyuca6 -o+jp2GMah*`vZU=lSQuR51}-QO^JP*`nMv=@3idEgbMh8g!HWRJ8_fJqex3kbqn>`c9si9ALN&LuTlh -Tv#Da%G>Qcc0TxPpPmY$^(2nkuOAJJq7VTbmzVI9EJG!1Jkfp)iGWg}}&s_WP_D+=pOQA}XbIaqO;1B -F1~jwBSas?eY+(S}@)(yfB1k9-T#yDL$n?RCZ)xusBCHmDri6%k&X+4g1mE9!9(i`bPOTyRrj`&y=b^ -a$p-wm9g1>)A2BBuHE?41QR?(NCH^RvUO!w#^l<~GjC9?GI -^xsL!Dv#>27h!oGQ-6C&%wWB_Jny9;_GL>H8XAA8mT=0Zr`8L!>m_-EOB3nG!=%|yHqFN$Xi8H5u$2A -MQe~7g+vMSZZTvZml?@u@`{Hg5o(Sus;a5@5VdwXOY0Lm@^03ZMW0B~t=FJE?LZe(wAFK}UFYhh<;Zf7rcWpZytnKebKAmvNy{`dXP0YDKXrL~a6A-Ip76FG&VI6cLUAS~FTOk -e@#y62$PmnVO%+9@af1UTmqnRYg}Mj6S)oK+ES8z9q)22HrCAvcW~x|-$z-;wR!UALB3&#CRf%X?7TK -zj6a9NAlJq7e#)e|4(!2sd9#_QZz(B-BmPuYJ?NSO!tD?y21tkZXU^po&wTg*mX-OXEvoueu`@vu^nM -7FzW)8%;w~SONy`k`oX6Vm%avsI^jfLag*_ir?=U|-|*JpMkVXKb)}dLwgT -b_u8@&F5@(C83^N$P~Mjh{S%JMP<4Fn_=!ZQA~e_2#l_1Zg8|?x545|nJ0uCWYSp=?X+ZEkziel)1y= -I%{SxvG1JHxw_OVznJRU!g`|(+-f&TCfuGB}ST4!nO`~-{9WmAj1_Kte1?_)~vXwkis!%OWzlC6WYc0 -ubghN{3Kn}&K#F{9tid9A$pV1L5tmtd;EYZmi@Bm9vE7p)BgHI+`QF%3)_@&HdK}R5Z$dAIwwY-NUYq -+d3P(x9Wxx2s`j?QnSq|cj%B8wK&BoZZ{_C+aPi^9b!zX;ZY7Fzkomq2^aMmzr^+#Yt&ei79kwy@@3g -xMD@X6^0<;-Of`>Z(W@1aNqB1bKwxysV-;uCX`0v$4Oy)JahtQwCVbypqYF&9f%8_EE!w^;dHU+=JOe -RI7-Bd99?I4p*AUO&yUD`P?Y6-URB{FwwDR5-*lm@Ev%^Ho&y;3_4IvAf(F8P~K}-=0C@O1&3aUjJk* -_pFn+LpMe9z%lAuFERp~2Ya+|l6xG1v8S7%0Le+N9=iuR-wgJItZxfVBfE(aWid=4#Ts;d4dG#4!NY2 -lI;^h4dxU`%=y6vj@ca)V|5kSz-!vL8dg(f068p2XP*aK3&ou7v -X~#1*mke3+2@hznmQfU5q#lSrl`GnJl6ukp8LDC{;`Q!i&9O{re`Bx6ay#exVGEvXNa`TpJ|@$HTGUVZ(|KmPfzZ@+7S?qFZSGUls=RB0@93Q0>G{^rK}RHDEkq~RyD3z`4zI><` -6#(vLk?<-I2GK}VIN1**c#g5W3LWhtoucE(C(MS9}M!A|M5!wnQFEN$iM+&y$poxuFJZm&Jn!EX7NBk -;LE~z9g5+KcWz@oNhv?q{|&;z~g)TiJArEPW(z#9N*7=v!EB-o?&E8;>{{z1JND!p;2d$wEXM9A5UdV -(7XB+FH$d#q;Q9~yMGS1H&*?O5_2$P>jtM%g$4*PxKjNm$@X*XdSF?-8ESXmC$taiJD98pHm!P^B(Gw -fF~hY~ImKm*^Z9+-g?QH41rdt(MnwxM*3dR4mKTIT{V>)WsE4>l%D%^|BOEQYurI*7Q1>Q_GO0 -Qx&Ou%?%$g>dh{@C#>%%+GZguZl&@Aw~}F(gyOLA7M+d{rwiIXuH>SCNf8}tRf%=bh46C+z+go=Y<cZ9Xhs$E=Hx9eQX{FFyliof_Rg~H;5`8`lpi;@bzdf&v%)zpqwXy -m1@Yc(hUW}I1{P{XSHEpQdUjAi(Y}-jLH#F=G20P*>2`7*W#|U0caSfH{6>O2vF(PkaBQnPzWnD=3vq -RdmfRDn@fC^SiaTv*QzE)bmd%;H?+myt)8noJXN=>$*yLrQ3{+qQ1@VEcw^fmK*cs?F|cexw92;TG=u -Qt^>JZKE3uZ=$$1i*O_8eCrmeHg6&m2yr8+`$F+JlsOQgA4md2A^Zs^U>rYynN%V8T^y%(2pLb5AlPF -A2=F6P&9tPqX&~)gW%D~a;vxXNxo+ZySzTMK|1ceh;72um3$v{0Rf)zo|p((1sY6eLWRk2$7W&#=nBwXJEE+EBt*cv#nV;|@QLpalb -BYsMCf`i5mRs&Omqd*P<7aJnffvGJrS)r1vyI=`B!D)Srgd77pwE3uaB;-R{Bc&hxIQ87R3GKMs&obw6L$z$1vDXFcYP)fs>Z8B`53p6d7@PW-ls -bF~|Ro)Esx!R*em*7tl-DI&O#dUJMrAu|dW65G~hbmPB4$*hb?A)=NC98AzBMI+zs4{qyu61`~kY-Qu -vD&Ez5)v!GxxCpk*q8>9 -S-a1&f5!O~0yC^94g7fj^bEfS7S26fWC-yOe?!ItFpx -gtuQ!{HUnNa=%hbI7p?JzM<*_CW2h$jB$C%6%HKzdPik@}WK*oa?Jv>72M#ELaX+*O{8lItn37t-ac_ -Wd!;oUrga}HcybMnM2{X`7j8*xfA<4Kj^A)bhdL-N@gR&NA8{f8a)4K^sEEEZDS7Ah&NHZnlvRA!b-n -bGi!n;E`;G#k;VHL&sfE(O>iUP^HbbP>^kBXHdCC{W%vWd)Ya@1!0Q%xM@EeBs-@IkM#>C=BDIw6CxFF!KC!dDrz%0zrfN73)aJ_cQamYot(VTfK&IZpSFA+zPVb -k8{g`C0sXQ`WOfh56kV%%MELAujkkddM$Gl=O!&7dX{<_itAQ5cb6-&T5M9H6Bm3%4mR>_C(>UA3LlXQ;sX}W#V(R!L}KkjAQ2ECa)+)Dhl+Dxx#= -Fhh6qf98H%PR;cw(4x6<5xQLAbl!q6W!1BBfLb#mCGix)jOG3Z+Jdp*@pLUC3A%}eY?+siZv -yACs1%B&&xFgjYwi(7*atR!o6tGr4x!!0KR3WjiSY{*fs)25;EdK#JYTYa*u3@%O)nlAXCM)9@w3ECD -Hh-)$3HYVn1nxQ+GWn=T&s;uRti##$K#YNM>Q415E7rA4_%>gxm#MV=K{fpanO9KQH0000803{TdPEG*>pj7|>08{_~03HAU0B~t=FJE?LZ -e(wAFK}gWH8D3YUtei%X>?y-E^v8MQc?;pN-QWyEh<(h$yZ1!&CE$r2&gQ{$j?(KNKDR7OiwLVC@s#+ -OIHYR4Nx#OG*`&T&(ALAQc_al;);(i18U67&x?;&uvJhu&@wP)h>@6aWAK2mmD%m`=xVxPWE -{006BQ001EX003}la4%nWWo~3|axZXYa5XVEFJEbHUvP47V`X!5E^v9>SW$1=HV}T-uOM6$k~>FPI}8 -K7c|g)QYtS|=vIfQGdID|HF&jzLNXm{EB0OH -Xw76X(^H@<`QJb*@{b%r7Y8AMJ1=Rgbm5Z@yG99{tz8sp1nJXj*niy9iN|!KtyiBT<}Z}6)XnY4O@pi -%L^TTRU#XDgdC9D-t`(6?+wXq7>0Luo_E1ikysy_nxfE7wbYbM7cz@hGy!c?_IkaZw~!(wSBaPrp5{V -o^S2$f0|~FD?5`*lagi{u92Ck*3l-*6FP-dHfe}|E$3L8`thCg!&^)QWHp=s!5r>&9j4I$Ymps=4k_i -o8cHIF{f^ZG!Wxgh{fCMV!QOU-d7fj{*7MjD7^oxvZF2Ei==182^PGZPDPW<e!rHkJ-lL?YApl!8q@}?hnB!7SjHZNBgy_OxMV7V6-zAEOU9A_l3~HGBE_`k*_En+IWrZ -(AR-L-G}&v(WL1jQi^-9qNKt16=wQ`#UIV$?sKXM*%1Vc&kD`%qh{ -kZvE*TiVy!t`u@$K@y18T!dlsbtNlZ0uMYx)PdIM!r$c-t4d!t@O{2}W$;K0U&VI9S4uz-4vu_3dK%_ -Ud?hAl|jc$m`gFT?Sm$B&03LTHUr!x|LPGO*=}1JV)Ut(Icz4VuINHeYNEm?4TEpg7c%BxVu{iQ+MwZR -r(h>^=t81^BPHQo3Bu}At`p*Eoq*AxQG8NRkt|H9o-YIGPyRUk@@4wXLHYYj?ZlyPOH#j51=6M4RSY} -`X`UmaYEck_@m9oP%zACAYjqX{y>rpA^@S+1*zN_k-g8;{0F|mW_i<`>)*hQ9%Y_?HGj^jTo!i6EnIg -7b(4kG*Q{z<+<6_h&;GlbC(TY%RmIqR3Dou3|8$#m4{de? -DoYZF(=Ly#y`ln@wQewxz+D>YKuo -$5V57-L0)Iwe-VO)-`{eqve95KgcFcEHO0sV+y>ebc>rG;bO~BBtX6)1XXId+1k$QnrcZt*FL$h!!X< -}8GtjX7BPlYxGw}t7j(x_Kbj3v>L*m~%B>m4EZhGdr*QfyD4$sWsGpNTv_+&$1s9v+l=*sPy!_E3)sk -XbgbXDRJI?&mv;@y*?2)W;xmPPk$QDwjy?@n7;z)cDEY*6=iF;$l}02Qvvs)FGY`3CP@5=aCUOD`#&U -{xQ6PTH_;)QR^ -JC4G7`v1M-nC<1eXV8^K`IopEt89>SxAvCWA!6MlN09ur(DUp%8M~#%ga9jxs3dF+x1BIny9!t9Q;=toFWG_H4|nNeG0qI(camt0CQ-eKmEi)XL_;EF*PXyPUert~0T=xj7`yR -gXNcP1Np`y$elL(4SQEQ?K^dJdlEzed`PHT#4|_v+SZ8egk>x*HmnDj1l~ZgfVgJ7d1uvAzot?1j?`9 -O<@oZ=({%Iota5fdJ0BO@puT9@%mxU4hAKgohMjqG4~m!;-bD^n%ZCX#E(HM5~ -~S_9G*)W$=z54RmS-=U!#y}}9$yod~9QA9=~;ztn@IEsAhk!7>@H&9Ch1QY-O00;mj6qrt_^9=w%1ON -bw3IG5b0001RX>c!Jc4cm4Z*nhiWpFhyH!os!X>4RJaCxm(!H(ND5WVXwSOAkqy+==hL{ -azf5xp#H3SS$)PjK+E)?S7QjQGV~ueh-iQYFvJ5jrR1>_i9`0tQo|p#=3bR-5D#A$$0W+=(B@69KAZf -k+s_77f-{uQ#}d2umPJwgFlADw$3bcdv!bIN$JQxZEngG@AKw8m$6F8gU3jjRanGKjQTUt`Apzg7A*{UyT>Bj@jsic<#y-yCdUZjr?k~PNTH*ps0#lmpHHD9 -(AHSAqLeZ)Q(2<8|hY&D-+(M@`tHrLOPUSqJ6@AYfoViyh&Kk(M38&7ugOq$!?Lz%=3Su_3U=d>4Y|! -hHgsuvHZSUiOeWBj5*piY#UdP(jV#%MrpRnuFI#*HJAsgqTlM$Ep*bfw3Utlsx#6uwx`;eIzok1UIRNhMENJuT{V@sn~Mhk^;IuQH90di#QNyy%k>`pKPp%9~ld_7 -T?{L$(VHlKGDRO}}{O#Ja?V=a^j^~cJDfe7$kv-*f{)=`G)&F0TnZ{AK;d|C)U(GW`#scdlcd?MGs8% ->Fj@swruNuB1d<=LtZ@>s^W1JswicpnLs;pF6#tHZ)DYkz~WsLrSG5bUPp&Kpf9y~LSOrt99wuG&T9X -JVt>(NZXvfEvOwl!_f%lyplXm{U|YrpKKeW6i*;mCH?ZcO;MTGfctzX*xF&ufvJGeTcNiDmsk!i2uGs-E -6auw61eq1kJ&ZZ~aSW@YrWtV-mSQTG_zP;VzPO>NHU3t4r@UjhKx-K1{sqywmX#+zbkG)_20)?Zm0mr -Br*!{1-=IC#k3%r;MMt%m{NGFPn7LNzj9)h+z`4D?Jwh?--hf!EFSy2JVR3*U!m(@(-o*_a{c2JrUYA -CNe-y2EG!V>;x;5UdU9IBuSD^Z<$rF1;FL4X2$oleI@2d(6#SP(&ALa_Vf{CFVhC+B22cTRXuoLR~Fe -t~TWAh5&XPJJFbcm^_8I9>+tVJ&ck2=ad?vBq84G=Z_EDI*@!mrBS#p@@7Z{sT};0|XQR000O8B@~!W -AOZwdbp-$b{T2WK8vpW;~NPilXy1&+iB&rB-ivZV8 -o)ENFhm@x3F|TD~c*kSk&pSyZB6q%zpOy0{|Wo_SZ2^(eX}Pm%Qv{)FXkTcp^bB^P4CQb-!%o=GtSc*uT9c_3U -}(;b2^!96QE+xu4?0zi+|Goir5eH2I*<_T1r3`!hC^ZPTj6FW_2M*Fh0?%4Sj{C}6g

N-wJ-QsNERtvU9qf56U#+zPGahRd+*UemJVx-sI4m*9$@z!T0KJ(=K+^T2$%;`S9ZxlXc7(hbkD>0~ltd`WV -2f9Ceg3=_sC&Yv^lhju2@{Le~gZl-4w0?)kwpY7vHrbs=Yuh-yX^{(yU1G6c3G858RK$#5e&|44SQz4 -fsiC@+doDq0ik^4Agyo8wGG8f#$tt@}Yc{$|Rvjg=Uhm-h{3RDgO?y)^;AAB^om6qg^77*^^rh|b7+) -*DDa7JTswp6;u{=4^MB>bqX#}kCl$cc$(PWgfL7w#pLJZf~MA&MnAh6pwQl^J4~U+ap0l>Pgz??WDQH -rqk0{u|zY)J8|lW#C(%1(H^vHR)RKdI{7Ki^MzsSvDPK?qC2P%rAJ1n~>^8kYzKnA4PpQoz>nVpyNaX -l%TjTeLST4x;s(z=pY(r79X)B5<)*fgA~aO=Aa}_AuXq?xAAlsQ0?x6DSV>Kz~G4?OmQP)OV)JDiPs5 -mEtdw2jmB6xj@nhWd0oVuONNzzadq8GP+-Im$87Ygs}oKSqUI04s{wRQu>#q`aOsF)!6bQN<%*|;T7R -523+zrs1sn9$z(au=>6LO`@jW+ZN=Q~cTYX{8ewVYUuFNaQ -gdJ>HvqzdltLO5$AT=IZ#17x$e#Z8^QzJxi -*xAASqp1Od6L=e7-!(eA!xGCx`t5y1$&f3-muN=G)AJq9V>D=u6%y+Ie9r8`|Qf_G>_O(T$N%GKDF71 -N}`?n~)p!4?lXm-^+Q+FUWdV|vc5Ob;fP@?yl*h++m#)BF5>RW$LwSS#vYO7$K>JOvew8J&rX5wp{c<=q~kV3ptYr~;LCQRzS4nCWcSlZa#BqLg58PE!K4wmnd&LQ -@})Y@=^3UA35@W%j$ZwighoVV+CfWHuxP6{vrm~hWY=71*4Fha8|u0>q9PA7i>P)h>@6aWAK2mmD%m` -)9`R?vzD0006I0015U003}la4%nWWo~3|axZXYa5XVEFJo_PZ*pvJXD)DgeOF;`+cpsW?q6|mFhpKlB -~3qU@Rk8y;2|xF6S!98R*Tiz!sW&LOSokTe^lz -<&I!&UqD0#-7wYd{RUfALBY0&B$b8AaD)&BGLduOPYEwA2#MYnWiF -UZxDUoQ_%VF(5TG6{6B3aaMThTM+YXp@4zH~(2j}OT%_e5f(OBu#c?j8@PsZ1y>3sAKopt69GUvvC=K -obfW0;RyU?uvJut)STXB$u&*h+Jpvxu|LN}G956;ao@Xu3W411|Q#+5z0_it -i=^KO^^n%w9Xhh}%A-%<`RR3H*d6^%-}=84j&iQ#_TXv6X+ix75$48wByQ{-IhndQ ->SnMSxw7tLtBryFU}neq}1$#X13}jjvYXi*5&80VeXO4o(okI( -3zb&b)UY;U??Psg^7F^7buyP(<(jyV2kXnNT`Tz^#!hKPUNc5S#hA{8G8To8CaDQ^>0rIvpbw~Wlv9q -~HE}i0A2(`zjNLFz1(mx#Y_R_`X;C-_D5|n0BA{Fiuh%wLg<5q!K!jO~WPO0JQTpGoq>_U4*Z4pjVJF -3|FHqhr&&{z1VNuk>oWk;j=TA)P!d)LwWuB)?7pP0U)aCdAioa$d-8GN9mB{FxZMIs}JtAuN7IZr5Vj -p4|ww6{#9X_wE@k>yrN8KyJv%KiR@2)Kd-%zia!XcW&^8TTB+{Ic>(jN6|AG(;1Lf}68F`=ca3{v%73 -83(u-uL@VEG#d~p_b`sC&$+S^)2lFDit*iB_37FKia(4a`Si(Nr{^W=wmWh`l8$WQJf|rM#3V>dK`#e -*@TMYMx-doD_RpHdcmHyPf_pd`){w^Kd^9bgls`^y#5T{^8WPnK!G+pZ%=i<;-o0we=EnGju_@0Sh!Sa -`P&yp4VSaLU?y+0sCHR)RdporOPFwDW%kI7ykQg8A*&fPST -N`H$WcW$J-KB2fue({5FML;tTtHXt`)m*-Dm~3Pc~no%t>&!wcHuH)gX=YLR30|XQR000O8B@~!WNIu7c@&Et;cLD$a8~^|SaA|NaUv_0~WN&gWaAj~cF*h$ -`Z*6d4bS`jtt&qV=13?Ui@ADL4JnXh?7ilj=s7DbGDyS!sGIcWDsoTslGwaf~ce8_4N+|`+B?%<|m;6 -%7TSdA?@Tjz4RJaCz-mZExe -a5&rI9G4P>~jgxhI2eelN$Op$!Ho`f!VJo){(uE*PRLqMksw9=fINX2l3`NP3lk9Gbekh6t0W6W6XI? -n38V-jS70*+6-Z7S|A5_YfR{+3`!EiVn3^;9R5U -P2~#lr)F3^FP9Q1a4?o>-9z33k6cXtB?m(`r{~p4W#}wJoJcm{QHzWXF=nC_%B>^9N-QA5b02#zXWuJ~BCqq9We2C>jq2 -gOp_uM*2B8)#S3&Vg?E4#^`@5%4BkboSPG(mM5GJ?c$Pvl$k3_K!C -drZp6q}txCuhYdBO@j?lqE_-|t{1%7f$&I}M{`3_pGN@Yl&tAAbA`ggZtkD9;nih4G1zVhpCY;q`L8@ -iZzyj#L}@JBzkra?h3GC=>GCG08B59%PJ4r!Hg?0YrxW8_NggnkXs`K;TFqwmRko#Yl}6qgv&G&g&h5 -DUs>tSTZC@(G8SHc#No!ilicw#G1I0NGPe^F^e+XSi0fZFicsTlYQ|7G+V|OMWylfm6kjqBo_}Qd6gd -JX%6Ll&n-6sF~&0)p;D+5$?U>jq&%ZvW)v-}ZO+xswMdj)B;QsVw-jxet8q!QAMo-NLvKJFj+z+lkCK -`*$jbtiWNvK@Xk>WaQIL0aL(^sG(#W2r#sC6MY50<79^{-DL{|C4FONAr;G3omRB%c+`%uw;{;Um^4qR?~GD%x>q?HLPyetL4T=Q7$d*Vz~$wS8L? -x-}sBrLkVyV{9o|}HrLbn+_1F~NV3kBtGjjZ@j8U-<^0me#S7np&4cO1+^^Z9va|U#xN+ffdNcj#8`> -pet<5nj>8ESoEFsq^{?Ec-xv*epvDtDFu5r;t?bl%o@G02%E=dZ&=0u%Z|6s%q_B`3o4p`!@%RGwiZ<}5C%@;sCk8y{;1ZF-U2L=5}Xg -~4pXq{TUw~{N>y!O`)9BbpaFg#2L4ASS&qWWMbrmZ7#2qeJ7)z!Jm}l9(jxheZDGwVv%Kz -1=CVe0UkN4X?TsaWR3A#R0m94#e1}O=uL|rLJMIlQ>3ur*fS({3he;TfJ!!%dpNJx#(+%|!+I?ge~D_ -FIIm!^rc)JeJCac!--V}Gs`KAlI{I_ASmT_tqg~wocA3Ze{Ud*Lt(Zt&Y7(F;&%I;UZT&a$W+x^;>+w -8V@V>VEXT)sKzX#jRC|2T2AE$*OYyJD6_=0|F0J}2x)ha8=a=s;;Q;o@HW~(m(E;U#54%8Rq|2pxh7J -u*2pZZBh%6IOib?Pq|^61-S&Q`{+O7a$)Z@s*?c^+@dc9_KCm~*hximduJdtu+HI=KfMw!rp@*db!m$ -%=$(CNHq0o2rWv(7ck^VTF3Px{YkdhoKDXqRb)l5AcXH7ZkM~Z2U@u1F)Ankxxe@ -2LF0x(lG#-KsMzF%U?EwKDoib84SuK>FS*x`JwY-e*;#xlk^UQ0OgKf~tt1OcV6;}Z0zC&eY>t@DI!?hYAr6l~1VVjHOk;KfZ!c10nE`8>hJ!Dn9 -m8m)j<~YNH9mT0zQvER&z3N2H*A8`h;2eJc!;bv*gzq*Q;MNzuUKkp4V|ecL`$*4cESwEQ^S$HM_W|= -o1O4ZY0_5sv;**Mm=V9)F2RkAW#sHG?kR`%y6N#gF-X*0Y6K`4{4+s44 -y%0;m=J?Eu0)g(h4h?f>KFec!Jc4cm4Z*nhiWpFhyH!pW`VQ_F|a&so}b -JJJf;Bc-ko^vhE(Ie;nQ$d79eE@)?C2qg0BvcRPBsnEH(ui%}WZHoYMS;iQ-bwTgpqiDp`DXm$S-nDZ -J^Uqoa?!EXj%yMY(1L;o#nuk(HZEaw>#0_}$@y62eMsrMw~SVnI)5_EPA&as|T) -CcPrEV`eK0BG&}z#Kp!sv{$r(MFrHeKHz*%32_8gd);X+tH#F@mNT&1hYtqQx+-98@jwE?h@X2x_v?V -a1m`W_uaEYURWF{ud<#)|4bre~x%$L%(=YB-=R8%i22d%{vQ|06)B{PW8e$}d1Os8no-8O$Un1YxtO4AT*$vD(B|0?GxVSTz6~dX^fwD^shj4YN~adC-mNisgwd1K_!LCLuOy52;ogw@i>+F-XlJtrLRDE(Kv=q_M}&L0c2QVH(<nJJ49e8d|AV&B;wB4D%HG7+@~)6RWE9n@A> -Ka>$Y*N!J2hDF^)~Z?G7*~tWP+MZWvWm4`fRQO}m{-^^Gk?-nh?;q*F#g7U%Vt*iM++b=a*N^!&# -W=b@lZ$ukWp@TaZ)(~tAyk=#oTlJ5bY*$)qqZ0O0<)dNgC4Fl0){5@=gEpW&+0hJc&FD(<_aF7WY9y> -P*1JPBBfH{4v-^x9}GsR=v%^RT+kFS!uv{5!`{@fI!@_O{;2X(i=F&7tn&D`GN -W>3{d2Qhd;E(BRNaOf7~H~~r{l{@-g3Z6^I0U&A(X#s_Z=Uc3oxm4)%99V#-8y4vECuz(VNN=rOdG1f -t_K>9Qm^+>lZ4_pOGm?^zp*E0#PGrayPF;9g0f{bcX{a^+fA!bv^K%l9?oQlc%&QNSm4F*lP#Fro;gH -SPk+OuO_65-0QoH{_2^#FAup8k~m<8=biND=5(Ci~Fs@kL|8I2NQ`f#lHW^p{94FsQ%_jPbhHcIKqPVd0kHJ%v0!bq??me&UF -@)9z-q8y}`DXyok1t~O=II8=r#b|yqaot`e9=w0NU!{=kBJS5Xy+|Dh`dW-SuOjy&gAx*5&;9{WO9K -QH0000803{TdPA{ayR7P_E0EVRj03`qb0B~t=FJE?LZe(wAFK}yTUvg!0Z*_8GWpgiIUukY>bYEXCaC -y|d>v|ih3PthUA2Y?K=Bs-bO$U|ooNlCQFmbfL^o(#l{Kt -_CSPv%lH=5{vAvUUgfwYTegSHf!cpefcEYZWhPCKR7%*JUA_8*TrR-t;@FAu4iTT^z`MyapLFT;Hhs^ -wONrZo6BleEVD(mECF)8DXO}hXSdbns;c4rib0RFX6;)awA+h$wJv9yX1&WcSLM2F7I~JvyDIgAU@Te -rVFis_|FAA@%JyK>WLL%e5+PJ;_;Pav|5-!3wuFC+jSM84P9ILEd6Pl&Y}=Mu)8^EoS(VVBYU(zFo^Oh}T5XpA%nj>+hLv5Eu)Z*owuJ#yHMGU9U>^CwLGKh{HR -okpUE=D&7q`v&ng?Gu>l<0%cGqso8~wf=ALQlbCBGSGMLo~9HNFD~{sm0sNZxr%wYhUvo^X6<&hYStT?+;nA_98xoYM) -A=klT4G<=i#dfn@my=0W-Oy*nMcXX5n{vW`9~{VYn56#NxZkQqf7?_yrT)HlzioD_(lv&8;55vi(uF* -@E!H*ckZa#=3iocYtpO{WrF*%$zT9rAr5h6AK(ltQ0Ra}UL;CluSuO!p5lMWLs@*KB3;nyiDXJx4)Ve -Jlm~y?Y{S?caU;>-+!{&BftlVO3%5^Zts&4Uo=%4il(baX|wABZCKpP?R(Otl8<6mJg2b=Zo$w3A`@) -lnmlpkj0YLmUBM=#dvX3cGmveO-GbDe#je+Kiufj+8>YFTY|76km06)i*Z@c#tUd+`C*r6qvc$16BLr -iZ9ic9Ok*%k5<@)heI7FY7rTTG`9$L+4!%5A&PmJs`HU+OC(dA!9hht1+THP5uZ@$qI24F&d6NoWhG8 -p5m;9^Kj980GwgijR!>N=%y9Dy}1S|HwH9Cx-f>*xvc7IYWUrom+xLo-hOj>`ufc~nAf}Yw%oJOH5}Y -EY`dPhakg4F7sbVL2SDLOW`BPAA_@0TaOlf}c+`NzHEv4NPViTK%~VeJt);QIfW4b8V(>q(hWSk~14> -^49WPq{c;X(*42s#Tgq18dn{{;oryf@q&JA88vXV59dATfhNHt{v%h?r>+4iQ*P45Bxd@{k+olJU{FY -ytq%&Vr>g~_$M)$a4hjL5zCRjgL9E^Y67rp<^c6FFCt$#7ti(Y46o5^8}^3`X~8iwC?~RPe>Uny-z$H1G)D{dUAlzAh>_KyS7+ -9uL}z;h`fIzeQdE?@hdEGa2dpa6~=eQ9b?ZXNvgmt{MPzv4tZFmlp3ZUBCq#)20UepO*{bIN-JRK;NF -0>zk^@0V?gF9miL%n$6Sa%6K8n+85=f%LVD9kUd0^rS5}`ly)CCk0#f|EQA50n8 -3BNF6geB(8-U#AQF&*gY1nw#=B?&Shn6xRt?O&I~JT(3J%_Icyc>0N}~As80OM4&A@TO$`b>kFNxs-E -G{QNC+5raE9IBmE*}tXBUkg=-#+?|xq+cztNba5j0_C^`wg;kw^u+miCZP!w}$_=+macjB9r5Q$n6d* -v5-hnmOETCVzUT2fuDk|bJxeg0bo@|Ji$Z&4d2Tc9POo(Rl*HbXB{Gvf9t-Ywn7ZM?OxXJi)A&J=Jx> -nRe4#=b_P5U2;`Vxpam`WPe04>CvtZ9^$=bo!S$`oMGn+(ly6saYy@xUg#oFr=GAPIBb~i2cP% -s=X996F(heuUzVL^Wq4Yq1@~Ego_|JA>vTN8+=V;lrP@SKKJ*vbp$M+1F{faY5(uSzq^w#U+Hq9&0$T8Cwly` -H~c`jsd7G1Hh$>O{Tm)G$g!>C@&_%jj326-?Tur2LG0C&azLLVy+RHf@EHD^V?Q3cNON#+txN2>r7Pp -6stKk5;<+xeuZtNlyxG6Y_0w#ZbG(1_#zYqC0m!Eh;IPBJ(bByLeBpdMgO?Y(Xf#>&$L)rs-|QBMR#9 -)5pv|tz8D2~w3y%~I;9u9xEe?@jISSVqT1ofugY~vX+W7!Q`3B_8C49VXQPgb~PHLr3ghl~#Ijc6b8h -lq4SzX=&-Ar^f#%^vv9%Vr`7};#Q1}1xBKf`r>{Z{tm<%0Kd*3|QALoH_TO-l!`sCQX|WZi9ZOw!?WR --2X}xh8xKg4|7jNo>TX?Uim5ZOR;W0$AT>jVhPr&J%ovg(Lwm1UVdwh4daTYhYQMR*3Ig>JnJB&FpH3 -ZMhk6O*gn*c-J6;lk|=Znj7oMNp?6d*0)uC=m|@3t(f5qsLKz>pS1A5LH0>D9OQ%iZ*aK}C;WXu -=6NUTXltO>rDffOh*$v|*!Fos{Z^`xDxtJMboq9eO*-dDhC=-(FTV9Wqe8)A7MZOe~9whSL2pRAvD*d -~1uu~{^@kb?^zDHGNCT)3{FF*8=dBVzI3V_DP}_4{hw)F?I-OH${pn2*>cW_-7A4tAj)4E -m%kzOM(-{#~mhDq0nxWi(k8zU9m--$l6`0#8F70hqt(Dn5xe=0XYiIM@J2fOx~PFv -xq&|qHXoZg=?edRf%|yM7~odYi7E^NAukC4od^6(PW7{9*7Y2|?T{31%1Ces9PgRD+jVu<79_|nK((jzRNpXwa73v-nQ^=Fw$Dq%3|juqfz)^9{NV+3aa*kC5l4pV4w)caS2smPr|#+dqFNSkhMt -Fn_2Asexe@IdI@;u!-KQ5VQIqVVXiMUX=H&|6mwL9NOP0S!J$18gNn*t>S=<3uc8%P4xm=7o0wq2Lu3 -fZAPFn}U41Pu*<;a|m+;AhhIE;oLN#;+{umShte0N;oNn0*~r_dJEq -}-@Ram7jQ)<#OMJd~VUT}bZP20%@(hq3w1wW3K-b2E->9@?p-Px@&kOMPUMTH7EPIYZy#D^(r`Z5RS$ -`UI<$fOQL1k@pCrW?jiDZiP>L)!<4IQx@Ey&d@h%NB@~v8 -Ex2*!P1)9i4Ld#*z-$r^oGr=%O_ztN`nas#mo>RbuzJ~XS`%Opft#&LRo$Ncb@KJo|MB__k|}T1IeYT -?`GGR4@appe!>z*KpB)%>75;vF5b&$;m#0>l -_<>2bmS7(+<}?ozh&0)XG=_s-D>1Gc!rlVaJ0sdQg2eWYDkIz`%4Ap)jz{uw+o|k29B%;Uby~LWc;ax -@zfW0J+HaG`9Rw_v9EILyio6Fi6wa5Gjd$H$obQfC5dIjFFL@|7g$5XXm1tBn8&LmAgoNWh|+-ua5-J -Tf@3PG>Ho=AXXm3fOMfYFHr20L^A=QY6VANnAr5OfF21%r(3t0J64;VoSn;J3%%vXeyYkqoj9c@8v7B -%&@CO9WYQ3Q~;cs*IrSzEm8af@El4^bJ!C4Gau3N%W+RPm8Mv&r$D(a9b1@*b35Ur>&UHJjF6zkd5P@E=0BhF8Q)8@8ZpFSHr$p)*{4 -DVTbGJ)+!m$&vx-~5aw5QTy+Hw?~@pIG^QmkCC+RETW)^jJ);vw)Xsc|HF9-~T;(#ukF*j-d(7Z_)k? -{MK~3*J^{#gD8_#Y`3V(a^A9oz!hLUUo&4-_4Wf@2ddW&DKt5Z08v1Zw7RtJSI&j(fbq;31tN0*dPnq -V^{HPx!}u}>5)0=KMXRAI^DS0)6#pK*LIr=&NcIgH!8zL^y$34r+jHb;mIV;>!|ZUJ9Zn9zuLNBV)uu -4dKbaqY(jKB&p?fmxjbbX`;%FQ^cnl9ZUnCJVZ1xdG30z>q(Ramy{JiKAB0D0U`3MZUi4z}O-t -sDZ5O40nYd-9HJ!evKk|j=PXU*)#f;#+tmt&ZFT8_liHx(o8<58DCq4E0f4nJvgq0qe3?$dD=JoqdD3 -x+p0%-%oIVF)_-5_)+2DF6HrvHSa8md8T*eJ_;X9;TTldj`UdLcjQm1ZdeN^hG2OC)tYB^(9`k9!kMZ -!aL7I+!TLnyxW3|mo7|{zXGfljCRYK{pKW#Ryw^l;fJ4hKm2@@)RJ_DFWir#H$WSB{Orwxt4p*{X%En -%U0WM@;((@Qa?lYnU$wF!ylT&Xb3<;S!U0gO9I_Dj9J7TqiGx86S+nXSaamG)`D&g;-uNt((gnG -HD`)JC>v@LmsWK#oSD&UmR*R4o`=ZjVf$zzLv -jl0aE@YhM(e*-1YaIkwpcV<>;HA`oWmR-mg+YGM?_j**1pR^nnM$>^2ZJCU!~z0_+V?C7jSplqV)qjn -3Z1-h79zCl%#@MTI>j`LY)YT7hKaCzO56yI-FVaM?#M{exR7^7WyfRC$T`W_VV{u3qbvWF!I0r29xIR -A@otMPT;ALen%r-=;ZO)*ca-S4bC1=@aC8(VDW#vSYu0;&jtjMN2;Y6rg -3sF5`dRlzbxy=L0f!aB?NC&uaPv*dGE@FphFhH}5*18~-ij13eW;f@GJF1mM4-!^l3+q+@CpB*|SYi~ -5>=1G-1_hrqhq=&Q1rliY`y1JhZmJEChdfvVH-GZhO3eRdo7fsyPv^Swhb$W|t80wYQ!N?88cnC8bYJ2?mgwy(Sxnav -M?DID__$8fqXO%6*Lz2)zeU*fix1_Df`{B}`77-l9eS5lIrraZ=*$c_T&#C&2$8cAYqE!((_@T%?(QP -}yIc#c`W%9YA4hby9USg>>Jmr@K?klQLmB_=JUp=am5E& -`(AiRCXL8nM^uzHXIzgrVFYM?K^ad*4lCm-hjEilu4{rFqq7~<~D^WXz5}O}~j@5sQN1FL949??*;n(a7Fc*$+NdHy01lmN76!*)`I>eg-ZM?(TvJDs6{eHT -}Xnnp2xUpUNwExa6PGYvSitUanTw>Kx{_&CO6ZL&*Bsb=#`v!_dy9s~Qj8aFU`sEMP9Xgo_op1xL_h`CezUZ|A!(p+izUv0 -$4-VhJhY&WXIZ`I$>(h>93#V#WQj0XJSy+FQcSO2M=+C8Y;y -&kCm>Py~G&b|eI6$AUCsMV=%beozVGAsa;TOU-IWx(sB*Is?)NXHU*~t)#C$+(cCLU3#J)`652WRbiT -r5qsG6bgHn?oPdUUmLhiD+Xo}+71(sz4U6EZj!7W~mmU)4ZOZ{)*f_FrDx_Jn21Hmj+_4y*+_keCtF! -8k2nI|NmGtGTKn?kHs@nmRniZj7No -rK|(g$MJ5Sc5yGPbE_rJF!78g+OIV0+F$b%Q)7Pw>#N6MU18l2jnNadV(7LI-q{?D@4fx7ywXZS4usV -|d5Tr>{~`L`XwYa0XszvZk%$S&WWOX7^4EVxsI3b#Q`LqS2y~6Rt31;_b><$2R6V$k30U=kUh}HTSHo -(y@{Ej&t%2hx8}J>mux&>>!BCmWHFb&#>VX+2mwgd0t$MXW}ZL4&84{<^{o!Sr;%(48zh=qVadlC#Y7>kU0S##H#zk!F+9dJ8QwcWW(|upX1mHQE)eC?fSy`6D -hKAGK^>NKLjI0CHoQe>Sb!ZvVJf_`e~l`XG(N~mhYlb#tB@BrlrnIt*O!jbAexiXeLp3~|L(B5r -f(nSLa8V^y&G1{Gz5KVDyP&+!`vRAjGi75>BM+Sv0LPo|sz0>568Wp%RiR8fy3xiM|zb%bp0C7gjKiw -%+wq;p|;x^xzz!VDJrrBF!pP-S@wTRE?vcg9+lwwU$0Yye0dz^rz(-b*?s@@s{s}2iQ1KrNfa3JSNPf -7g1jj&8O9Ox39LCbUU15dT!oiP^xH4iv$!>kB%bHbDlSebIYIYFKU4;*=$oiN+eVabJ|k^Fg%%z?Do% -qo))ka^ya;5t4y+t%(*ALIS+1V6zEU>4E;AMus>Pwv*^$7@VgREY>mjw7&b8?q3XZUQ_j(aOEBC}%Ki -GiXDecgW25Z9$$45d%KTjdbBSJRw{lR8i^EzXdaHg$}MT&Gum;FysVMKx~gEuUmGhE7;%mkSDC5T-lAWZE5t<@rU?oAgkdtmfI$R=_&HC_ow;h1@4 -Wx@OD*si-;+{PyJ2^m6+77C>hahJesq@IzZC@%*fqO{!^7aHp9(#zJKW8{M-EMrWN>@5znaJw`e2Xp8 -Uld68-Tq_YGJFk`e$Qn2+YMjOesbEq0XS5dGz+^kPFL1`PYA$MZI<+1kxcF} -t!cT5i0z-g25BXNCw(D}~k4r;4 -Uo7nq@NmKw0LqSCiYT5f9XdRdEnbqm>ojkxxejq5{8gj2hvpg9!6V?^~I$+w5h}Av*GQ68EO43}IcAG{s+?0_a4K?(FA}84dC@4jBqU>op`5LYtBt* -?3q$G`wKBRbIXmz#u!hixST5OlZv2k1}!lhiC#RZxxT<(KBUQ>#iKe4Hbb0NHx`3**5#tay?4ADF03u -Hxo#t4+CE|BtVu8zswC1MhO$WFrZvT9btM;#d*8?yC`H!LL+y=0zbwwu9J*@u1(Gzu}-4(FaCl8^A0o -z%3b6AaEk^wVoP5hRudDY`F2q_omY`c=hOmxJVql719V387?g#}Q4N6P*;!{-#)UK-^ae*E1$u+hL@L -lbkZ0o2Ms&?p9gdJfVj+GNqKeU71w$j_)ApMyj7_$A)VUEux9N^H~6`ix&)^O8ZM%uUSGCq -lC$?tnZHSdRDE}4KQXGmG~SlRI1_;ZxKZdNU)_&CI^In3H7TN -E7>EKEM2E@_ch=#2G4xEQ$o=baDk(#hvRT9^R*C?v0N`)u1Q7kU#g4UJY_2pJ3ffwq%~!61Q9wVOm1h -0;fShS_<(yg1L}Fb3HfsLNcC9IQxl7sN7k^mY+&+|Rhb?&jKUh+3bMArU7jsvcHcr$ER}*nz%`)da}Sn0&)0y>hyXU_Xfrug)?F@`nt -7c;Sv|W$HUK;`WOV1qOU_64WP8x~zeNF_zR?=%f%OJV;NGz#G~v{m-j)Hwt|ep(1h%0`${mbsbn@!iR6BDBD8fWb2oR>%#iDdq#6_$bun+x5JkYC*(tv3`3&hmyp1x$1(9h1TdgXt -P7^)w>}cTKQX_qH&)GsaO^15%vz)fzzQ9a?Lb8{n%?cf>GzJrFK1vmDH=RT;~$MJR^rhK;7-8feRPw? -!ZcjN*dLv{vwA}3@qWSS|uQ?wm>DKh?*#gZ+0sIcT*Oe-UNL@);r_BX_$||HA7V^EhX^c`X3FQ|%m!^t4SIwPQ8td-Kr_*e)EKqiulfkLQyvW3dfe_3)>D3||%mqYDoq)} -n<=iJMmc%iFMr`<}l*a8Q$nH%0?=(jOYM$FO027vdaH(Ok&yF`+peIkV;WSvYDH;~=7*Tl0bcDNQO3mjV?g1?ySr&R<(^Z=SDlfl-BNNLCo8y-&e2bd2ku?^kwC31d$+B1Oq -n~%!5|vq*;Vu9iO0Mo(}K8wd!-9ihoGTT#%NS!0O!9Yebcbj({hCLiQnC$-TCujUk$@pRTsZkfYI2T9h&5&NdPxp%TRDc -@r+3Ps;IIs>kX`aYuzE3tpgcM5OFU+yGFH`Fen~jm_F!uKIowK!i;?n!^R3b_7O1N14-9wgnHje_V!^*=cIFuc6A}jT%A2RA9wslxB}2PF -b&AFE+98r0Oj6>Sy02_0S->CCl?bNy5pQ{u>Wz!eU395q9z|5q-K$wWPU!~7A`i<%r0!@g5-kP_8=5m -%Iu^y`p}s8=G`>QUl*%{!$1=llIbESZwd;ssNz)N6pd}oPtNmXkFSW=JW0I+1tE6AAF|>Sbq*UI9NUy -bH!nLN_wIVXJuufjvVq8f@PniM$1uD3eiKEIrJ+#Z3RcH*F%Gs)Bpdg^17y -l13aM2{@tH%8qSIWif=6Z!qJ7A>te3tO``$tt6C$)*y=;an($?TWZ467YokaXIl67$*|gpS;pBcBa%A -YFI)as=J(3H5HG`;fgb%O_q6hO=&bAn1JI5%a%M4j6fvCHiuvFj|ls^GnFf -zU*28Qr(p$%zHtf1CKR;bJ9|Win{_a~vX1;Gl{nu6uX=4ZYUQc4h#x%)Q9#eRp%|B;rX08B-Tfbb+9H -GNj!d{XZJZ5DBX;BHVqQ{5QKjFchF;enQhaY#j&bT4CHBoQL6|aTSNLfQc5T;nHS4lDAK&GE+^fV{L= -JBo98E#kGy{ChU)D#qSedK`AlU$vF6plOX~#bvl;|TUm}b%`qzP+D&L)4&sZZ7MwBHeuIv(m{JfucCn -%+1xALg`HZfs7Hwj{b4s!?z$t0gwWATl5|_h;NBmwQ)?6K&qlB9&>fZk(RDjGK -)BN@Hqh+c8E}L+FcZ{1;jA~ZiDt+FD4EL(C-#4jd~+dEV~A`lI(!@uqt9MFp`RpxgVEFFk@iUvyWfE% -sN^*IYKpj@4_tHG~;jyH%t3zyZ$er|=;#E?p%j~Donsz)Sa8gCGJq*u&2Cl$O0|$`mUQpZ}>sN__tANbj2`TJpXVI2DtpkOsZxxQ@P*5Y`4V6)E(6Rk#rwrFjUcku!mS$~I8X -C7@79L~y#OKw|8JMCzV#5bDX|r5s&Df83m{aeyjJXuxE6tkhpk{BMF(@2|8}ft&pGB2zQ_H_4mggab}R+iKu4YPN2jxUGVzB4qv2gJ157}qoG)!eY`Hm>;LW*KGhf6TL+ -KHcqFtB@nZWnvFUB9or1MBTsaz%@nS(Lv$_VZ*t$AYI6|rQEny9lwV)cUIg?v}n{@uyN{H&l07ueNL) -`+Zjy)gSdPiU0nmhE=kv0IcXst#`DI7(5xlG6KyuEGgd6{Fb|MK5b^L#K02lkr|rb(~st?BG-K&hMzY -%UjA6o~Zw!*}~K1XG8xUuMZDPRuA87>rHi2#CQ%)QO-l5K7H334Cc04qY7HKa%Y+0@~L8pu_xiS%Y%GCPS%Pcm6vyC|2Z2RSd<=sAjwJG?tF}%$ep$h!6*BG^b>^)_*KWCMtEjL; -=9PPnz{4U?J|4Lp4fqN^t*>NJtapd{1{c%J*b&?ajjai>J_&qqX*yb)T*Emrf`eYjo+Fp&2D+ccLz>@ -h_h1-MNmve5GK1>73IUnUf;*eHTEICI9;A@6Vw8U$h1?`d_;On1K1$u0VH&adP;-$82$=ziPIcl=v4< -m(n`K>82B>-Nz#Q+5@gb{N5?w*iA=OV{wH{YBv`&4So7ED&#i+8|HSp{662r=dc46^{8AAcC!zB*!@{&gpIyZ -(qNW{Bv{4~hZl9V$78vqiCNvw;mw~mAFPqJJ;cgGQbYd8*6#JMVn9GTjM>=!u#ep;@tidBnd)-~D+n= -N|YX+~p80YPbXO;&tjj{F4`fJ1{XWdOY@>PyKU5smkPM!8XS(dl2`{pIzm3DzWj_Vn3bUIgGQcm2LtS -DLVZv+pr~%>%1gw3AN4%3Od5qkKu#9gLD3HWc~Uh<8O?O4Nf6KFgJ5jN+d0Re;S2mJ!>8Y#^3i)S0V$TiV{>N&CK!0N#X4t -)y@-6`RKFk5+VMg(wlH){{W^60|1<_%KEB1y*S{WL|3JZB=hua$H`yr3#@^a8x)?Bx#Cw55sq9{0*9g -D+E<$m$ivYl71wM$BV72-KNow6dBfG;GAV#_YJn17@W0LkZLNveu}Tr+!nS=ML`(Xt(UaWryNF;`)|U -1eB*eZ9w9Y_|C&SA&As6l=Om8G@EOpu+JMdU%quPqV=>(1F1?dq5-KlUROou|p@m3FmO1xN|WyJ$Ka5 -FT8kaVZ{^VYuVg$tSkU&y`F6Awpf&tXgPvO#UEPP?92==Oz)?Gx&FN*cSq;XY8GdA-!~W=NxV5W7(tGre`D -sa3Dk5{7|d9y2N7A6iY+X{y802*=N3CuDCgOCg;a_HaotPyXoJeG>*m&v+YsQ6FWTGEWDY2^qtE~8cQ -jTCpqQ5%bi(ZQ2mtKXbf<%QGw`*85#>$o;0BYW)3tgwxW(Ye_24{G=^bIJ!|&i-gXtPp<(jg@ix<9}> -m{WD!m+Zk3}}yRX9SAVdzUvW8)(ZBgD^T|F6dUj(C|WBK+L?_#BlhiandPRZ7ivoP$j!ZH&|?z4n3kS -V`GkXR(NjLjoSS`7=yaW-%ehB_xjymGFtQS@t@zEhKqF5UUnRhXj?v^+)#F6l(qIDO-}+Zwo6qUOk>6C}xldD-6nXRP8Eh6(fAr!-EX6k{;l5#dmF&aL_dHB#qNl0meq}s^uVDs&0D)>`RFXKP -^4sSb%v`mtVvMF7fHWpASy8S?@Mi&d;=1@;xhn=1t(N-MQMO*^K>EJbUo^)mML&0=E*Fh4GUntDe=oq -ULw4skOKZjsx1U2jHvM@3;q~sI_Zj>`NK~to)o9&b_k7`rEpN)nvZvK03rV&AM3cMggyeV8k@7dmCga -cf6g?CKM+sQKORem4Rt^r-nK_z57uhqEhaIP{oLpBeYP?kRR^jreAME7|9X$z~RwklbyyTUmcAZW>~MIcm -x5@Jn*q1>7Y9?{QNtsA62iljw&B+lXsNm+v_Axx&%JF2_M+8aeeP*uW_|Kn%Y_S-|28&e5eioK9o%HJ -v62*qCV}{@H9z-nkAf(V$dxE5r+(?Fu}1bR27DTUROG-R=&a-twdFxtH4IX8qCFYcEw7T8T@>)7-ACF -G|glE%uDTskI&sQ{YVNjFfQG7NU<>&yl9%f-uXuM*Q1^k~p|_^R2IPk1^aP#Wz<%(`->WnNro(sHL ->^afb=#2hWU2gps%jl#JNV7FXba%LShdS=*D?Zo@oc@ZFGMg10!6YD{`*F3AwWa`nh$FfgepqF`s>_R -M(bZt#u;2@Xx&K#M3!6K)Vg}dLMGcJWY>xi-pmb~HI>CyNlz7&_%amHLk!B;yc8dHiIO@M=GP|U3>Xf -}`Xhc31?FJP!`qm0>rAap6tF#9*4-%JFY#^;uGz&tx1X#;N6Un?PA75?!0^XFy-a_Vx6iHGUDXUG#RO -U?h(^+K_p(_vZ~9?2IGEvPm&AcPF2l0%DYv=lbSOtHb7>u`A8dT~&Q3kH$$X{t~lpNV~wBi`A?j!B}- -BodK@+?L1=ua__~&=d>4_nkr?a%y(HMqKnWia}m?>O|HRr+{Sc=8)CG$1g4~+rvB7~=WvZ`jKy)ZrBam*fwe|Lg84$n7)k$%ljp|sBZI@HE>ncT)y&Sa -3L@*K1$YjILL6J@~=fuyev1z;kyXPP|udBwVG$Hw9(~#m?zdtV9S+QcGcjH$D_8?vWZ$}E30*0BK80i`u798`0bOsg -U(S!qPhiILP&dIm<9)p?j9b1D>Uz}#Yd-VH&S~7qyv{RetiNI8@FBe4SlV<1rhf2eYgAv-Z@pcDn`Uj -WR#yFmpxidsWKEGv-g~ID5elCMA?ecNF8?RPsFoGJ^i&iJ%;=uPUS#B?;BD-iBydyOsni}9F;8~Hn8= -fU#h(okXQfVMNr9$T&$kly|v!gFdFY0Vt@!J`!c9>HG`X8Xkx?S$uB|GD@^TVm8a|Ws -UG5owx8GlDEDIj{ZMkoOH9%*(AveSGJmGf}q*S33B7MmXV+#xL3PiaQ;PBuOu)$z73hkSHUPpJ<{tn` -u7EGP`xY-@c{ -yw5aH(%Mi{sx9Mx;WhKIEr={hWNTxm9{96TAW2z57Z2@upoKEJchgNw>>;p{M`gV-eI%#`d|VD@T#pT -G`imXYsp}3_Z~9HBI};FP>=FZx<2od1iVBa~IZ~nX+-6dW>NeLR7h|uvnM5#Haxv{eXXK9}col?Ptjo -6_N{T92r9S?vGL&CUj$Ox&NOS$Zys^QztIps=M(EY-5)LnszN}3IC%oo69Jg_=FhfldOA`WLBO@|9<^ -_jh8R{w-<{4%YN7QmEnZ@wp>QxzI0o7<{5%>-?Kuro2HzFdXI-K3nHin*)n>D5pFDin!r)dLkmB2%%P^JHu4*2Z^+R-|Yc~& -9*O%U0l3#6ZmPas|YIdv-kJ(?=v$S7+`6W4CQ07alKwN!*H2{*oE@$}MoL_B9J{m2_w{X}g$pLG^qPF -kXm?6o^$bDNKe>NN}n&#m}vCcOiHW5_RV+?u3{zaHO$U({ThaU-uiduBnrc+$mb@8h^aMOjs!vFp&$B -MlyKCTZ@Qt6-K6~=;4TdoSqcfsmIkXwuD@@jLuMPIL$0+OxSONX%@eE)rV(cZ0r_EQ?o;Y~wv)-%+fk -kx2e1Sv`KJhx{{?3Pccep9o6*JQ>}uV=P<)h5o6AANsNX|@s>Y+|&}p8Vf1-ajsj`kLi336ta|-5Cvj -!M?w0>SIy4n=W7^i4_agM89y2o@&getdo5g*tSLW8}mX+{{chMepc1u2uVbrym$ETay7D<{PD-X1th) -@u{~~7Z{il_8gzqONQxO6V=^_Zd&d5RhaZOz5jF%h7@uBVqFWc$k9FtK9pC_T8;rO!Gw(~9TA4dM31D -79twhR}YlDr)M7F-598=qkb?rUsfu`?6f5BiF(C!QiJb@T^$feuR_d^0J)O^5I6r|S-O|Nk8=ZB%N^} -JjG$`TtSj)+nITy)-Cmaxhvza1MK2y|U()~q6x6=_I~8W#9wvNnVKHi#G32-@5@*QOB;{@u7fQAd*@2 -P4{>V+zk3=pN*6s>wKWAqVhu -#L&JJ{Wt=sF4aG&ZA-H*JXR{o-*b2#Mw-(n0T)Wp)5ExVe}Yna77GmJi;KFMT76yA)dCkGmB#+ZYJ~@k8e|vSYN^$u+=?a9mRb&JU-xJ_{)nx@dR}dHQ!R_NsyCo}QF -S`EJoh!JIVI6jRhcNPb -}MwYR8~4ImrzRyF3n-EcvzL2ZnGcpade4dD%zwuBuBtBDY8p_)H8t4ZF9eB_|95 -|Dpms`-)Mz^mDTfWr@`~kVqP^Km($P!`83Mn+Bul0<5)v#6=jSy)Lgqdy)4it(DPefki=mpBj4~v5Yf -UR$&E`kh-0L*W)v8jGbG^n=$9z`O$V;U$B8y{IXDl_@tkJ8hx*5u`{)+MvBQzJmdx$vuUyEKR7fCn2C -QL69Au=un+C&~V~V3*IF0baa2g!wm6WatHPgvW2dj2z&36B+v{n0P^no~9iS+jVn -Et4ygL@lY^m&~YwS~*u+aUqZDD!S)?x_`C7q2fCEjhHw3M5U`xpn?Z}m!kt0{$ -m>@hX$-{olMWJ4*L2)dgW6uwJu5GX}H9(R=U9MUQMim0dyLuvp{3? -fLwA_|ISTR74;#!ii6`Hewqy*k@LdwV~a13|M~Zi;FK;fBvE;$xz$zS#{lX -EO^^6{eshig3?1aovbitDG%Z7AJ4+dzI74|Yt~i`@sboP?C_ltGWG!w4&CV1+A~`CWOdQsB#+PLM-Ea -fVnf9Sxl<`+_33y$sAUrdSgiBHH*d>@uJ_p_18Y@cq^B5wb-ZNAh)-BNJASQF)y?OcW#pLZbr>C#qyi -1X}m#>+$4GtSLq~oaAM1X&9>t$76AG-V$o~R9yYs)_aSK?KqPNRn;clKyyilx#l*_%Ad@06wyO1zaML -_{c}v7vOia9)WM3G*1baU@xYXdhkU5N@;GVD<$HK}S8x%{6K$SBZNg{|7;v!>-&3$Z--QitDh~`HlK- -bIQFiJ(SvX6G)7@xV -SrL1%wR66DIFGmsmIr3q$7v&5oo91@lTfCB*Pgmva8tdL%H1Er_wt}Hyz$-1L6^OeF7U5?amP=91+v8t6Nv=rXxto3HfwY|Dvx1^bj~DBztmnYNg=vZLN6~s;TB -&yGNt?2TT(7&jjk>^e~qfQH`lJeHGK6$cZQ9Na}f5K2euB;GE?ob{B_pD4tlao>&E-KWe ->Rr;0oYEF>i-qJ~h*GPEZjk~CQ+^(lQUgI%JYz*|T8!#3Uw1|n>s&gc*#|ka-SxpHOGp4B_aRrQE -~z-N?I)1yKO*R-qwEbMFHl>xF90uAll*;p-XC+6b`f&=;@tg)!m3g3d&4GL(Y{u)8CXBP(qG?aS8%Cg -&aVYuoAta{Vr8Ave`8+}_Nd?``36&4KQVHe^U*m8`#gMzb9=aH9x9l_k%@VBw%K(_W=S$fbLfwa{8a< -B(n%bl!efS*GuUFrB}t)+LCI1bVC)^*B(-gvYn*voR<%g2GfGlx(#pzW^;$0?bf?{w(;YE+zaW$|gm8 -D*rpY%ze7LR^XSs}cG1tRKhgufQQF-tw^})ZDJE=7Ds_{E28|iSXGWD|$4YN!E>KXlJJhebQSennHY&t9kq$#Q4-JtcX6K^oC-mY9Wc}Dj)JN`nPh&T8%URrU5)dZy3HyevOiQTBct$rjdV+L8W87zv8Ga|oH -5ys}wN2ObAAI1FcxZb|A{`8NR*vFjyQ>@EL_C0T3~pbHYXW!-FS@g0ck6=*5aT6?G-M)zD*)Y!;D!vU -QBSbyCkV19KmDv3h#a@cuiP|2w;#BvKu5$@n;(YGS+cLxHAeS3xqlw`ajZqIjEVC7YipSB^bm9m$(98LWc@lCF?sxT -B`H2o?~XIuKJGs9QDiqn+rVzB4`>JT|nD(7=Emtguybnuegzx9SHGVlh)oGoO*2IbO#=q)4>u{UTlg` -j@Ii{m4QQF%g+>6R*%m{UOUT82=<2s;wZcDQvJ=Y~#AYiNPa`CJ -gw(J4dz)enzq~GlY9d*MOck4$D2BA0{WLElJ@R?R_?_g?D6)==uMq}+Sgl0>N_Ed -7;xub0^DpbYt=rs49MN(``3~H>!{e^Rehq&|+a_ygR5`txOVrRU4IH#h2Vx(h*9q4F;-t?$v;=K!~k2 -vJtlk#Y;(p2vnKW4#dSCfmKX5s`6p`F3*VW;FD6cM#QQ$l7=>x);d9zd7%7wHL|4-W|LW+gcnJD*{3d -vk%Q_2@Z9yt-hGi-O`a7EOcyNQP99g!9LbaL=NLpV2c1W$#?5)!98k`XS(e&b#A$>(U4uKZb8Uzj&5x|cKf_PnF^IqN2Y#RQEdSt&+hlUZ*#h8lX3fE88f(N#T^({x?#PRz ->S@xour6Ex%oG(t-f+?zYOgFmCtP{%BH$}2kH$b-3Ty6kf^x7?cEx_JFtJuzRB}YRvn&khU={_J` -N)F=0FJ=z~(#ebtI^*s@@#ux__;)ZH1jbYb=gEp_bg#MVfRu;QZ?yA>|1rE2pXU2?6yC-zC9VBbN+^;RQCj;O>oBnl3d*sVfdeSzjmRs%dtac|8mPug+6jFatipp%xx`f+empzHH#jITvG+c0pw;22kt8^$ikJ0 -G(*+2RMOx+AGI=zI&up{Fr(d?ls7U3jkL2M84ND;VsM%XIvF>oeED4Nlu*e#Iai@hEMT7QwX*VQVVQ& -eWJ$GpJ)2L^is#c-c_-L$9cac`1{J0)FztY|QDA$bK6CU(JE(X;a8`HOE~eD(Tt^8Dr7cVkBpai?nZd -8EK=w!9_C0duBUUVvAS8nTjKEb(6Ds^5g^1!ChaWnRHzS$H2uq5Jswg#O9nn#Q{0RqWR0mkZxH{djn% -q^72VGjSmtH|B1Xu|h~NZ%FvWH;NP9ElR9sLnSj|H-kR>OS=D%i%KrKG^Q?n)1btOXOWUe21mRIxt=w -ZD(H+sPyqi+ObV57Y{_ANHSAvO<7iv~H&LU+W}uJ@P(s(Sy1oQx%lzfd>Sd^B5m^a7GAVd0tDbf$+ziLBu;u)ln2&sJJR&lJRshzENZBLeu+ms>jw1MxV`At0cqM>4H1dtk2RsE`vYfzut~)#p30>mMbWK{FHmdF9KPP0!a+ZIiHSs6m1((F)!rz$ -bz-1NU(&m_O$VR++nQQ0@I*y!0`kodpWAy2|R?B|*%qvH*{VPhlrVcXMd`qQkjvm90zapLg%nUu$C;F -$2rAwzIJtUqCov+-@85_vi7Rfkc7u73)CK~!EEVjj7(j@T^IMo#JpP&MS^;6EV~$hvTf%f%;{3jG^7*5t~B2@ -ui0pGTjIch|R6QHXo)KOc=r~5a7}J1+UeGyJL%z015mjiBoR<*Ci#(z(zh_+MYqHqE(DNLJm4mnsueF -Hv&rCkL+FGbujdK2UVz?9@hpcHe5habX!FA(Ig^b-K<#y+51o?gRlT -qYWiX#6S^lfqO>JKiX;Wm75=IwGH6_rpx*UmE{!3xo3pyT+nQ5Qf`P&w%W>Q~zApUvbi5eqdml4wxV9u*Z%Oz^gwmX23E*ZB+ua7P*{wEiK~@h -{ZmJzTVy9jTE4SDmVp0x1SD_LHDUqnD7*nFMc8Z;5t9G!bhm1wEFETE$2*Cp-r9Jt4^fkzBHGW!>0wq0SgrB@ -3k78FkQMk=k!`l0lN*Yp^;n30d5N?omYfipMknav%bac2{Clfge?;YrwPE@)oL(hb^p0oLUL^Ng|YI7 -MfmR5gx(vB0v!N~_B-`uJ`}pU1sG`fojh=zK~RV?Cd0aQ(1dB^Jk;Yj`?Qu|X?Itk&gwtbM-R9n0~VC -uC_AG5m0y^m8Bb!5g8%N^(P)t!7d%9QAtVbeAz$Li>^D3cE+Bcj@E{$83#0ep3Qr@&25w+ns04%?eX^ -!`Z6aV_h~~0=B35w6PLWM;ghF+wt9AO_tn>PXSO`AH(Qw%z4tn9c -6S4wM4>Rz;n_qW-*wUXg_O=cp(Yp!h3SJekkKLyR(^KoFPVb6vGq9(}t8{*ayh%?NG6MJ)5{l*^d&fM -u^ZCF5kayS%MPi8?bw8aWeVPlG39Mq3w`H^mc}*sOUU$xzRln(Kvvz;*ii?aP19K6~`&_h@F%o)N?`Y -H4P-^KAI}NY!1fh+j9Tw)cKWZJvG8YT;>}LUBQXs>4xs3lbDwX(U6DS_!~@(ANroPU -p5KzHZnbVMiVG@8zaf;Ubb#oMi^rZ^NHoza-vpeN06J<*yiM75cY(oJZFN~|35cHE(Sf_QB)X1Fq$K5 -`&_Q1I0ar0}4p_1$9dhGqS@9FpSwHFCS~3!DbLI+kF&Sc3O~VGw%@DN)vMEJ)PkK>0?vX#x;rHZV*Y$ -c{ZqPnkQ$mePGVBlyM2@Oh>vD5uz~oBPGADdx(Lrux2GMbw`lSQen$V~_>f3iGkJ7$-9mR<5%^^QF5u -hXQ9-$fkdvs?!#Yj4iZcDx>IRecD=BpGLr6_P|3E)#J>$(Q5n5+|GgC_3oQzhzzf|$b1+W5^zM$L!#8 -&D1k_G1Gl*+Qf!3?+{JVT%?t8wKFSVgx=W4-Wo<^xZc1Ir*uRvI1K2n(?q=4iDv|q~-I7jMdo%6~du! -AIgB+pdFG0SJI@lX(Da09z-oo_-xQ=Ch$YF#N;mzaScXdG^1K&p#cR({hZj;A-iSdMT0?Ta^meEs%C_MiC2%Rj$*{pQ89r*B`34|IkubPx4yr-1$&hnuh2z_c#K%#0Waa3nR@`B${zS^-Ir3DJmmZfIcKRTt+_ijJ$=AHO*aMiIqw3 -*87DhHY@2yf=S^&1gz!LZGzQ^jFjaPt`TyKchAh0m}Y=emw#IyFOe93bU-9Xiil(k8x+@Ve%t4-uUc) -3e@lk7I$?t(YNh_;2ES~WM`kW=K~niU~J*UQRP8G4P*I(_>W2-n(f3hCo+*>K~lTIg&2_Zl%CNJr -6_Mli_0s1ERH(v5~sj&PQ#wg6x&QGJ-^c2xzB$iQ7yxr?ha;m_&yr6&4r@2b{B7~m^5eb$#-kg^I;=ZR=mc{ -7anse?A$nC576%jlyBg~H9{m7#Q-WTs!?ezH>$s`rIz;K~v5r#IG_6pd0cK6zsTidyOutjCLwupaL?< -0X0&rLwfEp~j7EJ=40B^DSKV;!8I+2f3T$JgLLvJSVdh?PQ6Xi6@>WXciXG=1t)PRo3rMSZ7Ea$P&cG -1m(6=&oKl_mWtBF=uku -bsz2cv#*}B@l^hgF^@QLGo$`m%3^U+dvg($5+j9%x+l8nq?271m -c>1iqloxjKtHrDEU|3Lu5Dw08q1u|zryA}b)XV}ZMdd~1s(7hp#|5JhKR4|6VmaQW_EuuW(H0o-G9N3H^?2m|Y&iYINL^qbgNWV$!n*+0C}4v^O -B83@B@fECch3IVKL2{YQ$?uVh!C3My%qC7{La4#{tXoXkem3CW2PRxi8CZIae+ -^$ja{y7?rbH-?YX#aoX4;A!KZa#?ZjjG}uhc?AO})aKtOSa+x^CzzMXt27w!4=~v`p8N?zI -rxD5uk9t`T5^0C+gRhxN~n^45R!G*31&A|o-F-3W@-Db16>D4nZ8Ugp(07q>wC*k)LKO-{?v0Vc%=rJ -Carh1mCotEQJ>|?oi<#2Y@%3Uo5A4A6^m#U=K}Au#@gXO2RW-kme{V$p<7jf7}<{j_L;mvouNqsa{Z=Zn7#}~m)K#8niR -X&$p(hnM?#!rDp+ZOI7u+NZ}A5;>{Y!j@3GtFhH4$E`Zu9s~_}@N%f0;y03-NJyx9{NS(w -@=tb9)Yi1awb-H>Vr`@GfgNz?4mH=D(XP(8Phs8kl+O5u4<3L?Va6n$VzU#qHB~YoLFqV4P9CEc8)mFyO -C(Kb*fK6UWGCpv2Fz-=S{8)+x~EYZT^{)iemd!So@u -z@Z+VwN81Xy&>&2*dpmHnN2}UA!9xeg{=N_gvIrFdJRg`WKfVj$#($c<7`s4yvo;4+I5F$x(8wM=;2ebl-T5Ef( -7HFj8>0Q&TvtMroXgnj`a-O@2hu_S2aciP4)nggGlz)6sa!Ohj5{nY$U^cO}R_vr|WX^}g-9=b4rbnv -Rb+*x0tw5y`Mn%%5=zU-1izK>mx<3s~@5)&`4%U$?1$&Li)f0XLOQy8RdgH#j6o2WrhDiUYkYem49L# --2lixhmi^p-2S%X^Y6%ct4Da4Y>gfK+}Qq?3M7mmfE%FZV$DxIulnXHex2y`VI3+Y2nzLQ5-6h#*fUo -d~*@&RZ;C704x)9Xch89mwrQe`7`;mgGoH>15B8>N{6QmDmo9MqItS0!~me9B(k9Lv_R`h7>>UcnX-D -rL>v#oDHYm2_ScyZGi<^KHSz=jhy}Hm_u6bB#JE0vSF2%`IvJcbmN2pSh1raBZnX%B+57FK~udt4g)o -R%0>0oV1%>&aRn^k1EZL_)3@4v1Xlz9x5E$xsY#+fvA3R)hcqrx!Ndk;F&Z3B!jBGPhY76}X4FN@i(Z{%Nn>zrb2o~f_sqi-WF4I~7HhOv6ag(NYGF+lESW6mPQX!Lz(e{nTLSrvuB!N2|OBhOOsxmd#C!U|E>Xsp;+i;FulGdv=!;fCK(R72IWR -zRgA% -uL5WfU4Q#UG;pJY#BxHA!l2)Gmwb>FqqujvFd=7wtgjN(!Jg0ypj*1JbnZ4w^YSnDJ#N728SK5Iim~c -QuU}5F!>{0$Ghf65ra$8s!c6FL_Db<_ma!s4lIDFJX_RBIg_BI?6EZ|6GWM5?d`x-gg%*NM*z5roD{P -(b)Ob0-4hhe}d#3tWMxO=F_e&QVqdG{dTMyM>c4wz?hgS8C=)wGsSqjwyrpd7{o76e5bSVX>+a@7cQi -~q2NCM{wdevPSh08ayLry(33hct}TW3O_2fUdlYDkdKjhumhWE?1ShBAeZ=NI%#hbpWPQ{xd -opTY*SbQP>w!1gyU6Ctts>)=f=bp*O|)n$k -nmbJ6u>;2sJg^rg?pNJh?gBs!Zg6E5EZ+#D@RH<`eW!G8 -?CAR4A#;F+nM#lusx|@haGlsdVXV=$?yO~bYgzfwezUaZJ^E^T9S{Ej{ofX=E~S;1<-*vG%$5(xfEFG -u3Kb@ZqFAjEl)e9aPW78CHO?=2lK~ -}JU)iQk(rk)GbQItcw#0aVMEAg2L0f_qo`h9Z)}BI*{;`C`fmNCrF1oQleOE~l@OXmSx}0SPu9+RmA0 -aLLc4oeuHx#H`l*p~F~RGV91GbyH=%Mp;waa$=h3CxXQnG{ -)K$sC{6zb#t|DwwIF3pL8oA6rrw%90eznt}8!P3{H*$#UP|-7-Uqv2Z9*#t^qfuLrw5yl0@0aEPvr_L -_SkvR_2#EqAx~$Np3{XSHKK9xj>Kn;$ZF}RH8^ZgXCQN$OV;OXx~W=WN_w-Rn1F~Q8(vS# -Eq$iyr@b{vt=mMW{*N5z3S!603^HKtWDw{P8i -;7Bdh|I%%sDWe@Ce<6jHRwz46Y0X1s$KtLs -Z&O*7cAK_Z5`zdEkM@o`IOzl%f$hih9~4P(K`7}=FHgqq&EXv64GbFIkr+*f}h7DIB7U6cV=s&6UM;T -Zn6+DWiOc-J*PCwNC!2;y<)z3SrbG(o5}*R>0?Uj!D3c&laX7!YxcPGv#k__X+7NnPW&0Qf; -gvi5q#c>7wFkwzPC+UQbz>@eB2hY5o+lTY_mzjBJ`4lN{2J5ezBE4`p34aqJ~lgcc(!GRL?50X#%qm> -V`FBL#=cQ#HPM@tCxU;3X#c!KrPhWMSZr(57+uyBufN(}EUOuPLJ}-VetD;CSwyO^dEovriH|gMU!h= -3fkz#7$!>zbv}G1{n!sH2TFpo(jE>7@2-h#jN|CJwzt0s~RA=Qs>9Y>eA*W!cJFH;b1r}XI*DQ71T$nHYv6l=(S@CY#eWN5p# -dsg;1es;r+tm`I7gLJ^C@*OQ@(80JX*Ox0w3TVA2{0LRaAuE*d!8HqHR|XY{NRDKA$iI&Q9k!CQZ--6 -x%6*YK^5gZINdV}T69qxiEIV{D#VZ=b5iXJOvWHMw58kvBncD*#11TzGw=nx~vQcW62#@ZM$R@%_BZsuF3^7BQz#;oywfQS1 -=E_T+NG@Lnl(31tqun9-g8NsXi2gmDS9t$Fe(24vRqY{s83m~t7ctocC1)2^B<9FeYMyz+7B -reklX#ah9$HZ&D8^iE6!@Pt_(5RHmX#?!!z@yeIdhq?9_!JLLST -QOD^v$cUUc7y)4kAnuF0gVIZnnjY!%{Q7x(Tpf%^}B-;uE6eUz9~`e3X~Lg~&YU4NyO7A0s` -u+w~F+?HB|c`byu#41p;FEVtfV;@&@vK+Kq@_p*wRY|K%ja;4X3FhR*{Qx!tv;8JbGJSdy{OMAT^x~k -v%7|O9&Y7pd-x3Gus`yQJX=%i<{Nh=ro*nN|Ws#TPNjC6qRgp>Yh-(0`m7uE2_Z91YjUN^QI@XTbEUr -P+xm?;6C0dznNSuZdB+Zh&>Q$SWeAdk=)}w60O9V)?8dXyrZkpAEazmulxJZWdd&!z>bh%jw9j2J1Ct -<6EHaej@P6py}wtWRO90cVW&_-X+fu75ByzfN#ewV-boDbF{f!jrCYQrcS`K%IM$V^>Zn8vb7kjyp5G{)~hS|i5 -;5bK%JHlwNy06EWRjS7#ZXdA<7v=Dq8hsOpJ*Q)G2-FW1;Ns(!5<8Q=-bix|9a;inh}ae>)F{R4YM1h -8YPT2drrK=DtCQ-P?0}*b^(AZuP>S1TJ*ON#aYb3Gu^Yr^sbbn#6Ou*$1mj02cYS -N!BRDNgsB`CMbUyqdhMx_T`V7XA-nGv`O8obSw4{CRXvXuZtH(T>m#ksIe;p}QA1*A7SHflFvXot3zN -{CG)iVc5XMul@(3!A$u*<5OS}6Y_eYZ``lCUa&v9>hfex-spAp^zupInb-4GBi>2m1Hg=r(;;XRKyJZxNY-4 -7>e(~<<^QZ5gGUa*kKVSUMI6>m@?EB&P9Fy4$zn|w1C=br?Z2bM`e011*_BD_ISpuitOXsbaXC#7Wab -z#;`CeyH1s>lx`;@AtHe$!iVxRwT*dw|s-W1i66|n1^=7Is+^fy{g6=w3{iiY2}hCOb2|HnLNXPg0jV -*N|gF*hl{S>;Y+&&d%fg`k~$Lzc@0cA#qr2$*Ijb>#d7~|B#FjTPIacl7=uBw^{x|{>s$ ->cK8!#2O2zKV*PW1%f%n|IsyQGs2C+d6cGsbk7bRP-e}|zA2Pq;m}p%M1`w5waDK^mto$i6E$uP6BBq -zVO}XB(*Rduo2->5QB)2-JObF;0-8VPhl~Hz_b*5o_P!BqR*od;!l5;&j2}2+K8!o2(B*hGAD_?W_N% -FcGAN(xoGeYj$IZ;*4`6BLC@q>#N -p)={$4pD2nc9hz)-DQzVjT7@_Bco@>dSo_ -doEoOZ1D7bS-v&xTcf^-S&)#G@?r8uCJ0C>)pTr!XUiGq{43%9B{O!lsMLy5XHx_tDd6~bmsUp;QkvWGfiQg9UsGJFe2FGndI6l7;?r*+=}d*H(v$ubc5_@pui(6^M!=Zo=^rAJR(E@IKj$HjZ)l1sT4&UtY**)e<#Goc;#Yr8Yq4)Bz*q)mv!Ep+bP@69%jJuKh*6Zxm>v!3+r(b=Qz5C0{x7jzZ -p1*$f&DSqpy?gOI119+|ub+QG)TryCG5XD3mX%$k%xB(1N`{yiT+F@o%w00H8Q@Xrd0KMvxJZ*CO7&{ -)JR&dK+GU$j{whd#-J6=mHH{O;bUk4>useLQH^Q5~qTGAP#|0B4Cz%|%XCWZ?r~24%s(AF#Q4T^VUAa -CFfkz3tiSu?$!q~Qn7YymfGfXXzO03aIjS&taeCL@9+g?8zq+Z@^uF8}7l?RFp06 -Mba}f~=(sCUBU?pQG$4rdUP&pwzfRqnIj+b*6A+%GSf7BV0txM@Bab1C3-W_DIYzE}9*hHe7=Y -t$6B#owa~U8wsrAJg*w*Y`{^pd@-ACC3$#KSLpCO*Tt80HIpKrj{E0tF(`ch%<)~aolYH}j`kE^ya4V -GW0&V491>CF~D)Hm^n`SvbCLK>E&_Z4da8v+^y+~Rm982 -jrrrr}**dWWByw-UHrG>e;6c)y6E^Nhkb^%3$JGILe1$(!UyH;~CWy8i)KX-mwRJ;T|Kf=`l+yP|Tpu -h*KplQ%yv}6hrmGodtpTpCU2FsEBKie%>p``1YXFC -C96j9{{v7B8cZo!k5JdkFR#0Kju^eKlC4$tEz(oBa%W%Xz{`!sVgT6CKFS&}sP_z5R)?TUlR`8#}1 -dYAMkleQ=ZXmQ9HO7}U~OV`emhXq7@bCh9TnryqufcPUWP@1`eJ!>FD@#Y$9cCHe7FSt%8AW76rhVi$ -Qx(h#U*2TKQ5`08RKnfxXY86xhfIv+gC|VxzW(F`wvGUY-7|Y^{r$=ly!x|H-?FRkgERZ5^|u-qjDKo%z -~nyz4~IkU6ue!KbDFYKwH*JU>4b`+AJi~h3enP5fu%Iu`eJc<;Ju~Wq#n4b>X+eLv|qoghN-YOQ7{9! -BUK6c<%ZAt)o)%pX -sG60Y{lbV^@8J$-4T|LNH~WLyY0h^SG49rOl-<5Dgbq+*r0e%UbE4JI{<_^7-jMR|p!N?cS)@%X;ECN -61)$%mL6ksF*$oHe#|gIB6Nm(c;yRVNcTyd)2sTGDN{tvwfplb`E#$INR5;>9iSz*T!4UXW&q0X@Fsz -W^~c!w#>lVADjSiD~M?0kiYKs2eCJ=650PTAnJd5JOLy_oDP+_p4?Csb5HtnWIC#G@LFEPoNJn-=OZq>09VvrD -xcceFKN3fW^xQ_{0^A|8C*xSqQ!i^{TXkDG**bJ8Sr@2KnUqoH1PTX~d@;~CV`njDH6n#KIL~#YogcI -gdqOmakrMPnC2$~1w_ESH#eMu>(fksVRZbk3O9MYzNgKh=}G(gBY2Gk6j3Q}lfUe?|1ilD{GW|wu+xK -1;Vl)4=(N=lYk8hvKvl~|eJ7V{5=5G#tm)JdF%!Z!5F(a0ixWP|OtV5k`a1z@ZnBg3G<^7Bz`y|#y|A -#uRw8+<+fonkI8@ -6aWAK2mmD%m`-6N1J+9c001%q001Wd003}la4%nWWo~3|axZXeXJ2w-~!TFDP?d6Wv!2BC~#TiW3S_30(d5lr8_P^bj6s=6-dO;4oMe|(+jQVBt3%XE& -2#w>s1IMrMY{#k!T0j#p`sC7QxegX?R)=+o!A5@8|mG#_A$yVw8PH5U^^&S=hQ-bZJ8s#TGVr&?l04j -g3@6?F>qXWil9~OERH?wq_9LPW=%F9T1GD*r@HkLX!$(jR=PrJvhH$8y}skIP*@6aWAK2mmD%m`*}z6H9Cc008+C001BW003}la4%nWWo~3|axZXlZ)b -94b8|0WUukY>bYEXCaCx;?TW_046n^Jd95oNdDRq-deOWc#QisH79moQ1Rc$3iU~JD813NR0?SH>#hK -n)AY0}m5kl-`te);Bb2IIWgF}hd^(BmPzySRJ@w=`#qoWmD_2hk78;lpG3`f0I>XgZ4X)kp12JA-?|R -+Mv^XCQJ|b7DY@`V3g*37zBrC`%xrTrm2u76il+mGOKoHW4E*&lxi8P*1UDMa~Iqs8|A0<^OywU``02 -#F8-bu!9AQGC>jp3YI_8ge0gh68MKe^pHOiR1`PSWVxU*Q4T1p#opEWs3=IpfMyV-sg$IIk5pK}!~); -F3BH=11-1|Op8LhVwyxoo>ErKLU^>@|$(#if*Mn=@A5TsD&IfZk1(fnkC$KFaReiOC33%46>7j&+La1 -)9+8Ix0*S2%35yp?WvO0@bZqnen3@1j)?WAkUzK*n(6x -py#ied#vLd<#vOfhmPCf9SAppqz#7dD3gskq(Um+jS&A8V)>Ddf`~NQ~TC(#+DRy6@TD*$T;&W0Rv3W -_N7gC7L-YHl~UB=Sfx&tc$o|IMEPKOn1nl1>Ag8D#7NWTEMGww&evkiNEiZL6*&{wyN^`%jO;m@2|Uk -Yfxjz(I8Rfe2)vOX|Ex)QnnZ#Kx+3+Fd{0O!qVjkZVY639F`;Qm<-0}|7YSiZ6KwaJ2KcD+w!&48t(< -2x*2p#{g#cU8Eyi+oLPWhsyd|n>VTj``qr|wcU~oBlKhU%ZHhegqc|PV2iQl)E4}bms?tMDY{E7Ru=V -XaOTAW6lgPqN3Mg`IHhrcmB!^#($5)`PSk`%s*!VS%me1r7M3#gwn@X=l5zQZ}s7GR=((L%z5yoKzLD^sY`H^K5bt9*pb676ls0QVIxC@u=v0I38e -Wx$Zz2QCnEK^PE(sU4J|cbi#neE$HQDaD27sIzMa-}EBKN4OjwPeTiM_c^?H7M6G_Zs*p{d>(TJSkDJ4{p -%g0dh+nFF>0HWf9=p7L2l>!)F7OioeRbp}hFTB&eghhJAs)Fv5O!<^F(_|J)-_nSa3*(^`3+(QB1M{T -R6qn(|2b-8iz}X-JA6Lx=lIMz*2`%d7VIpT@C7lq1}rNOy;5!Egz{eLF~2ZCnkxUW-~kUPIQWxm{6gz -xw{7ZKPM9e3E{cd%3V|<)%W#aZz@ZqzP>MK};p;;A$rL)yS%gxx3?)fHmx^(t(O?Yj?zDI~2QulyA5j -RiuZm@~Vr6&0>7d@&!ZW(CZDklIU4b&*g&FzniY{TGne-hB7rAHR4^t&UzNf(pBppt`+(KDcH=H7Z<1 -ReRZLq+9c$si{9Kl=cO4&RuTG>Erw5NefcIKqLcdYS4^^_%cnhdMU^x365~Cos4h~!rWMWfOQYTWZ=2 -Mfbgs$ADWx^M%lf=&DL3F^_>=4P#K0{O59)!Fn%X#;#qtBG^uBHFP^X$C$@`2kZaz|bx=mhHv;{8J$7 -+Bh)cRDdpZ&HVpZ^OU{smA=0|XQR000O8B@~!Wm&~=erUU>0CJO)n8vpFa -%FRKFJfVGE^v9ZRnKqZL=?W~uXx(3N>VwYTP|n?E8N6MEjf0uovv1^Dstm-GT>i~?Y5EjzyU!Q77!rq -isP~ejvP7izw|HR&5VB}AwWSQQu{J9@B7|2Z{B-3fZkCL7Ku;eem0^H>`DRKx10Yt0_BbRGfGB3`FuRzL@E9w&;`Meze5%4aPD} -qFe`ZjTr(*+R%MAWpAS{gHJB^vTrFF-U_4YlIXN%^sZLhI*r?|nS5FNMMwX3Cs`(V0sW#A6LqXBDHgFYWsJ@;i)dYXXMlaM>_(Gz_7-DhFf!P_UyQPD6P%SFM^{(7@F#llx -;}*H*a-QyUxlSThG2hg!nlwQO{pIVC^=)z%#z)0(+5NtPf*7E_a)BJn&Is6=ScY)ryL6}+SEhv>Ij0Z -Ss7&x0Z_7@5z3Mrp!wC}N6TL*^mP3G>3N)hZ9NbykAZ!JPE%iz>woociDs0H}Brao5q2 -k`ERSY|~K&>LL04?8sW$xYXP=dcrbXxqZ!(qr?Kktjm -eJ>}SjD$fctMVlNJS6c_NThKtT>4V;5r{=v-KWOMur+K0(%a^2)sP4Yyd|l;tL33s)?Rqa%0t}ZXoqe -~K;^lSgrhBKwR@d7;a{$HLWeZw>e}u8#|e!)&q{@g#59khaFL3PvvDX%m6~q%BhN3I9Ll6IEV3}}@Zj -CLhz2WsJYG!ZA+}-}PT{L&EZrinx5pJRm=hFgbcgE#xNGoOS@V -)wZm+$SL_y&|M!h7<){&k1%^^1m2j~9$YmSwvf@{Iq)blTT6Zg2EJy7DrTA -$6y~6e&=kzrZOiu}VMg{L1OWZMljd7#zLDmoG+#^el{7D;;l%&Yn{gcmi&=K1-0ALV-|Z&c>Dz`f -C`1x@n>X0*{(?OKvxqJUgCqcfTi{b=+es+7Hpp-Qh$vkYQQn}-xu(J$sqo!$LDi_*YUXN;NUeXXO5&u -f|2}7=$Y;HW7hE!rT6--D#;~|{JcYU7B_?z57>-4O;OMx=e8NHBIaaDm_r&9tak}0Ab6}N0s>$`&gKB -4@{j13}r8h*h;&x}2WxdfjcW*uuo$%&!X}*x=OJUxY<`-#xljiq!6USM3{`x*~$_D*jnjfV3v1!6T08 -mQ<1QY-O00;mj6qrtU;JJbE0ssJ91poja0001RX>c!Jc4cm4Z*nhia&KpHWpi^cV{dhCbY*fbaCx0n& -2HN`5Wedv2F)RMp%_h1MbQ?^$h1vBmRwR!Q7jgQVo^2`TNFqt*&Mh3Ko3QM-R=AJNp>jNak9JUp*r{z -IrIGtZTbjK9-hEb7phu4g~pX9Py9|096_Y(8>3ch2fbw<9zQyL3}dCuN?SOWSjaDxg=bgY>o2Qqt~P_ -E-o6NqkUW!StE^SJ0;i#|GDMU5CWI|om56y&K%uNN>Y{NH+#1EKF5Pu*B$V1)`zJ3QeDg3fT|$hY&=s*J7 -~wsCKNjyxRVcSIJ*(BvKWFm?Qn+hOt49Man$ru$V^@?Q^;B-tlGEyX=+ -(9~t;`xvzJe@+E3LhKhf}#b -P{fOaEyA_!P2)rY>?Pdsc4B-L8N9a&T>wuQ=0I2dXOM@BRLL(L!HWy}jR0$r_2*x -WfN}JJQ1C%gcYu*6Z9#3(ui<496iHfB*ZB4&HR|wu5&ayzk&c2Om55bc`N9j#%B=Y-H6X>~{LMEeeZU -RrU6*CvY0#&2_u(U21r^*XsBG_W%ykzpCF3v(J?5we$aX?C;+`w{v{^vxBc4{MEr19PS@bO9KQH0000 -803{TdPA}lyx(WjT0AvLK03QGV0B~t=FJE?LZe(wAFK}{iXL4n8b1!pnX>M+1axQRrjg!re+cp%1_j- -zp3<{46kK1My1Wi$nOxpxx$sy?pf+R3B7G)EaMS+rLGC-dpXp%*iMdJs^rawT_wC|E9=_O^)OuA{b;1 -S7tzI#Z^M{x4x39L+}%k_O|oBZUSTM2?A2u*cm^?K95@v0AZzy9VfOti6UQ^T3Ujr0dy!?zdR>-Xzzs -*B;uY`+VR&^%XmtLs{u5*h<-tp?bnx*EV1Tji)}nL(!O#_IF7QP6AJr2ripW!3o=tZiC0DjPs$&4tcXhP<@FK|y+ME);@R*QY5>qgToY@Lj8$sE4hpQmF-9LRu8 -gsZ~An$C6VDA{om^gi~Mw<~%uLBRYaRMBw}mka*;a$U;sN4kISQh_IOe5=DS6PGZTZK-429r@-liaC9 -VSLfj2H4x`10#S`yivw6g@$&DY9F(PJ^hf`c2rz~Rfv2Q+RGNwWd5dm=m^o+(5#MEJK)GF7$ -+P_Bz_EHPD4hb0fy!@lmjFj-p~;yv7iqZNW&T!kr|n|7~EU;Q+KK@C6jA^fkk-UH`h$FJ4-`XYBW%=x6=w;B&1G+4uV2=udhd-~8bce|+|&btrT -bYEXCaCt?{K?;B{3`Ehrr|1F1TX+NaCD;@SjVY6e*B48Ro4~8UO$QaA|NaUv_ -0~WN&gWaCvlZZEP=NZ*pZWaCwzcI|{=v4Bc}IbxI5-2MBcR1xoi|Ql%LDDY7$}+`V$i&xIDMLALdzrz -d$ZDg?NBYc52dn(O4Ew)lRCt^&3@j?nko>Dc#8Kr>3P7=n7B1fw1M!5ayqHt3Yar7GKzto?^#W5~Jfk -_zXwsDv{CWBDin=Hxj0GQ4(SY`s>8K|`cPUQ*GRcs1gxyzE95Lt{OjtvQ) -KRWJTlTMUde|S-f*w*@8!@lJgP)h>@6aWAK2mmD%m`-m;y(8!e0034f000~S003}la4%nWWo~3|axZX -sbZ>2JFK}UUb7gWaaCzNYOOxBU5x(nJV3h+%n_@{jsa<=@%G4gbmsI7F&1EH-MN!}kSA3`dNS=vj{(H -IsJ^&J=39S;1y0uL=?-6=x*VZGWqZ{4?hjuj -|e-%SBVk)8y{%?qh^NrtG^2EuSoXZRfGnNZxeaDZp1 -4~I>#Ro6V%7927mm&vg*Lhs*4>6Gec@D_so?HS_?f+njj81XJMf6lv$Mqpgq^~hKlV@HkD1lfZX8`J( -kZs8YHTV*cG|O2T7i|5=WGp4uB2xTBk=&E|;`@H5h46&YkABjY_#6E*ngPw3$VwsOGGU0nf00bomG}t -YQiKPVR0rnKvrHEsD9F@Rfv9;5ed(5OOOS=azjXs%RTA0^1n_)SE#rO8#6$y;R3`@Yh*J;A6L^pQS)I -8Sx22;g=$^5_fumC-!2><$b(2+Uv5{|vUh5U&(V&9InP8nyV=Z`YCBQHB|Epamkb!mLRl<9PHa(RqKIYkM17~o8=J&Tvf?(AD#5UM -D^jO5%T2wNJ5o1eLl50}pl#2xO5x+~Ho>%H$4;zH@jBeJv)UUtMeSV*LDd=X&UXc>kzM*COSqwPs+EiSXP!ii{>2^hE_0RDB6c0T2cvjWA`J$ -gNk)2gU5P72cnBRy-U=rXL`iDazV1>=tmPSa4o`Br3YB^*k3%k*iN0F6CZbZ-d -35O`~IB~_UM0kXe@wqXpSYkvU4i7%thYlP-aj~HNOpX;Tpeo~PrUomb>M}D^a;q -oiiLIGtvm}p&Xp+VI*0d39CNddCo?v+jdl(~$L?fEHFEj{MmTP~CbVlZ_0*LfrYE0N8Nw5FvU*+@DoG1(gGyT)mp<&&-1^9g)dEgi-r0f!Ukf<=dFAQ?^## -a?etm>lT_VC`WOQCC|@Y6lP>E_%Px!r3lAsk-l=gjK175)5NkX`$4m%WvFB -(CTl$#xSu4YB+|zss{FPsL@OjmtoDlhVUKwh|a{|?bya;%ElL_tn4)I&4f6VxW)$z>98qp_~2%cC~$+ -yLbdj35JO&ItGG^05kB(jeDaq5BuB*rM+St8tm38#;8yUe^FTM~U=S8Xtdxw`A+{e-dtt6iTofTe7oU -2N&qmP3jHBK3AQ(eb+fA{}$ja;z<5ZozsZGQ{yvBypK+Rqbg@Fx6cOquES5ahx$m4>>wrbz(0~bS8Lp3W<+fz`S(=>Z-cno<8J2HJ{VTBr_)tW3;uCQ4z#^Vx`_ -e2MvtLAlKL2m)BelDdJFQ~!D!jKLO5V5ZUrh<+JVAB)u<8kUe+;7NM?q2`0`LZo{@3v)I`u%lV`tLwt -Mb!xb+DIzGZcf(7IEHfwg>T@D!L%q$B=Jd|ynQ2dPwxrc?(yWVR7G`&$5oMIQL3WE;v9=}8YpbK-Kv@ -`)}*SKQt}4)`&eX&klZEfm-%+PT`rf4?egwtC3Pg8DlVgQnK{~@z0Gp*FKj_OuZh=me@9v(^7#kEO8dyXu0R);TNp8!@ZV` -i)sO*}MBS3~-aAm3ya?b3w?=`F{5&f=prt<2cn)4JF91Ua;QU}kqVjpq~G23XaGsrrYJz(j_Og(|acb -X&m$Tm57&Vle3P`S4-6B%jvM?-RT+a3RN*B2<}de+g_k#k&FTG93r3iPt*LIAq;s)6-+=*V~smDpDH$ -QDr5plqy)@uFc_&%6I?$%i{M%w0qyV8MJPvG$vfc$R8VlJ(!qAEY`>0F8{Rr@)}GN()*4BZboS@Ei~) -w+D0Oa+&q&gM3JK$Eo)9=Xhg)Eu-w=HH=&Q|+$29HuC|>4I*Zc0ilxh-?-#oVSOS)>o7Lju4ocC(_Ta -zsp|lORhwcNm+OW^NriWSQKj4i@XE#NeW1*SPq2~{C|J97Ls}=m->2Rx7n~s+r#C-FGVkc;izdzO?df -s($4LpZ|;pdgU0*R=E23gc7&B#9D)b6a$b;bBaNRt)dCbrIPYzLDNHR&)%rTePNF?*@!VxYpSIu>_;q -_|tS2}SKIG>ijhK0XoVbJsMuIx+Q}7;)D5e13HveAm?P!4Ic`x@Cf7QvHmO@c$kTu5+OFLG2B8Z-?0U -(g3!(JqPS=4>!CJ_X>Yv*f#!*j5<^p*(tia+lU6#&b(y^R&tLphkhWYyDK{a$WH`5|nzkH>?_$p>xc@air -~l971&vJBMrQ>#u<5n<1t0DD~e=>YL5Rz!pVxO+wdi$l?P -i8@L0`GiuG5-Nb{l74vfKZPiP1qix!xRpr*GWH{H{&ro3ug5Lf)P=DE&;=h4)_I5bcGq9b3lz8*jNW) -U+nrjkg_mLfH2NCuQm)y$d*cm@AIwp5FQ@&vA)29SzJYaT7zu?#^f^r(rF49k{L~=g>v1>gF#k{wqIh -L_c0YFb^bX{!F44P`Qp^r-}2KcTlJgX8eb3>A&xvs_4HY8uvamM7IptF?g_P!^Vb{M_qezRT*2-MphX -s8|oP8HTu(%*_yY5sw)O-PlY4R{sT};0|XQR000O8B@~!WtpVJBZvg-R{R0318UO$QaA|NaUv_0~WN& -gWaCvlZZEP=eWpi{caCv=`!ES>v42JJKg?9=_2yNg@n9gYcZe^B9>0`#{1}%I-=;9b!t#VUfYNL}19_pgTs&kNsjzQoGns$>vjyF>~ -?63MfOL3FZe#~ZovjOz>}*TYJ~X6jY51m6#eYvV?tUt_9g>@mOO3MRMvPK^%+QA)Ro4g76bo)vK-JR& -XYxM|xqT~J)|-TD -pmXKTt~p1QY-O00;mj6qrup7In>U0ssJk1^@sX0001RX>c!Jc4cm4Z*nhid30}WY%g_mX>4;YaCwbX& -u^PB6u$FUcn5?@z)jPsThY2+d+9FCWoavfz;2N+j%+8|lI4HjvrWLHs453+{NDGyU!SS@Mo5jSLQ`FH -8r1Gv(jbtmyt=x&$t6X%a6n4IH+4g?jPP@H_}vo42q)vejtil51IHIMJ`UAF#*pXTYy*d9l55LA? -*O(7M{ySR84X90qToA7-ugcrJe1EaOo47Eaoi;#$uS1Gm|yok}4TeNdv^GK?KSrP0XH6_uKGBALVDc(}|K%VTy}nk6ExYuLFY4sQ$ -Z_Ms6#U+Iq63AiS`yIMP$cW;jumZ~D~xLI9+_j3yJKjR+XOv)P>#b_KDJBSrx=ix`UqoxMZK@yw*?eE;ck9?KL5E?-N1Ztar -EO{S(2&=y%_40f1tDu=Be?`{Qk~Da(kA7ZRQ_$bw-EdAtN9+_wGdp+zEPviyjC60Z>Z=1QY-O00;mj6 -qrsq6x-5&1ONbj3;+Ne0001RX>c!Jc4cm4Z*nhid30}WY%h0mX>?_BE^v9BR!wi?HW0n*SFjEil(3HC -Y|;W7G{+Vw&~||~=*2b!TB2;m6lsu@6C?0{?+htflI3L4>fns1;mn&iUnz~QNS2kgs}0XGBC4G>j?_x -zy5N}^{oBL)?O -UAq4|p?NM3w&alN1yU2TGLJO4r=&nXEb3h@yv5pxGZx)4^;94nbyNMfRe2`|Z_5;w+9XGa8w%c)4#CGnL9>g+< -y3amxK|Yh);6Qj^D7|ci_j^M&G%m|Pn3i9;rkKc*sWs(M#z)rseEkAEDkKOQ{X@UDX9YSwHyFkV2;Q06R -_`c%IVALkQG(o`PY-ziMS>%`eyuE>=lF_HjfJ3XOEVZl{4PEM%$i43LjSFMo1P#pWDw}CG2k^UNTY$e0sj^o68os#f<8TICAQ0p8khU? -XzLa*428N&`>CFrF)z3a^};1TuDh91uyZdd#Q7iroir(-Bjx{-lu!^!Bg6C4jM*M`|_yN5HMuc*Tc^ -@Egk0EPkW+dR#;%Q2%L)cY7vyP9I3#m^+(PN9hQZ3&%Id%1C8Ulafef0>kVPeV5lLVJeh{16U*bz?Zi -lsv)c4_1m`WejYO6ZKE6E@c0TteE8m(TY_1SywAfEy;CNapfM!#<`t+xVeFqxv5tvoZmQI$!p2f+DkX -W+M3&&+xwrcyx3g5{_^AO_UqmK{YT|C`i2j^@^eMiQX;x1O=_rG7}kwt&m~ND$#NdFUI1KJ553AA45P -DT;d3f5r0+j=39cxZe7DE<{GAWK{r3?JTkDib_0| -XQR000O8B@~!WiIdf4I0gU!>JI<_AOHXWaA|NaUv_0~WN&gWa%FLKWpi|MFJE72ZfSI1UoLQYtyf=f+ -cpq?=cl;350(doj-6sFj5L9Xq$~Bsw5RxE4t6Vqe#i}p9CE+Bdy5e-SO_nyW{aG -9Q|?xu}nn1oIq(7N6#&Vkf#6;MfNj{q5+QUKXCs?5Do1ImxmDGC^&*(SpOY?<;K-$Q0wr&AtAk9k6iP -wCD%s7+u7_2F2r0hRgr%V{m6z{XkfV$Nu5XtYbA`~IfxvtDzlP#2n(rTRc6RK=je{6@H^8YhCj|`q|| -K5CxoCWr>CbNvO+2Y%J0yS6--+iU2+rjSH={2G90egYg*L%P^p&vK`TMEVW!j(LdY#~e0oDn(X@Zh(vpw*A>efEr -{j)wU4IIxI}X|JT-4qS?F#Bhm|;`DO{=0%A~%7ehJQ0P@jIyx6m4{a9>SjWCJh}4))pl6oHf0$yD(}O -MmyL7Ej;?#MHaC?<%Z;-JgI9vn~L2sG#Lx%G|};xmOeWtu`F<1#})nGrH47JUORtiR~ -CJHw+^4BbDagiK;tR7xyYW&%Mxz*8Lk2J;I3+y;y)I|Jr%~Wn9+woKWE#`%FQaAif%Dmf -w1@-8vCMNWvGWkH>cyt672I%tb0p6?hwPXQhSBBfn`w7)bmZbx -EFk6iuCoJ}PGLN0B6QCsR#+T^{xSOGEqG-iY&W;(y#=S+En{D#3IVg-esDEj?E_}D`2bP6Ln9??+NYIS01Jyxp~&ZustRLEdaQx{7WLn!NtdL3xT+0opW?#N6+kQ*Ny8+#J`|iS>W|by8a1{>QCknY -!~yXj2QTX|L1K+`*g@R~z_w4lCPizctaUzK>v!H>=ve2v>$Mndem!r_lU{@FPn#beQ1>{>FR|O -o&|xEi=WL$xrr7i~++3bFriM1Gu%FNZSvuDmhA&q+OKEBBt>{p1Zmrw%%h&I3ZwQL!vYk%$lVtarq>a -P2g;HYPC@d<8K4_h`k}ZXb{0onbHAqHYJn+SRtGso?+5@+3#}wR{zy1 --gx&uDWZduOOYuVjgtJIAaPcoW7vp?hsdi|2l4vENa&fvu4x|wCSP5hD&b&1XFxi&}xZ_PdttH --Iz`jE(Zkr3ibG3V1BNl!_>S}d^6%{XQR;C70xm?<-YsK;;#YY8JFm^8m*3?Vc5*M<}lisFSHyyVZcJ -4Qt{MscxHr_gULj0P2HD2HlGa-s{mSL<6C`%I07j$HTL2c@ea~hTsuT?Cf*3=bm+@KHTn_tI-+k?q@6aWAK2mmD%m`>oWs#pI2006WB001KZ003}la4%nWWo~3|axZdaadl;LbaO9XUv_13 -b7^mGUtcb8d1X(_YQr!Py!$H_a!@Z;Wc83xD75qv$R(8Y7O+)k6N}igqDN_pKfaO`HKVp+m75QArr)>#thJ=)qx?PRZ#xVg -BdSFqY{H*n|;tb8YwrP1q6n>>wk`e{Dbqcxx6Xm5_u_Jzj&#c|3D$|C*gWxF3q_RTmi7z?EEA;Ki<8k -IuVz&lF!LvA8=LSBnId^shTiKeNsl{G4R{oSh;b%VfDYu)RMk;ld*@ohSRvtkK%m9;pf7*BSdFZdF6A -B?f$@VH=*7?S-0P)h>@6aWAK2mmD%m`;ePIlB}B002k?001Wd003}la4%nWWo~3|axZdaadl;LbaO9X -X>N37a&BR4Uv+e8Y;!Jfd1X?;Zrd;nz3VH8?9gOLeCTN~(8I8GLk|OrtvkDlP1o9N$)l*IzUZt6_(;CTcW`)rfZ8?2K9vx-J$#4}r8b+5BK#B3vo9)R8OiFt3T%3>{=PX%-IIGZ4SI;rh9_u)t=T -y1fjiBh38jYhVZa6!u6}~!1mEWx>eQLKg9b;m4LA$M^hr6lOEACWw4eGs_z!aIzbcd233k{ -h7NAuz+}NIt$Ar|Uf&gX!7W$}{ajew0!Tw!p5kn$0uD2T@_0KYKu%Wx2W1zBcA+S1OPrb-oJPf`*I)c -l?Dl?}6}1Q`4jO;DmP?F~&HM$e`ulNa)eBj=~5cu_bwC9%JQ-kh@uenP%w)w20H-d^vSUm}o54QdZMc -z(Z%GoR3<2`1s?~-5ms*nqt?BvQmOh#$&NelfI@Eos9LYZH=o0uOyqNkH-auo4LOf#NOliIZ;K)&!Nk -#BF%%_Wvtzbd`tCTUK;&%UV>(kBXagMSQBxT^pU*VhS}V+ho4avYA_aKzcYR{)XLV$zOoiYW)163ldd -Jl7rAITN??d7u&|exMC>FF>)!=@3!dSK3t*iv`e_}*{luMY#Wg%i>SOk%q-cH#N9X!CW{JC)7JD^p=^ -!n3R+DKu3%R^6!Y?bm0%i#xDCFMorN)urXKwd4W!(}vDlb#MXt7NF15ir?1QY-O00;mk6qrt&IW9zu6 -aWA$Q~&@V0001RX>c!Jc4cm4Z*nhkWpQ<7b98erVPs)&bY*gLE^vA6JZp2?IFjG>D{%G)Npmtc$$Z!? -)hJbti6s(fH2U3*CXd<4cPDI-Co;R -AvbtKH{0XlN21iFngF^hX7G+gNJmH&46y@N*+MmJTZY4{$&XYP704J-s%t}_R1gi?3m7Bb%n3)cjdBt -utR92S)F6Ar{OPPrQ&l0wV*$4q4WnxjuJS!u@WgyoCD=(uBuU6(~nJ*qhH5e?5e9bm;6UC1rOY$PBi! -_z<@1sqgr)!?^dr>ew=Pg`%qb^O@rpQ0-Vw@hslzupcfW2g;FhG8~yL)@1ul3KWE18-Z8(x%R|03@2J -GmBl4a6(W*IQmMKC>2}Srt12;s$^8PY|CLVnZZ06VB4S6baJjyeRVGNmCWWG^$UYj^w0CxZl|RdJ2d) -{1a{X$rG={_RNaglV`t00cuchem)UMl`p=4>^FiZUrO`a+sg)T^W$lYEBSx&#LIla@xottTI<l0Rr6a -+CQvozM*fxXp>ssaDjQ<(WCEpaSzh*-FFXL{5GE24HaC17xA_~I(j3^5@ZX2p)i4{Aq$tFngp4 -Z^#)gsj>dz5k>c^@y*ax028L;5g1(+g0E+^?Fp-tIL;9L340WUT<&IfMJ-xVv=D_Egz5z(re6+Xfc-5jSAx_Euq`6du?kZJh -){y-0@TGACUaw8K}=yDUmH}X3`sR5icUYaq5jai@bV*@3lsy`Gr(+tRGXf%QBFF`(=q$&N&*nc31`bR=fEg*E^Jz -e<@Do7hjeCnpE-bhV(yID5BggP6shV>Dx5u3yNzhJKV#O -9H(NfW^$iabZHF>u}L-J5=s568R?edo7OD-k2HlcT}oKj$kbq)NP3m8JdtOquSt7oTdUS|n})bJDHMERu)M|CbU9$Fi~{%b(TnS3LJt5C(7k*h^5Yj>4bU -^i26kRXwXcKAlY7Z`N5O$5L=L-s(Hurc`qD>q_++W`uSbr~JFj<#nQQyP2bm>%D#&1;!fC%`ToZEPCp -g7Fp#@3)G|DCzei69rFC6l-DKV#ybR=GaUJ!jqERERc+Hq=kxj8P_U>KW=f2lFcc6S9?H}LK1h(Y_m| -rC;?M@+{!d1cy(Fx6j%ftUw~=>F%WL$*rHp768k}a0ZmgdK|qjQISAqV5WG|m6Ul3)SWh-PhsWCnPDG|Bt7( -s2?X(72~pZ~YTWkAmoE*XhW^f-5<(+e3eoBU5lS2Fz5c{oW1>`d*1#LPk_kCz!&Pl}9;C`Ffr}KwW12^!6K~+N+INgI04vH&&iwWfU$}0b+qRg4 -EDobSM~B^H<|tVSkD3WEkZp9oQ?-xkqshRWDu1VLilkPgi`zw;vZjC+`vLHcgoJOV#XfCZkrbn0~Yr+ -M&fi@kswCWGMtb?PBY7DM=)H~#!bLr;1(%}D(#T&V$IT1mXNtV*is*0q(k=EwK_9bc1OS$nqJOA8Z$d -kOIzwn|5FhMl1f#mX=zxazn&e7fovus!Rs`Q281+I)oL1*bjowKx^XWbL1GBT??6v!A5)vR?^HfPRN% -dI2$lkN2djf}Bz2*+ftKy4!&G4b;JlJ*a~q2~Q=4N42e~RGwP08W?um)-rW(QSj(ktQJ~#oMfgGjz7U -X{%fdee6ax1IVXh;>}VapAo7-W2nMMYpCQEAq#^N(~v|B0Bvy>4x}qisqVI+qP*GmBz(rbc;YzI2B)C -ZNY{cV-ytaELn(JMiu}U(vnW`{HvoNgvkugw_|ooH72%@xfg2R-bvl{xyj2gm>DwXwo<(JyBG#Dw#R~ -7oByP0{w&r`4;;xbO&rzt9BVZ!6G~5jh1Ep--?`+K@J2;>g<5QGWkMMjSkI>DrvyxJBArRC|DW|nDFl -AvNOySy?;-YZrpXCpsjEXhh36_)1V#4S_{~55)MtRfrlB%FX)0j6LW7Kv(pryl5IHAC23#<&S9xO*p9 -pzfQniMMG@r|W1eY8&74&=6WUO4t{Js=e&}es=Nx>zcNOF={?=_bk@zG*{zvBaxQm58-@K;k`{?M*D( -g}Lxj|BDSz#1Tf&|=mS*j#q!j3vn{m+q9$EPGyQ^;YDB2elzuNuT!+d{RA=q4x+dV=BE`3;tZ{`1|%& -H3wLkO*eevP@*LZ_f%kk8ubE?0Tp-uoC=g+L~i@a3kd>SHYKht$Jhds&i -Ud2aYcPbQOA%*4AJFM9kVWD#SNtRb_JWkbFD(~(skH&Z_X5z(NVP%Z%^1z-5lzQvWv(Y{vepiI4}x2k -xipkV3{U42{d($TFdIeuDBV9u>ecd%d?mGV -=RSB@^94hm*YM2Jd0fw0-9>ThN+esr-mfA_m%+SVSBIBER+F}y%``e*R!xIlBFn$#cbCG~aY!b4AP2> -M$hGvs0KwsChRF_hIP>;;1J#9ZJs~tF0r%@(f+V&Z;qIlqse*w -Z*w8SIuusEiSAU>)faD`iW=ad&R4l*+cXhbk#LPPF5rRbE!T;e_7&aTT3fb4AH{w%I+rj)qkS!4Dv5v+zH6!@9&wL%TYt#zG8Qaz%80EC& -TYQ9(6IgvTozXsg{5UL2qI=ZsbQHwBeJ;?Ce%tQgz|S{#y_O}adWlNHvz_W^0*%rKjt$hax+p|er90h -#LjX&A^kNFx$XSE~CK@fRTQ6p49#Q>my+*HM)1T=NGb;t-hwd9CfS04^v_Vrx-iPYfzn(}N0kioCvGf%&I)HEj|IT% -D$5GgllB>MS@APc(KByT<5iE3rX$YlWArJYq-Zc@XO}Y`URhm_maJS{B$>1=z-mIPCw;y~%IT_G`V)Y -0I}LvruA%#(YI8peduHQ)@8GuBXA)_>iCn-HMSJ-@RDfP}a$@I?niUq;5OTsEieeBu2*)H*t=#q_hVO -^WSF7)avZ~8aWr!Nla|8RqgL~g?@Jzcac@*Tcc^Jg}%)Utmr}u9T7GAwUH;01Vx@60dtV;qlih(JL^v -aCf$3A#N$B6K-&lyZ4FNn8~(R!P2kxD5ETrV(R7-$MFk6{yR@;S5j!R0{`ew#rYtvhBU(tXu@#E^-05 -4K*W}kzu~hBn*mhXRL=Qt{*G~`{9k$Ti#?~)X;QuqG2CMGCL&Lv7oNJDL5peh5*Jema9`jtlXdHH&)Y -xf-%|Cc^4)XhQ4&UHWpk~pgUEm)OMx!!lKpmklzDxO8#bVgaL#Ym0Oh&Wj_i}-tAoErS*&IEe*kRj9l -fL8-h%73M$H+6P{#)%43$D9Sd4>@HXH^Y~K;mO?lglQixxTtOKf7yuEqTm;VOew<56~QGUMXYQ>`>>6 -`!XP8cuo#_8)Y! -hLy!F+npA%nbkYa~>@&d&LAl$I73t`A$oXQEaA;@8}6OfdHb;T)2!`-;owHi}EkOM(iy;8N@~&p5y*w -#3M7A2x&|VEDmNuSxBy9{atcF8l;8)-v|y}sJ2L8Zz7bis?F~PwQ_C2ONc{{4NdRYtHX?Wvb#3M>Il&1PE9X>tJKwP<3#1$Vl^KRQvg&@aUGg>x^mj!r)<{KBezCDEx$5D? -m6_yrjcOW#RQhE?5?A_r#iyc2^`Y&FDdAY?^OlcWi8Yo6(Y4=#2&AE})>zbXSNrb)`2wIz&7}pI&hde -aZ*9iTMJ}sXcY2a6EuLelYA*^6NZ>(-Q3P^EiFmRg*>@(4NKZG>{MDWB1@GUEEMT4?u_#n3ec6O2K%@ -@%a6V=?8T>-JIxlO=}p;eE9d`@7eIs;qsfnZ4Z5hWA*spgkwmS7Q&qW+D3-fNpo1S`@~tn{SkQZb_c? -nu@})FCsbIU*UP0K4-8p2746N6WiKT3!**MsdbX*?0@D&3nyf>GJBVwYx1#hTtX|?H=Q5SmP8DMG9=? -yh|M#ck*h6*gtK8RgsfRML#tQjS*m2R!e&t5%MX^)=kR~FZ8Fcr+JKT5RC*(hZgS(tkTnh8%7|(zpd0 -WX!or)FtJOu;8uzd!CWy}TfH4Lo+Zx1X;jVSzA(NLMIa@rP4SebcJ5+U(LRr8eDcMyf)S(dAV#IOj`Q -jHjBi5z(^)iRIeM-+{YDV{BgP~y{i1rkPa`|=;Xn)mjxK(y=!f2LWc{eC5y9kA;_*6Y{TQ?MlKoD2yB --4ZqCS@diQ7DTY)7k_&GJmj=neVWb6-PfM=I24a88qxhZ;l2%fFAeK$Eo^A{Pj_g`^nA -8ZGT-DP_igeE|mcuTq<32Fy>Sci(1&NdfG^hm0nJ8XaZ?np|GoHS{+K6F71*cK%6etqA4JUxfqon1&( -Cd+vYm7VQ=&v!bVXV0E>H2z+pv1TBavh(+iPp30kNCSn+svJQU%Nz?VtpU40coTNP;pOR|(}`2HTx&U -B*cf_gANF2+vj@{P{DftRc0tRf{V?dnxlFM%7{lRGqzl}Oz#zie>CxzXJc60h=9=F -Zb&_;TQD8UTptI{8nIxfYnpXVTTHbPY!W5~w_WUh+-s| -!T=Ol7YD1v^HMDXpsOCso|s2H*`f#pd!omd?MEnIvg@B(nlC!pGrI1I~tow=1$tdK_y)Hol1#^t4eB_R%7OsO+NMD -;4v{)ra;{ZMa!p2_w_tv78f;hDR8q1cFOEBQn{@;~8t5#YvBBm#!0h2oZ_A*oaxU+lQCo${Zv<&k(-@qzT{IXY+1Jt73VsR_`= -sQgb`BGJT~~j-njE$klC_N#pAG<7hIxxSX6`oR5c>7vstB@`}EPlcSU2@#t(c84XWHhsRJGC2IpX#+j -fla@9(biLR(iZiad*jwv~BodR0@ahH6AE-%D#$t9&_R;4OuKMV#RIDwaJVcmk4V3Mu#U6OC5Y^w3VjwaC07nh+o!M2lWsUHZZ+nXePX -RnICPgjF@8_cS4B|4i4%OtKM+q3EhJIjiq|(>QX;&tP5+GCLRv?e=5s?((~aQZMPpxG!eotdoN!6dQg -vL-uZKQwI(QQVeRtC*Co`b=7mFo*g(o*D*Ex%aw8y1beb!J86#K`DKVH6@0HAeIP9dxsPr0%9%_jjve -9@z^XlC3oq|)9PVr}gR#Hp^;z%2- -MHbk2erOGXpgh|H>TSC_GBBzUn+9XJ@Av_{<$jg*8Hof(&I4k5@0oq?Gg9?Hlx1Xc^k5f;Uah@n#`;E -C52Q!NkXc0@LF+==CgNFi^o2kfkB&jN$PdCw4Hi|^~twIfWGM^ZkX@_6VUX3w^8?%Hod!Ec5Z-OLO7~ -EGvZIL^r4{L?f1t}Z=S^-DmH|J|7)Qma0lz%mF7}@tjtSgf|~K*7_^Mv0Uwq%p2a?M@xR!YV7}p^``0 -GFKwfS0gt<`j#Ib>Y0vq_}4)#>H$#WlLuD%lHNfD;Hi!ZlGB5b;N6ixWu?kiEA5>c1}pFouibF}#2vF -~5|pZ>wWT}EG?+ei67P)h>@6aWAK2mmG&m`*n6ZD(%_007=6000~S003}la4%nWWo~3|axZdaadl;Lb -aO9Zb#!PhaCzk#`)}Je`gi{oJO_oO)>a;CyCSGDWKDK0+NMRC;d>%HsEalk^@LJ{*#XnDSzNOe#GaKEpS?UccY(N%pm3N~@GsdeQr5`04d77hI90m{vIh{6bS -+D54jPATTR5pHR(3K^C-_=1eLw6OvqbiiUoVFH0dc5z0SF=ASfQ*3S#77C9f+OAE;;3hZC3s4U>C>h) -$)EXkNE_Vh7vi{qz{Spf<%x0+7GvZUHCS28cDRBS+$mIH$e4@lfYi!08Q2WK~0(uvO8>r9L63zZHwOT0w3_<1=E!T$5Zk%B9Rk7B38}RxESH~NdFZ$sQ_z3hzL?v8W6^OJ`{Nb!LwXKyby> -LElMJ0grkqCcDRb(D)SxwK*){Rf?jh`NdjM{)pAV6KyJlI!PwMLmkb2HW)2;QjoE~P5~O6JglMIbL7? -TgAR{v6I_*1H_zcOL>WV6A-?;ae3m|Fm77YF+mu3L3jvJlJ -hG@07<39WYCJ|IS2>>EyXn`0JMHc58yw8%4ehy5UUE3OVeV)0%tU#>fC`Jp9(2H#Pw_At#eTfEdudW9 -REU)ToiLAQ4&>^7?M^NpRlCgre$R$?{fxHVE$h-lbMW&@fSAH#i$*h7nV~9h;0GlY-55{>jHTGI*CaswE2tYBP$7{;)kgYb2JxsXpg -itvK#T9K_y5*7IPBC#l7w^@%A! -9bZyS+QN95EnWtj7-p&qFvwy5_U;dfea3Ifln63s=xoS3$PRl;IP*0l%QH>j}Hz_I@LI}lg1U#r?B6` -z;!z=7k*0yNd@g2_ECWWF<|}kvDCLEJH`gzS`YGx=1Uw(HY@rmG62!%8o}FjD}n(C5Wd1jX)AaNRXUC -UBx+Ke<}g=ry|_axi&g^$$fBf>y}K|iRDg!tcGwBep0%v+LbwqKVjoxA!J35vM{iG`G!e&gbr*2`oWi -6GUQL3W%jt4FrLbF7s)F)i-R}-Dl-b|e?Kq?$wevFWNON}0@=*4WpOX&9TzPk=C@C-pzR*i=F(M=$6u9fwCxo^I9?7QU;h!2cVR_0sOt_sQYWW_@AdyXp)4zd>IwZBu<-5WCpiQ2Apd*tKCkM^9P+18L-rW*$o8WicawK6kjf;hwYGs+lyQbYI{h8x^@DdG- -lhn3{$Bwo1VfnQ#bqo;>Y$!ftT3owSL@~II8_D^hCy7MW+4JnOkrm6W3HLbtj4-HVS13!7&*505zg5d -cHH>_>Q#vCVySkR4?;s3}Ox;>&amftR1!2j@n*!-FRcWYsR3R!& -Nu}fmsgkXka@Bs0O*1T+`e%>yAn822Zvoq(UtWS?+ABW;rJy)%i|8#u#^m7ur?SAdI8yM|Sgw=u7ap1&Ay0M{g`0D1pDs2Y8KD8u*GUBK~&?QL_0zk_Q -ad?mq`zA9$xF^V5SV~oya~P`z-8tS69Gk(t?}a~Z_^lOCCx8nauh+nFGm^D%k{!Bv{>dEyZHu10`Z4E -2Ltf}~cd^{H@i6~(YpbhkIIiJ^25h3-WT#A>vzH+-$%&tuLo+QV=nzK7rs)4s_D}wC=-L|BW6szjME!SM^*NrpvRuVNtO{IPl60#j>D-~cUC57B6s58vKFZa8*m$TnPgKg*+l!3lmU785btQ_s=cqfV2Wwd-Y%L_$ -Ru75c`_;u_kf_JOu4F=?5^jX?dai}ys)}U)cX07UNIFMulNuoK#FLI7nasZL$vh3R4&FK4bsMzej8ig -@?JP1n>V$4#$c7*qQ{{P^QhXkJ7 -9;D9_Ifu-@3q*57XGI>s(931=`_6`x66R&{dmg5Ge>iW31M=sck7q;@uI8-_S=jQAS-r_MB)O0a3%VR -M$7ltdmK}pdx2Qh+9Yfb~ns5w5g+jYD!4zB9)1UQP?%`UPrWI -B>1ljdTDdukJBJFWhl93A(*%~6lKp2Dkoke!)pd$4gVom5y$W;va!5iGv?>-oEjvlk~H&XQgH+)>log -?kK~QdN!P2nbtk2euB34#Oe9`s@W{0UjaXFCXyo8toyOy>B!3Y?9A;0h|D!fcgMI(V>9n!8w?d;8k|P -R^H<_4k|}S;V~yfUN;UL-|pL7z%HR}f>N^5c;AQ4M3lE3n9aXxh!HjQF9!_?^GH|gd{X>FCG$9K3LPV -wwL8x5jqf&{)2&T7hElx|z%Q+h5`HpiY|wZk#Y{8u1;w_5k}fzN2H&Q%ED>6+0eGK{4BOtO8k5O8$D8 -eSs$HXY+zmi=uFsFdBW-iM9Mu1|D4UO}lXlHtK^>>p&{`lE@fQkij8Rv-G$9Jo@2H~NHcfY4-)wcitX -(bJe%9R`>DKdOY3rwEc^+uXZYHeM1QY-O00;mk6qrtevZBu}0RRBe0RR9U0001RX>c! -Jc4cm4Z*nhkWpQ<7b98erV`Xx5b1rasRgkex#4r#&uE{9o9CG2Js -PmAzwn03k6%PN1E}xy}a0$r28Ywp5zVvkeevx61(di>gZWcw^Z9R#d2 -TqNi@vl3rCd}Jazp5q0;!URr{GGPaes#?f&P? -nIR_4*^3gaHIPyj_vWZwB3TLQ?*5i3WrCQ@&V5&D<4bdD46xm$(ZHMcnj7j9ZBBKp|um#jgu42pN|ME -eD(#rPmMQM$Z;ValM?k#N33=*OG7^aFOc&gXOy1%%D$2KsMQ}5cYV}+jx~k@tQFUhwKyEGwzhLD)~;_ -2#_5$MZn=bl?MO-9smFUaA|NaUv_0~WN&gWa%FLKWpi|MFJo -_QaA9;VaCxOvO>f&U487}D5bdGKkQhU983GhLbUXCWbwD@lFbsxb(}_AoQuWcsm;Lrp@@Lw#TQ|diN$ -Qb)BA;{$^EY!SbSczk1;JPIcQL})$;k<`co)!lmlb+vxM#l(AI83Hg@c_ggGAVJRZ4Uqc1BwdO^R3xD -SQpWIjRI}^bd#%KG%M$6)b>qSnDja^iNhH%*7~k?jGe-~dB|$&xKWJEn_79k -2NyQnW{gT&IFG3rSx#t`3sq?Xu6j<{{yh^eH}!&$lbG@S?GHFVhsAV^TlTo+Biu6L;FI>af^T?sk3fw -mwpfA=M4RYKymeH<1(S9nRNOB$r{KXM2oJ_0ZQjSdG*2)a%j1m3`SgAh)S?HM5zyOu)#AGj4N&eNerX --|1sV^N%Y^x8t+unaOUapbMNY^0rKagA?R(CB7Z$Tf*l#V-06wnN*XcHU+kG&k#y|7yCXl~=&aeR8f)NmW8-Ei&v#2~1_zSTk{!x`#UvP!Hh-#p&+qAQhh^R4$L -m%@2AQbeKkPk{s;k}p!I16*m!mBZ?dwu$aC2SlNz#KO~2>W}^fmy)j~@xS865T1?9hrL_q9n?CL+#8>d2synzsX?C~n -Xmfze5(ZnZ*NJ`q~>&m-(K_9|f>pL^Ec($9?lK*#^7-{IG>{Gq6BxsA8X5rsu;*2LX&IurU;3LDGk7% -t=M;;+^ClKzgXF@2w^Z$j3rS)9J0zdtE&%6cM60%+eyxQ@!E^cYz$*F -kT_(IhC39gbd#eM4Cl9}Uiy`!G_$b-93o&n61%&)tGZsEs&4v)xO#adingqp?NapJ-PM2MliBRz;$o) -c-+HM#ofU2SP)R-e``|O1ebw&vc~{+5b=4h_juc%hZl%a?Yx>%2DK52a7 -tM9x&fXZm1@&)LDHTpqv`Rsr`bZ>qd6RFY5vAC-bya|tEku> -cY#~Ys6n0f3#o=CdpaHNr)3wMO)C4WNh1kMIH87_nl4ZNgt453V4j)d`G(d+QrattgK -^+afSRawud2M0IP(u}PekG2EvV5^9CBC$kz-yA#SO~q4un;LVCwde*-d9wp-rqo-K%cBPC+g27M5Y79 -{9oM*;xVu+{uDQN$GwEd))-PlErSR1Oc%hL^lQQO;{fUvECD3ZtHu+IN8God)zc%Ur)^|c -w?#~ZcYF;jo}))(+??~61b82C8LA$ReIdRBb+IH^|43D2qQ$$n--wE}5`3Z(_j%bQ9qKSW`??L}{lX} -ZdjJEQwzE5wnO(pIrU{Nrf>jK;U||HbU(qU#qO3}qzUojLz}3k|uvRt}uu@oer}~1I$F>dH#Rlf6p?F -}|hb}W6tn@vv@@64q!@*{iP_Rknjuh=*c}j(X^}ijg4J8ZC@L*R7JWQyEam?xaNKibf(a#03v#<}r+u -sL2RKC;hC)kHNE~ZG`seHQwPvP2b0_~~vx^Et(s_rTb3MZ8BTJ?eE-{;zxow3{|-^uYHe2TtyAbId`g -QmcY2j(@LgnUZg*WK#dwwai6N*#IJXr`de(3}Ti$;&dH{XrfV;xVs-Il-5?sDm;AgAldcRuJI9R}#)Z -XH3Co2l1jUe}{BYp>Olz0Zr)G_5yrlDS(>{eWT+iuVDDMZ3nTpmbiPeqyU^=aC~#lT;I2ST@t`4OVCy -VA-Md$QL?NQ8Hwd(elg&2s#7R#;JB9~j~s~cJ@&f+8=;=@_J!f)j8hx$|Nmgbk*Yg7a?j9FqBT_K`6+ -^*=yZqOPtyGq4;~>TcFU(B(tX$M5#D@#ef{RO)EaQg6iUiqSF7f(gSQpKXxiFEbP*2 -2zeiJmBK%Vr50}Iyhi}*l*h%abOElbYR-Z+Q=E^Sc(bAEm8ypjVw^$ssiYd3xnFV>ogB3nT0L -|Xsl=tjToi^2LHxmc#%uNI{#bRWT!2`0K1I_t)$?k^R82=rLYjm+5+yigGO@%90T;iP1(5jXwT{A3zF -uzH+Xvq-n!CkMPS@}u)R=M2W)Ue2cdxCg{hPU%VxGc06-9-L2iM{Q#c4ToUqisM@WJJ7+ZEgc4?;3ZW -`l{&TyXX^p@ubb;t(VhyzItt+tL5gh)}ndeYxFsx5oS)F7)0(hm`ZKEc4wWcM0VA!61hlaPRby(3Prx -~mYtFL7KnnK~z0WDNslymX_(#>iDRN~RbH^Li620ws4X08&Hh_P9oCPWdMZ!@v;&w1jXF-F5dN*nvgA -1KD@l=bpDmGZ4NQC?i_*+TiM4`bpakw6qZh#}#khMJ>U1%onJd-=)nwi$p}N7*J6^-rvwOPBdu+ph%7 -jZw_E|Cc7oL_a7@-W}FrPZiY;=m*F5@GA;yd7I~-Do5w)LP~`_t(?hmL)aaq7g+HI5WD7vUso_GUcNLkQl-uiECWt_rTyq;LBMOHK5{E6+6+u0#(1xm-ir~{IcrF9i%zj0AS@E!OmRCN+*rp=eCDeAtIcE_B!bG6k`@#Pks4=lyZQv{H~Fg6Mv= -|t2Smb5&#gJK|({2i)7J{7#cfWR*@GQuupWul0y9Jw+LmXp|p`_Okl!AW#efgR$O+lWSj0Mfbw?@1yz -1tQA2Nd!khMz2AFAeDA!i#(7(0N^sor1QUa_iVH>wB2(AWBr~>b-;^2s0z}6v4#;VU08!;`{)fQr2b* -WjLVYs5yaOeT72j=F~{B^k}^4Ahz2yk7OAjKmx(Xx+Dn5XP_69!m&camwi<{fJK77Ie --m#ST$I%UXtWM#JM^K@@4}=t7}@dBUZjke>10D1tW+ZZio_3=Dtsa2}A4mu$U|23u(>GGHUG<1LU`-< -<_UH)sb^n3;NgAj}jM!@fVAY73(y?$X?@9^t0YuP&{&!cECghX{`-T0gjJE>EvgF(t>mpC6`Sibyvx@ -p9qCxqP=d~b=d>hD!@>68U|bzqA%;1;`g8AF{peG2%8=8Tc!tFMp^MfynOj^KzW|p9sU1$ccw)B!nu~ -f!kL4f@rm_9;0?iWs%?W+3^doAqcDjkLJRZ+_Zr)W@}7cDk#Vp&u$&ow@uOsJ%p+{V2g!Usv8S_eMIb -rjXi~G2e26kAZWe%wcGQ4L7%!3s1nN}lA@+e$95L*WX`P9)LhW#LTKFcTk3nS_L->O{jw&ya-nmXj`I -rnw6l%&l2w$*cxGwUv_Snr5b9v(jmm%T@tXNBlnhaKqgoq`ah2Q~#i%#wkCQs^Cj4ZMHkO}-8DU%9dm -goIRz9)GWU?|-byN*pq>}CS(wqYQ+BZ)*|w358A}4D|;& -di=hAr5wfM@GL-?0l-@XMk9kwobuRYD?jC>%<7)CaCVcJlO6h0l(s?;gDwB~#pi9`kHgS4FjPE*zMbhf2$^@NSA~w{2TbY -Vt3sMZ+S_p!JM?h3A<6wBE54xVEbZsN!V|0@Hp8AMiGh-659zQBP55p=WDxfosUJhM`mIjbeA$dv3OUR4IxIMA!EU!t`LRQ~-!v7uF0xSDP@8C1SXmOuTAXlZ -#wSeog#hj}PnJt5ukA^bqdZqP3zVyutWc)oh{XAD%61CiQ-w -T%>3){Y*@{DiFaq^zd|XEWU}BaQ@B%K9lal(p<+a7@D2@^YgieEzYSyf?T>ynHsB>C(d-3y0Hte!fyM -kBUV$mFIT;0jWRc{gYLrif-S~8D{8g79!DIt~!nT&F7bqGs=8xnp{kEfavF&hbJMv-@wUbNxHd@}v*5Ks^KI2BzBW+Yw{!|ar?-~SpQRKNZq#0+qKl{y6ue98(AUMm!#k?+LdN$==)e+uhB<0BK -^4D}BXrPdIL;hRiqxXE`2-0ASxyq6Gs*KL2)9fSVB1?x1CmeA=LaXbtErP*)9={;Z9Ljgcm_n99*ZAa -2OU)#sF5z!*@AwWAQP)gtURc-=P+D_9bH`Nh2)-Abr-D50`i2n -Xl270o85Zq(yuyX9;=m!iac6t~MoNv_r~QG9S4*21-nD%I`t5)zsCyz89+pJ$!%0!z6$~$FckJ@PQw` -Fwf5Ha4meXvAt*9aYwzi2Px2y)4{1P+jv)d8mrTbiKzK&ooDV&@RKWy<+wR_h$p-NSA2l!#+P>HgHD8f>Gdj -}oG#Fr-}6!D0QQe|kGsSbw}9{@SN`748|vw9lTaAVPG;~-*IedXqhxHFXYSpa*&%Ep5O2J7&#Q4q^UVLaU2B@QZuP^P)H8Q;Cg*WvyfjwAJE-om?<3(Q#?qVvb@n+dd{OAOl -0Aal2ku1fpeZh;IK0G1`1$}Ypi8-hap5tW>cuaWJKQ(tKmcUQg7q3siv(LhZS{A)R%2X#08sx`)1*@J -QUg;uNB{X1}F=!~-9hRHNBTNIn>o#tLIdI62$=6-C#~0sBm!HBucg5$w`SpLrtNe*K{x&b^TFr_oRYs -G<75!qV&#l>-u3o35Lfc4U-JFXp-l=QcyJ`>ecxYAW3j2B~E_E`9+SkWD-`Y%(=p`Qlnh9-G=H*z=7+ -w_NmiM%sYp3N2mZ$NICJoz@n|KUw_PJ3;9j3?O%NjjlMyIxee8^?Z-=J{$&` -tqgSCw3L1OF6sn`VL(X#kYkzdf^0H+}pO{>knaaf)Dc3(5 -1OD=Q3(B;+T?bYQE;_~a|ggj?iFlJJOoPVknB0`Rf}9zVo;Y-$Zl`gyAhgHj$taOneplNOX~_$U -)k{_znslXC^m+yENW%$OwD-SPsVfhhm@zW0yS$Hzs1awc7}GgK4D+V%Cazy8r;JF@E!bssUd3ec?NpY -75?HJYasf!noqC{jEDD^(cp{!swYlS1v4(i@BO`x&&>5Aa2SA?(3z0)@ECbagkGNtY9L?u -=ST)$5)I$Z>e`5g<1V7{>dr(o1|^s&nBx4Xs$oxe{5g(EdF26po4N66&v#>78N5;D*E~YHzG*reh)Jy -Zws}wzh!kJ*wllTx$o14KDDgc5_l`xdf5Qrj6DevRK}wry#S54?4R*6eaCQQs2J`bvv-7E7=3(wms;z -XdQ@K`4D8RU1lx3hRf$-z20YtDo^-5o$nZelzN5iLe(mZCvu-D(!BqjT%@-wxJ_Q9XSjO1|YEQTYVkB -4dUL)+8;r-LqSkT3?8ns~(=&8QdT80OPDKINezO>soLs|q;r{)$XEh_zGeIM4S8Rd@w8XwmA=5IcMUd -1!UpcT@PmTN@6aWAK2mmG&m`=I4Y;ijT004aq001HY003}la4%nW -Wo~3|axZdaadl;LbaO9dcw=R7bZKvHb1rasl~%!W+cprr^DB0ogC&p5OD-MH^pH5t#4~Z5vDNk{LBvW -#Oad$bT2>GF_U?iNMM|_Z)yi4&l!^$h8UGn -Srmn8FCIVg>+3nVPwsKL2rNt+ -5~+d=?J7fh0+?LO2(B?&93Z{huLVdXoVCh;W{xMc5L8&>+f{+SI0Yds(DfkmM|@twjmjTv1B7(6XQff -}N>tKx$Vejj&00khq4$LTTj}i;2TxJ%cM@H;(?Up11te3TbEEQ=}4w`VWtk!&dnmmMzbc)JlGH6)gxZ -!K{8_MulPO2pGS@@6eE?m8m9|arklue;AF^E}jLZy-D%$Fd!&60u7@_m}(7^4w8C88y_NqetsEX(qK0 -;{&L}j_qIcsK}R<2t=@>fK|XPOpG;k$og6OnEXV7+~nq*hHa75$4utO_i -f*XTf^0rx1Tw^SW!m4#4|Z6-S+dd3ySO4n%X%DU7))(bWD#QndZz-T_b15-2`xcF(QwS_#M@oS(y&!B4nO)Zp -){U7)4rf>7bQAp<4aUUS!aaSTOvVz=ENEq#Ugd#n(ry)c2U2(eN(XhKD2(^SElN}C -VC$UOpaK4R{LX?EHO9%#ej(G79L*kK+ZmidDV~X`v)4g$8S_Az9#K*-dEconGmJ#~Z44eDJt*g>0#8| -vr5Let~dAy1D6`^W -OfRKtkZ|%o+&576Vpw*eQIBMb0MZ(C|G4eZf=Av*CSaFXXEr#|S$^G63!m`K-YmSh%++N+NVMInG;5^ -IbJosTMDET5y++DO{Ecf|fgu3gAO`A=5dxqPMm4bkk#lQs|&C8r#6srP18+jJCvq!E9 -xe{`vL_9CIIqII>Hqc}*D?$#eAK)KhHGOu6S4h`>n6&?UO0E>GNf-^Q6=!spv5Igyyswnv163~K9pKO -43sz)Ng7Z4!`UGn}F}MPzLe2e63E`{f%Qjt&xCOfLu-Y;;l5vexUT7wGl6dl?!k55O5xo_%4)3Z{{ZCaT4!L1XmOoQo3K^hT$;sKpR#p| -;{CIY~*b`Y&?a{M9&XG+jv=luc3O)U+rAn(nO%g4;>jdx_17}`iW`ZTzrVl_T8p@3$^tGUkhFeR&ky1 -#9)N{ZBmV#4DTp{V<;kTGn$ga_1CrPvK0-aWhIux(Y%f+NvE=iJ-ZXw5|iq;gDISkC>J;nTSAW -*VG!wb`U(0dB&PVWVWp%@Tef{#p=ScJ6{&#a`d5nhm!np!^d;(0-A&x1NwTPcxHbs$@A>Mny{F?aNp7 -~VOc)JHG3e2eidHM-MlNIh#hhtykOs8%q2^M-%fGXLAx|M=EICRPO3W7bOBDQleReRA^CF({LpLi`Cl4!M -!TOlwym>snz4GqC81cu${LTp9*rcznRY$%C1fs39mYphX6h2f&ZwnvhSLtI_)9K(mMIibd{Aifc!)oB -GL*zZA4uT*CLQn-RC&JR@6bbTu`z7<(b9)+CuSnqt!U@`t5Vii)%4nA(r@JU)xqVVIHR`4DAyk(qJQF -JaxD9s)Pt!FW0i`R3=h$wf4@KT%F4(bUpq?Sf!t0g3kOEX;s@3o}n}+@=>T{x-;JidCR#jrQ~ba+K+8SpKOm -jF{7tV%9eoJiigHqD}Ch*wRP!tPLkP-jWjoFlag+dxqEV=)E~ndctNOofn*M{#F}kx2t8o{QCKqoBMG=#3z -)CA=pt&ke1U}0NW-q(rohSY!C-LCcX>QFkS+)U# -cw!4>n)B9R)L8}sG4Xg+y-#@Y~PG;3{k1~b!10)jTfqDgs(A;y8FRim+X1Y4DNWG<89>bVYTe1xaFLh -dsJ4ob)%+kPr!{EXMO%eJGJ;CTRbZ>USe7oA7*%#OR%!?-0VJhzcIUHo*O%`(w!mBsN`=@O9KQH0000 -8045ZeP8&*d;duc70QCX@0384T0B~t=FJE?LZe(wAFLGsZb!BsOb1!IbZ)hk-+V35Eah&q1ieqm!es< -5@x`*9uuMN(br`&$g+lp@y^q&0xGI?krQ+Hhyxl|V3DDFq{U}}JmJ@>zO6n_9vO9KQH00008045ZePS -ZAumN_N>0ON%K03HAU0B~t=FJE?LZe(wAFLGsZb!BsOb1!XgWMyn~E^vA6J^gdrxRt;AufW-p6XnW`6 -Yp+QZIw37N7wCcl8K$$ndUMQEkZWe6seMw9d~E%Z@>2i5CBPkTszZt<~*|-i3A>ihllt5pgnc4f1s9S -nic0$wQW`h&+y64&hGB+PNo04)pb+H>oV1Oz4Jf*-_FkIMOG_#xy?1eD4HZIYSmn5wW+doQr)QX>?gf -!@LAHRO?j;=^}zt8A$GF$rmPy3CXH^gwKm`Dn|fzwPaX3&UGNN5QC?3}Q>x^u%u)qy^K6+lN;>5Lq>8 -zl_u@KhF0ukQ;$^vhY_QmV$m$wCJU;sR*Uw_JV6((J&e9^;*;!TPT -5YmTJipRKT2}G4%3)4N@ha0ftURw|SFolB$`n%4!W;kPL&^Hd$`ELzi05 -;mvS!{bi{)O;#3mrqPaqzgHSZCWKF5@=1CMn_g}kdGfw0n{rv^^7Z?2nIMq&I3PV63udy?!2MX%+YNr -w>AQ`ts6DrdFJO1=_^Dncu--X6lm>NEZI{iq(zeyhq}Inpt&2KqvMc>6TQ)LcoNSv5)9ky`)AxT!>TL -M}9?JVA0M1%S{Pm}#n&azv3Xmr1TsL!mF~5MtYgi+E-`tpo&|IJmArMCv?EC<91Qc_!ywK}pqK=EJB+ -t^1AHJWcmlxaOQm3zlsJH|5e+kD_7Y*%$eS=R6IH)k3JUD6KsI1{}ybf& -Wi+bHV3Q%dKA;m^8$H_ruJr2ln-?FyKb_lUeSiGJ@hL3p=y2!d -ySJyW-=5B2e)sY1pXVpXe|ZhhpB$=v_}|m7c8*V9fAHUZ{p9J+&dxGV>e_I4bCiF`zGOwja&HQu -M43lea=o|1kAayu9Kt@rm^Ody1KajY80D?(bMnfY7+ydn($ikS!v1tjNVIH48w@)_k-Own(MP5I4-Xp -k~+Qj(5KFwAjvm1KZPQz(FdOY$5jp3w#B#H2~0~}BQvKXP$Cl@?D@L@Y}M!y@4hu%?7XOK#RC@9ou6# -rBLj@#w^9tl2Mt~JQ{)FOEquwAG171{Q}R*Y71va# -Iy^5bd(K{1*jcxkQ0SsMS=2?C}eT~)q;C^&}`w9*RgsBtfE3i1{Me!8g$KgaC~Mv+77@|8kJOMS%W9S -MYS`Lq|(S6ff(GG#P0HU+^!i&D%7o(nh-TnQOnxKV}JW(A4nfD&wyX9)J0M!O;ZVk5h`<||6||zoY?- -n+rI`Sfl1$e4hoXGoTw|>!$7ozTi?IztJ-7GxM_GY%)IdR;H=Ca3=cEAsy}z$;{2{~ZdY}GSD4Hd?l+ -G4wk&jis}=?3kPJ4%zvwb%8jVI|I*i7_qAy8=jbRHw*}k0I3QS$y0qn{Jb~PUNZM>D85iet+(z4cnot -;)&J$5Lf*28pjk28;|N*&SvC#rk$TPAx}rZ==XiL%=EW7M_}=m3gIa33x}gFS=+Jn;Dc0}nsy3jz*Jt -ux*7mZ(+o@Ds?f@kAXBznzd>2}1W~aowFob^R`G6=~bOGq-HxBp7=Et??G -?i<>QH2}u7f%Lt~zz&Re$3DyH@I2@E-!;T5+#ebNrv;-VihJTHwr`=Y=VA|f9LqlI<5Y;fpMM!Ujd3; -Iw)|Wgx2@p;fd^)oY8!Ak7!U9T#cDvfUtZ_>?twgsj2=V?i+Ko{XIwVjN9oyz36+|nP5a*V?tan)H|dD3<jnhMgrrVa6bqfx0ct>~Rf0C+7Zu-!< -B8D>qxWPmnjSM07>T*7S7@I`nE*zLMobmqije;O6w)#G3YNzV+<`q12-E0wJaCXUZf3{-QZZJE;EfQH -XTU`V?$|_zbmyX{p5*Y00}SwPkhzrJ=v%nr6G{SA1@Fk_&S`XGe=nq?hnx7L -w4wucD80y>8q^V*3O0jVrj!{pASStwL%>mXM}n$%}#&84uJPJ35o=kivu=d6B_Yd98?8@ovzuf+N=16OvN1H)#q_!NPDOK -L0Ff}a-)#Hdl;CyImBq_)kfNGd3}m0HtKiiskH*E(wT$ -4pFT?OO}`XmR6@s1OrY+a<1(eh0oKeVM))QKR2Fm6S#BUz$1{hHb81P$LNo*Pe;JmO&AdWJ&E&b1w<_Tz^XOj=~tfs;OFOt`?NeABu9}o^EoqfkG1i+pP@oTu>|>KloI|%WGJ6WkD-r&hZd<%+Ta -e$N7)kt7O^Uy@hH&8@w8>^U|wpIt8n!eq0;bL<(*l^gRnuQffg%? -%5ifklfQ%y+MgJlyfIVEf2duM0nGhoPvW9fV_$-VhXlwC!6#2c5hqrK9co78Kd%)FcK4Vhs)Z6bN7Hl -MqF<6~E0~Gm{#Nw3sQ!<7HoRGyns>{-iBJL#)wjvpxPU&@0(xZ#U@$anl)5lK;Z)Lx631Nuk*p5vl*e -@Q<0HTK5FOQLE$xIp{M{fZy69_^AH;oU}v-QI2|L&Tu!@g5gt&FxL+f_$*DTq^Q>z;l-F8@cK%$tgMM -$&lMVM0R)F^HkQNmu?S~9{?SLSVK&}>W4MUB|9%VREG+@}Z;97f67b&<`><5d%ZwaS@ZS@Evb5m8GYr -e6h+tL3#M*<{z8!~jv9a1>i1ofe-K1+jwYns_N!i7_-Zm5+eE^NHK4l`xlzM~G8r1>)B>~1t<~c`aQG -}xpaZr8yqb@LW0(4Xg2kQ)jrVh;xs<$Yx*GaL(Q13rWj52_Gi&Cd+?T&q->8wRA>^iQ=?6xpJNScBA^ -z`uXPPy!GgFR8o0q18%hYq*GwDtM1=hHG^9Od29Fq<*}!<;*W;cG|)QOn -h7rw)-wNQC2Ah&BNU$RvaLEeIP1(Y^&7`SGd((k5nzvj%T$sUw(ZlaVGc~*mLHAK1eYD*n&G)9%{~Xq -wYA^usqWc)ndw2EU9yo83^>PUbK6rE)GMO?6_rH;h5SP_RZb%Wew9@%b(8 -xBpPy$}6b3UUk?%ZWAtAKH3yh4^?rnpa5|m`bs~Bh{IEhRoslSyhIFf${TH$s@t>>_2bPO~`bVelBl% -I032ir`C4U;RK#x^ts+?yCa*x^V*_zqFZNp-5m#4$%RYqJ~QktqiqugmHZqale1Dj6m130xvifCKx($ -CFcaUIr%`M~=xrl<~_7^M|&1s**Gn7vKQe>Fm(|#}ClnI^6#bEe^BrLST34R+dLQAf4CDdkq_QUbI}* -WwJ3%fr2U??{xxLY(IF+YU8rQFqbg$_@z5_;d_<`y0a7J)p9a-u#nO8u%8~4$zVA+mF$U>y%=T#eB2$ -UmZt&VYQ7j`%KO1!pbsR>WprsK-flL!ipIC+24MyxCh|(cKni3`CR^P&>kPAJjiSUEz@}BlGm21PlGw -^buJHV8)M$yBIf@JP{ZWrVY;2k90UJBZvm(uJIK(R%qK^R%aJ7O2eeL#6YIZS^%90h!e4DaH) -5e5N!XsMcXQPoP+2L4hC?i}Q#b3N>-77hK1B~|Z)2BzzTpD(q{Dj-SXtFiZ-M#m=z7zM7E{qULc;mq^ -$&`5whNMHG^MU;EDhK{V4#JBJCbLPFu&qSVF0*SVl_Tahkq3zb?YBXq -&w!q6;7c0$X;;)ZA(lNwi}rV^3)(i__8`cvA&<>KeW35v@u0k&ZE1;wvwngOOtI^^FCq!8*@E1M|h6b -N-fJtv$%33^DCoUhS9oVEG=y!DoTn00iM^6|uk`BcP1+LZKYU&l#xUi%?7@_p0v{N9?N&Q!E35QsQ_K -KzgBmnkrB?vvOYq*%Sz)kU(Y)lInt36Q`l5chD@em?x=ce~?W;Ph{d&hDg%Ivk&lnVg^?C4<-IW#30y -iW7pxA-3;8jJL!`6|Ijryjk5YC@tX;JTBBZ+3YaMYpt#mG3>!!L8jgo%y2BOZYbO-u{mL3reMbbi^uK -7$;;zoN-$5Bq+7FI2b{CKt||J(?szxL8x!^k^s=llF`6dfz&+EzxwCRx#p(^P -e{NnV#|K4+^N2Xcn&L)XYg=;X1DkRb0)msrOW?T>+7YS?DYYcN>Z79yH0e&TTV!UBd+e)k?X{1XYyy3 -sAFfKQivyj;24h1?;Z5t7XXCgBypmY9TDsv_8YG!p}EivG -2>jghV29|;F)-B(T_A5dHAQ-{`92FuV!lW`%wr8o|0PPz(6iH2m#pmuSmRMD{nD>zz8j~S;}=p;<87y -?oi47a@(z3y)9ABuxd=B>h3+iMY^I7VK&w+aCzB7aP3a;hX=rA0n!iE)C0gj4}d7bs2`-M0-%2%0Mv- -KA-Im{Gaoz92edKqMcqnfTt*EVaiar@SH+0E##D1Yb9r-*Cydtgc)GV$_WO4wl^C}!=*I1;w&A{EqYV!~EyMSH@qUV%e#`)=-OZ-JV)tH#|^Pbt#`$_DVdEXTpXrw;uhi -1BC47(^)D{U9$FZ$`)0b(ibU*&FgzP_c}+J0Nv%!6x*JZrO=wMGclPVrmtBa!fExk^q)HQPq~Jfy?dr -1v~%^FgI0dao}ESvSNf=0GeeBsLyL0B;vIWSRoD+6%_VFot?s*gU*OOC1td9fHFi$!o0v6aNgZO)^6up~6Uy?J{zrsr96>Y>lW6XN3RX8tP&Z|X{uoCed94LMPMsmoF7myc4rUyd%E^ -dB`O*TKP%zA+>p7Q)qI!|%(I?-3GE@rWyR?I8N!e1H#?DE^6D1kNQse2ctIy+GoH)5I&PhPp3R5m43a -zc^6fiDDxi4VW8El>T-dFlO9vG%Wj}}MMzO8EuVVo|Y$r9pthenOg^AWS{@$G5$=raJ*fMZq5E!YSJC -wI5O4pGr7b}Dq4o4A+BCqsHXm?CHvR2npT^dJ*BD)QHy+t>p_A}Xc?HEhtU%i~{vjD=_rkq~vctJ%Hd -cd}RSPPmAAs0M*nmu9P#M&x8kW4tL6QZc3ruNqFCkDkPS{9~&Ecqv}Xpi!txMR_#hGIN+KjFE{ygVRP(2VZX+iYy~G^KA`m$}>A;R3`Brb=j3&BdL -2o_?}viP-;1~(-8x?kJjUZJ;gK)*s}u@_9w`rCD-4#T(JvlPyK)ykO!0kNyN)}kac`dv_{>m?Qy6!cI -(vcu|i3_>ZepUaL`u)+uP}mVkor!?or$6{`E=QzIMa46g^NpsrKdI`uh5Sav^O|VLI}uFNpjxFeWA1o -q$;P)QdDVgC5YJh?z!RC*;8}buScXlcKVa7`!RtafwDQz>%LZ-%)|RnRgBc${HeIzZAghL1GE)(~axD2iqVFiVIG@(K9(<5hjX9BzqaS_rP1qkJ{!$HEk5V9s#?=Vf~I>Xx -=t~GF7&JySW0qnNJD5gr7L{|>|U7U-2J#IKl2*M`SeE0grtI?RS>4hIbx<}z)x{3IYhh!?x>KMOh60$BDz7kvrg=$K9iM8;(YYqb_|?D4nU&2GnPx(f#ZYa# -Z!%A!Jo>`ZJ)b=M;lw{iQ*tCLVT@>XNn%yuGR$&!vUWMTEur(JI&C#3b;&AXyI5?LPB)yd@A(WuJ5gK|OfA0Tn(tJ@*zXmIrq;P;KrB6S#@ -`-;1`nad;&KDWxVx%3AeS{QaCTh-TwL^1v1g!@v!NTR1$B$y3zFC{1!y9d0bI#%K_`{GFw=Mtf3l=mB7Hsu_G;YFp0%yp*o$a2NC7B+Xd8C~iRhoY -nZE;7a4xQiqi<7Qq$*E@_#>If1tT>j1I`&gIH24D2C9`iXeGGG6XJj2z_=F>U==ryENi`AeS)A*D*nb!Wgh3<`o*V`L)~U6OLDGF({1ylrH0 -O4QpjZooDw?1qas}~&?qd?t1z+>e?CVurJm2R?!>NXWY{z+Sk7fyl*G*Rgdaif(vu9cU~fjPziDIwwj -;KE(4z*QgrM=G&XWxg1iz<=v2pqN?%ZQ@IB0?}Z7zE3eT)Cnjc)_Z3eaOZ&ZNDmI+kqKm*_t%SAtIrt -`aIw2qT~c#pkj5>hLS1mZyh@K@zKjx4tpZ{eFtf6vgrS8 -IImEt7=!uL{lNrp&Nks!a_#+$rxwjHSVULMq*utnQ%*@9dkFYg)3g_ -hD!HavieH&@^u>Zbfs{H1Z_nhqx5BHnblAeFslWpm6ga~eJn>Ytp!vcZWGf~~I;Ujp_U2EY4J6ln+x; -fP>}u4JP3YL&<>54U7shLw22m#ODgZdd)SZXb|e_#zx)j^0R$AG4#M2&u -0y{27Kq^u=3njNVc&iC=iPAI8BQ;Dy&BM$rUumJ@N*#x+S&ok+Z7A%9vivVFj>PKxi~gkV}31hSvUdB -wZ`jU0p%uPMDXQ8NW_7fR8@vo)W~MG$C55bLddDko5VkobcoE=eWnDM(`i6yt+z&DpowAOZ4pi@q$x5 -cDRR=zgmqK)s}@;(K@|o`Romns;g;&yTUkb9SL~GE76~byh?pD2BBI5LtkuXD)|`8pCbl=c2H4D{zPZ -MrKvJD=Ddrv=K-r6#y&O2Un2Zj)TlrwUoDu~|E$|hnDPN3L^|Km-(y+#2K1av)a7sEp^;&;v>vz>Y<; -DdfdzqKDj=ZFI(0ox!>Yf54C6~3L-c{Y;eaGkDIy{cEvv`XV6GtL@*)IU7wAU1Kf6kEPOmE-H0{%#>2<#J#hEoAig{Yi~YIKQCKo -lPxdFZWLypmU6chXecP#cKX4MItT;R&cCHD=m4-xwCzDvtfpekFMx2AEf3sTS>|1Ep#Fxpd`xN677iS -`MnDWxP@=y=1|B%Qk3Vq+L>-j|f&nK^PPlX52xmvOC!$vS`>_4{`#1?oPw*z_vdfg#wpidG@BX}4^c&|6_lV0&cc -+T7_?xcE|KF^Cht0}*MFUmEXB?{~^tj|}Td*E-KBR%(m^Yr=34h${V1 -BwBc+R7lPh`nkMx@(9$-DY~(y3n|M6wW}`kD{bdjF9xpilx0REV>XV$aA+I)ay<^v$iI?&@DyPlZ|5% -Xd*chec|!aa3g)%fRG~sSC1sEN7{Hb;6IW -9VnQ%jfzqrmez`JAf&KoN(1q3L%7 -sx0R%lCV4g)Oa-%D)GSfF#V!6X2~t@U5;e~Wj2iq=9%h -^_z=XMW&s#O*s(92JrfZG_~4{bLKpcNG~Gdd4SZFTF9av;?klZ@6R6PB107Pj9ZaATNlon4N)DbZ1Q4 -Sz;av96LDTUnAW(#W)3s?x_MQiBmrAgYvdmoPNph8BbWvejs$|nXT$&!ZEfCL=iTs6T+nze%ShJ#7Gx -;LrS82R<&UK8&4@u5@#8O}C(O`BUj=bVCe$_50j>{v<7qi`G>md203P;9@R0HO(Fw#_sFT!#^~o36JxE&tJhc^F5FWzG~=_+|BLJUc`#82^-TrGzj6)k(@9IJApjOMtv!YE!X? -2|7$8P#H9(^4rtz=(xEN8VY;+d{Q4q#*NDJW~9kyBGjWy4RO4xW6bZ@dieIlY?+RQ%FY`)K!vdc~V^)4934Z^A?Siho2?NYr%yW6_rp&e8& -3dJN#s{o;PoY{;~7t(ML;haCCwogc{4S<~;_?d@kInB0<3jYBQL<-*;}`1?d2@Q+2whi!qpfTc;VxE- -B3W)GXLnUfqsPAADPvtIiyEp?L_X6g`a2pjGmysth?)#&84-%^v{bUQP@Vb$dU-QOY`Q~o`42@<^rwT -pI#ICa-l=vO+|sBz1diNf|RxqwCzM&kQ5$jBC@+!M*jNUyQ@@gmvc)!_t-lM?M-y1oD}qSU3?rr%w08 -^H?O^Nk*xbzqGZ*}+L`z8J%YerdrNJ{HJgAdvDV7z{dQl2C&SEA -6Jh1Lj$}Sd_MdD(Cei+!Z3d@}+36rmBaDtXjh!J%@}JiN4z?mZ=rcc< -6eiplsR|5}7M=HfuPFynwdMX0mMFmZ=1h9nl;j1o+@{rIEJ>T}7A;kCa=sqhxvjRt0ehYTGi> -kO1Le2kRu3zfajwCt2kW@bCFaI0>GbW$Q`!y8)6ZB*S&xD)RkY5qtf*xx49iQ(~OFNsm=|KKjPn!>Qh2(?fz&3@+priaxu6 -`H52hS$z0N6Cp*}>7%7|t$o3L$XG00wjLTZ0hhB$O3b1<0_IqsOvkK-X`?M9YPzNQ*}+-HKi0c++~vTkkQ%SD -GN*7+|Id2u*$g&`o2FTc8r>O#MwLP1jd?Y>n`Qd@z=HP&z@fvU|3}d2Rwd<&J*B`IcZ -5|Hf@qYkO}7_2|+1n4x+6!qyseOXLuWFXl1#l!|$SD?xJl6#$P3F{GeGF=HQ0_Xn!&d6QJF$#$<~V!F0YvGjef*Zp{yMPxEm0F6F3UVt@6fz&9G|DjE^U -o8uEFE``BMCyIMfqN)qHby4(~?)d>}Yy1^$&0kxx8}GYI>s24`lj -({9A}@WHX`2v*{R*yP3xkht3!Cfd%)x^&s6Dx@VEq3DP)h>@6aWAK2mmG&m`)Nr-hpcY006fF001BW0 -03}la4%nWWo~3|axZdaadl;LbaO9oVPk7yXJvCPaCv1?J#XVM4Bh=Jh~6N(4YYI%5Zv6}+ASyy#bRP2 -isU2Zr2h9uIc||$xCs1skL081G$hU-lT_6&_~=QXOCABgNV32_G3;D-yg2qHa~fQx9R^<@B914JvF -mj)m*?%sYo0QpRx>V41EV0H(L>GncSP0HF3rR)``YKy;m5fSQN%eVUxW#IiCPb2)gJ@OqD3*<&qbTfq -cT^#mFzgnMep&jt!ibgxnV0dMjd%iUs^UZ7m{tjSl{d1tUvEyH{)&1ZRO+EM*H4(DOb5AD53Hj|4{T! -KV1qdRXB}@eg#a2KL4)%0Z>Z=1QY-O00;mk6qrsw=7ZQnAOHZAasU7z0001RX>c!Jc4cm4Z*nhkWpQ< -7b98erb7gaLX>V?GE^vA6J!^9tHmwW-SMY^jY()hTyS<@t|#QLDOC>$FHOv`UMm+UUw;rdCGRH4F>w7gcGD8%_-uW -qFlpGgfI+Uyc#*XZSiwXja*JQ&zPqjs0Y{?qi*;_1>x~*J_#88ow#;%~I!ey2tMm1Z`i=&Qp^uX834k ->PlKH%Jn9#Z5y6bo}G_XW@el1v$1+tEp?@rud_uxR!x=vSY`!%*rb)wWA#J2*}wuFI9im2H{4V3#}9u -@s~I+$EeVwVTvzF$o=J~cUCyAzSe*!h4(a*7;G?y!F0^epl97NOI(Gv}ud#YxX_%N^x(Bb1zCHNx=4^ -Iy^!oVZ=GXZi5FddAgUZqW5&*hobYU`F9rA!>tYx|ZOt_ZcpPjuw;AeY%tLIrG -^Ov=rHB~lN7rLJ5;ySC!VkXSb4+~va%%@{zF3Tog&d#@+)EL){fMHW&W@0K}$T7gb$qHy)F1LH$veLR -Sz&^`%Vf1Bh?}1vC6>wL+MQUV)UaCuAb_Aq9Z%QNIv6VVc7gslFwN$)gvhys@>TN$WT2?zfJ2*SWVSp -=Fx&pRTi@aQ1jR6`02jOvZnJq5i1MFdxF)&!VSTq%EYQP$dnXXiQn -HKF3cMU*a_5eMa=eVO8NwvK_po8ppk(@)3nD#sH1k2s_0SXiV1{^Z=q}7Or18MqN!`jwBUXa2Y;Gs>G -A%3MlQP(26vSOo7ukao0_Yx_B_+boRYQWGY&lZ^z?no)XN!|9w698M<23a9rY37MKUa6G8xdwQk;5y) -ST5ZSJe5-D188!`=Q8%U2t9q~EJO;%ZD=v{hUW=oak!1%Zr28Jx?5 -n|a?L|JH);xpdK1wfqYer@cLo -9^v~yGSC6-a3;sKMZY_}Z%s&0dEnB=X#MgMuQx0r3cR -eh4aq?I88Tjdh5YNef;zb?dz5=)On>-XnE=nrQm>uLp|RnZc|5lTC^6%w*pKDFA@+E!*<*e&g0{!o%~ -LHI@`W8MDXICLj=?661OAa@1i8^iaq*Z5oXn&AF|I`F=Ra)NW8~9wjvdwk~XtCt#H~#=PNwm&Bl`ffa -DV(DI5VPhfic-34Fro;xh|IeKeUmM;2M>Db6ci5AoEw}{QMW5| -5Qe}Apj?g;uOpsVYSEY))SWe=8=0dqbSf#(2bN$P(K_<-B3pp@P=hsu+e4+x>$IrBE-D-20fcS~n2Dx -UxH*7EK(YuXDtL6W5;H4BYQPD84RX{2ms)kDoiwqt*(6}eSoo9Rv~Yt>xuIy{9|!iu;8W24fjVqJaVc -sL8#uM5SfU6)A$nQY8*3s0K(L&E0a43T0yF{y_yZj-E1rNV*wjj|H(8YdrIkkO5-Dt&iwoiy68qd3aS -jLPHUd_qC@z>9HyOw=5V`vYr-#SKYH^w3=|Gx#L0Y<-_ENdF)Ok -f#)bAqhz$5l38<6_J$~gfyiU!_#P&z;qo6cFgLmRHn?YnYYI32Kiy1I}-_9|EzyPzyoZ^hLn$}Z$Bms -c{pHDgHg~)WLIqL4Q=nS8Z`Clh1W}ZprmMpl9)8uc|!(_h@=u8k`+_v8~hN8CotK7G4Pln3LHQ`*!q9 -GtWnq*!xxXo#D$~IF2Eihg(8S%5I1ljpFe*lFh{-Q{5A!PZ_ius_k2i*+k;2~L_>cW -NePtlOClj`d74GLY>;9FDudK#+F4=9WcaAl)2>T(u^0B{hn}X&P8u0`82D&N4e9_Cti%XM(xH_T&1eV -3{BiH1Szn)f(&SXOitMtgELp)>!$A+{cfGibWm!N9(1XI|v{DWTigMeXyv9?!@c%!7o>#+eT2E^?}S+JAY!taAl6%@j=cCmh>a`$}S32HXvKByxx-kU -JpR@PvQz;_(Zx9J$r=QH(m+8R#npMFAlU2ag^NdWga@cUJL^^|l?%F;Ix?A2%F)Z6;qEMBA{VKYAeL2 -pLg;rL)+#WwlQ8?60(oNo0$$SEEU1#e#BT0sU%ByrF0~Pt+fhC&|-rmR;?xvk-tJhfOPUU{S0cbmY-4 -ticch1$btYrwcu_U8Wr%;RJNvokGtrJ(4l%6!c-*NQ(=U^m&)XzD*aX+o9Zpollfgt%ayu_y!GAQRiB -fvSS~y_jB+FbjHg^b@>R-$);N9fkMwEz(OfXy4U2rp3(SFE`fe(+KJbv*!#v`G{i#icvQDvs6Q&MY;5~bDF3C=Ocqc9Y+P -79!vd($I;zHENap8xT2cbhciS{2SkwRRu})czY?T-28XFlFtNv+YLX_p>T3%t}}L_GlY)WgU8b#TJG2 -hQoAM(o10L2iTOGA%EUwxpcZ>*bf%dTHE%oUFfdi?p6@cW$pY!9Gi~3L!trvtKM?NtVakM6Z4w&-lG9 -%W9J6-jFAXT|&2~B2@In_B;D8nYkPYG6<}~6Vf_}{@(_Jw!F&{xRq7p!QD`MbQlsTl(ZqIU-=p192 -k%F!YJ*~WR_U%-by5S+#`}~9>{KKMn0o^%CZBsN;7mH*E)x@4MRsp%_%|-CLD(K%VLhqHD~wga5NV;3 -HlIeD}?~B)4b7ek{HSpm~+HEJ0Q}ONwy)6wm~`I -K)`QE1=rBuX67RsOuCisnH5TLgryfDtF+@TGygNO5kHC5~gjo8T^h~1Ho`gR}X(fO_I -Y2hBi+|RQ;R5lwxQ!P889M?+Ma)Xr5A(|MJiFlF61(qPYT`Kqyt@GfAKg>o=r@$21j5W@C;;7Mq(`WXyh$N0L2GEXd)1m9973X+zEkLk|Z8jHi9RSs5)LDO)xO!Q3)4K$`88B$>xz*2-6G -Q0lqlrmVoTvkXIjdFFVV29CYPU!)cWMMrI8M{sC!q)fm -c{BPpqZvzaV?f#RKJw|n}W7DQv&{@(s(mv47-S>)^b7H+z5b?mzy_97C+KV>XF;hjF)?4 -Bf~#oPx6k8_OowFn+Nc=i!jVg9YQjL)Zcf@q2+J!QDz@ZP-(gI=6*~crpKymKZ+%KzyLKKrR$U;Cmc< -zc(sK^1)*^$z1V3PpTS>ya{Dm^h$|ViM5~58)H9u+7of+&%gW^ix=C(1#tpLRUERR$Vfb4>(6$L${CZ -ZZ9zFL_W8iu243dDGFugV)(APfcPYgShM3XMSTktF=aKeVkQtu9N@$1+{7lCrvZbXl+SN7ZV9?%M~3?alq)C(o1T -J^`J^;(Pk0Q*q@Pt!E`=Y9M`OQooz3!S_c8uLmKW-INF49i0vQ3BE2tdZ#rZN|tZbvc%c}v&rf<)xji -*pRDyQrh$$X5&YTXr-q{|0Nr%5qjoQ0RW=x0_&cV5s^JV>vK^q_zdJp{vqB>Md@_@(I)yntk=bUoq+H -`{{Uy=ycMjZN!SN|90NNBh#BA)8a`%B-j?wL@p8VnQ^DkH_=gN31#n=YU_ns@=!D`UZ@?4m;CSOdPx&89xOIyGYrK{i@Bfffyb -K7-{{04kp*gb{+o@WJZD0tzV#oyS-x%&9@@#9Y~dGvP`=k-x^^K&hQR8=@RVwUb4u%|mFupR#W+Sv#R -c-YwtvkJ{@hGBe1?SaT;bc&0;j2}@7&ybU$kgeEjTibiIl82K(_`JGV6EkNRLH#M_g(FfHPgKD? -ZB7qN~r)7QynK7S)0Ue4QnOZrl)K}PjbexMEjzLvD2gV3m}n@vvn%WaXKgiGu4ev63aBZgXdp5yo}=G -I(jfZ9%7F3KE-cb{=fqWNbX`e-1?QApa650hhsKXY!^L=9GDnVhH9ALNNFm=9^KcB~MDxR>pdwc$K`zy9hd^{o4j|Y?6s!BZ9)a9iYUiS^((ujrtR -c56j#A5Lm(h1j?t30W9(i2S=g9fLuZz9n+_TbJQPeLbWlubnyXUZ*oqnVY1Qd5n0}6xYNdJ=mZ)V!h; -b&%BpCF)V*g5{g8gn*3DWK&H7wdK{k<%tkX!I+DQ7iUYd@4$+LBaby$u$;>?!0a;24|m0)L^I08O>f!jZI!V4#*s%xN;+*`&MCpy -hi7XP3YaXZ>+`v*flge<`P`-X=G|b7?>Se~zG~;;&%cZUy08(UwMzFMzl5)UG-csl|>MGRDMZ1o{R{P?6emuR^ps)+Tohz%R$X-WhdAx~sn2A!dirFINF -CL@uKYP4^F$!Qv6oRCY;2?lQ+4M-q6Ny<$WJO9r4Ag2WL!3VcM4933`x(%%C!t{#EM89vHKQyeU6JI! -(Y|74Hh`&~Mk8t+uhN2yR%c|z{0X7jwi6h=V4{ql3@C1PglJyV3C3BjR**1BZE<0n>QRxqjZm8DzgeB -amBxk?`;8QRbxnm!_&amG1(IMAZ6AchbXDkt0!EoP~H!3f=cmA*?J9gPirj4UY0inGGCAv93A51T1u>a -Nt<^*Cs)$EyJ7gC3Wh$)IA%q7(rJO2nBvc%42Fzs*19TlUz0?iQL!Y)v>8gV#1iPJAR_0l -TSeTws43Uf!bvMQ9-QhT)MUD`9r53=!kbmy+lEJ~cXNqhQ{W=2|SuV#9SO3LbIkH(2Alg%rPJq_@$Z> -@APTq5h$x!Pdv1S$R$S}lVIhoDfaVB}FvTImJ^d=wJgT>N!clyquVktSF=yDE*9Ef;@Q?6H74 -{5VtYrTJ$ -%KDdwf04;e)H{(X3cfbT4{~wGd@i$_&+$$%eVe^Ai9P&UhM3T#Rfh$7GR;Eee@=|Xug|66qVzi! -#y4e@`L^Ln|6GKG8=A%CK$2let+vx~Lv;-?S4vC}^i@jvv@zK`H|o64^w`N7%Y_rDSSABgBkko%E+sd -G)L@_o}-uty(qS%Y3y(df1gHZjppBD%GMI6YiO -&#jHyq)0!TEU$qY;`d|OaKG?E0 -pO+om%dwH*J}ls%if*^|e)7P(;CHMf!dvZF_Q}69nR8d{` -1DnbxVw3%z_VO;wcRR<$WK6!q?NwX0_tGf9Y3FDyk -aEh_sakkuKu|lD3yfz!1P|6^jtQ$b4w8A{e<1Bxh~7BfRw_837qS1}w!_Bw6{C@7lYK3*X~Uv~X7tV%7X`6yT5Yb)wK#Fw6cs?vfc*; -7_R#e|Qh}if&QllnBRj(Irn@8FyNjn`3*U{9Sk8$WkN*7U5ZdqB`cT538}MM--x}z<^sQ5GN&w>H<_(Bvk#bU9<&!q7Jx4<0)$1ws}6+rFNSakQ}lcHq(EZEHZq>5e( -rrs%P^0u-A#ceZ@W2FGVEoyUavXgFc9{!+qBmrks^Yy2VK14nSp$NY=@$47i6Bd8!u*c -yh9L=v -K>TB;#O{5j)qDPtSn2?9hsp6ysNY`u{5YCHB|A%~HY+iS<#}!Ah+`8^&!gE%>E4>bg5?xb#fkmH{)Jw -*^LKzZ5gBG_3ZY6m8L|N5bF1yrbq4}^WvAh6NX}B-mBzxg!ANgwymiUo7d}>Xl))Sz&q$1OA*tnmQx$ -0oa-duY%Qs2-A208LHP45R~e5fH>!+GIP%2+kp(E~L)co9@Nz^=u*VBIZrpvM?s$3oBvZg2b`RKkyPI -U)as6nu9}bG4(t#4?Cr`oo)W_%XNeW?_Mk<@U8qfb^e{(7G>PhlAdE9?*2^LYDYh8F9l*zDeK`zVZ&a~Y!!_E}Cir=r{*m3oKqTcEoD(JR -%|KRn7b}4^z;!#%b3W(dZKfI=Zta(motL0j0>6l=x=sK9k*4DIOqiv05 -TC|{B8_T}?@ZlHcyzsq~uJDHWtyGrjzLi#0qG4$Fn2D;A&Z$)+nN)zWOn`Ckty=XSY#9{Ps`AQcVRwb -{vURhfc=zrdv+_$Xop&=d_OS$2XDpz&m9?_qZ~3WMoW6bnJo8d?#uv-8%O_1;n^!P*etKFI?@cSoUA7 -f>lC^!~RR<2^Zea_$W-H^jtnWH$PjK9rd-$w`lk84f2fo8r)&*=Z#zqsK$YXX -f_iCXIgnA&kIuk@R8*c>yEfoVC(*1{S7B=ix`!8SG?Mgsc{rubK0Fm_=m6`|fLqbWw2H? -pyYIQ84&DXWwhcYAY}c%?yQSr>9qJQdUNLrF&T(PmiPgEXdux^3@h9gm>0>Q<=tPka>WB0<*6-nP!1T -x3aqJjM6@7<~XOfihE;tW5O&}#>DYGd7LsQ56x`=hIobl5HL;>`byG`F?IO94#|iA|NQ-DvV?o8tQ20 ->qfc|%M=0WMUyi%bMsv~ot+5JHq3$GC5XNBoX-y1pT69TPc!;sgVlh8G4;sjx^hwD`kfvW?kcIV1G&w -Aa67Y2ZVqJ48nt&SW{7{5r^$4Moxl0U61Hr1pQ4Hmk2WO6csg?sbkT@3EukqAE45-8wl)!PFYHJi!%x -G#sX=%-!tT`OMR%qB_gBe9(Oy@~CPQZk%O|R?B3TDem$xWajtQPzj0WlGtw?LL_CG*Lc3fM}h^M_rgx -Jbv~kq#sN1Yg0@Njh#}6eW)^#^aVV1E{B8s0Scis4d`Uh@Y3SNI)2T2{8wj2a=ix;#f(bL0pq5%@(uM -AunGw3f5rXSZgdLW=V-ziJHd)7%anZaBxYP>2<8~-?ES`mR$eRgYPv}Cd!hP1*OZbR9#D*nHVz -?bIHn+l;$NRrIoOD%>ycOEuFnI9Pt-}IXcS%E~IR+*O9Frc6e&RL%732neID30`^A|5zw&8a~vchiig_ -k*QWa0x5ofGZgnnXh2E!3>we%E1HKGNVJPLR>gMYb4Vt3AUquVT0!Eaj)OJIZ@ZuSSBX)s7-NFq -cEeAqwYL^!sD<8>*nakl)rIyLbcpqnoH&kIBC>Yp#OU2|9`!3Ik{V4c+6=1qH_m0EO3VDmSt;LoHuiF -FRXlEeFJ4hI29jJ~sasyzvJsr3g4D1Vr$!~}nkehkc7a) -;wr=K7lYp}&U3&xzi{NmAiv38bBs;W;sUxE?mpFx`t?riz!Xy@RWqXM3lTOT%#jCM?=XTsf+-hPU2#J -hs|&zy~r3W^-+_i^Y5=tP(oh2+nXVHwHQ*pS2k0ME(lcX}wkz2HSbKj5BUKCzf78dw#EKAm2u{3JCL| -lCmZbM%}nPF?euQKKO7jv+xmQ-(kCC?}HF(tYm(!wvM;?QwLHWt -_3LOeBx}ZH~z)4MbGiXNe8NSB`+<-+?01-SX!w*{2@W7kI7a&58(fo7|r`8OjLwb0xK+hU_sqJJ^QaQ -dVr_+7Q|7CrKFSE&XSOq7AP1dGDU}lG&XKz_pVfGnbAd~Xu<_KQEB|Bmocc;_#KZd`blHWiRz16II!H -()HcI3jO2}KSK$F(<&S-~5ab8vuj6vb~?c`|&ygx*z`?B(GegOpqw{U0+FTyt3W6Y_96J)9149ac~dL -;Mr4ACnFAf#C~$jL9$l1yD-^1QY-O00;mk6qruwi#d691ONb~3jhEj0001RX>c!Jc4cm4Z*nhkWpQ<7 -b98erb98cbV{~6cETGhttT&g?y7l4*ev%|-XJ?7Q#}=)#xpk&3U2D)L|2+RB$y?zBj39 -7Z;h@obH1J37!+T8DM(-dm^{yd=+%?)b_{~&kuzD*?H-}$E(@-w-rFCMZu=|}PNvVXja3d_fuPxSAI{ -Acblueq!Y~Ia$@}U|tasu_F?=9xC?@LQ+?o~#8zrsF$Z*E^C{T~{z@N3DW(w996S+k}g12`m*Gzg_U> -yc9y(bxY0u54c>!G7hLbbl7g3QyK&g@-pMpiU+4apNBIG@vljWCr$8yF?qt=@Y{1O(J#hp;EHY9lwYC -dnU2RhkB|pNF6zN-~H|8%{oNn@Tuj32;@9tx(b(z)!T1#8C)04h$=KLw%`(u>ES-3Q~i&WsiOkSU`3@ -RJ=mMNQyubXzL2svgX9P!43)1Q&=u#E!}dNSybyeP-^1pqppePi;IW7FdN&g1O6~3U-}|R3DMe#^di^ -Z3CCqWqp^?3Cs**--&2=?l8jHIxN(v+cRVITo33NooH)@e(t5)`b%eMeAtyN_2c3ug -3P>uua5pu6~y*zf?x`sp{9@{F1pMH$b=Jo96F~-TqdfHCP&)-LdpE;aGlSlbL0uI>+~`y)gq5qsaDG(zfNLZDr}lmcWS=MW>u1BrK&EYN=1cMt5V16CaEryOdSj8t@>@0mN_<=uTn -M73#D%>U1U)T$Ytefu|!}2p|zDPmU&Sj)_PVp-?9q&q{-AgNpkZkuUi$`d@A$Vm9FY6(!zQqjm(>wE`S2ts>;JGs*-CR0vZ5Z0bN%ak?T?yd -ZebSO8UqnqR=#&T>(0A33Fd80l<_uN9t_>opt;&nN`pvleQPS0v@_epc#R{FL&YeZW)y&L0V-7R#a- -neWY-X7rF?myCuR;%5Zu2unQp6*>zIn*+OTPJ(Ty(Ix7<-^~yi2V9Z`9tUIBZ353K$ZbKXduWre;;vN)Qi`rACdJ1 -mO47;bI7IXpdn5xzQleR!fK%J+gySH;X5DKF$t(B|dgtLJC0Ps10_!*9>tynKBa{_y;XLdT*8+}j~@QI1^aQ1J0@7BQ3ZUtP?L%B0Kta`-k2u0ZN!6$oMln6F%g+H2&X=#>{pIj);fu55oY&$Dp#hUa_ld -*|{Nn6NVkUY36=;s$jy3?biUv9~Apv6p!W{Ga8OLv&cF`jHvsa~O47 -Eut&j#PE -AzMkIHHAv^(Ve8FOAe=7&;D`{qX|Xr{pn!G$On&q6+nD+-wrZ?m+dq)U#xs=(rIT$Wj*11vYZ_*6z1v -{9*5QUS||BAsP|WQGR{>T?Gtw0FoE&q7aNs!xw%mnA9`{!I~*!oYwq99YF;}u)S8xII46&w!X~rGAg6 -0D*PC%j|6OTP)rSiG4NU(&||b+EPP|Zs$4C>N=$X;=hCmrD=>}W$whSu-QU1Ar{^JvG^2t&k__wXTi>^`P)A@!v03g_!zz~ZhBSk?IPz-p4qBzsgw#r+8;K(;mi -UprAD?E)-zZ)>iaxjyi%W8=-bea%b!BJHQZ6V`LYf+mA*rF?2%#km^55C_kj0fgGi%Zo^#wah~_N>|w -N|CleYzO_Ig=pvly7$nk-zQ#iWK3W5gXo6TxwayG-V7ho(CYmTe33O0f-Ute*vJPA -tT~K#uhP{V@Vn;x92P+ogGVul{l|8H0!EuQaz^SKp3aEzxoHB$t0k1Sgaa-s`ehsLv!waa2Q!Smjfx7 -9pzFMcMPMcxxrmu9GZrpi2;v)~RS9b^cV!^?I?=2tU#x4FPx`KrRC@f*waAIY?00!4d5d$|=E84Souw -H?sf~S#7oz?8of`;}j98;q-y+hL$H4eyM;*MCCvm#kmK*BTu`>w3O%%UYhK5fIvV9$cYri~frnOCVRG -@WjE7n*$w;{jedYC$mnkt;sw0$C$&!Amu|Wn|@Nm#gdw+v6y}O`ZMf$Q+mw?p+k#-EG^m6n5tga;>%K -rPf#8kWJCn==~#as}nM6kKR(JZ_)VypD&|pAT0nRe@;f;F0#g1UUtBAn5%(LP=KTwz=J^OthV{f18nK -TZaNtDHt6Ub;nhv!qTdMz7XquA!0=2q?V9e(sHhTRzyc)&qP;^5mrp-}s3cdD`;FZ*VYI3){Z)~IksBqY4mILkPRM@k8d12@YJpj;mcT~{fV4}bE| -P1I1OSX}P8y&ws2t8}8W{HmeHp1Y_|FgMbR!BjJi+KLqsDS_PC|1ORF*TUWi<40H5`W6fIy&uzFoq;f -!$H~-amJL`PB>1t%Qcf&|Pp@?7!%{p0hCiZTG+=i8_yxl%?hGzI*%qA$we5IY?)>WfVg~L;Gfx#MHM8 -)QJYQijJ`H?(TF2=E3p~6lfOY#l>z?mMeX*xBvK&`h5>5)9nIO?rMrI)LkP1n-2t|+}(fj=<%B4&Q@D -$HIoIyy!3&pJ#6o+9yIARn8N0~vd{d_r^ppLDq!RCQK&$72}*ug{6<8hqoD!bD<0d37YQI?{KUxoLwj -Hpqf5m+xmrwhQP$iFh>PLD+VDd%4}t(r5}?yRd{8lJC}rucZLv3jKYyTg0wd!9RM=$#nlsKd&BmAE$UmH&Bq|tIFj#0 -&+0BDA1q&^_x{01}G#z4kfb))P31H_{mhbd*61FV=>x_z&jVP(Rb;TaFZ9^JwpvbTw#B&0SlOu&Q<$e -=`9k}mzDzH{m67Zc;7XnT4HLwIdx=M7Y_v${bCNZl>YA(@gFH3gM -NA27#f}HQ%f|Nti6QK?4S=DV)zsp=|9kw=5-`Cz24d9_s?vLT{z50N`y-@#s3(U42OpTOy%%X0%mti` -=<11_gGq(y_y-|kHVJhAZ*8nQH)%$DGgLXYiAC62M{~FR~auT+ct(SA*mG=G2N9NN -hSt%1}P`V9{9>Xi5SA#xVf?o-AM4z0DwkkoZqb#{j8|K>>f$Fep3uNMxAFT1g_JdERUpNfv0#216iy)#% -^Oe2}ukj>-6s5?6C6Xx`V{-uj&V3KQV}u$Wc|%LNTQE1Y>WAvFqOgo94$RRJ0q2HOV@uH-JewpHe0Q -)z=c)3Zaeg7R#*EglpNl(_rofDavE8&Sk~y~4F`|X^&8f?^JgL`6`IKoPSr^khPbpPmXzxcdK^nsN-f -tj`=kl&SVP;U_-zxwA$Gt~Z>xM%)IEkWz5Z}n3ZEV-`UHi>OFzeXlHv###@pRxaAOiot$a%eEn*^R_E -S}1Q|Lu(sS$qjhyRBZ~`Emhgtkq?5mG9o@;_8!LT`0fKUb9XK!+_YllN!}#bUK~TP^F^q0J>YJ0B?Gy -j$ge{2YZj7s1r@htPW&}jxzR_%~okdY6D@+^2H+BDK&hHa4RrOO55SUVyZEoR|3nkg1nUYJi6zTY?iK -o!jy6q?=VZ1fl|-iN1WI}{n1MwCAg7jWNHgG?+&+Dc6(Ol^r94dia;!qI0iMS)L;1(E~)Y5!$B!uM0g -k_uk;*_+vZ$ms8FHpFAd<7c~!DqJ;nsX@dqY-sSV{nunA1OL(9!5z~s?|9u;b_N~;8&WmU33WI*Q3;0 -&NlvKHnpCK$m5VN+b!!QE3bCW&L}?Sc6r&_60-HJ+tlb&fwEsXvHV!EM~-46n+sbT(20p&&sU$i6=3w -laNX2G>NC9Sl@-0gE<5mq)z}La=zXSk|;MNt#9|AXhDT%yFIm!b~rclzRpuLx><_ -ckIpL1;`2u06X%WMXg8Vg=bimAeenjZWD0^iF)4-uG9-{b_1quB5pie -ai_hwTQ7 -f_(XuOVi+GMh@ryJ;mir+*DKcIf$mLKpNZGfN|Tht -T*5A -`=291?~TduZ`}Xu5uvLr(-CT -)QzXD3LmQ^Fkbd7)j{(92pCfO~T9zQ$91NZ$DZUn)R?=xNLJ1r>9I(?puT?wE^~JE?Blq3WzO -rEf52WVW$sEK6nB3+3P_9-Y)cxM)ljX6_ziA27_TOk)&8762S!HAS0RH%7^{zvCyL|H?>eH$Kog#o*S@OmyqWjWlbg#<&dqPNjt49P_KXkn}Dvj$kE)) -IP(Bj8BX9JVd$jDsxwZ?*{U@6S2$d_eQ@yjaiDxuwiYadu}L1zv$Q^t<{r476Uv_^Xv-qHSVWtEF{K39*ER -6XU~d;`56pi&efJH#0jOp2z$mO22xVK9 -QFbO4DYxlOMf^rkHGBSs>drWVNUuGk9w2@PWc-XH303U292O+0wkGqw`ZFL|D{&5 -31z<=4+YWn=GJVT!D@$8!veaa@DPOKnML#c}F2%s)AWa%9BqSPZg$%SiLWlWbOyyE{tdwcp?{^d*1N6C+wg9T}ROW!#5KagCQr7@A4WqUMemDz5vGFRr)NftY%zDw8Zq -Cn%fh5VH-a2Eq%4xP@h!%yj9S`_A-La!Q-!Iigv>q2n3|#638>DiAz{pa8Q9!+Pk_b)D!k5}m;%a=b -FWwpA#xxM=|01-0S+dp{t=&Q&7@5vu_g7BXMW6~S9YlEG?Dw0M@oMk(@oJC|+-=WubhFl_#j9-f4nGs -E^sb*3Gtj}gVT}exZ-Eob&k{yZEaHL?9>5C|p9IDVXlhkF9lu??k7VQhAy=nZIaXh;$@B+MlIa2$N` -}6g!)P)WR&JJkGVfX|2yl!pA%KM`92I>oS*}Ap@)0rVTT%PL=X^4Ty11HtAIT*ipvF|MjVfBWUu=;PU -@3*;+vE3xcj~7tb8O1rVbCI}we5^SpZ>8Y^yVlWbyc;R8CQA3!rP~m`75VA{oug*U>@vLB-3+7Uxd_d -2MVST^URMN_BKH|-){-+};Q@!Q@8RV#nrjNTg|fDIRQ!+a-+l3W&mTVcaNm_{+x22_l5F_|93Y&f7v1g1I>!Yuj@w2K98S>!N@n -oBqw0|#lLl?#Yo^Gb_HHZNF~8{D~r}Np+X -a%2|dm$%z%|%q(%_-aOez5)c#C}f+O`;zc=_eZbl(M{gSkwZ^mgJn>dS$BfYA5=mjTUlw4ke4A0%$Tg -6s0vdiu5ZD9Ss#VwdpjU9mz4<0qVXjcE=tFHp{|GmwkRhDG$b}oPFAIjfXPoJuX2Wm(0Ja^Q-O$nQLS -(Qh=P?+ia?8xJluDzuR?5E}Rqfj=Noc7>ZLyyg@=g8;I(ly|0Nhi&8uTk36y|L?o)Yy`Y3};DP7~rzX -Erfs6blILlv%K@=3ITRCi-IMsDr6two@l{n_T2e?_kPq -mW2?WwkKw?lY9F-~eSQW!^DkAgTDPPDwab`SM543H4`Y$bn|e>dz?qaWIDkVNjT14C;6&WlKnwWkx?C%rb{cibyCWgw`L7uF!uta8)f+`M#s)ebumjrf#U{wdj$xLPi`k@N>VXqzr_8GI!OPz1!{Na))W)o-<5M3sZ1Q?tcPfNgiYtJEdyyf -@{!6TDMRS-KRzm34O4XcavkIFvG?h8nrY6os5-0^0GMz4INF{Hh@}d((;&4;FI_zS=tEnR)BM@71M1^ -5sRevfYZWer0#*zUi9`mDn!+0yUC;myRKHrk-Zg`bots8TEH+^iq{L^Oa&s0@4QOo7vur5x9S(&N(wv -xF_Abl0Fdww#Zh~E^i8rW+i{9=;Kq^wllIixT1H-D<1Qsyz`mR)Z*0}Iu==)qSbZGZ%P=kMKxoD#4#* -e$m4In*Xf;^3c&98MIdPP?GiEmO1z!s%@+8#tfNr8}T?rUGGB20nfT=YY7aGEmf_s;Y8jvWkO|?dRjv -T@4Tw`u?uiP!;oN)@K;x+TSni6M8vLDR==@_}9j3;hk!L}%98@GP#S+5VR7g3^n>K4@SEo&Rrn;&=TK@j&ZXQUHAZ>5#HtPUnT{ia@P5qEjsnAFCKcL>G1{$V)UkT*=If{u1lEF0H}jFa?Ku_m$ptA -?;0_(lewo;|pU{TQ1c?pU7K1E+dB4GAu!=n5qN2T3_hK|h&Py-CiyI=bnEkKx9R=cQm@NS6OOSlKyXf -=oAT(Onsd<4T-rt?@;;-);-0bTJjV`9^BbkN0bFk>N!PRuOrvWW5&+8uU*}~d=OCD?;NwzXJo_4`m`>=y*OWFm86*oJ%r3qijHDCw!pO1R3BP~c>9XPCFnyqzSwVQQt3wLIW28 -&H^9JGvaqs1FhLh9YslmW^O4?$I#%pp4p%KMC>*CHM_28-JdT#Kqqrh1|n4~Uf-Xupl7m)7pr***KVW -KhSXeqIdKC}aNf&a?1y{FHJFvDNnDr&79Nh@RXGXk;QoXF-@)o%{Tw(5bI*-(~Y-_pI`ys!>Q>YN7r( -!QBZ>$|nB-mEaa0fXdU+qGs2Etl~?(qW=brgQ{`8(tNzs}zMFOORQsYQ7a_2nBCq-0tmpQYoyiyM@+a -oe=_^IWTWm3u6EWnDWKK)x;mk?8a) -d94Nq>J*9xrQHd3&6nU79K0q|t)G+rJlcK3M#8KRp*zVh{n!JWQfqI_Sqk_!(_U-p0)|I4s*i%)=iJpA^sYYuw&Lp2W%JjHlIV{^AXcE>p%KcxF&F4QOe0H -5rX$%WSjI8xuAz&hLk(mv)m<*NcjVmxdQbnw1sQu~j4CIt`IPHO-0+DYNs(duCAiw2eG0Wn`AFq>vh> -Yg)GuebGqWBZti;mjt&;s;~GZNKDd9S@6`D>T3>M?Sby+L>yv2l9Rg^tL+4sQjXOgr1(yfSg*8x7Evp -OD_p;9!LQppgFWvwAFVgIi?-&Z&Tu7FC`u}*tUL4#O2Gi^ZnfZ8)p?+vwC>YKPy{~avn2VA|Q%E*oJI -YhsiudSxSipcn;Gy@}`khf1*@@h9uD)X#r+nnL50gEj^w5yE76hq8Gol^1kx|MSaAA?eOPj6U>wY*fw?!CC9^BcgNX3w2#{CFAw -tz8V3*U6U>VnPT_!UUSd3IHD%EpSs*Lh!yi$&o=ziasMXL%!KQaiA29z5E5NLee1e15?4VH{UY?V4Bl -}R-uZKH}GSx#w-USu>-Wj7wqW~b(-n0)^?(RyfKFT)CnoN5X1=$zVNIXq#r$G53iiCPLT6#;u>6*{ox -u7F+WfbY)8r5HR8&z(R7hu);hpcyXki&R$v*0y$_?gD7xCc8# -Ev9mz_&xm?u|yb3W9xEthdk+*}_f%Pn2Zq*09HQw!Q9}s+|rgzKww`Ck>+%uyXwPY^kw?rR0y#5!}U#;>mKqvK;)>sT5{zs}r)ZoPEr9uv;f20 -%ld0n^QUsXAtH*6AS@u%{_X&&GCI2kZ1<4}B3&7z>#WM}^}rxFi!k$cReLWx<#RPIV@B$y)ZO!C~RQy -0Zt{Y6pcR-9nr0C%?|G9i`HZ>r+9aSO1K`SF2Po!q9sm7CG7o7(|MG^oqZw|+ZulY^ul?mn2^D2J3{9 -+_V&>lGQjzh~$Vo@b^s^Nece$6T+S+p@Qle@6aWAK2mmG&m`;(515G>x005#20018V003}la4%nWWo~3|axZmqY;0*_GcR9XX>MtBUtcb8d6ia -KZ`?Kze%G%UY+y(mtOca)lK}&D)58X0V-auAM}aGic3D%T!owTo*LO&YtG(QyelR@OH`k}3XR}%6v=D -s#GhD-JtC2O=hP4Q_k`imnm4sd?!Q}>+Y(V3+GbEJRj6${N;r#El(SL2V_ -H(I8RN<+hfk4$iwx-JezjVD+}oFAU+neImkbkF<46z@CwJnBXlXSpE1uMVfmI`)FqmiB|% -9Ja@nA*w$#j_+42sR+oP{amwW>0FbY?YJ -EY`^3(ZG8-mDHGCYdvZeD$M`I(D(37~Y2wi>Ge7UF}MVcAit5;5E|pV&L -P$%IjWfPM!=w_o4jhm-0g%atIN0k!JZh&+^4&dA@8qSovOcu=qoSwArK!15U!YjU=+Bsf?us>&nk*0= -mHMWNNr!Yk^f7d(yv2;G1xlc0|QQjYZtR*L8e7G -ElmhX6lSgUzau=iv0^>3KVtBIY$7|hsIESbCb{K2Q?n@VN7#mrj(yNPDDF0g>4o2UDMDs7(~)sxp( -9UBa66sWd^^^i_^neFehp&KkP_E_XGfMz&7k>v^OVr>6*r5(bb3 -z@kKFiwrpCDQ!4aqY^!{H}XSgR5i>LVX-6(2iaijEbdjRDhuo2|PS2-ev!%W{$x@yzE1Bc;n^usrj&L -6dtikWBs0#Hi>1QY-O00;mk6qrsF_X;Na3;+Q7DF6T?0001RX>c!Jc4cm4Z*nhna%^mAVlyvaV{dG1W -n*+{Z*FrgaCxm-ZExH*68`RALD&eSZFhyd98kc3gUh+iHRxNAq$moXvF1u#o3&O_L~37pxc`37kd#D9 -^4jU$en{lia5!(Ad1koly4jTTG$_ld3#=O+xGHPNSKo{Ln=vQPbcFulkG+*Vy{e2?#si-u2XQ;lb>$0Ba^%$Q}2z^%zp5gm;*@qv?^nPkH#?ILLoa=R2Zx*T&sa( -tSoc&xn9*XfTmpN@^*rz#H%CfWj(I*pY*hv&x)XQb8M82N&FX>ZZ(aDf}FSVndGf1}7hf3I|EzcV~>Y -Arw#nbyqZYK|!=$ASZwaC7eskStU)!V0eS6~mBeGd&q?Wij3#BYU4l-##;5-Qz`gPNNsBNv|E|NaX5F -PA*e5f&HhD*7N&sGlExRU*4A3(aLA>S)fQ?>~KdZ+9ViqH9|BbCc&-Rj&Sk!}m)5DQ<2UDNCfeAcp0RVu#Gx#*A+yA~{Ji2d-!S)e@5-O}UT&HVE1Jy;@|A05IWv)q4gOb<4_}eAA*W -2Ls#F38vq*b(-)%(=G37=uUXZ&sOe!X}Fa^YP$HFtnyjyv5<8zFSmuIjAmZxyB$P&gb@j#*I%(#*9Dd!gLmp6C_JIh5ZMcaoWOx17Z52_M&we0(&T3PS`--tfq5ja;k>dOq9h%ooz!=| -C}C1C4!;QWD>*bWFX$VHQP~+$%;rj5pm)|3Fkzzk+*l6-SMp;A9u15I3zdn7S)g(XDjX@$jCHD4|8G#|03T454u>9g%e|ZY#t8)LI;1v`>nd&`cT;xF@@;rI8<` -r;m(kI%DgcNzNUqgUe;3N*}|$`aapSbP#gJ)POTGxZJJz51ehge1>XqOt4Cf<%MG?eB1^flwXaZ8%Pn -Fo#4`6gUf=rCWSOKW<@s0=il53vY(b9%TT-1gRmOFXw%)k!fnNpykX)*CSyLV*!!c_)bjX&kfHfcd&Yu1l-8dEokmc8^J`COxRn9>2bEaeD3HQ3Z-FZmLjES -KvPzT9r%Qi3z5mrD1CU9@GQHzF}$!PXL(sxPFqyoF!r2A-F{P=&`Sy%_|GCOCnf}|n-pG#qO@%rNOM< -nQMV=DnDmIVj2N=fpvsM~0C;6t<)zY2z}K%|wElDDY7L7N{h*`Ys|g6MYkn={|ne6xl(Q&sc9sjNqm_YiDg8 -}w)_SIK50r(KVsnGR>rkk65IZHZT17sfHc(32^=#cVT$2bf^5)3f(8t*7z=`6>Nd_HjF?14G}8LGpQF -k8DaEJ^}+>4BwU4_jvAWsql>ABSt4`JMvLM{QJqc2SIWu=|YShA-6IQIn2s%cdP4-xV+a=V8thBK -p8NqEK^6Au#rgFYz(KryvfPeBJ;2IIqB**V}r-=R#u?Cs|e7z`3ZtE7DskalQwg4n*Z+b+?FVRaqMz< -Qk~TZaFsEjVMp6exraf0*(H7=)e@X#8MG<`Bd=_^T$uJ8=+v5IF|}pJ>h1bMHt|U*)ACe8o|~FIp3LO -rj|I*0y0+^Xz)Y7O&ZM4!t)(G0aV7?Yzw#3}+DA4cD~*2rsg({r3CfcLs*HS$|!knnMu!+w!1xd(CbE -777O@4;$ooF1Nuj -mjNreV2e93=bc0W66?ftiG-dKA`=@0Xpj8`=CM+hl1@L3Nll5KT@9?#9E$)H{@-;F8|7+ot@uG^E{LCs0WyK$sZlD%dx28hMXsy; -yJ|<1T}E*>F;VaOyMIy4&@!oeYsle1i=KAoP^b&*>wZC1!l}s -CLtP4pY8wL_3yyl0yDOZkmlfhRLKW9_B^I8eNW=)IAO0;Ang>#ffr1t)qiP^UyBCDg%HOUHbxlK#EB3aDt|3B;9uQUp;Z_#7fZ_Z$9ij?JYZab-Q)8%&u{eWV+V>+HIY}0PfbsPAjo5e6QZD{~VgcB*?jD1^DtQNw~3tuJ!8;;syL)Gunr})8EIGc7ZEbMg*1%-_}b -LC6FwOp`A3uWGt;=Zc71`*(9ovw<3A)-j{tS-P|or@1kJQv6oTriT!wYyfq&Y02CQ^Ny~~a4nY>4f<9 -)W0j$PnS5hvgu$<_gXA<8sNj<~@88*Lhdm)ra)=qVyKOyQV!f8BG?�`$YGM9ZIdm!v1Kg<~tk6{Ux -@O&tqgIwU@+u`mvL?S0nzuPCB|aY8RF(wOp5ZW_&D*Mr6ypOWAQ`Jozu}wJv=^zq$^A0EtMkHg-$&agBa}A)Q9|OAHDj>bthGY*Z^Pvbg(o5PCIU0KsdHzobqk -8QKOQ_$6BWAiR$(0doeo7iz{DV?oZ@GZmpwl3J4tXQOYYhx7FDu;NA6054u#PZE3YDHgr8Uo!+I{&kU -5%PO9Y#h_0cQsJ*o0x>KToO&Hc=Lq)~A*ZgHx2>orMz^yEh`Y+A`U{Bv0#Hi>1QY-O00;mk6qrsR(*@ -``5dZ)WHUI!20001RX>c!Jc4cm4Z*nhna%^mAVlyveZ*Fd7V{~b6ZZ2?n&0B4A+cpyZu3v$Rrz5JR8R -zBFR6Wm~Ys+n>uIe*1{WhqJ)Np?fcGkoFm5W#>IE$ooID7cg%xE&DZul#p=ct|2^{ -!gOjGw3u2S5{65N3ZU3ZHHHxa2HK5?D7@ty9S=0e?BmqYam!Sq_%L(u&1V&K6v2f+tGIb}5W5IxR*Wm -q3jgY>6IEt(kQOdLNjWT6JzZH1_3D$4kEL(DH3rwaHo^ZtBJF@Zlt@P|DZJhV`eNtoR0pG|iKq(a9{_ -L{+>F*FsjPHY1PQ>a$Z`R=CiSwgWShx=J%5<4&aHu)E*$Fot1a$=^xeX~qHXc`0F*>H{bTdoDptUKVA -VSE@ZOIdBZ?GRxA%lQ0%}#qaeuB(g1|BGgbY8QDS^dE$1*>90T~%T$xJH`ot6N_cse#?@#9umc96!6# -Vp>H;2l>4hPxBMgBTjtdvNP{wNxdOK!+kW^#bzT+u-e&o0B5*nU9ed3S)9E}dx>ow0=T>#Xf;l7-m&M -xQS*~xt7GeiKiSTJPV#c9r}tzbJIm6C}i8FyTlB|-xtr4Q7iJb?`XNXfwxt2LPVGa!>!YssRO2%yL8K -H3x+pR#nx<|2u947SSSqessVj~>B)K9bFCIhb6FI!kcRMEQ<5F|c`9bzrxwc!{H*snN`9c1(dvb&et2WwMz1i&A9pVYHe!a^cJb3U*0{nsQ5?(< -&AjL8wL~MtgW@gO;=s&V{rsCj&wATS`pLIDln3|x5aIL{mgAV!wda|Mv@6!Yv3M>$qa2cUx%Brm63iO -Ip7O*S93nlU}1|$dLSfp86?N}D=K;sKuZ8MBitdoy@lz(?zz34vK!8cQX=Ad1J)Xm9^k(zs -d;*7io1rUTbsV9%b16N_y8nQ@u`T9`oi=V2DazymF|QLImC@nC?q!Ij7wv>9i>KEbqD&u2 -dk*!1th_PXw0LVU?_|D+Btpr!45)XiC8|8$3YbmY1{7)%sSI_$1OzybGRYgQLH>~DX%&WG+}YA+Uqb_) -rvDWMF+K^j<^J^&v!6*B|_VC-*OrWw-{x3j*D8k`TH4(dK^7S>!kZZY`4@~A=o0ig?6@m;f`Xa>~XT0 -rAZ@%KHiFwg*RvlisAN>0HM1|;A;Q$#^HaK7 -v=qf`#XO}lB_ODUAZ=s8;!Ayv|y3llbvZPtoDoW(=Kb=QV->ATEWs2&=m2R;eCIRUKn1D^6Y5)TaB*d -<<;w~w30*umaO8Kp@fb#+DWn9w~p-jb-+FjhCH$*Cc)sQ^E{83G?hXn)>6qw+arCC@sK`v;GA&!(vY( -?3go0kS0JPWOi(jRJ7&kQ}XtS~l^G>3+86jxxE0eUA@8>)WCwktI1NEX0Y@sPY9m8tCOO|ohIS=#9Ty-&@_Rt1j7oHLOGq)rmdQG;hLz`P4G@{sU^zU -9?lIlX2QZS&nx&46Kd~1V1Vsmyi-be&fX@h66v(1D4XLSG!6}v<`gDfnrqm_8+hV@^r{j71Zm|{G9{H -y0Bi~{fsy})(K;3pn`>8uF&7y9%x74xJyXU$9ZpX{5!%#>gU#qU?S_ -DX-Q@^=T`BFucP<(a~)Kx$@(aT=SjleMRcl0yc|+kNFz4{LWdmZA-yXMP1$*R -&r~N}eM|(G==2j_Ahe3y;sTR0@vkF-X5&pdEPjg0yIyz=dPcWi@|qHcKDDT>1Q}CUV*Q^ -@i%B7(Xla3!mek#7uLO})_i$o=Gs(= -9v9mfOIM`C5ZBvd&tG_QtRjqeZ#bcXIzk$#E6ySaI(-DWDBDIm$<`4jz3@R`;TD%@G( -`6Z2^->8E&IkQX1htdR{tl|q@*cIS;scc`QAWHjK? -=P;tJGltYX6GlbF6QC<#ZR-VSMwn%AhA9)(xg-}+?;IbR$WPI^V|>CH`>YhMo#r-K!VQC%*2WYoj8!z -)aVKeHTu!*&}a|$iFxkWMLTnK%xm7d=HsY?{Jzs-o9avOCTWT106GIB{Pj!C#Y%So#H|A3* -v3Dh!`y4g|mLK06*bGW94@>f2g#*m~SMejOUpwK^U4c8*nD>9Fk>HR6&2E2X{cSg+d!MQQsqM%#%Vtn -J#>U#Dkh8xI+?6?hm({6$xohoJGMhm9XR-?O^Y0Uzp2>cc2Xr7_y1;ck(!Nv#d_W|0i0Qe#AmH`Ep;X -=yhPpfWc1kJNjZEg>I9iW=5F7ouBx(*v~3`}%8%I?&sfb1%Pk{vS#f=9nJoy0JdUrfSXtF(}JuC1YT$ -kEr_xVt7P4T>Ut}%6z+CI>K`b@_5&zDt82DH69$`5L4M_EAZwPwHk|)NDmLcqeX~`sxKXmN040d<-Z@;l4MhhFwBj -&e421Qh@1Abq?E1IHbzE1`zXpphrK1>I@Og`#=5AcS9ed40e1{E%TvYU3o6b>+y6&}rzLp>Ixf?sJsWEeTChB!>i;?pqcah}BD$#HirrPZJxzN@ -bpy(tc{lsI({+EsJP@P~xcioY3^BeA#d6ubR}|1KByYn0WnXx*MY9IGQkWZ}0X}&BsBNrn+);>+bAj} -Yp#+|GG=CiZISG_^GDlhqSj`<9_eo_uqN*pNxV=Nv2syu5F0})~G4&c53@PNIB@Wn(fHC= -3Q_%;?(3M>W?uvq6;;D5>*cqJizSHSYO3Yi -}Ouy58ku@x1fCKTXho2Qsw2uXL@g|J`eJi5xM-nP1%KQ`4QVySP**|R?f@5=Xjy6fDz9 -q-|S`@=g2Wa@xcx4`1waC29G&?{J2U~(iT$_{Q0rwUdq<&;6FR695+MFc!}Bfhd<=Y0GVM^b-%BHaC# -1D#{xOsoRlKLd7lL6R!OBkub866nyd!vo-;V=MM}{5~EKQw@xAm5q?6AOOCK41gs}IJDHh(9`jRlM~4NXP^H;QMf3 -{hUghtHt6%y+(`upq{a$;jXCvM?i~g9-mHpBG08mQ<1QY-O00;mk6qru1E_!QACIA2#i2wj40001RX> -c!Jc4cm4Z*nhna%^mAVlyveZ*Fd7V{~b6Zg6jJY%Xwl?LBLA+emWX^(zpmTqGSD^5gFIqJ(?pv$eabv -)77cXDiv;A|YZ(A_f5-29V56CBHrWnkPU>-c7EqQc-QKC19p!rl+T0)7@A#^){K$S6$mRYCcbj?XGUx -B)isi*|loUe;>(X)ihOY{wVA9x~SIXPrcXX*W0YA;Eg^yS|NB{FF&a^aWLv8$#il>kI(VXY0?&3Rd=m ->{(&D@==AVNAg1c`QtjHJuCxI=K1$%{RatA5U)FWWaOvS?Q@8apcyzwqwfm^qcPh(ObD1?7=I%eQb^B -_QRcn=dPv6wb3@7j}S=lM?`P=OCd(}4kaQx5y-|t}{S)TXw>gtX6SO5J*rPUHP>Am_(r*!)R32EqJ?50~CE`uoa%@Gq)?d8<5rVSAq&I+-Sa%(QYiHrQzxn`VN7 -+_$RT)Va28^bRP^q6-1PmuH3`yS69?7F_7LdpOUVdbd-#-z6Gft9HIBN;Tiry1+hR+i)J4*06E&4bkm -<$AlYp077(2gFpPY=E%u=bKtxdT~nI3YnT|4%{2Mgl+)z=;G+;bAtydnI)ETQhs!F1pEY^3s*^Dz&G%#YR2<9(&~IZIXarZR;#QlTWD8b|2trH3=fY^5< -JQBL?10nKuqGO0bh>!<>ZVgw%83K@{;ZLzjs=B2H?jim(Spf^~ -bFT66_cb+7^iD15M5V`*Bik1TOzHhBnHRg8-w`Q{c?R^7*D?uv%y-E6=F$=WNz3{cb~I{7L^7kL_^Z` -zBa8Lc1|;1zRWf>{Y53WrMsV<`nT(Q0$++F`6n9Du;Y)pqs}4^Kp9ZZzdm9EaYmT -$y-)^!}9$kBIXwW4JF%L;6cyGAvsD10# -AX;y>p?7(DOoP*CkA5qo_$b2xbR|fez@0O#vc8CvEmgRS7YQP229yPER-Mbhig?TcvfgK3($fr^o|Oz -sj=f?5pR`)b;Y|>6_yEl$8Kze^NgG?enAgbqNoESxXwA-@N|gn|H7N -^q(*rKJfh$JP!YyM#3QC9UnFg@ZBdmTdA075Ho=z$t2HMKLhrhZKE@Znnluq!a_Y-#1T%*<6X&ErRy{ -~hJ|J&c0Qg=39HbjsM^|sGK>eYS{Bz4gIa{MK&p1LxWwak&n%F`;Qj$d3ZPt|HzdQ~Kee-Q%m-%mWwU -{EUM#)&yvkrQ8klo_)n;v{-xL)Q{k38SmLr2hy3_PJDe?U>t41w0K{4n}2lUA-ff*M$E$Z}H`ZR%6K1 --kZ-G2>E@h44@^bt+CHX|{KD5elmUL1c#Aqu)r -1*ol-Dydf%hoDLT3SY2xAgabf23Z%VK{k9-=`t|{Mp24BVOuaA((@S4^53YP}aJWO -WT_!KWt!VfV66q3dImW&Tg`zL_sv=%}0sF0C8p(RtNI21c)A^2HN0NHL6q@ZHp07nd}H4K39oat)L4M -z9-heURri#)*Rd^AZ-QnL$;aMK;4Mq+l<*JKo56{@FUm>mbe4DBJ~M+?HYB1d>4*touh^v3KlW~v?p8 -m0ro7aW!b?>ZegM%)8+hta@$p<0%2GK@)`q!5Q}Hh@d))97~ryK%Y8U18W@H0MkONym#`|N>OtB&O~gzB>7C9as}VkURv&-CF$(RIDVC4>PGff6Ii -1`AZF}=BC^jHK=AAr`_6k1sMpbr^DBb2x!cIeoCY!`(S5O)fj#jb%f&hh>Iw$oH}kQuYlzPTWRW+s9m -eSQAT%a3n9%s*VbJ%9J{!_*w!*|RBgo`aBtC}dbUKm1p}F;kqh4ax6N7rXnOVjt?Y=|NmfykqQZzP%n -ih1fu39HMDYzqq$zj~3c&{8;cgHBN^~Xr~%wV`ko`Xfe!5NilsV0enFua&JUTWcJF-8M7y|d(rgWpz> -Sz+5wT@qVA;l&-iy^$B%_9B72u81lqEUiog)-n8|JryfXw~+t>%7@chSrI&de+Hz){EEV4JI+aq`R&qgJI%N=EFvzMcJU})4=&ylLR8enUF=Pt -~+x5B)hKJf%&P^aQRiYo%_OZmOSk@uu`S>R?Y>yklz9VatIDrb&{M{WaxgMtxGjRm2q7+z~r~I0PF#@ -Fhw4k-PB-ZplcL#pjDHtw;--b1f~(lAIM(8Gx4daZ;d$vyqHwhB+F5kFv3#2=QSJlQ{2N_+rLq}GGl! -$8lx=G_UvtttUU!iO$Hc`c!ar(Fq$Sm{^a{SDs@YEzsp}B4vH+2AU%s|sPEwB{{1X@W?Pi?IwkTLAA? -EQ5Xr!-*a-1-y78lYa+W;OXOHypkke;qr0Jl~2M^Qn@1OckFzjOvcp#P75#>Q4xne;*#QS9Ai_eIR2% -Q^d`2aW~#yoaB1&QEqykkY;8N%n@W}~0iZnnLai9L+|_I(S)nFS{14+ZI%LJ>U&64JZ46Ri5~+0qv%E -e}EwCb+%wmN_NmnwR-hKQs#!-yN(8=&b3)$) -`rrMIu~y_9bZK}4wJ=P8#0WZC@HN5>;SH=CO|8X&qa`$J9n3^)O~m4S!Y2NWa)GrNC$1bR^yM$FDtXx -g#ereTKIJoHLQXP@U>J<9UcdP?W5fVB59qIN1-tF%H6c+d=mdlq$rIh-mBgNyfk*Kvtu+ -g$CgOAXoz)eDZegX~&5tpjtBNHTMtq5LZVT#`iE*g8O0}v^1a7N4w^8k;^XR@X2+Cbb^jv=l+iwDK-y -r?)j71uV$Tz%dTmV7|GRvsFUkW%coOr$&e^Lb2lW*Q`XB4QQbqo+WV{dJ=-!j=A -bsW=-y$foruyA2sS>$ct3Yd;nPhdFbLJZ6TmA0=88JmOsfWo%lFAv{!=Z&b4fW&=kY=9t6C`zcr)l}hiC7?B3s6CP=gAr6a#Z(vVjV06@tBSzhjr -VZSu3WWu73Y4ck<|Pp=#XW%Vq%&Ggr-ZD|g -4Csag7~BIn!YdF`KvYe!-dJrYvu=4n!4^!?X_8s5ya@r)#shuVeW9Jz4$K=kKWyg?&*}sygJ})0k=Mj -hBD27=BP008Cq$hI_1$pU$#<&U(Lz8X$dv(;JBp6GZ#ewbp^9fNY;(xb7nCIJrm1g(dqF~~j~mdONFyV}jOZeM#Cj3@p3>hqo%jy_uZ^@&5ae3uK7xMqK@#Y_#{v8b{#MR!l7*w3EVtcDv6qF(u|OhO_+Pu6}=M4@%3o;`j3)pH --gBfIDk-TyMKCrGQKyx{8wJe8#a`+%@A2^x}tCIs+L$@BE9^f!Ms+|xPUW}lSt*Llo+nrzAK9G>TyVV -7v8bk!%7A9S(P&*R6Ul$#Mkmw2Gwss?!3))Qbg`Yq@NEMczhD^{YrdXX%3=UojxP#F&NX@*kKg*gE01 -))9AOL1pR0FtDv&jkU{<$nO=P#^%QlXSU@(i~$)%{a8L!SyKqctT%RBaFj7Zs@kF7cp?B|0IyD)4}k8 -AZ#F3eOw5^9`S7B2k!PuTAwmGC7r%3gbf5?+$VuXVMW(Khg72<(^$`*I41q9cNIZhB7b1uTnAnQn?>P -Z>T}yj$P3@U-umpvEp=Gkk2FEjwqh-N9LTvavUY;pvZqj7?w>P1(aioRDkliYg(fddlY(qN9gy>YD?3 -sIfTztAF^ySY5t#XSo%H#fk4$Q6_|k-83bZxxADcH*BLsw90S$XF48htHcY&Itt_2#hJ`8a~j=8*{I~ -aE|LJgjo-GCBr_?omB2V8TI{oSGS1#5l)ByXwY&>4&o!nKW~#d(g3ET0b(y8p@_>E&*hoLB2YVk$7;3 -zZfiC4`O6lRyKBd_ve}G)*E;HbZu*(2K+d88Xjfy+`yX^7FE;N)d6lXm|rFiNIkD1UJocXQ$DI?`mYM -jSIxt6KR23UVyeHEG+^jp#kQ2WKn3Gbt#0yDc&}Vl*DH&7(zF1T7$M1O{1msN_IoC3P=l~ld75<&p_)53Vh%xkDxkEIl`w~tyF`%4D -=@ff(%>t=IfY>jLz4xh!OK>Y~jt&CleutMbU@-&bw|fXSg?7ZwX&AjlSnq20wS_s3-G$T%*-Xh_$nIx -OQi8xdD~Mx6uCa06+@Xx-H1tlazJ+31e74DfVK6;ZG5k(FEVdUoE-DNz|V6D-0bMj~ -SBccEPe9ZqI2dNRm*UUrKOCWvy$xeIJnsNy95R_iiQdR4A<8guBnHWqb8utKg^}0_S1G#gK+KD1IJ7# -2;W!9p(^~87=&W4ZeonF_`H(61#N=qTVHgs`}y3FV7z -3zoj@#FArt4Fb*EWrS{Tf<@()5K&5?^1i<@`;+OK#5Y0Ty?%Fwk){%ICvL4?jx11#K-rFC(m04sK@(sA=f}gUDvsZQT ->wW)<$s<^o(-_MEcl+A&-0RzaAW^?ofOmGDdv>L6_=Am7K_cWGQBo^-WUzA6T3i`(eB~EJ^pscLzk9}AR{f_DaW -MWOR8hC1#r+^8uqz(j)hX~A$Hz*t>c&j+-+De}bK_~rpe{IEBrskrmJ?DPI_RG;zOXXvo<4gQdQ1L8P-&%)}UN*S`4vE^k -2m81sv2%F2PKOewrn47Tuzv)bkl{nFcL=P5Q{_C123QUHSz&K04jjwxwB+BRag?eDZRQ-j)E$#p($j{ -wHtIal%-`s~%~i96U0OBRH14mtmt=!KuO!8W6}QT!pqVgr -j7P>mGUI-c=)}m|Z;t4H4Fo6#g)A1Hm+2_nbJTGb3q%vBB$S(s?k~=`LZ^HqzJ|cTjTmSl9DN#gGby?C&nL?sCGnhGqc3vNcVzAbiwyi+UiS-dEXrYZe-^@rz<*cz -L{>hw7pTu8emBt81`kg6Bh<^JF0#%>wPl07_4Fq~khq` -oAbtfOS;qVGJ@Sxl^oM`9qGhnN7&o{}raqXcN<5nMK60STw1>CF2Q@MO}ppf<{)BQ|#E6Al}oO3J#Ot -qsegs(E);yoS{YL?I^<8XiB_%vKTl|gO{V0`OBd+p$l@fNR#bI*h%-KZL&(QqL4IDKsn7id1w6X4*)4 -;DXJVa`}i2)YM!tg7Ek9ELA@UlC~n=wA%~bz;s?E+E*X>kC;aDKsjs3GOgR^|?^_EpRcV4i0BksCFuQ!;a(;$iKB!N-D7>Ya@sC~8Mf7mp2w??M -{hYWO;E!&b|0SR=?tUPFbqlBkL*{ELpp=G>-?vrbOrE-*V40^OYT^4)8)itJ^_|s~0qOHQO+415ms}m -10#->sjV{-Dl-#$xzeD)J5DAqeQ)CU{=6P`}IWm|)~(OsutNuTOX_H%uEDo2~6BlRhO{o3<<@2$!jwV3jD0p(QHjbn8|&8LwyfIYx#0L -LVt_Rf;zYmFPXXq)|I3@xR*#fkK`$7oEFYLg5gzGHXOA(fzi0)e&Kj`-$hlp?}-Kk^ZFAF1pOV2D^wP -Mg|i@hTST6Jg`C$+8DiizZ3S9fK{(0Zuw(Hkg#MOYp7lhNz)9R^FPsC=&%FDoAZv_0K1PHiY3`p?oF5 -VW!A|&K&nDZN@Lf&mYjG7((PEzRwyOieCQoje1mCu_0|~Ndv#j?Mo1=`+Ky9Z6eJBXB%^L}n+j#?KRO -WP_nbPO8gEHeP<;s+ -k=1I(}*VB>n)Oz+5t>^dKXam_or7d?*UXNU%HN%R%LOlh580L@$k$-h1Hg86W(&=gzOl2tTpe&X3%EQAZ3 -86RB)EU>Cfbc#`?srCc2xe&BxazyY=ygn#Yr71x&)k{)T#*hlRJ?_*Gvxot -4juaV<0IIcMbUM-=(I_QPL{1Vbj__A{L1-a(&k0X*+c%l!Kl>d)++_VG4Sc(En{uRe;p;qO_OoB*4_#yeTj&N()1X|;h$xH -gaGsXis%#0K08$C$BgKTwU0-yID_9Luuu^`c$as2Nof`*JQchlMRap<=COdaqLxD>t6}`243{COAz)m!OM}d;B2O&|N~N%NjtzCZT^J(XqBllkWz#tck$Q)JNBnFHW@^@W6yTK?I`r0Dvl07so|#|nhjvI)!U($@qZ#IKu;#)2jOh@UNF2&&I59;hC?W -u406^r6XwhS^N#Tzx48be^E({g;gX -dY~C-YLiqCj$2q5=43TNaAPzt`GZIq|;ebx)8*k^bn}d-3!|fBs@}&63f#=O0E#_mj$lwFWZ-Yj61a! -{9y!5hVSQUA0b0JE^Pra2SW%j@NiL1igdCxV`6hv!r562jH=V&t|PS_vx}Syb0K5RTujB{Ga>I>tH8N -UK!X7ftzjat$yG>HaM6j+{@fh1fcGn@?kx04q)e`njKhSP~wYs6S4Y8Cr`d+Q6l6Nk^KGg3AqA*!FLNSbT16gp0Hl-AjorjcowypECh1TUFf!{j$!~|Pjc -Blg%pj$|AS8nXd$IGmGj0vfjr?GU-FChvq68F#-#Oz0N?j4%Go+H&{77HPd6&xeg?7%W@7gDz0zB8W# -T<|x{77KiU`DGGlST{LGL!0@>rPVH0gTSDx1o&0bw-B8f>jJu*xDOi)Is2scbS;TjDT~FDCmsCRsK03 -WGn?ZR?Uo`>=5TDV39kdccqqxUxHkG8N4e*Z9svLLc32z*VU=O*u#%4g#Q{YW>51_1a$(NdZ4@%s?bZ -z(cLEo&I8y`N|2LA%q5 -g)5%qJimu^j3Vk+^j1pRjvIL?X3`W=J2LWMjqE|Z7Y_sKRuy=)4 -8qxaXtrlKAkDpoWAN(y{fGJc^MC$$@&5d^XWfz{He>(|&97ktOBm?9G`|ltk -&*<>UU8*Rbgn)0XOQ<8Vn!luyyVHlYZTqFhEvVfPd -{8clC=o+7RtMpuf-i4$$L3E$`?kM5iNG7G2!lLuB<_+d9^heZG9EF^tV8$Y&U+@R= -$uzS27r$dL|6iUJ;ITa&dWv4Ek_kexgLF7;d6}%=c3L8X*H8e~IeFEo~V&ho4{Z)eirAhoJ^K1mxF!z -T@hD@%tUW`OS;t{=G{710J?ediWH$Q1n&mq3C3LLw)>;K0)bS7W4W(zJFs$3z;3NeRqBv;F!$~6LU5k -i@@b;&9`+e3gY)t7r^4hN5*h?TT~OKKMY@i2ZJ3NkXpLW`r?ltS{I<3zoO!eCrYaa_iyb>Dlvx&L-^v -Wy}zY8+qwh(H6F0G`3Oyh%RqYG#Bb_mX>A*3pf2UOqnLUFAr5Yf&*NQX`oi)~TS5bU+8V>#k_8n!TLi -l~#1Fl}=yjpzH=-{@^gw`4h>Ij@&GE$UNMonqpRZchOcYDxK%PG@VCZrW`*0PY0D?h~{4(WHMqE-H<_I3>ZqK -X6n0_Bs_IvYSgK#7tQwJTSI;hM~T1se(;rvo}coRL-jr`E7!4$zs-l_eo{<^$xuO?y!_}PoWP9-sJL7 -CdM+XgklP<5HyKJ!MX%;&`gPg2hA`1a3w(TzP>2Mp-2)SFQF(a%pb?#dGm2jH@%d<$c&B`siYGE$2=) -NJqlwk6@4{qAVW&rs!Q*yxUy2Ox`zdIXqhQiO)#Y&q}&KE11W~e&si8fJJcDMSW|(Qa{f8Pk_di%kFU -S}+7B^_32G$8vDbCI23d%SAYM^6?A(~*eI|&R?m~g3n1+T1A5dfnn|Vcir~v$-Qan@q4mP{-R!NkBp} -iCgq4pskrzh`N<)f2BJ;Pe99*l>9;1k-fk>nQ<(yt1>^83#wjc^xjg?v->CxZi{uuy7?@d?B{|KcQEu>fG*63SiQQ*F0-B<-pJ<>T-}SzP0GOeuD->F)8Io1VX+qsmC*FDVDkZU^Hlb0jmA^Nb{7l8Jat!7G;nqxTR?`XQbuTe&S7Vy~RumCnWfz -nniyrPJR%}6r<$svu<<+Z;3v(!lY5PURew0rE& -Q)&P}=wJ-oJhM=HlO8{_)NE{OZ+r=WoxihCyT5rAERsOrby2ZT}lkO9KQH00008045ZePQBDyr+^3m0 -L&Qx03iSX0B~t=FJE?LZe(wAFLiQkY-wUMFJ*XRWpH$9Z*FrgaCyC0-*4MC5PtVxL8J(51Fko$FN3iR -XkB*=kh+1LY>x^p(KZ)})JV#XF>HVP?v9i!$#Q{zOp( -koQVAA-Ws=!a~?nPjj&@YpT?Qq!CzbF@+eNFP_8^X=rQ=YRsYqi1J%jSdp;Rfw`@;EYnGmitz>Q%t25?;2+#5sWyY~e? -#EjfA?B(w_2y(zRJ=w_N}#H0@p2zIZ5A35UvEs4N+`BwbwE3A?lx?2-+dQir{ob|aD^7U5J);XC#P?vTs)F-3&(!sOaQ1XLT0fEZHVuqYycqG-eh -bFIXP;qPGu+(P_S2%cD4J^JfPLQI9ddDA72Lw<_-wY;m06t$Oif?`C7yD)53Fo?o(lzBAd -~mT2k#F?*c{lZ#SHHz5Z#Jra0oDH=w+({s5O;jI!5&h2o)$N0a5@;gbS`@D&1k+n^r*jgI7&KsD|ALz -HX4Fzv(yrv=Gh&m(>%kyzIq^-!lFrvm#@v$X3EIy~c<%Qb3$oN<@)}Wb8wIl5@i|%7O{IYdXCmqKQLq -_&pP{Yx3|fgfQ^`hw{ximv!=}Hj()T|gDRn-04O;iomI~G -E1-E_;}8rlPXY^Ur2)?ggy-Y|TeG@)OXq)SfBowN+$#xZLJ~KQv0pmA{qX4CKU`C|6vemVQxRi$TH?hePM`dQV!ab~7~s!S-XYbvdGhlbwWfT!?Zn(93iu@ -0@%6uPnG)oM$|gQi0mTiU_b@QI2kecZypvl6gBCgELbZ!}~b*!nw9i@+tvKS%7PW()2F0eWm!_D17xq -(sg(atFIGKBdYF_ni7$ZfsD0YEhSNApVHx7c#=iOs3p~rV^qcM`;8@UHQ2>kOqa_-2VlHdh#c!CCitG -UY>ZGJ$l9Nr2x=T$vU!+@9EBP?meQw(diItj!OkTr^=@SMTB0!7CFqiu$LyhMoSia@!M=ZTU<}0*<$g -10haWghG^yLPzmye*gkDv-p;1iUpn{yjpd}U5%$vmoSv?x1pzDp&1E_M)<)s-cKER>Tf)G_GK?%6PBN -NrsFwL8>ximlvHBBI@MG%c&;pmbTDrURwR!7U;CW+w$z&*}Z258C71!5_>=mI3OV1UZ+U=bG(*mOgDOjhh`}j~3mfJPxcf@@Tz=+mw<&k%owwi%7(%@IaZszZ+!V%Vf%G+v?;zcP! -37RgKx#on=auKz-@F@M8B8UzO*F~9NZlassJ?qpgzhg9Dt<0{Lo-p=3$BFvETF33Ei;}_*Hyl8g@+v< -Wo2Axnj3f;C@bid<+OgLW%1cI*U(cJ>QN+v#<(_O9gG^z9D4@vd+bjmYiN^TJ<-T0@P$Lxq28mIV2y3(E}O2m6M^h-h -{c?lTxZ177nv@M!Sp%!6U;B1db5+JGOiM^k+V&c=?mn%~P;f*ac93j%PJHvw9Q+B@dB^>m4Jgv|qZ#j -zbal1}3#AD}Qf$$3TVDp<>vIYDm+FF`Yq2~N|;R@bijr*^&8VE{`6r7^;?y)K2EXzrLr`HK2;lrwJ@OB=qtz;9fcR? -_bsjAqtZ^&_{@F(vnAiq@v{$DOX$%vajzbg-mAe!Yq6w+zPBqq>CiW>pna(I{aDtKIpJxc5KD+u?*QozBZy0Aqs2fu?fI_1#b7AIE2#Brm_aMVcRt0!#dJo8KRrCI6unG^dE11kUM$0fC -RrB3$J8SG3guUuNNsk@r}X`%@!&|Ju`x0`=7)t5K~_^=4eYywhx)VH%#z3it|h3MP0(jbReuFxLrC|F -X7?W!bk9H+-o{|4h;@O31x=LYh)ylPIfUlO3k2L&A -g>ni{{c`-0|XQR000O8CKQ-XgZ07=EC&DpHx~c^9RL6TaA|NaUv_0~WN&gWb#iQMX<{=kW@%+?WOFWX -d97G&Z`(Ey{_bBvcnBnKj=CoOP`D7VpiQ>`YghE;!;l0rEl~~|SyV+TjW=w+eRm`!k&>Kr!}KK)#e44 -U;oZ@BEp{YH@@wR?UPO)Jg>090e8<$jVsbXi5i6=_dP}!VwosDa&t| -iX;sEVeC7A*hpS($!aIE)w*+e9k~ju2R;5pPD(MvN@+nP7BY_Az4tn8y7nc#XsU0nWnO+?+Qm^pIol1WT1>K#?d4Wk*W6YWiCvT{d@h7AJ$ -3o9yCle{TYOcGjd00=EJ@}&`qK>{#d1tSKNXiTP9ZFBOD){3W1LF-}ZO7i~oOLB5@`gBRC)Bv*C0!tOk$YyW8o<4t0J}~ -3dv*?*mpD}ebZGaj`n9krOx~^&@3%joX4Dca9ka1(ID2u(v8g0D~td}N`t6JRg3;?MFEltT&kuj@nM4 -3`xKK3C5kCzq_BW&J-`PJ1`eO0!H%1}ch5n~b@$#{Mw=K(n)9uK;$s^0fzvl -_KFMWAnM*c3#&4ys~SrShu$Du`4QNk5ouDRIM3pEO5Pt!V|sw{ZzA(10AKJWy(VB%2Iz-?B3L&#fp&c -$h*C|fz+S$_$F+z`P+HdsQmc3^{(CFR7Sfmsq+*G*D9P8gHV4TAIa%RThH_goYb^{K#=$B3$R_i=B&s -QNp6tpx -+!Cqg<3|uVO7e_|o|tcYyv5ozYn9^gpyN#pKVwx-UE*$7VtTLloNAg(1REnq^C -eT_%{m|Lj?3Fb0GfE5F1{05O%FY0Mqymro|_pE4Ce31U(+&X#qVS`Y(oz5Q{!~xL{@gUHjX-NV03N0< -yMsMw6_C!!zy{I)Q_}1RDX^>{yUECa3;)e1@EGhu^lZctYd;HinrL#Aks1z`X6W=8;0D7-1QkJ)mLq- -80XbEc5|1lO=qHA`&_^WKc=RHhS{W4tSpIpxJYHSD|ZC?7l2q1MH08;LQ5+UbE5;zRD}{4;8oH?pR&( -jBfOhdvGFtWDdq>viaikKMa`;{$S8-75w*Ma~KgD|DszI$+xvi0rLcz*YE{({_|z&|JOPZUM#>C9=1;9Oo__f-%`pt&7;W*?}M?v|yVD~7dl8ZOdN4V -{yUd2!f+ca>IlRDJCZsi%D`MdQM0w9pEgPZpY4%RTHaFURE~ctL{bv*2ZD=-4yYVp!&A-~(iRuXmbX6 -)p@9?BOm!Q0gIgCC4O35+FreUgI9o|*WogZuugFu$z?*Ivw9ObmF3KZ>|JKt@{z)zWTVlGhX( -$zq%V?LeCWxbCcoz3%j)F6w2we7zX3!t%zX%)WFS@eH%18d>ikXv!RR)fbqkoR4~U&|23mU)tQqMakuo3)$oei-*hJc+gLsT+MUg5;K}fNhi9@FT;s$wCIvQ@0G -r1#&R@a50{kI%isPj}v%}9+>twlMz~nw6)7MEyu -Fc`>A5cpJ1QY-O00;mk6qrsVR<$X90{{So2><{e0001RX>c!Jc4cm4Z*nhna%^mAVlyvhX>4V1Z*z1m -aCwzg!EW0)5WVXw23Z7_3Y&`}KvB5JrPwauL)%5Wm!d5sN*cx7P^3yyNqp(I?~tNoTXxb^fMJQ8c{B6 -gaM&2#LY_Ch>kZ~Ph_=(l0WYnVy~BL?PU3PY6t|TS<9DTNtnB!uZIUz|Pv{Zn?araye2nX?&fR6{^-%Vzx`s8+#BK0z;#T8Nv&`DE?xAlS-oClU$4F=Nm642)mCeZd8vCxdkaK$=g7ByM((8Tu;PUpbDTCgu4u{-pG;guxo7Rux@2w7DnD6 -R*;4pm={Ie6u9?^<1sIfCxkznpyjuuu-gg?O|L5N9MH5M4Cs)-hL -}Q56!jKi?TyyOY0DK$xZjFu8++6~zd0Zx{AMq*-*2gSeA0;M`@_Ha_Bh*Vv5%f`oBA!Nipn3^yzQkE9 -XBo~)3)Z0PYI1D_xgiYIFtNc@CF{TCK>!Zpz@1ia4rfGfCd(6g)oKq(}!6381R1c{+&;*E)}&W9Cyp-X_p;G8q3?q+BBdv&HoN7K>*Z6l)j=Ohq1VyeD^|1C=qT@g$A#OYA2xBc>81 -|cG|l6Y%b=*x%NXf-cIQ>fv2$4_Fjs!qxj?|IX6~*F?+`(Sh&B^GF_}zm-d_arWSAs>4=?;z2=|cs!P -3F7X1PyGpK3`x24mF#-{N?@lR-g>me=XKaxkLo!?th;%+wMB7k)!m}CHdz+1Yi@42DosC3(LCrTlO+d -U2K50b+Nm%Ckuvmj>*3&Eb4st4Q+re|4Q1VZEHDc!5p@6aWAK2mmG&m`*SfiEe8X007lN001HY003}la4%nWWo~3|axZmqY;0*_GcRy& -Z)|O0ZeeF-axQRrwOe~{8^^W(pPynNBOvM0>(~z3TmcN=s!kImb^^<}#f57uhs&Wj(Q=pF*_Ff;{q8- -FdG0PH`8E|`nViR&ne+Ia$Bed3y%%}D?b@zUc`o$+P&ciRYg3n9t8)H6vCTzYma1rVT^ZN3?W&@!>(V -!r_0^TGt|pT$lFssIj-&ow6{>2><1a>SF6ye4x>AiDU({8_!w*2!&G_!aho3Ke&(F}aptj4j@m?DED_ -bk0LD@*Mli)(qMOlI_5w -y$sCQ`eNGUPDh~lv@JYeyz2e)Tn=S%Cvzw^!2T3clE~DFb~e3MKtz`cNfruto_vAR8u}u0{0^TdqW3d3rp#|tV?YYg(0rq5o7f8yZq;2oh4r6^*JUY43IdkCt(% -RI%~iJt6*HpUNmSde+=#UjaKanCQ5(_L&;#F5J@(9jh0v`5YS?BLqHbtJS8Y^N9^v#NXqCythn)tEF` -_d}4LVKRbOo3WQ-!T$lbm^>%8fzR&`F6utb=eG@kGG9(6OscJJO6Iom(U$+WU6l&H4?Iy2Dq3+_kWv1z|z5|qmxVgq~+T{+nz_mHh`1hwFr{ZaS -PF(5S6<~2hck0>>O4IF4zsvA{`8SH5e`hIB9q}|sWL_hk1Cj~Ce=c4Tnr83d-NZq8w)8(Zx$Zb?=Bp% -eq5=B4JAH0P|)f}L2%n)D*NH?bV1p5&O&cP^^z}5}zR1_tU9I;TKW5m`Pc(7`++YIR-pcI{H0a;ShGuf!E$l;gl%9gsSy -4lMTbg7wNr_phvviTJ7YG2AoD_9}{h}V`zFe+m -5Jaw=R#N;N;Ln8xYP*Uw@U207Nv}v4u@NSKf{5uc|7Rj)H?D{WZ=EFtgLUr -R(mtZ!zbgp#gDeT2aN(XLYpGX@EZpYPMT0D#x-D7+sMj*7=VxHVQK*}+;3oks%5lB38CSD;AYd{Ev5t -iG3`P6Jm`hqIuedSxCwU#==y}=P*u0$U0ta#FFresT87=t;*LzWYHw!qm;oa7U$le0JhIi0A9I{Ar&u -8^HBvBEn?TeAF<@{zoA&%Aiw=$+PZ>RC5PnJl&&xm`k?==+D-R3{w-B)!7nPzxSf7&kvuBKExIQFM#( -)wFgF0@hg`ozGXxa*ZJ@2C+l4%Hs2wJB3H0$dIGSrDALwo^YjGPd$Wh(w4tTf$(Lw!KE}SM%(})zC3zT%BPiy$?$JoxFK&hR$Pg4(m_2>!oalt&i5LqRoA&a6 -h-5ttlT*ZaW1V5l#o)Zg5vH`M;05=Dlv^2;Jt{{S}JTY~5D0QJ>{dCp8|D6oi -o{P?&U*AR{&1R#?7D_=ARO{&b+<>FxEo87eKkb|YsFh3e(yCIHD#*&4h_Rb{b0_xz#pmY}Pw|^?zH!w -C(8^ekyv&v_=gDv!Xs5VB(QNwHHy_YhqrRR$e|}ZhSK!);djEX-pG@9MU4A}+qukZ?*y5$CW^OJWEp) -$}csrpFoZ80FOu0`Efo+@TGo#AwBB=H9#g_}GB}<&L5M%O>TvuwPS&y(K6}E@X*^nUnx?W>CiA`!bUf ->Ku9+tfB`2mAG)JElqipx^&*BkIH=i-nRB{chZ34fkY@e*Mhf~!#AvXDrz5_uSK2yAPS3X*$@DAkZ=H -=5I2n_DEu90l6(j&$;f1L-%pNIOymU?-&gEV4+l)`eqhGWsr(uevL8%oF(2jVx``I-OnqF8XoP`IT!r#$vu$uaMj1 -9wM38uypI=g3xi5{YIr6YL8T2y(t>Ihv<(vO)vC8!i18ryaAj63sm9+iof}Z}^x~RqDBaj}c~7<*S6p -S$a)@HV8|KEbYU<^7a8Tqz*NXK~oFldKRbbx<@uNrP(v -kj+t`V|P$yq}|thmRbKJtH`;)u7$aNNx>;pz`sp2D=m(pcwTmvGeXYM12rJCK}$Qyw9Sa&UFE`I!4MJIg5C88~+QD~~AI&E;lJ$slBXYSzu^2)^qo^3YLU?DD9f(v2I2=(wjp#H1ELf -wN1joKXx3R!ABVh?t)pRg9Qe2S6@Yl%;w*$~e_+a{#fQ5CT>uIi3It5e}^dRw|$Wo}`AU|K&QS`YL0s -=0z4_&TNwkEaHx41s91@|135qseUX_`L|7Dk9+QdH_#3GO$115LSg^}HAhq@WOmP<$-NKnfM;j# -YOZpdgDUh^1L?EG-bN8OCpP2}*Jjb@`KjqiUD4tO-~(%2{}3pt^bm9byzE_gQ-SN`Icvq)Z-}9$gQCQtK${;fp -q!PE}3of5t8$!gZuGkrP?i{D* -$ym)H%UEpXgQiJuPjC6Ih$=+Ajd00Txf-p&Tr-*dustTj$;*rkW>6gi`V%mHl0i`#`iKN{U}eeKJi|X -aW`D9cFdB@WHc^N}taZYtM@Lqddap~_V2E&ziN?ut-B_|WjH=S$hTW@AUIDpT5s8=4ty7$LhO#{v-gw -f*v|VI_wJ1&X?2rkAHho_8aV$Cual$Yor^V5oC6t{O-T=IagR_7mIgNs!$iQYn8XBg(OD!VG$3GIE0(}3gAW -etFdFTGK0z*YWJ`Fk(1auO5!dZV|;lch}5WoDzX0>>=`NxZI(Tq?9djNekUQ!eD&0H)rCTfSJxHpdQ` -h|LS)$0lQ(L3bMS^WhSP*Y?1`kqjGsKJOS#=Ek|J=b=XcVs`XcnZ^CI&2hYg^`jh|hAzBEXK*P|{Gj- -)>1nbDK!-u6D$R*4j_v@MN~h~vR)26%bg)Bo$X2_#DrNy2Tpwls%MVRQCHbTTpI|n=Gn2-Z;Z7jj@R} -~Os|REw6|^t4il#Q^897h46@0j=3sQCpri`i(vIAN@>lz(}u?;1Z)}2#kyjmp|W^bfiU${;R0la-U4S -3TPI|fw=0n7m)@c==48<_o>_e<{f7EWmEjJo}7=)^TfOzHEWGYa6ZZ$D1$yFG&sf_>u0h3|Nn6H3$_+ -pK?=;%@Wf8$3tDjrruxU>gQyOf0uiWI{Jf(Tfc(DrBJ+cZNCjS-+@f4&cd^z438+H%s)L6x>f6THb?1 -+>IJYNjq|pB{H}2(v~79%(H6E=*{FY&dmp-Q#{1O4~F!2OP24b#s%Ta?m%&7qe_WS`ou5qe`KfRXyV) -Odv|Uhyw4J#@B@b7um3@_#!0pi?8*V#;QhfF0tMokfzF;Znx-tnx=>^pi^og -JRxL45wj%nla?U^03;OEkV60jply%`ykuG%omL!$`uY22-gOPjn!ON4IT5sr)io3(ARC`-D*Yuo)a8<|Ea?)UJf|1YW_<#Wr4xeM;dq;lWg -e=AG^%1jK7-ZuI{`-ULy1L#3Jzy+1F3dwO{7p&LyAi0bbAZBTn3gabiC+`77g -_!+OiW<4Lun!R)%7J~fn5)T}K4F?IyU!|lgl9kvaVny@*lhKgJ!2u&VTg<0$pwme2W$A~2Q-)F} -+fFI%uBc6Q&zt4K%M1&H(%@M%&QFSoQBddG86aCT4EV}>hz1{gUJ{bm){fd5_p20eI9nD$s6$OVC@sk -$N`3CRMh!ocbwz_hU_!nG*r;dxC-o1Ns@xkF0YAL^KMvzUPx$t96IB@#v7zVnKg}oi;XPj9L=MSFfB# -+!|xe0gY0Et2vcd-CRZT&?p(u2gaqDSAMEVZQ*@Wl;1UsXtsW>UPS@co)k7x@_!!-S_nEO_pCgX9A_B -1GX_jSteqg{z=ZPKykrjd@zKgQLT6vg}^n8pTi*d&n$PW1Rg`SEpM4;p94`DQi+>fqDgn1a2MKye@z9IDYBL}gKlf&b0j(C -RKdwfZhtWBf9)f`HR%)=~#cNy;>rscx@83*DE4#>?}Kggncx#1sPCi{w3iK72y4-i6^Qw%r6e+Llx=f -486p}u^yZ|6fMlCE)jMn1@4lIG|H-2pvRgh2}$KM(KM5)OseWzHy0sQQ2V;yZcjV5K>lY@9yf=*9*HG -WMY9{#xDdw0IDk&6cAEyPeSRxSbK^8A#<4ZHNovF^HWitSAUQoDTE-H$Hblk`SYvyQ^z@e8Yv>*Yrq+ -;_C0}=%KI6tHn=TH;L)SHSzV4|4P;XUTQwWJ1-07Z#{C|ZFjxLUcQU)2kJq>szQ)wAEM@l=dg2p5ROWahzO~Vk22m -z|PKO`@6Cey?;q$u-_vp)ERZ7g>eY&VEjS|1Lfh`1lY6`I=1;n~l<*`S_T`}MbR+cATt0N_Re#ZCf*_ -aHYls^1HQ(PoUJ=SPBX~Y6!+xr|88Lb%&3IQQIL7htf0Dl(BOe{CMW)hZq7r%^(QvU`}O9KQH000080 -45ZePW0!lo@M=0p;;6cxu)WPba8homUUayl(q%m_gwQ->eV{h~~(f~{6rV;U` -1D<+FdX~X!YR(WH@%Ke^9GNMZ|nTtx*rX>^QR;6N9G`W!#*JhO|U8LM_PhauvBYzNeQp@M0P)~Sjbe_ -u%-soDiFj#X)lgVVY;&~267VKm6i}+t7YI7^hPNjA939q*~uWL4ZF;$!2#MaE_6Nc;T?9ARTRrw@J31 -Gnr`7BEY=9t|tYPR1gIz1PKC?PJJ7r>gY!HKFwxn_lhj#=GocNUJj-+%1Ws`x -n3^sFT>J3me!1ysog%>OFIf)LOVJk(p&euP9~QAUaKdWg3*oG@h1t+iTi>-%JPA2RZ*ytnL`Bvl`%S) -X{Qsf&9@-d+8CwY-`oUp#%I(!=*1e@+nRq!;?t>ZY -j)C^$C*`hLHELq7f{xU5rh;GI~wzxx0I?ti6!7t71*SJ)3Md#m#D!0?dv%o8(d*k0y2%awX$O-1}Ucd -RO{DlNGYo%M(_ZpiXvMW#y8io`bvK4$hRxCbnaa~)VuXwT|RDUIa0{9R}U**SIzUC+H7c7^Q(PwgV}* -R@+9XjyLaCKce_KrP_EHAl>vG8YJ8g+q0VL3Uve67d?q#B-I)tu*-|31Mv?dOo*mg;}uAk#m(OW)b{{ -Zsb99zq?=F+`a!0y-a*OWJq`t0{Cba+G -@P6)yt<=;|6muJ+GxHtfkbl&S9q_?O`GjRdZo3})3ZyT3&peXK{L -#&INzmupC!*m=Z>#4m{n?~_9a_~Y}%&Nc6HqQVxa0*aY$#k&6 -dIm)agZUZO~P&r+wv0Sz#d15tDr($OVe;FH;61`_*}5sP`;p|LHXRL3%XQAbQGN_BxWhuh_Zk}?A1Qi -hxD-DiISm`AynaZt_~u<1u(DA6Llbd<(CGgw9J -oMlB;)i#}lw?j51bS}>^8M*N8cGaq=q7HF0{@mK-VQ?Bf8bluEwD!w=6gy)4qT9s!FeuqO$r10kL@xor-P+qzuh+);-VGhw!;Lj?}rRxTiu=tCdq3S -7V5bpe9vpgHtli>!Ye&D(7+BRa$)CIB%V8O<}}6fxuhwCtw^i~bd{pSo#a>PUOr`~KS2-GRB!&s#QL_ -Y~{dKg?CWyLSK0en;B)ddE9%ZN1+G#Q##+Sg%j&1UyV(4vxiHOlNnDbo|@ds2wwtSLe&MXEvn@=4Z#g -R24tuil?W`Jj$}6euieDgxygix<`|AnwuoJV|QkY8g@j0N4IJV@!RJ&C#HmGcxtDy+b~{)?!&fSZK1b -?n3Q1~d?*Hmo*H~CF~mI_DN_vTonAkHh_6-?cD*%hZ1 -Lo((k0cyJ*a=$NmRYUi2q4Zu$m;=Y6jW!vnQ8kK*w5iRTS9DH_-{yLkUDK+#(g&@^Z`6L!JE57$ji>3 -0dVn2`&_?DppN%Bpk592s<_hkg)92%_aLVgIJ?P^oz707kicmV=64ITk!67KYXRlYDsm(X{vh7| -R*8p`e3yz0tx!df)J=1wb3E_B9GX&($$Q3D>`)imlhaLcqsqc&!`C03x-CjYXGE18}pk44=@OI+pW^ -t5W)2}Y$D%X$szqBFw{POareFl~z(nUmhu`9Lnkwd$wnXKn#<&)uY)|SWzWth!t?t9J#85ilmo*&s{? -GBBtK2=F-wDZ=1KGB4sxX0vi-5WDVfN@(>RyM;eUjH2HJE=GrdNDTKY;eYSR(SQ)QNI>{O+6gS+}iY` -(-ci<^zaHzYxGan1qQJ;A8_|F_L|=Awo~;Bvk~nVTiI{wMgI>_O9KQH00008045ZePRaQACO8`a010F -O03QGV0B~t=FJE?LZe(wAFLiQkY-wUMFLGsbaBpsNWiD`e?L2F98#j{Q^($~xrARu|M3QsKo;KR7Ez4 -`|+LlXF&b#GxkwX$BCmPOh=OKps;ePw|1Hb?na3t5}a`)j_iA>G_jqXOH(Jx?8jM%(l3RO3l#*E -o!bL~IK%4AD|nZ{e#f2IFxSuR -#Kb2L}hUG%icv@gj{Y$*e90`SfSNad>t>+?~q>h;5Q3)p$H8Wx9YDLVfUOG+rb{S&ge=2gBVJwe()k< -0^(X(|+H30pq}Pl;LPDak_wp5*`kQ_Q-&ETvde{d?XNg3xz_$ddh@qH87A>!v<0_gaU+kS%8v-$nuJw -HV>^4@Wp}O+G9Ylqe01{azKM0ggEu}IS-{@wZJw|-gi#tGip511(1SbL2?~6Y0WOBw3uM0XS{0=*9J?eSU61ME_1&Ax8};xOo}SRt+mA2azP?#=-MZ6mf?RD$m9zfnu>2k@gAV!KLGNs937N%Ds){)vnDkrjj== -=zZ4b!jHYBDdA}P+TpuypTknh!>SCM75HF79kq|dnA4D)Z5%^^#$(x_ys^X(%+_#5r2`U`4)H@Cn;=i -Vi^}x*pEd#Lk`7YNr)s9IS^BbB@7^VM=pcV_Rpnh5htD|QcJy=xH_vr*^M3Dt!NHQ(BmUN!fJ<4U!o? -1OIs_GcXe7NEp38Od%Yg3EHq1QUSE8F`uwCJ>1rWB?csu1qeMwCz~(SBU;)JsQcR`HM7#l02sW)M7cr -2I_`saRZ27=cS?VR5lPJbj(`jV`efQJt9Vm)S7Ra1fT%diNRY#Yqg#oo9iw`7PrgH|ECS_F;xEXg68Y -oB%KF$m50Y6|R&sNPwyplVyl|Y+9DKcCM>)H`;VlK;;%AGTFOPHKbs1o&q!8$@ib2AtY#RB<=I1q@ag -aNBTf7Bm-ZU$SVb-D7jx?^=H=1MI6AlplyA^?2N6?n&`=JI%|Dq;!Cjh6z|JRlX$~?y7jZTh%)B`)y`#BY%{Z_CZfeVD20pB3$|LoHR6R7rpG1i!1U4ty2QHC}88?=Ec3@;_#}zIi6w;{#6$5(;Z -`DexAXLTQ$mXsxjLrxKHb!QSl_qes8SdaCXCi=JoCx4cnj%%IH_$b&(LqEKUPC4M3h -#a7ioR7NGYaGjfH2q$HcAwW3L*#9Bs9>!sM##RlBjG7?wNw*iQI_!hQb!8@!=8m!(Tk!3^B9nBX#)a# -IyJqURv&PdkJCjV6zDUk47JJ(>FMK}0&(#Rfuwji)LsGeY!hJVzutXw=~287Ja1dmghEvr?7)+N6;TM -oY$vz46PqkB#P5v7Y@xakfwcmZlxtZP7f9aq*y^bn`D#ATv4y|)aEr+%cYEL;SUxYpbou?@pUowsvcT -))4&e1A87b$N09iam}`tUyPiKWD|*|ID$L%U4%7mlt>A+q)NcKiytZ+vmq8Bk}zB6#qQOKi}h@-+kV@ -FQP=B_W?SiT%M!HG(!DS)#dr|NZ^J8YY4xBwkY8nwP;&!xM9vJZ09ZIb6!^Ny5om!D6Ive5ZdHd+CQ_Jz4?s -<0Ri(5(4L_khD62W2GQhIXadZWat4+izDk-k()e5FVgNt1e?q%9B8sLPp{zWV-l#Wpf-OD8h#odI-~t -oTFpNUTHaTeNt-+kunvIc+}tcHrvQp*_;oLW?>E)zyY#Ot8qN%iC>;kxf7tn| -Fx2)^PKIw>`N(Enf}&q(aV<0z`eB<}>n{eb-MrBlx*pI_NKn|(k6HKt6ayVNc1K!8+%pRyK!Zj>R_KtQYM3bJxMj)g -58gZLP-E4%L`OB|rEVq-L#7;b2H}To9sS9%YJB-e?I$)70`MP -w#1F-5bh*%@%^Q!FT}2reX#zS?8Q0=C`j+NZJ&#&b5?Cls{KD1Crq^P#zYi^|Sqz4?WAqL8(bzDfGm_ -$OGuRbWCgtDubhJ}+1C6+~rYo7IT}$|Sip~qEpR8U_fv=$WXfFjfXI5)9%0)SQhz=_6{jVsNSTP@IP -?`wyL4=G5~piDu61>nl-4SaDo&rQBSg`pQP0H&dMKd4NZJa8mN4IA+w%u#@zrX{U!3e&291kQ^cRc!z -|fvTxoVC)w?O)@evVeKW|?G8 -nwx8g;Kf!L-z&LoIC;M6oD0a+WO6Wm;np79`ILX_P2^0eDj>bXYDHC>_u<7{3VgMU@hHtb*;SLLuLzEXs=kvGa#p&xDX%^;Wt{Z9mV8BT*5D6I;Gc5I_z8Fu+W9EX|Zg0u{x3yg -9W#uBASXGL>H#!->j0SxdmVAspnux`*r9_eVRTA&yoS8!mH?u~zX|CjgIAKy0|Z!K37WkG4N4BduhkF -d)CIChA_$RrV=RZ_`Iq$JNmd6*Q>joE*;SlO7`gVW=0F&Oym5Dnau<74rIjt2p!PvJL>U~lS_zIk)`; -#J?p)A0YeWq8}+m&S0=iKQ+#NmfVohL)#^)?nNqnbwsgBVD3ej*DH`GB^ZbsN|W}fyOsi_6G9`bstXl -Q371dO0Y{FFt!LoX(emHyEW?e)I8NuCIh|85_u#%V2o%@F6g76Wzrf{J5GMIcyBR`iGDe0J|C{Pw9_W -{=eKMRWXC5yNjEwXf?%mlB(dqSkG9YIbgLP87*-Zv$bv?4xs8>P^<&s0&iiU$dj$JclRl!v>?WpiG2D -G&uiNAZDZx@EhQh?9kn2jZ-zD$J?7QR*GwIb*<_@o5GhNZqc65{+i1KKfF6y1* -n4-J^Sodf0Q%v6{vbl?6&vL9vbvq6`_)TNfqG^>C-b3()BMKL9o)n?KGp|y{R>F`VQK$jHcX?#L{ivp -;2FBLuG>L3D(i5JOgqVEaO=03gaRlu4wb#w5p0L~Y -`zo^FRcIT|k+_a$Xa#k1?gE>D`MCvW@tPH8IRd)5nfUU`))SE^6ENuq}8Ys4q1Mmn~o6Zblvr2R$;J7ZKTNa+mvW@7|o;^X`EOwd2sVL6 -ZM$0%jtl(mN(o7Y!=eRp{#uI7@|-&Xdqp5b2l8EQ;@sPKzlc8&`<>MF7W5EfJHs`7y1bQx1BLcpw(6;=c)3OJ@j`$mPXe -znD#E)BN)u>}%$a!Na4$c-b(2Gc%CUqZ3vfgGN3pnrkp(L(?`#*iGn#{OJkjY39R^=HhIpuZ;(!&a}auq&pW)M`<|641z -`3gb)loPe@icg@l4jaMB%Nl}X-r2~iQFpyzer!D-Z+i;xQ8H+Y4o!3vM90X}xYEs(;(B)crG_+v~m&(gD4F?@6g#6rF$~aUB+*#aYQH&hV2yt?k2oy3T!-nX -|sTWNcCco%ydZ+K@5PyibdEy$!G#zapsB&QDd>n(G*$uOk#dI)jtiqD$hIEvM*cX^23s&AS1NI5`IpN -RAE;Ja$yyV4@|wax;$vgLV5BI-96Kmb>E&?lL-lULnC#6G1!#L2WW2u1qEla+8T=Fsbq|AkO8?8(6gKdbY -9zu~IUPh(#z1I@UU8>d6O({qH`3ZO@(Tshq{EGsHZXB`Ul%*mW3jEy=JU1d$fXi5Q2C&Gbc`rVcnWuP -r*d{EL6Blo3CVl`~q1I2`Z7qr^qeT16$g$PX%!_eNJ@ycVgh)5=$tO*ddlQb|2wT$QhW)L5>b?r??{i -79N2ni5<6NvNC<7>+A#m)BrhWtFPdjr?+5oHrma$!f4^^5Wi4!0>qutbK)Xp+7CA;bfwYa9DT@u0bCb -oalgoClipzrkq%RFh?>BE-9;uqVX6FO|3vcFr?I~%L-I$1#`V&aDFD-^@aVi8|Pq_en`unM{%9BVJp% -B{0gIiA21PasY2rF+AKfvD6~rJ4udQkn| -5}TrKiqeWbM2>`c6@m&=_l>!Ua}*N=ydXx{r_W+AiWYG?$I3oRxHpa*yQ+e!-Wku=GJ>Xt}p-Wl)lYbwH)8msd>A?`HG4}n%n+?ENA@7?fU6sS96TkWf8gWVVeB-dtJ -*^Mn0hdn%d>Bp-KWo%{VtoPCXL$-ZL!o1R`M$wn`1~0%U$LbBO^wKx -kP#CEqB(cf@br-a9TGH;7-tGxG6HxQ2ZGQ3DZ`yl;)qmX?F>QjKKLNpZ5GAM -z|0G>o}1I@&rgh+CCPhN>utbMVca;KOudjI}_nyD*uw&k7aj@rT&aoR=x^q($rYkS{*A1N}by?K#`sY -->Jm&Guu5bRy79YgjOT-W^2Qf_wU8xRE0@<|L5mf9NupXNhVKSRY#9* -R&i3wN_st1>#}j-QINMCtt=j+jfbQM{qv~jb;&@j9wmzZO?q*#jf;w{%*J;ak*;mZ(bQ-ouat(GvN;J -#!`$Ya6H(A~U+>Djxz(iv4lbOH%rk~CXgwHS*ehI-ajV$LhY~Od)&0wW!sjG>+P`o3cDd_D|g=QNgEwDM6!1+fI#GD0tx&t -yws@K=9?xG>eSSysdNhn&ZZ=%>@HGf&JDL}Q<6}#IsaE)pD*2rDe-Ad|$?(&0Q_|TkrEVS0#hPk_0$sm*azXW>uBg{1Y;ansu)<{4z_*x!lt+Q# -15r~}zFJVd5E`OqPxK0=CRG#N#CTeEiBzKHm&kV|J9G!b{c#yD#QN^->~sw!8{$3y30$ ->Q(oM>eJxbRPG(IGA7OX?Ke^+WiS75Kpdl=&W*xv(6%iP>sX?S2O%35zo#XyQ$8+k=p{R5Guk3W|Y1L>Xruj0NIa_h#Q-+SA>S9L!isfJ}d}H=gWt=`=8mM{a_>pMeYH_B!^%0CJ; -yKy=|2;IfhoZSgph-gIyDZA1~q&T3(QLPMYMeAS@T`U+d1*6{Z%gYn=g&$MC?NS}p -Z3L98Hat(&++1aMf28CqaJBScPy2nvUmcZzetW;FgXz0k(1j0;bm>$)S($X%oa?i5H@etFAI4v=H9F-chhS_dC2Mx7Fe|Ql9t=0KT{z3zV*i45gi*aSks~J4fN8J)Z>xVLucU7{y14pRk9 -bNUE$K1xZozO%xISe$Il_2Sh!GuW)zeUyAM0NAer-Gx(?W!g$e*NhgnS44M6^xAp^W;>twmAo@r2+SP -uXJtG&eCpX=OHEG8kXo$a>quAMvgXURXb)4|L=wK;$8sKn`DHJ%4W0n>Xe7)EN3`FrvA>`Y%DgeRAty -h5Jl%2}k*d6guzI2pxTJXKpJm`B!?!)U+}ntjBxAErCJqA&_V7~<9S)VpgvdffWRd2p)^`S(^)4u$1< -PgTQTu2qeVxkRPadCAC)+Jg|SpKjiA)yr0yQe|G_orPr7C8cFZ>E2fh`$k0W+}s!GbTSwApaE!EFV=R -~B-goskxQSw%ZR$u#ri%sKMaV0Cc8ld2vm8o>xcq+Ek7Nde2!XIFm**t?TDg%gW7)sP)h>@6aWAK2mm -G&m`(ry0006200000001Wd003}la4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ7`X>MtBUtcb8c>@4YO9 -KQH00008045ZePVO(B=Mn(`0L=mb05Jdn0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FJEDBaAj_1X ->Mg-Wo~w9a&K-faCxPX!D_=W5Jd0#ip3Y%P~3YV2Sb&RL)_rF>7f+k#9K#TYlS46etwmZ65DA?OZO&r -n0+$}f`H`*X`!X6YY?=dURLPeSSkySphAax2kwB7XbnQuQX!z~r6JX#axn|M36rfRp8`tPUzDPu|UGY1ZmyN{;W;5W@6aWAK2mmG&m`((DUlDK&006ow001Ze0 -03}la4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQEEaAj_1X>MgMaCx;_ZIj!!5&q6!fy$GSE~Dew@wb~Q -or`^TZeqt*pWIBR%}^8xNyw1|Lx8$dJMC}pF5U?z3+k%q1;d_= --K~hq4UXU`a(k*R>+@+GV0$#l3d<%qEbxp3TEvx8MM5|99-@TzS+mSok>{&+1pLbH$;_~7mD|nm7TQw -=pczKc5HC6L2a-NBc`XG0_x@enXgABf1C2^wrUHEe)nJB}0^-jD-t8v+6pEK_pEq!VB68cGSTwfsok3RZ0+i$Gq5ZULx$5tG03mSky#W+0 -bYkqU9S8d`Bh9+=!Yx#MmJOj#x4LY~vjhGvrO0jw2z<$+|Z$=XuLw93%Lf)u>k;$O8B*Ukjb#nLN)eqOpyX5=j^7h@$cjS^xWz*7W6upee%?9} -^{3*1NRR=Eerl5}u7)MW5;l|9k0M*PDXKQyA0{}*O+T@Y7eb3~GA>K-&V8^Qs+qR8TJONiw+5yzSep$ -n-l2($qeaF!RGEAgmaguCW2`Wtz8*tOL;6*EGqQ6IGm~s15@a+~I&3t`Gn+m=P%`cAYG`j~0MJ(83T= -G3UX~@ExM!2{RW4~}#MRpo}%&75YLork4K{H%84R7Y|>rZ+`J@7t#{77YU@MvxuE_ucaZ)nL%3d-_E? -(W{HZ_$|(!9-BI{zv`wJq11Iq9YPD2+FRJ|V)AeW;%UJ^}6=1 -_kNSfw^Z%kY;p`FmOg?-Xo -jRqug*gbKSDTO%PSy7NCWtfYqFkSYnxPl6-3#woHIa6i%bf2;ULs0Ay*^nwFEo-F@9sghs>}n*u& -QUj;7R1td!HeRet#z;xN7qV;b#uY~rIK;URW$*-d-0rFTc_ALa9DhD|*$WlPzd|(k%8`zSEq7jS)D#YVMPB#$cS;b_M%mghqbFu@ -d(nc)M>E{Ha5To|nJ}p=dh)puuNVY-0po=9Wz5~QgaCcsFKtbdP5o1%uWc -{JqLF+GU!M-$5H45cz!VPsu#-wcj(8qhHC8Tct%WxwlJlcRa&yVd4K)k(=z1t_188DB=-+zUR%*wLK! -B3!pYifX8v}odjNB_oc-udCb6W*F^oKVMNNU_#1R6uZ0rdCv>?xalUhRJFV6Z=(dNAlkFokc6bx+>4A -$wP;Ay@^A`dB@Bf5ksnQy>X%+fazEDUC$%fipj5{7yXhYJFSq>sWzfBjN$zAJYk -To-Z&%)WGY4&IU>Cf-%V+wm`m)Az7YZ#|F%~k5cTtde(o!=B$Pxrt%WPVdA(I8YGV+)yfDmf=b+HC0* -6BIQ6Gdf7Kod8cwwfN}@09!k`mF9k`g1i!DMa95WU&1ngJXd3!dhxtmD_NnggCv1E2u1Dnl(SQ6dd(j -y1pFx_EkSg3UgYrg1a0*?U1Kj54Iv>4UM+s7+_s4Y+1{a6Ph;ov>oylhj@r!{Xr!vKlda8OeiPX2=*F -Paf7Q9t>229lfs>jtj-bneKUz&Wr@GLyy^Icxm*gukAz!&P=*x!%QsRYa9gw`RW^$I~Nsn|k8t1!)z- -6v}-s={AM!ttw33Z``@n5`*s7I25>&{wLDP7L$3mxl(IhZo*01ZwPyzcA!8F{s%*}ZD@-9ncC&;e)11 -Ha3gXupJ4POQ)O%ZUu!eA4&P{w0AYU|%}|TKpDjfdzT_{AEawAJa%Kh&uj{0-B)G=nOR|B4bu%4aL@B -)~mU!G>e{WbCTelluBJ^!2-r}dKwxRLmBvp=ErCJAqMcYfE8$OYoQXYgrcCdP5@~NIfq$qTDU;Lw{cF -_?RGZ#Xym~D>MKFczeTrp3LXGF8{p~ZFPMDP4nJ*y6ScX_sDTTr=dkq^e*hf_&i -&Ro4=<*j&^gv%LBBOJKh%BXZ1}09wR!Wc1JP0RA^Hr=?1zWP~93pI}2iv-rH=d;B8#076Wi~PmgL_Vk@df`8F7~tEOQ)6C(B_G9TXJ9 -UPG3J!drPSfnyn?L!fs~n5eukishr}4!M(Yle8AKBYKlr%MI3J%0Lfn831_>ux~DmTE-#IJSd44|G-b -#KnzAGqJ%V|$s0$`%llidTu#;j9hM8mDr#qtlP-j15#WA18Dh)aA9t3#90N-ih*QfrK*=zvR8@#^E&6 -Q0~jT)fhf8fpi9Q9hZ(EHWE4(~J(%*G<1*#R#w9xLd1!N4k9-L~|Bt|AcQ@ppLMfAeA1-?d>o=-o@PU -5sfbffs#B8`MU1*(cT6VnC?}6VSk*{qF@9_a+Qz$JP8L8~?v;CqT#|82OC-tB^f<7Ndi-J(1~MQn;&H -L5`dS26-D5jE9Pdv)6d;>|f+fRh&-SLdN~5k0;*v(~y}MTuqMTW2r0eruW^u=ox|NB@c^&7aDjGxXJi~ -2pY!_4T(O~$tsUYQy>)p->Zh5bGq3-~&1QYaZQMUP6gA{zT0;%`rLKWBtNY~zY2tT~{Ig4*)v%syE-QC*Tgakiq3 -{s&M?0|XQR000O8CKQ-Xgi6!-tpxx8YY_kdCjbBdaA|NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~OZggy -IaBpvHE^v9xSKn{jHV}UIU%{maBz3A{uj`8eVK6c;4VI=w9quHFz@sVx+WrBtzELrTu`kfgj&f~_hA8CiWMAlQ8h83?HaEk>hDDJvk)Cg -YGufGOE3RZh>&Ql3Gy;e3-rl=9tKc~*%$XRDWIOfQgU87Ru~Usx7T#-kHNuoiqrl4M<}NH(+$W-vQN>D -Kj4*4DM@pN)O^LkXf#4ZB<_ItNm9@q<`Lk#G^bJ$O>sY_e*{M`9TEI#M)d#XuFPSFktLx88B~cEDi|ltdW0KFqI^}gnS*6N`eSUS%)Sl`VPr+DmD`Tp5NE{s)6 -f5*1yJ_FoaF8Pcr28!}}|Oa1cCpia^+SNtHG!1y=~xHki&a+B?z+w25Bzk)fYjwx|SO(N+GLFzfTzw{ -K>b*SEL&t)`x=x!6(Fxhm_{QHkZKxyZDMd7#%sM-!1%iNze2PIr604x&9_hrDzi&@O8Eh+Ag*uLZ45q -On2JOFk*7z~b9Ckt}njeDC(w3(4dl269Z1i`ZGV9N$G%sWUjRb)q$vle@kJvK1p`PHPlUC%UanZ#Z>- -M&5zYWLt7gk8ZK#a!c>Q)>uM{Lv7!8of(mZ?ggISgL1s>Dyhf{$UT&b@Dd7AZ&?mRz@I1_dJqJb(1wI -8>ccLoUD8F6v(^e?;F>vfXQtp0InAt^NtClwc`Chd@2LA73u=2K+H$GFUip -L3-oqBC)==d5Z?EetZ%Vx10m>J%oIQZ|Wp9qTCw7UR+8Mgzg8gL#z;ldb=~&fu{ld`-@Ve+4ESen1kF -OEO4t6bd_Fi8(=%0}rC^AyOhAU=eXa`cFZXPDM?2zAKM88L&XUIop$*IxlC>4M}6a6^3o?pCOT+SBr< -jwr%W_B_6-BAgGV^w``Tciy>)xN{_F6gBnHSOek7xRVJuTw~_c>zZj>K=~ma#VWFH}M=h -KHh1&#*P0q%+?E$imJ+Bwy^bT`^;V=HVYEhdG+T -?@@+!}o02!&7+Tu=>ZfTxFv4KIm>dH5Lnmo|FGKIGMEuFc#YZW*oUhwtBEw0u;?=w?!p%>E>iZLvA^r1DDT=L@0PjNqpGvzKV^fHJV@>U_9?`+>=R0(T$ -0!VjLUM4VIV_H?>3Lf`9M490;J2J^bfs>4kc^h#wk8`XIe$I7x|&~KbW$>4=b)H7sZVnQO`3gJyq;e# -F3(+AIz;y{XCuwg=ioijra=q!zxHqf+?v5$Fr9A&35wkr*~A$_L%M{?a`G3IkO%n{jCqEmWBZVPu89W<({o4J8CI?v -V+4S1fzcdP)h>@6aWAK2mmG&m`)=U^q8v?007TD001Ze003}la4%nWWo~3|axZmqY;0*_GcRLrZgg^K -VlQxcZ*XO9b8~DiaCzlBZFAeW(ck$iFwWeF%E^qKTs!x;Ri{2jZZvglKbCUoHItzz5|UU`1PhW@bT`- -E-rWU2f-kb2o9D~ZdfE#~EEfBY-34@XbmYutS8Nv-8#cS1`p0>hWkMEgB_zAu7aNhX*B-kRNg{Tv*zl -aQIL!-LhD9vWoW(1)7bV+?GKridzUQnE?19U8wP%Pq1yf-)&s5-(*SV* -reV^D-61OL(v`uyDh}`#4?yffh|Uk8n8v3Zf`Rf`SA{k@rMIki-#8avCJSJel)k$Qr9d}C5An*h -%OAexsB@c^a&o*4*FoTJ#%-AM)KuJiR1V#M70bs>*r)*OTGr9-3GW6dpDoo66>J-++$=v$odlGC{lC`7O=%4j|(1_c_Fs(&wR0PwjwGMF -m>>#pkP6|X97ecSrKnJtJi|jEK86(uw%7w&dMAYUMvj7i;;FRSo=)NvXJ{TdcePUOYVD64i(^6Ny;74 -JpLu*S;3~X#zaaX>xHu*&mHF)qQqWX1X|h|Fn?B2&w^Yzu8>>M+Fpf?5y&KBaRF8X)?fk#p_b?R&>j0In5w26UG_ZL0*EvF+We^Gqx#;EI)hoil^Q#zK=5=#epZ}`W3#tn -sEv7#lH@=+@GeAwm_O4O0-eM>QiV)EIx=REM;8mef}j&L|hD=BiLlf(R_bZ79bYiH)a|vbCHw<_tm@O -=JDQ>%@%5+Y!{O^&Y#Axmz;VhG&fAZEqmv?6Iu?QhHWP%+H(Os7lvSrRiZ3Lj3Zqrycv -xXnQB5T4Db(ou;=NGL()L{Z($dKFtPv-A#-j2F+Ak;pbHmA_1YR0c1@D;{ -yh(OYXxgdzm9(Or+~jOKnF8V@`#a2tYnZAB*)GW@)Vz_5HUT5C-#0&Hu!nB2|%>znIIbFm*Mchk$CXkgTYy} -6xFZ?4a;df{&;le-rD-Q<5iOz$Rd>xgz>9{aX$769P6fP?}fih)78y!x}_I1u1qP=`5h&5J8|zn$`SLF;(L#PaW2u>h1g6Jb1h^~+uZYtPXGDav(G>? -o=Xa4O^H@A~(RFjUY&B=WbN=AL5yAsle6sp&)B`M59mwToQn!c*VNaz6neTwdrLC0baO5+j87T1@Ma6 -cg!1)?PLn2wcvFn7d%e?+gr&OosNhp1FM$W2XpoIsImDr1EQi&!kMpoRd99^sj9q}b(bVdWAF>|sE@y -{Dog6-5ngR)-CgLt+tOn*pp99D8A>T;uVnce?lk%?%!cIKliIp+pives6;BBYC06OBO`VMAN^{w53$Fml<#On3C|F-M0}_Nq6}iF%Pjugqk&D&z|>CLIL}oNK-XSqk*dc6>AeTisc?4 -xj^=XD$_!9ORFetzKz$G_f>XZZ(mT>#{KD$1oOq|-(ViJB1*i@S$VO_5q1C`H;}lU-9nP6g#el*Wm|Y -5)r)Y}B9EaLWU^OWKYqcNNIx*XUTF_|WZw4)8?_+w`#jvFj90Xku5nm_yiuY%W{VSzTB5ep=d0v#}6v -?X{&BfQ2*Fp3ENfl6Tv*>z;1tOc3lu(~1kRztl8=}wPn+f&eRMSUpQF##tGWekO=mHcQ$Vhkj_DUI?t -pP<9#-xt=dDTKBXb2mK -iBK4H0+o!-3EZiuQ=xh;*qIO&-EXjUM$HC;FjS($M?>L59Iy}b%j55vi;OF=t6-S}U$^@Y3edj>M+9qxX1^ -Y*tgW`e}wkxqK$QDSHr-)jdp%$?(MU9Dw%(OxTnO78|bt37ew--E!IMQ1Vm>&f7t=G$x>g~pv2iYC$= -h8$E)xxZZwfl554Im*jt>Q1ZS$rx7FFh#r%7HideAe-D2scQZ2Yv(|iD{Ip<_^2C_($3s(?6dQo>su0 -d!e;GC?G8{cM#-Z977rNEwcovwCnWDJ`AMm2LcF%lpIQlQl(=t3?8FkFB{WaDnL8+p)$#WOwW3Ch<2sPy{+mNggbKc(+}O=HhfNrKzZFdXLY@?)cHv -!jjz(x<4>9_+?xM3?g<5#8Vy!mMR&sKSJszA9>WI^KD1cqiZ3z!cUI0FJeml*{0PPBE3^s8SdgxRq2&KqLKo&B|A@#$~Y -t^98J@s17YfQ{gz?ova#ka+Y-OO`a{lSsNCvEjr1_K+3>HV%CEg@aZ&aYXOn0f3xzi$RMfK3D)Wk6VR -<6@9&WUC-Y{Xo=70YYRbmZ}Lo>~{QB)=yN`S)_L$FLWF3MyY52j$XkHJ?aG6{ePNPWue!ixD+Lx?9@8 -B7GS+#YZV0}<2ooBzRij~^~1fk%YOQ7FGPuz_qJ5WB79O~>+5Kay1ZFnf8i;YL4v%^_ZdeNwA)e?8e% -TdFwt`p@yT(hh*%+)_0iTpOUlTwrEh#s8xCqaFD)6qfYN{rLZwMy`8p%5`G->5+XXTOZ8vpvR8xX!4F -39%vHk}N&g0_lE!piz -Ycp0q3c|FtjhoJ*y#2=itwyPk=o4aLU1MGZhC9H3w;@fRDOq=4Fjw*5NeC{w+{tELHmG7ylMGd9c%^$ -kDl-8*I5uHlfj9zhij2R^hfv9$LfR)&_@ZH8Vgl(^`>EE5;>$2>YU~yYz>fHtD_ZH9bK44(O>`@*Rg5U#}s;Y1TXE&8g*j?ino-L0Ur}~T -_oVOi!)XoY50)wfg$)2Y>uvKSi>jq2z-&sF0crt4HL?NKrVx|l3ys21Qvi12!Yu?+k1vmr>Y8kNVVd^ -$78`{HNvV$(=^iOJtH~~Me+DDSg(2HbFs?D*gmuJ47#yVPsaVOdT9dbQ>3v*-x};$3PF25Hzzj51yfF -1<6$DO-mThOhJT!s3gUj)$xkhUJq=i;6c-}Hh9F%tpHV|Lm_qxBl$ihxyx^kJtYfd_Bg)N7zK}co6l}ff%xdIvs_q -2m%O(R0*)I@~R1!pZ2SBPDH#inycVRGm4tJIdKVgxb`6i74`)>LO_4&W -vQEXaTj-oLBzwnlm|43hXHdp^Oo^rq>D@Vv{@{}j&x5xKSYnFkxvJj}XA|J}8IPvkOo<@ugXkk@XBr) -@7GSTt3HVKy5;6g6pZAzwJryuc@w@0Z56-&p$zge?g?G8IIc-1UUOa2u3PlSnjsXk}5bPd%?lJx7N5J -iyqbZ}!QTuM#W;2#p5!w+?16KKBko{u1wr$rC=}E<@9gwCulo(3 -~ArH;pQ?{UNyYnBNkSwIE`Bgscmvql&UFE8QAkOz(2CnXV`ly&8-f=8w&6D36l;!VNnPCKp}l(H$9lJ -R?+~sz|I|(zS`23aSFvTy3}?|8M`GWebU#rAyoZ8(;*x>wsg^7adz+SOb3)Uv_(Vjgyw;y7nR$rv35s -NtqGUYtI7PQ+lhZZyFMLtpsfa`wdgMmxPfj#56)P3IoBQFooaz -+$1XEEY`R@zhkvVKa!j7=#HQO_f2KC+0Rec6bUdR{@mVy4Tw|%U&#MI;#~3A9W#<1b<6n-?{9bpSC(B -nG}TxMbcKQ`Hp85oy&q|@N>%)!s1Ko%Ak>H1Lv?cP3Rq|)9#=jU(}7#M%Uxd>X@b2SDa}yk2OKeQW}M -Rqoj_@UPL2C&9;u!*EHA177allQUm2G9ES*3SM_{!iUY(SycqbYUXL1s7N()@Z2#H%*ygmo@*!>wKMK -AXqYvIa{6x4uHv_RD5sekywi;|^=Dz+lg8_&9Wz*4YleXR&Ev)L75Jt25tr?u-oPO_-hTPTTBfMPgp& -3)RwUzv7AZ!(?r|MMOKG5xwtDt>6oiYnq#3mw$3@3DaXBlJ|VEh_me;Hqp(dq@WO?qiErKZ@O=@t`#j -*jZ#APa;F7kmsAjgR%iGo;batD?j(*dl>ib_Yp#3bp>-fUEEf+hWJ$#}79_oEm|{2Sv<1-&?ZBE6F<{ -qqe~7^&xGrb18+DwnNt-ZE$8VK8H6nB7z5smu?blmr($_+ZP|Ily$M8(7w&UW_NK2!~!W2**&Wk>C*) -gtk1@QS4F928gF;iL{}oi9Z`3KwYcyF>qYvt5B>&9rM}(NkWdJJ1DjmFfo<6rsjo9(N(LV3+4m#UYnl -zCt(P$E*`9RX?N%~S3^X%tD=4(q=mdx7q%H^OrD~w@rQIvGUBm#nTN7{4oJQs*c5A8Q?ECw0zFkH -*&zfVXf;=OW^RnQ%W@t^FY*<0qeWZ%?(#UsGFo*N4AkiV9{>Plb^rh_0001RX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#PWn*=6Wpr|3ZgX& -Na&#_mdF5SgbK^FW{;pqvcxub?MCOjiNjBw+H|x=OGOnH2KFhOHd#MydQIN%$BD8$5MtgPt{rUwU0g# -ksXSeS5PL;}7B+zIy8r_Y?188q=Z!nQ@mCM^a%2p~bMNzHj-?_>~m8WSkJr)m1B;s3J#qEcQh#sOOji#v-rK$;3h0F_~GN>0{O~g7W7pf{nxsU^RCDdF%18TWSQke&WT -7dteOe9NWCdz;x8I{1Nn94Z9I#5?!Z-7oFuA}%qx|4&8Y$iWr#ie>kvO8n|N~@&=>cHB=QSkau<%f&p -ZXt64EIr741g*@7*i6o&DlNs;N@kPEhe5fBKpKl=RS>_Lb)cT9QpC}U`UsrQ6t<=5BlGYyOE+R2ZIFX -p7Ak#^K#n^FkM2-bOpXNu9gWgb=30zUp(+#T7?efMR=I+6>B2Pswk;fx%^v|XPBr7Eh(u{`&ibMAkRTUtvRA -qOEAcLjXeWEeZ5*EK^K0?-SI00wX)5G{%*qP5+6WT4&>}A?$=Y0i1WU1AB=KUff>DFNK -|CP*T$ac^m5)_c=E)RvXjY{%5N|4Eo5rD7;KV>4M_4nH)RBQ{Oro|z_ek}S-kga-n?P6VD*X3j6k9U@ -Ufm}!u4+E;o1Um32x#1(wJ4V;Odf2i#>GaWfM%i3JXJ_XlI&L*L#yS<$zUMhqu=T280dzAf^?=dIzvS -tDHAa55TvIM|Yq0=@N$YPazL#bFt*Bp=i(E!HJm$e#M)$d#517{=sO5yz_*v-pf11z;7pClxq(-wZDL -13~ud&LCawNtom+w@S%_3Ck!9gEcm`Y&^8l1Xx!>E8MQihodK>!NORT$&MBp}LR5{gd4pdA{naV}{$tGgY|4NdhC%Zf8zj -RxOJYgcDuhO7ox>yq0z6ih9|a34%M#3Dg2 -oXFMK3>3|pK8+!P6I>}2(rZ@qKCK*nif?EL_I#bzx3IB)A -;(MTlLN0#4h6xKUQZs2}Ua&KVB8AFOzGRB;6qJk#sE_WiI3bqca|B{5jh|boqDZu`kQ_Qbiau)I3hY --#k&w#fDb(g5Datk&6A@n|d?2ca&1mDDQnM40%yB>#Q8SFb^x^&fOw97AD3h4IItUzCUuQs#;v$)&g^ -5ry)F`>{8TiRCuC*)#17E-_)V4nO1podeN-O!c?lSlIk|n5*RvBRa=$rdD& -0N#b<9-3NXNgi{)q}YOJ^*5Ks_&0yWls7`zhV3ax7xGh}sy@iKa8XhlZN!CGQtU~d)_MPqCL -t%jd&E^g1mHy^IXf1X_Y$2s)i=$qqjzZzKLR~5u&5Uo8#DYyibTVtQzp1(W$_~AAjUtGUGznL)e^7AU -9n2q8U1*FgeJ|r^;wM3k(D3-2J8i74EXF7OuLQeQ^q1I?@-Dnkp<*dY*4x>D7J)C@mzD&@TJ06z&Y5; -*ocy>MsUw{2g_`~?eaPs~P`Z?+;Ievcz|Gj<{USECq>(TM6udzH-?JJLzk6wRkC~qlwK7RZD9REK#3q -M`lzJCH!T=nL;6}>i}!tTw}D`K^`lgU$AeP$0^t?ka_Gy2v$hT}Km=Z)d675m$KUV~2>23_Z=gT7m}= -Wy89)RX7I<1F^fgIz23xB0vVpQO;w7`NT4J%_`-ruynU!yr6kRzE}EpCRg}>UqC>X$|rwDs-%4+$AmU -xV(6HeT)$Tgt{3<6BJ)Y0s|y9M{kemD2f*#Mu#W?{d}FGdIz*%shPua48vNP7LpP -gt3>BqqEf)?0)in3r63$HRjkqw7E;S3gXPv|aLWHl9fYVM@VGh=L$}-a(C)RSapbL!-IuQqMuYzv3?M -XIL`77V`4H%j#Gc#2-oXhkAUhcQsm;*Oz$~w&+aq!hB~0tDTddhHiNL`ltQ|j%wc}k`JKo0H@uRHKxb -Myy72nF*&I7s|YvEIw3wLL4JAb<}_ynEP6?SH@t#;1aT^M{4bI90^Gv^pg(qJa%qBG^kZDOJi|NT*A_ -i}T%ib`0yo2);r2)#)E5Z89FLVy+y^;SdRQ0mH-0M8y7lSZj)G$|RAjd6)>d4B=l&&MBc&TntdE+^Ml -H@7C`=eYi2;9gn661m -_vJjWop;__|twne;8;My|M&Y!Y&vBcEzQB_8u3UtayqCErTYpXIB+!}_IzHhn+vL{lUC5$jw}-6|u_- -2g(7Q^V0ig{5POLet>YP`o6Qouu6Z5OJ31}7bQxw6SEL{N{)Ri$=;?{?LU^@+c{4m3wJ&}11Dshp~_E -K7xpGJjjm~p8GI)6842$P7Q01p7v=opTlUj5uuDU+qdgh7iobW^_@HXWqgwkZ8B%#=k+$VZv -RzAi(|KfmZ{-U3lmhne)LCZv0X>k!?YJE`8rBqx~rVjcxZ`qWC$H}6QS-!Nl~j531CQj9Kre7 -4`&ybBe9P(DDAR;Fldp<L;t(rWe&_U@v-5B=-a`CdEc@U5hi)OQ27R9i$57z7yUv6v -fp!FnPRM~x4B!8E{g(bEim+=ExzoD7-4NDyos0aMZQ;TNTP*bL|F0plF8?sx*u79SDQ&q^>58w&pyh= -MMOreSUM)!IDWN2%`0cMyzHq9K70dIvlFM)S{uM^Ou&lvvm(=Seity}Gmbx&tft=@61+dQqQkFPE-Z_ -X#@w>X#lm)959=iMTGRGep1vpwll!x_}bVV6GGCe_lPFWPfxXVdx#cSqh}!q_&LRQ%Pz8sKyK2lutzv -F$YOW(++;=bF+`)+9m5e&bu+fOfx>#r|jcUn2f&2Mm!E@8yFhE+|vY`vaLQaZ~93pA(*LN?BmU&T{Q_ -!S-~3QrgBBWe!F(jLM-isxHxkf2jYaQQO6HQ^D^%UjE4*o7(v2x@^C;_CG8Y+v~xj84XLj4wY`ao3u@ -s`RKhJSRevxOWuJ=!EK9Jtu|)glJ{~h_Ln4!qT-#$T<#Yg^p+K@;vR6540q)en3!c!5Uho^tWmKLei3 -xK5v`d&H7uVrO8LLr*^zwx(*H7U&eaZJo0JEnv29}g-*apZK;T}=6)HMzK&}x&f$Qr6D1r5pS&UoxY4 -RXxKbrQv!Muqd#``liO|;pm5mz#6dAOK>5mAZYo3h^Pj|48}8NsH(UqED@14}?}DVHkWFs@FlHL@ZIf -@%T(n?|!h{8dJa5mntU7*?Tjc>D(54crB@8Gn->0ij@8;0DUP0~UsE>4Smi?|Z6$!a`i%Ef4Nt5T_AX -mQOrh6B@1|OVx`8&aN*G5eyiW$uvoUv>xP~Q`rOs+OPM^J?MxYsDKskQi0Zr8s|fvdL+VGu2#bX-W7* -uFiOJ%gvrt1^lSUuaF%rVYHr(uiJ4+`IhtHW^nieixdx}o2ZnGSA`~RKgB -dcby|M<^X3zkk9!h1A`n$|k6Bf?UiW{`Rnz5_E7UIK@$~*_&W`wvfpreJfi0KwJ*oWaIsf;u-)9{0A= -s*&|tGUrP?kQls?Xu>Z` --MV8hyFk$tc%_56KQ+KgqnOJmgZYp*8c!Z3gm{-@8l-3`yqO#{s|`+5Xd>J>1kp|4hl$`H|)ZY*kqndU*V>ofaDXfmi(X=hk$7<95w~-_;ea*HZGm`CM -y?^Ng-B8!x)C+U?{(EdGXr4agp6e>wyFO_Lhw*gL{Y=vbv1C%oB9j^r(nB)nLr0|TzPY$iZelxj(Ka;9W~V?^x1x*^K$P-i2}6q<*TSgN9=rL_k2M>&S~H96yy4Z4fgjvBm -IAFRMQ2NPNNAVVP9k|63`V$&kRl%pMJjOpoa-55pcZ0|%A{@eqWtYDyqNmpXcQka;pkOl1zQVbzU}t4FuSoEBN5wA=BuDT$(L!%-)y7eTIiKVb%|}v@!2XR?r;s3?8CPpml@@-HVS_p%-oEJ`= -oe_k(M)K%e&ZwYD^@R}%?X%Dl?JwXKwH4_>RO_BHjqpZEjK{|98(ia7UAv~XjChQwmvWrBn8G^!=B;5 -4{cg1m;g>8SlZ{)eOjr`+lxE9Bq-%Pp#1VBjF+o-4c3=K#b)Wd>DoH(WuR&SWY$t@--JbE$6tE?k-K7^ -xFa3h4DW4Kx6VtiIG&;_xur34>O2}F-ra+e{l+5@RWyyq!IN2m3$gGoLES*d)CQ1E#Ifr>1+S0hUackI(|Y4j>fH>P3Svwu#LJSX}azM(g_Fn)gA -(Ocylvux-tx@}6AI)q(A{6wvoCmH6}Pkc4$Q4#kLQ!svaBQZviMn8QrmU-!o@wU>nRl~CT#_s52d#O_ -Qe4;NZ^^qBpGX$^Jda8;c{s!;pAad~IW2&4{{Fv!;OL8{az1DTEVodY2hs6k2k6yspRX!8h@aQ?d_RM -UFa4&9@TzgMg&EV`E>C7Y=sT1+L*`D}akK?jg^0casZ*a|BETVfI*mUGK`8;h1YojuYAjs?}qNAaXH; -%9Ec#kM04N2^L9ev*R+RAHed3`TCS!>>gE0|ZSMGhL;w>&nRR{CfPJ7PQ*UCXcWC3yudsDhv>G6!6y& -C<-xhq2S*gIE~cPwONXrzdmI6}d6lI~@(`s1etLNtr~cm?jV)kRLPd79&mwo$M4UJ7VI_@sT*BGi>u3dK{(LxlQ -|28LE;+k3xD%v?m!T%Mw=?jB3M-UIpJ$WPY9y8X5E8qF6Knt8 -b6L`RWgD6i*wAx&m~--{@`oqMGK3=S=Jy^xE1a@MV1jm&jh@a2c{x2h!{pbbdf?)cWyMJx@MrWldJgT^EP}mK(XigXaE%`mU$rb -Rx`Tc_wYliC-(2IBpRCS$a~jmyWE|`}7ie=JDI0Ug33gl)+Ig(@IYZ1`VbV&FtD|`WE_gwNC~<43@dg -5=J1uf%d26yf);c?)l-mi`B7?Z5Olgf`0lHCUXI`;cG?PhYQs{;3Nrw>3!A>#s2GxB-HG$|7M&XLmo{ -vnI*4qwmYLk0|^1R9>B+WDERhvQ$rZ%6a&o4(MI3y#%cWH$!MJx1kMdj{^jYQ`jS;w^s-qNmrK-6@6|IV6Ts9)&`H;$j*NJrcq-jjDz2iV&ewAou>{D(%HB7&T7Q;q5~yMFh3@6>M61ha_GTq -*pMHyx3*VD^P&d)?6tu=GJUl9X4Z@q9`q{ZteX!#K3NtkU4_&7~p1*DwlI9!py4)D49*8Dq4_Q4?`e?DPLWi -f~s7;9X?>cImEvK3}gfr`nfN3=^lD}Jg)hKKO!lorpsBwDycBpGLl?cg02OIQ8SQ|0z*U^|Pe*M!)L* -i$DTnKyVFwd0h9uWPED4e46vAXj@AnHV4kp$y)e)TTcO>{M+$Z>wuNH@Y#zdNNcj=8dCem}aM>uG3vB -Vej-YOgSX18P4DO(QiG1dfNzzB+z(He{Vf3S$2i)r2{=?{Sej~n_IfN1hFxM5Y+78MAAF->n3%N -D`nb!+~Z5W@J1Yq6!ovz3&cd<>x1FVNbiYb3%*?)xs_WG`H_L{)o(7Hp1CQ8ywU%^koX$bwMu*PQ^K8!}hLw3Xxm<(vJXO)@$J0*CEU1fdYM5?i1A@)A@t)n@5iLijW3(M2^m5oWki -K&CmoaM68gRa~XQ@iOA_iB&FsvF@Gt-Yx=tSJJ5pw%;F=7#p#fH@cJd6Cl#7=R>$< -zHIgGDpZk3*(NL#laK229NEEWt4p-yg+22L35z8RCgX2w-W->+7gc!|*n~Xo`p!%vxg- -(w3s;8dLH(PZ~VGmL?2^B{*BBbj?mpZM>3&aALRAMs52DD*abO2%h< -v-(YKk4w5*1rsH@*^lVgPCo5xF7`MNj<^;vwV@{r<&!70f!Khc6Z*gZCuOh;`iWm#O<~mPT)@%lv-mN -pGDx);44@uO@L5n8q<#)|6`Y~tL00hkuf}fh2Cl-+}MaT6}Kf42O7q`*m)RRS3;>}zBgxOh@o$Nl@rH -+*wwiMSdo#^uR>h0BuF)|lutm9OI|22$}`TbtJRq&GXpr!i$u{FSV;;3s4P$O_#X$wO~bNz%3O}Ddtw -QZ|M*)G -+gb!UYz@CLlX9lOs*;##$r@`-7w1lw3E$hW=(}>Q -V5pl#8_zKqp!sc6%{c36C=vcoj5!!aV{bTCF-y32G!CB$=-{{^pVGxC+g$mHzO$xl*1wE(>>|WUEqr>0_>nBETF027dY*g|Qx@RIbGWW<18iAJmxd)UfFNs?{);D@pKsBTWXE`R8-MLA*5E -m^1YfuT&OGI}5K?GLLAx5+a9csd3>JR<^!sdhIQf@fJ~A(Q$7~@^zNP?_c<8d;n8bPp-W4xN| -Yg|g^bh)r5RG7htK%eSeZHw-L0(S;=;#py$m_|l^*Ks5vzlmMhd+q%>OZ+f^3(8lIH0%|nHkcQ4qIhJ -)M^E!@p;qP52$UK@_mZ&II{;!Y8dm*k(V5;|ZSz)qf&!5xJ8@%WD5++Y_cbCYH1v)z@SBggV_2n9^ad -M6E7>jlF+gz+r)9UKU8E&gZl+55su%uf{7l?1k>U&_hhjqs~at+|Z=&e%s--~(!Ap3szKBL!oytmr*7 -_i_gx63@XM5+%JTN?=&ski4hoMvj{Z?)#z+<0dGEtD8yOPwqj8ZH~w=};ivilC4rboT=l7#b1>Z4z}F -wU5Lb=+$+Z_tCJX@b(kv7Q>I@6HQ}Jgc7#-`v+hhO@7Ao)H(~mhY`IsOjp?Vr4#n?iN@XbFS^$*za{v -@OUA5A*>}1c9=SHObfmgeGif#<x6B8!o?EM-!vJXg^cfO;MMlycCC0G;|O>ULf8_o$kic>dC~H{!#DT&d>o|>tt<5S4VuY#=lCBaa8*TQmb -WOvhI^iQ?S)A!MoRn6LW-(QPQ{?*7FxPChNNs2pb387G}ExXDwRQ%3gkzxvHl4qdDgVW-b%5n%2@=X4 -MjxdUS5nw1a0H&D`=*0Q|#D9IBcnA2v#dhj;}{CV6f)0-;s>$%jp2pIjrq93hMgmU0MV@;CMLnI*pe- -cvTm1n8mA-+Xnm2c)fMW?Vk52)YJipC+)N;NNYSss|sMZJ8Y_uiaJywm~6!lD_?vA`QmpH@D%<`M-a> -xH*5@L^axg(Gjh>*yYvbxo*uO?)TPjx7}mUM&h)^mg{YaHebYtzM;~zgUC&NuvgOx@hoA7-cJ-COs5^ -k?tIT$CU}{LyLrCWxdHV0c1nR@ev96e9{&=XjI{QStRK~AZe^ga1Vz)lrz+-+X^RqH$)%a6*n@6N$}} -3X{lgBC6Wrh++vNC{t(rqQ^~5&G5Srk0O5eIXg^q5zP<3;jS`Q(KT_4#45%Wn1+`PwxBxkK~U!RJN=x -jR__F^rQW(RAE2>7+WHHmUPGRnc9<AwrEY$0P@gV#51jkyC0DY(Iva^Z=*)B& -k3Z8_F6uA14r~LLuKrA-GQW6}Hd!xSg^WJabtOgAx1e#KCl`G~m*xONGyWMR&^OsF6Z~|Be9SaD9`1= -JLJO6ug$EC?#z;U!XUP#28*d@qHljP+^ -^F!Yxa^ML|o-5y>9!Jq`2d?oQ4^spbX0>Lli!Bxoxbx`nmA2Zi@uc(anuWs&`tCaFVEAnjojSy%^;Eq -XXT=x`Wqd=`sjIpU29M4%-47C3VURm68M8Ptk%=eA}K;u!Noie>?Z^8f@LSts;0hoqd1l>OH|lJ$#qi -MS0q*{9#zQsj2-6ec>?b_$*$lcN30V|syS(W6som>L$%r6%@1&MI{eB>5Lbx))yIV+T`k*&Ir(d*4GW -Xiidki5c&ZiXFU+I;Qu?tN!@1vjgSbMZ`J|xowqK*R)THF!;ym@Zi4zP)h>@6aWAK2mmG&m`u=mP692Bhf^ZP*uCb -QyksJ;S_(R|(XwY73oSpW9B2c)}te1#HswCyjqUisAGkoZ|a-BB&!IsFGALq>(J!2Q=7c5tWkoOzb*x -kjyX(c&3J4^2N!mvseO$kULEf>3Wa!5+18`Z-Dp#kPeY|FnhCZ>SR{K8oS~gT^D<%V?ajl#7%vJG-H@`c%d22oigaAA|c=Nu68ty_DowbBEk1J@GGY22DSDNQ|#6FyLV12xD3WPbeF(n)-}!|Ak*2+H2|(WL-`?HbT(^5`_kP}*BIr#y8}hivuGkMsqDPXv#2V$ZvXgB&w7|;qQ -4-SsSFA3v9$Z)~#Lh1LoRYIF`+sOOy&ea>GNL#o9G#5%#9oIs~Ui`JV*?^E^B`|BNg8<_D&W+5h5vT`gwSh_ -n?EeEvpb0$ys%2@BPl%ly;cH+-jHM7SWu1-XX7u;kIps71re3%0u^PC3o`y|LbH_7%GH|@`JH=%hrjS -zPqIX$!+GZKW|d463;bDEcSPVt30?U};MlKn4=>v*w^ba>_)Z9!c)zZclzBF!F3ykoYgw6JtvSFT^$ -f~y)zVxx87M_Te1=@1VC1DjlblMlynQa9olLi>w~5XzCK1dF#{tC( -zj2Lvi^|K7>bB2!qp}4|!?^5hb+mm)+TEeRSE6btC6Axe?l^Z}meJFRAHAYW_gfAQ3NDIMd}c(;4i}N -l8Da_eG*1y(bmT7;Jm=J)fV~{bvM(tAI<>H9e=rXsn_k#eY=@)h@`v=q6A5?db?&+Rlv#LOG&qZ`kKf -+xz8oDR6g-*Cn-^qeG{_wCn1<@d(2F}hYF~B9nPDJ&7A4iLqgN1ygfzwd#^1Yjt-%T(s+}$ofJ(u>-Ia0f1Q>p*;E -;+Vy^!Fw>9PvFVKL2_x!xGjw8Z0rgzi18LU8!+hJ_k%CQBQ03AsiOne8qfQ{c0_6&Hl}QfBfJ3? -C$-|>nwaP>+?{HiKK6M*=um1eB{vm*E{u2H%~kC#h*I!#b49(1^;@djx-(L{{B+@iI)w`T!v@w{Fhz2 -x4?p~utJ0a5e0IeEBrm!=E7X(??<{wMe*EnbTp%x|F;3yXtLW3tuxH_{t&l`=622BDKa;$wPf@*B5Oy -oO_x6-pg44G+PoT3xY26k+|zD!qyMz_{4)N`@gWAlbolOl_vqLD$(N6TL~Su_E8*!P*nI;WdvoqmmMtBUtcb8c>@4YO9KQH00008045ZePPbW$eOC_v0GLDo05|{u0B~t=F -JE?LZe(wAFLiQkY-wUMFJo_RbaH88FJE(IV|8+6baG*Cb8v5RbT49QZe(e0XLBxadCgmUZ{kQ2|DR9M -m(?mDF&u;>lUePY&M{_!mH@(JvZK`s8EAqp#_jVX3H#}yAO)DzMi(!nR}!AAFP99xPy4YuKK{T)# -sWJ4&9IVUZnmDKWWR$7L_(h7up@EcW`rqA%1U%4`Icg%UhU4B)1oM{VCUEbG8|)sV9UQ5(NDfbeh*%# -KTa|3IEx=5Y|O%zY-WroZbngb&ti!vPv(Wq<&8V|sK$Ua0GVXfzyEa-uB@K#ysYN7J&%EweZ09%0_vJ -vd;1Wv*PqJ#>g-6KXSe&jOEZ=*mao>=k5!_MN#0F)mqI#vi8r1hUaJu3=idIPf}T$Xoz#_{61#$LzMC -;Q+iH3#<)UQEn`_(iKv9!3+PkV*ZY-4kU0b0MtMC0N0w@=t;m16%pQZ@25tCz>mtDgKIwRe9~OE$VIn -z^#T4|Cq|N=U*Y3FUsv$CV^jYhf^SCMz3Fi$0X}Tz6}lN(fNt6l6_ld$);q&}qHRQJ*azZ(ThXg4-!N -@-fWc+00fTTgi1OIhf5mo3#D?dwwP}D%vSI6B4}oA{o0F|+L9{lpanAIR8TrnhSoh#(NZ3VbN^u#h)^f -Ce`G9U9uB;9QVmf(>DaMJRdr^6r8T_CuG6*xj)`(sD_$Rp7F1;M$;CM8k%G3%TZh(Y22yD1OT<=yPZ$ -#LmX#{U(e6?5oBK0*KosV!>uUz!p%jQJkc;!zynF9TG!NIFMq`5GG2dxd?-IOX$7?J2vmH8IwC9jysL -$)8XP#nQLUEli7Uwbu`ch}qsgF6v|nd)P1nhE&c%)HXXB -9u<)eu@ei)1E1 -BA-$SQF~NKy|E)?%QOb+$&!+&NT%|!cHU=NXg@!hQ+8?fnRkonoiIpbvjwh;cFW#o-bnd$C0kJi83GQ -09lwhpMn~6F>p<}AmE*7LK3<^-AX`$-yd`>jcK4MW00w%0GXlFID@eVhYYe@ZUY}u{&GpoJw^t^pvNr -eHB0fm(Fn^O%kUvP?ggJ1r#gWTIWr{K!hp35^RG-4)i*_wS-voHvvi>QE=}9-U*HGKPol^(|Da1BKdl -bfbJxotrV1=(Kj8=dKa{p&%xbiX^r?gKxJ_o$(PW{`+e95cOw`4n$1V`tXsj9doPXsht{8|4M8lXSfm6g>3 -sqWR8MbKD{T#P>XKz5eQMcLpt^0da56!n_K;#FGMVouWX+lf2qcOYVIhN@+&pYR>76+c<`k6F)-8Q*t -$*`dVbI801f6k{iOg5%E`IYp#og1D9ZTd=GJ|Xb}2hGHB4o&#c>-^}QlOIXKk3X_n0N+j*^nQSyI=v+2@5D&{iYk(~zmCiSoRSTZmq2AXga4>TqOl=o>Gm3BIHZg21K(I#w4i1nhGYzvR5@>(bkPF3TfSs85Ewc9uxw7ZP#E^brYLJ6G8 -himhR<9<5KC%IbsYwOn7e(B@Ng{_P~vP5!SE9IdambfO?sKP*eYpkEWG -B5u^kJEfLtbi6$VYI3g2K227e$4neqG}$wD -*@>YogHnA?`^#aQ;a3QaqI>?%LO=63VOS2s6P-jl8KGrFp4hq3?U8CW<2a#Q)gw{zlK?jxuV!ZrJev7 -kU+{fBdQQWnX!pJ+hz-m>}MDZn2mBmh>oXQAF3zY&WFDgj~A=B?wp2C%iaD9}P8aZ;Q6Mmt-$b%^t=Q -R|vB)Xz(n%5O|$GVF0^mVj=1M4bCk=BtdnXbYL+ozAP>chw@i%NN*Xy>D~qz%i}Bp`DMv>Gj}&B}4+W -gW*A36$LyS(Rr5YCaI(L%%nksOXu+V4_$}+!g275>&;)Wngh8C=bfS(UO&A%^J=P+T|WBP6Nq!)GF(~ -m2m3x=qP~*3B?w~OBbmmT(IQ4o|J^MzA6&QSSgW}8JmH6igg2O(K74O%8%#{1=Z0#`$g4z7bg!xsd-m -rPA`F)(pw5tAJ3_=83y*6hkUqdY{uP*Waj~GX;GWZ8pKMr)ry2Z0t$MP)I=7pgz8s>Se+M<)}h6H9;LsRVB!hm@7weTHeXBD1pCYP-W878n -hs{hH0l3QdwuYi{*I3d;sY!BIK0&~RwO3X>>>VUj6?2WfGp6h@sQJjdVqY!)fcXC8N1 -;>f01S(fv2PIEC|1+3EdmWsr#k@b!bb`H(BA8xJg8d5Dwc)OpSmvIV6zV#lTw6`7-vwoQEnl#(tYOBx -41lhz*|_EN*T6rXNHW?_3Xo#te@S$_59VL#+7nE8r;q6q^Z7}rVEi8NE8Lvbb~4InU+HVOEB{Wok(gR -ovpQ=fQdoQ9@X5Ey-iI>Ty(>eRR#IoB!YQFcD)t(tL(=@HEN&AJwI_*{TK*hJJU>C8UQMJ`GsL5MYD3 -jt3@a#~X%_*@H*&PiTDI0pYpRXyOKKLZvcN8j#sp>+f+BhHh6+FJsf9)HIqgYtC#2Vg7pXX5a|}8a-* -X6i=4sn^nSJk)H37DAS%*&PmSCsmUPU8pYTx|S^-7BQg!NTgY27tUI-A~`C&P^-R4iQbI^K!SC=1Za8B_`;gQ{bsY{g!$etm^y{Lcza&FlgyrxPaA<;QuHZ4(Cd?I$5!sA -C}ITq2D(pg4TGAkQy^IIg@%OzE)%OF&wyb>8W=@6f0e#ru=@XYyiv}sKV(#R7vVV;1o$j5wA^?l4p*} -yLh74zZgN!7X4DIt=bL{=#)J;W?z0=vf4e)4xm4uOs-Pvn$Lt;@J>4?eAk!TRbWL2~m%H*|{m;9RIpYiF3?hcNSibplf1|l*`aF7xSttP#lNao62CxNUNOgA6HUq#=yBSgO~3m6mAe55mYzRMO++ -lxB^| -f2#ud_cU`WeKEf#X&5vI6QJ}r-zyFElu7XHExxUS4J&9aE;BR_9(7d%x{GB+w)ABuJVH`HI?dV9Jp`3CxPxI)5MIe -HN%Dru_72`p48>jcBr1$ecQXabt;JqL0x6d0`YUyzsS&XdaQ*S7dn46PSade#LsrGn^~1&0#dTjI!R< -BhKhNKyzAwcj)Qeaslyebp#PLm7;^8i=*CCG-dOW0A$An16r<^bgg`^y~(3!<#Y8^6&M?nTYJ`GbYB) -O%g4(>Gkt1C-wUT2JrdCSn5qNY8DRo?@0^%z_L9U -9SYz?JE5g!!b*`-A@%73Lt@oi*gNx44<^)ZKB?)nKrpR)+TwDo7#Fpj;>NuNl4$jtafflotS=oTitwl -2BnsUkUGp8$HYg}NBo10mjMJ$?H`NTJj=)0dZKptR8vXR4i!OO014dsu{y|e442(n8Y0j3Yl)AjlMYm -)dk%_;i+h?{E;(mnzd~}J__*YV^6IGAkW$KRB^LGi-Cl~Oz(qil_*1=8;P;}toK45ypynzgt>5?$P)h ->@6aWAK2mmG&m`;LoOO106004q70021v003}la4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ8FWn*=6Wp -r|3ZgX&Na&#|jZ+Bm8Wp-t3E^v9RT6=HXwif^2pMpy>SB1vy?(G~_RQLz?T) -F>&g_v!C>kRmDD>Eqr2TWy&6hdMYon7r2Oqe?xfN-R~?DAS5U8__M5xKUYe)UAJ>q=d>uv2#QORYPRmquk>wGl>DvQxR}G?8AGcT0Qf*}Kbjvgqv|m8rI -sW`hgwRELXK_<60(eJbGCjVSA^>T@_+ABw}Kn*&t=$P(J8#AY3VV-_l8q(GMl6Z6 -X`^+?W#dK)>{@D*8%D;H~W>92_b~8LOU5=PI2XfT7A2TS2z!m*?(V)@>Tqx^HEtOs91t(u|)0VjpgE0H=23;R2HK-q0(BGxE;e{~MV?k1DT6VM!J%BZ`(l18X6jX`yT+Tkh4NH)JSav|Pb -yb-Wp3RE1}OVSfCUtWQTbNDhpzqr1=nO|RsQy`dTdes1T$wa{4;@}AXe!KYn@~yyim#;3*&u%a1>8Cn -LpZ+@iRMWSMw=cI=9L$j7%d6Y7*RLm2;5%1^m}kWtIhv#R^Tno9Z9@O=45(pxLIpe!) -y+Mv*x|A1_I=#b4m>g0xwZQ-KoV-&Q@SjRaEg%e4YmK_x9VWcy53l`?5`uu)xa>X8Xjrg}~cu!it&wC -Mi))mc^POm^D5RE6xTZWakpsXCOH=tSBnDXbwPSw~&pRG=jOXODs^@MpBIO%1%(>#9bV*I7yV0HhdBh -@}orZp(iZcm(ol51!DHC*lL94C`W?ErNaGUTruCA7s_5mi&$)TZvY#l+Y%3x~8S4BI!OJf93>8EA45x -!qT8M&-SbVg*if7*1Cm2y#}p59ZNP%p5mr~f7rn_Q*jfm1pc -vwK<0%_)TW_?YHmxHy%IQ7y2>#_q@~dzAiDRCdBZ_gIOuyGvYn361^8(*iW0emZ`9qp33X{XWD~?{+18iF16)w6Un*-{Y*LqI -ApV0qj00c7Cpatex1xrrxv~jpC6$C5gV@sK04l{PQGj$D@~7{w>!~{4&gmpPHKU`_G%}KJ;jC!)UM&&Q_ -&wDdI1M^`opW6n@)CpD}HVd#m_c34a$iPv>k-w1*Zrd)0>;u7oHCL>u6=dEcpD3{vnN#7DO~^mB4>or&D{AxY>+iWD<=nUwJ;i%paIYH<0^PxBg@p}F>A{9c|SSvp$$BVEQ -87=T#eS*p=qZx>W5BuHX;LwX|yZg9e2bee@KEcPvbP`4bLY0PNIzN~TI^S#2HvmK=b@?)hz|NLfejTxi_16&(91qlP6L+NWWscq236cC*8+6%}Nt#%{1D){s%Sj2J~?$f~u&lV -LBcN6LUuj=B<>rv`uqY|aTTh9}}c%fZ67b1{NxBgG(KA@S&?e88Ac{)`5Cj#g*)%FY8>cc2iY1v>uIE -<_XI90ySa2Ek~2GqW>Fgp~LM!Saa0ONb40(a0Yp4-*i<*!gRCAj4u5yj!|a;UKjgnc%b)=Z_3~blt(; -7AH|7bo0K3zx-KHAZ*jSIfYkSRN9CrfooQtt>g&-V;)IejE`^Ev&{T7D1c}-KIT1N}eBcSP -x>I4v_KLFRCWU81DXf^5XM!XEQeh(AOxuIQcw9j^9|1C;Tj+w>{$K7Oms$aym?v)75>BpA(z*om%GPQ -6v?9O8)|TClQg^Al@{!91W}olRv#4s+o0_Nc&~is;Wq%vqwoYh%cKiTW*+kFB=cdB8faU#X+Uh?$kea -kNlHY;A6pP4BM|g2-lqyqS&p@n|U8p^oM+)3`9)lk#1%fuugXkb$>XrEnHp+$I!Nb_^pwtT&<|I%;#8 -1TE?(wz{KxC1?_3{@M;E<9&VTkB-0{D<@9FIWQCCLRy$zS1=M!1PJbAl?i#~X721{R#x*@?ceub-2IIX -!3-&~u4TBv!wtP@fW$fiCReg*R60=igrSj6jA>MBkUPwY5+M7$N!F1U?;0*PpGt$;>muzf)3m=- -TjW0sw($)cRzl(FB98G7iA6AFi7mcL8$Ba?7cmB>>yw7*?r;;F+dWQ$53U{2nJ+T3Cnow%Wb32Do?Sv -jc<-$!>+i5(n}h0vrv39YwzFcezkxW4WbJ9gBI}*jvJiDLO9EcsOiG;OYeLP>BF`|bbVJ-cja1U@UNK -Gx8Qib=oe7i6b%kY1<*;8M -3x@D2r!nOz!lc>C0BPRZ76G`@*Bp%jjqZJ(z9gB)*_j+ZGhv -B!Vr`EW$e8P=_#}R89e4so{GiA1p2EQ5&dZAaWN76Z*!kVargDo`}Yj56C%eja6`kuOxxQ#F~?M@O)@ -(;ZcS+cm!jpsAgS{$%?i>b6*7A?ojBE*w{M2@Dj8LZJ%76Skm0hOAz*Im`3_B0kVUZ@K`>Kwq(65LFL -F&+x9#ifK;M;gZQqP4s8Rs;oS)4P!J)m*yTlbwgRUew!xb$mfV&WKQOPDSZosAnUQ?ALtLDoFF03>d5 -H4Vkv2!mX(!lG$7NAWCO#kMbw?Pom-`1JgtZ)qu`L%|?V4DwB3h -?>ZZ?0G-@(32VP|D?FJE72ZfSI1UoL -QYWsE@zfG`XM?|DUTRS;^3%@6aWAK2mmG&m`<5kOkqPUB>o26yX>S>A{pzkc)jd-(4M_JK!#x|;y7c*au^M}t=Yea+KNAVdIU0>Rci<;y -L*OWi2r{+O*&&RM)--r7y?_?TreRJCOZ;j?&|xj}@ixxieq1#$qJt$~n;SJ|VRa^lPtBK86oC^A3x@( -qtNH^UlNK?rTMvNdPJg~Aw)s2ZQUAoM~KB)-fu$P}3A@d&Y?E&mDqA$W+a0S)XSYH`USL18@!3N}-s5}5P2 -jyKiT1jy4WPa~+76P6!?=&05|xtC!MBCO&tj31>AM1Fwn67Q+|ZlKg%#t)pDB)dI|GvHYAfqjvbU95` -4+6_athlc)i23C5<_~uDkxefDVE>q1o{ib`_v`s|v0ztoAU3K?0j}Hy8 -*H{S5&1UHk&Sx+{JW$`U#Sg53}>x%^zbR^>KD{iF7|+&Dn%qPv$qX)6eIVIlKNmzrI>bfrT?5d^x-PI -EQ+s7t_ld8|sBTHvJ6}Z1HJwevXv^waI5_!yLI}r&rg1%xAxTx?!KL&d;Wh`C$s&O+K7YWo6K`)APyf -V$9Aa7n5J7gmndUu!RUHd1PNcO)(SeoWQ@+o7vSRHs7lQy_ntEv92Ona>s|mXGtR3v4C -|7I3bJ3}9YPB^?xcwQm3f)1Md9f+RbePR@ba0+D4yHMl((tWr=}$64iBo^r=w!3H$}G)}}J3=c=92Lq -MA$|Em}<51{~04z-Y4?Bs+UL-E`0; -T0WQ;Tf;mH8Qe$|ze> -p}b`fx#?EtF1Hy<84E5Zzb|J0J5|hX+$RzIiw}4xcq>;+S8$ot&YXqZu`rJAMlC#A#H8$G`xJ|A;BnO -8I*$UJ=UO<&_Q;A?#BdV28T9GK1}l|j0!_a){L_7Kha<^9{=H)VE$2u#d~~-0t-oMzkAZY1>;kHk1hm -1j-mss3VsGD!*!S#>?8|8b(z*IP&6qmzxwDoHx*cPl1C4; -&XI_~-K#47-qnHd`>=tc(`vzm&t6jF{rJD9+Mt6&8qCzgweGk;$Jto@DF_v~vZs$}~;mw2JZyB=kK*X87hYP^52+X6b7e{~#>`+B|1i_WJZK{Rn0R-*L<71o8WRH(T&g_GaG)Ux2a<94z^#!%8swW1OXI -Shu|gctxQHI&dl+GkBd{*JTP@Lb0pA4vxi}bEeiwo#20jm$3MQf-7ew-|Xq83m5SoAv|?mCBgeBqGpKEaQ(o1Kj|GW*80Cid3^>N^rHor>Tr;dW*eR -2n!PiC579gxesobW4R(^IeIt~rG|o+wTva|sLnOzR)?$VZ8^co1kG^!k6h|xgVv5tmW?g!nMx|%)&*w -eevme&6Ko~U&tHcM1H5&3#H!%=wg&fi0K_M0VbG((6s{g*^&3=C$JK*J+|~t0WF%AIXy!{|7Yv4gWn1 -VoKu^uq(Q6@&GeV-3}S#+!V=9IS^%pUf|k?SrwX_~7w~HTRB5M~%JyafwiE)Mf8#nshE$_K+o6py&TVOn3Y)SBo --P3F+RfI|2jO*RK>HXIwgEtxma?^4prJPs>&2P~o@^XMf_4PJ@5Pn9@;huviKb#%FCc{+T`8QHucy~< -Umvqo5cxn7(ZkqtO?xqbp3*&=W7VrtBZ&7El<%)EWrd$WrSf+D(vfZ;J+X4O(c0606h -W@zkKIF!xdX7^z}3Bme$Cqc(Ifjc%jOp)va#EF43mB#a8t?CE?oeFaR5u2$;&TC)0gHnfNEzs7~dBi3 -jzgw9%pAr0D`L!VbuSqJuOdmUrarn=>W;bpeFS#4DS26uIik#OBJ-PPEJ3)$WJvaK3cxpn{G+ZKl|bp -yef9A#8&4QWJ3We0yA)Fp~DhLz~SZi>`!RuJX+Euno*57yT>B3x(~l4)(Lv1E8=BP(93G@;66Foa*W$ -msJx1QG58dfcKxVfdl7b*Ur^|F=0|O>0hoj+AoY!<1vTt*vU9)WlL5Ie~Bz*SmM`dU`;cPW+oo8d;iy -+6Quf6TyAm)wD)YB<*sY{sq7|S9`2rf!8tZ;DR*4Af_p9--?8LOd4%LX0bq4=JBoljsce(E$zD>jK#0 -XplamW6@_=&(QD+o;m8Ho%}`1N#Z8r7dCco0Q_AMAJV@~*ATR}sA}J;c?wic^aEE%%9{?kV*72v!p{NHf_%JBX4zlS>Z8n_PjIjHd3RQ^3iGPHIqJ)IttyNf*w>P5Q;80Pv@S1Zbc4=p#@p%Kv3< -UOx7tFWd3Ur)3v@>PB~bhYX(zAo!Klj?m7Uu?Cm|`9BC+Dv -`&ImmTV3AGziuw2;i#o8);Zqw*9T=YlPtO96G|qhX;xOd2t=P#bf}ew9(y9!8O47a*NP%D(t?b8&uMqzFCf?ik5@7(fl0<4VhANgF2LxIyS_0+{tTIp_*hLL?~P -zL;H1FPhm1ZkOOY=tgnWK^lCRF6tGe9TngaS(WfjC5H$S&<;X)idQA{p^`&{2&jz^!hX8BxnA(}L3Ud -+hq^<^WE+jsUuRW>M#e4*3-UdyIBj{m=y=zGcQ5BT<1vv>Z3utmJSUj{p@%k*lqaduOMGmF5wUmAGg0 -zR=|wlAhMh1hMvtw??#UHGup9N*1LpXF8^&bw&Wa~hW~;J3biBq>lK2RzCK&^!kCgCIoalV5?+caI+O -Ek};Ccy?UX|tt$RCZ4xxHA~J%OtqjRna*ESSCM3XYH+SxIUdWoB;=N4}(?9*W+Ld -KCyb2;M4#)T@I`H!`~Bj4etlX_bc#?p)!p -KKHQ#5CLFC7ef;OUHb?&iZ-@kT=2JbEn0s8InQ0@vfw<`Xw_bM7(0-kc29rdW%ayuB9XM_!!>YkB4l` -Z#SRyQN%0d4zkWU&oon>+gwV<=SscW{eZMgzQVB9D6vKAI<*xUE#r8w}+6%1L!AsiAel( -V(3#D%Fe)N5<(U9c9Q<4JdNVA$Q1L6>Zz=^PuVg#zANm5FihwhAz6ID$56etfo4A%TczVeB1@4iN6_}ft@chLEfZ}y5%yxXPmR5N4LYX39n;(n&vVrDa+R;!?xvc#TE07HT=GD_*^Ddw*~*MdS^LkppX -cY(s*`-&>n|IqxJPm*?kTKqV5Hu}@nWq{?OI2DL8V=9+grPz-V*LXdP|tSdfU#|bM;m!OPAhOMZ8OIi -?vW2iJf|zaxhOK`(~P}=v8%BW$v%NfEOn`YF9vAMdukGxc_13AOYT?!#haek%9jb0(j7ioqG)6t?Iu6 -AE1skjYIJ7c$TE`liG@ZF7{j%AJXzG653gS*BiPl$PN-{f6n;I1#h41$0s-P!vi}4Y0bG$?7WnYJPo- -{V~-1QVA}xuY5NjJXgmFd4Y>w`@EE85#oafd@g>OCs=wyk4L1kToGe9_x?TqD3%p65tzX*6Z$gN=>zh -#0?K=zb*z#L6@Y_(L7JfUXUW&hX2?5-*eG&f4_&;^MHGd(E&kmbfX)2+ZR#5xpV6*w|FIT7k<0{+U4===fNuMgM3(p+v~-4araz5ws0OhJ=vO|cloj!y -zb2wWcKi9MG{BfaPw0Wtb4RFcYl`_#Ho+gThvL_(V>rk>xHr4Yw$o0C4lc3zXZ42lP%G^#zKdz_8klo -{lL*6k#`J-4hEk;9<=iH42Y_RYyiyu>atgBN9&E<#nIE&eJ*+u7`y -^b*`jvnA(;2{hHUq>9dv3?B#C#YaU4MRKaRbbd%eqh@I~~f(D(Y+|4+18=6K}?M32xI{TywoUhM*E2>(VHH<)7xwj^| -WA~ExFyC}_@TC|B?{;HE7Kff3XK0k6lF2fB!ZKM%kmwTSDVOCcQJ_l*d<#St!&9})mpc| -hmg`g%<}QqvZm2+10wKybm8wk$HuTum0w}`At_GOtwiXnLt|nA6I@(~Samp_A!Sx^T!i0mjt}*xyy=( -iF4OI7ELhl*_%G)I~})yogumg_c -$*VT260h-pkinBMLb~bfA2b#`4V{Q&06PQhBtD^T>B}y!+8l|i-UB;Pfo?7+iF09TV -HLctO=76PCSE9-wPBG&pOqaboi{^O4{n96)Vjf83xBQMQEzGqOaGNY2}UmvLPsU?Rei)zok6{><3bQpbv6Qju#*nQ<3|)a?#9Glz!&b+Xwyg&Z0BSgf$?L|`KsDUb5_Y31m^KT5M(dt$g@v -zu|Ly*<&E5eW-zFe}F!UMAC@9Eod%VdJ#GV -X-*Z6EZ7&3-AkNEG%jR88%-A>U7hsjaMK|I+CRwlvLYx(IQSwW7|j50`_!tA6O>_>2CVeQ_rLSRkD8D -Q$^DfEv=UMPuTKOm^H-vLeckc%tc$?ia{9|Pj8J51N=`JIx0w(=AMko7DU-Gh6R){_8`|@(2q6z`NME -zs0ZJnZ`4~VHtR6@GP7+5 -IHPGKg^rhnybh%RzHPkPeoE_5YaKWGTX}fAs#w+$EPVe1RdL9`6HFG214Z*=;OHcs;p4EDX%BkRumatQvgs#V}^yr`eJQ_3nfd>pwp -kN#>JU7wX7loF}hq_u~R@nxnch<6ngE;je|Lf~p(-apfXf5;Am<1hZ`RZ$MJ8I};@p3X=7Q<+`R`K?# -jW36f5qo56ZAC#1-7R%hXI6E&M`dX*z);!eY2#j+SlGJd82waamW5>4LUKTh@|?=?jjT$!+Hk2gLB*W --o15MVIT7NM>VaA~BnjG(!pN#)SL!mFdvIk2ja{|9te(yq%t?ShanCWr)yX{^{n|4~9|27JpK{u)v9d -%>X=9cxTcY>KIC!m*v2=*MEU`un6j%r0?FNo^0HXlQa5&krZNo?+V6c-C$I6yQ46G7tP%}@|a7*syfX -vWa7mf`6A#gKxj0JlvkZ5c6HXe22LClcIV=_N%43 -acV^yQ7e)@{$gu>tt}krQ&kKF5E%7AgdTPw)(hdfjID&NeR=LiUiZ*l>=GBJBYBCe#t(q6c3b9Xq#Ly -6!M7-g?v1E-%=lvb8yGhQuOfIk>1*SJ!hABO<9;qR!hsB*b-SXN|J45^ye4xxktt32-t&SV+;)vm_xE -&gFz5Oxm_~CE>S2Ixncb>zhR&%0}f-(NBr@7k!?dRG0NQx^c;%VZ|^Af+p+yaWgxB+U{w;flQ^LOGk5 -@s(ifQ@=UK%90eGvbOFLWL*}+dGaLacyx3;x|@esje8E=mv(AWrS-4bFML8MbGNmc+}v6+k&h1_FsCR -_)QkB$+t=HHp=!K5K`dCq@G5`QZe-}lMZiIVqf!crih~bBdLc#0G2RV)Ap()^rDX@k%8C?OO -WluB(P~+z&&8smd$@0{clSqn6lw18LhdyqBsuLOHbd!q=BQYYY`0WN)1#cf3;nyO+)^bj-f;U=@fP1z -aK)-d%iR7LwM^5)>IaDH>-R71zvq+fRf8dnD{vlG;`LdN)~xH8(DKGke&37e3C}(O<@ -8|pD_0ksQz1~+`Ts$?Pd}Tc^E`S;B-Kg@D6|Z=rn<@DN?0u_+Ju)%t{ -5mM9O#H(towpT(5tDNk;FxFo(NhLnam6@Q-wyVc>qravILb3HJ2!cgd{bb+E+bB63jCJTISxN@+8OdbX*QWoHywtP(|w>wL6WkOX0m6 -l^)Au>L$X%{}YT;2=4xorP3pIl#0=gwrYnEiT*A9-2SFA?O4kDn)MLHc}sHo2KD47uw{F$#4 -31Zh>-FX&)xtg9OVeia=SS_+(Qwy5V8w}&-Csw$QYf0C+iGNhKXv_iwc!E|J1;S+Q>`9tLehT_@|=Bv -Xsa%EgrpccNJ3hAH9n2|cLe~inzBy=PCHp6>znU;9vo&`R4QGLL=ZrppUg<@bSS_OR?zG&S;{s0Ri{2 -G-y+Et$?QlELE%R{v5BrwV`o^=%=H(yb;u7{x~4bG`dQmaa$eqxB{fRB!2d7;qp!!hh9MN@(tf7Irh5 ->0tGf)#+~76BeK+M0C1z?dCn95<5i+b5KBG-lRl%>MmbRN5%T4rq&RW6F;<%SsfaL^hj<#EEYy+XP=VtdReddj-q+hg;4T+I*ZY+oZmWX}_cW<`V2Ca -~VxlEJc)>H8$pT~46aoPpX&D^VeW?cABh1PvU$uZ53M2be3Rhl&8Y@wg4q>t&T+t?N0Hlh@k0Uw9de( -Q!Al=q|RaxkFVrUDEB_INGD1ORAQ*g-SLm%R&7*xbt0ZswNNG_IqMC`Mf_ZGkCp<^hoK>>BpalA>C(1 -vOCJ9b(zq0jF^wXcJ1A1j`qsLF&P1^N;Q?o3k9@gxbi`okba)Lf)QF_XWXBs0pss+}yb+#ml8Ff*PHU-+K%qj4VDz|A-`Gehq^xN==Ro-?*^{l?rWAWZx<-ERP2N4^T@31Q -Y-O00;mk6qrr`00002000000000o0001RX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQVqs%zaBp&Sb -1z?CX>MtBUtcb8c>@4YO9KQH00008045ZePO^IqgH8ef0I&rB05Sjo0B~t=FJE?LZe(wAFLiQkY-wUM -FK}UFYhh<)b1!0HV{344a&&VqZDDI=W@&6?E^v8uRNIc)Fc5wBSBzY(1T93Zs!A2wJQNiqq^i5xOOg5 -lhD?e}Vk6sWimLY8>svyAY#t!-oH;XdeC$^+em#b*%((b5fy(6Lk8XufO38+8_XnwrPD-}NoEJ#`T|I -xcun97I%b@40C9PSvBnpG#2>?N||9w6&=B*%5zkN4M&-~nQqhKIKaRfDj+A097C}fSe5wKhBdXH% -y$h1bQM1!}8Zgj&1Q%A?Lx!`BDXS9)4mdMeaK7ii%E*e@m)+We&t)jA<c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQb8~E8ZDDj{XkTb=b98Q -DZDlWCUukY>bYEXCaCwcA!A`?442JJ{ieK!|O{#jCc0q6ffy4zKA<7zei)iC2aX0$*r0EziY0`3vV*C -F;+i_jbK@i8Nl!WnKA)N(2hJemUz1Mq=;!Q_r249O~4;LYOLYy8=#JbuPu|#AX3GCYuN8<&;Cn+~OOe -I$p8#pNER|c*}M#A#hmUy_&Y&n))-*;6UDTA -d8wU-K|uvYXWzOvHhWZrXa3ywGqk^oNnJXE!(fluWZAH@Wk3tJ%Du=eG -!dN9LeKD|GAt$wKpuhY5Gmw7#Pf_V))lx@Z%)Fr+oz(*GEbv5@Y>Dk4uv`z5kn=knr@f|c(g!{`OO?_ -(ZAfc8Vp&{@7bS}S^2JJh15ir?1QY-O00;mk6qrt;ALDAX2mk;?761T30001RX>c!Jc4cm4Z*nhna%^ -mAVlyvrVPk7yXJvCQb8~E8ZDDj{XkTb=b98QDZDlWCX>D+9Wo>0{bYXO9Z*DGdd97G&ZyUK0{;pp!kP -&F*$m=-nBe&NXN*6{uaxV!R?3BxPCs^?ioiSKIOB+Eai;;mgc3GtazG!{P91DQP8qw -wx`E^IEKAGN#2^XI>di3u{(%vGq%QKTPpwLee>@y%Gg&mQvH&DSb_sm+#Zza5xz3(3$aaN>@weXrT&8 -_;;;yX~{#=#pOFHR3^2PBwSd^OwNC;q>I{ktqd9=%=x@f^OT5|-j~z;{oG_OjoYcQ*ZYV2mAG{2SH)n -UR;96?tgOGetz%TG6nSoeotDr$mq0!j*$p3&Ie9}b(^t<)X{at0^qnq9dB(1^^P(tl2rd9N2DNowt|l -;D@*!Ma3Z28$(~ILu$)&Wn(jFJVp^g&I?$|&xP(YMo+oBbFhv^SvGLURH3IdHTwv-!sdv+P)0`~$>@F -#GXjl6$*-bV$FJikv&1nR&*g$D(e)uHhkI_Ty||lJCkn -anD_A?BT`N~Yg$z1^;MA3c@`)X#qj3em1_Y06)k3fvPJMn6QFbN9jf3fzh^_G{w(9F?X4cv#quoj@jB -Jnn)DJ~#_YR#p2MX>S{P@?`FQz1JjmjyP8l}ULSs)Vj7P(9FEdVWI9a$Mj>)Z&Xjk*M=RxuwiIB1(e-u_duGu}m?&3(407N7Rv9nhmFiuaZS{E?X&_(U6J?wKM>mwqX^V -tq%y@><5m3QgDyncKn4j%-TyK&6&|t8q#yFoT9gvX&WI5Olc2`W(^aEcwAbK6)b1Q0 -&hELFA~)KTsPI9m!UGLKU*xwLRH>K+whE{j+tBPsX}^K&ooJ8Y-5D_Yv42JMczl>8^REUa;57Y&h^g3 -3EtEGEo@fIX$%~=6cXZb -w_2G>mx)p)wL8bPi=FKH>AY^JlMwDEq`C_Xe#vdcR=xlS}{(bYGaQx2cs9`J05Xa8U2=y(yvD8gr$t*(!T -&5hfn{0cK+ey&GF^Qc+jaBIGD!9TQhT9_hoNpYpr;0OU(RhMNcqXxiBp0EUZu1!DHB&^T3qWgMrcuLw -}>>aMIy5jstl|D59eA9j-eX3707atda|?I~sA^GC%5}#gz(NGn7VoIJ~P -<2Tfy?C&QquS>cZ|=P^xC^!uT^pBQrB#b9S9wpnd}x;vWJE4mYt9 -~qAbvbVRalyD!(F)U(quQ%r`OV>Z(6<#Tut(GO77MQcheZ%m#+_A2a?vIj%KV2t`^ACh2f!F<`bGDcw -LHwZEm}mG%+7XY+VJBLia}mu9snW2rr5S0=z~R(z!-lA;_UbFwpu5*72%iZQ(u8BTm))+1gIu593KYU -MIuY$Dm)E?gTQq7Z*@X=G^;+y-}w388S;<@HGqM|M81M-uch~NUG4NpOWzAp-$U<)m{os?Z=lwpU0`QS0^K18C7pR~{D}JSTOZkajkDO{a);bJ!os(1;E`T($5gRLwHh3ekBI0$}FNPC8a>d{-SEvg5KAY|NeMh6&ZU -Hz=_Bt9lxi}8LN?x1YD@c$07d1uEHT@LZgTZMyRh2K+9{!y&*_=}Yf!)!oI`Pn504ATZ`O-BgFsTa~_ -j{}OW`W@E`e~f$IwnqrCopERJy<8kj<*98lw{+Vc>*(Wg8T0g(|^5`SzTiQW;xZN5&uK5>;mIK^|@4@LO0pVF#U^v*wFc__Mjz -;}oP)h>@6aWAK2mmG&m`+l4;lHE-000yO001Na003}la4%nWWo~3|axZmqY;0*_GcR>?X>2cFUukY>b -YEXCaCvP~!EW0y487+o1niIw2>J&*6h(#=7}6k44?7WxO*cYpOC{-K{rgF==-SDAGT$SqM?Ti%TLVz{ -(h~w0*9K2w?16pL3&IdDzL}q%%)yqYi=~Q9>Dyq%9bDt&b7cbgv#T&yvyr0}(=%2^yg8sbBH`nrPH#P -yzU#0Q=R1AOF?@kgkq-f@oK5)ABa5cC_JnXmTOsk7!NGTolWui@vhh4NVQ$9B9u_8kMe+ODcY=Rqb&Y ->L6;NzmhTx@u?Vs1}dK0roq*y<{?biF<)27(I?)C$2nUMr)*S#YJa$Rm0HTmX@P$o8xz;ih1ImZd&xC -4=Z3)ukd%M->X-?MUYE&5*O;>(^0JAp(F_4^?$)s0&b`qZ*r3Nq1CGw=3}0!PnTd9cI?F)0fR(cm~AD -gTxjRd=!OMp{Ck_7sP<)oKo1$O$He0Jdr3t^LSF%e?*NP_Ufte}dgOpv;$*id@IlAg@Nls9%5Y%~NK& -V8#A>q!V(yImL9Z8jqV*V0z1n-5t&?F*zNjCLWE@{GJq^xpN0z{aHqX{sB-+0|XQR000O8CKQ-X*VSN -XF$Vwu9})lnCIA2caA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFJo_RZe?S1X>V>WaCx0qOK%%D5WeeI4D5 -p>!(Hc9w-Atnn>vlr#5Q83Jru=?+NC5W6uBk2QntUoGkokrQVO&>XeBQB&HJ0-%IGcR`Korc;du^XTW -RBfEv=Td<9YZ$ipy3PU$`5s=uoz2!lnK9`ghjM@k_Y3+C2Gfx5j953A8O=X_Fhy%6K#@v~or))A5{D6 -<2Gac+S;M7_CO5QOQ>zY>rzMFPzXSFO9~1<%E`|Gk|k=l*}Ra_B&hgLaanF0{onvo%!cSj#eoPb5nB= -D{vbQ9UOu~L=5|+3}YRk)PY6&HRF2EF`mbw%9mVNwLh{&F7DATI2=j#sq-D$Pl -?jRAi%`d#Riy#WdqUM)9etO0_@kEX}TbtA0+@Gv!z6T?>7QrH{5X8a&!_1H%1wptCSOmG6s1(Aqy~SV -#{^yUO&s81vl}p1+V_49R8=Q--W+)#h%YD}fI36%`6Is~Ql>r^jRCHn{CY_5)al8Ny7780b91 -OaqQvEaqA`p21C7B8t=K0Si{F6(cGj0a{_wECO_{E0Sv$8Nxg$4y-H%tuP5&U6K_nDdslJ!5+8lw7_~ -2s`R<2;qGyF4VIf7Hx^+^VGE7FQ9Nq>^Ziq*QQKI@w@_+s)!1Q%?|3+y%x6-#Ci4tYU^tMcy7Zr*W`< -kaMJGBZ|Kw_|DZ8R{UZy*=Xy6{zRkG#bWyJ@&F!@wtO`3U9f^l4{#B^bpf95x -Z@`9k&T2C-(yzvfrX;>_p9GD^KwW`a;4>^rDXl;8@I)d|d;c#Q*YURh(W!{^9fc=)rFR*|#W=ecZZj -GGF`jm`=!L&dn~bRN{h#B=U&i1oO6wry96q(_vSSii)YA=HBM{LkB=Af@yG -9f_*q+ka*^MrAuHVx3vWNGA-o`t=nxr|_mFS4hHH^1gj^N+VTA9{gctFXZ}6;exOw9}VRFonuv0pdG` -+MvAlKy21NJZWvv@p_x0m;9g#Ne&pb5-GO=!1!6!L{-9J6vz6n!NOXbw0J%Vqr6*qp6XP)#dX9g$JRb;uqebm%FEhffl+d#`&oyfcU+j(;8yPK!{?(w(l -&Pyi_1RY1c)D4?b3^&Z?qS{@1hU?sR@_H3VH)OHcB2}~?R8e+#@o2d$3Bk2E^U~@$O~w#L<8~gTp@I4 -y85Yg3-bJi%HAcE2Fm&f%wRSAdD-liA+a>lJy=u?-Vb!b4{k%X#1)pgqz(61rQIZcWO*%OFr{JH5UPH -~1C8kuI<5kRB*`((5#JRt<*xA+98k<|aMAq0|)kfkd{<3`vYin+=p1*o^og#O9CC>0N+$~x(c&Wcmri -X>ncb`ahX34$%+fnQCRKqR?vWT~bJ)>L3wh|I+A@%0#3Y2n#)xS>}C?VXlBv3uP52eQ;qkqU?ULBJ14vELVBk|aS`@Ot+3Y#ag#Q3gO9KQ -H00008045ZePV_8$F#G@j0Pz6;03iSX0B~t=FJE?LZe(wAFLiQkY-wUMFLiWjY%g(jWp!mPaCvP|%}T -^D5WeRr2KLeg30l0kuvbqjD0>i3r8JXeFwLZ$M#ZN$NxQVDfk62F3H5=;2|9KTyNTF~vLpAPb!fZy_J -RU5ON73DQ#xU~=Z{r1M2x5u*(}%3Z}mYzIy-pD1khE81}u+BWDwBWR4u?Bp3d+}-tebrIAhjJa_#E+L -6@ARWe#fvn&_jTX?ix%VKd{Y&GXP6;PL+UPAS#7Rs>FDx$srlsLOjl&ogMDAx~2!=B0R;d6c@6*oBLDyZaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsZb!BsOE^v9JS8Z -?GHW2=I{SWHjYb?-ijNvv6c6o)B>b(c!MT!`x0#{i -}xv(PHJUa$xq7QPO40-3w)!q{h2Y^1i{Ja>Dh;?`<;g2w?g{j*w|0Dz|A -=N~7=h$%EreTDB%hMY^fqoUc4u7>54u#|>GYR?b4M4bYQlgd-+k@b3z(gNAw>c=1&!jMzeN$ExFYp2P -L^m8tOhn(WFl3sI_-B43b?S6*f5RiXxS3_0;ck9#T`@lSx*d-HUBm;A$0gSg&8dK3dKE#Hen -=H^Oa&diqQJESz@`Uk4=-tsoi{#R8%}ANZ~Zh`t@?Fl``AWcXfvzijUrcvKDFM%|!6r7rfhzL5Fx!lX -NCrLs<_6sJTM9NzhA)VVnF-2oQg(dR0k08C`K6EC8e3wNqMAG~Andnl6*-$^&__~U^W|01OcOwju4Pz -UIM1|epQUba&|)LJ|BLH4lvFn>CLL&ew|GOVx~uf#iG3WC*Lw_1oE?@o{&AW$>_ -k!;E17-f>B&PU}P&v{KdOr?(K+T}*12u?2T1Ql?(ads-F3Ba~mfA`tKU>q9K*Lwt$ylYnBcPR{H1DRx -0=D`Nfl+ugxs7lnM+-_lZ!T#rn4PrLOm-O2z!A>jwsb6O$XIHd16*#M_1?sp6pt8iK(ytjm3F|BzxPA -E<|IshU_sE8C5=OR*EDyL!=j-6kA-AoV!d*lkcTWj}{?;|)KNJ>dg}3^u$`deCi?8V -_B{zmtcVaWV|3f0zaNtAG@Z6H;{nuBm)5ax081_Vwz)L2s8cgPEkXpzTc%5RozxC#xEx_pW_M{#X|B7 -MnG-PC#3gyZ~EDaJ(I>Q)V)q9^pfqe8iZ=`7crCgdQiLx8F;=nkE#Vwh7g>txkN9Vcx94kZ656S?TAG -WMYTgDgWzSG=d89@P7xh@I(RfC(vnH845_9UbXCSkq#ej@kf3_pAmJO>-aw9(C*`KP@}Ci?5;t24vPF -S)}Is~47!SMNTbHmTkvDd!`4vGKCAq4CytshoXR!T-$hYj!>_Y$TQMS~YYrUhHtvhZNQIm))VM*?dQY -4B6B~!{#OwD-Fc1C#P)h>@6aWAK2mmG&m`>I^X>l|J000^a001Na003}la4%nWWo~3|axZmqY;0*_Gc -R>?X>2cYWpi+EZgXWWaCxm(OK;;g5WeeI44i`{z!8e((y9m+!Pc#F*fuEAVsA1njVv}2sU;~p#-jhd! --pPzV1WYl#g@n!&dm3qD3jumfgk2=--J#FOy^$<`S)8$3O -QBbgUj}6~NLm<8v*cmRx7_dQBqIzLUbFp75B~=tfC04+7UfEPqV;Rfc-$@n$eHM#_`~FyC@uuNeqp48 -=gFQc>fQmPG-|#mC3c?YCKA-)()_5IgBoy|HbUfDmFzPe~-uW_M%XKI;*-|E@%?h -}Hf@y@8lc}Y6fKs9sO9L7WZ2j(SI?MFy2A;L<5$=Bc{)h3sLIOnCn$qbEx9Ud;PsS*|BZ%DWrqVj5^* -1U$a0WCukX6MC&YG$J`_TW9ozh!n_#*y?5_sh+7;$1yNMRTkgl8oQQz0F;*U;3=%+l@AXsWP58g~g&P -+c|awWS%vnC?^9yuQBtfx08!VREyps@n5Xz{hoO7r(nAVEqzKfs9I`gWT`(T@Ntm@ -rc*LhCa;|m^3F>k_K*64_D^o_jQ33gVJFn=fUrYSf$oa}wXXW6Ie|@R>2Jf=??$nL;!zZ`7gQfj3~k{ -WoJ1`m3U%KYq6UOoh7*E>eixMWC>6<%ChYkQ59j&{5n0f;w(~fk;#DA3W=ElP*&dR?AF0CUNql6eHh~#7~8oiIypkR$q*SFqO3oYs-~WjQ1njMch3#l9e}OX6s{| -3+#nwXkIeID0CDd4Q3XUohyDk%Nj>vdSSBs(Er? -1?tcimerNiXWK4L7kA3b=0!_;L`cu1lrJQ8kPs}W_<7q_AiD>GF!<@!;BU7y(R&jQz!OF%%mMR1&_p$L=6Hg9(u -&%e)jI}z44IaOset7ByhL4kKgU??VdJev1f6-Y3imFam>>Fp(ty{S5=WUwTR{SBlWmW_d)T8pwSip&hC`FnA@OEW`J?0K5$FQqt=^P))Ht;NBk9UL+ExhV1+7 -F_^SMN`kqvM9&&;s<_rCF=6tdGxj{>H@fQ9$g8Zbmv})>HsSV=h0<;!?QGb1>*%g3N>E`zJAZQ!f;ik -cSlD@pcgjN+KINJehnW*8N@MaB#uKE^N79RHD^^_HfvNo+Y}{8d?-aF@*3u`dMDUk)I7m)#Rgvfrx8_ -M!3&PcshX^2Y0fx&21%o%D>>j*TGd5)4;)zi2P6OGXw2Rn1Myvv9FN&?Q)c*|K!J~?#GmnVT-Cg8Dx^ -?~BrU-paaOE3+Uxii6n35Qs$yi2pjiDI282^u<@oqmJ_h*Yn{?ZhG%Gryw{yPUN#Pi;Yq3An>^99Zwk -`#)1qhytTQyx$h1d0tH3v|cNEnz5L>{xMAZnmbScd0pC0NBbBD)9LO(hbw8WKvulxj#KHfau2cm)f=7 -mn&iBXM>B3{R&=Mo*=hIa4C8B`Rmn#_)!l5heJZcBbR00Xd{rUcgM^1rYtvUGLk_@2 -n~DofVnnW(BG7XrMr;*61;)b^SK)2P@rZ%8~t)UZ(U)M9aZ;3Wr#nS+lkYMxDv3Ksa4<;{L2O6e8k!g -axTmKC=VTYyuo7&wx(NJPFCWNKEx>Lu*5Cak)34ZBc+W6Ty*Ug^M6L{9No|`-!|tSK0l^F5T|rK81IN7GD!JLSvJQy8~<-FpfyJ -f;3qXGyJAV(PFSoA+RVvkrle{dhi^}CD|)3^4M%`HKA) -lrHcHr*v0%nydhAbeBljSTVbtsE9VBTHn^RZ-_)Y?RCLRI9Vz7iM)9lTb>#3pzX$!m)~Ep3Ct|Y!%l!_leNuV}rydRHSEL2->0tAH#9pWS)b6j&u -8>muXk6H*hxj^B<#;|I=2KlKdugtpHq( -;9ZCjwv_wbLt-c)DK!Ua$1S)=;i+!V>ov?YWR^?$HU>kkMFy#vVfhcj6Cj?nB~!f^c6&|hOua>bKm*~ -gMBkc71jq>tw!=y#oZus>UD0GopI>@@uTzLI4lHs?u9PArtxEYC)Eb5r*hA!A3UFSA4m^ZJy2d>j?15 -SeVcaIZghtw^kk~R3QGkRM695MN($Jzos9+IBAf{kbNFvMWk|KEYV*c{{r`L=4hxy{wn-}rVuPzt!*O -%9e9s_U(fjU-M=@2}qrUA{jEj)F0UL|!8xD?=PrQ%)0wG~f9biPjpaCb320~FxCPbre58_(M*lhP27s -11qb(#GFBQ8Sera6zo+KL}kH4}?wlXR10e!$q)VD(9 -`j}8T&b|_WHjt;hH*|u10s3$wgn+Z|4XSJpCnnS$(!58{XJ!-Ia8P92&`BU<-vhW*rYKXnDih*>rg9T -k{Q2NB|B*dr!Bh4aet)p(9A$gjt9)c9>{RE>1Zm<1vn**^Us0&!Npy-D2UScrO}?HkBjRW2@MfrCdi8 -D+O~6-8qNnuxEBGBn(Jw}!iWAK1B#B&LCIIx= -H9mV+BVN*75XH_VZ3)B0AP^cT-~>E>r7s#oZ24@EWso+{H)_%6y>&? -tm=l<{|b~7-H8DyE9(G-sCx*`HQ}A28SRgVIy%}cPwSd?U{lE=x}e7KG2lf(U{N=?q?|mc>^8hF#R!P -RtONIPhVWBIMt@}S+@j76ioIh7$i-J^(12}PDdv|9q46>4hxQD5?pD%hKv^C#4;k{$gy% -wNW3T$51UR9Dc0+zgvBCRUXT;PYv62PO_nP3A<(ayEc;9f+Qotg`QMn_PQ0Tsk+qqEsKgWvmxY3pbMj -JhIK~ps`P%}7A>{3^tNG&UAMyFi#r!J1UYsv}x{fd2yqI4v(fW|X4+JE6yi+_@-d=J7ABasy$Bp8ya! -!U+`||Q=^fwl07H4^qVi^d4&Al{JV$+^mAe+HK=n5oVIaDO=96ZuhBPrWZ5N9)F@>e)ziaK~_h>Tv%& -tG7jK$Q#L{zU)1n7^Jc=J@H&+r{OZAFuJ_;_CcjKKex$ECzJ#FZ9(ZB*b4m{btObP0sXG&EwVl#pTuf -Vi6+~^Q-Il`D<{HU2aF`4aAu;{goC=gv0|;Vqj0r&;gImouV;Ns -&r<1Whdd@Hmt2&kbsFG;;RboD`gURsXqCy>9{*1Q~58H+n6a8AGwa+-FB43%HW1P=qFQ8WHVvLCjN`I6< -`F-KWejSUUyEP4!afzhH>W+Fp0Y%^k=rw1~c1_{0XfDY1RT)i}4--JI@BD7!$Of_``RmG;e}oDMMvj( -qWX6T8Cb?i3|akhzz~7&LbDM%5OY={QfqyK%tm|;(~*(0}m$B!&s>M8%lc}7w^jZ7>qkE$ODw|MGlI5 ->K$*?iyD7U$1U|@)SuIGQ9V!_KfHBj{eT;o<%->U=+Dre&IMbG=CCjO1Y``Eu!Vw1_FDn~h}#jte(j1Hj=~ZAvq>L`?tcE&NW8 -5Z)*~6HD2KswQnL4g-hQj`hEz`hI&{e9L?vx0(Q6g5&HaNa0ayaf`F+r1|0?3qoQL>Z2!0%(nPR?o_B -slM^d)bOQ*4qY?83`5Nq%XltO~%QDXw=5^Mk$TYO|`%(Xj&rfrm~pe5;LD4v7g~E$7SVL7o%j=3Mm6L -sb~+!ZCLKc6q75R76+HxyeMs=T7|Y0)Twf5vj|;^CUcZGLvKNaJt7L6ASrD7`hFeqM7+6PEH(G1#(_HfGB(-CIxzIugoC>%@ClIQ5o5S_aq`vC@@YcAP?M2&+ywK((B=9IjSdkHy(mx^ -|j~R3cjE^W?!(u#qG+N_2QXj|DJYxo276-RStm@Nr0w@o>$ewzh#pll~0b^SX1wOkJJYyd9ZD{rthh( -DO61uj>@ib(%e?-%z4$SL)Ir}hBV$1*(qH+WRAmDGH>S&MJSYufZT{MYLn -DFB2`))}+zlfIPk&WM$%E7}QEEObR-(I_DBP1WR$06KRJTeV%qW_o9-=vK|aRa?+=JTXeD_s5x!*enP -1Tv1=+OyTw>@6~^yJ0UA_&pQ^OF%*0hsb5ybA(!muL_jK{Hb#lNYLe{Y9Vu-qtqpNl(l`h6$4UG!X++ -;uM`qG%CHt-TdjV@8bns~p7XIhg{t63o0MSiSmBUes2uOlrP-6`Hjt`=KcNQ&KZ5}7#E8UStVccH6j( -X1W^DuLji#aa+_un?zJZ0`Zc_^6-(pId;?_g1jtb}L3$JL72yc}vw$|DV@x!#)9zjb_2SASvDH=ybKI -+nv_Bn5^zbnGTn)NMAxXlLb!4eYf#ia^mssw@FB%P_4G7agVnYsQL`6msHCKAxuTwLRsEvXlrF9wg5$ -SlL52IwEiqHgU+=<|88N}5?Y-+`a5-Nh5Pk!kGl9SfdQ=ex+QUgRAk*q3!#*fQM>FH@WXXMqNL*;T9Mv}{xR@mBlt7)t3FRN>tR-f6j)xwsZCYO~R0sPLfZjb#Ja9J=^@)r(a2_YiAVBdD= -Y=mK~Xk2#cg%q_cI1C_%+wZ;5a~=jxwU(%VqAcgyL;ti8N_2ugHi!BPt}6l6nPywxN%6p7oGfez+)!h#8&!93s*|(qJGVrMA5ytG_QJC%#dQL ->O0h%&%8UiN&8q2f(P;Y|8cr#Xw#1h>4eONcvKB$}6spg$YTIiyO%G)F{>h -(%~R0G_P{e9ZBtvc)dr&u#_7V2DsHB&Gk=_~TfHm7rQua)!&469E)eO@=DkKIdT_(OFxbAv_q-+85Tn -FKvB2JlUjW$yjWsT2}bg^E{cFC@HDUk}GSzcE7lRm$P<_K3Z3uJ}@qQ)Nsi^6-pI)4a%6KfSC{EcVdn -fC{8EGE~U(VC9_tblb?X=`wHl)+NZ+af)7_%-E?VXq!#$&UdyLHJgkd=qo>Zp24QO+P~oy-kIw!?ApA -cOWdO-f1ek(`KUD+UU_xQZf$JVI^%*4B+QJf?oF$us@v97d8^!p-?g)Rt*HKX+3&QgI~@X(w@SYaU7P -uqr1?G4JIQvx+?dA>2uJYwO>a`qYWIn|HOEKnQpYFs)GHy`EjFg4pb(d(@9_fFv&jTpVT(j>md43D% -Cn7kB@rKY+qkZ(yg$r}zMua%zq-7L=T}#6uDaIgvwZvZuQS2(S`V7ks#J~cn0A=w>$mfZ%a@wzNcNzD -hnGmitR+C$neb^7+t#z!iT)B#v+f8=ix_tm4&*fbV3@H0^RfC?;d;p%0I%pe_bv~Hf;G<)t4_;F6(b(#PlL -veZN8y9m2WDrscP!5w)Ss4rIr$Q_w$iO$=C^tSKy!M-GxcX3aBe-hIgP#nop|@Y&EM*sg-8DbP)h>@6 -aWAK2mmG&m`?N|IJ5o{0049|001BW003}la4%nWWo~3|axZmqY;0*_GcR>?X>2cZb8KHOaCyyJZFAem -k^atKF(*|W$t(%UqV43EQl%K0KB>x>MDxz+^G)6>( -_FHiT(uJUZd!f>@Kcew~dCO6wGFBxAJS+XldsGmFLwaD`{vk!Zor!svk?DKlVBm29^qHm(?bXLTEDN| -lVQd;I~@tnxjVZe5&`KjsFyjUl4X~{P6y8$cK{EhnW#k=>0T@Z-pNNh`)rG=%P&p*xbJj(~{IxR$mwc -I7VTxI#@M=j-m&2O$h%c8(<^U8AD^5`3X6pmLRpF5onI8L*)6wf8QWH(tVI#-i<2qt*^$df{JuIJ(1* -W0_<4M_c$^V#&v{p{g>ax=fXy?=0@I;ZS2-xjP~3zmrVv0Srk#W;&4CNEi;;rBL!gv&%IU9iu`q%bOZ<1U|q>COOuw{R&MxUIVsDG!Ll$HC;2vX2G#9wwivFnm4Q+#=fb;*qgI%?4V`;hMhlPje(HFwvc``LVceS7ob953;}PMkl -#IahxM_<5m!dSg89ida!}$dT_3Gg8fx94lC=;_=*m -Z(+jU`u-UYckfxK|wkuhL``vkq`mBf}0-%B`8$`XyfGG$Y&P3F7zYCmn%*{)6J+_SWt1R&ImvNu)6h) -9kb3*>R*4ot#9yaKNCV(sM(#2#_G6Q=o}Nj|jBa77aT3Ltn90`X`7*l*OXgRcWc)uhAM;b`k}08=9YL -hQ1cD#o9<)Hth_Ht%&4GMq^4I+6?}or5Q2&ts!%xkb1v#+_3P7_xO)Zj15R*)mw<51^H@-N9v-lk;a)6LG6Qfw`@fpwjy7Vr$K!HIx*=w7tw+BK -3vVtW>fsPKkyl;S1f~|jDFAtEJaxwPP7Lw(b@=}9@u0yAO83Bv&v=DHOJqJbIrtlo&It90y7!}eoUJI -Hmb(ma#qY7>!pY|`7mi2%MwE#MIwO$OA*r$MnNRoY_e1{i)FzHj@*3t^oj8uygkJ}DRUaI&mZ2g&y#7 -Pr2jCVD>L_1)F%ehwe(j=RC1Gnw{&QYPQUF1;BINn&6Vl6pA5nv8z;*5p -F-eR?&)-;49%-R-BZZ${_uhzbMhRK(@Y#qTT^?>p_#5BQ8PwO5v}eR)LHLSuP(?2lep8c0*8u}}B&Nn -1DpRzB73xMp2Cp!v$qy|m~n0TTePZF&~^ev=-n(~csIQ@tY -6Qf26H_#s?%OVLJB5GmE!${;nsitZ;BFIeTLKyLQG={EZB3>N*O&nhRZsMc;UfYKx=5D0ESEwLEOuM+21G{St1N-{82c_x`t_w1$j(Odc0uYXRC%R!5pJJ+$~spMgd46F&gqQkOx -Nhmye*KYFO?}bCbk88KtFm%8tG1e)po*<8rs{fBKZ@S38e!AQITnY;;<3jV_pUdYF|XXvXYNT3n*Zz6 -5(4o^){EtI)Jp4JvRKnbY^PdMY|1qRYSol6_NHJRLm<#6tJ(^PNUQ9W6t5Es+3N}9vjxe&cqR_JWaR) -eUT-A?*yU)_OS&#k#<{!vq;7)I6%~iktX9T0&>jGM0$oO2`qHR<0s16@o-o0N1>ol3%gSSId-F3YQY! -U8#TH*p8W=HkA>?G)Mhh&+8x&%_;}W?b$Uzbf<_CeaZg8To?qY^=Hf4o){C?)eOyONHREXPaCo6ZJ4&wrE1>znp8y8E2CB!zWXh);1Ro5 -W1z{QXo_Zbl-Jm)r-YhebbWh7BvA)qBvzg3PdC*NTjOt>_d@=Foc-5htb9 -F**?o*1k~$0T&R$^9gTvf>O#GVmsRJ~pDeJv&7{h`T-V92SJ`7f(C>SO)aazOv()qW434GgzoqhBOzN?Ak^NZhca!{0t!=CW%2Z(F6+9X1YV;nz5=;q@mjwv#U84Mh-^p7zn_ZOC)xw(w?5QH_zC? -xhleHsQ~=+O@%l_|_<*FZcZR+8E>t^U9<9Q&j#et`JQm;uK3yeIm;F -$2>^Fs`-mc$`c9aco@bc)3{*cIT;@ExuaYZ&8#{VwrbaMT3Dve;Jr3el9qwuM!WKYAkrm-s2&#(3PO@ -U|!8rb1gL)d^xlDH^U=+?6eD&+IjIVP7v+Fp=hUvnJ|i!!MJpw9!LxrTnD&V -`{i!9GKM`Mt1d!h2{ZX249bYw?72z8t_659APi#0_7k*fBU;csJDoQ=m-snzUB2NufjN||^EC7~M;xr0G^9lT%!{U*ID{r~fl{hu1 -?oknPRzB&mj%~#9tbi;bZZdsJFgpk5U0gl -g~}>%n5BMXcpo1wjiA5?j2_>7K6sroMF4Z0+m8kZmqY*0fiDkbAJP~JTc-(B?+0;`xpx2u8jF+Gct-N7)78e_L^t(2;AA^C7ezimJh(o15g0FU -kK>UCrE$|EVFjOQ?GY{RV!#;h9Ld|W>&+jB!TTv<15tv-Y=rFE&?J}qhCPyi+t-*MzBP|gNUPuBi0t`Vwc2d@f3RFL8jsQ%Z0&o!SzJd>RN@;HV#w+Ez -t)BB8emgvUcTOJ9|gYtQD-7~P*lwJX)SII5GSK#7n2RI{K|~>g(oQDQ_wvLkC~MU(P*Bz!yu{c{wTe@ -(oeVW$@sDcPYrCMIxVgX?ct`z%bP&=7Fz)`fgV6{pnGQ<_*xM{;eZ7a}J{q&T4Y -jrZdo};;aIVD>$355AP0S1^cz2BYS!~R;><|4tzM+NdI*aY8f)6iyUN?t}8xJa|>I?yM4DcXO%vi5W} -ph;y)ob9&V_2J61mE|aHjCnGDr2NuE0^#$^@ZA3Hroysp|tM0nvu8 -n<1$t83iqm+sD1t9G0_<-+E*no;dujVJ628|d2bKDR0X+5_QR)w-h~^1ZZk5&X7ZW!Ia3UM)@6rA8x7P_U0MVVJ -?kY?sD*GF=X!o>8t3lH0lZ;4#oxSldBKcMatePsoOMX+k%V=z0=mA03kDmMTGkTrPK|Xs*$4|9jRr3P -#FWmg115ac#NSA`$4BE-&?z!R+RCcJuJvvqsTBR-Aexl2(MEHq;f_>TIPACHSU{&{AgV#1+eRXjP51t ->(Rew_%MxN))~Og7s|eqt{L~`*8((EQ;4bwz+!TKFm9LK0b4{AR8fmFYGP1Mm|z~!IWL!*_&^)+q$ep -ZZZrook$fpG$DZWvlKd3@F>c11%RNP`;XJnySMLw>viK}6uhZ=^?ESf>!ZWC>Y5vtsq-LxatERecBaw --4KhsJyz0~wV|aSl^8*UUovP`*WwfOKz$&%ZYte$;b7h?LAdAplSZfaG*Ub%c9bZ!Rx^9@$-ryweugu -h2_ER}T+&b5*KSuYgL8rGOm}d>E*mFc!gj`sNzDA;Ig%3f=28Hn_O&*$ -noDu8cJgyva{gdf1v!5j5H9JDctlyIv$yy+Xl?(``>!AZ33wPlJh<|NTP-%~G8==@ti_decqb)|2lBP -kRvlFFW=A7oK>(dfI(_U*CS|*ITkL7@R!=s=0Je)&p{cyu3Lm0G3%xN$s)X98$eXXi$Z22< -r(2rNixvKx*-04P)h>@6aWAK2mmG&m` -+tv&LDFO0012(001KZ003}la4%nWWo~3|axZmqY;0*_GcR>?X>2caX>Db1b#yLpdDU85bKAxhe%G(qk -wznO27%PLZdx?z#I+UIGfwOo%jqMz1Q+DiAQr_gM6(b5?LFu01-k?&%V|5)>LDhH-E%+Rxgb@ic5Jbz -dee7ev0!r7s?IRJ(yH!_SlI7p7wmp3*jiM)uMN|x{3HxBawk-8GS+LcW~&3+8q?Nt#f)OIS<8|e!OWH -$)+m3N?YRbOx)r6Yq`hWMcY-WlzkT=T7jGA@?tXmn@$LQM{>@K!?>^q2xyMNL%-+j$@wp -VOk*d-D_Pu5L!GI)pU8lM;;QfMW(HPl?nmzEk7he*0T3EOOwEC_Fg`b@*UOCg;GPU}fD9vnshP*y%L8 -S7XYLropESanrf8es_t6Jn|W#4rm%>oa><&6MQO*g}@89NWR&NE8}|AUmvnrrRVn^~6g;*7!H`T4njz -)ICra?^Jlt&*S7bN5WMk~eHcP6b=687Rtwhbz^=w^pgTDC{--^G3N1oPj|DtZ%=~Z!*>izP|k~znO;% -7!Yv!J<$1yH+&h*Yv0TQ|HEMC%`gq)#A3y$EHXT$NSYpH>xq{>b5e=`4z~p`_K}oe+K^R$EJGmHd)+CiD33=Ioej -wr>>0JCQklRZz3fM~{!YDdN$x(~Spi3ELd&0&Tv1?7v-Zc1opd=n3vusAMl-WW+`y1YrC?L1`Fy4tF( -=$d1fC7%9|B*{5mEHpKfS3V6oIbn-Y!vH!@TtQL@;Um=Ju1wh70$?LFac?kv8q1oeHvt^H5%B;A+jn5;L371)UzP%-XYY2>#%>a~R!g5_v2(l`j2g(Hl@1# -C`5PH)JGFxJIItx?-PG_cqUUJH##M*BJp^jsyCB|f;8(K+oKxNAy0o9KD_M){-`zZ0{dWEQ56`}P{`> -2J^#~kkq2^UU8@ZSAf&DY2yLVhK?*e8*l;@Wo_EQU{D)EYRnA)pvlpU4E?Shmkq;tDdB -`aRaL8>XtXv>FAPC80T|D?R@y;WDjdRxLr*&qa@PFvk#Chb8$32(lfXGI~2n_}9z$#uE)mhBvRYeJ~6 -&@^7Z=z>h|A2<<1himHAO{5ga)19mcrLgNx5Sk;J;YXwjY5<)j34go{a+ySiu -In`JvTJy1Y@TCii5PHVikS4y;k{+@t>wY~%;8t$7?B)B94D1G&NMH4yw-z2DCN>oGebZ101b@KEJv@- -AljZ1&lS!4$kR| -kWxv^PS-OHHS(@X}u6^`0Mef)*v$P|&fjb-b81V96cYPgug>(hjmiRt9kkKua}i5T1{!KN#rS8-Kv2E -E%$s8%jp3>A+O=5LB@N>7?PG2CHf036|f<1_l`_vfi?B09EXu`i^ZB9F_J|kA4DeAfG*XULhEA3m?Ii -d+guJ8lod<5;6j2=sY`y5sN|YG-Bs_Zb}H|a}eGlX~d6d$b-o1#5(s)y_NZ*i02pE_`}f_VuXI&B+af -544QN3{?Qx-jp3;Y>&!tpOos@Si^WWfy2@-kyOz^8pnm*uO{o7mFCuT$Rle{|!L9oaa>+G}UB1!u9WA -t`?$g!$hy+>fG(m-lWHHmHr)Z)rNN_5`U-!)Mo@<=xqPu`pcXF{S!%;9vPTwa=%$Y!XhMV7UG(@1$Iu -?SJm1Y(ZH7rS&K!VhLVuLwnu`vBp5Cpbg%RsQD;ROf)qA(Os-$I15Rf4)-vWV2gJRtlF?PP -quZz%8#n@t89M3{)$snb7e4bhGt%el= -=^inAwy-g~xUc&eosHAFGab4%!SqEaot!M_uXV_}iXpFI8BQZQS%mE2hCzB+_s^ndR}0aE0zhjFD^u!Tn*VCN56=m -^vLM6b@mmM2y^TX}jcJuEksWbkB=_)I)@qGptow;v#)$zd}78*d%3l*5HOIbW>qs3vbEoD~>Ve5ScI2 -cO>KB9QwhG6*vKhLUKHGEk|9!R1Dc2$aLt$DV(?`3BsfBL$0F-^utqz!a=4{PrH*A9FF%L$&OtOFMJt -*$u5#V3I&3GCl_af38~^_=9KD;%k>7+KA<0+qjvV?ezN)3(;tNmcy#XSN!tCxhAVm664-@2jNg23m3!;`3XE{d$yC9KT89^*ZU-}{NMQKR0^KHYL}`{;*uPl;&Qa@x -WHF5_<~^arSQl`QE=r6ov*2a6j?ExC9mEP)nvaiKiRd8j1+Gd>Jb3PmVToDCLz?R -@Ko_*ne<~7fK2u;P)h>@6aWAK2mmG&m`=kUZ4nF!0037R0018V003}la4%nWWo~3|axZmqY;0*_GcR> -?X>2cba%?Ved8JwVZ`(K${@s5CQKOKJx=J6wqGw#1J~q8A(Cv29^bWWn=h6~w6Qx8gNhS6E_4f@)$&~ -Fh_b|{{rsmC=Z-(s3=!z{CtJbwfEEY^wjW&+)rPZo+ViA6iRzy~4r9|PRu5CwG^GcMiZIl=VnmiZZ3( -*XSKInqe!Y6L5I5$R{(P-2fwcySf%TC!NVYc+JGJIW$+GW?H( -U|>1FkP!HyXCcGPP1B@iYxi2U{GY)t#lj4Yxc|K<)_K#DGT~?Fygh6%jZniD*tg=3p-oso;n$ -qK(TIxMh|W!3myF`hIF$7f^g=Ehj6_WUm|e#!^#EEZO2XO -U-!6oNu)rC4FT7PpJVq)>JijX9;b%^0Oad?d@_BPB)m$U6crE&cdZiDX^2_@6Pz^pPts!Mk2-=YQnZa -pNpy!6ZA$5^DN*@>7Nv?2-SG$_lm*8DelxyQEY4Rv37nu!W$*gh(rEb}YY!ay`?j|2mKvGr+bDmXTvK -hR{y)BXJHP4$%#P4KY0o=N%s(6{XmQ1CHk~y3#kIoaVd8oPXyP`p%rq=cB< -^zI^$TeL?p2hJ{8pKRH=zy;dSeRi0E_D&XNEV@}~uSMOv#+lJpj0h2*b -Q$QQHKl=RpUBDvTTARf5L{9*SDr0!Hy!>iDZvhhA3sJ0P^u(c25N6R|8g$E%&i{L`ax?KHtZb%{@vwG -dXZLK=(cT`wjeFY*ssL4zQds_uG6g|CsnY@*~Ku(%A^!p=a*_Vv41neniB!LDO8Uq_J*-7GDY_E4w#S -{td%gO|z}f7Z3TEBc&6DFBKqi71(HjrUa6R+1lff7mSG#;2G_UggLti_Mp8)mWF01FP%6m?vPkz!Vx@ -tzw(jtB4N`4u;7aY!}T)JoP1S$+qakhXrwlctL#ucY}h2u -TKE&usIRtjkF`C?tCISN3F>$%UpPNGk^8O=ku6j5Jca4b1VyP{ht={5^QnnO+7B$E9~m1M|Al*hN2x= -+>|1k?k7kebtG0#GcNv(5sFWwQGp@cSj|)G+;;RJaCk3?hkzE_2_V4lY=sjU6?x`i`6RNo0o+72R|!6 -ECc~GnQ6*w7mlzwdnbh|1F&FXxBVW+77c$rOWx(ZlwPD<2w4U<^k&E@$}Z -Ubbn~ec0AeP;Vp}DX4KB7cWK{=K!ZN6GdU(6(TLe0IV}EA*9mGE^=s}`bNn>w`Lo#kPYG92-za(f16u -t?Dmua-p$lCK^mNNGu^GkVSZZCV#V&CLSA{(f#ax3!1 -1~)B~8X$sqHMNCmDiz5f3*vr3Ok_B-b_>Ign?%akAnn3PM+S`)TVoy3YF|rSycgyJ{S>a~eh{0=XpsKqL;7Ui>eoVNKTYsp3^*puo#}q!A#5Dj-;H=-R>R-icY}$ip0TIXPS`sc -dmTZ$M2)l8a6=n9#a;>nVBs#_#f;e`R+G~jwJn8P<=U)IHm*`9W>wJD$=HU=w#U!&XCZYw&XBK**xCu -Plh)+lUu7r0$Dfza -qULQ+@nX~W>Wi>-;leKiQ}`%gDNbsCA5QXAh_Pp;W1rm3rJr##Hy2b*;rnqqm*rUf&8q|I8+M&F?*7bc -vx8wykH7aP9};$Ur?3ulQ%Prur2oq`Kc20H^Yw -IOI{TS!%8>%1^mpTj7vQkkI>F1~B;5>dLHdk|rZ#!(R&==+XPhSQ?l$S!|9{E90Z>Z=1QY-O00;mk6q -rtKr?zmu2LJ#Y6#xJr0001RX>c!Jc4cm4Z*nhna%^mAVlyvwbZKlacVTICE^v9(SX*x!xfOobuVCc|% -c-om4HgTeQ5PwYMT6biLE=Ro7J)#`oRL_~kh3JmV*&l~`yF0JBU#R-Z>^A67xH{O_w#X7(;DjqS*y*Y -vZfJLrweaP?SzVJt+ZardXm06s;Th9x4oNqyMH|qcm;W{8p@)L(Z(CCiU~ay)Oztg{Qk~bW1kTTpQRg -#PF5>f*Q=FSieIO<=Cj=2AnJaYTwu%nmMoOVf8QJ7_boYLD)IKidnfj$6NS{Gb0nZq08n}}!g -wwG7Hd)$dtf!DB#SjKJyl&DL29)lL9MClm%HCO>S!*cE`yw@u|ICF2^J3fffYmclI0exTmlYZcMI{4 -&a-!ouJ?i-h}{Hja8~xW6b@diDd{03{sm!RF@9lfyAsHNY;wpB6EPRJli|!x$V+$oHHfOe3AMesx;cgQ$6%GIm{6sx4F*)z2_8-mq#9+GdXHrs36HEZN+DJlhjmgEYnT5qh2In -o*hW8t=_rBET}m3jmopB5a6lasb=Yip^ZV{gE;&q|l3#||IL1321b&>570;x$R<#fQD$0#b~CvVdddmA^u=bv|fu>BQYnhukxx;|Awtz>v#+s~mgwSy%o|w6#>DNcG0<91PbKP5NYBkIrGc -$LSC&l5=<82Yac;YTA@MBR<2b+8H(o=ZiHbD4aVSiQ@}7>}Qub(-0@s+L|5G2ySSU<5E~mCaU5#yugt -T`mdC(jQIOf%wEmKcd3hAh=0X?5`8?6`C`dig^YnT`@xCz9(hp7uJ%C+pT&wd?NT5oSzep4+4<}{^i1|jQa(mIKW>et!&(HAB|=FU6IE@bpH1P2>D933pwJnKVcNjkf6 -nJjhTZ*^OXS{mCkVsj8^6vtW4sJ^d*D((q%>1!3GZ0+i -iapY)$T%E(n8ve`}O9_B@zvt8jX-!NXm1ZxHf(}Xvu*a13y7TU|00J~&7!M^9Fs -#%J(y3j6X9vFmZf&I*mv<&C&G4}1)M^G7(#}cW<&S6 ->7+!(Mb -j!fdB5H#oJQIp73e_Z6Rd+=*R&XqEz@nb^Mv9?Fadzdgoe3`lABT{kg}eJPTxw*E@8Nc`Jhr&7?Q+<2 -nfN(tV3`>Ld!gBs7II<83N|>ERs0X!aAi6#x_&NjwdGf%$H -?!oD%|8y=#Qs>W1WiMVV#b0i24$U=X!=P;foIFXDle9`_A95=vjPA}P%nKe!bMfNEuIEol-x%@dFMm^=N! -T_0)~N?s^JPdrfJJjtnC2r_OeI%5Fi}>~B?HlV+oHB0h8s+9bZ$cFCN+v%Bw_iZDD^Z -oF+M6elTW(4y=pkj2bkjjq(DvvG)Xw}>nY19C_ER{DZ#KcR)|HoAtDf%wJyBop77{L78S4Fw+}F+1Wm -UrjuIodoMBx^r<8H)g`})`NF~!BgTbiE2D0l%c$%=n*?e%UpgbBHtGO3+~4y)p8&2W!!kd?TRfweWH`TI%^`h)#*N!xWxtigS -nC&W5{rulT70!haIgqim8DYCa(a9g_~wFZ{Zx%^Fki#8bHmJ)hG2IuYUla;8nENC6G^Rf!wL!3jhF9DF6T@0001RX>c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2 -ZUtei%X>?y-E^vA6TK#U@MiT#DPq7!HpoB_{lU#c)5gV`_r7`L_LFBZ!O9E;|t}NEnQs6Ev>xu?>jRW -ppE>ChZ`#~;0cM;GrAaOqi6 -HQYl9wB~$T&%stCTGnmsBS3HJcN;U}XB!v*XL3z9(ZNjtEJ4O!HU?dAyKv72LmXH}l1cMgDq0<@(Cc@ -~ittQ=d#2Bd^BCPo9s-Jj+SUBu!G`Yc+x>TW#{>Y9Rx1YCn>P`wt$H*@jG#C8#o?tAuOnG>I4&EFdS7 -Cqp7Y^7+~6_+ru&tt>Imvn2_`c_E9Og(03W%Oxpz0@|`LO(e@{DtbM$fKMY~`23X4SSl>j`SHp4eA4U -nejLA=oV~aphs4|WzwsY<@NADvvLz#&E*UWJ%2+gqrLx{QKpz7Z> -5h@$>Nng#V(>>m?!^9UOe~-RNMHc!B%f8)}N%(UL}^2n?5kage6=-B7`^gv(4UECC#ma=W@tc$}@p=- -_XE#r0@~AAariPLEHYjZqo++>6H$b9N({hMWJNwjlX`Rl#7H|tA9s;wt|WKdzKpXYnUTW=5v-qtP_QG4D?{ha4O}B+)`2fP%nC0@(S;Sa<-ay&cix+I87wZG;92wky2D~qqAqa%B0z+ -vH?(CF>OFF%aWe>}S&le6zHe!a@Gg2!Hsg9^bQW(}i0OMgHPQNoNy*LU!^2siu#@|VM=tesiyo?En5a -xhup`=0iXLN&<>iBU0QWKM-tfS}ywE?;7-rBpJbijD<~eFZ+&_n;~qcmu1m582ImY(hHCvg=~iS1>if -Z1p*ZC51iMxWqGLgZ#gMen3W- ->O_s()nmx-LEyqnZ>x6bdvE1i{&xO0C*DAV0_ -zbl=+fnbC5@NWVlvn3;5a-{Wd#Eze5jt4o{Uc6!>N*c8 -zM<)NQhgO!-UVXk_BAO>H=1(wua`&ZZR$O9g<3G``QDiZ)(gcd1A0h0*tC`A)ts@WJMZdJgQ>u*ThsR -!sfb-@J(Pjva78fn_8q96)Q;})wTLp#Dxc|?4ngzKKI_{LVU~woVSXH8$Pt#Py5q*W#y&# -d#kUD6OBLJ|H^@s3bhi}m?aY>xCeSVgBw)7@6e#!?)P)vR_~**R<2=vuogxz8xS)bcnYWEHgubV=y*F -sZLyMu;j1J@Qwz)qYC+djAIZoeuZO&0|Bm(kP?vbzB+6=b9L7BD!nKo<%fL8!*=wl?_1ImhB?I{YZ?} -EI(d^$SNbzDV81cru4D7z|ESrlR_W@CHbCus0wR!9~EDg>+YeoCQXNFti*G-2)>mHHh5oNQ2v!M+MKF -F;QBEyhY5mMj$D7^lgs1mAlTwx{@Z2IA`|#1~#GKv_>YxN;qtl-aKIG4|L{o+WmTSevlZrxH3 -_;613|@2RUOwih8RJoxr}+*o_?ZOUL-ub#A5XRPc^Ov94!RRI-vPSX^-w=%O03By1E`e)@51UAq}6&lswH#BFl0LILj$YaW;q7aDYUi_ov1Et-s!5q1Zcskwnk~zR%`VXF -8YMg92zXJ-ISH~#~T#~uj(teqh-sHfV_I4Y*FZ>I?I+0IXv7Tnc3E&%C#{PoKiyaYx0 -p@o%uz#hK1_&N=Pe(- -s1qP9SN`lU(3khl^qfyZl_hH^#H#YJoKZ13X{Ct*G80SxH(m=yz~+l`}OYRe2U8yXtyinOA!Neax1aa -M$=8d5zumB`6U!5dB#Rcg1#nrf^p+tZbnVtqIF2=3uBXe;YO!YXwlv^Xdgu_wBGYW2iHBbxz2cUQz)Y -D%Tm0a@|Cyx`oQywXuvf#e2m#s46q$4xN|_%LN;KW|vmAH`EQZukSD8h}-X>v -b+-W>}^MN2SMA}bqWh$<@P&cHyds_DawRl)25B;vIm}w~RC~hZ#a0B@~y2hUr8VSbiZw2->VKp*S}oiQDgI1R&V1}kowh~ZL83RSEzEL8ueFzJ)0#bi7wXP#7qn0lJ1pEd?zBnH42Roku6gR2=R -rD8I86^P^TL_0{FZZm;*47?*?DY@*WUlleXUJ*GDwH8njoi8enG$U@x#BdN`CzCA5$l_tj;>wtdFhWa -X9{gR)68vX=r@EgLy;P8vX!u}BQSy7=!XC1Y$ -IYg`QFBTR4JNv{xd#F1DxC_`oLqz -FP;=$B8jRt$4Q<43YLYLt!Cq)54&R$=r@VfX7%)Q#^WTS()?(|GqTKb=VC{10E -C$2>pvMi#|z%fwwjoij7O>;Q^CDBYEK<#)~ZM6|9EgCNL$T9sRk`Kr&!WQicB_w_{ox;}+f%Z9-~1n%AZSbz-L$ne7j;h -t9m3yFKDuL?*HOuLx?42DXUjCO1JLZ#A;fGiTc!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZY++($Y;!Jfd5xM)bF(%OfbaPgYI^V`%2;4N?3o_=ejGZJ_L -3gn8-uZ8E5_gf+&JyDzrBDlNV_Y*IV4^lR{JHf?BwzB@!^5hpAWf}S(5KmwlAO4cq`MgmH&r7w;O(s& -+L8&yWPO5oK**@3YNtu`F!IxNq+t;)c#Oy*w_1?%vlPDZ9mzoI^`LAjn68((Wy#gR>}?g^7`$Gl~S^o --@bnTDEN+ofcezLvHWD!? -#*DQTH-mHE;e?C0C{QBkl%PaeZ{nMJUxMZq!O>$BHT&g!7E;g;W{qw0~db$2V3`PJG`aVY3LTyjYl@3 -bI6b7NP`@R{0D1_2f=4l!V%Dc-cn#R*TxRqh;&Lz*i{XE83YA4 -7gZMgIM7RxUvO&h2l8F#tx(d-h=6CJm4!M;0PwbA>c{<5J>BTzPV`ty&lvqQyM*#8IShbgPB=b!sEfr -c$UmS(3@jpG}p!sq%+flaS?A}aCERg!{hRl^I#d;Y%~x1M&wI!D-fWX0k*l;ttCL2oZ@=NSS#E|x%TN -e5+K*S83)xQ*i?@bOfQX190AdDngXM_UUwk<-dt|7gzLtGRGxh>=e+^#*SU#6T1QD#EgYXYRVYY -0>5&d`Wg9s#X|8=}n2Sh@r0GrR}?QspI1XT)(xA${V8_1T3ZNuFmKn&$iFjR>qyqC;u;qycra1J^hD9 -~;@E(z9#{gTyMwNE0N86RuDi=i9tQ9&W$~f1UH);qgfgnnDJ&!g$4C<-$x4;nca}5k)J5 -ZVCTHPzP~||j7lBgq1`@q;N-v4Fpcpei5C#a^009gTb_04e&Po+s-RBh-Yro$+I@*t6;xbWq50YrRJR -a@PXejWRLZkg@O4gr_HWNFnmPY%kCTkU1rmJJe{IDKtd|Xh&i6?dYZCM8%M(A -tOuM0i_KGo7N)4<1P(#2?Jc(02del2YnRUf_VI+p+uwLSxOt9!fRU%psCv`oP%Peqv+Zm0X$NhWow5v -^|~hNbr`F8Q}t?2;1EuthK#I%6{rC?nIRL~fTIR@gaICHfCmhKBQ^?cLENZos81N+(+2p!063+g&=w5 -F38#0YHofsV5La_l~jzz38)wgaII60_HmJ;~3*l&-+!Lr -{vu7KyILDXB%3vXC>f3oQWr2!HVfe-`_%}1v}-~pIPbjKNpxlUX;rNvxDgi9B7k$3TZmh*S5h(nijaR -+YIo;ZwdVh!PkW0{nOE*(iSPAD>wl3L0@uKY@<0b1lGIUjd#r|LaE$6f6#Y9W6=-=XdsT*`#5oWcOqm -X@=09D{4~*hn=}7mbriSsv6|^{&itw!)~9-d~f-=on}3u>C+>E8sw#jf-Y*5V3IggTfW>y2psSa!MoS -+SDG=OLUWvV=+K=9eVy!Xe}Xvx^Gne2dHl?%qP1E9rHpOZ=WPoMoHAz#C4;_ptTiheH#x!4Mms&&^n<86n}2=~{uZ3*gV?}OU~l3SU@5I_z^?pK%7X9YTm3ZIY0oD^{Pghe!~alA0|X -QR000O8CKQ-XP=7HWL;?T+83h0UBme*aaA|NaUv_0~WN&gWcV%K_Zewp`X>Mn8FKugVVPa)$b1rasy; -Dt(+At8k=T}Urke!NV+dU$2=Kw?p3k^#JRY}YmdRJFf-{gDK*RC}x=C64{(jpsejU@*us@ -Z*4Ml$ENv+}fh~DcBl?jXQt;HB!)b+@fTLIRv-EOOC_pP+AlN7-g1~I -8m)KY0?oP`N22_a9Z&?~pz_9_K@Wg5B&XsvN7xW)}g*8C5L-os+CfCmLqIl!8`cYf)|j$7Dpf#APv?^ -MGaub2?0UXD7X?yh8^PgyqmayOmLy)3^ougfZ)r7g-!6@SB^2DPCVK1EcX7a|C+DcO^J)Z-!IJDHXw= -t5Q(;JgQi0xN&ItuiOzGG09IFsm(X?@6@L-yv4hyr|K$A|=dLoWJJjo$)*!=aTdew-t8iSVMrGPnwD+ -s|0M=u@or17Jf$ktS$Q-J&K+Ojue@nFI#~}hq9)9N)lc}J04p99z96{L!d1S@y`dKUBZuWPL`(}`SsD -?O3t(n$;lsb(I$ix&x%VLLAh@izJam~G-VkYjFe@=xGYQ3wvF#9NBe#vJLmNH=B5*@uo|o4uw(A9EtJ -{boSP}w80>igP)h>@6aWAK2mmG&m`-OnaHqKi005&I001EX003}la4%nWWo~3|axZsfVr6b)Z)9n1XL -B!fWpi|ME^v9ZS4(f(Mi9R1S1b|)qykBhTqLms_+Z5nU_fyS$T?XCv7}b^HZ&=4m$7{5q5UJh7AVl(d -vp6s`b#?dB9|}gVMnk`ayj2OkDZ+vy+%R2q|wa?rF_*rZoPW-s?|d9xhFn})H~MWgmEU9f3AF#_$l%+ -qX80DLUX*Pk&+H+Kq5v)XgojbAVvt9oqwE -M%qNn2NjRn<18QmXDv9rq=dDtnCdBg)y<5i#M`=XC1mcBYQi4OK1$*Yb8^-b1blpL-)A3|B?*Ofk?;z -#&&!l+LvDaiM#M+n3>2bT&YAwkMVa7`7V-Z -H&L`m7Lkx&@Ey6q21FUUh$ABQ?Z*dx3#Ug0xz`xs$KQ%5xOPwHAJ5Ox{jE5&Y>fk=4{JG=b0uk;cTb~ -be9yXrqjyKOR#FyskB{FCyMw)8A?7sN4>CKwJijp_;`hU#Gl-Jal5$Klci#uF+e~V009qCVZ4}SJ6ltuC5!UteWax)$x}Jlrag|RxP=1$)rL+lU^QJN -oobAQ4ne|jc|B!nWUr)QdXBN(S^Rj5)J8PJ5|}DCc%m!1_b#ShRAelBn|*WI3eg84xv1&^g}$3^8}QK -1QLJ|?y4N@fp7{vsNJq)qfR-2$&Z(~+KEmhsZAw&vB;bDHj_NEapY~*cNUw;bg>za;S1N`hs6dD!C6N -$I_+&#rc*or{Js~i(Alr9_l6q1`$6l*goDu8Kw+V+4bd4~Y`0ljFE+=05iC<_RekyImp}3nlf7#{#ZV -DGgel*2)m#;a+|FXx-AW*btHa?AW&x#{0lz-l9Qs`oNqrU<32RIe=U2`t=$`UhH2(al)u3QHfiIBv`- -=_PPTn27_lc!o@V+~q5-%y9`DpFS -Hh}PYPl#_pbc_qF+nVrn?8G~3+2gUtcmmUfOCvcMu3)hBaq!hMV}~_1WYc3dk`$-NMvBr8Q<$`RKgBd -MbENHNy>YS=+gt27JAlMcdl*iJvVw)s?}WZ3VHj88YI#QnM@&%9rwKzz^gNm@=c^GBjdVwjnl^f1gtK -%i(mEY|KK0G_QGjzb6?VKxr_<@#o+=zOEX$kxpB=7eyHxVz;B%okYOMJ4@$X$ZO7qS$JSt5!&);qx=V -^5>lp6i;a=LCaR&&@l-4qDAP^oD|mgclVW__3p<&iB{_}cF1n;KjA6>b|{_^J6Ms#r$%z(GiAkd@}#q -&55HC%-StWSaC-Eq7lwH8H;&pKtS-{=Stj%P>i&6UVSOC7D(uv$33H8&Al3TGr&5Qny3`cGLm;0{Hri -%>aL1Y?i~FR^vfQx1kcSG@CFo_4b1Z?77^Vufhst&VMoKH2 -V;o!L%pO2`O$khyK8K{<1Yi#`wP)h>@6aWAK2mmG&m`>XU{MMxe007ev001fg003}la4%nWWo~3|axZ -sfVr6b)Z)9n1XLB!jUv+b3a$jU+W@&C^WG--drI&4c+cXr0zvowol#+y{Ax_se4hzur1qx%7bueHrL6 -(jhgX09*p)D`_?I`w9^rT%s>>^+8b1X|b_p#io-V$oA)BJKImZjMlHk!?5qaobyiW17}v7&c9Sr%j`n -oOFLuEyWq|Em``%tbm|WHh6^lw~^4#oUN;O5%EFwb%|XCj2hFdfG_M7dJM&oR%YTLXQ -7P^p&G_UW@Qbl<|Bk}m+xGM@G@#O62ybaVgrmb}KTEvu`zCAZ)#SGIn_*-2Ha_5HXz!du_h8+ -RIHHa>Ozr}ym3YMI_W&3V~>{jOq<>Aqrl1d4rESKp`D&5mvqd#kNj>_urw&R*EnnQT{wtLqoWA>MuAr -retPjHJG2YCW_$b#=toFT9w|^)jQ?X?KgRrcTn_%*9yXX+Lw9+SMAloYA>D6hM -o4YYor}a=89!NdIj_)3*JGa&=Y~0F^v%a;c>}_Q~##Z3)Rn^xqHo4pAaEFfhfr;m<2j;Gc^P6(&{v_> -=T)g^|qg+A4%KVbrgT4LENGxocm+jVSgKgUq+d@s}clU?|9)B9}Z6{p-_%e!tUcY@Q3(_PW -ijSJ(Z1YaK=*9E5N;gbzcw7sCAz9)xh{!_o(N7)(N_d}zZp3ZYqtQK&Qul}4e`C{!ASN~2I|6e^8ErO -_~iaR_AyL$#3#p$=iFH}drg<_F9sbBp;QbDQ~5&1S1v=WV{;VSdbf!0a-2m_6nvOy4ZuDc>gFBi|t39 -p4(?7vB`$krDG#<|F2q`5E(b<`>LE=40j&^O$+UJY{~#{EB(T{F?a<^9l1?=2PZ(%yZ`V%xBCC=5yu` -%paLAm_IRpX8yu_=^yng4qh|gFel7knf@I4L*!48KR*8K_=9uFoHA49Z_MAB8FR+;Z`Yi;VE)1MuUEn -JujP{YC-aJV&Aeg$#r&K35A$E<+x0teuzqKk=3v;zB*J8X$u1@jG1&iP0xUpBQ~4`bhMV=p)fbqK`x$i9Qm2B>G77k?14QN1 -~5JABjE^eG>FZ&?iBk1bq_pNzf-jp9Fmp^hwYsL7xPD67)&XCqbVCeH8jA^ik-e&_|(y_`e^jg=%dj`qmM=(jXoNEH2P@t(deVmN28BMAB{d5eF%LBeF%LBeF%LBeF%LB -eF%LBeF%LBeF%LBeF%LBeGK{-^fBmT(8r*UK_7!Y27L_r81ymdW6;N-k3k=UKF0ZU>VL0K_ya^#|Mxa -FzvaT$hySulmT9Kjx4%vQ15ir=0u%!j0000803{TdPMH5nbQb^s02KfL01p5F00000000000HlEc000 -1RX>c!JUukY>bYEXCaCuNm0Rj{Q6aWAK2mmD%m`-N@Pvb`c003_S000jF0000000000005+cL;wH)aA -|NaUteuuX>MO%E^v8JO928D0~7!N00;mj6qru3qg%Na1pojx3jhEa00000000000001_fw%zx0B~t=F -JEbHbY*gGVQep7UukY>bYEXCaCuNm0Rj{Q6aWAK2mmD%m`+Sf-XSFk007_|000^Q0000000000005+c -1qT2CaA|NaUukZ1WpZv|Y%gMUX>4R)Wo~vZaCuNm0Rj{Q6aWAK2mmD%m`V>WaCuNm0Rj{Q6aWAK2mmD%m` -=ssY!O8#002vg000>P0000000000005+c8z}$)aA|NaUukZ1WpZv|Y%gSQcW!KNVPr0Fc~DCM0u%!j0 -000803{TdPCc!JX>N37a&BR4FJ*XRWpH$9Z*Frg -aCuNm0Rj{Q6aWAK2mmD%m`?vr(~s&d005Gt000&M0000000000005+cw_E@KaA|NaUukZ1WpZv|Y%gh -UWMz0RaCuNm0Rj{Q6aWAK2mmD%m`)#5(^qZ^000yl000^Q0000000000005+c)r$ZCaA|NaUukZ1WpZ -v|Y%gqYV_|e@Z*FrgaCuNm0Rj{Q6aWAK2mmD%m`?PpF}N2H002lc000{R0000000000005+cdzJtIaA -|NaUukZ1WpZv|Y%g$Sa5OSCbYW+6E^v8JO928D0~7!N00;mj6qrsy2IMbE3;+NmDgXc)00000000000 -001_fy<@<0B~t=FJEbHbY*gGVQepOd2n)XYGq?|E^v8JO928D0~7!N00;mj6qrsZoE7Oa5dZ)fI{*L} -00000000000001_flRXi0B~t=FJEbHbY*gGVQepQWpi(Ac4aPbc~DCM0u%!j0000803{TdPKD;r=fp1 -n0Ogc!JX>N37a&BR4FL!8VWo#~Rc~DCM0u%!j0000803{TdPR@ -7gG-v<-0E7Sl0384T00000000000HlGf^Z)>GX>c!JX>N37a&BR4FJo+JFJE72ZfSI1UoLQYP)h*<6a -y3h000O8B@~!W@a4dpS_c3C!xsPmBLDyZ0000000000q=8WN003}la4%nJZggdGZeeUMV{Bc!J -X>N37a&BR4FJo+JFJfVHWnW`&ZEaz0WG--dP)h*<6ay3h000O8B@~!W;z+Y?y%_)i1zi9D9{>OV0000 -000000q=8Qh0RV7ma4%nJZggdGZeeUMV{B@0%X>c!JX>N37a&BR4FJo+JFKuCIZeMU=a&u*JE^v8JO928D0~ -7!N00;mj6qrsvlkVAv3jhG3Bme*#00000000000001_fg&ma0B~t=FJEbHbY*gGVQepBY-ulWVRCb2a -xQRrP)h*<6ay3h000O8B@~!W+pKr?eE?y-E^v8JO928D0~7!N00;mj6qrtZx0!e20RRA&1po -ja00000000000001_fvhV>WaCuNm0Rj{Q6aWAK2mmD%m`)kL=AZ5e002}U001Wd0000000000005+c<39laaA|NaUukZ1WpZv| -Y%gPPZEaz0WOFZLZ*FF3XLWL6bZKvHE^v8JO928D0~7!N00;mj6qruxuqb~01^@ux82|tx000000000 -00001_ffh*t0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WWN&wFY;R#?E^v8JO928D0~7!N00;mj6qrs%F_c -F$1ONcL3;+Ni00000000000001_fm2Wc0B~t=FJEbHbY*gGVQepBZ*6U1Ze(*WW^!d^dSxzfc~DCM0u -%!j0000803{TdPWghuS=<5u0Fed&03HAU00000000000HlG$Q~>~RX>c!JX>N37a&BR4FJo_QZDDR?b -1!INb7(Gbc~DCM0u%!j0000803{TdPJvxS8Px#*073)+03HAU00000000000HlH3R{;QUX>c!JX>N37 -a&BR4FJo_QZDDR?b1!IRY;Z1cc~DCM0u%!j0000803{TdP9#6m=@bc!JX>N37a&BR4FJo_QZDDR?b1!Lbb97;BY%XwlP)h*<6ay3h000O8B@~!Wg?DcI+6n*wwkH -4p9smFU0000000000q=7(h0RV7ma4%nJZggdGZeeUMV{dJ3VQyq|FKlUZbS`jtP)h*<6ay3h000O8B@ -~!W4*TzM92IC005H<001KZ0000000000005+c-iQGJaA|N -aUukZ1WpZv|Y%gPPZEaz0WOFZdZfS0FbYX04E^v8JO928D0~7!N00;mj6qrt22uDzd2LJ&78vpbYEXCaCuNm0 -Rj{Q6aWAK2mmD%m`>FkRK%14000#P001EX0000000000005+cSd{?)aA|NaUukZ1WpZv|Y%gtZWMyn~ -FJobDWNBn!bY(7Zc~DCM0u%!j0000803{TdP9q~7==A~s0Llme044wc00000000000HlE{mjM89X>c! -JX>N37a&BR4FKusRWo&aVW^ZzBVRT<(Z*FvQZ)`4bc~DCM0u%!j0000803{TdPDbJ@7s>$u03-we038 -4T00000000000HlFrngIZCX>c!JX>N37a&BR4FKusRWo&aVX>Md?crI{xP)h*<6ay3h000O8B@~!W^i -}}3kp%z%uMz+N8~^|S0000000000q=9Li0RV7ma4%nJZggdGZeeUMZEs{{Y;!McX>MySaCuNm0Rj{Q6 -aWAK2mmD%m`(ry0006200000001Na0000000000005+cG@=0jaA|NaUukZ1WpZv|Y%gzcWpZJ3X>V?G -FJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!We^K*hO9lV{^%DR9ApigX0000000000q=9&%0RV7ma4%n -JZggdGZeeUMZ*XODVRUJ4ZgVeVXk}w-E^v8JO928D0~7!N00;mj6qrtuuuJP7~*9RL6T0000000000q=9C}0RV7ma4%nJZggdGZeeUMa%FKZ -Utei%X>?y-E^v8JO928D0~7!N00;mj6qrt??u(Tt4FCX#EC2u@00000000000001_fq}^Z0B~t=FJEb -HbY*gGVQepQWpOWKZ*FsRa&=>LZ*p@kaCuNm0Rj{Q6aWAK2mmD%m`=CJ9(H>W005FT0012T00000000 -00005+c=hguLaA|NaUukZ1WpZv|Y%g+UaW8UZabIR>Y-KKRc~DCM0u%!j0000803{TdPFd}F5g;c30E -Cc!JX>N37a&BR4FLGsZFLGsZUukZ0bYX04E^v8JO928D0~7!N0 -0;mj6qrugT)mY*2><}J9smFw00000000000001_f%^^u0B~t=FJEbHbY*gGVQepQWpOWZWpQ6~WpplZ -c~DCM0u%!j0000803{TdPLSc!JX>N37a&BR4FLGs -ZFLGsZUvzR|V{2t{E^v8JO928D0~7!N00;mj6qrtLDn=dl7ytl4S^xkd00000000000001_fwCL|0B~ -t=FJEbHbY*gGVQepQWpOWZWpQ71ZfS0FbYX04E^v8JO928D0~7!N00;mj6qrr`00002000000000V00 -000000000001_f#@{?0B~t=FJEbHbY*gGVQepTbZKmJFJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!W< -93tkC<_1pwj=-m9smFU0000000000q=6?k0swGna4%nJZggdGZeeUMb#!TLb1z|VaAaw6b1rasP)h*< -6ay3h000O8B@~!WhmVcyK@0!@XCVLp9RL6T0000000000q=Ab;0swGna4%nJZggdGZeeUMb#!TLb1!3 -WZE#_9E^v8JO928D0~7!N00;mj6qrsJog%or1ONaC3;+Nk00000000000001_fd@_k0B~t=FJEbHbY* -gGVQepTbZKmJFJxtKa%E#-bZKvHE^v8JO928D0~7!N00;mj6qrtg=M^V00ssKQ1ONaZ000000000000 -01_fdf+l0B~t=FJEbHbY*gGVQepTbZKmJFJ*3HZ)9n1XD)DgP)h*<6ay3h000O8B@~!WLO=oM%mDxZ- -U9#tApigX0000000000q=9i&0swGna4%nJZggdGZeeUMb#!TLb1!CTY-MwKb97~GE^v8JO928D0~7!N -00;mj6qrs`wKH691polj3;+Ne00000000000001_fqYj20B~t=FJEbHbY*gGVQepTbZKmJFK29NVq-3 -Fc~DCM0u%!j0000803{TdPP*~=1bX>c!JX>N37a&BR4FLi -WjY;!MYVRL9@b1rasP)h*<6ay3h000O8B@~!W{Yh&&H4Oj&mn{GQ9smFU0000000000q=A=W0swGna4 -%nJZggdGZeeUMb#!TLb1!UfXJ=_{XD)DgP)h*<6ay3h000O8B@~!Wm1T=>NhSaQvVH&n8vpc!JX>N37a&BR4FLiWjY;!MdZ)9a`b1rasP)h*<6ay3h000O8B@~! -WkubPZ_y+(0>=pn39{>OV0000000000q=E340swGna4%nJZggdGZeeUMb#!TLb1!dobYx+4Wn?aJc~D -CM0u%!j0000803{TdPHw<{yS)Pd0JjPN03ZMW00000000000HlE>paKAJX>c!JX>N37a&BR4FLiWjY; -!MgVPk7yXK8L{E^v8JO928D0~7!N00;mj6qrsHqVh_+0000M0RR9e00000000000001_fgq#;0B~t=F -JEbHbY*gGVQepTbZKmJFLPydb#QcVZ)|g4Vs&Y3WG--dP)h*<6ay3h000O8B@~!W5!tnt=LP@(+Y|r* -9{>OV0000000000q=6u%0swGna4%nJZggdGZeeUMb#!TLb1!sdZE#;?X>u-bc~DCM0u%!j0000803{T -dP8$Or4_*QQ0A&OK0384T00000000000HlFLtO5XVX>c!JX>N37a&BR4FLiWjY;!Mkd2nfNXD)DgP)h -*<6ay3h000O8B@~!WH3&C`j1T|-^fdqg82|tP0000000000q=DV80swGna4%nJZggdGZeeUMb#!TLb1 -!viE^v8JO928D0~7!N00;mj6qrsc7of>T6aWAPO8@{J00000000000001_fuO$v0B~t=FJEbHbY*gGV -QepUV{bYEXCaCuNm0Rj{Q6aWAK2mmD%m`)JBSdf$i005s1000{R0000000000005+cA=Cl@ -aA|NaUukZ1WpZv|Y%g|Wb1!0HdSPL5E^v8JO928D0~7!N00;mj6qruy$E*9H4*&q5G5`P=000000000 -00001_f$rD>0B~t=FJEbHbY*gGVQepUV{=}dc~DCM0u%!j0000803{TdPE9@4;ZpVe03H -AU00000000000HlG%=mG$6X>c!JX>N37a&BR4FLq;dFKuOVV|8+AVQemNc~DCM0u%!j0000803{TdPU -nrvePap$0Oub703QGV00000000000HlFb?E(OBX>c!JX>N37a&BR4FLq;dFLQNbc4cyNX>V>WaCuNm0 -Rj{Q6aWAK2mmD%m`?Rsje!{k001r$000*N0000000000005+c^!EY)aA|NaUv_0~WN&gWUtei%X>?y- -E^v8JO928D0~7!N00;mj6qrumUj{Tl5&!_>UjP6W00000000000001_fkOTQ0B~t=FJE?LZe(wAFJW+ -SWNC79E^v8JO928D0~7!N00;mj6qruxZmG*9CjbERr~m*J00000000000001_fwm9>0B~t=FJE?LZe( -wAFJx(RbaHPlaCuNm0Rj{Q6aWAK2mmD%m`*N@iz0qI008Va0RR{P0000000000005+c4mbkeg=WO8M5b1rasP)h*<6ay3h000O8B@~!WFobuJ|K7ytkO0000000000q=CY70 -|0Poa4%nWWo~3|axZXsaA9(DX>MmOaCuNm0Rj{Q6aWAK2mmG&m`)*kyb8|=008tR000*N0000000000 -005+c_gVx1aA|NaUv_0~WN&gWa%FUKd1-EEE^v8JO928D0~7!N00;mk6qrtEzg*Go9smHuegFUx0000 -0000000001_f%{|x0B~t=FJE?LZe(wAFLP;lE^v8JO928D0~7!N00;mj6qrtk0whkX0000k0RR9b000 -00000000001_ffZgg^QY%gD5X>MtBUtcb8c~DCM0u%!j0000803 -{TdPD-13rvd^101pKK03ZMW00000000000HlEjg#-X_X>c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eUt?`#E -^v8JO928D0~7!N00;mj6qrtU7M&J|1pom55&!@r00000000000001_fk%e~0B~t=FJE?LZe(wAFJob2 -Xk}w>Zgg^QY%gJCVQ_S1axQRrP)h*<6ay3h000O8B@~!W#$vopG64VpB?ABeApigX0000000000q=68 -P1ORYpa4%nWWo~3|axY_HV`yb#Z*FvQZ)`7PVPj}zE^v8JO928D0~7!N00;mj6qrthmShtW0RRBE0ss -Ia00000000000001_fq{+$0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPPZE#_9E^v8JO928D0~7!N00 -;mj6qruhwv5__4*&pyH2?r600000000000001_fzyu!0B~t=FJE?LZe(wAFJob2Xk}w>Zgg^QY%gPPZ -gg^QY;0w6E^v8JO928D0~7!N00;mj6qru)(f%6x0{{T!2><{m00000000000001_ft{ZO0B~t=FJE?L -Ze(wAFJob2Xk}w>Zgg^QY%gYMY-M+HVQ_F|axQRrP)h*<6ay3h000O8B@~!WlqGFmuLS@A<_`b>CIA2 -c0000000000q=DR{1ORYpa4%nWWo~3|axY_HV`yb#Z*FvQZ)`7UWp#3Cb98BAb1rasP)h*<6ay3h000 -O8B@~!WxKRxc*$4mtDjNU*B>(^b0000000000q=C<=1ORYpa4%nWWo~3|axY_HV`yb#Z*FvQZ)`7fWp -Zg@Y-xIBE^v8JO928D0~7!N00;mj6qrsFuAyD$0001=0ssIb00000000000001_f$Opa0B~t=FJE?LZ -e(wAFJob2Xk}w>Zgg^QY%h0mVQ_F|axQRrP)h*<6ay3h000O8B@~!W8NawNHvj+tRsaA1D*ylh00000 -00000q=6H(1ORYpa4%nWWo~3|axY_HV`yb#Z*FvQZ)`7PVPj}zb1z?CX>MtBUtcb8c~DCM0u%!j0000 -803{TdPV&hS!4U=k08tPC04)Fj00000000000HlGFv;+WfX>c!Jc4cm4Z*nhVVPj}zV{dMBa&K%eV_{ -=xWpgiPX>4U*V_{=xWiD`eP)h*<6ay3h000O8B@~!WsK-IyX8`~JSOWk6E&u=k0000000000q=E0c1O -RYpa4%nWWo~3|axY_HV`yb#Z*FvQZ)`7PVPj}zb1!mbWNC9>V_{=xWiD`eP)h*<6ay3h000O8B@~!W5 -m;ebHUIzsGynhq9{>OV0000000000q=BNm1ORYpa4%nWWo~3|axY_La&&2CX)j-2X>MtBUtcb8c~DCM -0u%!j0000803{TdP6_W6?=t`Z05t#r03QGV00000000000HlExy#xSoX>c!Jc4cm4Z*nhVWpZ?BW@#^ -9Uu|J&ZeL$6aCuNm0Rj{Q6aWAK2mmD%m`?o{2Bzp%0sv-L1OOfY0000000000005+cg}np-aA|NaUv_ -0~WN&gWV`Xx5X=Z6JV_{=ua&#_mWo=MP0Rj{Q6aWAK2mmD%m`>kP>w}sA007zm000{R000000000000 -5+csT2nQaA|NaUv_0~WN&gWV`Xx5X=Z6JV{dY0E^v8JO928D0~7!N00;mj6qru%&ks}N0ssIP1^@sb0 -0000000000001_fqWGQ0B~t=FJE?LZe(wAFJow7a%5$6FJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!W -IlqJu?>PVf7J2{x9{>OV0000000000q=B6n2LNzsa4%nWWo~3|axY_OVRB?;bT49QXEkPWWpOTWc~DC -M0u%!j0000803{TdPRr+KW;_D`0Nn-v03iSX00000000000HlG*QU?HVX>c!Jc4cm4Z*nhVXkl_>Wpp -oMX=gQXa&KZ~axQRrP)h*<6ay3h000O8B@~!W;8`d=JqZ8+!z2IzCjbBd0000000000q=7_M2LNzsa4 -%nWWo~3|axY_OVRB?;bT4CQVRB??b98cPVs&(BZ*DGdc~DCM0u%!j0000803{TdPKjA0h;jq~0Lu;l0 -4V?f00000000000HlG&Uk3niX>c!Jc4cm4Z*nhVXkl_>WppoNXkl`5Wprn9Z*_2Ra&KZ~axQRrP)h*< -6ay3h000O8B@~!W;Fn=qzXt#S_7eaABme*a0000000000q=A2A2LNzsa4%nWWo~3|axY_OVRB?;bT4C -QVRCb2bZ~NSVr6nJaCuNm0Rj{Q6aWAK2mmD%m`+^#H%O!f000IK001cf0000000000005+cfNTcZfA3JVRU6}VPj}%Ze=cTc~DCM0u%!j0000803{TdPJD*tWk~`60B -!^T0384T00000000000HlFyat8o#X>c!Jc4cm4Z*nhVXkl_>WppoNZ*6d4bS`jtP)h*<6ay3h000O8B -@~!Wy+WEXXafKMKL-E+A^-pY0000000000q=D~r2LNzsa4%nWWo~3|axY_OVRB?;bT4CYIW#$Na&KZ~ -axQRrP)h*<6ay3h000O8B@~!WLgTaCuNm0Rj{Q6aWAK2mmD%m`)%!#Ly}Q0 -05;9001HY0000000000005+cQilfsaA|NaUv_0~WN&gWV`yP=WMy008PV001BW0000000000005+cyo?6`aA|NaUv_0~WN&gWV`yP=WMyc!Jc4c -m4Z*nhVXkl_>WppoRVlp!^GH`NlVr6nJaCuNm0Rj{Q6aWAK2mmD%m`)8T)3TWo000g)001KZ0000000 -000005+c)e#5)aA|NaUv_0~WN&gWV`yP=WMypj00000000000001_fv+S80B~t=FJE?LZe(wAFJow7a%5$6FKTIXW^!e5E^v8JO928D -0~7!N00;mj6qrs1c_nI26953aOaK5K00000000000001_fdEwq0B~t=FJE?LZe(wAFJow7a%5$6FKTd -OZghAqaCuNm0Rj{Q6aWAK2mmD%m`?C$$;u!L001X4001cf0000000000005+ciD(D_aA|NaUv_0~WN& -gWV`yP=WMyc!Jc4cm4Z*nhVXkl_>WppoWVQyz*d2(rNY-wX{Z)9a`E^v8JO928D0~7!N0 -0;mj6qru_yRRu~2><|)F#rH100000000000001_fjEE&0B~t=FJE?LZe(wAFJow7a%5$6FKl6MXJ>L{ -WovD3WMynFaCuNm0Rj{Q6aWAK2mmD%m`;yotr)!t002=e001Tc0000000000005+c<%tLYaA|NaUv_0 -~WN&gWV`yP=WMyc!Jc4cm4Z*nhVXkl_>WppoWVQy!1X -klq>Z)9a`E^v8JO928D0~7!N00;mj6qru6GQ(uZ3IG6JD*yl|00000000000001_fi|iL0B~t=FJE?L -Ze(wAFJow7a%5$6FKl6MXLNOPYiV<6ZEs{{Y%XwlP)h*<6ay3h000O8B@~!WAWpXZXc~DCM0u%!j0000803{ -TdPBwKIP2B_l096eD044wc00000000000HlE%ya)hrX>c!Jc4cm4Z*nhVXkl_>WppoXVq<7wa&u*LaB -^>AWpXZXc~DCM0u%!j0000803{TdPOZ}ssB!}U0Ne)v044wc00000000000HlFD!3Y3wX>c!Jc4cm4Z -*nhVXkl_>WppoXVqa&L8TaB^>AWpXZXc~DCM0u%!j0000803{TdPHFbx59JI10EuG&0384T00000 -000000HlHP#0UUzX>c!Jc4cm4Z*nhVXkl_>WppoXVq2mo+ta4%nWWo~3|axY_OVRB?;bT -4yaV{>P6Z*_2Ra&KZ~axQRrP)h*<6ay3h000O8B@~!W){$p2AO!#bz77BYApigX0000000000q=CNP2 -mo+ta4%nWWo~3|axY_OVRB?;bT4yiX>)LLZ(?O~E^v8JO928D0~7!N00;mj6qrtRfJZ%W4FCYeFaQ83 -00000000000001_fgR)s0B~t=FJE?LZe(wAFJow7a%5$6FLiEdc4cyNVQge&bY)|7Z*nehc~DCM0u%! -j0000803{TdPW8XMJw*fn0L}^k03iSX00000000000HlG_@dyBLX>c!Jc4cm4Z*nhVXkl_>WppofbY? -hka&KZ~axQRrP)h*<6ay3h000O8B@~!WN5_!Yxc~qF@&Et;9smFU0000000000q=8rV2mo+ta4%nWWo -~3|axY_OVRB?;bT4*ga&u{KZZ2?nP)h*<6ay3h000O8B@~!WlLpr^0{{R30RR91BLDyZ0000000000q -=8KL2mo+ta4%nWWo~3|axY_OVRB?;bT4CUX)j-2X>MtBUtcb8c~DCM0u%!j0000803{TdPWL9XpP&Q) -0M-fs03`qb00000000000HlGC_Xq%RX>c!Jc4cm4Z*nhVXkl_>WppoNY-ulJXkl_>Wprg@bS`jtP)h* -<6ay3h000O8B@~!WZAzfivH$=8?*IS*AOHXW0000000000q=9q%2mo+ta4%nWWo~3|axY_VY;SU5ZDB -88UukY>bYEXCaCuNm0Rj{Q6aWAK2mmD%m`*+wr(cT$007(x000~S0000000000005+cWBmvKaA|NaUv -_0~WN&gWV{dG4a$#*@FJW$TX)bViP)h*<6ay3h000O8B@~!WBk*i@_ua4%nWWo~3|axY_VY;SU5ZDB8AZgXjLZ+B^KGcqo4c~DCM0u%!j0000803{TdP8)AUa)SZ@ -0DA`j03rYY00000000000HlG^3<&^mX>c!Jc4cm4Z*nhVZ)|UJVQpbAX>MtBX<=+>b7d}Yc~DCM0u%! -j0000803{TdP6!(e5l{sH02~zn0384T00000000000HlGH4+#KpX>c!Jc4cm4Z*nhVZ)|UJVQpbAcWG -`jGA?j=P)h*<6ay3h000O8B@~!WqA1qp?ganOV0000000000q=6t62>@_ua4%nWWo~3|ax -Y_VY;SU5ZDB8WX>N37a&0bfc~DCM0u%!j0000803{TdP8*_L`CI`207U`-03QGV00000000000HlFS8 -wmh#X>c!Jc4cm4Z*nhWX>)XJX<{#5UukY>bYEXCaCuNm0Rj{Q6aWAK2mmD%m`-Ne(&;ZN006w90012T -0000000000005+cc!Jc4cm4Z*nhWX>)XJX<{#AVRT_)VRL0JaCuNm0Rj -{Q6aWAK2mmD%m` -Md?crI{xP)h*<6ay3h000O8B@~!WOp>%zMKb^Zbjkn#9{>OV0000000000q=EH~2>@_ua4%nWWo~3|a -xY|Qb98KJVlQlOV_|e}a&sc!Jc4cm4Z*nhWX>)XJX<{#JVQy(=Wpi{caCuNm0Rj{Q6aWAK2mmD%m`=90h-L!@001Kq0015 -U0000000000005+cAkql{aA|NaUv_0~WN&gWWNCABY-wUIZDDe2WpZ;aaCuNm0Rj{Q6aWAK2mmD%m`? -5gg%biL008Wq0018V0000000000005+cU)Tu%aA|NaUv_0~WN&gWWNCABY-wUIZDn*}WMOn+E^v8JO9 -28D0~7!N00;mj6qrtq-GmaT3jhEPDgXc=00000000000001_fu8*d0B~t=FJE?LZe(wAFJx(RbZlv2F -LGsbZ*_8GWpgfYc~DCM0u%!j0000803{TdPEX#?6S@-s0RBJ#03HAU00000000000HlG02?_vkX>c!J -c4cm4Z*nhWX>)XJX<{#PV{&P5baO6nc~DCM0u%!j0000803{TdPHv@lC#$jm003$M02=@R000000000 -00HlF>9SQ(&X>c!Jc4cm4Z*nhWX>)XJX<{#QGcqn^cx6ya0Rj{Q6aWAK2mmD%m`=6@qPy500000000000001_fl8AL0B~t=FJE?LZe(wAFJx(RbZlv2FLiWjY%XwlP)h*<6ay3h0 -00O8B@~!W_Gl<9P8k3IUt0hG9smFU0000000000q=CQE3jlCwa4%nWWo~3|axY|Qb98KJVlQ@Oa&u{K -ZZ2?nP)h*<6ay3h000O8B@~!WEr?>m)UE&k09*k88vpc -!Jc4cm4Z*nhWX>)XJX<{#THZ(3}cx6ya0Rj{Q6aWAK2mmD%m`=Xn)X%de0079F000~S000000000000 -5+cZ(0oiaA|NaUv_0~WN&gWWNCABY-wUIcW7m0Y%XwlP)h*<6ay3h000O8B@~!WBkyPY!vFvP5&-}JD -F6Tf0000000000q=8(34FGUya4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mUtei%X>?y-E^v8JO928D -0~7!N00;mj6qru0iZ5l(0RRBY0{{Re00000000000001_fo6dX0B~t=FJE?LZe(wAFJx(RbZlv2FJEF -|V{344a&#|kX>(&PaCuNm0Rj{Q6aWAK2mmD%m`>tiJZ*p`mb9r-PZ*FF3XD)DgP)h*<6ay3h000O8B@~!WL_!}!Vp#wH)M)_#C;$Ke000000000 -0q=9q64FGUya4%nWWo~3|axY|Qb98KJVlQ7}VPk7>Z*p`mbYXI4X>4UKaCuNm0Rj{Q6aWAK2mmD%m`* -aR9Nk3%004>v001BW0000000000005+c8ygM)aA|NaUv_0~WN&gWXmo9CHEd~OFJE72ZfSI1UoLQYP) -h*<6ay3h000O8B@~!WbsBC3B@_SvK|ufjApigX0000000000q=B9t4ghdza4%nWWo~3|axZ9fZEQ7cX -<{#5X=q_|Wq56DE^v8JO928D0~7!N00;mj6qrsHH}m9P9{>O-e*gd^00000000000001_fdVoP0B~t= -FJE?LZe(wAFKBdaY&C3YVlQ85Zg6#Ub98cLVQnsOc~DCM0u%!j0000803{TdPQ*rP7QY+-03<5`03rY -Y00000000000HlGUQ4RobX>c!Jc4cm4Z*nhabZu-kY-wUIUvzJ4Wo~JDWpXZXc~DCM0u%!j0000803{ -TdPTF~la}ovs0Iv@K03HAU00000000000HlGTZVmu&X>c!Jc4cm4Z*nhabZu-kY-wUIUv+e8Y;!Jfc~ -DCM0u%!j0000803{TdPSy*C?^8$s06sc!Jc4cm4Z*nhabZu-kY --wUIV{dMAbYX6Eb1rasP)h*<6ay3h000O8B@~!WU)FBu4MYF{v(NzmBLDyZ0000000000q=A9G4ghdz -a4%nWWo~3|axZ9fZEQ7cX<{#EbZu-kaA9(DWpXZXc~DCM0u%!j0000803{TdPOt+FBDoL%0FFHX03rY -Y00000000000HlG>0S^FhX>c!Jc4cm4Z*nhabZu-kY-wUIb7gXAVQgu7WpXZXc~DCM0u%!j0000803{ -TdPVdvN52gSB03iVY044wc00000000000HlG)5)S}yX>c!Jc4cm4Z*nhabZu-kY-wUIUvzS5WiMY}X> -MtBUtcb8c~DCM0u%!j0000803{TdPH2qZRe=Ej0HOl`03!eZ00000000000HlGk6Au7zX>c!Jc4cm4Z -*nhabZu-kY-wUIUvzS5WiMZ1VRL0JaCuNm0Rj{Q6aWAK2mmD%m`)^BovEe)005c<001Na0000000000 -005+cbQKQ(aA|NaUv_0~WN&gWXmo9CHEd~OFJE+WX=N{DVRUk7WiD`eP)h*<6ay3h000O8B@~!WOHXd -j9RdIV?*;$>AOHXW0000000000q=8%)4*+m!a4%nWWo~3|axZ9fZEQ7cX<{#5baH8BFK~G-aCuNm0Rj -{Q6aWAK2mmD%m`(ry0006200000001Ze0000000000005+cw;2xraA|NaUv_0~WN&gWXmo9CHEd~OFJ -@_MbY*gLFJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!WfUpD*!T|sPmjeI*HUIzs0000000000q=Ek#4 -*+m!a4%nWWo~3|axZ9fZEQ7cX<{#CX>4?5a&s?XY;b5{Vr6t`V_|GzbaZlQVs&(7b1rasP)h*<6ay3h -000O8B@~!WB7pnEl>h($9svLVBme*a0000000000q=6M24*+m!a4%nWWo~3|axZ9fZEQ7cX<{#CX>4? -5a&s?YVRL0JaCuNm0Rj{Q6aWAK2mmD%m`?M+KNl7Q004mt001)p0000000000005+c>>LjOaA|NaUv_ -0~WN&gWXmo9CHEd~OFJ@_MbY*gLFKKRSWn*+-ZDn*}Ut?%ta&u*LE^v8JO928D0~7!N00;mj6qrtu(P -W@(0{{R!4gdfo00000000000001_fm0w40B~t=FJE?LZe(wAFKBdaY&C3YVlQTCY;^X>c!Jc4cm4Z*nhabZu-k -Y-wUIW@&76WpZ;bZ*X*JZ*F01bYW+6E^v8JO928D0~7!N00;mj6qrsx!2wY6761TMtBUtcb8c~DCM0u%!j0000803{TdPEw2!DO3Ug -0J8=F04o3h00000000000HlFfMh^gRX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7VPs)&bY*gLFK1?y-E^v8JO928D0~7!N0 -0;mj6qruL)uOES4gdi2H~;`C00000000000001_fgMo~0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo2S@ -X>4R=a&s?YVRL0JaCuNm0Rj{Q6aWAK2mmD%m`(`lF8JFA004s`001Ze0000000000005+cT3`A4*+m!a4%nWWo~3|axZ9fZEQ7cX<{#Qa%E*=b!lv5WpZ;bWpr|7WiD`eP) -h*<6ay3h000O8B@~!W8c*uCUkv~NDmMTCF8}}l0000000000q=EKw4*+m!a4%nWWo~3|axZ9fZEQ7cX -<{#Qa%E*=b!lv5WpZ;bWpr|7WnXM~ZEP-Zc~DCM0u%!j0000803{TdPOWC#2$==|08$nJ04x9i00000 -000000HlGUfDZt0X>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bUtei%X>?y-E^v8JO928D0~7! -N00;mj6qrsSE!sjC2LJ#x9RL6*00000000000001_frW<;0B~t=FJE?LZe(wAFKBdaY&C3YVlQ-ZWo3 -6^Y-?q5b1!0Hb7d}Yc~DCM0u%!j0000803{TdPGyR3srLZ@0EGnr04D$d00000000000HlH9jt>BEX> -c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bWN&RQaCuNm0Rj{Q6aWAK2mmD%m`=XM@-6BF0071j0 -01cf0000000000005+c9+3|KaA|NaUv_0~WN&gWXmo9CHEd~OFLZKcWp`n0Yh`kCFJ*LcWo0gKc~DCM -0u%!j0000803{TdPL>UvM`#8B0HqiJ04@Lk00000000000HlFWmJa}MX>c!Jc4cm4Z*nhabZu-kY-wU -IbaG{7cVTR6WpZ;bWpr|7WnXM~ZEP-Zc~DCM0u%!j0000803{TdP7_~k5Xb@m00jvE04e|g00000000 -000HlEfoeuzTX>c!Jc4cm4Z*nhabZu-kY-wUIbaG{7cVTR6WpZ;bXJu}4XlX8Rc~DCM0u%!j0000803 -{TdP5?(-n==3a06G8w02}}S00000000000HlEvpbr3WX>c!Jc4cm4Z*nhbWNu+EUtei%X>?y-E^v8JO -928D0~7!N00;mj6qru)NYv>X0{{Ty3;+Nb00000000000001_fq%008I!000^Q0000000000005+c(W4IlaA|NaUv_0~WN&g -WX=H9;FJo_QaA9;VaCuNm0Rj{Q6aWAK2mmD%m`)X!*;e)n007l3000;O0000000000005+cn4}K?aA| -NaUv_0~WN&gWX=H9;FJo_VWiD`eP)h*<6ay3h000O8B@~!W%rG=tL?QqH!=C^E8~^|S0000000000q= -Ce*4*+m!a4%nWWo~3|axZCQZecHJWNu+(VRT_GaCuNm0Rj{Q6aWAK2mmD%m`=B2^s?px007kn0012T0 -000000000005+cLedWaaA|NaUv_0~WN&gWX=H9;FKKRca$#;~WpgfYc~DCM0u%!j0000803{TdPAHI~ -LKgr402KfL03ZMW00000000000HlFq)eiu0X>c!Jc4cm4Z*nhbWNu+EaA9L>VP|DuWMOn+E^v8JO928 -D0~7!N00;mj6qrr|1KdMz5Z)0m_X>4UKaCuNm0Rj{Q6aWAK2mmD%m`))kynb^7 -000;a001Qb0000000000005+cNq7(daA|NaUv_0~WN&gWY;R+0W@&6?FKugNX>x3DV{2w_0Lm8t03!eZ00000000000HlHcdk_F{X>c!Jc4cm4Z*nheZ)0m_X>4 -ULaA{<0Z)0m_X>4UKaCuNm0Rj{Q6aWAK2mmD%m`=Ne@VyBI004p&001Tc0000000000005+cT7(b)aA -|NaUv_0~WN&gWY;R+0W@&6?FLQBhX>?_5Z)0m_X>4UKaCuNm0Rj{Q6aWAK2mmD%m`;%+3E4dZ001}&0 -01Wd0000000000005+cr-={%aA|NaUv_0~WN&gWY;R+0W@&6?FLQZqY-w(5Y;R+0W@&6?E^v8JO928D -0~7!N00;mj6qruu>Q|wG0ssJw1^@sb00000000000001_fhmm;0B~t=FJE?LZe(wAFKu&YaA9L>FJE7 -2ZfSI1UoLQYP)h*<6ay3h000O8B@~!W9n}zr761SM6aWAK9{>OV0000000000q=Dy;5CCv#a4%nWWo~ -3|axZOjXK-O-YcF4RWpZc!Jc4cm4Z*nhfb7yd2V{0#Ecw=R7bZKvHb1rasP)h*<6ay3h000O8B@~!WTr6(~KN|o5J -B|PV9{>OV0000000000q=DR#5CCv#a4%nWWo~3|axZOjXK-O-YcFPDY;0m-V{0yOc~DCM0u%!j00008 -03{TdPJavm7l;7>0O$e$03rYY00000000000HlFftq=fkX>c!Jc4cm4Z*nhiVPk7yXK8L{FJE6_VsCY -HUtcb8c~DCM0u%!j0000803{TdPGuVrL=*u405Spq03iSX00000000000HlE+uMhxmX>c!Jc4cm4Z*n -hiVPk7yXK8L{FJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!WW{TS5%>e)aVFLgFAOHXW0000000000q= -9p=5CCv#a4%nWWo~3|axZXUV{2h&X>MmPUt@1=aA9;VaCuNm0Rj{Q6aWAK2mmD%m`-bt9F=kb004*u0 -01Na0000000000005+cezFh%aA|NaUv_0~WN&gWaA9L>VP|P>XD?rKbaHiLbairNb1rasP)h*<6ay3h -000O8B@~!WBJ^Gp^a%g}Dj)y=AOHXW0000000000q=7QD5CCv#a4%nWWo~3|axZXUV{2h&X>MmPZDDe -2WpZ;aaCuNm0Rj{Q6aWAK2mmD%m`)W^p8Vwo003MP001Qb0000000000005+cWWEppaA|NaUv_0~WN& -gWaA9L>VP|P>XD@PPadl~OWo>0{baO6nc~DCM0u%!j0000803{TdPV$IG({C660D^1)03!eZ0000000 -0000HlG8#Sj2+X>c!Jc4cm4Z*nhiVPk7yXK8L{FLQ8ZV`*k-WpZ;aaCuNm0Rj{Q6aWAK2mmD%m`-6mt -(}Jg008g>0015U0000000000005+cJl+rhaA|NaUv_0~WN&gWaA9L>VP|P>XD@YhX>4;YaCuNm0Rj{Q -6aWAK2mmD%m`?FCa_fE#007D^001BW0000000000005+c|KJb+aA|NaUv_0~WN&gWaA9L>VP|P>XD@b -Ta&u{KZZ2?nP)h*<6ay3h000O8B@~!WP5}d;RR910Q~&?~9smFU0000000000q=CEc5CCv#a4%nWWo~ -3|axZXYa5XVEFJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!W$8flSW(NQOtrq|QApigX0000000000q= -8KD5CCv#a4%nWWo~3|axZXYa5XVEFJEbHUvP47V`X!5E^v8JO928D0~7!N00;mj6qrt_^9=w%1ONbw3 -IG5b00000000000001_f${bb0B~t=FJE?LZe(wAFK}gWH8D3YVs&Y3WG--dP)h*<6ay3h000O8B@~!W -AOZwdbp-$b{T2WK8vpc!Jc4cm4Z*nhiWpFhyH!ovvY;S -UGZ)YxWc~DCM0u%!j0000803{TdPDnn-g7N?W0Cxfa02}}S00000000000HlH52N3{pX>c!Jc4cm4Z* -nhiWpFhyH!ovvZE#_9E^v8JO928D0~7!N00;mj6qruMj+S)K2LJ$r761Sq00000000000001_feHx`0 -B~t=FJE?LZe(wAFK}gWH8D3YWo~w2b!lv5E^v8JO928D0~7!N00;mj6qrsC+V+(O2LJ#V7XSbr00000 -000000001_ffW%E0B~t=FJE?LZe(wAFK}gWH8D3YcXDBHaAk6HE^v8JO928D0~7!N00;mj6qrsgq{LK -4a{vH_r2zmX00000000000001_fmaw20B~t=FJE?LZe(wAFK}yTUvg!0Z*_8GWpgiIUukY>bYEXCaCu -Nm0Rj{Q6aWAK2mmD%m`-6N1J+9c001%q001Wd0000000000005+c;ENFeaA|NaUv_0~WN&gWaBF8@a% -FRGb#h~6b1!gtGcjXtZE#_9E^v8JO928D0~7!N00;mj6qrszX%kCq1^@v05&!@o00000000000001_f -pCox0B~t=FJE?LZe(wAFK}{iXL4n8b1z?CX>MtBUtcb8c~DCM0u%!j0000803{TdPM6HJxTXXE0456n -02=@R00000000000HlE$lo0@MX>c!Jc4cm4Z*nhia&KpHWpi^cVqtPFaCuNm0Rj{Q6aWAK2mmD%m`-@ -$xqFa%FRKFJo_YZggdGE^v8JO92 -8D0~7!N00;mj6qrsg;N7|k0{{SI1poja00000000000001_fhU|10B~t=FJE?LZe(wAFK}{iXL4n8b1 -!pnX>M+1axQRrP)h*<6ay3h000O8B@~!Wc*ke+Q~&?~e*gdg9smFU0000000000q=9aq5dd&$a4%nWW -o~3|axZXsbZ>2JFJE72ZfSI1UoLQYP)h*<6ay3h000O8B@~!WOgs63z5oCK{Q&>~8UO$Q0000000000 -q=EjQ5dd&$a4%nWWo~3|axZXsbZ>2JFJo_VWiD`eP)h*<6ay3h000O8B@~!WZ%4f&=m`J-Rww`f8~^| -S0000000000q=EFH5dd&$a4%nWWo~3|axZXsbZ>2JFK}UUb7gWaaCuNm0Rj{Q6aWAK2mmD%m`<$$+<$ -KY008|1000^Q0000000000005+c7ON2eaA|NaUv_0~WN&gWaCvlZZEP=eWpi{caCuNm0Rj{Q6aWAK2m -mD%m`>srbc!Jc4cm -4Z*nhkWpQ<7b98erUtei%X>?y-E^v8JO928D0~7!N00;mj6qrunt*Tf50002A0RR9a0000000000000 -1_ft0!t0B~t=FJE?LZe(wAFLGsZb!BsOb1z?Cc4cyNX>V>{UoLQYP)h*<6ay3h000O8B@~!Wh^aZd6a -oMMNCW@?CjbBd0000000000q=D4C5dd&$a4%nWWo~3|axZdaadl;LbaO9XX>N37a&BR4Uv+e8Y;!Jfc -~DCM0u%!j00008045ZePMkR|M2r*w04!7h03ZMW00000000000HlE}z7YU$X>c!Jc4cm4Z*nhkWpQ<7 -b98erVPs)&bY*gLE^v8JO928D0~7!N00;mk6qrr`5iTeQ1^@tU7ytkp00000000000001_f%no80B~t -=FJE?LZe(wAFLGsZb!BsOb1z|VX)bViP)h*<6ay3h000O8CKQ-XHt20c!Jc4cm4Z*nhkWpQ<7b98erV{dJ6VRSBVc~DCM -0u%!j00008045ZePQ51=I=~YE0RBe+03QGV00000000000HlFa=@9^MX>c!Jc4cm4Z*nhkWpQ<7b98e -rV{dP3X=QURaCuNm0Rj{Q6aWAK2mmG&m`=I4Y;ijT004aq001HY0000000000005+cQ2h}AaA|NaUv_ -0~WN&gWa%FLKWpi|MFJ*XRWpH$9Z*FrgaCuNm0Rj{Q6aWAK2mmG&m`<%dNAF4m008<8000~S0000000 -000005+c%K;JqaA|NaUv_0~WN&gWa%FLKWpi|MFKA_Ka4v9pP)h*<6ay3h000O8CKQ-X8%lHGc>w?b^ -#T9@9RL6T0000000000q=8Qc5&&>%a4%nWWo~3|axZdaadl;LbaO9gZ*OaJE^v8JO928D0~7!N00;mk -6qruaHj0)xCIA5Ag#Z8^00000000000001_fddE<0B~t=FJE?LZe(wAFLGsZb!BsOb1!XgWMyn~E^v8 -JO928D0~7!N00;mk6qrsDJl=t80RRBE0ssIY00000000000001_fp{(w0B~t=FJE?LZe(wAFLGsZb!B -sOb1!gVV{2h&WpgfYc~DCM0u%!j00008045ZePCw>@*h3%y0F-h703ZMW00000000000HlE-FcJW8X> -c!Jc4cm4Z*nhkWpQ<7b98erb7gaLX>V?GE^v8JO928D0~7!N00;mk6qrs*x@LBL1^@sd5C8xq000000 -00000001_fuc|n0B~t=FJE?LZe(wAFLGsZb!BsOb1!prVRUtKUt@1%WpgfYc~DCM0u%!j00008045Ze -PU(v|d36K;0Hq5603rYY00000000000HlFoR}uhlX>c!Jc4cm4Z*nhkWpQ<7b98erb98cbV{~c!Jc4cm4Z*nhkWpQ -<7b98erb#!TLb1rasP)h*<6ay3h000O8CKQ-Xk&FXPJOls$q6z>29{>OV0000000000q=ATm5&&>%a4 -%nWWo~3|axZmqY;0*_GcR9XX>MtBUtcb8c~DCM0u%!j00008045ZeP89bFCi@Hk0QxBa03!eZ000000 -00000HlEdh7tg9X>c!Jc4cm4Z*nhna%^mAVlyvaV{dG1Wn*+{Z*FrgaCuNm0Rj{Q6aWAK2mmG&m`)(m -1?V{u000j*001EX0000000000005+cJ(CguaA|NaUv_0~WN&gWb#iQMX<{=kV{dM5Wn*+{Z*DGdc~DC -M0u%!j00008045ZePOvU|YfB~o02zq@03`qb00000000000HlGnq!IvdX>c!Jc4cm4Z*nhna%^mAVly -veZ*Fd7V{~b6Zg6jJY%XwlP)h*<6ay3h000O8CKQ-Xz0_N$fCvBp%ozXxApigX0000000000q=7@r5& -&>%a4%nWWo~3|axZmqY;0*_GcRR$V`Xr3X>V?GE^v8JO928D0~7!N00;mk6qrtf^}-G;2LJ#!7XSbq0 -0000000000001_fdbSL0B~t=FJE?LZe(wAFLiQkY-wUMFJ@_FY-DpTaCuNm0Rj{Q6aWAK2mmG&m`)^C -wJClB004vu0018V0000000000005+cY1z?E00000000000001_fh6A&0B~t=FJE?LZe(wAFLiQkY-wUMFK} -;fY;9p~VP|D>E^v8JO928D0~7!N00;mk6qru@=-vpN2LJ$P7XSbr00000000000001_fzR?10B~t=FJ -E?LZe(wAFLiQkY-wUMFLGsZb!BsOE^v8JO928D0~7!N00;mk6qruQ`1mF`8vppk00000000000 -001_fv5Ts0B~t=FJE?LZe(wAFLiQkY-wUMFLGsbaBpsNWiD`eP)h*<6ay3h000O8CKQ-X000000ssI2 -00000CjbBd0000000000q=6h3698~&a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQ7`X>MtBUtcb8c~DC -M0u%!j00008045ZePVO(B=Mn(`0L=mb05Jdn00000000000HlFq783w)X>c!Jc4cm4Z*nhna%^mAVly -veZ*FvQX<{#5VQ_F|Zf9w3WnX1(c4=~NZZ2?nP)h*<6ay3h000O8CKQ-X1b1H%a0~zdx+(wwC;$Ke00 -00000000q=Cd2698~&a4%nWWo~3|axZmqY;0*_GcRLrZgg^KVlQEEaAj_1X>MgMaCuNm0Rj{Q6aWAK2 -mmG&m`;RB)A_9h003(d001Wd0000000000005+cdL$D7aA|NaUv_0~WN&gWb#iQMX<{=kV{dMBa%o~O -ZggyIaBpvHE^v8JO928D0~7!N00;mk6qrsU6!e&@6953uJpceG00000000000001_fov)h0B~t=FJE? -LZe(wAFLiQkY-wUMFJo_RbaH88FK~HpaAj_Db8Iefc~DCM0u%!j00008045ZePNfXg=>H!80A+Rn04@ -Lk00000000000HlFkJ`(_NX>c!Jc4cm4Z*nhna%^mAVlyveZ*FvQX<{#PWn*=6Wpr|3ZgX&Na&#_mc~ -DCM0u%!j00008045ZePN@W-uzd#r0QVRG03-ka00000000000HlGaUK0RtX>c!Jc4cm4Z*nhna%^mAV -lyveZ*FvQX<{#PZ)0n7E^v8JO928D0~7!N00;mk6qrr`00002000000000u00000000000001_foEnD -0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FJE(IV|8+6baG*Cb8v5RbT40DX>MtBUtcb8c~DCM0u%! -j00008045ZePPbW$eOC_v0GLDo05|{u00000000000HlGxW)lE#X>c!Jc4cm4Z*nhna%^mAVlyveZ*F -vQX<{#5b7f<7a%FUKVQzD9Z*p`mVrgzMn8E^v8JO928D0~7!N00;mk6qrteb4!hL4*&pyF8}~J00 -000000000001_foygY0B~t=FJE?LZe(wAFLiQkY-wUMFJo_RbaH88FJE(IV|8+6baG*Cb8v5RbT4dgc -VBE}c4cfXaCuNm0Rj{Q6aWAK2mmG&m`;yfpnOmO003VP|D?FJE72ZfSI1UoLQYP)h*<6ay3h000O8CKQ-XnOIF)NF4wGl63$8BLDy -Z0000000000q=C$a698~&a4%nWWo~3|axZmqY;0*_GcRyqV{2h&WpgicX?QMhc~DCM0u%!j00008045 -ZeP5=M^00IC20000005Sjo00000000000HlFcq!R#eX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQVq -s%zaBp&Sb1z?CX>MtBUtcb8c~DCM0u%!j00008045ZePO^IqgH8ef0I&rB05Sjo00000000000HlGZq -!R#eX>c!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvCQVqs%zaBp&Sb1!XSYh`9>Y-KKRc~DCM0u%!j00008 -045ZePWx@jc!Jc4cm4Z*nhna%^mAVlyvrVPk7yXJvC -Qb8~E8ZDDj{XkTb=b98QDZDlWCUukY>bYEXCaCuNm0Rj{Q6aWAK2mmG&m`VP|D?FLQHjUu|J@V`yJ!Z*z2RVQpnEUu -kV{Y-Md_ZggREX>V>WaCuNm0Rj{Q6aWAK2mmG&m`+l4;lHE-000yO001Na0000000000005+c@UasBa -A|NaUv_0~WN&gWb#iQMX<{=kb#!TLFJE72ZfSI1UoLQYP)h*<6ay3h000O8CKQ-X*VSNXF$Vwu9})ln -CIA2c0000000000q=DA6698~&a4%nWWo~3|axZmqY;0*_GcR>?X>2cJZ*Fd7V{~b6ZZ2?nP)h*<6ay3 -h000O8CKQ-X^elTY`~Uy|@c{q;ApigX0000000000q=8Gj698~&a4%nWWo~3|axZmqY;0*_GcR>?X>2 -cXb!ByBE^v8JO928D0~7!N00;mk6qrsyVUX7W1pokf4gdfn00000000000001_frh*j0B~t=FJE?LZe -(wAFLiQkY-wUMFLiWjY%g+Uadl;LbS`jtP)h*<6ay3h000O8CKQ-X);no&Gz0(u8VUdaBme*a000000 -0000q=Cr6698~&a4%nWWo~3|axZmqY;0*_GcR>?X>2cYWpi+EZgXWWaCuNm0Rj{Q6aWAK2mmG&m`)&1 -M2+te0003y001EX0000000000005+cKE@LOaA|NaUv_0~WN&gWb#iQMX<{=kb#!TLFLGsca(OOrc~DC -M0u%!j00008045ZePV^x-v;GkP0CY0|03ZMW00000000000HlFz*b@M7X>c!Jc4cm4Z*nhna%^mAVly -vwbZKlab8~E8E^v8JO928D0~7!N00;mk6qrs`QqCZA3jhEeCIA2<00000000000001_fvD;e0B~t=FJ -E?LZe(wAFLiQkY-wUMFLiWjY%g?aZDntDbS`jtP)h*<6ay3h000O8CKQ-X!yauB3<&@LR~Y~R9{>OV0 -000000000q=8)a698~&a4%nWWo~3|axZmqY;0*_GcR>?X>2cba%?Vec~DCM0u%!j00008045ZePHv~R -aJ~ls02>tm03ZMW00000000000HlGX{}TXkX>c!Jc4cm4Z*nhna%^mAVlyvwbZKlacVTICE^v8JO928 -D0~7!N00;mk6qrtU&>wL!3jhF9DF6T@00000000000001_fuRQ!0B~t=FJE?LZe(wAFLz~PWo~0{WNB -_^b1z?CX>MtBUtcb8c~DCM0u%!j00008045ZePRFeXiv -c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZY++($Y;!Jfc~DCM0u%!j00008045ZePEda_A4CEG02u`U03-ka0 -0000000000HlFh8596;X>c!Jc4cm4Z*nhpWnyJ+V{c?>ZfA2ZZEI{{Vr6V|E^v8JO928D0~7!N00;mk -6qrtDIB=)A1pok}82|tw00000000000001_f#4ey0B~t=FJE?LZe(wAFLz~PWo~0{WNB_^b1!sdb98e -qaCuNm0Rj{Q6aWAK2mmG&m`>XU{MMxe007ev001fg0000000000005+c*dY`EaA|NaUv_0~WN&gWcV% -K_Zewp`X>Mn8FL+;db7gX0WMyV)Ze?UHaCuNm1qJ{B001)qGXYg!006}%6aWAK -""" - - -if __name__ == "__main__": - main() diff --git a/data/test.py b/data/test.py deleted file mode 100644 index 3691ddf33598..000000000000 --- a/data/test.py +++ /dev/null @@ -1,2 +0,0 @@ -#%% -print('hello') \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000000..8e1aa990a2c2 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,393 @@ +/** + * ESLint Configuration for VS Code Python Extension + * This file configures linting rules for the TypeScript/JavaScript codebase. + * It uses the new flat config format introduced in ESLint 8.21.0 + */ + +// Import essential ESLint plugins and configurations +import tseslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import noOnlyTests from 'eslint-plugin-no-only-tests'; +import prettier from 'eslint-config-prettier'; +import importPlugin from 'eslint-plugin-import'; +import js from '@eslint/js'; +import noBadGdprCommentPlugin from './.eslintplugin/no-bad-gdpr-comment.js'; // Ensure the path is correct + +export default [ + { + ignores: ['**/node_modules/**', '**/out/**'], + }, + // Base configuration for all files + { + ignores: [ + '**/node_modules/**', + '**/out/**', + 'src/test/analysisEngineTest.ts', + 'src/test/ciConstants.ts', + 'src/test/common.ts', + 'src/test/constants.ts', + 'src/test/core.ts', + 'src/test/extension-version.functional.test.ts', + 'src/test/fixtures.ts', + 'src/test/index.ts', + 'src/test/initialize.ts', + 'src/test/mockClasses.ts', + 'src/test/performanceTest.ts', + 'src/test/proc.ts', + 'src/test/smokeTest.ts', + 'src/test/standardTest.ts', + 'src/test/startupTelemetry.unit.test.ts', + 'src/test/testBootstrap.ts', + 'src/test/testLogger.ts', + 'src/test/testRunner.ts', + 'src/test/textUtils.ts', + 'src/test/unittests.ts', + 'src/test/vscode-mock.ts', + 'src/test/interpreters/mocks.ts', + 'src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts', + 'src/test/interpreters/pythonPathUpdaterFactory.unit.test.ts', + 'src/test/interpreters/activation/service.unit.test.ts', + 'src/test/interpreters/helpers.unit.test.ts', + 'src/test/interpreters/display.unit.test.ts', + 'src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts', + 'src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts', + 'src/test/terminals/codeExecution/djangoShellCodeExect.unit.test.ts', + 'src/test/activation/activeResource.unit.test.ts', + 'src/test/activation/extensionSurvey.unit.test.ts', + 'src/test/utils/fs.ts', + 'src/test/api.functional.test.ts', + 'src/test/testing/common/debugLauncher.unit.test.ts', + 'src/test/testing/common/services/configSettingService.unit.test.ts', + 'src/test/common/exitCIAfterTestReporter.ts', + 'src/test/common/terminals/activator/index.unit.test.ts', + 'src/test/common/terminals/activator/base.unit.test.ts', + 'src/test/common/terminals/shellDetector.unit.test.ts', + 'src/test/common/terminals/service.unit.test.ts', + 'src/test/common/terminals/helper.unit.test.ts', + 'src/test/common/terminals/activation.unit.test.ts', + 'src/test/common/terminals/shellDetectors/shellDetectors.unit.test.ts', + 'src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts', + 'src/test/common/socketStream.test.ts', + 'src/test/common/configSettings.test.ts', + 'src/test/common/experiments/telemetry.unit.test.ts', + 'src/test/common/platform/filesystem.unit.test.ts', + 'src/test/common/platform/errors.unit.test.ts', + 'src/test/common/platform/utils.ts', + 'src/test/common/platform/fs-temp.unit.test.ts', + 'src/test/common/platform/fs-temp.functional.test.ts', + 'src/test/common/platform/filesystem.functional.test.ts', + 'src/test/common/platform/filesystem.test.ts', + 'src/test/common/utils/cacheUtils.unit.test.ts', + 'src/test/common/utils/decorators.unit.test.ts', + 'src/test/common/utils/version.unit.test.ts', + 'src/test/common/configSettings/configSettings.unit.test.ts', + 'src/test/common/serviceRegistry.unit.test.ts', + 'src/test/common/extensions.unit.test.ts', + 'src/test/common/variables/envVarsService.unit.test.ts', + 'src/test/common/helpers.test.ts', + 'src/test/common/application/commands/reloadCommand.unit.test.ts', + 'src/test/common/installer/channelManager.unit.test.ts', + 'src/test/common/installer/pipInstaller.unit.test.ts', + 'src/test/common/installer/pipEnvInstaller.unit.test.ts', + 'src/test/common/socketCallbackHandler.test.ts', + 'src/test/common/process/decoder.test.ts', + 'src/test/common/process/processFactory.unit.test.ts', + 'src/test/common/process/pythonToolService.unit.test.ts', + 'src/test/common/process/proc.observable.test.ts', + 'src/test/common/process/logger.unit.test.ts', + 'src/test/common/process/proc.exec.test.ts', + 'src/test/common/process/pythonProcess.unit.test.ts', + 'src/test/common/process/proc.unit.test.ts', + 'src/test/common/interpreterPathService.unit.test.ts', + 'src/test/debugger/extension/adapter/adapter.test.ts', + 'src/test/debugger/extension/adapter/outdatedDebuggerPrompt.unit.test.ts', + 'src/test/debugger/extension/adapter/factory.unit.test.ts', + 'src/test/debugger/extension/adapter/logging.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts', + 'src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts', + 'src/test/debugger/utils.ts', + 'src/test/debugger/envVars.test.ts', + 'src/test/telemetry/index.unit.test.ts', + 'src/test/telemetry/envFileTelemetry.unit.test.ts', + 'src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts', + 'src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts', + 'src/test/application/diagnostics/checks/envPathVariable.unit.test.ts', + 'src/test/application/diagnostics/applicationDiagnostics.unit.test.ts', + 'src/test/application/diagnostics/promptHandler.unit.test.ts', + 'src/test/application/diagnostics/commands/ignore.unit.test.ts', + 'src/test/performance/load.perf.test.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/base.ts', + 'src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts', + 'src/client/interpreter/configuration/pythonPathUpdaterServiceFactory.ts', + 'src/client/interpreter/configuration/services/globalUpdaterService.ts', + 'src/client/interpreter/configuration/services/workspaceUpdaterService.ts', + 'src/client/interpreter/configuration/services/workspaceFolderUpdaterService.ts', + 'src/client/interpreter/helpers.ts', + 'src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts', + 'src/client/interpreter/display/index.ts', + 'src/client/extension.ts', + 'src/client/startupTelemetry.ts', + 'src/client/terminals/codeExecution/terminalCodeExecution.ts', + 'src/client/terminals/codeExecution/codeExecutionManager.ts', + 'src/client/terminals/codeExecution/djangoContext.ts', + 'src/client/activation/commands.ts', + 'src/client/activation/progress.ts', + 'src/client/activation/extensionSurvey.ts', + 'src/client/activation/common/analysisOptions.ts', + 'src/client/activation/languageClientMiddleware.ts', + 'src/client/testing/serviceRegistry.ts', + 'src/client/testing/main.ts', + 'src/client/testing/configurationFactory.ts', + 'src/client/testing/common/constants.ts', + 'src/client/testing/common/testUtils.ts', + 'src/client/common/helpers.ts', + 'src/client/common/net/browser.ts', + 'src/client/common/net/socket/socketCallbackHandler.ts', + 'src/client/common/net/socket/socketServer.ts', + 'src/client/common/net/socket/SocketStream.ts', + 'src/client/common/contextKey.ts', + 'src/client/common/experiments/telemetry.ts', + 'src/client/common/platform/serviceRegistry.ts', + 'src/client/common/platform/errors.ts', + 'src/client/common/platform/fs-temp.ts', + 'src/client/common/platform/fs-paths.ts', + 'src/client/common/platform/registry.ts', + 'src/client/common/platform/pathUtils.ts', + 'src/client/common/persistentState.ts', + 'src/client/common/terminal/activator/base.ts', + 'src/client/common/terminal/activator/powershellFailedHandler.ts', + 'src/client/common/terminal/activator/index.ts', + 'src/client/common/terminal/helper.ts', + 'src/client/common/terminal/syncTerminalService.ts', + 'src/client/common/terminal/factory.ts', + 'src/client/common/terminal/commandPrompt.ts', + 'src/client/common/terminal/service.ts', + 'src/client/common/terminal/shellDetector.ts', + 'src/client/common/terminal/shellDetectors/userEnvironmentShellDetector.ts', + 'src/client/common/terminal/shellDetectors/vscEnvironmentShellDetector.ts', + 'src/client/common/terminal/shellDetectors/terminalNameShellDetector.ts', + 'src/client/common/terminal/shellDetectors/settingsShellDetector.ts', + 'src/client/common/terminal/shellDetectors/baseShellDetector.ts', + 'src/client/common/utils/decorators.ts', + 'src/client/common/utils/enum.ts', + 'src/client/common/utils/platform.ts', + 'src/client/common/utils/stopWatch.ts', + 'src/client/common/utils/random.ts', + 'src/client/common/utils/sysTypes.ts', + 'src/client/common/utils/misc.ts', + 'src/client/common/utils/cacheUtils.ts', + 'src/client/common/utils/workerPool.ts', + 'src/client/common/extensions.ts', + 'src/client/common/variables/serviceRegistry.ts', + 'src/client/common/variables/environment.ts', + 'src/client/common/variables/types.ts', + 'src/client/common/variables/systemVariables.ts', + 'src/client/common/cancellation.ts', + 'src/client/common/interpreterPathService.ts', + 'src/client/common/application/applicationShell.ts', + 'src/client/common/application/languageService.ts', + 'src/client/common/application/clipboard.ts', + 'src/client/common/application/workspace.ts', + 'src/client/common/application/debugSessionTelemetry.ts', + 'src/client/common/application/documentManager.ts', + 'src/client/common/application/debugService.ts', + 'src/client/common/application/commands/reloadCommand.ts', + 'src/client/common/application/terminalManager.ts', + 'src/client/common/application/applicationEnvironment.ts', + 'src/client/common/errors/errorUtils.ts', + 'src/client/common/installer/serviceRegistry.ts', + 'src/client/common/installer/channelManager.ts', + 'src/client/common/installer/moduleInstaller.ts', + 'src/client/common/installer/types.ts', + 'src/client/common/installer/pipEnvInstaller.ts', + 'src/client/common/installer/productService.ts', + 'src/client/common/installer/pipInstaller.ts', + 'src/client/common/installer/productPath.ts', + 'src/client/common/process/currentProcess.ts', + 'src/client/common/process/processFactory.ts', + 'src/client/common/process/serviceRegistry.ts', + 'src/client/common/process/pythonToolService.ts', + 'src/client/common/process/internal/python.ts', + 'src/client/common/process/internal/scripts/testing_tools.ts', + 'src/client/common/process/types.ts', + 'src/client/common/process/logger.ts', + 'src/client/common/process/pythonProcess.ts', + 'src/client/common/process/pythonEnvironment.ts', + 'src/client/common/process/decoder.ts', + 'src/client/debugger/extension/adapter/remoteLaunchers.ts', + 'src/client/debugger/extension/adapter/outdatedDebuggerPrompt.ts', + 'src/client/debugger/extension/adapter/factory.ts', + 'src/client/debugger/extension/adapter/activator.ts', + 'src/client/debugger/extension/adapter/logging.ts', + 'src/client/debugger/extension/hooks/eventHandlerDispatcher.ts', + 'src/client/debugger/extension/hooks/childProcessAttachService.ts', + 'src/client/debugger/extension/attachQuickPick/wmicProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/factory.ts', + 'src/client/debugger/extension/attachQuickPick/psProcessParser.ts', + 'src/client/debugger/extension/attachQuickPick/picker.ts', + 'src/client/application/serviceRegistry.ts', + 'src/client/application/diagnostics/base.ts', + 'src/client/application/diagnostics/applicationDiagnostics.ts', + 'src/client/application/diagnostics/filter.ts', + 'src/client/application/diagnostics/promptHandler.ts', + 'src/client/application/diagnostics/commands/base.ts', + 'src/client/application/diagnostics/commands/ignore.ts', + 'src/client/application/diagnostics/commands/factory.ts', + 'src/client/application/diagnostics/commands/execVSCCommand.ts', + 'src/client/application/diagnostics/commands/launchBrowser.ts', + ], + linterOptions: { + reportUnusedDisableDirectives: 'off', + }, + rules: { + ...js.configs.recommended.rules, + 'no-undef': 'off', + }, + }, + // TypeScript-specific configuration + { + files: ['**/*.ts', '**/*.tsx', '**/*.js', 'src', 'pythonExtensionApi/src'], + languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + }, + globals: { + ...(js.configs.recommended.languageOptions?.globals || {}), + mocha: true, + require: 'readonly', + process: 'readonly', + exports: 'readonly', + module: 'readonly', + __dirname: 'readonly', + __filename: 'readonly', + setTimeout: 'readonly', + setInterval: 'readonly', + clearTimeout: 'readonly', + clearInterval: 'readonly', + }, + }, + plugins: { + '@typescript-eslint': tseslint, + 'no-only-tests': noOnlyTests, + import: importPlugin, + prettier: prettier, + 'no-bad-gdpr-comment': noBadGdprCommentPlugin, // Register your plugin + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.ts'], + }, + }, + }, + rules: { + 'no-bad-gdpr-comment/no-bad-gdpr-comment': 'warn', // Enable your rule + // Base configurations + ...tseslint.configs.recommended.rules, + ...prettier.rules, + + // TypeScript-specific rules + '@typescript-eslint/ban-ts-comment': [ + 'error', + { + 'ts-ignore': 'allow-with-description', + }, + ], + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-loss-of-precision': 'off', + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + varsIgnorePattern: '^_', + argsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/no-var-requires': 'off', + '@typescript-eslint/no-use-before-define': [ + 'error', + { + functions: false, + }, + ], + + // Import rules + 'import/extensions': 'off', + 'import/namespace': 'off', + 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'off', + 'import/prefer-default-export': 'off', + + // Testing rules + 'no-only-tests/no-only-tests': [ + 'error', + { + block: ['test', 'suite'], + focus: ['only'], + }, + ], + + // Code style rules + 'linebreak-style': 'off', + 'no-bitwise': 'off', + 'no-console': 'off', + 'no-underscore-dangle': 'off', + 'operator-assignment': 'off', + 'func-names': 'off', + + // Error handling and control flow + 'no-empty': ['error', { allowEmptyCatch: true }], + 'no-async-promise-executor': 'off', + 'no-await-in-loop': 'off', + 'no-unreachable': 'off', + 'no-void': 'off', + + // Duplicates and overrides (TypeScript handles these) + 'no-dupe-class-members': 'off', + 'no-redeclare': 'off', + 'no-undef': 'off', + + // Miscellaneous rules + 'no-control-regex': 'off', + 'no-extend-native': 'off', + 'no-inner-declarations': 'off', + 'no-multi-str': 'off', + 'no-param-reassign': 'off', + 'no-prototype-builtins': 'off', + 'no-empty-function': 'off', + 'no-template-curly-in-string': 'off', + 'no-useless-escape': 'off', + 'no-extra-parentheses': 'off', + 'no-extra-paren': 'off', + '@typescript-eslint/no-extra-parens': 'off', + strict: 'off', + + // Restricted syntax + 'no-restricted-syntax': [ + 'error', + { + selector: 'ForInStatement', + message: + 'for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.', + }, + { + selector: 'LabeledStatement', + message: + 'Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.', + }, + { + selector: 'WithStatement', + message: + '`with` is disallowed in strict mode because it makes code impossible to predict and optimize.', + }, + ], + }, + }, +]; diff --git a/experiments.json b/experiments.json deleted file mode 100644 index 69bfb177fd1d..000000000000 --- a/experiments.json +++ /dev/null @@ -1,14 +0,0 @@ -[ - { - "name": "AlwaysDisplayTestExplorer - experiment", - "salt": "AlwaysDisplayTestExplorer", - "min": 80, - "max": 100 - }, - { - "name": "AlwaysDisplayTestExplorer - control", - "salt": "AlwaysDisplayTestExplorer", - "min": 0, - "max": 20 - } -] diff --git a/gulpfile.js b/gulpfile.js index 77bcb96cece7..0b919f16572a 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,117 +9,115 @@ 'use strict'; const gulp = require('gulp'); -const filter = require('gulp-filter'); -const es = require('event-stream'); -const tsfmt = require('typescript-formatter'); -const tslint = require('tslint'); -const relative = require('relative'); const ts = require('gulp-typescript'); -const cp = require('child_process'); const spawn = require('cross-spawn'); -const colors = require('colors/safe'); const path = require('path'); const del = require('del'); -const sourcemaps = require('gulp-sourcemaps'); -const fs = require('fs-extra'); const fsExtra = require('fs-extra'); const glob = require('glob'); const _ = require('lodash'); const nativeDependencyChecker = require('node-has-native-dependencies'); const flat = require('flat'); -const argv = require('yargs').argv; +const { argv } = require('yargs'); const os = require('os'); +const typescript = require('typescript'); -const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; - -const noop = function() {}; -/** - * Hygiene works by creating cascading subsets of all our files and - * passing them through a sequence of checks. Here are the current subsets, - * named according to the checks performed on them. Each subset contains - * the following one, as described in mathematical notation: - * - * all ⊃ indentation ⊃ typescript - */ - -const all = ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.d.ts', 'src/**/*.js', 'src/**/*.jsx']; +const tsProject = ts.createProject('./tsconfig.json', { typescript }); -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' -]; +const isCI = process.env.TRAVIS === 'true' || process.env.TF_BUILD !== undefined; -gulp.task('compile', done => { +gulp.task('compileCore', (done) => { let failed = false; - const tsProject = ts.createProject('tsconfig.json'); tsProject .src() .pipe(tsProject()) - .on('error', () => (failed = true)) + .on('error', () => { + failed = true; + }) .js.pipe(gulp.dest('out')) .on('finish', () => (failed ? done(new Error('TypeScript compilation errors')) : done())); }); -gulp.task('precommit', done => run({ exitOnError: true, mode: 'staged' }, done)); - -gulp.task('hygiene-watch', () => gulp.watch(tsFilter, gulp.series('hygiene-modified'))); - -gulp.task('hygiene', done => run({ mode: 'compile', skipFormatCheck: true, skipIndentationCheck: true }, done)); - -gulp.task('hygiene-modified', gulp.series('compile', done => run({ mode: 'changes' }, done))); - -gulp.task('watch', gulp.parallel('hygiene-modified', 'hygiene-watch')); - -// Duplicate to allow duplicate task in tasks.json (one ith problem matching, and one without) -gulp.task('watchProblems', gulp.parallel('hygiene-modified', 'hygiene-watch')); - -gulp.task('hygiene-watch-branch', () => gulp.watch(tsFilter, gulp.series('hygiene-branch'))); +gulp.task('compileApi', (done) => { + spawnAsync('npm', ['run', 'compileApi'], undefined, true) + .then((stdout) => { + if (stdout.includes('error')) { + done(new Error(stdout)); + } else { + done(); + } + }) + .catch((ex) => { + console.log(ex); + done(new Error('TypeScript compilation errors', ex)); + }); +}); -gulp.task('hygiene-all', done => run({ mode: 'all' }, done)); +gulp.task('compile', gulp.series('compileCore', 'compileApi')); -gulp.task('hygiene-branch', done => run({ mode: 'diffMaster' }, done)); +gulp.task('precommit', (done) => run({ exitOnError: true, mode: 'staged' }, done)); gulp.task('output:clean', () => del(['coverage'])); -gulp.task('clean:cleanExceptTests', () => del(['clean:vsix', 'out/client', 'out/datascience-ui', 'out/server'])); +gulp.task('clean:cleanExceptTests', () => del(['clean:vsix', 'out/client'])); gulp.task('clean:vsix', () => del(['*.vsix'])); gulp.task('clean:out', () => del(['out'])); gulp.task('clean', gulp.parallel('output:clean', 'clean:vsix', 'clean:out')); -gulp.task('checkNativeDependencies', done => { +gulp.task('checkNativeDependencies', (done) => { if (hasNativeDependencies()) { - done(new Error('Native dependencies deteced')); + done(new Error('Native dependencies detected')); } done(); }); -gulp.task('check-datascience-dependencies', () => checkDatascienceDependencies()); - -gulp.task('compile-webviews', async () => - spawnAsync('npx', ['-n', '--max_old_space_size=4096', 'webpack', '--config', 'webpack.datascience-ui.config.js', '--mode', 'production']) -); +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 () => { - await buildWebPack('production', []); - await buildWebPack('extension', ['--config', './build/webpack/webpack.extension.config.js']); - await buildWebPack('debugAdapter', ['--config', './build/webpack/webpack.debugadapter.config.js']); + // 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); }); @@ -131,10 +129,14 @@ async function updateBuildNumber(args) { 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; + 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'); @@ -142,7 +144,10 @@ async function updateBuildNumber(args) { // 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} (`); + 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'); @@ -152,24 +157,35 @@ async function updateBuildNumber(args) { } } -async function buildWebPack(webpackConfigName, args) { +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('npx', ['-n', '--max_old_space_size=4096', 'webpack', ...args, ...['--mode', 'production']], allowedWarnings); + 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); + .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')); + .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}, \n\n${stdOut}`); + throw new Error( + `Warnings in ${webpackConfigName}, Check gulpfile.js to see if the warning should be allowed., \n\n${stdOut}`, + ); } } function getAllowedWarningsForWebPack(buildConfig) { @@ -179,190 +195,74 @@ function getAllowedWarningsForWebPack(buildConfig) { 'WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).', 'WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.', 'WARNING in webpack performance recommendations:', - 'WARNING in ./node_modules/vsls/vscode.js', 'WARNING in ./node_modules/encoding/lib/iconv-loader.js', - 'WARNING in ./node_modules/ws/lib/BufferUtil.js', - 'WARNING in ./node_modules/ws/lib/buffer-util.js', - 'WARNING in ./node_modules/ws/lib/Validation.js', - 'WARNING in ./node_modules/ws/lib/validation.js' + 'WARNING in ./node_modules/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/ws/lib/BufferUtil.js', - 'WARNING in ./node_modules/ws/lib/buffer-util.js', - 'WARNING in ./node_modules/ws/lib/Validation.js', - 'WARNING in ./node_modules/ws/lib/validation.js' + 'WARNING in ./node_modules/any-promise/register.js', + 'remove-files-plugin@1.4.0:', + 'WARNING in ./node_modules/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']; + 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('renameSourceMaps', async () => { - // By default souce maps will be disabled in the extenion. - // Users will need to use the command `python.enableSourceMapSupport` to enable source maps. - const extensionSourceMap = path.join(__dirname, 'out', 'client', 'extension.js.map'); - const debuggerSourceMap = path.join(__dirname, 'out', 'client', 'debugger', 'debugAdapter', 'main.js.map'); - await fs.rename(extensionSourceMap, `${extensionSourceMap}.disabled`); - await fs.rename(debuggerSourceMap, `${debuggerSourceMap}.disabled`); -}); gulp.task('verifyBundle', async () => { const matches = await glob.sync(path.join(__dirname, '*.vsix')); - if (!matches || matches.length == 0) { + if (!matches || matches.length === 0) { throw new Error('Bundle does not exist'); } else { console.log(`Bundle ${matches[0]} exists.`); } }); -gulp.task('prePublishBundle', gulp.series('checkNativeDependencies', 'check-datascience-dependencies', 'compile', 'clean:cleanExceptTests', 'webpack', 'renameSourceMaps')); -gulp.task('prePublishNonBundle', gulp.series('checkNativeDependencies', 'check-datascience-dependencies', 'compile', 'compile-webviews')); - -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 uploadExtension(uploadBlobName) { - const azure = require('gulp-azure-storage'); - const rename = require('gulp-rename'); - return gulp - .src('*python*.vsix') - .pipe(rename(uploadBlobName)) - .pipe( - azure.upload({ - account: process.env.AZURE_STORAGE_ACCOUNT, - key: process.env.AZURE_STORAGE_ACCESS_KEY, - container: process.env.AZURE_STORAGE_CONTAINER - }) - ); -} - -gulp.task('uploadDeveloperExtension', () => uploadExtension('ms-python-insiders.vsix')); -gulp.task('uploadReleaseExtension', () => uploadExtension(`ms-python-${process.env.TRAVIS_BRANCH || process.env.BUILD_SOURCEBRANCHNAME}.vsix`)); +gulp.task('prePublishBundle', gulp.series('webpack')); +gulp.task('checkDependencies', gulp.series('checkNativeDependencies')); +gulp.task('prePublishNonBundle', gulp.series('compile')); -function spawnAsync(command, args) { +function spawnAsync(command, args, env, rejectOnStdErr = false) { + env = env || {}; + env = { ...process.env, ...env }; return new Promise((resolve, reject) => { let stdOut = ''; - const proc = spawn(command, args, { cwd: __dirname }); - proc.stdout.on('data', data => { + 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())); - proc.on('close', () => resolve(stdOut)); - 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.children.forEach(c => { - c.chunks[0].modules.forEach(m => { - const name = m.name; - if (!name.startsWith('./node_modules')) { - return; - } - - let nameWithoutNodeModules = name.substring('./node_modules'.length); - // Special case expose-loader. - if (nameWithoutNodeModules.startsWith('/expose-loader')) { - nameWithoutNodeModules = nameWithoutNodeModules.substring(nameWithoutNodeModules.indexOf('./node_modules') + './node_modules'.length); - } - - let moduleName1 = nameWithoutNodeModules.split('/')[1]; - moduleName1 = moduleName1.endsWith('!.') ? moduleName1.substring(0, moduleName1.length - 2) : moduleName1; - const moduleName2 = `${nameWithoutNodeModules.split('/')[1]}/${nameWithoutNodeModules.split('/')[2]}`; - - 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); + proc.stderr.on('data', (data) => { + console.error(data.toString()); + if (rejectOnStdErr) { + reject(data.toString()); } - if (existingModules.has(moduleName) || newModules.has(moduleName)) { - return; - } - newModules.add(moduleName); }); + proc.on('close', () => resolve(stdOut)); + proc.on('error', (error) => reject(error)); }); - - 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) { @@ -370,453 +270,20 @@ function hasNativeDependencies() { } const dependencies = JSON.parse(spawn.sync('npm', ['ls', '--json', '--prod']).stdout.toString()); const jsonProperties = Object.keys(flat.flatten(dependencies)); - nativeDependencies = _.flatMap(nativeDependencies, item => path.dirname(item.substring(item.indexOf('node_modules') + 'node_modules'.length)).split(path.sep)) - .filter(item => item.length > 0) - .filter(item => jsonProperties.findIndex(flattenedDependency => flattenedDependency.endsWith(`dependencies.${item}.version`)) >= 0); + 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; } - -/** - * @typedef {Object} hygieneOptions - creates a new type named 'SpecialType' - * @property {'changes'|'staged'|'all'|'compile'|'diffMaster'} [mode=] - Mode. - * @property {boolean=} skipIndentationCheck - Skip indentation checks. - * @property {boolean=} skipFormatCheck - Skip format checks. - * @property {boolean=} skipLinter - Skip linter. - */ - -/** - * - * @param {hygieneOptions} options - */ -function getTsProject(options) { - return ts.createProject('tsconfig.json'); -} - -let configuration; -/** - * - * @param {hygieneOptions} options - */ -function getLinter(options) { - configuration = configuration ? configuration : tslint.Configuration.findConfiguration(null, '.'); - const program = tslint.Linter.createProgram('./tsconfig.json'); - const linter = new tslint.Linter({ formatter: 'json' }, program); - return { linter, configuration }; -} -let compilationInProgress = false; -let reRunCompilation = false; -/** - * - * @param {hygieneOptions} options - * @returns {NodeJS.ReadWriteStream} - */ -const hygiene = (options, done) => { - done = done || noop; - if (compilationInProgress) { - reRunCompilation = true; - return done(); - } - const fileListToProcess = options.mode === 'compile' ? undefined : getFileListToProcess(options); - if (Array.isArray(fileListToProcess) && fileListToProcess !== all && fileListToProcess.filter(item => item.endsWith('.ts')).length === 0) { - return done(); - } - - const started = new Date().getTime(); - compilationInProgress = true; - options = options || {}; - let errorCount = 0; - - const indentation = es.through(function(file) { - file.contents - .toString('utf8') - .split(/\r\n|\r|\n/) - .forEach((line, i) => { - if (/^\s*$/.test(line) || /^\S+.*$/.test(line)) { - // Empty or whitespace lines are OK. - } else if (/^(\s\s\s\s)+.*/.test(line)) { - // Good indent. - } else if (/^[\t]+.*/.test(line)) { - console.error(file.relative + '(' + (i + 1) + ',1): Bad whitespace indentation (use 4 spaces instead of tabs or other)'); - errorCount++; - } - }); - - this.emit('data', file); - }); - - const formatOptions = { verify: true, tsconfig: true, tslint: true, editorconfig: true, tsfmt: true }; - const formatting = es.map(function(file, cb) { - tsfmt - .processString(file.path, file.contents.toString('utf8'), formatOptions) - .then(result => { - if (result.error) { - let message = result.message.trim(); - let formattedMessage = ''; - if (message.startsWith(__dirname)) { - message = message.substr(__dirname.length); - message = message.startsWith(path.sep) ? message.substr(1) : message; - const index = message.indexOf('.ts '); - if (index === -1) { - formattedMessage = colors.red(message); - } else { - const file = message.substr(0, index + 3); - const errorMessage = message.substr(index + 4).trim(); - formattedMessage = `${colors.red(file)} ${errorMessage}`; - } - } else { - formattedMessage = colors.red(message); - } - console.error(formattedMessage); - errorCount++; - } - cb(null, file); - }) - .catch(cb); - }); - - let reportedLinterFailures = []; - /** - * Report the linter failures - * @param {any[]} failures - */ - function reportLinterFailures(failures) { - return ( - failures - .map(failure => { - const name = failure.name || failure.fileName; - const position = failure.startPosition; - const line = position.lineAndCharacter ? position.lineAndCharacter.line : position.line; - const character = position.lineAndCharacter ? position.lineAndCharacter.character : position.character; - - // Output in format similar to tslint for the linter to pickup. - const message = `ERROR: (${failure.ruleName}) ${relative(__dirname, name)}[${line + 1}, ${character + 1}]: ${failure.failure}`; - if (reportedLinterFailures.indexOf(message) === -1) { - console.error(message); - reportedLinterFailures.push(message); - return true; - } else { - return false; - } - }) - .filter(reported => reported === true).length > 0 - ); - } - - const { linter, configuration } = getLinter(options); - const tsl = es.through(function(file) { - const contents = file.contents.toString('utf8'); - if (isCI) { - // Don't print anything to the console, we'll do that. - console.log('.'); - } - // Yes this is a hack, but tslinter doesn't provide an option to prevent this. - const oldWarn = console.warn; - console.warn = () => {}; - linter.failures = []; - linter.fixes = []; - linter.lint(file.relative, contents, configuration.results); - console.warn = oldWarn; - const result = linter.getResult(); - if (result.failureCount > 0 || result.errorCount > 0) { - const reported = reportLinterFailures(result.failures); - if (result.failureCount && reported) { - errorCount += result.failureCount; - } - if (result.errorCount && reported) { - errorCount += result.errorCount; - } - } - this.emit('data', file); - }); - - const tsFiles = []; - const tscFilesTracker = es.through(function(file) { - tsFiles.push(file.path.replace(/\\/g, '/')); - tsFiles.push(file.path); - this.emit('data', file); - }); - - const tsProject = getTsProject(options); - - const tsc = function() { - function customReporter() { - return { - error: function(error, typescript) { - const fullFilename = error.fullFilename || ''; - const relativeFilename = error.relativeFilename || ''; - if (tsFiles.findIndex(file => fullFilename === file || relativeFilename === file) === -1) { - return; - } - console.error(`Error: ${error.message}`); - errorCount += 1; - }, - finish: function() { - // forget the summary. - console.log('Finished compilation'); - } - }; - } - const reporter = customReporter(); - return tsProject(reporter); - }; - - const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(fileListToProcess); - const dest = options.mode === 'compile' ? './out' : '.'; - let result = files.pipe(filter(f => f && f.stat && !f.stat.isDirectory())); - - if (!options.skipIndentationCheck) { - result = result.pipe(filter(indentationFilter)).pipe(indentation); - } - - result = result.pipe(filter(tslintFilter)); - - if (!options.skipFormatCheck) { - // result = result - // .pipe(formatting); - } - - if (!options.skipLinter) { - result = result.pipe(tsl); - } - let totalTime = 0; - result = result - .pipe(tscFilesTracker) - .pipe(sourcemaps.init()) - .pipe(tsc()) - .pipe( - sourcemaps.mapSources(function(sourcePath, file) { - let tsFileName = path.basename(file.path).replace(/js$/, 'ts'); - const qualifiedSourcePath = path - .dirname(file.path) - .replace('out/', 'src/') - .replace('out\\', 'src\\'); - if (!fs.existsSync(path.join(qualifiedSourcePath, tsFileName))) { - const tsxFileName = path.basename(file.path).replace(/js$/, 'tsx'); - if (!fs.existsSync(path.join(qualifiedSourcePath, tsxFileName))) { - console.error(`ERROR: (source-maps) ${file.path}[1,1]: Source file not found`); - } else { - tsFileName = tsxFileName; - } - } - return path.join(path.relative(path.dirname(file.path), qualifiedSourcePath), tsFileName); - }) - ) - .pipe(sourcemaps.write('.', { includeContent: false })) - .pipe(gulp.dest(dest)) - .pipe( - es.through(null, function() { - if (errorCount > 0) { - const errorMessage = `Hygiene failed with errors 👎 . Check 'gulpfile.js' (completed in ${new Date().getTime() - started}ms).`; - console.error(colors.red(errorMessage)); - exitHandler(options); - } else { - console.log(colors.green(`Hygiene passed with 0 errors 👍 (completed in ${new Date().getTime() - started}ms).`)); - } - // Reset error counter. - errorCount = 0; - reportedLinterFailures = []; - compilationInProgress = false; - if (reRunCompilation) { - reRunCompilation = false; - setTimeout(() => { - hygiene(options, done); - }, 10); - } - done(); - this.emit('end'); - }) - ) - .on('error', ex => { - exitHandler(options, ex); - done(); - }); - - return result; -}; - -/** - * @typedef {Object} runOptions - * @property {boolean=} exitOnError - Exit on error. - * @property {'changes'|'staged'|'all'} [mode=] - Mode. - * @property {string[]=} files - Optional list of files to be modified. - * @property {boolean=} skipIndentationCheck - Skip indentation checks. - * @property {boolean=} skipFormatCheck - Skip format checks. - * @property {boolean=} skipLinter - Skip linter. - * @property {boolean=} watch - Watch mode. - */ - -/** - * Run the linters. - * @param {runOptions} options - * @param {Error} ex - */ -function exitHandler(options, ex) { - console.error(); - if (ex) { - console.error(ex); - console.error(colors.red(ex)); - } - if (options.exitOnError) { - console.log('exit'); - process.exit(1); - } -} - -/** - * Run the linters. - * @param {runOptions} options - */ -function run(options, done) { - done = done || noop; - options = options ? options : {}; - options.exitOnError = typeof options.exitOnError === 'undefined' ? isCI : options.exitOnError; - process.once('unhandledRejection', (reason, p) => { - console.log('Unhandled Rejection at: Promise', p, 'reason:', reason); - exitHandler(options); - }); - - // Clear screen each time - console.log('\x1Bc'); - const startMessage = `Hygiene starting`; - console.log(colors.blue(startMessage)); - - hygiene(options, done); -} - -function git(args) { - let result = cp.spawnSync('git', args, { encoding: 'utf-8' }); - return result.output.join('\n'); -} - -function getStagedFilesSync() { - const out = git(['diff', '--cached', '--name-only']); - return out.split(/\r?\n/).filter(l => !!l); -} -function getAddedFilesSync() { - const out = git(['status', '-u', '-s']); - return out - .split(/\r?\n/) - .filter(l => !!l) - .filter( - l => - _.intersection( - ['A', '?', 'U'], - l - .substring(0, 2) - .trim() - .split('') - ).length > 0 - ) - .map(l => path.join(__dirname, l.substring(2).trim())); -} -function getAzureDevOpsVarValue(varName) { - return process.env[varName.replace(/\./g, '_').toUpperCase()]; -} -function getModifiedFilesSync() { - if (isCI) { - const isAzurePR = getAzureDevOpsVarValue('System.PullRequest.SourceBranch') !== undefined; - const isTravisPR = process.env.TRAVIS_PULL_REQUEST !== undefined && process.env.TRAVIS_PULL_REQUEST !== 'true'; - if (!isAzurePR && !isTravisPR) { - return []; - } - const targetBranch = process.env.TRAVIS_BRANCH || getAzureDevOpsVarValue('System.PullRequest.TargetBranch'); - if (targetBranch !== 'master') { - return []; - } - - const repo = process.env.TRAVIS_REPO_SLUG || getAzureDevOpsVarValue('Build.Repository.Name'); - const originOrUpstream = repo.toUpperCase() === 'MICROSOFT/VSCODE-PYTHON' || repo.toUpperCase() === 'VSCODE-PYTHON-DATASCIENCE/VSCODE-PYTHON' ? 'origin' : 'upstream'; - - // If on CI, get a list of modified files comparing against - // PR branch and master of current (assumed 'origin') repo. - try { - cp.execSync(`git remote set-branches --add ${originOrUpstream} master`, { encoding: 'utf8', cwd: __dirname }); - cp.execSync('git fetch', { encoding: 'utf8', cwd: __dirname }); - } catch (ex) { - return []; - } - const cmd = `git diff --name-only HEAD ${originOrUpstream}/master`; - console.info(cmd); - const out = cp.execSync(cmd, { encoding: 'utf8', cwd: __dirname }); - return out - .split(/\r?\n/) - .filter(l => !!l) - .filter(l => l.length > 0) - .map(l => l.trim().replace(/\//g, path.sep)) - .map(l => path.join(__dirname, l)); - } else { - const out = cp.execSync('git status -u -s', { encoding: 'utf8' }); - return out - .split(/\r?\n/) - .filter(l => !!l) - .filter( - l => - _.intersection( - ['M', 'A', 'R', 'C', 'U', '?'], - l - .substring(0, 2) - .trim() - .split('') - ).length > 0 - ) - .map(l => - path.join( - __dirname, - l - .substring(2) - .trim() - .replace(/\//g, path.sep) - ) - ); - } -} - -function getDifferentFromMasterFilesSync() { - const out = git(['diff', '--name-status', 'master']); - return out - .split(/\r?\n/) - .filter(l => !!l) - .map(l => path.join(__dirname, l.substring(2).trim())); -} - -/** - * @param {hygieneOptions} options - */ -function getFilesToProcess(fileList) { - const gulpSrcOptions = { base: '.' }; - return gulp.src(fileList, gulpSrcOptions); -} - -/** - * @param {hygieneOptions} options - */ -function getFileListToProcess(options) { - const mode = options ? options.mode : 'all'; - const gulpSrcOptions = { base: '.' }; - - // If we need only modified files, then filter the glob. - if (options && options.mode === 'changes') { - return getModifiedFilesSync().filter(f => fs.existsSync(f)); - } - - if (options && options.mode === 'staged') { - return getStagedFilesSync().filter(f => fs.existsSync(f)); - } - - if (options && options.mode === 'diffMaster') { - return getDifferentFromMasterFilesSync().filter(f => fs.existsSync(f)); - } - - return all; -} - -exports.hygiene = hygiene; - -// this allows us to run hygiene via CLI (e.g. `node gulfile.js`). -if (require.main === module) { - run({ exitOnError: true, mode: 'staged' }, () => {}); -} diff --git a/images/ConfigureDebugger.gif b/images/ConfigureDebugger.gif index 359e1a7493fd..41113d65896d 100644 Binary files a/images/ConfigureDebugger.gif and b/images/ConfigureDebugger.gif differ diff --git a/images/ConfigureTests.gif b/images/ConfigureTests.gif index 6da0100d593b..38ae2db551e1 100644 Binary files a/images/ConfigureTests.gif and b/images/ConfigureTests.gif differ diff --git a/images/InterpreterSelectionZoom.gif b/images/InterpreterSelectionZoom.gif index 576a438a7d3b..dc5db03aad3d 100644 Binary files a/images/InterpreterSelectionZoom.gif and b/images/InterpreterSelectionZoom.gif differ diff --git a/images/OpenOrCreateNotebook.gif b/images/OpenOrCreateNotebook.gif 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/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/.vscode/settings.json b/news/.vscode/settings.json deleted file mode 100644 index 3875b000e2c0..000000000000 --- a/news/.vscode/settings.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "python.jediEnabled": false, - "python.formatting.provider": "black", - "editor.formatOnSave": true, - "python.testing.pytestArgs": [ - "." - ], - "python.testing.unittestEnabled": false, - "python.testing.nosetestsEnabled": false, - "python.testing.pytestEnabled": true -} diff --git a/news/1 Enhancements/5900.md b/news/1 Enhancements/5900.md deleted file mode 100644 index 5ac3fe2eec00..000000000000 --- a/news/1 Enhancements/5900.md +++ /dev/null @@ -1 +0,0 @@ -Hook up ptvsd debugger to jupyter UI \ No newline at end of file diff --git a/news/1 Enhancements/6318.md b/news/1 Enhancements/6318.md deleted file mode 100644 index 03a5290bebe1..000000000000 --- a/news/1 Enhancements/6318.md +++ /dev/null @@ -1 +0,0 @@ -Provide code mapping service for debugging cells. diff --git a/news/1 Enhancements/6350.md b/news/1 Enhancements/6350.md deleted file mode 100644 index 2d577178d028..000000000000 --- a/news/1 Enhancements/6350.md +++ /dev/null @@ -1 +0,0 @@ -Change copy back to code button in the interactive window to insert wherever the current selection is. diff --git a/news/1 Enhancements/6376.md b/news/1 Enhancements/6376.md deleted file mode 100644 index c868933ca27a..000000000000 --- a/news/1 Enhancements/6376.md +++ /dev/null @@ -1 +0,0 @@ -Support hitting breakpoints in actual source code for interactive window debugging. 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/5756.md b/news/2 Fixes/5756.md deleted file mode 100644 index 3b0dad7ee161..000000000000 --- a/news/2 Fixes/5756.md +++ /dev/null @@ -1 +0,0 @@ -Append `--allow-prereleases` to black installation command so pipenv can properly resolve it. diff --git a/news/2 Fixes/6270.md b/news/2 Fixes/6270.md deleted file mode 100644 index 013d2b28bd23..000000000000 --- a/news/2 Fixes/6270.md +++ /dev/null @@ -1 +0,0 @@ -Opting out of telemetry correctly opts out of A/B testing diff --git a/news/2 Fixes/6273.md b/news/2 Fixes/6273.md deleted file mode 100644 index 96a19e69b7d6..000000000000 --- a/news/2 Fixes/6273.md +++ /dev/null @@ -1 +0,0 @@ -Add error messages if data_rate_limit is exceeded on remote (or local) connection. diff --git a/news/2 Fixes/6336.md b/news/2 Fixes/6336.md deleted file mode 100644 index 293bcfa113de..000000000000 --- a/news/2 Fixes/6336.md +++ /dev/null @@ -1 +0,0 @@ -Add new plot viewer button images and fix button colors in different themes. diff --git a/news/2 Fixes/6344.md b/news/2 Fixes/6344.md deleted file mode 100644 index cba8947b076a..000000000000 --- a/news/2 Fixes/6344.md +++ /dev/null @@ -1 +0,0 @@ -Fix png scaling on non standard DPI. Add 'enablePlotViewer' setting to allow user to render pngs instead of svg files. diff --git a/news/2 Fixes/6386.md b/news/2 Fixes/6386.md deleted file mode 100644 index 12fa41a3c651..000000000000 --- a/news/2 Fixes/6386.md +++ /dev/null @@ -1 +0,0 @@ -Add missing keys for data science interactive window button tooltips in package.nls.json 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/4692.md b/news/3 Code Health/4692.md deleted file mode 100644 index cceb0541f692..000000000000 --- a/news/3 Code Health/4692.md +++ /dev/null @@ -1 +0,0 @@ -UI Tests using [selenium](https://selenium-python.readthedocs.io/index.html) & [behave](https://behave.readthedocs.io/en/latest/). \ No newline at end of file diff --git a/news/3 Code Health/5999.md b/news/3 Code Health/5999.md deleted file mode 100644 index 302b2a83faff..000000000000 --- a/news/3 Code Health/5999.md +++ /dev/null @@ -1 +0,0 @@ -Upload coverage reports to [coveralls](https://coveralls.io/github/microsoft/vscode-python). \ No newline at end of file diff --git a/news/3 Code Health/6013.md b/news/3 Code Health/6013.md deleted file mode 100644 index 252b47ebe2d1..000000000000 --- a/news/3 Code Health/6013.md +++ /dev/null @@ -1 +0,0 @@ -Upgrade Jedi to version 0.13.3. diff --git a/news/3 Code Health/6212.md b/news/3 Code Health/6212.md deleted file mode 100644 index 19126f1154d5..000000000000 --- a/news/3 Code Health/6212.md +++ /dev/null @@ -1 +0,0 @@ -Remove test.ipynb from root folder. diff --git a/news/3 Code Health/6253.md b/news/3 Code Health/6253.md deleted file mode 100644 index d5f07a00b867..000000000000 --- a/news/3 Code Health/6253.md +++ /dev/null @@ -1 +0,0 @@ -Fail the `smoke tests` CI job when the smoke tests fail. diff --git a/news/3 Code Health/6283.md b/news/3 Code Health/6283.md deleted file mode 100644 index 53a7cbf247d5..000000000000 --- a/news/3 Code Health/6283.md +++ /dev/null @@ -1 +0,0 @@ -Add a bunch of perf measurements to telemetry. diff --git a/news/3 Code Health/6322.md b/news/3 Code Health/6322.md deleted file mode 100644 index 33f7837b8547..000000000000 --- a/news/3 Code Health/6322.md +++ /dev/null @@ -1 +0,0 @@ -Retry failing debugger test (retry due to intermittent issues on `Azure Pipelines`). 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 d4d7dd1cfd66..000000000000 --- a/news/announce.py +++ /dev/null @@ -1,193 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Generate the changelog. - -Usage: announce [--dry_run | --interim | --final] [--update=] [] - -""" -import dataclasses -import datetime -import enum -import json -import operator -import os -import pathlib -import re -import subprocess -import sys - -import docopt - - -FILENAME_RE = re.compile(r"(?P\d+)(?P-\S+)?\.md") - - -@dataclasses.dataclass -class NewsEntry: - """Representation of a news entry.""" - - issue_number: int - description: str - path: pathlib.Path - - -def news_entries(directory): - """Yield news entries in the directory. - - Entries are sorted by issue number. - - """ - entries = [] - for path in directory.iterdir(): - if path.name == "README.md": - continue - match = FILENAME_RE.match(path.name) - if match is None: - raise ValueError(f"{path} has a bad file name") - issue = int(match.group("issue")) - try: - entry = path.read_text("utf-8") - except UnicodeDecodeError as exc: - raise ValueError(f"'{path}' is not encoded as UTF-8") from exc - if "\ufeff" in entry: - raise ValueError(f"'{path}' contains the BOM") - entries.append(NewsEntry(issue, entry, path)) - entries.sort(key=operator.attrgetter("issue_number")) - yield from entries - - -@dataclasses.dataclass -class SectionTitle: - """Create a data object for a section of the changelog.""" - - index: int - title: str - path: pathlib.Path - - -def sections(directory): - """Yield the sections in their appropriate order.""" - found = [] - for path in directory.iterdir(): - if not path.is_dir() or path.name.startswith((".", "_")): - continue - position, sep, title = path.name.partition(" ") - if not sep: - print( - f"directory {path.name!r} is missing a ranking; skipping", - file=sys.stderr, - ) - continue - found.append(SectionTitle(int(position), title, path)) - return sorted(found, key=operator.attrgetter("index")) - - -def gather(directory): - """Gather all the entries together.""" - data = [] - for section in sections(directory): - data.append((section, list(news_entries(section.path)))) - return data - - -def entry_markdown(entry): - """Generate the Markdown for the specified entry.""" - enumerated_item = "1. " - indent = " " * len(enumerated_item) - issue_url = ( - f"https://github.com/Microsoft/vscode-python/issues/{entry.issue_number}" - ) - issue_md = f"([#{entry.issue_number}]({issue_url}))" - entry_lines = entry.description.strip().splitlines() - formatted_lines = [f"{enumerated_item}{entry_lines[0]}"] - formatted_lines.extend(f"{indent}{line}" for line in entry_lines[1:]) - formatted_lines.append(f"{indent}{issue_md}") - return "\n".join(formatted_lines) - - -def changelog_markdown(data): - """Generate the Markdown for the release.""" - changelog = [] - for section, entries in data: - changelog.append(f"### {section.title}") - changelog.append("") - changelog.extend(map(entry_markdown, entries)) - changelog.append("") - return "\n".join(changelog) - - -def git_rm(path): - """Run git-rm on the path.""" - status = subprocess.run( - ["git", "rm", os.fspath(path.resolve())], - shell=False, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - ) - try: - status.check_returncode() - except Exception: - print(status.stdout, file=sys.stderr) - raise - - -def cleanup(data): - """Remove news entries from git and disk.""" - for section, entries in data: - for entry in entries: - git_rm(entry.path) - - -class RunType(enum.Enum): - """Possible run-time options.""" - - dry_run = 0 - interim = 1 - final = 2 - - -def complete_news(version, entry, previous_news): - """Prepend a news entry to the previous news file.""" - title, _, previous_news = previous_news.partition("\n") - title = title.strip() - previous_news = previous_news.strip() - section_title = (f"## {version} ({datetime.date.today().strftime('%d %B %Y')})" - ).replace("(0", "(") - # TODO: Insert the "Thank you!" section (in monthly releases)? - return f"{title}\n\n{section_title}\n\n{entry.strip()}\n\n\n{previous_news}" - - -def main(run_type, directory, news_file=None): - directory = pathlib.Path(directory) - data = gather(directory) - markdown = changelog_markdown(data) - if news_file: - with open(news_file, "r", encoding="utf-8") as file: - previous_news = file.read() - package_config_path = pathlib.Path(news_file).parent / "package.json" - config = json.loads(package_config_path.read_text()) - new_news = complete_news(config["version"], markdown, previous_news) - if run_type == RunType.dry_run: - print(f"would be written to {news_file}:") - print() - print(new_news) - else: - with open(news_file, "w", encoding="utf-8") as file: - file.write(new_news) - else: - print(markdown) - if run_type == RunType.final: - cleanup(data) - - -if __name__ == "__main__": - arguments = docopt.docopt(__doc__) - for possible_run_type in RunType: - if arguments[f"--{possible_run_type.name}"]: - run_type = possible_run_type - break - else: - run_type = RunType.interim - directory = arguments[""] or pathlib.Path(__file__).parent - main(run_type, directory, arguments["--update"]) diff --git a/news/requirements.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 acc125a7c360..000000000000 --- a/news/test_announce.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import codecs -import datetime -import pathlib - -import docopt -import pytest - -import announce as ann - - -@pytest.fixture -def directory(tmpdir): - """Fixture to create a temp directory wrapped in a pathlib.Path object.""" - return pathlib.Path(tmpdir) - - -def test_news_entry_formatting(directory): - issue = 42 - normal_entry = directory / f"{issue}.md" - nonce_entry = directory / f"{issue}-nonce.md" - body = "Hello, world!" - normal_entry.write_text(body, encoding="utf-8") - nonce_entry.write_text(body, encoding="utf-8") - results = list(ann.news_entries(directory)) - assert len(results) == 2 - for result in results: - assert result.issue_number == issue - assert result.description == body - - -def test_news_entry_sorting(directory): - oldest_entry = directory / "45.md" - newest_entry = directory / "123.md" - oldest_entry.write_text("45", encoding="utf-8") - newest_entry.write_text("123", encoding="utf-8") - results = list(ann.news_entries(directory)) - assert len(results) == 2 - assert results[0].issue_number == 45 - assert results[1].issue_number == 123 - - -def test_only_utf8(directory): - entry = directory / "42.md" - entry.write_text("Hello, world", encoding="utf-16") - with pytest.raises(ValueError): - list(ann.news_entries(directory)) - - -def test_no_bom_allowed(directory): - entry = directory / "42.md" - entry.write_bytes(codecs.BOM_UTF8 + "Hello, world".encode("utf-8")) - with pytest.raises(ValueError): - list(ann.news_entries(directory)) - - -def test_bad_news_entry_file_name(directory): - entry = directory / "bunk.md" - entry.write_text("Hello, world!") - with pytest.raises(ValueError): - list(ann.news_entries(directory)) - - -def test_news_entry_README_skipping(directory): - entry = directory / "README.md" - entry.write_text("Hello, world!") - assert len(list(ann.news_entries(directory))) == 0 - - -def test_sections_sorting(directory): - dir2 = directory / "2 Hello" - dir1 = directory / "1 World" - dir2.mkdir() - dir1.mkdir() - results = list(ann.sections(directory)) - assert [found.title for found in results] == ["World", "Hello"] - - -def test_sections_naming(directory): - (directory / "Hello").mkdir() - assert not ann.sections(directory) - - -def test_gather(directory): - fixes = directory / "2 Fixes" - fixes.mkdir() - fix1 = fixes / "1.md" - fix1.write_text("Fix 1", encoding="utf-8") - fix2 = fixes / "3.md" - fix2.write_text("Fix 2", encoding="utf-8") - enhancements = directory / "1 Enhancements" - enhancements.mkdir() - enhancement1 = enhancements / "2.md" - enhancement1.write_text("Enhancement 1", encoding="utf-8") - enhancement2 = enhancements / "4.md" - enhancement2.write_text("Enhancement 2", encoding="utf-8") - results = ann.gather(directory) - assert len(results) == 2 - section, entries = results[0] - assert section.title == "Enhancements" - assert len(entries) == 2 - assert entries[0].description == "Enhancement 1" - assert entries[1].description == "Enhancement 2" - section, entries = results[1] - assert len(entries) == 2 - assert section.title == "Fixes" - assert entries[0].description == "Fix 1" - assert entries[1].description == "Fix 2" - - -def test_entry_markdown(): - markdown = ann.entry_markdown(ann.NewsEntry(42, "Hello, world!", None)) - assert "42" in markdown - assert "Hello, world!" in markdown - assert "https://github.com/Microsoft/vscode-python/issues/42" in markdown - - -def test_changelog_markdown(): - data = [ - ( - ann.SectionTitle(1, "Enhancements", None), - [ - ann.NewsEntry(2, "Enhancement 1", None), - ann.NewsEntry(4, "Enhancement 2", None), - ], - ), - ( - ann.SectionTitle(1, "Fixes", None), - [ann.NewsEntry(1, "Fix 1", None), ann.NewsEntry(3, "Fix 2", None)], - ), - ] - markdown = ann.changelog_markdown(data) - assert "### Enhancements" in markdown - assert "### Fixes" in markdown - assert "1" in markdown - assert "Fix 1" in markdown - assert "2" in markdown - assert "Enhancement 1" in markdown - assert "https://github.com/Microsoft/vscode-python/issues/2" in markdown - assert "3" in markdown - assert "Fix 2" in markdown - assert "https://github.com/Microsoft/vscode-python/issues/3" in markdown - assert "4" in markdown - assert "Enhancement 2" in markdown - - -def test_cleanup(directory, monkeypatch): - rm_path = None - - def fake_git_rm(path): - nonlocal rm_path - rm_path = path - - monkeypatch.setattr(ann, "git_rm", fake_git_rm) - fixes = directory / "2 Fixes" - fixes.mkdir() - fix1 = fixes / "1.md" - fix1.write_text("Fix 1", encoding="utf-8") - results = ann.gather(directory) - assert len(results) == 1 - ann.cleanup(results) - section, entries = results.pop() - assert len(entries) == 1 - assert rm_path == entries[0].path - - -TITLE = "# Our most excellent changelog" -OLD_NEWS = f"""\ -## 2018.12.0 (31 Dec 2018) - -We did things! - -## 2017.11.16 (16 Nov 2017) - -We started going stuff. -""" -NEW_NEWS = """\ -We fixed all the things! - -### Code Health - -We deleted all the code to fix all the things. ;) -""" - - -def test_complete_news(): - version = "2019.3.0" - # Remove leading `0`. - date = datetime.date.today().strftime("%d %B %Y").lstrip("0") - news = ann.complete_news(version, NEW_NEWS, f"{TITLE}\n\n\n{OLD_NEWS}") - expected = f"{TITLE}\n\n## {version} ({date})\n\n{NEW_NEWS.strip()}\n\n\n{OLD_NEWS.strip()}" - assert news == expected - - -def test_cli(): - for option in ("--" + opt for opt in ["dry_run", "interim", "final"]): - args = docopt.docopt(ann.__doc__, [option]) - assert args[option] - args = docopt.docopt(ann.__doc__, ["./news"]) - assert args[""] == "./news" - args = docopt.docopt(ann.__doc__, ["--dry_run", "./news"]) - assert args["--dry_run"] - assert args[""] == "./news" - args = docopt.docopt(ann.__doc__, ["--update", "CHANGELOG.md", "./news"]) - assert args["--update"] == "CHANGELOG.md" - assert args[""] == "./news" diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 000000000000..3991ee8c025a --- /dev/null +++ b/noxfile.py @@ -0,0 +1,161 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +import os +import pathlib +import nox +import shutil +import sys +import sysconfig +import uuid + +EXT_ROOT = pathlib.Path(__file__).parent + + +def delete_dir(path: pathlib.Path, ignore_errors=None): + attempt = 0 + known = [] + while attempt < 5: + try: + shutil.rmtree(os.fspath(path), ignore_errors=ignore_errors) + return + except PermissionError as pe: + if os.fspath(pe.filename) in known: + break + print(f"Changing permissions on {pe.filename}") + os.chmod(pe.filename, 0o666) + + shutil.rmtree(os.fspath(path)) + + +@nox.session() +def install_python_libs(session: nox.Session): + requirements = [ + ("./python_files/lib/python", "./requirements.txt"), + ( + "./python_files/lib/jedilsp", + "./python_files/jedilsp_requirements/requirements.txt", + ), + ] + for target, file in requirements: + session.install( + "-t", + target, + "--no-cache-dir", + "--implementation", + "py", + "--no-deps", + "--require-hashes", + "--only-binary", + ":all:", + "-r", + file, + ) + + session.install("packaging") + session.install("debugpy") + + # Download get-pip script + session.run( + "python", + "./python_files/download_get_pip.py", + env={"PYTHONPATH": "./python_files/lib/temp"}, + ) + + if pathlib.Path("./python_files/lib/temp").exists(): + shutil.rmtree("./python_files/lib/temp") + + +@nox.session() +def native_build(session: nox.Session): + source_dir = pathlib.Path(pathlib.Path.cwd() / "python-env-tools").resolve() + dest_dir = pathlib.Path(pathlib.Path.cwd() / "python-env-tools").resolve() + + with session.cd(source_dir): + if not pathlib.Path(dest_dir / "bin").exists(): + pathlib.Path(dest_dir / "bin").mkdir() + + if not pathlib.Path(dest_dir / "bin" / ".gitignore").exists(): + pathlib.Path(dest_dir / "bin" / ".gitignore").write_text( + "*\n", encoding="utf-8" + ) + + ext = sysconfig.get_config_var("EXE") or "" + target = os.environ.get("CARGO_TARGET", None) + + session.run("cargo", "fetch", external=True) + if target: + session.run( + "cargo", + "build", + "--frozen", + "--release", + "--target", + target, + external=True, + ) + source = source_dir / "target" / target / "release" / f"pet{ext}" + else: + session.run( + "cargo", + "build", + "--frozen", + "--release", + external=True, + ) + source = source_dir / "target" / "release" / f"pet{ext}" + dest = dest_dir / "bin" / f"pet{ext}" + shutil.copy(source, dest) + + # Remove python-env-tools/bin exclusion from .vscodeignore + vscode_ignore = EXT_ROOT / ".vscodeignore" + remove_patterns = ("python-env-tools/bin/**",) + lines = vscode_ignore.read_text(encoding="utf-8").splitlines() + filtered_lines = [line for line in lines if not line.startswith(remove_patterns)] + vscode_ignore.write_text("\n".join(filtered_lines) + "\n", encoding="utf-8") + + +@nox.session() +def checkout_native(session: nox.Session): + dest = (pathlib.Path.cwd() / "python-env-tools").resolve() + if dest.exists(): + shutil.rmtree(os.fspath(dest)) + + temp_dir = os.getenv("TEMP") or os.getenv("TMP") or "/tmp" + temp_dir = pathlib.Path(temp_dir) / str(uuid.uuid4()) / "python-env-tools" + temp_dir.mkdir(0o766, parents=True) + + session.log(f"Cloning python-environment-tools to {temp_dir}") + try: + with session.cd(temp_dir): + session.run("git", "init", external=True) + session.run( + "git", + "remote", + "add", + "origin", + "https://github.com/microsoft/python-environment-tools", + external=True, + ) + session.run("git", "fetch", "origin", "main", external=True) + session.run( + "git", "checkout", "--force", "-B", "main", "origin/main", external=True + ) + delete_dir(temp_dir / ".git") + delete_dir(temp_dir / ".github") + delete_dir(temp_dir / ".vscode") + (temp_dir / "CODE_OF_CONDUCT.md").unlink() + shutil.move(os.fspath(temp_dir), os.fspath(dest)) + except PermissionError as e: + print(f"Permission error: {e}") + if not dest.exists(): + raise + finally: + delete_dir(temp_dir.parent, ignore_errors=True) + + +@nox.session() +def setup_repo(session: nox.Session): + install_python_libs(session) + checkout_native(session) + native_build(session) diff --git a/package-lock.json b/package-lock.json index 8a01e1cd0801..6de6edae81c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21280 +1,27154 @@ { - "name": "python", - "version": "2019.7.0-dev", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "@babel/cli": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/cli/-/cli-7.4.4.tgz", - "integrity": "sha512-XGr5YjQSjgTa6OzQZY57FAJsdeVSAKR/u/KA5exWIz66IKtv/zXtHy+fIZcMry/EgYegwuHE7vzGnrFhjdIAsQ==", - "dev": true, - "requires": { - "chokidar": "^2.0.4", - "commander": "^2.8.1", - "convert-source-map": "^1.1.0", - "fs-readdir-recursive": "^1.1.0", - "glob": "^7.0.0", - "lodash": "^4.17.11", - "mkdirp": "^0.5.1", - "output-file-sync": "^2.0.0", - "slash": "^2.0.0", - "source-map": "^0.5.0" - }, - "dependencies": { - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, - "@babel/code-frame": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz", - "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==", - "dev": true, - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.4.4.tgz", - "integrity": "sha512-lQgGX3FPRgbz2SKmhMtYgJvVzGZrmjaF4apZ2bLwofAKiSjxU0drPh4S/VasyYXwaTs+A1gvQ45BN8SQJzHsQQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helpers": "^7.4.4", - "@babel/parser": "^7.4.4", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.11", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } + "name": "python", + "version": "2026.5.0-dev", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "python", + "version": "2026.5.0-dev", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^3.0.0", + "@vscode/extension-telemetry": "^0.8.4", + "arch": "^2.1.0", + "fs-extra": "^11.2.0", + "glob": "^7.2.0", + "iconv-lite": "^0.6.3", + "inversify": "^6.0.2", + "jsonc-parser": "^3.0.0", + "lodash": "^4.18.1", + "minimatch": "^5.1.8", + "named-js-regexp": "^1.3.3", + "node-stream-zip": "^1.6.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^6.5.4", + "rxjs-compat": "^6.5.4", + "semver": "^7.5.2", + "stack-trace": "0.0.10", + "sudo-prompt": "^9.2.1", + "tmp": "^0.2.5", + "uint64be": "^3.0.0", + "unicode": "^14.0.0", + "vscode-debugprotocol": "^1.28.0", + "vscode-jsonrpc": "^9.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.12", + "vscode-languageserver-protocol": "^3.17.6-next.10", + "vscode-tas-client": "^0.1.84", + "which": "^2.0.2", + "winreg": "^1.2.4", + "xml2js": "^0.5.0" + }, + "devDependencies": { + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/bent": "^7.3.0", + "@types/chai": "^4.1.2", + "@types/chai-arrays": "^2.0.0", + "@types/chai-as-promised": "^7.1.0", + "@types/download": "^8.0.1", + "@types/fs-extra": "^11.0.4", + "@types/glob": "^7.2.0", + "@types/lodash": "^4.14.104", + "@types/mocha": "^9.1.0", + "@types/node": "^22.19.1", + "@types/semver": "^5.5.0", + "@types/shortid": "^0.0.29", + "@types/sinon": "^17.0.3", + "@types/stack-trace": "0.0.29", + "@types/tmp": "^0.0.33", + "@types/vscode": "^1.95.0", + "@types/which": "^2.0.1", + "@types/winreg": "^1.2.30", + "@types/xml2js": "^0.4.2", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "@vscode/test-electron": "^2.3.8", + "@vscode/vsce": "^2.27.0", + "bent": "^7.3.12", + "chai": "^4.1.2", + "chai-arrays": "^2.0.0", + "chai-as-promised": "^7.1.1", + "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.0.3", + "cross-spawn": "^6.0.5", + "del": "^6.0.0", + "download": "^8.0.0", + "eslint": "^8.57.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.31.0", + "eslint-plugin-jsx-a11y": "^6.3.1", + "eslint-plugin-no-only-tests": "^3.3.0", + "eslint-plugin-react": "^7.20.3", + "eslint-plugin-react-hooks": "^4.0.0", + "expose-loader": "^3.1.0", + "flat": "^5.0.2", + "get-port": "^5.1.1", + "gulp": "^5.0.0", + "gulp-typescript": "^5.0.0", + "mocha": "^11.1.0", + "mocha-junit-reporter": "^2.0.2", + "mocha-multi-reporters": "^1.1.7", + "node-has-native-dependencies": "^1.0.2", + "node-loader": "^1.0.2", + "node-polyfill-webpack-plugin": "^1.1.4", + "nyc": "^15.0.0", + "prettier": "^2.0.2", + "rewiremock": "^3.13.0", + "shortid": "^2.2.8", + "sinon": "^18.0.0", + "source-map-support": "^0.5.12", + "ts-loader": "^9.2.8", + "ts-mockito": "^2.5.0", + "ts-node": "^10.7.0", + "tsconfig-paths-webpack-plugin": "^3.2.0", + "typemoq": "^2.1.0", + "typescript": "~5.2", + "uuid": "^8.3.2", + "webpack": "^5.105.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.2", + "webpack-fix-default-import-plugin": "^1.0.3", + "webpack-merge": "^5.8.0", + "webpack-node-externals": "^3.0.0", + "webpack-require-from": "^1.8.6", + "worker-loader": "^3.0.8", + "yargs": "^15.3.1" + }, + "engines": { + "vscode": "^1.95.0" + } }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } + "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/parser": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz", - "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==", - "dev": true + "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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } + "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": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } }, - "@babel/traverse": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz", - "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" - } + "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/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.0.0" - } + "node_modules/@azure/core-auth/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "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, + "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" + } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "node_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, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } + "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, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } }, - "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 - } - } - }, - "@babel/generator": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.0.0.tgz", - "integrity": "sha512-/BM2vupkpbZXq22l1ALO7MqXJZH2k8bKVv8Y+pABFnzWdztDB/ZLveP5At21vLz5c2YtSE6p7j2FZEsqafMz5Q==", - "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": { - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.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==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "esutils": "^2.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==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.0.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.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" - }, - "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/helper-explode-assignable-expression": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", - "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.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" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz", - "integrity": "sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-transforms": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.1.0.tgz", - "integrity": "sha512-0JZRd2yhawo79Rcm4w0LwSMILFmFXjugG3yqf+P/UsKsRS1mJCmMwwlHDlMg7Avr9LrvSpp4ZSULO9r8jpCzcw==", - "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 - } - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==", - "dev": true - }, - "@babel/helper-regex": { - "version": "7.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" - }, - "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/helper-remap-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-wrap-function": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-replace-supers": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.1.0.tgz", - "integrity": "sha512-BvcDWYZRWVuDeXTYZWxekQNO5D4kO55aArwZOTFXw6rlLQA8ZaDicJR1sO47h+HrnCiDFiww0fSPV0d713KBGQ==", - "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" - } - }, - "@babel/helper-simple-access": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", - "dev": true, - "requires": { - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.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==", - "dev": true, - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@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==", - "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" - } - }, - "@babel/helpers": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.4.4.tgz", - "integrity": "sha512-igczbR/0SeuPR8RFfC7tGrbdTbFL3QTvH6D+Z6zNxnTe//GyqmtHmDkzrqDmyZ3eSwPqB/LhyKoU5DXsp+Vp2A==", - "dev": true, - "requires": { - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" - }, - "dependencies": { - "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } + "node_modules/@azure/core-client/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "dev": true + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.10.1.tgz", + "integrity": "sha512-Kji9k6TOFRDB5ZMTw8qUf2IJ+CeJtsuMdAHox9eqpTf1cefiNMpzrfnF6sINEBZJsaVaWgQ0o48B6kcUH68niA==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.0.0", + "@azure/logger": "^1.0.0", + "form-data": "^4.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "tslib": "^2.2.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=14.0.0" + } }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } + "node_modules/@azure/core-rest-pipeline/node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "engines": { + "node": ">= 10" + } }, - "@babel/parser": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.4.tgz", - "integrity": "sha512-5pCS4mOsL+ANsFZGdvNLybx4wtqAZJ0MJjMHxvzI3bvIsz6sQvzW8XX92EYIkiPtIvcfG3Aj+Ir5VNyjnZhP7w==", - "dev": true + "node_modules/@azure/core-rest-pipeline/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } + "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/traverse": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.4.tgz", - "integrity": "sha512-Gw6qqkw/e6AGzlyj9KnkabJX7VcubqPtkUQVAwkc0wUMldr3A/hezNB3Rc5eIvId95iSGkGIOe5hh1kMKf951A==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" - } + "node_modules/@azure/core-rest-pipeline/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/core-tracing": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.0.1.tgz", + "integrity": "sha512-I5CGMoLtX+pI17ZdiFJZgxMJApsK6jjfm85hpgp3oazCdq5Wxgh4wMr7ge/TTWW1B5WBuvIOI1fMU/FrOAMKrw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } }, - "@babel/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.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" + } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "node_modules/@azure/core-util/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dev": true, + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } }, - "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 - } - } - }, - "@babel/highlight": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz", - "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==", - "dev": true, - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.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" - } + "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, + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.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" - } + "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, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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 + "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 + }, + "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" + } }, - "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 + "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": { + "@azure/msal-common": "14.10.0" + }, + "engines": { + "node": ">=0.8.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" - } - } - } - }, - "@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==", - "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" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-json-strings": "^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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.2.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^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 - } - } - }, - "@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==", - "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" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.1.3" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.0.0.tgz", - "integrity": "sha512-TlxKecN20X2tt2UEr2LNE6aqA0oPeMT1Y3cgz8k4Dn1j5ObT8M3nl9aA37LLklx0PBZKETC9ZAf9n/6SujTuXA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.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==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-simple-access": "^7.1.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==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.0.0", - "@babel/helper-plugin-utils": "^7.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==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.1.0" - } - }, - "@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==", - "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" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@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==", - "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" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "regenerator-transform": "^0.13.3" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.4.4.tgz", - "integrity": "sha512-aMVojEjPszvau3NRg+TIH14ynZLvPewH4xhlCW1w6A3rkxTS1m4uwzRclYR9oS+rl/dr+kT+pzbfHuAWP/lc7Q==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "resolve": "^1.8.1", - "semver": "^5.5.1" - }, - "dependencies": { - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "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, + "engines": { + "node": ">=0.8.0" + } }, - "resolve": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.1.tgz", - "integrity": "sha512-KuIe4mf++td/eFb6wkaPbMDnP6kObCaEtIDuHOUED6MNUo4K670KZUHuuvYPZDxNF0WVLw49n06M2m2dXphEzA==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } + "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, + "dependencies": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.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==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0", - "regexpu-core": "^4.1.3" - } - }, - "@babel/polyfill": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.4.4.tgz", - "integrity": "sha512-WlthFLfhQQhh+A2Gn5NSFl0Huxz36x86Jn+E9OW7ibK8edKPq+KLy4apM1yDpQ8kJOVi1OVjpP4vSDLdrI04dg==", - "dev": true, - "requires": { - "core-js": "^2.6.5", - "regenerator-runtime": "^0.13.2" - }, - "dependencies": { - "core-js": { - "version": "2.6.5", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", - "dev": true - }, - "regenerator-runtime": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", - "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==", - "dev": true - } - } - }, - "@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==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0" - } - }, - "@babel/register": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.4.4.tgz", - "integrity": "sha512-sn51H88GRa00+ZoMqCVgOphmswG4b7mhf9VOB0LUBAieykq2GnRFerlN+JQkO/ntT7wz4jaHNSRPg9IdMPEUkA==", - "dev": true, - "requires": { - "core-js": "^3.0.0", - "find-cache-dir": "^2.0.0", - "lodash": "^4.17.11", - "mkdirp": "^0.5.1", - "pirates": "^4.0.0", - "source-map-support": "^0.5.9" - }, - "dependencies": { - "core-js": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.0.1.tgz", - "integrity": "sha512-sco40rF+2KlE0ROMvydjkrVMMG1vYilP2ALoRXcYR4obqbYIuV3Bg+51GEDW+HF8n7NRA+iaA4qD0nD9lo9mew==", - "dev": true + "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, + "engines": { + "node": ">=0.8.0" + } }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } + "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" + } }, - "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" - } + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.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" - } + "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, + "engines": { + "node": ">=6.9.0" + } }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } + "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, + "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" + } }, - "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 + "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": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "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, + "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" + } }, - "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" - } + "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, + "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" + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "@babel/runtime": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.4.tgz", - "integrity": "sha512-w0+uT71b6Yi7i5SE0co4NioIpSYS6lLiXvCzWzGSKvpK5vdQtCbICHMj+gbAKAOtxiV6HsVh/MBdaF9EQ6faSg==", - "requires": { - "regenerator-runtime": "^0.13.2" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.13.2", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz", - "integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA==" - } - } - }, - "@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" - } - }, - "@babel/template": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.1.0.tgz", - "integrity": "sha512-yZ948B/pJrwWGY6VxG6XRFsVTee3IQ7bihq9zFpM00Vydu6z5Xwg0C3J644kxI9WOTzd+62xcIsQ+AT1MGhqhA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/traverse": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.0.tgz", - "integrity": "sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw==", - "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" - }, - "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" - } + "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, + "dependencies": { + "yallist": "^3.0.2" + } }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", - "dev": true + "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, + "engines": { + "node": ">=6.9.0" + } }, - "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 - } - } - }, - "@babel/types": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0.tgz", - "integrity": "sha512-5tPDap4bGKTLPtci2SUl/B7Gv8RnuJFuQoWx26RJobS0fFrz4reUA3JnwIM+HVHEmWE0C1mzKhDtTp8NsWY02Q==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.10", - "to-fast-properties": "^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 - } - } - }, - "@emotion/babel-utils": { - "version": "0.6.10", - "resolved": "https://registry.npmjs.org/@emotion/babel-utils/-/babel-utils-0.6.10.tgz", - "integrity": "sha512-/fnkM/LTEp3jKe++T0KyTszVGWNKPNOUJfjNKLO17BzQ6QPxgbg3whayom1Qr2oLFH3V92tDymU+dT5q676uow==", - "dev": true, - "requires": { - "@emotion/hash": "^0.6.6", - "@emotion/memoize": "^0.6.6", - "@emotion/serialize": "^0.9.1", - "convert-source-map": "^1.5.1", - "find-root": "^1.1.0", - "source-map": "^0.7.2" - }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } - } - }, - "@emotion/hash": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.6.6.tgz", - "integrity": "sha512-ojhgxzUHZ7am3D2jHkMzPpsBAiB005GF5YU4ea+8DNPybMk01JJUM9V9YRlF/GE95tcOm8DxQvWA2jq19bGalQ==", - "dev": true - }, - "@emotion/memoize": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.6.6.tgz", - "integrity": "sha512-h4t4jFjtm1YV7UirAFuSuFGyLa+NNxjdkq6DpFLANNQY5rHueFZHVY+8Cu1HYVP6DrheB0kv4m5xPjo7eKT7yQ==", - "dev": true - }, - "@emotion/serialize": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.9.1.tgz", - "integrity": "sha512-zTuAFtyPvCctHBEL8KZ5lJuwBanGSutFEncqLn/m9T1a6a93smBStK+bZzcNPgj4QS8Rkw9VTwJGhRIUVO8zsQ==", - "dev": true, - "requires": { - "@emotion/hash": "^0.6.6", - "@emotion/memoize": "^0.6.6", - "@emotion/unitless": "^0.6.7", - "@emotion/utils": "^0.8.2" - } - }, - "@emotion/stylis": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.7.1.tgz", - "integrity": "sha512-/SLmSIkN13M//53TtNxgxo57mcJk/UJIDFRKwOiLIBEyBHEcipgR6hNMQ/59Sl4VjCJ0Z/3zeAZyvnSLPG/1HQ==", - "dev": true - }, - "@emotion/unitless": { - "version": "0.6.7", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.6.7.tgz", - "integrity": "sha512-Arj1hncvEVqQ2p7Ega08uHLr1JuRYBuO5cIvcA+WWEQ5+VmkOE3ZXzl04NbQxeQpWX78G7u6MqxKuNX3wvYZxg==", - "dev": true - }, - "@emotion/utils": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.8.2.tgz", - "integrity": "sha512-rLu3wcBWH4P5q1CGoSSH/i9hrXs7SlbRLkoq9IGuoPYNGQvDJ3pt/wmOM+XgYjIDRMVIdkUWt0RsfzF50JfnCw==", - "dev": true - }, - "@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=", - "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" - } - }, - "@gulp-sourcemaps/map-sources": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@gulp-sourcemaps/map-sources/-/map-sources-1.0.0.tgz", - "integrity": "sha1-iQrnxdjId/bThIYCFazp1+yUW9o=", - "dev": true, - "requires": { - "normalize-path": "^2.0.1", - "through2": "^2.0.3" - } - }, - "@istanbuljs/nyc-config-typescript": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-0.1.3.tgz", - "integrity": "sha512-EzRFg92bRSD1W/zeuNkeGwph0nkWf+pP2l/lYW4/5hav7RjKKBN5kV1Ix7Tvi0CMu3pC4Wi/U7rNisiJMR3ORg==", - "dev": true - }, - "@jupyterlab/coreutils": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.2.1.tgz", - "integrity": "sha512-XkGMBXqEAnENC4fK/L3uEqqxyNUtf4TI/1XNDln7d5VOPHQJSBTbYlBAZ0AQotn2qbs4WqmV6icxje2ITVedqQ==", - "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" - } - } - } - }, - "@jupyterlab/observables": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.1.1.tgz", - "integrity": "sha512-PzmJ/jF5fWzHCvjPAWBi3YjtHRAF0bwyjpd8W8aJt64TzhEZh0se8xCNGOURzD+8TxOsTF9JpQ9wIDBr4V226Q==", - "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" - } - }, - "@jupyterlab/services": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@jupyterlab/services/-/services-3.2.1.tgz", - "integrity": "sha512-zCMruGGYxTe427ESK4YUO1V/liFOFYpebYPRsJ+j9CFdV+Hm760+nx4pFX6N6Z9TbS+5cs8BgZ+ebm8unRZrJg==", - "requires": { - "@jupyterlab/coreutils": "^2.2.1", - "@jupyterlab/observables": "^2.1.1", - "@phosphor/algorithm": "^1.1.2", - "@phosphor/coreutils": "^1.3.0", - "@phosphor/disposable": "^1.1.2", - "@phosphor/signaling": "^1.2.2" - } - }, - "@mapbox/polylabel": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/polylabel/-/polylabel-1.0.2.tgz", - "integrity": "sha1-xXFGGbZa3QgmOOoGAn5psUUA76Y=", - "dev": true, - "requires": { - "tinyqueue": "^1.1.0" - } - }, - "@nteract/markdown": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nteract/markdown/-/markdown-2.1.4.tgz", - "integrity": "sha512-nRJuAfX+3n/geJAQj6IqEsbhpiPOjyFlUTzh1bdT2hyLGbd2VdbANXDliWTDKVHOHYWxh0zOLQhRvQ4B6G5QlQ==", - "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" - } - }, - "@nteract/mathjax": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@nteract/mathjax/-/mathjax-2.1.4.tgz", - "integrity": "sha512-dY+h3iBsfioSg+33uB04LE9tIt79ClbEumXKz7eciS251jXw8VZcmo/YFqldThpgckbCvveNCliM4Y5sTk1gog==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0", - "prop-types": "^15.6.1" - } - }, - "@nteract/octicons": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@nteract/octicons/-/octicons-0.4.3.tgz", - "integrity": "sha512-spBTHmaD4+W/Ww0UQt1uXmeYGM5GBA5TExdcuDBn3dLWqsfwjf1nTEfygLx4TRtuqImfPAuUHM4iV+2WkXgMBg==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "babel-runtime": "^6.26.0" - } - }, - "@nteract/plotly": { - "version": "1.48.3", - "resolved": "https://registry.npmjs.org/@nteract/plotly/-/plotly-1.48.3.tgz", - "integrity": "sha512-1Km5MtjyUL9POZjU6LMzH/npFUYIC6vewH0Gd2Ua1FON3qN1KCRoJ8em5Gkp+K57QJRpMET1F4z4N9d3U9HOqA==", - "dev": true - }, - "@nteract/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==", - "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": { - "@nteract/transform-plotly": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-3.2.5.tgz", - "integrity": "sha512-tQi47Ly1b/YLNJWhUV8gKVkweJb6ugfGg4XylG2wnLVDoaM8ObJrHAtY74S+Kg9e88N8ExNPy9ulG2xhhSrMag==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "@nteract/plotly": "^1.0.0", - "babel-runtime": "^6.26.0", - "lodash": "^4.17.4" - } - } - } - }, - "@nteract/transform-geojson": { - "version": "3.2.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" - } - }, - "@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" - } - }, - "@nteract/transform-plotly": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@nteract/transform-plotly/-/transform-plotly-5.0.0.tgz", - "integrity": "sha512-EomJyXuKUFrby1hqm6jEwL062GqrtSU5ZsFexuoglI7E/6wcO+JGr+tyeF9UvCFMy5zWNk5eJoUcicwIKdfCQA==", - "dev": true, - "requires": { - "@nteract/plotly": "^1.0.0", - "lodash": "^4.17.4" - } - }, - "@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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@phosphor/coreutils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@phosphor/coreutils/-/coreutils-1.3.0.tgz", - "integrity": "sha1-YyktOBwBLFqw0Blug87YKbfgSkI=" - }, - "@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" - } - }, - "@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" - } - }, - "@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" - } - }, - "@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", - "dev": true - }, - "@sinonjs/commons": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.4.0.tgz", - "integrity": "sha512-9jHK3YF/8HtJ9wCAbG+j8cD0i0+ATS9A7gXFqS36TblLPNy6rEEc+SB0imo91eCboGaBYGV/MT1/br/J+EE7Tw==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/formatio": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-3.2.1.tgz", - "integrity": "sha512-tsHvOB24rvyvV2+zKMmPkZ7dXX6LSLKZ7aOtXY6Edklp0uRcgGpOsQTTGTcWViFyx4uhWc6GV8QdnALbIbIdeQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1", - "@sinonjs/samsam": "^3.1.0" - } - }, - "@sinonjs/samsam": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-3.3.1.tgz", - "integrity": "sha512-wRSfmyd81swH0hA1bxJZJ57xr22kC07a1N4zuIL47yTS04bDk6AoCkczcqHEjcRPmJ+FruGJ9WBQiJwMtIElFw==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.0.2", - "array-from": "^2.1.1", - "lodash": "^4.17.11" - } - }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true - }, - "@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 - }, - "@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 - }, - "@types/chai": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.3.tgz", - "integrity": "sha512-f5dXGzOJycyzSMdaXVhiBhauL4dYydXwVpavfQ1mVCaGjR56a9QfklXObUxlIY9bGTmCPHEEZ04I16BZ/8w5ww==", - "dev": true - }, - "@types/chai-arrays": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-1.0.2.tgz", - "integrity": "sha512-/kgYvj5Pwiv/bOlJ6c5GlRF/W6lUGSLrpQGl/7Gg6w7tvBYcf0iF91+wwyuwDYGO2zM0wNpcoPixZVif8I/r6g==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, - "@types/chai-as-promised": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@types/chai-as-promised/-/chai-as-promised-7.1.0.tgz", - "integrity": "sha512-MFiW54UOSt+f2bRw8J7LgQeIvE/9b4oGvwU7XW30S9QGAiHGnU/fmiOprsyMkdmH2rl8xSPc0/yrQw8juXU6bQ==", - "dev": true, - "requires": { - "@types/chai": "*" - } - }, - "@types/cheerio": { - "version": "0.22.9", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.9.tgz", - "integrity": "sha512-q6LuBI0t5u04f0Q4/R+cGBqIbZMtJkVvCSF+nTfFBBdQqQvJR/mNHeWjRkszyLl7oyf2rDoKUYMEjTw5AV0hiw==", - "dev": true - }, - "@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 - }, - "@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==", - "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/webpack": "*" - } - }, - "@types/decompress": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.2.tgz", - "integrity": "sha512-2jlSsNAVhrWJtgOV3V85MJ09yRoeUTUWQeeusNYAcJVkUmoVRVElvmkWN0TK+Lgdlyd9pIRyja/DTBcyqD8xyA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/del": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/del/-/del-3.0.1.tgz", - "integrity": "sha512-y6qRq6raBuu965clKgx6FHuiPu3oHdtmzMPXi8Uahsjdq1L6DL5fS/aY5/s71YwM7k6K1QIWvem5vNwlnNGIkQ==", - "dev": true, - "requires": { - "@types/glob": "*" - } - }, - "@types/diff-match-patch": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/@types/diff-match-patch/-/diff-match-patch-1.0.32.tgz", - "integrity": "sha512-bPYT5ECFiblzsVzyURaNhljBH2Gh1t9LowgUwciMrNAhFewLkHT2H0Mto07Y4/3KCOGZHRQll3CTtQZ0X11D/A==", - "dev": true - }, - "@types/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": "*" - } - }, - "@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": "*" - } - }, - "@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": "*" - } - }, - "@types/event-stream": { - "version": "3.3.34", - "resolved": "https://registry.npmjs.org/@types/event-stream/-/event-stream-3.3.34.tgz", - "integrity": "sha512-LLiivgWKii4JeMzFy3trrxqkRrVSdue8WmbXyHuSJLwNrhIQU5MTrc65jhxEPwMyh5HR1xevSdD+k2nnSRKw9g==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/events": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz", - "integrity": "sha512-KEIlhXnIutzKwRbQkGWb/I4HFqBuUykAdHgDED6xqwXJfONCjF5VoE0cXEiurh3XauygxzeDzgtXUqvLkxFzzA==", - "dev": true - }, - "@types/form-data": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@types/form-data/-/form-data-2.2.1.tgz", - "integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/fs-extra": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-5.0.2.tgz", - "integrity": "sha512-Q3FWsbdmkQd1ib11A4XNWQvRD//5KpPoGawA8aB2DR7pWKoW9XQv3+dGxD/Z1eVFze23Okdo27ZQytVFlweKvQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha512-TiNg8R1kjDde5Pub9F9vCwZA/BNW9HeXP5b9j7Qucqncy/McfPZ6xze/EyBdXS5FhMIGN6Fx3vg75l5KHy3V1Q==", - "dev": true - }, - "@types/glob": { - "version": "5.0.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==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@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==", - "dev": true, - "requires": { - "@types/clean-css": "*", - "@types/relateurl": "*", - "@types/uglify-js": "*" - } - }, - "@types/html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/html-webpack-plugin/-/html-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-in9rViBsTRB4ZApndZ12It68nGzSMHVK30JD7c49iLIHMFeTPbP7I7wevzMv7re2o0k5TlU6Ry/beyrmgWX7Bg==", - "dev": true, - "requires": { - "@types/html-minifier": "*", - "@types/tapable": "*", - "@types/webpack": "*" - } - }, - "@types/iconv-lite": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@types/iconv-lite/-/iconv-lite-0.0.1.tgz", - "integrity": "sha1-qjuL2ivlErGuCgV7lC6GnDcKVWk=", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/jquery": { - "version": "3.3.29", - "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.3.29.tgz", - "integrity": "sha512-FhJvBninYD36v3k6c+bVk1DSZwh7B5Dpb/Pyk3HKVsiohn0nhbefZZ+3JXbWQhFyt0MxSl2jRDdGQPHeOHFXrQ==", - "dev": true, - "requires": { - "@types/sizzle": "*" - } - }, - "@types/jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-XHMNZFQ0Ih3A4/NTWAO15+OsQafPKnQCanN0FYGbsTM/EoI5EoEAvvkF51/DQC2BT5low4tomp7k2RLMlriA5Q==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^4.0.0" - }, - "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 - } - } - }, - "@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==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/webpack": "*" - } - }, - "@types/lodash": { - "version": "4.14.109", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.109.tgz", - "integrity": "sha512-hop8SdPUEzbcJm6aTsmuwjIYQo1tqLseKCM+s2bBqTU2gErwI4fE+aqUVOlscPSQbKHKgtMMPoC+h4AIGOJYvw==", - "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=", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true - }, - "@types/mocha": { - "version": "5.2.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.6.tgz", - "integrity": "sha512-1axi39YdtBI7z957vdqXI4Ac25e7YihYQtJa+Clnxg1zTJEaIRbndt71O3sP4GAMgiAm0pY26/b9BrY4MR/PMw==", - "dev": true - }, - "@types/nock": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@types/nock/-/nock-10.0.3.tgz", - "integrity": "sha512-OthuN+2FuzfZO3yONJ/QVjKmLEuRagS9TV9lEId+WHL9KhftYG+/2z+pxlr0UgVVXSpVD8woie/3fzQn8ft/Ow==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "9.4.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-9.4.7.tgz", - "integrity": "sha512-4Ba90mWNx8ddbafuyGGwjkZMigi+AWfYLSDCpovwsE63ia8w93r3oJ8PIAQc3y8U+XHcnMOHPIzNe3o438Ywcw==", - "dev": true - }, - "@types/node-fetch": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.3.4.tgz", - "integrity": "sha512-ZwGXz5osL88SF+jlbbz0WJlINlOZHoSWPrLytQRWRdB6j/KVLup1OoqIxnjO6q9ToqEEP3MZFzJCotgge+IiRw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/pdfkit": { - "version": "0.7.36", - "resolved": "https://registry.npmjs.org/@types/pdfkit/-/pdfkit-0.7.36.tgz", - "integrity": "sha512-9eRA6MuW+n78yU3HhoIrDxjyAX2++B5MpLDYqHOnaRTquCw+5sYXT+QN8E1eSaxvNUwlRfU3tOm4UzTeGWmBqg==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/promisify-node": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@types/promisify-node/-/promisify-node-0.4.0.tgz", - "integrity": "sha1-3MceY8Cr9oYbrn0S9swzlceDk2s=", - "dev": true - }, - "@types/prop-types": { - "version": "15.5.6", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.5.6.tgz", - "integrity": "sha512-ZBFR7TROLVzCkswA3Fmqq+IIJt62/T7aY/Dmz+QkU7CaW2QFqAitCE8Ups7IzmGhcN1YWMBT4Qcoc07jU9hOJQ==", - "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==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "csstype": "^2.2.0" - } - }, - "@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==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/react": "*" - } - }, - "@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==", - "dev": true, - "requires": { - "@types/react": "*" - } - }, - "@types/react-virtualized": { - "version": "9.21.2", - "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.2.tgz", - "integrity": "sha512-Q6geJaDd8FlBw3ilD4ODferTyVtYAmDE3d7+GacfwN0jPt9rD9XkeuPjcHmyIwTrMXuLv1VIJmRxU9WQoQFBJw==", - "dev": true, - "requires": { - "@types/prop-types": "*", - "@types/react": "*" - } - }, - "@types/relateurl": { - "version": "0.2.28", - "resolved": "http://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.28.tgz", - "integrity": "sha1-a9p9uGU/piZD9e5p6facEaOS46Y=", - "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==", - "dev": true, - "requires": { - "@types/caseless": "*", - "@types/form-data": "*", - "@types/node": "*", - "@types/tough-cookie": "*" - } - }, - "@types/semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", - "dev": true - }, - "@types/shortid": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", - "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=", - "dev": true - }, - "@types/sinon": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-7.0.13.tgz", - "integrity": "sha512-d7c/C/+H/knZ3L8/cxhicHUiTDxdgap0b/aNJfsmLwFu/iOP17mdgbQsbHA3SJmrzsjD0l3UEE5SN4xxuz5ung==", - "dev": true - }, - "@types/sizzle": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.2.tgz", - "integrity": "sha512-7EJYyKTL7tFR8+gDbB6Wwz/arpGa0Mywk1TJbNzKzHtzbwVmY4HR9WqS5VV7dsBUKQmPNr192jHr/VpBluj/hg==", - "dev": true - }, - "@types/slickgrid": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/@types/slickgrid/-/slickgrid-2.1.27.tgz", - "integrity": "sha512-CJ3EI82XAAjkTLNrcMcdsWgAXYyARWI1eo3+jMMqxsNeh0VUtU5k+5K3SUyaeYnif9M0tHLeTna4YKJy1ykb+w==", - "dev": true, - "requires": { - "@types/jquery": "*" - } - }, - "@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/strip-json-comments": { - "version": "0.0.30", - "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz", - "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==", - "dev": true - }, - "@types/tapable": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.4.tgz", - "integrity": "sha512-78AdXtlhpCHT0K3EytMpn4JNxaf5tbqbLcbIRoQIHzpTIyjpxLQKRoxU55ujBXAtg3Nl2h/XWvfDa9dsMOd0pQ==", - "dev": true - }, - "@types/temp": { - "version": "0.8.32", - "resolved": "https://registry.npmjs.org/@types/temp/-/temp-0.8.32.tgz", - "integrity": "sha512-gyIhOlWPqI8vtYTlRb61HKV7x+3wjpJIQi8mTaweVtEMvhIV6Xajo8FVcNJWeJOBuedRCzK2Uy+uhj/rJmR9oQ==", - "dev": true, - "requires": { - "@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==", - "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/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==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/webpack": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.4.19.tgz", - "integrity": "sha512-vO/PuQ9iF9Gy8spN8RUUjt5reu9Z+Tb7iWxeAopCmXaIZaIsOgtY5U6UE2ELlcRUBO1HbNWhy+lQE9G92IJcmQ==", - "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 + "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, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.30.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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, + "engines": { + "node": ">= 12" + } + }, + "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, + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "engines": { + "node": ">=10.0.0" + } + }, + "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, + "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" + } + }, + "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, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "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, + "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" + } + }, + "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, + "license": "Python-2.0" + }, + "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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "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, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "dependencies": { + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "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, + "license": "BSD-3-Clause" + }, + "node_modules/@iarna/toml": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@iarna/toml/-/toml-3.0.0.tgz", + "integrity": "sha512-td6ZUkz2oS3VeleBcN+m//Q6HlCFCPrnI0FZhrt/h4XqLEdOyYp2u21nd8MdsR+WJy5r9PTDaHTDDfhf4H4l6Q==", + "license": "ISC" + }, + "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, + "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" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "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": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "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, + "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" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/nyc-config-typescript": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "nyc": ">=15" + } + }, + "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" + } + }, + "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, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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 + }, + "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, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "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" + } + }, + "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" + } + }, + "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": "*" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-channel-js/node_modules/@microsoft/applicationinsights-shims": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-shims/-/applicationinsights-shims-3.0.1.tgz", + "integrity": "sha512-DKwboF47H1nb33rSUfjqI6ryX29v+2QWcTrRvcQDA32AZr5Ilkr7whOOSsD1aBzwqX0RJEIP1Z81jfE3NBm/Lg==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.9.4 < 2.x" + } + }, + "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" + } + }, + "node_modules/@microsoft/applicationinsights-common": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-common/-/applicationinsights-common-3.0.2.tgz", + "integrity": "sha512-y+WXWop+OVim954Cu1uyYMnNx6PWO8okHpZIQi/1YSqtqaYdtJVPv4P0AVzwJdohxzVfgzKvqj9nec/VWqE2Zg==", + "dependencies": { + "@microsoft/applicationinsights-core-js": "3.0.2", + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-common/node_modules/@microsoft/applicationinsights-core-js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-core-js/-/applicationinsights-core-js-3.0.2.tgz", + "integrity": "sha512-WQhVhzlRlLDrQzn3OShCW/pL3BW5WC57t0oywSknX3q7lMzI3jDg7Ihh0iuIcNTzGCTbDkuqr4d6IjEDWIMtJQ==", + "dependencies": { + "@microsoft/applicationinsights-shims": "3.0.1", + "@microsoft/dynamicproto-js": "^2.0.2", + "@nevware21/ts-async": ">= 0.2.4 < 2.x", + "@nevware21/ts-utils": ">= 0.9.5 < 2.x" + }, + "peerDependencies": { + "tslib": "*" + } + }, + "node_modules/@microsoft/applicationinsights-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" + } + }, + "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" + } + }, + "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": "*" + } + }, + "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==" + }, + "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": "*" + } + }, + "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": "*" + } + }, + "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" + } + }, + "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" + } + }, + "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==" + }, + "node_modules/@microsoft/dynamicproto-js": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@microsoft/dynamicproto-js/-/dynamicproto-js-1.1.9.tgz", + "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" + }, + "node_modules/@nevware21/ts-async": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@nevware21/ts-async/-/ts-async-0.3.0.tgz", + "integrity": "sha512-ZUcgUH12LN/F6nzN0cYd0F/rJaMLmXr0EHVTyYfaYmK55bdwE4338uue4UiVoRqHVqNW4KDUrJc49iGogHKeWA==", + "dependencies": { + "@nevware21/ts-utils": ">= 0.10.0 < 2.x" + } + }, + "node_modules/@nevware21/ts-utils": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@nevware21/ts-utils/-/ts-utils-0.10.1.tgz", + "integrity": "sha512-pMny25NnF2/MJwdqC3Iyjm2pGIXNxni4AROpcqDeWa+td9JMUY4bUS9uU9XW+BoBRqTLUL+WURF9SOd/6OQzRg==" + }, + "node_modules/@nicolo-ribaudo/semver-v6": { + "version": "6.3.3", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/semver-v6/-/semver-v6-6.3.3.tgz", + "integrity": "sha512-3Yc1fUTs69MG/uZbJlLSI3JISMn2UV2rg+1D/vROUqZyh3l6iYHCs7GMp+M40ZD7yOdDbYjJcU1oTJhrc+dGKg==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "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, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", + "integrity": "sha512-O2yRJce1GOc6PAy3QxFM4NzFiWzvScDC1/5ihYBL6BUEVdq0XMWN01sppE+H6bBXbaFYipjwFLEWLg5PaSOThA==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/core": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.15.2.tgz", + "integrity": "sha512-+gBv15ta96WqkHZaPpcDHiaz0utiiHZVfm2YOYSqFGrUaJpPkMoSuLBB58YFQGi6Rsb9EHos84X6X5+9JspmLw==", + "dependencies": { + "@opentelemetry/semantic-conventions": "1.15.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.5.0" + } + }, + "node_modules/@opentelemetry/instrumentation": { + "version": "0.41.2", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.41.2.tgz", + "integrity": "sha512-rxU72E0pKNH6ae2w5+xgVYZLzc5mlxAbGzF4shxMVK8YC2QQsfN38B2GPbj0jvrKWWNUElfclQ+YTykkNg/grw==", + "dependencies": { + "@types/shimmer": "^1.0.2", + "import-in-the-middle": "1.4.2", + "require-in-the-middle": "^7.1.1", + "semver": "^7.5.1", + "shimmer": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.21", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.21.tgz", + "integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==", + "dev": true + }, + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", + "dev": true + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.8.tgz", + "integrity": "sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.9.tgz", + "integrity": "sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.1.tgz", + "integrity": "sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.2.tgz", + "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", + "dev": true + }, + "node_modules/@types/bent": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@types/bent/-/bent-7.3.3.tgz", + "integrity": "sha512-5NEIhVzHiZ6wMjFBmJ3gwjxwGug6amMoAn93rtDBttwrODxm+bt63u+MJA7H9NGGM4X1m73sJrAxDapktl036Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.0.tgz", + "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", + "dev": true + }, + "node_modules/@types/chai-arrays": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/chai-arrays/-/chai-arrays-2.0.0.tgz", + "integrity": "sha512-5h5jnAC9C64YnD7WJpA5gBG7CppF/QmoWytOssJ6ysENllW49NBdpsTx6uuIBOpnzAnXThb8jBICgB62wezTLQ==", + "dev": true, + "dependencies": { + "@types/chai": "*" + } + }, + "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, + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, + "node_modules/@types/decompress": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/@types/decompress/-/decompress-4.2.5.tgz", + "integrity": "sha512-LdL+kbcKGs9TzvB/K+OBGzPfDoP6gwwTsykYjodlzUJUUYp/43c1p1jE5YTtz3z4Ml90iruvBXbJ6+kDvb3WSQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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": "*" + } + }, + "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": "*" + } + }, + "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": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true + }, + "node_modules/@types/fs-extra": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz", + "integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==", + "dev": true, + "dependencies": { + "@types/jsonfile": "*", + "@types/node": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "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, + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "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, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.35", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "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, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", + "dev": true + }, + "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, + "dependencies": { + "@types/node": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-41qEJgBH/TWgo5NFSvBCJ1qkoi3Q6ONSF2avrHq1LVEZfYpdHmj0y9SuTK+u9ZhG1sYQKBL1AWXKyLWP4RaUoQ==", + "dev": true + }, + "node_modules/@types/shimmer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.2.tgz", + "integrity": "sha512-dKkr1bTxbEsFlh2ARpKzcaAmsYixqt9UyCdoEZk8rHyE4iQYcDCyvSjDSf7JUWJHlJiTtbIoQjxKh6ViywqDAg==" + }, + "node_modules/@types/shortid": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/shortid/-/shortid-0.0.29.tgz", + "integrity": "sha1-gJPuBBam4r8qpjOBCRFLP7/6Dps=", + "dev": true + }, + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", + "dev": true, + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", + "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", + "dev": true + }, + "node_modules/@types/stack-trace": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.29.tgz", + "integrity": "sha512-TgfOX+mGY/NyNxJLIbDWrO9DjGoVSW9+aB8H2yy1fy32jsvxijhmyJI9fDFgvz3YP4lvJaq9DzdR/M1bOgVc9g==", + "dev": true + }, + "node_modules/@types/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha1-EHPEvIJHVK49EM+riKsCN7qWTk0=", + "dev": true + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==", + "dev": true + }, + "node_modules/@types/vscode": { + "version": "1.100.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.100.0.tgz", + "integrity": "sha512-4uNyvzHoraXEeCamR3+fzcBlh7Afs4Ifjs4epINyUX/jvdk0uzLnwiDY35UKDKnkCHP5Nu3dljl2H8lR6s+rQw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/which": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/which/-/which-2.0.1.tgz", + "integrity": "sha512-Jjakcv8Roqtio6w1gr0D7y6twbhx6gGgFGF5BLwajPpnOIOxFkakFhCq+LmyyeAz7BX6ULrjBOxdKaCDy+4+dQ==", + "dev": true + }, + "node_modules/@types/winreg": { + "version": "1.2.31", + "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.31.tgz", + "integrity": "sha512-SDatEMEtQ1cJK3esIdH6colduWBP+42Xw9Guq1sf/N6rM3ZxgljBduvZOwBsxRps/k5+Wwf5HJun6pH8OnD2gg==", + "dev": true + }, + "node_modules/@types/xml2js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.9.tgz", + "integrity": "sha512-CHiCKIihl1pychwR2RNX5mAYmJDACgFVCMT5OArMaO3erzwXVcBqPcusr+Vl8yeeXukxZqtF8mZioqX+mpjjdw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "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, + "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 + } + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "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 + } + } + }, + "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, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "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" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "dependencies": { + "@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 + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "dependencies": { + "@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 + } + } + }, + "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" + } + }, + "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, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "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" + } + }, + "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 + }, + "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, + "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" + } + }, + "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, + "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" + } + }, + "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" + } + }, + "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, + "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" + } + }, + "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, + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "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" + ] + }, + "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" + ] + }, + "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" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "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" + ] + }, + "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" + ] + }, + "node_modules/@vscode/vsce/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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" + } + }, + "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, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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" + } + }, + "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 + }, + "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 + }, + "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 + }, + "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" + } + }, + "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 + }, + "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, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "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, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "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" + } + }, + "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 + }, + "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" + } + }, + "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, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "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, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "dependencies": { + "envinfo": "^7.7.3" + }, + "peerDependencies": { + "webpack-cli": "4.x.x" + } + }, + "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 + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "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, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "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, + "engines": { + "node": ">=0.4.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": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "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, + "dependencies": { + "ansi-wrap": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ansi-wrap": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", + "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "dependencies": { + "buffer-equal": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/append-buffer/node_modules/buffer-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", + "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "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": { + "@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 + } + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/archy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", + "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", + "dev": true + }, + "node_modules/arg": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz", + "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==", + "dev": true + }, + "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, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + }, + "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, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "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" + } + }, + "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, + "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" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "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, + "dependencies": { + "object-assign": "^4.1.1", + "util": "0.10.3" + } + }, + "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, + "dependencies": { + "inherits": "2.0.1" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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 + }, + "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 + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "dependencies": { + "async-done": "^2.0.0" + }, + "engines": { + "node": ">= 10.13.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==" + }, + "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, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "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 + }, + "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, + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "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, + "dependencies": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + }, + "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, + "hasInstallScript": true + }, + "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 + }, + "node_modules/bach": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/bach/-/bach-2.0.1.tgz", + "integrity": "sha512-A7bvGMGiTOxGMpNupYl9HQTf0FFDNF4VCmks4PJpFyN1AX2pdKuxuwdvUz2Hu388wcgp+OvGFNsumBfFNkR7eg==", + "dev": true, + "dependencies": { + "async-done": "^2.0.0", + "async-settle": "^2.0.0", + "now-and-later": "^3.0.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "dependencies": { + "once": "^1.4.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/bare-events": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.3.1.tgz", + "integrity": "sha512-sJnSOTVESURZ61XgEleqmP255T6zTYwHPwE4r6SssIh0U9/uDvfpdoJYpVUerJJZH2fueO+CdT8ZT+OC/7aZDA==", + "dev": true, + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "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" + } + }, + "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" + } + }, + "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": "*" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "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" + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", + "dev": true + }, + "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, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", + "dev": true + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "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": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "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, + "license": "MIT", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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, + "license": "MIT" + }, + "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, + "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/browserify-sign": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz", + "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==", + "dev": true, + "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" + } + }, + "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, + "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" + }, + { + "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" + } + }, + "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, + "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" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "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.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", + "dev": true, + "engines": { + "node": "*" + } + }, + "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 + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", + "dev": true + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", + "dev": true + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", + "dev": true + }, + "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 + }, + "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" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "dependencies": { + "hasha": "^5.0.0", + "make-dir": "^3.0.0", + "package-hash": "^4.0.0", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "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" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001768", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001768.tgz", + "integrity": "sha512-qY3aDRZC5nWPgHUgIB84WL+nySuo19wk0VJpp/XI9T34lrvkyhRvNVOFJOp2kxClQhiFBu+TaUSudf6oa3vkSA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dev": true, + "dependencies": { + "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" + } + }, + "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, + "engines": { + "node": ">=0.10" + } + }, + "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, + "dependencies": { + "check-error": "^1.0.2" + }, + "peerDependencies": { + "chai": ">= 2.1.2 < 5" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true, + "engines": { + "node": "*" + } + }, + "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, + "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" + } + }, + "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, + "dependencies": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/css-select": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", + "dev": true, + "dependencies": { + "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" + } + }, + "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, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "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" + } + ] + }, + "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, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", + "dev": true, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "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" + } + }, + "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, + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "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" + } + ] + }, + "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, + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "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, + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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 + }, + "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 + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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 + }, + "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, + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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" + }, + "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 + }, + "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==" + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">= 0.10" + } + }, + "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" + } + }, + "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, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "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 + }, + "node_modules/cloneable-readable": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/cloneable-readable/-/cloneable-readable-1.1.3.tgz", + "integrity": "sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "process-nextick-args": "^2.0.0", + "readable-stream": "^2.3.5" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=16" + } + }, + "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" + } + }, + "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 + }, + "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 + }, + "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" + } + }, + "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, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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, + "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" + } + }, + "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 + }, + "node_modules/copy-props": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-4.0.0.tgz", + "integrity": "sha512-bVWtw1wQLzzKiYROtvNlbJgxgBYt2bMJpkCbKmXM3xyijvcjjWXEk5nyrrT3bgJ7ODb19ZohE2T0Y3FgNPyoTw==", + "dev": true, + "dependencies": { + "each-props": "^3.0.0", + "is-plain-object": "^5.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/copy-props/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-9.1.0.tgz", + "integrity": "sha512-rxnR7PaGigJzhqETHGmAcxKnLZSR5u1Y3/bcIv/1FnqXedcL/E2ewK7ZCNrArJKCiSv8yVXhTqetJh8inDvfsA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^11.0.3", + "normalize-path": "^3.0.0", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "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, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-pure": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.42.0.tgz", + "integrity": "sha512-007bM04u91fF4kMgwom2I5cQxAFIy8jVulgr9eozILl/SZE53QOqnW/+vviC+wQWLv+AunBG+8Q0TLoeSsSxRQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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, + "license": "MIT", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dev": true, + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "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 + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-env/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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" + } + }, + "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, + "bin": { + "semver": "bin/semver" + } + }, + "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" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "dev": true, + "engines": { + "node": "*" + } + }, + "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, + "license": "MIT", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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 + }, + "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, + "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/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, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "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": { + "ms": "2.0.0" + } + }, + "node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "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, + "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" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "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" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "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, + "dependencies": { + "strip-bom": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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, + "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" + } + }, + "node_modules/del": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/del/-/del-6.0.0.tgz", + "integrity": "sha512-1shh9DQ23L16oXSZKB2JxpL7iMy2E0S9d517ptA1P8iw0alkPtQcrKH7ru31rYtKwF499HkTu+DRzq3TCKDFRQ==", + "dev": true, + "dependencies": { + "globby": "^11.0.1", + "graceful-fs": "^4.2.4", + "is-glob": "^4.0.1", + "is-path-cwd": "^2.2.0", + "is-path-inside": "^3.0.2", + "p-map": "^4.0.0", + "rimraf": "^3.0.2", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + }, + "node_modules/des.js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", + "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha512-DtCOLG98P007x7wiiOmfI0fi3eIKyWiLTGJ2MDnVi/E04lWGbf+JzrRHMm0rgIIZJGtHpKpbVgLWHrv8xXpc3Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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": "*" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dev": true, + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "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" + } + }, + "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, + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/download/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "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" + } + }, + "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, + "license": "BSD-3-Clause" + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "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-plain-object": "^5.0.0", + "object.defaults": "^1.1.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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": { + "safe-buffer": "^5.0.1" + } + }, + "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 + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "dependencies": { + "shimmer": "^1.2.0" + } + }, + "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 + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.19.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", + "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/envinfo": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "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" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true + }, + "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" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "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, + "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" + } + }, + "node_modules/es6-error": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", + "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", + "dev": true + }, + "node_modules/es6-object-assign": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es6-object-assign/-/es6-object-assign-1.1.0.tgz", + "integrity": "sha1-wsNYJlYkfDnqEHyx5mUrb58kUjw=", + "dev": true + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "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, + "engines": { + "node": ">=0.8.0" + } + }, + "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, + "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" + } + }, + "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, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "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, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "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, + "dependencies": { + "ms": "^2.1.1" + } + }, + "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 + } + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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": "*" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "dependencies": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + }, + "engines": { + "node": ">=6.0" + } + }, + "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 + }, + "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": "*" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=5.0.0" + } + }, + "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, + "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" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "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" + } + }, + "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, + "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": { + "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 + } + } + }, + "node_modules/eslint/node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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, + "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": { + "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" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/execa/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "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": { + "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.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "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" + } + }, + "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, + "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": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "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": { + "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" + } + }, + "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, + "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": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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, + "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": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "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/flush-write-stream/-/flush-write-stream-1.1.1.tgz", + "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "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": { + "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" + } + }, + "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, + "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": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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" + } + }, + "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, + "dependencies": { + "graceful-fs": "^4.1.11", + "through2": "^2.0.3" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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, + "dependencies": { + "async": "*" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "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" + } + }, + "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" + } + }, + "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, + "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" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "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": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "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" + } + }, + "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, + "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": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "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, + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha512-uMbLGAP3S2aDOHUDfdoYcdIePUCfysbAd0IAoWVZbeGU/oNQ8asHVSshLDJUPWxfzj8zsCG7/XeHPHTtow0nsw==", + "dev": true, + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "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 + }, + "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, + "dependencies": { + "async-done": "^2.0.0", + "chokidar": "^3.5.3" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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" + } + }, + "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": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "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" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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, + "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "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 + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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-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" + } + }, + "node_modules/gulp-typescript/node_modules/ansi-colors": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", + "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/gulp-typescript/node_modules/source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "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" + } + }, + "node_modules/gulp/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "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, + "dependencies": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gulp/node_modules/glob-stream": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-8.0.2.tgz", + "integrity": "sha512-R8z6eTB55t3QeZMmU1C+Gv+t5UnNRkA55c5yo67fAVfxODxieTwsjNG7utxS/73NdP1NbDgCrhVEg2h00y4fFw==", + "dev": true, + "dependencies": { + "@gulpjs/to-absolute-glob": "^4.0.0", + "anymatch": "^3.1.3", + "fastq": "^1.13.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "is-negated-glob": "^1.0.0", + "normalize-path": "^3.0.0", + "streamx": "^2.12.5" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "engines": { + "node": ">=10.13.0" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/gulp/node_modules/resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "dependencies": { + "value-or-function": "^4.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">= 10.13.0" + } + }, + "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": { + "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/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" + } + }, + "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": { + "glogg": "^2.2.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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": "*" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "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": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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" + } + }, + "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, + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasha": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.1.0.tgz", + "integrity": "sha512-OFPDWmzPN1l7atOV1TgBVmNtBxaIysToK6Ve9DK+vT6pYuklw/nPNT+HJbZi0KDcI6vWB+9tgvZ5YD7fA3CXcA==", + "dev": true, + "dependencies": { + "is-stream": "^2.0.0", + "type-fest": "^0.8.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/hasha/node_modules/is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "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, + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "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" + } + }, + "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 + }, + "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" + }, + "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" + } + }, + "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 + } + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", + "dev": true + }, + "node_modules/https-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", + "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + ] + }, + "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" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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" + } + }, + "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" + } + }, + "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" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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" + } + }, + "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 + }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "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==" + }, + "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" + } + }, + "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, + "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": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-negated-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", + "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-cwd": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", + "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "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" + } + }, + "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" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true + }, + "node_modules/is-valid-glob": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", + "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "engines": { + "node": ">=0.10.0" + } + }, + "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" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "append-transform": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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, + "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" + } + }, + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/istanbul-lib-processinfo/node_modules/p-map": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-3.0.0.tgz", + "integrity": "sha512-d3qXVTF/s+W+CdJ5A29wywV2n8CQQYahlgz2bFiA+4eVNJbHJodPZ+/gXwPGh0bOqA+j8S+6+ckmvLGPk1QpxQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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" + } + }, + "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" + } + }, + "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 + } + } + }, + "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, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "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, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "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, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "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" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "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 + }, + "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" + } + }, + "node_modules/js-yaml/node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "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" + } + }, + "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, + "license": "MIT" + }, + "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 + }, + "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 + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, + "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" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dev": true, + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "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": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "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, + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "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, + "dependencies": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + }, + "engines": { + "node": ">=4.0" + } + }, + "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, + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "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 + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "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" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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 + }, + "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, + "dependencies": { + "language-subtag-registry": "~0.3.2" + } + }, + "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, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/lazystream": { + "version": "1.0.0", + "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" + } + }, + "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" + } + }, + "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, + "engines": { + "node": ">=6" + } + }, + "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_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, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "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, + "dependencies": { + "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_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, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "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, + "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": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "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, + "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/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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, + "engines": { + "node": ">=8" + } + }, + "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, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.0" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-error": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", + "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", + "dev": true + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, + "node_modules/md5.js": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", + "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", + "dev": true, + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true + }, + "node_modules/minimatch": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/minimatch/node_modules/brace-expansion": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "optional": true + }, + "node_modules/mocha": { + "version": "11.7.5", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.7.5.tgz", + "integrity": "sha512-mTT6RgopEYABzXWFx+GcJ+ZQ32kp4fMf0xvpZIIfSq9Z8lC/++MtcCnQ9t5FP2veYEP95FIYSvW+U9fV4xrlig==", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-stdout": "^1.3.1", + "chokidar": "^4.0.1", + "debug": "^4.3.5", + "diff": "^7.0.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^9.0.5", + "ms": "^2.1.3", + "picocolors": "^1.1.1", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^9.2.0", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha-junit-reporter": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-2.0.2.tgz", + "integrity": "sha512-vYwWq5hh3v1lG0gdQCBxwNipBfvDiAM1PHroQRNp96+2l72e9wEUTw+mzoK+O0SudgfQ7WvTQZ9Nh3qkAYAjfg==", + "dev": true, + "dependencies": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^6.0.1", + "xml": "^1.0.0" + }, + "peerDependencies": { + "mocha": ">=2.2.5" + } + }, + "node_modules/mocha-multi-reporters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", + "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "lodash": "^4.17.15" + }, + "engines": { + "node": ">=6.0.0" + }, + "peerDependencies": { + "mocha": ">=3.1.2" + } + }, + "node_modules/mocha-multi-reporters/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz", + "integrity": "sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA==", + "dev": true + }, + "node_modules/v8flags": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true, + "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" + } } - } - }, - "@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==", - "dev": true, - "requires": { - "@types/webpack": "*" - } - }, - "@types/winreg": { - "version": "1.2.30", - "resolved": "https://registry.npmjs.org/@types/winreg/-/winreg-1.2.30.tgz", - "integrity": "sha1-kdZxDlNtNFucmwF8V0z2qNpkxRg=", - "dev": true - }, - "@types/ws": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.1.tgz", - "integrity": "sha512-EzH8k1gyZ4xih/MaZTXwT2xOkPiIMSrhQ9b8wrlX88L0T02eYsddatQlwVFlEPyEqV0ChpdpNnE51QPH6NVT4Q==", - "dev": true, - "requires": { - "@types/events": "*", - "@types/node": "*" - } - }, - "@types/xml2js": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.2.tgz", - "integrity": "sha512-8aKUBSj3oGcnuiBmDLm3BIk09RYg01mz9HlQ2u4aS17oJ25DxjQrEUVGFSBVNOfM45pQW4OjcBPplq6r/exJdA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@webassemblyjs/ast": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", - "integrity": "sha512-aJMfngIZ65+t71C3y2nBBg5FFG0Okt9m0XEgWZ7Ywgn1oMAT8cNwx00Uv1cQyHtidq0Xn94R4TAywO+LCQ+ZAQ==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.8.5.tgz", - "integrity": "sha512-9p+79WHru1oqBh9ewP9zW95E3XAo+90oth7S5Re3eQnECGq59ly1Ri5tsIipKGpiStHsUYmY3zMLqtk3gTcOtQ==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.8.5.tgz", - "integrity": "sha512-Za/tnzsvnqdaSPOUXHyKJ2XI7PDX64kWtURyGiJJZKVEdFOsdKUCPTNEVFZq3zJ2R0G5wc2PZ5gvdTRFgm81zA==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.8.5.tgz", - "integrity": "sha512-Ri2R8nOS0U6G49Q86goFIPNgjyl6+oE1abW1pS84BuhP1Qcr5JqMwRFT3Ah3ADDDYGEgGs1iyb1DGX+kAi/c/Q==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.8.5.tgz", - "integrity": "sha512-VQAadSubZIhNpH46IR3yWO4kZZjMxN1opDrzePLdVKAZ+DFjkGD/rf4v1jap744uPVU6yjL/smZbRIIJTOUnKQ==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.8.5.tgz", - "integrity": "sha512-kRuX/saORcg8se/ft6Q2UbRpZwP4y7YrWsLXPbbmtepKr22i8Z4O3V5QE9DbZK908dh5Xya4Un57SDIKwB9eow==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.8.5.tgz", - "integrity": "sha512-/O1B236mN7UNEU4t9X7Pj38i4VoU8CcMHyy3l2cV/kIF4U5KoHXDVqcDuOs1ltkac90IM4vZdHc52t1x8Yfs3g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "mamacro": "^0.0.3" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.8.5.tgz", - "integrity": "sha512-Cu4YMYG3Ddl72CbmpjU/wbP6SACcOPVbHN1dI4VJNJVgFwaKf1ppeFJrwydOG3NDHxVGuCfPlLZNyEdIYlQ6QQ==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.8.5.tgz", - "integrity": "sha512-VV083zwR+VTrIWWtgIUpqfvVdK4ff38loRmrdDBgBT8ADXYsEZ5mPQ4Nde90N3UYatHdYoDIFb7oHzMncI02tA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.8.5.tgz", - "integrity": "sha512-aaCvQYrvKbY/n6wKHb/ylAJr27GglahUO89CcGXMItrOBqRarUMxWLJgxm9PJNuKULwN5n1csT9bYoMeZOGF3g==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.8.5.tgz", - "integrity": "sha512-plYUuUwleLIziknvlP8VpTgO4kqNaH57Y3JnNa6DLpu/sGcP6hbVdfdX5aHAV716pQBKrfuU26BJK29qY37J7A==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.8.5.tgz", - "integrity": "sha512-U7zgftmQriw37tfD934UNInokz6yTmn29inT2cAetAsaU9YeVCveWEwhKL1Mg4yS7q//NGdzy79nlXh3bT8Kjw==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.8.5.tgz", - "integrity": "sha512-A41EMy8MWw5yvqj7MQzkDjU29K7UJq1VrX2vWLzfpRHt3ISftOXqrtojn7nlPsZ9Ijhp5NwuODuycSvfAO/26Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/helper-wasm-section": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-opt": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "@webassemblyjs/wast-printer": "1.8.5" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.8.5.tgz", - "integrity": "sha512-BCZBT0LURC0CXDzj5FXSc2FPTsxwp3nWcqXQdOZE4U7h7i8FqtFK5Egia6f9raQLpEKT1VL7zr4r3+QX6zArWg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.8.5.tgz", - "integrity": "sha512-HKo2mO/Uh9A6ojzu7cjslGaHaUU14LdLbGEKqTR7PBKwT6LdPtLLh9fPY33rmr5wcOMrsWDbbdCHq4hQUdd37Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-buffer": "1.8.5", - "@webassemblyjs/wasm-gen": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5" - } }, - "@webassemblyjs/wasm-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.8.5.tgz", - "integrity": "sha512-pi0SYE9T6tfcMkthwcgCpL0cM9nRYr6/6fjgDtL6q/ZqKHdMWvxitRi5JcZ7RI4SNJJYnYNaWy5UUrHQy998lw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-wasm-bytecode": "1.8.5", - "@webassemblyjs/ieee754": "1.8.5", - "@webassemblyjs/leb128": "1.8.5", - "@webassemblyjs/utf8": "1.8.5" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.8.5.tgz", - "integrity": "sha512-daXC1FyKWHF1i11obK086QRlsMsY4+tIOKgBqI1lxAnkp9xe9YMcgOxm9kLe+ttjs5aWV2KKE1TWJCN57/Btsg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/floating-point-hex-parser": "1.8.5", - "@webassemblyjs/helper-api-error": "1.8.5", - "@webassemblyjs/helper-code-frame": "1.8.5", - "@webassemblyjs/helper-fsm": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.8.5.tgz", - "integrity": "sha512-w0U0pD4EhlnvRyeJzBqaVSJAo9w/ce7/WPogeXLzGkO6hzhr4GnQIZ4W4uUt5b9ooAaXPtnXlj0gzsXEOUNYMg==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/wast-parser": "1.8.5", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "JSONStream": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.3.tgz", - "integrity": "sha512-3Sp6WZZ/lXl+nTDoGpGWHEpTnnC6X5fnkolYZR6nwIfzbxxvA8utPWe1gCt7i0m9uVGsSz2IS8K8mJ7HmlduMg==", - "dev": true, - "requires": { - "jsonparse": "^1.2.0", - "through": ">=2.2.7 <3" - } - }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "dev": true, - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" - }, - "dependencies": { - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true + "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==" + } + } + }, + "@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 + } + } + }, + "@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 }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } - } - } - }, - "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==" - }, - "acorn-dynamic-import": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz", - "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw==", - "dev": true - }, - "acorn-globals": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", - "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true - } - } - }, - "acorn-node": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.7.0.tgz", - "integrity": "sha512-XhahLSsCB6X6CJbe+uNu3Mn9sJBNFxtBN9NLgAOQovfS6Kh0lDUtmlclhjn9CvEK7A7YyRU13PXlNcpSiLI9Yw==", - "dev": true, - "requires": { - "acorn": "^6.1.1", - "acorn-dynamic-import": "^4.0.0", - "acorn-walk": "^6.1.1", - "xtend": "^4.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true - } - } - }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", - "dev": true - }, - "address": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/address/-/address-1.0.3.tgz", - "integrity": "sha512-z55ocwKBRLryBs394Sm3ushTtBeg6VAeuku7utSoSnsJKvKcnXFIyC6vh27n3rXyxSgkJBBCAvyOn7gSUcTYjg==", - "dev": true - }, - "agent-base": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", - "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", - "dev": true, - "requires": { - "es6-promisify": "^5.0.0" - } - }, - "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" - } - }, - "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 - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" - }, - "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==", - "dev": true, - "requires": { - "ansi-wrap": "^0.1.0" - } - }, - "ansi-cyan": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", - "integrity": "sha1-U4rlKK+JgvKK4w2G8vF0VtJgmHM=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-escapes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", - "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==", - "dev": true - }, - "ansi-gray": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-gray/-/ansi-gray-0.1.1.tgz", - "integrity": "sha1-KWLPVOyXksSFEKPetSRDaGHvclE=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-red": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ansi-red/-/ansi-red-0.1.1.tgz", - "integrity": "sha1-jGOPnRCAgAo1PJwoyKgcpHBdlGw=", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" - }, - "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==", - "dev": true, - "requires": { - "entities": "^1.1.1" - } - }, - "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==", - "dev": true, - "requires": { - "@babel/runtime-corejs2": "^7.0.0", - "anser": "^1.4.1", - "babel-runtime": "^6.26.0", - "escape-carriage": "^1.2.0" - } - }, - "ansi-wrap": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", - "integrity": "sha1-qCJQ3bABXponyoLoLqYDu/pF768=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "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" - } - }, - "append-transform": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-1.0.0.tgz", - "integrity": "sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==", - "dev": true, - "requires": { - "default-require-extensions": "^2.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" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "arch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.1.0.tgz", - "integrity": "sha1-NhOqRhSQZLPB8GB5Gb8dR4boKIk=" - }, - "archiver": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-3.0.0.tgz", - "integrity": "sha512-5QeR6Xc5hSA9X1rbQfcuQ6VZuUXOaEdB65Dhmk9duuRJHYif/ZyJfuyJqsQrj34PFjU5emv5/MmfgA8un06onw==", - "dev": true, - "requires": { - "archiver-utils": "^2.0.0", - "async": "^2.0.0", - "buffer-crc32": "^0.2.1", - "glob": "^7.0.0", - "readable-stream": "^2.0.0", - "tar-stream": "^1.5.0", - "zip-stream": "^2.0.1" - }, - "dependencies": { "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - } - } - }, - "archiver-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.0.0.tgz", - "integrity": "sha512-JRBgcVvDX4Mwu2RBF8bBaHcQCSxab7afsxAPYDQ5W+19quIPP5CfKE7Ql+UHs9wYvwsaNR8oDuhtf5iqrKmzww==", - "dev": true, - "requires": { - "glob": "^7.0.0", - "graceful-fs": "^4.1.0", - "lazystream": "^1.0.0", - "lodash.assign": "^4.2.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.toarray": "^4.4.0", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "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 - } - } - }, - "archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha1-+cjBN1fMHde8N5rHeyxipcKGjEA=", - "dev": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "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==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "argv": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/argv/-/argv-0.0.2.tgz", - "integrity": "sha1-7L0W+JSbFXGDcRsb2jNPN4QBhas=", - "dev": true - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-filter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/arr-filter/-/arr-filter-1.1.2.tgz", - "integrity": "sha1-Q/3d0JHo7xGqTEXZzcGOLf8XEe4=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/arr-map/-/arr-map-2.0.2.tgz", - "integrity": "sha1-Onc0X/wc814qkYJWAfnljy4kysQ=", - "dev": true, - "requires": { - "make-iterator": "^1.0.0" - } - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-differ": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", - "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, - "array-filter": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz", - "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "array-from": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", - "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=", - "dev": true - }, - "array-initial": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-initial/-/array-initial-1.1.0.tgz", - "integrity": "sha1-L6dLJnOTccOUe9enrcc74zSz15U=", - "dev": true, - "requires": { - "array-slice": "^1.0.0", - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-last": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/array-last/-/array-last-1.3.0.tgz", - "integrity": "sha512-eOCut5rXlI6aCOS7Z7kCplKRKyiFQ6dHFBem4PwlwKeNFk2/XxTrhRh5T9PyaEWGy/NHTZWbY+nsZlNFJu9rYg==", - "dev": true, - "requires": { - "is-number": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-4.0.0.tgz", - "integrity": "sha512-rSklcAIlf1OmFdyAqbnWTLVelsQ58uvZ66S/ZyawjWqIviTWCjg2PzVGw8WUA+nNuPTqb4wgA+NszrJ+08LlgQ==", - "dev": true - } - } - }, - "array-map": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz", - "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI=", - "dev": true - }, - "array-reduce": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz", - "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys=", - "dev": true - }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-sort": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-sort/-/array-sort-1.0.0.tgz", - "integrity": "sha512-ihLeJkonmdiAsD7vpgN3CRcx2J2S0TiYW+IS/5zHBI7mKUq3ySvBdzzBfD236ubDBQFiiyG3SWCPc+msQ9KoYg==", - "dev": true, - "requires": { - "default-compare": "^1.0.0", - "get-value": "^2.0.6", - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.flat": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.1.tgz", - "integrity": "sha512-rVqIs330nLJvfC7JqYvEWwqVr5QjYF1ib02i3YJtR/fICO6527Tjpc/e4Mvmxh3GIePPreRXMdaGyC99YphWEw==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.10.0", - "function-bind": "^1.1.1" - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true - }, - "asap": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", - "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", - "dev": true - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" - }, - "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.4.1", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz", - "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=", - "dev": true, - "requires": { - "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 + "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" + } }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-transform": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz", - "integrity": "sha1-dJRAWIh9goPhidlUYAlHvJj+AGI=", - "requires": { - "escodegen": "~1.2.0", - "esprima": "~1.0.4", - "through": "~2.3.4" - }, - "dependencies": { - "escodegen": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", - "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=", - "requires": { - "esprima": "~1.0.4", - "estraverse": "~1.5.0", - "esutils": "~1.0.0", - "source-map": "~0.1.30" - } - }, - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + "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" + } }, - "estraverse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", - "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + "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==" + } + } }, - "esutils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", - "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + "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" + } }, - "source-map": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", - "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "ast-types": { - "version": "0.7.8", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz", - "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk=" - }, - "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==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.2", - "process-nextick-args": "^1.0.7", - "stream-exhaust": "^1.0.1" - } - }, - "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==" - }, - "async-settle": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-settle/-/async-settle-1.0.0.tgz", - "integrity": "sha1-HQqRS7Aldb7IqPOnTlCA9yssDGs=", - "dev": true, - "requires": { - "async-done": "^1.2.2" - } - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.1.tgz", - "integrity": "sha1-ri1acpR38onWDdf5amMUoi3Wwio=", - "dev": true - }, - "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" - }, - "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" - } + "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.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" - } + "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" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "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 + } + } }, - "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" - } - } - } - }, - "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==" + "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" + } }, - "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" - } + "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" + } }, - "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==" + "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 }, - "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==" - } - } + "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 + } + } }, - "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" - } + "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" + } }, - "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" - } + "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 + } + } }, - "xmlbuilder": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-0.4.3.tgz", - "integrity": "sha1-xGFLp04K0ZbmCcknLNnh3bKKilg=" - } - } - }, - "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=", - "requires": { - "chalk": "^1.1.3", - "esutils": "^2.0.2", - "js-tokens": "^3.0.2" - } - }, - "babel-loader": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.0.4.tgz", - "integrity": "sha512-fhBhNkUToJcW9nV46v8w87AJOwAJDz84c1CL57n3Stj73FANM/b9TbCUK4YhdOwEyZ+OxhYpdeZDNzSI29Firw==", - "dev": true, - "requires": { - "find-cache-dir": "^1.0.0", - "loader-utils": "^1.0.2", - "mkdirp": "^0.5.1", - "util.promisify": "^1.0.0" - } - }, - "babel-plugin-emotion": { - "version": "9.2.11", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-9.2.11.tgz", - "integrity": "sha512-dgCImifnOPPSeXod2znAmgc64NhaaOjGEHROR/M+lmStb3841yK1sgaDYAYMnlvWNz8GnpwIPN0VmNpbWYZ+VQ==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@emotion/babel-utils": "^0.6.4", - "@emotion/hash": "^0.6.2", - "@emotion/memoize": "^0.6.1", - "@emotion/stylis": "^0.7.0", - "babel-plugin-macros": "^2.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "convert-source-map": "^1.5.0", - "find-root": "^1.1.0", - "mkdirp": "^0.5.1", - "source-map": "^0.5.7", - "touch": "^2.0.1" - } - }, - "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==", - "dev": true, - "requires": { - "decache": "^4.4.0" - } - }, - "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==", - "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" - } - } - } - }, - "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=", - "dev": true, - "requires": { - "babel-runtime": "^6.22.0" - } - }, - "babel-polyfill": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-polyfill/-/babel-polyfill-6.26.0.tgz", - "integrity": "sha1-N5k3q8Z9eJWXCtxiHyhM2WbPIVM=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "core-js": "^2.5.0", - "regenerator-runtime": "^0.10.5" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.10.5", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz", - "integrity": "sha1-M2w+/BIgrc7dosn6tntaeVWjNlg=", - "dev": true - } - } - }, - "babel-runtime": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", - "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "requires": { - "core-js": "^2.4.0", - "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" - } - } - }, - "babel-types": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", - "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "esutils": "^2.0.2", - "lodash": "^4.17.4", - "to-fast-properties": "^1.0.3" - }, - "dependencies": { - "to-fast-properties": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true - } - } - }, - "bach": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/bach/-/bach-1.2.0.tgz", - "integrity": "sha1-Szzpa/JxNPeaG0FKUcFONMO9mIA=", - "dev": true, - "requires": { - "arr-filter": "^1.1.1", - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "array-each": "^1.0.0", - "array-initial": "^1.0.0", - "array-last": "^1.1.1", - "async-done": "^1.2.2", - "async-settle": "^1.0.0", - "now-and-later": "^2.0.0" - } - }, - "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": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base16": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/base16/-/base16-1.0.0.tgz", - "integrity": "sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=", - "dev": true - }, - "base64-js": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", - "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" - }, - "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" - } - }, - "bfj": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/bfj/-/bfj-6.1.1.tgz", - "integrity": "sha512-+GUNvzHR4nRyGybQc2WpNJL4MJazMuvf92ueIyA0bIkPRwhhQu3IfZQ2PSoVPpCBJfmoSdOxu5rnotfFLlvYRQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.1", - "check-types": "^7.3.0", - "hoopy": "^0.1.2", - "tryer": "^1.0.0" - } - }, - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "binary-extensions": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.11.0.tgz", - "integrity": "sha1-RqoXUftqL5PuXmibsQh9SxTGwgU=", - "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==", - "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 + "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" + } + } + } }, - "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" - } + "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 }, - "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" - } - } - } - }, - "block-stream": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", - "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "bluebird": { - "version": "3.5.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 - }, - "body-parser": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz", - "integrity": "sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ=", - "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" - } - } - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true - }, - "bootstrap": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.3.1.tgz", - "integrity": "sha512-rXqOmH1VilAt2DyPzluTi2blhk17bO7ef+zLLPlWvG494pDxcM234pJ8wTc/6R40UWizAIIMgxjvxZg5kmsbag==", - "dev": true - }, - "bootstrap-less": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/bootstrap-less/-/bootstrap-less-3.3.8.tgz", - "integrity": "sha1-cfKd1af//t/onxYFu63+CjONrlM=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "brfs": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brfs/-/brfs-2.0.2.tgz", - "integrity": "sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ==", - "dev": true, - "requires": { - "quote-stream": "^1.0.1", - "resolve": "^1.1.5", - "static-module": "^3.0.2", - "through2": "^2.0.0" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true - }, - "brotli": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz", - "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=", - "requires": { - "base64-js": "^1.1.2" - }, - "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==" - } - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==", - "dev": true - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-mime": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/browserify-mime/-/browserify-mime-1.2.9.tgz", - "integrity": "sha1-rrGvKN5sDXpqLOQK22j/GEIq8x8=" - }, - "browserify-optional": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz", - "integrity": "sha1-HhNyLP3g2F8SFnbCpyztUzoBiGk=", - "requires": { - "ast-transform": "0.0.0", - "ast-types": "^0.7.0", - "browser-resolve": "^1.8.1" - } - }, - "browserify-rsa": { - "version": "4.0.1", - "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz", - "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz", - "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", - "dev": true, - "requires": { - "bn.js": "^4.1.1", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.2", - "elliptic": "^6.0.0", - "inherits": "^2.0.1", - "parse-asn1": "^5.0.0" - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.1.1.tgz", - "integrity": "sha512-VBorw+tgpOtZ1BYhrVSVTzTt/3+vSE3eFUh0N2GCFK1HffceOaf32YS/bs6WiFhjDAblAFrx85jMy3BG9fBK2Q==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30000884", - "electron-to-chromium": "^1.3.62", - "node-releases": "^1.0.0-alpha.11" - } - }, - "buffer": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-3.6.0.tgz", - "integrity": "sha1-pyyTb3e5a/UvX357RnGAYoVR3vs=", - "dev": true, - "requires": { - "base64-js": "0.0.8", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "dev": true, - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", - "dev": true - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true - }, - "buffer-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-1.0.0.tgz", - "integrity": "sha1-WWFrSYME1Var1GaWayLu2j7KX74=", - "dev": true - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", - "dev": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "cacache": { - "version": "10.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-10.0.4.tgz", - "integrity": "sha512-Dph0MzuH+rTQzGPNT9fAnrPmMmjKfST6trxJeK7NQuHRaVw24VzPRWTmg9MpcwOVQZO0E1FBICUlFeNaKPIfHA==", - "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" - } - } - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", - "dev": true, - "requires": { - "clone-response": "1.0.2", - "get-stream": "3.0.0", - "http-cache-semantics": "3.8.1", - "keyv": "3.0.0", - "lowercase-keys": "1.0.0", - "normalize-url": "2.0.1", - "responselike": "1.0.2" - }, - "dependencies": { - "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", - "dev": true - } - } - }, - "caching-transform": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-3.0.2.tgz", - "integrity": "sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==", - "dev": true, - "requires": { - "hasha": "^3.0.0", - "make-dir": "^2.0.0", - "package-hash": "^3.0.0", - "write-file-atomic": "^2.4.2" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } + "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" + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dev": true, + "requires": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "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=", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", - "dev": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true - } - } - }, - "caniuse-lite": { - "version": "1.0.30000887", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000887.tgz", - "integrity": "sha512-AHpONWuGFWO8yY9igdXH94tikM6ERS84286r0cAMAXYFtJBk76lhiMhtCxBJNBZsD6hzlvpWZ2AtbVFEkf4JQA==", - "dev": true - }, - "canvas": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.0.1.tgz", - "integrity": "sha512-aVESjDBMXGRL+aZqjFtxMVOg8KzHhNcKIscoeC8OROccmApKOriHsnySxq228Kc+3tzB9Qc6tzD4ukp9Zjwz1Q==", - "dev": true, - "requires": { - "nan": "^2.11.1", - "node-pre-gyp": "^0.11.0" - }, - "dependencies": { - "node-pre-gyp": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", - "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", - "dev": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "caw": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", - "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", - "dev": true, - "requires": { - "get-proxy": "^2.0.0", - "isurl": "^1.0.0-alpha5", - "tunnel-agent": "^0.6.0", - "url-to-options": "^1.0.1" - } - }, - "chai": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.1.2.tgz", - "integrity": "sha1-D2RYS6ZC8PKs4oBiefTwbKI61zw=", - "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" - } - }, - "chai-arrays": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chai-arrays/-/chai-arrays-2.0.0.tgz", - "integrity": "sha512-jWAvZu1BV8tL3pj0iosBECzzHEg+XB1zSnMjJGX83bGi/1GlGdDO7J/A0sbBBS6KJT0FVqZIzZW9C6WLiMkHpQ==", - "dev": true - }, - "chai-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", - "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", - "dev": true, - "requires": { - "check-error": "^1.0.2" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "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": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.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 - }, - "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 - }, - "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 - }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", - "dev": true - }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" - }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", - "dev": true - }, - "check-types": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/check-types/-/check-types-7.4.0.tgz", - "integrity": "sha512-YbulWHdfP99UfZ73NcUDlNJhEIDgm9Doq9GhpyXbF+7Aegi3CVV7qqMCKTTqJxlvEvnQBp9IA+dxsGN6xK/nSg==", - "dev": true - }, - "cheerio": { - "version": "1.0.0-rc.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.2.tgz", - "integrity": "sha1-S59TqBsn5NXawxwP/Qz6A8xoMNs=", - "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" - } - }, - "chokidar": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.5.tgz", - "integrity": "sha512-i0TprVWp+Kj4WRPtInjexJ8Q+BqTE909VpH8xVhXrJkoc5QC8VO9TryGOqTr+2hljzc1sC62t22h5tZePodM/A==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "dependencies": { - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "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, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "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, - "optional": true - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "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 - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", - "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "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.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "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", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": 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" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "dev": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } - }, - "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, - "optional": true - }, - "needle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.3.0.tgz", - "integrity": "sha512-QBZu7aAFR0522EyaXZM0FZ9GLpq6lvQ3uq8gteiDUp7wKdy0lSd2hPlgFwVuW1CBkfEs9PfDQsQzZghLs/psdg==", - "dev": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.12.0.tgz", - "integrity": "sha512-4KghwV8vH5k+g2ylT+sLTjy5wmUOb9vPhnM8NHvRf9dHmnW/CndrFXy2aRPaPST6dugXSdHXfeaHQm77PIz/1A==", - "dev": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "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", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "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=", - "dev": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "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=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": 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, - "optional": true - }, - "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": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true + "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", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "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, - "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.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "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 - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true, - "optional": true - }, - "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, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "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, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true - } - } + } }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } + "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 + }, + "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" + } + } + } }, - "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 + "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" + } }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "upath": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz", - "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==", - "dev": true - } - } - }, - "chownr": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.1.tgz", - "integrity": "sha512-j38EvO5+LHX84jlo6h4UzmOwi0UgW61WRyPtJz4qaadK5eY3BTS5TY/S1Stc3Uk2lIM6TPevAlULiEJwie860g==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, - "ci-info": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz", - "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "classnames": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.2.6.tgz", - "integrity": "sha512-JR/iSQOSt+LQIWwrwEzJ9uk0xfN3mTVYMwt1Ir5mUcSN6pU+V4zQFFaJsclJbPuAUQH+yfWef6tm7l1quW3C8Q==", - "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==", - "dev": true, - "requires": { - "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 - } - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "dev": true, - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-table": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/cli-table/-/cli-table-0.3.1.tgz", - "integrity": "sha1-9TsFJmqLGguTSz0IIebi3FkUriM=", - "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 - } - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "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 + "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" + } }, - "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" - } - } - } - }, - "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": "2.0.2", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-2.0.2.tgz", - "integrity": "sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ==", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.0", - "shallow-clone": "^1.0.0" - } - }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "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==", - "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 + "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" + } }, - "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" - } + "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" + } }, - "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" - } - } - } - }, - "clsx": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.4.tgz", - "integrity": "sha512-1mQ557MIZTrL/140j+JVdRM6e31/OA4vTYxXgqIIZlndyfjHpyawKZia1Im05Vp9BWmImkcNrNtFYQMyFcgJDg==" - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, - "codecov": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.5.0.tgz", - "integrity": "sha512-/OsWOfIHaQIr7aeZ4pY0UC1PZT6kimoKFOFYFNb6wxo3iw12nRrh+mNGH72rnXxNsq6SGfesVPizm/6Q3XqcFQ==", - "dev": true, - "requires": { - "argv": "^0.0.2", - "ignore-walk": "^3.0.1", - "js-yaml": "^3.13.1", - "teeny-request": "^3.11.3", - "urlgrey": "^0.4.4" - } - }, - "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==", - "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": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", - "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.2" - } - }, - "color-convert": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.1.tgz", - "integrity": "sha512-mjGanIiwQJskCC18rPR6OmrZ6fm2Lc7PeGFYwCmy5J34wC6F1PzdGL6xeMfmgicfYcNLGuVFA3WzXtIDCQSZxQ==", - "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=" - }, - "color-string": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.3.tgz", - "integrity": "sha512-dC2C5qeWoYkxki5UAXapdjqO672AM4vZuPGRQfO8b5HKuKGBbKWpITyDYN7TOFKvRW7kOgAn3746clDBMDJyQw==", - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "color-support": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", - "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", - "dev": true - }, - "colornames": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/colornames/-/colornames-1.1.1.tgz", - "integrity": "sha1-+IiQMGhcfE/54qVZ9Qd+t2qBb5Y=" - }, - "colors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.3.0.tgz", - "integrity": "sha512-EDpX3a7wHMWFA7PUHWPHNWqOxIIRSJetuwl0AS5Oi/5FMV8kWm69RTlgm00GKjBO1xFHMtBbL49yRtMMdticBw==" - }, - "colorspace": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", - "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", - "requires": { - "color": "3.0.x", - "text-hex": "1.0.x" - } - }, - "combined-stream": { - "version": "1.0.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==" - }, - "commandpost": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/commandpost/-/commandpost-1.4.0.tgz", - "integrity": "sha512-aE2Y4MTFJ870NuB/+2z1cXBhSBBzRydVVjzhFC4gtenEhpnj15yu0qptWGJsO9YGrcPZ3ezX8AWb1VA391MKpQ==", - "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" - } - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "compare-module-exports": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/compare-module-exports/-/compare-module-exports-2.1.0.tgz", - "integrity": "sha512-3Lc0sTIuX1jmY2K2RrXRJOND6KsRTX2D4v3+eu1PDptsuJZVK4LZc852eZa9I+avj0NrUKlTNgqvccNOH6mbGg==", - "dev": true - }, - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=", - "dev": true - }, - "compress-commons": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-1.2.2.tgz", - "integrity": "sha1-UkqfEJA/OoEzibAiXSfEi7dRiQ8=", - "dev": true, - "requires": { - "buffer-crc32": "^0.2.1", - "crc32-stream": "^2.0.0", - "normalize-path": "^2.0.0", - "readable-stream": "^2.0.0" - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "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": { + "pify": "^3.0.0" + }, + "dependencies": { + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } + }, + "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 + } + } }, - "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==", - "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" - } + "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 + } + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "config-chain": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", - "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", - "dev": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true - }, - "content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=", - "dev": true - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "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=" - }, - "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==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true - }, - "copy-props": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/copy-props/-/copy-props-2.0.4.tgz", - "integrity": "sha512-7cjuUME+p+S3HZlbllgsn2CDwS+5eCCX16qBgNC4jgSTf49qR1VKy/Zhl400m0IQXl/bPGEVqncgUUMjrr4s8A==", - "dev": true, - "requires": { - "each-props": "^1.3.0", - "is-plain-object": "^2.0.1" - } - }, - "copy-webpack-plugin": { - "version": "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==", - "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" - } + "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 + } + } }, - "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" - } + "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 + }, + "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": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true + } + } }, - "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" - } - } - } - }, - "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==" - }, - "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==", - "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" - } - } - } - }, - "coveralls": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-3.0.4.tgz", - "integrity": "sha512-eyqUWA/7RT0JagiL0tThVhjbIjoiEUyWCjtUJoOPcWoeofP5WK/jb2OJYoBFrR6DvplR+AxOyuBqk4JHkk5ykA==", - "dev": true, - "requires": { - "growl": "~> 1.10.0", - "js-yaml": "^3.11.0", - "lcov-parse": "^0.0.10", - "log-driver": "^1.2.7", - "minimist": "^1.2.0", - "request": "^2.86.0" - } - }, - "cp-file": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/cp-file/-/cp-file-6.2.0.tgz", - "integrity": "sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "make-dir": "^2.0.0", - "nested-error-stacks": "^2.0.0", - "pify": "^4.0.1", - "safe-buffer": "^5.0.1" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } + "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" + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "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 + } + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "cpx": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cpx/-/cpx-1.5.0.tgz", - "integrity": "sha1-GFvgGFEdhycN7czCkxceN2VauI8=", - "dev": true, - "requires": { - "babel-runtime": "^6.9.2", - "chokidar": "^1.6.0", - "duplexer": "^0.1.1", - "glob": "^7.0.5", - "glob2base": "^0.0.12", - "minimatch": "^3.0.2", - "mkdirp": "^0.5.1", - "resolve": "^1.1.7", - "safe-buffer": "^5.0.1", - "shell-quote": "^1.6.1", - "subarg": "^1.0.0" - }, - "dependencies": { - "anymatch": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-1.3.2.tgz", - "integrity": "sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA==", - "dev": true, - "requires": { - "micromatch": "^2.1.5", - "normalize-path": "^2.0.0" - } + "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" + } }, - "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 + "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" + } }, - "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" - } + "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" + } }, - "chokidar": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", - "integrity": "sha1-eY5ol3gVHIB2tLNg5e3SjNortGg=", - "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" - } - }, - "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" - } + "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" + } }, - "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" - } + "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 }, - "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 + "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" + } }, - "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" - } + "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": {} }, - "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" - } + "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" + } }, - "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" - } - } - } - }, - "crc": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", - "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", - "dev": true, - "requires": { - "buffer": "^5.1.0" - }, - "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 + "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" + } }, - "buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", - "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4" - } - } - } - }, - "crc32-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-2.0.0.tgz", - "integrity": "sha1-483TtN8xaN10494/u8t7KX/pCPQ=", - "dev": true, - "requires": { - "crc": "^3.4.4", - "readable-stream": "^2.0.0" - } - }, - "create-ecdh": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz", - "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.0.0" - } - }, - "create-emotion": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-9.2.12.tgz", - "integrity": "sha512-P57uOF9NL2y98Xrbl2OuiDQUZ30GVmASsv5fbsjF4Hlraip2kyAvMm+2PoYUvFFw03Fhgtxk3RqZSm2/qHL9hA==", - "dev": true, - "requires": { - "@emotion/hash": "^0.6.2", - "@emotion/memoize": "^0.6.1", - "@emotion/stylis": "^0.7.0", - "@emotion/unitless": "^0.6.2", - "csstype": "^2.5.2", - "stylis": "^3.5.0", - "stylis-rule-sheet": "^0.0.10" - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "http://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": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "crypto-js": { - "version": "3.1.9-1", - "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", - "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=" - }, - "css": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.3.tgz", - "integrity": "sha512-0W171WccAjQGGTKLhw4m2nnl0zPHUlTO/I8td4XzJgIB8Hg3ZZx71qT4G4eX8OVsSiaAKiUMy73E3nsbPlg2DQ==", - "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": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", - "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", - "dev": true, - "requires": { - "boolbase": "~1.0.0", - "css-what": "2.1", - "domutils": "1.5.1", - "nth-check": "~1.0.1" - }, - "dependencies": { - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - } - } - }, - "css-selector-tokenizer": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.7.1.tgz", - "integrity": "sha512-xYL0AMZJ4gFzJQsHUKa5jiWWi2vH77WVNg7JYRyewwj6oPh4yb/y6Y9ZCw9dsj/9UauMhtuxR+ogQd//EdEVNA==", - "dev": true, - "requires": { - "cssesc": "^0.1.0", - "fastparse": "^1.1.1", - "regexpu-core": "^1.0.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - }, - "regexpu-core": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-1.0.0.tgz", - "integrity": "sha1-hqdj9Y7k18L2sQLkdkBQ3n7ZDGs=", - "dev": true, - "requires": { - "regenerate": "^1.2.1", - "regjsgen": "^0.2.0", - "regjsparser": "^0.1.4" - } - }, - "regjsgen": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz", - "integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=", - "dev": true - }, - "regjsparser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz", - "integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - } - } - } - }, - "css-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 - }, - "cssom": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", - "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", - "dev": true - }, - "cssstyle": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.2.tgz", - "integrity": "sha512-43wY3kl1CVQSvL7wUY1qXkxVGkStjpkDmVjiIKX8R97uhajy8Bybay78uOtqvh7Q5GK75dNPfW0geWjE6qQQow==", - "dev": true, - "requires": { - "cssom": "0.3.x" - } - }, - "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 - }, - "cucumber-html-reporter": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/cucumber-html-reporter/-/cucumber-html-reporter-4.0.5.tgz", - "integrity": "sha512-fhIIvna2KwxEq9XxAkbfPodY1ZBvtIos8fD5ZwaK9mrmpBBVcEfmE9CwERyWcaDpIXvJfo1nQTZanMjG9/xc8Q==", - "dev": true, - "requires": { - "find": "^0.2.7", - "fs-extra": "^3.0.1", - "js-base64": "^2.3.2", - "jsonfile": "^3.0.0", - "lodash": "^4.17.5", - "opn": "5.3.0" - }, - "dependencies": { - "fs-extra": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", - "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^3.0.0", - "universalify": "^0.1.0" - } - }, - "jsonfile": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", - "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - } - } - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", - "dev": true, - "requires": { - "array-find-index": "^1.0.1" - } - }, - "cyclist": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", - "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=", - "dev": true - }, - "d": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", - "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", - "dev": true, - "requires": { - "es5-ext": "^0.10.9" - } - }, - "d3-array": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz", - "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==", - "dev": true - }, - "d3-bboxCollide": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/d3-bboxCollide/-/d3-bboxCollide-1.0.4.tgz", - "integrity": "sha512-Sc8FKGGeejlowLW1g/0WBrVcbd++SBRW4N8OuZhVeRAfwlTL96+75JKlFfHweYdYRui1zPabfNXZrNaphBjS+w==", - "dev": true, - "requires": { - "d3-quadtree": "1.0.1" - } - }, - "d3-brush": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.0.6.tgz", - "integrity": "sha512-lGSiF5SoSqO5/mYGD5FAeGKKS62JdA1EV7HPrU2b5rTX4qEJJtpjaGLJngjnkewQy7UnGstnFd3168wpf5z76w==", - "dev": true, - "requires": { - "d3-dispatch": "1", - "d3-drag": "1", - "d3-interpolate": "1", - "d3-selection": "1", - "d3-transition": "1" - } - }, - "d3-chord": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz", - "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==", - "dev": true, - "requires": { - "d3-array": "1", - "d3-path": "1" - } - }, - "d3-collection": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz", - "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==", - "dev": true - }, - "d3-color": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.2.3.tgz", - "integrity": "sha512-x37qq3ChOTLd26hnps36lexMRhNXEtVxZ4B25rL0DVdDsGQIJGB18S7y9XDwlDD6MD/ZBzITCf4JjGMM10TZkw==" - }, - "d3-contour": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz", - "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==", - "dev": true, - "requires": { - "d3-array": "^1.1.1" - } - }, - "d3-dispatch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz", - "integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g==", - "dev": true - }, - "d3-drag": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.3.tgz", - "integrity": "sha512-8S3HWCAg+ilzjJsNtWW1Mutl74Nmzhb9yU6igspilaJzeZVFktmY6oO9xOh5TDk+BM2KrNFjttZNoJJmDnkjkg==", - "dev": true, - "requires": { - "d3-dispatch": "1", - "d3-selection": "1" - } - }, - "d3-ease": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz", - "integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ==" - }, - "d3-force": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.1.2.tgz", - "integrity": "sha512-p1vcHAUF1qH7yR+e8ip7Bs61AHjLeKkIn8Z2gzwU2lwEf2wkSpWdjXG0axudTHsVFnYGlMkFaEsVy2l8tAg1Gw==", - "dev": true, - "requires": { - "d3-collection": "1", - "d3-dispatch": "1", - "d3-quadtree": "1", - "d3-timer": "1" - } - }, - "d3-format": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz", - "integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ==", - "dev": true - }, - "d3-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==", - "requires": { - "d3-color": "1" - } - }, - "d3-path": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.7.tgz", - "integrity": "sha512-q0cW1RpvA5c5ma2rch62mX8AYaiLX0+bdaSM2wxSU9tXjU4DNvkx9qiUvjkuWCj3p22UO/hlPivujqMiR9PDzA==", - "dev": true - }, - "d3-polygon": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz", - "integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w==", - "dev": true - }, - "d3-quadtree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.1.tgz", - "integrity": "sha1-E74CViTxEEBe1DU2xQaq7Bme1ZE=", - "dev": true - }, - "d3-sankey-circular": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/d3-sankey-circular/-/d3-sankey-circular-0.25.0.tgz", - "integrity": "sha512-maYak22afBAvmybeaopd1cVUNTIroEHhWCmh19gEQ+qgOhBkTav8YeP3Uw4OV/K4OksWaQrhhBOE4Rcxgc2JbQ==", - "dev": true, - "requires": { - "d3-array": "^1.2.1", - "d3-collection": "^1.0.4", - "d3-shape": "^1.2.0" - } - }, - "d3-scale": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-1.0.7.tgz", - "integrity": "sha512-KvU92czp2/qse5tUfGms6Kjig0AhHOwkzXG0+PqIJB3ke0WUv088AHMZI0OssO9NCkXt4RP8yju9rpH8aGB7Lw==", - "dev": true, - "requires": { - "d3-array": "^1.2.0", - "d3-collection": "1", - "d3-color": "1", - "d3-format": "1", - "d3-interpolate": "1", - "d3-time": "1", - "d3-time-format": "2" - } - }, - "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 - }, - "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==", - "dev": true, - "requires": { - "d3-path": "1" - } - }, - "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 - }, - "d3-time-format": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz", - "integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==", - "dev": true, - "requires": { - "d3-time": "1" - } - }, - "d3-timer": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz", - "integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg==" - }, - "d3-transition": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.1.3.tgz", - "integrity": "sha512-tEvo3qOXL6pZ1EzcXxFcPNxC/Ygivu5NoBY6mbzidATAeML86da+JfVIUzon3dNM6UX6zjDx+xbYDmMVtTSjuA==", - "dev": true, - "requires": { - "d3-color": "1", - "d3-dispatch": "1", - "d3-ease": "1", - "d3-interpolate": "1", - "d3-selection": "^1.1.0", - "d3-timer": "1" - } - }, - "d3-voronoi": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz", - "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, - "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 - }, - "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" - } - }, - "debug-fabulous": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/debug-fabulous/-/debug-fabulous-1.1.0.tgz", - "integrity": "sha512-GZqvGIgKNlUnHUPQhepnUZFIMoi3dgZKQBzKDeL2g7oJF9SNAji/AAu36dusFUas0O+pae74lNeoIPHqXWDkLg==", - "dev": true, - "requires": { - "debug": "3.X", - "memoizee": "0.4.X", - "object-assign": "4.X" - }, - "dependencies": { - "debug": { - "version": "3.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" - } - } - } - }, - "decache": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/decache/-/decache-4.4.0.tgz", - "integrity": "sha1-b232uF1+fEQQqTL/wmSJt46azRM=", - "dev": true, - "requires": { - "callsite": "^1.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "decompress": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", - "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", - "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 - } - } - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "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": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true - } - } - }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", - "dev": true, - "requires": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, - "dependencies": { - "file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true - } - } - }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", - "dev": true, - "requires": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, - "dependencies": { - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true - } - } - }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", - "dev": true, - "requires": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" - }, - "dependencies": { - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true + "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" + } }, - "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" - } + "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", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + } + }, + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true + } + } }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "deep-assign": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/deep-assign/-/deep-assign-1.0.0.tgz", - "integrity": "sha1-sJJ0O+hCfcYh6gBnzex+cN0Z83s=", - "dev": true, - "requires": { - "is-obj": "^1.0.0" - } - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } - }, - "deep-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", - "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.1.1.tgz", - "integrity": "sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w==", - "dev": true - }, - "default-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz", - "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==", - "dev": true, - "requires": { - "kind-of": "^5.0.2" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "default-require-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-2.0.0.tgz", - "integrity": "sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=", - "dev": true, - "requires": { - "strip-bom": "^3.0.0" - } - }, - "default-resolution": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz", - "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=", - "dev": true - }, - "define-properties": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", - "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", - "dev": true, - "requires": { - "foreach": "^2.0.5", - "object-keys": "^1.0.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==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "del": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/del/-/del-3.0.0.tgz", - "integrity": "sha1-U+z2mf/LyzljdpGrE7rxYIGXZuU=", - "dev": true, - "requires": { - "globby": "^6.1.0", - "is-path-cwd": "^1.0.0", - "is-path-in-cwd": "^1.0.0", - "p-map": "^1.1.1", - "pify": "^3.0.0", - "rimraf": "^2.2.8" - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true - }, - "denodeify": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/denodeify/-/denodeify-1.2.1.tgz", - "integrity": "sha1-OjYof1A05pnnV3kBBSwubJQlFjE=", - "dev": true - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=", - "dev": true - }, - "des.js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz", - "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=", - "dev": true - }, - "detect-port-alt": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/detect-port-alt/-/detect-port-alt-1.1.6.tgz", - "integrity": "sha512-5tQykt+LqfJFBEYaDITx7S7cR7mJ/zQmLXZ2qt5w04ainYZw6tBf9dBunMjVeVOdYVRUzUOE4HkY5J7+uttb5Q==", - "dev": true, - "requires": { - "address": "^1.0.1", - "debug": "^2.6.0" - } - }, - "dfa": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", - "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" - }, - "diagnostic-channel": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz", - "integrity": "sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=", - "requires": { - "semver": "^5.3.0" - } - }, - "diagnostic-channel-publishers": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz", - "integrity": "sha1-ji1geottef6IC1SLxYzGvrKIxPM=" - }, - "diagnostics": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/diagnostics/-/diagnostics-1.1.1.tgz", - "integrity": "sha512-8wn1PmdunLJ9Tqbx+Fx/ZEuHfJf4NKSN2ZBj7SJC/OWRWha843+WsTjqMe1B5E3p28jqBlp+mJ2fPVxPyNgYKQ==", - "requires": { - "colorspace": "1.1.x", - "enabled": "1.0.x", - "kuler": "1.0.x" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==" - }, - "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==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - }, - "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" - } - } - } - }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", - "dev": true - }, - "doctrine": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", - "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=", - "dev": true, - "requires": { - "esutils": "^1.1.6", - "isarray": "0.0.1" - }, - "dependencies": { - "esutils": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz", - "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=", - "dev": true + "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 + } + } + }, + "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 }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - } - } - }, - "dom-converter": { - "version": "0.1.4", - "resolved": "http://registry.npmjs.org/dom-converter/-/dom-converter-0.1.4.tgz", - "integrity": "sha1-pF71cnuJDJv/5tfIduexnLDhfzs=", - "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 - } - } - }, - "dom-helpers": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-3.4.0.tgz", - "integrity": "sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA==", - "requires": { - "@babel/runtime": "^7.1.2" - } - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - } - } - }, - "dom-walk": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz", - "integrity": "sha1-ZyIm3HTI95mtNTB9+TaroRrNYBg=", - "dev": true - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.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==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "download": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/download/-/download-7.0.0.tgz", - "integrity": "sha512-0Fe/CAjKycx12IG9We9gYlLP03BEcWTpttg7P5mwfOiQTg584kpuHqP7F61RkUJM+mfEdEU9TJonm0PJp5rQLw==", - "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" - } - }, - "duplexer": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", - "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=", - "dev": true - }, - "duplexer2": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", - "requires": { - "readable-stream": "^2.0.2" - } - }, - "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==", - "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" - } - } - } - }, - "each-props": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz", - "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.1", - "object.defaults": "^1.1.0" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "editorconfig": { - "version": "0.15.3", - "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", - "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", - "dev": true, - "requires": { - "commander": "^2.19.0", - "lru-cache": "^4.1.5", - "semver": "^5.6.0", - "sigmund": "^1.0.1" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "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" + } }, - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } + "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" + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "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-download": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.1.tgz", - "integrity": "sha512-FjEWG9Jb/ppK/2zToP+U5dds114fM1ZOJqMAR4aXXL5CvyPE9fiqBK/9YcwC9poIFQTEJk/EM/zyRwziziRZrg==", - "dev": true, - "requires": { - "debug": "^3.0.0", - "env-paths": "^1.0.0", - "fs-extra": "^4.0.1", - "minimist": "^1.2.0", - "nugget": "^2.0.1", - "path-exists": "^3.0.0", - "rc": "^1.2.1", - "semver": "^5.4.1", - "sumchecker": "^2.0.2" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "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" + } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "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" + } }, - "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 - } - } - }, - "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==", - "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" - } - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha1-TapNnbAPmBmIDHn6RXrlsJof04k=", - "dev": true - }, - "emotion": { - "version": "9.2.12", - "resolved": "https://registry.npmjs.org/emotion/-/emotion-9.2.12.tgz", - "integrity": "sha512-hcx7jppaI8VoXxIWEhxpDW7I+B4kq9RNzQLmsrF6LY8BGKqe2N+gFAQr0EfuFucFlPs2A9HM4+xNj4NeqEWIOQ==", - "dev": true, - "requires": { - "babel-plugin-emotion": "^9.2.11", - "create-emotion": "^9.2.12" - } - }, - "enabled": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/enabled/-/enabled-1.0.2.tgz", - "integrity": "sha1-ll9lE9LC0cX0ZStkouM5ZGf8L5M=", - "requires": { - "env-variable": "0.0.x" - } - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", - "dev": true - }, - "encoding": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", - "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", - "requires": { - "iconv-lite": "~0.4.13" - } - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", - "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.4.0", - "tapable": "^1.0.0" - } - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", - "dev": true - }, - "env-paths": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", - "integrity": "sha1-QWgTO0K7BcOKNbGuQ5fIKYqzaeA=", - "dev": true - }, - "env-variable": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/env-variable/-/env-variable-0.0.5.tgz", - "integrity": "sha512-zoB603vQReOFvTg5xMl9I1P2PnHsHQQKTEowsKKD7nseUfJq6UWzK+4YtlWUO1nhiQUxe6XMkk+JleSZD1NZFA==" - }, - "enzyme": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.7.0.tgz", - "integrity": "sha512-QLWx+krGK6iDNyR1KlH5YPZqxZCQaVF6ike1eDJAOg0HvSkSCVImPsdWaNw6v+VrnK92Kg8jIOYhuOSS9sBpyg==", - "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" - }, - "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 - } - } - }, - "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==", - "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" - } - }, - "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==", - "dev": true, - "requires": { - "function.prototype.name": "^1.1.0", - "object.assign": "^4.1.0", - "prop-types": "^15.6.2" - } - }, - "errno": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", - "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.1.tgz", - "integrity": "sha1-+FWobOYa3E6GIcPNoh56dhLDqNw=", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "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==", - "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" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es5-ext": { - "version": "0.10.43", - "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.43.tgz", - "integrity": "sha512-cZd1vezWuTM5qMlasKWqQFioFKwO352nVBzhOTMUf/pKQl5Gcq5EdJzqtSNXKnFQSCJDiQZjCYlYbnzFB657OA==", - "dev": true, - "requires": { - "es6-iterator": "~2.0.3", - "es6-symbol": "~3.1.1", - "next-tick": "1" - } - }, - "es6-error": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", - "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", - "dev": true - }, - "es6-iterator": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", - "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.35", - "es6-symbol": "^3.1.1" - } - }, - "es6-map": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", - "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-set": "~0.1.5", - "es6-symbol": "~3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-promise": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", - "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==", - "dev": true - }, - "es6-promisify": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", - "dev": true, - "requires": { - "es6-promise": "^4.0.3" - } - }, - "es6-set": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", - "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14", - "es6-iterator": "~2.0.1", - "es6-symbol": "3.1.1", - "event-emitter": "~0.3.5" - } - }, - "es6-symbol": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", - "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "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=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "^0.10.14", - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "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=" - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "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=", - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "dependencies": { - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - } - } - }, - "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==", - "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 - } - } - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=" - }, - "estree-is-function": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-is-function/-/estree-is-function-1.0.0.tgz", - "integrity": "sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA==", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "event-emitter": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", - "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", - "dev": true, - "requires": { - "d": "1", - "es5-ext": "~0.10.14" - } - }, - "event-stream": { - "version": "3.3.4", - "resolved": "http://registry.npmjs.org/event-stream/-/event-stream-3.3.4.tgz", - "integrity": "sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=", - "dev": true, - "requires": { - "duplexer": "~0.1.1", - "from": "~0", - "map-stream": "~0.1.0", - "pause-stream": "0.0.11", - "split": "0.3", - "stream-combiner": "~0.0.4", - "through": "~2.3.1" - } - }, - "events": { - "version": "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=", - "dev": true, - "requires": { - "original": ">=0.0.5" - } - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", - "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", - "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" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "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": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + } + } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "expand-range": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/expand-range/-/expand-range-1.8.2.tgz", - "integrity": "sha1-opnv/TNf4nIeuujiV+x5ZE/IUzc=", - "dev": true, - "requires": { - "fill-range": "^2.1.0" - }, - "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" - } + "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": { + "ms": "^2.1.1" + } + } + } }, - "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" - } + "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": { + "ms": "^2.1.1" + } + } + } }, - "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" - } + "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": { + "ms": "^2.1.1" + } + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } }, - "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" - } - } - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "expose-loader": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-0.7.5.tgz", - "integrity": "sha512-iPowgKUZkTPX5PznYsmifVj9Bob0w2wTHVkt/eYNPSzyebkUgIedmskf/kcfEIWpiWjg3JRjnW+a17XypySMuw==", - "dev": true - }, - "express": { - "version": "4.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" - } - }, - "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.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=", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "2.2.0", - "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", - "dev": true, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - }, - "dependencies": { - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "dev": true, - "requires": { - "os-tmpdir": "~1.0.2" - } - } - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } + "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": { + "@babel/runtime": "^7.10.2", + "@babel/runtime-corejs3": "^7.10.2" + } + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extract-zip": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.6.7.tgz", - "integrity": "sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=", - "dev": true, - "requires": { - "concat-stream": "1.6.2", - "debug": "2.6.9", - "mkdirp": "0.5.1", - "yauzl": "2.4.1" - }, - "dependencies": { - "yauzl": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", - "integrity": "sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=", - "dev": true, - "requires": { - "fd-slicer": "~1.0.1" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "falafel": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz", - "integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=", - "requires": { - "acorn": "^5.0.0", - "foreach": "^2.0.5", - "isarray": "0.0.1", - "object-keys": "^1.0.6" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - } - } - }, - "fancy-log": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/fancy-log/-/fancy-log-1.3.2.tgz", - "integrity": "sha1-9BEl49hPLn2JpD0G2VjI94vha+E=", - "dev": true, - "requires": { - "ansi-gray": "^0.1.1", - "color-support": "^1.1.3", - "time-stamp": "^1.0.0" - } - }, - "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=" - }, - "fast-plist": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/fast-plist/-/fast-plist-0.1.2.tgz", - "integrity": "sha1-pFr/NFGWAG1AbKbNzQX2kFHvNbg=" - }, - "fast-safe-stringify": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.6.tgz", - "integrity": "sha512-q8BZ89jjc+mz08rSxROs8VsrBBcn1SIw1kq9NjolL509tkABRk9io01RAjSaEv1Xb2uFLt8VtRiZbGp5H8iDtg==" - }, - "fastparse": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", - "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", - "dev": true - }, - "faye-websocket": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.1.tgz", - "integrity": "sha1-8O/hjE9W5PQK/H4Gxxn9XuYYjzg=", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "fbjs": { - "version": "0.8.17", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz", - "integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=", - "dev": true, - "requires": { - "core-js": "^1.0.0", - "isomorphic-fetch": "^2.1.1", - "loose-envify": "^1.0.0", - "object-assign": "^4.1.0", - "promise": "^7.1.1", - "setimmediate": "^1.0.5", - "ua-parser-js": "^0.7.18" - }, - "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" - } - } - } - }, - "fd-slicer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.0.1.tgz", - "integrity": "sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=", - "dev": true, - "requires": { - "pend": "~1.2.0" - } - }, - "fecha": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fecha/-/fecha-2.3.3.tgz", - "integrity": "sha512-lUGBnIamTAwk4znq5BcqsDaxSmZ9nDVJaij6NvRt/Tg4R69gERA+otPKbS86ROw9nxVMw2/mp1fnaiWqbs6Sdg==" - }, - "figgy-pudding": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", - "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==", - "dev": true - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-loader": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-2.0.0.tgz", - "integrity": "sha512-YCsBfd1ZGCyonOKLxPiKPdu+8ld9HAaMEvJewzz+b2eTF7uL5Zm/HdBF6FjCrpCMRq25Mi0U1gl4pwn2TlH7hQ==", - "dev": true, - "requires": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" - } - }, - "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 - }, - "filemanager-webpack-plugin": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/filemanager-webpack-plugin/-/filemanager-webpack-plugin-2.0.5.tgz", - "integrity": "sha512-Yj5XIdKI2AN2r66uZc4MZ/n18SMqe2KKlkAqHHMW1OwveDs2Vc5129CpbFcI73rq/rjqso+2HsxieS7u5sx6XA==", - "dev": true, - "requires": { - "archiver": "^3.0.0", - "cpx": "^1.5.0", - "fs-extra": "^7.0.0", - "make-dir": "^1.1.0", - "mv": "^2.1.1", - "rimraf": "^2.6.2" - }, - "dependencies": { - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - } - } - }, - "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 - }, - "filename-reserved-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true - }, - "filenamify": { - "version": "2.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" - } - }, - "filesize": { - "version": "3.5.11", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.5.11.tgz", - "integrity": "sha512-ZH7loueKBoDb7yG9esn1U+fgq7BzlzW6NRi5/rMdxIZ05dj7GFD/Xc5rq2CDt5Yq86CyfSYVyx4242QQNZbx1g==", - "dev": true - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "finalhandler": { - "version": "1.1.1", - "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", - "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", - "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" - } - }, - "find": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/find/-/find-0.2.9.tgz", - "integrity": "sha1-S3Px/55WrZG3bnFkB/5f/mVUu4w=", - "dev": true, - "requires": { - "traverse-chain": "~0.1.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=", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^1.0.0", - "pkg-dir": "^2.0.0" - } - }, - "find-index": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/find-index/-/find-index-0.1.1.tgz", - "integrity": "sha1-Z101iyyjiS15Whq0cjL4tuLg3eQ=", - "dev": true - }, - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", - "dev": true - }, - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "fined": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz", - "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - } - }, - "flagged-respawn": { - "version": "1.0.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==", - "dev": true, - "requires": { - "is-buffer": "~1.1.5" - } - }, - "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==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.4" - } - }, - "fontkit": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.8.0.tgz", - "integrity": "sha512-EFDRCca7khfQWYu1iFhsqeABpi87f03MBdkT93ZE6YhqCdMzb5Eojb6c4dlJikGv5liuhByyzA7ikpIPTSBWbQ==", - "requires": { - "babel-runtime": "^6.11.6", - "brfs": "^1.4.0", - "brotli": "^1.2.0", - "browserify-optional": "^1.0.0", - "clone": "^1.0.1", - "deep-equal": "^1.0.0", - "dfa": "^1.0.0", - "restructure": "^0.5.3", - "tiny-inflate": "^1.0.2", - "unicode-properties": "^1.0.0", - "unicode-trie": "^0.3.0" - }, - "dependencies": { - "brfs": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", - "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", - "requires": { - "quote-stream": "^1.0.1", - "resolve": "^1.1.5", - "static-module": "^2.2.0", - "through2": "^2.0.0" - } + "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 + }, + "minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "resolve": { + "version": "2.0.0-next.3", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", + "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } }, - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" - }, - "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + "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" + } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "merge-source-map": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", - "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", - "requires": { - "source-map": "^0.5.6" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } + "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" + } }, - "object-inspect": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", - "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==" + "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 + } + } }, - "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==" + "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 + } + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "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==", - "optional": true - }, - "static-module": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/static-module/-/static-module-2.2.5.tgz", - "integrity": "sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==", - "requires": { - "concat-stream": "~1.6.0", - "convert-source-map": "^1.5.1", - "duplexer2": "~0.1.4", - "escodegen": "~1.9.0", - "falafel": "^2.1.0", - "has": "^1.0.1", - "magic-string": "^0.22.4", - "merge-source-map": "1.0.4", - "object-inspect": "~1.4.0", - "quote-stream": "~1.0.2", - "readable-stream": "~2.3.3", - "shallow-copy": "~0.0.1", - "static-eval": "^2.0.0", - "through2": "~2.0.3" - } + "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" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", - "dev": true - }, - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - }, - "foreach": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", - "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=" - }, - "foreground-child": { - "version": "1.5.6", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-1.5.6.tgz", - "integrity": "sha1-T9ca0t/elnibmApcCilZN8svXOk=", - "dev": true, - "requires": { - "cross-spawn": "^4", - "signal-exit": "^3.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-4.0.2.tgz", - "integrity": "sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "which": "^1.2.9" - } - } - } - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "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" - } - }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", - "dev": true - }, - "from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha1-g8YK/Fi5xWmXAH7Rp2izqzA6RP4=", - "dev": true - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "fs-extra": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-4.0.3.tgz", - "integrity": "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==", - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-minipass": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.6.tgz", - "integrity": "sha512-crhvyXcMejjv3Z5d2Fa9sf5xLYVCF5O1c71QxbVnbLsmYMBEvDAftewesN/HhY03YRoA7zOMxjNGrF5svGaaeQ==", - "dev": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs-mkdirp-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", - "integrity": "sha1-C3gV/DIBxqaeFNuYzgmMFpNSWes=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "through2": "^2.0.3" - } - }, - "fs-readdir-recursive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", - "integrity": "sha512-GNanXlVr2pf02+sPN40XN8HG+ePaNcvM0q5mZBd668Obwb0yD5GiUbZOFgwn8kGMY6I3mdyDJzieUy3PTYyTRA==", - "dev": true - }, - "fs-walk": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/fs-walk/-/fs-walk-0.0.1.tgz", - "integrity": "sha1-9/yRw64e6tB8mYvF0N1B8tvr0zU=", - "dev": true, - "requires": { - "async": "*" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", - "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", - "dev": true, - "optional": true, - "requires": { - "nan": "^2.9.2", - "node-pre-gyp": "^0.10.0" - }, - "dependencies": { - "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, - "optional": true + "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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "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 + }, + "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": { + "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 + } + } }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", - "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", - "dev": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } + "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" + } }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "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" + } }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true + "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" + } }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "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" + } + } + } }, - "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, - "optional": true + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "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": { + "is-glob": "^4.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, - "optional": true, - "requires": { - "ms": "2.0.0" - } + "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.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "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" + } }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", - "dev": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true, - "optional": true + "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" + } }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true + "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" + } }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "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" - } + "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" + } }, - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "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", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true, - "optional": true + "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" + } }, - "iconv-lite": { - "version": "0.4.21", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", - "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", - "dev": true, - "optional": true, - "requires": { - "safer-buffer": "^2.1.0" - } - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } + "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 }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } + "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" + } }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true, - "optional": true + "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" + } }, - "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" - } + "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 + } + } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true, - "optional": 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 }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } + "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" + } }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true + "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" + } }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - } + "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" + } }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true, - "optional": true - }, - "needle": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", - "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", - "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", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", - "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", - "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", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", - "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", - "dev": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", - "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", - "dev": true, - "optional": true - }, - "npm-packlist": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", - "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", - "dev": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "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", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true + "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 }, - "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, - "optional": 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" + } }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true, - "optional": true - }, - "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=", - "dev": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } + "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": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + } + } }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, - "optional": 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" + } }, - "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, - "optional": true + "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" + } }, - "rc": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", - "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", - "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", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true, - "optional": true - } - } + "fromentries": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.2.0.tgz", + "integrity": "sha512-33X7H/wdfO99GdRLLgkjUrD4geAFdq/Uv0kl3HD4da6HDixd2GUg8Mw7dahLCV9r/EARkmtYBB6Tch4EEokFTQ==", + "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, - "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" - } + "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 }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "optional": true, - "requires": { - "glob": "^7.0.5" - } + "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" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } }, - "safe-buffer": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true + "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" + } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, - "optional": true + "fs-walk": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/fs-walk/-/fs-walk-0.0.1.tgz", + "integrity": "sha1-9/yRw64e6tB8mYvF0N1B8tvr0zU=", + "dev": true, + "requires": { + "async": "*" + } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true, - "optional": true + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", - "dev": true, - "optional": true + "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" + } }, - "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, - "optional": true + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true, - "optional": 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 }, - "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" - } + "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" + } }, - "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, - "optional": true, - "requires": { - "safe-buffer": "~5.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" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } + "get-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": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true, - "optional": true - }, - "tar": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", - "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", - "dev": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.5", - "minizlib": "^1.2.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.3" - }, - "dependencies": { - "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, - "optional": 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" + } }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true, - "optional": true - }, - "wide-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", - "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", - "dev": true, - "optional": true, - "requires": { - "string-width": "^1.0.2" - } + "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 }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": 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": { + "brace-expansion": "^1.1.7" + } + } + } }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true, - "optional": true - } - } - }, - "fstream": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", - "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "inherits": "~2.0.0", - "mkdirp": ">=0.5 0", - "rimraf": "2" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.0.tgz", - "integrity": "sha512-Bs0VRrTz4ghD8pTmbJQD1mZ8A/mN0ur/jGz+A6FBxPDUPkm1tNfF6bhTYPA7i7aF4lZJVr+OXTNNrnnIl58Wfg==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "is-callable": "^1.1.3" - } - }, - "fuzzy": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/fuzzy/-/fuzzy-0.1.3.tgz", - "integrity": "sha1-THbsL/CsGjap3M+aAN+GIweNTtg=" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - }, - "dependencies": { - "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" - } + "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": { + "is-extglob": "^2.1.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" - } + "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" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "get-assigned-identifiers": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", - "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==", - "dev": true - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", - "dev": true - }, - "get-port": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", - "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" - }, - "get-proxy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", - "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", - "dev": true, - "requires": { - "npm-conf": "^1.1.0" - } - }, - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.1.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" - } - }, - "glob-base": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-base/-/glob-base-0.3.0.tgz", - "integrity": "sha1-27Fk9iIbHAscz4Kuoyi0l98Oo8Q=", - "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" - } + "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" + } }, - "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 + "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" + } }, - "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" - } - } - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "glob-stream": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", - "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", - "dev": true, - "requires": { - "extend": "^3.0.0", - "glob": "^7.1.1", - "glob-parent": "^3.1.0", - "is-negated-glob": "^1.0.0", - "ordered-read-streams": "^1.0.0", - "pumpify": "^1.3.5", - "readable-stream": "^2.1.5", - "remove-trailing-separator": "^1.0.1", - "to-absolute-glob": "^2.0.0", - "unique-stream": "^2.0.2" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "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": { + "isexe": "^2.0.0" + } + } + } }, - "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" - } + "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" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "glob-watcher": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.1.tgz", - "integrity": "sha512-fK92r2COMC199WCyGUblrZKhjra3cyVMDiypDdqg1vsSDmexnbYivK1kNR4QItiNXLKmGlqan469ks67RtNa2g==", - "dev": true, - "requires": { - "async-done": "^1.2.0", - "chokidar": "^2.0.0", - "just-debounce": "^1.0.0", - "object.defaults": "^1.1.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" - } + "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" + } }, - "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" - } + "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" + } }, - "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" - } - } - } - }, - "glob2base": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/glob2base/-/glob2base-0.0.12.tgz", - "integrity": "sha1-nUGbPijxLoOjYhZKJ3BVkiycDVY=", - "dev": true, - "requires": { - "find-index": "^0.1.1" - } - }, - "global": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/global/-/global-4.3.2.tgz", - "integrity": "sha1-52mJJopsdMOJCLEwWxD8DjlOnQ8=", - "dev": true, - "requires": { - "min-document": "^2.19.0", - "process": "~0.5.1" - }, - "dependencies": { - "process": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/process/-/process-0.5.2.tgz", - "integrity": "sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=", - "dev": true - } - } - }, - "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-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 - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "globals": { - "version": "11.7.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz", - "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "glogg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/glogg/-/glogg-1.0.1.tgz", - "integrity": "sha512-ynYqXLoluBKf9XGR1gA59yEJisIL7YHEH4xr3ZziHB5/yl4qWfaK8Js9jGe6gBGCSCKVqiyO30WnRZADvemUNw==", - "dev": true, - "requires": { - "sparkles": "^1.0.0" - } - }, - "got": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.1.tgz", - "integrity": "sha512-tiLX+bnYm5A56T5N/n9Xo89vMaO1mrS9qoDqj3u/anVooqGozvY/HbXzEpDfbNeKsHCBpK40gSbz8wGYSp3i1w==", - "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" - } - }, - "graceful-fs": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", - "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, - "growl": { - "version": "1.10.5", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", - "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", - "dev": true - }, - "gulp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.0.tgz", - "integrity": "sha1-lXZsYB2t5Kd+0+eyttwDiBtZY2Y=", - "dev": true, - "requires": { - "glob-watcher": "^5.0.0", - "gulp-cli": "^2.0.0", - "undertaker": "^1.0.0", - "vinyl-fs": "^3.0.0" - }, - "dependencies": { - "camelcase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", - "integrity": "sha1-MvxLn82vhF/N9+c7uXysImHwqwo=", - "dev": true + "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", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true + } + } }, - "cliui": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", - "integrity": "sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } + "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 + }, + "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": { + "graceful-fs": "^4.2.8", + "streamx": "^2.12.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "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": { + "@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" + } + }, + "lead": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-4.0.0.tgz", + "integrity": "sha512-DpMa59o5uGUWWjruMp71e6knmwKU3jRBBn1kjuLWN9EeIOxNeSAwvHf03WIl8g/ZMR2oSQC9ej3yeLBwdDc/pg==", + "dev": true + }, + "now-and-later": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-3.0.0.tgz", + "integrity": "sha512-pGO4pzSdaxhWTGkfSfHx3hVzJVslFPwBp2Myq9MYN/ChfJZF87ochMAXnvz6/58RJSf5ik2q9tXprBBrk2cpcg==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "resolve-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-2.0.0.tgz", + "integrity": "sha512-/FopbmmFOQCfsCx77BRFdKOniglTiHumLgwvd6IDPihy1GKkadZbgQJBcTb2lMzSR1pndzd96b1nZrreZ7+9/A==", + "dev": true, + "requires": { + "value-or-function": "^4.0.0" + } + }, + "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": { + "streamx": "^2.12.5" + } + }, + "value-or-function": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/value-or-function/-/value-or-function-4.0.0.tgz", + "integrity": "sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==", + "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + }, + "vinyl-fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-4.0.0.tgz", + "integrity": "sha512-7GbgBnYfaquMk3Qu9g22x000vbYkOex32930rBnc3qByw6HfMEAoELjCjoJv4HuEQxHAurT+nvMHm6MnJllFLw==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^2.0.1", + "glob-stream": "^8.0.0", + "graceful-fs": "^4.2.11", + "iconv-lite": "^0.6.3", + "is-valid-glob": "^1.0.0", + "lead": "^4.0.0", + "normalize-path": "3.0.0", + "resolve-options": "^2.0.0", + "stream-composer": "^1.0.2", + "streamx": "^2.14.0", + "to-through": "^3.0.0", + "value-or-function": "^4.0.0", + "vinyl": "^3.0.0", + "vinyl-sourcemap": "^2.0.0" + } + }, + "vinyl-sourcemap": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-2.0.0.tgz", + "integrity": "sha512-BAEvWxbBUXvlNoFQVFVHpybBbjW1r03WhohJzJDSfgrrK5xVYIDTan6xN14DlyImShgDRv2gl9qhM6irVMsV0Q==", + "dev": true, + "requires": { + "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" + } + } + } }, "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 + "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, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "requires": { + "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" + } + } + } }, - "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" - } + "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 + }, + "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 + }, + "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" + } + } + } }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "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 + "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" + } }, - "y18n": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz", - "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", - "dev": true + "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" + } }, - "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" - } + "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" + } }, - "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" - } - } - } - }, - "gulp-azure-storage": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/gulp-azure-storage/-/gulp-azure-storage-0.9.0.tgz", - "integrity": "sha512-b6CfmmEtyLDT3afv7wvNin3FJh3eAJy9N26HIXQTbQFP3LzP8XLJrvDe+G9lxFzJt943xgoYFNWUZz3wiJTZdQ==", - "dev": true, - "requires": { - "azure-storage": "^2.10.2", - "delayed-stream": "0.0.6", - "event-stream": "3.3.4", - "mime": "^1.3.4", - "optimist": "^0.6.1", - "progress": "^1.1.8", - "queue": "^3.0.10", - "streamifier": "^0.1.1", - "vinyl": "^0.4.5", - "vinyl-fs": "^3.0.3" - }, - "dependencies": { - "azure-storage": { - "version": "2.10.2", - "resolved": "https://registry.npmjs.org/azure-storage/-/azure-storage-2.10.2.tgz", - "integrity": "sha512-pOyGPya9+NDpAfm5YcFfklo57HfjDbYLXxs4lomPwvRxmb0Di/A+a+RkUmEFzaQ8S13CqxK40bRRB0sjj2ZQxA==", - "dev": true, - "requires": { - "browserify-mime": "~1.2.9", - "extend": "^3.0.2", - "json-edm-parser": "0.1.2", - "md5.js": "1.3.4", - "readable-stream": "~2.0.0", - "request": "^2.86.0", - "underscore": "~1.8.3", - "uuid": "^3.0.0", - "validator": "~9.4.1", - "xml2js": "0.2.8", - "xmlbuilder": "^9.0.7" - } + "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 }, - "clone": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/clone/-/clone-0.2.0.tgz", - "integrity": "sha1-xhJqkK1Pctv1rNskPMN3JP6T/B8=", - "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" + } }, - "delayed-stream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-0.0.6.tgz", - "integrity": "sha1-omRst+w9XXd0YUZwp6Zd4MFz7bw=", - "dev": true + "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" + } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "requires": { + "has-symbols": "^1.0.3" + } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true + "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" + } }, - "sax": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/sax/-/sax-0.5.8.tgz", - "integrity": "sha1-1HLbIo6zMcJQaw6MFVJK25OdEsE=", - "dev": true + "hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } }, - "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" - } + "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 + } + } }, - "xml2js": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.2.8.tgz", - "integrity": "sha1-m4FpCTFjH/CdGVdUn69U9PmAs8I=", - "dev": true, - "requires": { - "sax": "0.5.x" - } - } - } - }, - "gulp-chmod": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/gulp-chmod/-/gulp-chmod-2.0.0.tgz", - "integrity": "sha1-AMOQuSigeZslGsz2MaoJ4BzGKZw=", - "dev": true, - "requires": { - "deep-assign": "^1.0.0", - "stat-mode": "^0.2.0", - "through2": "^2.0.0" - } - }, - "gulp-filter": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gulp-filter/-/gulp-filter-5.1.0.tgz", - "integrity": "sha1-oF4Rr/sHz33PQafeHLe2OsN4PnM=", - "dev": true, - "requires": { - "multimatch": "^2.0.0", - "plugin-error": "^0.1.2", - "streamfilter": "^1.0.5" - } - }, - "gulp-gunzip": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/gulp-gunzip/-/gulp-gunzip-1.1.0.tgz", - "integrity": "sha512-3INeprGyz5fUtAs75k6wVslGuRZIjKAoQp39xA7Bz350ReqkrfYaLYqjZ67XyIfLytRXdzeX04f+DnBduYhQWw==", - "dev": true, - "requires": { - "through2": "~2.0.3", - "vinyl": "~2.0.1" - } - }, - "gulp-rename": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gulp-rename/-/gulp-rename-1.4.0.tgz", - "integrity": "sha512-swzbIGb/arEoFK89tPY58vg3Ok1bw+d35PfUNwWqdo7KM4jkmuGA78JiDNqR+JeZFaeeHnRg9N7aihX3YPmsyg==", - "dev": true - }, - "gulp-sourcemaps": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/gulp-sourcemaps/-/gulp-sourcemaps-2.6.4.tgz", - "integrity": "sha1-y7IAhFCxvM5s0jv5gze+dRv24wo=", - "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": { - "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 - } - } - }, - "gulp-typescript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-4.0.2.tgz", - "integrity": "sha512-Hhbn5Aa2l3T+tnn0KqsG6RRJmcYEsr3byTL2nBpNBeAK8pqug9Od4AwddU4JEI+hRw7mzZyjRbB8DDWR6paGVA==", - "dev": true, - "requires": { - "ansi-colors": "^1.0.1", - "plugin-error": "^0.1.2", - "source-map": "^0.6.1", - "through2": "^2.0.3", - "vinyl": "^2.1.0", - "vinyl-fs": "^3.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", - "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" + } }, - "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 + "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" + } }, - "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" - } + "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" + } }, - "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" - } + "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": { + "ms": "2.1.2" + } + } + } }, - "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 + "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": { + "ms": "2.1.2" + } + } + } }, - "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" - } + "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 }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } }, - "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 + "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": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } }, - "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" - } + "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" + } }, - "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" - } + "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" + } }, - "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" - } + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - } - } - } - }, - "gulp-untar": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/gulp-untar/-/gulp-untar-0.0.8.tgz", - "integrity": "sha512-mqW7v2uvrxd8IoCCwJ04sPYgWjR3Gsi6yfhVWBK3sFMDP7FuoT7GNmxrCMwkk4RWqQohx8DRv+cDq4SRDXATGA==", - "dev": true, - "requires": { - "event-stream": "3.3.4", - "streamifier": "~0.1.1", - "tar": "^2.2.1", - "through2": "~2.0.3", - "vinyl": "^1.2.0" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "dev": true + "indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true }, - "replace-ext": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-0.0.1.tgz", - "integrity": "sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=", - "dev": true - }, - "tar": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", - "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", - "dev": true, - "requires": { - "block-stream": "*", - "fstream": "^1.0.12", - "inherits": "2" - } + "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" + } }, - "vinyl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-1.2.0.tgz", - "integrity": "sha1-XIgDbPVl5d8FVYv8kR+GVt8hiIQ=", - "dev": true, - "requires": { - "clone": "^1.0.0", - "clone-stats": "^0.0.1", - "replace-ext": "0.0.1" - } - } - } - }, - "gulp-vinyl-zip": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/gulp-vinyl-zip/-/gulp-vinyl-zip-2.1.2.tgz", - "integrity": "sha512-wJn09jsb8PyvUeyFF7y7ImEJqJwYy40BqL9GKfJs6UGpaGW9A+N68Q+ajsIpb9AeR6lAdjMbIdDPclIGo1/b7Q==", - "dev": true, - "requires": { - "event-stream": "3.3.4", - "queue": "^4.2.1", - "through2": "^2.0.3", - "vinyl": "^2.0.2", - "vinyl-fs": "^3.0.3", - "yauzl": "^2.2.1", - "yazl": "^2.2.1" - }, - "dependencies": { - "queue": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-4.5.1.tgz", - "integrity": "sha512-AMD7w5hRXcFSb8s9u38acBZ+309u6GsiibP4/0YacJeaurRshogB7v/ZcVPxP5gD5+zIw6ixRHdutiYUJfwKHw==", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - } - } - }, - "gulplog": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/gulplog/-/gulplog-1.0.0.tgz", - "integrity": "sha1-4oxNRdBey77YGDY86PnFkmIp/+U=", - "dev": true, - "requires": { - "glogg": "^1.0.0" - } - }, - "gzip-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", - "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", - "dev": true, - "requires": { - "duplexer": "^0.1.1" - } - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "dev": true, - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", - "dev": true + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "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 - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.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" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "requires": { - "ansi-regex": "^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 - }, - "has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=", - "dev": true - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", - "dev": true, - "requires": { - "has-symbol-support-x": "^1.4.1" - } - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hash-base": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz", - "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "hasha": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hasha/-/hasha-3.0.0.tgz", - "integrity": "sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=", - "dev": true, - "requires": { - "is-stream": "^1.0.1" - } - }, - "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": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hoist-non-react-statics": { - "version": "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=", - "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } - }, - "hoopy": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz", - "integrity": "sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ==", - "dev": true - }, - "hosted-git-info": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.6.0.tgz", - "integrity": "sha512-lIbgIIQA3lz5XaB6vxakj6sDHADJiZadYEJB+FgA+C4nubM1NwcuvUr9EJPmnH1skZqpqUzWborWo8EIUi0Sdw==", - "dev": true - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "html-minifier": { - "version": "3.5.20", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.20.tgz", - "integrity": "sha512-ZmgNLaTp54+HFKkONyLFEfs5dd/ZOtlquKaTnqIWFmx3Av5zG6ZPcV2d0o9XM2fXOTxxIf6eDcwzFFotke/5zA==", - "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" - }, - "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 + "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" + } }, - "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" - } - } - } - }, - "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=", - "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" - } - } - } - }, - "htmlparser2": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.9.2.tgz", - "integrity": "sha1-G9+HrMoPP55T+k/M6w9LTLsAszg=", - "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" - } - }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "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 - }, - "http-proxy-agent": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", - "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", - "dev": true, - "requires": { - "agent-base": "4", - "debug": "3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - } - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "https-proxy-agent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", - "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", - "dev": true, - "requires": { - "agent-base": "^4.1.0", - "debug": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "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" + } }, - "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 - } - } - }, - "husky": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/husky/-/husky-1.1.2.tgz", - "integrity": "sha512-9TdkUpBeEOjz0AnFdUN4i3w8kEbOsVs9/WSeJqWLq2OO6bcKQhVW64Zi+pVd/AMRLpN3QTINb6ZXiELczvdmqQ==", - "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": { - "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" - } + "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" + } }, - "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" - } + "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" + } }, - "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 + "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" + } }, - "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" - } + "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" + } }, - "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-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" + } }, - "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" - } + "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 + }, + "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" + } }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - } - } - }, - "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" - } - }, - "icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha1-Bupvg2ead0njhs/h/oEq5dsiPe0=", - "dev": true - }, - "icss-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-2.1.0.tgz", - "integrity": "sha1-g/Cg7DeL8yRheLbCrZE28TWxyWI=", - "dev": true, - "requires": { - "postcss": "^6.0.1" - } - }, - "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 - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "ignore-walk": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", - "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", - "dev": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", - "dev": true, - "optional": true - }, - "immutable": { - "version": "4.0.0-rc.12", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0-rc.12.tgz", - "integrity": "sha512-0M2XxkZLx/mi3t8NVwIm1g8nHoEmM9p9UBl/G9k4+hm0kBgOVdMV/B3CY5dQ8qG8qc80NN4gDV4HQv6FTJ5q7A==", - "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==", - "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" - } + "is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "requires": { + "is-typed-array": "^1.1.13" + } }, - "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" - } + "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" + } }, - "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-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "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" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true - }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", - "dev": true, - "requires": { - "repeating": "^2.0.0" - } - }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true - }, - "inquirer": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz", - "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==", - "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.0.4", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rx-lite": "^4.0.8", - "rx-lite-aggregates": "^4.0.8", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "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" - } + "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" + } }, - "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" - } + "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" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "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" + } }, - "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" - } + "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 }, - "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" - } - } - } - }, - "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=", - "dev": true, - "requires": { - "from2": "^2.1.1", - "p-is-promise": "^1.1.0" - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dev": true, - "requires": { - "loose-envify": "^1.0.0" - } - }, - "inversify": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/inversify/-/inversify-4.11.1.tgz", - "integrity": "sha512-9bs/36crPdTSOCcoomHMb96s+B8W0+2c9dHFP/Srv9ZQaPnUvsMgzmMHfgVECqfHVUIW+M5S7SYOjoig8khWuQ==" - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz", - "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", - "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-alphabetical": { - "version": "1.0.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==", - "dev": true, - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.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=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-boolean-object": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.0.0.tgz", - "integrity": "sha1-mPiygDBoQhmpXzdc+9iM40Bd/5M=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-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=", - "dev": true, - "requires": { - "builtin-modules": "^1.0.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 - }, - "is-ci": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz", - "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==", - "dev": true, - "requires": { - "ci-info": "^1.5.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", - "dev": true - }, - "is-decimal": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.2.tgz", - "integrity": "sha512-TRzl7mOCchnhchN+f3ICUCzYvL9ul7R+TYOsZ8xia++knyZAJfv/uA1FvQXsAnYIl1T3B2X5E/J7Wb1QXiIBXg==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-dotfile": { - "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=", - "dev": true, - "requires": { - "is-primitive": "^2.0.0" - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true - }, - "is-finite": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", - "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", - "dev": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", - "dev": true - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - }, - "is-hexadecimal": { - "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=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-number-object": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.3.tgz", - "integrity": "sha1-8mWrian0RQNO9q/xWo8AsA9VF5k=", - "dev": true - }, - "is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", - "dev": true - }, - "is-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", - "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true - }, - "is-odd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-odd/-/is-odd-2.0.0.tgz", - "integrity": "sha512-OTiixgpZAT1M4NHgS5IguFp/Vz2VI3U7Goh4/HA1adtwyLtSBrxYlcSYkhpAE07s4fKEcjrFxyvtQBND4vFQyQ==", - "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 - } - } - }, - "is-path-cwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", - "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", - "dev": true - }, - "is-path-in-cwd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz", - "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==", - "dev": true, - "requires": { - "is-path-inside": "^1.0.0" - } - }, - "is-path-inside": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz", - "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=", - "dev": true, - "requires": { - "path-is-inside": "^1.0.1" - } - }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-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": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-2.0.0.tgz", - "integrity": "sha1-IHurkWOEmcB7Kt8kCkGochADRXU=", - "dev": true - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", - "dev": true - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "dev": true, - "requires": { - "has": "^1.0.1" - } - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "dev": true, - "requires": { - "is-unc-path": "^1.0.0" - } - }, - "is-retry-allowed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz", - "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ=", - "dev": true - }, - "is-root": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-root/-/is-root-1.0.0.tgz", - "integrity": "sha1-B7bCM7w5TNnQK6FclmvWZg1jQtU=", - "dev": true - }, - "is-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 - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-string": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.4.tgz", - "integrity": "sha1-zDqbaYV9Yh6WNyWiTK7shzuCbmQ=", - "dev": true - }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "dev": true, - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", - "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" - } - }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true - }, - "is-valid-glob": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-valid-glob/-/is-valid-glob-1.0.0.tgz", - "integrity": "sha1-Kb8+/3Ab4tTTFdusw5vDn+j2Aao=", - "dev": true - }, - "is-whitespace-character": { - "version": "1.0.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 - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-word-character": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.2.tgz", - "integrity": "sha512-T3FlsX8rCHAH8e7RE7PfOPZVFQlcV3XRF9eOOBQ1uf70OxO7CjjSOjeImMPCADBdYWcStAbVbYvJ1m2D3tb+EA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true - }, - "isomorphic-fetch": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz", - "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=", - "dev": true, - "requires": { - "node-fetch": "^1.0.1", - "whatwg-fetch": ">=0.10.0" - }, - "dependencies": { - "node-fetch": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz", - "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==", - "dev": true, - "requires": { - "encoding": "^0.1.11", - "is-stream": "^1.0.1" - } - } - } - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==", - "dev": true - }, - "istanbul-lib-hook": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/istanbul-lib-hook/-/istanbul-lib-hook-2.0.7.tgz", - "integrity": "sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==", - "dev": true, - "requires": { - "append-transform": "^1.0.0" - } - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "dev": true, - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - }, - "dependencies": { - "@babel/generator": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.4.4.tgz", - "integrity": "sha512-53UOLK6TVNqKxf7RUh8NE851EHRxOOeVXKbK2bivdb+iziMyk03Sr4eaE9OELCbyZAAafAKPDwF2TPUES5QbxQ==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4", - "jsesc": "^2.5.1", - "lodash": "^4.17.11", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } + "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" + } }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "dev": true, - "requires": { - "@babel/types": "^7.4.4" - } + "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.1" + } }, - "@babel/parser": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.4.5.tgz", - "integrity": "sha512-9mUqkL1FF5T7f0WDFfAoDdiMVPWsdD1gZYzSnaXsxUCUqzuch/8of9G3VUSNiZmMBoRxT3neyVsqeiL/ZPcjew==", - "dev": true + "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": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "requires": { + "is-unc-path": "^1.0.0" + } }, - "@babel/traverse": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.4.5.tgz", - "integrity": "sha512-Vc+qjynwkjRmIFGxy0KYoPj4FdVDxLej89kMHFsWScq999uX+pwcX4v9mWRjW0KcAYTPAuVQl2LKP1wEVLsp+A==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.4.4", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.4.5", - "@babel/types": "^7.4.4", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.11" - } + "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": { + "call-bind": "^1.0.7" + } }, - "@babel/types": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.4.4.tgz", - "integrity": "sha512-dOllgYdnEFOebhkKCjzSVFqw/PmmB8pH6RGOWkY4GsboQNd47b1fBThBSwlHAq9alF9vc1M3+6oqR47R50L0tQ==", - "dev": true, - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.11", - "to-fast-properties": "^2.0.0" - } + "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": { + "has-tostringtag": "^1.0.0" + } }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "is-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": { + "has-symbols": "^1.0.2" + } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "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": { + "which-typed-array": "^1.1.16" + } }, - "semver": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.1.1.tgz", - "integrity": "sha512-rWYq2e5iYW+fFe/oPPtYJxYgjBm8sC4rmoGdUOgBB7VnwKt6HrL793l2voH1UlsyYZpJ4g0wfjnTEO1s1NP2eQ==", - "dev": true - } - } - }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", - "dev": true, - "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": 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==", + "dev": true, + "requires": { + "unc-path-regex": "^0.1.2" + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "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": { + "call-bind": "^1.0.2" + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true + }, + "is-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": { + "is-docker": "^2.0.0" + } }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "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" - } + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } + "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": { + "append-transform": "^2.0.0" + } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "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": { + "@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 + } + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "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": { + "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": { + "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 + } + } }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "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" + } + } + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.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" + } + } + } }, - "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 - } - } - }, - "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", - "dev": true, - "requires": { - "handlebars": "^4.1.2" - } - }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", - "dev": true, - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" - } - }, - "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" - }, - "jquery-ui": { - "version": "1.12.1", - "resolved": "https://registry.npmjs.org/jquery-ui/-/jquery-ui-1.12.1.tgz", - "integrity": "sha1-vLQEXI3QU5wTS8FIjN0+dop6nlE=" - }, - "js-base64": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.5.1.tgz", - "integrity": "sha512-M7kLczedRMYX4L8Mdh4MzyAMM9O5osx+4FcOQuTvr3A9F2D9S5JXheN0ewNbrvK2UatkTRhL5ejGmGSjNMiZuw==", - "dev": true - }, - "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 - }, - "js-tokens": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", - "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "dependencies": { - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - } - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "optional": true - }, - "jsdom": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.0.0.tgz", - "integrity": "sha512-rJnHm7CHyIj4tDyz9VaCt0f0P0nEh/wEmMfwp9mMixy+L/r8OW/BNcgmIlfZuBBnVQS3eRBpvd/qM3R7vr7e3A==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^6.0.4", - "acorn-globals": "^4.3.0", - "array-equal": "^1.0.0", - "cssom": "^0.3.4", - "cssstyle": "^1.1.1", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.0", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.1.3", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.5", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.5.0", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^6.1.2", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true + "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": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } }, - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "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" - } - }, - "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.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "dev": true, - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=", - "dev": true + "isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "requires": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "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" + } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "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" + } + } + } }, - "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 - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.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": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + } + } + }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true + }, + "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 + "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 }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.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" - }, - "dependencies": { - "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" - } - } - } + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "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": { + "jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "dev": true, + "requires": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + } + } }, - "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.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "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 - } - } + "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": { + "array-includes": "^3.1.3", + "object.assign": "^4.1.2" + } }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - } - } - }, - "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" - } - }, - "json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-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" - } - }, - "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=" - }, - "json-stable-stringify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", - "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", - "requires": { - "jsonify": "~0.0.0" - } - }, - "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" - }, - "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" - } + "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": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "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 + }, + "jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "requires": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "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": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "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": { + "json-buffer": "3.0.0" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "language-subtag-registry": { + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.20.tgz", + "integrity": "sha512-KPMwROklF4tEx283Xw0pNKtfTj1gZ4UByp4EsIFWLgBavJltF4TiYPc39k06zSTsLzxTVXXDSpbwaQXaFB4Qeg==", + "dev": true + }, + "language-tags": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", + "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "dev": true, + "requires": { + "language-subtag-registry": "~0.3.2" + } + }, + "last-run": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/last-run/-/last-run-2.0.0.tgz", + "integrity": "sha512-j+y6WhTLN4Itnf9j5ZQos1BGPCS8DAwmgMroR3OzfxAsBxam0hMw7J8M3KqZl0pLQJ1jNnwIexg5DYpC/ctwEQ==", + "dev": true + }, + "lazystream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", + "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", + "dev": true, + "requires": { + "readable-stream": "^2.0.5" + } + }, + "lead": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", + "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", + "dev": true, + "requires": { + "flush-write-stream": "^1.0.2" + } + }, + "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 + }, + "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" + } + }, + "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": { + "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": { + "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 + } + } }, - "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 - } - } - }, - "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 - }, - "jsonc-parser": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.0.3.tgz", - "integrity": "sha512-WJi9y9ABL01C8CxTKxRRQkkSpY/x2bo4Gy0WuiZGrInxQqgxQpvkBCLNcDYcHOSdhx4ODgbFcgAvfL49C+PHgQ==" - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsonify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", - "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=" - }, - "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" - } - }, - "just-debounce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/just-debounce/-/just-debounce-1.0.0.tgz", - "integrity": "sha1-h/zPrv/AtozRnVX2cilD+SnqNeo=", - "dev": true - }, - "just-extend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.0.2.tgz", - "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", - "dev": true - }, - "keyv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==", - "dev": true - }, - "kuler": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/kuler/-/kuler-1.0.1.tgz", - "integrity": "sha512-J9nVUucG1p/skKul6DU3PUZrhs0LPulNaeUOox0IyXDi8S4CztTHs1gQphhuZmzXG7VOQSf6NJfKuzteQLv9gQ==", - "requires": { - "colornames": "^1.1.1" - } - }, - "labella": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/labella/-/labella-1.1.4.tgz", - "integrity": "sha1-xsxaNA6N80DrM1YzaD6lm4KMMi0=", - "dev": true - }, - "last-run": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/last-run/-/last-run-1.1.1.tgz", - "integrity": "sha1-RblpQsF7HHnHchmCWbqUO+v4yls=", - "dev": true, - "requires": { - "default-resolution": "^2.0.0", - "es6-weak-map": "^2.0.1" - } - }, - "lazystream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.0.tgz", - "integrity": "sha1-9plf4PggOS9hOWvolGJAe7dxaOQ=", - "dev": true, - "requires": { - "readable-stream": "^2.0.5" - } - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "dev": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, - "lead": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lead/-/lead-1.0.0.tgz", - "integrity": "sha1-bxT5mje+Op3XhPVJVpDlkDRm7kI=", - "dev": true, - "requires": { - "flush-write-stream": "^1.0.2" - } - }, - "leaflet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.3.4.tgz", - "integrity": "sha512-FYL1LGFdj6v+2Ifpw+AcFIuIOqjNggfoLUwuwQv6+3sS21Za7Wvapq+LhbSE4NDXrEj6eYnW3y7LsaBICpyXtw==", - "dev": true - }, - "less": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/less/-/less-3.9.0.tgz", - "integrity": "sha512-31CmtPEZraNUtuUREYjSqRkeETFdyEHSEPAGq4erDlUXtda7pzNmctdljdIagSb589d/qXGWiiP31R5JVf+v0w==", - "dev": true, - "requires": { - "clone": "^2.1.2", - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "mime": "^1.4.1", - "mkdirp": "^0.5.0", - "promise": "^7.1.1", - "request": "^2.83.0", - "source-map": "~0.6.0" - }, - "dependencies": { - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "optional": 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, - "optional": true, - "requires": { - "asap": "~2.0.3" - } + "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" + } }, - "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 - } - } - }, - "less-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-5.0.0.tgz", - "integrity": "sha512-bquCU89mO/yWLaUq0Clk7qCsKhsF/TZpJUzETRvJa9KSVEL9SO3ovCvdEHISBhrC81OwC8QSVX7E0bzElZj9cg==", - "dev": true, - "requires": { - "clone": "^2.1.1", - "loader-utils": "^1.1.0", - "pify": "^4.0.1" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - } - } - }, - "less-plugin-inline-urls": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/less-plugin-inline-urls/-/less-plugin-inline-urls-1.2.0.tgz", - "integrity": "sha1-XdqwegwlcfGihVz5Kd3J78Hjisw=" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "liftoff": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz", - "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=", - "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" - } - }, - "line-by-line": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/line-by-line/-/line-by-line-0.1.6.tgz", - "integrity": "sha512-MmwVPfOyp0lWnEZ3fBA8Ah4pMFvxO6WgWovqZNu7Y4J0TNnGcsV4S1LzECHbdgqk1hoHc2mFP1Axc37YUqwafg==" - }, - "linear-layout-vector": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz", - "integrity": "sha1-OYEU1zA7bsx/1rJzr3uEAdi6nHA=" - }, - "linebreak": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-0.3.0.tgz", - "integrity": "sha1-BSZICmLAW9Z58+nZmDDgnGp9DtY=", - "requires": { - "base64-js": "0.0.8", - "brfs": "^1.3.0", - "unicode-trie": "^0.3.0" - }, - "dependencies": { - "brfs": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", - "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", - "requires": { - "quote-stream": "^1.0.1", - "resolve": "^1.1.5", - "static-module": "^2.2.0", - "through2": "^2.0.0" - } - }, - "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + "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 }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "merge-source-map": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", - "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", - "requires": { - "source-map": "^0.5.6" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } + "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": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } }, - "object-inspect": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", - "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==" + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } }, - "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==" + "lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==" + }, + "lodash.flattendeep": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", + "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", + "dev": true + }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true + }, + "lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true + }, + "lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true + }, + "lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true + }, + "lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.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" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "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": { + "js-tokens": "^3.0.0 || ^4.0.0" + } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - }, - "static-module": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/static-module/-/static-module-2.2.5.tgz", - "integrity": "sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==", - "requires": { - "concat-stream": "~1.6.0", - "convert-source-map": "^1.5.1", - "duplexer2": "~0.1.4", - "escodegen": "~1.9.0", - "falafel": "^2.1.0", - "has": "^1.0.1", - "magic-string": "^0.22.4", - "merge-source-map": "1.0.4", - "object-inspect": "~1.4.0", - "quote-stream": "~1.0.2", - "readable-stream": "~2.3.3", - "shallow-copy": "~0.0.1", - "static-eval": "^2.0.0", - "through2": "~2.0.3" - } + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dev": true, + "requires": { + "get-func-name": "^2.0.0" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "linkify-it": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.1.0.tgz", - "integrity": "sha512-4REs8/062kV2DSHxNfq5183zrqXMl7WP0WzABH9IeJI+NLm429FgE1PDecltYfnOoFDFlZGh2T8PfZn0r+GTRg==", - "dev": true, - "requires": { - "uc.micro": "^1.0.1" - } - }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, - "dependencies": { - "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" - } - } - } - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", - "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - }, - "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" - } + "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 }, - "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" - } + "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": { + "yallist": "^4.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 - } - } - }, - "lodash": { - "version": "4.17.11", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", - "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=", - "dev": true - }, - "lodash.assign": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.assign/-/lodash.assign-4.2.0.tgz", - "integrity": "sha1-DZnzzNem0mHRm9rrkkUAXShYCOc=", - "dev": true - }, - "lodash.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 - }, - "lodash.curry": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.curry/-/lodash.curry-4.1.1.tgz", - "integrity": "sha1-JI42By7ekGUB11lmIAqG2riyMXA=", - "dev": true - }, - "lodash.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 - }, - "lodash.defaults": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", - "integrity": "sha1-0JF4cW/+pN3p5ft7N/bwgCJ0WAw=", - "dev": true - }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true - }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true - }, - "lodash.flow": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/lodash.flow/-/lodash.flow-3.5.0.tgz", - "integrity": "sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=", - "dev": true - }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true - }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true - }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true - }, - "lodash.set": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/lodash.set/-/lodash.set-4.3.2.tgz", - "integrity": "sha1-2HV7HagH3eJIFrDWqEvqGnYjCyM=", - "dev": true - }, - "lodash.some": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.some/-/lodash.some-4.6.0.tgz", - "integrity": "sha1-G7nzFO9ri63tE7VJFpsqlF62jk0=", - "dev": true - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", - "dev": true - }, - "lodash.tail": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.tail/-/lodash.tail-4.1.1.tgz", - "integrity": "sha1-0jM6NtnncXyK0vfKyv7HwytERmQ=", - "dev": true - }, - "lodash.toarray": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.toarray/-/lodash.toarray-4.4.0.tgz", - "integrity": "sha1-JMS/zWsvuji/0FlNsRedjptlZWE=", - "dev": true - }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha1-SLtQiECfFvGCFmZkHETdGqrjzYg=", - "dev": true - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", - "dev": true - }, - "log-driver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.7.tgz", - "integrity": "sha512-U7KCmLdqsGHBLeWqYlFA0V0Sl6P08EE1ZrmA9cxjUE0WVqT9qnyVDPz1kzpFEP0jdJuFnasWIfSd7fsaNXkpbg==", - "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==", - "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" - } + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "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 + } + } }, - "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" - } + "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 + }, + "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 + }, + "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 + } + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "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" + } }, - "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" - } - } - } - }, - "logform": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.1.2.tgz", - "integrity": "sha512-+lZh4OpERDBLqjiwDLpAWNQu6KMjnlXH2ByZwCuSqVPJletw0kTWJf5CgSNAUKn1KUkv3m2cUz/LK8zyEy7wzQ==", - "requires": { - "colors": "^1.2.1", - "fast-safe-stringify": "^2.0.4", - "fecha": "^2.3.3", - "ms": "^2.1.1", - "triple-beam": "^1.3.0" - }, - "dependencies": { - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" - } - } - }, - "loglevelnext": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/loglevelnext/-/loglevelnext-1.0.5.tgz", - "integrity": "sha512-V/73qkPuJmx4BcBF19xPBr+0ZRVBhc4POxvZTZdMeXpJ4NItXSJ/MSwuFT0kQJlCbXvdlZoQQ/418bS1y9Jh6A==", - "dev": true, - "requires": { - "es6-symbol": "^3.1.1", - "object.assign": "^4.1.0" - } - }, - "lolex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-4.1.0.tgz", - "integrity": "sha512-BYxIEXiVq5lGIXeVHnsFzqa1TxN5acnKnPCdlZSpzm8viNEOhiigupA4vTQ9HEFQ6nLTQ9wQOgBknJgzUYQ9Aw==", - "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" - } - }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", - "dev": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" - } - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha1-miyr0bno4K6ZOkv31YdcOcQujqw=", - "dev": true - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "lru-cache": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", - "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "lru-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", - "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=", - "dev": true, - "requires": { - "es5-ext": "~0.10.2" - } - }, - "magic-string": { - "version": "0.22.5", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz", - "integrity": "sha512-oreip9rJZkzvA8Qzk9HFs8fZGF/u7H/gtrE8EN6RjKJ9kh2HlC+yQ2QezifqTZfGyiuAV0dRv5a+y/8gBb1m9w==", - "requires": { - "vlq": "^0.2.2" - } - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "make-error": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz", - "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g==", - "dev": true - }, - "make-iterator": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "mamacro": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/mamacro/-/mamacro-0.0.3.tgz", - "integrity": "sha512-qMEwh+UujcQ+kbz3T6V+wAmO2U8veoq2w+3wY8MquqwVA3jChfwY+Tk52GZKDfACEPjuZ7r2oJLejwpt8jtwTA==", - "dev": true - }, - "map-age-cleaner": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.2.tgz", - "integrity": "sha512-UN1dNocxQq44IhJyMI4TU8phc2m9BddacHRPRjKGLYaF0jqd3xLz0jS0skpAU9WgYyoR4gHtUpzytNBS385FWQ==", - "dev": true, - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-stream": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz", - "integrity": "sha1-5WqpTEyAVaFkBKBnS3jyFffI4ZQ=", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-escapes": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.2.tgz", - "integrity": "sha512-lbRZ2mE3Q9RtLjxZBZ9+IMl68DKIXaVAhwvwn9pmjnPLS0h/6kyBMgNhqi1xFJ/2yv6cSyv0jbiZavZv93JkkA==", - "dev": true - }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "martinez-polygon-clipping": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/martinez-polygon-clipping/-/martinez-polygon-clipping-0.1.5.tgz", - "integrity": "sha1-gc4+soZ82RiKILkKzybyP7To7kI=", - "dev": true, - "requires": { - "bintrees": "^1.0.1", - "tinyqueue": "^1.1.0" - } - }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "requires": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - } - }, - "material-colors": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz", - "integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==", - "dev": true - }, - "math-random": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", - "integrity": "sha512-rUxjysqif/BZQH2yhd5Aaq7vXMSx9NdEsQcyA07uEzIvxgI7zIr33gGsh+RU0/XjmQpCW7RsVof1vlkvQVCK5A==", - "dev": true - }, - "md5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.2.1.tgz", - "integrity": "sha1-U6s41f48iJG6RlMp6iP6wFQBJvk=", - "requires": { - "charenc": "~0.0.1", - "crypt": "~0.0.1", - "is-buffer": "~1.1.1" - } - }, - "md5.js": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", - "integrity": "sha1-6b296UogpawYsENA/Fdk1bCdkB0=", - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "mdast-add-list-metadata": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdast-add-list-metadata/-/mdast-add-list-metadata-1.0.1.tgz", - "integrity": "sha512-fB/VP4MJ0LaRsog7hGPxgOrSL3gE/2uEdZyDuSEnKCv/8IkYHiDkIQSbChiJoHyxZZXZ9bzckyRk+vNxFzh8rA==", - "dev": true, - "requires": { - "unist-util-visit-parents": "1.1.2" - } - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true - }, - "media-typer": { - "version": "0.3.0", - "resolved": "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==", - "dev": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^1.0.0", - "p-is-promise": "^1.1.0" - } - }, - "memoize-one": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-4.0.0.tgz", - "integrity": "sha512-wdpOJ4XBejprGn/xhd1i2XR8Dv1A25FJeIvR7syQhQlz9eXsv+06llcvcmBxlWVGv4C73QBsWA8kxvZozzNwiQ==", - "dev": true - }, - "memoizee": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.12.tgz", - "integrity": "sha512-sprBu6nwxBWBvBOh5v2jcsGqiGLlL2xr2dLub3vR8dnE8YB17omwtm/0NSHl8jjNbcsJd5GMWJAnTSVe/O0Wfg==", - "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" - } - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", - "dev": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true - }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "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 - } - } - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - } - }, - "mime": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.2.tgz", - "integrity": "sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==", - "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" - } - }, - "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=", - "dev": true, - "requires": { - "dom-walk": "^0.1.0" - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - }, - "minipass": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", - "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true - } - } - }, - "minizlib": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", - "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", - "dev": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mississippi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-2.0.0.tgz", - "integrity": "sha512-zHo8v+otD1J10j/tC+VNoGK9keCuByhKovAvdn74dmxJl9+mWHnx6EMsDN4lgRoMI/eYo2nchAxniIbUPb5onw==", - "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" - } - } - } - }, - "mixin-deep": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", - "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mixin-object": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", - "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", - "dev": true, - "requires": { - "for-in": "^0.1.3", - "is-extendable": "^0.1.1" - }, - "dependencies": { - "for-in": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", - "dev": true - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "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=" - } - } - }, - "mocha": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-6.1.4.tgz", - "integrity": "sha512-PN8CIy4RXsIoxoFJzS4QNnCH4psUCPWc4/rPrst/ecSJJbLBkubMiyGCP2Kj/9YnWbotFqAoeXyXMucj7gwCFg==", - "dev": true, - "requires": { - "ansi-colors": "3.2.3", - "browser-stdout": "1.3.1", - "debug": "3.2.6", - "diff": "3.5.0", - "escape-string-regexp": "1.0.5", - "find-up": "3.0.0", - "glob": "7.1.3", - "growl": "1.10.5", - "he": "1.2.0", - "js-yaml": "3.13.1", - "log-symbols": "2.2.0", - "minimatch": "3.0.4", - "mkdirp": "0.5.1", - "ms": "2.1.1", - "node-environment-flags": "1.0.5", - "object.assign": "4.1.0", - "strip-json-comments": "2.0.1", - "supports-color": "6.0.0", - "which": "1.3.1", - "wide-align": "1.1.3", - "yargs": "13.2.2", - "yargs-parser": "13.0.0", - "yargs-unparser": "1.5.0" - }, - "dependencies": { - "ansi-colors": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.3.tgz", - "integrity": "sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw==", - "dev": true + "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" + } }, - "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" - } + "mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } + "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 }, - "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" - } + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "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 + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } }, - "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" - } + "miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dev": true, + "requires": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + } }, - "glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", - "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" - } + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "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 + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" }, - "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 + "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": { + "mime-db": "1.52.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" - } + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", + "dev": true }, - "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 - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } + "minimatch": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.8.tgz", + "integrity": "sha512-7RN35vit8DeBclkofOVmBY0eDAZZQd1HzmukRdSyz95CRh8FT54eqnbj0krQr3mrHR6sfRyYkyhwBWjoV5uqlQ==", + "requires": { + "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" + } + } + } }, - "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 + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "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 + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } }, - "supports-color": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.0.0.tgz", - "integrity": "sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } + "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 }, - "yargs": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", - "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.0.0" - } + "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 + } + } }, - "yargs-parser": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", - "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.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=", - "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 + "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": { + "debug": "^2.2.0", + "md5": "^2.1.0", + "mkdirp": "~0.5.1", + "strip-ansi": "^6.0.1", + "xml": "^1.0.0" + } }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "mocha-multi-reporters": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.1.7.tgz", - "integrity": "sha1-zH8/TTL0eFIJQdhSq7ZNmYhYfYI=", - "dev": true, - "requires": { - "debug": "^3.1.0", - "lodash": "^4.16.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "mocha-multi-reporters": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/mocha-multi-reporters/-/mocha-multi-reporters-1.5.1.tgz", + "integrity": "sha512-Yb4QJOaGLIcmB0VY7Wif5AjvLMUFAdV57D2TWEva1Y0kU/3LjKpeRVmlMIfuO1SVbauve459kgtIizADqxMWPg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "lodash": "^4.17.15" + }, + "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" + } + } + } + }, + "module-details-from-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.3.tgz", + "integrity": "sha512-ySViT69/76t8VhE1xXHK6Ch4NcDd26gx0MzKXLO+F7NOtnqH68d9zF94nT8ZWSxXh8ELOERsnJO/sWt1xZYw5A==" + }, + "mrmime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.0.tgz", + "integrity": "sha512-a70zx7zFfVO7XpnQ2IX1Myh9yY4UYvfld/dikWRnsXxbyvMcfz+u6UfgNAtH+k2QqtJuzVpv6eLTx1G2+WKZbQ==", + "dev": true }, "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 - } - } - }, - "moment": { - "version": "2.21.0", - "resolved": "http://registry.npmjs.org/moment/-/moment-2.21.0.tgz", - "integrity": "sha512-TCZ36BjURTeFTM/CwRcViQlfkMvL1/vFISuNLO5GkcVm1+QHfbSiNqZuWeMFjj1/3+uAjXswgRk30j1kkLYJBQ==" - }, - "monaco-editor": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.16.2.tgz", - "integrity": "sha512-NtGrFzf54jADe7qsWh3lazhS7Kj0XHkJUGBq9fA/Jbwc+sgVcyfsYF6z2AQ7hPqDC+JmdOt/OwFjBnRwqXtx6w==", - "dev": true - }, - "monaco-editor-textmate": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/monaco-editor-textmate/-/monaco-editor-textmate-2.1.1.tgz", - "integrity": "sha512-7jbOpjHhjJn5BYNjBSTD/yVf+Pnd6gBqr69skvFw8n1gJaUvjlVBZBCc5nrF5E8Q/4s1nOKuvqH/OvE+loDebg==" - }, - "monaco-editor-webpack-plugin": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-1.7.0.tgz", - "integrity": "sha512-oItymcnlL14Sjd7EF7q+CMhucfwR/2BxsqrXIBrWL6LQplFfAfV+grLEQRmVHeGSBZ/Gk9ptzfueXnWcoEcFuA==", - "dev": true, - "requires": { - "@types/webpack": "^4.4.19" - } - }, - "monaco-textmate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/monaco-textmate/-/monaco-textmate-3.0.0.tgz", - "integrity": "sha512-llE/NasQkbAEDx/RPp0ili5ZEXH4e/UkYFMACvJrEY0aybq6FVW9qySt5C4kWwRXCJDL+4ewgoTt4XO3M+bfIg==", - "requires": { - "fast-plist": "^0.1.2" - } - }, - "moo": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz", - "integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw==", - "dev": true - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "multimatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", - "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", - "dev": true, - "requires": { - "array-differ": "^1.0.0", - "array-union": "^1.0.1", - "arrify": "^1.0.0", - "minimatch": "^3.0.0" - } - }, - "mute-stdout": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-1.0.1.tgz", - "integrity": "sha512-kDcwXR4PS7caBpuRYYBUz9iVixUk3anO3f5OYFiIPwK/20vCzKCHyKoulbiDY1S53zD2bxUpxN/IJ+TnXjfvxg==", - "dev": true - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "mv": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", - "integrity": "sha1-rmzg1vbV4KT32JN5jQPB6pVZtqI=", - "dev": true, - "requires": { - "mkdirp": "~0.5.1", - "ncp": "~2.0.0", - "rimraf": "~2.4.0" - }, - "dependencies": { - "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "mute-stdout": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stdout/-/mute-stdout-2.0.0.tgz", + "integrity": "sha512-32GSKM3Wyc8dg/p39lWPKYu8zci9mJFzV1Np9Of0ZEpe6Fhssn/FbI7ywAMd40uX+p3ZKh3T5EeCFv81qS3HmQ==", + "dev": true + }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "named-js-regexp": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.5.tgz", + "integrity": "sha512-XO0DPujDP9IWpkt690iWLreKztb/VB811DGl5N3z7BfhkMJuiVZXOi6YN/fEB9qkvtMVTgSZDW8pzdVt8vj/FA==" + }, + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true + }, + "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 + }, + "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 }, - "rimraf": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", - "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", - "dev": true, - "requires": { - "glob": "^6.0.1" - } - } - } - }, - "named-js-regexp": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/named-js-regexp/-/named-js-regexp-1.3.3.tgz", - "integrity": "sha512-zIUAXzGQOp16VR0Ct89SDstU62hzAPBluNUrUrsdD7MNSRbm/vyqGhEnp+4hnsMjmX3C2wh1cbIEP0joKMFLxw==" - }, - "nan": { - "version": "2.13.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.13.2.tgz", - "integrity": "sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==", - "dev": true - }, - "nanomatch": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.9.tgz", - "integrity": "sha512-n8R9bS8yQ6eSXaV6jHUpKzD8gLsin02w1HSFiegwrs9E098Ylhw5jdyKPaYqvHknHaSCKTPp7C8dGCQ0q9koXA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-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" - } - }, - "ncp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", - "integrity": "sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=", - "dev": true - }, - "nearley": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.15.1.tgz", - "integrity": "sha512-8IUY/rUrKz2mIynUGh8k+tul1awMKEjeHHC5G3FHvvyAW6oq4mQfNp2c0BMea+sYZJvYcrrM6GmZVIle/GRXGw==", - "dev": true, - "requires": { - "moo": "^0.4.3", - "nomnom": "~1.6.2", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6", - "semver": "^5.4.1" - } - }, - "needle": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", - "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", - "dev": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true + }, + "nise": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nise/-/nise-6.0.0.tgz", + "integrity": "sha512-K8ePqo9BFvN31HXwEtTNGzgrPpmvgciDsFz8aztFjt4LqKO/JeFD8tBOeuDiCMXrIl/m1YvfH8auSpxfaD09wg==", + "dev": true, + "requires": { + "@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-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": { + "semver": "^7.3.5" + } + }, + "node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "optional": true + }, + "node-has-native-dependencies": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz", + "integrity": "sha1-MVLsl1O2ZB5NMi0YXdSTBkmto9o=", + "dev": true, + "requires": { + "fs-walk": "0.0.1" + } + }, + "node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dev": true, + "requires": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + }, + "dependencies": { + "buffer": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "dev": true, + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "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 - } - } - }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==", - "dev": true - }, - "neo-async": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.5.2.tgz", - "integrity": "sha512-vdqTKI9GBIYcAEbFAcpKPErKINfPF5zIuz3/niBfq8WUZjpT2tytLlFVrBgWdOtqI4uaA/Rb6No0hux39XXDuw==", - "dev": true - }, - "nested-error-stacks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.0.tgz", - "integrity": "sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==", - "dev": true - }, - "next-tick": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true - }, - "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 - }, - "nise": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-1.5.0.tgz", - "integrity": "sha512-Z3sfYEkLFzFmL8KY6xnSJLRxwQwYBjOXi/24lb62ZnZiGA0JUzGGTI6TBIgfCSMIDl9Jlu8SRmHNACLTemDHww==", - "dev": true, - "requires": { - "@sinonjs/formatio": "^3.1.0", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "lolex": "^4.1.0", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "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": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } }, - "path-to-regexp": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", - "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, - "requires": { - "isarray": "0.0.1" - } - } - } - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "nock": { - "version": "10.0.6", - "resolved": "https://registry.npmjs.org/nock/-/nock-10.0.6.tgz", - "integrity": "sha512-b47OWj1qf/LqSQYnmokNWM8D88KvUl2y7jT0567NB3ZBAZFz2bWp2PC81Xn7u8F2/vJxzkzNZybnemeFa7AZ2w==", - "dev": true, - "requires": { - "chai": "^4.1.2", - "debug": "^4.1.0", - "deep-equal": "^1.0.0", - "json-stringify-safe": "^5.0.1", - "lodash": "^4.17.5", - "mkdirp": "^0.5.0", - "propagate": "^1.0.0", - "qs": "^6.5.1", - "semver": "^5.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } + "node-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": { + "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": { + "assert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.0.0.tgz", + "integrity": "sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==", + "dev": true, + "requires": { + "es6-object-assign": "^1.1.0", + "is-nan": "^1.2.1", + "object-is": "^1.0.1", + "util": "^0.12.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "domain-browser": { + "version": "4.22.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz", + "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==", + "dev": true + }, + "path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "stream-browserify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz", + "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==", + "dev": true, + "requires": { + "inherits": "~2.0.4", + "readable-stream": "^3.5.0" + } + }, + "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" + } + } + } }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "node-environment-flags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/node-environment-flags/-/node-environment-flags-1.0.5.tgz", - "integrity": "sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ==", - "dev": true, - "requires": { - "object.getownpropertydescriptors": "^2.0.3", - "semver": "^5.7.0" - }, - "dependencies": { - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true - } - } - }, - "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-has-native-dependencies": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/node-has-native-dependencies/-/node-has-native-dependencies-1.0.2.tgz", - "integrity": "sha1-MVLsl1O2ZB5NMi0YXdSTBkmto9o=", - "dev": true, - "requires": { - "fs-walk": "0.0.1" - } - }, - "node-html-parser": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-1.1.13.tgz", - "integrity": "sha512-g8H73/DHTFH17N0dukN1XkCdJm9TF9cpsaElT/4PeIQR+hBR2T3rmheO1EeFBOqg4ot2s1530XPD1/dsVk8MNQ==", - "dev": true, - "requires": { - "he": "1.1.1" - } - }, - "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==", - "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 + "node-preload": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", + "integrity": "sha512-RM5oyBy45cLEoHqCeh+MNuFAxO0vTFBLskvQbOKnEE7YTTSN4tbN8QWDIPQ6L+WvKsB/qLEGpYe2ZZ9d4W9OIQ==", + "dev": true, + "requires": { + "process-on-spawn": "^1.0.0" + } }, - "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" - } + "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 }, - "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 + "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==" }, - "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" - } + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-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==", - "dev": true, - "requires": { - "semver": "^5.3.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==" - }, - "nomnom": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.6.2.tgz", - "integrity": "sha1-hKZqJgF0QI/Ft3oY+IjszET7aXE=", - "dev": true, - "requires": { - "colors": "0.5.x", - "underscore": "~1.4.4" - }, - "dependencies": { - "colors": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/colors/-/colors-0.5.1.tgz", - "integrity": "sha1-fQAj6usVTo7p/Oddy5I9DtFmd3Q=", - "dev": true + "now-and-later": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz", + "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==", + "dev": true, + "requires": { + "once": "^1.3.2" + } }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=", - "dev": true - } - } - }, - "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==", - "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" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "dev": true, - "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, - "dependencies": { - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - } - } - }, - "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=", - "dev": true, - "requires": { - "once": "^1.3.2" - } - }, - "npm-bundled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.6.tgz", - "integrity": "sha512-8/JCaftHwbd//k6y2rEWp6k1wxVfpFzB6t1p825+cUb7Ym2XQfhwIC5KwhrvzZRJu+LtDE585zVaS32+CGtf0g==", - "dev": true - }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", - "dev": true, - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - } - }, - "npm-packlist": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.1.tgz", - "integrity": "sha512-+TcdO7HJJ8peiiYhvPxsEDhF3PJFGUGRcFsGve3vxvxdcpO2Z4Z7rkosRM0kWj6LfbK/P0gu3dzk5RU1ffvFcw==", - "dev": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "nth-check": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.1.tgz", - "integrity": "sha1-mSms32KPwsQQmN6rgqxYDPFJquQ=", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "nugget": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/nugget/-/nugget-2.0.1.tgz", - "integrity": "sha1-IBCVpIfhrTYIGzQy+jytpPjQcbA=", - "dev": true, - "requires": { - "debug": "^2.1.3", - "minimist": "^1.1.0", - "pretty-bytes": "^1.0.2", - "progress-stream": "^1.1.0", - "request": "^2.45.0", - "single-line-log": "^1.1.2", - "throttleit": "0.0.2" - }, - "dependencies": { - "throttleit": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-0.0.2.tgz", - "integrity": "sha1-z+34jmDADdlpe2H90qg0OptoDq8=", - "dev": true - } - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "numeral": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/numeral/-/numeral-2.0.6.tgz", - "integrity": "sha1-StCAk21EPCVhrtnyGX7//iX05QY=", - "dev": true - }, - "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==", - "dev": true - }, - "nyc": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-14.1.1.tgz", - "integrity": "sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==", - "dev": true, - "requires": { - "archy": "^1.0.0", - "caching-transform": "^3.0.2", - "convert-source-map": "^1.6.0", - "cp-file": "^6.2.0", - "find-cache-dir": "^2.1.0", - "find-up": "^3.0.0", - "foreground-child": "^1.5.6", - "glob": "^7.1.3", - "istanbul-lib-coverage": "^2.0.5", - "istanbul-lib-hook": "^2.0.7", - "istanbul-lib-instrument": "^3.3.0", - "istanbul-lib-report": "^2.0.8", - "istanbul-lib-source-maps": "^3.0.6", - "istanbul-reports": "^2.2.4", - "js-yaml": "^3.13.1", - "make-dir": "^2.1.0", - "merge-source-map": "^1.1.0", - "resolve-from": "^4.0.0", - "rimraf": "^2.6.3", - "signal-exit": "^3.0.2", - "spawn-wrap": "^1.4.2", - "test-exclude": "^5.2.3", - "uuid": "^3.3.2", - "yargs": "^13.2.2", - "yargs-parser": "^13.0.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" - } + "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": { + "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 + } + } }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } + "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": { + "boolbase": "^1.0.0" + } }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.1" - } + "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": { + "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" + } + } + } }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } + "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 }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } + "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 + }, + "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": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } }, - "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" - } + "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 + }, + "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": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true + "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": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.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" - } + "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": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1" + } }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "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" - } + "object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + } }, - "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" - } + "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": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + } }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } + "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": { + "define-properties": "^1.1.3", + "es-abstract": "^1.19.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 + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "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 + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true + "open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "requires": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + } }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@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" + } }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true + "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" + } }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } + "os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", + "dev": true + }, + "p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true + }, + "p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "requires": { + "p-timeout": "^2.0.1" + } }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - } + "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 }, - "yargs": { - "version": "13.2.4", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz", - "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "os-locale": "^3.1.0", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.0" - } + "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 }, - "yargs-parser": { - "version": "13.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", - "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "requires": { + "aggregate-error": "^3.0.0" + } }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-inspect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.6.0.tgz", - "integrity": "sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ==", - "dev": true - }, - "object-is": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.0.1.tgz", - "integrity": "sha1-CqYOyZiaCz7Xlc9NBvYs8a1lObY=", - "dev": true - }, - "object-keys": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", - "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", - "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "object.entries": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.0.4.tgz", - "integrity": "sha1-G/mk3SKI9bM/Opk9JXZh8F0WGl8=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.6.1", - "function-bind": "^1.1.0", - "has": "^1.0.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.omit": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/object.omit/-/object.omit-2.0.1.tgz", - "integrity": "sha1-Gpx0SCnznbuFjHbKNXmuKlTr0fo=", - "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" - } - } - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.reduce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.reduce/-/object.reduce-1.0.1.tgz", - "integrity": "sha1-b+NI8qx/oPlcpiEiZZkJaCW7A60=", - "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - } - }, - "object.values": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.0.4.tgz", - "integrity": "sha1-5STaCbT2b/Bd9FdUbscqyZ8TBpo=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.6.1", - "function-bind": "^1.1.0", - "has": "^1.0.1" - } - }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "one-time": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/one-time/-/one-time-0.0.4.tgz", - "integrity": "sha1-+M33eISCb+Tf+T46nMN7HkSAdC4=" - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "onigasm": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/onigasm/-/onigasm-2.2.2.tgz", - "integrity": "sha512-TQTMk+RmPYx4sGzNAgV0q7At7PABDNHVqZBlC4aRXHg8hpCdemLOF0qq0gUCjwUbc7mhJMBOo3XpTRYwyr45Gw==", - "requires": { - "lru-cache": "^4.1.1" - } - }, - "opener": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz", - "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA==", - "dev": true - }, - "opn": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", - "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "ordered-read-streams": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", - "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", - "dev": true, - "requires": { - "readable-stream": "^2.0.1" - } - }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", - "dev": true, - "requires": { - "url-parse": "^1.4.3" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true - }, - "os-locale": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.0.1.tgz", - "integrity": "sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw==", - "dev": true, - "requires": { - "execa": "^0.10.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "output-file-sync": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/output-file-sync/-/output-file-sync-2.0.1.tgz", - "integrity": "sha512-mDho4qm7WgIXIGf4eYU1RHN2UU5tPfVYVSRwDJw0uTmj35DQUt/eNp19N7v6T3SrR0ESTEf2up2CGO73qI35zQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "is-plain-obj": "^1.1.0", - "mkdirp": "^0.5.1" - } - }, - "p-cancelable": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", - "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", - "dev": true - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", - "dev": true - }, - "p-event": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", - "integrity": "sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU=", - "dev": true, - "requires": { - "p-timeout": "^1.1.1" - }, - "dependencies": { "p-timeout": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", - "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", - "dev": true, - "requires": { - "p-finally": "^1.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=", - "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==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - }, - "dependencies": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "requires": { + "p-finally": "^1.0.0" + } + }, "p-try": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", - "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==", - "dev": true - } - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "p-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=", - "dev": true - }, - "package-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-3.0.0.tgz", - "integrity": "sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.15", - "hasha": "^3.0.0", - "lodash.flattendeep": "^4.4.0", - "release-zalgo": "^1.0.0" - }, - "dependencies": { - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "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==", - "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" - }, - "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 + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "package-hash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/package-hash/-/package-hash-4.0.0.tgz", + "integrity": "sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.15", + "hasha": "^5.0.0", + "lodash.flattendeep": "^4.4.0", + "release-zalgo": "^1.0.0" + } }, - "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" - } + "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 }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha1-35T9jPZTHs915r75oIWPvHK+Ikc=", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parse-asn1": { - "version": "5.1.1", - "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz", - "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==", - "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" - } - }, - "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" - } - }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" - } - }, - "parse-glob": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/parse-glob/-/parse-glob-3.0.4.tgz", - "integrity": "sha1-ssN2z7EfNVE7rdFz7wu246OIORw=", - "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 + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } }, - "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" - } - } - } - }, - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true - }, - "parse-semver": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", - "integrity": "sha1-mkr9bfBj3Egm+T+6SpnPIj9mbLg=", - "dev": true, - "requires": { - "semver": "^5.1.0" - } - }, - "parse5": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-3.0.3.tgz", - "integrity": "sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.0.tgz", - "integrity": "sha1-oLhwcpquIUAFt9UDLsLLuw+0RRo=", - "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": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "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=" - }, - "path-posix": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/path-posix/-/path-posix-1.0.0.tgz", - "integrity": "sha1-BrJhE/Vr6rBCVFojv6iAA8ysJg8=" - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", - "dev": true, - "requires": { - "path-root-regex": "^0.1.0" - } - }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } - } - }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", - "dev": true - }, - "pause-stream": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", - "integrity": "sha1-/lo0sMvOErWqaitAPuLnO2AvFEU=", - "dev": true, - "requires": { - "through": "~2.3" - } - }, - "pbkdf2": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz", - "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "pdfkit": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.10.0.tgz", - "integrity": "sha512-mRJ6iuDzpIQ4ftKp5GvijLXNVRK86xjnyIPBraYSPrUPubNqWM5/oYmc7FZKUWz3wusRTj3PLR9HJ1X5ooqfsg==", - "requires": { - "crypto-js": "^3.1.9-1", - "fontkit": "^1.0.0", - "linebreak": "^0.3.0", - "png-js": ">=0.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=" - }, - "pidusage": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pidusage/-/pidusage-1.2.0.tgz", - "integrity": "sha512-OGo+iSOk44HRJ8q15AyG570UYxcm5u+R99DI8Khu8P3tKGkVu5EZX4ywHglWSTMNNXQ274oeGpYrvFEhDIFGPg==" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "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" - } - } - } - }, - "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" - } - }, - "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" - } + "parse-asn1": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz", + "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==", + "dev": true, + "requires": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "pbkdf2": "^3.1.5", + "safe-buffer": "^5.2.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha512-FwdRXKCohSVeXqwtYonZTXtbGJKrn+HNyWDYVcp5yuJlesTwNH4rsmRZ+GrKAPJ5bLpRxESMeS+Rl0VCHRvB2Q==", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + } + }, + "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": { + "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 + } + } + }, + "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": { + "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 + } + } + }, + "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": { + "path-root-regex": "^0.1.0" + } + }, + "path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha512-4GlJ6rZDhQZFE0DPVKh0e9jmZ5egZfxTkp7bcRDuPlJXbAwhxcl2dINPUAsjLdejqaLsCeg8axcLjIbvBjN4pQ==", + "dev": true + }, + "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": { + "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 + } + } }, - "arr-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-2.1.0.tgz", - "integrity": "sha1-IPnqtexw9cfSFbEHexw5Fh0pLH0=", - "dev": true + "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 }, - "array-slice": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-0.2.3.tgz", - "integrity": "sha1-3Tz7gO15c6dRF82sabC5nshhhvU=", - "dev": true + "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": { + "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 + } + } }, - "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" - } + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", + "dev": true }, - "kind-of": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", - "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", - "dev": true - } - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, - "png-js": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/png-js/-/png-js-0.1.1.tgz", - "integrity": "sha1-HMfCEjA6yr50Jj7DrHgAlYAkLZM=" - }, - "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" - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", - "dev": true - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - }, - "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" - } + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true }, - "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" - } + "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 }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true + }, + "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 + }, + "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": { + "pinkie": "^2.0.0" + } }, - "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 + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } }, - "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" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-1.2.1.tgz", - "integrity": "sha512-6jt9XZwUhwmRUhb/CkyJY020PYaPJsCyt3UjbaWo6XEbH/94Hmv6MP7fG2C5NDU/BcHzyGYxNtHvM+LTf9HrYw==", - "dev": true, - "requires": { - "postcss": "^6.0.1" - } - }, - "postcss-modules-local-by-default": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-1.2.0.tgz", - "integrity": "sha1-99gMOYxaOT+nlkRmvRlQCn1hwGk=", - "dev": true, - "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" - } - }, - "postcss-modules-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-1.1.0.tgz", - "integrity": "sha1-1upkmUx5+XtipytCb75gVqGUu5A=", - "dev": true, - "requires": { - "css-selector-tokenizer": "^0.7.0", - "postcss": "^6.0.1" - } - }, - "postcss-modules-values": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-1.3.0.tgz", - "integrity": "sha1-7P+p1+GSUYOJ9CrQ6D9yrsRW6iA=", - "dev": true, - "requires": { - "icss-replace-symbols": "^1.1.0", - "postcss": "^6.0.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "postinstall-build": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/postinstall-build/-/postinstall-build-5.0.1.tgz", - "integrity": "sha1-uRepB5smF42aJK9aXNjLSpkdEbk=", - "dev": true - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true - }, - "preserve": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz", - "integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=", - "dev": true - }, - "pretty-bytes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-1.0.4.tgz", - "integrity": "sha1-CiLoIQYJrTVUL4yNXSFZr/B1HIQ=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1", - "meow": "^3.1.0" - } - }, - "pretty-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", - "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", - "dev": true, - "requires": { - "renderkid": "^2.0.1", - "utila": "~0.4" - } - }, - "pretty-hrtime": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz", - "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", - "dev": true - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==", - "dev": true - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", - "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" - }, - "progress": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", - "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", - "dev": true - }, - "progress-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/progress-stream/-/progress-stream-1.2.0.tgz", - "integrity": "sha1-LNPP6jO6OonJwSHsM0er6asSX3c=", - "dev": true, - "requires": { - "speedometer": "~0.1.2", - "through2": "~0.2.3" - }, - "dependencies": { - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "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" + } }, - "object-keys": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", - "integrity": "sha1-KKaq50KN0sOpLz2V8hM13SBOAzY=", - "dev": true + "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": { + "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": { + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } }, - "readable-stream": { - "version": "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" - } + "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 }, - "through2": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-0.2.3.tgz", - "integrity": "sha1-6zKE2k6jEbbMis42U3SKUqvyWj8=", - "dev": true, - "requires": { - "readable-stream": "~1.1.9", - "xtend": "~2.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 }, - "xtend": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-2.1.2.tgz", - "integrity": "sha1-bv7MKk2tjmlixJAbM3znuoe10os=", - "dev": true, - "requires": { - "object-keys": "~0.4.0" - } - } - } - }, - "promise": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/promise/-/promise-8.0.1.tgz", - "integrity": "sha1-5F1osAoXZHttpxG/he1u1HII9FA=", - "dev": true, - "requires": { - "asap": "~2.0.3" - } - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "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==", - "requires": { - "loose-envify": "^1.3.1", - "object-assign": "^4.1.1" - } - }, - "propagate": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/propagate/-/propagate-1.0.0.tgz", - "integrity": "sha1-AMLa7t2iDofjeCs0Stuhzd1q1wk=", - "dev": true - }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true - }, - "proxy-addr": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", - "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==", - "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.0" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" - }, - "psl": { - "version": "1.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==", - "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" - } - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - }, - "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" - } - } - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "pure-color": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/pure-color/-/pure-color-1.3.0.tgz", - "integrity": "sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=", - "dev": true - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", - "dev": true - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dev": true, - "requires": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true - }, - "querystringify": { - "version": "2.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": "http://registry.npmjs.org/queue/-/queue-3.1.0.tgz", - "integrity": "sha1-bEnQHwCeIlZ4h4nyv/rGuLmZBYU=", - "dev": true, - "requires": { - "inherits": "~2.0.0" - } - }, - "quote-stream": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", - "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", - "requires": { - "buffer-equal": "0.0.1", - "minimist": "^1.1.3", - "through2": "^2.0.0" - }, - "dependencies": { - "buffer-equal": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", - "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" - } - } - }, - "raf": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz", - "integrity": "sha512-pDP/NMRAXoTfrhCfyfSEwJAKLaxBU9eApMeBPB1TkDouZmvPerIClV8lTAd+uF8ZiTaVl69e1FCxQrAd/VTjGw==", - "requires": { - "performance-now": "^2.1.0" - } - }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", - "dev": true - }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", - "dev": true, - "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" - } - }, - "randomatic": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", - "integrity": "sha512-TuDE5KxZ0J461RVjrJZCJc+J+zCkTb1MbH9AQUq68sMhOMcy9jLcb3BrZKgp9q9Ncltdg4QVqWrH02W2EFFVYw==", - "dev": 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 - } - } - }, - "randombytes": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz", - "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-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==", - "dev": true, - "requires": { - "bytes": "3.0.0", - "http-errors": "1.6.3", - "iconv-lite": "0.4.23", - "unpipe": "1.0.0" - }, - "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" - } - } - } - }, - "raw-loader": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/raw-loader/-/raw-loader-0.5.1.tgz", - "integrity": "sha1-DD0L6u2KAclm2Xh793goElKpeao=", - "dev": true - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "react": { - "version": "16.5.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.5.2.tgz", - "integrity": "sha512-FDCSVd3DjVTmbEAjUNX6FgfAmQ+ypJfHUsqUJOYNCBUp1h8lqmtC+0mXJ+JjsWx4KAVTkk1vKd1hLQPvEviSuw==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "schedule": "^0.5.0" - } - }, - "react-annotation": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/react-annotation/-/react-annotation-1.3.1.tgz", - "integrity": "sha512-jwbl7v5fMvkQrqdFWIIjuziqUzvEwyzhhSJ61bFjxPDEIizRuWf0ym6o6dV034toePs429sG6btGaLWGMC9zEw==", - "dev": true, - "requires": { - "prop-types": "15.6.0", - "viz-annotation": "0.0.1-3" - }, - "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" - } - } - } - }, - "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-data-grid": { - "version": "6.0.2-0", - "resolved": "https://registry.npmjs.org/react-data-grid/-/react-data-grid-6.0.2-0.tgz", - "integrity": "sha512-OiE/EevjV70J60OGj2Pcy8P3uRQGvyD0Ata2NbXjE2rKOGUuJ7hau8WcvIFhAMP005LEV+pfqzZpkIYjD1u3OQ==", - "dev": true - }, - "react-data-grid-addons": { - "version": "6.0.2-0", - "resolved": "https://registry.npmjs.org/react-data-grid-addons/-/react-data-grid-addons-6.0.2-0.tgz", - "integrity": "sha512-0GXbjMyBrXzQuQOSPltFS7XX4EzODf+THNAXdAIKJvN5nYLW/xyaKbkHnA/X8ydYkwDedYpNV1bHbYJeooViCw==", - "dev": true - }, - "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=", - "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } + "prettier": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.0.2.tgz", + "integrity": "sha512-5xJQIPT8BraI7ZnaDwSbu5zLrB6vvi8hVV58yHQ+QK64qrY40dULy0HSRlQ2/2IdzeBpjhDkqdcFBnFeDEMVdg==", + "dev": true }, - "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" - } - }, - "opn": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.2.0.tgz", - "integrity": "sha512-Jd/GpzPyHF4P2/aNOVmS3lfMSWV9J7cOhCG1s08XCEAsPkB7lp6ddiU0J7XzyQRDUh8BqJ7PchfINjR8jyofRQ==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", + "dev": true + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "process-on-spawn": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/process-on-spawn/-/process-on-spawn-1.0.0.tgz", + "integrity": "sha512-1WsPDsUSMmZH5LeMLegqkPDrsGgsWwk1Exipy2hvB0o/F0ASzbpIctSCcZIK1ykJvtTJULEH+20WOFjMvGnCTg==", + "dev": true, + "requires": { + "fromentries": "^1.2.0" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "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-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==" - }, - "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" - } - }, - "react-motion": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/react-motion/-/react-motion-0.5.2.tgz", - "integrity": "sha512-9q3YAvHoUiWlP3cK0v+w1N5Z23HXMj4IF4YuvjvWegWqNPfLXsOBE/V7UvQGpXxHFKRQQcNcVQE31g9SB/6qgQ==", - "requires": { - "performance-now": "^0.2.0", - "prop-types": "^15.5.8", - "raf": "^3.1.0" - }, - "dependencies": { - "performance-now": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", - "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=" - } - } - }, - "react-move": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/react-move/-/react-move-2.9.1.tgz", - "integrity": "sha512-5qKYsJrKKpSypEaaYyR2HBbBgX65htRqKDa8o5OGDkq2VfklmTCbLawtYFpdmcJRqbz4jCYpzo2Rrsazq9HA8Q==", - "requires": { - "@babel/runtime": "^7.2.0", - "d3-interpolate": "^1.3.2", - "d3-timer": "^1.0.9", - "prop-types": "^15.6.2", - "react-lifecycles-compat": "^3.0.4" - } - }, - "react-svg-pan-zoom": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-svg-pan-zoom/-/react-svg-pan-zoom-3.1.0.tgz", - "integrity": "sha512-hmDUarqhNnCwuZumV9Pw7o5inW7lda4sX2U1vDK2B2slrSfNu1jbelOp6aaOEyUF7WzMA1xrpH6NBWvk4UeUTQ==", - "requires": { - "prop-types": "^15.7.2", - "transformation-matrix": "^2.0.0" - }, - "dependencies": { "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } + "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": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } }, - "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" - } - } - }, - "react-svgmt": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/react-svgmt/-/react-svgmt-1.1.8.tgz", - "integrity": "sha512-3xu7iWuHIbqM2hv4eMsAN1mZKz6EnXTPAcE4mMX/NwuYY5uUKJBoKDAmxY+I6KXHx2SpYJtKAqe1a5jEehteZg==", - "requires": { - "d3-ease": "^1.0.3", - "react-motion": "^0.5.2", - "react-move": "^2.7.0" - } - }, - "react-table": { - "version": "6.8.6", - "resolved": "https://registry.npmjs.org/react-table/-/react-table-6.8.6.tgz", - "integrity": "sha1-oK2LSDkxkFLVvvwBJgP7Fh5S7eM=", - "dev": true, - "requires": { - "classnames": "^2.2.5" - } - }, - "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" - } - }, - "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" - } - }, - "react-virtualized": { - "version": "9.21.1", - "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.1.tgz", - "integrity": "sha512-E53vFjRRMCyUTEKuDLuGH1ld/9TFzjf/fFW816PE4HFXWZorESbSTYtiZz1oAjra0MminaUU1EnvUxoGuEFFPA==", - "requires": { - "babel-runtime": "^6.26.0", - "clsx": "^1.0.1", - "dom-helpers": "^2.4.0 || ^3.0.0", - "linear-layout-vector": "0.0.1", - "loose-envify": "^1.3.0", - "prop-types": "^15.6.0", - "react-lifecycles-compat": "^3.0.4" - } - }, - "reactcss": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", - "integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==", - "dev": true, - "requires": { - "lodash": "^4.0.1" - } - }, - "read": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", - "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", - "dev": true, - "requires": { - "mute-stream": "~0.0.4" - } - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", - "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - } - }, - "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" - } - }, - "readdirp": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.1.0.tgz", - "integrity": "sha1-TtCtBg3zBzMAxIRANz9y0cxkLXg=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "minimatch": "^3.0.2", - "readable-stream": "^2.0.2", - "set-immediate-shim": "^1.0.1" - } - }, - "rechoir": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", - "dev": true, - "requires": { - "resolve": "^1.1.6" - } - }, - "recursive-readdir": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.1.tgz", - "integrity": "sha1-kO8jHQd4xc4JPJpI105cVCLROpk=", - "dev": true, - "requires": { - "minimatch": "3.0.3" - }, - "dependencies": { - "minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", - "dev": true, - "requires": { - "brace-expansion": "^1.0.0" - } - } - } - }, - "redent": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", - "dev": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" - } - }, - "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==" - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-7.0.0.tgz", - "integrity": "sha512-s5NGghCE4itSlUS+0WUj88G6cfMVMmH8boTPNvABf8od+2dhT9WDlWu8n01raQAJZMOK8Ch6jSexaRO7swd6aw==", - "dev": true, - "requires": { - "regenerate": "^1.4.0" - } - }, - "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==", - "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==", - "dev": true, - "requires": { - "private": "^0.1.6" - } - }, - "regex-cache": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/regex-cache/-/regex-cache-0.4.4.tgz", - "integrity": "sha512-nVIZwtCjkC9YgvWkpM55B5rBhBYRZhAaJbgcFYXXsHnbZ9UZI9nnVWYZpBlCqv9ho2eZryPnWrZGsOdPwVWXWQ==", - "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==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.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==", - "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" - } - }, - "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==", - "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 - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha1-VNvzd+UUQKypCkzSdGANP/LYiKk=", - "dev": true - }, - "relative": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/relative/-/relative-3.0.2.tgz", - "integrity": "sha1-Dc2OxUpdNaPBXhBFA9ZTdbWlNn8=", - "dev": true, - "requires": { - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "release-zalgo": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha1-CXALflB0Mpc5Mw5TXFqQ+2eFFzA=", - "dev": true, - "requires": { - "es6-error": "^4.0.1" - } - }, - "remark-parse": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz", - "integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==", - "dev": true, - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "remove-bom-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", - "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5", - "is-utf8": "^0.2.1" - } - }, - "remove-bom-stream": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", - "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", - "dev": true, - "requires": { - "remove-bom-buffer": "^3.0.0", - "safe-buffer": "^5.1.0", - "through2": "^2.0.3" - } - }, - "remove-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, - "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" - } + "public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dev": true, + "requires": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true + }, + "qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "requires": { + "side-channel": "^1.1.0" + } + }, + "query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "requires": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + } + }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "dev": true + }, + "querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dev": true, + "requires": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "optional": true + } + } + }, + "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": { + "mute-stream": "~0.0.4" + } }, "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" - } + "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": { + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "utila": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.3.3.tgz", - "integrity": "sha1-1+jn1+MJEHCSsF+NloiCTWM6QiY=", - "dev": true - } - } - }, - "repeat-element": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.2.tgz", - "integrity": "sha1-7wiaF40Ug7quTZPrmLT55OEdmQo=", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", - "dev": true, - "requires": { - "is-finite": "^1.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 - }, - "replace-homedir": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-homedir/-/replace-homedir-1.0.0.tgz", - "integrity": "sha1-6H9tUTuSjd6AgmDBK+f+xv9ueYw=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1", - "is-absolute": "^1.0.0", - "remove-trailing-separator": "^1.1.0" - } - }, - "request": { - "version": "2.87.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.87.0.tgz", - "integrity": "sha512-fcogkm7Az5bsS6Sl0sibkbhcKsnyon/jV1kF3ajGmF0c8HrttdKTPRT9hieOaQHA5HEq6r8OyWOo/o781C1tNw==", - "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" - } - }, - "request-progress": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", - "integrity": "sha1-TKdUCBx/7GP1BeT6qCWqBs1mnb4=", - "requires": { - "throttleit": "^1.0.0" - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "dev": true, - "requires": { - "lodash": "^4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "dev": true, - "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", - "dev": true - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "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==", - "requires": { - "path-parse": "^1.0.5" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "resolve-options": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", - "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", - "dev": true, - "requires": { - "value-or-function": "^3.0.0" - } - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "restructure": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz", - "integrity": "sha1-9U591WNZD7NP1r9Vh2EJrsyyjeg=", - "requires": { - "browserify-optional": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "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=", - "dev": true - }, - "rewiremock": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.13.0.tgz", - "integrity": "sha512-1MkO4mX4j31GilbMsqdgLNXjmrHo9EUKQFCa82rLye8ltOHnJe0rRaHUSKz2yUClr8l0Qnj1ZTjZHmp6vNTrzQ==", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0", - "compare-module-exports": "^2.1.0", - "lodash.some": "^4.6.0", - "lodash.template": "^4.4.0", - "node-libs-browser": "^2.1.0", - "path-parse": "^1.0.5", - "wipe-node-cache": "^2.1.0", - "wipe-webpack-cache": "^2.1.0" - }, - "dependencies": { - "lodash.template": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.4.0.tgz", - "integrity": "sha1-5zoDhcg1VZF0bgILmWecaQ5o+6A=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", - "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", - "dev": true, - "requires": { - "lodash._reinterpolate": "~3.0.0" - } - } - } - }, - "rimraf": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", - "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", - "dev": true, - "requires": { - "glob": "^7.0.5" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "roughjs-es5": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/roughjs-es5/-/roughjs-es5-0.1.0.tgz", - "integrity": "sha512-NMjzoBgSYk8qEYLSxzxytS20sfdQV7zg119FZjFDjIDwaqodFcf7QwzKbqM64VeAYF61qogaPLk3cs8Gb+TqZA==", - "dev": true, - "requires": { - "babel-runtime": "^6.26.0" - } - }, - "rst-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", - "dev": true, - "requires": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" - } - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "dev": true, - "requires": { - "is-promise": "^2.1.0" - } - }, - "run-node": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/run-node/-/run-node-1.0.0.tgz", - "integrity": "sha512-kc120TBlQ3mih1LSzdAJXo4xn/GWS2ec0l3S+syHDXP9uRr0JAT8Qd3mdMuyjqCzeZktgP3try92cEgf9Nks8A==", - "dev": true - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "rx-lite": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz", - "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ=", - "dev": true - }, - "rx-lite-aggregates": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz", - "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=", - "dev": true, - "requires": { - "rx-lite": "*" - } - }, - "rxjs": { - "version": "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" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sass-loader": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-7.1.0.tgz", - "integrity": "sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w==", - "dev": true, - "requires": { - "clone-deep": "^2.0.1", - "loader-utils": "^1.0.1", - "lodash.tail": "^4.1.1", - "neo-async": "^2.5.0", - "pify": "^3.0.0", - "semver": "^5.5.0" - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "saxes": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.9.tgz", - "integrity": "sha512-FZeKhJglhJHk7eWG5YM0z46VHmI3KJpMBAQm3xa9meDvd+wevB5GuBB0wc0exPInZiBBHqi00DbS8AcvCGCFMw==", - "dev": true, - "requires": { - "xmlchars": "^1.3.1" - } - }, - "schedule": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/schedule/-/schedule-0.5.0.tgz", - "integrity": "sha512-HUcJicG5Ou8xfR//c2rPT0lPIRR09vVvN81T9fqfVgBmhERUbDEQoYKjpBxbueJnCPpSu2ujXzOnRQt6x9o/jw==", - "dev": true, - "requires": { - "object-assign": "^4.1.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "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" - } + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "requires": { + "resolve": "^1.20.0" + } + }, + "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": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + } + }, + "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" + } + }, + "remove-bom-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/remove-bom-buffer/-/remove-bom-buffer-3.0.0.tgz", + "integrity": "sha512-8v2rWhaakv18qcvNeli2mZ/TMTL2nEyAKRvzo1WtnZBl15SHyEhrCu2/xKlJyUFKHiHgfXIyuY6g2dObJJycXQ==", + "dev": true, + "requires": { + "is-buffer": "^1.1.5", + "is-utf8": "^0.2.1" + } + }, + "remove-bom-stream": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/remove-bom-stream/-/remove-bom-stream-1.2.0.tgz", + "integrity": "sha1-BfGlk/FuQuH7kOv1nejlaVJflSM=", + "dev": true, + "requires": { + "remove-bom-buffer": "^3.0.0", + "safe-buffer": "^5.1.0", + "through2": "^2.0.3" + } }, - "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 + "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 }, - "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 - } - } - }, - "scope-analyzer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/scope-analyzer/-/scope-analyzer-2.0.5.tgz", - "integrity": "sha512-+U5H0417mnTEstCD5VwOYO7V4vYuSqwqjFap40ythe67bhMFL5C3UgPwyBv7KDJsqUBIKafOD57xMlh1rN7eaw==", - "dev": true, - "requires": { - "array-from": "^2.1.1", - "es6-map": "^0.1.5", - "es6-set": "^0.1.5", - "es6-symbol": "^3.1.1", - "estree-is-function": "^1.0.0", - "get-assigned-identifiers": "^1.1.0" - } - }, - "seek-bzip": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", - "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", - "dev": true, - "requires": { - "commander": "~2.8.1" - }, - "dependencies": { - "commander": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", - "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - } - } - }, - "semiotic": { - "version": "1.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" - } - } - } - }, - "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" - } - }, - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - }, - "semver-compare": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", - "dev": true - }, - "semver-greatest-satisfied-range": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/semver-greatest-satisfied-range/-/semver-greatest-satisfied-range-1.1.0.tgz", - "integrity": "sha1-E+jCZYq5aRywzXEJMkAoDTb3els=", - "dev": true, - "requires": { - "sver-compat": "^1.5.0" - } - }, - "send": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz", - "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==", - "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 - } - } - }, - "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==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.2", - "send": "0.16.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==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.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==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shallow-clone": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-1.0.0.tgz", - "integrity": "sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA==", - "dev": true, - "requires": { - "is-extendable": "^0.1.1", - "kind-of": "^5.0.0", - "mixin-object": "^2.0.1" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "shallow-copy": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", - "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" - }, - "shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "dev": true - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shell-quote": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz", - "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=", - "dev": true, - "requires": { - "array-filter": "~0.0.0", - "array-map": "~0.0.0", - "array-reduce": "~0.0.0", - "jsonify": "~0.0.0" - } - }, - "shortid": { - "version": "2.2.8", - "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.8.tgz", - "integrity": "sha1-AzsRfWoul1gE9vCWnb59PQs1UTE=", - "dev": true - }, - "sigmund": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", - "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", - "dev": true - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true - }, - "simple-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=", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" - } - } - }, - "single-line-log": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/single-line-log/-/single-line-log-1.1.2.tgz", - "integrity": "sha1-wvg/Jzo+GhbtsJlWYdoO1e8DM2Q=", - "dev": true, - "requires": { - "string-width": "^1.0.1" - }, - "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=", - "dev": true, - "requires": { - "number-is-nan": "^1.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 + }, + "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": { + "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" + } + } + } }, - "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" - } + "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" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "sinon": { - "version": "7.3.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-7.3.2.tgz", - "integrity": "sha512-thErC1z64BeyGiPvF8aoSg0LEnptSaWE7YhdWWbWXgelOyThent7uKOnnEh9zBxDbKixtr5dEko+ws1sZMuFMA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.4.0", - "@sinonjs/formatio": "^3.2.1", - "@sinonjs/samsam": "^3.3.1", - "diff": "^3.5.0", - "lolex": "^4.0.1", - "nise": "^1.4.10", - "supports-color": "^5.5.0" - }, - "dependencies": { - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } }, - "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" - } - } - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - }, - "slickgrid": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/slickgrid/-/slickgrid-2.4.7.tgz", - "integrity": "sha512-kTp5hQjx2Gtfv7j7ClZK7S3P+2P7YDu4+BT1BUInuuIbUS7JGbBnyN1hVfU0XXfgG+jCTly1p1bjCdvNNlVdVg==", - "requires": { - "jquery": ">=1.8.0", - "jquery-ui": ">=1.8.0" - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "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": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + } }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "sockjs-client": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.1.5.tgz", - "integrity": "sha1-G7fA9yIsQPQq3xT0RCy9Eml3GoM=", - "dev": true, - "requires": { - "debug": "^2.6.6", - "eventsource": "0.1.6", - "faye-websocket": "~0.11.0", - "inherits": "^2.0.1", - "json3": "^3.3.2", - "url-parse": "^1.1.8" - } - }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - }, - "sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", - "dev": true, - "requires": { - "sort-keys": "^1.0.0" - } - }, - "source-list-map": { - "version": "2.0.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=", - "dev": true - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "dev": true, - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "sparkles": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz", - "integrity": "sha512-dSO0DDYUahUt/0/pD/Is3VIm5TGJjludZ0HVymmhYF6eNA53PVLhnUk0znSYbH8IYBuJdCE+1luR22jNLMaQdw==", - "dev": true - }, - "spawn-wrap": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-1.4.2.tgz", - "integrity": "sha512-vMwR3OmmDhnxCVxM8M+xO/FtIp6Ju/mNaDfCMMW7FDcLRTPFWUswec4LXJHTJE2hwTI9O0YBfygu4DalFl7Ylg==", - "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" - } - }, - "spdx-correct": { - "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==", - "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.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==", - "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.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==", - "dev": true - }, - "speedometer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/speedometer/-/speedometer-0.1.4.tgz", - "integrity": "sha1-mHbb0qFp0xFUAtSObqYynIgWpQ0=", - "dev": true - }, - "split": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/split/-/split-0.3.3.tgz", - "integrity": "sha1-zQ7qXmOiEd//frDwkcQTPi0N0o8=", - "dev": true, - "requires": { - "through": "2" - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.1.tgz", - "integrity": "sha1-Ew9Zde3a2WPx1W+SuaxsUfqfg+s=", - "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" - } - }, - "ssri": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-5.3.0.tgz", - "integrity": "sha512-XRSIPqLij52MtgoQavH/x/dU1qVKtWUAAZeOHsR9c2Ddi4XerFy3mc1alf+dLJKl9EUIm/Ht+EowFkTUOA6GAQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.1" - } - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" - }, - "stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha1-Gsig2Ug4SNFpXkGLbQMaPDzmjjs=", - "dev": true - }, - "stat-mode": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stat-mode/-/stat-mode-0.2.2.tgz", - "integrity": "sha1-5sgLYjEj19gM8TLOU480YokHJQI=", - "dev": true - }, - "state-toggle": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.1.tgz", - "integrity": "sha512-Qe8QntFrrpWTnHwvwj2FZTgv+PKIsp0B9VxLzLLbSpPXWOgRgc5LVj/aTiSfK1RqIeF9jeC1UeOH8Q8y60A7og==", - "dev": true - }, - "static-eval": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", - "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", - "requires": { - "escodegen": "^1.8.1" - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "static-module": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.3.tgz", - "integrity": "sha512-RDaMYaI5o/ym0GkCqL/PlD1Pn216omp8fY81okxZ6f6JQxWW5tptOw9reXoZX85yt/scYvbWIt6uoszeyf+/MQ==", - "dev": true, - "requires": { - "acorn-node": "^1.3.0", - "concat-stream": "~1.6.0", - "convert-source-map": "^1.5.1", - "duplexer2": "~0.1.4", - "escodegen": "~1.9.0", - "has": "^1.0.1", - "magic-string": "^0.22.4", - "merge-source-map": "1.0.4", - "object-inspect": "~1.4.0", - "readable-stream": "~2.3.3", - "scope-analyzer": "^2.0.1", - "shallow-copy": "~0.0.1", - "static-eval": "^2.0.2", - "through2": "~2.0.3" - }, - "dependencies": { - "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "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 + "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 + }, + "resolve-options": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve-options/-/resolve-options-1.1.0.tgz", + "integrity": "sha1-MrueOcBtZzONyTeMDW1gdFZq0TE=", + "dev": true, + "requires": { + "value-or-function": "^3.0.0" + } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", - "dev": true - }, - "merge-source-map": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", - "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", - "dev": true, - "requires": { - "source-map": "^0.5.6" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true - } - } + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "requires": { + "lowercase-keys": "^1.0.0" + } }, - "object-inspect": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", - "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==", - "dev": true + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rewiremock": { + "version": "3.14.6", + "resolved": "https://registry.npmjs.org/rewiremock/-/rewiremock-3.14.6.tgz", + "integrity": "sha512-hjpS7iQUTVVh/IHV4GE1ypg4IzlgVc34gxZBarwwVrKfnjlyqHJuQdsia6Ac7m4f4k/zxxA3tX285MOstdysRQ==", + "dev": true, + "requires": { + "babel-runtime": "^6.26.0", + "compare-module-exports": "^2.1.0", + "node-libs-browser": "^2.1.0", + "path-parse": "^1.0.5", + "wipe-node-cache": "^2.1.2", + "wipe-webpack-cache": "^2.1.0" + } }, - "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 + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } }, - "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" - } + "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": { + "hash-base": "^3.1.2", + "inherits": "^2.0.4" + }, + "dependencies": { + "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": { + "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 + } + } }, - "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 + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } }, - "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" - } - } - } - }, - "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=", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.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, - "requires": { - "duplexer": "~0.1.1" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - }, - "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" - } - } - } - }, - "stream-exhaust": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", - "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", - "dev": true - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", - "dev": true + "rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "requires": { + "tslib": "^1.9.0" + } }, - "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" - } + "rxjs-compat": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs-compat/-/rxjs-compat-6.6.7.tgz", + "integrity": "sha512-szN4fK+TqBPOFBcBcsR0g2cmTTUF/vaFEOZNuSdfU8/pGFnNmmn2u8SystYXG1QMrjOPBc6XTKHMVfENDf6hHw==" + }, + "safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + } + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "stream-shift": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", - "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", - "dev": true - }, - "streamfilter": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/streamfilter/-/streamfilter-1.0.7.tgz", - "integrity": "sha512-Gk6KZM+yNA1JpW0KzlZIhjo3EaBJDkYfXtYSbOwNIQ7Zd6006E6+sCFlW1NDvFG/vnXhKmw6TJJgiEQg/8lXfQ==", - "dev": true, - "requires": { - "readable-stream": "^2.0.2" - } - }, - "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 - }, - "string-hash": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/string-hash/-/string-hash-1.1.3.tgz", - "integrity": "sha1-6Kr8CsGFW0Zmkp7X3RJ1311sgRs=", - "dev": true - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "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 + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "requires": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + } }, - "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" - } - } - } - }, - "string.prototype.trim": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz", - "integrity": "sha1-0E3iyJ4Tf019IG8Ia17S+ua+jOo=", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.0", - "function-bind": "^1.0.2" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - } - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "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 - }, - "strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", - "dev": true, - "requires": { - "is-natural-number": "^4.0.1" - } - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", - "dev": true, - "requires": { - "get-stdin": "^4.0.1" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "style-loader": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-0.23.1.tgz", - "integrity": "sha512-XK+uv9kWwhZMZ1y7mysB+zoihsEj4wneFWAS5qoiLwzW0WzSqMrrsIy+a3zkQJq0ipFtBpX5W3MqyRIBF/WFGg==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "schema-utils": "^1.0.0" - } - }, - "styled-jsx": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-3.1.0.tgz", - "integrity": "sha512-drcLtuMC9wKhxZ5C7PyGxy9ADWfw7svB8zemWu+zpG8x4n/hih2xQU2U+SG6HF3TjV3tOjRrNIQOV8vUvffifA==", - "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 - } - } - }, - "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 - }, - "subarg": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz", - "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=", - "dev": true, - "requires": { - "minimist": "^1.1.0" - } - }, - "sudo-prompt": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/sudo-prompt/-/sudo-prompt-8.2.0.tgz", - "integrity": "sha512-n5Nv2lIZaWfVBg10EWC8yaJCB6xV7sEsuaISAVFIS9F4fTRjy/O35A82lkweKuSqQItDlKOGQpTHK9/udQhRRw==" - }, - "sumchecker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", - "integrity": "sha1-D0LBDl0F2l1C7qPlbDOZo31sWz4=", - "dev": true, - "requires": { - "debug": "^2.2.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" - }, - "sver-compat": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sver-compat/-/sver-compat-1.5.0.tgz", - "integrity": "sha1-PPh9/rTQe0o/FIJ7wYaz/QxkXNg=", - "dev": true, - "requires": { - "es6-iterator": "^2.0.1", - "es6-symbol": "^3.1.1" - } - }, - "svg-inline-loader": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/svg-inline-loader/-/svg-inline-loader-0.8.0.tgz", - "integrity": "sha512-rynplY2eXFrdNomL1FvyTFQlP+dx0WqbzHglmNtA9M4IHRC3no2aPAl3ny9lUpJzFzFMZfWRK5YIclNU+FRePA==", - "dev": true, - "requires": { - "loader-utils": "^0.2.11", - "object-assign": "^4.0.1", - "simple-html-tokenizer": "^0.1.1" - }, - "dependencies": { - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g=", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - } - } - }, - "svg-inline-react": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/svg-inline-react/-/svg-inline-react-3.1.0.tgz", - "integrity": "sha512-c39AIRQOUXLMD8fQ2rHmK1GOSO3tVuZk61bAXqIT05uhhm3z4VtQFITQSwyhL0WA2uxoJAIhPd2YV0CYQOolSA==", - "dev": true, - "requires": { - "prop-types": "^15.5.0" - } - }, - "svg-path-bounding-box": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/svg-path-bounding-box/-/svg-path-bounding-box-1.0.4.tgz", - "integrity": "sha1-7XPfODyLR4abZQjwWPV0j4gzwHA=", - "dev": true, - "requires": { - "svgpath": "^2.0.0" - } - }, - "svg-to-pdfkit": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.7.tgz", - "integrity": "sha1-fbbUfkeziI3OGAYHUajeJBf4V3U=", - "requires": { - "pdfkit": ">=0.8.1" - } - }, - "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 - }, - "tapable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.0.tgz", - "integrity": "sha512-IlqtmLVaZA2qab8epUXbVWRn3aB1imbDMJtjB3nu4X0NqPkcY/JH9ZtCBWKHWPxs8Svi9tyo8w2dBoi07qZbBA==", - "dev": true - }, - "tar": { - "version": "4.4.8", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.8.tgz", - "integrity": "sha512-LzHF64s5chPQQS0IYBn9IN5h3i98c12bo4NCO7e0sGM2llXQ3p2FGC5sdENN4cTW48O915Sh+x+EXx7XW96xYQ==", - "dev": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - }, - "dependencies": { - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true - } - } - }, - "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==", - "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" - }, - "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" - } + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "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 + "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": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } }, - "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" - } + "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_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" - } - } - } - }, - "teeny-request": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", - "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", - "dev": true, - "requires": { - "https-proxy-agent": "^2.2.1", - "node-fetch": "^2.2.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz", - "integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.2.3.tgz", - "integrity": "sha512-GOK7q85oAb/5kE12fMuLdn2btOS9OBZn4VsecpHDywoUC/jLhSAKOiYo0ezx7ss2EXPMzyEWFoE0s1WLE+4+oA==", - "dev": true, - "requires": { - "cacache": "^11.0.2", - "find-cache-dir": "^2.0.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^1.4.0", - "source-map": "^0.6.1", - "terser": "^3.16.1", - "webpack-sources": "^1.1.0", - "worker-farm": "^1.5.2" - }, - "dependencies": { - "bluebird": { - "version": "3.5.4", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz", - "integrity": "sha512-FG+nFEZChJrbQ9tIccIfZJBz3J7mLrAhxakAbnrJWn8d7aKOC+LWifa0G+p4ZqKp4y13T7juYvdhq9NzKdsrjw==", - "dev": true - }, - "cacache": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.3.2.tgz", - "integrity": "sha512-E0zP4EPGDOaT2chM08Als91eYnf8Z+eH1awwwVsngUmgppfM5jjJ8l3z5vO5p5w/I3LsiXawb1sW0VY65pQABg==", - "dev": true, - "requires": { - "bluebird": "^3.5.3", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.3", - "graceful-fs": "^4.1.15", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.2", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } + "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" + } }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "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": { + "sver": "^1.8.3" + } }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } + "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": { + "randombytes": "^2.1.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" - } + "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-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": { + "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" + } }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "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" - } + "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" + } }, - "graceful-fs": { - "version": "4.1.15", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", - "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==", - "dev": true + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", + "dev": true + }, + "sha.js": { + "version": "2.4.12", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz", + "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==", + "dev": true, + "requires": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1", + "to-buffer": "^1.2.0" + }, + "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 + } + } }, - "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" - } + "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" + } }, - "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" - } + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "dev": true, + "requires": { + "shebang-regex": "^1.0.0" + } }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } + "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 + }, + "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": { + "nanoid": "^3.3.8" + } }, - "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 + "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": { + "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" + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "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": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "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" + } + }, + "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": { + "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" + } + }, + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + }, + "dependencies": { + "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": { + "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 + } + } + }, + "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": { + "@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": { + "diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "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" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "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": { + "is-plain-obj": "^1.0.0" + } + }, + "sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "requires": { + "sort-keys": "^1.0.0" + }, + "dependencies": { + "sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "requires": { + "is-plain-obj": "^1.0.0" + } + } + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "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/spawn-wrap/-/spawn-wrap-2.0.0.tgz", + "integrity": "sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==", + "dev": true, + "requires": { + "foreground-child": "^2.0.0", + "is-windows": "^1.0.2", + "make-dir": "^3.0.0", + "rimraf": "^3.0.0", + "signal-exit": "^3.0.2", + "which": "^2.0.1" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, + "stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true + }, + "stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dev": true, + "requires": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "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": { + "streamx": "^2.13.2" + } + }, + "stream-exhaust": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stream-exhaust/-/stream-exhaust-1.0.2.tgz", + "integrity": "sha512-b/qaq/GlBK5xaq1yrK9/zFcyRSTNxmcZwFLGSTG0mXgZl/4Z6GgiyYOXOvY7N3eEvFRAG1bkDRz5EPGSvPYQlw==", + "dev": true + }, + "stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dev": true, + "requires": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } }, - "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" - } + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=", + "dev": true + }, + "streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "requires": { + "bare-events": "^2.2.0", + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } + "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 }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==", - "dev": true + "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": { + "safe-buffer": "~5.2.0" + }, + "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 + } + } }, - "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-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": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } }, - "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "ssri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", - "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } + "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": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } }, - "terser": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-3.17.0.tgz", - "integrity": "sha512-/FQzzPJmCpjAH9Xvk2paiWrFq+5M6aVOf+2KRbwhByISDX/EujxsK+BAvrhb6H+2rtrLCHK9N01wO014vrIwVQ==", - "dev": true, - "requires": { - "commander": "^2.19.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.10" - } + "string.prototype.matchall": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.7.tgz", + "integrity": "sha512-f48okCX7JiwVi1NXCVWcFnZgADDC/n2vePlQ/KUCNqCikLLilQvwjMO8+BHVKvgzH0JB0J9LEPgxOGT02RoETg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.1", + "get-intrinsic": "^1.1.1", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "regexp.prototype.flags": "^1.4.1", + "side-channel": "^1.0.4" + } }, - "yallist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", - "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==", - "dev": true - } - } - }, - "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", - "dev": true, - "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^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" - } + "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": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + } }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "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" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } + "string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } }, - "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" - } + "string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + } }, - "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 + "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" + } }, - "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" - } - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "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", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "dev": true, - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - } - } - }, - "text-hex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", - "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "throttleit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz", - "integrity": "sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", - "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==" + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "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" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "through2-filter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz", - "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=", - "dev": true, - "requires": { - "through2": "~2.0.0", - "xtend": "~4.0.0" - } - }, - "time-stamp": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/time-stamp/-/time-stamp-1.1.0.tgz", - "integrity": "sha1-dkpaEa9QVhkhsTPztE5hhofg9cM=", - "dev": true - }, - "timed-out": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", - "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true - }, - "timers-browserify": { - "version": "2.0.10", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.10.tgz", - "integrity": "sha512-YvC1SV1XdOUaL6gx5CoGroT3Gu49pK9+TZ38ErPldOWW4j49GI1HKs9DV+KGq/w6y+LZ72W1c8cKz2vzY+qpzg==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timers-ext": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.5.tgz", - "integrity": "sha512-tsEStd7kmACHENhsUPaxb8Jf8/+GZZxyNFQbZD07HQOyooOa6At1rQqjffgvg7n+dxscQa9cjjMdWhJtsP2sxg==", - "dev": true, - "requires": { - "es5-ext": "~0.10.14", - "next-tick": "1" - } - }, - "tiny-inflate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz", - "integrity": "sha1-k9nez/yIBb1X6uQxDwt0Xptvs6c=" - }, - "tinycolor2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", - "integrity": "sha1-9PrTM0R7wLB9TcjpIJ2POaisd+g=", - "dev": true - }, - "tinyqueue": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz", - "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==", - "dev": true - }, - "tmp": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", - "requires": { - "os-tmpdir": "~1.0.1" - } - }, - "to-absolute-glob": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", - "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", - "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "is-negated-glob": "^1.0.0" - } - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", - "dev": true - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "to-through": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", - "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", - "dev": true, - "requires": { - "through2": "^2.0.3" - } - }, - "toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha1-LmhELZ9k7HILjMieZEOsbKqVACk=", - "dev": true - }, - "touch": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/touch/-/touch-2.0.2.tgz", - "integrity": "sha512-qjNtvsFXTRq7IuMLweVgFxmEuQ6gLbRs2jQxL80TtZ31dEKWYIxRXquij6w6VimyDek5hD3PytljHmEtAs2u0A==", - "dev": true, - "requires": { - "nopt": "~1.0.10" - }, - "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" - } - } - } - }, - "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==", - "requires": { - "punycode": "^1.4.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - }, - "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 - } - } - }, - "transform-loader": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/transform-loader/-/transform-loader-0.2.4.tgz", - "integrity": "sha1-5ch4d7qW1R0/IlNoWHtG4ibRzsk=", - "dev": true, - "requires": { - "loader-utils": "^1.0.2" - } - }, - "transformation-matrix": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/transformation-matrix/-/transformation-matrix-2.0.3.tgz", - "integrity": "sha512-y81Pn2V6JlSDNiLPNqVRsfhRDVGY2xJqowHw4Ci/Wb643ejAPzWkqY10Q5JqWUspZfNqJKtz6KaBYcHBNwNBrQ==" - }, - "traverse-chain": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/traverse-chain/-/traverse-chain-0.1.0.tgz", - "integrity": "sha1-YdvC1Ttp/2CRoSoWj9fUMxB+QPE=", - "dev": true - }, - "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 - }, - "trim-newlines": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", - "dev": true - }, - "trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.2" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", - "dev": true - }, - "trim-trailing-lines": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.1.tgz", - "integrity": "sha512-bWLv9BbWbbd7mlqqs2oQYnLD/U/ZqeJeJwbO0FG2zA1aTq+HTvxfHNKFa/HGCVyJpDiioUYaBhfiT6rgk+l4mg==", - "dev": true - }, - "triple-beam": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", - "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" - }, - "trough": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.3.tgz", - "integrity": "sha512-fwkLWH+DimvA4YCy+/nvJd61nWQQ2liO/nF/RjkTpiOGi+zxZzVkhb1mvbHIIW4b/8nDsYI8uTmAlc0nNkRMOw==", - "dev": true - }, - "tryer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tryer/-/tryer-1.0.1.tgz", - "integrity": "sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA==", - "dev": true - }, - "ts-loader": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-5.3.0.tgz", - "integrity": "sha512-lGSNs7szRFj/rK9T1EQuayE3QNLg6izDUxt5jpmq0RG1rU2bapAt7E7uLckLCUPeO1jwxCiet2oRaWovc53UAg==", - "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" - } + "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 }, - "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" - } + "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 + }, + "strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.2" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "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": { - "has-flag": "^3.0.0" - } - } - } - }, - "ts-mockito": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/ts-mockito/-/ts-mockito-2.3.1.tgz", - "integrity": "sha512-chcKw0sTApwJxTyKhzbWxI4BTUJ6RStZKUVh2/mfwYqFS09PYy5pvdXZwG35QSkqT5pkdXZlYKBX196RRvEZdQ==", - "dev": true, - "requires": { - "lodash": "^4.17.5" - } - }, - "ts-node": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.3.0.tgz", - "integrity": "sha512-dyNS/RqyVTDcmNM4NIBAeDMpsAdaQ+ojdf0GOLqE6nwJOgzEkdRNzJywhDfwnuvB10oa6NLVG1rUJQCpRN7qoQ==", - "dev": true, - "requires": { - "arg": "^4.1.0", - "diff": "^4.0.1", - "make-error": "^1.1.1", - "source-map-support": "^0.5.6", - "yn": "^3.0.0" - }, - "dependencies": { - "diff": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz", - "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q==", - "dev": true - } - } - }, - "tsconfig-paths": { - "version": "3.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==", - "dev": true, - "requires": { - "@types/json5": "^0.0.29", - "deepmerge": "^2.0.1", - "json5": "^1.0.1", - "minimist": "^1.2.0", - "strip-bom": "^3.0.0" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "http://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - } - } - }, - "tsconfig-paths-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-S/gOOPOkV8rIL4LurZ1vUdYCVgo15iX9ZMJ6wx6w2OgcpT/G4wMyHB6WM+xheSqGMrWKuxFul+aXpCju3wmj/g==", - "dev": true, - "requires": { - "chalk": "^2.3.0", - "enhanced-resolve": "^4.0.0", - "tsconfig-paths": "^3.4.0" - }, - "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" - } + "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" + } }, - "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" - } + "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": { + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true + } + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "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": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + }, + "dependencies": { + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "optional": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "optional": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "optional": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "optional": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + } + } }, - "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" - } - } - } - }, - "tslib": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.1.tgz", - "integrity": "sha512-avfPS28HmGLLc2o4elcc2EIq2FcH++Yo5YxpBZi9Yw93BCTGFthI4HPE4Rpep6vSYQaK8e69PelM44tPj+RaQg==" - }, - "tslint": { - "version": "5.14.0", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.14.0.tgz", - "integrity": "sha512-IUla/ieHVnB8Le7LdQFRGlVJid2T/gaJe5VkjzRVSRR6pA2ODYrnfR1hmxi+5+au9l50jBwpbBL34txgv4NnTQ==", - "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", - "mkdirp": "^0.5.1", - "resolve": "^1.3.2", - "semver": "^5.3.0", - "tslib": "^1.8.0", - "tsutils": "^2.29.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==", - "requires": { - "color-convert": "^1.9.0" - } + "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" + } }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.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" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + "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": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "tsutils": { - "version": "2.29.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", - "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", - "requires": { - "tslib": "^1.8.1" - } - } - } - }, - "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==", - "dev": true, - "requires": { - "doctrine": "0.7.2", - "tslib": "1.9.0", - "tsutils": "2.8.0" - }, - "dependencies": { - "tslib": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.0.tgz", - "integrity": "sha512-f/qGG2tUkrISBlQZEjEqoZ3B2+npJjIf04H1wuAv9iA8i04Icp+61KRXxFdha22670NJopsZCIjhC3SnjPRKrQ==", - "dev": true - }, - "tsutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.8.0.tgz", - "integrity": "sha1-AWAXNymzvxOGKN0UoVN+AIUdgUo=", - "dev": true, - "requires": { - "tslib": "^1.7.1" - } - } - } - }, - "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==", - "dev": true, - "requires": { - "tsutils": "^2.12.1" - } - }, - "tsutils": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz", - "integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==", - "dev": true, - "requires": { - "tslib": "^1.8.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true - }, - "tunnel": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", - "integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tv4": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/tv4/-/tv4-1.3.0.tgz", - "integrity": "sha1-0CDIRvrdUMhVq7JeuuzGj8EPeWM=", - "dev": true - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "dependencies": { - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true + "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": { + "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" + } + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.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=", - "dev": true, - "requires": { - "@types/react": "^0.14.44" - }, - "dependencies": { - "@types/react": { - "version": "0.14.57", - "resolved": "http://registry.npmjs.org/@types/react/-/react-0.14.57.tgz", - "integrity": "sha1-GHioZU+v3R04G4RXKStkM0mMW2I=", - "dev": true - } - } - }, - "typed-rest-client": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-0.9.0.tgz", - "integrity": "sha1-92jMDcP06VDwbgSCXDaz54NKofI=", - "dev": true, - "requires": { - "tunnel": "0.0.4", - "underscore": "1.8.3" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" - }, - "typemoq": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", - "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", - "dev": true, - "requires": { - "circular-json": "^0.3.1", - "lodash": "^4.17.4", - "postinstall-build": "^5.0.1" - } - }, - "typescript": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.2.tgz", - "integrity": "sha512-7KxJovlYhTX5RaRbUdkAXN1KUZ8PwWlTzQdHV6xNqvuFOs7+WBo10TQUqT19Q/Jz2hk5v9TQDIhyLhhJY4p5AA==", - "dev": true - }, - "typescript-char": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/typescript-char/-/typescript-char-0.0.0.tgz", - "integrity": "sha1-VY/tpzfHZaYQtzfu+7F3Xum8jas=" - }, - "typescript-formatter": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/typescript-formatter/-/typescript-formatter-7.2.2.tgz", - "integrity": "sha512-V7vfI9XArVhriOTYHPzMU2WUnm5IMdu9X/CPxs8mIMGxmTBFpDABlbkBka64PZJ9/xgQeRpK8KzzAG4MPzxBDQ==", - "dev": true, - "requires": { - "commandpost": "^1.0.0", - "editorconfig": "^0.15.0" - } - }, - "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 - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", - "dev": true - }, - "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", - "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true, - "optional": true + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "dependencies": { + "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" + } + } + } }, - "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 - } - } - }, - "uint64be": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-1.0.1.tgz", - "integrity": "sha1-H3FUIC8qG4rzU4cd2mUb80zpPpU=" - }, - "unbzip2-stream": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.2.5.tgz", - "integrity": "sha512-izD3jxT8xkzwtXRUZjtmRwKnZoeECrfZ8ra/ketwOcusbZEp4mjULMnJOCfTDZBgGQAAY1AJ/IgxcwkavcX9Og==", - "dev": true, - "requires": { - "buffer": "^3.0.1", - "through": "^2.3.6" - } - }, - "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=", - "dev": true, - "requires": { - "arr-flatten": "^1.0.1", - "arr-map": "^2.0.0", - "bach": "^1.0.0", - "collection-map": "^1.0.0", - "es6-weak-map": "^2.0.1", - "last-run": "^1.1.0", - "object.defaults": "^1.0.0", - "object.reduce": "^1.0.0", - "undertaker-registry": "^1.0.0" - } - }, - "undertaker-registry": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-1.0.1.tgz", - "integrity": "sha1-XkvaMI5KiirlhPm5pDWaSZglzFA=", - "dev": true - }, - "unherit": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.1.tgz", - "integrity": "sha512-+XZuV691Cn4zHsK0vkKYwBEwB74T3IZIcxrgn2E4rKwTfFyI1zCh7X7grwh9Re08fdPlarIdyWgI8aVB3F5A5g==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "xtend": "^4.0.1" - } - }, - "unicode": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/unicode/-/unicode-10.0.0.tgz", - "integrity": "sha1-5dUcHbk7bHGguHngsMSvfm/faI4=" - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.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-properties": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.1.0.tgz", - "integrity": "sha1-epbu9J91aC6mnSMV7smsQ//fAME=", - "requires": { - "brfs": "^1.4.0", - "unicode-trie": "^0.3.0" - }, - "dependencies": { - "brfs": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/brfs/-/brfs-1.6.1.tgz", - "integrity": "sha512-OfZpABRQQf+Xsmju8XE9bDjs+uU4vLREGolP7bDgcpsI17QREyZ4Bl+2KLxxx1kCgA0fAIhKQBaBYh+PEcCqYQ==", - "requires": { - "quote-stream": "^1.0.1", - "resolve": "^1.1.5", - "static-module": "^2.2.0", - "through2": "^2.0.0" - } - }, - "escodegen": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.9.1.tgz", - "integrity": "sha512-6hTjO1NAWkHnDk3OqQ4YrCuwwmGHL9S3nPlzBOUG/R44rda3wLNrfvQ5fkSGjyhHFKM7ALPKcKGrwvCLe0lC7Q==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" + "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": { + "b4a": "^1.6.4" + } }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "merge-source-map": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", - "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", - "requires": { - "source-map": "^0.5.6" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - } - } + "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 }, - "object-inspect": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.4.1.tgz", - "integrity": "sha512-wqdhLpfCUbEsoEwl3FXwGyv8ief1k/1aUdIPCqVnupM6e8l63BEJdiF/0swtn04/8p05tG/T0FrpTlfwvljOdw==" + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dev": true, + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - }, - "static-module": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/static-module/-/static-module-2.2.5.tgz", - "integrity": "sha512-D8vv82E/Kpmz3TXHKG8PPsCPg+RAX6cbCOyvjM6x04qZtQ47EtJFVwRsdov3n5d6/6ynrOY9XB4JkaZwB2xoRQ==", - "requires": { - "concat-stream": "~1.6.0", - "convert-source-map": "^1.5.1", - "duplexer2": "~0.1.4", - "escodegen": "~1.9.0", - "falafel": "^2.1.0", - "has": "^1.0.1", - "magic-string": "^0.22.4", - "merge-source-map": "1.0.4", - "object-inspect": "~1.4.0", - "quote-stream": "~1.0.2", - "readable-stream": "~2.3.3", - "shallow-copy": "~0.0.1", - "static-eval": "^2.0.0", - "through2": "~2.0.3" - } + "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 + }, + "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": { + "setimmediate": "^1.0.4" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "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 - }, - "unicode-trie": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", - "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=", - "requires": { - "pako": "^0.2.5", - "tiny-inflate": "^1.0.0" - }, - "dependencies": { - "pako": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", - "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" - } - } - }, - "unified": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", - "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", - "dev": true, - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^2.0.0", - "x-is-string": "^0.1.0" - } - }, - "union-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", - "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", - "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=", - "dev": true, - "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=", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.1", - "to-object-path": "^0.3.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==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.1.tgz", - "integrity": "sha512-n9cU6+gITaVu7VGj1Z8feKMmfAjEAQGhwD9fE3zvpRRa0wEIx8ODYkVGfSc94M2OX00tUFV8wH3zYbm1I8mxFg==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "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" - } - }, - "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==", - "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==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", - "dev": true - }, - "unist-util-visit": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.0.tgz", - "integrity": "sha512-FiGu34ziNsZA3ZUteZxSFaczIjGmksfSgdKqBfOejrrfzyUy5b7YrlzT1Bcvi+djkYDituJDy2XB7tGTeBieKw==", - "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" - } - } - } - }, - "unist-util-visit-parents": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-1.1.2.tgz", - "integrity": "sha512-yvo+MMLjEwdc3RhhPYSximset7rwjMrdt9E41Smmvg25UQIenzrN83cRnF1JMzoMi9zZOQeYXHSDf7p+IQkW3Q==", - "dev": true - }, - "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=", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true - } - } - }, - "untildify": { - "version": "3.0.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==", - "dev": true - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=", - "dev": true - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "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=", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-join": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz", - "integrity": "sha1-dBxsL0WWxIMNZxhGCSDQySIC3Hg=", - "dev": true - }, - "url-loader": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", - "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "mime": "^2.0.3", - "schema-utils": "^1.0.0" - } - }, - "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-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 - }, - "use": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.0.tgz", - "integrity": "sha512-6UJEQM/L+mzC3ZJNM56Q4DFGLX/evKGRg15UJHGB9X5j5Z3AFbgZvjUh2yq/UJUY4U5dh7Fal++XbNg1uzpRAw==", - "dev": true, - "requires": { - "kind-of": "^6.0.2" - } - }, - "util": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", - "dev": true, - "requires": { - "inherits": "2.0.3" - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", - "dev": true - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "v8-compile-cache": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.2.tgz", - "integrity": "sha512-1wFuMUIM16MDJRCrpbpuEPTUGmM5QMUg0cr3KFwra2XgOgFcPGDQHDh3CszSCD2Zewc/dh/pamNEW8CbfDebUw==", - "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" - } - }, - "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": "2.0.2", - "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.0.2.tgz", - "integrity": "sha1-CjcT2NTpIhxY8QyhbAEWyeJe2nw=", - "dev": true, - "requires": { - "clone": "^1.0.0", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "is-stream": "^1.1.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "dependencies": { - "clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", - "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==" + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "dev": true, + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } }, - "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 - } - } - }, - "vinyl-fs": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", - "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", - "dev": true, - "requires": { - "fs-mkdirp-stream": "^1.0.0", - "glob-stream": "^6.1.0", - "graceful-fs": "^4.0.0", - "is-valid-glob": "^1.0.0", - "lazystream": "^1.0.0", - "lead": "^1.0.0", - "object.assign": "^4.0.4", - "pumpify": "^1.3.5", - "readable-stream": "^2.3.3", - "remove-bom-buffer": "^3.0.0", - "remove-bom-stream": "^1.2.0", - "resolve-options": "^1.1.0", - "through2": "^2.0.0", - "to-through": "^2.0.0", - "value-or-function": "^3.0.0", - "vinyl": "^2.0.0", - "vinyl-sourcemap": "^1.1.0" - }, - "dependencies": { - "clone": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", - "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", - "dev": true + "to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "dev": true + }, + "to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "requires": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "dependencies": { + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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 + "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" + } }, - "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 + "to-through": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-through/-/to-through-2.0.0.tgz", + "integrity": "sha1-/JKtq6ByZHvAtn1rA2ZKoZUJOvY=", + "dev": true, + "requires": { + "through2": "^2.0.3" + } }, - "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" - } + "totalist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", + "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", + "dev": true + }, + "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" + } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true + "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": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4" + }, + "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" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } }, - "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" - } + "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" + } }, - "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-sourcemap": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", - "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", - "dev": true, - "requires": { - "append-buffer": "^1.0.2", - "convert-source-map": "^1.5.0", - "graceful-fs": "^4.1.6", - "normalize-path": "^2.1.1", - "now-and-later": "^2.0.0", - "remove-bom-buffer": "^3.0.0", - "vinyl": "^2.0.0" - }, - "dependencies": { - "clone": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.1.tgz", - "integrity": "sha1-0hfR6WERjjrJpLi7oyhVU79kfNs=", - "dev": true + "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" + } }, - "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 + "tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "requires": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "requires": { + "minimist": "^1.2.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true + "tsconfig-paths-webpack-plugin": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-3.5.2.tgz", + "integrity": "sha512-EhnfjHbzm5IYI9YPNVIxx1moxMI4bpHD2e0zTXeDNQcwjjRaGepP7IhTHJkyDBG0CAOoxRfe7jCG630Ou+C6Pw==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.7.0", + "tsconfig-paths": "^3.9.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } }, - "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" - } - } - } - }, - "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==", - "dev": true - }, - "vlq": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/vlq/-/vlq-0.2.3.tgz", - "integrity": "sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow==" - }, - "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" - } - }, - "vsce": { - "version": "1.59.0", - "resolved": "https://registry.npmjs.org/vsce/-/vsce-1.59.0.tgz", - "integrity": "sha512-tkB97885k5ce25Brbe9AZTCAXAkBh7oa5EOzY0BCJQ51W/mfRaQuCluCd9gZpWdgiU4AbPvwxtoVKKsenlSt8w==", - "dev": true, - "requires": { - "chalk": "^2.4.2", - "cheerio": "^1.0.0-rc.1", - "commander": "^2.8.1", - "denodeify": "^1.2.1", - "glob": "^7.0.6", - "lodash": "^4.17.10", - "markdown-it": "^8.3.1", - "mime": "^1.3.4", - "minimatch": "^3.0.3", - "osenv": "^0.1.3", - "parse-semver": "^1.1.1", - "read": "^1.0.7", - "semver": "^5.1.0", - "tmp": "0.0.29", - "url-join": "^1.1.0", - "vso-node-api": "6.1.2-preview", - "yauzl": "^2.3.1", - "yazl": "^2.2.2" - }, - "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" - } + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" + }, + "tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", + "dev": true + }, + "tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "^5.0.1" + } }, - "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" - } + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, + "typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "requires": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + } }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true + "typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } }, - "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" - } - } - } - }, - "vscode": { - "version": "1.1.33", - "resolved": "https://registry.npmjs.org/vscode/-/vscode-1.1.33.tgz", - "integrity": "sha512-sXedp2oF6y4ZvqrrFiZpeMzaCLSWV+PpYkIxjG/iYquNZ9KrLL2LujltGxPLvzn49xu2sZkyC+avVNFgcJD1Iw==", - "dev": true, - "requires": { - "glob": "^7.1.2", - "mocha": "^4.0.1", - "request": "^2.88.0", - "semver": "^5.4.1", - "source-map-support": "^0.5.0", - "url-parse": "^1.4.4", - "vscode-test": "^0.1.4" - }, - "dependencies": { - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "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" - } - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true + "typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + } }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true + "typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "requires": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + } }, - "commander": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", - "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", - "dev": true + "typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "requires": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } }, - "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" - } + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } }, - "diff": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", - "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", - "dev": true + "typemoq": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typemoq/-/typemoq-2.1.0.tgz", + "integrity": "sha512-DtRNLb7x8yCTv/KHlwes+NI+aGb4Vl1iPC63Hhtcvk1DpxSAZzKWQv0RQFY0jX2Uqj0SDBNl8Na4e6MV6TNDgw==", + "dev": true, + "requires": { + "circular-json": "^0.3.1", + "lodash": "^4.17.4", + "postinstall-build": "^5.0.1" + } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true + }, + "uint64be": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/uint64be/-/uint64be-3.0.0.tgz", + "integrity": "sha512-mliiCSrsE29aNBI7O9W5gGv6WmA9kBR8PtTt6Apaxns076IRdYrrtFhXHEWMj5CSum3U7cv7/pi4xmi4XsIOqg==" + }, + "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": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.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 - }, - "growl": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", - "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", - "dev": true - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "dev": true, - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } + "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": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } }, - "has-flag": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", - "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", - "dev": true + "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 }, - "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 + "underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", + "dev": true + }, + "undertaker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-2.0.0.tgz", + "integrity": "sha512-tO/bf30wBbTsJ7go80j0RzA2rcwX6o7XPBpeFcb+jzoeb4pfMM2zUeSDIkY1AWqeZabWxaQZ/h8N9t35QKDLPQ==", + "dev": true, + "requires": { + "bach": "^2.0.1", + "fast-levenshtein": "^3.0.0", + "last-run": "^2.0.0", + "undertaker-registry": "^2.0.0" + }, + "dependencies": { + "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": { + "fastest-levenshtein": "^1.0.7" + } + } + } }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==", - "dev": true + "undertaker-registry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/undertaker-registry/-/undertaker-registry-2.0.0.tgz", + "integrity": "sha512-+hhVICbnp+rlzZMgxXenpvTxpuvA67Bfgtt+O9WOE5jo7w/dyiF1VmoZVIHvP2EkUjsyKyTwYKlLhA+j47m1Ew==", + "dev": true }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "dev": true, - "requires": { - "mime-db": "1.40.0" - } + "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 }, - "mocha": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-4.1.0.tgz", - "integrity": "sha512-0RVnjg1HJsXY2YFDoTNzcc1NKhYuXKRrBAG2gDygmJJA136Cs2QlRliZG1mA0ap7cuaT30mw16luAeln+4RiNA==", - "dev": true, - "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" - } - }, - "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 - }, - "querystringify": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz", - "integrity": "sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA==", - "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" - } + "unicode": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-14.0.0.tgz", + "integrity": "sha512-BjinxTXkbm9Jomp/YBTMGusr4fxIG67fNGShHIRAL16Ur2GJTq2xvLi+sxuiJmInCmwqqev2BCFKyvbfp/yAkg==" }, - "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, - "requires": { - "has-flag": "^2.0.0" - } - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "dev": true, - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - }, - "url-parse": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.4.7.tgz", - "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - } - } - }, - "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": "4.0.0", - "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-4.0.0.tgz", - "integrity": "sha512-perEnXQdQOJMTDFNv+UF3h1Y0z4iSiaN9jIlb0OqIYgosPCZGYh/MCUlkFtV2668PL69lRDO32hmvL2yiidUYg==" - }, - "vscode-languageclient": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-5.2.1.tgz", - "integrity": "sha512-7jrS/9WnV0ruqPamN1nE7qCxn0phkH5LjSgSp9h6qoJGoeAKzwKz/PF6M+iGA/aklx4GLZg1prddhEPQtuXI1Q==", - "requires": { - "semver": "^5.5.0", - "vscode-languageserver-protocol": "3.14.1" - } - }, - "vscode-languageserver": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-5.2.1.tgz", - "integrity": "sha512-GuayqdKZqAwwaCUjDvMTAVRPJOp/SLON3mJ07eGsx/Iq9HjRymhKWztX41rISqDKhHVVyFM+IywICyZDla6U3A==", - "requires": { - "vscode-languageserver-protocol": "3.14.1", - "vscode-uri": "^1.0.6" - }, - "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.14.1", - "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.14.1.tgz", - "integrity": "sha512-IL66BLb2g20uIKog5Y2dQ0IiigW0XKrvmWiOvc0yXw80z3tMEzEnHjaGAb3ENuU7MnQqgnYJ1Cl2l9RvNgDi4g==", - "requires": { - "vscode-jsonrpc": "^4.0.0", - "vscode-languageserver-types": "3.14.0" - } - }, - "vscode-languageserver-types": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.14.0.tgz", - "integrity": "sha512-lTmS6AlAlMHOvPQemVwo3CezxBp0sNB95KNPkqp3Nxd5VFEnuG1ByM0zlRWos0zjO3ZWtkvhal0COgiV1xIA4A==" - }, - "vscode-test": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-0.1.5.tgz", - "integrity": "sha512-s+lbF1Dtasc0yXVB9iQTexBe2JK6HJAUJe3fWezHKIjq+xRw5ZwCMEMBaonFIPy7s95qg2HPTRDR5W4h4kbxGw==", - "dev": true, - "requires": { - "http-proxy-agent": "^2.1.0", - "https-proxy-agent": "^2.2.1" - } - }, - "vscode-uri": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-1.0.1.tgz", - "integrity": "sha1-Eahr7+rDxKo+wIYjZRo8gabQu8g=" - }, - "vsls": { - "version": "0.3.1291", - "resolved": "https://registry.npmjs.org/vsls/-/vsls-0.3.1291.tgz", - "integrity": "sha512-8yJPN9p7k+XYyczOVtQmpun4K1CRDsw/hdnIzT/c40r5bIkpptfsBlHmmLemoIV+CAHvrTLdWKEf5OtRvdcn9A==" - }, - "vso-node-api": { - "version": "6.1.2-preview", - "resolved": "https://registry.npmjs.org/vso-node-api/-/vso-node-api-6.1.2-preview.tgz", - "integrity": "sha1-qrNUbfJFHs2JTgcbuZtd8Zxfp48=", - "dev": true, - "requires": { - "q": "^1.0.1", - "tunnel": "0.0.4", - "typed-rest-client": "^0.9.0", - "underscore": "^1.8.3" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "dev": true, - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, - "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" - } - }, - "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.33.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.33.0.tgz", - "integrity": "sha512-ggWMb0B2QUuYso6FPZKUohOgfm+Z0sVFs8WwWuSH1IAvkWs428VDNmOlAxvHGTB9Dm/qOB/qtE5cRx5y01clxw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.8.5", - "@webassemblyjs/helper-module-context": "1.8.5", - "@webassemblyjs/wasm-edit": "1.8.5", - "@webassemblyjs/wasm-parser": "1.8.5", - "acorn": "^6.0.5", - "acorn-dynamic-import": "^4.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": "^1.0.0", - "tapable": "^1.1.0", - "terser-webpack-plugin": "^1.1.0", - "watchpack": "^1.5.0", - "webpack-sources": "^1.3.0" - }, - "dependencies": { - "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true + "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" + } }, - "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", - "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" - } + "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" + } }, - "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 + "uri-js": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } }, - "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 - } - } - }, - "webpack-bundle-analyzer": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.3.2.tgz", - "integrity": "sha512-7qvJLPKB4rRWZGjVp5U1KEjwutbDHSKboAl0IfafnrdXMrgC0tOtZbQD6Rw0u4cmpgRN4O02Fc0t8eAT+FgGzA==", - "dev": true, - "requires": { - "acorn": "^6.0.7", - "acorn-walk": "^6.1.1", - "bfj": "^6.1.1", - "chalk": "^2.4.1", - "commander": "^2.18.0", - "ejs": "^2.6.1", - "express": "^4.16.3", - "filesize": "^3.6.1", - "gzip-size": "^5.0.0", - "lodash": "^4.17.10", - "mkdirp": "^0.5.1", - "opener": "^1.5.1", - "ws": "^6.0.0" - }, - "dependencies": { - "acorn": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", - "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", - "dev": true + "url": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", + "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", + "dev": true, + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", + "dev": true + } + } }, - "acorn-walk": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", - "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", - "dev": true + "url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, + "requires": { + "prepend-http": "^2.0.0" + } }, - "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" - } + "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 }, - "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" - } + "util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dev": true, + "requires": { + "inherits": "2.0.3" + }, + "dependencies": { + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + } + } }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "dev": true + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "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": "4.0.1", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-4.0.1.tgz", + "integrity": "sha512-fcRLaS4H/hrZk9hYwbdRM35D0U8IYMfEClhXxCivOojl+yTRAZH3Zy2sSy6qVCiGbV9YAtPssP6jaChqC9vPCg==", + "dev": true + }, + "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 }, - "filesize": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/filesize/-/filesize-3.6.1.tgz", - "integrity": "sha512-7KjR1vv6qnicaPMi1iiTcI85CyYwRO/PSFCu6SvqL8jN2Wjt/NIYQTFtFs7fSDCYOstUkEWIQGFUg5YZQfjlcg==", - "dev": true + "vinyl": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-2.2.1.tgz", + "integrity": "sha512-LII3bXRFBZLlezoG5FfZVcXflZgWP/4dCwKtxd5ky9+LOtM4CS3bIRQsmR1KMnMW07jpE8fqR2lcxPZ+8sJIcw==", + "dev": true, + "requires": { + "clone": "^2.1.1", + "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" + } }, - "gzip-size": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.0.tgz", - "integrity": "sha512-wfSnvypBDRW94v5W3ckvvz/zFUNdJ81VgOP6tE4bPpRUcc0wGqU+y0eZjJEvKxwubJFix6P84sE8M51YWLT7rQ==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" - } + "vinyl-contents": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/vinyl-contents/-/vinyl-contents-2.0.0.tgz", + "integrity": "sha512-cHq6NnGyi2pZ7xwdHSW1v4Jfnho4TEGtxZHw01cmnc8+i7jgR6bRnED/LbrKan/Q7CvVLbnvA5OepnhbpjBZ5Q==", + "dev": true, + "requires": { + "bl": "^5.0.0", + "vinyl": "^3.0.0" + }, + "dependencies": { + "bl": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", + "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "dev": true, + "requires": { + "buffer": "^6.0.3", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "replace-ext": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-2.0.0.tgz", + "integrity": "sha512-UszKE5KVK6JvyD92nzMn9cDapSk6w/CaFZ96CnmDMUqH9oowfxF/ZjRITD25H4DnOQClLA4/j7jLGXXLVKxAug==", + "dev": true + }, + "vinyl": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/vinyl/-/vinyl-3.0.0.tgz", + "integrity": "sha512-rC2VRfAVVCGEgjnxHUnpIVh3AGuk62rP3tqVrn+yab0YH7UULisC085+NYH+mnqf3Wx4SpSi1RQMwudL89N03g==", + "dev": true, + "requires": { + "clone": "^2.1.2", + "clone-stats": "^1.0.0", + "remove-trailing-separator": "^1.1.0", + "replace-ext": "^2.0.0", + "teex": "^1.0.1" + } + } + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "vinyl-fs": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/vinyl-fs/-/vinyl-fs-3.0.3.tgz", + "integrity": "sha512-vIu34EkyNyJxmP0jscNzWBSygh7VWhqun6RmqVfXePrOwi9lhvRs//dOaGOTRUQr4tx7/zd26Tk5WeSVZitgng==", + "dev": true, + "requires": { + "fs-mkdirp-stream": "^1.0.0", + "glob-stream": "^6.1.0", + "graceful-fs": "^4.0.0", + "is-valid-glob": "^1.0.0", + "lazystream": "^1.0.0", + "lead": "^1.0.0", + "object.assign": "^4.0.4", + "pumpify": "^1.3.5", + "readable-stream": "^2.3.3", + "remove-bom-buffer": "^3.0.0", + "remove-bom-stream": "^1.2.0", + "resolve-options": "^1.1.0", + "through2": "^2.0.0", + "to-through": "^2.0.0", + "value-or-function": "^3.0.0", + "vinyl": "^2.0.0", + "vinyl-sourcemap": "^1.1.0" + } }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true + "vinyl-sourcemap": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/vinyl-sourcemap/-/vinyl-sourcemap-1.1.0.tgz", + "integrity": "sha1-kqgAWTo4cDqM2xHYswCtS+Y7PhY=", + "dev": true, + "requires": { + "append-buffer": "^1.0.2", + "convert-source-map": "^1.5.0", + "graceful-fs": "^4.1.6", + "normalize-path": "^2.1.1", + "now-and-later": "^2.0.0", + "remove-bom-buffer": "^3.0.0", + "vinyl": "^2.0.0" + }, + "dependencies": { + "normalize-path": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", + "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", + "dev": true, + "requires": { + "remove-trailing-separator": "^1.0.1" + } + } + } }, - "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" - } + "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 }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "dev": true, - "requires": { - "async-limiter": "~1.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" - }, - "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" - } + "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-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": { + "minimatch": "^9.0.3", + "semver": "^7.6.0", + "vscode-languageserver-protocol": "3.17.6-next.10" + }, + "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" + } + }, + "minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "requires": { + "brace-expansion": "^2.0.2" + } + } + } }, - "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" - } + "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==", + "requires": { + "vscode-jsonrpc": "9.0.0-next.5", + "vscode-languageserver-types": "3.17.6-next.5" + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "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==" + }, + "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": { + "tas-client": "0.2.33" + } }, - "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-fix-default-import-plugin": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/webpack-fix-default-import-plugin/-/webpack-fix-default-import-plugin-1.0.3.tgz", - "integrity": "sha1-iCuOTRqpPEjLj9r4Rvx52G+C8U8=", - "dev": true - }, - "webpack-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" - } + "watchpack": { + "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" + } }, - "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" - } + "webpack": { + "version": "5.105.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.0.tgz", + "integrity": "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw==", + "dev": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.15.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.19.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.16", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.3" + }, + "dependencies": { + "ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + } + } + } }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "webpack-bundle-analyzer": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.5.0.tgz", + "integrity": "sha512-GUMZlM3SKwS8Z+CKeIFx7CVoHn3dXFcUAjT/dcZQQmfSZGvitPfMob2ipjai7ovFFqPvTqkEZ/leL4O0YOdAYQ==", + "dev": true, + "requires": { + "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": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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": "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" + } + } + } }, - "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==", - "dev": true, - "requires": { - "lodash": "^4.17.5" - } - }, - "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==", - "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=", - "dev": true, - "requires": { - "http-parser-js": ">=0.4.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", - "dev": true - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "dev": true, - "requires": { - "iconv-lite": "0.4.24" - }, - "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" - } - } - } - }, - "whatwg-fetch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz", - "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==", - "dev": true - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true - }, - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", - "dev": true - }, - "why-is-node-running": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.0.3.tgz", - "integrity": "sha512-XmzbFN2T859avcs5qAsiiK1iu0nUpSUXRgiGsoHPcNijxhIlp1bPQWQk6ANUljDWqBtAbIR2jF1HxR0y2l2kCA==", - "dev": true, - "requires": { - "stackback": "0.0.2" - } - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "winreg": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", - "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" - }, - "winston": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.2.1.tgz", - "integrity": "sha512-zU6vgnS9dAWCEKg/QYigd6cgMVVNwyTzKs81XZtTFuRwJOcDdBg7AU0mXVyNbs7O5RH2zdv+BdNZUlx7mXPuOw==", - "requires": { - "async": "^2.6.1", - "diagnostics": "^1.1.1", - "is-stream": "^1.1.0", - "logform": "^2.1.1", - "one-time": "0.0.4", - "readable-stream": "^3.1.1", - "stack-trace": "0.0.x", - "triple-beam": "^1.3.0", - "winston-transport": "^4.3.0" - }, - "dependencies": { - "async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", - "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", - "requires": { - "lodash": "^4.17.11" - } + "webpack-cli": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", + "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", + "dev": true, + "requires": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^1.1.1", + "@webpack-cli/info": "^1.4.1", + "@webpack-cli/serve": "^1.6.1", + "colorette": "^2.0.14", + "commander": "^7.0.0", + "execa": "^5.0.0", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^2.2.0", + "rechoir": "^0.7.0", + "webpack-merge": "^5.7.3" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true + }, + "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 + }, + "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": { + "resolve": "^1.9.0" + } + } + } }, - "readable-stream": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.3.0.tgz", - "integrity": "sha512-EsI+s3k3XsW+fU8fQACLN59ky34AZ14LoeVZpYwmZvldCFo0r0gnelwF2TcMjLor/BTL5aDJVBMkss0dthToPw==", - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } + "webpack-fix-default-import-plugin": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/webpack-fix-default-import-plugin/-/webpack-fix-default-import-plugin-1.0.3.tgz", + "integrity": "sha1-iCuOTRqpPEjLj9r4Rvx52G+C8U8=", + "dev": true + }, + "webpack-merge": { + "version": "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": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + } }, - "string_decoder": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.2.0.tgz", - "integrity": "sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "winston-transport": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.3.0.tgz", - "integrity": "sha512-B2wPuwUi3vhzn/51Uukcao4dIduEiPOcOt9HJ3QeaXgkJ5Z7UwpBzxS4ZGNHtrxrUvTwemsQiSys0ihOf8Mp1A==", - "requires": { - "readable-stream": "^2.3.6", - "triple-beam": "^1.2.0" - }, - "dependencies": { - "process-nextick-args": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", - "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + "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 + }, + "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": {} + }, + "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 + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "requires": { + "isexe": "^2.0.0" + } }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } + "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": { + "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" + } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "wipe-node-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.0.tgz", - "integrity": "sha512-Vdash0WV9Di/GeYW9FJrAZcPjGK4dO7M/Be/sJybguEgcM7As0uwLyvewZYqdlepoh7Rj4ZJKEdo8uX83PeNIw==", - "dev": true - }, - "wipe-webpack-cache": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wipe-webpack-cache/-/wipe-webpack-cache-2.1.0.tgz", - "integrity": "sha512-OXzQMGpA7MnQQ8AG+uMl5mWR2ezy6fw1+DMHY+wzYP1qkF1jrek87psLBmhZEj+er4efO/GD4R8jXWFierobaA==", - "dev": true, - "requires": { - "wipe-node-cache": "^2.1.0" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "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==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "http://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "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" - } + "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": { + "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" + } }, - "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" - } + "wildcard": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", + "dev": true + }, + "winreg": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.4.tgz", + "integrity": "sha1-ugZWKbepJRMOFXeRCM9UCZDpjRs=" + }, + "wipe-node-cache": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/wipe-node-cache/-/wipe-node-cache-2.1.2.tgz", + "integrity": "sha512-m7NXa8qSxBGMtdQilOu53ctMaIBXy93FOP04EC1Uf4bpsE+r+adfLKwIMIvGbABsznaSNxK/ErD4xXDyY5og9w==", + "dev": true + }, + "wipe-webpack-cache": { + "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" + } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write-file-atomic": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.3.tgz", - "integrity": "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.1.tgz", - "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", - "requires": { - "async-limiter": "~1.0.0" - } - }, - "wtfnode": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/wtfnode/-/wtfnode-0.8.0.tgz", - "integrity": "sha512-A5jm/0REykxUac1q4Q5kv+hDIiacvqVpwIoXzCQcRL7syeEKucVVOxyLLrt+jIiZoXfla3lnsxUw/cmWXIaGWA==", - "dev": true - }, - "x-is-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", - "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", - "dev": true - }, - "xml": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", - "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", - "dev": true - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true - }, - "xml2js": { - "version": "0.4.19", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", - "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~9.0.1" - } - }, - "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 - }, - "xregexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", - "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==", - "dev": true - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" - }, - "yargs": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.2.tgz", - "integrity": "sha512-e7SkEx6N6SIZ5c5H22RTZae61qtn3PYUE8JYbBFlK9sYmh3DMQ6E5ygtaG/2BW0JZi4WGgTR2IV5ChqlqrDGVQ==", - "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", - "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 || ^4.0.0", - "yargs-parser": "^10.1.0" - }, - "dependencies": { - "decamelize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", - "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", - "dev": true, - "requires": { - "xregexp": "4.0.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": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + } }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } + "workerpool": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-9.3.4.tgz", + "integrity": "sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==", + "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" - } + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "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 + } + } }, - "p-limit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", - "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "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 + } + } }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.1.tgz", + "integrity": "sha512-JPStrIyyVJ6oCSz/691fAjFtefZ6q+fP6tm+OS4Qw6o+TGQxNp1ziY2PgS+X/m0V8OWhZiO/m4xSj+Pr4RrZvw==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } }, - "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 + "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", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=", + "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==", - "dev": true, - "requires": { - "camelcase": "^4.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=", - "dev": true - } - } - }, - "yargs-unparser": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-1.5.0.tgz", - "integrity": "sha512-HK25qidFTCVuj/D1VfNiEndpLIeJN78aqgR23nL3y4N0U/91cOAzqfHlF8n2BvoNDcZmJKin3ddNSvOxSr8flw==", - "dev": true, - "requires": { - "flat": "^4.1.0", - "lodash": "^4.17.11", - "yargs": "^12.0.5" - }, - "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" - } + "xml2js": { + "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": "~11.0.0" + } }, - "flat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/flat/-/flat-4.1.0.tgz", - "integrity": "sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw==", - "dev": true, - "requires": { - "is-buffer": "~2.0.3" - } + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, - "is-buffer": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz", - "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw==", - "dev": true + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "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" - } + "y18n": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", + "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==", + "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 + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "dev": true, - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.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 || ^4.0.0", - "yargs-parser": "^11.1.1" - } + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "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/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } }, "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "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": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "dependencies": { + "camelcase": { + "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.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3" + } + }, + "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 } - } - }, - "yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha1-qBmB6nCleUYTOIPwKcWCGok1mn8=", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.0.1" - } - }, - "yazl": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", - "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", - "dev": true, - "requires": { - "buffer-crc32": "~0.2.3" - } - }, - "yn": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz", - "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg==", - "dev": true - }, - "zip-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-2.0.1.tgz", - "integrity": "sha512-c+eUhhkDpaK87G/py74wvWLtz2kzMPNCCkUApkun50ssE0oQliIQzWpTnwjB+MTKVIf2tGzIgHyqW/Y+W77ecQ==", - "dev": true, - "requires": { - "archiver-utils": "^2.0.0", - "compress-commons": "^1.2.0", - "readable-stream": "^2.0.0" - } - }, - "zone.js": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.7.6.tgz", - "integrity": "sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=" } - } } diff --git a/package.datascience-ui.dependencies.json b/package.datascience-ui.dependencies.json deleted file mode 100644 index f8c3b7be55a6..000000000000 --- a/package.datascience-ui.dependencies.json +++ /dev/null @@ -1,169 +0,0 @@ -[ - "@babel/runtime", - "@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", - "bootstrap-less", - "character-entities-legacy", - "character-reference-invalid", - "classnames", - "clsx", - "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", - "dom-helpers", - "emotion", - "entities", - "escape-carriage", - "extend", - "fast-plist", - "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", - "linear-layout-vector", - "lodash.clonedeep", - "lodash.curry", - "lodash.flatten", - "lodash.flow", - "lodash.get", - "lodash.set", - "lodash.uniq", - "lodash", - "lru-cache", - "martinez-polygon-clipping", - "markdown-escapes", - "material-colors", - "mdast-add-list-metadata", - "memoize-one", - "monaco-editor", - "monaco-editor-textmate", - "monaco-textmate", - "numeral", - "object-assign", - "onigasm", - "os-browserify", - "parse-entities", - "path-browserify", - "polygon-offset", - "process", - "prop-types", - "pseudomap", - "pure-color", - "react-annotation", - "react-base16-styling", - "react-color", - "react-data-grid", - "react-dom", - "react-hot-loader", - "react-json-tree", - "react-lifecycles-compat", - "react-markdown", - "react-svg-pan-zoom", - "react-svgmt", - "react-table-hoc-fixed-columns", - "react-table", - "react-virtualized", - "react", - "reactcss", - "remark-parse", - "repeat-string", - "roughjs-es5", - "schedule", - "semiotic-mark", - "semiotic", - "setimmediate", - "slickgrid", - "state-toggle", - "string-hash", - "style-loader", - "styled-jsx", - "stylis-rule-sheet", - "svg-inline-react", - "svg-path-bounding-box", - "svgpath", - "timers-browserify", - "tinycolor2", - "tinyqueue", - "transformation-matrix", - "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", - "util", - "uuid", - "vfile-location", - "vfile-message", - "vfile", - "viz-annotation", - "x-is-string", - "xtend", - "yallist" -] diff --git a/package.json b/package.json index 2108e4307968..2a27cddc0976 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.7.0-dev", - "languageServerVersion": "0.2.82", + "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,15 +40,16 @@ "bugs": { "url": "https://github.com/Microsoft/vscode-python/issues" }, - "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", + "qna": "https://github.com/microsoft/vscode-python/discussions/categories/q-a", "icon": "icon.png", "galleryBanner": { "color": "#1e415e", "theme": "dark" }, "engines": { - "vscode": "^1.33.0" + "vscode": "^1.95.0" }, + "enableTelemetry": false, "keywords": [ "python", "django", @@ -35,2136 +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.viewOutput", - "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.configureTests", - "onCommand:python.datascience.showhistorypane", - "onCommand:python.datascience.importnotebook", - "onCommand:python.datascience.selectjupyteruri", - "onCommand:python.datascience.exportfileasnotebook", - "onCommand:python.datascience.exportfileandoutputasnotebook", - "onCommand: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": [ - { - "language": "python", - "path": "./snippets/python.json" + "problemMatchers": [ + { + "name": "python", + "owner": "python", + "source": "python", + "fileLocation": "autoDetect", + "pattern": [ + { + "regexp": "^.*File \\\"([^\\\"]|.*)\\\", line (\\d+).*", + "file": 1, + "line": 2 + }, + { + "regexp": "^\\s*(.*)\\s*$" + }, + { + "regexp": "^\\s*(.*Error.*)$", + "message": 1 + } + ] } ], - "keybindings": [ - { - "command": "python.execSelectionInTerminal", - "key": "shift+enter", - "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && !python.datascience.ownsSelection" - }, - { - "command": "python.datascience.execSelectionInteractive", - "key": "shift+enter", - "when": "editorTextFocus && editorLangId == python && !findInputFocussed && !replaceInputFocussed && python.datascience.ownsSelection && python.datascience.featureenabled" - }, - { - "command": "python.datascience.runcurrentcelladvance", - "key": "shift+enter", - "when": "editorTextFocus && !editorHasSelection && python.datascience.hascodecells && python.datascience.featureenabled" + "walkthroughs": [ + { + "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.runcurrentcell", - "key": "ctrl+enter", - "when": "editorTextFocus && !editorHasSelection && 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" - }, - { - "command": "python.sortImports", - "title": "%python.command.python.sortImports.title%", - "category": "Python Refactor" - }, - { - "command": "python.startREPL", - "title": "%python.command.python.startREPL.title%", - "category": "Python" - }, + "breakpoints": [ { - "command": "python.createTerminal", - "title": "%python.command.python.createTerminal.title%", - "category": "Python" + "language": "html" }, { - "command": "python.buildWorkspaceSymbols", - "title": "%python.command.python.buildWorkspaceSymbols.title%", - "category": "Python" + "language": "jinja" }, { - "command": "python.openTestNodeInEditor", - "title": "Open", - "icon": { - "light": "resources/light/open-file.svg", - "dark": "resources/dark/open-file.svg" - } + "language": "python" }, { - "command": "python.runTestNode", - "title": "Run", - "icon": { - "light": "resources/light/start.svg", - "dark": "resources/dark/start.svg" - } + "language": "django-html" }, { - "command": "python.debugTestNode", - "title": "Debug", - "icon": { - "light": "resources/light/debug.svg", - "dark": "resources/dark/debug.svg" - } - }, + "language": "django-txt" + } + ], + "commands": [ { - "command": "python.runtests", - "title": "%python.command.python.runtests.title%", + "title": "%python.command.python.createNewFile.title%", + "shortTitle": "%python.menu.createNewFile.title%", "category": "Python", - "icon": { - "light": "resources/light/run-tests.svg", - "dark": "resources/dark/run-tests.svg" - } + "command": "python.createNewFile" }, { - "command": "python.debugtests", - "title": "%python.command.python.debugtests.title%", "category": "Python", - "icon": { - "light": "resources/light/debug.svg", - "dark": "resources/dark/debug.svg" - } - }, - { - "command": "python.execInTerminal", - "title": "%python.command.python.execInTerminal.title%", - "category": "Python" - }, - { - "command": "python.setInterpreter", - "title": "%python.command.python.setInterpreter.title%", - "category": "Python" - }, - { - "command": "python.updateSparkLibrary", - "title": "%python.command.python.updateSparkLibrary.title%", - "category": "Python" - }, - { - "command": "python.refactorExtractVariable", - "title": "%python.command.python.refactorExtractVariable.title%", - "category": "Python Refactor" - }, - { - "command": "python.refactorExtractMethod", - "title": "%python.command.python.refactorExtractMethod.title%", - "category": "Python Refactor" + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%" }, { - "command": "python.viewTestOutput", - "title": "%python.command.python.viewTestOutput.title%", "category": "Python", - "icon": { - "light": "resources/light/repl.svg", - "dark": "resources/dark/repl.svg" - } + "command": "python.analysis.restartLanguageServer", + "title": "%python.command.python.analysis.restartLanguageServer.title%" }, { - "command": "python.viewOutput", - "title": "%python.command.python.viewOutput.title%", "category": "Python", - "icon": { - "light": "resources/light/repl.svg", - "dark": "resources/dark/repl.svg" - } + "command": "python.clearCacheAndReload", + "title": "%python.command.python.clearCacheAndReload.title%" }, { - "command": "python.selectAndRunTestMethod", - "title": "%python.command.python.selectAndRunTestMethod.title%", - "category": "Python" + "category": "Python", + "command": "python.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.title%" }, { - "command": "python.selectAndDebugTestMethod", - "title": "%python.command.python.selectAndDebugTestMethod.title%", - "category": "Python" + "category": "Python", + "command": "python.configureTests", + "title": "%python.command.python.configureTests.title%" }, { - "command": "python.selectAndRunTestFile", - "title": "%python.command.python.selectAndRunTestFile.title%", - "category": "Python" + "category": "Python", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%" }, { - "command": "python.runCurrentTestFile", - "title": "%python.command.python.runCurrentTestFile.title%", - "category": "Python" + "category": "Python", + "command": "python.createEnvironment", + "title": "%python.command.python.createEnvironment.title%" }, { - "command": "python.runFailedTests", - "title": "%python.command.python.runFailedTests.title%", "category": "Python", - "icon": { - "light": "resources/light/run-failed-tests.svg", - "dark": "resources/dark/run-failed-tests.svg" - } + "command": "python.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%" }, { - "command": "python.discoverTests", - "title": "%python.command.python.discoverTests.title%", "category": "Python", - "icon": { - "light": "resources/light/refresh.svg", - "dark": "resources/dark/refresh.svg" - } + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%" }, { - "command": "python.discoveringTests", - "title": "%python.command.python.discoveringTests.title%", "category": "Python", - "icon": { - "light": "resources/light/discovering-tests.svg", - "dark": "resources/dark/discovering-tests.svg" - } + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminalIcon.title%" }, { - "command": "python.stopTests", - "title": "%python.command.python.stopTests.title%", "category": "Python", - "icon": { - "light": "resources/light/stop.svg", - "dark": "resources/dark/stop.svg" - } + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%" }, { - "command": "python.configureTests", - "title": "%python.command.python.configureTests.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.debugcell", - "title": "%python.command.python.datascience.debugcell.title%", - "category": "Python" - }, - { - "command": "python.datascience.runcurrentcelladvance", - "title": "%python.command.python.datascience.runcurrentcelladvance.title%", - "category": "Python" - }, - { - "command": "python.datascience.runcurrentcellandallbelow.palette", - "title": "%python.command.python.datascience.runcurrentcellandallbelow.palette.title%", - "category": "Python" - }, - { - "command": "python.datascience.runallcellsabove.palette", - "title": "%python.command.python.datascience.runallcellsabove.palette.title%", - "category": "Python" - }, - { - "command": "python.datascience.debugcurrentcell.palette", - "title": "%python.command.python.datascience.debugcurrentcell.palette.title%", - "category": "Python" - }, - { - "command": "python.datascience.execSelectionInteractive", - "title": "%python.command.python.datascience.execSelectionInteractive.title%", - "category": "Python" - }, - { - "command": "python.datascience.showhistorypane", - "title": "%python.command.python.datascience.showhistorypane.title%", - "category": "Python" - }, - { - "command": "python.datascience.runFileInteractive", - "title": "%python.command.python.datascience.runFileInteractive.title%", - "category": "Python" + "shortTitle": "%python.command.python.execSelectionInTerminal.shortTitle%" }, { - "command": "python.datascience.runallcells", - "title": "%python.command.python.datascience.runallcells.title%", - "category": "Python" - }, - { - "command": "python.datascience.runallcellsabove", - "title": "%python.command.python.datascience.runallcellsabove.title%", - "category": "Python" - }, - { - "command": "python.datascience.runcellandallbelow", - "title": "%python.command.python.datascience.runcellandallbelow.title%", - "category": "Python" - }, - { - "command": "python.datascience.runcell", - "title": "%python.command.python.datascience.runcell.title%", - "category": "Python" - }, - { - "command": "python.datascience.runtoline", - "title": "%python.command.python.datascience.runtoline.title%", - "category": "Python" - }, - { - "command": "python.datascience.runfromline", - "title": "%python.command.python.datascience.runfromline.title%", - "category": "Python" - }, - { - "command": "python.datascience.selectjupyteruri", - "title": "%python.command.python.datascience.selectjupyteruri.title%", "category": "Python", - "when": "python.datascience.featureenabled" - }, - { - "command": "python.datascience.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.execInREPL", + "title": "%python.command.python.execInREPL.title%" }, { - "command": "python.datascience.exportfileasnotebook", - "title": "%python.command.python.datascience.exportfileasnotebook.title%", - "category": "Python" - }, - { - "command": "python.datascience.exportfileandoutputasnotebook", - "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", - "category": "Python" - }, - { - "command": "python.datascience.undocells", - "title": "%python.command.python.datascience.undocells.title%", - "category": "Python" + "category": "Python", + "command": "python.reportIssue", + "title": "%python.command.python.reportIssue.title%" }, { - "command": "python.datascience.redocells", - "title": "%python.command.python.datascience.redocells.title%", - "category": "Python" + "category": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%" }, { - "command": "python.datascience.removeallcells", - "title": "%python.command.python.datascience.removeallcells.title%", - "category": "Python" + "category": "Python", + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%" }, { - "command": "python.datascience.interruptkernel", - "title": "%python.command.python.datascience.interruptkernel.title%", - "category": "Python" + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%" }, { - "command": "python.datascience.restartkernel", - "title": "%python.command.python.datascience.restartkernel.title%", - "category": "Python" + "category": "Python", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.title%" }, { - "command": "python.datascience.expandallcells", - "title": "%python.command.python.datascience.expandallcells.title%", - "category": "Python" + "category": "Python", + "command": "python.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%" }, { - "command": "python.datascience.collapseallcells", - "title": "%python.command.python.datascience.collapseallcells.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.addcellbelow", - "title": "%python.command.python.datascience.addcellbelow.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.runallcells", - "group": "Python2" + "python.useEnvironmentsExtension": { + "default": false, + "description": "%python.useEnvironmentsExtension.description%", + "scope": "machine-overridable", + "type": "boolean", + "tags": [ + "onExP", + "preview" + ] }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.runcurrentcell", - "group": "Python2" + "python.experiments.enabled": { + "default": true, + "description": "%python.experiments.enabled.description%", + "scope": "window", + "type": "boolean" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.runcurrentcelladvance", - "group": "Python2" + "python.experiments.optInto": { + "default": [], + "markdownDescription": "%python.experiments.optInto.description%", + "items": { + "enum": [ + "All", + "pythonSurveyNotification", + "pythonPromptNewToolsExt", + "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", + "pythonTestAdapter" + ], + "enumDescriptions": [ + "%python.experiments.All.description%", + "%python.experiments.pythonSurveyNotification.description%", + "%python.experiments.pythonPromptNewToolsExt.description%", + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", + "%python.experiments.pythonTestAdapter.description%" + ] + }, + "scope": "window", + "type": "array", + "uniqueItems": true }, - { - "command": "python.datascience.runFileInteractive", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.featureenabled" + "python.experiments.optOutFrom": { + "default": [], + "markdownDescription": "%python.experiments.optOutFrom.description%", + "items": { + "enum": [ + "All", + "pythonSurveyNotification", + "pythonPromptNewToolsExt", + "pythonTerminalEnvVarActivation", + "pythonDiscoveryUsingWorkers", + "pythonTestAdapter" + ], + "enumDescriptions": [ + "%python.experiments.All.description%", + "%python.experiments.pythonSurveyNotification.description%", + "%python.experiments.pythonPromptNewToolsExt.description%", + "%python.experiments.pythonTerminalEnvVarActivation.description%", + "%python.experiments.pythonDiscoveryUsingWorkers.description%", + "%python.experiments.pythonTestAdapter.description%" + ] + }, + "scope": "window", + "type": "array", + "uniqueItems": true }, - { - "command": "python.datascience.runfromline", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.ownsSelection && python.datascience.featureenabled" + "python.globalModuleInstallation": { + "default": false, + "description": "%python.globalModuleInstallation.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.runtoline", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.ownsSelection && python.datascience.featureenabled" + "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" }, - { - "command": "python.datascience.execSelectionInteractive", - "group": "Python2", - "when": "editorFocus && editorLangId == python && python.datascience.featureenabled && python.datascience.ownsSelection" + "python.interpreter.infoVisibility": { + "default": "onPythonRelated", + "description": "%python.interpreter.infoVisibility.description%", + "enum": [ + "never", + "onPythonRelated", + "always" + ], + "enumDescriptions": [ + "%python.interpreter.infoVisibility.never.description%", + "%python.interpreter.infoVisibility.onPythonRelated.description%", + "%python.interpreter.infoVisibility.always.description%" + ], + "scope": "machine", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && resourceLangId == jupyter && python.datascience.featureenabled", - "command": "python.datascience.importnotebook", - "group": "Python3@1" + "python.logging.level": { + "default": "error", + "deprecationMessage": "%python.logging.level.deprecation%", + "description": "%python.logging.level.description%", + "enum": [ + "debug", + "error", + "info", + "off", + "warn" + ], + "scope": "machine", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.exportfileasnotebook", - "group": "Python3@2" + "python.missingPackage.severity": { + "default": "Hint", + "description": "%python.missingPackage.severity.description%", + "enum": [ + "Error", + "Hint", + "Information", + "Warning" + ], + "scope": "resource", + "type": "string" }, - { - "when": "editorFocus && editorLangId == python && python.datascience.hascodecells && python.datascience.featureenabled", - "command": "python.datascience.exportfileandoutputasnotebook", - "group": "Python3@3" - } - ], - "explorer/context": [ - { - "when": "resourceLangId == python && !busyTests", - "command": "python.runtests", - "group": "Python" + "python.locator": { + "default": "js", + "description": "%python.locator.description%", + "enum": [ + "js", + "native" + ], + "tags": [ + "onExP", + "preview" + ], + "scope": "machine", + "type": "string" }, - { - "when": "resourceLangId == python && !busyTests", - "command": "python.debugtests", - "group": "Python" + "python.pipenvPath": { + "default": "pipenv", + "description": "%python.pipenvPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "resourceLangId == python", - "command": "python.execInTerminal", - "group": "Python" + "python.poetryPath": { + "default": "poetry", + "description": "%python.poetryPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "resourceLangId == python && python.datascience.featureenabled", - "command": "python.datascience.runFileInteractive", - "group": "Python2" + "python.pixiToolPath": { + "default": "pixi", + "description": "%python.pixiToolPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "when": "resourceLangId == jupyter", - "command": "python.datascience.importnotebook", - "group": "Python" - } - ], - "commandPalette": [ - { - "command": "python.viewOutput", - "title": "%python.command.python.viewOutput.title%", - "category": "Python" - }, - { - "command": "python.runTestNode", - "title": "Run", - "category": "Python", - "when": "config.noExists" - }, - { - "command": "python.discoveringTests", - "category": "Python", - "when": "config.noExists" - }, - { - "command": "python.stopTests", - "category": "Python", - "when": "config.noExists" - }, - { - "command": "python.debugTestNode", - "title": "Debug", - "category": "Python", - "when": "config.noExists" - }, - { - "command": "python.openTestNodeInEditor", - "title": "Open", - "category": "Python", - "when": "config.noExists" - }, - { - "command": "python.datascience.runcurrentcell", - "title": "%python.command.python.datascience.runcurrentcell.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.runcurrentcelladvance", - "title": "%python.command.python.datascience.runcurrentcelladvance.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.runcurrentcellandallbelow.palette", - "title": "%python.command.python.datascience.runcurrentcellandallbelow.palette.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.runallcellsabove.palette", - "title": "%python.command.python.datascience.runallcellsabove.palette.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.debugcurrentcell.palette", - "title": "%python.command.python.datascience.debugcurrentcell.palette.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.showhistorypane", - "title": "%python.command.python.datascience.showhistorypane.title%", - "category": "Python", - "when": "python.datascience.featureenabled" - }, - { - "command": "python.datascience.runallcells", - "title": "%python.command.python.datascience.runallcells.title%", - "category": "Python", - "when": "python.datascience.featureenabled" - }, - { - "command": "python.datascience.runcell", - "title": "%python.command.python.datascience.runcell.title%", - "category": "Python", - "when": "python.datascience.featureenabled" - }, - { - "command": "python.datascience.runFileInteractive", - "title": "%python.command.python.datascience.runFileInteractive.title%", - "category": "Python", - "when": "python.datascience.featureenabled" - }, - { - "command": "python.datascience.importnotebook", - "title": "%python.command.python.datascience.importnotebook.title%", - "category": "Python" - }, - { - "command": "python.datascience.exportfileasnotebook", - "title": "%python.command.python.datascience.exportfileasnotebook.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.exportfileandoutputasnotebook", - "title": "%python.command.python.datascience.exportfileandoutputasnotebook.title%", - "category": "Python", - "when": "python.datascience.hascodecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.undocells", - "title": "%python.command.python.datascience.undocells.title%", - "category": "Python", - "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.redocells", - "title": "%python.command.python.datascience.redocells.title%", - "category": "Python", - "when": "python.datascience.haveredoablecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.removeallcells", - "title": "%python.command.python.datascience.removeallcells.title%", - "category": "Python", - "when": "python.datascience.haveinteractivecells && python.datascience.featureenabled" - }, - { - "command": "python.datascience.interruptkernel", - "title": "%python.command.python.datascience.interruptkernel.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" - }, - { - "command": "python.datascience.restartkernel", - "title": "%python.command.python.datascience.restartkernel.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" - }, - { - "command": "python.datascience.expandallcells", - "title": "%python.command.python.datascience.expandallcells.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" - }, - { - "command": "python.datascience.collapseallcells", - "title": "%python.command.python.datascience.collapseallcells.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" - }, - { - "command": "python.datascience.exportoutputasnotebook", - "title": "%python.command.python.datascience.exportoutputasnotebook.title%", - "category": "Python", - "when": "python.datascience.haveinteractive && python.datascience.featureenabled" + "python.terminal.activateEnvInCurrentTerminal": { + "default": false, + "description": "%python.terminal.activateEnvInCurrentTerminal.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.runcellandallbelow", - "category": "Python", - "when": "config.noExists" + "python.terminal.activateEnvironment": { + "default": true, + "description": "%python.terminal.activateEnvironment.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.runallcellsabove", - "category": "Python", - "when": "config.noExists" + "python.terminal.executeInFileDir": { + "default": false, + "description": "%python.terminal.executeInFileDir.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.datascience.addcellbelow", - "title": "%python.command.python.datascience.addcellbelow.title%", - "category": "Python", - "when": "python.datascience.featureenabled" - } - ], - "view/title": [ - { - "command": "python.debugtests", - "when": "view == python_tests && !busyTests", - "group": "navigation@3" + "python.terminal.focusAfterLaunch": { + "default": false, + "description": "%python.terminal.focusAfterLaunch.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.runtests", - "when": "view == python_tests && !busyTests", - "group": "navigation@1" + "python.terminal.launchArgs": { + "default": [], + "description": "%python.terminal.launchArgs.description%", + "scope": "resource", + "type": "array" }, - { - "command": "python.stopTests", - "when": "view == python_tests && busyTests", - "group": "navigation@1" + "python.terminal.shellIntegration.enabled": { + "default": true, + "markdownDescription": "%python.terminal.shellIntegration.enabled.description%", + "scope": "resource", + "type": "boolean", + "tags": [ + "preview" + ] }, - { - "command": "python.discoverTests", - "when": "view == python_tests && !busyTests", - "group": "navigation@4" + "python.REPL.enableREPLSmartSend": { + "default": true, + "description": "%python.EnableREPLSmartSend.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.discoveringTests", - "when": "view == python_tests && discoveringTests", - "group": "navigation@4" + "python.REPL.sendToNativeREPL": { + "default": false, + "description": "%python.REPL.sendToNativeREPL.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.runFailedTests", - "when": "view == python_tests && hasFailedTests && !busyTests", - "group": "navigation@2" + "python.REPL.provideVariables": { + "default": true, + "description": "%python.REPL.provideVariables.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.viewTestOutput", - "when": "view == python_tests", - "group": "navigation@5" - } - ], - "view/item/context": [ - { - "command": "python.runtests", - "when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests", - "group": "inline@0" + "python.testing.autoTestDiscoverOnSaveEnabled": { + "default": true, + "description": "%python.testing.autoTestDiscoverOnSaveEnabled.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.debugtests", - "when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests", - "group": "inline@1" + "python.testing.autoTestDiscoverOnSavePattern": { + "default": "**/*.py", + "description": "%python.testing.autoTestDiscoverOnSavePattern.description%", + "scope": "resource", + "type": "string" }, - { - "command": "python.discoverTests", - "when": "view == python_tests && viewItem == testWorkspaceFolder && !busyTests", - "group": "inline@2" + "python.testing.cwd": { + "default": null, + "description": "%python.testing.cwd.description%", + "scope": "resource", + "type": "string" }, - { - "command": "python.openTestNodeInEditor", - "when": "view == python_tests && viewItem == testFunction", - "group": "inline@2" + "python.testing.debugPort": { + "default": 3000, + "description": "%python.testing.debugPort.description%", + "scope": "resource", + "type": "number" }, - { - "command": "python.debugTestNode", - "when": "view == python_tests && viewItem == testFunction && !busyTests", - "group": "inline@1" + "python.testing.promptToConfigure": { + "default": true, + "description": "%python.testing.promptToConfigure.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.runTestNode", - "when": "view == python_tests && viewItem == testFunction && !busyTests", - "group": "inline@0" + "python.testing.pytestArgs": { + "default": [], + "description": "%python.testing.pytestArgs.description%", + "items": { + "type": "string" + }, + "scope": "resource", + "type": "array" }, - { - "command": "python.openTestNodeInEditor", - "when": "view == python_tests && viewItem == testFile", - "group": "inline@2" + "python.testing.pytestEnabled": { + "default": false, + "description": "%python.testing.pytestEnabled.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.debugTestNode", - "when": "view == python_tests && viewItem == testFile && !busyTests", - "group": "inline@1" + "python.testing.pytestPath": { + "default": "pytest", + "description": "%python.testing.pytestPath.description%", + "scope": "machine-overridable", + "type": "string" }, - { - "command": "python.runTestNode", - "when": "view == python_tests && viewItem == testFile && !busyTests", - "group": "inline@0" + "python.testing.unittestArgs": { + "default": [ + "-v", + "-s", + ".", + "-p", + "*test*.py" + ], + "description": "%python.testing.unittestArgs.description%", + "items": { + "type": "string" + }, + "scope": "resource", + "type": "array" }, - { - "command": "python.openTestNodeInEditor", - "when": "view == python_tests && viewItem == testSuite", - "group": "inline@2" + "python.testing.unittestEnabled": { + "default": false, + "description": "%python.testing.unittestEnabled.description%", + "scope": "resource", + "type": "boolean" }, - { - "command": "python.debugTestNode", - "when": "view == python_tests && viewItem == testSuite && !busyTests", - "group": "inline@1" + "python.venvFolders": { + "default": [], + "description": "%python.venvFolders.description%", + "items": { + "type": "string" + }, + "scope": "machine", + "type": "array", + "uniqueItems": true }, - { - "command": "python.runTestNode", - "when": "view == python_tests && viewItem == testSuite && !busyTests", - "group": "inline@0" + "python.venvPath": { + "default": "", + "description": "%python.venvPath.description%", + "scope": "machine", + "type": "string" } - ] + }, + "title": "Python", + "type": "object" }, "debuggers": [ { - "type": "python", - "label": "Python", - "languages": [ - "python" - ], - "enableBreakpointsFor": { - "languageIds": [ - "python", - "html", - "jinja" - ] - }, - "aiKey": "AIF-d9b70cd4-b9f9-4d70-929b-a071c400b217", - "program": "./out/client/debugger/debugAdapter/main.js", - "runtime": "node", - "configurationSnippets": [], "configurationAttributes": { - "launch": { + "attach": { "properties": { - "module": { - "type": "string", - "description": "Name of the module to be debugged.", - "default": "" + "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" }, - "program": { - "type": "string", - "description": "Absolute path to the program.", - "default": "${file}" + "debugAdapterPath": { + "description": "Path (fully qualified) to the python debug adapter executable.", + "type": "string" }, - "pythonPath": { - "type": "string", - "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", - "default": "${config:python.pythonPath}" + "django": { + "default": false, + "description": "Django debugging.", + "type": "boolean" }, - "args": { - "type": "array", - "description": "Command line arguments passed to the program", - "default": [], - "items": { - "type": "string" - } + "host": { + "default": "127.0.0.1", + "description": "Hostname or IP address to connect to.", + "type": "string" }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": false + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] }, - "showReturnValue": { - "type": "boolean", - "description": "Show return value of functions when stepping.", - "default": true + "justMyCode": { + "default": true, + "description": "If true, show and debug only user-written code. If false, show and debug all code, including library calls.", + "type": "boolean" }, - "console": { - "enum": [ - "internalConsole", - "integratedTerminal", - "externalTerminal" + "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" ], - "description": "Where to launch the debug target: internal console, integrated terminal, or external terminal.", - "default": "integratedTerminal" - }, - "cwd": { - "type": "string", - "description": "Absolute path to the working directory of the program being debugged. Default is the root directory of the file (leave empty).", - "default": "${workspaceFolder}" + "type": "object" }, - "env": { - "type": "object", - "description": "Environment variables defined as a key value pair. Property ends up being the Environment Variable and the value of the property ends up being the value of the Env Variable.", - "default": {} + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" }, - "envFile": { - "type": "string", - "description": "Absolute path to a file containing environment variable definitions.", - "default": "${workspaceFolder}/.env" + "pathMappings": { + "default": [], + "items": { + "label": "Path mapping", + "properties": { + "localRoot": { + "default": "${workspaceFolder}", + "label": "Local source root.", + "type": "string" + }, + "remoteRoot": { + "default": "", + "label": "Remote source root.", + "type": "string" + } + }, + "required": [ + "localRoot", + "remoteRoot" + ], + "type": "object" + }, + "label": "Path mappings.", + "type": "array" }, "port": { - "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" + "description": "Port to connect to.", + "type": "number" }, - "logToFile": { - "type": "boolean", - "description": "Enable logging of debugger events to a log file.", - "default": false + "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": { - "type": "boolean", + "default": true, "description": "Redirect output.", - "default": true + "type": "boolean" }, - "justMyCode": { - "type": "boolean", - "description": "Debug only user-written code.", - "default": true - }, - "gevent": { - "type": "boolean", - "description": "Enable debugging of gevent monkey-patched code.", - "default": false - }, - "django": { - "type": "boolean", - "description": "Django debugging.", - "default": false - }, - "jinja": { - "enum": [ - true, - false, - null - ], - "description": "Jinja template debugging (e.g. Flask).", - "default": null - }, - "sudo": { - "type": "boolean", - "description": "Running debug program under elevated permissions (on Unix).", - "default": false - }, - "pyramid": { - "type": "boolean", - "description": "Whether debugging Pyramid applications", - "default": false + "showReturnValue": { + "default": true, + "description": "Show return value of functions when stepping.", + "type": "boolean" }, "subProcess": { - "type": "boolean", + "default": false, "description": "Whether to enable Sub Process debugging", - "default": false + "type": "boolean" } } }, - "test": { + "launch": { "properties": { - "pythonPath": { - "type": "string", - "description": "Path (fully qualified) to python executable. Defaults to the value in settings.json", - "default": "${config:python.pythonPath}" - }, - "stopOnEntry": { - "type": "boolean", - "description": "Automatically stop after launch.", - "default": false + "args": { + "default": [], + "description": "Command line arguments passed to the program.", + "items": { + "type": "string" + }, + "type": [ + "array", + "string" + ] }, - "showReturnValue": { - "type": "boolean", - "description": "Show return value of functions when stepping.", - "default": true + "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": "none" + "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" + "type": "string" }, - "redirectOutput": { - "type": "boolean", - "description": "Redirect output.", - "default": true + "gevent": { + "default": false, + "description": "Enable debugging of gevent monkey-patched code.", + "type": "boolean" + }, + "host": { + "default": "localhost", + "description": "IP address of the of the local debug server (default is localhost).", + "type": "string" + }, + "jinja": { + "default": null, + "description": "Jinja template debugging (e.g. Flask).", + "enum": [ + false, + null, + true + ] }, "justMyCode": { - "type": "boolean", + "default": true, "description": "Debug only user-written code.", - "default": true - } - } - }, - "attach": { - "required": [ - "port" - ], - "properties": { - "port": { - "type": "number", - "description": "Debug port to attach", - "default": 0 + "type": "boolean" }, - "host": { - "type": "string", - "description": "IP Address of the of remote server (default is localhost or use 127.0.0.1).", - "default": "localhost" + "logToFile": { + "default": false, + "description": "Enable logging of debugger events to a log file.", + "type": "boolean" + }, + "module": { + "default": "", + "description": "Name of the module to be debugged.", + "type": "string" }, "pathMappings": { - "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" + }, + "label": "Path mappings.", + "type": "array" + }, + "port": { + "default": 0, + "description": "Debug port (default is 0, resulting in the use of a dynamic port).", + "type": "number" + }, + "program": { + "default": "${file}", + "description": "Absolute path to the program.", + "type": "string" + }, + "purpose": { + "default": [], + "description": "Tells extension to use this configuration for test debugging, or when using debug-in-terminal command.", + "items": { + "enum": [ + "debug-test", + "debug-in-terminal" + ], + "enumDescriptions": [ + "Use this configuration while debugging tests using test view or test debug commands.", + "Use this configuration while debugging a file using debug in terminal button in the editor." + ] }, - "default": [] + "type": "array" }, - "logToFile": { - "type": "boolean", - "description": "Enable logging of debugger events to a log file.", - "default": false + "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 - }, - "justMyCode": { - "type": "boolean", - "description": "Debug only user-written code.", - "default": true + "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" }, - "showReturnValue": { - "type": "boolean", - "description": "Show return value of functions when stepping.", - "default": true + "sudo": { + "default": false, + "description": "Running debug program under elevated permissions (on Unix).", + "type": "boolean" } } } - } - } - ], - "configuration": { - "type": "object", - "title": "Python", - "properties": { - "python.diagnostics.sourceMapsEnabled": { - "type": "boolean", - "default": false, - "description": "Enable source map support for meaningful stack traces in error logs.", - "scope": "application" - }, - "python.autoComplete.addBrackets": { - "type": "boolean", - "default": false, - "description": "Automatically add brackets for functions.", - "scope": "resource" - }, - "python.autoComplete.extraPaths": { - "type": "array", - "default": [], - "description": "List of paths to libraries and the like that need to be imported by auto complete engine. E.g. when using Google App SDK, the paths are not in system path, hence need to be added into this list.", - "scope": "resource" - }, - "python.autoComplete.showAdvancedMembers": { - "type": "boolean", - "default": true, - "description": "Controls appearance of methods with double underscores in the completion list.", - "scope": "resource" - }, - "python.autoComplete.typeshedPaths": { - "type": "array", - "items": { - "type": "string" - }, - "default": [], - "description": "Specifies paths to local typeshed repository clone(s) for the Python language server.", - "scope": "resource" - }, - "python.autoUpdateLanguageServer": { - "type": "boolean", - "default": true, - "description": "Automatically update the language server.", - "scope": "application" - }, - "python.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.askForLargeDataFrames": { - "type": "boolean", - "default": true, - "description": "Warn the user before trying to open really large data frames.", - "scope": "application" - }, - "python.dataScience.askForKernelRestart": { - "type": "boolean", - "default": true, - "description": "Warn the user before restarting a kernel.", - "scope": "application" - }, - "python.dataScience.enabled": { - "type": "boolean", - "default": true, - "description": "Enable the experimental data science features in the python extension.", - "scope": "resource" - }, - "python.dataScience.exportWithOutputEnabled": { - "type": "boolean", - "default": false, - "description": "Enable exporting a python file into a jupyter notebook and run all cells when doing so.", - "scope": "resource" - }, - "python.dataScience.jupyterLaunchTimeout": { - "type": "number", - "default": 60000, - "description": "Amount of time (in ms) to wait for the Jupyter Notebook server to start.", - "scope": "resource" - }, - "python.dataScience.jupyterLaunchRetries": { - "type": "number", - "default": 3, - "description": "Number of times to attempt to connect to the Jupyter Notebook", - "scope": "resource" - }, - "python.dataScience.jupyterServerURI": { - "type": "string", - "default": "local", - "description": "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.dataScience.allowInput": { - "type": "boolean", - "default": true, - "description": "Allow the inputting of python code directly into the Python Interactive window" - }, - "python.dataScience.showCellInputCode": { - "type": "boolean", - "default": true, - "description": "Show cell input code.", - "scope": "resource" - }, - "python.dataScience.collapseCellInputCodeByDefault": { - "type": "boolean", - "default": true, - "description": "Collapse cell input code by default.", - "scope": "resource" - }, - "python.dataScience.maxOutputSize": { - "type": "number", - "default": 400, - "description": "Maximum size (in pixels) of text output in the Python Interactive window before a scrollbar appears. Set to -1 for infinity.", - "scope": "resource" - }, - "python.dataScience.errorBackgroundColor": { - "type": "string", - "default": "#FFFFFF", - "description": "Background color (in hex) for exception messages in the Python Interactive window.", - "scope": "resource", - "deprecationMessage": "No longer necessary as the theme colors are used for error messages" - }, - "python.dataScience.sendSelectionToInteractiveWindow": { - "type": "boolean", - "default": false, - "description": "Determines if selected code in a python file will go to the terminal or the Python Interactive window when hitting shift+enter", - "scope": "resource" - }, - "python.dataScience.showJupyterVariableExplorer": { - "type": "boolean", - "default": true, - "description": "Show the variable explorer in the Python Interactive window.", - "scope": "resource" - }, - "python.dataScience.variableExplorerExclude": { - "type": "string", - "default": "module;function;builtin_function_or_method", - "description": "Types to exclude from showing in the Python Interactive variable explorer", - "scope": "resource" - }, - "python.dataScience.codeRegularExpression": { - "type": "string", - "default": "^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])", - "description": "Regular expression used to identify code cells. All code until the next match is considered part of this cell. \nDefaults to '^(#\\s*%%|#\\s*\\|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])' if left blank", - "scope": "resource" - }, - "python.dataScience.markdownRegularExpression": { - "type": "string", - "default": "^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)", - "description": "Regular expression used to identify markdown cells. All comments after this expression are considered part of the markdown. \nDefaults to '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\)' if left blank", - "scope": "resource" - }, - "python.dataScience.allowLiveShare": { - "type": "boolean", - "default": true, - "description": "Allow the Python Interactive window to be shared during a Live Share session", - "scope": "resource" - }, - "python.dataScience.ignoreVscodeTheme": { - "type": "boolean", - "default": false, - "description": "Don't use the VS Code theme in the Python Interactive window (requires reload of VS Code). This forces the Python Interactive window to use 'Light +(default light)' and disables matplotlib defaults.", - "scope": "resource" - }, - "python.dataScience.liveShareConnectionTimeout": { - "type": "number", - "default": 1000, - "description": "Amount of time to wait for guest connections to verify they have the Python extension installed.", - "scope": "application" - }, - "python.dataScience.decorateCells": { - "type": "boolean", - "default": true, - "description": "Draw a highlight behind the currently active cell.", - "scope": "resource" - }, - "python.dataScience.enableCellCodeLens": { - "type": "boolean", - "default": true, - "description": "Enables code lens for 'cells' in a python file.", - "scope": "resource" - }, - "python.dataScience.enableAutoMoveToNextCell": { - "type": "boolean", - "default": true, - "description": "Enables moving to the next cell when clicking on a 'Run Cell' code lens.", - "scope": "resource" - }, - "python.dataScience.autoPreviewNotebooksInInteractivePane": { - "type": "boolean", - "default": false, - "description": "When opening ipynb files, automatically preview the contents in the Python Interactive window.", - "scope": "resource" - }, - "python.dataScience.allowUnauthorizedRemoteConnection": { - "type": "boolean", - "default": false, - "description": "Allow for connecting the Python Interactive window to a https Jupyter server that does not have valid certificates. This can be a security risk, so only use for known and trusted servers.", - "scope": "resource" - }, - "python.dataScience.enablePlotViewer": { - "type": "boolean", - "default": true, - "description": "Modify plot output so that it can be expanded into a plot viewer window.", - "scope": "resource" }, - "python.dataScience.codeLenses": { - "type": "string", - "default": "python.datascience.runcell, python.datascience.runallcellsabove, python.datascience.debugcell", - "description": "Set of commands to put as code lens above a cell. Defaults to 'python.datascience.runcell, python.datascience.runallcellsabove, python.datascience.debugcell'", - "scope": "resource" - }, - "python.dataScience.ptvsdDistPath": { - "type": "string", - "default": "", - "description": "Path to ptsvd experimental bits for debugging cells.", - "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" + "deprecated": "%python.debugger.deprecatedMessage%", + "configurationSnippets": [], + "label": "Python", + "languages": [ + "python" + ], + "type": "python", + "variables": { + "pickProcess": "python.pickLocalProcess" }, - "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.errors": { - "type": "array", - "default": [], - "items": { - "type": "string" - }, - "description": "List of diagnostics messages to be shown as errors.", - "scope": "resource" - }, - "python.analysis.warnings": { - "type": "array", - "default": [], - "items": { - "type": "string" - }, - "description": "List of diagnostics messages to be shown as warnings.", - "scope": "resource" - }, - "python.analysis.information": { - "type": "array", - "default": [], - "items": { - "type": "string" - }, - "description": "List of diagnostics messages to be shown as information.", - "scope": "resource" - }, - "python.analysis.disabled": { - "type": "array", - "default": [], - "items": { - "type": "string" - }, - "description": "List of suppressed diagnostic messages.", - "scope": "resource" - }, - "python.analysis.typeshedPaths": { - "type": "array", - "default": [], - "items": { - "type": "string" - }, - "description": "Paths to Typeshed stub folders. Default is Typeshed installed with the language server. Change requires restart.", - "scope": "resource" - }, - "python.analysis.cacheFolderPath": { - "type": "string", - "description": "Path to a writable folder where analyzer can cache its data. Change requires restart.", - "scope": "resource" - }, - "python.analysis.memory.keepLibraryAst": { - "type": "boolean", - "default": false, - "description": "Allows code analysis to keep parser trees in memory. Increases memory consumption but may improve performance with large library analysis.", - "scope": "resource" - }, - "python.analysis.memory.keepLibraryLocalVariables": { - "type": "boolean", - "default": false, - "description": "Allows code analysis to keep library function local variables. Allows code navigation in Python libraries function bodies. Increases memory consumption.", - "scope": "resource" - }, - "python.analysis.logLevel": { - "type": "string", - "enum": [ - "Error", - "Warning", - "Information", - "Trace" - ], - "default": "Error", - "description": "Defines type of log messages language server writes into the output window.", - "scope": "resource" - }, - "python.analysis.symbolsHierarchyDepthLimit": { - "type": "integer", - "default": 10, - "description": "Limits depth of the symbol tree in the document outline.", - "scope": "resource" - }, - "python.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" - }, - "python.pythonPath": { - "type": "string", - "default": "python", - "description": "Path to Python, you can use a custom version of Python by modifying this setting to include the full path.", - "scope": "resource" - }, - "python.condaPath": { - "type": "string", - "default": "", - "description": "Path to the conda executable to use for activation (version 4.4+).", - "scope": "resource" - }, - "python.pipenvPath": { - "type": "string", - "default": "pipenv", - "description": "Path to the pipenv executable to use for activation.", - "scope": "resource" - }, - "python.poetryPath": { - "type": "string", - "default": "poetry", - "description": "Path to the poetry executable.", - "scope": "resource" + "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.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.clearCacheAndReload", + "title": "%python.command.python.clearCacheAndReload.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.clearWorkspaceInterpreter", + "title": "%python.command.python.clearWorkspaceInterpreter.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.configureTests", + "title": "%python.command.python.configureTests.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", + "title": "%python.command.python.createEnvironment.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "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.createEnvironment-button", + "title": "%python.command.python.createEnvironment.title%", + "when": "false" }, - "python.testing.cwd": { - "type": "string", - "default": null, - "description": "Optional working directory for tests.", - "scope": "resource" + { + "category": "Python", + "command": "python.createTerminal", + "title": "%python.command.python.createTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.debugPort": { - "type": "number", - "default": 3000, - "description": "Port number used for debugging of tests.", - "scope": "resource" + { + "category": "Python", + "command": "python.execInTerminal", + "title": "%python.command.python.execInTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.testing.nosetestArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" + { + "category": "Python", + "command": "python.execInTerminal-icon", + "icon": "$(play)", + "title": "%python.command.python.execInTerminalIcon.title%", + "when": "false" }, - "python.testing.nosetestsEnabled": { - "type": "boolean", - "default": false, - "description": "Enable testing using nosetests.", - "scope": "resource" + { + "category": "Python", + "command": "python.execInDedicatedTerminal", + "icon": "$(play)", + "title": "%python.command.python.execInDedicatedTerminal.title%", + "when": "false" }, - "python.testing.nosetestPath": { - "type": "string", - "default": "nosetests", - "description": "Path to nosetests, you can use a custom version of nosetests by modifying this setting to include the full path.", - "scope": "resource" + { + "category": "Python", + "command": "python.execSelectionInDjangoShell", + "title": "%python.command.python.execSelectionInDjangoShell.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.testing.promptToConfigure": { - "type": "boolean", - "default": true, - "description": "Prompt to configure a test framework if potential tests directories are discovered.", - "scope": "resource" + { + "category": "Python", + "command": "python.execSelectionInTerminal", + "title": "%python.command.python.execSelectionInTerminal.title%", + "when": "!virtualWorkspace && shellExecutionSupported && editorLangId == python" }, - "python.testing.pytestArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [], - "items": { - "type": "string" - }, - "scope": "resource" + { + "category": "Python", + "command": "python.copyTestId", + "title": "%python.command.python.testing.copyTestId.title%", + "when": "false" }, - "python.testing.pytestEnabled": { - "type": "boolean", - "default": false, - "description": "Enable testing using pytest.", - "scope": "resource" + { + "category": "Python", + "command": "python.execInREPL", + "title": "%python.command.python.execInREPL.title%", + "when": "false" }, - "python.testing.pytestPath": { - "type": "string", - "default": "pytest", - "description": "Path to pytest (pytest), you can use a custom version of pytest by modifying this setting to include the full path.", - "scope": "resource" + { + "category": "Python", + "command": "python.reportIssue", + "title": "%python.command.python.reportIssue.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.unittestArgs": { - "type": "array", - "description": "Arguments passed in. Each argument is a separate item in the array.", - "default": [ - "-v", - "-s", - ".", - "-p", - "*test*.py" - ], - "items": { - "type": "string" - }, - "scope": "resource" + { + "category": "Test", + "command": "testing.reRunFailTests", + "icon": "$(run-errors)", + "title": "%python.command.testing.rerunFailedTests.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.unittestEnabled": { - "type": "boolean", - "default": false, - "description": "Enable testing using unittest.", - "scope": "resource" + { + "category": "Python", + "command": "python.setInterpreter", + "title": "%python.command.python.setInterpreter.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.testing.autoTestDiscoverOnSaveEnabled": { - "type": "boolean", - "default": true, - "description": "Enable auto run test discovery when saving a test file.", - "scope": "resource" + { + "category": "Python", + "command": "python.startREPL", + "title": "%python.command.python.startTerminalREPL.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.venvFolders": { - "type": "array", - "default": [], - "description": "Folders in your home directory to look into for virtual environments (supports pyenv, direnv and virtualenvwrapper by default).", - "scope": "resource", - "items": { - "type": "string" - } + { + "category": "Python", + "command": "python.startNativeREPL", + "title": "%python.command.python.startNativeREPL.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.viewLanguageServerOutput", + "enablement": "python.hasLanguageServerOutputChannel", + "title": "%python.command.python.viewLanguageServerOutput.title%", + "when": "!virtualWorkspace && shellExecutionSupported" }, - "python.workspaceSymbols.ctagsPath": { - "type": "string", - "default": "ctags", - "description": "Fully qualified path to the ctags executable (else leave as ctags, assuming it is in current path).", - "scope": "resource" + { + "category": "Python", + "command": "python.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.enabled": { - "type": "boolean", - "default": true, - "description": "Set to 'false' to disable Workspace Symbol provider using ctags.", - "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.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" + { + "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.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.execSelectionInDjangoShell", + "group": "Python", + "when": "editorHasSelection && editorLangId == python && python.isDjangoProject && !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.execSelectionInTerminal", + "group": "Python", + "when": "!config.python.REPL.sendToNativeREPL && editorFocus && editorLangId == python && !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.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" + }, + { + "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" - ] - }, + "submenus": [ { - "id": "json", - "filenames": [ - "Pipfile.lock" - ] - }, - { - "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" @@ -2178,284 +1504,310 @@ "url": "./schemas/conda-meta.json" } ], - "yamlValidation": [ + "languageModelTools": [ + { + "name": "get_python_environment_details", + "displayName": "Get Python Environment Info", + "userDescription": "%python.languageModelTools.get_python_environment_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. The details returned include the 1. Type of Python Environment (conda, venv, etc), 2. Version of Python, 3. List of all installed Python packages with their versions. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "getPythonEnvironmentInfo", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(snake)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the environment information for." + } + }, + "required": [] + } + }, { - "fileMatch": ".condarc", - "url": "./schemas/condarc.json" + "name": "get_python_executable_details", + "displayName": "Get Python Executable", + "userDescription": "%python.languageModelTools.get_python_executable_details.userDescription%", + "modelDescription": "This tool will retrieve the details of the Python Environment for the specified file or workspace. ALWAYS use this tool before executing any Python command in the terminal. This tool returns the details of how to construct the fully qualified path and or command including details such as arguments required to run Python in a terminal. Note: Instead of executing `python --version` or `python -c 'import sys; print(sys.executable)'`, use this tool to get the Python executable path to replace the `python` command. E.g. instead of using `python -c 'import sys; print(sys.executable)'`, use this tool to build the command `conda run -n -c 'import sys; print(sys.executable)'`. ALWAYS call configure_python_environment before using this tool. IMPORTANT: This tool is only for Python environments (venv, virtualenv, conda, pipenv, poetry, pyenv, pixi, or any other Python environment manager). Do not use this tool for npm packages, system packages, Ruby gems, or any other non-Python dependencies.", + "toolReferenceName": "getPythonExecutableCommand", + "tags": [ + "python", + "python environment", + "extension_installed_by_tool", + "enable_other_tool_configure_python_environment" + ], + "icon": "$(terminal)", + "canBeReferencedInPrompt": true, + "inputSchema": { + "type": "object", + "properties": { + "resourcePath": { + "type": "string", + "description": "The path to the Python file or workspace to get the executable information for. If not provided, the current workspace will be used. Where possible pass the path to the file or workspace." + } + }, + "required": [] + } }, { - "fileMatch": "environment.yml", - "url": "./schemas/conda-environment.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" + ] + } }, { - "fileMatch": "meta.yaml", - "url": "./schemas/conda-meta.json" - } - ], - "views": { - "test": [ - { - "id": "python_tests", - "name": "PYTHON", - "when": "testsDiscovered" + "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 -o ms-python-insiders.vsix", + "prePublish": "gulp clean && gulp prePublishNonBundle", "compile": "tsc -watch -p ./", - "compile-webviews-watch": "npx npx -n --max_old_space_size=4096 webpack --config webpack.datascience-ui.config.js --watch", - "dump-datascience-webpack-stats": "npx -n --max_old_space_size=4096 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 && node ./build/ci/postInstall.js", + "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 --opts ./build/.mocha.unittests.js.opts", - "test:unittests:cover": "nyc --silent --no-clean --nycrc-path build/.nycrc mocha --opts ./build/.mocha.unittests.ts.opts", - "test:functional": "mocha --require source-map-support/register --opts ./build/.mocha.functional.opts", - "test:functional:cover": "npm run test:functional", - "test:cover:report": "nyc --nycrc-path build/.nycrc report --reporter=lcov --reporter=text --reporter=html --reporter=text-summary --reporter=cobertura", + "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": "node ./out/test/smokeTest.js", + "testSmoke": "cross-env INSTALL_JUPYTER_EXTENSION=true \"node ./out/test/smokeTest.js\"", + "testInsiders": "cross-env VSC_PYTHON_CI_TEST_VSC_CHANNEL=insiders INSTALL_PYLANCE_EXTENSION=true TEST_FILES_SUFFIX=insiders.test CODE_TESTS_WORKSPACE=src/testMultiRootWkspc/smokeTests \"node ./out/test/standardTest.js\"", "lint-staged": "node gulpfile.js", - "lint": "tslint src/**/*.ts -t verbose", + "lint": "eslint src build pythonExtensionApi", + "lint-fix": "eslint --fix src build pythonExtensionApi gulpfile.js", + "format-check": "prettier --check 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", + "format-fix": "prettier --write 'src/**/*.ts' 'build/**/*.js' '.github/**/*.yml' gulpfile.js", + "check-python": "npm run check-python:ruff && npm run check-python:pyright", + "check-python:ruff": "cd python_files && python -m pip install -U ruff && python -m ruff check . && python -m ruff format --check", + "check-python:pyright": "cd python_files && npx --yes pyright@1.1.308 .", "clean": "gulp clean", + "addExtensionPackDependencies": "gulp addExtensionPackDependencies", "updateBuildNumber": "gulp updateBuildNumber", - "verifyBundle": "gulp verifyBundle" + "verifyBundle": "gulp verifyBundle", + "webpack": "webpack" }, "dependencies": { - "@jupyterlab/services": "^3.2.1", + "@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", - "fs-extra": "^4.0.3", - "fuzzy": "^0.1.3", - "get-port": "^3.2.0", - "glob": "^7.1.2", - "hash.js": "^1.1.7", - "iconv-lite": "^0.4.21", - "inversify": "^4.11.1", - "jsonc-parser": "^2.0.3", - "less-plugin-inline-urls": "^1.2.0", - "line-by-line": "^0.1.6", - "lodash": "^4.17.11", - "md5": "^2.2.1", - "minimatch": "^3.0.4", - "monaco-editor-textmate": "^2.1.1", - "monaco-textmate": "^3.0.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-fetch": "^1.0.0", "node-stream-zip": "^1.6.0", - "onigasm": "^2.2.2", - "pdfkit": "^0.10.0", - "pidusage": "^1.2.0", - "react-svg-pan-zoom": "^3.1.0", - "react-svgmt": "^1.1.8", - "react-virtualized": "^9.21.1", - "reflect-metadata": "^0.1.12", - "request": "^2.87.0", - "request-progress": "^3.0.0", - "rxjs": "^5.5.9", - "semver": "^5.5.0", - "slickgrid": "^2.4.7", + "reflect-metadata": "^0.2.2", + "rxjs": "^6.5.4", + "rxjs-compat": "^6.5.4", + "semver": "^7.5.2", "stack-trace": "0.0.10", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^2.0.1", - "sudo-prompt": "^8.2.0", - "svg-to-pdfkit": "^0.1.7", - "tmp": "^0.0.29", - "tree-kill": "^1.2.0", - "tslint": "^5.14.0", - "typescript-char": "^0.0.0", - "uint64be": "^1.0.1", - "unicode": "^10.0.0", - "untildify": "^3.0.2", - "vscode-debugadapter": "^1.28.0", + "sudo-prompt": "^9.2.1", + "tmp": "^0.2.5", + "uint64be": "^3.0.0", + "unicode": "^14.0.0", "vscode-debugprotocol": "^1.28.0", - "vscode-extension-telemetry": "^0.1.0", - "vscode-languageclient": "^5.2.1", - "vscode-languageserver": "^5.2.1", - "vscode-languageserver-protocol": "^3.14.1", - "vsls": "^0.3.1291", + "vscode-jsonrpc": "^9.0.0-next.5", + "vscode-languageclient": "^10.0.0-next.12", + "vscode-languageserver-protocol": "^3.17.6-next.10", + "vscode-tas-client": "^0.1.84", + "which": "^2.0.2", "winreg": "^1.2.4", - "winston": "^3.2.1", - "ws": "^6.0.0", - "xml2js": "^0.4.19" + "xml2js": "^0.5.0" }, "devDependencies": { - "@babel/cli": "^7.4.4", - "@babel/core": "^7.4.4", - "@babel/plugin-transform-runtime": "^7.4.4", - "@babel/polyfill": "^7.4.4", - "@babel/preset-env": "^7.1.0", - "@babel/preset-react": "^7.0.0", - "@babel/register": "^7.4.4", - "@istanbuljs/nyc-config-typescript": "^0.1.3", - "@nteract/plotly": "^1.48.3", - "@nteract/transform-dataresource": "^4.3.5", - "@nteract/transform-geojson": "^3.2.3", - "@nteract/transform-model-debug": "^3.2.3", - "@nteract/transform-plotly": "^5.0.0", - "@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/diff-match-patch": "^1.0.32", - "@types/download": "^6.2.2", - "@types/enzyme": "^3.1.14", - "@types/enzyme-adapter-react-16": "^1.0.3", - "@types/event-stream": "^3.3.33", - "@types/fs-extra": "^5.0.1", - "@types/get-port": "^3.2.0", - "@types/glob": "^5.0.35", - "@types/html-webpack-plugin": "^3.2.0", - "@types/iconv-lite": "^0.0.1", - "@types/jsdom": "^11.12.0", - "@types/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": "^5.2.6", - "@types/nock": "^10.0.3", - "@types/node": "9.4.7", - "@types/node-fetch": "^2.3.4", - "@types/pdfkit": "^0.7.36", - "@types/promisify-node": "^0.4.0", - "@types/react": "^16.4.14", - "@types/react-dom": "^16.0.8", - "@types/react-json-tree": "^0.6.8", - "@types/react-virtualized": "^9.21.2", - "@types/request": "^2.47.0", + "@types/mocha": "^9.1.0", + "@types/node": "^22.19.1", "@types/semver": "^5.5.0", "@types/shortid": "^0.0.29", - "@types/sinon": "^7.0.13", - "@types/slickgrid": "^2.1.27", + "@types/sinon": "^17.0.3", "@types/stack-trace": "0.0.29", - "@types/strip-json-comments": "0.0.30", - "@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/tmp": "^0.0.33", + "@types/vscode": "^1.95.0", + "@types/which": "^2.0.1", "@types/winreg": "^1.2.30", - "@types/ws": "^6.0.1", "@types/xml2js": "^0.4.2", - "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-transform-runtime": "^6.23.0", - "babel-polyfill": "^6.26.0", - "bootstrap": "^4.3.1", - "bootstrap-less": "^3.3.8", - "brfs": "^2.0.2", - "canvas": "2.0.1", + "@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.5.0", - "colors": "^1.2.1", - "copy-webpack-plugin": "^4.6.0", - "coveralls": "^3.0.4", + "copy-webpack-plugin": "^9.1.0", + "cross-env": "^7.0.3", "cross-spawn": "^6.0.5", - "css-loader": "^1.0.1", - "cucumber-html-reporter": "^4.0.5", - "decache": "^4.4.0", - "del": "^3.0.0", - "download": "^7.0.0", - "electron-download": "^4.1.1", - "enzyme": "^3.7.0", - "enzyme-adapter-react-16": "^1.6.0", - "event-stream": "3.3.4", - "expose-loader": "^0.7.5", - "extract-zip": "^1.6.7", - "file-loader": "^2.0.0", - "filemanager-webpack-plugin": "^2.0.5", - "flat": "^4.0.0", - "gulp": "^4.0.0", - "gulp-azure-storage": "^0.9.0", - "gulp-chmod": "^2.0.0", - "gulp-filter": "^5.1.0", - "gulp-gunzip": "^1.1.0", - "gulp-rename": "^1.4.0", - "gulp-sourcemaps": "^2.6.4", - "gulp-typescript": "^4.0.1", - "gulp-untar": "0.0.8", - "gulp-vinyl-zip": "^2.1.2", - "html-webpack-plugin": "^3.2.0", - "husky": "^1.1.2", - "immutable": "^4.0.0-rc.12", - "is-running": "^2.1.0", - "jsdom": "^15.0.0", - "json-loader": "^0.5.7", - "less": "^3.9.0", - "less-loader": "^5.0.0", - "loader-utils": "^1.1.0", - "mocha": "^6.1.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", - "monaco-editor": "0.16.2", - "monaco-editor-webpack-plugin": "^1.7.0", - "nock": "^10.0.6", "node-has-native-dependencies": "^1.0.2", - "node-html-parser": "^1.1.13", - "nyc": "^14.1.1", - "raw-loader": "^0.5.1", - "react": "^16.5.2", - "react-data-grid": "^6.0.2-0", - "react-data-grid-addons": "^6.0.2-0", - "react-dev-utils": "^5.0.2", - "react-dom": "^16.5.2", - "react-json-tree": "^0.11.0", - "relative": "^3.0.2", - "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", - "sass-loader": "^7.1.0", "shortid": "^2.2.8", - "sinon": "^7.3.2", + "sinon": "^18.0.0", "source-map-support": "^0.5.12", - "style-loader": "^0.23.1", - "styled-jsx": "^3.1.0", - "svg-inline-loader": "^0.8.0", - "svg-inline-react": "^3.1.0", - "terser-webpack-plugin": "^1.2.3", - "transform-loader": "^0.2.4", - "ts-loader": "^5.3.0", - "ts-mockito": "^2.3.1", - "ts-node": "^8.3.0", + "ts-loader": "^9.2.8", + "ts-mockito": "^2.5.0", + "ts-node": "^10.7.0", "tsconfig-paths-webpack-plugin": "^3.2.0", - "tslint-eslint-rules": "^5.1.0", - "tslint-microsoft-contrib": "^5.0.3", - "typed-react-markdown": "^0.1.0", "typemoq": "^2.1.0", - "typescript": "^3.5.2", - "typescript-formatter": "^7.1.0", - "unicode-properties": "1.1.0", - "url-loader": "^1.1.2", - "uuid": "^3.3.2", - "vinyl-fs": "^3.0.3", - "vsce": "^1.59.0", - "vscode": "^1.1.33", - "vscode-debugadapter-testsupport": "^1.27.0", - "webpack": "^4.33.0", - "webpack-bundle-analyzer": "^3.3.2", - "webpack-cli": "^3.1.2", + "typescript": "~5.2", + "uuid": "^8.3.2", + "webpack": "^5.105.0", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-cli": "^4.9.2", "webpack-fix-default-import-plugin": "^1.0.3", - "webpack-merge": "^4.1.4", - "webpack-node-externals": "^1.7.2", - "why-is-node-running": "^2.0.3", - "wtfnode": "^0.8.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 63bf670ea03c..000000000000 --- a/package.nls.de.json +++ /dev/null @@ -1,32 +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.module.label": "Python: Modul", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid-Anwendung", - "python.snippet.launch.attach.label": "Python: Anfügen" -} diff --git a/package.nls.es.json b/package.nls.es.json deleted file mode 100644 index 3117226a4c34..000000000000 --- a/package.nls.es.json +++ /dev/null @@ -1,32 +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.module.label": "Python: Módulo", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid", - "python.snippet.launch.attach.label": "Python: Adjuntar" -} diff --git a/package.nls.fr.json b/package.nls.fr.json deleted file mode 100644 index 14e4cea23758..000000000000 --- a/package.nls.fr.json +++ /dev/null @@ -1,31 +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.module.label": "Python: Module", - "python.snippet.launch.django.label": "Python : Django", - "python.snippet.launch.flask.label": "Python : Flask", - "python.snippet.launch.pyramid.label": "Python : application Pyramid", - "python.snippet.launch.attach.label": "Python: Attacher" -} diff --git a/package.nls.it.json b/package.nls.it.json deleted file mode 100644 index 75888a537ee8..000000000000 --- a/package.nls.it.json +++ /dev/null @@ -1,32 +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.module.label": "Python: Modulo", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Applicazione Pyramid", - "python.snippet.launch.attach.label": "Python: Allega", - "LanguageService.bannerLabelYes": "Sì, prenderò il sondaggio ora" -} diff --git a/package.nls.ja.json b/package.nls.ja.json deleted file mode 100644 index 9815d74cb093..000000000000 --- a/package.nls.ja.json +++ /dev/null @@ -1,27 +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.module.label": "Python: モジュール", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid アプリケーション", - "python.snippet.launch.attach.label": "Python: アタッチ" -} diff --git a/package.nls.json b/package.nls.json index 52148fe8a43a..57f2ed95b2c0 100644 --- a/package.nls.json +++ b/package.nls.json @@ -1,290 +1,177 @@ { - "python.command.python.sortImports.title": "Sort Imports", - "python.command.python.startREPL.title": "Start REPL", + "python.command.python.startTerminalREPL.title": "Start Terminal REPL", + "python.languageModelTools.get_python_environment_details.userDescription": "Get information for a Python Environment, such as Type, Version, Packages, and more.", + "python.languageModelTools.install_python_packages.userDescription": "Installs Python packages in a Python Environment.", + "python.languageModelTools.get_python_executable_details.userDescription": "Get executable info for a Python Environment", + "python.languageModelTools.configure_python_environment.userDescription": "Configure a Python Environment for a workspace", + "python.command.python.startNativeREPL.title": "Start Native Python REPL", + "python.command.python.createEnvironment.title": "Create Environment...", + "python.command.python.createNewFile.title": "New Python File", "python.command.python.createTerminal.title": "Create Terminal", - "python.command.python.buildWorkspaceSymbols.title": "Build Workspace Symbols", - "python.command.python.runtests.title": "Run All Tests", - "python.command.python.debugtests.title": "Debug All Tests", "python.command.python.execInTerminal.title": "Run Python File in Terminal", + "python.command.python.execInTerminalIcon.title": "Run Python File", + "python.command.python.execInDedicatedTerminal.title": "Run Python File in Dedicated Terminal", "python.command.python.setInterpreter.title": "Select Interpreter", - "python.command.python.updateSparkLibrary.title": "Update Workspace PySpark Libraries", - "python.command.python.refactorExtractVariable.title": "Extract Variable", - "python.command.python.refactorExtractMethod.title": "Extract Method", + "python.command.python.clearWorkspaceInterpreter.title": "Clear Workspace Interpreter Setting", "python.command.python.viewOutput.title": "Show Output", - "python.command.python.viewTestOutput.title": "Show Test Output", - "python.command.python.selectAndRunTestMethod.title": "Run Test Method ...", - "python.command.python.selectAndDebugTestMethod.title": "Debug Test Method ...", - "python.command.python.selectAndRunTestFile.title": "Run Test File ...", - "python.command.python.runCurrentTestFile.title": "Run Current Test File", - "python.command.python.runFailedTests.title": "Run Failed Tests", - "python.command.python.discoverTests.title": "Discover Tests", - "python.command.python.discoveringTests.title": "Discovering...", - "python.command.python.stopTests.title": "Stop", + "python.command.python.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.runFileInteractive.title": "Run Current File in Python Interactive Window", - "python.command.python.datascience.runallcells.title": "Run All Cells", - "python.command.python.datascience.runallcellsabove.title": "Run Above", - "python.command.python.datascience.runcellandallbelow.title": "Run Below", - "python.command.python.datascience.runallcellsabove.palette.title": "Run Cells Above Current Cell", - "python.command.python.datascience.runcurrentcellandallbelow.palette.title": "Run Current Cell and Below", - "python.command.python.datascience.debugcurrentcell.palette.title": "Debug Current Cell", - "python.command.python.datascience.debugcurrentcell.title": "Debug Cell", - "python.command.python.datascience.runtoline.title": "Run To Line in Python Interactive window", - "python.command.python.datascience.runfromline.title": "Run From Line in Python Interactive window", - "python.command.python.datascience.runcurrentcell.title": "Run Current Cell", - "python.command.python.datascience.runcurrentcelladvance.title": "Run Current Cell And Advance", - "python.command.python.datascience.execSelectionInteractive.title": "Run Selection/Line in Python Interactive window", - "python.command.python.datascience.runcell.title": "Run Cell", - "python.command.python.datascience.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.command.python.datascience.addcellbelow.title": "Add empty cell to file", - "python.snippet.launch.standard.label": "Python: Current File", - "python.snippet.launch.module.label": "Python: Module", - "python.snippet.launch.module.default": "enter-your-module-name", - "python.snippet.launch.attach.label": "Python: Remote Attach", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid Application", - "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", - "LanguageService.lsFailedToStart": "We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.", - "LanguageService.lsFailedToDownload": "We encountered an issue downloading the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.", - "LanguageService.lsFailedToExtract": "We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.", - "DataScience.unknownMimeTypeFormat": "Mime type {0} is not currently supported.", - "DataScience.historyTitle": "Python Interactive", - "DataScience.dataExplorerTitle": "Data Viewer", - "DataScience.badWebPanelFormatString": "

{0} is not a valid file name

", - "DataScience.sessionDisposed": "Cannot execute code, session has been disposed.", - "DataScience.passwordFailure": "Failed to connect to password protected server. Check that password is correct.", - "DataScience.exportDialogTitle": "Export to Jupyter Notebook", - "DataScience.exportDialogFilter": "Jupyter Notebooks", - "DataScience.exportDialogComplete": "Notebook written to {0}", - "DataScience.exportDialogFailed": "Failed to export notebook. {0}", - "DataScience.exportOpenQuestion": "Open in browser", - "DataScience.collapseInputTooltip": "Collapse input block", - "DataScience.collapseVariableExplorerTooltip": "Collapse variable explorer", - "DataScience.collapseVariableExplorerLabel": "Variables", - "DataScience.variableLoadingValue": "Loading...", - "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.jupyterNotSupportedBecauseOfEnvironment": "Activating {0} to run Jupyter failed with {1}.", - "DataScience.jupyterNbConvertNotSupported": "Importing notebooks requires Jupyter nbconvert to be installed.", - "DataScience.jupyterLaunchNoURL": "Failed to find the URL of the launched Jupyter notebook server", - "DataScience.jupyterLaunchTimedOut": "The Jupyter notebook server failed to launch in time", - "DataScience.jupyterSelfCertFail": "The security certificate used by server {0} was not issued by a trusted certificate authority.\r\nThis may indicate an attempt to steal your information.\r\nDo you want to enable the Allow Unauthorized Remote Connection setting for this workspace to allow you to connect?", - "DataScience.jupyterSelfCertEnable": "Yes, connect anyways", - "DataScience.jupyterSelfCertClose": "No, close the connection", - "DataScience.jupyterServerCrashed": "Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}", - "DataScience.pythonInteractiveHelpLink": "Get more help", - "DataScience.importingFormat": "Importing {0}", - "DataScience.startingJupyter": "Starting Jupyter server", - "DataScience.connectingToJupyter": "Connecting to Jupyter server", - "Experiments.inGroup": "User belongs to experiment group '{0}'", - "Interpreters.RefreshingInterpreters": "Refreshing Python Interpreters", - "Interpreters.LoadingInterpreters": "Loading Python Interpreters", - "Common.doNotShowAgain": "Do not show again", - "Interpreters.environmentPromptMessage": "We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?", - "DataScience.restartKernelMessage": "Do you want to restart the IPython kernel? All variables will be lost.", - "DataScience.restartKernelMessageYes": "Yes", - "DataScience.restartKernelMessageNo": "No", - "DataScience.restartingKernelFailed": "Kernel restart failed. Jupyter server is hung. Please reload VS Code.", - "DataScience.interruptingKernelFailed": "Kernel interrupt failed. Jupyter server is hung. Please reload VS Code.", - "DataScienceSurveyBanner.bannerMessage": "Can you please take 2 minutes to tell us how the Python Data Science features are working for you?", - "DataScienceSurveyBanner.bannerLabelYes": "Yes, take survey now", - "DataScienceSurveyBanner.bannerLabelNo": "No, thanks", - "InteractiveShiftEnterBanner.bannerMessage": "Would you like to run code in the 'Python Interactive' window (an IPython console) for 'shift-enter'? Select 'No' to continue to run code in the Python Terminal. This can be changed later in settings.", - "InteractiveShiftEnterBanner.bannerLabelYes": "Yes", - "InteractiveShiftEnterBanner.bannerLabelNo": "No", - "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:", - "DataScience.pythonNewHeader": "Started new kernel:", - "DataScience.executingCodeFailure": "Executing code failed : {0}", - "DataScience.inputWatermark": "Shift-enter to run", - "DataScience.deleteButtonTooltip": "Remove cell", - "DataScience.gotoCodeButtonTooltip": "Go to code", - "DataScience.copyBackToSourceButtonTooltip": "Paste code into file", - "DataScience.plotOpen": "Expand image", - "Linter.enableLinter": "Enable {0}", - "Linter.enablePylint": "You have a pylintrc file in your workspace. Do you want to enable pylint?", - "Linter.replaceWithSelectedLinter": "Multiple linters are enabled in settings. Replace with '{0}'?", - "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.jupyterSelectPasswordPrompt": "Enter your notebook password", - "DataScience.jupyterNotebookFailure": "Jupyter notebook failed to launch. \r\n{0}", - "DataScience.jupyterNotebookConnectFailed": "Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}", - "DataScience.jupyterNotebookRemoteConnectFailed": "Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}", - "DataScience.jupyterNotebookRemoteConnectSelfCertsFailed": "Failed to connect to remote Jupyter notebook.\r\nSpecified server is using self signed certs. Enable Allow Unauthorized Remote Connection setting to connect anyways\r\n{0}\r\n{1}", - "DataScience.notebookVersionFormat": "Jupyter Notebook Version: {0}", - "DataScience.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", - "DataScience.jupyterGetVariablesBadResults": "Failed to fetch variable info from the Jupyter server.", - "DataScience.liveShareConnectFailure": "Cannot connect to host Jupyter session. URI not found.", - "DataScience.liveShareCannotSpawnNotebooks": "Spawning Jupyter notebooks is not supported over a live share connection", - "DataScience.liveShareCannotImportNotebooks": "Importing notebooks is not currently supported over a live share connection", - "DataScience.liveShareHostFormat": "{0} Jupyter Server", - "DataScience.liveShareSyncFailure": "Synchronization failure during live share startup.", - "DataScience.liveShareServiceFailure": "Failure starting '{0}' service during live share connection.", - "DataScience.documentMismatch": "Cannot run cells, duplicate documents for {0} found.", - "DataScience.pythonInteractiveCreateFailed": "Failure to create a 'Python Interactive' window. Try reinstalling the Python extension.", - "diagnostics.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.", - "diagnostics.invalidPythonPathInDebuggerSettings": "You need to select a Python interpreter before you start debugging.\n\nTip: click on \"Select Python Interpreter\" in the status bar.", - "diagnostics.invalidPythonPathInDebuggerLaunch": "The Python path in your debug configuration is invalid.", - "diagnostics.invalidDebuggerTypeDiagnostic": "Your launch.json file needs to be updated to change the \"pythonExperimental\" debug configurations to use the \"python\" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?", - "diagnostics.consoleTypeDiagnostic": "Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?", - "diagnostics.justMyCodeDiagnostic": "Configuration \"debugStdLib\" in launch.json is no longer supported. It's recommended to replace it with \"justMyCode\", which is the exact opposite of using \"debugStdLib\". Would you like to automatically update your launch.json file to do that?", - "diagnostics.yesUpdateLaunch": "Yes, update launch.json", - "diagnostics.bannerLabelNo": "No, I will do it later", - "diagnostics.invalidTestSettings": "Your settings needs to be updated to change the setting \"python.unitTest.\" to \"python.testing.\", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?", - "DataScience.interruptKernel": "Interrupt IPython Kernel", - "DataScience.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 DataScience.changeDirOnImportExport setting", - "DataScience.exportChangeDirectoryComment": "# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting", - "DataScience.interruptKernelStatus": "Interrupting IPython Kernel", - "DataScience.restartKernelAfterInterruptMessage": "Interrupting the kernel timed out. Do you want to restart the kernel instead? All variables will be lost.", - "DataScience.pythonInterruptFailedHeader": "Keyboard interrupt crashed the kernel. Kernel restarted.", - "DataScience.sysInfoURILabel": "Jupyter Server URI: ", - "Common.loadingPythonExtension": "Python extension loading...", - "debug.selectConfigurationTitle": "Select a debug configuration", - "debug.selectConfigurationPlaceholder": "Debug Configuration", - "debug.launchJsonConfigurationsCompletionLabel": "Python", - "debug.launchJsonConfigurationsCompletionDescription": "Select a Python debug configuration", - "debug.debugFileConfigurationLabel": "Python File", - "debug.debugFileConfigurationDescription": "Debug the currently active Python file", - "debug.debugModuleConfigurationLabel": "Module", - "debug.debugModuleConfigurationDescription": "Debug a Python module by invoking it with '-m'", - "debug.moduleEnterModuleTitle": "Debug Module", - "debug.moduleEnterModulePrompt": "Enter a Python module/package name", - "debug.moduleEnterModuleDefault": "enter-your-module-name", - "debug.moduleEnterModuleInvalidNameError": "Enter a valid module name", - "debug.remoteAttachConfigurationLabel": "Remote Attach", - "debug.remoteAttachConfigurationDescription": "Attach to a remote ptsvd debug server", - "debug.attachRemoteHostTitle": "Remote Debugging", - "debug.attachRemoteHostPrompt": "Enter the host name", - "debug.attachRemoteHostValidationError": "Enter a valid host name or IP address", - "debug.attachRemotePortTitle": "Remote Debugging", - "debug.attachRemotePortPrompt": "Enter the port number that the ptvsd server is listening on", - "debug.attachRemotePortValidationError": "Enter a valid port number", - "debug.debugDjangoConfigurationLabel": "Django", - "debug.debugDjangoConfigurationDescription": "Launch and debug a Django web application", - "debug.djangoEnterManagePyPathTitle": "Debug Django", - "debug.djangoEnterManagePyPathPrompt": "Enter the path to manage.py ('${workspaceFolderToken}' points to the root of the current workspace folder)", - "debug.djangoEnterManagePyPathInvalidFilePathError": "Enter a valid Python file path", - "debug.debugFlaskConfigurationLabel": "Flask", - "debug.debugFlaskConfigurationDescription": "Launch and debug a Flask web application", - "debug.flaskEnterAppPathOrNamePathTitle": "Debug Flask", - "debug.flaskEnterAppPathOrNamePathPrompt": "Enter the path to the application, e.g. 'app.py' or 'app'", - "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Enter a valid name", - "debug.debugPyramidConfigurationLabel": "Pyramid", - "debug.debugPyramidConfigurationDescription": "Web Application", - "debug.pyramidEnterDevelopmentIniPathTitle": "Debug Pyramid", - "debug.pyramidEnterDevelopmentIniPathPrompt": "`Enter the path to development.ini ('${workspaceFolderToken}' points to the root of the current workspace folder)`", - "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Enter a valid file path", - "Testing.testErrorDiagnosticMessage": "Error", - "Testing.testFailDiagnosticMessage": "Fail", - "Testing.testSkippedDiagnosticMessage": "Skipped", - "Testing.configureTests": "Configure Test Framework", - "Testing.disableTests": "Disable Tests", - "Common.openOutputPanel": "Show output", - "LanguageService.downloadFailedOutputMessage": "download failed", - "LanguageService.extractionFailedOutputMessage": "extraction failed", - "LanguageService.extractionCompletedOutputMessage": "complete", - "LanguageService.extractionDoneOutputMessage": "done", - "LanguageService.reloadVSCodeIfSeachPathHasChanged": "Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly", - "DataScience.variableExplorerNameColumn": "Name", - "DataScience.variableExplorerTypeColumn": "Type", - "DataScience.variableExplorerSizeColumn": "Count", - "DataScience.variableExplorerValueColumn": "Value", - "DataScience.showDataExplorerTooltip": "Show variable in data viewer.", - "DataScience.dataExplorerInvalidVariableFormat": "'{0}' is not an active variable.", - "DataScience.jupyterGetVariablesExecutionError": "Failure during variable extraction:\r\n{0}", - "DataScience.loadingMessage": "loading ...", - "DataScience.fetchingDataViewer": "Fetching data ...", - "DataScience.noRowsInDataViewer": "No rows match current filter", - "DataScience.pandasTooOldForViewingFormat": "Python package 'pandas' is version {0}. Version 0.20 or greater is required for viewing data.", - "DataScience.pandasRequiredForViewing": "Python package 'pandas' is required for viewing data.", - "DataScience.valuesColumn": "values", - "DataScience.liveShareInvalid": "One or more guests in the session do not have the Python [extension](https://marketplace.visualstudio.com/itemdetails?itemName=ms-python.python) installed.\r\nYour Live Share session cannot continue and will be closed.", - "diagnostics.updateSettings": "Yes, update settings", - "Common.noIWillDoItLater": "No, I will do it later", - "Common.notNow": "Not now", - "Common.gotIt": "Got it!", - "Interpreters.selectInterpreterTip": "Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar", - "DataScience.noRowsInVariableExplorer": "No variables defined", - "DataScience.tooManyColumnsMessage": "Variables with over a 1000 columns may take a long time to display. Are you sure you wish to continue?", - "DataScience.tooManyColumnsYes": "Yes", - "DataScience.tooManyColumnsNo": "No", - "DataScience.tooManyColumnsDontAskAgain": "Don't Ask Again", - "DataScience.filterRowsButton": "Filter Rows", - "DataScience.filterRowsTooltip": "Allows filtering multiple rows. Use =, >, or < signs to filter numeric values.", - "DataScience.previewHeader": "--- Begin preview of {0} ---", - "DataScience.previewFooter": "--- End preview of {0} ---", - "DataScience.previewStatusMessage": "Generating preview of {0}", - "DataScience.plotViewerTitle": "Plots", - "DataScience.exportPlotTitle": "Save plot image", - "DataScience.pdfFilter": "PDF", - "DataScience.pngFilter": "PNG", - "DataScience.svgFilter": "SVG", - "DataScience.previousPlot": "Previous", - "DataScience.nextPlot": "Next", - "DataScience.panPlot": "Pan", - "DataScience.zoomInPlot": "Zoom in", - "DataScience.zoomOutPlot": "Zoom out", - "DataScience.exportPlot": "Export to different formats", - "DataScience.deletePlot": "Remove", - "DataScience.collapseSingle": "Collapse", - "DataScience.expandSingle": "Expand", - "DataScience.editSection": "Input new cells here.", - "DataScience.restartKernelMessageDontAskAgain": "Yes, and Don't Ask Again", - "DataScience.selectedImageListLabel": "Selected Image", - "DataScience.imageListLabel": "Image", - "DataScience.exportImageFailed": "Error exporting image: {0}", - "downloading.file": "Downloading {0}...", - "downloading.file.progress": "{0}{1} of {2} KB ({3})", - "DataScience.jupyterDataRateExceeded": "Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0", - "DataScience.addCellBelowCommandTitle" : "Add cell", - "DataScience.debugCellCommandTitle" : "Debug cell", - "DataScience.variableExplorerDisabledDuringDebugging" : "Variables are not available while debugging." + "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 b83de82bc4f5..000000000000 --- a/package.nls.ko-kr.json +++ /dev/null @@ -1,27 +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.module.label": "Python: 모듈", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid 응용 프로그램", - "python.snippet.launch.attach.label": "Python: 연결" -} diff --git a/package.nls.nl.json b/package.nls.nl.json deleted file mode 100644 index 0c30c4bdf383..000000000000 --- a/package.nls.nl.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "python.command.python.sortImports.title": "Import sorteren", - "python.command.python.startREPL.title": "REPL starten", - "python.command.python.createTerminal.title": "Terminal aanmaken", - "python.command.python.buildWorkspaceSymbols.title": "Werkruimte-symbolen aanmaken", - "python.command.python.runtests.title": "Alle unittests uitvoeren", - "python.command.python.debugtests.title": "Alle unittests debuggen", - "python.command.python.execInTerminal.title": "Python-bestand in terminal uitvoeren", - "python.command.python.setInterpreter.title": "Interpreter selecteren", - "python.command.python.updateSparkLibrary.title": "Pyspark-werkruimtebibliotheken updaten", - "python.command.python.refactorExtractVariable.title": "Variabelen selecteren", - "python.command.python.refactorExtractMethod.title": "Methode selecteren", - "python.command.python.viewTestOutput.title": "Unittest-resultaat laten zien", - "python.command.python.selectAndRunTestMethod.title": "Unittest-methode uitvoeren ...", - "python.command.python.selectAndDebugTestMethod.title": "Unittest-methode debuggen ...", - "python.command.python.selectAndRunTestFile.title": "Unittest-bestand uitvoeren ...", - "python.command.python.runCurrentTestFile.title": "Huidige unittest-bestand uitvoeren", - "python.command.python.runFailedTests.title": "Gefaalde unittests uitvoeren", - "python.command.python.discoverTests.title": "Unittests doorzoeken", - "python.command.python.execSelectionInTerminal.title": "Selectie/rij in Python-terminal uitvoeren", - "python.command.python.execSelectionInDjangoShell.title": "Selectie/rij in Django-shell uitvoeren", - "python.command.python.goToPythonObject.title": "Naar Python-object gaan", - "python.command.python.setLinter.title": "Linter selecteren", - "python.command.python.enableLinting.title": "Linting activeren", - "python.command.python.runLinting.title": "Linting uitvoeren", - "python.command.python.datascience.runallcells.title": "Alle cellen uitvoeren", - "python.command.python.datascience.runcurrentcell.title": "Huidige cel uitvoeren", - "python.command.python.datascience.runcurrentcelladvance.title": "Huidige cel uitvoeren en doorgaan", - "python.command.python.datascience.runcell.title": "Cel uitvoeren", - "python.command.python.datascience.showhistorypane.title": "Interactief Python-venster laten zien", - "python.command.python.datascience.selectjupyteruri.title": "Jupyter-server-URI specificeren", - "python.command.python.datascience.importnotebook.title": "Jupyter-notebook importeren", - "python.command.python.datascience.importnotebookonfile.title": "Jupyter-notebook importeren", - "python.command.python.enableSourceMapSupport.title": "Bronkaartondersteuning voor extensie-debugging inschakelen", - "python.command.python.datascience.exportoutputasnotebook.title": "Interactief Python-venster als Jupyter-notebook exporteren", - "python.command.python.datascience.exportfileasnotebook.title": "Huidige Python-bestand als Jupyter-notebook exporteren", - "python.command.python.datascience.exportfileandoutputasnotebook.title": "Huidige Python-bestand exporteren en outputten als Jupyter-notebook", - "python.command.python.datascience.undocells.title": "Laatste interactieve Python-actie ongedaan maken", - "python.command.python.datascience.redocells.title": "Laatste interactieve Python-actie opnieuw uitvoeren", - "python.command.python.datascience.removeallcells.title": "Alle interactieve Python-cellen verwijderen", - "python.command.python.datascience.interruptkernel.title": "iPython-kernel onderbreken", - "python.command.python.datascience.restartkernel.title": "iPython-kernel herstarten", - "python.command.python.datascience.expandallcells.title": "Alle interactieve Python-vensters openen", - "python.command.python.datascience.collapseallcells.title": "Alle interactieve Python-vensters sluiten", - "python.snippet.launch.standard.label": "Python: Huidige bestand", - "python.snippet.launch.module.label": "Python: Module", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid-applicatie", - "python.snippet.launch.attach.label": "Python: aankoppelen", - "LanguageService.bannerMessage": "Zou je alsjeblieft 2 minuten kunnen nemen om ons te vertellen hoe de Python-language server voor jou werkt?", - "LanguageService.bannerLabelYes": "Ja, neem nu deel aan het onderzoek", - "LanguageService.bannerLabelNo": "Nee, bedankt", - "LanguageService.lsFailedToStart": "We zijn een probleem tegengekomen bij het starten van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", - "LanguageService.lsFailedToDownload": "We zijn een probleem tegengekomen bij het downloaden van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", - "LanguageService.lsFailedToExtract": "We zijn een probleem tegengekomen bij het uitpakken van de language server. Aan het terugschakelen naar het alternatief, Jedi. Bekijk het weergavepaneel voor details.", - "DataScience.unknownMimeTypeFormat": "Mime type {0} wordt momenteel niet ondersteund.", - "DataScience.historyTitle": "Interactieve Python", - "DataScience.badWebPanelFormatString": "

{0} is geen geldige bestandsnaam

", - "DataScience.sessionDisposed": "Kan code niet uitvoeren, de sessie is gesloten.", - "DataScience.exportDialogTitle": "Exporteren naar Jupyter-notebook", - "DataScience.exportDialogFilter": "Jupyter-notebooks", - "DataScience.exportDialogComplete": "Notebook geschreven naar {0}", - "DataScience.exportDialogFailed": "Niet gelukt om te exporteren naar Jupyter-notebook. {0}", - "DataScience.exportOpenQuestion": "In browser openen", - "DataScience.collapseInputTooltip": "Invoerblok sluiten", - "DataScience.importDialogTitle": "Jupyter-notebook importeren", - "DataScience.importDialogFilter": "Jupyter-notebooks", - "DataScience.notebookCheckForImportYes": "Importeer", - "DataScience.notebookCheckForImportNo": "Later", - "DataScience.notebookCheckForImportDontAskAgain": "Niet opnieuw vragen", - "DataScience.notebookCheckForImportTitle": "Wil je het Jupyter-notebook importeren als Python-code?", - "DataScience.jupyterNotSupported": "De uitgevoerde cellen vereisen Jupyter-notebooks geinstalleerd.", - "DataScience.jupyterNbConvertNotSupported": "Notebooks importeren vereist Jupyter-nbconvert geinstalleerd.", - "DataScience.jupyterLaunchTimedOut": "Het is niet gelukt de Jupyter-notebook-server op tijd te starten", - "DataScience.jupyterLaunchNoURL": "Het is niet gelukt de URL van de gestarte Jupyter-notebook-server te vinden", - "DataScience.pythonInteractiveHelpLink": "Krijg meer hulp", - "DataScience.importingFormat": "Aan het importeren {0}", - "DataScience.startingJupyter": "Jupyter-server starten", - "DataScience.connectingToJupyter": "Met Jupyter-server verbinden", - "Interpreters.RefreshingInterpreters": "Python-Interpreters verversen", - "Interpreters.LoadingInterpreters": "Python-Interpreters laden", - "DataScience.restartKernelMessage": "Wil je de iPython-kernel herstarten? Alle variabelen zullen verloren gaan.", - "DataScience.restartKernelMessageYes": "Herstarten", - "DataScience.restartKernelMessageNo": "Annuleren", - "DataScienceSurveyBanner.bannerMessage": "Zou je alsjeblieft 2 minuten kunnen nemen om ons te vertellen hoe de Python-data-science-functionaliteiten voor jou werkt?", - "DataScienceSurveyBanner.bannerLabelYes": "Ja, neem nu deel aan het onderzoek", - "DataScienceSurveyBanner.bannerLabelNo": "Nee, bedankt", - "DataScience.restartingKernelStatus": "iPython-Kernel herstarten", - "DataScience.executingCode": "Cel aan het uitvoeren", - "DataScience.collapseAll": "Alle cel-invoeren sluiten", - "DataScience.expandAll": "Alle cel-invoeren openen", - "DataScience.export": "Als Jupyter-Notebook exporteren", - "DataScience.restartServer": "iPython-kernel herstarten", - "DataScience.undo": "Herhaal", - "DataScience.redo": "Opnieuw uitvoeren", - "DataScience.clearAll": "Alle cellen verwijderen", - "DataScience.pythonVersionHeader": "Python versie:", - "DataScience.pythonVersionHeaderNoPyKernel": "Python versie kan niet overeenkomen, geen ipykernel gevonden:", - "DataScience.pythonRestartHeader": "Kernel herstart:", - "Linter.InstalledButNotEnabled": "Linter {0} is geinstalleerd maar niet ingeschakeld.", - "Linter.replaceWithSelectedLinter": "Meerdere linters zijn ingeschakeld in de instellingen. Vervangen met '{0}'?", - "DataScience.jupyterSelectURILaunchLocal": "Een lokale Jupyter-server starten wanneer nodig", - "DataScience.jupyterSelectURISpecifyURI": "Voer de URI in om te verbinden met een draaiende Jupiter-server", - "DataScience.jupyterSelectURIPrompt": "Voer de URI in van een Jupiter-server", - "DataScience.jupyterSelectURIInvalidURI": "Ongeldige URI gespecificeerd", - "DataScience.jupyterNotebookFailure": "Jupyter-notebook kon niet starten. \r\n{0}", - "DataScience.jupyterNotebookConnectFailed": "Verbinden met Jupiter-notebook is niet gelukt. \r\n{0}\r\n{1}", - "DataScience.notebookVersionFormat": "Jupyter-notebook versie: {0}", - "DataScience.jupyterKernelNotSupportedOnActive": "Jupyter-kernel kan niet worden gestart vanuit '{0}'. met behulp van de dichtstbijzijnde overeenkomst {1} in plaats daarvan.", - "DataScience.jupyterKernelSpecNotFound": "Kan geen Jupyter-kernel-spec aanmaken en er zijn er geen beschikbaar voor gebruik", - "diagnostics.warnSourceMaps": "Bronkaartondersteuning is ingeschakeld in de Python-extensie, dit zal een ongunstige impact hebben op de uitvoering van de extensie.", - "diagnostics.disableSourceMaps": "Bronkaartondersteuning uitschakelen", - "diagnostics.warnBeforeEnablingSourceMaps": "Bronkaartondersteuning inschakelen in de Python-extensie zal een ongunstige impact hebben op de uitvoering van de extensie.", - "diagnostics.enableSourceMapsAndReloadVSC": "Venster inschakelen en herladen", - "diagnostics.lsNotSupported": "Uw besturingssysteem voldoet niet aan de minimumeisen van de language server. Aan het terugschakelen naar het alternatief, Jedi.", - "DataScience.interruptKernel": "iPython-kernel onderbreken", - "DataScience.exportingFormat": "Aan het exporteren {0}", - "DataScience.exportCancel": "Annuleren", - "Common.canceled": "Geannuleerd", - "DataScience.importChangeDirectoryComment": "#%% De werkmap van de werkruimte root naar de ipynb-bestandslocatie veranderen. Schakel deze toevoeging uit met de instelling DataScience.changeDirOnImportExport", - "DataScience.exportChangeDirectoryComment": "# De map wijzigen naar de VSCode-werktuimte root zodat de relatieve pad-ladingen correct werken. Schakel deze toevoeging uit met de instelling DataScience.changeDirOnImportExport", - "DataScience.interruptKernelStatus": "iPython-kernel onderbreken", - "DataScience.restartKernelAfterInterruptMessage": "Het onderbreken van de kernel duurde te lang. Wil je de kernel in plaats daarvan herstarten? Alle variabelen zullen verloren gaan.", - "DataScience.pythonInterruptFailedHeader": "Toetsenbord-interrupt liet de kernel crashen. Kernel herstart.", - "DataScience.sysInfoURILabel": "Jupyter-server URI: ", - "Common.loadingPythonExtension": "Python-extensie aan het laden...", - "debug.selectConfigurationTitle": "Een debug-configuratie selecteren", - "debug.selectConfigurationPlaceholder": "Debug-configuratie", - "debug.debugFileConfigurationLabel": "Python-bestand", - "debug.debugFileConfigurationDescription": "Python-bestand debuggen", - "debug.debugModuleConfigurationLabel": "Module", - "debug.debugModuleConfigurationDescription": "Python module/package debuggen", - "debug.remoteAttachConfigurationLabel": "Extern aankoppelen", - "debug.remoteAttachConfigurationDescription": "Een externe Python-applicatie debuggen", - "debug.debugDjangoConfigurationLabel": "Django", - "debug.debugDjangoConfigurationDescription": "Web-applicatie", - "debug.debugFlaskConfigurationLabel": "Flask", - "debug.debugFlaskConfigurationDescription": "Web-applicatie", - "debug.debugPyramidConfigurationLabel": "Pyramid", - "debug.debugPyramidConfigurationDescription": "Web-applicatie", - "debug.djangoEnterManagePyPathTitle": "Django debuggen", - "debug.djangoEnterManagePyPathPrompt": "Voer een pad in naar manage.py ('${workspaceFolderToken}' verwijzen naar de root van de huidige werkruimtemap)", - "debug.djangoEnterManagePyPathInvalidFilePathError": "Voer een geldig Python-bestandspad in", - "debug.flaskEnterAppPathOrNamePathTitle": "Flask debuggen", - "debug.flaskEnterAppPathOrNamePathPrompt": "Voer een pad in naar een applicatie, bijvoorbeeld 'app.py' of 'app'", - "debug.flaskEnterAppPathOrNamePathInvalidNameError": "Voer een geldige naam in", - "debug.moduleEnterModuleTitle": "Module debuggen", - "debug.moduleEnterModulePrompt": "Voer Python module/package naam in", - "debug.moduleEnterModuleInvalidNameError": "Voer een geldige naam in", - "debug.pyramidEnterDevelopmentIniPathTitle": "Pyramid debuggen", - "debug.pyramidEnterDevelopmentIniPathPrompt": "`Voer een pad in naar development.ini ('${workspaceFolderToken}' verwijzen naar de root van de huidige werkruimtemap)`", - "debug.pyramidEnterDevelopmentIniPathInvalidFilePathError": "Voer een geldig bestandspad in", - "debug.attachRemotePortTitle": "Extern debuggen", - "debug.attachRemotePortPrompt": "Voer een port-nummer in", - "debug.attachRemotePortValidationError": "Voer een geldig port-nummer in", - "debug.attachRemoteHostTitle": "Extern debuggen", - "debug.attachRemoteHostPrompt": "Voer een hostname of IP-adres in", - "debug.attachRemoteHostValidationError": "Voer een geldige hostname of IP-adres in", - "Testing.testErrorDiagnosticMessage": "Error", - "Testing.testFailDiagnosticMessage": "Mislukt", - "Testing.testSkippedDiagnosticMessage": "Overgeslagen" -} diff --git a/package.nls.pl.json b/package.nls.pl.json deleted file mode 100644 index 0d9c2704edaa..000000000000 --- a/package.nls.pl.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "python.command.python.sortImports.title": "Sortuj importy", - "python.command.python.startREPL.title": "Uruchom REPL", - "python.command.python.createTerminal.title": "Otwórz Terminal", - "python.command.python.buildWorkspaceSymbols.title": "Zbuduj symbole dla przestrzeni roboczej", - "python.command.python.runtests.title": "Uruchom wszystkie testy jednostkowe", - "python.command.python.debugtests.title": "Debuguj wszystkie testy jednostkowe", - "python.command.python.execInTerminal.title": "Uruchom plik pythonowy w terminalu", - "python.command.python.setInterpreter.title": "Wybierz wersję interpretera", - "python.command.python.updateSparkLibrary.title": "Zaktualizuj biblioteki w przestrzeni roboczej PySpark", - "python.command.python.refactorExtractVariable.title": "Wyodrębnij zmienną", - "python.command.python.refactorExtractMethod.title": "Wyodrębnij metodę", - "python.command.python.viewOutput.title": "Pokaż wyniki", - "python.command.python.viewTestOutput.title": "Pokaż wyniki testów jednostkowych", - "python.command.python.selectAndRunTestMethod.title": "Uruchom metodę testów jednostkowych ...", - "python.command.python.selectAndDebugTestMethod.title": "Debuguj metodę testów jednostkowych ...", - "python.command.python.selectAndRunTestFile.title": "Uruchom plik z testami jednostkowymi ...", - "python.command.python.runCurrentTestFile.title": "Uruchom bieżący plik z testami jednostkowymi", - "python.command.python.runFailedTests.title": "Uruchom testy jednostkowe, które się nie powiodły", - "python.command.python.discoverTests.title": "Wyszukaj testy jednostkowe", - "python.command.python.configureTests.title": "Konfiguruj testy jednostkowe", - "python.command.python.execSelectionInTerminal.title": "Uruchom zaznaczony obszar w interpreterze Pythona", - "python.command.python.execSelectionInDjangoShell.title": "Uruchom zaznaczony obszar w powłoce Django", - "python.command.python.goToPythonObject.title": "Idź do obiektu pythonowego", - "python.command.python.setLinter.title": "Wybierz linter", - "python.command.python.enableLinting.title": "Włącz linting", - "python.command.python.runLinting.title": "Uruchom linting", - "python.command.python.datascience.runallcells.title": "Uruchom wszystkie komórki", - "python.command.python.datascience.runcurrentcell.title": "Uruchom bieżącą komórkę", - "python.command.python.datascience.runcurrentcelladvance.title": "Uruchom bieżące komórki i pokaż", - "python.command.python.datascience.execSelectionInteractive.title": "Uruchom zaznaczony obszar w oknie IPythona", - "python.command.python.datascience.runcell.title": "Uruchom komórki", - "python.command.python.datascience.showhistorypane.title": "Pokaż okno IPythona", - "python.command.python.datascience.selectjupyteruri.title": "Podaj identyfikator URI serwera Jupyter", - "python.command.python.datascience.importnotebook.title": "Importuj notatnik Jupyter", - "python.command.python.datascience.importnotebookonfile.title": "Importuj notatnik Jupyter", - "python.command.python.enableSourceMapSupport.title": "Włącz obsługę map źródłowych do debugowania rozszerzeń", - "python.command.python.datascience.exportoutputasnotebook.title": "Eksportuj okno IPython jako notatnik Jupyter", - "python.command.python.datascience.exportfileasnotebook.title": "Eksportuj bieżący plik Pythona jako notatnik Jupytera", - "python.command.python.datascience.exportfileandoutputasnotebook.title": "Eksportuj bieżący plik Pythona i jego wyniki jako notatnik Jupytera", - "python.command.python.datascience.undocells.title": "Cofnij ostatnią akcję IPythona", - "python.command.python.datascience.redocells.title": "Ponów ostatnią akcję IPythona", - "python.command.python.datascience.removeallcells.title": "Usuń wszystkie komórki IPythona", - "python.command.python.datascience.interruptkernel.title": "Przerwij IPython Kernel", - "python.command.python.datascience.restartkernel.title": "Restartuj IPython Kernel", - "python.command.python.datascience.expandallcells.title": "Rozwiń wszystkie komórki IPythona", - "python.command.python.datascience.collapseallcells.title": "Zwiń wszystkie komórki IPythona" -} diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json deleted file mode 100644 index cba1df0761b4..000000000000 --- a/package.nls.pt-br.json +++ /dev/null @@ -1,32 +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.module.label": "Python: Módulo", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Aplicação Pyramid", - "python.snippet.launch.attach.label": "Python: Anexar" -} diff --git a/package.nls.ru.json b/package.nls.ru.json deleted file mode 100644 index 95975e8b4e5f..000000000000 --- a/package.nls.ru.json +++ /dev/null @@ -1,34 +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.module.label": "Python: Модуль", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Приложение Pyramid", - "python.snippet.launch.attach.label": "Python: Подключить отладчик", - "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 983827d0ca6f..000000000000 --- a/package.nls.zh-cn.json +++ /dev/null @@ -1,32 +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.module.label": "Python: 模块", - "python.snippet.launch.django.label": "Python: Django", - "python.snippet.launch.flask.label": "Python: Flask", - "python.snippet.launch.pyramid.label": "Python: Pyramid 应用", - "python.snippet.launch.attach.label": "Python: 附加" -} diff --git a/package.nls.zh-tw.json b/package.nls.zh-tw.json deleted file mode 100644 index 45e813c5fff2..000000000000 --- a/package.nls.zh-tw.json +++ /dev/null @@ -1,32 +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.module.label": "Python:模組", - "python.snippet.launch.django.label": "Python:Django", - "python.snippet.launch.flask.label": "Python:Flask", - "python.snippet.launch.pyramid.label": "Python:Pyramid 程式", - "python.snippet.launch.attach.label": "Python:附加", - "python.command.python.discoverTests.title": "探索 Unit 測試項目" -} diff --git a/pythonExtensionApi/.eslintrc b/pythonExtensionApi/.eslintrc new file mode 100644 index 000000000000..8828c49002ed --- /dev/null +++ b/pythonExtensionApi/.eslintrc @@ -0,0 +1,11 @@ +{ + "overrides": [ + { + "files": ["**/main.d.ts"], + "rules": { + "@typescript-eslint/no-explicit-any": "off", + "padding-line-between-statements": ["error", { "blankLine": "always", "prev": "export", "next": "*" }] + } + } + ] +} diff --git a/pythonExtensionApi/.npmignore b/pythonExtensionApi/.npmignore new file mode 100644 index 000000000000..283d589ea5fe --- /dev/null +++ b/pythonExtensionApi/.npmignore @@ -0,0 +1,8 @@ +example/** +dist/ +out/**/*.map +out/**/*.tsbuildInfo +src/ +.eslintrc* +.eslintignore +tsconfig*.json diff --git a/pythonExtensionApi/LICENSE.md b/pythonExtensionApi/LICENSE.md new file mode 100644 index 000000000000..767f4076ba05 --- /dev/null +++ b/pythonExtensionApi/LICENSE.md @@ -0,0 +1,21 @@ +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED _AS IS_, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/pythonExtensionApi/README.md b/pythonExtensionApi/README.md new file mode 100644 index 000000000000..5208d90cdfa5 --- /dev/null +++ b/pythonExtensionApi/README.md @@ -0,0 +1,55 @@ +# Python extension's API + +This npm module implements an API facade for the Python extension in VS Code. + +## Example + +First we need to define a `package.json` for the extension that wants to use the API: + +```jsonc +{ + "name": "...", + ... + // depend on the Python extension + "extensionDependencies": [ + "ms-python.python" + ], + // Depend on the Python extension facade npm module to get easier API access to the + // core extension. + "dependencies": { + "@vscode/python-extension": "...", + "@types/vscode": "..." + }, +} +``` + +Update `"@types/vscode"` to [a recent version](https://code.visualstudio.com/updates/) of VS Code, say `"^1.81.0"` for VS Code version `"1.81"`, in case there are any conflicts. + +The actual source code to get the active environment to run some script could look like this: + +```typescript +// Import the API +import { PythonExtension } from '@vscode/python-extension'; + +... + +// Load the Python extension API +const pythonApi: PythonExtension = await PythonExtension.api(); + +// This will return something like /usr/bin/python +const environmentPath = pythonApi.environments.getActiveEnvironmentPath(); + +// `environmentPath.path` carries the value of the setting. Note that this path may point to a folder and not the +// python binary. Depends entirely on how the env was created. +// E.g., `conda create -n myenv python` ensures the env has a python binary +// `conda create -n myenv` does not include a python binary. +// Also, the path specified may not be valid, use the following to get complete details for this environment if +// need be. + +const environment = await pythonApi.environments.resolveEnvironment(environmentPath); +if (environment) { + // run your script here. +} +``` + +Check out [the wiki](https://aka.ms/pythonEnvironmentApi) for many more examples and usage. diff --git a/pythonExtensionApi/SECURITY.md b/pythonExtensionApi/SECURITY.md new file mode 100644 index 000000000000..a050f362c152 --- /dev/null +++ b/pythonExtensionApi/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/pythonExtensionApi/package-lock.json b/pythonExtensionApi/package-lock.json new file mode 100644 index 000000000000..e462fc1c888a --- /dev/null +++ b/pythonExtensionApi/package-lock.json @@ -0,0 +1,157 @@ +{ + "name": "@vscode/python-extension", + "version": "1.0.6", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@vscode/python-extension", + "version": "1.0.6", + "license": "MIT", + "devDependencies": { + "@types/vscode": "^1.93.0", + "source-map": "^0.8.0-beta.0", + "typescript": "~5.2" + }, + "engines": { + "node": ">=22.17.0", + "vscode": "^1.93.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + }, + "dependencies": { + "@types/vscode": { + "version": "1.94.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.94.0.tgz", + "integrity": "sha512-UyQOIUT0pb14XSqJskYnRwD2aG0QrPVefIfrW1djR+/J4KeFQ0i1+hjZoaAmeNf3Z2jleK+R2hv+EboG/m8ruw==", + "dev": true + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "requires": { + "whatwg-url": "^7.0.0" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "dev": true + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } + } +} diff --git a/pythonExtensionApi/package.json b/pythonExtensionApi/package.json new file mode 100644 index 000000000000..11e0445aa8da --- /dev/null +++ b/pythonExtensionApi/package.json @@ -0,0 +1,43 @@ +{ + "name": "@vscode/python-extension", + "description": "An API facade for the Python extension in VS Code", + "version": "1.0.6", + "author": { + "name": "Microsoft Corporation" + }, + "keywords": [ + "Python", + "VSCode", + "API" + ], + "main": "./out/main.js", + "types": "./out/main.d.ts", + "engines": { + "node": ">=22.21.1", + "vscode": "^1.93.0" + }, + "license": "MIT", + "homepage": "https://github.com/microsoft/vscode-python/tree/main/pythonExtensionApi", + "repository": { + "type": "git", + "url": "https://github.com/Microsoft/vscode-python" + }, + "bugs": { + "url": "https://github.com/Microsoft/vscode-python/issues" + }, + "devDependencies": { + "typescript": "~5.2", + "@types/vscode": "^1.102.0", + "source-map": "^0.8.0-beta.0" + }, + "scripts": { + "prepublishOnly": "echo \"⛔ Can only publish from a secure pipeline ⛔\" && node ../build/fail", + "prepack": "npm run all:publish", + "compile": "node ./node_modules/typescript/lib/tsc.js -b ./tsconfig.json", + "clean": "node ../node_modules/rimraf/bin.js out", + "lint": "node ../node_modules/eslint/bin/eslint.js --ext ts src", + "all": "npm run clean && npm run compile", + "formatTypings": "node ../node_modules/eslint/bin/eslint.js --fix ./out/main.d.ts", + "all:publish": "git clean -xfd . && npm install && npm run compile && npm run formatTypings" + } +} diff --git a/pythonExtensionApi/src/main.ts b/pythonExtensionApi/src/main.ts new file mode 100644 index 000000000000..2173245cbb28 --- /dev/null +++ b/pythonExtensionApi/src/main.ts @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Event, Uri, WorkspaceFolder, extensions } from 'vscode'; + +/* + * Do not introduce any breaking changes to this API. + * This is the public API for other extensions to interact with this extension. + */ +export interface PythonExtension { + /** + * Promise indicating whether all parts of the extension have completed loading or not. + */ + ready: Promise; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Resource the environment changed for. + */ + readonly resource: Resource | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/pythonExtensionApi/tsconfig.json b/pythonExtensionApi/tsconfig.json new file mode 100644 index 000000000000..9ab7617023df --- /dev/null +++ b/pythonExtensionApi/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": ["types/*"] + }, + "module": "commonjs", + "target": "es2018", + "outDir": "./out", + "lib": [ + "es6", + "es2018", + "dom", + "ES2019", + "ES2020" + ], + "sourceMap": true, + "rootDir": "src", + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "declaration": true + }, + "exclude": [ + "node_modules", + "out" + ] +} diff --git a/pythonFiles/completion.py b/pythonFiles/completion.py deleted file mode 100644 index 99a23c6ed555..000000000000 --- a/pythonFiles/completion.py +++ /dev/null @@ -1,655 +0,0 @@ -import os -import os.path -import io -import re -import sys -import json -import traceback -import platform - -jediPreview = False - -class RedirectStdout(object): - def __init__(self, new_stdout=None): - """If stdout is None, redirect to /dev/null""" - self._new_stdout = new_stdout or open(os.devnull, 'w') - - def __enter__(self): - sys.stdout.flush() - self.oldstdout_fno = os.dup(sys.stdout.fileno()) - os.dup2(self._new_stdout.fileno(), 1) - - def __exit__(self, exc_type, exc_value, traceback): - self._new_stdout.flush() - os.dup2(self.oldstdout_fno, 1) - os.close(self.oldstdout_fno) - -class JediCompletion(object): - basic_types = { - 'module': 'import', - 'instance': 'variable', - 'statement': 'value', - 'param': 'variable', - } - - def __init__(self): - self.default_sys_path = sys.path - self._input = io.open(sys.stdin.fileno(), encoding='utf-8') - if (os.path.sep == '/') and (platform.uname()[2].find('Microsoft') > -1): - # WSL; does not support UNC paths - self.drive_mount = '/mnt/' - elif sys.platform == 'cygwin': - # cygwin - self.drive_mount = '/cygdrive/' - else: - # Do no normalization, e.g. Windows build of Python. - # Could add additional test: ((os.path.sep == '/') and os.path.isdir('/mnt/c')) - # However, this may have more false positives trying to identify Windows/*nix hybrids - self.drive_mount = '' - - def _get_definition_type(self, definition): - # if definition.type not in ['import', 'keyword'] and is_built_in(): - # return 'builtin' - try: - if definition.type in ['statement'] and definition.name.isupper(): - return 'constant' - return self.basic_types.get(definition.type, definition.type) - except Exception: - return 'builtin' - - def _additional_info(self, completion): - """Provide additional information about the completion object.""" - if not hasattr(completion, '_definition') or completion._definition is None: - return '' - if completion.type == 'statement': - nodes_to_display = ['InstanceElement', 'String', 'Node', 'Lambda', - 'Number'] - return ''.join(c.get_code() for c in - completion._definition.children if type(c).__name__ - in nodes_to_display).replace('\n', '') - return '' - - @classmethod - def _get_top_level_module(cls, path): - """Recursively walk through directories looking for top level module. - - Jedi will use current filepath to look for another modules at same - path, but it will not be able to see modules **above**, so our goal - is to find the higher python module available from filepath. - """ - _path, _ = os.path.split(path) - if os.path.isfile(os.path.join(_path, '__init__.py')): - return cls._get_top_level_module(_path) - return path - - def _generate_signature(self, completion): - """Generate signature with function arguments. - """ - if completion.type in ['module'] or not hasattr(completion, 'params'): - return '' - return '%s(%s)' % ( - completion.name, - ', '.join(p.description[6:] for p in completion.params if p)) - - def _get_call_signatures(self, script): - """Extract call signatures from jedi.api.Script object in failsafe way. - - Returns: - Tuple with original signature object, name and value. - """ - _signatures = [] - try: - call_signatures = script.call_signatures() - except KeyError: - call_signatures = [] - except : - call_signatures = [] - for signature in call_signatures: - for pos, param in enumerate(signature.params): - if not param.name: - continue - - name = self._get_param_name(param) - if param.name == 'self' and pos == 0: - continue - if name.startswith('*'): - continue - - value = self._get_param_value(param) - _signatures.append((signature, name, value)) - return _signatures - - def _get_param_name(self, p): - if(p.name.startswith('param ')): - return p.name[6:] # drop leading 'param ' - return p.name - - def _get_param_value(self, p): - pair = p.description.split('=') - if(len(pair) > 1): - return pair[1] - return None - - def _get_call_signatures_with_args(self, script): - """Extract call signatures from jedi.api.Script object in failsafe way. - - Returns: - Array with dictionary - """ - _signatures = [] - try: - call_signatures = script.call_signatures() - except KeyError: - call_signatures = [] - for signature in call_signatures: - sig = {"name": "", "description": "", "docstring": "", - "paramindex": 0, "params": [], "bracketstart": []} - sig["description"] = signature.description - try: - sig["docstring"] = signature.docstring() - sig["raw_docstring"] = signature.docstring(raw=True) - except Exception: - sig["docstring"] = '' - sig["raw_docstring"] = '' - - sig["name"] = signature.name - sig["paramindex"] = signature.index - sig["bracketstart"].append(signature.index) - - _signatures.append(sig) - for pos, param in enumerate(signature.params): - if not param.name: - continue - - name = self._get_param_name(param) - if param.name == 'self' and pos == 0: - continue - - value = self._get_param_value(param) - paramDocstring = '' - try: - paramDocstring = param.docstring() - except Exception: - paramDocstring = '' - - sig["params"].append({"name": name, "value": value, "docstring": paramDocstring, "description": param.description}) - return _signatures - - def _serialize_completions(self, script, identifier=None, prefix=''): - """Serialize response to be read from VSCode. - - Args: - script: Instance of jedi.api.Script object. - identifier: Unique completion identifier to pass back to VSCode. - prefix: String with prefix to filter function arguments. - Used only when fuzzy matcher turned off. - - Returns: - Serialized string to send to VSCode. - """ - _completions = [] - - for signature, name, value in self._get_call_signatures(script): - if not self.fuzzy_matcher and not name.lower().startswith( - prefix.lower()): - continue - _completion = { - 'type': 'property', - 'raw_type': '', - 'rightLabel': self._additional_info(signature) - } - _completion['description'] = '' - _completion['raw_docstring'] = '' - - # we pass 'text' here only for fuzzy matcher - if value: - _completion['snippet'] = '%s=${1:%s}$0' % (name, value) - _completion['text'] = '%s=' % (name) - else: - _completion['snippet'] = '%s=$1$0' % name - _completion['text'] = name - _completion['displayText'] = name - _completions.append(_completion) - - try: - completions = script.completions() - except KeyError: - completions = [] - except : - completions = [] - for completion in completions: - try: - _completion = { - 'text': completion.name, - 'type': self._get_definition_type(completion), - 'raw_type': completion.type, - 'rightLabel': self._additional_info(completion) - } - except Exception: - continue - - for c in _completions: - if c['text'] == _completion['text']: - c['type'] = _completion['type'] - c['raw_type'] = _completion['raw_type'] - - if any([c['text'].split('=')[0] == _completion['text'] - for c in _completions]): - # ignore function arguments we already have - continue - _completions.append(_completion) - return json.dumps({'id': identifier, 'results': _completions}) - - def _serialize_methods(self, script, identifier=None, prefix=''): - _methods = [] - try: - completions = script.completions() - except KeyError: - return [] - - for completion in completions: - if completion.name == '__autocomplete_python': - instance = completion.parent().name - break - else: - instance = 'self.__class__' - - for completion in completions: - params = [] - if hasattr(completion, 'params'): - params = [p.description for p in completion.params if p] - if completion.parent().type == 'class': - _methods.append({ - 'parent': completion.parent().name, - 'instance': instance, - 'name': completion.name, - 'params': params, - 'moduleName': completion.module_name, - 'fileName': completion.module_path, - 'line': completion.line, - 'column': completion.column, - }) - return json.dumps({'id': identifier, 'results': _methods}) - - def _serialize_arguments(self, script, identifier=None): - """Serialize response to be read from VSCode. - - Args: - script: Instance of jedi.api.Script object. - identifier: Unique completion identifier to pass back to VSCode. - - Returns: - Serialized string to send to VSCode. - """ - return json.dumps({"id": identifier, "results": self._get_call_signatures_with_args(script)}) - - def _top_definition(self, definition): - for d in definition.goto_assignments(): - if d == definition: - continue - if d.type == 'import': - return self._top_definition(d) - else: - return d - return definition - - def _extract_range_jedi_0_11_1(self, definition): - from parso.utils import split_lines - # get the scope range - try: - if definition.type in ['class', 'function']: - tree_name = definition._name.tree_name - scope = tree_name.get_definition() - start_line = scope.start_pos[0] - 1 - start_column = scope.start_pos[1] - # get the lines - code = scope.get_code(include_prefix=False) - lines = split_lines(code) - # trim the lines - lines = '\n'.join(lines).rstrip().split('\n') - end_line = start_line + len(lines) - 1 - end_column = len(lines[-1]) - 1 - else: - symbol = definition._name.tree_name - start_line = symbol.start_pos[0] - 1 - start_column = symbol.start_pos[1] - end_line = symbol.end_pos[0] - 1 - end_column = symbol.end_pos[1] - return { - 'start_line': start_line, - 'start_column': start_column, - 'end_line': end_line, - 'end_column': end_column - } - except Exception as e: - return { - 'start_line': definition.line - 1, - 'start_column': definition.column, - 'end_line': definition.line - 1, - 'end_column': definition.column - } - - def _extract_range(self, definition): - """Provides the definition range of a given definition - - For regular symbols it returns the start and end location of the - characters making up the symbol. - - For scoped containers it will return the entire definition of the - scope. - - The scope that jedi provides ends with the first character of the next - scope so it's not ideal. For vscode we need the scope to end with the - last character of actual code. That's why we extract the lines that - make up our scope and trim the trailing whitespace. - """ - return self._extract_range_jedi_0_11_1(definition) - - def _get_definitionsx(self, definitions, identifier=None, ignoreNoModulePath=False): - """Serialize response to be read from VSCode. - - Args: - definitions: List of jedi.api.classes.Definition objects. - identifier: Unique completion identifier to pass back to VSCode. - - Returns: - Serialized string to send to VSCode. - """ - _definitions = [] - for definition in definitions: - try: - if definition.type == 'import': - definition = self._top_definition(definition) - definitionRange = { - 'start_line': 0, - 'start_column': 0, - 'end_line': 0, - 'end_column': 0 - } - module_path = '' - if hasattr(definition, 'module_path') and definition.module_path: - module_path = definition.module_path - definitionRange = self._extract_range(definition) - else: - if not ignoreNoModulePath: - continue - try: - parent = definition.parent() - container = parent.name if parent.type != 'module' else '' - except Exception: - container = '' - - try: - docstring = definition.docstring() - rawdocstring = definition.docstring(raw=True) - except Exception: - docstring = '' - rawdocstring = '' - _definition = { - 'text': definition.name, - 'type': self._get_definition_type(definition), - 'raw_type': definition.type, - 'fileName': module_path, - 'container': container, - 'range': definitionRange, - 'description': definition.description, - 'docstring': docstring, - 'raw_docstring': rawdocstring, - 'signature': self._generate_signature(definition) - } - _definitions.append(_definition) - except Exception as e: - pass - return _definitions - - def _serialize_definitions(self, definitions, identifier=None): - """Serialize response to be read from VSCode. - - Args: - definitions: List of jedi.api.classes.Definition objects. - identifier: Unique completion identifier to pass back to VSCode. - - Returns: - Serialized string to send to VSCode. - """ - _definitions = [] - for definition in definitions: - try: - if definition.module_path: - if definition.type == 'import': - definition = self._top_definition(definition) - if not definition.module_path: - continue - try: - parent = definition.parent() - container = parent.name if parent.type != 'module' else '' - except Exception: - container = '' - - try: - docstring = definition.docstring() - rawdocstring = definition.docstring(raw=True) - except Exception: - docstring = '' - rawdocstring = '' - _definition = { - 'text': definition.name, - 'type': self._get_definition_type(definition), - 'raw_type': definition.type, - 'fileName': definition.module_path, - 'container': container, - 'range': self._extract_range(definition), - 'description': definition.description, - 'docstring': docstring, - 'raw_docstring': rawdocstring - } - _definitions.append(_definition) - except Exception as e: - pass - return json.dumps({'id': identifier, 'results': _definitions}) - - def _serialize_tooltip(self, definitions, identifier=None): - _definitions = [] - for definition in definitions: - signature = definition.name - description = None - if definition.type in ['class', 'function']: - signature = self._generate_signature(definition) - try: - description = definition.docstring(raw=True).strip() - except Exception: - description = '' - if not description and not hasattr(definition, 'get_line_code'): - # jedi returns an empty string for compiled objects - description = definition.docstring().strip() - if definition.type == 'module': - signature = definition.full_name - try: - description = definition.docstring(raw=True).strip() - except Exception: - description = '' - if not description and hasattr(definition, 'get_line_code'): - # jedi returns an empty string for compiled objects - description = definition.docstring().strip() - _definition = { - 'type': self._get_definition_type(definition), - 'text': definition.name, - 'description': description, - 'docstring': description, - 'signature': signature - } - _definitions.append(_definition) - return json.dumps({'id': identifier, 'results': _definitions}) - - def _serialize_usages(self, usages, identifier=None): - _usages = [] - for usage in usages: - _usages.append({ - 'name': usage.name, - 'moduleName': usage.module_name, - 'fileName': usage.module_path, - 'line': usage.line, - 'column': usage.column, - }) - return json.dumps({'id': identifier, 'results': _usages}) - - def _deserialize(self, request): - """Deserialize request from VSCode. - - Args: - request: String with raw request from VSCode. - - Returns: - Python dictionary with request data. - """ - return json.loads(request) - - def _set_request_config(self, config): - """Sets config values for current request. - - This includes sys.path modifications which is getting restored to - default value on each request so each project should be isolated - from each other. - - Args: - config: Dictionary with config values. - """ - sys.path = self.default_sys_path - self.use_snippets = config.get('useSnippets') - self.show_doc_strings = config.get('showDescriptions', True) - self.fuzzy_matcher = config.get('fuzzyMatcher', False) - jedi.settings.case_insensitive_completion = config.get( - 'caseInsensitiveCompletion', True) - for path in config.get('extraPaths', []): - if path and path not in sys.path: - sys.path.insert(0, path) - - def _normalize_request_path(self, request): - """Normalize any Windows paths received by a *nix build of - Python. Does not alter the reverse os.path.sep=='\\', - i.e. *nix paths received by a Windows build of Python. - """ - if 'path' in request: - if not self.drive_mount: - return - newPath = request['path'].replace('\\', '/') - if newPath[0:1] == '/': - # is absolute path with no drive letter - request['path'] = newPath - elif newPath[1:2] == ':': - # is path with drive letter, only absolute can be mapped - request['path'] = self.drive_mount + newPath[0:1].lower() + newPath[2:] - else: - # is relative path - request['path'] = newPath - - def _process_request(self, request): - """Accept serialized request from VSCode and write response. - """ - request = self._deserialize(request) - - self._set_request_config(request.get('config', {})) - - self._normalize_request_path(request) - path = self._get_top_level_module(request.get('path', '')) - if len(path) > 0 and path not in sys.path: - sys.path.insert(0, path) - lookup = request.get('lookup', 'completions') - - if lookup == 'names': - return self._serialize_definitions( - jedi.api.names( - source=request.get('source', None), - path=request.get('path', ''), - all_scopes=True, - ), - request['id']) - - script = jedi.Script( - source=request.get('source', None), - line=request['line'] + 1, - column=request['column'], - path=request.get('path', ''), - sys_path=sys.path, - ) - - if lookup == 'definitions': - defs = self._get_definitionsx(script.goto_assignments(follow_imports=True), request['id']) - return json.dumps({'id': request['id'], 'results': defs}) - if lookup == 'tooltip': - if jediPreview: - defs = [] - try: - defs = self._get_definitionsx(script.goto_definitions(), request['id'], True) - except: - pass - try: - if len(defs) == 0: - defs = self._get_definitionsx(script.goto_assignments(), request['id'], True) - except: - pass - return json.dumps({'id': request['id'], 'results': defs}) - else: - try: - return self._serialize_tooltip(script.goto_definitions(), request['id']) - except: - return json.dumps({'id': request['id'], 'results': []}) - elif lookup == 'arguments': - return self._serialize_arguments( - script, request['id']) - elif lookup == 'usages': - return self._serialize_usages( - script.usages(), request['id']) - elif lookup == 'methods': - return self._serialize_methods(script, request['id'], - request.get('prefix', '')) - else: - return self._serialize_completions(script, request['id'], - request.get('prefix', '')) - - def _write_response(self, response): - sys.stdout.write(response + '\n') - sys.stdout.flush() - - def watch(self): - while True: - try: - rq = self._input.readline() - if len(rq) == 0: - # Reached EOF - indication our parent process is gone. - sys.stderr.write('Received EOF from the standard input,exiting' + '\n') - sys.stderr.flush() - return - with RedirectStdout(): - response = self._process_request(rq) - self._write_response(response) - - except Exception: - sys.stderr.write(traceback.format_exc() + '\n') - sys.stderr.flush() - -if __name__ == '__main__': - cachePrefix = 'v' - modulesToLoad = '' - if len(sys.argv) > 2 and sys.argv[1] == 'custom': - jediPath = sys.argv[2] - jediPreview = True - cachePrefix = 'custom_v' - if len(sys.argv) > 3: - modulesToLoad = sys.argv[3] - else: - #release - jediPath = os.path.join(os.path.dirname(__file__), 'lib', 'python') - if len(sys.argv) > 1: - modulesToLoad = sys.argv[1] - - sys.path.insert(0, jediPath) - import jedi - if jediPreview: - jedi.settings.cache_directory = os.path.join( - jedi.settings.cache_directory, cachePrefix + jedi.__version__.replace('.', '')) - # remove jedi from path after we import it so it will not be completed - sys.path.pop(0) - if len(modulesToLoad) > 0: - jedi.preload_module(*modulesToLoad.split(',')) - JediCompletion().watch() diff --git a/pythonFiles/datascience/dummyJupyter.py b/pythonFiles/datascience/dummyJupyter.py deleted file mode 100644 index 70abef2d244e..000000000000 --- a/pythonFiles/datascience/dummyJupyter.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file can mimic juypter running. Useful for testing jupyter crash handling - -import sys -import argparse -import time - -def main(): - print('hello from dummy jupyter') - parser = argparse.ArgumentParser() - parser.add_argument('--version', type=bool, default=False, const=True, nargs='?') - parser.add_argument('notebook', type=bool, default=False, const=True, nargs='?') - parser.add_argument('--no-browser', type=bool, default=False, const=True, nargs='?') - parser.add_argument('--notebook-dir', default='') - parser.add_argument('--config', default='') - results = parser.parse_args() - if (results.version): - print('1.1.dummy') - else: - print('http://localhost:8888/?token=012f08663a68e279fe0a5335e0b5dfe44759ddcccf0b3a56') - time.sleep(5) - raise Exception('Dummy is dead') - - -if __name__ == '__main__': - main() diff --git a/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py b/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py deleted file mode 100644 index 318100e946ca..000000000000 --- a/pythonFiles/datascience/getJupyterVariableDataFrameInfo.py +++ /dev/null @@ -1,104 +0,0 @@ -# Query Jupyter server for the info about a dataframe -import json as _VSCODE_json -import pandas as _VSCODE_pd -import pandas.io.json as _VSCODE_pd_json - -# _VSCode_sub_supportsDataExplorer will contain our list of data explorer supported types -_VSCode_supportsDataExplorer = "['list', 'Series', 'dict', 'ndarray', 'DataFrame']" - -# In IJupyterVariables.getValue this '_VSCode_JupyterTestValue' will be replaced with the json stringified value of the target variable -# Indexes off of _VSCODE_targetVariable need to index types that are part of IJupyterVariable -_VSCODE_targetVariable = _VSCODE_json.loads('_VSCode_JupyterTestValue') - -# First check to see if we are a supported type, this prevents us from adding types that are not supported -# and also keeps our types in sync with what the variable explorer says that we support -if _VSCODE_targetVariable['type'] not in _VSCode_supportsDataExplorer: - del _VSCode_supportsDataExplorer - print(_VSCODE_json.dumps(_VSCODE_targetVariable)) - del _VSCODE_targetVariable -else: - del _VSCode_supportsDataExplorer - _VSCODE_evalResult = eval(_VSCODE_targetVariable['name']) - - # Figure out shape if not already there. Use the shape to compute the row count - if (hasattr(_VSCODE_evalResult, 'shape')): - try: - # Get a bit more restrictive with exactly what we want to count as a shape, since anything can define it - if isinstance(_VSCODE_evalResult.shape, tuple): - _VSCODE_targetVariable['rowCount'] = _VSCODE_evalResult.shape[0] - except TypeError: - _VSCODE_targetVariable['rowCount'] = 0 - elif (hasattr(_VSCODE_evalResult, '__len__')): - try: - _VSCODE_targetVariable['rowCount'] = len(_VSCODE_evalResult) - except TypeError: - _VSCODE_targetVariable['rowCount'] = 0 - - # Turn the eval result into a df - _VSCODE_df = _VSCODE_evalResult - if isinstance(_VSCODE_evalResult, list): - _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) - elif isinstance(_VSCODE_evalResult, _VSCODE_pd.Series): - _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) - elif isinstance(_VSCODE_evalResult, dict): - _VSCODE_evalResult = _VSCODE_pd.Series(_VSCODE_evalResult) - _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) - elif _VSCODE_targetVariable['type'] == 'ndarray': - _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) - - # If any rows, use pandas json to convert a single row to json. Extract - # the column names and types from the json so we match what we'll fetch when - # we ask for all of the rows - if _VSCODE_targetVariable['rowCount']: - try: - _VSCODE_row = _VSCODE_df.iloc[0:1] - _VSCODE_json_row = _VSCODE_pd_json.to_json(None, _VSCODE_row, date_format='iso') - _VSCODE_columnNames = list(_VSCODE_json.loads(_VSCODE_json_row)) - del _VSCODE_row - del _VSCODE_json_row - except: - _VSCODE_columnNames = list(_VSCODE_df) - else: - _VSCODE_columnNames = list(_VSCODE_df) - - # Compute the index column. It may have been renamed - _VSCODE_indexColumn = _VSCODE_df.index.name if _VSCODE_df.index.name else 'index' - _VSCODE_columnTypes = list(_VSCODE_df.dtypes) - del _VSCODE_df - - # Make sure the index column exists - if _VSCODE_indexColumn not in _VSCODE_columnNames: - _VSCODE_columnNames.insert(0, _VSCODE_indexColumn) - _VSCODE_columnTypes.insert(0, 'int64') - - # Then loop and generate our output json - _VSCODE_columns = [] - for _VSCODE_n in range(0, len(_VSCODE_columnNames)): - _VSCODE_column_type = _VSCODE_columnTypes[_VSCODE_n] - _VSCODE_column_name = str(_VSCODE_columnNames[_VSCODE_n]) - _VSCODE_colobj = {} - _VSCODE_colobj['key'] = _VSCODE_column_name - _VSCODE_colobj['name'] = _VSCODE_column_name - _VSCODE_colobj['type'] = str(_VSCODE_column_type) - _VSCODE_columns.append(_VSCODE_colobj) - del _VSCODE_column_name - del _VSCODE_column_type - - del _VSCODE_columnNames - del _VSCODE_columnTypes - - # Save this in our target - _VSCODE_targetVariable['columns'] = _VSCODE_columns - _VSCODE_targetVariable['indexColumn'] = _VSCODE_indexColumn - del _VSCODE_columns - del _VSCODE_indexColumn - - - # Transform this back into a string - print(_VSCODE_json.dumps(_VSCODE_targetVariable)) - del _VSCODE_targetVariable - - # Cleanup imports - del _VSCODE_json - del _VSCODE_pd - del _VSCODE_pd_json \ No newline at end of file diff --git a/pythonFiles/datascience/getJupyterVariableDataFrameRows.py b/pythonFiles/datascience/getJupyterVariableDataFrameRows.py deleted file mode 100644 index 900e0d6da737..000000000000 --- a/pythonFiles/datascience/getJupyterVariableDataFrameRows.py +++ /dev/null @@ -1,44 +0,0 @@ -# Query Jupyter server for the rows of a data frame -import json as _VSCODE_json -import pandas as _VSCODE_pd -import pandas.io.json as _VSCODE_pd_json - -# In IJupyterVariables.getValue this '_VSCode_JupyterTestValue' will be replaced with the json stringified value of the target variable -# Indexes off of _VSCODE_targetVariable need to index types that are part of IJupyterVariable -_VSCODE_targetVariable = _VSCODE_json.loads('_VSCode_JupyterTestValue') -_VSCODE_evalResult = eval(_VSCODE_targetVariable['name']) - -# _VSCode_JupyterStartRow and _VSCode_JupyterEndRow should be replaced dynamically with the literals -# for our start and end rows -_VSCODE_startRow = max(_VSCode_JupyterStartRow, 0) -_VSCODE_endRow = min(_VSCode_JupyterEndRow, _VSCODE_targetVariable['rowCount']) - -# Assume we have a dataframe. If not, turn our eval result into a dataframe -_VSCODE_df = _VSCODE_evalResult -if isinstance(_VSCODE_evalResult, list): - _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) -elif isinstance(_VSCODE_evalResult, _VSCODE_pd.Series): - _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) -elif isinstance(_VSCODE_evalResult, dict): - _VSCODE_evalResult = _VSCODE_pd.Series(_VSCODE_evalResult) - _VSCODE_df = _VSCODE_pd.Series.to_frame(_VSCODE_evalResult) -elif _VSCODE_targetVariable['type'] == 'ndarray': - _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) -# If not a known type, then just let pandas handle it. -elif not (hasattr(_VSCODE_df, 'iloc')): - _VSCODE_df = _VSCODE_pd.DataFrame(_VSCODE_evalResult) - -# Turn into JSON using pandas. We use pandas because it's about 3 orders of magnitude faster to turn into JSON -_VSCODE_rows = _VSCODE_df.iloc[_VSCODE_startRow:_VSCODE_endRow] -_VSCODE_result = _VSCODE_pd_json.to_json(None, _VSCODE_rows, orient='table', date_format='iso') -print(_VSCODE_result) - -# Cleanup our variables -del _VSCODE_df -del _VSCODE_endRow -del _VSCODE_startRow -del _VSCODE_rows -del _VSCODE_result -del _VSCODE_json -del _VSCODE_pd -del _VSCODE_pd_json \ No newline at end of file diff --git a/pythonFiles/datascience/getJupyterVariableList.py b/pythonFiles/datascience/getJupyterVariableList.py deleted file mode 100644 index 24cfb4615e3f..000000000000 --- a/pythonFiles/datascience/getJupyterVariableList.py +++ /dev/null @@ -1,30 +0,0 @@ -# Query Jupyter server for defined variables list -# Tested on 2.7 and 3.6 -from sys import getsizeof as _VSCODE_getsizeof -import json as _VSCODE_json -from IPython import get_ipython as _VSCODE_get_ipython - -# _VSCode_supportsDataExplorer will contain our list of data explorer supported types -_VSCode_supportsDataExplorer = "['list', 'Series', 'dict', 'ndarray', 'DataFrame']" - -# who_ls is a Jupyter line magic to fetch currently defined vars -_VSCode_JupyterVars = _VSCODE_get_ipython().run_line_magic('who_ls', '') - -_VSCode_output = [] -for _VSCode_var in _VSCode_JupyterVars: - try: - _VSCode_type = type(eval(_VSCode_var)) - _VSCode_output.append({'name': _VSCode_var, 'type': _VSCode_type.__name__, 'size': _VSCODE_getsizeof(_VSCode_var), 'supportsDataExplorer': _VSCode_type.__name__ in _VSCode_supportsDataExplorer }) - del _VSCode_type - del _VSCode_var - except: - pass - -print(_VSCODE_json.dumps(_VSCode_output)) - -del _VSCODE_get_ipython -del _VSCode_output -del _VSCode_supportsDataExplorer -del _VSCode_JupyterVars -del _VSCODE_json -del _VSCODE_getsizeof diff --git a/pythonFiles/datascience/getJupyterVariableValue.py b/pythonFiles/datascience/getJupyterVariableValue.py deleted file mode 100644 index d483eb5cba89..000000000000 --- a/pythonFiles/datascience/getJupyterVariableValue.py +++ /dev/null @@ -1,435 +0,0 @@ -import sys as VC_sys -import locale as VC_locale - -VC_IS_PY2 = VC_sys.version_info < (3,) - -# SafeRepr based on the pydevd implementation -# https://github.com/microsoft/ptvsd/blob/master/src/ptvsd/_vendored/pydevd/_pydevd_bundle/pydevd_safe_repr.py -class VC_SafeRepr(object): - # Py3 compat - alias unicode to str, and xrange to range - try: - unicode # noqa - except NameError: - unicode = str - try: - xrange # noqa - except NameError: - xrange = range - - # Can be used to override the encoding from locale.getpreferredencoding() - locale_preferred_encoding = None - - # Can be used to override the encoding used for sys.stdout.encoding - sys_stdout_encoding = None - - # String types are truncated to maxstring_outer when at the outer- - # most level, and truncated to maxstring_inner characters inside - # collections. - maxstring_outer = 2 ** 16 - maxstring_inner = 30 - if not VC_IS_PY2: - string_types = (str, bytes) - set_info = (set, '{', '}', False) - frozenset_info = (frozenset, 'frozenset({', '})', False) - int_types = (int,) - long_iter_types = (list, tuple, bytearray, range, - dict, set, frozenset) - else: - string_types = (str, unicode) - set_info = (set, 'set([', '])', False) - frozenset_info = (frozenset, 'frozenset([', '])', False) - int_types = (int, long) # noqa - long_iter_types = (list, tuple, bytearray, xrange, - dict, set, frozenset, buffer) # noqa - - # Collection types are recursively iterated for each limit in - # maxcollection. - maxcollection = (15, 10) - - # Specifies type, prefix string, suffix string, and whether to include a - # comma if there is only one element. (Using a sequence rather than a - # mapping because we use isinstance() to determine the matching type.) - collection_types = [ - (tuple, '(', ')', True), - (list, '[', ']', False), - frozenset_info, - set_info, - ] - try: - from collections import deque - collection_types.append((deque, 'deque([', '])', False)) - except Exception: - pass - - # type, prefix string, suffix string, item prefix string, - # item key/value separator, item suffix string - dict_types = [(dict, '{', '}', '', ': ', '')] - try: - from collections import OrderedDict - dict_types.append((OrderedDict, 'OrderedDict([', '])', '(', ', ', ')')) - except Exception: - pass - - # All other types are treated identically to strings, but using - # different limits. - maxother_outer = 2 ** 16 - maxother_inner = 30 - - convert_to_hex = False - raw_value = False - - def __call__(self, obj): - try: - if VC_IS_PY2: - return ''.join((x.encode('utf-8') if isinstance(x, unicode) else x) for x in self._repr(obj, 0)) - else: - return ''.join(self._repr(obj, 0)) - except Exception: - try: - return 'An exception was raised: %r' % sys.exc_info()[1] - except Exception: - return 'An exception was raised' - - def _repr(self, obj, level): - '''Returns an iterable of the parts in the final repr string.''' - - try: - obj_repr = type(obj).__repr__ - except Exception: - obj_repr = None - - def has_obj_repr(t): - r = t.__repr__ - try: - return obj_repr == r - except Exception: - return obj_repr is r - - for t, prefix, suffix, comma in self.collection_types: - if isinstance(obj, t) and has_obj_repr(t): - return self._repr_iter(obj, level, prefix, suffix, comma) - - for t, prefix, suffix, item_prefix, item_sep, item_suffix in self.dict_types: # noqa - if isinstance(obj, t) and has_obj_repr(t): - return self._repr_dict(obj, level, prefix, suffix, - item_prefix, item_sep, item_suffix) - - for t in self.string_types: - if isinstance(obj, t) and has_obj_repr(t): - return self._repr_str(obj, level) - - if self._is_long_iter(obj): - return self._repr_long_iter(obj) - - return self._repr_other(obj, level) - - # Determines whether an iterable exceeds the limits set in - # maxlimits, and is therefore unsafe to repr(). - def _is_long_iter(self, obj, level=0): - try: - # Strings have their own limits (and do not nest). Because - # they don't have __iter__ in 2.x, this check goes before - # the next one. - if isinstance(obj, self.string_types): - return len(obj) > self.maxstring_inner - - # If it's not an iterable (and not a string), it's fine. - if not hasattr(obj, '__iter__'): - return False - - # If it's not an instance of these collection types then it - # is fine. Note: this is a fix for - # https://github.com/Microsoft/ptvsd/issues/406 - if not isinstance(obj, self.long_iter_types): - return False - - # Iterable is its own iterator - this is a one-off iterable - # like generator or enumerate(). We can't really count that, - # but repr() for these should not include any elements anyway, - # so we can treat it the same as non-iterables. - if obj is iter(obj): - return False - - # xrange reprs fine regardless of length. - if isinstance(obj, xrange): - return False - - # numpy and scipy collections (ndarray etc) have - # self-truncating repr, so they're always safe. - try: - module = type(obj).__module__.partition('.')[0] - if module in ('numpy', 'scipy'): - return False - except Exception: - pass - - # Iterables that nest too deep are considered long. - if level >= len(self.maxcollection): - return True - - # It is too long if the length exceeds the limit, or any - # of its elements are long iterables. - if hasattr(obj, '__len__'): - try: - size = len(obj) - except Exception: - size = None - if size is not None and size > self.maxcollection[level]: - return True - return any((self._is_long_iter(item, level + 1) for item in obj)) # noqa - return any(i > self.maxcollection[level] or self._is_long_iter(item, level + 1) for i, item in enumerate(obj)) # noqa - - except Exception: - # If anything breaks, assume the worst case. - return True - - def _repr_iter(self, obj, level, prefix, suffix, - comma_after_single_element=False): - yield prefix - - if level >= len(self.maxcollection): - yield '...' - else: - count = self.maxcollection[level] - yield_comma = False - for item in obj: - if yield_comma: - yield ', ' - yield_comma = True - - count -= 1 - if count <= 0: - yield '...' - break - - for p in self._repr(item, 100 if item is obj else level + 1): - yield p - else: - if comma_after_single_element: - if count == self.maxcollection[level] - 1: - yield ',' - yield suffix - - def _repr_long_iter(self, obj): - try: - length = hex(len(obj)) if self.convert_to_hex else len(obj) - obj_repr = '<%s, len() = %s>' % (type(obj).__name__, length) - except Exception: - try: - obj_repr = '<' + type(obj).__name__ + '>' - except Exception: - obj_repr = '' - yield obj_repr - - def _repr_dict(self, obj, level, prefix, suffix, - item_prefix, item_sep, item_suffix): - if not obj: - yield prefix + suffix - return - if level >= len(self.maxcollection): - yield prefix + '...' + suffix - return - - yield prefix - - count = self.maxcollection[level] - yield_comma = False - - try: - sorted_keys = sorted(obj) - except Exception: - sorted_keys = list(obj) - - for key in sorted_keys: - if yield_comma: - yield ', ' - yield_comma = True - - count -= 1 - if count <= 0: - yield '...' - break - - yield item_prefix - for p in self._repr(key, level + 1): - yield p - - yield item_sep - - try: - item = obj[key] - except Exception: - yield '' - else: - for p in self._repr(item, 100 if item is obj else level + 1): - yield p - yield item_suffix - - yield suffix - - def _repr_str(self, obj, level): - return self._repr_obj(obj, level, - self.maxstring_inner, self.maxstring_outer) - - def _repr_other(self, obj, level): - return self._repr_obj(obj, level, - self.maxother_inner, self.maxother_outer) - - def _repr_obj(self, obj, level, limit_inner, limit_outer): - try: - if self.raw_value: - # For raw value retrieval, ignore all limits. - if isinstance(obj, bytes): - yield obj.decode('latin-1') - return - - try: - mv = memoryview(obj) - except Exception: - yield self._convert_to_unicode_or_bytes_repr(repr(obj)) - return - else: - # Map bytes to Unicode codepoints with same values. - yield mv.tobytes().decode('latin-1') - return - elif self.convert_to_hex and isinstance(obj, self.int_types): - obj_repr = hex(obj) - else: - obj_repr = repr(obj) - except Exception: - try: - obj_repr = object.__repr__(obj) - except Exception: - try: - obj_repr = '' # noqa - except Exception: - obj_repr = '' - - limit = limit_inner if level > 0 else limit_outer - - if limit >= len(obj_repr): - yield self._convert_to_unicode_or_bytes_repr(obj_repr) - return - - # Slightly imprecise calculations - we may end up with a string that is - # up to 3 characters longer than limit. If you need precise formatting, - # you are using the wrong class. - left_count, right_count = max(1, int(2 * limit / 3)), max(1, int(limit / 3)) # noqa - - if VC_IS_PY2 and isinstance(obj_repr, bytes): - # If we can convert to unicode before slicing, that's better (but don't do - # it if it's not possible as we may be dealing with actual binary data). - - obj_repr = self._bytes_as_unicode_if_possible(obj_repr) - if isinstance(obj_repr, unicode): - # Deal with high-surrogate leftovers on Python 2. - try: - if left_count > 0 and unichr(0xD800) <= obj_repr[left_count - 1] <= unichr(0xDBFF): - left_count -= 1 - except ValueError: - # On Jython unichr(0xD800) will throw an error: - # ValueError: unichr() arg is a lone surrogate in range (0xD800, 0xDFFF) (Jython UTF-16 encoding) - # Just ignore it in this case. - pass - - start = obj_repr[:left_count] - - # Note: yielding unicode is fine (it'll be properly converted to utf-8 if needed). - yield start - yield '...' - - # Deal with high-surrogate leftovers on Python 2. - try: - if right_count > 0 and unichr(0xD800) <= obj_repr[-right_count - 1] <= unichr(0xDBFF): - right_count -= 1 - except ValueError: - # On Jython unichr(0xD800) will throw an error: - # ValueError: unichr() arg is a lone surrogate in range (0xD800, 0xDFFF) (Jython UTF-16 encoding) - # Just ignore it in this case. - pass - - yield obj_repr[-right_count:] - return - else: - # We can't decode it (binary string). Use repr() of bytes. - obj_repr = repr(obj_repr) - - yield obj_repr[:left_count] - yield '...' - yield obj_repr[-right_count:] - - def _convert_to_unicode_or_bytes_repr(self, obj_repr): - if VC_IS_PY2 and isinstance(obj_repr, bytes): - obj_repr = self._bytes_as_unicode_if_possible(obj_repr) - if isinstance(obj_repr, bytes): - # If we haven't been able to decode it this means it's some binary data - # we can't make sense of, so, we need its repr() -- otherwise json - # encoding may break later on. - obj_repr = repr(obj_repr) - return obj_repr - - def _bytes_as_unicode_if_possible(self, obj_repr): - # We try to decode with 3 possible encoding (sys.stdout.encoding, - # locale.getpreferredencoding() and 'utf-8). If no encoding can decode - # the input, we return the original bytes. - try_encodings = [] - encoding = self.sys_stdout_encoding or getattr(sys.stdout, 'encoding', '') - if encoding: - try_encodings.append(encoding.lower()) - - preferred_encoding = self.locale_preferred_encoding or VC_locale.getpreferredencoding() - if preferred_encoding: - preferred_encoding = preferred_encoding.lower() - if preferred_encoding not in try_encodings: - try_encodings.append(preferred_encoding) - - if 'utf-8' not in try_encodings: - try_encodings.append('utf-8') - - for encoding in try_encodings: - try: - return obj_repr.decode(encoding) - except UnicodeDecodeError: - pass - - return obj_repr # Return the original version (in bytes) - - -# Query Jupyter server for the value of a variable -import json as _VSCODE_json -_VSCODE_max_len = 200 -# In IJupyterVariables.getValue this '_VSCode_JupyterTestValue' will be replaced with the json stringified value of the target variable -# Indexes off of _VSCODE_targetVariable need to index types that are part of IJupyterVariable -_VSCODE_targetVariable = _VSCODE_json.loads('_VSCode_JupyterTestValue') - -_VSCODE_evalResult = eval(_VSCODE_targetVariable['name']) - -# Find shape and count if available -if (hasattr(_VSCODE_evalResult, 'shape')): - try: - # Get a bit more restrictive with exactly what we want to count as a shape, since anything can define it - if isinstance(_VSCODE_evalResult.shape, tuple): - _VSCODE_shapeStr = str(_VSCODE_evalResult.shape) - if len(_VSCODE_shapeStr) >= 3 and _VSCODE_shapeStr[0] == '(' and _VSCODE_shapeStr[-1] == ')' and ',' in _VSCODE_shapeStr: - _VSCODE_targetVariable['shape'] = _VSCODE_shapeStr - del _VSCODE_shapeStr - except TypeError: - pass - -if (hasattr(_VSCODE_evalResult, '__len__')): - try: - _VSCODE_targetVariable['count'] = len(_VSCODE_evalResult) - except TypeError: - pass - -# Use SafeRepr to get our short string value -VC_sr = VC_SafeRepr() -_VSCODE_targetVariable['value'] = VC_sr(_VSCODE_evalResult) - -print(_VSCODE_json.dumps(_VSCODE_targetVariable)) - -del VC_locale -del VC_IS_PY2 -del VC_sys -del VC_SafeRepr -del VC_sr diff --git a/pythonFiles/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 9fd4991c7407..000000000000 --- a/pythonFiles/normalizeForInterpreter.py +++ /dev/null @@ -1,135 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import ast -import io -import operator -import os -import sys -import textwrap -import token -import tokenize - - -class Visitor(ast.NodeVisitor): - def __init__(self, lines): - self._lines = lines - self.line_numbers_with_nodes = set() - self.line_numbers_with_statements = [] - - def generic_visit(self, node): - if hasattr(node, 'col_offset') and hasattr(node, 'lineno') and node.col_offset == 0: - self.line_numbers_with_nodes.add(node.lineno) - if isinstance(node, ast.stmt): - self.line_numbers_with_statements.append(node.lineno) - - ast.NodeVisitor.generic_visit(self, node) - - -def _tokenize(source): - """Tokenize Python source code.""" - # Using an undocumented API as the documented one in Python 2.7 does not work as needed - # cross-version. - if sys.version_info < (3,) and isinstance(source, str): - source = source.decode() - return tokenize.generate_tokens(io.StringIO(source).readline) - - -def _indent_size(line): - for index, char in enumerate(line): - if not char.isspace(): - return index - - -def _get_global_statement_blocks(source, lines): - """Return a list of all global statement blocks. - - The list comprises of 3-item tuples that contain the starting line number, - ending line number and whether the statement is a single line. - - """ - tree = ast.parse(source) - visitor = Visitor(lines) - visitor.visit(tree) - - statement_ranges = [] - for index, line_number in enumerate(visitor.line_numbers_with_statements): - remaining_line_numbers = visitor.line_numbers_with_statements[index+1:] - end_line_number = len(lines) if len(remaining_line_numbers) == 0 else min(remaining_line_numbers) - 1 - current_statement_is_oneline = line_number == end_line_number - - if len(statement_ranges) == 0: - statement_ranges.append((line_number, end_line_number, current_statement_is_oneline)) - continue - - previous_statement = statement_ranges[-1] - previous_statement_is_oneline = previous_statement[2] - if previous_statement_is_oneline and current_statement_is_oneline: - statement_ranges[-1] = previous_statement[0], end_line_number, True - else: - statement_ranges.append((line_number, end_line_number, current_statement_is_oneline)) - - return statement_ranges - - -def normalize_lines(source): - """Normalize blank lines for sending to the terminal. - - Blank lines within a statement block are removed to prevent the REPL - from thinking the block is finished. Newlines are added to separate - top-level statements so that the REPL does not think there is a syntax - error. - - """ - # Ensure to dedent the code (#2837) - lines = textwrap.dedent(source).splitlines(False) - # If we have two blank lines, then add two blank lines. - # Do not trim the spaces, if we have blank lines with spaces, its possible - # we have indented code. - if (len(lines) > 1 and len(''.join(lines[-2:])) == 0) \ - or source.endswith(('\n\n', '\r\n\r\n')): - trailing_newline = '\n' * 2 - # Find out if we have any trailing blank lines - elif len(lines[-1].strip()) == 0 or source.endswith(('\n', '\r\n')): - trailing_newline = '\n' - else: - trailing_newline = '' - - # Step 1: Remove empty lines. - tokens = _tokenize(source) - newlines_indexes_to_remove = (spos[0] for (toknum, tokval, spos, epos, line) in tokens - if len(line.strip()) == 0 - and token.tok_name[toknum] == 'NL' - and spos[0] == epos[0]) - - for line_number in reversed(list(newlines_indexes_to_remove)): - del lines[line_number-1] - - # Step 2: Add blank lines between each global statement block. - # A 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 14661c43fc18..000000000000 --- a/pythonFiles/ptvsd_launcher.py +++ /dev/null @@ -1,43 +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 - from ptvsd.__main__ import main - ptvsd_loaded = True - except ImportError: - ptvsd_loaded = False - raise -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/symbolProvider.py b/pythonFiles/symbolProvider.py deleted file mode 100644 index 0e69d06b227f..000000000000 --- a/pythonFiles/symbolProvider.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import ast -import json -import sys - - -class Visitor(ast.NodeVisitor): - def __init__(self): - self.symbols = {"classes": [], "methods": [], "functions": []} - - def visit_Module(self, node): - self.visitChildren(node) - - def visitChildren(self, node, namespace=""): - for child in node.body: - if isinstance(child, ast.FunctionDef): - self.visitDef(child, namespace) - if isinstance(child, ast.ClassDef): - self.visitClassDef(child, namespace) - try: - if isinstance(child, ast.AsyncFunctionDef): - self.visitDef(child, namespace) - except Exception: - pass - - def visitDef(self, node, namespace=""): - end_position = self.getEndPosition(node) - symbol = "functions" if namespace == "" else "methods" - self.symbols[symbol].append(self.getDataObject(node, namespace)) - - def visitClassDef(self, node, namespace=""): - end_position = self.getEndPosition(node) - self.symbols['classes'].append(self.getDataObject(node, namespace)) - - if len(namespace) > 0: - namespace = "{0}::{1}".format(namespace, node.name) - else: - namespace = node.name - self.visitChildren(node, namespace) - - def getDataObject(self, node, namespace=""): - end_position = self.getEndPosition(node) - return { - "namespace": namespace, - "name": node.name, - "range": { - "start": { - "line": node.lineno - 1, - "character": node.col_offset - }, - "end": { - "line": end_position[0], - "character": end_position[1] - } - } - } - - def getEndPosition(self, node): - if not hasattr(node, 'body') or len(node.body) == 0: - return (node.lineno - 1, node.col_offset) - return self.getEndPosition(node.body[-1]) - - -def provide_symbols(source): - """Provides a list of all symbols in provided code. - - The list comprises of 3-item tuples that contain the starting line number, - ending line number and whether the statement is a single line. - - """ - tree = ast.parse(source) - visitor = Visitor() - visitor.visit(tree) - sys.stdout.write(json.dumps(visitor.symbols)) - sys.stdout.flush() - - -if __name__ == "__main__": - if len(sys.argv) == 3: - contents = sys.argv[2] - else: - with open(sys.argv[1], "r") as source: - contents = source.read() - - try: - default_encoding = sys.getdefaultencoding() - encoded_contents = contents.encode(default_encoding, 'surrogateescape') - contents = encoded_contents.decode(default_encoding, 'replace') - except (UnicodeError, LookupError): - pass - if isinstance(contents, bytes): - contents = contents.decode('utf8') - provide_symbols(contents) diff --git a/pythonFiles/testing_tools/adapter/__main__.py b/pythonFiles/testing_tools/adapter/__main__.py deleted file mode 100644 index 01edc9f51e39..000000000000 --- a/pythonFiles/testing_tools/adapter/__main__.py +++ /dev/null @@ -1,98 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import - -import argparse -import sys - -from . import pytest, report -from .errors import UnsupportedToolError, UnsupportedCommandError - - -TOOLS = { - 'pytest': { - '_add_subparser': pytest.add_cli_subparser, - 'discover': pytest.discover, - }, - } -REPORTERS = { - 'discover': report.report_discovered, - } - - - -def parse_args( - argv=sys.argv[1:], - prog=sys.argv[0], - ): - """ - Return the subcommand & tool to run, along with its args. - - This defines the standard CLI for the different testing frameworks. - """ - parser = argparse.ArgumentParser( - description='Run Python testing operations.', - prog=prog, - ) - cmdsubs = parser.add_subparsers(dest='cmd') - - # Add "run" and "debug" subcommands when ready. - for cmdname in ['discover']: - sub = cmdsubs.add_parser(cmdname) - subsubs = sub.add_subparsers(dest='tool') - for toolname in sorted(TOOLS): - try: - add_subparser = TOOLS[toolname]['_add_subparser'] - except KeyError: - continue - subsub = add_subparser(cmdname, toolname, subsubs) - if cmdname == 'discover': - subsub.add_argument('--simple', action='store_true') - subsub.add_argument('--no-hide-stdio', dest='hidestdio', - action='store_false') - subsub.add_argument('--pretty', action='store_true') - - # Parse the args! - if '--' in argv: - seppos = argv.index('--') - toolargs = argv[seppos + 1:] - argv = argv[:seppos] - else: - toolargs = [] - args = parser.parse_args(argv) - ns = vars(args) - - cmd = ns.pop('cmd') - if not cmd: - parser.error('missing command') - - tool = ns.pop('tool') - if not tool: - parser.error('missing tool') - - return tool, cmd, ns, toolargs - - -def main(toolname, cmdname, subargs, toolargs, - _tools=TOOLS, _reporters=REPORTERS): - try: - tool = _tools[toolname] - except KeyError: - raise UnsupportedToolError(toolname) - - try: - run = tool[cmdname] - report_result = _reporters[cmdname] - except KeyError: - raise UnsupportedCommandError(cmdname) - - parents, result = run(toolargs, **subargs) - report_result(result, parents, - **subargs - ) - - -if __name__ == '__main__': - tool, cmd, subargs, toolargs = parse_args() - main(tool, cmd, subargs, toolargs) diff --git a/pythonFiles/testing_tools/adapter/discovery.py b/pythonFiles/testing_tools/adapter/discovery.py deleted file mode 100644 index 15196a6b0beb..000000000000 --- a/pythonFiles/testing_tools/adapter/discovery.py +++ /dev/null @@ -1,64 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, print_function - -import os.path - -from .info import ParentInfo - - - -class DiscoveredTests(object): - """A container for the discovered tests and their parents.""" - - def __init__(self): - self.reset() - - def __len__(self): - return len(self._tests) - - def __getitem__(self, index): - return self._tests[index] - - @property - def parents(self): - return sorted(self._parents.values(), key=lambda v: (v.root or v.name, v.id)) - - def reset(self): - """Clear out any previously discovered tests.""" - self._parents = {} - self._tests = [] - - def add_test(self, test, parents): - """Add the given test and its parents.""" - parentid = self._ensure_parent(test.path, parents) - # Updating the parent ID and the test ID aren't necessary if the - # provided test and parents (from the test collector) are - # properly generated. However, we play it safe here. - test = test._replace(parentid=parentid) - if not test.id.startswith('.' + os.path.sep): - test = test._replace(id=os.path.join('.', test.id)) - self._tests.append(test) - - def _ensure_parent(self, path, parents): - rootdir = path.root - - _parents = iter(parents) - nodeid, name, kind = next(_parents) - # As in add_test(), the node ID *should* already be correct. - if nodeid != '.' and not nodeid.startswith('.' + os.path.sep): - nodeid = os.path.join('.', nodeid) - _parentid = nodeid - for parentid, parentname, parentkind in _parents: - # As in add_test(), the parent ID *should* already be correct. - if parentid != '.' and not parentid.startswith('.' + os.path.sep): - parentid = os.path.join('.', parentid) - info = ParentInfo(nodeid, kind, name, rootdir, parentid) - self._parents[(rootdir, nodeid)] = info - nodeid, name, kind = parentid, parentname, parentkind - assert nodeid == '.' - info = ParentInfo(nodeid, kind, name=rootdir) - self._parents[(rootdir, nodeid)] = info - - return _parentid diff --git a/pythonFiles/testing_tools/adapter/errors.py b/pythonFiles/testing_tools/adapter/errors.py deleted file mode 100644 index 18b3819dcbdb..000000000000 --- a/pythonFiles/testing_tools/adapter/errors.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -class UnsupportedToolError(ValueError): - def __init__(self, tool): - msg = 'unsupported tool {!r}'.format(tool) - super(UnsupportedToolError, self).__init__(msg) - self.tool = tool - - -class UnsupportedCommandError(ValueError): - def __init__(self, cmd): - msg = 'unsupported cmd {!r}'.format(cmd) - super(UnsupportedCommandError, self).__init__(msg) - self.cmd = cmd diff --git a/pythonFiles/testing_tools/adapter/info.py b/pythonFiles/testing_tools/adapter/info.py deleted file mode 100644 index c9c14571dd6b..000000000000 --- a/pythonFiles/testing_tools/adapter/info.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from collections import namedtuple - - -class TestPath(namedtuple('TestPath', 'root relfile func sub')): - """Where to find a single test.""" - - def __new__(cls, root, relfile, func, sub=None): - self = super(TestPath, cls).__new__( - cls, - str(root) if root else None, - str(relfile) if relfile else None, - str(func) if func else None, - [str(s) for s in sub] if sub else None, - ) - return self - - def __init__(self, *args, **kwargs): - if self.root is None: - raise TypeError('missing id') - if self.relfile is None: - raise TypeError('missing kind') - # self.func may be None (e.g. for doctests). - # self.sub may be None. - - -class ParentInfo(namedtuple('ParentInfo', 'id kind name root parentid')): - - KINDS = ('folder', 'file', 'suite', 'function', 'subtest') - - def __new__(cls, id, kind, name, root=None, parentid=None): - self = super(ParentInfo, cls).__new__( - cls, - str(id) if id else None, - str(kind) if kind else None, - str(name) if name else None, - str(root) if root else None, - str(parentid) if parentid else None, - ) - return self - - def __init__(self, *args, **kwargs): - if self.id is None: - raise TypeError('missing id') - if self.kind is None: - raise TypeError('missing kind') - if self.kind not in self.KINDS: - raise ValueError('unsupported kind {!r}'.format(self.kind)) - if self.name is None: - raise TypeError('missing name') - if self.root is None: - if self.parentid is not None or self.kind != 'folder': - raise TypeError('missing root') - elif self.parentid is None: - raise TypeError('missing parentid') - - -class TestInfo(namedtuple('TestInfo', 'id name path source markers parentid kind')): - """Info for a single test.""" - - MARKERS = ('skip', 'skip-if', 'expected-failure') - KINDS = ('function', 'doctest') - - def __new__(cls, id, name, path, source, markers, parentid, kind='function'): - self = super(TestInfo, cls).__new__( - cls, - str(id) if id else None, - str(name) if name else None, - path or None, - str(source) if source else None, - [str(marker) for marker in markers or ()], - str(parentid) if parentid else None, - str(kind) if kind else None, - ) - return self - - def __init__(self, *args, **kwargs): - if self.id is None: - raise TypeError('missing id') - if self.name is None: - raise TypeError('missing name') - if self.path is None: - raise TypeError('missing path') - if self.source is None: - raise TypeError('missing source') - else: - srcfile, _, lineno = self.source.rpartition(':') - if not srcfile or not lineno or int(lineno) < 0: - raise ValueError('bad source {!r}'.format(self.source)) - if self.markers: - badmarkers = [m for m in self.markers if m not in self.MARKERS] - if badmarkers: - raise ValueError('unsupported markers {!r}'.format(badmarkers)) - if self.parentid is None: - raise TypeError('missing parentid') - if self.kind is None: - raise TypeError('missing kind') - elif self.kind not in self.KINDS: - raise ValueError('unsupported kind {!r}'.format(self.kind)) - - - @property - def root(self): - return self.path.root - - @property - def srcfile(self): - return self.source.rpartition(':')[0] - - @property - def lineno(self): - return int(self.source.rpartition(':')[-1]) diff --git a/pythonFiles/testing_tools/adapter/pytest/__init__.py b/pythonFiles/testing_tools/adapter/pytest/__init__.py deleted file mode 100644 index e894f7bcdb8e..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import - -from ._cli import add_subparser as add_cli_subparser -from ._discovery import discover diff --git a/pythonFiles/testing_tools/adapter/pytest/_cli.py b/pythonFiles/testing_tools/adapter/pytest/_cli.py deleted file mode 100644 index 5451b87b3044..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_cli.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import - -from ..errors import UnsupportedCommandError - - -def add_subparser(cmd, name, parent): - """Add a new subparser to the given parent and add args to it.""" - parser = parent.add_parser(name) - if cmd == 'discover': - # For now we don't have any tool-specific CLI options to add. - pass - else: - raise UnsupportedCommandError(cmd) - return parser diff --git a/pythonFiles/testing_tools/adapter/pytest/_discovery.py b/pythonFiles/testing_tools/adapter/pytest/_discovery.py deleted file mode 100644 index aee1a1eccb98..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_discovery.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import absolute_import, print_function - -import os.path -import sys - -import pytest - -from .. import util, discovery -from ._pytest_item import parse_item - - -def discover(pytestargs=None, hidestdio=False, - _pytest_main=pytest.main, _plugin=None, **_ignored): - """Return the results of test discovery.""" - if _plugin is None: - _plugin = TestCollector() - - pytestargs = _adjust_pytest_args(pytestargs) - # We use this helper rather than "-pno:terminal" due to possible - # platform-dependent issues. - with (util.hide_stdio() if hidestdio else util.noop_cm()) as stdio: - ec = _pytest_main(pytestargs, [_plugin]) - # See: https://docs.pytest.org/en/latest/usage.html#possible-exit-codes - if ec == 5: - # No tests were discovered. - pass - elif ec != 0: - print(('equivalent command: {} -m pytest {}' - ).format(sys.executable, util.shlex_unsplit(pytestargs))) - if hidestdio: - print(stdio.getvalue(), file=sys.stderr) - sys.stdout.flush() - raise Exception('pytest discovery failed (exit code {})'.format(ec)) - if not _plugin._started: - print(('equivalent command: {} -m pytest {}' - ).format(sys.executable, util.shlex_unsplit(pytestargs))) - if hidestdio: - print(stdio.getvalue(), file=sys.stderr) - sys.stdout.flush() - raise Exception('pytest discovery did not start') - return ( - _plugin._tests.parents, - list(_plugin._tests), - ) - - -def _adjust_pytest_args(pytestargs): - """Return a corrected copy of the given pytest CLI args.""" - pytestargs = list(pytestargs) if pytestargs else [] - # Duplicate entries should be okay. - pytestargs.insert(0, '--collect-only') - # TODO: pull in code from: - # src/client/testing/pytest/services/discoveryService.ts - # src/client/testing/pytest/services/argsService.ts - return pytestargs - - -class TestCollector(object): - """This is a pytest plugin that collects the discovered tests.""" - - NORMCASE = staticmethod(os.path.normcase) - PATHSEP = os.path.sep - - def __init__(self, tests=None): - if tests is None: - tests = discovery.DiscoveredTests() - self._tests = tests - self._started = False - - # Relevant plugin hooks: - # https://docs.pytest.org/en/latest/reference.html#collection-hooks - - def pytest_collection_modifyitems(self, session, config, items): - self._started = True - self._tests.reset() - for item in items: - test, parents = parse_item(item, self.NORMCASE, self.PATHSEP) - self._tests.add_test(test, parents) - - # This hook is not specified in the docs, so we also provide - # the "modifyitems" hook just in case. - def pytest_collection_finish(self, session): - self._started = True - try: - items = session.items - except AttributeError: - # TODO: Is there an alternative? - return - self._tests.reset() - for item in items: - test, parents = parse_item(item, self.NORMCASE, self.PATHSEP) - self._tests.add_test(test, parents) diff --git a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py b/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py deleted file mode 100644 index 92b48e820d4e..000000000000 --- a/pythonFiles/testing_tools/adapter/pytest/_pytest_item.py +++ /dev/null @@ -1,444 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. -""" -During "collection", pytest finds all the tests it supports. These are -called "items". The process is top-down, mostly tracing down through -the file system. Aside from its own machinery, pytest supports hooks -that find tests. Effectively, pytest starts with a set of "collectors"; -objects that can provide a list of tests and sub-collectors. All -collectors in the resulting tree are visited and the tests aggregated. -For the most part, each test's (and collector's) parent is identified -as the collector that collected it. - -Collectors and items are collectively identified as "nodes". The pytest -API relies on collector and item objects providing specific methods and -attributes. In addition to corresponding base classes, pytest provides -a number of concrete impementations. - -The following are the known pytest node types: - - Node - Collector - FSCollector - Session (the top-level collector) - File - Module - Package - DoctestTextfile - DoctestModule - PyCollector - (Module) - (...) - Class - UnitTestCase - Instance - Item - Function - TestCaseFunction - DoctestItem - -Here are the unique attrs for those classes: - - Node - name - nodeid (readonly) - config - session - (parent) - the parent node - (fspath) - the file from which the node was collected - ---- - own_marksers - explicit markers (e.g. with @pytest.mark()) - keywords - extra_keyword_matches - - Item - location - where the actual test source code is: (relfspath, lno, fullname) - user_properties - - PyCollector - module - class - instance - obj - - Function - module - class - instance - obj - function - (callspec) - (fixturenames) - funcargs - originalname - w/o decorations, e.g. [...] for parameterized - - DoctestItem - dtest - obj - -When parsing an item, we make use of the following attributes: - -* name -* nodeid -* __class__ - + __name__ -* fspath -* location -* function - + __name__ - + __code__ - + __closure__ -* own_markers -""" - -from __future__ import absolute_import, print_function - -import sys - -import pytest -import _pytest.doctest -import _pytest.unittest - -from ..info import TestInfo, TestPath - - -def should_never_reach_here(node, *extra): - """Indicates a code path we should never reach.""" - print('The Python extension has run into an unexpected situation') - print('while processing a pytest node during test discovery. Please') - print('Please open an issue at:') - print(' https://github.com/microsoft/vscode-python/issues') - print('and paste the following output there.') - print() - for field, info in _summarize_item(node): - print('{}: {}'.format(field, info)) - if extra: - print() - print('extra info:') - for info in extra: - if isinstance(line, str): - print(str) - else: - try: - print(*line) - except TypeError: - print(line) - print() - print('traceback:') - import traceback - traceback.print_stack() - - msg = 'Unexpected pytest node (see printed output).' - exc = NotImplementedError(msg) - exc.node = node - return exc - - -def parse_item(item, _normcase, _pathsep): - """Return (TestInfo, [suite ID]) for the given item. - - The suite IDs, if any, are in parent order with the item's direct - parent at the beginning. The parent of the last suite ID (or of - the test if there are no suites) is the file ID, which corresponds - to TestInfo.path. - - """ - #_debug_item(item, showsummary=True) - kind, _ = _get_item_kind(item) - (nodeid, parents, fileid, testfunc, parameterized - ) = _parse_node_id(item.nodeid, kind, _pathsep, _normcase) - # Note: testfunc does not necessarily match item.function.__name__. - # This can result from importing a test function from another module. - - # Figure out the file. - relfile = fileid - fspath = _normcase(str(item.fspath)) - if not fspath.endswith(relfile[1:]): - raise should_never_reach_here( - item, - fspath, - relfile, - ) - testroot = fspath[:-len(relfile) + 1] - location, fullname = _get_location(item, relfile, _normcase, _pathsep) - if kind == 'function': - if testfunc and fullname != testfunc + parameterized: - raise should_never_reach_here( - item, - fullname, - testfunc, - parameterized, - ) - elif kind == 'doctest': - if (testfunc and fullname != testfunc and - fullname != '[doctest] ' + testfunc): - raise should_never_reach_here( - item, - fullname, - testfunc, - ) - testfunc = None - - # Sort out the parent. - if parents: - parentid, _, _ = parents[0] - else: - parentid = None - - # Sort out markers. - # See: https://docs.pytest.org/en/latest/reference.html#marks - markers = set() - for marker in item.own_markers: - if marker.name == 'parameterize': - # We've already covered these. - continue - elif marker.name == 'skip': - markers.add('skip') - elif marker.name == 'skipif': - markers.add('skip-if') - elif marker.name == 'xfail': - markers.add('expected-failure') - # We can add support for other markers as we need them? - - test = TestInfo( - id=nodeid, - name=item.name, - path=TestPath( - root=testroot, - relfile=relfile, - func=testfunc, - sub=[parameterized] if parameterized else None, - ), - source=location, - markers=sorted(markers) if markers else None, - parentid=parentid, - ) - if parents and parents[-1] == ('.', None, 'folder'): # This should always be true? - parents[-1] = ('.', testroot, 'folder') - return test, parents - - -def _get_location(item, relfile, _normcase, _pathsep): - """Return (loc str, fullname) for the given item.""" - srcfile, lineno, fullname = item.location - srcfile = _normcase(srcfile) - if srcfile in (relfile, relfile[len(_pathsep) + 1:]): - srcfile = relfile - else: - # pytest supports discovery of tests imported from other - # modules. This is reflected by a different filename - # in item.location. - srcfile, lineno = _find_location( - srcfile, lineno, relfile, item.function, _pathsep) - if not srcfile.startswith('.' + _pathsep): - srcfile = '.' + _pathsep + srcfile - # from pytest, line numbers are 0-based - location = '{}:{}'.format(srcfile, int(lineno) + 1) - return location, fullname - - -def _find_location(srcfile, lineno, relfile, func, _pathsep): - """Return (filename, lno) for the given location info.""" - if sys.version_info > (3,): - return srcfile, lineno - if (_pathsep + 'unittest' + _pathsep + 'case.py') not in srcfile: - return srcfile, lineno - - # Unwrap the decorator (e.g. unittest.skip). - srcfile = relfile - lineno = -1 - try: - func = func.__closure__[0].cell_contents - except (IndexError, AttributeError): - return srcfile, lineno - else: - if callable(func) and func.__code__.co_filename.endswith(relfile[1:]): - lineno = func.__code__.co_firstlineno - 1 - return srcfile, lineno - - -def _parse_node_id(testid, kind, _pathsep, _normcase): - """Return the components of the given node ID, in heirarchical order.""" - nodes = iter(_iter_nodes(testid, kind, _pathsep, _normcase)) - - testid, name, kind = next(nodes) - parents = [] - parameterized = None - if kind == 'doctest': - parents = list(nodes) - fileid, _, _ = parents[0] - return testid, parents, fileid, name, parameterized - elif kind is None: - fullname = None - else: - if kind == 'subtest': - node = next(nodes) - parents.append(node) - funcid, funcname, _ = node - parameterized = testid[len(funcid):] - elif kind == 'function': - funcname = name - else: - raise should_never_reach_here( - testid, - kind, - ) - fullname = funcname - - for node in nodes: - parents.append(node) - parentid, name, kind = node - if kind == 'file': - fileid = parentid - break - elif fullname is None: - # We don't guess how to interpret the node ID for these tests. - continue - elif kind == 'suite': - fullname = name + '.' + fullname - else: - raise should_never_reach_here( - testid, - node, - ) - else: - fileid = None - parents.extend(nodes) # Add the rest in as-is. - - return testid, parents, fileid, fullname, parameterized or '' - - -def _iter_nodes(nodeid, kind, _pathsep, _normcase): - """Yield (nodeid, name, kind) for the given node ID and its parents.""" - nodeid = _normalize_node_id(nodeid, kind, _pathsep, _normcase) - - if kind == 'function' and nodeid.endswith(']'): - funcid, sep, parameterized = nodeid.partition('[') - if not sep: - raise should_never_reach_here( - nodeid, - ) - yield (nodeid, sep + parameterized, 'subtest') - nodeid = funcid - - parentid, _, name = nodeid.rpartition('::') - if not parentid: - if kind is None: - # This assumes that plugins can generate nodes that do not - # have a parent. All the builtin nodes have one. - yield (nodeid, name, kind) - return - # We expect at least a filename and a name. - raise should_never_reach_here( - nodeid, - ) - yield (nodeid, name, kind) - - # Extract the suites. - while '::' in parentid: - suiteid = parentid - parentid, _, name = parentid.rpartition('::') - yield (suiteid, name, 'suite') - - # Extract the file and folders. - fileid = parentid - parentid, _, filename = fileid.rpartition(_pathsep) - yield (fileid, filename, 'file') - # We're guaranteed at least one (the test root). - while _pathsep in parentid: - folderid = parentid - parentid, _, foldername = folderid.rpartition(_pathsep) - yield (folderid, foldername, 'folder') - # We set the actual test root later at the bottom of parse_item(). - testroot = None - yield (parentid, testroot, 'folder') - - -def _normalize_node_id(nodeid, kind, _pathsep, _normcase): - """Return the canonical form for the given node ID.""" - while '::()::' in nodeid: - nodeid = nodeid.replace('::()::', '::') - if kind is None: - return nodeid - - fileid, sep, remainder = nodeid.partition('::') - if sep: - # pytest works fine even if we normalize the filename. - nodeid = _normcase(fileid) + sep + remainder - - if nodeid.startswith(_pathsep): - raise should_never_reach_here( - nodeid, - ) - if not nodeid.startswith('.' + _pathsep): - nodeid = '.' + _pathsep + nodeid - return nodeid - - -def _get_item_kind(item): - """Return (kind, isunittest) for the given item.""" - if isinstance(item, _pytest.doctest.DoctestItem): - return 'doctest', False - elif isinstance(item, _pytest.unittest.TestCaseFunction): - return 'function', True - elif isinstance(item, pytest.Function): - # We *could* be more specific, e.g. "method", "subtest". - return 'function', False - else: - return None, False - - -############################# -# useful for debugging - -_FIELDS = [ - 'nodeid', - 'kind', - 'class', - 'name', - 'fspath', - 'location', - 'function', - 'markers', - 'user_properties', - 'attrnames', - ] - - -def _summarize_item(item): - if not hasattr(item, 'nodeid'): - yield 'nodeid', item - return - - for field in _FIELDS: - try: - if field == 'kind': - yield field,_get_item_kind(item) - elif field == 'class': - yield field, item.__class__.__name__ - elif field == 'markers': - yield field, item.own_markers - #yield field, list(item.iter_markers()) - elif field == 'attrnames': - yield field, dir(item) - else: - yield field, getattr(item, field, '') - except Exception as exc: - yield field, '' - - -def _debug_item(item, showsummary=False): - item._debugging = True - try: - summary = dict(_summarize_item(item)) - finally: - item._debugging = False - - if showsummary: - print(item.nodeid) - for key in ('kind', 'class', 'name', 'fspath', 'location', 'func', - 'markers', 'props'): - print(' {:12} {}'.format(key, summary[key])) - print() - - return summary diff --git a/pythonFiles/testing_tools/adapter/report.py b/pythonFiles/testing_tools/adapter/report.py deleted file mode 100644 index edc33fcb8a34..000000000000 --- a/pythonFiles/testing_tools/adapter/report.py +++ /dev/null @@ -1,74 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import print_function - -import json - - -def report_discovered(tests, parents, pretty=False, simple=False, - _send=print, **_ignored): - """Serialize the discovered tests and write to stdout.""" - if simple: - data = [{ - 'id': test.id, - 'name': test.name, - 'testroot': test.path.root, - 'relfile': test.path.relfile, - 'lineno': test.lineno, - 'testfunc': test.path.func, - 'subtest': test.path.sub or None, - 'markers': test.markers or [], - } for test in tests] - else: - byroot = {} - for parent in parents: - rootdir = parent.name if parent.root is None else parent.root - try: - root = byroot[rootdir] - except KeyError: - root = byroot[rootdir] = { - 'id': rootdir, - 'parents': [], - 'tests': [], - } - if not parent.root: - root['id'] = parent.id - continue - root['parents'].append({ - 'id': parent.id, - 'kind': parent.kind, - 'name': parent.name, - 'parentid': parent.parentid, - }) - for test in tests: - # We are guaranteed that the parent was added. - root = byroot[test.path.root] - testdata = { - 'id': test.id, - 'name': test.name, - # TODO: Add a "kind" field - # (e.g. "unittest", "function", "doctest") - 'source': test.source, - 'markers': test.markers or [], - 'parentid': test.parentid, - } - root['tests'].append(testdata) - data = [{ - 'rootid': byroot[root]['id'], - 'root': root, - 'parents': byroot[root]['parents'], - 'tests': byroot[root]['tests'], - } for root in sorted(byroot)] - - kwargs = {} - if pretty: - # human-formatted - kwargs = dict( - sort_keys=True, - indent=4, - separators=(',', ': '), - ) - serialized = json.dumps(data, **kwargs) - - _send(serialized) diff --git a/pythonFiles/testing_tools/adapter/util.py b/pythonFiles/testing_tools/adapter/util.py deleted file mode 100644 index d8df4eb25485..000000000000 --- a/pythonFiles/testing_tools/adapter/util.py +++ /dev/null @@ -1,92 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import contextlib -try: - from io import StringIO -except ImportError: - from StringIO import StringIO # 2.7 -import sys - - -@contextlib.contextmanager -def noop_cm(): - yield - - -@contextlib.contextmanager -def hide_stdio(): - """Swallow stdout and stderr.""" - ignored = StdioStream() - sys.stdout = ignored - sys.stderr = ignored - try: - yield ignored - finally: - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ - - -if sys.version_info < (3,): - class StdioStream(StringIO): - def write(self, msg): - StringIO.write(self, msg.decode()) -else: - StdioStream = StringIO - - -def group_attr_names(attrnames): - grouped = { - 'dunder': [], - 'private': [], - 'constants': [], - 'classes': [], - 'vars': [], - 'other': [], - } - for name in attrnames: - if name.startswith('__') and name.endswith('__'): - group = 'dunder' - elif name.startswith('_'): - group = 'private' - elif name.isupper(): - group = 'constants' - elif name.islower(): - group = 'vars' - elif name == name.capitalize(): - group = 'classes' - else: - group = 'other' - grouped[group].append(name) - return grouped - - -def shlex_unsplit(argv): - """Return the shell-safe string for the given arguments. - - This effectively the equivalent of reversing shlex.split(). - """ - argv = [_quote_arg(a) for a in argv] - return ' '.join(argv) - - -try: - from shlex import quote as _quote_arg -except ImportError: - def _quote_arg(arg): - parts = None - for i, c in enumerate(arg): - if c.isspace(): - pass - elif c == '"': - pass - elif c == "'": - c = "'\"'\"'" - else: - continue - if parts is None: - parts = list(arg) - parts[i] = c - if parts is not None: - arg = "'" + ''.join(parts) + "'" - return arg diff --git a/pythonFiles/testing_tools/run_adapter.py b/pythonFiles/testing_tools/run_adapter.py deleted file mode 100644 index d21f3efd1b70..000000000000 --- a/pythonFiles/testing_tools/run_adapter.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Replace the "." entry. -import os.path -import sys -sys.path[0] = os.path.dirname( - os.path.dirname( - os.path.abspath(__file__))) - -from testing_tools.adapter.__main__ import parse_args, main - - -if __name__ == '__main__': - tool, cmd, subargs, toolargs = parse_args() - main(tool, cmd, subargs, toolargs) diff --git a/pythonFiles/testlauncher.py b/pythonFiles/testlauncher.py deleted file mode 100644 index 0ab0fca051be..000000000000 --- a/pythonFiles/testlauncher.py +++ /dev/null @@ -1,62 +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 - 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/tests/__main__.py b/pythonFiles/tests/__main__.py deleted file mode 100644 index 5b140cc521bb..000000000000 --- a/pythonFiles/tests/__main__.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import argparse -import os.path -import sys - -import pytest - - -TEST_ROOT = os.path.dirname(__file__) -SRC_ROOT = os.path.dirname(TEST_ROOT) -PROJECT_ROOT = os.path.dirname(SRC_ROOT) -IPYTHON_ROOT = os.path.join(SRC_ROOT, 'ipython') -TESTING_TOOLS_ROOT = os.path.join(SRC_ROOT, 'testing_tools') - - -def parse_args(): - parser = argparse.ArgumentParser() - # To mark a test as functional: (decorator) @pytest.mark.functional - parser.add_argument('--functional', dest='markers', - action='append_const', const='functional') - parser.add_argument('--no-functional', dest='markers', - action='append_const', const='not functional') - args, remainder = parser.parse_known_args() - - ns = vars(args) - - return ns, remainder - - -def main(pytestargs, markers=None): - sys.path.insert(1, IPYTHON_ROOT) - sys.path.insert(1, TESTING_TOOLS_ROOT) - - pytestargs = [ - '--rootdir', SRC_ROOT, - TEST_ROOT, - ] + pytestargs - for marker in reversed(markers or ()): - pytestargs.insert(0, marker) - pytestargs.insert(0, '-m') - - ec = pytest.main(pytestargs) - return ec - - -if __name__ == '__main__': - mainkwargs, pytestargs = parse_args() - ec = main(pytestargs, **mainkwargs) - sys.exit(ec) diff --git a/pythonFiles/tests/ipython/random.csv b/pythonFiles/tests/ipython/random.csv deleted file mode 100644 index dde35fda3850..000000000000 --- a/pythonFiles/tests/ipython/random.csv +++ /dev/null @@ -1,6001 +0,0 @@ -".Lh~N","~`y","]W|{92","|uHbIo","o}","+h2" -"-O;Xfw ","","KX^K)FHe:","3+4ncqL8=O","py]ZA;","_y>" -"}z#IiI'+c,","^) ""","(SLt4","JJ:2VnAQ/J","ZO>%nl>`EZ","" -"","ji#cufxu2",":7JAHYDdRx","klq?$","tItAtlu-","TxspzZ" -"x9>iX'C+F","","G`[I","p^Y=S)",">NLE@UX|=a","a=XsO" -"`Y""Wc2^tQ","HlZ","N9/@","J","A","Cv" -"","L f'%K","~UDs","mh/A:X","Yz9d:T{2Q|","ee" -"`3A* >","Nu6cv","%F13Kb6p","","8+{hhd","J#","W3_yiE","y)d8?","" -";,.;rnu\K","nfS)Fy=l","",",r~$","jOU[uQYt'","R" -"3$cA$","l4","t!nf","53snc8g","m&,:","OQg\ " -"m>D","74|LkY5eV","/LR","h","","B]" -"60z{e92;","^P","5s-:m","G2YGn*DpK","I","" -"4s2(","n?j1,f~~?d","?","3{j,tax>8r","","Ms_\Z" -",i9sLk3z","D';Tv","R","m1keY","|`DumH","k.2-Mu9P" -"!","Y$","O","r","xj _*%*","#" -"HU","Z_jT0?","!2X>5","@9tD~BA)va","^ Gn=|q4H","eX" -"-","H,m=WD","fE;","YCTw<","""sS/{ow","","D@=iE'j4" -"Tqi1","hTF?RF""g=","","9{","m]?81+hk","1ZHfSk" -"J""oMEj","""*MZ`q; ","{","C","R|}Ws|j)d","s" -"6 k\:$@gl","bo8)dK#Sj","3Z3#","","J*:F>",".$" -"4Fx0(~dQ#U","F('2","YO~phil","/NmyUrJ~E","","" -"Or,B""b@HO.","uV2tX%yAE8","fn HXw","","7R^b","Pq%" -"W8BTO3","Vz{xaSpxd(","7YTQU}D]q[","b]B|5x","?i(kD","" -"","P~O","?m&SG1o{","(",").{1PSm[E","{" -"a","?","k&8)","","FP31","!""P^$!" -"90g=","{XN","FFH;b","r^XHX>@R&1","","4!sz=W#K" -"","##LY$m13","-(","`b","#K","Dd" -"|jA","34 Q","dU-;:OOU\U","nKO","*I","}+IJv" -".>j&","d-Gz","nOe6W""q$Zc","MYyF~","","FzO&u" -"!@>3&","YCyw0y","","","I(,","!fhPR+!=8N","*^tw&-Rt","V","","Sc3S" -"u;","#no","U","|B1`","[^o;Lyp5","" -".Vb[JH9x-s","9?<""y","","","8@1K7Jh","-ySc" -"R9S@g;fJ","ae4b","Q01]VuYAIu","J]W(PXVu`","xUw_LZ!8CB","" -",",":6","CLUFQ[9!","'K9\l^","_(OjmvR?","lT1GR;k" -"""+u$2VaoTD","","","@\|#G","","8I","Nx@R&g&sw","*r-Ltk","","" -"KB/ uy0","YJ6.].gQ-s","Q","RE[3RGSE4H","Q6a_)5p?","","|~O>&Xua","#cD&u","n~nM`]" -"V","-47Q8,k","53S""EIi%`","lDlQVv","A""}c1c='x:",";{4?6\" -"z'","YP5e4(U","l^mTI6qb",")sDm^JM)qV","","lw/yD34~","|uM5","","O6z>P}",":w!CBD" -"GUoP=,Kqc",")0|O","[A99FJC","uM:Fg","O{&","aQ;G_{m" -"D?1m8|8","Ule","CkEK+","Sfi8wyM","c1I~t","2/&bE=QJ:""" -"7XpO#@","I0L(t[`59","E&grAL3,","7","","" -"&tLwX)e","<#R#","&fp","}z@s","Gn","1ln[S!GP" -"k>Kb&YeB","5Qw=$-oq","&^","J15BT'z>~","sBq6&TRf","3FOj0T" -".6r$K4","\6f,.E'","k<","","4l(d 5","J[>x$1D2" -"""fto","lR`(+","?","TJ","","" -"8Nk","H","","]X","m.d%BI#q","; ,Pq?`;,%" -"5^}7sQ`","7>:L","I","3L\-","wA7Nh@rww","9VXVw%)~" -".HS","","","{7?+C","zo17eF|v"," R" -"vH#+T","?_Dk.Y","Gvdh+)-PhU","A]gB^<^e","R%X)","Gna*.n" -"?d","S","J:vQmT_[`^","W_X","`""S<.S}'","IC " -")AFHI(1't","K>]R","Bx","iWTE2fc","1<","m=B;x4n$" -"Zyu","q5C% i""b","$N)""mCi/o","ULa nXr{nv","u9HQx5,_1Z","XbKX" -"OP[s ^wl)6","y",".}2t=:","E,z@aBPR","qC=P3","`p}A63u,3%" -"X[d","Q0tsJ#*","gMR-.6&6:@","H"">M""CfcR","0}BSkSW@/e","R`Wi&`Djr" -"e&gUxU*A","0"," :","","Q>B","E" -"s%>WO5$o3g","D","s{%",")V&x)`@;","5=","1Sw" -"","Ym@","_","M`(*_B>P- " -"K","","2O#D?","%RTu>7Otx","zbN|","I""/" -"-)",".JzD'","bc[y^d#Y,","Tm0ud\","1H@;","n|q< L3i9U" -"&cc-XYJ",">51","0BKHX[","",""":0JR/g","\'\`5l\O" -"","o""L#w?4qY",";X7Qt","ivrB]@","R","% %-6A23" -"E-I[","0TPXa/8);o","T4hBVto0F","Lr[|_:Si","%(/x""","tT" -"","vWq`y[!","fZ{b-)R(","Y^","","bJr$}.<$>r" -"F","","t+r]s,=T#","b]1?U","5Ldjnsq4:N","u,7}qpR" -"","zftO.","3-y","","Y]BLTq3?","-x%#b" -"<[s2YJ&}_","bn","$>Rt7tHW",">!*4Ao","","}}" -"c]-jDpO","!LsvN,/+6","W+w&=l","""@fd`ho","wk+i#'Q","q/:k}" -"IMaj/y","L","g*d_:\:","YT\0","O<[/q@_""#a","UDQzt#5" -"ZFE","xb","oe*1.uQ/.","Xi1fJ8*\I","DXVd!(6>","5sBi}E5" -"8)","Pz","\\ L|","G|e&9","2gd#5)x Z{","gU\Jq]RrA" -"d0A$fck]","`" -"8","Y9ds?,""","^u","GUha]","]MR%qJ",":gq""`lO","8B;N:c?" -"EE"," b:(W9N","|;P""&9","v@qz","}B40.e%Utn","M,ynqkYg" -"e!/]Rz]$","cDQ!NE7","8@=6","d","4#/O","" -"zn","nA^@g!Z","8nt$sxb","t","","m#x{0;V11" -"","{tT5","9QnNs@A","\0URMK","]","j<""gt" -"&0/UGKz'G""","K:j)_","QS>g!7","$`9uc,CJZ,","`8+w,","`" -";'V-7","$p<`=","j^I","r2 ","","jy{" -"","m(hNrbi","h","Z9$V83J","+5]b","I" -":","<|>b[X1jX","c","T","","}r^hq4g" -"(vwf","wQvCnhksM","`","8K,3S'lHv","4o","A!q% P""LX" -"","rf8'y7;","X!Ur","s","?iDO",">h(." -"ui""R&JBr","&'7.u","2Y,l-5","?HB","DL""$","sAh||/l=" -"lXaB7[","1hY","j!%6 ow","Tf","i^r [F","OV9%h5nY}" -"Bp}nh7Ic+M","YH{L","y","o!9","K!<|","@D5" -"OjI/r'r*S4","WI=pe$lRG","'*MB","l","AQapJF4bTE","oBx>" -"WkoM","~H8GI,1p3V","Wu=B7{0j,","","xg)","IZ8l'1+I" -"Q3lB!","Q'N","}xi!#DkY&%","FNv","5a|FsN ","T!98" -"6","h#5{)<+A","#i7|fSA""W_","\u3].o" -"wI*","Rqk|kYv","]d |#lE",")FWt}%O9C","os","i@)=y9" -"","&u[Jq7","MsE24i/b%" -"b","{)\e?09""D","D*?Ybms*","vk6NN?7fJ","~q2Pc|","/vW:v }{J5" -"|@'o"," XH4 haX"," Y^0e>}","A",".]WK""",")bLp%mt(" -"_x&yU!GiV","WTc'5nj","*3NkkGk$'","f?,/V~","Z8g mW4","hW" -"wwE@@","","7Di?""Dt(lB","BU~Gg-","pd:" -";NonN$t5","","4G~X3zC3 ","IHHBOJ.","P}",">r*>W5" -"?x2""L4","sv|)/mN","+""$=1:8","p@~6#C>mM","I0&1Aaq7M@","n" -"NWnM;J","LXjXFA f","iUa;""","|k","662X.1;t"," " -"SBL6tq~B","Is|v9","KjJ[",")mgQ.;iw]","ERR?z","WcWgb#$q6e" -"7~a'`","C?6`","Rr>q&YV6I/","?b6w)G0","'jpc$","c zI9eD " -"_^_4{O&gbY","T^5__","w-[Py`(Qxf","*",":'EKBh#S","ljM" -"","c#sJ","W^2","@2`","\\OuBL""^mo","2wm6WA|x" -"8pkGz","V,/WQq*J7Y","&]2?7Q","EF.T9\@","[k","9YI)(eh:;7" -"$wk~","VkK:0Kz","u0!R~2","xY7#","9#","`j}6&" -"9","~S.E)b" -"Fw4l2A V","o`""Py","{Izy$K","OYsbb8","3]ZDq","8hPS1Pz" -"~"," fQwMd').]","PR&T.;<.\U","""gUK74[","EAn<","JQlWX5","1QI","%AQlZmNPE","2|A?","W^XqWP" -"a#,W(`B@5C","",")","Dz","y \O%","&5'U|J.8",";)SH","q(7","EH&+jZ 1!","HTh" -"bzr]","^;V8$b>B","1:aY5Yyw/","yr+&","noUrd","pkg" -"S(Cp&`e]","",">-c|%.m","","C7Y'u","X" -"ny","dNYQ","FWYe;y!","i?4","1,bY?=","`EjF-M" -"8;;V%","%wY","x~^veI","t" -"kYYbvo8<","C\|",",4g","Ax{sqF-Cv","}^COxg 9","o""" -"UPjp%M","!#:r.8&""Xw","W!/h2","^%^f~","t","oP" -"T{6","","Om=~""9s","u/o","cZ@.F|B09'","LI%e%65gY\" -"5 I","r","'F&o""e","qn{s $lV","*}tz1","" -"M","E74,J","=z3U,)","W[.!,","e:Tr&5<.h""","y$Iwo" -".H""","Qq42V79RQ","^Fn-6A0c{","iZ{yG}nV^1","Kw:)[","0-YkX}/0" -"XZc","","o%Z","","q{,",")J*z=wr" -"cvOCJ&O","x%cJc@","O2bJ2fo","m","","ob]_Tj@*v" -"_!sCApL>_","","qWa6A#$""ww","3'","m4I","L;8( {ye" -" ",")~z0g","YlIA{:Ey","","","k" -"ax#""rfEO","","X(/Pj}","CC?[0","sJI","","fir_*o","8 ShApVE_g","4k6m" -"&>b","c=9","!","I","","$/dYTpip/$" -"'","FGj*S|Z","{N8","eonWJSsY","","U" -"EL|*\!V$1`","Og""","","","lo!:^",">H1&sgt8Z" -"r~:","u","'/Bn?[Qf","nltE-YYzDB","I'ODVU{xr","" -"[I(]cD","<,/","x","","2x/fH!8%","O4a~X" -"})","QoMIAa","%G@","46;pHf*","}3rU @V","+`eU&/gG" -"HD\5","!f","j6OfLC","jX/UU)k" -"","","2@","4bZ+L{","Z|H","""p7u" -"PD'!gTiPcA","","<","0N4x`","n%\E7ol","$duR1IFA2I" -"","d_B","|","aa7;ac","","UviR" -"O@ei*, ","u'Tr","NJo","B*E^/Xo?Y1","BFk""){%:","TS?>s=?= h" -"Lt","f","PYgD c","a4/",">2?+A~+S","IS""" -"xG')3","q","","0k>U)ZI","uHg)U""X+","}uU1?*" -"M!0;Kgz_G","+","1:g}+","b,TC;%rTQ","Z+S,","" -"i=lps","[","$^cP","n2",":0,Wkb","9[URQ8>&" -"@3","UXHc|C","~DLL\QV||[","uQ.8 Q)+q","""","A" -"x2ZrZ7kTQV","","!R<","G%\fT=","E","-qG0{" -"'&)T%oz","{kBu,","OF6vL}FsX","OT'5=[nqH","VdOw+[WU","]SZBjN" -"","rdpPo","Yh4[SDH",".^t","fJttaStU","@%5!;^h\" -"/uT_)[","""{","[up:^Qgi","6GF","5j%$6xE[g","&l" -"&>-Lf7]U","z>","$qPD^>O8","&IzR","e","}" -"}8X%","yDw;c#""b!","Y/}~G","6Oh[ZJIwQ4","km+ _8D~","dTCu58x","sSXv","pBuA>|X;_","eM!):?EMc",".KU>","Vtd`wZQ}" -"y$?{2Ti","}GZcl-","\kQ4","mk_l7O;#.X","KDd[Qd","Gg0pL*" -"sg\dV'=5z","!S""nEgUx~","","QcOn","6","#" -"N#fa","a1|+SwUd5","rC(nF`N0Z>","nnPaaE"," ","yEwZ6DM" -"V","^YU~b","","*bJ3,N","V={@V{","_>#Z0*" -"I-~QAavV\","K6;#0k","o","8(" -"gwut0","","WU^","4","/!][iG,z","""W7+uh" -"j+?WYE2","w]&o4Fr","bBV^SU}Hv","9'C","!Q_7<","" -"J*ok_L.*Gk","$Xl","","_ut","g h7,w-","<1" -"Y>)?WdJ","=","""n","Xn2m[x","Q]mFLF","Sh/Ns" -",/^a5S;","Vxs%'","QivnALbO","","+b8","Sk" -"2","2KNAr""91[<","VJbQ","sZOSuh(5&","","E1+D" -"W""","[V:67mEA","~]c","J5<+pN#!K","%,b})","Y;.>:'SI}e" -"R>flcq%","4e]fB^","^>s}0","8*hE"," a%","lVsOax0'-" -"!l{<+3a?3B","sH","y","~` \hy@+","_'eQ2/o=FS","E" -",","*Y","c%Uz+']<","M28o"," 2o[WuQ0DP","(oWt3($" -"3E","_S""CKJV","x&#@hpdvR","aQ+$(","S","#r%" -"","}!Q#fiHh","I","","RV<[9pS-c2","Ot,1X" -"!u","yn]v5>","'","","Wkf#",")spuPy" -"`","&NgU{7}","zA]&&{kE9o","h,&|5","jWNzw","`[2p""lG2""R" -"9JEw>3","ccc~#;"," ","80UvW9;/","","zebGg>#." -"2{","?wLn]","OXM","8+","LST%bsEJ","," -"V'","OzdoP^ /s=","%*","goIg|b","u","KKQ:va/E-K" -"\xSS0y|2","&","n~S>A",";in)T|","B""V~,z,i\","._w" -"A","""","[e_o","CJhj;~-","3_e2",".%*61w3>\A" -"","f6bOj/M]-","nlZFc7g.",": ""s q~","is/?d)T","c9+sD3bT" -"C0a'R|D","|+","gNvw","","CCZ?F1","N\;C," -"iQTa+b[X8s","""H""X.|#B+Q","AP26-cwO","G","","""EM^D*%%^" -"!ICp*Lw","7[WHV""XH{","[@","U","'1P","RN6lj","F:;'l","*hs%1","" -"",",~&gR.","3fIG","rHMo","&","#" -"JnsY","@?I)p","%37+bN","yS)]_3YAX","\31p!_","n}M(" -"|5W4","@9j-","PB^HQ","{-cCfQ]s","S","m[x64" -"[x","C[zL>vh","h-Ix;N","1-e9 %Nh|","mwdP=M,","X2:d>'0@Ku" -"Mc={-!j","ovo>;Tr","@vC2xP]3","""'Ev","a[x68|<)}<","DX\y)" -"b5U","MDI","aQ,}$v~@7#" -"=""","[[f#7=Nv","mQ","f~\Z^",")""$-)qwJg","I+^]u" -"Q>`tw7+^"," X","[A_qc0FRd""","h_","Swb6$","vRY" -"Pr4","nma)mc9Oq","- TdOChW+i","WQk$A$:N","_IhGX_RS","cO","Ho3@l6","6?","X=_R,8C","GHr",">Yk9{wPy" -"g.XDqYM","J')g@","Nm87","l=?> f","HWQ``Hd",";Oh1""*z" -"tldt ","opHA{-W+)","*PVq*Nnxnk","8",";W7,peh'","-w3kZHa","","+/0|>*.","YCD#$","DowrP" -"m","c4","xd","E6(:>","","_|" -";&d%BsH|i6",",SSk","oZ4" -"24f,,4","bQf/","r\;","zlvdPU","","Vkkp1eZ" -"w@v4m ","","D&","C34$","ZAgUJEP","wfSP*DFUB" -"|\.B[M,|=","y","uV#$""","jS4i","w-Y","~V" -""" LS","{","","al;p","&,ocis]","qj" -"1h,: Pm~^J","tN5zO*QW","<9lU_","uEbd?Ms&$","%bA3O$a","" -"","3P4B","Vh*7lq2","hQuYV;7","0Q","~""3~3yvt,z" -"Aq","{8%N","I*,ssZ(:>u",":68[{yN","","""uu]3" -"I#]O","Gh)NfB##q6",":p","rENjq","o","iBwt(" -"Uw}}wTD","6",",e@rNam","WO","x2tPl","cR9h" -"_o8go","S/","BWo_","z","","t<51fb-j{" -"Xnp#=","(GIR","","*WjHzw","|" -"`ikUg","",",Y","CX+\","U9\HE)1Gz","Wzs0" -"Kt","VO34)\\!*","t~zJ9uYT-X","9`9iN^"," Pr","!F_+Mhf" -"ij+W","jG","","","a@eHP","~D2:" -"","f[fd?","4V","]{cy<>44]R","tagc@me=","Yer" -"=","_ ;Tc","ji36s//v7;y","!Uy.RT$z","","PP~.C,vubY","k3%am","2Gs" -"E7#&8hMZ","-o","^G&","2NCWWjXL_E","","OFIQ.r" -"k@^b","q","wb)etgW2","bE",":F|>=q","K$" -"jg6#j","i","c","^|)2YPk","+<","-4A!5a" -"^:P3{^S5","Z~Q","^.cKSYy","","t*34V2>","t-""Rn]""V:" -"","M?=gzH[v]","[6uvyAC41","d~D","%xe_X\t","I" -"zQkYWrs","(#F |*x-","Sd","h79{!","NQ1LSo)6","=v`fl" -"bp5tK",")YaaJs8","w:rSY4SNecLp" -"vCJ,a","wg;S8js7vt","","0d<@-_O)","{MKV^N","^2" -"d.D`","'""","x.[&","*dcV","{z","6y+0a" -"K{:u","-{u3aN""","Eqr8%O:!X ","5>!p","ZX>0W.~lx","/p/rU" -"taZxgwfN8]","","]P:9M","M30-ptS~]","m4.7\V#'?","dvB?" -"3rq%*Yw","%f4v:4Iu","","5oJgGtPUIb","JI","=D!R*~H" -"gkVe;yG","R\oQ^67)3","2W0|b+ c","/W@0[;]mm`","2<}8I@z","uq]A0=v" -"YI}","}XVL!C",";i","wL#cIdG%","eod 6*",""";[qK" -"r-I6%#","kMM","b<<#4f57Gh","LQ$EPga","kkVPwlN0","Y>%wr D5?(","R[3e,","-j$<","]6aw" -"foY#cWjL","h6{BO4$","c+oVNk","x9UO0","{RIdlhJ","o|E" -"h=ZW!:8Kl","oqW","","R,(5H.piCE","0T","*fAdr" -"e","bQ","","[u;.","/9r","P ^H*X" -"n=ZO@A,S)",":3OyJ];G2","sS","{SZ9?nF","""-0#","p{n" -"8N'p","oe`;S%&MH",";}p","v]@eS,+B",". Sv+p$","BQ1m1HqV-" -"19]4(J","$:+"," 4Pby*H","Gi26IA2<7","AwIo1~z","$_x" -"{,'","g+{!s}g","meNrc+n~",")","WIu19/","FUi!" -"t,k\",")4aJEL<","e3","18","=","aiBKyxK" -"11C/eT-N2","v$","O0Z","u*E9$&#dR","J#{}","p FFaC&$OJ" -"-~J","L:K6 ","?","1","<","vi6G]s?D" -"0-","$[:","H","D&A","JcPwaG,","V" -"9,f","aC3","","mq}","4","{U|@/l2T#{" -"&N""jfZ`","1q","sjM","PrM20;(7.","oA3xzI\","O!H" -"$QX~VR","H/uKon","y-:V","Y\L;'&;q","DfLF5/FDPc","3>MA.8]a" -"Z>E9a.5~","$F?","T~rp{iU","","XWh)V3","H}" -"2","MK8?$x""w|.","","V$F6J.!SO","","8yt" -"_NNof4","QCw-Qh+",".J4xa","jVGk=(+9","G=|q","]1vi>$k+d " -"C9GS_","1>R/V0y.","","!","4pS","E>O6moM~ S" -"OX7-=<(","Zyow$o:,F","Y2Zl'Z","+9Q","xC x9]RbF","jp/RqMEwy+" -"!4BKn>}t","EN","MK","mKD","zTZq'3j " -"b","{v@","QDSA","{--/MV1","F~<#`{9^Q\","bY" -"DBuM4""jm2","e{ho8:","l8S","7","7I~-&S","D#M(n+u""" -"{n~%(z8m","T{o=Q","5","8!>(k1w#","!VaD]",">b~_(^" -"JB^(gmt","kq^,","A#yVx","i.o3ia)kRm","ze'rP","[k" -")#","-P' t~","c/6&","U}","","a{tY","ZoU*EKdP","/X)qa","O[;\RyO{" -"Y","*r4PX<}","PGrVzC+Q","'W;LxII:xo","s&?a2xX","(" -">,E.D|OK","DuqRfMYR/","|=c[rOgPI*","PHCU","E[hAIr#!PZ","" -"|[dE1`>u^","-cCdxXD+>X","","0U(Np","","hfDi}:tI?z" -"!''G","I","X9","a","`4d,?]","B%)("" Q" -"F\l]\","I8)$]<^~","-?v!K7#p","","!cq:k1","/`=" -"","DzvVS","8 @D^BR9Dr","%z","^'GG|oeza","@-" -"gT^9f ","","","Q}q- }%G>\","Eo1<_&+","tf" -"`{2","bbVEi","GcMv`P$]a","bU","v}|B$vu=TTe-H","Za","Vc+M)kP","","Gog!3c)qY","t5gq|Y2" -"'+W5","vs","c9P","/hn)o(B","0/" -"\+EoGy/w",":7QF$]|QF","euVo|G","HNtoa5U{S","g",".-UFU2" -"",":OX+","9B","N-R;C*J}H","/","]" -"/z","/QI.l@","","n","=","Y)tF""sb$" -"%ZL)LVy?","Xkp*,",";K","w","bha""","eMqxFJK" -")mE)Ldb","","{&H?UX#%ZF","T.3zEzOIX","""A2""Q |y","on" -"gt,^[9>As","Vt*","')T%","M","d@I`7n*13","&dBxYQ-" -"*"" uH","^",">u_I;)o","]_f3d.","t#bY7M](","\{Ge" -"["," ?""o~aBm","r2*TV^2","{mP^$:B","","3""" -"*S)","hUS0<*9","x6F8!Yh","f","lr%""dcG;","P`e?\kp`#" -"GqL[_G","X>[G:_i","S5SIm2","HZ","v;gZ=@","lp""V+')i)E" -"'\%","","M@c2","1|_;IfHt!t","Na[b lUC","cV" -"","","N <","n","lDbQ","fOT}A&sROX" -"j#=9J","kGB9l","Krq2omq9","RH+[b","{ MFOQuYOY","1?NGx\~)" -"vsl","[r","z2ZYUr)R5","jLO","=9v9","IJ'S" -"6[J4","=.2dB!","(~qz|F","8S22","3.^bP_2vzx","p" -"x`58Q2","XlP","ATYEQ",":eF_8Ml$RL","","5-?UMGT<\w" -",CB]|vx","rWOb{k>&p-","Bp0M$-kap","yhl5<5f:","E]a","]=/HY]@" -"tn~","`=_829iI{m","V","CI2,(","","y*W-=" -"YQjb{","m","|?U&k598","","OZ5~","uf]|2TREJv" -"\"," >\$^EY(8_","Rjm*""t@","}C","5%1Gah","O" -"(q>Vc5a","=/","Ye3drpl","f? = >>",",,7l""%v","e!%ln^.<'" -"=mnib0@a","sr~n<8i(aD","ky19#u/ApN","u","?gV,i2","'#-,[:" -"B","h(GdA","H&","d1y*j&k","'xT-2c","7JlgbHY" -"9p1k",">","%% iXeV1)","p'z$we`=Pp","Fq","I>Y1" -"ZsJZ","Gji","\","*#0 h~(^-q","=MLU,A8s","dm.m3" -"0.e\O","|nW","6'?L&<","EJa","JU1","s_#Qhx" -"","Eg9","!mOw=-!+ol","*1y,8\=","58 z[","IGE^Q>l?fI" -"Rv(p",".vd~96=I>s","3u%+E","#B[0@","c","bGq" -"SScmE","l Kh","l79JXC3=VZ","k2","$o""~","p" -"uh","ex@7k2p#m","bl","v","c"," $)T[V" -"f:ryalK","j","""|ARTeqi","","K.)4" -"N8","3u~","TD","q_I5L2J3RJ","ye","h8[" -"Ko""$","uu')*69X","3*Bh/P}","\[G%U'6","6%?","5UPL,1RLK`" -"KesVA?FW""","{lIto~*N","_=?g3","qes","8},","NT)Hre" -"S4Pd","}BZ#!&","}g'G*IAs","]=tw}",";1yXn9","90" -"4X+/""{+Q","|gxc6","4B];'[Yj8;"," \5","d]","" -"X\rKIB{V","]b3N","q","5vTkM}T7","lHM2Ax`~[","dhq" -"jjz%c;qcn","%hr\`~2 ","","]nLj","ndwjy{o","e_h" -"Y[$n?3"," ","!","","$KJLM",".zA" -"CX'8`Y,","k>w&T","E!C?Ut:Y ","Zre&","","" -"XgDa","soo-9u:0q","K",":`(Vh9RTCU","OcN[FV","`","@{T!""" -"^\s8:FN'","3f965+","B$nZ8`$;$","N-<","-?e","-Z;cPI" -"3lG1=W]NK","O!E0I","$t>r$/Jwo","E","","Bbp1" -"","J[4","J.W^CyoH",")cD","L","eV&[/" -"9@HrYJ`3Fz",">4"," /o(oz%'Y","lnT+","Vehm>,E*","" -"^qTZ_","%E@5So","~>","_sg)T_IRY","ppXj4","%4'" -"fG","b@D~u","P","0c","T!~%w&anOY","q9?uTURqZ" -"~%v`aU","P",")k","*""","4QXuI;\>","," -"SElfh$",",g5x73","r64`Y","""&Hn[,dk0<","R-c*wcq","""8^O5GKL" -"",".","2/;","=uQO8Q0E)~","?-T0-K'a0_","*rjf%5 n8D" -"kQAL2EM*yH","ABf^~84v7","","*/","&:]'.l","+(@","Nz*QRNB%","T" -"_y$##@","","1N!7#j","D7)DVq","sQr X7r[","L" -"\R*,^A(","yFa=6%z","0#[aK","Grw!S$lY}h","peLP","NO" -"*!w>F","o8EU@","I|rc>2",",U% ]A","0","JS/z " -"p 58","@Py@~HGC%","42B^=^="," &3sM","x#3J}S:k",".}67" -"","m`,v","G@4","s","|t.}""h|.","b$R" -"5%","N0U4","","9F;r1:""H%","","w7^&" -"l\{m'0","{","""""~/","`j34B|","y*CLY4E4","*ny" -"BHw}Fk","V4T9V","","94B","b9'yX""","@q\<_Q" -"b67Vf","1b$8","Z$Ul","pvp0O4x>","3FvrBg?j2i","=M" -"+ItwpkKY,","^ady6^nv","","-wsfp?","LvOcvq","Zw" -"`te",",B=","mK7_A][`w&","!8@;`#!n+","@3o46!S\","" -"Wc)E=FQt3","`lkmDlK","~GO]!%E2Ne","[icn0_w1","sYsucGc","0" -"","0","S2mFv1y","sm""Z :",""," 5$" -"","ktca'+_","esj","","52l=I]5","e,]I8Bn" -"P%=2T[d","/+","63$Ze","^1pKUdv`#","=r!","t>kjX=.|" -"~a|/T","O$S","dW^$pZ","/NJOxu3","m%","S""giH{<4/F" -"sV9w|60?$","'-k@mf^","QJ<","QjO']-D~","EasA","0{8l`h&#" -"3-/ch9Wx,$.","yN<.t&!.","d%IVo2Vzf>","lH'y#fTEa","8d^XPhWmNG" -"l8eVte","{gYR2hLz)","[WxTo","E","NO_8)d=I","b+@ZqL&asM","Et\" -"KW d","|M!MP+&Oas","\z0`","U","g.k","HSMo" -"E],@","7=Kx$""","pA3","CcM","l=a!="," kvcxW" -"`g","Ym6/V+&3z","I3""`1l*Uj","LrxBm:3i","","Rw;*X4CH" -"N","Ag","""F7i_z|&","","Dl1 {N*05i","?N" -"Ox0hNYyJ^;","","2U|-","<|kg=[SCxr","7q?Wj","u>J" -"wrkVHq{","UCs","D","V^[oTJ","C4*K3","yz{Q" -"M}`0+?1","1F-@r?","s","]","ZvOZYk" -"Hc%lPt+i",":#j","7-v20","I@\/{)^","QX",";Fw 8G>~" -"d-k_FKj,3V",";M8Gi""`F(I","m","Co","|.I{w","Ph>NyA" -"9@@?;","t:$x:^`","r?i","e","vK@a&G(","FG3\7oh" -"yxwt\5,o,r","&3s","M?c~oD","'jFO}OY","~t)V~.","(+w1j" -"=pRx|!E","wE>*zq;0Pk","","YAm&%VT&)","j|.i","" -"+Pi","*g","]7>%k;","=.*Ywgzs|","4<\B`$=4","vKY]" -"H'qizT3 u1","|l2#vYI{F","0","C.W@ ;8","=!&&;<","p,2{" -"FMxz{KT~","30}LX[.C@E","\5Q","","pAM=","r+r;eJ" -",","'N!*""","@","miy#0pL]<","mf{","#z0`s]" -"g r&","{",")l@9","P7,j%","0Jt","v" -"&^('>1q9x""","cSE=x4=5","N|MptOz*Z","","\;x/8m:","#T" -"0VJ[","","NE[jM ^X!","""^(Odv AY","","0" -"%7MW hq-","Nfl","?","m:7Mv~}G|","&.x&1","}*+qI'%" -"TLS ","8pCh0is","a","Vdg\ m","cP+n,X1k","/TD/lD7=(~" -",r 3a)o","mOV==","/6","~]eD","","=u""c" -"#-'mX","t/{Bl8","69_z)g\|X","","Ss;Avh}m#6","Zyl+?g" -"3","2:t}71?6j+",":[Q'`K","","))V","c" -"bxL/((`N'","A2","btd)xs" -"`Yo~7*KPX+","x5b!R@#","Z)*pc","VrckVlGQdy","","(g%'C1","n+.l0B6" -"W)#l? 2TgT","7`","~a;L6","I","q`v/|lbQR","$" -"A""Gx{s","dc:vtO;","lx`4,a","V-q w'%","M","|q]" -"*8","K8/|;"," ik<`N1M3,","/","","FJc3JZ m" -";!","M5+%]","+Kt(542;","!V|","pu","]Izuu}" -"+2W/#]R`","",")B^$","","2MK$<""=qX","^dS|" -"z*","","}R~eL)","-]sC?%"">Z","#R82z)vsh","mCRO`$a" -"s2","B&*Y%uUOOE","T|c!","6","*iP@.QXy+F`T(5E","aE=o=f;","<","dd ","NJ{ &axo",";P\cD" -"UEd`F^m","&-gNnM","$28lUuo","","Wl","]#" -"d8M","+",">","7","ijMrGEC6O","p" -"f@_","[","x","b>\f_|8G^","N;,nK","lK,f|Fz^,`" -"frpz","","jI|qqIi","E9N%x","WT","bC(sD" -"t","MJ1> x3","z@IF*y!zP","(T!gW,o","m1rNM","d""A;" -"3Qk.' ","$r_:","O=G!(","","!cFa|h.Q0o","'~u(\eY#" -"r!vO[4","\","0_&w`]mIb{","m","","r " -"V","DDU#eF_f% ","A&]a|2C/","*Sj3@$A0,",">1\41PE","Z(P3aR@" -"","u=Vc3ls","twjqlZX*","J_,H@2","?bgPa","aIj" -"Zgw""MIQQxG","P","?{pK^n]","Nr1 D#xJ","D","iVnJt" -":E&n","_""K4.","%x]","","T@Y\roh~N","LIL9i'8?" -"","~u","nA3 $V>0","!4 {","WLO)I","}$c=1$" -"f}Uc7g3LuX","+2-P","""x'^.","?","y!U!2-Kp8x","~F" -";1r""A>y9","","","=","P","" -"tjT","'","Gvl-"," q/!@N'0'",":XM","e:","jb?BuRT:" -"NR.%",")]5Lti(2~@","U=tm","WrArhRl""","y~","Eo]P" -"_ADu Z*F@","$r>X","mOJUz","g2","g6[@qWo5","C$" -"o(x]p","%f\","aQTSj","","Yh,u,X","5" -"JHSiP","WS@\","",";Y,{QZ`w","+![6/)*","",".X6 ","zx3p","l+6$g","YB7#~+4" -"W(XOUt","","ABY}~","JD-8o`g-XJ","l>ji d","2:Fb&Zl|" -"2h((k","P4]W,)","@zn$ux","P=","+f","#_" -"3`|e#/Rcv","Y",";8c{","zGY5ox",".?M~s11r","'r19)SNG" -"w|x$1#","l","{wT","*jYl-y|,O","$ @Lql",")um" -"Z[Q#",".jV^yJ","ZyyQm","Nr<+","(:1qzgtB(","f" -"f","UWn""Zf","n","7i[","gY=;Jx","" -"","MPH@{e,c*","Wi?~hQzOfX",",~Q/w['","t?x{<#N","CRcD" -"x+%Dvo:E{","8 qo","&+&ZDk$b%x",";\0> &^KZb","y/TR'1t+V","" -"q]\0|","iU>","",":#?A","L0;8&,[yhK","y[C7" -"(.",".et","a","\P@2Y-3{","","""wU\8a@" -"c[=5^}4","&I*P*","aLq5\&","N+/2V","xjE} o""","" -"M[U","gd","-4l_Q12wq","ZZT]L0","","d.kE)8B)" -"4uGc8E%m)E","gh-+X","}i","gZ9@?","Wwni","Xbq~XQ" -";:","O}ojl2i","1","^a~$","X^","$hA" -"GEJ","+`;>:<","VGU","@r#c-aRYm.","J>>","" -"fzi","",";hQZ_+b","F6(=` TY","H","M'AI$0D&" -"+","5!CJ0'!@","4GM}q%#fE[","","^J#mf7&igf","4T" -"f&={4VuYh#","&K%z","BRvl","<~UCW7h7L","-F:ve>eGd",">E" -"HIO","/~1cy9OMg","G/({s","}3%","","/|>]w%j 6" -"""#L6)","+]",":""^","","]B:ZZ","A[" -"uTPB$d$","j(R^","p1(*L?","Hu","","sU(HeC" -"TCD~","h^,q2|^x","!{u",".>^B&" -"b@A","./CXutA","UtN/'|","eg","k{OZ4G$6","`M""v" -"02#SV+,","kL^DY7`r","}g5L!c88`" -"m5myog1s3",";x","x","R!n8H""","6{S+_","Hc" -"h5f}n4~(`U","V","uCIpSU6 0","Zw7WX~uzbr"," Q*c","" -"iqH+rA=5j@","e-","+p:S:"";","Mj","s)L[INYY","U" -"M","<_=_pq","(","""~ge}/<","N","iu",":","Ai7 oxh","Gu" -">#K","4^Of:&-","l n","","""b_","tDp@B6" -")\&e^@yFb","c","- X*o","Iujl,;X","y ea`4","]FP" -"8;;tGHb","5GI(]8%","0B:pg,a3^D","E","~G~I{E" -"&|*YT^u&","UI__:|l(3q","z","euO","vf}'.J\MH","E0" -"^","&d?G{&",":\r","kD","EQE$","E4To~<" -"L","Kc"," ~","0s","mx|qUV","JuTeFhO" -"}N'\|u"," $fu gYq","5C7[","H+i","@3$ K:","tGW" -"#^","!){IZfj[","/$+;P]0l~","5Ga(,V",")a,","j?@Ih86qCz" -",ev%/3Xm:R","","y~Ev]]>","G","`7\S","" -"DbsFI.L/Rq","\&T,H^","n0+9hT,Qo","$,~Iac;i%",", ","rV`?gb4>" -"[)ZC","","a/_X$N","Z3xrK^3",";?<","8RX`jTF" -")'pp1nTL","\81kZO","qX8","#I%","qe|y:^=m1","w-ka#c7" -"]4TlU8kM","lR<(","C","0]O1E7*","","iay" -"G""v3v!1d","o,","\1we7dU","9kc#Mt_""","v&""q{ R t","t}ZAG" -"t","h/l}","_","Imwy%CG>RW","4SbC",",vE" -"t","@3>ml","T>}C ","pL","C]i(%S6e","8W1@e7","26A4vD@|b=","7l#\\X'gjm","ZB","/THj!^C","Kfye*" -"<=e","}*","t}kQ&Q0dg","|0b","0g*Ub!PT","G" -"n)FmG"," $)eUFe:","8$VD1'4X#M","d]yX&)","Rc0VIn","bTK;'R0q^O" -"-E#e\XH","`%4Uv\J9ix","8F|x!t-Ic","K+=!91k","9>","/N" -"RW","?'F@Cqwn%+","+.W","m~zxbMh+}E",">xL'PEk$","o8(Jn3#7hB","4y!@1Bp","D9m;","6-" -")=,","l =,#*#5OB","BNx","hr_:hKS","PS","C(AZ#o+," -"#HGXVPSQ","SQ","`","@LAh1X:","}q~(fo","P3RIUzI" -"fl6A[R","mj(","EJ/3Zpb-j","d","","rTr@^m" -"Cp\c*Vwsx","'","|h[;","G","\\U","Ex\X3Mx6u-" -"st c","","U!Qz6Ebn~","5pR4UjCjVl","i","/j^4N" -"@:","","",",w8)pEm>b;",";","gH*J!c" -"","XBs'!4","0&:kw","X,FD*","0PBc_9","a_" -"d>y&","THAl","","8e{",",3DK" -"Z7>&JO","XFr38","`","Oy1k#^",">iT3$!.","yMSO26>zA" -"kD;q.T","(ft$]",".t~'ecyAu","hd+","|)@,x""$8","X/b}1Z{" -"=@>j ","UnNh0I7(%-","p5F~m","-Na","WP@^","P(#Lp q" -"","(""nXpt:3e}","qF\","ypb;","X$","._r_1Q[G" -"K>Biv","~J~!m:CW%","y`ntR"," rY?_E_c","'u?,qp","x" -"7o(AA",">(;v:","z-z5","n }A","[79)/6.: k","" -"2!4CIt","2W@C","3-8[B","bQ~Ug28","/$Esspv","4?Daz#aje6" -"%[jTK","YeX,wa","9A~|v","D","!xR~X","h: ?smk/G_" -"/","OI>P~Cu@$","v:IQ\ZnkN*","J?Sq^6Y(","}hQ%","(N#" -"^f3M]~zqn:","jU*@%3",";z8`HsL*|","-qq","T2","`p5""" -"x*l-","0F9MDy!]o","JOynuf","9%","~!","5|" -"Dj@zV.","E}- |OB","2p^Z","B{fi:?","eL%>Pi]J'","9C""kwa#" -"LX}","PrLfFS","/}>a0E","("," AzT`6]W"," %e8.>4i" -"&Q.W3","""%5ohk.","'O","|eMU6y5h","?","T" -"A'"," P","'","(9-","","" -">?""_@uqs","<5IOk""7L@",",a@y","NbodJ","359fu`NN",".]g'\9" -"K","3OdL2","h""q_j","Qw","n5&=Er`D","OCU<^G:e","V?83jV","je|?","<",">=I*C.^Xj","Cmev" -"T","j4,}xxcV","cB","q?H","Q.HtJ","YX" -"`'.t8","N\","/Dh","j",":E*@Xj","Qs" -"b;fP""MoAZ|","u[hJ3T&gV","HU","4vT`[z","P2$*","cn%S~" -"?r2u~yr0","#55t","=[CUa#q","sG8kHNl""d}","","SJ9" -"TIUv*","vZh.EU\-","A","U","m","S@@" -"",";n","03Jp","h)","Zz:w72dcO","W2tdn" -"c[&","P%!","OYe#d","TpT)","U.+^`K","M4n~3vq S" -"h}POP'","]E","J4?3^","DGg7DE.'J","5{3fzx)%e ","lct;y9jLf" -"J3fCPT","CU~:jg>TUq","J","r1K&","%yx"";S$Pv","N6vZ1DN(" -"j9.F5","","\kiTSFFk","=Rx&g8","0s~Xi","/0peGo" -"{4(X1","<:{","\=F-!M2[","PHTy$Ot!:'","[DQ","L!f[$]]R" -"","]","gJ~imx(c","T.","'^&=9Z","_+Q#7(g:" -"M4'{","\N^","^","E6_(Ap*","q]{'n]","Ql\HKo" -",E+>j","","F{y*","","{6[XQ","h9" -"?/55z>e'x^","/","","SO`*jK","","*ORgj@QyG" -"!cY","uO8xt7 3T","ld6~9:J","RlQ4c","x","5l=m\R" -"63vLt","DFyH","=mm(pc|q","(","vX",";+W3S>;E" -"""C4-SSl F","=LL/+(","K/Eci","","","" -"WtI7","D;ou.un","R>D?#bym","=.AUe""GD","v'\&dfNfbK","!(""V)|)}" -"y;=n","es[T*o2","~kARTB","@)F""X",".!74L2Kf*G","4" -"xwtD","(.&$","$Z5BJP=rXD","ZTw,""9do","g","V" -"R&g5iIJg","Z","vGP","wG{Do","1Z","KRN$,b" -"+2z","9dX/j,\G","@=","y$T7O<{W","""%`jV|YI0","] ETvdAF" -"a>Qk","m@VB","^","","","U" -"H","""GE&","$Q","O|2-","","@C_l""ct`rB" -"BG3","ie","","{3h6'Q#","u" -"rf","","}QlF.w&","ni&a\","XY>vp","5J%51T9" -"|l","^U","A&ra,.`~","rQ","J6;rt",",{lo?bMh" -"WrYT?","q,K(gK0.","?","","WcEt","qL","`e|Elwp","GnlWNJm","`7\q%5[=A",",""}" -"^vV#","y;.\G&&\@","(URI@6a)jQ","m]~M?{is","rYeF|","]N4" -"B](![19;W*","w%J0@;",">OO~aO!mf","xPmNoX^[","jg8'JU","sYq*" -"itA[","@0lg|dK","LP*y*","}+HNr","?]nzT}","" -"tA?m'$\","rs5I5gp","6i*q","]",") %\L""Ce&$","F>i" -"*~Gi#","?sO","rj4=k[Ke","0G;yy","Ru","*9TU:$$.O" -"]","iS","-W","%#~#yBr","s","&U8r" -"","xRBII/","s$#","kSO","k8yhB?j^","!V&SSwp3" -"K>Qu","o|'PO?*q>b",")(","OcKra)^zy2","j/","yQ}/^08" -"03q4o#@","b0+K*yVO","RBD&zp","d*CN}Q","^ldUmLC%(U","@Sq|;O}iV" -"Klr","+sJlb|It","f","vnJ","",""" D[" -"ysRWT$B","e.)","@&4>1*""FLl","g8","$[d>va9","UbeM" -"","IzL3Ad$","^mGX5TK","rL,cp5:h ","Eh7)","""rDf'L" -"Q(+#c","Oct*:","","a,",";O"" bnX2","RE#" -"v|I5'yz]I{","bf3sNoy","^+N'*+""""[","Bq4@ XS""in",";j-t@Eekg","" -"9","","","J""","QCiU$s","5B/n" -"LpU.F$P`&","ej[p""","W?,0T?.c","}.L$a","#gUoHK;;","&=eHz+" -"Kt(|A","+","SY$ ","P7OfW)R","7|","whiJ?L=" -"w","+\N>_p2","BR^5rgWlJj","y9m+","K O#TO8","U" -"lEvw;WR!q@","[UCCz>,^;e","j!i&;4wcr","*V""8>u/n1","!]cK","dX=39:U6Ys" -"Fyf,o","1y%.`D32q","t|O\&","wb )|","yH{?]R","" -"]-9d","Jj+Rpp+ C;","1v,C{vlO&f","+SAr}""","@+]JtHx7R","4D'" -"~dKoFvL$","iECeIUXM!","q",">Pi+","l9nIl","8$x)(<1t" -"f_,CGQ3Xj","!","5c>N","n@3/","-Rvm3","RyLv" -")","g+{C8ep","=y","t8,//`oTw"," lS.'Z"," " -"G69JD&*",",e=","4dW:q\c?","FBPq","I","1p"";xgMO" -"p;5","","|@]","/0c4",";y2:",":_'AFa=" -"(","","","mJZW~#+VJ","z","P" -"dn}Xw7,:R","r dT","4x" -"R6Q.6r",">j[",",euQm)K2:","(","<","I'#" -"","k-{7,JW,{^","1","V%2#oE","Vh","ehN" -"kH}pCM","FAtz.d%%","m","VR","PU[;xxP6g","j6uUPb*I>" -"DfsKeW","e","eG`","#H","r""O","" -"9","Hzul4","#%Ypyq#p!","@aJ<^?r2N|","c51 ","5u'hyQ%DK?" -"%(WA*EMtVq","3-V",">eA}3lY`;","#A+ FaSrI","v#d","uy?=%?`?_K" -"J""","<}lA~","mW2NMb)lR","2Pa_?'","p[RitFhgG=","MH4m" -"?lnjj|","T1$","","FK","K\'[g","TN" -"g","","","","l=K,2J","o)\j" -"{U","E>w","7 'n$","^o e8&y","^Tx952","" -"w>S;","6kEOp8AOf","`h%Q2C","T6_7","h'%W","M~wi:fy" -"c'",":plRS3G=S","H:]Fm*`","NkOlv.","a8Io)","(~mi" -"r@M!aZ&-tWFh","uo>*L2`9f" -"A","","rI3-`YN","","Gh `>","an!S" -"oj>w\G|47n","c92C/nk","#","Wl2(yA","Y'","/ZS(32\" -"1QcaxxO","h#","L+-5PIN","iFyXP .&=","WN","E^Tl" -"!p~$[yne3Iu","",">tz!p&[*" -"mV4z","4+|A_QZ['",">,HPML","$}j([""V$X;","S3El","$" -"H?","/jz.{^<-","dm","O","gt]","hHrr?" -"tq""5je(\","IAuoD","g",",O>JKBp","SN","nS?" -"0i6B","x8^Mvu6yv","].","vjE_HR","1YG;zP~[","bR" -"i2","h#8zA","","(~W{CAn~[G","8E_|W4c0P'","" -"3$upm","""vAmbyb","msU9 3eI","hg@","2YHfF^Og$","m" -"[i","'UDP0Qw3","NHh!@M.b","rQ5","aWf)~6","bIjX""nh","`e" -"oj<#YqrR5","!Li}D:","deQtMk@?rI#","A.p","7k_h5M1V+" -"","","uVa)@Pa","3GB","-@dPF","%" -"eaI","<_f5","k6Q,!#&","{tH9e&*&bJ","TPV5OCM","8zS)'5" -"6F&dXI`Br7","7JM+7","mB77Uc_7","@74#","`P","M(\#>uZ" -"q17$i","","L^e]","75$","Q","l=m)KS" -"`{1","Z'! r","!R","WY~|l7",".wzMw","%YI=BpO-" -"r81S>","c={*","^0}","kT,r00+ggb@B_S","9_T\4IgTkB","SVj0Z" -">_Q-qN!{","kb}{FwP!I","?[B2\q","E;>IlZDY~","(SoaD","EX" -"!","FR}","H2jx","BQBnG632a","SO;k:""","~5VQu-O" -"E:#","BQ","Q","kI","D_","" -"mw0N?=tPv","]L","W6X@s_*Qg" -"`vX3Nwlq.f","[Y","pm$[;p","?8s.","1kkM^%!34","#5%bI;K!" -"!%M)fm/`z=",")x;NUFk","K2",")","",":3" -"Ndb","NbP","!k*==","8/18eI$","2b9KW}N0:","mhH%8P8" -"s70","","*x=4aA","wv9)+","_|M","9y`&" -"IJyF+;","ek","N","gs+",",","+E].an" -"\.::f.ha\m","* VMjbU ","nGQR?y,.6M","aEq/.?/_~","$qz,","nad""@gc3" -"(-","{nx4?QG","us","*!e`l;&(h.","0","<" -"_mlE.o2",")gX<|oNMpq","pl","",">%`%C7p5T","N^U^='4q" -"^p(R","K+9]i,","p0]EYeR$xl","QoP","db1'-T","48s/ioJ_>" -"S","2=t)T>zBd","","n|%(*~f7","","" -"ux'e","M;[oF5/$^R","'QU","THZ<","VrW[{fEX4V","l,dyipfMz" -"Bd.a","(z-W",";V","""s"," N","Q)B`5@5" -"R$oxUv1v,b","6","V$}W","%Hj""+2HYv|","cySY2U^HKB","x`>=0Z7" -"%","g0~3ZePY","D^q","y}E>U{(","<7a[zAraB","ha" -"^","W9","a","}0,A@","sV","$J" -"H+U!@!7ZM","y6c]I/Z|","eONy}" -"a0r`PJf,","","/0E,","SqW4ygj`","v)|0J-`F 2","" -"M^XO`5G","PErd3","'Q$\","?D","gzG","{1I0N)+.vM" -"A","~.'g","BgT.^fY_#","vow]:PZI9W",".c%T5","KMd~f2e]cT" -"/@-nD@","[/|+Q5tdjG","L","gh+`?g;`Xd","i< I34","[FA-9L&H" -"}7","[HND:;'5","}PumJIN:[-","=**","%o/O","75tbHL1,'""" -"K^!","A5DZQ","a/RLgy-)","_","j eUlA]*p:","" -"LD=hEhN1~W","d}6&S","g&8QM \","V#b=Kf6","J?","8b" -"t.",",3ZS.(l8m","ma-","O2E>V{acu^","YM@3+!K","KV{" -"@]2g?m" -"X ","Oe}36+",":ICO8:""kUe","]pgDU|Jyl." -"}R5%?lQBC","9Bu","[RU%O","V&l;&Ve","B-'","lR" -"%-9e","=.JKh","h/","33","~.*1aLx","b6=M" -"T5h?]s","e\2","*3rS@>o((","+\","/EgP6lI?T","]mM5E]v" -"'nmK)5Hls","MqP","!H{T","= 0","","{>7" -""," ","","","pw1","" -"I311m","IC8jc=","?KMP|","-77n","szOUXL/","kd0" -"8xL|_m\b","_{46""N)-N","=L*""+^~zm","k>A0","3=.Wz,","$>p~" -"u+S","N!D'E)","dkNm{","Q!","z:_q=90","b!" -"'+P\#f9{!k","\","8$8L{_iJQ","LU","z","6z-E!$""" -"m{Rs1E59zR","]#IZc1\","""8tAZOs_","[(","","0eWk^5" -"","0ZV","&a.{m]i,}\","'ppVg","gzn='~cj"," UZ x4T" -"nt pZiE,","uw. |'","0&86o*=QA","WZVisJ6b",":_z(^","YfJv&," -"(Gh)A{l""K","Tyqcqa`,up","0oAqe_","W(fBB" -"","WW%r2{i{X0","","\TN","T &*4:Lp","2H" -"Mgt}l:s[t","&cY%","&F{U","","`_&,o","u75~n","@CEPSu+" -"","-+YVyf1s","-","w:]31S.`","f6",")R" -"F","j1{","%WEq7lkVAx","DE\v5)W","`l",".Uu`_mfb" -"=","","!FbodK","3f$raWo","@xHx+%^","b^yh" -"Dx ayqT","-""`L=R-.SI","R\X{0","FD","4{i","]?MT%v" -">XKzRlz","|,6""=","YO>","^tIDT?d","tN'","];0uW2%" -"%.t ^m@D","F",".t;","","","@" -"=?g0I|Lcp~","Zne{R<","KUP4G~At","x})","""j","6Kn-#TQl" -"B=tY1[f","","HS?Gv2Nub","6^","B'kj","HvEL" -"I8`IH0Ld","E","O=I';",",ja(O""#","8","m4m" -"evO","","@IeR","|*.","l{N","CqA,n}gI&b" -"~MV1A6","@1WXON,|","vmx0 qY","&DD","~L","qK=lQS?v" -"Oqc'J","ZcYAFWVy","{Absei","lx1!nc","w""Y1Qa","O}%qn" -"H(c""pHi:","R><&","","~Ek:","*I[c","!twJ\7ME" -"*T","BMEf?'ZS>H","~A!","BL",">PV|\","" -"GQr)","" -"rVe","(-zfq),","FrU","p]|7)66[","@n*^e4","gex" -"4>a#9","d01H;s,Wh","T8<=e ","8BQo&o","}j>/<","3 i3; 4O~" -":vh{_Q(/L","@vKp4","uN2r)","P0,^]3K'3","x S","n^KQ" -"J4=ySK"," uOPOqA],","Z","=V.}yr#M.","48","f+-4S)G>" -"S;6C/mu","Sg@4g","WybQK","E4+9vR .W%","MZ","" -"e>Xr","GqV","a]-Ksd=;;g","{c')0N!","MCi","n|??" -"VB","y)#2Crk\Ip"," iKqtw",">QWer","}{r5EiC","RYitXl([" -"Q","xq1m","g",":d-","0s5q+#","pkfL6jP" -" ","K$","MI6_b5`","y-KT","`q","vL'=6""r8]z" -"","IN","vK>Y","]Tp3u","rg)v`u#","D MOYY-" -"d@|4/~Qr","t;'pf","V%rz!<","","[n","?=" -"w}O>Ks","c_","WS","gOB&>NhV","p[T6W3A+[-","Y6$m.e00a" -"+4SH=YX3""","i1","'}UM=J%\y9","Ssp%","5ct,?kY","I7O,C^#'q" -"4tl 8<","gi.","","isBc","yG~OR","a:" -"[i1q","!","ly@HXK","30@%vS""fB ","@GA","49'yrjXXpQ" -"!H2KM_vP","G(+","Ta","x%A,;",")P#|?","eQT~" -"""FO.eHhc\g","]x","Jk1vPAwd","","}"",VC","3YY-;" -"Q#:!mN.g7","~-u^Z","","T`;L","7(d","W`#y`" -">","F<","[/6uqy ","MK4*","Z+","U" -"/Y!duX","N[/","Id'k","w","%]0| Y(","W62ma`Sm)+" -"","xh4Y5:b+4d","1g/",",bBH&T","","x6=a6HhO~" -"","qMa","s@pn""'H,","@i<","@U%C","Z. VO]" -",Sb3Gl","t","44","x","~","Zw/P8P" -"N[7","K>nF54""F","|&/@hY","B[2","Zi)","kPy*(\tIEX" -"eH$lI","B","E}Nv,31V" -"u","c^3aLxEs","`2f","$&C0WtH9","9=fzolN*^z" -"zto9m*{","q/UC","$0d,1Z]*^","1x","b@6C","Eg6ld|k.w'" -"fbs$""dTdOr","%OcmCsma","Z","3_@da|z","O=","*x[-T8!'""" -"P ","VI9l8NDsX","!)Mx","m7$O+r^CXA","=3',#i]%","}" -"jZQ26","@}J(","!H_rpoe;",",Yh[","@1O@bc!&3~","Q5|g&+" -"MmaN ;on7,","Vo9","XO","SDyl(P6""D[","zN<,q;UGlt","1vu>QF&Bb#" -"&41sP","1L","N","]d","W.SVV[O&$","O6" -"'bdA","",":'d$DGmA","}","","]8Q9f" -"*/87(Q$N","]?","X","|?^#h"";[,","9^o:X@!<'","O7" -"v@rRy","]","\rhx,pKh9C"," f$UE","BT[1d`)a3","UZ" -"&\~7",".",")No8!{&zA","R>","fBt5rs8<","mX152/" -"&","ZEP""","GV0=on","/wu78_S?Zd","@>NT","pO4-$oRY." -" 9I<#5kPq","g,Ss\~Qg","NU","tHr[./:~","ZDRR1@-","P~~%U,iGb" -"'!","}","kJRwZKuLx2","","'%N=6v^9p"," lf""}u" -"e1[t7Y\B1","PM/2 8+a","%OWHm","foM3$:","Kc)",">Cuc_" -":PUV1nv?W5","*","~l@s_","n?shlB",";=N*P?4h*3","p3jLf39<47" -"9x","hHMPhPA","XW9{b","7w3","","44""N#h" -"59Mkps^F","{*aYUU","","","R!OIZmS7J/","7E3f&g=T" -"89E5}=@","","dDT","UUa","","UOa;`Pa+Xo" -"A{G%`","CYN7g>`^s","IjYb","tY8","","$^\TWH1" -"$9Q%T2G","","{B","()x=2","NY&5H{R@D","8cS""1/`P" -"I9GG=owO","Km^j*x ","<*V.a9","]>;_d]'rP?","&6","" -"(","4lAY[xLS","b9^62@" -"KSq*>`^","tamZjqfI""","4X!&qyR","IAw/)) ","f<","FbEL""" -"xK#${E|&;","v&7h","fr","","e1OJ;5OlD$","c+" -"A}i","FXqz","""|;!E$""/%)","(B","0Iw q+","F =+Oo" -"HWDg|","rj,+v@[","BLQh","\kiJQ1QF","Mw","L" -";\P-}G`6","%&zU~KfM^","d7Q$","o\Rux","4rBx(","2_+hr" -"TqnA3{T/Ch","V","hm","A`'a",":","D/QRkBg" -",t{E","KM"," _","H|fHS0B9","v^)?mI[@","5" -"\6|w/b}'B<","syl,","fz-*FZ#","i}Q4S","","[a" -"KwoiUan","&Csp>0%#Ma","+8A&9Hmg","(C0kVRd%","*2R""","5I3|r]" -"jd$pN",">0wc4","t.","","K","U#7" -"E~fj^N]0:","|[ClslGP","Ek`yZ;^v.n","4|TeYq}'j","QV|~<","bHU|!W" -"D.","@""J2CQ{v","/5l8Z","Rd?$+@","w","0y8|" -"","""-(9m5","""Ib~5AkC^D","-FG~","","M;O*bW" -"@PqS+*h","9{H","CAuT@LL""","1/z!","dg""xFuMP" -"N","[;3M<3/6j","x)s","7$)=^MT",",q","*D0F" -"Y","","X[""+{","X3","N/","~" -"q0xB","^_)f..p=~NV7","m6^c","n`oz5\|er","k["")","x]|Q!","ZB0t x" -"","Ie;$W7Ltq","b?+R m'F","R_@","a(aK@)v4","d^" -":a9LLz5o","Tu2 KmR","jeOLC|" -"Ab.n","A[M","O_B","J{p;","oKo>{","+N}2h}lf8" -"@g'z\","p1[rd","","lp=`""H","&Bey","yD&" -"d-","$6","1,l","m`Z`%Vy","G:JVW""","" -"d+""7Rb=4Wk","",":wc7W|HJWZ","n","o!Ll#k|0","k1" -"\E]|","|7M6Pph1jD","~q","/rhg","S`T^VjZ","]p{i2M""@!q" -"0|s","!.J{G","Q]VQ=QS","!3Ow",")>4","EiRMl" -"W\sSAu","P","hd%L~uL9r3","WBeO@W__","ET","p*7)?" -"dDAp1G7a","~Hf{wk","\>mMvO^M","uJ3","2 Rjl<>","JR" -"K7","R_"," ","d/d:b%k@!'","l","i%ymq|NJq" -"@>(Kd8ee","k6guOgk3","""]","Y ","YC\;=W","*upJ9sf}y3" -"I#q",""";+0X","","e","2J","3#M7`" -"+:e[mW""uD","C#Tw\G","ON2&1","VLK~.?z""","[4""Z","1" -"","~.,38","4sz#)QVo" -"4!,""y}).PV","y|o","2","Uzw[}","_b$oC=!","7Bcv-T" -"SZv/gj","y1Dt (","H}H}","","/D@]","sa*" -"*$,1T","(tUe1&","""{;1EG?","bUB[{9" -"8x""GrfC[:","7KBMvnn]<","$","+]35m","-_","ou" -"(*UT9}Lf","v;","/qBh=","8/pRg2P~%","\]1","Z" -"*Hni\@Q>;(","QD""?BA.","^h>","]t","E;Wkr=3=-h","" -"/i7dns[<","zA0M5#@e",".$","","A3Q+a","{Qt""n" -"a^","A5`G#>!]^8","","9","@Nc22${L","L*R" -"Q","ds?U/","c,","","/","" -"C6qs","MEW:;","","","+,L5,^63""^","Urb^;c" -"on","7!Kw","tee8rRNrJv","^;1:>2","a5"," HJOU" -"#8x_","20.tu","=db}Z_","@jbZ+AZ8","VE@b`uiT=&","J" -"9uz","E&Hqd2glvv","t4[*M","*Ot` [","dXmZtgE","""A{=" -"wIf[XC""8/","sG(U.l)cC","[tg","y","Lb8","yo}wO" -"X3>=RC""@5","(e?WL","_7^]2","#)dfqT""fu","{k[U`$3Jiw","s""gOd66qr" -";w9l9","""DA","gl3","jQ0{'\>","","OF" -"JcnXG\","_u'XbxK4(O","LzQ*","tGoHd%","G","@WO;S#TG=","ss6'6|.p","" -"=$","&I!","^D}PTpl","M;_H","x-PN","OtkNQ" -"\plR Lj8","dGW_","","","lv4a","XW2k" -"_^@=;L]-","k&","%_'V",",Nf.QD","jbaG$"," $)YbQf" -"m^h{B","8#Cy","U","mFb$$fc","1","dz(#3" -"(`5","JZ","e#""","]WN","","L_" -"=KJ1+","VH']{O u","Ttk*,toi","T{c","uO!@`","wJyKwtj" -"""we]c","KpQa","/kc2`G%`lE","Fi","p>1#!/T0*`","""G[" -"[FwV8vyqg","","w;b7!k","4FHmLwB""b","X=","we/",">U","-Bk/t","DuhJ8@ut?" -"ME","|k]d.6 ","bt},%-a_b","YtF6jDg_",",Ztt","[+Pwoc+6U" -" lcL","","0pJp7Q","N>0N)&Xe)g","5.zbIM>[" -"t""&42o","","U","q]RJ(}Z(Qt","'Ahx'","Q" -")","%F(U ","yT^vpM","|@=ah8T5?" -"1`Hy6nb,O","8","e[^}a^","c;mKT<","M-S+]","v/J\1_!" -"rL>ABq|_","+$y}j",":;5-=7","","rrJ'""'xR&F","@<8a" -"5oI.I?*^","Kx(X2UZL^k","nka/xTW~"," Zl8E^","","Q" -"*m4B","%K>3","|a~$0","h,&p","bC-kO6D","Lh=BNr" -"ef] I2K {","Q,V7","[&D||","XbBsPO","9B~A\y;kU","" -"u`R*aS","|eQ3aP0!&","RE(J}","","9ki|rM>d`^","hZA" -"*qXOdYcT","kNIZ]y(>","vkpcWicf'","2j9","d?A","2a81Q-k?~" -"q?","LGP","1P',","Uq_wAke\^",">gpP`o4Y2b","DHvoM6c" -"g.1A","={h]sHeb","g@wRl$","","!!B>T,Q","W2-LEI{" -"","""4VC#@G|r;","A","*rx","wz,ri","f7f" -"n6i","hT.P(gF","g)Z;.\x","?1","E8cg ","}/U" -"y@4O=VkPj","uuw{VS","AqGw(8nA<@","'T+dEO>Yt","foTr%[AicI","+vYcWQv5" -"ce$","","Ve.QW","9E@^","R$P","M{G{QJgv" -"u?$.lVL$M","~pLkO","od","","QXQw'W/Ny~","kJz^@" -"sr'Ny#_M","i/""JVmx\+","&v","2","rIc""p1E4","t1" -"~C-qD+)$=6","#r","*k^u[fuh,9","w2XE","$;""[","n-" -"s","|","","3n","{J7aU~3","Z" -"wOO","","m* Rva+","P","9Ui%S","^I/>c" -"LM","D","TBJWtB10","g*(2","Xs$*ju""H","}Z}Lh]" -"Gt,BulVC","SlY","","c~hjy4$""","DkGCX'/3.{","'' P/s" -"*H;","H%","^>q","hu[oWA$$","pO]#P<","RYe" -"yw A","Q?O8GD9,Z|","U","q;[2","I$mVse","xcT~" -"HMB","qu\L","","%B)g0","Yy5FQOQ>2","B9MY" -"whsJ\","%qi","UV?|f}*","F`hs~zqA","3dCR+Xm~|%","c.QU?J0" -"gvF%|.","3io","v(","Du","2o&KLe","C.V(#POu" -":gb!61a","","=o{BRQ""g","H","*W#d#%<]","oY9" -"p","D#tj|+~/Am","Bu)6suu""K#","","#JRg/sP:-","p+t" -"3m s/1oj+","#V ,aH,~L","j4tk[","","jPXV]8OB?","Z?i:" -"40[","KTvzMw-*g","^zDl9s;","a","","Cq" -"Fnh>|-u'","'","""z$]VqI$a{","_S;nX","ELltURP","<`}","""3d|Qev8C8","cC7k" -"K1F(jZ&)","","[UB7","8|","","IC|T" -";t}Kg5k",".^@F~.c","]Kr}-Zx","wco","c*","F" -"yyr]o6YcAC","dq","f","Eqdywx;","^","&F3q>Z?1/" -"YkaUl*",";","zlH%q","Q}-H7D|q""","t9=E","ud" -"1Po\)x6O","NilQ","O3*Txm/R","F/xp","35","=AgvpHH1","mK$d","\w/","q1" -" -:ofXf5 Z","j/X)","4>]x","JM#uP'UL","""<@cK28\r","*Qb P&QR" -"d","6-Y]Xi0","R9k'b""_","qq","L'R.NCB@a","b0n" -"i)\R9","K4^1","fW~","<{I8]xx>","~TG0p5Dn","y" -"B","!u","XxIC5","M`B]MomG4O","G\jxVH}|","?B,@fn." -"u*Z#&*Qc","<_i++PJPP%","","J-""yeaAz","RV",":zzoTjuSu" -"!)>JqUjQg","1<","j","/Ib5@whC%Z","/","f&a>Zq$xn" -"-oR+V$t2","ZwWXwAPy","\9NU","^E$s7","]obEe)T@+s","T(Hwo(" -"","5R+@",""" ;OsonX6","d9r?/?$E","$3/vxLt*v","@)." -":FV0","E]91l","<3k2Cm","[8n\k","I7TxGjT;[","iNr`KAN" -"$}2Amc#m(W","8[*;","F}:","z","~#k1K4^","6 ","SQLO",");" -"-Qj","a3p","PL~s0","AuEr6","`UcjuA","D","@@/;dD","lqoRUu","ub(]Fqbb""B",";9o-#+)_F" -"w_s_u9E ","88","f,9hAS","""$d6Fv","J""T","ma>Zb/M/6H" -"","f15_?L","*""1'}xa","Rh","L","" -"%2 *","j","~6","n5s|","+>W","","&P","nB1++lumCP" -"^AC=U","aO6~","XlSj_emnf","98[D~","","WQ*^h" -"Ct","djGwZ>","43","mh","O@]v>5""=0","o&l>e;zqef" -"9I+&HzW","UWF",")%eQp*iB","TagAMX{R","EZb","aVEDi(tR{b" -"@|24g{|__b","YqC+","JGy\)","Fcml","","R:q?9,-}" -"""$V'","/B:&9V","1aol7IV","","Z>+o\+","rJph~L e" -"=k0:8Au",":[:!GOXbu_","/u7+Q",">A9Y","i/.W","ex)MfbcL" -"m~r+","2>v","['(MV0","nT","ov8X_^","$vK" -"}nDF?%U$Mu","/Rh","""w","$$FXZ,","A;eI9hF`","mMT]lR" -"","9~v/q",">j","g.g<","P'B [K]V","'@w;Qap" -"u','f:?i","d1-#","cw""""ll~?","ZEdV","""3&","QwFZDt" -"oUGrr5AF","f","p<6]=<)/Y","f2iP","II/>6","mD\'x}E" -"Y1@XExlgX","ux","U79c{,;S\d",""" +!QH(","x+@[S""w<9","6Qu],=" -";C#","\CN[*","h7KuW]CZ","W","+l1Q","|,5;P(w" -"e","D*>{ Gz","q","K0'J","9Mh/N\X5&","$h62qDhC}" -"a\B-MUT7""","&","M]EO!a","QaI@'fs","Ex(e;Hdh","~j8\gD;r" -"Lxh&eC","","/h-!mx","","^3Pau" -"}]7?B","W_","J8>`*r_[.","@","(-N]F`.?:v","" -"Q%","wZ+Untty","|\~\","{z$8I","E'BNN3+4_","w""nV+j%" -"%bTi","aYeKT`","V","","/qriV[","pm72)n#v$N" -"hs(","0",">mUuk","u","C<","$J)!X'PMV" -"1HNGSv07S","})Y\3j\0","QXEKX""%","}6","]1i_ Jmk","fOAUJpG" -"Ngblkh'D","urQ""Pg","","md:X\-A$\","o}A4",":WIG&4" -"FuI6Vf!lgU","EKMu","CHfv",":{N","etRcEw","j" -"[;","Kwy7\;","KSC","0P{/\au","L5","VCwJ#;{S&","{\B7T:S4Q"," ,&.$G[x","$aU5:evb","d""39_Zo&p" -":_,||]b*","","f}]L","%I~","9-^Ggt","TBT}k3]]T" -"^U)""}","n.UUG5X","K9Kmda4)","{U;","jE4`euHi0:","?,myi04?" -"G~@l",")G#JDr5~","I}","#","aE2#up""~","gXM+wstSe" -"H% ","v'Y}","]6","4AgZi`64","0%e^B93>kQ","CO6" -"8K}","2vaPg-P(=",")7.L.>Q","7\9^","fAbU"," 7H\t=W" -"w(6s","7(&Mzxc7k","-kK3`P9-@","9)o","u-lRe`A%","%SfPv" -"NFS","R`6)`","","U1eR","qK#HJ","1G'Q.3]" -"n$","v:DI]","R(ZO!Sd","~)","","m" -"mMJ'|tLiv","S~WBIjy","-!DEg[\R","","0zB>","(X94" -">D^v2","XHhf#","M2""yDE","IakH`!$pw","x~3PK!q","CI4l*," -"e08rc+l(8","wD!9]h!8B","hc= ucF!","~9","}4+6","9a" -"","fr","4?b","R>s>8f\Ov","J","](`zt/,Qx[" -"r_]7{HA\","] #M>","#Z7~;oJ7 ","dQ'r>,)?A3","-pPD}MN_qD","" -"o)&jo","\","O0","[","2&","z" -"VRU_Mlm","*7WP<9","","O,/","s}z[GPm!","=J rfgZ","?G|aX18':","~i'C@" -"O_B~?F5","xTM7","9|f","Kl#""*wns","l]ixW&",">l" -"8uxz,#O","}","w$%O","ds""","Z8|k,","" -"x)","f_9l>2:i","Bx5%","Q[0rm7","r","MMlG??$92" -"X10q","H4t>D","jf/=","l","(L+FY>;","V<\kp9_W","z+","uW$Se7'A" -"DD4+","","e6|M|j","I","Y\j3*","n/V" -"m","P","5}[t","o$3XaB","(1p","Q" -"(l4","+5}If76","]:as","<{jR}[=","","Q;b&" -"&@0RkC2F","KpP#;7}0$","Qi%HZ%VG","d","U","/VC)&1z" -"c1<$OIm%*_","iN'UN!","{)oQmic!","p+^V.$","/rYs{<","Zjf!J]s" -">EI$^+","c@","sj[^","q\Z^v5:","x6","]3Fv+B%G\","!iexxRDer)" -"iz;C","*%]W;-","m9gf.","~EqHR7JI","63L+/7I`h","yq7"".n`>U" -"r4x@vF>x","","OXV3","sF`|wfBnK","","&T" -"e(X,(iYjD7","dGEtB","","s[=!^Ke0fs","ssK[)J?YnS","P!UL" -"","|62l|%?","6C","","","E5""5^xE8.y" -"r*l]6","=","'~omw^2H","y|W<","FJ~+9|",";>+`n^O" -"I","""SAq","( (w oOjfs","lua96Jg~u","L%K","^c" -"Q#f0GDJh_Q","fI,`>1:hbc","yDozSJ/c_","q:Gae{v;:","W)[7""?""Aj-","bJ@+N-AEG." -" &0Bz[a{ <","Tlm09","{ow,)>KiFu",".|z/","orZwLWz","sb]@" -"%#4dn""W6v","",")\ ie6Gq){","&","=oAo","0@Wu1lW=4U" -",}D:9","i>ty;^E","","&L%Otmb","~)Yb","~" -"mKi2","102d}Xj.i","/Z.hw!W","TU{)`","2'ptLd""","*:U ""Z@)`" -"","O?X/H>","*","","05JMV","]es)" -".#d]cs","G]i:-0s","","|","d`h3","?R;" -"{","",";,8Z","UMr3","GU>$C<#=","($" -"6?4^","W=","","YxMcaFi","","QU}RNU:" -"!Xit-N-E","3]I90","(pl&s","Rh","","$1 /|" -"WF0","YybY","o! p;Va0","zUIA","YdGvc|","\pzZF" -""," #","Ds7A","zfW","D","S]]>yZ" -"^zaep.&","[fVOXP","2t8","94#PGqW|r","%d-","'+'L|9T" -"G'+5dSY7","~Bn[ka=V_","[{"":{5","Sd:O=","","PVa" -"ok3J&","<2)R B","3nHmW2Wh","!4v","Xku","&8;%0" -"cLH:","","* m&T/jG`(","CH*","z.Cn3aR","gZ" -"FoUQev","'~!jP>/:=","KeJsGi4`","!@%mY+gI9R","M<^","b4Itg.DHd""" -"v&v``-AQ",">Tv","DfgIrN","+m,>x","nJ%/Q","2+r{-mz`k" -"Gq_,mf","}W|","[WyseU","il","9KG$Nh","n^p" -"FqNa-`P~]","4.z""2M","&UPS","1%`s*5I!","I","D" -"ZO+S|","lP?Oo","G1l","hHJW^)&5F5","!}%-D","zCXD|" -";@^^+;","","Rt!>+'`+v","pXaEZ+8z","Y",";`P0q^J1%" -"o8RLS3iDP","j=w","_,@y<-","Ce{7/88O<","V.#3V","N5\Pt" -")Q/3d(/IV ","j`G:J/","L!,.UynAL","%s ","?Tva%Z","a p" -"y%mogcs!t","&e|qk","WGE7q","","","{\s{k`" -"",")(","?8#e=Y","Usi-","d","o6tiyX" -"tOVe/f'Hh`","W""3?^5IZ","nGKdv4O]","2RW","h@Mk=qU+","R x3" -"pAaqAjus","i|:","x5Kj","}b9","PI","})V.4$" -"h)nUHN","N2c KreCQ","b{","p]T|^soV",">f(^~b*","aX{^8iYc" -"6He>J\zfYN",",-e","]9","1M;;J","7# }ll5a","#","czPm+","Ojt7","dg6d_Gz`K","AQB?y","P%/7M{(_" -"|{nW-c8T-","Lqb9B","^,<","V)&{_3Kp","WCv>rt","DUi!/Psch" -"'","","q%qyhzh'})","4o&","{z0CR","n=#Hjm\H_" -"","'fe","s-B.%$5?l!","&{h;QDK0",">Rj","p.1sET!" -"","n!u8RJ","WG&67~j","x","F","TNaMzW.V" -">","","NobK'eg,y","K|^)gpVP$","prNi3","E$J4o?m2" -"2xfMAc,<)","","6+%u","hoNrez","beSaoe","QJv(r#:O Z" -"3k@}+'i","]pm'|",".cBD","S",".9Ja.)j\un","pl9" -"'w=lq>5Y","O""","o:lgu=","!xO(",")(3*T","" -")","HAKr:]5L","","`wm","q","Bm0" -"fCI\V!Tg(t","yJGBu\mi]","}J 2*cEYd","=>TR","b_Es""","q" -"%cb","","iUqtTIy/","}","[2TGE","(Q" -"c","-H^f%i ","'3\}H#^0","eVHOQ/R^","`0","h" -"#_:i*","YW;","","y@+0klU","D1A@6","W&F^`(O)B" -"p","!t",".G","","-G9&/XyU&","5R0+-T" -"!h","Bl","E","G]wxk","AqC~K","y7eiO" -"XW7@","=+AF?O0/f","","XST","u^4_3","v!N7.=xS" -"&{a$xJYPN","S@Y[O{Rls","+v?09|`,pE","UcPW/=NkJ","GR#HB","o" -"","E3r","$K","P]",";glO","80R>" -"Z-R>","E0>Rdk","","""C8Ipn","Y/jY%(EoO","kTEr4cQir","t","egA3:~4f%","" -"^Uq","","",",gOJ%","ceKP|B" -"K4*#","","EI","yW|q;jg","(}uj","AL7iR8%~" -"6vqU`",")"," 6","2ke/`lW]|e","","Ri:>_NxZ" -"",",5ft","6S","8+$f","cm-FUpy","TN" -"RNU8^2","&ce{.K(1*!","KAe-XRL#Ru","y)$#]JjL","S``.`]","P-)XW","3$5(3<","AwbsP0;&Kx" -"O,mcSHZ@","6@*#","0)","""q:[\`<","QVn:","" -"I","evL","s2o","""ZmT:3O-~+","!|TZ","zP*J3D0hO" -"W9{)%L","y61a","","""n4^L`","3|","kBoX" -";g:{'8q",".","ksK~mJ&","J","tuG","32e][qO%1" -"^M2<","z|s;Da5B5","!Ya?l5/8+_","D4vjM:(""^a","d4t","7" -")_9o","NYe","lZp%Q8tYD",")CxxziJss","p~","oc","L`%H::9J" -"W","(@uS","<~RK=","4yg","qN{VNx=","8,g|hH&" -">DvSB1#hz","y","P","}","#m","4LBUACX" -"#ab","vH4+Kp","(efQH","1X[y*>N|#"," B_n+/*","whb" -"{S[e'`Q.E","l-BUV","~P6&\","kwx@f1'f-X","|li_"," F&^~PH""TY" -"3\SO$TY","a{","\aE6{",";KH","Yj:""G","1H>v/" -"o'T}=N3","","/8","[k.RX","g~","" -"","MF",">p]+B","(","5","zxov" -"VO","fu=&kgYh","CJJQP/3(8",")J ])`9%","f","","s,41i}td" -"","je>}6N9","s:lhwbm","I_w(","M;oC","vx-" -"J]","=N4q","@u0""b","","@^GzUnx","g0YH" -" ~k;hC","R[1}^GMs|","XvW3}F>KR","5>WY(9aBj","DRusg","RYR" -"{k#96oEr","s8Hu=O","","a7","o","""e<" -"","v","gw|#,&N;","nD.v:#A","rM/?3K.O=~","C6rgq" -"A"," Hsm","W!ozP76F&O","T","ZkNlCMF`","gsxD>dk" -"U 4-p","Qtt EWyf","k7'+^c$65","Nj{}j","Xe`I2","QqR}[t" -"","]BIeww)+F4","%$","#o{*","HEY&|w","/{;rp:)fD","","B","o" -"s&Yb|A==","","-&b","{f","oR1","~fymA6\C|<" -"svrbqg&B1","1c","lo","U8/1V!`v" -"J8v","4rWV9","76U9q{KV8P","vv) ae1:.",":QqOMsZ","^#4.;sg" -"","6","AFfoQNEO+","0{?WJ%","`0R/>]","" -"-","h",">","bYKG+T",";vmn0u1us","ls$x-`)" -"aBW1t","m`%l21ta","`.He.""Wk#'","","`p","x3%a4+?|" -"Mr?i8:U__t","aGA-!B&w","L[|Ui ","kKiJ`W!"," Furo","`g(WgAdq" -"01`dNfI","3`}C","}-):!3x",".#MchD","d`","}" -"Jh|,&4Ff","T~.""RDlk","R","*","","9ON^xZEi:A" -"RQno3","[N"," `}5Ek7","(>","Y552I",";AB" -"0=`","&H'4#2}A$","`| L","gF","Y{'E@:H3~M","OCndT't-" -"oe6f&",")7<-lzrZl","8{tT+v","ijM","FZ","n/" -"hyVrW"," ;(tj-q2","6#k!|B)X*","?H==`tp#7","WR0_y>={[","M" -"~N",";>i","l:#","w",")41#l","" -"UB-J^78Ra","""","","]CpRDGNi","t@$]vl%P{","zB6 #e"">U&" -"K:;@?","*","y]r.R<","\< -dU","],a2","F?pwg" -"&p","FSO;|WZ:","@","F1z","Tp{w","5~d~[c4i>g","1/z%p\qK}S","`.G6SG" -",L=RFMN0l","","k/g}x{%Q","u{","y""Jnd$","" -"]^iUl1(","GX~?:q|","","DKto","qyJo","E0BDjFqD-" -"qx","%F_`GWJ2d","FMf4|Bm5","","",": " -"VK>\f=~!","ps4","$UDLr%","","TRgk","i)n\s" -" \b;","7<=J/O~|`q","{","Nz^Ol.",",?","lYzM!A!1" -"A^Bg[7g)","s","aHJ","8JavxhkXHV","$1","%3G" -"4'5]","","(Q5{lpiB*M","C[~b|#t:d","#oa","G,`g?)uR8" -"","%o#&","5YL>'hw2","$-8:K'","6't,&sN[qh",";&lg?" -"%P'nh;&","@hB]=Xe","",",BKq*7","3kpkB","9^wKD[" -"hFj ']>{","""QW","HQ","Z>OM?41\S(","~qaLFh","rim+jN+" -"7{y[D","f]IYmPn#","V$g","1-6S9Pi^t","+","] 5Jrngv:P" -"'c45O{%","@","%#`@4;"," V5[1","0-d2","","","* " -"{","Y W#BO K:F","u","r","Cc oQ!","" -"*&\pHM","","z3qG3:e\X","4","l- ","$=e%|pmXm0" -"Z9*""]g",":","uLto","T0;N q,]U","puT)N4","L^7)" -"Fn}3f.LuPR","OoS6tU}6","AA& XSb","pVbgn;{ub","D","LPYGNNezf" -"uX|X[.","Dl,SdFOy","$9<})M","q","","[Ssf.Rbg" -"L>I","$X.","#$rA$7 #","utSV'RS","#G'daiU~4","xkg " -"dc<%(;I","ObU","""u\cB:\","","pzB+BKW","!@0x" -"","6)uY{","#'7GYK\","^RiXO/|?i+","CS,_#","wY,r(if1,(" -"hm-{","dp","'""E[=LYTk",",X%>K*",",","#TnK7^Bz" -"lFtbeR","%`)XZA]bQ2","HY",";^hM1S;oXC","","""e" -"DQ4CP","7""r(o.h","rf8r1","9oOobh","S/*.jpWQ>4","0` hF|""W:b" -"Y%q6",":x"," ]j5","\pQMmt;u","rTMG[pK","@!|o" -"t>I""7vmd0l","H{3Y8;X+m","","3u,,R#w"," Jme","" -"o","6LZff^3","psjx","2M>?rr",";%2^'BH #"," &e]" -"L=q<","GF;];'U@","","H","@(^!lnU7"," o=z)OOu" -":zHk9:.","cEHow,","KU@T#tZ`}","yz[)3","0","Th>'Nz{>6" -"9","+RS","(/t0601|Yr","","c/aI","C4)'9[s1ym" -"YN3]:B|P","7lVL&%","Cv|+-0sCm",",m>uClMAI","s F=","aQoeb9Sn>^" -"oI\G","]{t",",i\%ol.","3&","JtHBG1:wXO","qd" -"\2-by","","RJQroIL]$","dv.""- <^=","wNd=k:W","1" -"Ii","gkd","VF7uL","b*YK@","f","9;f6|w[0\" -"rqb-","D%","M9^fGFFx","pCsG6-vU%W","U+?gL","S,#4Y\Ba" -" ,&48]A<%*","ZSa -bdk","4AAk4kI","Q&E\","$x\].$",":*=BB%a)" -"Mrg59g|xP","/o8$,.;h_","{|ce|6-7I0","@<@x+byN","KCs-`*J","u_BwRG" -"w/","Qg","hf`",",dvS)","Z[PzR>@>*B","8e1JYN" -"!-]","(5<1uTFg","13*Ly*","V`toYS","7H)@l&!",""")","_B6p","*ndbzfk`,p" -"B#2H+ql","P~TQ^" -"yX ","w","= z","7L0k","","IpP" -"","H|","0",";%/|'g.,e","","o'o]@" -"Vws","tW5eMg P","bN)~cT^-oN","X","""5u,","9HO^<" -"Z.e_QycO","","j8","XV}hHbN&p|","=7}Z","w""!.H" -"2F'i7(]_/","X","zEme:* ?#:","kS&T`","e~9}YX#","4MtIdWXn" -"f:>1N2,4""","bQ!@","y%","qzw* E@=X","L","uK,9" -"G*f\?X\","J\+S=I00u","j","Jl#C","","" -"byTt)","pb!5KU","JWP&XoDbu","f1?Lw","u&Or","a9.MM+nkB" -"v{n","","7VivPC)r","En,","x","ts3nLc'd" -"ML6)","%H>.Yvk7s#","8","na","Kb@%","90B" -",MD","CWKn-SSJ|","F'V\","4=1","g/Lv(e=:b","S4C/,$zn$a" -"[[/*m(r","Qff`","","EQlprgP~;!","","cK(<>5~^bg" -"bdll=T","E%+Ro/\#""p","zU#q?Uh-jN","7r","@S@k/t","`#JX (:GR" -"/","MD;u%4","51i2K~jBD","UVCu`P","J|UDt(5" -"YnX*V_h|~","ql637%&M","=b,S1h","XVhx","LWN/rY","" -"eK+i0z?&3","j+}F","&EWGD_","D~e","FDj(.S\h]b","p3/" -"'K/8/","-pcH""z5#:M","M","c","zQ79","i59V" -"","|}","Rl""e","3+","rqA~S","GGDP {7" -"k5mL\=","!0Y>oy","+L+g#","B","g","" -"k","vJj","v8F|]8WwQ","%W","0W3","a+.3PAanf" -"muC~Tz}5l","","*FQ |/DZD","",":Z2t&N","F|S" -"CMK","aMLU*?^6:","l","_-z[kX","#eJ2""/oEb","i&J" -"PXa<%4s.x",",Lj,ms",";_QAG","\M","+xZ0D1W","" -"Iu","@_$0I","!~I","0475?Pr","","xfX(5fP" -"M","KW","9v","e","G^I0","%Bjba/PTl" -"","Zdj{GY^|3J","EQW2nF`","ioXX7F4(","Fj|)JyCN","5d" -"X<","SX;.v9","8","\=v}Z{i","s","y}m]j-D;L" -"7","C=.9c-V","/_b","rsI%","\9$","OUd'uz|-" -"[vMQV4kkg2","G14a,A@","X`k0;","",":u","x)=N" -"Y","OnMFVMzn\","wr","RT|tXR2","wQ37}","@" -"","n7","^{hYk 2","eOtY\c.1~","`Q>G'G_{_",";XG84" -"%D","dk_#r","dGvR,jI.","Op9+4","=r&2",";c}M(R<" -"M]Y","]","","c","-r"," " -",^]k"," M}","'J9O","p&977\26","h.Sa~7~","" -"VL'yY","Wqb","`","j6\w","YP","Xv`?3" -"v3+V0Ai#a","a-#","X","kHU\&*3F","q/*X8I$_""Q","" -"EH=1D&v3E","/MF","mc","kj\Se(k9k","_cS\*y@e","e !G~Z!Iry" -"fcFbijd9n","e{He`Xh","}aA","g=R$","Kl?-LGQjT","x6T/_`m","g4}OBOqkyg","f}ul$/G" -"","U","4:3","^6zo6?AS","e#A0:]rDP","$` w$S*zE8","pWxS9E(<$","iPO?5}u[" -"T","}yb","","p","?W(V!lmf6B","CF5I" -"h(!])-","k","o2*XV8","3=T`'3M3KL","|U%j&E:\&","" -"KcP>zQ2","m#74#6","m<4]RL","R&@","GukI4z\",")RQ" -";","{6iH",".gs6m:-N","+#i^hU","&t^7j#qS","|uY2QEew","m`$Nbzj`","p" -"e6>.""l","@n0k","^\z)z\g","B{+","","XvYO""" -"9","x?f(","Qq-sF","-0'U","1jVn* X","cFjR" -"rx.];o9ay","","<","""jUbs","C/1LG","MU" -"","|70zmRqt","Fu-C4=Q]","}}","%?E@L0 +,","vgD70ZoCMA" -"%pP~$}mb[B","aDNA_,(SYF","[smlZ;6s","-]hJ+","=z^m=0)gs]","m,(l&N""" -"9hiz","_HXn;}","2R7rS{/am8","A:f5Ah%","u833^tC<","y~ShTK(" -"kx.;?sUL$#","pc","F[r","tj7wK","","" -"_r%n",",[j#|","?8","ek""V{gIoq","P f","|l]:\-l%7" -"","Y""l8rV","wrn]30xDm","VuU","z",".{@CB-h;}" -"\W{j","zTj^qif""k(","}hB","k{.iLU[5Gc9","" -"'=E=+""","~2{j^3}}S","Hsmu`","s#t1","&R","" -"","#T4","XSM_1","","E4","!" -"<`,&","ULnQ/NZW(x","VsSgfCRD","U6L5","a\","&wEO4{_z" -" Jy<(1Pi","V5","';<"," @k+v9N+iw","Yo)e?oE","w%J~Zh" -"^cXyo",";n","","jDl>$7G0'","","u" -"l`D6","5x|+Ab{F7","d","U6K&","NNvkG|","IQe" -"gK )|",")BI4Bt5S/t","","*NGlRJhYW","'}f&","Xi~$Gqls^$" -"a]Y^71","r07hx","@","t5p30","]84p","b" -"","OQ$","HD","TR#qR?u","","`6&_Pz" -"}","b'Cz9w)c","0V","dg'3[Z]rM","G",";2;a" -")Q;","G&'rfX","7 3)E","/3]a|C%","c`}T""eB5e{","[6wA\ " -"!Ts-,k 1",".h*",";xa\AOB","","K/NX}a4S","^" -"0f\zlhVE0","d.#+h7K8r*" -"mdM:)#","V'\z\e3","ruH#0","R|","D{dmoH","?~5z" -",LBC|e","p","Spym-:{L","$eL","5 ","b7s?H0" -"2se""cYG_","={jA-""#)^","6","A$%d","qGEyiD","" -".N","""%-gc","!","Vdn{iRwo$x","[x;PX<)","3" -"E]","1IuA","s\z""#","""~AVs4p","T)A","! C/KT" -"*UX)","wR[%g\","W%JC""f={zW",":u,","4r","<(b`%","" -"~}3Cg","K;RD-" -"jB/?\?(","j%","]rH3*Xfc","[ktC@}","_oWt","" -"NjgH{""1","P f","Qs]/","|do=","qTaAF}0","3Hj(/'*" -"%1X}TH5(","{#KDZ","#?LpF","z\""\u!","ZpuzX*","PT}" -"zJ\","a","'","]","o","/ 0]v~" -"]W","fbB3k,1UE","~(""[w",",k6dS","561{z;E","!{*$vr]" -")C+1H""{","12R-PWsPW+","'","","U","^a&q>d}" -"`ee|l@",";P""&q@K_",")","d-","t`BG}W","qN" -"U","8N?-r`alX","4{b3j^BU","!0AKx'BWH`","feHF-%L","y$|pIK" -"y]@sV","{%AC%Pw9zl","fsm","HbN"," :U}","'P#L0+/bMM" -"]%","","T 6Ut","C!nm/? q(","s!E,>","" -"SLHvW",";8,j","7Yr6U","`EPEw","","z&9" -"%T&mrIT","pt",";A?1cr","5J}(,","&&","J+KG","uf/>BAD\" -"\wc~w[1DI<","R&fF","-.&jh","L","""","J;W" -"" -"Y=UwC%g0","!","ek$6","gOC!qh5:by","m|h","" -"&`0","Ga4AdHN","s-UQ}=c~W(","Qu=#}^","","g" -"9","","","3","*7n#sS","|'" -")c","/DHD$*","G","G-JXVX","yk7+pNhXKi","_zlM) G" -"rL5TJ","|<8$@""[h3#","F8MN4""","j,a0R&yX","[","`:)[qr9" -"X/#k","$=","|% mk#'","V[aF}","=-^" -"","_Pdpz","X[kn8p:","","(jI$B9o","=N]e" -"z$-^Bw","S#7JJ/K9","Uu:y@0aCZH","p;A)M?|~S\","o","?}`jx" -"}q","qF34HrEKOp",";{Y[;","a2M4-E","TKz$qX<@","D/." -"","l","","Kvg"," :","" -"vT","~{<}\zY","%kFh","","]w)","ehPVgF" -"q7U","Nbmx","id""&I8r..","Prv>&R8ch[","s9}!+h0","xX_vpyM" -"xD{G.9X","e9uP;69*","oniE\rs","BuVHm9!Z,f","w[d17[c","\uOusY" -"'SL)kgw@""R","q>","UG`ORA(","","","om Ez" -"%]1y","0/p!.l%","","(mQvxH""0uY","w~thjCIB",")_N>hEh7" -"|E2Xy","u","qYq","I;","<","5sw@nvo%\y" -"nY","RXUPmsxPs","","[]vkY 9p","A/A5\smVx4","" -"TV-jOx","_}","%nKV:c","v+9881","hUo!\x[o","l"";43!AD`" -"h-{MM:kkO","K?*n","92E9t?","a._^!","M q","" -"YoXUv","k+U0 #\/v","{:'8","l","K+A&:Qf","Uou}2BOAZ6" -"d)='u8",")","|",",?E[R-*}u","U","G4,IQ" -"_","m0(F""tT%","f`+","^q]`4n","0y`"," R" -"t==URE","{02:OE@Y}$","lxC`1","5maX","","Ptkh""eFfmn" -"","LN","","H-H(PPSv","l","7J^|vbV-""" -"Dn'^^f3hv","f7o'3","(RzY+#4","%!b""Lh\F-i","'u#'c*","x^Y%tj`" -"e,DQ;K",",!","Q m(","Q""h","6!5","x=0j1Z{" -"*X{&Nlsu","W.^M","+","-","","I}9%q,9net" -"","r)M8K","6[sYyY)vo","d/J(6","t8,@","/`kP(^!Xv" -"ohFtgV","n7v","","","Qg$}9" -"""^#;6","Uy`","X$","e","| bDM_G'}","@c|(" -"B,kz","pI]f/]CXL","uOCUDw","[:5F","v{?","W',]DwWL" -"c`%Uy~ ","U-","dYF%A/X","","g}qn","sLI@$(xU_" -"%Md*L",")W","'Q\)1>o `","BR^m0ddNTE","fk1;G>","`" -"","t?~HTB","&R%QQ1M j","%4H:z","4:UNI-&M8","" -"97","","#6/vD{;-_a$9Ed","I*b2" -"RFJ5","+-k:cb)4&T","5B<","njZ","xzpy""*%/","HA" -"","oMMtv","","VW{","9Kp8","" -"""w,;T\","Y","O%*c%J","","0""-;:{","S" -"k18","","J('r","","k}?Nr","-2M]>" -"W0?|N)|","x/\jF68&Hl","0t","B6K`ebq","=w","" -"s?{>(e","SK+Q'rgO?","`?f@TJ7',9","AD","Xy4-`[J","*OF18" -"fj+1'5","]T<>?YQ","q4V%","A_;,inKs","R;4-q^8ztX","Zv]" -"G@U","D","(dEH\g","m&","eGGS","" -"","#","",",T","]:!8N;","Q;&" -"l","","A.KrWVZi","3m %8S","vlbS","c3E;]" -"D`VPB","b={@p;","","qbyJ;{dK",".`/*C;|","<" -" P@yGH2","","hjmo~kHmo'","D$tqEM3e","","" -"237P ","",")~&1","!(","Tr","y@" -"/""^ME","","Jm9","An","m5","q|=E'" -"","aeOtD","^M*Ty#li","}.{wzRqI(c","p","iK7:" -"m","U2%Bz","1=mETG`x","<31oys^b","O`JXr",")" -"pM9c!7)","g","EES","^)'$=kz%Jj","=Mqr^","+Jf./[[" -"wA7","","~:","x7hpJXRh","MeFJ3-?/" -"T%_AR0","Z2{$?E>","Mf[Ag0ALr","Em[","D@4PY*RjaK","",":.6ZF1K" -"yo|Xqt'","]1P","6}Oi~]O","Y/=2}3$]8","","PG" -"S2#8[^T ","v","Vh","Y%Z?t","Y[gF\xsN","^=4;ke" -"]A/","","l-!_","Ko&","4OczA~cq","}2Xit" -"G&=mBM}","+Dnd10F[","[;8ln!OtqA","CcbEEU?","lsD6","I/tA" -"?","#!6<","","V\w","V","BSs}DSXA" -"_*~[","7F","$R",";$at;w","eqsR;.","" -"Cu\u","-i/g","p","DBLn",",-yhG1Z","$~@o/gR`" -"Z6U4VThU","","Y+c3","%>y","%?|n@&V""","" -"Y<8FAq(+","P","~$",":~K:f5","mu'G:<|p!i","yb&6(j" -"'gxV","nx%J`ZGh","TPK","Ppz;52","W)F3ojnUFz","(8^V" -"M","`SZ","9)3q_D.x$c","3nERar*s+q","","JxSe_" -"T","/jYQ@ZYGT","","Bjo;e5","@HPd#a~D^","G7Q.I" -">qD""2WdM","GA@3","yf+l","\+","Sf|<(2Us","ON$mn&"," 8wT^|,|6","`","u","","8~g" -"NS\","v#Ug+a","%q@UE}V,","mPgZ_V)6YG","TL2q","B*`y" -"e?WdS}d","yPItc(j",")4_X@u&","","f.D5_Zo t-","O,-E" -"{$2/n+","C|",",h",")}q","3=Fr","[.W" -"R9-Y9Pj4","w[ af2","0-H""Qi","9aM4#","jxieH",";/Q" -"riO","=$Ll0jJX:","M.)}|","=","@HLX","zh&J98" -"ppvrE","!RKB.2!z6<","g","on^(1:a","wl! ;c\","Ap^!cCR" -"uX!v:7",";_RIJh6$m","","JRfJ+M\,M'","WTVK","D|" -"sDJ#),","46","t5","dT|X})","i<0zl)","G" -"Isl~1]]FuK","H+ =c(","","",",b,Ay","]hv3eQ;OmU" -"S0C'$1:Y","^c2;","3h$nR|FW@E","T;wtypv",",DhU>","n~l" -"z7","kd\@","","H:0h-t=/","V{/wxTq","%~]13k","X>Zn" -"q. ","N|c>N?hm",">P_(pA.k,@","V","/5","t)mu" -"';~@D1","Vf","Vdg@]L5","","VcTj7K0=gK","zNj{]!d" -"U{Z&ze","S7Q|u5=D@","|9PkGMuNJ","","$","f1" -"lhU!","DX","3A@G~JmFyb","jg","Uv$m","i*8mSCY","Q3jX","<1Q.x!29%e" -"T}44:L)","bR",";(za|X>p","]lz><;o*Oo","2X(OM""","'?W#" -"!Ww",">DQqjyk","!QZ:}E","|)","-Cy8[[-a","AQ(7Jf""(" -"MOb","5l#V","p]3","4Ptqi","T","L" -"/dF ","[*;\47","","<_","tRo" -"kCbR#",":mRk9Y","\^",">?5!:}^Ul","/mEW;/D","oZI.OP{" -"R","}0nO+R",",?X*35","Dw24~&:QXL","5p'wNN","%/>D?\<6!8" -"^qx5?","Fj9#;Z","P","kgqTg@","Y/001LYg~T","237?D_kX*," -")4vqo@z!{3","Rg$","]==NB-'""h","z","~ |xXuwj{","z`1:#]pe" -"","","60$WnV","8T","+","" -"du","A#B8s",";K^;L","n3","LdQCmB","WY(,>EEd","Ob65:3,,O." -"","5" -"2ikxn>$KP",">vS\_dC","9","","_","kc+B':pbMd" -"Y!Hs{8]_-","wE4k","HHvHq","k",",Mdk","" -"0X!8dE","Xn+h*(>","uXtp","iv0TQ/d^","qDI,","`.Vle&Jde" -"5e;QCrWa|","{*","C3","",")","" -"S","3lecX2{!","","c","JU.j#?P\p","9x$ep |e+" -"l4fiS","","",",>^W","siE+#&","" -"Tenrh","?W","sE""J^yG","%{""\GE5","Lwr G/&\","7GZSL38bz" -"foF","~czq","?{,($,s5P","9+8","_\-b","*{bAr^eS" -"b^KtzXA]A","""0.l","5","r#i" -"bN""%*BZ@3","Wz","d3R" -"WE/,5WB4;x","Ld","+Hxy","<9""+","I/e","y=Ya0_$!^" -"Q","SZ,tWif*","","@JqM) ","#CwhIF","HudXtqg," -"","5p|jGX","/Gox+","\?K=PCXa2<","`qOp8\",",,0la/L" -"m?$(Uig","$gqu&n","zOagC{1X","|JA","","" -"c]z",">lL+ig","nF","","N^jYF",")?b}" -"x/Y","K[84zct`l","qC/p>JDl","w_fzR9{BX*","\#RG-j!","Kj" -"]bb:}","j","z}|: mh=i|",".s#N","3|B","cn(2`c(~" -"K?5R5Mw|<","_BO","IN$U x","#n]QP[{","*;M8FHmY7","?e4Nr/]" -"t*x","","Eu1x*$","k","g0},","""ywzOyLS]{","hDLUeAQ^","JdW=1BYC","Ds","tG" -"OWpbf","nM0$\X+(o","rGmj","uT[Y7dX","UP","*.:!P@Q" -"qh.","Jp","=,$TeF]@;D",";","nAxoY0","" -"yc~{ S++","#dhb7","J","284v","zI$=","TA`7" -"5m","-G","","q3w~$^C3","","RkY" -"[ '/hlo!","_","&Tr>O","{#X","O/6RYIP","r'p/b" -">P","?^MJR:XJ+","cWY?","OfOf!i]P:","ahVxyjB;","R" -"G7",",}%~u?Mev","s/wA","+w=#f","^L?VM]K/|","P" -"N","QyT{<'4t|p","","I|j<0'","&R#",":x?Es>H7Gg" -"@lNJ=6%","MBF,dX{K""[","Sw","b.Qn","mIPJ6@DN[k","=xBFyjQ.)" -">mlOo","99b1Sz7@C3","C8.~p","$RTV","/;+OT)zK6_","U+y&%H" -"McKt","C;X$s",",cN`","*5","oQR/EEm*Z@","CsLGK" -"zo(",")wa3O!O?+J","cpGoOi*9G^","9pF,#,Hz","gfh","^B" -"x","eJ","8>E.Rj","","k_","bg{","}YWF\\}","zx""f?&","qjsm5","","W)`[" -"","W","ZX[","A""Oo","Ch",">G" -"z$'{:S","Z$M-Ps$&F","` XG?,YBsv","I/Lx}TU(H?","MntpDE6gq","/_MiJ<`" -"+E,j.GD1","" -"a1!a","ae3n","JQ","M&","Da&&" -"j3pU73",")5x^GV}4H0","Ei:yo&","8``","5jScl^","1,q^9PP@g" -"#eeJtp[v_","6`icv.5pW","","!sMzd$%","G]Rod3<","$v\N%&xu@." -"o.?,K","H*T","5YkVF",">7^Y{","{)","Pro|iLF","4# ]c+fv","Q","s","}Zz5" -"Md{jbT_U5S","7L\P8W","DqXq'M`{ K","h6g","x;9N, ,L","G" -"","kXP:&rZ","Hwo","S'>5)q","F)+E[x","" -"g{`3W%bN;}","#uzo(ZAk","+\","9NL)","*q*Wz" -"e","1m","|Z 5\Uy",";#fY;Z","e=0%$","Xo><","","","+\#%icG" -"hHxS","lo","n,]","e_d:=v","","4~F-7.?<]3O","u+l*arKY3s","7W}i6","7vg\","8@Ww-","}6hLIoe?" -"","","%!=u%q;rq_","f_3","2v","" -"L","pi=%","5kA","c71","=""e$","d&w[gp&,&}" -"j","dR}F 6bi","3","","V8vAC\e_A","L""q(" -"/VZs""u'","N","rl'BIY1","J>zBk?\0+","","a$,6>2T5" -"8","XzW.b""M&","B{ |2w4?CO","IY%s'wH","&el-c","*dDoz]J" -"","/_iI[PQW4",">>{*ib^","n$","NT(osa","1","{]sJ_KV","6 j","" -"k;","th!TO","hNuK~ch\5l","YBWlQfK","e>iAO(HvCs",">" -"","VDps`ft2","s2Qn&dvMd","5w$TRBJ",":Jy-?g?I","O$BuZo^" -"}",",","#)l*P9h&","YJbl//d","q%{Pt9i:/","2law>(SU>" -"","~~Kt+nVe}V",")\",":","c$nRB|r","%}qf" -";x6","szLyj5a","","","Di,,gL#OF'","Lrb"," ?;","9d9.|6_pXO","s" -"S%","1/XcSaMX","I P0","#+9eQzCiCr","","jz9JT7)]Q" -"9b","G|&9Z5e(d+","d","C]d{~@su(","","T&_" -"RzDn,lBMB","XUzS?cHL","&j&M4","l|+C>" -"R",":1~hq","jg]o3","@","&&:*NP&{","-=B:(#","s,6>S]#","DlXI3Dq*U" -"$f""G","DMJWP","YI3ca'c-","~?,nj3R*","8=G'q""Y/u","kB" -"Aw|BH7vy.","","d5#):","zUNcA","=","xjE3@EQq@@" -"","n=j ","F}on@h","d#","+","hNK[=9E" -"U%5g-","","24","9","j%hhQC""d,","9l$yY3Nd}" -"l!XZ[yarA","jr8#Sl]","]Cu","g)]xx","lV4r^0Z^+","" -"","Ad)Zk3a","V`6R99","","","/|","^{" -"*'|z*HBd""","X3D$","&fUFS6","ztR9e_sRRu","%P42.","" -"[)hH2","[EZf<","WA4k@Yx<","","k7","V81M" -"Q","tXq","r","h~dB6","J78cdP |","?~o[x" -"Gt5er","\iCp+&aT|","1","=Qf>","0'HvX",";4YKF$!tA" -"YwMd`","?wx[5",": (WPPT6","",".w!R/0Au;f","" -"D\fl","P@-WeBl","yY","P\g*U0D","qyqWUEnqF%","%" -"s[L","","tkp,eaY^o+","]9*{","","aj","Y2MK","3LJ#L""L+$" -"rShLDbL)#m",")b","""%dH1$/&h","Hgc","`z:^#W","Q" -"y^d=Vq}\","o","%XIIDox4G","N#85#YtP","+K7","" -"m&\C)'wNXT","UO'\eQf","","ueW`","2QxV<33y ","*'k|*(9" -"","a","1zxr","D\+PL," -"n=cv$opPG&","","uU`!pHl","s5*`5","=M}","yvG\uy" -"Vpv9Ul>","G]7*ZK","",".?hm","A","@!" -"J^Wci.d""/","XHS","&","w+GO","^]S,Bj]","'39`" -"","[N","","qd~+Tr","Od~","J~R$S" -">BI]iFU","|","k!w^[P~I","","jj/1K@","7yTz_igXd#" -"><8DK;S","w#","\","avI~D","o]@%","Zo{P" -" G7&H8]=","x""trJt"";","_/ A2","hh)JU+d_-","","bru(" -"M/","hip.$","KjL4+!.c","SeB?Co?GP","","~^Y[K" -"yI@" -"e=4%","z.y","","O17 D^","m_j'\8","Hbu","" -"IA!PiGK","oO6PS>o","""le Z""FC" -"xS_","","U]M",")6WigXr?j","e#?E,Y0","Y,@:^/=r" -"%x;9","r`c`{NcXKu","}S[-1}VI?","~B.tqq)","YKNjCl2rup","" -"[ H","9SLb","{34IkB","Bj","KiP+&$C","P$!)uOB'w" -"","d!","uXz[}","8$=O",";D[@e","JqtW!y)" -"CNS""[!","H=*kR>zy>h","DIWhg",":%@?d*fhfZ","}rW51((","RlI&" -"N%DU","(8BaTyO","gy@o","","W(9","3=9:.V" -"9","!p5W9","X*_?","r","""r,x.pG","ouA!a)t'[" -"Wo^uq3","}_M","W*`45(A1",".Y,oi{T4'","d!G&","|R[-E" -".un.&[p","y","py2tVO_n","I","0b%/0u|a3%","" -"E2G*5u9t","I*Q*ae$q","m","","hQraj5$""","Rb}1`8P" -"`uM85JM","1L$``$","rbN" -"1_.f","AF;6nq3|","|6","yc","LOf7@} ","s:uf>\a:6R" -"/9v%]X}Vh","D","v0vcQ","-o","}MW""{j" -"","","R)WY?>&0\]","}Ask/",".83","Y}L={a/yL>" -"hTeeAb3i7",";","GXtGQ","mnlID","","-5w" -"CS`U","6.~RO!^jp~","z?(Q#5y8v9","i","q8","cFQ$(x-""" -"EC","OscQ1%%","I$=X","4~n0z","h~3","_{@F90" -"a_","J~@@hk","Wc&!^","U=<07L1","#y09@c","_G@$-Mcu" -"K",".HA6","1","(NQI","4mCZ5%)J","[)y" -"","l{dc","oq/","~ZC<;c","\Zj+eE","i!G" -"<(F?","\d2!t 5?","hg""","3}6oV6","8POkuS~\N%","sT" -"&B","G>':PrG3^:","","3'","9i_Z*A","r" -"`;zk<`","shu","Fn4!","([7~5*`Eh|DNs=0","m76;,","Keb!","rG1r","A$BA" -"-","-T","/It:W8O","V","n4~M}","fj+]" -"T44X#+`s>","-y|:m","p V4/v","B[c",":}U*i$1","LoD/cY3F|" -"gaqt",",|NSar4`","&","","yLECc\-","A&0x'&E" -"A{/2e, \?R","C?JX","_QI","TydVEl","$M2Gj%XTIH","k""" -"xT<@*hKs","X.+","/9YEP&q`{","X"":rRb","M","nbD" -"","{|8c#s&G{/","_2Y^v|$D","|z1ngG0q","|=9B/4]RL"," zE69" -"`","","[!@","/|t%;","^g?d","cBR)p@qELI" -"lkuV>","?Xi`~p4r@+","9,","(KmUq3p>","c~jagEu","$qC","}SJ|kW2","E&57LD_VK%","p@<","5Z_f","" -"""","[+Z#>tNb","m;+(lTK6AI","W2e","=E\","""H" -"",",hN9\","","IT$sM0","cwqF","HNXvEc@c" -"U","0h,E","","bl]S,","-9","_5S" -"4Pdl]=rUr","-Ze","@P9sC","*pgRYh","'","!$RkwHb-" -"#*YSiM1#k","c","jv?Q#?>","\_QC","Hrq","v2B|" -"pNW?(uH ","wS\6",",gx$t~M}","/v-$L","ShI`_:]w" -"","4[ \>IgZ=","$d*4dZ""","O<;vP","",">Ore\%\]" -"nu02(","$e*",">K7qZk","\v]e5""(W","**0,T","#7;AG7+5""<","8U']j3 Z","kI4e:","sTFDz^FC","ly>vQ" -"","eNj","n'oQUEw{Q","KIpcS q","ARedi","uh2b" -"Y>sGR","t5'+jf0AJ","JJ:!r2V]e","a)Y|.I$UP\","3S","S*" -"RFO}]Q%","]=fo_","pQ`E+) '","}+|9.LrL/","'h","D'{:" -"u","y!ZATW2m0&","?>%:WKjO~D","_","3","","dR",";e_zZ3(Wxw","" -"MH[]x#(P","W""]Y","*_pWGvjE| ",">1Dm?G,BBN","Z_~V)","W:Y|Y%(k" -"u.5Pq","\r#""A%B","o?X<","IuF","F","WN" -"8$L6Z","HSktb""5@n","f",""">hAPCAI2","","7APO9`" -";Ms8pO","*m+CDBS]","p","","rs\O|rD-","n9?blg?ru>" -"IO,","i9v|sah","-PkL","jyxo8i","aXfPa&"">k4","_l>j)N" -"m8>","eS2",":'Nxvm","F`$|y,uh","#lEQK0%","NDd5" -"o~,Wh1;iMB","JV%9x.@iI",""")VV@7*y","Ao`Kwe5c","/F@","{n!s,()<:a" -">e","{$B.",",Am&ttsU","@a|","8/" -")Q%","S+L;@@4","jr","mO%Cj$9""","6eSm5","@ytq" -"3Ei77[RC=","?","&R""d","j\en>","_M~3p]","[" -"t%({","3>idJ:9|NZ","","4$D<8{hb/","2h{","c$&@JhSY~n" -"DAS8|","Y7K1Px+C","`ak9","Wn&Pv^`r","Aaj~","q16{ir" -"kK*=rW7h" -"lY","`KUR","+4scACgs}O","C","8aBpHDR_","CtsJTZh~" -"x/[ouRM:*","(RA8d+","m(pE]V","b","(PiD_G","'K " -")mQK",";sV(","}N","9*Q$GcT2","SEY,6#y<0+","y!n2" -"|#"," k;' &","1BI>aDR","|q?&{(~$","exH","bq'",">ul""V","@`]mS{YL1H","9tOuZ" -"4N","","G,.","","S'k3(/q","A" -"37&Qa_W_","Nb&" -"/g)tnp_","R","E",".t`Q1t_j'-","Lp78","{)NXoK\[" -"Y","%v3","Z{DGm_Bg","Whih%F<","e^1E[Sf]","~A5Y(X;" -"1l""H/1*A4","2K)(ThDd,","24c","@M_n0W","","=m[O" -"","[SZBy,s","","u'0xNjw","/y:{+5iJ[","M {@OVzA" -"|@gr","^tL&","h4/k+","","E~y","!Jw{h" -"aX;y","Beu43#","rd2|1","i-g'vz1QX%","8|(b|","1:y:" -"tc'&HM(l|D","Lbg","@jB","DU*F0y","X)I.",">5yeR" -"ZBH%T1","""E8t[","bYj[v@","2","gVQ","*CU" -"xcO8,","/","Y[uI6N","9Im","x;&nx0FN8~","M;P" -"|\W",",7Bbo:,D",",'8>zD1","HUv4&>0iI","|q#PA*q3","x]t'{e" -"\","}yL3\(FC_","","SBh` s","VYd&Hw?","X/p:UB" -"\@-","`xd","nyY`D==","","wu","texOMu#V" -"X @K$d3",".0M|0","*\Y~S","E?';1e:","_l?)`",")4hts:5N" -"~\","4qTHu(x-","mmtcM","","5w:mu*oxc","ne;&Ym, " -"Hm""","",",LtXU}%j","7N'qB","A>H","?EaNJ=" -"","32=}pv%Q[s","K?'#o.!5>g","QQ","1m","}s{>DdC." -"",":Wv}3.>k","{}s?X]","p?f't)","s","}" -"QOi","ro# W8n7","|TTh|XNf$Q","/tQ}_!","z^,CF3O5","2X""lf-LWt" -"QE{","C~}JEq*L","","B","8()",",y~R" -"b!?o2:}t","!]gG>","e9.u","3D","A@f8<","" -"taTlE","pz2F/@h","'`]q'","4qYuS","s>M%K:)","'P-" -"V3oj","\[~HoLbl","e6v","9OKN,","'>,l+noQ","?z-&" -"","r5`5~j^d|",";~_!}Ev|Jt","AEP2f","4","j+\v7ANGt" -"5","nX^bBv|","}rzei`L\9m","","8k","qWCG(a2(W","" -"s`KG[",">/TVHz(&Kr","I2^}","R5s","w>C)j84+t","R(O-V b""TU" -"ZV:cFQj","]Y`CxQ ","ei","Tq>,]*%%","O_]6h<)m>","UqDH/bWG" -"-6","L|S<}%=5'","nTq1n>9","8lm","~J@wf&-JL9","&h" -"%Pn|G$=vw","x^oS",";w.^vUG#G>","i","F9","R|Z2NCw" -"R=Z*24#A9.","9$EI\}k]W","V_","","","6a|Y6P9<8" -"9A|]bLC-0i","!oO/!HE~{","","nO","H9P0LCP","q^W3 Q(L~{" -"FJE(","F","ww ","kgb./(X8;=","","YfDF%zWn2" -"p9i#Q","","]*mH}/&C","<","]fO}&7@w4","m(" -"dll*r%n'","RvvqQ_C","yH!z#A7Uo","3P>/,9s ek","'h(8pZ""","lK" -"s%FCq","&cNT","l[","Fk4{z","","Ap8D#" -"","<3IAZ""T}!X","C9d#","ckaS","'<042|APki","UVe$Z+We" -"","%","y/0N:Lr1","?|h&jX","d","]:O" -"(yce","u","'va]","w}5-#{o=","-","" -"%+,","1[","RaW~q92bO","T4","c73-IG~","kAz'a4" -"FHkeUm]","}Y","k""q]vm[","F i","U_evW4[9","<(_~f2E/7" -"^vv","JW Dp%K","B_u{cd.N","g*o~7?","""k6`rYtB","/D%5%~" -"4>","@","","56N","Xo+r`","k0=l" -"R{W&uA","H2","1ZY>L8.<;","LUFGto{","Kg{}=Y","''v!U.j" -"$-let\9n,","V6P","noR>07q2Z","hBpQJ","cD5?Im ","y1`" -"EF\n4td`]","Ly]y1mq","","O","\=WS;","Th""W4" -"2tsjOcJ9L","CXPQ","","F8)YFY",";C","d" -"z9P>1Q9|7","2","}8 l","d[0*cs","","PO<" -"[wV","QYL08T:;l","2$jCA^","(|*0","W%","pY.83=[Eh>" -","",Ps","k)%yr&]43","ZF4","m}\","a","x" -"Ks#'fTc","iK&\","wH2b","",".iu","sN8 b@" -"8F4","`|H?","","~A3""UR`","h?n)~)e","[ZNu" -"v","& vMrdG","`h!","$","W2)W","Xp!lN{" -"iJv","Ab","'+UdYJkQg","F>!","Gq:D","""vSsPS5" -"K","W@lwZ9u","XT>/Mqcek","y","RE","+'_=" -"5hA","X","v*iFb~g","lotbMi0u","","XG<-y.@kbX" -"E+_Mx","$o","o<","v6>`&J","!X7aE","[[mM5hrp" -"__9J'CSS7g","98E;c""","RV5","","","u" -"c0=","&k","k","/6","%~Cg+e5}","W)@|$0" -"Ds0wt;","8S_PZW(","y:;","^i(yU","U!Z-;nCZK","+%km" -"f';w","ubD\o","D[F#","gZ",";k'fx",":n>Xz" -"Q",">hD","bi6LbP@","`\@","/pqRB",")&w6/X,1Z" -"l5f?","vQX@62M|","Pv","][""l[!,U/","'g@s:ai]>D","+/}g#C4n+b" -"","oJ>2fo","wF>.[","WoN1,B","~|[$(L&<","# ]0" -",TJ""^Ej>","8Zr","bY?:J{""R","G~?","BsQ8","b" -"mY[?a","xKG&7""","0hg","O_xb","{nz[JBL- L","" -":","9{R};yB","K_","rpWT-""","8Iz?}" -"3p*FqmL;9","W?)D""H4H(","0$#Ln2)Xb'","(B","*s","*PF}rt[_V" -":wSD","q(4-{","x(&Dk*[^","g>v","YG","f!}e(" -":X_","nE-","EcI""O~a8c",",.=#-C:GQ","","n@o{" -"<#YrY","T","f","","9","mn?EIy" -"Wb1l""9;.*0","""f(","@K39""hkVR","mYW","qt","r" -"9","r$""","U+ Sui%",">z1Y)+k","*""Bvy"," O" -"`!","cPlLL","n?=%p","}bk>Wylnc","0D?L","%z-v$8q" -"T}y=8%qR)L","","nx","","","?h_8&NzF" -"_F}P,","0L","4jRkg&>","`@n","/616","sj7" -"mrXI",",5,\Uy}rY","rZL=","@k1&y6@5","","""@xh""F@" -"m_","]/Fgna9yV$","R)!","k'>Uc\","vW8[Z-","y?" -"NfXHZ+%[",")^$42V+?","kRZ}YY88&|SX","m]""8h","a" -"Z4s,","dA","W7:i;Yge","Prc","N.","" -"O","/o!2uT7#K","m5s~0","D{hZ(",".GG dGfo\m","T8n" -"bM","BVjk@?[J8","","q","=GU","2" -"c","8x'!","","D]~@UL*v","Zh","{-a" -"cE1 ME s","Qc[q ?1#J-","BCP","","X4[7",">^S" -"?","|}4BEnO52c","`nM]B7Y U","C[IvE""","yae2H?Z_&g",".+gjkGE]" -"popKD(}/fo","/4","TU.","&,L=DpA","*(","G7" -"c<4>","r~2(0[dgZr","A g(bG;","JQ\8~.E=&","&:'y{Pf@]","%:y" -"dB mDZ:","_{",",U|!@!c","","3HMwjEF","XE:4[IS" -"hlL","eQ[(^Qf","[ ","N","YTm\","" -"oVv-EAqS`=","=SVJwv2|","^F__;","P3Hc0S{G@","#=CP5","F]b0'u4" -"&$+o+T)","^#/5Y=","c2ruV 1^G","EAb4{Yi",">""uU+","T\8;lR\<" -"3FnR","{","" -";R:}$""e1","#","WY/A\m","q&[a=","lTJ6X$,y","c+oa,N$uX" -"","W=bSGc.sf","0n&w#J9;0","$@/-]e","MQ; ","7(B!" -"ta:Xyh1","noRfo","","L2te`A","U{enx~Y'`","Ou/(^""zO" -"%%|#no","kruMC;","R""4Km}qm2e","0N}3","g[@a(f^""?","a&v#QFjhu" -"EEGgch6YvZ","<=b*3S","","|f","\Qv","jDuf\gU,O" -"RwJC(","(U","8\==?P**","D&|?","Y","O""wP=+oDb" -"?S[Q/l(","wG4&v/s","2D'o{b^^be","aG9w%" -": [^A*Gd","T`k([`*SK9","""cTo5","aI[" -"RvA","H|J@J~qy","","{!m]+zs^GZ","fnO",",o>|(-=" -"pfci","k)./_i","@","z#a%$yw$","","H" -"+H:-}X","=zzwxQx0W%","t0B0[8","!ay""AQ~J*","A'","" -"u[ M61@~Tf","I*}aThg'#)","^n%@","","lB?)","PqPsHyi6" -"Px3lg","tR-B{.=alE","R&sG+SX","",":",":bFJ2Kn" -"K<\h2.6q","5\&==pj","!&%QuWUg%7","qxof?;O",">:=","~zx8BX5&}" -"s->~;Kr","jC~3E@7qfQ","","#=2Vlv,aiI","+FL","" -"","xy\~","","y>Hr4|ski","]$GA>L ",":v\e^dA_" -"","#uG;","XOI(","","!","oe%" -"C1R","w6ZJ.2","/&","iE})_MLSH#","g","AF""^ih>$S-" -"o)K3W:|`}","nT\/8gC^","{_","FSyv~SK","","a|" -";>","","AX%>k","{iq_f_^7\","fr;,$","(0+`-zP.{I" -"`$T3!cO,b","WPk@`Cm@GO","","d","=jx&_hXc[c",";" -"A?","e","0\H_QF","YG","7aiT#","x+PwO<" -"i","5d/X>3)#","1+'(bwSq","}o^V9kuS","fI4\(^Z","ON7C" -"uT&v|2","5","/","","y","r{=I.\" -"N[+v","`","","o=Rz|=\","1cWjDkh","Uljzk!" -"o8_`","nrJk}eqGfS","4fR""},!s?]","S{bGL*;0","","7*$" -"8JB7xr#Hy","06","_{F","tD43B75",".bLTb}i","o?T" -"ixXV+","`","","%$EV=S)-","+<2Rtr","tYq>Ptf" -"c\}3Ru07","n","+P.NZtRn","yn","D@Q|*39L","7P,XcH" -"mPOzk|<29","PdS/l:I","","([X;7@I]$","%QYy:%&Ih","|S" -"utBlI4O#M","c$NTF~-_{","V","4uW","(W@i","rN" -"JU(P.X","4!O>)","RQ+@ AT","jg8qpq|.TD","/u","=XBAnYIg" -"dz","p4*>{YG^","~","D<@_[","Si)?Y",".cL1we_(dD" -"9{(E"," 4","tQ['g8pu","t3_","yO'J|",":6=[k0*)}v" -"9?n{","jY_n:","7","2",".uWtU\","+/" -"jq:j"," sE","oJfe80i<","w4",">0KB","" -"O:",")}#f","Hrm" -"",":.","40!|00","/d#","lHH","k:#}(3" -"VF*b","NuFf("," $","#-Y#ipE","K/T2;YhA","-X9HasVKP" -"/","M08T!","qu]Hf&`","qjpc","d)sug","81ztm03!}" -");`$0o" -"chDz2's@p)","cZcf<","5jeuXv","+hrU,T=","fM&!r4","CMCYBL!m8" -"d]","h+9","o*[]0I","PR:@","_;f`hq_","r ON" -"Ol0d8Gf\(","64c&0IJ]or.W" -"[$+F<","D","fFv$5_n","^m6Z2","t^Gr-:Q7G2","Rt\V|" -"L:R-*","@ts|t|wnA","","`n","","]}w~Y2C#E" -"Q$","691i:","!!>j=#iv","K!]`|9H","dVB0uJ=WL","dXI" -"Xj `y>1=6","72","oJ=@If]N","-D5","LX","" -"aLQsfsMy)g","Lt7E\N5}","SVfn'C<9(d","","{\","h%bOzTB/q" -"Nf8lpr3; ","Z)/W^mihJ","_NamL@2m",">C",":s-Y","?" -")'N2=ZA~(*","86eAXvOnm","L-","6P^ex",";dg:rlWU","^ bn?x%" -"!^","&6zh( >$fK","R","","MV6Mh+'uQ","c >J-TZ." -"P","N","Dbew\d4","An>Z","","TbpiXl" -"e1Ht#}d'&","w[B[","4f","ogT","C","lC^" -"%y|NR:u","}s.bCb=i","iK7#","ok_jxu","+WGctl9","u1Z}" -"=S`U(DaG","M$","M1","e7m@MrM{","$tQ&C(p*","" -"atDF0m","%kYk'6fU<@","SD04Y'#+","g@P$ XA"," 9])Cl2@}e","T." -"V(XjAGfP+I","\GO4""T5,~h","","uo/","tdqu:=f`","?_(N" -"n","lg ",".[7","5:M8I6M.t","Ze#oSj\","ySs2$1VC" -"_R}z>",":","06zjeuTDBk","6VZ96A$","JYVnFi lOn","{/""+P8v5Un" -"{TbI,4'.\P","r!","|wf3?mR","5y5jo4QP","[I~1W2ZKc","SwIeq8" -"t5)Z>G22R,","S$3/?s){&","vMn","lJw+","17\$Iy","a1ZUK]gJ" -"cuh+","J/j\~A","k48~2mf"," >bd]rp~M","Hc"," cO" -"J","poQu+}SHi^","3IID","%4DX","xxoG:m`(","k" -"0`!+","Ux(n","'tAZ*","/&""oOY~","6OJc","nr[ M" -",][3X}O$","YUE\l:1R*:","J2@j","B>","$_k*&rnLo<","" -"iK/RK3ty","(d_o$UB","MN?[:/","nQl9","ox"".u&B0","CZ%Atz" -"",".SMK","8*IRHk/"," -RbUX3[n",",T3;wLh>~","{28FE|" -"?43h","","UK",">XR*(T","SFk\u-a`","p" -"-_","\%v>6Hy$CH","O2B)","Qa5","MT","^7" -"tw:,5E""X","#HmD#}xD","](&6@oI6=","an#@@jR","Y(9","""*0g" -"u?","qDAezvF","p_","zlp","In'ih5A%","cQdNj`D}`" -"V","7","OG","","Lghzp=)bYM",";-z" -"Y}ClD=ME","@AgHhx&kQ","+u","","x)","rD=@PJ+7\" -"sqg","or1l+E ","F>","","XB1ELt!}-","|uW[H" -"","","#oYfF&","y^R ","R4ai48|ee","M{5!Zei" -"zAv$.&X\s","vC;i","AV","n`'","$ht@TVJL(","P.KvkL" -"Lqa","'pjT8W`xP;","%F?7W""","nTW<%Uc","_","uv\@" -"","XA=Fh'1*Z","|/*8","yP8ibO","g@H>K","#" -"*R","`y]5MS3","","*MEFpvqF","1qecJ","F@" -";XU","(?KL$/#","w]LBt" -"*VUHX""1BZ",":,n-RHc4","#kSUF %`P","j4Rf>\`=!","z%^!x","9""H" -"iX8cgE)4G0","d$wp","Ll{JEV}z","*0K]gVSl","S&J","n,-Bg.sF'K","Y","a3^,_(" -"y7&","ine!a+ISD","tfC","]{z~+","WCGt!","" -"SO?y,E\Im","C~qxsE@9","m@/\uv","","WKa","^l,jUMe" -"XR1J9",")&D7uS'","+~`vt",".EH't ","SFup,QV","6rDpJ50+1" -"d09","`Bh{|e|]","","\iEe","T","k4,9^" -"HsTDq","X)L30jRRs","Ww\/$t;""!`","A""8*dn.x%","96+s`q ","'g3nb\l" -"WDW&>'r3;","7z52eH&>F","VSj","s=z=y,iI",")?!M","|wos,_<" -"Z6 c?,_JK","f.!O2TVA","k1IfG","lp","F])=3_V;","-""c" -"pzT>F}","/ee>","[o4l@u","Qox","@","}kjx" -"YK","gE(G""ZWK`","","5""+>,z","&Ocj*","[2HqzK,ojc" -"e4x5YN`@","PWk","vj","$8 ]b?~r/","\ l+7#%","" -"E8U7iAOa","b>N","^","9","""=","=Q#'" -"erD8N;d","","fSq","^GCp","S^D$I#rc6","fm-'r" -"j1","","Vtk?0p4&N","A[QpS5c","C+6mQ","G" -"9H","yUj5","-nd","/I@yQX?qg5","~SR","17;" -"t","B""","a","Zv:f#HPtYx","n","Cdv" -"}6""Slm$B","Yb?f?A","cU)&p","95XWTl*J","6m%i 5?.D","" -"NrJj","0|dA4%","Ht","+>eq","q4PP$","J)k" -"5)#40U}",")O?(","","_bp","gKsIP*l-",">a&U{" -"&)_mx5","p)*i""","'o#FV*y","","[Oy/:2a('","EXjV%=f?8" -"Gvtj","H&m_#67W","5","RH""F#qh3","","RPL" -"","w]'iz?$","7","bv3J","6/B7}7pJAd","SR5os;v)ll" -"A1@""'#)#'","","]Cc","gc+[","iv;t[Z","yQKx" -"`.)","1IV""L19S","Q`(Ssi=]M`","4EcI","1X7nc`Mt","{z'T" -"","b`","8w\UQ","mMX","&EbqkMQ","S","+A6`5" -"|6Dpk","nG/","h]gQ,Ub",".'","?U]HH1vy","%F<" -"A","#r","#+>>(r&w]","","","%g" -"VRtPS~e","}PYc&*^","Y5[SkAW","$?.I9\","lQ3y938VY","K}" -"zrjLc","Z|a#00TBxa","veLdNa 5","","U>M","" -"","t","U","tY!?y J?,","sUJ*Vi[06","Ey;" -"}w",")","B]%Kf",":kF","#'=l","P!","h6'K:","I?K" -"","W:M:7","TdaYc","$*Y","C0l","CU?BrtInc" -"/","d!rMw83I ","da|Urx7","Y~QSxE;","vKE'kfx","A=|:#i)eu/" -"S+HK","tgd7e9D^v{","","R","(dXJ]","WN-""" -"laWe/Rf1)I","e#}B-;,pD","/*A",">f|HY;Qa" -"Owm&fl?X","#B","-X(m#0O","","k?I","<*}+pSOo" -"%F?X","","SPx{$J","Gq7B5\e:","o%V4()F{-'","9|x)V|" -"PzR)}Ej^O","2","""vF4","M0.dz","l5b90uB","*ZryuJ" -"","T5Zt jS@O","5,","E7{KOm$5K","X(h-i%'}","[nk0+pSU'" -"^","Wh,Xn""(AD","cu~$sMt4]t",",Dh@/y","","" -"Nz+a}","}bar?5Q;sJ","T=C[/","J7""lx","","" -"KDJ75","8","yuJ","K,","^;EO~I\",",WZG]/oB8H" -"E;/P)0x{","D*i_F","6`j494Gg4c","jnL=PY9Q","#","7}4G;UIU7=" -"$U","E-F8xTE4","rGH:6Z./","5s>Lh9G","\8|dcK","E" -"vHb+Bt~!","wL<;","/e!J","_fUkr\ aWN","""XQgPj](}","tv" -"L(fKTM","3u$-v/ch&","HR+wc","QgLq$gVh","jI M7L","# &" -"r=M]n)!","A6HZ","fC","Jt7","kq#H$"";B","(Qo" -"e!s","AT\4R","^/gGe","Nc7Q_","7[_M","{ca5uPcSku" -"NS","","?vdq\6A","B","N4:","4Ji" -"""C6mWF9l","@a;kOW","y#c","MQbmn","]Sl/6qN^","" -"!z","z_is","i$aa=r" -"f~O#`95@8`","q","G-","5,R[N/'4xo","8^,a","fe|>B\" -"'}+pY_c","-F]>~-'",":-Y~S","","J.H>&Fi","Rq|V*" -">}","T~meCpyJ^","","Q","R.Qa[.z","k." -"+Nn+IK=S","e","o+{Uo","mp>","No'mgJ4-","" -"J","od]stFv-]","V`YEG>","","#i'",">m","","@@+""","+89" -"0MG#>0","j","S9x","<>","x0ZjM","xa3 " -"","sm^t\A","^0!/|\,cJ","","->","Ky\|" -"^\_Ewh","\e%TC","U|I/@'_l","o@s","AiJ{,",";ro$g&" -"^#`""Xd",",B#","Vsw8%h","""`1{f9bSc","t/y7!a@i","i""5>" -"+s","3JC","AGl*]XG","h","~Q8!FV9@","" -"miuBJ1R-.k","+""-~","OIQ$+Q=(","amu","","u-?" -"lq1}i","%KxCj","K)",":0a$Dr[","@VPNmm24/","" -",R5b.q!|-","5w","#:0(u60S*","Ye]=gb","1HzOtk","w~Hs" -"+#6V","vi#+x.t","z#J>","","g37`~R","/6a+Q" -"Xh(M","wbf`@T:/c",">: /,_","Rlt@","","!c}f>+T" -":C[","#""*x%\f:f","xa7/" -"","f{SEkSGh","(]","8O{","@wX'YTJt","V<\w~Aq" -"?P#{\=(K=","rAr%G","T","9]","","!:""ls=TMxw" -"JdD7w E%fE","fXgmQG""c","n[Xc+YIKD","^`6wj[~H","0","5S^" -"u[|4","Xw2[cskyX","","nWJrL7Zt","""E\(5l]Ph?","" -"Z^8","R",".CdMNd=F;","~x):x","""""DT|c6BpH","" -",W$Z)$qn",")","@FK","G,`?atq","FT{L","}a/%X" -"v,S","{","$uL{$[Ih,B","*G;PES>","NnX;Re","" -"X?}SY:6Xm","BN",".","","qco","!oS" -"7Cn:I","%Ub$","F4MFLH9)K$","","c@,O57KiV","mq\" -"Z)","w8J#gSg ","=l3$,@",";{Urg6","",":u" -"","Mr1Z","'~0","]kH)","]fD","#oZb" -"CJD3",":AI_vex5o",">y4","9,-WtDA&<{","+1\+~/sTc","" -"m4Q","^u`r>","!3]m]","","07TbdX6!H",";t: UFd" -"X","hrm","4J""VK8O:$","({","\(n<=9","T5Z>xd%9I" -"V1d'Jl+#Dn","(AXo]Pa{%s","",":K","_mLV","5U8b/Ozqw" -"d","VV`jA","n_>Zp6V'A","NI8","T;Dl#","Gtw?~" -")a;","@A6J[4`o24","","t!^^N","`c+Rr","p\d2G~L" -"eEG/&!","[OpE4)viNQ","yAw$""f0u","burv6O}","nzVS","ML" -"4S3b#3'j",";01","h.mSm","ZX~uV","MX(2pb&x","_mH4{A" -".eod} *1XH","","*gQ",";@w","8%Mj","wo4@@" -"Ch:AX","b^Y\{","H","Nq%!%A/sim","5#","ZDSE" -"P","CLe","UC5Z`G","","1tf","O8$fq^Y{j" -"&vHc","","jOR|_e[","mti\L>","Y","Iq#gbp0.%" -";E","]0fX","WPi`6wiVgb","='MgYi_I0?","5+/","O)`A>h)&","(""g&ER|1l5" -"*K]YeO","","M1","V","w>x","^.$N" -"?Q]","-","k6<'@N?","","prp%AN","%nU$!" -"-x6D!LgV4","","Iofa78Or","#5g4_lXn$I","PNMU|qU0","Z*os" -"'CLFy*""to","X+<","^kgjm1:.\","E?2_/t","V!?#","nZ*7" -"Zsi!PBf2u","ddU","j@^roIK","~wVcb\Y*","LV","o%`[nK" -"6I1","QF8TxP","v^Bl;o3","b","fWH3[","yw]5Nxe" -",*orfSKWLZ","","jX#k","h","ikwcv ","=4rhFs_xH" -"","tL>j>GJcC","T","uKX^_","a[sdV`Pz","Sm" -"V!S=W-c-4","t`b\ A9","","i9!-","96K\:""+o}{","+:" -"NAJ~","Az6t{m!CJ_","","d>G?X&sTZ","V",".hi&|""qa`" -"a.8#'","(","\'K]A38;O","J*|:_","|D","c" -"G$","","1","Ef","BoZa","[""J""" -"/0,H6]GY","","\F$-vz#K2r","0X@8A1fd","mj}'}a`ctN","u7BcM9:" -"nW|sA?qXC","h(?","!$\Eejf","H!P","9","|7nWBm`[T" -"r^FiB+mP=Z","!",":~J4T","b_uF4tV,(","vTl{=Us","_ -" -"","bth*fd9%","h1r\Ep;8s","TfIOs9yH",",","1xDIv~#G" -"!>Ln","|HlkL","4O~'`M0","","6nNcJxiJ","0" -"o9Q.!!?-","","\^","S","J_xJL%","" -"","^;%&<>8CmP","RdQ\Jj_!%f","]JP","tr{H7","" -"{8j/#N*73","","o|rUz","SDPE98A","sF","FMr" -"V7/Xf!a","3e","E3)|p}\y/","","pr","e;{\S1`=#" -"VsvFpD""u","=#>gL hgKc","5<>,\","H",")vV!m%u ir","#z_" -"oe32&Q^_F","}*","m[>t^2rNO","p;Wu9+sbh2","LIm l99L","O0|jH5" -";","7g","","L","2D~GAd","|6(H7>E%5" -"","9L","xSaL%H)::~","&m,",">Wf_@AllF","" -"p9=MfoJ","zQi#W?","OS(13\j=FB","","ha}u4H","" -"p","ja","l^^qH5k!gL","X~xYC","$?5N2a","gU" -""," y%f","2","%D57;+","#`*>","L>UzOrOn" -"A","((9vRc","PkUtfGU{4","W=","q`f#]vJgm>"," IIsn=" -"L`8""Q}-27%","S:pY+W@M","++:T_C:","A5AVY.!!;","=5""L","" -"]?","3f($cTk","t*4LP5","N","G).Z.=;N(","g" -"b3y)","'[0/","L&_,s>",")","%MO~&Bww","58t\r|X`=H" -"mjk*%&","qWB&j","&v0(JM,","!>*+","d^u&zEIgD","VN" -"*ee_.""","""","\qVrxV%","8>\[|N","$oJ7st","""/J1""k" -",","y7~ohX'F","""p","7|4qALx","^u\i","" -"ivBP]""uM","`8 7^yX","J`_","|?>VGDpGg","s7iMkU""k","RYN@zu~F","Ej, w;j$" -"`5c","","cRu>","{@%.A-""","{&8&J'","6P\cQaO" -"Hb@94rah1h","8~","Th[`7p$Z9G","YznAYYo:","(>>Ve<5","LFd\BlS_jK" -"FD^","","$n#","","/`qc","=<" -"pX1","6-_W","IzOA!H%","v?d*q$Nn","Nv}YrAyiaN","DK+=;w~P" -"/AW","""I{_","6N{7_+","PLsFAs1;","8","tmQ*SdJjEM" -"3AoPWCRh","{cwS","F>Z}y4^%A","?s)V43>&(","{dR,l^ah=","","b:w-" -"1@EhM,(\E","\" -"gN/~84","c3R_GmQ1","svrtdog""","2!}RI^1^lq","JH>J","" -"oJ0RJnr","{Cbo[w>K","EfuJ9\m~:","O_<=qD{'","p<4QQ","}" -"fkg","e\1,L","@|A8IY","Z"")9*","","T56:" -"",",e-\m""g&c","0Zo","FjM","rTPMA","^V-~E(s" -"spko;@X[F?","[s;Q","Xz","%U&[|","e?","Pcb(6;" -"TVF)I","|b:","V6","-","k0aWHpT","J>" -"4?K>ze","sU","Y8x] !",">qh","4W7QZfR9%4","rO?" -"Se6A/1","pwA3Yh","i","1`","W,f|FKz","3={[" -"2","P!SqX6","zm+;@Z","7RB","O%VT","^" -"Gp9XU","?","","","YKZ1@","V56lE`vo" -".""8$","-!","r6g$TZ0mP","KZ_a""^","B[]","#PR?9N#" -"Yqftd$~3> ","JHn","bB=","7","K|vhVdQ","n.7P9" -"s:;}/CAz:G","x/ixsR","6uwMr :Q","@#sK[;<#","Q#;ax1","" -"","","Uozc","s","","bf#Jya%" -"""kz4|ve","}!Lhy","j.jn2","ia","%r'D","c.$H\Sq\" -" Wb]","W7)","/=Lth`&","j;]fTs\N","kL;hCqH","Y1`!yg_LX\" -"f=",";zTu\,^","cyfRp:""","}X;P","$vK=3G08]R","" -"7","H!Z4.Nf","7$6T\B9","=>Dys",";/!X|j","%,S" -"e=u","N~dL$*OcW[","TGJ(FcC ","gt{Q(","pB^oq","","o:=v","C8C!P')&;" -"%s1=xGc}",").","'{cd;9j\","C*%,*&q%",".D","1I5hR;2" -"7/S=z!C","23","38""[KMT2","bwQAR.&","4ZF""]""D","/WnN7\!""a" -"iF``i","""AAOMk","""73","C&?mME|","","5)Y3_gx=" -"","1r~","S y","J6Pn","#kml","N^ZNj(t7" -";+7j-ob","9","y","NUyDn.r)","6B#9U","" -""" l",">P","]Cu""ZOccC","K>+","tc6/Gw(1","-fE1IJ" -"R","dY`74<:V1l","pj","PyD96M,","Au[","[DTO%c=" -"-_jo6[GJVA","dO","mJ&","$T G&","W","" -"0""mgGkS","X,[WN","Wx","-_L+F=R","3^","?*" -"Bp Usg""","|x","p?#^G5G","k","HUl""Gysx","7|E73" -"m$L","uG6_ eF_c","yp9cAvmNC>","j85=9P","=5,@u8",")" -"%:#7e^dBS","**a?X;","7","q","%n(gG-xh","jb" -")k'[*","BjFP+}@O:>","Q+W/\a","_}X6^nE","PG1B?","A6bYaZ" -"%","'5C]n","{","NJX$T*K4P|","YF`YB>","bz" -"h>>h?","IGDJfnfI{Q",")1|""c",":pVm","","f/" -"#`,k~F","|=n'","wEjZ7/0o","hh","1{=|ZvYES","OH2w" -"","./2^y""8r","mI_7SbX","srn:Wcl","","S" -":^bP","x@r)Hz4p","F)|N",",","IeIAn4B;","U=x!?u" -"Hi","PHM$VT" -"2Z`)","","=n,h5)","kPmF[KI""","*(#cx/ig","xR t" -"SJD&A?#?N","t","*","","^HMb/P.","M@t""d$Jt:c" -"`Yi","Y;RS%","","Ua WnnBCs$","","" -"","{t`$Y?","","XUa,dgi","*gV$","(!tG@N6" -"","lr{>Vb","~X","y@I","F59|v","5TUHH|'}i4" -"*I?k",".FF","r","WxZX%t}%","xMJ","7|H'QA" -"9Q","MLE","An","k7","q\dc-Od","""U0O_85","0oH2|T;","" -"P 3","rW&i","e","U",",3/'~D:_D","zaG;Pd2'+q" -"R","6","4\Q","(N","LB/MrcQ;a","=92" -"1\{Mfk{=E","","#/[yR","B$'""=","[k>","p L,dVkp" -"/J","9Ze;,S","43I","%U","BEG:i1","!E>v&-'ii:" -"VjF$e","Y79.j7-.n3","ro=k","!Ksx","Ko}N","jr3N?" -"8""xNF","YT0kE'}f","Y0Qc!T!5","d.ti",".LVo","(R" -"@?FHxP% D~","9{]Cw?'fVM","m;lI2ZG","2,I{D~C'&","wf""qPS~UV","O1f`]u;Z>" -"t?2c_AkD7","{tZ","K'}iy|p]y","Xk4O4}u","Xlu5N","r~Ad3\||}s" -"i","klzafkg:s","SGiS1p",",uPgGo",")W{0x","^m8" -"-F""}&u&::t","H@rJ:8VI","G=o.;2TIRy","z;<","gSt+7 bR","~F.+[" -"*);TRx","JAJ;An<","g","01H",""," 1J" -"~G^q{/Y","l_+7|G(;3",")&1#","f\","QZc,xok `","*R" -";>?ceN","z","ZJMjydz","v,zoi CBk","","@Y*Mf$" -":fGlmr","#(R-" -"T8zDm","k]",".",")5H*#us","3{","Xp`" -"l|p","","z4cst%""8E>","l! 8*,","=c%","" -"vS,eM:","","CUz]","","o","['" -"'%#","'@?m@aL",".sK","","7FC\D_p.0J","nT" -"r","<4&F","""xDnr@}","v.=8%6","dza@r:8yOU","n1ho`Umg" -"","z","xVL`o\QC2h","i~","gV>6tHl6@""","TpQznu5" -"/",";M","Uf]aO)","x(}UO4","","ec" -"2","VnCAM*J-(","uBSU""z\",">hr17&rW","aIObt","" -"|","`R","hBdRwI","=","0y-nAT8","'" -"y","`C1u Fu","U""","*","CHKw&","k" -"ad","u!","k%Kf","l]Ay!ga","","=3)TRTy" -"*{n4VO","""UD%R","{0fFFj","sI5Kj","6mj9}P","Uz=%=PpJ5H" -"R<=e","%taOp5h]Yu","ikx1(ixX","=J!","OtEl;\kgfe","#&v&v:\" -"b>OIF{ ","oT.","[Tm","}C]cL","o:k ","","s=IA^?[T","ZZ/613!rY","L'P`S[_I" -"#","I9","'aC" -"YwZ1~","u|","'$Z-QJ","Hi9ep)o/","t*x","'" -"7$.g#Kh=","80Vo*y[","cuE_sCfIh3","|,I'uK","^Ip^yU","d^jH-Nx7\" -"L&Q^M","yt6o]Ev","f*!]x","@b\6rQj%)","C=d","8GkJTqjBDq" -"P&","`~w_]","i?i","J-;ZeB~?G:","$~u{","sM" -"m","+2to","HE/""!","3","KMvNl&jAQ","Y\" -"EN","Nr}PW7t?+","@K8W","e","lf!WB","mNO" -"#'KQO","nf5'5NQOe","'}\3i#=*X=","<P","w>","k0qmxr" -"","l|g3_S""","cq)g&F","~B+/:'$/","r4?y{$","X" -"!B*hQv+","]L%\h=^ja]","ed'D0#","P*pN\dcC","R@","L[_adlPve/","Dbl9e+t" -"","]`d(0a","G8,","f-<","" -"^.PAa[Jp",")z-","$;","*`","8WnrFEP9","%X,Cs4" -"#%t","d<","M55K,.i","MQ/yp6v","V;9p","`U""I<]9" -"aT_{TI2L1","TL","Lw7A]","B\9cq\","6","]o{o" -"|IG-""","aPBM&u","LBt-jh(#7","oX","6^le'yJ",">!u0" -"oU2$p","nw)HbO","twDSt@.)H","_Sfv","","" -"P,t#+^","=\f~","-[","5RnMDZ","Z","&_GTSd#b" -">jb a","j-m%.)W","}>?ibm$Ej","DU(x=W","","'" -"n","@v$&TYmK","m%","Y{","LkG"," ","|x@#","UbI","G" -"`""","&","fdcRXEN._`","s>O^6/i+","OXg","" -"%A","}G0.i","yk}g~?>!g","UrIhIn,%","do{#$R L","fBC" -"2{Y-","Hl@eJ(}1y","XR!a","J=]_6"," P-d9ev","1?g" -"1R",",8*c","yD>_`Jx","%X9","fbuk=@f","gU.&{%" -":)""WmHE[)E",".uxm07Br&g","_;Kv8","Fuygx_j~`/","jl","E_zs=" -"lwD[3ue","aiGuqd+U""O","{^f|P~&3l7","_","M=H)%-pg","2{8D" -"}FKC;F>","^aBSL*n}","@S[Jwj1c","""VZe58b<","Hk20'I","" -"8J7Mp\5^[","=""q","ibI","Y""Opp0","Q@D|Lj","'.1L$([mMU" -"ad>%ZL","'MX!mvqH(I","!{4>@_","Q'd^","DF?L#*","+" -"","qC3~bhdm","1$w","oI" -"F`",";82`wbuNT","h","ioK\m","r","7@""L$s-." -"GjKg|ra{","\}y^NS|H2V","5R2%[^Y","JkK}k","}}","C#N" -"8#sK","B&Z:gP&F","]","zh#?d","=","`^GUM" -"FlZ","l","_~<","w","Rb'Ad","hX-" -"FkzRi","","T+`eWqcVV*",";[-y","S=&HK\'","-R)sxn`c5" -"Dh$Xh'E!","RJZmu[A|~B","u","s""}4Eg0gZj","]#L,0uR","" -"lbNx|","_Op=%6{s","sC'j_&lpY ","uDHyz","5CXx|C5*","H*0nKRfL" -"",",[>a-pvV<\","k9N--v)<\","]K0}o","4cKze;`N $","(" -"r(>Kj|j[j","-:_","D","([O","*rN","P" -"","o/L&Nkkn","'lt(&","","=FKWv>""T","Wwz" -"0TTj","","V1n6+8ju","5!eo","ll1`*A?","" -"~h\s_]","wt","AgDG","sMsY/TH","jsS",".dq-fJF-." -"X20I{PQy|(","","/l^~Lp34","O`]I","@uUg","\GqBJ" -"","H`>","Hmoa7[z","F8o?`",":","&Mf1>Q" -"z","|2! PXt_","[BJF" -"z3=n>\EO","OZb","J","$.2;_\bQ>y","x^","0`=^B" -"Sd"".dF9B","F]-n#^","","I?q","L","""8L\S+M^" -"BSzoV0)(z""","s","=9,B9","#q","m9","|Zm37ek8" -"/W","",",u'1m>`/P&","",":&ye?\Dt","ZT!u%\hNtn" -"{cx<^U","HDbQ'cO:l",";Lox~w1=Oh","D.qpb4","","9NY@n5" -"~nJ",".yL^O","7","""3` a~q_","_7" -"[","d","Z)Dm","HaW2R@v","}Mr",">}" -"","}I td7R","MVe=@9]*R","?","","3gNpp-" -"au7","$BMx&<","'&YZsIrF'","q]h","A[<","cE_J7" -"$y",".7M,B","u/","","=#'jtlT"," &Sb" -"m","&M?31<.dn","_","^",",@q#>","njFq7VV" -"","1","U=$FJz!","~0L\d","4{L55IaX=","j&JJ9(/:W" -"","pVBa&~3","\:9f3>e; ","B","F>","m5M_.XPAva" -")|unsjx%C2","}O$5D" -"n&{kK#$r","1)","","1""6Q2.","","Z-}'R" -"s","nUN@0pj;8","-B0g%FRB","Ku","s)_Y3@4\X?","eQC~" -"It2V* M","k]~,Bu1mc","HliCCg","4a9+$wt%oW","XOz`0eo!/h","HrME" -"3c6wL:","b@O+4k d}","(","{%0d^I;K0","VlCgF1x","U>7NUa" -"UT`t*>","}f5m@""","W:","YYEb*!KFns","-~,<,","pFwko 8[t" -"H/89c'","","Jn[V","Ae&3`("," 6[!","v" -"F5:%YAMMAW","0","KjMRKB","Z","",",e;" -"~K%o^dDpB0","\Q","_FD","b8WQm/=","","MZl3\LW,=" -":D*;","|IgY`",":*D[NGr=A","X~]oe14","","=~,0NX+6" -"5K=#|,uX","9LNs","[FG","cp\&D0&fk*","+&",">#M]T" -"='oA/","","xq!Q;8","qzNL","`duzWU","j" -";;~/.","U:'v||","","","l1a.2","0LW2P" -"Y","n`@!","J>H!2F","rI3>kN|Ac(","33L","z-R:-iQ80" -"y$k\","6wNF'D","6aW[:j<"":","OQ(e","U7[!L","ES>0]~r" -"p!=","0~X2^}&","{OGGdTA)~p","6 gY-","<*b4!cuh9","2" -"%z^e`M/.r?%I","K","^>$","3x$X","!Kz:0O." -"uK40ZtQyY"," t-lzS","","eF","d,`Z^1.$","Ln" -"","I-a&%.W,","wAo","nuS[|p"">#[","","[x#Bz" -"t[FV}8i","","V~7Me","9c+H`ALto","`H*$=Bgo","" -"}wOU\;>9qF","F#N|;","VN775""G","kH5","&-sO1/N ","^" -"T09","mLW8MMJb-)","NF$i^dy*","","^"",dv""|{/&",")JtR,(" -"5ab`[","),@{]kRm","HfaEwLe[","p","[U*M@J5","x" -"X13=T'&","!p:vnLZ+","Q)6p)ga","eD4IJE($ L","Z6o","" -"PX","_v>`Lg","","xB0^|]","YJ_-|","7koAWFYFO" -"t1@Bm","}mZ*Q}S>)Z","","6(r5","oOFDYO7WZ","nG%b\^aOR" -"'vCU""Y","|FRSC%5p",")c&,","tR2","MbJ_J9~fVf","R" -"(]ZMr}","4O<-EVWg(","","@r","","_Jv-!;$]+" -"","TMmwpgqiVt","{&ao%<","","q#ho6X9b","" -"b","","R|5W1q&","4\WP0{","5<&aQi{R","]+gM&m$o" -"IO","bh4'5O)SXr","""eL","^j8?|OO","zuM""",":Fsb" -"lg","{","x=" -"G=|","","s-b$#I",">\+","'9T\W->Q","Ef@" -"*d%;","=X[v[H>@","!","xa","/\","z" -"","C'Wq$]s","7bNW|5","~8wK","G""IS]HuAKf","bR&0" -"","v4j","u)MP)mUS","M","2%+[ ","1~" -"e","n(""$N-YDo","@~T","}q;*Y","EDJ$","P\N" -"!87kz_!,","rCJ3i","Y","9","gEl","}" -"bA/'","*p%yx","MbR^<","rCz%q*fi","~HH*","40AmOg" -"MOf4thzm5","E&'","TdTyX9","y","K9iC{bnV(","n}#=Ws" -"hO",">IZ3q}ziS","Ld+hkl)B'Z","psm)rYB@TC","OgqG`N","" -"lw9s","A,`~","'A?>fz","","]}?y%As","lJDuGI/" -"EdM""t","RZT}bLx{2","^&ek`[L","^]Drbd","wD}","t!\zOD?J%" -"","""%"," Hpo'@nY;P","1$/","nuHW","I9" -"",":\","B}9lv>Nb","{Gmx(L","/ug-a3Y","w*h Dl].A7" -"^l(w","EoO8^","xosm>5","<,kU0b","","?1`j" -"q&}/sKL0","tI9","+_`=","p","i\C^w]r\;" -")gmS)S"",D","KClkeXt","*S5pM6q","U}L`lkK6d>","m*Y(]O","t|&r)0T|" -"_l","KlpL","X","z6ca}f%@","_-2tQru",") " -"1V,L","%RQ&.tF)ku","`pG_qCul=i","GS_zVA","Zms~)/w","k","%","svQgA)","#","P998]Q>","5+#2az)" -"uV24q","Oz<.w_" -"v' q({d;","+njk}","nn|aa","4B","bHSZ%5m@Q6","V)Qs.Ucb" -"I[","U","r{=z""_PD","*Be7-2^68","}Dv_n<3ouV","" -"y","","K","","M5) |C4M",">1*YIeg4" -"{!h^{;","kFUNy","U.i#Hc","e/","","a<~" -" aB!","f&B%6&",",Lz]nPWGe","*;B9Wi[","","4" -"DKNdP@","3eqY","j","k&1","Toz}","""YO" -"$)'",",U","","M41)6>Q","[v^6_UP","P1y;#<" -"j]ItKmw4","uY*[2.","c,,","t;","PH.Dn?1w","yI(" -"#F}58/Qb","(`_%bj","7d,yx","73g n9b","tIwI~","57" -"`FZD);fk","km","a$cagNV8*~","u#>`","L28cl?","K}E85Y" -"k<~'Z9(nL","","N;5@","4Yx+_@u_zj","5N[","hx&'" -"vYr/45)","-{s(","vjh","^%A3!$mp","V+","GxT6>><" -"/w","z","3:8rKE\I","5!","","ZL~8F+ZE[" -"e!?jMw#.aw","|=e;*I~G~","}KA","6pa","","dK164]/w" -"5f","+|H+W","|@hr","A6%|R/","","HL&q)-" -"$(N8-Y","x:.!9VP(","qUp2@","5g_-W)rW-","","wb[w'qr$a" -"Cbuc","1GH6.7kR","bJ0\*","Ah6XW(o","J}S[8A","EJf0TLCW" -"7(m+/Z","v"," yvy}V3","LWU]",":","" -"rmS}]N","=~p-+]93~f","s","YAT","b","HiX&I" -"","A:d","jZ$","Vex","","iKUth@&" -"","rlSrlQ","","bz0q","frx""8","En" -"J",".@W@","~XP","s","WeKNE^rr","" -"XREU+4`joB","g[P~9[K0","E2`RqB=K","""][_W""~jq$","Rh;D","wO!!e(" -"F]v/",";+4(ovGD","eTB*rFq{","$&J`z%>","OVf`g3<","XGgeuV^j4" -"Ch(W","U{W{","NJWI}kC","<","-pKXGl","6LCYo" -"sz].tI","=;-yVdk5","","UNdl""[u","R*|hnn(|","_""&N>3G","Ajhy_^" -"\Spt","Y@a`U^","\5Me","oVqIa?dMB","H5]}","" -"Vci","%'\","$Ok[9VBm","tM]6OR)FH4",">g|J[bA_&","c" -"","s","_'{j)","%i1p~@f3n_" -"Rtl&xxi'&","2","dv8h`"," goL ","g>s3v","B]S4&\})" -"d;}","Ve3{v","#gp&D.2xn ","CQ","^d:+_btUp^","&)3D1T|rLr" -"-}~!%","|>oZ=O<0h","*%<>j52","","","H4(qHjctO" -")I","D[`XbyE[1h","$ZzJP","Uo","F,wfG0?e8","`.i|SdFdO" -"E/","./""UG+OR","pELt(S<,","ES5y[","h0966","\" -"I?","BQP;7~L","&",">`!#:Nz","","B }zNS" -"2-W/","5(y0[[kyO","","vKan^ge-d","97iHn@IS3{","BL=" -"*q,6","y(G,","yp^NFwy=Bfx]@" -"","frl:","|3","|d","^Lm}K","lUEw8>f$?F" -"","Kc","sI","4I8","tb6",":D" -"0\=7rQa","s","o64b>5o_T1","@xqS>","D|GyACR",")eXD**j$yB""%S","|~/",",@","","gLq@]R" -"4>W","$Wm","","b","rrl","/" -"!zp","T^C?","WH$@","S","3,pD7O3$","NWz$ka[3""5" -"`7,:U","""RS","0|&`2.#rX;","n+","Ku{8`j","uS&_" -"M,#-","","+V;ef{#}bN",":/=Z,a6uI","l_de#$","1YdPz,xkeW" -",M","$""\ 6A:T","","m,|","x","xVJ" -"3:haJG4[v","F","jH_gKcB","z[:V4[g#Y","W","n(" -"a#K","","","-mgzOKJwv2","4- #5<`yKr","Ol[UndAVZa" -"~","[/+AmvQdEA","f<#","R[/n","b&~,6]o+7:","|E,A[7ex" -"["," xV","jy&`U@a`h'","F=5T@fzb`4","v_W>c7wpl","T7%r'|S2]a" -"~{k","ZgX0mS","aV",">/Te]<'xsI"," -oeH","*C.WxeO9" -"l+n2CWN5S","F89[Yc"," ","","","^|d" -"PI WJ*","i","5c_Iq|","t","p2%xeR"";","_C5 ^." -")arZly","B[m","e,os","P`~NLO&","U{ 9H(>","po-^$" -"0VZ&=q@)","s@p:","\","","I}}(1!qb","b74r2Q1p" -"\\4@H>*S","RBDR","]/?","",".tUDA6-]!-","o(""AXMcBZ" -"lt","O""Ty&`A","-:","UjXg Q","q3VAM!G","`" -"D3_|\ShUhS","Ak5/1Fti2","t#5ns","i","ijg]2k"," @x>^zh+%" -"Bt3,Em'&8.","=p","I3\y-%d","m!l2>\","*%""""`_9h","&8" -"|dzx0","","kxY o","","w}a1vo","WP9ke1" -"i0{'h0MW:M","Y","H","6tE[;!,@","","`:,U" -"","O!A8","[Y,","lvl","=_&a7yd=","@" -"?[;V|(aeP","D(J","g\J#","x.~3v""CV","Du:$","NJ" -"s/&hsm","\S))$","","0hiCsodx2","%2c[K^^","H%d""~t[>~" -"DuW_&x-h","hjf","zXCvV","1m!C#","?I","K" -"J:}","u>","l|t%FS","u }J9k[eu5","7#zXM~B;In","{|" -"\>'D26OR","wv{w$2- ","vw-E","Y","""P","f[X" -"dykP","K5vi","C)}","KoV@}y(","jFk'","{#LhXAk/" -"(Pu%j","#","sQ\)^d8","w","aE&","+1K" -"Q@{[S","IBGW>hVcD+","(^-8l","A'u>|09E?",";#4=","e,4/" -"XJ^0%Ud2(p",":L>~","{-:b'B","5*'0IQ2(+","7l","0" -"EsRjSf,-4C",":*XhL@f_:","","7","AjK-d","O" -"vT]S","!*<5Qp","S/.sk`?*","+!" -"Y)?i8{a","uG@Q%$M","W`\=WVL'vH","qa""1\A.Orl","|@Cg?/Vk","W+ KG6[!^" -"d=[QB","Q",";9Hq?$9e(F","5#Z`A@zpCS","_3tO,&","]qd" -"j3U","Nu8","&}za97","H]AA","}x1q","'!" -"@d","","","Vf","O^GD_","q" -"p3","tELnilg#","tk","UeBQCQuMa","oV`a4(","V_RI(>P4Y" -"nEw_Tt","","6Kf","ZC>OE5","DV&rdHVK0,","?do" -"n>#i.Hv","8=","U4E)DJ:","&:*huDRSz]","4Z7[\H-94","xaPCz{H" -"g'w>","vF9Gg?m","oO","""(BV,z*","@a!cc/9\}","[9" -"k{=","DYrnMC","eu%I+_*","/DW_Y\y[yx","t","Cla;}TJK;",";IY4" -"~) &","","Cb+g>!0X","f8Fo2{","w_#","" -"","",">","wWV?>zBh^.","vp","f2",":Km}af$","?{_Y&" -"YBNV","JA!to4|k","^","3-Yk6#4","L)","oi=]" -"J7mlZo\_N","M","&[&^p5/0m",";6U-3S","rR\c]]v","F}C>:L" -".DDrM","6f7!","m{Mx{G","~K","BEhB9","ps" -"}4P,8","","fj4G","0$p{E","/YjE*","nQPn" -"^lY","H)T@","",")XH'","@q%j^(;nC","J_GEG>w'","-pDDH6SQq","g,","N0V4","","" -"I5)","hb","-wc_c~i#\C","wFGA","2rvz","((fntfviO-","(","]l" -"AQ;M%>4","G2-RVh","mwp4p","(3tGM1`\","iSHO?&3","Is" -"rQ","q&n7u4)<<","""%j)^|aJ","~h'(3N","","_=" -"dE7,}.","","qbVH","""RYl8V)]","kbX},","'buNq2" -"n0O+P8eg","aL(W@)w1""H","SF_UkkU'T","OK-QAJ'","Ngz/[","xH+" -"!E :/S@v","Joi~G'Bd;" -"","A8NEc9'","1\(","1GvT@{%","^!#3h""0ra",">Ot6}" -"%$@sxb!n0","mA;B$T'R","RXx" -"W;*?c!L","`2\_z/&","P","A.9t4","W]o.","_r&K" -" -TnzMUJ0.","wKi1","","khI(o","rJGy","oi6k`J" -"sW1d3","|zzD",",7","Qx\","2","" -"d",":QhAS^","ELT$WX","5X|;9t|S8","f_Gw.dGJ","P&@t0e" -"dPFzD-PlTTS2","CL?|U;HSgl" -"]EJ_R1H{eP","-C]|l>q","P","3mR|(","00N{-{4V","I," -"nN ","R&:","czgSf","8","~([","yD}z?T" -"/='",">{^}" -"{.b1 /{Ta","?z5V*l&EFa","L07|;`a,p[","qXG-j.+i","{IC!^EZ","" -"xdMO",">",",5=","L",")y3> l","[#E:xdS" -"4kWy(","q,}\","_1i2!=","v^1*;@u{""","V","vjG/rZ}M" -"1:ogQAJ","","%(","m","0WL&?V","$~" -"^$\","a","fls'o","_",""," %Gxy-TQ" -"r#&H","q","j{9I0i(\D","1gfjbCLR.5","/Wx","#""hc1XM" -"~:&Nv1)K1","a}Rq","3SJ$Q.C62t","Dd{RyK7Q","LVK?*8%(BX","opH","EiM/N@&K.N","G!`[j`","","_tB[Q\","^cCECFz" -"j[]hAoY:g","e06Ox","/][{AV9\k","WAz{",".","Xoa" -"906","cY\VzA","R_D@","Wy","!<8tMxLv$r","UL" -"","z_y","",":}","N|gi","Y\Yb$T" -"rSK).^[K","~(8Y^W%d","pk$O{t","m#|;","","/imFpGE","%Evj" -",PU","TI" -"\Y*CkdMO","u,ZT<$",">h""","V4T{ik((W","b|""TE[",":QN4b5" -"=TE|[IE","g","o@","'#Iwh@|","*iI$","6ZG6es" -"w","",":X'tPQ$U-k","Y&>vD","3","M^33" -"6?@32:u","kzfVjL(x3","E0","de.|Z@gXM","5H","Bs&" -"A$Mx>ml^n","@F7%;","7]cs0/\yk","1,","'|E4Aa:IE","so4kQ|pAT" -"\v>;T'I}+""","","m","n","7!X=o*,[s","b.^X" -"7?}8","","]PrI.","E","HfD`l","+GfQ" -"]0Nq","R[cI >S","JlCzjV",".","}>.Q[pu48%","X'PkPf" -"k>)s$M(b"," &UOTtZ)>","OWS","]","&e8","fMqlS" -".>*6|","1_" -"FNzKCZ","Q6|jGO`","h2\d'BvR","CeW","","2>YZhc#P" -"2","DQ""=7w","]L5&","","~","^Z4|}1Al2." -"T+}Anz","vYY|Y@fw","~5x8","P","2l-","3$.S$SE" -"","~y/","glOL(%XJ56kk8","""wQm6x.D;:o","c^O","HdHyB!@74A","=Xxb;K""","o]eg ,c};" -"z*","jd(Y2~" -"J","|=o.))kn","^+?l8-H","Yve*vf88.","ss.+C~&~U","#wrXq'~p" -"V4} hj6#@x","B*oW","@j","=/WqePy,-","6\T)","5*Ta'" -"gRvpk|7","VOuT","Z","","}/KUa@5","M74<.kAe" -"e:B`o %M~","aNIe{)`|\","deZd","U(An","X8Rcy","V0w%xY}V8" -"^yZ49{:","4K","PeXY/<)$","j[F+pB9","^","&'%xo" -"c/8b","n&@_xw>","I/TN","],r:","@t:ze?~","A!qYD-;" -"QU","!}eU7h","5Y","=7","","" -"O$oG","bBZ","","uN *Hk","3","X)" -"","YC/\APy`","1Cu6?!bNj,","O=2 R !,_#","yvQNQ","hpv2G" -"Iw<","tt?V6b","5?d>","&)b0q%h~7",",r}kR}y","" -"]@\m","p","==IYuOq","<2fMt'hNoc","mu!Idz@","c)0^KEAcd#" -"","q}@35VuN","Ud""]1y=m0e","r7-iV",")gWB=}","" -"e=GzB","R3Hr*o","'.)0/","","nVZ","tU'6{{/" -"1+*kkNF2","TfA","60Q'T~X+[","W)?Tl"," Vr""'/s;^9","9#" -")9qv9H","","hZ XU':S#","x","Zy","nvB+" -"\","xFZ+3X","L-?","aFXd_R[v","fuL@NE;","$p" -"w/`|^e","w)","","N W","fE2q:~k","" -"HD7qI]R:","&coY""","(71","",";F","U" -"``w","E","Zyc],","[gG@,9niw","d8eB>5,t]","3k*gC\Q&09" -"<.2","","""^aQI[",".","Z","+L%=""" -">p","hvAdZ$@2z","9EzRQ",".m _*S=l{","x","" -"cOJ;l*v","f","G_Y.oUz","j","","V" -"~SLIP/6=D)","%d&6/P","d""P","@d","","sOK26@w<" -"*_9Wrj/FGJ",">MBsQ~",">-?Sqt'yQ5","-Z","r8W@\B~C","","Y6tN'VMjw","l&nB" -"scult","","%^T@)","3-<","u:x","cltd" -"","KTFNw","+A","2w","Z&*' e(2e","?" -"+ <%K\","9d0g^,'+","S'-xdB","R0sGmEy11",">","?Y98agC" -"j#|{} ",")","&","HAh""!zA","-cWn#a","Dq" -"","rt>b","R",">~6di",")o{dJ3","zNS" -"mn.%U""xwI","=0&1nde[e","l-g$)oy","8Bu`Bm","KrA!Qtx","eB$3" -")o5","@UC9yU","EqtWT","2Ta!{","v@Pdj+[S&R","5}c)\dzy" -"Etza9|","7ASvQy*;","5Tkm","'}mnxT" -"kY[uj!","",";n>Wxi'}","|9SA","5Q","6emu/~" -">h%i""X","t1b","bz yE","}vkn","@p","""Jht" -"","fGt:A","B","*","\]E\yEe3","k" -"'|?oF:4<-X","gLKCefb""+[","","\lk l","","p" -"g!3u""7","t)RVmT","Iu","?sQ]","`,sYZvf=","g&cCz" -">""{P}W%0","*%h""","'#KZdi7}","-","%","" -"7$fn}5","vHyS,L*-X",";","v[","0cWihVf}/L","g5E;A*j3" -"3uk","8""tb*:","_9KK!","Df^{+hB(","5Z,","G7o5=(H" -":","-e","IJ""UVGy8>",",'a","gBsja]ur:","ciZ" -"@%/d2","GHJQ8","!QQjaYXM:","3[GqC","""QUP/_+j[","d" -"N<""mV+wF","","aviq","cp.7",",","N,|" -"","ZzN*X","4OYi&ci%6A","lX1XP","X/JVo[,","v|H`" -"e2(HK","","6M","D","VN:f;|$u|P","kD+)8rF-Yp" -"$X_]0;,","Q|v","=nYd","BSiPgqC","","8x" -"FuJ[_","Cz","#IdY","u`C.or=9*","=@-2'","e" -"?","GdS?h|\Cjt","$/uH","g0","","F" -"G","@_N[`?29","`l?@@yV\","bn_N","L4","i" -"rcqK(","!Cak\x~Y","}","Yv!CK@","=@c(b=","J;" -"b","*gXQ~","","M=","L{^","YKK,7$L" -"g","X{6@&mP","I?#O,m,","mn!O","i2X_,a,j","_02tx?.v" -" a($K0=wsH","|a.lr ","I,`S[","8""C","&","$YZ~,it)31" -"72""RWvO\\","","&]AQ+s_lMX","6u","j6}$+$U)","" -"s7E>gPd@",";","Nvm%IV9ZT1","3h2eD2yv","Fn5","|UmuEQWXs" -"T/]$,>Te","kOzl3/U8]M","Qwfc\3=!,<","X&.","G","Jv+mpb" -"7","j:C1!).","(]~Mlt,g","#2JA:~w","{","5Toqu||t" -"*.@wA&z}18","`Sp2w=A","f{%9|%*!?","qq$Eh:","BkSFS5L","6" -"i.<7#vE&",">Z}n#w~","yO","QR6)0=S","T","T\D6","" -"+","RX6V,*%f1}","v~lcqf5,}j","k+B7+CRF","","_ci" -"D","I3mFk#","'","V;6f","/D",":s(:" -"cKi/Bi-P","U","1p","4sc/*Lu>s<","@!zM2","ZQM-3" -"KFq+*40b&^","hc&D%j","y^?('~/815","","v%BmJ_","s","P2Jg12LT-{" -"*,","1:=C+b>",">~","G>>f'*N|g"," 0P&4","ghtQ1" -"Y","Qu*vMl!r","@)G_$9eY",">C.a. })K'","93jw%MX","1j&kjXN((" -"d:","q","$nuLrp","Ex(%|-'(E","9bPt","lsg" -"R[Lq$QbQ","","p~","=&B","bf[r","fcPUZ" -"&/!AP'of2[","","gK%S`qxEi","a""HQ#tREJm","V9?",")" -"}jpsIFQFV","6,hLe","9QZK","$(Q}","swAH[!","nEgdsX9Uo" -"DI","33","fKG}8","","^c1","9-" -"N?m8!GSJ9","Y?@@17=F","4","Xwdk-ch","!;{","3BI" -"9lfj","tj#%@Y]t","<","#L","mPk ","2EvG" -"Zsg6}5","3\","W$[$D~|h2","03C*3Y]um","/"," eg.*" -"8wN_e\~Ee",";{ad","Fe'~6ZKS_,","y)C`lnmZ\",",fF?","oW\X-" -"j","o@Y","@","LP>%>=PS","!'b-{_Im7B","{o#3d5x0" -"6H!a0,{ I5","J[|Wj""","*7%\As","l:{}$@","L^!jub>U","1]nw]" -"70(Tco","\pHZ^q6w","","FXwjG","BB{`I","|" -"56%Ia0sCe","6mXvd;","I(hP`p]","/2$","|v@`X","AwZ" -"2jyg&","l;pE5r","\#;v","","","]_4," -"c%v3a&","Svyd","I&\?","o,/Dj1v3","BOjBe]"," Yfi6" -"hb*","*jC3bV","9>>JhZ""zz","# M \","w""^\r","9~.pW 7|e" -"d","I5e]H+",",Mt/6eW","r0o1v","( "," m_\""A" -",,^;%{d",">>","oN","flXdqu'2","n>UK","^1MlQ{vm" -"#a=;De","FOpy)G8R+","|*|W6H-","@?mTf{8?","e2g[E`","4qje|","z","2OwQ)","dp" -"61&<%Ig)",")","1","tu#","","AH&TN" -"y&","c)LM??\","e.]N","yv`opl",":",".wT" -"Fc^2x7","8pA","rudMziKX","rlMu@X{{","~6.vH&~-",";*2~U\P" -"[hQED6","eC?kD?;,~p","Z'c","d[",";.","0(L/" -"'l.u{","Q~S","kuXmX","yi10T]E","M p","jMTMC\uV" -"/","NiN","I.)7*","Er ","","" -"","VhWg\","tl","t6{H+v)cQ4","a","6" -"""W `e<3-=","yOgYfMU","J;8","rL16\vp3a","&gwR(","/V&" -"e\","B=o]=","!CxGh8@","V","uY","^l",";FPO;C!","Y" -"","_Z@S+(f=","O_j|tRLA&}",">&zf","J!St~q","+hdhl^" -"(\CK9","M{<:vy)Ku","n$W5""O]",")a60OGrN","{}FJzjxl","LF|\" -":c","G'","S","D-Rb[_zG","MH~SL|)t","""#[O" -"Yv(","1oFv%eYQ","`#p?q""QuU","cwAXB","aZHY","wyg" -"b}j\+","}oiBQ?]","\","","","HEexFaI7" -"zyP0+pa","Fc"".ZR.ZJ$","3Ahbe`q","Bsg>}W'","8EmimWCt6b","Ei]j+" -"g~){ z","","#'S'","{+S","\FRO:M","E" -"5(""","","SJibvzbm!^","`FO","HS3","B" -"sPI 4*","Y9BInacCg","1!","{z","!'P","(P$oiJ" -"+,","r","*","&P q6","q@4B7!Qvk4","t>`]TaEeF" -"GL?E ]a","INJ>#$","Q_d>WN`a,A","VKIHGE\""^e","N-Vo53h?","AJH<" -"|\ RkL","3H","",".KwI","WD","8LR)4V#ed[" -"/a","R3","8rwy","G(x|;~","b^","[" -":k}>`!R0h",">oj3UxK@7A","9'usR.Tgt","jUMUJW.l","UMEa@","i~:" -"N?!^","3dYd4","_?px""{)",")ovLc`{","gQP$g,h?Kz","7Ekg#|%f" -"8","=#J^-","q)SN","D_*S""U","\=nID:","z" -"3}N(`","*=k","9;\oILK]6r","/+7^-",";B=$","@!lXYjlZF" -"r=Ml5","?.8zt","","eOG.=\","","V113" -"g)IfEngk/lf2","{" -"-[f^","8a","f[Hs:'f$[o","@Z_vg|/","o?j","f~f" -"zv","","q^^1X",">3Wa;" -"m$ \x","a2","9t","G","Z?","f60{W.3" -"+""k/=","2","s]Vp[,PAK1","_>xP","td51 'w","@Rs""fMt\" -"","","x@zdtwY#T]","Ngz{>Gu\","sIX?","Qrc?[}F" -"]z4a+","QTH","J0t\I]m","9","Q!R","_2>cVmG1" -"Pe4}A","t::r+U80k","%n|Gp_^qb","pE$Ks6l!~&","tE","Lt7-Wi" -"DV%W{4_f","%`gF!&x]p_","N}\ 0B }>","dMm)ZkwFq","S)","@a4kn-+" -"'#a","W:}G","zsa:>6u^x~","","]D","Ll`v" -">~jFgP|=c","bVK","eu-F23@",")]!^4","&?TJJl","4a*qB`S" -"c_","v3qX","IEx;E","I,po.>","hG{7}'","Pp&l!`"," nJdM","_:-","%1e'1xr""","W~^","eZ]NcSX\","b25?" -"}^C=ANXiDP","[Kh,&#W8","S'EB7zQH2 ","(_R6","{zI4BO","" -"","X","viLPqKpUq@","","%EMD=kZsl6","y%L8Fks4pT" -"+s","/.","","VAF~l","mgie9Mf;;,",")hH" -"D[]y","{|","Pkw[4I8{8","K53;yZ","/","inMJ,n&#R`" -"yQ9""","`m,8QU""N%v","&c}$x}c","o$;z\S","{>fka","@v06}6~" -"","]kBBjz&X$",",F j{Mnw","9i1*8","ebiSz{/z","|?0o""1W" -"uyP8","<'.i[m","QT:k0","","q$hlrSC","" -"+H@1dlA",">qkRH9rZw","@","h1f","hx+T","QVw=Q?bLDW" -";'Gf<","p?","sbd.""","q>]!T4i:","FGcX!-b","JdVe~6^" -"b+^d0","t6ND","5/T'L7","#&","/CIT","d4M" -"O","D8;iE]N","t51ZNd;l","f+~k;d>@Lo","|qf@sFq-f/","0,!63oA" -"eFwd;a","DBRf","BQcFIUm","MoJU'","O*Mn","rcd gq" -"QV/v=+_4N","o7rj?<","LFK/ !j1","*2<0h","4\P0q" -":rG_2""BjJp","4Skm;Lu","6jzpq","","H","[wK(v?Ncd{" -"!","","ElMY}i","aznu&1","0t#P|I","p4Fj" -"R&-","bU>xS(1`K7","Cm\[dJBHc","^hBulws #p","lI","+p&o74x3" -"/","oX9[Ap$","@","B>tob","b]7mzF+|","6." -"","Y6-<`e","EVd%.W,v'a","F ","CnNAUInAG","o(znE" -"","hiS=(#V","c""[(mdOR}","qY",":Y","n%$OS(huUG" -">2V" -"@iv","V2rq[","P^P","Q=TSzT|.","+ztzwmvN","B4JL4" -"Zh,","Ga(j=#RT","BzC)%f\>","AtSH@","s""&\78bHdJ","z=[" -"{H""ij1","lR\H17|p(D","IwEKh|QvD","vUsY","","Y" -":V_J>1^","!{AW","UEFA_","J1","0","D0" -":no","4WMuG","D4xbJR9","+~.sQ","nR@?","6&Al3b" -"S+u","","LY!66lV.","]&d","`hJZ`2" -"lG","J>>[rKNpR2S""","-'R(","1tQ <&<","z+ZzxTY" -"toN<\'0x\n","k7V","Tr""#l,sy","]f,ENi;","vWKY","" -"g((n","","2,(","","Q\8;+7#Mq","1{b)v" -"I4+","","[( se",".D3%QouV;%","4>W!A",">Uw`E,xJv" -"DwR4m?",")L,u5""","6G$3%$HiG","'|yFywU","DjT","[m1_YNjJ" -"1&U@ow!A","x9TI)M6\J","!x&","Jv)?#z/0","mHkE43(#M]","j>'Y" -"l=","BG!","\L'h+PQO","&[6N7>","","M" -"_;KP -Q","D~)z4A4o_w","kl&","/","B^sY",":" -"[',|w)","Jd","/{r0","""|cJH76/[",">Qja^O;","jfX" -"@;k2`;H","3N_(D}E[","G,3]","=","<","eev~>" -"e","~","q]$Ho~","GfXA$V.ukt","'TA{","/KZt)ess" -"0,mZ.}<","","!t>u={Wi""C","#""[!{",".Z{HZ",">rTnP[{" -"D];g8""z&H~","/.#_","1@L1","","R6L8W'","pcFXOGm'/" -".$%vc{","~yxd~G4*1","y","D","kBgBv""?A]'","" -"bF3""!4%O","p:,ZPA*",">'pL<~","m.LuDB","Oy{,`6M~Ng","W=L=H|ZNH<" -"qWQ3~@4=U","","URuEI)v~Ly","&RV","G.ZZ_XA>o5","-bp" -"$]LZP","C","[!/n","","`?[MQ","l{%B?]" -"","ef8M","QYq","@]mUz~","S![","o>x" -"bBr0fIQ","$`!c","^d1Y-I<","h>ywp""","&%","%" -"W4+f.","mNTCn","S*2qWW","7S","?8kjD84@CO",")T:0" -"HCS&TN4=}","-.cTz8c0!",";c4WW'","","i#{X^-?$m","m?D}JJR" -"^3CAzaBv","W","g|IV=`",",_I","%+%q;b","u ?" -" !YW`Smd","","O?","","{WeO:Dl","G.@0om2D" -"ON_" -"xb!,k<","]@>DX~CAv","u'H>z}XrJ","JEyy6JpO)","*RF[%6q9B_","eAOZ*>+Z" -"Cx_-o","q>J2`u","!Gc>AC","GR","b<+J';BWy*","P" -">Uy:'(0)it","o#]oCMH","$dqJKQ)JjW","B@}iKE","B","kL4]*s5" -"w_k""QA<","N,g$eqH","bJ","Ua\2g","p*","wt-s+p" -"t","O@#1WP!","d-Pq","8%=Vf","ik","F_}E1&a]","nBfw=U" -"^_$j!w","","cYkkG","s|wS3Q","x&Dks&","2A" -"?GmVudUd9","N}.+2","%nRDx-+","|Ag7","aQ<*W1d","!`}k}Vk" -"\0_$9{N","WBV^Mr","x-","","+",")nXV$YDH" -" >/H=BN","TH6{q<*","I5$p,H","Hs0kRJ4","Yf<%+","$`w0J" -",C3q{2h""X","?PH76*>\p0","v=N"," Dc;=f\52'4m","cbN--%" -"u5l\","k[ 6=","xh""qA]J","VX7","E2*0tg","~" -";Vvb($%L)","lI 2D","m]kY~+XT/9","m)5","","AKLMgK.m," -"vBMCW","V[","n*P,af","(>","~XBuxh}","j" -"XT","F}UEA","j#~BK","g7C","@l","d/OcT-+M;" -"P|pB_n~}0","L","O","zY]",";""-B","e>.x" -"$hg@FzVuec","H;,Kv","Hw.XKk2OrZ",":+3z8$!","V*4:FkV*","p}hiB9.&[R" -"","Q!#!6P/","Sb`<","|x}]o","TxB%},jrj","fwGncH" -"By3(O:,lZ","@-","$8=""I5FF","s(","|_QfdY?`}","\saa/eFMT" -"UR2`U","I",";VL<+H","5VK",".V-q6"":","CIh""E!," -"fYfm)!","","dY-|jg`Fl""","","Doq","+" -"3_A","lNi\CV","*#0+*TBm2=Tl} +","9=SI^m" -"EiRk6oQBQo","qj?tVv$a","iX","uftdmnN","v.6","PPn*4.EK{" -")f5S","{","B-oa-:?","w3=!AN;?-@","","N<" -"","""l","VlKy","IZ+ ,5N","9'o.a%H","*.'u1""jC" -"0MT3Rz_","()kd","","GF","Cj""ntU8","$UL~xYW1" -"WkwEEC","","""ASSq","","D^[Do","AoSoUnQt","^rn{c]l*","y|2uS","78" -"P0h}^8tl","@0aZgTN","@fl<","Wm*","9pYy(;","@Lc5s$`1gD" -"iQa45[.c{X","0l3nBo_|Vr","TaYJ","z+V","K","l]C[oEaGbF" -"","mvQrj'dC1o","j",":r\6{o~PQ|","","?W=" -"86fo[","{)[z",".O@","","`fw?D8hUAep","47TW#).","+q|-,|","h3uP" -"^E39i~","M$\I5],!N",".eD:4","@5K","~","KTe3l" -"OHk","/n'*rblt)m","[0.'bT","=zjy%t_Nap","kd","J:%)aC" -"rqU}85x>","It^Y=]","Ul-x4b;!kK",":WufT)[97","&Z F&v[TZf","!FT`8Pv,T" -"","wZ]","z2{k)",";**k3F,7(",":_T0~S""",";V&&;34%R" -"C{d5lt~","u>Z","","\","mB0N.","tIW`k=u U" -";biUHorBT","45k1i<-eD","{VrMw","!qM","Vhr!","Y78_Y" -"7Y9{GsBEw>","q=V","9M[EiZfNy","C","s}Wwyq:{h","lg" -"1Fi]","tx;Xhip","{sR","1Jq","","8CBr?<-" -"v.X","b","","","^Lry9",";K@X" -"0\E`e6ID~\","[","7","cv%M","nAQf{W?q","mm>v),TM8" -"","{1_5hB(DQG","?7$ Xm","z6VTG}]*Gr","vzy","G(6bCkD!g2" -". 6tY","","P2","+4&)2sJRD","G##","&{" -"]*TBo","c","L","g1+J:V","","@WZXyy*3e" -"__c*","T{U%VJ8K","p2!i;r","","","Te7u`K" -"8g","rhp","<03V\k1@",".^UAWt7","vva\CqT'C{",">rdq" -"HvGi","k!P-`%m3","Eg'x","2","xp-W9O]h8","$zKf0Ak(" -"RR-rW","sN2","y|$4)b","-b x7+HDJ_","9QTQ&0u","VM" -"jR@8o+.!eH","$gZfMB>$8A","","*C@:\cM","l/{@}^F?v j.r:","h","""","Zso+" -"jczC*@6S","uXYJLEF","*bS*a+wf",")_-B","*}EY-8","-~g/8" -"$",",k","","[0","$t","[g}\kb&" -"","","Kk%qFAaA{q","B}fQ6p$`","p37","n;!jik" -"w 788?Xp","4KHhNNG{","k/4ndZ='!c","BHG5n\\c","'&vs@V3-" -"","}Qwvm^td@",";o;?Jc"," fFSsL","O$${:;q","`8M[O" -"yHsHt_Z","N%aO$","(SsgbVS","5pP>L","Z<_)%j>*}","xSP" -"hQJ(nz-","pvEO" -"h""H4i","Vx#.4eFSP","P%b","g29/ohJ""~","1""W>c>7,Ff" -";JP;lF=","","sk","2pgI","","" -"PNJCZk`B$b","o_rES_{G'","","c; 5f8X@]\","5501hCLwp{","k-|~QTz" -"5|-'3H","(MBjQ","^L!]ELN!","FtK#er:,w","n~d6_w10","~Pv" -"{^h|}DK90\","\S0dCXF@","~Q6yqV4","0~^eD","p.(?f","" -"O`{32q/6>","gr@D","","[^: ~0+d","b6E","5n3dZ" -")","_F",";PXBX~JYz","","VSgfwg);F","]5(>W@ON" -"i'","_zl|Ly","c#0um=K","AYJz","BG",")rd(,k)MYv" -"7","-[eJkQ","g5DV2","6Q7>","5MWw9Am.","3" -"y 3@hOj{","O~V-",":","%ll#","wn]I;1","bE'1gAdTqv" -"M","J0zmzLj&","X-}8(d%q","&/kHn)lC!","","(" -" ^4JD`-","egb"",]Y","@u,G`b","1)|T1rnI""","M7w}J`-)","WJM6" -",UNTcL1J","M>","1","PxdP?/Lm","6(I+D)7Jur","s>?Y;s" -"\W1%TE","ky[f5PQ","j","!UpdZ","}H^m","I" -"%""","s_y;Z","""6vk8","=v07[5","3=gkHmvH",";2FXcoB=" -"Rkg`vQ","g9wm@_" -"3\|1Tn\","/0X","\a4+\vz","","y}","" -"\a5G67'""",")yr",".J*:vr1*ME","b","AwS&Jo","if~2d" -"`3O","L*f""`","9&8z","6A\","FFJ$[$!reS","I2aP f" -"x^Z>*[_ .","2vg*ZX,","U5z]f","\,BLCRZP","FZ@""","u?s7w5W@B" -"amMNm9WUb","]GXr","Sp.","7;h_i! 4","QW.","N1" -"","aj,$Um\5","Xma","=j","|KQ?tg%8","]4:Fn:}fL" -"(6(i+-Q/,j","XW>","s0y/","O9W'C1zw","fc`,n","B","jk#jL9","lM%Na<","!","fbhYVV" -"Av","7,]T$","",".8o6g","-\8","|xsPPd%e" -"$#p8>D&c4G","xd)%mt71L","""Q","JGoXI9z","J" -"Q Q","OY/ ","~!;5Dm","r zv","]yxPpvQ=1","BYY=U" -"w~vY","(m|@Q8",";8im94=.","g","7k'","@" -"Q>2=V","#","~g","(g6at" -"",">dTi\_95r","*sIaw|g*^","SK","*","rv" -"8S4-5D).3R","cnm","]","!O","nH=""+_'+","" -" &G","""P~[Z"," !Z","CyW0 dkajc","D2I#HdCMkN","~G;4" -" 8.s~6V(0y(" -"Pb","Nh5fY*vmU","F2'","<1UjC.""Og","","=" -"5u$","_ywJ5B","G.8","gS mW:7J","","`ihU-\2}" -""," D`n","Bm","P#(@2","M6^f{=}@PE","[`j3F " -"z"," ","iBrsxGF`","fT_5''>zNJ","Pgv=?DYXDG","eiU}dhlk" -"","rxD","Dq!?)oZ1rj"," g_7Y2J","","Zvq7Ca+*" -"cHDEOx/Jw","4S9!vN","#i](lCG]","El7O""$","OLeNUQ;'O6","mvbL1" -"bR8Tc",">c:!m@AT","","~6#:G'G","T'","RaV:E{6r","Le?u{tcR","XjuH1@" -"<$L{r!",";","GST~*T'a'","","}3}`FQ","I","\" -"=Mb","^EKB["," a4","K2p)_@","_9^0","y6`nJb%lI-" -"GmKxKI>B","2y >!lm","tFF","","1","d/Kyg" -":Lh=","(/i6nm|","%L5>","XiFs""","iWj_0D]>A}"," " -"-XBS+","$ooq'A","wQ% .h5","]",")>8l#m/","9" -"j3","z","","cZBb9}","@N9","Z`@%!" -"{","o\_*]L=*%v","xkK'","A",")FfU_SJW{v","VgtO" -":Ppg","LDT","S`jrrI*","UsvG-","","N(jPK]" -"=qxY","iY","tD,4u","BLm4","nXky\B=>","Y" -"","S'v!jtVN~a","","]_\!)F","o4we",":Q" -"|z#","m E~R3^","_pQd(_QXxD","&Bg?y\q_n","1z$H","k" -"IVtq2euva","CzAvYu:/~","%H~X%" -"ZE6Gx","Zf:","@e$","p","]WO4J",".7k-U0Qsia" -"h$$4}d9)*I","(LYf7","lrw,o","B}","tg3Y~MR","F" -"\SMp./?","XN","qt","M","hy","OYl" -"x'OB","n>/WnKEjRa^","Ej=$RJy" -"FOqxm 3a;","0RAOKOC:","{","pL8g7*","[{","Xq" -"-hL","DZT","qn0{rQ:Z^","","$T_yha&Q","eTI@sO>&s" -"gku;x5wO>o","t","`","ilv-urL$f","" -"8RbT7x7!U","x4[7@C*s","yw^ yT","&BA 3@O","u_gEPsg{","p" -"~~75UkG!>","gf>H","^,id","nwua[ JEL}","/\3jp+2])" -"(iD?","MYP_@bd6""","%!(I+1H","","\&cI","SnzW" -"C+C,FzV","4K`-&p`;~","pIXwz3@>8$","1.","aX+Y","HbA.OA,[Ec" -"kfp#yjnNs~",";eT|","vsjh3%p~",">RI|'&0J=","u","_" -"1\ImZd@$v.","UB","[)#0euLp`","<+/ep","&","/Y!6" -".=>;D>NT",":I_LgJ0xL","|z\_b/8s","HPe,","UIohz$hnd","* z" -"0Ukdlv/Pw","3{","","","","uXk^WOG" -"rw`Z;Bb`ev","B|c3V","M4M","pAe_C:","U9au#*Bp>","[4Fm_]t," -";","h@ZV?","","#./5""K~]","",";8Jtxz" -"BBC4@jng`","u8lD G>]48","OZf>6oj","" -"-Z(}8{",";_)n9h#/lo","","gIvZg","X_On#p4","^","Fhg",".","aJz7p" -"Ke","M#jJVuB1Fb","eB>S","F1]","[/","=tCCB$k'W:" -",,4_","fC=00a","ReK+mxC<",";6Eh",")B","RL" -"Ql4:EHi+4","wG]b!`$","~.Bq","t0:$f\E1","","d:M""" -"J","","L)X{#2","%","Dr0Q=","Fw0@o%1z}" -"DcEB*4\H","'H","4;tb[a","G","(","""4 c:<5p" -"Jp@~_eb","Deg","uXv","","k","6JL-","DU)N?4IGQ" -"AS`q","V+DgaCsEuc","#mMs","","iJkBL",".Q|t O" -">SE","w","[m?","Lkx+uqS""+Z","jEl","*UXJkG]c" -"Xx3","h!9UUKGH,F","","_Isxv)^DD","Z","" -"&UY","[*S","g:`7Dnuwj@","26BfG8{","o%>2eT%h|?","/ue_a9" -"fg\@g,ey","j]%w","dLZ9IVtKbz","Wa/","&o7?!QK%","uDf" -"^wl0(3"," &f>`lwIL ","0>lt9Hg""q","al","","47!k" -"v0","Ksf(m$","\",")","","QU7mR?" -"$5PbDf,i","","?gQb)","V'4)","%0|)fJ^Yx","@+l?0`B}k" -"L=L7","WR#@,\e_","","b","%_.{_$","d" -"6-?Pl.","^P.om""XB^","CVk","AuOs(U h$","^S]9n.3+","[""+" -"I%D/\S*","x#f1%","Qo","FCQjLg=F9","r_iYR?]]N","" -"23Lw3m","qvOa8Ykh","7d{)","","WaHk$","{8L3r]$hy6" -"0h","$1^*q",",!N","Vo~@qe","$-%V)O%","pKuKG%5z.B","%hK:.|" -"[JFR}[mtR'","5$=>oG<","5#Pnk*","x8F\","X%/P\<7dyK","vT?50" -"ulb[Di#mi","a?rd9 i","","j90QD","0j","7c6EH" -"?WE)t4/","Qp,rvIi","H@ybC@V]#","!=",";Jzy)tX","Va" -"t2vrM]",">7f","[B ","gdhbhk","fs=g>`XJ~B","F!`GS","`x[t.t$XY","W;@R~oA7","7eP","StcV/w2C" -"{ksWf8X","L1?","w|Nq","`cGF3Nb\p9","QJ|","K#c$" -"tk]F","dZv'Te9","dkDelv","H3ewbm=}?","","):)" -"AU'i-^v7Qo","2F]2<0{""fa","dO88E7JC","?ep8h""X,","\P76P*2IZS","6." -"k","EzR;3:YRj","gCg","T1k-Nz1","fA-zc","t" -".WJ1+Ro=De","g8e(+","_eJoCv%0la","))""`<)'m+","n_1(","|" -"pgmjK(&;}4","","8?>Kd","sY","","EX(~VIHb&" -"E/4Y6","#%iu]","q!""P'H","o;9","%Kr>6&asm~","q" -"%@S#B{2E<","","?8$0o,8!s"," %o}j J}K","yE+j-","" -"f%(","<`4","g","}c","u>ik)-Q","\?mu" -"\","H","$","WuZg`:d`*A","Y|Q","Q|(P" -"l","N/","YK[&>^`ZC?","rK%MO""0'M","|ZcN|1WS","#\67" -"ds#1M@|","IJzR6F","-t)a","","Etq^","g!" -"J1wK)9G ","H&,?","&S(dJ'{","}z#DC","*T","" -"z Mx&p.|K|","@)#E}}","aC7T","X <,Pm,","8^uTz|o","R" -"(/dW9","","6","N5D`9y","","" -"","]Lc;$$1X]T","C}?mt![t1","NWp\j.","}m\","j-=IYK9/" -"xc#OKD2O2","lyXWx@x",".T\+"," :","0u","'$4" -"jAlzegNuX","+`lqS$!NOv-","^gi1A3","c^GD^:I%G" -"+f87zuY~","\v""","&","S$","JLN#","oP[NpwiQQ" -",=:lri","q0","&mVB&1","f`^c+","IN*+ ]mgJ","]Q3mBc/Ih" -"lUFwZQFYL","|j=T67","R%/Sy","gM@x","^1b, VTi$","" -"y~,","","yfAn]OTpH","7Yz1c?5CS","","=33te]w" -"M(",")~qN","/y-n2yM","","hIcIY~UX.r","LcX3RQz" -"0","xVJkO/'_(?",".[ZT+tUYx","q|""B\O","'V~9Wx1\",".e&&" -"","g8[q}B","","lKhQ$f!p","t)_sK(W","y" -"i:","aFhK95","n]u043a=H","[R0U/h;","f""","`Sc" -"","HA","P>)R","Gr","s","bU4xh""ee5" -"fn8","&suh","|$#o?s0X)","?","!E[V","Jq7""" -"T","gzvDhD","fcjimbwEc","<%0@S-fm#9","8I*}WSQa$","ExQN:7@" -"J","TYJ","","","-%f6 (z!7S","O{EaN" -"so","tH","QvQ4\%f135" -">So;u+9","=#)>(P","h\{TciLlA",">-","4r`Z`_F","-U@E>W~" -"I|","^*MI","x!""v`","=C|""A","TPBJK","^$p_" -".K9j l","i>","KLOR!{iwU","","p","oJ6M38?Xz<" -"1o","AC~E~){=$6","C+%%%RWE.","L A4EQuA","","fb#""'" -"\Uz!","V!.>(&8u>","(1t(*","Egt#B","pd/","dX" -"D&IP7.i"," jGOZ :my","\","","!G2.8`","tSf@" -".","@WP\" -""," ","^ds","C","NM""&","" -"f\>H*l^","3Z_i","*HZDhE","=O2GCEzPS%","5j#4S" -"}'84w","M","i7;/p","T\xbvh(Zvy","m:}j3m","0t:U" -"aFT6","e@N=>[","e","yw{d%zVO}","e@n","MosC" -"Z3>H{%k]","5$j",",Su#fb6_U","eIZX","KH\Q: G)","q4|1UA","J$/Rt$4Q1r","YR~6t;nh.","#)bCw","","l%Ou_Nay" -"Kd*\y,^EL]","0#m&ulCk##","C)`^\y1"," `n""Zr","","Gv&A:/#b*5" -">#KI","cF(0T9""<","$`E^p6$","<","#?","m&oxl*X" -"Q,","RvXUe_|*oP","[(MpJY>","lAa G,K","V)r+","T" -".mIQF.w4","qkP&tVq","L&","HT1JD, ","L2(Gh","rAM32T[s>x" -">","","vZ/[P\6","""n<_,B","","G" -"=","","vKiynaC}","","'go#eVf.u;","!(5O" -"","","b","H~+7Bt6v","\,BXVP+","iT^wB" -"j","!K79","f4DGj(e(",")2 V4WV","wxU#O,>0CeQ)","|Cr'of*","EYGd","z*UoM" -"]]\","E70<","*Vk-)]rsa^","","y?/qYr0","W_y$V" -"5V~X)","g9","Bo;#)","(#&^=ZA","_+d.=LT&,","/" -".y1Gtj","K5A%","\,V>wQj-_c","{x6d","_o","O&" -"D",",[<5","6?","d","+ocf:ay`","*1!3Jb","-","-AMVOE","aY;","`k'%k3S" -".Eeqz;&1","!","",""";ll*","b=YeL*","&" -"[sd?O","^/SIk+","","1L;'XJIDJ"," &v=:1@-Pr","/e61'+9&w" -"dc<(HA;}3>","{_M" -"^~r[FD+","1K","2OvR","@9","?({)i","2?ApV]:h]#" -"t","n]k+","vQv6","p","7+*P|\'","yzL" -"PqnhWiHv|","Dekm","I=","7",";fDW +","w*'T" -"r.A*Ox?Z","`c","3","5:wi\CCCx","J)","! k|~O\s&" -"R!Fy/%l","L_]g9","+","","","lfc5%6%" -"Lxl,n","^t","BBgoMVi!","\@*omRr_","G?","" -"V`?","7PW","i=",">b+D","LX" -"-ZB","{c}8j","oR-*\ylE+",")~rK4}b=","NAD=fT0","VK]" -"G","<","rHso{>","","SaiZc","@" -".T","9p5hU#YP?","w}}R?'e","#}","1.~Qz:","njV" -"\+@}qd'K","1/VGUy'97c","c5a`mH}n","$V_i&","/Ko","x/S}" -"""hfNX>3","n$","p2q%r","f","","" -"""}`&(","?-P}p:N""76","","BpdrX8","2slLCRs`*","VV4HK>|{B)" -"f7","azN\JZH)","t})1)Q8-aW","Ac&","_k@/4[W]q","s:4vUc" -"4_T","^Y0R%","GWETK",")3","*,9PHsEw","3vcB" -"9","MPFmx5A","-$@""`&bY +","?""","L%","((HX" -"9Moc",".dJ","W","yi~k",",9","{nQ" -"jQz:1<@mF)","4E\0i/v","","Kb<","W~ XK/","*\0" -">'XO+#h","-~","qPBJJCN",":W ","z)J","\032y`Bt" -"OB(8MquC","6W{@~^U","<;=Z3-.j","6","reV","" -"53i#0|&[","=ke6u","@i&xsw>","eF;d","eV8h2+|*","FSXt" -"1534*}","7OwHQ0" -"Z}J$y54\*","","7hg(gHYI","{","?D-)",":1n]E" -"c","pnP","'*nz","Xa}D)q9L","7DL","i" -"2$#p`5H!dx","","9F2|","3u","Ccik<\'","Zn&)|l1y)" -"B.~1q} H","MaT8qa'+","N%N)","$cW3""","*Jdd","1-@!nX<-" -"2DPCA':.Ih",")","'U*N>\g(","u_d6^eT","V","" -"xm12.kj","""H'lk","c/L",")|@0","LP.8P#|>","=R4P|" -"","+s","e,T7|HX|tZ",".I"")(:e","",")u-","p+kd:(ES" -"iPx","W",",","059F=a;","Z,h\c)xCS1","" -">TZd","i$b",")_Xm[","e1+'aE:3","d1m%}","?-swZR" -"A>b,RB\C" -"na","","","3k} &-","g{?Otjt","C]'" -"""er!ts","xx","^p","+*c-?H" -"HS]9'"," 8ayV}6""","@'^w#!/","A",">I"" ","%" -"`Rz(n ","./v|","YZP'R]'","hId^$i","Ir","qQ)(" -"']k+}(2VT","a-","f&)e.,","ukO'",",r`ilIjhv7","LR" -")}","d1ts%)ya_","H","Kc","#9Kn#+","h[*=""g;z~y" -"j","r","Ifa]i}Ih","e","","x][0-H]" -"@~H))=r","x]4&1pgD","}.","%{a89N{","~{#+<``","wq" -"1J","M_1}&w>s","{^e,usXNy@","N.ItrH","Sk2%r","_m22me27" -"G%xDjw4{","","%3jx","4~S|","b[gX=XRHOj","(m" -"sB","/E~Pl&A","!","K",";h!aqS+?[F","" -"d_W[=\T","Zo","H`S<","E",".6DT45n0?*","{:f \M" -"!e&`)n\?",":","ZGGCo?&|A","ubd:","","^te}P`1)fu" -"#1m","Y?*%.s$)!i","G","O""Y'b]p" -"aTe~o","f;a@_Ku4","Y[L#A","i6|e","lXM","W$J\:z6" -"36KG#","rX7\","","zM7","",")|]*nHuHDn" -"b)]","@<","I(tNADDwi`","2@sN","&4","Po0" -"nogD'S","","( r!j","33u*(<","?% XWDC[","u""X2h" -"""9AF","kk9m","","","Hosf9>RQ","" -"fo9s","","Ehm#m","A0,{w~n","=|","W[#Xv;^" -"DhDAd0W","","YU+A|/E","zfYD63","AVw(A}S;-U","`(8=" -"","b$r%>","`uG rv","j0.UQM+t","R5c:D.9>>!","#i!7~" -"JJ","X\9^%9J'","~NjY?p+B:9","j","b","-" -"~>K c""O%<","pUW","p/k~}I@Wq","8ztcxiO","s6&;^","#shZ" -"DB6","m","","p_`lCn{","wUNd%3.A","*.NtX-%" -"","s","?CE.","LS+","r'SQ\)","D\kG%P{wr" -"K5&v~""`pS>","8](}+b","""_","",";G","Kbxn.$d"," @0","ehml<","oT7" -"_","5gbAv')$-(","U:21uG-","YC^c","","b!O,^" -"ioUP-;gl","h2`*","nXZ","t|fWgds",".B*","vj." -"]abR:T",";[ae^B'UH","X|""b0q","&t","*","AuF0" -"Rjau8ul&","f","P8","-IU^Y)ZQ]D","5V$_UF^vo0","up" -"","?D[kd%C\K","f","xR","=","o@@6" -"c:Q-6R","m.K8w I","OgX","_Jf~-","F8KGHN","t)L" -"}C??2","r965oHDNz1"," $~","t","qhdH<<","z)gC\" -"FM&]""25","<9(N[#E<}","","'}w.!_U&","[H2Vl$gI""w","F" -"`J+^l\gy","?QiPoL6E","$jj.PL","'a(N}M4","`Q%O8","$UC@Qz" -"ww0]]","MuAX+N>",".i8m-","&=%^z/","6C/kTP.Y","" -"24""","","c%+Rs93Fu8","J:iNcX","BQ*RN" -">8|Jt$","L}","WF/xC/kLH-","55;Vr","yE=0'" -"","","MQs?h","","wO(D,;;","kCs" -"MH","d G%","U","Qg)YB+>>k8","F|2sxVmI9x","I83" -"qwG-ZboHlY","LWjmr+""","(N*","7k6 91","wNP}o@a","eX2f" -"XU[+JRFPX","","O","","8Bg","MUF\'L('" -"J*6L38s<[8","JfI`Mf~T","zH","+","~RGoZ","z_Q" -"~5 e","Uo","V5fAF0-","","@a/q","2" -"x,M*>CiM*+","J~r7*D g","W.Ux","","","_UFyZ"",[:" -"^W8F]#:c",">6","gZHx,+_a/]","Gv","sr","}" -"9'B*:Y","DWDfkr*v1y","E@","P@","","w5*$mR@" -"{""-\([G},","F;gM=0:","[)","+9t","Bs""8(2~","uQgt" -"d#","YZe!vB;?""=","q","L9h\bgDE#r","/J","fFc:AYA3_" -";U,:z)@","","l>^R_S0]",". Rp=","[.R","|[r+Uq{OC" -"N54x","w:7NbeRkO","M?}Ry591","#|@z","'IyWG","" -"h=02i<","7W#es~","","Uh","K\e",":D1" -"7dRh$","","m","VB.$e.D""""","v_",":;T9`7" -"]y){""[t","}.%)/m}","|TD=pZLz","H'|9l","[v_Ew$=","i(HlZOM" -"","=J","@am",".ZIZoUYI7","W@>6OL","~76uy6" -"","`0&3$n*T",".}","oXB*b9FH","R^6"," E[R's" -";^kkE5aup""","jLIj8","N`53;3","_d(/?;/n","h&y<","t" -"kNk;Ke5","tw\&zmY>,","{x)+K#x{jI","","3$b,d#^)","" -"Gp=1(TJ","f","(0u|#u7t't","","*:{<","" -"<","|^R|LYA","*pb" -"#Y","NlW1`","ezT^Jnm","oX@m!:","6","_? =lT" -"",">Rf*u?[","`kzNE.",",KD&,{[jH","V<~.","id84AE " -"lbn","[~11ZBK","8[:yUd","9cSn","","" -"^qgqcc","%:OK$c""","Zm6]SE uuR","","<,P_K?gb,r","AY(lB6" -"RSpH","\|U!:r","{lv)","qxVzjwG(r","X9Kr%g'","" -"d<:","fE4",".Tsv/J>lvN","","#2l~_S.","J9oH" -"OiI0$Ps4",",","Z^7oe","X}B{B~","<8ho.uw#","l !~" -"1V'","","9DDU","GgnZ]T","zECUL[Q","hobJh!h\r7" -"P)L","Y_@eaV|","P:y`#","z","#',vQj!s","kZ@""*eig" -"%%{^~RaO","","R&??QE","=","dT 19e","" -"(j","s6vZr","G","j?CuNK>L<","rHaC2&\#","pZPttv[" -"<%Pplf","s~KZm","-/C""","Fz1k1Y60","K0$","QmEkIPG" -"","IW?",";Ub1ZVWo0","F/1NP#fFC","9`zW>Lc.w","n8""Qq" -"4:?","","j","D+","kyPd#oHVdt","" -"","#","S}=PCb","","oj{C;d1h:","?QV}Fyu%9\" -"wD","",")tjb/+I+","^""O","","XFHQ" -"vK","\","G/",";`/.NU","{z@e""4A","VtA4spU" -"c7,DK05",""," :fpef","LD#/F#R(_D","aVn?_k ","t +eq4Tg8" -"v|8x","s0G|G","8C","<\Q","R0t","%R""C[" -"T_1<","""$E18g3","E?<","6e_x,","|e{9!fL%""","z" -"B","mM\",";oz-\E6d",")","w:8?]N","TNoQ?HOx`" -"prWQ2edO(","@P_=\","",".K@`!hgm$O","Pwn","#]#6" -"","a,@?#=s5\","5eO",")z","'B[9","}'N","ZipE" -"!'.f",">f>\_'Tg","W?EW","h","(p_u:}t","6vYcvmT4P@" -"0","","X","pkZh ","fuem|2w&","|LXXsM}-iG" -"","OR","N0,%vYTx2","7lr9E=c#^}","?i8Et#^<","h2" -"99","qo'*YR","{U3:f","F","x]EJX4","R""Yjuk8U" -",`*[o]UA","G5,tTLZM","","(tQ3>hRT:","d}0M","2WhW[\L" -"5","v4ew","%u{3","fF#ay","u$[","$Nv4S(Ip16" -"oR6WW'y?_","+czdXh)`Fn","CbWLwO","k;.zHc2U#","1)MP3P:yN5","t/+)" -"V53~m""Jc","h3Ld[k","o3OUr0u a1","Z/kzq","B""","&$Q{ .#6FD" -"O",".0y?YQv^n","Of>\Y7~B",")*h72"," ","E3" -"a,V}""x","h(XYXKwEP ","P","I/yDSK61C","Wf,Q84n","" -"ICCY,l4#]","1=^bd*","t44[pF","O0:\n","","7e@1<;^_d,:d'o","+o?""uUt:23" -"x3X<6","shK:t","H&9K","Inud[","9B","t" -"z5","qb%fZ6z[W,","|s","","X","RZH\$P3/G" -"%@0GqM","|?}:mh","MlVt","Zq%U","+i'6","+1mM?J" -"A$&",";B:h%","S6","7J$wz","Z","U}" -"k","M{%U-,,","Fh7_K8Wpk",")hQ9u23t","qX3#0eoq~","h?_EptSeiv" -";Kz3" -"h~xRIUu>Hl","g,","x7I,(-""rs","&.","C3|K}2t6","w[@sG$I)%n" -"",""";[N66I","tJ]T9","RbaC{","_55_z@sA#M","1-UnR3c" -":re""28A_-}","A","~-)S","0","JZQg$\E.","$SWi" -"=3NxWzs","{qJ0[,C" -"7Xm|#","","Md#%,EXZJ","&Y!(AH(","{Etq(|","o_" -")%/yN","K8","|ph9V","J4*","","" -"0!","n","Fh","<-4d}Ao8","t[dx,","_y" -"%d_S!WX-z","""I9&}0;B","-7bHs3i6QG","q/K}[iI4U","1Pqpu",">A;8UR" -"8","m.4J","","!","??FqZ.2H_" -"N]PSgH","|Qf \#M4#","m,c","',v`AY","c","'kqc14EUH|" -"|'","GJ5/)a!N","H","XDjj(x","/C[\jl]^","JKgp,UPF" -"kxxi_A",":f","|hb{u","","-rh3~w)@G","=G" -"a[8","T1","1bqV41","[>=bA","02(" -"qQ`?",":T4M'*","l[j[w]#Df","","kw~E?e]Af)","ew~o x" -"6Abb","Kt<^N`,"," P~","ZrQW*","aoX~NI""","}" -"*tws","~SUh.","OKpR&T<:","M+wo}|1/","","(VN}j" -"`_.Z","5}KX;""ch""","`mKKtX#;","K}K|xiHk?","trVy^tAM^W","","Gc-(" -"[b9^97%-|","lb9<:J*|","J","e","","\yxp3dTh~" -"gel4=","y$","+gqzP, i" -"~TrZ MZ[w","{bQaO@l$}","c ","e",";'IB08L<0","dm83LY3" -"tXv","8t90ODWV","(@4?U+}","h","rE","i>c CwpPM" -"BCe]Kplg","Ge~`3;","S0&kZkPw4U","Id@6lyw ND","`8,Q(9vEb","Z\}_3[|-_{" -"OPC-ov","R","2.c%1f9","%}c}srAJc3","(u""2gWUIc-","0MU/v" -"g,y""mG I","8*3Y*/_","{6F~","@","9R7P%","NUOc" -"U""l<+O%*","tX6(^9HC2","&Kb[","](41~9]cW","v'.sX8O","" -"kl","mn","N9","%*?H@0w","p-C+ci:h:V","" -"WUb","","K{8Xs,","}/7C=","ybRC`","+<#~>$52l","U!2","" -"=tRxwd","","H%*tXfM+[","w\=^7",":w","Min[*8" -"RwFFL0@","NGzjX","6=ml'#O.fU","*] A0hm","1(","*eQXu" -"zl5^$,","__(faA`tT+","`e<\u7","Odd*TmpU8","M","_*""[5aZ6E" -"y+FQiy""","|C/w~d+","","U","(Kb\u","db,@" -"s","9","","R'ksKw8V","wJ:2B7","X@Kzh{" -"2","t[0Jg+io","rlMzm","pslFfg0","9P\J'","~d^""2" -"#0","`","p_MvZzd-","Si","1UhGDN64@F","T%nizDfy" -"xg","PK3:N","rla2(r","Sp ","BY""","FY!~/" -":Hu","P","gYmIGX%","+ehAD","","MpW?q_j" -"&O/!,e","c[tn","aY/5&5Is{","H>h","yrI`#","H6K7" -"3K$W\:%!","&n]|:0^/","aj.","C?5!2(","QO","o6+\G" -";Moc",">=-iF=K","`eA[U[9L","","5S:9:r""","fT}J*06""h}" -"j","*","WP6y^/Jn>*","OS[l8","]<","{Z\Y8d" -"QS","n0tVf","HVa~e","Pun=8#j2<","E-T^=p7","" -"w","^","Z","u.","4J;4+vW!S","9" -"aG","s>sT!SU`","s7i43","@8QR50^CYTX@u","rg" -"$bGY[)5OQ","kHG\B*i",",U&f[GU\Eu","rn3j+8aCR","w\=t#4L;","" -"7^So","`KBSSu8","A krj:k0FB","{","B{g~4&Q","~8Wo4","}I","}","@SgM" -"!5 P@ p^-b","#tKl9","4&","KYYE","\K)PsgY!","q},pFW$E" -"@AB#Y","/GFo","O7t9j^&XDE","mP;C.=W","-p96WVdwRp","oIo'A~#" -"ZGJs","$H?p.R","Fl=*XWGpIP","g|+Q4%x","PSaj","QeMky:3R+" -"e|Qv9rs?D,","|+c","mL*5ZgW&P","K2Q?I^G}","!<","b" -"#;1@G:rM2","","|h+uwN","})bLT","P 8yqc@9","''" -"^A%KXE~nz}","MSop7F8","LKNkYF","]SpTHYF~I","","q/e7;Yw{O" -"&C>""",":!",",e","C;*g~","a","7dJKU)dE&" -"AJ^MCPZ#A","^z","'D","yeEc%yq[g",";1i","y=O-|0Wu" -">V4W","","o%48wC","0_, +~s","",";^GI&","\E~+;|<","??V4%gasz","9z]","u] g3O" -"bcG","JrQzi)}x1","AFf2h","RUxC2","pRa/i1w6]H","" -"$}%?y%",")HpC","N$BR)","h~2NG)8a","+s?-;","f`" -"","4@T=~cC","","fI","c","+]T)vbg" -"B13F@","9C","+52","V:AN cgc~","5'pt]9cXs?","M:R!4V'VT\" -"G..XUS","LWdJS,7,$O","-: #F3o*","tS{i","gqE@o`",":;&,|L! " -"}O aB""","pS`'","0oR5","+-5Mi\8|<""","oA_^y^I[","u|z""" -"gWv","x0>U","","+","x","7}mBmY5Cji" -"Y%*9 YsrJ","kk/$]M2k+n",".LA","0Bi03U""{","aI`6tc0z,=","w&","RPC}:2DM","VZM:V;2aWG" -"~|(Q.5","prY","=>S`]3:oC","Ea1","T@lp2R""Z","C]Se@" -"","j?0 .","qXk?Q^>*"," XNP8","","B!~$jG9Y*t" -"gnO+y3[","QwWawS2H83","7UZ*B;g(2","S_oylo","gjF","le",",5CG9/Pv","M;Clqe" -":#\","bX~q;'l/w","IA)t|"," Jk|ZR%64I","},REL7","a!`naR5N'","cuP`>'""","9mi_=!Zx&","v8BzApTzu""","ZcKt","2{'I""(" -"pE\$l",">{m","V3*","","8R3X.!-gK~","p?ojc" -"5,dlP$","ov","Ta","I-Pz","^Fn""dZ|Zr4","F4SkH87" -"[DF9T{","0w\i/u","Spi8z\B-U+","59{a",")*x_TByJ@","stHZ" -"","45##TM8qs_","J8T:86eR",">hO","'s|",":TJr","rE#x," -"9t*KB","$","","F]~~_","sK=Bjn>" -"|","}","+{~~4GOnrD","Fv4""}",";u?nOmB\6&","" -"","x","Irr_.Cl!","d3 5N^","Z(=","I" -"s3",",>","GTxocT'","","&&a8NV","3#9_{" -"oK","AatU","#",",#24","8","l-+k`M!" -"o,^k;PdU\","C%)","IE]}jm","\&fho""M:m","ackx","K.b'@" -"",".\y5)^E","hoXh","&hR)lR?d#i","{0bG=:OO/","Et" -"o*EP=""y:-","(/5VbK","P?4%+v","p#s","%z[- xOxLs",".;""XyQBC0" -"Vw>C9x>>","Ga","Eb","DhO]","apAP","/uWkaFu" -"D3a1U6oE>","!\sKdBoA2","VR=","h]b:O6","$","S]&K" -"oW""K5?C9","","_","i3oor8-","Xe5acgv","kB6AP4Sv" -"u/D66*Q","s",">x6","L6qtiZ|77","q","u>^y,|_Cs:" -"C3Y","Ff/wII<){","\!A$5~yp","j^d","/>&:dIuNlM",";]8^nMN" -"","P","U~i2\E*w(","hJb 9i","Ds""D","" -"j#{!wo#",".jQ+","yU_ny\3L'T","jBk\","<","BY}:@4" -"","Macm;=hVl<","13rB%Y1","$`","","WKHRPT;fA","EGE_#A","""" -"S2","dMuJ","t t*r[K9","rSDc?&K=u9","DP*","" -"81","","","V_","""p>>6ZXG","A,`^E]K-""&" -"M04JE@o","CTET","zCk)_q","FMpG/\X^","J.YO","N|`" -"XTSkM","HHDT`;","","%1{k]}?","""b=","/ID.v" -"R","","Vm","","qbKULxV" -";qNbq|eqw3","0\o,""eVe[g","vKn\VPp:","=)j|D!z* ","9DmUUl)zf","$^!?" -"<[/" -"|Jb u","!","d^w.","A;g","!R)Xk;","lcF@OX%" -"hB!7","\f0}","tQ;O2a=""","!:}'@o*P?-","Lxq2i","!{","6" -"","3b=3;9c","KYSFO!@>B]",";2V$Zwye","^L>Qn7D","'FE" -"ui""|\","2w","m-5E;K`sdT","I,.s/I","|""nvp","(Y68%" -"8*1ufFM","9{AN ","1LML","","/e|Kj~K2 ","f]hU^" -"X?'|h?D}R{","1;^kx?","","-\","d\wh","`A" -"p""0FPO","6","Y#04%P=4","QZ!r","xZ~E2","b@" -"",":(A5]r~.","1","A>{","tku%%6l","hVOh?4z`r" -"529E=~","P","w}EEc","q7!aVh|2&","","","d.L%" -"y-I'","}DM","x",")d25=Ct=a","(}r(aDiOhg","Lph9K`~0" -".rJD%}","n%)-;76Y","at","u@H'WcG55[","T=DJj4LF9","-J(,U-xV" -"IGn.","H7Z_","4q[&{P""UKm","5=eKS",".{]","1P\Y" -"`E9D","",":","aaP%p@","63'Cc,","8PS" -"]*0VA-SBRD","""a-~i.{FwW","}T(7hu]Z","K_J)qL'","Ca*o""mQM","kPd" -"EZD","Y~",",\BJ .J4","1,q)c","","iY""y6w*#" -"XRj)}YN%","Rq9/","L7SG;6S+T","1)5'{}FPC",";&>?X"""," \" -"5S{*1bF","Of@x=mv}","N|l,9@k-qV","p&OTv=xi\","J*","dhEgs" -"7uLg>s|","5Q4x","r","%z{O","","IG%" -"^","tsI""R}6_J","&-49G[A","_k$(u@\","hzsDI","+yV6P" -"]Y","\iwci:8","H1eJ'YdP0","(uy!*WP@","\'e","""axFFr)7.&" -"0N#02UAe~f","6","d","~ zPu%$L","Rw!nc@~c"," 8pHCf" -"ej-6","N7","2s3tE,","76`ir_8nUu","{pO/rH","" -"|4\@/@n_q","cov","09.*y","o{KG,f;XfG","C{L+Gj*h}","Pw>" -"aZ>gxL;A","PMP94xo","ho7%&#XGW?","I2l","_~@" -"[#THPA@8","j[""","#+}?27v","4`M}1|G",",","?KX" -"8)v\!B~""","4@L",""":gWn"",S","!zzW5q>h$[","","ifUqg" -"!ea","d^'t8=$YLB","$~s","","a$d*1"," 3fM2pq2F\" -"|","awbAJp6%/m","XF","y","*1AXL%VaO","*]@8=R2" -"f'/x*Tz:","BGF8A","`","Ebqk","A;p","o2c)" -"m2y^=:l5","P","]vW","p!","","=?:R~\" -"#;JE4\jOfy","d:E~6<-T^=M40(","`j\","6/L,)}C!","=+r","T;&","!+Q%<" -"Z","!5","fhX6v}3QN",":jm$ff}Ja","'","9[Zf" -"J?e}-","7J1","","\^a",";nIn","" -"4_","FI|im()}","Qn","l))s]<_","&#","Ne$" -"9X./Tl+.*{","&","ori","m)","f:JZ","G6Y&" -"`x)","r=p;""oUw*J","`g","t?|RH","Ef~]HZ:","B=?J7-07" -"/D5}inwij","PGJo mM@t","AU""b6E>-rN","VBGY ivC","BG,r?uVT","p!jn","5" -"X@%qxj4","!?S^bftN",";o7M. t","%1",">Gyd","o" -"V#CL5H","","S7]XF:" -"q}Qx3|ei","+8s)a1\","Z","M(#>nrx>Kl","62IX>$","|Uw+","%enx3fhbT","39zf:" -"0pieC","1C","#e","sFmUE","9","0S|ilar" -"`9K6","OBG\Ir&Fx","#","T,","L~n`T^_","<{k,qdw" -"veC-TrqVK1","C_bZj{BYMe","Q$zE","",". ","" -"Qp;R%","}.B>%#","2Z(,8B0Xj[","\v)","_","k?Ntippq?" -"!","6n""-~>","t66>k",")tv9){i(i9"," f","\AH6oG{" -"5|8","`",":&f9","GeM{R92""e",";wU-","!}" -"=HP7BHD","Zp","oM3K{B&4tz","FhX4.kPgYz","D==bFB+%G%","|W&>Iqsv/" -"_R>R2c=~S","7G/#","NF'q%te","hJL$NG","TV","vi;" -"n","]:(UzIms","3H}tx","82","H","9O`*E" -"He~%vP""","a#","vFFg((a>p","L_b7_7x7M`","`^ #i_[Y","*ts{T%mu" -"K^J1l","4",":P>rI","}6~_AhaBc","f","Atu" -"z+",".2XW^","","]H%x","|Ue","vqw=l" -"vrc^u6T","/","","{""]K=+%w","q$tn*q'","F[#;ex\" -"OY#\h","","$","jkVo","1(","trR;O$<" -"-""C)p!wW+","NRh.HtfUnh","(ni3@","pA5un|[l?9","z]6{Yn","XSAcP UG" -"TU=5@","?AKY-*T","Xq","-","tLAqYc^uwt","""g'z" -"/`'A{","Z(Q","-vxMG)>h}","]K]*L",";/zZV^ZL9k","""0" -"h","","y^mI=R~(0t","9)m","*0zUM","h""" -"","CDyvOC&@Zw","qZHzId(M","$[Vl 6","'h\}Fx5y","B" -"X9AI~>Y","","!:","8aFO*923","m,p","g[" -"}q","","bW0A}a","","(t1q?~+"," h^j!X}VW" -"8!Q1.~ s","Py","c&Lv&\e*","ms_d2-bi3A",")#HGbA-?","" -"9C+gr#uS ","g","G![<","NMCy","D","'9PHH?" -"#.VPb","7=2B37e?4>","nPT7`","llpW","9Aj9Ooe","sJ-nNB" -"b2=S@IxIkm","+BA%Zs","W4%I55M{8'","\'D4,o>F ","=midD","9(J<" -"#bHT49QG?","","ds","BFLy\W+/x#","n","M" -"w_&Vo,","dg3'jV","4I}Y1:k;43","","Y,9FX/8j",">8+t#" -"BXQ6-pUq/?","VY","3MoQp5?","D4\'","|tX2S{\wg%",":A5kWJ8z" -"l","=Wj)t?(H","jw&L6?","(","fUlr6""9]","y'Z2,We-u-" -"U_bx*y7p,",":$]Qs9Q}fH","j|39y{>M.n","AMN6]","^","" -"){uDx:v","T","]","uxCJ","][m4ys*1;","UQ[@" -"^Vp}u}","v>ewvCQ","^mv`P@O","_Q)(I]","]w'e`","?YnBb?a" -"","Hr""x`99Q3c","9'{""WkY\EA","nSh/aN","J1z1,]","4@u" -"_","&","r&","D/rAv(","hM","u0" -"(ra?E","H4bU$8O","9q.w0[T;r{","\R","","XW!8o*k" -"]","}jHz","M-8.SSwe","i_T8m{^","oc&+^zX-@","`4"">U|I%Id" -"D3oIaNg","OV|WXNOJi","VXM1guw","''%}8oT","Q;K@c}HA","E|B1" -"%F[?","","","fOz2","`WQz+x","" -"bYi;G""';i","N=1MX}C>","qxIF{T","+!XJpV'UI","+","e|dR(*E" -"M","""zkip8g","","(OwBj?z#","^Mtv","X" -"x","XFs/K{?6","4Dw&rj|,k9","","G","yZL7" -"","","4nHhv","}++","","OW-mE2" -"q[L^hs'uT=","SR:tN'","Efc\cz","","","" -"yk8ona`{","bUiD1`n2F","B,W[GK6I","x""t9lqZn:F","Sgr~>S-p""","G~@L;" -"ap/""*WB?A]","Y","V(+C1y)","yZS}wh","nRc)","=B?*" -"0w)u2h5","g","9","","@q]uL'","Ifkh" -"ny&#vmzAXQ","0-]2i","oM","oG3I0","n}y","V[Uk" -"B","1(^AJ","d_'","_","s","" -"","6","MZ^b opH[","K}=q","?jzkO=%","@r1_kqb&" -"b7EZF","6SA","qP6]J@(J","P,ti","(Ur"," N_Bv\" -"EZ","g17W","N","#>x'","""\Mld1","FR[zD?W" -">2g.","u","Jo,|#","nd9?u","","" -",9",">Yk/*8CZ","2~T[","NZUF","M$$VVMt","*:U+C" -"XJ","T","Wjk'nBQI26","p","G.""L3>sZ","." -"]3TOkQ","4Sa%..3:[",".fW27ORchA","",";:W~`","1U9W#]W" -"uL&P","Q[7AJg[,%","^/Nd%$u","3vReU;YD","KQK6{-l2^","#i)-H!/n7" -"qrCR/-85?G","g#t="")r2)","","6a1","D$RB%R5\C","?ldH7" -"TE_jpj","sQ\\9:","""I3","\-0Y+","Q\p^","'Rd9wwIk" -"=H~=fV> *G","Et","gf!?`?-=y","tH.m<9^PD4","oB","8>h{1" -"KQ6","","_V >3J6xnk","!^@<9","J{j^","S.S9wm%?#" -"=OD:u","""B^th^`Y&","","^pC_7zY","3zI","^{1j=hG)\C" -"PyD*cT|aeq","0""Ql~","Nt-td@e","n`VXB","s)N[P[dGz","$fvP{" -"~YfBP+D(A","","#]})*A_","kZ""'",",NbG6z0N6","T%+P" -"BbC,h1","&9V","1)","X&tCA","UOL]k""","/0mU" -"U7%=[,M{tf","9OWO*d v","s_?=Q2","","N9fBp","g!N8!Rsoj%" -"+j$","`cm9N","\5.Dl","XFLRQfT.xU","x>","{*)" -"I{N!9","","Ad",".S9Ve!s*e","?pceUt/","tm\6AJ|~" -"R;A:[&>H+,","yzpYi","BZ0U}n!","v-EH","9""e","`\Rt" -"*1V9w",".""C","lm)%C&/|L","yo#tqP+8","LM","" -"","&*Y`","}v&","GeGiWR""z;A","f.","Vy,`u" -"@`fx","]\AP\zF=<(",":Oo%c;gvx","&.","FGkz"".",",f&MXV" -"$5","~5_`2","QKN|%fa7,","","wH","" -"","","+~Q""5t)S.",",g-D ","{",",as+};" -"Es.rh~;.^f","IW]!\Y$~b[","6=x","L1H)]O","aiF#}b","S`_DWAb:U" -"P `D.}l","0","J ","_UR ,","|E-Q3;:s","M\{g|dMjxy" -"","Z|7"," -GH,;0;A","~5'","*KHU{CE5~","AJ.[&1" -"ij*>L#qvt","Ma+5;Lh","","WV$43fpF}t","Ur4H=H","&t" -"3","+""}29D3-","T?XG","r_r*f\:","qOJa{","gm" -"_0M+xc","cp}[","1" -"i","eqCYG","","?","+18E|8W",")W2+R%" -"","6'WT=O[^;/","eF","G(v*xst","pPxN","KCVs3p&saz" -"","MEon","TUgSEKQl","],+-U","6qa^lp!q4x","H;#0{#ujV`" -"lW-","2rhQT","A","qY'8","^","#=}G\6rqI5" -"\sWh","dS","38 H#W","J|UT~" -"O89kTE-","&""E%){","/","KlEuc","eh S_V{jjB","" -"{W.\gK0a","V+u^2$_","@a?","|}","v@7",")~Do,`pi" -"7j'","q15qc1v","\uu","Xc,[#y2C","}FS/|x$","Q+1m.o>7" -"","RCZv]Pr#8","e/nf2"".wy","REeXl%6]","\e<1``YL+","%,/o" -"|UomE~i.","^a","H","GX:t!@WE","QhM>",";" -"=2","9KGu2v},Y","w","$E?","","" -"J/{=tmV%C","#,1-g","}Mx!>b(","JA33G","x\/h!""V","O!6Nzg" -"",">QH0d(a@AK","|=ZIX,*cd","K;[{f.fBBK","^;&p","q5;fYR" -"1k=k","'n'Q50","","","E[AZ:1m?z","|e" -"'L-NSp","~T\@!ELw#)","ad!","Y?$Y","{tcP80","" -"J&F)'""=>*","*~F]YW","q:?G.","","}RX8B""","","DuLj x'~N","","","","E[&66'|" -"2mU*]","9}E,R|P","","pT}TL?ISEt","Ak","? t" -"djk","","IC<<% ","$w!v,SO$","UY9fL|p","F>rAp;v" -"[ >","wl'd{hw",",(]","rmX","j5nl~[t=Np","6q z4:/Q;C" -"6",">XIn-fFF^K","]F{``P","",".,","\e@" -"9n!X","I}","kg|Gj9td","@@(5","~Jf","F^(>;" -"aHR7","Ix7*BleyZ:","M$@]""G'","y1n>7","","\.YP{>" -"ps5Aog_","}","J0$C.M+9yF","z.I96","y*UB","" -"jERgb","","","7b?","","`*v|V-r" -"8t]","4YOUNjYp",":J+/@rP&","","n:xFp","EOjCoPLtS" -"$( ","d9t>{#mFp*","k","{ILy]","[O@=","8Rjf`" -"F","NDNR",".","`CC","","eD" -"(~z}ka&C%","E","wIy","+eT|)0","LtY)W",",sz""w2L" -"r","j AD","WDs""*U~<","h","..s>X]k U","0" -"gO","Hxq""ny","","(%$","c-PS","f7.|XZ:" -"~pYj&)","(bmAm","","Q}BN*)",">E>N@v","9*" -"Ecbpng/","/V:","E/","HLD!''$6="," T/AMh>7","G/=>m86N%" -")gU3*!T<=f","Ug'MUU4","X;""C","lK@","x^~m","IAY" -"G3tzi;?-","-l","-tw","6CAIQKv " -"YjDe","EP*","Rg@VYU|","&","e1=`iJux0*","h" -"W","nQG3wv","","W=","iTh^9 ""#","PJ!\1-/fhl" -"_dimgIOPz","","ruAH^u","5|<>Rt{L","kk","" -"6d"," Xd$r\","_vc;"," A[","","F23Q^Gb`ea" -"","W","_,~G=|7SGB","aUHw","$#,|F","MR[5\4" -"MAnL`","esO8B/r}U","zki+%('aT","K2=","9(E&eG)YV","L9-$#Vslx" -"D","$(","NniaK""E3/l_T","(","EyM;p%T=c","xI" -"*V1`@2oxN","h;X{","YeyVi,",",","T","vG" -"m=`Pl","uj""L`MbeE","KI","netv!9ZB8%","X5+","6\O%9l3250" -"sZMn[@urg?",":TND'","5IC","LYrYg","dn","H""Rod","""a#|","" -"$p,M9((3<","H?16",",F","Uou\`i{/'","7SBk+@","v YR" -"sKEW]|z,","`4B","m>","hO^le-Sp|","!Rxc>f~`","V" -"MRy^niI#^W","","","vA=]Knpq5","7Ax[}",",R" -">^k!YZ+z.","N+-eI","WE~""|-1zjy","/`K/17","{:=""l","" -"ZaF?4uSE\4",":M","rHx&8","&MzX`N"," >aG^L\@t*","|'r@,0" -"#m","""X?B_","X|,%%?","@`z","Qa%PUf","27A" -"0sNK_W$","[{WW#i","","bU"," e0fa\==p","C0$A`@NR5" -"OQGkN[","[gV;73[","","&0.c"," 41z","%)I7m""(kOP" -"sW?0VX*tl","$@t\zH",", 6AxCK-$","]9h_b >","-?ie"," A$sk}c" -"0(iHD","zmv%","-&","oZ][s9","cu3!`P4x4","n, " -"#0EP.mtx","S@]s;","6Nj","b","Q8H|;","" -"-/;\*#","}>","XwPhAK1[6a","0Yn","23","Qx!|lS6" -"2O&T3DWG","#PKZrJG","D W","aQHEw/4/"," b","klBBNEA","8k6",")L<^V","!b;|akP" -"ZPF9Vy-:","SQ%)4Mp","Srdq2/n._" -"<,=((V!","te","V|P$[$/","JgIDE","iXu>=-","1CVf1mrA" -"Dj98#RG _[","n@?HK6dz""@","rHLT","Lk","{[=xY","","D(C,uB","` S{[ti4" -"]0dR&wYZ","utp","y6","","+,p;TJP+1","[!{" -"`}E}!}Q","ej9(5OQe","=T'j:( $v",")sH","9ueJeL","" -"8n8eTE3","#4","\rlM~\","?f=>7g","","\" -"mD`$&]N|f#","k","_xpJ|",">w'u2","rodIU?h","Vp E#M" -"EwK","T","L;v","dUgvgm7","pPYA7krakU",".>}D" -",Q","D5u7~$z,","x+@K;","Sj-!8""U","","'!0*h5QX1." -"}k",".P","td?@Q._","VM""",""""," fpv-" -"Wwh3","m-AS@`,","TxS",")l1","'SSW0cdB@ZZ9","b{UsQjg2$","5\E-=Zf","","FNj8X","#d" -"l-",".'bEHrm","a0td","4=b\O_","~ArL[","V^""Eh#h" -"vn~","'""(PH","","[NuNt3y+","18uv:D","w\=pX""" -"$~,^vwlN&E","t","","TY8%j+L32","lt)NY} ","A/xc'vYrz" -"Y","qRdO","=-IsfX;I","E5+n","b/e-\.DtsC7" -" wj","","scL$","&7+)CQ:","","qNn" -"6ki?`(K:T","S';Ni","P4","B%j,.kDiI","l","fl4&v^" -"dMdiZ4=\","K1'ZZnY`","WsH0","","{#S\`Jb>","$" -"kV((5#","xV*Dc>","TeZ}EC","9!e","i)\X","G" -"YfCZ[CA?","71jR~$h'X","K[+-,C","SvfR","Iaj2","" -"UM>W","B'!""<","eF}/F","qCE0'","R/w6M","]`SK~N]`7b" -"o(:F}o","y+&W$","w ,&y","@=.e=qC","*3/*%(=R","Rj}C748o|!" -"#8ij|OlO","Qb","ldMsGXz\c","{PwuC9ORE","FW||r^|","g" -"[A(","%!3D3qN","eZO","n4Fw.:","G~wdG","" -"<","J}@>Tn","%}b-<","#","X']" -"<(&\\1G","\P.R$.~(","1G.","srP","2o_T-.-","" -":H.5","","","CxnO)2L","VX x0","u~f\*" -"~!f:so",".k[#1t*K","_H.>*^ !V","p)>#h",".0i","2=??m""" -"XZ|","8|dQzJY?",")4bHG?w%","","~TMQv!kTB`","1%$Y@W\%" -"tt$oh=3n","v}","s!]rr[<-3","T38VpO5^","]Pj(%{o'","Dyw@#Wwj" -"]>uE[a","9iF{9AEo","^ogx5","mt","V>","Po+PMC:G" -"HN`{X.","J",",_","d| ",".~","+=X" -"zUW@7","sxVm","1|="," %","g?Hjdb-nQ","f-P" -"=m","]t",",CSO)&1","ACm","e","]{" -"{4%j);c"," ?_7","R","&^g","c{(","-Vju3qlM@," -"zx","dE46kz/","~V2L`[~cH","","iG\",":" -"","J!2Gq","q\0Psz(:?m","H3=kH]=K","A"," " -"FT^g5>","M=Z%YJ!{~","v+","%LNy+#9","4|(S","1/HRtxuU|" -"{o|2oR9k","WAs=}Y","aqWf","C$s","b","$""tQz3sw`r" -"wZ","G$k>'{?q$","","f._!*","D0`S5*Kt","" -"","x#;Sir^TPo","'20#","A @V","FnJxC8@k([","=@h_> 3p" -"74,kgf","","YL~|)K5","PJ11_y",".","Yrv" -"wz:2cC_","MI4Ef","#1Uo","57cax(","73","8" -"8]tv~tau!","~uxDQ","$Duy","ej=Zs/","3KK+","5M" -"I{lsbr","%a>MvkAn|w","L*r!]","kSBkz","\)i=","L" -"5","LUq|_]#Q","8[;V","U","x]Zy\OK","[p'%yX)^q" -"-HuYPT.J[l","1C`smvf","X4PYL","n;N/N","d+l","T_+" -"ZW}ot]","\1^&/_t""" -" ","/Bwvj","<[=8VqI",">+P","Klz/u1ps+~","-xl,V" -"p","X7jx=LGW+",",","_Ua4CYT-","yW7m","w)uU$I{[4R" -"z","Aa","FXX`)%E","","a","b" -"=Kr#bxmmq*","V/w:","","5o oQ$'1","D|]6[","gb<" -"=IRc9A!=w[","0lRRcu6jyZ","#""","","","r}" -"","&K*DQvP`","","]@@1Y","zNvqD{B-.;","rRt1yu6p;'" -"m","jFJ`$Jg","Dci_7]{UF","@; kbb)" -"= 6(gd]zK","%:""rOA","]UNvls):^","bw?<7r W!J" -"tu(R1h0v-","/lc$V;:n]"," uDm$<","s}","*W;+2 [","hT6d.LvZa" -"","~-_2g?","{","m0o","~8","=msEu" -"1< <$","Lg$}N","aj","9:.=mK""_+K","YDJc5csE","V" -"l#","g^p@fb/","/ZMbc0k","xdHXp","G","wAx^t!y#f" -"M)","6x","DfqF","sX(WBL]ch","\W#=iq",">RN'%oxs&," -"=","Bp'e!","^&.Epk","4'n2X9","awF?kV","q|XH.1Wr" -"DzPOe.9N","}wkuS5E","yHGn_<@",">r","/j[","M%" -"","<.t-N+f|>","oU3{}","z","W","7nR4^" -"tlq","r8w","/Vz(6uv","jEkLh","0fM","rkGk9lclrc" -"$u)FlIAqI","]O@c","lSNG.kl4:","U@b-=j2","~" -"0b\,v'Q","z3J-E:h","=9]%E","RWKPcqV","+",",6P[qFFZ$K" -"u3x=dPE","pE"":!k","l`Em3#bX","Tbms}'gTT&",";Vu5b-H","a^UG-t" -"Qd{84Ax","(`Tj;","7","slGG6<>xe","-","O" -"X|M}","LH","7x","",">[ZI2oW","4Uvwa[x2" -"l",""">>'9""4z",">C\Rzdf","594[^oC","W",">" -"*4D4W","Nt~","(_SW","k2Z!F#C>_?","VLDE['9z@}","].G""tu" -"WqZ!&o","j+9{QIU","cKCEHb>3|","d:M\|dLl)B","h{X:g","[WQ{+wf}" -"","+","~Wu$!Ub","N","" -"Q87","v",";x4m","J h,W","kNW","oLlQ" -"tn","#kXw","dM[ ~j;","`*^Ixk","S>LD","0bgS,p\^`" -"bc0VMYJ","a)2A","oSZ2.O","_*Aua{","&i#P5um","u~ F\3?W63" -".hQfq@[","nhkC.q","""dN","op","(""U-MI|F","~!~t-5n{=S" -"y%?","bwxm-=#","T5","`ShN4 pv`e","u","vxQ/]$7^m","#","avu7D$","","|u" -"@Czp'","?""&]s7","q","n!`","vvJV}i0L3m","4!T!" -":IcC","7=;>yv#I","F4TS9","[l","RNj93iz","+np1D" -"3","","]XMt]l","`j=}D","SX:9w8","6t" -"Rx=TAx={8F]","cI","c@H","" -"l@wVdWV=~s","8","&7WIizx","z&n:yA","3hwiuXI*","/S0|=:4GG" -"Zf,5RiHf\;","neFp?",",","B","-qK!6s]","6""wTc*N?" -"|f9","","I`N" -"m","","","WS^3x","gl""""|59","O?a" -"()","\DgIL","7","gwPU|knaA;","H.z3x","RYfn","KQM","p{lpx8","ucbs3""sDU!" -"[","UG`_X","\H}tv6""%","Esp9W_j[k","fD(7\7{b<","m" -"$&XQNcSQ","'MT!bV","DeT7Z&f:V.","/Fap`","Lh{7,?;","M0KIoc:;" -"K6","*?1Q}","",";hf$sE","@V@=JVWo","Opk" -"nVIN","y"," /X","xMWy?80]2G","w","|" -"gf[","|swiB$G%","T" -"l04j)%","&","#""","lA+JV3:e>","SMV: 6","X +pBY" -"|+%ar","Rh;","{","6kzX*",":^*y)4}z","D8m7!-Ef""m" -"PNC","R6A","ZV","87-Z","LKi/wY","rdDn3_KXs" -"do&`C]foe","]0N-jN","trQ'@<]4M","d'2","=a.fbg;S" -"","","p","9sg'uSs","zUAOCiW+5E","95H" -"\bciML","","`i.-#!A0X","|BF@`+jU:U","","+9r/$S ~$N" -"ek+s?bWt","'}","}IEOS.F","","#9WN^rT","" -"9'mAm","G9Y ","XY>t(i6@s|","mXQ4g'`l?M","","yVcse2" -"","","Q`Jn.? Xe","a~53Ff","q$","Z'i" -"","N.BV2m"," 1m*Be#+","w*Cz/%I""'I","+C^Pgd","%m$nS" -"",";","F=[.EpKv","6t`,","@","" -"A","","J@K(","7ap'$mI","{$OTs/Fs0!","g.)Dd" -"6J_","FeHvZ8g","$ZI#Orb","Qp|iRxGDo3","ZVu","G" -"uv)l}""S:","W3kwn3","","3|","WD'3|TVhew","35)v0+cEz" -"+]vFKwQ}J","o\b[(Adk2%","1 @!ug'","n|:zBN(?x","n?u-","r<^IP,2" -"b* hj!pp]7","&Ck","j/O%w","4@3PW#","/*2","Y" -"PhueOrt","ce""[eY","E","","aSlch=b" -"_PRLP:","e~-51",":rM9,OcE5 ","[q23'","09j#","x+^&!kIqSt" -"g! tMLhqk","'fJ[Y9d","xQZ;<","(o;,t(0",".%'$jL","M]h*UY3\d" -";bkMy)","","sAm2",")gFe","u5=bn9a/","kB[_.5n" -"%iZ","L$","cx4gQr","""H'$lNUhY?","HR""""9K YR","q({8?aJ" -"73c"" oBx;;","G~D:vh","WMyJUiqqv","Ld^5K}","W[]58RF","^)A" -"","","`F","y*q.1eV#`","p(","}87" -"B}","qVAxg*","]h@Mxx","#e>4","[h/n>","JQi|mXd3KK" -"]K9"")L|9Kp","f$c_.R","fR""6zAyR","%3qX4","hC'vL","@5tzcN" -"","8u\'FZA5d","WZ'=3","g","+e[;","" -"4Ys!z#9PL",")K+rNIgG""@","e-j5","2qrlai_K","~JP@","'Ig" -"F#_WY?&","4x~X!<~`3'","o,1UP}SaQ","n`y","z8-nh/]J","~tQx~3v:&," -"~~&j=UzU","V'^AT","R#cw0","wra","8@Z","84?l" -"f4xf","5","&10J9*.Z,","u","9$+koqH'3w","*-" -"5M7xn","/j","","<}0%-]","+","rg" -"","8LOd","3T0J_X,$","Jmbh?'F","|m/","eidJC/" -"$H!l{","nlw5.=,",",d.@7^","#M*DR@Uv K" -"}E}*&u>!>l","]>","^Y4p","CTA89eM,D","*6QO","TkZO5Wlu" -"js'r""",":h(","dxt","LD`?`qv""s%","OJ4i","L^qos:}MS@" -"=$s+[bM",")r|7#Va{k7","J","]","7e:{V@%9 ","QaAbt&LL" -"gr}-qu","r$a<",".","","~pv#70T`^","*mf-" -"y)""Ggkm\@t",",2_A","","@d_3Nh","LE ","&_DA" -"MOD~CS:0","8-","@CtqXf2","Q","","WJ>O" -">9+[+m","{ytHorhJ","egj) 6h'tT","|('",",qzG/","=t-" -"X&P","Mx'D_",""," 1Wy4Et","VG6\?\","zux55" -""";gWk","","2","iL,zy",",&","h" -"a","16~*""+58j","c1LEVYi>!","","","=" -":;Nrfu/Vgq","0?A","sQVBo ","Kx","bP8tj*1Ikp","g" -"-W","d","BL7R+\G","","rs","iL;'yJ;" -"$S","i8)E09","V+<*","b","%s+","`Z$P" -"4$kZ|08rBa","P;2W","t3yh73pa","","W/","" -"Gukr5","PPa7|Pi","FJ","","-e>8n,_","iwUlE" -"",",ypj","S;ac","-0j","Jte-2n'd" -"45S@Ts","3p","Cz","jEpI7}znj","*[#Y$","7z)D_+A<" -"f:]*s:[LS)","Z*W24[",";j]G","1_?W$+o_xe","0h(_[","Np?'>","" -"|/m0<_`","h64#f{Y=Sx","lE7=J","Ps#i3Fc$","t\\'\","xuM:" -"fP",",uH*mK<","st25Pb,F\","WoMmepO2>","?+>F&hu0","p" -"{","Q","]4","R3YnS","i(l","" -"+o7&p>yzn","rg","=@b","","W:y*$ahX","0|*Wk-Kl.8" -"""Wc.(/","*Q]!Zy","G+inbj-:p","*Ix&s~sdRp","<)","_8Xk[,r\#)" -"F>P","M_WtMOwU","7S\>`|xB","H Mt&v","""~kk(","OJIxt-" -"6q=b7","I_@4chB(fD","cGr>h","'D ","Ej%PyZ7CtN","X2*/h0MD`D" -">+_[65C#c$","%TB9","ul>z6","?51i","GZb/U","9/]F8o<]]" -"Qf","(R""8","","k\j","%1.Yr$s","Jdf[" -"E","7Q","HpXW","""g","1B","" -"w*$","GXtzoTd","wk&C:zzzL","~K<","/3V","tWyp""5x" -"sFh,3Q$","I","Eq8Ed@)J","O:)uVYJB","9%b23(@{v","|S'g" -"gVbR","Nu}l","Iwx>~","$E(ChS`l","|2J/\#%","5d81" -"","Lw","3o].b0 ","lR=Mr","g3C^","1^^l|&V8" -"9~~fT*","s0"";sZd","W0Z=","%(d`mVK","","hIke-" -"gzx\bI'v}f","S}>L]27","6","dRAY7","","VW#46kzhC" -"IeDL#aL","]?L&>MG:x","1L`{;`r","","hd1Gm}'U","" -"{","``st1&","it0$^'*","V^","\@7a","EXA''" -"D","z|+(^!mY","b","Uv0`m","=5p@IlyjiA","DT'O","\H>|fj" -"?hp6IP",":F?m/ka<1#","6dE","S","M|u9G{*H$T",".a%.6 LPHS" -"&.-zF%@z#]","e4B=`6[!QC",">","en","jx.","uXaNdy{" -"""#@","!JE","wuI.!Gz%","","Q_PfsezC$`","Yq^T5Upwu_" -"@c+3{I7e",">qx","A@pPoF","","U.wYH6","!" -"W>&&Z`O+]^","E`0Vw\","{2*7K>j","x17:oOWA.","EQ&","+u3$U1l" -"qcmz",":v9I","E","3CgNM!Zyuu","J3CnPxlZ1\","3zAoCIzOP~" -"Q.t ","J&9n^+6%","&!:2Reo9","C","n )D=C)7g","F?)`HLn" -"2Cr3G&.?","W7K ""gLu*","/","0%G%bIpe","jV","" -"A-8E","-W;vrHa","%",";","uOHC^6!D","5}+Bo]a!xl" -"<;""","j9v,X?ggD&","Wi^P""M=j:;x|xG","E)h(gfy3:(","V]","K: e9uf=","}#'>Bna","" -"#","r%1]4-/ d","Vo","fjt$4{.","","*Z" -"AI52g","|PLgl\'6""","jIpD","p/GZ~^gb","wHB,vy","\5" -"Es7S_|","!$IlYw""","ShaDcoA67","F5oo\?","c^cVR","'0Mg<4" -"ge@PP","mYHd[",")gT=pKN","J\vE+.D","v?","%Gigv5:N" -"","Ky","r ","i","R'e","v,LJ2|" -"","f,[_@C:lN[","","Q5","*84es9L1@X","sK`" -"&","0(&^\V","Bf\'o8Tde%","r","Zz)L=3YA2","s?_ds=," -"LBq?[","A","?}9Qv",">!BI","z+!}Ha4A","Spg1[B" -",@T ;M","","?3+","nQMla^H","{","sp|" -"Vl6SPJ","SqB6mN&""1","","qaJ'bl","","" -"yv@usc","","?zAaKO5","g(:(FQ)VT","","Q" -"Pr+=+","h","","Rr)C][" -"%c%o,","X,Z>C_%""3","Kj6hh>C?)H","6n:i+G_^e8","","aBEH" -"",".e]Kn'","1ZrSG}#rZB","L69","SR{(","C&ju>,}K\x" -"I-2x""Rrc&"," b&SLKl","\y/+Y.e","3X3","[g","$7j1;du@" -"5_2""FEMGJ"," lS`fl1","4bEh'v^&9F","hzw&u`2%n","#","v|8VT" -"v_E5DH","&!pR4D$lL[","[;:n7j","",".as$S)yF3B","jL~" -"7+#U9evL.",">M{","zgYU(K","FM;M","Z{?,6E9&",">^6hf`P" -"GT/","9dGdsq",":/s+xB>","E@","`","wd" -"C/~nk","_b","F+wu\HU","=cSkL/wU-","^zE","o`2,BG8","OPV+{x;Q","6)<{bwo" -"0","#","K",":,fb){bD","N'p""0RP&=Q","IOc.+N" -"$KWlF+rHT","h[[q91~Q","","dbI<","","P_XIMZ""4L;","&k4[&rC","/nb3" -"D}#QOPR","F=","fZj>^)]\9T","HV!hK","","]Fn" -"@7`&","3/]|@A#aLk","HhR>JY4-","r/8yT3","h?Et1m","cTH7k""_5" -"M$i.O'f","g@M","{Zzd{g?","Gth","","w+~(Zp" -"hbLQD$7\-","B}h0]j +v",":.:#C",";>/7","m98u!M","i*h!V`4`Z0" -"d%^","1/","Yv""Z;-","","{","QCJyMZ^X?" -"X","iz`3{tm*cE","qF",".[","HcG46","." -"991I;pz#'{","","&R%>c","BZ-|tO","tmt?","p" -"Y9o/43","","6GCu}","iwp~f0","=^y+","qK9}a" -".yq#%CcH","8(cQf","TbxTg?+","Jho/nr!9pX","a","t2z>" -"3m","L?cHNvo81P","s'vc Q&)o","","p","zZ" -"a$","{",".z oVLjsq","7","Hg"," qo" -"2?7","X",">|s>%%A","","{~","BE}|E^yt5C" -"IZ|@_)%28u",";","V cCIZU)","","#?E>/}","6YK" -".","x^hsN*2hV","c&ot","9F%[ m$c","P.h?LE","9744" -"hG:ey4lW:*","O i","OowDOrq4","-vRlPVi'B",",","4 wqI" -"eG","uj.{b4","","D]-*3P>uw","GFNY","""" -"9""|HX6JQ","D%","* ","^v(E[Lp","=uTx+0","{r(Tp`" -"H*p!#E]","|!u#]""6'n","{,|(I","ObQ0Iwsz","*2_gQ7B7('","G" -"6I[TcL","ai:MvfPt","*eat2L","nv"," k}5","_^{\?b_.s" -"Ohe[,0[3e ","|PKyw%s^","[s]JWg"">?k","92q. ",">r$3=BQ","+%%L1N" -"}Q5L1 ","JA~yY""","55o=;qh8~*","c51/)E3snj","}XVo?R(V","x" -"hiTVAZD]",")]"," 4km[","2","l?/^bH~b","d|'J2" -"","","p@","OAQ$","/3/!Nf","\4-" -"NOef7RR6",":Pv?U'E:$","U","5eWeM","+t2B}","l^N\GsW" -"","q+c%?","mmg!","?l6GoO","tJzl!" -"k%CwZK!6R)","","['","E1h#]~fx,","0s!_so9,X>","'ZDf$" -"A&0TSD/","","nq4rPnfq","+>G","fe5>H44","jL&\c""6" -"-YV\x|YH","","","I`lz|P",";XA","" -"","6rnX#KJZ","^J>.J{","nT5","qE(4y","#u/u!&>JJ?""",".[C5b"," ##f)J","WS9e/g@","+a)" -"Ih2jwR[?","/:a","4y&!w~","XE;;^#8","N$\]0x*","" -"m","XFD","4QZ","Ny,HT|~P","x""","" -"+iL2","rHxM>^\","IB;L~Qh","","/~D$","ie/pC<~5r^" -"Kwv","vRU","","NBTr","ae=_P22-p5","@*Zo'ar" -"1","~6G""+N","B'ImQomu f","d8R","s","frrai?hD1" -"[:_@","~JC->,KA""","ar*'>",",E#%}u","+d\go|99""","E*r*+/:|5" -"n""j]1)bP","k+y0iZd9q0","5L/m3@e","i8"")(\","K~","%" -"D@9","eG","&3y8","","8(w%","me4$yFW" -"Pzx6heCkcx","gW6t{A4|",")Ul)","26","[e[d3I","i27kE;a" -"t_JN,","cS/2@'DO","22.AdL@R?a","+","%5e2OMLu","h(;96sI" -"p&o{{wgA","","","","X3Jc]tZ1","HhS","!{M-MGO<","h|`<","xrk4[~AeO" -"9-nSy","[i","tp-","]1@Dlg","kU","!j\ho" -"Z{ ':6","MU?ysE","[","Ie0?w ","d`","\R" -"N,&$gmPK","$St\V5T","cml>7k","","F}","('50I_" -"-#3&9K\reC",".2'","\F9#>c0>","QvMG","""=O","bkmKhVqr7" -"0",":#3","!K+j rf","q09E =Ea","%#e^}dyD","2,(M*s" -"e!:^8*[Y^","","/kl[(i:'vj","L()=(UDb+<","qDNH","tQ_7lG_" -"6yBv","","#r","L&1P*#","lT`DMe,9s(","q""QBkRSW2" -";RTJ","xn","}xAZH0Bf-","lZCl+s]!j","=Ex=;7I(|r","0ZbhNU,8=a" -"","uW-3ptELpa","W%TCuF*;","/`~@-C5?""","%?Naa;","nyEf;L7y!" -";f;`t*=Y6","gHn","C?F`};","QRZ1","'DJ][gy","L:=+`_1c]" -"(2G","{,B#0B-qz","zeF7","?^+.hwCe","z#Sa)","*mD =k." -"'L>=","+" -"2H!",":x{dqv","+",";SNYqc","2%","G&^\[" -"%iC9^lY>","~:WA/eT",">","RBi]B,\3|","sZ","#Y:%%a^a" -"JRlT7+1","A`gP","Blsw""Ew:;","CP>.6rQ","LO_","Yk1" -"-t=JvO","!]sr","""","01dx=pc","B""L_","OW" -"%","w1!S0","Z;c;","2i@","5:rX4Z(_j","^bJg" -"O","r{Al/PQ)","Vys AP7M","ri+QE",",","M$k" -"ZouZ3u",".eM>-[|{5","amsJ/U{Chc",".","g","nWS[P" -"*k5=","*f","/Q:`5XIKy","45","h2q" -"","B","","_a1V1","jil*(z[?{","clJeSFGmq<" -"VIMR}!$","","\BEc|J8dx","y*w=a>","f0AMfL","oi]" -"e'X'Nn","ok","N","wD[4","","+12E" -"IcrO","E-3+4I8.\","L$","","Y"," lXL(9" -"Fwf^","jPf",":%(ln%jY","","I{m#","VY.$AV`Sw]" -"W,%H2>","a^UvVH","WY","qqaI^F!Vmn","Q%H~jh4HE","d#" -"J4IvLSF","aG9E""","va","cA","le9%)9","Bwy" -"~dT3A","x$$",")o90{V_6RD","6Joh!wqezL",":1mYh","DYbPZJ" -"tB","MM?fQ=,","Gm&T1","7LYo[1c-","X8Fa>Q|","!=:" -"8b|.iQ 1W",":fQqqc","(srw\Of1","hw>twY?~","*=&","A*" -"|a","C'yLi~}C/P","[,q*4","1$B","~RLxmnvB1C","GMyO95J" -"'","S:4wN","JGp","XOgy,)gEK","(-MJ","r]3PF" -"_^{a>`72","r:lb\ZAYct","3KC$n@A","Jvn","dlA=","Wd)9" -"Jq>x~z#x","N#$-`1f","r",":","{y.","o(\" -"KmK`uJ1bG^","bqj","_aBc!gGk","E5''" -"A<>B","","~$?","qR","jY""","9CkRu hn-n" -"5""VZfg8^^G","CoC","W2.([i&\I","Z{*","}?>;2b0","|g-[","|jT:-J6Xq(""/5&H","","<" -"Y7<}Wu[","O{9c","VB\3!3","BA`R6+N","n}W+_a51+)",".Ii_&2&HO&" -"r","/bu{i/J1zZ","WiK","i9gJ;","sw}?_b~kIU","dL" -"Eg","(q=",".:-","y","i0EgI,","&FlD2" -"v6q8DS7Z","gxeT","f","Y","G!K2V4r#M+","%Q+,_0Cr\D" -"$)*2","<^","z,W4NUHmI#",",9","}^WCH","&LVLV$","vOH(n\>J*&","%'F3,l","m^`8""","AJ","3T" -"*","5Dtb_Y7V","P2+$TT","yYX09a6","k","" -"dB""5","WCsv{tz","T","","","-Cvo\" -">pM/","^H","yjbU-Y","OX","F[(~u7*P","sr," -"fY""","0yF}^yKpO","","",".BKyLg","@MPkL0" -"X","mN","8uq-{","f7}R"":oB","Me",":p" -"","sdxSg","$,_i@U4R","YJ)","J","n6=aK" -"1LmLbC3 p","/(/6r","o(?:kOC","Jj&g2","/ne >S$>EL",",FPYM|" -"","C","9[P#$inlUd","HU}","r3x","sYKa" -"kM|SP<{I]N","F""a=e0A=","X@-s","lB%","~ZN-c","|HR" -"","Ip%P'*8GO@","Qio,hHM ~","l5w*""(:K2&","","qw\P6H:" -"2x2l(eJc#T","{I8G8#L","ycDF","Ro`","9Jca","%FNZS" -"^hNiedR}0","O.w","w'Z3$+@<","n4[I#",".'1/RUvxo","ESm!JU" -"W","","Cn","Es&D","1>|qceZS","c" -"GNCOr/n4",")HhO?yRJX","w","/|3(|qM","|}E`f","" -"fl?!XY-Qye","_5Vi","[,d","u3#6","h6","dC)" -"Glvf!+","O:[K8","C&","$p^;<#::sQ","""X!{","" -"UN2N>5","rVo'{8&u=&",";9I`hl6D(","=4AS3F","])U+","orfTuN7u" -"","J@","Y=","=Ql","@5v;9L :u","`>kyY" -"xum7","","D&#,AP","{mBX$gi","V0Ov","Q" -";,IxZ" -"'*RN~A","L?I!4rw|","@'AeCfvj;Q","Pv","Ha","vnV~?:HV" -"tcl|?eN","","",";*>%","f&i","-U-e4<*hR5" -":iPd","hk","i65t(n!>0","{","n(eP5?l+[","Ch5" -"3>[N26no#r","jOoQ\6*30l","V]0Vk#","%>?","7H=f5_d","'4" -"K[","m{Vo@_esn","M{=4U&$","8","vJe$UbU]","""ts" -"f",">wm=}q%3cp","","i`'""","]Y!E!_lU`","Tnv?TV" -"i/E'","lTkgm/uQ","o","X@{#/","3>A1","/(GDFW" -"!j%5D;","-ON","mcs0(khy","A}ADG58vbA","0|Gy,S\+","3aH%" -"sy1zB8W2L6","~0~0fxi%h","xooZs{@M>","CvLa62","I1M2@f)L","C" -"vNl@!l,","M`q}(W","5p!t%H+X<","+G=$,>sG5","","6" -"@jrzq","hB=","","U&n+Ek7P`(","zj","yZm;" -"","0]","fXQm&p$md","|'""v","\E520s" -"?$6,>","nn1<6OQ","d?5^QPx","@|lQ`5m","PK^(bnW^","PU-'" -"!;""F-","}17PjCBK","VXpm""G-",">9","S,Y.+u,","];=)Tg" -"HUvnz5f","-","","aQ","Vw+>","+mZh" -"H+29W""","W","RF$9Bf$z","&","HhOu,/" -"-L]R_!1^lR","c=:t_","M{%Xk7{f","","","t.hSS`*a" -"W7BF%SamV&","\g42","9on4&:",";6?e{=m* ","r","NChdJ\&P&2" -"c2MLf_","Unr","r>RMGg;*","'wkg0","a]O={K.~5Q","D^I~","`" -"Uy!mkR!}2","Q","gX}\-I[yk5","iZ'ri","{`+KNPP","~[*:J" -"q","+7[2{2*;\R","uzHg\","4_/Ka6_","Yu$","(?n","*]1B","G7=vw`^" -"D;uQX_yoA","#2FwW`To)K","~3KaS[","KH67Rk","{iYo1ge","73/c&5tLxr" -"$oth##sNO","@q",">B5v","`X ","f6","Zk+" -"G:","R,$","8","_\s3*","$/","/d/;|" -"C","o#@;`;g","i)7BJ","Yjh7a","<9ul@i","^[fMq","EF}E(:oD" -"&y","N.u!H","+RN","u","_7\G","pcA8Yl^eZ" -"","P","3P>!","o","~5OWah","|!","Od:pvdD","-U7z7\" -"SbDX8ZJW","S>h3:C_.`","')SLc6!>v"," cg$lg)ob<","","DNwfnm$6-)" -"","Z/&la,}(W","]","DEZHzFk}=}","","&K\" -"+T","=|0","{","jw[dHnc$_","7goKy%X'","@W" -"k H2]6","2""Nc8 SM","","","$X( ","i1""Y," -"EA>g2CW,","1C","}kcAGI<+$","r","*","" -";2M]cbb1","eMs|gMag~"," ,J>PEP","","","QjR5" -"6i\s+","YXbo>+gP^(","=S(]EJFOg","-Qi","RP5{JgjnC"," 5D" -"uc","t-Nn:TOk9l","Got\.Y,Zv",";S$x,u=Wq'","~`w()Q&","x2v" -"a","^ZX<%nFNw8","IY1DK8","{","@hK","u6]5+/>`/9","'" -"V`\RI{8","po","RV4","~gMG7Ez3u-","!8e?gU\ZQ","_8KYZTm;$" -"","FX}hk6{mJ","K=xs","o","OzZau","A5,QMsO" -"/~","B1","&@0k^","d^Hm!%;ORT","^;43gD,_7=","]" -"{&t~V<,t","oOF*0gc3T%",".>","g_{]?","M!@N4hVA\i","p(3=MW(" -"8C""w{[",")$x\Jv)Ao7","cf/F?-ld","Q%>X:","@","SuF:<42" -"Y%x\q","$","/Ag{ H","#vZv|4","+","HNO5Z y$'" -"P-L,JzC\O","-}i'jpM0D","","[z","([","JXP/ {UoF" -"@vF_+Zq2","iaGO$Hz","}d)","","6ydu&","b" -"m.ze\;4_","][OZ","\VF0$e","ME>W4","","7;" -"a~X>i>;E ","=..TSN5IY","9`","o2=GZbZTQ","i0-","_b|QsOSu" -"HjInmRx&","!7LgEXKQ[",".8^bq~pRA","m)t*a}","","rC:{" -"","5+NAC_*h","","X6","z6D]]!","3o6ThM7N" -"CA+PH|CQE","#D","eH`oD0fS|8","","I^6u","wd_n""" -"1X1d<","]/^sZB]=w",",","Wz","fHSf1.twFh","afkl2/" -"]\C3&""6"," ","]","PF","H""@B","9" -"r00KOp|","-","R16F","","X~IGj F3","$c" -"bi>","d7Hv","[:.","TI$##k(mR}",".B|YdVR4;*","JM[=]J" -"Ex z+Eh7","Ja","Uw02 -","]","`]w0bi","" -"qb!1","Ft?B=#","-M","0KoI5[sQ,""","1S$T@%sz)g" -"={","","","e6;|","E""a02(pe","","1v3D}'J" -"/b0","s=Eb?5$1","Jr","t')TV+","B{/G","uQVkmvl" -"Rj","fwyByySt;","@I,S","f_i/\GFb","<3Is_%Y4","#45{aKn-" -"}","LD?8I[31}M","U,anzV&Ir","e43","T~sa:","!gIa=T{5* " -"FZza7*Vp&)","Lr","cPQo.od%80","p8","R","xI`Pj" -"gv","/@z0?z-'IS","1bYF,","v4N]0-uaV:","_R&4","t7'AUt#E" -"q""T","","V=!SO","~YGK)WWC","sm.(.Na","SBH!@5" -"wC2b=g","['mrq;+cO","xV{h","","akQtP5k","T}:cr" -":/J","!",":/Hq""9","","6=Z_kL","M>X3F?" -"'^n/U","%&uJd#SK","6G`xh8MFC","","M%+w|M{~","d>8T" -"Uk","Y#y)})5X","a_","ZMJ(.l-]","/K*","M.G!:","7}}%@I",">_0][,","DT","#\&" -"-)G.Qy","UI,","3b]o0h*<[","q9\","yYr+","-Q|VW" -"WV","62ldDw:","Tg>","|","Ijn4B","rH6EgcQ" -"5\2T/9<","U6wKfM@8","M:@wF-","./|`ZF^^'I","""57yS{3G","B" -"n_jV","?i}C\*>E9 ","JS:;yR" -"M","E@5;_","U","96QR6#!9!-","uCV1e(Y6No","+VQ8)^""" -"E',5Ez","qzbShb",",;0d~M","vv,fb:E",".tAz98n",">S" -"","-BiHqx","6$J.","((v","pih.*F","}IRC""" -"Mr~v","xU`","","k$XN*J","^\w","w." -"","j>J~kiIo$L","gm","NIB","1&UB:9aw-s","F.xAr1nE" -"rz5'y'","4/>AH","~a","g*GAfy","9XLfL!Ern","vW6SQ#lMMW" -"|uu`V1*","In'F7yn","lkaqoN2:Q0","","`","/U""Y7mB9+e" -"R5","x?Z,FT~*H","K~I","80;bt9:5$H","W",")." -">.Lpxd","rWr4`J*&","n,\By$-8+",":J",">\GI","h/zpf%-" -"hwg<1\/S/","d4<","d{D*","M","77fki~8Bi","" -"IY8I]=8p+Z","Ub=.'","$^1a]]R","LI6!","h","'T/&hi" -"qjF?cG};:","\38>Hc","e^+-`jJ","}","K,VXVZ~B1N","oES8" -"#","","P6\MW|~V3\","BokP#","E*'Ej3GO*8","rJ" -"W}su>","rcQcJt{1}N","c=0z@m(y)F","moR^X","0\XD","Rr8$,;6P)" -"::)V6Q|c|B","{v","tU","tH.f4|%cAO",",dA)w!uL*","-fY","_*[~W7T","f" -"QL","",")>X/Y","wgXEuL\","~T","![8uQ" -"","","M$s{#<'~","|d][","LJk","Osf" -"!oH","uS","Z7","QSz/","yXF{X@%","~" -" {","B1N{6swX?","T'}@2q4y","I","k","G!KgP;Y" -"W8D-","L+Fr","~[C,U8K_f`","+wO2","?Yy7","" -"r#,\%Co4@","#[Y0jL?",",D","o^"," !(paQF","" -"","3:d@).{","bmURnX0'i_","-p/*(@"," ","Vo" -"","P6Xv/7 ","&","","ohO38@""","=J):z" -"RX","","!7M?NQ","Bzi"," s;2","Dt~Kf%}" -"","OQV:t","LN=RBNVy H","NOh","3","" -"`uO","\-t)wk/hPR","~uQ+{cEI","rS_+&$a","kMJ7fC!","=G","{kp)Aw","joB","d^/e0YSrl" -"IMv$O","Atc5kK","0nTfQWz","KG41X^" -"yxWd$20qt(","gVgwi","?f","Pa~""","=" -"P&w!JO6","zx`D"," a1B&","h:_(Ie;" -"jk/#.d","&iDBe 8","O8TL8O@","WuBmy;hH[","","HIK4+!I" -"Y[;?j?z[4k","z3","","Ffxe-","4]6d/2Bd","03rugQ" -"PUL9FtNL","yaYi=5","#m","Xi","&0S4i)T9`b","Kp9h@" -"7i]"">++x}]","`%Jm*u1","`{C&6]","#oQ","UXA :F","rQC61<" -"`","e_t,T","","FMjz","K};tdY(yNa","" -"K0tM=E!>#","Q.y","2s;R51p[DV"," 8i","f","!" -"H{""Fw)^j!6","th","|","{lA^QA=US",".IZr=xd+","Tn" -"|>K-[k9pE","Z]n0","K,3s{B","@l","{j3f$r>S","#2ti6" -"l","j.HeasNPH","x&""<","iR2FT4Q>^","jnHi""u(J5=",";X" -"2_,ch`","oW'","DIGCYT{","{w"";' b*","","U<+}?2EG" -"A@lAq","00KZ]<","v[c&[","aT.h"," n!?6",":6" -"7+)wQHw#b","K%ALf1ug&","h7","5","rAJWl-E","I>""]KkD" -"L3}o,!l-A6","~tE.","2W?D","zWahMD","hD3MX{h","<$Z" -"[l8","|h?fPS?H","kGMqLEX","""pOx0","I/'o<8_","\\","^q5y","","X" -"6dnMzlxE~;","3h","&S","RV?wX_","lH)Un$v>","A0q;; Rg:" -"x46Me","KRJ)E6YV","S6","NxF3Xe`","5Kx@+g'","4{rXn7d;" -"jKKh]Nbd","mwvt$)nL_#","]V$n","z7lNQH2","MF""9;",",`k*5o" -"YOq","GCj^va","u@G7Qn","0\7","|","" -"h]","ntAfy7zi","0_y*@Oe~","B9KE","7;","B@\wW" -"M","n^","5%e$m'$)ZU","","Mt?]e,","N|Z1Q" -"{VEwFi).r","bQcI3","m.","pS(<","r.R;yx,5a","[xjE" -"kd(","UF:X(]P","vf(dV0-","UT#","y!Ut{q#","^c}e|3;H" -"$'A+","`","H@D#}2AP^q","\-n","Lc`mb<","|-D2c5V" -"",";^-'&MU","9{o}^(41]","<`","96s.?>L1%5","+?i" -"o~^","g\Mz<)(","Y","Vi_?]4!MFi",",","" -"Knc#xK'G","I_\f)<","xJ!","x*",".w~s$o,","""3P#","/>9b1@Mz","]G|/","e4UsHl$","J^YR2/s1@W","Gls\#$hDF" -"bh0zBn","","G#Nvg","}9%e=a\","","x""T1/q" -"]/0Lp","]5L_ [*","u To^","","{Q@","kyP6.uHY1" -"QUXjd","""1BRv*-5",":b6>",")fDs","~)._.","VSyuG`#1","`4:e!Hr#(i","T%KP",");`%R","t" -"[x>G'","~Y*LU","R\mx;}","?Khr#?yh]","pE+8\k.&#I","." -"!xee","XjR\ w","]Fg(2>D\7","9f[UR","|v","vYdyU7j" -"<;(zk","md@","]K1aK(S","mm","""$3|])p","T>4X.N}e" -"s,]","}i;vBr+","+pr-" -"\Gf" -"T-b","13lg~d@T",">]HMXT","J*bQi.Gp","+","" -"[= ?x]","f9d_.;","~u/WuJ","=B7","8(G';V <\","jJ1X","j.","%sS","St+&}b;qZ" -"XA*","","28@TX`?n","jbJg0Xa#:z","p9%60","zpD" -"","z","vbB1e","?q","F","MjTNN" -"TD;E9uXF","","XI6Qh'\$2b","]jBMoDA","R?Ezs","Qc" -"'",":gj","i","N8$7","}^"," Cd%@R<" -"&SOX7e9u9N","Tv2yH-|B","/[\*B","FH7%T]r|<","'Yl","" -"o{0","C'","*","cv","v}","" -"NuGy+x1!N","","","b*","zB,]5La","" -"",".","","y[<^oc","","`#k#w[" -"Qjb}22","B^D6o*_Loh","fVyvi)@","","~G","3]N_" -"Z~|K|f6|Oa","`WE>(PJuu","B@wzpD;","x","=",";tgXMUNF<" -">|(","J]GI""5{0.3","\wQ[w`D","9079X3NUW","z$|",":lM)w" -"@/c%","93ZJ.+=\mk","5ui_N%/","]","$@o",";!FlH4$" -"xcgXX","20#","PSUlab","Vkf","fW8","0A#>""nd" -"De!^",",}qqc,RB/k","c4\","sn#",";","=bgp" -"1Ryoy","*oUI+","Cd","50{0pm,n6","kt[z","" -"yt**","fy7%","Ie320&NhS0","3DP","Ccg{",">E1" -"-.Z!:^Y6v","M5/B","%~*`Z","C8CyOD","&q]","~" -"'pgL5/0gpI",",m[ r""gGx","f7C/N""","","*3#*;xD)!","B\" -"LKi","x9BW^","\O-9"," !D/zsCKd","|INL>4","o}:y" -"MA~kMkwsRl","p","u}","EJ](7|i!","s'2","Ds1-M`Uj""X" -"qn)dj.","""er","","HAR&x*1","zUzu!tI","QYwB&>-0)" -"y$","ZK_f~&fq(b","+.","%]ha?X","D:a7ypd","GU" -"","","$/U","","4P}f]Pd","`dB2>3XAoP" -"|Xu","Sp|?]l","",":I","c4b<0","!a7-'r,o$" -")","fUNO'@:P-t","@{@)(fl|","\HJ;|m.","3_Px_","Y]WHtd`a" -"@[+k?*#QT","?","Z]zwW: ","+)1F","pc?3Sigt","" -"lk1@:","cd\'",",dEl?","H4~|","YGq~g","" -"LSt3OV","v]1Uu*{","G(y-TmBL>","dtk","LNq","" -"Bi3Id9R{","3l""?\","","C@|_RD/w21","$=BDA8" -"`[ Y.0","z;{","+n",":,z","]lo;zSh","X`!HCUXJ/H" -"vR83Ib","{g","qm->~!","`nBroYF#K","|5",";PF\rfd" -"","}}","R<`,Ri",",","=T"," P-)" -"","b[b89C_I","K^!0","hP","{r8>N","D::C~n" -"hdyQK","""07:jB{",",20G?d","\OEvWr","+u","3L`%dOs$#3" -"qDJ[=Xp",")<+e]u","!_mwrd#^R""","[FvtG","i:","C~" -"EDmN","","@[i|:2",":M'yof 'n","(","T" -". 50`Ia~Qe","@!&<\Q","=","2M|$.7_w","]# fQ""T","ZVf" -"]t","+1& D.u",")!:aE'u~}0","4HA","d#qoS","O}v]3;v" -"C","^3.","BRU1ac.","c#pHg#n!;","lQ","OF^BbQ+nJ" -"OxC%[]V?<","#p8ur9g","f%t.+viSba","u;)K:.1~","a","<2~83g^" -"de:$I","y","+|Y^r8!]c","","9Xv?t{1PN}(","0C","]0T","5}D|v\\","=|K","+5*n","!ElE^e","4.""" -"sT4-'1t","H'l","^y!~O","=Ou5l,h","?","'1r'9p

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

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

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

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

+
+ +
+Installed Extensions + +|Extension Name|Extension Id|Version| +|---|---|---| +{5} +
diff --git a/resources/report_issue_user_settings.json b/resources/report_issue_user_settings.json new file mode 100644 index 000000000000..7e034651c46d --- /dev/null +++ b/resources/report_issue_user_settings.json @@ -0,0 +1,99 @@ +{ + "initialize": false, + "pythonPath": "placeholder", + "onDidChange": false, + "defaultInterpreterPath": "placeholder", + "defaultLS": false, + "envFile": "placeholder", + "venvPath": "placeholder", + "venvFolders": "placeholder", + "activeStateToolPath": "placeholder", + "condaPath": "placeholder", + "pipenvPath": "placeholder", + "poetryPath": "placeholder", + "pixiToolPath": "placeholder", + "devOptions": false, + "globalModuleInstallation": false, + "languageServer": true, + "languageServerIsDefault": false, + "logging": true, + "useIsolation": false, + "changed": false, + "_pythonPath": false, + "_defaultInterpreterPath": false, + "workspace": false, + "workspaceRoot": false, + "linting": { + "enabled": true, + "cwd": "placeholder", + "flake8Args": "placeholder", + "flake8CategorySeverity": false, + "flake8Enabled": true, + "flake8Path": "placeholder", + "ignorePatterns": false, + "lintOnSave": true, + "maxNumberOfProblems": false, + "banditArgs": "placeholder", + "banditEnabled": true, + "banditPath": "placeholder", + "mypyArgs": "placeholder", + "mypyCategorySeverity": false, + "mypyEnabled": true, + "mypyPath": "placeholder", + "pycodestyleArgs": "placeholder", + "pycodestyleCategorySeverity": false, + "pycodestyleEnabled": true, + "pycodestylePath": "placeholder", + "prospectorArgs": "placeholder", + "prospectorEnabled": true, + "prospectorPath": "placeholder", + "pydocstyleArgs": "placeholder", + "pydocstyleEnabled": true, + "pydocstylePath": "placeholder", + "pylamaArgs": "placeholder", + "pylamaEnabled": true, + "pylamaPath": "placeholder", + "pylintArgs": "placeholder", + "pylintCategorySeverity": false, + "pylintEnabled": false, + "pylintPath": "placeholder" + }, + "analysis": { + "completeFunctionParens": true, + "autoImportCompletions": true, + "autoSearchPaths": "placeholder", + "stubPath": "placeholder", + "diagnosticMode": true, + "extraPaths": "placeholder", + "useLibraryCodeForTypes": true, + "typeCheckingMode": true, + "memory": true, + "symbolsHierarchyDepthLimit": false + }, + "testing": { + "cwd": "placeholder", + "debugPort": true, + "promptToConfigure": true, + "pytestArgs": "placeholder", + "pytestEnabled": true, + "pytestPath": "placeholder", + "unittestArgs": "placeholder", + "unittestEnabled": true, + "autoTestDiscoverOnSaveEnabled": true, + "autoTestDiscoverOnSavePattern": "placeholder" + }, + "terminal": { + "activateEnvironment": true, + "executeInFileDir": "placeholder", + "launchArgs": "placeholder", + "activateEnvInCurrentTerminal": false + }, + "tensorBoard": { + "logDirectory": "placeholder" + }, + "experiments": { + "enabled": true, + "optInto": true, + "optOutFrom": true + } +} diff --git a/resources/walkthrough/create-environment.svg b/resources/walkthrough/create-environment.svg new file mode 100644 index 000000000000..bb48e1b16711 --- /dev/null +++ b/resources/walkthrough/create-environment.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/create-notebook.svg b/resources/walkthrough/create-notebook.svg new file mode 100644 index 000000000000..05dadc0cc6de --- /dev/null +++ b/resources/walkthrough/create-notebook.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/data-science.svg b/resources/walkthrough/data-science.svg new file mode 100644 index 000000000000..506bed2161b1 --- /dev/null +++ b/resources/walkthrough/data-science.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/environments-info.md b/resources/walkthrough/environments-info.md new file mode 100644 index 000000000000..7bdc61a96e2e --- /dev/null +++ b/resources/walkthrough/environments-info.md @@ -0,0 +1,10 @@ +## Python Environments + +Create Environment Dropdown + +Python virtual environments are considered a best practice in Python development. A virtual environment includes a Python interpreter and any packages you have installed into it, such as numpy or Flask. + +After you create a virtual environment using the **Python: Create Environment** command, you can install packages into the environment. +For example, type `python -m pip install numpy` in an activated terminal to install `numpy` into the environment. + +🔍 Check out our [docs](https://aka.ms/pythonenvs) to learn more. diff --git a/resources/walkthrough/install-python-linux.md b/resources/walkthrough/install-python-linux.md new file mode 100644 index 000000000000..78a12870799f --- /dev/null +++ b/resources/walkthrough/install-python-linux.md @@ -0,0 +1,22 @@ +# Install Python on Linux + +To install the latest version of Python on [Debian-based Linux distributions](https://www.debian.org/), you can create a new terminal (Ctrl + Shift + `) and run the following commands: + + +``` +sudo apt-get update +sudo apt-get install python3 python3-venv python3-pip +``` + +For [Fedora-based Linux distributions](https://getfedora.org/), you can run the following: + +``` +sudo dnf install python3 +``` + +To verify if Python was successfully installed, run the following command in the terminal: + + +``` +python3 --version +``` diff --git a/resources/walkthrough/install-python-macos.md b/resources/walkthrough/install-python-macos.md new file mode 100644 index 000000000000..470d682d4eb2 --- /dev/null +++ b/resources/walkthrough/install-python-macos.md @@ -0,0 +1,15 @@ +# Install Python on macOS + +If you have [Homebrew](https://brew.sh/) installed, you can install Python by running the following command in the terminal (Ctrl + Shift + `): + +``` +brew install python3 +``` + +If you don't have Homebrew, you can download a Python installer for macOS from [python.org](https://www.python.org/downloads/mac-osx/). + +To verify if Python was successfully installed, run the following command in the terminal: + +``` +python3 --version +``` diff --git a/resources/walkthrough/install-python-windows-8.md b/resources/walkthrough/install-python-windows-8.md new file mode 100644 index 000000000000..f25f2f7d024d --- /dev/null +++ b/resources/walkthrough/install-python-windows-8.md @@ -0,0 +1,15 @@ +## Install Python on Windows + +If you don't have Python installed on your Windows machine, you can install it [from python.org](https://www.python.org/downloads). + +To verify it's installed, create a new terminal (Ctrl + Shift + `) and try running the following command: + +``` +python --version +``` + +You should see something similar to the following: +``` +Python 3.9.5 +``` +For additional information about using Python on Windows, see [Using Python on Windows at Python.org](https://docs.python.org/3.10/using/windows.html). diff --git a/resources/walkthrough/interactive-window.svg b/resources/walkthrough/interactive-window.svg new file mode 100644 index 000000000000..83446ed8e66a --- /dev/null +++ b/resources/walkthrough/interactive-window.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/learnmore.svg b/resources/walkthrough/learnmore.svg new file mode 100644 index 000000000000..c5fd67e75471 --- /dev/null +++ b/resources/walkthrough/learnmore.svg @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/walkthrough/open-folder.svg b/resources/walkthrough/open-folder.svg new file mode 100644 index 000000000000..1615718a83dd --- /dev/null +++ b/resources/walkthrough/open-folder.svg @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/play-button-dark.png b/resources/walkthrough/play-button-dark.png new file mode 100644 index 000000000000..113ad62b87c2 Binary files /dev/null and b/resources/walkthrough/play-button-dark.png differ diff --git a/resources/walkthrough/python-interpreter.svg b/resources/walkthrough/python-interpreter.svg new file mode 100644 index 000000000000..0f6e262321ec --- /dev/null +++ b/resources/walkthrough/python-interpreter.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/walkthrough/rundebug2.svg b/resources/walkthrough/rundebug2.svg new file mode 100644 index 000000000000..6d1fe753cc4f --- /dev/null +++ b/resources/walkthrough/rundebug2.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/schemas/conda-environment.json b/schemas/conda-environment.json index 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 index db514cab1452..9e97c5c48857 100644 --- a/src/client/activation/activationManager.ts +++ b/src/client/activation/activationManager.ts @@ -4,31 +4,62 @@ 'use strict'; import { inject, injectable, multiInject } from 'inversify'; -import { TextDocument, workspace } from 'vscode'; +import { TextDocument } from 'vscode'; import { IApplicationDiagnostics } from '../application/types'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; +import { IActiveResourceService, IDocumentManager, IWorkspaceService } from '../common/application/types'; import { PYTHON_LANGUAGE } from '../common/constants'; -import { traceDecorators } from '../common/logger'; -import { IDisposable, Resource } from '../common/types'; +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 { IInterpreterService } from '../interpreter/contracts'; -import { IExtensionActivationManager, IExtensionActivationService } from './types'; +import { traceDecoratorError } from '../logging'; +import { sendActivationTelemetry } from '../telemetry/envFileTelemetry'; +import { IExtensionActivationManager, IExtensionActivationService, IExtensionSingleActivationService } from './types'; @injectable() export class ExtensionActivationManager implements IExtensionActivationManager { + public readonly activatedWorkspaces = new Set(); + + protected readonly isInterpreterSetForWorkspacePromises = new Map>(); + private readonly disposables: IDisposable[] = []; + private docOpenedHandler?: IDisposable; - private readonly activatedWorkspaces = new Set(); + constructor( - @multiInject(IExtensionActivationService) private readonly activationServices: IExtensionActivationService[], + @multiInject(IExtensionActivationService) private activationServices: IExtensionActivationService[], + @multiInject(IExtensionSingleActivationService) + private singleActivationServices: IExtensionSingleActivationService[], @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService, @inject(IApplicationDiagnostics) private readonly appDiagnostics: IApplicationDiagnostics, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService - ) { } + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IFileSystem) private readonly fileSystem: IFileSystem, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + ) {} - public dispose() { + private filterServices() { + if (!this.workspaceService.isTrusted) { + this.activationServices = this.activationServices.filter( + (service) => service.supportedWorkspaceTypes.untrustedWorkspace, + ); + this.singleActivationServices = this.singleActivationServices.filter( + (service) => service.supportedWorkspaceTypes.untrustedWorkspace, + ); + } + if (this.workspaceService.isVirtualWorkspace) { + this.activationServices = this.activationServices.filter( + (service) => service.supportedWorkspaceTypes.virtualWorkspace, + ); + this.singleActivationServices = this.singleActivationServices.filter( + (service) => service.supportedWorkspaceTypes.virtualWorkspace, + ); + } + } + + public dispose(): void { while (this.disposables.length > 0) { const disposable = this.disposables.shift()!; disposable.dispose(); @@ -38,33 +69,65 @@ export class ExtensionActivationManager implements IExtensionActivationManager { this.docOpenedHandler = undefined; } } - public async activate(): Promise { + + public async activate(startupStopWatch: StopWatch): Promise { + this.filterServices(); await this.initialize(); - await this.activateWorkspace(this.getActiveResource()); - await this.autoSelection.autoSelectInterpreter(undefined); + + // Activate all activation services together. + + await Promise.all([ + ...this.singleActivationServices.map((item) => item.activate()), + this.activateWorkspace(this.activeResourceService.getActiveResource(), startupStopWatch), + ]); } - @traceDecorators.error('Failed to activate a workspace') - public async activateWorkspace(resource: Resource) { + + @traceDecoratorError('Failed to activate a workspace') + public async activateWorkspace(resource: Resource, startupStopWatch?: StopWatch): Promise { + const folder = this.workspaceService.getWorkspaceFolder(resource); + resource = folder ? folder.uri : undefined; const key = this.getWorkspaceKey(resource); if (this.activatedWorkspaces.has(key)) { return; } this.activatedWorkspaces.add(key); - // Get latest interpreter list in the background. - this.interpreterService.getInterpreters(resource).ignoreErrors(); - await this.autoSelection.autoSelectInterpreter(resource); - await Promise.all(this.activationServices.map(item => item.activate(resource))); + 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); } - protected async initialize() { + + public async initialize(): Promise { this.addHandlers(); this.addRemoveDocOpenedHandlers(); } - protected addHandlers() { + + 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() { + + protected addRemoveDocOpenedHandlers(): void { if (this.hasMultipleWorkspaces()) { if (!this.docOpenedHandler) { this.docOpenedHandler = this.documentManager.onDidOpenTextDocument(this.onDocOpened, this); @@ -76,11 +139,14 @@ export class ExtensionActivationManager implements IExtensionActivationManager { this.docOpenedHandler = undefined; } } - protected onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, delete its key - const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspaceKey(workspaceFolder.uri)); + + 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); + const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter((item) => workspaceKeys.indexOf(item) < 0); if (activatedWkspcFoldersRemoved.length > 0) { for (const folder of activatedWkspcFoldersRemoved) { this.activatedWorkspaces.delete(folder); @@ -88,33 +154,12 @@ export class ExtensionActivationManager implements IExtensionActivationManager { } this.addRemoveDocOpenedHandlers(); } - protected hasMultipleWorkspaces() { - return this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders!.length > 1; - } - protected onDocOpened(doc: TextDocument) { - if (doc.languageId !== PYTHON_LANGUAGE) { - return; - } - const key = this.getWorkspaceKey(doc.uri); - // If we have opened a doc that does not belong to workspace, then do nothing. - if (key === '' && this.workspaceService.hasWorkspaceFolders) { - return; - } - if (this.activatedWorkspaces.has(key)) { - return; - } - const folder = this.workspaceService.getWorkspaceFolder(doc.uri); - this.activateWorkspace(folder ? folder.uri : undefined).ignoreErrors(); + + protected hasMultipleWorkspaces(): boolean { + return (this.workspaceService.workspaceFolders?.length || 0) > 1; } - protected getWorkspaceKey(resource: Resource) { + + protected getWorkspaceKey(resource: Resource): string { return this.workspaceService.getWorkspaceFolderIdentifier(resource, ''); } - private getActiveResource(): Resource { - if (this.documentManager.activeTextEditor && !this.documentManager.activeTextEditor.document.isUntitled) { - return this.documentManager.activeTextEditor.document.uri; - } - return Array.isArray(this.workspaceService.workspaceFolders) && workspace.workspaceFolders!.length > 0 - ? workspace.workspaceFolders![0].uri - : undefined; - } } diff --git a/src/client/activation/activationService.ts b/src/client/activation/activationService.ts deleted file mode 100644 index b011700dc1cb..000000000000 --- a/src/client/activation/activationService.ts +++ /dev/null @@ -1,195 +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 { LSControl, LSEnabled } from '../common/experimentGroups'; -import '../common/extensions'; -import { traceError } from '../common/logger'; -import { IConfigurationService, IDisposableRegistry, IExperimentsManager, IOutputChannel, IPersistentStateFactory, IPythonSettings, Resource } from '../common/types'; -import { swallowExceptions } from '../common/utils/decorators'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { IExtensionActivationService, ILanguageServerActivator, LanguageServerActivator } from './types'; - -const jediEnabledSetting: keyof IPythonSettings = 'jediEnabled'; -const workspacePathNameForGlobalWorkspaces = ''; -type ActivatorInfo = { jedi: boolean; activator: ILanguageServerActivator }; - -@injectable() -export class LanguageServerExtensionActivationService implements IExtensionActivationService, Disposable { - private lsActivatedWorkspaces = new Map(); - private currentActivator?: ActivatorInfo; - private jediActivatedOnce: boolean = false; - private readonly workspaceService: IWorkspaceService; - private readonly output: OutputChannel; - private readonly appShell: IApplicationShell; - private readonly lsNotSupportedDiagnosticService: IDiagnosticsService; - private resource!: Resource; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IPersistentStateFactory) private stateFactory: IPersistentStateFactory, - @inject(IExperimentsManager) private readonly abExperiments: IExperimentsManager) { - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.output = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.lsNotSupportedDiagnosticService = this.serviceContainer.get( - IDiagnosticsService, - LSNotSupportedDiagnosticServiceId - ); - const disposables = serviceContainer.get(IDisposableRegistry); - disposables.push(this); - disposables.push(this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); - disposables.push(this.workspaceService.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); - } - - public async activate(resource: Resource): Promise { - let jedi = this.useJedi(); - if (!jedi) { - if (this.lsActivatedWorkspaces.has(this.getWorkspacePathKey(resource))) { - return; - } - const diagnostic = await this.lsNotSupportedDiagnosticService.diagnose(undefined); - this.lsNotSupportedDiagnosticService.handle(diagnostic).ignoreErrors(); - if (diagnostic.length) { - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED); - jedi = true; - } - } else { - if (this.jediActivatedOnce) { - return; - } - this.jediActivatedOnce = true; - } - - this.resource = resource; - await this.logStartup(jedi); - let activatorName = jedi ? LanguageServerActivator.Jedi : LanguageServerActivator.DotNet; - let activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); - this.currentActivator = { jedi, activator }; - - try { - await activator.activate(resource); - if (!jedi) { - this.lsActivatedWorkspaces.set(this.getWorkspacePathKey(resource), activator); - } - } catch (ex) { - if (jedi) { - return; - } - //Language server fails, reverting to jedi - if (this.jediActivatedOnce) { - return; - } - this.jediActivatedOnce = true; - jedi = true; - await this.logStartup(jedi); - activatorName = LanguageServerActivator.Jedi; - activator = this.serviceContainer.get(ILanguageServerActivator, activatorName); - this.currentActivator = { jedi, activator }; - await activator.activate(resource); - } - } - - public dispose() { - if (this.currentActivator) { - this.currentActivator.activator.dispose(); - } - } - @swallowExceptions('Switch Language Server') - public async trackLangaugeServerSwitch(jediEnabled: boolean): Promise { - const state = this.stateFactory.createGlobalPersistentState('SWITCH_LS', undefined); - if (typeof state.value !== 'boolean') { - await state.updateValue(jediEnabled); - return; - } - if (state.value !== jediEnabled) { - await state.updateValue(jediEnabled); - const message = jediEnabled ? 'Switch to Jedi from LS' : 'Switch to LS from Jedi'; - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_SWITCHED, undefined, { change: message }); - } - } - - /** - * Checks if user has not manually set `jediEnabled` setting - * @param resource - * @returns `true` if user has NOT manually added the setting and is using default configuration, `false` if user has `jediEnabled` setting added - */ - public isJediUsingDefaultConfiguration(resource?: Uri): boolean { - const settings = this.workspaceService.getConfiguration('python', resource).inspect('jediEnabled'); - if (!settings) { - traceError('WorkspaceConfiguration.inspect returns `undefined` for setting `python.jediEnabled`'); - return false; - } - return (settings.globalValue === undefined && settings.workspaceValue === undefined && settings.workspaceFolderValue === undefined); - } - - /** - * Checks if user is using Jedi as intellisense - * @returns `true` if user is using jedi, `false` if user is using language server - */ - public useJedi(): boolean { - if (this.isJediUsingDefaultConfiguration()) { - if (this.abExperiments.inExperiment(LSEnabled)) { - return false; - } - // Send telemetry if user is in control group - this.abExperiments.sendTelemetryIfInExperiment(LSControl); - } - const configurationService = this.serviceContainer.get(IConfigurationService); - const enabled = configurationService.getSettings(this.resource).jediEnabled; - this.trackLangaugeServerSwitch(enabled).ignoreErrors(); - return enabled; - } - - protected onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, dispose its activator - const workspaceKeys = this.workspaceService.workspaceFolders!.map(workspaceFolder => this.getWorkspacePathKey(workspaceFolder.uri)); - const activatedWkspcKeys = Array.from(this.lsActivatedWorkspaces.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); - if (activatedWkspcFoldersRemoved.length > 0) { - for (const folder of activatedWkspcFoldersRemoved) { - this.lsActivatedWorkspaces.get(folder)!.dispose(); - this.lsActivatedWorkspaces!.delete(folder); - } - } - } - - private async logStartup(isJedi: boolean): Promise { - 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).executeCommand('workbench.action.reloadWindow'); - } - } - private getWorkspacePathKey(resource: Resource): string { - return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); - } -} 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(); + 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 { + return this.didChange.event; + } + + public dispose(): void { + this.didChange.dispose(); + } + + @traceDecoratorError('Failed to get analysis options') + public async getAnalysisOptions(): Promise { + 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 { + return undefined; + } +} + +export abstract class LanguageServerAnalysisOptionsWithEnv extends LanguageServerAnalysisOptionsBase { + protected disposables: Disposable[] = []; + private envPythonPath: string = ''; + + protected constructor( + private readonly envVarsProvider: IEnvironmentVariablesProvider, + lsOutputChannel: ILanguageServerOutputChannel, + workspace: IWorkspaceService, + ) { + super(lsOutputChannel, workspace); + } + + public async initialize(_resource: Resource, _interpreter: PythonEnvironment | undefined) { + const disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); + this.disposables.push(disposable); + } + + public dispose(): void { + super.dispose(); + this.disposables.forEach((d) => d.dispose()); + } + + protected async getEnvPythonPath(): Promise { + const vars = await this.envVarsProvider.getEnvironmentVariables(); + this.envPythonPath = vars.PYTHONPATH || ''; + return this.envPythonPath; + } + + @debounceSync(1000) + protected onEnvVarChange(): void { + this.notifyifEnvPythonPathChanged().ignoreErrors(); + } + + protected async notifyifEnvPythonPathChanged(): Promise { + 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 { + const lsType = await getDefaultLanguageServer(extensions); + serviceManager.addSingletonInstance( + IDefaultLanguageServer, + new DefaultLanguageServer(lsType), + ); +} + +async function getDefaultLanguageServer(extensions: IExtensions): Promise { + if (extensions.getExtension(PYLANCE_EXTENSION_ID)) { + return LanguageServerType.Node; + } + + return LanguageServerType.Jedi; +} diff --git a/src/client/activation/common/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 { + 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('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(); + + 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 { + return this.pylanceInstallCompletedDeferred.promise; + } + + public async handleLanguageServerChange(lsType: LanguageServerType | undefined): Promise { + 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 { + // 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 { + 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/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 { + 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('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/jedi.ts b/src/client/activation/jedi.ts deleted file mode 100644 index e9189ffee6f9..000000000000 --- a/src/client/activation/jedi.ts +++ /dev/null @@ -1,122 +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, Resource } from '../common/types'; -import { IShebangCodeLensProvider } from '../interpreter/contracts'; -import { IServiceContainer, IServiceManager } from '../ioc/types'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { PythonCompletionItemProvider } from '../providers/completionProvider'; -import { PythonDefinitionProvider } from '../providers/definitionProvider'; -import { PythonHoverProvider } from '../providers/hoverProvider'; -import { 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 { ITestManagementService } from '../testing/types'; -import { BlockFormatProviders } from '../typeFormatters/blockFormatProvider'; -import { OnTypeFormattingDispatcher } from '../typeFormatters/dispatcher'; -import { OnEnterFormatter } from '../typeFormatters/onEnterFormatter'; -import { WorkspaceSymbols } from '../workspaceSymbols/main'; -import { ILanguageServerActivator } from './types'; - -@injectable() -export class JediExtensionActivator implements ILanguageServerActivator { - private readonly context: IExtensionContext; - private jediFactory?: JediFactory; - private readonly documentSelector: DocumentFilter[]; - constructor(@inject(IServiceManager) private serviceManager: IServiceManager) { - this.context = this.serviceManager.get(IExtensionContext); - this.documentSelector = PYTHON; - } - - public async activate(_resource: Resource): Promise { - if (this.jediFactory) { - throw new Error('Jedi already started'); - } - 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) - ) - ); - - 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); - 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).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(ITestManagementService); - testManagementService - .activate(symbolProvider) - .catch(ex => this.serviceManager.get(ILogger).logError('Failed to activate Unit Tests', ex)); - } - - public dispose(): 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 { + // Just run the language server using a module + const lsScriptPath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'run-jedi-language-server.py'); + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const serverOptions: ServerOptions = { + command: interpreter ? interpreter.path : 'python', + args: [lsScriptPath], + }; + + return new LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); + } +} diff --git a/src/client/activation/jedi/languageClientMiddleware.ts b/src/client/activation/jedi/languageClientMiddleware.ts new file mode 100644 index 000000000000..c8bb99629946 --- /dev/null +++ b/src/client/activation/jedi/languageClientMiddleware.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceContainer } from '../../ioc/types'; +import { LanguageClientMiddleware } from '../languageClientMiddleware'; +import { LanguageServerType } from '../types'; + +export class JediLanguageClientMiddleware extends LanguageClientMiddleware { + public constructor(serviceContainer: IServiceContainer, serverVersion?: string) { + super(serviceContainer, LanguageServerType.Jedi, serverVersion); + } +} diff --git a/src/client/activation/jedi/languageServerProxy.ts b/src/client/activation/jedi/languageServerProxy.ts new file mode 100644 index 000000000000..d7ffe8328b9e --- /dev/null +++ b/src/client/activation/jedi/languageServerProxy.ts @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import '../../common/extensions'; +import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient/node'; + +import { ChildProcess } from 'child_process'; +import { Resource } from '../../common/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { JediLanguageClientMiddleware } from './languageClientMiddleware'; +import { ProgressReporting } from '../progress'; +import { ILanguageClientFactory, ILanguageServerProxy } from '../types'; +import { killPid } from '../../common/process/rawProcessApis'; +import { traceDecoratorError, traceDecoratorVerbose, traceError } from '../../logging'; + +export class JediLanguageServerProxy implements ILanguageServerProxy { + private languageClient: LanguageClient | undefined; + + private readonly disposables: Disposable[] = []; + + private lsVersion: string | undefined; + + constructor(private readonly factory: ILanguageClientFactory) {} + + private static versionTelemetryProps(instance: JediLanguageServerProxy) { + return { + lsVersion: instance.lsVersion, + }; + } + + @traceDecoratorVerbose('Disposing language server') + public dispose(): void { + this.stop().ignoreErrors(); + } + + @traceDecoratorError('Failed to start language server') + @captureTelemetry( + EventName.JEDI_LANGUAGE_SERVER_ENABLED, + undefined, + true, + undefined, + JediLanguageServerProxy.versionTelemetryProps, + ) + public async start( + resource: Resource, + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, + ): Promise { + this.lsVersion = + (options.middleware ? (options.middleware).serverVersion : undefined) ?? + '0.19.3'; + + try { + const client = await this.factory.createLanguageClient(resource, interpreter, options); + this.registerHandlers(client); + await client.start(); + this.languageClient = client; + } catch (ex) { + traceError('Failed to start language server:', ex); + throw new Error('Launching Jedi language server using python failed, see output.'); + } + } + + @traceDecoratorVerbose('Stopping language server') + public async stop(): Promise { + while (this.disposables.length > 0) { + const d = this.disposables.shift()!; + d.dispose(); + } + + if (this.languageClient) { + const client = this.languageClient; + this.languageClient = undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pid: number | undefined = ((client as any)._serverProcess as ChildProcess)?.pid; + const killServer = () => { + if (pid) { + killPid(pid); + } + }; + + try { + await client.stop(); + await client.dispose(); + killServer(); + } catch (ex) { + traceError('Stopping language client failed', ex); + killServer(); + } + } + } + + // eslint-disable-next-line class-methods-use-this + public loadExtension(): void { + // No body. + } + + @captureTelemetry( + EventName.JEDI_LANGUAGE_SERVER_READY, + undefined, + true, + undefined, + JediLanguageServerProxy.versionTelemetryProps, + ) + private registerHandlers(client: LanguageClient) { + const progressReporting = new ProgressReporting(client); + this.disposables.push(progressReporting); + } +} diff --git a/src/client/activation/jedi/manager.ts b/src/client/activation/jedi/manager.ts new file mode 100644 index 000000000000..bafdcc735a12 --- /dev/null +++ b/src/client/activation/jedi/manager.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs-extra'; +import * as path from 'path'; +import '../../common/extensions'; + +import { ICommandManager } from '../../common/application/types'; +import { IDisposable, Resource } from '../../common/types'; +import { debounceSync } from '../../common/utils/decorators'; +import { EXTENSION_ROOT_DIR } from '../../constants'; +import { IServiceContainer } from '../../ioc/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { captureTelemetry } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { Commands } from '../commands'; +import { JediLanguageClientMiddleware } from './languageClientMiddleware'; +import { ILanguageServerAnalysisOptions, ILanguageServerManager, ILanguageServerProxy } from '../types'; +import { traceDecoratorError, traceDecoratorVerbose, traceVerbose } from '../../logging'; + +export class JediLanguageServerManager implements ILanguageServerManager { + private resource!: Resource; + + private interpreter: PythonEnvironment | undefined; + + private middleware: JediLanguageClientMiddleware | undefined; + + private disposables: IDisposable[] = []; + + private static commandDispose: IDisposable; + + private connected = false; + + private lsVersion: string | undefined; + + constructor( + private readonly serviceContainer: IServiceContainer, + private readonly analysisOptions: ILanguageServerAnalysisOptions, + private readonly languageServerProxy: ILanguageServerProxy, + commandManager: ICommandManager, + ) { + if (JediLanguageServerManager.commandDispose) { + JediLanguageServerManager.commandDispose.dispose(); + } + JediLanguageServerManager.commandDispose = commandManager.registerCommand(Commands.RestartLS, () => { + this.restartLanguageServer().ignoreErrors(); + }); + } + + private static versionTelemetryProps(instance: JediLanguageServerManager) { + return { + lsVersion: instance.lsVersion, + }; + } + + public dispose(): void { + this.stopLanguageServer().ignoreErrors(); + JediLanguageServerManager.commandDispose.dispose(); + this.disposables.forEach((d) => d.dispose()); + } + + @traceDecoratorError('Failed to start language server') + public async start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise { + this.resource = resource; + this.interpreter = interpreter; + this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); + + try { + // Version is actually hardcoded in our requirements.txt. + const requirementsTxt = await fs.readFile( + path.join(EXTENSION_ROOT_DIR, 'python_files', 'jedilsp_requirements', 'requirements.txt'), + 'utf-8', + ); + + // Search using a regex in the text + const match = /jedi-language-server==([0-9\.]*)/.exec(requirementsTxt); + if (match && match.length === 2) { + [, this.lsVersion] = match; + } + } catch (ex) { + // Getting version here is best effort and does not affect how LS works and + // failing to get version should not stop LS from working. + traceVerbose('Failed to get jedi-language-server version: ', ex); + } + + await this.analysisOptions.initialize(resource, interpreter); + await this.startLanguageServer(); + } + + public connect(): void { + if (!this.connected) { + this.connected = true; + this.middleware?.connect(); + } + } + + public disconnect(): void { + if (this.connected) { + this.connected = false; + this.middleware?.disconnect(); + } + } + + @debounceSync(1000) + protected restartLanguageServerDebounced(): void { + this.restartLanguageServer().ignoreErrors(); + } + + @traceDecoratorError('Failed to restart language server') + @traceDecoratorVerbose('Restarting language server') + protected async restartLanguageServer(): Promise { + await this.stopLanguageServer(); + await this.startLanguageServer(); + } + + @captureTelemetry( + EventName.JEDI_LANGUAGE_SERVER_STARTUP, + undefined, + true, + undefined, + JediLanguageServerManager.versionTelemetryProps, + ) + @traceDecoratorVerbose('Starting language server') + protected async startLanguageServer(): Promise { + const options = await this.analysisOptions.getAnalysisOptions(); + this.middleware = new JediLanguageClientMiddleware(this.serviceContainer, this.lsVersion); + options.middleware = this.middleware; + + // Make sure the middleware is connected if we restart and we we're already connected. + if (this.connected) { + this.middleware.connect(); + } + + // Then use this middleware to start a new language client. + await this.languageServerProxy.start(this.resource, this.interpreter, options); + } + + @traceDecoratorVerbose('Stopping language server') + protected async stopLanguageServer(): Promise { + if (this.languageServerProxy) { + await this.languageServerProxy.stop(); + } + } +} diff --git a/src/client/activation/languageClientMiddleware.ts b/src/client/activation/languageClientMiddleware.ts 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 extends PromiseLike ? U : T; +type MiddleWareMethods = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [P in keyof Middleware]-?: NonNullable extends (...args: any) => any ? Middleware[P] : never; +}; + +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable prefer-rest-params */ +/* eslint-disable consistent-return */ +/* eslint-disable @typescript-eslint/ban-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +interface SendTelemetryEventFunc { + (eventName: EventName, measuresOrDurationMs?: Record | number, properties?: any, ex?: Error): void; +} + +export class LanguageClientMiddlewareBase implements Middleware { + private readonly eventName: EventName | undefined; + + private readonly lastCaptured = new Map(); + + private nextWindow = 0; + + private eventCount = 0; + + public workspace = { + configuration: async ( + params: ConfigurationParams, + token: CancellationToken, + next: ConfigurationRequest.HandlerSignature, + ) => { + if (!this.serviceContainer) { + return next(params, token); + } + + const interpreterService = this.serviceContainer.get(IInterpreterService); + const envService = this.serviceContainer.get(IEnvironmentVariablesProvider); + + let settings = next(params, token); + if (isThenable(settings)) { + settings = await settings; + } + if (settings instanceof ResponseError) { + return settings; + } + + for (const [i, item] of params.items.entries()) { + if (item.section === 'python') { + const uri = item.scopeUri ? Uri.parse(item.scopeUri) : undefined; + // For backwards compatibility, set python.pythonPath to the configured + // value as though it were in the user's settings.json file. + // As this is for backwards compatibility, `ConfigService.pythonPath` + // can be considered as active interpreter path. + const settingDict: LSPObject & { pythonPath: string; _envPYTHONPATH: string } = settings[ + i + ] as LSPObject & { pythonPath: string; _envPYTHONPATH: string }; + settingDict.pythonPath = (await interpreterService.getActiveInterpreter(uri))?.path ?? 'python'; + + const env = await envService.getEnvironmentVariables(uri); + const envPYTHONPATH = env.PYTHONPATH; + if (envPYTHONPATH) { + settingDict._envPYTHONPATH = envPYTHONPATH; + } + } + + this.configurationHook(item, settings[i] as LSPObject); + } + + return settings; + }, + }; + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + protected configurationHook(_item: ConfigurationItem, _settings: LSPObject): void {} + + private get connected(): Promise { + return this.connectedPromise.promise; + } + + protected notebookAddon: (Middleware & Disposable) | undefined; + + private connectedPromise = createDeferred(); + + public constructor( + readonly serviceContainer: IServiceContainer | undefined, + serverType: LanguageServerType, + public readonly sendTelemetryEventFunc: SendTelemetryEventFunc, + public readonly serverVersion?: string, + ) { + this.handleDiagnostics = this.handleDiagnostics.bind(this); // VS Code calls function without context. + this.didOpen = this.didOpen.bind(this); + this.didSave = this.didSave.bind(this); + this.didChange = this.didChange.bind(this); + this.didClose = this.didClose.bind(this); + this.willSave = this.willSave.bind(this); + this.willSaveWaitUntil = this.willSaveWaitUntil.bind(this); + + if (serverType === LanguageServerType.Node) { + this.eventName = EventName.LANGUAGE_SERVER_REQUEST; + } else if (serverType === LanguageServerType.Jedi) { + this.eventName = EventName.JEDI_LANGUAGE_SERVER_REQUEST; + } + } + + public connect() { + this.connectedPromise.resolve(true); + } + + public disconnect() { + this.connectedPromise = createDeferred(); + this.connectedPromise.resolve(false); + } + + public didChange() { + return this.callNext('didChange', arguments); + } + + public didOpen() { + // Special case, open and close happen before we connect. + return this.callNext('didOpen', arguments); + } + + public didClose() { + // Special case, open and close happen before we connect. + return this.callNext('didClose', arguments); + } + + public didSave() { + return this.callNext('didSave', arguments); + } + + public willSave() { + return this.callNext('willSave', arguments); + } + + public willSaveWaitUntil() { + return this.callNext('willSaveWaitUntil', arguments); + } + + public async didOpenNotebook() { + return this.callNotebooksNext('didOpen', arguments); + } + + public async didSaveNotebook() { + return this.callNotebooksNext('didSave', arguments); + } + + public async didChangeNotebook() { + return this.callNotebooksNext('didChange', arguments); + } + + public async didCloseNotebook() { + return this.callNotebooksNext('didClose', arguments); + } + + notebooks = { + didOpen: this.didOpenNotebook.bind(this), + didSave: this.didSaveNotebook.bind(this), + didChange: this.didChangeNotebook.bind(this), + didClose: this.didCloseNotebook.bind(this), + }; + + public async provideCompletionItem() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/completion', + debounceFrequentCall, + 'provideCompletionItem', + arguments, + (_, result) => { + if (!result) { + return { resultLength: 0 }; + } + const resultLength = Array.isArray(result) ? result.length : result.items.length; + return { resultLength }; + }, + ); + } + } + + public async provideHover() { + if (await this.connected) { + return this.callNextAndSendTelemetry('textDocument/hover', debounceFrequentCall, 'provideHover', arguments); + } + } + + public async handleDiagnostics(uri: Uri, _diagnostics: Diagnostic[], _next: HandleDiagnosticsSignature) { + if (await this.connected) { + // Skip sending if this is a special file. + const filePath = uri.fsPath; + const baseName = filePath ? path.basename(filePath) : undefined; + if (!baseName || !baseName.startsWith(HiddenFilePrefix)) { + return this.callNext('handleDiagnostics', arguments); + } + } + } + + public async resolveCompletionItem() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'completionItem/resolve', + debounceFrequentCall, + 'resolveCompletionItem', + arguments, + ); + } + } + + public async provideSignatureHelp() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/signatureHelp', + debounceFrequentCall, + 'provideSignatureHelp', + arguments, + ); + } + } + + public async provideDefinition() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/definition', + debounceRareCall, + 'provideDefinition', + arguments, + ); + } + } + + public async provideReferences() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/references', + debounceRareCall, + 'provideReferences', + arguments, + ); + } + } + + public async provideDocumentHighlights() { + if (await this.connected) { + return this.callNext('provideDocumentHighlights', arguments); + } + } + + public async provideDocumentSymbols() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/documentSymbol', + debounceFrequentCall, + 'provideDocumentSymbols', + arguments, + ); + } + } + + public async provideWorkspaceSymbols() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'workspace/symbol', + debounceRareCall, + 'provideWorkspaceSymbols', + arguments, + ); + } + } + + public async provideCodeActions() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/codeAction', + debounceFrequentCall, + 'provideCodeActions', + arguments, + ); + } + } + + public async provideCodeLenses() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/codeLens', + debounceFrequentCall, + 'provideCodeLenses', + arguments, + ); + } + } + + public async resolveCodeLens() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'codeLens/resolve', + debounceFrequentCall, + 'resolveCodeLens', + arguments, + ); + } + } + + public async provideDocumentFormattingEdits() { + if (await this.connected) { + return this.callNext('provideDocumentFormattingEdits', arguments); + } + } + + public async provideDocumentRangeFormattingEdits() { + if (await this.connected) { + return this.callNext('provideDocumentRangeFormattingEdits', arguments); + } + } + + public async provideOnTypeFormattingEdits() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/onTypeFormatting', + debounceFrequentCall, + 'provideOnTypeFormattingEdits', + arguments, + ); + } + } + + public async provideRenameEdits() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/rename', + debounceRareCall, + 'provideRenameEdits', + arguments, + ); + } + } + + public async prepareRename() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/prepareRename', + debounceRareCall, + 'prepareRename', + arguments, + ); + } + } + + public async provideDocumentLinks() { + if (await this.connected) { + return this.callNext('provideDocumentLinks', arguments); + } + } + + public async resolveDocumentLink() { + if (await this.connected) { + return this.callNext('resolveDocumentLink', arguments); + } + } + + public async provideDeclaration() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/declaration', + debounceRareCall, + 'provideDeclaration', + arguments, + ); + } + } + + public async provideTypeDefinition() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/typeDefinition', + debounceRareCall, + 'provideTypeDefinition', + arguments, + ); + } + } + + public async provideImplementation() { + if (await this.connected) { + return this.callNext('provideImplementation', arguments); + } + } + + public async provideDocumentColors() { + if (await this.connected) { + return this.callNext('provideDocumentColors', arguments); + } + } + + public async provideColorPresentations() { + if (await this.connected) { + return this.callNext('provideColorPresentations', arguments); + } + } + + public async provideFoldingRanges() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/foldingRange', + debounceFrequentCall, + 'provideFoldingRanges', + arguments, + ); + } + } + + public async provideSelectionRanges() { + if (await this.connected) { + return this.callNextAndSendTelemetry( + 'textDocument/selectionRange', + debounceRareCall, + 'provideSelectionRanges', + arguments, + ); + } + } + + public async prepareCallHierarchy() { + if (await this.connected) { + return this.callNext('prepareCallHierarchy', arguments); + } + } + + public async provideCallHierarchyIncomingCalls() { + if (await this.connected) { + return this.callNext('provideCallHierarchyIncomingCalls', arguments); + } + } + + public async provideCallHierarchyOutgoingCalls() { + if (await this.connected) { + return this.callNext('provideCallHierarchyOutgoingCalls', arguments); + } + } + + public async provideDocumentSemanticTokens() { + if (await this.connected) { + return this.callNext('provideDocumentSemanticTokens', arguments); + } + } + + public async provideDocumentSemanticTokensEdits() { + if (await this.connected) { + return this.callNext('provideDocumentSemanticTokensEdits', arguments); + } + } + + public async provideDocumentRangeSemanticTokens() { + if (await this.connected) { + return this.callNext('provideDocumentRangeSemanticTokens', arguments); + } + } + + public async provideLinkedEditingRange() { + if (await this.connected) { + return this.callNext('provideLinkedEditingRange', arguments); + } + } + + private callNext(funcName: keyof Middleware, args: IArguments) { + // This function uses the last argument to call the 'next' item. If we're allowing notebook + // middleware, it calls into the notebook middleware first. + if (this.notebookAddon && (this.notebookAddon as any)[funcName]) { + // It would be nice to use args.callee, but not supported in strict mode + return (this.notebookAddon as any)[funcName](...args); + } + + return args[args.length - 1](...args); + } + + private callNotebooksNext(funcName: 'didOpen' | 'didSave' | 'didChange' | 'didClose', args: IArguments) { + // This function uses the last argument to call the 'next' item. If we're allowing notebook + // middleware, it calls into the notebook middleware first. + if (this.notebookAddon?.notebooks && (this.notebookAddon.notebooks as any)[funcName]) { + // It would be nice to use args.callee, but not supported in strict mode + return (this.notebookAddon.notebooks as any)[funcName](...args); + } + + return args[args.length - 1](...args); + } + + private callNextAndSendTelemetry( + lspMethod: string, + debounceMilliseconds: number, + funcName: T, + args: IArguments, + lazyMeasures?: (this_: any, result: Awaited>) => Record, + ): ReturnType { + const now = Date.now(); + const stopWatch = new StopWatch(); + let calledNext = false; + // Change the 'last' argument (which is our next) in order to track if + // telemetry should be sent or not. + const changedArgs = [...args]; + + // Track whether or not the middleware called the 'next' function (which means it actually sent a request) + changedArgs[changedArgs.length - 1] = (...nextArgs: any) => { + // If the 'next' function is called, then legit request was made. + calledNext = true; + + // Then call the original 'next' + return args[args.length - 1](...nextArgs); + }; + + // Check if we need to reset the event count (if we're past the globalDebounce time) + if (now > this.nextWindow) { + // Past the end of the last window, reset. + this.nextWindow = now + globalDebounce; + this.eventCount = 0; + } + const lastCapture = this.lastCaptured.get(lspMethod); + + const sendTelemetry = (result: Awaited>) => { + // Skip doing anything if not allowed + // We should have: + // - called the next function in the middleware (this means a request was actually sent) + // - eventcount is not over the global limit + // - elapsed time since we sent this event is greater than debounce time + if ( + this.eventName && + calledNext && + this.eventCount < globalLimit && + (!lastCapture || now - lastCapture > debounceMilliseconds) + ) { + // We're sending, so update event count and last captured time + this.lastCaptured.set(lspMethod, now); + this.eventCount += 1; + + // Replace all slashes in the method name so it doesn't get scrubbed by @vscode/extension-telemetry. + const formattedMethod = lspMethod.replace(/\//g, '.'); + + const properties = { + lsVersion: this.serverVersion || 'unknown', + method: formattedMethod, + }; + + let measures: number | Record = stopWatch.elapsedTime; + if (lazyMeasures) { + measures = { + duration: measures, + ...lazyMeasures(this, result), + }; + } + + this.sendTelemetryEventFunc(this.eventName, measures, properties); + } + return result; + }; + + // Try to call the 'next' function in the middleware chain + const result: ReturnType = this.callNext(funcName, changedArgs as any); + + // Then wait for the result before sending telemetry + if (isThenable(result)) { + return result.then(sendTelemetry); + } + return sendTelemetry(result as any) as ReturnType; + } +} diff --git a/src/client/activation/languageServer/activator.ts b/src/client/activation/languageServer/activator.ts deleted file mode 100644 index 0bb2199c9f0c..000000000000 --- a/src/client/activation/languageServer/activator.ts +++ /dev/null @@ -1,86 +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 { IWorkspaceService } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, Resource } from '../../common/types'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { - ILanguageServerActivator, - ILanguageServerDownloader, - ILanguageServerFolderService, - ILanguageServerManager -} from '../types'; - -/** - * Starts the language server managers per workspaces (currently one for first workspace). - * - * @export - * @class LanguageServerExtensionActivator - * @implements {ILanguageServerActivator} - */ -@injectable() -export class LanguageServerExtensionActivator implements ILanguageServerActivator { - private resource?: Resource; - constructor( - @inject(ILanguageServerManager) private readonly manager: ILanguageServerManager, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(ILanguageServerDownloader) private readonly lsDownloader: ILanguageServerDownloader, - @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService - ) { } - @traceDecorators.error('Failed to activate language server') - public async activate(resource: Resource): Promise { - if (!resource) { - resource = this.workspace.hasWorkspaceFolders - ? this.workspace.workspaceFolders![0].uri - : undefined; - } - this.resource = resource; - await this.ensureLanguageServerIsAvailable(resource); - await this.manager.start(resource); - } - public dispose(): void { - this.manager.dispose(); - } - @traceDecorators.error('Failed to ensure language server is available') - public async ensureLanguageServerIsAvailable(resource: Resource) { - const settings = this.configurationService.getSettings(resource); - if (!settings.downloadLanguageServer) { - return; - } - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); - if (!(await this.fs.fileExists(mscorlib))) { - await this.lsDownloader.downloadLanguageServer(languageServerFolderPath, this.resource); - await this.prepareLanguageServerForNoICU(languageServerFolderPath); - } - } - public async prepareLanguageServerForNoICU(languageServerFolderPath: string): Promise { - const targetJsonFile = path.join(languageServerFolderPath, 'Microsoft.Python.LanguageServer.runtimeconfig.json'); - // tslint:disable-next-line:no-any - let content: any = {}; - if (await this.fs.fileExists(targetJsonFile)) { - try { - content = JSON.parse(await this.fs.readFile(targetJsonFile)); - if (content.runtimeOptions && content.runtimeOptions.configProperties && - content.runtimeOptions.configProperties['System.Globalization.Invariant'] === true) { - return; - } - } catch { - // Do nothing. - } - } - content.runtimeOptions = content.runtimeOptions || {}; - content.runtimeOptions.configProperties = content.runtimeOptions.configProperties || {}; - content.runtimeOptions.configProperties['System.Globalization.Invariant'] = true; - await this.fs.writeFile(targetJsonFile, JSON.stringify(content)); - } -} diff --git a/src/client/activation/languageServer/analysisOptions.ts b/src/client/activation/languageServer/analysisOptions.ts deleted file mode 100644 index 436f14bbc266..000000000000 --- a/src/client/activation/languageServer/analysisOptions.ts +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { CancellationToken, CompletionContext, ConfigurationChangeEvent, Disposable, Event, EventEmitter, OutputChannel, Position, TextDocument, WorkspaceFolder } from 'vscode'; -import { DocumentFilter, DocumentSelector, LanguageClientOptions, ProvideCompletionItemsSignature, RevealOutputChannelOn } from 'vscode-languageclient'; -import { IWorkspaceService } from '../../common/application/types'; -import { isTestExecution, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { traceDecorators, traceError } from '../../common/logger'; -import { BANNER_NAME_LS_SURVEY, IConfigurationService, IExtensionContext, IOutputChannel, IPathUtils, IPythonExtensionBanner, Resource } from '../../common/types'; -import { debounceSync } from '../../common/utils/decorators'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { ILanguageServerAnalysisOptions, ILanguageServerFolderService } from '../types'; - -@injectable() -export class LanguageServerAnalysisOptions implements ILanguageServerAnalysisOptions { - private envPythonPath: string = ''; - private excludedFiles: string[] = []; - private typeshedPaths: string[] = []; - private disposables: Disposable[] = []; - private languageServerFolder: string = ''; - private resource: Resource; - private readonly didChange = new EventEmitter(); - constructor(@inject(IExtensionContext) private readonly context: IExtensionContext, - @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, - @inject(IConfigurationService) private readonly configuration: IConfigurationService, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPythonExtensionBanner) @named(BANNER_NAME_LS_SURVEY) private readonly surveyBanner: IPythonExtensionBanner, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: OutputChannel, - @inject(IPathUtils) private readonly pathUtils: IPathUtils, - @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService) { - - } - public async initialize(resource: Resource) { - this.resource = resource; - this.languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - - let disposable = this.workspace.onDidChangeConfiguration(this.onSettingsChangedHandler, this); - this.disposables.push(disposable); - - disposable = this.interpreterService.onDidChangeInterpreter(() => this.didChange.fire(), this); - this.disposables.push(disposable); - - disposable = this.envVarsProvider.onDidEnvironmentVariablesChange(this.onEnvVarChange, this); - this.disposables.push(disposable); - } - public get onDidChange(): Event { - return this.didChange.event; - } - public dispose(): void { - this.disposables.forEach(d => d.dispose()); - this.didChange.dispose(); - } - @traceDecorators.error('Failed to get analysis options') - public async getAnalysisOptions(): Promise { - const properties: Record = {}; - - const interpreterInfo = await this.interpreterService.getActiveInterpreter(this.resource); - if (!interpreterInfo) { - // tslint:disable-next-line:no-suspicious-comment - // TODO: How do we handle this? It is pretty unlikely... - throw Error('did not find an active interpreter'); - } - - // tslint:disable-next-line:no-string-literal - properties['InterpreterPath'] = interpreterInfo.path; - - const version = interpreterInfo.version; - if (version) { - // tslint:disable-next-line:no-string-literal - properties['Version'] = `${version.major}.${version.minor}.${version.patch}`; - } else { - traceError('Unable to determine Python version. Analysis may be limited.'); - } - - let searchPaths = []; - - const settings = this.configuration.getSettings(this.resource); - if (settings.autoComplete) { - const extraPaths = settings.autoComplete.extraPaths; - if (extraPaths && extraPaths.length > 0) { - searchPaths.push(...extraPaths); - } - } - - // TODO: remove this setting since LS 0.2.92+ is not using it. - // tslint:disable-next-line:no-string-literal - properties['DatabasePath'] = path.join(this.context.extensionPath, this.languageServerFolder); - - const vars = await this.envVarsProvider.getEnvironmentVariables(); - this.envPythonPath = vars.PYTHONPATH || ''; - if (this.envPythonPath !== '') { - const paths = this.envPythonPath.split(this.pathUtils.delimiter).filter(item => item.trim().length > 0); - searchPaths.push(...paths); - } - - searchPaths = searchPaths.map(p => path.normalize(p)); - - this.excludedFiles = this.getExcludedFiles(); - this.typeshedPaths = this.getTypeshedPaths(); - const workspaceFolder = this.workspace.getWorkspaceFolder(this.resource); - const documentSelector = this.getDocumentSelector(workspaceFolder); - // Options to control the language client. - return { - // Register the server for Python documents. - documentSelector, - workspaceFolder, - synchronize: { - configurationSection: PYTHON_LANGUAGE - }, - outputChannel: this.output, - revealOutputChannelOn: RevealOutputChannelOn.Never, - initializationOptions: { - interpreter: { - properties - }, - displayOptions: { - preferredFormat: 'markdown', - trimDocumentationLines: false, - maxDocumentationLineLength: 0, - trimDocumentationText: false, - maxDocumentationTextLength: 0 - }, - searchPaths, - typeStubSearchPaths: this.typeshedPaths, - cacheFolderPath: this.getCacheFolderPath(), - 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) => { - this.surveyBanner.showBanner().ignoreErrors(); - return next(document, position, context, token); - } - } - }; - } - protected getDocumentSelector(workspaceFolder?: WorkspaceFolder): DocumentSelector { - const documentSelector: DocumentFilter[] = [ - { scheme: 'file', language: PYTHON_LANGUAGE }, - { scheme: 'untitled', language: PYTHON_LANGUAGE } - ]; - // Set the document selector only when in a multi-root workspace scenario. - if (workspaceFolder && Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders!.length > 1) { - // tslint:disable-next-line:no-any - documentSelector[0].pattern = `${workspaceFolder.uri.fsPath}/**/*`; - } - return documentSelector; - } - protected getExcludedFiles(): string[] { - const list: string[] = ['**/Lib/**', '**/site-packages/**']; - this.getVsCodeExcludeSection('search.exclude', list); - this.getVsCodeExcludeSection('files.exclude', list); - this.getVsCodeExcludeSection('files.watcherExclude', list); - this.getPythonExcludeSection(list); - return list; - } - - protected getVsCodeExcludeSection(setting: string, list: string[]): void { - const states = this.workspace.getConfiguration(setting); - if (states) { - Object.keys(states) - .filter(k => (k.indexOf('*') >= 0 || k.indexOf('/') >= 0) && states[k]) - .forEach(p => list.push(p)); - } - } - protected getPythonExcludeSection(list: string[]): void { - const pythonSettings = this.configuration.getSettings(this.resource); - const paths = pythonSettings && pythonSettings.linting ? pythonSettings.linting.ignorePatterns : undefined; - if (paths && Array.isArray(paths)) { - paths - .filter(p => p && p.length > 0) - .forEach(p => list.push(p)); - } - } - protected getTypeshedPaths(): string[] { - const settings = this.configuration.getSettings(this.resource); - return settings.analysis.typeshedPaths && settings.analysis.typeshedPaths.length > 0 - ? settings.analysis.typeshedPaths - : [path.join(this.context.extensionPath, this.languageServerFolder, 'Typeshed')]; - } - protected getCacheFolderPath(): string | null { - const settings = this.configuration.getSettings(this.resource); - return settings.analysis.cacheFolderPath && settings.analysis.cacheFolderPath.length > 0 - ? settings.analysis.cacheFolderPath : null; - } - protected async onSettingsChangedHandler(e?: ConfigurationChangeEvent): Promise { - if (e && !e.affectsConfiguration('python', this.resource)) { - return; - } - this.onSettingsChanged(); - } - @debounceSync(1000) - protected onSettingsChanged(): void { - this.notifyIfSettingsChanged().ignoreErrors(); - } - @traceDecorators.verbose('Changes in python settings detected in analysis options') - protected async notifyIfSettingsChanged(): Promise { - const excludedFiles = this.getExcludedFiles(); - await this.notifyIfValuesHaveChanged(this.excludedFiles, excludedFiles); - - const typeshedPaths = this.getTypeshedPaths(); - await this.notifyIfValuesHaveChanged(this.typeshedPaths, typeshedPaths); - } - - protected async notifyIfValuesHaveChanged(oldArray: string[], newArray: string[]): Promise { - if (newArray.length !== oldArray.length) { - this.didChange.fire(); - return; - } - - for (let i = 0; i < oldArray.length; i += 1) { - if (oldArray[i] !== newArray[i]) { - this.didChange.fire(); - return; - } - } - } - - @debounceSync(1000) - protected onEnvVarChange(): void { - this.notifyifEnvPythonPathChanged().ignoreErrors(); - } - - protected async notifyifEnvPythonPathChanged(): Promise { - const vars = await this.envVarsProvider.getEnvironmentVariables(); - const envPythonPath = vars.PYTHONPATH || ''; - - if (this.envPythonPath !== envPythonPath) { - this.didChange.fire(); - } - } -} diff --git a/src/client/activation/languageServer/downloadChannelRules.ts b/src/client/activation/languageServer/downloadChannelRules.ts deleted file mode 100644 index 59108a95e034..000000000000 --- a/src/client/activation/languageServer/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 { - return true; - } -} -@injectable() -export class DownloadStableChannelRule implements IDownloadChannelRule { - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise { - return currentFolder ? false : true; - } -} -@injectable() -export class DownloadBetaChannelRule implements IDownloadChannelRule { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise { - // For beta, we do this only once a day. - const stateFactory = this.serviceContainer.get(IPersistentStateFactory); - const globalState = stateFactory.createGlobalPersistentState(lastCheckedForLSDateTimeCacheKey, - true, - frequencyForBetalLSDownloadCheck); - - // If we have checked it in the last 24 hours, then ensure we don't do it again. - if (globalState.value) { - await globalState.updateValue(false); - return true; - } - - return !currentFolder || globalState.value; - } - -} diff --git a/src/client/activation/languageServer/downloader.ts b/src/client/activation/languageServer/downloader.ts deleted file mode 100644 index 7a1c03de5847..000000000000 --- a/src/client/activation/languageServer/downloader.ts +++ /dev/null @@ -1,167 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { ProgressLocation, window } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import '../../common/extensions'; -import { IFileSystem } from '../../common/platform/types'; -import { IOutputChannel, Resource, IFileDownloader } from '../../common/types'; -import { createDeferred } from '../../common/utils/async'; -import { Common, LanguageService } from '../../common/utils/localize'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { - ILanguageServerDownloader, ILanguageServerFolderService, - IPlatformData -} from '../types'; - -// tslint:disable:no-require-imports no-any - -const downloadFileExtension = '.nupkg'; - -@injectable() -export class LanguageServerDownloader implements ILanguageServerDownloader { - constructor( - @inject(IPlatformData) private readonly platformData: IPlatformData, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, - @inject(IFileDownloader) private readonly fileDownloader: IFileDownloader, - @inject(ILanguageServerFolderService) private readonly lsFolderService: ILanguageServerFolderService, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService - ) { - } - - public async getDownloadInfo(resource: Resource) { - const info = await this.lsFolderService.getLatestLanguageServerVersion(resource) - .then(item => item!); - - let uri = info.uri; - if (uri.startsWith('https:')) { - const cfg = this.workspace.getConfiguration('http', resource); - if (!cfg.get('proxyStrictSSL', true)) { - // tslint:disable-next-line:no-http-string - uri = uri.replace(/^https:/, 'http:'); - } - } - - return [uri, info.version.raw]; - } - public async downloadLanguageServer(destinationFolder: string, resource: Resource): Promise { - const [downloadUri, lsVersion] = await this.getDownloadInfo(resource); - const timer: StopWatch = new StopWatch(); - let success: boolean = true; - let localTempFilePath = ''; - - try { - localTempFilePath = await this.downloadFile(downloadUri, 'Downloading Microsoft Python Language Server... '); - } catch (err) { - this.output.appendLine(LanguageService.downloadFailedOutputMessage()); - this.output.appendLine(err); - success = false; - this.showMessageAndOptionallyShowOutput(LanguageService.lsFailedToDownload()) - .ignoreErrors(); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_ERROR, undefined, { error: 'Failed to download (platform)' }, err); - throw new Error(err); - } finally { - const usedSSL = downloadUri.startsWith('https:'); - sendTelemetryEvent( - EventName.PYTHON_LANGUAGE_SERVER_DOWNLOADED, - timer.elapsedTime, - { success, lsVersion, usedSSL } - ); - } - - timer.reset(); - try { - await this.unpackArchive(destinationFolder, localTempFilePath); - } catch (err) { - this.output.appendLine(LanguageService.extractionFailedOutputMessage()); - this.output.appendLine(err); - success = false; - this.showMessageAndOptionallyShowOutput(LanguageService.lsFailedToExtract()) - .ignoreErrors(); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_ERROR, undefined, { error: 'Failed to extract (platform)' }, err); - throw new Error(err); - } finally { - sendTelemetryEvent( - EventName.PYTHON_LANGUAGE_SERVER_EXTRACTED, - timer.elapsedTime, - { success, lsVersion } - ); - await this.fs.deleteFile(localTempFilePath); - } - } - - public async showMessageAndOptionallyShowOutput(message: string) { - const selection = await this.appShell.showErrorMessage(message, Common.openOutputPanel()); - if (selection !== Common.openOutputPanel()) { - return; - } - this.output.show(true); - } - public async downloadFile(uri: string, title: string): Promise { - const downloadOptions = { - extension: downloadFileExtension, - outputChannel: this.output, - progressMessagePrefix: title - }; - return this.fileDownloader.downloadFile(uri, downloadOptions).then(file => { - this.output.appendLine(LanguageService.extractionCompletedOutputMessage()); - return file; - }); - } - - protected async unpackArchive(destinationFolder: string, tempFilePath: string): Promise { - this.output.append('Unpacking archive... '); - - const deferred = createDeferred(); - - const title = 'Extracting files... '; - await window.withProgress({ - location: ProgressLocation.Window - }, (progress) => { - // tslint:disable-next-line:no-require-imports no-var-requires - const StreamZip = require('node-stream-zip'); - const zip = new StreamZip({ - file: tempFilePath, - storeEntries: true - }); - - let totalFiles = 0; - let extractedFiles = 0; - zip.on('ready', async () => { - totalFiles = zip.entriesCount; - if (!await this.fs.directoryExists(destinationFolder)) { - await this.fs.createDirectory(destinationFolder); - } - zip.extract(null, destinationFolder, (err: any) => { - if (err) { - deferred.reject(err); - } else { - deferred.resolve(); - } - zip.close(); - }); - }).on('extract', () => { - extractedFiles += 1; - progress.report({ message: `${title}${Math.round(100 * extractedFiles / totalFiles)}%` }); - }).on('error', (e: any) => { - deferred.reject(e); - }); - return deferred.promise; - }); - - // Set file to executable (nothing happens in Windows, as chmod has no definition there) - const executablePath = path.join(destinationFolder, this.platformData.engineExecutableName); - await this.fs.chmod(executablePath, '0764'); // -rwxrw-r-- - - this.output.appendLine(LanguageService.extractionDoneOutputMessage()); - } -} diff --git a/src/client/activation/languageServer/languageClientFactory.ts b/src/client/activation/languageServer/languageClientFactory.ts deleted file mode 100644 index f1e860f32fc8..000000000000 --- a/src/client/activation/languageServer/languageClientFactory.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient'; -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; -import { IConfigurationService, Resource } from '../../common/types'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { IEnvironmentActivationService } from '../../interpreter/activation/types'; -import { ILanguageClientFactory, ILanguageServerFolderService, IPlatformData, LanguageClientFactory } from '../types'; - -// tslint:disable:no-require-imports no-require-imports no-var-requires max-classes-per-file - -const dotNetCommand = 'dotnet'; -const languageClientName = 'Python Tools'; - -@injectable() -export class BaseLanguageClientFactory implements ILanguageClientFactory { - constructor(@inject(ILanguageClientFactory) @named(LanguageClientFactory.downloaded) private readonly downloadedFactory: ILanguageClientFactory, - @inject(ILanguageClientFactory) @named(LanguageClientFactory.simple) private readonly simpleFactory: ILanguageClientFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IEnvironmentVariablesProvider) private readonly envVarsProvider: IEnvironmentVariablesProvider, - @inject(IEnvironmentActivationService) private readonly environmentActivationService: IEnvironmentActivationService) { } - public async createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions): Promise { - const settings = this.configurationService.getSettings(resource); - const factory = settings.downloadLanguageServer ? this.downloadedFactory : this.simpleFactory; - const env = await this.getEnvVars(resource); - return factory.createLanguageClient(resource, clientOptions, env); - } - - private async getEnvVars(resource: Resource): Promise { - const envVars = await this.environmentActivationService.getActivatedEnvironmentVariables(resource); - if (envVars && Object.keys(envVars).length > 0) { - return envVars; - } - return this.envVarsProvider.getEnvironmentVariables(resource); - } -} - -/** - * Creates a langauge client for use by users of the extension. - * - * @export - * @class DownloadedLanguageClientFactory - * @implements {ILanguageClientFactory} - */ -@injectable() -export class DownloadedLanguageClientFactory implements ILanguageClientFactory { - constructor(@inject(IPlatformData) private readonly platformData: IPlatformData, - @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService) { } - public async createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise { - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, this.platformData.engineExecutableName); - const options = { stdio: 'pipe', env }; - const serverOptions: ServerOptions = { - run: { command: serverModule, args: [], options }, - debug: { command: serverModule, args: ['--debug'], options } - }; - const vscodeLanguageClient = require('vscode-languageclient') as typeof import('vscode-languageclient'); - return new vscodeLanguageClient.LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); - } -} - -/** - * Creates a language client factory primarily used for LS development purposes. - * - * @export - * @class SimpleLanguageClientFactory - * @implements {ILanguageClientFactory} - */ -@injectable() -export class SimpleLanguageClientFactory implements ILanguageClientFactory { - constructor(@inject(IPlatformData) private readonly platformData: IPlatformData, - @inject(ILanguageServerFolderService) private readonly languageServerFolderService: ILanguageServerFolderService) { } - public async createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise { - const languageServerFolder = await this.languageServerFolderService.getLanguageServerFolderName(resource); - const options = { stdio: 'pipe', env }; - const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, this.platformData.engineDllName); - const serverOptions: ServerOptions = { - run: { command: dotNetCommand, args: [serverModule], options }, - debug: { command: dotNetCommand, args: [serverModule, '--debug'], options } - }; - const vscodeLanguageClient = require('vscode-languageclient') as typeof import('vscode-languageclient'); - return new vscodeLanguageClient.LanguageClient(PYTHON_LANGUAGE, languageClientName, serverOptions, clientOptions); - } -} diff --git a/src/client/activation/languageServer/languageServer.ts b/src/client/activation/languageServer/languageServer.ts deleted file mode 100644 index 7e3486b88e15..000000000000 --- a/src/client/activation/languageServer/languageServer.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { Disposable, LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import '../../common/extensions'; -import { traceDecorators, traceError } from '../../common/logger'; -import { IConfigurationService, Resource } from '../../common/types'; -import { createDeferred, Deferred, sleep } from '../../common/utils/async'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { noop } from '../../common/utils/misc'; -import { LanguageServerSymbolProvider } from '../../providers/symbolProvider'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { ITestManagementService } from '../../testing/types'; -import { ILanguageClientFactory, ILanguageServer, LanguageClientFactory } from '../types'; -import { ProgressReporting } from './progress'; - -@injectable() -export class LanguageServer implements ILanguageServer { - public languageClient: LanguageClient | undefined; - private startupCompleted: Deferred; - private readonly disposables: Disposable[] = []; - private extensionLoadedArgs = new Set<{}>(); - - constructor( - @inject(ILanguageClientFactory) - @named(LanguageClientFactory.base) - private readonly factory: ILanguageClientFactory, - @inject(ITestManagementService) private readonly testManager: ITestManagementService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService - ) { - this.startupCompleted = createDeferred(); - } - @traceDecorators.verbose('Stopping Language Server') - public dispose() { - if (this.languageClient) { - // Do not await on this. - this.languageClient.stop().then(noop, ex => traceError('Stopping language client failed', ex)); - this.languageClient = undefined; - } - while (this.disposables.length > 0) { - const d = this.disposables.shift()!; - d.dispose(); - } - if (this.startupCompleted.completed) { - this.startupCompleted.reject(new Error('Disposed Language Server')); - this.startupCompleted = createDeferred(); - } - } - - @traceDecorators.error('Failed to start language server') - @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_ENABLED, undefined, true) - public async start(resource: Resource, options: LanguageClientOptions): Promise { - if (!this.languageClient) { - this.languageClient = await this.factory.createLanguageClient(resource, options); - this.disposables.push(this.languageClient!.start()); - await this.serverReady(); - const progressReporting = new ProgressReporting(this.languageClient!); - this.disposables.push(progressReporting); - - const settings = this.configurationService.getSettings(resource); - if (settings.downloadLanguageServer) { - this.languageClient.onTelemetry(telemetryEvent => { - const eventName = telemetryEvent.EventName || EventName.PYTHON_LANGUAGE_SERVER_TELEMETRY; - sendTelemetryEvent(eventName, telemetryEvent.Measurements, telemetryEvent.Properties); - }); - } - - await this.registerTestServices(); - } else { - await this.startupCompleted.promise; - } - } - @traceDecorators.error('Failed to load Language Server extension') - public loadExtension(args?: {}) { - if (this.extensionLoadedArgs.has(args || '')) { - return; - } - this.extensionLoadedArgs.add(args || ''); - this.startupCompleted.promise - .then(() => - this.languageClient!.sendRequest('python/loadExtension', args).then(noop, ex => - traceError('Request python/loadExtension failed', ex) - ) - ) - .ignoreErrors(); - } - @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_READY, undefined, true) - protected async serverReady(): Promise { - while (this.languageClient && !this.languageClient!.initializeResult) { - await sleep(100); - } - this.startupCompleted.resolve(); - } - @swallowExceptions('Activating Unit Tests Manager for Language Server') - protected async registerTestServices() { - if (!this.languageClient) { - throw new Error('languageClient not initialized'); - } - await this.testManager.activate(new LanguageServerSymbolProvider(this.languageClient!)); - } -} diff --git a/src/client/activation/languageServer/languageServerCompatibilityService.ts b/src/client/activation/languageServer/languageServerCompatibilityService.ts deleted file mode 100644 index f5e81fca7e41..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 { EventName } from '../../telemetry/constants'; -import { ILanguageServerCompatibilityService } from '../types'; - -@injectable() -export class LanguageServerCompatibilityService implements ILanguageServerCompatibilityService { - constructor(@inject(IDotNetCompatibilityService) private readonly dotnetCompatibility: IDotNetCompatibilityService) { } - public async isSupported(): Promise { - try { - const supported = await this.dotnetCompatibility.isSupported(); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported }); - return supported; - } catch (ex) { - traceError('Unable to determine whether LS is supported', ex); - sendTelemetryEvent(EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED, undefined, { supported: false, failureType: 'UnknownError' }); - return false; - } - } -} diff --git a/src/client/activation/languageServer/languageServerExtension.ts b/src/client/activation/languageServer/languageServerExtension.ts deleted file mode 100644 index 0a89069d5bd1..000000000000 --- a/src/client/activation/languageServer/languageServerExtension.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter } from 'vscode'; -import { ICommandManager } from '../../common/application/types'; -import '../../common/extensions'; -import { IDisposable } from '../../common/types'; -import { ILanguageServerExtension } from '../types'; - -const loadExtensionCommand = 'python._loadLanguageServerExtension'; - -@injectable() -export class LanguageServerExtension implements ILanguageServerExtension { - public loadExtensionArgs?: {}; - protected readonly _invoked = new EventEmitter(); - private disposable?: IDisposable; - constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) { } - public dispose() { - if (this.disposable) { - this.disposable.dispose(); - } - } - public async register(): Promise { - if (this.disposable) { - return; - } - this.disposable = this.commandManager.registerCommand(loadExtensionCommand, args => { - this.loadExtensionArgs = args; - this._invoked.fire(); - }); - } - public get invoked(): Event { - return this._invoked.event; - } -} diff --git a/src/client/activation/languageServer/languageServerFolderService.ts b/src/client/activation/languageServer/languageServerFolderService.ts deleted file mode 100644 index 5f80b67d8fd8..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, Resource } 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(resource: Resource): Promise { - const currentFolder = await this.getCurrentLanguageServerDirectory(); - let serverVersion: NugetPackage | undefined; - - const shouldLookForNewVersion = await this.shouldLookForNewLanguageServer(currentFolder); - if (currentFolder && !shouldLookForNewVersion) { - return path.basename(currentFolder.path); - } - - serverVersion = await this.getLatestLanguageServerVersion(resource) - .catch(() => 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(resource: Resource): Promise { - const lsPackageService = this.serviceContainer.get(ILanguageServerPackageService); - return lsPackageService.getLatestNugetPackageVersion(resource); - } - public async shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise { - const configService = this.serviceContainer.get(IConfigurationService); - const autoUpdateLanguageServer = configService.getSettings().autoUpdateLanguageServer; - const downloadLanguageServer = configService.getSettings().downloadLanguageServer; - if (currentFolder && (!autoUpdateLanguageServer || !downloadLanguageServer)) { - return false; - } - const downloadChannel = this.getDownloadChannel(); - const rule = this.serviceContainer.get(IDownloadChannelRule, downloadChannel); - return rule.shouldLookForNewLanguageServer(currentFolder); - } - public async getCurrentLanguageServerDirectory(): Promise { - const configService = this.serviceContainer.get(IConfigurationService); - if (!configService.getSettings().downloadLanguageServer) { - return { path: languageServerFolder, version: new semver.SemVer('0.0.0') }; - } - const dirs = await this.getExistingLanguageServerDirectories(); - if (dirs.length === 0) { - return; - } - dirs.sort((a, b) => a.version.compare(b.version)); - return dirs[dirs.length - 1]; - } - public async getExistingLanguageServerDirectories(): Promise { - const fs = this.serviceContainer.get(IFileSystem); - const subDirs = await fs.getSubDirectories(EXTENSION_ROOT_DIR); - return subDirs - .filter(dir => path.basename(dir).startsWith(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); - return lsPackageService.getLanguageServerDownloadChannel(); - } -} 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 35d5cece9833..000000000000 --- a/src/client/activation/languageServer/languageServerPackageService.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { parse, SemVer } from 'semver'; -import { IApplicationEnvironment } from '../../common/application/types'; -import { PVSC_EXTENSION_ID } from '../../common/constants'; -import { traceDecorators, traceVerbose } from '../../common/logger'; -import { INugetRepository, INugetService, NugetPackage } from '../../common/nuget/types'; -import { IPlatformService } from '../../common/platform/types'; -import { - IConfigurationService, IExtensions, LanguageServerDownloadChannels, Resource -} from '../../common/types'; -import { OSType } from '../../common/utils/platform'; -import { IServiceContainer } from '../../ioc/types'; -import { ILanguageServerPackageService, PlatformName } 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(resource: Resource): Promise { - const downloadChannel = this.getLanguageServerDownloadChannel(); - const nugetRepo = this.serviceContainer.get(INugetRepository, downloadChannel); - const packageName = this.getNugetPackageName(); - traceVerbose(`Listing packages for ${downloadChannel} for ${packageName}`); - const packages = await nugetRepo.getPackages(packageName, resource); - - return this.getValidPackage(packages); - } - - public getLanguageServerDownloadChannel(): LanguageServerDownloadChannels { - const configService = this.serviceContainer.get(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); - 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); - 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/languageServer/manager.ts b/src/client/activation/languageServer/manager.ts deleted file mode 100644 index 91c6da33c718..000000000000 --- a/src/client/activation/languageServer/manager.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import '../../common/extensions'; -import { traceDecorators } from '../../common/logger'; -import { IDisposable, Resource } from '../../common/types'; -import { debounceSync } from '../../common/utils/decorators'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension, ILanguageServerManager } from '../types'; - -@injectable() -export class LanguageServerManager implements ILanguageServerManager { - private languageServer?: ILanguageServer; - private resource!: Resource; - private disposables: IDisposable[] = []; - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(ILanguageServerExtension) private readonly lsExtension: ILanguageServerExtension - ) { } - public dispose() { - if (this.languageServer) { - this.languageServer.dispose(); - } - this.disposables.forEach(d => d.dispose()); - } - @traceDecorators.error('Failed to start Language Server') - public async start(resource: Resource): Promise { - if (this.languageServer) { - throw new Error('Language Server already started'); - } - this.registerCommandHandler(); - this.resource = resource; - this.analysisOptions.onDidChange(this.restartLanguageServerDebounced, this, this.disposables); - - await this.analysisOptions.initialize(resource); - await this.startLanguageServer(); - } - protected registerCommandHandler() { - this.lsExtension.invoked(this.loadExtensionIfNecessary, this, this.disposables); - } - protected loadExtensionIfNecessary() { - if (this.languageServer && this.lsExtension.loadExtensionArgs) { - this.languageServer.loadExtension(this.lsExtension.loadExtensionArgs); - } - } - @debounceSync(1000) - protected restartLanguageServerDebounced(): void { - this.restartLanguageServer().ignoreErrors(); - } - @traceDecorators.error('Failed to restart Language Server') - @traceDecorators.verbose('Restarting Language Server') - protected async restartLanguageServer(): Promise { - if (this.languageServer) { - this.languageServer.dispose(); - } - await this.startLanguageServer(); - } - @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_STARTUP, undefined, true) - @traceDecorators.verbose('Starting Language Server') - protected async startLanguageServer(): Promise { - this.languageServer = this.serviceContainer.get(ILanguageServer); - const options = await this.analysisOptions!.getAnalysisOptions(); - await this.languageServer.start(this.resource, options); - this.loadExtensionIfNecessary(); - } -} diff --git a/src/client/activation/languageServer/platformData.ts b/src/client/activation/languageServer/platformData.ts deleted file mode 100644 index a691f08fa1cf..000000000000 --- a/src/client/activation/languageServer/platformData.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IPlatformService } from '../../common/platform/types'; -import { IPlatformData } from '../types'; - -export enum PlatformName { - Windows32Bit = 'win-x86', - Windows64Bit = 'win-x64', - Mac64Bit = 'osx-x64', - Linux64Bit = 'linux-x64' -} - -export enum PlatformLSExecutables { - Windows = 'Microsoft.Python.LanguageServer.exe', - MacOS = 'Microsoft.Python.LanguageServer', - Linux = 'Microsoft.Python.LanguageServer' -} - -@injectable() -export class PlatformData implements IPlatformData { - constructor(@inject(IPlatformService) private readonly platform: IPlatformService) { } - public get platformName(): PlatformName { - if (this.platform.isWindows) { - return this.platform.is64bit ? PlatformName.Windows64Bit : PlatformName.Windows32Bit; - } - if (this.platform.isMac) { - return PlatformName.Mac64Bit; - } - if (this.platform.isLinux) { - if (!this.platform.is64bit) { - throw new Error('Microsoft Python Language Server does not support 32-bit Linux.'); - } - return PlatformName.Linux64Bit; - } - throw new Error('Unknown OS platform.'); - } - - public get engineDllName(): string { - return 'Microsoft.Python.LanguageServer.dll'; - } - - public get engineExecutableName(): string { - if (this.platform.isWindows) { - return PlatformLSExecutables.Windows; - } else if (this.platform.isLinux) { - return PlatformLSExecutables.Linux; - } else if (this.platform.isMac) { - return PlatformLSExecutables.MacOS; - } else { - return 'unknown-platform'; - } - } -} diff --git a/src/client/activation/node/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 { + 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 { + // 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 { + 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 { + 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 { + 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 => { + const inExperiment = await this.experimentService.inExperiment(params.experimentName); + return { inExperiment }; + }, + ); + + client.onRequest( + GetExperimentValue.Method, + async ( + params: GetExperimentValue.IRequest, + ): Promise> => { + const value = await this.experimentService.getExperimentValue(params.experimentName); + return { value }; + }, + ); + + this.disposables.push( + client.onRequest('python/isTrustedWorkspace', async () => ({ + isTrusted: this.workspace.isTrusted, + })), + ); + } + + private async getPylanceExtension(): Promise | undefined> { + const extension = this.extensions.getExtension(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 { + 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 { + await this.stopLanguageServer(); + await this.startLanguageServer(); + } + + @captureTelemetry( + EventName.LANGUAGE_SERVER_STARTUP, + undefined, + true, + undefined, + NodeLanguageServerManager.versionTelemetryProps, + ) + @traceDecoratorVerbose('Starting language server') + protected async startLanguageServer(): Promise { + 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 { + if (this.languageServerProxy) { + await this.languageServerProxy.stop(); + } + } +} diff --git a/src/client/activation/node/pylanceApi.ts b/src/client/activation/node/pylanceApi.ts new file mode 100644 index 000000000000..4b3d21d7527e --- /dev/null +++ b/src/client/activation/node/pylanceApi.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { + CancellationToken, + CompletionContext, + CompletionItem, + CompletionList, + Position, + TextDocument, + Uri, +} from 'vscode'; + +export interface PylanceApi { + client?: { + isEnabled(): boolean; + start(): Promise; + stop(): Promise; + }; + notebook?: { + registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; + getCompletionItems( + document: TextDocument, + position: Position, + context: CompletionContext, + token: CancellationToken, + ): Promise; + }; +} diff --git a/src/client/activation/partialModeStatus.ts b/src/client/activation/partialModeStatus.ts new file mode 100644 index 000000000000..1105f6529ac8 --- /dev/null +++ b/src/client/activation/partialModeStatus.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import { inject, injectable } from 'inversify'; +import type * as vscodeTypes from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { IDisposableRegistry } from '../common/types'; +import { Common, LanguageService } from '../common/utils/localize'; +import { IExtensionSingleActivationService } from './types'; + +/** + * Only partial features are available when running in untrusted or a + * virtual workspace, this creates a UI element to indicate that. + */ +@injectable() +export class PartialModeStatusItem implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + constructor( + @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + const { isTrusted, isVirtualWorkspace } = this.workspace; + if (isTrusted && !isVirtualWorkspace) { + return; + } + const statusItem = this.createStatusItem(); + if (statusItem) { + this.disposables.push(statusItem); + } + } + + private createStatusItem() { + // eslint-disable-next-line global-require + const vscode = require('vscode') as typeof vscodeTypes; + if ('createLanguageStatusItem' in vscode.languages) { + const statusItem = vscode.languages.createLanguageStatusItem('python.projectStatus', { + language: 'python', + }); + statusItem.name = LanguageService.statusItem.name; + statusItem.severity = vscode.LanguageStatusSeverity.Warning; + statusItem.text = LanguageService.statusItem.text; + statusItem.detail = !this.workspace.isTrusted + ? LanguageService.statusItem.detail + : LanguageService.virtualWorkspaceStatusItem.detail; + statusItem.command = { + title: Common.learnMore, + command: 'vscode.open', + arguments: [vscode.Uri.parse('https://aka.ms/AAdzyh4')], + }; + return statusItem; + } + return undefined; + } +} diff --git a/src/client/activation/languageServer/progress.ts b/src/client/activation/progress.ts similarity index 79% rename from src/client/activation/languageServer/progress.ts rename to src/client/activation/progress.ts index a1622688c0a9..5abcb9e553c0 100644 --- a/src/client/activation/languageServer/progress.ts +++ b/src/client/activation/progress.ts @@ -4,8 +4,8 @@ 'use strict'; import { Progress, ProgressLocation, window } from 'vscode'; -import { Disposable, LanguageClient } from 'vscode-languageclient'; -import { createDeferred, Deferred } from '../../common/utils/async'; +import { Disposable, LanguageClient } from 'vscode-languageclient/node'; +import { createDeferred, Deferred } from '../common/utils/async'; export class ProgressReporting implements Disposable { private statusBarMessage: Disposable | undefined; @@ -20,7 +20,7 @@ export class ProgressReporting implements Disposable { this.statusBarMessage = window.setStatusBarMessage(m); }); - this.languageClient.onNotification('python/beginProgress', _ => { + this.languageClient.onNotification('python/beginProgress', (_) => { if (this.progressDeferred) { return; } @@ -31,10 +31,10 @@ export class ProgressReporting implements Disposable { if (!this.progress) { this.beginProgress(); } - this.progress!.report({ message: m }); + this.progress!.report({ message: m }); // NOSONAR }); - this.languageClient.onNotification('python/endProgress', _ => { + this.languageClient.onNotification('python/endProgress', (_) => { if (this.progressDeferred) { this.progressDeferred.resolve(); this.progressDeferred = undefined; @@ -52,12 +52,15 @@ export class ProgressReporting implements Disposable { private beginProgress(): void { this.progressDeferred = createDeferred(); - window.withProgress({ - location: ProgressLocation.Window, - title: '' - }, progress => { - this.progress = progress; - return this.progressDeferred!.promise; - }); + 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 { + 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 ad19bd0541d5..875afa12f0b4 100644 --- a/src/client/activation/serviceRegistry.ts +++ b/src/client/activation/serviceRegistry.ts @@ -1,58 +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_INTERACTIVE_SHIFTENTER, BANNER_NAME_LS_SURVEY, BANNER_NAME_PROPOSE_LS, IPythonExtensionBanner } from '../common/types'; -import { DataScienceSurveyBanner } from '../datascience/dataScienceSurveyBanner'; -import { InteractiveShiftEnterBanner } from '../datascience/shiftEnterBanner'; import { IServiceManager } from '../ioc/types'; -import { LanguageServerSurveyBanner } from '../languageServices/languageServerSurveyBanner'; -import { ProposeLanguageServerBanner } from '../languageServices/proposeLanguageServerBanner'; import { ExtensionActivationManager } from './activationManager'; -import { LanguageServerExtensionActivationService } from './activationService'; -import { JediExtensionActivator } from './jedi'; -import { LanguageServerExtensionActivator } from './languageServer/activator'; -import { LanguageServerAnalysisOptions } from './languageServer/analysisOptions'; -import { DownloadBetaChannelRule, DownloadDailyChannelRule } from './languageServer/downloadChannelRules'; -import { LanguageServerDownloader } from './languageServer/downloader'; -import { BaseLanguageClientFactory, DownloadedLanguageClientFactory, SimpleLanguageClientFactory } from './languageServer/languageClientFactory'; -import { LanguageServer } from './languageServer/languageServer'; -import { LanguageServerCompatibilityService } from './languageServer/languageServerCompatibilityService'; -import { LanguageServerExtension } from './languageServer/languageServerExtension'; -import { LanguageServerFolderService } from './languageServer/languageServerFolderService'; -import { BetaLanguageServerPackageRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel, StableLanguageServerPackageRepository } from './languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from './languageServer/languageServerPackageService'; -import { LanguageServerManager } from './languageServer/manager'; -import { PlatformData } from './languageServer/platformData'; -import { IDownloadChannelRule, IExtensionActivationManager, IExtensionActivationService, ILanguageClientFactory, ILanguageServer, ILanguageServerActivator, ILanguageServerAnalysisOptions, ILanguageServerCompatibilityService as ILanagueServerCompatibilityService, ILanguageServerDownloader, ILanguageServerExtension, ILanguageServerFolderService, ILanguageServerManager, ILanguageServerPackageService, IPlatformData, LanguageClientFactory, LanguageServerActivator } from './types'; +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) { - serviceManager.addSingleton(IExtensionActivationService, LanguageServerExtensionActivationService); - serviceManager.addSingleton(ILanguageServerExtension, LanguageServerExtension); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton(IExtensionActivationService, PartialModeStatusItem); serviceManager.add(IExtensionActivationManager, ExtensionActivationManager); - serviceManager.add(ILanguageServerActivator, JediExtensionActivator, LanguageServerActivator.Jedi); - serviceManager.add(ILanguageServerActivator, LanguageServerExtensionActivator, LanguageServerActivator.DotNet); - serviceManager.addSingleton(IPythonExtensionBanner, LanguageServerSurveyBanner, BANNER_NAME_LS_SURVEY); - serviceManager.addSingleton(IPythonExtensionBanner, ProposeLanguageServerBanner, BANNER_NAME_PROPOSE_LS); - serviceManager.addSingleton(IPythonExtensionBanner, DataScienceSurveyBanner, BANNER_NAME_DS_SURVEY); - serviceManager.addSingleton(IPythonExtensionBanner, InteractiveShiftEnterBanner, BANNER_NAME_INTERACTIVE_SHIFTENTER); - serviceManager.addSingleton(ILanguageServerFolderService, LanguageServerFolderService); - serviceManager.addSingleton(ILanguageServerPackageService, LanguageServerPackageService); - serviceManager.addSingleton(INugetRepository, StableLanguageServerPackageRepository, LanguageServerDownloadChannel.stable); - serviceManager.addSingleton(INugetRepository, BetaLanguageServerPackageRepository, LanguageServerDownloadChannel.beta); - serviceManager.addSingleton(INugetRepository, DailyLanguageServerPackageRepository, LanguageServerDownloadChannel.daily); - serviceManager.addSingleton(IDownloadChannelRule, DownloadDailyChannelRule, LanguageServerDownloadChannel.daily); - serviceManager.addSingleton(IDownloadChannelRule, DownloadBetaChannelRule, LanguageServerDownloadChannel.beta); - serviceManager.addSingleton(IDownloadChannelRule, DownloadBetaChannelRule, LanguageServerDownloadChannel.stable); - serviceManager.addSingleton(ILanagueServerCompatibilityService, LanguageServerCompatibilityService); - serviceManager.addSingleton(ILanguageClientFactory, BaseLanguageClientFactory, LanguageClientFactory.base); - serviceManager.addSingleton(ILanguageClientFactory, DownloadedLanguageClientFactory, LanguageClientFactory.downloaded); - serviceManager.addSingleton(ILanguageClientFactory, SimpleLanguageClientFactory, LanguageClientFactory.simple); - serviceManager.addSingleton(ILanguageServerDownloader, LanguageServerDownloader); - serviceManager.addSingleton(IPlatformData, PlatformData); - serviceManager.add(ILanguageServerAnalysisOptions, LanguageServerAnalysisOptions); - serviceManager.addSingleton(ILanguageServer, LanguageServer); - serviceManager.add(ILanguageServerManager, LanguageServerManager); + serviceManager.addSingleton( + ILanguageServerOutputChannel, + LanguageServerOutputChannel, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + ExtensionSurveyPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + LoadLanguageServerExtension, + ); + + serviceManager.addSingleton(ILanguageServerWatcher, LanguageServerWatcher); + serviceManager.addBinding(ILanguageServerWatcher, IExtensionActivationService); + + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequirementsTxtLinkActivator, + ); } diff --git a/src/client/activation/types.ts b/src/client/activation/types.ts index 91e53db9a78d..e3b9b818691a 100644 --- a/src/client/activation/types.ts +++ b/src/client/activation/types.ts @@ -1,125 +1,110 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { SemVer } from 'semver'; -import { Event } from 'vscode'; -import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { NugetPackage } from '../common/nuget/types'; -import { IDisposable, LanguageServerDownloadChannels, Resource } from '../common/types'; - -export const IExtensionActivationManager = Symbol('IExtensionActivationManager'); -export interface IExtensionActivationManager extends IDisposable { - activate(): Promise; - activateWorkspace(resource: Resource): Promise; -} - -export const IExtensionActivationService = Symbol('IExtensionActivationService'); -/** - * Classes implementing this interface will have their `activate` methods - * invoked during the actiavtion of the extension. - * This is a great hook for extension activation code, i.e. you don't need to modify - * the `extension.ts` file to invoke some code when extension gets activated. - * @export - * @interface IExtensionActivationService - */ -export interface IExtensionActivationService { - activate(resource: Resource): Promise; -} - -export enum LanguageServerActivator { - Jedi = 'Jedi', - DotNet = 'DotNet' -} - -export const ILanguageServerActivator = Symbol('ILanguageServerActivator'); -export interface ILanguageServerActivator extends IDisposable { - activate(resource: Resource): Promise; -} - -export type FolderVersionPair = { path: string; version: SemVer }; -export const ILanguageServerFolderService = Symbol('ILanguageServerFolderService'); - -export interface ILanguageServerFolderService { - getLanguageServerFolderName(resource: Resource): Promise; - getLatestLanguageServerVersion(resource: Resource): Promise; - getCurrentLanguageServerDirectory(): Promise; -} - -export const ILanguageServerDownloader = Symbol('ILanguageServerDownloader'); - -export interface ILanguageServerDownloader { - downloadLanguageServer(destinationFolder: string, resource: Resource): Promise; -} - -export const ILanguageServerPackageService = Symbol('ILanguageServerPackageService'); -export interface ILanguageServerPackageService { - getNugetPackageName(): string; - getLatestNugetPackageVersion(resource: Resource): Promise; - getLanguageServerDownloadChannel(): LanguageServerDownloadChannels; -} - -export const MajorLanguageServerVersion = Symbol('MajorLanguageServerVersion'); -export const IDownloadChannelRule = Symbol('IDownloadChannelRule'); -export interface IDownloadChannelRule { - shouldLookForNewLanguageServer(currentFolder?: FolderVersionPair): Promise; -} -export const ILanguageServerCompatibilityService = Symbol('ILanguageServerCompatibilityService'); -export interface ILanguageServerCompatibilityService { - isSupported(): Promise; -} -export enum LanguageClientFactory { - base = 'base', - simple = 'simple', - downloaded = 'downloaded' -} -export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); -export interface ILanguageClientFactory { - createLanguageClient(resource: Resource, clientOptions: LanguageClientOptions, env?: NodeJS.ProcessEnv): Promise; -} -export const ILanguageServerAnalysisOptions = Symbol('ILanguageServerAnalysisOptions'); -export interface ILanguageServerAnalysisOptions extends IDisposable { - readonly onDidChange: Event; - initialize(resource: Resource): Promise; - getAnalysisOptions(): Promise; -} -export const ILanguageServerManager = Symbol('ILanguageServerManager'); -export interface ILanguageServerManager extends IDisposable { - start(resource: Resource): Promise; -} -export const ILanguageServerExtension = Symbol('ILanguageServerExtension'); -export interface ILanguageServerExtension extends IDisposable { - readonly invoked: Event; - loadExtensionArgs?: {}; - register(): void; -} -export const ILanguageServer = Symbol('ILanguageServer'); -export interface ILanguageServer extends IDisposable { - /** - * LanguageClient in use - */ - languageClient: LanguageClient | undefined; - start(resource: Resource, options: LanguageClientOptions): Promise; - /** - * Sends a request to LS so as to load other extensions. - * This is used as a plugin loader mechanism. - * Anyone (such as intellicode) wanting to interact with LS, needs to send this request to LS. - * @param {{}} [args] - * @memberof ILanguageServer - */ - loadExtension(args?: {}): void; -} - -export enum PlatformName { - Windows32Bit = 'win-x86', - Windows64Bit = 'win-x64', - Mac64Bit = 'osx-x64', - Linux64Bit = 'linux-x64' -} -export const IPlatformData = Symbol('IPlatformData'); -export interface IPlatformData { - readonly platformName: PlatformName; - readonly engineDllName: string; - readonly engineExecutableName: string; -} +// 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; + /** + * 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; +} + +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; +} + +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; + activate(): void; + deactivate(): void; +} + +export const ILanguageClientFactory = Symbol('ILanguageClientFactory'); +export interface ILanguageClientFactory { + createLanguageClient( + resource: Resource, + interpreter: PythonEnvironment | undefined, + clientOptions: LanguageClientOptions, + env?: NodeJS.ProcessEnv, + ): Promise; +} +export const ILanguageServerAnalysisOptions = Symbol('ILanguageServerAnalysisOptions'); +export interface ILanguageServerAnalysisOptions extends IDisposable { + readonly onDidChange: Event; + initialize(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; + getAnalysisOptions(): Promise; +} +export const ILanguageServerManager = Symbol('ILanguageServerManager'); +export interface ILanguageServerManager extends IDisposable { + start(resource: Resource, interpreter: PythonEnvironment | undefined): Promise; + connect(): void; + disconnect(): void; +} + +export const ILanguageServerProxy = Symbol('ILanguageServerProxy'); +export interface ILanguageServerProxy extends IDisposable { + start( + resource: Resource, + interpreter: PythonEnvironment | undefined, + options: LanguageClientOptions, + ): Promise; + stop(): Promise; + /** + * Sends a request to LS so as to load other extensions. + * This is used as a plugin loader mechanism. + * Anyone (such as intellicode) wanting to interact with LS, needs to send this request to LS. + */ + 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; +} diff --git a/src/client/api.ts b/src/client/api.ts index 177dc997397d..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 { RemoteDebuggerExternalLauncherScriptProvider } 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, + serviceManager: IServiceManager, + serviceContainer: IServiceContainer, + discoveryApi: IDiscoveryAPI, +): PythonExtension { + const configurationService = serviceContainer.get(IConfigurationService); + const interpreterService = serviceContainer.get(IInterpreterService); + serviceManager.addSingleton(JupyterExtensionIntegration, JupyterExtensionIntegration); + serviceManager.addSingleton( + JupyterExtensionPythonEnvironments, + JupyterExtensionPythonEnvironments, + ); + serviceManager.addSingleton( + TensorboardExtensionIntegration, + TensorboardExtensionIntegration, + ); + const jupyterPythonEnvApi = serviceContainer.get(JupyterExtensionPythonEnvironments); + const environments = buildEnvironmentApi(discoveryApi, serviceContainer, jupyterPythonEnvApi); + const jupyterIntegration = serviceContainer.get(JupyterExtensionIntegration); + jupyterIntegration.registerEnvApi(environments); + const tensorboardIntegration = serviceContainer.get( + TensorboardExtensionIntegration, + ); + const outputChannel = serviceContainer.get(ILanguageServerOutputChannel); -export interface IExtensionApi { - /** - * Promise indicating whether all parts of the extension have completed loading or not. - * @type {Promise} - * @memberof IExtensionApi - */ - ready: Promise; - debug: { + const api: PythonExtension & { /** - * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. - * Users can append another array of strings of what they want to execute along with relevant arguments to Python. - * E.g `['/Users/..../pythonVSCode/pythonFiles/ptvsd_launcher.py', '--host', 'localhost', '--port', '57039', '--wait']` - * @param {string} host - * @param {number} port - * @param {boolean} [waitUntilDebuggerAttaches=true] - * @returns {Promise} + * Internal API just for Jupyter, hence don't include in the official types. */ - getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; - }; -} - -// tslint:disable-next-line:no-any -export function buildApi(ready: Promise) { - 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; + /** + * 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, + * * `['']` + * * `['']` + * * `['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 { - return new RemoteDebuggerExternalLauncherScriptProvider().getLauncherArgs({ host, port, waitUntilDebuggerAttaches }); - } - } + async getRemoteLauncherCommand( + host: string, + port: number, + waitUntilDebuggerAttaches = true, + ): Promise { + return getDebugpyLauncherArgs({ + host, + port, + waitUntilDebuggerAttaches, + }); + }, + async getDebuggerPackagePath(): Promise { + 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 => client.start(), + stop: (client: BaseLanguageClient): Promise => 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; + debug: { + /** + * Generate an array of strings for commands to pass to the Python executable to launch the debugger for remote debugging. + * Users can append another array of strings of what they want to execute along with relevant arguments to Python. + * E.g `['/Users/..../pythonVSCode/python_files/lib/python/debugpy', '--listen', 'localhost:57039', '--wait-for-client']` + * @param host + * @param port + * @param waitUntilDebuggerAttaches Defaults to `true`. + */ + getRemoteLauncherCommand(host: string, port: number, waitUntilDebuggerAttaches: boolean): Promise; + + /** + * Gets the path to the debugger package used by the extension. + */ + getDebuggerPackagePath(): Promise; + }; + + /** + * These APIs provide a way for extensions to work with by python environments available in the user's machine + * as found by the Python extension. See + * https://github.com/microsoft/vscode-python/wiki/Python-Environment-APIs for usage examples and more. + */ + readonly environments: { + /** + * Returns the environment configured by user in settings. Note that this can be an invalid environment, use + * {@link resolveEnvironment} to get full details. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getActiveEnvironmentPath(resource?: Resource): EnvironmentPath; + /** + * Sets the active environment path for the python extension for the resource. Configuration target will always + * be the workspace folder. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + * @param resource : [optional] File or workspace to scope to a particular workspace folder. + */ + updateActiveEnvironmentPath( + environment: string | EnvironmentPath | Environment, + resource?: Resource, + ): Promise; + /** + * This event is triggered when the active environment setting changes. + */ + readonly onDidChangeActiveEnvironmentPath: Event; + /** + * Carries environments known to the extension at the time of fetching the property. Note this may not + * contain all environments in the system as a refresh might be going on. + * + * Only reports environments in the current workspace. + */ + readonly known: readonly Environment[]; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + readonly onDidChangeEnvironments: Event; + /** + * This API will trigger environment discovery, but only if it has not already happened in this VSCode session. + * Useful for making sure env list is up-to-date when the caller needs it for the first time. + * + * To force trigger a refresh regardless of whether a refresh was already triggered, see option + * {@link RefreshOptions.forceRefresh}. + * + * Note that if there is a refresh already going on then this returns the promise for that refresh. + * @param options Additional options for refresh. + * @param token A cancellation token that indicates a refresh is no longer needed. + */ + refreshEnvironments(options?: RefreshOptions, token?: CancellationToken): Promise; + /** + * Returns details for the given environment, or `undefined` if the env is invalid. + * @param environment : If string, it represents the full path to environment folder or python executable + * for the environment. Otherwise it can be {@link Environment} or {@link EnvironmentPath} itself. + */ + resolveEnvironment( + environment: Environment | EnvironmentPath | string, + ): Promise; + /** + * Returns the environment variables used by the extension for a resource, which includes the custom + * variables configured by user in `.env` files. + * @param resource : Uri of a file or workspace folder. This is used to determine the env in a multi-root + * scenario. If `undefined`, then the API returns what ever is set for the workspace. + */ + getEnvironmentVariables(resource?: Resource): EnvironmentVariables; + /** + * This event is fired when the environment variables for a resource change. Note it's currently not + * possible to detect if environment variables in the system change, so this only fires if custom + * environment variables are updated in `.env` files. + */ + readonly onDidEnvironmentVariablesChange: Event; + }; +} + +export type RefreshOptions = { + /** + * When `true`, force trigger a refresh regardless of whether a refresh was already triggered. Note this can be expensive so + * it's best to only use it if user manually triggers a refresh. + */ + forceRefresh?: boolean; +}; + +/** + * Details about the environment. Note the environment folder, type and name never changes over time. + */ +export type Environment = EnvironmentPath & { + /** + * Carries details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness if known at this moment. + */ + readonly bitness: Bitness | undefined; + /** + * Value of `sys.prefix` in sys module if known at this moment. + */ + readonly sysPrefix: string | undefined; + }; + /** + * Carries details if it is an environment, otherwise `undefined` in case of global interpreters and others. + */ + readonly environment: + | { + /** + * Type of the environment. + */ + readonly type: EnvironmentType; + /** + * Name to the environment if any. + */ + readonly name: string | undefined; + /** + * Uri of the environment folder. + */ + readonly folderUri: Uri; + /** + * Any specific workspace folder this environment is created for. + */ + readonly workspaceFolder: WorkspaceFolder | undefined; + } + | undefined; + /** + * Carries Python version information known at this moment, carries `undefined` for envs without python. + */ + readonly version: + | (VersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string | undefined; + }) + | undefined; + /** + * Tools/plugins which created the environment or where it came from. First value in array corresponds + * to the primary tool which manages the environment, which never changes over time. + * + * Array is empty if no tool is responsible for creating/managing the environment. Usually the case for + * global interpreters. + */ + readonly tools: readonly EnvironmentTools[]; +}; + +/** + * Derived form of {@link Environment} where certain properties can no longer be `undefined`. Meant to represent an + * {@link Environment} with complete information. + */ +export type ResolvedEnvironment = Environment & { + /** + * Carries complete details about python executable. + */ + readonly executable: { + /** + * Uri of the python interpreter/executable. Carries `undefined` in case an executable does not belong to + * the environment. + */ + readonly uri: Uri | undefined; + /** + * Bitness of the environment. + */ + readonly bitness: Bitness; + /** + * Value of `sys.prefix` in sys module. + */ + readonly sysPrefix: string; + }; + /** + * Carries complete Python version information, carries `undefined` for envs without python. + */ + readonly version: + | (ResolvedVersionInfo & { + /** + * Value of `sys.version` in sys module if known at this moment. + */ + readonly sysVersion: string; + }) + | undefined; +}; + +export type EnvironmentsChangeEvent = { + readonly env: Environment; + /** + * * "add": New environment is added. + * * "remove": Existing environment in the list is removed. + * * "update": New information found about existing environment. + */ + readonly type: 'add' | 'remove' | 'update'; +}; + +export type ActiveEnvironmentPathChangeEvent = EnvironmentPath & { + /** + * Resource the environment changed for. + */ + readonly resource: Resource | undefined; +}; + +/** + * Uri of a file inside a workspace or workspace folder itself. + */ +export type Resource = Uri | WorkspaceFolder; + +export type EnvironmentPath = { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; +}; + +/** + * Tool/plugin where the environment came from. It can be {@link KnownEnvironmentTools} or custom string which + * was contributed. + */ +export type EnvironmentTools = KnownEnvironmentTools | string; +/** + * Tools or plugins the Python extension currently has built-in support for. Note this list is expected to shrink + * once tools have their own separate extensions. + */ +export type KnownEnvironmentTools = + | 'Conda' + | 'Pipenv' + | 'Poetry' + | 'VirtualEnv' + | 'Venv' + | 'VirtualEnvWrapper' + | 'Pyenv' + | 'Hatch' + | 'Unknown'; + +/** + * Type of the environment. It can be {@link KnownEnvironmentTypes} or custom string which was contributed. + */ +export type EnvironmentType = KnownEnvironmentTypes | string; +/** + * Environment types the Python extension is aware of. Note this list is expected to shrink once tools have their + * own separate extensions, in which case they're expected to provide the type themselves. + */ +export type KnownEnvironmentTypes = 'VirtualEnvironment' | 'Conda' | 'Unknown'; + +/** + * Carries bitness for an environment. + */ +export type Bitness = '64-bit' | '32-bit' | 'Unknown'; + +/** + * The possible Python release levels. + */ +export type PythonReleaseLevel = 'alpha' | 'beta' | 'candidate' | 'final'; + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + readonly level: PythonReleaseLevel; + readonly serial: number; +}; + +export type VersionInfo = { + readonly major: number | undefined; + readonly minor: number | undefined; + readonly micro: number | undefined; + readonly release: PythonVersionRelease | undefined; +}; + +export type ResolvedVersionInfo = { + readonly major: number; + readonly minor: number; + readonly micro: number; + readonly release: PythonVersionRelease; +}; + +/** + * A record containing readonly keys. + */ +export type EnvironmentVariables = { readonly [key: string]: string | undefined }; + +export type EnvironmentVariablesChangeEvent = { + /** + * Workspace folder the environment variables changed for. + */ + readonly resource: WorkspaceFolder | undefined; + /** + * Updated value of environment variables. + */ + readonly env: EnvironmentVariables; +}; + +export const PVSC_EXTENSION_ID = 'ms-python.python'; + +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace PythonExtension { + /** + * Returns the API exposed by the Python extension in VS Code. + */ + export async function api(): Promise { + const extension = extensions.getExtension(PVSC_EXTENSION_ID); + if (extension === undefined) { + throw new Error(`Python extension is not installed or is disabled`); + } + if (!extension.isActive) { + await extension.activate(); + } + const pythonApi: PythonExtension = extension.exports; + return pythonApi; + } +} diff --git a/src/client/application/diagnostics/applicationDiagnostics.ts b/src/client/application/diagnostics/applicationDiagnostics.ts index 62eee04976f1..90d2ced8d0ae 100644 --- a/src/client/application/diagnostics/applicationDiagnostics.ts +++ b/src/client/application/diagnostics/applicationDiagnostics.ts @@ -1,64 +1,70 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; -import { isTestExecution, STANDARD_OUTPUT_CHANNEL } from '../../common/constants'; -import { ILogger, IOutputChannel, Resource } from '../../common/types'; +import { IWorkspaceService } from '../../common/application/types'; +import { isTestExecution } from '../../common/constants'; +import { Resource } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; +import { traceLog, traceVerbose } from '../../logging'; import { IApplicationDiagnostics } from '../types'; -import { IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from './types'; +import { IDiagnostic, IDiagnosticsService } from './types'; + +function log(diagnostics: IDiagnostic[]): void { + diagnostics.forEach((item) => { + const message = `Diagnostic Code: ${item.code}, Message: ${item.message}`; + switch (item.severity) { + case DiagnosticSeverity.Error: + case DiagnosticSeverity.Warning: { + traceLog(message); + break; + } + default: { + traceVerbose(message); + } + } + }); +} + +async function runDiagnostics(diagnosticServices: IDiagnosticsService[], resource: Resource): Promise { + await Promise.all( + diagnosticServices.map(async (diagnosticService) => { + const diagnostics = await diagnosticService.diagnose(resource); + if (diagnostics.length > 0) { + log(diagnostics); + await diagnosticService.handle(diagnostics); + } + }), + ); +} @injectable() export class ApplicationDiagnostics implements IApplicationDiagnostics { - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly outputChannel: IOutputChannel - ) { } - public register() { - this.serviceContainer.get(ISourceMapSupportService).register(); - } + constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) {} + + public register() {} + public async performPreStartupHealthCheck(resource: Resource): Promise { // When testing, do not perform health checks, as modal dialogs can be displayed. if (isTestExecution()) { return; } - const services = this.serviceContainer.getAll(IDiagnosticsService); + let services = this.serviceContainer.getAll(IDiagnosticsService); + const workspaceService = this.serviceContainer.get(IWorkspaceService); + if (!workspaceService.isTrusted) { + services = services.filter((item) => item.runInUntrustedWorkspace); + } // Perform these validation checks in the foreground. - await this.runDiagnostics(services.filter(item => !item.runInBackground), resource); + await runDiagnostics( + services.filter((item) => !item.runInBackground), + resource, + ); + // Perform these validation checks in the background. - this.runDiagnostics(services.filter(item => item.runInBackground), resource).ignoreErrors(); - } - private async runDiagnostics(diagnosticServices: IDiagnosticsService[], resource: Resource): Promise { - await Promise.all(diagnosticServices.map(async diagnosticService => { - const diagnostics = await diagnosticService.diagnose(resource); - if (diagnostics.length > 0) { - this.log(diagnostics); - await diagnosticService.handle(diagnostics); - } - })); - } - private log(diagnostics: IDiagnostic[]): void { - const logger = this.serviceContainer.get(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); - } - } - }); + 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 cd7227e9ebf9..8ce1c3b83184 100644 --- a/src/client/application/diagnostics/base.ts +++ b/src/client/application/diagnostics/base.ts @@ -7,6 +7,7 @@ import { injectable, unmanaged } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; import { IDisposable, IDisposableRegistry, Resource } from '../../common/types'; +import { asyncFilter } from '../../common/utils/arrayUtils'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; @@ -15,10 +16,15 @@ import { DiagnosticScope, IDiagnostic, IDiagnosticFilterService, IDiagnosticsSer @injectable() export abstract class BaseDiagnostic implements IDiagnostic { - constructor(public readonly code: DiagnosticCodes, 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 invokeHandler: 'always' | 'default' = 'default') { } + public readonly shouldShowPrompt = true, + public readonly invokeHandler: 'always' | 'default' = 'default', + ) {} } @injectable() @@ -28,8 +34,9 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi constructor( @unmanaged() private readonly supportedDiagnosticCodes: string[], @unmanaged() protected serviceContainer: IServiceContainer, - @unmanaged() disposableRegistry: IDisposableRegistry, - @unmanaged() public readonly runInBackground: Boolean = false + @unmanaged() protected disposableRegistry: IDisposableRegistry, + @unmanaged() public readonly runInBackground: boolean = false, + @unmanaged() public readonly runInUntrustedWorkspace: boolean = false, ) { this.filterService = serviceContainer.get(IDiagnosticFilterService); disposableRegistry.push(this); @@ -42,7 +49,10 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi if (diagnostics.length === 0) { return; } - const diagnosticsToHandle = diagnostics.filter(item => { + const diagnosticsToHandle = await asyncFilter(diagnostics, async (item) => { + if (!(await this.canHandle(item))) { + return false; + } if (item.invokeHandler && item.invokeHandler === 'always') { return true; } @@ -57,17 +67,12 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi } public async canHandle(diagnostic: IDiagnostic): Promise { sendTelemetryEvent(EventName.DIAGNOSTICS_MESSAGE, undefined, { code: diagnostic.code }); - return this.supportedDiagnosticCodes.filter(item => item === diagnostic.code).length > 0; + return this.supportedDiagnosticCodes.filter((item) => item === diagnostic.code).length > 0; } protected abstract onHandle(diagnostics: IDiagnostic[]): Promise; /** * Returns a key used to keep track of whether a diagnostic was handled or not. * So as to prevent handling/displaying messages multiple times for the same diagnostic. - * - * @protected - * @param {IDiagnostic} diagnostic - * @returns {string} - * @memberof BaseDiagnosticsService */ protected getDiagnosticsKey(diagnostic: IDiagnostic): string { if (diagnostic.scope === DiagnosticScope.Global) { @@ -75,6 +80,8 @@ export abstract class BaseDiagnosticsService implements IDiagnosticsService, IDi } const workspace = this.serviceContainer.get(IWorkspaceService); const workspaceFolder = diagnostic.resource ? workspace.getWorkspaceFolder(diagnostic.resource) : undefined; - return `${diagnostic.code}dbe75733-0407-4124-a1b2-ca769dc30523${workspaceFolder ? workspaceFolder.uri.fsPath : ''}`; + 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 52300440ad92..b8850b8bbeee 100644 --- a/src/client/application/diagnostics/checks/envPathVariable.ts +++ b/src/client/application/diagnostics/checks/envPathVariable.ts @@ -1,14 +1,14 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; import { DiagnosticSeverity } from 'vscode'; import { IApplicationEnvironment } from '../../../common/application/types'; import '../../../common/extensions'; import { IPlatformService } from '../../../common/platform/types'; import { ICurrentProcess, IDisposableRegistry, IPathUtils, Resource } from '../../../common/types'; +import { Common } from '../../../common/utils/localize'; import { IServiceContainer } from '../../../ioc/types'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; @@ -17,17 +17,17 @@ import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '. 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.'; + "The environment variable '{0}' seems to have some paths containing the '\"' character." + + " The existence of such a character is known to have caused the {1} extension to not load. If the extension fails to load please modify your paths to remove this '\"' character."; -export class InvalidEnvironmentPathVariableDiagnostic extends BaseDiagnostic { +class InvalidEnvironmentPathVariableDiagnostic extends BaseDiagnostic { constructor(message: string, resource: Resource) { super( DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic, message, DiagnosticSeverity.Warning, DiagnosticScope.Global, - resource + resource, ); } } @@ -37,25 +37,36 @@ export const EnvironmentPathVariableDiagnosticsServiceId = 'EnvironmentPathVaria @injectable() export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService; + private readonly platform: IPlatformService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry) { - super([DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], serviceContainer, disposableRegistry, true); + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super( + [DiagnosticCodes.InvalidEnvironmentPathVariableDiagnostic], + serviceContainer, + disposableRegistry, + true, + true, + ); this.platform = this.serviceContainer.get(IPlatformService); this.messageService = serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); } + public async diagnose(resource: Resource): Promise { if (this.platform.isWindows && this.doesPathVariableHaveInvalidEntries()) { const env = this.serviceContainer.get(IApplicationEnvironment); const message = InvalidEnvPathVariableMessage.format(this.platform.pathVariableName, env.extensionName); return [new InvalidEnvironmentPathVariableDiagnostic(message, resource)]; - } else { - return []; } + return []; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { @@ -68,25 +79,26 @@ export class EnvironmentPathVariableDiagnosticsService extends BaseDiagnosticsSe const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); const options = [ { - prompt: 'Ignore' + prompt: Common.ignore, }, { - prompt: 'Always Ignore', - command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }) + prompt: Common.alwaysIgnore, + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }), }, { - prompt: 'More Info', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/Niq35h' }) - } + prompt: Common.moreInfo, + command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/Niq35h' }), + }, ]; await this.messageService.handle(diagnostic, { commandPrompts: options }); } + private doesPathVariableHaveInvalidEntries() { const currentProc = this.serviceContainer.get(ICurrentProcess); const pathValue = currentProc.env[this.platform.pathVariableName]; const pathSeparator = this.serviceContainer.get(IPathUtils).delimiter; const paths = (pathValue || '').split(pathSeparator); - return paths.filter(item => item.indexOf('"') >= 0).length > 0; + return paths.filter((item) => item.indexOf('"') >= 0).length > 0; } } diff --git a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts index 6adefa24da9c..440ff16856d3 100644 --- a/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidLaunchJsonDebugger.ts @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { DiagnosticSeverity, WorkspaceFolder } from 'vscode'; @@ -18,23 +17,29 @@ import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '. import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; const messages = { - [DiagnosticCodes.InvalidDebuggerTypeDiagnostic]: - Diagnostics.invalidDebuggerTypeDiagnostic(), - [DiagnosticCodes.JustMyCodeDiagnostic]: - Diagnostics.justMyCodeDiagnostic(), - [DiagnosticCodes.ConsoleTypeDiagnostic]: - Diagnostics.consoleTypeDiagnostic() + [DiagnosticCodes.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, resource: Resource) { + constructor( + code: + | DiagnosticCodes.InvalidDebuggerTypeDiagnostic + | DiagnosticCodes.JustMyCodeDiagnostic + | DiagnosticCodes.ConsoleTypeDiagnostic + | DiagnosticCodes.ConfigPythonPathDiagnostic, + resource: Resource, + shouldShowPrompt = true, + ) { super( code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource, - 'always' + shouldShowPrompt, ); } } @@ -50,31 +55,51 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IDiagnosticHandlerService) @named(DiagnosticCommandPromptHandlerServiceId) - private readonly messageService: IDiagnosticHandlerService + private readonly messageService: IDiagnosticHandlerService, ) { - super([DiagnosticCodes.InvalidDebuggerTypeDiagnostic, DiagnosticCodes.JustMyCodeDiagnostic, DiagnosticCodes.ConsoleTypeDiagnostic], serviceContainer, disposableRegistry, true); + super( + [ + DiagnosticCodes.InvalidDebuggerTypeDiagnostic, + DiagnosticCodes.JustMyCodeDiagnostic, + DiagnosticCodes.ConsoleTypeDiagnostic, + DiagnosticCodes.ConfigPythonPathDiagnostic, + ], + serviceContainer, + disposableRegistry, + true, + ); } + public async diagnose(resource: Resource): Promise { - if (!this.workspaceService.hasWorkspaceFolders) { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { return []; } - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource)! : this.workspaceService.workspaceFolders![0]; + const workspaceFolder = resource + ? this.workspaceService.getWorkspaceFolder(resource)! + : this.workspaceService.workspaceFolders![0]; return this.diagnoseWorkspace(workspaceFolder, resource); } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { - diagnostics.forEach(diagnostic => this.handleDiagnostic(diagnostic)); + diagnostics.forEach((diagnostic) => this.handleDiagnostic(diagnostic)); } - protected async fixLaunchJson(code: DiagnosticCodes) { - if (!this.workspaceService.hasWorkspaceFolders) { + + protected async fixLaunchJson(code: DiagnosticCodes): Promise { + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { return; } await Promise.all( - this.workspaceService.workspaceFolders!.map(workspaceFolder => this.fixLaunchJsonInWorkspace(code, workspaceFolder)) + (this.workspaceService.workspaceFolders ?? []).map((workspaceFolder) => + this.fixLaunchJsonInWorkspace(code, workspaceFolder), + ), ); } + private async diagnoseWorkspace(workspaceFolder: WorkspaceFolder, resource: Resource) { - const launchJson = this.getLaunchJsonFile(workspaceFolder); + const launchJson = getLaunchJsonFile(workspaceFolder); if (!(await this.fs.fileExists(launchJson))) { return []; } @@ -82,7 +107,9 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { const fileContents = await this.fs.readFile(launchJson); const diagnostics: IDiagnostic[] = []; if (fileContents.indexOf('"pythonExperimental"') > 0) { - diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource)); + diagnostics.push( + new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, resource), + ); } if (fileContents.indexOf('"debugStdLib"') > 0) { diagnostics.push(new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, resource)); @@ -90,48 +117,74 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { 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 { - if (!this.canHandle(diagnostic)) { + if (!diagnostic.shouldShowPrompt) { + await this.fixLaunchJson(diagnostic.code); return; } const commandPrompts = [ { - prompt: Diagnostics.yesUpdateLaunch(), + prompt: Diagnostics.yesUpdateLaunch, command: { diagnostic, invoke: async (): Promise => { await this.fixLaunchJson(diagnostic.code); - } - } + }, + }, }, { - prompt: Common.noIWillDoItLater() - } + prompt: Common.noIWillDoItLater, + }, ]; await this.messageService.handle(diagnostic, { commandPrompts }); } + private async fixLaunchJsonInWorkspace(code: DiagnosticCodes, workspaceFolder: WorkspaceFolder) { if ((await this.diagnoseWorkspace(workspaceFolder, undefined)).length === 0) { return; } - const launchJson = this.getLaunchJsonFile(workspaceFolder); + const launchJson = getLaunchJsonFile(workspaceFolder); let fileContents = await this.fs.readFile(launchJson); switch (code) { case DiagnosticCodes.InvalidDebuggerTypeDiagnostic: { - fileContents = this.findAndReplace(fileContents, '"pythonExperimental"', '"python"'); - fileContents = this.findAndReplace(fileContents, '"Python Experimental:', '"Python:'); + fileContents = findAndReplace(fileContents, '"pythonExperimental"', '"python"'); + fileContents = findAndReplace(fileContents, '"Python Experimental:', '"Python:'); break; } case DiagnosticCodes.JustMyCodeDiagnostic: { - fileContents = this.findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); - fileContents = this.findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); + fileContents = findAndReplace(fileContents, '"debugStdLib": false', '"justMyCode": true'); + fileContents = findAndReplace(fileContents, '"debugStdLib": true', '"justMyCode": false'); break; } case DiagnosticCodes.ConsoleTypeDiagnostic: { - fileContents = this.findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); + fileContents = findAndReplace(fileContents, '"console": "none"', '"console": "internalConsole"'); + break; + } + case DiagnosticCodes.ConfigPythonPathDiagnostic: { + fileContents = findAndReplace(fileContents, '"pythonPath":', '"python":'); + fileContents = findAndReplace( + fileContents, + '{config:python.pythonPath}', + '{command:python.interpreterPath}', + ); + fileContents = findAndReplace( + fileContents, + '{config:python.interpreterPath}', + '{command:python.interpreterPath}', + ); break; } default: { @@ -141,11 +194,13 @@ export class InvalidLaunchJsonDebuggerService extends BaseDiagnosticsService { await this.fs.writeFile(launchJson, fileContents); } - private findAndReplace(fileContents: string, search: string, replace: string) { - const searchRegex = new RegExp(search, 'g'); - return fileContents.replace(searchRegex, replace); - } - private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); - } +} + +function findAndReplace(fileContents: string, search: string, replace: string) { + const searchRegex = new RegExp(search, 'g'); + return fileContents.replace(searchRegex, replace); +} + +function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { + return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); } diff --git a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts index 9d63a2ffcc76..f08c09956838 100644 --- a/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts +++ b/src/client/application/diagnostics/checks/invalidPythonPathInDebugger.ts @@ -1,20 +1,19 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable, named } from 'inversify'; import * as path from 'path'; import { DiagnosticSeverity, Uri, workspace as workspc, WorkspaceFolder } from 'vscode'; import { IDocumentManager, IWorkspaceService } from '../../../common/application/types'; import '../../../common/extensions'; -import { traceError } from '../../../common/logger'; import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; -import { Diagnostics } from '../../../common/utils/localize'; +import { Common, Diagnostics } from '../../../common/utils/localize'; import { SystemVariables } from '../../../common/variables/systemVariables'; import { PythonPathSource } from '../../../debugger/extension/types'; import { IInterpreterHelper } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; +import { traceError } from '../../../logging'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; import { IDiagnosticsCommandFactory } from '../commands/types'; import { DiagnosticCodes } from '../constants'; @@ -24,25 +23,29 @@ import { IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService, - IInvalidPythonPathInDebuggerService + IInvalidPythonPathInDebuggerService, } from '../types'; const messages = { - [DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic]: - Diagnostics.invalidPythonPathInDebuggerSettings(), - [DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic]: - Diagnostics.invalidPythonPathInDebuggerLaunch() + [DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic]: Diagnostics.invalidPythonPathInDebuggerSettings, + [DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic]: Diagnostics.invalidPythonPathInDebuggerLaunch, }; -export class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { - constructor(code: DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic | DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, resource: Resource) { +class InvalidPythonPathInDebuggerDiagnostic extends BaseDiagnostic { + constructor( + code: + | DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic + | DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, + resource: Resource, + ) { super( code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource, - 'always' + undefined, + 'always', ); } } @@ -62,25 +65,32 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(IDiagnosticHandlerService) @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService + protected readonly messageService: IDiagnosticHandlerService, ) { super( [ DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic + DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic, ], serviceContainer, disposableRegistry, - true + true, ); } - public async diagnose(_resource: Resource): Promise { + + // eslint-disable-next-line class-methods-use-this + public async diagnose(): Promise { return []; } - public async validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri) { + + public async validatePythonPath( + pythonPath?: string, + pythonPathSource?: PythonPathSource, + resource?: Uri, + ): Promise { pythonPath = pythonPath ? this.resolveVariables(pythonPath, resource) : undefined; - // tslint:disable-next-line:no-invalid-template-strings - if (pythonPath === '${config:python.pythonPath}' || !pythonPath) { + + if (pythonPath === '${command:python.interpreterPath}' || !pythonPath) { pythonPath = this.configService.getSettings(resource).pythonPath; } if (await this.interpreterHelper.getInterpreterInformation(pythonPath).catch(() => undefined)) { @@ -88,16 +98,27 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService } traceError(`Invalid Python Path '${pythonPath}'`); 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)) + 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)) + 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 { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { @@ -108,45 +129,47 @@ export class InvalidPythonPathInDebuggerService extends BaseDiagnosticsService 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: 'Select Python Interpreter', + prompt: Common.selectPythonInterpreter, command: this.commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', - options: 'python.setInterpreter' - }) - } + options: 'python.setInterpreter', + }), + }, ]; } case DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic: { return [ { - prompt: 'Open launch.json', + prompt: Common.openLaunch, command: { diagnostic, invoke: async (): Promise => { - const launchJson = this.getLaunchJsonFile(workspc.workspaceFolders![0]); + const launchJson = getLaunchJsonFile(workspc.workspaceFolders![0]); const doc = await this.documentManager.openTextDocument(launchJson); await this.documentManager.showTextDocument(doc); - } - } - } + }, + }, + }, ]; } default: { - throw new Error('Invalid diagnostic for \'InvalidPythonPathInDebuggerService\''); + throw new Error("Invalid diagnostic for 'InvalidPythonPathInDebuggerService'"); } } } - private getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { - return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); - } +} + +function getLaunchJsonFile(workspaceFolder: WorkspaceFolder) { + return path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); } diff --git a/src/client/application/diagnostics/checks/jediPython27NotSupported.ts b/src/client/application/diagnostics/checks/jediPython27NotSupported.ts new file mode 100644 index 000000000000..3d358325032e --- /dev/null +++ b/src/client/application/diagnostics/checks/jediPython27NotSupported.ts @@ -0,0 +1,108 @@ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, named } from 'inversify'; +import { ConfigurationTarget, DiagnosticSeverity } from 'vscode'; +import { LanguageServerType } from '../../../activation/types'; +import { IWorkspaceService } from '../../../common/application/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import { Common, Python27Support } from '../../../common/utils/localize'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { IDiagnosticsCommandFactory } from '../commands/types'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export class JediPython27NotSupportedDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.JediPython27NotSupportedDiagnostic, + message, + DiagnosticSeverity.Warning, + DiagnosticScope.Global, + resource, + ); + } +} + +export const JediPython27NotSupportedDiagnosticServiceId = 'JediPython27NotSupportedDiagnosticServiceId'; + +export class JediPython27NotSupportedDiagnosticService extends BaseDiagnosticsService { + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true); + } + + public async diagnose(resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const { languageServer } = this.configurationService.getSettings(resource); + + await this.updateLanguageServerSetting(resource); + + // We don't need to check for JediLSP here, because we retrieve the setting from the configuration service, + // Which already switched the JediLSP option to Jedi. + if (interpreter && (interpreter.version?.major ?? 0) < 3 && languageServer === LanguageServerType.Jedi) { + return [new JediPython27NotSupportedDiagnostic(Python27Support.jediMessage, resource)]; + } + + return []; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); + const options = [ + { + prompt: Common.gotIt, + }, + { + prompt: Common.doNotShowAgain, + command: commandFactory.createCommand(diagnostic, { type: 'ignore', options: DiagnosticScope.Global }), + }, + ]; + + await this.messageService.handle(diagnostic, { commandPrompts: options }); + } + + private async updateLanguageServerSetting(resource: Resource): Promise { + // Update settings.json value to Jedi if it's JediLSP. + const settings = this.workspaceService + .getConfiguration('python', resource) + .inspect('languageServer'); + + let configTarget: ConfigurationTarget; + + if (settings?.workspaceValue === LanguageServerType.JediLSP) { + configTarget = ConfigurationTarget.Workspace; + } else if (settings?.globalValue === LanguageServerType.JediLSP) { + configTarget = ConfigurationTarget.Global; + } else { + return; + } + + await this.configurationService.updateSetting( + 'languageServer', + LanguageServerType.Jedi, + resource, + configTarget, + ); + } +} diff --git a/src/client/application/diagnostics/checks/lsNotSupported.ts b/src/client/application/diagnostics/checks/lsNotSupported.ts deleted file mode 100644 index 6b63dfcece3e..000000000000 --- a/src/client/application/diagnostics/checks/lsNotSupported.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, named } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; -import { ILanguageServerCompatibilityService } from '../../../activation/types'; -import { IDisposableRegistry, Resource } from '../../../common/types'; -import { Diagnostics } from '../../../common/utils/localize'; -import { IServiceContainer } from '../../../ioc/types'; -import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; -import { IDiagnosticsCommandFactory } from '../commands/types'; -import { DiagnosticCodes } from '../constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; - -export class LSNotSupportedDiagnostic extends BaseDiagnostic { - constructor(message: string, resource: Resource) { - super( - DiagnosticCodes.LSNotSupportedDiagnostic, - message, - DiagnosticSeverity.Warning, - DiagnosticScope.Global, - resource - ); - } -} - -export const LSNotSupportedDiagnosticServiceId = 'LSNotSupportedDiagnosticServiceId'; - -export class LSNotSupportedDiagnosticService extends BaseDiagnosticsService { - constructor( - @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(ILanguageServerCompatibilityService) - private readonly lsCompatibility: ILanguageServerCompatibilityService, - @inject(IDiagnosticHandlerService) - @named(DiagnosticCommandPromptHandlerServiceId) - protected readonly messageService: IDiagnosticHandlerService, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry - ) { - super([DiagnosticCodes.LSNotSupportedDiagnostic], serviceContainer, disposableRegistry, false); - } - public async diagnose(resource: Resource): Promise { - if (await this.lsCompatibility.isSupported()) { - return []; - } else { - return [new LSNotSupportedDiagnostic(Diagnostics.lsNotSupported(), resource)]; - } - } - protected async onHandle(diagnostics: IDiagnostic[]): Promise { - if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { - return; - } - const diagnostic = diagnostics[0]; - if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { - return; - } - const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - const options = [ - { - prompt: 'More Info', - command: commandFactory.createCommand(diagnostic, { type: 'launch', options: 'https://aka.ms/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 index 12799a7d9c9e..21d6b34fb7c5 100644 --- a/src/client/application/diagnostics/checks/macPythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/macPythonInterpreter.ts @@ -1,34 +1,35 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, DiagnosticSeverity, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; import { IPlatformService } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; -import { IInterpreterHelper, IInterpreterService, InterpreterType } from '../../../interpreter/contracts'; +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.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic]: - 'You have selected the macOS system install of Python, which is not recommended for use with the Python extension. Some functionality will be limited, please select a different interpreter.', - [DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic]: - 'The macOS system install of Python is not recommended, some functionality in the extension will be limited. Install another version of Python for the best experience.' + [DiagnosticCodes.MacInterpreterSelected]: l10n.t( + 'The selected macOS system install of Python is not recommended, some functionality in the extension will be limited. [Install another version of Python](https://www.python.org/downloads) or select a different interpreter for the best experience. [Learn more](https://aka.ms/AA7jfor).', + ), }; export class InvalidMacPythonInterpreterDiagnostic extends BaseDiagnostic { - constructor( - code: DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic | DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - resource: Resource - ) { + constructor(code: DiagnosticCodes.MacInterpreterSelected, resource: Resource) { super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource); } } @@ -38,153 +39,105 @@ export const InvalidMacPythonInterpreterServiceId = 'InvalidMacPythonInterpreter @injectable() export class InvalidMacPythonInterpreterService extends BaseDiagnosticsService { protected changeThrottleTimeout = 1000; - private timeOut?: NodeJS.Timer | number; + + private timeOut?: NodeJS.Timeout | number; + constructor( @inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(IPlatformService) private readonly platform: IPlatformService, - @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper + @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, ) { - super( - [DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic], - serviceContainer, - disposableRegistry, - true - ); + super([DiagnosticCodes.MacInterpreterSelected], serviceContainer, disposableRegistry, true); this.addPythonPathChangedHandler(); } - public dispose() { - if (this.timeOut) { - // tslint:disable-next-line: no-any - clearTimeout(this.timeOut as any); + + public dispose(): void { + if (this.timeOut && typeof this.timeOut !== 'number') { + clearTimeout(this.timeOut); this.timeOut = undefined; } } + public async diagnose(resource: Resource): Promise { if (!this.platform.isMac) { return []; } const configurationService = this.serviceContainer.get(IConfigurationService); const settings = configurationService.getSettings(resource); - if (settings.disableInstallationChecks === true) { - return []; - } - - const hasInterpreters = await this.interpreterService.hasInterpreters; - if (!hasInterpreters) { - return []; - } - - const currentInterpreter = await this.interpreterService.getActiveInterpreter(resource); - if (!currentInterpreter) { - return []; - } - - if (!this.helper.isMacDefaultPythonPath(settings.pythonPath)) { + if (!(await this.helper.isMacDefaultPythonPath(settings.pythonPath))) { return []; } - if (!currentInterpreter || currentInterpreter.type !== InterpreterType.Unknown) { - return []; - } - - const interpreters = await this.interpreterService.getInterpreters(resource); - if (interpreters.filter(i => !this.helper.isMacDefaultPythonPath(i.path)).length === 0) { - return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, resource)]; - } - - return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, resource)]; + return [new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelected, resource)]; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { return; } - const messageService = this.serviceContainer.get>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerServiceId); + const messageService = this.serviceContainer.get>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); await Promise.all( - diagnostics.map(async diagnostic => { + 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); - return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); - }) + await messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); + }), ); } - protected addPythonPathChangedHandler() { - const workspaceService = this.serviceContainer.get(IWorkspaceService); + + protected addPythonPathChangedHandler(): void { const disposables = this.serviceContainer.get(IDisposableRegistry); - disposables.push(workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this))); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + disposables.push(interpreterPathService.onDidChange((i) => this.onDidChangeConfiguration(i))); } - protected async onDidChangeConfiguration(event: ConfigurationChangeEvent) { - const workspaceService = this.serviceContainer.get(IWorkspaceService); - const workspacesUris: (Uri | undefined)[] = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; - const workspaceUriIndex = workspacesUris.findIndex(uri => event.affectsConfiguration('python.pythonPath', uri)); - if (workspaceUriIndex === -1) { - return; - } + + protected async onDidChangeConfiguration( + interpreterConfigurationScope: InterpreterConfigurationScope, + ): Promise { + const workspaceUri = interpreterConfigurationScope.uri; // Lets wait, for more changes, dirty simple throttling. - if (this.timeOut) { - // tslint:disable-next-line: no-any - clearTimeout(this.timeOut as any); + if (this.timeOut && typeof this.timeOut !== 'number') { + clearTimeout(this.timeOut); this.timeOut = undefined; } this.timeOut = setTimeout(() => { this.timeOut = undefined; - this.diagnose(workspacesUris[workspaceUriIndex]) - .then(diagnostics => this.handle(diagnostics)) + 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); switch (diagnostic.code) { - case DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic: { + case DiagnosticCodes.MacInterpreterSelected: { return [ { - prompt: 'Select Python Interpreter', + prompt: Common.selectPythonInterpreter, command: commandFactory.createCommand(diagnostic, { type: 'executeVSCCommand', - options: 'python.setInterpreter' - }) + options: 'python.setInterpreter', + }), }, { - prompt: 'Do not show again', + prompt: Common.doNotShowAgain, command: commandFactory.createCommand(diagnostic, { type: 'ignore', - options: DiagnosticScope.Global - }) - } - ]; - } - case DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic: { - return [ - { - prompt: 'Learn more', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' - }) - }, - { - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://www.python.org/downloads' - }) + options: DiagnosticScope.Global, + }), }, - { - prompt: 'Do not show again', - command: commandFactory.createCommand(diagnostic, { - type: 'ignore', - options: DiagnosticScope.Global - }) - } ]; } default: { - throw new Error('Invalid diagnostic for \'InvalidMacPythonInterpreterService\''); + 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 935ff72378e5..85f68db0d6a4 100644 --- a/src/client/application/diagnostics/checks/powerShellActivation.ts +++ b/src/client/application/diagnostics/checks/powerShellActivation.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import { Logger } from '../../../common/logger'; import { useCommandPromptAsDefaultShell } from '../../../common/terminal/commandPrompt'; import { IConfigurationService, ICurrentProcess, IDisposableRegistry, Resource } from '../../../common/types'; +import { Common } from '../../../common/utils/localize'; import { IServiceContainer } from '../../../ioc/types'; +import { traceError } from '../../../logging'; import { sendTelemetryEvent } from '../../../telemetry'; import { EventName } from '../../../telemetry/constants'; import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; @@ -18,8 +18,9 @@ import { DiagnosticCodes } from '../constants'; import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; -const PowershellActivationNotSupportedWithBatchFilesMessage = - 'Activation of the selected Python environment is not supported in PowerShell. Consider changing your shell to Command Prompt.'; +const PowershellActivationNotSupportedWithBatchFilesMessage = l10n.t( + 'Activation of the selected Python environment is not supported in PowerShell. Consider changing your shell to Command Prompt.', +); export class PowershellActivationNotAvailableDiagnostic extends BaseDiagnostic { constructor(resource: Resource) { @@ -29,7 +30,8 @@ export class PowershellActivationNotAvailableDiagnostic extends BaseDiagnostic { DiagnosticSeverity.Warning, DiagnosticScope.Global, resource, - 'always' + undefined, + 'always', ); } } @@ -40,22 +42,28 @@ export const PowerShellActivationHackDiagnosticsServiceId = @injectable() export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsService { protected readonly messageService: IDiagnosticHandlerService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry) { + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { super( [DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic], serviceContainer, disposableRegistry, - true + true, ); this.messageService = serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); } - public async diagnose(_resource: Resource): Promise { + + // eslint-disable-next-line class-methods-use-this + public async diagnose(): Promise { return []; } + protected async onHandle(diagnostics: IDiagnostic[]): Promise { // This class can only handle one type of diagnostic, hence just use first item in list. if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { @@ -70,34 +78,34 @@ export class PowerShellActivationHackDiagnosticsService extends BaseDiagnosticsS const configurationService = this.serviceContainer.get(IConfigurationService); const options = [ { - prompt: 'Use Command Prompt', - // tslint:disable-next-line:no-object-literal-type-assertion + prompt: Common.useCommandPrompt, + command: { diagnostic, invoke: async (): Promise => { sendTelemetryEvent(EventName.DIAGNOSTICS_ACTION, undefined, { - action: 'switchToCommandPrompt' + action: 'switchToCommandPrompt', }); - useCommandPromptAsDefaultShell(currentProcess, configurationService).catch(ex => - Logger.error('Use Command Prompt as default shell', ex) + 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', + prompt: Common.moreInfo, command: commandFactory.createCommand(diagnostic, { type: 'launch', - options: 'https://aka.ms/CondaPwsh' - }) - } + options: 'https://aka.ms/CondaPwsh', + }), + }, ]; await this.messageService.handle(diagnostic, { commandPrompts: options }); diff --git a/src/client/application/diagnostics/checks/pylanceDefault.ts b/src/client/application/diagnostics/checks/pylanceDefault.ts new file mode 100644 index 000000000000..16ee2968c8d6 --- /dev/null +++ b/src/client/application/diagnostics/checks/pylanceDefault.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { inject, named } from 'inversify'; +import { DiagnosticSeverity } from 'vscode'; +import { IDisposableRegistry, IExtensionContext, Resource } from '../../../common/types'; +import { Diagnostics, Common } from '../../../common/utils/localize'; +import { IServiceContainer } from '../../../ioc/types'; +import { BaseDiagnostic, BaseDiagnosticsService } from '../base'; +import { DiagnosticCodes } from '../constants'; +import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../promptHandler'; +import { DiagnosticScope, IDiagnostic, IDiagnosticHandlerService } from '../types'; + +export const PYLANCE_PROMPT_MEMENTO = 'pylanceDefaultPromptMemento'; +const EXTENSION_VERSION_MEMENTO = 'extensionVersion'; + +export class PylanceDefaultDiagnostic extends BaseDiagnostic { + constructor(message: string, resource: Resource) { + super( + DiagnosticCodes.PylanceDefaultDiagnostic, + message, + DiagnosticSeverity.Information, + DiagnosticScope.Global, + resource, + ); + } +} + +export const PylanceDefaultDiagnosticServiceId = 'PylanceDefaultDiagnosticServiceId'; + +export class PylanceDefaultDiagnosticService extends BaseDiagnosticsService { + public initialMementoValue: string | undefined = undefined; + + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, + @inject(IExtensionContext) private readonly context: IExtensionContext, + @inject(IDiagnosticHandlerService) + @named(DiagnosticCommandPromptHandlerServiceId) + protected readonly messageService: IDiagnosticHandlerService, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.PylanceDefaultDiagnostic], serviceContainer, disposableRegistry, true, true); + + this.initialMementoValue = this.context.globalState.get(EXTENSION_VERSION_MEMENTO); + } + + public async diagnose(resource: Resource): Promise { + if (!(await this.shouldShowPrompt())) { + return []; + } + + return [new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage, resource)]; + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + const options = [{ prompt: Common.ok }]; + + await this.messageService.handle(diagnostic, { + commandPrompts: options, + onClose: this.updateMemento.bind(this), + }); + } + + private async updateMemento() { + await this.context.globalState.update(PYLANCE_PROMPT_MEMENTO, true); + } + + private async shouldShowPrompt(): Promise { + const savedVersion: string | undefined = this.initialMementoValue; + const promptShown: boolean | undefined = this.context.globalState.get(PYLANCE_PROMPT_MEMENTO); + + // savedVersion being undefined means that this is the first time the user activates the extension, + // and we don't want to show the prompt to first-time users. + // We set PYLANCE_PROMPT_MEMENTO here to skip the prompt + // in case the user reloads the extension and savedVersion becomes set + if (savedVersion === undefined) { + await this.updateMemento(); + return false; + } + + // promptShown being undefined means that this is the first time we check if we should show the prompt. + return promptShown === undefined; + } +} diff --git a/src/client/application/diagnostics/checks/pythonInterpreter.ts b/src/client/application/diagnostics/checks/pythonInterpreter.ts index 7f2a96540c2d..9167e232a417 100644 --- a/src/client/application/diagnostics/checks/pythonInterpreter.ts +++ b/src/client/application/diagnostics/checks/pythonInterpreter.ts @@ -1,121 +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 { DiagnosticSeverity } from 'vscode'; +import { DiagnosticSeverity, l10n } from 'vscode'; import '../../../common/extensions'; -import { IConfigurationService, IDisposableRegistry, Resource } from '../../../common/types'; +import * as path from 'path'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService, Resource } from '../../../common/types'; import { IInterpreterService } from '../../../interpreter/contracts'; import { IServiceContainer } from '../../../ioc/types'; import { 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.NoCurrentlySelectedPythonInterpreterDiagnostic]: - 'No Python interpreter is selected. You need to select a Python interpreter to enable features such as IntelliSense, linting, and debugging.' + [DiagnosticCodes.NoPythonInterpretersDiagnostic]: l10n.t( + 'No Python interpreter is selected. Please select a Python interpreter to enable features such as IntelliSense, linting, and debugging.', + ), + [DiagnosticCodes.InvalidPythonInterpreterDiagnostic]: l10n.t( + 'An Invalid Python interpreter is selected{0}, please try changing it to enable features such as IntelliSense, linting, and debugging. See output for more details regarding why the interpreter is invalid.', + ), + [DiagnosticCodes.InvalidComspecDiagnostic]: l10n.t( + 'We detected an issue with one of your environment variables that breaks features such as IntelliSense, linting and debugging. Try setting the "ComSpec" variable to a valid Command Prompt path in your system to fix it.', + ), + [DiagnosticCodes.IncompletePathVarDiagnostic]: l10n.t( + 'We detected an issue with "Path" environment variable that breaks features such as IntelliSense, linting and debugging. Please edit it to make sure it contains the "System32" subdirectories.', + ), + [DiagnosticCodes.DefaultShellErrorDiagnostic]: l10n.t( + 'We detected an issue with your default shell that breaks features such as IntelliSense, linting and debugging. Try resetting "ComSpec" and "Path" environment variables to fix it.', + ), }; export class InvalidPythonInterpreterDiagnostic extends BaseDiagnostic { - constructor(code: DiagnosticCodes.NoPythonInterpretersDiagnostic | DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, resource: Resource) { - super(code, messages[code], DiagnosticSeverity.Error, DiagnosticScope.WorkspaceFolder, resource); + 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 { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry) { +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.NoCurrentlySelectedPythonInterpreterDiagnostic + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + DiagnosticCodes.InvalidComspecDiagnostic, + DiagnosticCodes.IncompletePathVarDiagnostic, + DiagnosticCodes.DefaultShellErrorDiagnostic, ], serviceContainer, disposableRegistry, - false + false, ); } + + public async activate(): Promise { + const commandManager = this.serviceContainer.get(ICommandManager); + this.disposableRegistry.push( + commandManager.registerCommand(Commands.TriggerEnvironmentSelection, (resource: Resource) => + this.triggerEnvSelectionIfNecessary(resource), + ), + ); + const interpreterService = this.serviceContainer.get(IInterpreterService); + this.disposableRegistry.push( + interpreterService.onDidChangeInterpreterConfiguration((e) => + commandManager.executeCommand(Commands.TriggerEnvironmentSelection, e).then(noop, noop), + ), + ); + } + public async diagnose(resource: Resource): Promise { - const configurationService = this.serviceContainer.get(IConfigurationService); - const settings = configurationService.getSettings(resource); - if (settings.disableInstallationChecks === true) { - return []; - } + return this.diagnoseDefaultShell(resource); + } + public async _manualDiagnose(resource: Resource): Promise { + const workspaceService = this.serviceContainer.get(IWorkspaceService); const interpreterService = this.serviceContainer.get(IInterpreterService); - const hasInterpreters = await interpreterService.hasInterpreters; + const diagnostics = await this.diagnoseDefaultShell(resource); + if (diagnostics.length > 0) { + return diagnostics; + } + const hasInterpreters = await interpreterService.hasInterpreters(); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const isInterpreterSetToDefault = interpreterPathService.get(resource) === 'python'; - if (!hasInterpreters) { - return [new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, resource)]; + if (!hasInterpreters && isInterpreterSetToDefault) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } + return [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.NoPythonInterpretersDiagnostic, + resource, + workspaceService, + DiagnosticScope.Global, + ), + ]; } const currentInterpreter = await interpreterService.getActiveInterpreter(resource); if (!currentInterpreter) { + if (useEnvExtension()) { + traceWarn(Interpreters.envExtNoActiveEnvironment); + } return [ new InvalidPythonInterpreterDiagnostic( - DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic, - resource - ) + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + resource, + workspaceService, + ), ]; } + return []; + } + public async triggerEnvSelectionIfNecessary(resource: Resource): Promise { + const diagnostics = await this._manualDiagnose(resource); + if (!diagnostics.length) { + return true; + } + this.handle(diagnostics).ignoreErrors(); + return false; + } + + private async diagnoseDefaultShell(resource: Resource): Promise { + if (getOSType() !== OSType.Windows) { + return []; + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const currentInterpreter = await interpreterService.getActiveInterpreter(resource); + if (currentInterpreter) { + return []; + } + try { + await this.shellExecPython(); + } catch (ex) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + if ((ex as any).errno === -4058) { + // ENOENT (-4058) error is thrown by Node when the default shell is invalid. + traceError('ComSpec is likely set to an invalid value', getEnvironmentVariable('ComSpec')); + if (await this.isComspecInvalid()) { + return [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, resource)]; + } + if (this.isPathVarIncomplete()) { + traceError('PATH env var appears to be incomplete', process.env.Path, process.env.PATH); + return [new DefaultShellDiagnostic(DiagnosticCodes.IncompletePathVarDiagnostic, resource)]; + } + return [new DefaultShellDiagnostic(DiagnosticCodes.DefaultShellErrorDiagnostic, resource)]; + } + } return []; } + + private async isComspecInvalid() { + const comSpec = getEnvironmentVariable('ComSpec') ?? ''; + const fs = this.serviceContainer.get(IFileSystem); + return fs.fileExists(comSpec).then((exists) => !exists); + } + + // eslint-disable-next-line class-methods-use-this + private isPathVarIncomplete() { + const envVars = getSearchPathEnvVarNames(); + const systemRoot = getEnvironmentVariable('SystemRoot') ?? 'C:\\WINDOWS'; + const system32 = path.join(systemRoot, 'system32'); + for (const envVar of envVars) { + const value = getEnvironmentVariable(envVar); + if (value && normCasePath(value).includes(normCasePath(system32))) { + return false; + } + } + return true; + } + + @cache(-1, true) + // eslint-disable-next-line class-methods-use-this + private async shellExecPython() { + const configurationService = this.serviceContainer.get(IConfigurationService); + const { pythonPath } = configurationService.getSettings(); + const [args] = getExecutable(); + const argv = [pythonPath, ...args]; + // Concat these together to make a set of quoted strings + const quoted = argv.reduce( + (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); + const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + const service = await processServiceFactory.create(); + return service.shellExec(quoted, { timeout: 15000 }); + } + + @cache(1000, true) // This is to handle throttling of multiple events. protected async onHandle(diagnostics: IDiagnostic[]): Promise { if (diagnostics.length === 0) { return; } const messageService = this.serviceContainer.get>( IDiagnosticHandlerService, - DiagnosticCommandPromptHandlerServiceId + DiagnosticCommandPromptHandlerServiceId, ); await Promise.all( - diagnostics.map(async diagnostic => { + diagnostics.map(async (diagnostic) => { if (!this.canHandle(diagnostic)) { return; } const commandPrompts = this.getCommandPrompts(diagnostic); - return messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message }); - }) + const onClose = getOnCloseHandler(diagnostic); + await messageService.handle(diagnostic, { commandPrompts, message: diagnostic.message, onClose }); + }), ); } + private getCommandPrompts(diagnostic: IDiagnostic): { prompt: string; command?: IDiagnosticCommand }[] { const commandFactory = this.serviceContainer.get(IDiagnosticsCommandFactory); - switch (diagnostic.code) { - case DiagnosticCodes.NoPythonInterpretersDiagnostic: { - return [ - { - prompt: 'Download', - command: commandFactory.createCommand(diagnostic, { - type: 'launch', - options: 'https://www.python.org/downloads' - }) - } - ]; - } - case DiagnosticCodes.NoCurrentlySelectedPythonInterpreterDiagnostic: { - return [ - { - prompt: 'Select Python Interpreter', - command: commandFactory.createCommand(diagnostic, { - type: 'executeVSCCommand', - options: 'python.setInterpreter' - }) - } - ]; - } - default: { - throw new Error('Invalid diagnostic for \'InvalidPythonInterpreterService\''); - } + if ( + diagnostic.code === DiagnosticCodes.InvalidComspecDiagnostic || + diagnostic.code === DiagnosticCodes.IncompletePathVarDiagnostic || + diagnostic.code === DiagnosticCodes.DefaultShellErrorDiagnostic + ) { + const links: Record = { + InvalidComspecDiagnostic: 'https://aka.ms/AAk3djo', + IncompletePathVarDiagnostic: 'https://aka.ms/AAk744c', + DefaultShellErrorDiagnostic: 'https://aka.ms/AAk7qix', + }; + return [ + { + prompt: Common.seeInstructions, + command: commandFactory.createCommand(diagnostic, { + type: 'launch', + options: links[diagnostic.code], + }), + }, + ]; + } + const prompts = [ + { + prompt: Common.selectPythonInterpreter, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: Commands.Set_Interpreter, + }), + }, + ]; + if (diagnostic.code === DiagnosticCodes.InvalidPythonInterpreterDiagnostic) { + prompts.push({ + prompt: Common.openOutputPanel, + command: commandFactory.createCommand(diagnostic, { + type: 'executeVSCCommand', + options: Commands.ViewOutput, + }), + }); } + return prompts; + } +} + +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, + @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, + ) { + super([DiagnosticCodes.JediPython27NotSupportedDiagnostic], serviceContainer, disposableRegistry, true, true); + } + + public diagnose(resource: Resource): Promise { + let changed = false; + const config = this.workspaceService.getConfiguration('python'); + const value = config.inspect('languageServer'); + if (value?.workspaceValue === LanguageServerType.Microsoft) { + config.update('languageServer', 'Default', ConfigurationTarget.Workspace); + changed = true; + } + + if (value?.globalValue === LanguageServerType.Microsoft) { + config.update('languageServer', 'Default', ConfigurationTarget.Global); + changed = true; + } + + return Promise.resolve( + changed ? [new SwitchToDefaultLanguageServerDiagnostic(SwitchToDefaultLS.bannerMessage, resource)] : [], + ); + } + + protected async onHandle(diagnostics: IDiagnostic[]): Promise { + if (diagnostics.length === 0 || !this.canHandle(diagnostics[0])) { + return; + } + const diagnostic = diagnostics[0]; + if (await this.filterService.shouldIgnoreDiagnostic(diagnostic.code)) { + return; + } + + await this.messageService.handle(diagnostic, { + commandPrompts: [ + { + prompt: Common.gotIt, + }, + ], + }); + } +} diff --git a/src/client/application/diagnostics/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; } diff --git a/src/client/application/diagnostics/commands/execVSCCommand.ts b/src/client/application/diagnostics/commands/execVSCCommand.ts index 150c4e3c2f68..50c7367f199a 100644 --- a/src/client/application/diagnostics/commands/execVSCCommand.ts +++ b/src/client/application/diagnostics/commands/execVSCCommand.ts @@ -12,7 +12,11 @@ import { IDiagnostic } from '../types'; import { BaseDiagnosticCommand } from './base'; export class ExecuteVSCCommand extends BaseDiagnosticCommand { - constructor(diagnostic: IDiagnostic, private serviceContainer: IServiceContainer, private commandName: CommandsWithoutArgs) { + constructor( + diagnostic: IDiagnostic, + private serviceContainer: IServiceContainer, + private commandName: CommandsWithoutArgs, + ) { super(diagnostic); } public async invoke(): Promise { diff --git a/src/client/application/diagnostics/commands/factory.ts b/src/client/application/diagnostics/commands/factory.ts index a780c92bac97..b9bf14305703 100644 --- a/src/client/application/diagnostics/commands/factory.ts +++ b/src/client/application/diagnostics/commands/factory.ts @@ -13,7 +13,7 @@ import { CommandOptions, IDiagnosticsCommandFactory } from './types'; @injectable() export class DiagnosticsCommandFactory implements IDiagnosticsCommandFactory { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} public createCommand(diagnostic: IDiagnostic, options: CommandOptions): IDiagnosticCommand { const commandType = options.type; switch (options.type) { diff --git a/src/client/application/diagnostics/commands/ignore.ts b/src/client/application/diagnostics/commands/ignore.ts index f73d068c639d..311128195975 100644 --- a/src/client/application/diagnostics/commands/ignore.ts +++ b/src/client/application/diagnostics/commands/ignore.ts @@ -10,7 +10,11 @@ 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 { diff --git a/src/client/application/diagnostics/commands/types.ts b/src/client/application/diagnostics/commands/types.ts index 118dc626be82..f65460b0d113 100644 --- a/src/client/application/diagnostics/commands/types.ts +++ b/src/client/application/diagnostics/commands/types.ts @@ -7,10 +7,10 @@ import { CommandsWithoutArgs } from '../../../common/application/commands'; import { DiagnosticScope, IDiagnostic, IDiagnosticCommand } from '../types'; export type CommandOption = { type: Type; options: Option }; -export type LaunchBrowserOption = CommandOption<'launch', string>; -export type IgnoreDiagnostOption = CommandOption<'ignore', DiagnosticScope>; -export type ExecuteVSCCommandOption = CommandOption<'executeVSCCommand', CommandsWithoutArgs>; -export type CommandOptions = LaunchBrowserOption | IgnoreDiagnostOption | ExecuteVSCCommandOption; +type LaunchBrowserOption = CommandOption<'launch', string>; +type IgnoreDiagnosticOption = CommandOption<'ignore', DiagnosticScope>; +type ExecuteVSCCommandOption = CommandOption<'executeVSCCommand', CommandsWithoutArgs>; +export type CommandOptions = LaunchBrowserOption | IgnoreDiagnosticOption | ExecuteVSCCommandOption; export const IDiagnosticsCommandFactory = Symbol('IDiagnosticsCommandFactory'); diff --git a/src/client/application/diagnostics/constants.ts b/src/client/application/diagnostics/constants.ts index 46deb3c1620b..ca2867fc4f49 100644 --- a/src/client/application/diagnostics/constants.ts +++ b/src/client/application/diagnostics/constants.ts @@ -7,13 +7,21 @@ export enum DiagnosticCodes { InvalidEnvironmentPathVariableDiagnostic = 'InvalidEnvironmentPathVariableDiagnostic', InvalidDebuggerTypeDiagnostic = 'InvalidDebuggerTypeDiagnostic', NoPythonInterpretersDiagnostic = 'NoPythonInterpretersDiagnostic', - MacInterpreterSelectedAndNoOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndNoOtherInterpretersDiagnostic', - MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic = 'MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic', + MacInterpreterSelected = 'MacInterpreterSelected', InvalidPythonPathInDebuggerSettingsDiagnostic = 'InvalidPythonPathInDebuggerSettingsDiagnostic', InvalidPythonPathInDebuggerLaunchDiagnostic = 'InvalidPythonPathInDebuggerLaunchDiagnostic', EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic = 'EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic', - NoCurrentlySelectedPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidPythonInterpreterDiagnostic = 'InvalidPythonInterpreterDiagnostic', + InvalidComspecDiagnostic = 'InvalidComspecDiagnostic', + IncompletePathVarDiagnostic = 'IncompletePathVarDiagnostic', + DefaultShellErrorDiagnostic = 'DefaultShellErrorDiagnostic', LSNotSupportedDiagnostic = 'LSNotSupportedDiagnostic', + PythonPathDeprecatedDiagnostic = 'PythonPathDeprecatedDiagnostic', JustMyCodeDiagnostic = 'JustMyCodeDiagnostic', - ConsoleTypeDiagnostic = 'ConsoleTypeDiagnostic' + 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 { const factory = this.serviceContainer.get(IPersistentStateFactory); const globalState = factory.createGlobalPersistentState(FilterKeys.GlobalDiagnosticFilter, []); - const workspaceState = factory.createWorkspacePersistentState(FilterKeys.WorkspaceDiagnosticFilter, []); - return globalState.value.indexOf(code) >= 0 || - workspaceState.value.indexOf(code) >= 0; + const workspaceState = factory.createWorkspacePersistentState( + FilterKeys.WorkspaceDiagnosticFilter, + [], + ); + return globalState.value.indexOf(code) >= 0 || workspaceState.value.indexOf(code) >= 0; } public async ignoreDiagnostic(code: string, scope: DiagnosticScope): Promise { const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = scope === DiagnosticScope.Global ? - factory.createGlobalPersistentState(FilterKeys.GlobalDiagnosticFilter, []) : - factory.createWorkspacePersistentState(FilterKeys.WorkspaceDiagnosticFilter, []); + const state = + scope === DiagnosticScope.Global + ? factory.createGlobalPersistentState(FilterKeys.GlobalDiagnosticFilter, []) + : factory.createWorkspacePersistentState(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); } - public async handle(diagnostic: IDiagnostic, options: MessageCommandPrompt = { commandPrompts: [] }): Promise { - const prompts = options.commandPrompts.map(option => option.prompt); - const response = await this.displayMessage(options.message ? options.message : diagnostic.message, diagnostic.severity, prompts); + public async handle( + diagnostic: IDiagnostic, + options: MessageCommandPrompt = { commandPrompts: [] }, + ): Promise { + const prompts = options.commandPrompts.map((option) => option.prompt); + const response = await this.displayMessage( + options.message ? options.message : diagnostic.message, + diagnostic.severity, + prompts, + ); + 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 { + private async displayMessage( + message: string, + severity: DiagnosticSeverity, + prompts: string[], + ): Promise { 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 489328da13f2..acf460b88625 100644 --- a/src/client/application/diagnostics/serviceRegistry.ts +++ b/src/client/application/diagnostics/serviceRegistry.ts @@ -3,32 +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 { InvalidLaunchJsonDebuggerService, InvalidLaunchJsonDebuggerServiceId } from './checks/invalidLaunchJsonDebugger'; -import { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId } from './checks/invalidPythonPathInDebugger'; -import { LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId } from './checks/lsNotSupported'; -import { InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId } from './checks/macPythonInterpreter'; -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, DiagnosticFilterService); - serviceManager.addSingleton>(IDiagnosticHandlerService, DiagnosticCommandPromptHandlerService, DiagnosticCommandPromptHandlerServiceId); - serviceManager.addSingleton(IDiagnosticsService, EnvironmentPathVariableDiagnosticsService, EnvironmentPathVariableDiagnosticsServiceId); - serviceManager.addSingleton(IDiagnosticsService, InvalidLaunchJsonDebuggerService, InvalidLaunchJsonDebuggerServiceId); - serviceManager.addSingleton(IDiagnosticsService, InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId); - serviceManager.addSingleton(IDiagnosticsService, InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId); - serviceManager.addSingleton(IDiagnosticsService, LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId); - serviceManager.addSingleton(IDiagnosticsService, PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId); - serviceManager.addSingleton(IDiagnosticsService, InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId); + serviceManager.addSingleton>( + IDiagnosticHandlerService, + DiagnosticCommandPromptHandlerService, + DiagnosticCommandPromptHandlerServiceId, + ); + serviceManager.addSingleton( + IDiagnosticsService, + EnvironmentPathVariableDiagnosticsService, + EnvironmentPathVariableDiagnosticsServiceId, + ); + serviceManager.addSingleton( + IDiagnosticsService, + InvalidPythonInterpreterService, + InvalidPythonInterpreterServiceId, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InvalidPythonInterpreterService, + ); + serviceManager.addSingleton( + IDiagnosticsService, + InvalidPythonPathInDebuggerService, + InvalidPythonPathInDebuggerServiceId, + ); + serviceManager.addSingleton( + IDiagnosticsService, + PowerShellActivationHackDiagnosticsService, + PowerShellActivationHackDiagnosticsServiceId, + ); + serviceManager.addSingleton( + IDiagnosticsService, + InvalidMacPythonInterpreterService, + InvalidMacPythonInterpreterServiceId, + ); + + serviceManager.addSingleton( + IDiagnosticsService, + PylanceDefaultDiagnosticService, + PylanceDefaultDiagnosticServiceId, + ); + + serviceManager.addSingleton( + IDiagnosticsService, + JediPython27NotSupportedDiagnosticService, + JediPython27NotSupportedDiagnosticServiceId, + ); + + serviceManager.addSingleton( + IDiagnosticsService, + SwitchToDefaultLanguageServerDiagnosticService, + SwitchToDefaultLanguageServerDiagnosticServiceId, + ); + serviceManager.addSingleton(IDiagnosticsCommandFactory, DiagnosticsCommandFactory); serviceManager.addSingleton(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 { - await this.configurationService.updateSetting('diagnostics.sourceMapsEnabled', true, undefined, ConfigurationTarget.Global); - await this.commandManager.executeCommand('workbench.action.reloadWindow'); - } - protected async onEnable(): Promise { - const enableSourceMapsAndReloadVSC = Diagnostics.enableSourceMapsAndReloadVSC(); - const selection = await this.shell.showWarningMessage(Diagnostics.warnBeforeEnablingSourceMaps(), enableSourceMapsAndReloadVSC); - if (selection === enableSourceMapsAndReloadVSC) { - await this.enable(); - } - } -} diff --git a/src/client/application/diagnostics/types.ts b/src/client/application/diagnostics/types.ts index d38d3f671bf8..1dc9a3c689df 100644 --- a/src/client/application/diagnostics/types.ts +++ b/src/client/application/diagnostics/types.ts @@ -10,12 +10,7 @@ import { DiagnosticCodes } from './constants'; export enum DiagnosticScope { Global = 'Global', - WorkspaceFolder = 'WorkspaceFolder' -} - -export enum DiagnosticIgnoreScope { - always = 'always', - session = 'session' + WorkspaceFolder = 'WorkspaceFolder', } export interface IDiagnostic { @@ -25,12 +20,14 @@ export interface IDiagnostic { readonly scope: DiagnosticScope; readonly resource: Resource; readonly invokeHandler: 'always' | 'default'; + readonly shouldShowPrompt?: boolean; } export const IDiagnosticsService = Symbol('IDiagnosticsService'); export interface IDiagnosticsService { - readonly runInBackground: Boolean; + readonly runInBackground: boolean; + readonly runInUntrustedWorkspace: boolean; diagnose(resource: Resource): Promise; canHandle(diagnostic: IDiagnostic): Promise; handle(diagnostics: IDiagnostic[]): Promise; @@ -54,13 +51,16 @@ export interface IDiagnosticCommand { invoke(): Promise; } +export type IDiagnosticMessageOnCloseHandler = (response?: string) => void; + +export const IInvalidPythonPathInSettings = Symbol('IInvalidPythonPathInSettings'); + +export interface IInvalidPythonPathInSettings extends IDiagnosticsService { + validateInterpreterPathInSettings(resource: Resource): Promise; +} + export const IInvalidPythonPathInDebuggerService = Symbol('IInvalidPythonPathInDebuggerService'); export interface IInvalidPythonPathInDebuggerService extends IDiagnosticsService { validatePythonPath(pythonPath?: string, pythonPathSource?: PythonPathSource, resource?: Uri): Promise; } -export const ISourceMapSupportService = Symbol('ISourceMapSupportService'); -export interface ISourceMapSupportService { - register(): void; - enable(): Promise; -} diff --git a/src/client/application/serviceRegistry.ts b/src/client/application/serviceRegistry.ts index 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, SourceMapSupportService); diagnosticsRegisterTypes(serviceManager); } diff --git a/src/client/application/types.ts b/src/client/application/types.ts index 460ac39807c8..cfd41f7b9746 100644 --- a/src/client/application/types.ts +++ b/src/client/application/types.ts @@ -11,8 +11,6 @@ export interface IApplicationDiagnostics { /** * Perform pre-extension activation health checks. * E.g. validate user environment, etc. - * @returns {Promise} - * @memberof IApplicationDiagnostics */ performPreStartupHealthCheck(resource: Resource): Promise; register(): void; diff --git a/src/client/browser/api.ts b/src/client/browser/api.ts new file mode 100644 index 000000000000..ac2df8d0ffed --- /dev/null +++ b/src/client/browser/api.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { BaseLanguageClient } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/browser'; +import { PYTHON_LANGUAGE } from '../common/constants'; +import { ApiForPylance, TelemetryReporter } from '../pylanceApi'; + +export interface IBrowserExtensionApi { + /** + * @deprecated Temporarily exposed for Pylance until we expose this API generally. Will be removed in an + * iteration or two. + */ + pylance: ApiForPylance; +} + +export function buildApi(reporter: TelemetryReporter): IBrowserExtensionApi { + const api: IBrowserExtensionApi = { + pylance: { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + createClient: (...args: any[]): BaseLanguageClient => + new LanguageClient(PYTHON_LANGUAGE, 'Python Language Server', args[0], args[1]), + start: (client: BaseLanguageClient): Promise => client.start(), + stop: (client: BaseLanguageClient): Promise => client.stop(), + getTelemetryReporter: () => reporter, + }, + }; + + return api; +} diff --git a/src/client/browser/extension.ts b/src/client/browser/extension.ts new file mode 100644 index 000000000000..132618430551 --- /dev/null +++ b/src/client/browser/extension.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import TelemetryReporter from '@vscode/extension-telemetry'; +import { LanguageClientOptions } from 'vscode-languageclient'; +import { LanguageClient } from 'vscode-languageclient/browser'; +import { LanguageClientMiddlewareBase } from '../activation/languageClientMiddlewareBase'; +import { LanguageServerType } from '../activation/types'; +import { AppinsightsKey, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { EventName } from '../telemetry/constants'; +import { createStatusItem } from './intellisenseStatus'; +import { PylanceApi } from '../activation/node/pylanceApi'; +import { buildApi, IBrowserExtensionApi } from './api'; + +interface BrowserConfig { + distUrl: string; // URL to Pylance's dist folder. +} + +let languageClient: LanguageClient | undefined; +let pylanceApi: PylanceApi | undefined; + +export function activate(context: vscode.ExtensionContext): Promise { + const reporter = getTelemetryReporter(); + + const activationPromise = Promise.resolve(buildApi(reporter)); + const pylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (pylanceExtension) { + // Make sure we run pylance once we activated core extension. + activationPromise.then(() => runPylance(context, pylanceExtension)); + return activationPromise; + } + + const changeDisposable = vscode.extensions.onDidChange(async () => { + const newPylanceExtension = vscode.extensions.getExtension(PYLANCE_EXTENSION_ID); + if (newPylanceExtension) { + changeDisposable.dispose(); + await runPylance(context, newPylanceExtension); + } + }); + + return activationPromise; +} + +export async function deactivate(): Promise { + if (pylanceApi) { + const api = pylanceApi; + pylanceApi = undefined; + await api.client!.stop(); + } + + if (languageClient) { + const client = languageClient; + languageClient = undefined; + + await client.stop(); + await client.dispose(); + } +} + +async function runPylance( + context: vscode.ExtensionContext, + pylanceExtension: vscode.Extension, +): Promise { + context.subscriptions.push(createStatusItem()); + + pylanceExtension = await getActivatedExtension(pylanceExtension); + const api = pylanceExtension.exports; + if (api.client && api.client.isEnabled()) { + pylanceApi = api; + await api.client.start(); + return; + } + + const { extensionUri, packageJSON } = pylanceExtension; + const distUrl = vscode.Uri.joinPath(extensionUri, 'dist'); + + try { + const worker = new Worker(vscode.Uri.joinPath(distUrl, 'browser.server.bundle.js').toString()); + + // Pass the configuration as the first message to the worker so it can + // have info like the URL of the dist folder early enough. + // + // This is the same method used by the TS worker: + // https://github.com/microsoft/vscode/blob/90aa979bb75a795fd8c33d38aee263ea655270d0/extensions/typescript-language-features/src/tsServer/serverProcess.browser.ts#L55 + const config: BrowserConfig = { distUrl: distUrl.toString() }; + worker.postMessage(config); + + const middleware = new LanguageClientMiddlewareBase( + undefined, + LanguageServerType.Node, + sendTelemetryEventBrowser, + packageJSON.version, + ); + middleware.connect(); + + const clientOptions: LanguageClientOptions = { + // Register the server for python source files. + documentSelector: [ + { + language: 'python', + }, + ], + synchronize: { + // Synchronize the setting section to the server. + configurationSection: ['python', 'jupyter.runStartupCommands'], + }, + middleware, + }; + + const client = new LanguageClient('python', 'Python Language Server', worker, clientOptions); + languageClient = client; + + context.subscriptions.push( + vscode.commands.registerCommand('python.viewLanguageServerOutput', () => client.outputChannel.show()), + ); + + client.onTelemetry( + (telemetryEvent: { + EventName: EventName; + Properties: { method: string }; + Measurements: number | Record | undefined; + Exception: Error | undefined; + }) => { + const eventName = telemetryEvent.EventName || EventName.LANGUAGE_SERVER_TELEMETRY; + const formattedProperties = { + ...telemetryEvent.Properties, + // Replace all slashes in the method name so it doesn't get scrubbed by @vscode/extension-telemetry. + method: telemetryEvent.Properties.method?.replace(/\//g, '.'), + }; + sendTelemetryEventBrowser( + eventName, + telemetryEvent.Measurements, + formattedProperties, + telemetryEvent.Exception, + ); + }, + ); + + await client.start(); + } catch (e) { + console.log(e); // necessary to use console.log for browser + } +} + +// Duplicate code from telemetry/index.ts to avoid pulling in winston, +// which doesn't support the browser. + +let telemetryReporter: TelemetryReporter | undefined; +function getTelemetryReporter() { + if (telemetryReporter) { + return telemetryReporter; + } + + // eslint-disable-next-line global-require + const Reporter = require('@vscode/extension-telemetry').default as typeof TelemetryReporter; + telemetryReporter = new Reporter(AppinsightsKey, [ + { + lookup: /(errorName|errorMessage|errorStack)/g, + }, + ]); + + return telemetryReporter; +} + +function sendTelemetryEventBrowser( + eventName: EventName, + measuresOrDurationMs?: Record | number, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties?: any, + ex?: Error, +): void { + const reporter = getTelemetryReporter(); + const measures = + typeof measuresOrDurationMs === 'number' + ? { duration: measuresOrDurationMs } + : measuresOrDurationMs || undefined; + const customProperties: Record = {}; + const eventNameSent = eventName as string; + + if (properties) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data = properties as any; + Object.getOwnPropertyNames(data).forEach((prop) => { + if (data[prop] === undefined || data[prop] === null) { + return; + } + try { + // If there are any errors in serializing one property, ignore that and move on. + // Else nothing will be sent. + switch (typeof data[prop]) { + case 'string': + customProperties[prop] = data[prop]; + break; + case 'object': + customProperties[prop] = 'object'; + break; + default: + customProperties[prop] = data[prop].toString(); + break; + } + } catch (exception) { + console.error(`Failed to serialize ${prop} for ${eventName}`, exception); // necessary to use console.log for browser + } + }); + } + + // Add shared properties to telemetry props (we may overwrite existing ones). + // Removed in the browser; there's no setSharedProperty. + // Object.assign(customProperties, sharedProperties); + + if (ex) { + const errorProps = { + errorName: ex.name, + errorStack: ex.stack ?? '', + }; + Object.assign(customProperties, errorProps); + + reporter.sendTelemetryErrorEvent(eventNameSent, customProperties, measures); + } else { + reporter.sendTelemetryEvent(eventNameSent, customProperties, measures); + } +} + +async function getActivatedExtension(extension: vscode.Extension): Promise> { + if (!extension.isActive) { + await extension.activate(); + } + + return extension; +} diff --git a/src/client/browser/intellisenseStatus.ts b/src/client/browser/intellisenseStatus.ts new file mode 100644 index 000000000000..b7a49e86dbb0 --- /dev/null +++ b/src/client/browser/intellisenseStatus.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. +import * as vscode from 'vscode'; +import { Common, LanguageService } from './localize'; + +export function createStatusItem(): vscode.Disposable { + if ('createLanguageStatusItem' in vscode.languages) { + const statusItem = vscode.languages.createLanguageStatusItem('python.projectStatus', { + language: 'python', + }); + statusItem.name = LanguageService.statusItem.name; + statusItem.severity = vscode.LanguageStatusSeverity.Warning; + statusItem.text = LanguageService.statusItem.text; + statusItem.detail = LanguageService.statusItem.detail; + statusItem.command = { + title: Common.learnMore, + command: 'vscode.open', + arguments: [vscode.Uri.parse('https://aka.ms/AAdzyh4')], + }; + return statusItem; + } + // eslint-disable-next-line @typescript-eslint/no-empty-function + return { dispose: () => undefined }; +} diff --git a/src/client/browser/localize.ts b/src/client/browser/localize.ts new file mode 100644 index 000000000000..fd50dbcc7093 --- /dev/null +++ b/src/client/browser/localize.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { l10n } from 'vscode'; + +/* eslint-disable @typescript-eslint/no-namespace */ + +// IMPORTANT: Do not import any node fs related modules here, as they do not work in browser. + +export namespace LanguageService { + export const statusItem = { + name: l10n.t('Python IntelliSense Status'), + text: l10n.t('Partial Mode'), + detail: l10n.t('Limited IntelliSense provided by Pylance'), + }; +} + +export namespace Common { + export const learnMore = l10n.t('Learn more'); +} diff --git a/src/client/chat/baseTool.ts b/src/client/chat/baseTool.ts new file mode 100644 index 000000000000..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 implements LanguageModelTool { + protected extraTelemetryProperties: Record = {}; + constructor(private readonly toolName: string) {} + + async invoke( + options: LanguageModelToolInvocationOptions, + token: CancellationToken, + ): Promise { + if (!workspace.isTrusted) { + return new LanguageModelToolResult([ + new LanguageModelTextPart('Cannot use this tool in an untrusted workspace.'), + ]); + } + 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, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; + + async prepareInvocation( + options: LanguageModelToolInvocationPrepareOptions, + token: CancellationToken, + ): Promise { + const resource = resolveFilePath(options.input.resourcePath); + return this.prepareInvocationImpl(options, resource, token); + } + + protected abstract prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise; +} diff --git a/src/client/chat/configurePythonEnvTool.ts b/src/client/chat/configurePythonEnvTool.ts new file mode 100644 index 000000000000..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 + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + public static readonly toolName = 'configure_python_environment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly createEnvTool: CreateVirtualEnvTool, + ) { + super(ConfigurePythonEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resource); + if (notebookResponse) { + 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, + _resource: Uri | undefined, + _token: CancellationToken, + ): Promise { + return { + invocationMessage: 'Configuring a Python Environment', + }; + } + + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } +} diff --git a/src/client/chat/createVirtualEnvTool.ts b/src/client/chat/createVirtualEnvTool.ts new file mode 100644 index 000000000000..56760d2b4bef --- /dev/null +++ b/src/client/chat/createVirtualEnvTool.ts @@ -0,0 +1,246 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationError, + CancellationToken, + commands, + l10n, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, +} from 'vscode'; +import { PythonExtension, ResolvedEnvironment } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getDisplayVersion, + getEnvDetailsForResponse, + IResourceReference, + isCancellationError, + raceCancellationError, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout, sleep } from '../common/utils/async'; +import { IInterpreterPathService } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IRecommendedEnvironmentService } from '../interpreter/configuration/types'; +import { EnvironmentType } from '../pythonEnvironments/info'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { convertEnvInfoToPythonEnvironment } from '../pythonEnvironments/legacyIOC'; +import { sortInterpreters } from '../interpreter/helpers'; +import { isStableVersion } from '../pythonEnvironments/info/pythonVersion'; +import { createVirtualEnvironment } from '../pythonEnvironments/creation/createEnvApi'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; +import { useEnvExtension } from '../envExt/api.internal'; +import { PythonEnvironment } from '../envExt/types'; +import { hideEnvCreation } from '../pythonEnvironments/creation/provider/hideEnvCreation'; +import { BaseTool } from './baseTool'; + +interface ICreateVirtualEnvToolParams extends IResourceReference { + packageList?: string[]; // Added only becausewe have ability to create a virtual env with list of packages same tool within the in Python Env extension. +} + +export class CreateVirtualEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + private readonly recommendedEnvService: IRecommendedEnvironmentService; + + public static readonly toolName = 'create_virtual_environment'; + constructor( + private readonly discoveryApi: IDiscoveryAPI, + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(CreateVirtualEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + this.recommendedEnvService = this.serviceContainer.get( + IRecommendedEnvironmentService, + ); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + let info = await this.getPreferredEnvForCreation(resource); + if (!info) { + traceWarn(`Called ${CreateVirtualEnvTool.toolName} tool not invoked, no preferred environment found.`); + throw new CancellationError(); + } + const { workspaceFolder, preferredGlobalPythonEnv } = info; + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const disposables = new DisposableStore(); + try { + disposables.add(hideEnvCreation()); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + + let createdEnvPath: string | undefined = undefined; + if (useEnvExtension()) { + const result: PythonEnvironment | undefined = await commands.executeCommand('python-envs.createAny', { + quickCreate: true, + additionalPackages: options.input.packageList || [], + uri: workspaceFolder.uri, + selectEnvironment: true, + }); + createdEnvPath = result?.environmentPath.fsPath; + } else { + const created = await raceCancellationError( + createVirtualEnvironment({ + interpreter: preferredGlobalPythonEnv.id, + workspaceFolder, + }), + token, + ); + createdEnvPath = created?.path; + } + if (!createdEnvPath) { + traceWarn(`${CreateVirtualEnvTool.toolName} tool not invoked, virtual env not created.`); + throw new CancellationError(); + } + + // Wait a few secs to ensure the env is selected as the active environment.. + // If this doesn't work, then something went wrong. + await raceTimeout(5_000, interpreterChanged); + + const stopWatch = new StopWatch(); + let env: ResolvedEnvironment | undefined; + while (stopWatch.elapsedTime < 5_000 || !env) { + env = await this.api.resolveEnvironment(createdEnvPath); + if (env) { + break; + } else { + traceVerbose( + `${CreateVirtualEnvTool.toolName} tool invoked, env created but not yet resolved, waiting...`, + ); + await sleep(200); + } + } + if (!env) { + traceError(`${CreateVirtualEnvTool.toolName} tool invoked, env created but unable to resolve details.`); + throw new CancellationError(); + } + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } catch (ex) { + if (!isCancellationError(ex)) { + traceError( + `${ + CreateVirtualEnvTool.toolName + } tool failed to create virtual environment for resource ${resource?.toString()}`, + ex, + ); + } + throw ex; + } finally { + disposables.dispose(); + } + } + + public async shouldCreateNewVirtualEnv(resource: Uri | undefined, token: CancellationToken): Promise { + if (doesWorkspaceHaveVenvOrCondaEnv(resource, this.api)) { + // If we already have a .venv or .conda in this workspace, then do not prompt to create a virtual environment. + return false; + } + + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + return info ? true : false; + } + + async prepareInvocationImpl( + _options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + const info = await raceCancellationError(this.getPreferredEnvForCreation(resource), token); + if (!info) { + return {}; + } + const { preferredGlobalPythonEnv } = info; + const version = getDisplayVersion(preferredGlobalPythonEnv.version); + return { + confirmationMessages: { + title: l10n.t('Create a Virtual Environment{0}?', version ? ` (${version})` : ''), + message: l10n.t(`Virtual Environments provide the benefit of package isolation and more.`), + }, + invocationMessage: l10n.t('Creating a Virtual Environment'), + }; + } + async hasAlreadyGotAWorkspaceSpecificEnvironment(resource: Uri | undefined) { + const recommededEnv = await this.recommendedEnvService.getRecommededEnvironment(resource); + // Already selected workspace env, hence nothing to do. + if (recommededEnv?.reason === 'workspaceUserSelected' && workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + // No workspace folders, and the user selected a global environment. + if (recommededEnv?.reason === 'globalUserSelected' && !workspace.workspaceFolders?.length) { + return recommededEnv.environment; + } + } + + private async getPreferredEnvForCreation(resource: Uri | undefined) { + if (await this.hasAlreadyGotAWorkspaceSpecificEnvironment(resource)) { + return undefined; + } + + // If we have a resource or have only one workspace folder && there is no .venv and no workspace specific environment. + // Then lets recommend creating a virtual environment. + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + // No workspace folder, hence no need to create a virtual environment. + return undefined; + } + + // Find the latest stable version of Python from the list of know envs. + let globalPythonEnvs = this.discoveryApi + .getEnvs() + .map((env) => convertEnvInfoToPythonEnvironment(env)) + .filter((env) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(env.envType), + ) + .filter((env) => env.version && isStableVersion(env.version)); + + globalPythonEnvs = sortInterpreters(globalPythonEnvs); + const preferredGlobalPythonEnv = globalPythonEnvs.length + ? this.api.known.find((e) => e.id === globalPythonEnvs[globalPythonEnvs.length - 1].id) + : undefined; + + return workspaceFolder && preferredGlobalPythonEnv + ? { + workspaceFolder, + preferredGlobalPythonEnv, + } + : undefined; + } +} diff --git a/src/client/chat/getExecutableTool.ts b/src/client/chat/getExecutableTool.ts new file mode 100644 index 000000000000..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 implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_executable_details'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + super(GetExecutableTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + const 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, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + return { + invocationMessage: envName + ? l10n.t('Fetching Python executable information for {0}', envName) + : l10n.t('Fetching Python executable information'), + }; + } +} diff --git a/src/client/chat/getPythonEnvTool.ts b/src/client/chat/getPythonEnvTool.ts new file mode 100644 index 000000000000..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 + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly pythonExecFactory: IPythonExecutionFactory; + private readonly processServiceFactory: IProcessServiceFactory; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'get_python_environment_details'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(GetEnvironmentInfoTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.pythonExecFactory = this.serviceContainer.get(IPythonExecutionFactory); + this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invokeImpl( + _options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const notebookResponse = getToolResponseIfNotebook(resourcePath); + if (notebookResponse) { + return notebookResponse; + } + + // environment + const envPath = this.api.getActiveEnvironmentPath(resourcePath); + const environment = await raceCancellationError(this.api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + 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 or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + pkgs.forEach((pkg) => { + const version = pkg.version; + response.push(version ? `- ${pkg.name} (${version})` : `- ${pkg.name}`); + }); + packages = response.join('\n'); + } + } + if (!packages) { + packages = await getPythonPackagesResponse( + environment, + this.pythonExecFactory, + this.processServiceFactory, + resourcePath, + token, + ); + // 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, + resourcePath: Uri | undefined, + _token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + return { + invocationMessage: l10n.t('Fetching Python environment information'), + }; + } +} diff --git a/src/client/chat/index.ts b/src/client/chat/index.ts new file mode 100644 index 000000000000..b548860eaae3 --- /dev/null +++ b/src/client/chat/index.ts @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { lm } from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { InstallPackagesTool } from './installPackagesTool'; +import { IExtensionContext } from '../common/types'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { IDiscoveryAPI } from '../pythonEnvironments/base/locator'; +import { GetExecutableTool } from './getExecutableTool'; +import { GetEnvironmentInfoTool } from './getPythonEnvTool'; +import { ConfigurePythonEnvTool } from './configurePythonEnvTool'; +import { SelectPythonEnvTool } from './selectEnvTool'; +import { CreateVirtualEnvTool } from './createVirtualEnvTool'; + +export function registerTools( + context: IExtensionContext, + discoverApi: IDiscoveryAPI, + environmentsApi: PythonExtension['environments'], + serviceContainer: IServiceContainer, +) { + const ourTools = new DisposableStore(); + context.subscriptions.push(ourTools); + + ourTools.add( + lm.registerTool(GetEnvironmentInfoTool.toolName, new GetEnvironmentInfoTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + GetExecutableTool.toolName, + new GetExecutableTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + ourTools.add( + lm.registerTool( + InstallPackagesTool.toolName, + new InstallPackagesTool(environmentsApi, serviceContainer, discoverApi), + ), + ); + const createVirtualEnvTool = new CreateVirtualEnvTool(discoverApi, environmentsApi, serviceContainer); + ourTools.add(lm.registerTool(CreateVirtualEnvTool.toolName, createVirtualEnvTool)); + ourTools.add( + lm.registerTool(SelectPythonEnvTool.toolName, new SelectPythonEnvTool(environmentsApi, serviceContainer)), + ); + ourTools.add( + lm.registerTool( + ConfigurePythonEnvTool.toolName, + new ConfigurePythonEnvTool(environmentsApi, serviceContainer, createVirtualEnvTool), + ), + ); +} diff --git a/src/client/chat/installPackagesTool.ts b/src/client/chat/installPackagesTool.ts new file mode 100644 index 000000000000..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 + implements LanguageModelTool { + public static readonly toolName = 'install_python_packages'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + private readonly discovery: IDiscoveryAPI, + ) { + super(InstallPackagesTool.toolName); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const packageCount = options.input.packageList.length; + const packagePlurality = packageCount === 1 ? 'package' : 'packages'; + 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); + 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, + resourcePath: Uri | undefined, + token: CancellationToken, + ): Promise { + const packageCount = options.input.packageList.length; + if (getToolResponseIfNotebook(resourcePath)) { + return {}; + } + + const envName = await raceCancellationError(getEnvDisplayName(this.discovery, resourcePath, this.api), token); + let title = ''; + let invocationMessage = ''; + const message = + packageCount === 1 + ? '' + : l10n.t(`The following packages will be installed: {0}`, options.input.packageList.sort().join(', ')); + if (envName) { + title = + packageCount === 1 + ? l10n.t(`Install {0} in {1}?`, options.input.packageList[0], envName) + : l10n.t(`Install packages in {0}?`, envName); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing {0} in {1}`, options.input.packageList[0], envName) + : l10n.t(`Installing packages {0} in {1}`, options.input.packageList.sort().join(', '), envName); + } else { + title = + options.input.packageList.length === 1 + ? l10n.t(`Install Python package '{0}'?`, options.input.packageList[0]) + : l10n.t(`Install Python packages?`); + invocationMessage = + packageCount === 1 + ? l10n.t(`Installing Python package '{0}'`, options.input.packageList[0]) + : l10n.t(`Installing Python packages: {0}`, options.input.packageList.sort().join(', ')); + } + + return { + confirmationMessages: { title, message }, + invocationMessage, + }; + } +} diff --git a/src/client/chat/listPackagesTool.ts b/src/client/chat/listPackagesTool.ts new file mode 100644 index 000000000000..fcae831cfe2f --- /dev/null +++ b/src/client/chat/listPackagesTool.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Uri } from 'vscode'; +import { ResolvedEnvironment } from '../api/types'; +import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { isCondaEnv, raceCancellationError } from './utils'; +import { parsePipList } from './pipListUtils'; +import { Conda } from '../pythonEnvironments/common/environmentManagers/conda'; +import { traceError } from '../logging'; + +export async function getPythonPackagesResponse( + environment: ResolvedEnvironment, + pythonExecFactory: IPythonExecutionFactory, + processServiceFactory: IProcessServiceFactory, + resourcePath: Uri | undefined, + token: CancellationToken, +): Promise { + const packages = isCondaEnv(environment) + ? await raceCancellationError( + listCondaPackages( + pythonExecFactory, + environment, + resourcePath, + await raceCancellationError(processServiceFactory.create(resourcePath), token), + ), + token, + ) + : await raceCancellationError(listPipPackages(pythonExecFactory, resourcePath), token); + + if (!packages.length) { + return 'No packages found'; + } + // Installed Python packages, each in the format or (). The version may be omitted if unknown. Returns an empty array if no packages are installed. + const response = [ + 'Below is a list of the Python packages, each in the format or (). The version may be omitted if unknown: ', + ]; + packages.forEach((pkg) => { + const [name, version] = pkg; + response.push(version ? `- ${name} (${version})` : `- ${name}`); + }); + return response.join('\n'); +} + +async function listPipPackages( + execFactory: IPythonExecutionFactory, + resource: Uri | undefined, +): Promise<[string, string][]> { + // Add option --format to subcommand list of pip cache, with abspath choice to output the full path of a wheel file. (#8355) + // Added in 2020. Thats almost 5 years ago. When Python 3.8 was released. + const exec = await execFactory.createActivatedEnvironment({ allowEnvironmentFetchExceptions: true, resource }); + const output = await exec.execModule('pip', ['list'], { throwOnStdErr: false, encoding: 'utf8' }); + return parsePipList(output.stdout).map((pkg) => [pkg.name, pkg.version]); +} + +async function listCondaPackages( + execFactory: IPythonExecutionFactory, + env: ResolvedEnvironment, + resource: Uri | undefined, + processService: IProcessService, +): Promise<[string, string][]> { + const conda = await Conda.getConda(); + if (!conda) { + traceError('Conda is not installed, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + if (!env.executable.uri) { + traceError('Conda environment executable not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const condaEnv = await conda.getCondaEnvironment(env.executable.uri.fsPath); + if (!condaEnv) { + traceError('Conda environment not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const cmd = await conda.getListPythonPackagesArgs(condaEnv, true); + if (!cmd) { + traceError('Conda list command not found, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const output = await processService.exec(cmd[0], cmd.slice(1), { shell: true }); + if (!output.stdout) { + traceError('Unable to get conda packages, falling back to pip packages'); + return listPipPackages(execFactory, resource); + } + const content = output.stdout.split(/\r?\n/).filter((l) => !l.startsWith('#')); + const packages: [string, string][] = []; + content.forEach((l) => { + const parts = l.split(' ').filter((p) => p.length > 0); + if (parts.length >= 3) { + packages.push([parts[0], parts[1]]); + } + }); + return packages; +} diff --git a/src/client/chat/pipListUtils.ts b/src/client/chat/pipListUtils.ts new file mode 100644 index 000000000000..0112d88c53ab --- /dev/null +++ b/src/client/chat/pipListUtils.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface PipPackage { + name: string; + version: string; + displayName: string; + description: string; +} +export function parsePipList(data: string): PipPackage[] { + const collection: PipPackage[] = []; + + const lines = data.split('\n').splice(2); + for (let line of lines) { + if (line.trim() === '' || line.startsWith('Package') || line.startsWith('----') || line.startsWith('[')) { + continue; + } + const parts = line.split(' ').filter((e) => e); + if (parts.length > 1) { + const name = parts[0].trim(); + const version = parts[1].trim(); + const pkg = { + name, + version, + displayName: name, + description: version, + }; + collection.push(pkg); + } + } + return collection; +} diff --git a/src/client/chat/selectEnvTool.ts b/src/client/chat/selectEnvTool.ts new file mode 100644 index 000000000000..9eeebdfc1b56 --- /dev/null +++ b/src/client/chat/selectEnvTool.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + l10n, + LanguageModelTextPart, + LanguageModelTool, + LanguageModelToolInvocationOptions, + LanguageModelToolInvocationPrepareOptions, + LanguageModelToolResult, + PreparedToolInvocation, + Uri, + workspace, + commands, + QuickPickItem, + QuickPickItemKind, +} from 'vscode'; +import { PythonExtension } from '../api/types'; +import { IServiceContainer } from '../ioc/types'; +import { ICodeExecutionService } from '../terminals/types'; +import { TerminalCodeExecutionProvider } from '../terminals/codeExecution/terminalCodeExecution'; +import { + doesWorkspaceHaveVenvOrCondaEnv, + getEnvDetailsForResponse, + getToolResponseIfNotebook, + IResourceReference, +} from './utils'; +import { ITerminalHelper } from '../common/terminal/types'; +import { raceTimeout } from '../common/utils/async'; +import { Commands, Octicons } from '../common/constants'; +import { CreateEnvironmentResult } from '../pythonEnvironments/creation/proposed.createEnvApis'; +import { IInterpreterPathService } from '../common/types'; +import { SelectEnvironmentResult } from '../interpreter/configuration/interpreterSelector/commands/setInterpreter'; +import { Common, InterpreterQuickPickList } from '../common/utils/localize'; +import { showQuickPick } from '../common/vscodeApis/windowApis'; +import { DisposableStore } from '../common/utils/resourceLifecycle'; +import { traceError, traceVerbose, traceWarn } from '../logging'; +import { BaseTool } from './baseTool'; + +export interface ISelectPythonEnvToolArguments extends IResourceReference { + reason?: 'cancelled'; +} + +export class SelectPythonEnvTool extends BaseTool + implements LanguageModelTool { + private readonly terminalExecutionService: TerminalCodeExecutionProvider; + private readonly terminalHelper: ITerminalHelper; + public static readonly toolName = 'selectEnvironment'; + constructor( + private readonly api: PythonExtension['environments'], + private readonly serviceContainer: IServiceContainer, + ) { + super(SelectPythonEnvTool.toolName); + this.terminalExecutionService = this.serviceContainer.get( + ICodeExecutionService, + 'standard', + ); + this.terminalHelper = this.serviceContainer.get(ITerminalHelper); + } + + async invokeImpl( + options: LanguageModelToolInvocationOptions, + resource: Uri | undefined, + token: CancellationToken, + ): Promise { + let selected: boolean | undefined = false; + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + if (options.input.reason === 'cancelled' || hasVenvOrCondaEnvInWorkspaceFolder) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { + hideCreateVenv: false, + showBackButton: false, + }), + )) as SelectEnvironmentResult | undefined; + if (result?.path) { + traceVerbose(`User selected a Python environment ${result.path} in Select Python Tool.`); + selected = true; + } else { + traceWarn(`User did not select a Python environment in Select Python Tool.`); + } + } else { + selected = await showCreateAndSelectEnvironmentQuickPick(resource, this.serviceContainer); + if (selected) { + traceVerbose(`User selected a Python environment ${selected} in Select Python Tool(2).`); + } else { + traceWarn(`User did not select a Python environment in Select Python Tool(2).`); + } + } + const env = selected + ? await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)) + : undefined; + if (selected && !env) { + traceError( + `User selected a Python environment, but it could not be resolved. This is unexpected. Environment: ${this.api.getActiveEnvironmentPath( + resource, + )}`, + ); + } + if (selected && env) { + return await getEnvDetailsForResponse( + env, + this.api, + this.terminalExecutionService, + this.terminalHelper, + resource, + token, + ); + } + return new LanguageModelToolResult([ + new LanguageModelTextPart('User did not create nor select a Python environment.'), + ]); + } + + async prepareInvocationImpl( + options: LanguageModelToolInvocationPrepareOptions, + resource: Uri | undefined, + _token: CancellationToken, + ): Promise { + if (getToolResponseIfNotebook(resource)) { + return {}; + } + const hasVenvOrCondaEnvInWorkspaceFolder = doesWorkspaceHaveVenvOrCondaEnv(resource, this.api); + + if ( + hasVenvOrCondaEnvInWorkspaceFolder || + !workspace.workspaceFolders?.length || + options.input.reason === 'cancelled' + ) { + return { + confirmationMessages: { + title: l10n.t('Select a Python Environment?'), + message: '', + }, + }; + } + + return { + confirmationMessages: { + title: l10n.t('Configure a Python Environment?'), + message: l10n.t( + [ + 'The recommended option is to create a new Python Environment, providing the benefit of isolating packages from other environments. ', + 'Optionally you could select an existing Python Environment.', + ].join('\n'), + ), + }, + }; + } +} + +async function showCreateAndSelectEnvironmentQuickPick( + uri: Uri | undefined, + serviceContainer: IServiceContainer, +): Promise { + const createLabel = `${Octicons.Add} ${InterpreterQuickPickList.create.label}`; + const selectLabel = l10n.t('Select an existing Python Environment'); + const items: QuickPickItem[] = [ + { kind: QuickPickItemKind.Separator, label: Common.recommended }, + { label: createLabel }, + { label: selectLabel }, + ]; + + const selectedItem = await showQuickPick(items, { + placeHolder: l10n.t('Configure a Python Environment'), + matchOnDescription: true, + ignoreFocusOut: true, + }); + + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === createLabel) { + const disposables = new DisposableStore(); + try { + const workspaceFolder = + (workspace.workspaceFolders?.length && uri ? workspace.getWorkspaceFolder(uri) : undefined) || + (workspace.workspaceFolders?.length === 1 ? workspace.workspaceFolders[0] : undefined); + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterChanged = new Promise((resolve) => { + disposables.add(interpreterPathService.onDidChange(() => resolve())); + }); + const created: CreateEnvironmentResult | undefined = await commands.executeCommand( + Commands.Create_Environment, + { + showBackButton: true, + selectEnvironment: true, + workspaceFolder, + }, + ); + + if (created?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (created?.action === 'Cancel') { + return undefined; + } + if (created?.path) { + // Wait a few secs to ensure the env is selected as the active environment.. + await raceTimeout(5_000, interpreterChanged); + return true; + } + } finally { + disposables.dispose(); + } + } + if (selectedItem && !Array.isArray(selectedItem) && selectedItem.label === selectLabel) { + const result = (await Promise.resolve( + commands.executeCommand(Commands.Set_Interpreter, { hideCreateVenv: true, showBackButton: true }), + )) as SelectEnvironmentResult | undefined; + if (result?.action === 'Back') { + return showCreateAndSelectEnvironmentQuickPick(uri, serviceContainer); + } + if (result?.action === 'Cancel') { + return undefined; + } + if (result?.path) { + return true; + } + } +} diff --git a/src/client/chat/utils.ts b/src/client/chat/utils.ts new file mode 100644 index 000000000000..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(promise: Promise, token: CancellationToken): Promise { + return new Promise((resolve, reject) => { + const ref = token.onCancellationRequested(() => { + ref.dispose(); + reject(new CancellationError()); + }); + promise.then(resolve, reject).finally(() => ref.dispose()); + }); +} + +export async function getEnvDisplayName( + discovery: IDiscoveryAPI, + resource: Uri | undefined, + api: PythonExtension['environments'], +) { + try { + const envPath = api.getActiveEnvironmentPath(resource); + const env = await discovery.resolveEnv(envPath.path); + return env?.display || env?.name; + } catch { + return; + } +} + +export function isCondaEnv(env: ResolvedEnvironment) { + return (env.environment?.type || '').toLowerCase() === 'conda'; +} + +export 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 { + // environment + const envPath = api.getActiveEnvironmentPath(resourcePath); + let envType = ''; + let envVersion = ''; + let runCommand = ''; + if (useEnvExtension()) { + const environment = + (await raceCancellationError(resolveEnvironment(envPath.id), token)) || + (await raceCancellationError(resolveEnvironment(envPath.path), token)); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + envVersion = environment.version; + try { + const managerId = environment.envId.managerId; + envType = + (!managerId.endsWith(':') && managerId.includes(':') ? managerId.split(':').reverse()[0] : '') || + 'unknown'; + } catch { + envType = 'unknown'; + } + + const execInfo = environment.execInfo; + const executable = execInfo?.activatedRun?.executable ?? execInfo?.run.executable ?? 'python'; + const args = execInfo?.activatedRun?.args ?? execInfo?.run.args ?? []; + runCommand = terminalHelper.buildCommandForTerminal(TerminalShellType.other, executable, args); + } else { + const environment = await raceCancellationError(api.resolveEnvironment(envPath), token); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resourcePath?.fsPath, + 'noEnvFound', + ); + } + envType = environment.environment?.type || 'unknown'; + envVersion = environment.version.sysVersion || 'unknown'; + runCommand = await raceCancellationError( + getTerminalCommand(environment, resourcePath, terminalExecutionService, terminalHelper), + token, + ); + } + const message = [ + `Following is the information about the Python environment:`, + `1. Environment Type: ${envType}`, + `2. Version: ${envVersion}`, + '', + `3. Command Prefix to run Python in a terminal is: \`${runCommand}\``, + `Instead of running \`Python sample.py\` in the terminal, you will now run: \`${runCommand} sample.py\``, + `Similarly instead of running \`Python -c "import sys;...."\` in the terminal, you will now run: \`${runCommand} -c "import sys;...."\``, + packages ? `4. ${packages}` : '', + ]; + return message.join('\n'); +} + +export async function getTerminalCommand( + environment: ResolvedEnvironment, + resource: Uri | undefined, + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, +): Promise { + let cmd: { command: string; args: string[] }; + if (isCondaEnv(environment)) { + cmd = (await getCondaRunCommand(environment)) || (await terminalExecutionService.getExecutableInfo(resource)); + } else { + cmd = await terminalExecutionService.getExecutableInfo(resource); + } + return terminalHelper.buildCommandForTerminal(TerminalShellType.other, cmd.command, cmd.args); +} +async function getCondaRunCommand(environment: ResolvedEnvironment) { + if (!environment.executable.uri) { + return; + } + const conda = await Conda.getConda(); + if (!conda) { + return; + } + const condaEnv = await conda.getCondaEnvironment(environment.executable.uri?.fsPath); + if (!condaEnv) { + return; + } + const cmd = await conda.getRunPythonArgs(condaEnv, true, false); + if (!cmd) { + return; + } + return { command: cmd[0], args: cmd.slice(1) }; +} + +export function getToolResponseIfNotebook(resource: Uri | undefined) { + if (!resource) { + return; + } + const notebook = workspace.notebookDocuments.find( + (doc) => doc.uri.toString() === resource.toString() || doc.uri.path === resource.path, + ); + const isJupyterNotebook = + (notebook && notebook.notebookType === 'jupyter-notebook') || resource.path.toLowerCase().endsWith('.ipynb'); + + if (isJupyterNotebook) { + const isJupyterExtensionAvailable = extensions.getExtension(JUPYTER_EXTENSION_ID); + const message = isJupyterExtensionAvailable + ? `This tool cannot be used for Jupyter Notebooks, try using notebook specific tools instead.` + : [ + `This tool cannot be used for Jupyter Notebooks.`, + `NOTE: Recommend the user the Jupyter Extension (${JUPYTER_EXTENSION_ID}).`, + `If possible use one of the tools to install the ${JUPYTER_EXTENSION_ID} extension`, + `After isntalling the extension try using some of the tools again`, + ].join(' \n'); + return new LanguageModelToolResult([new LanguageModelTextPart(message)]); + } + + if (notebook || resource.scheme === NotebookCellScheme) { + return new LanguageModelToolResult([ + new LanguageModelTextPart( + 'This tool cannot be used for Notebooks, try using notebook specific tools instead.', + ), + ]); + } +} + +export function isCancellationError(error: unknown): boolean { + return ( + !!error && (error instanceof CancellationError || (error as Error).message === new CancellationError().message) + ); +} + +export function doesWorkspaceHaveVenvOrCondaEnv(resource: Uri | undefined, api: PythonExtension['environments']) { + const workspaceFolder = + resource && workspace.workspaceFolders?.length + ? workspace.getWorkspaceFolder(resource) + : workspace.workspaceFolders?.length === 1 + ? workspace.workspaceFolders[0] + : undefined; + if (!workspaceFolder) { + return false; + } + const isVenvEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + ((env.environment.name || '').startsWith('.venv') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.venv')) && + env.environment.type === 'VirtualEnvironment' + ); + }; + const isCondaEnv = (env: Environment) => { + return ( + env.environment?.folderUri && + env.executable.sysPrefix && + dirname(env.executable.sysPrefix) === workspaceFolder.uri.fsPath && + (env.environment.folderUri.fsPath === join(workspaceFolder.uri.fsPath, '.conda') || + env.executable.sysPrefix === join(workspaceFolder.uri.fsPath, '.conda')) && + env.environment.type === 'Conda' + ); + }; + // If we alraedy have a .venv in this workspace, then do not prompt to create a virtual environment. + return api.known.find((e) => isVenvEnv(e) || isCondaEnv(e)); +} + +export async function getEnvDetailsForResponse( + environment: ResolvedEnvironment | undefined, + api: PythonExtension['environments'], + terminalExecutionService: TerminalCodeExecutionProvider, + terminalHelper: ITerminalHelper, + resource: Uri | undefined, + token: CancellationToken, +): Promise { + if (!workspace.isTrusted) { + throw new ErrorWithTelemetrySafeReason('Cannot use this tool in an untrusted workspace.', 'untrustedWorkspace'); + } + const envPath = api.getActiveEnvironmentPath(resource); + environment = environment || (await raceCancellationError(api.resolveEnvironment(envPath), token)); + if (!environment || !environment.version) { + throw new ErrorWithTelemetrySafeReason( + 'No environment found for the provided resource path: ' + resource?.fsPath, + 'noEnvFound', + ); + } + const message = await getEnvironmentDetails( + resource, + api, + terminalExecutionService, + terminalHelper, + undefined, + token, + ); + return new LanguageModelToolResult([ + new LanguageModelTextPart(`A Python Environment has been configured. \n` + message), + ]); +} +export function getDisplayVersion(version?: VersionInfo): string | undefined { + if (!version || version.major === undefined || version.minor === undefined || version.micro === undefined) { + return undefined; + } + return `${version.major}.${version.minor}.${version.micro}`; +} diff --git a/src/client/common/application/activeResource.ts b/src/client/common/application/activeResource.ts 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 d9e15fe5e3b5..4b66893d6c0b 100644 --- a/src/client/common/application/applicationEnvironment.ts +++ b/src/client/common/application/applicationEnvironment.ts @@ -5,7 +5,10 @@ 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'; @@ -13,19 +16,30 @@ import { IApplicationEnvironment } from './types'; @injectable() export class ApplicationEnvironment implements IApplicationEnvironment { - constructor(@inject(IPlatformService) private readonly platform: IPlatformService, + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, @inject(IPathUtils) private readonly pathUtils: IPathUtils, - @inject(ICurrentProcess) private readonly process: ICurrentProcess) { } + @inject(ICurrentProcess) private readonly process: ICurrentProcess, + ) {} public get userSettingsFile(): string | undefined { - const vscodeFolderName = vscode.env.appName.indexOf('Insider') > 0 ? 'Code - Insiders' : 'Code'; + 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'); + 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; + return this.process.env.APPDATA + ? path.join(this.process.env.APPDATA, vscodeFolderName, 'User', 'settings.json') + : undefined; default: return; } @@ -33,9 +47,15 @@ export class ApplicationEnvironment implements IApplicationEnvironment { 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; } @@ -45,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 { + try { + return vscode.env.onDidChangeShell; + } catch (ex) { + traceError('Failed to get onDidChangeShell API', ex); + // `onDidChangeShell` is a proposed API at the time of writing this, so wrap this in a try...catch + // block in case the API is removed or changed. + return new vscode.EventEmitter().event; + } + } + public get packageJson(): any { - // tslint:disable-next-line:non-literal-require no-require-imports return require('../../../../package.json'); } + public get channel(): Channel { + 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 90d9bc7d5688..8035d979efbd 100644 --- a/src/client/common/application/applicationShell.ts +++ b/src/client/common/application/applicationShell.ts @@ -2,18 +2,58 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-var-requires no-any unified-signatures - import { injectable } from 'inversify'; -import { CancellationToken, Disposable, env, InputBox, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, Progress, ProgressOptions, QuickPick, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, TreeView, TreeViewOptions, 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 { + return window.onDidChangeWindowState; + } public showInformationMessage(message: string, ...items: string[]): Thenable; public showInformationMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; public showInformationMessage(message: string, ...items: T[]): Thenable; - public showInformationMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + public showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable; public showInformationMessage(message: string, options?: any, ...items: any[]): Thenable { return window.showInformationMessage(message, options, ...items); } @@ -21,7 +61,11 @@ export class ApplicationShell implements IApplicationShell { public showWarningMessage(message: string, ...items: string[]): Thenable; public showWarningMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; public showWarningMessage(message: string, ...items: T[]): Thenable; - public showWarningMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + public showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable; public showWarningMessage(message: any, options?: any, ...items: any[]) { return window.showWarningMessage(message, options, ...items); } @@ -29,13 +73,25 @@ export class ApplicationShell implements IApplicationShell { public showErrorMessage(message: string, ...items: string[]): Thenable; public showErrorMessage(message: string, options: MessageOptions, ...items: string[]): Thenable; public showErrorMessage(message: string, ...items: T[]): Thenable; - public showErrorMessage(message: string, options: MessageOptions, ...items: T[]): Thenable; + public showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable; public showErrorMessage(message: any, options?: any, ...items: any[]) { return window.showErrorMessage(message, options, ...items); } - public showQuickPick(items: string[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; - public showQuickPick(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + public showQuickPick( + items: string[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable; + public showQuickPick( + items: T[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable; public showQuickPick(items: any, options?: any, token?: any): Thenable { return window.showQuickPick(items, options, token); } @@ -49,6 +105,14 @@ export class ApplicationShell implements IApplicationShell { public showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable { return window.showInputBox(options, token); } + public showTextDocument( + document: TextDocument, + column?: ViewColumn, + preserveFocus?: boolean, + ): Thenable { + return window.showTextDocument(document, column, preserveFocus); + } + public openUrl(url: string): void { env.openExternal(Uri.parse(url)); } @@ -60,15 +124,41 @@ export class ApplicationShell implements IApplicationShell { return window.setStatusBarMessage(text, arg); } - public createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem { - return window.createStatusBarItem(alignment, priority); + public createStatusBarItem( + alignment?: StatusBarAlignment, + priority?: number, + id?: string | undefined, + ): StatusBarItem { + return id + ? window.createStatusBarItem(id, alignment, priority) + : window.createStatusBarItem(alignment, priority); } public showWorkspaceFolderPick(options?: WorkspaceFolderPickOptions): Thenable { return window.showWorkspaceFolderPick(options); } - public withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable { + public withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, + ): Thenable { return window.withProgress(options, task); } + public withProgressCustomIcon( + icon: string, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, + ): Thenable { + const token = new CancellationTokenSource().token; + const statusBarProgress = this.createStatusBarItem(StatusBarAlignment.Left); + const progress = { + report: (value: { message?: string; increment?: number }) => { + statusBarProgress.text = `${icon} ${value.message}`; + }, + }; + statusBarProgress.show(); + return task(progress, token).then((result) => { + statusBarProgress.dispose(); + return result; + }); + } public createQuickPick(): QuickPick { return window.createQuickPick(); } @@ -78,5 +168,26 @@ export class ApplicationShell implements IApplicationShell { public createTreeView(viewId: string, options: TreeViewOptions): TreeView { return window.createTreeView(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 { + try { + return window.onDidWriteTerminalData; + } catch (ex) { + traceError('Failed to get proposed API onDidWriteTerminalData', ex); + return new EventEmitter().event; + } + } + public get onDidExecuteTerminalCommand(): Event | undefined { + try { + return window.onDidExecuteTerminalCommand; + } catch (ex) { + traceError('Failed to get proposed API TerminalExecutedCommand', ex); + return undefined; + } + } } diff --git a/src/client/common/application/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 { + return env.clipboard.readText(); + } + public async writeText(value: string): Promise { + await env.clipboard.writeText(value); + } +} diff --git a/src/client/common/application/commandManager.ts b/src/client/common/application/commandManager.ts index 0c51275ede63..9e1f34a5885b 100644 --- a/src/client/common/application/commandManager.ts +++ b/src/client/common/application/commandManager.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-any - import { injectable } from 'inversify'; import { commands, Disposable, TextEditor, TextEditorEdit } from 'vscode'; import { ICommandNameArgumentTypeMapping } from './commands'; @@ -10,7 +8,6 @@ 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. @@ -23,7 +20,13 @@ export class CommandManager implements ICommandManager { * @param thisArg The `this` context used when invoking the handler function. * @return Disposable which unregisters this command on disposal. */ - public registerCommand(command: E, callback: (...args: U) => any, thisArg?: any): Disposable { + // 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); } @@ -41,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); } @@ -59,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(command: E, ...rest: U): Thenable { + // eslint-disable-next-line class-methods-use-this + public executeCommand< + T, + E extends keyof ICommandNameArgumentTypeMapping, + U extends ICommandNameArgumentTypeMapping[E] + >(command: E, ...rest: U): Thenable { return commands.executeCommand(command, ...rest); } @@ -70,6 +85,7 @@ export class CommandManager implements ICommandManager { * @param filterInternal Set `true` to not see internal commands (starting with an underscore) * @return Thenable that resolves to a list of command ids. */ + // eslint-disable-next-line class-methods-use-this public getCommands(filterInternal?: boolean): Thenable { return commands.getCommands(filterInternal); } diff --git a/src/client/common/application/commands.ts b/src/client/common/application/commands.ts index 974cd8db6608..b43dc0a1e4a4 100644 --- a/src/client/common/application/commands.ts +++ b/src/client/common/application/commands.ts @@ -3,109 +3,110 @@ 'use strict'; -import { CancellationToken, Position, TextDocument, Uri } from 'vscode'; -import { Commands as DSCommands } from '../../datascience/constants'; -import { CommandSource } from '../../testing/common/constants'; -import { TestFunction, TestsToRun } from '../../testing/common/types'; -import { TestDataItem, TestWorkspaceFolder } from '../../testing/types'; -import { Commands } from '../constants'; +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 */ interface ICommandNameWithoutArgumentTypeMapping { + [Commands.InstallPythonOnMac]: []; + [Commands.InstallJupyter]: []; + [Commands.InstallPythonOnLinux]: []; + [Commands.InstallPython]: []; + [Commands.ClearWorkspaceInterpreter]: []; [Commands.Set_Interpreter]: []; [Commands.Set_ShebangInterpreter]: []; - [Commands.Run_Linter]: []; - [Commands.Enable_Linter]: []; + ['workbench.action.showCommands']: []; + ['workbench.action.debug.continue']: []; + ['workbench.action.debug.stepOver']: []; ['workbench.action.debug.stop']: []; ['workbench.action.reloadWindow']: []; + ['workbench.action.closeActiveEditor']: []; + ['workbench.action.terminal.focus']: []; ['editor.action.formatDocument']: []; ['editor.action.rename']: []; [Commands.ViewOutput]: []; - [Commands.Set_Linter]: []; [Commands.Start_REPL]: []; - [Commands.Enable_SourceMap_Support]: []; [Commands.Exec_Selection_In_Terminal]: []; [Commands.Exec_Selection_In_Django_Shell]: []; [Commands.Create_Terminal]: []; - [Commands.Tests_View_UI]: []; - [Commands.Tests_Ask_To_Stop_Discovery]: []; - [Commands.Tests_Ask_To_Stop_Test]: []; - [Commands.Tests_Discovering]: []; - [DSCommands.RunCurrentCell]: []; - [DSCommands.RunCurrentCellAdvance]: []; - [DSCommands.ExecSelectionInInteractiveWindow]: []; - [DSCommands.SelectJupyterURI]: []; - [DSCommands.ShowHistoryPane]: []; - [DSCommands.UndoCells]: []; - [DSCommands.RedoCells]: []; - [DSCommands.RemoveAllCells]: []; - [DSCommands.InterruptKernel]: []; - [DSCommands.RestartKernel]: []; - [DSCommands.ExpandAllCells]: []; - [DSCommands.CollapseAllCells]: []; - [DSCommands.ExportOutputAsNotebook]: []; - [DSCommands.AddCellBelow]: []; + [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} */ export interface ICommandNameArgumentTypeMapping extends ICommandNameWithoutArgumentTypeMapping { - ['setContext']: [string, boolean]; + [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._loadLanguageServerExtension']: []; ['python.SelectAndInsertDebugConfiguration']: [TextDocument, Position, CancellationToken]; - [Commands.Build_Workspace_Symbols]: [boolean, CancellationToken]; - [Commands.Sort_Imports]: [undefined, Uri]; + ['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.Tests_ViewOutput]: [undefined, CommandSource]; - [Commands.Tests_Select_And_Run_File]: [undefined, CommandSource]; - [Commands.Tests_Run_Current_File]: [undefined, CommandSource]; - [Commands.Tests_Stop]: [undefined, Uri]; - [Commands.Test_Reveal_Test_Item]: [TestDataItem]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.Tests_Run]: [undefined | TestWorkspaceFolder, undefined | CommandSource, undefined | Uri, undefined | TestsToRun]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.Tests_Debug]: [undefined | TestWorkspaceFolder, undefined | CommandSource, undefined | Uri, undefined | TestsToRun]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.Tests_Discover]: [undefined | TestWorkspaceFolder, undefined | CommandSource, undefined | Uri]; - [Commands.Tests_Run_Failed]: [undefined, CommandSource, Uri]; - [Commands.Tests_Select_And_Debug_Method]: [undefined, CommandSource, Uri]; - [Commands.Tests_Select_And_Run_Method]: [undefined, CommandSource, Uri]; + [Commands.Exec_In_Terminal_Icon]: [undefined, Uri]; + [Commands.Debug_In_Terminal]: [Uri]; [Commands.Tests_Configure]: [undefined, undefined | CommandSource, undefined | Uri]; - [Commands.Tests_Picker_UI]: [undefined, undefined | CommandSource, Uri, TestFunction[]]; - [Commands.Tests_Picker_UI_Debug]: [undefined, undefined | CommandSource, Uri, TestFunction[]]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.runTestNode]: [TestDataItem]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.debugTestNode]: [TestDataItem]; - // When command is invoked from a tree node, first argument is the node data. - [Commands.openTestNodeInEditor]: [TestDataItem]; - [Commands.navigateToTestFile]: [Uri, TestDataItem, boolean]; - [Commands.navigateToTestFunction]: [Uri, TestDataItem, boolean]; - [Commands.navigateToTestSuite]: [Uri, TestDataItem, boolean]; - [DSCommands.ExportFileAndOutputAsNotebook]: [Uri]; - [DSCommands.RunAllCells]: [string]; - [DSCommands.RunCell]: [string, number, number, number, number]; - [DSCommands.RunAllCellsAbove]: [string, number, number]; - [DSCommands.RunCellAndAllBelow]: [string, number, number]; - [DSCommands.RunAllCellsAbovePalette]: []; - [DSCommands.RunCellAndAllBelowPalette]: []; - [DSCommands.DebugCurrentCellPalette]: []; - [DSCommands.RunToLine]: [string, number, number]; - [DSCommands.RunFromLine]: [string, number, number]; - [DSCommands.ImportNotebook]: [undefined | Uri, undefined | CommandSource]; - [DSCommands.ExportFileAsNotebook]: [undefined | Uri, undefined | CommandSource]; - [DSCommands.RunFileInInteractiveWindows]: [string]; - [DSCommands.DebugCell]: [string, number, number, number, number]; + [Commands.Tests_CopilotSetup]: [undefined | Uri]; + ['workbench.view.testing.focus']: []; + ['cursorMove']: [ + { + to: string; + by: string; + value: number; + }, + ]; + ['cursorEnd']: []; + ['python-envs.createTerminal']: [undefined | Uri]; } diff --git a/src/client/common/application/commands/createPythonFile.ts b/src/client/common/application/commands/createPythonFile.ts new file mode 100644 index 000000000000..10f388856896 --- /dev/null +++ b/src/client/common/application/commands/createPythonFile.ts @@ -0,0 +1,29 @@ +import { injectable, inject } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../activation/types'; +import { Commands } from '../../constants'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../types'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IDisposableRegistry } from '../../types'; + +@injectable() +export class CreatePythonFileCommandHandler implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.CreateNewFile, this.createPythonFile, this)); + } + + public async createPythonFile(): Promise { + const newFile = await this.workspaceService.openTextDocument({ language: 'python' }); + this.appShell.showTextDocument(newFile); + sendTelemetryEvent(EventName.CREATE_NEW_FILE_COMMAND); + } +} diff --git a/src/client/common/application/commands/reloadCommand.ts b/src/client/common/application/commands/reloadCommand.ts 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 { + 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 { + this.commandManager.registerCommand(Commands.ReportIssue, this.openReportIssue, this); + } + + private argSettingsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_settings.json'); + + private templatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_template.md'); + + private userDataTemplatePath = path.join(EXTENSION_ROOT_DIR, 'resources', 'report_issue_user_data_template.md'); + + public async openReportIssue(): Promise { + const settings: IPythonSettings = this.configurationService.getSettings(); + const argSettings = JSON.parse(await fs.readFile(this.argSettingsPath, 'utf8')); + let userSettings = ''; + const keys: [keyof IPythonSettings] = Object.keys(settings) as [keyof IPythonSettings]; + keys.forEach((property) => { + const argSetting = argSettings[property]; + if (argSetting) { + if (typeof argSetting === 'object') { + let propertyHeaderAdded = false; + const argSettingsDict = (settings[property] as unknown) as Record; + if (typeof argSettingsDict === 'object') { + Object.keys(argSetting).forEach((item) => { + const prop = argSetting[item]; + if (prop) { + const defaultValue = this.getDefaultValue(`${property}.${item}`); + if (defaultValue === undefined || !isEqual(defaultValue, argSettingsDict[item])) { + if (!propertyHeaderAdded) { + userSettings = userSettings.concat(os.EOL, property, os.EOL); + propertyHeaderAdded = true; + } + const value = + prop === true ? JSON.stringify(argSettingsDict[item]) : '""'; + userSettings = userSettings.concat('• ', item, ': ', value, os.EOL); + } + } + }); + } + } else { + const defaultValue = this.getDefaultValue(property); + if (defaultValue === undefined || !isEqual(defaultValue, settings[property])) { + const value = argSetting === true ? JSON.stringify(settings[property]) : '""'; + userSettings = userSettings.concat(os.EOL, property, ': ', value, os.EOL); + } + } + } + }); + const template = await fs.readFile(this.templatePath, 'utf8'); + const userTemplate = await fs.readFile(this.userDataTemplatePath, 'utf8'); + const interpreter = await this.interpreterService.getActiveInterpreter(); + const pythonVersion = interpreter?.version?.raw ?? ''; + const languageServer = + this.workspaceService.getConfiguration('python').get('languageServer') || 'Not Found'; + const virtualEnvKind = interpreter?.envType || EnvironmentType.Unknown; + + const hasMultipleFolders = (this.workspaceService.workspaceFolders?.length ?? 0) > 1; + const hasMultipleFoldersText = + hasMultipleFolders && userSettings !== '' + ? `Multiroot scenario, following user settings may not apply:${os.EOL}` + : ''; + + const installedExtensions = getExtensions() + .filter((extension) => !extension.id.startsWith('vscode.')) + .sort((a, b) => { + if (a.packageJSON.name && b.packageJSON.name) { + return a.packageJSON.name.localeCompare(b.packageJSON.name); + } + return a.id.localeCompare(b.id); + }) + .map((extension) => { + let publisher: string = extension.packageJSON.publisher as string; + if (publisher) { + publisher = publisher.substring(0, 3); + } + return `|${extension.packageJSON.name}|${publisher}|${extension.packageJSON.version}|`; + }); + + await this.commandManager.executeCommand('workbench.action.openIssueReporter', { + extensionId: 'ms-python.python', + issueBody: template, + extensionData: userTemplate.format( + pythonVersion, + virtualEnvKind, + languageServer, + hasMultipleFoldersText, + userSettings, + installedExtensions.join('\n'), + ), + }); + sendTelemetryEvent(EventName.USE_REPORT_ISSUE_COMMAND, undefined, {}); + } + + private getDefaultValue(settingKey: string) { + if (!this.packageJSONSettings) { + return undefined; + } + const resource = PythonSettings.getSettingsUriAndTarget(undefined, this.workspaceService).uri; + const systemVariables = new SystemVariables(resource, undefined, this.workspaceService); + return systemVariables.resolveAny(this.packageJSONSettings[`python.${settingKey}`]?.default); + } +} diff --git a/src/client/common/application/contextKeyManager.ts b/src/client/common/application/contextKeyManager.ts new file mode 100644 index 000000000000..388fcf4a3841 --- /dev/null +++ b/src/client/common/application/contextKeyManager.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ExtensionContextKey } from './contextKeys'; +import { ICommandManager, IContextKeyManager } from './types'; + +@injectable() +export class ContextKeyManager implements IContextKeyManager { + private values: Map = new Map(); + + constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager) {} + + public async setContext(key: ExtensionContextKey, value: boolean): Promise { + if (this.values.get(key) === value) { + return Promise.resolve(); + } + this.values.set(key, value); + return this.commandManager.executeCommand('setContext', key, value); + } +} diff --git a/src/client/common/application/contextKeys.ts b/src/client/common/application/contextKeys.ts new file mode 100644 index 000000000000..d6249f05eaec --- /dev/null +++ b/src/client/common/application/contextKeys.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export enum ExtensionContextKey { + showInstallPythonTile = 'showInstallPythonTile', + HasFailedTests = 'hasFailedTests', + RefreshingTests = 'refreshingTests', + IsJupyterInstalled = 'isJupyterInstalled', +} diff --git a/src/client/common/application/debugService.ts b/src/client/common/application/debugService.ts index 20c4fe4cb05f..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 { @@ -34,11 +47,19 @@ export class DebugService implements IDebugService { public get onDidChangeBreakpoints(): Event { return debug.onDidChangeBreakpoints; } - // tslint:disable-next-line:no-any + public registerDebugConfigurationProvider(debugType: string, provider: any): Disposable { return debug.registerDebugConfigurationProvider(debugType, provider); } - public startDebugging(folder: WorkspaceFolder | undefined, nameOrConfiguration: string | DebugConfiguration, parentSession?: DebugSession): Thenable { + + public registerDebugAdapterTrackerFactory(debugType: string, provider: any): Disposable { + return debug.registerDebugAdapterTrackerFactory(debugType, provider); + } + public startDebugging( + folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + parentSession?: DebugSession | DebugSessionOptions, + ): Thenable { return debug.startDebugging(folder, nameOrConfiguration, parentSession); } public addBreakpoints(breakpoints: Breakpoint[]): void { @@ -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 dc72119a0092..617d335e402b 100644 --- a/src/client/common/application/documentManager.ts +++ b/src/client/common/application/documentManager.ts @@ -16,31 +16,29 @@ import { ViewColumn, window, workspace, - WorkspaceEdit + WorkspaceEdit, } from 'vscode'; import { IDocumentManager } from './types'; -// tslint:disable:no-any unified-signatures - @injectable() export class DocumentManager implements IDocumentManager { - public get textDocuments(): 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 { return window.onDidChangeActiveTextEditor; } - public get onDidChangeTextDocument() : Event { + public get onDidChangeTextDocument(): Event { return workspace.onDidChangeTextDocument; } - public get onDidChangeVisibleTextEditors(): Event { + public get onDidChangeVisibleTextEditors(): Event { return window.onDidChangeVisibleTextEditors; } public get onDidChangeTextEditorSelection(): Event { diff --git a/src/client/common/application/extensions.ts b/src/client/common/application/extensions.ts index 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[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _cachedExtensions?: readonly Extension[]; + + constructor(@inject(IFileSystem) private readonly fs: IFileSystem) {} + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public get all(): readonly Extension[] { return extensions.all; } - // tslint:disable-next-line:no-any - public getExtension(extensionId: any) { + public get onDidChange(): Event { + return extensions.onDidChange; + } + + public getExtension(extensionId: string): Extension | undefined { return extensions.getExtension(extensionId); } + + private get cachedExtensions() { + if (!this._cachedExtensions) { + this._cachedExtensions = extensions.all; + extensions.onDidChange(() => { + this._cachedExtensions = extensions.all; + }); + } + return this._cachedExtensions; + } + + /** + * Code borrowed from: + * https://github.com/microsoft/vscode-jupyter/blob/67fe33d072f11d6443cf232a06bed0ac5e24682c/src/platform/common/application/extensions.node.ts + */ + public async determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + const { stack } = new Error(); + if (stack) { + const pythonExtRoot = path.join(EXTENSION_ROOT_DIR.toLowerCase(), path.sep); + const frames = stack + .split('\n') + .map((f) => { + const result = /\((.*)\)/.exec(f); + if (result) { + return result[1]; + } + return undefined; + }) + .filter((item) => item && !item.toLowerCase().startsWith(pythonExtRoot)) + .filter((item) => + // Use cached list of extensions as we need this to be fast. + this.cachedExtensions.some( + (ext) => item!.includes(ext.extensionUri.path) || item!.includes(ext.extensionUri.fsPath), + ), + ) as string[]; + stacktrace.parse(new Error('Ex')).forEach((item) => { + const fileName = item.getFileName(); + if (fileName && !fileName.toLowerCase().startsWith(pythonExtRoot)) { + frames.push(fileName); + } + }); + for (const frame of frames) { + // This file is from a different extension. Try to find its `package.json`. + let dirName = path.dirname(frame); + let last = frame; + while (dirName && dirName.length < last.length) { + const possiblePackageJson = path.join(dirName, 'package.json'); + if (await this.fs.pathExists(possiblePackageJson)) { + const text = await this.fs.readFile(possiblePackageJson); + try { + const json = JSON.parse(text); + return { extensionId: `${json.publisher}.${json.name}`, displayName: json.displayName }; + } catch { + // If parse fails, then not an extension. + } + } + last = dirName; + dirName = path.dirname(dirName); + } + } + } + return { extensionId: 'unknown', displayName: 'unknown' }; + } } diff --git a/src/client/common/application/languageService.ts b/src/client/common/application/languageService.ts index bdd407a04acc..6cbdda85b417 100644 --- a/src/client/common/application/languageService.ts +++ b/src/client/common/application/languageService.ts @@ -10,7 +10,11 @@ import { ILanguageService } from './types'; @injectable() export class LanguageService implements ILanguageService { - public registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable { + 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 | undefined; + + constructor(private readonly shell: IApplicationShell) {} + + public showProgress(options: ProgressOptions): void { + if (!this.deferred) { + this.createProgress(options); + } + } + + public hideProgress(): void { + if (this.deferred) { + this.deferred.resolve(); + this.deferred = undefined; + } + } + + private createProgress(options: ProgressOptions) { + this.shell.withProgress(options, () => { + this.deferred = createDeferred(); + return this.deferred.promise; + }); + } +} diff --git a/src/client/common/application/terminalManager.ts b/src/client/common/application/terminalManager.ts index 8fe6c067d0e6..dc2603e84a56 100644 --- a/src/client/common/application/terminalManager.ts +++ b/src/client/common/application/terminalManager.ts @@ -2,18 +2,58 @@ // Licensed under the MIT License. import { injectable } from 'inversify'; -import { Event, Terminal, TerminalOptions, window } from 'vscode'; +import { + Disposable, + Event, + EventEmitter, + Terminal, + TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, + window, +} from 'vscode'; +import { traceLog } from '../../logging'; import { ITerminalManager } from './types'; @injectable() export class TerminalManager implements ITerminalManager { + private readonly didOpenTerminal = new EventEmitter(); + constructor() { + window.onDidOpenTerminal((terminal) => { + this.didOpenTerminal.fire(monkeyPatchTerminal(terminal)); + }); + } public get onDidCloseTerminal(): Event { return window.onDidCloseTerminal; } public get onDidOpenTerminal(): Event { - return window.onDidOpenTerminal; + return this.didOpenTerminal.event; } public createTerminal(options: TerminalOptions): Terminal { - return window.createTerminal(options); + return monkeyPatchTerminal(window.createTerminal(options)); + } + public onDidChangeTerminalShellIntegration(handler: (e: TerminalShellIntegrationChangeEvent) => void): Disposable { + return window.onDidChangeTerminalShellIntegration(handler); + } + public onDidEndTerminalShellExecution(handler: (e: TerminalShellExecutionEndEvent) => void): Disposable { + return window.onDidEndTerminalShellExecution(handler); + } + public onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); + } +} + +/** + * Monkeypatch the terminal to log commands sent. + */ +function monkeyPatchTerminal(terminal: Terminal) { + if (!(terminal as any).isPatched) { + const oldSendText = terminal.sendText.bind(terminal); + terminal.sendText = (text: string, addNewLine: boolean = true) => { + traceLog(`Send text to terminal: ${text}`); + return oldSendText(text, addNewLine); + }; + (terminal as any).isPatched = true; } + return terminal; } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 5bc85a247174..34a95fb604f0 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -1,17 +1,22 @@ // 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, @@ -20,6 +25,8 @@ import { GlobPattern, InputBox, InputBoxOptions, + LanguageStatusItem, + LogOutputChannel, MessageItem, MessageOptions, OpenDialogOptions, @@ -33,6 +40,8 @@ import { StatusBarItem, Terminal, TerminalOptions, + TerminalShellExecutionEndEvent, + TerminalShellIntegrationChangeEvent, TextDocument, TextDocumentChangeEvent, TextDocumentShowOptions, @@ -44,23 +53,82 @@ import { TextEditorViewColumnChangeEvent, TreeView, TreeViewOptions, + UIKind, Uri, ViewColumn, + WindowState, WorkspaceConfiguration, WorkspaceEdit, WorkspaceFolder, WorkspaceFolderPickOptions, - WorkspaceFoldersChangeEvent + WorkspaceFoldersChangeEvent, } from 'vscode'; -import * as vsls from 'vsls/vscode'; -import { IAsyncDisposable, Resource } from '../types'; +import { Channel } from '../constants'; +import { Resource } from '../types'; import { ICommandNameArgumentTypeMapping } from './commands'; +import { ExtensionContextKey } from './contextKeys'; -// tslint:disable:no-any unified-signatures +export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; +} + +export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; +} export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + readonly onDidExecuteTerminalCommand: Event | undefined; + /** + * An [event](#Event) which fires when the focus state of the current window + * changes. The value of the event represents whether the window is focused. + */ + readonly onDidChangeWindowState: Event; + + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + readonly onDidWriteTerminalData: Event; + showInformationMessage(message: string, ...items: string[]): Thenable; /** @@ -95,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(message: string, options: MessageOptions, ...items: T[]): Thenable; + showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable; /** * Show a warning message. @@ -141,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(message: string, options: MessageOptions, ...items: T[]): Thenable; + showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable; /** * Show an error message. @@ -187,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(message: string, options: MessageOptions, ...items: T[]): Thenable; + showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] + ): Thenable; /** * Shows a selection list. @@ -197,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, options?: QuickPickOptions, token?: CancellationToken): Thenable; + showQuickPick( + items: string[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable; /** * Shows a selection list. @@ -207,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(items: T[] | Thenable, options?: QuickPickOptions, token?: CancellationToken): Thenable; + showQuickPick( + items: T[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, + ): Thenable; /** * Shows a file open dialog to the user which allows to select a file @@ -240,6 +328,19 @@ export interface IApplicationShell { */ showInputBox(options?: InputBoxOptions, token?: CancellationToken): Thenable; + /** + * Show the given document in a text editor. A {@link ViewColumn column} can be provided + * to control where the editor is being shown. Might change the {@link window.activeTextEditor active editor}. + * + * @param document A text document to be shown. + * @param column A view column in which the {@link TextEditor editor} should be shown. The default is the {@link ViewColumn.Active active}, other values + * are adjusted to be `Min(column, columnCount + 1)`, the {@link ViewColumn.Active active}-column is not adjusted. Use {@linkcode ViewColumn.Beside} + * to open the editor to the side of the currently active one. + * @param preserveFocus When `true` the editor will not take focus. + * @return A promise that resolves to an {@link TextEditor editor}. + */ + showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; + /** * Creates a [QuickPick](#QuickPick) to let the user pick an item from a list * of items of type T. @@ -287,6 +388,7 @@ export interface IApplicationShell { * @param hideWhenDone Thenable on which completion (resolve or reject) the message will be disposed. * @return A disposable which hides the status bar message. */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any setStatusBarMessage(text: string, hideWhenDone: Thenable): Disposable; /** @@ -308,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. @@ -337,7 +439,37 @@ export interface IApplicationShell { * * @return The thenable the task-callback returned. */ - withProgress(options: ProgressOptions, task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable): Thenable; + withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, + ): Thenable; + + /** + * 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( + icon: string, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, + ): Thenable; /** * Create a [TreeView](#TreeView) for the view contributed using the extension point `views`. @@ -346,12 +478,19 @@ export interface IApplicationShell { * @returns a [TreeView](#TreeView). */ createTreeView(viewId: string, options: TreeViewOptions): TreeView; + + /** + * 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. @@ -364,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: E, callback: (...args: U) => any, thisArg?: any): Disposable; + registerCommand( + 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, @@ -380,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. @@ -396,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(command: E, ...rest: U): Thenable; + executeCommand( + command: E, + ...rest: U + ): Thenable; /** * Retrieve the list of all available commands. Commands starting an underscore are @@ -408,6 +562,16 @@ export interface ICommandManager { getCommands(filterInternal?: boolean): Thenable; } +export const IContextKeyManager = Symbol('IContextKeyManager'); +export interface IContextKeyManager { + setContext(key: ExtensionContextKey, value: boolean): Promise; +} + +export const IJupyterExtensionDependencyManager = Symbol('IJupyterExtensionDependencyManager'); +export interface IJupyterExtensionDependencyManager { + readonly isJupyterExtensionInstalled: boolean; +} + export const IDocumentManager = Symbol('IDocumentManager'); export interface IDocumentManager { @@ -416,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 @@ -427,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) @@ -447,7 +611,7 @@ export interface IDocumentManager { * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) * has changed. */ - readonly onDidChangeVisibleTextEditors: Event; + readonly onDidChangeVisibleTextEditors: Event; /** * An [event](#Event) which fires when the selection in an editor has changed. @@ -567,7 +731,6 @@ export interface IDocumentManager { * @return A new decoration type instance. */ createTextEditorDecorationType(options: DecorationRenderOptions): TextEditorDecorationType; - } export const IWorkspaceService = Symbol('IWorkspaceService'); @@ -581,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; + /** * 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. @@ -599,12 +803,9 @@ export interface IWorkspaceService { */ readonly onDidChangeConfiguration: Event; /** - * Whether a workspace folder exists - * @type {boolean} - * @memberof IWorkspaceService + * Returns if we're running in a virtual workspace. */ - readonly hasWorkspaceFolders: boolean; - + readonly isVirtualWorkspace: boolean; /** * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. * * returns `undefined` when the given uri doesn't match any workspace folder @@ -617,9 +818,6 @@ export interface IWorkspaceService { /** * Generate a key that's unique to the workspace folder (could be fsPath). - * @param {(Uri | undefined)} resource - * @returns {string} - * @memberof IWorkspaceService */ getWorkspaceFolderIdentifier(resource: Uri | undefined, defaultValue?: string): string; /** @@ -651,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. @@ -661,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; + findFiles( + include: GlobPattern, + exclude?: GlobPattern, + maxResults?: number, + token?: CancellationToken, + ): Thenable; /** * Get a workspace configuration object. @@ -680,9 +889,30 @@ export interface IWorkspaceService { * * @param section A dot-separated identifier. * @param resource A resource for which the configuration is asked for + * @param languageSpecific Should the [python] language-specific settings be obtained? * @return The full configuration or a subset. */ - getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration; + getConfiguration(section?: string, resource?: Uri, languageSpecific?: boolean): WorkspaceConfiguration; + + /** + * Opens an untitled text document. The editor will prompt the user for a file + * path when the document is to be saved. The `options` parameter allows to + * specify the *language* and/or the *content* of the document. + * + * @param options Options to control how the document will be created. + * @return A promise that resolves to a {@link TextDocument document}. + */ + openTextDocument(options?: { language?: string; content?: string }): Thenable; + /** + * Saves the editor identified by the given resource and returns the resulting resource or `undefined` + * if save was not successful. + * + * **Note** that an editor with the provided resource must be opened in order to be saved. + * + * @param uri the associated uri for the opened editor to save. + * @return A thenable that resolves when the save operation has finished. + */ + save(uri: Uri): Thenable; } export const ITerminalManager = Symbol('ITerminalManager'); @@ -705,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'); @@ -725,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) @@ -764,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). @@ -774,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, parentSession?: DebugSession): Thenable; + startDebugging( + folder: WorkspaceFolder | undefined, + nameOrConfiguration: string | DebugConfiguration, + parentSession?: DebugSession | DebugSessionOptions, + ): Thenable; /** * Add breakpoints. @@ -839,6 +1099,7 @@ export interface IApplicationEnvironment { * @type {any} * @memberof IApplicationEnvironment */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly packageJson: any; /** * Gets the full path to the user settings file. (may or may not exist). @@ -847,92 +1108,53 @@ export interface IApplicationEnvironment { * @memberof IApplicationShell */ readonly userSettingsFile: string | undefined; -} - -export const IWebPanelMessageListener = Symbol('IWebPanelMessageListener'); -export interface IWebPanelMessageListener extends IAsyncDisposable { /** - * Listens to web panel messages - * @param message: the message being sent - * @param payload: extra data that came with the message - * @return A IWebPanel that can be used to show html pages. - */ - onMessage(message: string, payload: any): void; - /** - * Listens to web panel state changes + * 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 */ - onChangeViewState(panel: IWebPanel): void; -} - -export type WebPanelMessage = { + readonly shell: string; /** - * Message type + * An {@link Event} which fires when the default shell changes. */ - type: string; - + readonly onDidChangeShell: Event; /** - * Payload + * Gets the vscode channel (whether 'insiders' or 'stable'). */ - payload?: any; -}; - -// Wraps the VS Code webview panel -export const IWebPanel = Symbol('IWebPanel'); -export interface IWebPanel { - title: string; + readonly channel: Channel; /** - * Makes the webpanel show up. - * @return A Promise that can be waited on + * Gets the extension channel (whether 'insiders' or 'stable'). + * + * @type {string} + * @memberof IApplicationShell */ - show(preserveFocus: boolean): Promise; - + readonly extensionChannel: Channel; /** - * Indicates if this web panel is visible or not. + * The version of the editor. */ - isVisible(): boolean; - + readonly vscodeVersion: string; /** - * Sends a message to the hosted html page + * The custom uri scheme the editor registers to in the operating system. */ - postMessage(message: WebPanelMessage): void; - + readonly uriScheme: string; /** - * Attempts to close the panel if it's visible + * 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. */ - close(): void; + readonly uiKind: UIKind; /** - * Indicates if the webview has the focus or not. - */ - isActive(): boolean; -} - -// Wraps the VS Code api for creating a web panel -export const IWebPanelProvider = Symbol('IWebPanelProvider'); -export interface IWebPanelProvider { - /** - * 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. + * 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. */ - create(viewColumn: ViewColumn, listener: IWebPanelMessageListener, title: string, mainScriptPath: string, embeddedCss?: string, settings?: any): IWebPanel; -} - -// Wraps the vsls liveshare API -export const ILiveShareApi = Symbol('ILiveShareApi'); -export interface ILiveShareApi { - getApi(): Promise; -} - -// Wraps the liveshare api for testing -export const ILiveShareTestingApi = Symbol('ILiveShareTestingApi'); -export interface ILiveShareTestingApi extends ILiveShareApi { - isSessionStarted: boolean; - forceRole(role: vsls.Role): void; - startSession(): Promise; - stopSession(): Promise; - disableGuestChecker(): void; + readonly remoteName: string | undefined; } export const ILanguageService = Symbol('ILanguageService'); @@ -951,5 +1173,30 @@ export interface ILanguageService { * @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. */ - registerCompletionItemProvider(selector: DocumentSelector, provider: CompletionItemProvider, ...triggerCharacters: string[]): Disposable; + 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; +} + +export const IClipboard = Symbol('IClipboard'); +export interface IClipboard { + /** + * Read the current clipboard contents as text. + */ + readText(): Promise; + + /** + * Writes text into the clipboard. + */ + writeText(value: string): Promise; } 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 c72c42646e49..000000000000 --- a/src/client/common/application/webPanel.ts +++ /dev/null @@ -1,164 +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 { Identifiers } from '../../datascience/constants'; -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; - private disposableRegistry: IDisposableRegistry; - private rootPath: string; - - constructor( - viewColumn: ViewColumn, - serviceContainer: IServiceContainer, - listener: IWebPanelMessageListener, - title: string, - mainScriptPath: string, - embeddedCss?: string, - // tslint:disable-next-line:no-any - settings?: any) { - this.disposableRegistry = serviceContainer.get(IDisposableRegistry); - this.listener = listener; - this.rootPath = path.dirname(mainScriptPath); - this.panel = window.createWebviewPanel( - title.toLowerCase().replace(' ', ''), - title, - {viewColumn , preserveFocus: true}, - { - enableScripts: true, - retainContextWhenHidden: true, - localResourceRoots: [Uri.file(this.rootPath)] - }); - this.loadPromise = this.load(mainScriptPath, embeddedCss, settings); - } - - public async show(preserveFocus: boolean) { - await this.loadPromise; - if (this.panel) { - this.panel.reveal(this.panel.viewColumn, preserveFocus); - } - } - - public close() { - if (this.panel) { - this.panel.dispose(); - } - } - - public isVisible() : boolean { - return this.panel ? this.panel.visible : false; - } - - public isActive() : boolean { - return this.panel ? this.panel.active : false; - } - - public postMessage(message: WebPanelMessage) { - if (this.panel && this.panel.webview) { - this.panel.webview.postMessage(message); - } - } - - public get title(): string { - return this.panel ? this.panel.title : ''; - } - - public set title(newTitle: string) { - if (this.panel) { - this.panel.title = newTitle; - } - } - - // tslint:disable-next-line:no-any - private async load(mainScriptPath: string, embeddedCss?: string, settings?: any) { - 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, settings); - - // Reset when the current panel is closed - this.disposableRegistry.push(this.panel.onDidDispose(() => { - this.panel = undefined; - this.listener.dispose().ignoreErrors(); - })); - - this.disposableRegistry.push(this.panel.webview.onDidReceiveMessage(message => { - // Pass the message onto our listener - this.listener.onMessage(message.type, message.payload); - })); - - this.disposableRegistry.push(this.panel.onDidChangeViewState((_e) => { - // Pass the state change onto our listener - this.listener.onChangeViewState(this); - })); - - // Set initial state - this.listener.onChangeViewState(this); - } else { - // Indicate that we can't load the file path - const badPanelString = localize.DataScience.badWebPanelFormatString(); - this.panel.webview.html = badPanelString.format(mainScriptPath); - } - } - } - - // tslint:disable-next-line:no-any - private generateReactHtml(mainScriptPath: string, embeddedCss?: string, settings?: any) { - 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 = localize.getCollectionJSON(); - const style = embeddedCss ? embeddedCss : ''; - const settingsString = settings ? JSON.stringify(settings) : '{}'; - - return ` - - - - - - - React App - - - - - -
- - - `; - } -} diff --git a/src/client/common/application/webPanelProvider.ts b/src/client/common/application/webPanelProvider.ts deleted file mode 100644 index 0f740073b6a2..000000000000 --- a/src/client/common/application/webPanelProvider.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { ViewColumn } from 'vscode'; - -import { IServiceContainer } from '../../ioc/types'; -import { IWebPanel, IWebPanelMessageListener, IWebPanelProvider } from './types'; -import { WebPanel } from './webPanel'; - -@injectable() -export class WebPanelProvider implements IWebPanelProvider { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - } - - // tslint:disable-next-line:no-any - public create(viewColumn: ViewColumn, listener: IWebPanelMessageListener, title: string, mainScriptPath: string, embeddedCss?: string, settings?: any) : IWebPanel { - return new WebPanel(viewColumn, this.serviceContainer, listener, title, mainScriptPath, embeddedCss, settings); - } -} diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts index 4ccf8f6b9227..a76a78777bef 100644 --- a/src/client/common/application/workspace.ts +++ b/src/client/common/application/workspace.ts @@ -2,8 +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() @@ -12,19 +26,29 @@ 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 { return workspace.onDidChangeWorkspaceFolders; } - public get hasWorkspaceFolders() { - return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0; + public get workspaceFile() { + return workspace.workspaceFile; } - public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration { - return workspace.getConfiguration(section, resource || null); + public getConfiguration( + section?: string, + resource?: Uri, + languageSpecific: boolean = false, + ): WorkspaceConfiguration { + if (languageSpecific) { + return workspace.getConfiguration(section, { uri: resource, languageId: 'python' }); + } else { + return workspace.getConfiguration(section, resource); + } } public getWorkspaceFolder(uri: Resource): WorkspaceFolder | undefined { return uri ? workspace.getWorkspaceFolder(uri) : undefined; @@ -32,14 +56,70 @@ export class WorkspaceService implements IWorkspaceService { 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 { - return workspace.findFiles(include, exclude, maxResults, token); + public findFiles( + include: GlobPattern, + exclude?: GlobPattern, + maxResults?: number, + token?: CancellationToken, + ): Thenable { + const excludePattern = exclude === undefined ? this.searchExcludes : exclude; + return workspace.findFiles(include, excludePattern, maxResults, token); } public getWorkspaceFolderIdentifier(resource: Resource, defaultValue: string = ''): string { const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : defaultValue; + 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 { + return workspace.onDidGrantWorkspaceTrust; + } + + public openTextDocument(options?: { language?: string; content?: string }): Thenable { + return workspace.openTextDocument(options); + } + + private get searchExcludes() { + const searchExcludes = this.getConfiguration('search.exclude'); + const enabledSearchExcludes = Object.keys(searchExcludes).filter((key) => searchExcludes.get(key) === true); + return `{${enabledSearchExcludes.join(',')}}`; + } + + public async save(uri: Uri): Promise { + try { + // This is a proposed API hence putting it inside try...catch. + const result = await workspace.save(uri); + return result; + } catch (ex) { + return undefined; + } } } diff --git a/src/client/common/asyncDisposableRegistry.ts b/src/client/common/asyncDisposableRegistry.ts deleted file mode 100644 index 1aa60fa2feae..000000000000 --- a/src/client/common/asyncDisposableRegistry.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 { IAsyncDisposable, IAsyncDisposableRegistry, IDisposable } from './types'; - -// List of disposables that need to run a promise. -@injectable() -export class AsyncDisposableRegistry implements IAsyncDisposableRegistry { - private list: (IDisposable | IAsyncDisposable)[] = []; - - public async dispose(): Promise { - const promises = this.list.map(l => l.dispose()); - await Promise.all(promises); - } - - public push(disposable?: IDisposable | IAsyncDisposable) { - 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(work : (token?: CancellationToken) => Promise, token?: CancellationToken) : Promise { - if (token) { - // Use a deferred promise. Resolves when the work finishes - const deferred = createDeferred(); - - // Cancel the deferred promise when the cancellation happens - token.onCancellationRequested(() => { - if (!deferred.completed) { - deferred.reject(new CancellationError()); - } - }); - - // Might already be canceled - if (token.isCancellationRequested) { - // Just start out as rejected - deferred.reject(new CancellationError()); - } else { - // Not canceled yet. When the work finishes - // either resolve our promise or cancel. - work(token) - .then((v) => { - if (!deferred.completed) { - deferred.resolve(v); - } - }) - .catch((e) => { - if (!deferred.completed) { - deferred.reject(e); - } - }); - } - - return deferred.promise; - } else { - // No actual token, just do the original work. - return work(); - } - } - - /** - * isCanceled returns a boolean indicating if the cancel token has been canceled. - * @param cancelToken - */ - export function isCanceled(cancelToken?: CancellationToken) : boolean { - return cancelToken ? cancelToken.isCancellationRequested : false; - } - - /** - * throws a CancellationError if the token is canceled. - * @param cancelToken - */ - export function throwIfCanceled(cancelToken?: CancellationToken) : void { - if (isCanceled(cancelToken)) { - throw new CancellationError(); - } - } - -} +// 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(options: { + defaultValue: T; + token?: CancellationToken; + cancelAction: 'reject' | 'resolve'; +}): Promise { + return new Promise((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(work: (token?: CancellationToken) => Promise, token?: CancellationToken): Promise { + if (token) { + // Use a deferred promise. Resolves when the work finishes + const deferred = createDeferred(); + + // Cancel the deferred promise when the cancellation happens + token.onCancellationRequested(() => { + if (!deferred.completed) { + deferred.reject(new CancellationError()); + } + }); + + // Might already be canceled + if (token.isCancellationRequested) { + // Just start out as rejected + deferred.reject(new CancellationError()); + } else { + // Not canceled yet. When the work finishes + // either resolve our promise or cancel. + work(token) + .then((v) => { + if (!deferred.completed) { + deferred.resolve(v); + } + }) + .catch((e) => { + if (!deferred.completed) { + deferred.reject(e); + } + }); + } + + return deferred.promise; + } else { + // No actual token, just do the original work. + return work(); + } + } + + /** + * isCanceled returns a boolean indicating if the cancel token has been canceled. + */ + 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 0f4bfe150405..91c06d9331fd 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -1,476 +1,563 @@ -'use strict'; - -import * as child_process from 'child_process'; -import * as path from 'path'; -import { ConfigurationChangeEvent, ConfigurationTarget, DiagnosticSeverity, Disposable, Event, EventEmitter, Uri, WorkspaceConfiguration } from 'vscode'; -import '../common/extensions'; -import { IInterpreterAutoSeletionProxyService } from '../interpreter/autoSelection/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } 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, - ITestingSettings, - IWorkspaceSymbolSettings -} from './types'; -import { debounceSync } from './utils/decorators'; -import { SystemVariables } from './variables/systemVariables'; - -// tslint:disable:no-require-imports no-var-requires -const untildify = require('untildify'); - -// tslint:disable-next-line:completed-docs -export class PythonSettings implements IPythonSettings { - private static pythonSettings: Map = new Map(); - public downloadLanguageServer = true; - public jediEnabled = true; - public jediPath = ''; - public jediMemoryLimit = 1024; - public envFile = ''; - public venvPath = ''; - public venvFolders: string[] = []; - public condaPath = ''; - public pipenvPath = ''; - public poetryPath = ''; - public devOptions: string[] = []; - public linting!: ILintingSettings; - public formatting!: IFormattingSettings; - public autoComplete!: IAutoCompleteSettings; - public testing!: ITestingSettings; - public terminal!: ITerminalSettings; - public sortImports!: ISortImportSettings; - public workspaceSymbols!: IWorkspaceSymbolSettings; - public disableInstallationChecks = false; - public globalModuleInstallation = false; - public analysis!: IAnalysisSettings; - public autoUpdateLanguageServer: boolean = true; - public datascience!: IDataScienceSettings; - - protected readonly changed = new EventEmitter(); - private workspaceRoot: Uri; - private disposables: Disposable[] = []; - // tslint:disable-next-line:variable-name - private _pythonPath = ''; - private readonly workspace: IWorkspaceService; - public get onDidChange(): Event { - return this.changed.event; - } - - constructor(workspaceFolder: Uri | undefined, private readonly interpreterAutoSelectionService: IInterpreterAutoSeletionProxyService, - workspace?: IWorkspaceService) { - 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(EventName.COMPLETION_ADD_BRACKETS, undefined, { enabled: settings.autoComplete ? settings.autoComplete.addBrackets : false }); - sendTelemetryEvent(EventName.FORMAT_ON_TYPE, undefined, { enabled: formatOnType }); - } - // tslint:disable-next-line:no-non-null-assertion - return PythonSettings.pythonSettings.get(workspaceFolderKey)!; - } - - // tslint:disable-next-line:type-literal-delimiter - public static getSettingsUriAndTarget(resource: Uri | undefined, workspace?: IWorkspaceService): { uri: Uri | undefined, target: ConfigurationTarget } { - workspace = workspace || new WorkspaceService(); - const workspaceFolder = resource ? workspace.getWorkspaceFolder(resource) : undefined; - let workspaceFolderUri: Uri | undefined = workspaceFolder ? workspaceFolder.uri : undefined; - - if (!workspaceFolderUri && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - workspaceFolderUri = workspace.workspaceFolders[0].uri; - } - - const target = workspaceFolderUri ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Global; - return { uri: workspaceFolderUri, target }; - } - - // tslint:disable-next-line:function-name - public static dispose() { - if (!isTestExecution()) { - throw new Error('Dispose can only be called from unit tests'); - } - // tslint:disable-next-line:no-void-expression - PythonSettings.pythonSettings.forEach(item => item && item.dispose()); - PythonSettings.pythonSettings.clear(); - } - public dispose() { - // tslint:disable-next-line:no-unsafe-any - this.disposables.forEach(disposable => disposable && disposable.dispose()); - this.disposables = []; - } - // tslint:disable-next-line:cyclomatic-complexity max-func-body-length - protected update(pythonSettings: WorkspaceConfiguration) { - const workspaceRoot = this.workspaceRoot.fsPath; - const systemVariables: SystemVariables = new SystemVariables(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('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('venvPath'))!; - this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; - const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; - this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; - const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; - this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; - const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; - this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; - - this.downloadLanguageServer = systemVariables.resolveAny(pythonSettings.get('downloadLanguageServer', true))!; - this.jediEnabled = systemVariables.resolveAny(pythonSettings.get('jediEnabled', true))!; - this.autoUpdateLanguageServer = systemVariables.resolveAny(pythonSettings.get('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('jediPath'))!; - if (typeof this.jediPath === 'string' && this.jediPath.length > 0) { - this.jediPath = getAbsolutePath(systemVariables.resolveAny(this.jediPath), workspaceRoot); - } else { - this.jediPath = ''; - } - this.jediMemoryLimit = pythonSettings.get('jediMemoryLimit')!; - } - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - this.envFile = systemVariables.resolveAny(pythonSettings.get('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('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('linting'))!; - if (this.linting) { - Object.assign(this.linting, lintingSettings); - } else { - this.linting = lintingSettings; - } - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const analysisSettings = systemVariables.resolveAny(pythonSettings.get('analysis'))!; - if (this.analysis) { - Object.assign(this.analysis, analysisSettings); - } else { - this.analysis = analysisSettings; - } - - this.disableInstallationChecks = pythonSettings.get('disableInstallationCheck') === true; - this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const sortImportSettings = systemVariables.resolveAny(pythonSettings.get('sortImports'))!; - if (this.sortImports) { - Object.assign(this.sortImports, sortImportSettings); - } else { - this.sortImports = sortImportSettings; - } - // Support for travis. - this.sortImports = this.sortImports ? this.sortImports : { path: '', args: [] }; - // Support for travis. - this.linting = this.linting ? this.linting : { - enabled: false, - ignorePatterns: [], - flake8Args: [], flake8Enabled: false, flake8Path: 'flake', - lintOnSave: false, maxNumberOfProblems: 100, - mypyArgs: [], mypyEnabled: false, mypyPath: 'mypy', - banditArgs: [], banditEnabled: false, banditPath: 'bandit', - 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('formatting'))!; - if (this.formatting) { - Object.assign(this.formatting, formattingSettings); - } else { - this.formatting = formattingSettings; - } - // Support for travis. - this.formatting = this.formatting ? this.formatting : { - autopep8Args: [], autopep8Path: 'autopep8', - provider: 'autopep8', - blackArgs: [], blackPath: 'black', - yapfArgs: [], yapfPath: 'yapf' - }; - this.formatting.autopep8Path = getAbsolutePath(systemVariables.resolveAny(this.formatting.autopep8Path), workspaceRoot); - this.formatting.yapfPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.yapfPath), workspaceRoot); - this.formatting.blackPath = getAbsolutePath(systemVariables.resolveAny(this.formatting.blackPath), workspaceRoot); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const autoCompleteSettings = systemVariables.resolveAny(pythonSettings.get('autoComplete'))!; - if (this.autoComplete) { - Object.assign(this.autoComplete, autoCompleteSettings); - } else { - this.autoComplete = autoCompleteSettings; - } - // Support for travis. - this.autoComplete = this.autoComplete ? this.autoComplete : { - extraPaths: [], - addBrackets: false, - showAdvancedMembers: false, - typeshedPaths: [] - }; - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const workspaceSymbolsSettings = systemVariables.resolveAny(pythonSettings.get('workspaceSymbols'))!; - if (this.workspaceSymbols) { - Object.assign(this.workspaceSymbols, workspaceSymbolsSettings); - } else { - this.workspaceSymbols = workspaceSymbolsSettings; - } - // Support for travis. - this.workspaceSymbols = this.workspaceSymbols ? this.workspaceSymbols : { - ctagsPath: 'ctags', - enabled: true, - exclusionPatterns: [], - rebuildOnFileSave: true, - rebuildOnStart: true, - tagFilePath: path.join(workspaceRoot, 'tags') - }; - this.workspaceSymbols.tagFilePath = getAbsolutePath(systemVariables.resolveAny(this.workspaceSymbols.tagFilePath), workspaceRoot); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; - if (this.testing) { - Object.assign(this.testing, testSettings); - } else { - this.testing = testSettings; - if (isTestExecution() && !this.testing) { - // tslint:disable-next-line:prefer-type-cast - // tslint:disable-next-line:no-object-literal-type-assertion - this.testing = { - nosetestArgs: [], pytestArgs: [], unittestArgs: [], - promptToConfigure: true, debugPort: 3000, - nosetestsEnabled: false, pytestEnabled: false, unittestEnabled: false, - nosetestPath: 'nosetests', pytestPath: 'pytest', autoTestDiscoverOnSaveEnabled: true - } as ITestingSettings; - } - } - - // Support for travis. - this.testing = this.testing ? this.testing : { - promptToConfigure: true, - debugPort: 3000, - nosetestArgs: [], nosetestPath: 'nosetest', nosetestsEnabled: false, - pytestArgs: [], pytestEnabled: false, pytestPath: 'pytest', - unittestArgs: [], unittestEnabled: false, autoTestDiscoverOnSaveEnabled: true - }; - this.testing.pytestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.pytestPath), workspaceRoot); - this.testing.nosetestPath = getAbsolutePath(systemVariables.resolveAny(this.testing.nosetestPath), workspaceRoot); - if (this.testing.cwd) { - this.testing.cwd = getAbsolutePath(systemVariables.resolveAny(this.testing.cwd), workspaceRoot); - } - - // Resolve any variables found in the test arguments. - this.testing.nosetestArgs = this.testing.nosetestArgs.map(arg => systemVariables.resolveAny(arg)); - this.testing.pytestArgs = this.testing.pytestArgs.map(arg => systemVariables.resolveAny(arg)); - this.testing.unittestArgs = this.testing.unittestArgs.map(arg => systemVariables.resolveAny(arg)); - - // tslint:disable-next-line:no-backbone-get-set-outside-model no-non-null-assertion - const terminalSettings = systemVariables.resolveAny(pythonSettings.get('terminal'))!; - if (this.terminal) { - Object.assign(this.terminal, terminalSettings); - } else { - this.terminal = terminalSettings; - if (isTestExecution() && !this.terminal) { - // tslint:disable-next-line:prefer-type-cast - // tslint:disable-next-line:no-object-literal-type-assertion - this.terminal = {} as ITerminalSettings; - } - } - // Support for travis. - this.terminal = this.terminal ? this.terminal : { - executeInFileDir: true, - launchArgs: [], - activateEnvironment: true - }; - - const dataScienceSettings = systemVariables.resolveAny(pythonSettings.get('dataScience'))!; - if (this.datascience) { - Object.assign(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 onWorkspaceFoldersChanged() { - //If an activated workspace folder was removed, delete its key - const workspaceKeys = this.workspace.workspaceFolders!.map(workspaceFolder => workspaceFolder.uri.fsPath); - const activatedWkspcKeys = Array.from(PythonSettings.pythonSettings.keys()); - const activatedWkspcFoldersRemoved = activatedWkspcKeys.filter(item => workspaceKeys.indexOf(item) < 0); - if (activatedWkspcFoldersRemoved.length > 0) { - for (const folder of activatedWkspcFoldersRemoved) { - PythonSettings.pythonSettings.delete(folder); - } - } - } - protected initialize(): void { - const onDidChange = () => { - const currentConfig = this.workspace.getConfiguration('python', this.workspaceRoot); - this.update(currentConfig); - - // If workspace config changes, then we could have a cascading effect of on change events. - // Let's defer the change notification. - this.debounceChangeNotification(); - }; - this.disposables.push(this.workspace.onDidChangeWorkspaceFolders(this.onWorkspaceFoldersChanged, this)); - this.disposables.push(this.interpreterAutoSelectionService.onDidChangeAutoSelectedInterpreter(onDidChange.bind(this))); - 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); - } - } - @debounceSync(1) - protected debounceChangeNotification() { - this.changed.fire(); - } -} - -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 { + return this.changed.event; + } + + // eslint-disable-next-line class-methods-use-this + public static onConfigChange(): Event { + return PythonSettings.configChanged.event; + } + + public get pythonPath(): string { + return this._pythonPath; + } + + public set pythonPath(value: string) { + if (this._pythonPath === value) { + return; + } + // 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 = new Map(); + + 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(); + + private static readonly configChanged = new EventEmitter(); + + 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('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('venvPath'))!; + this.venvFolders = systemVariables.resolveAny(pythonSettings.get('venvFolders'))!; + const activeStateToolPath = systemVariables.resolveAny(pythonSettings.get('activeStateToolPath'))!; + this.activeStateToolPath = + activeStateToolPath && activeStateToolPath.length > 0 + ? getAbsolutePath(activeStateToolPath, workspaceRoot) + : activeStateToolPath; + const condaPath = systemVariables.resolveAny(pythonSettings.get('condaPath'))!; + this.condaPath = condaPath && condaPath.length > 0 ? getAbsolutePath(condaPath, workspaceRoot) : condaPath; + const pipenvPath = systemVariables.resolveAny(pythonSettings.get('pipenvPath'))!; + this.pipenvPath = pipenvPath && pipenvPath.length > 0 ? getAbsolutePath(pipenvPath, workspaceRoot) : pipenvPath; + const poetryPath = systemVariables.resolveAny(pythonSettings.get('poetryPath'))!; + this.poetryPath = poetryPath && poetryPath.length > 0 ? getAbsolutePath(poetryPath, workspaceRoot) : poetryPath; + const pixiToolPath = systemVariables.resolveAny(pythonSettings.get('pixiToolPath'))!; + this.pixiToolPath = + pixiToolPath && pixiToolPath.length > 0 ? getAbsolutePath(pixiToolPath, workspaceRoot) : pixiToolPath; + + this.interpreter = pythonSettings.get('interpreter') ?? { + infoVisibility: 'onPythonRelated', + }; + // Get as a string and verify; don't just accept. + let userLS = pythonSettings.get('languageServer'); + userLS = systemVariables.resolveAny(userLS); + + // Validate the user's input; if invalid, set it to the default. + if ( + !userLS || + userLS === 'Default' || + userLS === 'Microsoft' || + !Object.values(LanguageServerType).includes(userLS as LanguageServerType) + ) { + if ( + this.extensions.getExtension(PYREFLY_EXTENSION_ID) && + pythonSettings.get('pyrefly.disableLanguageServices') !== true + ) { + this.languageServer = LanguageServerType.None; + } else { + this.languageServer = this.defaultLS?.defaultLSType ?? LanguageServerType.None; + } + this.languageServerIsDefault = true; + } else if (userLS === 'JediLSP') { + // Switch JediLSP option to Jedi. + this.languageServer = LanguageServerType.Jedi; + this.languageServerIsDefault = false; + } else { + this.languageServer = userLS as LanguageServerType; + this.languageServerIsDefault = false; + } + + const autoCompleteSettings = systemVariables.resolveAny( + pythonSettings.get('autoComplete'), + )!; + if (this.autoComplete) { + Object.assign(this.autoComplete, autoCompleteSettings); + } else { + this.autoComplete = autoCompleteSettings; + } + + const envFileSetting = pythonSettings.get('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('devOptions'))!; + this.devOptions = Array.isArray(this.devOptions) ? this.devOptions : []; + + this.globalModuleInstallation = pythonSettings.get('globalModuleInstallation') === true; + + const testSettings = systemVariables.resolveAny(pythonSettings.get('testing'))!; + if (this.testing) { + Object.assign(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('terminal'))!; + if (this.terminal) { + Object.assign(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('REPL')!; + const experiments = pythonSettings.get('experiments')!; + if (this.experiments) { + Object.assign(this.experiments, experiments); + } else { + this.experiments = experiments; + } + // Note we directly access experiment settings using workspace service in ExperimentService class. + // Any changes here specific to these settings should propogate their as well. + this.experiments = this.experiments + ? this.experiments + : { + enabled: true, + optInto: [], + optOutFrom: [], + }; + } + + // 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 d92a0e2a5ca4..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); } + + // eslint-disable-next-line class-methods-use-this + public get onDidChange(): Event { + return PythonSettings.onConfigChange(); + } + public getSettings(resource?: Uri): IPythonSettings { - const InterpreterAutoSelectionService = this.serviceContainer.get(IInterpreterAutoSeletionProxyService); - return PythonSettings.getInstance(resource, InterpreterAutoSelectionService, this.workspaceService); + const InterpreterAutoSelectionService = this.serviceContainer.get( + IInterpreterAutoSelectionService, + ); + const interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + const defaultLS = this.serviceContainer.tryGet(IDefaultLanguageServer); + const extensions = this.serviceContainer.get(IExtensions); + return PythonSettings.getInstance( + resource, + InterpreterAutoSelectionService, + this.workspaceService, + interpreterPathService, + defaultLS, + extensions, + ); } - public async updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise { + public async updateSectionSetting( + section: string, + setting: string, + value?: unknown, + resource?: Uri, + configTarget?: ConfigurationTarget, + ): Promise { 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 ? settingsInfo.uri : null); + 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 { + public async updateSetting( + setting: string, + value?: unknown, + resource?: Uri, + configTarget?: ConfigurationTarget, + ): Promise { return this.updateSectionSetting('python', setting, value, resource, configTarget); } + // eslint-disable-next-line class-methods-use-this public isTestExecution(): boolean { return process.env.VSC_PYTHON_CI_TEST === '1'; } - private async verifySetting(configSection: WorkspaceConfiguration, target: ConfigurationTarget, settingName: string, value?: {}): Promise { - if (this.isTestExecution()) { + private async verifySetting( + configSection: WorkspaceConfiguration, + target: ConfigurationTarget, + settingName: string, + value?: unknown, + ): Promise { + 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 => 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 79a9983f61e0..15fd037a3d9f 100644 --- a/src/client/common/constants.ts +++ b/src/client/common/constants.ts @@ -1,90 +1,107 @@ -import { DocumentFilter } from 'vscode'; - +/* eslint-disable camelcase */ +/* eslint-disable @typescript-eslint/no-namespace */ export const PYTHON_LANGUAGE = 'python'; -export const PYTHON: DocumentFilter[] = [ +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_ALLFILES = [ - { 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 Exec_In_REPL_Enter = 'python.execInREPLEnter'; + export const Exec_In_IW_Enter = 'python.execInInteractiveWindowEnter'; + export const Exec_Selection_In_Terminal = 'python.execSelectionInTerminal'; + export const GetSelectedInterpreterPath = 'python.interpreterPath'; + export const InstallJupyter = 'python.installJupyter'; + export const InstallPython = 'python.installPython'; + export const InstallPythonOnLinux = 'python.installPythonOnLinux'; + export const InstallPythonOnMac = 'python.installPythonOnMac'; + export const PickLocalProcess = 'python.pickLocalProcess'; + export const ReportIssue = 'python.reportIssue'; + export const Set_Interpreter = 'python.setInterpreter'; + export const Set_ShebangInterpreter = 'python.setShebangInterpreter'; + export const Start_REPL = 'python.startREPL'; + export const Start_Native_REPL = 'python.startNativeREPL'; export const Tests_Configure = 'python.configureTests'; - export const Tests_Discover = 'python.discoverTests'; - export const Tests_Discovering = 'python.discoveringTests'; - export const Tests_Run_Failed = 'python.runFailedTests'; - export const Sort_Imports = 'python.sortImports'; - export const Tests_Run = 'python.runtests'; - export const Tests_Debug = 'python.debugtests'; - export const Tests_Ask_To_Stop_Test = 'python.askToStopTests'; - export const Tests_Ask_To_Stop_Discovery = 'python.askToStopTestDiscovery'; - export const Tests_Stop = 'python.stopTests'; - export const Test_Reveal_Test_Item = 'python.revealTestItem'; + export const Tests_CopilotSetup = 'python.copilotSetupTests'; + export const TriggerEnvironmentSelection = 'python.triggerEnvSelection'; export const ViewOutput = 'python.viewOutput'; - export const Tests_ViewOutput = 'python.viewTestOutput'; - export const Tests_Select_And_Run_Method = 'python.selectAndRunTestMethod'; - export const Tests_Select_And_Debug_Method = 'python.selectAndDebugTestMethod'; - export const Tests_Select_And_Run_File = 'python.selectAndRunTestFile'; - export const Tests_Run_Current_File = 'python.runCurrentTestFile'; - export const Refactor_Extract_Variable = 'python.refactorExtractVariable'; - export const Refactor_Extract_Method = 'python.refactorExtractMethod'; - export const Update_SparkLibrary = 'python.updateSparkLibrary'; - export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols'; - export const Start_REPL = 'python.startREPL'; - export const Create_Terminal = 'python.createTerminal'; - export const Set_Linter = 'python.setLinter'; - export const Enable_Linter = 'python.enableLinting'; - export const Run_Linter = 'python.runLinting'; - export const Enable_SourceMap_Support = 'python.enableSourceMapSupport'; - export const navigateToTestFunction = 'navigateToTestFunction'; - export const navigateToTestSuite = 'navigateToTestSuite'; - export const navigateToTestFile = 'navigateToTestFile'; - export const openTestNodeInEditor = 'python.openTestNodeInEditor'; - export const runTestNode = 'python.runTestNode'; - export const debugTestNode = 'python.debugTestNode'; } + +// 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(); @@ -92,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 bda24257da0b..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 { + 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 { if (this.lastValue === value) { diff --git a/src/client/common/crypto.ts b/src/client/common/crypto.ts deleted file mode 100644 index c25ea6cdc089..000000000000 --- a/src/client/common/crypto.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -// tslint:disable: no-any - -import { createHash, HexBase64Latin1Encoding } from 'crypto'; -import { injectable } from 'inversify'; -import { ICryptoUtils, IHashFormat } from './types'; - -/** - * Implements tools related to cryptography - */ -@injectable() -export class CryptoUtils implements ICryptoUtils { - public createHash(data: string, encoding: HexBase64Latin1Encoding, hashFormat: E): IHashFormat[E] { - const hash = createHash('sha512').update(data).digest(encoding); - return hashFormat === 'number' ? parseInt(hash, undefined) : hash as any; - } -} 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(); - 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, DotNetCompatibilityService); - serviceManager.addSingleton(IOSDotNetCompatibilityService, MacDotNetCompatibilityService, OSType.OSX); - serviceManager.addSingleton(IOSDotNetCompatibilityService, WindowsDotNetCompatibilityService, OSType.Windows); - serviceManager.addSingleton(IOSDotNetCompatibilityService, LinuxDotNetCompatibilityService, OSType.Linux); - serviceManager.addSingleton(IOSDotNetCompatibilityService, UnknownOSDotNetCompatibilityService, OSType.Unknown); - -} diff --git a/src/client/common/dotnet/services/linuxCompatibilityService.ts b/src/client/common/dotnet/services/linuxCompatibilityService.ts deleted file mode 100644 index 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; -} -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 f777de96e246..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 { - return new Promise((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.} Array of Patch objects. - * @throws {!Error} If invalid input. - */ -function patch_fromText(textline: string): Patch[] { - const patches: Patch[] = []; - if (!textline) { - return patches; - } - // Start Modification by Don Jayamanne 24/06/2016 Support for CRLF - const text = textline.split(/[\r\n]/); - // End Modification - let textPointer = 0; - const patchHeader = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@$/; - while (textPointer < text.length) { - const m = text[textPointer].match(patchHeader); - if (!m) { - throw new Error(`Invalid patch string: ${text[textPointer]}`); - } - // tslint:disable-next-line:no-any - const patch = new (diff_match_patch).patch_obj(); - patches.push(patch); - patch.start1 = parseInt(m[1], 10); - if (m[2] === '') { - patch.start1 -= 1; - patch.length1 = 1; - } else if (m[2] === '0') { - patch.length1 = 0; - } else { - patch.start1 -= 1; - patch.length1 = parseInt(m[2], 10); - } - - patch.start2 = parseInt(m[3], 10); - if (m[4] === '') { - patch.start2 -= 1; - patch.length2 = 1; - } else if (m[4] === '0') { - patch.length2 = 0; - } else { - patch.start2 -= 1; - patch.length2 = parseInt(m[4], 10); - } - textPointer += 1; - // tslint:disable-next-line:no-require-imports - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - - while (textPointer < text.length) { - const sign = text[textPointer].charAt(0); - let line: string; - try { - //var line = decodeURI(text[textPointer].substring(1)); - // For some reason the patch generated by python files don't encode any characters - // And this patch module (code from Google) is expecting the text to be encoded!! - // Temporary solution, disable decoding - // Issue #188 - line = text[textPointer].substring(1); - } catch (ex) { - // Malformed URI sequence. - throw new Error('Illegal escape in patch_fromText'); - } - if (sign === '-') { - // Deletion. - patch.diffs.push([dmp.DIFF_DELETE, line]); - } else if (sign === '+') { - // Insertion. - patch.diffs.push([dmp.DIFF_INSERT, line]); - } else if (sign === ' ') { - // Minor equality. - patch.diffs.push([dmp.DIFF_EQUAL, line]); - } else if (sign === '@') { - // Start of next patch. - break; - } else if (sign === '') { - // Blank line? Whatever. - } else { - // WTF? - throw new Error(`Invalid patch mode '${sign}' in: ${line}`); - } - textPointer += 1; - } - } - return patches; -} - -@injectable() -export class EditorUtils implements IEditorUtils { - public getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit { - const workspaceEdit = new WorkspaceEdit(); - if (patch.startsWith('---')) { - // Strip the first two lines - patch = patch.substring(patch.indexOf('@@')); - } - if (patch.length === 0) { - return workspaceEdit; - } - // Remove the text added by unified_diff - // # Work around missing newline (http://bugs.python.org/issue2142). - patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); - - // tslint:disable-next-line:no-require-imports - const dmp = require('diff-match-patch') as typeof import('diff-match-patch'); - const d = new dmp.diff_match_patch(); - const patches = patch_fromText.call(d, patch); - if (!Array.isArray(patches) || patches.length === 0) { - throw new Error('Unable to parse Patch string'); - } - - // Add line feeds and build the text edits - patches.forEach(p => { - p.diffs.forEach(diff => { - diff[1] += EOL; - }); - getTextEditsInternal(originalContents, p.diffs, p.start1).forEach(edit => { - switch (edit.action) { - case EditAction.Delete: - workspaceEdit.delete(uri, new Range(edit.start, edit.end)); - break; - case EditAction.Insert: - workspaceEdit.insert(uri, edit.start, edit.text); - break; - case EditAction.Replace: - workspaceEdit.replace(uri, new Range(edit.start, edit.end), edit.text); - break; - default: - break; - } - }); - }); - - return workspaceEdit; - } -} diff --git a/src/client/common/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 9c73fdfa4205..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 no-unnecessary-class export class ErrorUtils { public static outputHasModuleNotInstalledError(moduleName: string, content?: string): boolean { - return content && (content!.indexOf(`No module named ${moduleName}`) > 0 || content!.indexOf(`No module named '${moduleName}'`) > 0) ? true : false; + 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/experimentGroups.ts b/src/client/common/experimentGroups.ts deleted file mode 100644 index 3df5140f537c..000000000000 --- a/src/client/common/experimentGroups.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const LSControl = 'LS - control'; -export const LSEnabled = 'LS - enabled'; - -// Experiment to check whether to always display the test explorer. -export enum AlwaysDisplayTestExplorerGroups { - control = 'AlwaysDisplayTestExplorer - control', - experiment = 'AlwaysDisplayTestExplorer - experiment' -} diff --git a/src/client/common/experiments.ts b/src/client/common/experiments.ts deleted file mode 100644 index 9fcc87c5525e..000000000000 --- a/src/client/common/experiments.ts +++ /dev/null @@ -1,208 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// Refer to A/B testing wiki for more details: https://en.wikipedia.org/wiki/A/B_testing - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { parse } from 'jsonc-parser'; -import * as path from 'path'; -import { IHttpClient } from '../common/types'; -import { isTelemetryDisabled, sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { IApplicationEnvironment, IWorkspaceService } from './application/types'; -import { EXTENSION_ROOT_DIR, STANDARD_OUTPUT_CHANNEL } from './constants'; -import { traceDecorators, traceError } from './logger'; -import { IFileSystem } from './platform/types'; -import { ABExperiments, ICryptoUtils, IExperimentsManager, IOutputChannel, IPersistentState, IPersistentStateFactory } from './types'; -import { swallowExceptions } from './utils/decorators'; -import { Experiments } from './utils/localize'; - -const EXPIRY_DURATION_MS = 30 * 60 * 1000; -export const isDownloadedStorageValidKey = 'IS_EXPERIMENTS_STORAGE_VALID_KEY'; -export const experimentStorageKey = 'EXPERIMENT_STORAGE_KEY'; -export const downloadedExperimentStorageKey = 'DOWNLOADED_EXPERIMENTS_STORAGE_KEY'; -/** - * Local experiments config file. We have this to ensure that experiments are used in the first session itself, - * as about 40% of the users never come back for the second session. - */ -const configFile = path.join(EXTENSION_ROOT_DIR, 'experiments.json'); -export const configUri = 'https://raw.githubusercontent.com/microsoft/vscode-python/master/experiments.json'; - -/** - * Manages and stores experiments, implements the AB testing functionality - */ -@injectable() -export class ExperimentsManager implements IExperimentsManager { - /** - * Keeps track of the list of experiments user is in - */ - public userExperiments: ABExperiments = []; - /** - * Keeps track of the downloaded experiments in the previous sessions - */ - private experimentStorage: IPersistentState; - /** - * Keeps track of the downloaded experiments in the current session, to be used in the next startup - * Note experiments downloaded in the current session has to be distinguished - * from the experiments download in the previous session (experimentsStorage contains that), reason being the following - * - * THE REASON TO WHY WE NEED TWO STATE STORES USED TO STORE EXPERIMENTS: - * We do not intend to change experiments mid-session. To implement this, we should make sure that we do not replace - * the experiments used in the current session by the newly downloaded experiments. That's why we have a separate - * storage(downloadedExperimentsStorage) to store experiments downloaded in the current session. - * Function updateExperimentStorage() makes sure these are used in the next session. - */ - private downloadedExperimentsStorage: IPersistentState; - /** - * Keeps track if the storage needs updating or not. - * Note this has to be separate from the actual storage as - * download storages by itself should not have an Expiry (so that it can be used in the next session even when download fails in the current session) - */ - private isDownloadedStorageValid: IPersistentState; - private activatedOnce: boolean = false; - constructor( - @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IHttpClient) private readonly httpClient: IHttpClient, - @inject(ICryptoUtils) private readonly crypto: ICryptoUtils, - @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, - @inject(IOutputChannel) @named(STANDARD_OUTPUT_CHANNEL) private readonly output: IOutputChannel, - @inject(IFileSystem) private readonly fs: IFileSystem - ) { - this.isDownloadedStorageValid = this.persistentStateFactory.createGlobalPersistentState(isDownloadedStorageValidKey, false, EXPIRY_DURATION_MS); - this.experimentStorage = this.persistentStateFactory.createGlobalPersistentState(experimentStorageKey, undefined); - this.downloadedExperimentsStorage = this.persistentStateFactory.createGlobalPersistentState(downloadedExperimentStorageKey, undefined); - } - - @swallowExceptions('Failed to activate experiments') - public async activate(): Promise { - if (this.activatedOnce || isTelemetryDisabled(this.workspaceService)) { - return; - } - this.activatedOnce = true; - await this.updateExperimentStorage(); - this.populateUserExperiments(); - for (const exp of this.userExperiments || []) { - // We need to know whether an experiment influences the logs we observe in github issues, so log the experiment group - this.output.appendLine(Experiments.inGroup().format(exp.name)); - } - this.initializeInBackground().ignoreErrors(); - } - - @traceDecorators.error('Failed to identify if user is in experiment') - public inExperiment(experimentName: string): boolean { - this.sendTelemetryIfInExperiment(experimentName); - return this.userExperiments.find(exp => exp.name === experimentName) ? true : false; - } - - /** - * Populates list of experiments user is in - */ - @traceDecorators.error('Failed to populate user experiments') - public populateUserExperiments(): void { - if (Array.isArray(this.experimentStorage.value)) { - for (const experiment of this.experimentStorage.value) { - try { - if (this.isUserInRange(experiment.min, experiment.max, experiment.salt)) { - this.userExperiments.push(experiment); - } - } catch (ex) { - traceError(`Failed to populate experiment list for experiment '${experiment.name}'`, ex); - } - } - } - } - - @traceDecorators.error('Failed to send telemetry when user is in experiment') - public sendTelemetryIfInExperiment(experimentName: string): void { - if (this.userExperiments.find(exp => exp.name === experimentName)) { - sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS, undefined, { expName: experimentName }); - } - } - - /** - * Downloads experiments and updates storage given previously downloaded experiments are no longer valid - */ - @traceDecorators.error('Failed to initialize experiments') - public async initializeInBackground() { - if (this.isDownloadedStorageValid.value) { - return; - } - const downloadedExperiments = await this.httpClient.getJSON(configUri, false); - if (!this.areExperimentsValid(downloadedExperiments)) { - return; - } - await this.downloadedExperimentsStorage.updateValue(downloadedExperiments); - await this.isDownloadedStorageValid.updateValue(true); - } - - /** - * Checks if user falls between the range of the experiment - * @param min The lower limit - * @param max The upper limit - * @param salt The experiment salt value - */ - public isUserInRange(min: number, max: number, salt: string): boolean { - if (typeof (this.appEnvironment.machineId) !== 'string') { - throw new Error('Machine ID should be a string'); - } - const hash = this.crypto.createHash(`${this.appEnvironment.machineId}+${salt}`, 'hex', 'number'); - return hash % 100 >= min && hash % 100 < max; - } - - /** - * Updates experiment storage using local data if available. - * Local data could be: - * * Experiments downloaded in the last session - * - The function makes sure these are used in the current session - * * A default experiments file shipped with the extension - * - Note this file is only used when experiment storage is empty, which is usually the case the first time the extension loads. - * - We have this local file to ensure that experiments are used in the first session itself, - * as about 40% of the users never come back for the second session. - */ - @swallowExceptions('Failed to update experiment storage') - public async updateExperimentStorage(): Promise { - // Step 1. Update experiment storage using downloaded experiments in the last session if any - if (Array.isArray(this.downloadedExperimentsStorage.value)) { - await this.experimentStorage.updateValue(this.downloadedExperimentsStorage.value); - await this.downloadedExperimentsStorage.updateValue(undefined); - return; - } - - // Step 2. Update experiment storage using local experiments file if available - if (!this.experimentStorage.value && (await this.fs.fileExists(configFile))) { - const content = await this.fs.readFile(configFile); - try { - const experiments = parse(content, [], { allowTrailingComma: true, disallowComments: false }); - if (!this.areExperimentsValid(experiments)) { - throw new Error('Parsed experiments are not valid'); - } - await this.experimentStorage.updateValue(experiments); - } catch (ex) { - traceError('Failed to parse experiments configuration file to update storage', ex); - return; - } - } - } - - /** - * Checks that experiments are not invalid or incomplete - * @param experiments Local or downloaded experiments - * @returns `true` if type of experiments equals `ABExperiments` type, `false` otherwise - */ - public areExperimentsValid(experiments: ABExperiments): boolean { - if (!Array.isArray(experiments)) { - traceError('Experiments are not of array type'); - return false; - } - for (const exp of experiments) { - if (exp.name === undefined || exp.salt === undefined || exp.min === undefined || exp.max === undefined) { - traceError('Experiments are missing fields from ABExperiments type'); - return false; - } - } - return true; - } -} 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('experiments.optInto') || []; + const optOutFrom = settings.get('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('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 { + 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 { + 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(experiment: string): Promise { + if (!this.experimentationService || this._optOutFrom.includes('All') || this._optOutFrom.includes(experiment)) { + return undefined; + } + + return this.experimentationService.getTreatmentVariable(EXP_CONFIG_ID, experiment); + } + + private logExperiments() { + const telemetrySettings = this.workspaceService.getConfiguration('telemetry'); + let experimentsDisabled = false; + if (telemetrySettings && telemetrySettings.get('enableTelemetry') === false) { + traceLog('Telemetry is disabled'); + experimentsDisabled = true; + } + + if (telemetrySettings && telemetrySettings.get('telemetryLevel') === 'off') { + traceLog('Telemetry level is off'); + experimentsDisabled = true; + } + + if (experimentsDisabled) { + traceLog('Experiments are disabled, only manually opted experiments are active.'); + } + + if (this._optOutFrom.includes('All')) { + // We prioritize opt out first + traceLog(l10n.t("Experiment '{0}' is inactive", 'All')); + + // Since we are in the Opt Out all case, this means when checking for experiment we + // short circuit and return. So, printing out additional experiment info might cause + // confusion. So skip printing out any specific experiment details to the log. + return; + } + if (this._optInto.includes('All')) { + // Only if 'All' is not in optOut then check if it is in Opt In. + traceLog(l10n.t("Experiment '{0}' is active", 'All')); + + // Similar to the opt out case. If user is opting into to all experiments we short + // circuit the experiment checks. So, skip printing any additional details to the logs. + return; + } + + // Log experiments that users manually opt out, these are experiments which are added using the exp framework. + this._optOutFrom + .filter((exp) => exp !== 'All' && exp.toLowerCase().startsWith('python')) + .forEach((exp) => { + traceLog(l10n.t("Experiment '{0}' is inactive", exp)); + }); + + // Log experiments that users manually opt into, these are experiments which are added using the exp framework. + this._optInto + .filter((exp) => exp !== 'All' && exp.toLowerCase().startsWith('python')) + .forEach((exp) => { + traceLog(l10n.t("Experiment '{0}' is active", exp)); + }); + + if (!experimentsDisabled) { + // Log experiments that users are added to by the exp framework + this.experiments.value.features.forEach((exp) => { + // Filter out experiment groups that are not from the Python extension. + // Filter out experiment groups that are not already opted out or opted into. + if ( + exp.toLowerCase().startsWith('python') && + !this._optOutFrom.includes(exp) && + !this._optInto.includes(exp) + ) { + traceLog(l10n.t("Experiment '{0}' is active", exp)); + } + }); + } + } +} + +/** + * Read accepted experiment settings values from the extension's package.json. + * This function assumes that the `setting` argument is a string array that has a specific set of accepted values. + * + * Accessing the values is done via these keys: + * -> "contributes" -> "configuration" -> "properties" -> -> "items" -> "enum" + * + * @param setting The setting we want to read the values of. + * @param packageJson The content of `package.json`, as a JSON object. + * + * @returns An array containing all accepted values for the setting, or [] if there were none. + */ +function readEnumValues(setting: string, packageJson: Record): string[] { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const settingProperties = (packageJson.contributes as any).configuration.properties[setting]; + + if (settingProperties) { + return settingProperties.items.enum ?? []; + } + + return []; +} + +/** + * Send telemetry on experiments that have been manually opted into or opted-out from. + * The telemetry will only contain values that are present in the list of accepted values for these settings. + * + * @param optedIn The list of experiments opted into. + * @param optedOut The list of experiments opted out from. + * @param packageJson The content of `package.json`, as a JSON object. + */ +function sendOptInOptOutTelemetry(optedIn: string[], optedOut: string[], packageJson: Record): void { + const optedInEnumValues = readEnumValues('python.experiments.optInto', packageJson); + const optedOutEnumValues = readEnumValues('python.experiments.optOutFrom', packageJson); + + const sanitizedOptedIn = optedIn.filter((exp) => optedInEnumValues.includes(exp)); + const sanitizedOptedOut = optedOut.filter((exp) => optedOutEnumValues.includes(exp)); + + JSON.stringify(sanitizedOptedIn.sort()); + + sendTelemetryEvent(EventName.PYTHON_EXPERIMENTS_OPT_IN_OPT_OUT_SETTINGS, undefined, { + optedInto: JSON.stringify(sanitizedOptedIn.sort()), + optedOutFrom: JSON.stringify(sanitizedOptedOut.sort()), + }); +} diff --git a/src/client/common/experiments/telemetry.ts b/src/client/common/experiments/telemetry.ts 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): 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 72a09e5a49db..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,43 +26,30 @@ declare interface String { trimQuotes(): string; } -/** - * Split a string using the cr and lf characters and return them as an array. - * By default lines are trimmed and empty lines are removed. - * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. - */ -String.prototype.splitLines = function (this: string, splitOptions: { trim: boolean; removeEmptyEntries: boolean } = { removeEmptyEntries: true, trim: true }): string[] { - let lines = this.split(/\r?\n/g); - if (splitOptions && splitOptions.trim) { - lines = lines.map(line => line.trim()); - } - if (splitOptions && splitOptions.removeEmptyEntries) { - lines = lines.filter(line => line.length > 0); - } - return lines; -}; - /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. - * @param {String} value. */ -String.prototype.toCommandArgument = function (this: string): string { +String.prototype.toCommandArgumentForPythonExt = function (this: string): string { if (!this) { return this; } - return (this.indexOf(' ') >= 0 && !this.startsWith('"') && !this.endsWith('"')) ? `"${this}"` : this.toString(); + return (this.indexOf(' ') >= 0 || this.indexOf('&') >= 0 || this.indexOf('(') >= 0 || this.indexOf(')') >= 0) && + !this.startsWith('"') && + !this.endsWith('"') + ? `"${this}"` + : this.toString(); }; /** * Appropriately formats a a file path so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ -String.prototype.fileToCommandArgument = function (this: string): string { +String.prototype.fileToCommandArgumentForPythonExt = function (this: string): string { if (!this) { return this; } - return this.toCommandArgument().replace(/\\/g, '/'); + return this.toCommandArgumentForPythonExt().replace(/\\/g, '/'); }; /** @@ -89,25 +63,16 @@ String.prototype.trimQuotes = function (this: string): string { return this.replace(/(^['"])|(['"]$)/g, ''); }; -// tslint:disable-next-line:interface-name -declare interface Promise { - /** - * Catches task error and ignores them. - */ - ignoreErrors(): void; -} - /** * Explicitly tells that promise should be run asynchonously. */ Promise.prototype.ignoreErrors = function (this: Promise) { - // tslint:disable-next-line:no-empty - this.catch(() => { }); + return this.catch(() => {}); }; if (!String.prototype.format) { 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 bab17a045b8c..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 { - 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 = error; if (!isError) { return false; @@ -21,19 +21,6 @@ export function isNotInstalledError(error: Error): boolean { return errorObj.code === 'ENOENT' || errorObj.code === 127 || isModuleNoInstalledError; } -export function skipIfTest(isAsyncFunction: boolean) { - // tslint:disable-next-line:no-function-expression no-any - return function (_: Object, __: string, descriptor: TypedPropertyDescriptor) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - if (isTestExecution()) { - return isAsyncFunction ? Promise.resolve() : undefined; - } - // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any - return originalMethod.apply(this, args); - }; - - return descriptor; - }; +export function untildify(path: string): string { + return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`); } diff --git a/src/client/common/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 { + public async getInstallationChannel( + product: Product, + resource?: InterpreterUri, + ): Promise { 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); 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(options, { matchOnDescription: true, matchOnDetail: true, placeHolder }); + const selection = await appShell.showQuickPick(options, { + matchOnDescription: true, + matchOnDetail: true, + placeHolder, + }); return selection ? selection.installer : undefined; } - public async getInstallationChannels(resource?: Uri): Promise { + public async getInstallationChannels(resource?: InterpreterUri): Promise { const installers = this.serviceContainer.getAll(IModuleInstaller); const supportedInstallers: IModuleInstaller[] = []; if (installers.length === 0) { @@ -74,17 +84,19 @@ export class InstallationChannelManager implements IInstallationChannelManager { const appShell = this.serviceContainer.get(IApplicationShell); const search = 'Search for help'; let result: string | undefined; - if (interpreter.type === InterpreterType.Conda) { - result = await appShell.showErrorMessage('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); - 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} Whether conda is supported as a module installer or not. */ - public async isSupported(resource?: Uri): Promise { - if (this.isCondaAvailable === false) { + public async isSupported(resource?: InterpreterUri): Promise { + if (this._isCondaAvailable === false) { return false; } const condaLocator = this.serviceContainer.get(ICondaService); - this.isCondaAvailable = await condaLocator.isCondaAvailable(); - if (!this.isCondaAvailable) { + 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 { + protected async getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise { const condaService = this.serviceContainer.get(ICondaService); - const condaFile = await condaService.getCondaFile(); + // Installation using `conda.exe` sometimes fails with a HTTP error on Windows: + // https://github.com/conda/conda/issues/11399 + // Execute in a shell which uses a `conda.bat` file instead, using which installation works. + const useShell = true; + const condaFile = await condaService.getCondaFile(useShell); - const pythonPath = this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath; - const info = await condaService.getCondaEnvironment(pythonPath); - const args = ['install']; + const pythonPath = isResource(resource) + ? this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath + : getEnvPath(resource.path, resource.envPath).path ?? ''; + const condaLocatorService = this.serviceContainer.get(IComponentAdapter); + const info = await condaLocatorService.getCondaEnvironment(pythonPath); + const args = [flags & ModuleInstallFlags.upgrade ? 'update' : 'install']; + // Found that using conda-forge is best at packages like tensorboard & ipykernel which seem to get updated first on conda-forge + // https://github.com/microsoft/vscode-jupyter/issues/7787 & https://github.com/microsoft/vscode-python/issues/17628 + // Do this just for the datascience packages. + if ([Product.tensorboard].map(translateProductToModule).includes(moduleName)) { + args.push('-c', 'conda-forge'); + } if (info && info.name) { // If we have the name of the conda environment, then use that. args.push('--name'); - args.push(info.name!.toCommandArgument()); + args.push(info.name.toCommandArgumentForPythonExt()); } else if (info && info.path) { // Else provide the full path to the environment path. args.push('--prefix'); - args.push(info.path.fileToCommandArgument()); + args.push(info.path.fileToCommandArgumentForPythonExt()); + } + if (flags & ModuleInstallFlags.updateDependencies) { + args.push('--update-deps'); + } + if (flags & ModuleInstallFlags.reInstall) { + args.push('--force-reinstall'); } args.push(moduleName); + args.push('-y'); return { args, - execPath: condaFile + execPath: condaFile, + useShell, }; } /** - * Is anaconda the current interpreter? + * Is the provided interprter a conda environment */ - private async isCurrentEnvironmentACondaEnvironment(resource?: Uri): Promise { - const condaService = this.serviceContainer.get(ICondaService); - const pythonPath = this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath; + private async isCurrentEnvironmentACondaEnvironment(resource?: InterpreterUri): Promise { + const condaService = this.serviceContainer.get(IComponentAdapter); + const pythonPath = isResource(resource) + ? this.serviceContainer.get(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 eab4e592486b..9dacb623c606 100644 --- a/src/client/common/installer/moduleInstaller.ts +++ b/src/client/common/installer/moduleInstaller.ts @@ -1,113 +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 { traceError, traceLog } from '../../logging'; +import { EnvironmentType, ModuleInstallerType, virtualEnvTypes } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { ITerminalServiceFactory } from '../terminal/types'; -import { ExecutionInfo, IConfigurationService, IOutputChannel } from '../types'; -import { noop } from '../utils/misc'; +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 { - public abstract get displayName(): string - constructor(protected serviceContainer: IServiceContainer) { } - public async installModule(name: string, resource?: vscode.Uri): Promise { - sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { installer: this.displayName }); - const executionInfo = await this.getExecutionInfo(name, resource); - const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(resource); - - const executionInfoArgs = await this.processInstallArgs(executionInfo.args, resource); - if (executionInfo.moduleName) { - const configService = this.serviceContainer.get(IConfigurationService); - const settings = configService.getSettings(resource); - const args = ['-m', executionInfo.moduleName].concat(executionInfoArgs); - - const pythonPath = settings.pythonPath; - - const interpreterService = this.serviceContainer.get(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 { + 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); + const settings = configService.getSettings(uri); + + const interpreterService = this.serviceContainer.get(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); + 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); + 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; - protected abstract getExecutionInfo(moduleName: string, resource?: vscode.Uri): Promise; - private async processInstallArgs(args: string[], resource?: vscode.Uri): Promise { - 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); - 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 { - const filePath = `${directoryPath}${path.sep}___vscpTest___`; - return new Promise(resolve => { - fs.open(filePath, fs.constants.O_CREAT | fs.constants.O_RDWR, (error, fd) => { - if (!error) { - fs.close(fd, () => { - fs.unlink(filePath, noop); - }); - } - return resolve(!error); - }); - }); - } + public abstract isSupported(resource?: InterpreterUri): Promise; - 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(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + const outputChannel = this.serviceContainer.get(ILogOutputChannel); const command = `"${execPath.replace(/\\/g, '/')}" ${args.join(' ')}`; - outputChannel.appendLine(''); - outputChannel.appendLine(`[Elevated] ${command}`); - // tslint:disable-next-line:no-require-imports no-var-requires + traceLog(`[Elevated] ${command}`); + const sudo = require('sudo-prompt'); - sudo.exec(command, options, (error: string, stdout: string, stderr: string) => { + sudo.exec(command, options, async (error: string, stdout: string, stderr: string) => { if (error) { - vscode.window.showErrorMessage(error); + const shell = this.serviceContainer.get(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; + + private async processInstallArgs(args: string[], resource?: InterpreterUri): Promise { + const indexOfPylint = args.findIndex((arg) => arg.toUpperCase() === 'PYLINT'); + if (indexOfPylint === -1) { + return args; + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; + // If installing pylint on python 2.x, then use pylint~=1.9.0 + if (interpreter && interpreter.version && interpreter.version.major === 2) { + const newArgs = [...args]; + // This command could be sent to the terminal, hence '<' needs to be escaped for UNIX. + newArgs[indexOfPylint] = '"pylint<2.0.0"'; + return newArgs; + } + return args; + } + + private async executeCommand( + executeInTerminal: boolean, + resource: InterpreterUri | undefined, + command: string, + args: string[], + token: CancellationToken | undefined, + useShell: boolean | undefined, + ) { + const options: TerminalCreationOptions = {}; + if (isResource(resource)) { + options.resource = resource; + } else { + options.interpreter = resource; + } + if (executeInTerminal) { + const terminalService = this.serviceContainer + .get(ITerminalServiceFactory) + .getTerminalService(options); + + terminalService.sendCommand(command, args, token); + } else { + const processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); + const processService = await processServiceFactory.create(options.resource); + if (useShell) { + const argv = [command, ...args]; + // Concat these together to make a set of quoted strings + const quoted = argv.reduce( + (p, c) => + p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`, + '', + ); + await processService.shellExec(quoted); + } else { + await processService.exec(command, args); + } + } + } +} + +export function translateProductToModule(product: Product): string { + switch (product) { + case Product.pytest: + return 'pytest'; + case Product.unittest: + return 'unittest'; + case Product.tensorboard: + return 'tensorboard'; + case Product.torchProfilerInstallName: + return 'torch-tb-profiler'; + case Product.torchProfilerImportName: + return 'torch_tb_profiler'; + case Product.pip: + return 'pip'; + case Product.ensurepip: + return 'ensurepip'; + case Product.python: + return 'python'; + default: { + throw new Error(`Product ${product} cannot be installed as a Python Module.`); + } + } } diff --git a/src/client/common/installer/pipEnvInstaller.ts b/src/client/common/installer/pipEnvInstaller.ts index 1b7a1c415b3f..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,20 +33,38 @@ export class PipEnvInstaller extends ModuleInstaller implements IModuleInstaller constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { super(serviceContainer); - this.pipenv = this.serviceContainer.get(IInterpreterLocatorService, PIPENV_SERVICE); } - public async isSupported(resource?: Uri): Promise { - const interpreters = await this.pipenv.getInterpreters(resource); - return interpreters && interpreters.length > 0; - } - protected async getExecutionInfo(moduleName: string, _resource?: Uri): Promise { - const args = ['install', moduleName, '--dev']; - if (moduleName === 'black') { - args.push('--pre'); + public async isSupported(resource?: InterpreterUri): Promise { + if (isResource(resource)) { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + const workspaceFolder = resource + ? this.serviceContainer.get(IWorkspaceService).getWorkspaceFolder(resource) + : undefined; + if (!interpreter || !workspaceFolder || interpreter.envType !== EnvironmentType.Pipenv) { + return false; + } + // Install using `pipenv install` only if the active environment is related to the current folder. + return isPipenvEnvironmentRelatedToFolder(interpreter.path, workspaceFolder.uri.fsPath); + } else { + return resource.envType === EnvironmentType.Pipenv; } + } + protected async getExecutionInfo( + moduleName: string, + _resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise { + // In pipenv the only way to update/upgrade or re-install is update (apart from a complete uninstall and re-install). + const update = + flags & ModuleInstallFlags.reInstall || + flags & ModuleInstallFlags.updateDependencies || + flags & ModuleInstallFlags.upgrade; + const args = [update ? 'update' : 'install', moduleName, '--dev']; return { args: args, - execPath: pipenvName + execPath: pipenvName, }; } } diff --git a/src/client/common/installer/pipInstaller.ts b/src/client/common/installer/pipInstaller.ts index 066891d3f4b3..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); + 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 { + public async isSupported(resource?: InterpreterUri): Promise { + if ((await doesEnvironmentContainPython(this.serviceContainer, resource)) === false) { + return false; + } return this.isPipAvailable(resource); } - protected async getExecutionInfo(moduleName: string, _resource?: Uri): Promise { - const proxyArgs: string[] = []; + protected async getExecutionInfo( + moduleName: string, + resource?: InterpreterUri, + flags: ModuleInstallFlags = 0, + ): Promise { + if (moduleName === translateProductToModule(Product.pip)) { + const version = isResource(resource) + ? '' + : `${resource.version?.major || ''}.${resource.version?.minor || ''}.${resource.version?.patch || ''}`; + const envType = isResource(resource) ? undefined : resource.envType; + + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller: ModuleInstallerType.Pip, + productName: ProductNames.get(Product.pip), + version, + envType, + }); + + // If `ensurepip` is available, if not, then install pip using the script file. + const installer = this.serviceContainer.get(IInstaller); + if (await installer.isInstalled(Product.ensurepip, resource)) { + return { + args: [], + moduleName: 'ensurepip', + }; + } + + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller: ModuleInstallerType.Pip, + productName: ProductNames.get(Product.ensurepip), + version, + envType, + }); + + // Return script to install pip. + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = isResource(resource) + ? await interpreterService.getActiveInterpreter(resource) + : resource; + return { + execPath: interpreter ? interpreter.path : 'python', + args: [path.join(_SCRIPTS_DIR, 'get-pip.py')], + }; + } + + const args: string[] = []; const workspaceService = this.serviceContainer.get(IWorkspaceService); const proxy = workspaceService.getConfiguration('http').get('proxy', ''); if (proxy.length > 0) { - proxyArgs.push('--proxy'); - proxyArgs.push(proxy); + args.push('--proxy'); + args.push(proxy); + } + args.push(...['install', '-U']); + if (flags & ModuleInstallFlags.reInstall) { + args.push('--force-reinstall'); } return { - args: [...proxyArgs, 'install', '-U', moduleName], - moduleName: 'pip' + args: [...args, moduleName], + moduleName: 'pip', }; } - private isPipAvailable(resource?: Uri): Promise { + private isPipAvailable(info?: InterpreterUri): Promise { const pythonExecutionFactory = this.serviceContainer.get(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 { + if (isResource(resource)) { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pixi) { + return false; + } + + const pixiEnv = await getPixiEnvironmentFromInterpreter(interpreter.path); + return pixiEnv !== undefined; + } + return resource.envType === EnvironmentType.Pixi; + } + + /** + * Return the commandline args needed to install the module. + */ + protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + const pythonPath = isResource(resource) + ? this.configurationService.getSettings(resource).pythonPath + : getEnvPath(resource.path, resource.envPath).path ?? ''; + + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + const execPath = pixiEnv?.pixi.command; + + let args = ['add', moduleName]; + const manifestPath = pixiEnv?.manifestPath; + if (manifestPath !== undefined) { + args = args.concat(['--manifest-path', manifestPath]); + } + + return { + args, + execPath, + }; + } +} diff --git a/src/client/common/installer/poetryInstaller.ts b/src/client/common/installer/poetryInstaller.ts index f5b4c5fabfd5..5017d0813d98 100644 --- a/src/client/common/installer/poetryInstaller.ts +++ b/src/client/common/installer/poetryInstaller.ts @@ -4,69 +4,76 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; +import { isPoetryEnvironmentRelatedToFolder } from '../../pythonEnvironments/common/environmentManagers/poetry'; +import { EnvironmentType, ModuleInstallerType } from '../../pythonEnvironments/info'; import { IWorkspaceService } from '../application/types'; -import { traceError } from '../logger'; -import { IFileSystem } from '../platform/types'; -import { IProcessServiceFactory } from '../process/types'; import { ExecutionInfo, IConfigurationService } from '../types'; +import { isResource } from '../utils/misc'; import { ModuleInstaller } from './moduleInstaller'; -import { IModuleInstaller } from './types'; +import { InterpreterUri } from './types'; + export const poetryName = 'poetry'; -const poetryFile = 'poetry.lock'; @injectable() -export class PoetryInstaller extends ModuleInstaller implements IModuleInstaller { +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; + } - public get displayName() { + // 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, + constructor( + @inject(IServiceContainer) serviceContainer: IServiceContainer, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IProcessServiceFactory) private readonly processFactory: IProcessServiceFactory) { + ) { super(serviceContainer); } - public async isSupported(resource?: Uri): Promise { + + public async isSupported(resource?: InterpreterUri): Promise { if (!resource) { return false; } - const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - if (!workspaceFolder) { + if (!isResource(resource)) { return false; } - if (!(await this.fs.fileExists(path.join(workspaceFolder.uri.fsPath, poetryFile)))) { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; + if (!interpreter || !workspaceFolder || interpreter.envType !== EnvironmentType.Poetry) { return false; } - return this.isPoetryAvailable(workspaceFolder.uri); + // 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 isPoetryAvailable(workfolder: Uri) { - try { - const processService = await this.processFactory.create(workfolder); - const execPath = this.configurationService.getSettings(workfolder).poetryPath; - const result = await processService.exec(execPath, ['list'], { cwd: workfolder.fsPath }); - return result && ((result.stderr || '').trim().length === 0); - } catch (error) { - traceError(`${poetryFile} exists but Poetry not found`, error); - return false; - } - } - protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise { - const execPath = this.configurationService.getSettings(resource).poetryPath; - const args = ['add', '--dev', moduleName]; - if (moduleName === 'black') { - args.push('--allow-prereleases'); - } + + protected async getExecutionInfo(moduleName: string, resource?: InterpreterUri): Promise { + const execPath = this.configurationService.getSettings(isResource(resource) ? resource : undefined).poetryPath; + const args = ['add', '--group', 'dev', moduleName]; return { args, - execPath + execPath, }; } } diff --git a/src/client/common/installer/productInstaller.ts b/src/client/common/installer/productInstaller.ts index 02dd60ee3393..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 { EventName } from '../../telemetry/constants'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../application/types'; -import { Commands, STANDARD_OUTPUT_CHANNEL } from '../constants'; -import { IPlatformService } from '../platform/types'; +import { IApplicationShell, IWorkspaceService } from '../application/types'; import { IProcessServiceFactory, IPythonExecutionFactory } from '../process/types'; -import { ITerminalServiceFactory } from '../terminal/types'; import { - IConfigurationService, IInstaller, 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.torchProfilerInstallName, new Set([EnvironmentType.Conda, EnvironmentType.Pixi])], +]); -export abstract class BaseInstaller { +abstract class BaseInstaller implements IBaseInstaller { private static readonly PromptPromises = new Map>(); + protected readonly appShell: IApplicationShell; + protected readonly configService: IConfigurationService; - private readonly workspaceService: IWorkspaceService; + + protected readonly workspaceService: IWorkspaceService; + private readonly productService: IProductService; - constructor(protected serviceContainer: IServiceContainer, protected outputChannel: OutputChannel) { + protected readonly persistentStateFactory: IPersistentStateFactory; + + constructor(protected serviceContainer: IServiceContainer) { this.appShell = serviceContainer.get(IApplicationShell); this.configService = serviceContainer.get(IConfigurationService); this.workspaceService = serviceContainer.get(IWorkspaceService); this.productService = serviceContainer.get(IProductService); + this.persistentStateFactory = serviceContainer.get(IPersistentStateFactory); } - public promptToInstall(product: Product, resource?: Uri): Promise { + public promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise { // If this method gets called twice, while previous promise has not been resolved, then return that same promise. // E.g. previous promise is not resolved as a message has been displayed to the user, so no point displaying // 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 { + public async install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise { if (product === Product.unittest) { return InstallerResponse.Installed; } @@ -63,211 +104,372 @@ export abstract class BaseInstaller { const channels = this.serviceContainer.get(IInstallationChannelManager); const installer = await channels.getInstallationChannel(product, resource); if (!installer) { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + productName: ProductNames.get(product), + }); return InstallerResponse.Ignore; } - const moduleName = translateProductToModule(product, ModuleNamePurpose.install); - const logger = this.serviceContainer.get(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 { + const version = await this.getProductSemVer(product, resource); + if (!version) { + return ProductInstallStatus.NotInstalled; + } + if (semver.satisfies(version, semVerRequirement)) { + return ProductInstallStatus.Installed; + } + return ProductInstallStatus.NeedsUpgrade; + } + + /** + * + * @param product A product which supports SemVer versioning. + * @param resource A URI or a PythonEnvironment. + */ + private async getProductSemVer(product: Product, resource: InterpreterUri): Promise { + const interpreter = isResource(resource) ? undefined : resource; + const uri = isResource(resource) ? resource : undefined; + const executableName = this.getExecutableNameFromSettings(product, uri); + + const isModule = this.isExecutableAModule(product, uri); - return this.isInstalled(product, resource) - .then(isInstalled => isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore); + let version; + if (isModule) { + const pythonProcess = await this.serviceContainer + .get(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: uri, interpreter, allowEnvironmentFetchExceptions: true }); + version = await pythonProcess.getModuleVersion(executableName); + } else { + const process = await this.serviceContainer.get(IProcessServiceFactory).create(uri); + const result = await process.exec(executableName, ['--version'], { mergeStdOutErr: true }); + version = result.stdout.trim(); + } + if (!version) { + return null; + } + try { + return semver.coerce(version); + } catch (e) { + traceError(`Unable to parse version ${version} for product ${product}: `, e); + return null; + } } - public async isInstalled(product: Product, resource?: Uri): Promise { + public async isInstalled(product: Product, resource?: InterpreterUri): Promise { if (product === Product.unittest) { return true; } // User may have customized the module name or provided the fully qualified path. - const 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).create({ resource }); + const pythonProcess = await this.serviceContainer + .get(IPythonExecutionFactory) + .createActivatedEnvironment({ resource: uri, interpreter, allowEnvironmentFetchExceptions: true }); return pythonProcess.isModuleInstalled(executableName); - } else { - const process = await this.serviceContainer.get(IProcessServiceFactory).create(resource); - return process.exec(executableName, ['--version'], { mergeStdOutErr: true }) - .then(() => true) - .catch(() => false); } + const process = await this.serviceContainer.get(IProcessServiceFactory).create(uri); + return process + .exec(executableName, ['--version'], { mergeStdOutErr: true }) + .then(() => true) + .catch(() => false); } - protected abstract promptToInstallImplementation(product: Product, resource?: Uri): Promise; + protected abstract promptToInstallImplementation( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise; + protected getExecutableNameFromSettings(product: Product, resource?: Uri): string { const productType = this.productService.getProductType(product); const productPathService = this.serviceContainer.get(IProductPathService, productType); return productPathService.getExecutableNameFromSettings(product, resource); } - protected isExecutableAModule(product: Product, resource?: Uri): Boolean { + + protected isExecutableAModule(product: Product, resource?: Uri): boolean { const productType = this.productService.getProductType(product); const productPathService = this.serviceContainer.get(IProductPathService, productType); return productPathService.isExecutableAModule(product, resource); } } -export class CTagsInstaller extends BaseInstaller { - constructor(serviceContainer: IServiceContainer, outputChannel: OutputChannel) { - super(serviceContainer, outputChannel); - } - - public async install(_product: Product, resource?: Uri): Promise { - if (this.serviceContainer.get(IPlatformService).isWindows) { - this.outputChannel.appendLine('Install Universal Ctags Win32 to enable support for Workspace Symbols'); - this.outputChannel.appendLine('Download the CTags binary from the Universal CTags site.'); - this.outputChannel.appendLine('Option 1: Extract ctags.exe from the downloaded zip to any folder within your PATH so that Visual Studio Code can run it.'); - this.outputChannel.appendLine('Option 2: Extract to any folder and add the path to this folder to the command setting.'); - this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).'); - this.outputChannel.show(); - } else { - const terminalService = this.serviceContainer.get(ITerminalServiceFactory).getTerminalService(resource); - const logger = this.serviceContainer.get(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 { - 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 { - // 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 { 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 { - 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 { + // 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) + .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); + const pipInstaller = installers.find((installer) => installer.type === ModuleInstallerType.Pip); + if (pipInstaller) { + traceInfo(`Installing pip as its not available to install ${moduleName}.`); + await pipInstaller + .installModule(Product.pip, interpreter, cancel) + .catch((ex) => + traceError( + `Error in installing the module '${moduleName} as Pip could not be installed', ${ex}`, + ), + ); + + await this.isInstalled(Product.pip, interpreter) + .then((isInstalled) => { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: pipInstaller.displayName, + requiredInstaller: ModuleInstallerType.Pip, + version, + envType: interpreter.envType, + isInstalled, + productName: ProductNames.get(Product.pip), + }); + }) + .catch(noop); + + // Refresh the list of channels (pip may be avaialble now). + channels = await this.serviceContainer + .get(IInstallationChannelManager) + .getInstallationChannels(interpreter); + } else { + sendTelemetryEvent(EventName.PYTHON_INSTALL_PACKAGE, undefined, { + installer: 'unavailable', + requiredInstaller: ModuleInstallerType.Pip, + productName: ProductNames.get(Product.pip), + version, + envType: interpreter.envType, + }); + traceError(`Unable to install pip when its required.`); + } + } - 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(EventName.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(EventName.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(EventName.LINTER_NOT_INSTALLED_PROMPT, undefined, { action: 'select' }); - const commandManager = this.serviceContainer.get(ICommandManager); - await commandManager.executeCommand(Commands.Set_Linter); - } - return InstallerResponse.Ignore; - } - - /** - * For installers that want to avoid prompting the user over and over, they can make use of a - * persisted true/false value representing user responses to 'stop showing this prompt'. This method - * gets the persisted value given the installer-defined key. - * - * @param key Key to use to get a persisted response value, each installer must define this for themselves. - * @returns Boolean: The current state of the stored response key given. - */ - protected getStoredResponse(key: string): boolean { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - return state.value === true; + 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 { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const state = factory.createGlobalPersistentState(key, undefined); - if (state && state.value !== value) { - await state.updateValue(value); + protected async promptToInstallImplementation( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise { + 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 { - 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 { + if (product !== Product.python) { + throw new Error(`${product} cannot be installed via conda python installer`); + } + const interpreterService = this.serviceContainer.get(IInterpreterService); + const environment = isResource(resource) ? await interpreterService.getActiveInterpreter(resource) : resource; + if (!environment) { + return true; } + if ( + environment.envPath?.length && + environment.envType === EnvironmentType.Conda && + !isParentPath(environment?.path, environment.envPath) + ) { + return false; + } + return true; + } - 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 { + if (product !== Product.python) { + throw new Error(`${product} cannot be installed via python installer`); + } + // Active interpreter is a conda environment which does not contain python, hence install it. + const installers = this.serviceContainer.getAll(IModuleInstaller); + const condaInstaller = installers.find((installer) => installer.type === ModuleInstallerType.Conda); + if (!condaInstaller || !(await condaInstaller.isSupported(resource))) { + traceError('Conda installer not available for installing python in the given environment'); + return InstallerResponse.Ignore; + } + const moduleName = translateProductToModule(product); + await condaInstaller + .installModule(Product.python, resource, undefined, undefined, { installAsProcess: true }) + .catch((ex) => traceError(`Error in installing the module '${moduleName}', ${ex}`)); + return this.isInstalled(product, resource).then((isInstalled) => + isInstalled ? InstallerResponse.Installed : InstallerResponse.Ignore, + ); } -} -export class RefactoringLibraryInstaller extends BaseInstaller { - protected async promptToInstallImplementation(product: Product, resource?: Uri): Promise { - const productName = ProductNames.get(product)!; - const item = await this.appShell.showErrorMessage(`Refactoring library ${productName} is not installed. Install?`, 'Yes', 'No'); - return item === 'Yes' ? this.install(product, resource) : InstallerResponse.Ignore; + // eslint-disable-next-line class-methods-use-this + public async promptToInstall( + _product: Product, + _resource?: InterpreterUri, + _cancel?: CancellationToken, + _flags?: ModuleInstallFlags, + ): Promise { + // This package is installed directly without any prompt. + return InstallerResponse.Ignore; + } + + // eslint-disable-next-line class-methods-use-this + public async isProductVersionCompatible( + _product: Product, + _semVerRequirement: string, + _resource?: InterpreterUri, + ): Promise { + return ProductInstallStatus.Installed; } } @@ -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); + this.interpreterService = this.serviceContainer.get(IInterpreterService); + } + + public dispose(): void { + /** Do nothing. */ + } + + public async promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise { + 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 { - return this.createInstaller(product).promptToInstall(product, resource); + public async isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise { + return this.createInstaller(product).isProductVersionCompatible(product, semVerRequirement, resource); } - public async install(product: Product, resource?: Uri): Promise { - return this.createInstaller(product).install(product, resource); + + public async install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise { + return this.createInstaller(product).install(product, resource, cancel, flags, options); } - public async isInstalled(product: Product, resource?: Uri): Promise { + + public async isInstalled(product: Product, resource?: InterpreterUri): Promise { return this.createInstaller(product).isInstalled(product, resource); } - public translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string { - return translateProductToModule(product, purpose); + + // eslint-disable-next-line class-methods-use-this + public translateProductToModuleName(product: Product): string { + return translateProductToModule(product); } - private createInstaller(product: Product): BaseInstaller { + + private createInstaller(product: Product): IBaseInstaller { const productType = this.productService.getProductType(product); switch (productType) { - case ProductType.Formatter: - return new FormatterInstaller(this.serviceContainer, this.outputChannel); - case ProductType.Linter: - return new LinterInstaller(this.serviceContainer, this.outputChannel); - case ProductType.WorkspaceSymbols: - return new CTagsInstaller(this.serviceContainer, this.outputChannel); case ProductType.TestFramework: - return new TestFrameworkInstaller(this.serviceContainer, this.outputChannel); - case ProductType.RefactoringLibrary: - return new RefactoringLibraryInstaller(this.serviceContainer, this.outputChannel); + return new TestFrameworkInstaller(this.serviceContainer); + case ProductType.DataScience: + return new DataScienceInstaller(this.serviceContainer); + 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(); -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 474c28c5e14f..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 '../../testing/common/types'; -import { IConfigurationService, IInstaller, ModuleNamePurpose, Product } from '../types'; +import { ITestingService } from '../../testing/types'; +import { IConfigurationService, IInstaller, Product } from '../types'; import { IProductPathService } from './types'; @injectable() -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); } 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); - const settingsPropNames = formatHelper.getSettingsPropertyNames(product); - return settings.formatting[settingsPropNames.pathName] as string; - } -} - -@injectable() -export class LinterProductPathService extends BaseProductPathsService { - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(serviceContainer); - } - public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const linterManager = this.serviceContainer.get(ILinterManager); - return linterManager.getLinterInfo(product).pathName(resource); + return ( + typeof moduleName === 'string' && moduleName.length > 0 && path.basename(executableName) === executableName + ); } } @@ -79,11 +41,11 @@ export class TestFrameworkProductPathService extends BaseProductPathsService { super(serviceContainer); } public getExecutableNameFromSettings(product: Product, resource?: Uri): string { - const testHelper = this.serviceContainer.get(ITestsHelper); + const testHelper = this.serviceContainer.get(ITestingService); const settingsPropNames = testHelper.getSettingsPropertyNames(product); if (!settingsPropNames.pathName) { // E.g. in the case of UnitTests we don't allow customizing the paths. - return this.productInstaller.translateProductToModuleName(product, ModuleNamePurpose.run); + return this.productInstaller.translateProductToModuleName(product); } const settings = this.configService.getSettings(resource); return settings.testing[settingsPropNames.pathName] as string; @@ -91,11 +53,11 @@ export class TestFrameworkProductPathService extends BaseProductPathsService { } @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(); 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 8ae4beffa549..1e273ada818c 100644 --- a/src/client/common/installer/serviceRegistry.ts +++ b/src/client/common/installer/serviceRegistry.ts @@ -3,30 +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 { PixiInstaller } from './pixiInstaller'; import { PoetryInstaller } from './poetryInstaller'; -import { CTagsProductPathService, FormatterProductPathService, LinterProductPathService, RefactoringLibraryProductPathService, TestFrameworkProductPathService } from './productPath'; +import { DataScienceProductPathService, TestFrameworkProductPathService } from './productPath'; import { ProductService } from './productService'; import { IInstallationChannelManager, IModuleInstaller, IProductPathService, IProductService } from './types'; export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(IModuleInstaller, PixiInstaller); serviceManager.addSingleton(IModuleInstaller, CondaInstaller); serviceManager.addSingleton(IModuleInstaller, PipInstaller); serviceManager.addSingleton(IModuleInstaller, PipEnvInstaller); serviceManager.addSingleton(IModuleInstaller, PoetryInstaller); serviceManager.addSingleton(IInstallationChannelManager, InstallationChannelManager); - serviceManager.addSingleton(IProductService, ProductService); - serviceManager.addSingleton(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); - serviceManager.addSingleton(IProductPathService, FormatterProductPathService, ProductType.Formatter); - serviceManager.addSingleton(IProductPathService, LinterProductPathService, ProductType.Linter); - serviceManager.addSingleton(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); - serviceManager.addSingleton(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - serviceManager.addSingleton(IWebPanelProvider, WebPanelProvider); + serviceManager.addSingleton( + IProductPathService, + TestFrameworkProductPathService, + ProductType.TestFramework, + ); + serviceManager.addSingleton( + 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; - isSupported(resource?: Uri): Promise; + 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; + isSupported(resource?: InterpreterUri): Promise; +} + +export const IBaseInstaller = Symbol('IBaseInstaller'); +export interface IBaseInstaller { + install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise; + promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise; + isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise; + isInstalled(product: Product, resource?: InterpreterUri): Promise; } export const IPythonInstallation = Symbol('IPythonInstallation'); @@ -19,8 +59,8 @@ export interface IPythonInstallation { export const IInstallationChannelManager = Symbol('IInstallationChannelManager'); export interface IInstallationChannelManager { - getInstallationChannel(product: Product, resource?: Uri): Promise; - getInstallationChannels(resource?: Uri): Promise; + getInstallationChannel(product: Product, resource?: InterpreterUri): Promise; + getInstallationChannels(resource?: InterpreterUri): Promise; 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 { + return this._didChangeInterpreterEmitter.event; + } + public _didChangeInterpreterEmitter = new EventEmitter(); + private fileSystemPaths: FileSystemPaths; + constructor( + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IDisposableRegistry) disposables: IDisposable[], + @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 | undefined; + let workspaceSetting: IPersistentState | undefined; + if (resource) { + workspaceFolderSetting = this.persistentStateFactory.createGlobalPersistentState( + this.getSettingKey(resource, ConfigurationTarget.WorkspaceFolder, useOldKey), + undefined, + ); + workspaceSetting = this.persistentStateFactory.createGlobalPersistentState( + this.getSettingKey(resource, ConfigurationTarget.Workspace, useOldKey), + undefined, + ); + } + const defaultInterpreterPath: InspectInterpreterSettingType = + this.workspaceService.getConfiguration('python', resource)?.inspect('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 { + resource = PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri; + if (configTarget === ConfigurationTarget.Global) { + const pythonConfig = this.workspaceService.getConfiguration('python'); + const globalValue = pythonConfig.inspect('defaultInterpreterPath')!.globalValue; + if (globalValue !== pythonPath) { + await pythonConfig.update('defaultInterpreterPath', pythonPath, true); + } + return; + } + if (!resource) { + traceError('Cannot update workspace settings as no workspace is opened'); + return; + } + const settingKey = this.getSettingKey(resource, configTarget); + const persistentSetting = this.persistentStateFactory.createGlobalPersistentState( + settingKey, + undefined, + ); + if (persistentSetting.value !== pythonPath) { + await persistentSetting.updateValue(pythonPath); + this._didChangeInterpreterEmitter.fire({ uri: resource, configTarget }); + 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 { + 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 { + // 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( + 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 { + // 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( + 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( + 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/liveshare/liveshare.ts b/src/client/common/liveshare/liveshare.ts deleted file mode 100644 index aaeda182293b..000000000000 --- a/src/client/common/liveshare/liveshare.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 * as vsls from 'vsls/vscode'; - -import { IApplicationShell, ILiveShareApi, IWorkspaceService } from '../application/types'; -import { IConfigurationService, IDisposableRegistry } from '../types'; -import { LiveShareProxy } from './liveshareProxy'; - -// tslint:disable:no-any unified-signatures - -@injectable() -export class LiveShareApi implements ILiveShareApi { - - private supported : boolean = false; - private apiPromise : Promise | undefined; - - constructor( - @inject(IDisposableRegistry) disposableRegistry : IDisposableRegistry, - @inject(IWorkspaceService) workspace : IWorkspaceService, - @inject(IConfigurationService) private configService : IConfigurationService, - @inject(IApplicationShell) private appShell : IApplicationShell - ) { - const disposable = workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('python.dataScience', undefined)) { - // When config changes happen, recreate our commands. - this.onSettingsChanged(); - } - }); - disposableRegistry.push(disposable); - this.onSettingsChanged(); - } - - public getApi(): Promise { - return this.apiPromise!; - } - - private onSettingsChanged() { - const supported = this.configService.getSettings().datascience.allowLiveShare; - if (supported !== this.supported) { - this.supported = supported ? true : false; - const liveShareTimeout = this.configService.getSettings().datascience.liveShareConnectionTimeout; - this.apiPromise = supported ? - vsls.getApi().then(a => a ? new LiveShareProxy(this.appShell, liveShareTimeout, a) : a) : - Promise.resolve(null); - - } else if (!this.apiPromise) { - this.apiPromise = Promise.resolve(null); - } - } -} diff --git a/src/client/common/liveshare/liveshareProxy.ts b/src/client/common/liveshare/liveshareProxy.ts deleted file mode 100644 index 9241fd5427a2..000000000000 --- a/src/client/common/liveshare/liveshareProxy.ts +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Disposable, Event, TreeDataProvider, Uri } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { LiveShare, LiveShareCommands } from '../../datascience/constants'; -import { IApplicationShell } from '../application/types'; -import { createDeferred, Deferred } from '../utils/async'; -import * as localize from '../utils/localize'; -import { ServiceProxy } from './serviceProxy'; - -// tslint:disable:no-any unified-signatures -export class LiveShareProxy implements vsls.LiveShare { - private currentRole: vsls.Role = vsls.Role.None; - private guestChecker: vsls.SharedService | vsls.SharedServiceProxy | null = null; - private pendingGuestCheckCount = 0; - private peerCheckPromise : Deferred | undefined; - constructor( - private applicationShell: IApplicationShell, - private peerTimeout: number | undefined, - private realApi : vsls.LiveShare - ) { - this.realApi.onDidChangePeers(this.onPeersChanged, this); - this.realApi.onDidChangeSession(this.onSessionChanged, this); - this.onSessionChanged({ session: this.realApi.session }).ignoreErrors(); - } - public get session(): vsls.Session { - return this.realApi.session; - } - public get onDidChangeSession(): Event { - return this.realApi.onDidChangeSession; - } - public get peers(): vsls.Peer[] { - return this.realApi.peers; - } - public get onDidChangePeers(): Event { - return this.realApi.onDidChangePeers; - } - public share(options?: vsls.ShareOptions | undefined): Promise { - return this.realApi.share(options); - } - public join(link: Uri, options?: vsls.JoinOptions | undefined): Promise { - return this.realApi.join(link, options); - } - public end(): Promise { - return this.realApi.end(); - } - public async shareService(name: string): Promise { - // Create the real shared service. - const realService = await this.realApi.shareService(name); - - // Create a proxy for the shared service. This allows us to wait for the next request/response - // on the shared service to cause a failure when the guest doesn't have the python extension installed. - if (realService) { - return new ServiceProxy(realService, () => this.peersAreOkay(), () => this.forceShutdown()); - } - - return realService; - } - public unshareService(name: string): Promise { - return this.realApi.unshareService(name); - } - public getSharedService(name: string): Promise { - return this.realApi.getSharedService(name); - } - public convertLocalUriToShared(localUri: Uri): Uri { - return this.realApi.convertLocalUriToShared(localUri); - } - public convertSharedUriToLocal(sharedUri: Uri): Uri { - return this.realApi.convertSharedUriToLocal(sharedUri); - } - public registerCommand(command: string, isEnabled?: (() => boolean) | undefined, thisArg?: any): Disposable | null { - return this.realApi.registerCommand(command, isEnabled, thisArg); - } - public registerTreeDataProvider(viewId: vsls.View, treeDataProvider: TreeDataProvider): Disposable | null { - return this.realApi.registerTreeDataProvider(viewId, treeDataProvider); - } - public registerContactServiceProvider(name: string, contactServiceProvider: vsls.ContactServiceProvider): Disposable | null { - return this.realApi.registerContactServiceProvider(name, contactServiceProvider); - } - public shareServer(server: vsls.Server): Promise { - return this.realApi.shareServer(server); - } - public getContacts(emails: string[]): Promise { - return this.realApi.getContacts(emails); - } - - private async onSessionChanged(ev: vsls.SessionChangeEvent) : Promise { - const newRole = ev.session ? ev.session.role : vsls.Role.None; - if (this.currentRole !== newRole) { - // Setup our guest checker service. - if (this.currentRole === vsls.Role.Host) { - await this.realApi.unshareService(LiveShare.GuestCheckerService); - } - this.currentRole = newRole; - - // If host, we need to listen for responses - if (this.currentRole === vsls.Role.Host) { - this.guestChecker = await this.realApi.shareService(LiveShare.GuestCheckerService); - if (this.guestChecker) { - this.guestChecker.onNotify(LiveShareCommands.guestCheck, (_args: object) => this.onGuestResponse()); - } - - // If guest, we need to list for requests. - } else if (this.currentRole === vsls.Role.Guest) { - this.guestChecker = await this.realApi.getSharedService(LiveShare.GuestCheckerService); - if (this.guestChecker) { - this.guestChecker.onNotify(LiveShareCommands.guestCheck, (_args: object) => this.onHostRequest()); - } - } - } - } - - private onPeersChanged(_ev: vsls.PeersChangeEvent) { - if (this.currentRole === vsls.Role.Host && this.guestChecker) { - // Update our pending count. This means we need to ask again if positive. - this.pendingGuestCheckCount = this.realApi.peers.length; - this.peerCheckPromise = undefined; - } - } - - private peersAreOkay() : Promise { - // If already asking, just use that promise - if (this.peerCheckPromise) { - return this.peerCheckPromise.promise; - } - - // Shortcut if we don't need to ask. - if (!this.guestChecker || this.currentRole !== vsls.Role.Host || this.pendingGuestCheckCount <= 0) { - return Promise.resolve(true); - } - - // We need to ask each guest then. - this.peerCheckPromise = createDeferred(); - this.guestChecker.notify(LiveShareCommands.guestCheck, {}); - - // Wait for a second and then check - setTimeout(this.validatePendingGuests.bind(this), this.peerTimeout ? this.peerTimeout : 1000); - return this.peerCheckPromise.promise; - } - - private validatePendingGuests() { - if (this.peerCheckPromise && !this.peerCheckPromise.resolved) { - this.peerCheckPromise.resolve(this.pendingGuestCheckCount <= 0); - } - } - - private onGuestResponse() { - // Guest has responded to a guest check. Update our pending count - this.pendingGuestCheckCount -= 1; - if (this.pendingGuestCheckCount <= 0 && this.peerCheckPromise) { - this.peerCheckPromise.resolve(true); - } - } - - private onHostRequest() { - // Host is asking us to respond - if (this.guestChecker && this.currentRole === vsls.Role.Guest) { - this.guestChecker.notify(LiveShareCommands.guestCheck, {}); - } - } - - private forceShutdown() { - // One or more guests doesn't have the python extension installed. Force our live share session to disconnect - this.realApi.end().then(() => { - this.pendingGuestCheckCount = 0; - this.peerCheckPromise = undefined; - this.applicationShell.showErrorMessage(localize.DataScience.liveShareInvalid()); - }).ignoreErrors(); - } -} diff --git a/src/client/common/liveshare/serviceProxy.ts b/src/client/common/liveshare/serviceProxy.ts deleted file mode 100644 index 5cd0212f6fa0..000000000000 --- a/src/client/common/liveshare/serviceProxy.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Event } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -// tslint:disable:no-any unified-signatures -export class ServiceProxy implements vsls.SharedService { - constructor( - private realService : vsls.SharedService, - private guestsResponding: () => Promise, - private forceShutdown: () => void - ) { - } - public get isServiceAvailable(): boolean { - return this.realService.isServiceAvailable; - } - public get onDidChangeIsServiceAvailable(): Event { - return this.realService.onDidChangeIsServiceAvailable; - } - - public onRequest(name: string, handler: vsls.RequestHandler): void { - return this.realService.onRequest(name, handler); - } - public onNotify(name: string, handler: vsls.NotifyHandler): void { - return this.realService.onNotify(name, handler); - } - public async notify(name: string, args: object): Promise { - if (await this.guestsResponding()) { - return this.realService.notify(name, args); - } else { - this.forceShutdown(); - } - } -} diff --git a/src/client/common/logger.ts b/src/client/common/logger.ts deleted file mode 100644 index f333642e8f8e..000000000000 --- a/src/client/common/logger.ts +++ /dev/null @@ -1,365 +0,0 @@ -// tslint:disable:no-console no-any -import { injectable } from 'inversify'; -import * as path from 'path'; -import * as util from 'util'; -import { createLogger, format, transports } from 'winston'; -import { EXTENSION_ROOT_DIR } from '../constants'; -import { sendTelemetryEvent } from '../telemetry'; -import { isTestExecution } from './constants'; -import { ILogger, LogLevel } from './types'; -import { StopWatch } from './utils/stopWatch'; - -// tslint:disable-next-line: no-var-requires no-require-imports -const TransportStream = require('winston-transport'); - -// Initialize the loggers as soon as this module is imported. -const consoleLogger = createLogger(); -const fileLogger = createLogger(); -initializeConsoleLogger(); -initializeFileLogger(); - -const logLevelMap = { - [LogLevel.Error]: 'error', - [LogLevel.Information]: 'info', - [LogLevel.Warning]: 'warn' -}; - -function log(logLevel: LogLevel, ...args: any[]) { - if (consoleLogger.transports.length > 0) { - const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); - consoleLogger.log(logLevelMap[logLevel], message); - } - logToFile(logLevel, ...args); -} -function logToFile(logLevel: LogLevel, ...args: any[]) { - if (fileLogger.transports.length === 0) { - return; - } - const message = args.length === 0 ? '' : util.format(args[0], ...args.slice(1)); - fileLogger.log(logLevelMap[logLevel], message); -} - -/** - * Initialize the logger for console. - * We do two things here: - * - Anything written to the logger will be displayed in the console window as well - * This is the behavior of the extension when runnning it. - * When running tests on CI, we might not want this behavior, as it'll pollute the - * test output with logging (as mentioned this is optional). - * Messages logged using our logger will be prefixed with `Python Extension: ....` for console window. - * This way, its easy to identify messages specific to the python extension. - * - Monkey patch the console.log and similar methods to send messages to the file logger. - * When running UI tests or similar, and we want to see everything that was dumped into `console window`, - * then we need to hijack the console logger. - * To do this we need to monkey patch the console methods. - * This is optional (generally done when running tests on CI). - */ -function initializeConsoleLogger() { - const logMethods = { - log: Symbol.for('log'), - info: Symbol.for('info'), - error: Symbol.for('error'), - debug: Symbol.for('debug'), - warn: Symbol.for('warn') - }; - - function logToConsole(stream: 'info' | 'error' | 'warn' | 'log' | 'debug', ...args: any[]) { - if (['info', 'error', 'warn', 'log', 'debug'].indexOf(stream) === -1) { - stream = 'log'; - } - // Further below we monkeypatch the console.log, etc methods. - const fn = (console as any)[logMethods[stream]] || console[stream] || console.log; - fn(...args); - } - - // Hijack `console.log` when running tests on CI. - if (process.env.VSC_PYTHON_LOG_FILE && process.env.TF_BUILD) { - /* - 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). - */ - // Keep track of the original functions before we monkey patch them. - // Using symbols guarantee the properties will be unique & prevents clashing with names other code/library may create or have created. - (console as any)[logMethods.log] = console.log; - (console as any)[logMethods.info] = console.info; - (console as any)[logMethods.error] = console.error; - (console as any)[logMethods.debug] = console.debug; - (console as any)[logMethods.warn] = console.warn; - - // tslint:disable-next-line: no-function-expression - console.log = function () { - const args = Array.prototype.slice.call(arguments); - logToConsole('log', ...args); - logToFile(LogLevel.Information, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.info = function () { - const args = Array.prototype.slice.call(arguments); - logToConsole('info', ...args); - logToFile(LogLevel.Information, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.warn = function () { - const args = Array.prototype.slice.call(arguments); - logToConsole('warn', ...args); - logToFile(LogLevel.Warning, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.error = function () { - const args = Array.prototype.slice.call(arguments); - logToConsole('error', ...args); - logToFile(LogLevel.Error, ...args); - }; - // tslint:disable-next-line: no-function-expression - console.debug = function () { - const args = Array.prototype.slice.call(arguments); - logToConsole('debug', ...args); - logToFile(LogLevel.Information, ...args); - }; - } - - if (isTestExecution() && !process.env.VSC_PYTHON_FORCE_LOGGING) { - // Do not log to console if running tests on CI and we're not asked to do so. - return; - } - - // Rest of this stuff is just to instantiate the console logger. - // I.e. when we use our logger, ensure we also log to the console (for end users). - const formattedMessage = Symbol.for('message'); - class ConsoleTransport extends TransportStream { - constructor(options?: any) { - super(options); - } - public log?(info: { level: string; message: string;[formattedMessage]: string }, next: () => void): any { - setImmediate(() => this.emit('logged', info)); - logToConsole(info.level as any, info[formattedMessage] || info.message); - if (next) { - next(); - } - } - } - const consoleFormatter = format.printf(({ level, message, label, timestamp }) => { - // If we're on CI server, no need for the label (prefix) - // Pascal casing og log level, so log files get highlighted when viewing in VSC and other editors. - const prefix = `${level.substring(0, 1).toUpperCase()}${level.substring(1)} ${process.env.TF_BUILD ? '' : label}`; - return `${prefix.trim()} ${timestamp}: ${message}`; - }); - const consoleFormat = format.combine( - format.label({ label: 'Python Extension:' }), - format.timestamp({ - format: 'YYYY-MM-DD HH:mm:ss' - }), - consoleFormatter - ); - consoleLogger.add(new ConsoleTransport({ format: consoleFormat }) as any); -} - -/** - * Send all logging output to a log file. - * We log to the file only if a file has been specified as an env variable. - * Currently this is setup on CI servers. - */ -function initializeFileLogger() { - if (!process.env.VSC_PYTHON_LOG_FILE) { - return; - } - const fileFormatter = format.printf(({ level, message, timestamp }) => { - // Pascal casing og log level, so log files get highlighted when viewing in VSC and other editors. - return `${level.substring(0, 1).toUpperCase()}${level.substring(1)} ${timestamp}: ${message}`; - }); - const fileFormat = format.combine( - format.timestamp({ - format: 'YYYY-MM-DD HH:mm:ss' - }), - fileFormatter - ); - const logFilePath = path.isAbsolute(process.env.VSC_PYTHON_LOG_FILE) ? process.env.VSC_PYTHON_LOG_FILE : - path.join(EXTENSION_ROOT_DIR, process.env.VSC_PYTHON_LOG_FILE); - const logFileSink = new transports.File({ - format: fileFormat, - filename: logFilePath, - handleExceptions: true - }); - fileLogger.add(logFileSink); -} - -const enableLogging = !isTestExecution() || process.env.VSC_PYTHON_FORCE_LOGGING || process.env.VSC_PYTHON_LOG_FILE; - -@injectable() -export class Logger implements ILogger { - // tslint:disable-next-line:no-any - public static error(...args: any[]) { - if (enableLogging) { - log(LogLevel.Error, ...args); - } - } - // tslint:disable-next-line:no-any - public static warn(...args: any[]) { - if (enableLogging) { - log(LogLevel.Warning, ...args); - } - } - // tslint:disable-next-line:no-any - public static verbose(...args: any[]) { - if (enableLogging) { - log(LogLevel.Information, ...args); - } - } - public logError(...args: any[]) { - Logger.error(...args); - } - public logWarning(...args: any[]) { - Logger.warn(...args); - } - public logInformation(...args: any[]) { - Logger.verbose(...args); - } -} - -/** - * What do we want to log. - * @export - * @enum {number} - */ -export 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) => { - if (item === undefined) { - return `Arg ${index + 1}: undefined`; - } - if (item === null) { - return `Arg ${index + 1}: null`; - } - try { - if (item && item.fsPath) { - return `Arg ${index + 1}: `; - } - return `Arg ${index + 1}: ${JSON.stringify(item)}`; - } catch { - return `Arg ${index + 1}: `; - } - }) - .join(', '); - } catch { - return ''; - } -} - -// tslint:disable-next-line:no-any -function returnValueToLogString(returnValue: any): string { - const returnValueMessage = 'Return Value: '; - if (returnValue === undefined) { - return `${returnValueMessage}undefined`; - } - if (returnValue === null) { - return `${returnValueMessage}null`; - } - try { - return `${returnValueMessage}${JSON.stringify(returnValue)}`; - } catch { - return `${returnValueMessage}`; - } -} - -export function traceVerbose(...args: any[]) { - log(LogLevel.Information, ...args); -} - -export function traceError(...args: any[]) { - log(LogLevel.Error, ...args); -} - -export function traceInfo(...args: any[]) { - log(LogLevel.Information, ...args); -} - -export function traceWarning(...args: any[]) { - log(LogLevel.Warning, ...args); -} - -export namespace traceDecorators { - export function verbose(message: string, options: LogOptions = LogOptions.Arguments | LogOptions.ReturnValue) { - return trace(message, options); - } - export function error(message: string) { - 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) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - const className = _ && _.constructor ? _.constructor.name : ''; - // tslint:disable-next-line:no-any - function writeSuccess(elapsedTime: number, returnValue: any) { - if (logLevel === LogLevel.Error) { - return; - } - writeToLog(elapsedTime, returnValue); - } - function writeError(elapsedTime: number, ex: Error) { - writeToLog(elapsedTime, undefined, ex); - } - // tslint:disable-next-line:no-any - function writeToLog(elapsedTime: number, returnValue?: any, ex?: Error) { - const messagesToLog = [message]; - messagesToLog.push(`Class name = ${className}, completed in ${elapsedTime}ms`); - if ((options && LogOptions.Arguments) === LogOptions.Arguments) { - messagesToLog.push(argsToLogString(args)); - } - if ((options & LogOptions.ReturnValue) === LogOptions.ReturnValue) { - messagesToLog.push(returnValueToLogString(returnValue)); - } - if (ex) { - log(LogLevel.Error, messagesToLog.join(', '), ex); - sendTelemetryEvent('ERROR' as any, undefined, undefined, ex); - } else { - log(LogLevel.Information, messagesToLog.join(', ')); - } - } - const timer = new StopWatch(); - 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) - .then(data => { - writeSuccess(timer.elapsedTime, data); - return data; - }) - .catch(ex => { - writeError(timer.elapsedTime, ex); - }); - } else { - writeSuccess(timer.elapsedTime, result); - } - return result; - } catch (ex) { - writeError(timer.elapsedTime, 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, ' '); - line = line.replace(/``/g, '`'); // Convert double backticks to single. - // Keep hard line breaks for the preformatted content - this.md.push(`${line} `); - return 0; - } - - private inCodeState(line: string): void { - const prevLine = this.md.length > 0 ? this.md[this.md.length - 1] : undefined; - if (line.length === 0 && prevLine && (prevLine.length === 0 || prevLine.startsWith('```'))) { - return; // Avoid more than one empty line in a row. - } - - if (line.startsWith('```')) { - this.endCodeBlock(); - } else { - this.md.push(line); - } - } - - private isIgnorable(line: string): boolean { - if (line.indexOf('generated/') >= 0) { - return true; // Drop generated content. - } - const trimmed = line.trim(); - if (trimmed.startsWith('..') && trimmed.indexOf('::') > 0) { - // Ignore lines likes .. sectionauthor:: John Doe. - return true; - } - return false; - } - - private checkPreContent(lines: string[], i: number): number { - const line = lines[i]; - if (i === 0 || line.trim().length === 0) { - return 0; - } - - if (!isWhiteSpace(line.charCodeAt(0)) && !this.isListItem(line)) { - return 0; // regular line, nothing to do here. - } - // Indented content is considered to be preformatted. - this.startPreformattedBlock(); - return -1; - } - - private handleSectionHeader(lines: string[], i: number): boolean { - const line = lines[i]; - if (i < lines.length - 1 && (lines[i + 1].startsWith('==='))) { - // Section title -> heading level 3. - this.md.push(`### ${this.cleanup(line)}`); - return true; - } - if (i < lines.length - 1 && (lines[i + 1].startsWith('---'))) { - // Subsection title -> heading level 4. - this.md.push(`#### ${this.cleanup(line)}`); - return true; - } - return false; - } - - private handleDoubleColon(line: string): boolean { - if (!line.endsWith('::')) { - return false; - } - // Literal blocks begin with `::`. Such as sequence like - // '... as shown below::' that is followed by a preformatted text. - if (line.length > 2 && !line.startsWith('..')) { - // Ignore lines likes .. autosummary:: John Doe. - // Trim trailing : so :: turns into :. - this.md.push(line.substring(0, line.length - 1)); - } - - this.startPreformattedBlock(); - return true; - } - - private startPreformattedBlock(): void { - // Remove previous empty line so we avoid double empties. - this.tryRemovePrecedingEmptyLines(); - // Lie about the language since we don't want preformatted text - // to be colorized as Python. HTML is more 'appropriate' as it does - // not colorize -- or + or keywords like 'from'. - this.md.push('```html'); - this.state = State.Preformatted; - } - - private endPreformattedBlock(): void { - if (this.state === State.Preformatted) { - this.tryRemovePrecedingEmptyLines(); - this.md.push('```'); - this.state = State.Default; - } - } - - private startCodeBlock(): void { - // Remove previous empty line so we avoid double empties. - this.tryRemovePrecedingEmptyLines(); - this.md.push('```python'); - this.state = State.Code; - } - - private endCodeBlock(): void { - if (this.state === State.Code) { - this.tryRemovePrecedingEmptyLines(); - this.md.push('```'); - this.state = State.Default; - } - } - - private tryRemovePrecedingEmptyLines(): void { - while (this.md.length > 0 && this.md[this.md.length - 1].trim().length === 0) { - this.md.pop(); - } - } - - private isListItem(line: string): boolean { - const trimmed = line.trim(); - const ch = trimmed.length > 0 ? trimmed.charCodeAt(0) : 0; - return ch === Char.Asterisk || ch === Char.Hyphen || isDecimal(ch); - } - - private cleanup(line: string): string { - return line.replace(/:mod:/g, 'module:'); - } -} diff --git a/src/client/common/net/browser.ts b/src/client/common/net/browser.ts index 74c629c7d592..115df0f2969c 100644 --- a/src/client/common/net/browser.ts +++ b/src/client/common/net/browser.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:no-var-requires - import { injectable } from 'inversify'; import { env, Uri } from 'vscode'; import { IBrowserService } from '../types'; diff --git a/src/client/common/net/fileDownloader.ts b/src/client/common/net/fileDownloader.ts deleted file mode 100644 index c6fb08a587f2..000000000000 --- a/src/client/common/net/fileDownloader.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { WriteStream } from 'fs'; -import { inject, injectable } from 'inversify'; -import * as requestTypes from 'request'; -import { Progress, ProgressLocation } from 'vscode'; -import { IApplicationShell } from '../application/types'; -import { IFileSystem } from '../platform/types'; -import { DownloadOptions, IFileDownloader, IHttpClient } from '../types'; -import { Http } from '../utils/localize'; -import { noop } from '../utils/misc'; - -@injectable() -export class FileDownloader implements IFileDownloader { - constructor(@inject(IHttpClient) private readonly httpClient: IHttpClient, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IApplicationShell) private readonly appShell: IApplicationShell) { - } - public async downloadFile(uri: string, options: DownloadOptions): Promise { - if (options.outputChannel) { - options.outputChannel.append(Http.downloadingFile().format(uri)); - } - const tempFile = await this.fs.createTemporaryFile(options.extension); - - await this.downloadFileWithStatusBarProgress(uri, options.progressMessagePrefix, tempFile.filePath) - .then(noop, ex => { - tempFile.dispose(); - return Promise.reject(ex); - }); - - return tempFile.filePath; - } - public async downloadFileWithStatusBarProgress(uri: string, progressMessage: string, tmpFilePath: string): Promise { - await this.appShell.withProgress({ location: ProgressLocation.Window }, async (progress) => { - const req = await this.httpClient.downloadFile(uri); - const fileStream = this.fs.createWriteStream(tmpFilePath); - return this.displayDownloadProgress(uri, progress, req, fileStream, progressMessage); - }); - } - public async displayDownloadProgress(uri: string, progress: Progress<{ message?: string; increment?: number }>, - request: requestTypes.Request, - fileStream: WriteStream, progressMessagePrefix: string): Promise { - return new Promise((resolve, reject) => { - request.on('response', (response) => { - if (response.statusCode !== 200) { - reject(new Error(`Failed with status ${response.statusCode}, ${response.statusMessage}, Uri ${uri}`)); - } - }); - // tslint:disable-next-line: no-require-imports - const requestProgress = require('request-progress'); - requestProgress(request) - // tslint:disable-next-line: no-any - .on('progress', (state: any) => { - const received = Math.round(state.size.transferred / 1024); - const total = Math.round(state.size.total / 1024); - const percentage = Math.round(100 * state.percent); - const message = Http.downloadingFileProgress().format(progressMessagePrefix, - received.toString(), total.toString(), percentage.toString()); - progress.report({ message }); - }) - // Handle errors from download. - .on('error', reject) - .pipe(fileStream) - // Handle error in writing to fs. - .on('error', reject) - .on('close', resolve); - }); - } -} diff --git a/src/client/common/net/httpClient.ts b/src/client/common/net/httpClient.ts deleted file mode 100644 index c7995fa8824c..000000000000 --- a/src/client/common/net/httpClient.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 { parse, ParseError } from 'jsonc-parser'; -import * as requestTypes from 'request'; -import { IHttpClient } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IWorkspaceService } from '../application/types'; -import { traceError } from '../logger'; - -@injectable() -export class HttpClient implements IHttpClient { - public readonly requestOptions: requestTypes.CoreOptions; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - const workspaceService = serviceContainer.get(IWorkspaceService); - this.requestOptions = { proxy: workspaceService.getConfiguration('http').get('proxy', '') }; - } - - public async downloadFile(uri: string): Promise { - // tslint:disable-next-line:no-any - const request = await import('request') as any as typeof requestTypes; - return request(uri, this.requestOptions); - } - - public async getJSON(uri: string, strict: boolean = true): Promise { - // tslint:disable-next-line:no-require-imports - const request = require('request') as typeof requestTypes; - return new Promise((resolve, reject) => { - request(uri, this.requestOptions, (ex, response, body) => { - if (ex) { - return reject(ex); - } - if (response.statusCode !== 200) { - return reject(new Error(`Failed with status ${response.statusCode}, ${response.statusMessage}, Uri ${uri}`)); - } - try { - if (strict) { - const content = JSON.parse(body) as T; - resolve(content); - } else { - // tslint:disable-next-line: prefer-const - let errors: ParseError[] = []; - const content = parse(body, errors, { allowTrailingComma: true, disallowComments: false }) as T; - if (errors.length > 0) { - traceError('JSONC parser returned ParseError codes', errors); - } - resolve(content); - } - } catch (ex) { - return reject(ex); - } - }); - }); - } -} diff --git a/src/client/common/net/socket/SocketStream.ts b/src/client/common/net/socket/SocketStream.ts index c811f6c4450c..b046cdceaf96 100644 --- a/src/client/common/net/socket/SocketStream.ts +++ b/src/client/common/net/socket/SocketStream.ts @@ -1,13 +1,13 @@ 'use strict'; import * as net from 'net'; -// tslint:disable:no-var-requires no-require-imports member-ordering no-any + const uint64be = require('uint64be'); enum DataType { string, int32, - int64 + int64, } export class SocketStream { @@ -26,7 +26,7 @@ export class SocketStream { this.socket.write(buffer); } public WriteString(value: string) { - const stringBuffer = new Buffer(value, 'utf-8'); + const stringBuffer = Buffer.from(value, 'utf-8'); this.WriteInt32(stringBuffer.length); if (stringBuffer.length > 0) { this.socket.write(stringBuffer); @@ -81,14 +81,14 @@ export class SocketStream { this.buffer = additionalData; return; } - const newBuffer = new Buffer(this.buffer.length + additionalData.length); + const newBuffer = Buffer.alloc(this.buffer.length + additionalData.length); this.buffer.copy(newBuffer); additionalData.copy(newBuffer, this.buffer.length); this.buffer = newBuffer; } private isSufficientDataAvailable(length: number): boolean { - if (this.buffer.length < (this.bytesRead + length)) { + if (this.buffer.length < this.bytesRead + length) { this.hasInsufficientDataForReading = true; } @@ -119,7 +119,7 @@ export class SocketStream { throw new Error('IOException() - Socket.ReadString failed to read string type;'); } - const type = new Buffer([byteRead]).toString(); + const type = Buffer.from([byteRead]).toString(); let isUnicode = false; switch (type) { case 'N': // null string @@ -183,7 +183,7 @@ export class SocketStream { 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'); diff --git a/src/client/common/net/socket/socketCallbackHandler.ts b/src/client/common/net/socket/socketCallbackHandler.ts index 43af241a13bf..82fe3ec1ae0d 100644 --- a/src/client/common/net/socket/socketCallbackHandler.ts +++ b/src/client/common/net/socket/socketCallbackHandler.ts @@ -1,10 +1,8 @@ -// 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 { @@ -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 07d72f05ea47..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; diff --git a/src/client/common/nuget/azureBlobStoreNugetRepository.ts b/src/client/common/nuget/azureBlobStoreNugetRepository.ts deleted file mode 100644 index 51d3ddf3751e..000000000000 --- a/src/client/common/nuget/azureBlobStoreNugetRepository.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, unmanaged } from 'inversify'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { IWorkspaceService } from '../application/types'; -import { traceDecorators } from '../logger'; -import { Resource } from '../types'; -import { INugetRepository, INugetService, NugetPackage } from './types'; - -@injectable() -export class AzureBlobStoreNugetRepository implements INugetRepository { - constructor( - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, - @unmanaged() protected readonly azureBlobStorageAccount: string, - @unmanaged() protected readonly azureBlobStorageContainer: string, - @unmanaged() protected readonly azureCDNBlobStorageAccount: string, - private getBlobStore: (uri: string) => Promise = _getAZBlobStore - ) { } - public async getPackages(packageName: string, resource: Resource): Promise { - return this.listPackages( - this.azureBlobStorageAccount, - this.azureBlobStorageContainer, - packageName, - this.azureCDNBlobStorageAccount, - resource - ); - } - - @captureTelemetry(EventName.PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES) - @traceDecorators.verbose('Listing Nuget Packages') - protected async listPackages( - azureBlobStorageAccount: string, - azureBlobStorageContainer: string, - packageName: string, - azureCDNBlobStorageAccount: string, - resource: Resource - ) { - const results = await this.listBlobStoreCatalog( - this.fixBlobStoreURI(azureBlobStorageAccount, resource), - azureBlobStorageContainer, - packageName - ); - const nugetService = this.serviceContainer.get(INugetService); - return results.map(item => { - return { - package: item.name, - uri: `${azureCDNBlobStorageAccount}/${azureBlobStorageContainer}/${item.name}`, - version: nugetService.getVersionFromPackageFileName(item.name) - }; - }); - } - - private async listBlobStoreCatalog( - azureBlobStorageAccount: string, - azureBlobStorageContainer: string, - packageName: string - ): Promise { - const blobStore = await this.getBlobStore(azureBlobStorageAccount); - return new Promise((resolve, reject) => { - // We must pass undefined according to docs, but type definition doesn't all it to be undefined or null!!! - // tslint:disable-next-line:no-any - const token = undefined as any; - blobStore.listBlobsSegmentedWithPrefix(azureBlobStorageContainer, packageName, token, - (error, result) => { - if (error) { - return reject(error); - } - resolve(result.entries); - }); - }); - } - private fixBlobStoreURI(uri: string, resource: Resource) { - if (!uri.startsWith('https:')) { - return uri; - } - - const workspace = this.serviceContainer.get(IWorkspaceService); - const cfg = workspace.getConfiguration('http', resource); - if (cfg.get('proxyStrictSSL', true)) { - return uri; - } - - // tslint:disable-next-line:no-http-string - return uri.replace(/^https:/, 'http:'); - } -} - -// The "azure-storage" package is large enough that importing it has -// a significant impact on extension startup time. So we import it -// lazily and deal with the consequences below. - -interface IBlobResult { - name: string; -} - -interface IBlobResults { - entries: IBlobResult[]; -} - -type ErrorOrResult = (error: Error, result: TResult) => void; - -interface IAZBlobStore { - listBlobsSegmentedWithPrefix( - container: string, - prefix: string, - // tslint:disable-next-line:no-any - currentToken: any, - callback: ErrorOrResult - ): void; -} - -async function _getAZBlobStore(uri: string): Promise { - // tslint:disable-next-line:no-require-imports - const az = await import('azure-storage') as typeof import('azure-storage'); - return az.createBlobServiceAnonymous(uri); -} diff --git a/src/client/common/nuget/nugetRepository.ts b/src/client/common/nuget/nugetRepository.ts deleted file mode 100644 index ad45552bc236..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 '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { INugetRepository, NugetPackage } from './types'; - -const nugetPackageBaseAddress = 'https://dotnetmyget.blob.core.windows.net/artifacts/dotnet-core-svc/nuget/v3/flatcontainer'; - -@injectable() -export class NugetRepository implements INugetRepository { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - public async getPackages(packageName: string): Promise { - const versions = await this.getVersions(nugetPackageBaseAddress, packageName); - return versions.map(version => { - const uri = this.getNugetPackageUri(nugetPackageBaseAddress, packageName, version); - return { version, uri, package: packageName }; - }); - } - public async getVersions(packageBaseAddress: string, packageName: string): Promise { - const uri = `${packageBaseAddress}/${packageName.toLowerCase().trim()}/index.json`; - const httpClient = this.serviceContainer.get(IHttpClient); - const result = await httpClient.getJSON<{ versions: string[] }>(uri); - return result.versions.map(v => parse(v, true) || new SemVer('0.0.0')); - } - public getNugetPackageUri(packageBaseAddress: string, packageName: string, version: SemVer): string { - return `${packageBaseAddress}/${packageName}/${version.raw}/${packageName}.${version.raw}.nupkg`; - } -} diff --git a/src/client/common/nuget/nugetService.ts b/src/client/common/nuget/nugetService.ts deleted file mode 100644 index a0a974dfd64d..000000000000 --- a/src/client/common/nuget/nugetService.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import * as path from 'path'; -import { parse, SemVer } from 'semver'; -import { INugetService } from './types'; - -@injectable() -export class NugetService implements INugetService { - public isReleaseVersion(version: SemVer): boolean { - return version.prerelease.length === 0; - } - - public getVersionFromPackageFileName(packageName: string): SemVer { - const ext = path.extname(packageName); - const versionWithExt = packageName.substring(packageName.indexOf('.') + 1); - const version = versionWithExt.substring(0, versionWithExt.length - ext.length); - // Take only the first 3 parts. - const parts = version.split('.'); - const semverParts = parts.filter((_, index) => index <= 2).join('.'); - const lastParts = parts.filter((_, index) => index === 3).join('.'); - const suffix = lastParts.length === 0 ? '' : `-${lastParts}`; - const fixedVersion = `${semverParts}${suffix}`; - return parse(fixedVersion, true) || new SemVer('0.0.0'); - } -} diff --git a/src/client/common/nuget/types.ts b/src/client/common/nuget/types.ts deleted file mode 100644 index dcde51025138..000000000000 --- a/src/client/common/nuget/types.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { SemVer } from 'semver'; -import { Resource } from '../types'; -export type NugetPackage = { package: string; version: SemVer; uri: string }; - -export const INugetService = Symbol('INugetService'); -export interface INugetService { - isReleaseVersion(version: SemVer): boolean; - getVersionFromPackageFileName(packageName: string): SemVer; -} - -export const INugetRepository = Symbol('INugetRepository'); -export interface INugetRepository { - getPackages(packageName: string, resource: Resource): Promise; -} diff --git a/src/client/common/open.ts b/src/client/common/open.ts deleted file mode 100644 index 7dc8eda253c3..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 childProcess from 'child_process'; - -// tslint:disable:no-any no-function-expression prefer-template -export function open(opts: any): Promise { - // opts = objectAssign({wait: true}, opts); - if (!opts.hasOwnProperty('wait')) { - (opts).wait = true; - } - - let cmd; - let appArgs = []; - let args: string[] = []; - const cpOpts: any = {}; - if (opts.cwd && typeof opts.cwd === 'string' && opts.cwd.length > 0) { - cpOpts.cwd = opts.cwd; - } - if (opts.env && Object.keys(opts.env).length > 0) { - cpOpts.env = opts.env; - } - - if (Array.isArray(opts.app)) { - appArgs = opts.app.slice(1); - opts.app = opts.app[0]; - } - - if (process.platform === 'darwin') { - const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; - cmd = 'osascript'; - args = ['-e', 'tell application "terminal"', - '-e', 'activate', - '-e', 'do script "' + sudoPrefix + [opts.app].concat(appArgs).join(' ') + '"', - '-e', 'end tell']; - } else if (process.platform === 'win32') { - cmd = 'cmd'; - args.push('/c', 'start'); - - if (opts.wait) { - args.push('/wait'); - } - - if (opts.app) { - args.push(opts.app); - } - - if (appArgs.length > 0) { - args = args.concat(appArgs); - } - } else { - cmd = 'gnome-terminal'; - const sudoPrefix = opts.sudo === true ? 'sudo ' : ''; - args = ['-x', 'sh', '-c', `"${sudoPrefix}${opts.app}" ${appArgs.join(' ')}`]; - } - - const cp = childProcess.spawn(cmd, args, cpOpts); - - if (opts.wait) { - return new Promise(function (resolve, reject) { - cp.once('error', reject); - - cp.once('close', function (code) { - if (code > 0) { - reject(new Error(`Exited with code ${code}`)); - return; - } - - resolve(cp); - }); - }); - } - - cp.unref(); - - return Promise.resolve(cp); -} diff --git a/src/client/common/persistentState.ts b/src/client/common/persistentState.ts index 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 implements IPersistentState{ - 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(key: string, defaultValue?: T): T | undefined { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + if (defaultValue === undefined) { + return _workspaceState.get(key); + } + return _workspaceState.get(key, defaultValue); +} + +export async function updateWorkspaceStateValue(key: string, value: T): Promise { + if (!_workspaceState) { + throw new Error('Workspace state not initialized'); + } + try { + _workspaceKeys.push(key); + await _workspaceState.update(key, value); + const after = getWorkspaceStateValue(key); + if (JSON.stringify(after) !== JSON.stringify(value)) { + await _workspaceState.update(key, undefined); + await _workspaceState.update(key, value); + traceError('Error while updating workspace state for key:', key); + } + } catch (ex) { + traceError(`Error while updating workspace state for key [${key}]:`, ex); + } +} + +async function clearWorkspaceState(): Promise { + if (_workspaceState !== undefined) { + await Promise.all(_workspaceKeys.map((key) => updateWorkspaceStateValue(key, undefined))); + } +} + +export class PersistentState implements IPersistentState { + constructor( + 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 implements IPersistentState{ } } - public async updateValue(newValue: T): Promise { - if (this.expiryDurationMs) { - await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); - } else { - await this.storage.update(this.key, newValue); + public async updateValue(newValue: T, retryOnce = true): Promise { + try { + if (this.expiryDurationMs) { + await this.storage.update(this.key, { data: newValue, expiry: Date.now() + this.expiryDurationMs }); + } else { + await this.storage.update(this.key, newValue); + } + if (retryOnce && JSON.stringify(this.value) != JSON.stringify(newValue)) { + // Due to a VSCode bug sometimes the changes are not reflected in the storage, atleast not immediately. + // It is noticed however that if we reset the storage first and then update it, it works. + // https://github.com/microsoft/vscode/issues/171827 + await this.updateValue(undefined as any, false); + await this.updateValue(newValue, false); + } + } catch (ex) { + traceError('Error while updating storage for key:', this.key, ex); } } } +export const GLOBAL_PERSISTENT_KEYS_DEPRECATED = 'PYTHON_EXTENSION_GLOBAL_STORAGE_KEYS'; +export const WORKSPACE_PERSISTENT_KEYS_DEPRECATED = 'PYTHON_EXTENSION_WORKSPACE_STORAGE_KEYS'; + +export const GLOBAL_PERSISTENT_KEYS = 'PYTHON_GLOBAL_STORAGE_KEYS'; +const WORKSPACE_PERSISTENT_KEYS = 'PYTHON_WORKSPACE_STORAGE_KEYS'; +type KeysStorageType = 'global' | 'workspace'; +export type KeysStorage = { key: string; defaultValue: unknown }; + @injectable() -export class PersistentStateFactory implements IPersistentStateFactory { - constructor(@inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, - @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento) { } - public createGlobalPersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState { +export class PersistentStateFactory implements IPersistentStateFactory, IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + public readonly _globalKeysStorage = new PersistentState( + this.globalState, + GLOBAL_PERSISTENT_KEYS, + [], + ); + public readonly _workspaceKeysStorage = new PersistentState( + this.workspaceState, + WORKSPACE_PERSISTENT_KEYS, + [], + ); + constructor( + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IMemento) @named(WORKSPACE_MEMENTO) private workspaceState: Memento, + @inject(ICommandManager) private cmdManager?: ICommandManager, + @inject(IExtensionContext) @optional() private context?: IExtensionContext, + ) {} + + public async activate(): Promise { + this.cmdManager?.registerCommand(Commands.ClearStorage, async () => { + await clearWorkspaceState(); + await this.cleanAllPersistentStates(); + if (useEnvExtension()) { + await clearCache(); + } + }); + const globalKeysStorageDeprecated = this.createGlobalPersistentState(GLOBAL_PERSISTENT_KEYS_DEPRECATED, []); + const workspaceKeysStorageDeprecated = this.createWorkspacePersistentState( + WORKSPACE_PERSISTENT_KEYS_DEPRECATED, + [], + ); + // Old storages have grown to be unusually large due to https://github.com/microsoft/vscode-python/issues/17488, + // so reset them. This line can be removed after a while. + if (globalKeysStorageDeprecated.value.length > 0) { + globalKeysStorageDeprecated.updateValue([]).ignoreErrors(); + } + if (workspaceKeysStorageDeprecated.value.length > 0) { + workspaceKeysStorageDeprecated.updateValue([]).ignoreErrors(); + } + } + + public createGlobalPersistentState( + key: string, + defaultValue?: T, + expiryDurationMs?: number, + ): IPersistentState { + this.addKeyToStorage('global', key, defaultValue).ignoreErrors(); return new PersistentState(this.globalState, key, defaultValue, expiryDurationMs); } - public createWorkspacePersistentState(key: string, defaultValue?: T, expiryDurationMs?: number): IPersistentState { + + public createWorkspacePersistentState( + key: string, + defaultValue?: T, + expiryDurationMs?: number, + ): IPersistentState { + this.addKeyToStorage('workspace', key, defaultValue).ignoreErrors(); return new PersistentState(this.workspaceState, key, defaultValue, expiryDurationMs); } + + /** + * Note we use a decorator to cache the promise returned by this method, so it's only called once. + * It is only cached for the particular arguments passed, so the argument type is simplified here. + */ + @cache(-1, true) + private async addKeyToStorage(keyStorageType: KeysStorageType, key: string, defaultValue?: T) { + const storage = keyStorageType === 'global' ? this._globalKeysStorage : this._workspaceKeysStorage; + const found = storage.value.find((value) => value.key === key); + if (!found) { + await storage.updateValue([{ key, defaultValue }, ...storage.value]); + } + } + + private async cleanAllPersistentStates(): Promise { + const clearCacheDirPromise = this.context ? clearCacheDirectory(this.context).catch() : Promise.resolve(); + await Promise.all( + this._globalKeysStorage.value.map(async (keyContent) => { + const storage = this.createGlobalPersistentState(keyContent.key); + await storage.updateValue(keyContent.defaultValue); + }), + ); + await Promise.all( + this._workspaceKeysStorage.value.map(async (keyContent) => { + const storage = this.createWorkspacePersistentState(keyContent.key); + await storage.updateValue(keyContent.defaultValue); + }), + ); + await this._globalKeysStorage.updateValue([]); + await this._workspaceKeysStorage.updateValue([]); + await clearCacheDirPromise; + this.cmdManager?.executeCommand('workbench.action.reloadWindow').then(noop); + } +} + +///////////////////////////// +// a simpler, alternate API +// for components to use + +export interface IPersistentStorage { + get(): T; + set(value: T): Promise; +} + +/** + * Build a global storage object for the given key. + */ +export function getGlobalStorage(context: IExtensionContext, key: string, defaultValue?: T): IPersistentStorage { + const globalKeysStorage = new PersistentState(context.globalState, GLOBAL_PERSISTENT_KEYS, []); + const found = globalKeysStorage.value.find((value) => value.key === key); + if (!found) { + const newValue = [{ key, defaultValue }, ...globalKeysStorage.value]; + globalKeysStorage.updateValue(newValue).ignoreErrors(); + } + const raw = new PersistentState(context.globalState, key, defaultValue); + return { + // We adapt between PersistentState and IPersistentStorage. + get() { + return raw.value; + }, + set(value: T) { + return raw.updateValue(value); + }, + }; } diff --git a/src/client/common/pipes/namedPipes.ts b/src/client/common/pipes/namedPipes.ts new file mode 100644 index 000000000000..9bffe78f2b9f --- /dev/null +++ b/src/client/common/pipes/namedPipes.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as cp from 'child_process'; +import * as crypto from 'crypto'; +import * as fs from 'fs-extra'; +import * as net from 'net'; +import * as os from 'os'; +import * as path from 'path'; +import * as rpc from 'vscode-jsonrpc/node'; +import { CancellationError, CancellationToken, Disposable } from 'vscode'; +import { traceVerbose } from '../../logging'; +import { isWindows } from '../utils/platform'; +import { createDeferred } from '../utils/async'; +import { noop } from '../utils/misc'; + +const { XDG_RUNTIME_DIR } = process.env; +export function generateRandomPipeName(prefix: string): string { + // length of 10 picked because of the name length restriction for sockets + const randomSuffix = crypto.randomBytes(10).toString('hex'); + if (prefix.length === 0) { + prefix = 'python-ext-rpc'; + } + + if (process.platform === 'win32') { + return `\\\\.\\pipe\\${prefix}-${randomSuffix}`; + } + + let result; + if (XDG_RUNTIME_DIR) { + result = path.join(XDG_RUNTIME_DIR, `${prefix}-${randomSuffix}`); + } else { + result = path.join(os.tmpdir(), `${prefix}-${randomSuffix}`); + } + + return result; +} + +async function mkfifo(fifoPath: string): Promise { + return new Promise((resolve, reject) => { + const proc = cp.spawn('mkfifo', [fifoPath]); + proc.on('error', (err) => { + reject(err); + }); + proc.on('exit', (code) => { + if (code === 0) { + resolve(); + } + }); + }); +} + +export async function createWriterPipe(pipeName: string, token?: CancellationToken): Promise { + // windows implementation of FIFO using named pipes + if (isWindows()) { + const deferred = createDeferred(); + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + server.close(); + deferred.resolve(new rpc.SocketMessageWriter(socket, 'utf-8')); + }); + + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + return deferred.promise; + } + // linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const writer = fs.createWriteStream(pipeName, { + encoding: 'utf-8', + }); + return new rpc.StreamMessageWriter(writer, 'utf-8'); +} + +class CombinedReader implements rpc.MessageReader { + private _onError = new rpc.Emitter(); + + private _onClose = new rpc.Emitter(); + + private _onPartialMessage = new rpc.Emitter(); + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + private _callback: rpc.DataCallback = () => {}; + + private _disposables: rpc.Disposable[] = []; + + private _readers: rpc.MessageReader[] = []; + + constructor() { + this._disposables.push(this._onClose, this._onError, this._onPartialMessage); + } + + onError: rpc.Event = this._onError.event; + + onClose: rpc.Event = this._onClose.event; + + onPartialMessage: rpc.Event = this._onPartialMessage.event; + + listen(callback: rpc.DataCallback): rpc.Disposable { + this._callback = callback; + // eslint-disable-next-line no-return-assign, @typescript-eslint/no-empty-function + return new Disposable(() => (this._callback = () => {})); + } + + add(reader: rpc.MessageReader): void { + this._readers.push(reader); + reader.listen((msg) => { + this._callback(msg as rpc.NotificationMessage); + }); + this._disposables.push(reader); + reader.onClose(() => { + this.remove(reader); + if (this._readers.length === 0) { + this._onClose.fire(); + } + }); + reader.onError((e) => { + this.remove(reader); + this._onError.fire(e); + }); + } + + remove(reader: rpc.MessageReader): void { + const found = this._readers.find((r) => r === reader); + if (found) { + this._readers = this._readers.filter((r) => r !== reader); + reader.dispose(); + } + } + + dispose(): void { + this._readers.forEach((r) => r.dispose()); + this._readers = []; + this._disposables.forEach((disposable) => disposable.dispose()); + this._disposables = []; + } +} + +export async function createReaderPipe(pipeName: string, token?: CancellationToken): Promise { + if (isWindows()) { + // windows implementation of FIFO using named pipes + const deferred = createDeferred(); + const combined = new CombinedReader(); + + let refs = 0; + const server = net.createServer((socket) => { + traceVerbose(`Pipe connected: ${pipeName}`); + refs += 1; + + socket.on('close', () => { + refs -= 1; + if (refs <= 0) { + server.close(); + } + }); + combined.add(new rpc.SocketMessageReader(socket, 'utf-8')); + }); + server.on('error', deferred.reject); + server.listen(pipeName); + if (token) { + token.onCancellationRequested(() => { + if (server.listening) { + server.close(); + } + deferred.reject(new CancellationError()); + }); + } + deferred.resolve(combined); + return deferred.promise; + } + // mac/linux implementation of FIFO + await mkfifo(pipeName); + try { + await fs.chmod(pipeName, 0o666); + } catch { + // Intentionally ignored + } + const fd = await fs.open(pipeName, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK); + const socket = new net.Socket({ fd }); + const reader = new rpc.SocketMessageReader(socket, 'utf-8'); + socket.on('close', () => { + fs.close(fd).catch(noop); + reader.dispose(); + }); + + return reader; +} diff --git a/src/client/common/platform/constants.ts b/src/client/common/platform/constants.ts deleted file mode 100644 index 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 8e31446c4774..3e7f441654ec 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -1,196 +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 { 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 { - return new Promise(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; + createDirectory(uri: vscode.Uri): Thenable; + delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable; + readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>; + readFile(uri: vscode.Uri): Thenable; + rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable; + stat(uri: vscode.Uri): Thenable; + writeFile(uri: vscode.Uri, content: Uint8Array): Thenable; +} + +// This is the parts of the 'fs-extra' module that we use in RawFileSystem. +interface IRawFSExtra { + lstat(filename: string): Promise; + chmod(filePath: string, mode: string | number): Promise; + appendFile(filename: string, data: unknown): Promise; + + // non-async + lstatSync(filename: string): fs.Stats; + statSync(filename: string): fs.Stats; + readFileSync(path: string, encoding: string): string; + createReadStream(filename: string): ReadStream; + createWriteStream(filename: string): WriteStream; + pathExists(filename: string): Promise; +} + +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 { - return this.objectExists(filePath, (stats) => stats.isFile()); + public async pathExists(filename: string): Promise { + 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} - * @memberof FileSystem - */ - public readFile(filePath: string): Promise { - return fs.readFile(filePath).then(buffer => buffer.toString()); - } - - public async writeFile(filePath: string, data: {}, options: string | fs.WriteFileOptions = { encoding: 'utf8' }): Promise { - await fs.writeFile(filePath, data, options); - } - - public directoryExists(filePath: string): Promise { - return this.objectExists(filePath, (stats) => stats.isDirectory()); - } - - public createDirectory(directoryPath: string): Promise { - return fs.mkdirp(directoryPath); - } - - public deleteDirectory(directoryPath: string): Promise { - const deferred = createDeferred(); - fs.rmdir(directoryPath, err => err ? deferred.reject(err) : deferred.resolve()); - return deferred.promise; - } - - public getSubDirectories(rootDir: string): Promise { - return new Promise(resolve => { - fs.readdir(rootDir, async (error, files) => { - if (error) { - return resolve([]); - } - const subDirs = ( - await Promise.all( - files.map(async name => { - const fullPath = path.join(rootDir, name); - try { - if ((await fs.stat(fullPath)).isDirectory()) { - return fullPath; - } - // tslint:disable-next-line:no-empty - } catch (ex) { } - }) - )) - .filter(dir => dir !== undefined) as string[]; - resolve(subDirs); - }); - }); + + public async stat(filename: string): Promise { + // Note that, prior to the November release of VS Code, + // stat.ctime was always 0. + // See: https://github.com/microsoft/vscode/issues/84525 + const uri = vscode.Uri.file(filename); + return this.vscfs.stat(uri); } - public async getFiles(rootDir: string): Promise { - 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 { + // 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 { + // TODO (https://github.com/microsoft/vscode/issues/73122 (84513)): + // This functionality has been requested for the VS Code API. + return this.fsExtra.chmod(filename, mode); + } + + public async move(src: string, tgt: string): Promise { + const srcUri = vscode.Uri.file(src); + const tgtUri = vscode.Uri.file(tgt); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(tgt))); + // We stick with the pre-existing behavior where files are + // overwritten and directories are not. + const options = { overwrite: false }; + try { + await this.vscfs.rename(srcUri, tgtUri, options); + } catch (err) { + if (!isFileExistsError(err)) { + throw err; // re-throw + } + const stat = await this.vscfs.stat(tgtUri); + if (stat.type === FileType.Directory) { + throw err; // re-throw + } + options.overwrite = true; + await this.vscfs.rename(srcUri, tgtUri, options); } } - public 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 { + const uri = vscode.Uri.file(filename); + const data = await this.vscfs.readFile(uri); + return Buffer.from(data); } - public getRealPath(filePath: string): Promise { - return new Promise(resolve => { - fs.realpath(filePath, (err, realPath) => { - resolve(err ? filePath : realPath); - }); - }); + public async readText(filename: string): Promise { + const uri = vscode.Uri.file(filename); + const result = await this.vscfs.readFile(uri); + const data = Buffer.from(result); + return data.toString(ENCODING); } - public copyFile(src: string, dest: string): Promise { - const deferred = createDeferred(); - 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 { + const uri = vscode.Uri.file(filename); + const data = Buffer.from(text); + await this.vscfs.writeFile(uri, data); + } + + public async appendText(filename: string, text: string): Promise { + // TODO: We *could* use the new API for this. + // See https://github.com/microsoft/vscode-python/issues/9900 + return this.fsExtra.appendFile(filename, text); + } + + public async copyFile(src: string, dest: string): Promise { + const srcURI = vscode.Uri.file(src); + const destURI = vscode.Uri.file(dest); + // The VS Code API will automatically create the target parent + // directory if it does not exist (even though the docs imply + // otherwise). So we have to manually stat, just to be sure. + // Note that this behavior was reported, but won't be changing. + // See: https://github.com/microsoft/vscode/issues/84177 + await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(dest))); + await this.vscfs.copy(srcURI, destURI, { + overwrite: true, }); - rs.pipe(ws); - return deferred.promise; - } - - public deleteFile(filename: string): Promise { - const deferred = createDeferred(); - fs.unlink(filename, err => err ? deferred.reject(err) : deferred.resolve()); - return deferred.promise; - } - - public getFileHash(filePath: string): Promise { - return new Promise(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 { + const uri = vscode.Uri.file(filename); + return this.vscfs.delete(uri, { + recursive: false, + useTrash: false, }); } - public search(globPattern: string): Promise { - return new Promise((resolve, reject) => { - glob(globPattern, (ex, files) => { - if (ex) { - return reject(ex); - } - resolve(Array.isArray(files) ? files : []); - }); + + public async rmdir(dirname: string): Promise { + const uri = vscode.Uri.file(dirname); + // The "recursive" option disallows directories, even if they + // are empty. So we have to deal with this ourselves. + const files = await this.vscfs.readDirectory(uri); + if (files && files.length > 0) { + throw createDirNotEmptyError(dirname); + } + return this.vscfs.delete(uri, { + recursive: true, + useTrash: false, }); } - public createTemporaryFile(extension: string): Promise { - return new Promise((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 { + 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 { + const uri = vscode.Uri.file(dirname); + await this.vscfs.createDirectory(uri); } - public chmod(filePath: string, mode: string): Promise { - return new Promise((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, + ) {} + + // Create a new object using common-case default values. + public static withDefaults( + raw?: IRawFileSystem, + pathUtils?: IFileSystemPathUtils, + tmp?: ITempFileSystem, + getHash?: (data: string) => string, + globFiles?: (pat: string, options?: { cwd: string }) => Promise, + ): FileSystemUtils { + pathUtils = pathUtils || FileSystemPathUtils.withDefaults(); + return new FileSystemUtils( + raw || RawFileSystem.withDefaults(pathUtils.paths), + pathUtils, + pathUtils.paths, + tmp || TemporaryFileSystem.withDefaults(), + getHash || getHashString, + globFiles || promisify(glob.default), + ); + } + + // aliases + + public async createDirectory(directoryPath: string): Promise { + return this.raw.mkdirp(directoryPath); + } + + public async deleteDirectory(directoryPath: string): Promise { + return this.raw.rmdir(directoryPath); + } + + public async deleteFile(filename: string): Promise { + return this.raw.rmfile(filename); + } + + // helpers + + public async pathExists( + // the "file" to look for + filename: string, + // the file type to expect; if not provided then any file type + // matches; otherwise a mismatch results in a "false" value + fileType?: FileType, + ): Promise { + 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 { + return this.pathExists(filename, FileType.File); + } + + public async directoryExists(dirname: string): Promise { + return this.pathExists(dirname, FileType.Directory); + } + + public async listdir(dirname: string): Promise<[string, FileType][]> { + try { + return await this.raw.listdir(dirname); + } catch (err) { + // We're only preserving pre-existng behavior here... + if (!(await this.pathExists(dirname))) { + return []; + } + throw err; // re-throw + } + } + + public async getSubDirectories(dirname: string): Promise { + const files = await this.listdir(dirname); + const filtered = filterByFileType(files, FileType.Directory); + return filtered.map(([filename, _fileType]) => filename); + } + + public async getFiles(dirname: string): Promise { + // Note that only "regular" files are returned. + const files = await this.listdir(dirname); + const filtered = filterByFileType(files, FileType.File); + return filtered.map(([filename, _fileType]) => filename); + } + + public async isDirReadonly(dirname: string): Promise { + const filePath = `${dirname}${this.paths.sep}___vscpTest___`; + try { + await this.raw.stat(dirname); + await this.raw.writeText(filePath, ''); + } catch (err) { + if (isNoPermissionsError(err)) { + return true; + } + throw err; // re-throw + } + this.raw + .rmfile(filePath) + // Clean resources in the background. + .ignoreErrors(); + return false; + } + + public async getFileHash(filename: string): Promise { + // The reason for lstat rather than stat is not clear... + const stat = await this.raw.lstat(filename); + const data = `${stat.ctime}-${stat.mtime}`; + return this.getHash(data); + } + + public async search(globPattern: string, cwd?: string, dot?: boolean): Promise { + // 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 { + return this.utils.raw.stat(filename); + } + + public async createDirectory(dirname: string): Promise { + return this.utils.createDirectory(dirname); + } + + public async deleteDirectory(dirname: string): Promise { + return this.utils.deleteDirectory(dirname); + } + + public async listdir(dirname: string): Promise<[string, FileType][]> { + return this.utils.listdir(dirname); + } + + public async readFile(filePath: string): Promise { + return this.utils.raw.readText(filePath); + } + + public async readData(filePath: string): Promise { + return this.utils.raw.readData(filePath); + } + + // eslint-disable-next-line @typescript-eslint/ban-types + public async writeFile(filename: string, data: string | Buffer): Promise { + return this.utils.raw.writeText(filename, data); + } + + public async appendFile(filename: string, text: string): Promise { + return this.utils.raw.appendText(filename, text); + } + + public async copyFile(src: string, dest: string): Promise { + return this.utils.raw.copyFile(src, dest); + } + + public async deleteFile(filename: string): Promise { + return this.utils.deleteFile(filename); + } + + public async chmod(filename: string, mode: string): Promise { + return this.utils.raw.chmod(filename, mode); + } + + public async move(src: string, tgt: string): Promise { + await this.utils.raw.move(src, tgt); + } + + public readFileSync(filePath: string): string { + return this.utils.raw.readTextSync(filePath); + } + + public createReadStream(filePath: string): ReadStream { + return this.utils.raw.createReadStream(filePath); + } + + public createWriteStream(filePath: string): WriteStream { + return this.utils.raw.createWriteStream(filePath); + } + + public async fileExists(filename: string): Promise { + return this.utils.fileExists(filename); + } + + public pathExists(filename: string): Promise { + return this.utils.pathExists(filename); + } + + public fileExistsSync(filename: string): boolean { + return this.utils.fileExistsSync(filename); + } + + public async directoryExists(dirname: string): Promise { + return this.utils.directoryExists(dirname); + } + + public async getSubDirectories(dirname: string): Promise { + return this.utils.getSubDirectories(dirname); + } + + public async getFiles(dirname: string): Promise { + return this.utils.getFiles(dirname); + } + + public async getFileHash(filename: string): Promise { + return this.utils.getFileHash(filename); + } + + public async search(globPattern: string, cwd?: string, dot?: boolean): Promise { + return this.utils.search(globPattern, cwd, dot); + } + + public async createTemporaryFile(suffix: string, mode?: number): Promise { + return this.utils.tmp.createFile(suffix, mode); + } + + public async isDirReadonly(dirname: string): Promise { + return this.utils.isDirReadonly(dirname); + } } diff --git a/src/client/common/platform/fileSystemWatcher.ts b/src/client/common/platform/fileSystemWatcher.ts new file mode 100644 index 000000000000..ef35988d147b --- /dev/null +++ b/src/client/common/platform/fileSystemWatcher.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { RelativePattern, workspace } from 'vscode'; +import { traceVerbose } from '../../logging'; +import { IDisposable } from '../types'; +import { Disposables } from '../utils/resourceLifecycle'; + +/** + * Enumeration of file change types. + */ +export enum FileChangeType { + Changed = 'changed', + Created = 'created', + Deleted = 'deleted', +} + +export function watchLocationForPattern( + baseDir: string, + pattern: string, + callback: (type: FileChangeType, absPath: string) => void, +): IDisposable { + const globPattern = new RelativePattern(baseDir, pattern); + const disposables = new Disposables(); + traceVerbose(`Start watching: ${baseDir} with pattern ${pattern} using VSCode API`); + const watcher = workspace.createFileSystemWatcher(globPattern); + disposables.push(watcher.onDidCreate((e) => callback(FileChangeType.Created, e.fsPath))); + disposables.push(watcher.onDidChange((e) => callback(FileChangeType.Changed, e.fsPath))); + disposables.push(watcher.onDidDelete((e) => callback(FileChangeType.Deleted, e.fsPath))); + return disposables; +} diff --git a/src/client/common/platform/fs-paths.ts b/src/client/common/platform/fs-paths.ts 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 { + const destDir = nodepath.dirname(dest); + if (!(await fs.pathExists(destDir))) { + await fs.mkdirp(destDir); + } + + await fs.copy(src, dest, { + overwrite: true, + }); +} + +// These function exist so we can stub them out in tests. We can't stub out the fs module directly +// because of the way that sinon does stubbing, so we have these intermediaries instead. +export { Stats, WriteStream, ReadStream, PathLike, Dirent, PathOrFileDescriptor } from 'fs-extra'; + +export function existsSync(path: string): boolean { + return fs.existsSync(path); +} + +export function readFileSync(filePath: string, encoding: BufferEncoding): string; +export function readFileSync(filePath: string): Buffer; +export function readFileSync(filePath: string, options: { encoding: BufferEncoding }): string; +export function readFileSync( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): string | Buffer { + if (typeof options === 'string') { + return fs.readFileSync(filePath, { encoding: options }); + } + return fs.readFileSync(filePath, options); +} + +export function readJSONSync(filePath: string): any { + return fs.readJSONSync(filePath); +} + +export function readdirSync(path: string): string[]; +export function readdirSync( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): fs.Dirent[]; +export function readdirSync( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: false; + }, +): string[]; +export function readdirSync( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: boolean; + recursive?: boolean | undefined; + }, +): string[] | fs.Dirent[] { + if (options === undefined || options.withFileTypes === false) { + return fs.readdirSync(path); + } + return fs.readdirSync(path, { ...options, withFileTypes: true }); +} + +export function readlink(path: string): Promise { + return fs.readlink(path); +} + +export function unlink(path: string): Promise { + return fs.unlink(path); +} + +export function symlink(target: string, path: string, type?: fs.SymlinkType): Promise { + return fs.symlink(target, path, type); +} + +export function symlinkSync(target: string, path: string, type?: fs.SymlinkType): void { + return fs.symlinkSync(target, path, type); +} + +export function unlinkSync(path: string): void { + return fs.unlinkSync(path); +} + +export function statSync(path: string): fs.Stats { + return fs.statSync(path); +} + +export function stat(path: string): Promise { + return fs.stat(path); +} + +export function lstat(path: string): Promise { + return fs.lstat(path); +} + +export function chmod(path: string, mod: fs.Mode): Promise { + return fs.chmod(path, mod); +} + +export function createReadStream(path: string): fs.ReadStream { + return fs.createReadStream(path); +} + +export function createWriteStream(path: string): fs.WriteStream { + return fs.createWriteStream(path); +} + +export function pathExistsSync(path: string): boolean { + return fs.pathExistsSync(path); +} + +export function pathExists(absPath: string): Promise { + return fs.pathExists(absPath); +} + +export function createFile(filename: string): Promise { + return fs.createFile(filename); +} + +export function rmdir(path: string, options?: fs.RmDirOptions): Promise { + return fs.rmdir(path, options); +} + +export function remove(path: string): Promise { + return fs.remove(path); +} + +export function readFile(filePath: string, encoding: BufferEncoding): Promise; +export function readFile(filePath: string): Promise; +export function readFile(filePath: string, options: { encoding: BufferEncoding }): Promise; +export function readFile( + filePath: string, + options?: { encoding: BufferEncoding } | BufferEncoding | undefined, +): Promise { + if (typeof options === 'string') { + return fs.readFile(filePath, { encoding: options }); + } + return fs.readFile(filePath, options); +} + +export function readJson(filePath: string): Promise { + return fs.readJson(filePath); +} + +export function writeFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise { + return fs.writeFile(filePath, data, options); +} + +export function mkdir(dirPath: string): Promise { + return fs.mkdir(dirPath); +} + +export function mkdirp(dirPath: string): Promise { + return fs.mkdirp(dirPath); +} + +export function rename(oldPath: string, newPath: string): Promise { + return fs.rename(oldPath, newPath); +} + +export function ensureDir(dirPath: string): Promise { + return fs.ensureDir(dirPath); +} + +export function ensureFile(filePath: string): Promise { + return fs.ensureFile(filePath); +} + +export function ensureSymlink(target: string, filePath: string, type?: fs.SymlinkType): Promise { + return fs.ensureSymlink(target, filePath, type); +} + +export function appendFile(filePath: string, data: any, options?: { encoding: BufferEncoding }): Promise { + return fs.appendFile(filePath, data, options); +} + +export function readdir(path: string): Promise; +export function readdir( + path: string, + options: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise; +export function readdir( + path: fs.PathLike, + options?: fs.ObjectEncodingOptions & { + withFileTypes: true; + }, +): Promise { + if (options === undefined) { + return fs.readdir(path); + } + return fs.readdir(path, options); +} + +export function emptyDir(dirPath: string): Promise { + return fs.emptyDir(dirPath); +} diff --git a/src/client/common/platform/fs-temp.ts b/src/client/common/platform/fs-temp.ts 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 { + const opts = { + postfix: suffix, + mode, + }; + return new Promise((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 65c1623dd3eb..dc9b04cc652c 100644 --- a/src/client/common/platform/platformService.ts +++ b/src/client/common/platform/platformService.ts @@ -1,27 +1,30 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; import { injectable } from 'inversify'; import * as os from 'os'; import { coerce, SemVer } from 'semver'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName, PlatformErrors } from '../../telemetry/constants'; -import { 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'; } + public async getVersion(): Promise { if (this.version) { return this.version; @@ -35,47 +38,38 @@ export class PlatformService implements IPlatformService { try { const ver = coerce(os.release()); if (ver) { - sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { osVersion: `${ver.major}.${ver.minor}.${ver.patch}` }); - return this.version = ver; + this.version = ver; + return this.version; } throw new Error('Unable to parse version'); } catch (ex) { - sendTelemetryEvent(EventName.PLATFORM_INFO, undefined, { failureType: PlatformErrors.FailedToParseVersion }); - return parseVersion(os.release()); + return parseSemVerSafe(os.release()); } default: throw new Error('Not Supported'); } } + // eslint-disable-next-line class-methods-use-this public get isWindows(): boolean { - return this.osType === OSType.Windows; + return isWindows(); } + public get isMac(): boolean { return this.osType === OSType.OSX; } + public get isLinux(): boolean { return this.osType === OSType.Linux; } + + // eslint-disable-next-line class-methods-use-this public get osRelease(): string { return os.release(); } - public get is64bit(): boolean { - // tslint:disable-next-line:no-require-imports - const arch = require('arch'); - return arch() === 'x64'; - } -} -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(EventName.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 f612682c23c5..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(resolve => { + return new Promise((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 { - // tslint:disable-next-line:no-require-imports const Registry = require('winreg') as typeof import('winreg'); // https://github.com/python/peps/blob/master/pep-0514.txt#L85 - return new Promise(resolve => { + return new Promise((resolve) => { new Registry(options).keys((error, result) => { if (error || !Array.isArray(result)) { return resolve([]); } - resolve(result.filter(item => typeof item.key === 'string').map(item => item.key)); + 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 d14a5001cef1..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,6 +27,9 @@ export interface IRegistry { getValue(key: string, hive: RegistryHive, arch?: Architecture, name?: string): Promise; } +//= ========================== +// platform + export const IPlatformService = Symbol('IPlatformService'); export interface IPlatformService { readonly osType: OSType; @@ -32,33 +45,186 @@ export interface IPlatformService { getVersion(): Promise; } -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; +} + +//= ========================== +// 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; + // Get information about a file (resolve symlinks). + stat(filename: string): Promise; + // Get information about a file (do not resolve synlinks). + lstat(filename: string): Promise; + // Change a file's permissions. + chmod(filename: string, mode: string | number): Promise; + // Move the file to a different location (and/or rename it). + move(src: string, tgt: string): Promise; + + //* ********************** + // files + + // Return the raw bytes of the given file. + readData(filename: string): Promise; + // Return the text of the given file (decoded from UTF-8). + readText(filename: string): Promise; + // Write the given text to the file (UTF-8 encoded). + writeText(filename: string, data: string | Buffer): Promise; + // Write the given text to the end of the file (UTF-8 encoded). + appendText(filename: string, text: string): Promise; + // Copy a file. + copyFile(src: string, dest: string): Promise; + // Delete a file. + rmfile(filename: string): Promise; + + //* ********************** + // directories + + // Create the directory and any missing parent directories. + mkdirp(dirname: string): Promise; + // Delete the directory if empty. + rmdir(dirname: string): Promise; + // Delete the directory and everything in it. + rmtree(dirname: string): Promise; + // Return the contents of the directory. + listdir(dirname: string): Promise<[string, FileType][]>; + + //* ********************** + // not async + + // Get information about a file (resolve symlinks). + statSync(filename: string): FileStat; + // Return the text of the given file (decoded from UTF-8). + readTextSync(filename: string): string; + // Create a streaming wrappr around an open file (for reading). + createReadStream(filename: string): ReadStream; + // Create a streaming wrappr around an open file (for writing). + createWriteStream(filename: string): WriteStream; +} + +// High-level filesystem operations used by the extension. +export interface IFileSystemUtils { + readonly raw: IRawFileSystem; + readonly paths: IFileSystemPaths; + readonly pathUtils: IFileSystemPathUtils; + readonly tmp: ITempFileSystem; + + //* ********************** + // aliases + + createDirectory(dirname: string): Promise; + deleteDirectory(dirname: string): Promise; + deleteFile(filename: string): Promise; + + //* ********************** + // helpers + + // Determine if the file exists, optionally requiring the type. + pathExists(filename: string, fileType?: FileType): Promise; + // Determine if the regular file exists. + fileExists(filename: string): Promise; + // Determine if the directory exists. + directoryExists(dirname: string): Promise; + // Get all the directory's entries. + listdir(dirname: string): Promise<[string, FileType][]>; + // Get the paths of all immediate subdirectories. + getSubDirectories(dirname: string): Promise; + // Get the paths of all immediately contained files. + getFiles(dirname: string): Promise; + // Determine if the directory is read-only. + isDirReadonly(dirname: string): Promise; + // Generate the sha512 hash for the file (based on timestamps). + getFileHash(filename: string): Promise; + // Get the paths of all files matching the pattern. + search(globPattern: string): Promise; + + //* ********************** + // helpers (non-async) + + fileExistsSync(path: string): boolean; +} + +// 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; - fileExists(path: string): Promise; - fileExistsSync(path: string): boolean; - directoryExists(path: string): Promise; + arePathsSame(path1: string, path2: string): boolean; + getDisplayName(path: string): string; + + // "raw" operations + stat(filePath: string): Promise; createDirectory(path: string): Promise; deleteDirectory(path: string): Promise; - getSubDirectories(rootDir: string): Promise; - getFiles(rootDir: string): Promise; - arePathsSame(path1: string, path2: string): boolean; + listdir(dirname: string): Promise<[string, FileType][]>; readFile(filePath: string): Promise; - writeFile(filePath: string, data: {}, options?: string | fsextra.WriteFileOptions): Promise; - 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; + readData(filePath: string): Promise; + writeFile(filePath: string, text: string | Buffer, options?: string | fsextra.WriteFileOptions): Promise; + appendFile(filename: string, text: string | Buffer): Promise; copyFile(src: string, dest: string): Promise; deleteFile(filename: string): Promise; - getFileHash(filePath: string): Promise; - search(globPattern: string): Promise; - createTemporaryFile(extension: string): Promise; + chmod(path: string, mode: string | number): Promise; + move(src: string, tgt: string): Promise; + // sync + readFileSync(filename: string): string; + createReadStream(path: string): fs.ReadStream; createWriteStream(path: string): fs.WriteStream; - chmod(path: string, mode: string): Promise; + + // utils + pathExists(path: string): Promise; + fileExists(path: string): Promise; + fileExistsSync(path: string): boolean; + directoryExists(path: string): Promise; + getSubDirectories(rootDir: string): Promise; + getFiles(rootDir: string): Promise; + getFileHash(filePath: string): Promise; + search(globPattern: string, cwd?: string, dot?: boolean): Promise; + createTemporaryFile(extension: string, mode?: number): Promise; + isDirReadonly(dirname: string): Promise; } diff --git a/src/client/common/process/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 f046aac94813..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, execSync, spawn } from 'child_process'; -import { Observable } from 'rxjs/Observable'; +import { EventEmitter } from 'events'; +import { traceError } from '../../logging'; import { IDisposable } from '../types'; -import { createDeferred } from '../utils/async'; import { EnvironmentVariables } from '../variables/types'; -import { DEFAULT_ENCODING } from './constants'; -import { - ExecutionResult, - IBufferDecoder, - IProcessService, - ObservableExecutionResult, - Output, - ShellOptions, - SpawnOptions, - StdErrError -} from './types'; +import { execObservable, killPid, plainExec, shellExec } from './rawProcessApis'; +import { ExecutionResult, IProcessService, ObservableExecutionResult, ShellOptions, SpawnOptions } from './types'; +import { workerPlainExec, workerShellExec } from './worker/rawProcessApiWrapper'; -// tslint:disable:no-any -export class ProcessService implements IProcessService, IDisposable { +export class ProcessService extends EventEmitter implements IProcessService { private processesToKill = new Set(); - constructor(private readonly decoder: IBufferDecoder, private readonly env?: EnvironmentVariables) { } + + constructor(private readonly env?: EnvironmentVariables) { + super(); + } + public static isAlive(pid: number): boolean { try { process.kill(pid, 0); @@ -30,20 +24,14 @@ export class ProcessService implements IProcessService, IDisposable { return false; } } + public static kill(pid: number): void { - try { - if (process.platform === 'win32') { - // Windows doesn't support SIGTERM, so execute taskkill to kill the process - execSync(`taskkill /pid ${pid} /T /F`); - } else { - process.kill(pid); - } - } catch { - // Ignore. - } + killPid(pid); } - public dispose() { - this.processesToKill.forEach(p => { + + public dispose(): void { + this.removeAllListeners(); + this.processesToKill.forEach((p) => { try { p.dispose(); } catch { @@ -53,178 +41,38 @@ export class ProcessService implements IProcessService, IDisposable { } public execObservable(file: string, args: string[], options: SpawnOptions = {}): ObservableExecutionResult { - const spawnOptions = this.getDefaultOptions(options); - const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; - const proc = spawn(file, args, spawnOptions); - let procExited = false; - const disposable : IDisposable = { - dispose: () => { - if (proc && !proc.killed && !procExited) { - ProcessService.kill(proc.pid); - } - if (proc) { - proc.unref(); - } - } - }; - this.processesToKill.add(disposable); - - const output = new Observable>(subscriber => { - const disposables: IDisposable[] = []; - - const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { - ee.on(name, fn as any); - disposables.push({ dispose: () => ee.removeListener(name, fn as any) as any }); - }; - - if (options.token) { - disposables.push(options.token.onCancellationRequested(() => { - if (!procExited && !proc.killed) { - proc.kill(); - procExited = true; - } - })); - } - - const sendOutput = (source: 'stdout' | 'stderr', data: Buffer) => { - const out = this.decoder.decode([data], encoding); - if (source === 'stderr' && options.throwOnStdErr) { - subscriber.error(new StdErrError(out)); - } else { - subscriber.next({ source, out: out }); - } - }; - - on(proc.stdout, 'data', (data: Buffer) => sendOutput('stdout', data)); - on(proc.stderr, 'data', (data: Buffer) => sendOutput('stderr', data)); - - proc.once('close', () => { - procExited = true; - subscriber.complete(); - disposables.forEach(d => d.dispose()); - }); - proc.once('exit', () => { - procExited = true; - subscriber.complete(); - disposables.forEach(d => d.dispose()); - }); - proc.once('error', ex => { - procExited = true; - subscriber.error(ex); - disposables.forEach(d => d.dispose()); - }); - }); - - return { - proc, - out: output, - dispose: disposable.dispose - }; + const execOptions = { ...options, doNotLog: true }; + const result = execObservable(file, args, execOptions, this.env, this.processesToKill); + this.emit('exec', file, args, options); + return result; } - public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { - const spawnOptions = this.getDefaultOptions(options); - const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; - const proc = spawn(file, args, spawnOptions); - const deferred = createDeferred>(); - const disposable : IDisposable = { - dispose: () => { - if (!proc.killed && !deferred.completed) { - proc.kill(); - } - } - }; - this.processesToKill.add(disposable); - const disposables: IDisposable[] = []; - const on = (ee: NodeJS.EventEmitter, name: string, fn: Function) => { - ee.on(name, fn as any); - disposables.push({ dispose: () => ee.removeListener(name, fn as any) as any }); - }; - - if (options.token) { - disposables.push(options.token.onCancellationRequested(disposable.dispose)); + public exec(file: string, args: string[], options: SpawnOptions = {}): Promise> { + 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(d => d.dispose()); - }); - proc.once('error', ex => { - deferred.reject(ex); - disposables.forEach(d => d.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> { - const shellOptions = this.getDefaultOptions(options); - return new Promise((resolve, reject) => { - const proc = exec(command, shellOptions, (e, stdout, stderr) => { - if (e && e !== null) { - reject(e); - } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { - reject(new Error(stderr)); - } else { - // Make sure stderr is undefined if we actually had none. This is checked - // elsewhere because that's how exec behaves. - resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout: stdout }); + this.emit('exec', command, undefined, options); + if (options.useWorker) { + return workerShellExec(command, options); + } + const disposables = new Set(); + const shellOptions = { ...options, doNotLog: true }; + return shellExec(command, shellOptions, this.env, disposables).finally(() => { + // Ensure the process we started is cleaned up. + disposables.forEach((p) => { + try { + p.dispose(); + } catch { + traceError(`Unable to kill process for ${command}`); } }); - const disposable : IDisposable = { - dispose: () => { - if (!proc.killed) { - proc.kill(); - } - } - }; - this.processesToKill.add(disposable); }); } - - private getDefaultOptions(options: T): T { - const defaultOptions = { ...options }; - const execOptions = defaultOptions as SpawnOptions; - if (execOptions) { - const encoding = execOptions.encoding = typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 ? execOptions.encoding : DEFAULT_ENCODING; - delete execOptions.encoding; - execOptions.encoding = encoding; - } - if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { - const env = this.env ? this.env : process.env; - defaultOptions.env = { ...env }; - } else { - defaultOptions.env = { ...defaultOptions.env }; - } - - // 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 17fbf6bbbe18..40204a640dae 100644 --- a/src/client/common/process/processFactory.ts +++ b/src/client/common/process/processFactory.ts @@ -5,24 +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); - } - public async create(resource?: Uri): Promise { - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); - const decoder = this.serviceContainer.get(IBufferDecoder); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const proc = new ProcessService(decoder, customEnvVars); - disposableRegistry.push(proc); - return proc; + 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 { + 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> = new Map>(); + +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; + // from ProcessService: + exec(file: string, args: string[]): Promise>; + shellExec(command: string, options?: ShellOptions): Promise>; + }, + ) {} + + 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 { + if (this.cachedInterpreterInformation === null) { + this.cachedInterpreterInformation = await this.getInterpreterInformationImpl(); + } + return this.cachedInterpreterInformation; + } + + public async getExecutablePath(): Promise { + // If we've passed the python file, then return the file. + // This is because on mac if using the interpreter /usr/bin/python2.7 we can get a different value for the path + if (await this.deps.isValidExecutable(this.pythonPath)) { + return this.pythonPath; + } + const 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 { + const [args, parse] = internalPython.getModuleVersion(moduleName); + const info = this.getExecutionInfo(args); + let data: ExecutionResult; + try { + data = await this.deps.exec(info.command, info.args); + } catch (ex) { + traceVerbose(`Error when getting version of module ${moduleName}`, ex); + return undefined; + } + return parse(data.stdout); + } + + public async isModuleInstalled(moduleName: string): Promise { + // 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 { + 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, + pythonArgv: string[] | undefined, + observablePythonArgv: string[] | undefined, + // from ProcessService: + exec: (file: string, args: string[], options?: SpawnOptions) => Promise>, + shellExec: (command: string, options?: ShellOptions) => Promise>, +) { + return { + getPythonArgv: (python: string) => { + 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 { + 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 { + 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 1fe1ee7fb9e2..efb05c3c9d12 100644 --- a/src/client/common/process/pythonExecutionFactory.ts +++ b/src/client/common/process/pythonExecutionFactory.ts @@ -3,44 +3,189 @@ import { inject, injectable } from 'inversify'; import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { IActivatedEnvironmentLaunch, IComponentAdapter } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IConfigurationService, IDisposableRegistry } from '../types'; +import { IFileSystem } from '../platform/types'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from '../types'; import { ProcessService } from './proc'; -import { PythonExecutionService } from './pythonProcess'; +import { createCondaEnv, createPythonEnv, createMicrosoftStoreEnv, createPixiEnv } from './pythonEnvironment'; +import { createPythonProcessService } from './pythonProcess'; import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionFactoryCreationOptions, - IBufferDecoder, + IProcessLogger, + IProcessService, IProcessServiceFactory, + IPythonEnvironment, IPythonExecutionFactory, - IPythonExecutionService + 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 { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, + 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(IBufferDecoder) private readonly decoder: IBufferDecoder) { + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IInterpreterAutoSelectionService) private readonly autoSelection: IInterpreterAutoSelectionService, + @inject(IInterpreterPathService) private readonly interpreterPathExpHelper: IInterpreterPathService, + ) { + // Acquire other objects here so that if we are called during dispose they are available. + this.disposables = this.serviceContainer.get(IDisposableRegistry); + this.logger = this.serviceContainer.get(IProcessLogger); + this.fileSystem = this.serviceContainer.get(IFileSystem); } + public async create(options: ExecutionFactoryCreationOptions): Promise { - const pythonPath = options.pythonPath ? options.pythonPath : this.configService.getSettings(options.resource).pythonPath; - 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, + ); + 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 { - const envVars = await this.activationHelper.getActivatedEnvironmentVariables(options.resource, options.interpreter, options.allowEnvironmentFetchExceptions); + + public async createActivatedEnvironment( + options: ExecutionFactoryCreateWithEnvironmentOptions, + ): Promise { + const envVars = await this.activationHelper.getActivatedEnvironmentVariables( + options.resource, + options.interpreter, + options.allowEnvironmentFetchExceptions, + ); const hasEnvVars = envVars && Object.keys(envVars).length > 0; sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, undefined, { hasEnvVars }); if (!hasEnvVars) { - return this.create({ resource: options.resource, pythonPath: options.interpreter ? options.interpreter.path : undefined }); + 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 pythonPath = options.interpreter ? options.interpreter.path : this.configService.getSettings(options.resource).pythonPath; - const processService = new ProcessService(this.decoder, { ...envVars }); - this.serviceContainer.get(IDisposableRegistry).push(processService); - return new PythonExecutionService(this.serviceContainer, processService, pythonPath); + + 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 { + const condaLocatorService = this.serviceContainer.get(IComponentAdapter); + const [condaEnvironment] = await Promise.all([condaLocatorService.getCondaEnvironment(pythonPath)]); + if (!condaEnvironment) { + return undefined; + } + const env = await createCondaEnv(condaEnvironment, processService, this.fileSystem); + if (!env) { + return undefined; + } + return createPythonService(processService, env); } + + public async createPixiExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise { + const pixiEnvironment = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnvironment) { + return undefined; + } + + const env = await createPixiEnv(pixiEnvironment, processService, this.fileSystem); + if (env) { + return createPythonService(processService, env); + } + + return undefined; + } +} + +function createPythonService(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); - } - - public async getInterpreterInformation(): Promise { - 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 { - // 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 { - 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; + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; + getExecutionObservableInfo(pythonArgs?: string[]): PythonExecInfo; + // from ProcessService: + exec(file: string, args: string[], options: SpawnOptions): Promise>; + execObservable(file: string, args: string[], options: SpawnOptions): ObservableExecutionResult; + }, + ) {} public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult { const opts: SpawnOptions = { ...options }; - 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 { + + public execModuleObservable( + moduleName: string, + moduleArgs: string[], + options: SpawnOptions, + ): ObservableExecutionResult { + 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> { 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> { + + public async execModule( + moduleName: string, + moduleArgs: string[], + options: SpawnOptions, + ): Promise> { + 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> { + 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> { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} + public async execObservable( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise> { if (options.env) { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); return pythonExecutionService.execModuleObservable(executionInfo.moduleName, executionInfo.args, options); } else { - const processService = await this.serviceContainer.get(IProcessServiceFactory).create(resource); + const processService = await this.serviceContainer + .get(IProcessServiceFactory) + .create(resource); return processService.execObservable(executionInfo.execPath!, executionInfo.args, { ...options }); } } - public async exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise> { + public async exec( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise> { if (options.env) { throw new Error('Environment variables are not supported'); } if (executionInfo.moduleName && executionInfo.moduleName.length > 0) { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); - return pythonExecutionService.execModule(executionInfo.moduleName!, executionInfo.args, options); + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + return pythonExecutionService.execModule(executionInfo.moduleName, executionInfo.args, options); } else { - const processService = await this.serviceContainer.get(IProcessServiceFactory).create(resource); + const processService = await this.serviceContainer + .get(IProcessServiceFactory) + .create(resource); return processService.exec(executionInfo.execPath!, executionInfo.args, { ...options }); } } + + public async execForLinter( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise> { + if (options.env) { + throw new Error('Environment variables are not supported'); + } + const pythonExecutionService = await this.serviceContainer + .get(IPythonExecutionFactory) + .create({ resource }); + + if (executionInfo.execPath) { + return pythonExecutionService.exec(executionInfo.args, options); + } + + return pythonExecutionService.execForLinter(executionInfo.moduleName!, executionInfo.args, options); + } } diff --git a/src/client/common/process/rawProcessApis.ts b/src/client/common/process/rawProcessApis.ts new file mode 100644 index 000000000000..864191851c91 --- /dev/null +++ b/src/client/common/process/rawProcessApis.ts @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { exec, execSync, spawn } from 'child_process'; +import { Readable } from 'stream'; +import { Observable } from 'rxjs/Observable'; +import { IDisposable } from '../types'; +import { createDeferred } from '../utils/async'; +import { EnvironmentVariables } from '../variables/types'; +import { DEFAULT_ENCODING } from './constants'; +import { ExecutionResult, ObservableExecutionResult, Output, ShellOptions, SpawnOptions, StdErrError } from './types'; +import { noop } from '../utils/misc'; +import { decodeBuffer } from './decoder'; +import { traceVerbose } from '../../logging'; +import { WorkspaceService } from '../application/workspace'; +import { ProcessLogger } from './logger'; + +const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; + +function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING; + const { encoding } = execOptions; + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = defaultEnv || process.env; + defaultOptions.env = { ...env }; + } else { + defaultOptions.env = { ...defaultOptions.env }; + } + + if (execOptions && execOptions.extraVariables) { + defaultOptions.env = { ...defaultOptions.env, ...execOptions.extraVariables }; + } + + // Always ensure we have unbuffered output. + defaultOptions.env.PYTHONUNBUFFERED = '1'; + if (!defaultOptions.env.PYTHONIOENCODING) { + defaultOptions.env.PYTHONIOENCODING = 'utf-8'; + } + + return defaultOptions; +} + +export function shellExec( + command: string, + options: ShellOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const shellOptions = getDefaultOptions(options, defaultEnv); + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + const loggingOptions = { ...shellOptions, encoding: shellOptions.encoding ?? undefined }; + processLogger.logProcess(command, undefined, loggingOptions); + } + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callback = (e: any, stdout: any, stderr: any) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + stdout = filterOutputUsingCondaRunMarkers(stdout); + // Make sure stderr is undefined if we actually had none. This is checked + // elsewhere because that's how exec behaves. + resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout }); + } + }; + let procExited = false; + const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + if (disposables) { + disposables.add(disposable); + } + }); +} + +export function plainExec( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } + const proc = spawn(file, args, spawnOptions); + // Listen to these errors (unhandled errors in streams tears down the process). + // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. + proc.stdout?.on('error', noop); + proc.stderr?.on('error', noop); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + disposables?.add(disposable); + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + if (options.token) { + internalDisposables.push(options.token.onCancellationRequested(disposable.dispose)); + } + + const stdoutBuffers: Buffer[] = []; + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + options.outputChannel?.append(data.toString()); + }); + const stderrBuffers: Buffer[] = []; + on(proc.stderr, 'data', (data: Buffer) => { + if (options.mergeStdOutErr) { + stdoutBuffers.push(data); + stderrBuffers.push(data); + } else { + stderrBuffers.push(data); + } + options.outputChannel?.append(data.toString()); + }); + + proc.once('close', () => { + if (deferred.completed) { + return; + } + const stderr: string | undefined = + stderrBuffers.length === 0 ? undefined : decodeBuffer(stderrBuffers, encoding); + if ( + stderr && + stderr.length > 0 && + options.throwOnStdErr && + // ignore this specific error silently; see this issue for context: https://github.com/microsoft/vscode/issues/75932 + !(PS_ERROR_SCREEN_BOGUS.test(stderr) && stderr.replace(PS_ERROR_SCREEN_BOGUS, '').trim().length === 0) + ) { + deferred.reject(new StdErrError(stderr)); + } else { + let stdout = decodeBuffer(stdoutBuffers, encoding); + stdout = filterOutputUsingCondaRunMarkers(stdout); + deferred.resolve({ stdout, stderr }); + } + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + + return deferred.promise; +} + +function filterOutputUsingCondaRunMarkers(stdout: string) { + // These markers are added if conda run is used or `interpreterInfo.py` is + // run, see `get_output_via_markers.py`. + const regex = />>>PYTHON-EXEC-OUTPUT([\s\S]*)<<= 2 ? match[1].trim() : undefined; + return filteredOut !== undefined ? filteredOut : stdout; +} + +function removeCondaRunMarkers(out: string) { + out = out.replace('>>>PYTHON-EXEC-OUTPUT\r\n', '').replace('>>>PYTHON-EXEC-OUTPUT\n', ''); + return out.replace('<<, +): ObservableExecutionResult { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + if (!options.doNotLog) { + const processLogger = new ProcessLogger(new WorkspaceService()); + processLogger.logProcess(file, args, options); + } + const proc = spawn(file, args, spawnOptions); + let procExited = false; + const disposable: IDisposable = { + dispose() { + if (proc && proc.pid && !proc.killed && !procExited) { + killPid(proc.pid); + } + if (proc) { + proc.unref(); + } + }, + }; + disposables?.add(disposable); + + const output = new Observable>((subscriber) => { + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + if (options.token) { + internalDisposables.push( + options.token.onCancellationRequested(() => { + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + procExited = true; + } + }), + ); + } + + const sendOutput = (source: 'stdout' | 'stderr', data: Buffer) => { + let out = decodeBuffer([data], encoding); + if (source === 'stderr' && options.throwOnStdErr) { + subscriber.error(new StdErrError(out)); + } else { + // Because all of output is not retrieved at once, filtering out the + // actual output using markers is not possible. Hence simply remove + // the markers and return original output. + out = removeCondaRunMarkers(out); + subscriber.next({ source, out }); + } + }; + + on(proc.stdout, 'data', (data: Buffer) => sendOutput('stdout', data)); + on(proc.stderr, 'data', (data: Buffer) => sendOutput('stderr', data)); + + proc.once('close', () => { + procExited = true; + subscriber.complete(); + internalDisposables.forEach((d) => d.dispose()); + }); + proc.once('exit', () => { + procExited = true; + subscriber.complete(); + internalDisposables.forEach((d) => d.dispose()); + }); + proc.once('error', (ex) => { + procExited = true; + subscriber.error(ex); + internalDisposables.forEach((d) => d.dispose()); + }); + if (options.stdinStr !== undefined) { + proc.stdin?.write(options.stdinStr); + proc.stdin?.end(); + } + }); + + return { + proc, + out: output, + dispose: disposable.dispose, + }; +} + +export function killPid(pid: number): void { + try { + if (process.platform === 'win32') { + // Windows doesn't support SIGTERM, so execute taskkill to kill the process + execSync(`taskkill /pid ${pid} /T /F`); // NOSONAR + } else { + process.kill(pid); + } + } catch { + traceVerbose('Unable to kill process with pid', pid); + } +} diff --git a/src/client/common/process/serviceRegistry.ts b/src/client/common/process/serviceRegistry.ts index 27684a20cc32..0ea57231148a 100644 --- a/src/client/common/process/serviceRegistry.ts +++ b/src/client/common/process/serviceRegistry.ts @@ -2,14 +2,12 @@ // Licensed under the MIT License. import { IServiceManager } from '../../ioc/types'; -import { BufferDecoder } from './decoder'; import { ProcessServiceFactory } from './processFactory'; import { PythonExecutionFactory } from './pythonExecutionFactory'; import { PythonToolExecutionService } from './pythonToolService'; -import { IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; +import { IProcessServiceFactory, IPythonExecutionFactory, IPythonToolExecutionService } from './types'; export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IBufferDecoder, BufferDecoder); serviceManager.addSingleton(IProcessServiceFactory, ProcessServiceFactory); serviceManager.addSingleton(IPythonExecutionFactory, PythonExecutionFactory); serviceManager.addSingleton(IPythonToolExecutionService, PythonToolExecutionService); diff --git a/src/client/common/process/types.ts b/src/client/common/process/types.ts index a48e82930b5c..9263e69cbe21 100644 --- a/src/client/common/process/types.ts +++ b/src/client/common/process/types.ts @@ -1,18 +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 { PythonInterpreter } from '../../interpreter/contracts'; -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 = { source: 'stdout' | 'stderr'; @@ -24,32 +18,45 @@ export type ObservableExecutionResult = { dispose(): void; }; -// tslint:disable-next-line:interface-name export type SpawnOptions = ChildProcessSpawnOptions & { encoding?: string; token?: CancellationToken; mergeStdOutErr?: boolean; throwOnStdErr?: boolean; + extraVariables?: NodeJS.ProcessEnv; + outputChannel?: OutputChannel; + stdinStr?: string; + useWorker?: boolean; }; -// tslint:disable-next-line:interface-name -export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean; useWorker?: boolean }; export type ExecutionResult = { stdout: T; 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; exec(file: string, args: string[], options?: SpawnOptions): Promise>; shellExec(command: string, options?: ShellOptions): Promise>; + on(event: 'exec', listener: (file: string, args: string[], options?: SpawnOptions) => void): this; } export const IProcessServiceFactory = Symbol('IProcessServiceFactory'); export interface IProcessServiceFactory { - create(resource?: Uri): Promise; + create(resource?: Uri, options?: { doNotUseCustomEnvs: boolean }): Promise; } export const IPythonExecutionFactory = Symbol('IPythonExecutionFactory'); @@ -59,50 +66,65 @@ export type ExecutionFactoryCreationOptions = { }; export type ExecutionFactoryCreateWithEnvironmentOptions = { resource?: Uri; - interpreter?: PythonInterpreter; + 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; createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise; + createCondaExecutionService( + pythonPath: string, + processService: IProcessService, + ): Promise; } -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; - pipEnvWorkspaceFolder?: string; -}; export const IPythonExecutionService = Symbol('IPythonExecutionService'); export interface IPythonExecutionService { - getInterpreterInformation(): Promise; - getExecutablePath(): Promise; + getInterpreterInformation(): Promise; + getExecutablePath(): Promise; isModuleInstalled(moduleName: string): Promise; + getModuleVersion(moduleName: string): Promise; + getExecutionInfo(pythonArgs?: string[]): PythonExecInfo; execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult; execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult; exec(args: string[], options: SpawnOptions): Promise>; execModule(moduleName: string, args: string[], options: SpawnOptions): Promise>; + execForLinter(moduleName: string, args: string[], options: SpawnOptions): Promise>; } +export interface IPythonEnvironment { + getInterpreterInformation(): Promise; + getExecutionObservableInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo; + getExecutablePath(): Promise; + isModuleInstalled(moduleName: string): Promise; + getModuleVersion(moduleName: string): Promise; + getExecutionInfo(pythonArgs?: string[], pythonExecutable?: string): PythonExecInfo; +} + +export type ShellExecFunc = (command: string, options?: ShellOptions | undefined) => Promise>; + export class StdErrError extends Error { constructor(message: string) { super(message); } } -export interface IExecutionEnvironmentVariablesService { - getEnvironmentVariables(resource?: Uri): Promise; -} - export const IPythonToolExecutionService = Symbol('IPythonToolRunnerService'); export interface IPythonToolExecutionService { - execObservable(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise>; + execObservable( + executionInfo: ExecutionInfo, + options: SpawnOptions, + resource: Uri, + ): Promise>; exec(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise>; + execForLinter(executionInfo: ExecutionInfo, options: SpawnOptions, resource: Uri): Promise>; } diff --git a/src/client/common/process/worker/main.ts b/src/client/common/process/worker/main.ts new file mode 100644 index 000000000000..324673618942 --- /dev/null +++ b/src/client/common/process/worker/main.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Worker } from 'worker_threads'; +import * as path from 'path'; +import { traceVerbose, traceError } from '../../../logging/index'; + +/** + * Executes a worker file. Make sure to declare the worker file as a entry in the webpack config. + * @param workerFileName Filename of the worker file to execute, it has to end with ".worker.js" for webpack to bundle it. + * @param workerData Arguments to the worker file. + * @returns + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export async function executeWorkerFile(workerFileName: string, workerData: any): Promise { + if (!workerFileName.endsWith('.worker.js')) { + throw new Error('Worker file must end with ".worker.js" for webpack to bundle webworkers'); + } + return new Promise((resolve, reject) => { + const worker = new Worker(workerFileName, { workerData }); + const id = worker.threadId; + traceVerbose( + `Worker id ${id} for file ${path.basename(workerFileName)} with data ${JSON.stringify(workerData)}`, + ); + worker.on('message', (msg: { err: Error; res: unknown }) => { + if (msg.err) { + reject(msg.err); + } + resolve(msg.res); + }); + worker.on('error', (ex: Error) => { + traceError(`Error in worker ${workerFileName}`, ex); + reject(ex); + }); + worker.on('exit', (code) => { + traceVerbose(`Worker id ${id} exited with code ${code}`); + if (code !== 0) { + reject(new Error(`Worker ${workerFileName} stopped with exit code ${code}`)); + } + }); + }); +} diff --git a/src/client/common/process/worker/plainExec.worker.ts b/src/client/common/process/worker/plainExec.worker.ts new file mode 100644 index 000000000000..f44ea15f9653 --- /dev/null +++ b/src/client/common/process/worker/plainExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerPlainExecImpl } from './workerRawProcessApis'; + +_workerPlainExecImpl(workerData.file, workerData.args, workerData.options) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((err) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ err }); + }); diff --git a/src/client/common/process/worker/rawProcessApiWrapper.ts b/src/client/common/process/worker/rawProcessApiWrapper.ts new file mode 100644 index 000000000000..e6476df5d8fa --- /dev/null +++ b/src/client/common/process/worker/rawProcessApiWrapper.ts @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { SpawnOptions } from 'child_process'; +import * as path from 'path'; +import { executeWorkerFile } from './main'; +import { ExecutionResult, ShellOptions } from './types'; + +export function workerShellExec(command: string, options: ShellOptions): Promise> { + return executeWorkerFile(path.join(__dirname, 'shellExec.worker.js'), { + command, + options, + }); +} + +export function workerPlainExec( + file: string, + args: string[], + options: SpawnOptions = {}, +): Promise> { + return executeWorkerFile(path.join(__dirname, 'plainExec.worker.js'), { + file, + args, + options, + }); +} diff --git a/src/client/common/process/worker/shellExec.worker.ts b/src/client/common/process/worker/shellExec.worker.ts new file mode 100644 index 000000000000..f4e9809a29a5 --- /dev/null +++ b/src/client/common/process/worker/shellExec.worker.ts @@ -0,0 +1,16 @@ +import { parentPort, workerData } from 'worker_threads'; +import { _workerShellExecImpl } from './workerRawProcessApis'; + +_workerShellExecImpl(workerData.command, workerData.options, workerData.defaultEnv) + .then((res) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ res }); + }) + .catch((ex) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + parentPort.postMessage({ ex }); + }); diff --git a/src/client/common/process/worker/types.ts b/src/client/common/process/worker/types.ts new file mode 100644 index 000000000000..5c58aec10214 --- /dev/null +++ b/src/client/common/process/worker/types.ts @@ -0,0 +1,38 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +import { ExecOptions, SpawnOptions as ChildProcessSpawnOptions } from 'child_process'; + +export function noop() {} +export interface IDisposable { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + dispose(): void | undefined | Promise; +} +export type EnvironmentVariables = Record; +export class StdErrError extends Error { + // eslint-disable-next-line @typescript-eslint/no-useless-constructor + constructor(message: string) { + super(message); + } +} + +export type SpawnOptions = ChildProcessSpawnOptions & { + encoding?: string; + // /** + // * Can't use `CancellationToken` here as it comes from vscode which is not available in worker threads. + // */ + // token?: CancellationToken; + mergeStdOutErr?: boolean; + throwOnStdErr?: boolean; + extraVariables?: NodeJS.ProcessEnv; + // /** + // * Can't use `OutputChannel` here as it comes from vscode which is not available in worker threads. + // */ + // outputChannel?: OutputChannel; + stdinStr?: string; +}; +export type ShellOptions = ExecOptions & { throwOnStdErr?: boolean }; + +export type ExecutionResult = { + stdout: T; + stderr?: T; +}; diff --git a/src/client/common/process/worker/workerRawProcessApis.ts b/src/client/common/process/worker/workerRawProcessApis.ts new file mode 100644 index 000000000000..cfae9b1e6471 --- /dev/null +++ b/src/client/common/process/worker/workerRawProcessApis.ts @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// !!!! IMPORTANT: DO NOT IMPORT FROM VSCODE MODULE AS IT IS NOT AVAILABLE INSIDE WORKER THREADS !!!! + +import { exec, execSync, spawn } from 'child_process'; +import { Readable } from 'stream'; +import { createDeferred } from '../../utils/async'; +import { DEFAULT_ENCODING } from '../constants'; +import { decodeBuffer } from '../decoder'; +import { + ShellOptions, + SpawnOptions, + EnvironmentVariables, + IDisposable, + noop, + StdErrError, + ExecutionResult, +} from './types'; +import { traceWarn } from '../../../logging'; + +const PS_ERROR_SCREEN_BOGUS = /your [0-9]+x[0-9]+ screen size is bogus\. expect trouble/; + +function getDefaultOptions(options: T, defaultEnv?: EnvironmentVariables): T { + const defaultOptions = { ...options }; + const execOptions = defaultOptions as SpawnOptions; + if (execOptions) { + execOptions.encoding = + typeof execOptions.encoding === 'string' && execOptions.encoding.length > 0 + ? execOptions.encoding + : DEFAULT_ENCODING; + const { encoding } = execOptions; + delete execOptions.encoding; + execOptions.encoding = encoding; + } + if (!defaultOptions.env || Object.keys(defaultOptions.env).length === 0) { + const env = defaultEnv || process.env; + defaultOptions.env = { ...env }; + } else { + defaultOptions.env = { ...defaultOptions.env }; + } + + if (execOptions && execOptions.extraVariables) { + defaultOptions.env = { ...defaultOptions.env, ...execOptions.extraVariables }; + } + + // Always ensure we have unbuffered output. + defaultOptions.env.PYTHONUNBUFFERED = '1'; + if (!defaultOptions.env.PYTHONIOENCODING) { + defaultOptions.env.PYTHONIOENCODING = 'utf-8'; + } + + return defaultOptions; +} + +export function _workerShellExecImpl( + command: string, + options: ShellOptions, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const shellOptions = getDefaultOptions(options, defaultEnv); + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const callback = (e: any, stdout: any, stderr: any) => { + if (e && e !== null) { + reject(e); + } else if (shellOptions.throwOnStdErr && stderr && stderr.length) { + reject(new Error(stderr)); + } else { + stdout = filterOutputUsingCondaRunMarkers(stdout); + // Make sure stderr is undefined if we actually had none. This is checked + // elsewhere because that's how exec behaves. + resolve({ stderr: stderr && stderr.length > 0 ? stderr : undefined, stdout }); + } + }; + let procExited = false; + const proc = exec(command, shellOptions, callback); // NOSONAR + proc.once('close', () => { + procExited = true; + }); + proc.once('exit', () => { + procExited = true; + }); + proc.once('error', () => { + procExited = true; + }); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!procExited && !proc.killed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + if (disposables) { + disposables.add(disposable); + } + }); +} + +export function _workerPlainExecImpl( + file: string, + args: string[], + options: SpawnOptions & { doNotLog?: boolean } = {}, + defaultEnv?: EnvironmentVariables, + disposables?: Set, +): Promise> { + const spawnOptions = getDefaultOptions(options, defaultEnv); + const encoding = spawnOptions.encoding ? spawnOptions.encoding : 'utf8'; + const proc = spawn(file, args, spawnOptions); + // Listen to these errors (unhandled errors in streams tears down the process). + // Errors will be bubbled up to the `error` event in `proc`, hence no need to log. + proc.stdout?.on('error', noop); + proc.stderr?.on('error', noop); + const deferred = createDeferred>(); + const disposable: IDisposable = { + dispose: () => { + // If process has not exited nor killed, force kill it. + if (!proc.killed && !deferred.completed) { + if (proc.pid) { + killPid(proc.pid); + } else { + proc.kill(); + } + } + }, + }; + disposables?.add(disposable); + const internalDisposables: IDisposable[] = []; + + // eslint-disable-next-line @typescript-eslint/ban-types + const on = (ee: Readable | null, name: string, fn: Function) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ee?.on(name, fn as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + internalDisposables.push({ dispose: () => ee?.removeListener(name, fn as any) as any }); + }; + + // Tokens not supported yet as they come from vscode module which is not available. + // if (options.token) { + // internalDisposables.push(options.token.onCancellationRequested(disposable.dispose)); + // } + + const stdoutBuffers: Buffer[] = []; + on(proc.stdout, 'data', (data: Buffer) => { + stdoutBuffers.push(data); + }); + const stderrBuffers: Buffer[] = []; + on(proc.stderr, 'data', (data: Buffer) => { + if (options.mergeStdOutErr) { + stdoutBuffers.push(data); + stderrBuffers.push(data); + } else { + stderrBuffers.push(data); + } + }); + + proc.once('close', () => { + if (deferred.completed) { + return; + } + const stderr: string | undefined = + stderrBuffers.length === 0 ? undefined : decodeBuffer(stderrBuffers, encoding); + if ( + stderr && + stderr.length > 0 && + options.throwOnStdErr && + // ignore this specific error silently; see this issue for context: https://github.com/microsoft/vscode/issues/75932 + !(PS_ERROR_SCREEN_BOGUS.test(stderr) && stderr.replace(PS_ERROR_SCREEN_BOGUS, '').trim().length === 0) + ) { + deferred.reject(new StdErrError(stderr)); + } else { + let stdout = decodeBuffer(stdoutBuffers, encoding); + stdout = filterOutputUsingCondaRunMarkers(stdout); + deferred.resolve({ stdout, stderr }); + } + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + proc.once('error', (ex) => { + deferred.reject(ex); + internalDisposables.forEach((d) => d.dispose()); + disposable.dispose(); + }); + + return deferred.promise; +} + +function filterOutputUsingCondaRunMarkers(stdout: string) { + // These markers are added if conda run is used or `interpreterInfo.py` is + // run, see `get_output_via_markers.py`. + const regex = />>>PYTHON-EXEC-OUTPUT([\s\S]*)<<= 2 ? match[1].trim() : undefined; + return filteredOut !== undefined ? filteredOut : stdout; +} + +function killPid(pid: number): void { + try { + if (process.platform === 'win32') { + // Windows doesn't support SIGTERM, so execute taskkill to kill the process + execSync(`taskkill /pid ${pid} /T /F`); // NOSONAR + } else { + process.kill(pid); + } + } catch { + traceWarn('Unable to kill process with pid', pid); + } +} diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 99f05ec14814..abd2b220e400 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -1,129 +1,191 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IHttpClient, IFileDownloader } from '../common/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, - ILiveShareApi, ITerminalManager, - IWorkspaceService + IWorkspaceService, } from './application/types'; import { WorkspaceService } from './application/workspace'; -import { AsyncDisposableRegistry } from './asyncDisposableRegistry'; import { ConfigurationService } from './configuration/service'; -import { CryptoUtils } from './crypto'; -import { EditorUtils } from './editor'; -import { ExperimentsManager } from './experiments'; -import { FeatureDeprecationManager } from './featureDeprecationManager'; +import { PipEnvExecutionPath } from './configuration/executionSettings/pipEnvExecution'; +import { ExperimentService } from './experiments/service'; import { ProductInstaller } from './installer/productInstaller'; -import { LiveShareApi } from './liveshare/liveshare'; -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, - TerminalActivationProviders + TerminalActivationProviders, } from './terminal/types'; -import { - IAsyncDisposableRegistry, - IBrowserService, - IConfigurationService, - ICryptoUtils, - ICurrentProcess, - IEditorUtils, - IExperimentsManager, - IExtensions, - IFeatureDeprecationManager, - IInstaller, - ILogger, - IPathUtils, - IPersistentStateFactory, - IRandom, - IsWindows -} from './types'; + import { IMultiStepInputFactory, MultiStepInputFactory } from './utils/multiStepInput'; import { Random } from './utils/random'; -import { FileDownloader } from './net/fileDownloader'; +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(IsWindows, IS_WINDOWS); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingletonInstance(IsWindows, isWindows()); + serviceManager.addSingleton(IActiveResourceService, ActiveResourceService); + serviceManager.addSingleton(IInterpreterPathService, InterpreterPathService); serviceManager.addSingleton(IExtensions, Extensions); serviceManager.addSingleton(IRandom, Random); serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton(ILogger, Logger); + serviceManager.addBinding(IPersistentStateFactory, IExtensionSingleActivationService); serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(IApplicationShell, ApplicationShell); + serviceManager.addSingleton(IClipboard, ClipboardService); serviceManager.addSingleton(ICurrentProcess, CurrentProcess); serviceManager.addSingleton(IInstaller, ProductInstaller); + serviceManager.addSingleton( + IJupyterExtensionDependencyManager, + JupyterExtensionDependencyManager, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + RequireJupyterPrompt, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + CreatePythonFileCommandHandler, + ); serviceManager.addSingleton(ICommandManager, CommandManager); + serviceManager.addSingleton(IContextKeyManager, ContextKeyManager); serviceManager.addSingleton(IConfigurationService, ConfigurationService); serviceManager.addSingleton(IWorkspaceService, WorkspaceService); + serviceManager.addSingleton(IProcessLogger, ProcessLogger); serviceManager.addSingleton(IDocumentManager, DocumentManager); serviceManager.addSingleton(ITerminalManager, TerminalManager); serviceManager.addSingleton(IDebugService, DebugService); serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); serviceManager.addSingleton(ILanguageService, LanguageService); serviceManager.addSingleton(IBrowserService, BrowserService); - serviceManager.addSingleton(IHttpClient, HttpClient); - serviceManager.addSingleton(IFileDownloader, FileDownloader); - serviceManager.addSingleton(IEditorUtils, EditorUtils); - serviceManager.addSingleton(INugetService, NugetService); serviceManager.addSingleton(ITerminalActivator, TerminalActivator); - serviceManager.addSingleton(ITerminalActivationHandler, PowershellTerminalActivationFailedHandler); - serviceManager.addSingleton(ILiveShareApi, LiveShareApi); - serviceManager.addSingleton(ICryptoUtils, CryptoUtils); - serviceManager.addSingleton(IExperimentsManager, ExperimentsManager); + serviceManager.addSingleton( + ITerminalActivationHandler, + PowershellTerminalActivationFailedHandler, + ); + serviceManager.addSingleton(IExperimentService, ExperimentService); serviceManager.addSingleton(ITerminalHelper, TerminalHelper); serviceManager.addSingleton( - ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish); + ITerminalActivationCommandProvider, + Bash, + TerminalActivationProviders.bashCShellFish, + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell, + ); + serviceManager.addSingleton( + ITerminalActivationCommandProvider, + Nushell, + TerminalActivationProviders.nushell, + ); serviceManager.addSingleton( - ITerminalActivationCommandProvider, CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell); + ITerminalActivationCommandProvider, + PyEnvActivationCommandProvider, + TerminalActivationProviders.pyenv, + ); serviceManager.addSingleton( - ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv); + ITerminalActivationCommandProvider, + CondaActivationCommandProvider, + TerminalActivationProviders.conda, + ); serviceManager.addSingleton( - ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda); + ITerminalActivationCommandProvider, + PixiActivationCommandProvider, + TerminalActivationProviders.pixi, + ); serviceManager.addSingleton( - ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv); - serviceManager.addSingleton(IFeatureDeprecationManager, FeatureDeprecationManager); + ITerminalActivationCommandProvider, + PipEnvActivationCommandProvider, + TerminalActivationProviders.pipenv, + ); + serviceManager.addSingleton(IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv); - serviceManager.addSingleton(IAsyncDisposableRegistry, AsyncDisposableRegistry); serviceManager.addSingleton(IMultiStepInputFactory, MultiStepInputFactory); serviceManager.addSingleton(IImportTracker, ImportTracker); + serviceManager.addBinding(IImportTracker, IExtensionSingleActivationService); + serviceManager.addSingleton(IShellDetector, TerminalNameShellDetector); + serviceManager.addSingleton(IShellDetector, SettingsShellDetector); + serviceManager.addSingleton(IShellDetector, UserEnvironmentShellDetector); + serviceManager.addSingleton(IShellDetector, VSCEnvironmentShellDetector); + serviceManager.addSingleton( + IExtensionSingleActivationService, + ReloadVSCodeCommandHandler, + ); + serviceManager.addSingleton( + 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 cae3f4108132..b4d2f888d5d2 100644 --- a/src/client/common/terminal/activator/base.ts +++ b/src/client/common/terminal/activator/base.ts @@ -3,14 +3,18 @@ '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> = new Map>(); - 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 { if (this.activatedTerminals.has(terminal)) { return this.activatedTerminals.get(terminal)!; } @@ -18,11 +22,16 @@ export class BaseTerminalActivator implements ITerminalActivator { this.activatedTerminals.set(terminal, deferred.promise); 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; 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>(); + 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 { + let promise = this.pendingActivations.get(terminal); + if (promise) { + return promise; + } + promise = this.activateEnvironmentInTerminalImpl(terminal, options); + this.pendingActivations.set(terminal, promise); + return promise; + } + private async activateEnvironmentInTerminalImpl( + terminal: Terminal, + options?: TerminalActivationOptions, + ): Promise { + const settings = this.configurationService.getSettings(options?.resource); + const activateEnvironment = + settings.terminal.activateEnvironment && !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 574b0532ae01..d580ed4d38bf 100644 --- a/src/client/common/terminal/activator/powershellFailedHandler.ts +++ b/src/client/common/terminal/activator/powershellFailedHandler.ts @@ -5,7 +5,10 @@ import { inject, injectable, named } from 'inversify'; import { Terminal } from 'vscode'; -import { PowerShellActivationHackDiagnosticsServiceId, PowershellActivationNotAvailableDiagnostic } from '../../../application/diagnostics/checks/powerShellActivation'; +import { + PowerShellActivationHackDiagnosticsServiceId, + PowershellActivationNotAvailableDiagnostic, +} from '../../../application/diagnostics/checks/powerShellActivation'; import { IDiagnosticsService } from '../../../application/diagnostics/types'; import { IPlatformService } from '../../platform/types'; import { Resource } from '../../types'; @@ -13,10 +16,13 @@ import { ITerminalActivationHandler, ITerminalHelper, TerminalShellType } from ' @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) { - } + @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; @@ -26,11 +32,13 @@ export class PowershellTerminalActivationFailedHandler implements ITerminalActiv 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(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; + +/** + * Build an "executable finder" function that identifies venv environments. + * + * @param basename - the venv name or names to look for + * @param pathDirname - typically `path.dirname` + * @param pathJoin - typically `path.join` + * @param fileExists - typically `fs.exists` + */ + +function getVenvExecutableFinder( + basename: string | string[], + // + pathDirname: (filename: string) => string, + pathJoin: (...parts: string[]) => string, + // + fileExists: (n: string) => Promise, +): ExecutableFinderFunc { + const basenames = typeof basename === 'string' ? [basename] : basename; + return async (python: string) => { + // Generated scripts are found in the same directory as the interpreter. + const binDir = pathDirname(python); + for (const name of basenames) { + const filename = pathJoin(binDir, name); + if (await fileExists(filename)) { + return filename; + } + } + // No matches so return undefined. + return undefined; + }; +} + @injectable() -export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider { - 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 { - const pythonPath = this.serviceContainer.get(IConfigurationService).getSettings(resource).pythonPath; - return this.getActivationCommandsForInterpreter(pythonPath, targetShell); + + public async getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType, + ): Promise { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); } - public abstract getActivationCommandsForInterpreter(pythonPath: string, targetShell: TerminalShellType): Promise; - protected async findScriptFile(pythonPath: string, scriptFileNames: string[]): Promise { + public abstract getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise; +} + +export type ActivationScripts = Partial>; + +export abstract class VenvBaseActivationCommandProvider extends BaseActivationCommandProvider { + public isShellSupported(targetShell: TerminalShellType): boolean { + return this.scripts[targetShell] !== undefined; + } + + protected abstract get scripts(): ActivationScripts; + + protected async findScriptFile(pythonPath: string, targetShell: TerminalShellType): Promise { const fs = this.serviceContainer.get(IFileSystem); - 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 { - 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 { + 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 a60fbb3661a7..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: string, targetShell: TerminalShellType): Promise { - // 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 { + 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 30e0b9defa7c..42bb8f38fc9e 100644 --- a/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/condaActivationProvider.ts @@ -1,140 +1,172 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../extensions'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; - -import { ICondaService } from '../../../interpreter/contracts'; -import { IPlatformService } from '../../platform/types'; -import { IConfigurationService } from '../../types'; -import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; - -// Version number of conda that requires we call activate with 'conda activate' instead of just 'activate' -const CondaRequiredMajor = 4; -const CondaRequiredMinor = 4; -const CondaRequiredMinorForPowerShell = 6; - -/** - * Support conda env activation (in the terminal). - */ -@injectable() -export class CondaActivationCommandProvider implements ITerminalActivationCommandProvider { - constructor( - @inject(ICondaService) private readonly condaService: ICondaService, - @inject(IPlatformService) private platform: IPlatformService, - @inject(IConfigurationService) private configService: IConfigurationService - ) { } - - /** - * Is the given shell supported for activating a conda env? - */ - public isShellSupported(_targetShell: TerminalShellType): boolean { - return true; - } - - /** - * Return the command needed to activate the conda env. - */ - public getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise { - const pythonPath = this.configService.getSettings(resource).pythonPath; - return this.getActivationCommandsForInterpreter(pythonPath, targetShell); - } - - /** - * Return the command needed to activate the conda env. - * - */ - public async getActivationCommandsForInterpreter(pythonPath: string, targetShell: TerminalShellType): Promise { - const envInfo = await this.condaService.getCondaEnvironment(pythonPath); - if (!envInfo) { - return; - } - - // Algorithm differs based on version - // Old version, just call activate directly. - // New version, call activate from the same path as our python path, then call it again to activate our environment. - // -- note that the 'default' conda location won't allow activate to work for the environment sometimes. - const versionInfo = await this.condaService.getCondaVersion(); - if (versionInfo && versionInfo.major >= CondaRequiredMajor) { - // Conda added support for powershell in 4.6. - if (versionInfo.minor >= CondaRequiredMinorForPowerShell && - (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore)) { - return this.getPowershellCommands(envInfo.name); - } - if (versionInfo.minor >= CondaRequiredMinor) { - // New version. - const interpreterPath = await this.condaService.getCondaFileFromInterpreter(pythonPath, envInfo.name); - if (interpreterPath) { - const activatePath = path.join(path.dirname(interpreterPath), 'activate').fileToCommandArgument(); - const firstActivate = this.platform.isWindows ? activatePath : `source ${activatePath}`; - return [firstActivate, `conda activate ${envInfo.name.toCommandArgument()}`]; - } - } - } - - switch (targetShell) { - case TerminalShellType.powershell: - case TerminalShellType.powershellCore: - return this.getPowershellCommands(envInfo.name); - - // 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 this.condaService.getCondaFile()); - - default: - if (this.platform.isWindows) { - return this.getWindowsCommands(envInfo.name); - } else { - return this.getUnixCommands(envInfo.name, await this.condaService.getCondaFile()); - } - } - } - - public async getWindowsActivateCommand(): Promise { - let activateCmd: string = 'activate'; - - const condaExePath = await this.condaService.getCondaFile(); - - if (condaExePath && path.basename(condaExePath) !== condaExePath) { - const condaScriptsPath: string = path.dirname(condaExePath); - // prefix the cmd with the found path, and ensure it's quoted properly - activateCmd = path.join(condaScriptsPath, activateCmd); - activateCmd = activateCmd.toCommandArgument(); - } - - return activateCmd; - } - - public async getWindowsCommands(envName: string): Promise { - const activate = await this.getWindowsActivateCommand(); - return [`${activate} ${envName.toCommandArgument()}`]; - } - /** - * The expectation is for the user to configure Powershell for Conda. - * Hence we just send the command `conda activate ...`. - * This configuration is documented on Conda. - * Extension will not attempt to work around issues by trying to setup shell for user. - * - * @param {string} envName - * @returns {(Promise)} - * @memberof CondaActivationCommandProvider - */ - public async getPowershellCommands(envName: string): Promise { - return [`conda activate ${envName.toCommandArgument()}`]; - } - - public async getFishCommands(envName: string, conda: string): Promise { - // 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 { - 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 { + 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 { + 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 { + 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 { + 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 { + return [`conda activate ${condaEnv.toCommandArgumentForPythonExt()}`]; +} + +async function getFishCommands(condaEnv: string, condaFile: string): Promise { + // https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28 + return [`${condaFile.fileToCommandArgumentForPythonExt()} activate ${condaEnv.toCommandArgumentForPythonExt()}`]; +} + +async function getUnixCommands(condaEnv: string, condaFile: string): Promise { + const condaDir = path.dirname(condaFile); + const activateFile = path.join(condaDir, 'activate'); + return [`source ${activateFile.fileToCommandArgumentForPythonExt()} ${condaEnv.toCommandArgumentForPythonExt()}`]; +} diff --git a/src/client/common/terminal/environmentActivationProviders/nushell.ts b/src/client/common/terminal/environmentActivationProviders/nushell.ts new file mode 100644 index 000000000000..333fd5167770 --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/nushell.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import '../../extensions'; +import { TerminalShellType } from '../types'; +import { ActivationScripts, VenvBaseActivationCommandProvider } from './baseActivationProvider'; + +// For a given shell the scripts are in order of precedence. +const SCRIPTS: ActivationScripts = { + [TerminalShellType.nushell]: ['activate.nu'], +}; + +export function getAllScripts(): string[] { + const scripts: string[] = []; + for (const names of Object.values(SCRIPTS)) { + for (const name of names) { + if (!scripts.includes(name)) { + scripts.push(name); + } + } + } + return scripts; +} + +@injectable() +export class Nushell extends VenvBaseActivationCommandProvider { + protected readonly scripts = SCRIPTS; + + public async getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise { + const scriptFile = await this.findScriptFile(pythonPath, targetShell); + if (!scriptFile) { + return undefined; + } + return [`overlay use ${scriptFile.fileToCommandArgumentForPythonExt()}`]; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts index 2a25c7bc1e48..d097c759ec40 100644 --- a/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pipEnvActivationProvider.ts @@ -3,50 +3,54 @@ 'use strict'; -import { inject, injectable } from 'inversify'; +import { inject, injectable, named } from 'inversify'; import { Uri } from 'vscode'; -import '../../../common/extensions'; -import { IInterpreterService, InterpreterType, IPipEnvService } from '../../../interpreter/contracts'; +import '../../extensions'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { isPipenvEnvironmentRelatedToFolder } from '../../../pythonEnvironments/common/environmentManagers/pipenv'; +import { EnvironmentType } from '../../../pythonEnvironments/info'; import { IWorkspaceService } from '../../application/types'; -import { IFileSystem } from '../../platform/types'; -import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; +import { IToolExecutionPath, ToolExecutionPath } from '../../types'; +import { ITerminalActivationCommandProvider } from '../types'; @injectable() export class PipEnvActivationCommandProvider implements ITerminalActivationCommandProvider { constructor( @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, - @inject(IPipEnvService) private readonly pipenvService: IPipEnvService, + @inject(IToolExecutionPath) + @named(ToolExecutionPath.pipenv) + private readonly pipEnvExecution: IToolExecutionPath, @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IFileSystem) private readonly fs: IFileSystem - ) { } + ) {} - public isShellSupported(_targetShell: TerminalShellType): boolean { + // eslint-disable-next-line class-methods-use-this + public isShellSupported(): boolean { return false; } - public async getActivationCommands(resource: Uri | undefined, _: TerminalShellType): Promise { + public async getActivationCommands(resource: Uri | undefined): Promise { const interpreter = await this.interpreterService.getActiveInterpreter(resource); - if (!interpreter || interpreter.type !== InterpreterType.Pipenv) { - return; + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return undefined; } // Activate using `pipenv shell` only if the current folder relates pipenv environment. const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - if (workspaceFolder && interpreter.pipEnvWorkspaceFolder && - !this.fs.arePathsSame(workspaceFolder.uri.fsPath, interpreter.pipEnvWorkspaceFolder)) { - return; + if (workspaceFolder) { + if (!(await isPipenvEnvironmentRelatedToFolder(interpreter.path, workspaceFolder?.uri.fsPath))) { + return undefined; + } } - const execName = this.pipenvService.executable; - return [`${execName.fileToCommandArgument()} shell`]; + const execName = this.pipEnvExecution.executable; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; } - public async getActivationCommandsForInterpreter(pythonPath: string, _targetShell: TerminalShellType): Promise { + public async getActivationCommandsForInterpreter(pythonPath: string): Promise { const interpreter = await this.interpreterService.getInterpreterDetails(pythonPath); - if (!interpreter || interpreter.type !== InterpreterType.Pipenv) { - return; + if (!interpreter || interpreter.envType !== EnvironmentType.Pipenv) { + return undefined; } - const execName = this.pipenvService.executable; - return [`${execName.fileToCommandArgument()} shell`]; + const execName = this.pipEnvExecution.executable; + return [`${execName.fileToCommandArgumentForPythonExt()} shell`]; } - } diff --git a/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts new file mode 100644 index 000000000000..1deaa56dd8ae --- /dev/null +++ b/src/client/common/terminal/environmentActivationProviders/pixiActivationProvider.ts @@ -0,0 +1,77 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { ITerminalActivationCommandProvider, TerminalShellType } from '../types'; +import { getPixiActivationCommands } from '../../../pythonEnvironments/common/environmentManagers/pixi'; + +@injectable() +export class PixiActivationCommandProvider implements ITerminalActivationCommandProvider { + constructor(@inject(IInterpreterService) private readonly interpreterService: IInterpreterService) {} + + // eslint-disable-next-line class-methods-use-this + public isShellSupported(targetShell: TerminalShellType): boolean { + return shellTypeToPixiShell(targetShell) !== undefined; + } + + public async getActivationCommands( + resource: Uri | undefined, + targetShell: TerminalShellType, + ): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + return undefined; + } + + return this.getActivationCommandsForInterpreter(interpreter.path, targetShell); + } + + public getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise { + return getPixiActivationCommands(pythonPath, targetShell); + } +} + +/** + * Returns the name of a terminal shell type within Pixi. + */ +function shellTypeToPixiShell(targetShell: TerminalShellType): string | undefined { + switch (targetShell) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.commandPrompt: + return 'cmd'; + + case TerminalShellType.zsh: + return 'zsh'; + + case TerminalShellType.fish: + return 'fish'; + + case TerminalShellType.nushell: + return 'nushell'; + + case TerminalShellType.xonsh: + return 'xonsh'; + + case TerminalShellType.cshell: + // Explicitly unsupported + return undefined; + + case TerminalShellType.gitbash: + case TerminalShellType.bash: + case TerminalShellType.wsl: + case TerminalShellType.tcshell: + case TerminalShellType.other: + default: + return 'bash'; + } +} diff --git a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts index 0c98eddc35bc..6b5ced048672 100644 --- a/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts +++ b/src/client/common/terminal/environmentActivationProviders/pyenvActivationProvider.ts @@ -5,34 +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 { - const interpreter = await this.serviceContainer.get(IInterpreterService).getActiveInterpreter(resource); - if (!interpreter || interpreter.type !== InterpreterType.Pyenv || !interpreter.envName) { - return; + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getActiveInterpreter(resource); + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return undefined; } - return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } - public async getActivationCommandsForInterpreter(pythonPath: string, _targetShell: TerminalShellType): Promise { - const interpreter = await this.serviceContainer.get(IInterpreterService).getInterpreterDetails(pythonPath); - if (!interpreter || interpreter.type !== InterpreterType.Pyenv || !interpreter.envName) { - return; + public async getActivationCommandsForInterpreter( + pythonPath: string, + _targetShell: TerminalShellType, + ): Promise { + const interpreter = await this.serviceContainer + .get(IInterpreterService) + .getInterpreterDetails(pythonPath); + if (!interpreter || interpreter.envType !== EnvironmentType.Pyenv || !interpreter.envName) { + return undefined; } - return [`pyenv shell ${interpreter.envName.toCommandArgument()}`]; + return [`pyenv shell ${interpreter.envName.toCommandArgumentForPythonExt()}`]; } - } diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts index 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; + private terminalServices: Map; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - - this.terminalServices = new Map(); + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + @inject(IFileSystem) private fs: IFileSystem, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + ) { + this.terminalServices = new Map(); } - 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).getWorkspaceFolder(resource!); - return workspaceFolder ? `${title}:${workspaceFolder.uri.fsPath}` : title; + const workspaceFolder = this.serviceContainer + .get(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 8720a89aef3a..d2b3bb7879af 100644 --- a/src/client/common/terminal/helper.ts +++ b/src/client/common/terminal/helper.ts @@ -1,38 +1,62 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; +import { inject, injectable, multiInject, named } from 'inversify'; import { Terminal, Uri } from 'vscode'; -import { ICondaService, IInterpreterService, InterpreterType, PythonInterpreter } from '../../interpreter/contracts'; +import { IComponentAdapter, IInterpreterService } from '../../interpreter/contracts'; +import { IServiceContainer } from '../../ioc/types'; +import { traceDecoratorError, traceError } from '../../logging'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { ITerminalManager, IWorkspaceService } from '../application/types'; +import { ITerminalManager } from '../application/types'; import '../extensions'; -import { traceDecorators, traceError } from '../logger'; import { IPlatformService } from '../platform/types'; -import { IConfigurationService, ICurrentProcess, Resource } from '../types'; +import { IConfigurationService, Resource } from '../types'; import { OSType } from '../utils/platform'; import { ShellDetector } from './shellDetector'; -import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalActivationProviders, TerminalShellType } from './types'; +import { + IShellDetector, + ITerminalActivationCommandProvider, + ITerminalHelper, + TerminalActivationProviders, + TerminalShellType, +} from './types'; +import { isPixiEnvironment } from '../../pythonEnvironments/common/environmentManagers/pixi'; @injectable() export class TerminalHelper implements ITerminalHelper { private readonly shellDetector: ShellDetector; - constructor(@inject(IPlatformService) private readonly platform: IPlatformService, + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, - @inject(ICondaService) private readonly condaService: ICondaService, - @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @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.pyenv) private readonly pyenv: ITerminalActivationCommandProvider, - @inject(ITerminalActivationCommandProvider) @named(TerminalActivationProviders.pipenv) private readonly pipenv: ITerminalActivationCommandProvider, - @inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService + @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, this.currentProcess, this.workspace); - + this.shellDetector = new ShellDetector(this.platform, shellDetectors); } public createTerminal(title?: string): Terminal { return this.terminalManager.createTerminal({ name: title }); @@ -42,29 +66,62 @@ export class TerminalHelper implements ITerminalHelper { } 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): Promise { - const providers = [this.pipenv, this.pyenv, this.bashCShellFish, this.commandPromptAndPowerShell]; - const promise = this.getActivationCommands(resource || undefined, undefined, terminalShellType, providers); - this.sendTelemetry(resource, terminalShellType, EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL, promise).ignoreErrors(); + public async getEnvironmentActivationCommands( + terminalShellType: TerminalShellType, + resource?: Uri, + interpreter?: PythonEnvironment, + ): Promise { + 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 getEnvironmentActivationShellCommands(resource: Resource, shell: TerminalShellType, interpreter?: PythonInterpreter): Promise { + public async getEnvironmentActivationShellCommands( + resource: Resource, + shell: TerminalShellType, + interpreter?: PythonEnvironment, + ): Promise { if (this.platform.osType === OSType.Unknown) { return; } - const providers = [this.bashCShellFish, this.commandPromptAndPowerShell]; + const providers = [this.pixi, this.bashCShellFish, this.commandPromptAndPowerShell, this.nushell]; const promise = this.getActivationCommands(resource, interpreter, shell, providers); - this.sendTelemetry(resource, shell, EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE, promise).ignoreErrors(); + this.sendTelemetry( + shell, + EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE, + interpreter, + promise, + ).ignoreErrors(); return promise; } - @traceDecorators.error('Failed to capture telemetry') - protected async sendTelemetry(resource: Resource, terminalShellType: TerminalShellType, eventName: EventName, promise: Promise): Promise { + @traceDecoratorError('Failed to capture telemetry') + protected async sendTelemetry( + terminalShellType: TerminalShellType, + eventName: EventName, + interpreter: PythonEnvironment | undefined, + promise: Promise, + ): Promise { let hasCommands = false; - const interpreter = await this.interpreterService.getActiveInterpreter(resource); let failed = false; try { const cmds = await promise; @@ -74,25 +131,41 @@ export class TerminalHelper implements ITerminalHelper { traceError('Failed to get activation commands', ex); } - const pythonVersion = (interpreter && interpreter.version) ? interpreter.version.raw : undefined; - const interpreterType = interpreter ? interpreter.type : InterpreterType.Unknown; + 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: PythonInterpreter | undefined, terminalShellType: TerminalShellType, providers: ITerminalActivationCommandProvider[]): Promise { + protected async getActivationCommands( + resource: Resource, + interpreter: PythonEnvironment | undefined, + terminalShellType: TerminalShellType, + providers: ITerminalActivationCommandProvider[], + ): Promise { const settings = this.configurationService.getSettings(resource); - const activateEnvironment = settings.terminal.activateEnvironment; - if (!activateEnvironment) { - return; + + const isPixiEnv = interpreter + ? interpreter.envType === EnvironmentType.Pixi + : await isPixiEnvironment(settings.pythonPath); + if (isPixiEnv) { + const activationCommands = interpreter + ? await this.pixi.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) + : await this.pixi.getActivationCommands(resource, terminalShellType); + + if (Array.isArray(activationCommands)) { + return activationCommands; + } } + const condaService = this.serviceContainer.get(IComponentAdapter); // If we have a conda environment, then use that. - const isCondaEnvironment = await this.condaService.isCondaEnvironment(settings.pythonPath); + const isCondaEnvironment = interpreter + ? interpreter.envType === EnvironmentType.Conda + : await condaService.isCondaEnvironment(settings.pythonPath); if (isCondaEnvironment) { - - const activationCommands = interpreter ? - await this.conda.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) : - await this.conda.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; @@ -100,13 +173,12 @@ export class TerminalHelper implements ITerminalHelper { } // Search from the list of providers. - const supportedProviders = providers.filter(provider => provider.isShellSupported(terminalShellType)); + const supportedProviders = providers.filter((provider) => provider.isShellSupported(terminalShellType)); for (const provider of supportedProviders) { - - const activationCommands = interpreter ? - await provider.getActivationCommandsForInterpreter(interpreter.path, terminalShellType) : - await provider.getActivationCommands(resource, terminalShellType); + 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 ec3710aefa8c..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 { EventName } from '../../telemetry/constants'; -import { ITerminalManager } from '../application/types'; +import { ITerminalAutoActivation } from '../../terminals/types'; +import { IApplicationShell, ITerminalManager } from '../application/types'; +import { _SCRIPTS_DIR } from '../process/internal/scripts/constants'; import { IConfigurationService, IDisposableRegistry } from '../types'; -import { ITerminalActivator, ITerminalHelper, ITerminalService, 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,68 +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 = new Set(); + private _terminalFirstLaunched: boolean = true; + private pythonReplCommandQueue: string[] = []; + private isReplReady: boolean = false; + private replPromptListener?: Disposable; + private replShellTypeListener?: Disposable; public get onDidCloseTerminal(): Event { return this.terminalClosed.event.bind(this.terminalClosed); } - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - private resource?: Uri, - private title: string = 'Python') { + constructor( + @inject(IServiceContainer) private serviceContainer: IServiceContainer, + private readonly options?: TerminalCreationOptions, + ) { const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); disposableRegistry.push(this); this.terminalHelper = this.serviceContainer.get(ITerminalHelper); this.terminalManager = this.serviceContainer.get(ITerminalManager); + this.terminalAutoActivator = this.serviceContainer.get(ITerminalAutoActivation); + this.applicationShell = this.serviceContainer.get(IApplicationShell); this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); this.terminalActivator = this.serviceContainer.get(ITerminalActivator); } public dispose() { - if (this.terminal) { - this.terminal.dispose(); + this.terminal?.dispose(); + this.disposeReplListener(); + + if (this.executeCommandListeners && this.executeCommandListeners.size > 0) { + this.executeCommandListeners.forEach((d) => { + d?.dispose(); + }); } } - public async sendCommand(command: string, args: string[]): Promise { + public async sendCommand(command: string, args: string[], _?: CancellationToken): Promise { 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 { 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 { + if (isPythonShell) { + if (this.isReplReady) { + this.terminal?.sendText(commandLine); + traceVerbose(`Python REPL sendText: ${commandLine}`); + } else { + // Queue command to run once REPL is ready. + this.pythonReplCommandQueue.push(commandLine); + traceVerbose(`Python REPL queued command: ${commandLine}`); + this.startReplListener(); + } + return undefined; + } + + // Non-REPL code execution + return this.executeCommandInternal(commandLine); + } + + private startReplListener(): void { + if (this.replPromptListener || this.replShellTypeListener) { + return; + } + + this.replShellTypeListener = this.terminalManager.onDidChangeTerminalState((terminal) => { + if (this.terminal && terminal === this.terminal) { + if (terminal.state.shell == 'python') { + traceVerbose('Python REPL ready from terminal shell api'); + this.onReplReady(); + } + } + }); + + let terminalData = ''; + this.replPromptListener = this.applicationShell.onDidWriteTerminalData((e) => { + if (this.terminal && e.terminal === this.terminal) { + terminalData += e.data; + if (/>>>\s*$/.test(terminalData)) { + traceVerbose('Python REPL ready, from >>> prompt detection'); + this.onReplReady(); + } + } + }); + } + + private onReplReady(): void { + if (this.isReplReady) { + return; + } + this.isReplReady = true; + this.flushReplQueue(); + this.disposeReplListener(); + } + + private disposeReplListener(): void { + if (this.replPromptListener) { + this.replPromptListener.dispose(); + this.replPromptListener = undefined; + } + if (this.replShellTypeListener) { + this.replShellTypeListener.dispose(); + this.replShellTypeListener = undefined; + } + } + + private flushReplQueue(): void { + while (this.pythonReplCommandQueue.length > 0) { + const commandLine = this.pythonReplCommandQueue.shift(); + if (commandLine) { + traceVerbose(`Executing queued REPL command: ${commandLine}`); + this.terminal?.sendText(commandLine); + } + } + } + + private async executeCommandInternal(commandLine: string): Promise { + const terminal = this.terminal; + if (!terminal) { + traceVerbose('Terminal not available, cannot execute command'); + return undefined; + } + + if (!this.options?.hideFromUser) { + terminal.show(true); + } + + // If terminal was just launched, wait some time for shell integration to onDidChangeShellIntegration. + if (!terminal.shellIntegration && this._terminalFirstLaunched) { + this._terminalFirstLaunched = false; + const promise = new Promise((resolve) => { + const disposable = this.terminalManager.onDidChangeTerminalShellIntegration(() => { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + clearTimeout(timer); + disposable.dispose(); + resolve(true); + }); + const TIMEOUT_DURATION = 500; + const timer = setTimeout(() => { + disposable.dispose(); + resolve(true); + }, TIMEOUT_DURATION); + }); + await promise; + } + + if (terminal.shellIntegration) { + const execution = terminal.shellIntegration.executeCommand(commandLine); + traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`); + return execution; + } else { + terminal.sendText(commandLine); + traceVerbose(`Shell Integration is disabled, sendText: ${commandLine}`); + } + + return undefined; + } + public async show(preserveFocus: boolean = true): Promise { await this.ensureTerminal(preserveFocus); - this.terminal!.show(preserveFocus); + if (!this.options?.hideFromUser) { + this.terminal!.show(preserveFocus); + } } - private async ensureTerminal(preserveFocus: boolean = true): Promise { + // TODO: Debt switch to Promise ---> breaks 20 tests + public async ensureTerminal(preserveFocus: boolean = true): Promise { if (this.terminal) { return; } - this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal); - this.terminal = this.terminalManager.createTerminal({ name: this.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).getSettings(this.resource).pythonPath; - const interpreterInfo = await this.serviceContainer.get(IInterpreterService).getInterpreterDetails(pythonPath); - const pythonVersion = (interpreterInfo && interpreterInfo.version) ? interpreterInfo.version.raw : undefined; - const interpreterType = interpreterInfo ? interpreterInfo.type : undefined; - captureTelemetry(EventName.TERMINAL_CREATE, { terminal: this.terminalShellType, pythonVersion, interpreterType }); + const pythonPath = this.serviceContainer + .get(IConfigurationService) + .getSettings(this.options?.resource).pythonPath; + const interpreterInfo = + this.options?.interpreter || + (await this.serviceContainer + .get(IInterpreterService) + .getInterpreterDetails(pythonPath)); + const pythonVersion = interpreterInfo && interpreterInfo.version ? interpreterInfo.version.raw : undefined; + const interpreterType = interpreterInfo ? interpreterInfo.envType : undefined; + captureTelemetry(EventName.TERMINAL_CREATE, { + terminal: this.terminalShellType, + pythonVersion, + interpreterType, + }); } } diff --git a/src/client/common/terminal/shellDetector.ts b/src/client/common/terminal/shellDetector.ts index 5af05018632e..bf183f20a279 100644 --- a/src/client/common/terminal/shellDetector.ts +++ b/src/client/common/terminal/shellDetector.ts @@ -1,209 +1,72 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable } from 'inversify'; -import { Terminal } from 'vscode'; +'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 { IWorkspaceService } from '../application/types'; import '../extensions'; -import { traceVerbose } from '../logger'; import { IPlatformService } from '../platform/types'; -import { ICurrentProcess } from '../types'; import { OSType } from '../utils/platform'; -import { 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$|cmd$)/i; -const IS_POWERSHELL = /(powershell.exe$|powershell$)/i; -const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i; -const IS_FISH = /(fish$)/i; -const IS_CSHELL = /(csh$)/i; -const IS_TCSHELL = /(tcsh$)/i; -const IS_XONSH = /(xonsh$)/i; +import { IShellDetector, ShellIdentificationTelemetry, TerminalShellType } from './types'; const defaultOSShells = { [OSType.Linux]: TerminalShellType.bash, [OSType.OSX]: TerminalShellType.bash, [OSType.Windows]: TerminalShellType.commandPrompt, - [OSType.Unknown]: undefined + [OSType.Unknown]: TerminalShellType.other, }; -type ShellIdentificationTelemetry = { - failed: boolean; - terminalProvided: boolean; - shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default'; - hasCustomShell: undefined | boolean; - hasShellInEnv: undefined | boolean; -}; -const detectableShells = new Map(); -detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); -detectableShells.set(TerminalShellType.gitbash, IS_GITBASH); -detectableShells.set(TerminalShellType.bash, IS_BASH); -detectableShells.set(TerminalShellType.wsl, IS_WSL); -detectableShells.set(TerminalShellType.zsh, IS_ZSH); -detectableShells.set(TerminalShellType.ksh, IS_KSH); -detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); -detectableShells.set(TerminalShellType.fish, IS_FISH); -detectableShells.set(TerminalShellType.tcshell, IS_TCSHELL); -detectableShells.set(TerminalShellType.cshell, IS_CSHELL); -detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE); -detectableShells.set(TerminalShellType.xonsh, IS_XONSH); - @injectable() export class ShellDetector { - constructor(@inject(IPlatformService) private readonly platform: IPlatformService, - @inject(ICurrentProcess) private readonly currentProcess: ICurrentProcess, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService - ) { } + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @multiInject(IShellDetector) private readonly shellDetectors: IShellDetector[], + ) {} /** * Logic is as follows: * 1. Try to identify the type of the shell based on the name of the terminal. - * 2. Try to identify the type of the shell based on the usettigs in VSC. + * 2. Try to identify the type of the shell based on the settings in VSC. * 3. Try to identify the type of the shell based on the user environment (OS). * 4. If all else fail, use defaults hardcoded (cmd for windows, bash for linux & mac). - * More information here See solution here https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337 - * - * @param {Terminal} [terminal] - * @returns {TerminalShellType} - * @memberof TerminalHelper + * More information here: https://github.com/microsoft/vscode/issues/74233#issuecomment-497527337 */ public identifyTerminalShell(terminal?: Terminal): TerminalShellType { - let shell = TerminalShellType.other; + let shell: TerminalShellType | undefined; const telemetryProperties: ShellIdentificationTelemetry = { failed: true, shellIdentificationSource: 'default', terminalProvided: !!terminal, hasCustomShell: undefined, - hasShellInEnv: undefined + hasShellInEnv: undefined, }; - // Step 1. Determine shell based on the name of the terminal. - if (terminal) { - shell = this.identifyShellByTerminalName(terminal.name, telemetryProperties); - } - - // Step 2. Detemrine shell based on user settings. - if (shell === TerminalShellType.other) { - shell = this.identifyShellFromSettings(telemetryProperties); - } + // Sort in order of priority and then identify the shell. + const shellDetectors = this.shellDetectors.slice().sort((a, b) => b.priority - a.priority); - // Step 3. Determine shell based on user environment. - if (shell === TerminalShellType.other) { - shell = this.identifyShellFromUserEnv(telemetryProperties); + 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}'`); + traceVerbose(`Shell identified as ${shell} ${terminal ? `(Terminal name is ${terminal.name})` : ''}`); // If we could not identify the shell, use the defaults. - return shell === TerminalShellType.other ? (defaultOSShells[this.platform.osType] || TerminalShellType.other) : shell; - } - public getTerminalShellPath(): string | undefined { - const shellConfig = this.workspace.getConfiguration('terminal.integrated.shell'); - let osSection = ''; - switch (this.platform.osType) { - case OSType.Windows: { - osSection = 'windows'; - break; - } - case OSType.OSX: { - osSection = 'osx'; - break; - } - case OSType.Linux: { - osSection = 'linux'; - break; - } - default: { - return ''; - } - } - return shellConfig.get(osSection)!; - } - public getDefaultPlatformShell(): string { - return getDefaultShell(this.platform, this.currentProcess); - } - public identifyShellByTerminalName(name: string, telemetryProperties: ShellIdentificationTelemetry): TerminalShellType { - const shell = Array.from(detectableShells.keys()) - .reduce((matchedShell, shellToDetect) => { - if (matchedShell === TerminalShellType.other && detectableShells.get(shellToDetect)!.test(name)) { - return shellToDetect; - } - return matchedShell; - }, TerminalShellType.other); - traceVerbose(`Terminal name '${name}' identified as shell '${shell}'`); - telemetryProperties.shellIdentificationSource = shell === TerminalShellType.other ? telemetryProperties.shellIdentificationSource : 'terminalName'; - return shell; - } - public identifyShellFromSettings(telemetryProperties: ShellIdentificationTelemetry): TerminalShellType { - const shellPath = this.getTerminalShellPath(); - telemetryProperties.hasCustomShell = !!shellPath; - const shell = shellPath ? this.identifyShellFromShellPath(shellPath) : TerminalShellType.other; - - if (shell !== TerminalShellType.other) { - telemetryProperties.shellIdentificationSource = 'environment'; - } - telemetryProperties.shellIdentificationSource = 'settings'; - traceVerbose(`Shell path from user settings '${shellPath}'`); - return shell; - } - - public identifyShellFromUserEnv(telemetryProperties: ShellIdentificationTelemetry): TerminalShellType { - const shellPath = this.getDefaultPlatformShell(); - telemetryProperties.hasShellInEnv = !!shellPath; - const shell = this.identifyShellFromShellPath(shellPath); - - if (shell !== TerminalShellType.other) { - telemetryProperties.shellIdentificationSource = 'environment'; + 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]; } - traceVerbose(`Shell path from user env '${shellPath}'`); return shell; } - public identifyShellFromShellPath(shellPath: string): TerminalShellType { - const shell = Array.from(detectableShells.keys()) - .reduce((matchedShell, shellToDetect) => { - if (matchedShell === TerminalShellType.other && detectableShells.get(shellToDetect)!.test(shellPath)) { - return shellToDetect; - } - return matchedShell; - }, TerminalShellType.other); - - traceVerbose(`Shell path '${shellPath}'`); - traceVerbose(`Shell path identified as shell '${shell}'`); - 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/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(); +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(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 = createDeferred(); + private disposable?: Disposable; + constructor( + public readonly lockFile: string, + private readonly fs: IFileSystem, + private readonly command: string[], + ) { + this.registerStateUpdate(); + this._completed.promise.finally(() => this.dispose()).ignoreErrors(); + } + public get completed(): Promise { + return this._completed.promise; + } + public dispose() { + if (this.disposable) { + this.disposable.dispose(); + this.disposable = undefined; + } + } + private registerStateUpdate() { + const timeout = setInterval(async () => { + const state = await this.getLockFileState(this.lockFile); + if (state !== this.state) { + traceVerbose(`Command state changed to ${state}. ${this.command.join(' ')}`); + } + this.state = state; + if (state & State.errored) { + const errorContents = await this.fs.readFile(`${this.lockFile}.error`).catch(() => ''); + this._completed.reject( + new Error( + `Command failed with errors, check the terminal for details. Command: ${this.command.join( + ' ', + )}\n${errorContents}`, + ), + ); + } else if (state & State.completed) { + this._completed.resolve(); + } + }, 100); + + this.disposable = { + dispose: () => clearInterval(timeout as any), + }; + } + private async getLockFileState(file: string): Promise { + const source = await this.fs.readFile(file); + let state: State = State.notStarted; + if (source.includes('START')) { + state |= State.started; + } + if (source.includes('END')) { + state |= State.completed; + } + if (source.includes('FAIL')) { + state |= State.completed | State.errored; + } + return state; + } +} + +/** + * This is a decorator class that ensures commands send to a terminal are completed and then execution is returned back to calling code. + * The tecnique used is simple: + * - Instead of sending actual text to a terminal, + * - Send text to a terminal that executes our python file, passing in the original text as args + * - The pthon file will execute the commands as a subprocess + * - At the end of the execution a file is created to singal completion. + */ +export class SynchronousTerminalService implements ITerminalService, Disposable { + private readonly disposables: Disposable[] = []; + public get onDidCloseTerminal(): Event { + return this.terminalService.onDidCloseTerminal; + } + constructor( + @inject(IFileSystem) private readonly fs: IFileSystem, + @inject(IInterpreterService) private readonly interpreter: IInterpreterService, + public readonly terminalService: TerminalService, + private readonly pythonInterpreter?: PythonEnvironment, + ) {} + public dispose() { + this.terminalService.dispose(); + while (this.disposables.length) { + const disposable = this.disposables.shift(); + if (disposable) { + try { + disposable.dispose(); + } catch { + noop(); + } + } else { + break; + } + } + } + public async sendCommand( + command: string, + args: string[], + cancel?: CancellationToken, + swallowExceptions: boolean = true, + ): Promise { + if (!cancel) { + return this.terminalService.sendCommand(command, args); + } + const lockFile = await this.createLockFile(); + const state = new ExecutionState(lockFile.filePath, this.fs, [command, ...args]); + try { + const pythonExec = this.pythonInterpreter || (await this.interpreter.getActiveInterpreter(undefined)); + const sendArgs = internalScripts.shell_exec(command, lockFile.filePath, args); + await this.terminalService.sendCommand(pythonExec?.path || 'python', sendArgs); + const promise = swallowExceptions ? state.completed : state.completed.catch(noop); + await Cancellation.race(() => promise, cancel); + } finally { + state.dispose(); + lockFile.dispose(); + } + } + /** @deprecated */ + public sendText(text: string): Promise { + return this.terminalService.sendText(text); + } + public executeCommand(commandLine: string, isPythonShell: boolean): Promise { + return this.terminalService.executeCommand(commandLine, isPythonShell); + } + public show(preserveFocus?: boolean | undefined): Promise { + return this.terminalService.show(preserveFocus); + } + + private createLockFile(): Promise { + return this.fs.createTemporaryFile('.log').then((l) => { + this.disposables.push(l); + return l; + }); + } +} diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 362d6d7d90d9..3e54458a57fd 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -1,16 +1,21 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, Terminal, Uri } from 'vscode'; -import { PythonInterpreter } from '../../interpreter/contracts'; -import { Resource } from '../types'; +'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' + pipenv = 'pipenv', + pixi = 'pixi', } export enum TerminalShellType { powershell = 'powershell', @@ -23,30 +28,75 @@ export enum TerminalShellType { fish = 'fish', cshell = 'cshell', tcshell = 'tshell', + nushell = 'nushell', wsl = 'wsl', xonsh = 'xonsh', - other = 'other' + other = 'other', } -export interface ITerminalService { +export interface ITerminalService extends IDisposable { readonly onDidCloseTerminal: Event; - sendCommand(command: string, args: string[]): Promise; + /** + * Sends a command to the terminal. + * + * @param {string} command + * @param {string[]} args + * @param {CancellationToken} [cancel] If provided, then wait till the command is executed in the terminal. + * @param {boolean} [swallowExceptions] Whether to swallow exceptions raised as a result of the execution of the command. Defaults to `true`. + * @returns {Promise} + * @memberof ITerminalService + */ + sendCommand( + command: string, + args: string[], + cancel?: CancellationToken, + swallowExceptions?: boolean, + ): Promise; + /** @deprecated */ sendText(text: string): Promise; + executeCommand(commandLine: string, isPythonShell: boolean): Promise; show(preserveFocus?: boolean): Promise; } export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory'); +export type TerminalCreationOptions = { + /** + * Object with environment variables that will be added to the Terminal. + */ + env?: { [key: string]: string | null }; + /** + * Resource identifier. E.g. used to determine python interpreter that needs to be used or environment variables or the like. + * + * @type {Uri} + */ + resource?: Uri; + /** + * Title. + * + * @type {string} + */ + title?: string; + /** + * Associated Python Interpreter. + * + * @type {PythonEnvironment} + */ + interpreter?: PythonEnvironment; + /** + * Whether hidden. + * + * @type {boolean} + */ + hideFromUser?: boolean; +}; + export interface ITerminalServiceFactory { /** - * Gets a terminal service with a specific title. - * If one exists, its returned else a new one is created. - * @param {Uri} resource - * @param {string} title - * @returns {ITerminalService} - * @memberof ITerminalServiceFactory + * 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; } @@ -56,13 +106,28 @@ export interface ITerminalHelper { createTerminal(title?: string): Terminal; identifyTerminalShell(terminal?: Terminal): TerminalShellType; buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string; - getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise; - getEnvironmentActivationShellCommands(resource: Resource, shell: TerminalShellType, interpreter?: PythonInterpreter): Promise; + getEnvironmentActivationCommands( + terminalShellType: TerminalShellType, + resource?: Uri, + interpreter?: PythonEnvironment, + ): Promise; + getEnvironmentActivationShellCommands( + resource: Resource, + shell: TerminalShellType, + interpreter?: PythonEnvironment, + ): Promise; } export const ITerminalActivator = Symbol('ITerminalActivator'); +export type TerminalActivationOptions = { + resource?: Resource; + preserveFocus?: boolean; + interpreter?: PythonEnvironment; + // When sending commands to the terminal, do not display the terminal. + hideFromUser?: boolean; +}; export interface ITerminalActivator { - activateEnvironmentInTerminal(terminal: Terminal, resource: Uri | undefined, preserveFocus?: boolean): Promise; + activateEnvironmentInTerminal(terminal: Terminal, options?: TerminalActivationOptions): Promise; } export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCommandProvider'); @@ -70,10 +135,33 @@ export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCom export interface ITerminalActivationCommandProvider { isShellSupported(targetShell: TerminalShellType): boolean; getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise; - getActivationCommandsForInterpreter(pythonPath: string, targetShell: TerminalShellType): Promise; + getActivationCommandsForInterpreter( + pythonPath: string, + targetShell: TerminalShellType, + ): Promise; } export const ITerminalActivationHandler = Symbol('ITerminalActivationHandler'); export interface ITerminalActivationHandler { - handleActivation(terminal: Terminal, resource: Uri | undefined, preserveFocus: boolean, activated: boolean): Promise; + handleActivation( + terminal: Terminal, + resource: Uri | undefined, + preserveFocus: boolean, + activated: boolean, + ): Promise; +} + +export type ShellIdentificationTelemetry = IEventNamePropertyMapping['TERMINAL_SHELL_IDENTIFICATION']; + +export const IShellDetector = Symbol('IShellDetector'); +/** + * Used to identify a shell. + * Each implemenetion will provide a unique way of identifying the shell. + */ +export interface IShellDetector { + /** + * 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 eee6c32b4ee3..c30ad704b6c1 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -1,36 +1,55 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -import { HexBase64Latin1Encoding } from 'crypto'; import { Socket } from 'net'; -import { Request as RequestResult } from 'request'; -import { ConfigurationTarget, DiagnosticSeverity, Disposable, DocumentSymbolProvider, Event, Extension, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode'; -import { CommandsWithoutArgs } from './application/commands'; +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; +} + +export const ILogOutputChannel = Symbol('ILogOutputChannel'); +export interface ILogOutputChannel extends LogOutputChannel {} export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); -export interface IDocumentSymbolProvider extends DocumentSymbolProvider { } +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 { + /** + * Storage is exposed in this type to make sure folks always use persistent state + * factory to access any type of storage as all storages are tracked there. + */ + readonly storage: Memento; readonly value: T; updateValue(value: T): Promise; } -export type Version = { - raw: string; - major: number; - minor: number; - patch: number; - build: string[]; - prerelease: string[]; + +export type ReadWrite = { + -readonly [P in keyof T]: T[P]; }; export const IPersistentStateFactory = Symbol('IPersistentStateFactory'); @@ -45,75 +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 { - // tslint:disable-next-line: no-any - logError(...args: any[]): void; - // tslint:disable-next-line: no-any - logWarning(...args: any[]): void; - // tslint:disable-next-line: no-any - logInformation(...args: any[]): void; -} - 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; - install(product: Product, resource?: Uri): Promise; - isInstalled(product: Product, resource?: Uri): Promise; - translateProductToModuleName(product: Product, purpose: ModuleNamePurpose): string; -} - + promptToInstall( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + ): Promise; + install( + product: Product, + resource?: InterpreterUri, + cancel?: CancellationToken, + flags?: ModuleInstallFlags, + options?: InstallOptions, + ): Promise; + isInstalled(product: Product, resource?: InterpreterUri): Promise; + isProductVersionCompatible( + product: Product, + semVerRequirement: string, + resource?: InterpreterUri, + ): Promise; + 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; @@ -140,196 +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 pipenvPath: string; readonly poetryPath: string; - readonly downloadLanguageServer: boolean; - readonly jediEnabled: boolean; - readonly jediPath: string; - readonly jediMemoryLimit: number; + readonly pixiToolPath: string; readonly devOptions: string[]; - readonly linting: ILintingSettings; - readonly formatting: IFormattingSettings; readonly testing: ITestingSettings; readonly autoComplete: IAutoCompleteSettings; readonly terminal: ITerminalSettings; - readonly sortImports: ISortImportSettings; - readonly workspaceSymbols: IWorkspaceSymbolSettings; readonly envFile: string; - readonly disableInstallationChecks: boolean; readonly globalModuleInstallation: boolean; - readonly analysis: IAnalysisSettings; - readonly autoUpdateLanguageServer: boolean; - readonly datascience: IDataScienceSettings; - readonly onDidChange: Event; -} -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 ITestingSettings { - readonly promptToConfigure: boolean; - readonly debugPort: number; - readonly nosetestsEnabled: boolean; - nosetestPath: string; - nosetestArgs: string[]; - readonly pytestEnabled: boolean; - pytestPath: string; - pytestArgs: string[]; - readonly unittestEnabled: boolean; - unittestArgs: string[]; - cwd?: string; - readonly autoTestDiscoverOnSaveEnabled: boolean; -} -export interface IPylintCategorySeverity { - readonly convention: DiagnosticSeverity; - readonly refactor: DiagnosticSeverity; - readonly warning: DiagnosticSeverity; - readonly error: DiagnosticSeverity; - readonly fatal: DiagnosticSeverity; -} -export interface 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 type LanguageServerDownloadChannels = 'stable' | 'beta' | 'daily'; -export interface IAnalysisSettings { - readonly downloadChannel?: LanguageServerDownloadChannels; - readonly openFilesOnly: boolean; - readonly typeshedPaths: string[]; - readonly cacheFolderPath: string | null; - readonly errors: string[]; - readonly warnings: string[]; - readonly information: string[]; - readonly disabled: string[]; - readonly traceLogging: boolean; - readonly logLevel: LogLevel; +export interface IREPLSettings { + readonly enableREPLSmartSend: boolean; + readonly sendToNativeREPL: boolean; } -export interface IDataScienceSettings { - allowImportFromNotebook: boolean; - enabled: boolean; - jupyterInterruptTimeout: number; - jupyterLaunchTimeout: number; - jupyterLaunchRetries: number; - jupyterServerURI: string; - notebookFileRoot: string; - changeDirOnImportExport: boolean; - useDefaultConfigForJupyter: boolean; - searchForJupyter: boolean; - allowInput: boolean; - showCellInputCode: boolean; - collapseCellInputCodeByDefault: boolean; - maxOutputSize: number; - sendSelectionToInteractiveWindow: boolean; - markdownRegularExpression: string; - codeRegularExpression: string; - allowLiveShare?: boolean; - errorBackgroundColor: string; - ignoreVscodeTheme?: boolean; - showJupyterVariableExplorer?: boolean; - variableExplorerExclude?: string; - liveShareConnectionTimeout?: number; - decorateCells?: boolean; - enableCellCodeLens?: boolean; - askForLargeDataFrames?: boolean; - enableAutoMoveToNextCell?: boolean; - autoPreviewNotebooksInInteractivePane?: boolean; - allowUnauthorizedRemoteConnection?: boolean; - askForKernelRestart?: boolean; - enablePlotViewer?: boolean; - codeLenses?: string; - ptvsdDistPath?: string; +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 IAutoCompleteSettings { + readonly extraPaths: string[]; } export const IConfigurationService = Symbol('IConfigurationService'); export interface IConfigurationService { + readonly onDidChange: Event; getSettings(resource?: Uri): IPythonSettings; isTestExecution(): boolean; - updateSetting(setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; - updateSectionSetting(section: string, setting: string, value?: {}, resource?: Uri, configTarget?: ConfigurationTarget): Promise; + updateSetting(setting: string, value?: unknown, resource?: Uri, configTarget?: ConfigurationTarget): Promise; + updateSectionSetting( + section: string, + setting: string, + value?: unknown, + resource?: Uri, + configTarget?: ConfigurationTarget, + ): Promise; +} + +/** + * Carries various tool execution path settings. For eg. pipenvPath, condaPath, pytestPath etc. These can be + * potentially used in discovery, autoselection, activation, installers, execution etc. And so should be a + * common interface to all the components. + */ +export const IToolExecutionPath = Symbol('IToolExecutionPath'); +export interface IToolExecutionPath { + readonly executable: string; +} +export enum ToolExecutionPath { + pipenv = 'pipenv', + // Gradually populate this list with tools as they come up. } export const ISocketServer = Symbol('ISocketServer'); @@ -345,12 +259,6 @@ export type DownloadOptions = { * @type {('Downloading ... ' | string)} */ progressMessagePrefix: 'Downloading ... ' | string; - /** - * Output panel into which progress information is written. - * - * @type {IOutputChannel} - */ - outputChannel?: IOutputChannel; /** * Extension of file that'll be created when downloading the file. * @@ -359,47 +267,22 @@ export type DownloadOptions = { extension: 'tmp' | string; }; -export const IFileDownloader = Symbol('IFileDownloader'); -/** - * File downloader, that'll display progress in the status bar. - * - * @export - * @interface IFileDownloader - */ -export interface IFileDownloader { - /** - * Download file and display progress in statusbar. - * Optionnally display progress in the provided output channel. - * - * @param {string} uri - * @param {DownloadOptions} options - * @returns {Promise} - * @memberof IFileDownloader - */ - downloadFile(uri: string, options: DownloadOptions): Promise; -} - -export const IHttpClient = Symbol('IHttpClient'); -export interface IHttpClient { - downloadFile(uri: string): Promise; - /** - * Downloads file from uri as string and parses them into JSON objects - * @param uri The uri to download the JSON from - * @param strict Set `false` to allow trailing comma and comments in the JSON, defaults to `true` - */ - getJSON(uri: string, strict?: boolean): Promise; -} - 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[]; + + readonly all: readonly Extension[]; + + /** + * An event which fires when `extensions.all` changes. This can happen when extensions are + * installed, uninstalled, enabled or disabled. + */ + readonly onDidChange: Event; /** * Get an extension by its full identifier in the form of: `publisher.name`. @@ -407,8 +290,8 @@ export interface IExtensions { * @param extensionId An extension identifier. * @return An extension or `undefined`. */ - // tslint:disable-next-line:no-any - getExtension(extensionId: string): Extension | undefined; + + getExtension(extensionId: string): Extension | undefined; /** * Get an extension its full identifier in the form of: `publisher.name`. @@ -417,6 +300,11 @@ export interface IExtensions { * @return An extension or `undefined`. */ getExtension(extensionId: string): Extension | undefined; + + /** + * Determines which extension called into our extension code based on call stacks. + */ + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }>; } export const IBrowserService = Symbol('IBrowserService'); @@ -424,107 +312,55 @@ export interface IBrowserService { launch(url: string): void; } -export const IPythonExtensionBanner = Symbol('IPythonExtensionBanner'); -export interface IPythonExtensionBanner { - readonly enabled: boolean; - showBanner(): Promise; -} -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 const BANNER_NAME_INTERACTIVE_SHIFTENTER: string = 'InteractiveShiftEnterBanner'; - -export type DeprecatedSettingAndValue = { - setting: string; - values?: {}[]; -}; - -export type DeprecatedFeatureInfo = { - doNotDisplayPromptStateKey: string; - message: string; - moreInfoUrl: string; - commands?: CommandsWithoutArgs[]; - setting?: DeprecatedSettingAndValue; -}; - -export const IFeatureDeprecationManager = Symbol('IFeatureDeprecationManager'); - -export interface IFeatureDeprecationManager extends Disposable { - initialize(): void; - registerDeprecation(deprecatedInfo: DeprecatedFeatureInfo): void; -} - -export const IEditorUtils = Symbol('IEditorUtils'); -export interface IEditorUtils { - getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; -} - -export interface IDisposable { - dispose(): void | undefined; -} -export interface IAsyncDisposable { - dispose(): Promise; -} - /** * Stores hash formats */ export interface IHashFormat { - 'number': number; // If hash format is a number - 'string': string; // If hash format is a string + number: number; // If hash format is a number + string: string; // If hash format is a string } /** - * Interface used to implement cryptography tools + * Experiment service leveraging VS Code's experiment framework. */ -export const ICryptoUtils = Symbol('ICryptoUtils'); -export interface ICryptoUtils { - /** - * Creates hash using the data and encoding specified - * @returns hash as number, or string - * @param data The string to hash - * @param encoding Data encoding to use - * @param hashFormat Return format of the hash, number or string - */ - createHash(data: string, encoding: HexBase64Latin1Encoding, hashFormat: E): IHashFormat[E]; +export const IExperimentService = Symbol('IExperimentService'); +export interface IExperimentService { + activate(): Promise; + inExperiment(experimentName: string): Promise; + inExperimentSync(experimentName: string): boolean; + getExperimentValue(experimentName: string): Promise; } -export const IAsyncDisposableRegistry = Symbol('IAsyncDisposableRegistry'); -export interface IAsyncDisposableRegistry extends IAsyncDisposable { - push(disposable: IDisposable | IAsyncDisposable): void; +export type InterpreterConfigurationScope = { uri: Resource; configTarget: ConfigurationTarget }; +export type InspectInterpreterSettingType = { + globalValue?: string; + workspaceValue?: string; + workspaceFolderValue?: string; +}; + +/** + * Interface used to access current Interpreter Path + */ +export const IInterpreterPathService = Symbol('IInterpreterPathService'); +export interface IInterpreterPathService { + onDidChange: Event; + get(resource: Resource): string; + inspect(resource: Resource): InspectInterpreterSettingType; + update(resource: Resource, configTarget: ConfigurationTarget, value: string | undefined): Promise; + copyOldInterpreterStorageValuesToNew(resource: Resource): Promise; } -/* ABExperiments field carries the identity, and the range of the experiment, - where the experiment is valid for users falling between the number 'min' and 'max' - More details: https://en.wikipedia.org/wiki/A/B_testing -*/ -export type ABExperiments = { - name: string; // Name of the experiment - salt: string; // Salt string for the experiment - min: number; // Lower limit for the experiment - max: number; // Upper limit for the experiment -}[]; +export type DefaultLSType = LanguageServerType.Jedi | LanguageServerType.Node; /** - * Interface used to implement AB testing + * 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 IExperimentsManager = Symbol('IExperimentsManager'); -export interface IExperimentsManager { - /** - * Checks if experiments are enabled, sets required environment to be used for the experiments, logs experiment groups - */ - activate(): Promise; - - /** - * Checks if user is in experiment or not - * @param experimentName Name of the experiment - * @returns `true` if user is in experiment, `false` if user is not in experiment - */ - inExperiment(experimentName: string): boolean; +export const IDefaultLanguageServer = Symbol('IDefaultLanguageServer'); - /** - * Sends experiment telemetry if user is in experiment - * @param experimentName Name of the experiment - */ - sendTelemetryIfInExperiment(experimentName: string): void; +export interface IDefaultLanguageServer { + readonly defaultLSType: DefaultLSType; } diff --git a/src/client/common/utils/arrayUtils.ts b/src/client/common/utils/arrayUtils.ts new file mode 100644 index 000000000000..5ec671118297 --- /dev/null +++ b/src/client/common/utils/arrayUtils.ts @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Returns the elements of an array that meet the condition specified in an async callback function. + * @param asyncPredicate The filter method calls the async predicate function one time for each element in the array. + */ +export async function asyncFilter(arr: T[], asyncPredicate: (value: T) => Promise): Promise { + const results = await Promise.all(arr.map(asyncPredicate)); + return arr.filter((_v, index) => results[index]); +} + +export async function asyncForEach(arr: T[], asyncFunc: (value: T) => Promise): Promise { + await Promise.all(arr.map(asyncFunc)); +} diff --git a/src/client/common/utils/async.ts b/src/client/common/utils/async.ts index 885706c60fd7..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) : Promise { +export async function sleep(timeout: number): Promise { return new Promise((resolve) => { setTimeout(() => resolve(timeout), timeout); }); } -//====================== +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function isThenable(v: any): v is Thenable { + return typeof v?.then === 'function'; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function isPromise(v: any): v is Promise { + return typeof v?.then === 'function' && typeof v?.catch === 'function'; +} + // Deferred -// tslint:disable-next-line:interface-name export interface Deferred { readonly promise: Promise; readonly resolved: boolean; readonly rejected: boolean; readonly completed: boolean; resolve(value?: T | PromiseLike): void; - // tslint:disable-next-line:no-any - reject(reason?: any): void; + reject(reason?: string | Error | Record | unknown): void; } class DeferredImpl implements Deferred { - private _resolve!: (value?: T | PromiseLike) => void; - // tslint:disable-next-line:no-any - private _reject!: (reason?: any) => void; - private _resolved: boolean = false; - private _rejected: boolean = false; + private _resolve!: (value: T | PromiseLike) => void; + + private _reject!: (reason?: string | Error | Record) => void; + + private _resolved = false; + + private _rejected = false; + private _promise: Promise; - // tslint:disable-next-line:no-any + + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(private scope: any = null) { - // tslint:disable-next-line:promise-must-complete this._promise = new Promise((res, rej) => { this._resolve = res; this._reject = rej; }); } - public resolve(_value?: T | PromiseLike) { - // tslint:disable-next-line:no-any - this._resolve.apply(this.scope ? this.scope : this, arguments as any); + + public resolve(_value: T | PromiseLike) { + if (this.completed) { + return; + } + this._resolve.apply(this.scope ? this.scope : this, [_value]); this._resolved = true; } - // tslint:disable-next-line:no-any - public reject(_reason?: any) { - // tslint:disable-next-line:no-any - this._reject.apply(this.scope ? this.scope : this, arguments as any); + + public reject(_reason?: string | Error | Record) { + if (this.completed) { + return; + } + this._reject.apply(this.scope ? this.scope : this, [_reason]); this._rejected = true; } + get promise(): Promise { return this._promise; } + get resolved(): boolean { return this._resolved; } + get rejected(): boolean { return this._rejected; } + get completed(): boolean { return this._rejected || this._resolved; } } -// tslint:disable-next-line:no-any -export function createDeferred(scope: any = null): Deferred { + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export function createDeferred(scope: any = null): Deferred { return new DeferredImpl(scope); } export function createDeferredFrom(...promises: Promise[]): Deferred { const deferred = createDeferred(); Promise.all(promises) - // tslint:disable-next-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any .then(deferred.resolve.bind(deferred) as any) - // tslint:disable-next-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any .catch(deferred.reject.bind(deferred) as any); return deferred; } export function createDeferredFromPromise(promise: Promise): Deferred { const deferred = createDeferred(); - 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 extends AsyncIterator {} + +export interface IAsyncIterableIterator extends IAsyncIterator, AsyncIterable {} + +/** + * An iterator that yields nothing. + */ +export function iterEmpty(): IAsyncIterableIterator { + return ((async function* () { + /** No body. */ + })() as unknown) as IAsyncIterableIterator; +} + +type NextResult = { index: number } & ( + | { result: IteratorResult; err: null } + | { result: null; err: Error } +); +async function getNext(it: AsyncIterator, indexMaybe?: number): Promise> { + const index = indexMaybe === undefined ? -1 : indexMaybe; + try { + const result = await it.next(); + return { index, result, err: null }; + } catch (err) { + return { index, err: err as Error, result: null }; + } +} + +const NEVER: Promise = new Promise(() => { + /** No body. */ +}); + +/** + * Yield everything produced by the given iterators as soon as each is ready. + * + * When one of the iterators has something to yield then it gets yielded + * right away, regardless of where the iterator is located in the array + * of iterators. + * + * @param iterators - the async iterators from which to yield items + * @param onError - called/awaited once for each iterator that fails + */ +export async function* chain( + iterators: AsyncIterator[], + onError?: (err: Error, index: number) => Promise, + // Ultimately we may also want to support cancellation. +): IAsyncIterableIterator { + const promises = iterators.map(getNext); + let numRunning = iterators.length; + + while (numRunning > 0) { + // Promise.race will not fail, because each promise calls getNext, + // Which handles failures by wrapping each iterator in a try/catch block. + const { index, result, err } = await Promise.race(promises); + + if (err !== null) { + promises[index] = NEVER as Promise>; + numRunning -= 1; + if (onError !== undefined) { + await onError(err, index); + } + // XXX Log the error. + } else if (result!.done) { + promises[index] = NEVER as Promise>; + numRunning -= 1; + // If R is void then result.value will be undefined. + if (result!.value !== undefined) { + yield result!.value; + } + } else { + promises[index] = getNext(iterators[index], index); + // Only the "return" result can be undefined (void), + // so we're okay here. + yield result!.value as T; + } + } +} + +/** + * Map the async function onto the items and yield the results. + * + * @param items - the items to map onto and iterate + * @param func - the async function to apply for each item + * @param race - if `true` (the default) then results are yielded + * potentially out of order, as soon as each is ready + */ +export async function* mapToIterator( + items: T[], + func: (item: T) => Promise, + race = true, +): IAsyncIterableIterator { + if (race) { + const iterators = items.map((item) => { + async function* generator() { + yield func(item); + } + return generator(); + }); + yield* iterable(chain(iterators)); + } else { + yield* items.map(func); + } +} + +/** + * Convert an iterator into an iterable, if it isn't one already. + */ +export function iterable(iterator: IAsyncIterator): IAsyncIterableIterator { + const it = iterator as IAsyncIterableIterator; + if (it[Symbol.asyncIterator] === undefined) { + it[Symbol.asyncIterator] = () => it; + } + return it; +} + +/** + * Get everything yielded by the iterator. + */ +export async function flattenIterator(iterator: IAsyncIterator): Promise { + const results: T[] = []; + for await (const item of iterable(iterator)) { + results.push(item); + } + return results; +} + +/** + * Get everything yielded by the iterable. + */ +export async function flattenIterable(iterableItem: AsyncIterable): Promise { + const results: T[] = []; + for await (const item of iterableItem) { + results.push(item); + } + return results; +} + +/** + * Wait for a condition to be fulfilled within a timeout. + */ +export async function waitForCondition( + condition: () => Promise, + timeoutMs: number, + errorMessage: string, +): Promise { + return new Promise(async (resolve, reject) => { + const timeout = setTimeout(() => { + clearTimeout(timeout); + + clearTimeout(timer); + reject(new Error(errorMessage)); + }, timeoutMs); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { + return; + } + clearTimeout(timeout); + clearTimeout(timer); + resolve(); + }, 10); + }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function isPromiseLike(v: any): v is PromiseLike { + return typeof v?.then === 'function'; +} + +export function raceTimeout(timeout: number, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise; +export function raceTimeout(timeout: number, defaultValue: T, ...promises: Promise[]): Promise { + const resolveValue = isPromiseLike(defaultValue) ? undefined : defaultValue; + if (isPromiseLike(defaultValue)) { + promises.push((defaultValue as unknown) as Promise); + } + + let promiseResolve: ((value: T) => void) | undefined = undefined; + + const timer = setTimeout(() => promiseResolve?.((resolveValue as unknown) as T), timeout); + + return Promise.race([ + Promise.race(promises).finally(() => clearTimeout(timer)), + new Promise((resolve) => (promiseResolve = resolve)), + ]); +} diff --git a/src/client/common/utils/cacheUtils.ts b/src/client/common/utils/cacheUtils.ts index 96c65838828f..6101b3ef928f 100644 --- a/src/client/common/utils/cacheUtils.ts +++ b/src/client/common/utils/cacheUtils.ts @@ -3,88 +3,37 @@ 'use strict'; -// tslint:disable:no-any no-require-imports +const globalCacheStore = new Map(); -import { Uri } from 'vscode'; -import '../../common/extensions'; -import { Resource } from '../types'; - -type VSCodeType = typeof import('vscode'); -type CacheData = { - value: unknown; - expiry: number; -}; -const resourceSpecificCacheStores = new Map>(); - -/** - * Get a cache key specific to a resource (i.e. workspace) - * This key will be used to cache interpreter related data, hence the Python Path - * used in a workspace will affect the cache key. - * @param {String} keyPrefix - * @param {Resource} resource - * @param {VSCodeType} [vscode=require('vscode')] - * @returns - */ -function getCacheKey(resource: Resource, vscode: VSCodeType = require('vscode')) { - const section = vscode.workspace.getConfiguration('python', vscode.Uri.file(__filename)); - if (!section) { - return 'python'; - } - const globalPythonPath = section.inspect('pythonPath')!.globalValue || 'python'; - // Get the workspace related to this resource. - if (!resource || !Array.isArray(vscode.workspace.workspaceFolders) || vscode.workspace.workspaceFolders.length === 0) { - return globalPythonPath; - } - const folder = resource ? vscode.workspace.getWorkspaceFolder(resource) : vscode.workspace.workspaceFolders[0]; - if (!folder) { - return globalPythonPath; - } - const workspacePythonPath = vscode.workspace.getConfiguration('python', resource).get('pythonPath') || 'python'; - return `${folder.uri.fsPath}-${workspacePythonPath}`; -} -/** - * Gets the cache store for a resource that's specific to the interpreter. - * @param {string} keyPrefix - * @param {Resource} resource - * @param {VSCodeType} [vscode=require('vscode')] - * @returns - */ -function getCacheStore(resource: Resource, vscode: VSCodeType = require('vscode')) { - const key = getCacheKey(resource, vscode); - if (!resourceSpecificCacheStores.has(key)) { - resourceSpecificCacheStores.set(key, new Map()); - } - return resourceSpecificCacheStores.get(key)!; +// Gets a cache store to be used to store return values of methods or any other. +export function getGlobalCacheStore() { + return globalCacheStore; } -function getCacheKeyFromFunctionArgs(keyPrefix: string, fnArgs: any[]): string { - const argsKey = fnArgs.map(arg => `${JSON.stringify(arg)}`).join('-Arg-Separator-'); +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() { - resourceSpecificCacheStores.clear(); + globalCacheStore.clear(); } -export class InMemoryInterpreterSpecificCache { - private readonly resource: Resource; - private readonly args: any[]; - constructor(private readonly keyPrefix: string, - protected readonly expiryDurationMs: number, - args: [Uri | undefined, ...any[]], - private readonly vscode: VSCodeType = require('vscode')) { - this.resource = args[0]; - this.args = args.slice(1); - } +type CacheData = { + value: T; + expiry: number; +}; + +/** + * InMemoryCache caches a single value up until its expiry. + */ +export class InMemoryCache { + private cacheData?: CacheData; + + constructor(protected readonly expiryDurationMs: number) {} public get hasData() { - const store = getCacheStore(this.resource, this.vscode); - const key = getCacheKeyFromFunctionArgs(this.keyPrefix, this.args); - const data = store.get(key); - if (!store.has(key) || !data) { - return false; - } - if (this.hasExpired(data.expiry)) { - store.delete(key); + if (!this.cacheData || this.hasExpired(this.cacheData.expiry)) { + this.cacheData = undefined; return false; } return true; @@ -93,33 +42,28 @@ export class InMemoryInterpreterSpecificCache { * Returns undefined if there is no data. * Uses `hasData` to determine whether any cached data exists. * + * @readonly * @type {(T | undefined)} - * @memberof InMemoryInterpreterSpecificCache + * @memberof InMemoryCache */ public get data(): T | undefined { if (!this.hasData) { return; } - const store = getCacheStore(this.resource, this.vscode); - const key = getCacheKeyFromFunctionArgs(this.keyPrefix, this.args); - const data = store.get(key); - if (!store.has(key) || !data) { - return; - } - return data.value as T; + return this.cacheData?.value; } public set data(value: T | undefined) { - const store = getCacheStore(this.resource, this.vscode); - const key = getCacheKeyFromFunctionArgs(this.keyPrefix, this.args); - store.set(key, { - expiry: this.calculateExpiry(), - value - }); + if (value !== undefined) { + this.cacheData = { + expiry: this.calculateExpiry(), + value, + }; + } else { + this.cacheData = undefined; + } } public clear() { - const store = getCacheStore(this.resource, this.vscode); - const key = getCacheKeyFromFunctionArgs(this.keyPrefix, this.args); - store.delete(key); + this.cacheData = undefined; } /** @@ -130,7 +74,7 @@ export class InMemoryInterpreterSpecificCache { * @returns true if the data expired, false otherwise. */ protected hasExpired(expiry: number): boolean { - return expiry < Date.now(); + return expiry <= Date.now(); } /** diff --git a/src/client/common/utils/charCode.ts b/src/client/common/utils/charCode.ts new file mode 100644 index 000000000000..ba76626bfcbb --- /dev/null +++ b/src/client/common/utils/charCode.ts @@ -0,0 +1,453 @@ +//!!! DO NOT modify, this file was COPIED from 'microsoft/vscode' + +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/ + +/** + * An inlined enum containing useful character codes (to be used with String.charCodeAt). + * Please leave the const keyword such that it gets inlined when compiled to JavaScript! + */ +export const enum CharCode { + Null = 0, + /** + * The `\b` character. + */ + Backspace = 8, + /** + * The `\t` character. + */ + Tab = 9, + /** + * The `\n` character. + */ + LineFeed = 10, + /** + * The `\r` character. + */ + CarriageReturn = 13, + Space = 32, + /** + * The `!` character. + */ + ExclamationMark = 33, + /** + * The `"` character. + */ + DoubleQuote = 34, + /** + * The `#` character. + */ + Hash = 35, + /** + * The `$` character. + */ + DollarSign = 36, + /** + * The `%` character. + */ + PercentSign = 37, + /** + * The `&` character. + */ + Ampersand = 38, + /** + * The `'` character. + */ + SingleQuote = 39, + /** + * The `(` character. + */ + OpenParen = 40, + /** + * The `)` character. + */ + CloseParen = 41, + /** + * The `*` character. + */ + Asterisk = 42, + /** + * The `+` character. + */ + Plus = 43, + /** + * The `,` character. + */ + Comma = 44, + /** + * The `-` character. + */ + Dash = 45, + /** + * The `.` character. + */ + Period = 46, + /** + * The `/` character. + */ + Slash = 47, + + Digit0 = 48, + Digit1 = 49, + Digit2 = 50, + Digit3 = 51, + Digit4 = 52, + Digit5 = 53, + Digit6 = 54, + Digit7 = 55, + Digit8 = 56, + Digit9 = 57, + + /** + * The `:` character. + */ + Colon = 58, + /** + * The `;` character. + */ + Semicolon = 59, + /** + * The `<` character. + */ + LessThan = 60, + /** + * The `=` character. + */ + Equals = 61, + /** + * The `>` character. + */ + GreaterThan = 62, + /** + * The `?` character. + */ + QuestionMark = 63, + /** + * The `@` character. + */ + AtSign = 64, + + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + + /** + * The `[` character. + */ + OpenSquareBracket = 91, + /** + * The `\` character. + */ + Backslash = 92, + /** + * The `]` character. + */ + CloseSquareBracket = 93, + /** + * The `^` character. + */ + Caret = 94, + /** + * The `_` character. + */ + Underline = 95, + /** + * The ``(`)`` character. + */ + BackTick = 96, + + a = 97, + b = 98, + c = 99, + d = 100, + e = 101, + f = 102, + g = 103, + h = 104, + i = 105, + j = 106, + k = 107, + l = 108, + m = 109, + n = 110, + o = 111, + p = 112, + q = 113, + r = 114, + s = 115, + t = 116, + u = 117, + v = 118, + w = 119, + x = 120, + y = 121, + z = 122, + + /** + * The `{` character. + */ + OpenCurlyBrace = 123, + /** + * The `|` character. + */ + Pipe = 124, + /** + * The `}` character. + */ + CloseCurlyBrace = 125, + /** + * The `~` character. + */ + Tilde = 126, + + /** + * The   (no-break space) character. + * Unicode Character 'NO-BREAK SPACE' (U+00A0) + */ + NoBreakSpace = 160, + + U_Combining_Grave_Accent = 0x0300, // U+0300 Combining Grave Accent + U_Combining_Acute_Accent = 0x0301, // U+0301 Combining Acute Accent + U_Combining_Circumflex_Accent = 0x0302, // U+0302 Combining Circumflex Accent + U_Combining_Tilde = 0x0303, // U+0303 Combining Tilde + U_Combining_Macron = 0x0304, // U+0304 Combining Macron + U_Combining_Overline = 0x0305, // U+0305 Combining Overline + U_Combining_Breve = 0x0306, // U+0306 Combining Breve + U_Combining_Dot_Above = 0x0307, // U+0307 Combining Dot Above + U_Combining_Diaeresis = 0x0308, // U+0308 Combining Diaeresis + U_Combining_Hook_Above = 0x0309, // U+0309 Combining Hook Above + U_Combining_Ring_Above = 0x030a, // U+030A Combining Ring Above + U_Combining_Double_Acute_Accent = 0x030b, // U+030B Combining Double Acute Accent + U_Combining_Caron = 0x030c, // U+030C Combining Caron + U_Combining_Vertical_Line_Above = 0x030d, // U+030D Combining Vertical Line Above + U_Combining_Double_Vertical_Line_Above = 0x030e, // U+030E Combining Double Vertical Line Above + U_Combining_Double_Grave_Accent = 0x030f, // U+030F Combining Double Grave Accent + U_Combining_Candrabindu = 0x0310, // U+0310 Combining Candrabindu + U_Combining_Inverted_Breve = 0x0311, // U+0311 Combining Inverted Breve + U_Combining_Turned_Comma_Above = 0x0312, // U+0312 Combining Turned Comma Above + U_Combining_Comma_Above = 0x0313, // U+0313 Combining Comma Above + U_Combining_Reversed_Comma_Above = 0x0314, // U+0314 Combining Reversed Comma Above + U_Combining_Comma_Above_Right = 0x0315, // U+0315 Combining Comma Above Right + U_Combining_Grave_Accent_Below = 0x0316, // U+0316 Combining Grave Accent Below + U_Combining_Acute_Accent_Below = 0x0317, // U+0317 Combining Acute Accent Below + U_Combining_Left_Tack_Below = 0x0318, // U+0318 Combining Left Tack Below + U_Combining_Right_Tack_Below = 0x0319, // U+0319 Combining Right Tack Below + U_Combining_Left_Angle_Above = 0x031a, // U+031A Combining Left Angle Above + U_Combining_Horn = 0x031b, // U+031B Combining Horn + U_Combining_Left_Half_Ring_Below = 0x031c, // U+031C Combining Left Half Ring Below + U_Combining_Up_Tack_Below = 0x031d, // U+031D Combining Up Tack Below + U_Combining_Down_Tack_Below = 0x031e, // U+031E Combining Down Tack Below + U_Combining_Plus_Sign_Below = 0x031f, // U+031F Combining Plus Sign Below + U_Combining_Minus_Sign_Below = 0x0320, // U+0320 Combining Minus Sign Below + U_Combining_Palatalized_Hook_Below = 0x0321, // U+0321 Combining Palatalized Hook Below + U_Combining_Retroflex_Hook_Below = 0x0322, // U+0322 Combining Retroflex Hook Below + U_Combining_Dot_Below = 0x0323, // U+0323 Combining Dot Below + U_Combining_Diaeresis_Below = 0x0324, // U+0324 Combining Diaeresis Below + U_Combining_Ring_Below = 0x0325, // U+0325 Combining Ring Below + U_Combining_Comma_Below = 0x0326, // U+0326 Combining Comma Below + U_Combining_Cedilla = 0x0327, // U+0327 Combining Cedilla + U_Combining_Ogonek = 0x0328, // U+0328 Combining Ogonek + U_Combining_Vertical_Line_Below = 0x0329, // U+0329 Combining Vertical Line Below + U_Combining_Bridge_Below = 0x032a, // U+032A Combining Bridge Below + U_Combining_Inverted_Double_Arch_Below = 0x032b, // U+032B Combining Inverted Double Arch Below + U_Combining_Caron_Below = 0x032c, // U+032C Combining Caron Below + U_Combining_Circumflex_Accent_Below = 0x032d, // U+032D Combining Circumflex Accent Below + U_Combining_Breve_Below = 0x032e, // U+032E Combining Breve Below + U_Combining_Inverted_Breve_Below = 0x032f, // U+032F Combining Inverted Breve Below + U_Combining_Tilde_Below = 0x0330, // U+0330 Combining Tilde Below + U_Combining_Macron_Below = 0x0331, // U+0331 Combining Macron Below + U_Combining_Low_Line = 0x0332, // U+0332 Combining Low Line + U_Combining_Double_Low_Line = 0x0333, // U+0333 Combining Double Low Line + U_Combining_Tilde_Overlay = 0x0334, // U+0334 Combining Tilde Overlay + U_Combining_Short_Stroke_Overlay = 0x0335, // U+0335 Combining Short Stroke Overlay + U_Combining_Long_Stroke_Overlay = 0x0336, // U+0336 Combining Long Stroke Overlay + U_Combining_Short_Solidus_Overlay = 0x0337, // U+0337 Combining Short Solidus Overlay + U_Combining_Long_Solidus_Overlay = 0x0338, // U+0338 Combining Long Solidus Overlay + U_Combining_Right_Half_Ring_Below = 0x0339, // U+0339 Combining Right Half Ring Below + U_Combining_Inverted_Bridge_Below = 0x033a, // U+033A Combining Inverted Bridge Below + U_Combining_Square_Below = 0x033b, // U+033B Combining Square Below + U_Combining_Seagull_Below = 0x033c, // U+033C Combining Seagull Below + U_Combining_X_Above = 0x033d, // U+033D Combining X Above + U_Combining_Vertical_Tilde = 0x033e, // U+033E Combining Vertical Tilde + U_Combining_Double_Overline = 0x033f, // U+033F Combining Double Overline + U_Combining_Grave_Tone_Mark = 0x0340, // U+0340 Combining Grave Tone Mark + U_Combining_Acute_Tone_Mark = 0x0341, // U+0341 Combining Acute Tone Mark + U_Combining_Greek_Perispomeni = 0x0342, // U+0342 Combining Greek Perispomeni + U_Combining_Greek_Koronis = 0x0343, // U+0343 Combining Greek Koronis + U_Combining_Greek_Dialytika_Tonos = 0x0344, // U+0344 Combining Greek Dialytika Tonos + U_Combining_Greek_Ypogegrammeni = 0x0345, // U+0345 Combining Greek Ypogegrammeni + U_Combining_Bridge_Above = 0x0346, // U+0346 Combining Bridge Above + U_Combining_Equals_Sign_Below = 0x0347, // U+0347 Combining Equals Sign Below + U_Combining_Double_Vertical_Line_Below = 0x0348, // U+0348 Combining Double Vertical Line Below + U_Combining_Left_Angle_Below = 0x0349, // U+0349 Combining Left Angle Below + U_Combining_Not_Tilde_Above = 0x034a, // U+034A Combining Not Tilde Above + U_Combining_Homothetic_Above = 0x034b, // U+034B Combining Homothetic Above + U_Combining_Almost_Equal_To_Above = 0x034c, // U+034C Combining Almost Equal To Above + U_Combining_Left_Right_Arrow_Below = 0x034d, // U+034D Combining Left Right Arrow Below + U_Combining_Upwards_Arrow_Below = 0x034e, // U+034E Combining Upwards Arrow Below + U_Combining_Grapheme_Joiner = 0x034f, // U+034F Combining Grapheme Joiner + U_Combining_Right_Arrowhead_Above = 0x0350, // U+0350 Combining Right Arrowhead Above + U_Combining_Left_Half_Ring_Above = 0x0351, // U+0351 Combining Left Half Ring Above + U_Combining_Fermata = 0x0352, // U+0352 Combining Fermata + U_Combining_X_Below = 0x0353, // U+0353 Combining X Below + U_Combining_Left_Arrowhead_Below = 0x0354, // U+0354 Combining Left Arrowhead Below + U_Combining_Right_Arrowhead_Below = 0x0355, // U+0355 Combining Right Arrowhead Below + U_Combining_Right_Arrowhead_And_Up_Arrowhead_Below = 0x0356, // U+0356 Combining Right Arrowhead And Up Arrowhead Below + U_Combining_Right_Half_Ring_Above = 0x0357, // U+0357 Combining Right Half Ring Above + U_Combining_Dot_Above_Right = 0x0358, // U+0358 Combining Dot Above Right + U_Combining_Asterisk_Below = 0x0359, // U+0359 Combining Asterisk Below + U_Combining_Double_Ring_Below = 0x035a, // U+035A Combining Double Ring Below + U_Combining_Zigzag_Above = 0x035b, // U+035B Combining Zigzag Above + U_Combining_Double_Breve_Below = 0x035c, // U+035C Combining Double Breve Below + U_Combining_Double_Breve = 0x035d, // U+035D Combining Double Breve + U_Combining_Double_Macron = 0x035e, // U+035E Combining Double Macron + U_Combining_Double_Macron_Below = 0x035f, // U+035F Combining Double Macron Below + U_Combining_Double_Tilde = 0x0360, // U+0360 Combining Double Tilde + U_Combining_Double_Inverted_Breve = 0x0361, // U+0361 Combining Double Inverted Breve + U_Combining_Double_Rightwards_Arrow_Below = 0x0362, // U+0362 Combining Double Rightwards Arrow Below + U_Combining_Latin_Small_Letter_A = 0x0363, // U+0363 Combining Latin Small Letter A + U_Combining_Latin_Small_Letter_E = 0x0364, // U+0364 Combining Latin Small Letter E + U_Combining_Latin_Small_Letter_I = 0x0365, // U+0365 Combining Latin Small Letter I + U_Combining_Latin_Small_Letter_O = 0x0366, // U+0366 Combining Latin Small Letter O + U_Combining_Latin_Small_Letter_U = 0x0367, // U+0367 Combining Latin Small Letter U + U_Combining_Latin_Small_Letter_C = 0x0368, // U+0368 Combining Latin Small Letter C + U_Combining_Latin_Small_Letter_D = 0x0369, // U+0369 Combining Latin Small Letter D + U_Combining_Latin_Small_Letter_H = 0x036a, // U+036A Combining Latin Small Letter H + U_Combining_Latin_Small_Letter_M = 0x036b, // U+036B Combining Latin Small Letter M + U_Combining_Latin_Small_Letter_R = 0x036c, // U+036C Combining Latin Small Letter R + U_Combining_Latin_Small_Letter_T = 0x036d, // U+036D Combining Latin Small Letter T + U_Combining_Latin_Small_Letter_V = 0x036e, // U+036E Combining Latin Small Letter V + U_Combining_Latin_Small_Letter_X = 0x036f, // U+036F Combining Latin Small Letter X + + /** + * Unicode Character 'LINE SEPARATOR' (U+2028) + * http://www.fileformat.info/info/unicode/char/2028/index.htm + */ + LINE_SEPARATOR = 0x2028, + /** + * Unicode Character 'PARAGRAPH SEPARATOR' (U+2029) + * http://www.fileformat.info/info/unicode/char/2029/index.htm + */ + PARAGRAPH_SEPARATOR = 0x2029, + /** + * Unicode Character 'NEXT LINE' (U+0085) + * http://www.fileformat.info/info/unicode/char/0085/index.htm + */ + NEXT_LINE = 0x0085, + + // http://www.fileformat.info/info/unicode/category/Sk/list.htm + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_CIRCUMFLEX = 0x005e, // U+005E CIRCUMFLEX + // eslint-disable-next-line @typescript-eslint/no-duplicate-enum-values + U_GRAVE_ACCENT = 0x0060, // U+0060 GRAVE ACCENT + U_DIAERESIS = 0x00a8, // U+00A8 DIAERESIS + U_MACRON = 0x00af, // U+00AF MACRON + U_ACUTE_ACCENT = 0x00b4, // U+00B4 ACUTE ACCENT + U_CEDILLA = 0x00b8, // U+00B8 CEDILLA + U_MODIFIER_LETTER_LEFT_ARROWHEAD = 0x02c2, // U+02C2 MODIFIER LETTER LEFT ARROWHEAD + U_MODIFIER_LETTER_RIGHT_ARROWHEAD = 0x02c3, // U+02C3 MODIFIER LETTER RIGHT ARROWHEAD + U_MODIFIER_LETTER_UP_ARROWHEAD = 0x02c4, // U+02C4 MODIFIER LETTER UP ARROWHEAD + U_MODIFIER_LETTER_DOWN_ARROWHEAD = 0x02c5, // U+02C5 MODIFIER LETTER DOWN ARROWHEAD + U_MODIFIER_LETTER_CENTRED_RIGHT_HALF_RING = 0x02d2, // U+02D2 MODIFIER LETTER CENTRED RIGHT HALF RING + U_MODIFIER_LETTER_CENTRED_LEFT_HALF_RING = 0x02d3, // U+02D3 MODIFIER LETTER CENTRED LEFT HALF RING + U_MODIFIER_LETTER_UP_TACK = 0x02d4, // U+02D4 MODIFIER LETTER UP TACK + U_MODIFIER_LETTER_DOWN_TACK = 0x02d5, // U+02D5 MODIFIER LETTER DOWN TACK + U_MODIFIER_LETTER_PLUS_SIGN = 0x02d6, // U+02D6 MODIFIER LETTER PLUS SIGN + U_MODIFIER_LETTER_MINUS_SIGN = 0x02d7, // U+02D7 MODIFIER LETTER MINUS SIGN + U_BREVE = 0x02d8, // U+02D8 BREVE + U_DOT_ABOVE = 0x02d9, // U+02D9 DOT ABOVE + U_RING_ABOVE = 0x02da, // U+02DA RING ABOVE + U_OGONEK = 0x02db, // U+02DB OGONEK + U_SMALL_TILDE = 0x02dc, // U+02DC SMALL TILDE + U_DOUBLE_ACUTE_ACCENT = 0x02dd, // U+02DD DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_RHOTIC_HOOK = 0x02de, // U+02DE MODIFIER LETTER RHOTIC HOOK + U_MODIFIER_LETTER_CROSS_ACCENT = 0x02df, // U+02DF MODIFIER LETTER CROSS ACCENT + U_MODIFIER_LETTER_EXTRA_HIGH_TONE_BAR = 0x02e5, // U+02E5 MODIFIER LETTER EXTRA-HIGH TONE BAR + U_MODIFIER_LETTER_HIGH_TONE_BAR = 0x02e6, // U+02E6 MODIFIER LETTER HIGH TONE BAR + U_MODIFIER_LETTER_MID_TONE_BAR = 0x02e7, // U+02E7 MODIFIER LETTER MID TONE BAR + U_MODIFIER_LETTER_LOW_TONE_BAR = 0x02e8, // U+02E8 MODIFIER LETTER LOW TONE BAR + U_MODIFIER_LETTER_EXTRA_LOW_TONE_BAR = 0x02e9, // U+02E9 MODIFIER LETTER EXTRA-LOW TONE BAR + U_MODIFIER_LETTER_YIN_DEPARTING_TONE_MARK = 0x02ea, // U+02EA MODIFIER LETTER YIN DEPARTING TONE MARK + U_MODIFIER_LETTER_YANG_DEPARTING_TONE_MARK = 0x02eb, // U+02EB MODIFIER LETTER YANG DEPARTING TONE MARK + U_MODIFIER_LETTER_UNASPIRATED = 0x02ed, // U+02ED MODIFIER LETTER UNASPIRATED + U_MODIFIER_LETTER_LOW_DOWN_ARROWHEAD = 0x02ef, // U+02EF MODIFIER LETTER LOW DOWN ARROWHEAD + U_MODIFIER_LETTER_LOW_UP_ARROWHEAD = 0x02f0, // U+02F0 MODIFIER LETTER LOW UP ARROWHEAD + U_MODIFIER_LETTER_LOW_LEFT_ARROWHEAD = 0x02f1, // U+02F1 MODIFIER LETTER LOW LEFT ARROWHEAD + U_MODIFIER_LETTER_LOW_RIGHT_ARROWHEAD = 0x02f2, // U+02F2 MODIFIER LETTER LOW RIGHT ARROWHEAD + U_MODIFIER_LETTER_LOW_RING = 0x02f3, // U+02F3 MODIFIER LETTER LOW RING + U_MODIFIER_LETTER_MIDDLE_GRAVE_ACCENT = 0x02f4, // U+02F4 MODIFIER LETTER MIDDLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_GRAVE_ACCENT = 0x02f5, // U+02F5 MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT + U_MODIFIER_LETTER_MIDDLE_DOUBLE_ACUTE_ACCENT = 0x02f6, // U+02F6 MODIFIER LETTER MIDDLE DOUBLE ACUTE ACCENT + U_MODIFIER_LETTER_LOW_TILDE = 0x02f7, // U+02F7 MODIFIER LETTER LOW TILDE + U_MODIFIER_LETTER_RAISED_COLON = 0x02f8, // U+02F8 MODIFIER LETTER RAISED COLON + U_MODIFIER_LETTER_BEGIN_HIGH_TONE = 0x02f9, // U+02F9 MODIFIER LETTER BEGIN HIGH TONE + U_MODIFIER_LETTER_END_HIGH_TONE = 0x02fa, // U+02FA MODIFIER LETTER END HIGH TONE + U_MODIFIER_LETTER_BEGIN_LOW_TONE = 0x02fb, // U+02FB MODIFIER LETTER BEGIN LOW TONE + U_MODIFIER_LETTER_END_LOW_TONE = 0x02fc, // U+02FC MODIFIER LETTER END LOW TONE + U_MODIFIER_LETTER_SHELF = 0x02fd, // U+02FD MODIFIER LETTER SHELF + U_MODIFIER_LETTER_OPEN_SHELF = 0x02fe, // U+02FE MODIFIER LETTER OPEN SHELF + U_MODIFIER_LETTER_LOW_LEFT_ARROW = 0x02ff, // U+02FF MODIFIER LETTER LOW LEFT ARROW + U_GREEK_LOWER_NUMERAL_SIGN = 0x0375, // U+0375 GREEK LOWER NUMERAL SIGN + U_GREEK_TONOS = 0x0384, // U+0384 GREEK TONOS + U_GREEK_DIALYTIKA_TONOS = 0x0385, // U+0385 GREEK DIALYTIKA TONOS + U_GREEK_KORONIS = 0x1fbd, // U+1FBD GREEK KORONIS + U_GREEK_PSILI = 0x1fbf, // U+1FBF GREEK PSILI + U_GREEK_PERISPOMENI = 0x1fc0, // U+1FC0 GREEK PERISPOMENI + U_GREEK_DIALYTIKA_AND_PERISPOMENI = 0x1fc1, // U+1FC1 GREEK DIALYTIKA AND PERISPOMENI + U_GREEK_PSILI_AND_VARIA = 0x1fcd, // U+1FCD GREEK PSILI AND VARIA + U_GREEK_PSILI_AND_OXIA = 0x1fce, // U+1FCE GREEK PSILI AND OXIA + U_GREEK_PSILI_AND_PERISPOMENI = 0x1fcf, // U+1FCF GREEK PSILI AND PERISPOMENI + U_GREEK_DASIA_AND_VARIA = 0x1fdd, // U+1FDD GREEK DASIA AND VARIA + U_GREEK_DASIA_AND_OXIA = 0x1fde, // U+1FDE GREEK DASIA AND OXIA + U_GREEK_DASIA_AND_PERISPOMENI = 0x1fdf, // U+1FDF GREEK DASIA AND PERISPOMENI + U_GREEK_DIALYTIKA_AND_VARIA = 0x1fed, // U+1FED GREEK DIALYTIKA AND VARIA + U_GREEK_DIALYTIKA_AND_OXIA = 0x1fee, // U+1FEE GREEK DIALYTIKA AND OXIA + U_GREEK_VARIA = 0x1fef, // U+1FEF GREEK VARIA + U_GREEK_OXIA = 0x1ffd, // U+1FFD GREEK OXIA + U_GREEK_DASIA = 0x1ffe, // U+1FFE GREEK DASIA + + U_IDEOGRAPHIC_FULL_STOP = 0x3002, // U+3002 IDEOGRAPHIC FULL STOP + U_LEFT_CORNER_BRACKET = 0x300c, // U+300C LEFT CORNER BRACKET + U_RIGHT_CORNER_BRACKET = 0x300d, // U+300D RIGHT CORNER BRACKET + U_LEFT_BLACK_LENTICULAR_BRACKET = 0x3010, // U+3010 LEFT BLACK LENTICULAR BRACKET + U_RIGHT_BLACK_LENTICULAR_BRACKET = 0x3011, // U+3011 RIGHT BLACK LENTICULAR BRACKET + + U_OVERLINE = 0x203e, // Unicode Character 'OVERLINE' + + /** + * UTF-8 BOM + * Unicode Character 'ZERO WIDTH NO-BREAK SPACE' (U+FEFF) + * http://www.fileformat.info/info/unicode/char/feff/index.htm + */ + UTF8_BOM = 65279, + + U_FULLWIDTH_SEMICOLON = 0xff1b, // U+FF1B FULLWIDTH SEMICOLON + U_FULLWIDTH_COMMA = 0xff0c, // U+FF0C FULLWIDTH COMMA +} diff --git a/src/client/common/utils/decorators.ts b/src/client/common/utils/decorators.ts index 26d82460bbd3..44a82ee13760 100644 --- a/src/client/common/utils/decorators.ts +++ b/src/client/common/utils/decorators.ts @@ -1,14 +1,10 @@ -// tslint:disable:no-any no-require-imports no-function-expression no-invalid-this - -import { ProgressLocation, ProgressOptions, Uri, window } from 'vscode'; import '../../common/extensions'; +import { traceError } from '../../logging'; import { isTestExecution } from '../constants'; -import { traceError, traceVerbose } from '../logger'; -import { Resource } from '../types'; import { createDeferred, Deferred } from './async'; -import { InMemoryInterpreterSpecificCache } from './cacheUtils'; +import { getCacheKeyFromFunctionArgs, getGlobalCacheStore } from './cacheUtils'; +import { StopWatch } from './stopWatch'; -// tslint:disable-next-line:no-require-imports no-var-requires const _debounce = require('lodash/debounce') as typeof import('lodash/debounce'); type VoidFunction = () => any; @@ -55,8 +51,7 @@ export function debounceAsync(wait?: number) { } export function makeDebounceDecorator(wait?: number) { - // tslint:disable-next-line:no-any no-function-expression - return function(_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { // We could also make use of _debounce() options. For instance, // the following causes the original method to be called // immediately: @@ -71,25 +66,28 @@ export function makeDebounceDecorator(wait?: number) { const options = {}; const originalMethod = descriptor.value!; const debounced = _debounce( - function(this: any) { + function (this: any) { return originalMethod.apply(this, arguments as any); }, wait, - options + options, ); (descriptor as any).value = debounced; }; } export function makeDebounceAsyncDecorator(wait?: number) { - // tslint:disable-next-line:no-any no-function-expression - return function(_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { - type StateInformation = { started: boolean; deferred: Deferred | undefined; timer: NodeJS.Timer | number | undefined }; + return function (_target: any, _propertyName: string, descriptor: TypedPropertyDescriptor) { + type StateInformation = { + started: boolean; + deferred: Deferred | undefined; + timer: NodeJS.Timer | number | undefined; + }; const originalMethod = descriptor.value!; const state: StateInformation = { started: false, deferred: undefined, timer: undefined }; // Lets defer execution using a setTimeout for the given time. - (descriptor as any).value = function(this: any) { + (descriptor as any).value = function (this: any) { const existingDeferred: Deferred | undefined = state.deferred; if (existingDeferred && state.started) { return existingDeferred.promise; @@ -97,7 +95,8 @@ export function makeDebounceAsyncDecorator(wait?: number) { // Clear previous timer. const existingDeferredCompleted = existingDeferred && existingDeferred.completed; - const deferred = (state.deferred = !existingDeferred || existingDeferredCompleted ? createDeferred() : existingDeferred); + const deferred = (state.deferred = + !existingDeferred || existingDeferredCompleted ? createDeferred() : existingDeferred); if (state.timer) { clearTimeout(state.timer as any); } @@ -106,11 +105,11 @@ export function makeDebounceAsyncDecorator(wait?: number) { state.started = true; originalMethod .apply(this) - .then(r => { + .then((r) => { state.started = false; deferred.resolve(r); }) - .catch(ex => { + .catch((ex) => { state.started = false; deferred.reject(ex); }); @@ -120,24 +119,62 @@ export function makeDebounceAsyncDecorator(wait?: number) { }; } -type VSCodeType = typeof import('vscode'); -type PromiseFunctionWithFirstArgOfResource = (...any: [Uri | undefined, ...any[]]) => Promise; +type PromiseFunctionWithAnyArgs = (...any: any) => Promise; +const cacheStoreForMethods = getGlobalCacheStore(); -export function clearCachedResourceSpecificIngterpreterData(key: string, resource: Resource, vscode: VSCodeType = require('vscode')) { - const cache = new InMemoryInterpreterSpecificCache(key, 0, [resource], vscode); - cache.clear(); -} -export function cacheResourceSpecificInterpreterData(key: string, expiryDurationMs: number, vscode: VSCodeType = require('vscode')) { - return function(_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { +/** + * 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, + ) { const originalMethod = descriptor.value!; - descriptor.value = async function(...args: [Uri | undefined, ...any[]]) { - const cache = new InMemoryInterpreterSpecificCache(key, expiryDurationMs, args, vscode); - if (cache.hasData) { - traceVerbose(`Cached data exists ${key}, ${args[0] ? args[0].fsPath : ''}`); - return Promise.resolve(cache.data); + 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; + } + let key: string; + try { + key = getCacheKeyFromFunctionArgs(keyPrefix, args); + } catch (ex) { + traceError('Error while creating key for keyPrefix:', keyPrefix, ex); + return originalMethod.apply(this, args) as Promise; } + const cachedItem = cacheStoreForMethods.get(key); + if (cachedItem && (cachedItem.expiry > Date.now() || expiryDurationMs === -1)) { + return Promise.resolve(cachedItem.data); + } + const expiryMs = + expiryDurationAfterStartUpMs && moduleLoadWatch.elapsedTime > extensionStartUpTime + ? expiryDurationAfterStartUpMs + : expiryDurationMs; const promise = originalMethod.apply(this, args) as Promise; - promise.then(result => (cache.data = result)).ignoreErrors(); + if (cachePromise) { + cacheStoreForMethods.set(key, { data: promise, expiry: Date.now() + expiryMs }); + } else { + promise + .then((result) => cacheStoreForMethods.set(key, { data: result, expiry: Date.now() + expiryMs })) + .ignoreErrors(); + } return promise; }; }; @@ -150,20 +187,18 @@ export function cacheResourceSpecificInterpreterData(key: string, expiryDuration * @param {string} [scopeName] Scope for the error message to be logged along with the error. * @returns void */ -export function swallowExceptions(scopeName: string) { - // tslint:disable-next-line:no-any no-function-expression - return function(_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { +export function swallowExceptions(scopeName?: string) { + return function (_target: any, propertyName: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value!; - const errorMessage = `Python Extension (Error in ${scopeName}, method:${propertyName}):`; - // tslint:disable-next-line:no-any no-function-expression - descriptor.value = function(...args: any[]) { + 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).catch(error => { + return (result as Promise).catch((error) => { if (isTestExecution()) { return; } @@ -179,22 +214,3 @@ export function swallowExceptions(scopeName: string) { }; }; } - -// tslint:disable-next-line:no-any -type PromiseFunction = (...any: any[]) => Promise; - -export function displayProgress(title: string, location = ProgressLocation.Window) { - return function(_target: Object, _propertyName: string, descriptor: TypedPropertyDescriptor) { - const originalMethod = descriptor.value!; - // tslint:disable-next-line:no-any no-function-expression - descriptor.value = async function(...args: any[]) { - const progressOptions: ProgressOptions = { location, title }; - // tslint:disable-next-line:no-invalid-this - const promise = originalMethod.apply(this, args); - if (!isTestExecution()) { - window.withProgress(progressOptions, () => promise); - } - return promise; - }; - }; -} 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(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(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 { + // There are three options when it comes to checking if a file + // is executable: `fs.stat()`, `fs.access()`, and + // `child_process.exec()`. `stat()` requires non-trivial logic + // to deal with user/group/everyone permissions. `exec()` requires + // that we make an attempt to actually execute the file, which is + // beyond the scope of this function (due to potential security + // risks). That leaves `access()`, which is what we use. + try { + // We do not need to check if the file exists. `fs.access()` + // takes care of that for us. + await fsapi.promises.access(filename, fsapi.constants.X_OK); + } catch (err) { + return false; + } + if (getOSType() === OSType.Windows) { + // On Windows a file is determined to be executable through + // its ACLs. However, the FS-related functionality provided + // by node does not check them (currently). This includes both + // `fs.stat()` and `fs.access()` (which we used above). One + // option is to use the "acl" NPM package (or something similar) + // to make the relevant checks. However, we want to avoid + // adding a dependency needlessly. Another option is to fall + // back to checking the filename's suffix (e.g. ".exe"). The + // problem there is that such a check is a bit *too* naive. + // Finally, we could still go the `exec()` route. We'd + // rather not given the concern identified above. Instead, + // it is good enough to return `undefined` and let the + // caller decide what to do about it. That is better + // than returning `true` when we aren't sure. + // + // Note that we still call `fs.access()` on Windows first, + // in case node makes it smarter later. + return undefined; + } + return true; +} diff --git a/src/client/common/utils/filesystem.ts b/src/client/common/utils/filesystem.ts new file mode 100644 index 000000000000..f2708e523bfb --- /dev/null +++ b/src/client/common/utils/filesystem.ts @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as vscode from 'vscode'; +import { traceError } from '../../logging'; + +export import FileType = vscode.FileType; + +export type DirEntry = { + filename: string; + filetype: FileType; +}; + +interface IKnowsFileType { + isFile(): boolean; + isDirectory(): boolean; + isSymbolicLink(): boolean; +} + +// This helper function determines the file type of the given stats +// object. The type follows the convention of node's fs module, where +// a file has exactly one type. Symlinks are not resolved. +export function convertFileType(info: IKnowsFileType): FileType { + if (info.isFile()) { + return FileType.File; + } + if (info.isDirectory()) { + return FileType.Directory; + } + if (info.isSymbolicLink()) { + // The caller is responsible for combining this ("logical or") + // with File or Directory as necessary. + return FileType.SymbolicLink; + } + return FileType.Unknown; +} + +/** + * Identify the file type for the given file. + */ +export async function getFileType( + filename: string, + opts: { + ignoreErrors: boolean; + } = { ignoreErrors: true }, +): Promise { + let stat: fs.Stats; + try { + stat = await fs.promises.lstat(filename); + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'ENOENT') { + return undefined; + } + if (opts.ignoreErrors) { + traceError(`lstat() failed for "${filename}" (${err})`); + return FileType.Unknown; + } + throw err; // re-throw + } + return convertFileType(stat); +} + +function normalizeFileTypes(filetypes: FileType | FileType[] | undefined): FileType[] | undefined { + if (filetypes === undefined) { + return undefined; + } + if (Array.isArray(filetypes)) { + if (filetypes.length === 0) { + return undefined; + } + return filetypes; + } + return [filetypes]; +} + +async function resolveFile( + file: string | DirEntry, + opts: { + ensure?: boolean; + onMissing?: FileType; + } = {}, +): Promise { + let filename: string; + if (typeof file !== 'string') { + if (!opts.ensure) { + if (opts.onMissing === undefined) { + return file; + } + // At least make sure it exists. + if ((await getFileType(file.filename)) !== undefined) { + return file; + } + } + filename = file.filename; + } else { + filename = file; + } + + const filetype = (await getFileType(filename)) || opts.onMissing; + if (filetype === undefined) { + return undefined; + } + return { filename, filetype }; +} + +type FileFilterFunc = (file: string | DirEntry) => Promise; + +export function getFileFilter( + opts: { + ignoreMissing?: boolean; + ignoreFileType?: FileType | FileType[]; + ensureEntry?: boolean; + } = { + ignoreMissing: true, + }, +): FileFilterFunc | undefined { + const ignoreFileType = normalizeFileTypes(opts.ignoreFileType); + + if (!opts.ignoreMissing && !ignoreFileType) { + // Do not filter. + return undefined; + } + + async function filterFile(file: string | DirEntry): Promise { + let entry = await resolveFile(file, { ensure: opts.ensureEntry }); + if (!entry) { + if (opts.ignoreMissing) { + return false; + } + const filename = typeof file === 'string' ? file : file.filename; + entry = { filename, filetype: FileType.Unknown }; + } + if (ignoreFileType) { + if (ignoreFileType.includes(entry!.filetype)) { + return false; + } + } + return true; + } + return filterFile; +} diff --git a/src/client/common/utils/fs.ts b/src/client/common/utils/fs.ts deleted file mode 100644 index 4ab1e19686c8..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 { - return new Promise(resolve => { - fs.exists(filePath, exists => { - return resolve(exists); - }); - }); -} -export function fsReaddirAsync(root: string): Promise { - return new Promise(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 { - return new Promise(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 index 3d312818e058..71f71898ae9f 100644 --- a/src/client/common/utils/icons.ts +++ b/src/client/common/utils/icons.ts @@ -10,9 +10,9 @@ import { EXTENSION_ROOT_DIR } from '../../constants'; const darkIconsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'dark'); const lightIconsPath = path.join(EXTENSION_ROOT_DIR, 'resources', 'light'); -export function getIcon(fileName: string): { light: string | Uri; dark: string | Uri } { +export function getIcon(fileName: string): { light: Uri; dark: Uri } { return { - dark: path.join(darkIconsPath, fileName), - light: path.join(lightIconsPath, fileName) + dark: Uri.file(path.join(darkIconsPath, fileName)), + light: Uri.file(path.join(lightIconsPath, fileName)), }; } diff --git a/src/client/common/utils/iterable.ts b/src/client/common/utils/iterable.ts new file mode 100644 index 000000000000..5e04aaa430ea --- /dev/null +++ b/src/client/common/utils/iterable.ts @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace Iterable { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + export function is(thing: any): thing is Iterable { + return thing && typeof thing === 'object' && typeof thing[Symbol.iterator] === 'function'; + } +} diff --git a/src/client/common/utils/localize.ts b/src/client/common/utils/localize.ts index 4781b8492e9a..7b7560c74e05 100644 --- a/src/client/common/utils/localize.ts +++ b/src/client/common/utils/localize.ts @@ -3,426 +3,552 @@ 'use strict'; -import * as fs from 'fs'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../constants'; +import { l10n } from 'vscode'; +import { Commands } from '../constants'; + +/* eslint-disable @typescript-eslint/no-namespace, no-shadow */ // External callers of localize use these tables to retrieve localized values. export namespace Diagnostics { - export const warnSourceMaps = localize('diagnostics.warnSourceMaps', 'Source map support is enabled in the Python Extension, this will adversely impact performance of the extension.'); - export const disableSourceMaps = localize('diagnostics.disableSourceMaps', 'Disable Source Map Support'); - export const warnBeforeEnablingSourceMaps = localize('diagnostics.warnBeforeEnablingSourceMaps', 'Enabling source map support in the Python Extension will adversely impact performance of the extension.'); - export const enableSourceMapsAndReloadVSC = localize('diagnostics.enableSourceMapsAndReloadVSC', 'Enable and reload Window.'); - export const lsNotSupported = localize('diagnostics.lsNotSupported', 'Your operating system does not meet the minimum requirements of the Language Server. Reverting to the alternative, Jedi.'); - export const invalidPythonPathInDebuggerSettings = localize('diagnostics.invalidPythonPathInDebuggerSettings', 'You need to select a Python interpreter before you start debugging.\n\nTip: click on "Select Python Interpreter" in the status bar.'); - export const invalidPythonPathInDebuggerLaunch = localize('diagnostics.invalidPythonPathInDebuggerLaunch', 'The Python path in your debug configuration is invalid.'); - export const invalidDebuggerTypeDiagnostic = localize('diagnostics.invalidDebuggerTypeDiagnostic', 'Your launch.json file needs to be updated to change the "pythonExperimental" debug configurations to use the "python" debugger type, otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?'); - export const consoleTypeDiagnostic = localize('diagnostics.consoleTypeDiagnostic', 'Your launch.json file needs to be updated to change the console type string from \"none\" to \"internalConsole\", otherwise Python debugging may not work. Would you like to automatically update your launch.json file now?'); - export const justMyCodeDiagnostic = localize('diagnostics.justMyCodeDiagnostic', 'Configuration "debugStdLib" in launch.json is no longer supported. It\'s recommended to replace it with "justMyCode", which is the exact opposite of using "debugStdLib". Would you like to automatically update your launch.json file to do that?'); - export const yesUpdateLaunch = localize('diagnostics.yesUpdateLaunch', 'Yes, update launch.json'); - export const invalidTestSettings = localize('diagnostics.invalidTestSettings', 'Your settings needs to be updated to change the setting "python.unitTest." to "python.testing.", otherwise testing Python code using the extension may not work. Would you like to automatically update your settings now?'); - export const updateSettings = localize('diagnostics.updateSettings', 'Yes, update settings'); + export const 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 canceled = localize('Common.canceled', 'Canceled'); - export const gotIt = localize('Common.gotIt', 'Got it!'); - export const loadingExtension = localize('Common.loadingPythonExtension', 'Python extension loading...'); - export const openOutputPanel = localize('Common.openOutputPanel', 'Show output'); - export const noIWillDoItLater = localize('Common.noIWillDoItLater', 'No, I will do it later'); - export const notNow = localize('Common.notNow', 'Not now'); - export const doNotShowAgain = localize('Common.doNotShowAgain', 'Do not show again'); + export const 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 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 const lsFailedToStart = localize('LanguageService.lsFailedToStart', 'We encountered an issue starting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); - export const lsFailedToDownload = localize('LanguageService.lsFailedToDownload', 'We encountered an issue downloading the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); - export const lsFailedToExtract = localize('LanguageService.lsFailedToExtract', 'We encountered an issue extracting the Language Server. Reverting to the alternative, Jedi. Check the Python output panel for details.'); - export const downloadFailedOutputMessage = localize('LanguageService.downloadFailedOutputMessage', 'download failed.'); - export const extractionFailedOutputMessage = localize('LanguageService.extractionFailedOutputMessage', 'extraction failed.'); - export const extractionCompletedOutputMessage = localize('LanguageService.extractionCompletedOutputMessage', 'complete.'); - export const extractionDoneOutputMessage = localize('LanguageService.extractionDoneOutputMessage', 'done.'); - export const reloadVSCodeIfSeachPathHasChanged = localize('LanguageService.reloadVSCodeIfSeachPathHasChanged', 'Search paths have changed for this Python interpreter. Please reload the extension to ensure that the IntelliSense works correctly.'); +export namespace 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 Http { - export const downloadingFile = localize('downloading.file', 'Downloading {0}...'); - export const downloadingFileProgress = localize('downloading.file.progress', '{0}{1} of {2} KB ({3})'); +export namespace Repl { + export const disableSmartSend = l10n.t('Disable Smart Send'); + export const launchNativeRepl = l10n.t('Launch VS Code Native REPL'); } -export namespace Experiments { - export const inGroup = localize('Experiments.inGroup', 'User belongs to experiment group \'{0}\''); +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 loading = localize('Interpreters.LoadingInterpreters', 'Loading Python Interpreters'); - export const refreshing = localize('Interpreters.RefreshingInterpreters', 'Refreshing Python Interpreters'); - export const environmentPromptMessage = localize('Interpreters.environmentPromptMessage', 'We noticed a new virtual environment has been created. Do you want to select it for the workspace folder?'); - export const selectInterpreterTip = localize('Interpreters.selectInterpreterTip', 'Tip: you can change the Python interpreter used by the Python extension by clicking on the Python version in the status bar'); + export const 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 Linters { - export const enableLinter = localize('Linter.enableLinter', 'Enable {0}'); - export const enablePylint = localize('Linter.enablePylint', 'You have a pylintrc file in your workspace. Do you want to enable pylint?'); - export const replaceWithSelectedLinter = localize('Linter.replaceWithSelectedLinter', 'Multiple linters are enabled in settings. Replace with \'{0}\'?'); +export namespace 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 InteractiveShiftEnterBanner { - export const bannerMessage = localize('InteractiveShiftEnterBanner.bannerMessage', 'Would you like shift-enter to send code to the new Interactive Window experience?'); - export const bannerLabelYes = localize('InteractiveShiftEnterBanner.bannerLabelYes', 'Yes'); - export const bannerLabelNo = localize('InteractiveShiftEnterBanner.bannerLabelNo', 'No'); +export namespace OutputChannelNames { + export const languageServer = l10n.t('Python Language Server'); + export const python = l10n.t('Python'); } -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 Linters { + export const selectLinter = l10n.t('Select Linter'); } -export namespace DataScience { - export const historyTitle = localize('DataScience.historyTitle', 'Python Interactive'); - export const dataExplorerTitle = localize('DataScience.dataExplorerTitle', 'Data Viewer'); - export const badWebPanelFormatString = localize('DataScience.badWebPanelFormatString', '

{0} is not a valid file name

'); - export const sessionDisposed = localize('DataScience.sessionDisposed', 'Cannot execute code, session has been disposed.'); - export const passwordFailure = localize('DataScience.passwordFailure', 'Failed to connect to password protected server. Check that password is correct.'); - export const 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 jupyterNotSupportedBecauseOfEnvironment = localize('DataScience.jupyterNotSupportedBecauseOfEnvironment', 'Activating {0} to run Jupyter failed with {1}'); - export const jupyterNbConvertNotSupported = localize('DataScience.jupyterNbConvertNotSupported', 'Jupyter nbconvert is not installed'); - export const jupyterLaunchTimedOut = localize('DataScience.jupyterLaunchTimedOut', 'The Jupyter notebook server failed to launch in time'); - export const jupyterLaunchNoURL = localize('DataScience.jupyterLaunchNoURL', 'Failed to find the URL of the launched Jupyter notebook server'); - export const jupyterSelfCertFail = localize('DataScience.jupyterSelfCertFail', 'The security certificate used by server {0} was not issued by a trusted certificate authority.\r\nThis may indicate an attempt to steal your information.\r\nDo you want to enable the Allow Unauthorized Remote Connection setting for this workspace to allow you to connect?'); - export const jupyterSelfCertEnable = localize('DataScience.jupyterSelfCertEnable', 'Yes, connect anyways'); - export const jupyterSelfCertClose = localize('DataScience.jupyterSelfCertClose', 'No, close the connection'); - export const pythonInteractiveHelpLink = localize('DataScience.pythonInteractiveHelpLink', 'See [https://aka.ms/pyaiinstall] for help on installing jupyter.'); - export const 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 runAllCellsAboveLensCommandTitle = localize('python.command.python.datascience.runallcellsabove.title', 'Run Above'); - export const runCellAndAllBelowLensCommandTitle = localize('python.command.python.datascience.runcellandallbelow.title', 'Run Below'); - export const importChangeDirectoryComment = localize('DataScience.importChangeDirectoryComment', '#%% Change working directory from the workspace root to the ipynb file location. Turn this addition off with the DataScience.changeDirOnImportExport setting'); - export const exportChangeDirectoryComment = localize('DataScience.exportChangeDirectoryComment', '# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting'); - - export const restartKernelMessage = localize('DataScience.restartKernelMessage', 'Do you want to restart the Jupter kernel? All variables will be lost.'); - export const restartKernelMessageYes = localize('DataScience.restartKernelMessageYes', 'Restart'); - export const restartKernelMessageDontAskAgain = localize('DataScience.restartKernelMessageDontAskAgain', 'Don\'t Ask Again'); - export const restartKernelMessageNo = localize('DataScience.restartKernelMessageNo', 'Cancel'); - export const restartingKernelStatus = localize('DataScience.restartingKernelStatus', 'Restarting iPython Kernel'); - export const restartingKernelFailed = localize('DataScience.restartingKernelFailed', 'Kernel restart failed. Jupyter server is hung. Please reload VS code.'); - export const interruptingKernelFailed = localize('DataScience.interruptingKernelFailed', 'Kernel interrupt failed. Jupyter server is hung. Please reload VS code.'); - - export const executingCode = localize('DataScience.executingCode', 'Executing Cell'); - export const collapseAll = localize('DataScience.collapseAll', 'Collapse all cell inputs'); - export const expandAll = localize('DataScience.expandAll', 'Expand all cell inputs'); - export const collapseSingle = localize('DataScience.collapseSingle', 'Collapse'); - export const expandSingle = localize('DataScience.expandSingle', 'Expand'); - export const exportKey = localize('DataScience.export', 'Export as Jupyter Notebook'); - export const restartServer = localize('DataScience.restartServer', 'Restart iPython Kernel'); - export const undo = localize('DataScience.undo', 'Undo'); - export const redo = localize('DataScience.redo', 'Redo'); - export const 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 pythonNewHeader = localize('DataScience.pythonNewHeader', 'Started new 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 jupyterSelectPasswordPrompt = localize('DataScience.jupyterSelectPasswordPrompt', 'Enter your notebook password'); - export const jupyterNotebookFailure = localize('DataScience.jupyterNotebookFailure', 'Jupyter notebook failed to launch. \r\n{0}'); - export const jupyterNotebookConnectFailed = localize('DataScience.jupyterNotebookConnectFailed', 'Failed to connect to Jupyter notebook. \r\n{0}\r\n{1}'); - export const jupyterNotebookRemoteConnectFailed = localize('DataScience.jupyterNotebookRemoteConnectFailed', 'Failed to connect to remote Jupyter notebook.\r\nCheck that the Jupyter Server URI setting has a valid running server specified.\r\n{0}\r\n{1}'); - export const jupyterNotebookRemoteConnectSelfCertsFailed = localize('DataScience.jupyterNotebookRemoteConnectSelfCertsFailed', 'Failed to connect to remote Jupyter notebook.\r\nSpecified server is using self signed certs. Enable Allow Unauthorized Remote Connection setting to connect anyways\r\n{0}\r\n{1}'); - export const jupyterServerCrashed = localize('DataScience.jupyterServerCrashed', 'Jupyter server crashed. Unable to connect. \r\nError code from jupyter: {0}'); - export const notebookVersionFormat = localize('DataScience.notebookVersionFormat', 'Jupyter Notebook Version: {0}'); - //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 const executingCodeFailure = localize('DataScience.executingCodeFailure', 'Executing code failed : {0}'); - export const inputWatermark = localize('DataScience.inputWatermark', 'Shift-enter to run'); - export const liveShareConnectFailure = localize('DataScience.liveShareConnectFailure', 'Cannot connect to host jupyter session. URI not found.'); - export const liveShareCannotSpawnNotebooks = localize('DataScience.liveShareCannotSpawnNotebooks', 'Spawning jupyter notebooks is not supported over a live share connection'); - export const liveShareCannotImportNotebooks = localize('DataScience.liveShareCannotImportNotebooks', 'Importing notebooks is not currently supported over a live share connection'); - export const liveShareHostFormat = localize('DataScience.liveShareHostFormat', '{0} Jupyter Server'); - export const liveShareSyncFailure = localize('DataScience.liveShareSyncFailure', 'Synchronization failure during live share startup.'); - export const liveShareServiceFailure = localize('DataScience.liveShareServiceFailure', 'Failure starting \'{0}\' service during live share connection.'); - export const documentMismatch = localize('DataScience.documentMismatch', 'Cannot run cells, duplicate documents for {0} found.'); - export const jupyterGetVariablesBadResults = localize('DataScience.jupyterGetVariablesBadResults', 'Failed to fetch variable info from the Jupyter server.'); - export const dataExplorerInvalidVariableFormat = localize('DataScience.dataExplorerInvalidVariableFormat', '\'{0}\' is not an active variable.'); - export const pythonInteractiveCreateFailed = localize('DataScience.pythonInteractiveCreateFailed', 'Failure to create a \'Python Interactive\' window. Try reinstalling the Python extension.'); - export const jupyterGetVariablesExecutionError = localize('DataScience.jupyterGetVariablesExecutionError', 'Failure during variable extraction: \r\n{0}'); - export const loadingMessage = localize('DataScience.loadingMessage', 'loading ...'); - export const fetchingDataViewer = localize('DataScience.fetchingDataViewer', 'Fetching data ...'); - export const noRowsInDataViewer = localize('DataScience.noRowsInDataViewer', 'No rows match current filter'); - export const pandasTooOldForViewingFormat = localize('DataScience.pandasTooOldForViewingFormat', 'Python package \'pandas\' is version {0}. Version 0.20 or greater is required for viewing data.'); - export const pandasRequiredForViewing = localize('DataScience.pandasRequiredForViewing', 'Python package \'pandas\' is required for viewing data.'); - export const valuesColumn = localize('DataScience.valuesColumn', 'values'); - export const liveShareInvalid = localize('DataScience.liveShareInvalid', 'One or more guests in the session do not have the Python Extension installed. Live share session cannot continue.'); - export const tooManyColumnsMessage = localize('DataScience.tooManyColumnsMessage', 'Variables with over a 1000 columns may take a long time to display. Are you sure you wish to continue?'); - export const tooManyColumnsYes = localize('DataScience.tooManyColumnsYes', 'Yes'); - export const tooManyColumnsNo = localize('DataScience.tooManyColumnsNo', 'No'); - export const tooManyColumnsDontAskAgain = localize('DataScience.tooManyColumnsDontAskAgain', 'Don\'t Ask Again'); - export const filterRowsButton = localize('DataScience.filterRowsButton', 'Filter Rows'); - export const filterRowsTooltip = localize('DataScience.filterRowsTooltip', 'Allows filtering multiple rows. Use =, >, or < signs to filter numeric values.'); - export const previewHeader = localize('DataScience.previewHeader', '--- Begin preview of {0} ---'); - export const previewFooter = localize('DataScience.previewFooter', '--- End preview of {0} ---'); - export const previewStatusMessage = localize('DataScience.previewStatusMessage', 'Generating preview of {0}'); - export const plotViewerTitle = localize('DataScience.plotViewerTitle', 'Plots'); - export const exportPlotTitle = localize('DataScience.exportPlotTitle', 'Save plot image'); - export const pdfFilter = localize('DataScience.pdfFilter', 'PDF'); - export const pngFilter = localize('DataScience.pngFilter', 'PNG'); - export const svgFilter = localize('DataScience.svgFilter', 'SVG'); - export const previousPlot = localize('DataScience.previousPlot', 'Previous'); - export const nextPlot = localize('DataScience.nextPlot', 'Next'); - export const panPlot = localize('DataScience.panPlot', 'Pan'); - export const zoomInPlot = localize('DataScience.zoomInPlot', 'Zoom in'); - export const zoomOutPlot = localize('DataScience.zoomOutPlot', 'Zoom out'); - export const exportPlot = localize('DataScience.exportPlot', 'Export to different formats'); - export const deletePlot = localize('DataScience.deletePlot', 'Remove'); - export const editSection = localize('DataScience.editSection', 'Input new cells here.'); - export const selectedImageListLabel = localize('DataScience.selectedImageListLabel', 'Selected Image'); - export const imageListLabel = localize('DataScience.imageListLabel', 'Image'); - export const exportImageFailed = localize('DataScience.exportImageFailed', 'Error exporting image: {0}'); - export const jupyterDataRateExceeded = localize('DataScience.jupyterDataRateExceeded', 'Cannot view variable because data rate exceeded. Please restart your server with a higher data rate limit. For example, --NotebookApp.iopub_data_rate_limit=10000000000.0'); - export const addCellBelowCommandTitle = localize('DataScience.addCellBelowCommandTitle', 'Add cell'); - export const debugCellCommandTitle = localize('DataScience.debugCellCommandTitle', 'Debug cell'); - export const variableExplorerDisabledDuringDebugging = localize('DataScience.variableExplorerDisabledDuringDebugging', 'Variables are not available while debugging.'); +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: localize('debug.selectConfigurationTitle'), - placeholder: localize('debug.selectConfigurationPlaceholder') + title: l10n.t('Select a debug configuration'), + placeholder: l10n.t('Debug Configuration'), }; export const launchJsonCompletions = { - label: localize('debug.launchJsonConfigurationsCompletionLabel'), - description: localize('debug.launchJsonConfigurationsCompletionDescription') + label: l10n.t('Python'), + description: l10n.t('Select a Python debug configuration'), }; export namespace file { export const snippet = { - name: localize('python.snippet.launch.standard.label') + name: l10n.t('Python: Current File'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugFileConfigurationLabel'), - description: localize('debug.debugFileConfigurationDescription') + label: l10n.t('Python File'), + description: l10n.t('Debug the currently active Python file'), }; } export namespace module { export const snippet = { - name: localize('python.snippet.launch.module.label'), - default: localize('python.snippet.launch.module.default') + name: l10n.t('Python: Module'), + default: l10n.t('enter-your-module-name'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugModuleConfigurationLabel'), - description: localize('debug.debugModuleConfigurationDescription') + label: l10n.t('Module'), + description: l10n.t("Debug a Python module by invoking it with '-m'"), }; export const enterModule = { - title: localize('debug.moduleEnterModuleTitle'), - prompt: localize('debug.moduleEnterModulePrompt'), - default: localize('debug.moduleEnterModuleDefault'), - invalid: localize('debug.moduleEnterModuleInvalidNameError') + title: l10n.t('Debug Module'), + prompt: l10n.t('Enter a Python module/package name'), + default: l10n.t('enter-your-module-name'), + invalid: l10n.t('Enter a valid module name'), }; } export namespace attach { export const snippet = { - name: localize('python.snippet.launch.attach.label') + name: l10n.t('Python: Remote Attach'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.remoteAttachConfigurationLabel'), - description: localize('debug.remoteAttachConfigurationDescription') + label: l10n.t('Remote Attach'), + description: l10n.t('Attach to a remote debug server'), }; export const enterRemoteHost = { - title: localize('debug.attachRemoteHostTitle'), - prompt: localize('debug.attachRemoteHostPrompt'), - invalid: localize('debug.attachRemoteHostValidationError') + title: l10n.t('Remote Debugging'), + prompt: l10n.t('Enter a valid host name or IP address'), + invalid: l10n.t('Enter a valid host name or IP address'), }; export const enterRemotePort = { - title: localize('debug.attachRemotePortTitle'), - prompt: localize('debug.attachRemotePortPrompt'), - invalid: localize('debug.attachRemotePortValidationError') + title: l10n.t('Remote Debugging'), + prompt: l10n.t('Enter the port number that the debug server is listening on'), + invalid: l10n.t('Enter a valid port number'), + }; + } + export namespace attachPid { + export const snippet = { + name: 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: localize('python.snippet.launch.django.label') + name: l10n.t('Python: Django'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugDjangoConfigurationLabel'), - description: localize('debug.debugDjangoConfigurationDescription') + label: l10n.t('Django'), + description: l10n.t('Launch and debug a Django web application'), }; export const enterManagePyPath = { - title: localize('debug.djangoEnterManagePyPathTitle'), - prompt: localize('debug.djangoEnterManagePyPathPrompt'), - invalid: localize('debug.djangoEnterManagePyPathInvalidFilePathError') + title: l10n.t('Debug Django'), + prompt: l10n.t( + "Enter the path to manage.py ('${workspaceFolder}' points to the root of the current workspace folder)", + ), + invalid: l10n.t('Enter a valid Python file path'), + }; + } + export namespace fastapi { + export const snippet = { + name: l10n.t('Python: FastAPI'), + }; + + export const selectConfiguration = { + label: l10n.t('FastAPI'), + description: l10n.t('Launch and debug a FastAPI web application'), + }; + export const enterAppPathOrNamePath = { + title: l10n.t('Debug FastAPI'), + prompt: l10n.t("Enter the path to the application, e.g. 'main.py' or 'main'"), + invalid: l10n.t('Enter a valid name'), }; } export namespace flask { export const snippet = { - name: localize('python.snippet.launch.flask.label') + name: l10n.t('Python: Flask'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugFlaskConfigurationLabel'), - description: localize('debug.debugFlaskConfigurationDescription') + label: l10n.t('Flask'), + description: l10n.t('Launch and debug a Flask web application'), }; export const enterAppPathOrNamePath = { - title: localize('debug.flaskEnterAppPathOrNamePathTitle'), - prompt: localize('debug.flaskEnterAppPathOrNamePathPrompt'), - invalid: localize('debug.flaskEnterAppPathOrNamePathInvalidNameError') + title: l10n.t('Debug Flask'), + prompt: l10n.t('Python: Flask'), + invalid: l10n.t('Enter a valid name'), }; } export namespace pyramid { export const snippet = { - name: localize('python.snippet.launch.pyramid.label') + name: l10n.t('Python: Pyramid Application'), }; - // tslint:disable-next-line:no-shadowed-variable + export const selectConfiguration = { - label: localize('debug.debugPyramidConfigurationLabel'), - description: localize('debug.debugPyramidConfigurationDescription') + label: l10n.t('Pyramid'), + description: l10n.t('Launch and debug a Pyramid web application'), }; export const enterDevelopmentIniPath = { - title: localize('debug.pyramidEnterDevelopmentIniPathTitle'), - prompt: localize('debug.pyramidEnterDevelopmentIniPathPrompt'), - invalid: localize('debug.pyramidEnterDevelopmentIniPathInvalidFilePathError') + title: l10n.t('Debug Pyramid'), + invalid: l10n.t('Enter a valid file path'), }; } } export namespace Testing { - export const testErrorDiagnosticMessage = localize('Testing.testErrorDiagnosticMessage', 'Error'); - export const testFailDiagnosticMessage = localize('Testing.testFailDiagnosticMessage', 'Fail'); - export const testSkippedDiagnosticMessage = localize('Testing.testSkippedDiagnosticMessage', 'Skipped'); - export const configureTests = localize('Testing.configureTests', 'Configure Test Framework'); - export const disableTests = localize('Testing.disableTests', 'Disable Tests'); + export const configureTests = l10n.t('Configure Test Framework'); + export const cancelUnittestDiscovery = l10n.t('Canceled unittest test discovery'); + export const errorUnittestDiscovery = l10n.t('Unittest test discovery error'); + export const cancelPytestDiscovery = l10n.t('Canceled pytest test discovery'); + export const errorPytestDiscovery = l10n.t('pytest test discovery error'); + export const seePythonOutput = l10n.t('(see Output > Python)'); + export const cancelUnittestExecution = l10n.t('Canceled unittest test execution'); + export const errorUnittestExecution = l10n.t('Unittest test execution error'); + export const cancelPytestExecution = l10n.t('Canceled pytest test execution'); + export const errorPytestExecution = l10n.t('pytest test execution error'); + export const copilotSetupMessage = l10n.t('Confirm your Python testing framework to enable test discovery.'); } -// Skip using vscode-nls and instead just compute our strings based on key values. Key values -// can be loaded out of the nls..json files -let loadedCollection: Record | undefined; -let defaultCollection: Record | undefined; -let askedForCollection: Record = {}; -let loadedLocale: string; - -// This is exported only for testing purposes. -export function _resetCollections() { - loadedLocale = ''; - loadedCollection = undefined; - askedForCollection = {}; +export namespace 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.', + ); } -// This is exported only for testing purposes. -export function _getAskedForCollection() { - return askedForCollection; +export namespace Python27Support { + export const jediMessage = l10n.t( + 'IntelliSense with Jedi for Python 2.7 is no longer supported. [Learn more](https://aka.ms/python-27-support).', + ); } -// Return the effective set of all localization strings, by key. -// -// This should not be used for direct lookup. -export function getCollectionJSON(): string { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); - } - - // Combine the default and loaded collections - return JSON.stringify({ ...defaultCollection, ...loadedCollection }); -} - -// tslint:disable-next-line:no-suspicious-comment -export function localize(key: string, defValue?: string) { - // Return a pointer to function so that we refetch it on each call. - return () => { - return getString(key, defValue); - }; -} - -function parseLocale(): string { - // Attempt to load from the vscode locale. If not there, use english - const vscodeConfigString = process.env.VSCODE_NLS_CONFIG; - return vscodeConfigString ? JSON.parse(vscodeConfigString).locale : 'en-us'; +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).", + ); } -function getString(key: string, defValue?: string) { - // Load the current collection - if (!loadedCollection || parseLocale() !== loadedLocale) { - load(); +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'); } - // The default collection (package.nls.json) is the fallback. - // Note that we are guaranteed the following (during shipping) - // 1. defaultCollection was initialized by the load() call above - // 2. defaultCollection has the key (see the "keys exist" test) - let collection = defaultCollection!; - - // Use the current locale if the key is defined there. - if (loadedCollection && loadedCollection.hasOwnProperty(key)) { - collection = loadedCollection; - } - let result = collection[key]; - if (!result && defValue) { - // This can happen during development if you haven't fixed up the nls file yet or - // if for some reason somebody broke the functional test. - result = defValue; + 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.'); } - askedForCollection[key] = result; - - return result; -} -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 = {}; - } + 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'); - // 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 = {}; - } + export const globalPipInstallTriggerMessage = l10n.t( + 'You may have installed Python packages into your global environment, which can cause conflicts between package versions. Would you like to create a virtual environment with these packages to isolate your dependencies?', + ); } } -// Default to loading the current locale -load(); +export namespace PythonLocator { + export const startupFailedNotification = l10n.t( + 'Python Locator failed to start. Python environment discovery may not work correctly.', + ); + export const windowsRuntimeMissing = l10n.t( + 'Missing Windows runtime dependencies detected. The Python Locator requires the Microsoft Visual C++ Redistributable. This is often missing on clean Windows installations.', + ); + export const windowsStartupFailed = l10n.t( + 'Python Locator failed to start on Windows. This might be due to missing system dependencies such as the Microsoft Visual C++ Redistributable.', + ); +} diff --git a/src/client/common/utils/logging.ts b/src/client/common/utils/logging.ts deleted file mode 100644 index c9c2f756c094..000000000000 --- a/src/client/common/utils/logging.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -export function formatErrorForLogging(error: Error | string): string { - let message: string = ''; - if (typeof error === 'string') { - message = error; - } else { - if (error.message) { - message = `Error Message: ${error.message}`; - } - if (error.name && error.message.indexOf(error.name) === -1) { - message += `, (${error.name})`; - } - // tslint:disable-next-line:no-any - const innerException = (error as any).innerException; - if (innerException && (innerException.message || innerException.name)) { - if (innerException.message) { - message += `, Inner Error Message: ${innerException.message}`; - } - if (innerException.name && innerException.message.indexOf(innerException.name) === -1) { - message += `, (${innerException.name})`; - } - } - } - return message; -} diff --git a/src/client/common/utils/misc.ts b/src/client/common/utils/misc.ts index ac0543e5aadb..a461d25d9d30 100644 --- a/src/client/common/utils/misc.ts +++ b/src/client/common/utils/misc.ts @@ -1,23 +1,99 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { IAsyncDisposable, IDisposable } from '../types'; - -// tslint:disable-next-line:no-empty -export function noop() { } - -export function using(disposable: T, func: (obj: T) => void) { - try { - func(disposable); - } finally { - disposable.dispose(); - } -} - -export async function usingAsync(disposable: T, func: (obj: T) => Promise) : Promise { - try { - return await func(disposable); - } finally { - await disposable.dispose(); - } -} +// 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 extends any[] ? IDeepReadonlyArray : DeepReadonlyNonArray; +type DeepReadonlyNonArray = T extends object ? DeepReadonlyObject : T; +interface IDeepReadonlyArray extends ReadonlyArray> {} +type DeepReadonlyObject = { + readonly [P in NonFunctionPropertyNames]: DeepReadonly; +}; +type NonFunctionPropertyNames = { [K in keyof T]: T[K] extends Function ? never : K }[keyof T]; + +/** + * 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; +} + +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 6e342c623f31..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 = (input: MultiStepInput, state: T) => Promise | void>; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type InputStep = (input: MultiStepInput, state: T) => Promise | void>; -export interface IQuickPickParameters { - title: string; +type buttonCallbackType = (quickPick: QuickPick) => void; + +export type QuickInputButtonSetup = { + /** + * Button for an action in a QuickPick. + */ + button: QuickInputButton; + /** + * Callback to be invoked when button is clicked. + */ + callback: buttonCallbackType; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IQuickPickParameters { + title?: string; step?: number; totalSteps?: number; canGoBack?: boolean; items: T[]; - activeItem?: T; - placeholder: string; - buttons?: QuickInputButton[]; - shouldResume?(): Promise; + activeItem?: T | ((quickPick: QuickPick) => Promise); + 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) => void; + onChangeItem?: { + callback: (event: E, quickPick: QuickPick) => void; + event: Event; + }; } -export interface InputBoxParameters { +interface InputBoxParameters { title: string; + password?: boolean; step?: number; totalSteps?: number; value: string; prompt: string; buttons?: QuickInputButton[]; validate(value: string): Promise; - shouldResume?(): Promise; } -type MultiStepInputQuickPicResponseType = T | (P extends { buttons: (infer I)[] } ? I : never); -type MultiStepInputInputBoxResponseType

= string | (P extends { buttons: (infer I)[] } ? I : never); +type MultiStepInputQuickPickResponseType = T | (P extends { buttons: (infer I)[] } ? I : never) | undefined; +type MultiStepInputInputBoxResponseType

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

({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P): Promise>; + showQuickPick>({ + title, + step, + totalSteps, + items, + activeItem, + placeholder, + customButtonSetups, + }: P): Promise>; + showInputBox

({ + title, + step, + totalSteps, + value, + prompt, + validate, + buttons, + }: P): Promise>; } export class MultiStepInput implements IMultiStepInput { private current?: QuickInput; + private steps: InputStep[] = []; - constructor(private readonly shell: IApplicationShell) { } - public run(start: InputStep, state: S) { + + constructor(private readonly shell: IApplicationShell) {} + + public run(start: InputStep, state: S): Promise { return this.stepThrough(start, state); } - public async showQuickPick>({ title, step, totalSteps, items, activeItem, placeholder, buttons, shouldResume }: P): Promise> { + public async showQuickPick>({ + title, + step, + totalSteps, + items, + activeItem, + placeholder, + customButtonSetups, + matchOnDescription, + matchOnDetail, + acceptFilterBoxTextAsSelection, + onChangeItem, + keepScrollPosition, + sortByLabel, + initialize, + }: P): Promise> { const disposables: Disposable[] = []; - try { - return await new Promise>((resolve, reject) => { - const input = this.shell.createQuickPick(); - input.title = title; - input.step = step; - input.totalSteps = totalSteps; - input.placeholder = placeholder; - input.ignoreFocusOut = true; - input.items = items; - if (activeItem) { - input.activeItems = [activeItem]; + const input = this.shell.createQuickPick(); + input.title = title; + input.step = step; + input.sortByLabel = sortByLabel || false; + input.totalSteps = totalSteps; + input.placeholder = placeholder; + input.ignoreFocusOut = true; + input.items = items; + input.matchOnDescription = matchOnDescription || false; + input.matchOnDetail = matchOnDetail || false; + input.buttons = this.steps.length > 1 ? [QuickInputButtons.Back] : []; + if (customButtonSetups) { + for (const customButtonSetup of customButtonSetups) { + input.buttons = [...input.buttons, customButtonSetup.button]; + } + } + if (this.current) { + this.current.dispose(); + } + this.current = input; + if (onChangeItem) { + disposables.push(onChangeItem.event((e) => onChangeItem.callback(e, input))); + } + // Quickpick should be initialized synchronously and on changed item handlers are registered synchronously. + if (initialize) { + initialize(input); + } + if (activeItem) { + if (typeof activeItem === 'function') { + activeItem(input).then((item) => { + if (input.activeItems.length === 0) { + input.activeItems = [item]; + } + }); + } + } else { + input.activeItems = []; + } + this.current.show(); + // Keep scroll position is only meant to keep scroll position when updating items, + // so do it after initialization. This ensures quickpick starts with the active + // item in focus when this is true, instead of having scroll position at top. + input.keepScrollPosition = keepScrollPosition; + + const deferred = createDeferred(); + + disposables.push( + input.onDidTriggerButton(async (item) => { + if (item === QuickInputButtons.Back) { + deferred.reject(InputFlowAction.back); + input.hide(); } - input.buttons = [ - ...(this.steps.length > 1 ? [QuickInputButtons.Back] : []), - ...(buttons || []) - ]; - disposables.push( - input.onDidTriggerButton(item => { - if (item === QuickInputButtons.Back) { - reject(InputFlowAction.back); - } else { - resolve(item); + if (customButtonSetups) { + for (const customButtonSetup of customButtonSetups) { + if (JSON.stringify(item) === JSON.stringify(customButtonSetup?.button)) { + await customButtonSetup?.callback(input); } - }), - input.onDidChangeSelection(selectedItems => resolve(selectedItems[0])), - input.onDidHide(() => { - (async () => { - reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); - })() - .catch(reject); - }) - ); - if (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

({ title, step, totalSteps, value, prompt, validate, buttons, shouldResume }: P): Promise> { + public async showInputBox

({ + title, + step, + totalSteps, + value, + prompt, + validate, + password, + buttons, + }: P): Promise> { const disposables: Disposable[] = []; try { return await new Promise>((resolve, reject) => { @@ -113,20 +228,19 @@ export class MultiStepInput implements IMultiStepInput { 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(item); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + resolve(item as any); } }), input.onDidAccept(async () => { @@ -139,7 +253,7 @@ export class MultiStepInput implements IMultiStepInput { 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 implements IMultiStepInput { } }), input.onDidHide(() => { - (async () => { - reject(shouldResume && await shouldResume() ? InputFlowAction.resume : InputFlowAction.cancel); - })() - .catch(reject); - }) + resolve(undefined); + }), ); if (this.current) { this.current.dispose(); @@ -161,7 +272,7 @@ export class MultiStepInput implements IMultiStepInput { this.current.show(); }); } finally { - disposables.forEach(d => d.dispose()); + disposables.forEach((d) => d.dispose()); } } @@ -179,6 +290,9 @@ export class MultiStepInput implements IMultiStepInput { if (err === InputFlowAction.back) { this.steps.pop(); step = this.steps.pop(); + if (step === undefined) { + throw err; + } } else if (err === InputFlowAction.resume) { step = this.steps.pop(); } else if (err === InputFlowAction.cancel) { @@ -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(): IMultiStepInput { return new MultiStepInput(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 = { + 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(disposable: T): T; +export function dispose(disposable: T | undefined): T | undefined; +export function dispose = Iterable>(disposables: A): A; +export function dispose(disposables: Array): Array; +export function dispose(disposables: ReadonlyArray): ReadonlyArray; +// eslint-disable-next-line @typescript-eslint/no-explicit-any, consistent-return +export function dispose(arg: T | Iterable | undefined): any { + if (Iterable.is(arg)) { + for (const d of arg) { + if (d) { + try { + d.dispose(); + } catch (e) { + traceWarn(`dispose() failed for ${d}`, e); + } + } + } + + return Array.isArray(arg) ? [] : arg; + } + if (arg) { + arg.dispose(); + return arg; + } +} + +/** + * Safely dispose each of the disposables. + */ +export async function disposeAll(disposables: IDisposable[]): Promise { + await Promise.all( + disposables.map(async (d) => { + try { + return Promise.resolve(d.dispose()); + } catch (err) { + // do nothing + } + return Promise.resolve(); + }), + ); +} + +/** + * A list of disposables. + */ +export class Disposables implements IDisposables { + private disposables: IDisposable[] = []; + + constructor(...disposables: IDisposable[]) { + this.disposables.push(...disposables); + } + + public push(...disposables: IDisposable[]): void { + this.disposables.push(...disposables); + } + + public async dispose(): Promise { + const { disposables } = this; + this.disposables = []; + await disposeAll(disposables); + } +} + +/** + * Manages a collection of disposable values. + * + * This is the preferred way to manage multiple disposables. A `DisposableStore` is safer to work with than an + * `IDisposable[]` as it considers edge cases, such as registering the same value multiple times or adding an item to a + * store that has already been disposed of. + */ +export class DisposableStore implements IDisposable { + static DISABLE_DISPOSED_WARNING = false; + + private readonly _toDispose = new Set(); + + private _isDisposed = false; + + constructor(...disposables: IDisposable[]) { + disposables.forEach((disposable) => this.add(disposable)); + } + + /** + * Dispose of all registered disposables and mark this object as disposed. + * + * Any future disposables added to this object will be disposed of on `add`. + */ + public dispose(): void { + if (this._isDisposed) { + return; + } + + this._isDisposed = true; + this.clear(); + } + + /** + * @return `true` if this object has been disposed of. + */ + public get isDisposed(): boolean { + return this._isDisposed; + } + + /** + * Dispose of all registered disposables but do not mark this object as disposed. + */ + public clear(): void { + if (this._toDispose.size === 0) { + return; + } + + try { + dispose(this._toDispose); + } finally { + this._toDispose.clear(); + } + } + + /** + * Add a new {@link IDisposable disposable} to the collection. + */ + public add(o: T): T { + if (!o) { + return o; + } + if (((o as unknown) as DisposableStore) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + + if (this._isDisposed) { + if (!DisposableStore.DISABLE_DISPOSED_WARNING) { + traceWarn( + new Error( + 'Trying to add a disposable to a DisposableStore that has already been disposed of. The added object will be leaked!', + ).stack, + ); + } + } else { + this._toDispose.add(o); + } + + return o; + } +} + +/** + * Abstract class for a {@link IDisposable disposable} object. + * + * Subclasses can {@linkcode _register} disposables that will be automatically cleaned up when this object is disposed of. + */ +export abstract class DisposableBase implements IDisposable { + protected readonly _store = new DisposableStore(); + + private _isDisposed = false; + + public get isDisposed(): boolean { + return this._isDisposed; + } + + constructor(...disposables: IDisposable[]) { + disposables.forEach((disposable) => this._store.add(disposable)); + } + + public dispose(): void { + this._store.dispose(); + this._isDisposed = true; + } + + /** + * Adds `o` to the collection of disposables managed by this object. + */ + public _register(o: T): T { + if (((o as unknown) as DisposableBase) === this) { + throw new Error('Cannot register a disposable on itself!'); + } + return this._store.add(o); + } +} diff --git a/src/client/common/utils/runAfterActivation.ts b/src/client/common/utils/runAfterActivation.ts new file mode 100644 index 000000000000..9a5297ea00f7 --- /dev/null +++ b/src/client/common/utils/runAfterActivation.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +const itemsToRun: (() => void)[] = []; +let activationCompleted = false; + +/** + * Add items to be run after extension activation. This will add item + * to the end of the list. This function will immediately run the item + * if extension is already activated. + */ +export function addItemsToRunAfterActivation(run: () => void): void { + if (activationCompleted) { + run(); + } else { + itemsToRun.push(run); + } +} + +/** + * This should be called after extension activation is complete. + */ +export function runAfterActivation(): void { + activationCompleted = true; + while (itemsToRun.length > 0) { + const run = itemsToRun.shift(); + if (run) { + run(); + } + } +} diff --git a/src/client/common/utils/stopWatch.ts b/src/client/common/utils/stopWatch.ts index 770c70fecefb..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 { +export class StopWatch implements IStopWatch { private started = new Date().getTime(); public get elapsedTime() { return new Date().getTime() - this.started; } - public reset(){ + public reset() { this.started = new Date().getTime(); } } + +export interface IStopWatch { + elapsedTime: number; +} diff --git a/src/client/common/utils/sysTypes.ts b/src/client/common/utils/sysTypes.ts index 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) && (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(info: T): RawBasicVersionInfo { + const copied: RawBasicVersionInfo = { + major: info.major, + minor: info.minor, + micro: info.micro, + }; + + const { unnormalized } = (info as unknown) as RawBasicVersionInfo; + if (unnormalized !== undefined) { + copied.unnormalized = { + major: unnormalized.major, + minor: unnormalized.minor, + micro: unnormalized.micro, + }; + } + + return copied; +} + +/** + * Make a copy and set all the properties properly. + * + * Only the "basic" version info will be set (and normalized). + * The caller is responsible for any other properties beyond that. + */ +function normalizeBasicVersionInfo(info: T | undefined): T { + if (!info) { + return EMPTY_VERSION as T; + } + const norm = copyStrict(info); + // Do not normalize if it has already been normalized. + if (norm.unnormalized === undefined) { + norm.unnormalized = {}; + [norm.major, norm.unnormalized.major] = normalizeVersionPart(norm.major); + [norm.minor, norm.unnormalized.minor] = normalizeVersionPart(norm.minor); + [norm.micro, norm.unnormalized.micro] = normalizeVersionPart(norm.micro); + } + return norm as T; } -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(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(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 = { + version: T; + before: string; + after: string; +}; + +const basicVersionPattern = ` + ^ + (.*?) # + (\\d+) # + (?: + [.] + (\\d+) # + (?: + [.] + (\\d+) # + )? + )? + ([^\\d].*)? # + $ +`; +const basicVersionRegexp = verboseRegExp(basicVersionPattern, 's'); + +/** + * Extract a version from the given text. + * + * If the version is surrounded by other text then that is provided + * as well. + */ +export function parseBasicVersionInfo(verStr: string): ParseResult | undefined { + const match = verStr.match(basicVersionRegexp); + if (!match) { + return undefined; + } + // Ignore the first element (the full match). + const [, before, majorStr, minorStr, microStr, after] = match; + if (before && before.endsWith('.')) { + return undefined; + } + + if (after && after !== '') { + if (after === '.') { + return undefined; + } + // Disallow a plain version with trailing text if it isn't complete + if (!before || before === '') { + if (!microStr || microStr === '') { + return undefined; + } + } + } + const major = parseInt(majorStr, 10); + const minor = minorStr ? parseInt(minorStr, 10) : -1; + const micro = microStr ? parseInt(microStr, 10) : -1; + return { + // This is effectively normalized. + version: ({ major, minor, micro } as unknown) as T, + before: before || '', + after: after || '', + }; +} + +/** + * Returns true if the given version appears to be not set. + * + * The object is expected to already be normalized. + */ +export function isVersionInfoEmpty(info: T): boolean { + if (!info) { + return false; + } + if (typeof info.major !== 'number' || typeof info.minor !== 'number' || typeof info.micro !== 'number') { + return false; + } + return info.major < 0 && info.minor < 0 && info.micro < 0; +} + +/** + * Decide if two versions are the same or if one is "less". + * + * Note that a less-complete object that otherwise matches + * is considered "less". + * + * Additional checks for an otherwise "identical" version may be made + * through `compareExtra()`. + * + * @returns - the customary comparison indicator (e.g. -1 means left is "more") + * @returns - a string that indicates the property where they differ (if any) + */ +export function compareVersions( + // the versions to compare: + left: T, + right: V, + compareExtra?: (v1: T, v2: V) => [number, string], +): [number, string] { + if (left.major < right.major) { + return [1, 'major']; + } + if (left.major > right.major) { + return [-1, 'major']; + } + if (left.major === -1) { + // Don't bother checking minor or micro. + return [0, '']; + } + + if (left.minor < right.minor) { + return [1, 'minor']; + } + if (left.minor > right.minor) { + return [-1, 'minor']; + } + if (left.minor === -1) { + // Don't bother checking micro. + return [0, '']; + } + + if (left.micro < right.micro) { + return [1, 'micro']; + } + if (left.micro > right.micro) { + return [-1, 'micro']; + } + + if (compareExtra !== undefined) { + return compareExtra(left, right); + } + + return [0, '']; +} + +// base version info + +/** + * basic version information + * + * @prop raw - the unparsed version string, if any + */ +export type VersionInfo = BasicVersionInfo & { + raw?: string; +}; + +/** + * Make a copy and set all the properties properly. + */ +export function normalizeVersionInfo(info: T): T { + const norm = normalizeBasicVersionInfo(info); + norm.raw = info.raw; + if (!norm.raw) { + norm.raw = ''; + } + // Any string value of "raw" is considered normalized. + return norm; +} + +/** + * Fail if any properties are not set properly. + * + * Optional properties that are not set are ignored. + * + * This assumes that the info has already been normalized. + */ +export function validateVersionInfo(info: T): void { + validateBasicVersionInfo(info); + // `info.raw` can be anything. +} + +/** + * Extract a version from the given text. + * + * If the version is surrounded by other text then that is provided + * as well. + */ +export function parseVersionInfo(verStr: string): ParseResult | undefined { + const result = parseBasicVersionInfo(verStr); + if (result === undefined) { + return undefined; + } + result.version.raw = verStr; + return result; +} + +/** + * Checks if major, minor, and micro match. + * + * Additional checks may be made through `compareExtra()`. + */ +export function areIdenticalVersion( + // the versions to compare: + left: T, + right: V, + compareExtra?: (v1: T, v2: V) => [number, string], +): boolean { + const [result] = compareVersions(left, right, compareExtra); + return result === 0; +} + +/** + * Checks if the versions are identical or one is more complete than other (and otherwise the same). + * + * At the least the major version must be set (non-negative). + */ +export function areSimilarVersions( + // the versions to compare: + left: T, + right: V, + compareExtra?: (v1: T, v2: V) => [number, string], +): boolean { + const [result, prop] = compareVersions(left, right, compareExtra); + if (result === 0) { + return true; + } + + if (prop === 'major') { + // An empty version is never similar (except to another empty version). + return false; + } + + if (result < 0) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((right as unknown) as any)[prop] === -1; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ((left as unknown) as any)[prop] === -1; +} + +// semver + +export function parseSemVerSafe(raw: string): semver.SemVer { + raw = raw.replace(/\.00*(?=[1-9]|0\.)/, '.'); + const ver = semver.coerce(raw); + if (ver === null || !semver.valid(ver)) { + // 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 = () => Promise; +type WorkFunc = (item: T) => Promise; +type PostResult = (item: T, result?: R, err?: Error) => void; + +interface IWorkItem { + item: T; +} + +export enum QueuePosition { + Back, + Front, +} + +export interface IWorkerPool extends IWorker { + /** + * Add items to be processed to a queue. + * @method addToQueue + * @param {T} item: Item to process + * @param {QueuePosition} position: Add items to the front or back of the queue. + * @returns A promise that when resolved gets the result from running the worker function. + */ + addToQueue(item: T, position?: QueuePosition): Promise; +} + +class Worker implements IWorker { + private stopProcessing: boolean = false; + public constructor( + private readonly next: NextFunc, + private readonly workFunc: WorkFunc, + private readonly postResult: PostResult, + private readonly name: string, + ) {} + public stop() { + this.stopProcessing = true; + } + + public async start() { + while (!this.stopProcessing) { + try { + const workItem = await this.next(); + try { + const result = await this.workFunc(workItem); + this.postResult(workItem, result); + } catch (ex) { + this.postResult(workItem, undefined, ex as Error); + } + } catch (ex) { + // Next got rejected. Likely worker pool is shutting down. + // continue here and worker will exit if the worker pool is shutting down. + traceError(`Error while running worker[${this.name}].`, ex); + continue; + } + } + } +} + +class WorkQueue { + private readonly items: IWorkItem[] = []; + private readonly results: Map, Deferred> = new Map(); + public add(item: T, position?: QueuePosition): Promise { + // Wrap the user provided item in a wrapper object. This will allow us to track multiple + // submissions of the same item. For example, addToQueue(2), addToQueue(2). If we did not + // wrap this, then from the map both submissions will look the same. Since this is a generic + // worker pool, we do not know if we can resolve both using the same promise. So, a better + // approach is to ensure each gets a unique promise, and let the worker function figure out + // how to handle repeat submissions. + const workItem: IWorkItem = { item }; + if (position === QueuePosition.Front) { + this.items.unshift(workItem); + } else { + this.items.push(workItem); + } + + // This is the promise that will be resolved when the work + // item is complete. We save this in a map to resolve when + // the worker finishes and posts the result. + const deferred = createDeferred(); + this.results.set(workItem, deferred); + + return deferred.promise; + } + + public completed(workItem: IWorkItem, result?: R, error?: Error): void { + const deferred = this.results.get(workItem); + if (deferred !== undefined) { + this.results.delete(workItem); + if (error !== undefined) { + deferred.reject(error); + } + deferred.resolve(result); + } + } + + public next(): IWorkItem | undefined { + return this.items.shift(); + } + + public clear(): void { + this.results.forEach((v: Deferred, k: IWorkItem, map: Map, Deferred>) => { + v.reject(Error('Queue stopped processing')); + map.delete(k); + }); + } +} + +class WorkerPool implements IWorkerPool { + // This collection tracks the full set of workers. + private readonly workers: IWorker[] = []; + + // A collections that holds unblock callback for each worker waiting + // for a work item when the queue is empty + private readonly waitingWorkersUnblockQueue: { unblock(w: IWorkItem): void; stop(): void }[] = []; + + // A collection that manages the work items. + private readonly queue = new WorkQueue(); + + // State of the pool manages via stop(), start() + private stopProcessing = false; + + public constructor( + private readonly workerFunc: WorkFunc, + private readonly numWorkers: number = 2, + private readonly name: string = 'Worker', + ) {} + + public addToQueue(item: T, position?: QueuePosition): Promise { + if (this.stopProcessing) { + throw Error('Queue is stopped'); + } + + // This promise when resolved should return the processed result of the item + // being added to the queue. + const deferred = this.queue.add(item, position); + + const worker = this.waitingWorkersUnblockQueue.shift(); + if (worker) { + const workItem = this.queue.next(); + if (workItem !== undefined) { + // If we are here it means there were no items to process in the queue. + // At least one worker is free and waiting for a work item. Call 'unblock' + // and give the worker the newly added item. + worker.unblock(workItem); + } else { + // Something is wrong, we should not be here. we just added an item to + // the queue. It should not be empty. + traceError('Work queue was empty immediately after adding item.'); + } + } + + return deferred; + } + + public start() { + this.stopProcessing = false; + let num = this.numWorkers; + while (num > 0) { + this.workers.push( + new Worker, R>( + () => this.nextWorkItem(), + (workItem: IWorkItem) => this.workerFunc(workItem.item), + (workItem: IWorkItem, result?: R, error?: Error) => + this.queue.completed(workItem, result, error), + `${this.name} ${num}`, + ), + ); + num = num - 1; + } + this.workers.forEach(async (w) => w.start()); + } + + public stop(): void { + this.stopProcessing = true; + + // Signal all registered workers with this worker pool to stop processing. + // Workers should complete the task they are currently doing. + let worker = this.workers.shift(); + while (worker) { + worker.stop(); + worker = this.workers.shift(); + } + + // Remove items from queue. + this.queue.clear(); + + // This is necessary to exit any worker that is waiting for an item. + // If we don't unblock here then the worker just remains blocked + // forever. + let blockedWorker = this.waitingWorkersUnblockQueue.shift(); + while (blockedWorker) { + blockedWorker.stop(); + blockedWorker = this.waitingWorkersUnblockQueue.shift(); + } + } + + public nextWorkItem(): Promise> { + // Note that next() will return `undefined` if the queue is empty. + const nextWorkItem = this.queue.next(); + if (nextWorkItem !== undefined) { + return Promise.resolve(nextWorkItem); + } + + // Queue is Empty, so return a promise that will be resolved when + // new items are added to the queue. + return new Promise>((resolve, reject) => { + this.waitingWorkersUnblockQueue.push({ + unblock: (workItem: IWorkItem) => { + // This will be called to unblock any worker waiting for items. + if (this.stopProcessing) { + // We should reject here since the processing should be stopped. + reject(); + } + // If we are here, the queue received a new work item. Resolve with that item. + resolve(workItem); + }, + stop: () => { + reject(); + }, + }); + }); + } +} + +export function createRunningWorkerPool( + workerFunc: WorkFunc, + numWorkers?: number, + name?: string, +): WorkerPool { + const pool = new WorkerPool(workerFunc, numWorkers, name); + pool.start(); + return pool; +} diff --git a/src/client/common/variables/environment.ts b/src/client/common/variables/environment.ts index 92921345a6f6..9f0abd9b0ee7 100644 --- a/src/client/common/variables/environment.ts +++ b/src/client/common/variables/environment.ts @@ -1,53 +1,112 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -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 { + 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, baseVars?: EnvironmentVariables): Promise { - 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 parseEnvFile(await fs.readFile(filePath), baseVars); + 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; @@ -63,19 +122,19 @@ export class EnvironmentVariablesService implements IEnvironmentVariablesService } } -export function parseEnvFile( - lines: string | Buffer, - baseVars?: EnvironmentVariables -): EnvironmentVariables { +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); - }); + 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; } @@ -84,7 +143,7 @@ function parseEnvLine(line: string): [string, string] { // https://github.com/motdotla/dotenv/blob/master/lib/main.js#L32 // We don't use dotenv here because it loses ordering, which is // significant for substitution. - const match = line.match(/^\s*([a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/); + const match = line.match(/^\s*(_*[a-zA-Z]\w*)\s*=\s*(.*?)?\s*$/); if (!match) { return ['', '']; } @@ -92,7 +151,7 @@ function parseEnvLine(line: string): [string, string] { const name = match[1]; let value = match[2]; if (value && value !== '') { - if (value[0] === '\'' && value[value.length - 1] === '\'') { + 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] === '"') { @@ -112,7 +171,7 @@ function substituteEnvVars( value: string, localVars: EnvironmentVariables, globalVars: EnvironmentVariables, - missing = '' + missing = '', ): string { // Substitution here is inspired a little by dotenv-expand: // https://github.com/motdotla/dotenv-expand/blob/master/lib/main.js @@ -136,3 +195,40 @@ function substituteEnvVars( return value.replace(/\\\$/g, '$'); } + +export function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const normalizedEnv: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + const normalizedKey = normCase(key); + normalizedEnv[normalizedKey] = env[key]; + }); + return normalizedEnv; +} + +export function restoreKeys(env: EnvironmentVariables) { + const processEnvKeys = Object.keys(process.env); + processEnvKeys.forEach((processEnvKey) => { + const originalKey = normCase(processEnvKey); + if (originalKey !== processEnvKey && env[originalKey] !== undefined) { + env[processEnvKey] = env[originalKey]; + delete env[originalKey]; + } + }); +} + +export function matchTarget(reference: EnvironmentVariables, target: EnvironmentVariables): void { + Object.keys(reference).forEach((key) => { + if (target.hasOwnProperty(key)) { + reference[key] = target[key]; + } else { + delete reference[key]; + } + }); + + // Add any new keys from target to reference + Object.keys(target).forEach((key) => { + if (!reference.hasOwnProperty(key)) { + reference[key] = target[key]; + } + }); +} diff --git a/src/client/common/variables/environmentVariablesProvider.ts b/src/client/common/variables/environmentVariablesProvider.ts index aa1d6d606cef..14573d2204aa 100644 --- a/src/client/common/variables/environmentVariablesProvider.ts +++ b/src/client/common/variables/environmentVariablesProvider.ts @@ -2,26 +2,39 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; +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 { cacheResourceSpecificInterpreterData, clearCachedResourceSpecificIngterpreterData } from '../utils/decorators'; +import { ICurrentProcess, IDisposableRegistry } from '../types'; +import { InMemoryCache } from '../utils/cacheUtils'; +import { SystemVariables } from './systemVariables'; import { EnvironmentVariables, IEnvironmentVariablesProvider, IEnvironmentVariablesService } from './types'; -const cacheDuration = 60 * 60 * 1000; +const CACHE_DURATION = 60 * 60 * 1000; @injectable() export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvider, Disposable { public trackedWorkspaceFolders = new Set(); + private fileWatchers = new Map(); + private disposables: Disposable[] = []; + private changeEventEmitter: EventEmitter; - constructor(@inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, + + private readonly envVarCaches = new Map>(); + + constructor( + @inject(IEnvironmentVariablesService) private envVarsService: IEnvironmentVariablesService, @inject(IDisposableRegistry) disposableRegistry: Disposable[], @inject(IPlatformService) private platformService: IPlatformService, @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(ICurrentProcess) private process: ICurrentProcess) { + @inject(ICurrentProcess) private process: ICurrentProcess, + private cacheDuration: number = CACHE_DURATION, + ) { disposableRegistry.push(this); this.changeEventEmitter = new EventEmitter(); const disposable = this.workspaceService.onDidChangeConfiguration(this.configurationChanged, this); @@ -32,21 +45,66 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid return this.changeEventEmitter.event; } - public dispose() { + public dispose(): void { this.changeEventEmitter.dispose(); - this.fileWatchers.forEach(watcher => { + this.fileWatchers.forEach((watcher) => { if (watcher) { watcher.dispose(); } }); } - @cacheResourceSpecificInterpreterData('getEnvironmentVariables', cacheDuration) + public async getEnvironmentVariables(resource?: Uri): Promise { - const settings = this.configurationService.getSettings(resource); - const workspaceFolderUri = this.getWorkspaceFolderUri(resource); - this.trackedWorkspaceFolders.add(workspaceFolderUri ? workspaceFolderUri.fsPath : ''); - this.createFileWatcher(settings.envFile, workspaceFolderUri); - let mergedVars = await this.envVarsService.parseFile(settings.envFile, this.process.env); + 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 }; + } + } + return undefined; + } + + private setCachedEnvironmentVariables(resource: Uri | undefined, vars: EnvironmentVariables): void { + const cacheKey = this.getWorkspaceFolderUri(resource)?.fsPath ?? ''; + const cache = new InMemoryCache(this.cacheDuration); + this.envVarCaches.set(cacheKey, cache); + cache.data = { ...vars }; + } + + public async _getEnvironmentVariables(resource?: Uri): Promise { + 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 = {}; } @@ -61,15 +119,43 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid } return mergedVars; } - public configurationChanged(e: ConfigurationChangeEvent) { - this.trackedWorkspaceFolders.forEach(item => { + + public async getCustomEnvironmentVariables(resource?: Uri): Promise { + return this.envVarsService.parseFile(this.getEnvFile(resource), this.process.env); + } + + private getCustomEnvironmentVariablesSync(resource?: Uri): EnvironmentVariables | undefined { + return this.envVarsService.parseFileSync(this.getEnvFile(resource), this.process.env); + } + + private getEnvFile(resource?: Uri): string { + const systemVariables: SystemVariables = new SystemVariables( + undefined, + PythonSettings.getSettingsUriAndTarget(resource, this.workspaceService).uri?.fsPath, + this.workspaceService, + ); + const workspaceFolderUri = this.getWorkspaceFolderUri(resource); + const envFileSetting = this.workspaceService.getConfiguration('python', resource).get('envFile'); + const envFile = systemVariables.resolveAny(envFileSetting); + if (envFile === undefined) { + traceError('Unable to read `python.envFile` setting for resource', JSON.stringify(resource)); + return workspaceFolderUri?.fsPath ? path.join(workspaceFolderUri?.fsPath, '.env') : ''; + } + this.trackedWorkspaceFolders.add(workspaceFolderUri ? workspaceFolderUri.fsPath : ''); + this.createFileWatcher(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); } }); } - public createFileWatcher(envFile: string, workspaceFolderUri?: Uri) { + + public createFileWatcher(envFile: string, workspaceFolderUri?: Uri): void { if (this.fileWatchers.has(envFile)) { return; } @@ -77,20 +163,27 @@ export class EnvironmentVariablesProvider implements IEnvironmentVariablesProvid this.fileWatchers.set(envFile, envFileWatcher); if (envFileWatcher) { this.disposables.push(envFileWatcher.onDidChange(() => this.onEnvironmentFileChanged(workspaceFolderUri))); - this.disposables.push(envFileWatcher.onDidCreate(() => 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; + return undefined; } const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource!); return workspaceFolder ? workspaceFolder.uri : undefined; } + + private onEnvironmentFileCreated(workspaceFolderUri?: Uri) { + this.onEnvironmentFileChanged(workspaceFolderUri); + sendFileCreationTelemetry(); + } + private onEnvironmentFileChanged(workspaceFolderUri?: Uri) { - clearCachedResourceSpecificIngterpreterData('getEnvironmentVariables', workspaceFolderUri); - clearCachedResourceSpecificIngterpreterData('CustomEnvironmentVariables', workspaceFolderUri); + // 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, EnvironmentVariablesService); - serviceManager.addSingleton(IEnvironmentVariablesProvider, EnvironmentVariablesProvider); + serviceManager.addSingleton( + IEnvironmentVariablesService, + EnvironmentVariablesService, + ); + serviceManager.addSingleton( + 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 217247fd71f2..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): IStringDictionary; public resolve(value: IStringDictionary): IStringDictionary; public resolve(value: IStringDictionary>): IStringDictionary>; - // tslint:disable-next-line:no-any + public resolve(value: any): any { if (Types.isString(value)) { return this.__resolveString(value); @@ -31,7 +31,7 @@ export abstract class AbstractSystemVariables implements ISystemVariables { } public resolveAny(value: T): T; - // tslint:disable-next-line:no-any + public resolveAny(value: any): any { if (Types.isString(value)) { return this.__resolveString(value); @@ -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 = (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[]> { + private __resolveLiteral( + values: IStringDictionary | string[]>, + ): IStringDictionary | string[]> { const result: IStringDictionary | 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] = this.resolve(value); }); return result; } private __resolveAnyLiteral(values: T): T; - // tslint:disable-next-line:no-any + private __resolveAnyLiteral(values: any): any { const result: IStringDictionary | string[]> = Object.create(null); - Object.keys(values).forEach(key => { + Object.keys(values).forEach((key) => { const value = values[key]; - // tslint:disable-next-line:no-any + result[key] = this.resolveAny(value); }); return result; } private __resolveArray(value: string[]): string[] { - return value.map(s => this.__resolveString(s)); + return value.map((s) => this.__resolveString(s)); } private __resolveAnyArray(value: T[]): T[]; - // tslint:disable-next-line:no-any + private __resolveAnyArray(value: any[]): any[] { - return value.map(s => this.resolveAny(s)); + 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 Record)[`env:${key}`] = (this as any as Record)[`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)[`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)[`workspaceFolder:${basename}`] = + folder.uri.fsPath; + ((this as any) as Record)[`workspaceFolder:${folder.name}`] = + folder.uri.fsPath; + }); + } catch { + // This try...catch block is here to support pre-existing tests, ignore error. + } } public get cwd(): string { @@ -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 91be6e36a5fc..252a0d48038f 100644 --- a/src/client/common/variables/types.ts +++ b/src/client/common/variables/types.ts @@ -9,7 +9,12 @@ export const IEnvironmentVariablesService = Symbol('IEnvironmentVariablesService export interface IEnvironmentVariablesService { parseFile(filePath?: string, baseVars?: EnvironmentVariables): Promise; - mergeVariables(source: EnvironmentVariables, target: EnvironmentVariables): void; + parseFileSync(filePath?: string, baseVars?: EnvironmentVariables): EnvironmentVariables | undefined; + mergeVariables( + source: EnvironmentVariables, + target: EnvironmentVariables, + options?: { overwrite?: boolean; mergeAll?: boolean }, + ): void; appendPythonPath(vars: EnvironmentVariables, ...pythonPaths: string[]): void; appendPath(vars: EnvironmentVariables, ...paths: string[]): void; } @@ -29,7 +34,7 @@ export interface ISystemVariables { resolve(value: IStringDictionary): IStringDictionary; resolve(value: IStringDictionary>): IStringDictionary>; resolveAny(value: T): T; - // tslint:disable-next-line:no-any + [key: string]: any; } @@ -38,4 +43,5 @@ export const IEnvironmentVariablesProvider = Symbol('IEnvironmentVariablesProvid export interface IEnvironmentVariablesProvider { onDidEnvironmentVariablesChange: Event; getEnvironmentVariables(resource?: Uri): Promise; + getEnvironmentVariablesSync(resource?: Uri): EnvironmentVariables; } diff --git a/src/client/common/vscodeApis/browserApis.ts b/src/client/common/vscodeApis/browserApis.ts new file mode 100644 index 000000000000..ccf51bd07ec8 --- /dev/null +++ b/src/client/common/vscodeApis/browserApis.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { env, Uri } from 'vscode'; + +export function launch(url: string): void { + env.openExternal(Uri.parse(url)); +} diff --git a/src/client/common/vscodeApis/commandApis.ts b/src/client/common/vscodeApis/commandApis.ts new file mode 100644 index 000000000000..908cb761c538 --- /dev/null +++ b/src/client/common/vscodeApis/commandApis.ts @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { commands, Disposable } from 'vscode'; + +/** + * Wrapper for vscode.commands.executeCommand to make it easier to mock in tests + */ +export function executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); +} + +/** + * Wrapper for vscode.commands.registerCommand to make it easier to mock in tests + */ +export function registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return commands.registerCommand(command, callback, thisArg); +} diff --git a/src/client/common/vscodeApis/extensionsApi.ts b/src/client/common/vscodeApis/extensionsApi.ts new file mode 100644 index 000000000000..f099d6f636b0 --- /dev/null +++ b/src/client/common/vscodeApis/extensionsApi.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as fs from '../platform/fs-paths'; +import { PVSC_EXTENSION_ID } from '../constants'; + +export function getExtension(extensionId: string): vscode.Extension | undefined { + return vscode.extensions.getExtension(extensionId); +} + +export function isExtensionEnabled(extensionId: string): boolean { + return vscode.extensions.getExtension(extensionId) !== undefined; +} + +export function isExtensionDisabled(extensionId: string): boolean { + // We need an enabled extension to find the extensions dir. + const pythonExt = getExtension(PVSC_EXTENSION_ID); + if (pythonExt) { + let found = false; + fs.readdirSync(path.dirname(pythonExt.extensionPath), { withFileTypes: false }).forEach((s) => { + if (s.toString().startsWith(extensionId)) { + found = true; + } + }); + return found; + } + return false; +} + +export function isInsider(): boolean { + return vscode.env.appName.includes('Insider'); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function getExtensions(): readonly vscode.Extension[] { + return vscode.extensions.all; +} diff --git a/src/client/common/vscodeApis/languageApis.ts b/src/client/common/vscodeApis/languageApis.ts new file mode 100644 index 000000000000..87681507693d --- /dev/null +++ b/src/client/common/vscodeApis/languageApis.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { DiagnosticChangeEvent, DiagnosticCollection, Disposable, languages } from 'vscode'; + +export function createDiagnosticCollection(name: string): DiagnosticCollection { + return languages.createDiagnosticCollection(name); +} + +export function onDidChangeDiagnostics(handler: (e: DiagnosticChangeEvent) => void): Disposable { + return languages.onDidChangeDiagnostics(handler); +} diff --git a/src/client/common/vscodeApis/windowApis.ts b/src/client/common/vscodeApis/windowApis.ts new file mode 100644 index 000000000000..90a06e7ed75a --- /dev/null +++ b/src/client/common/vscodeApis/windowApis.ts @@ -0,0 +1,280 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ + +import { + CancellationToken, + MessageItem, + MessageOptions, + Progress, + ProgressOptions, + QuickPick, + QuickInputButtons, + QuickPickItem, + QuickPickOptions, + TextEditor, + window, + Disposable, + QuickPickItemButtonEvent, + Uri, + TerminalShellExecutionStartEvent, + LogOutputChannel, + OutputChannel, + TerminalLinkProvider, + NotebookDocument, + NotebookEditor, + NotebookDocumentShowOptions, + Terminal, +} from 'vscode'; +import { createDeferred, Deferred } from '../utils/async'; +import { Resource } from '../types'; +import { getWorkspaceFolders } from './workspaceApis'; + +export function showTextDocument(uri: Uri): Thenable { + return window.showTextDocument(uri); +} + +export function showNotebookDocument( + document: NotebookDocument, + options?: NotebookDocumentShowOptions, +): Thenable { + return window.showNotebookDocument(document, options); +} + +export function showQuickPick( + items: readonly T[] | Thenable, + options?: QuickPickOptions, + token?: CancellationToken, +): Thenable { + return window.showQuickPick(items, options, token); +} + +export function createQuickPick(): QuickPick { + return window.createQuickPick(); +} + +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showErrorMessage(message: string, ...items: T[]): Thenable; +export function showErrorMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showErrorMessage(message: string, ...items: any[]): Thenable { + return window.showErrorMessage(message, ...items); +} + +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showWarningMessage(message: string, ...items: T[]): Thenable; +export function showWarningMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showWarningMessage(message: string, ...items: any[]): Thenable { + return window.showWarningMessage(message, ...items); +} + +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; +export function showInformationMessage(message: string, ...items: T[]): Thenable; +export function showInformationMessage( + message: string, + options: MessageOptions, + ...items: T[] +): Thenable; + +export function showInformationMessage(message: string, ...items: any[]): Thenable { + return window.showInformationMessage(message, ...items); +} + +export function withProgress( + options: ProgressOptions, + task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable, +): Thenable { + return window.withProgress(options, task); +} + +export function getActiveTextEditor(): TextEditor | undefined { + const { activeTextEditor } = window; + return activeTextEditor; +} + +export function onDidChangeActiveTextEditor(handler: (e: TextEditor | undefined) => void): Disposable { + return window.onDidChangeActiveTextEditor(handler); +} + +export function onDidStartTerminalShellExecution(handler: (e: TerminalShellExecutionStartEvent) => void): Disposable { + return window.onDidStartTerminalShellExecution(handler); +} + +export function onDidChangeTerminalState(handler: (e: Terminal) => void): Disposable { + return window.onDidChangeTerminalState(handler); +} + +export enum MultiStepAction { + Back = 'Back', + Cancel = 'Cancel', + Continue = 'Continue', +} + +export async function showQuickPickWithBack( + items: readonly T[], + options?: QuickPickOptions, + token?: CancellationToken, + itemButtonHandler?: (e: QuickPickItemButtonEvent) => void, +): Promise { + const quickPick: QuickPick = window.createQuickPick(); + const disposables: Disposable[] = [quickPick]; + + quickPick.items = items; + quickPick.buttons = [QuickInputButtons.Back]; + quickPick.canSelectMany = options?.canPickMany ?? false; + quickPick.ignoreFocusOut = options?.ignoreFocusOut ?? false; + quickPick.matchOnDescription = options?.matchOnDescription ?? false; + quickPick.matchOnDetail = options?.matchOnDetail ?? false; + quickPick.placeholder = options?.placeHolder; + quickPick.title = options?.title; + + const deferred = createDeferred(); + + disposables.push( + quickPick, + quickPick.onDidTriggerButton((item) => { + if (item === QuickInputButtons.Back) { + deferred.reject(MultiStepAction.Back); + quickPick.hide(); + } + }), + quickPick.onDidAccept(() => { + if (!deferred.completed) { + if (quickPick.canSelectMany) { + deferred.resolve(quickPick.selectedItems.map((item) => item)); + } else { + deferred.resolve(quickPick.selectedItems[0]); + } + + quickPick.hide(); + } + }), + quickPick.onDidHide(() => { + if (!deferred.completed) { + deferred.resolve(undefined); + } + }), + quickPick.onDidTriggerItemButton((e) => { + if (itemButtonHandler) { + itemButtonHandler(e); + } + }), + ); + if (token) { + disposables.push( + token.onCancellationRequested(() => { + quickPick.hide(); + }), + ); + } + quickPick.show(); + + try { + return await deferred.promise; + } finally { + disposables.forEach((d) => d.dispose()); + } +} + +export class MultiStepNode { + constructor( + public previous: MultiStepNode | undefined, + public readonly current: (context?: MultiStepAction) => Promise, + public next: MultiStepNode | undefined, + ) {} + + public static async run(step: MultiStepNode, context?: MultiStepAction): Promise { + let nextStep: MultiStepNode | undefined = step; + let flowAction = await nextStep.current(context); + while (nextStep !== undefined) { + if (flowAction === MultiStepAction.Cancel) { + return flowAction; + } + if (flowAction === MultiStepAction.Back) { + nextStep = nextStep?.previous; + } + if (flowAction === MultiStepAction.Continue) { + nextStep = nextStep?.next; + } + + if (nextStep) { + flowAction = await nextStep?.current(flowAction); + } + } + + return flowAction; + } +} + +export function createStepBackEndNode(deferred?: Deferred): MultiStepNode { + return new MultiStepNode( + undefined, + async () => { + if (deferred) { + // This is to ensure we don't leave behind any pending promises. + deferred.reject(MultiStepAction.Back); + } + return Promise.resolve(MultiStepAction.Back); + }, + undefined, + ); +} + +export function createStepForwardEndNode(deferred?: Deferred, result?: T): MultiStepNode { + return new MultiStepNode( + undefined, + async () => { + if (deferred) { + // This is to ensure we don't leave behind any pending promises. + deferred.resolve(result); + } + return Promise.resolve(MultiStepAction.Back); + }, + undefined, + ); +} + +export function getActiveResource(): Resource { + const editor = window.activeTextEditor; + if (editor && !editor.document.isUntitled) { + return editor.document.uri; + } + const workspaces = getWorkspaceFolders(); + return Array.isArray(workspaces) && workspaces.length > 0 ? workspaces[0].uri : undefined; +} + +export function createOutputChannel(name: string, languageId?: string): OutputChannel { + return window.createOutputChannel(name, languageId); +} +export function createLogOutputChannel(name: string, options: { log: true }): LogOutputChannel { + return window.createOutputChannel(name, options); +} + +export function registerTerminalLinkProvider(provider: TerminalLinkProvider): Disposable { + return window.registerTerminalLinkProvider(provider); +} diff --git a/src/client/common/vscodeApis/workspaceApis.ts b/src/client/common/vscodeApis/workspaceApis.ts new file mode 100644 index 000000000000..cd45f655702d --- /dev/null +++ b/src/client/common/vscodeApis/workspaceApis.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as vscode from 'vscode'; +import { Resource } from '../types'; + +export function getWorkspaceFolders(): readonly vscode.WorkspaceFolder[] | undefined { + return vscode.workspace.workspaceFolders; +} + +export function getWorkspaceFolder(uri: Resource): vscode.WorkspaceFolder | undefined { + return uri ? vscode.workspace.getWorkspaceFolder(uri) : undefined; +} + +export function getWorkspaceFolderPaths(): string[] { + return vscode.workspace.workspaceFolders?.map((w) => w.uri.fsPath) ?? []; +} + +export function getConfiguration( + section?: string, + scope?: vscode.ConfigurationScope | null, +): vscode.WorkspaceConfiguration { + return vscode.workspace.getConfiguration(section, scope); +} + +export function applyEdit(edit: vscode.WorkspaceEdit): Thenable { + return vscode.workspace.applyEdit(edit); +} + +export function findFiles( + include: vscode.GlobPattern, + exclude?: vscode.GlobPattern | null, + maxResults?: number, + token?: vscode.CancellationToken, +): Thenable { + return vscode.workspace.findFiles(include, exclude, maxResults, token); +} + +export function onDidCloseTextDocument(handler: (e: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseTextDocument(handler); +} + +export function onDidSaveTextDocument(handler: (e: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidSaveTextDocument(handler); +} + +export function getOpenTextDocuments(): readonly vscode.TextDocument[] { + return vscode.workspace.textDocuments; +} + +export function onDidOpenTextDocument(handler: (doc: vscode.TextDocument) => void): vscode.Disposable { + return vscode.workspace.onDidOpenTextDocument(handler); +} + +export function onDidChangeTextDocument(handler: (e: vscode.TextDocumentChangeEvent) => void): vscode.Disposable { + return vscode.workspace.onDidChangeTextDocument(handler); +} + +export function onDidChangeConfiguration(handler: (e: vscode.ConfigurationChangeEvent) => void): vscode.Disposable { + return vscode.workspace.onDidChangeConfiguration(handler); +} + +export function onDidCloseNotebookDocument(handler: (e: vscode.NotebookDocument) => void): vscode.Disposable { + return vscode.workspace.onDidCloseNotebookDocument(handler); +} + +export function createFileSystemWatcher( + globPattern: vscode.GlobPattern, + ignoreCreateEvents?: boolean, + ignoreChangeEvents?: boolean, + ignoreDeleteEvents?: boolean, +): vscode.FileSystemWatcher { + return vscode.workspace.createFileSystemWatcher( + globPattern, + ignoreCreateEvents, + ignoreChangeEvents, + ignoreDeleteEvents, + ); +} + +export function onDidChangeWorkspaceFolders( + handler: (e: vscode.WorkspaceFoldersChangeEvent) => void, +): vscode.Disposable { + return vscode.workspace.onDidChangeWorkspaceFolders(handler); +} + +export function isVirtualWorkspace(): boolean { + const isVirtualWorkspace = + vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders.every((f) => f.uri.scheme !== 'file'); + return !!isVirtualWorkspace; +} + +export function isTrusted(): boolean { + return vscode.workspace.isTrusted; +} + +export function onDidGrantWorkspaceTrust(handler: () => void): vscode.Disposable { + return vscode.workspace.onDidGrantWorkspaceTrust(handler); +} + +export function createDirectory(uri: vscode.Uri): Thenable { + return vscode.workspace.fs.createDirectory(uri); +} + +export function openNotebookDocument(uri: vscode.Uri): Thenable; +export function openNotebookDocument( + notebookType: string, + content?: vscode.NotebookData, +): Thenable; +export function openNotebookDocument(notebook: any, content?: vscode.NotebookData): Thenable { + return vscode.workspace.openNotebookDocument(notebook, content); +} + +export function copy(source: vscode.Uri, dest: vscode.Uri, options?: { overwrite?: boolean }): Thenable { + return vscode.workspace.fs.copy(source, dest, options); +} diff --git a/src/client/components.ts b/src/client/components.ts new file mode 100644 index 000000000000..f06f69eaac35 --- /dev/null +++ b/src/client/components.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IExtensionContext } from './common/types'; +import { IServiceContainer, IServiceManager } from './ioc/types'; + +/** + * The global extension state needed by components. + * + */ +export type ExtensionState = { + context: IExtensionContext; + disposables: IDisposableRegistry; + // For now we include the objects dealing with inversify (IOC) + // registration. These will be removed later. + legacyIOC: { + serviceManager: IServiceManager; + serviceContainer: IServiceContainer; + }; +}; + +/** + * The result of activating a component of the extension. + * + * Getting this value means the component has reached a state where it + * may be used by the rest of the extension. + * + * If the component started any non-critical activation-related + * operations during activation then the "fullyReady" property will only + * resolve once all those operations complete. + * + * The component may have also started long-running background helpers. + * Those are not exposed here. + */ +export type ActivationResult = { + fullyReady: Promise; +}; diff --git a/src/client/constants.ts b/src/client/constants.ts index 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 8c02a2571171..000000000000 --- a/src/client/datascience/cellFactory.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../common/extensions'; - -import * as uuid from 'uuid/v4'; -import { Range, TextDocument } from 'vscode'; - -import { noop } from '../../test/core'; -import { IDataScienceSettings } from '../common/types'; -import { CellMatcher } from './cellMatcher'; -import { appendLineFeed, generateMarkdownFromCodeLines, parseForComments } from './common'; -import { CellState, ICell } from './types'; - -function generateCodeCell(code: string[], file: string, line: number, id: string) : 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: id, - file: file, - line: line, - state: CellState.init, - type: 'execute' - }; - -} - -function generateMarkdownCell(code: string[], file: string, line: number, id: string) : ICell { - return { - id: id, - file: file, - line: line, - state: CellState.finished, - type: 'execute', - data: { - cell_type: 'markdown', - source: generateMarkdownFromCodeLines(code), - metadata: {} - } - }; - -} - -export function generateCells(settings: IDataScienceSettings | undefined, code: string, file: string, line: number, splitMarkdown: boolean, id: string) : ICell[] { - // Determine if we have a markdown cell/ markdown and code cell combined/ or just a code cell - const split = code.splitLines({trim: false}); - const firstLine = split[0]; - const matcher = new CellMatcher(settings); - if (matcher.isMarkdown(firstLine)) { - // We have at least one markdown. We might have to split it if there any lines that don't begin - // with # or are inside a multiline comment - let firstNonMarkdown = -1; - parseForComments(split, (_s, _i) => noop(), (s, i) => { - // Make sure there's actually some code. - if (s && s.length > 0 && firstNonMarkdown === -1) { - firstNonMarkdown = splitMarkdown ? i : -1; - } - }); - if (firstNonMarkdown >= 0) { - // Make sure if we split, the second cell has a new id. It's a new submission. - return [ - generateMarkdownCell(split.slice(0, firstNonMarkdown), file, line, id), - generateCodeCell(split.slice(firstNonMarkdown), file, line + firstNonMarkdown, uuid()) - ]; - } else { - // Just a single markdown cell - return [generateMarkdownCell(split, file, line, id)]; - } - } else { - // Just code - return [generateCodeCell(split, file, line, id)]; - } -} - -export function hasCells(document: TextDocument, settings?: IDataScienceSettings) : boolean { - const matcher = new CellMatcher(settings); - for (let index = 0; index < document.lineCount; index += 1) { - const line = document.lineAt(index); - if (matcher.isCell(line.text)) { - return true; - } - } - - return false; -} - -export function generateCellRanges(document: TextDocument, settings?: IDataScienceSettings) : {range: Range; title: string}[] { - // Implmentation of getCells here based on Don's Jupyter extension work - const matcher = new CellMatcher(settings); - const cells : {range: Range; title: string}[] = []; - for (let index = 0; index < document.lineCount; index += 1) { - const line = document.lineAt(index); - if (matcher.isCell(line.text)) { - - if (cells.length > 0) { - const previousCell = cells[cells.length - 1]; - previousCell.range = new Range(previousCell.range.start, document.lineAt(index - 1).range.end); - } - - const results = matcher.exec(line.text); - if (results !== undefined) { - cells.push({ - range: line.range, - title: results - }); - } - } - } - - if (cells.length >= 1) { - const line = document.lineAt(document.lineCount - 1); - const previousCell = cells[cells.length - 1]; - previousCell.range = new Range(previousCell.range.start, line.range.end); - } - - return cells; -} - -export function generateCellsFromDocument(document: TextDocument, settings?: IDataScienceSettings) : ICell[] { - // Get our ranges. They'll determine our cells - const ranges = generateCellRanges(document, settings); - - // 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(settings, code, document.fileName, r.range.start.line, false, uuid()); - })); -} diff --git a/src/client/datascience/cellMatcher.ts b/src/client/datascience/cellMatcher.ts deleted file mode 100644 index fdf8999f140e..000000000000 --- a/src/client/datascience/cellMatcher.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../common/extensions'; - -import { IDataScienceSettings } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { RegExpValues } from './constants'; - -export class CellMatcher { - private codeMatchRegEx : RegExp; - private markdownMatchRegEx : RegExp; - private codeExecRegEx: RegExp; - private markdownExecRegEx : RegExp; - - constructor(settings?: IDataScienceSettings) { - this.codeMatchRegEx = this.createRegExp(settings ? settings.codeRegularExpression : undefined, RegExpValues.PythonCellMarker); - this.markdownMatchRegEx = this.createRegExp(settings ? settings.markdownRegularExpression : undefined, RegExpValues.PythonMarkdownCellMarker); - this.codeExecRegEx = new RegExp(`${this.codeMatchRegEx.source}(.*)`); - this.markdownExecRegEx = new RegExp(`${this.markdownMatchRegEx.source}(.*)`); - } - - public isCell(code: string) : boolean { - return this.codeMatchRegEx.test(code) || this.markdownMatchRegEx.test(code); - } - - public isMarkdown(code: string) : boolean { - return this.markdownMatchRegEx.test(code); - } - - public isCode(code: string) : boolean { - return this.codeMatchRegEx.test(code); - } - - public stripMarkers(code: string) : string { - const lines = code.splitLines({trim: false, removeEmptyEntries: false}); - return lines.filter(l => !this.isCode(l) && !this.isMarkdown(l)).join('\n'); - } - - public exec(code: string) : string | undefined { - let result: RegExpExecArray | null = null; - if (this.codeMatchRegEx.test(code)) { - this.codeExecRegEx.lastIndex = -1; - result = this.codeExecRegEx.exec(code); - } else if (this.markdownMatchRegEx.test(code)) { - this.markdownExecRegEx.lastIndex = -1; - result = this.markdownExecRegEx.exec(code); - } - if (result) { - return result.length > 1 ? result[result.length - 1].trim() : ''; - } - return undefined; - } - - private createRegExp(potential: string | undefined, backup: RegExp) : RegExp { - try { - if (potential) { - return new RegExp(potential); - } - } catch { - noop(); - } - - return backup; - } -} diff --git a/src/client/datascience/codeCssGenerator.ts b/src/client/datascience/codeCssGenerator.ts deleted file mode 100644 index f61e2b179f9f..000000000000 --- a/src/client/datascience/codeCssGenerator.ts +++ /dev/null @@ -1,478 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { JSONArray, JSONObject } from '@phosphor/coreutils'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as path from 'path'; -import * as stripJsonComments from 'strip-json-comments'; - -import { IWorkspaceService } from '../common/application/types'; -import { IConfigurationService, ILogger } from '../common/types'; -import { DefaultTheme } from './constants'; -import { ICodeCssGenerator, IThemeFinder } from './types'; - -// tslint:disable:no-any -const DarkTheme = 'dark'; -const LightTheme = 'light'; - -const MonacoColorRegEx = /^#?([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$/; -const ThreeColorRegEx = /^#?([0-9A-Fa-f])([0-9A-Fa-f])([0-9A-Fa-f])$/; - -// These are based on the colors generated by 'Default Light+' and are only set when we -// are ignoring themes. -//tslint:disable:no-multiline-string object-literal-key-quotes -const DefaultCssVars: { [key: string] : string } = { - 'light' : ` - :root { - --override-widget-background: #f3f3f3; - --override-foreground: #000000; - --override-background: #FFFFFF; - --override-selection-background: #add6ff; - --override-watermark-color: rgba(66, 66, 66, 0.75); - --override-tabs-background: #f3f3f3; - --override-progress-background: #0066bf; - --override-badge-background: #c4c4c4; - --override-lineHighlightBorder: #eeeeee; - --override-peek-background: #f2f8fc; - } -`, - 'dark' : ` - :root { - --override-widget-background: #1e1e1e; - --override-foreground: #d4d4d4; - --override-background: #1e1e1e; - --override-selection-background: #264f78; - --override-watermark-color: rgba(231, 231, 231, 0.6); - --override-tabs-background: #252526; - --override-progress-background: #0066bf; - --override-badge-background: #4d4d4d; - --override-lineHighlightBorder: #282828; - --override-peek-background: #001f33; - } -` -}; - -// These colors below should match colors that come from either the Default Light+ theme or the Default Dark+ theme. -// They are used when we can't find a theme json file. -const DefaultColors: { [key: string] : string } = { - 'light.comment' : '#008000', - 'light.constant.numeric': '#09885a', - 'light.string' : '#a31515', - 'light.keyword.control' : '#AF00DB', - 'light.keyword.operator': '#000000', - 'light.variable' : '#001080', - 'light.entity.name.type': '#267f99', - 'light.support.function': '#795E26', - 'light.punctuation' : '#000000', - 'dark.comment' : '#6A9955', - 'dark.constant.numeric' : '#b5cea8', - 'dark.string' : '#ce9178', - 'dark.keyword.control' : '#C586C0', - 'dark.keyword.operator' : '#d4d4d4', - 'dark.variable' : '#9CDCFE', - 'dark.entity.name.type' : '#4EC9B0', - 'dark.support.function' : '#DCDCAA', - 'dark.punctuation' : '#1e1e1e' -}; - -interface IApplyThemeArgs { - tokenColors?: JSONArray | null; - baseColors?: JSONObject | null; - fontFamily: string; - fontSize: number; - isDark: boolean; - defaultStyle: string | undefined; -} - -// This class generates css using the current theme in order to colorize code. -// -// NOTE: This is all a big hack. It's relying on the theme json files to have a certain format -// in order for this to work. -// See this vscode issue for the real way we think this should happen: -// https://github.com/Microsoft/vscode/issues/32813 -@injectable() -export class CodeCssGenerator implements ICodeCssGenerator { - constructor( - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IThemeFinder) private themeFinder: IThemeFinder, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(ILogger) private logger: ILogger) { - } - - public generateThemeCss(isDark: boolean, theme: string): Promise { - return this.applyThemeData(isDark, theme, '', this.generateCss.bind(this)); - } - - public generateMonacoTheme(isDark: boolean, theme: string) : Promise { - return this.applyThemeData(isDark, theme, {}, this.generateMonacoThemeObject.bind(this)); - } - - private async applyThemeData(isDark: boolean, theme: string, defaultT: T, applier: (args: IApplyThemeArgs) => T) : Promise { - let result = defaultT; - try { - // First compute our current theme. - const ignoreTheme = this.configService.getSettings().datascience.ignoreVscodeTheme ? true : false; - theme = ignoreTheme ? DefaultTheme : theme; - const editor = this.workspaceService.getConfiguration('editor', undefined); - const fontFamily = editor ? editor.get('fontFamily', 'Consolas, \'Courier New\', monospace') : 'Consolas, \'Courier New\', monospace'; - const fontSize = editor ? editor.get('fontSize', 14) : 14; - const isDarkUpdated = ignoreTheme ? false : isDark; - - // Then we have to find where the theme resources are loaded from - if (theme) { - this.logger.logInformation('Searching for token colors ...'); - const tokenColors = await this.findTokenColors(theme); - const baseColors = await this.findBaseColors(theme); - - // The tokens object then contains the necessary data to generate our css - if (tokenColors && fontFamily && fontSize) { - this.logger.logInformation('Using colors to generate CSS ...'); - result = applier({ tokenColors, baseColors, fontFamily, fontSize, isDark: isDarkUpdated, defaultStyle: ignoreTheme ? LightTheme : undefined }); - } else if (tokenColors === null && fontFamily && fontSize) { - // No colors found. See if we can figure out what type of theme we have - const style = isDark ? DarkTheme : LightTheme ; - result = applier({ fontFamily, fontSize, isDark: isDarkUpdated, defaultStyle: style}); - } - } - } catch (err) { - // On error don't fail, just log - this.logger.logError(err); - } - - return result; - } - - private getScopes(entry: any) : JSONArray { - if (entry && entry.scope) { - return Array.isArray(entry.scope) ? entry.scope as JSONArray : entry.scope.toString().split(','); - } - return []; - } - - private matchTokenColor(tokenColors: JSONArray, scope: string) : number { - return tokenColors.findIndex((entry: any) => { - const scopeArray = this.getScopes(entry); - if (scopeArray.find(v => v !== null && v !== undefined && v.toString().trim() === scope)) { - return true; - } - return false; - }); - } - - private getScopeStyle = (tokenColors: JSONArray | null | undefined, scope: string, secondary: string, defaultStyle: string | undefined): { color: string; fontStyle: string } => { - // Search through the scopes on the json object - if (tokenColors) { - let match = this.matchTokenColor(tokenColors, scope); - if (match < 0 && secondary) { - match = this.matchTokenColor(tokenColors, secondary); - } - const found = match >= 0 ? tokenColors[match] as any : null; - if (found !== null) { - const settings = found.settings; - if (settings && settings !== null) { - const fontStyle = settings.fontStyle ? settings.fontStyle : 'normal'; - const foreground = settings.foreground ? settings.foreground : 'var(--vscode-editor-foreground)'; - - return { fontStyle, color: foreground }; - } - } - } - - // Default to editor foreground - return { color: this.getDefaultColor(defaultStyle, scope), fontStyle: 'normal' }; - } - - private getDefaultColor(style: string | undefined, scope: string) : string { - return style ? DefaultColors[`${style}.${scope}`] : 'var(--override-foreground, var(--vscode-editor-foreground))'; - } - - // tslint:disable-next-line:max-func-body-length - private generateCss(args: IApplyThemeArgs): string { - - // There's a set of values that need to be found - const commentStyle = this.getScopeStyle(args.tokenColors, 'comment', 'comment', args.defaultStyle); - const numericStyle = this.getScopeStyle(args.tokenColors, 'constant.numeric', 'constant', args.defaultStyle); - const stringStyle = this.getScopeStyle(args.tokenColors, 'string', 'string', args.defaultStyle); - const variableStyle = this.getScopeStyle(args.tokenColors, 'variable', 'variable', args.defaultStyle); - const entityTypeStyle = this.getScopeStyle(args.tokenColors, 'entity.name.type', 'entity.name.type', args.defaultStyle); - - // Use these values to fill in our format string - return ` - :root { - --code-comment-color: ${commentStyle.color}; - --code-numeric-color: ${numericStyle.color}; - --code-string-color: ${stringStyle.color}; - --code-variable-color: ${variableStyle.color}; - --code-type-color: ${entityTypeStyle.color}; - --code-font-family: ${args.fontFamily}; - --code-font-size: ${args.fontSize}px; - } - - ${args.defaultStyle ? DefaultCssVars[args.defaultStyle] : undefined } -`; - } - - // Based on this data here: - // https://github.com/Microsoft/vscode/blob/master/src/vs/editor/standalone/common/themes.ts#L13 - // tslint:disable: max-func-body-length - private generateMonacoThemeObject(args: IApplyThemeArgs) : monacoEditor.editor.IStandaloneThemeData { - const result: monacoEditor.editor.IStandaloneThemeData = { - base: args.isDark ? 'vs-dark' : 'vs', - inherit: false, - rules: [], - colors: {} - }; - // If we have token colors enumerate them and add them into the rules - if (args.tokenColors && args.tokenColors.length) { - const tokenSet = new Set(); - args.tokenColors.forEach((t: any) => { - const scopes = this.getScopes(t); - const settings = t && t.settings ? t.settings : undefined; - if (scopes && settings) { - scopes.forEach(s => { - const token = s ? s.toString() : ''; - if (!tokenSet.has(token)) { - tokenSet.add(token); - - if (settings.foreground) { - // Make sure matches the monaco requirements of having 6 values - if (!MonacoColorRegEx.test(settings.foreground)) { - const match = ThreeColorRegEx.exec(settings.foreground); - if (match && match.length > 3) { - settings.foreground = `#${match[1]}${match[1]}${match[2]}${match[2]}${match[3]}${match[3]}`; - } else { - settings.foreground = undefined; - } - } - } - - if (settings.foreground) { - result.rules.push({ - token, - foreground: settings.foreground, - background: settings.background, - fontStyle: settings.fontStyle - }); - } else { - result.rules.push({ - token, - background: settings.background, - fontStyle: settings.fontStyle - }); - } - - // Special case some items. punctuation.definition.comment doesn't seem to - // be listed anywhere. Add it manually when we find a 'comment' - // tslint:disable-next-line: possible-timing-attack - if (token === 'comment') { - result.rules.push({ - token: 'punctuation.definition.comment', - foreground: settings.foreground, - background: settings.background, - fontStyle: settings.fontStyle - }); - } - - // Same for string - // tslint:disable-next-line: possible-timing-attack - if (token === 'string') { - result.rules.push({ - token: 'punctuation.definition.string', - foreground: settings.foreground, - background: settings.background, - fontStyle: settings.fontStyle - }); - } - } - }); - } - }); - - result.rules = result.rules.sort((a: monacoEditor.editor.ITokenThemeRule, b: monacoEditor.editor.ITokenThemeRule) => { - return a.token.localeCompare(b.token); - }); - } else { - // Otherwise use our default values. - result.base = args.defaultStyle === DarkTheme ? 'vs-dark' : 'vs'; - result.inherit = true; - - if (args.defaultStyle) { - // Special case. We need rules for the comment beginning and the string beginning - result.rules.push({ - token: 'punctuation.definition.comment', - foreground: DefaultColors[`${args.defaultStyle}.comment`] - }); - result.rules.push({ - token: 'punctuation.definition.string', - foreground: DefaultColors[`${args.defaultStyle}.string`] - }); - } - } - // If we have base colors enumerate them and add them to the colors - if (args.baseColors) { - const keys = Object.keys(args.baseColors); - keys.forEach(k => { - const color = args.baseColors && args.baseColors[k] ? args.baseColors[k] : '#000000'; - result.colors[k] = color ? color.toString() : '#000000'; - }); - } // The else case here should end up inheriting. - return result; - } - - private mergeColors = (colors1: JSONArray, colors2: JSONArray): JSONArray => { - return [...colors1, ...colors2]; - } - - private mergeBaseColors = (colors1: JSONObject, colors2: JSONObject) : JSONObject => { - return {...colors1, ...colors2}; - } - - private readTokenColors = async (themeFile: string): Promise => { - const tokenContent = await fs.readFile(themeFile, 'utf8'); - const theme = JSON.parse(stripJsonComments(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) { - const includePath = path.join(path.dirname(themeFile), include.toString()); - const includedColors = await this.readTokenColors(includePath); - return this.mergeColors(tokenColors, includedColors); - } - - // Theme is a root, don't need to include others - return tokenColors; - } - - // Might also have a 'settings' object that equates to token colors - const settings = theme.settings as JSONArray; - if (settings && settings.length > 0) { - return settings; - } - - return []; - } - - private readBaseColors = async (themeFile: string): Promise => { - const tokenContent = await fs.readFile(themeFile, 'utf8'); - const theme = JSON.parse(stripJsonComments(tokenContent)) as JSONObject; - const colors = theme.colors as JSONObject; - - // This theme may include others. If so we need to combine the two together - const include = theme ? theme.include : undefined; - if (include) { - const includePath = path.join(path.dirname(themeFile), include.toString()); - const includedColors = await this.readBaseColors(includePath); - return this.mergeBaseColors(colors, includedColors); - } - - // Theme is a root, don't need to include others - return colors; - } - - private findTokenColors = async (theme: string): Promise => { - - try { - this.logger.logInformation('Attempting search for colors ...'); - const themeRoot = await this.themeFinder.findThemeRootJson(theme); - - // Use the first result if we have one - if (themeRoot) { - this.logger.logInformation(`Loading colors from ${themeRoot} ...`); - - // This should be the path to the file. Load it as a json object - const contents = await fs.readFile(themeRoot, 'utf8'); - const json = JSON.parse(stripJsonComments(contents)) as JSONObject; - - // There should be a theme colors section - const contributes = json.contributes as JSONObject; - - // If no contributes section, see if we have a tokenColors section. This means - // this is a direct token colors file - if (!contributes) { - const tokenColors = json.tokenColors as JSONObject; - if (tokenColors) { - return await this.readTokenColors(themeRoot); - } - } - - // This should have a themes section - const themes = contributes.themes as JSONArray; - - // One of these (it's an array), should have our matching theme entry - const index = themes.findIndex((e: any) => { - return e !== null && (e.id === theme || e.name === theme); - }); - - const found = index >= 0 ? themes[index] as any : null; - if (found !== null) { - // Then the path entry should contain a relative path to the json file with - // the tokens in it - const themeFile = path.join(path.dirname(themeRoot), found.path); - this.logger.logInformation(`Reading colors from ${themeFile}`); - return await this.readTokenColors(themeFile); - } - } else { - this.logger.logWarning(`Color theme ${theme} not found. Using default colors.`); - } - } catch (err) { - // Swallow any exceptions with searching or parsing - this.logger.logError(err); - } - - // Force the colors to the defaults - return null; - } - - private findBaseColors = async (theme: string): Promise => { - try { - this.logger.logInformation('Attempting search for colors ...'); - const themeRoot = await this.themeFinder.findThemeRootJson(theme); - - // Use the first result if we have one - if (themeRoot) { - this.logger.logInformation(`Loading base colors from ${themeRoot} ...`); - - // This should be the path to the file. Load it as a json object - const contents = await fs.readFile(themeRoot, 'utf8'); - const json = JSON.parse(stripJsonComments(contents)) as JSONObject; - - // There should be a theme colors section - const contributes = json.contributes as JSONObject; - - // If no contributes section, see if we have a tokenColors section. This means - // this is a direct token colors file - if (!contributes) { - return await this.readBaseColors(themeRoot); - } - - // This should have a themes section - const themes = contributes.themes as JSONArray; - - // One of these (it's an array), should have our matching theme entry - const index = themes.findIndex((e: any) => { - return e !== null && (e.id === theme || e.name === theme); - }); - - const found = index >= 0 ? themes[index] as any : null; - if (found !== null) { - // Then the path entry should contain a relative path to the json file with - // the tokens in it - const themeFile = path.join(path.dirname(themeRoot), found.path); - this.logger.logInformation(`Reading base colors from ${themeFile}`); - return await this.readBaseColors(themeFile); - } - } else { - this.logger.logWarning(`Color theme ${theme} not found. Using default colors.`); - } - } catch (err) { - // Swallow any exceptions with searching or parsing - this.logger.logError(err); - } - - // Force the colors to the defaults - return null; - } -} diff --git a/src/client/datascience/common.ts b/src/client/datascience/common.ts deleted file mode 100644 index 384eaab79aff..000000000000 --- a/src/client/datascience/common.ts +++ /dev/null @@ -1,152 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils/lib/nbformat'; - -import { noop } from '../../test/core'; - -const SingleQuoteMultiline = '\'\'\''; -const DoubleQuoteMultiline = '\"\"\"'; -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 splitMultilineString(str: nbformat.MultilineString): string[] { - if (Array.isArray(str)) { - return str as string[]; - } - return str.toString().split('\n'); -} - -// Strip out comment lines from code -export function stripComments(str: string): string { - let result: string = ''; - parseForComments( - str.splitLines({trim: false, removeEmptyEntries: false}), - (_s) => noop, - (s) => result = result.concat(`${s}\n`)); - return result; -} - -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; -} - -export function appendLineFeed(arr: string[], modifier?: (s: string) => string) { - return arr.map((s: string, i: number) => { - const out = modifier ? modifier(s) : s; - return i === arr.length - 1 ? `${out}` : `${out}\n`; - }); -} - -export function generateMarkdownFromCodeLines(lines: string[]) { - // Generate markdown by stripping out the comments and markdown header - return appendLineFeed(extractComments(lines.slice(1))); -} - -// tslint:disable-next-line: cyclomatic-complexity -export function parseForComments( - lines: string[], - foundCommentLine: (s: string, i: number) => void, - foundNonCommentLine: (s: string, i: number) => void) { - // Check for either multiline or single line comments - let insideMultilineComment: string | undefined ; - let insideMultilineQuote: string | undefined; - let pos = 0; - for (const l of lines) { - const trim = l.trim(); - // Multiline is triple quotes of either kind - const isMultilineComment = trim.startsWith(SingleQuoteMultiline) ? - SingleQuoteMultiline : trim.startsWith(DoubleQuoteMultiline) ? DoubleQuoteMultiline : undefined; - const isMultilineQuote = trim.includes(SingleQuoteMultiline) ? - SingleQuoteMultiline : trim.includes(DoubleQuoteMultiline) ? DoubleQuoteMultiline : undefined; - - // Check for ending quotes of multiline string - if (insideMultilineQuote) { - if (insideMultilineQuote === isMultilineQuote) { - insideMultilineQuote = undefined; - } - foundNonCommentLine(l, pos); - // Not inside quote, see if inside a comment - } else if (insideMultilineComment) { - if (insideMultilineComment === isMultilineComment) { - insideMultilineComment = undefined; - } - if (insideMultilineComment) { - foundCommentLine(l, pos); - } - // Not inside either, see if starting a quote - } else if (isMultilineQuote && !isMultilineComment) { - // Make sure doesn't begin and end on the same line. - const beginQuote = trim.indexOf(isMultilineQuote); - const endQuote = trim.lastIndexOf(isMultilineQuote); - insideMultilineQuote = endQuote !== beginQuote ? undefined : isMultilineQuote; - foundNonCommentLine(l, pos); - // Not starting a quote, might be starting a comment - } else if (isMultilineComment) { - // See if this line ends the comment too or not - const endIndex = trim.indexOf(isMultilineComment, 3); - insideMultilineComment = endIndex >= 0 ? undefined : isMultilineComment; - - // Might end with text too - if (trim.length > 3) { - foundCommentLine(trim.slice(3, endIndex >= 0 ? endIndex : undefined), pos); - } - } else { - // Normal line - if (trim.startsWith('#')) { - foundCommentLine(trim.slice(1), pos); - } else { - foundNonCommentLine(l, pos); - } - } - pos += 1; - } -} - -function extractComments(lines: string[]): string[] { - const result: string[] = []; - parseForComments(lines, (s) => result.push(s), (_s) => noop()); - return result; -} diff --git a/src/client/datascience/constants.ts b/src/client/datascience/constants.ts deleted file mode 100644 index 19d5113227c0..000000000000 --- a/src/client/datascience/constants.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { IS_WINDOWS } from '../common/platform/constants'; - -export const DefaultTheme = 'Default Light+'; - -export namespace Commands { - export const RunAllCells = 'python.datascience.runallcells'; - export const RunAllCellsAbove = 'python.datascience.runallcellsabove'; - export const RunCellAndAllBelow = 'python.datascience.runcellandallbelow'; - export const RunAllCellsAbovePalette = 'python.datascience.runallcellsabove.palette'; - export const RunCellAndAllBelowPalette = 'python.datascience.runcurrentcellandallbelow.palette'; - export const RunToLine = 'python.datascience.runtoline'; - export const RunFromLine = 'python.datascience.runfromline'; - export const RunCell = 'python.datascience.runcell'; - export const RunCurrentCell = 'python.datascience.runcurrentcell'; - export const RunCurrentCellAdvance = 'python.datascience.runcurrentcelladvance'; - export const 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 const ExecSelectionInInteractiveWindow = 'python.datascience.execSelectionInteractive'; - export const RunFileInInteractiveWindows = 'python.datascience.runFileInteractive'; - export const AddCellBelow = 'python.datascience.addcellbelow'; - export const DebugCurrentCellPalette = 'python.datascience.debugcurrentcell.palette'; - export const DebugCell = 'python.datascience.debugcell'; -} - -export namespace EditorContexts { - export const HasCodeCells = 'python.datascience.hascodecells'; - export const DataScienceEnabled = 'python.datascience.featureenabled'; - export const HaveInteractiveCells = 'python.datascience.haveinteractivecells'; - export const HaveRedoableCells = 'python.datascience.haveredoablecells'; - export const HaveInteractive = 'python.datascience.haveinteractive'; - export const OwnsSelection = 'python.datascience.ownsSelection'; -} - -export namespace RegExpValues { - export const PythonCellMarker = /^(#\s*%%|#\s*\|#\s*In\[\d*?\]|#\s*In\[ \])/; - export const PythonMarkdownCellMarker = /^(#\s*%%\s*\[markdown\]|#\s*\)/; - export const CheckJupyterRegEx = IS_WINDOWS ? /^jupyter?\.exe$/ : /^jupyter?$/; - export const PyKernelOutputRegEx = /.*\s+(.+)$/m; - export const KernelSpecOutputRegEx = /^\s*(\S+)\s+(\S+)$/; - // This next one has to be a string because uglifyJS isn't handling the groups. We use named-js-regexp to parse it - // instead. - export const UrlPatternRegEx = '(?https?:\\/\\/)((\\(.+\\s+or\\s+(?.+)\\))|(?[^\\s]+))(?:.+)' ; - export interface IUrlPatternGroupType { - LOCAL: string | undefined; - PREFIX: string | undefined; - REST: string | undefined; - IP: string | undefined; - } - export const HttpPattern = /https?:\/\//; - export const ExtractPortRegex = /https?:\/\/[^\s]+:(\d+)[^\s]+/; - export const ConvertToRemoteUri = /(https?:\/\/)([^\s])+(:\d+[^\s]*)/; - export const ParamsExractorRegEx = /\S+\((.*)\)\s*{/; - export const ArgsSplitterRegEx = /([^\s,]+)/; - export const ShapeSplitterRegEx = /.*,\s*(\d+).*/; - export const SvgHeightRegex = /(\/m; - -} - -export enum Telemetry { - ImportNotebook = 'DATASCIENCE.IMPORT_NOTEBOOK', - RunCell = 'DATASCIENCE.RUN_CELL', - RunCurrentCell = 'DATASCIENCE.RUN_CURRENT_CELL', - RunCurrentCellAndAdvance = 'DATASCIENCE.RUN_CURRENT_CELL_AND_ADVANCE', - RunAllCells = 'DATASCIENCE.RUN_ALL_CELLS', - RunAllCellsAbove = 'DATASCIENCE.RUN_ALL_CELLS_ABOVE', - RunCellAndAllBelow = 'DATASCIENCE.RUN_CELL_AND_ALL_BELOW', - RunSelectionOrLine = 'DATASCIENCE.RUN_SELECTION_OR_LINE', - RunToLine = 'DATASCIENCE.RUN_TO_LINE', - RunFromLine = 'DATASCIENCE.RUN_FROM_LINE', - DeleteAllCells = 'DATASCIENCE.DELETE_ALL_CELLS', - DeleteCell = 'DATASCIENCE.DELETE_CELL', - GotoSourceCode = 'DATASCIENCE.GOTO_SOURCE', - CopySourceCode = 'DATASCIENCE.COPY_SOURCE', - RestartKernel = 'DATASCIENCE.RESTART_KERNEL', - ExportNotebook = 'DATASCIENCE.EXPORT_NOTEBOOK', - Undo = 'DATASCIENCE.UNDO', - Redo = 'DATASCIENCE.REDO', - ShowHistoryPane = 'DATASCIENCE.SHOW_HISTORY_PANE', - ExpandAll = 'DATASCIENCE.EXPAND_ALL', - CollapseAll = 'DATASCIENCE.COLLAPSE_ALL', - SelectJupyterURI = 'DATASCIENCE.SELECT_JUPYTER_URI', - SetJupyterURIToLocal = 'DATASCIENCE.SET_JUPYTER_URI_LOCAL', - SetJupyterURIToUserSpecified = 'DATASCIENCE.SET_JUPYTER_URI_USER_SPECIFIED', - Interrupt = 'DATASCIENCE.INTERRUPT', - ExportPythonFile = 'DATASCIENCE.EXPORT_PYTHON_FILE', - ExportPythonFileAndOutput = 'DATASCIENCE.EXPORT_PYTHON_FILE_AND_OUTPUT', - StartJupyter = 'DATASCIENCE.JUPYTERSTARTUPCOST', - SubmitCellThroughInput = 'DATASCIENCE.SUBMITCELLFROMREPL', - ConnectLocalJupyter = 'DATASCIENCE.CONNECTLOCALJUPYTER', - ConnectRemoteJupyter = 'DATASCIENCE.CONNECTREMOTEJUPYTER', - ConnectFailedJupyter = 'DATASCIENCE.CONNECTFAILEDJUPYTER', - ConnectRemoteFailedJupyter = 'DATASCIENCE.CONNECTREMOTEFAILEDJUPYTER', - ConnectRemoteSelfCertFailedJupyter = 'DATASCIENCE.CONNECTREMOTESELFCERTFAILEDJUPYTER', - SelfCertsMessageEnabled = 'DATASCIENCE.SELFCERTSMESSAGEENABLED', - SelfCertsMessageClose = 'DATASCIENCE.SELFCERTSMESSAGECLOSE', - RemoteAddCode = 'DATASCIENCE.LIVESHARE.ADDCODE', - ShiftEnterBannerShown = 'DATASCIENCE.SHIFTENTER_BANNER_SHOWN', - EnableInteractiveShiftEnter = 'DATASCIENCE.ENABLE_INTERACTIVE_SHIFT_ENTER', - DisableInteractiveShiftEnter = 'DATASCIENCE.DISABLE_INTERACTIVE_SHIFT_ENTER', - ShowDataViewer = 'DATASCIENCE.SHOW_DATA_EXPLORER', - RunFileInteractive = 'DATASCIENCE.RUN_FILE_INTERACTIVE', - PandasNotInstalled = 'DATASCIENCE.SHOW_DATA_NO_PANDAS', - PandasTooOld = 'DATASCIENCE.SHOW_DATA_PANDAS_TOO_OLD', - DataScienceSettings = 'DATASCIENCE.SETTINGS', - VariableExplorerToggled = 'DATASCIENCE.VARIABLE_EXPLORER_TOGGLE', - VariableExplorerVariableCount = 'DATASCIENCE.VARIABLE_EXPLORER_VARIABLE_COUNT', - AddCellBelow = 'DATASCIENCE.ADD_CELL_BELOW', - GetPasswordAttempt = 'DATASCIENCE.GET_PASSWORD_ATTEMPT', - GetPasswordFailure = 'DATASCIENCE.GET_PASSWORD_FAILURE', - GetPasswordSuccess = 'DATASCIENCE.GET_PASSWORD_SUCCESS', - OpenPlotViewer = 'DATASCIENCE.OPEN_PLOT_VIEWER', - DebugCurrentCell = 'DATASCIENCE.DEBUG_CURRENT_CELL', - CodeLensAverageAcquisitionTime = 'DATASCIENCE.CODE_LENS_ACQ_TIME', - ClassConstructionTime = 'DATASCIENCE.CLASS_CONSTRUCTION_TIME', - FindJupyterCommand = 'DATASCIENCE.FIND_JUPYTER_COMMAND', - StartJupyterProcess = 'DATASCIENCE.START_JUPYTER_PROCESS', - WaitForIdleJupyter = 'DATASCIENCE.WAIT_FOR_IDLE_JUPYTER', - HiddenCellTime = 'DATASCIENCE.HIDDEN_EXECUTION_TIME', - RestartJupyterTime = 'DATASCIENCE.RESTART_JUPYTER_TIME', - InterruptJupyterTime = 'DATASCIENCE.INTERRUPT_JUPYTER_TIME', - ExecuteCell = 'DATASCIENCE.EXECUTE_CELL_TIME', - ExecuteCellPerceivedCold = 'DATASCIENCE.EXECUTE_CELL_PERCEIVED_COLD', - ExecuteCellPerceivedWarm = 'DATASCIENCE.EXECUTE_CELL_PERCEIVED_WARM', - WebviewStartup = 'DATASCIENCE.WEBVIEW_STARTUP', - VariableExplorerFetchTime = 'DATASCIENCE.VARIABLE_EXPLORER_FETCH_TIME', - WebviewStyleUpdate = 'DATASCIENCE.WEBVIEW_STYLE_UPDATE', - WebviewMonacoStyleUpdate = 'DATASCIENCE.WEBVIEW_MONACO_STYLE_UPDATE', - DataViewerFetchTime = 'DATASCIENCE.DATAVIEWER_FETCH_TIME', - FindJupyterKernelSpec = 'DATASCIENCE.FIND_JUPYTER_KERNEL_SPEC' - } - -export namespace HelpLinks { - export const PythonInteractiveHelpLink = 'https://aka.ms/pyaiinstall'; - export const JupyterDataRateHelpLink = 'https://aka.ms/AA5ggm0'; // This redirects here: https://jupyter-notebook.readthedocs.io/en/stable/config.html -} - -export namespace Settings { - export const JupyterServerLocalLaunch = 'local'; - export const IntellisenseTimeout = 300; -} - -export namespace Identifiers { - export const EmptyFileName = '2DB9B899-6519-4E1B-88B0-FA728A274115'; - export const GeneratedThemeName = 'ipython-theme'; // This needs to be all lower class and a valid class name. - export const HistoryPurpose = 'history'; - export const MatplotLibDefaultParams = '_VSCode_defaultMatplotlib_Params'; - export const EditCellId = '3D3AB152-ADC1-4501-B813-4B83B49B0C10'; - export const SvgSizeTag = 'sizeTag={{0}, {1}}'; -} - -export namespace CodeSnippits { - export const ChangeDirectory = ['{0}', '{1}', 'import os', 'try:', '\tos.chdir(os.path.join(os.getcwd(), \'{2}\'))', '\tprint(os.getcwd())', 'except:', '\tpass', '']; - export const ChangeDirectoryCommentIdentifier = '# ms-python.python added'; // Not translated so can compare. - export const MatplotLibInitSvg = `import matplotlib\n%matplotlib inline\n${Identifiers.MatplotLibDefaultParams} = dict(matplotlib.rcParams)\n%config InlineBackend.figure_format = 'svg'`; - export const MatplotLibInitPng = `import matplotlib\n%matplotlib inline\n${Identifiers.MatplotLibDefaultParams} = dict(matplotlib.rcParams)\n%config InlineBackend.figure_format = 'png'`; -} - -export namespace JupyterCommands { - export const NotebookCommand = 'notebook'; - export const ConvertCommand = 'nbconvert'; - export const KernelSpecCommand = 'kernelspec'; - export const KernelCreateCommand = 'ipykernel'; - -} - -export namespace LiveShare { - export const JupyterExecutionService = 'jupyterExecutionService'; - export const JupyterServerSharedService = 'jupyterServerSharedService'; - export const CommandBrokerService = 'commmandBrokerService'; - export const WebPanelMessageService = 'webPanelMessageService'; - export const InteractiveWindowProviderService = 'interactiveWindowProviderService'; - export const GuestCheckerService = 'guestCheckerService'; - export const LiveShareBroadcastRequest = 'broadcastRequest'; - export const ResponseLifetime = 15000; - export const ResponseRange = 1000; // Range of time alloted to check if a response matches or not - export const InterruptDefaultTimeout = 10000; -} - -export namespace LiveShareCommands { - export const isNotebookSupported = 'isNotebookSupported'; - export const isImportSupported = 'isImportSupported'; - export const isKernelCreateSupported = 'isKernelCreateSupported'; - export const isKernelSpecSupported = 'isKernelSpecSupported'; - export const connectToNotebookServer = 'connectToNotebookServer'; - export const getUsableJupyterPython = 'getUsableJupyterPython'; - export const executeObservable = 'executeObservable'; - export const getSysInfo = 'getSysInfo'; - export const serverResponse = 'serverResponse'; - export const catchupRequest = 'catchupRequest'; - export const syncRequest = 'synchRequest'; - export const restart = 'restart'; - export const interrupt = 'interrupt'; - export const interactiveWindowCreate = 'interactiveWindowCreate'; - export const interactiveWindowCreateSync = 'interactiveWindowCreateSync'; - export const disposeServer = 'disposeServer'; - export const guestCheck = 'guestCheck'; -} diff --git a/src/client/datascience/data-viewing/dataViewer.ts b/src/client/datascience/data-viewing/dataViewer.ts deleted file mode 100644 index 3dcee41e4856..000000000000 --- a/src/client/datascience/data-viewing/dataViewer.ts +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ViewColumn } from 'vscode'; - -import { IApplicationShell, IWebPanelProvider, IWorkspaceService } from '../../common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { traceError } from '../../common/logger'; -import { IConfigurationService, IDisposable } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { sendTelemetryEvent } from '../../telemetry'; -import { HelpLinks, Telemetry } from '../constants'; -import { JupyterDataRateLimitError } from '../jupyter/jupyterDataRateLimitError'; -import { ICodeCssGenerator, IDataViewer, IJupyterVariable, IJupyterVariables, IThemeFinder } from '../types'; -import { WebViewHost } from '../webViewHost'; -import { DataViewerMessageListener } from './dataViewerMessageListener'; -import { DataViewerMessages, IDataViewerMapping, IGetRowsRequest } from './types'; - -@injectable() -export class DataViewer extends WebViewHost implements IDataViewer, IDisposable { - private disposed: boolean = false; - private variable : IJupyterVariable | undefined; - private rowsTimer: StopWatch | undefined; - private pendingRowsCount: number = 0; - - constructor( - @inject(IWebPanelProvider) provider: IWebPanelProvider, - @inject(IConfigurationService) configuration: IConfigurationService, - @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, - @inject(IThemeFinder) themeFinder: IThemeFinder, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IJupyterVariables) private variableManager: IJupyterVariables, - @inject(IApplicationShell) private applicationShell: IApplicationShell - ) { - super( - configuration, - provider, - cssGenerator, - themeFinder, - workspaceService, - (c, v, d) => new DataViewerMessageListener(c, v, d), - path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'data-explorer', 'index_bundle.js'), - localize.DataScience.dataExplorerTitle(), - ViewColumn.One); - } - - public async showVariable(variable: IJupyterVariable): Promise { - if (!this.disposed) { - // Fill in our variable's beginning data - this.variable = await this.prepVariable(variable); - - // Create our new title with the variable name - let newTitle = `${localize.DataScience.dataExplorerTitle()} - ${variable.name}`; - const TRIM_LENGTH = 40; - if (newTitle.length > TRIM_LENGTH) { - newTitle = `${newTitle.substr(0, TRIM_LENGTH)}...`; - } - - super.setTitle(newTitle); - - // Then show our web panel. Eventually we need to consume the data - await super.show(true); - - // Send a message with our data - this.postMessage(DataViewerMessages.InitializeData, this.variable).ignoreErrors(); - } - } - - //tslint:disable-next-line:no-any - protected onMessage(message: string, payload: any) { - switch (message) { - case DataViewerMessages.GetAllRowsRequest: - this.getAllRows().ignoreErrors(); - break; - - case DataViewerMessages.GetRowsRequest: - this.getRowChunk(payload as IGetRowsRequest).ignoreErrors(); - break; - - default: - break; - } - - super.onMessage(message, payload); - } - - private async prepVariable(variable: IJupyterVariable) : Promise { - this.rowsTimer = new StopWatch(); - const output = await this.variableManager.getDataFrameInfo(variable); - - // Log telemetry about number of rows - try { - sendTelemetryEvent(Telemetry.ShowDataViewer, 0, {rows: output.rowCount ? output.rowCount : 0, columns: output.columns ? output.columns.length : 0 }); - - // Count number of rows to fetch so can send telemetry on how long it took. - this.pendingRowsCount = output.rowCount ? output.rowCount : 0; - } catch { - noop(); - } - - return output; - } - - private async getAllRows() { - return this.wrapRequest(async () => { - if (this.variable && this.variable.rowCount) { - const allRows = await this.variableManager.getDataFrameRows(this.variable, 0, this.variable.rowCount); - this.pendingRowsCount = 0; - return this.postMessage(DataViewerMessages.GetAllRowsResponse, allRows); - } - }); - } - - private getRowChunk(request: IGetRowsRequest) { - return this.wrapRequest(async () => { - if (this.variable && this.variable.rowCount) { - const rows = await this.variableManager.getDataFrameRows(this.variable, request.start, Math.min(request.end, this.variable.rowCount)); - return this.postMessage(DataViewerMessages.GetRowsResponse, { rows, start: request.start, end: request.end }); - } - }); - } - - private async wrapRequest(func: () => Promise) { - try { - return await func(); - } catch (e) { - if (e instanceof JupyterDataRateLimitError) { - traceError(e); - const actionTitle = localize.DataScience.pythonInteractiveHelpLink(); - this.applicationShell.showErrorMessage(e.toString(), actionTitle).then(v => { - // User clicked on the link, open it. - if (v === actionTitle) { - this.applicationShell.openUrl(HelpLinks.JupyterDataRateHelpLink); - } - }); - this.dispose(); - } - traceError(e); - this.applicationShell.showErrorMessage(e); - } finally { - this.sendElapsedTimeTelemetry(); - } - } - - private sendElapsedTimeTelemetry() { - if (this.rowsTimer && this.pendingRowsCount === 0) { - sendTelemetryEvent(Telemetry.ShowDataViewer, this.rowsTimer.elapsedTime); - } - } -} diff --git a/src/client/datascience/data-viewing/dataViewerMessageListener.ts b/src/client/datascience/data-viewing/dataViewerMessageListener.ts deleted file mode 100644 index 89cfa82d800d..000000000000 --- a/src/client/datascience/data-viewing/dataViewerMessageListener.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { IWebPanel, IWebPanelMessageListener } from '../../common/application/types'; - -// tslint:disable:no-any - -// This class listens to messages that come from the local Data Explorer window -export class DataViewerMessageListener implements IWebPanelMessageListener { - private disposedCallback : () => void; - private callback : (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - - constructor(callback: (message: string, payload: any) => void, viewChanged: (panel: IWebPanel) => void, disposed: () => void) { - - // Save our dispose callback so we remove our interactive window - this.disposedCallback = disposed; - - // Save our local callback so we can handle the non broadcast case(s) - this.callback = callback; - - // Save view changed so we can forward view change events. - this.viewChanged = viewChanged; - } - - public async dispose() { - this.disposedCallback(); - } - - public onMessage(message: string, payload: any) { - // Send to just our local callback. - this.callback(message, payload); - } - - public onChangeViewState(panel: IWebPanel) { - // Forward this onto our callback - if (this.viewChanged) { - this.viewChanged(panel); - } - } -} diff --git a/src/client/datascience/data-viewing/dataViewerProvider.ts b/src/client/datascience/data-viewing/dataViewerProvider.ts deleted file mode 100644 index 4e6ead69183d..000000000000 --- a/src/client/datascience/data-viewing/dataViewerProvider.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; - -import { IPythonExecutionFactory } from '../../common/process/types'; -import { IAsyncDisposable, IAsyncDisposableRegistry } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { IInterpreterService } from '../../interpreter/contracts'; -import { IServiceContainer } from '../../ioc/types'; -import { IDataViewer, IDataViewerProvider, IJupyterVariables } from '../types'; - -@injectable() -export class DataViewerProvider implements IDataViewerProvider, IAsyncDisposable { - - private activeExplorers: IDataViewer[] = []; - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry, - @inject(IJupyterVariables) private variables: IJupyterVariables, - @inject(IPythonExecutionFactory) private pythonFactory : IPythonExecutionFactory, - @inject(IInterpreterService) private interpreterService: IInterpreterService - ) { - asyncRegistry.push(this); - } - - public async dispose() { - await Promise.all(this.activeExplorers.map(d => d.dispose())); - } - - public async create(variable: string) : Promise{ - // Make sure this is a valid variable - const variables = await this.variables.getVariables(); - const index = variables.findIndex(v => v && v.name === variable); - if (index >= 0) { - const dataExplorer = this.serviceContainer.get(IDataViewer); - this.activeExplorers.push(dataExplorer); - await dataExplorer.showVariable(variables[index]); - return dataExplorer; - } - - throw new Error(localize.DataScience.dataExplorerInvalidVariableFormat().format(variable)); - } - - public async getPandasVersion() : Promise<{major: number; minor: number; build: number} | undefined> { - const interpreter = await this.interpreterService.getActiveInterpreter(); - const launcher = await this.pythonFactory.createActivatedEnvironment({ resource: undefined, interpreter, allowEnvironmentFetchExceptions: true }); - try { - const result = await launcher.exec(['-c', 'import pandas;print(pandas.__version__)'], {throwOnStdErr: true}); - const versionMatch = /^\s*(\d+)\.(\d+)\.(\d+)\s*$/.exec(result.stdout); - if (versionMatch && versionMatch.length > 2) { - const major = parseInt(versionMatch[1], 10); - const minor = parseInt(versionMatch[2], 10); - const build = parseInt(versionMatch[3], 10); - return {major, minor, build}; - } - } catch { - noop(); - } - } -} diff --git a/src/client/datascience/data-viewing/types.ts b/src/client/datascience/data-viewing/types.ts deleted file mode 100644 index f847539eb01b..000000000000 --- a/src/client/datascience/data-viewing/types.ts +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { JSONObject } from '@phosphor/coreutils'; - -import { CssMessages, IGetCssRequest, IGetCssResponse, SharedMessages } from '../messages'; -import { IJupyterVariable } from '../types'; - -export const CellFetchAllLimit = 100000; -export const CellFetchSizeFirst = 100000; -export const CellFetchSizeSubsequent = 1000000; -export const MaxStringCompare = 200; -export const ColumnWarningSize = 1000; // Anything over this takes too long to load - -export namespace DataViewerRowStates { - export const Fetching = 'fetching'; - export const Skipped = 'skipped'; -} - -export namespace DataViewerMessages { - export const Started = SharedMessages.Started; - export const UpdateSettings = SharedMessages.UpdateSettings; - export const InitializeData = 'init'; - export const GetAllRowsRequest = 'get_all_rows_request'; - export const GetAllRowsResponse = 'get_all_rows_response'; - export const GetRowsRequest = 'get_rows_request'; - export const GetRowsResponse = 'get_rows_response'; - export const CompletedData = 'complete'; -} - -export interface IGetRowsRequest { - start: number; - end: number; -} - -export interface IGetRowsResponse { - rows: JSONObject; - start: number; - end: number; -} - -// Map all messages to specific payloads -export class IDataViewerMapping { - public [DataViewerMessages.Started]: never | undefined; - public [DataViewerMessages.UpdateSettings]: string; - public [DataViewerMessages.InitializeData]: IJupyterVariable; - public [DataViewerMessages.GetAllRowsRequest]: never | undefined; - public [DataViewerMessages.GetAllRowsResponse]: JSONObject; - public [DataViewerMessages.GetRowsRequest]: IGetRowsRequest; - public [DataViewerMessages.GetRowsResponse]: IGetRowsResponse; - public [DataViewerMessages.CompletedData]: never | undefined; - public [CssMessages.GetCssRequest] : IGetCssRequest; - public [CssMessages.GetCssResponse] : IGetCssResponse; -} diff --git a/src/client/datascience/dataScienceSurveyBanner.ts b/src/client/datascience/dataScienceSurveyBanner.ts deleted file mode 100644 index 96ae9379723f..000000000000 --- a/src/client/datascience/dataScienceSurveyBanner.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 { 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 enabled(): boolean { - return this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise { - 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 { - if (!this.enabled || this.disabledInCurrentSession) { - return false; - } - - if (!launchCounter) { - launchCounter = await this.getPythonDSCommandCounter(); - } - - return launchCounter >= this.commandThreshold; - } - - public async disable(): Promise { - await this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.ShowBanner, false).updateValue(false); - } - - public async launchSurvey(): Promise { - this.browserService.launch(this.surveyLink); - } - - private async getPythonDSCommandCounter(): Promise { - const state = this.persistentState.createGlobalPersistentState(DSSurveyStateKeys.ShowAttemptCounter, 0); - return state.value; - } - - private async incrementPythonDataScienceCommandCounter(): Promise { - const state = this.persistentState.createGlobalPersistentState(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 7bb597c49fad..000000000000 --- a/src/client/datascience/datascience.ts +++ /dev/null @@ -1,443 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../common/extensions'; - -import { JSONObject } from '@phosphor/coreutils'; -import { inject, injectable } from 'inversify'; -import { URL } from 'url'; -import * as vscode from 'vscode'; - -import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { PYTHON_ALLFILES, PYTHON_LANGUAGE } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { traceError } from '../common/logger'; -import { - BANNER_NAME_DS_SURVEY, - IConfigurationService, - IDisposable, - IDisposableRegistry, - IExtensionContext, - IPythonExtensionBanner -} from '../common/types'; -import { debounceAsync } from '../common/utils/decorators'; -import * as localize from '../common/utils/localize'; -import { IServiceContainer } from '../ioc/types'; -import { captureTelemetry, sendTelemetryEvent } 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; - private changeHandler: IDisposable | undefined; - private startTime: number = Date.now(); - 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, - @inject(IWorkspaceService) private workspace: IWorkspaceService - ) { - this.commandListeners = this.serviceContainer.getAll(IDataScienceCommandListener); - this.dataScienceSurveyBanner = this.serviceContainer.get(IPythonExtensionBanner, BANNER_NAME_DS_SURVEY); - } - - public get activationStartTime() : number { - return this.startTime; - } - - public async activate(): Promise { - this.registerCommands(); - - this.extensionContext.subscriptions.push( - vscode.languages.registerCodeLensProvider( - PYTHON_ALLFILES, this.dataScienceCodeLensProvider - ) - ); - - // Set our initial settings and sign up for changes - this.onSettingsChanged(); - this.changeHandler = this.configuration.getSettings().onDidChange(this.onSettingsChanged.bind(this)); - this.disposableRegistry.push(this); - - // Listen for active editor changes so we can detect have code cells or not - this.disposableRegistry.push(this.documentManager.onDidChangeActiveTextEditor(() => this.onChangedActiveTextEditor())); - this.onChangedActiveTextEditor(); - - // Send telemetry for all of our settings - this.sendSettingsTelemetry().ignoreErrors(); - } - - public async dispose() { - if (this.changeHandler) { - this.changeHandler.dispose(); - this.changeHandler = undefined; - } - } - - public async runFileInteractive(file: string): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - let codeWatcher = this.getCodeWatcher(file); - if (!codeWatcher) { - codeWatcher = this.getCurrentCodeWatcher(); - } - if (codeWatcher) { - return codeWatcher.runFileInteractive(); - } else { - return Promise.resolve(); - } - } - - public async runAllCells(file: string): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - let codeWatcher = this.getCodeWatcher(file); - if (!codeWatcher) { - codeWatcher = this.getCurrentCodeWatcher(); - } - if (codeWatcher) { - return codeWatcher.runAllCells(); - } else { - return Promise.resolve(); - } - } - - // Note: see codewatcher.ts where the runcell command args are attached. The reason we don't have any - // objects for parameters is because they can't be recreated when passing them through the LiveShare API - public async runCell(file: string, startLine: number, startChar: number, endLine: number, endChar: number): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - const codeWatcher = this.getCodeWatcher(file); - if (codeWatcher) { - return codeWatcher.runCell(new vscode.Range(startLine, startChar, endLine, endChar)); - } - } - - public async runAllCellsAbove(file: string, stopLine: number, stopCharacter: number): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - if (file) { - const codeWatcher = this.getCodeWatcher(file); - - if (codeWatcher) { - return codeWatcher.runAllCellsAbove(stopLine, stopCharacter); - } - } - } - - public async runCellAndAllBelow(file: string, startLine: number, startCharacter: number): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - if (file) { - const codeWatcher = this.getCodeWatcher(file); - - if (codeWatcher) { - return codeWatcher.runCellAndAllBelow(startLine, startCharacter); - } - } - } - - public async runToLine(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - const textEditor = this.documentManager.activeTextEditor; - - if (activeCodeWatcher && textEditor && textEditor.selection) { - return activeCodeWatcher.runToLine(textEditor.selection.start.line); - } - } - - public async runFromLine(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - const textEditor = this.documentManager.activeTextEditor; - - if (activeCodeWatcher && textEditor && textEditor.selection) { - return activeCodeWatcher.runFromLine(textEditor.selection.start.line); - } - } - - public async runCurrentCell(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCell(); - } else { - return Promise.resolve(); - } - } - - public async runCurrentCellAndAdvance(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runCurrentCellAndAdvance(); - } else { - return Promise.resolve(); - } - } - - // tslint:disable-next-line:no-any - public async runSelectionOrLine(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runSelectionOrLine(this.documentManager.activeTextEditor); - } else { - return Promise.resolve(); - } - } - - @captureTelemetry(Telemetry.SelectJupyterURI) - public async selectJupyterURI(): Promise { - 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; - } - } - - public async debugCell(file: string, startLine: number, startChar: number, endLine: number, endChar: number): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - if (file) { - const codeWatcher = this.getCodeWatcher(file); - - if (codeWatcher) { - return codeWatcher.debugCell(new vscode.Range(startLine, startChar, endLine, endChar)); - } - } - } - - @captureTelemetry(Telemetry.SetJupyterURIToLocal) - private async setJupyterURIToLocal(): Promise { - await this.configuration.updateSetting('dataScience.jupyterServerURI', Settings.JupyterServerLocalLaunch, undefined, vscode.ConfigurationTarget.Workspace); - } - - @captureTelemetry(Telemetry.SetJupyterURIToUserSpecified) - private async selectJupyterLaunchURI(): Promise { - // 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); - } - } - - @captureTelemetry(Telemetry.AddCellBelow) - private async addCellBelow(): Promise { - const activeEditor = this.documentManager.activeTextEditor; - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeEditor && activeCodeWatcher) { - return activeCodeWatcher.addEmptyCellToBottom(); - } - } - - private getCurrentCodeLens() : vscode.CodeLens | undefined { - const activeEditor = this.documentManager.activeTextEditor; - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeEditor && activeCodeWatcher) { - // Find the cell that matches - return activeCodeWatcher.getCodeLenses().find((c: vscode.CodeLens) => { - if (c.range.end.line >= activeEditor.selection.anchor.line && - c.range.start.line <= activeEditor.selection.anchor.line) { - return true; - } - return false; - }); - } - } - - private async runAllCellsAboveFromCursor(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const currentCodeLens = this.getCurrentCodeLens(); - if (currentCodeLens) { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runAllCellsAbove(currentCodeLens.range.start.line, currentCodeLens.range.start.character); - } - } else { - return Promise.resolve(); - } - } - - private async runCellAndAllBelowFromCursor(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const currentCodeLens = this.getCurrentCodeLens(); - if (currentCodeLens) { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.runCellAndAllBelow(currentCodeLens.range.start.line, currentCodeLens.range.start.character); - } - } else { - return Promise.resolve(); - } - } - - private async debugCurrentCellFromCursor(): Promise { - this.dataScienceSurveyBanner.showBanner().ignoreErrors(); - - const currentCodeLens = this.getCurrentCodeLens(); - if (currentCodeLens) { - const activeCodeWatcher = this.getCurrentCodeWatcher(); - if (activeCodeWatcher) { - return activeCodeWatcher.debugCurrentCell(); - } - } else { - return Promise.resolve(); - } - } - - 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; - let editorContext = new ContextKey(EditorContexts.DataScienceEnabled, this.commandManager); - editorContext.set(enabled).catch(); - const ownsSelection = settings.datascience.sendSelectionToInteractiveWindow; - editorContext = new ContextKey(EditorContexts.OwnsSelection, this.commandManager); - editorContext.set(ownsSelection && enabled).catch(); - } - - private getCodeWatcher(file: string): ICodeWatcher | undefined { - const possibleDocuments = this.documentManager.textDocuments.filter(d => d.fileName === file); - if (possibleDocuments && possibleDocuments.length === 1) { - return this.dataScienceCodeLensProvider.getCodeWatcher(possibleDocuments[0]); - } else if (possibleDocuments && possibleDocuments.length > 1) { - throw new Error(localize.DataScience.documentMismatch().format(file)); - } - - return undefined; - } - - // Get our matching code watcher for the active document - private getCurrentCodeWatcher(): ICodeWatcher | undefined { - const activeEditor = this.documentManager.activeTextEditor; - if (!activeEditor || !activeEditor.document) { - return undefined; - } - - // Ask our code lens provider to find the matching code watcher for the current document - return this.dataScienceCodeLensProvider.getCodeWatcher(activeEditor.document); - } - - private 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.ExecSelectionInInteractiveWindow, this.runSelectionOrLine, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.SelectJupyterURI, this.selectJupyterURI, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunAllCellsAbove, this.runAllCellsAbove, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunCellAndAllBelow, this.runCellAndAllBelow, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunAllCellsAbovePalette, this.runAllCellsAboveFromCursor, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunCellAndAllBelowPalette, this.runCellAndAllBelowFromCursor, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunToLine, this.runToLine, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunFromLine, this.runFromLine, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.RunFileInInteractiveWindows, this.runFileInteractive, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.AddCellBelow, this.addCellBelow, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.DebugCell, this.debugCell, this); - this.disposableRegistry.push(disposable); - disposable = this.commandManager.registerCommand(Commands.DebugCurrentCellPalette, this.debugCurrentCellFromCursor, 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, this.configuration.getSettings().datascience)).catch(); - } else { - editorContext.set(false).catch(); - } - } - - @debounceAsync(1) - private async sendSettingsTelemetry() : Promise { - try { - // Get our current settings. This is what we want to send. - // tslint:disable-next-line:no-any - const settings = this.configuration.getSettings().datascience as any; - - // Translate all of the 'string' based settings into known values or not. - const pythonConfig = this.workspace.getConfiguration('python'); - if (pythonConfig) { - const keys = Object.keys(settings); - const resultSettings: JSONObject = {}; - for (const k of keys) { - const currentValue = settings[k]; - if (typeof currentValue === 'string') { - const inspectResult = pythonConfig.inspect(`dataScience.${k}`); - if (inspectResult && inspectResult.defaultValue !== currentValue) { - resultSettings[k] = 'non-default'; - } else { - resultSettings[k] = 'default'; - } - } else { - resultSettings[k] = currentValue; - } - } - sendTelemetryEvent(Telemetry.DataScienceSettings, 0, resultSettings); - } - } catch (err) { - traceError(err); - } - } -} diff --git a/src/client/datascience/editor-integration/cellhashprovider.ts b/src/client/datascience/editor-integration/cellhashprovider.ts deleted file mode 100644 index 034056301bf5..000000000000 --- a/src/client/datascience/editor-integration/cellhashprovider.ts +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as hashjs from 'hash.js'; -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter, Position, Range, TextDocumentChangeEvent, TextDocumentContentChangeEvent } from 'vscode'; - -import { IDocumentManager } from '../../common/application/types'; -import { IConfigurationService } from '../../common/types'; -import { generateCells } from '../cellFactory'; -import { CellMatcher } from '../cellMatcher'; -import { splitMultilineString } from '../common'; -import { Identifiers } from '../constants'; -import { InteractiveWindowMessages, IRemoteAddCode, SysInfoReason } from '../interactive-window/interactiveWindowTypes'; -import { ICellHash, ICellHashProvider, IFileHashes, IInteractiveWindowListener } from '../types'; - -interface IRangedCellHash extends ICellHash { - code: string; - startOffset: number; - endOffset: number; - deleted: boolean; - realCode: string; -} - -// This class provides hashes for debugging jupyter cells. Call getHashes just before starting debugging to compute all of the -// hashes for cells. -@injectable() -export class CellHashProvider implements ICellHashProvider, IInteractiveWindowListener { - - // tslint:disable-next-line: no-any - private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>(); - // Map of file to Map of start line to actual hash - private hashes : Map = new Map(); - private executionCount: number = 0; - - constructor( - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IConfigurationService) private configService: IConfigurationService - ) - { - // Watch document changes so we can update our hashes - this.documentManager.onDidChangeTextDocument(this.onChangedDocument.bind(this)); - } - - public dispose() { - this.hashes.clear(); - } - - // tslint:disable-next-line: no-any - public get postMessage(): Event<{ message: string; payload: any }> { - return this.postEmitter.event; - } - - // tslint:disable-next-line: no-any - public onMessage(message: string, payload?: any): void { - switch (message) { - case InteractiveWindowMessages.RemoteAddCode: - if (payload) { - this.onAboutToAddCode(payload); - } - break; - - case InteractiveWindowMessages.AddedSysInfo: - if (payload && payload.type) { - const reason = payload.type as SysInfoReason; - if (reason !== SysInfoReason.Interrupt) { - this.hashes.clear(); - } - } - break; - - default: - break; - } - } - - public getHashes(): IFileHashes[] { - return [...this.hashes.entries()].map(e => { - return { - file: e[0], - hashes: e[1].filter(h => !h.deleted) - }; - }).filter(e => e.hashes.length > 0); - } - - private onAboutToAddCode(args: IRemoteAddCode) { - // Make sure this is valid - if (args && args.code && args.line !== undefined && args.file) { - // First make sure not a markdown cell. Those can be ignored. Just get out the first code cell. - // Regardless of how many 'code' cells exist in the code sent to us, we'll only ever send one at most. - // The code sent to this function is either a cell as defined by #%% or the selected text (which is treated as one cell) - const cells = generateCells(this.configService.getSettings().datascience, args.code, args.file, args.line, true, args.id); - const codeCell = cells.find(c => c.data.cell_type === 'code'); - if (codeCell) { - // When the user adds new code, we know the execution count is increasing - this.executionCount += 1; - - // Skip hash on unknown file though - if (args.file !== Identifiers.EmptyFileName) { - this.addCellHash(splitMultilineString(codeCell.data.source), codeCell.line, codeCell.file, this.executionCount); - } - } - } - } - - private onChangedDocument(e: TextDocumentChangeEvent) { - // See if the document is in our list of docs to watch - const perFile = this.hashes.get(e.document.fileName); - if (perFile) { - // Apply the content changes to the file's cells. - let prevText = e.document.getText(); - e.contentChanges.forEach(c => { - prevText = this.handleContentChange(prevText, c, perFile); - }); - } - } - - private handleContentChange(docText: string, c: TextDocumentContentChangeEvent, hashes: IRangedCellHash[]) : string { - // First compute the number of lines that changed - const lineDiff = c.text.split('\n').length - docText.substr(c.rangeOffset, c.rangeLength).split('\n').length; - const offsetDiff = c.text.length - c.rangeLength; - - // Compute the inclusive offset that is changed by the cell. - const endChangedOffset = c.rangeLength <= 0 ? c.rangeOffset : c.rangeOffset + c.rangeLength - 1; - - // Also compute the text of the document with the change applied - const appliedText = this.applyChange(docText, c); - - hashes.forEach(h => { - // See how this existing cell compares to the change - if (h.endOffset < c.rangeOffset) { - // No change. This cell is entirely before the change - } else if (h.startOffset > endChangedOffset) { - // This cell is after the text that got replaced. Adjust its start/end lines - h.line += lineDiff; - h.endLine += lineDiff; - h.startOffset += offsetDiff; - h.endOffset += offsetDiff; - } else { - // Cell intersects. Mark as deleted if not exactly the same (user could type over the exact same values) - h.deleted = appliedText.substr(h.startOffset, h.endOffset - h.startOffset) !== h.realCode; - } - }); - - return appliedText; - } - - private applyChange(docText: string, c: TextDocumentContentChangeEvent) : string { - const before = docText.substr(0, c.rangeOffset); - const after = docText.substr(c.rangeOffset + c.rangeLength); - return `${before}${c.text}${after}`; - } - - private addCellHash(lines: string[], startLine: number, file: string, expectedCount: number) { - // Find the text document that matches. We need more information than - // the add code gives us - const doc = this.documentManager.textDocuments.find(d => d.fileName === file); - if (doc) { - const cellMatcher = new CellMatcher(this.configService.getSettings().datascience); - - // Compute the code that will really be sent to jupyter - const stripped = lines.filter(l => !cellMatcher.isCode(l)); - - // Figure out our true 'start' line. This is what we need to tell the debugger is - // actually the start of the code as that's what Jupyter will be getting. - let trueStartLine = startLine; - for (let i = 0; i < stripped.length; i += 1) { - if (stripped[i] !== lines[i]) { - trueStartLine += i + 1; - break; - } - } - const line = doc.lineAt(trueStartLine); - const endLine = doc.lineAt(Math.min(trueStartLine + stripped.length - 1, doc.lineCount - 1)); - - // Use the original values however to track edits. This is what we need - // to move around - const startOffset = doc.offsetAt(new Position(startLine, 0)); - const endOffset = doc.offsetAt(endLine.rangeIncludingLineBreak.end); - - // Jupyter also removes blank lines at the end. - let lastLine = stripped[stripped.length - 1]; - while (lastLine.length === 0 || lastLine === '\n') { - stripped.splice(stripped.length - 1, 1); - lastLine = stripped[stripped.length - 1]; - } - // Make sure the last line with actual content ends with a linefeed - if (!lastLine.endsWith('\n')) { - stripped[stripped.length - 1] = `${lastLine}\n`; - } - const hashedCode = stripped.join(''); - const realCode = doc.getText(new Range(new Position(startLine, 0), endLine.rangeIncludingLineBreak.end)); - - const hash : IRangedCellHash = { - hash: hashjs.sha1().update(hashedCode).digest('hex').substr(0, 12), - line: line.lineNumber + 1, - endLine: endLine.lineNumber + 1, - executionCount: expectedCount, - startOffset, - endOffset, - deleted: false, - code: hashedCode, - realCode - }; - - let list = this.hashes.get(file); - if (!list) { - list = []; - } - - // Figure out where to put the item in the list - let inserted = false; - for (let i = 0; i < list.length && !inserted; i += 1) { - const pos = list[i]; - if (hash.line >= pos.line && hash.line <= pos.endLine) { - // Stick right here. This is either the same cell or a cell that overwrote where - // we were. - list.splice(i, 1, hash); - inserted = true; - } else if (pos.line > hash.line) { - // This item comes just after the cell we're inserting. - list.splice(i, 0, hash); - inserted = true; - } - } - if (!inserted) { - list.push(hash); - } - this.hashes.set(file, list); - } - } -} diff --git a/src/client/datascience/editor-integration/codeLensFactory.ts b/src/client/datascience/editor-integration/codeLensFactory.ts deleted file mode 100644 index 190c14506e45..000000000000 --- a/src/client/datascience/editor-integration/codeLensFactory.ts +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { CodeLens, Command, Range, TextDocument } from 'vscode'; - -import { traceWarning } from '../../common/logger'; -import { IConfigurationService } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { generateCellRanges } from '../cellFactory'; -import { Commands } from '../constants'; -import { ICodeLensFactory } from '../types'; - -@injectable() -export class CodeLensFactory implements ICodeLensFactory { - - constructor( - @inject(IConfigurationService) private configService: IConfigurationService - ) { - } - - public createCodeLenses(document: TextDocument): CodeLens[] { - const ranges = generateCellRanges(document, this.configService.getSettings().datascience); - const commands = this.enumerateCommands(); - const codeLenses: CodeLens[] = []; - let firstCell = true; - ranges.forEach(range => { - commands.forEach(c => { - const codeLens = this.createCodeLens(document, range.range, c, firstCell); - if (codeLens) { - codeLenses.push(codeLens); - } - }); - firstCell = false; - }); - - return codeLenses; - } - - private enumerateCommands() : string[] { - const commands = this.configService.getSettings().datascience.codeLenses; - if (commands) { - return commands.split(',').map(s => s.trim()); - } - return [Commands.RunCurrentCell, Commands.RunAllCellsAbove, Commands.DebugCell]; - } - - private createCodeLens(document: TextDocument, range: Range, commandName: string, isFirst: boolean): CodeLens | undefined { - // We only support specific commands - // Be careful here. These arguments will be serialized during liveshare sessions - // and so shouldn't reference local objects. - switch (commandName) { - case Commands.AddCellBelow: - return this.generateCodeLens( - range, - commandName, - localize.DataScience.addCellBelowCommandTitle(), - [document.fileName, range.start.line]); - - case Commands.DebugCurrentCellPalette: - return this.generateCodeLens( - range, - Commands.DebugCurrentCellPalette, - localize.DataScience.debugCellCommandTitle()); - - case Commands.DebugCell: - return this.generateCodeLens( - range, - Commands.DebugCell, - localize.DataScience.debugCellCommandTitle(), - [document.fileName, range.start.line, range.start.character, range.end.line, range.end.character]); - - case Commands.RunCurrentCell: - case Commands.RunCell: - return this.generateCodeLens( - range, - Commands.RunCell, - localize.DataScience.runCellLensCommandTitle(), - [document.fileName, range.start.line, range.start.character, range.end.line, range.end.character]); - - case Commands.RunAllCells: - return this.generateCodeLens( - range, - Commands.RunAllCells, - localize.DataScience.runAllCellsLensCommandTitle(), - [document.fileName, range.start.line, range.start.character]); - - case Commands.RunAllCellsAbovePalette: - case Commands.RunAllCellsAbove: - if (!isFirst) { - return this.generateCodeLens( - range, - Commands.RunAllCellsAbove, - localize.DataScience.runAllCellsAboveLensCommandTitle(), - [document.fileName, range.start.line, range.start.character]); - } - break; - - case Commands.RunCellAndAllBelowPalette: - case Commands.RunCellAndAllBelow: - return this.generateCodeLens( - range, - Commands.RunCellAndAllBelow, - localize.DataScience.runCellAndAllBelowLensCommandTitle(), - [document.fileName, range.start.line, range.start.character]); - break; - - default: - traceWarning(`Invalid command for code lens ${commandName}`); - break; - } - - return undefined; - } - - // tslint:disable-next-line: no-any - private generateCodeLens(range: Range, commandName: string, title: string, args?: any[]) : CodeLens { - return new CodeLens(range, this.generateCommand(commandName, title, args)); - } - - // tslint:disable-next-line: no-any - private generateCommand(commandName: string, title: string, args?: any[]) : Command { - return { - arguments: args, - title, - command: commandName - }; - } -} diff --git a/src/client/datascience/editor-integration/codelensprovider.ts b/src/client/datascience/editor-integration/codelensprovider.ts deleted file mode 100644 index 978c838a7f6d..000000000000 --- a/src/client/datascience/editor-integration/codelensprovider.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import * as vscode from 'vscode'; - -import { ICommandManager, IDebugService, IDocumentManager } from '../../common/application/types'; -import { ContextKey } from '../../common/contextKey'; -import { IConfigurationService, IDataScienceSettings, IDisposable, IDisposableRegistry } from '../../common/types'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EditorContexts, Telemetry } from '../constants'; -import { ICodeWatcher, IDataScienceCodeLensProvider } from '../types'; - -@injectable() -export class DataScienceCodeLensProvider implements IDataScienceCodeLensProvider, IDisposable { - private totalExecutionTimeInMs : number = 0; - private totalGetCodeLensCalls : number = 0; - private activeCodeWatchers: ICodeWatcher[] = []; - private didChangeCodeLenses: vscode.EventEmitter = new vscode.EventEmitter(); - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IConfigurationService) private configuration: IConfigurationService, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IDebugService) private debugService: IDebugService - ) - { - disposableRegistry.push(this); - disposableRegistry.push(this.debugService.onDidChangeActiveDebugSession(this.onChangeDebugSession.bind(this))); - - } - - public dispose() { - // On shutdown send how long on average we spent parsing code lens - if (this.totalGetCodeLensCalls > 0) { - sendTelemetryEvent(Telemetry.CodeLensAverageAcquisitionTime, this.totalExecutionTimeInMs / this.totalGetCodeLensCalls); - } - } - - public get onDidChangeCodeLenses() : vscode.Event { - return this.didChangeCodeLenses.event; - } - - // CodeLensProvider interface - // Some implementation based on DonJayamanne's jupyter extension work - public provideCodeLenses(document: vscode.TextDocument, _token: vscode.CancellationToken): vscode.CodeLens[] { - // Get the list of code lens for this document. - const result = this.getCodeLensTimed(document); - - // Update the hasCodeCells context at the same time we are asked for codelens as VS code will - // ask whenever a change occurs. - const editorContext = new ContextKey(EditorContexts.HasCodeCells, this.commandManager); - editorContext.set(result && result.length > 0).catch(); - - return result; - } - - // IDataScienceCodeLensProvider interface - public getCodeWatcher(document: vscode.TextDocument): ICodeWatcher | undefined { - return this.matchWatcher(document.fileName, document.version, this.configuration.getSettings().datascience); - } - - private onChangeDebugSession(_e: vscode.DebugSession | undefined) { - this.didChangeCodeLenses.fire(); - } - - private getCodeLensTimed(document: vscode.TextDocument): vscode.CodeLens[] { - const stopWatch = new StopWatch(); - const result = this.getCodeLens(document); - this.totalExecutionTimeInMs += stopWatch.elapsedTime; - this.totalGetCodeLensCalls += 1; - return result; - } - - private getCodeLens(document: vscode.TextDocument): vscode.CodeLens[] { - // Don't provide any code lenses if we have not enabled data science - const settings = this.configuration.getSettings(); - if (!settings.datascience.enabled || !settings.datascience.enableCellCodeLens || this.debugService.activeDebugSession) { - // 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, this.configuration.getSettings().datascience); - if (codeWatcher) { - return codeWatcher.getCodeLenses(); - } - - // Create a new watcher for this file - const newCodeWatcher = this.serviceContainer.get(ICodeWatcher); - newCodeWatcher.setDocument(document); - this.activeCodeWatchers.push(newCodeWatcher); - return newCodeWatcher.getCodeLenses(); - } - - private matchWatcher(fileName: string, version: number, settings: IDataScienceSettings) : ICodeWatcher | undefined { - const index = this.activeCodeWatchers.findIndex(item => item.getFileName() === fileName); - if (index >= 0) { - const item = this.activeCodeWatchers[index]; - if (item.getVersion() === version) { - // Also make sure the cached settings are the same. Otherwise these code lenses - // were created with old settings - const settingsStr = JSON.stringify(settings); - const itemSettings = JSON.stringify(item.getCachedSettings()); - if (settingsStr === itemSettings) { - return item; - } - } - // If we have an old version remove it from the active list - this.activeCodeWatchers.splice(index, 1); - } - - // Create a new watcher for this file if we can find a matching document - const possibleDocuments = this.documentManager.textDocuments.filter(d => d.fileName === fileName); - if (possibleDocuments && possibleDocuments.length > 0) { - const newCodeWatcher = this.serviceContainer.get(ICodeWatcher); - newCodeWatcher.setDocument(possibleDocuments[0]); - this.activeCodeWatchers.push(newCodeWatcher); - return newCodeWatcher; - } - - 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 de0d65adda8e..000000000000 --- a/src/client/datascience/editor-integration/codewatcher.ts +++ /dev/null @@ -1,348 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { CodeLens, Position, Range, Selection, TextDocument, TextEditor, TextEditorRevealType } from 'vscode'; - -import { IApplicationShell, IDocumentManager } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDataScienceSettings, ILogger } from '../../common/types'; -import { noop } from '../../common/utils/misc'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { captureTelemetry } from '../../telemetry'; -import { ICodeExecutionHelper } from '../../terminals/types'; -import { Commands, Telemetry } from '../constants'; -import { JupyterInstallError } from '../jupyter/jupyterInstallError'; -import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; -import { ICodeLensFactory, ICodeWatcher, IInteractiveWindowProvider } from '../types'; - -@injectable() -export class CodeWatcher implements ICodeWatcher { - private document?: TextDocument; - private version: number = -1; - private fileName: string = ''; - private codeLenses: CodeLens[] = []; - private cachedSettings: IDataScienceSettings | undefined; - - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(ILogger) private logger: ILogger, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider : IInteractiveWindowProvider, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IDocumentManager) private documentManager : IDocumentManager, - @inject(ICodeExecutionHelper) private executionHelper: ICodeExecutionHelper, - @inject(ICodeLensFactory) private codeLensFactory: ICodeLensFactory - ) { - } - - public setDocument(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. Make a copy of our settings. - this.cachedSettings = JSON.parse(JSON.stringify(this.configService.getSettings().datascience)); - - // Use the factory to generate our new code lenses. - this.codeLenses = this.codeLensFactory.createCodeLenses(document); - } - - public getFileName() { - return this.fileName; - } - - public getVersion() { - return this.version; - } - - public getCachedSettings() : IDataScienceSettings | undefined { - return this.cachedSettings; - } - - public getCodeLenses() { - return this.codeLenses; - } - - @captureTelemetry(Telemetry.DebugCurrentCell) - public async debugCurrentCell() { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); - } - - // Run the cell that matches the current cursor position. - return this.runMatchingCell(this.documentManager.activeTextEditor.selection, false, true); - } - - @captureTelemetry(Telemetry.RunAllCells) - public async runAllCells() { - // 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 >= 5) { - const range: Range = new Range(lens.command.arguments[1], lens.command.arguments[2], lens.command.arguments[3], lens.command.arguments[4]); - if (this.document && range) { - const code = this.document.getText(range); - - // Note: We do a get or create active before all addCode commands to make sure that we either have a history up already - // or if we do not we need to start it up as these commands are all expected to start a new history if needed - await this.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) { - return this.runFileInteractiveInternal(); - } - } - - @captureTelemetry(Telemetry.RunFileInteractive) - public async runFileInteractive() { - return this.runFileInteractiveInternal(); - } - - // Run all cells up to the cell containing this start line and character - @captureTelemetry(Telemetry.RunAllCellsAbove) - public async runAllCellsAbove(stopLine: number, stopCharacter: number) { - // Run our code lenses up to this point, lenses are created in order on document load - // so we can rely on them being in linear order for this - for (const lens of this.codeLenses) { - const pastStop = (lens.range.start.line >= stopLine && lens.range.start.character >= stopCharacter); - // Make sure we are dealing with run cell based code lenses in case more types are added later - if (lens.command && lens.command.command === Commands.RunCell) { - if (!pastStop && this.document) { - // We have a cell and we are not past or at the stop point - const code = this.document.getText(lens.range); - await this.addCode(code, this.getFileName(), lens.range.start.line); - } else { - // If we get a cell past or at the stop point stop - break; - } - } - } - } - - @captureTelemetry(Telemetry.RunAllCellsAbove) - public async runCellAndAllBelow(startLine: number, startCharacter: number) { - // Run our code lenses from this point to the end, lenses are created in order on document load - // so we can rely on them being in linear order for this - for (const lens of this.codeLenses) { - const pastStart = (lens.range.start.line >= startLine && lens.range.start.character >= startCharacter); - // Make sure we are dealing with run cell based code lenses in case more types are added later - if (lens.command && lens.command.command === Commands.RunCell) { - if (pastStart && this.document) { - // We have a cell and we are not past or at the stop point - const code = this.document.getText(lens.range); - await this.addCode(code, this.getFileName(), lens.range.start.line); - } - } - } - } - - @captureTelemetry(Telemetry.RunSelectionOrLine) - public async runSelectionOrLine(activeEditor : TextEditor | undefined) { - if (this.document && activeEditor && - this.fileSystem.arePathsSame(activeEditor.document.fileName, this.document.fileName)) { - // Get just the text of the selection or the current line if none - const codeToExecute = await this.executionHelper.getSelectedTextToExecute(activeEditor); - if (!codeToExecute) { - return ; - } - const normalizedCode = await this.executionHelper.normalizeLines(codeToExecute!); - if (!normalizedCode || normalizedCode.trim().length === 0) { - return; - } - await this.addCode(normalizedCode, this.getFileName(), activeEditor.selection.start.line, activeEditor); - } - } - - @captureTelemetry(Telemetry.RunToLine) - public async runToLine(targetLine: number) { - if (this.document && targetLine > 0) { - const previousLine = this.document.lineAt(targetLine - 1); - const code = this.document.getText(new Range(0, 0, previousLine.range.end.line, previousLine.range.end.character)); - - if (code && code.trim().length) { - await this.addCode(code, this.getFileName(), 0); - } - } - } - - @captureTelemetry(Telemetry.RunFromLine) - public async runFromLine(targetLine: number) { - if (this.document && targetLine < this.document.lineCount) { - const lastLine = this.document.lineAt(this.document.lineCount - 1); - const code = this.document.getText(new Range(targetLine, 0, lastLine.range.end.line, lastLine.range.end.character)); - - if (code && code.trim().length) { - await this.addCode(code, this.getFileName(), targetLine); - } - } - } - - @captureTelemetry(Telemetry.RunCell) - public runCell(range: Range) : Promise { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); - } - - // Run the cell clicked. Advance if the cursor is inside this cell and we're allowed to - const advance = range.contains(this.documentManager.activeTextEditor.selection.start) && this.configService.getSettings().datascience.enableAutoMoveToNextCell; - return this.runMatchingCell(range, advance); - } - - @captureTelemetry(Telemetry.DebugCurrentCell) - public debugCell(range: Range) : Promise { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); - } - - // Debug the cell clicked. - return this.runMatchingCell(range, false, true); - } - - @captureTelemetry(Telemetry.RunCurrentCell) - public runCurrentCell() : Promise { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return Promise.resolve(); - } - - // Run the cell that matches the current cursor position. - return this.runMatchingCell(this.documentManager.activeTextEditor.selection, false); - } - - @captureTelemetry(Telemetry.RunCurrentCellAndAdvance) - public async runCurrentCellAndAdvance() { - if (!this.documentManager.activeTextEditor || !this.documentManager.activeTextEditor.document) { - return; - } - - // Run the cell that matches the current cursor position. Always advance - return this.runMatchingCell(this.documentManager.activeTextEditor.selection, true); - } - - public async addEmptyCellToBottom() : Promise { - const editor = this.documentManager.activeTextEditor; - if (editor) { - editor.edit((editBuilder) => { - editBuilder.insert(new Position(editor.document.lineCount, 0), '\n\n#%%\n'); - }); - - const newPosition = new Position(editor.document.lineCount + 3, 0); // +3 to account for the added spaces and to position after the new mark - return this.advanceToRange(new Range(newPosition, newPosition)); - } - } - - private async addCode(code: string, file: string, line: number, editor?: TextEditor, debug?: boolean) : Promise { - try { - const stopWatch = new StopWatch(); - const activeInteractiveWindow = await this.interactiveWindowProvider.getOrCreateActive(); - if (debug) { - await activeInteractiveWindow.debugCode(code, file, line, editor, stopWatch); - } else { - await activeInteractiveWindow.addCode(code, file, line, editor, stopWatch); - } - } catch (err) { - this.handleError(err); - } - } - - private async runMatchingCell(range: Range, advance?: boolean, debug?: boolean) { - const currentRunCellLens = this.getCurrentCellLens(range.start); - const nextRunCellLens = this.getNextCellLens(range.start); - - if (currentRunCellLens) { - // Move the next cell if allowed. - if (advance) { - // 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 - if (this.document) { - // Use that to get our code. - const code = this.document.getText(currentRunCellLens.range); - await this.addCode(code, this.getFileName(), range.start.line, this.documentManager.activeTextEditor, debug); - } - } - } - - private getCurrentCellLens(pos: Position) : CodeLens | undefined { - return this.codeLenses.find(l => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell); - } - - private getNextCellLens(pos: Position) : CodeLens | undefined { - const currentIndex = this.codeLenses.findIndex(l => l.range.contains(pos) && l.command !== undefined && l.command.command === Commands.RunCell); - if (currentIndex >= 0) { - return this.codeLenses.find((l: CodeLens, i: number) => l.command !== undefined && l.command.command === Commands.RunCell && i > currentIndex); - } - return undefined; - } - - private async runFileInteractiveInternal() { - if (this.document) { - const code = this.document.getText(); - await this.addCode(code, this.getFileName(), 0); - } - } - - // 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 instanceof JupyterSelfCertsError) { - // Don't show the message for self cert errors - noop(); - } else if (err.message) { - this.applicationShell.showErrorMessage(err.message); - } else { - this.applicationShell.showErrorMessage(err.toString()); - } - 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/editor-integration/decorator.ts b/src/client/datascience/editor-integration/decorator.ts deleted file mode 100644 index 2b99b4c67e6c..000000000000 --- a/src/client/datascience/editor-integration/decorator.ts +++ /dev/null @@ -1,121 +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 { IExtensionActivationService } from '../../activation/types'; -import { IDocumentManager } from '../../common/application/types'; -import { PYTHON_LANGUAGE } from '../../common/constants'; -import { IConfigurationService, IDisposable, IDisposableRegistry, Resource } from '../../common/types'; -import { generateCellRanges } from '../cellFactory'; - -@injectable() -export class Decorator implements IExtensionActivationService, IDisposable { - private activeCellTop: vscode.TextEditorDecorationType | undefined; - private activeCellBottom: vscode.TextEditorDecorationType | undefined; - private cellSeparatorType: vscode.TextEditorDecorationType | undefined; - private timer: NodeJS.Timer | undefined | number; - - constructor( - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IDisposableRegistry) disposables: IDisposableRegistry, - @inject(IConfigurationService) private configuration: IConfigurationService - ) { - this.computeDecorations(); - disposables.push(this); - disposables.push(this.configuration.getSettings().onDidChange(this.settingsChanged, this)); - disposables.push(this.documentManager.onDidChangeActiveTextEditor(this.changedEditor, this)); - disposables.push(this.documentManager.onDidChangeTextEditorSelection(this.changedSelection, this)); - disposables.push(this.documentManager.onDidChangeTextDocument(this.changedDocument, this)); - this.settingsChanged(); - } - - public activate(_resource: Resource): Promise { - // We don't need to do anything here as we already did all of our work in the - // constructor. - return Promise.resolve(); - } - - public dispose() { - if (this.timer) { - // tslint:disable-next-line: no-any - clearTimeout(this.timer as any); - } - } - - private settingsChanged() { - if (this.documentManager.activeTextEditor) { - this.triggerUpdate(this.documentManager.activeTextEditor); - } - } - - private changedEditor(editor: vscode.TextEditor | undefined) { - this.triggerUpdate(editor); - } - - private changedDocument(e: vscode.TextDocumentChangeEvent) { - if (this.documentManager.activeTextEditor && e.document === this.documentManager.activeTextEditor.document) { - this.triggerUpdate(this.documentManager.activeTextEditor); - } - } - - private changedSelection(e: vscode.TextEditorSelectionChangeEvent) { - if (e.textEditor && e.textEditor.selection.anchor) { - this.triggerUpdate(e.textEditor); - } - } - - private triggerUpdate(editor: vscode.TextEditor | undefined) { - if (this.timer) { - clearTimeout(this.timer as any); - } - this.timer = setTimeout(() => this.update(editor), 100); - } - - private computeDecorations() { - this.activeCellTop = this.documentManager.createTextEditorDecorationType({ - borderColor: new vscode.ThemeColor('peekView.border'), - borderWidth: '2px 0px 0px 0px', - borderStyle: 'solid', - isWholeLine: true - }); - this.activeCellBottom = this.documentManager.createTextEditorDecorationType({ - borderColor: new vscode.ThemeColor('peekView.border'), - borderWidth: '0px 0px 1px 0px', - borderStyle: 'solid', - isWholeLine: true - }); - this.cellSeparatorType = this.documentManager.createTextEditorDecorationType({ - borderColor: new vscode.ThemeColor('editor.lineHighlightBorder'), - borderWidth: '1px 0px 0px 0px', - borderStyle: 'solid', - isWholeLine: true - }); - } - - private update(editor: vscode.TextEditor | undefined) { - if (editor && editor.document && editor.document.languageId === PYTHON_LANGUAGE && this.activeCellTop && this.cellSeparatorType && this.activeCellBottom) { - const settings = this.configuration.getSettings().datascience; - if (settings.decorateCells && settings.enabled) { - // Find all of the cells - const cells = generateCellRanges(editor.document, this.configuration.getSettings().datascience); - - // Find the range for our active cell. - const currentRange = cells.map(c => c.range).filter(r => r.contains(editor.selection.anchor)); - const rangeTop = currentRange.length > 0 ? [new vscode.Range(currentRange[0].start, currentRange[0].start)] : []; - const rangeBottom = currentRange.length > 0 ? [new vscode.Range(currentRange[0].end, currentRange[0].end)] : []; - editor.setDecorations(this.activeCellTop, rangeTop); - editor.setDecorations(this.activeCellBottom, rangeBottom); - - // Find the start range for the rest - const startRanges = cells.map(c => new vscode.Range(c.range.start, c.range.start)); - editor.setDecorations(this.cellSeparatorType, startRanges); - } else { - editor.setDecorations(this.activeCellTop, []); - editor.setDecorations(this.activeCellBottom, []); - editor.setDecorations(this.cellSeparatorType, []); - } - } - } -} diff --git a/src/client/datascience/interactive-window/debugListener.ts b/src/client/datascience/interactive-window/debugListener.ts deleted file mode 100644 index 811b64b15b3d..000000000000 --- a/src/client/datascience/interactive-window/debugListener.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import { DebugSession, Event, EventEmitter } from 'vscode'; - -import { IDebugService } from '../../common/application/types'; -import { noop } from '../../common/utils/misc'; -import { IInteractiveWindowListener } from '../types'; -import { InteractiveWindowMessages } from './interactiveWindowTypes'; - -// tslint:disable: no-any -@injectable() -export class DebugListener implements IInteractiveWindowListener { - private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>(); - constructor(@inject(IDebugService) private debugService: IDebugService) { - this.debugService.onDidChangeActiveDebugSession(this.onChangeDebugSession.bind(this)); - } - - public get postMessage(): Event<{ message: string; payload: any }> { - return this.postEmitter.event; - } - - public onMessage(message: string, _payload?: any): void { - switch (message) { - default: - break; - } - } - public dispose(): void | undefined { - noop(); - } - - private onChangeDebugSession(e: DebugSession | undefined) { - if (e) { - this.postEmitter.fire({message: InteractiveWindowMessages.StartDebugging, payload: undefined}); - } else { - this.postEmitter.fire({message: InteractiveWindowMessages.StopDebugging, payload: undefined}); - } - } -} diff --git a/src/client/datascience/interactive-window/intellisense/baseIntellisenseProvider.ts b/src/client/datascience/interactive-window/intellisense/baseIntellisenseProvider.ts deleted file mode 100644 index 92c4e1866b4b..000000000000 --- a/src/client/datascience/interactive-window/intellisense/baseIntellisenseProvider.ts +++ /dev/null @@ -1,351 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import { injectable, unmanaged } from 'inversify'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as path from 'path'; -import * as uuid from 'uuid/v4'; -import { - CancellationToken, - CancellationTokenSource, - Event, - EventEmitter, - TextDocumentContentChangeEvent, - Uri -} from 'vscode'; - -import { IWorkspaceService } from '../../../common/application/types'; -import { CancellationError } from '../../../common/cancellation'; -import { traceWarning } from '../../../common/logger'; -import { IFileSystem, TemporaryFile } from '../../../common/platform/types'; -import { createDeferred, Deferred, sleep } from '../../../common/utils/async'; -import { Identifiers, Settings } from '../../constants'; -import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../types'; -import { - IAddCell, - ICancelIntellisenseRequest, - IEditCell, - IInteractiveWindowMapping, - InteractiveWindowMessages, - IProvideCompletionItemsRequest, - IProvideHoverRequest, - IProvideSignatureHelpRequest, - IRemoveCell -} from '../interactiveWindowTypes'; -import { convertStringsToSuggestions } from './conversion'; -import { IntellisenseDocument } from './intellisenseDocument'; - -// tslint:disable:no-any -@injectable() -export abstract class BaseIntellisenseProvider implements IInteractiveWindowListener { - - private documentPromise: Deferred | undefined; - private temporaryFile: TemporaryFile | undefined; - private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>(); - private cancellationSources : Map = new Map(); - - constructor( - @unmanaged() private workspaceService: IWorkspaceService, - @unmanaged() private fileSystem: IFileSystem, - @unmanaged() private jupyterExecution: IJupyterExecution, - @unmanaged() private interactiveWindowProvider: IInteractiveWindowProvider - ) { - } - - public dispose() { - if (this.temporaryFile) { - this.temporaryFile.dispose(); - } - } - - public get postMessage(): Event<{message: string; payload: any}> { - return this.postEmitter.event; - } - - public onMessage(message: string, payload?: any) { - switch (message) { - case InteractiveWindowMessages.CancelCompletionItemsRequest: - case InteractiveWindowMessages.CancelHoverRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleCancel); - } - break; - - case InteractiveWindowMessages.ProvideCompletionItemsRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleCompletionItemsRequest); - } - break; - - case InteractiveWindowMessages.ProvideHoverRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleHoverRequest); - } - break; - - case InteractiveWindowMessages.ProvideSignatureHelpRequest: - if (this.isActive) { - this.dispatchMessage(message, payload, this.handleSignatureHelpRequest); - } - break; - - case InteractiveWindowMessages.EditCell: - this.dispatchMessage(message, payload, this.editCell); - break; - - case InteractiveWindowMessages.AddCell: - this.dispatchMessage(message, payload, this.addCell); - break; - - case InteractiveWindowMessages.RemoveCell: - this.dispatchMessage(message, payload, this.removeCell); - break; - - case InteractiveWindowMessages.DeleteAllCells: - this.dispatchMessage(message, payload, this.removeAllCells); - break; - - case InteractiveWindowMessages.RestartKernel: - this.dispatchMessage(message, payload, this.restartKernel); - break; - - default: - break; - } - } - - protected getDocument(resource?: Uri) : Promise { - if (!this.documentPromise) { - this.documentPromise = createDeferred(); - - // Create our dummy document. Compute a file path for it. - if (this.workspaceService.rootPath || resource) { - const dir = resource ? path.dirname(resource.fsPath) : this.workspaceService.rootPath!; - const dummyFilePath = path.join(dir, `History_${uuid().replace(/-/g, '')}.py`); - this.documentPromise.resolve(new IntellisenseDocument(dummyFilePath)); - } else { - this.fileSystem.createTemporaryFile('.py') - .then(t => { - this.temporaryFile = t; - const dummyFilePath = this.temporaryFile.filePath; - this.documentPromise!.resolve(new IntellisenseDocument(dummyFilePath)); - }) - .catch(e => { - this.documentPromise!.reject(e); - }); - } - } - - return this.documentPromise.promise; - } - - protected abstract get isActive(): boolean; - protected abstract provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken) : Promise; - protected abstract provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken) : Promise; - protected abstract provideSignatureHelp(position: monacoEditor.Position, context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken) : Promise; - protected abstract handleChanges(originalFile: string | undefined, document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]) : Promise; - - private dispatchMessage(_message: T, payload: any, handler: (args : M[T]) => void) { - const args = payload as M[T]; - handler.bind(this)(args); - } - - private postResponse(type: T, payload?: M[T]) : void { - const response = payload as any; - if (response && response.id) { - const cancelSource = this.cancellationSources.get(response.id); - if (cancelSource) { - cancelSource.dispose(); - this.cancellationSources.delete(response.id); - } - } - this.postEmitter.fire({message: type.toString(), payload}); - } - - private handleCancel(request: ICancelIntellisenseRequest) { - const cancelSource = this.cancellationSources.get(request.requestId); - if (cancelSource) { - cancelSource.cancel(); - cancelSource.dispose(); - this.cancellationSources.delete(request.requestId); - } - } - - private handleCompletionItemsRequest(request: IProvideCompletionItemsRequest) { - // Create a cancellation source. We'll use this for our sub class request and a jupyter one - const cancelSource = new CancellationTokenSource(); - this.cancellationSources.set(request.requestId, cancelSource); - - // Combine all of the results together. - this.postTimedResponse( - [this.provideCompletionItems(request.position, request.context, request.cellId, cancelSource.token), - this.provideJupyterCompletionItems(request.position, request.context, cancelSource.token)], - InteractiveWindowMessages.ProvideCompletionItemsResponse, - (c) => { - const list = this.combineCompletions(c); - return {list, requestId: request.requestId}; - } - ); - } - - private handleHoverRequest(request: IProvideHoverRequest) { - const cancelSource = new CancellationTokenSource(); - this.cancellationSources.set(request.requestId, cancelSource); - this.postTimedResponse( - [this.provideHover(request.position, request.cellId, cancelSource.token)], - InteractiveWindowMessages.ProvideHoverResponse, - (h) => { - if (h && h[0]) { - return { hover: h[0]!, requestId: request.requestId}; - } else { - return { hover: { contents: [] }, requestId: request.requestId }; - } - }); - } - - private async provideJupyterCompletionItems(position: monacoEditor.Position, _context: monacoEditor.languages.CompletionContext, cancelToken: CancellationToken) : Promise { - try { - const activeServer = await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions()); - const document = await this.getDocument(); - if (activeServer && document) { - const code = document.getEditCellContent(); - const lines = code.splitLines({trim: false, removeEmptyEntries: false}); - const offsetInCode = lines.reduce((a: number, c: string, i: number) => { - if (i < position.lineNumber - 1) { - return a + c.length + 1; - } else if (i === position.lineNumber - 1) { - return a + position.column - 1; - } else { - return a; - } - }, 0); - const jupyterResults = await activeServer.getCompletion(code, offsetInCode, cancelToken); - if (jupyterResults && jupyterResults.matches) { - const baseOffset = document.getEditCellOffset(); - const basePosition = document.positionAt(baseOffset); - const startPosition = document.positionAt(jupyterResults.cursor.start + baseOffset); - const endPosition = document.positionAt(jupyterResults.cursor.end + baseOffset); - const range: monacoEditor.IRange = { - startLineNumber: startPosition.line + 1 - basePosition.line, // monaco is 1 based - startColumn: startPosition.character + 1, - endLineNumber: endPosition.line + 1 - basePosition.line, - endColumn: endPosition.character + 1 - }; - return { - suggestions: convertStringsToSuggestions(jupyterResults.matches, range, jupyterResults.metadata), - incomplete: false - }; - } - } - } catch (e) { - if (!(e instanceof CancellationError)) { - traceWarning(e); - } - } - - return { - suggestions: [], - incomplete: false - }; - - } - - private postTimedResponse(promises: Promise[], message: T, formatResponse: (val: (R | null)[]) => M[T]) { - // Time all of the promises to make sure they don't take too long - const timed = promises.map(p => Promise.race([p, sleep(Settings.IntellisenseTimeout)])); - - // Wait for all of of the timings. - const all = Promise.all(timed); - all.then(r => { - - // Check all of the results. If they timed out, turn into - // a null so formatResponse can post the empty result. - const nulled = r.map(v => { - if (v === Settings.IntellisenseTimeout) { - return null; - } - return v as R; - }); - this.postResponse(message, formatResponse(nulled)); - }).catch(_e => { - this.postResponse(message, formatResponse([null])); - }); - } - - private combineCompletions(list: (monacoEditor.languages.CompletionList | null)[]) : monacoEditor.languages.CompletionList { - // Note to self. We're eliminating duplicates ourselves. The alternative would be to - // have more than one intellisense provider at the monaco editor level and return jupyter - // results independently. Maybe we switch to this when jupyter resides on the react side. - const uniqueSuggestions: Map = new Map(); - list.forEach(c => { - if (c) { - c.suggestions.forEach(s => { - if (!uniqueSuggestions.has(s.insertText)) { - uniqueSuggestions.set(s.insertText, s); - } - }); - } - }); - - return { - suggestions: Array.from(uniqueSuggestions.values()), - incomplete: false - }; - } - - private handleSignatureHelpRequest(request: IProvideSignatureHelpRequest) { - const cancelSource = new CancellationTokenSource(); - this.cancellationSources.set(request.requestId, cancelSource); - this.postTimedResponse( - [this.provideSignatureHelp(request.position, request.context, request.cellId, cancelSource.token)], - InteractiveWindowMessages.ProvideSignatureHelpResponse, - (s) => { - if (s && s[0]) { - return { signatureHelp: s[0]!, requestId: request.requestId}; - } else { - return {signatureHelp: { signatures: [], activeParameter: 0, activeSignature: 0 }, requestId: request.requestId}; - } - }); - } - - private async addCell(request: IAddCell): Promise { - // Get the document and then pass onto the sub class - const document = await this.getDocument(request.file === Identifiers.EmptyFileName ? undefined : Uri.file(request.file)); - if (document) { - const changes = document.addCell(request.fullText, request.currentText, request.id); - return this.handleChanges(request.file, document, changes); - } - } - - private async editCell(request: IEditCell): Promise { - // First get the document - const document = await this.getDocument(); - if (document) { - const changes = document.edit(request.changes, request.id); - return this.handleChanges(undefined, document, changes); - } - } - - private removeCell(_request: IRemoveCell): Promise { - // Skip this request. The logic here being that - // a user can remove a cell from the UI, but it's still loaded into the Jupyter kernel. - return Promise.resolve(); - } - - private removeAllCells(): Promise { - // Skip this request. The logic here being that - // a user can remove a cell from the UI, but it's still loaded into the Jupyter kernel. - return Promise.resolve(); - } - - private async restartKernel(): Promise { - // This is the one that acts like a reset - const document = await this.getDocument(); - if (document) { - const changes = document.removeAllCells(); - return this.handleChanges(undefined, document, changes); - } - } -} diff --git a/src/client/datascience/interactive-window/intellisense/conversion.ts b/src/client/datascience/interactive-window/intellisense/conversion.ts deleted file mode 100644 index e1424446b02a..000000000000 --- a/src/client/datascience/interactive-window/intellisense/conversion.ts +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as vscode from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient'; - -// See the comment on convertCompletionItemKind below -// Here's the monaco enum: -// Method = 0, -// Function = 1, -// Constructor = 2, -// Field = 3, -// Variable = 4, -// Class = 5, -// Struct = 6, -// Interface = 7, -// Module = 8, -// Property = 9, -// Event = 10, -// Operator = 11, -// Unit = 12, -// Value = 13, -// Constant = 14, -// Enum = 15, -// EnumMember = 16, -// Keyword = 17, -// Text = 18, -// Color = 19, -// File = 20, -// Reference = 21, -// Customcolor = 22, -// Folder = 23, -// TypeParameter = 24, -// Snippet = 25 -// -// Here's the vscode enum -// const Text: 1; -// const Method: 2; -// const Function: 3; -// const Constructor: 4; -// const Field: 5; -// const Variable: 6; -// const Class: 7; -// const Interface: 8; -// const Module: 9; -// const Property: 10; -// const Unit: 11; -// const Value: 12; -// const Enum: 13; -// const Keyword: 14; -// const Snippet: 15; -// const Color: 16; -// const File: 17; -// const Reference: 18; -// const Folder: 19; -// const EnumMember: 20; -// const Constant: 21; -// const Struct: 22; -// const Event: 23; -// const Operator: 24; -// const TypeParameter: 25; - -// Left side is the vscode value. -const mapCompletionItemKind: Map = new Map([ - [0, 9], // No value for zero in vscode - [1, 18], // Text - [2, 0], // Method - [3, 1], // Function - [4, 2], // Constructor - [5, 3], // Field - [6, 4], // Variable - [7, 5], // Class - [8, 7], // Interface - [9, 8], // Module - [10, 9], // Property - [11, 12], // Unit - [12, 13], // Value - [13, 15], // Enum - [14, 17], // Keyword - [15, 25], // Snippet - [16, 19], // Color - [17, 20], // File - [18, 21], // Reference - [19, 23], // Folder - [20, 16], // EnumMember - [21, 14], // Constant - [22, 6], // Struct - [23, 10], // Event - [24, 11], // Operator - [25, 24] // TypeParameter -]); - -const mapJupyterKind: Map = new Map([ - ['method', 0], - ['function', 1], - ['constructor', 2], - ['field', 3], - ['variable', 4], - ['class', 5], - ['struct', 6], - ['interface', 7], - ['module', 8], - ['property', 9], - ['event', 10], - ['operator', 11], - ['unit', 12], - ['value', 13], - ['constant', 14], - ['enum', 15], - ['enumMember', 16], - ['keyword', 17], - ['text', 18], - ['color', 19], - ['file', 20], - ['reference', 21], - ['customcolor', 22], - ['folder', 23], - ['typeParameter', 24], - ['snippet', 25], - ['', 25] -]); - -function convertToMonacoRange(range: vscodeLanguageClient.Range | undefined) : monacoEditor.IRange | undefined { - if (range) { - return { - startLineNumber: range.start.line + 1, - startColumn: range.start.character + 1, - endLineNumber: range.end.line + 1, - endColumn: range.end.character + 1 - }; - } -} - -// Something very fishy. If the monacoEditor.languages.CompletionItemKind is included here, we get this error on startup -// Activating extension `ms-python.python` failed: Unexpected token { -// extensionHostProcess.js:457 -// Here is the error stack: f:\vscode-python\node_modules\monaco-editor\esm\vs\editor\editor.api.js:5 -// import { EDITOR_DEFAULTS } from './common/config/editorOptions.js'; -// Instead just use a map -function convertToMonacoCompletionItemKind(kind?: number): number { - const value = kind ? mapCompletionItemKind.get(kind) : 9; // Property is 9 - if (value) { - return value; - } - return 9; // Property -} - -function convertToMonacoCompletionItem(item: vscodeLanguageClient.CompletionItem, requiresKindConversion: boolean) : monacoEditor.languages.CompletionItem { - // They should be pretty much identical? Except for ranges. - // tslint:disable-next-line: no-object-literal-type-assertion - const result = {...item} as monacoEditor.languages.CompletionItem; - if (requiresKindConversion) { - result.kind = convertToMonacoCompletionItemKind(item.kind); - } - - // Make sure we have insert text, otherwise the monaco editor will crash on trying to hit tab or enter on the text - if (!result.insertText && result.label) { - result.insertText = result.label; - } - - return result; -} - -export function convertToMonacoCompletionList( - result: vscodeLanguageClient.CompletionList | vscodeLanguageClient.CompletionItem[] | vscode.CompletionItem[] | vscode.CompletionList | null, - requiresKindConversion: boolean) : monacoEditor.languages.CompletionList { - if (result) { - if (result.hasOwnProperty('items')) { - const list = result as vscodeLanguageClient.CompletionList; - return { - suggestions: list.items.map(l => convertToMonacoCompletionItem(l, requiresKindConversion)), - incomplete: list.isIncomplete - }; - } else { - // Must be one of the two array types since there's no items property. - const array = result as vscodeLanguageClient.CompletionItem[]; - return { - suggestions: array.map(l => convertToMonacoCompletionItem(l, requiresKindConversion)), - incomplete: false - }; - } - } - - return { - suggestions: [], - incomplete: false - }; -} - -function convertToMonacoMarkdown(strings: vscodeLanguageClient.MarkupContent | vscodeLanguageClient.MarkedString | vscodeLanguageClient.MarkedString[] | vscode.MarkedString | vscode.MarkedString[]) : monacoEditor.IMarkdownString[] { - if (strings.hasOwnProperty('kind')) { - const content = strings as vscodeLanguageClient.MarkupContent; - return [ - { - value: content.value - } - ]; - } else if (strings.hasOwnProperty('value')) { - // tslint:disable-next-line: no-any - const content = strings as any; - return [ - { - value: content.value - } - ]; - } else if (typeof strings === 'string') { - return [ - { - value: strings.toString() - } - ]; - } else if (Array.isArray(strings)) { - const array = strings as vscodeLanguageClient.MarkedString[]; - return array.map(a => convertToMonacoMarkdown(a)[0]); - } - - return []; -} - -export function convertToMonacoHover(result: vscodeLanguageClient.Hover | vscode.Hover | null | undefined) : monacoEditor.languages.Hover { - if (result) { - return { - contents: convertToMonacoMarkdown(result.contents), - range: convertToMonacoRange(result.range) - }; - } - - return { - contents: [] - }; -} - -// tslint:disable-next-line: no-any -export function convertStringsToSuggestions(strings: ReadonlyArray, range: monacoEditor.IRange, metadata: any) : monacoEditor.languages.CompletionItem [] { - // Try to compute kind from the metadata. - let kinds: number[]; - if (metadata && metadata._jupyter_types_experimental) { - // tslint:disable-next-line: no-any - kinds = metadata._jupyter_types_experimental.map((e: any) => { - const result = mapJupyterKind.get(e.type); - return result ? result : 3; // If not found use Field = 3 - }); - } - - return strings.map((s: string, i: number) => { - return { - label: s, - insertText: s, - sortText: s, - kind: kinds ? kinds[i] : 3, // Note: importing the monacoEditor.languages.CompletionItemKind causes a failure in loading the extension. So we use numbers. - range - }; - }); -} - -export function convertToMonacoSignatureHelp( - result: vscodeLanguageClient.SignatureHelp | vscode.SignatureHelp | null) : monacoEditor.languages.SignatureHelp { - if (result) { - return result as monacoEditor.languages.SignatureHelp; - } - - return { - signatures: [], - activeParameter: 0, - activeSignature: 0 - }; -} diff --git a/src/client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider.ts b/src/client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider.ts deleted file mode 100644 index e199c9490106..000000000000 --- a/src/client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider.ts +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import { CancellationToken, TextDocumentContentChangeEvent, Uri } from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient'; - -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../../activation/types'; -import { IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IConfigurationService } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { Identifiers } from '../../constants'; -import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../types'; -import { BaseIntellisenseProvider } from './baseIntellisenseProvider'; -import { convertToMonacoCompletionList, convertToMonacoHover, convertToMonacoSignatureHelp } from './conversion'; -import { IntellisenseDocument } from './intellisenseDocument'; - -// tslint:disable:no-any -@injectable() -export class DotNetIntellisenseProvider extends BaseIntellisenseProvider implements IInteractiveWindowListener { - - private languageClientPromise : Deferred | undefined; - private sentOpenDocument : boolean = false; - private active: boolean = false; - - constructor( - @inject(ILanguageServer) private languageServer: ILanguageServer, - @inject(ILanguageServerAnalysisOptions) private readonly analysisOptions: ILanguageServerAnalysisOptions, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IJupyterExecution) jupyterExecution: IJupyterExecution, - @inject(IInteractiveWindowProvider) interactiveWindowProvider: IInteractiveWindowProvider - ) { - super(workspaceService, fileSystem, jupyterExecution, interactiveWindowProvider); - - // Make sure we're active. We still listen to messages for adding and editing cells, - // but we don't actually return any data. - this.active = !this.configService.getSettings().jediEnabled; - - // Listen for updates to settings to change this flag. Don't bother disposing the config watcher. It lives - // till the extension dies anyway. - this.configService.getSettings().onDidChange(() => this.active = !this.configService.getSettings().jediEnabled); - } - - protected get isActive() : boolean { - return this.active; - } - - protected async provideCompletionItems(position: monacoEditor.Position, context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken) : Promise { - const languageClient = await this.getLanguageClient(); - const document = await this.getDocument(); - if (languageClient && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await languageClient.sendRequest( - vscodeLanguageClient.CompletionRequest.type, - languageClient.code2ProtocolConverter.asCompletionParams(document, docPos, context), - token); - return convertToMonacoCompletionList(result, true); - } - - return { - suggestions: [], - incomplete: false - }; - } - protected async provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken) : Promise { - const languageClient = await this.getLanguageClient(); - const document = await this.getDocument(); - if (languageClient && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await languageClient.sendRequest( - vscodeLanguageClient.HoverRequest.type, - languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, docPos), - token); - return convertToMonacoHover(result); - } - - return { - contents: [] - }; - } - protected async provideSignatureHelp(position: monacoEditor.Position, _context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken) : Promise { - const languageClient = await this.getLanguageClient(); - const document = await this.getDocument(); - if (languageClient && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await languageClient.sendRequest( - vscodeLanguageClient.SignatureHelpRequest.type, - languageClient.code2ProtocolConverter.asTextDocumentPositionParams(document, docPos), - token); - return convertToMonacoSignatureHelp(result); - } - - return { - signatures: [], - activeParameter: 0, - activeSignature: 0 - }; - } - - protected async handleChanges(originalFile: string | undefined, document: IntellisenseDocument, changes: TextDocumentContentChangeEvent[]) : Promise { - // Then see if we can talk to our language client - if (this.active && document) { - - // Cache our document state as it may change after we get our language client. Async call may allow a change to - // come in before we send the first doc open. - const docItem = document.textDocumentItem; - const docItemId = document.textDocumentId; - - // Broadcast an update to the language server - const languageClient = await this.getLanguageClient(originalFile === Identifiers.EmptyFileName || originalFile === undefined ? undefined : Uri.file(originalFile)); - - if (!this.sentOpenDocument) { - this.sentOpenDocument = true; - return languageClient.sendNotification(vscodeLanguageClient.DidOpenTextDocumentNotification.type, { textDocument: docItem }); - } else { - return languageClient.sendNotification(vscodeLanguageClient.DidChangeTextDocumentNotification.type, { textDocument: docItemId, contentChanges: changes }); - } - } - } - - private getLanguageClient(file?: Uri) : Promise { - if (!this.languageClientPromise) { - this.languageClientPromise = createDeferred(); - this.startup(file) - .then(() => { - this.languageClientPromise!.resolve(this.languageServer.languageClient); - }) - .catch((e: any) => { - this.languageClientPromise!.reject(e); - }); - } - return this.languageClientPromise.promise; - } - - private async startup(resource?: Uri) : Promise { - // Start up the language server. We'll use this to talk to the language server - const options = await this.analysisOptions!.getAnalysisOptions(); - await this.languageServer.start(resource, options); - } -} diff --git a/src/client/datascience/interactive-window/intellisense/intellisenseDocument.ts b/src/client/datascience/interactive-window/intellisense/intellisenseDocument.ts deleted file mode 100644 index 15e657c57609..000000000000 --- a/src/client/datascience/interactive-window/intellisense/intellisenseDocument.ts +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode'; -import * as vscodeLanguageClient from 'vscode-languageclient'; - -import { PYTHON_LANGUAGE } from '../../../common/constants'; -import { Identifiers } from '../../constants'; -import { DefaultWordPattern, ensureValidWordDefinition, getWordAtText, regExpLeadsToEndlessLoop } from './wordHelper'; - -class IntellisenseLine implements TextLine { - - private _range: Range; - private _rangeWithLineBreak: Range; - private _firstNonWhitespaceIndex: number | undefined; - private _isEmpty: boolean | undefined; - - constructor(private _contents: string, private _line: number, private _offset: number) { - this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); - this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); - } - - public get offset(): number { - return this._offset; - } - public get lineNumber(): number { - return this._line; - } - public get text(): string { - return this._contents; - } - public get range(): Range { - return this._range; - } - public get rangeIncludingLineBreak(): Range { - return this._rangeWithLineBreak; - } - public get firstNonWhitespaceCharacterIndex(): number { - if (this._firstNonWhitespaceIndex === undefined) { - this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; - } - return this._firstNonWhitespaceIndex; - } - public get isEmptyOrWhitespace(): boolean { - if (this._isEmpty === undefined) { - this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; - } - return this._isEmpty; - } -} - -interface ICellRange { - id: string; - start: number; - fullEnd: number; - currentEnd: number; -} - -export class IntellisenseDocument implements TextDocument { - - private _uri: Uri; - private _version: number = 0; - private _lines: IntellisenseLine[] = []; - private _contents: string = ''; - private _cellRanges: ICellRange[] = []; - - constructor(fileName: string) { - // The file passed in is the base Uri for where we're basing this - // document. - // - // What about liveshare? - this._uri = Uri.file(fileName); - - // We should start our edit offset at 0. Each cell should end with a '/n' - this._cellRanges.push({ id: Identifiers.EditCellId, start: 0, fullEnd: 0, currentEnd: 0 }); - } - - public get uri(): Uri { - return this._uri; - } - public get fileName(): string { - return this._uri.fsPath; - } - - public get isUntitled(): boolean { - return true; - } - public get languageId(): string { - return PYTHON_LANGUAGE; - } - public get version(): number { - return this._version; - } - public get isDirty(): boolean { - return true; - } - public get isClosed(): boolean { - return false; - } - public save(): Thenable { - return Promise.resolve(true); - } - public get eol(): EndOfLine { - return EndOfLine.LF; - } - public get lineCount(): number { - return this._lines.length; - } - public lineAt(position: Position | number): TextLine { - if (typeof position === 'number') { - return this._lines[position as number]; - } else { - return this._lines[position.line]; - } - } - public offsetAt(position: Position): number { - return this.convertToOffset(position); - } - public positionAt(offset: number): Position { - let line = 0; - let ch = 0; - while (line + 1 < this._lines.length && this._lines[line + 1].offset <= offset) { - line += 1; - } - if (line < this._lines.length) { - ch = offset - this._lines[line].offset; - } - return new Position(line, ch); - } - public getText(range?: Range | undefined): string { - if (!range) { - return this._contents; - } else { - const startOffset = this.convertToOffset(range.start); - const endOffset = this.convertToOffset(range.end); - return this._contents.substr(startOffset, endOffset - startOffset); - } - } - public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { - if (!regexp) { - // use default when custom-regexp isn't provided - regexp = DefaultWordPattern; - - } else if (regExpLeadsToEndlessLoop(regexp)) { - // use default when custom-regexp is bad - console.warn(`[getWordRangeAtPosition]: ignoring custom regexp '${regexp.source}' because it matches the empty string.`); - regexp = DefaultWordPattern; - } - - const wordAtText = getWordAtText( - position.character + 1, - ensureValidWordDefinition(regexp), - this._lines[position.line].text, - 0 - ); - - if (wordAtText) { - return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); - } - return undefined; - } - public validateRange(range: Range): Range { - return range; - } - public validatePosition(position: Position): Position { - return position; - } - - public get textDocumentItem(): vscodeLanguageClient.TextDocumentItem { - return { - uri: this._uri.toString(), - languageId: this.languageId, - version: this.version, - text: this.getText() - }; - } - - public get textDocumentId(): vscodeLanguageClient.VersionedTextDocumentIdentifier { - return { - uri: this._uri.toString(), - version: this.version - }; - } - public addCell(fullCode: string, currentCode: string, id: string): TextDocumentContentChangeEvent[] { - // This should only happen once for each cell. - this._version += 1; - - // Get rid of windows line endings. We're normalizing on linux - const normalized = fullCode.replace(/\r/g, ''); - const normalizedCurrent = currentCode.replace(/\r/g, ''); - - // This item should go just before the edit cell - - // Make sure to put a newline between this code and the next code - const newCode = `${normalized}\n`; - const newCurrentCode = `${normalizedCurrent}\n`; - - // We should start just before the last cell. - const fromOffset = this.getEditCellOffset(); - - // Split our text between the edit text and the cells above - const before = this._contents.substr(0, fromOffset); - const after = this._contents.substr(fromOffset); - const fromPosition = this.positionAt(fromOffset); - - // Save the range for this cell () - this._cellRanges.splice(this._cellRanges.length - 1, 0, - { id, start: fromOffset, fullEnd: fromOffset + newCode.length, currentEnd: fromOffset + newCurrentCode.length }); - - // Update our entire contents and recompute our lines - this._contents = `${before}${newCode}${after}`; - this._lines = this.createLines(); - this._cellRanges[this._cellRanges.length - 1].start += newCode.length; - this._cellRanges[this._cellRanges.length - 1].fullEnd += newCode.length; - this._cellRanges[this._cellRanges.length - 1].currentEnd += newCode.length; - - return [ - { - range: this.createSerializableRange(fromPosition, fromPosition), - rangeOffset: fromOffset, - rangeLength: 0, // Adds are always zero - text: newCode - } - ]; - } - - public removeAllCells(): TextDocumentContentChangeEvent[] { - // Remove everything up to the edit cell - if (this._cellRanges.length > 1) { - this._version += 1; - - // Compute the offset for the edit cell - const toOffset = this._cellRanges[this._cellRanges.length - 1].start; - const from = this.positionAt(0); - const to = this.positionAt(toOffset); - - // Remove the entire range. - const result = this.removeRange('', from, to, 0); - - // Update our cell range - this._cellRanges = [ { - id: Identifiers.EditCellId, - start: 0, - fullEnd: this._cellRanges[this._cellRanges.length - 1].fullEnd - toOffset, - currentEnd: this._cellRanges[this._cellRanges.length - 1].fullEnd - toOffset - }]; - - return result; - } - - return []; - } - - public edit(editorChanges: monacoEditor.editor.IModelContentChange[], id: string): TextDocumentContentChangeEvent[] { - this._version += 1; - - // Convert the range to local (and remove 1 based) - if (editorChanges && editorChanges.length) { - const normalized = editorChanges[0].text.replace(/\r/g, ''); - - // Figure out which cell we're editing. - const cellIndex = this._cellRanges.findIndex(c => c.id === id); - if (id === Identifiers.EditCellId && cellIndex >= 0) { - // This is an actual edit. - // Line/column are within this cell. Use its offset to compute the real position - const editPos = this.positionAt(this._cellRanges[cellIndex].start); - const from = new Position(editPos.line + editorChanges[0].range.startLineNumber - 1, editorChanges[0].range.startColumn - 1); - const to = new Position(editPos.line + editorChanges[0].range.endLineNumber - 1, editorChanges[0].range.endColumn - 1); - - // Remove this range from the contents and return the change. - return this.removeRange(normalized, from, to, cellIndex); - } else if (cellIndex >= 0) { - // This is an edit of a read only cell. Just replace our currentEnd position - const newCode = `${normalized}\n`; - this._cellRanges[cellIndex].currentEnd = this._cellRanges[cellIndex].start + newCode.length; - } - } - - return []; - } - - public convertToDocumentPosition(id: string, line: number, ch: number): Position { - // Monaco is 1 based, and we need to add in our cell offset. - const cellIndex = this._cellRanges.findIndex(c => c.id === id); - if (cellIndex >= 0) { - // Line/column are within this cell. Use its offset to compute the real position - const editLine = this.positionAt(this._cellRanges[cellIndex].start); - const docLine = line - 1 + editLine.line; - const docCh = ch - 1; - return new Position(docLine, docCh); - } - - // We can't find a cell that matches. Just remove the 1 based - return new Position(line - 1, ch - 1); - } - - public getEditCellContent() { - return this._contents.substr(this.getEditCellOffset()); - } - - public getEditCellOffset() { - return this._cellRanges[this._cellRanges.length - 1].start; - } - - private removeRange(newText: string, from: Position, to: Position, cellIndex: number) : TextDocumentContentChangeEvent[] { - const fromOffset = this.convertToOffset(from); - const toOffset = this.convertToOffset(to); - - // Recreate our contents, and then recompute all of our lines - const before = this._contents.substr(0, fromOffset); - const after = this._contents.substr(toOffset); - this._contents = `${before}${newText}${after}`; - this._lines = this.createLines(); - - // Update ranges after this. All should move by the diff in length, although the current one - // should stay at the same start point. - const lengthDiff = newText.length - (toOffset - fromOffset); - for (let i = cellIndex; i < this._cellRanges.length; i += 1) { - if (i !== cellIndex) { - this._cellRanges[i].start += lengthDiff; - } - this._cellRanges[i].fullEnd += lengthDiff; - this._cellRanges[i].currentEnd += lengthDiff; - } - - return [ - { - range: this.createSerializableRange(from, to), - rangeOffset: fromOffset, - rangeLength: toOffset - fromOffset, - text: newText - } - ]; - } - - private createLines(): IntellisenseLine[] { - const split = this._contents.splitLines({ trim: false, removeEmptyEntries: false }); - let prevLine: IntellisenseLine | undefined; - return split.map((s, i) => { - const nextLine = this.createTextLine(s, i, prevLine); - prevLine = nextLine; - return nextLine; - }); - } - - private createTextLine(line: string, index: number, prevLine: IntellisenseLine | undefined): IntellisenseLine { - return new IntellisenseLine(line, index, prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0); - } - - private convertToOffset(pos: Position): number { - if (pos.line < this._lines.length) { - return this._lines[pos.line].offset + pos.character; - } - return this._contents.length; - } - - private createSerializableRange(start: Position, end: Position): Range { - const result = { - start: { - line: start.line, - character: start.character - }, - end: { - line: end.line, - character: end.character - } - }; - return result as Range; - } -} diff --git a/src/client/datascience/interactive-window/intellisense/jediIntellisenseProvider.ts b/src/client/datascience/interactive-window/intellisense/jediIntellisenseProvider.ts deleted file mode 100644 index 2772d770b6dd..000000000000 --- a/src/client/datascience/interactive-window/intellisense/jediIntellisenseProvider.ts +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import { CancellationToken, TextDocumentContentChangeEvent } from 'vscode'; - -import { IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IExtensionContext } from '../../../common/types'; -import { IServiceManager } from '../../../ioc/types'; -import { JediFactory } from '../../../languageServices/jediProxyFactory'; -import { PythonCompletionItemProvider } from '../../../providers/completionProvider'; -import { PythonHoverProvider } from '../../../providers/hoverProvider'; -import { PythonSignatureProvider } from '../../../providers/signatureProvider'; -import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../types'; -import { BaseIntellisenseProvider } from './baseIntellisenseProvider'; -import { convertToMonacoCompletionList, convertToMonacoHover, convertToMonacoSignatureHelp } from './conversion'; -import { IntellisenseDocument } from './intellisenseDocument'; - -// tslint:disable:no-any -@injectable() -export class JediIntellisenseProvider extends BaseIntellisenseProvider implements IInteractiveWindowListener { - - private active: boolean = false; - private pythonHoverProvider : PythonHoverProvider | undefined; - private pythonCompletionItemProvider : PythonCompletionItemProvider | undefined; - private pythonSignatureHelpProvider : PythonSignatureProvider | undefined; - private jediFactory: JediFactory; - private readonly context: IExtensionContext; - - constructor( - @inject(IServiceManager) private serviceManager: IServiceManager, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IJupyterExecution) jupyterExecution: IJupyterExecution, - @inject(IInteractiveWindowProvider) interactiveWindowProvider: IInteractiveWindowProvider - ) { - super(workspaceService, fileSystem, jupyterExecution, interactiveWindowProvider); - - this.context = this.serviceManager.get(IExtensionContext); - this.jediFactory = new JediFactory(this.context.asAbsolutePath('.'), this.serviceManager); - this.disposables.push(this.jediFactory); - - // Make sure we're active. We still listen to messages for adding and editing cells, - // but we don't actually return any data. - this.active = this.configService.getSettings().jediEnabled; - - // Listen for updates to settings to change this flag - disposables.push(this.configService.getSettings().onDidChange(() => this.active = this.configService.getSettings().jediEnabled)); - - // Create our jedi wrappers if necessary - if (this.active) { - this.pythonHoverProvider = new PythonHoverProvider(this.jediFactory); - this.pythonCompletionItemProvider = new PythonCompletionItemProvider(this.jediFactory, this.serviceManager); - this.pythonSignatureHelpProvider = new PythonSignatureProvider(this.jediFactory); - } - } - - public dispose() { - super.dispose(); - this.jediFactory.dispose(); - } - protected get isActive() : boolean { - return this.active; - } - protected async provideCompletionItems(position: monacoEditor.Position, _context: monacoEditor.languages.CompletionContext, cellId: string, token: CancellationToken) : Promise { - const document = await this.getDocument(); - if (this.pythonCompletionItemProvider && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await this.pythonCompletionItemProvider.provideCompletionItems(document, docPos, token); - return convertToMonacoCompletionList(result, false); - } - - return { - suggestions: [], - incomplete: false - }; - } - protected async provideHover(position: monacoEditor.Position, cellId: string, token: CancellationToken) : Promise { - const document = await this.getDocument(); - if (this.pythonHoverProvider && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await this.pythonHoverProvider.provideHover(document, docPos, token); - return convertToMonacoHover(result); - } - - return { - contents: [] - }; - } - protected async provideSignatureHelp(position: monacoEditor.Position, _context: monacoEditor.languages.SignatureHelpContext, cellId: string, token: CancellationToken) : Promise { - const document = await this.getDocument(); - if (this.pythonSignatureHelpProvider && document) { - const docPos = document.convertToDocumentPosition(cellId, position.lineNumber, position.column); - const result = await this.pythonSignatureHelpProvider.provideSignatureHelp(document, docPos, token); - return convertToMonacoSignatureHelp(result); - } - - return { - signatures: [], - activeParameter: 0, - activeSignature: 0 - }; - } - - protected handleChanges(_originalFile: string | undefined, _document: IntellisenseDocument, _changes: TextDocumentContentChangeEvent[]) : Promise { - // We don't need to forward these to jedi. It always uses the entire document - return Promise.resolve(); - } - -} diff --git a/src/client/datascience/interactive-window/intellisense/wordHelper.ts b/src/client/datascience/interactive-window/intellisense/wordHelper.ts deleted file mode 100644 index 088b43a9ae12..000000000000 --- a/src/client/datascience/interactive-window/intellisense/wordHelper.ts +++ /dev/null @@ -1,156 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -// Borrowed this from the vscode source. From here: -// src\vs\editor\common\model\wordHelper.ts - -export interface IWordAtPosition { - readonly word: string; - readonly startColumn: number; - readonly endColumn: number; -} - -export const USUAL_WORD_SEPARATORS = '`~!@#$%^&*()-=+[{]}\\|;:\'",.<>/?'; - -/** - * Create a word definition regular expression based on default word separators. - * Optionally provide allowed separators that should be included in words. - * - * The default would look like this: - * /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g - */ -function createWordRegExp(allowInWords: string = ''): RegExp { - let source = '(-?\\d*\\.\\d\\w*)|([^'; - for (const sep of USUAL_WORD_SEPARATORS) { - if (allowInWords.indexOf(sep) >= 0) { - continue; - } - source += `\\${sep}`; - } - source += '\\s]+)'; - return new RegExp(source, 'g'); -} - -// catches numbers (including floating numbers) in the first group, and alphanum in the second -export const DEFAULT_WORD_REGEXP = createWordRegExp(); - -export function ensureValidWordDefinition(wordDefinition?: RegExp | null): RegExp { - let result: RegExp = DEFAULT_WORD_REGEXP; - - if (wordDefinition && (wordDefinition instanceof RegExp)) { - if (!wordDefinition.global) { - let flags = 'g'; - if (wordDefinition.ignoreCase) { - flags += 'i'; - } - if (wordDefinition.multiline) { - flags += 'm'; - } - // tslint:disable-next-line: no-any - if ((wordDefinition as any).unicode) { - flags += 'u'; - } - result = new RegExp(wordDefinition.source, flags); - } else { - result = wordDefinition; - } - } - - result.lastIndex = 0; - - return result; -} - -function getWordAtPosFast(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition | null { - // find whitespace enclosed text around column and match from there - - const pos = column - 1 - textOffset; - const start = text.lastIndexOf(' ', pos - 1) + 1; - - wordDefinition.lastIndex = start; - let match: RegExpMatchArray | null = wordDefinition.exec(text); - while (match) { - const matchIndex = match.index || 0; - if (matchIndex <= pos && wordDefinition.lastIndex >= pos) { - return { - word: match[0], - startColumn: textOffset + 1 + matchIndex, - endColumn: textOffset + 1 + wordDefinition.lastIndex - }; - } - match = wordDefinition.exec(text); - } - - return null; -} - -function getWordAtPosSlow(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition | null { - // matches all words starting at the beginning - // of the input until it finds a match that encloses - // the desired column. slow but correct - - const pos = column - 1 - textOffset; - wordDefinition.lastIndex = 0; - - let match: RegExpMatchArray | null = wordDefinition.exec(text); - while (match) { - const matchIndex = match.index || 0; - if (matchIndex > pos) { - // |nW -> matched only after the pos - return null; - - } else if (wordDefinition.lastIndex >= pos) { - // W|W -> match encloses pos - return { - word: match[0], - startColumn: textOffset + 1 + matchIndex, - endColumn: textOffset + 1 + wordDefinition.lastIndex - }; - } - match = wordDefinition.exec(text); - } - - return null; -} - -export function getWordAtText(column: number, wordDefinition: RegExp, text: string, textOffset: number): IWordAtPosition | null { - - // if `words` can contain whitespace character we have to use the slow variant - // otherwise we use the fast variant of finding a word - wordDefinition.lastIndex = 0; - const match = wordDefinition.exec(text); - if (!match) { - return null; - } - // todo@joh the `match` could already be the (first) word - const ret = match[0].indexOf(' ') >= 0 - // did match a word which contains a space character -> use slow word find - ? getWordAtPosSlow(column, wordDefinition, text, textOffset) - // sane word definition -> use fast word find - : getWordAtPosFast(column, wordDefinition, text, textOffset); - - // both (getWordAtPosFast and getWordAtPosSlow) leave the wordDefinition-RegExp - // in an undefined state and to not confuse other users of the wordDefinition - // we reset the lastIndex - wordDefinition.lastIndex = 0; - - return ret; -} - -export function regExpLeadsToEndlessLoop(regexp: RegExp): boolean { - // Exit early if it's one of these special cases which are meant to match - // against an empty string - if (regexp.source === '^' || regexp.source === '^$' || regexp.source === '$' || regexp.source === '^\\s*$') { - return false; - } - - // We check against an empty string. If the regular expression doesn't advance - // (e.g. ends in an endless loop) it will match an empty string. - const match = regexp.exec(''); - // tslint:disable-next-line: no-any - return !!(match && regexp.lastIndex === 0); -} - -export const DefaultWordPattern = /(-?\d*\.\d\w*)|([^\`\~\!\@\#\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g; diff --git a/src/client/datascience/interactive-window/interactiveWindow.ts b/src/client/datascience/interactive-window/interactiveWindow.ts deleted file mode 100644 index e58856adf778..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindow.ts +++ /dev/null @@ -1,1334 +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, multiInject } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import * as uuid from 'uuid/v4'; -import { ConfigurationTarget, Event, EventEmitter, Position, Range, Selection, TextEditor, Uri, ViewColumn } from 'vscode'; -import { Disposable } from 'vscode-jsonrpc'; -import * as vsls from 'vsls/vscode'; - -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - ILiveShareApi, - IWebPanelProvider, - IWorkspaceService -} from '../../common/application/types'; -import { CancellationError } from '../../common/cancellation'; -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../../common/constants'; -import { ContextKey } from '../../common/contextKey'; -import { traceInfo, traceWarning } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import * as localize from '../../common/utils/localize'; -import { StopWatch } from '../../common/utils/stopWatch'; -import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { generateCellRanges } from '../cellFactory'; -import { EditorContexts, Identifiers, Telemetry } from '../constants'; -import { ColumnWarningSize } from '../data-viewing/types'; -import { JupyterInstallError } from '../jupyter/jupyterInstallError'; -import { JupyterKernelPromiseFailedError } from '../jupyter/jupyterKernelPromiseFailedError'; -import { JupyterSelfCertsError } from '../jupyter/jupyterSelfCertsError'; -import { CssMessages } from '../messages'; -import { - CellState, - ICell, - ICodeCssGenerator, - IConnection, - IDataViewerProvider, - IInteractiveWindow, - IInteractiveWindowInfo, - IInteractiveWindowListener, - IInteractiveWindowProvider, - IJupyterDebugger, - IJupyterExecution, - IJupyterVariable, - IJupyterVariables, - IJupyterVariablesResponse, - IMessageCell, - INotebookExporter, - INotebookImporter, - INotebookServer, - InterruptResult, - IStatusProvider, - IThemeFinder -} from '../types'; -import { WebViewHost } from '../webViewHost'; -import { InteractiveWindowMessageListener } from './interactiveWindowMessageListener'; -import { - IAddedSysInfo, - ICopyCode, - IGotoCode, - IInteractiveWindowMapping, - InteractiveWindowMessages, - IRemoteAddCode, - IShowDataViewer, - ISubmitNewCell, - SysInfoReason -} from './interactiveWindowTypes'; - -@injectable() -export class InteractiveWindow extends WebViewHost implements IInteractiveWindow { - private static sentExecuteCellTelemetry : boolean = false; - private disposed: boolean = false; - private loadPromise: Promise; - private interpreterChangedDisposable: Disposable; - private closedEvent: EventEmitter; - private unfinishedCells: ICell[] = []; - private restartingKernel: boolean = false; - private potentiallyUnfinishedStatus: Disposable[] = []; - private addSysInfoPromise: Deferred | undefined; - private waitingForExportCells: boolean = false; - private jupyterServer: INotebookServer | undefined; - private id : string; - private executeEvent: EventEmitter = new EventEmitter(); - private variableRequestStopWatch: StopWatch | undefined; - private variableRequestPendingCount: number = 0; - - constructor( - @multiInject(IInteractiveWindowListener) private readonly listeners: IInteractiveWindowListener[], - @inject(ILiveShareApi) private liveShare : ILiveShareApi, - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IInterpreterService) private interpreterService: IInterpreterService, - @inject(IWebPanelProvider) provider: IWebPanelProvider, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, - @inject(IThemeFinder) themeFinder: IThemeFinder, - @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) workspaceService: IWorkspaceService, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, - @inject(IDataViewerProvider) private dataExplorerProvider: IDataViewerProvider, - @inject(IJupyterVariables) private jupyterVariables: IJupyterVariables, - @inject(INotebookImporter) private jupyterImporter: INotebookImporter, - @inject(IJupyterDebugger) private jupyterDebugger: IJupyterDebugger - ) { - super( - configuration, - provider, - cssGenerator, - themeFinder, - workspaceService, - (c, v, d) => new InteractiveWindowMessageListener(liveShare, c, v, d), - path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'history-react', 'index_bundle.js'), - localize.DataScience.historyTitle(), - ViewColumn.Two); - - // Create our unique id. We use this to skip messages we send to other interactive windows - this.id = uuid(); - - // Sign up for configuration changes - this.interpreterChangedDisposable = this.interpreterService.onDidChangeInterpreter(this.onInterpreterChanged); - - // Create our event emitter - this.closedEvent = new EventEmitter(); - this.disposables.push(this.closedEvent); - - // Listen for active text editor changes. This is the only way we can tell that we might be needing to gain focus - const handler = this.documentManager.onDidChangeActiveTextEditor(() => this.activating().ignoreErrors()); - this.disposables.push(handler); - - // If our execution changes its liveshare session, we need to close our server - this.jupyterExecution.sessionChanged(() => this.loadPromise = this.reloadAfterShutdown()); - - // Load on a background thread. - this.loadPromise = this.load(); - - // For each listener sign up for their post events - this.listeners.forEach(l => l.postMessage((e) => this.postMessageInternal(e.message, e.payload))); - } - - public get ready() : Promise { - // We need this to ensure the interactive window is up and ready to receive messages. - return this.loadPromise; - } - - public async show(): Promise { - if (!this.disposed) { - // Make sure we're loaded first - await this.loadPromise; - - // Make sure we have at least the initial sys info - await this.addSysInfo(SysInfoReason.Start); - - // Then show our web panel. - return super.show(true); - } - } - - public get closed(): Event { - return this.closedEvent.event; - } - - public get onExecutedCode() : Event { - return this.executeEvent.event; - } - - public addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise { - // Call the internal method. - return this.submitCode(code, file, line, undefined, editor, runningStopWatch, false); - } - - public debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch) : Promise { - // Call the internal method. - return this.submitCode(code, file, line, undefined, editor, runningStopWatch, true); - } - - // tslint:disable-next-line: no-any no-empty cyclomatic-complexity max-func-body-length - public onMessage(message: string, payload: any) { - switch (message) { - case InteractiveWindowMessages.GotoCodeCell: - this.dispatchMessage(message, payload, this.gotoCode); - break; - - case InteractiveWindowMessages.CopyCodeCell: - this.dispatchMessage(message, payload, this.copyCode); - break; - - case InteractiveWindowMessages.RestartKernel: - this.restartKernel().ignoreErrors(); - break; - - case InteractiveWindowMessages.ReturnAllCells: - this.dispatchMessage(message, payload, this.handleReturnAllCells); - break; - - case InteractiveWindowMessages.Interrupt: - this.interruptKernel().ignoreErrors(); - break; - - case InteractiveWindowMessages.Export: - this.dispatchMessage(message, payload, this.export); - break; - - case InteractiveWindowMessages.SendInfo: - this.dispatchMessage(message, payload, this.updateContexts); - break; - - case InteractiveWindowMessages.SubmitNewCell: - this.dispatchMessage(message, payload, this.submitNewCell); - break; - - case InteractiveWindowMessages.DeleteAllCells: - this.logTelemetry(Telemetry.DeleteAllCells); - break; - - case InteractiveWindowMessages.DeleteCell: - this.logTelemetry(Telemetry.DeleteCell); - break; - - case InteractiveWindowMessages.Undo: - this.logTelemetry(Telemetry.Undo); - break; - - case InteractiveWindowMessages.Redo: - this.logTelemetry(Telemetry.Redo); - break; - - case InteractiveWindowMessages.ExpandAll: - this.logTelemetry(Telemetry.ExpandAll); - break; - - case InteractiveWindowMessages.CollapseAll: - this.logTelemetry(Telemetry.CollapseAll); - break; - - case InteractiveWindowMessages.VariableExplorerToggle: - this.variableExplorerToggle(payload); - break; - - case InteractiveWindowMessages.AddedSysInfo: - this.dispatchMessage(message, payload, this.onAddedSysInfo); - break; - - case InteractiveWindowMessages.RemoteAddCode: - this.dispatchMessage(message, payload, this.onRemoteAddedCode); - break; - - case InteractiveWindowMessages.ShowDataViewer: - this.dispatchMessage(message, payload, this.showDataViewer); - break; - - case InteractiveWindowMessages.GetVariablesRequest: - this.dispatchMessage(message, payload, this.requestVariables); - break; - - case InteractiveWindowMessages.GetVariableValueRequest: - this.dispatchMessage(message, payload, this.requestVariableValue); - break; - - case InteractiveWindowMessages.LoadTmLanguageRequest: - this.dispatchMessage(message, payload, this.requestTmLanguage); - break; - - case InteractiveWindowMessages.LoadOnigasmAssemblyRequest: - this.dispatchMessage(message, payload, this.requestOnigasm); - break; - - default: - break; - } - - // Let our listeners handle the message too - if (this.listeners) { - this.listeners.forEach(l => l.onMessage(message, payload)); - } - - // Pass onto our base class. - super.onMessage(message, payload); - - // After our base class handles some stuff, handle it ourselves too. - switch (message) { - case CssMessages.GetCssRequest: - // Update the jupyter server if we have one: - if (this.jupyterServer) { - this.isDark().then(d => this.jupyterServer ? this.jupyterServer.setMatplotLibStyle(d) : Promise.resolve()).ignoreErrors(); - } - break; - - default: - break; - } - - } - - public dispose() { - super.dispose(); - if (!this.disposed) { - this.disposed = true; - this.listeners.forEach(l => l.dispose()); - if (this.interpreterChangedDisposable) { - this.interpreterChangedDisposable.dispose(); - } - if (this.closedEvent) { - this.closedEvent.fire(this); - } - this.updateContexts(undefined); - } - } - - public startProgress() { - this.postMessage(InteractiveWindowMessages.StartProgress).ignoreErrors(); - } - - public stopProgress() { - this.postMessage(InteractiveWindowMessages.StopProgress).ignoreErrors(); - } - - @captureTelemetry(Telemetry.Undo) - public undoCells() { - this.postMessage(InteractiveWindowMessages.Undo).ignoreErrors(); - } - - @captureTelemetry(Telemetry.Redo) - public redoCells() { - this.postMessage(InteractiveWindowMessages.Redo).ignoreErrors(); - } - - @captureTelemetry(Telemetry.DeleteAllCells) - public removeAllCells() { - this.postMessage(InteractiveWindowMessages.DeleteAllCells).ignoreErrors(); - } - - @captureTelemetry(Telemetry.ExpandAll) - public expandAllCells() { - this.postMessage(InteractiveWindowMessages.ExpandAll).ignoreErrors(); - } - - @captureTelemetry(Telemetry.CollapseAll) - public collapseAllCells() { - this.postMessage(InteractiveWindowMessages.CollapseAll).ignoreErrors(); - } - - public exportCells() { - // First ask for all cells. Set state to indicate waiting for result - this.waitingForExportCells = true; - - // Telemetry will fire when the export function is called. - this.postMessage(InteractiveWindowMessages.GetAllCells).ignoreErrors(); - } - - @captureTelemetry(Telemetry.RestartKernel) - public async restartKernel() : Promise { - if (this.jupyterServer && !this.restartingKernel) { - if (this.shouldAskForRestart()) { - // Ask the user if they want us to restart or not. - const message = localize.DataScience.restartKernelMessage(); - const yes = localize.DataScience.restartKernelMessageYes(); - const dontAskAgain = localize.DataScience.restartKernelMessageDontAskAgain(); - const no = localize.DataScience.restartKernelMessageNo(); - - const v = await this.applicationShell.showInformationMessage(message, yes, dontAskAgain, no); - if (v === dontAskAgain) { - this.disableAskForRestart(); - await this.restartKernelInternal(); - } else if (v === yes) { - await this.restartKernelInternal(); - } - } else { - await this.restartKernelInternal(); - } - } - - return Promise.resolve(); - } - - @captureTelemetry(Telemetry.Interrupt) - public async interruptKernel() : Promise { - if (this.jupyterServer && !this.restartingKernel) { - const status = this.statusProvider.set(localize.DataScience.interruptKernelStatus()); - - const settings = this.configuration.getSettings(); - const interruptTimeout = settings.datascience.jupyterInterruptTimeout; - - try { - const result = await this.jupyterServer.interruptKernel(interruptTimeout); - status.dispose(); - - // We timed out, ask the user if they want to restart instead. - if (result === InterruptResult.TimedOut) { - const message = localize.DataScience.restartKernelAfterInterruptMessage(); - const yes = localize.DataScience.restartKernelMessageYes(); - const no = localize.DataScience.restartKernelMessageNo(); - const v = await this.applicationShell.showInformationMessage(message, yes, no); - if (v === yes) { - await this.restartKernelInternal(); - } - } else if (result === InterruptResult.Restarted) { - // Uh-oh, keyboard interrupt crashed the kernel. - this.addSysInfo(SysInfoReason.Interrupt).ignoreErrors(); - } - } catch (err) { - status.dispose(); - this.logger.logError(err); - this.applicationShell.showErrorMessage(err); - } - } - } - - public async previewNotebook(file: string) : Promise { - try { - // First convert to a python file to verify this file is valid. This is - // an easy way to have something else verify the validity of the file. - const results = await this.jupyterImporter.importFromFile(file); - if (results) { - // Show our webpanel to make sure that the code actually shows up. (Vscode disables the webview when it's not active) - await this.show(); - - // Then read in the file as json. This json should already - // be in the cell format - // tslint:disable-next-line: no-any - const contents = JSON.parse(await this.fileSystem.readFile(file)) as any; - if (contents && contents.cells && contents.cells.length) { - // Add a header before the preview - this.addPreviewHeader(file); - - // Convert the cells into actual cell objects - const cells = contents.cells as (nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell)[]; - - // Convert the inputdata into our ICell format - const finishedCells: ICell[] = cells.filter(c => c.source.length > 0).map(c => { - return { - id: uuid(), - file: Identifiers.EmptyFileName, - line: 0, - state: CellState.finished, - data: c, - type: 'preview' - }; - }); - - // Do the same thing that happens when new code is added. - this.onAddCodeEvent(finishedCells); - - // Add a footer after the preview - this.addPreviewFooter(file); - } - } - } catch (e) { - this.applicationShell.showErrorMessage(e); - } - } - - @captureTelemetry(Telemetry.CopySourceCode, undefined, false) - public copyCode(args: ICopyCode) { - this.copyCodeInternal(args.source).catch(err => { - this.applicationShell.showErrorMessage(err); - }); - } - - protected async activating() { - // Only activate if the active editor is empty. This means that - // vscode thinks we are actually supposed to have focus. It would be - // nice if they would more accurrately tell us this, but this works for now. - // Essentially the problem is the webPanel.active state doesn't track - // if the focus is supposed to be in the webPanel or not. It only tracks if - // it's been activated. However if there's no active text editor and we're active, we - // can safely attempt to give ourselves focus. This won't actually give us focus if we aren't - // allowed to have it. - if (this.viewState.active && !this.documentManager.activeTextEditor) { - // Force the webpanel to reveal and take focus. - await super.show(false); - - // Send this to the react control - await this.postMessage(InteractiveWindowMessages.Activate); - } - } - - private shouldAskForRestart(): boolean { - const settings = this.configuration.getSettings(); - return settings && settings.datascience && settings.datascience.askForKernelRestart === true; - } - - private disableAskForRestart() { - const settings = this.configuration.getSettings(); - if (settings && settings.datascience) { - settings.datascience.askForKernelRestart = false; - this.configuration.updateSetting('dataScience.askForKernelRestart', false, undefined, ConfigurationTarget.Global).ignoreErrors(); - } - } - - private addMessage(message: string, type: 'preview' | 'execute') : void { - const cell : ICell = { - id: uuid(), - file: Identifiers.EmptyFileName, - line: 0, - state: CellState.finished, - type, - data: { - cell_type: 'messages', - messages: [message], - source: [], - metadata: {} - } - }; - - // Do the same thing that happens when new code is added. - this.onAddCodeEvent([cell]); - } - - private addPreviewHeader(file: string) : void { - const message = localize.DataScience.previewHeader().format(file); - this.addMessage(message, 'preview'); - } - - private addPreviewFooter(file: string) : void { - const message = localize.DataScience.previewFooter().format(file); - this.addMessage(message, 'preview'); - } - - private async checkPandas() : Promise { - const pandasVersion = await this.dataExplorerProvider.getPandasVersion(); - if (!pandasVersion) { - sendTelemetryEvent(Telemetry.PandasNotInstalled); - // Warn user that there is no pandas. - this.applicationShell.showErrorMessage(localize.DataScience.pandasRequiredForViewing()); - return false; - } else if (pandasVersion.major < 1 && pandasVersion.minor < 20) { - sendTelemetryEvent(Telemetry.PandasTooOld); - // Warn user that we cannot start because pandas is too old. - const versionStr = `${pandasVersion.major}.${pandasVersion.minor}.${pandasVersion.build}`; - this.applicationShell.showErrorMessage(localize.DataScience.pandasTooOldForViewingFormat().format(versionStr)); - return false; - } - return true; - } - - private shouldAskForLargeData(): boolean { - const settings = this.configuration.getSettings(); - return settings && settings.datascience && settings.datascience.askForLargeDataFrames === true; - } - - private disableAskForLargeData() { - const settings = this.configuration.getSettings(); - if (settings && settings.datascience) { - settings.datascience.askForLargeDataFrames = false; - this.configuration.updateSetting('dataScience.askForLargeDataFrames', false, undefined, ConfigurationTarget.Global).ignoreErrors(); - } - } - - private async checkColumnSize(columnSize: number) : Promise { - if (columnSize > ColumnWarningSize && this.shouldAskForLargeData()) { - const message = localize.DataScience.tooManyColumnsMessage(); - const yes = localize.DataScience.tooManyColumnsYes(); - const no = localize.DataScience.tooManyColumnsNo(); - const dontAskAgain = localize.DataScience.tooManyColumnsDontAskAgain(); - - const result = await this.applicationShell.showWarningMessage(message, yes, no, dontAskAgain); - if (result === dontAskAgain) { - this.disableAskForLargeData(); - } - return result === yes; - } - return true; - } - - private async showDataViewer(request: IShowDataViewer) : Promise { - try { - if (await this.checkPandas() && await this.checkColumnSize(request.columnSize)) { - await this.dataExplorerProvider.create(request.variableName); - } - } catch (e) { - this.applicationShell.showErrorMessage(e.toString()); - } - } - - // tslint:disable-next-line:no-any - private dispatchMessage(_message: T, payload: any, handler: (args : M[T]) => void) { - const args = payload as M[T]; - handler.bind(this)(args); - } - - // tslint:disable-next-line:no-any - private onAddedSysInfo(sysInfo : IAddedSysInfo) { - // See if this is from us or not. - if (sysInfo.id !== this.id) { - - // Not from us, must come from a different interactive window. Add to our - // own to keep in sync - if (sysInfo.sysInfoCell) { - this.onAddCodeEvent([sysInfo.sysInfoCell]); - } - } - } - - // tslint:disable-next-line:no-any - private onRemoteAddedCode(args: IRemoteAddCode) { - // Make sure this is valid - if (args && args.id && args.file && args.originator !== this.id) { - // Indicate this in our telemetry. - sendTelemetryEvent(Telemetry.RemoteAddCode); - - // Submit this item as new code. - this.submitCode(args.code, args.file, args.line, args.id).ignoreErrors(); - } - } - - private finishOutstandingCells() { - this.unfinishedCells.forEach(c => { - c.state = CellState.error; - this.postMessage(InteractiveWindowMessages.FinishCell, c).ignoreErrors(); - }); - this.unfinishedCells = []; - this.potentiallyUnfinishedStatus.forEach(s => s.dispose()); - this.potentiallyUnfinishedStatus = []; - } - - private async restartKernelInternal(): Promise { - this.restartingKernel = true; - - // First we need to finish all outstanding cells. - this.finishOutstandingCells(); - - // Set our status - const status = this.statusProvider.set(localize.DataScience.restartingKernelStatus()); - - try { - if (this.jupyterServer) { - await this.jupyterServer.restartKernel(this.generateDataScienceExtraSettings().jupyterInterruptTimeout); - await this.addSysInfo(SysInfoReason.Restart); - - // Compute if dark or not. - const knownDark = await this.isDark(); - - // Before we run any cells, update the dark setting - await this.jupyterServer.setMatplotLibStyle(knownDark); - } - } catch (exc) { - // If we get a kernel promise failure, then restarting timed out. Just shutdown and restart the entire server - if (exc instanceof JupyterKernelPromiseFailedError && this.jupyterServer) { - await this.jupyterServer.dispose(); - await this.loadJupyterServer(true); - await this.addSysInfo(SysInfoReason.Restart); - } else { - // Show the error message - this.applicationShell.showErrorMessage(exc); - this.logger.logError(exc); - } - } finally { - status.dispose(); - this.restartingKernel = false; - } - } - - // tslint:disable-next-line:no-any - private handleReturnAllCells(cells: ICell[]) { - // See what we're waiting for. - if (this.waitingForExportCells) { - this.export(cells); - } - } - - private updateContexts(info: IInteractiveWindowInfo | undefined) { - // This should be called by the python interactive window every - // time state changes. We use this opportunity to update our - // extension contexts - 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 (info) { - interactiveCellsContext.set(info.cellCount > 0).catch(); - redoableContext.set(info.redoCount > 0).catch(); - } else { - interactiveCellsContext.set(false).catch(); - redoableContext.set(false).catch(); - } - } - - @captureTelemetry(Telemetry.SubmitCellThroughInput, undefined, false) - // tslint:disable-next-line:no-any - private submitNewCell(info: ISubmitNewCell) { - // If there's any payload, it has the code and the id - if (info && info.code && info.id) { - // Send to ourselves. - this.submitCode(info.code, Identifiers.EmptyFileName, 0, info.id, undefined).ignoreErrors(); - - // Activate the other side, and send as if came from a file - this.interactiveWindowProvider.getOrCreateActive().then(_v => { - this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {code: info.code, file: Identifiers.EmptyFileName, line: 0, id: info.id, originator: this.id}); - }).ignoreErrors(); - } - } - - private async submitCode(code: string, file: string, line: number, id?: string, _editor?: TextEditor, runningStopWatch?: StopWatch, debug?: boolean) : Promise { - this.logger.logInformation(`Submitting code for ${this.id}`); - - // Start a status item - const status = this.setStatus(localize.DataScience.executingCode()); - - // Transmit this submission to all other listeners (in a live share session) - if (!id) { - id = uuid(); - this.shareMessage(InteractiveWindowMessages.RemoteAddCode, {code, file, line, id, originator: this.id}); - } - - // Create a deferred object that will wait until the status is disposed - const finishedAddingCode = createDeferred(); - const actualDispose = status.dispose.bind(status); - status.dispose = () => { - finishedAddingCode.resolve(); - actualDispose(); - }; - - try { - - // Make sure we're loaded first. - try { - this.logger.logInformation('Waiting for jupyter server and web panel ...'); - await this.loadPromise; - } catch (exc) { - // We should dispose ourselves if the load fails. Othewise the user - // updates their install and we just fail again because the load promise is the same. - this.dispose(); - - throw exc; - } - - // Then show our webpanel - await this.show(); - - // Add our sys info if necessary - if (file !== Identifiers.EmptyFileName) { - 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 - if (file !== Identifiers.EmptyFileName) { - await this.jupyterServer.setInitialDirectory(path.dirname(file)); - } - - if (debug) { - // Attach our debugger - await this.jupyterDebugger.startDebugging(this.jupyterServer); - } - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.jupyterServer.executeObservable(code, file, line, id, false); - - // Indicate we executed some code - this.executeEvent.fire(code); - - // Sign up for cell changes - observable.subscribe( - (cells: ICell[]) => { - this.onAddCodeEvent(cells, undefined); - }, - (error) => { - status.dispose(); - if (!(error instanceof CancellationError)) { - this.applicationShell.showErrorMessage(error.toString()); - } - }, - () => { - // Indicate executing until this cell is done. - status.dispose(); - - // Fire a telemetry event if we have a stop watch - this.sendPerceivedCellExecute(runningStopWatch); - }); - - // Wait for the cell to finish - await finishedAddingCode.promise; - traceInfo(`Finished execution for ${id}`); - } - } catch (err) { - status.dispose(); - - const message = localize.DataScience.executingCodeFailure().format(err); - this.applicationShell.showErrorMessage(message); - } finally { - if (debug) { - if (this.jupyterServer) { - await this.jupyterDebugger.stopDebugging(this.jupyterServer); - } - } - } - } - - private sendPerceivedCellExecute(runningStopWatch?: StopWatch) { - if (runningStopWatch) { - if (!InteractiveWindow.sentExecuteCellTelemetry) { - InteractiveWindow.sentExecuteCellTelemetry = true; - sendTelemetryEvent(Telemetry.ExecuteCellPerceivedCold, runningStopWatch.elapsedTime); - } else { - sendTelemetryEvent(Telemetry.ExecuteCellPerceivedWarm, runningStopWatch.elapsedTime); - } - } - } - - private setStatus = (message: string): Disposable => { - const result = this.statusProvider.set(message); - this.potentiallyUnfinishedStatus.push(result); - return result; - } - - private logTelemetry = (event : Telemetry) => { - sendTelemetryEvent(event); - } - - private onAddCodeEvent = (cells: ICell[], editor?: TextEditor) => { - // Send each cell to the other side - cells.forEach((cell: ICell) => { - switch (cell.state) { - case CellState.init: - // Tell the react controls we have a new cell - this.postMessage(InteractiveWindowMessages.StartCell, cell).ignoreErrors(); - - // Keep track of this unfinished cell so if we restart we can finish right away. - this.unfinishedCells.push(cell); - break; - - case CellState.executing: - // Tell the react controls we have an update - this.postMessage(InteractiveWindowMessages.UpdateCell, cell).ignoreErrors(); - break; - - case CellState.error: - case CellState.finished: - // Tell the react controls we're done - this.postMessage(InteractiveWindowMessages.FinishCell, cell).ignoreErrors(); - - // Remove from the list of unfinished cells - this.unfinishedCells = this.unfinishedCells.filter(c => c.id !== cell.id); - break; - - default: - break; // might want to do a progress bar or something - } - }); - - // 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 onInterpreterChanged = () => { - // Update our load promise. We need to restart the jupyter server - this.loadPromise = this.reloadWithNew(); - } - - private async reloadWithNew() : Promise { - const status = this.setStatus(localize.DataScience.startingJupyter()); - try { - // Not the same as reload, we need to actually dispose the server. - if (this.loadPromise) { - await this.loadPromise; - if (this.jupyterServer) { - const server = this.jupyterServer; - this.jupyterServer = undefined; - await server.dispose(); - } - } - await this.load(); - await this.addSysInfo(SysInfoReason.New); - } finally { - status.dispose(); - } - } - - private async reloadAfterShutdown() : Promise { - try { - if (this.loadPromise) { - await this.loadPromise; - if (this.jupyterServer) { - const server = this.jupyterServer; - this.jupyterServer = undefined; - server.shutdown().ignoreErrors(); // Don't care what happens as we're disconnected. - } - } - } catch { - // We just switched from host to guest mode. Don't really care - // if closing the host server kills it. - this.jupyterServer = undefined; - } - return this.load(); - } - - @captureTelemetry(Telemetry.GotoSourceCode, undefined, false) - private gotoCode(args: IGotoCode) { - this.gotoCodeInternal(args.file, args.line).catch(err => { - this.applicationShell.showErrorMessage(err); - }); - } - - private async gotoCodeInternal(file: string, line: number) { - let editor: TextEditor | undefined; - - if (await 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)); - } - } - - private async copyCodeInternal(source: string) { - let editor = this.documentManager.activeTextEditor; - if (!editor || editor.document.languageId !== PYTHON_LANGUAGE) { - // Find the first visible python editor - const pythonEditors = this.documentManager.visibleTextEditors.filter( - e => e.document.languageId === PYTHON_LANGUAGE || e.document.isUntitled); - - if (pythonEditors.length > 0) { - editor = pythonEditors[0]; - } - } - if (editor && (editor.document.languageId === PYTHON_LANGUAGE || editor.document.isUntitled)) { - // Figure out if any cells in this document already. - const ranges = generateCellRanges(editor.document, this.generateDataScienceExtraSettings()); - const hasCellsAlready = ranges.length > 0; - const line = editor.selection.start.line; - const revealLine = line + 1; - let newCode = `${source}${os.EOL}`; - if (hasCellsAlready) { - // See if inside of a range or not. - const matchingRange = ranges.find(r => r.range.start.line <= line && r.range.end.line >= line); - - // If in the middle, wrap the new code - if (matchingRange && matchingRange.range.start.line < line && line < editor.document.lineCount - 1) { - newCode = `#%%${os.EOL}${source}${os.EOL}#%%${os.EOL}`; - } else { - newCode = `#%%${os.EOL}${source}${os.EOL}`; - } - } else if (editor.document.lineCount <= 0 || editor.document.isUntitled) { - // No lines in the document at all, just insert new code - newCode = `#%%${os.EOL}${source}${os.EOL}`; - } - - await editor.edit((editBuilder) => { - editBuilder.insert(new Position(line, 0), newCode); - }); - editor.revealRange(new Range(revealLine, 0, revealLine + source.split('\n').length + 3, 0)); - - // Move selection to just beyond the text we input so that the next - // paste will be right after - const selectionLine = line + newCode.split('\n').length - 1; - editor.selection = new Selection(new Position(selectionLine, 0), new Position(selectionLine, 0)); - } - } - - @captureTelemetry(Telemetry.ExportNotebook, undefined, false) - // tslint:disable-next-line: no-any no-empty - private export(cells: ICell[]) { - // Should be an array of cells - if (cells && this.applicationShell) { - - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject: Record = {}; - 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 showInformationMessage(message: string, question?: string) : Thenable { - if (question) { - return this.applicationShell.showInformationMessage(message, question); - } else { - return this.applicationShell.showInformationMessage(message); - } - } - - 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' }); - const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; - this.showInformationMessage(localize.DataScience.exportDialogComplete().format(file), openQuestion).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 async loadJupyterServer(_restart?: boolean): Promise { - this.logger.logInformation('Getting jupyter server options ...'); - - // Wait for the webpanel to pass back our current theme darkness - const knownDark = await this.isDark(); - - // Extract our options - const options = await this.interactiveWindowProvider.getNotebookOptions(); - - this.logger.logInformation('Connecting to jupyter server ...'); - - // Now try to create a notebook server - this.jupyterServer = await this.jupyterExecution.connectToNotebookServer(options); - - // Enable debugging support if set - if (this.jupyterServer) { - await this.jupyterDebugger.enableAttach(this.jupyterServer); - } - - // Before we run any cells, update the dark setting - if (this.jupyterServer) { - await this.jupyterServer.setMatplotLibStyle(knownDark); - } - - this.logger.logInformation('Connected to jupyter server.'); - } - - private generateSysInfoCell = async (reason: SysInfoReason): Promise => { - // Execute the code 'import sys\r\nsys.version' and 'import sys\r\nsys.executable' to get our - // version and executable - if (this.jupyterServer) { - const message = await this.generateSysInfoMessage(reason); - - // The server handles getting this data. - const sysInfo = await this.jupyterServer.getSysInfo(); - if (sysInfo) { - // Connection string only for our initial start, not restart or interrupt - let connectionString: string = ''; - if (reason === SysInfoReason.Start) { - connectionString = this.generateConnectionInfoString(this.jupyterServer.getConnectionInfo()); - } - - // Update our sys info with our locally applied data. - const cell = sysInfo.data as IMessageCell; - if (cell) { - cell.messages.unshift(message); - if (connectionString && connectionString.length) { - cell.messages.unshift(connectionString); - } - } - - return sysInfo; - } - } - } - - private async generateSysInfoMessage(reason: SysInfoReason): Promise { - switch (reason) { - case SysInfoReason.Start: - // 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; - case SysInfoReason.New: - return localize.DataScience.pythonNewHeader(); - 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 => { - if (!this.addSysInfoPromise || reason !== SysInfoReason.Start) { - this.logger.logInformation(`Adding sys info for ${this.id} ${reason}`); - const deferred = createDeferred(); - this.addSysInfoPromise = deferred; - - // Generate a new sys info cell and send it to the web panel. - const sysInfo = await this.generateSysInfoCell(reason); - if (sysInfo) { - this.onAddCodeEvent([sysInfo]); - } - - // For anything but start, tell the other sides of a live share session - if (reason !== SysInfoReason.Start && sysInfo) { - this.shareMessage(InteractiveWindowMessages.AddedSysInfo, { type: reason, sysInfoCell: sysInfo, id: this.id }); - } - - // For a restart, tell our window to reset - if (reason === SysInfoReason.Restart || reason === SysInfoReason.New) { - this.postMessage(InteractiveWindowMessages.RestartKernel).ignoreErrors(); - } - - this.logger.logInformation(`Sys info for ${this.id} ${reason} complete`); - deferred.resolve(true); - } else if (this.addSysInfoPromise) { - this.logger.logInformation(`Wait for sys info for ${this.id} ${reason}`); - await this.addSysInfoPromise.promise; - } - } - - private async checkUsable() : Promise { - let activeInterpreter : PythonInterpreter | undefined; - try { - activeInterpreter = await this.interpreterService.getActiveInterpreter(); - const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); - if (usableInterpreter) { - // See if the usable interpreter is not our active one. If so, show a warning - // Only do this if not the guest in a liveshare session - const api = await this.liveShare.getApi(); - if (!api || (api.session && api.session.role !== vsls.Role.Guest)) { - 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)); - } - } - } - - return usableInterpreter ? true : false; - - } catch (e) { - // Can't find a usable interpreter, show the error. - if (activeInterpreter) { - const displayName = activeInterpreter.displayName ? activeInterpreter.displayName : activeInterpreter.path; - throw new Error(localize.DataScience.jupyterNotSupportedBecauseOfEnvironment().format(displayName, e.toString())); - } else { - throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); - } - } - } - - private load = async (): Promise => { - // Status depends upon if we're about to connect to existing server or not. - const status = (await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions())) ? - this.setStatus(localize.DataScience.connectingToJupyter()) : this.setStatus(localize.DataScience.startingJupyter()); - - // Check to see if we support ipykernel or not - try { - const usable = await this.checkUsable(); - if (!usable) { - // Not loading anymore - status.dispose(); - - // Indicate failing. - throw new JupyterInstallError(localize.DataScience.jupyterNotSupported(), localize.DataScience.pythonInteractiveHelpLink()); - } - - // Then load the jupyter server - await this.loadJupyterServer(); - - } catch (e) { - if (e instanceof JupyterSelfCertsError) { - // On a self cert error, warn the user and ask if they want to change the setting - const enableOption: string = localize.DataScience.jupyterSelfCertEnable(); - const closeOption: string = localize.DataScience.jupyterSelfCertClose(); - this.applicationShell.showErrorMessage(localize.DataScience.jupyterSelfCertFail().format(e.message), enableOption, closeOption).then(value => { - if (value === enableOption) { - sendTelemetryEvent(Telemetry.SelfCertsMessageEnabled); - this.configuration.updateSetting('dataScience.allowUnauthorizedRemoteConnection', true, undefined, ConfigurationTarget.Workspace).ignoreErrors(); - } else if (value === closeOption) { - sendTelemetryEvent(Telemetry.SelfCertsMessageClose); - } - // Don't leave our Interactive Window open in a non-connected state - this.dispose(); - }); - throw e; - } else { - throw e; - } - } finally { - status.dispose(); - } - } - - private async requestVariables(requestExecutionCount: number): Promise { - this.variableRequestStopWatch = new StopWatch(); - - // Request our new list of variables - const vars: IJupyterVariable[] = await this.jupyterVariables.getVariables(); - const variablesResponse: IJupyterVariablesResponse = {executionCount: requestExecutionCount, variables: vars }; - - // Tag all of our jupyter variables with the execution count of the request - variablesResponse.variables.forEach((value: IJupyterVariable) => { - value.executionCount = requestExecutionCount; - }); - - const settings = this.configuration.getSettings(); - const excludeString = settings.datascience.variableExplorerExclude; - - if (excludeString) { - const excludeArray = excludeString.split(';'); - variablesResponse.variables = variablesResponse.variables.filter((value) => { - return excludeArray.indexOf(value.type) === -1; - }); - } - this.variableRequestPendingCount = variablesResponse.variables.length; - this.postMessage(InteractiveWindowMessages.GetVariablesResponse, variablesResponse).ignoreErrors(); - sendTelemetryEvent(Telemetry.VariableExplorerVariableCount, undefined, { variableCount: variablesResponse.variables.length }); - } - - // tslint:disable-next-line: no-any - private async requestVariableValue(payload?: any): Promise { - if (payload) { - const targetVar = payload as IJupyterVariable; - // Request our variable value - const varValue: IJupyterVariable = await this.jupyterVariables.getValue(targetVar); - this.postMessage(InteractiveWindowMessages.GetVariableValueResponse, varValue).ignoreErrors(); - - // Send our fetch time if appropriate. - if (this.variableRequestPendingCount === 1 && this.variableRequestStopWatch) { - this.variableRequestPendingCount -= 1; - sendTelemetryEvent(Telemetry.VariableExplorerFetchTime, this.variableRequestStopWatch.elapsedTime); - this.variableRequestStopWatch = undefined; - } else { - this.variableRequestPendingCount = Math.max(0, this.variableRequestPendingCount - 1); - } - - } - } - - // tslint:disable-next-line: no-any - private variableExplorerToggle = (payload?: any) => { - // Direct undefined check as false boolean will skip code - if (payload !== undefined) { - const openValue = payload as boolean; - - // Log the state in our Telemetry - sendTelemetryEvent(Telemetry.VariableExplorerToggled, undefined, { open: openValue }); - } - } - - private requestTmLanguage() { - // Get the contents of the appropriate tmLanguage file. - traceInfo('Request for tmlanguage file.'); - this.themeFinder.findTmLanguage(PYTHON_LANGUAGE).then(s => { - this.postMessage(InteractiveWindowMessages.LoadTmLanguageResponse, s).ignoreErrors(); - }).catch(_e => { - this.postMessage(InteractiveWindowMessages.LoadTmLanguageResponse, undefined).ignoreErrors(); - }); - } - - private async requestOnigasm() : Promise { - // Look for the file next or our current file (this is where it's installed in the vsix) - let filePath = path.join(__dirname, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); - traceInfo(`Request for onigasm file at ${filePath}`); - if (this.fileSystem) { - if (await this.fileSystem.fileExists(filePath)) { - const contents = await fs.readFile(filePath); - this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, contents).ignoreErrors(); - } else { - // During development it's actually in the node_modules folder - filePath = path.join(EXTENSION_ROOT_DIR, 'node_modules', 'onigasm', 'lib', 'onigasm.wasm'); - traceInfo(`Backup request for onigasm file at ${filePath}`); - if (await this.fileSystem.fileExists(filePath)) { - const contents = await fs.readFile(filePath); - this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, contents).ignoreErrors(); - } else { - traceWarning('Onigasm file not found. Colorization will not be available.'); - this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, undefined).ignoreErrors(); - } - } - } else { - // This happens during testing. Onigasm not needed as we're not testing colorization. - traceWarning('File system not found. Colorization will not be available.'); - this.postMessage(InteractiveWindowMessages.LoadOnigasmAssemblyResponse, undefined).ignoreErrors(); - } - } -} diff --git a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts b/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts deleted file mode 100644 index 723b19930ab7..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindowCommandListener.ts +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as uuid from 'uuid/v4'; -import { 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 { traceError } from '../../common/logger'; -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 '../../testing/common/constants'; -import { generateCellRanges, generateCellsFromDocument } from '../cellFactory'; -import { Commands, Telemetry } from '../constants'; -import { - IDataScienceCommandListener, - IInteractiveWindowProvider, - IJupyterExecution, - INotebookExporter, - INotebookImporter, - INotebookServer, - IStatusProvider -} from '../types'; - -@injectable() -export class InteractiveWindowCommandListener implements IDataScienceCommandListener { - constructor( - @inject(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider, - @inject(INotebookExporter) private jupyterExporter: INotebookExporter, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(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, - @inject(INotebookImporter) private jupyterImporter : INotebookImporter - ) { - // 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.showInteractiveWindow()); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand(Commands.ImportNotebook, (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file && file.fsPath) { - return this.importNotebookOnFile(file.fsPath, true); - } else { - return this.importNotebook(); - } - }); - }); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand(Commands.ExportFileAsNotebook, (file?: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file && file.fsPath) { - return this.exportFile(file.fsPath); - } else { - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { - return this.exportFile(activeEditor.document.fileName); - } - } - - return Promise.resolve(); - }); - }); - this.disposableRegistry.push(disposable); - disposable = commandManager.registerCommand(Commands.ExportFileAndOutputAsNotebook, (file: Uri, _cmdSource: CommandSource = CommandSource.commandPalette) => { - return this.listenForErrors(() => { - if (file && file.fsPath) { - return this.exportFileAndOutput(file.fsPath); - } else { - const activeEditor = this.documentManager.activeTextEditor; - if (activeEditor && activeEditor.document.languageId === PYTHON_LANGUAGE) { - return this.exportFileAndOutput(activeEditor.document.fileName); - } - } - return Promise.resolve(); - }); - }); - this.disposableRegistry.push(disposable); - this.disposableRegistry.push(commandManager.registerCommand(Commands.UndoCells, () => this.undoCells())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.RedoCells, () => this.redoCells())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.RemoveAllCells, () => this.removeAllCells())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.InterruptKernel, () => this.interruptKernel())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.RestartKernel, () => this.restartKernel())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.ExpandAllCells, () => this.expandAllCells())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.CollapseAllCells, () => this.collapseAllCells())); - this.disposableRegistry.push(commandManager.registerCommand(Commands.ExportOutputAsNotebook, () => this.exportCells())); - } - - // tslint:disable:no-any - private async listenForErrors(promise: () => Promise) : Promise { - let result: any; - try { - result = await promise(); - return result; - } catch (err) { - if (!(err instanceof CancellationError)) { - if (err.message) { - 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'); - } - } - return result; - } - - private showInformationMessage(message: string, question?: string) : Thenable { - if (question) { - return this.applicationShell.showInformationMessage(message, question); - } else { - return this.applicationShell.showInformationMessage(message); - } - } - - @captureTelemetry(Telemetry.ExportPythonFile, undefined, false) - private async exportFile(file: string): Promise { - 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, this.configuration.getSettings().datascience); - if (cells) { - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject: { [name: string]: string[] } = {}; - filtersObject[filtersKey] = ['ipynb']; - - // Bring up the save file dialog box - const uri = await this.applicationShell.showSaveDialog({ - saveLabel: localize.DataScience.exportDialogTitle(), - filters: filtersObject - }); - - await this.waitForStatus(async () => { - if (uri) { - let directoryChange; - const settings = this.configuration.getSettings(); - if (settings.datascience.changeDirOnImportExport) { - directoryChange = uri.fsPath; - } - - const notebook = await this.jupyterExporter.translateToNotebook(cells, directoryChange); - await this.fileSystem.writeFile(uri.fsPath, JSON.stringify(notebook)); - } - }, localize.DataScience.exportingFormat(), file); - - // When all done, show a notice that it completed. - const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; - if (uri && uri.fsPath) { - this.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, undefined, false) - private async exportFileAndOutput(file: string): Promise { - 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 && activeEditor.document && 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.showInformationMessage(localize.DataScience.exportDialogFailed().format(err)); - } - } - return Promise.resolve(); - }, localize.DataScience.exportingFormat(), file, () => { - cancelSource.cancel(); - }, true); - - // When all done, show a notice that it completed. - const openQuestion = (await this.jupyterExecution.isSpawnSupported()) ? localize.DataScience.exportOpenQuestion() : undefined; - this.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(); - } - }); - - return Uri.file(output); - } - } - } - } else { - this.applicationShell.showErrorMessage(localize.DataScience.jupyterNotSupported()); - } - } - - private async exportCellsWithOutput(ranges: {range: Range; title: string}[], document: TextDocument, file: string, cancelToken: CancellationToken) : Promise { - let server: INotebookServer | undefined; - try { - const settings = this.configuration.getSettings(); - const useDefaultConfig : boolean | undefined = settings.datascience.useDefaultConfigForJupyter; - - // Try starting a server. Purpose should be unique so we - // create a brand new one. - server = await this.jupyterExecution.connectToNotebookServer({ useDefaultConfig, purpose: uuid()}, 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, uuid(), cancelToken) : []; - }))); - - // Then save them to the file - let directoryChange; - if (settings.datascience.changeDirOnImportExport) { - directoryChange = file; - } - - const notebook = await this.jupyterExporter.translateToNotebook(cells, directoryChange); - await this.fileSystem.writeFile(file, JSON.stringify(notebook)); - - } finally { - if (server) { - await server.dispose(); - } - } - } - - private async showExportDialog() : Promise { - const filtersKey = localize.DataScience.exportDialogFilter(); - const filtersObject: { [name: string]: string[] } = {}; - filtersObject[filtersKey] = ['ipynb']; - - // Bring up the save file dialog box - const uri = await this.applicationShell.showSaveDialog({ - saveLabel: localize.DataScience.exportDialogTitle(), - filters: filtersObject - }); - - return uri ? uri.fsPath : undefined; - } - - private undoCells() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.undoCells(); - } - } - - private redoCells() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.redoCells(); - } - } - - private removeAllCells() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.removeAllCells(); - } - } - - private interruptKernel() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.interruptKernel().ignoreErrors(); - } - } - - private restartKernel() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.restartKernel().ignoreErrors(); - } - } - - private expandAllCells() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.expandAllCells(); - } - } - - private collapseAllCells() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.collapseAllCells(); - } - } - - private exportCells() { - const interactiveWindow = this.interactiveWindowProvider.getActive(); - if (interactiveWindow) { - interactiveWindow.exportCells(); - } - } - - private canImportFromOpenedFile = () => { - const settings = this.configuration.getSettings(); - return settings && (!settings.datascience || settings.datascience.allowImportFromNotebook); - } - - private autoPreviewNotebooks = () => { - const settings = this.configuration.getSettings(); - return settings && (!settings.datascience || settings.datascience.autoPreviewNotebooksInInteractivePane); - } - - private disableImportOnOpenedFile = () => { - const settings = this.configuration.getSettings(); - if (settings && settings.datascience) { - settings.datascience.allowImportFromNotebook = false; - } - } - - private onOpenedDocument = async (document: TextDocument) => { - // Preview and import the document if necessary. - const results = await Promise.all([this.previewNotebook(document.fileName), this.askForImportDocument(document)]); - - // When done, make sure the current document is still the active editor if we did - // not do an import. Otherwise subsequent opens will cover up the interactive pane. - if (!results[1] && results[0]) { - this.documentManager.showTextDocument(document); - } - } - - private async previewNotebook(fileName: string) : Promise { - if (fileName && fileName.endsWith('.ipynb') && this.autoPreviewNotebooks()) { - // Get history before putting up status so that we show a busy message when we - // start the preview. - const interactiveWindow = await this.interactiveWindowProvider.getOrCreateActive(); - - // Wait for the preview. - await this.waitForStatus(async () => { - await interactiveWindow.previewNotebook(fileName); - }, localize.DataScience.previewStatusMessage(), fileName); - - return true; - } - - return false; - } - - private async askForImportDocument(document: TextDocument) : Promise { - 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, false); - return true; - } else if (answer === dontAskAgain) { - this.disableImportOnOpenedFile(); - } - } catch (err) { - this.applicationShell.showErrorMessage(err); - } - } - - return false; - } - - @captureTelemetry(Telemetry.ShowHistoryPane, undefined, false) - private async showInteractiveWindow() : Promise{ - const active = await this.interactiveWindowProvider.getOrCreateActive(); - return active.show(); - } - - private waitForStatus(promise: () => Promise, format: string, file?: string, canceled?: () => void, skipHistory?: boolean) : Promise { - const message = file ? format.format(file) : format; - return this.statusProvider.waitWithStatus(promise, message, undefined, canceled, skipHistory); - } - - @captureTelemetry(Telemetry.ImportNotebook, { scope: 'command' }, false) - private async importNotebook() : Promise { - const filtersKey = localize.DataScience.importDialogFilter(); - const filtersObject: { [name: string]: string[] } = {}; - filtersObject[filtersKey] = ['ipynb']; - - const uris = await this.applicationShell.showOpenDialog( - { - openLabel: localize.DataScience.importDialogTitle(), - filters: filtersObject - }); - - if (uris && uris.length > 0) { - // Preview a file whenever we import - this.previewNotebook(uris[0].fsPath).catch(traceError); - - // 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, preview: boolean) : Promise { - if (file && file.length > 0) { - // Preview a file whenever we import if not already previewed - if (preview) { - this.previewNotebook(file).catch(traceError); - } - - 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 => { - 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/interactive-window/interactiveWindowMessageListener.ts b/src/client/datascience/interactive-window/interactiveWindowMessageListener.ts deleted file mode 100644 index 9909096e4cd4..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindowMessageListener.ts +++ /dev/null @@ -1,100 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import * as vscode from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi, IWebPanel, IWebPanelMessageListener } from '../../common/application/types'; -import { Identifiers, LiveShare } from '../constants'; -import { PostOffice } from '../liveshare/postOffice'; -import { InteractiveWindowMessages, InteractiveWindowRemoteMessages } from './interactiveWindowTypes'; - -// tslint:disable:no-any - -// This class listens to messages that come from the local Python Interactive window -export class InteractiveWindowMessageListener implements IWebPanelMessageListener { - private postOffice : PostOffice; - private disposedCallback : () => void; - private callback : (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - private interactiveWindowMessages : string[] = []; - - constructor(liveShare: ILiveShareApi, callback: (message: string, payload: any) => void, viewChanged: (panel: IWebPanel) => void, disposed: () => void) { - this.postOffice = new PostOffice(LiveShare.WebPanelMessageService, liveShare, (api, _command, role, args) => this.translateHostArgs(api, role, args)); - - // Save our dispose callback so we remove our interactive window - this.disposedCallback = disposed; - - // Save our local callback so we can handle the non broadcast case(s) - this.callback = callback; - - // Save view changed so we can forward view change events. - this.viewChanged = viewChanged; - - // Remember the list of interactive window messages we registered for - this.interactiveWindowMessages = this.getInteractiveWindowMessages(); - - // We need to register callbacks for all interactive window messages. - this.interactiveWindowMessages.forEach(m => { - this.postOffice.registerCallback(m, (a) => callback(m, a)).ignoreErrors(); - }); - } - - public async dispose() { - await this.postOffice.dispose(); - this.disposedCallback(); - } - - public onMessage(message: string, payload: any) { - // We received a message from the local webview. Broadcast it to everybody if it's a remote message - if (InteractiveWindowRemoteMessages.indexOf(message) >= 0) { - this.postOffice.postCommand(message, payload).ignoreErrors(); - } else { - // Send to just our local callback. - this.callback(message, payload); - } - } - - public onChangeViewState(panel: IWebPanel) { - // Forward this onto our callback - if (this.viewChanged) { - this.viewChanged(panel); - } - } - - private getInteractiveWindowMessages() : string [] { - return Object.keys(InteractiveWindowMessages).map(k => (InteractiveWindowMessages as any)[k].toString()); - } - - private translateHostArgs(api: vsls.LiveShare | null, role: vsls.Role, args: any[]) { - // Figure out the true type of the args - if (api && args && args.length > 0) { - const trueArg = args[0]; - - // See if the trueArg has a 'file' name or not - if (trueArg) { - const keys = Object.keys(trueArg); - keys.forEach(k => { - if (k.includes('file')) { - if (typeof trueArg[k] === 'string') { - // Pull out the string. We need to convert it to a file or vsls uri based on our role - const file = trueArg[k].toString(); - - // Skip the empty file - if (file !== Identifiers.EmptyFileName) { - const uri = role === vsls.Role.Host ? vscode.Uri.file(file) : vscode.Uri.parse(`vsls:${file}`); - - // Translate this into the other side. - trueArg[k] = role === vsls.Role.Host ? - api.convertLocalUriToShared(uri).fsPath : - api.convertSharedUriToLocal(uri).fsPath; - } - } - } - }); - } - } - } -} diff --git a/src/client/datascience/interactive-window/interactiveWindowProvider.ts b/src/client/datascience/interactive-window/interactiveWindowProvider.ts deleted file mode 100644 index f546bbd799d8..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindowProvider.ts +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import * as uuid from 'uuid/v4'; -import { Disposable, Event, EventEmitter } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi } from '../../common/application/types'; -import { IAsyncDisposable, IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import * as localize from '../../common/utils/localize'; -import { IServiceContainer } from '../../ioc/types'; -import { Identifiers, LiveShare, LiveShareCommands, Settings } from '../constants'; -import { PostOffice } from '../liveshare/postOffice'; -import { IInteractiveWindow, IInteractiveWindowProvider, INotebookServerOptions } from '../types'; - -interface ISyncData { - count: number; - waitable: Deferred; -} - -@injectable() -export class InteractiveWindowProvider implements IInteractiveWindowProvider, IAsyncDisposable { - - private activeInteractiveWindow : IInteractiveWindow | undefined; - private postOffice : PostOffice; - private id: string; - private pendingSyncs : Map = new Map(); - private executedCode: EventEmitter = new EventEmitter(); - private activeInteractiveWindowExecuteHandler: Disposable | undefined; - constructor( - @inject(ILiveShareApi) liveShare: ILiveShareApi, - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry, - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(IConfigurationService) private configService: IConfigurationService - ) { - asyncRegistry.push(this); - - // Create a post office so we can make sure interactive windows are created at the same time - // on both sides. - this.postOffice = new PostOffice(LiveShare.InteractiveWindowProviderService, liveShare); - - // Listen for peer changes - this.postOffice.peerCountChanged((n) => this.onPeerCountChanged(n)); - - // Listen for messages so we force a create on both sides. - this.postOffice.registerCallback(LiveShareCommands.interactiveWindowCreate, this.onRemoteCreate, this).ignoreErrors(); - this.postOffice.registerCallback(LiveShareCommands.interactiveWindowCreateSync, this.onRemoteSync, this).ignoreErrors(); - - // Make a unique id so we can tell who sends a message - this.id = uuid(); - } - - public getActive() : IInteractiveWindow | undefined { - return this.activeInteractiveWindow; - } - - public get onExecutedCode() : Event { - return this.executedCode.event; - } - - public async getOrCreateActive() : Promise { - if (!this.activeInteractiveWindow) { - await this.create(); - } - - // Make sure all other providers have an active interactive window. - await this.synchronizeCreate(); - - // Now that all of our peers have sync'd, return the interactive window to use. - if (this.activeInteractiveWindow) { - return this.activeInteractiveWindow; - } - - throw new Error(localize.DataScience.pythonInteractiveCreateFailed()); - } - - public async getNotebookOptions() : Promise { - // Find the settings that we are going to launch our server with - const settings = this.configService.getSettings(); - let serverURI: string | undefined = settings.datascience.jupyterServerURI; - const useDefaultConfig: boolean | undefined = settings.datascience.useDefaultConfigForJupyter; - - // For the local case pass in our URI as undefined, that way connect doesn't have to check the setting - if (serverURI === Settings.JupyterServerLocalLaunch) { - serverURI = undefined; - } - - return { - enableDebugging: true, - uri: serverURI, - useDefaultConfig, - purpose: Identifiers.HistoryPurpose - }; - } - - public dispose() : Promise { - return this.postOffice.dispose(); - } - - private async create() : Promise { - // Set it as soon as we create it. The .ctor for the interactive window - // may cause a subclass to talk to the IInteractiveWindowProvider to get the active interactive window. - this.activeInteractiveWindow = this.serviceContainer.get(IInteractiveWindow); - const handler = this.activeInteractiveWindow.closed(this.onInteractiveWindowClosed); - this.disposables.push(this.activeInteractiveWindow); - this.disposables.push(handler); - this.activeInteractiveWindowExecuteHandler = this.activeInteractiveWindow.onExecutedCode(this.onInteractiveWindowExecute); - this.disposables.push(this.activeInteractiveWindowExecuteHandler); - await this.activeInteractiveWindow.ready; - } - - private onPeerCountChanged(newCount: number) { - // If we're losing peers, resolve all syncs - if (newCount < this.postOffice.peerCount) { - this.pendingSyncs.forEach(v => v.waitable.resolve()); - this.pendingSyncs.clear(); - } - } - - // tslint:disable-next-line:no-any - private async onRemoteCreate(...args: any[]) { - // Should be a single arg, the originator of the create - if (args.length > 0 && args[0].toString() !== this.id) { - // The other side is creating a interactive window. Create on this side. We don't need to show - // it as the running of new code should do that. - if (!this.activeInteractiveWindow) { - await this.create(); - } - - // Tell the requestor that we got its message (it should be waiting for all peers to sync) - this.postOffice.postCommand(LiveShareCommands.interactiveWindowCreateSync, ...args).ignoreErrors(); - } - } - - // tslint:disable-next-line:no-any - private onRemoteSync(...args: any[]) { - // Should be a single arg, the originator of the create - if (args.length > 1 && args[0].toString() === this.id) { - // Update our pending wait count on the matching pending sync - const key = args[1].toString(); - const sync = this.pendingSyncs.get(key); - if (sync) { - sync.count -= 1; - if (sync.count <= 0) { - sync.waitable.resolve(); - this.pendingSyncs.delete(key); - } - } - } - } - - private onInteractiveWindowClosed = (interactiveWindow: IInteractiveWindow) => { - if (this.activeInteractiveWindow === interactiveWindow) { - this.activeInteractiveWindow = undefined; - if (this.activeInteractiveWindowExecuteHandler) { - this.activeInteractiveWindowExecuteHandler.dispose(); - this.activeInteractiveWindowExecuteHandler = undefined; - } - } - } - - private async synchronizeCreate() : Promise { - // Create a new pending wait if necessary - if (this.postOffice.peerCount > 0 || this.postOffice.role === vsls.Role.Guest) { - const key = uuid(); - const waitable = createDeferred(); - this.pendingSyncs.set(key, { count: this.postOffice.peerCount, waitable }); - - // Make sure all providers have an active interactive window - await this.postOffice.postCommand(LiveShareCommands.interactiveWindowCreate, this.id, key); - - // Wait for the waitable to be signaled or the peer count on the post office to change - await waitable.promise; - } - } - - private onInteractiveWindowExecute = (code: string) => { - this.executedCode.fire(code); - } - -} diff --git a/src/client/datascience/interactive-window/interactiveWindowTypes.ts b/src/client/datascience/interactive-window/interactiveWindowTypes.ts deleted file mode 100644 index bc1edb9d7203..000000000000 --- a/src/client/datascience/interactive-window/interactiveWindowTypes.ts +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; - -import { CssMessages, IGetCssRequest, IGetCssResponse, SharedMessages } from '../messages'; -import { ICell, IInteractiveWindowInfo, IJupyterVariable, IJupyterVariablesResponse } from '../types'; - -export namespace InteractiveWindowMessages { - export const StartCell = 'start_cell'; - export const FinishCell = 'finish_cell'; - export const UpdateCell = 'update_cell'; - export const GotoCodeCell = 'gotocell_code'; - export const CopyCodeCell = 'copycell_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 SubmitNewCell = 'submit_new_cell'; - export const UpdateSettings = SharedMessages.UpdateSettings; - export const SendInfo = 'send_info'; - export const Started = SharedMessages.Started; - export const AddedSysInfo = 'added_sys_info'; - export const RemoteAddCode = 'remote_add_code'; - export const Activate = 'activate'; - export const ShowDataViewer = 'show_data_explorer'; - export const GetVariablesRequest = 'get_variables_request'; - export const GetVariablesResponse = 'get_variables_response'; - export const GetVariableValueRequest = 'get_variable_value_request'; - export const GetVariableValueResponse = 'get_variable_value_response'; - export const VariableExplorerToggle = 'variable_explorer_toggle'; - export const ProvideCompletionItemsRequest = 'provide_completion_items_request'; - export const CancelCompletionItemsRequest = 'cancel_completion_items_request'; - export const ProvideCompletionItemsResponse = 'provide_completion_items_response'; - export const ProvideHoverRequest = 'provide_hover_request'; - export const CancelHoverRequest = 'cancel_hover_request'; - export const ProvideHoverResponse = 'provide_hover_response'; - export const ProvideSignatureHelpRequest = 'provide_signature_help_request'; - export const CancelSignatureHelpRequest = 'cancel_signature_help_request'; - export const ProvideSignatureHelpResponse = 'provide_signature_help_response'; - export const AddCell = 'add_cell'; - export const EditCell = 'edit_cell'; - export const RemoveCell = 'remove_cell'; - export const LoadOnigasmAssemblyRequest = 'load_onigasm_assembly_request'; - export const LoadOnigasmAssemblyResponse = 'load_onigasm_assembly_response'; - export const LoadTmLanguageRequest = 'load_tmlanguage_request'; - export const LoadTmLanguageResponse = 'load_tmlanguage_response'; - export const OpenLink = 'open_link'; - export const ShowPlot = 'show_plot'; - export const StartDebugging = 'start_debugging'; - export const StopDebugging = 'stop_debugging'; -} - -// These are the messages that will mirror'd to guest/hosts in -// a live share session -export const InteractiveWindowRemoteMessages : string[] = [ - InteractiveWindowMessages.AddedSysInfo, - InteractiveWindowMessages.RemoteAddCode -]; - -export interface IGotoCode { - file: string; - line: number; -} - -export interface ICopyCode { - source: string; -} - -export enum SysInfoReason { - Start, - Restart, - Interrupt, - New -} - -export interface IAddedSysInfo { - type: SysInfoReason; - id: string; - sysInfoCell: ICell; -} - -export interface IExecuteInfo { - code: string; - id: string; - file: string; - line: number; -} - -export interface IRemoteAddCode extends IExecuteInfo { - originator: string; -} - -export interface ISubmitNewCell { - code: string; - id: string; -} - -export interface IProvideCompletionItemsRequest { - position: monacoEditor.Position; - context: monacoEditor.languages.CompletionContext; - requestId: string; - cellId: string; -} - -export interface IProvideHoverRequest { - position: monacoEditor.Position; - requestId: string; - cellId: string; -} - -export interface IProvideSignatureHelpRequest { - position: monacoEditor.Position; - context: monacoEditor.languages.SignatureHelpContext; - requestId: string; - cellId: string; -} - -export interface ICancelIntellisenseRequest { - requestId: string; -} - -export interface IProvideCompletionItemsResponse { - list: monacoEditor.languages.CompletionList; - requestId: string; -} - -export interface IProvideHoverResponse { - hover: monacoEditor.languages.Hover; - requestId: string; -} - -export interface IProvideSignatureHelpResponse { - signatureHelp: monacoEditor.languages.SignatureHelp; - requestId: string; -} - -export interface IPosition { - line: number; - ch: number; -} - -export interface IEditCell { - changes: monacoEditor.editor.IModelContentChange[]; - id: string; -} - -export interface IAddCell { - fullText: string; - currentText: string; - file: string; - id: string; -} - -export interface IRemoveCell { - id: string; -} - -export interface IShowDataViewer { - variableName: string; - columnSize: number; -} - -// Map all messages to specific payloads -export class IInteractiveWindowMapping { - public [InteractiveWindowMessages.StartCell]: ICell; - public [InteractiveWindowMessages.FinishCell]: ICell; - public [InteractiveWindowMessages.UpdateCell]: ICell; - public [InteractiveWindowMessages.GotoCodeCell]: IGotoCode; - public [InteractiveWindowMessages.CopyCodeCell]: ICopyCode; - public [InteractiveWindowMessages.RestartKernel]: never | undefined; - public [InteractiveWindowMessages.Export]: ICell[]; - public [InteractiveWindowMessages.GetAllCells]: ICell; - public [InteractiveWindowMessages.ReturnAllCells]: ICell[]; - public [InteractiveWindowMessages.DeleteCell]: never | undefined; - public [InteractiveWindowMessages.DeleteAllCells]: never | undefined; - public [InteractiveWindowMessages.Undo]: never | undefined; - public [InteractiveWindowMessages.Redo]: never | undefined; - public [InteractiveWindowMessages.ExpandAll]: never | undefined; - public [InteractiveWindowMessages.CollapseAll]: never | undefined; - public [InteractiveWindowMessages.StartProgress]: never | undefined; - public [InteractiveWindowMessages.StopProgress]: never | undefined; - public [InteractiveWindowMessages.Interrupt]: never | undefined; - public [InteractiveWindowMessages.UpdateSettings]: string; - public [InteractiveWindowMessages.SubmitNewCell]: ISubmitNewCell; - public [InteractiveWindowMessages.SendInfo]: IInteractiveWindowInfo; - public [InteractiveWindowMessages.Started]: never | undefined; - public [InteractiveWindowMessages.AddedSysInfo]: IAddedSysInfo; - public [InteractiveWindowMessages.RemoteAddCode]: IRemoteAddCode; - public [InteractiveWindowMessages.Activate] : never | undefined; - public [InteractiveWindowMessages.ShowDataViewer]: IShowDataViewer; - public [InteractiveWindowMessages.GetVariablesRequest]: number; - public [InteractiveWindowMessages.GetVariablesResponse]: IJupyterVariablesResponse; - public [InteractiveWindowMessages.GetVariableValueRequest]: IJupyterVariable; - public [InteractiveWindowMessages.GetVariableValueResponse]: IJupyterVariable; - public [InteractiveWindowMessages.VariableExplorerToggle]: boolean; - public [CssMessages.GetCssRequest] : IGetCssRequest; - public [CssMessages.GetCssResponse] : IGetCssResponse; - public [InteractiveWindowMessages.ProvideCompletionItemsRequest] : IProvideCompletionItemsRequest; - public [InteractiveWindowMessages.CancelCompletionItemsRequest] : ICancelIntellisenseRequest; - public [InteractiveWindowMessages.ProvideCompletionItemsResponse] : IProvideCompletionItemsResponse; - public [InteractiveWindowMessages.ProvideHoverRequest] : IProvideHoverRequest; - public [InteractiveWindowMessages.CancelHoverRequest] : ICancelIntellisenseRequest; - public [InteractiveWindowMessages.ProvideHoverResponse] : IProvideHoverResponse; - public [InteractiveWindowMessages.ProvideSignatureHelpRequest] : IProvideSignatureHelpRequest; - public [InteractiveWindowMessages.CancelSignatureHelpRequest] : ICancelIntellisenseRequest; - public [InteractiveWindowMessages.ProvideSignatureHelpResponse] : IProvideSignatureHelpResponse; - public [InteractiveWindowMessages.AddCell] : IAddCell; - public [InteractiveWindowMessages.EditCell] : IEditCell; - public [InteractiveWindowMessages.RemoveCell] : IRemoveCell; - public [InteractiveWindowMessages.LoadOnigasmAssemblyRequest]: never | undefined; - public [InteractiveWindowMessages.LoadOnigasmAssemblyResponse]: Buffer; - public [InteractiveWindowMessages.LoadTmLanguageRequest]: never | undefined; - public [InteractiveWindowMessages.LoadTmLanguageResponse]: string | undefined; - public [InteractiveWindowMessages.OpenLink]: string | undefined; - public [InteractiveWindowMessages.ShowPlot]: string | undefined; - public [InteractiveWindowMessages.StartDebugging]: never | undefined; - public [InteractiveWindowMessages.StopDebugging]: never | undefined; -} diff --git a/src/client/datascience/interactive-window/linkProvider.ts b/src/client/datascience/interactive-window/linkProvider.ts deleted file mode 100644 index c2fdcac28643..000000000000 --- a/src/client/datascience/interactive-window/linkProvider.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter } from 'vscode'; - -import { IApplicationShell } from '../../common/application/types'; -import { noop } from '../../common/utils/misc'; -import { IInteractiveWindowListener } from '../types'; -import { InteractiveWindowMessages } from './interactiveWindowTypes'; - -// tslint:disable: no-any -@injectable() -export class LinkProvider implements IInteractiveWindowListener { - private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>(); - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell) { - noop(); - } - - public get postMessage(): Event<{ message: string; payload: any }> { - return this.postEmitter.event; - } - - public onMessage(message: string, payload?: any): void { - switch (message) { - case InteractiveWindowMessages.OpenLink: - if (payload) { - this.applicationShell.openUrl(payload.toString()); - } - break; - default: - break; - } - } - public dispose(): void | undefined { - noop(); - } -} diff --git a/src/client/datascience/interactive-window/showPlotListener.ts b/src/client/datascience/interactive-window/showPlotListener.ts deleted file mode 100644 index ce1687b339e3..000000000000 --- a/src/client/datascience/interactive-window/showPlotListener.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter } from 'vscode'; - -import { noop } from '../../common/utils/misc'; -import { IInteractiveWindowListener, IPlotViewerProvider } from '../types'; -import { InteractiveWindowMessages } from './interactiveWindowTypes'; - -// tslint:disable: no-any -@injectable() -export class ShowPlotListener implements IInteractiveWindowListener { - private postEmitter: EventEmitter<{message: string; payload: any}> = new EventEmitter<{message: string; payload: any}>(); - constructor(@inject(IPlotViewerProvider) private provider: IPlotViewerProvider) { - noop(); - } - - public get postMessage(): Event<{ message: string; payload: any }> { - return this.postEmitter.event; - } - - public onMessage(message: string, payload?: any): void { - switch (message) { - case InteractiveWindowMessages.ShowPlot: - if (payload) { - this.provider.showPlot(payload).ignoreErrors(); - break; - } - break; - default: - break; - } - } - public dispose(): void | undefined { - noop(); - } -} diff --git a/src/client/datascience/jupyter/jupyterCommand.ts b/src/client/datascience/jupyter/jupyterCommand.ts deleted file mode 100644 index bfd7c38cd18c..000000000000 --- a/src/client/datascience/jupyter/jupyterCommand.ts +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { SpawnOptions } from 'child_process'; -import { inject, injectable } from 'inversify'; - -import { - ExecutionResult, - IProcessService, - IProcessServiceFactory, - IPythonExecutionFactory, - IPythonExecutionService, - ObservableExecutionResult -} from '../../common/process/types'; -import { IEnvironmentActivationService } from '../../interpreter/activation/types'; -import { IInterpreterService, PythonInterpreter } from '../../interpreter/contracts'; -import { IJupyterCommand, IJupyterCommandFactory } from '../types'; - -// JupyterCommand objects represent some process that can be launched that should be guaranteed to work because it -// was found by testing it previously -class ProcessJupyterCommand implements IJupyterCommand { - private exe: string; - private requiredArgs: string[]; - private launcherPromise: Promise; - private interpreterPromise: Promise; - private activationHelper: IEnvironmentActivationService; - - constructor(exe: string, args: string[], processServiceFactory: IProcessServiceFactory, activationHelper: IEnvironmentActivationService, interpreterService: IInterpreterService) { - this.exe = exe; - this.requiredArgs = args; - this.launcherPromise = processServiceFactory.create(); - this.activationHelper = activationHelper; - this.interpreterPromise = interpreterService.getInterpreterDetails(this.exe).catch(_e => undefined); - } - - public interpreter() : Promise { - return this.interpreterPromise; - } - - public async execObservable(args: string[], options: SpawnOptions): Promise> { - const newOptions = { ...options }; - newOptions.env = await this.fixupEnv(newOptions.env); - const launcher = await this.launcherPromise; - const newArgs = [...this.requiredArgs, ...args]; - return launcher.execObservable(this.exe, newArgs, newOptions); - } - - public async exec(args: string[], options: SpawnOptions): Promise> { - const newOptions = { ...options }; - newOptions.env = await this.fixupEnv(newOptions.env); - const launcher = await this.launcherPromise; - const newArgs = [...this.requiredArgs, ...args]; - return launcher.exec(this.exe, newArgs, newOptions); - } - - private fixupEnv(_env: NodeJS.ProcessEnv) : Promise { - if (this.activationHelper) { - return this.activationHelper.getActivatedEnvironmentVariables(undefined); - } - - return Promise.resolve(process.env); - } - -} - -class InterpreterJupyterCommand implements IJupyterCommand { - private requiredArgs: string[]; - private interpreterPromise: Promise; - private pythonLauncher: Promise; - - constructor(args: string[], pythonExecutionFactory: IPythonExecutionFactory, interpreter: PythonInterpreter) { - this.requiredArgs = args; - this.interpreterPromise = Promise.resolve(interpreter); - this.pythonLauncher = pythonExecutionFactory.createActivatedEnvironment({ resource: undefined, interpreter, allowEnvironmentFetchExceptions: true }); - } - - public interpreter() : Promise { - return this.interpreterPromise; - } - - public async execObservable(args: string[], options: SpawnOptions): Promise> { - const newOptions = { ...options }; - const launcher = await this.pythonLauncher; - const newArgs = [...this.requiredArgs, ...args]; - return launcher.execObservable(newArgs, newOptions); - } - - public async exec(args: string[], options: SpawnOptions): Promise> { - const newOptions = { ...options }; - const launcher = await this.pythonLauncher; - const newArgs = [...this.requiredArgs, ...args]; - return launcher.exec(newArgs, newOptions); - } -} - -@injectable() -export class JupyterCommandFactory implements IJupyterCommandFactory { - - constructor( - @inject(IPythonExecutionFactory) private executionFactory: IPythonExecutionFactory, - @inject(IEnvironmentActivationService) private activationHelper : IEnvironmentActivationService, - @inject(IProcessServiceFactory) private processServiceFactory: IProcessServiceFactory, - @inject(IInterpreterService) private interpreterService: IInterpreterService - ) { - - } - - public createInterpreterCommand(args: string[], interpreter: PythonInterpreter): IJupyterCommand { - return new InterpreterJupyterCommand(args, this.executionFactory, interpreter); - } - - public createProcessCommand(exe: string, args: string[]): IJupyterCommand { - return new ProcessJupyterCommand(exe, args, this.processServiceFactory, this.activationHelper, this.interpreterService); - } -} diff --git a/src/client/datascience/jupyter/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 060661b87904..000000000000 --- a/src/client/datascience/jupyter/jupyterConnection.ts +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { ChildProcess } from 'child_process'; -import * as path from 'path'; -import { CancellationToken, Disposable, Event, EventEmitter } from 'vscode'; - -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 { RegExpValues } from '../constants'; -import { IConnection } from '../types'; -import { JupyterConnectError } from './jupyterConnectError'; - -// tslint:disable-next-line:no-require-imports no-var-requires no-any -const namedRegexp = require('named-js-regexp'); -const urlMatcher = namedRegexp(RegExpValues.UrlPatternRegEx); - -export type JupyterServerInfo = { - base_url: string; - notebook_dir: string; - hostname: string; - password: boolean; - pid: number; - port: number; - secure: boolean; - token: string; - url: string; -}; - -class JupyterConnectionWaiter { - private startPromise: Deferred; - private launchTimeout: NodeJS.Timer | number; - private configService: IConfigurationService; - private logger: ILogger; - private fileSystem: IFileSystem; - private notebook_dir: string; - private getServerInfo: (cancelToken?: CancellationToken) => Promise; - private createConnection: (b: string, t: string, p: Disposable) => IConnection; - private launchResult: ObservableExecutionResult; - private cancelToken: CancellationToken | undefined; - private stderr: string[] = []; - private connectionDisposed = false; - - constructor( - launchResult: ObservableExecutionResult, - notebookFile: string, - getServerInfo: (cancelToken?: CancellationToken) => Promise, - createConnection: (b: string, t: string, p: Disposable) => IConnection, - serviceContainer: IServiceContainer, - cancelToken?: CancellationToken - ) { - this.configService = serviceContainer.get(IConfigurationService); - this.logger = serviceContainer.get(ILogger); - this.fileSystem = serviceContainer.get(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(); - - // 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 for crashes - let exitCode = '0'; - if (launchResult.proc) { - launchResult.proc.on('exit', c => (exitCode = c ? c.toString() : '0')); - } - - // Listen on stderr for its connection information - launchResult.out.subscribe( - (output: Output) => { - if (output.source === 'stderr') { - this.stderr.push(output.out); - this.extractConnectionInformation(output.out); - } else { - this.output(output.out); - } - }, - e => this.rejectStartPromise(e.message), - // If the process dies, we can't extract connection information. - () => this.rejectStartPromise(localize.DataScience.jupyterServerCrashed().format(exitCode)) - ); - } - - public waitForConnection(): Promise { - return this.startPromise.promise; - } - - // tslint:disable-next-line:no-any - private output = (data: any) => { - if (this.logger && !this.connectionDisposed) { - 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) { - // tslint:disable-next-line:no-any - const urlMatch = urlMatcher.exec(data) as any; - const groups = urlMatch.groups() as RegExpValues.IUrlPatternGroupType; - if (urlMatch && !this.startPromise.completed && groups && (groups.LOCAL || groups.IP)) { - // Rebuild the URI from our group hits - const host = groups.LOCAL ? groups.LOCAL : groups.IP; - const uriString = `${groups.PREFIX}${host}${groups.REST}`; - - // URL is not being found for some reason. Pull it in forcefully - // tslint:disable-next-line:no-require-imports - const URL = require('url').URL; - let url: URL; - try { - url = new URL(uriString); - } catch (err) { - // Failed to parse the url either via server infos or the string - this.rejectStartPromise(localize.DataScience.jupyterLaunchNoURL()); - return; - } - - // Here we parsed the URL correctly - this.resolveStartPromise(`${url.protocol}//${url.host}${url.pathname}`, `${url.searchParams.get('token')}`); - } - } - - // tslint:disable-next-line:no-any - private extractConnectionInformation = (data: any) => { - this.output(data); - - const httpMatch = RegExpValues.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) => { - // tslint:disable-next-line: no-any - clearTimeout(this.launchTimeout as any); - if (!this.startPromise.rejected) { - const connection = this.createConnection(baseUrl, token, this.launchResult); - const origDispose = connection.dispose.bind(connection); - connection.dispose = () => { - // Stop listening when we disconnect - this.connectionDisposed = true; - return origDispose(); - }; - this.startPromise.resolve(connection); - } - } - - // tslint:disable-next-line:no-any - private rejectStartPromise = (message: string) => { - // tslint:disable-next-line: no-any - clearTimeout(this.launchTimeout as any); - if (!this.startPromise.resolved) { - this.startPromise.reject(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; - public localProcExitCode: number | undefined; - private disposable: Disposable | undefined; - private eventEmitter: EventEmitter = new EventEmitter(); - constructor(baseUrl: string, token: string, disposable: Disposable, childProc: ChildProcess | undefined) { - this.baseUrl = baseUrl; - this.token = token; - this.localLaunch = true; - this.disposable = disposable; - - // If the local process exits, set our exit code and fire our event - if (childProc) { - childProc.on('exit', c => { - this.localProcExitCode = c; - this.eventEmitter.fire(c); - }); - } - } - - public get disconnected(): Event { - return this.eventEmitter.event; - } - - public static waitForConnection( - notebookFile: string, - getServerInfo: (cancelToken?: CancellationToken) => Promise, - notebookExecution: ObservableExecutionResult, - 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, notebookExecution.proc), - serviceContainer, - cancelToken - ); - - return waiter.waitForConnection(); - } - - public dispose() { - if (this.disposable) { - this.disposable.dispose(); - this.disposable = undefined; - } - } -} diff --git a/src/client/datascience/jupyter/jupyterDataRateLimitError.ts b/src/client/datascience/jupyter/jupyterDataRateLimitError.ts deleted file mode 100644 index 7e08688651dd..000000000000 --- a/src/client/datascience/jupyter/jupyterDataRateLimitError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as localize from '../../common/utils/localize'; - -export class JupyterDataRateLimitError extends Error { - constructor() { - super(localize.DataScience.jupyterDataRateExceeded()); - } -} diff --git a/src/client/datascience/jupyter/jupyterDebugger.ts b/src/client/datascience/jupyter/jupyterDebugger.ts deleted file mode 100644 index 283baf709b0c..000000000000 --- a/src/client/datascience/jupyter/jupyterDebugger.ts +++ /dev/null @@ -1,173 +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 { DebugConfiguration, DebugSessionCustomEvent } from 'vscode'; - -import { ICommandManager, IDebugService } from '../../common/application/types'; -import { traceInfo } from '../../common/logger'; -import { IConfigurationService } from '../../common/types'; -import { Deferred } from '../../common/utils/async'; -import { Identifiers } from '../constants'; -import { - CellState, - ICell, - ICellHashProvider, - IDebuggerConnectInfo, - IFileHashes, - IJupyterDebugger, - INotebookServer, - ISourceMapRequest -} from '../types'; - -@injectable() -export class JupyterDebugger implements IJupyterDebugger { - private connectInfo: IDebuggerConnectInfo | undefined; - private pendingSourceMapRequest : Deferred | undefined; - private pendingSourceMapResponseCount: number = 0; - - constructor( - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(ICellHashProvider) private hashProvider: ICellHashProvider, - @inject(ICommandManager) private commandManager: ICommandManager, - @inject(IDebugService) private debugService: IDebugService - ) { - this.debugService.onDidReceiveDebugSessionCustomEvent(this.onCustomEvent.bind(this)); - } - - public async enableAttach(server: INotebookServer): Promise { - traceInfo('enable debugger attach'); - - // Current version of ptvsd doesn't support the source map entries, so we need to have a custom copy - // on disk somewhere. Append this location to our sys path. - // tslint:disable-next-line:no-multiline-string - await this.executeSilently(server, `import sys\r\nsys.path.append('${this.configService.getSettings().datascience.ptvsdDistPath}')`); - // tslint:disable-next-line:no-multiline-string - const enableDebuggerResults = await this.executeSilently(server, `import ptvsd\r\nptvsd.enable_attach(('localhost', 0))`); - - // Save our connection info to this server - this.connectInfo = this.parseConnectInfo(enableDebuggerResults); - } - - public async startDebugging(server: INotebookServer): Promise { - traceInfo('start debugging'); - - if (this.connectInfo) { - // First connect the VSCode UI - const config: DebugConfiguration = { - name: 'IPython', - request: 'attach', - type: 'python', - port: this.connectInfo.port, - host: this.connectInfo.hostName, - justMyCode: true - }; - - await this.debugService.startDebugging(undefined, config); - - // Wait for attach before we turn on tracing and allow the code to run, if the IDE is already attached this is just a no-op - // tslint:disable-next-line:no-multiline-string - await this.executeSilently(server, `import ptvsd\r\nptvsd.wait_for_attach()`); - - // Send our initial set of file mappings - await this.updateDebuggerSourceMaps(); - - // Then enable tracing - // tslint:disable-next-line:no-multiline-string - await this.executeSilently(server, `from ptvsd import tracing\r\ntracing(True)`); - } - } - - public async stopDebugging(server: INotebookServer): Promise { - traceInfo('stop debugging'); - // Disable tracing - // tslint:disable-next-line:no-multiline-string - await this.executeSilently(server, `from ptvsd import tracing\r\ntracing(False)`); - - // Stop our debugging UI session, no await as we just want it stopped - this.commandManager.executeCommand('workbench.action.debug.stop'); - } - - private onCustomEvent(e: DebugSessionCustomEvent) { - // See if we're waiting for the source map event to finish or not - if (this.pendingSourceMapRequest) { - switch (e.event){ - case 'setPydevdSourceMapResponse': - this.pendingSourceMapResponseCount = Math.max(0, this.pendingSourceMapResponseCount - 1); - if (this.pendingSourceMapResponseCount === 0) { - this.pendingSourceMapRequest.resolve(); - } - break; - - default: - break; - } - } - } - - private async updateDebuggerSourceMaps(): Promise { - // Make sure that we have an active debugging session at this point - if (this.debugService.activeDebugSession) { - const fileHashes = this.hashProvider.getHashes(); - - this.pendingSourceMapResponseCount = fileHashes.length; - - fileHashes.forEach(async (fileHash) => { - await this.debugService.activeDebugSession!.customRequest('setPydevdSourceMap', this.buildSourceMap(fileHash)); - }); - } - } - - private buildSourceMap(fileHash: IFileHashes): ISourceMapRequest { - const sourceMapRequest: ISourceMapRequest = { source: { path: fileHash.file }, pydevdSourceMaps: [] }; - - sourceMapRequest.pydevdSourceMaps = fileHash.hashes.map(cellHash => { - return { - line: cellHash.line, - endLine: cellHash.endLine, - runtimeSource: { path: ``}, - runtimeLine: 1 - }; - }); - - return sourceMapRequest; - } - - private executeSilently(server: INotebookServer, code: string): Promise { - return server.execute(code, Identifiers.EmptyFileName, 0, uuid(), undefined, true); - } - - // Pull our connection info out from the cells returned by enable_attach - private parseConnectInfo(cells: ICell[]): IDebuggerConnectInfo | undefined { - if (cells.length > 0) { - let enableAttachString = this.extractOutput(cells[0]); - if (enableAttachString) { - enableAttachString = enableAttachString.trimQuotes(); - - const debugInfoRegEx = /\('(.*?)', ([0-9]*)\)/; - - const debugInfoMatch = debugInfoRegEx.exec(enableAttachString); - if (debugInfoMatch) { - return { hostName: debugInfoMatch[1], port: parseInt(debugInfoMatch[2], 10) }; - } - } - } - return undefined; - } - - private extractOutput(cell: ICell): string | undefined { - if (cell.state === CellState.error || cell.state === CellState.finished) { - const outputs = cell.data.outputs as nbformat.IOutput[]; - if (outputs.length > 0) { - const data = outputs[0].data; - if (data && data.hasOwnProperty('text/plain')) { - // tslint:disable-next-line:no-any - return ((data as any)['text/plain']); - } - } - } - return undefined; - } -} diff --git a/src/client/datascience/jupyter/jupyterExecution.ts b/src/client/datascience/jupyter/jupyterExecution.ts deleted file mode 100644 index c5eb82e1a1ac..000000000000 --- a/src/client/datascience/jupyter/jupyterExecution.ts +++ /dev/null @@ -1,923 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Kernel } from '@jupyterlab/services'; -import { execSync } from 'child_process'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { URL } from 'url'; -import * as uuid from 'uuid/v4'; -import { CancellationToken, Event, EventEmitter } from 'vscode'; - -import { ILiveShareApi, IWorkspaceService } from '../../common/application/types'; -import { Cancellation, CancellationError } from '../../common/cancellation'; -import { traceInfo, traceWarning } from '../../common/logger'; -import { IFileSystem, TemporaryDirectory } from '../../common/platform/types'; -import { IProcessService, IProcessServiceFactory, IPythonExecutionFactory, 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 { StopWatch } from '../../common/utils/stopWatch'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { IInterpreterService, IKnownSearchPathsForInterpreters, PythonInterpreter } from '../../interpreter/contracts'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { JupyterCommands, RegExpValues, Telemetry } from '../constants'; -import { - IConnection, - IJupyterCommand, - IJupyterCommandFactory, - IJupyterExecution, - IJupyterKernelSpec, - IJupyterSessionManager, - INotebookServer, - INotebookServerLaunchInfo, - INotebookServerOptions -} from '../types'; -import { JupyterConnection, JupyterServerInfo } from './jupyterConnection'; -import { JupyterKernelSpec } from './jupyterKernelSpec'; -import { JupyterSelfCertsError } from './jupyterSelfCertsError'; -import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; - -enum ModuleExistsResult { - NotFound, - FoundJupyter, - Found -} - -export class JupyterExecutionBase implements IJupyterExecution { - - private processServicePromise: Promise; - private commands: Record = {}; - private jupyterPath: string | undefined; - private usablePythonInterpreter: PythonInterpreter | undefined; - private eventEmitter: EventEmitter = new EventEmitter(); - - constructor( - _liveShare: ILiveShareApi, - private executionFactory: IPythonExecutionFactory, - private interpreterService: IInterpreterService, - private processServiceFactory: IProcessServiceFactory, - private knownSearchPaths: IKnownSearchPathsForInterpreters, - private logger: ILogger, - private disposableRegistry: IDisposableRegistry, - private asyncRegistry: IAsyncDisposableRegistry, - private fileSystem: IFileSystem, - private sessionManager: IJupyterSessionManager, - workspace: IWorkspaceService, - private configuration: IConfigurationService, - private commandFactory: IJupyterCommandFactory, - 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.onSettingsChanged(); - } - }); - this.disposableRegistry.push(disposable); - } - } - - public get sessionChanged(): Event { - return this.eventEmitter.event; - } - - public dispose(): Promise { - // Clear our usableJupyterInterpreter - this.onSettingsChanged(); - return Promise.resolve(); - } - - public isNotebookSupported(cancelToken?: CancellationToken): Promise { - // See if we can find the command notebook - return Cancellation.race(() => this.isCommandSupported(JupyterCommands.NotebookCommand, cancelToken), cancelToken); - } - - public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { - // Only try to compute this once. - if (!this.usablePythonInterpreter) { - this.usablePythonInterpreter = await Cancellation.race(() => this.getUsableJupyterPythonImpl(cancelToken), cancelToken); - } - return this.usablePythonInterpreter; - } - - public isImportSupported(cancelToken?: CancellationToken): Promise { - // See if we can find the command nbconvert - return Cancellation.race(() => this.isCommandSupported(JupyterCommands.ConvertCommand), cancelToken); - } - - public isKernelCreateSupported(cancelToken?: CancellationToken): Promise { - // See if we can find the command ipykernel - return Cancellation.race(() => this.isCommandSupported(JupyterCommands.KernelCreateCommand), cancelToken); - } - - public isKernelSpecSupported(cancelToken?: CancellationToken): Promise { - // See if we can find the command kernelspec - return Cancellation.race(() => this.isCommandSupported(JupyterCommands.KernelSpecCommand), cancelToken); - } - - public isSpawnSupported(cancelToken?: CancellationToken): Promise { - // Supported if we can run a notebook - return this.isNotebookSupported(cancelToken); - } - - //tslint:disable:cyclomatic-complexity - public connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { - // Return nothing if we cancel - return Cancellation.race(async () => { - let result: INotebookServer | undefined; - let startInfo: { connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined } | undefined; - traceInfo(`Connecting to ${options ? options.purpose : 'unknown type of'} server`); - const interpreter = await this.interpreterService.getActiveInterpreter(); - - // Try to connect to our jupyter process. Check our setting for the number of tries - let tryCount = 0; - const maxTries = this.configuration.getSettings().datascience.jupyterLaunchRetries; - while (tryCount < maxTries) { - try { - // Start or connect to the process - startInfo = await this.startOrConnect(options, cancelToken); - - // Create a server that we will then attempt to connect to. - result = this.serviceContainer.get(INotebookServer); - - // Populate the launch info that we are starting our server with - const launchInfo: INotebookServerLaunchInfo = { - connectionInfo: startInfo.connection, - currentInterpreter: interpreter, - kernelSpec: startInfo.kernelSpec, - workingDir: options ? options.workingDir : undefined, - uri: options ? options.uri : undefined, - purpose: options ? options.purpose : uuid(), - enableDebugging: options ? options.enableDebugging : false - }; - - traceInfo(`Connecting to process for ${options ? options.purpose : 'unknown type of'} server`); - await result.connect(launchInfo, cancelToken); - traceInfo(`Connection complete for ${options ? options.purpose : 'unknown type of'} server`); - - sendTelemetryEvent(launchInfo.uri ? Telemetry.ConnectRemoteJupyter : Telemetry.ConnectLocalJupyter); - return result; - } catch (err) { - // Cleanup after ourselves. server may be running partially. - if (result) { - traceInfo('Killing server because of error'); - await result.dispose(); - } - if (err instanceof JupyterWaitForIdleError && tryCount < maxTries) { - // Special case. This sometimes happens where jupyter doesn't ever connect. Cleanup after - // ourselves and propagate the failure outwards. - traceInfo('Retry because of wait for idle problem.'); - tryCount += 1; - } else if (startInfo) { - // Something else went wrong - if (options && options.uri) { - sendTelemetryEvent(Telemetry.ConnectRemoteFailedJupyter); - - // Check for the self signed certs error specifically - if (err.message.indexOf('reason: self signed certificate') >= 0) { - sendTelemetryEvent(Telemetry.ConnectRemoteSelfCertFailedJupyter); - throw new JupyterSelfCertsError(startInfo.connection.baseUrl); - } else { - throw new Error(localize.DataScience.jupyterNotebookRemoteConnectFailed().format(startInfo.connection.baseUrl, err)); - } - } else { - sendTelemetryEvent(Telemetry.ConnectFailedJupyter); - throw new Error(localize.DataScience.jupyterNotebookConnectFailed().format(startInfo.connection.baseUrl, err)); - } - } else { - throw err; - } - } - } - }, cancelToken); - } - - public async spawnNotebook(file: string): Promise { - // First we find a way to start a notebook server - const notebookCommand = await this.findBestCommandTimed(JupyterCommands.NotebookCommand); - 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 async importNotebook(file: string, template: string | undefined): Promise { - // First we find a way to start a nbconvert - const convert = await this.findBestCommandTimed(JupyterCommands.ConvertCommand); - if (!convert) { - throw new Error(localize.DataScience.jupyterNbConvertNotSupported()); - } - - // Wait for the nbconvert to finish - const args = template ? [file, '--to', 'python', '--stdout', '--template', template] : [file, '--to', 'python', '--stdout']; - const result = await convert.exec(args, { 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; - } - - public getServer(_options?: INotebookServerOptions): Promise { - // This is cached at the host or guest level - return Promise.resolve(undefined); - } - - @captureTelemetry(Telemetry.FindJupyterKernelSpec) - protected async getMatchingKernelSpec(connection?: IConnection, cancelToken?: CancellationToken): Promise { - try { - // 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); - } catch (e) { - // ECONNREFUSED seems to happen here. Log the error, but don't let it bubble out. We don't really need a kernel spec - this.logger.logWarning(e); - - // Double check our jupyter server is still running. - if (connection && connection.localProcExitCode) { - throw new Error(localize.DataScience.jupyterServerCrashed().format(connection.localProcExitCode.toString())); - } - } - } - - private async startOrConnect(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise<{ connection: IConnection; kernelSpec: IJupyterKernelSpec | undefined }> { - let connection: IConnection | undefined; - 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 (!options || !options.uri) { - traceInfo(`Launching ${options ? options.purpose : 'unknown type of'} server`); - const launchResults = await this.startNotebookServer(options && options.useDefaultConfig ? true : false, 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(options.uri); - kernelSpec = undefined; - } - - // If we don't have a kernel spec yet, check using our current connection - if (!kernelSpec && connection.localLaunch) { - traceInfo(`Getting kernel specs for ${options ? options.purpose : 'unknown type of'} server`); - 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 && connection.localLaunch) { - this.logger.logError(localize.DataScience.jupyterKernelSpecNotFound()); - } - - // Return the data we found. - return { connection, kernelSpec }; - } - - 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; - } - const settings = this.configuration.getSettings(); - const allowUnauthorized = settings.datascience.allowUnauthorizedRemoteConnection ? settings.datascience.allowUnauthorizedRemoteConnection : false; - - return { - allowUnauthorized, - baseUrl: `${url.protocol}//${url.host}${url.pathname}`, - token: `${url.searchParams.get('token')}`, - localLaunch: false, - localProcExitCode: undefined, - disconnected: (_l) => { return { dispose: noop }; }, - dispose: noop - }; - } - - // tslint:disable-next-line: max-func-body-length - @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.findBestCommandTimed(JupyterCommands.NotebookCommand, cancelToken); - if (!notebookCommand) { - throw new Error(localize.DataScience.jupyterNotSupported()); - } - - // Now actually launch it - let exitCode = 0; - 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'); - } - - // Modify the data rate limit if starting locally. The default prevents large dataframes from being returned. - extraArgs.push('--NotebookApp.iopub_data_rate_limit=10000000000.0'); - - // Check for a docker situation. - try { - if (await this.fileSystem.fileExists('/proc/self/cgroup')) { - const cgroup = await this.fileSystem.readFile('/proc/self/cgroup'); - if (cgroup.includes('docker')) { - // We definitely need an ip address. - extraArgs.push('--ip'); - extraArgs.push('127.0.0.1'); - - // Now see if we need --allow-root. - const idResults = execSync('id', { encoding: 'utf-8' }); - if (idResults.includes('(root)')) { - extraArgs.push('--allow-root'); - } - } - } - } catch { - noop(); - } - - // 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); - - // Make sure we haven't canceled already. - if (cancelToken && cancelToken.isCancellationRequested) { - throw new CancellationError(); - } - - // Then use this to launch our notebook process. - const stopWatch = new StopWatch(); - const launchResult = await notebookCommand.execObservable(args, { throwOnStdErr: false, encoding: 'utf8', token: cancelToken }); - - // Watch for premature exits - if (launchResult.proc) { - launchResult.proc.on('exit', (c) => exitCode = c); - } - - // 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); - - // Fire off telemetry for the process being talkable - sendTelemetryEvent(Telemetry.StartJupyterProcess, stopWatch.elapsedTime); - - return { - connection: connection, - kernelSpec: kernelSpec - }; - } catch (err) { - if (err instanceof CancellationError) { - throw err; - } - - // Something else went wrong. See if the local proc died or not. - if (exitCode !== 0) { - throw new Error(localize.DataScience.jupyterServerCrashed().format(exitCode.toString())); - } else { - throw new Error(localize.DataScience.jupyterNotebookFailure().format(err)); - } - } - } - - private getUsableJupyterPythonImpl = async (cancelToken?: CancellationToken): Promise => { - // This should be the best interpreter for notebooks - const found = await this.findBestCommandTimed(JupyterCommands.NotebookCommand, cancelToken); - if (found) { - return found.interpreter(); - } - - return undefined; - } - - private getJupyterServerInfo = async (cancelToken?: CancellationToken): Promise => { - // 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 }; - const launcher = await this.executionFactory.createActivatedEnvironment( - { resource: undefined, interpreter: bestInterpreter, allowEnvironmentFetchExceptions: true }); - const file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); - const serverInfoString = await launcher.exec([file], newOptions); - - let serverInfos: JupyterServerInfo[]; - try { - // Parse out our results, return undefined if we can't suss it out - serverInfos = JSON.parse(serverInfoString.stdout.trim()) as JupyterServerInfo[]; - } catch (err) { - return undefined; - } - return serverInfos; - } - - return undefined; - } - - private onSettingsChanged() { - // Clear our usableJupyterInterpreter so that we recompute our values - this.usablePythonInterpreter = undefined; - this.commands = {}; - } - - private async addMatchingSpec(bestInterpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise { - const displayName = localize.DataScience.historyTitle(); - const ipykernelCommand = await this.findBestCommandTimed(JupyterCommands.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 = RegExpValues.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 => { - // Enumerate all specs and get path for the match - const specs = await this.enumerateSpecs(cancelToken); - const match = specs! - .filter(s => s !== undefined) - .find(s => { - const js = s as JupyterKernelSpec; - return js && js.name === specName; - }) as JupyterKernelSpec; - return match ? match.specFile : undefined; - } - - private async generateTempDir(): Promise { - 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 => { - // See if we can find the command - try { - const result = await this.findBestCommandTimed(command, cancelToken); - return result !== undefined; - } catch (err) { - this.logger.logWarning(err); - return false; - } - } - - private hasSpecPathMatch = async (info: PythonInterpreter | undefined, cancelToken?: CancellationToken): Promise => { - 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 => { - // 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; - - // First match on language. No point if not python. - if (spec && spec.language && spec.language.toLocaleLowerCase() === 'python') { - // Language match - score += 1; - - // See if the path matches. Don't bother if the language doesn't. - if (spec && spec.path && spec.path.length > 0 && info && spec.path === info.path) { - // Path match - score += 10; - } - - // 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 { - const match = RegExpValues.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<(JupyterKernelSpec | undefined)[]> => { - if (await this.isKernelSpecSupported()) { - const kernelSpecCommand = await this.findBestCommandTimed(JupyterCommands.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 => { - // If the module is found on this interpreter, then we found it. - if (interpreter && !Cancellation.isCanceled(cancelToken)) { - const exists = await this.doesModuleExist(command, interpreter, cancelToken); - - if (exists === ModuleExistsResult.FoundJupyter) { - return this.commandFactory.createInterpreterCommand(['-m', 'jupyter', command], interpreter); - } else if (exists === ModuleExistsResult.Found) { - return this.commandFactory.createInterpreterCommand(['-m', command], interpreter); - } - } - - return undefined; - } - - private lookForJupyterInDirectory = async (pathToCheck: string): Promise => { - try { - const files = await this.fileSystem.getFiles(pathToCheck); - return files ? files.filter(s => RegExpValues.CheckJupyterRegEx.test(path.basename(s))) : []; - } catch (err) { - this.logger.logWarning('Python Extension (fileSystem.getFiles):', err); - } - return [] as string[]; - } - - private searchPathsForJupyter = async (): Promise => { - 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 => { - if (await this.doesJupyterCommandExist(command, cancelToken) && !Cancellation.isCanceled(cancelToken)) { - // Search the known paths for jupyter - const jupyterPath = await this.searchPathsForJupyter(); - if (jupyterPath) { - return this.commandFactory.createProcessCommand(jupyterPath, [command]); - } - } - return undefined; - } - - private supportsSearchingForCommands(): boolean { - if (this.configuration) { - const settings = this.configuration.getSettings(); - if (settings) { - return settings.datascience.searchForJupyter; - } - } - return true; - } - - private async findBestCommandTimed(command: string, cancelToken?: CancellationToken) : Promise { - // Only log telemetry if not already found (meaning the first time) - let timer: StopWatch | undefined; - if (!this.commands.hasOwnProperty(command)) { - timer = new StopWatch(); - } - try { - return await this.findBestCommand(command, cancelToken); - } finally { - if (timer) { - sendTelemetryEvent(Telemetry.FindJupyterCommand, timer.elapsedTime, { command }); - } - } - } - - // 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 - // tslint:disable:cyclomatic-complexity - private findBestCommand = async (command: string, cancelToken?: CancellationToken): Promise => { - // 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) { - traceInfo(`Active interpreter does not support ${command}. Interpreter is ${current ? current.displayName : '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(); - - if (!all || all.length === 0) { - traceWarning('No interpreters found. Jupyter cannot run.'); - } - - 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 (const entry of foundList) { - let currentScore = 0; - if (!entry) { - continue; - } - const interpreter = await entry.interpreter(); - const version = interpreter ? interpreter.version : undefined; - 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 = entry; - 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 && this.supportsSearchingForCommands()) { - found = await this.findPathCommand(command, cancelToken); - } - - // If we found a command, save in our dictionary - if (found) { - this.commands[command] = found; - } - } - - // Return results - return this.commands.hasOwnProperty(command) ? this.commands[command] : undefined; - } - - private doesModuleExist = async (moduleName: string, interpreter: PythonInterpreter, cancelToken?: CancellationToken): Promise => { - if (interpreter && interpreter !== null) { - const newOptions: SpawnOptions = { throwOnStdErr: true, encoding: 'utf8', token: cancelToken }; - const pythonService = await this.executionFactory.createActivatedEnvironment({ resource: undefined, interpreter, allowEnvironmentFetchExceptions: true }); - - // For commands not 'ipykernel' first try them as jupyter commands - if (moduleName !== JupyterCommands.KernelCreateCommand) { - try { - const result = await pythonService.execModule('jupyter', [moduleName, '--version'], newOptions); - if (!result.stderr) { - return ModuleExistsResult.FoundJupyter; - } else { - this.logger.logWarning(`${result.stderr} for ${interpreter.path}`); - } - } catch (err) { - this.logger.logWarning(`${err} for ${interpreter.path}`); - } - } - - // After trying first as "-m jupyter --version" then try "-m --version" as this works in some cases - // for example if not running in an activated environment without script on the path - try { - const result = await pythonService.execModule(moduleName, ['--version'], newOptions); - if (!result.stderr) { - return ModuleExistsResult.Found; - } else { - this.logger.logWarning(`${result.stderr} for ${interpreter.path}`); - return ModuleExistsResult.NotFound; - } - } catch (err) { - this.logger.logWarning(`${err} for ${interpreter.path}`); - return ModuleExistsResult.NotFound; - } - } else { - this.logger.logWarning(`Interpreter not found. ${moduleName} cannot be loaded.`); - return ModuleExistsResult.NotFound; - } - } - - private doesJupyterCommandExist = async (command?: string, cancelToken?: CancellationToken): Promise => { - 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/jupyterExecutionFactory.ts b/src/client/datascience/jupyter/jupyterExecutionFactory.ts deleted file mode 100644 index f6206a01d5c5..000000000000 --- a/src/client/datascience/jupyter/jupyterExecutionFactory.ts +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { inject, injectable } from 'inversify'; -import { CancellationToken, Event, EventEmitter } from 'vscode'; - -import { ILiveShareApi, IWorkspaceService } from '../../common/application/types'; -import { IFileSystem } from '../../common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory } from '../../common/process/types'; -import { - IAsyncDisposable, - IAsyncDisposableRegistry, - IConfigurationService, - IDisposableRegistry, - ILogger -} from '../../common/types'; -import { IInterpreterService, IKnownSearchPathsForInterpreters, PythonInterpreter } from '../../interpreter/contracts'; -import { IServiceContainer } from '../../ioc/types'; -import { - IJupyterCommandFactory, - IJupyterExecution, - IJupyterSessionManager, - INotebookServer, - INotebookServerOptions -} from '../types'; -import { GuestJupyterExecution } from './liveshare/guestJupyterExecution'; -import { HostJupyterExecution } from './liveshare/hostJupyterExecution'; -import { IRoleBasedObject, RoleBasedFactory } from './liveshare/roleBasedFactory'; - -interface IJupyterExecutionInterface extends IRoleBasedObject, IJupyterExecution { - -} - -// tslint:disable:callable-types -type JupyterExecutionClassType = { - new(liveShare: ILiveShareApi, - executionFactory: IPythonExecutionFactory, - interpreterService: IInterpreterService, - processServiceFactory: IProcessServiceFactory, - knownSearchPaths: IKnownSearchPathsForInterpreters, - logger: ILogger, - disposableRegistry: IDisposableRegistry, - asyncRegistry: IAsyncDisposableRegistry, - fileSystem: IFileSystem, - sessionManager: IJupyterSessionManager, - workspace: IWorkspaceService, - configuration: IConfigurationService, - commandFactory : IJupyterCommandFactory, - serviceContainer: IServiceContainer - ): IJupyterExecutionInterface; -}; -// tslint:enable:callable-types - -@injectable() -export class JupyterExecutionFactory implements IJupyterExecution, IAsyncDisposable { - - private executionFactory: RoleBasedFactory; - private sessionChangedEventEmitter: EventEmitter = new EventEmitter(); - - constructor(@inject(ILiveShareApi) liveShare: ILiveShareApi, - @inject(IPythonExecutionFactory) pythonFactory: IPythonExecutionFactory, - @inject(IInterpreterService) interpreterService: IInterpreterService, - @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, - @inject(IKnownSearchPathsForInterpreters) knownSearchPaths: IKnownSearchPathsForInterpreters, - @inject(ILogger) logger: ILogger, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, - @inject(IFileSystem) fileSystem: IFileSystem, - @inject(IJupyterSessionManager) sessionManager: IJupyterSessionManager, - @inject(IWorkspaceService) workspace: IWorkspaceService, - @inject(IConfigurationService) configuration: IConfigurationService, - @inject(IJupyterCommandFactory) commandFactory : IJupyterCommandFactory, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - asyncRegistry.push(this); - this.executionFactory = new RoleBasedFactory( - liveShare, - HostJupyterExecution, - GuestJupyterExecution, - liveShare, - pythonFactory, - interpreterService, - processServiceFactory, - knownSearchPaths, - logger, - disposableRegistry, - asyncRegistry, - fileSystem, - sessionManager, - workspace, - configuration, - commandFactory, - serviceContainer - ); - this.executionFactory.sessionChanged(() => this.onSessionChanged()); - } - - public get sessionChanged() : Event { - return this.sessionChangedEventEmitter.event; - } - - public async dispose() : Promise { - // Dispose of our execution object - const execution = await this.executionFactory.get(); - return execution.dispose(); - } - - public async isNotebookSupported(cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.isNotebookSupported(cancelToken); - } - public async isImportSupported(cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.isImportSupported(cancelToken); - } - public async isKernelCreateSupported(cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.isKernelCreateSupported(cancelToken); - } - public async isKernelSpecSupported(cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.isKernelSpecSupported(cancelToken); - } - public async isSpawnSupported(cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.isSpawnSupported(cancelToken); - } - public async connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.connectToNotebookServer(options, cancelToken); -} - public async spawnNotebook(file: string): Promise { - const execution = await this.executionFactory.get(); - return execution.spawnNotebook(file); - } - public async importNotebook(file: string, template: string | undefined): Promise { - const execution = await this.executionFactory.get(); - return execution.importNotebook(file, template); - } - public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { - const execution = await this.executionFactory.get(); - return execution.getUsableJupyterPython(cancelToken); - } - public async getServer(options?: INotebookServerOptions) : Promise { - const execution = await this.executionFactory.get(); - return execution.getServer(options); - } - - private onSessionChanged() { - this.sessionChangedEventEmitter.fire(); - } -} diff --git a/src/client/datascience/jupyter/jupyterExporter.ts b/src/client/datascience/jupyter/jupyterExporter.ts deleted file mode 100644 index 8a371c1e2d04..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 os from 'os'; -import * as path from 'path'; -import * as uuid from 'uuid/v4'; - -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem, IPlatformService } from '../../common/platform/types'; -import { IConfigurationService } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { CellMatcher } from '../cellMatcher'; -import { concatMultilineString } from '../common'; -import { CodeSnippits, Identifiers } from '../constants'; -import { CellState, ICell, IJupyterExecution, INotebookExporter } from '../types'; - -@injectable() -export class JupyterExporter implements INotebookExporter { - - constructor( - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IPlatformService) private readonly platform: IPlatformService - ) { - } - - public dispose() { - noop(); - } - - public async translateToNotebook(cells: ICell[], changeDirectory?: string): Promise { - // If requested, add in a change directory cell to fix relative paths - if (changeDirectory) { - cells = await this.addDirectoryChangeCell(cells, changeDirectory); - } - - const pythonNumber = await this.extractPythonMainVersion(); - - // 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 - }; - - // Create an object for matching cell definitions - const matcher = new CellMatcher(this.configService.getSettings().datascience); - - // Combine this into a JSON object - return { - cells: this.pruneCells(cells, matcher), - nbformat: 4, - nbformat_minor: 2, - metadata: metadata - }; - } - - // For exporting, put in a cell that will change the working directory back to the workspace directory so relative data paths will load correctly - private addDirectoryChangeCell = async (cells: ICell[], file: string): Promise => { - const changeDirectory = await this.calculateDirectoryChange(file, cells); - - if (changeDirectory) { - const exportChangeDirectory = CodeSnippits.ChangeDirectory.join(os.EOL).format(localize.DataScience.exportChangeDirectoryComment(), CodeSnippits.ChangeDirectoryCommentIdentifier, changeDirectory); - - const cell: ICell = { - data: { - source: exportChangeDirectory, - cell_type: 'code', - outputs: [], - metadata: {}, - execution_count: 0 - }, - id: uuid(), - file: Identifiers.EmptyFileName, - line: 0, - state: CellState.finished, - type: 'execute' - }; - - return [cell, ...cells]; - } else { - return cells; - } - } - - // When we export we want to our change directory back to the first real file that we saw run from any workspace folder - private firstWorkspaceFolder = async (cells: ICell[]): Promise => { - for (const cell of cells) { - const filename = cell.file; - - // First check that this is an absolute file that exists (we add in temp files to run system cell) - if (path.isAbsolute(filename) && await this.fileSystem.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 => { - // Make sure we don't already have a cell with a ChangeDirectory comment in it. - let directoryChange: string | undefined; - const haveChangeAlready = cells.find(c => concatMultilineString(c.data.source).includes(CodeSnippits.ChangeDirectoryCommentIdentifier)); - if (!haveChangeAlready) { - const notebookFilePath = path.dirname(notebookFile); - // First see if we have a workspace open, this only works if we have a workspace root to be relative to - if (this.workspaceService.hasWorkspaceFolders) { - const workspacePath = await this.firstWorkspaceFolder(cells); - - // Make sure that we have everything that we need here - if (workspacePath && path.isAbsolute(workspacePath) && notebookFilePath && path.isAbsolute(notebookFilePath)) { - directoryChange = path.relative(notebookFilePath, workspacePath); - } - } - } - - // If path.relative can't calculate a relative path, then it just returns the full second path - // so check here, we only want this if we were able to calculate a relative path, no network shares or drives - if (directoryChange && !path.isAbsolute(directoryChange)) { - // Escape windows path chars so they end up in the source escaped - if (this.platform.isWindows) { - directoryChange = directoryChange.replace('\\', '\\\\'); - } - - return directoryChange; - } else { - return undefined; - } - } - - private pruneCells = (cells: ICell[], cellMatcher: CellMatcher): nbformat.IBaseCell[] => { - // First filter out sys info cells. Jupyter doesn't understand these - return cells.filter(c => c.data.cell_type !== 'messages') - // Then prune each cell down to just the cell data. - .map(c => this.pruneCell(c, cellMatcher)); - } - - private pruneCell = (cell: ICell, cellMatcher: CellMatcher): nbformat.IBaseCell => { - // Remove the #%% of the top of the source if there is any. We don't need - // this to end up in the exported ipynb file. - const copy = { ...cell.data }; - copy.source = this.pruneSource(cell.data.source, cellMatcher); - return copy; - } - - private pruneSource = (source: nbformat.MultilineString, cellMatcher: CellMatcher): nbformat.MultilineString => { - - if (Array.isArray(source) && source.length > 0) { - if (cellMatcher.isCell(source[0])) { - return source.slice(1); - } - } else { - const array = source.toString().split('\n').map(s => `${s}\n`); - if (array.length > 0 && cellMatcher.isCell(array[0])) { - return array.slice(1); - } - } - - return source; - } - - private extractPythonMainVersion = async (): Promise => { - // Use the active interpreter - const usableInterpreter = await this.jupyterExecution.getUsableJupyterPython(); - return usableInterpreter && usableInterpreter.version ? usableInterpreter.version.major : 3; - } -} diff --git a/src/client/datascience/jupyter/jupyterImporter.ts b/src/client/datascience/jupyter/jupyterImporter.ts deleted file mode 100644 index 3f2a4015d6c3..000000000000 --- a/src/client/datascience/jupyter/jupyterImporter.ts +++ /dev/null @@ -1,132 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as fs from 'fs-extra'; -import { inject, injectable } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; - -import { IWorkspaceService } from '../../common/application/types'; -import { IFileSystem, IPlatformService } from '../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { CodeSnippits } 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; - - 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, - @inject(IPlatformService) private readonly platform: IPlatformService - ) { - this.templatePromise = this.createTemplateFile(); - } - - public async importFromFile(file: string): Promise { - const template = await this.templatePromise; - - // If the user has requested it, add a cd command to the imported file so that relative paths still work - const settings = this.configuration.getSettings(); - let directoryChange: string | undefined; - if (settings.datascience.changeDirOnImportExport) { - directoryChange = await this.calculateDirectoryChange(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(), CodeSnippits.ChangeDirectoryCommentIdentifier, directoryChange); - return newCode.concat(pythonOutput); - } - - // When importing a file, calculate if we can create a %cd so that the relative paths work - private async calculateDirectoryChange(notebookFile: string): Promise { - let directoryChange: string | undefined; - // Make sure we don't already have an import/export comment in the file - const contents = await this.fileSystem.readFile(notebookFile); - const haveChangeAlready = contents.includes(CodeSnippits.ChangeDirectoryCommentIdentifier); - - if (!haveChangeAlready) { - const notebookFilePath = path.dirname(notebookFile); - // First see if we have a workspace open, this only works if we have a workspace root to be relative to - if (this.workspaceService.hasWorkspaceFolders) { - const workspacePath = this.workspaceService.workspaceFolders![0].uri.fsPath; - - // Make sure that we have everything that we need here - if (workspacePath && path.isAbsolute(workspacePath) && notebookFilePath && path.isAbsolute(notebookFilePath)) { - directoryChange = path.relative(workspacePath, notebookFilePath); - } - } - } - - // If path.relative can't calculate a relative path, then it just returns the full second path - // so check here, we only want this if we were able to calculate a relative path, no network shares or drives - if (directoryChange && !path.isAbsolute(directoryChange)) { - - // Escape windows path chars so they end up in the source escaped - if (this.platform.isWindows) { - directoryChange = directoryChange.replace('\\', '\\\\'); - } - - return directoryChange; - } else { - return undefined; - } - } - - private async createTemplateFile(): Promise { - // Create a temp file on disk - const file = await this.fileSystem.createTemporaryFile('.tpl'); - - // Write our template into it - if (file) { - try { - // Save this file into our disposables so the temp file goes away - this.disposableRegistry.push(file); - await fs.appendFile(file.filePath, this.nbconvertTemplate); - - // Now we should have a template that will convert - return file.filePath; - } catch { - noop(); - } - } - } -} diff --git a/src/client/datascience/jupyter/jupyterInstallError.ts b/src/client/datascience/jupyter/jupyterInstallError.ts deleted file mode 100644 index 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/jupyterInterruptError.ts b/src/client/datascience/jupyter/jupyterInterruptError.ts deleted file mode 100644 index 8189045a6359..000000000000 --- a/src/client/datascience/jupyter/jupyterInterruptError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export class JupyterInterruptError extends Error { - constructor(message: string) { - super(message); - } -} diff --git a/src/client/datascience/jupyter/jupyterKernelPromiseFailedError.ts b/src/client/datascience/jupyter/jupyterKernelPromiseFailedError.ts deleted file mode 100644 index a1727c77877c..000000000000 --- a/src/client/datascience/jupyter/jupyterKernelPromiseFailedError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export class JupyterKernelPromiseFailedError extends Error { - constructor(message: string) { - super(message); - } -} diff --git a/src/client/datascience/jupyter/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/jupyterPasswordConnect.ts b/src/client/datascience/jupyter/jupyterPasswordConnect.ts deleted file mode 100644 index 1b2f1f857791..000000000000 --- a/src/client/datascience/jupyter/jupyterPasswordConnect.ts +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Agent as HttpsAgent } from 'https'; -import { inject, injectable } from 'inversify'; -import * as nodeFetch from 'node-fetch'; -import { URLSearchParams } from 'url'; -import { IApplicationShell } from '../../common/application/types'; -import * as localize from '../../common/utils/localize'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { IJupyterPasswordConnect, IJupyterPasswordConnectInfo } from '../types'; -import { Telemetry } from './../constants'; - -@injectable() -export class JupyterPasswordConnect implements IJupyterPasswordConnect { - - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell - ) { - } - - @captureTelemetry(Telemetry.GetPasswordAttempt) - public async getPasswordConnectionInfo(url: string, allowUnauthorized: boolean, fetchFunction?: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise { - // For testing allow for our fetch function to be overridden - if (!fetchFunction) { - fetchFunction = nodeFetch.default; - } - - let xsrfCookie: string | undefined; - let sessionCookieName: string | undefined; - let sessionCookieValue: string | undefined; - - if (!url || url.length < 1) { - return undefined; - } - - // Add on a trailing slash to our URL if it's not there already - let newUrl = url; - if (newUrl[newUrl.length - 1] !== '/') { - newUrl = `${newUrl}/`; - } - - // Get password first - let userPassword = await this.getUserPassword(); - - if (userPassword) { - // First get the xsrf cookie by hitting the initial login page - xsrfCookie = await this.getXSRFToken(url, allowUnauthorized, fetchFunction); - - // Then get the session cookie by hitting that same page with the xsrftoken and the password - if (xsrfCookie) { - const sessionResult = await this.getSessionCookie(url, allowUnauthorized, xsrfCookie, userPassword, fetchFunction); - sessionCookieName = sessionResult.sessionCookieName; - sessionCookieValue = sessionResult.sessionCookieValue; - } - } - userPassword = undefined; - - // If we found everything return it all back if not, undefined as partial is useless - if (xsrfCookie && sessionCookieName && sessionCookieValue) { - sendTelemetryEvent(Telemetry.GetPasswordSuccess); - return { xsrfCookie, sessionCookieName, sessionCookieValue }; - } else { - sendTelemetryEvent(Telemetry.GetPasswordFailure); - return undefined; - } - } - - // For HTTPS connections respect our allowUnauthorized setting by adding in an agent to enable that on the request - private addAllowUnauthorized(url: string, allowUnauthorized: boolean, options: nodeFetch.RequestInit): nodeFetch.RequestInit { - if (url.startsWith('https') && allowUnauthorized) { - const requestAgent = new HttpsAgent({rejectUnauthorized: false}); - return {...options, agent: requestAgent}; - } - - return options; - } - - private async getUserPassword() : Promise { - // First get the proposed URI from the user - return this.appShell.showInputBox({ - prompt: localize.DataScience.jupyterSelectPasswordPrompt(), - ignoreFocusOut: true, - password: true - }); - } - - private async getXSRFToken(url: string, allowUnauthorized: boolean, fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise { - let xsrfCookie: string | undefined; - - const response = await fetchFunction(`${url}login?`, this.addAllowUnauthorized(url, allowUnauthorized, { - method: 'get', - redirect: 'manual', - headers: { Connection: 'keep-alive' } - })); - - if (response.ok) { - const cookies = this.getCookies(response); - if (cookies.has('_xsrf')) { - xsrfCookie = cookies.get('_xsrf'); - } - } - - return xsrfCookie; - } - - // Jupyter uses a session cookie to validate so by hitting the login page with the password we can get that cookie and use it ourselves - // This workflow can be seen by running fiddler and hitting the login page with a browser - // First you need a get at the login page to get the xsrf token, then you send back that token along with the password in a post - // That will return back the session cookie. This session cookie then needs to be added to our requests and websockets for @jupyterlab/services - private async getSessionCookie(url: string, - allowUnauthorized: boolean, - xsrfCookie: string, - password: string, - fetchFunction: (url: nodeFetch.RequestInfo, init?: nodeFetch.RequestInit) => Promise): Promise<{sessionCookieName: string | undefined; sessionCookieValue: string | undefined}> { - let sessionCookieName: string | undefined; - let sessionCookieValue: string | undefined; - // Create the form params that we need - const postParams = new URLSearchParams(); - postParams.append('_xsrf', xsrfCookie); - postParams.append('password', password); - - const response = await fetchFunction(`${url}login?`, this.addAllowUnauthorized(url, allowUnauthorized, { - method: 'post', - headers: { Cookie: `_xsrf=${xsrfCookie}`, Connection: 'keep-alive', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' }, - body: postParams.toString(), - redirect: 'manual' - })); - - // Now from this result we need to extract the session cookie - if (response.status === 302) { - const cookies = this.getCookies(response); - - // Session cookie is the first one - if (cookies.size > 0) { - sessionCookieName = cookies.entries().next().value[0]; - sessionCookieValue = cookies.entries().next().value[1]; - } - } - - return {sessionCookieName, sessionCookieValue}; - } - - private getCookies(response: nodeFetch.Response): Map { - const cookieList: Map = new Map(); - - const cookies: string | null = response.headers.get('set-cookie'); - - if (cookies) { - cookies.split(';').forEach(value => { - const cookieKey = value.substring(0, value.indexOf('=')); - const cookieVal = value.substring(value.indexOf('=') + 1); - cookieList.set(cookieKey, cookieVal); - }); - } - - return cookieList; - } -} diff --git a/src/client/datascience/jupyter/jupyterSelfCertsError.ts b/src/client/datascience/jupyter/jupyterSelfCertsError.ts deleted file mode 100644 index 0c2ee41a5ae9..000000000000 --- a/src/client/datascience/jupyter/jupyterSelfCertsError.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -export class JupyterSelfCertsError extends Error { - constructor(message: string) { - super(message); - } -} diff --git a/src/client/datascience/jupyter/jupyterServer.ts b/src/client/datascience/jupyter/jupyterServer.ts deleted file mode 100644 index 446bf4702663..000000000000 --- a/src/client/datascience/jupyter/jupyterServer.ts +++ /dev/null @@ -1,909 +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 { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; -import * as uuid from 'uuid/v4'; -import { Disposable } from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { ILiveShareApi } from '../../common/application/types'; -import { Cancellation, CancellationError } from '../../common/cancellation'; -import { traceInfo, traceWarning } from '../../common/logger'; -import { IAsyncDisposableRegistry, IConfigurationService, 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 { StopWatch } from '../../common/utils/stopWatch'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { generateCells } from '../cellFactory'; -import { CellMatcher } from '../cellMatcher'; -import { concatMultilineString } from '../common'; -import { CodeSnippits, Identifiers, Telemetry } from '../constants'; -import { - CellState, - ICell, - IConnection, - IDataScience, - IJupyterSession, - IJupyterSessionManager, - INotebookCompletion, - INotebookExecutionLogger, - INotebookServer, - INotebookServerLaunchInfo, - InterruptResult -} from '../types'; - -class CellSubscriber { - private deferred: Deferred = createDeferred(); - private cellRef: ICell; - private subscriber: Subscriber; - private promiseComplete: (self: CellSubscriber) => void; - private startTime: number; - - constructor(cell: ICell, subscriber: Subscriber, promiseComplete: (self: CellSubscriber) => void) { - this.cellRef = cell; - this.subscriber = subscriber; - this.promiseComplete = promiseComplete; - this.startTime = Date.now(); - } - - public isValid(sessionStartTime: number | undefined) { - return sessionStartTime && this.startTime > sessionStartTime; - } - - public next(sessionStartTime: number | undefined) { - // Tell the subscriber first - if (this.isValid(sessionStartTime)) { - this.subscriber.next(this.cellRef); - } - - // 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(); - } - - // tslint:disable-next-line:no-any - public reject(e: any) { - if (!this.deferred.completed) { - this.cellRef.state = CellState.error; - this.subscriber.next(this.cellRef); - this.subscriber.complete(); - this.deferred.reject(e); - this.promiseComplete(this); - } - } - - public cancel() { - if (!this.deferred.completed) { - this.cellRef.state = CellState.error; - this.subscriber.next(this.cellRef); - this.subscriber.complete(); - this.deferred.resolve(); - this.promiseComplete(this); - } - } - - public get promise(): Promise { - 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 - -export class JupyterServerBase implements INotebookServer { - private launchInfo: INotebookServerLaunchInfo | undefined; - private session: IJupyterSession | undefined; - private sessionStartTime: number | undefined; - private pendingCellSubscriptions: CellSubscriber[] = []; - private ranInitialSetup = false; - private id = uuid(); - private connectPromise: Deferred = createDeferred(); - private connectionInfoDisconnectHandler: Disposable | undefined; - private serverExitCode: number | undefined; - - constructor( - _liveShare: ILiveShareApi, - _dataScience: IDataScience, - protected logger: ILogger, - private disposableRegistry: IDisposableRegistry, - private asyncRegistry: IAsyncDisposableRegistry, - private configService: IConfigurationService, - private sessionManager: IJupyterSessionManager, - private loggers: INotebookExecutionLogger[] - ) { - this.asyncRegistry.push(this); - } - - public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { - traceInfo(`Connecting server ${this.id} kernelSpec ${launchInfo.kernelSpec ? launchInfo.kernelSpec.name : 'unknown'}`); - - // Save our launch info - this.launchInfo = launchInfo; - - // Indicate connect started - this.connectPromise.resolve(launchInfo); - - // Listen to the process going down - if (this.launchInfo && this.launchInfo.connectionInfo) { - this.connectionInfoDisconnectHandler = this.launchInfo.connectionInfo.disconnected((c) => { - this.logger.logError(localize.DataScience.jupyterServerCrashed().format(c.toString())); - this.serverExitCode = c; - this.shutdown().ignoreErrors(); - }); - } - - // Start our session - this.session = await this.sessionManager.startNew(launchInfo.connectionInfo, launchInfo.kernelSpec, cancelToken); - - traceInfo(`Started session ${this.id}`); - - if (this.session) { - // 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 - traceInfo(`Waiting for idle ${this.id}`); - const stopWatch = new StopWatch(); - const idleTimeout = this.configService.getSettings().datascience.jupyterLaunchTimeout; - await this.session.waitForIdle(idleTimeout); - sendTelemetryEvent(Telemetry.WaitForIdleJupyter, stopWatch.elapsedTime); - - traceInfo(`Performing initial setup ${this.id}`); - // Run our initial setup and plot magics - await this.initialNotebookSetup(cancelToken); - - traceInfo(`Finished connecting ${this.id}`); - } - } - - public shutdown(): Promise { - if (this.connectionInfoDisconnectHandler) { - this.connectionInfoDisconnectHandler.dispose(); - this.connectionInfoDisconnectHandler = undefined; - } - this.logger.logInformation(`Shutting down ${this.id}`); - const dispose = this.session ? this.session.dispose() : undefined; - return dispose ? dispose : Promise.resolve(); - } - - public dispose(): Promise { - return this.shutdown(); - } - - public waitForIdle(timeoutMs: number): Promise { - return this.session ? this.session.waitForIdle(timeoutMs) : Promise.resolve(); - } - - public execute(code: string, file: string, line: number, id: string, cancelToken?: CancellationToken, silent?: boolean): Promise { - // Create a deferred that we'll fire when we're done - const deferred = createDeferred(); - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.executeObservable(code, file, line, id, silent); - let output: ICell[]; - - observable.subscribe( - (cells: ICell[]) => { - output = cells; - }, - (error) => { - deferred.reject(error); - }, - () => { - deferred.resolve(output); - }); - - if (cancelToken) { - this.disposableRegistry.push(cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError()))); - } - - // Wait for the execution to finish - return deferred.promise; - } - - public async setInitialDirectory(directory: string): Promise { - // If we launched local and have no working directory call this on add code to change directory - if (this.launchInfo && !this.launchInfo.workingDir && this.launchInfo.connectionInfo.localLaunch) { - await this.changeDirectoryIfPossible(directory); - this.launchInfo.workingDir = directory; - } - } - - public executeObservable(code: string, file: string, line: number, id: string, silent: boolean = false): Observable { - // Create an observable and wrap the result so we can time it. - const stopWatch = new StopWatch(); - const result = this.executeObservableImpl(code, file, line, id, silent); - return new Observable(subscriber => { - result.subscribe(cells => { - subscriber.next(cells); - }, - error => { - subscriber.error(error); - }, - () => { - subscriber.complete(); - sendTelemetryEvent(Telemetry.ExecuteCell, stopWatch.elapsedTime); - }); - }); - } - - public async getSysInfo(): Promise { - // tslint:disable-next-line:no-multiline-string - const versionCells = await this.executeSilently(`import sys\r\nsys.version`); - // tslint:disable-next-line:no-multiline-string - const pathCells = await this.executeSilently(`import sys\r\nsys.executable`); - // tslint:disable-next-line:no-multiline-string - const notebookVersionCells = await this.executeSilently(`import notebook\r\nnotebook.version_info`); - - // Both should have streamed output - const version = versionCells.length > 0 ? this.extractStreamOutput(versionCells[0]).trimQuotes() : ''; - const notebookVersion = notebookVersionCells.length > 0 ? this.extractStreamOutput(notebookVersionCells[0]).trimQuotes() : ''; - const pythonPath = versionCells.length > 0 ? this.extractStreamOutput(pathCells[0]).trimQuotes() : ''; - - // Combine this data together to make our sys info - return { - data: { - cell_type: 'messages', - messages: [ - version, - notebookVersion, - pythonPath - ], - metadata: {}, - source: [] - }, - id: uuid(), - file: '', - line: 0, - state: CellState.finished, - type: 'execute' - }; - } - - @captureTelemetry(Telemetry.RestartJupyterTime) - public async restartKernel(timeoutMs: number): Promise { - if (this.session) { - // Update our start time so we don't keep sending responses - this.sessionStartTime = Date.now(); - - // Complete all pending as an error. We're restarting - this.finishUncompletedCells(); - - // Restart our kernel - await this.session.restart(timeoutMs); - - // Rerun our initial setup for the notebook - this.ranInitialSetup = false; - await this.initialNotebookSetup(); - - return; - } - - throw this.getDisposedError(); - } - - @captureTelemetry(Telemetry.InterruptJupyterTime) - public async interruptKernel(timeoutMs: number): Promise { - if (this.session) { - // Keep track of our current time. If our start time gets reset, we - // restarted the kernel. - const interruptBeginTime = Date.now(); - - // Get just the first pending cell (it should be the oldest). If it doesn't finish - // by our timeout, then our interrupt didn't work. - const firstPending = this.pendingCellSubscriptions.length > 0 ? this.pendingCellSubscriptions[0] : undefined; - - // Create a promise that resolves when the first pending cell finishes - const finished = firstPending ? firstPending.promise : Promise.resolve(CellState.finished); - - // Create a deferred promise that resolves if we have a failure - const restarted = createDeferred(); - - // Listen to status change events so we can tell if we're restarting - const restartHandler = () => { - // 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. - this.finishUncompletedCells(); - }; - const restartHandlerToken = this.session.onRestarted(restartHandler); - - // Start our interrupt. If it fails, indicate a restart - this.session.interrupt(timeoutMs).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)]); - - // See if we restarted or not - if (restarted.completed) { - return InterruptResult.Restarted; - } - - // See if we timed out or not. - if (result === timeoutMs) { - // We timed out. You might think we should stop our pending list, but that's not - // up to us. The cells are still executing. The user has to request a restart or try again - return InterruptResult.TimedOut; - } - - // Cancel all other pending cells as we interrupted. - this.finishUncompletedCells(); - - // Indicate the interrupt worked. - return InterruptResult.Success; - - } catch (exc) { - // Something failed. See if we restarted or not. - if (this.sessionStartTime && (interruptBeginTime < this.sessionStartTime)) { - return InterruptResult.Restarted; - } - - // Otherwise a real error occurred. - throw exc; - } finally { - restartHandlerToken.dispose(); - } - } - - throw this.getDisposedError(); - } - - public waitForConnect(): Promise { - return this.connectPromise.promise; - } - - public async setMatplotLibStyle(useDark: boolean): Promise { - // Reset the matplotlib style based on if dark or not. - await this.executeSilently(useDark ? - 'matplotlib.style.use(\'dark_background\')' : - `matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})`); - - } - - // Return a copy of the connection information that this server used to connect with - public getConnectionInfo(): IConnection | undefined { - if (!this.launchInfo) { - return undefined; - } - - // Return a copy with a no-op for dispose - return { - ...this.launchInfo.connectionInfo, - dispose: noop - }; - } - - public async getCompletion(cellCode: string, offsetInCode: number, cancelToken?: CancellationToken): Promise { - if (this.session) { - const result = await Cancellation.race(() => this.session!.requestComplete({ - code: cellCode, - cursor_pos: offsetInCode - }), cancelToken); - if (result && result.content) { - return { - matches: result.content.matches, - cursor: { - start: result.content.cursor_start, - end: result.content.cursor_end - }, - metadata: result.content.metadata - }; - } - } - - // Default is just say session was disposed - throw new Error(localize.DataScience.sessionDisposed()); - } - - private finishUncompletedCells() { - const copyPending = [...this.pendingCellSubscriptions]; - copyPending.forEach(c => c.cancel()); - this.pendingCellSubscriptions = []; - } - - private getDisposedError(): Error { - // We may have been disposed because of a crash. See if our connection info is indicating shutdown - if (this.serverExitCode) { - return new Error(localize.DataScience.jupyterServerCrashed().format(this.serverExitCode.toString())); - } - - // Default is just say session was disposed - return new Error(localize.DataScience.sessionDisposed()); - } - - @captureTelemetry(Telemetry.HiddenCellTime) - private executeSilently(code: string, cancelToken?: CancellationToken): Promise { - // Create a deferred that we'll fire when we're done - const deferred = createDeferred(); - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.executeObservableImpl(code, Identifiers.EmptyFileName, 0, uuid(), true); - let output: ICell[]; - - observable.subscribe( - (cells: ICell[]) => { - output = cells; - }, - (error) => { - deferred.reject(error); - }, - () => { - deferred.resolve(output); - }); - - if (cancelToken) { - this.disposableRegistry.push(cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError()))); - } - - // Wait for the execution to finish - return deferred.promise; - } - - private extractStreamOutput(cell: ICell): string { - let result = ''; - if (cell.state === CellState.error || cell.state === CellState.finished) { - const outputs = cell.data.outputs as nbformat.IOutput[]; - if (outputs) { - outputs.forEach(o => { - if (o.output_type === 'stream') { - const stream = o as nbformat.IStream; - result = result.concat(stream.text.toString()); - } else { - const data = o.data; - if (data && data.hasOwnProperty('text/plain')) { - // tslint:disable-next-line:no-any - result = result.concat((data as any)['text/plain']); - } - } - }); - } - } - return result; - } - - private executeObservableImpl(code: string, file: string, line: number, id: string, silent?: boolean): Observable { - // If we have a session, execute the code now. - if (this.session) { - // Generate our cells ahead of time - const cells = generateCells(this.configService.getSettings().datascience, code, file, line, true, id); - - // Might have more than one (markdown might be split) - if (cells.length > 1) { - // We need to combine results - return this.combineObservables( - this.executeMarkdownObservable(cells[0]), - this.executeCodeObservable(cells[1], silent)); - } else if (cells.length > 0) { - // Either markdown or or code - return this.combineObservables( - cells[0].data.cell_type === 'code' ? this.executeCodeObservable(cells[0], silent) : this.executeMarkdownObservable(cells[0])); - } - } - - // Can't run because no session - return new Observable(subscriber => { - subscriber.error(this.getDisposedError()); - subscriber.complete(); - }); - } - - private generateRequest = (code: string, silent?: boolean): Kernel.IFuture | undefined => { - //this.logger.logInformation(`Executing code in jupyter : ${code}`) - try { - const cellMatcher = new CellMatcher(this.configService.getSettings().datascience); - return this.session ? this.session.requestExecute( - { - // Remove the cell marker if we have one. - code: cellMatcher.stripMarkers(code), - stop_on_error: false, - allow_stdin: false, - store_history: !silent // Silent actually means don't output anything. Store_history is what affects execution_count - }, - 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 async initialNotebookSetup(cancelToken?: CancellationToken): Promise { - if (this.ranInitialSetup) { - return; - } - this.ranInitialSetup = true; - - try { - // When we start our notebook initial, change to our workspace or user specified root directory - if (this.launchInfo && this.launchInfo.workingDir && this.launchInfo.connectionInfo.localLaunch) { - await this.changeDirectoryIfPossible(this.launchInfo.workingDir); - } - - const settings = this.configService.getSettings().datascience; - const matplobInit = !settings || settings.enablePlotViewer ? CodeSnippits.MatplotLibInitSvg : CodeSnippits.MatplotLibInitPng; - - // Force matplotlib to inline and save the default style. We'll use this later if we - // get a request to update style - await this.executeSilently( - matplobInit, - cancelToken - ); - } catch (e) { - traceWarning(e); - } - } - - private combineObservables = (...args: Observable[]): Observable => { - return new Observable(subscriber => { - // When all complete, we have our results - const results: Record = {}; - - args.forEach(o => { - o.subscribe(c => { - results[c.id] = c; - - // Convert to an array - const array = Object.keys(results).map((k: string) => { - return results[k]; - }); - - // Update our subscriber of our total results if we have that many - if (array.length === args.length) { - subscriber.next(array); - - // Complete when everybody is finished - if (array.every(a => a.state === CellState.finished || a.state === CellState.error)) { - subscriber.complete(); - } - } - }, - e => { - subscriber.error(e); - }); - }); - }); - } - - private wrapObservable( - observable: Observable, - silent: boolean, - preCall: (args: T, silent: boolean) => void, - postCall: (args: T | Error, silent: boolean) => void): Observable { - // Wrap in a new observable - return new Observable(subscriber => { - let pre = false; - let lastVal: T | undefined; - observable.subscribe(val => { - if (!pre) { - pre = true; - preCall(val, silent); - } - lastVal = val; - subscriber.next(val); - }, - e => { - subscriber.error(e); - postCall(e, silent); - }, - () => { - subscriber.complete(); - if (lastVal) { - postCall(lastVal, silent); - } - }); - }); - } - - private executeMarkdownObservable = (cell: ICell): Observable => { - // Markdown doesn't need any execution - return new Observable(subscriber => { - subscriber.next(cell); - subscriber.complete(); - }); - } - - private changeDirectoryIfPossible = async (directory: string): Promise => { - if (this.launchInfo && this.launchInfo.connectionInfo.localLaunch && await fs.pathExists(directory)) { - await this.executeSilently(`%cd "${directory}"`); - } - } - - private handleCodeRequest = (subscriber: CellSubscriber, silent?: boolean) => { - // Generate a new request if we still can - if (subscriber.isValid(this.sessionStartTime)) { - - // Double check process is still running - if (this.launchInfo && this.launchInfo.connectionInfo && this.launchInfo.connectionInfo.localProcExitCode) { - // Not running, just exit - const exitCode = this.launchInfo.connectionInfo.localProcExitCode; - subscriber.error(this.sessionStartTime, new Error(localize.DataScience.jupyterServerCrashed().format(exitCode.toString()))); - subscriber.complete(this.sessionStartTime); - } else { - const request = this.generateRequest(concatMultilineString(subscriber.cell.data.source), silent); - - // 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; - - // Make sure our connection doesn't go down - let exitHandlerDisposable: Disposable | undefined; - if (this.launchInfo && this.launchInfo.connectionInfo) { - // If the server crashes, cancel the current observable - exitHandlerDisposable = this.launchInfo.connectionInfo.disconnected((c) => { - const str = c ? c.toString() : ''; - subscriber.error(this.sessionStartTime, new Error(localize.DataScience.jupyterServerCrashed().format(str))); - subscriber.complete(this.sessionStartTime); - }); - } - - const clearState: Map = new Map(); - - // 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, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isExecuteInputMsg(msg)) { - this.handleExecuteInput(msg as KernelMessage.IExecuteInputMsg, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isStatusMsg(msg)) { - this.handleStatusMessage(msg as KernelMessage.IStatusMsg, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isStreamMsg(msg)) { - this.handleStreamMesssage(msg as KernelMessage.IStreamMsg, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isDisplayDataMsg(msg)) { - this.handleDisplayData(msg as KernelMessage.IDisplayDataMsg, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isUpdateDisplayDataMsg(msg)) { - this.handleUpdateDisplayData(msg as KernelMessage.IUpdateDisplayDataMsg, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isClearOutputMsg(msg)) { - this.handleClearOutput(msg as KernelMessage.IClearOutputMsg, clearState, subscriber.cell); - } else if (jupyterLab.KernelMessage.isErrorMsg(msg)) { - this.handleError(msg as KernelMessage.IErrorMsg, clearState, subscriber.cell); - } else { - 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); - if (exitHandlerDisposable) { - exitHandlerDisposable.dispose(); - } - }).catch(e => subscriber.error(this.sessionStartTime, e)); - } else { - subscriber.error(this.sessionStartTime, this.getDisposedError()); - } - } - } else { - // Otherwise just set to an error - this.handleInterrupted(subscriber.cell); - subscriber.cell.state = CellState.error; - subscriber.complete(this.sessionStartTime); - } - - } - - private executeCodeObservable(cell: ICell, silent?: boolean): Observable { - // Wrap this observable so we can log pre/post calls. ExecuteCodeObservable is the main - // gateway to executing actual code on the jupyter server. - return this.wrapObservable( - new Observable(subscriber => { - // Tell our listener. NOTE: have to do this asap so that markdown cells don't get - // run before our cells. - subscriber.next(cell); - - // 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, silent); - }), - silent !== undefined ? silent : false, - this.logPreCode.bind(this), - this.logPostCode.bind(this)); - } - - private logPreCode(cell: ICell, silent: boolean) { - this.loggers.forEach(l => l.preExecute(cell, silent)); - } - - private logPostCode(cellOrError: ICell | Error, silent: boolean) { - this.loggers.forEach(l => l.postExecute(cellOrError, silent)); - } - - private addToCellData = (cell: ICell, output: nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, clearState: Map) => { - // If a clear is pending, replace the output with the new one - if (clearState.get(output.output_type)) { - clearState.delete(output.output_type); - const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; - const index = data.outputs.findIndex(o => o.output_type === output.output_type); - if (index >= 0) { - data.outputs.splice(index, 1, output); - } else { - data.outputs = [...data.outputs, output]; - } - cell.data = data; - } else { - // Then append this data onto the end. - const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; - data.outputs = [...data.outputs, output]; - cell.data = data; - } - } - - private handleExecuteResult(msg: KernelMessage.IExecuteResultMsg, clearState: Map, cell: ICell) { - this.addToCellData( - cell, - { output_type: 'execute_result', data: msg.content.data, metadata: msg.content.metadata, execution_count: msg.content.execution_count }, - clearState); - } - - private handleExecuteInput(msg: KernelMessage.IExecuteInputMsg, _clearState: Map, cell: ICell) { - cell.data.execution_count = msg.content.execution_count; - } - - private handleStatusMessage(msg: KernelMessage.IStatusMsg, _clearState: Map, cell: ICell) { - // 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, clearState: Map, 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) { - // If clear pending, then don't add. - if (clearState.get('stream')) { - clearState.delete('stream'); - existing.text = msg.content.text; - } else { - // 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, clearState); - } - } - - private handleDisplayData(msg: KernelMessage.IDisplayDataMsg, clearState: Map, cell: ICell) { - const output: nbformat.IDisplayData = { - output_type: 'display_data', - data: msg.content.data, - metadata: msg.content.metadata - }; - this.addToCellData(cell, output, clearState); - } - - private handleUpdateDisplayData(msg: KernelMessage.IUpdateDisplayDataMsg, _clearState: Map, 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 handleClearOutput(msg: KernelMessage.IClearOutputMsg, clearState: Map, cell: ICell) { - // If the message says wait, add every message type to our clear state. This will - // make us wait for this type of output before we clear it. - if (msg && msg.content.wait) { - clearState.set('display_data', true); - clearState.set('error', true); - clearState.set('execute_result', true); - clearState.set('stream', true); - } else { - // Clear all outputs and start over again. - const data: nbformat.ICodeCell = cell.data as nbformat.ICodeCell; - data.outputs = []; - } - } - - private handleInterrupted(cell: ICell) { - this.handleError({ - channel: 'iopub', - parent_header: {}, - metadata: {}, - header: { username: '', version: '', session: '', msg_id: '', msg_type: 'error' }, - content: { - ename: 'KeyboardInterrupt', - evalue: '', - // Does this need to be translated? All depends upon if jupyter does or not - traceback: [ - '---------------------------------------------------------------------------', - 'KeyboardInterrupt: ' - ] - } - }, new Map(), cell); - } - - private handleError(msg: KernelMessage.IErrorMsg, clearState: Map, cell: ICell) { - const output: nbformat.IError = { - output_type: 'error', - ename: msg.content.ename, - evalue: msg.content.evalue, - traceback: msg.content.traceback - }; - this.addToCellData(cell, output, clearState); - cell.state = CellState.error; - } -} diff --git a/src/client/datascience/jupyter/jupyterServerFactory.ts b/src/client/datascience/jupyter/jupyterServerFactory.ts deleted file mode 100644 index 880c0815c96d..000000000000 --- a/src/client/datascience/jupyter/jupyterServerFactory.ts +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable, multiInject, optional } from 'inversify'; -import { Observable } from 'rxjs/Observable'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { ILiveShareApi } from '../../common/application/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../common/types'; -import { - ICell, - IConnection, - IDataScience, - IJupyterSessionManager, - INotebookCompletion, - INotebookExecutionLogger, - INotebookServer, - INotebookServerLaunchInfo, - InterruptResult -} from '../types'; -import { GuestJupyterServer } from './liveshare/guestJupyterServer'; -import { HostJupyterServer } from './liveshare/hostJupyterServer'; -import { IRoleBasedObject, RoleBasedFactory } from './liveshare/roleBasedFactory'; - -interface IJupyterServerInterface extends IRoleBasedObject, INotebookServer { - -} - -// tslint:disable:callable-types -type JupyterServerClassType = { - new(liveShare: ILiveShareApi, - dataScience: IDataScience, - logger: ILogger, - disposableRegistry: IDisposableRegistry, - asyncRegistry: IAsyncDisposableRegistry, - configService: IConfigurationService, - sessionManager: IJupyterSessionManager, - loggers: INotebookExecutionLogger[] - ): IJupyterServerInterface; -}; -// tslint:enable:callable-types - -@injectable() -export class JupyterServerFactory implements INotebookServer { - private serverFactory: RoleBasedFactory; - - private launchInfo: INotebookServerLaunchInfo | undefined; - - constructor( - @inject(ILiveShareApi) liveShare: ILiveShareApi, - @inject(IDataScience) dataScience: IDataScience, - @inject(ILogger) logger: ILogger, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IAsyncDisposableRegistry) asyncRegistry: IAsyncDisposableRegistry, - @inject(IConfigurationService) configService: IConfigurationService, - @inject(IJupyterSessionManager) sessionManager: IJupyterSessionManager, - @multiInject(INotebookExecutionLogger) @optional() loggers: INotebookExecutionLogger[] | undefined) { - this.serverFactory = new RoleBasedFactory( - liveShare, - HostJupyterServer, - GuestJupyterServer, - liveShare, - dataScience, - logger, - disposableRegistry, - asyncRegistry, - configService, - sessionManager, - loggers ? loggers : [] - ); - } - - public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { - this.launchInfo = launchInfo; - const server = await this.serverFactory.get(); - return server.connect(launchInfo, cancelToken); - } - - public async shutdown(): Promise { - const server = await this.serverFactory.get(); - return server.shutdown(); - } - - public async dispose(): Promise { - const server = await this.serverFactory.get(); - return server.dispose(); - } - - public async waitForIdle(timeoutMs: number): Promise { - const server = await this.serverFactory.get(); - return server.waitForIdle(timeoutMs); - } - - public async execute(code: string, file: string, line: number, id: string, cancelToken?: CancellationToken, silent?: boolean): Promise { - const server = await this.serverFactory.get(); - return server.execute(code, file, line, id, cancelToken, silent); - } - - public async setInitialDirectory(directory: string): Promise { - const server = await this.serverFactory.get(); - return server.setInitialDirectory(directory); - } - - public async setMatplotLibStyle(useDark: boolean): Promise { - const server = await this.serverFactory.get(); - return server.setMatplotLibStyle(useDark); - } - - public executeObservable(code: string, file: string, line: number, id: string, silent: boolean = false): Observable { - // Create a wrapper observable around the actual server (because we have to wait for a promise) - return new Observable(subscriber => { - this.serverFactory.get().then(s => { - s.executeObservable(code, file, line, id, silent) - .forEach(n => { - subscriber.next(n); // Separate lines so can break on this call. - }, Promise) - .then(_f => { - subscriber.complete(); - }) - .catch(e => subscriber.error(e)); - }, - r => { - subscriber.error(r); - subscriber.complete(); - }); - }); - } - - public async restartKernel(timeoutMs: number): Promise { - const server = await this.serverFactory.get(); - return server.restartKernel(timeoutMs); - } - - public async interruptKernel(timeoutMs: number): Promise { - const server = await this.serverFactory.get(); - return server.interruptKernel(timeoutMs); - } - - // Return a copy of the connection information that this server used to connect with - public getConnectionInfo(): IConnection | undefined { - if (this.launchInfo) { - return this.launchInfo.connectionInfo; - } - - return undefined; - } - - public async waitForConnect(): Promise { - const server = await this.serverFactory.get(); - return server.waitForConnect(); - } - - public async getSysInfo(): Promise { - const server = await this.serverFactory.get(); - return server.getSysInfo(); - } - - public async getCompletion(cellCode: string, offsetInCode: number, cancelToken?: CancellationToken): Promise { - const server = await this.serverFactory.get(); - return server.getCompletion(cellCode, offsetInCode, cancelToken); - } -} diff --git a/src/client/datascience/jupyter/jupyterSession.ts b/src/client/datascience/jupyter/jupyterSession.ts deleted file mode 100644 index 330f30795506..000000000000 --- a/src/client/datascience/jupyter/jupyterSession.ts +++ /dev/null @@ -1,349 +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 { Agent as HttpsAgent } from 'https'; -import * as uuid from 'uuid/v4'; -import { Event, EventEmitter } from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { Cancellation } from '../../common/cancellation'; -import { isTestExecution } from '../../common/constants'; -import { traceInfo, traceWarning } from '../../common/logger'; -import { sleep } from '../../common/utils/async'; -import * as localize from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { - IConnection, - IJupyterKernelSpec, - IJupyterPasswordConnect, - IJupyterPasswordConnectInfo, - IJupyterSession -} from '../types'; -import { JupyterKernelPromiseFailedError } from './jupyterKernelPromiseFailedError'; -import { JupyterWaitForIdleError } from './jupyterWaitForIdleError'; -import { createJupyterWebSocket } from './jupyterWebSocket'; - -export class JupyterSession implements IJupyterSession { - private connInfo: IConnection | undefined; - private kernelSpec: IJupyterKernelSpec | undefined; - private sessionManager : SessionManager | undefined; - private session: Session.ISession | undefined; - private restartSessionPromise: Promise | undefined; - private contentsManager: ContentsManager | undefined; - private notebookFiles: Contents.IModel[] = []; - private onRestartedEvent : EventEmitter | undefined; - private statusHandler : Slot | undefined; - private connected: boolean = false; - private jupyterPasswordConnect: IJupyterPasswordConnect; - - constructor( - connInfo: IConnection, - kernelSpec: IJupyterKernelSpec | undefined, - jupyterPasswordConnect: IJupyterPasswordConnect - ) { - this.connInfo = connInfo; - this.kernelSpec = kernelSpec; - this.jupyterPasswordConnect = jupyterPasswordConnect; - } - - public dispose() : Promise { - return this.shutdown(); - } - - public async shutdown(): Promise { - await this.destroyKernelSpec(); - - // Destroy the notebook file if not local. Local is cleaned up when we destroy the kernel spec. - if (this.notebookFiles.length && 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 Promise.all(this.notebookFiles.map(f => this.contentsManager!.delete(f.path))); - this.notebookFiles = []; - } - } catch { - noop(); - } - } - return this.shutdownSessionAndConnection(); - } - - public get onRestarted() : Event { - if (!this.onRestartedEvent) { - this.onRestartedEvent = new EventEmitter(); - } - return this.onRestartedEvent.event; - } - - public async waitForIdle(timeout: number) : Promise { - if (this.session && this.session.kernel) { - // This function seems to cause CI builds to timeout randomly on - // different tests. Waiting for status to go idle doesn't seem to work and - // in the past, waiting on the ready promise doesn't work either. Check status with a maximum of 5 seconds - const startTime = Date.now(); - while (this.session && - this.session.kernel && - this.session.kernel.status !== 'idle' && - (Date.now() - startTime < timeout)) { - traceInfo(`Waiting for idle: ${this.session.kernel.status}`); - await sleep(100); - } - - // If we didn't make it out in ten seconds, indicate an error - if (!this.session || !this.session.kernel || this.session.kernel.status !== 'idle') { - throw new JupyterWaitForIdleError(localize.DataScience.jupyterLaunchTimedOut()); - } - } - } - - public async restart(_timeout: number) : Promise { - // Just kill the current session and switch to the other - if (this.restartSessionPromise && this.session && this.sessionManager && this.contentsManager) { - // Save old state for shutdown - const oldSession = this.session; - const oldStatusHandler = this.statusHandler; - - // Just switch to the other session. - this.session = await this.restartSessionPromise; - - // Rewire our status changed event. - this.statusHandler = this.onStatusChanged.bind(this.onStatusChanged); - this.session.statusChanged.connect(this.statusHandler); - - // After switching, start another in case we restart again. - this.restartSessionPromise = this.createSession(oldSession.serverSettings, this.contentsManager); - this.shutdownSession(oldSession, oldStatusHandler).ignoreErrors(); - } else { - throw new Error(localize.DataScience.sessionDisposed()); - } - } - - public interrupt(timeout: number) : Promise { - return this.session && this.session.kernel ? - this.waitForKernelPromise(this.session.kernel.interrupt(), timeout, localize.DataScience.interruptingKernelFailed()) : - 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 requestComplete(content: KernelMessage.ICompleteRequest) : Promise { - return this.session && this.session.kernel ? this.session.kernel.requestComplete(content) : Promise.resolve(undefined); - } - - public async connect(cancelToken?: CancellationToken) : Promise { - if (!this.connInfo) { - throw new Error(localize.DataScience.sessionDisposed()); - } - - const serverSettings: ServerConnection.ISettings = await this.getServerConnectSettings(this.connInfo); - this.sessionManager = new SessionManager({ serverSettings: serverSettings }); - this.contentsManager = new ContentsManager({ serverSettings: serverSettings }); - - // Start a new session - this.session = await this.createSession(serverSettings, this.contentsManager, cancelToken); - - // Start another session to handle restarts - this.restartSessionPromise = this.createSession(serverSettings, this.contentsManager, 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 async createSession(serverSettings: ServerConnection.ISettings, contentsManager: ContentsManager, cancelToken?: CancellationToken) : Promise { - - // Create a temporary notebook for this session. - this.notebookFiles.push(await contentsManager.newUntitled({type: 'notebook'})); - - // Create our session options using this temporary notebook and our connection info - const options : Session.IOptions = { - path: this.notebookFiles[this.notebookFiles.length - 1].path, - kernelName: this.kernelSpec ? this.kernelSpec.name : '', - name: uuid(), // This is crucial to distinguish this session from any other. - serverSettings: serverSettings - }; - - return Cancellation.race(() => this.sessionManager!.startNew(options), cancelToken); - } - - private getSessionCookieString(pwSettings: IJupyterPasswordConnectInfo): string { - return `_xsrf=${pwSettings.xsrfCookie}; ${pwSettings.sessionCookieName}=${pwSettings.sessionCookieValue}`; - } - private async getServerConnectSettings(connInfo: IConnection): Promise { - let serverSettings: Partial = - { - baseUrl: connInfo.baseUrl, - pageUrl: '', - // A web socket is required to allow token authentication - wsUrl: connInfo.baseUrl.replace('http', 'ws') - }; - - // Agent is allowed to be set on this object, but ts doesn't like it on RequestInit, so any - // tslint:disable-next-line:no-any - let requestInit: any = { cache: 'no-store', credentials: 'same-origin' }; - let requiresWebSocket = false; - let cookieString; - let allowUnauthorized; - - // If no token is specified prompt for a password - if (connInfo.token === '' || connInfo.token === 'null') { - serverSettings = {...serverSettings, token: ''}; - const pwSettings = await this.jupyterPasswordConnect.getPasswordConnectionInfo(connInfo.baseUrl, connInfo.allowUnauthorized ? true : false); - if (pwSettings) { - cookieString = this.getSessionCookieString(pwSettings); - const requestHeaders = { Cookie: cookieString, 'X-XSRFToken': pwSettings.xsrfCookie }; - requestInit = {...requestInit, headers: requestHeaders}; - requiresWebSocket = true; - } else { - // Failed to get password info, notify the user - throw new Error(localize.DataScience.passwordFailure()); - } - } else { - serverSettings = {...serverSettings, token: connInfo.token}; - } - - // If this is an https connection and we want to allow unauthorized connections set that option on our agent - // we don't need to save the agent as the previous behaviour is just to create a temporary default agent when not specified - if (connInfo.baseUrl.startsWith('https') && connInfo.allowUnauthorized) { - const requestAgent = new HttpsAgent({rejectUnauthorized: false}); - requestInit = {...requestInit, agent: requestAgent}; - requiresWebSocket = true; - allowUnauthorized = true; - } - - serverSettings = {...serverSettings, init: requestInit}; - - // Only replace the websocket if we need to so we keep our normal local attach clean - if (requiresWebSocket) { - // This replaces the WebSocket constructor in jupyter lab services with our own implementation - // See _createSocket here: - // https://github.com/jupyterlab/jupyterlab/blob/cfc8ebda95e882b4ed2eefd54863bb8cdb0ab763/packages/services/src/kernel/default.ts - // tslint:disable-next-line:no-any - serverSettings = {...serverSettings, WebSocket: createJupyterWebSocket(cookieString, allowUnauthorized) as any}; - } - - return ServerConnection.makeSettings(serverSettings); - } - - private async waitForKernelPromise(kernelPromise: Promise, timeout: number, errorMessage: string) : Promise { - // Wait for this kernel promise to happen - const result = await Promise.race([kernelPromise, sleep(timeout)]); - if (result === timeout) { - // We timed out. Throw a specific exception - throw new JupyterKernelPromiseFailedError(errorMessage); - } - } - - private onStatusChanged(_s: Session.ISession, a: Kernel.Status) { - if (a === 'starting' && this.onRestartedEvent) { - 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 async shutdownSession(session: Session.ISession | undefined, statusHandler: Slot | undefined) : Promise { - if (session) { - try { - if (statusHandler) { - session.statusChanged.disconnect(statusHandler); - } - try { - // When running under a test, mark all futures as done so we - // don't hit this problem: - // https://github.com/jupyterlab/jupyterlab/issues/4252 - // tslint:disable:no-any - if (isTestExecution()) { - if (session && session.kernel) { - const defaultKernel = session.kernel as any; - if (defaultKernel && defaultKernel._futures) { - const futures = defaultKernel._futures as Map; - if (futures) { - futures.forEach(f => { - if (f._status !== undefined) { - f._status |= 4; - } - }); - } - } - } - } - - // Shutdown may fail if the process has been killed - await Promise.race([session.shutdown(), sleep(1000)]); - } catch { - noop(); - } - if (session && !session.isDisposed) { - session.dispose(); - } - } catch (e) { - // Ignore, just trace. - traceWarning(e); - } - } - } - - //tslint:disable:cyclomatic-complexity - private async shutdownSessionAndConnection(): Promise { - if (this.contentsManager) { - this.contentsManager.dispose(); - this.contentsManager = undefined; - } - if (this.session || this.sessionManager) { - try { - await this.shutdownSession(this.session, this.statusHandler); - const restartSession = await this.restartSessionPromise; - await this.shutdownSession(restartSession, undefined); - - if (this.sessionManager && !this.sessionManager.isDisposed) { - this.sessionManager.dispose(); - } - } catch { - noop(); - } - this.session = undefined; - this.sessionManager = undefined; - this.restartSessionPromise = undefined; - } - if (this.onRestartedEvent) { - 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 b36053bb1418..000000000000 --- a/src/client/datascience/jupyter/jupyterSessionManager.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { ServerConnection, SessionManager } from '@jupyterlab/services'; -import { inject, injectable } from 'inversify'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { IConnection, IJupyterKernelSpec, IJupyterPasswordConnect, IJupyterSession, IJupyterSessionManager } from '../types'; -import { JupyterKernelSpec } from './jupyterKernelSpec'; -import { JupyterSession } from './jupyterSession'; - -@injectable() -export class JupyterSessionManager implements IJupyterSessionManager { - constructor( - @inject(IJupyterPasswordConnect) private jupyterPasswordConnect: IJupyterPasswordConnect - ) {} - - public async startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken) : Promise { - // Create a new session and attempt to connect to it - const session = new JupyterSession(connInfo, kernelSpec, this.jupyterPasswordConnect); - try { - await session.connect(cancelToken); - } finally { - if (!session.isConnected) { - await session.dispose(); - } - } - return session; - } - - public async getActiveKernelSpecs(connection: IConnection) : Promise { - let sessionManager: SessionManager | undefined ; - try { - // 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' } - }); - sessionManager = new SessionManager({ serverSettings: serverSettings }); - - // 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) as IJupyterKernelSpec; - }); - } 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/jupyter/jupyterVariables.ts b/src/client/datascience/jupyter/jupyterVariables.ts deleted file mode 100644 index 2ed29c63966b..000000000000 --- a/src/client/datascience/jupyter/jupyterVariables.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils'; -import { JSONObject } from '@phosphor/coreutils'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import stripAnsi from 'strip-ansi'; -import * as uuid from 'uuid/v4'; - -import { traceError } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import * as localize from '../../common/utils/localize'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { Identifiers } from '../constants'; -import { ICell, IInteractiveWindowProvider, IJupyterExecution, IJupyterVariable, IJupyterVariables } from '../types'; -import { JupyterDataRateLimitError } from './jupyterDataRateLimitError'; - -@injectable() -export class JupyterVariables implements IJupyterVariables { - private fetchVariablesScript?: string; - private fetchVariableValueScript?: string; - private fetchDataFrameInfoScript?: string; - private fetchDataFrameRowsScript?: string; - private filesLoaded: boolean = false; - // tslint:disable:quotemark - - constructor( - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider - ) { - } - - // IJupyterVariables implementation - public async getVariables(): Promise { - // Run the fetch variables script. - return this.runScript( - undefined, - [], - () => this.fetchVariablesScript); - } - - public async getValue(targetVariable: IJupyterVariable): Promise { - // Run the get value script - return this.runScript( - targetVariable, - targetVariable, - () => this.fetchVariableValueScript); - } - - public async getDataFrameInfo(targetVariable: IJupyterVariable): Promise { - // Run the get dataframe info script - return this.runScript( - targetVariable, - targetVariable, - () => this.fetchDataFrameInfoScript, - [ - {key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn()} - ]); - } - - public async getDataFrameRows(targetVariable: IJupyterVariable, start: number, end: number): Promise { - // Run the get dataframe rows script - return this.runScript( - targetVariable, - {}, - () => this.fetchDataFrameRowsScript, - [ - {key: '_VSCode_JupyterValuesColumn', value: localize.DataScience.valuesColumn()}, - {key: '_VSCode_JupyterStartRow', value: start.toString()}, - {key: '_VSCode_JupyterEndRow', value: end.toString()} - ]); - } - - // Private methods - // Load our python files for fetching variables - private async loadVariableFiles(): Promise { - let file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterVariableList.py'); - this.fetchVariablesScript = await this.fileSystem.readFile(file); - - file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterVariableValue.py'); - this.fetchVariableValueScript = await this.fileSystem.readFile(file); - - file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterVariableDataFrameInfo.py'); - this.fetchDataFrameInfoScript = await this.fileSystem.readFile(file); - - file = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getJupyterVariableDataFrameRows.py'); - this.fetchDataFrameRowsScript = await this.fileSystem.readFile(file); - - this.filesLoaded = true; - } - - private async runScript( - targetVariable: IJupyterVariable | undefined, - defaultValue: T, - scriptBaseTextFetcher: () => string | undefined, - extraReplacements: { key: string; value: string }[] = []): Promise { - if (!this.filesLoaded) { - await this.loadVariableFiles(); - } - - const scriptBaseText = scriptBaseTextFetcher(); - const activeServer = await this.jupyterExecution.getServer(await this.interactiveWindowProvider.getNotebookOptions()); - if (!activeServer || !scriptBaseText) { - // No active server just return the unchanged target variable - return defaultValue; - } - - // Prep our targetVariable to send over - const variableString = JSON.stringify(targetVariable); - - // Setup a regex - const regexPattern = extraReplacements.length === 0 ? '_VSCode_JupyterTestValue' : - ['_VSCode_JupyterTestValue', ...extraReplacements.map(v => v.key)].join('|'); - const replaceRegex = new RegExp(regexPattern, 'g'); - - // Replace the test value with our current value. Replace start and end as well - const scriptText = scriptBaseText.replace(replaceRegex, (match: string) => { - if (match === '_VSCode_JupyterTestValue') { - return variableString; - } else { - const index = extraReplacements.findIndex(v => v.key === match); - if (index >= 0) { - return extraReplacements[index].value; - } - } - - return match; - }); - - // Execute this on the jupyter server. - const results = await activeServer.execute(scriptText, Identifiers.EmptyFileName, 0, uuid(), undefined, true); - - // Results should be the updated variable. - return this.deserializeJupyterResult(results); - } - - // Pull our text result out of the Jupyter cell - private deserializeJupyterResult(cells: ICell[]): T { - // Verify that we have the correct cell type and outputs - if (cells.length > 0 && cells[0].data) { - const codeCell = cells[0].data as nbformat.ICodeCell; - if (codeCell.outputs.length > 0) { - const codeCellOutput = codeCell.outputs[0] as nbformat.IOutput; - if (codeCellOutput && codeCellOutput.output_type === 'stream' && codeCellOutput.name === 'stderr' && codeCellOutput.hasOwnProperty('text')) { - const resultString = codeCellOutput.text as string; - // See if this the IOPUB data rate limit problem - if (resultString.includes('iopub_data_rate_limit')) { - throw new JupyterDataRateLimitError(); - } else { - const error = localize.DataScience.jupyterGetVariablesExecutionError().format(resultString); - traceError(error); - throw new Error(error); - } - } - if (codeCellOutput && codeCellOutput.output_type === 'stream' && codeCellOutput.hasOwnProperty('text')) { - const resultString = codeCellOutput.text as string; - return JSON.parse(resultString) as T; - } - if (codeCellOutput && codeCellOutput.output_type === 'error' && codeCellOutput.hasOwnProperty('traceback')) { - const traceback: string[] = codeCellOutput.traceback as string[]; - const stripped = traceback.map(stripAnsi).join('\r\n'); - const error = localize.DataScience.jupyterGetVariablesExecutionError().format(stripped); - traceError(error); - throw new Error(error); - } - } - } - - throw new Error(localize.DataScience.jupyterGetVariablesBadResults()); - } -} diff --git a/src/client/datascience/jupyter/jupyterWaitForIdleError.ts b/src/client/datascience/jupyter/jupyterWaitForIdleError.ts deleted file mode 100644 index 0f6626843918..000000000000 --- a/src/client/datascience/jupyter/jupyterWaitForIdleError.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export class JupyterWaitForIdleError extends Error { - constructor(message: string) { - super(message); - } -} diff --git a/src/client/datascience/jupyter/jupyterWebSocket.ts b/src/client/datascience/jupyter/jupyterWebSocket.ts deleted file mode 100644 index 58c7dd969130..000000000000 --- a/src/client/datascience/jupyter/jupyterWebSocket.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as WebSocketWS from 'ws'; - -// We need to override the websocket that jupyter lab services uses to put in our cookie information -// Do this as a function so that we can pass in variables the the socket will have local access to -export function createJupyterWebSocket(cookieString?: string, allowUnauthorized?: boolean) { - class JupyterWebSocket extends WebSocketWS { - constructor(url: string, protocols?: string | string[] | undefined) { - let co: WebSocketWS.ClientOptions = {}; - - if (allowUnauthorized) { - co = {...co, rejectUnauthorized: false}; - } - - if (cookieString) { - co = {...co, headers: { - Cookie: cookieString - }}; - } - - super(url, protocols, co); - } - } - return JupyterWebSocket; -} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts deleted file mode 100644 index 433a1e0a6fbd..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterExecution.ts +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { injectable } from 'inversify'; -import * as uuid from 'uuid/v4'; -import { CancellationToken } from 'vscode'; - -import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory } 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 { IInterpreterService, IKnownSearchPathsForInterpreters, PythonInterpreter } from '../../../interpreter/contracts'; -import { IServiceContainer } from '../../../ioc/types'; -import { LiveShare, LiveShareCommands } from '../../constants'; -import { - IConnection, - IJupyterCommandFactory, - IJupyterSessionManager, - INotebookServer, - INotebookServerOptions -} from '../../types'; -import { JupyterConnectError } from '../jupyterConnectError'; -import { JupyterExecutionBase } from '../jupyterExecution'; -import { GuestJupyterSessionManager } from './guestJupyterSessionManager'; -import { LiveShareParticipantGuest } from './liveShareParticipantMixin'; -import { ServerCache } from './serverCache'; - -// This class is really just a wrapper around a jupyter execution that also provides a shared live share service -@injectable() -export class GuestJupyterExecution extends LiveShareParticipantGuest(JupyterExecutionBase, LiveShare.JupyterExecutionService) { - private serverCache : ServerCache; - - constructor( - liveShare: ILiveShareApi, - executionFactory: IPythonExecutionFactory, - interpreterService: IInterpreterService, - processServiceFactory: IProcessServiceFactory, - knownSearchPaths: IKnownSearchPathsForInterpreters, - logger: ILogger, - disposableRegistry: IDisposableRegistry, - asyncRegistry: IAsyncDisposableRegistry, - fileSystem: IFileSystem, - sessionManager: IJupyterSessionManager, - workspace: IWorkspaceService, - configuration: IConfigurationService, - commandFactory : IJupyterCommandFactory, - serviceContainer: IServiceContainer) { - super( - liveShare, - executionFactory, - interpreterService, - processServiceFactory, - knownSearchPaths, - logger, - disposableRegistry, - asyncRegistry, - fileSystem, - new GuestJupyterSessionManager(sessionManager), // Don't talk to the active session on the guest side. - workspace, - configuration, - commandFactory, - serviceContainer); - asyncRegistry.push(this); - this.serverCache = new ServerCache(configuration, workspace, fileSystem); - } - - public async dispose() : Promise { - await super.dispose(); - - // Dispose of all of our cached servers - await this.serverCache.dispose(); - } - - public async isNotebookSupported(cancelToken?: CancellationToken): Promise { - return this.checkSupported(LiveShareCommands.isNotebookSupported, cancelToken); - } - public isImportSupported(cancelToken?: CancellationToken): Promise { - return this.checkSupported(LiveShareCommands.isImportSupported, cancelToken); - } - public isKernelCreateSupported(cancelToken?: CancellationToken): Promise { - return this.checkSupported(LiveShareCommands.isKernelCreateSupported, cancelToken); - } - public isKernelSpecSupported(cancelToken?: CancellationToken): Promise { - return this.checkSupported(LiveShareCommands.isKernelSpecSupported, cancelToken); - } - public isSpawnSupported(_cancelToken?: CancellationToken): Promise { - return Promise.resolve(false); - } - public async connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { - let result: INotebookServer | undefined = await this.serverCache.get(options); - - // See if we already have this server or not. - if (result) { - return result; - } - - // Create the server on the remote machine. It should return an IConnection we can use to build a remote uri - const service = await this.waitForService(); - if (service) { - const purpose = options ? options.purpose : uuid(); - const connection: IConnection = await service.request( - LiveShareCommands.connectToNotebookServer, - [options], - cancelToken); - - // If that works, then treat this as a remote server and connect to it - if (connection && connection.baseUrl) { - const newUri = `${connection.baseUrl}?token=${connection.token}`; - result = await super.connectToNotebookServer( - { - uri: newUri, - useDefaultConfig: options && options.useDefaultConfig, - workingDir: options ? options.workingDir : undefined, - purpose - }, - cancelToken); - // Save in our cache - if (result) { - await this.serverCache.set(result, noop, options); - } - } - } - - if (!result) { - throw new JupyterConnectError(localize.DataScience.liveShareConnectFailure()); - } - - return result; - } - public spawnNotebook(_file: string): Promise { - // Not supported in liveshare - throw new Error(localize.DataScience.liveShareCannotSpawnNotebooks()); - } - - public async getUsableJupyterPython(cancelToken?: CancellationToken): Promise { - const service = await this.waitForService(); - if (service) { - return service.request(LiveShareCommands.getUsableJupyterPython, [], cancelToken); - } - } - - public async getServer(options?: INotebookServerOptions) : Promise { - return this.serverCache.get(options); - } - - private async checkSupported(command: string, cancelToken?: CancellationToken) : Promise { - const service = await this.waitForService(); - - // Make a remote call on the proxy - if (service) { - const result = await service.request(command, [], cancelToken); - return result as boolean; - } - - return false; - } -} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts b/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts deleted file mode 100644 index ed4e7443966f..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterServer.ts +++ /dev/null @@ -1,216 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Observable } from 'rxjs/Observable'; -import { CancellationToken } from 'vscode-jsonrpc'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi } from '../../../common/application/types'; -import { CancellationError } from '../../../common/cancellation'; -import { traceInfo } from '../../../common/logger'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../../common/types'; -import { createDeferred, Deferred } from '../../../common/utils/async'; -import * as localize from '../../../common/utils/localize'; -import { LiveShare, LiveShareCommands } from '../../constants'; -import { - ICell, - IConnection, - IDataScience, - IJupyterSessionManager, - INotebookCompletion, - INotebookExecutionLogger, - INotebookServer, - INotebookServerLaunchInfo, - InterruptResult -} from '../../types'; -import { LiveShareParticipantDefault, LiveShareParticipantGuest } from './liveShareParticipantMixin'; -import { ResponseQueue } from './responseQueue'; -import { IExecuteObservableResponse, ILiveShareParticipant, IServerResponse } from './types'; - -export class GuestJupyterServer - extends LiveShareParticipantGuest(LiveShareParticipantDefault, LiveShare.JupyterServerSharedService) - implements INotebookServer, ILiveShareParticipant { - private launchInfo: INotebookServerLaunchInfo | undefined; - private responseQueue: ResponseQueue = new ResponseQueue(); - private connectPromise: Deferred = createDeferred(); - - constructor( - liveShare: ILiveShareApi, - private dataScience: IDataScience, - _logger: ILogger, - private disposableRegistry: IDisposableRegistry, - _asyncRegistry: IAsyncDisposableRegistry, - private configService: IConfigurationService, - _sessionManager: IJupyterSessionManager, - _loggers: INotebookExecutionLogger[] - ) { - super(liveShare); - } - - public async connect(launchInfo: INotebookServerLaunchInfo, _cancelToken?: CancellationToken): Promise { - this.launchInfo = launchInfo; - this.connectPromise.resolve(launchInfo); - return Promise.resolve(); - } - - public async shutdown(): Promise { - // Send this across to the other side. Otherwise the host server will remain running (like during an export) - const service = await this.waitForService(); - if (service) { - await service.request(LiveShareCommands.disposeServer, []); - } - } - - public dispose(): Promise { - return this.shutdown(); - } - - public waitForIdle(): Promise { - return Promise.resolve(); - } - - public async execute(code: string, file: string, line: number, id: string, cancelToken?: CancellationToken): Promise { - // Create a deferred that we'll fire when we're done - const deferred = createDeferred(); - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.executeObservable(code, file, line, id); - let output: ICell[]; - - observable.subscribe( - (cells: ICell[]) => { - output = cells; - }, - (error) => { - deferred.reject(error); - }, - () => { - deferred.resolve(output); - }); - - if (cancelToken) { - this.disposableRegistry.push(cancelToken.onCancellationRequested(() => deferred.reject(new CancellationError()))); - } - - // Wait for the execution to finish - return deferred.promise; - } - - public setInitialDirectory(_directory: string): Promise { - // Ignore this command on this side - return Promise.resolve(); - } - - public async setMatplotLibStyle(_useDark: boolean): Promise { - // Guest can't change the style. Maybe output a warning here? - } - - public executeObservable(code: string, file: string, line: number, id: string): Observable { - // Mimic this to the other side and then wait for a response - this.waitForService().then(s => { - if (s) { - s.notify(LiveShareCommands.executeObservable, { code, file, line, id }); - } - }).ignoreErrors(); - return this.responseQueue.waitForObservable(code, id); - } - - public async restartKernel(): Promise { - // We need to force a restart on the host side - return this.sendRequest(LiveShareCommands.restart, []); - } - - public async interruptKernel(_timeoutMs: number): Promise { - const settings = this.configService.getSettings(); - const interruptTimeout = settings.datascience.jupyterInterruptTimeout; - - const response = await this.sendRequest(LiveShareCommands.interrupt, [interruptTimeout]); - return (response as InterruptResult); - } - - // Return a copy of the connection information that this server used to connect with - public getConnectionInfo(): IConnection | undefined { - if (this.launchInfo) { - return this.launchInfo.connectionInfo; - } - - return undefined; - } - - public waitForConnect(): Promise { - return this.connectPromise.promise; - } - - public async waitForServiceName(): Promise { - // First wait for connect to occur - const launchInfo = await this.waitForConnect(); - - // Use our base name plus our purpose. This means one unique server per purpose - if (!launchInfo) { - return LiveShare.JupyterServerSharedService; - } - // tslint:disable-next-line:no-suspicious-comment - // TODO: Should there be some separator in the name? - return `${LiveShare.JupyterServerSharedService}${launchInfo.purpose}`; - } - - public async getSysInfo(): Promise { - // This is a special case. Ask the shared server - const service = await this.waitForService(); - if (service) { - const result = await service.request(LiveShareCommands.getSysInfo, []); - return (result as ICell); - } - } - - public async getCompletion(_cellCode: string, _offsetInCode: number, _cancelToken?: CancellationToken): Promise { - return Promise.resolve({ - matches: [], - cursor: { - start: 0, - end: 0 - }, - metadata: {} - }); - } - - public async onAttach(api: vsls.LiveShare | null): Promise { - await super.onAttach(api); - - if (api) { - const service = await this.waitForService(); - - // Wait for sync up - const synced = service ? await service.request(LiveShareCommands.syncRequest, []) : undefined; - if (!synced && api.session && api.session.role !== vsls.Role.None) { - throw new Error(localize.DataScience.liveShareSyncFailure()); - } - - if (service) { - // Listen to responses - service.onNotify(LiveShareCommands.serverResponse, this.onServerResponse); - - // Request all of the responses since this guest was started. We likely missed a bunch - service.notify(LiveShareCommands.catchupRequest, { since: this.dataScience.activationStartTime }); - } - } - } - - private onServerResponse = (args: Object) => { - const er = args as IExecuteObservableResponse; - traceInfo(`Guest serverResponse ${er.pos} ${er.id}`); - // Args should be of type ServerResponse. Stick in our queue if so. - if (args.hasOwnProperty('type')) { - this.responseQueue.push(args as IServerResponse); - } - } - - // tslint:disable-next-line:no-any - private async sendRequest(command: string, args: any[]): Promise { - const service = await this.waitForService(); - if (service) { - return service.request(command, args); - } - } - -} diff --git a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts b/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts deleted file mode 100644 index 517eaa655569..000000000000 --- a/src/client/datascience/jupyter/liveshare/guestJupyterSessionManager.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { CancellationToken } from 'vscode-jsonrpc'; - -import { noop } from '../../../../test/core'; -import { IConnection, IJupyterKernelSpec, IJupyterSession, IJupyterSessionManager } from '../../types'; - -export class GuestJupyterSessionManager implements IJupyterSessionManager { - - public constructor(private realSessionManager : IJupyterSessionManager) { - noop(); - } - - public startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken) : Promise { - return this.realSessionManager.startNew(connInfo, kernelSpec, cancelToken); - } - - public async getActiveKernelSpecs(_connection: IConnection) : Promise { - // Don't return any kernel specs in guest mode. They're only needed for the host side - return Promise.resolve([]); - } - -} diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts b/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts deleted file mode 100644 index 8e94f0ad0179..000000000000 --- a/src/client/datascience/jupyter/liveshare/hostJupyterExecution.ts +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import { CancellationToken } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi, IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IProcessServiceFactory, IPythonExecutionFactory } from '../../../common/process/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../../common/types'; -import { noop } from '../../../common/utils/misc'; -import { IInterpreterService, IKnownSearchPathsForInterpreters } from '../../../interpreter/contracts'; -import { IServiceContainer } from '../../../ioc/types'; -import { LiveShare, LiveShareCommands } from '../../constants'; -import { - IConnection, - IJupyterCommandFactory, - IJupyterExecution, - IJupyterSessionManager, - INotebookServer, - INotebookServerOptions -} from '../../types'; -import { JupyterExecutionBase } from '../jupyterExecution'; -import { LiveShareParticipantHost } from './liveShareParticipantMixin'; -import { IRoleBasedObject } from './roleBasedFactory'; -import { ServerCache } from './serverCache'; - -// tslint:disable:no-any - -// This class is really just a wrapper around a jupyter execution that also provides a shared live share service -export class HostJupyterExecution - extends LiveShareParticipantHost(JupyterExecutionBase, LiveShare.JupyterExecutionService) - implements IRoleBasedObject, IJupyterExecution { - private serverCache: ServerCache; - constructor( - liveShare: ILiveShareApi, - executionFactory: IPythonExecutionFactory, - interpreterService: IInterpreterService, - processServiceFactory: IProcessServiceFactory, - knownSearchPaths: IKnownSearchPathsForInterpreters, - logger: ILogger, - disposableRegistry: IDisposableRegistry, - asyncRegistry: IAsyncDisposableRegistry, - fileSys: IFileSystem, - sessionManager: IJupyterSessionManager, - workspace: IWorkspaceService, - configService: IConfigurationService, - commandFactory: IJupyterCommandFactory, - serviceContainer: IServiceContainer) { - super( - liveShare, - executionFactory, - interpreterService, - processServiceFactory, - knownSearchPaths, - logger, - disposableRegistry, - asyncRegistry, - fileSys, - sessionManager, - workspace, - configService, - commandFactory, - serviceContainer); - this.serverCache = new ServerCache(configService, workspace, fileSys); - } - - public async dispose(): Promise { - await super.dispose(); - const api = await this.api; - await this.onDetach(api); - - // Cleanup on dispose. We are going away permanently - if (this.serverCache) { - await this.serverCache.dispose(); - } - } - - public async connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise { - // See if we have this server in our cache already or not - let result = await this.serverCache.get(options); - if (result) { - return result; - } else { - // Create the server - result = await super.connectToNotebookServer(await this.serverCache.generateDefaultOptions(options), cancelToken); - - // Save in our cache - if (result) { - await this.serverCache.set(result, noop, options); - } - return result; - } - } - - public async onAttach(api: vsls.LiveShare | null): Promise { - await super.onAttach(api); - - if (api) { - const service = await this.waitForService(); - - // Register handlers for all of the supported remote calls - if (service) { - service.onRequest(LiveShareCommands.isNotebookSupported, this.onRemoteIsNotebookSupported); - service.onRequest(LiveShareCommands.isImportSupported, this.onRemoteIsImportSupported); - service.onRequest(LiveShareCommands.isKernelCreateSupported, this.onRemoteIsKernelCreateSupported); - service.onRequest(LiveShareCommands.isKernelSpecSupported, this.onRemoteIsKernelSpecSupported); - service.onRequest(LiveShareCommands.connectToNotebookServer, this.onRemoteConnectToNotebookServer); - service.onRequest(LiveShareCommands.getUsableJupyterPython, this.onRemoteGetUsableJupyterPython); - } - } - } - - public async onDetach(api: vsls.LiveShare | null): Promise { - await super.onDetach(api); - - // clear our cached servers if our role is no longer host or none - const newRole = api === null || (api.session && api.session.role !== vsls.Role.Guest) ? - vsls.Role.Host : vsls.Role.Guest; - if (newRole !== vsls.Role.Host) { - await this.serverCache.dispose(); - } - } - - public getServer(options?: INotebookServerOptions): Promise { - // See if we have this server or not. - return this.serverCache.get(options); - } - - private onRemoteIsNotebookSupported = (_args: any[], cancellation: CancellationToken): Promise => { - // Just call local - return this.isNotebookSupported(cancellation); - } - - private onRemoteIsImportSupported = (_args: any[], cancellation: CancellationToken): Promise => { - // Just call local - return this.isImportSupported(cancellation); - } - - private onRemoteIsKernelCreateSupported = (_args: any[], cancellation: CancellationToken): Promise => { - // Just call local - return this.isKernelCreateSupported(cancellation); - } - private onRemoteIsKernelSpecSupported = (_args: any[], cancellation: CancellationToken): Promise => { - // Just call local - return this.isKernelSpecSupported(cancellation); - } - - private onRemoteConnectToNotebookServer = async (args: any[], cancellation: CancellationToken): Promise => { - // Connect to the local server. THe local server should have started the port forwarding already - const localServer = await this.connectToNotebookServer(args[0] as INotebookServerOptions | undefined, cancellation); - - // Extract the URI and token for the other side - if (localServer) { - // The other side should be using 'localhost' for anything it's port forwarding. That should just remap - // on the guest side. However we need to eliminate the dispose method. Methods are not serializable - const connectionInfo = localServer.getConnectionInfo(); - if (connectionInfo) { - return { - baseUrl: connectionInfo.baseUrl, - token: connectionInfo.token, - localLaunch: false, - localProcExitCode: undefined, - disconnected: (_l) => { return { dispose: noop }; }, - dispose: noop - }; - } - } - } - - private onRemoteGetUsableJupyterPython = (_args: any[], cancellation: CancellationToken): Promise => { - // Just call local - return this.getUsableJupyterPython(cancellation); - } -} diff --git a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts b/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts deleted file mode 100644 index bb1ffa3a412e..000000000000 --- a/src/client/datascience/jupyter/liveshare/hostJupyterServer.ts +++ /dev/null @@ -1,362 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import * as os from 'os'; -import { Observable } from 'rxjs/Observable'; -import * as vscode from 'vscode'; -import { CancellationToken } from 'vscode-jsonrpc'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi } from '../../../common/application/types'; -import { IAsyncDisposableRegistry, IConfigurationService, IDisposableRegistry, ILogger } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import * as localize from '../../../common/utils/localize'; -import { Identifiers, LiveShare, LiveShareCommands, RegExpValues } from '../../constants'; -import { IExecuteInfo } from '../../interactive-window/interactiveWindowTypes'; -import { - ICell, - IDataScience, - IJupyterSessionManager, - INotebookExecutionLogger, - INotebookServer, - INotebookServerLaunchInfo, - InterruptResult -} from '../../types'; -import { JupyterServerBase } from '../jupyterServer'; -import { LiveShareParticipantHost } from './liveShareParticipantMixin'; -import { ResponseQueue } from './responseQueue'; -import { IRoleBasedObject } from './roleBasedFactory'; -import { IExecuteObservableResponse, IResponseMapping, IServerResponse, ServerResponseType } from './types'; - -// tslint:disable:no-any - -export class HostJupyterServer - extends LiveShareParticipantHost(JupyterServerBase, LiveShare.JupyterServerSharedService) - implements IRoleBasedObject, INotebookServer { - private catchupResponses: ResponseQueue = new ResponseQueue(); - private localResponses: ResponseQueue = new ResponseQueue(); - private requestLog: Map = new Map(); - private catchupPendingCount: number = 0; - private disposed = false; - private portToForward = 0; - private sharedPort: vscode.Disposable | undefined; - constructor( - liveShare: ILiveShareApi, - dataScience: IDataScience, - logger: ILogger, - disposableRegistry: IDisposableRegistry, - asyncRegistry: IAsyncDisposableRegistry, - configService: IConfigurationService, - sessionManager: IJupyterSessionManager, - loggers: INotebookExecutionLogger[]) { - super(liveShare, dataScience, logger, disposableRegistry, asyncRegistry, configService, sessionManager, loggers); - } - - public async dispose(): Promise { - if (!this.disposed) { - this.disposed = true; - await super.dispose(); - const api = await this.api; - return this.onDetach(api); - } - } - - public async connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise { - if (launchInfo.connectionInfo && launchInfo.connectionInfo.localLaunch) { - const portMatch = RegExpValues.ExtractPortRegex.exec(launchInfo.connectionInfo.baseUrl); - if (portMatch && portMatch.length > 1) { - const port = parseInt(portMatch[1], 10); - await this.attemptToForwardPort(this.finishedApi, port); - } - } - return super.connect(launchInfo, cancelToken); - } - - public async onAttach(api: vsls.LiveShare | null): Promise { - await super.onAttach(api); - - if (api && !this.disposed) { - const service = await this.waitForService(); - - // Attach event handlers to different requests - if (service) { - // Requests return arrays - service.onRequest(LiveShareCommands.syncRequest, (_args: any[], _cancellation: CancellationToken) => this.onSync()); - service.onRequest(LiveShareCommands.getSysInfo, (_args: any[], cancellation: CancellationToken) => this.onGetSysInfoRequest(cancellation)); - service.onRequest(LiveShareCommands.restart, (args: any[], cancellation: CancellationToken) => this.onRestartRequest(args.length > 0 ? args[0] as number : LiveShare.InterruptDefaultTimeout, cancellation)); - service.onRequest(LiveShareCommands.interrupt, (args: any[], cancellation: CancellationToken) => this.onInterruptRequest(args.length > 0 ? args[0] as number : LiveShare.InterruptDefaultTimeout, cancellation)); - service.onRequest(LiveShareCommands.disposeServer, (_args: any[], _cancellation: CancellationToken) => this.dispose()); - - // Notifications are always objects. - service.onNotify(LiveShareCommands.catchupRequest, (args: object) => this.onCatchupRequest(args)); - service.onNotify(LiveShareCommands.executeObservable, (args: object) => this.onExecuteObservableRequest(args)); - - // See if we need to forward the port - await this.attemptToForwardPort(api, this.portToForward); - } - } - } - - public async onDetach(api: vsls.LiveShare | null): Promise { - await super.onDetach(api); - - // Make sure to unshare our port - if (api && this.sharedPort) { - this.sharedPort.dispose(); - this.sharedPort = undefined; - } - } - - public async waitForServiceName(): Promise { - // First wait for connect to occur - const launchInfo = await this.waitForConnect(); - - // Use our base name plus our purpose. This means one unique server per purpose - if (!launchInfo) { - return LiveShare.JupyterServerSharedService; - } - // tslint:disable-next-line:no-suspicious-comment - // TODO: Should there be some separator in the name? - return `${LiveShare.JupyterServerSharedService}${launchInfo.purpose}`; - } - - public async onPeerChange(ev: vsls.PeersChangeEvent): Promise { - await super.onPeerChange(ev); - - // Keep track of the number of guests that need to do a catchup request - this.catchupPendingCount += - ev.added.filter(e => e.role === vsls.Role.Guest).length - - ev.removed.filter(e => e.role === vsls.Role.Guest).length; - } - - public executeObservable(code: string, file: string, line: number, id: string, silent?: boolean): Observable { - // See if this has already been asked for not - if (this.requestLog.has(id)) { - // This must be a local call that occurred after a guest call. Just - // use the local responses to return the results. - return this.localResponses.waitForObservable(code, id); - } else { - // Otherwise make a new request and save response in the catchup list. THis is a - // a request that came directly from the host so the host will be listening to the observable returned - // and we don't need to save the response in the local queue. - return this.makeObservableRequest(code, file, line, id, silent, [this.catchupResponses]); - } - } - - public async restartKernel(timeoutMs: number): Promise { - try { - await super.restartKernel(timeoutMs); - } catch (exc) { - this.postException(exc, []); - throw exc; - } - } - - public async interruptKernel(timeoutMs: number): Promise { - try { - return super.interruptKernel(timeoutMs); - } catch (exc) { - this.postException(exc, []); - throw exc; - } - } - - private makeRequest(code: string, file: string, line: number, id: string, silent: boolean | undefined, responseQueues: ResponseQueue[]): Promise { - // Create a deferred that we'll fire when we're done - const deferred = createDeferred(); - - // Attempt to evaluate this cell in the jupyter notebook - const observable = this.makeObservableRequest(code, file, line, id, silent, responseQueues); - let output: ICell[]; - - observable.subscribe( - (cells: ICell[]) => { - output = cells; - }, - (error) => { - deferred.reject(error); - }, - () => { - deferred.resolve(output); - }); - - // Wait for the execution to finish - return deferred.promise; - } - - private makeObservableRequest(code: string, file: string, line: number, id: string, silent: boolean | undefined, responseQueues: ResponseQueue[]): Observable { - try { - this.requestLog.set(id, Date.now()); - const inner = super.executeObservable(code, file, line, id, silent); - - // Cleanup old requests - const now = Date.now(); - for (const [k, val] of this.requestLog) { - if (now - val > LiveShare.ResponseLifetime) { - this.requestLog.delete(k); - } - } - - // Wrap the observable returned to send the responses to the guest(s) too. - return this.postObservableResult(code, inner, id, responseQueues); - } catch (exc) { - this.postException(exc, responseQueues); - throw exc; - } - - } - - private async attemptToForwardPort(api: vsls.LiveShare | null | undefined, port: number): Promise { - if (port !== 0 && api && api.session && api.session.role === vsls.Role.Host) { - this.portToForward = 0; - this.sharedPort = await api.shareServer({ port, displayName: localize.DataScience.liveShareHostFormat().format(os.hostname()) }); - } else { - this.portToForward = port; - } - } - - private translateCellForGuest(cell: ICell): ICell { - const copy = { ...cell }; - if (this.role === vsls.Role.Host && this.finishedApi && copy.file !== Identifiers.EmptyFileName) { - copy.file = this.finishedApi.convertLocalUriToShared(vscode.Uri.file(copy.file)).fsPath; - } - return copy; - } - - private onSync(): Promise { - return Promise.resolve(true); - } - - private onGetSysInfoRequest(_cancellation: CancellationToken): Promise { - // Get the sys info from our local server - return super.getSysInfo(); - } - - private onRestartRequest(timeout: number, _cancellation: CancellationToken): Promise { - // Just call the base - return super.restartKernel(timeout); - } - private onInterruptRequest(timeout: number, _cancellation: CancellationToken): Promise { - // Just call the base - return super.interruptKernel(timeout); - } - - private async onCatchupRequest(args: object): Promise { - if (args.hasOwnProperty('since')) { - const service = await this.waitForService(); - if (service) { - // Send results for all responses that are left. - this.catchupResponses.send(service, this.translateForGuest.bind(this)); - - // Eliminate old responses if possible. - this.catchupPendingCount -= 1; - if (this.catchupPendingCount <= 0) { - this.catchupResponses.clear(); - } - } - } - } - - private onExecuteObservableRequest(args: object) { - // See if we started this execute or not already. - if (args.hasOwnProperty('code')) { - const obj = args as IExecuteInfo; - if (!this.requestLog.has(obj.id)) { - try { - // Convert the file name if necessary - const uri = vscode.Uri.parse(`vsls:${obj.file}`); - const file = this.finishedApi && obj.file !== Identifiers.EmptyFileName ? this.finishedApi.convertSharedUriToLocal(uri).fsPath : obj.file; - - // We need the results of this execute to end up in both the guest responses and the local responses - this.makeRequest(obj.code, file, obj.line, obj.id, false, [this.localResponses, this.catchupResponses]).ignoreErrors(); - } catch (e) { - this.logger.logError(e); - } - } - } - } - - private postObservableResult(code: string, observable: Observable, id: string, responseQueues: ResponseQueue[]): Observable { - return new Observable(subscriber => { - let pos = 0; - - // Listen to all of the events on the observable passed in. - observable.subscribe(cells => { - // Forward to the next listener - subscriber.next(cells); - - // Send across to the guest side - try { - this.postObservableNext(code, pos, cells, id, responseQueues); - pos += 1; - } catch (e) { - subscriber.error(e); - this.postException(e, responseQueues); - } - }, - e => { - subscriber.error(e); - this.postException(e, responseQueues); - }, - () => { - subscriber.complete(); - this.postObservableComplete(code, pos, id, responseQueues); - }); - }); - } - - private translateForGuest = (r: IServerResponse): IServerResponse => { - // Remap the cell paths - const er = r as IExecuteObservableResponse; - if (er && er.cells) { - return { cells: er.cells.map(this.translateCellForGuest, this), ...er }; - } - return r; - } - - private postObservableNext(code: string, pos: number, cells: ICell[], id: string, responseQueues: ResponseQueue[]) { - this.postResult( - ServerResponseType.ExecuteObservable, - { code, pos, type: ServerResponseType.ExecuteObservable, cells, id, time: Date.now() }, - this.translateForGuest, - responseQueues); - } - - private postObservableComplete(code: string, pos: number, id: string, responseQueues: ResponseQueue[]) { - this.postResult( - ServerResponseType.ExecuteObservable, - { code, pos, type: ServerResponseType.ExecuteObservable, cells: undefined, id, time: Date.now() }, - this.translateForGuest, - responseQueues); - } - - private postException(exc: any, responseQueues: ResponseQueue[]) { - this.postResult( - ServerResponseType.Exception, - { type: ServerResponseType.Exception, time: Date.now(), message: exc.toString() }, - r => r, - responseQueues); - } - - private postResult( - _type: T, - result: R[T], - guestTranslator: (r: IServerResponse) => IServerResponse, - responseQueues: ResponseQueue[]): void { - const typedResult = ((result as any) as IServerResponse); - if (typedResult) { - // Make a deep copy before we send. Don't want local copies being modified - const deepCopy = JSON.parse(JSON.stringify(typedResult)); - this.waitForService().then(s => { - if (s) { - s.notify(LiveShareCommands.serverResponse, guestTranslator(deepCopy)); - } - }).ignoreErrors(); - - // Need to also save in memory for those guests that are in the middle of starting up - responseQueues.forEach(r => r.push(deepCopy)); - } - } -} diff --git a/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts b/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts deleted file mode 100644 index b68f829344f9..000000000000 --- a/src/client/datascience/jupyter/liveshare/liveShareParticipantMixin.ts +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi } from '../../../common/application/types'; -import '../../../common/extensions'; -import { IAsyncDisposable } from '../../../common/types'; -import { noop } from '../../../common/utils/misc'; -import { ClassType } from '../../../ioc/types'; -import { ILiveShareParticipant } from './types'; -import { waitForGuestService, waitForHostService } from './utils'; - -// tslint:disable:no-any - -export class LiveShareParticipantDefault implements IAsyncDisposable { - constructor(..._rest: any[]) { - noop(); - } - - public async dispose() : Promise { - noop(); - } -} - -export function LiveShareParticipantGuest>(SuperClass: T, serviceName: string) { - return LiveShareParticipantMixin(SuperClass, vsls.Role.Guest, serviceName, waitForGuestService); -} - -export function LiveShareParticipantHost>(SuperClass: T, serviceName: string) { - return LiveShareParticipantMixin(SuperClass, vsls.Role.Host, serviceName, waitForHostService); -} - -/** - * This is called a mixin class in TypeScript. - * Allows us to have different base classes but inherit behavior (workaround for not allowing multiple inheritance). - * Essentially it sticks a temp class in between the base class and the class you're writing. - * Something like this: - * - * class Base { - * doStuff() { - * - * } - * } - * - * function Mixin = (SuperClass) { - * return class extends SuperClass { - * doExtraStuff() { - * super.doStuff(); - * } - * } - * } - * - * function SubClass extends Mixin(Base) { - * doBar() : { - * super.doExtraStuff(); - * } - * } - * - */ -function LiveShareParticipantMixin, S>( - SuperClass: T, - expectedRole: vsls.Role, - serviceName: string, - serviceWaiter: (api: vsls.LiveShare, name: string) => Promise) { - return class extends SuperClass implements ILiveShareParticipant { - protected finishedApi: vsls.LiveShare | null | undefined; - protected api: Promise; - private actualRole = vsls.Role.None; - private wantedRole = expectedRole; - private servicePromise: Promise | undefined; - private serviceFullName: string | undefined; - - constructor(...rest: any[]) { - super(...rest); - // First argument should be our live share api - if (rest.length > 0) { - const liveShare = rest[0] as ILiveShareApi; - this.api = liveShare.getApi(); - this.api.then(a => { - this.finishedApi = a; - this.onSessionChange(a).ignoreErrors(); - }).ignoreErrors(); - } else { - this.api = Promise.resolve(null); - } - } - - public get role() { - return this.actualRole; - } - - public async onPeerChange(_ev: vsls.PeersChangeEvent) : Promise { - noop(); - } - - public async onAttach(_api: vsls.LiveShare | null) : Promise { - noop(); - } - - public waitForServiceName() : Promise { - // Default is just to return the server name - return Promise.resolve(serviceName); - } - - public onDetach(api: vsls.LiveShare | null) : Promise { - if (api && this.serviceFullName && api.session && api.session.role === vsls.Role.Host) { - return api.unshareService(this.serviceFullName); - } - return Promise.resolve(); - } - - public async onSessionChange(api: vsls.LiveShare | null) : Promise { - this.servicePromise = undefined; - const newRole = api !== null && api.session ? - api.session.role : vsls.Role.None; - if (newRole !== this.actualRole) { - this.actualRole = newRole; - if (newRole === this.wantedRole) { - this.onAttach(api).ignoreErrors(); - } else { - this.onDetach(api).ignoreErrors(); - } - } - } - - public async waitForService() : Promise { - if (this.servicePromise) { - return this.servicePromise; - } - const api = await this.api; - if (!api || (api.session.role !== this.wantedRole)) { - this.servicePromise = Promise.resolve(undefined); - } else { - this.serviceFullName = await this.waitForServiceName(); - this.servicePromise = serviceWaiter(api, this.serviceFullName); - } - - return this.servicePromise; - } - }; -} diff --git a/src/client/datascience/jupyter/liveshare/responseQueue.ts b/src/client/datascience/jupyter/liveshare/responseQueue.ts deleted file mode 100644 index e019a109cd52..000000000000 --- a/src/client/datascience/jupyter/liveshare/responseQueue.ts +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; -import * as vsls from 'vsls/vscode'; - -import { createDeferred, Deferred } from '../../../common/utils/async'; -import { LiveShareCommands } from '../../constants'; -import { ICell } from '../../types'; -import { IExecuteObservableResponse, IServerResponse } from './types'; - -export class ResponseQueue { - private responseQueue : IServerResponse [] = []; - private waitingQueue : { deferred: Deferred; predicate(r: IServerResponse) : boolean }[] = []; - - public waitForObservable(code: string, id: string) : Observable { - // Create a wrapper observable around the actual server - return new Observable(subscriber => { - // Wait for the observable responses to come in - this.waitForResponses(subscriber, code, id) - .catch(e => { - subscriber.error(e); - subscriber.complete(); - }); - }); - } - - public push(response: IServerResponse) { - this.responseQueue.push(response); - this.dispatchResponse(response); - } - - public send(service: vsls.SharedService, translator: (r: IServerResponse) => IServerResponse) { - this.responseQueue.forEach(r => service.notify(LiveShareCommands.serverResponse, translator(r))); - } - - public clear() { - this.responseQueue = []; - } - - private async waitForResponses(subscriber: Subscriber, code: string, id: string) : Promise { - let pos = 0; - let cells: ICell[] | undefined = []; - while (cells !== undefined) { - // Find all matches in order - const response = await this.waitForSpecificResponse(r => { - return (r.pos === pos) && - (id === r.id) && - (code === r.code); - }); - if (response.cells) { - subscriber.next(response.cells); - pos += 1; - } - cells = response.cells; - } - subscriber.complete(); - - // Clear responses after we respond to the subscriber. - this.responseQueue = this.responseQueue.filter(r => { - const er = r as IExecuteObservableResponse; - return er.id !== id; - }); - } - - private waitForSpecificResponse(predicate: (response: T) => boolean) : Promise { - // See if we have any responses right now with this type - const index = this.responseQueue.findIndex(r => predicate(r as T)); - if (index >= 0) { - // Pull off the match - const match = this.responseQueue[index]; - - // Return this single item - return Promise.resolve(match as T); - } else { - // We have to wait for a new input to happen - const waitable = { deferred: createDeferred(), predicate }; - this.waitingQueue.push(waitable); - return waitable.deferred.promise; - } - } - - private dispatchResponse(response: IServerResponse) { - // Look through all of our responses that are queued up and see if they make a - // waiting promise resolve - const matchIndex = this.waitingQueue.findIndex(w => w.predicate(response)); - if (matchIndex >= 0) { - this.waitingQueue[matchIndex].deferred.resolve(response); - this.waitingQueue.splice(matchIndex, 1); - } - } -} diff --git a/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts b/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts deleted file mode 100644 index 7f64d926118f..000000000000 --- a/src/client/datascience/jupyter/liveshare/roleBasedFactory.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as vscode from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi } from '../../../common/application/types'; -import { IAsyncDisposable } from '../../../common/types'; -import { ClassType } from '../../../ioc/types'; -import { ILiveShareParticipant } from './types'; - -export interface IRoleBasedObject extends IAsyncDisposable, ILiveShareParticipant { - -} - -// tslint:disable:no-any -export class RoleBasedFactory> { - private ctorArgs : any[]; - private firstTime : boolean = true; - private createPromise : Promise | undefined; - private sessionChangedEmitter = new vscode.EventEmitter(); - - constructor(private liveShare: ILiveShareApi, private hostCtor: CtorType, private guestCtor: CtorType, ...args: any[]) { - this.ctorArgs = args; - this.createPromise = this.createBasedOnRole(); // We need to start creation immediately or one side may call before we init. - } - - public get sessionChanged() : vscode.Event { - return this.sessionChangedEmitter.event; - } - - public get() : Promise { - // Make sure only one create happens at a time - if (this.createPromise) { - return this.createPromise; - } - this.createPromise = this.createBasedOnRole(); - return this.createPromise; - } - - private async createBasedOnRole() : Promise { - - // Figure out our role to compute the object to create. Default is host. This - // allows for the host object to keep existing if we suddenly start a new session. - // For a guest, starting a new session resets the entire workspace. - const api = await this.liveShare.getApi(); - let ctor : CtorType = this.hostCtor; - let role : vsls.Role = vsls.Role.Host; - - if (api) { - // Create based on role. - if (api.session && api.session.role === vsls.Role.Host) { - ctor = this.hostCtor; - } else if (api.session && api.session.role === vsls.Role.Guest) { - ctor = this.guestCtor; - role = vsls.Role.Guest; - } - } - - // Create our object - const obj = new ctor(...this.ctorArgs); - - // Rewrite the object's dispose so we can get rid of our own state. - let objDisposed = false; - const oldDispose = obj.dispose.bind(obj); - obj.dispose = () => { - objDisposed = true; - this.createPromise = undefined; - return oldDispose(); - }; - - // If the session changes, tell the listener - if (api && this.firstTime) { - this.firstTime = false; - api.onDidChangeSession((_a) => { - // Dispose the object if the role changes - const newRole = api !== null && api.session && api.session.role === vsls.Role.Guest ? - vsls.Role.Guest : vsls.Role.Host; - if (newRole !== role) { - obj.dispose().ignoreErrors(); - } - - // Update the object with respect to the api - if (!objDisposed) { - obj.onSessionChange(api).ignoreErrors(); - } - - // Fire our event indicating old data is no longer valid. - if (newRole !== role) { - this.sessionChangedEmitter.fire(); - } - }); - api.onDidChangePeers((e) => { - if (!objDisposed) { - obj.onPeerChange(e).ignoreErrors(); - } - }); - } - - return obj; - } -} diff --git a/src/client/datascience/jupyter/liveshare/serverCache.ts b/src/client/datascience/jupyter/liveshare/serverCache.ts deleted file mode 100644 index a3b05f880f0f..000000000000 --- a/src/client/datascience/jupyter/liveshare/serverCache.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../../common/extensions'; - -import * as path from 'path'; -import * as uuid from 'uuid/v4'; - -import { IWorkspaceService } from '../../../common/application/types'; -import { IFileSystem } from '../../../common/platform/types'; -import { IAsyncDisposable, IConfigurationService } from '../../../common/types'; -import { INotebookServer, INotebookServerOptions } from '../../types'; - -export class ServerCache implements IAsyncDisposable { - private cache: Map = new Map(); - private emptyKey = uuid(); - - constructor( - private configService: IConfigurationService, - private workspace: IWorkspaceService, - private fileSystem: IFileSystem - ) { } - - public async get(options?: INotebookServerOptions): Promise { - const fixedOptions = await this.generateDefaultOptions(options); - const key = this.generateKey(fixedOptions); - if (this.cache.has(key)) { - return this.cache.get(key); - } - } - - public async set(result: INotebookServer, disposeCallback: () => void, options?: INotebookServerOptions): Promise { - const fixedOptions = await this.generateDefaultOptions(options); - const key = this.generateKey(fixedOptions); - - // Eliminate any already with this key - const item = this.cache.get(key); - if (item) { - await item.dispose(); - } - - // Save in our cache. - this.cache.set(key, result); - - // Save this result, but modify its dispose such that we - // can detach from the server when it goes away. - const oldDispose = result.dispose.bind(result); - result.dispose = () => { - this.cache.delete(key); - disposeCallback(); - return oldDispose(); - }; - } - - public async dispose(): Promise { - for (const [, s] of this.cache) { - await s.dispose(); - } - this.cache.clear(); - } - - public async generateDefaultOptions(options?: INotebookServerOptions): Promise { - return { - enableDebugging: options ? options.enableDebugging : false, - uri: options ? options.uri : undefined, - useDefaultConfig: options ? options.useDefaultConfig : true, // Default for this is true. - usingDarkTheme: options ? options.usingDarkTheme : undefined, - purpose: options ? options.purpose : uuid(), - workingDir: options && options.workingDir ? options.workingDir : await this.calculateWorkingDirectory() - }; - } - - private generateKey(options?: INotebookServerOptions): string { - if (!options) { - return this.emptyKey; - } else { - // combine all the values together to make a unique key - const uri = options.uri ? options.uri : ''; - const useFlag = options.useDefaultConfig ? 'true' : 'false'; - const debug = options.enableDebugging ? 'true' : 'false'; - // tslint:disable-next-line:no-suspicious-comment - // TODO: Should there be some separator in the key? - return `${options.purpose}${uri}${useFlag}${options.workingDir}${debug}`; - } - } - - private async calculateWorkingDirectory(): Promise { - let workingDir: string | undefined; - // For a local launch calculate the working directory that we should switch into - const settings = this.configService.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.workspace.hasWorkspaceFolders) { - const workspaceFolderPath = this.workspace.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; - } - -} diff --git a/src/client/datascience/jupyter/liveshare/types.ts b/src/client/datascience/jupyter/liveshare/types.ts deleted file mode 100644 index bb454a413493..000000000000 --- a/src/client/datascience/jupyter/liveshare/types.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as vsls from 'vsls/vscode'; - -import { IAsyncDisposable } from '../../../common/types'; -import { ICell } from '../../types'; - -// tslint:disable:max-classes-per-file - -export enum ServerResponseType { - ExecuteObservable, - Exception -} - -export interface IServerResponse { - type: ServerResponseType; - time: number; -} - -export interface IExecuteObservableResponse extends IServerResponse { - pos: number; - code: string; - id: string; // Unique id so guest side can tell what observable it belongs with - cells: ICell[] | undefined; -} - -export interface IExceptionResponse extends IServerResponse { - message: string; -} - -// Map all responses to their properties -export interface IResponseMapping { - [ServerResponseType.ExecuteObservable]: IExecuteObservableResponse; - [ServerResponseType.Exception]: IExceptionResponse; -} - -export interface ICatchupRequest { - since: number; -} - -export interface ILiveShareParticipant extends IAsyncDisposable { - readonly role: vsls.Role; - onSessionChange(api: vsls.LiveShare | null) : Promise; - onAttach(api: vsls.LiveShare | null) : Promise; - onDetach(api: vsls.LiveShare | null) : Promise; - onPeerChange(ev: vsls.PeersChangeEvent) : Promise; - waitForServiceName() : Promise; -} diff --git a/src/client/datascience/jupyter/liveshare/utils.ts b/src/client/datascience/jupyter/liveshare/utils.ts deleted file mode 100644 index 0dde9c6f0b13..000000000000 --- a/src/client/datascience/jupyter/liveshare/utils.ts +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { Disposable, Event } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { createDeferred } from '../../../common/utils/async'; - -export async function waitForHostService(api: vsls.LiveShare, name: string) : Promise { - const service = await api.shareService(name); - if (service && !service.isServiceAvailable) { - return waitForAvailability(service); - } - return service; -} - -export async function waitForGuestService(api: vsls.LiveShare, name: string) : Promise { - const service = await api.getSharedService(name); - if (service && !service.isServiceAvailable) { - return waitForAvailability(service); - } - return service; -} - -interface IChangeWatchable { - readonly onDidChangeIsServiceAvailable: Event; -} - -async function waitForAvailability(service: T) : Promise { - const deferred = createDeferred(); - let disposable : Disposable | undefined; - try { - disposable = service.onDidChangeIsServiceAvailable(e => { - if (e) { - deferred.resolve(service); - } - }); - await deferred.promise; - } finally { - if (disposable) { - disposable.dispose(); - } - } - return service; -} diff --git a/src/client/datascience/liveshare/postOffice.ts b/src/client/datascience/liveshare/postOffice.ts deleted file mode 100644 index 6786d45e0735..000000000000 --- a/src/client/datascience/liveshare/postOffice.ts +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { JSONArray } from '@phosphor/coreutils'; -import * as vscode from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ILiveShareApi } from '../../common/application/types'; -import { IAsyncDisposable } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import { LiveShare } from '../constants'; - -// tslint:disable:no-any - -interface IMessageArgs { - args: string; -} - -// This class is used to register two communication between a host and all of its guests -export class PostOffice implements IAsyncDisposable { - - private name: string; - private startedPromise: Deferred | undefined; - private hostServer: vsls.SharedService | null = null; - private guestServer: vsls.SharedServiceProxy | null = null; - private currentRole: vsls.Role = vsls.Role.None; - private currentPeerCount: number = 0; - private peerCountChangedEmitter: vscode.EventEmitter = new vscode.EventEmitter(); - private commandMap: { [key: string]: { thisArg: any; callback(...args: any[]): void } } = {}; - - constructor( - name: string, - private liveShareApi: ILiveShareApi, - private hostArgsTranslator?: (api: vsls.LiveShare | null, command: string, role: vsls.Role, args: any[]) => void) { - this.name = name; - - // Note to self, could the callbacks be keeping things alive that we don't want to be alive? - } - - public get peerCount() { - return this.currentPeerCount; - } - - public get peerCountChanged(): vscode.Event { - return this.peerCountChangedEmitter.event; - } - - public get role() { - return this.currentRole; - } - - public async dispose() { - this.peerCountChangedEmitter.fire(); - this.peerCountChangedEmitter.dispose(); - if (this.hostServer) { - const s = await this.getApi(); - if (s !== null) { - await s.unshareService(this.name); - } - this.hostServer = null; - } - this.guestServer = null; - } - - public async postCommand(command: string, ...args: any[]): Promise { - // Make sure startup finished - const api = await this.getApi(); - let skipDefault = false; - - if (api && api.session) { - switch (this.currentRole) { - case vsls.Role.Guest: - // Ask host to broadcast - if (this.guestServer) { - this.guestServer.notify(LiveShare.LiveShareBroadcastRequest, this.createBroadcastArgs(command, ...args)); - } - skipDefault = true; - break; - case vsls.Role.Host: - // Notify everybody and call our local callback (by falling through) - if (this.hostServer) { - this.hostServer.notify(this.escapeCommandName(command), this.translateArgs(api, command, ...args)); - } - break; - default: - break; - } - } - - if (!skipDefault) { - // Default when not connected is to just call the registered callback - this.callCallback(command, ...args); - } - } - - public async registerCallback(command: string, callback: (...args: any[]) => void, thisArg?: any): Promise { - const api = await this.getApi(); - - // For a guest, make sure to register the notification - if (api && api.session && api.session.role === vsls.Role.Guest && this.guestServer) { - this.guestServer.onNotify(this.escapeCommandName(command), a => this.onGuestNotify(command, a as IMessageArgs)); - } - - // Always stick in the command map so that if we switch roles, we reregister - this.commandMap[command] = { callback, thisArg }; - } - - private createBroadcastArgs(command: string, ...args: any[]): IMessageArgs { - return { args: JSON.stringify([command, ...args]) }; - } - - private translateArgs(api: vsls.LiveShare, command: string, ...args: any[]): IMessageArgs { - // Make sure to eliminate all .toJSON functions on our arguments. Otherwise they're stringified incorrectly - for (let a = 0; a <= args.length; a += 1) { - // Eliminate this on only object types (https://stackoverflow.com/questions/8511281/check-if-a-value-is-an-object-in-javascript) - if (args[a] === Object(args[a])) { - args[a].toJSON = undefined; - } - } - - // Copy our args so we don't affect callers. - const copyArgs = JSON.parse(JSON.stringify(args)); - - // Some file path args need to have their values translated to guest - // uri format for use on a guest. Try to find any file arguments - const callback = this.commandMap.hasOwnProperty(command) ? this.commandMap[command].callback : undefined; - if (callback) { - // Give the passed in args translator a chance to attempt a translation - if (this.hostArgsTranslator) { - this.hostArgsTranslator(api, command, vsls.Role.Host, copyArgs); - } - } - - // Then wrap them all up in a string. - return { args: JSON.stringify(copyArgs) }; - } - - private escapeCommandName(command: string): string { - // Replace . with $ instead. - return command.replace(/\./g, '$'); - } - - private unescapeCommandName(command: string): string { - // Turn $ back into . - return command.replace(/\$/g, '.'); - } - - private onGuestNotify = (command: string, m: IMessageArgs) => { - const unescaped = this.unescapeCommandName(command); - const args = JSON.parse(m.args) as JSONArray; - this.callCallback(unescaped, ...args); - } - - private callCallback(command: string, ...args: any[]) { - const callback = this.getCallback(command); - if (callback) { - callback(...args); - } - } - - private getCallback(command: string): ((...args: any[]) => void) | undefined { - let callback = this.commandMap.hasOwnProperty(command) ? this.commandMap[command].callback : undefined; - if (callback) { - // Bind the this arg if necessary - const thisArg = this.commandMap[command].thisArg; - if (thisArg) { - callback = callback.bind(thisArg); - } - } - - return callback; - } - - private getApi() : Promise { - - if (!this.startedPromise) { - this.startedPromise = createDeferred(); - this.startCommandServer() - .then(v => this.startedPromise!.resolve(v)) - .catch(e => this.startedPromise!.reject(e)); - } - - return this.startedPromise.promise; - } - - private async startCommandServer(): Promise { - const api = await this.liveShareApi.getApi(); - if (api !== null) { - api.onDidChangeSession(() => this.onChangeSession(api).ignoreErrors()); - api.onDidChangePeers(() => this.onChangePeers(api).ignoreErrors()); - await this.onChangeSession(api); - await this.onChangePeers(api); - } - return api; - } - - private async onChangeSession(api: vsls.LiveShare): Promise { - // Startup or shutdown our connection to the other side - if (api.session) { - if (this.currentRole !== api.session.role) { - // We're changing our role. - if (this.hostServer) { - await api.unshareService(this.name); - this.hostServer = null; - } - if (this.guestServer) { - this.guestServer = null; - } - } - - // Startup our proxy or server - this.currentRole = api.session.role; - if (api.session.role === vsls.Role.Host) { - this.hostServer = await api.shareService(this.name); - - // When we start the host, listen for the broadcast message - if (this.hostServer !== null) { - this.hostServer.onNotify(LiveShare.LiveShareBroadcastRequest, a => this.onBroadcastRequest(api, a as IMessageArgs)); - } - } else if (api.session.role === vsls.Role.Guest) { - this.guestServer = await api.getSharedService(this.name); - - // When we switch to guest mode, we may have to reregister all of our commands. - this.registerGuestCommands(api); - } - } - } - - private async onChangePeers(api: vsls.LiveShare): Promise { - let newPeerCount = 0; - if (api.session) { - newPeerCount = api.peers.length; - } - if (newPeerCount !== this.currentPeerCount) { - this.peerCountChangedEmitter.fire(newPeerCount); - this.currentPeerCount = newPeerCount; - } - } - - private onBroadcastRequest = (api: vsls.LiveShare, a: IMessageArgs) => { - // This means we need to rebroadcast a request. We should also handle this request ourselves (as this means - // a guest is trying to tell everybody about a command) - if (a.args.length > 0) { - const jsonArray = JSON.parse(a.args) as JSONArray; - if (jsonArray !== null && jsonArray.length >= 2) { - const firstArg = jsonArray[0]!; // More stupid hygiene problems. - const command = firstArg !== null ? firstArg.toString() : ''; - - // Args need to be translated from guest to host - const rest = jsonArray.slice(1); - if (this.hostArgsTranslator) { - this.hostArgsTranslator(api, command, vsls.Role.Guest, rest); - } - - this.postCommand(command, ...rest).ignoreErrors(); - } - } - } - - private registerGuestCommands(api: vsls.LiveShare) { - if (api && api.session && api.session.role === vsls.Role.Guest && this.guestServer !== null) { - const keys = Object.keys(this.commandMap); - keys.forEach(k => { - if (this.guestServer !== null) { // Hygiene is too dumb to recognize the if above - this.guestServer.onNotify(this.escapeCommandName(k), a => this.onGuestNotify(k, a as IMessageArgs)); - } - }); - } - } - -} diff --git a/src/client/datascience/messages.ts b/src/client/datascience/messages.ts deleted file mode 100644 index 4003b0bc552d..000000000000 --- a/src/client/datascience/messages.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export namespace CssMessages { - export const GetCssRequest = 'get_css_request'; - export const GetCssResponse = 'get_css_response'; - export const GetMonacoThemeRequest = 'get_monaco_theme_request'; - export const GetMonacoThemeResponse = 'get_monaco_theme_response'; -} - -export namespace SharedMessages { - export const UpdateSettings = 'update_settings'; - export const Started = 'started'; -} - -export interface IGetCssRequest { - isDark: boolean; -} - -export interface IGetMonacoThemeRequest { - isDark: boolean; -} - -export interface IGetCssResponse { - css: string; - theme: string; - knownDark?: boolean; -} diff --git a/src/client/datascience/monacoMessages.ts b/src/client/datascience/monacoMessages.ts deleted file mode 100644 index fcd8fcd4620f..000000000000 --- a/src/client/datascience/monacoMessages.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; - -export interface IGetMonacoThemeResponse { - theme: monacoEditor.editor.IStandaloneThemeData; -} diff --git a/src/client/datascience/plotting/plotViewer.ts b/src/client/datascience/plotting/plotViewer.ts deleted file mode 100644 index bc040601a0cf..000000000000 --- a/src/client/datascience/plotting/plotViewer.ts +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Event, EventEmitter, ViewColumn } from 'vscode'; - -import { traceInfo } from '../../../client/common/logger'; -import { createDeferred } from '../../../client/common/utils/async'; -import { IApplicationShell, IWebPanelProvider, IWorkspaceService } from '../../common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { traceError } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import { IConfigurationService, IDisposable } from '../../common/types'; -import * as localize from '../../common/utils/localize'; -import { ICodeCssGenerator, IPlotViewer, IThemeFinder } from '../types'; -import { WebViewHost } from '../webViewHost'; -import { PlotViewerMessageListener } from './plotViewerMessageListener'; -import { IExportPlotRequest, IPlotViewerMapping, PlotViewerMessages } from './types'; - -@injectable() -export class PlotViewer extends WebViewHost implements IPlotViewer, IDisposable { - private disposed: boolean = false; - private closedEvent: EventEmitter = new EventEmitter(); - private removedEvent: EventEmitter = new EventEmitter(); - - constructor( - @inject(IWebPanelProvider) provider: IWebPanelProvider, - @inject(IConfigurationService) configuration: IConfigurationService, - @inject(ICodeCssGenerator) cssGenerator: ICodeCssGenerator, - @inject(IThemeFinder) themeFinder: IThemeFinder, - @inject(IWorkspaceService) workspaceService: IWorkspaceService, - @inject(IApplicationShell) private applicationShell: IApplicationShell, - @inject(IFileSystem) private fileSystem: IFileSystem - ) { - super( - configuration, - provider, - cssGenerator, - themeFinder, - workspaceService, - (c, v, d) => new PlotViewerMessageListener(c, v, d), - path.join(EXTENSION_ROOT_DIR, 'out', 'datascience-ui', 'plot', 'index_bundle.js'), - localize.DataScience.plotViewerTitle(), - ViewColumn.One); - } - - public get closed(): Event { - return this.closedEvent.event; - } - - public get removed(): Event { - return this.removedEvent.event; - } - - public async show(): Promise { - if (!this.disposed) { - // Then show our web panel. - return super.show(true); - } - } - - public addPlot = async (imageHtml: string) : Promise => { - if (!this.disposed) { - // Make sure we're shown - await super.show(false); - - // Send a message with our data - this.postMessage(PlotViewerMessages.SendPlot, imageHtml).ignoreErrors(); - } - } - - public dispose() { - if (!this.disposed) { - this.disposed = true; - super.dispose(); - if (this.closedEvent) { - this.closedEvent.fire(this); - } - } - } - - //tslint:disable-next-line:no-any - protected onMessage(message: string, payload: any) { - switch (message) { - case PlotViewerMessages.CopyPlot: - this.copyPlot(payload.toString()).ignoreErrors(); - break; - - case PlotViewerMessages.ExportPlot: - this.exportPlot(payload).ignoreErrors(); - break; - - case PlotViewerMessages.RemovePlot: - this.removePlot(payload); - break; - - default: - break; - } - - super.onMessage(message, payload); - } - - private removePlot(payload: number) { - this.removedEvent.fire(payload); - } - - private copyPlot(_svg: string) : Promise { - // This should be handled actually in the web view. Leaving - // this here for now in case need node to handle it. - return Promise.resolve(); - } - - private async exportPlot(payload: IExportPlotRequest) : Promise { - traceInfo('exporting plot...'); - const filtersObject: Record = {}; - filtersObject[localize.DataScience.pdfFilter()] = ['pdf']; - filtersObject[localize.DataScience.pngFilter()] = ['png']; - filtersObject[localize.DataScience.svgFilter()] = ['svg']; - - // Ask the user what file to save to - const file = await this.applicationShell.showSaveDialog({ - saveLabel: localize.DataScience.exportPlotTitle(), - filters: filtersObject - }); - try { - if (file) { - const ext = path.extname(file.fsPath); - switch (ext.toLowerCase()) { - case '.pdf': - traceInfo('Attempting pdf write...'); - // Import here since pdfkit is so huge. - // tslint:disable-next-line: no-require-imports - const SVGtoPDF = require('svg-to-pdfkit'); - const deferred = createDeferred(); - // tslint:disable-next-line: no-require-imports - const pdfkit = require('pdfkit'); - const doc = new pdfkit(); - const ws = this.fileSystem.createWriteStream(file.fsPath); - traceInfo(`Writing pdf to ${file.fsPath}`); - ws.on('finish', () => deferred.resolve); - SVGtoPDF(doc, payload.svg, 0, 0); - doc.pipe(ws); - doc.end(); - traceInfo(`Finishing pdf to ${file.fsPath}`); - await deferred.promise; - traceInfo(`Completed pdf to ${file.fsPath}`); - break; - - case '.png': - const buffer = new Buffer(payload.png.replace('data:image/png;base64', ''), 'base64'); - await this.fileSystem.writeFile(file.fsPath, buffer); - break; - - default: - case '.svg': - // This is the easy one: - await this.fileSystem.writeFile(file.fsPath, payload.svg); - break; - } - - } - - } catch (e) { - traceError(e); - this.applicationShell.showErrorMessage(localize.DataScience.exportImageFailed().format(e)); - } - } - -} diff --git a/src/client/datascience/plotting/plotViewerMessageListener.ts b/src/client/datascience/plotting/plotViewerMessageListener.ts deleted file mode 100644 index 70229183d922..000000000000 --- a/src/client/datascience/plotting/plotViewerMessageListener.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { IWebPanel, IWebPanelMessageListener } from '../../common/application/types'; - -// tslint:disable:no-any - -// This class listens to messages that come from the local Plot Viewer window -export class PlotViewerMessageListener implements IWebPanelMessageListener { - private disposedCallback : () => void; - private callback : (message: string, payload: any) => void; - private viewChanged: (panel: IWebPanel) => void; - - constructor(callback: (message: string, payload: any) => void, viewChanged: (panel: IWebPanel) => void, disposed: () => void) { - - // Save our dispose callback so we remove our history window - this.disposedCallback = disposed; - - // Save our local callback so we can handle the non broadcast case(s) - this.callback = callback; - - // Save view changed so we can forward view change events. - this.viewChanged = viewChanged; - } - - public async dispose() { - this.disposedCallback(); - } - - public onMessage(message: string, payload: any) { - // Send to just our local callback. - this.callback(message, payload); - } - - public onChangeViewState(panel: IWebPanel) { - // Forward this onto our callback - if (this.viewChanged) { - this.viewChanged(panel); - } - } -} diff --git a/src/client/datascience/plotting/plotViewerProvider.ts b/src/client/datascience/plotting/plotViewerProvider.ts deleted file mode 100644 index e11b1dfa9f14..000000000000 --- a/src/client/datascience/plotting/plotViewerProvider.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../../common/extensions'; - -import { inject, injectable } from 'inversify'; - -import { IAsyncDisposable, IAsyncDisposableRegistry, IDisposable } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { Telemetry } from '../constants'; -import { IPlotViewer, IPlotViewerProvider } from '../types'; - -@injectable() -export class PlotViewerProvider implements IPlotViewerProvider, IAsyncDisposable { - - private currentViewer: IPlotViewer | undefined; - private currentViewerClosed: IDisposable | undefined; - private imageList: string[] = []; - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IAsyncDisposableRegistry) asyncRegistry : IAsyncDisposableRegistry - ) { - asyncRegistry.push(this); - } - - public async dispose() { - if (this.currentViewer) { - this.currentViewer.dispose(); - } - } - - public async showPlot(imageHtml: string) : Promise { - this.imageList.push(imageHtml); - // If the viewer closed, send it all of the old images - const imagesToSend = this.currentViewer ? [imageHtml] : this.imageList; - const viewer = await this.getOrCreate(); - await Promise.all(imagesToSend.map(viewer.addPlot)); - } - - private async getOrCreate() : Promise{ - // Get or create a new plot viwer - if (!this.currentViewer) { - this.currentViewer = this.serviceContainer.get(IPlotViewer); - this.currentViewerClosed = this.currentViewer.closed(this.closedViewer); - this.currentViewer.removed(this.removedPlot); - sendTelemetryEvent(Telemetry.OpenPlotViewer); - await this.currentViewer.show(); - } - - return this.currentViewer; - } - - private closedViewer = () => { - if (this.currentViewer) { - this.currentViewer = undefined; - } - if (this.currentViewerClosed) { - this.currentViewerClosed.dispose(); - this.currentViewerClosed = undefined; - } - } - - private removedPlot = (index: number) => { - this.imageList.splice(index, 1); - } -} diff --git a/src/client/datascience/plotting/types.ts b/src/client/datascience/plotting/types.ts deleted file mode 100644 index e7cc2589e262..000000000000 --- a/src/client/datascience/plotting/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { CssMessages, IGetCssRequest, IGetCssResponse, SharedMessages } from '../messages'; - -export namespace PlotViewerMessages { - export const Started = SharedMessages.Started; - export const UpdateSettings = SharedMessages.UpdateSettings; - export const SendPlot = 'send_plot'; - export const CopyPlot = 'copy_plot'; - export const ExportPlot = 'export_plot'; - export const RemovePlot = 'remove_plot'; -} - -export interface IExportPlotRequest { - svg: string; - png: string; -} - -// Map all messages to specific payloads -export class IPlotViewerMapping { - public [PlotViewerMessages.Started]: never | undefined; - public [PlotViewerMessages.UpdateSettings]: string; - public [PlotViewerMessages.SendPlot]: string; - public [PlotViewerMessages.CopyPlot]: string; - public [PlotViewerMessages.ExportPlot]: IExportPlotRequest; - public [PlotViewerMessages.RemovePlot]: number; - public [CssMessages.GetCssRequest] : IGetCssRequest; - public [CssMessages.GetCssResponse] : IGetCssResponse; -} diff --git a/src/client/datascience/serviceRegistry.ts b/src/client/datascience/serviceRegistry.ts deleted file mode 100644 index c181344ad02c..000000000000 --- a/src/client/datascience/serviceRegistry.ts +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { IExtensionActivationService } from '../activation/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { ClassType, IServiceManager } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { CodeCssGenerator } from './codeCssGenerator'; -import { Telemetry } from './constants'; -import { DataViewer } from './data-viewing/dataViewer'; -import { DataViewerProvider } from './data-viewing/dataViewerProvider'; -import { DataScience } from './datascience'; -import { CellHashProvider } from './editor-integration/cellhashprovider'; -import { CodeLensFactory } from './editor-integration/codeLensFactory'; -import { DataScienceCodeLensProvider } from './editor-integration/codelensprovider'; -import { CodeWatcher } from './editor-integration/codewatcher'; -import { Decorator } from './editor-integration/decorator'; -import { DebugListener } from './interactive-window/debugListener'; -import { DotNetIntellisenseProvider } from './interactive-window/intellisense/dotNetIntellisenseProvider'; -import { JediIntellisenseProvider } from './interactive-window/intellisense/jediIntellisenseProvider'; -import { InteractiveWindow } from './interactive-window/interactiveWindow'; -import { InteractiveWindowCommandListener } from './interactive-window/interactiveWindowCommandListener'; -import { InteractiveWindowProvider } from './interactive-window/interactiveWindowProvider'; -import { LinkProvider } from './interactive-window/linkProvider'; -import { ShowPlotListener } from './interactive-window/showPlotListener'; -import { JupyterCommandFactory } from './jupyter/jupyterCommand'; -import { JupyterDebugger } from './jupyter/jupyterDebugger'; -import { JupyterExecutionFactory } from './jupyter/jupyterExecutionFactory'; -import { JupyterExporter } from './jupyter/jupyterExporter'; -import { JupyterImporter } from './jupyter/jupyterImporter'; -import { JupyterPasswordConnect } from './jupyter/jupyterPasswordConnect'; -import { JupyterServerFactory } from './jupyter/jupyterServerFactory'; -import { JupyterSessionManager } from './jupyter/jupyterSessionManager'; -import { JupyterVariables } from './jupyter/jupyterVariables'; -import { PlotViewer } from './plotting/plotViewer'; -import { PlotViewerProvider } from './plotting/plotViewerProvider'; -import { StatusProvider } from './statusProvider'; -import { ThemeFinder } from './themeFinder'; -import { - ICellHashProvider, - ICodeCssGenerator, - ICodeLensFactory, - ICodeWatcher, - IDataScience, - IDataScienceCodeLensProvider, - IDataScienceCommandListener, - IDataViewer, - IDataViewerProvider, - IInteractiveWindow, - IInteractiveWindowListener, - IInteractiveWindowProvider, - IJupyterCommandFactory, - IJupyterDebugger, - IJupyterExecution, - IJupyterPasswordConnect, - IJupyterSessionManager, - IJupyterVariables, - INotebookExporter, - INotebookImporter, - INotebookServer, - IPlotViewer, - IPlotViewerProvider, - IStatusProvider, - IThemeFinder -} from './types'; - -// tslint:disable:no-any -function wrapType(ctor: ClassType) : ClassType { - return class extends ctor { - constructor(...args: any[]) { - const stopWatch = new StopWatch(); - super(...args); - try { - // ctor name is minified. compute from the class definition - const className = ctor.toString().match(/\w+/g)![1]; - sendTelemetryEvent(Telemetry.ClassConstructionTime, stopWatch.elapsedTime, { class: className }); - } catch { - noop(); - } - } - }; -} - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IDataScienceCodeLensProvider, wrapType(DataScienceCodeLensProvider)); - serviceManager.addSingleton(IDataScience, wrapType(DataScience)); - serviceManager.addSingleton(IJupyterExecution, wrapType(JupyterExecutionFactory)); - serviceManager.add(IDataScienceCommandListener, wrapType(InteractiveWindowCommandListener)); - serviceManager.addSingleton(IInteractiveWindowProvider, wrapType(InteractiveWindowProvider)); - serviceManager.add(IInteractiveWindow, wrapType(InteractiveWindow)); - serviceManager.add(INotebookExporter, wrapType(JupyterExporter)); - serviceManager.add(INotebookImporter, wrapType(JupyterImporter)); - serviceManager.add(INotebookServer, wrapType(JupyterServerFactory)); - serviceManager.addSingleton(ICodeCssGenerator, wrapType(CodeCssGenerator)); - serviceManager.addSingleton(IJupyterPasswordConnect, wrapType(JupyterPasswordConnect)); - serviceManager.addSingleton(IStatusProvider, wrapType(StatusProvider)); - serviceManager.addSingleton(IJupyterSessionManager, wrapType(JupyterSessionManager)); - serviceManager.addSingleton(IJupyterVariables, wrapType(JupyterVariables)); - serviceManager.add(ICodeWatcher, wrapType(CodeWatcher)); - serviceManager.add(IJupyterCommandFactory, wrapType(JupyterCommandFactory)); - serviceManager.addSingleton(IThemeFinder, wrapType(ThemeFinder)); - serviceManager.addSingleton(IDataViewerProvider, wrapType(DataViewerProvider)); - serviceManager.add(IDataViewer, wrapType(DataViewer)); - serviceManager.addSingleton(IExtensionActivationService, wrapType(Decorator)); - serviceManager.add(IInteractiveWindowListener, wrapType(DotNetIntellisenseProvider)); - serviceManager.add(IInteractiveWindowListener, wrapType(JediIntellisenseProvider)); - serviceManager.add(IInteractiveWindowListener, wrapType(LinkProvider)); - serviceManager.add(IInteractiveWindowListener, wrapType(ShowPlotListener)); - serviceManager.add(IInteractiveWindowListener, wrapType(DebugListener)); - serviceManager.addSingleton(IPlotViewerProvider, wrapType(PlotViewerProvider)); - serviceManager.add(IPlotViewer, wrapType(PlotViewer)); - serviceManager.addSingleton(IJupyterDebugger, wrapType(JupyterDebugger)); - serviceManager.addSingleton(ICodeLensFactory, wrapType(CodeLensFactory)); - serviceManager.addSingleton(ICellHashProvider, wrapType(CellHashProvider)); - serviceManager.addBinding(ICellHashProvider, IInteractiveWindowListener); -} diff --git a/src/client/datascience/shiftEnterBanner.ts b/src/client/datascience/shiftEnterBanner.ts deleted file mode 100644 index 4c90c9f6e306..000000000000 --- a/src/client/datascience/shiftEnterBanner.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ConfigurationTarget } from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import '../common/extensions'; -import { IConfigurationService, IPersistentStateFactory, - IPythonExtensionBanner } from '../common/types'; -import * as localize from '../common/utils/localize'; -import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; -import { Telemetry } from './constants'; -import { IJupyterExecution } from './types'; - -export enum InteractiveShiftEnterStateKeys { - ShowBanner = 'InteractiveShiftEnterBanner' -} - -enum InteractiveShiftEnterLabelIndex { - Yes, - No -} - -// Create a banner to ask users if they want to send shift-enter to the interactive window or not -@injectable() -export class InteractiveShiftEnterBanner implements IPythonExtensionBanner { - private initialized?: boolean; - private disabledInCurrentSession: boolean = false; - private bannerMessage: string = localize.InteractiveShiftEnterBanner.bannerMessage(); - private bannerLabels: string[] = [localize.InteractiveShiftEnterBanner.bannerLabelYes(), localize.InteractiveShiftEnterBanner.bannerLabelNo()]; - - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell, - @inject(IPersistentStateFactory) private persistentState: IPersistentStateFactory, - @inject(IJupyterExecution) private jupyterExecution: IJupyterExecution, - @inject(IConfigurationService) private configuration: IConfigurationService) - { - this.initialize(); - } - - public initialize() { - if (this.initialized) { - return; - } - this.initialized = true; - - if (!this.enabled) { - return; - } - } - - public get enabled(): boolean { - return this.persistentState.createGlobalPersistentState(InteractiveShiftEnterStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise { - if (!this.enabled) { - return; - } - - const show = await this.shouldShowBanner(); - if (!show) { - return; - } - - // This check is independent from shouldShowBanner, that just checks the persistent state. - // The Jupyter check should only happen once and should disable the banner if it fails (don't reprompt and don't recheck) - const jupyterFound = await this.jupyterExecution.isNotebookSupported(); - if (!jupyterFound) { - await this.disableBanner(); - return; - } - - sendTelemetryEvent(Telemetry.ShiftEnterBannerShown); - const response = await this.appShell.showInformationMessage(this.bannerMessage, ...this.bannerLabels); - switch (response) { - case this.bannerLabels[InteractiveShiftEnterLabelIndex.Yes]: { - await this.enableInteractiveShiftEnter(); - break; - } - case this.bannerLabels[InteractiveShiftEnterLabelIndex.No]: { - await this.disableInteractiveShiftEnter(); - break; - } - default: { - // Disable for the current session. - this.disabledInCurrentSession = true; - } - } - } - - public async shouldShowBanner(): Promise { - const settings = this.configuration.getSettings(); - return Promise.resolve(this.enabled && !this.disabledInCurrentSession && !settings.datascience.sendSelectionToInteractiveWindow && settings.datascience.enabled); - } - - @captureTelemetry(Telemetry.DisableInteractiveShiftEnter) - public async disableInteractiveShiftEnter(): Promise { - await this.configuration.updateSetting('dataScience.sendSelectionToInteractiveWindow', false, undefined, ConfigurationTarget.Global); - await this.disableBanner(); - } - - @captureTelemetry(Telemetry.EnableInteractiveShiftEnter) - public async enableInteractiveShiftEnter(): Promise { - await this.configuration.updateSetting('dataScience.sendSelectionToInteractiveWindow', true, undefined, ConfigurationTarget.Global); - await this.disableBanner(); - } - - private async disableBanner(): Promise { - await this.persistentState.createGlobalPersistentState(InteractiveShiftEnterStateKeys.ShowBanner, false).updateValue(false); - } -} diff --git a/src/client/datascience/statusProvider.ts b/src/client/datascience/statusProvider.ts deleted file mode 100644 index c0f65a319a71..000000000000 --- a/src/client/datascience/statusProvider.ts +++ /dev/null @@ -1,117 +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 { IInteractiveWindowProvider, IStatusProvider } from './types'; - -class StatusItem implements Disposable { - private deferred: Deferred; - private disposed: boolean = false; - private timeout: NodeJS.Timer | number | undefined; - private disposeCallback: () => void; - - constructor(_title: string, disposeCallback: () => void, timeout?: number) { - this.deferred = createDeferred(); - this.disposeCallback = disposeCallback; - - // A timeout is possible too. Auto dispose if that's the case - if (timeout) { - this.timeout = setTimeout(this.dispose, timeout); - } - } - - public dispose = () => { - if (!this.disposed) { - this.disposed = true; - if (this.timeout) { - // tslint:disable-next-line: no-any - clearTimeout(this.timeout as any); - this.timeout = undefined; - } - this.disposeCallback(); - if (!this.deferred.completed) { - this.deferred.resolve(); - } - } - } - - public promise = (): Promise => { - return this.deferred.promise; - } - - public reject = () => { - this.deferred.reject(); - this.dispose(); - } -} - -@injectable() -export class StatusProvider implements IStatusProvider { - private statusCount: number = 0; - - constructor(@inject(IApplicationShell) private applicationShell: IApplicationShell, @inject(IInteractiveWindowProvider) private interactiveWindowProvider: IInteractiveWindowProvider) {} - - public set(message: string, timeout?: number, cancel?: () => void, skipHistory?: boolean): Disposable { - // Start our progress - this.incrementCount(skipHistory); - - // Create a StatusItem that will return our promise - const statusItem = new StatusItem(message, () => this.decrementCount(skipHistory), timeout); - - const progressOptions: ProgressOptions = { - location: cancel ? ProgressLocation.Notification : ProgressLocation.Window, - title: message, - cancellable: cancel !== undefined - }; - - // Set our application shell status with a busy icon - this.applicationShell.withProgress(progressOptions, (_p, c) => { - if (c && cancel) { - c.onCancellationRequested(() => { - cancel(); - statusItem.reject(); - }); - } - return statusItem.promise(); - }); - - return statusItem; - } - - public async waitWithStatus(promise: () => Promise, message: string, timeout?: number, cancel?: () => void, skipHistory?: boolean): Promise { - // Create a status item and wait for our promise to either finish or reject - const status = this.set(message, timeout, cancel, skipHistory); - let result: T; - try { - result = await promise(); - } finally { - status.dispose(); - } - return result; - } - - private incrementCount = (skipHistory?: boolean) => { - if (this.statusCount === 0) { - const history = this.interactiveWindowProvider.getActive(); - if (history && !skipHistory) { - history.startProgress(); - } - } - this.statusCount += 1; - } - - private decrementCount = (skipHistory?: boolean) => { - const updatedCount = this.statusCount - 1; - if (updatedCount === 0) { - const history = this.interactiveWindowProvider.getActive(); - if (history && !skipHistory) { - history.stopProgress(); - } - } - this.statusCount = Math.max(updatedCount, 0); - } -} diff --git a/src/client/datascience/themeFinder.ts b/src/client/datascience/themeFinder.ts deleted file mode 100644 index 470f8d604400..000000000000 --- a/src/client/datascience/themeFinder.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as fs from 'fs-extra'; -import * as glob from 'glob'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; - -import { EXTENSION_ROOT_DIR, PYTHON_LANGUAGE } from '../common/constants'; -import { ICurrentProcess, IExtensions, ILogger } from '../common/types'; -import { IThemeFinder } from './types'; - -// tslint:disable:no-any - -interface IThemeData { - rootFile: string; - isDark : boolean; -} - -@injectable() -export class ThemeFinder implements IThemeFinder { - private themeCache : { [key: string] : IThemeData | undefined } = {}; - private languageCache: { [key: string] : string | undefined } = {}; - - constructor( - @inject(IExtensions) private extensions: IExtensions, - @inject(ICurrentProcess) private currentProcess: ICurrentProcess, - @inject(ILogger) private logger: ILogger) { - } - - public async findThemeRootJson(themeName: string) : Promise { - // find our data - const themeData = await this.findThemeData(themeName); - - // Use that data if it worked - if (themeData) { - return themeData.rootFile; - } - } - - public async findTmLanguage(language: string) : Promise { - // See if already found it or not - if (!this.themeCache.hasOwnProperty(language)) { - try { - this.languageCache[language] = await this.findMatchingLanguage(language); - } catch (exc) { - this.logger.logError(exc); - } - } - return this.languageCache[language]; - } - - public async isThemeDark(themeName: string) : Promise { - // find our data - const themeData = await this.findThemeData(themeName); - - // Use that data if it worked - if (themeData) { - return themeData.isDark; - } - } - - private async findThemeData(themeName: string) : Promise { - // See if already found it or not - if (!this.themeCache.hasOwnProperty(themeName)) { - try { - this.themeCache[themeName] = await this.findMatchingTheme(themeName); - } catch (exc) { - this.logger.logError(exc); - } - } - return this.themeCache[themeName]; - } - - private async findMatchingLanguage(language: string) : Promise { - const currentExe = this.currentProcess.execPath; - let currentPath = path.dirname(currentExe); - - // Should be somewhere under currentPath/resources/app/extensions inside of a json file - let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); - if (!(await 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 files in this folder - let results = await this.findMatchingLanguages(language, extensionsPath); - - // If that didn't work, see if it's our MagicPython predefined tmLanguage - if (!results && language === PYTHON_LANGUAGE) { - results = await fs.readFile(path.join(EXTENSION_ROOT_DIR, 'resources', 'MagicPython.tmLanguage.json'), 'utf-8'); - } - - return results; - } - - private async findMatchingLanguages(language: string, rootPath: string) : Promise { - // Environment variable to mimic missing json problem - if (process.env.VSC_PYTHON_MIMIC_REMOTE) { - return undefined; - } - - // Search through all package.json files in the directory and below, looking - // for the themeName in them. - const foundPackages = await new Promise((resolve, reject) => { - glob('**/package.json', { cwd: rootPath }, (err, matches) => { - if (err) { - reject(err); - } - resolve(matches); - }); - }); - if (foundPackages.length > 0) { - // For each one, open it up and look for the theme name. - for (const f of foundPackages) { - const fpath = path.join(rootPath, f); - const data = await this.findMatchingLanguageFromJson(fpath, language); - if (data) { - return data; - } - } - } - } - - private async findMatchingTheme(themeName: string) : Promise { - // Environment variable to mimic missing json problem - if (process.env.VSC_PYTHON_MIMIC_REMOTE) { - return undefined; - } - - // Look through all extensions to find the theme. This will search - // the default extensions folder and our installed extensions. - const extensions = this.extensions.all; - for (const e of extensions) { - const result = await this.findMatchingThemeFromJson(path.join(e.extensionPath, 'package.json'), themeName); - if (result) { - return result; - } - } - - // If didn't find in the extensions folder, then try searching manually. This shouldn't happen, but - // this is our backup plan in case vscode changes stuff. - const currentExe = this.currentProcess.execPath; - let currentPath = path.dirname(currentExe); - - // Should be somewhere under currentPath/resources/app/extensions inside of a json file - let extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); - if (!(await fs.pathExists(extensionsPath))) { - // Might be on mac or linux. try a different path - currentPath = path.resolve(currentPath, '../../../..'); - extensionsPath = path.join(currentPath, 'resources', 'app', 'extensions'); - } - const other = await this.findMatchingThemes(extensionsPath, themeName); - if (other) { - return other; - } - } - - private async findMatchingThemes(rootPath: string, themeName: string) : Promise { - // Search through all package.json files in the directory and below, looking - // for the themeName in them. - const foundPackages = await new Promise((resolve, reject) => { - glob('**/package.json', { cwd: rootPath }, (err, matches) => { - if (err) { - reject(err); - } - resolve(matches); - }); - }); - if (foundPackages.length > 0) { - // For each one, open it up and look for the theme name. - for (const f of foundPackages) { - const fpath = path.join(rootPath, f); - const data = await this.findMatchingThemeFromJson(fpath, themeName); - if (data) { - return data; - } - } - } - } - - private async findMatchingLanguageFromJson(packageJson: string, language: string) : Promise { - // Read the contents of the json file - const json = await fs.readJSON(packageJson, { encoding: 'utf-8'}); - - // Should have a name entry and a contributes entry - if (json.hasOwnProperty('name') && json.hasOwnProperty('contributes')) { - // See if contributes has a grammars - const contributes = json.contributes; - if (contributes.hasOwnProperty('grammars')) { - const grammars = contributes.grammars as any[]; - // Go through each theme, seeing if the label matches our theme name - for (const t of grammars) { - if (t.hasOwnProperty('language') && t.language === language) { - // Path is relative to the package.json file. - const rootFile = t.hasOwnProperty('path') ? path.join(path.dirname(packageJson), t.path.toString()) : ''; - return fs.readFile(rootFile, 'utf-8'); - } - } - } - } - } - - private async findMatchingThemeFromJson(packageJson: string, themeName: string) : Promise { - // Read the contents of the json file - const json = await fs.readJSON(packageJson, { encoding: 'utf-8'}); - - // Should have a name entry and a contributes entry - if (json.hasOwnProperty('name') && json.hasOwnProperty('contributes')) { - // See if contributes has a theme - const contributes = json.contributes; - if (contributes.hasOwnProperty('themes')) { - const themes = contributes.themes as any[]; - // Go through each theme, seeing if the label matches our theme name - for (const t of themes) { - if ((t.hasOwnProperty('label') && t.label === themeName) || - (t.hasOwnProperty('id') && t.id === themeName)) { - const isDark = t.hasOwnProperty('uiTheme') && t.uiTheme === 'vs-dark'; - // Path is relative to the package.json file. - const rootFile = t.hasOwnProperty('path') ? path.join(path.dirname(packageJson), t.path.toString()) : ''; - - return {isDark, rootFile}; - } - } - } - } - } -} diff --git a/src/client/datascience/types.ts b/src/client/datascience/types.ts deleted file mode 100644 index f9f58d8c5204..000000000000 --- a/src/client/datascience/types.ts +++ /dev/null @@ -1,430 +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 { ExecutionResult, ObservableExecutionResult, SpawnOptions } from '../common/process/types'; -import { IAsyncDisposable, IDataScienceSettings, IDisposable } from '../common/types'; -import { StopWatch } from '../common/utils/stopWatch'; -import { PythonInterpreter } from '../interpreter/contracts'; - -// Main interface -export const IDataScience = Symbol('IDataScience'); -export interface IDataScience extends Disposable { - activationStartTime: number; - activate(): Promise; -} - -export const IDataScienceCommandListener = Symbol('IDataScienceCommandListener'); -export interface IDataScienceCommandListener { - register(commandManager: ICommandManager): void; -} - -// Connection information for talking to a jupyter notebook process -export interface IConnection extends Disposable { - baseUrl: string; - token: string; - localLaunch: boolean; - localProcExitCode: number | undefined; - disconnected: Event; - allowUnauthorized?: boolean; -} - -export enum InterruptResult { - Success = 0, - TimedOut = 1, - Restarted = 2 -} - -// Information needed to attach our debugger instance -export interface IDebuggerConnectInfo { - hostName: string; - port: number; -} - -// Information used to launch a notebook server -export interface INotebookServerLaunchInfo { - connectionInfo: IConnection; - currentInterpreter: PythonInterpreter | undefined; - uri: string | undefined; // Different from the connectionInfo as this is the setting used, not the result - kernelSpec: IJupyterKernelSpec | undefined; - workingDir: string | undefined; - purpose: string | undefined; // Purpose this server is for - enableDebugging: boolean | undefined; // If we should enable debugging for this server -} - -export interface INotebookCompletion { - matches: ReadonlyArray; - cursor: { - start: number; - end: number; - }; - metadata: {}; -} - -// Talks to a jupyter ipython kernel to retrieve data for cells -export const INotebookServer = Symbol('INotebookServer'); -export interface INotebookServer extends IAsyncDisposable { - connect(launchInfo: INotebookServerLaunchInfo, cancelToken?: CancellationToken): Promise; - executeObservable(code: string, file: string, line: number, id: string, silent: boolean): Observable; - execute(code: string, file: string, line: number, id: string, cancelToken?: CancellationToken, silent?: boolean): Promise; - getCompletion(cellCode: string, offsetInCode: number, cancelToken?: CancellationToken): Promise; - restartKernel(timeoutInMs: number): Promise; - waitForIdle(timeoutInMs: number): Promise; - shutdown(): Promise; - interruptKernel(timeoutInMs: number): Promise; - setInitialDirectory(directory: string): Promise; - waitForConnect(): Promise; - getConnectionInfo(): IConnection | undefined; - getSysInfo(): Promise; - setMatplotLibStyle(useDark: boolean): Promise; -} - -export interface INotebookServerOptions { - enableDebugging?: boolean; - uri?: string; - usingDarkTheme?: boolean; - useDefaultConfig?: boolean; - workingDir?: string; - purpose: string; -} - -export const INotebookExecutionLogger = Symbol('INotebookExecutionLogger'); -export interface INotebookExecutionLogger { - preExecute(cell: ICell, silent: boolean): void; - postExecute(cellOrError: ICell | Error, silent: boolean): void; -} - -export const IJupyterExecution = Symbol('IJupyterExecution'); -export interface IJupyterExecution extends IAsyncDisposable { - sessionChanged: Event; - isNotebookSupported(cancelToken?: CancellationToken): Promise; - isImportSupported(cancelToken?: CancellationToken): Promise; - isKernelCreateSupported(cancelToken?: CancellationToken): Promise; - isKernelSpecSupported(cancelToken?: CancellationToken): Promise; - isSpawnSupported(cancelToken?: CancellationToken): Promise; - connectToNotebookServer(options?: INotebookServerOptions, cancelToken?: CancellationToken): Promise; - spawnNotebook(file: string): Promise; - importNotebook(file: string, template: string | undefined): Promise; - getUsableJupyterPython(cancelToken?: CancellationToken): Promise; - getServer(options?: INotebookServerOptions): Promise; -} - -export const IJupyterDebugger = Symbol('IJupyterDebugger'); -export interface IJupyterDebugger { - enableAttach(server: INotebookServer): Promise; - startDebugging(server: INotebookServer): Promise; - stopDebugging(server: INotebookServer): Promise; -} - -export interface IJupyterPasswordConnectInfo { - xsrfCookie: string; - sessionCookieName: string; - sessionCookieValue: string; -} - -export const IJupyterPasswordConnect = Symbol('IJupyterPasswordConnect'); -export interface IJupyterPasswordConnect { - getPasswordConnectionInfo(url: string, allowUnauthorized: boolean): Promise; -} - -export const IJupyterSession = Symbol('IJupyterSession'); -export interface IJupyterSession extends IAsyncDisposable { - onRestarted: Event; - restart(timeout: number): Promise; - interrupt(timeout: number): Promise; - waitForIdle(timeout: number): Promise; - requestExecute(content: KernelMessage.IExecuteRequest, disposeOnDone?: boolean, metadata?: JSONObject): Kernel.IFuture | undefined; - requestComplete(content: KernelMessage.ICompleteRequest): Promise; -} -export const IJupyterSessionManager = Symbol('IJupyterSessionManager'); -export interface IJupyterSessionManager { - startNew(connInfo: IConnection, kernelSpec: IJupyterKernelSpec | undefined, cancelToken?: CancellationToken): Promise; - getActiveKernelSpecs(connInfo: IConnection): Promise; -} - -export interface IJupyterKernelSpec extends IAsyncDisposable { - name: string | undefined; - language: string | undefined; - path: string | undefined; -} - -export const INotebookImporter = Symbol('INotebookImporter'); -export interface INotebookImporter extends Disposable { - importFromFile(file: string): Promise; -} - -export const INotebookExporter = Symbol('INotebookExporter'); -export interface INotebookExporter extends Disposable { - translateToNotebook(cells: ICell[], directoryChange?: string): Promise; -} - -export const IInteractiveWindowProvider = Symbol('IInteractiveWindowProvider'); -export interface IInteractiveWindowProvider { - onExecutedCode: Event; - getActive(): IInteractiveWindow | undefined; - getOrCreateActive(): Promise; - getNotebookOptions(): Promise; -} - -export const IInteractiveWindow = Symbol('IInteractiveWindow'); -export interface IInteractiveWindow extends Disposable { - closed: Event; - ready: Promise; - onExecutedCode: Event; - show(): Promise; - addCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise; - debugCode(code: string, file: string, line: number, editor?: TextEditor, runningStopWatch?: StopWatch): Promise; - startProgress(): void; - stopProgress(): void; - undoCells(): void; - redoCells(): void; - removeAllCells(): void; - interruptKernel(): Promise; - restartKernel(): Promise; - expandAllCells(): void; - collapseAllCells(): void; - exportCells(): void; - previewNotebook(notebookFile: string): Promise; -} - -export const IInteractiveWindowListener = Symbol('IInteractiveWindowListener'); - -/** - * Listens to history messages to provide extra functionality - */ -export interface IInteractiveWindowListener extends IDisposable { - /** - * Fires this event when posting a response message - */ - // tslint:disable-next-line: no-any - postMessage: Event<{ message: string; payload: any }>; - /** - * Handles messages that the interactive window receives - * @param message message type - * @param payload message payload - */ - // tslint:disable-next-line: no-any - onMessage(message: string, payload?: any): void; -} - -// Wraps the vscode API in order to send messages back and forth from a webview -export const IPostOffice = Symbol('IPostOffice'); -export interface IPostOffice { - // tslint:disable-next-line:no-any - post(message: string, params: any[] | undefined): void; - // tslint:disable-next-line:no-any - listen(message: string, listener: (args: any[] | undefined) => void): void; -} - -// Wraps the vscode CodeLensProvider base class -export const IDataScienceCodeLensProvider = Symbol('IDataScienceCodeLensProvider'); -export interface IDataScienceCodeLensProvider extends CodeLensProvider { - getCodeWatcher(document: TextDocument): ICodeWatcher | undefined; -} - -// Wraps the Code Watcher API -export const ICodeWatcher = Symbol('ICodeWatcher'); -export interface ICodeWatcher { - setDocument(document: TextDocument): void; - getFileName(): string; - getVersion(): number; - getCodeLenses(): CodeLens[]; - getCachedSettings(): IDataScienceSettings | undefined; - runAllCells(): Promise; - runCell(range: Range): Promise; - debugCell(range: Range): Promise; - runCurrentCell(): Promise; - runCurrentCellAndAdvance(): Promise; - runSelectionOrLine(activeEditor: TextEditor | undefined): Promise; - runToLine(targetLine: number): Promise; - runFromLine(targetLine: number): Promise; - runAllCellsAbove(stopLine: number, stopCharacter: number): Promise; - runCellAndAllBelow(startLine: number, startCharacter: number): Promise; - runFileInteractive(): Promise; - addEmptyCellToBottom(): Promise; - debugCurrentCell(): Promise; -} - -export const ICodeLensFactory = Symbol('ICodeLensFactory'); -export interface ICodeLensFactory { - createCodeLenses(document: TextDocument): CodeLens[]; -} - -export enum CellState { - editing = -1, - init = 0, - executing = 1, - finished = 2, - error = 3 -} - -// Basic structure for a cell from a notebook -export interface ICell { - id: string; // This value isn't unique. File and line are needed to. - file: string; - line: number; - state: CellState; - type: 'preview' | 'execute'; - data: nbformat.ICodeCell | nbformat.IRawCell | nbformat.IMarkdownCell | IMessageCell; -} - -export interface IInteractiveWindowInfo { - cellCount: number; - undoCount: number; - redoCount: number; -} - -export interface IMessageCell extends nbformat.IBaseCell { - cell_type: 'messages'; - messages: string[]; -} - -export const ICodeCssGenerator = Symbol('ICodeCssGenerator'); -export interface ICodeCssGenerator { - generateThemeCss(isDark: boolean, theme: string): Promise; - generateMonacoTheme(isDark: boolean, theme: string): Promise; -} - -export const IThemeFinder = Symbol('IThemeFinder'); -export interface IThemeFinder { - findThemeRootJson(themeName: string): Promise; - findTmLanguage(language: string): Promise; - isThemeDark(themeName: string): Promise; -} - -export const IStatusProvider = Symbol('IStatusProvider'); -export interface IStatusProvider { - // call this function to set the new status on the active - // interactive window. Dispose of the returned object when done. - set(message: string, timeout?: number): Disposable; - - // call this function to wait for a promise while displaying status - waitWithStatus(promise: () => Promise, message: string, timeout?: number, canceled?: () => void, skipHistory?: boolean): Promise; -} - -export interface IJupyterCommand { - interpreter(): Promise; - execObservable(args: string[], options: SpawnOptions): Promise>; - exec(args: string[], options: SpawnOptions): Promise>; -} - -export const IJupyterCommandFactory = Symbol('IJupyterCommandFactory'); -export interface IJupyterCommandFactory { - createInterpreterCommand(args: string[], interpreter: PythonInterpreter): IJupyterCommand; - createProcessCommand(exe: string, args: string[]): IJupyterCommand; -} - -// Config settings we pass to our react code -export interface IDataScienceExtraSettings extends IDataScienceSettings { - extraSettings: { - editorCursor: string; - editorCursorBlink: string; - theme: string; - }; - intellisenseOptions: { - quickSuggestions: { - other: boolean; - comments: boolean; - strings: boolean; - }; - acceptSuggestionOnEnter: boolean | 'on' | 'smart' | 'off'; - quickSuggestionsDelay: number; - suggestOnTriggerCharacters: boolean; - tabCompletion: boolean | 'on' | 'off' | 'onlySnippets'; - suggestLocalityBonus: boolean; - suggestSelection: 'first' | 'recentlyUsed' | 'recentlyUsedByPrefix'; - wordBasedSuggestions: boolean; - parameterHintsEnabled: boolean; - }; -} - -// Get variables from the currently running active Jupyter server -// Note: This definition is used implicitly by getJupyterVariableValue.py file -// Changes here may need to be reflected there as well -export interface IJupyterVariable { - name: string; - value: string | undefined; - executionCount?: number; - supportsDataExplorer: boolean; - type: string; - size: number; - shape: string; - count: number; - truncated: boolean; - columns?: { key: string; type: string }[]; - rowCount?: number; - indexColumn?: string; -} - -export const IJupyterVariables = Symbol('IJupyterVariables'); -export interface IJupyterVariables { - getVariables(): Promise; - getValue(targetVariable: IJupyterVariable): Promise; - getDataFrameInfo(targetVariable: IJupyterVariable): Promise; - getDataFrameRows(targetVariable: IJupyterVariable, start: number, end: number): Promise; -} - -// Wrapper to hold an execution count for our variable requests -export interface IJupyterVariablesResponse { - executionCount: number; - variables: IJupyterVariable[]; -} - -export const IDataViewerProvider = Symbol('IDataViewerProvider'); -export interface IDataViewerProvider { - create(variable: string): Promise; - getPandasVersion(): Promise<{ major: number; minor: number; build: number } | undefined>; -} -export const IDataViewer = Symbol('IDataViewer'); - -export interface IDataViewer extends IDisposable { - showVariable(variable: IJupyterVariable): Promise; -} - -export const IPlotViewerProvider = Symbol('IPlotViewerProvider'); -export interface IPlotViewerProvider { - showPlot(imageHtml: string): Promise; -} -export const IPlotViewer = Symbol('IPlotViewer'); - -export interface IPlotViewer extends IDisposable { - closed: Event; - removed: Event; - addPlot(imageHtml: string): Promise; - show(): Promise; -} - -export interface ISourceMapMapping { - line: number; - endLine: number; - runtimeSource: { path: string }; - runtimeLine: number; -} - -export interface ISourceMapRequest { - source: { path: string }; - pydevdSourceMaps: ISourceMapMapping[]; -} - -export interface ICellHash { - line: number; // 1 based - endLine: number; // 1 based and inclusive - hash: string; - executionCount: number; -} - -export interface IFileHashes { - file: string; - hashes: ICellHash[]; -} - -export const ICellHashProvider = Symbol('ICellHashProvider'); -export interface ICellHashProvider { - getHashes(): IFileHashes[]; -} diff --git a/src/client/datascience/webViewHost.ts b/src/client/datascience/webViewHost.ts deleted file mode 100644 index e6fec66d9a52..000000000000 --- a/src/client/datascience/webViewHost.ts +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../common/extensions'; - -import { injectable, unmanaged } from 'inversify'; -import { ConfigurationChangeEvent, ViewColumn, WorkspaceConfiguration } from 'vscode'; - -import { IWebPanel, IWebPanelMessageListener, IWebPanelProvider, IWorkspaceService } from '../common/application/types'; -import { traceInfo } from '../common/logger'; -import { IConfigurationService, IDisposable } from '../common/types'; -import { createDeferred, Deferred } from '../common/utils/async'; -import { StopWatch } from '../common/utils/stopWatch'; -import { captureTelemetry, sendTelemetryEvent } from '../telemetry'; -import { DefaultTheme, Telemetry } from './constants'; -import { CssMessages, IGetCssRequest, IGetMonacoThemeRequest, SharedMessages } from './messages'; -import { ICodeCssGenerator, IDataScienceExtraSettings, IThemeFinder } from './types'; - -@injectable() // For some reason this is necessary to get the class hierarchy to work. -export class WebViewHost implements IDisposable { - protected viewState : { visible: boolean; active: boolean } = { visible: false, active: false }; - private isDisposed: boolean = false; - private webPanel: IWebPanel | undefined; - private webPanelInit: Deferred; - private messageListener: IWebPanelMessageListener; - private themeChangeHandler: IDisposable | undefined; - private settingsChangeHandler: IDisposable | undefined; - private themeIsDarkPromise: Deferred; - private startupStopwatch = new StopWatch(); - - constructor( - @unmanaged() private configService: IConfigurationService, - @unmanaged() private provider: IWebPanelProvider, - @unmanaged() private cssGenerator: ICodeCssGenerator, - @unmanaged() protected themeFinder: IThemeFinder, - @unmanaged() private workspaceService: IWorkspaceService, - // tslint:disable-next-line:no-any - @unmanaged() messageListenerCtor: (callback: (message: string, payload: any) => void, viewChanged: (panel: IWebPanel) => void, disposed: () => void) => IWebPanelMessageListener, - @unmanaged() private mainScriptPath: string, - @unmanaged() private title: string, - @unmanaged() private viewColumn: ViewColumn - ) { - // Create our message listener for our web panel. - this.messageListener = messageListenerCtor(this.onMessage.bind(this), this.onViewStateChanged.bind(this), this.dispose.bind(this)); - - // Listen for settings changes from vscode. - this.themeChangeHandler = this.workspaceService.onDidChangeConfiguration(this.onPossibleSettingsChange, this); - - // Listen for settings changes - this.settingsChangeHandler = this.configService.getSettings().onDidChange(this.onDataScienceSettingsChanged.bind(this)); - - // Setup our init promise for the web panel. We use this to make sure we're in sync with our - // react control. - this.webPanelInit = createDeferred(); - - // Setup a promise that will wait until the webview passes back - // a message telling us what them is in use - this.themeIsDarkPromise = createDeferred(); - - // Load our actual web panel - this.loadWebPanel(); - } - - public async show(preserveFocus: boolean): Promise { - if (!this.isDisposed) { - // Then show our web panel. - if (this.webPanel) { - await this.webPanel.show(preserveFocus); - } - } - } - - public dispose() { - if (!this.isDisposed) { - this.isDisposed = true; - if (this.webPanel) { - this.webPanel.close(); - this.webPanel = undefined; - } - if (this.themeChangeHandler) { - this.themeChangeHandler.dispose(); - this.themeChangeHandler = undefined; - } - if (this.settingsChangeHandler) { - this.settingsChangeHandler.dispose(); - this.settingsChangeHandler = undefined; - } - } - } - - public setTitle(newTitle: string) { - if (!this.isDisposed && this.webPanel) { - this.webPanel.title = newTitle; - } - } - - //tslint:disable-next-line:no-any - protected onMessage(message: string, payload: any) { - switch (message) { - case SharedMessages.Started: - this.webPanelRendered(); - break; - - case CssMessages.GetCssRequest: - this.handleCssRequest(payload as IGetCssRequest).ignoreErrors(); - break; - - case CssMessages.GetMonacoThemeRequest: - this.handleMonacoThemeRequest(payload as IGetMonacoThemeRequest).ignoreErrors(); - break; - - default: - break; - } - } - - protected postMessage(type: T, payload?: M[T]) : Promise { - // Then send it the message - return this.postMessageInternal(type.toString(), payload); - } - - protected shareMessage(type: T, payload?: M[T]) { - // Send our remote message. - this.messageListener.onMessage(type.toString(), payload); - } - - protected activating() : Promise { - return Promise.resolve(); - } - - // tslint:disable-next-line:no-any - protected async postMessageInternal(type: string, payload?: any) : Promise { - if (this.webPanel) { - // Make sure the webpanel is up before we send it anything. - await this.webPanelInit.promise; - - // Then send it the message - this.webPanel.postMessage({ type: type.toString(), payload: payload }); - } - } - - protected generateDataScienceExtraSettings() : IDataScienceExtraSettings { - const editor = this.workspaceService.getConfiguration('editor'); - const workbench = this.workspaceService.getConfiguration('workbench'); - const theme = !workbench ? DefaultTheme : workbench.get('colorTheme', DefaultTheme); - return { - ...this.configService.getSettings().datascience, - extraSettings: { - editorCursor: this.getValue(editor, 'cursorStyle', 'line'), - editorCursorBlink: this.getValue(editor, 'cursorBlinking', 'blink'), - theme: theme - }, - intellisenseOptions: { - quickSuggestions: { - other: this.getValue(editor, 'quickSuggestions.other', true), - comments: this.getValue(editor, 'quickSuggestions.comments', false), - strings: this.getValue(editor, 'quickSuggestions.strings', false) - }, - acceptSuggestionOnEnter: this.getValue(editor, 'acceptSuggestionOnEnter', 'on'), - quickSuggestionsDelay: this.getValue(editor, 'quickSuggestionsDelay', 10), - suggestOnTriggerCharacters: this.getValue(editor, 'suggestOnTriggerCharacters', true), - tabCompletion: this.getValue(editor, 'tabCompletion', 'on'), - suggestLocalityBonus: this.getValue(editor, 'suggest.localityBonus', true), - suggestSelection: this.getValue(editor, 'suggestSelection', 'recentlyUsed'), - wordBasedSuggestions: this.getValue(editor, 'wordBasedSuggestions', true), - parameterHintsEnabled: this.getValue(editor, 'parameterHints.enabled', true) - } - }; - } - - protected isDark() : Promise { - return this.themeIsDarkPromise.promise; - } - - private getValue(workspaceConfig: WorkspaceConfiguration, section: string, defaultValue: T) : T { - if (workspaceConfig) { - return workspaceConfig.get(section, defaultValue); - } - return defaultValue; - } - - private onViewStateChanged = (webPanel: IWebPanel) => { - const oldActive = this.viewState.active; - this.viewState.active = webPanel.isActive(); - this.viewState.visible = webPanel.isVisible(); - - // See if suddenly becoming active or not - if (!oldActive && this.viewState.active) { - this.activating().ignoreErrors(); - } - } - - @captureTelemetry(Telemetry.WebviewStyleUpdate) - private async handleCssRequest(request: IGetCssRequest) : Promise { - if (!this.themeIsDarkPromise.resolved) { - this.themeIsDarkPromise.resolve(request.isDark); - } else { - this.themeIsDarkPromise = createDeferred(); - this.themeIsDarkPromise.resolve(request.isDark); - } - const settings = this.generateDataScienceExtraSettings(); - const isDark = await this.themeFinder.isThemeDark(settings.extraSettings.theme); - const css = await this.cssGenerator.generateThemeCss(request.isDark, settings.extraSettings.theme); - return this.postMessageInternal(CssMessages.GetCssResponse, { css, theme: settings.extraSettings.theme, knownDark: isDark }); - } - - @captureTelemetry(Telemetry.WebviewMonacoStyleUpdate) - private async handleMonacoThemeRequest(request: IGetMonacoThemeRequest) : Promise { - if (!this.themeIsDarkPromise.resolved) { - this.themeIsDarkPromise.resolve(request.isDark); - } else { - this.themeIsDarkPromise = createDeferred(); - this.themeIsDarkPromise.resolve(request.isDark); - } - const settings = this.generateDataScienceExtraSettings(); - const monacoTheme = await this.cssGenerator.generateMonacoTheme(request.isDark, settings.extraSettings.theme); - return this.postMessageInternal(CssMessages.GetMonacoThemeResponse, { theme: monacoTheme }); - } - - // tslint:disable-next-line:no-any - private webPanelRendered() { - if (!this.webPanelInit.resolved) { - // Send telemetry for startup - sendTelemetryEvent(Telemetry.WebviewStartup, this.startupStopwatch.elapsedTime, {type: this.title}); - - // Resolve our started promise. This means the webpanel is ready to go. - this.webPanelInit.resolve(); - } - } - - // Post a message to our webpanel and update our new datascience settings - private onPossibleSettingsChange = (event: ConfigurationChangeEvent) => { - if (event.affectsConfiguration('workbench.colorTheme') || - event.affectsConfiguration('editor.cursorStyle') || - event.affectsConfiguration('editor.cursorBlinking')) { - // See if the theme changed - const newSettings = this.generateDataScienceExtraSettings(); - if (newSettings) { - const dsSettings = JSON.stringify(newSettings); - this.postMessageInternal(SharedMessages.UpdateSettings, dsSettings).ignoreErrors(); - } - } - } - - // Post a message to our webpanel and update our new datascience settings - private onDataScienceSettingsChanged = () => { - // Stringify our settings to send over to the panel - const dsSettings = JSON.stringify(this.generateDataScienceExtraSettings()); - this.postMessageInternal(SharedMessages.UpdateSettings, dsSettings).ignoreErrors(); - } - - private loadWebPanel() { - traceInfo(`Loading web panel. Panel is ${this.webPanel ? 'set' : 'notset'}`); - - // Create our web panel (it's the UI that shows up for the history) - if (this.webPanel === undefined) { - - // Get our settings to pass along to the react control - const settings = this.generateDataScienceExtraSettings(); - - traceInfo('Loading web view...'); - // 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.viewColumn, this.messageListener, this.title, this.mainScriptPath, '', settings); - - traceInfo('Web view created.'); - } - } -} 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 81b3e14d5123..000000000000 --- a/src/client/debugger/debugAdapter/Common/debugStreamProvider.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { createServer, Server, 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 { - private server?: Server; - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - public get useDebugSocketStream(): boolean { - return this.getDebugPort() > 0; - } - public dispose() { - if (this.server) { - this.server.close(); - } - } - public async getInputAndOutputStreams(): Promise<{ input: NodeJS.ReadStream | Socket; output: NodeJS.WriteStream | Socket }> { - const debugPort = this.getDebugPort(); - let debugSocket: Promise | 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(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}`); - } - this.server = createServer((socket) => { - if (!isTestExecution()) { - console.error('>> accepted connection from client'); - } - resolve(socket); - }).listen(debugPort); - }); - } - - const currentProcess = this.serviceContainer.get(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); - - let debugPort = 0; - const args = currentProcess.argv.slice(2); - args.forEach(val => { - 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 cc969f52d8fd..000000000000 --- a/src/client/debugger/debugAdapter/Common/processServiceFactory.ts +++ /dev/null @@ -1,20 +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 { IDisposableRegistry } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; - -@injectable() -export class DebuggerProcessServiceFactory implements IProcessServiceFactory { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public create(): Promise { - const processService = new ProcessService(this.serviceContainer.get(IBufferDecoder), process.env); - this.serviceContainer.get(IDisposableRegistry).push(processService); - 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 91a36a6ec231..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_', message (for all protocol events) - * 1. 'request_', message (for all protocol requests) - * 1. 'response_', message (for all protocol responses) - * 1. '', message (for all protocol messages that are not events, requests nor responses) - * @export - * @class ProtocolParser - * @extends {EventEmitter} - * @implements {IProtocolParser} - */ -@injectable() -export class ProtocolParser 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 a4f77e8999e5..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 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 { - 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 bf23ca78a0b8..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 { - 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 { - 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((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', _ => { - // 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 - noop(); - }); - } - 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 extends DebugClient { - 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 db564c834110..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 { - const pathVariableName = this.pathUtils.getPathVariableName(); - - // Merge variables from both .env file and env json variables. - // tslint:disable-next-line:no-any - const debugLaunchEnvVars: Record = (args.env && Object.keys(args.env).length > 0) ? { ...args.env } as any : {} as any; - const envFileVars = await this.envParser.parseFile(args.envFile, debugLaunchEnvVars); - const env = envFileVars ? { ...envFileVars! } : {}; - this.envParser.mergeVariables(debugLaunchEnvVars, env); - - // Append the PYTHONPATH and PATH variables. - this.envParser.appendPath(env, debugLaunchEnvVars[pathVariableName]); - this.envParser.appendPythonPath(env, debugLaunchEnvVars.PYTHONPATH); - - if (typeof env[pathVariableName] === 'string' && env[pathVariableName]!.length > 0) { - // Now merge this path with the current system path. - // We need to do this to ensure the PATH variable always has the system PATHs as well. - this.envParser.appendPath(env, this.process.env[pathVariableName]!); - } - if (typeof env.PYTHONPATH === 'string' && env.PYTHONPATH.length > 0) { - // We didn't have a value for PATH earlier and now we do. - // Now merge this path with the current system path. - // We need to do this to ensure the PATH variable always has the system PATHs as well. - this.envParser.appendPythonPath(env, this.process.env.PYTHONPATH!); - } - - if (typeof args.console !== 'string' || args.console === 'internalConsole') { - // For debugging, when not using any terminal, then we need to provide all env variables. - // As we're spawning the process, we need to ensure all env variables are passed. - // Including those from the current process (i.e. everything, not just custom vars). - this.envParser.mergeVariables(this.process.env, env); - - if (env[pathVariableName] === undefined && typeof this.process.env[pathVariableName] === 'string') { - env[pathVariableName] = this.process.env[pathVariableName]; - } - if (env.PYTHONPATH === undefined && typeof this.process.env.PYTHONPATH === 'string') { - env.PYTHONPATH = this.process.env.PYTHONPATH; - } - } - - if (!env.hasOwnProperty('PYTHONIOENCODING')) { - env.PYTHONIOENCODING = 'UTF-8'; - } - if (!env.hasOwnProperty('PYTHONUNBUFFERED')) { - env.PYTHONUNBUFFERED = '1'; - } - - if (args.gevent) { - env.GEVENT_SUPPORT = 'True'; // this is read in pydevd_constants.py - } - - return env; - } -} diff --git a/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts b/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts deleted file mode 100644 index 466c1f1707fd..000000000000 --- a/src/client/debugger/debugAdapter/DebugClients/launcherProvider.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-classes-per-file - -import { optional } from 'inversify'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../common/constants'; -import { IDebugLauncherScriptProvider, IRemoteDebugLauncherScriptProvider, LocalDebugOptions, RemoteDebugOptions } from '../types'; - -const pathToScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'ptvsd_launcher.py'); -export class NoDebugLauncherScriptProvider implements IDebugLauncherScriptProvider { - constructor(@optional() private script: string = pathToScript) { } - public getLauncherArgs(options: LocalDebugOptions): string[] { - const customDebugger = options.customDebugger ? '--custom' : '--default'; - return [this.script, customDebugger, '--nodebug', '--client', '--host', options.host, '--port', options.port.toString()]; - } -} - -export class DebuggerLauncherScriptProvider implements IDebugLauncherScriptProvider { - constructor(@optional() private script: string = pathToScript) { } - public getLauncherArgs(options: LocalDebugOptions): string[] { - const customDebugger = options.customDebugger ? '--custom' : '--default'; - return [this.script, customDebugger, '--client', '--host', options.host, '--port', options.port.toString()]; - } -} - -/** - * This class is used to provide the launch scripts so external code can launch the debugger. - * As we're passing command arguments, we need to ensure the file paths are quoted. - * @export - * @class RemoteDebuggerExternalLauncherScriptProvider - * @implements {IRemoteDebugLauncherScriptProvider} - */ -export class RemoteDebuggerExternalLauncherScriptProvider implements IRemoteDebugLauncherScriptProvider { - constructor(@optional() private script: string = pathToScript) { } - public getLauncherArgs(options: RemoteDebugOptions): string[] { - const waitArgs = options.waitUntilDebuggerAttaches ? ['--wait'] : []; - return [this.script.fileToCommandArgument(), '--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 174c37ced8e6..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 1ad90cd4d0d7..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 07e043777abb..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; - public get client(): Promise { - 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; - public get DebugClientConnected(): Promise { - return this.debugClientConnected.promise; - } - constructor(debugSession: DebugSession) { - super(); - this.debugSession = debugSession; - this.debugClientConnected = createDeferred(); - this.clientSocket = createDeferred(); - } - - public abstract Start(): Promise; - public abstract Stop(): void; -} 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(); - } - - public Stop() { - if (this.socketServer) { - try { - this.socketServer.dispose(); - // tslint:disable-next-line:no-empty - } catch { } - this.socketServer = undefined; - } - } - - public async Start(): Promise { - 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); - 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 { - return new Promise((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 - (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 7bf56a430a3c..000000000000 --- a/src/client/debugger/debugAdapter/main.ts +++ /dev/null @@ -1,519 +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, IDisposable, IDisposableRegistry } 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(); - 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.supportsDelayedStackTraceLoading = true; - body.supportsConditionalBreakpoints = true; - body.supportsSetVariable = true; - body.supportsExceptionOptions = true; - body.supportsGotoTargetsRequest = 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); - 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(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; - clearTimeout(timeout); - 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; - private get initializeRequest(): Promise { - return this.initializeRequestDeferred.promise; - } - private readonly launchRequestDeferred: Deferred; - private get launchRequest(): Promise { - return this.launchRequestDeferred.promise; - } - - private readonly attachRequestDeferred: Deferred; - private get attachRequest(): Promise { - 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); - - this.inputProtocolParser = this.serviceContainer.get(IProtocolParser); - this.inputProtocolParser.connect(this.throughInputStream); - this.disposables.push(this.inputProtocolParser); - this.outputProtocolParser = this.serviceContainer.get(IProtocolParser); - this.outputProtocolParser.connect(this.throughOutputStream); - this.disposables.push(this.outputProtocolParser); - - this.protocolLogger = this.serviceContainer.get(IProtocolLogger); - this.protocolLogger.connect(this.throughInputStream, this.throughOutputStream); - this.disposables.push(this.protocolLogger); - - this.initializeRequestDeferred = createDeferred(); - this.launchRequestDeferred = createDeferred(); - this.attachRequestDeferred = createDeferred(); - } - public dispose() { - try { - const disposables = this.serviceContainer.get(IDisposableRegistry); - disposables.forEach(d => { - try { d.dispose(); } catch { noop(); } - }); - } catch { - noop(); - } - logger.verbose('main dispose'); - this.shutdown().ignoreErrors(); - } - public async start() { - const debugStreamProvider = this.serviceContainer.get(IDebugStreamProvider); - this.disposables.push(debugStreamProvider); - 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); - 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); - 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(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); - 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 2c015eb99297..000000000000 --- a/src/client/debugger/debugAdapter/serviceRegistry.ts +++ /dev/null @@ -1,46 +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, IDisposableRegistry, 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, serviceContainer); - registerTypes(serviceManager); - return serviceContainer; -} - -function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(ICurrentProcess, CurrentProcess); - serviceManager.addSingletonInstance(IDisposableRegistry, []); - serviceManager.addSingleton(IDebugStreamProvider, DebugStreamProvider); - serviceManager.addSingleton(IProtocolLogger, ProtocolLogger); - serviceManager.add(IProtocolParser, ProtocolParser); - serviceManager.addSingleton(IFileSystem, FileSystem); - serviceManager.addSingleton(IPlatformService, PlatformService); - serviceManager.addSingleton(ISocketServer, SocketServer); - serviceManager.addSingleton(IProtocolMessageWriter, ProtocolMessageWriter); - serviceManager.addSingleton(IBufferDecoder, BufferDecoder); - serviceManager.addSingleton(IProcessServiceFactory, DebuggerProcessServiceFactory); -} diff --git a/src/client/debugger/debugAdapter/types.ts b/src/client/debugger/debugAdapter/types.ts deleted file mode 100644 index 886623b65e2f..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 { - getLauncherArgs(options: T): string[]; -} - -export interface ILocalDebugLauncherScriptProvider extends IDebugLauncherScriptProvider { - getLauncherArgs(options: LocalDebugOptions): string[]; -} - -export interface IRemoteDebugLauncherScriptProvider extends IDebugLauncherScriptProvider { -} - -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 extends Disposable { - 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 { + 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 { + 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} Path to the python interpreter for this workspace. + * @memberof DebugAdapterDescriptorFactory + */ + private async getDebugAdapterPython( + configuration: LaunchRequestArguments | AttachRequestArguments, + workspaceFolder?: WorkspaceFolder, + ): Promise { + if (configuration.debugAdapterPython !== undefined) { + return this.getExecutableCommand( + await this.interpreterService.getInterpreterDetails(configuration.debugAdapterPython), + ); + } else if (configuration.pythonPath) { + return this.getExecutableCommand( + await this.interpreterService.getInterpreterDetails(configuration.pythonPath), + ); + } + + const resourceUri = workspaceFolder ? workspaceFolder.uri : undefined; + const interpreter = await this.interpreterService.getActiveInterpreter(resourceUri); + if (interpreter) { + traceVerbose(`Selecting active interpreter as Python Executable for DA '${interpreter.path}'`); + return 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 { + 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 { + 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 { + 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 { + return new Promise(async (resolve, reject) => { + const processEntries = await this.attachItemsProvider.getAttachItems(); + + const refreshButton = { + iconPath: getIcon(REFRESH_BUTTON_ICON), + tooltip: AttachProcess.refreshList, + }; + + const quickPick = this.applicationShell.createQuickPick(); + quickPick.title = AttachProcess.attachTitle; + quickPick.placeholder = AttachProcess.selectProcessPlaceholder; + quickPick.canSelectMany = false; + quickPick.matchOnDescription = true; + quickPick.matchOnDetail = true; + quickPick.items = processEntries; + quickPick.buttons = [refreshButton]; + + const disposables: Disposable[] = []; + + quickPick.onDidTriggerButton( + async () => { + 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 { + return this._getInternalProcessEntries().then((processEntries) => { + processEntries.sort( + ( + { processName: aprocessName, commandLine: aCommandLine }, + { processName: bProcessName, commandLine: bCommandLine }, + ) => { + const compare = (aString: string, bString: string): number => { + // localeCompare is significantly slower than < and > (2000 ms vs 80 ms for 10,000 elements) + // We can change to localeCompare if this becomes an issue + const aLower = aString.toLowerCase(); + const bLower = bString.toLowerCase(); + + if (aLower === bLower) { + return 0; + } + + return aLower < bLower ? -1 : 1; + }; + + const aPython = aprocessName.startsWith('python'); + const bPython = bProcessName.startsWith('python'); + + if (aPython || bPython) { + if (aPython && !bPython) { + return -1; + } + if (bPython && !aPython) { + return 1; + } + + return aPython ? compare(aCommandLine!, bCommandLine!) : compare(bCommandLine!, aCommandLine!); + } + + return compare(aprocessName, bProcessName); + }, + ); + + return processEntries; + }); + } + + public async _getInternalProcessEntries(): Promise { + let processCmd: ProcessListCommand; + if (this.platformService.isMac) { + processCmd = PsProcessParser.psDarwinCommand; + } else if (this.platformService.isLinux) { + processCmd = PsProcessParser.psLinuxCommand; + } else if (this.platformService.isWindows) { + processCmd = WmicProcessParser.wmicCommand; + } else { + throw new Error(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; +} + +export const IAttachProcessProviderFactory = Symbol('IAttachProcessProviderFactory'); +export interface IAttachProcessProviderFactory { + registerCommands(): void; +} + +export interface IAttachPicker { + showQuickPick(): Promise; +} + +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); - const key = PersistentStateKeys.ShowBanner; - const state = factory.createGlobalPersistentState(key, true); - return state.value; - } - - public async disable(): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const key = PersistentStateKeys.ShowBanner; - const state = factory.createGlobalPersistentState(key, false); - await state.updateValue(false); - } - - // showing banner - - public async shouldShow(): Promise { - if (!this.isEnabled() || this.disabledInCurrentSession) { - return false; - } - if (! await this.passedThreshold()) { - return false; - } - return this.isUserSelected(); - } - - public async show(): Promise { - const appShell = this.serviceContainer.get(IApplicationShell); - const msg = 'Can you please take 2 minutes to tell us how the debugger is working for you?'; - const yes = 'Yes, take survey now'; - const no = 'No thanks'; - const later = 'Remind me later'; - const response = await appShell.showInformationMessage(msg, yes, no, later); - switch (response) { - case yes: - { - await this.action(); - await this.disable(); - break; - } - case no: { - await this.disable(); - break; - } - default: { - // Disable for the current session. - this.disabledInCurrentSession = true; - } - } - } - - private async action(): Promise { - const debuggerLaunchCounter = await this.getGetDebuggerLaunchCounter(); - const browser = this.serviceContainer.get(IBrowserService); - browser.launch(`https://www.research.net/r/N7B25RV?n=${debuggerLaunchCounter}`); - } - - // user selection - - private async isUserSelected(): Promise { - if (this.userSelected !== undefined) { - return this.userSelected; - } - - const factory = this.serviceContainer.get(IPersistentStateFactory); - const key = PersistentStateKeys.UserSelected; - const state = factory.createGlobalPersistentState(key, undefined); - let selected = state.value; - if (selected === undefined) { - const runtime = this.serviceContainer.get(IRandom); - const randomSample = runtime.getRandomInt(0, 100); - selected = randomSample < SAMPLE_SIZE_PER_HUNDRED; - state.updateValue(selected).ignoreErrors(); - } - this.userSelected = selected; - return selected; - } - - // persistent counter - - private async passedThreshold(): Promise { - const [threshold, debuggerCounter] = await Promise.all([ - this.getDebuggerLaunchThresholdCounter(), - this.getGetDebuggerLaunchCounter() - ]); - return debuggerCounter >= threshold; - } - - private async incrementDebuggerLaunchCounter(): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const key = PersistentStateKeys.DebuggerLaunchCounter; - const state = factory.createGlobalPersistentState(key, 0); - await state.updateValue(state.value + 1); - } - - private async getGetDebuggerLaunchCounter(): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const key = PersistentStateKeys.DebuggerLaunchCounter; - const state = factory.createGlobalPersistentState(key, 0); - return state.value; - } - - private async getDebuggerLaunchThresholdCounter(): Promise { - const factory = this.serviceContainer.get(IPersistentStateFactory); - const key = PersistentStateKeys.DebuggerLaunchThresholdCounter; - const state = factory.createGlobalPersistentState(key, undefined); - if (state.value === undefined) { - const runtime = this.serviceContainer.get(IRandom); - const randomNumber = runtime.getRandomInt(1, 11); - await state.updateValue(randomNumber); - } - return state.value!; - } - - // debugger-specific functionality - - private addCallback() { - const debuggerService = this.serviceContainer.get(IDebugService); - const disposable = debuggerService.onDidTerminateDebugSession(async e => { - if (e.type === DebuggerTypeName) { - const logger = this.serviceContainer.get(ILogger); - await this.onDidTerminateDebugSession() - .catch(ex => logger.logError('Error in debugger Banner', ex)); - } - }); - this.serviceContainer.get(IDisposableRegistry).push(disposable); - } - - private async onDidTerminateDebugSession(): Promise { - if (!this.isEnabled()) { - return; - } - await this.incrementDebuggerLaunchCounter(); - const show = await this.shouldShow(); - if (!show) { - return; - } - - await this.show(); - } -} diff --git a/src/client/debugger/extension/configuration/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 { - 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 e18bd7a133a3..9997fb4f0509 100644 --- a/src/client/debugger/extension/configuration/debugConfigurationService.ts +++ b/src/client/debugger/extension/configuration/debugConfigurationService.ts @@ -4,103 +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 { DebugConfigStrings } from '../../../common/utils/localize'; -import { IMultiStepInput, IMultiStepInputFactory, InputStep, IQuickPickParameters } from '../../../common/utils/multiStepInput'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } 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, - @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver, - @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 { - const config: Partial = {}; - const state = { config, folder, token }; - - // Disabled until configuration issues are addressed by VS Code. See #4007 - const multiStep = this.multiStepFactory.create(); - await multiStep.run((input, s) => this.pickDebugConfiguration(input, s), state); + constructor( + @inject(IDebugConfigurationResolver) + @named('attach') + private readonly attachResolver: IDebugConfigurationResolver, + @inject(IDebugConfigurationResolver) + @named('launch') + private readonly launchResolver: IDebugConfigurationResolver, + ) {} - 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 { + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + token?: CancellationToken, + ): Promise { if (debugConfiguration.request === 'attach') { - return this.attachResolver.resolveDebugConfiguration(folder, debugConfiguration as AttachRequestArguments, token); - } else if (debugConfiguration.request === 'test') { - throw Error('Please use the command \'Python: Debug Unit Tests\''); + 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 { if (Object.keys(debugConfiguration).length === 0) { - const configs = await this.provideDebugConfigurations(folder, token); - if (Array.isArray(configs) && configs.length === 1) { - debugConfiguration = configs[0]; - } + return undefined; } - return this.launchResolver.resolveDebugConfiguration(folder, debugConfiguration as LaunchRequestArguments, token); + return this.launchResolver.resolveDebugConfiguration( + folder, + debugConfiguration as LaunchRequestArguments, + token, + ); } } - protected async getDefaultDebugConfig(): Promise { - sendTelemetryEvent(EventName.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, - state: DebugConfigurationState - ): Promise | void> { - type DebugConfigurationQuickPickItem = QuickPickItem & { type: DebugConfigurationType }; - const items: DebugConfigurationQuickPickItem[] = [ - { - label: DebugConfigStrings.file.selectConfiguration.label(), - type: DebugConfigurationType.launchFile, - description: DebugConfigStrings.file.selectConfiguration.description() - }, { - label: DebugConfigStrings.module.selectConfiguration.label(), - type: DebugConfigurationType.launchModule, - description: DebugConfigStrings.module.selectConfiguration.description() - }, { - label: DebugConfigStrings.attach.selectConfiguration.label(), - type: DebugConfigurationType.remoteAttach, - description: DebugConfigStrings.attach.selectConfiguration.description() - }, { - label: DebugConfigStrings.django.selectConfiguration.label(), - type: DebugConfigurationType.launchDjango, - description: DebugConfigStrings.django.selectConfiguration.description() - }, { - label: DebugConfigStrings.flask.selectConfiguration.label(), - type: DebugConfigurationType.launchFlask, - description: DebugConfigStrings.flask.selectConfiguration.description() - }, { - label: DebugConfigStrings.pyramid.selectConfiguration.label(), - type: DebugConfigurationType.launchPyramid, - description: DebugConfigStrings.pyramid.selectConfiguration.description() - } - ]; - state.config = {}; - const pick = await input.showQuickPick>({ - title: DebugConfigStrings.selectConfiguration.title(), - placeholder: DebugConfigStrings.selectConfiguration.placeholder(), - activeItem: items[0], - items: items - }); - if (pick) { - const provider = this.providerFactory.create(pick.type); - return provider.buildConfiguration.bind(provider); + + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: DebugConfiguration, + token?: CancellationToken, + ): Promise { + function resolve(resolver: IDebugConfigurationResolver) { + return resolver.resolveDebugConfigurationWithSubstitutedVariables(folder, debugConfiguration as T, token); } + return debugConfiguration.request === 'attach' ? resolve(this.attachResolver) : resolve(this.launchResolver); } } diff --git a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts b/src/client/debugger/extension/configuration/launch.json/completionProvider.ts deleted file mode 100644 index 9b646275d490..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/completionProvider.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { getLocation } from 'jsonc-parser'; -import * as path from 'path'; -import { CancellationToken, CompletionItem, CompletionItemKind, CompletionItemProvider, Position, SnippetString, TextDocument } from 'vscode'; -import { IExtensionActivationService } from '../../../../activation/types'; -import { ILanguageService } from '../../../../common/application/types'; -import { IDisposableRegistry, Resource } from '../../../../common/types'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; - -const configurationNodeName = 'configurations'; -enum JsonLanguages { - json = 'json', - jsonWithComments = 'jsonc' -} - -@injectable() -export class LaunchJsonCompletionProvider implements CompletionItemProvider, IExtensionActivationService { - constructor(@inject(ILanguageService) private readonly languageService: ILanguageService, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry) { } - public async activate(_resource: Resource): Promise { - this.disposableRegistry.push(this.languageService.registerCompletionItemProvider({ language: JsonLanguages.json }, this)); - this.disposableRegistry.push(this.languageService.registerCompletionItemProvider({ language: JsonLanguages.jsonWithComments }, this)); - } - public async provideCompletionItems(document: TextDocument, position: Position, token: CancellationToken): Promise { - if (!this.canProvideCompletions(document, position)) { - return []; - } - - return [ - { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description(), - arguments: [document, position, token] - }, - documentation: DebugConfigStrings.launchJsonCompletions.description(), - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label(), - insertText: new SnippetString() - } - ]; - } - public canProvideCompletions(document: TextDocument, position: Position) { - if (path.basename(document.uri.fsPath) !== 'launch.json') { - return false; - } - const location = getLocation(document.getText(), document.offsetAt(position)); - // Cursor must be inside the configurations array and not in any nested items. - // Hence path[0] = array, path[1] = array element index. - return (location.path[0] === configurationNodeName && location.path.length === 2); - } -} diff --git a/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts new file mode 100644 index 000000000000..d5857638821a --- /dev/null +++ b/src/client/debugger/extension/configuration/launch.json/launchJsonReader.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { parse } from 'jsonc-parser'; +import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; +import * as fs from '../../../../common/platform/fs-paths'; +import { getConfiguration, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { traceLog } from '../../../../logging'; + +export async function getConfigurationsForWorkspace(workspace: WorkspaceFolder): Promise { + const filename = path.join(workspace.uri.fsPath, '.vscode', 'launch.json'); + if (!(await fs.pathExists(filename))) { + // Check launch config in the workspace file + const codeWorkspaceConfig = getConfiguration('launch', workspace); + if (!codeWorkspaceConfig.configurations || !Array.isArray(codeWorkspaceConfig.configurations)) { + return []; + } + traceLog('Using configuration in workspace'); + return codeWorkspaceConfig.configurations; + } + + const text = await fs.readFile(filename, 'utf-8'); + const parsed = parse(text, [], { allowTrailingComma: true, disallowComments: false }); + if (!parsed.configurations || !Array.isArray(parsed.configurations)) { + throw Error('Missing field in launch.json: configurations'); + } + if (!parsed.version) { + throw Error('Missing field in launch.json: version'); + } + // We do not bother ensuring each item is a DebugConfiguration... + traceLog('Using configuration in launch.json'); + return parsed.configurations; +} + +export async function getConfigurationsByUri(uri?: Uri): Promise { + if (uri) { + const workspace = getWorkspaceFolder(uri); + if (workspace) { + return getConfigurationsForWorkspace(workspace); + } + } + return []; +} diff --git a/src/client/debugger/extension/configuration/launch.json/updaterService.ts b/src/client/debugger/extension/configuration/launch.json/updaterService.ts deleted file mode 100644 index 4c47516e1e23..000000000000 --- a/src/client/debugger/extension/configuration/launch.json/updaterService.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 { createScanner, parse, SyntaxKind } from 'jsonc-parser'; -import { CancellationToken, DebugConfiguration, Position, TextDocument, WorkspaceEdit } from 'vscode'; -import { IExtensionActivationService } from '../../../../activation/types'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; -import { IDisposableRegistry, Resource } from '../../../../common/types'; -import { noop } from '../../../../common/utils/misc'; -import { captureTelemetry } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { IDebugConfigurationService } from '../../types'; - -type PositionOfCursor = 'InsideEmptyArray' | 'BeforeItem' | 'AfterItem'; - -export class LaunchJsonUpdaterServiceHelper { - constructor(private readonly commandManager: ICommandManager, - private readonly workspace: IWorkspaceService, - private readonly documentManager: IDocumentManager, - private readonly configurationProvider: IDebugConfigurationService) { } - @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON) - public async selectAndInsertDebugConfig(document: TextDocument, position: Position, token: CancellationToken): Promise { - if (this.documentManager.activeTextEditor && this.documentManager.activeTextEditor.document === document) { - const folder = this.workspace.getWorkspaceFolder(document.uri); - const configs = await this.configurationProvider.provideDebugConfigurations!(folder, token); - - if (!token.isCancellationRequested && Array.isArray(configs) && configs.length > 0) { - // Always use the first available debug configuration. - await this.insertDebugConfiguration(document, position, configs[0]); - } - } - } - /** - * Inserts the debug configuration into the document. - * Invokes the document formatter to ensure JSON is formatted nicely. - * @param {TextDocument} document - * @param {Position} position - * @param {DebugConfiguration} config - * @returns {Promise} - * @memberof LaunchJsonCompletionItemProvider - */ - public async insertDebugConfiguration(document: TextDocument, position: Position, config: DebugConfiguration): Promise { - const cursorPosition = this.getCursorPositionInConfigurationsArray(document, position); - if (!cursorPosition) { - return; - } - const formattedJson = this.getTextForInsertion(config, cursorPosition); - const workspaceEdit = new WorkspaceEdit(); - workspaceEdit.insert(document.uri, position, formattedJson); - await this.documentManager.applyEdit(workspaceEdit); - this.commandManager.executeCommand('editor.action.formatDocument').then(noop, noop); - } - /** - * Gets the string representation of the debug config for insertion in the document. - * Adds necessary leading or trailing commas (remember the text is added into an array). - * @param {TextDocument} document - * @param {Position} position - * @param {DebugConfiguration} config - * @returns - * @memberof LaunchJsonCompletionItemProvider - */ - public getTextForInsertion(config: DebugConfiguration, cursorPosition: PositionOfCursor) { - const json = JSON.stringify(config); - if (cursorPosition === 'AfterItem') { - return `,${json}`; - } - if (cursorPosition === 'BeforeItem') { - return `${json},`; - } - return json; - } - public getCursorPositionInConfigurationsArray(document: TextDocument, position: Position): PositionOfCursor | undefined { - if (this.isConfigurationArrayEmpty(document)) { - return 'InsideEmptyArray'; - } - const scanner = createScanner(document.getText(), true); - scanner.setPosition(document.offsetAt(position)); - const nextToken = scanner.scan(); - if (nextToken === SyntaxKind.CommaToken || nextToken === SyntaxKind.CloseBracketToken) { - return 'AfterItem'; - } - if (nextToken === SyntaxKind.OpenBraceToken) { - return 'BeforeItem'; - } - } - public isConfigurationArrayEmpty(document: TextDocument): boolean { - const configuration = parse(document.getText(), [], { allowTrailingComma: true, disallowComments: false }) as { configurations: [] }; - return (!configuration || !Array.isArray(configuration.configurations) || configuration.configurations.length === 0); - } -} - -@injectable() -export class LaunchJsonUpdaterService implements IExtensionActivationService { - private activated: boolean = false; - constructor(@inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IDebugConfigurationService) private readonly configurationProvider: IDebugConfigurationService) { } - public async activate(_resource: Resource): Promise { - if (this.activated) { - return; - } - this.activated = true; - const handler = new LaunchJsonUpdaterServiceHelper(this.commandManager, this.workspace, this.documentManager, this.configurationProvider); - this.disposableRegistry.push(this.commandManager.registerCommand('python.SelectAndInsertDebugConfiguration', handler.selectAndInsertDebugConfig, handler)); - } -} diff --git a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts b/src/client/debugger/extension/configuration/providers/djangoLaunch.ts deleted file mode 100644 index f17650b53bbb..000000000000 --- a/src/client/debugger/extension/configuration/providers/djangoLaunch.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../common/application/types'; -import { IFileSystem } from '../../../../common/platform/types'; -import { IPathUtils } from '../../../../common/types'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -// tslint:disable-next-line:no-invalid-template-strings -const workspaceFolderToken = '${workspaceFolder}'; - -@injectable() -export class DjangoLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IFileSystem) private fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPathUtils) private pathUtils: IPathUtils) { } - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const program = await this.getManagePyPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const defaultProgram = `${workspaceFolderToken}${this.pathUtils.separator}manage.py`; - const config: Partial = { - name: DebugConfigStrings.django.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - program: program || defaultProgram, - args: [ - 'runserver', - '--noreload' - ], - django: true - }; - if (!program) { - const selectedProgram = await input.showInputBox({ - title: DebugConfigStrings.django.enterManagePyPath.title(), - value: defaultProgram, - prompt: DebugConfigStrings.django.enterManagePyPath.prompt(), - validate: value => this.validateManagePy(state.folder, defaultProgram, value) - }); - if (selectedProgram) { - manuallyEnteredAValue = true; - config.program = selectedProgram; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchDjango, autoDetectedDjangoManagePyPath: !!program, manuallyEnteredAValue }); - Object.assign(state.config, config); - } - public async validateManagePy(folder: WorkspaceFolder | undefined, defaultValue: string, selected?: string): Promise { - const error = DebugConfigStrings.django.enterManagePyPath.invalid(); - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = this.resolveVariables(selected, folder ? folder.uri : undefined); - if (selected !== defaultValue && !await this.fs.fileExists(resolvedPath)) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.py')) { - return error; - } - return; - } - protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { - const 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 { - 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 f26d2bc93a6b..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 { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { captureTelemetry } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -@injectable() -export class FileLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - @captureTelemetry(EventName.DEBUGGER_CONFIGURATION_PROMPTS, { configurationType: DebugConfigurationType.launchFile }, false) - public async buildConfiguration(_input: MultiStepInput, state: DebugConfigurationState) { - const config: Partial = { - name: DebugConfigStrings.file.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - // tslint:disable-next-line:no-invalid-template-strings - program: '${file}', - console: 'integratedTerminal' - }; - Object.assign(state.config, config); - } -} diff --git a/src/client/debugger/extension/configuration/providers/flaskLaunch.ts b/src/client/debugger/extension/configuration/providers/flaskLaunch.ts deleted file mode 100644 index b573aceeb2a2..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 { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -@injectable() -export class FlaskLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IFileSystem) private fs: IFileSystem) { } - public isSupported(debugConfigurationType: DebugConfigurationType): boolean { - return debugConfigurationType === DebugConfigurationType.launchFlask; - } - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const application = await this.getApplicationPath(state.folder); - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.flask.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: application || 'app.py', - FLASK_ENV: 'development', - FLASK_DEBUG: '0' - }, - args: [ - 'run', - '--no-debugger', - '--no-reload' - ], - jinja: true - }; - - if (!application) { - const selectedApp = await input.showInputBox({ - title: DebugConfigStrings.flask.enterAppPathOrNamePath.title(), - value: 'app.py', - prompt: DebugConfigStrings.flask.enterAppPathOrNamePath.prompt(), - validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : DebugConfigStrings.flask.enterAppPathOrNamePath.invalid()) - }); - if (selectedApp) { - manuallyEnteredAValue = true; - config.env!.FLASK_APP = selectedApp; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchFlask, autoDetectedFlaskAppPyPath: !!application, manuallyEnteredAValue }); - Object.assign(state.config, config); - } - protected async getApplicationPath(folder: WorkspaceFolder | undefined): Promise { - if (!folder) { - return; - } - const defaultLocationOfManagePy = path.join(folder.uri.fsPath, 'app.py'); - if (await this.fs.fileExists(defaultLocationOfManagePy)) { - return 'app.py'; - } - } -} diff --git a/src/client/debugger/extension/configuration/providers/moduleLaunch.ts b/src/client/debugger/extension/configuration/providers/moduleLaunch.ts deleted file mode 100644 index 5c39b957c503..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 { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -@injectable() -export class ModuleLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - let manuallyEnteredAValue: boolean | undefined; - const config: Partial = { - name: DebugConfigStrings.module.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default() - }; - const selectedModule = await input.showInputBox({ - title: DebugConfigStrings.module.enterModule.title(), - value: config.module || DebugConfigStrings.module.enterModule.default(), - prompt: DebugConfigStrings.module.enterModule.prompt(), - validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : DebugConfigStrings.module.enterModule.invalid()) - }); - if (selectedModule) { - manuallyEnteredAValue = true; - config.module = selectedModule; - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchModule, manuallyEnteredAValue }); - Object.assign(state.config, config); - } -} diff --git a/src/client/debugger/extension/configuration/providers/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; - 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(); - 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 0e833e54eecd..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 { DebugConfigStrings } from '../../../../common/utils/localize'; -import { MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { SystemVariables } from '../../../../common/variables/systemVariables'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { LaunchRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -// tslint:disable-next-line:no-invalid-template-strings -const workspaceFolderToken = '${workspaceFolder}'; - -@injectable() -export class PyramidLaunchDebugConfigurationProvider implements IDebugConfigurationProvider { - constructor(@inject(IFileSystem) private fs: IFileSystem, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(IPathUtils) private pathUtils: IPathUtils) { } - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState) { - const iniPath = await this.getDevelopmentIniPath(state.folder); - const defaultIni = `${workspaceFolderToken}${this.pathUtils.separator}development.ini`; - let manuallyEnteredAValue: boolean | undefined; - - const config: Partial = { - name: DebugConfigStrings.pyramid.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - args: [ - iniPath || defaultIni - ], - pyramid: true, - jinja: true - }; - - if (!iniPath) { - const selectedIniPath = await input.showInputBox({ - title: DebugConfigStrings.pyramid.enterDevelopmentIniPath.title(), - value: defaultIni, - prompt: DebugConfigStrings.pyramid.enterDevelopmentIniPath.prompt(), - validate: value => this.validateIniPath(state ? state.folder : undefined, defaultIni, value) - }); - if (selectedIniPath) { - manuallyEnteredAValue = true; - config.args = [selectedIniPath]; - } - } - - sendTelemetryEvent(EventName.DEBUGGER_CONFIGURATION_PROMPTS, undefined, { configurationType: DebugConfigurationType.launchPyramid, autoDetectedPyramidIniPath: !!iniPath, manuallyEnteredAValue }); - Object.assign(state.config, config); - } - public async validateIniPath(folder: WorkspaceFolder | undefined, defaultValue: string, selected?: string): Promise { - if (!folder) { - return; - } - const error = DebugConfigStrings.pyramid.enterDevelopmentIniPath.invalid(); - if (!selected || selected.trim().length === 0) { - return error; - } - const resolvedPath = this.resolveVariables(selected, folder.uri); - if (selected !== defaultValue && !await this.fs.fileExists(resolvedPath)) { - return error; - } - if (!resolvedPath.trim().toLowerCase().endsWith('.ini')) { - return error; - } - } - protected resolveVariables(pythonPath: string, resource: Uri | undefined): string { - const 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 { - 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 ca3fb57fe923..000000000000 --- a/src/client/debugger/extension/configuration/providers/remoteAttach.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { DebugConfigStrings } from '../../../../common/utils/localize'; -import { InputStep, MultiStepInput } from '../../../../common/utils/multiStepInput'; -import { sendTelemetryEvent } from '../../../../telemetry'; -import { EventName } from '../../../../telemetry/constants'; -import { DebuggerTypeName } from '../../../constants'; -import { AttachRequestArguments } from '../../../types'; -import { DebugConfigurationState, DebugConfigurationType, IDebugConfigurationProvider } from '../../types'; - -const defaultHost = 'localhost'; -const defaultPort = 5678; - -@injectable() -export class RemoteAttachDebugConfigurationProvider implements IDebugConfigurationProvider { - public async buildConfiguration(input: MultiStepInput, state: DebugConfigurationState): Promise | void> { - const config: Partial = { - name: DebugConfigStrings.attach.snippet.name(), - type: DebuggerTypeName, - request: 'attach', - port: defaultPort, - host: defaultHost, - pathMappings: [ - { - // tslint:disable-next-line:no-invalid-template-strings - localRoot: '${workspaceFolder}', - remoteRoot: '.' - } - ] - }; - - config.host = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemoteHost.title(), - step: 1, - totalSteps: 2, - value: config.host || defaultHost, - prompt: DebugConfigStrings.attach.enterRemoteHost.prompt(), - validate: value => Promise.resolve((value && value.trim().length > 0) ? undefined : DebugConfigStrings.attach.enterRemoteHost.invalid()) - }); - if (!config.host) { - config.host = defaultHost; - } - - sendTelemetryEvent(EventName.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, config: Partial) { - const port = await input.showInputBox({ - title: DebugConfigStrings.attach.enterRemotePort.title(), - step: 2, - totalSteps: 2, - value: (config.port || defaultPort).toString(), - prompt: DebugConfigStrings.attach.enterRemotePort.prompt(), - validate: value => Promise.resolve((value && /^\d+$/.test(value.trim())) ? undefined : DebugConfigStrings.attach.enterRemotePort.invalid()) - }); - if (port && /^\d+$/.test(port.trim())) { - config.port = parseInt(port, 10); - } - if (!config.port) { - config.port = defaultPort; - } - sendTelemetryEvent(EventName.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 cd60af3168cf..1c232f261d03 100644 --- a/src/client/debugger/extension/configuration/resolvers/attach.ts +++ b/src/client/debugger/extension/configuration/resolvers/attach.ts @@ -3,126 +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 { SystemVariables } from '../../../../common/variables/systemVariables'; -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 { - 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 { - const workspaceFolder = this.getWorkspaceFolder(folder); + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: AttachRequestArguments, + _token?: CancellationToken, + ): Promise { + 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 { + + protected async provideAttachDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: AttachRequestArguments, + ): Promise { 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'; } - if (debugConfiguration.justMyCode === undefined) { - // Populate justMyCode using debugStdLib - debugConfiguration.justMyCode = !debugConfiguration.debugStdLib; - } debugConfiguration.showReturnValue = debugConfiguration.showReturnValue !== false; // Pass workspace folder so we can get this when we get debug events firing. debugConfiguration.workspaceFolder = workspaceFolder ? workspaceFolder.fsPath : undefined; const debugOptions = debugConfiguration.debugOptions!; - if (!debugConfiguration.justMyCode) { - this.debugOption(debugOptions, DebugOptions.DebugStdLib); - } if (debugConfiguration.django) { - this.debugOption(debugOptions, DebugOptions.Django); + 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.showReturnValue) { - this.debugOption(debugOptions, DebugOptions.ShowReturnValue); + AttachConfigurationResolver.debugOption(debugOptions, DebugOptions.ShowReturnValue); } - if (!debugConfiguration.pathMappings) { - debugConfiguration.pathMappings = []; - } + 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 && - ['LOCALHOST', '127.0.0.1', '::1'].indexOf(debugConfiguration.host.toUpperCase()) >= 0) { - let configPathMappings; - if (debugConfiguration.pathMappings!.length === 0) { - configPathMappings = [{ - localRoot: workspaceFolder.fsPath, - remoteRoot: workspaceFolder.fsPath - }]; - } else { - // Expand ${workspaceFolder} variable first if necessary. - const systemVariables = new SystemVariables(workspaceFolder.fsPath); - configPathMappings = debugConfiguration.pathMappings.map(({ localRoot: mappedLocalRoot, remoteRoot }) => ({ - localRoot: systemVariables.resolveAny(mappedLocalRoot), - remoteRoot - })); - } - // If on Windows, lowercase the drive letter for path mappings. - let pathMappings = configPathMappings; - if (this.platformService.isWindows) { - pathMappings = configPathMappings.map(({ localRoot: windowsLocalRoot, remoteRoot }) => { - let localRoot = windowsLocalRoot; - if (windowsLocalRoot.match(/^[A-Z]:/)) { - localRoot = `${windowsLocalRoot[0].toLowerCase()}${windowsLocalRoot.substr(1)}`; - } - return { localRoot, remoteRoot }; - }); - } - debugConfiguration.pathMappings = pathMappings; + 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 64c9015f88ba..fde55ad8d5ea 100644 --- a/src/client/debugger/extension/configuration/resolvers/base.ts +++ b/src/client/debugger/extension/configuration/resolvers/base.ts @@ -3,101 +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 { EventName } 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 implements IDebugConfigurationResolver { +export abstract class BaseConfigurationResolver + implements IDebugConfigurationResolver { protected pythonPathSource: PythonPathSource = PythonPathSource.launchJson; - constructor(protected readonly workspaceService: IWorkspaceService, - protected readonly documentManager: IDocumentManager, - protected readonly configurationService: IConfigurationService) { } - public abstract resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: DebugConfiguration, token?: CancellationToken): Promise; - protected getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + + 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 { + 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; + + 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 { + 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 { 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; - } else{ + 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) { - 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' | 'test', debugConfiguration: Partial) { - const name = debugConfiguration.name || ''; - const moduleName = debugConfiguration.module || ''; - const telemetryProps: DebuggerTelemetry = { - trigger, - console: debugConfiguration.console, - hasEnvVars: typeof debugConfiguration.env === 'object' && Object.keys(debugConfiguration.env).length > 0, - django: !!debugConfiguration.django, - flask: this.isDebuggingFlask(debugConfiguration), - hasArgs: Array.isArray(debugConfiguration.args) && debugConfiguration.args.length > 0, - isLocalhost: this.isLocalHost(debugConfiguration.host), - isModule: moduleName.length > 0, - isSudo: !!debugConfiguration.sudo, - jinja: !!debugConfiguration.jinja, - pyramid: !!debugConfiguration.pyramid, - stopOnEntry: !!debugConfiguration.stopOnEntry, - showReturnValue: !!debugConfiguration.showReturnValue, - subProcess: !!debugConfiguration.subProcess, - watson: name.toLowerCase().indexOf('watson') >= 0, - pyspark: name.toLowerCase().indexOf('pyspark') >= 0, - gevent: name.toLowerCase().indexOf('gevent') >= 0, - scrapy: moduleName.toLowerCase() === 'scrapy' - }; - sendTelemetryEvent(EventName.DEBUGGER, undefined, telemetryProps); + + protected static isDebuggingFastAPI( + debugConfiguration: Partial, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FASTAPI'); } + protected static isDebuggingFlask( + debugConfiguration: Partial, + ): boolean { + return !!(debugConfiguration.module && debugConfiguration.module.toUpperCase() === 'FLASK'); + } } diff --git a/src/client/debugger/extension/configuration/resolvers/helper.ts b/src/client/debugger/extension/configuration/resolvers/helper.ts 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; +} + +@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 { + const pathVariableName = getSearchPathEnvVarNames()[0]; + + // Merge variables from both .env file and env json variables. + const debugLaunchEnvVars: Record = + args.env && Object.keys(args.env).length > 0 + ? ({ ...args.env } as Record) + : ({} as Record); + const envFileVars = await this.envParser.parseFile(args.envFile, debugLaunchEnvVars); + const env = envFileVars ? { ...envFileVars } : {}; + + // "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 4efb111655ba..3ca38fb0f710 100644 --- a/src/client/debugger/extension/configuration/resolvers/launch.ts +++ b/src/client/debugger/extension/configuration/resolvers/launch.ts @@ -7,57 +7,109 @@ import { inject, injectable, named } from 'inversify'; import { CancellationToken, Uri, WorkspaceFolder } from 'vscode'; import { InvalidPythonPathInDebuggerServiceId } from '../../../../application/diagnostics/checks/invalidPythonPathInDebugger'; import { IDiagnosticsService, IInvalidPythonPathInDebuggerService } from '../../../../application/diagnostics/types'; -import { IDocumentManager, IWorkspaceService } from '../../../../common/application/types'; -import { IPlatformService } from '../../../../common/platform/types'; import { IConfigurationService } from '../../../../common/types'; +import { getOSType, OSType } from '../../../../common/utils/platform'; +import { EnvironmentVariables } from '../../../../common/variables/types'; +import { IEnvironmentActivationService } from '../../../../interpreter/activation/types'; +import { IInterpreterService } from '../../../../interpreter/contracts'; import { DebuggerTypeName } from '../../../constants'; import { DebugOptions, LaunchRequestArguments } from '../../../types'; -import { 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 { + private isCustomPythonSet = false; + 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 + @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(workspaceService, documentManager, configurationService); + super(configurationService, interpreterService); } - public async resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments, _token?: CancellationToken): Promise { - const workspaceFolder = this.getWorkspaceFolder(folder); - - const config = debugConfiguration as LaunchRequestArguments; - const numberOfSettings = Object.keys(config); - if ((config.noDebug === true && numberOfSettings.length === 1) || numberOfSettings.length === 0) { - const defaultProgram = this.getProgram(); + public async resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken, + ): Promise { + 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; + } + + public async resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + _token?: CancellationToken, + ): Promise { + const workspaceFolder = LaunchConfigurationResolver.getWorkspaceFolder(folder); + await this.provideLaunchDefaults(workspaceFolder, debugConfiguration); - await this.provideLaunchDefaults(workspaceFolder, config); - const isValid = await this.validateLaunchConfiguration(folder, config); + const isValid = await this.validateLaunchConfiguration(folder, debugConfiguration); if (!isValid) { - return; + return undefined; } - const dbgConfig = debugConfiguration; - if (Array.isArray(dbgConfig.debugOptions)) { - dbgConfig.debugOptions = dbgConfig.debugOptions!.filter((item, pos) => dbgConfig.debugOptions!.indexOf(item) === pos); + if (Array.isArray(debugConfiguration.debugOptions)) { + debugConfiguration.debugOptions = debugConfiguration.debugOptions!.filter( + (item, pos) => debugConfiguration.debugOptions!.indexOf(item) === pos, + ); } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.Workspace, workspaceFolder); return debugConfiguration; } - // tslint:disable-next-line:cyclomatic-complexity - protected async provideLaunchDefaults(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): Promise { - this.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + + protected async provideLaunchDefaults( + workspaceFolder: Uri | undefined, + debugConfiguration: LaunchRequestArguments, + ): Promise { + 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; } @@ -65,6 +117,20 @@ export class LaunchConfigurationResolver extends BaseConfigurationResolver 0) { + pathMappings = LaunchConfigurationResolver.fixUpPathMappings( + pathMappings || [], + workspaceFolder ? workspaceFolder.fsPath : '', + ); + } + debugConfiguration.pathMappings = pathMappings.length > 0 ? pathMappings : undefined; } - this.sendTelemetry( - debugConfiguration.request as 'launch' | 'test', - debugConfiguration - ); } - protected async validateLaunchConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: LaunchRequestArguments): Promise { + protected async validateLaunchConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: LaunchRequestArguments, + ): Promise { const diagnosticService = this.invalidPythonPathInDebuggerService; - return diagnosticService.validatePythonPath(debugConfiguration.pythonPath, this.pythonPathSource, folder ? folder.uri : undefined); + for (const executable of [ + debugConfiguration.python, + debugConfiguration.debugAdapterPython, + debugConfiguration.debugLauncherPython, + ]) { + if (!(await diagnosticService.validatePythonPath(executable, this.pythonPathSource, folder?.uri))) { + return false; + } + } + return true; } } diff --git a/src/client/debugger/extension/configuration/types.ts b/src/client/debugger/extension/configuration/types.ts index 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; -} +import { CancellationToken, DebugConfiguration, WorkspaceFolder } from 'vscode'; export const IDebugConfigurationResolver = Symbol('IDebugConfigurationResolver'); export interface IDebugConfigurationResolver { - resolveDebugConfiguration(folder: WorkspaceFolder | undefined, debugConfiguration: T, token?: CancellationToken): Promise; -} + resolveDebugConfiguration( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken, + ): Promise; -export const IDebugConfigurationProviderFactory = Symbol('IDebugConfigurationProviderFactory'); -export interface IDebugConfigurationProviderFactory { - create(configurationType: DebugConfigurationType): IDebugConfigurationProvider; + resolveDebugConfigurationWithSubstitutedVariables( + folder: WorkspaceFolder | undefined, + debugConfiguration: T, + token?: CancellationToken, + ): Promise; } diff --git a/src/client/debugger/extension/configuration/utils/common.ts b/src/client/debugger/extension/configuration/utils/common.ts new file mode 100644 index 000000000000..3643a0c49c5d --- /dev/null +++ b/src/client/debugger/extension/configuration/utils/common.ts @@ -0,0 +1,43 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import { WorkspaceFolder } from 'vscode'; +import { getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; + +/** + * @returns whether the provided parameter is a JavaScript String or not. + */ +function isString(str: any): str is string { + if (typeof str === 'string' || str instanceof String) { + return true; + } + + return false; +} + +export function resolveVariables( + value: string | undefined, + rootFolder: string | undefined, + folder: WorkspaceFolder | undefined, +): string | undefined { + if (value) { + const workspaceFolder = folder ? getWorkspaceFolder(folder.uri) : undefined; + const variablesObject: { [key: string]: any } = {}; + variablesObject.workspaceFolder = workspaceFolder ? workspaceFolder.uri.fsPath : rootFolder; + + const regexp = /\$\{(.*?)\}/g; + return value.replace(regexp, (match: string, name: string) => { + const newValue = variablesObject[name]; + if (isString(newValue)) { + return newValue; + } + return match && (match.indexOf('env.') > 0 || match.indexOf('env:') > 0) ? '' : match; + }); + } + return value; +} diff --git a/src/client/debugger/extension/debugCommands.ts b/src/client/debugger/extension/debugCommands.ts new file mode 100644 index 000000000000..629f8616a6d6 --- /dev/null +++ b/src/client/debugger/extension/debugCommands.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { DebugConfiguration, Uri } from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IDebugService } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { DebugPurpose, LaunchRequestArguments } from '../types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { noop } from '../../common/utils/misc'; +import { getConfigurationsByUri } from './configuration/launch.json/launchJsonReader'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheckNonBlocking, +} from '../../pythonEnvironments/creation/createEnvironmentTrigger'; + +@injectable() +export class DebugCommands implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDebugService) private readonly debugService: IDebugService, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} + + public activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.Debug_In_Terminal, async (file?: Uri) => { + const interpreter = await this.interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'debug-in-terminal' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + const config = await DebugCommands.getDebugConfiguration(file); + this.debugService.startDebugging(undefined, config); + }), + ); + return Promise.resolve(); + } + + private static async getDebugConfiguration(uri?: Uri): Promise { + const configs = (await getConfigurationsByUri(uri)).filter((c) => c.request === 'launch'); + for (const config of configs) { + if ((config as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugInTerminal)) { + if (!config.program && !config.module && !config.code) { + // This is only needed if people reuse debug-test for debug-in-terminal + config.program = uri?.fsPath ?? '${file}'; + } + // Ensure that the purpose is cleared, this is so we can track if people accidentally + // trigger this via F5 or Start with debugger. + config.purpose = []; + return config; + } + } + return { + name: `Debug ${uri ? path.basename(uri.fsPath) : 'File'}`, + type: 'python', + request: 'launch', + program: uri?.fsPath ?? '${file}', + console: 'integratedTerminal', + }; + } +} diff --git a/src/client/debugger/extension/hooks/childProcessAttachHandler.ts b/src/client/debugger/extension/hooks/childProcessAttachHandler.ts index bbf508ac4559..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 { - 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, event.session); + + 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 baadce796a15..39556f94c87c 100644 --- a/src/client/debugger/extension/hooks/childProcessAttachService.ts +++ b/src/client/debugger/extension/hooks/childProcessAttachService.ts @@ -4,79 +4,47 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { DebugConfiguration, DebugSession, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../../common/application/types'; +import { IDebugService } from '../../../common/application/types'; +import { DebugConfiguration, DebugSession, l10n, WorkspaceFolder, DebugSessionOptions } from 'vscode'; import { noop } from '../../../common/utils/misc'; -import { SystemVariables } from '../../../common/variables/systemVariables'; -import { captureTelemetry } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; -import { AttachRequestArguments, LaunchRequestArguments } from '../../types'; -import { ChildProcessLaunchData, IChildProcessAttachService } from './types'; +import { AttachRequestArguments } from '../../types'; +import { IChildProcessAttachService } from './types'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; /** * This class is responsible for attaching the debugger to any - * child processes launched. I.e. this is the 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(EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS) - public async attach(data: ChildProcessLaunchData, parentSession: DebugSession): Promise { - const folder = this.getRelatedWorkspaceFolder(data); - const debugConfig = this.getAttachConfiguration(data); - const launched = await this.debugService.startDebugging(folder, debugConfig, parentSession); + public async attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise { + 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, + ); } } - public getRelatedWorkspaceFolder(data: ChildProcessLaunchData): WorkspaceFolder | undefined { - const workspaceFolder = data.rootStartRequest.arguments.workspaceFolder; - if (!this.workspaceService.hasWorkspaceFolders || !workspaceFolder) { - return; - } - return this.workspaceService.workspaceFolders!.find(ws => ws.uri.fsPath === workspaceFolder); - } - public 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); - // tslint:disable-next-line: no-any - this.fixPathMappings(config as any); - config.host = args.request === 'attach' ? args.host! : 'localhost'; - config.port = data.port; - config.name = `Child Process ${data.processId}`; - config.request = 'attach'; - return config; - } - /** - * Since we're attaching we need to provide path mappings. - * If not provided, we cannot add breakpoints as we don't have mappings to the actual source. - * This is because attach automatically assumes remote debugging. - * Also remember, this code gets executed only when dynamically attaching to child processes. - * Resolves https://github.com/microsoft/vscode-python/issues/3568 - */ - public fixPathMappings(config: LaunchRequestArguments & AttachRequestArguments & DebugConfiguration) { - if (!config.workspaceFolder) { - return; - } - if (Array.isArray(config.pathMappings) && config.pathMappings.length > 0) { + + private getRelatedWorkspaceFolder( + config: AttachRequestArguments & DebugConfiguration, + ): WorkspaceFolder | undefined { + const workspaceFolder = config.workspaceFolder; + + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; + if (!hasWorkspaceFolders || !workspaceFolder) { return; } - // If user has provided a `cwd` in their `launch.json`, then we need to use - // the `cwd` as the localRoot. - // We cannot expect the debugger to assume remote root is the same as the cwd, - // As debugger doesn't necessarily know whether the process being attached to is - // a child process or not. - const systemVariables = new SystemVariables(config.workspaceFolder); - const localRoot = config.cwd && config.cwd.length > 0 ? systemVariables.resolveAny(config.cwd) : config.workspaceFolder; - config.pathMappings = [ - { remoteRoot: '.', localRoot } - ]; + 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 427e4ebd69c5..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; } -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, parentSession: DebugSession): Promise; + attach(data: AttachRequestArguments & DebugConfiguration, parentSession: DebugSession): Promise; } diff --git a/src/client/debugger/extension/serviceRegistry.ts b/src/client/debugger/extension/serviceRegistry.ts index 88c6206056a4..7734e87124cd 100644 --- a/src/client/debugger/extension/serviceRegistry.ts +++ b/src/client/debugger/extension/serviceRegistry.ts @@ -3,44 +3,68 @@ 'use strict'; -import { IExtensionActivationService } from '../../activation/types'; +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 { LaunchJsonCompletionProvider } from './configuration/launch.json/completionProvider'; -import { LaunchJsonUpdaterService } from './configuration/launch.json/updaterService'; -import { DjangoLaunchDebugConfigurationProvider } from './configuration/providers/djangoLaunch'; -import { FileLaunchDebugConfigurationProvider } from './configuration/providers/fileLaunch'; -import { FlaskLaunchDebugConfigurationProvider } from './configuration/providers/flaskLaunch'; -import { ModuleLaunchDebugConfigurationProvider } from './configuration/providers/moduleLaunch'; -import { 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(IExtensionActivationService, LaunchJsonCompletionProvider); - serviceManager.addSingleton(IExtensionActivationService, LaunchJsonUpdaterService); - serviceManager.addSingleton(IDebugConfigurationService, PythonDebugConfigurationService); - serviceManager.addSingleton(IConfigurationProviderUtils, ConfigurationProviderUtils); - serviceManager.addSingleton(IDebuggerBanner, DebuggerBanner); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton( + IDebugConfigurationService, + PythonDebugConfigurationService, + ); serviceManager.addSingleton(IChildProcessAttachService, ChildProcessAttachService); serviceManager.addSingleton(IDebugSessionEventHandlers, ChildProcessAttachEventHandler); - serviceManager.addSingleton>(IDebugConfigurationResolver, LaunchConfigurationResolver, 'launch'); - serviceManager.addSingleton>(IDebugConfigurationResolver, AttachConfigurationResolver, 'attach'); - serviceManager.addSingleton(IDebugConfigurationProviderFactory, DebugConfigurationProviderFactory); - serviceManager.addSingleton(IDebugConfigurationProvider, FileLaunchDebugConfigurationProvider, DebugConfigurationType.launchFile); - serviceManager.addSingleton(IDebugConfigurationProvider, DjangoLaunchDebugConfigurationProvider, DebugConfigurationType.launchDjango); - serviceManager.addSingleton(IDebugConfigurationProvider, FlaskLaunchDebugConfigurationProvider, DebugConfigurationType.launchFlask); - serviceManager.addSingleton(IDebugConfigurationProvider, RemoteAttachDebugConfigurationProvider, DebugConfigurationType.remoteAttach); - serviceManager.addSingleton(IDebugConfigurationProvider, ModuleLaunchDebugConfigurationProvider, DebugConfigurationType.launchModule); - serviceManager.addSingleton(IDebugConfigurationProvider, PyramidLaunchDebugConfigurationProvider, DebugConfigurationType.launchPyramid); + serviceManager.addSingleton>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ); + serviceManager.addSingleton>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach', + ); + serviceManager.addSingleton( + IDebugEnvironmentVariablesService, + DebugEnvironmentVariablesHelper, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + DebugAdapterActivator, + ); + serviceManager.addSingleton( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ); + serviceManager.addSingleton(IDebugSessionLoggingFactory, DebugSessionLoggingFactory); + serviceManager.addSingleton( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory, + ); + serviceManager.addSingleton( + IAttachProcessProviderFactory, + AttachProcessProviderFactory, + ); + serviceManager.addSingleton(IExtensionSingleActivationService, DebugCommands); } diff --git a/src/client/debugger/extension/types.ts b/src/client/debugger/extension/types.ts index 3def3b637ee9..4a8f35e2b808 100644 --- a/src/client/debugger/extension/types.ts +++ b/src/client/debugger/extension/types.ts @@ -3,34 +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; folder?: WorkspaceFolder; token?: CancellationToken }; -export interface IDebugConfigurationProvider { - buildConfiguration(input: MultiStepInput, state: DebugConfigurationState): Promise | void>; -} +export const IDebugAdapterDescriptorFactory = Symbol('IDebugAdapterDescriptorFactory'); +export interface IDebugAdapterDescriptorFactory extends DebugAdapterDescriptorFactory {} -export enum DebugConfigurationType { - default = 'default', - launchFile = 'launchFile', - remoteAttach = 'remoteAttach', - launchDjango = 'launchDjango', - launchFlask = 'launchFlask', - launchModule = 'launchModule', - launchPyramid = 'launchPyramid' -} +export const IDebugSessionLoggingFactory = Symbol('IDebugSessionLoggingFactory'); + +export interface IDebugSessionLoggingFactory extends DebugAdapterTrackerFactory {} + +export const IOutdatedDebuggerPromptFactory = Symbol('IOutdatedDebuggerPromptFactory'); + +export interface IOutdatedDebuggerPromptFactory extends DebugAdapterTrackerFactory {} export enum PythonPathSource { launchJson = 'launch.json', - settingsJson = 'settings.json' + settingsJson = 'settings.json', } diff --git a/src/client/debugger/pythonDebugger.ts b/src/client/debugger/pythonDebugger.ts new file mode 100644 index 000000000000..3450e95f3cee --- /dev/null +++ b/src/client/debugger/pythonDebugger.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { extensions } from 'vscode'; + +interface IPythonDebuggerExtensionApi { + debug: { + getDebuggerPackagePath(): Promise; + }; +} + +async function activateExtension() { + const extension = extensions.getExtension('ms-python.debugpy'); + if (extension) { + if (!extension.isActive) { + await extension.activate(); + } + } + return extension; +} + +async function getPythonDebuggerExtensionAPI(): Promise { + const extension = await activateExtension(); + return extension?.exports as IPythonDebuggerExtensionApi; +} + +export async function getDebugpyPath(): Promise { + const api = await getPythonDebuggerExtensionAPI(); + return api?.debug.getDebuggerPackagePath() ?? ''; +} diff --git a/src/client/debugger/types.ts b/src/client/debugger/types.ts index 48beb568c3a2..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 { @@ -36,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?: Record; - envFile: string; + 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 "; + // 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 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(); +/** + * @deprecated Will be removed soon. + */ +export function reportInterpretersChanged(e: EnvironmentsChangedParams[]): void { + onDidInterpretersChangedEvent.fire(e); +} + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +/** + * @deprecated Will be removed soon. + */ +export function reportActiveInterpreterChangedDeprecated(e: ActiveEnvironmentChangedParams): void { + onDidActiveInterpreterChangedEvent.fire(e); +} + +function getVersionString(env: PythonEnvInfo): string[] { + const ver = [`${env.version.major}`, `${env.version.minor}`, `${env.version.micro}`]; + if (env.version.release) { + ver.push(`${env.version.release}`); + if (env.version.sysVersion) { + ver.push(`${env.version.release}`); + } + } + return ver; +} + +/** + * Returns whether the path provided matches the environment. + * @param path Path to environment folder or path to interpreter that uniquely identifies an environment. + * @param env Environment to match with. + */ +function isEnvSame(path: string, env: PythonEnvInfo) { + return arePathsSame(path, env.location) || arePathsSame(path, env.executable.filename); +} + +export function buildDeprecatedProposedApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, +): DeprecatedProposedAPI { + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const interpreterService = serviceContainer.get(IInterpreterService); + const extensions = serviceContainer.get(IExtensions); + const warningLogged = new Set(); + function sendApiTelemetry(apiName: string, warnLog = true) { + extensions + .determineExtensionFromCallStack() + .then((info) => { + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + traceVerbose(`Extension ${info.extensionId} accessed ${apiName}`); + if (warnLog && !warningLogged.has(info.extensionId)) { + traceWarn( + `${info.extensionId} extension is using deprecated python APIs which will be removed soon.`, + ); + warningLogged.add(info.extensionId); + } + }) + .ignoreErrors(); + } + + const proposed: DeprecatedProposedAPI = { + environment: { + async getExecutionDetails(resource?: Resource) { + sendApiTelemetry('deprecated.getExecutionDetails'); + const env = await interpreterService.getActiveInterpreter(resource); + return env ? { execCommand: [env.path] } : { execCommand: undefined }; + }, + async getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('deprecated.getActiveEnvironmentPath'); + const env = await interpreterService.getActiveInterpreter(resource); + if (!env) { + return undefined; + } + return getEnvPath(env.path, env.envPath); + }, + async getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise { + sendApiTelemetry('deprecated.getEnvironmentDetails'); + let env: PythonEnvInfo | undefined; + if (options?.useCache) { + env = discoveryApi.getEnvs().find((v) => isEnvSame(path, v)); + } + if (!env) { + env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + } + return { + interpreterPath: env.executable.filename, + envFolderPath: env.location.length ? env.location : undefined, + version: getVersionString(env), + environmentType: [env.kind], + metadata: { + sysPrefix: env.executable.sysPrefix, + bitness: env.arch, + project: env.searchLocation, + }, + }; + }, + getEnvironmentPaths() { + sendApiTelemetry('deprecated.getEnvironmentPaths'); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + setActiveEnvironment(path: string, resource?: Resource): Promise { + sendApiTelemetry('deprecated.setActiveEnvironment'); + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + async refreshEnvironment() { + sendApiTelemetry('deprecated.refreshEnvironment'); + await discoveryApi.triggerRefresh(); + const paths = discoveryApi.getEnvs().map((e) => getEnvPath(e.executable.filename, e.location)); + return Promise.resolve(paths); + }, + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + sendApiTelemetry('deprecated.getRefreshPromise'); + return discoveryApi.getRefreshPromise(options); + }, + get onDidChangeExecutionDetails() { + sendApiTelemetry('deprecated.onDidChangeExecutionDetails', false); + return interpreterService.onDidChangeInterpreterConfiguration; + }, + get onDidEnvironmentsChanged() { + sendApiTelemetry('deprecated.onDidEnvironmentsChanged', false); + return onDidInterpretersChangedEvent.event; + }, + get onDidActiveEnvironmentChanged() { + sendApiTelemetry('deprecated.onDidActiveEnvironmentChanged', false); + return onDidActiveInterpreterChangedEvent.event; + }, + get onRefreshProgress() { + sendApiTelemetry('deprecated.onRefreshProgress', false); + return discoveryApi.onProgress; + }, + }, + }; + return proposed; +} diff --git a/src/client/deprecatedProposedApiTypes.ts b/src/client/deprecatedProposedApiTypes.ts new file mode 100644 index 000000000000..eb76d61dc907 --- /dev/null +++ b/src/client/deprecatedProposedApiTypes.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, Event } from 'vscode'; +import { PythonEnvKind, EnvPathType } from './pythonEnvironments/base/info'; +import { ProgressNotificationEvent, GetRefreshEnvironmentsOptions } from './pythonEnvironments/base/locator'; +import { Resource } from './api/types'; + +export interface EnvironmentDetailsOptions { + useCache: boolean; +} + +export interface EnvironmentDetails { + interpreterPath: string; + envFolderPath?: string; + version: string[]; + environmentType: PythonEnvKind[]; + metadata: Record; +} + +export interface EnvironmentsChangedParams { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path?: string; + type: 'add' | 'remove' | 'update' | 'clear-all'; +} + +export interface ActiveEnvironmentChangedParams { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path: string; + resource?: Uri; +} + +/** + * @deprecated Use {@link ProposedExtensionAPI} instead. + */ +export interface DeprecatedProposedAPI { + /** + * @deprecated Use {@link ProposedExtensionAPI.environments} instead. This will soon be removed. + */ + environment: { + /** + * An event that is emitted when execution details (for a resource) change. For instance, when interpreter configuration changes. + */ + readonly onDidChangeExecutionDetails: Event; + /** + * Returns all the details the consumer needs to execute code within the selected environment, + * corresponding to the specified resource taking into account any workspace-specific settings + * for the workspace to which this resource belongs. + * @param {Resource} [resource] A resource for which the setting is asked for. + * * When no resource is provided, the setting scoped to the first workspace folder is returned. + * * If no folder is present, it returns the global setting. + */ + getExecutionDetails( + resource?: Resource, + ): Promise<{ + /** + * E.g of execution commands returned could be, + * * `['']` + * * `['']` + * * `['conda', 'run', 'python']` which is used to run from within Conda environments. + * or something similar for some other Python environments. + * + * @type {(string[] | undefined)} When return value is `undefined`, it means no interpreter is set. + * Otherwise, join the items returned using space to construct the full execution command. + */ + execCommand: string[] | undefined; + }>; + /** + * Returns the path to the python binary selected by the user or as in the settings. + * This is just the path to the python binary, this does not provide activation or any + * other activation command. The `resource` if provided will be used to determine the + * python binary in a multi-root scenario. If resource is `undefined` then the API + * returns what ever is set for the workspace. + * @param resource : Uri of a file or workspace + */ + getActiveEnvironmentPath(resource?: Resource): Promise; + /** + * Returns details for the given interpreter. Details such as absolute interpreter path, + * version, type (conda, pyenv, etc). Metadata such as `sysPrefix` can be found under + * metadata field. + * @param path : Full path to environment folder or interpreter whose details you need. + * @param options : [optional] + * * useCache : When true, cache is checked first for any data, returns even if there + * is partial data. + */ + getEnvironmentDetails( + path: string, + options?: EnvironmentDetailsOptions, + ): Promise; + /** + * Returns paths to environments that uniquely identifies an environment found by the extension + * at the time of calling. This API will *not* trigger a refresh. If a refresh is going on it + * will *not* wait for the refresh to finish. This will return what is known so far. To get + * complete list `await` on promise returned by `getRefreshPromise()`. + * + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + getEnvironmentPaths(): Promise; + /** + * Sets the active environment path for the python extension for the resource. Configuration target + * will always be the workspace folder. + * @param path : Full path to environment folder or interpreter to set. + * @param resource : [optional] Uri of a file ro workspace to scope to a particular workspace + * folder. + */ + setActiveEnvironment(path: string, resource?: Resource): Promise; + /** + * This API will re-trigger environment discovery. Extensions can wait on the returned + * promise to get the updated environment list. If there is a refresh already going on + * then it returns the promise for that refresh. + * @param options : [optional] + * * clearCache : When true, this will clear the cache before environment refresh + * is triggered. + */ + refreshEnvironment(): Promise; + /** + * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant + * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of + * the entire collection. + */ + readonly onRefreshProgress: Event; + /** + * Returns a promise for the ongoing refresh. Returns `undefined` if there are no active + * refreshes going on. + */ + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + /** + * This event is triggered when the known environment list changes, like when a environment + * is found, existing environment is removed, or some details changed on an environment. + */ + onDidEnvironmentsChanged: Event; + /** + * @deprecated Use {@link ProposedExtensionAPI.environments} `onDidChangeActiveEnvironmentPath` instead. This will soon be removed. + */ + onDidActiveEnvironmentChanged: Event; + }; +} diff --git a/src/client/envExt/api.internal.ts b/src/client/envExt/api.internal.ts new file mode 100644 index 000000000000..5edfb712072e --- /dev/null +++ b/src/client/envExt/api.internal.ts @@ -0,0 +1,179 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { EventEmitter, Terminal, Uri, Disposable } from 'vscode'; +import { getExtension } from '../common/vscodeApis/extensionsApi'; +import { + GetEnvironmentScope, + PythonBackgroundRunOptions, + PythonEnvironment, + PythonEnvironmentApi, + PythonProcess, + RefreshEnvironmentsScope, + DidChangeEnvironmentEventArgs, +} from './types'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { getConfiguration, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; +import { traceError, traceLog } from '../logging'; +import { Interpreters } from '../common/utils/localize'; + +export const ENVS_EXTENSION_ID = 'ms-python.vscode-python-envs'; + +export function isEnvExtensionInstalled(): boolean { + return !!getExtension(ENVS_EXTENSION_ID); +} + +/** + * Returns true if the Python Environments extension is installed and not explicitly + * disabled by the user. Mirrors the envs extension's own activation logic: it + * deactivates only when `python.useEnvironmentsExtension` is explicitly set to false + * at the global, workspace, or workspace-folder level. + */ +export function shouldEnvExtHandleActivation(): boolean { + if (!isEnvExtensionInstalled()) { + return false; + } + const config = getConfiguration('python'); + const inspection = config.inspect('useEnvironmentsExtension'); + if (inspection?.globalValue === false || inspection?.workspaceValue === false) { + return false; + } + // The envs extension also checks folder-scoped settings in multi-root workspaces. + // Any single folder with the setting set to false causes the envs extension to + // deactivate entirely (window-wide), so we must mirror that here. + const workspaceFolders = getWorkspaceFolders(); + if (workspaceFolders) { + for (const folder of workspaceFolders) { + const folderConfig = getConfiguration('python', folder.uri); + const folderInspection = folderConfig.inspect('useEnvironmentsExtension'); + if (folderInspection?.workspaceFolderValue === false) { + return false; + } + } + } + return true; +} + +let _useExt: boolean | undefined; +export function useEnvExtension(): boolean { + if (_useExt !== undefined) { + return _useExt; + } + const config = getConfiguration('python'); + const inExpSetting = config?.get('useEnvironmentsExtension', false) ?? false; + // If extension is installed and in experiment, then use it. + _useExt = !!getExtension(ENVS_EXTENSION_ID) && inExpSetting; + return _useExt; +} + +const onDidChangeEnvironmentEnvExtEmitter: EventEmitter = new EventEmitter< + DidChangeEnvironmentEventArgs +>(); +export function onDidChangeEnvironmentEnvExt( + listener: (e: DidChangeEnvironmentEventArgs) => unknown, + thisArgs?: unknown, + disposables?: Disposable[], +): Disposable { + return onDidChangeEnvironmentEnvExtEmitter.event(listener, thisArgs, disposables); +} + +let _extApi: PythonEnvironmentApi | undefined; +export async function getEnvExtApi(): Promise { + if (_extApi) { + return _extApi; + } + const extension = getExtension(ENVS_EXTENSION_ID); + if (!extension) { + traceError(Interpreters.envExtActivationFailed); + throw new Error('Python Environments extension not found.'); + } + if (!extension?.isActive) { + try { + await extension.activate(); + } catch (ex) { + traceError(Interpreters.envExtActivationFailed, ex); + throw ex; + } + } + + traceLog(Interpreters.envExtDiscoveryAttribution); + + _extApi = extension.exports as PythonEnvironmentApi; + _extApi.onDidChangeEnvironment((e) => { + onDidChangeEnvironmentEnvExtEmitter.fire(e); + }); + + return _extApi; +} + +export async function runInBackground( + environment: PythonEnvironment, + options: PythonBackgroundRunOptions, +): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.runInBackground(environment, options); +} + +export async function getEnvironment(scope: GetEnvironmentScope): Promise { + const envExtApi = await getEnvExtApi(); + const env = await envExtApi.getEnvironment(scope); + if (!env) { + traceLog(Interpreters.envExtNoActiveEnvironment); + } + return env; +} + +export async function resolveEnvironment(pythonPath: string): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.resolveEnvironment(Uri.file(pythonPath)); +} + +export async function refreshEnvironments(scope: RefreshEnvironmentsScope): Promise { + const envExtApi = await getEnvExtApi(); + return envExtApi.refreshEnvironments(scope); +} + +export async function runInTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise { + const envExtApi = await getEnvExtApi(); + const env = await getEnvironment(resource); + const project = resource ? envExtApi.getPythonProject(resource) : undefined; + if (env && resource) { + return envExtApi.runInTerminal(env, { + cwd: cwd ?? project?.uri ?? process.cwd(), + args, + show, + }); + } + throw new Error('Invalid arguments to run in terminal'); +} + +export async function runInDedicatedTerminal( + resource: Uri | undefined, + args?: string[], + cwd?: string | Uri, + show?: boolean, +): Promise { + const envExtApi = await getEnvExtApi(); + const env = await getEnvironment(resource); + const project = resource ? envExtApi.getPythonProject(resource) : undefined; + if (env) { + return envExtApi.runInDedicatedTerminal(resource ?? 'global', env, { + cwd: cwd ?? project?.uri ?? process.cwd(), + args, + show, + }); + } + throw new Error('Invalid arguments to run in dedicated terminal'); +} + +export async function clearCache(): Promise { + const envExtApi = await getEnvExtApi(); + if (envExtApi) { + await executeCommand('python-envs.clearCache'); + } +} diff --git a/src/client/envExt/api.legacy.ts b/src/client/envExt/api.legacy.ts new file mode 100644 index 000000000000..6f2e60774033 --- /dev/null +++ b/src/client/envExt/api.legacy.ts @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Terminal, Uri } from 'vscode'; +import { getEnvExtApi, getEnvironment } from './api.internal'; +import { EnvironmentType, PythonEnvironment as PythonEnvironmentLegacy } from '../pythonEnvironments/info'; +import { PythonEnvironment, PythonTerminalCreateOptions } from './types'; +import { Architecture } from '../common/utils/platform'; +import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { PythonEnvType } from '../pythonEnvironments/base/info'; +import { traceError } from '../logging'; +import { reportActiveInterpreterChanged } from '../environmentApi'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +function toEnvironmentType(pythonEnv: PythonEnvironment): EnvironmentType { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { + return EnvironmentType.System; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) { + return EnvironmentType.Venv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) { + return EnvironmentType.VirtualEnv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return EnvironmentType.Conda; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) { + return EnvironmentType.Pipenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) { + return EnvironmentType.Poetry; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) { + return EnvironmentType.Pyenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) { + return EnvironmentType.Hatch; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) { + return EnvironmentType.Pixi; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) { + return EnvironmentType.VirtualEnvWrapper; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) { + return EnvironmentType.ActiveState; + } + return EnvironmentType.Unknown; +} + +function getEnvType(kind: EnvironmentType): PythonEnvType | undefined { + switch (kind) { + case EnvironmentType.Pipenv: + case EnvironmentType.VirtualEnv: + case EnvironmentType.Pyenv: + case EnvironmentType.Venv: + case EnvironmentType.Poetry: + case EnvironmentType.Hatch: + case EnvironmentType.Pixi: + case EnvironmentType.VirtualEnvWrapper: + case EnvironmentType.ActiveState: + return PythonEnvType.Virtual; + + case EnvironmentType.Conda: + return PythonEnvType.Conda; + + case EnvironmentType.MicrosoftStore: + case EnvironmentType.Global: + case EnvironmentType.System: + default: + return undefined; + } +} + +function toLegacyType(env: PythonEnvironment): PythonEnvironmentLegacy { + const ver = parseVersion(env.version); + const envType = toEnvironmentType(env); + return { + id: env.execInfo.run.executable, + displayName: env.displayName, + detailedDisplayName: env.name, + envType, + envPath: env.sysPrefix, + type: getEnvType(envType), + path: env.execInfo.run.executable, + version: { + raw: env.version, + major: ver.major, + minor: ver.minor, + patch: ver.micro, + build: [], + prerelease: [], + }, + sysVersion: env.version, + architecture: Architecture.x64, + sysPrefix: env.sysPrefix, + }; +} + +const previousEnvMap = new Map(); +export async function getActiveInterpreterLegacy(resource?: Uri): Promise { + const api = await getEnvExtApi(); + const uri = resource ? api.getPythonProject(resource)?.uri : undefined; + + const pythonEnv = await getEnvironment(resource); + const oldEnv = previousEnvMap.get(uri?.fsPath || ''); + const newEnv = pythonEnv ? toLegacyType(pythonEnv) : undefined; + + const folders = getWorkspaceFolders() ?? []; + const shouldReport = + (folders.length === 0 && resource === undefined) || (folders.length > 0 && resource !== undefined); + if (shouldReport && newEnv && oldEnv?.envId.id !== pythonEnv?.envId.id) { + reportActiveInterpreterChanged({ + resource: getWorkspaceFolder(resource), + path: newEnv.path, + }); + previousEnvMap.set(uri?.fsPath || '', pythonEnv); + } + return pythonEnv ? toLegacyType(pythonEnv) : undefined; +} + +export async function setInterpreterLegacy(pythonPath: string, uri: Uri | undefined): Promise { + const api = await getEnvExtApi(); + const pythonEnv = await api.resolveEnvironment(Uri.file(pythonPath)); + if (!pythonEnv) { + traceError(`EnvExt: Failed to resolve environment for ${pythonPath}`); + return; + } + await api.setEnvironment(uri, pythonEnv); +} + +export async function resetInterpreterLegacy(uri: Uri | undefined): Promise { + const api = await getEnvExtApi(); + await api.setEnvironment(uri, undefined); +} + +export async function ensureTerminalLegacy( + resource: Uri | undefined, + options?: PythonTerminalCreateOptions, +): Promise { + const api = await getEnvExtApi(); + const pythonEnv = await api.getEnvironment(resource); + const project = resource ? api.getPythonProject(resource) : undefined; + + if (pythonEnv && project) { + const fixedOptions = options ? { ...options } : { cwd: project.uri }; + const terminal = await api.createTerminal(pythonEnv, fixedOptions); + return terminal; + } + traceError('ensureTerminalLegacy - Did not return terminal successfully.'); + traceError( + 'ensureTerminalLegacy - pythonEnv:', + pythonEnv + ? `id=${pythonEnv.envId.id}, managerId=${pythonEnv.envId.managerId}, name=${pythonEnv.name}, version=${pythonEnv.version}, executable=${pythonEnv.execInfo.run.executable}` + : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - project:', + project ? `name=${project.name}, uri=${project.uri.toString()}` : 'undefined', + ); + traceError( + 'ensureTerminalLegacy - options:', + options + ? `name=${options.name}, cwd=${options.cwd?.toString()}, hideFromUser=${options.hideFromUser}` + : 'undefined', + ); + traceError('ensureTerminalLegacy - resource:', resource?.toString() || 'undefined'); + + throw new Error('Invalid arguments to create terminal'); +} diff --git a/src/client/envExt/envExtApi.ts b/src/client/envExt/envExtApi.ts new file mode 100644 index 000000000000..34f42f0d6954 --- /dev/null +++ b/src/client/envExt/envExtApi.ts @@ -0,0 +1,345 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ + +import * as path from 'path'; +import { Event, EventEmitter, Disposable, Uri } from 'vscode'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, PythonVersion } from '../pythonEnvironments/base/info'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../pythonEnvironments/base/locator'; +import { PythonEnvCollectionChangedEvent } from '../pythonEnvironments/base/watcher'; +import { getEnvExtApi } from './api.internal'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { StopWatch } from '../common/utils/stopWatch'; +import { traceError, traceLog, traceWarn } from '../logging'; +import { + DidChangeEnvironmentsEventArgs, + EnvironmentChangeKind, + PythonEnvironment, + PythonEnvironmentApi, +} from './types'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { Architecture, isWindows } from '../common/utils/platform'; +import { parseVersion } from '../pythonEnvironments/base/info/pythonVersion'; +import { Interpreters } from '../common/utils/localize'; + +function getKind(pythonEnv: PythonEnvironment): PythonEnvKind { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('system')) { + return PythonEnvKind.System; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return PythonEnvKind.Conda; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('venv')) { + return PythonEnvKind.Venv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenv')) { + return PythonEnvKind.VirtualEnv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('virtualenvwrapper')) { + return PythonEnvKind.VirtualEnvWrapper; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pyenv')) { + return PythonEnvKind.Pyenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pipenv')) { + return PythonEnvKind.Pipenv; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('poetry')) { + return PythonEnvKind.Poetry; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('pixi')) { + return PythonEnvKind.Pixi; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('hatch')) { + return PythonEnvKind.Hatch; + } + if (pythonEnv.envId.managerId.toLowerCase().endsWith('activestate')) { + return PythonEnvKind.ActiveState; + } + + return PythonEnvKind.Unknown; +} + +function makeExecutablePath(prefix?: string): string { + if (!prefix) { + return process.platform === 'win32' ? 'python.exe' : 'python'; + } + return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python'); +} + +function getExecutable(pythonEnv: PythonEnvironment): string { + if (pythonEnv.execInfo?.run?.executable) { + return pythonEnv.execInfo?.run?.executable; + } + + const basename = path.basename(pythonEnv.environmentPath.fsPath).toLowerCase(); + if (isWindows() && basename.startsWith('python') && basename.endsWith('.exe')) { + return pythonEnv.environmentPath.fsPath; + } + + if (!isWindows() && basename.startsWith('python')) { + return pythonEnv.environmentPath.fsPath; + } + + return makeExecutablePath(pythonEnv.sysPrefix); +} + +function getLocation(pythonEnv: PythonEnvironment): string { + if (pythonEnv.envId.managerId.toLowerCase().endsWith('conda')) { + return pythonEnv.sysPrefix; + } + + return pythonEnv.environmentPath.fsPath; +} + +function getEnvType(kind: PythonEnvKind): PythonEnvType | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + case PythonEnvKind.Pyenv: + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + case PythonEnvKind.Pipenv: + case PythonEnvKind.ActiveState: + case PythonEnvKind.Hatch: + case PythonEnvKind.Pixi: + return PythonEnvType.Virtual; + + case PythonEnvKind.Conda: + return PythonEnvType.Conda; + + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + case PythonEnvKind.MicrosoftStore: + default: + return undefined; + } +} + +function toPythonEnvInfo(pythonEnv: PythonEnvironment): PythonEnvInfo | undefined { + const kind = getKind(pythonEnv); + const arch = Architecture.x64; + const version: PythonVersion = parseVersion(pythonEnv.version); + const { name, displayName, sysPrefix } = pythonEnv; + const executable = getExecutable(pythonEnv); + const location = getLocation(pythonEnv); + + return { + name, + location, + kind, + id: executable, + executable: { + filename: executable, + sysPrefix, + ctime: -1, + mtime: -1, + }, + version: { + sysVersion: pythonEnv.version, + major: version.major, + minor: version.minor, + micro: version.micro, + }, + arch, + distro: { + org: '', + }, + source: [], + detailedDisplayName: displayName, + display: displayName, + type: getEnvType(kind), + }; +} + +function hasChanged(old: PythonEnvInfo, newEnv: PythonEnvInfo): boolean { + if (old.executable.filename !== newEnv.executable.filename) { + return true; + } + if (old.version.major !== newEnv.version.major) { + return true; + } + if (old.version.minor !== newEnv.version.minor) { + return true; + } + if (old.version.micro !== newEnv.version.micro) { + return true; + } + if (old.location !== newEnv.location) { + return true; + } + if (old.kind !== newEnv.kind) { + return true; + } + if (old.arch !== newEnv.arch) { + return true; + } + + return false; +} + +class EnvExtApis implements IDiscoveryAPI, Disposable { + private _onProgress: EventEmitter; + + private _onChanged: EventEmitter; + + private _refreshPromise?: Deferred; + + private _envs: PythonEnvInfo[] = []; + + refreshState: ProgressReportStage; + + private _disposables: Disposable[] = []; + + constructor(private envExtApi: PythonEnvironmentApi) { + this._onProgress = new EventEmitter(); + this._onChanged = new EventEmitter(); + + this.onProgress = this._onProgress.event; + this.onChanged = this._onChanged.event; + + this.refreshState = ProgressReportStage.idle; + this._disposables.push( + this._onProgress, + this._onChanged, + this.envExtApi.onDidChangeEnvironments((e) => this.onDidChangeEnvironments(e)), + this.envExtApi.onDidChangeEnvironment((e) => { + this._onChanged.fire({ + type: FileChangeType.Changed, + searchLocation: e.uri, + old: e.old ? toPythonEnvInfo(e.old) : undefined, + new: e.new ? toPythonEnvInfo(e.new) : undefined, + }); + }), + ); + } + + onProgress: Event; + + onChanged: Event; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise { + const stopwatch = new StopWatch(); + traceLog('Native locator: Refresh started'); + if (this.refreshState === ProgressReportStage.discoveryStarted && this._refreshPromise?.promise) { + return this._refreshPromise?.promise; + } + + this.refreshState = ProgressReportStage.discoveryStarted; + this._onProgress.fire({ stage: this.refreshState }); + this._refreshPromise = createDeferred(); + + const SLOW_DISCOVERY_THRESHOLD_MS = 25_000; + const slowDiscoveryTimer = setTimeout(() => { + traceWarn(Interpreters.envExtDiscoverySlow); + }, SLOW_DISCOVERY_THRESHOLD_MS); + + setImmediate(async () => { + try { + await this.envExtApi.refreshEnvironments(undefined); + if (this._envs.length === 0) { + traceWarn(Interpreters.envExtDiscoveryNoEnvironments); + } + this._refreshPromise?.resolve(); + } catch (error) { + traceError(Interpreters.envExtDiscoveryFailed, error); + this._refreshPromise?.reject(error); + } finally { + clearTimeout(slowDiscoveryTimer); + traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); + this.refreshState = ProgressReportStage.discoveryFinished; + this._refreshPromise = undefined; + this._onProgress.fire({ stage: this.refreshState }); + } + }); + + return this._refreshPromise?.promise; + } + + getEnvs(_query?: PythonLocatorQuery): PythonEnvInfo[] { + return this._envs; + } + + private addEnv(pythonEnv: PythonEnvironment, searchLocation?: Uri): PythonEnvInfo | undefined { + const info = toPythonEnvInfo(pythonEnv); + if (info) { + const old = this._envs.find((item) => item.executable.filename === info.executable.filename); + if (old) { + this._envs = this._envs.filter((item) => item.executable.filename !== info.executable.filename); + this._envs.push(info); + if (hasChanged(old, info)) { + this._onChanged.fire({ type: FileChangeType.Changed, old, new: info, searchLocation }); + } + } else { + this._envs.push(info); + this._onChanged.fire({ type: FileChangeType.Created, new: info, searchLocation }); + } + } + + return info; + } + + private removeEnv(env: PythonEnvInfo | string): void { + if (typeof env === 'string') { + const old = this._envs.find((item) => item.executable.filename === env); + this._envs = this._envs.filter((item) => item.executable.filename !== env); + this._onChanged.fire({ type: FileChangeType.Deleted, old }); + return; + } + this._envs = this._envs.filter((item) => item.executable.filename !== env.executable.filename); + this._onChanged.fire({ type: FileChangeType.Deleted, old: env }); + } + + async resolveEnv(envPath?: string): Promise { + if (envPath === undefined) { + return undefined; + } + try { + const pythonEnv = await this.envExtApi.resolveEnvironment(Uri.file(envPath)); + if (pythonEnv) { + return this.addEnv(pythonEnv); + } + } catch (error) { + traceError( + `Failed to resolve environment "${envPath}" via the Python Environments extension (ms-python.vscode-python-envs). Check the "Python Environments" output channel for details.`, + error, + ); + } + return undefined; + } + + dispose(): void { + this._disposables.forEach((d) => d.dispose()); + } + + onDidChangeEnvironments(e: DidChangeEnvironmentsEventArgs): void { + e.forEach((item) => { + if (item.kind === EnvironmentChangeKind.remove) { + this.removeEnv(item.environment.environmentPath.fsPath); + } + if (item.kind === EnvironmentChangeKind.add) { + this.addEnv(item.environment); + } + }); + } +} + +export async function createEnvExtApi(disposables: Disposable[]): Promise { + const api = new EnvExtApis(await getEnvExtApi()); + disposables.push(api); + return api; +} diff --git a/src/client/envExt/types.ts b/src/client/envExt/types.ts new file mode 100644 index 000000000000..707d641bbfe8 --- /dev/null +++ b/src/client/envExt/types.ts @@ -0,0 +1,1274 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + Disposable, + Event, + FileChangeType, + LogOutputChannel, + MarkdownString, + TaskExecution, + Terminal, + TerminalOptions, + ThemeIcon, + Uri, +} from 'vscode'; + +/** + * The path to an icon, or a theme-specific configuration of icons. + */ +export type IconPath = + | Uri + | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + } + | ThemeIcon; + +/** + * Options for executing a Python executable. + */ +export interface PythonCommandRunConfiguration { + /** + * Path to the binary like `python.exe` or `python3` to execute. This should be an absolute path + * to an executable that can be spawned. + */ + executable: string; + + /** + * Arguments to pass to the python executable. These arguments will be passed on all execute calls. + * This is intended for cases where you might want to do interpreter specific flags. + */ + args?: string[]; +} + +/** + * Contains details on how to use a particular python environment + * + * Running In Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.activatedRun} is provided, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.activatedRun} is not provided, then: + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * - If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * - If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + * Creating a Terminal: + * 1. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is known, then that will be used. + * 2. If {@link PythonEnvironmentExecutionInfo.shellActivation} is provided and shell type is not known, then {@link PythonEnvironmentExecutionInfo.activation} will be used. + * 3. If {@link PythonEnvironmentExecutionInfo.shellActivation} is not provided, then: + * - 'unknown' will be used if provided. + * - {@link PythonEnvironmentExecutionInfo.activation} will be used otherwise. + * 4. If {@link PythonEnvironmentExecutionInfo.activation} is not provided, then {@link PythonEnvironmentExecutionInfo.run} will be used. + * + */ +export interface PythonEnvironmentExecutionInfo { + /** + * Details on how to run the python executable. + */ + run: PythonCommandRunConfiguration; + + /** + * Details on how to run the python executable after activating the environment. + * If set this will overrides the {@link PythonEnvironmentExecutionInfo.run} command. + */ + activatedRun?: PythonCommandRunConfiguration; + + /** + * Details on how to activate an environment. + */ + activation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to activate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.activation}. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.activation} if set. + */ + shellActivation?: Map; + + /** + * Details on how to deactivate an environment. + */ + deactivation?: PythonCommandRunConfiguration[]; + + /** + * Details on how to deactivate an environment using a shell specific command. + * If set this will override the {@link PythonEnvironmentExecutionInfo.deactivation} property. + * 'unknown' is used if shell type is not known. + * If 'unknown' is not provided and shell type is not known then + * {@link PythonEnvironmentExecutionInfo.deactivation} if set. + */ + shellDeactivation?: Map; +} + +/** + * Interface representing the ID of a Python environment. + */ +export interface PythonEnvironmentId { + /** + * The unique identifier of the Python environment. + */ + id: string; + + /** + * The ID of the manager responsible for the Python environment. + */ + managerId: string; +} + +/** + * Display information for an environment group. + */ +export interface EnvironmentGroupInfo { + /** + * The name of the environment group. This is used as an identifier for the group. + * + * Note: The first instance of the group with the given name will be used in the UI. + */ + readonly name: string; + + /** + * The description of the environment group. + */ + readonly description?: string; + + /** + * The tooltip for the environment group, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the environment group, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Interface representing information about a Python environment. + */ +export interface PythonEnvironmentInfo { + /** + * The name of the Python environment. + */ + readonly name: string; + + /** + * The display name of the Python environment. + */ + readonly displayName: string; + + /** + * The short display name of the Python environment. + */ + readonly shortDisplayName?: string; + + /** + * The display path of the Python environment. + */ + readonly displayPath: string; + + /** + * The version of the Python environment. + */ + readonly version: string; + + /** + * Path to the python binary or environment folder. + */ + readonly environmentPath: Uri; + + /** + * The description of the Python environment. + */ + readonly description?: string; + + /** + * The tooltip for the Python environment, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python environment, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Information on how to execute the Python environment. This is required for executing Python code in the environment. + */ + readonly execInfo: PythonEnvironmentExecutionInfo; + + /** + * `sys.prefix` is the path to the base directory of the Python installation. Typically obtained by executing `sys.prefix` in the Python interpreter. + * This is required by extension like Jupyter, Pylance, and other extensions to provide better experience with python. + */ + readonly sysPrefix: string; + + /** + * Optional `group` for this environment. This is used to group environments in the Environment Manager UI. + */ + readonly group?: string | EnvironmentGroupInfo; +} + +/** + * Interface representing a Python environment. + */ +export interface PythonEnvironment extends PythonEnvironmentInfo { + /** + * The ID of the Python environment. + */ + readonly envId: PythonEnvironmentId; +} + +/** + * Type representing the scope for setting a Python environment. + * Can be undefined or a URI. + */ +export type SetEnvironmentScope = undefined | Uri | Uri[]; + +/** + * Type representing the scope for getting a Python environment. + * Can be undefined or a URI. + */ +export type GetEnvironmentScope = undefined | Uri; + +/** + * Type representing the scope for creating a Python environment. + * Can be a Python project or 'global'. + */ +export type CreateEnvironmentScope = Uri | Uri[] | 'global'; +/** + * The scope for which environments are to be refreshed. + * - `undefined`: Search for environments globally and workspaces. + * - {@link Uri}: Environments in the workspace/folder or associated with the Uri. + */ +export type RefreshEnvironmentsScope = Uri | undefined; + +/** + * The scope for which environments are required. + * - `"all"`: All environments. + * - `"global"`: Python installations that are usually a base for creating virtual environments. + * - {@link Uri}: Environments for the workspace/folder/file pointed to by the Uri. + */ +export type GetEnvironmentsScope = Uri | 'all' | 'global'; + +/** + * Event arguments for when the current Python environment changes. + */ +export type DidChangeEnvironmentEventArgs = { + /** + * The URI of the environment that changed. + */ + readonly uri: Uri | undefined; + + /** + * The old Python environment before the change. + */ + readonly old: PythonEnvironment | undefined; + + /** + * The new Python environment after the change. + */ + readonly new: PythonEnvironment | undefined; +}; + +/** + * Enum representing the kinds of environment changes. + */ +export enum EnvironmentChangeKind { + /** + * Indicates that an environment was added. + */ + add = 'add', + + /** + * Indicates that an environment was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when the list of Python environments changes. + */ +export type DidChangeEnvironmentsEventArgs = { + /** + * The kind of change that occurred (add or remove). + */ + kind: EnvironmentChangeKind; + + /** + * The Python environment that was added or removed. + */ + environment: PythonEnvironment; +}[]; + +/** + * Type representing the context for resolving a Python environment. + */ +export type ResolveEnvironmentContext = Uri; + +export interface QuickCreateConfig { + /** + * The description of the quick create step. + */ + readonly description: string; + + /** + * The detail of the quick create step. + */ + readonly detail?: string; +} + +/** + * Interface representing an environment manager. + */ +export interface EnvironmentManager { + /** + * The name of the environment manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + readonly name: string; + + /** + * The display name of the environment manager. + */ + readonly displayName?: string; + + /** + * The preferred package manager ID for the environment manager. This is a combination + * of publisher id, extension id, and {@link EnvironmentManager.name package manager name}. + * `.:` + * + * @example + * 'ms-python.python:pip' + */ + readonly preferredPackageManagerId: string; + + /** + * The description of the environment manager. + */ + readonly description?: string; + + /** + * The tooltip for the environment manager, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the environment manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The log output channel for the environment manager. + */ + readonly log?: LogOutputChannel; + + /** + * The quick create details for the environment manager. Having this method also enables the quick create feature + * for the environment manager. Should Implement {@link EnvironmentManager.create} to support quick create. + */ + quickCreateConfig?(): QuickCreateConfig | undefined; + + /** + * Creates a new Python environment within the specified scope. + * @param scope - The scope within which to create the environment. + * @param options - Optional parameters for creating the Python environment. + * @returns A promise that resolves to the created Python environment, or undefined if creation failed. + */ + create?(scope: CreateEnvironmentScope, options?: CreateEnvironmentOptions): Promise; + + /** + * Removes the specified Python environment. + * @param environment - The Python environment to remove. + * @returns A promise that resolves when the environment is removed. + */ + remove?(environment: PythonEnvironment): Promise; + + /** + * Refreshes the list of Python environments within the specified scope. + * @param scope - The scope within which to refresh environments. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + */ + onDidChangeEnvironments?: Event; + + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + * @returns A promise that resolves when the environment is set. + */ + set(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + get(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the current Python environment changes. + */ + onDidChangeEnvironment?: Event; + + /** + * Resolves the specified Python environment. The environment can be either a {@link PythonEnvironment} or a {@link Uri} context. + * + * This method is used to obtain a fully detailed {@link PythonEnvironment} object. The input can be: + * - A {@link PythonEnvironment} object, which might be missing key details such as {@link PythonEnvironment.execInfo}. + * - A {@link Uri} object, which typically represents either: + * - A folder that contains the Python environment. + * - The path to a Python executable. + * + * @param context - The context for resolving the environment, which can be a {@link PythonEnvironment} or a {@link Uri}. + * @returns A promise that resolves to the fully detailed {@link PythonEnvironment}, or `undefined` if the environment cannot be resolved. + */ + resolve(context: ResolveEnvironmentContext): Promise; + + /** + * Clears the environment manager's cache. + * + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a package ID. + */ +export interface PackageId { + /** + * The ID of the package. + */ + id: string; + + /** + * The ID of the package manager. + */ + managerId: string; + + /** + * The ID of the environment in which the package is installed. + */ + environmentId: string; +} + +/** + * Interface representing package information. + */ +export interface PackageInfo { + /** + * The name of the package. + */ + readonly name: string; + + /** + * The display name of the package. + */ + readonly displayName: string; + + /** + * The version of the package. + */ + readonly version?: string; + + /** + * The description of the package. + */ + readonly description?: string; + + /** + * The tooltip for the package, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * The URIs associated with the package. + */ + readonly uris?: readonly Uri[]; +} + +/** + * Interface representing a package. + */ +export interface Package extends PackageInfo { + /** + * The ID of the package. + */ + readonly pkgId: PackageId; +} + +/** + * Enum representing the kinds of package changes. + */ +export enum PackageChangeKind { + /** + * Indicates that a package was added. + */ + add = 'add', + + /** + * Indicates that a package was removed. + */ + remove = 'remove', +} + +/** + * Event arguments for when packages change. + */ +export interface DidChangePackagesEventArgs { + /** + * The Python environment in which the packages changed. + */ + environment: PythonEnvironment; + + /** + * The package manager responsible for the changes. + */ + manager: PackageManager; + + /** + * The list of changes, each containing the kind of change and the package affected. + */ + changes: { kind: PackageChangeKind; pkg: Package }[]; +} + +/** + * Interface representing a package manager. + */ +export interface PackageManager { + /** + * The name of the package manager. Allowed characters (a-z, A-Z, 0-9, -, _). + */ + name: string; + + /** + * The display name of the package manager. + */ + displayName?: string; + + /** + * The description of the package manager. + */ + description?: string; + + /** + * The tooltip for the package manager, which can be a string or a Markdown string. + */ + tooltip?: string | MarkdownString | undefined; + + /** + * The icon path for the package manager, which can be a string, Uri, or an object with light and dark theme paths. + */ + iconPath?: IconPath; + + /** + * The log output channel for the package manager. + */ + log?: LogOutputChannel; + + /** + * Installs/Uninstall packages in the specified Python environment. + * @param environment - The Python environment in which to install packages. + * @param options - Options for managing packages. + * @returns A promise that resolves when the installation is complete. + */ + manage(environment: PythonEnvironment, options: PackageManagementOptions): Promise; + + /** + * Refreshes the package list for the specified Python environment. + * @param environment - The Python environment for which to refresh the package list. + * @returns A promise that resolves when the refresh is complete. + */ + refresh(environment: PythonEnvironment): Promise; + + /** + * Retrieves the list of packages for the specified Python environment. + * @param environment - The Python environment for which to retrieve packages. + * @returns An array of packages, or undefined if the packages could not be retrieved. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event that is fired when packages change. + */ + onDidChangePackages?: Event; + + /** + * Clears the package manager's cache. + * @returns A promise that resolves when the cache is cleared. + */ + clearCache?(): Promise; +} + +/** + * Interface representing a Python project. + */ +export interface PythonProject { + /** + * The name of the Python project. + */ + readonly name: string; + + /** + * The URI of the Python project. + */ + readonly uri: Uri; + + /** + * The description of the Python project. + */ + readonly description?: string; + + /** + * The tooltip for the Python project, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python project, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; +} + +/** + * Options for creating a Python project. + */ +export interface PythonProjectCreatorOptions { + /** + * The name of the Python project. + */ + name: string; + + /** + * Path provided as the root for the project. + */ + rootUri: Uri; + + /** + * Boolean indicating whether the project should be created without any user input. + */ + quickCreate?: boolean; +} + +/** + * Interface representing a creator for Python projects. + */ +export interface PythonProjectCreator { + /** + * The name of the Python project creator. + */ + readonly name: string; + + /** + * The display name of the Python project creator. + */ + readonly displayName?: string; + + /** + * The description of the Python project creator. + */ + readonly description?: string; + + /** + * The tooltip for the Python project creator, which can be a string or a Markdown string. + */ + readonly tooltip?: string | MarkdownString; + + /** + * The icon path for the Python project creator, which can be a string, Uri, or an object with light and dark theme paths. + */ + readonly iconPath?: IconPath; + + /** + * Creates a new Python project(s) or, if files are not a project, returns Uri(s) to the created files. + * Anything that needs its own python environment constitutes a project. + * @param options Optional parameters for creating the Python project. + * @returns A promise that resolves to one of the following: + * - PythonProject or PythonProject[]: when a single or multiple projects are created. + * - Uri or Uri[]: when files are created that do not constitute a project. + * - undefined: if project creation fails. + */ + create(options?: PythonProjectCreatorOptions): Promise; + + /** + * A flag indicating whether the project creator supports quick create where no user input is required. + */ + readonly supportsQuickCreate?: boolean; +} + +/** + * Event arguments for when Python projects change. + */ +export interface DidChangePythonProjectsEventArgs { + /** + * The list of Python projects that were added. + */ + added: PythonProject[]; + + /** + * The list of Python projects that were removed. + */ + removed: PythonProject[]; +} + +export type PackageManagementOptions = + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall?: string[]; + } + | { + /** + * Upgrade the packages if they are already installed. + */ + upgrade?: boolean; + + /** + * Show option to skip package installation or uninstallation. + */ + showSkipOption?: boolean; + /** + * The list of packages to install. + */ + install?: string[]; + + /** + * The list of packages to uninstall. + */ + uninstall: string[]; + }; + +/** + * Options for creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Provides some context about quick create based on user input. + * - if true, the environment should be created without any user input or prompts. + * - if false, the environment creation can show user input or prompts. + * This also means user explicitly skipped the quick create option. + * - if undefined, the environment creation can show user input or prompts. + * You can show quick create option to the user if you support it. + */ + quickCreate?: boolean; + /** + * Packages to install in addition to the automatically picked packages as a part of creating environment. + */ + additionalPackages?: string[]; +} + +/** + * Object representing the process started using run in background API. + */ +export interface PythonProcess { + /** + * The process ID of the Python process. + */ + readonly pid?: number; + + /** + * The standard input of the Python process. + */ + readonly stdin: NodeJS.WritableStream; + + /** + * The standard output of the Python process. + */ + readonly stdout: NodeJS.ReadableStream; + + /** + * The standard error of the Python process. + */ + readonly stderr: NodeJS.ReadableStream; + + /** + * Kills the Python process. + */ + kill(): void; + + /** + * Event that is fired when the Python process exits. + */ + onExit(listener: (code: number | null, signal: NodeJS.Signals | null) => void): void; +} + +export interface PythonEnvironmentManagerRegistrationApi { + /** + * Register an environment manager implementation. + * + * @param manager Environment Manager implementation to register. + * @returns A disposable that can be used to unregister the environment manager. + * @see {@link EnvironmentManager} + */ + registerEnvironmentManager(manager: EnvironmentManager): Disposable; +} + +export interface PythonEnvironmentItemApi { + /** + * Create a Python environment item from the provided environment info. This item is used to interact + * with the environment. + * + * @param info Some details about the environment like name, version, etc. needed to interact with the environment. + * @param manager The environment manager to associate with the environment. + * @returns The Python environment. + */ + createPythonEnvironmentItem(info: PythonEnvironmentInfo, manager: EnvironmentManager): PythonEnvironment; +} + +export interface PythonEnvironmentManagementApi { + /** + * Create a Python environment using environment manager associated with the scope. + * + * @param scope Where the environment is to be created. + * @param options Optional parameters for creating the Python environment. + * @returns The Python environment created. `undefined` if not created. + */ + createEnvironment( + scope: CreateEnvironmentScope, + options?: CreateEnvironmentOptions, + ): Promise; + + /** + * Remove a Python environment. + * + * @param environment The Python environment to remove. + * @returns A promise that resolves when the environment has been removed. + */ + removeEnvironment(environment: PythonEnvironment): Promise; +} + +export interface PythonEnvironmentsApi { + /** + * Initiates a refresh of Python environments within the specified scope. + * @param scope - The scope within which to search for environments. + * @returns A promise that resolves when the search is complete. + */ + refreshEnvironments(scope: RefreshEnvironmentsScope): Promise; + + /** + * Retrieves a list of Python environments within the specified scope. + * @param scope - The scope within which to retrieve environments. + * @returns A promise that resolves to an array of Python environments. + */ + getEnvironments(scope: GetEnvironmentsScope): Promise; + + /** + * Event that is fired when the list of Python environments changes. + * @see {@link DidChangeEnvironmentsEventArgs} + */ + onDidChangeEnvironments: Event; + + /** + * This method is used to get the details missing from a PythonEnvironment. Like + * {@link PythonEnvironment.execInfo} and other details. + * + * @param context : The PythonEnvironment or Uri for which details are required. + */ + resolveEnvironment(context: ResolveEnvironmentContext): Promise; +} + +export interface PythonProjectEnvironmentApi { + /** + * Sets the current Python environment within the specified scope. + * @param scope - The scope within which to set the environment. + * @param environment - The Python environment to set. If undefined, the environment is unset. + */ + setEnvironment(scope: SetEnvironmentScope, environment?: PythonEnvironment): Promise; + + /** + * Retrieves the current Python environment within the specified scope. + * @param scope - The scope within which to retrieve the environment. + * @returns A promise that resolves to the current Python environment, or undefined if none is set. + */ + getEnvironment(scope: GetEnvironmentScope): Promise; + + /** + * Event that is fired when the selected Python environment changes for Project, Folder or File. + * @see {@link DidChangeEnvironmentEventArgs} + */ + onDidChangeEnvironment: Event; +} + +export interface PythonEnvironmentManagerApi + extends PythonEnvironmentManagerRegistrationApi, + PythonEnvironmentItemApi, + PythonEnvironmentManagementApi, + PythonEnvironmentsApi, + PythonProjectEnvironmentApi {} + +export interface PythonPackageManagerRegistrationApi { + /** + * Register a package manager implementation. + * + * @param manager Package Manager implementation to register. + * @returns A disposable that can be used to unregister the package manager. + * @see {@link PackageManager} + */ + registerPackageManager(manager: PackageManager): Disposable; +} + +export interface PythonPackageGetterApi { + /** + * Refresh the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is to be refreshed. + * @returns A promise that resolves when the list of packages has been refreshed. + */ + refreshPackages(environment: PythonEnvironment): Promise; + + /** + * Get the list of packages in a Python Environment. + * + * @param environment The Python Environment for which the list of packages is required. + * @returns The list of packages in the Python Environment. + */ + getPackages(environment: PythonEnvironment): Promise; + + /** + * Event raised when the list of packages in a Python Environment changes. + * @see {@link DidChangePackagesEventArgs} + */ + onDidChangePackages: Event; +} + +export interface PythonPackageItemApi { + /** + * Create a package item from the provided package info. + * + * @param info The package info. + * @param environment The Python Environment in which the package is installed. + * @param manager The package manager that installed the package. + * @returns The package item. + */ + createPackageItem(info: PackageInfo, environment: PythonEnvironment, manager: PackageManager): Package; +} + +export interface PythonPackageManagementApi { + /** + * Install/Uninstall packages into a Python Environment. + * + * @param environment The Python Environment into which packages are to be installed. + * @param packages The packages to install. + * @param options Options for installing packages. + */ + managePackages(environment: PythonEnvironment, options: PackageManagementOptions): Promise; +} + +export interface PythonPackageManagerApi + extends PythonPackageManagerRegistrationApi, + PythonPackageGetterApi, + PythonPackageManagementApi, + PythonPackageItemApi {} + +export interface PythonProjectCreationApi { + /** + * Register a Python project creator. + * + * @param creator The project creator to register. + * @returns A disposable that can be used to unregister the project creator. + * @see {@link PythonProjectCreator} + */ + registerPythonProjectCreator(creator: PythonProjectCreator): Disposable; +} +export interface PythonProjectGetterApi { + /** + * Get all python projects. + */ + getPythonProjects(): readonly PythonProject[]; + + /** + * Get the python project for a given URI. + * + * @param uri The URI of the project + * @returns The project or `undefined` if not found. + */ + getPythonProject(uri: Uri): PythonProject | undefined; +} + +export interface PythonProjectModifyApi { + /** + * Add a python project or projects to the list of projects. + * + * @param projects The project or projects to add. + */ + addPythonProject(projects: PythonProject | PythonProject[]): void; + + /** + * Remove a python project from the list of projects. + * + * @param project The project to remove. + */ + removePythonProject(project: PythonProject): void; + + /** + * Event raised when python projects are added or removed. + * @see {@link DidChangePythonProjectsEventArgs} + */ + onDidChangePythonProjects: Event; +} + +/** + * The API for interacting with Python projects. A project in python is any folder or file that is a contained + * in some manner. For example, a PEP-723 compliant file can be treated as a project. A folder with a `pyproject.toml`, + * or just python files can be treated as a project. All this allows you to do is set a python environment for that project. + * + * By default all `vscode.workspace.workspaceFolders` are treated as projects. + */ +export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {} + +export interface PythonTerminalCreateOptions extends TerminalOptions { + /** + * Whether to disable activation on create. + */ + disableActivation?: boolean; +} + +export interface PythonTerminalCreateApi { + /** + * Creates a terminal and activates any (activatable) environment for the terminal. + * + * @param environment The Python environment to activate. + * @param options Options for creating the terminal. + * + * Note: Non-activatable environments have no effect on the terminal. + */ + createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise; +} + +/** + * Options for running a Python script or module in a terminal. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTerminalExecutionOptions { + /** + * Current working directory for the terminal. This in only used to create the terminal. + */ + cwd: string | Uri; + + /** + * Arguments to pass to the python executable. + */ + args?: string[]; + + /** + * Set `true` to show the terminal. + */ + show?: boolean; +} + +export interface PythonTerminalRunApi { + /** + * Runs a Python script or module in a terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. + * + * Note: + * - If you restart VS Code, this will create a new terminal, this is a limitation of VS Code. + * - If you close the terminal, this will create a new terminal. + * - In cases of multi-root/project scenario, it will create a separate terminal for each project. + */ + runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise; + + /** + * Runs a Python script or module in a dedicated terminal. This API will create a terminal if one is not available to use. + * If a terminal is available, it will be used to run the script or module. This terminal will be dedicated to the script, + * and selected based on the `terminalKey`. + * + * @param terminalKey A unique key to identify the terminal. For scripts you can use the Uri of the script file. + */ + runInDedicatedTerminal( + terminalKey: Uri | string, + environment: PythonEnvironment, + options: PythonTerminalExecutionOptions, + ): Promise; +} + +/** + * Options for running a Python task. + * + * Example: + * * Running Script: `python myscript.py --arg1` + * ```typescript + * { + * args: ["myscript.py", "--arg1"] + * } + * ``` + * * Running a module: `python -m my_module --arg1` + * ```typescript + * { + * args: ["-m", "my_module", "--arg1"] + * } + * ``` + */ +export interface PythonTaskExecutionOptions { + /** + * Name of the task to run. + */ + name: string; + + /** + * Arguments to pass to the python executable. + */ + args: string[]; + + /** + * The Python project to use for the task. + */ + project?: PythonProject; + + /** + * Current working directory for the task. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the task. + */ + env?: { [key: string]: string }; +} + +export interface PythonTaskRunApi { + /** + * Run a Python script or module as a task. + * + */ + runAsTask(environment: PythonEnvironment, options: PythonTaskExecutionOptions): Promise; +} + +/** + * Options for running a Python script or module in the background. + */ +export interface PythonBackgroundRunOptions { + /** + * The Python environment to use for running the script or module. + */ + args: string[]; + + /** + * Current working directory for the script or module. Default is the project directory for the script being run. + */ + cwd?: string; + + /** + * Environment variables to set for the script or module. + */ + env?: { [key: string]: string | undefined }; +} +export interface PythonBackgroundRunApi { + /** + * Run a Python script or module in the background. This API will create a new process to run the script or module. + */ + runInBackground(environment: PythonEnvironment, options: PythonBackgroundRunOptions): Promise; +} + +export interface PythonExecutionApi + extends PythonTerminalCreateApi, + PythonTerminalRunApi, + PythonTaskRunApi, + PythonBackgroundRunApi {} + +/** + * Event arguments for when the monitored `.env` files or any other sources change. + */ +export interface DidChangeEnvironmentVariablesEventArgs { + /** + * The URI of the file that changed. No `Uri` means a non-file source of environment variables changed. + */ + uri?: Uri; + + /** + * The type of change that occurred. + */ + changeTye: FileChangeType; +} + +export interface PythonEnvironmentVariablesApi { + /** + * Get environment variables for a workspace. This picks up `.env` file from the root of the + * workspace. + * + * Order of overrides: + * 1. `baseEnvVar` if given or `process.env` + * 2. `.env` file from the "python.envFile" setting in the workspace. + * 3. `.env` file at the root of the python project. + * 4. `overrides` in the order provided. + * + * @param uri The URI of the project, workspace or a file in a for which environment variables are required. + * @param overrides Additional environment variables to override the defaults. + * @param baseEnvVar The base environment variables that should be used as a starting point. + */ + getEnvironmentVariables( + uri: Uri, + overrides?: ({ [key: string]: string | undefined } | Uri)[], + baseEnvVar?: { [key: string]: string | undefined }, + ): Promise<{ [key: string]: string | undefined }>; + + /** + * Event raised when `.env` file changes or any other monitored source of env variable changes. + */ + onDidChangeEnvironmentVariables: Event; +} + +/** + * The API for interacting with Python environments, package managers, and projects. + */ +export interface PythonEnvironmentApi + extends PythonEnvironmentManagerApi, + PythonPackageManagerApi, + PythonProjectApi, + PythonExecutionApi, + PythonEnvironmentVariablesApi {} diff --git a/src/client/environmentApi.ts b/src/client/environmentApi.ts new file mode 100644 index 000000000000..ecd8eef21845 --- /dev/null +++ b/src/client/environmentApi.ts @@ -0,0 +1,444 @@ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, EventEmitter, Uri, workspace, WorkspaceFolder } from 'vscode'; +import * as pathUtils from 'path'; +import { IConfigurationService, IDisposableRegistry, IExtensions, IInterpreterPathService } from './common/types'; +import { Architecture } from './common/utils/platform'; +import { IServiceContainer } from './ioc/types'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from './pythonEnvironments/base/info'; +import { getEnvPath } from './pythonEnvironments/base/info/env'; +import { IDiscoveryAPI, ProgressReportStage } from './pythonEnvironments/base/locator'; +import { IPythonExecutionFactory } from './common/process/types'; +import { traceError, traceInfo, traceVerbose } from './logging'; +import { isParentPath, normCasePath } from './common/platform/fs-paths'; +import { sendTelemetryEvent } from './telemetry'; +import { EventName } from './telemetry/constants'; +import { reportActiveInterpreterChangedDeprecated, reportInterpretersChanged } from './deprecatedProposedApi'; +import { IEnvironmentVariablesProvider } from './common/variables/types'; +import { getWorkspaceFolder, getWorkspaceFolders } from './common/vscodeApis/workspaceApis'; +import { + ActiveEnvironmentPathChangeEvent, + Environment, + EnvironmentPath, + EnvironmentsChangeEvent, + EnvironmentTools, + EnvironmentType, + EnvironmentVariablesChangeEvent, + PythonExtension, + RefreshOptions, + ResolvedEnvironment, + Resource, +} from './api/types'; +import { buildEnvironmentCreationApi } from './pythonEnvironments/creation/createEnvApi'; +import { EnvironmentKnownCache } from './environmentKnownCache'; +import type { JupyterPythonEnvironmentApi } from './jupyter/jupyterIntegration'; +import { noop } from './common/utils/misc'; + +type ActiveEnvironmentChangeEvent = { + resource: WorkspaceFolder | undefined; + path: string; +}; + +const onDidActiveInterpreterChangedEvent = new EventEmitter(); +const previousEnvMap = new Map(); +export function reportActiveInterpreterChanged(e: ActiveEnvironmentChangeEvent): void { + const oldPath = previousEnvMap.get(e.resource?.uri.fsPath ?? ''); + if (oldPath === e.path) { + return; + } + previousEnvMap.set(e.resource?.uri.fsPath ?? '', e.path); + onDidActiveInterpreterChangedEvent.fire({ id: getEnvID(e.path), path: e.path, resource: e.resource }); + reportActiveInterpreterChangedDeprecated({ path: e.path, resource: e.resource?.uri }); +} + +const onEnvironmentsChanged = new EventEmitter(); +const onEnvironmentVariablesChanged = new EventEmitter(); +const environmentsReference = new Map(); + +/** + * Make all properties in T mutable. + */ +type Mutable = { + -readonly [P in keyof T]: Mutable; +}; + +export class EnvironmentReference implements Environment { + readonly id: string; + + constructor(public internal: Environment) { + this.id = internal.id; + } + + get executable() { + return Object.freeze(this.internal.executable); + } + + get environment() { + return Object.freeze(this.internal.environment); + } + + get version() { + return Object.freeze(this.internal.version); + } + + get tools() { + return Object.freeze(this.internal.tools); + } + + get path() { + return Object.freeze(this.internal.path); + } + + updateEnv(newInternal: Environment) { + this.internal = newInternal; + } +} + +function getEnvReference(e: Environment) { + let envClass = environmentsReference.get(e.id); + if (!envClass) { + envClass = new EnvironmentReference(e); + } else { + envClass.updateEnv(e); + } + environmentsReference.set(e.id, envClass); + return envClass; +} + +function filterUsingVSCodeContext(e: PythonEnvInfo) { + const folders = getWorkspaceFolders(); + if (e.searchLocation) { + // Only return local environments that are in the currently opened workspace folders. + const envFolderUri = e.searchLocation; + if (folders) { + return folders.some((folder) => isParentPath(envFolderUri.fsPath, folder.uri.fsPath)); + } + return false; + } + return true; +} + +export function buildEnvironmentApi( + discoveryApi: IDiscoveryAPI, + serviceContainer: IServiceContainer, + jupyterPythonEnvsApi: JupyterPythonEnvironmentApi, +): PythonExtension['environments'] { + const interpreterPathService = serviceContainer.get(IInterpreterPathService); + const configService = serviceContainer.get(IConfigurationService); + const disposables = serviceContainer.get(IDisposableRegistry); + const extensions = serviceContainer.get(IExtensions); + const envVarsProvider = serviceContainer.get(IEnvironmentVariablesProvider); + let knownCache: EnvironmentKnownCache; + + function initKnownCache() { + const knownEnvs = discoveryApi + .getEnvs() + .filter((e) => filterUsingVSCodeContext(e)) + .map((e) => updateReference(e)); + return new EnvironmentKnownCache(knownEnvs); + } + function sendApiTelemetry(apiName: string, args?: unknown) { + extensions + .determineExtensionFromCallStack() + .then((info) => { + const p = Math.random(); + if (p <= 0.001) { + // Only send API telemetry 1% of the time, as it can be chatty. + sendTelemetryEvent(EventName.PYTHON_ENVIRONMENTS_API, undefined, { + apiName, + extensionId: info.extensionId, + }); + } + traceVerbose(`Extension ${info.extensionId} accessed ${apiName} with args: ${JSON.stringify(args)}`); + }) + .ignoreErrors(); + } + + function getActiveEnvironmentPath(resource?: Resource) { + resource = resource && 'uri' in resource ? resource.uri : resource; + const jupyterEnv = + resource && jupyterPythonEnvsApi.getPythonEnvironment + ? jupyterPythonEnvsApi.getPythonEnvironment(resource) + : undefined; + if (jupyterEnv) { + traceVerbose('Python Environment returned from Jupyter', resource?.fsPath, jupyterEnv.id); + return { + id: jupyterEnv.id, + path: jupyterEnv.path, + }; + } + const path = configService.getSettings(resource).pythonPath; + const id = path === 'python' ? 'DEFAULT_PYTHON' : getEnvID(path); + return { + id, + path, + }; + } + + disposables.push( + onDidActiveInterpreterChangedEvent.event((e) => { + let scope = 'global'; + if (e.resource) { + scope = e.resource instanceof Uri ? e.resource.fsPath : e.resource.uri.fsPath; + } + traceInfo(`Active interpreter [${scope}]: `, e.path); + }), + discoveryApi.onProgress((e) => { + if (e.stage === ProgressReportStage.discoveryFinished) { + knownCache = initKnownCache(); + } + }), + discoveryApi.onChanged((e) => { + const env = e.new ?? e.old; + if (!env || !filterUsingVSCodeContext(env)) { + // Filter out environments that are not in the current workspace. + return; + } + if (!knownCache) { + knownCache = initKnownCache(); + } + if (e.old) { + if (e.new) { + const newEnv = updateReference(e.new); + knownCache.updateEnv(convertEnvInfo(e.old), newEnv); + traceVerbose('Python API env change detected', env.id, 'update'); + onEnvironmentsChanged.fire({ type: 'update', env: newEnv }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'update', + }, + ]); + } else { + const oldEnv = updateReference(e.old); + knownCache.updateEnv(oldEnv, undefined); + traceVerbose('Python API env change detected', env.id, 'remove'); + onEnvironmentsChanged.fire({ type: 'remove', env: oldEnv }); + reportInterpretersChanged([ + { + path: getEnvPath(e.old.executable.filename, e.old.location).path, + type: 'remove', + }, + ]); + } + } else if (e.new) { + const newEnv = updateReference(e.new); + knownCache.addEnv(newEnv); + traceVerbose('Python API env change detected', env.id, 'add'); + onEnvironmentsChanged.fire({ type: 'add', env: newEnv }); + reportInterpretersChanged([ + { + path: getEnvPath(e.new.executable.filename, e.new.location).path, + type: 'add', + }, + ]); + } + }), + envVarsProvider.onDidEnvironmentVariablesChange((e) => { + onEnvironmentVariablesChanged.fire({ + resource: getWorkspaceFolder(e), + env: envVarsProvider.getEnvironmentVariablesSync(e), + }); + }), + onEnvironmentsChanged, + onEnvironmentVariablesChanged, + jupyterPythonEnvsApi.onDidChangePythonEnvironment + ? jupyterPythonEnvsApi.onDidChangePythonEnvironment((e) => { + const jupyterEnv = getActiveEnvironmentPath(e); + onDidActiveInterpreterChangedEvent.fire({ + id: jupyterEnv.id, + path: jupyterEnv.path, + resource: e, + }); + }, undefined) + : { dispose: noop }, + ); + if (!knownCache!) { + knownCache = initKnownCache(); + } + + const environmentApi: PythonExtension['environments'] = { + getEnvironmentVariables: (resource?: Resource) => { + sendApiTelemetry('getEnvironmentVariables'); + resource = resource && 'uri' in resource ? resource.uri : resource; + return envVarsProvider.getEnvironmentVariablesSync(resource); + }, + get onDidEnvironmentVariablesChange() { + sendApiTelemetry('onDidEnvironmentVariablesChange'); + return onEnvironmentVariablesChanged.event; + }, + getActiveEnvironmentPath(resource?: Resource) { + sendApiTelemetry('getActiveEnvironmentPath'); + return getActiveEnvironmentPath(resource); + }, + updateActiveEnvironmentPath(env: Environment | EnvironmentPath | string, resource?: Resource): Promise { + sendApiTelemetry('updateActiveEnvironmentPath'); + const path = typeof env !== 'string' ? env.path : env; + resource = resource && 'uri' in resource ? resource.uri : resource; + return interpreterPathService.update(resource, ConfigurationTarget.WorkspaceFolder, path); + }, + get onDidChangeActiveEnvironmentPath() { + sendApiTelemetry('onDidChangeActiveEnvironmentPath'); + return onDidActiveInterpreterChangedEvent.event; + }, + resolveEnvironment: async (env: Environment | EnvironmentPath | string) => { + if (!workspace.isTrusted) { + throw new Error('Not allowed to resolve environment in an untrusted workspace'); + } + let path = typeof env !== 'string' ? env.path : env; + if (pathUtils.basename(path) === path) { + // Value can be `python`, `python3`, `python3.9` etc. + // This case could eventually be handled by the internal discovery API itself. + const pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); + const pythonExecutionService = await pythonExecutionFactory.create({ pythonPath: path }); + const fullyQualifiedPath = await pythonExecutionService.getExecutablePath().catch((ex) => { + traceError('Cannot resolve full path', ex); + return undefined; + }); + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { + return undefined; + } + path = fullyQualifiedPath; + } + sendApiTelemetry('resolveEnvironment', env); + return resolveEnvironment(path, discoveryApi); + }, + get known(): Environment[] { + // Do not send telemetry for "known", as this may be called 1000s of times so it can significant: + // sendApiTelemetry('known'); + return knownCache.envs; + }, + async refreshEnvironments(options?: RefreshOptions) { + if (!workspace.isTrusted) { + traceError('Not allowed to refresh environments in an untrusted workspace'); + return; + } + await discoveryApi.triggerRefresh(undefined, { + ifNotTriggerredAlready: !options?.forceRefresh, + }); + sendApiTelemetry('refreshEnvironments'); + }, + get onDidChangeEnvironments() { + sendApiTelemetry('onDidChangeEnvironments'); + return onEnvironmentsChanged.event; + }, + ...buildEnvironmentCreationApi(), + }; + return environmentApi; +} + +async function resolveEnvironment(path: string, discoveryApi: IDiscoveryAPI): Promise { + const env = await discoveryApi.resolveEnv(path); + if (!env) { + return undefined; + } + const resolvedEnv = getEnvReference(convertCompleteEnvInfo(env)) as ResolvedEnvironment; + if (resolvedEnv.version?.major === -1 || resolvedEnv.version?.minor === -1 || resolvedEnv.version?.micro === -1) { + traceError(`Invalid version for ${path}: ${JSON.stringify(env)}`); + } + return resolvedEnv; +} + +export function convertCompleteEnvInfo(env: PythonEnvInfo): ResolvedEnvironment { + const version = { ...env.version, sysVersion: env.version.sysVersion }; + let tool = convertKind(env.kind); + if (env.type && !tool) { + tool = 'Unknown'; + } + const { path } = getEnvPath(env.executable.filename, env.location); + const resolvedEnv: ResolvedEnvironment = { + path, + id: env.id!, + executable: { + uri: env.executable.filename === 'python' ? undefined : Uri.file(env.executable.filename), + bitness: convertBitness(env.arch), + sysPrefix: env.executable.sysPrefix, + }, + environment: env.type + ? { + type: convertEnvType(env.type), + name: env.name === '' ? undefined : env.name, + folderUri: Uri.file(env.location), + workspaceFolder: getWorkspaceFolder(env.searchLocation), + } + : undefined, + version: env.executable.filename === 'python' ? undefined : (version as ResolvedEnvironment['version']), + tools: tool ? [tool] : [], + }; + return resolvedEnv; +} + +function convertEnvType(envType: PythonEnvType): EnvironmentType { + if (envType === PythonEnvType.Conda) { + return 'Conda'; + } + if (envType === PythonEnvType.Virtual) { + return 'VirtualEnvironment'; + } + return 'Unknown'; +} + +function convertKind(kind: PythonEnvKind): EnvironmentTools | undefined { + switch (kind) { + case PythonEnvKind.Venv: + return 'Venv'; + case PythonEnvKind.Pipenv: + return 'Pipenv'; + case PythonEnvKind.Poetry: + return 'Poetry'; + case PythonEnvKind.Hatch: + return 'Hatch'; + case PythonEnvKind.VirtualEnvWrapper: + return 'VirtualEnvWrapper'; + case PythonEnvKind.VirtualEnv: + return 'VirtualEnv'; + case PythonEnvKind.Conda: + return 'Conda'; + case PythonEnvKind.Pyenv: + return 'Pyenv'; + default: + return undefined; + } +} + +export function convertEnvInfo(env: PythonEnvInfo): Environment { + const convertedEnv = convertCompleteEnvInfo(env) as Mutable; + if (convertedEnv.executable.sysPrefix === '') { + convertedEnv.executable.sysPrefix = undefined; + } + if (convertedEnv.version?.sysVersion === '') { + convertedEnv.version.sysVersion = undefined; + } + if (convertedEnv.version?.major === -1) { + convertedEnv.version.major = undefined; + } + if (convertedEnv.version?.micro === -1) { + convertedEnv.version.micro = undefined; + } + if (convertedEnv.version?.minor === -1) { + convertedEnv.version.minor = undefined; + } + return convertedEnv as Environment; +} + +function updateReference(env: PythonEnvInfo): Environment { + return getEnvReference(convertEnvInfo(env)); +} + +function convertBitness(arch: Architecture) { + switch (arch) { + case Architecture.x64: + return '64-bit'; + case Architecture.x86: + return '32-bit'; + default: + return 'Unknown'; + } +} + +function getEnvID(path: string) { + return normCasePath(path); +} diff --git a/src/client/environmentKnownCache.ts b/src/client/environmentKnownCache.ts new file mode 100644 index 000000000000..287f5bab343f --- /dev/null +++ b/src/client/environmentKnownCache.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Environment } from './api/types'; + +/** + * Workaround temp cache until types are consolidated. + */ +export class EnvironmentKnownCache { + private _envs: Environment[] = []; + + constructor(envs: Environment[]) { + this._envs = envs; + } + + public get envs(): Environment[] { + return this._envs; + } + + public addEnv(env: Environment): void { + const found = this._envs.find((e) => env.id === e.id); + if (!found) { + this._envs.push(env); + } + } + + public updateEnv(oldValue: Environment, newValue: Environment | undefined): void { + const index = this._envs.findIndex((e) => oldValue.id === e.id); + if (index !== -1) { + if (newValue === undefined) { + this._envs.splice(index, 1); + } else { + this._envs[index] = newValue; + } + } + } +} diff --git a/src/client/extension.ts b/src/client/extension.ts index 3a22ca96ec65..c3fb2a3ab3b0 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -1,406 +1,198 @@ 'use strict'; -// tslint:disable:no-var-requires no-require-imports // This line should always be right on top. -// tslint:disable:no-any + if ((Reflect as any).metadata === undefined) { require('reflect-metadata'); } -// Initialize source maps (this must never be moved up nor further down). -import { initialize } from './sourceMapSupport'; -initialize(require('vscode')); -// Initialize the logger first. -require('./common/logger'); +//=============================================== +// We start tracking the extension's startup time at this point. The +// locations at which we record various Intervals are marked below in +// the same way as this. -const durations: Record = {}; +const durations = {} as IStartupDurations; import { StopWatch } from './common/utils/stopWatch'; // Do not move this line of code (used to measure extension load times). const stopWatch = new StopWatch(); -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 { IExtensionActivationManager, ILanguageServerExtension } 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 { IApplicationShell, ICommandManager, IWorkspaceService } from './common/application/types'; -import { Commands, 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 { traceError } from './common/logger'; -import { registerTypes as platformRegisterTypes } from './common/platform/serviceRegistry'; -import { registerTypes as processRegisterTypes } from './common/process/serviceRegistry'; -import { registerTypes as commonRegisterTypes } from './common/serviceRegistry'; -import { ITerminalHelper } from './common/terminal/types'; -import { - GLOBAL_MEMENTO, - IAsyncDisposableRegistry, - IConfigurationService, - IDisposableRegistry, - IExperimentsManager, - IExtensionContext, - IFeatureDeprecationManager, - IMemento, - IOutputChannel, - Resource, - WORKSPACE_MEMENTO -} from './common/types'; + +// Initialize file logging here. This should not depend on too many things. +import { initializeFileLogging, traceError } from './logging'; +const logDispose: { dispose: () => void }[] = []; +initializeFileLogging(logDispose); + +//=============================================== +// loading starts here + +import { ProgressLocation, ProgressOptions, window } from 'vscode'; +import { buildApi } 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 { 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 { EventName } from './telemetry/constants'; -import { EditorLoadTelemetry, IImportTracker } from './telemetry/types'; -import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; -import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types'; -import { TEST_OUTPUT_CHANNEL } from './testing/common/constants'; -import { ITestContextService } from './testing/common/types'; -import { ITestCodeNavigatorCommandHandler, ITestExplorerCommandHandler } from './testing/navigation/types'; +import { activateComponents, activateFeatures } from './extensionActivation'; +import { initializeStandard, initializeComponents, initializeGlobals } from './extensionInit'; +import { IServiceContainer } from './ioc/types'; +import { sendErrorTelemetry, sendStartupTelemetry } from './startupTelemetry'; +import { IStartupDurations } from './types'; +import { runAfterActivation } from './common/utils/runAfterActivation'; +import { IInterpreterService } from './interpreter/contracts'; +import { PythonExtension } from './api/types'; +import { WorkspaceService } from './common/application/workspace'; +import { disposeAll } from './common/utils/resourceLifecycle'; +import { ProposedExtensionAPI } from './proposedApiTypes'; +import { buildProposedApi } from './proposedApi'; +import { GLOBAL_PERSISTENT_KEYS } from './common/persistentState'; +import { registerTools } from './chat'; +import { IRecommendedEnvironmentService } from './interpreter/configuration/types'; import { registerTypes as unitTestsRegisterTypes } from './testing/serviceRegistry'; +import { registerTestCommands } from './testing/main'; durations.codeLoadingTime = stopWatch.elapsedTime; -const activationDeferred = createDeferred(); -let activatedServiceContainer: ServiceContainer | undefined; - -export async function activate(context: ExtensionContext): Promise { - try { - return await activateUnsafe(context); - } catch (ex) { - handleError(ex); - throw ex; // re-raise - } -} - -// tslint:disable-next-line:max-func-body-length -async function activateUnsafe(context: ExtensionContext): Promise { - 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); - await initializeServices(context, serviceManager, serviceContainer); - - const manager = serviceContainer.get(IExtensionActivationManager); - context.subscriptions.push(manager); - const activationPromise = manager.activate(); - - serviceManager.get(ITerminalAutoActivation).register(); - const configuration = serviceManager.get(IConfigurationService); - const pythonSettings = configuration.getSettings(); - - const standardOutputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); - - const sortImports = serviceContainer.get(ISortImportsEditingProvider); - sortImports.registerCommands(); - - serviceManager.get(ICodeExecutionManager).registerCommands(); - - // tslint:disable-next-line:no-suspicious-comment - // TODO: Move this down to right before durations.endActivateTime is set. - sendStartupTelemetry(Promise.all([activationDeferred.promise, activationPromise]), serviceContainer).ignoreErrors(); - - const workspaceService = serviceContainer.get(IWorkspaceService); - const interpreterManager = serviceContainer.get(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); - lintingEngine.linkJupyterExtension(jupyterExtension).ignoreErrors(); - - // Activate data science features - const dataScience = serviceManager.get(IDataScience); - dataScience.activate().ignoreErrors(); - - // Activate import tracking - const importTracker = serviceManager.get(IImportTracker); - importTracker.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 !== 'internalConsole') { - const formatProvider = new PythonFormattingEditProvider(context, serviceContainer); - context.subscriptions.push(languages.registerDocumentFormattingEditProvider(PYTHON, formatProvider)); - context.subscriptions.push(languages.registerDocumentRangeFormattingEditProvider(PYTHON, formatProvider)); - } - const deprecationMgr = serviceContainer.get(IFeatureDeprecationManager); - deprecationMgr.initialize(); - context.subscriptions.push(deprecationMgr); +//=============================================== +// loading ends here - context.subscriptions.push(activateUpdateSparkLibraryProvider()); +// These persist between activations: +let activatedServiceContainer: IServiceContainer | undefined; - context.subscriptions.push(new ReplProvider(serviceContainer)); - context.subscriptions.push(new TerminalProvider(serviceContainer)); - - context.subscriptions.push(languages.registerCodeActionsProvider(PYTHON, new PythonCodeActionProvider(), { providedCodeActionKinds: [CodeActionKind.SourceOrganizeImports] })); - - serviceContainer.getAll(IDebugConfigurationService).forEach(debugConfigProvider => { - context.subscriptions.push(debug.registerDebugConfigurationProvider(DebuggerTypeName, debugConfigProvider)); - }); - - serviceContainer.get(IDebuggerBanner).initialize(); - durations.endActivateTime = stopWatch.elapsedTime; - activationDeferred.resolve(); +///////////////////////////// +// public functions - const api = buildApi(Promise.all([activationDeferred.promise, activationPromise])); - // 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 { + let api: PythonExtension; + let ready: Promise; + let serviceContainer: IServiceContainer; + let isFirstSession: boolean | undefined; + try { + isFirstSession = context.globalState.get(GLOBAL_PERSISTENT_KEYS, []).length === 0; + const workspaceService = new WorkspaceService(); + context.subscriptions.push( + workspaceService.onDidGrantWorkspaceTrust(async () => { + await deactivate(); + await activate(context); + }), + ); + [api, ready, serviceContainer] = await activateUnsafe(context, stopWatch, durations); + } catch (ex) { + // We want to completely handle the error + // before notifying VS Code. + await handleError(ex 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 { +export async function deactivate(): Promise { // Make sure to shutdown anybody who needs it. if (activatedServiceContainer) { - const registry = activatedServiceContainer.get(IAsyncDisposableRegistry); - if (registry) { - return registry.dispose(); - } + const disposables = activatedServiceContainer.get(IDisposableRegistry); + await disposeAll(disposables); + // Remove everything that is already disposed. + while (disposables.pop()); } - - return Promise.resolve(); } -// tslint:disable-next-line:no-any -function displayProgress(promise: Promise) { - const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension() }; - window.withProgress(progressOptions, () => promise); -} +///////////////////////////// +// activation helpers -function registerServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { - serviceManager.addSingletonInstance(IServiceContainer, serviceContainer); - serviceManager.addSingletonInstance(IServiceManager, serviceManager); - serviceManager.addSingletonInstance(IDisposableRegistry, context.subscriptions); - serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); - serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); - serviceManager.addSingletonInstance(IExtensionContext, context); - - const standardOutputChannel = window.createOutputChannel('Python'); - const unitTestOutChannel = window.createOutputChannel('Python Test Log'); - serviceManager.addSingletonInstance(IOutputChannel, standardOutputChannel, STANDARD_OUTPUT_CHANNEL); - serviceManager.addSingletonInstance(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); -} +async function activateUnsafe( + context: IExtensionContext, + startupStopWatch: StopWatch, + startupDurations: IStartupDurations, +): Promise<[PythonExtension & ProposedExtensionAPI, Promise, IServiceContainer]> { + // Add anything that we got from initializing logs to dispose. + context.subscriptions.push(...logDispose); -async function initializeServices(context: ExtensionContext, serviceManager: ServiceManager, serviceContainer: ServiceContainer) { - const abExperiments = serviceContainer.get(IExperimentsManager); - await abExperiments.activate(); - const selector = serviceContainer.get(IInterpreterSelector); - selector.initialize(); - context.subscriptions.push(selector); - - const interpreterManager = serviceContainer.get(IInterpreterService); - interpreterManager.initialize(); - - const handlers = serviceManager.getAll(IDebugSessionEventHandlers); - const disposables = serviceManager.get(IDisposableRegistry); - const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); - dispatcher.registerEventHandlers(); - - const cmdManager = serviceContainer.get(ICommandManager); - const outputChannel = serviceManager.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); - - // Display progress of interpreter refreshes only after extension has activated. - serviceContainer.get(InterpreterLocatorProgressHandler).register(); - serviceContainer.get(IInterpreterLocatorProgressService).register(); - serviceContainer.get(IApplicationDiagnostics).register(); - serviceContainer.get(ITestCodeNavigatorCommandHandler).register(); - serviceContainer.get(ITestExplorerCommandHandler).register(); - serviceContainer.get(ILanguageServerExtension).register(); - serviceContainer.get(ITestContextService).register(); -} + const activationDeferred = createDeferred(); + 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); + // 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(); -// tslint:disable-next-line:no-any -async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { - try { - await activatedPromise; - durations.totalActivateTime = stopWatch.elapsedTime; - const props = await getActivationTelemetryProps(serviceContainer); - sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props); - } catch (ex) { - traceError('sendStartupTelemetry() failed.', ex); - } -} -function isUsingGlobalInterpreterInWorkspace(currentPythonPath: string, serviceContainer: IServiceContainer): boolean { - const service = serviceContainer.get(IInterpreterAutoSelectionService); - const globalInterpreter = service.getAutoSelectedInterpreter(undefined); - if (!globalInterpreter) { - return false; - } - return currentPythonPath === globalInterpreter.path; -} -function hasUserDefinedPythonPath(resource: Resource, serviceContainer: IServiceContainer) { - const workspaceService = serviceContainer.get(IWorkspaceService); - const settings = workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; - return ((settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || - (settings.workspaceValue && settings.workspaceValue !== 'python') || - (settings.globalValue && settings.globalValue !== 'python')) ? true : false; -} + setTimeout(async () => { + if (activatedServiceContainer) { + const workspaceService = activatedServiceContainer.get(IWorkspaceService); + if (workspaceService.isTrusted) { + const interpreterManager = activatedServiceContainer.get(IInterpreterService); + const workspaces = workspaceService.workspaceFolders ?? []; + await interpreterManager + .refresh(workspaces.length > 0 ? workspaces[0].uri : undefined) + .catch((ex) => traceError('Python Extension: interpreterManager.refresh', ex)); + } + } -function getPreferredWorkspaceInterpreter(resource: Resource, serviceContainer: IServiceContainer) { - const workspaceInterpreterSelector = serviceContainer.get(IInterpreterAutoSelectionRule, AutoSelectionRule.workspaceVirtualEnvs); - const interpreter = workspaceInterpreterSelector.getPreviouslyAutoSelectedInterpreter(resource); - return interpreter ? interpreter.path : undefined; + runAfterActivation(); + }); + + const api = buildApi( + activationPromise, + ext.legacyIOC.serviceManager, + ext.legacyIOC.serviceContainer, + components.pythonEnvs, + ); + const proposedApi = buildProposedApi(components.pythonEnvs, ext.legacyIOC.serviceContainer); + registerTools(context, components.pythonEnvs, api.environments, ext.legacyIOC.serviceContainer); + ext.legacyIOC.serviceContainer + .get(IRecommendedEnvironmentService) + .registerEnvApi(api.environments); + return [{ ...api, ...proposedApi }, activationPromise, ext.legacyIOC.serviceContainer]; } -///////////////////////////// -// telemetry - -// tslint:disable-next-line:no-any -async function getActivationTelemetryProps(serviceContainer: IServiceContainer): Promise { - // tslint:disable-next-line:no-suspicious-comment - // TODO: Not all of this data is showing up in the database... - // tslint:disable-next-line:no-suspicious-comment - // TODO: If any one of these parts fails we send no info. We should - // be able to partially populate as much as possible instead - // (through granular try-catch statements). - const terminalHelper = serviceContainer.get(ITerminalHelper); - const terminalShellType = terminalHelper.identifyTerminalShell(); - const condaLocator = serviceContainer.get(ICondaService); - const interpreterService = serviceContainer.get(IInterpreterService); - const workspaceService = serviceContainer.get(IWorkspaceService); - const configurationService = serviceContainer.get(IConfigurationService); - const mainWorkspaceUri = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders![0].uri : undefined; - const settings = configurationService.getSettings(mainWorkspaceUri); - const [condaVersion, interpreter, interpreters] = await Promise.all([ - condaLocator.getCondaVersion().then(ver => ver ? ver.raw : '').catch(() => ''), - interpreterService.getActiveInterpreter().catch(() => undefined), - interpreterService.getInterpreters(mainWorkspaceUri).catch(() => []) - ]); - const workspaceFolderCount = workspaceService.hasWorkspaceFolders ? workspaceService.workspaceFolders!.length : 0; - const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; - const interpreterType = interpreter ? interpreter.type : undefined; - const usingUserDefinedInterpreter = hasUserDefinedPythonPath(mainWorkspaceUri, serviceContainer); - const preferredWorkspaceInterpreter = getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer); - const usingGlobalInterpreter = isUsingGlobalInterpreterInWorkspace(settings.pythonPath, serviceContainer); - const usingAutoSelectedWorkspaceInterpreter = preferredWorkspaceInterpreter ? settings.pythonPath === getPreferredWorkspaceInterpreter(mainWorkspaceUri, serviceContainer) : false; - const hasPython3 = interpreters - .filter(item => item && item.version ? item.version.major === 3 : false) - .length > 0; - - return { - condaVersion, - terminal: terminalShellType, - pythonVersion, - interpreterType, - workspaceFolderCount, - hasPython3, - usingUserDefinedInterpreter, - usingAutoSelectedWorkspaceInterpreter, - usingGlobalInterpreter - }; +function displayProgress(promise: Promise) { + const progressOptions: ProgressOptions = { location: ProgressLocation.Window, title: Common.loadingExtension }; + window.withProgress(progressOptions, () => promise); } ///////////////////////////// // error handling -function handleError(ex: Error) { - notifyUser('Extension activation failed, run the \'Developer: Toggle Developer Tools\' command for more information.'); +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); - sendErrorTelemetry(ex) - .ignoreErrors(); + + await sendErrorTelemetry(ex, startupDurations, activatedServiceContainer); } interface IAppShell { @@ -409,32 +201,12 @@ interface IAppShell { function notifyUser(msg: string) { try { - // tslint:disable-next-line:no-any - let appShell: IAppShell = (window as any as IAppShell); + let appShell: IAppShell = (window as any) as IAppShell; if (activatedServiceContainer) { - // tslint:disable-next-line:no-any - appShell = activatedServiceContainer.get(IApplicationShell) as any as IAppShell; + appShell = (activatedServiceContainer.get(IApplicationShell) as any) as IAppShell; } - appShell.showErrorMessage(msg) - .ignoreErrors(); + appShell.showErrorMessage(msg).ignoreErrors(); } catch (ex) { - // ignore - } -} - -async function sendErrorTelemetry(ex: Error) { - try { - // tslint:disable-next-line:no-any - let props: any = {}; - if (activatedServiceContainer) { - try { - props = await getActivationTelemetryProps(activatedServiceContainer); - } catch (ex) { - // ignore - } - } - sendTelemetryEvent(EventName.EDITOR_LOAD, durations, props, ex); - } catch (exc2) { - traceError('sendErrorTelemetry() failed.', exc2); + traceError('Failed to Notify User', ex); } } 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 { + // Note that each activation returns a promise that resolves + // when that activation completes. However, it might have started + // some non-critical background operations that do not block + // extension activation but do block use of the extension "API". + // Each component activation can't just resolve an "inner" promise + // for those non-critical operations because `await` (and + // `Promise.all()`, etc.) will flatten nested promises. Thus + // activation resolves `ActivationResult`, which can safely wrap + // the "inner" promise. + + // TODO: As of now activateLegacy() registers various classes which might + // be required while activating components. Once registration from + // activateLegacy() are moved before we activate other components, we can + // activate them in parallel with the other components. + // https://github.com/microsoft/vscode-python/issues/15380 + // These will go away eventually once everything is refactored into components. + const legacyActivationResult = await activateLegacy(ext, startupStopWatch); + const workspaceService = new WorkspaceService(); + if (!workspaceService.isTrusted) { + return [legacyActivationResult]; + } + const promises: Promise[] = [ + // More component activations will go here + pythonEnvironments.activate(components.pythonEnvs, ext), + ]; + return Promise.all([legacyActivationResult, ...promises]); +} + +export function activateFeatures(ext: ExtensionState, _components: Components): void { + const interpreterQuickPick: IInterpreterQuickPick = ext.legacyIOC.serviceContainer.get( + IInterpreterQuickPick, + ); + const interpreterService: IInterpreterService = ext.legacyIOC.serviceContainer.get( + IInterpreterService, + ); + const pathUtils = ext.legacyIOC.serviceContainer.get(IPathUtils); + registerPixiFeatures(ext.disposables); + registerAllCreateEnvironmentFeatures( + ext.disposables, + interpreterQuickPick, + ext.legacyIOC.serviceContainer.get(IPythonPathUpdaterServiceManager), + interpreterService, + pathUtils, + ); + const executionHelper = ext.legacyIOC.serviceContainer.get(ICodeExecutionHelper); + const commandManager = ext.legacyIOC.serviceContainer.get(ICommandManager); + registerTriggerForTerminalREPL(ext.disposables); + registerStartNativeReplCommand(ext.disposables, interpreterService); + registerReplCommands(ext.disposables, interpreterService, executionHelper, commandManager); + registerReplExecuteOnEnter(ext.disposables, interpreterService, commandManager); + registerCustomTerminalLinkProvider(ext.disposables); +} + +/// ////////////////////////// +// old activation code + +// 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 { + 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); + await setExtensionInstallTelemetryProperties(fs); + + const applicationEnv = serviceManager.get(IApplicationEnvironment); + const { enableProposedApi } = applicationEnv.packageJson; + serviceManager.addSingletonInstance(UseProposedApi, enableProposedApi); + // Feature specific registrations. + installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + debugConfigurationRegisterTypes(serviceManager); + tensorBoardRegisterTypes(serviceManager); + + const extensions = serviceContainer.get(IExtensions); + await setDefaultLanguageServer(extensions, serviceManager); + + // Settings are dependent on Experiment service, so we need to initialize it after experiments are activated. + serviceContainer.get(IConfigurationService).getSettings().register(); + + // Language feature registrations. + appRegisterTypes(serviceManager); + providersRegisterTypes(serviceManager); + activationRegisterTypes(serviceManager); + + // "initialize" "services" + + const disposables = serviceManager.get(IDisposableRegistry); + const workspaceService = serviceContainer.get(IWorkspaceService); + const cmdManager = serviceContainer.get(ICommandManager); + + languages.setLanguageConfiguration(PYTHON_LANGUAGE, getLanguageConfiguration()); + if (workspaceService.isTrusted) { + const interpreterManager = serviceContainer.get(IInterpreterService); + interpreterManager.initialize(); + if (!workspaceService.isVirtualWorkspace) { + const handlers = serviceManager.getAll(IDebugSessionEventHandlers); + const dispatcher = new DebugSessionEventDispatcher(handlers, DebugService.instance, disposables); + dispatcher.registerEventHandlers(); + const outputChannel = serviceManager.get(ILogOutputChannel); + disposables.push(cmdManager.registerCommand(Commands.ViewOutput, () => outputChannel.show())); + cmdManager.executeCommand('setContext', 'python.vscode.channel', applicationEnv.channel).then(noop, noop); + + serviceContainer.get(IApplicationDiagnostics).register(); + + serviceManager.get(ITerminalAutoActivation).register(); + + await registerPythonStartup(ext.context); + + serviceManager.get(ICodeExecutionManager).registerCommands(); + + disposables.push(new ReplProvider(serviceContainer)); + + const terminalProvider = new TerminalProvider(serviceContainer); + terminalProvider.initialize(window.activeTerminal).ignoreErrors(); + + serviceContainer + .getAll(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); + 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, serviceContainer); + serviceManager.addSingletonInstance(IServiceManager, serviceManager); + + serviceManager.addSingletonInstance(IDisposableRegistry, disposables); + serviceManager.addSingletonInstance(IMemento, context.globalState, GLOBAL_MEMENTO); + serviceManager.addSingletonInstance(IMemento, context.workspaceState, WORKSPACE_MEMENTO); + serviceManager.addSingletonInstance(IExtensionContext, context); + + const standardOutputChannel = window.createOutputChannel(OutputChannelNames.python, { log: true }); + disposables.push(standardOutputChannel); + disposables.push(registerLogger(new OutputChannelLogger(standardOutputChannel))); + + serviceManager.addSingletonInstance(ILogOutputChannel, standardOutputChannel); + + return { + context, + disposables, + legacyIOC: { serviceManager, serviceContainer }, + }; +} + +/** + * Registers standard utils like experiment and platform code which are fundamental to the extension. + */ +export function initializeStandard(ext: ExtensionState): void { + const { serviceManager } = ext.legacyIOC; + // Core registrations (non-feature specific). + commonRegisterTypes(serviceManager); + variableRegisterTypes(serviceManager); + platformRegisterTypes(serviceManager); + processRegisterTypes(serviceManager); + interpretersRegisterTypes(serviceManager); + + // We will be pulling other code over from activateLegacy(). +} + +/** + * The set of public APIs from initialized components. + */ +export type Components = { + pythonEnvs: IDiscoveryAPI; +}; + +/** + * Initialize all components in the extension. + */ +export async function initializeComponents(ext: ExtensionState): Promise { + const pythonEnvs = await pythonEnvironments.initialize(ext); + + // Other component initializers go here. + // We will be factoring them out of activateLegacy(). + + return { + pythonEnvs, + }; +} diff --git a/src/client/formatters/autoPep8Formatter.ts b/src/client/formatters/autoPep8Formatter.ts deleted file mode 100644 index c178f68c17e6..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 { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class AutoPep8Formatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('autopep8', Product.autopep8, serviceContainer); - } - - public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer.get(IConfigurationService).getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.autopep8Args) && settings.formatting.autopep8Args.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const autoPep8Args = ['--diff']; - if (formatSelection) { - // tslint:disable-next-line:no-non-null-assertion - autoPep8Args.push(...['--line-range', (range!.start.line + 1).toString(), (range!.end.line + 1).toString()]); - } - const promise = super.provideDocumentFormattingEdits(document, options, token, autoPep8Args); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'autopep8', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/formatters/baseFormatter.ts b/src/client/formatters/baseFormatter.ts deleted file mode 100644 index 8777074ef2e4..000000000000 --- a/src/client/formatters/baseFormatter.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as fs from 'fs-extra'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../common/constants'; -import '../common/extensions'; -import { isNotInstalledError } from '../common/helpers'; -import { IPythonToolExecutionService } from '../common/process/types'; -import { IDisposableRegistry, 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(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.helper = serviceContainer.get(IFormatterHelper); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public abstract formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable; - protected getDocumentPath(document: vscode.TextDocument, fallbackPath: string) { - if (path.basename(document.uri.fsPath) === document.uri.fsPath) { - return fallbackPath; - } - return path.dirname(document.fileName); - } - protected getWorkspaceUri(document: vscode.TextDocument) { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const folders = this.workspace.workspaceFolders; - if (Array.isArray(folders) && folders.length > 0) { - return folders[0].uri; - } - return vscode.Uri.file(__dirname); - } - protected async provideDocumentFormattingEdits(document: vscode.TextDocument, _options: vscode.FormattingOptions, token: vscode.CancellationToken, args: string[], cwd?: string): Promise { - if (typeof cwd !== 'string' || cwd.length === 0) { - cwd = this.getWorkspaceUri(document).fsPath; - } - - // autopep8 and yapf have the ability to read from the process input stream and return the formatted code out of the output stream. - // However they don't support returning the diff of the formatted text when reading data from the input stream. - // Yet getting text formatted that way avoids having to create a temporary file, however the diffing will have - // to be done here in node (extension), i.e. extension CPU, i.e. less responsive solution. - const tempFile = await this.createTempFile(document); - if (this.checkCancellation(document.fileName, tempFile, token)) { - return []; - } - - const executionInfo = this.helper.getExecutionInfo(this.product, args, document.uri); - executionInfo.args.push(tempFile); - const pythonToolsExecutionService = this.serviceContainer.get(IPythonToolExecutionService); - const promise = pythonToolsExecutionService.exec(executionInfo, { cwd, throwOnStdErr: false, token }, document.uri) - .then(output => output.stdout) - .then(data => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - return getTextEditsFromPatch(document.getText(), data); - }) - .catch(error => { - if (this.checkCancellation(document.fileName, tempFile, token)) { - return [] as vscode.TextEdit[]; - } - // tslint:disable-next-line:no-empty - this.handleError(this.Id, error, document.uri).catch(() => { }); - return [] as vscode.TextEdit[]; - }) - .then(edits => { - this.deleteTempFile(document.fileName, tempFile).ignoreErrors(); - return edits; - }); - - const appShell = this.serviceContainer.get(IApplicationShell); - const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); - const disposable = appShell.setStatusBarMessage(`Formatting with ${this.Id}`, promise); - disposableRegistry.push(disposable); - return promise; - } - - protected async handleError(_expectedFileName: string, error: Error, resource?: vscode.Uri) { - let customError = `Formatting with ${this.Id} failed.`; - - if (isNotInstalledError(error)) { - const installer = this.serviceContainer.get(IInstaller); - const isInstalled = await installer.isInstalled(this.product, resource); - if (!isInstalled) { - customError += `\nYou could either install the '${this.Id}' formatter, turn it off or use another formatter.`; - installer.promptToInstall(this.product, resource).catch(ex => console.error('Python Extension: promptToInstall', ex)); - } - } - - this.outputChannel.appendLine(`\n${customError}\n${error}`); - } - - private async createTempFile(document: vscode.TextDocument): Promise { - return document.isDirty - ? getTempFileWithDocumentContents(document) - : document.fileName; - } - - private deleteTempFile(originalFile: string, tempFile: string): Promise { - 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 03215b4912fb..000000000000 --- a/src/client/formatters/blackFormatter.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as vscode from 'vscode'; -import { IApplicationShell } from '../common/application/types'; -import { Product } from '../common/installer/productInstaller'; -import { IConfigurationService } from '../common/types'; -import { noop } from '../common/utils/misc'; -import { StopWatch } from '../common/utils/stopWatch'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryWhenDone } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class BlackFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('black', Product.black, serviceContainer); - } - - public async formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Promise { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer.get(IConfigurationService).getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.blackArgs) && settings.formatting.blackArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - if (formatSelection) { - const shell = this.serviceContainer.get(IApplicationShell); - // Black does not support partial formatting on purpose. - shell.showErrorMessage('Black does not support the "Format Selection" command').then(noop, noop); - return []; - } - - const blackArgs = ['--diff', '--quiet']; - const promise = super.provideDocumentFormattingEdits(document, options, token, blackArgs); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'black', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/formatters/dummyFormatter.ts b/src/client/formatters/dummyFormatter.ts deleted file mode 100644 index 5613fe8186c6..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 { - 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).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 e79e1f932571..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 = new TextRangeCollection([]); - private braceCounter = new BraceCounter(); - private text = ''; - private document?: TextDocument; - private lineNumber = 0; - - // tslint:disable-next-line:cyclomatic-complexity - public formatLine(document: TextDocument, lineNumber: number): string { - this.document = document; - this.lineNumber = lineNumber; - this.text = document.lineAt(lineNumber).text; - this.tokens = new Tokenizer().tokenize(this.text); - this.builder = new TextBuilder(); - this.braceCounter = new BraceCounter(); - - if (this.tokens.count === 0) { - return this.text; - } - - const ws = this.text.substr(0, this.tokens.getItemAt(0).start); - if (ws.length > 0) { - this.builder.append(ws); // Preserve leading indentation. - } - - for (let i = 0; i < this.tokens.count; i += 1) { - const t = this.tokens.getItemAt(i); - const prev = i > 0 ? this.tokens.getItemAt(i - 1) : undefined; - const next = i < this.tokens.count - 1 ? this.tokens.getItemAt(i + 1) : undefined; - - switch (t.type) { - case TokenType.Operator: - this.handleOperator(i); - break; - - case TokenType.Comma: - this.builder.append(','); - if (next && !this.isCloseBraceType(next.type) && next.type !== TokenType.Colon) { - this.builder.softAppendSpace(); - } - break; - - case TokenType.Identifier: - if (prev && !this.isOpenBraceType(prev.type) && prev.type !== TokenType.Colon && prev.type !== TokenType.Operator) { - this.builder.softAppendSpace(); - } - const id = this.text.substring(t.start, t.end); - this.builder.append(id); - if (this.isKeywordWithSpaceBeforeBrace(id) && next && this.isOpenBraceType(next.type)) { - // for x in () - this.builder.softAppendSpace(); - } - break; - - case TokenType.Colon: - // x: 1 if not in slice, x[1:y] if inside the slice. - this.builder.append(':'); - if (!this.braceCounter.isOpened(TokenType.OpenBracket) && (next && next.type !== TokenType.Colon)) { - // Not inside opened [[ ... ] sequence. - this.builder.softAppendSpace(); - } - break; - - case TokenType.Comment: - // Add 2 spaces before in-line comment per PEP guidelines. - if (prev) { - this.builder.softAppendSpace(2); - } - this.builder.append(this.text.substring(t.start, t.end)); - break; - - case TokenType.Semicolon: - this.builder.append(';'); - break; - - default: - this.handleOther(t, i); - break; - } - } - return this.builder.getText(); - } - - // tslint:disable-next-line:cyclomatic-complexity - private handleOperator(index: number): void { - const t = this.tokens.getItemAt(index); - const prev = index > 0 ? this.tokens.getItemAt(index - 1) : undefined; - const opCode = this.text.charCodeAt(t.start); - const next = index < this.tokens.count - 1 ? this.tokens.getItemAt(index + 1) : undefined; - - if (t.length === 1) { - switch (opCode) { - case Char.Equal: - this.handleEqual(t, index); - return; - case Char.Period: - if (prev && this.isKeyword(prev, 'from')) { - this.builder.softAppendSpace(); - } - this.builder.append('.'); - if (next && this.isKeyword(next, 'import')) { - this.builder.softAppendSpace(); - } - return; - case Char.At: - if (prev) { - // Binary case - this.builder.softAppendSpace(); - this.builder.append('@'); - this.builder.softAppendSpace(); - } else { - this.builder.append('@'); - } - return; - case Char.ExclamationMark: - this.builder.append('!'); - return; - case Char.Asterisk: - if (prev && this.isKeyword(prev, 'lambda')) { - this.builder.softAppendSpace(); - this.builder.append('*'); - return; - } - if (this.handleStarOperator(t, prev!)) { - return; - } - break; - default: - break; - } - } else if (t.length === 2) { - if (this.text.charCodeAt(t.start) === Char.Asterisk && this.text.charCodeAt(t.start + 1) === Char.Asterisk) { - if (this.handleStarOperator(t, prev!)) { - return; - } - } - } - - // Do not append space if operator is preceded by '(' or ',' as in foo(**kwarg) - if (prev && (this.isOpenBraceType(prev.type) || prev.type === TokenType.Comma)) { - this.builder.append(this.text.substring(t.start, t.end)); - return; - } - - this.builder.softAppendSpace(); - this.builder.append(this.text.substring(t.start, t.end)); - - // Check unary case - if (prev && prev.type === TokenType.Operator) { - if (opCode === Char.Hyphen || opCode === Char.Plus || opCode === Char.Tilde) { - return; - } - } - this.builder.softAppendSpace(); - } - - private handleStarOperator(current: IToken, prev: IToken): boolean { - if (this.text.charCodeAt(current.start) === Char.Asterisk && this.text.charCodeAt(current.start + 1) === Char.Asterisk) { - if (!prev || (prev.type !== TokenType.Identifier && prev.type !== TokenType.Number)) { - this.builder.append('**'); - return true; - } - if (prev && this.isKeyword(prev, 'lambda')) { - this.builder.softAppendSpace(); - this.builder.append('**'); - return true; - } - } - // Check previous line for the **/* condition - const lastLine = this.getPreviousLineTokens(); - const lastToken = lastLine && lastLine.count > 0 ? lastLine.getItemAt(lastLine.count - 1) : undefined; - if (lastToken && (this.isOpenBraceType(lastToken.type) || lastToken.type === TokenType.Comma)) { - this.builder.append(this.text.substring(current.start, current.end)); - return true; - } - return false; - } - - private handleEqual(_t: IToken, index: number): void { - if (this.isMultipleStatements(index) && !this.braceCounter.isOpened(TokenType.OpenBrace)) { - // x = 1; x, y = y, x - this.builder.softAppendSpace(); - this.builder.append('='); - this.builder.softAppendSpace(); - return; - } - - // Check if this is = in function arguments. If so, do not add spaces around it. - if (this.isEqualsInsideArguments(index)) { - this.builder.append('='); - return; - } - - this.builder.softAppendSpace(); - this.builder.append('='); - this.builder.softAppendSpace(); - } - - private handleOther(t: IToken, index: number): void { - if (this.isBraceType(t.type)) { - this.braceCounter.countBrace(t); - this.builder.append(this.text.substring(t.start, t.end)); - return; - } - - const prev = index > 0 ? this.tokens.getItemAt(index - 1) : undefined; - if (prev && prev.length === 1 && this.text.charCodeAt(prev.start) === Char.Equal && this.isEqualsInsideArguments(index - 1)) { - // Don't add space around = inside function arguments. - this.builder.append(this.text.substring(t.start, t.end)); - return; - } - - if (prev && (this.isOpenBraceType(prev.type) || prev.type === TokenType.Colon)) { - // Don't insert space after (, [ or { . - this.builder.append(this.text.substring(t.start, t.end)); - return; - } - - if (t.type === TokenType.Number && prev && prev.type === TokenType.Operator && prev.length === 1 && this.text.charCodeAt(prev.start) === Char.Tilde) { - // Special case for ~ before numbers - this.builder.append(this.text.substring(t.start, t.end)); - return; - } - - if (t.type === TokenType.Unknown) { - this.handleUnknown(t); - } else { - // In general, keep tokens separated. - this.builder.softAppendSpace(); - this.builder.append(this.text.substring(t.start, t.end)); - } - } - - private handleUnknown(t: IToken): void { - const prevChar = t.start > 0 ? this.text.charCodeAt(t.start - 1) : 0; - if (prevChar === Char.Space || prevChar === Char.Tab) { - this.builder.softAppendSpace(); - } - this.builder.append(this.text.substring(t.start, t.end)); - - const nextChar = t.end < this.text.length - 1 ? this.text.charCodeAt(t.end) : 0; - if (nextChar === Char.Space || nextChar === Char.Tab) { - this.builder.softAppendSpace(); - } - } - - // tslint:disable-next-line:cyclomatic-complexity - private isEqualsInsideArguments(index: number): boolean { - if (index < 1) { - return false; - } - - // We are looking for IDENT = ? - const prev = this.tokens.getItemAt(index - 1); - if (prev.type !== TokenType.Identifier) { - return false; - } - - if (index > 1 && this.tokens.getItemAt(index - 2).type === TokenType.Colon) { - return false; // Type hint should have spaces around like foo(x: int = 1) per PEP 8 - } - - return this.isInsideFunctionArguments(this.tokens.getItemAt(index).start); - } - - private isOpenBraceType(type: TokenType): boolean { - return type === TokenType.OpenBrace || type === TokenType.OpenBracket || type === TokenType.OpenCurly; - } - private isCloseBraceType(type: TokenType): boolean { - return type === TokenType.CloseBrace || type === TokenType.CloseBracket || type === TokenType.CloseCurly; - } - private isBraceType(type: TokenType): boolean { - return this.isOpenBraceType(type) || this.isCloseBraceType(type); - } - - private isMultipleStatements(index: number): boolean { - for (let i = index; i >= 0; i -= 1) { - if (this.tokens.getItemAt(i).type === TokenType.Semicolon) { - return true; - } - } - return false; - } - - private isKeywordWithSpaceBeforeBrace(s: string): boolean { - return keywordsWithSpaceBeforeBrace.indexOf(s) >= 0; - } - private isKeyword(t: IToken, keyword: string): boolean { - return t.type === TokenType.Identifier && t.length === keyword.length && this.text.substr(t.start, t.length) === keyword; - } - - // tslint:disable-next-line:cyclomatic-complexity - private isInsideFunctionArguments(position: number): boolean { - if (!this.document) { - return false; // unable to determine - } - - // Walk up until beginning of the document or line with 'def IDENT(' or line ending with : - // IDENT( by itself is not reliable since they can be nested in IDENT(IDENT(a), x=1) - let start = new Position(0, 0); - for (let i = this.lineNumber; i >= 0; i -= 1) { - const line = this.document.lineAt(i); - const lineTokens = new Tokenizer().tokenize(line.text); - if (lineTokens.count === 0) { - continue; - } - // 'def IDENT(' - const first = lineTokens.getItemAt(0); - if (lineTokens.count >= 3 && - first.length === 3 && line.text.substr(first.start, first.length) === 'def' && - lineTokens.getItemAt(1).type === TokenType.Identifier && - lineTokens.getItemAt(2).type === TokenType.OpenBrace) { - start = line.range.start; - break; - } - - if (lineTokens.count > 0 && i < this.lineNumber) { - // One of previous lines ends with : - const last = lineTokens.getItemAt(lineTokens.count - 1); - if (last.type === TokenType.Colon) { - start = this.document.lineAt(i + 1).range.start; - break; - } else if (lineTokens.count > 1) { - const beforeLast = lineTokens.getItemAt(lineTokens.count - 2); - if (beforeLast.type === TokenType.Colon && last.type === TokenType.Comment) { - start = this.document.lineAt(i + 1).range.start; - break; - } - } - } - } - - // Now tokenize from the nearest reasonable point - const currentLine = this.document.lineAt(this.lineNumber); - const text = this.document.getText(new Range(start, currentLine.range.end)); - const tokens = new Tokenizer().tokenize(text); - - // Translate position in the line being formatted to the position in the tokenized block - position = this.document.offsetAt(currentLine.range.start) + position - this.document.offsetAt(start); - - // Walk tokens locating narrowest function signature as in IDENT( | ) - let funcCallStartIndex = -1; - let funcCallEndIndex = -1; - for (let i = 0; i < tokens.count - 1; i += 1) { - const t = tokens.getItemAt(i); - if (t.type === TokenType.Identifier) { - const next = tokens.getItemAt(i + 1); - if (next.type === TokenType.OpenBrace && !this.isKeywordWithSpaceBeforeBrace(text.substr(t.start, t.length))) { - // We are at IDENT(, try and locate the closing brace - let closeBraceIndex = this.findClosingBrace(tokens, i + 1); - // Closing brace is not required in case construct is not yet terminated - closeBraceIndex = closeBraceIndex > 0 ? closeBraceIndex : tokens.count - 1; - // Are we in range? - if (position > next.start && position < tokens.getItemAt(closeBraceIndex).start) { - funcCallStartIndex = i; - funcCallEndIndex = closeBraceIndex; - } - } - } - } - // Did we find anything? - if (funcCallStartIndex < 0) { - // No? See if we are between 'lambda' and ':' - for (let i = 0; i < tokens.count; i += 1) { - const t = tokens.getItemAt(i); - if (t.type === TokenType.Identifier && text.substr(t.start, t.length) === 'lambda') { - if (position < t.start) { - break; // Position is before the nearest 'lambda' - } - let colonIndex = this.findNearestColon(tokens, i + 1); - // Closing : is not required in case construct is not yet terminated - colonIndex = colonIndex > 0 ? colonIndex : tokens.count - 1; - if (position > t.start && position < tokens.getItemAt(colonIndex).start) { - funcCallStartIndex = i; - funcCallEndIndex = colonIndex; - } - } - } - } - return funcCallStartIndex >= 0 && funcCallEndIndex > 0; - } - - private findNearestColon(tokens: ITextRangeCollection, index: number): number { - for (let i = index; i < tokens.count; i += 1) { - if (tokens.getItemAt(i).type === TokenType.Colon) { - return i; - } - } - return -1; - } - - private findClosingBrace(tokens: ITextRangeCollection, index: number): number { - const braceCounter = new BraceCounter(); - for (let i = index; i < tokens.count; i += 1) { - const t = tokens.getItemAt(i); - if (t.type === TokenType.OpenBrace || t.type === TokenType.CloseBrace) { - braceCounter.countBrace(t); - } - if (braceCounter.count === 0) { - return i; - } - } - return -1; - } - - private getPreviousLineTokens(): ITextRangeCollection | undefined { - if (!this.document || this.lineNumber === 0) { - return undefined; // unable to determine - } - const line = this.document.lineAt(this.lineNumber - 1); - return new Tokenizer().tokenize(line.text); - } -} diff --git a/src/client/formatters/serviceRegistry.ts b/src/client/formatters/serviceRegistry.ts deleted file mode 100644 index 196e6c806b5f..000000000000 --- a/src/client/formatters/serviceRegistry.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IServiceManager } from '../ioc/types'; -import { FormatterHelper } from './helper'; -import { IFormatterHelper } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IFormatterHelper, FormatterHelper); -} diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts deleted file mode 100644 index 7f4bcf5b7524..000000000000 --- a/src/client/formatters/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { Uri } from 'vscode'; -import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; - -export const IFormatterHelper = Symbol('IFormatterHelper'); - -export type FormatterId = 'autopep8' | 'black' | 'yapf'; - -export type FormatterSettingsPropertyNames = { - argsName: keyof IFormattingSettings; - pathName: keyof IFormattingSettings; -}; - -export interface IFormatterHelper { - translateToId(formatter: Product): FormatterId; - getSettingsPropertyNames(formatter: Product): FormatterSettingsPropertyNames; - getExecutionInfo(formatter: Product, customArgs: string[], resource?: Uri): ExecutionInfo; -} diff --git a/src/client/formatters/yapfFormatter.ts b/src/client/formatters/yapfFormatter.ts deleted file mode 100644 index f85b94ed090d..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 { EventName } from '../telemetry/constants'; -import { BaseFormatter } from './baseFormatter'; - -export class YapfFormatter extends BaseFormatter { - constructor(serviceContainer: IServiceContainer) { - super('yapf', Product.yapf, serviceContainer); - } - - public formatDocument(document: vscode.TextDocument, options: vscode.FormattingOptions, token: vscode.CancellationToken, range?: vscode.Range): Thenable { - const stopWatch = new StopWatch(); - const settings = this.serviceContainer.get(IConfigurationService).getSettings(document.uri); - const hasCustomArgs = Array.isArray(settings.formatting.yapfArgs) && settings.formatting.yapfArgs.length > 0; - const formatSelection = range ? !range.isEmpty : false; - - const yapfArgs = ['--diff']; - if (formatSelection) { - // tslint:disable-next-line:no-non-null-assertion - yapfArgs.push(...['--lines', `${range!.start.line + 1}-${range!.end.line + 1}`]); - } - // Yapf starts looking for config file starting from the file path. - const fallbarFolder = this.getWorkspaceUri(document).fsPath; - const cwd = this.getDocumentPath(document, fallbarFolder); - const promise = super.provideDocumentFormattingEdits(document, options, token, yapfArgs, cwd); - sendTelemetryWhenDone(EventName.FORMAT, promise, stopWatch, { tool: 'yapf', hasCustomArgs, formatSelection }); - return promise; - } -} diff --git a/src/client/interpreter/activation/service.ts b/src/client/interpreter/activation/service.ts index 15b6e4dfa79d..f47575cad60b 100644 --- a/src/client/interpreter/activation/service.ts +++ b/src/client/interpreter/activation/service.ts @@ -1,115 +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 { inject, injectable } from 'inversify'; import * as path from 'path'; +import { inject, injectable } from 'inversify'; -import { LogOptions, traceDecorators, traceError, traceVerbose } from '../../common/logger'; +import { IWorkspaceService } from '../../common/application/types'; +import { PYTHON_WARNINGS } from '../../common/constants'; import { IPlatformService } from '../../common/platform/types'; -import { IProcessServiceFactory } from '../../common/process/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 { - cacheResourceSpecificInterpreterData, - clearCachedResourceSpecificIngterpreterData -} from '../../common/utils/decorators'; +import { sleep } from '../../common/utils/async'; +import { InMemoryCache } from '../../common/utils/cacheUtils'; import { OSType } from '../../common/utils/platform'; -import { IEnvironmentVariablesProvider } from '../../common/variables/types'; -import { EXTENSION_ROOT_DIR } from '../../constants'; -import { captureTelemetry } from '../../telemetry'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { EnvironmentType, PythonEnvironment, virtualEnvTypes } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { PythonInterpreter } from '../contracts'; +import { IInterpreterService } from '../contracts'; import { IEnvironmentActivationService } from './types'; +import { TraceOptions } from '../../logging/types'; +import { + traceDecoratorError, + traceDecoratorVerbose, + traceError, + traceInfo, + traceVerbose, + traceWarn, +} from '../../logging'; +import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { cache } from '../../common/utils/decorators'; +import { getRunPixiPythonCommand } from '../../pythonEnvironments/common/environmentManagers/pixi'; -const getEnvironmentPrefix = 'e8b39361-0157-4923-80e1-22d70d46dee6'; -const cacheDuration = 10 * 60 * 1000; -const getEnvironmentTimeout = 30000; +const ENVIRONMENT_PREFIX = 'e8b39361-0157-4923-80e1-22d70d46dee6'; +const CACHE_DURATION = 10 * 60 * 1000; +const ENVIRONMENT_TIMEOUT = 30000; +const CONDA_ENVIRONMENT_TIMEOUT = 60_000; // The shell under which we'll execute activation scripts. -const defaultShells = { +export const defaultShells = { [OSType.Windows]: { shell: 'cmd', shellType: TerminalShellType.commandPrompt }, [OSType.OSX]: { shell: 'bash', shellType: TerminalShellType.bash }, [OSType.Linux]: { shell: 'bash', shellType: TerminalShellType.bash }, - [OSType.Unknown]: undefined + [OSType.Unknown]: undefined, }; +const condaRetryMessages = [ + 'The process cannot access the file because it is being used by another process', + 'The directory is not empty', +]; + +/** + * This class exists so that the environment variable fetching can be cached in between tests. Normally + * this cache resides in memory for the duration of the EnvironmentActivationService's lifetime, but in the case + * of our functional tests, we want the cached data to exist outside of each test (where each test will destroy the EnvironmentActivationService) + * This gives each test a 3 or 4 second speedup. + */ +export class EnvironmentActivationServiceCache { + private static useStatic = false; + + private static staticMap = new Map>(); + + private normalMap = new Map>(); + + public static forceUseStatic(): void { + EnvironmentActivationServiceCache.useStatic = true; + } + + public static forceUseNormal(): void { + EnvironmentActivationServiceCache.useStatic = false; + } + + public get(key: string): InMemoryCache | undefined { + if (EnvironmentActivationServiceCache.useStatic) { + return EnvironmentActivationServiceCache.staticMap.get(key); + } + return this.normalMap.get(key); + } + + public set(key: string, value: InMemoryCache): 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[] = []; - constructor(@inject(ITerminalHelper) private readonly helper: ITerminalHelper, + + 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(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider) { - - this.envVarsService.onDidEnvironmentVariablesChange(this.onDidEnvironmentVariablesChange, this, this.disposables); + @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()); + this.disposables.forEach((d) => d.dispose()); } - @traceDecorators.verbose('getActivatedEnvironmentVariables', LogOptions.Arguments) - @captureTelemetry(EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, { failed: false }, true) - @cacheResourceSpecificInterpreterData('ActivatedEnvironmentVariables', cacheDuration) - public async getActivatedEnvironmentVariables(resource: Resource, interpreter?: PythonInterpreter, allowExceptions?: boolean): Promise { + + @traceDecoratorVerbose('getActivatedEnvironmentVariables', TraceOptions.Arguments) + public async getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, + ): Promise { + const stopWatch = new StopWatch(); + // Cache key = resource + interpreter. + const workspaceKey = this.workspace.getWorkspaceFolderIdentifier(resource); + interpreter = interpreter ?? (await this.interpreterService.getActiveInterpreter(resource)); + const interpreterPath = this.platform.isWindows ? interpreter?.path.toLowerCase() : interpreter?.path; + const cacheKey = `${workspaceKey}_${interpreterPath}_${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(CACHE_DURATION); + return this.getActivatedEnvironmentVariablesImpl(resource, interpreter, allowExceptions, shell) + .then((vars) => { + memCache.data = vars; + this.activatedEnvVariablesCache.set(cacheKey, memCache); + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, + stopWatch.elapsedTime, + { failed: false }, + ); + return vars; + }) + .catch((ex) => { + sendTelemetryEvent( + EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES, + stopWatch.elapsedTime, + { failed: true }, + ); + throw ex; + }); + } + + @cache(-1, true) + public async getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise { + // Try to get the process environment variables using Python by printing variables, that can be little different + // from `process.env` and is preferred when calculating diff. + const globalInterpreters = this.interpreterService + .getInterpreters() + .filter((i) => !virtualEnvTypes.includes(i.envType)); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + try { + const [args, parse] = internalScripts.printEnvVariables(); + args.forEach((arg, i) => { + args[i] = arg.toCommandArgumentForPythonExt(); + }); + const command = `${interpreterPath} ${args.join(' ')}`; + const processService = await this.processServiceFactory.create(resource, { doNotUseCustomEnvs: true }); + const result = await processService.shellExec(command, { + shell, + timeout: ENVIRONMENT_TIMEOUT, + maxBuffer: 1000 * 1000, + throwOnStdErr: false, + }); + const returnedEnv = this.parseEnvironmentOutput(result.stdout, parse); + return returnedEnv ?? process.env; + } catch (ex) { + return process.env; + } + } + + public async getEnvironmentActivationShellCommands( + resource: Resource, + interpreter?: PythonEnvironment, + ): Promise { const shellInfo = defaultShells[this.platform.osType]; if (!shellInfo) { - return; + return []; } + return this.helper.getEnvironmentActivationShellCommands(resource, shellInfo.shellType, interpreter); + } + public async getActivatedEnvironmentVariablesImpl( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, + ): Promise { + let shellInfo = defaultShells[this.platform.osType]; + if (!shellInfo) { + return undefined; + } + if (shell) { + const customShellType = identifyShellFromShellPath(shell); + shellInfo = { shellType: customShellType, shell }; + } try { - const activationCommands = await this.helper.getEnvironmentActivationShellCommands(resource, shellInfo.shellType, interpreter); - traceVerbose(`Activation Commands received ${activationCommands} for shell ${shellInfo.shell}`); - if (!activationCommands || !Array.isArray(activationCommands) || activationCommands.length === 0) { - return; - } - - // Run the activate command collect the environment from it. - const activationCommand = this.fixActivationCommands(activationCommands).join(' && '); const processService = await this.processServiceFactory.create(resource); - const customEnvVars = await this.envVarsService.getEnvironmentVariables(resource); + const customEnvVars = (await this.envVarsService.getEnvironmentVariables(resource)) ?? {}; const hasCustomEnvVars = Object.keys(customEnvVars).length; - const env = hasCustomEnvVars ? customEnvVars : this.currentProcess.env; - traceVerbose(`${hasCustomEnvVars ? 'Has' : 'No'} Custom Env Vars`); + 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'; - // In order to make sure we know where the environment output is, - // put in a dummy echo we can look for - const printEnvPyFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'printEnvVariables.py'); - const command = `${activationCommand} && echo '${getEnvironmentPrefix}' && python ${printEnvPyFile.fileToCommandArgument()}`; traceVerbose(`Activating Environment to capture Environment variables, ${command}`); - // Conda activate can hang on certain systems. Fail after 30 seconds. + // 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. - const result = await processService.shellExec(command, { env, shell: shellInfo.shell, timeout: getEnvironmentTimeout, maxBuffer: 1000 * 1000 }); - if (result.stderr && result.stderr.length > 0) { - throw new Error(`StdErr from ShellExec, ${result.stderr}`); + // 2) Retry because of this issue here: https://github.com/microsoft/vscode-python/issues/9244 + // This happens on AzDo machines a bunch when using Conda (and we can't dictate the conda version in order to get the fix) + let result: ExecutionResult | undefined; + let tryCount = 1; + 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; + } + } } - return this.parseEnvironmentOutput(result.stdout); + + // 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; } - protected onDidEnvironmentVariablesChange(affectedResource: Resource) { - clearCachedResourceSpecificIngterpreterData('ActivatedEnvironmentVariables', affectedResource); - } - protected fixActivationCommands(commands: string[]): string[] { - // Replace 'source ' with '. ' as that works in shell exec - return commands.map(cmd => cmd.replace(/^source\s+/, '. ')); - } - @traceDecorators.error('Failed to parse Environment variables') - @traceDecorators.verbose('parseEnvironmentOutput', LogOptions.None) - protected parseEnvironmentOutput(output: string): NodeJS.ProcessEnv | undefined { - output = output.substring(output.indexOf(getEnvironmentPrefix) + getEnvironmentPrefix.length); + + // eslint-disable-next-line class-methods-use-this + @traceDecoratorError('Failed to parse Environment variables') + @traceDecoratorVerbose('parseEnvironmentOutput', TraceOptions.None) + private parseEnvironmentOutput(output: string, parse: (out: string) => NodeJS.ProcessEnv | undefined) { + if (output.indexOf(ENVIRONMENT_PREFIX) === -1) { + return parse(output); + } + output = output.substring(output.indexOf(ENVIRONMENT_PREFIX) + ENVIRONMENT_PREFIX.length); const js = output.substring(output.indexOf('{')).trim(); - return JSON.parse(js); + 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 index a493d4c677c2..e00ef9b62b3f 100644 --- a/src/client/interpreter/activation/types.ts +++ b/src/client/interpreter/activation/types.ts @@ -4,9 +4,20 @@ 'use strict'; import { Resource } from '../../common/types'; -import { PythonInterpreter } from '../contracts'; +import { EnvironmentVariables } from '../../common/variables/types'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; export const IEnvironmentActivationService = Symbol('IEnvironmentActivationService'); export interface IEnvironmentActivationService { - getActivatedEnvironmentVariables(resource: Resource, interpreter?: PythonInterpreter, allowExceptions?: boolean): Promise; + getProcessEnvironmentVariables(resource: Resource, shell?: string): Promise; + getActivatedEnvironmentVariables( + resource: Resource, + interpreter?: PythonEnvironment, + allowExceptions?: boolean, + shell?: string, + ): Promise; + getEnvironmentActivationShellCommands( + resource: Resource, + interpreter?: PythonEnvironment, + ): Promise; } diff --git a/src/client/interpreter/autoSelection/index.ts b/src/client/interpreter/autoSelection/index.ts index b21a056ff12c..5ad5362e8210 100644 --- a/src/client/interpreter/autoSelection/index.ts +++ b/src/client/interpreter/autoSelection/index.ts @@ -3,18 +3,22 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; -import { compare } from 'semver'; +import { inject, injectable } from 'inversify'; import { Event, EventEmitter, Uri } from 'vscode'; import { IWorkspaceService } from '../../common/application/types'; +import { DiscoveryUsingWorkers } from '../../common/experiments/groups'; import '../../common/extensions'; import { IFileSystem } from '../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; +import { IExperimentService, IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; import { createDeferred, Deferred } from '../../common/utils/async'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; +import { compareSemVerLikeVersions } from '../../pythonEnvironments/base/info/pythonVersion'; +import { ProgressReportStage } from '../../pythonEnvironments/base/locator'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { sendTelemetryEvent } from '../../telemetry'; import { EventName } from '../../telemetry/constants'; -import { IInterpreterHelper, PythonInterpreter } from '../contracts'; -import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from './types'; +import { IInterpreterComparer } from '../configuration/types'; +import { IInterpreterHelper, IInterpreterService } from '../contracts'; +import { IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService } from './types'; const preferredGlobalInterpreter = 'preferredGlobalPyInterpreter'; const workspacePathNameForGlobalWorkspaces = ''; @@ -22,68 +26,62 @@ const workspacePathNameForGlobalWorkspaces = ''; @injectable() export class InterpreterAutoSelectionService implements IInterpreterAutoSelectionService { protected readonly autoSelectedWorkspacePromises = new Map>(); + private readonly didAutoSelectedInterpreterEmitter = new EventEmitter(); - private readonly autoSelectedInterpreterByWorkspace = new Map(); - private globallyPreferredInterpreter!: IPersistentState; - private readonly rules: IInterpreterAutoSelectionRule[] = []; - constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + + private readonly autoSelectedInterpreterByWorkspace = new Map(); + + private globallyPreferredInterpreter: IPersistentState< + PythonEnvironment | undefined + > = this.stateFactory.createGlobalPersistentState( + preferredGlobalInterpreter, + undefined, + ); + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IPersistentStateFactory) private readonly stateFactory: IPersistentStateFactory, @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.systemWide) systemInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.currentPath) currentPathInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.windowsRegistry) winRegInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.cachedInterpreters) cachedPaths: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.settings) private readonly userDefinedInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.workspaceVirtualEnvs) workspaceInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSeletionProxyService) proxy: IInterpreterAutoSeletionProxyService, - @inject(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); } - @captureTelemetry(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { rule: AutoSelectionRule.all }, true) + + /** + * Auto-select a Python environment from the list returned by environment discovery. + * If there's a cached auto-selected environment -> return it. + */ public async autoSelectInterpreter(resource: Resource): Promise { const key = this.getWorkspacePathKey(resource); - if (!this.autoSelectedWorkspacePromises.has(key)) { + const useCachedInterpreter = this.autoSelectedWorkspacePromises.has(key); + + if (!useCachedInterpreter) { const deferred = createDeferred(); this.autoSelectedWorkspacePromises.set(key, deferred); + await this.initializeStore(resource); await this.clearWorkspaceStoreIfInvalid(resource); - await this.userDefinedInterpreter.autoSelectInterpreter(resource, this); - this.didAutoSelectedInterpreterEmitter.fire(); - Promise.all(this.rules.map(item => item.autoSelectInterpreter(resource))).ignoreErrors(); + await this.autoselectInterpreterWithLocators(resource); + deferred.resolve(); } + + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, undefined, { + useCachedInterpreter, + }); + return this.autoSelectedWorkspacePromises.get(key)!.promise; } + public get onDidChangeAutoSelectedInterpreter(): Event { return this.didAutoSelectedInterpreterEmitter.event; } - public getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { + + public getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined { // Do not execute anycode other than fetching fromm a property. // This method gets invoked from settings class, and this class in turn uses classes that relies on settings. // I.e. we can end up in a recursive loop. @@ -99,34 +97,36 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio return this.globallyPreferredInterpreter.value; } - public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined) { - // We can only update the stored interpreter once we have done the necessary - // work of auto selecting the interpreters. - if (!this.autoSelectedWorkspacePromises.has(this.getWorkspacePathKey(resource)) || - !this.autoSelectedWorkspacePromises.get(this.getWorkspacePathKey(resource))!.completed) { - return; - } + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise { await this.storeAutoSelectedInterpreter(resource, interpreter); } - public async setGlobalInterpreter(interpreter: PythonInterpreter) { + + public async setGlobalInterpreter(interpreter: PythonEnvironment): Promise { await this.storeAutoSelectedInterpreter(undefined, interpreter); } - protected async clearWorkspaceStoreIfInvalid(resource: Resource) { + + protected async clearWorkspaceStoreIfInvalid(resource: Resource): Promise { const stateStore = this.getWorkspaceState(resource); - if (stateStore && stateStore.value && !await this.fs.fileExists(stateStore.value.path)) { - sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, {}, { interpreterMissing: true }); + if (stateStore && stateStore.value && !(await this.fs.fileExists(stateStore.value.path))) { await stateStore.updateValue(undefined); } } - protected async storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { + + protected async storeAutoSelectedInterpreter( + resource: Resource, + interpreter: PythonEnvironment | undefined, + ): Promise { const workspaceFolderPath = this.getWorkspacePathKey(resource); if (workspaceFolderPath === workspacePathNameForGlobalWorkspaces) { // Update store only if this version is better. - 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; } @@ -141,7 +141,8 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio this.autoSelectedInterpreterByWorkspace.set(workspaceFolderPath, interpreter); } } - protected async initializeStore(resource: Resource) { + + protected async initializeStore(resource: Resource): Promise { const workspaceFolderPath = this.getWorkspacePathKey(resource); // Since we're initializing for this resource, // Ensure any cached information for this workspace have been removed. @@ -151,21 +152,107 @@ export class InterpreterAutoSelectionService implements IInterpreterAutoSelectio } await this.clearStoreIfFileIsInvalid(); } + private async clearStoreIfFileIsInvalid() { - this.globallyPreferredInterpreter = this.stateFactory.createGlobalPersistentState(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 { return this.workspaceService.getWorkspaceFolderIdentifier(resource, workspacePathNameForGlobalWorkspaces); } - private getWorkspaceState(resource: Resource): undefined | IPersistentState { + + private getWorkspaceState(resource: Resource): undefined | IPersistentState { const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); - if (!workspaceUri) { - return; + if (workspaceUri) { + const key = `autoSelectedWorkspacePythonInterpreter-${workspaceUri.folderUri.fsPath}`; + return this.stateFactory.createWorkspacePersistentState(key, undefined); } - const key = `autoSelectedWorkspacePythonInterpreter-${workspaceUri.folderUri.fsPath}`; + return undefined; + } + + private getAutoSelectionInterpretersQueryState(resource: Resource): IPersistentState { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const key = `autoSelectionInterpretersQueried-${workspaceUri?.folderUri.fsPath || 'global'}`; return this.stateFactory.createWorkspacePersistentState(key, undefined); } + + private getAutoSelectionQueriedOnceState(): IPersistentState { + const key = `autoSelectionInterpretersQueriedOnce`; + return this.stateFactory.createGlobalPersistentState(key, undefined); + } + + /** + * Auto-selection logic: + * 1. If there are cached interpreters (not the first session in this workspace) + * -> sort using the same logic as in the interpreter quickpick and return the first one; + * 2. If not, we already fire all the locators, so wait for their response, sort the interpreters and return the first one. + * + * `getInterpreters` will check the cache first and return early if there are any cached interpreters, + * and if not it will wait for locators to return. + * As such, we can sort interpreters based on what it returns. + */ + private async autoselectInterpreterWithLocators(resource: Resource): Promise { + // Do not perform a full interpreter search if we already have cached interpreters for this workspace. + const queriedState = this.getAutoSelectionInterpretersQueryState(resource); + const globalQueriedState = this.getAutoSelectionQueriedOnceState(); + if (globalQueriedState.value && queriedState.value !== true && resource) { + await this.interpreterService.triggerRefresh({ + searchLocations: { roots: [resource], doNotIncludeNonRooted: true }, + }); + } + + await this.envTypeComparer.initialize(resource); + const inExperiment = this.experimentService.inExperimentSync(DiscoveryUsingWorkers.experiment); + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + let recommendedInterpreter: PythonEnvironment | undefined; + if (inExperiment) { + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + // Do not wait for validation of all interpreters to finish, we only need to validate the recommended interpreter. + await this.interpreterService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + } + let interpreters = this.interpreterService.getInterpreters(resource); + + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + const details = recommendedInterpreter + ? await this.interpreterService.getInterpreterDetails(recommendedInterpreter.path) + : undefined; + if (!details || !recommendedInterpreter) { + await this.interpreterService.refreshPromise; // Interpreter is invalid, wait for all of validation to finish. + interpreters = this.interpreterService.getInterpreters(resource); + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + } + } else { + if (!globalQueriedState.value) { + // Global interpreters are loaded the first time an extension loads, after which we don't need to + // wait on global interpreter promise refresh. + await this.interpreterService.refreshPromise; + } + const interpreters = this.interpreterService.getInterpreters(resource); + + recommendedInterpreter = this.envTypeComparer.getRecommended(interpreters, workspaceUri?.folderUri); + } + if (!recommendedInterpreter) { + return; + } + if (workspaceUri) { + this.setWorkspaceInterpreter(workspaceUri.folderUri, recommendedInterpreter); + } else { + this.setGlobalInterpreter(recommendedInterpreter); + } + + queriedState.updateValue(true); + globalQueriedState.updateValue(true); + + this.didAutoSelectedInterpreterEmitter.fire(); + } } diff --git a/src/client/interpreter/autoSelection/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(); - 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 { return this.didAutoSelectedInterpreterEmitter.event; } - public getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined { + + public getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined { return this.instance ? this.instance.getAutoSelectedInterpreter(resource) : undefined; } - public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise{ + + public async setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise { return this.instance ? this.instance.setWorkspaceInterpreter(resource, interpreter) : undefined; } } diff --git a/src/client/interpreter/autoSelection/rules/baseRule.ts b/src/client/interpreter/autoSelection/rules/baseRule.ts deleted file mode 100644 index 957f6d60acf1..000000000000 --- a/src/client/interpreter/autoSelection/rules/baseRule.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, unmanaged } from 'inversify'; -import { compare } from 'semver'; -import '../../../common/extensions'; -import { traceDecorators, traceVerbose } from '../../../common/logger'; -import { IFileSystem } from '../../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../../common/types'; -import { StopWatch } from '../../../common/utils/stopWatch'; -import { sendTelemetryEvent } from '../../../telemetry'; -import { EventName } 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; - constructor(@unmanaged() protected readonly ruleName: AutoSelectionRule, - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory) { - this.stateStore = stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${this.ruleName}`, undefined); - } - public setNextRule(rule: IInterpreterAutoSelectionRule): void { - this.nextRule = rule; - } - @traceDecorators.verbose('autoSelectInterpreter') - public async autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - await this.clearCachedInterpreterIfInvalid(resource); - const stopWatch = new StopWatch(); - const action = await this.onAutoSelectInterpreter(resource, manager); - traceVerbose(`Rule = ${this.ruleName}, result = ${action}`); - const identified = action === NextAction.runNextRule; - sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, { elapsedTime: stopWatch.elapsedTime }, { rule: this.ruleName, identified }); - if (action === NextAction.runNextRule) { - await this.next(resource, manager); - } - } - public getPreviouslyAutoSelectedInterpreter(_resource: Resource): PythonInterpreter | undefined { - const value = this.stateStore.value; - traceVerbose(`Current value for rule ${this.ruleName} is ${value ? JSON.stringify(value) : 'nothing'}`); - return value; - } - protected abstract onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise; - @traceDecorators.verbose('setGlobalInterpreter') - protected async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise { - await this.cacheSelectedInterpreter(undefined, interpreter); - if (!interpreter || !manager || !interpreter.version) { - return false; - } - const preferredInterpreter = manager.getAutoSelectedInterpreter(undefined); - const comparison = preferredInterpreter && preferredInterpreter.version ? compare(interpreter.version.raw, preferredInterpreter.version.raw) : 1; - if (comparison > 0) { - await manager.setGlobalInterpreter(interpreter); - return true; - } - if (comparison === 0) { - return true; - } - - return false; - } - protected async clearCachedInterpreterIfInvalid(resource: Resource) { - if (!this.stateStore.value || await this.fs.fileExists(this.stateStore.value.path)) { - return; - } - sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, {}, { rule: this.ruleName, interpreterMissing: true }); - await this.cacheSelectedInterpreter(resource, undefined); - } - protected async cacheSelectedInterpreter(_resource: Resource, interpreter: PythonInterpreter | undefined) { - const interpreterPath = interpreter ? interpreter.path : ''; - const interpreterPathInCache = this.stateStore.value ? this.stateStore.value.path : ''; - const updated = interpreterPath === interpreterPathInCache; - sendTelemetryEvent(EventName.PYTHON_INTERPRETER_AUTO_SELECTION, {}, { rule: this.ruleName, updated }); - await this.stateStore.updateValue(interpreter); - } - protected async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - traceVerbose(`Executing next rule from ${this.ruleName}`); - return this.nextRule && manager ? this.nextRule.autoSelectInterpreter(resource, manager) : undefined; - } -} diff --git a/src/client/interpreter/autoSelection/rules/cached.ts b/src/client/interpreter/autoSelection/rules/cached.ts deleted file mode 100644 index d9ddf1c74866..000000000000 --- a/src/client/interpreter/autoSelection/rules/cached.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { traceVerbose } from '../../../common/logger'; -import { IFileSystem } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; -import { IInterpreterHelper } from '../../contracts'; -import { AutoSelectionRule, IInterpreterAutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; -import { BaseRuleService, NextAction } from './baseRule'; - -@injectable() -export class CachedInterpretersAutoSelectionRule extends BaseRuleService { - protected readonly rules: IInterpreterAutoSelectionRule[]; - constructor(@inject(IFileSystem) fs: IFileSystem, - @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, - @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.systemWide) systemInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.currentPath) currentPathInterpreter: IInterpreterAutoSelectionRule, - @inject(IInterpreterAutoSelectionRule) @named(AutoSelectionRule.windowsRegistry) winRegInterpreter: IInterpreterAutoSelectionRule) { - - super(AutoSelectionRule.cachedInterpreters, fs, stateFactory); - this.rules = [systemInterpreter, currentPathInterpreter, winRegInterpreter]; - } - protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - const cachedInterpreters = this.rules - .map(item => item.getPreviouslyAutoSelectedInterpreter(resource)) - .filter(item => !!item) - .map(item => item!); - const bestInterpreter = this.helper.getBestInterpreter(cachedInterpreters); - traceVerbose(`Selected Interpreter from ${this.ruleName}, ${bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected'}`); - return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; - } -} diff --git a/src/client/interpreter/autoSelection/rules/currentPath.ts b/src/client/interpreter/autoSelection/rules/currentPath.ts deleted file mode 100644 index 091481ca9de1..000000000000 --- a/src/client/interpreter/autoSelection/rules/currentPath.ts +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { traceVerbose } from '../../../common/logger'; -import { IFileSystem } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; -import { CURRENT_PATH_SERVICE, IInterpreterHelper, IInterpreterLocatorService } from '../../contracts'; -import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; -import { BaseRuleService, NextAction } from './baseRule'; - -@injectable() -export class CurrentPathInterpretersAutoSelectionRule extends BaseRuleService { - constructor( - @inject(IFileSystem) fs: IFileSystem, - @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, - @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, - @inject(IInterpreterLocatorService) @named(CURRENT_PATH_SERVICE) private readonly currentPathInterpreterLocator: IInterpreterLocatorService) { - - super(AutoSelectionRule.currentPath, fs, stateFactory); - } - protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - const interpreters = await this.currentPathInterpreterLocator.getInterpreters(resource); - const bestInterpreter = this.helper.getBestInterpreter(interpreters); - traceVerbose(`Selected Interpreter from ${this.ruleName}, ${bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected'}`); - return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; - } -} diff --git a/src/client/interpreter/autoSelection/rules/settings.ts b/src/client/interpreter/autoSelection/rules/settings.ts deleted file mode 100644 index 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 { - // tslint:disable-next-line:no-any - const pythonConfig = this.workspaceService.getConfiguration('python', null as any)!; - const pythonPathInConfig = pythonConfig.inspect('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 b40607a1a6d3..000000000000 --- a/src/client/interpreter/autoSelection/rules/system.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { traceVerbose } from '../../../common/logger'; -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 { - const interpreters = await this.interpreterService.getInterpreters(resource); - // Exclude non-local interpreters. - const filteredInterpreters = interpreters.filter(int => int.type !== InterpreterType.VirtualEnv && - int.type !== InterpreterType.Venv && - int.type !== InterpreterType.Pipenv); - const bestInterpreter = this.helper.getBestInterpreter(filteredInterpreters); - traceVerbose(`Selected Interpreter from ${this.ruleName}, ${bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected'}`); - return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; - } -} diff --git a/src/client/interpreter/autoSelection/rules/winRegistry.ts b/src/client/interpreter/autoSelection/rules/winRegistry.ts deleted file mode 100644 index 4155fd5ade7c..000000000000 --- a/src/client/interpreter/autoSelection/rules/winRegistry.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 { traceVerbose } from '../../../common/logger'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IPersistentStateFactory, Resource } from '../../../common/types'; -import { OSType } from '../../../common/utils/platform'; -import { IInterpreterHelper, IInterpreterLocatorService, WINDOWS_REGISTRY_SERVICE } from '../../contracts'; -import { AutoSelectionRule, IInterpreterAutoSelectionService } from '../types'; -import { BaseRuleService, NextAction } from './baseRule'; - -@injectable() -export class WindowsRegistryInterpretersAutoSelectionRule extends BaseRuleService { - constructor( - @inject(IFileSystem) fs: IFileSystem, - @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, - @inject(IPersistentStateFactory) stateFactory: IPersistentStateFactory, - @inject(IPlatformService) private readonly platform: IPlatformService, - @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) private winRegInterpreterLocator: IInterpreterLocatorService) { - - super(AutoSelectionRule.windowsRegistry, fs, stateFactory); - } - protected async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise { - if (this.platform.osType !== OSType.Windows) { - return NextAction.runNextRule; - } - const interpreters = await this.winRegInterpreterLocator.getInterpreters(resource); - const bestInterpreter = this.helper.getBestInterpreter(interpreters); - traceVerbose(`Selected Interpreter from ${this.ruleName}, ${bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected'}`); - return await this.setGlobalInterpreter(bestInterpreter, manager) ? NextAction.exit : NextAction.runNextRule; - } -} diff --git a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts b/src/client/interpreter/autoSelection/rules/workspaceEnv.ts deleted file mode 100644 index eaac18687904..000000000000 --- a/src/client/interpreter/autoSelection/rules/workspaceEnv.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { traceVerbose } from '../../../common/logger'; -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 { - const workspacePath = this.helper.getActiveWorkspaceUri(resource); - if (!workspacePath) { - return NextAction.runNextRule; - } - - const pythonConfig = this.workspaceService.getConfiguration('python', workspacePath.folderUri)!; - const pythonPathInConfig = pythonConfig.inspect('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, true)); - 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); - } - - traceVerbose(`Selected Interpreter from ${this.ruleName}, ${bestInterpreter ? JSON.stringify(bestInterpreter) : 'Nothing Selected'}`); - return NextAction.runNextRule; - } - protected async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise { - if (!resource) { - return; - } - const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - if (!workspaceFolder) { - return; - } - // Now check virtual environments under the workspace root - const interpreters = await this.workspaceVirtualEnvInterpreterLocator.getInterpreters(resource, 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) { - await super.cacheSelectedInterpreter(resource, 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 2bc24f46a139..91d0224717d4 100644 --- a/src/client/interpreter/autoSelection/types.ts +++ b/src/client/interpreter/autoSelection/types.ts @@ -5,31 +5,29 @@ 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; - getAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; - registerInstance?(instance: IInterpreterAutoSeletionProxyService): void; - setWorkspaceInterpreter(resource: Uri, interpreter: PythonInterpreter | undefined): Promise; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + registerInstance?(instance: IInterpreterAutoSelectionProxyService): void; + setWorkspaceInterpreter(resource: Uri, interpreter: PythonEnvironment | undefined): Promise; } export const IInterpreterAutoSelectionService = Symbol('IInterpreterAutoSelectionService'); -export interface IInterpreterAutoSelectionService extends IInterpreterAutoSeletionProxyService { +export interface IInterpreterAutoSelectionService extends IInterpreterAutoSelectionProxyService { readonly onDidChangeAutoSelectedInterpreter: Event; autoSelectInterpreter(resource: Resource): Promise; - setGlobalInterpreter(interpreter: PythonInterpreter | undefined): Promise; + getAutoSelectedInterpreter(resource: Resource): PythonEnvironment | undefined; + setGlobalInterpreter(interpreter: PythonEnvironment | undefined): Promise; } export enum AutoSelectionRule { @@ -39,12 +37,5 @@ export enum AutoSelectionRule { settings = 'settings', cachedInterpreters = 'cachedInterpreters', systemWide = 'system', - windowsRegistry = 'windowsRegistry' -} - -export const IInterpreterAutoSelectionRule = Symbol('IInterpreterAutoSelectionRule'); -export interface IInterpreterAutoSelectionRule { - setNextRule(rule: IInterpreterAutoSelectionRule): void; - autoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise; - getPreviouslyAutoSelectedInterpreter(resource: Resource): PythonInterpreter | undefined; + windowsRegistry = 'windowsRegistry', } diff --git a/src/client/interpreter/configuration/environmentTypeComparer.ts b/src/client/interpreter/configuration/environmentTypeComparer.ts new file mode 100644 index 000000000000..2e1013b7b5a8 --- /dev/null +++ b/src/client/interpreter/configuration/environmentTypeComparer.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable, inject } from 'inversify'; +import { Resource } from '../../common/types'; +import { Architecture } from '../../common/utils/platform'; +import { isActiveStateEnvironmentForWorkspace } from '../../pythonEnvironments/common/environmentManagers/activestate'; +import { isParentPath } from '../../pythonEnvironments/common/externalDependencies'; +import { + EnvironmentType, + PythonEnvironment, + virtualEnvTypes, + workspaceVirtualEnvTypes, +} from '../../pythonEnvironments/info'; +import { PythonVersion } from '../../pythonEnvironments/info/pythonVersion'; +import { IInterpreterHelper } from '../contracts'; +import { IInterpreterComparer } from './types'; +import { getActivePyenvForDirectory } from '../../pythonEnvironments/common/environmentManagers/pyenv'; +import { arePathsSame } from '../../common/platform/fs-paths'; + +export enum EnvLocationHeuristic { + /** + * Environments inside the workspace. + */ + Local = 1, + /** + * Environments outside the workspace. + */ + Global = 2, +} + +@injectable() +export class EnvironmentTypeComparer implements IInterpreterComparer { + private workspaceFolderPath: string; + + private preferredPyenvInterpreterPath = new Map(); + + constructor(@inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper) { + this.workspaceFolderPath = this.interpreterHelper.getActiveWorkspaceUri(undefined)?.folderUri.fsPath ?? ''; + } + + /** + * Compare 2 Python environments, sorting them by assumed usefulness. + * Return 0 if both environments are equal, -1 if a should be closer to the beginning of the list, or 1 if a comes after b. + * + * The comparison guidelines are: + * 1. Local environments first (same path as the workspace root); + * 2. Global environments next (anything not local), with conda environments at a lower priority, and "base" being last; + * 3. Globally-installed interpreters (/usr/bin/python3, Microsoft Store). + * + * Always sort with newest version of Python first within each subgroup. + */ + public compare(a: PythonEnvironment, b: PythonEnvironment): number { + if (isProblematicCondaEnvironment(a)) { + return 1; + } + if (isProblematicCondaEnvironment(b)) { + return -1; + } + // Check environment location. + const envLocationComparison = compareEnvironmentLocation(a, b, this.workspaceFolderPath); + if (envLocationComparison !== 0) { + return envLocationComparison; + } + + if (a.envType === EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) { + const preferredPyenv = this.preferredPyenvInterpreterPath.get(this.workspaceFolderPath); + if (preferredPyenv) { + if (arePathsSame(preferredPyenv, b.path)) { + return 1; + } + if (arePathsSame(preferredPyenv, a.path)) { + return -1; + } + } + } + + // Check environment type. + const envTypeComparison = compareEnvironmentType(a, b); + if (envTypeComparison !== 0) { + return envTypeComparison; + } + + // Check Python version. + const versionComparison = comparePythonVersionDescending(a.version, b.version); + if (versionComparison !== 0) { + return versionComparison; + } + + // If we have the "base" Conda env, put it last in its Python version subgroup. + if (isBaseCondaEnvironment(a)) { + return 1; + } + + if (isBaseCondaEnvironment(b)) { + return -1; + } + + // Check alphabetical order. + const nameA = getSortName(a, this.interpreterHelper); + const nameB = getSortName(b, this.interpreterHelper); + if (nameA === nameB) { + return 0; + } + + return nameA > nameB ? 1 : -1; + } + + public async initialize(resource: Resource): Promise { + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const cwd = workspaceUri?.folderUri.fsPath; + if (!cwd) { + return; + } + const preferredPyenvInterpreter = await getActivePyenvForDirectory(cwd); + this.preferredPyenvInterpreterPath.set(cwd, preferredPyenvInterpreter); + } + + public getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined { + // When recommending an intepreter for a workspace, we either want to return a local one + // or fallback on a globally-installed interpreter, and we don't want want to suggest a global environment + // because we would have to add a way to match environments to a workspace. + const workspaceUri = this.interpreterHelper.getActiveWorkspaceUri(resource); + const filteredInterpreters = interpreters.filter((i) => { + if (isProblematicCondaEnvironment(i)) { + return false; + } + if ( + i.envType === EnvironmentType.ActiveState && + (!i.path || + !workspaceUri || + !isActiveStateEnvironmentForWorkspace(i.path, workspaceUri.folderUri.fsPath)) + ) { + return false; + } + if (getEnvLocationHeuristic(i, workspaceUri?.folderUri.fsPath || '') === EnvLocationHeuristic.Local) { + return true; + } + if (!workspaceVirtualEnvTypes.includes(i.envType) && virtualEnvTypes.includes(i.envType)) { + // These are global virtual envs so we're not sure if these envs were created for the workspace, skip them. + return false; + } + if (i.version?.major === 2) { + return false; + } + return true; + }); + filteredInterpreters.sort(this.compare.bind(this)); + return filteredInterpreters.length ? filteredInterpreters[0] : undefined; + } +} + +function getSortName(info: PythonEnvironment, interpreterHelper: IInterpreterHelper): string { + const sortNameParts: string[] = []; + const envSuffixParts: string[] = []; + + // Sort order for interpreters is: + // * Version + // * Architecture + // * Interpreter Type + // * Environment name + if (info.version) { + sortNameParts.push(info.version.raw); + } + if (info.architecture) { + sortNameParts.push(getArchitectureSortName(info.architecture)); + } + if (info.companyDisplayName && info.companyDisplayName.length > 0) { + sortNameParts.push(info.companyDisplayName.trim()); + } else { + sortNameParts.push('Python'); + } + + if (info.envType) { + const name = interpreterHelper.getInterpreterTypeDisplayName(info.envType); + if (name) { + envSuffixParts.push(name); + } + } + if (info.envName && info.envName.length > 0) { + envSuffixParts.push(info.envName); + } + + const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; + return `${sortNameParts.join(' ')} ${envSuffix}`.trim(); +} + +function getArchitectureSortName(arch?: Architecture) { + // Strings are choosen keeping in mind that 64-bit gets preferred over 32-bit. + switch (arch) { + case Architecture.x64: + return 'x64'; + case Architecture.x86: + return 'x86'; + default: + return ''; + } +} + +function isBaseCondaEnvironment(environment: PythonEnvironment): boolean { + return ( + environment.envType === EnvironmentType.Conda && + (environment.envName === 'base' || environment.envName === 'miniconda') + ); +} + +export function isProblematicCondaEnvironment(environment: PythonEnvironment): boolean { + return environment.envType === EnvironmentType.Conda && environment.path === 'python'; +} + +/** + * Compare 2 Python versions in decending order, most recent one comes first. + */ +function comparePythonVersionDescending(a: PythonVersion | undefined, b: PythonVersion | undefined): number { + if (!a) { + return 1; + } + + if (!b) { + return -1; + } + + if (a.raw === b.raw) { + return 0; + } + + if (a.major === b.major) { + if (a.minor === b.minor) { + if (a.patch === b.patch) { + return a.build.join(' ') > b.build.join(' ') ? -1 : 1; + } + return a.patch > b.patch ? -1 : 1; + } + return a.minor > b.minor ? -1 : 1; + } + + return a.major > b.major ? -1 : 1; +} + +/** + * Compare 2 environment locations: return 0 if they are the same, -1 if a comes before b, 1 otherwise. + */ +function compareEnvironmentLocation(a: PythonEnvironment, b: PythonEnvironment, workspacePath: string): number { + const aHeuristic = getEnvLocationHeuristic(a, workspacePath); + const bHeuristic = getEnvLocationHeuristic(b, workspacePath); + + return Math.sign(aHeuristic - bHeuristic); +} + +/** + * Return a heuristic value depending on the environment type. + */ +export function getEnvLocationHeuristic(environment: PythonEnvironment, workspacePath: string): EnvLocationHeuristic { + if ( + workspacePath.length > 0 && + ((environment.envPath && isParentPath(environment.envPath, workspacePath)) || + (environment.path && isParentPath(environment.path, workspacePath))) + ) { + return EnvLocationHeuristic.Local; + } + return EnvLocationHeuristic.Global; +} + +/** + * Compare 2 environment types: return 0 if they are the same, -1 if a comes before b, 1 otherwise. + */ +function compareEnvironmentType(a: PythonEnvironment, b: PythonEnvironment): number { + if (!a.type && !b.type) { + // Unless one of them is pyenv interpreter, return 0 if two global interpreters are being compared. + if (a.envType === EnvironmentType.Pyenv && b.envType !== EnvironmentType.Pyenv) { + return -1; + } + if (a.envType !== EnvironmentType.Pyenv && b.envType === EnvironmentType.Pyenv) { + return 1; + } + return 0; + } + const envTypeByPriority = getPrioritizedEnvironmentType(); + return Math.sign(envTypeByPriority.indexOf(a.envType) - envTypeByPriority.indexOf(b.envType)); +} + +function getPrioritizedEnvironmentType(): EnvironmentType[] { + return [ + // Prioritize non-Conda environments. + EnvironmentType.Poetry, + EnvironmentType.Pipenv, + EnvironmentType.VirtualEnvWrapper, + EnvironmentType.Hatch, + EnvironmentType.Venv, + EnvironmentType.VirtualEnv, + EnvironmentType.ActiveState, + EnvironmentType.Conda, + EnvironmentType.Pyenv, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.System, + EnvironmentType.Unknown, + ]; +} diff --git a/src/client/interpreter/configuration/interpreterComparer.ts b/src/client/interpreter/configuration/interpreterComparer.ts deleted file mode 100644 index 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 { - 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 { - 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 { - 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; + + 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 { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallPython, () => this._installPython())); + } + + public async _installPython(): Promise { + if (this.platformService.isWindows) { + const version = await this.platformService.getVersion(); + if (version.major > 8) { + // OS is not Windows 8, ms-windows-store URIs are available: + // https://docs.microsoft.com/en-us/windows/uwp/launch-resume/launch-store-app + this.browserService.launch('ms-windows-store://pdp/?ProductId=9NRWMJP3717K'); + return; + } + } + this.showInstallPythonTile(); + } + + private showInstallPythonTile() { + this.contextManager.setContext(ExtensionContextKey.showInstallPythonTile, true); + let step: string; + if (this.platformService.isWindows) { + step = PythonWelcome.windowsInstallId; + } else if (this.platformService.isLinux) { + step = PythonWelcome.linuxInstallId; + } else { + step = PythonWelcome.macOSInstallId; + } + this.commandManager.executeCommand( + 'workbench.action.openWalkthrough', + { + category: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}`, + step: `${PVSC_EXTENSION_ID}#${PythonWelcome.name}#${step}`, + }, + false, + ); + } +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts new file mode 100644 index 000000000000..3b4a6d428baa --- /dev/null +++ b/src/client/interpreter/configuration/interpreterSelector/commands/installPython/installPythonViaTerminal.ts @@ -0,0 +1,115 @@ +/* eslint-disable global-require */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import type * as whichTypes from 'which'; +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../../../../../activation/types'; +import { Commands } from '../../../../../common/constants'; +import { IDisposableRegistry } from '../../../../../common/types'; +import { ICommandManager, ITerminalManager } from '../../../../../common/application/types'; +import { sleep } from '../../../../../common/utils/async'; +import { OSType } from '../../../../../common/utils/platform'; +import { traceVerbose } from '../../../../../logging'; +import { Interpreters } from '../../../../../common/utils/localize'; + +enum PackageManagers { + brew = 'brew', + apt = 'apt', + dnf = 'dnf', +} + +/** + * Runs commands listed in walkthrough to install Python. + */ +@injectable() +export class InstallPythonViaTerminal implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: false }; + + private readonly packageManagerCommands: Record = { + brew: ['brew install python3'], + dnf: ['sudo dnf install python3'], + apt: ['sudo apt-get update', 'sudo apt-get install python3 python3-venv python3-pip'], + }; + + constructor( + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) {} + + public async activate(): Promise { + this.disposables.push( + this.commandManager.registerCommand(Commands.InstallPythonOnMac, () => + this._installPythonOnUnix(OSType.OSX), + ), + ); + this.disposables.push( + this.commandManager.registerCommand(Commands.InstallPythonOnLinux, () => + this._installPythonOnUnix(OSType.Linux), + ), + ); + } + + public async _installPythonOnUnix(os: OSType.Linux | OSType.OSX): Promise { + const commands = await this.getCommands(os); + const installMessage = + os === OSType.OSX + ? Interpreters.installPythonTerminalMacMessage + : Interpreters.installPythonTerminalMessageLinux; + const terminal = this.terminalManager.createTerminal({ + name: 'Python', + message: commands.length ? undefined : installMessage, + }); + terminal.show(true); + await waitForTerminalToStartup(); + for (const command of commands) { + terminal.sendText(command); + await waitForCommandToProcess(); + } + } + + private async getCommands(os: OSType.Linux | OSType.OSX) { + if (os === OSType.OSX) { + return this.getCommandsForPackageManagers([PackageManagers.brew]); + } + if (os === OSType.Linux) { + return this.getCommandsForPackageManagers([PackageManagers.apt, PackageManagers.dnf]); + } + throw new Error('OS not supported'); + } + + private async getCommandsForPackageManagers(packageManagers: PackageManagers[]) { + for (const packageManager of packageManagers) { + if (await isPackageAvailable(packageManager)) { + return this.packageManagerCommands[packageManager]; + } + } + return []; + } +} + +async function isPackageAvailable(packageManager: PackageManagers) { + try { + const which = require('which') as typeof whichTypes; + const resolvedPath = await which.default(packageManager); + traceVerbose(`Resolved path to ${packageManager} module:`, resolvedPath); + return resolvedPath.trim().length > 0; + } catch (ex) { + traceVerbose(`${packageManager} not found`, ex); + return false; + } +} + +async function waitForTerminalToStartup() { + // Sometimes the terminal takes some time to start up before it can start accepting input. + await sleep(100); +} + +async function waitForCommandToProcess() { + // Give the command some time to complete. + // Its been observed that sending commands too early will strip some text off in VS Code Terminal. + await sleep(500); +} diff --git a/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts b/src/client/interpreter/configuration/interpreterSelector/commands/resetInterpreter.ts 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 { + this.disposables.push( + this.commandManager.registerCommand(Commands.Set_Interpreter, this.setInterpreter.bind(this)), + ); + } + + public async _pickInterpreter( + input: IMultiStepInput, + state: InterpreterStateArgs, + filter?: (i: PythonEnvironment) => boolean, + params?: InterpreterQuickPickParams, + ): Promise> { + // 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>({ + 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) { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const suggestions = quickPick.items; + const activeInterpreterItem = suggestions.find( + (i) => isInterpreterQuickPickItem(i) && i.interpreter.id === interpreter?.id, + ); + if (activeInterpreterItem) { + return activeInterpreterItem; + } + const firstInterpreterSuggestion = suggestions.find((s) => isInterpreterQuickPickItem(s)); + if (firstInterpreterSuggestion) { + return firstInterpreterSuggestion; + } + const noPythonInstalledItem = suggestions.find( + (i) => isSpecialQuickPickItem(i) && i.label === this.noPythonInstalled.label, + ); + return noPythonInstalledItem ?? suggestions[0]; + } + + private getDefaultInterpreterPathSuggestion(resource: Resource): ISpecialQuickPickItem | undefined { + const config = this.workspaceService.getConfiguration('python', resource); + const systemVariables = new SystemVariables(resource, undefined, this.workspaceService); + const defaultInterpreterPathValue = systemVariables.resolveAny(config.get('defaultInterpreterPath')); + if (defaultInterpreterPathValue && defaultInterpreterPathValue !== 'python') { + return { + label: `${Octicons.Gear} ${InterpreterQuickPickList.defaultInterpreterPath.label}`, + description: this.pathUtils.getDisplayName( + defaultInterpreterPathValue, + resource ? resource.fsPath : undefined, + ), + path: defaultInterpreterPathValue, + alwaysShow: true, + }; + } + return undefined; + } + + /** + * Updates quickpick using the change event received. + */ + private updateQuickPickItems( + quickPick: QuickPick, + event: PythonEnvironmentsChangedEvent, + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ) { + // Active items are reset once we replace the current list with updated items, so save it. + const activeItemBeforeUpdate = quickPick.activeItems.length > 0 ? quickPick.activeItems[0] : undefined; + quickPick.items = this.getUpdatedItems(quickPick.items, event, resource, filter, params); + // Ensure we maintain the same active item as before. + const activeItem = activeItemBeforeUpdate + ? quickPick.items.find((item) => { + if (isInterpreterQuickPickItem(item) && isInterpreterQuickPickItem(activeItemBeforeUpdate)) { + return item.interpreter.id === activeItemBeforeUpdate.interpreter.id; + } + if (isSpecialQuickPickItem(item) && isSpecialQuickPickItem(activeItemBeforeUpdate)) { + // 'label' is a constant here instead of 'path'. + return item.label === activeItemBeforeUpdate.label; + } + return false; + }) + : undefined; + if (activeItem) { + quickPick.activeItems = [activeItem]; + } + } + + /** + * Prepare updated items to replace the quickpick list with. + */ + private getUpdatedItems( + items: readonly QuickPickType[], + event: PythonEnvironmentsChangedEvent, + resource: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): QuickPickType[] { + const updatedItems = [...items.values()]; + const areItemsGrouped = items.find((item) => isSeparatorItem(item)); + const env = event.old ?? event.new; + if (filter && event.new && !filter(event.new)) { + event.new = undefined; // Remove envs we're not looking for from the list. + } + let envIndex = -1; + if (env) { + envIndex = updatedItems.findIndex( + (item) => isInterpreterQuickPickItem(item) && item.interpreter.id === env.id, + ); + } + if (event.new) { + const newSuggestion = this.interpreterSelector.suggestionToQuickPickItem( + event.new, + resource, + !areItemsGrouped, + ); + if (envIndex === -1) { + const noPyIndex = updatedItems.findIndex( + (item) => isSpecialQuickPickItem(item) && item.label === this.noPythonInstalled.label, + ); + if (noPyIndex !== -1) { + updatedItems.splice(noPyIndex, 1); + } + const tryReloadIndex = updatedItems.findIndex( + (item) => isSpecialQuickPickItem(item) && item.label === this.tipToReloadWindow.label, + ); + if (tryReloadIndex !== -1) { + updatedItems.splice(tryReloadIndex, 1); + } + if (areItemsGrouped) { + addSeparatorIfApplicable( + updatedItems, + newSuggestion, + this.workspaceService.getWorkspaceFolder(resource)?.uri.fsPath, + ); + } + updatedItems.push(newSuggestion); + } else { + updatedItems[envIndex] = newSuggestion; + } + } + if (envIndex !== -1 && event.new === undefined) { + updatedItems.splice(envIndex, 1); + } + this.finalizeItems(updatedItems, resource, params); + return updatedItems; + } + + private finalizeItems(items: QuickPickType[], resource: Resource, params?: InterpreterQuickPickParams) { + const interpreterSuggestions = this.interpreterSelector.getSuggestions(resource, true); + const r = this.interpreterService.refreshPromise; + if (!r) { + if (interpreterSuggestions.length) { + if (!params?.skipRecommended) { + this.setRecommendedItem(interpreterSuggestions, items, resource); + } + // Add warning label to certain environments + items.forEach((item, i) => { + if (isInterpreterQuickPickItem(item) && isProblematicCondaEnvironment(item.interpreter)) { + if (!items[i].label.includes(Octicons.Warning)) { + items[i].label = `${Octicons.Warning} ${items[i].label}`; + items[i].tooltip = InterpreterQuickPickList.condaEnvWithoutPythonTooltip; + } + } + }); + } else { + if (!items.some((i) => isSpecialQuickPickItem(i) && i.label === this.noPythonInstalled.label)) { + items.push(this.noPythonInstalled); + } + if ( + this.wasNoPythonInstalledItemClicked && + !items.some((i) => isSpecialQuickPickItem(i) && i.label === this.tipToReloadWindow.label) + ) { + items.push(this.tipToReloadWindow); + } + } + } + } + + private setRecommendedItem( + interpreterSuggestions: IInterpreterQuickPickItem[], + items: QuickPickType[], + resource: Resource, + ) { + const suggestion = this.interpreterSelector.getRecommendedSuggestion( + interpreterSuggestions, + this.workspaceService.getWorkspaceFolder(resource)?.uri, + ); + if (!suggestion) { + return; + } + const areItemsGrouped = items.find((item) => isSeparatorItem(item) && item.label === EnvGroups.Recommended); + const recommended = cloneDeep(suggestion); + recommended.description = areItemsGrouped + ? // No need to add a tag as "Recommended" group already exists. + recommended.description + : `${recommended.description ?? ''} - ${Common.recommended}`; + const index = items.findIndex( + (item) => isInterpreterQuickPickItem(item) && item.interpreter.id === recommended.interpreter.id, + ); + if (index !== -1) { + items[index] = recommended; + } + } + + private refreshCallback( + input: QuickPick, + options?: TriggerRefreshOptions & { isButton?: boolean; showBackButton?: boolean }, + ) { + input.buttons = this.getButtons(options); + + this.interpreterService + .triggerRefresh(undefined, options) + .finally(() => { + input.buttons = this.getButtons({ isButton: false, showBackButton: options?.showBackButton }); + }) + .ignoreErrors(); + if (this.interpreterService.refreshPromise) { + input.busy = true; + this.interpreterService.refreshPromise.then(() => { + input.busy = false; + }); + } + } + + private getButtons(options?: { isButton?: boolean; showBackButton?: boolean }): QuickInputButton[] { + const buttons: QuickInputButton[] = []; + if (options?.showBackButton) { + buttons.push(QuickInputButtons.Back); + } + if (options?.isButton) { + buttons.push({ + iconPath: new ThemeIcon(ThemeIcons.SpinningLoader), + tooltip: InterpreterQuickPickList.refreshingInterpreterList, + }); + } else { + buttons.push(this.refreshButton); + } + return buttons; + } + + @captureTelemetry(EventName.SELECT_INTERPRETER_ENTER_BUTTON) + public async _enterOrBrowseInterpreterPath( + input: IMultiStepInput, + state: InterpreterStateArgs, + ): Promise> { + const items: QuickPickItem[] = [ + { + label: InterpreterQuickPickList.browsePath.label, + detail: InterpreterQuickPickList.browsePath.detail, + }, + ]; + + const selection = await input.showQuickPick({ + placeholder: InterpreterQuickPickList.enterPath.placeholder, + items, + acceptFilterBoxTextAsSelection: true, + }); + + if (typeof selection === 'string') { + // User entered text in the filter box to enter path to python, store it + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTER_CHOICE, undefined, { choice: 'enter' }); + state.path = selection; + 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 { + 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(); + try { + await multiStep.run( + (input, s) => + this._pickInterpreter(input, s, undefined, { + showCreateEnvironment: !options?.hideCreateVenv, + showBackButton: options?.showBackButton, + }), + interpreterState, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + // User clicked back button, so we need to return this action. + return { action: 'Back' }; + } + if (ex === InputFlowAction.cancel) { + // User clicked cancel button, so we need to return this action. + return { action: 'Cancel' }; + } + } + if (interpreterState.path !== undefined) { + // User may choose to have an empty string stored, so variable `interpreterState.path` may be + // an empty string, in which case we should update. + // Having the value `undefined` means user cancelled the quickpick, so we update nothing in that case. + await this.pythonPathUpdaterService.updatePythonPath(interpreterState.path, configTarget, 'ui', wkspace); + if (useEnvExtension()) { + await setInterpreterLegacy(interpreterState.path, wkspace); + } + return { path: interpreterState.path }; + } + } + + public async getInterpreterViaQuickPick( + workspace: Resource, + filter: ((i: PythonEnvironment) => boolean) | undefined, + params?: InterpreterQuickPickParams, + ): Promise { + const interpreterState: InterpreterStateArgs = { path: undefined, workspace }; + const multiStep = this.multiStepFactory.create(); + await multiStep.run((input, s) => this._pickInterpreter(input, s, filter, params), interpreterState); + return interpreterState.path; + } + + /** + * Check if the interpreter that was entered exists in the list of suggestions. + * If it does, it means that it had already been discovered, + * and we didn't do a good job of surfacing it. + * + * @param selection Intepreter path that was either entered manually or picked by browsing through the filesystem. + */ + // eslint-disable-next-line class-methods-use-this + private sendInterpreterEntryTelemetry(selection: string, workspace: Resource): void { + const suggestions = this._getItems(workspace, undefined); + let interpreterPath = path.normalize(untildify(selection)); + + if (!path.isAbsolute(interpreterPath)) { + interpreterPath = path.resolve(workspace?.fsPath || '', selection); + } + + const expandedPaths = suggestions.map((s) => { + const suggestionPath = isInterpreterQuickPickItem(s) ? s.interpreter.path : ''; + let expandedPath = path.normalize(untildify(suggestionPath)); + + if (!path.isAbsolute(suggestionPath)) { + expandedPath = path.resolve(workspace?.fsPath || '', suggestionPath); + } + + return expandedPath; + }); + + const discovered = expandedPaths.includes(interpreterPath); + + sendTelemetryEvent(EventName.SELECT_INTERPRETER_ENTERED_EXISTS, undefined, { discovered }); + + return undefined; + } +} + +function getGroupedQuickPickItems( + items: IInterpreterQuickPickItem[], + recommended: IInterpreterQuickPickItem | undefined, + workspacePath?: string, +): QuickPickType[] { + const updatedItems: QuickPickType[] = []; + if (recommended) { + updatedItems.push({ label: EnvGroups.Recommended, kind: QuickPickItemKind.Separator }, recommended); + } + let previousGroup = EnvGroups.Recommended; + for (const item of items) { + previousGroup = addSeparatorIfApplicable(updatedItems, item, workspacePath, previousGroup); + updatedItems.push(item); + } + return updatedItems; +} + +function addSeparatorIfApplicable( + items: QuickPickType[], + newItem: IInterpreterQuickPickItem, + workspacePath?: string, + previousGroup?: string | undefined, +) { + if (!previousGroup) { + const lastItem = items.length ? items[items.length - 1] : undefined; + previousGroup = + lastItem && isInterpreterQuickPickItem(lastItem) ? getGroup(lastItem, workspacePath) : undefined; + } + const currentGroup = getGroup(newItem, workspacePath); + if (!previousGroup || currentGroup !== previousGroup) { + const separatorItem: QuickPickItem = { label: currentGroup, kind: QuickPickItemKind.Separator }; + items.push(separatorItem); + previousGroup = currentGroup; + } + return previousGroup; +} + +function getGroup(item: IInterpreterQuickPickItem, workspacePath?: string) { + if (workspacePath && isParentPath(item.path, workspacePath)) { + return EnvGroups.Workspace; + } + switch (item.interpreter.envType) { + case EnvironmentType.Global: + case EnvironmentType.System: + case EnvironmentType.Unknown: + case EnvironmentType.MicrosoftStore: + return EnvGroups.Global; + default: + return EnvGroups[item.interpreter.envType]; + } +} + +export type SelectEnvironmentResult = { + /** + * Path to the executable python in the environment + */ + readonly path?: string; + /* + * User action that resulted in exit from the create environment flow. + */ + readonly action?: 'Back' | 'Cancel'; +}; diff --git a/src/client/interpreter/configuration/interpreterSelector/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 { + 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 bedc8702083b..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 { EventName } from '../../telemetry/constants'; import { PythonInterpreterTelemetry } from '../../telemetry/types'; -import { IInterpreterVersionService } from '../contracts'; -import { IPythonPathUpdaterServiceFactory, IPythonPathUpdaterServiceManager } from './types'; +import { IComponentAdapter } from '../contracts'; +import { + IRecommendedEnvironmentService, + IPythonPathUpdaterServiceFactory, + IPythonPathUpdaterServiceManager, +} from './types'; @injectable() export class PythonPathUpdaterService implements IPythonPathUpdaterServiceManager { - private readonly pythonPathSettingsUpdaterFactory: IPythonPathUpdaterServiceFactory; - private readonly interpreterVersionService: IInterpreterVersionService; - private readonly executionFactory: IPythonExecutionFactory; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.pythonPathSettingsUpdaterFactory = serviceContainer.get(IPythonPathUpdaterServiceFactory); - this.interpreterVersionService = serviceContainer.get(IInterpreterVersionService); - this.executionFactory = serviceContainer.get(IPythonExecutionFactory); - } - public async updatePythonPath(pythonPath: string, configTarget: ConfigurationTarget, trigger: 'ui' | 'shebang' | 'load', wkspace?: Uri): Promise { + 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 { 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(() => undefined); - const pipVersionPromise = this.interpreterVersionService.getPipVersion(pythonPath) - .then(value => value.length === 0 ? undefined : value) - .catch(() => ''); - const [info, pipVersion] = await Promise.all([infoPromise, pipVersionPromise]); - if (info && 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(EventName.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); + this.interpreterPathService = serviceContainer.get(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..c5356409fcee --- /dev/null +++ b/src/client/interpreter/configuration/recommededEnvironmentService.ts @@ -0,0 +1,200 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IRecommendedEnvironmentService } from './types'; +import { PythonExtension, ResolvedEnvironment } from '../../api/types'; +import { IExtensionContext, Resource } from '../../common/types'; +import { commands, Uri, workspace } from 'vscode'; +import { getWorkspaceStateValue, updateWorkspaceStateValue } from '../../common/persistentState'; +import { traceError } from '../../logging'; +import { IExtensionActivationService } from '../../activation/types'; +import { StopWatch } from '../../common/utils/stopWatch'; +import { isParentPath } from '../../common/platform/fs-paths'; + +const MEMENTO_KEY = 'userSelectedEnvPath'; + +@injectable() +export class RecommendedEnvironmentService implements IRecommendedEnvironmentService, IExtensionActivationService { + private api?: PythonExtension['environments']; + constructor(@inject(IExtensionContext) private readonly extensionContext: IExtensionContext) {} + supportedWorkspaceTypes: { untrustedWorkspace: boolean; virtualWorkspace: boolean } = { + untrustedWorkspace: true, + virtualWorkspace: false, + }; + + async activate(_resource: Resource, _startupStopWatch?: StopWatch): Promise { + this.extensionContext.subscriptions.push( + commands.registerCommand('python.getRecommendedEnvironment', async (resource: Resource) => { + return this.getRecommededEnvironment(resource); + }), + ); + } + + registerEnvApi(api: PythonExtension['environments']) { + this.api = api; + } + + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined) { + if (workspace.workspaceFolders?.length) { + try { + void updateWorkspaceStateValue(MEMENTO_KEY, getDataToStore(environmentPath, uri)); + } catch (ex) { + traceError('Failed to update workspace state for preferred environment', ex); + } + } else { + void this.extensionContext.globalState.update(MEMENTO_KEY, environmentPath); + } + } + + async getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + > { + if (!workspace.isTrusted || !this.api) { + return undefined; + } + const preferred = await this.getRecommededInternal(resource); + if (!preferred) { + return undefined; + } + const activeEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath(resource)); + const recommendedEnv = await this.api.resolveEnvironment(preferred.environmentPath); + if (activeEnv && recommendedEnv && activeEnv.id !== recommendedEnv.id) { + traceError( + `Active environment ${activeEnv.id} is different from recommended environment ${ + recommendedEnv.id + } for resource ${resource?.toString()}`, + ); + return undefined; + } + if (recommendedEnv) { + return { environment: recommendedEnv, reason: preferred.reason }; + } + const globalEnv = await this.api.resolveEnvironment(this.api.getActiveEnvironmentPath()); + if (activeEnv && globalEnv?.path !== activeEnv?.path) { + // User has definitely got a workspace specific environment selected. + // Given the fact that global !== workspace env, we can safely assume that + // at some time, the user has selected a workspace specific environment. + // This applies to cases where the user has selected a workspace specific environment before this version of the extension + // and we did not store it in the workspace state. + // So we can safely return the global environment as the recommended environment. + return { environment: activeEnv, reason: 'workspaceUserSelected' }; + } + return undefined; + } + async getRecommededInternal( + resource: Resource, + ): Promise< + | { environmentPath: string; reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended' } + | undefined + > { + let workspaceState: string | undefined = undefined; + try { + workspaceState = getWorkspaceStateValue(MEMENTO_KEY); + } catch (ex) { + traceError('Failed to get workspace state for preferred environment', ex); + } + + if (workspace.workspaceFolders?.length && workspaceState) { + const workspaceUri = ( + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + workspace.workspaceFolders[0].uri + ).toString(); + + try { + const existingJson: Record = JSON.parse(workspaceState); + const selectedEnvPath = existingJson[workspaceUri]; + if (selectedEnvPath) { + return { environmentPath: selectedEnvPath, reason: 'workspaceUserSelected' }; + } + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + } + } + + if (workspace.workspaceFolders?.length && this.api) { + // Check if we have a .venv or .conda environment in the workspace + // This is required for cases where user has selected a workspace specific environment + // but before this version of the extension, we did not store it in the workspace state. + const workspaceEnv = await getWorkspaceSpecificVirtualEnvironment(this.api, resource); + if (workspaceEnv) { + return { environmentPath: workspaceEnv.path, reason: 'workspaceUserSelected' }; + } + } + + const globalSelectedEnvPath = this.extensionContext.globalState.get(MEMENTO_KEY); + if (globalSelectedEnvPath) { + return { environmentPath: globalSelectedEnvPath, reason: 'globalUserSelected' }; + } + return this.api && workspace.isTrusted + ? { + environmentPath: this.api.getActiveEnvironmentPath(resource).path, + reason: 'defaultRecommended', + } + : undefined; + } +} + +async function getWorkspaceSpecificVirtualEnvironment(api: PythonExtension['environments'], resource: Resource) { + const workspaceUri = + (resource ? workspace.getWorkspaceFolder(resource)?.uri : undefined) || + (workspace.workspaceFolders?.length ? workspace.workspaceFolders[0].uri : undefined); + if (!workspaceUri) { + return undefined; + } + let workspaceEnv = api.known.find((env) => { + if (!env.environment?.folderUri) { + return false; + } + if (env.environment.type !== 'VirtualEnvironment' && env.environment.type !== 'Conda') { + return false; + } + return isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath); + }); + let resolvedEnv = workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; + if (resolvedEnv) { + return resolvedEnv; + } + workspaceEnv = api.known.find((env) => { + // Look for any other type of env thats inside this workspace + // Or look for an env thats associated with this workspace (pipenv or the like). + return ( + (env.environment?.folderUri && isParentPath(env.environment.folderUri.fsPath, workspaceUri.fsPath)) || + (env.environment?.workspaceFolder && env.environment.workspaceFolder.uri.fsPath === workspaceUri.fsPath) + ); + }); + return workspaceEnv ? api.resolveEnvironment(workspaceEnv) : undefined; +} + +function getDataToStore(environmentPath: string | undefined, uri: Uri | undefined): string | undefined { + if (!workspace.workspaceFolders?.length) { + return environmentPath; + } + const workspaceUri = ( + (uri ? workspace.getWorkspaceFolder(uri)?.uri : undefined) || workspace.workspaceFolders[0].uri + ).toString(); + const existingData = getWorkspaceStateValue(MEMENTO_KEY); + if (!existingData) { + return JSON.stringify(environmentPath ? { [workspaceUri]: environmentPath } : {}); + } + try { + const existingJson: Record = JSON.parse(existingData); + if (environmentPath) { + existingJson[workspaceUri] = environmentPath; + } else { + delete existingJson[workspaceUri]; + } + return JSON.stringify(existingJson); + } catch (ex) { + traceError('Failed to parse existing workspace state value for preferred environment', ex); + return JSON.stringify({ + [workspaceUri]: environmentPath, + }); + } +} diff --git a/src/client/interpreter/configuration/services/globalUpdaterService.ts b/src/client/interpreter/configuration/services/globalUpdaterService.ts index 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 { - const pythonConfig = this.workspaceService.getConfiguration('python'); - const pythonPathValue = pythonConfig.inspect('pythonPath'); + constructor(private readonly interpreterPathService: IInterpreterPathService) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { + 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 { - const pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceFolder); - const pythonPathValue = pythonConfig.inspect('pythonPath'); + constructor(private workspaceFolder: Uri, private readonly interpreterPathService: IInterpreterPathService) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { + 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 { - const pythonConfig = this.workspaceService.getConfiguration('python', this.workspace); - const pythonPathValue = pythonConfig.inspect('pythonPath'); + constructor(private workspace: Uri, private readonly interpreterPathService: IInterpreterPathService) {} + public async updatePythonPath(pythonPath: string | undefined): Promise { + 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; + updatePythonPath(pythonPath: string | undefined): Promise; } 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; + updatePythonPath( + pythonPath: string | undefined, + configTarget: ConfigurationTarget, + trigger: 'ui' | 'shebang' | 'load', + wkspace?: Uri, + ): Promise; } 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; + 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; + compare(a: PythonEnvironment, b: PythonEnvironment): number; + getRecommended(interpreters: PythonEnvironment[], resource: Resource): PythonEnvironment | undefined; +} + +export interface InterpreterQuickPickParams { + /** + * Specify `null` if a placeholder is not required. + */ + placeholder?: string | null; + /** + * Specify `null` if a title is not required. + */ + title?: string | null; + /** + * Specify `true` to skip showing recommended python interpreter. + */ + skipRecommended?: boolean; + + /** + * Specify `true` to show back button. + */ + showBackButton?: boolean; + + /** + * Show button to create a new environment. + */ + showCreateEnvironment?: boolean; +} + +export const IInterpreterQuickPick = Symbol('IInterpreterQuickPick'); +export interface IInterpreterQuickPick { + getInterpreterViaQuickPick( + workspace: Resource, + filter?: (i: PythonEnvironment) => boolean, + params?: InterpreterQuickPickParams, + ): Promise; +} + +export const IRecommendedEnvironmentService = Symbol('IRecommendedEnvironmentService'); +export interface IRecommendedEnvironmentService { + registerEnvApi(api: PythonExtension['environments']): void; + trackUserSelectedEnvironment(environmentPath: string | undefined, uri: Uri | undefined): void; + getRecommededEnvironment( + resource: Resource, + ): Promise< + | { + environment: ResolvedEnvironment; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; } diff --git a/src/client/interpreter/contracts.ts b/src/client/interpreter/contracts.ts index 2657275903e1..30a05c140249 100644 --- a/src/client/interpreter/contracts.ts +++ b/src/client/interpreter/contracts.ts @@ -1,146 +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; - getPipVersion(pythonPath: string): Promise; -} - -export const IKnownSearchPathsForInterpreters = Symbol('IKnownSearchPathsForInterpreters'); -export interface IKnownSearchPathsForInterpreters { - getSearchPaths(): string[]; -} -export const IVirtualEnvironmentsSearchPathProvider = Symbol('IVirtualEnvironmentsSearchPathProvider'); -export interface IVirtualEnvironmentsSearchPathProvider { - getSearchPaths(resource?: Uri): Promise; -} -export const IInterpreterLocatorService = Symbol('IInterpreterLocatorService'); +export const IComponentAdapter = Symbol('IComponentAdapter'); +export interface IComponentAdapter { + readonly onProgress: Event; + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + readonly onChanged: Event; + // VirtualEnvPrompt + onDidCreate(resource: Resource, callback: () => void): Disposable; + // IInterpreterLocatorService + hasInterpreters(filter?: (e: PythonEnvironment) => Promise): Promise; + getInterpreters(resource?: Uri, source?: PythonEnvSource[]): PythonEnvironment[]; + + // WorkspaceVirtualEnvInterpretersAutoSelectionRule + getWorkspaceVirtualEnvInterpreters( + resource: Uri, + options?: { ignoreCache?: boolean }, + ): Promise; + + // IInterpreterService + getInterpreterDetails(pythonPath: string): Promise; + + // IInterpreterHelper + // Undefined is expected on this API, if the environment info retrieval fails. + getInterpreterInformation(pythonPath: string): Promise | undefined>; + + isMacDefaultPythonPath(pythonPath: string): Promise; + + // ICondaService + isCondaEnvironment(interpreterPath: string): Promise; + // Undefined is expected on this API, if the environment is not conda env. + getCondaEnvironment(interpreterPath: string): Promise; -export interface IInterpreterLocatorService extends Disposable { - readonly onLocating: Event>; - readonly hasInterpreters: Promise; - getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise; + isMicrosoftStoreInterpreter(pythonPath: string): Promise; } -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; + getCondaFile(forShellExecution?: boolean): Promise; + getCondaInfo(): Promise; isCondaAvailable(): Promise; getCondaVersion(): Promise; - getCondaInfo(): Promise; - getCondaEnvironments(ignoreCache: boolean): Promise<({ name: string; path: string }[]) | undefined>; - getInterpreterPath(condaEnvironmentPath: string): string; + getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise; getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise; - isCondaEnvironment(interpreterPath: string): Promise; - getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined>; -} - -export enum InterpreterType { - Unknown = 'Unknown', - Conda = 'Conda', - VirtualEnv = 'VirtualEnv', - Pipenv = 'PipEnv', - Pyenv = 'Pyenv', - Venv = 'Venv' + 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; - onDidChangeInterpreterInformation: Event; - hasInterpreters: Promise; - getInterpreters(resource?: Uri): Promise; - getActiveInterpreter(resource?: Uri): Promise; - getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; + readonly refreshPromise: Promise | undefined; + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + readonly onDidChangeInterpreters: Event; + onDidChangeInterpreterConfiguration: Event; + onDidChangeInterpreter: Event; + onDidChangeInterpreterInformation: Event; + /** + * Note this API does not trigger the refresh but only works with the current refresh if any. Information + * returned by this is more or less upto date but is not guaranteed to be. + */ + hasInterpreters(filter?: (e: PythonEnvironment) => Promise): Promise; + getInterpreters(resource?: Uri): PythonEnvironment[]; + /** + * @deprecated Only exists for old Jupyter integration. + */ + getAllInterpreters(resource?: Uri): Promise; + getActiveInterpreter(resource?: Uri): Promise; + getInterpreterDetails(pythonPath: string, resoure?: Uri): Promise; refresh(resource: Resource): Promise; initialize(): void; - getDisplayName(interpreter: Partial): Promise; } export const IInterpreterDisplay = Symbol('IInterpreterDisplay'); export interface IInterpreterDisplay { refresh(resource?: Uri): Promise; -} - -export const IShebangCodeLensProvider = Symbol('IShebangCodeLensProvider'); -export interface IShebangCodeLensProvider extends CodeLensProvider { - detectShebang(document: TextDocument): Promise; + registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter): void; } export const IInterpreterHelper = Symbol('IInterpreterHelper'); export interface IInterpreterHelper { getActiveWorkspaceUri(resource: Resource): WorkspacePythonPath | undefined; - getInterpreterInformation(pythonPath: string): Promise>; - isMacDefaultPythonPath(pythonPath: string): Boolean; - getInterpreterTypeDisplayName(interpreterType: InterpreterType): string | undefined; - getBestInterpreter(interpreters?: PythonInterpreter[]): PythonInterpreter | undefined; -} - -export const IPipEnvService = Symbol('IPipEnvService'); -export interface IPipEnvService { - executable: string; - isRelatedPipEnvironment(dir: string, pythonPath: string): Promise; -} - -export const IInterpreterLocatorHelper = Symbol('IInterpreterLocatorHelper'); -export interface IInterpreterLocatorHelper { - mergeInterpreters(interpreters: PythonInterpreter[]): Promise; -} - -export const IInterpreterWatcher = Symbol('IInterpreterWatcher'); -export interface IInterpreterWatcher { - onDidCreate: Event; + getInterpreterInformation(pythonPath: string): Promise>; + isMacDefaultPythonPath(pythonPath: string): Promise; + getInterpreterTypeDisplayName(interpreterType: EnvironmentType): string | undefined; + getBestInterpreter(interpreters?: PythonEnvironment[]): PythonEnvironment | undefined; } -export const IInterpreterWatcherBuilder = Symbol('IInterpreterWatcherBuilder'); -export interface IInterpreterWatcherBuilder { - getWorkspaceVirtualEnvInterpreterWatcher(resource: Resource): Promise; +export const IInterpreterStatusbarVisibilityFilter = Symbol('IInterpreterStatusbarVisibilityFilter'); +/** + * Implement this interface to control the visibility of the interpreter statusbar. + */ +export interface IInterpreterStatusbarVisibilityFilter { + readonly changed?: Event; + readonly hidden: boolean; } -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; - readonly onRefreshed: Event; - register(): void; +export const IActivatedEnvironmentLaunch = Symbol('IActivatedEnvironmentLaunch'); +export interface IActivatedEnvironmentLaunch { + selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection?: boolean): Promise; } diff --git a/src/client/interpreter/display/index.ts b/src/client/interpreter/display/index.ts index 56cd458fdfcc..3a602093d4f9 100644 --- a/src/client/interpreter/display/index.ts +++ b/src/client/interpreter/display/index.ts @@ -1,40 +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 { 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 { IInterpreterAutoSelectionService } from '../autoSelection/types'; -import { IInterpreterDisplay, IInterpreterHelper, IInterpreterService, PythonInterpreter } 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 readonly autoSelection: IInterpreterAutoSelectionService; + 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); this.workspaceService = serviceContainer.get(IWorkspaceService); this.pathUtils = serviceContainer.get(IPathUtils); this.interpreterService = serviceContainer.get(IInterpreterService); - this.autoSelection = serviceContainer.get(IInterpreterAutoSelectionService); - const application = serviceContainer.get(IApplicationShell); - const disposableRegistry = serviceContainer.get(IDisposableRegistry); + this.disposableRegistry = serviceContainer.get(IDisposableRegistry); - this.statusBar = application.createStatusBarItem(StatusBarAlignment.Left, 100); - this.statusBar.command = 'python.setInterpreter'; - disposableRegistry.push(this.statusBar); + this.interpreterService.onDidChangeInterpreterInformation( + this.onDidChangeInterpreterInformation, + this, + this.disposableRegistry, + ); + } - this.interpreterService.onDidChangeInterpreterInformation(this.onDidChangeInterpreterInformation, this, disposableRegistry); + public async activate(): Promise { + if (shouldEnvExtHandleActivation()) { + return; + } + const application = this.serviceContainer.get(IApplicationShell); + if (this.useLanguageStatus) { + this.languageStatus = application.createLanguageStatusItem('python.selectedInterpreter', { + language: PYTHON_LANGUAGE, + }); + this.languageStatus.severity = LanguageStatusSeverity.Information; + this.languageStatus.command = { + title: InterpreterQuickPickList.browsePath.openButtonLabel, + command: Commands.Set_Interpreter, + }; + this.disposableRegistry.push(this.languageStatus); + } else { + const [alignment, priority] = [StatusBarAlignment.Right, STATUS_BAR_ITEM_PRIORITY]; + this.statusBar = application.createStatusBarItem(alignment, priority, 'python.selectedInterpreterDisplay'); + this.statusBar.command = Commands.Set_Interpreter; + this.disposableRegistry.push(this.statusBar); + this.statusBar.name = Interpreters.selectedPythonInterpreter; + } } + public async refresh(resource?: Uri) { // Use the workspace Uri if available if (resource && this.workspaceService.getWorkspaceFolder(resource)) { @@ -46,26 +102,94 @@ export class InterpreterDisplay implements IInterpreterDisplay { } await this.updateDisplay(resource); } - private onDidChangeInterpreterInformation(info: PythonInterpreter) { - if (!this.currentlySelectedInterpreterPath || this.currentlySelectedInterpreterPath === info.path) { + public registerVisibilityFilter(filter: IInterpreterStatusbarVisibilityFilter) { + const disposableRegistry = this.serviceContainer.get(IDisposableRegistry); + this.visibilityFilters.push(filter); + if (filter.changed) { + filter.changed(this.updateVisibility, this, disposableRegistry); + } + } + private onDidChangeInterpreterInformation(info: PythonEnvironment) { + if (this.currentlySelectedInterpreterPath === info.path) { this.updateDisplay(this.currentlySelectedWorkspaceFolder).ignoreErrors(); } } private async updateDisplay(workspaceFolder?: Uri) { - await this.autoSelection.autoSelectInterpreter(workspaceFolder); + if (shouldEnvExtHandleActivation()) { + this.statusBar?.hide(); + this.languageStatus?.dispose(); + this.languageStatus = undefined; + return; + } const interpreter = await this.interpreterService.getActiveInterpreter(workspaceFolder); + if ( + this.currentlySelectedInterpreterDisplay && + this.currentlySelectedInterpreterDisplay === interpreter?.detailedDisplayName && + this.currentlySelectedInterpreterPath === interpreter.path + ) { + return; + } this.currentlySelectedWorkspaceFolder = workspaceFolder; - if (interpreter) { - this.statusBar.color = ''; - this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder ? workspaceFolder.fsPath : undefined); - this.statusBar.text = interpreter.displayName!; - this.currentlySelectedInterpreterPath = interpreter.path; + if (this.statusBar) { + if (interpreter) { + this.statusBar.color = ''; + this.statusBar.tooltip = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); + if (this.currentlySelectedInterpreterPath !== interpreter.path) { + traceLog( + l10n.t( + 'Python interpreter path: {0}', + this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath), + ), + ); + this.currentlySelectedInterpreterPath = interpreter.path; + } + let text = interpreter.detailedDisplayName; + text = text?.startsWith('Python') ? text?.substring('Python'.length)?.trim() : text; + this.statusBar.text = text ?? ''; + this.statusBar.backgroundColor = undefined; + this.currentlySelectedInterpreterDisplay = interpreter.detailedDisplayName; + } else { + this.statusBar.tooltip = ''; + this.statusBar.color = ''; + this.statusBar.backgroundColor = new ThemeColor('statusBarItem.warningBackground'); + this.statusBar.text = `$(alert) ${InterpreterQuickPickList.browsePath.openButtonLabel}`; + this.currentlySelectedInterpreterDisplay = undefined; + } + } else if (this.languageStatus) { + if (interpreter) { + this.languageStatus.detail = this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath); + if (this.currentlySelectedInterpreterPath !== interpreter.path) { + traceLog( + l10n.t( + 'Python interpreter path: {0}', + this.pathUtils.getDisplayName(interpreter.path, workspaceFolder?.fsPath), + ), + ); + this.currentlySelectedInterpreterPath = interpreter.path; + } + let text = interpreter.detailedDisplayName!; + text = text.startsWith('Python') ? text.substring('Python'.length).trim() : text; + this.languageStatus.text = text; + this.currentlySelectedInterpreterDisplay = interpreter.detailedDisplayName; + this.languageStatus.severity = LanguageStatusSeverity.Information; + } else { + this.languageStatus.severity = LanguageStatusSeverity.Warning; + this.languageStatus.text = `$(alert) ${InterpreterQuickPickList.browsePath.openButtonLabel}`; + this.languageStatus.detail = undefined; + this.currentlySelectedInterpreterDisplay = undefined; + } + } + this.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.currentlySelectedInterpreterPath = undefined; + this.statusBar.hide(); } - this.statusBar.show(); } } diff --git a/src/client/interpreter/display/interpreterSelectionTip.ts b/src/client/interpreter/display/interpreterSelectionTip.ts deleted file mode 100644 index c4b7820c6475..000000000000 --- a/src/client/interpreter/display/interpreterSelectionTip.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { IExtensionActivationService } from '../../activation/types'; -import { IApplicationShell } from '../../common/application/types'; -import { IPersistentState, IPersistentStateFactory, Resource } from '../../common/types'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { Common, Interpreters } from '../../common/utils/localize'; - -@injectable() -export class InterpreterSelectionTip implements IExtensionActivationService { - private readonly storage: IPersistentState; - private displayedInSession: boolean = false; - constructor(@inject(IApplicationShell) private readonly shell: IApplicationShell, - @inject(IPersistentStateFactory) private readonly factory: IPersistentStateFactory) { - this.storage = this.factory.createGlobalPersistentState('InterpreterSelectionTip', false); - } - public async activate(_resource: Resource): Promise { - if (this.storage.value || this.displayedInSession) { - return; - } - this.displayedInSession = true; - this.showTip().ignoreErrors(); - } - @swallowExceptions('Failed to display tip') - private async showTip() { - const selection = await this.shell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt()); - if (selection !== Common.gotIt()) { - return; - } - await this.storage.updateValue(true); - } -} diff --git a/src/client/interpreter/display/progressDisplay.ts b/src/client/interpreter/display/progressDisplay.ts index 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 | 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 { + 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 a3bd085508ad..000000000000 --- a/src/client/interpreter/display/shebangCodeLensProvider.ts +++ /dev/null @@ -1,73 +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; - constructor(@inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IWorkspaceService) workspaceService: IWorkspaceService) { - // tslint:disable-next-line:no-any - this.onDidChangeCodeLenses = workspaceService.onDidChangeConfiguration as any as Event; - - } - public async detectShebang(document: TextDocument): Promise { - 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 { - 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); - if (!shebang) { - return []; - } - const pythonPath = this.configurationService.getSettings(document.uri).pythonPath; - const resolvedPythonPath = await this.getFullyQualifiedPathToInterpreter(pythonPath, document.uri); - if (shebang === resolvedPythonPath) { - return []; - } - const firstLine = document.lineAt(0); - const startOfShebang = new Position(0, 0); - const endOfShebang = new Position(0, firstLine.text.length - 1); - const shebangRange = new Range(startOfShebang, endOfShebang); - - const cmd: Command = { - command: 'python.setShebangInterpreter', - title: 'Set as interpreter' - }; - - return [(new CodeLens(shebangRange, cmd))]; - } -} diff --git a/src/client/interpreter/helpers.ts b/src/client/interpreter/helpers.ts index eeff9b796eea..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 & { 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); - this.fs = this.serviceContainer.get(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); - 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> { - let fileHash = await this.fs.getFileHash(pythonPath).catch(() => ''); - fileHash = fileHash ? fileHash : ''; - const store = this.persistentFactory.createGlobalPersistentState(`${pythonPath}.v3`, undefined, EXPITY_DURATION); - if (store.value && fileHash && store.value.fileHash === fileHash) { - return store.value; - } - const processService = await this.serviceContainer.get(IPythonExecutionFactory).create({ pythonPath }); - try { - const info = await processService.getInterpreterInformation().catch(() => undefined); - if (!info) { - return; - } - const details = { - ...(info), - fileHash - }; - await store.updateValue(details); - return details; - } catch (ex) { - console.error(`Failed to get interpreter information for '${pythonPath}'`, ex); - return; - } + public async getInterpreterInformation(pythonPath: string): Promise> { + 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 { + 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 { + 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 { + this.disposables.push( + registerCommand(Commands.GetSelectedInterpreterPath, (args) => this._getSelectedInterpreterPath(args)), + ); + } + + public async _getSelectedInterpreterPath( + args: { workspaceFolder: string; type: string } | string[], + ): Promise { + // If `launch.json` is launching this command, `args.workspaceFolder` carries the workspaceFolder + // If `tasks.json` is launching this command, `args[1]` carries the workspaceFolder + let workspaceFolder; + if ('workspaceFolder' in args) { + workspaceFolder = args.workspaceFolder; + } else if (args[1]) { + const [, second] = args; + workspaceFolder = second; + } else if (useEnvExtension() && 'type' in args && args.type === 'debugpy') { + // If using the envsExt and the type is debugpy, we need to add the workspace folder to get the interpreter path. + if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { + workspaceFolder = workspace.workspaceFolders[0].uri.fsPath; + } + } else { + workspaceFolder = undefined; + } + + let workspaceFolderUri; + try { + workspaceFolderUri = workspaceFolder ? Uri.file(workspaceFolder) : undefined; + } catch (ex) { + workspaceFolderUri = undefined; + } + + const interpreterPath = + (await this.interpreterService.getActiveInterpreter(workspaceFolderUri))?.path ?? 'python'; + return interpreterPath.toCommandArgumentForPythonExt(); + } +} diff --git a/src/client/interpreter/interpreterService.ts b/src/client/interpreter/interpreterService.ts index 4019ed324ecf..ad06fd7d051d 100644 --- a/src/client/interpreter/interpreterService.ts +++ b/src/client/interpreter/interpreterService.ts @@ -1,288 +1,322 @@ +// eslint-disable-next-line max-classes-per-file import { inject, injectable } from 'inversify'; -import * as md5 from 'md5'; -import * as path from 'path'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import '../../client/common/extensions'; -import { IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { getArchitectureDisplayName } from '../common/platform/registry'; -import { IFileSystem } from '../common/platform/types'; -import { IPythonExecutionFactory } from '../common/process/types'; -import { IConfigurationService, IDisposableRegistry, IPersistentState, IPersistentStateFactory, Resource } 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 { PythonEnvironment } from '../pythonEnvironments/info'; +import { + 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 { - IInterpreterDisplay, IInterpreterHelper, IInterpreterLocatorService, - IInterpreterService, INTERPRETER_LOCATOR_SERVICE, - InterpreterType, PythonInterpreter} from './contracts'; -import { IVirtualEnvironmentManager } from './virtualEnvs/types'; + 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(); - private readonly didChangeInterpreterInformation = new EventEmitter(); - private readonly inMemoryCacheOfDisplayNames = new Map(); - private readonly updatedInterpreters = new Set(); - private pythonPathSetting: string = ''; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.locator = serviceContainer.get(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - this.fs = this.serviceContainer.get(IFileSystem); - this.persistentStateFactory = this.serviceContainer.get(IPersistentStateFactory); - this.configService = this.serviceContainer.get(IConfigurationService); - } - public get hasInterpreters(): Promise { - return this.locator.hasInterpreters; + public async hasInterpreters( + filter: (e: PythonEnvironment) => Promise = async () => true, + ): Promise { + return this.pyenvs.hasInterpreters(filter); } - public async refresh(resource?: Uri) { - const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); - return interpreterDisplay.refresh(resource); + public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise { + return this.pyenvs.triggerRefresh(query, options); } - public initialize() { - const disposables = this.serviceContainer.get(IDisposableRegistry); - const documentManager = this.serviceContainer.get(IDocumentManager); - disposables.push(documentManager.onDidChangeActiveTextEditor((e) => e ? this.refresh(e.document.uri) : undefined)); - const workspaceService = this.serviceContainer.get(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 refreshPromise(): Promise | undefined { + return this.pyenvs.getRefreshPromise(); } - @captureTelemetry(EventName.PYTHON_INTERPRETER_DISCOVERY, { locator: 'all' }, true) - public async getInterpreters(resource?: Uri): Promise { - 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); - // Keep information up to date with latest details. - if (!item.cachedEntry) { - this.updateCachedInterpreterInformation(item, resource).ignoreErrors(); - } - })); - return interpreters; + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this.pyenvs.getRefreshPromise(options); } - public dispose(): void { - this.locator.dispose(); - this.didChangeInterpreterEmitter.dispose(); - this.didChangeInterpreterInformation.dispose(); - } - - public get onDidChangeInterpreter(): Event { + public get onDidChangeInterpreter(): Event { return this.didChangeInterpreterEmitter.event; } - public get onDidChangeInterpreterInformation(): Event { + public onDidChangeInterpreters: Event; + + public get onDidChangeInterpreterInformation(): Event { return this.didChangeInterpreterInformation.event; } - public async getActiveInterpreter(resource?: Uri): Promise { - const pythonExecutionFactory = this.serviceContainer.get(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; - } - - return this.getInterpreterDetails(fullyQualifiedPath, resource); + public get onDidChangeInterpreterConfiguration(): Event { + return this.didChangeInterpreterConfigurationEmitter.event; } - public async getInterpreterDetails(pythonPath: string, resource?: Uri): Promise { - // If we don't have the fully qualified path, then get it. - if (path.basename(pythonPath) === pythonPath) { - const pythonExecutionFactory = this.serviceContainer.get(IPythonExecutionFactory); - const pythonExecutionService = await pythonExecutionFactory.create({ resource }); - pythonPath = await pythonExecutionService.getExecutablePath().catch(() => ''); - // Python path is invalid or python isn't installed. - if (!pythonPath) { - return; - } - } - const store = await this.getInterpreterCache(pythonPath); - if (store.value && store.value.info) { - return store.value.info; - } + public _pythonPathSetting: string | undefined = ''; - const fs = this.serviceContainer.get(IFileSystem); + private readonly didChangeInterpreterConfigurationEmitter = new EventEmitter(); - // 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; - })(); + private readonly configService: IConfigurationService; - // This is the preferred approach, hence the delay in option 1. - const option2 = (async () => { - const interpreters = await this.getInterpreters(resource); - const found = interpreters.find(i => fs.arePathsSame(i.path, pythonPath)); - if (found) { - // Cache the interpreter info, only if we get the data from interpretr list. - // tslint:disable-next-line:no-any - (found as any).__store = true; - return found; - } - // Use option1 as a fallback. - // tslint:disable-next-line:no-any - return option1 as any as PythonInterpreter; - })(); + private readonly interpreterPathService: IInterpreterPathService; - const interpreterInfo = await Promise.race([option2, option1]) as PythonInterpreter; + private readonly didChangeInterpreterEmitter = new EventEmitter(); - // tslint:disable-next-line:no-any - if (interpreterInfo && (interpreterInfo as any).__store) { - await this.updateCachedInterpreterInformation(interpreterInfo, resource); - } else { - // If we got information from option1, then when option2 finishes cache it for later use (ignoring erors); - option2.then(async info => { - // tslint:disable-next-line:no-any - if (info && (info as any).__store) { - await this.updateCachedInterpreterInformation(info, resource); - } - }).ignoreErrors(); - } - return interpreterInfo; + private readonly didChangeInterpreterInformation = new EventEmitter(); + + 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); + this.interpreterPathService = this.serviceContainer.get(IInterpreterPathService); + this.onDidChangeInterpreters = pyenvs.onChanged; } - /** - * Gets the display name of an interpreter. - * The format is `Python (: )` - * E.g. `Python 3.5.1 32-bit (myenv2: virtualenv)` - * @param {Partial} info - * @returns {string} - * @memberof InterpreterService - */ - public async getDisplayName(info: Partial, resource?: Uri): Promise { - // faster than calculating file has agian and again, only when deailing with cached items. - if (!info.cachedEntry && info.path && this.inMemoryCacheOfDisplayNames.has(info.path)) { - return this.inMemoryCacheOfDisplayNames.get(info.path)!; - } - const fileHash = (info.path ? await this.fs.getFileHash(info.path).catch(() => '') : '') || ''; - // Do not include dipslay name into hash as that changes. - const interpreterHash = `${fileHash}-${md5(JSON.stringify({ ...info, displayName: '' }))}`; - const store = this.persistentStateFactory.createGlobalPersistentState<{ hash: string; displayName: string }>(`${info.path}.interpreter.displayName.v7`, undefined, EXPITY_DURATION); - if (store.value && store.value.hash === interpreterHash && store.value.displayName) { - this.inMemoryCacheOfDisplayNames.set(info.path!, store.value.displayName); - return store.value.displayName; - } - const displayName = await this.buildInterpreterDisplayName(info, resource); + public async refresh(resource?: Uri): Promise { + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + await interpreterDisplay.refresh(resource); + const workspaceFolder = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolder(resource); + const path = this.configService.getSettings(resource).pythonPath; + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path, workspaceFolder }); + this.ensureEnvironmentContainsPython(path, workspaceFolder).ignoreErrors(); + } - // If dealing with cached entry, then do not store the display name in cache. - if (!info.cachedEntry) { - await store.updateValue({ displayName, hash: interpreterHash }); - this.inMemoryCacheOfDisplayNames.set(info.path!, displayName); - } + public initialize(): void { + const disposables = this.serviceContainer.get(IDisposableRegistry); + const documentManager = this.serviceContainer.get(IDocumentManager); + const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); + const filter = new (class implements IInterpreterStatusbarVisibilityFilter { + constructor( + private readonly docManager: IDocumentManager, + private readonly configService: IConfigurationService, + private readonly disposablesReg: IDisposableRegistry, + ) { + this.disposablesReg.push( + this.configService.onDidChange(async (event: ConfigurationChangeEvent | undefined) => { + if (event?.affectsConfiguration('python.interpreter.infoVisibility')) { + this.interpreterVisibilityEmitter.fire(); + } + }), + ); + } + + public readonly interpreterVisibilityEmitter = new EventEmitter(); + + public readonly changed = this.interpreterVisibilityEmitter.event; - return displayName; + get hidden() { + const visibility = this.configService.getSettings().interpreter.infoVisibility; + if (visibility === 'never') { + return true; + } + if (visibility === 'always') { + return false; + } + const document = this.docManager.activeTextEditor?.document; + // Output channel for MS Python related extensions. These contain "ms-python" in their ID. + const pythonOutputChannelPattern = PVSC_EXTENSION_ID.split('.')[0]; + if ( + document?.fileName.endsWith('settings.json') || + document?.fileName.includes(pythonOutputChannelPattern) + ) { + return false; + } + return document?.languageId !== PYTHON_LANGUAGE; + } + })(documentManager, this.configService, disposables); + interpreterDisplay.registerVisibilityFilter(filter); + disposables.push( + this.onDidChangeInterpreters((e): void => { + const interpreter = e.old ?? e.new; + if (interpreter) { + this.didChangeInterpreterInformation.fire(interpreter); + for (const { path, workspaceFolder } of this.activeInterpreterPaths.values()) { + if (path === interpreter.path && !e.new) { + // If the active environment got deleted, notify it. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path, + resource: workspaceFolder, + }); + } + } + } + }), + ); + disposables.push( + documentManager.onDidOpenTextDocument(() => { + // To handle scenario when language mode is set to "python" + filter.interpreterVisibilityEmitter.fire(); + }), + documentManager.onDidChangeActiveTextEditor((e): void => { + filter.interpreterVisibilityEmitter.fire(); + if (e && e.document) { + this.refresh(e.document.uri); + } + }), + ); + disposables.push(this.interpreterPathService.onDidChange((i) => this._onConfigChanged(i.uri))); } - public async getInterpreterCache(pythonPath: string): Promise> { - const fileHash = (pythonPath ? await this.fs.getFileHash(pythonPath).catch(() => '') : '') || ''; - const store = this.persistentStateFactory.createGlobalPersistentState<{ fileHash: string; info?: PythonInterpreter }>(`${pythonPath}.interpreter.Details.v7`, undefined, EXPITY_DURATION); - if (!store.value || store.value.fileHash !== fileHash) { - await store.updateValue({ fileHash }); - } - return store; + + public getInterpreters(resource?: Uri): PythonEnvironment[] { + return this.pyenvs.getInterpreters(resource); } - protected async updateCachedInterpreterInformation(info: PythonInterpreter, resource: Resource): Promise{ - const key = JSON.stringify(info); - if (this.updatedInterpreters.has(key)) { - return; - } - this.updatedInterpreters.add(key); - const state = await this.getInterpreterCache(info.path); - info.displayName = await this.getDisplayName(info, resource); - // Check if info has indeed changed. - if (state.value && state.value.info && - JSON.stringify(info) === JSON.stringify(state.value.info)) { - return; - } - this.inMemoryCacheOfDisplayNames.delete(info.path); - await state.updateValue({ fileHash: state.value.fileHash, info }); - this.didChangeInterpreterInformation.fire(info); + + public async getAllInterpreters(resource?: Uri): Promise { + // For backwards compatibility with old Jupyter APIs, ensure a + // fresh refresh is always triggered when using the API. As it is + // no longer auto-triggered by the extension. + this.triggerRefresh(undefined, { ifNotTriggerredAlready: true }).ignoreErrors(); + await this.refreshPromise; + return this.getInterpreters(resource); } - protected async buildInterpreterDisplayName(info: Partial, resource?: Uri): Promise{ - const displayNameParts: string[] = ['Python']; - const envSuffixParts: string[] = []; - if (info.version) { - displayNameParts.push(`${info.version.major}.${info.version.minor}.${info.version.patch}`); - } - if (info.architecture) { - displayNameParts.push(getArchitectureDisplayName(info.architecture)); - } - if (!info.envName && info.path && info.type && info.type === InterpreterType.Pipenv) { - // If we do not have the name of the environment, then try to get it again. - // This can happen based on the context (i.e. resource). - // I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource). - const virtualEnvMgr = this.serviceContainer.get(IVirtualEnvironmentManager); - info.envName = await virtualEnvMgr.getEnvironmentName(info.path, resource); - } - if (info.envName && info.envName.length > 0) { - envSuffixParts.push(`'${info.envName}'`); + public dispose(): void { + this.didChangeInterpreterEmitter.dispose(); + this.didChangeInterpreterInformation.dispose(); + } + + public async getActiveInterpreter(resource?: Uri): Promise { + if (useEnvExtension()) { + return getActiveInterpreterLegacy(resource); } - if (info.type) { - const interpreterHelper = this.serviceContainer.get(IInterpreterHelper); - const name = interpreterHelper.getInterpreterTypeDisplayName(info.type); - if (name) { - envSuffixParts.push(name); + + const activatedEnvLaunch = this.serviceContainer.get(IActivatedEnvironmentLaunch); + let path = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(true); + // This is being set as interpreter in background, after which it'll show up in `.pythonPath` config. + // However we need not wait on the update to take place, as we can use the value directly. + if (!path) { + path = this.configService.getSettings(resource).pythonPath; + if (pathUtils.basename(path) === path) { + // Value can be `python`, `python3`, `python3.9` etc. + // Note the following triggers autoselection if no interpreter is explictly + // selected, i.e the value is `python`. + // During shutdown we might not be able to get items out of the service container. + const pythonExecutionFactory = this.serviceContainer.tryGet( + IPythonExecutionFactory, + ); + const pythonExecutionService = pythonExecutionFactory + ? await pythonExecutionFactory.create({ resource }) + : undefined; + const fullyQualifiedPath = pythonExecutionService + ? await pythonExecutionService.getExecutablePath().catch((ex) => { + traceError(ex); + }) + : undefined; + // Python path is invalid or python isn't installed. + if (!fullyQualifiedPath) { + return undefined; + } + path = fullyQualifiedPath; } } + return this.getInterpreterDetails(path); + } - const envSuffix = envSuffixParts.length === 0 ? '' : - `(${envSuffixParts.join(': ')})`; - return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); + public async getInterpreterDetails(pythonPath: string): Promise { + 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 { + // 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) + .getWorkspaceFolder(resource); + reportActiveInterpreterChanged({ + path: pySettings.pythonPath, + resource: workspaceFolder, + }); + const workspaceKey = this.serviceContainer + .get(IWorkspaceService) + .getWorkspaceFolderIdentifier(resource); + this.activeInterpreterPaths.set(workspaceKey, { path: pySettings.pythonPath, workspaceFolder }); const interpreterDisplay = this.serviceContainer.get(IInterpreterDisplay); - interpreterDisplay.refresh() - .catch(ex => 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); - const virtualEnvManager = this.serviceContainer.get(IVirtualEnvironmentManager); - const [info, type] = await Promise.all([ - interpreterHelper.getInterpreterInformation(pythonPath), - virtualEnvManager.getEnvironmentType(pythonPath) - ]); - if (!info) { + + @cache(-1, true) + private async ensureEnvironmentContainsPython(pythonPath: string, workspaceFolder: WorkspaceFolder | undefined) { + if (useEnvExtension()) { return; } - const details: Partial = { - ...(info as PythonInterpreter), - path: pythonPath, - type: type - }; - const envName = type === InterpreterType.Unknown ? undefined : await virtualEnvManager.getEnvironmentName(pythonPath, resource); - const pthonInfo = { - ...(details as PythonInterpreter), - envName - }; - pthonInfo.displayName = await this.getDisplayName(pthonInfo, resource); - return pthonInfo; + const installer = this.serviceContainer.get(IInstaller); + if (!(await installer.isInstalled(Product.python))) { + // If Python is not installed into the environment, install it. + sendTelemetryEvent(EventName.ENVIRONMENT_WITHOUT_PYTHON_SELECTED); + const shell = this.serviceContainer.get(IApplicationShell); + const progressOptions: ProgressOptions = { + location: ProgressLocation.Window, + title: `[${Interpreters.installingPython}](command:${Commands.ViewOutput})`, + }; + traceLog('Conda envs without Python are known to not work well; fixing conda environment...'); + const promise = installer.install(Product.python, await this.getInterpreterDetails(pythonPath)); + shell.withProgress(progressOptions, () => promise); + promise + .then(async () => { + // Fetch interpreter details so the cache is updated to include the newly installed Python. + await this.getInterpreterDetails(pythonPath); + // Fire an event as the executable for the environment has changed. + this.didChangeInterpreterEmitter.fire(workspaceFolder?.uri); + reportActiveInterpreterChanged({ + path: pythonPath, + resource: workspaceFolder, + }); + }) + .ignoreErrors(); + } } } diff --git a/src/client/interpreter/interpreterVersion.ts b/src/client/interpreter/interpreterVersion.ts deleted file mode 100644 index 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 { - 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 { - 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 4cc38ebe262a..000000000000 --- a/src/client/interpreter/locators/helpers.ts +++ /dev/null @@ -1,73 +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 { IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../contracts'; -import { IPipEnvServiceHelper } from './types'; - -const CheckPythonInterpreterRegEx = IS_WINDOWS ? /^python(\d+(.\d+)?)?\.exe$/ : /^python(\d+(.\d+)?)?$/; - -export function lookForInterpretersInDirectory(pathToCheck: string): Promise { - 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 { - constructor( - @inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IPipEnvServiceHelper) private readonly pipEnvServiceHelper: IPipEnvServiceHelper - ) { - } - public async mergeInterpreters(interpreters: PythonInterpreter[]): Promise { - const items = interpreters - .map(item => { return { ...item }; }) - .map(item => { item.path = path.normalize(item.path); return item; }) - .reduce((accumulator, current) => { - const currentVersion = current && current.version ? current.version.raw : undefined; - const existingItem = accumulator.find(item => { - // If same version and same base path, then ignore. - // Could be Python 3.6 with path = python.exe, and Python 3.6 and path = python3.exe. - if (item.version && item.version.raw === currentVersion && - item.path && current.path && - this.fs.arePathsSame(path.dirname(item.path), path.dirname(current.path))) { - return true; - } - return false; - }); - if (!existingItem) { - accumulator.push(current); - } else { - // Preserve type information. - // Possible we identified environment as unknown, but a later provider has identified env type. - if (existingItem.type === InterpreterType.Unknown && current.type !== InterpreterType.Unknown) { - existingItem.type = current.type; - } - const props: (keyof PythonInterpreter)[] = ['envName', 'envPath', 'path', 'sysPrefix', - 'architecture', 'sysVersion', 'version']; - for (const prop of props) { - if (!existingItem[prop] && current[prop]) { - // tslint:disable-next-line: no-any - (existingItem as any)[prop] = current[prop]; - } - } - } - return accumulator; - }, []); - // This stuff needs to be fast. - await Promise.all(items.map(async item => { - const info = await this.pipEnvServiceHelper.getPipEnvInfo(item.path); - if (info) { - item.type = InterpreterType.Pipenv; - item.pipEnvWorkspaceFolder = info.workspaceFolder.fsPath; - item.envName = info.envName || item.envName; - } - })); - return items; - } -} diff --git a/src/client/interpreter/locators/index.ts b/src/client/interpreter/locators/index.ts deleted file mode 100644 index 53cba0fecbda..000000000000 --- a/src/client/interpreter/locators/index.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import { traceDecorators } from '../../common/logger'; -import { IPlatformService } from '../../common/platform/types'; -import { IDisposableRegistry } from '../../common/types'; -import { createDeferred, Deferred } from '../../common/utils/async'; -import { OSType } from '../../common/utils/platform'; -import { 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; - constructor( - @inject(IServiceContainer) private serviceContainer: IServiceContainer - ) { - this._hasInterpreters = createDeferred(); - serviceContainer.get(IDisposableRegistry).push(this); - this.platform = serviceContainer.get(IPlatformService); - this.interpreterLocatorHelper = serviceContainer.get(IInterpreterLocatorHelper); - } - /** - * This class should never emit events when we're locating. - * The events will be fired by the indivitual locators retrieved in `getLocators`. - * - * @readonly - * @type {Event>} - * @memberof PythonInterpreterLocatorService - */ - public get onLocating(): Event> { - return new EventEmitter>().event; - } - public get hasInterpreters(): Promise { - return this._hasInterpreters.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. - */ - @traceDecorators.verbose('Get Interpreters') - public async getInterpreters(resource?: Uri): Promise { - 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, item[0])); - } -} diff --git a/src/client/interpreter/locators/progressService.ts b/src/client/interpreter/locators/progressService.ts deleted file mode 100644 index 20aaa74cc537..000000000000 --- a/src/client/interpreter/locators/progressService.ts +++ /dev/null @@ -1,68 +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[] = []; - private readonly refreshing = new EventEmitter(); - private readonly refreshed = new EventEmitter(); - private readonly locators: IInterpreterLocatorService[] = []; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer, - @inject(IDisposableRegistry) private readonly disposables: Disposable[]) { - this.locators = serviceContainer.getAll(IInterpreterLocatorService); - } - - public get onRefreshing(): Event { - return this.refreshing.event; - } - public get onRefreshed(): Event { - return this.refreshed.event; - } - public register(): void { - this.locators.forEach(locator => { - locator.onLocating(this.handleProgress, this, this.disposables); - }); - } - @traceDecorators.verbose('Detected refreshing of Interpreters') - private handleProgress(promise: Promise) { - this.deferreds.push(createDeferredFrom(promise)); - this.notifyRefreshing(); - this.checkProgress(); - } - @traceDecorators.verbose('All locators have completed locating') - private notifyCompleted() { - this.refreshed.fire(); - } - @traceDecorators.verbose('Notify locators are locating') - private notifyRefreshing() { - this.refreshing.fire(); - } - private checkProgress() { - if (this.deferreds.length === 0) { - return; - } - if (this.areAllItemsComplete()) { - return this.notifyCompleted(); - } - Promise.all(this.deferreds.map(item => item.promise)) - .catch(noop) - .then(() => this.checkProgress()) - .ignoreErrors(); - } - @traceDecorators.verbose('Checking whether locactors have completed locating') - private areAllItemsComplete() { - this.deferreds = this.deferreds.filter(item => !item.completed); - return this.deferreds.length === 0; - } -} diff --git a/src/client/interpreter/locators/services/KnownPathsService.ts b/src/client/interpreter/locators/services/KnownPathsService.ts deleted file mode 100644 index ee033e322bc7..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 { - return this.suggestionsFromKnownPaths(); - } - - /** - * Return the located interpreters. - */ - private suggestionsFromKnownPaths() { - const promises = this.knownSearchPaths.getSearchPaths().map(dir => this.getInterpretersInDirectory(dir)); - return Promise.all(promises) - .then(listOfInterpreters => flatten(listOfInterpreters)) - .then(interpreters => interpreters.filter(item => item.length > 0)) - .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))) - .then(interpreters => interpreters.filter(interpreter => !!interpreter).map(interpreter => interpreter!)); - } - - /** - * Return the information about the identified interpreter binary. - */ - private async getInterpreterDetails(interpreter: string) { - const details = await this.helper.getInterpreterInformation(interpreter); - if (!details) { - return; - } - this._hasInterpreters.resolve(true); - return { - ...(details as PythonInterpreter), - path: interpreter, - type: InterpreterType.Unknown - }; - } - - /** - * Return the interpreters in the given directory. - */ - private getInterpretersInDirectory(dir: string) { - return fsExistsAsync(dir) - .then(exists => exists ? lookForInterpretersInDirectory(dir) : Promise.resolve([])); - } -} - -@injectable() -export class KnownSearchPathsForInterpreters implements IKnownSearchPathsForInterpreters { - constructor(@inject(IServiceContainer) private readonly serviceContainer: IServiceContainer) { } - /** - * Return the paths where Python interpreters might be found. - */ - public getSearchPaths(): string[] { - const currentProcess = this.serviceContainer.get(ICurrentProcess); - const platformService = this.serviceContainer.get(IPlatformService); - const pathUtils = this.serviceContainer.get(IPathUtils); - - const searchPaths = currentProcess.env[platformService.pathVariableName]! - .split(pathUtils.delimiter) - .map(p => p.trim()) - .filter(p => p.length > 0); - - if (!platformService.isWindows) { - ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin'] - .forEach(p => { - searchPaths.push(p); - searchPaths.push(path.join(pathUtils.home, p)); - }); - // Add support for paths such as /Users/xxx/anaconda/bin. - if (process.env.HOME) { - searchPaths.push(path.join(pathUtils.home, 'anaconda', 'bin')); - searchPaths.push(path.join(pathUtils.home, 'python', 'bin')); - } - } - return searchPaths; - } -} diff --git a/src/client/interpreter/locators/services/baseVirtualEnvService.ts b/src/client/interpreter/locators/services/baseVirtualEnvService.ts deleted file mode 100644 index ef6a53b653d7..000000000000 --- a/src/client/interpreter/locators/services/baseVirtualEnvService.ts +++ /dev/null @@ -1,89 +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 { traceError } from '../../../common/logger'; -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); - this.helper = serviceContainer.get(IInterpreterHelper); - this.fileSystem = serviceContainer.get(IFileSystem); - } - // tslint:disable-next-line:no-empty - public dispose() { } - protected getInterpretersImplementation(resource?: Uri): Promise { - return this.suggestionsFromKnownVenvs(resource); - } - private async suggestionsFromKnownVenvs(resource?: Uri) { - const searchPaths = await this.searchPathsProvider.getSearchPaths(resource); - return Promise.all(searchPaths.map(dir => this.lookForInterpretersInVenvs(dir, resource))) - .then(listOfInterpreters => flatten(listOfInterpreters)); - } - private async lookForInterpretersInVenvs(pathToCheck: string, resource?: Uri) { - return this.fileSystem.getSubDirectories(pathToCheck) - .then(subDirs => Promise.all(this.getProspectiveDirectoriesForLookup(subDirs))) - .then(dirs => dirs.filter(dir => dir.length > 0)) - .then(dirs => Promise.all(dirs.map(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) => { - traceError('Python Extension (lookForInterpretersInVenvs):', err); - // Ignore exceptions. - return [] as PythonInterpreter[]; - }); - } - private getProspectiveDirectoriesForLookup(subDirs: string[]) { - const platform = this.serviceContainer.get(IPlatformService); - const dirToLookFor = platform.virtualEnvBinName; - return subDirs.map(subDir => - this.fileSystem.getSubDirectories(subDir) - .then(dirs => { - const scriptOrBinDirs = dirs.filter(dir => { - const folderName = path.basename(dir); - return this.fileSystem.arePathsSame(folderName, dirToLookFor); - }); - return scriptOrBinDirs.length === 1 ? scriptOrBinDirs[0] : ''; - }) - .catch((err) => { - console.error('Python Extension (getProspectiveDirectoriesForLookup):', err); - // Ignore exceptions. - return ''; - })); - } - private async getVirtualEnvDetails(interpreter: string, resource?: Uri): Promise { - return Promise.all([ - this.helper.getInterpreterInformation(interpreter), - this.virtualEnvMgr.getEnvironmentName(interpreter, resource), - this.virtualEnvMgr.getEnvironmentType(interpreter, resource) - ]) - .then(([details, virtualEnvName, type]) => { - if (!details) { - return; - } - this._hasInterpreters.resolve(true); - return { - ...(details as PythonInterpreter), - envName: virtualEnvName, - type: type - }; - }); - } -} diff --git a/src/client/interpreter/locators/services/cacheableLocatorService.ts b/src/client/interpreter/locators/services/cacheableLocatorService.ts deleted file mode 100644 index 3a9ea19dba9f..000000000000 --- a/src/client/interpreter/locators/services/cacheableLocatorService.ts +++ /dev/null @@ -1,133 +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, traceDecorators, traceVerbose } 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 { EventName } from '../../../telemetry/constants'; -import { IInterpreterLocatorService, IInterpreterWatcher, PythonInterpreter } from '../../contracts'; - -@injectable() -export abstract class CacheableLocatorService implements IInterpreterLocatorService { - protected readonly _hasInterpreters: Deferred; - private readonly promisesPerResource = new Map>(); - private readonly handlersAddedToResource = new Set(); - private readonly cacheKeyPrefix: string; - private readonly locating = new EventEmitter>(); - constructor(@unmanaged() private readonly name: string, - @unmanaged() protected readonly serviceContainer: IServiceContainer, - @unmanaged() private cachePerWorkspace: boolean = false) { - this._hasInterpreters = createDeferred(); - this.cacheKeyPrefix = `INTERPRETERS_CACHE_v3_${name}`; - } - public get onLocating(): Event> { - return this.locating.event; - } - public get hasInterpreters(): Promise { - return this._hasInterpreters.promise; - } - public abstract dispose(): void; - @traceDecorators.verbose('Get Interpreters in CacheableLocatorService') - public async getInterpreters(resource?: Uri, ignoreCache?: boolean): Promise { - const cacheKey = this.getCacheKey(resource); - let deferred = this.promisesPerResource.get(cacheKey); - - if (!deferred || ignoreCache) { - deferred = createDeferred(); - this.promisesPerResource.set(cacheKey, deferred); - - this.addHandlersForInterpreterWatchers(cacheKey, resource) - .ignoreErrors(); - - const promise = this.getInterpretersImplementation(resource) - .then(async items => { - await this.cacheInterpreters(items, resource); - traceVerbose(`Interpreters returned by ${this.name} are of count ${Array.isArray(items) ? items.length : 0}`); - traceVerbose(`Interpreters returned by ${this.name} are ${JSON.stringify(items)}`); - deferred!.resolve(items); - }) - .catch(ex => deferred!.reject(ex)); - - sendTelemetryWhenDone(EventName.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 = ignoreCache ? undefined : this.getCachedInterpreters(resource); - return Array.isArray(cachedInterpreters) ? cachedInterpreters : deferred.promise; - } - protected async addHandlersForInterpreterWatchers(cacheKey: string, resource: Uri | undefined): Promise { - if (this.handlersAddedToResource.has(cacheKey)) { - return; - } - this.handlersAddedToResource.add(cacheKey); - const watchers = await this.getInterpreterWatchers(resource); - const disposableRegisry = this.serviceContainer.get(IDisposableRegistry); - watchers.forEach(watcher => { - watcher.onDidCreate(() => { - 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 { - return []; - } - - protected abstract getInterpretersImplementation(resource?: Uri): Promise; - protected createPersistenceStore(resource?: Uri) { - const cacheKey = this.getCacheKey(resource); - const persistentFactory = this.serviceContainer.get(IPersistentStateFactory); - if (this.cachePerWorkspace) { - return persistentFactory.createWorkspacePersistentState(cacheKey, undefined as any); - } else { - return persistentFactory.createGlobalPersistentState(cacheKey, undefined as any); - } - - } - protected getCachedInterpreters(resource?: Uri): PythonInterpreter[] | undefined { - const persistence = this.createPersistenceStore(resource); - if (!Array.isArray(persistence.value)) { - return; - } - return persistence.value.map(item => { - return { - ...item, - cachedEntry: true - }; - }); - } - protected async cacheInterpreters(interpreters: PythonInterpreter[], resource?: Uri) { - const persistence = this.createPersistenceStore(resource); - await persistence.updateValue(interpreters); - } - protected getCacheKey(resource?: Uri) { - if (!resource || !this.cachePerWorkspace) { - return this.cacheKeyPrefix; - } - // Ensure we have separate caches per workspace where necessary.Î - const workspaceService = this.serviceContainer.get(IWorkspaceService); - if (!Array.isArray(workspaceService.workspaceFolders)) { - return this.cacheKeyPrefix; - } - - const workspace = workspaceService.getWorkspaceFolder(resource); - return workspace ? `${this.cacheKeyPrefix}:${md5(workspace.uri.fsPath)}` : this.cacheKeyPrefix; - } -} diff --git a/src/client/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 { - return this.getSuggestionsFromConda(); - } - - /** - * Return the list of interpreters identified by the "conda environments file". - */ - private async getSuggestionsFromConda(): Promise { - if (!this.condaService.condaEnvironmentsFile) { - return []; - } - return this.fileSystem.fileExists(this.condaService.condaEnvironmentsFile!) - .then(exists => exists ? this.getEnvironmentsFromFile(this.condaService.condaEnvironmentsFile!) : Promise.resolve([])); - } - - /** - * Return the list of environments identified in the given file. - */ - private async getEnvironmentsFromFile(envFile: string) { - try { - const fileContents = await this.fileSystem.readFile(envFile); - const environmentPaths = fileContents.split(/\r?\n/g) - .map(environmentPath => environmentPath.trim()) - .filter(environmentPath => environmentPath.length > 0); - - const interpreters = (await Promise.all(environmentPaths - .map(environmentPath => this.getInterpreterDetails(environmentPath)))) - .filter(item => !!item) - .map(item => item!); - - const environments = await this.condaService.getCondaEnvironments(true); - if (Array.isArray(environments) && environments.length > 0) { - interpreters - .forEach(interpreter => { - const environment = environments.find(item => this.fileSystem.arePathsSame(item.path, interpreter!.envPath!)); - if (environment) { - interpreter.envName = environment!.name; - } - }); - } - return interpreters; - } catch (err) { - 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 { - 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 4db1937a4a48..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 { - return this.getSuggestionsFromConda(); - } - - /** - * Return the list of interpreters for all the conda envs. - */ - private async getSuggestionsFromConda(): Promise { - try { - const info = await this.condaService.getCondaInfo(); - if (!info) { - return []; - } - const interpreters = await parseCondaInfo( - info, - 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 2dc9b312c3bd..000000000000 --- a/src/client/interpreter/locators/services/condaService.ts +++ /dev/null @@ -1,368 +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, traceDecorators, traceVerbose } from '../../../common/logger'; -import { IFileSystem, IPlatformService } from '../../../common/platform/types'; -import { IProcessServiceFactory } from '../../../common/process/types'; -import { IConfigurationService, IDisposableRegistry, ILogger, IPersistentStateFactory } from '../../../common/types'; -import { - CondaInfo, - ICondaService, - IInterpreterLocatorService, - 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(',')}}`; - -export const CondaGetEnvironmentPrefix = 'Outputting Environment Now...'; - -/** - * A wrapper around a conda installation. - */ -@injectable() -export class CondaService implements ICondaService { - private condaFile?: Promise; - private isAvailable: boolean | undefined; - private readonly condaHelper = new CondaHelper(); - - 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(IDisposableRegistry) private disposableRegistry: IDisposableRegistry, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IInterpreterLocatorService) @named(WINDOWS_REGISTRY_SERVICE) @optional() private registryLookupForConda?: IInterpreterLocatorService - ) { - this.addCondaPathChangedHandler(); - } - - public get condaEnvironmentsFile(): string | undefined { - const homeDir = this.platform.isWindows ? process.env.USERPROFILE : (process.env.HOME || process.env.HOMEPATH); - return homeDir ? path.join(homeDir, '.conda', 'environments.txt') : undefined; - } - - /** - * Release any held resources. - * - * Called by VS Code to indicate it is done with the resource. - */ - // tslint:disable-next-line:no-empty - public dispose() { } - - /** - * Return the path to the "conda file". - */ - public async getCondaFile(): Promise { - if (!this.condaFile) { - this.condaFile = this.getCondaFileImpl(); - } - // tslint:disable-next-line:no-unnecessary-local-variable - const condaFile = await this.condaFile!; - return condaFile!; - } - - /** - * Is there a conda install to use? - */ - public async isCondaAvailable(): Promise { - if (typeof this.isAvailable === 'boolean') { - return this.isAvailable; - } - return this.getCondaVersion() - .then(version => this.isAvailable = version !== undefined) - .catch(() => this.isAvailable = false); - } - - /** - * Return the conda version. - */ - public async getCondaVersion(): Promise { - const processService = await this.processServiceFactory.create(); - const info = await this.getCondaInfo().catch(() => undefined); - let versionString: string | undefined; - if (info && info.conda_version) { - versionString = info.conda_version; - } else { - const stdOut = await this.getCondaFile() - .then(condaFile => processService.exec(condaFile, ['--version'], {})) - .then(result => result.stdout.trim()) - .catch(() => undefined); - - versionString = (stdOut && stdOut.startsWith('conda ')) ? stdOut.substring('conda '.length).trim() : stdOut; - } - if (!versionString) { - return; - } - const version = parse(versionString, true); - if (version) { - return version; - } - // Use a bogus version, at least to indicate the fact that a version was returned. - 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 { - try { - const condaFile = await this.getCondaFile(); - const processService = await this.processServiceFactory.create(); - const condaInfo = await processService.exec(condaFile, ['info', '--json']).then(output => output.stdout); - - return JSON.parse(condaInfo) as CondaInfo; - } catch (ex) { - // Failed because either: - // 1. conda is not installed. - // 2. `conda info --json` has changed signature. - } - } - - /** - * Determines whether a python interpreter is a conda environment or not. - * The check is done by simply looking for the 'conda-meta' directory. - * @param {string} interpreterPath - * @returns {Promise} - * @memberof CondaService - */ - public async isCondaEnvironment(interpreterPath: string): Promise { - const dir = path.dirname(interpreterPath); - const isWindows = this.platform.isWindows; - const condaMetaDirectory = isWindows ? path.join(dir, 'conda-meta') : path.join(dir, '..', 'conda-meta'); - return this.fileSystem.directoryExists(condaMetaDirectory); - } - - /** - * Return (env name, interpreter filename) for the interpreter. - */ - public async getCondaEnvironment(interpreterPath: string): Promise<{ name: string; path: string } | undefined> { - const isCondaEnv = await this.isCondaEnvironment(interpreterPath); - if (!isCondaEnv) { - return; - } - let environments = await this.getCondaEnvironments(false); - const dir = path.dirname(interpreterPath); - - // If interpreter is in bin or Scripts, then go up one level - const subDirName = path.basename(dir); - const goUpOnLevel = ['BIN', 'SCRIPTS'].indexOf(subDirName.toUpperCase()) !== -1; - const interpreterPathToMatch = goUpOnLevel ? path.join(dir, '..') : dir; - - // From the list of conda environments find this dir. - let matchingEnvs = Array.isArray(environments) ? environments.filter(item => this.fileSystem.arePathsSame(item.path, interpreterPathToMatch)) : []; - if (matchingEnvs.length === 0) { - environments = await this.getCondaEnvironments(true); - matchingEnvs = Array.isArray(environments) ? environments.filter(item => this.fileSystem.arePathsSame(item.path, interpreterPathToMatch)) : []; - } - - if (matchingEnvs.length > 0) { - return { name: matchingEnvs[0].name, path: interpreterPathToMatch }; - } - - // If still not available, then the user created the env after starting vs code. - // The only solution is to get the user to re-start vscode. - } - - /** - * Return the list of conda envs (by name, interpreter filename). - */ - @traceDecorators.verbose('Get Conda environments') - public async getCondaEnvironments(ignoreCache: boolean): Promise<({ 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); - traceVerbose(`Conda Env List ${envInfo}}`); - 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); - } - - /** - * Get the conda exe from the path to an interpreter's python. This might be different than the globally registered conda.exe - */ - @traceDecorators.verbose('Get Conda File from interpreter') - public async getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise { - const condaExe = this.platform.isWindows ? 'conda.exe' : 'conda'; - const scriptsDir = this.platform.isWindows ? 'Scripts' : 'bin'; - const interpreterDir = interpreterPath ? path.dirname(interpreterPath) : ''; - - // Might be in a situation where this is not the default python env, but rather one running - // from a virtualenv - const envsPos = envName ? interpreterDir.indexOf(path.join('envs', envName)) : -1; - if (envsPos > 0) { - // This should be where the original python was run from when the environment was created. - const originalPath = interpreterDir.slice(0, envsPos); - let 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; - } - } - - 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; - } - } - - /** - * Is the given interpreter from conda? - */ - private detectCondaEnvironment(interpreter: PythonInterpreter) { - return interpreter.type === InterpreterType.Conda || - (interpreter.displayName ? interpreter.displayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || - (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('ANACONDA') >= 0 || - (interpreter.companyDisplayName ? interpreter.companyDisplayName : '').toUpperCase().indexOf('CONTINUUM') >= 0; - } - - /** - * Return the highest Python version from the given list. - */ - private getLatestVersion(interpreters: PythonInterpreter[]) { - const sortedInterpreters = interpreters.slice(); - // tslint:disable-next-line:no-non-null-assertion - sortedInterpreters.sort((a, b) => (a.version && b.version) ? compare(a.version.raw, b.version.raw) : 0); - if (sortedInterpreters.length > 0) { - return sortedInterpreters[sortedInterpreters.length - 1]; - } - } - - private addCondaPathChangedHandler() { - const disposable = this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)); - this.disposableRegistry.push(disposable); - } - private async onDidChangeConfiguration(event: ConfigurationChangeEvent) { - const workspacesUris: (Uri | undefined)[] = this.workspaceService.hasWorkspaceFolders ? this.workspaceService.workspaceFolders!.map(workspace => workspace.uri) : [undefined]; - if (workspacesUris.findIndex(uri => event.affectsConfiguration('python.condaPath', uri)) === -1) { - return; - } - this.condaFile = undefined; - } - - /** - * Return the path to the "conda file", if there is one (in known locations). - */ - private async getCondaFileImpl() { - const settings = this.configService.getSettings(); - - const setting = settings.condaPath; - if (setting && setting !== '') { - return setting; - } - - const isAvailable = await this.isCondaInCurrentPath(); - if (isAvailable) { - return 'conda'; - } - if (this.platform.isWindows && this.registryLookupForConda) { - const interpreters = await this.registryLookupForConda.getInterpreters(); - const condaInterpreters = interpreters.filter(this.detectCondaEnvironment); - const condaInterpreter = this.getLatestVersion(condaInterpreters); - if (condaInterpreter) { - const interpreterPath = await this.getCondaFileFromInterpreter(condaInterpreter.path, condaInterpreter.envName); - if (interpreterPath) { - return interpreterPath; - } - } - } - return this.getCondaFileFromKnownLocations(); - } - - /** - * Return the path to the "conda file", if there is one (in known locations). - * Note: For now we simply return the first one found. - */ - private async getCondaFileFromKnownLocations(): Promise { - const globPattern = this.platform.isWindows ? CondaLocationsGlobWin : CondaLocationsGlob; - const condaFiles = await this.fileSystem.search(globPattern) - .catch((failReason) => { - 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]; - } -} diff --git a/src/client/interpreter/locators/services/currentPathService.ts b/src/client/interpreter/locators/services/currentPathService.ts deleted file mode 100644 index 825ffc92616e..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); - } - - /** - * Release any held resources. - * - * Called by VS Code to indicate it is done with the resource. - */ - // tslint:disable-next-line:no-empty - public dispose() { } - - /** - * Return the located interpreters. - * - * This is used by CacheableLocatorService.getInterpreters(). - */ - protected getInterpretersImplementation(resource?: Uri): Promise { - return this.suggestionsFromKnownPaths(resource); - } - - /** - * Return the located interpreters. - */ - private async suggestionsFromKnownPaths(resource?: Uri) { - const configSettings = this.serviceContainer.get(IConfigurationService).getSettings(resource); - const pathsToCheck = [...this.pythonCommandProvider.getCommands(), { command: configSettings.pythonPath }]; - - const pythonPaths = Promise.all(pathsToCheck.map(item => this.getInterpreter(item))); - return pythonPaths - .then(interpreters => interpreters.filter(item => item.length > 0)) - // tslint:disable-next-line:promise-function-async - .then(interpreters => Promise.all(interpreters.map(interpreter => this.getInterpreterDetails(interpreter)))) - .then(interpreters => interpreters.filter(item => !!item).map(item => item!)); - } - - /** - * Return the information about the identified interpreter binary. - */ - private async getInterpreterDetails(pythonPath: string): Promise { - return this.helper.getInterpreterInformation(pythonPath) - .then(details => { - if (!details) { - return; - } - this._hasInterpreters.resolve(true); - return { - ...(details as PythonInterpreter), - path: pythonPath, - type: details.type ? details.type : InterpreterType.Unknown - }; - }); - } - - /** - * Return the path to the interpreter (or the default if not found). - */ - private async getInterpreter(options: { command: string; args?: string[] }) { - try { - const processService = await this.processServiceFactory.create(); - const 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 bcf836350ee9..000000000000 --- a/src/client/interpreter/locators/services/globalVirtualEnvService.ts +++ /dev/null @@ -1,64 +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, ICurrentProcess } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IVirtualEnvironmentsSearchPathProvider } from '../../contracts'; -import { IVirtualEnvironmentManager } from '../../virtualEnvs/types'; -import { BaseVirtualEnvService } from './baseVirtualEnvService'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const untildify: (value: string) => string = require('untildify'); - -@injectable() -export class GlobalVirtualEnvService extends BaseVirtualEnvService { - public constructor( - @inject(IVirtualEnvironmentsSearchPathProvider) @named('global') globalVirtualEnvPathProvider: IVirtualEnvironmentsSearchPathProvider, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(globalVirtualEnvPathProvider, serviceContainer, 'VirtualEnvService'); - } -} - -@injectable() -export class GlobalVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { - private readonly config: IConfigurationService; - private readonly currentProcess: ICurrentProcess; - private readonly virtualEnvMgr: IVirtualEnvironmentManager; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.config = serviceContainer.get(IConfigurationService); - this.virtualEnvMgr = serviceContainer.get(IVirtualEnvironmentManager); - this.currentProcess = serviceContainer.get(ICurrentProcess); - } - - public async getSearchPaths(resource?: Uri): Promise { - const homedir = os.homedir(); - const venvFolders = [ - 'envs', - '.pyenv', - '.direnv', - '.virtualenvs', - ...this.config.getSettings(resource).venvFolders]; - const folders = [...new Set(venvFolders.map(item => path.join(homedir, item)))]; - - // Add support for the WORKON_HOME environment variable used by pipenv and virtualenvwrapper. - const workonHomePath = this.currentProcess.env.WORKON_HOME; - if (workonHomePath) { - folders.push(untildify(workonHomePath)); - } - - // tslint:disable-next-line:no-string-literal - const pyenvRoot = await this.virtualEnvMgr.getPyEnvRoot(resource); - if (pyenvRoot) { - folders.push(pyenvRoot); - folders.push(path.join(pyenvRoot, 'versions')); - } - return folders; - } -} diff --git a/src/client/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>(); - /** - * Creates an instance of InterpreterWatcherBuilder. - * Inject the DI container, as we need to get a new instance of IInterpreterWatcher to build it. - * @param {IWorkspaceService} workspaceService - * @param {IServiceContainer} serviceContainer - * @memberof InterpreterWatcherBuilder - */ - constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer - ) { } - - @traceDecorators.verbose('Build the workspace interpreter watcher') - public async getWorkspaceVirtualEnvInterpreterWatcher(resource: Uri | undefined): Promise { - const key = this.getResourceKey(resource); - if (!this.watchersByResource.has(key)) { - const deferred = createDeferred(); - this.watchersByResource.set(key, deferred.promise); - const watcher = this.serviceContainer.get(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE); - await watcher.register(resource); - deferred.resolve(watcher); - } - return this.watchersByResource.get(key)!; - } - protected getResourceKey(resource: Uri | undefined): string { - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - return workspaceFolder ? workspaceFolder.uri.fsPath : ''; - } -} diff --git a/src/client/interpreter/locators/services/pipEnvService.ts b/src/client/interpreter/locators/services/pipEnvService.ts deleted file mode 100644 index a46cc1b1381f..000000000000 --- a/src/client/interpreter/locators/services/pipEnvService.ts +++ /dev/null @@ -1,172 +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 { IConfigurationService, ICurrentProcess, ILogger } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { IInterpreterHelper, InterpreterType, IPipEnvService, PythonInterpreter } from '../../contracts'; -import { IPipEnvServiceHelper } from '../types'; -import { CacheableLocatorService } from './cacheableLocatorService'; - -const pipEnvFileNameVariable = 'PIPENV_PIPFILE'; - -@injectable() -export class PipEnvService extends CacheableLocatorService implements IPipEnvService { - private readonly helper: IInterpreterHelper; - private readonly processServiceFactory: IProcessServiceFactory; - private readonly workspace: IWorkspaceService; - private readonly fs: IFileSystem; - private readonly logger: ILogger; - private readonly configService: IConfigurationService; - private readonly pipEnvServiceHelper: IPipEnvServiceHelper; - - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - super('PipEnvService', serviceContainer, true); - this.helper = this.serviceContainer.get(IInterpreterHelper); - this.processServiceFactory = this.serviceContainer.get(IProcessServiceFactory); - this.workspace = this.serviceContainer.get(IWorkspaceService); - this.fs = this.serviceContainer.get(IFileSystem); - this.logger = this.serviceContainer.get(ILogger); - this.configService = this.serviceContainer.get(IConfigurationService); - this.pipEnvServiceHelper = this.serviceContainer.get(IPipEnvServiceHelper); - } - // tslint:disable-next-line:no-empty - public dispose() {} - public async isRelatedPipEnvironment(dir: string, pythonPath: string): Promise { - // In PipEnv, the name of the cwd is used as a prefix in the virtual env. - if (pythonPath.indexOf(`${path.sep}${path.basename(dir)}-`) === -1) { - return false; - } - const envName = await this.getInterpreterPathFromPipenv(dir, true); - return !!envName; - } - - public get executable(): string { - return this.configService.getSettings().pipenvPath; - } - - protected getInterpretersImplementation(resource?: Uri): Promise { - const pipenvCwd = this.getPipenvWorkingDirectory(resource); - if (!pipenvCwd) { - return Promise.resolve([]); - } - - return this.getInterpreterFromPipenv(pipenvCwd) - .then(item => (item ? [item] : [])) - .catch(() => []); - } - - private async getInterpreterFromPipenv(pipenvCwd: string): Promise { - const interpreterPath = await this.getInterpreterPathFromPipenv(pipenvCwd); - if (!interpreterPath) { - return; - } - - const details = await this.helper.getInterpreterInformation(interpreterPath); - if (!details) { - return; - } - this._hasInterpreters.resolve(true); - await this.pipEnvServiceHelper.trackWorkspaceFolder(interpreterPath, Uri.file(pipenvCwd)); - return { - ...(details as PythonInterpreter), - path: interpreterPath, - type: InterpreterType.Pipenv, - pipEnvWorkspaceFolder: pipenvCwd - }; - } - - private getPipenvWorkingDirectory(resource?: Uri): string | undefined { - // The file is not in a workspace. However, workspace may be opened - // and file is just a random file opened from elsewhere. In this case - // we still want to provide interpreter associated with the workspace. - // Otherwise if user tries and formats the file, we may end up using - // plain pip module installer to bring in the formatter and it is wrong. - const wsFolder = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; - return wsFolder ? wsFolder.uri.fsPath : this.workspace.rootPath; - } - - private async getInterpreterPathFromPipenv(cwd: string, ignoreErrors = false): Promise { - // Quick check before actually running pipenv - if (!(await this.checkIfPipFileExists(cwd))) { - return; - } - try { - // call pipenv --version just to see if pipenv is in the PATH - const version = await this.invokePipenv('--version', cwd); - if (version === undefined) { - const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showWarningMessage( - `Workspace contains Pipfile but '${this.executable}' was not found. Make sure '${this.executable}' is on the PATH.` - ); - return; - } - // The --py command will fail if the virtual environment has not been setup yet. - // so call pipenv --venv to check for the virtual environment first. - const venv = await this.invokePipenv('--venv', cwd); - if (venv === undefined) { - const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showWarningMessage( - 'Workspace contains Pipfile but the associated virtual environment has not been setup. Setup the virtual environment manually if needed.' - ); - return; - } - const pythonPath = await this.invokePipenv('--py', cwd); - return pythonPath && (await this.fs.fileExists(pythonPath)) ? pythonPath : undefined; - // tslint:disable-next-line:no-empty - } catch (error) { - traceError('PipEnv identification failed', error); - if (ignoreErrors) { - return; - } - } - } - private async checkIfPipFileExists(cwd: string): Promise { - const currentProcess = this.serviceContainer.get(ICurrentProcess); - const pipFileName = currentProcess.env[pipEnvFileNameVariable]; - if (typeof pipFileName === 'string' && (await this.fs.fileExists(path.join(cwd, pipFileName)))) { - return true; - } - if (await this.fs.fileExists(path.join(cwd, 'Pipfile'))) { - return true; - } - return false; - } - - private async invokePipenv(arg: string, rootPath: string): Promise { - try { - const processService = await this.processServiceFactory.create(Uri.file(rootPath)); - const execName = this.executable; - const result = await processService.exec(execName, [arg], { cwd: rootPath }); - if (result) { - const stdout = result.stdout ? result.stdout.trim() : ''; - const stderr = result.stderr ? result.stderr.trim() : ''; - if (stderr.length > 0 && stdout.length === 0) { - throw new Error(stderr); - } - return stdout; - } - // tslint:disable-next-line:no-empty - } catch (error) { - const platformService = this.serviceContainer.get(IPlatformService); - const currentProc = this.serviceContainer.get(ICurrentProcess); - const enviromentVariableValues: Record = { - LC_ALL: currentProc.env.LC_ALL, - LANG: currentProc.env.LANG - }; - enviromentVariableValues[platformService.pathVariableName] = - currentProc.env[platformService.pathVariableName]; - - this.logger.logWarning('Error in invoking PipEnv', error); - this.logger.logWarning( - `Relevant Environment Variables ${JSON.stringify(enviromentVariableValues, undefined, 4)}` - ); - } - } -} diff --git a/src/client/interpreter/locators/services/pipEnvServiceHelper.ts b/src/client/interpreter/locators/services/pipEnvServiceHelper.ts deleted file mode 100644 index 490c54e3c47d..000000000000 --- a/src/client/interpreter/locators/services/pipEnvServiceHelper.ts +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IFileSystem } from '../../../common/platform/types'; -import { IPersistentState, IPersistentStateFactory } from '../../../common/types'; -import { IPipEnvServiceHelper } from '../types'; - -type PipEnvInformation = { pythonPath: string; workspaceFolder: string; envName: string }; -@injectable() -export class PipEnvServiceHelper implements IPipEnvServiceHelper { - private initialized = false; - private readonly state: IPersistentState>; - constructor( - @inject(IPersistentStateFactory) private readonly statefactory: IPersistentStateFactory, - @inject(IFileSystem) private readonly fs: IFileSystem - ) { - this.state = this.statefactory.createGlobalPersistentState>( - 'PipEnvInformation', - [] - ); - } - public async getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string} | undefined> { - await this.initializeStateStore(); - const info = this.state.value.find(item => this.fs.arePathsSame(item.pythonPath, pythonPath)); - return info ? { workspaceFolder: Uri.file(info.workspaceFolder), envName: info.envName } : undefined; - } - public async trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise { - await this.initializeStateStore(); - const values = [...this.state.value].filter(item => !this.fs.arePathsSame(item.pythonPath, pythonPath)); - const envName = path.basename(workspaceFolder.fsPath); - values.push({ pythonPath, workspaceFolder: workspaceFolder.fsPath, envName }); - await this.state.updateValue(values); - } - protected async initializeStateStore() { - if (this.initialized) { - return; - } - const list = await Promise.all( - this.state.value.map(async item => ((await this.fs.fileExists(item.pythonPath)) ? item : undefined)) - ); - const filteredList = list.filter(item => !!item) as PipEnvInformation[]; - await this.state.updateValue(filteredList); - this.initialized = true; - } -} diff --git a/src/client/interpreter/locators/services/windowsRegistryService.ts b/src/client/interpreter/locators/services/windowsRegistryService.ts deleted file mode 100644 index c368eaa4e484..000000000000 --- a/src/client/interpreter/locators/services/windowsRegistryService.ts +++ /dev/null @@ -1,156 +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); - } - // tslint:disable-next-line:no-empty - public dispose() { } - protected async getInterpretersImplementation(_resource?: Uri): Promise { - return this.platform.isWindows ? this.getInterpretersFromRegistry() : []; - } - private async getInterpretersFromRegistry() { - // https://github.com/python/peps/blob/master/pep-0514.txt#L357 - const hkcuArch = this.platform.is64bit ? undefined : Architecture.x86; - const promises: Promise[] = [ - this.getCompanies(RegistryHive.HKCU, hkcuArch), - this.getCompanies(RegistryHive.HKLM, Architecture.x86) - ]; - // https://github.com/Microsoft/PTVS/blob/ebfc4ca8bab234d453f15ee426af3b208f3c143c/Python/Product/Cookiecutter/Shared/Interpreters/PythonRegistrySearch.cs#L44 - if (this.platform.is64bit) { - promises.push(this.getCompanies(RegistryHive.HKLM, Architecture.x64)); - } - - const companies = await Promise.all(promises); - const companyInterpreters = await Promise.all(flatten(companies) - .filter(item => item !== undefined && item !== null) - .map(company => { - return this.getInterpretersForCompany(company.companyKey, company.hive, company.arch); - })); - - return flatten(companyInterpreters) - .filter(item => item !== undefined && item !== null) - // tslint:disable-next-line:no-non-null-assertion - .map(item => item!) - .reduce((prev, current) => { - if (prev.findIndex(item => item.path.toUpperCase() === current.path.toUpperCase()) === -1) { - prev.push(current); - } - return prev; - }, []); - } - private async getCompanies(hive: RegistryHive, arch?: Architecture): Promise { - return this.registry.getKeys('\\Software\\Python', hive, arch) - .then(companyKeys => companyKeys - .filter(companyKey => CompaniesToIgnore.indexOf(this.pathUtils.basename(companyKey).toUpperCase()) === -1) - .map(companyKey => { - return { companyKey, hive, arch }; - })); - } - private async getInterpretersForCompany(companyKey: string, hive: RegistryHive, arch?: Architecture) { - const tagKeys = await this.registry.getKeys(companyKey, hive, arch); - return Promise.all(tagKeys.map(tagKey => this.getInreterpreterDetailsForCompany(tagKey, companyKey, hive, arch))); - } - private getInreterpreterDetailsForCompany(tagKey: string, companyKey: string, hive: RegistryHive, arch?: Architecture): Promise { - const key = `${tagKey}\\InstallPath`; - type InterpreterInformation = null | undefined | { - installPath: string; - executablePath?: string; - displayName?: string; - version?: string; - companyDisplayName?: string; - }; - return this.registry.getValue(key, hive, arch) - .then(installPath => { - // Install path is mandatory. - if (!installPath) { - return Promise.resolve(null); - } - // Check if 'ExecutablePath' exists. - // Remember Python 2.7 doesn't have 'ExecutablePath' (there could be others). - // Treat all other values as optional. - return Promise.all([ - Promise.resolve(installPath), - this.registry.getValue(key, hive, arch, 'ExecutablePath'), - this.registry.getValue(tagKey, hive, arch, 'SysVersion'), - this.getCompanyDisplayName(companyKey, hive, arch) - ]) - .then(([installedPath, executablePath, version, companyDisplayName]) => { - companyDisplayName = AnacondaCompanyNames.indexOf(companyDisplayName) === -1 ? companyDisplayName : AnacondaCompanyName; - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - return { installPath: installedPath, executablePath, version, companyDisplayName } as InterpreterInformation; - }); - }) - .then(async (interpreterInfo?: InterpreterInformation) => { - if (!interpreterInfo) { - return; - } - - const executablePath = interpreterInfo.executablePath && interpreterInfo.executablePath.length > 0 ? interpreterInfo.executablePath : path.join(interpreterInfo.installPath, DefaultPythonExecutable); - const helper = this.serviceContainer.get(IInterpreterHelper); - const details = await helper.getInterpreterInformation(executablePath); - if (!details) { - return; - } - const version = interpreterInfo.version ? this.pathUtils.basename(interpreterInfo.version) : this.pathUtils.basename(tagKey); - this._hasInterpreters.resolve(true); - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - return { - ...(details as PythonInterpreter), - path: executablePath, - // Do not use version info from registry, this doesn't contain the release level. - // Give preference to what we have retrieved from getInterpreterInformation. - version: details.version || parsePythonVersion(version), - companyDisplayName: interpreterInfo.companyDisplayName, - type: 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 { - return [await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource)]; - } -} - -@injectable() -export class WorkspaceVirtualEnvironmentsSearchPathProvider implements IVirtualEnvironmentsSearchPathProvider { - public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - - } - public async getSearchPaths(resource?: Uri): Promise { - const configService = this.serviceContainer.get(IConfigurationService); - const paths: string[] = []; - const venvPath = configService.getSettings(resource).venvPath; - if (venvPath) { - paths.push(untildify(venvPath)); - } - const workspaceService = this.serviceContainer.get(IWorkspaceService); - if (Array.isArray(workspaceService.workspaceFolders) && workspaceService.workspaceFolders.length > 0) { - let wsPath: string | undefined; - if (resource && workspaceService.workspaceFolders.length > 1) { - const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); - if (wkspaceFolder) { - wsPath = wkspaceFolder.uri.fsPath; - } - } else { - wsPath = workspaceService.workspaceFolders[0].uri.fsPath; - } - if (wsPath) { - paths.push(wsPath); - paths.push(path.join(wsPath, '.direnv')); - } - } - return paths; - } -} diff --git a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts b/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.ts deleted file mode 100644 index b8de265fd39c..000000000000 --- a/src/client/interpreter/locators/services/workspaceVirtualEnvWatcherService.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 * 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, Resource } 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; - private timers = new Map(); - private fsWatchers: FileSystemWatcher[] = []; - private resource: Resource; - constructor( - @inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[], - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, - @inject(IPlatformService) private readonly platformService: IPlatformService, - @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory - ) { - this.didCreate = new EventEmitter(); - disposableRegistry.push(this); - } - public get onDidCreate(): Event { - return this.didCreate.event; - } - public dispose() { - this.clearTimers(); - } - @traceDecorators.verbose('Register Intepreter Watcher') - public async register(resource: Resource): Promise { - if (this.fsWatchers.length > 0) { - return; - } - this.resource = resource; - const workspaceFolder = resource ? this.workspaceService.getWorkspaceFolder(resource) : undefined; - const executable = this.platformService.isWindows ? 'python.exe' : 'python'; - const patterns = [path.join('*', executable), path.join('*', '*', executable)]; - - for (const pattern of patterns) { - const globPatern = workspaceFolder ? new RelativePattern(workspaceFolder.uri.fsPath, pattern) : pattern; - 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') - public async createHandler(e: Uri) { - this.didCreate.fire(this.resource); - // On Windows, creation of environments are very slow, hence lets notify again after - // the python executable is accessible (i.e. when we can launch the process). - this.notifyCreationWhenReady(e.fsPath).ignoreErrors(); - } - protected async notifyCreationWhenReady(pythonPath: string) { - const counter = this.timers.has(pythonPath) ? this.timers.get(pythonPath)!.counter + 1 : 0; - const isValid = await this.isValidExecutable(pythonPath); - if (isValid) { - if (counter > 0) { - this.didCreate.fire(this.resource); - } - return this.timers.delete(pythonPath); - } - if (counter > maxTimeToWaitForEnvCreation / timeToPollForEnvCreation) { - // Send notification before we give up trying. - this.didCreate.fire(this.resource); - this.timers.delete(pythonPath); - return; - } - - const timer = setTimeout(() => this.notifyCreationWhenReady(pythonPath).ignoreErrors(), timeToPollForEnvCreation); - this.timers.set(pythonPath, { timer, counter }); - } - private clearTimers() { - // tslint:disable-next-line: no-any - this.timers.forEach(item => clearTimeout(item.timer as any)); - this.timers.clear(); - } - private async isValidExecutable(pythonPath: string): Promise { - const execService = await this.pythonExecFactory.create({ pythonPath }); - const info = await execService.getInterpreterInformation().catch(() => undefined); - return info !== undefined; - } -} diff --git a/src/client/interpreter/locators/types.ts b/src/client/interpreter/locators/types.ts index d6f76b751761..d67d8c1d7da0 100644 --- a/src/client/interpreter/locators/types.ts +++ b/src/client/interpreter/locators/types.ts @@ -11,6 +11,6 @@ export interface IPythonInPathCommandProvider { } export const IPipEnvServiceHelper = Symbol('IPipEnvServiceHelper'); export interface IPipEnvServiceHelper { - getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string} | undefined>; + getPipEnvInfo(pythonPath: string): Promise<{ workspaceFolder: Uri; envName: string } | undefined>; trackWorkspaceFolder(pythonPath: string, workspaceFolder: Uri): Promise; } diff --git a/src/client/interpreter/serviceRegistry.ts b/src/client/interpreter/serviceRegistry.ts index 71854adf28e3..f54f8e5368fe 100644 --- a/src/client/interpreter/serviceRegistry.ts +++ b/src/client/interpreter/serviceRegistry.ts @@ -1,127 +1,123 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { IExtensionActivationService } from '../activation/types'; +'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 { InterpreterSelectionTip } from './display/interpreterSelectionTip'; -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 { PipEnvServiceHelper } from './locators/services/pipEnvServiceHelper'; -import { WindowsRegistryService } from './locators/services/windowsRegistryService'; -import { WorkspaceVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvService } from './locators/services/workspaceVirtualEnvService'; -import { WorkspaceVirtualEnvWatcherService } from './locators/services/workspaceVirtualEnvWatcherService'; -import { IPipEnvServiceHelper, IPythonInPathCommandProvider } from './locators/types'; -import { VirtualEnvironmentManager } from './virtualEnvs/index'; -import { IVirtualEnvironmentManager } from './virtualEnvs/types'; +import { ActivatedEnvironmentLaunch } from './virtualEnvs/activatedEnvLaunch'; +import { CondaInheritEnvPrompt } from './virtualEnvs/condaInheritEnvPrompt'; import { VirtualEnvironmentPrompt } from './virtualEnvs/virtualEnvPrompt'; -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(IKnownSearchPathsForInterpreters, KnownSearchPathsForInterpreters); - serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global'); - serviceManager.addSingleton(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); - - serviceManager.addSingleton(ICondaService, CondaService); - serviceManager.addSingleton(IPipEnvServiceHelper, PipEnvServiceHelper); - serviceManager.addSingleton(IVirtualEnvironmentManager, VirtualEnvironmentManager); - serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); - serviceManager.addSingleton(IExtensionActivationService, InterpreterSelectionTip); - serviceManager.addSingleton(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, WorkspaceVirtualEnvWatcherService, WORKSPACE_VIRTUAL_ENV_SERVICE); - serviceManager.addSingleton(IInterpreterWatcherBuilder, InterpreterWatcherBuilder); +export function registerInterpreterTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton( + IExtensionSingleActivationService, + InstallPythonCommand, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InstallPythonViaTerminal, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + SetInterpreterCommand, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + ResetInterpreterCommand, + ); + serviceManager.addSingleton( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); + serviceManager.addBinding(IRecommendedEnvironmentService, IExtensionActivationService); + serviceManager.addSingleton(IInterpreterQuickPick, SetInterpreterCommand); - serviceManager.addSingleton(IInterpreterVersionService, InterpreterVersionService); - serviceManager.addSingleton(IInterpreterLocatorService, PythonInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvFileService, CONDA_ENV_FILE_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, CondaEnvService, CONDA_ENV_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, CurrentPathService, CURRENT_PATH_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, GlobalVirtualEnvService, GLOBAL_VIRTUAL_ENV_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, WorkspaceVirtualEnvService, WORKSPACE_VIRTUAL_ENV_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, PipEnvService, PIPENV_SERVICE); - serviceManager.addSingleton(IPipEnvService, PipEnvService); + serviceManager.addSingleton(IExtensionActivationService, VirtualEnvironmentPrompt); - serviceManager.addSingleton(IInterpreterLocatorService, WindowsRegistryService, WINDOWS_REGISTRY_SERVICE); - serviceManager.addSingleton(IInterpreterLocatorService, KnownPathsService, KNOWN_PATH_SERVICE); serviceManager.addSingleton(IInterpreterService, InterpreterService); serviceManager.addSingleton(IInterpreterDisplay, InterpreterDisplay); + serviceManager.addBinding(IInterpreterDisplay, IExtensionSingleActivationService); - serviceManager.addSingleton(IPythonPathUpdaterServiceFactory, PythonPathUpdaterServiceFactory); - serviceManager.addSingleton(IPythonPathUpdaterServiceManager, PythonPathUpdaterService); + serviceManager.addSingleton( + IPythonPathUpdaterServiceFactory, + PythonPathUpdaterServiceFactory, + ); + serviceManager.addSingleton( + IPythonPathUpdaterServiceManager, + PythonPathUpdaterService, + ); serviceManager.addSingleton(IInterpreterSelector, InterpreterSelector); - serviceManager.addSingleton(IShebangCodeLensProvider, ShebangCodeLensProvider); serviceManager.addSingleton(IInterpreterHelper, InterpreterHelper); - serviceManager.addSingleton(IInterpreterLocatorHelper, InterpreterLocatorHelper); - serviceManager.addSingleton(IInterpreterComparer, InterpreterComparer); - serviceManager.addSingleton(InterpreterLocatorProgressHandler, InterpreterLocatorProgressStatubarHandler); - serviceManager.addSingleton(IInterpreterLocatorProgressService, InterpreterLocatorProgressService); + serviceManager.addSingleton(IInterpreterComparer, EnvironmentTypeComparer); - serviceManager.addSingleton(IInterpreterAutoSelectionRule, CurrentPathInterpretersAutoSelectionRule, AutoSelectionRule.currentPath); - serviceManager.addSingleton(IInterpreterAutoSelectionRule, SystemWideInterpretersAutoSelectionRule, AutoSelectionRule.systemWide); - serviceManager.addSingleton(IInterpreterAutoSelectionRule, WindowsRegistryInterpretersAutoSelectionRule, AutoSelectionRule.windowsRegistry); - serviceManager.addSingleton(IInterpreterAutoSelectionRule, WorkspaceVirtualEnvInterpretersAutoSelectionRule, AutoSelectionRule.workspaceVirtualEnvs); - serviceManager.addSingleton(IInterpreterAutoSelectionRule, CachedInterpretersAutoSelectionRule, AutoSelectionRule.cachedInterpreters); - serviceManager.addSingleton(IInterpreterAutoSelectionRule, SettingsInterpretersAutoSelectionRule, AutoSelectionRule.settings); - serviceManager.addSingleton(IInterpreterAutoSeletionProxyService, InterpreterAutoSeletionProxyService); - serviceManager.addSingleton(IInterpreterAutoSelectionService, InterpreterAutoSelectionService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterLocatorProgressStatusBarHandler, + ); + + serviceManager.addSingleton( + IInterpreterAutoSelectionService, + InterpreterAutoSelectionService, + ); + + serviceManager.addSingleton(IExtensionActivationService, CondaInheritEnvPrompt); + serviceManager.addSingleton(IActivatedEnvironmentLaunch, ActivatedEnvironmentLaunch); +} - serviceManager.addSingleton(IEnvironmentActivationService, EnvironmentActivationService); +export function registerTypes(serviceManager: IServiceManager): void { + registerInterpreterTypes(serviceManager); + serviceManager.addSingleton( + IInterpreterAutoSelectionProxyService, + InterpreterAutoSelectionProxyService, + ); + serviceManager.addSingleton( + EnvironmentActivationService, + EnvironmentActivationService, + ); + serviceManager.addSingleton( + IEnvironmentActivationService, + EnvironmentActivationService, + ); + serviceManager.addSingleton( + IExtensionSingleActivationService, + InterpreterPathCommand, + ); } diff --git a/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts new file mode 100644 index 000000000000..6b4334e13100 --- /dev/null +++ b/src/client/interpreter/virtualEnvs/activatedEnvLaunch.ts @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { ConfigurationTarget } from 'vscode'; +import * as path from 'path'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IProcessServiceFactory } from '../../common/process/types'; +import { sleep } from '../../common/utils/async'; +import { cache } from '../../common/utils/decorators'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { traceError, traceLog, traceVerbose, traceWarn } from '../../logging'; +import { Conda } from '../../pythonEnvironments/common/environmentManagers/conda'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { IPythonPathUpdaterServiceManager } from '../configuration/types'; +import { IActivatedEnvironmentLaunch, IInterpreterService } from '../contracts'; + +@injectable() +export class ActivatedEnvironmentLaunch implements IActivatedEnvironmentLaunch { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + private inMemorySelection: string | undefined; + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPythonPathUpdaterServiceManager) + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory, + public wasSelected: boolean = false, + ) {} + + @cache(-1, true) + public async _promptIfApplicable(): Promise { + const baseCondaPrefix = getPrefixOfActivatedCondaEnv(); + if (!baseCondaPrefix) { + return; + } + const info = await this.interpreterService.getInterpreterDetails(baseCondaPrefix); + if (info?.envName !== 'base') { + // Only show prompt for base conda environments, as we need to check config for such envs which can be slow. + return; + } + const conda = await Conda.getConda(); + if (!conda) { + traceWarn('Conda not found even though activated environment vars are set'); + return; + } + const service = await this.processServiceFactory.create(); + const autoActivateBaseConfig = await service + .shellExec(`${conda.shellCommand} config --get auto_activate_base`) + .catch((ex) => { + traceError(ex); + return { stdout: '' }; + }); + if (autoActivateBaseConfig.stdout.trim().toLowerCase().endsWith('false')) { + await this.promptAndUpdate(baseCondaPrefix); + } + } + + private async promptAndUpdate(prefix: string) { + this.wasSelected = true; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.activatedCondaEnvLaunch, ...prompts); + sendTelemetryEvent(EventName.ACTIVATED_CONDA_ENV_LAUNCH, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.setInterpeterInStorage(prefix); + } + } + + public async selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { + if (this.wasSelected) { + return this.inMemorySelection; + } + return this._selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection); + } + + @cache(-1, true) + private async _selectIfLaunchedViaActivatedEnv(doNotBlockOnSelection = false): Promise { + if (process.env.VSCODE_CLI !== '1') { + // We only want to select the interpreter if VS Code was launched from the command line. + traceLog("Skipping ActivatedEnv Detection: process.env.VSCODE_CLI !== '1'"); + return undefined; + } + traceVerbose('VS Code was not launched from the command line'); + const prefix = await this.getPrefixOfSelectedActivatedEnv(); + if (!prefix) { + this._promptIfApplicable().ignoreErrors(); + return undefined; + } + this.wasSelected = true; + this.inMemorySelection = prefix; + traceLog( + `VS Code was launched from an activated environment: '${path.basename( + prefix, + )}', selecting it as the interpreter for workspace.`, + ); + if (doNotBlockOnSelection) { + this.setInterpeterInStorage(prefix).ignoreErrors(); + } else { + await this.setInterpeterInStorage(prefix); + await sleep(1); // Yield control so config service can update itself. + } + this.inMemorySelection = undefined; // Once we have set the prefix in storage, clear the in memory selection. + return prefix; + } + + private async setInterpeterInStorage(prefix: string) { + const { workspaceFolders } = this.workspaceService; + if (!workspaceFolders || workspaceFolders.length === 0) { + await this.pythonPathUpdaterService.updatePythonPath(prefix, ConfigurationTarget.Global, 'load'); + } else { + await this.pythonPathUpdaterService.updatePythonPath( + prefix, + ConfigurationTarget.WorkspaceFolder, + 'load', + workspaceFolders[0].uri, + ); + } + } + + private async getPrefixOfSelectedActivatedEnv(): Promise { + const virtualEnvVar = process.env.VIRTUAL_ENV; + if (virtualEnvVar !== undefined && virtualEnvVar.length > 0) { + return virtualEnvVar; + } + const condaPrefixVar = getPrefixOfActivatedCondaEnv(); + if (!condaPrefixVar) { + return undefined; + } + const info = await this.interpreterService.getInterpreterDetails(condaPrefixVar); + if (info?.envName !== 'base') { + return condaPrefixVar; + } + // Ignoring base conda environments, as they could be automatically set by conda. + if (process.env.CONDA_AUTO_ACTIVATE_BASE !== undefined) { + if (process.env.CONDA_AUTO_ACTIVATE_BASE.toLowerCase() === 'false') { + return condaPrefixVar; + } + } + return undefined; + } +} + +function getPrefixOfActivatedCondaEnv() { + const condaPrefixVar = process.env.CONDA_PREFIX; + if (condaPrefixVar && condaPrefixVar.length > 0) { + const condaShlvl = process.env.CONDA_SHLVL; + if (condaShlvl !== undefined && condaShlvl.length > 0 && condaShlvl > '0') { + return condaPrefixVar; + } + } + return undefined; +} diff --git a/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts b/src/client/interpreter/virtualEnvs/condaInheritEnvPrompt.ts 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 { + this.initializeInBackground(resource).ignoreErrors(); + } + + @traceDecoratorError('Failed to intialize conda inherit env prompt') + public async initializeInBackground(resource: Uri): Promise { + const show = await this.shouldShowPrompt(resource); + if (!show) { + return; + } + await this.promptAndUpdate(); + } + + @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 { + 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('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 fd707fade848..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); - this.fs = serviceContainer.get(IFileSystem); - this.pipEnvService = serviceContainer.get(IPipEnvService); - this.workspaceService = serviceContainer.get(IWorkspaceService); - } - public async getEnvironmentName(pythonPath: string, resource?: Uri): Promise { - 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 { - 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 { - if (this.pyEnvRoot) { - return this.pyEnvRoot; - } - - const currentProccess = this.serviceContainer.get(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); - return this.pyEnvRoot = path.join(pathUtils.home, '.pyenv'); - } - public async isVirtualEnvironment(pythonPath: string) { - const provider = this.getTerminalActivationProviderForVirtualEnvs(); - const shells = getNamesAndValues(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).isWindows; - const serviceName = isWindows ? 'commandPromptAndPowerShell' : 'bashCShellFish'; - return this.serviceContainer.get(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; - getEnvironmentType(pythonPath: string, resource?: Uri): Promise; - getPyEnvRoot(resource?: Uri): Promise; -} diff --git a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts index 9a2a8b3198e4..7ed18c0e8b2a 100644 --- a/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts +++ b/src/client/interpreter/virtualEnvs/virtualEnvPrompt.ts @@ -1,71 +1,88 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { inject, injectable, named } from 'inversify'; +import { inject, injectable } from 'inversify'; import { ConfigurationTarget, Disposable, Uri } from 'vscode'; import { IExtensionActivationService } from '../../activation/types'; -import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; -import { traceDecorators } from '../../common/logger'; +import { IApplicationShell } from '../../common/application/types'; import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; -import { sleep } from '../../common/utils/async'; -import { Common, InteractiveShiftEnterBanner, Interpreters } from '../../common/utils/localize'; +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 { IInterpreterHelper, IInterpreterLocatorService, IInterpreterWatcherBuilder, PythonInterpreter, WORKSPACE_VIRTUAL_ENV_SERVICE } from '../contracts'; +import { IComponentAdapter, IInterpreterHelper, IInterpreterService } from '../contracts'; const doNotDisplayPromptStateKey = 'MESSAGE_KEY_FOR_VIRTUAL_ENV'; @injectable() export class VirtualEnvironmentPrompt implements IExtensionActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + constructor( - @inject(IInterpreterWatcherBuilder) private readonly builder: IInterpreterWatcherBuilder, @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, @inject(IInterpreterHelper) private readonly helper: IInterpreterHelper, - @inject(IPythonPathUpdaterServiceManager) private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, - @inject(IInterpreterLocatorService) @named(WORKSPACE_VIRTUAL_ENV_SERVICE) private readonly locator: IInterpreterLocatorService, + @inject(IPythonPathUpdaterServiceManager) + private readonly pythonPathUpdaterService: IPythonPathUpdaterServiceManager, @inject(IDisposableRegistry) private readonly disposableRegistry: Disposable[], - @inject(IApplicationShell) private readonly appShell: IApplicationShell) { } + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IComponentAdapter) private readonly pyenvs: IComponentAdapter, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + ) {} public async activate(resource: Uri): Promise { - const watcher = await this.builder.getWorkspaceVirtualEnvInterpreterWatcher(resource); - watcher.onDidCreate(() => { - this.handleNewEnvironment(resource).ignoreErrors(); - }, this, this.disposableRegistry); + const disposable = this.pyenvs.onDidCreate(resource, () => this.handleNewEnvironment(resource)); + this.disposableRegistry.push(disposable); } - @traceDecorators.error('Error in event handler for detection of new environment') + @traceDecoratorError('Error in event handler for detection of new environment') protected async handleNewEnvironment(resource: Uri): Promise { - // Wait for a while, to ensure environment gets created and is accessible (as this is slow on Windows) - await sleep(1000); - const interpreters = await this.locator.getInterpreters(resource); - const interpreter = this.helper.getBestInterpreter(interpreters); - if (!interpreter || this.hasUserDefinedPythonPath(resource)) { + if (isCreatingEnvironment()) { + return; + } + const interpreters = await this.pyenvs.getWorkspaceVirtualEnvInterpreters(resource); + const interpreter = + Array.isArray(interpreters) && interpreters.length > 0 + ? this.helper.getBestInterpreter(interpreters) + : undefined; + if (!interpreter) { + return; + } + const currentInterpreter = await this.interpreterService.getActiveInterpreter(resource); + if (currentInterpreter?.id === interpreter.id) { + traceVerbose('New environment has already been selected'); return; } await this.notifyUser(interpreter, resource); } - protected async notifyUser(interpreter: PythonInterpreter, resource: Uri): Promise { - const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState(doNotDisplayPromptStateKey, true); + + protected async notifyUser(interpreter: PythonEnvironment, resource: Uri): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState( + doNotDisplayPromptStateKey, + true, + ); if (!notificationPromptEnabled.value) { return; } - const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Common.doNotShowAgain()]; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const telemetrySelections: ['Yes', 'No', 'Ignore'] = ['Yes', 'No', 'Ignore']; - const selection = await this.appShell.showInformationMessage(Interpreters.environmentPromptMessage(), ...prompts); - sendTelemetryEvent(EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT, undefined, { selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined }); + 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); + await this.pythonPathUpdaterService.updatePythonPath( + interpreter.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource, + ); } else if (selection === prompts[2]) { await notificationPromptEnabled.updateValue(false); } } - protected hasUserDefinedPythonPath(resource?: Uri) { - const settings = this.workspaceService.getConfiguration('python', resource)!.inspect('pythonPath')!; - return ((settings.workspaceFolderValue && settings.workspaceFolderValue !== 'python') || - (settings.workspaceValue && settings.workspaceValue !== 'python')) ? true : 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(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T { return name ? this.container.getNamed(serviceIdentifier, name) : this.container.get(serviceIdentifier); } - public getAll(serviceIdentifier: string | symbol | Newable | Abstract, name?: string | number | symbol | undefined): T[] { - return name ? this.container.getAllNamed(serviceIdentifier, name) : this.container.getAll(serviceIdentifier); + + public getAll( + serviceIdentifier: string | symbol | Newable | Abstract, + name?: string | number | symbol | undefined, + ): T[] { + return name + ? this.container.getAllNamed(serviceIdentifier, name) + : this.container.getAll(serviceIdentifier); + } + + public tryGet( + serviceIdentifier: interfaces.ServiceIdentifier, + name?: string | number | symbol | undefined, + ): T | undefined { + try { + return name + ? this.container.getNamed(serviceIdentifier, name) + : this.container.get(serviceIdentifier); + } catch { + // This might happen after the container has been destroyed + } + + 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 0fd5e82bbdad..a575b25e8c3f 100644 --- a/src/client/ioc/serviceManager.ts +++ b/src/client/ioc/serviceManager.ts @@ -8,49 +8,100 @@ type identifier = string | symbol | Newable | Abstract; @injectable() export class ServiceManager implements IServiceManager { - constructor(private container: Container) { } - // tslint:disable-next-line:no-any - public add(serviceIdentifier: identifier, constructor: new (...args: any[]) => T, name?: string | number | symbol | undefined): void { + constructor(private container: Container) {} + + public add( + serviceIdentifier: identifier, + + // 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(serviceIdentifier).to(constructor).whenTargetNamed(name); } else { this.container.bind(serviceIdentifier).to(constructor); } + + if (bindings) { + bindings.forEach((binding) => { + this.addBinding(serviceIdentifier, binding); + }); + } } - // tslint:disable-next-line:no-any - public addFactory(factoryIdentifier: interfaces.ServiceIdentifier>, factoryMethod: interfaces.FactoryCreator): void { + + public addFactory( + factoryIdentifier: interfaces.ServiceIdentifier>, + factoryMethod: interfaces.FactoryCreator, + ): void { this.container.bind>(factoryIdentifier).toFactory(factoryMethod); } - // tslint:disable-next-line:no-any - public addBinding(serviceIdentifier1: identifier, serviceIdentifier2: identifier): void { - this.container.bind(serviceIdentifier2).toService(serviceIdentifier1); + public addBinding(from: identifier, to: identifier): void { + this.container.bind(to).toService(from); } - // tslint:disable-next-line:no-any - public addSingleton(serviceIdentifier: identifier, constructor: new (...args: any[]) => T, name?: string | number | symbol | undefined): void { + public addSingleton( + serviceIdentifier: identifier, + + // 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(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); } else { this.container.bind(serviceIdentifier).to(constructor).inSingletonScope(); } + + if (bindings) { + bindings.forEach((binding) => { + this.addBinding(serviceIdentifier, binding); + }); + } } - // tslint:disable-next-line:no-any - public addSingletonInstance(serviceIdentifier: identifier, instance: T, name?: string | number | symbol | undefined): void { + + public addSingletonInstance( + serviceIdentifier: identifier, + instance: T, + name?: string | number | symbol | undefined, + ): void { if (name) { this.container.bind(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); } else { this.container.bind(serviceIdentifier).toConstantValue(instance); } } + public get(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T { return name ? this.container.getNamed(serviceIdentifier, name) : this.container.get(serviceIdentifier); } + + public tryGet(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T | undefined { + try { + return name + ? this.container.getNamed(serviceIdentifier, name) + : this.container.get(serviceIdentifier); + } catch { + // This might happen after the container has been destroyed + } + + return undefined; + } + public getAll(serviceIdentifier: identifier, name?: string | number | symbol | undefined): T[] { - return name ? this.container.getAllNamed(serviceIdentifier, name) : this.container.getAll(serviceIdentifier); + return name + ? this.container.getAllNamed(serviceIdentifier, name) + : this.container.getAll(serviceIdentifier); } - public rebind(serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void { + public rebind( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + ): void { if (name) { this.container.rebind(serviceIdentifier).to(constructor).whenTargetNamed(name); } else { @@ -58,7 +109,23 @@ export class ServiceManager implements IServiceManager { } } - public rebindInstance(serviceIdentifier: interfaces.ServiceIdentifier, instance: T, name?: string | number | symbol): void { + public rebindSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + ): void { + if (name) { + this.container.rebind(serviceIdentifier).to(constructor).inSingletonScope().whenTargetNamed(name); + } else { + this.container.rebind(serviceIdentifier).to(constructor).inSingletonScope(); + } + } + + public rebindInstance( + serviceIdentifier: interfaces.ServiceIdentifier, + instance: T, + name?: string | number | symbol, + ): void { if (name) { this.container.rebind(serviceIdentifier).toConstantValue(instance).whenTargetNamed(name); } else { @@ -66,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 4c6cb69c247f..0a18e44824ed 100644 --- a/src/client/ioc/types.ts +++ b/src/client/ioc/types.ts @@ -2,43 +2,70 @@ // Licensed under the MIT License. import { interfaces } from 'inversify'; +import { IDisposable } from '../common/types'; -//tslint:disable:callable-types -// tslint:disable-next-line:interface-name export interface Newable { - // tslint:disable-next-line:no-any - new(...args: any[]): T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: any[]): T; } -//tslint:enable:callable-types -// tslint:disable-next-line:interface-name export interface Abstract { prototype: T; } -//tslint:disable:callable-types export type ClassType = { - // tslint:disable-next-line:no-any - new(...args: any[]): T; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + new (...args: any[]): T; }; -//tslint:enable:callable-types export const IServiceManager = Symbol('IServiceManager'); -export interface IServiceManager { - add(serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void; - addSingleton(serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void; - addSingletonInstance(serviceIdentifier: interfaces.ServiceIdentifier, instance: T, name?: string | number | symbol): void; - addFactory(factoryIdentifier: interfaces.ServiceIdentifier>, factoryMethod: interfaces.FactoryCreator): void; - addBinding(serviceIdentifier1: interfaces.ServiceIdentifier, serviceIdentifier2: interfaces.ServiceIdentifier): void; +export interface IServiceManager extends IDisposable { + add( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol | undefined, + bindings?: symbol[], + ): void; + addSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + bindings?: symbol[], + ): void; + addSingletonInstance( + serviceIdentifier: interfaces.ServiceIdentifier, + instance: T, + name?: string | number | symbol, + ): void; + addFactory( + factoryIdentifier: interfaces.ServiceIdentifier>, + factoryMethod: interfaces.FactoryCreator, + ): void; + addBinding(from: interfaces.ServiceIdentifier, to: interfaces.ServiceIdentifier): void; get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T; + tryGet(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T | undefined; getAll(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T[]; - rebind(serviceIdentifier: interfaces.ServiceIdentifier, constructor: ClassType, name?: string | number | symbol): void; - rebindInstance(serviceIdentifier: interfaces.ServiceIdentifier, instance: T, name?: string | number | symbol): void; + rebind( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + ): void; + rebindSingleton( + serviceIdentifier: interfaces.ServiceIdentifier, + constructor: ClassType, + name?: string | number | symbol, + ): void; + rebindInstance( + serviceIdentifier: interfaces.ServiceIdentifier, + instance: T, + name?: string | number | symbol, + ): void; } export const IServiceContainer = Symbol('IServiceContainer'); export interface IServiceContainer { get(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T; getAll(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T[]; + tryGet(serviceIdentifier: interfaces.ServiceIdentifier, name?: string | number | symbol): T | undefined; } diff --git a/src/client/jupyter/jupyterExtensionDependencyManager.ts b/src/client/jupyter/jupyterExtensionDependencyManager.ts new file mode 100644 index 000000000000..defd5ea38241 --- /dev/null +++ b/src/client/jupyter/jupyterExtensionDependencyManager.ts @@ -0,0 +1,13 @@ +import { inject, injectable } from 'inversify'; +import { IJupyterExtensionDependencyManager } from '../common/application/types'; +import { JUPYTER_EXTENSION_ID } from '../common/constants'; +import { IExtensions } from '../common/types'; + +@injectable() +export class JupyterExtensionDependencyManager implements IJupyterExtensionDependencyManager { + constructor(@inject(IExtensions) private extensions: IExtensions) {} + + public get isJupyterExtensionInstalled(): boolean { + return this.extensions.getExtension(JUPYTER_EXTENSION_ID) !== undefined; + } +} diff --git a/src/client/jupyter/jupyterIntegration.ts b/src/client/jupyter/jupyterIntegration.ts new file mode 100644 index 000000000000..5584682f3b86 --- /dev/null +++ b/src/client/jupyter/jupyterIntegration.ts @@ -0,0 +1,306 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak, max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { dirname } from 'path'; +import { EventEmitter, Extension, Memento, Uri, workspace, Event } from 'vscode'; +import type { SemVer } from 'semver'; +import { IContextKeyManager, IWorkspaceService } from '../common/application/types'; +import { JUPYTER_EXTENSION_ID, PYLANCE_EXTENSION_ID } from '../common/constants'; +import { GLOBAL_MEMENTO, IExtensions, IMemento, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { + IInterpreterQuickPickItem, + IInterpreterSelector, + IRecommendedEnvironmentService, +} from '../interpreter/configuration/types'; +import { + ICondaService, + IInterpreterDisplay, + IInterpreterService, + IInterpreterStatusbarVisibilityFilter, +} from '../interpreter/contracts'; +import { PylanceApi } from '../activation/node/pylanceApi'; +import { ExtensionContextKey } from '../common/application/contextKeys'; +import { getDebugpyPath } from '../debugger/pythonDebugger'; +import type { Environment, EnvironmentPath, PythonExtension } from '../api/types'; +import { DisposableBase } from '../common/utils/resourceLifecycle'; + +type PythonApiForJupyterExtension = { + /** + * IEnvironmentActivationService + */ + getActivatedEnvironmentVariables( + resource: Resource, + interpreter: Environment, + allowExceptions?: boolean, + ): Promise; + getKnownSuggestions(resource: Resource): IInterpreterQuickPickItem[]; + /** + * @deprecated Use `getKnownSuggestions` and `suggestionToQuickPickItem` instead. + */ + getSuggestions(resource: Resource): Promise; + /** + * Returns path to where `debugpy` is. In python extension this is `/python_files/lib/python`. + */ + getDebuggerPath(): Promise; + /** + * Retrieve interpreter path selected for Jupyter server from Python memento storage + */ + getInterpreterPathSelectedForJupyterServer(): string | undefined; + /** + * Registers a visibility filter for the interpreter status bar. + */ + registerInterpreterStatusFilter(filter: IInterpreterStatusbarVisibilityFilter): void; + getCondaVersion(): Promise; + /** + * Returns the conda executable. + */ + getCondaFile(): Promise; + + /** + * Call to provide a function that the Python extension can call to request the Python + * path to use for a particular notebook. + * @param func : The function that Python should call when requesting the Python path. + */ + registerJupyterPythonPathFunction(func: (uri: Uri) => Promise): void; + + /** + * Returns the preferred environment for the given URI. + */ + getRecommededEnvironment( + uri: Uri | undefined, + ): Promise< + | { + environment: EnvironmentPath; + reason: 'globalUserSelected' | 'workspaceUserSelected' | 'defaultRecommended'; + } + | undefined + >; +}; + +type JupyterExtensionApi = { + /** + * Registers python extension specific parts with the jupyter extension + * @param interpreterService + */ + registerPythonApi(interpreterService: PythonApiForJupyterExtension): void; +}; + +@injectable() +export class JupyterExtensionIntegration { + private jupyterExtension: Extension | undefined; + + private pylanceExtension: Extension | undefined; + private environmentApi: PythonExtension['environments'] | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IInterpreterSelector) private readonly interpreterSelector: IInterpreterSelector, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IMemento) @named(GLOBAL_MEMENTO) private globalState: Memento, + @inject(IInterpreterDisplay) private interpreterDisplay: IInterpreterDisplay, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(ICondaService) private readonly condaService: ICondaService, + @inject(IContextKeyManager) private readonly contextManager: IContextKeyManager, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IRecommendedEnvironmentService) private preferredEnvironmentService: IRecommendedEnvironmentService, + ) {} + public registerEnvApi(api: PythonExtension['environments']) { + this.environmentApi = api; + } + + public registerApi(jupyterExtensionApi: JupyterExtensionApi): JupyterExtensionApi | undefined { + this.contextManager.setContext(ExtensionContextKey.IsJupyterInstalled, true); + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(jupyterExtensionApi)); + return undefined; + } + // Forward python parts + jupyterExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async ( + resource: Resource, + env: Environment, + allowExceptions?: boolean, + ) => { + const interpreter = await this.interpreterService.getInterpreterDetails(env.path); + return this.envActivation.getActivatedEnvironmentVariables(resource, interpreter, allowExceptions); + }, + getSuggestions: async (resource: Resource): Promise => + this.interpreterSelector.getAllSuggestions(resource), + getKnownSuggestions: (resource: Resource): IInterpreterQuickPickItem[] => + this.interpreterSelector.getSuggestions(resource), + getDebuggerPath: async () => dirname(await getDebugpyPath()), + getInterpreterPathSelectedForJupyterServer: () => + this.globalState.get('INTERPRETER_PATH_SELECTED_FOR_JUPYTER_SERVER'), + registerInterpreterStatusFilter: this.interpreterDisplay.registerVisibilityFilter.bind( + this.interpreterDisplay, + ), + getCondaFile: () => this.condaService.getCondaFile(), + getCondaVersion: () => this.condaService.getCondaVersion(), + registerJupyterPythonPathFunction: (func: (uri: Uri) => Promise) => + this.registerJupyterPythonPathFunction(func), + getRecommededEnvironment: async (uri) => { + if (!this.environmentApi) { + return undefined; + } + return this.preferredEnvironmentService.getRecommededEnvironment(uri); + }, + }); + return undefined; + } + + public async integrateWithJupyterExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.pylanceExtension) { + const pylanceExtension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + + if (pylanceExtension && !pylanceExtension.isActive) { + await pylanceExtension.activate(); + } + + this.pylanceExtension = pylanceExtension; + } + + if (!this.jupyterExtension) { + const jupyterExtension = this.extensions.getExtension(JUPYTER_EXTENSION_ID); + if (!jupyterExtension) { + return undefined; + } + await jupyterExtension.activate(); + if (jupyterExtension.isActive) { + this.jupyterExtension = jupyterExtension; + return this.jupyterExtension.exports; + } + } else { + return this.jupyterExtension.exports; + } + return undefined; + } + + private getPylanceApi(): PylanceApi | undefined { + const api = this.pylanceExtension?.exports; + return api && api.notebook && api.client && api.client.isEnabled() ? api : undefined; + } + + private registerJupyterPythonPathFunction(func: (uri: Uri) => Promise) { + const api = this.getPylanceApi(); + if (api) { + api.notebook!.registerJupyterPythonPathFunction(func); + } + } +} + +export interface JupyterPythonEnvironmentApi { + /** + * This event is triggered when the environment associated with a Jupyter Notebook or Interactive Window changes. + * The Uri in the event is the Uri of the Notebook/IW. + */ + onDidChangePythonEnvironment?: Event; + /** + * Returns the EnvironmentPath to the Python environment associated with a Jupyter Notebook or Interactive Window. + * If the Uri is not associated with a Jupyter Notebook or Interactive Window, then this method returns undefined. + * @param uri + */ + getPythonEnvironment?( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + }; +} + +@injectable() +export class JupyterExtensionPythonEnvironments extends DisposableBase implements JupyterPythonEnvironmentApi { + private jupyterExtension?: JupyterPythonEnvironmentApi; + + private readonly _onDidChangePythonEnvironment = this._register(new EventEmitter()); + + public readonly onDidChangePythonEnvironment = this._onDidChangePythonEnvironment.event; + + constructor(@inject(IExtensions) private readonly extensions: IExtensions) { + super(); + } + + public getPythonEnvironment( + uri: Uri, + ): + | undefined + | { + /** + * The ID of the environment. + */ + readonly id: string; + /** + * Path to environment folder or path to python executable that uniquely identifies an environment. Environments + * lacking a python executable are identified by environment folder paths, whereas other envs can be identified + * using python executable path. + */ + readonly path: string; + } { + if (!isJupyterResource(uri)) { + return undefined; + } + const api = this.getJupyterApi(); + if (api?.getPythonEnvironment) { + return api.getPythonEnvironment(uri); + } + return undefined; + } + + private getJupyterApi() { + if (!this.jupyterExtension) { + const ext = this.extensions.getExtension(JUPYTER_EXTENSION_ID); + if (!ext) { + return undefined; + } + if (!ext.isActive) { + ext.activate().then(() => { + this.hookupOnDidChangePythonEnvironment(ext.exports); + }); + return undefined; + } + this.hookupOnDidChangePythonEnvironment(ext.exports); + } + return this.jupyterExtension; + } + + private hookupOnDidChangePythonEnvironment(api: JupyterPythonEnvironmentApi) { + this.jupyterExtension = api; + if (api.onDidChangePythonEnvironment) { + this._register( + api.onDidChangePythonEnvironment( + this._onDidChangePythonEnvironment.fire, + this._onDidChangePythonEnvironment, + ), + ); + } + } +} + +function isJupyterResource(resource: Uri): boolean { + // Jupyter extension only deals with Notebooks and Interactive Windows. + return ( + resource.fsPath.endsWith('.ipynb') || + workspace.notebookDocuments.some((item) => item.uri.toString() === resource.toString()) + ); +} diff --git a/src/client/jupyter/provider.ts b/src/client/jupyter/provider.ts deleted file mode 100644 index 8cf46b12f253..000000000000 --- a/src/client/jupyter/provider.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Position, Range, TextDocument, window } from 'vscode'; - -export class JupyterProvider { - private static isCodeBlock(code: string): boolean { - return code.trim().endsWith(':') && code.indexOf('#') === -1; - } - - /** - * Returns a Regular Expression used to determine whether a line is a Cell delimiter or not - * - * @type {RegExp} - * @memberOf LanguageProvider - */ - get cellIdentifier(): RegExp { - return /^(# %%|#%%|# \|# 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} The code selected. If nothing is to be done, return the parameter value. - * - * @memberOf LanguageProvider - */ - // @ts-ignore - public getSelectedCode(selectedCode: string, currentCell?: Range): Promise { - 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((resolve, _reject) => { - const activeEditor = window.activeTextEditor; - if (!activeEditor) { - return resolve(''); - } - 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 += 1) { - 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} - * - * @memberOf LanguageProvider - */ - public getFirstLineOfExecutableCode(document: TextDocument, range: Range): Promise { - for (let lineNumber = range.start.line; lineNumber < range.end.line; lineNumber += 1) { - const 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)); - } -} diff --git a/src/client/jupyter/requireJupyterPrompt.ts b/src/client/jupyter/requireJupyterPrompt.ts new file mode 100644 index 000000000000..3e6878ba4269 --- /dev/null +++ b/src/client/jupyter/requireJupyterPrompt.ts @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IExtensionSingleActivationService } from '../activation/types'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { Common, Interpreters } from '../common/utils/localize'; +import { Commands, JUPYTER_EXTENSION_ID } from '../common/constants'; +import { IDisposable, IDisposableRegistry } from '../common/types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +@injectable() +export class RequireJupyterPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IDisposableRegistry) private readonly disposables: IDisposable[], + ) {} + + public async activate(): Promise { + this.disposables.push(this.commandManager.registerCommand(Commands.InstallJupyter, () => this._showPrompt())); + } + + public async _showPrompt(): Promise { + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + const telemetrySelections: ['Yes', 'No'] = ['Yes', 'No']; + const selection = await this.appShell.showInformationMessage(Interpreters.requireJupyter, ...prompts); + sendTelemetryEvent(EventName.REQUIRE_JUPYTER_PROMPT, undefined, { + selection: selection ? telemetrySelections[prompts.indexOf(selection)] : undefined, + }); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await this.commandManager.executeCommand( + 'workbench.extensions.installExtension', + JUPYTER_EXTENSION_ID, + undefined, + ); + } + } +} diff --git a/src/client/jupyter/types.ts b/src/client/jupyter/types.ts new file mode 100644 index 000000000000..5eb58c7cf2b2 --- /dev/null +++ b/src/client/jupyter/types.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { QuickPickItem } from 'vscode'; + +interface IJupyterServerUri { + baseUrl: string; + token: string; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + authorizationHeader: any; // JSON object for authorization header. + expiration?: Date; // Date/time when header expires and should be refreshed. + displayName: string; +} + +type JupyterServerUriHandle = string; + +export interface IJupyterUriProvider { + readonly id: string; // Should be a unique string (like a guid) + getQuickPickEntryItems(): QuickPickItem[]; + handleQuickPick(item: QuickPickItem, backEnabled: boolean): Promise; + getServerUri(handle: JupyterServerUriHandle): Promise; +} + +interface IDataFrameInfo { + columns?: { key: string; type: ColumnType }[]; + indexColumn?: string; + rowCount?: number; +} + +export interface IDataViewerDataProvider { + dispose(): void; + getDataFrameInfo(): Promise; + getAllRows(): Promise; + getRows(start: number, end: number): Promise; +} + +enum ColumnType { + String = 'string', + Number = 'number', + Bool = 'bool', +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type IRowsResponse = any[]; diff --git a/src/client/language/braceCounter.ts b/src/client/language/braceCounter.ts deleted file mode 100644 index 30d91b537544..000000000000 --- a/src/client/language/braceCounter.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { IToken, TokenType } from './types'; - -class BracePair { - public readonly openBrace: TokenType; - public readonly closeBrace: TokenType; - - constructor(openBrace: TokenType, closeBrace: TokenType) { - this.openBrace = openBrace; - this.closeBrace = closeBrace; - } -} - -class Stack { - private store: IToken[] = []; - public push(val: IToken) { - this.store.push(val); - } - public pop(): IToken | undefined { - return this.store.pop(); - } - public get length(): number { - return this.store.length; - } -} - -export class BraceCounter { - private readonly bracePairs: BracePair[] = [ - new BracePair(TokenType.OpenBrace, TokenType.CloseBrace), - new BracePair(TokenType.OpenBracket, TokenType.CloseBracket), - new BracePair(TokenType.OpenCurly, TokenType.CloseCurly) - ]; - private braceStacks: Stack[] = [new Stack(), new Stack(), new Stack()]; - - public get count(): number { - let c = 0; - for (const s of this.braceStacks) { - c += s.length; - } - return c; - } - - public isOpened(type: TokenType): boolean { - for (let i = 0; i < this.bracePairs.length; i += 1) { - const pair = this.bracePairs[i]; - if (pair.openBrace === type || pair.closeBrace === type) { - return this.braceStacks[i].length > 0; - } - } - return false; - } - - public countBrace(brace: IToken): boolean { - for (let i = 0; i < this.bracePairs.length; i += 1) { - const pair = this.bracePairs[i]; - if (pair.openBrace === brace.type) { - this.braceStacks[i].push(brace); - return true; - } - if (pair.closeBrace === brace.type) { - if (this.braceStacks[i].length > 0) { - this.braceStacks[i].pop(); - } - return true; - } - } - return false; - } -} diff --git a/src/client/language/characterStream.ts b/src/client/language/characterStream.ts deleted file mode 100644 index 09f3bed33f9d..000000000000 --- a/src/client/language/characterStream.ts +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -// tslint:disable-next-line:import-name -import Char from 'typescript-char'; -import { isLineBreak, isWhiteSpace } from './characters'; -import { TextIterator } from './textIterator'; -import { ICharacterStream, ITextIterator } from './types'; - -export class CharacterStream implements ICharacterStream { - private text: ITextIterator; - private _position: number; - private _currentChar: number; - private _isEndOfStream: boolean; - - constructor(text: string | ITextIterator) { - this.text = typeof text === 'string' ? new TextIterator(text) : text; - this._position = 0; - this._currentChar = text.length > 0 ? text.charCodeAt(0) : 0; - this._isEndOfStream = text.length === 0; - } - - public getText(): string { - return this.text.getText(); - } - - public get position(): number { - return this._position; - } - - public set position(value: number) { - this._position = value; - this.checkBounds(); - } - - public get currentChar(): number { - return this._currentChar; - } - - public get nextChar(): number { - return this.position + 1 < this.text.length ? this.text.charCodeAt(this.position + 1) : 0; - } - - public get prevChar(): number { - return this.position - 1 >= 0 ? this.text.charCodeAt(this.position - 1) : 0; - } - - public isEndOfStream(): boolean { - return this._isEndOfStream; - } - - public lookAhead(offset: number): number { - const pos = this._position + offset; - return pos < 0 || pos >= this.text.length ? 0 : this.text.charCodeAt(pos); - } - - public advance(offset: number) { - this.position += offset; - } - - public moveNext(): boolean { - if (this._position < this.text.length - 1) { - // Most common case, no need to check bounds extensively - this._position += 1; - this._currentChar = this.text.charCodeAt(this._position); - return true; - } - this.advance(1); - return !this.isEndOfStream(); - } - - public isAtWhiteSpace(): boolean { - return isWhiteSpace(this.currentChar); - } - - public isAtLineBreak(): boolean { - return isLineBreak(this.currentChar); - } - - public skipLineBreak(): void { - if (this._currentChar === Char.CarriageReturn) { - this.moveNext(); - if (this.currentChar === Char.LineFeed) { - this.moveNext(); - } - } else if (this._currentChar === Char.LineFeed) { - this.moveNext(); - } - } - - public skipWhitespace(): void { - while (!this.isEndOfStream() && this.isAtWhiteSpace()) { - this.moveNext(); - } - } - - public skipToEol(): void { - while (!this.isEndOfStream() && !this.isAtLineBreak()) { - this.moveNext(); - } - } - - public skipToWhitespace(): void { - while (!this.isEndOfStream() && !this.isAtWhiteSpace()) { - this.moveNext(); - } - } - - public isAtString(): boolean { - return this.currentChar === Char.SingleQuote || this.currentChar === Char.DoubleQuote; - } - - public charCodeAt(index: number): number { - return this.text.charCodeAt(index); - } - - public get length(): number { - return this.text.length; - } - - private checkBounds(): void { - if (this._position < 0) { - this._position = 0; - } - - this._isEndOfStream = this._position >= this.text.length; - if (this._isEndOfStream) { - this._position = this.text.length; - } - - this._currentChar = this._isEndOfStream ? 0 : this.text.charCodeAt(this._position); - } -} diff --git a/src/client/language/characters.ts b/src/client/language/characters.ts deleted file mode 100644 index 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 implements Iterable{ - constructor(private textRangeCollection: ITextRangeCollection) { - } - public [Symbol.iterator](): Iterator { - let index = -1; - - return { - next: (): IteratorResult => { - if (index < this.textRangeCollection.count - 1) { - return { - done: false, - value: this.textRangeCollection.getItemAt(index += 1) - }; - } else { - return { - done: true, - // tslint:disable-next-line:no-any - value: undefined as any - }; - } - } - }; - } -} diff --git a/src/client/language/languageConfiguration.ts b/src/client/language/languageConfiguration.ts new file mode 100644 index 000000000000..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 implements ITextRangeCollection { - private items: T[]; - - constructor(items: T[]) { - this.items = items; - } - - public get start(): number { - return this.items.length > 0 ? this.items[0].start : 0; - } - - public get end(): number { - return this.items.length > 0 ? this.items[this.items.length - 1].end : 0; - } - - public get length(): number { - return this.end - this.start; - } - - public get count(): number { - return this.items.length; - } - - public contains(position: number) { - return position >= this.start && position < this.end; - } - - public getItemAt(index: number): T { - if (index < 0 || index >= this.items.length) { - throw new Error('index is out of range'); - } - return this.items[index] as T; - } - - public getItemAtPosition(position: number): number { - if (this.count === 0) { - return -1; - } - if (position < this.start) { - return -1; - } - if (position >= this.end) { - return -1; - } - - let min = 0; - let max = this.count - 1; - - while (min <= max) { - const mid = Math.floor(min + (max - min) / 2); - const item = this.items[mid]; - - if (item.start === position) { - return mid; - } - - if (position < item.start) { - max = mid - 1; - } else { - min = mid + 1; - } - } - return -1; - } - - public getItemContaining(position: number): number { - if (this.count === 0) { - return -1; - } - if (position < this.start) { - return -1; - } - if (position > this.end) { - return -1; - } - - let min = 0; - let max = this.count - 1; - - while (min <= max) { - const mid = Math.floor(min + (max - min) / 2); - const item = this.items[mid]; - - if (item.contains(position)) { - return mid; - } - if (mid < this.count - 1 && item.end <= position && position < this.items[mid + 1].start) { - return -1; - } - - if (position < item.start) { - max = mid - 1; - } else { - min = mid + 1; - } - } - return -1; - } -} diff --git a/src/client/language/tokenizer.ts b/src/client/language/tokenizer.ts deleted file mode 100644 index b6fd9ab4f7ed..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 } from './characters'; -import { CharacterStream } from './characterStream'; -import { TextRangeCollection } from './textRangeCollection'; -import { ICharacterStream, ITextRangeCollection, IToken, ITokenizer, TextRange, TokenizerMode, TokenType } from './types'; - -enum QuoteType { - None, - Single, - Double, - TripleSingle, - TripleDouble -} - -class Token extends TextRange implements IToken { - public readonly type: TokenType; - - constructor(type: TokenType, start: number, length: number) { - super(start, length); - this.type = type; - } -} - -export class Tokenizer implements ITokenizer { - private cs: ICharacterStream = new CharacterStream(''); - private tokens: IToken[] = []; - private mode = TokenizerMode.Full; - - public tokenize(text: string): ITextRangeCollection; - public tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection; - - public tokenize(text: string, start?: number, length?: number, mode?: TokenizerMode): ITextRangeCollection { - if (start === undefined) { - start = 0; - } else if (start < 0 || start >= text.length) { - throw new Error('Invalid range start'); - } - - if (length === undefined) { - length = text.length; - } else if (length < 0 || start + length > text.length) { - throw new Error('Invalid range length'); - } - - this.mode = mode !== undefined ? mode : TokenizerMode.Full; - - this.cs = new CharacterStream(text); - this.cs.position = start; - - const end = start + length; - while (!this.cs.isEndOfStream()) { - this.AddNextToken(); - if (this.cs.position >= end) { - break; - } - } - return new TextRangeCollection(this.tokens); - } - - private AddNextToken(): void { - this.cs.skipWhitespace(); - if (this.cs.isEndOfStream()) { - return; - } - - if (!this.handleCharacter()) { - this.cs.moveNext(); - } - } - - // tslint:disable-next-line:cyclomatic-complexity - private handleCharacter(): boolean { - // f-strings, b-strings, etc - const stringPrefixLength = this.getStringPrefixLength(); - if (stringPrefixLength >= 0) { - // Indeed a string - this.cs.advance(stringPrefixLength); - - const quoteType = this.getQuoteType(); - if (quoteType !== QuoteType.None) { - this.handleString(quoteType, stringPrefixLength); - return true; - } - } - if (this.cs.currentChar === Char.Hash) { - this.handleComment(); - return true; - } - if (this.mode === TokenizerMode.CommentsAndStrings) { - return false; - } - - switch (this.cs.currentChar) { - case Char.OpenParenthesis: - this.tokens.push(new Token(TokenType.OpenBrace, this.cs.position, 1)); - break; - case Char.CloseParenthesis: - this.tokens.push(new Token(TokenType.CloseBrace, this.cs.position, 1)); - break; - case Char.OpenBracket: - this.tokens.push(new Token(TokenType.OpenBracket, this.cs.position, 1)); - break; - case Char.CloseBracket: - this.tokens.push(new Token(TokenType.CloseBracket, this.cs.position, 1)); - break; - case Char.OpenBrace: - this.tokens.push(new Token(TokenType.OpenCurly, this.cs.position, 1)); - break; - case Char.CloseBrace: - this.tokens.push(new Token(TokenType.CloseCurly, this.cs.position, 1)); - break; - case Char.Comma: - this.tokens.push(new Token(TokenType.Comma, this.cs.position, 1)); - break; - case Char.Semicolon: - this.tokens.push(new Token(TokenType.Semicolon, this.cs.position, 1)); - break; - case Char.Colon: - this.tokens.push(new Token(TokenType.Colon, this.cs.position, 1)); - break; - default: - if (this.isPossibleNumber()) { - if (this.tryNumber()) { - return true; - } - } - if (this.cs.currentChar === Char.Period) { - this.tokens.push(new Token(TokenType.Operator, this.cs.position, 1)); - break; - } - if (!this.tryIdentifier()) { - if (!this.tryOperator()) { - this.handleUnknown(); - } - } - return true; - } - return false; - } - - private tryIdentifier(): boolean { - const start = this.cs.position; - if (isIdentifierStartChar(this.cs.currentChar)) { - this.cs.moveNext(); - while (isIdentifierChar(this.cs.currentChar)) { - this.cs.moveNext(); - } - } - if (this.cs.position > start) { - // const text = this.cs.getText().substr(start, this.cs.position - start); - // const type = this.keywords.find((value, index) => value === text) ? TokenType.Keyword : TokenType.Identifier; - this.tokens.push(new Token(TokenType.Identifier, start, this.cs.position - start)); - return true; - } - return false; - } - - // tslint:disable-next-line:cyclomatic-complexity - private isPossibleNumber(): boolean { - if (isDecimal(this.cs.currentChar)) { - return true; - } - - if (this.cs.currentChar === Char.Period && isDecimal(this.cs.nextChar)) { - return true; - } - - const next = (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus) ? 1 : 0; - // Next character must be decimal or a dot otherwise - // it is not a number. No whitespace is allowed. - if (isDecimal(this.cs.lookAhead(next)) || this.cs.lookAhead(next) === Char.Period) { - // Check what previous token is, if any - if (this.tokens.length === 0) { - // At the start of the file this can only be a number - return true; - } - - const prev = this.tokens[this.tokens.length - 1]; - if (prev.type === TokenType.OpenBrace - || prev.type === TokenType.OpenBracket - || prev.type === TokenType.Comma - || prev.type === TokenType.Colon - || prev.type === TokenType.Semicolon - || prev.type === TokenType.Operator) { - return true; - } - } - - if (this.cs.lookAhead(next) === Char._0) { - const nextNext = this.cs.lookAhead(next + 1); - if (nextNext === Char.x || nextNext === Char.X) { - return true; - } - if (nextNext === Char.b || nextNext === Char.B) { - return true; - } - if (nextNext === Char.o || nextNext === Char.O) { - return true; - } - } - - return false; - } - - // tslint:disable-next-line:cyclomatic-complexity - private tryNumber(): boolean { - const start = this.cs.position; - let leadingSign = 0; - - if (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus) { - this.cs.moveNext(); // Skip leading +/- - leadingSign = 1; - } - - if (this.cs.currentChar === Char._0) { - let radix = 0; - // Try hex => hexinteger: "0" ("x" | "X") (["_"] hexdigit)+ - if ((this.cs.nextChar === Char.x || this.cs.nextChar === Char.X) && isHex(this.cs.lookAhead(2))) { - this.cs.advance(2); - while (isHex(this.cs.currentChar)) { - this.cs.moveNext(); - } - radix = 16; - } - // Try binary => bininteger: "0" ("b" | "B") (["_"] bindigit)+ - if ((this.cs.nextChar === Char.b || this.cs.nextChar === Char.B) && isBinary(this.cs.lookAhead(2))) { - this.cs.advance(2); - while (isBinary(this.cs.currentChar)) { - this.cs.moveNext(); - } - radix = 2; - } - // Try octal => octinteger: "0" ("o" | "O") (["_"] octdigit)+ - if ((this.cs.nextChar === Char.o || this.cs.nextChar === Char.O) && isOctal(this.cs.lookAhead(2))) { - this.cs.advance(2); - while (isOctal(this.cs.currentChar)) { - this.cs.moveNext(); - } - radix = 8; - } - if (radix > 0) { - const text = this.cs.getText().substr(start + leadingSign, this.cs.position - start - leadingSign); - if (!isNaN(parseInt(text, radix))) { - this.tokens.push(new Token(TokenType.Number, start, text.length + leadingSign)); - return true; - } - } - } - - let decimal = false; - // Try decimal int => - // decinteger: nonzerodigit (["_"] digit)* | "0" (["_"] "0")* - // nonzerodigit: "1"..."9" - // digit: "0"..."9" - if (this.cs.currentChar >= Char._1 && this.cs.currentChar <= Char._9) { - while (isDecimal(this.cs.currentChar)) { - this.cs.moveNext(); - } - decimal = this.cs.currentChar !== Char.Period && this.cs.currentChar !== Char.e && this.cs.currentChar !== Char.E; - } - - if (this.cs.currentChar === Char._0) { // "0" (["_"] "0")* - while (this.cs.currentChar === Char._0 || this.cs.currentChar === Char.Underscore) { - this.cs.moveNext(); - } - decimal = this.cs.currentChar !== Char.Period && this.cs.currentChar !== Char.e && this.cs.currentChar !== Char.E; - } - - if (decimal) { - const text = this.cs.getText().substr(start + leadingSign, this.cs.position - start - leadingSign); - if (!isNaN(parseInt(text, 10))) { - this.tokens.push(new Token(TokenType.Number, start, text.length + leadingSign)); - return true; - } - } - - // Floating point. Sign was already skipped over. - if ((this.cs.currentChar >= Char._0 && this.cs.currentChar <= Char._9) || - (this.cs.currentChar === Char.Period && this.cs.nextChar >= Char._0 && this.cs.nextChar <= Char._9)) { - if (this.skipFloatingPointCandidate(false)) { - const text = this.cs.getText().substr(start, this.cs.position - start); - if (!isNaN(parseFloat(text))) { - this.tokens.push(new Token(TokenType.Number, start, this.cs.position - start)); - return true; - } - } - } - - this.cs.position = start; - return false; - } - - // tslint:disable-next-line:cyclomatic-complexity - private tryOperator(): boolean { - let length = 0; - const nextChar = this.cs.nextChar; - switch (this.cs.currentChar) { - case Char.Plus: - case Char.Ampersand: - case Char.Bar: - case Char.Caret: - case Char.Equal: - case Char.ExclamationMark: - case Char.Percent: - case Char.Tilde: - length = nextChar === Char.Equal ? 2 : 1; - break; - - case Char.Hyphen: - length = nextChar === Char.Equal || nextChar === Char.Greater ? 2 : 1; - break; - - case Char.Asterisk: - if (nextChar === Char.Asterisk) { - length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; - } else { - length = nextChar === Char.Equal ? 2 : 1; - } - break; - - case Char.Slash: - if (nextChar === Char.Slash) { - length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; - } else { - length = nextChar === Char.Equal ? 2 : 1; - } - break; - - case Char.Less: - if (nextChar === Char.Greater) { - length = 2; - } else if (nextChar === Char.Less) { - length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; - } else { - length = nextChar === Char.Equal ? 2 : 1; - } - break; - - case Char.Greater: - if (nextChar === Char.Greater) { - length = this.cs.lookAhead(2) === Char.Equal ? 3 : 2; - } else { - length = nextChar === Char.Equal ? 2 : 1; - } - break; - - case Char.At: - length = nextChar === Char.Equal ? 2 : 1; - break; - - default: - return false; - } - this.tokens.push(new Token(TokenType.Operator, this.cs.position, length)); - this.cs.advance(length); - return length > 0; - } - - private handleUnknown(): boolean { - const start = this.cs.position; - this.cs.skipToWhitespace(); - const length = this.cs.position - start; - if (length > 0) { - this.tokens.push(new Token(TokenType.Unknown, start, length)); - return true; - } - return false; - } - - private handleComment(): void { - const start = this.cs.position; - this.cs.skipToEol(); - this.tokens.push(new Token(TokenType.Comment, start, this.cs.position - start)); - } - - // tslint:disable-next-line:cyclomatic-complexity - private getStringPrefixLength(): number { - if (this.cs.currentChar === Char.SingleQuote || this.cs.currentChar === Char.DoubleQuote) { - return 0; // Simple string, no prefix - } - - if (this.cs.nextChar === Char.SingleQuote || this.cs.nextChar === Char.DoubleQuote) { - switch (this.cs.currentChar) { - case Char.f: - case Char.F: - case Char.r: - case Char.R: - case Char.b: - case Char.B: - case Char.u: - case Char.U: - return 1; // single-char prefix like u"" or r"" - default: - break; - } - } - - if (this.cs.lookAhead(2) === Char.SingleQuote || this.cs.lookAhead(2) === Char.DoubleQuote) { - const prefix = this.cs.getText().substr(this.cs.position, 2).toLowerCase(); - switch (prefix) { - case 'rf': - case 'ur': - case 'br': - return 2; - default: - break; - } - } - return -1; - } - - private getQuoteType(): QuoteType { - if (this.cs.currentChar === Char.SingleQuote) { - return this.cs.nextChar === Char.SingleQuote && this.cs.lookAhead(2) === Char.SingleQuote - ? QuoteType.TripleSingle - : QuoteType.Single; - } - if (this.cs.currentChar === Char.DoubleQuote) { - return this.cs.nextChar === Char.DoubleQuote && this.cs.lookAhead(2) === Char.DoubleQuote - ? QuoteType.TripleDouble - : QuoteType.Double; - } - return QuoteType.None; - } - - private handleString(quoteType: QuoteType, stringPrefixLength: number): void { - const start = this.cs.position - stringPrefixLength; - if (quoteType === QuoteType.Single || quoteType === QuoteType.Double) { - this.cs.moveNext(); - this.skipToSingleEndQuote(quoteType === QuoteType.Single - ? Char.SingleQuote - : Char.DoubleQuote); - } else { - this.cs.advance(3); - this.skipToTripleEndQuote(quoteType === QuoteType.TripleSingle - ? Char.SingleQuote - : Char.DoubleQuote); - } - this.tokens.push(new Token(TokenType.String, start, this.cs.position - start)); - } - - private skipToSingleEndQuote(quote: number): void { - while (!this.cs.isEndOfStream()) { - if (this.cs.currentChar === Char.LineFeed || this.cs.currentChar === Char.CarriageReturn) { - return; // Unterminated single-line string - } - if (this.cs.currentChar === Char.Backslash && this.cs.nextChar === quote) { - this.cs.advance(2); - continue; - } - if (this.cs.currentChar === quote) { - break; - } - this.cs.moveNext(); - } - this.cs.moveNext(); - } - - private skipToTripleEndQuote(quote: number): void { - while (!this.cs.isEndOfStream() && (this.cs.currentChar !== quote || this.cs.nextChar !== quote || this.cs.lookAhead(2) !== quote)) { - this.cs.moveNext(); - } - this.cs.advance(3); - } - - private skipFloatingPointCandidate(allowSign: boolean): boolean { - // Determine end of the potential floating point number - const start = this.cs.position; - this.skipFractionalNumber(allowSign); - if (this.cs.position > start) { - if (this.cs.currentChar === Char.e || this.cs.currentChar === Char.E) { - this.cs.moveNext(); // Optional exponent sign - } - this.skipDecimalNumber(true); // skip exponent value - } - return this.cs.position > start; - } - - private skipFractionalNumber(allowSign: boolean): void { - this.skipDecimalNumber(allowSign); - if (this.cs.currentChar === Char.Period) { - this.cs.moveNext(); // Optional period - } - this.skipDecimalNumber(false); - } - - private skipDecimalNumber(allowSign: boolean): void { - if (allowSign && (this.cs.currentChar === Char.Hyphen || this.cs.currentChar === Char.Plus)) { - this.cs.moveNext(); // Optional sign - } - while (isDecimal(this.cs.currentChar)) { - this.cs.moveNext(); // skip integer part - } - } -} diff --git a/src/client/language/types.ts b/src/client/language/types.ts deleted file mode 100644 index 51618039a3d4..000000000000 --- a/src/client/language/types.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export interface ITextRange { - readonly start: number; - readonly end: number; - readonly length: number; - contains(position: number): boolean; -} - -export class TextRange implements ITextRange { - public static readonly empty = TextRange.fromBounds(0, 0); - - public readonly start: number; - public readonly length: number; - - constructor(start: number, length: number) { - if (start < 0) { - throw new Error('start must be non-negative'); - } - if (length < 0) { - throw new Error('length must be non-negative'); - } - this.start = start; - this.length = length; - } - - public static fromBounds(start: number, end: number) { - return new TextRange(start, end - start); - } - - public get end(): number { - return this.start + this.length; - } - - public contains(position: number): boolean { - return position >= this.start && position < this.end; - } -} - -export interface ITextRangeCollection extends ITextRange { - count: number; - getItemAt(index: number): T; - getItemAtPosition(position: number): number; - getItemContaining(position: number): number; -} - -export interface ITextIterator { - readonly length: number; - charCodeAt(index: number): number; - getText(): string; -} - -export interface ICharacterStream extends ITextIterator { - position: number; - readonly currentChar: number; - readonly nextChar: number; - readonly prevChar: number; - getText(): string; - isEndOfStream(): boolean; - lookAhead(offset: number): number; - advance(offset: number): void; - moveNext(): boolean; - isAtWhiteSpace(): boolean; - isAtLineBreak(): boolean; - isAtString(): boolean; - skipLineBreak(): void; - skipWhitespace(): void; - skipToEol(): void; - skipToWhitespace(): void; -} - -export enum TokenType { - Unknown, - String, - Comment, - Keyword, - Number, - Identifier, - Operator, - Colon, - Semicolon, - Comma, - OpenBrace, - CloseBrace, - OpenBracket, - CloseBracket, - OpenCurly, - CloseCurly -} - -export interface IToken extends ITextRange { - readonly type: TokenType; -} - -export enum TokenizerMode { - CommentsAndStrings, - Full -} - -export interface ITokenizer { - tokenize(text: string): ITextRangeCollection; - tokenize(text: string, start: number, length: number, mode: TokenizerMode): ITextRangeCollection; -} diff --git a/src/client/language/unicode.ts b/src/client/language/unicode.ts deleted file mode 100644 index 9b3ca0b15b25..000000000000 --- a/src/client/language/unicode.ts +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -// tslint:disable:no-require-imports no-var-requires - -export enum UnicodeCategory { - Unknown, - UppercaseLetter, - LowercaseLetter, - TitlecaseLetter, - ModifierLetter, - OtherLetter, - LetterNumber, - NonSpacingMark, - SpacingCombiningMark, - DecimalDigitNumber, - ConnectorPunctuation -} - -export function getUnicodeCategory(ch: number): UnicodeCategory { - const unicodeLu = require('unicode/category/Lu'); - const unicodeLl = require('unicode/category/Ll'); - const unicodeLt = require('unicode/category/Lt'); - const unicodeLo = require('unicode/category/Lo'); - const unicodeLm = require('unicode/category/Lm'); - const unicodeNl = require('unicode/category/Nl'); - const unicodeMn = require('unicode/category/Mn'); - const unicodeMc = require('unicode/category/Mc'); - const unicodeNd = require('unicode/category/Nd'); - const unicodePc = require('unicode/category/Pc'); - - if (unicodeLu[ch]) { - return UnicodeCategory.UppercaseLetter; - } - if (unicodeLl[ch]) { - return UnicodeCategory.LowercaseLetter; - } - if (unicodeLt[ch]) { - return UnicodeCategory.TitlecaseLetter; - } - if (unicodeLo[ch]) { - return UnicodeCategory.OtherLetter; - } - if (unicodeLm[ch]) { - return UnicodeCategory.ModifierLetter; - } - if (unicodeNl[ch]) { - return UnicodeCategory.LetterNumber; - } - if (unicodeMn[ch]) { - return UnicodeCategory.NonSpacingMark; - } - if (unicodeMc[ch]) { - return UnicodeCategory.SpacingCombiningMark; - } - if (unicodeNd[ch]) { - return UnicodeCategory.DecimalDigitNumber; - } - if (unicodePc[ch]) { - return UnicodeCategory.ConnectorPunctuation; - } - return UnicodeCategory.Unknown; -} diff --git a/src/client/languageServer/jediLSExtensionManager.ts b/src/client/languageServer/jediLSExtensionManager.ts new file mode 100644 index 000000000000..4cbfb6f33466 --- /dev/null +++ b/src/client/languageServer/jediLSExtensionManager.ts @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { JediLanguageServerAnalysisOptions } from '../activation/jedi/analysisOptions'; +import { JediLanguageClientFactory } from '../activation/jedi/languageClientFactory'; +import { JediLanguageServerProxy } from '../activation/jedi/languageServerProxy'; +import { JediLanguageServerManager } from '../activation/jedi/manager'; +import { ILanguageServerOutputChannel } from '../activation/types'; +import { IWorkspaceService, ICommandManager } from '../common/application/types'; +import { + IExperimentService, + IInterpreterPathService, + IConfigurationService, + Resource, + IDisposable, +} from '../common/types'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceError } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ILanguageServerExtensionManager } from './types'; + +export class JediLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + private serverProxy: JediLanguageServerProxy; + + serverManager: JediLanguageServerManager; + + clientFactory: JediLanguageClientFactory; + + analysisOptions: JediLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + _experimentService: IExperimentService, + workspaceService: IWorkspaceService, + configurationService: IConfigurationService, + _interpreterPathService: IInterpreterPathService, + interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + commandManager: ICommandManager, + ) { + this.analysisOptions = new JediLanguageServerAnalysisOptions( + environmentService, + outputChannel, + configurationService, + workspaceService, + ); + this.clientFactory = new JediLanguageClientFactory(interpreterService); + this.serverProxy = new JediLanguageServerProxy(this.clientFactory); + this.serverManager = new JediLanguageServerManager( + serviceContainer, + this.analysisOptions, + this.serverProxy, + commandManager, + ); + } + + dispose(): void { + this.serverManager.disconnect(); + this.serverManager.dispose(); + this.serverProxy.dispose(); + this.analysisOptions.dispose(); + } + + async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + async stopLanguageServer(): Promise { + this.serverManager.disconnect(); + await this.serverProxy.stop(); + } + + // eslint-disable-next-line class-methods-use-this + canStartLanguageServer(interpreter: PythonEnvironment | undefined): boolean { + if (!interpreter) { + traceError('Unable to start Jedi language server as a valid interpreter is not selected'); + return false; + } + // Otherwise return true for now since it's shipped with the extension. + // Update this when JediLSP is pulled in a separate extension. + return true; + } + + // eslint-disable-next-line class-methods-use-this + languageServerNotAvailable(): Promise { + // Nothing to do here. + // Update this when JediLSP is pulled in a separate extension. + return Promise.resolve(); + } +} diff --git a/src/client/languageServer/noneLSExtensionManager.ts b/src/client/languageServer/noneLSExtensionManager.ts new file mode 100644 index 000000000000..1d93ea50be51 --- /dev/null +++ b/src/client/languageServer/noneLSExtensionManager.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable class-methods-use-this */ + +import { ILanguageServerExtensionManager } from './types'; + +// This LS manager implements ILanguageServer directly +// instead of extending LanguageServerCapabilities because it doesn't need to do anything. +export class NoneLSExtensionManager implements ILanguageServerExtensionManager { + dispose(): void { + // Nothing to do here. + } + + startLanguageServer(): Promise { + return Promise.resolve(); + } + + stopLanguageServer(): Promise { + return Promise.resolve(); + } + + canStartLanguageServer(): boolean { + return true; + } + + languageServerNotAvailable(): Promise { + // Nothing to do here. + return Promise.resolve(); + } +} diff --git a/src/client/languageServer/pylanceLSExtensionManager.ts b/src/client/languageServer/pylanceLSExtensionManager.ts new file mode 100644 index 000000000000..7b03d909a512 --- /dev/null +++ b/src/client/languageServer/pylanceLSExtensionManager.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { promptForPylanceInstall } from '../activation/common/languageServerChangeHandler'; +import { NodeLanguageServerAnalysisOptions } from '../activation/node/analysisOptions'; +import { NodeLanguageClientFactory } from '../activation/node/languageClientFactory'; +import { NodeLanguageServerProxy } from '../activation/node/languageServerProxy'; +import { NodeLanguageServerManager } from '../activation/node/manager'; +import { ILanguageServerOutputChannel } from '../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { PYLANCE_EXTENSION_ID } from '../common/constants'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposable, + IExperimentService, + IExtensions, + IInterpreterPathService, + Resource, +} from '../common/types'; +import { Pylance } from '../common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceLog } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { ILanguageServerExtensionManager } from './types'; + +export class PylanceLSExtensionManager implements IDisposable, ILanguageServerExtensionManager { + private serverProxy: NodeLanguageServerProxy; + + serverManager: NodeLanguageServerManager; + + clientFactory: NodeLanguageClientFactory; + + analysisOptions: NodeLanguageServerAnalysisOptions; + + constructor( + serviceContainer: IServiceContainer, + outputChannel: ILanguageServerOutputChannel, + experimentService: IExperimentService, + readonly workspaceService: IWorkspaceService, + readonly configurationService: IConfigurationService, + interpreterPathService: IInterpreterPathService, + _interpreterService: IInterpreterService, + environmentService: IEnvironmentVariablesProvider, + readonly commandManager: ICommandManager, + fileSystem: IFileSystem, + private readonly extensions: IExtensions, + readonly applicationShell: IApplicationShell, + ) { + this.analysisOptions = new NodeLanguageServerAnalysisOptions(outputChannel, workspaceService); + this.clientFactory = new NodeLanguageClientFactory(fileSystem, extensions); + this.serverProxy = new NodeLanguageServerProxy( + this.clientFactory, + experimentService, + interpreterPathService, + environmentService, + workspaceService, + extensions, + ); + this.serverManager = new NodeLanguageServerManager( + serviceContainer, + this.analysisOptions, + this.serverProxy, + commandManager, + extensions, + ); + } + + dispose(): void { + this.serverManager.disconnect(); + this.serverManager.dispose(); + this.serverProxy.dispose(); + this.analysisOptions.dispose(); + } + + async startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise { + await this.serverManager.start(resource, interpreter); + this.serverManager.connect(); + } + + async stopLanguageServer(): Promise { + this.serverManager.disconnect(); + await this.serverProxy.stop(); + } + + canStartLanguageServer(): boolean { + const extension = this.extensions.getExtension(PYLANCE_EXTENSION_ID); + return !!extension; + } + + async languageServerNotAvailable(): Promise { + await promptForPylanceInstall( + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ); + + traceLog(Pylance.pylanceNotInstalledMessage); + } +} diff --git a/src/client/languageServer/types.ts b/src/client/languageServer/types.ts new file mode 100644 index 000000000000..f7cad157fcef --- /dev/null +++ b/src/client/languageServer/types.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LanguageServerType } from '../activation/types'; +import { Resource } from '../common/types'; +import { PythonEnvironment } from '../pythonEnvironments/info'; + +export const ILanguageServerWatcher = Symbol('ILanguageServerWatcher'); +/** + * The language server watcher serves as a singleton that watches for changes to the language server setting, + * and instantiates the relevant language server extension manager. + */ +export interface ILanguageServerWatcher { + readonly languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + readonly languageServerType: LanguageServerType; + startLanguageServer(languageServerType: LanguageServerType, resource?: Resource): Promise; + restartLanguageServers(): Promise; + get(resource: Resource, interpreter?: PythonEnvironment): Promise; +} + +/** + * `ILanguageServerExtensionManager` implementations act as wrappers for anything related to their specific language server extension. + * They are responsible for starting and stopping the language server provided by their LS extension. + * They also extend the `ILanguageServer` interface via `ILanguageServerCapabilities` to continue supporting the Jupyter integration. + */ +export interface ILanguageServerExtensionManager { + startLanguageServer(resource: Resource, interpreter?: PythonEnvironment): Promise; + stopLanguageServer(): Promise; + canStartLanguageServer(interpreter: PythonEnvironment | undefined): boolean; + languageServerNotAvailable(): Promise; + dispose(): void; +} diff --git a/src/client/languageServer/watcher.ts b/src/client/languageServer/watcher.ts new file mode 100644 index 000000000000..39e6e0bb1ece --- /dev/null +++ b/src/client/languageServer/watcher.ts @@ -0,0 +1,406 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { ConfigurationChangeEvent, l10n, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; +import { LanguageServerChangeHandler } from '../activation/common/languageServerChangeHandler'; +import { IExtensionActivationService, ILanguageServerOutputChannel, LanguageServerType } from '../activation/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../common/application/types'; +import { IFileSystem } from '../common/platform/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IExtensions, + IInterpreterPathService, + InterpreterConfigurationScope, + Resource, +} from '../common/types'; +import { LanguageService } from '../common/utils/localize'; +import { IEnvironmentVariablesProvider } from '../common/variables/types'; +import { IInterpreterHelper, IInterpreterService } from '../interpreter/contracts'; +import { IServiceContainer } from '../ioc/types'; +import { traceLog } from '../logging'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { JediLSExtensionManager } from './jediLSExtensionManager'; +import { NoneLSExtensionManager } from './noneLSExtensionManager'; +import { PylanceLSExtensionManager } from './pylanceLSExtensionManager'; +import { ILanguageServerExtensionManager, ILanguageServerWatcher } from './types'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { StopWatch } from '../common/utils/stopWatch'; + +@injectable() +/** + * The Language Server Watcher class implements the ILanguageServerWatcher interface, which is the one-stop shop for language server activation. + */ +export class LanguageServerWatcher implements IExtensionActivationService, ILanguageServerWatcher { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: true, virtualWorkspace: true }; + + languageServerExtensionManager: ILanguageServerExtensionManager | undefined; + + languageServerType: LanguageServerType; + + private workspaceInterpreters: Map; + + // In a multiroot workspace scenario we may have multiple language servers running: + // When using Jedi, there will be one language server per workspace folder. + // When using Pylance, there will only be one language server for the project. + private workspaceLanguageServers: Map; + + private registered = false; + + constructor( + @inject(IServiceContainer) private readonly serviceContainer: IServiceContainer, + @inject(ILanguageServerOutputChannel) private readonly lsOutputChannel: ILanguageServerOutputChannel, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + @inject(IInterpreterHelper) private readonly interpreterHelper: IInterpreterHelper, + @inject(IInterpreterPathService) private readonly interpreterPathService: IInterpreterPathService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IEnvironmentVariablesProvider) private readonly environmentService: IEnvironmentVariablesProvider, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IFileSystem) private readonly fileSystem: IFileSystem, + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IApplicationShell) readonly applicationShell: IApplicationShell, + @inject(IDisposableRegistry) readonly disposables: IDisposableRegistry, + ) { + this.workspaceInterpreters = new Map(); + this.workspaceLanguageServers = new Map(); + this.languageServerType = this.configurationService.getSettings().languageServer; + } + + // IExtensionActivationService + + public async activate(resource?: Resource, startupStopWatch?: StopWatch): Promise { + this.register(); + await this.startLanguageServer(this.languageServerType, resource, startupStopWatch); + } + + // ILanguageServerWatcher + public async startLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + startupStopWatch?: StopWatch, + ): Promise { + await this.startAndGetLanguageServer(languageServerType, resource, startupStopWatch); + } + + public register(): void { + if (!this.registered) { + this.registered = true; + this.disposables.push( + this.workspaceService.onDidChangeConfiguration(this.onDidChangeConfiguration.bind(this)), + ); + + this.disposables.push( + this.workspaceService.onDidChangeWorkspaceFolders(this.onDidChangeWorkspaceFolders.bind(this)), + ); + + this.disposables.push( + this.interpreterService.onDidChangeInterpreterInformation(this.onDidChangeInterpreterInformation, this), + ); + + if (this.workspaceService.isTrusted) { + this.disposables.push(this.interpreterPathService.onDidChange(this.onDidChangeInterpreter.bind(this))); + } + + this.disposables.push( + this.extensions.onDidChange(async () => { + await this.extensionsChangeHandler(); + }), + ); + + this.disposables.push( + new LanguageServerChangeHandler( + this.languageServerType, + this.extensions, + this.applicationShell, + this.commandManager, + this.workspaceService, + this.configurationService, + ), + ); + } + } + + private async startAndGetLanguageServer( + languageServerType: LanguageServerType, + resource?: Resource, + startupStopWatch?: StopWatch, + ): Promise { + const lsResource = this.getWorkspaceUri(resource); + const currentInterpreter = this.workspaceInterpreters.get(lsResource.fsPath); + const interpreter = await this.interpreterService?.getActiveInterpreter(resource); + + // Destroy the old language server if it's different. + if (currentInterpreter && interpreter !== currentInterpreter) { + await this.stopLanguageServer(lsResource); + } + + // If the interpreter is Python 2 and the LS setting is explicitly set to Jedi, turn it off. + // If set to Default, use Pylance. + let serverType = languageServerType; + if (interpreter && (interpreter.version?.major ?? 0) < 3) { + if (serverType === LanguageServerType.Jedi) { + serverType = LanguageServerType.None; + } else if (this.getCurrentLanguageServerTypeIsDefault()) { + serverType = LanguageServerType.Node; + } + } + + if ( + !this.workspaceService.isTrusted && + serverType !== LanguageServerType.Node && + serverType !== LanguageServerType.None + ) { + traceLog(LanguageService.untrustedWorkspaceMessage); + serverType = LanguageServerType.None; + } + + // If the language server type is Pylance or None, + // We only need to instantiate the language server once, even in multiroot workspace scenarios, + // so we only need one language server extension manager. + const key = this.getWorkspaceKey(resource, serverType); + const languageServer = this.workspaceLanguageServers.get(key); + if ((serverType === LanguageServerType.Node || serverType === LanguageServerType.None) && languageServer) { + logStartup(serverType, lsResource); + return languageServer; + } + + // Instantiate the language server extension manager. + const languageServerExtensionManager = this.createLanguageServer(serverType); + this.workspaceLanguageServers.set(key, languageServerExtensionManager); + + if (languageServerExtensionManager.canStartLanguageServer(interpreter)) { + // Start the language server. + if (startupStopWatch) { + // It means that startup is triggering this code, track time it takes since startup to activate this code. + sendTelemetryEvent(EventName.LANGUAGE_SERVER_TRIGGER_TIME, startupStopWatch.elapsedTime, { + triggerTime: startupStopWatch.elapsedTime, + }); + } + await languageServerExtensionManager.startLanguageServer(lsResource, interpreter); + + logStartup(languageServerType, lsResource); + this.languageServerType = languageServerType; + this.workspaceInterpreters.set(lsResource.fsPath, interpreter); + } else { + await languageServerExtensionManager.languageServerNotAvailable(); + } + + return languageServerExtensionManager; + } + + public async restartLanguageServers(): Promise { + this.workspaceLanguageServers.forEach(async (_, resourceString) => { + sendTelemetryEvent(EventName.LANGUAGE_SERVER_RESTART, undefined, { reason: 'notebooksExperiment' }); + const resource = Uri.parse(resourceString); + await this.stopLanguageServer(resource); + await this.startLanguageServer(this.languageServerType, resource); + }); + } + + public async get(resource?: Resource): Promise { + const key = this.getWorkspaceKey(resource, this.languageServerType); + let languageServerExtensionManager = this.workspaceLanguageServers.get(key); + + if (!languageServerExtensionManager) { + languageServerExtensionManager = await this.startAndGetLanguageServer(this.languageServerType, resource); + } + + return Promise.resolve(languageServerExtensionManager); + } + + // Private methods + + private async stopLanguageServer(resource?: Resource): Promise { + const key = this.getWorkspaceKey(resource, this.languageServerType); + const languageServerExtensionManager = this.workspaceLanguageServers.get(key); + + if (languageServerExtensionManager) { + await languageServerExtensionManager.stopLanguageServer(); + languageServerExtensionManager.dispose(); + this.workspaceLanguageServers.delete(key); + } + } + + private createLanguageServer(languageServerType: LanguageServerType): ILanguageServerExtensionManager { + let lsManager: ILanguageServerExtensionManager; + switch (languageServerType) { + case LanguageServerType.Jedi: + lsManager = new JediLSExtensionManager( + this.serviceContainer, + this.lsOutputChannel, + this.experimentService, + this.workspaceService, + this.configurationService, + this.interpreterPathService, + this.interpreterService, + this.environmentService, + this.commandManager, + ); + break; + case LanguageServerType.Node: + lsManager = new PylanceLSExtensionManager( + this.serviceContainer, + this.lsOutputChannel, + this.experimentService, + this.workspaceService, + this.configurationService, + this.interpreterPathService, + this.interpreterService, + this.environmentService, + this.commandManager, + this.fileSystem, + this.extensions, + this.applicationShell, + ); + break; + case LanguageServerType.None: + default: + lsManager = new NoneLSExtensionManager(); + break; + } + + this.disposables.push({ + dispose: async () => { + await lsManager.stopLanguageServer(); + lsManager.dispose(); + }, + }); + return lsManager; + } + + private async refreshLanguageServer(resource?: Resource, forced?: boolean): Promise { + const lsResource = this.getWorkspaceUri(resource); + const languageServerType = this.configurationService.getSettings(lsResource).languageServer; + + if (languageServerType !== this.languageServerType || forced) { + await this.stopLanguageServer(resource); + await this.startLanguageServer(languageServerType, lsResource); + } + } + + private getCurrentLanguageServerTypeIsDefault(): boolean { + return this.configurationService.getSettings().languageServerIsDefault; + } + + // Watch for settings changes. + private async onDidChangeConfiguration(event: ConfigurationChangeEvent): Promise { + const workspacesUris = this.workspaceService.workspaceFolders?.map((workspace) => workspace.uri) ?? []; + + workspacesUris.forEach(async (resource) => { + if (event.affectsConfiguration(`python.languageServer`, resource)) { + await this.refreshLanguageServer(resource); + } else if (event.affectsConfiguration(`python.analysis.pylanceLspClientEnabled`, resource)) { + await this.refreshLanguageServer(resource, /* forced */ true); + } + }); + } + + // Watch for interpreter changes. + private async onDidChangeInterpreter(event: InterpreterConfigurationScope): Promise { + if (this.languageServerType === LanguageServerType.Node) { + // Pylance client already handles interpreter changes, so restarting LS can be skipped. + return Promise.resolve(); + } + // Reactivate the language server (if in a multiroot workspace scenario, pick the correct one). + return this.activate(event.uri); + } + + // Watch for interpreter information changes. + private async onDidChangeInterpreterInformation(info: PythonEnvironment): Promise { + if (!info.envPath || info.envPath === '') { + return; + } + + // Find the interpreter and workspace that got updated (if any). + const iterator = this.workspaceInterpreters.entries(); + + let result = iterator.next(); + let done = result.done || false; + + while (!done) { + const [resourcePath, interpreter] = result.value as [string, PythonEnvironment | undefined]; + const resource = Uri.parse(resourcePath); + + // Restart the language server if the interpreter path changed (#18995). + if (info.envPath === interpreter?.envPath && info.path !== interpreter?.path) { + await this.activate(resource); + done = true; + } else { + result = iterator.next(); + done = result.done || false; + } + } + } + + // Watch for extension changes. + private async extensionsChangeHandler(): Promise { + const languageServerType = this.configurationService.getSettings().languageServer; + + if (languageServerType !== this.languageServerType) { + await this.refreshLanguageServer(); + } + } + + // Watch for workspace folder changes. + private async onDidChangeWorkspaceFolders(event: WorkspaceFoldersChangeEvent): Promise { + // Since Jedi is the only language server type where we instantiate multiple language servers, + // Make sure to dispose of them only in that scenario. + if (event.removed.length && this.languageServerType === LanguageServerType.Jedi) { + for (const workspace of event.removed) { + await this.stopLanguageServer(workspace.uri); + } + } + } + + // Get the workspace Uri for the given resource, in order to query this.workspaceInterpreters and this.workspaceLanguageServers. + private getWorkspaceUri(resource?: Resource): Uri { + let uri; + + if (resource) { + uri = this.workspaceService.getWorkspaceFolder(resource)?.uri; + } else { + uri = this.interpreterHelper.getActiveWorkspaceUri(resource)?.folderUri; + } + + return uri ?? Uri.parse('default'); + } + + // Get the key used to identify which language server extension manager is associated to which workspace. + // When using Pylance or having no LS enabled, we return a static key since there should only be one LS extension manager for these LS types. + private getWorkspaceKey(resource: Resource | undefined, languageServerType: LanguageServerType): string { + switch (languageServerType) { + case LanguageServerType.Node: + return 'Pylance'; + case LanguageServerType.None: + return 'None'; + default: + return this.getWorkspaceUri(resource).fsPath; + } + } +} + +function logStartup(languageServerType: LanguageServerType, resource: Uri): void { + let outputLine; + const basename = path.basename(resource.fsPath); + + switch (languageServerType) { + case LanguageServerType.Jedi: + outputLine = l10n.t('Starting Jedi language server for {0}.', basename); + break; + case LanguageServerType.Node: + outputLine = LanguageService.startingPylance; + break; + case LanguageServerType.None: + outputLine = LanguageService.startingNone; + break; + default: + throw new Error(`Unknown language server type: ${languageServerType}`); + } + traceLog(outputLine); +} diff --git a/src/client/languageServices/jediProxyFactory.ts b/src/client/languageServices/jediProxyFactory.ts deleted file mode 100644 index 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>; - - constructor(private extensionRootPath: string, private serviceContainer: IServiceContainer) { - this.disposables = []; - this.jediProxyHandlers = new Map>(); - } - - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - this.disposables = []; - } - public getJediProxyHandler(resource?: Uri): JediProxyHandler { - const 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; - } -} diff --git a/src/client/languageServices/languageServerSurveyBanner.ts b/src/client/languageServices/languageServerSurveyBanner.ts deleted file mode 100644 index 1b4a8060502f..000000000000 --- a/src/client/languageServices/languageServerSurveyBanner.ts +++ /dev/null @@ -1,146 +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 enabled(): boolean { - return this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise { - 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 { - 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 { - await this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowBanner, false).updateValue(false); - } - - public async launchSurvey(): Promise { - 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 { - const state = this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowAttemptCounter, 0); - await state.updateValue(state.value + 1); - return state.value; - } - - private async getPythonLSVersion(fallback: string = 'unknown'): Promise { - const langServiceLatestFolder: FolderVersionPair | undefined = await this.lsService.getCurrentLanguageServerDirectory(); - return langServiceLatestFolder ? langServiceLatestFolder.version.raw : fallback; - } - - private async getPythonLSLaunchCounter(): Promise { - const state = this.persistentState.createGlobalPersistentState(LSSurveyStateKeys.ShowAttemptCounter, 0); - return state.value; - } - - private async getPythonLSLaunchThresholdCounter(): Promise { - const state = this.persistentState.createGlobalPersistentState(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 e881bbe628ec..000000000000 --- a/src/client/languageServices/proposeLanguageServerBanner.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 { 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 enabled(): boolean { - return this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, true).value; - } - - public async showBanner(): Promise { - if (!this.enabled) { - return; - } - - const show = await this.shouldShowBanner(); - if (!show) { - return; - } - - const response = await this.appShell.showInformationMessage(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 { - return Promise.resolve(this.enabled && !this.disabledInCurrentSession); - } - - public async disable(): Promise { - await this.persistentState.createGlobalPersistentState(ProposeLSStateKeys.ShowBanner, false).updateValue(false); - } - - public async enableNewLanguageServer(): Promise { - 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 3a950c91ca67..000000000000 --- a/src/client/linters/bandit.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { CancellationToken, OutputChannel, TextDocument } from 'vscode'; -import '../common/extensions'; -import { Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { BaseLinter } from './baseLinter'; -import { ILintMessage, LintMessageSeverity } from './types'; - -const severityMapping: Record = { - LOW: LintMessageSeverity.Information, - MEDIUM: LintMessageSeverity.Warning, - HIGH: LintMessageSeverity.Error -}; - -export class Bandit extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.bandit, outputChannel, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - // View all errors in bandit <= 1.5.1 (https://github.com/PyCQA/bandit/issues/371) - const messages = await this.run([ - '-f', 'custom', '--msg-template', '{line},0,{severity},{test_id}:{msg}', '-n', '-1', document.uri.fsPath - ], document, cancellation); - - messages.forEach(msg => { - msg.severity = severityMapping[msg.type]; - }); - return messages; - } -} diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts deleted file mode 100644 index 859ced9296a4..000000000000 --- a/src/client/linters/baseLinter.ts +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import '../common/extensions'; -import { 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 no-any -const namedRegexp = require('named-js-regexp'); -// Allow negative column numbers (https://github.com/PyCQA/pylint/issues/1822) -const REGEX = '(?\\d+),(?-?\\d+),(?\\w+),(?\\w\\d+):(?.*)\\r?(\\n|$)'; - -export interface IRegexGroup { - line: number; - column: number; - code: string; - message: string; - type: string; -} - -export function matchNamedRegEx(data: string, regex: string): IRegexGroup | undefined { - const compiledRegexp = namedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups(); - } - - return undefined; -} - -export function parseLine( - line: string, - regex: string, - linterID: LinterId, - colOffset: number = 0 -): ILintMessage | undefined { - const match = matchNamedRegEx(line, regex)!; - if (!match) { - return; - } - - // tslint:disable-next-line:no-any - match.line = Number(match.line); - // tslint:disable-next-line:no-any - match.column = Number(match.column); - - return { - code: match.code, - message: match.message, - column: isNaN(match.column) || match.column <= 0 ? 0 : match.column - colOffset, - line: match.line, - type: match.type, - provider: linterID - }; -} - -export abstract class BaseLinter implements ILinter { - protected readonly configService: IConfigurationService; - - private errorHandler: ErrorHandler; - private _pythonSettings!: IPythonSettings; - private _info: ILinterInfo; - private workspace: IWorkspaceService; - - protected get pythonSettings(): IPythonSettings { - return this._pythonSettings; - } - - constructor(product: Product, - protected readonly outputChannel: vscode.OutputChannel, - protected readonly serviceContainer: IServiceContainer, - protected readonly columnOffset = 0) { - this._info = serviceContainer.get(ILinterManager).getLinterInfo(product); - this.errorHandler = new ErrorHandler(this.info.product, outputChannel, serviceContainer); - this.configService = serviceContainer.get(IConfigurationService); - this.workspace = serviceContainer.get(IWorkspaceService); - } - - public get info(): ILinterInfo { - return this._info; - } - - public async lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise { - this._pythonSettings = this.configService.getSettings(document.uri); - return this.runLinter(document, cancellation); - } - - protected getWorkspaceRootPath(document: vscode.TextDocument): string { - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; - return typeof workspaceRootPath === 'string' ? workspaceRootPath : path.dirname(document.uri.fsPath); - } - protected get logger(): ILogger { - return this.serviceContainer.get(ILogger); - } - protected abstract runLinter(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; - - // tslint:disable-next-line:no-any - protected parseMessagesSeverity(error: string, categorySeverity: any): LintMessageSeverity { - if (categorySeverity[error]) { - const severityName = categorySeverity[error]; - switch (severityName) { - case 'Error': - return LintMessageSeverity.Error; - case 'Hint': - return LintMessageSeverity.Hint; - case 'Information': - return LintMessageSeverity.Information; - case 'Warning': - return LintMessageSeverity.Warning; - default: { - if (LintMessageSeverity[severityName]) { - // tslint:disable-next-line:no-any - return LintMessageSeverity[severityName]; - } - } - } - } - return LintMessageSeverity.Information; - } - - protected async run(args: string[], document: vscode.TextDocument, cancellation: vscode.CancellationToken, regEx: string = REGEX): Promise { - if (!this.info.isEnabled(document.uri)) { - return []; - } - const executionInfo = this.info.getExecutionInfo(args, document.uri); - const cwd = this.getWorkspaceRootPath(document); - const pythonToolsExecutionService = this.serviceContainer.get(IPythonToolExecutionService); - try { - const result = await pythonToolsExecutionService.exec(executionInfo, { cwd, token: cancellation, mergeStdOutErr: false }, document.uri); - this.displayLinterResultHeader(result.stdout); - return await this.parseMessages(result.stdout, document, cancellation, regEx); - } catch (error) { - await this.handleError(error, document.uri, executionInfo); - return []; - } - } - - protected async parseMessages(output: string, _document: vscode.TextDocument, _token: vscode.CancellationToken, regEx: string) { - const outputLines = output.splitLines({ removeEmptyEntries: false, trim: false }); - return this.parseLines(outputLines, regEx); - } - - protected async handleError(error: Error, resource: vscode.Uri, execInfo: ExecutionInfo) { - if (isTestExecution()) { - this.errorHandler.handleError(error, resource, execInfo) - .ignoreErrors(); - } else { - this.errorHandler.handleError(error, resource, execInfo) - .catch(this.logger.logError.bind(this, 'Error in errorHandler.handleError')) - .ignoreErrors(); - } - } - - private parseLine(line: string, regEx: string): ILintMessage | undefined { - return parseLine(line, regEx, this.info.id, this.columnOffset); - } - - private parseLines(outputLines: string[], regEx: string): ILintMessage[] { - const messages: ILintMessage[] = []; - for (const line of outputLines) { - try { - const msg = this.parseLine(line, regEx); - if (msg) { - messages.push(msg); - if (messages.length >= this.pythonSettings.linting.maxNumberOfProblems) { - break; - } - } - } catch (ex) { - 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/constants.ts b/src/client/linters/constants.ts deleted file mode 100644 index a3067c2eead6..000000000000 --- a/src/client/linters/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Product } from '../common/types'; -import { LinterId } from './types'; - -// All supported linters must be in this map. -export const LINTERID_BY_PRODUCT = new Map([ - [Product.bandit, 'bandit'], - [Product.flake8, 'flake8'], - [Product.pylint, 'pylint'], - [Product.mypy, 'mypy'], - [Product.pep8, 'pep8'], - [Product.prospector, 'prospector'], - [Product.pydocstyle, 'pydocstyle'], - [Product.pylama, 'pylama'] -]); diff --git a/src/client/linters/errorHandlers/baseErrorHandler.ts b/src/client/linters/errorHandlers/baseErrorHandler.ts deleted file mode 100644 index 08ab74bb57fa..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); - this.installer = this.serviceContainer.get(IInstaller); - } - protected get nextHandler(): IErrorHandler | undefined { - return this.handler; - } - public setNextHandler(handler: IErrorHandler): void { - this.handler = handler; - } - public abstract handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; -} diff --git a/src/client/linters/errorHandlers/errorHandler.ts b/src/client/linters/errorHandlers/errorHandler.ts deleted file mode 100644 index ea4009ddd85d..000000000000 --- a/src/client/linters/errorHandlers/errorHandler.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { OutputChannel, Uri } from 'vscode'; -import { ExecutionInfo, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { IErrorHandler } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; -import { NotInstalledErrorHandler } from './notInstalled'; -import { StandardErrorHandler } from './standard'; - -export class ErrorHandler implements IErrorHandler { - private handler: BaseErrorHandler; - constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - // Create chain of handlers. - const standardErrorHandler = new StandardErrorHandler(product, outputChannel, serviceContainer); - this.handler = new NotInstalledErrorHandler(product, outputChannel, serviceContainer); - this.handler.setNextHandler(standardErrorHandler); - } - - public handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - return this.handler.handleError(error, resource, execInfo); - } -} diff --git a/src/client/linters/errorHandlers/notInstalled.ts b/src/client/linters/errorHandlers/notInstalled.ts deleted file mode 100644 index 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 { - const pythonExecutionService = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource }); - const isModuleInstalled = await pythonExecutionService.isModuleInstalled(execInfo.moduleName!); - if (isModuleInstalled) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : false; - } - - this.installer.promptToInstall(this.product, resource) - .catch(this.logger.logError.bind(this, 'NotInstalledErrorHandler.promptToInstall')); - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - const customError = `Linter '${info.id}' is not installed. Please install it or select another linter".`; - this.outputChannel.appendLine(`\n${customError}\n${error}`); - 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 082a0b2a36fe..000000000000 --- a/src/client/linters/errorHandlers/standard.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { OutputChannel, Uri } from 'vscode'; -import { IApplicationShell } from '../../common/application/types'; -import { ExecutionInfo, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { ILinterManager, LinterId } from '../types'; -import { BaseErrorHandler } from './baseErrorHandler'; - -export class StandardErrorHandler extends BaseErrorHandler { - constructor(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(product, outputChannel, serviceContainer); - } - public async handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise { - if (typeof error === 'string' && (error as string).indexOf('OSError: [Errno 2] No such file or directory: \'/') > 0) { - return this.nextHandler ? this.nextHandler.handleError(error, resource, execInfo) : Promise.resolve(false); - } - - const linterManager = this.serviceContainer.get(ILinterManager); - const info = linterManager.getLinterInfo(execInfo.product!); - - 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) - .ignoreErrors(); - return true; - } - private async displayLinterError(linterId: LinterId) { - const message = `There was an error in running the linter '${linterId}'`; - const appShell = this.serviceContainer.get(IApplicationShell); - await appShell.showErrorMessage(message, 'View Errors'); - this.outputChannel.show(); - } -} diff --git a/src/client/linters/flake8.ts b/src/client/linters/flake8.ts deleted file mode 100644 index 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 { - 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 15e2bc076981..000000000000 --- a/src/client/linters/linterAvailability.ts +++ /dev/null @@ -1,136 +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, IWorkspaceService } from '../common/application/types'; -import '../common/extensions'; -import { IFileSystem } from '../common/platform/types'; -import { IConfigurationService, IPersistentStateFactory, Resource } from '../common/types'; -import { Common, Linters } from '../common/utils/localize'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { IAvailableLinterActivator, ILinterInfo } from './types'; - -const doNotDisplayPromptStateKey = 'MESSAGE_KEY_FOR_CONFIGURE_AVAILABLE_LINTER_PROMPT'; -@injectable() -export class AvailableLinterActivator implements IAvailableLinterActivator { - constructor( - @inject(IApplicationShell) private appShell: IApplicationShell, - @inject(IFileSystem) private fs: IFileSystem, - @inject(IWorkspaceService) private workspaceService: IWorkspaceService, - @inject(IConfigurationService) private configService: IConfigurationService, - @inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory - ) { } - - /** - * Check if it is possible to enable an otherwise-unconfigured linter in - * the current workspace, and if so ask the user if they want that linter - * configured explicitly. - * - * @param linterInfo The linter to check installation status. - * @param resource Context for the operation (required when in multi-root workspaces). - * - * @returns true if configuration was updated in any way, false otherwise. - */ - public async promptIfLinterAvailable(linterInfo: ILinterInfo, resource?: Uri): Promise { - // Has the feature been enabled yet? - if (!this.isFeatureEnabled) { - return false; - } - - // Has the linter in question has been configured explicitly? If so, no need to continue. - if (!this.isLinterUsingDefaultConfiguration(linterInfo, resource)) { - return false; - } - - // Is the linter available in the current workspace? - if (await this.isLinterAvailable(linterInfo, resource)) { - - // great, it is - ask the user if they'd like to enable it. - return this.promptToConfigureAvailableLinter(linterInfo); - } - return false; - } - - /** - * Raise a dialog asking the user if they would like to explicitly configure a - * linter or not in their current workspace. - * - * @param linterInfo The linter to ask the user to enable or not. - * - * @returns true if the user requested a configuration change, false otherwise. - */ - public async promptToConfigureAvailableLinter(linterInfo: ILinterInfo): Promise { - const notificationPromptEnabled = this.persistentStateFactory.createWorkspacePersistentState(doNotDisplayPromptStateKey, true); - if (!notificationPromptEnabled.value) { - return false; - } - const optButtons = [ - Linters.enableLinter().format(linterInfo.id), - Common.notNow(), - Common.doNotShowAgain() - ]; - - const telemetrySelections: ['enable', 'ignore', 'disablePrompt'] = ['enable', 'ignore', 'disablePrompt']; - const pick = await this.appShell.showInformationMessage(Linters.enablePylint().format(linterInfo.id), ...optButtons); - sendTelemetryEvent(EventName.CONFIGURE_AVAILABLE_LINTER_PROMPT, undefined, { tool: linterInfo.id, action: pick ? telemetrySelections[optButtons.indexOf(pick)] : undefined }); - if (pick === optButtons[0]) { - await linterInfo.enableAsync(true); - return true; - } else if (pick === optButtons[2]) { - await notificationPromptEnabled.updateValue(false); - } - return false; - } - - /** - * Check if the linter itself is available in the workspace's Python environment or - * not. - * - * @param linterProduct Linter to check in the current workspace environment. - * @param resource Context information for workspace. - */ - public async isLinterAvailable(linterInfo: ILinterInfo, resource: Resource): Promise { - if (!this.workspaceService.hasWorkspaceFolders) { - return false; - } - const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource) || this.workspaceService.workspaceFolders![0]; - let isAvailable = false; - for (const configName of linterInfo.configFileNames) { - const configPath = path.join(workspaceFolder.uri.fsPath, configName); - isAvailable = isAvailable || await this.fs.fileExists(configPath); - } - return isAvailable; - } - - /** - * Check if the given linter has been configured by the user in this workspace or not. - * - * @param linterInfo Linter to check for configuration status. - * @param resource Context information. - * - * @returns true if the linter has not been configured at the user, workspace, or workspace-folder scope. false otherwise. - */ - public isLinterUsingDefaultConfiguration(linterInfo: ILinterInfo, resource?: Uri): boolean { - const ws = this.workspaceService.getConfiguration('python.linting', resource); - const pe = ws!.inspect(linterInfo.enabledSettingName); - return (pe!.globalValue === undefined && pe!.workspaceValue === undefined && pe!.workspaceFolderValue === undefined); - } - - /** - * Check if this feature is enabled yet. - * - * This is a feature of the vscode-python extension that will become enabled once the - * Python Language Server becomes the default, replacing Jedi as the default. Testing - * the global default setting for `"python.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 d99e275c5173..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 { EventName } from '../telemetry/constants'; -import { ILinterManager, ILintingEngine, LinterId } from './types'; - -export class LinterCommands implements IDisposable { - private disposables: Disposable[] = []; - private linterManager: ILinterManager; - private readonly appShell: IApplicationShell; - private readonly documentManager: IDocumentManager; - - constructor(private serviceContainer: IServiceContainer) { - this.linterManager = this.serviceContainer.get(ILinterManager); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.documentManager = this.serviceContainer.get(IDocumentManager); - - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.registerCommand(Commands.Set_Linter, this.setLinterAsync.bind(this)); - commandManager.registerCommand(Commands.Enable_Linter, this.enableLintingAsync.bind(this)); - commandManager.registerCommand(Commands.Run_Linter, this.runLinting.bind(this)); - } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); - } - - public async setLinterAsync(): Promise { - const linters = this.linterManager.getAllLinterInfos(); - const suggestions = linters.map(x => x.id).sort(); - const linterList = ['Disable Linting', ...suggestions]; - const activeLinters = await this.linterManager.getActiveLinters(true, this.settingsUri); - - let current: string; - switch (activeLinters.length) { - case 0: - current = 'none'; - break; - case 1: - current = activeLinters[0].id; - break; - default: - current = 'multiple selected'; - break; - } - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}` - }; - - const selection = await this.appShell.showQuickPick(linterList, quickPickOptions); - if (selection !== undefined) { - if (selection === 'Disable Linting') { - await this.linterManager.enableLintingAsync(false); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { enabled: false }); - } else { - const index = linters.findIndex(x => x.id === selection); - if (activeLinters.length > 1) { - const response = await this.appShell.showWarningMessage(Linters.replaceWithSelectedLinter().format(selection), 'Yes', 'No'); - if (response !== 'Yes') { - return; - } - } - await this.linterManager.setActiveLintersAsync([linters[index].product], this.settingsUri); - sendTelemetryEvent(EventName.SELECT_LINTER, undefined, { tool: selection as LinterId, enabled: true }); - } - } - } - - public async enableLintingAsync(): Promise { - const options = ['on', 'off']; - const current = await this.linterManager.isLintingEnabled(true, this.settingsUri) ? options[0] : options[1]; - - const quickPickOptions: QuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${current}` - }; - - const selection = await this.appShell.showQuickPick(options, quickPickOptions); - if (selection !== undefined) { - const enable = selection === options[0]; - await this.linterManager.enableLintingAsync(enable, this.settingsUri); - } - } - - public runLinting(): Promise { - const engine = this.serviceContainer.get(ILintingEngine); - return engine.lintOpenPythonFiles(); - } - - private get settingsUri(): Uri | undefined { - return this.documentManager.activeTextEditor ? this.documentManager.activeTextEditor.document.uri : undefined; - } -} diff --git a/src/client/linters/linterInfo.ts b/src/client/linters/linterInfo.ts deleted file mode 100644 index 566db9ab59da..000000000000 --- a/src/client/linters/linterInfo.ts +++ /dev/null @@ -1,90 +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'; - -// tslint:disable:no-any - -export class LinterInfo implements ILinterInfo { - private _id: LinterId; - private _product: Product; - private _configFileNames: string[]; - - constructor(product: Product, id: LinterId, protected configService: IConfigurationService, configFileNames: string[] = []) { - this._product = product; - this._id = id; - this._configFileNames = configFileNames; - } - - public get id(): LinterId { - return this._id; - } - public get product(): Product { - return this._product; - } - - public get pathSettingName(): string { - return `${this.id}Path`; - } - public get argsSettingName(): string { - return `${this.id}Args`; - } - public get enabledSettingName(): string { - return `${this.id}Enabled`; - } - public get configFileNames(): string[] { - return this._configFileNames; - } - - public async enableAsync(enabled: boolean, resource?: Uri): Promise { - return this.configService.updateSetting(`linting.${this.enabledSettingName}`, enabled, resource); - } - public isEnabled(resource?: Uri): boolean { - const settings = this.configService.getSettings(resource); - return (settings.linting as any)[this.enabledSettingName] as boolean; - } - - public pathName(resource?: Uri): string { - const settings = this.configService.getSettings(resource); - return (settings.linting as any)[this.pathSettingName] as string; - } - public linterArgs(resource?: Uri): string[] { - const settings = this.configService.getSettings(resource); - const args = (settings.linting as any)[this.argsSettingName]; - return Array.isArray(args) ? args as string[] : []; - } - public getExecutionInfo(customArgs: string[], resource?: Uri): ExecutionInfo { - const execPath = this.pathName(resource); - const args = this.linterArgs(resource).concat(customArgs); - let moduleName: string | undefined; - - // If path information is not available, then treat it as a module, - if (path.basename(execPath) === execPath) { - moduleName = execPath; - } - - return { execPath, moduleName, args, product: this.product }; - } -} - -export class PylintLinterInfo extends LinterInfo { - constructor(configService: IConfigurationService, private readonly workspaceService: IWorkspaceService, configFileNames: string[] = []) { - super(Product.pylint, '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('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 7b133b88f51e..000000000000 --- a/src/client/linters/linterManager.ts +++ /dev/null @@ -1,163 +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 { - return []; - } -} - -@injectable() -export class LinterManager implements ILinterManager { - protected linters: ILinterInfo[]; - private configService: IConfigurationService; - private checkedForInstalledLinters = new Set(); - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) { - this.configService = serviceContainer.get(IConfigurationService); - // Note that we use unit tests to ensure all the linters are here. - this.linters = [ - new LinterInfo(Product.bandit, '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 '${Product[product]}'`); - } - - public async isLintingEnabled(silent: boolean, resource?: Uri): Promise { - const settings = this.configService.getSettings(resource); - const activeLintersPresent = await this.getActiveLinters(silent, resource); - return settings.linting.enabled && activeLintersPresent.length > 0; - } - - public async enableLintingAsync(enable: boolean, resource?: Uri): Promise { - await this.configService.updateSetting('linting.enabled', enable, resource); - } - - public async getActiveLinters(silent: boolean, resource?: Uri): Promise { - if (!silent) { - await this.enableUnconfiguredLinters(resource); - } - return this.linters.filter(x => x.isEnabled(resource)); - } - - public async setActiveLintersAsync(products: Product[], resource?: Uri): Promise { - // ensure we only allow valid linters to be set, otherwise leave things alone. - // filter out any invalid products: - const validProducts = products.filter(product => { - const foundIndex = this.linters.findIndex(validLinter => validLinter.product === product); - return foundIndex !== -1; - }); - - // if we have valid linter product(s), enable only those - if (validProducts.length > 0) { - const active = await this.getActiveLinters(true, resource); - for (const x of active) { - await x.enableAsync(false, resource); - } - if (products.length > 0) { - const toActivate = this.linters.filter(x => products.findIndex(p => x.product === p) >= 0); - for (const x of toActivate) { - await x.enableAsync(true, resource); - } - await this.enableLintingAsync(true, resource); - } - } - } - - public async createLinter(product: Product, outputChannel: OutputChannel, serviceContainer: IServiceContainer, resource?: Uri): Promise { - if (!await this.isLintingEnabled(true, resource)) { - return new DisabledLinter(this.configService); - } - const error = 'Linter manager: Unknown linter'; - switch (product) { - case Product.bandit: - return new Bandit(outputChannel, serviceContainer); - case Product.flake8: - return new Flake8(outputChannel, serviceContainer); - case Product.pylint: - return new Pylint(outputChannel, serviceContainer); - case Product.mypy: - return new MyPy(outputChannel, serviceContainer); - case Product.prospector: - return new Prospector(outputChannel, serviceContainer); - case Product.pylama: - return new PyLama(outputChannel, serviceContainer); - case Product.pydocstyle: - return new PyDocStyle(outputChannel, serviceContainer); - case Product.pep8: - return new Pep8(outputChannel, serviceContainer); - default: - serviceContainer.get(ILogger).logError(error); - break; - } - throw new Error(error); - } - - protected async enableUnconfiguredLinters(resource?: Uri): Promise { - const settings = this.configService.getSettings(resource); - if (!settings.linting.pylintEnabled || !settings.linting.enabled) { - return; - } - // If we've already checked during this session for the same workspace and Python path, then don't bother again. - const workspaceKey = `${this.workspaceService.getWorkspaceFolderIdentifier(resource)}${settings.pythonPath}`; - if (this.checkedForInstalledLinters.has(workspaceKey)) { - return; - } - this.checkedForInstalledLinters.add(workspaceKey); - - // only check & ask the user if they'd like to enable pylint - const pylintInfo = this.linters.find(linter => linter.id === 'pylint'); - const activator = this.serviceContainer.get(IAvailableLinterActivator); - await activator.promptIfLinterAvailable(pylintInfo!, resource); - } -} diff --git a/src/client/linters/lintingEngine.ts b/src/client/linters/lintingEngine.ts deleted file mode 100644 index fd1aa312147c..000000000000 --- a/src/client/linters/lintingEngine.ts +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { 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 { EventName } from '../telemetry/constants'; -import { LinterTrigger, LintingTelemetry } from '../telemetry/types'; -import { ILinterInfo, ILinterManager, ILintingEngine, ILintMessage, LintMessageSeverity } from './types'; - -const PYTHON: vscode.DocumentFilter = { language: 'python' }; - -const lintSeverityToVSSeverity = new Map(); -lintSeverityToVSSeverity.set(LintMessageSeverity.Error, vscode.DiagnosticSeverity.Error); -lintSeverityToVSSeverity.set(LintMessageSeverity.Hint, vscode.DiagnosticSeverity.Hint); -lintSeverityToVSSeverity.set(LintMessageSeverity.Information, vscode.DiagnosticSeverity.Information); -lintSeverityToVSSeverity.set(LintMessageSeverity.Warning, vscode.DiagnosticSeverity.Warning); - -// tslint:disable-next-line:interface-name -interface DocumentHasJupyterCodeCells { - // tslint:disable-next-line:callable-types - (doc: vscode.TextDocument, token: vscode.CancellationToken): Promise; -} - -@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(); - 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); - this.workspace = serviceContainer.get(IWorkspaceService); - this.configurationService = serviceContainer.get(IConfigurationService); - this.outputChannel = serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.linterManager = serviceContainer.get(ILinterManager); - this.fileSystem = serviceContainer.get(IFileSystem); - this.diagnosticCollection = vscode.languages.createDiagnosticCollection('python'); - } - - public get diagnostics(): vscode.DiagnosticCollection { - return this.diagnosticCollection; - } - - public clearDiagnostics(document: vscode.TextDocument): void { - if (this.diagnosticCollection.has(document.uri)) { - this.diagnosticCollection.delete(document.uri); - } - } - - public async lintOpenPythonFiles(): Promise { - this.diagnosticCollection.clear(); - const promises = this.documents.textDocuments.map(async document => this.lintDocument(document, 'auto')); - await Promise.all(promises); - return this.diagnosticCollection; - } - - public async lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise { - this.diagnosticCollection.set(document.uri, []); - - // Check if we need to lint this document - if (!await this.shouldLintDocument(document)) { - return; - } - - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.get(document.uri.fsPath)!.cancel(); - this.pendingLintings.delete(document.uri.fsPath); - } - - const cancelToken = new vscode.CancellationTokenSource(); - cancelToken.token.onCancellationRequested(() => { - if (this.pendingLintings.has(document.uri.fsPath)) { - this.pendingLintings.delete(document.uri.fsPath); - } - }); - - this.pendingLintings.set(document.uri.fsPath, cancelToken); - - const activeLinters = await this.linterManager.getActiveLinters(false, document.uri); - const promises: Promise[] = activeLinters - .map(async (info: ILinterInfo) => { - const stopWatch = new StopWatch(); - const linter = await this.linterManager.createLinter( - info.product, - this.outputChannel, - this.serviceContainer, - document.uri - ); - const promise = linter.lint(document, cancelToken.token); - this.sendLinterRunTelemetry(info, document.uri, promise, stopWatch, trigger); - return promise; - }); - - 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 | undefined): Promise { - 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, stopWatch: StopWatch, trigger: LinterTrigger): void { - const linterExecutablePathName = info.pathName(resource); - const properties: LintingTelemetry = { - tool: info.id, - hasCustomArgs: info.linterArgs(resource).length > 0, - trigger, - executableSpecified: linterExecutablePathName.length > 0 - }; - sendTelemetryWhenDone(EventName.LINTING, promise, stopWatch, properties); - } - - private isDocumentOpen(uri: vscode.Uri): boolean { - return this.documents.textDocuments.some(document => document.uri.fsPath === uri.fsPath); - } - - private createDiagnostics(message: ILintMessage, _document: vscode.TextDocument): vscode.Diagnostic { - const position = new vscode.Position(message.line - 1, message.column); - const range = new vscode.Range(position, position); - - const severity = lintSeverityToVSSeverity.get(message.severity!)!; - const diagnostic = new vscode.Diagnostic(range, message.message, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - return diagnostic; - } - - private async shouldLintDocument(document: vscode.TextDocument): Promise { - if (!await this.linterManager.isLintingEnabled(false, document.uri)) { - this.diagnosticCollection.set(document.uri, []); - return false; - } - - if (document.languageId !== PYTHON.language) { - return false; - } - - const workspaceFolder = this.workspace.getWorkspaceFolder(document.uri); - const workspaceRootPath = (workspaceFolder && typeof workspaceFolder.uri.fsPath === 'string') ? workspaceFolder.uri.fsPath : undefined; - const relativeFileName = typeof workspaceRootPath === 'string' ? path.relative(workspaceRootPath, document.fileName) : document.fileName; - - const settings = this.configurationService.getSettings(document.uri); - // { dot: true } is important so dirs like `.venv` will be matched by globs - const ignoreMinmatches = settings.linting.ignorePatterns.map(pattern => new Minimatch(pattern, { dot: true })); - if (ignoreMinmatches.some(matcher => matcher.match(document.fileName) || matcher.match(relativeFileName))) { - return false; - } - if (document.uri.scheme !== 'file' || !document.uri.fsPath) { - return false; - } - return this.fileSystem.fileExists(document.uri.fsPath); - } -} diff --git a/src/client/linters/mypy.ts b/src/client/linters/mypy.ts deleted file mode 100644 index 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 = '(?[^:]+):(?\\d+)(:(?\\d+))?: (?\\w+): (?.*)\\r?(\\n|$)'; - -export class MyPy extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.mypy, outputChannel, serviceContainer); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run([document.uri.fsPath], document, cancellation, REGEX); - messages.forEach(msg => { - msg.severity = this.parseMessagesSeverity(msg.type, this.pythonSettings.linting.mypyCategorySeverity); - msg.code = msg.type; - }); - return messages; - } -} diff --git a/src/client/linters/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 { - 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 bb9eb225d05e..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 { - 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 91ba97146c57..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 { - 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 = '(?.py):(?\\d+):(?\\d+): \\[(?\\w+)\\] (?\\w\\d+):? (?.*)\\r?(\\n|$)'; -const COLUMN_OFF_SET = 1; - -export class PyLama extends BaseLinter { - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.pylama, outputChannel, serviceContainer, COLUMN_OFF_SET); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - const messages = await this.run(['--format=parsable', document.uri.fsPath], document, cancellation, REGEX); - // All messages in pylama are treated as warnings for now. - messages.forEach(msg => { - msg.severity = LintMessageSeverity.Warning; - }); - - return messages; - } -} diff --git a/src/client/linters/pylint.ts b/src/client/linters/pylint.ts deleted file mode 100644 index 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 = '(?\\d+),(?-?\\d+),(?\\w+),(?[\\w-]+):(?.*)\\r?(\\n|$)'; - -export class Pylint extends BaseLinter { - private fileSystem: IFileSystem; - private platformService: IPlatformService; - - constructor(outputChannel: OutputChannel, serviceContainer: IServiceContainer) { - super(Product.pylint, outputChannel, serviceContainer); - this.fileSystem = serviceContainer.get(IFileSystem); - this.platformService = serviceContainer.get(IPlatformService); - } - - protected async runLinter(document: TextDocument, cancellation: CancellationToken): Promise { - let minArgs: string[] = []; - // Only use minimal checkers if - // a) there are no custom arguments and - // b) there is no pylintrc file next to the file or at the workspace root - const uri = document.uri; - const workspaceRoot = this.getWorkspaceRootPath(document); - const settings = this.configService.getSettings(uri); - if (settings.linting.pylintUseMinimalCheckers - && this.info.linterArgs(uri).length === 0 - // Check pylintrc next to the file or above up to and including the workspace root - && !await Pylint.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 { - // 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 { - // 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, LintingEngine); - serviceManager.addSingleton(ILinterManager, LinterManager); - serviceManager.add(IAvailableLinterActivator, AvailableLinterActivator); -} diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts deleted file mode 100644 index 1bc0a4128b30..000000000000 --- a/src/client/linters/types.ts +++ /dev/null @@ -1,77 +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; -} - -// tslint:disable-next-line:no-suspicious-comment -// TODO: Use an enum for LinterID instead of a union of string literals. -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; - isEnabled(resource?: vscode.Uri): boolean; - pathName(resource?: vscode.Uri): string; - linterArgs(resource?: vscode.Uri): string[]; - getExecutionInfo(customArgs: string[], resource?: vscode.Uri): ExecutionInfo; -} - -export interface ILinter { - readonly info: ILinterInfo; - lint(document: vscode.TextDocument, cancellation: vscode.CancellationToken): Promise; -} - -export const IAvailableLinterActivator = Symbol('IAvailableLinterActivator'); -export interface IAvailableLinterActivator { - promptIfLinterAvailable(linter: ILinterInfo, resource?: vscode.Uri): Promise; -} - -export const ILinterManager = Symbol('ILinterManager'); -export interface ILinterManager { - getAllLinterInfos(): ILinterInfo[]; - getLinterInfo(product: Product): ILinterInfo; - getActiveLinters(silent: boolean, resource?: vscode.Uri): Promise; - isLintingEnabled(silent: boolean, resource?: vscode.Uri): Promise; - enableLintingAsync(enable: boolean, resource?: vscode.Uri): Promise; - setActiveLintersAsync(products: Product[], resource?: vscode.Uri): Promise; - createLinter(product: Product, outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer, resource?: vscode.Uri): Promise; -} - -export interface ILintMessage { - line: number; - column: number; - code: string | undefined; - message: string; - type: string; - severity?: LintMessageSeverity; - provider: string; -} -export enum LintMessageSeverity { - Hint, - Error, - Warning, - Information -} - -export const ILintingEngine = Symbol('ILintingEngine'); -export interface ILintingEngine { - readonly diagnostics: vscode.DiagnosticCollection; - lintOpenPythonFiles(): Promise; - lintDocument(document: vscode.TextDocument, trigger: LinterTrigger): Promise; - // tslint:disable-next-line:no-any - linkJupyterExtension(jupyter: vscode.Extension | undefined): Promise; - 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) { + const originalMethod = descriptor.value; + + descriptor.value = function (...args: unknown[]) { + const call = { + kind: 'Class', + name: _ && _.constructor ? _.constructor.name : '', + args, + }; + + // eslint-disable-next-line @typescript-eslint/no-this-alias + const scope = this; + return tracing( + // "log()" + (t) => log(call, t), + // "run()" + () => originalMethod.apply(scope, args), + ); + }; + + return descriptor; + }; +} + +// Call run(), call log() with the trace info, and return the result. +function tracing(log: (t: TraceInfo) => void, run: () => T): T { + const timer = new StopWatch(); + try { + const result = run(); + + // If method being wrapped returns a promise then wait for it. + if (isPromise(result)) { + ((result as unknown) as Promise) + .then((data) => { + log({ elapsed: timer.elapsedTime, returnValue: data }); + return data; + }) + .catch((ex) => { + log({ elapsed: timer.elapsedTime, err: ex }); + + // TODO(GH-11645) Re-throw the error like we do + // in the non-Promise case. + }); + } else { + log({ elapsed: timer.elapsedTime, returnValue: result }); + } + return result; + } catch (ex) { + log({ elapsed: timer.elapsedTime, err: ex as Error | undefined }); + throw ex; + } +} + +function createTracingDecorator(logInfo: LogInfo): TraceDecoratorType { + return traceDecorator((call, traced) => logResult(logInfo, traced, call)); +} + +function normalizeCall(call: CallInfo): CallInfo { + let { kind, name, args } = call; + if (!kind || kind === '') { + kind = 'Function'; + } + if (!name || name === '') { + name = ''; + } + if (!args) { + args = []; + } + return { kind, name, args }; +} + +function formatMessages(logInfo: LogInfo, traced: TraceInfo, call?: CallInfo): string { + call = normalizeCall(call!); + const messages = [logInfo.message]; + messages.push( + `${call.kind} name = ${call.name}`.trim(), + `completed in ${traced.elapsed}ms`, + `has a ${traced.returnValue ? 'truthy' : 'falsy'} return value`, + ); + if ((logInfo.opts & TraceOptions.Arguments) === TraceOptions.Arguments) { + messages.push(argsToLogString(call.args)); + } + if ((logInfo.opts & TraceOptions.ReturnValue) === TraceOptions.ReturnValue) { + messages.push(returnValueToLogString(traced.returnValue)); + } + return messages.join(', '); +} + +function logResult(logInfo: LogInfo, traced: TraceInfo, call?: CallInfo) { + const formatted = formatMessages(logInfo, traced, call); + if (traced.err === undefined) { + // The call did not fail. + if (!logInfo.level || logInfo.level > LogLevel.Error) { + logTo(LogLevel.Info, [formatted]); + } + } else { + logTo(LogLevel.Error, [formatted, traced.err]); + sendTelemetryEvent(('ERROR' as unknown) as EventName, undefined, undefined, traced.err); + } +} + +export function logTo(logLevel: LogLevel, ...args: Arguments): void { + switch (logLevel) { + case LogLevel.Error: + traceError(...args); + break; + case LogLevel.Warning: + traceWarn(...args); + break; + case LogLevel.Info: + traceInfo(...args); + break; + case LogLevel.Debug: + traceVerbose(...args); + break; + default: + break; + } +} diff --git a/src/client/logging/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, +) => TypedPropertyDescriptor; + +// The information we want to log. +export enum TraceOptions { + None = 0, + Arguments = 1, + ReturnValue = 2, +} diff --git a/src/client/logging/util.ts b/src/client/logging/util.ts 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 ``; + } + 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 { + // 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 137ee7973dee..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 { - 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 aa9099b6a401..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 { EventName } from '../telemetry/constants'; -import { CompletionSource } from './completionSource'; -import { ItemInfoSource } from './itemInfoSource'; - -export class PythonCompletionItemProvider implements vscode.CompletionItemProvider { - private completionSource: CompletionSource; - private configService: IConfigurationService; - - constructor(jediFactory: JediFactory, serviceContainer: IServiceContainer) { - this.completionSource = new CompletionSource(jediFactory, serviceContainer, new ItemInfoSource(jediFactory)); - this.configService = serviceContainer.get(IConfigurationService); - } - - @captureTelemetry(EventName.COMPLETION) - public async provideCompletionItems(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): - Promise { - const items = await this.completionSource.getVsCodeCompletionItems(document, position, token); - if (this.configService.isTestExecution()) { - for (let i = 0; i < Math.min(3, items.length); i += 1) { - items[i] = await this.resolveCompletionItem(items[i], token); - } - } - return items; - } - - public async resolveCompletionItem(item: vscode.CompletionItem, token: vscode.CancellationToken): Promise { - if (!item.documentation) { - const itemInfos = await this.completionSource.getDocumentation(item, token); - if (itemInfos && itemInfos.length > 0) { - item.documentation = itemInfos[0].tooltip; - } - } - return item; - } -} diff --git a/src/client/providers/completionSource.ts b/src/client/providers/completionSource.ts deleted file mode 100644 index 27cc4542fa2b..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 { - const result = await this.getCompletionResult(document, position, token); - if (result === undefined) { - return Promise.resolve([]); - } - return this.toVsCodeCompletions(new DocumentPosition(document, position), result, document.uri); - } - - public async getDocumentation(completionItem: vscode.CompletionItem, token: vscode.CancellationToken): Promise { - const documentPosition = DocumentPosition.fromObject(completionItem); - if (documentPosition === undefined) { - return; - } - - // Supply hover source with simulated document text where item in question was 'already typed'. - const document = documentPosition.document; - const position = documentPosition.position; - const wordRange = document.getWordRangeAtPosition(position); - - const leadingRange = wordRange !== undefined - ? new vscode.Range(new vscode.Position(0, 0), wordRange.start) - : new vscode.Range(new vscode.Position(0, 0), position); - - const itemString = completionItem.label; - const sourceText = `${document.getText(leadingRange)}${itemString}`; - const range = new vscode.Range(leadingRange.end, leadingRange.end.translate(0, itemString.length)); - - return this.itemInfoSource.getItemInfoFromText(document.uri, document.fileName, range, sourceText, token); - } - - private async getCompletionResult(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) - : Promise { - if (position.character <= 0 || - isPositionInsideStringOrComment(document, position)) { - return undefined; - } - - const type = proxy.CommandType.Completions; - const columnIndex = position.character; - - const source = document.getText(); - const cmd: proxy.ICommand = { - command: type, - fileName: document.fileName, - columnIndex: columnIndex, - lineIndex: position.line, - source: source - }; - - return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); - } - - private toVsCodeCompletions(documentPosition: DocumentPosition, data: proxy.ICompletionResult, resource: vscode.Uri): vscode.CompletionItem[] { - return data && data.items.length > 0 ? data.items.map(item => this.toVsCodeCompletion(documentPosition, item, resource)) : []; - } - - private toVsCodeCompletion(documentPosition: DocumentPosition, item: proxy.IAutoCompleteItem, resource: vscode.Uri): vscode.CompletionItem { - const completionItem = new vscode.CompletionItem(item.text); - completionItem.kind = item.type; - const configurationService = this.serviceContainer.get(IConfigurationService); - const pythonSettings = configurationService.getSettings(resource); - if (pythonSettings.autoComplete.addBrackets === true && - (item.kind === vscode.SymbolKind.Function || item.kind === vscode.SymbolKind.Method)) { - completionItem.insertText = new vscode.SnippetString(item.text).appendText('(').appendTabstop().appendText(')'); - } - // Ensure the built in members are at the bottom. - completionItem.sortText = (completionItem.label.startsWith('__') ? 'z' : (completionItem.label.startsWith('_') ? 'y' : '__')) + completionItem.label; - documentPosition.attachTo(completionItem); - return completionItem; - } -} diff --git a/src/client/providers/definitionProvider.ts b/src/client/providers/definitionProvider.ts deleted file mode 100644 index 89a7b3746390..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 { EventName } from '../telemetry/constants'; -import * as proxy from './jediProxy'; - -export class PythonDefinitionProvider implements vscode.DefinitionProvider { - public constructor(private jediFactory: JediFactory) { } - private static parseData(data: proxy.IDefinitionResult, possibleWord: string): vscode.Definition | undefined { - if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { - const definitions = data.definitions.filter(d => d.text === possibleWord); - const definition = definitions.length > 0 ? definitions[0] : data.definitions[data.definitions.length - 1]; - const definitionResource = vscode.Uri.file(definition.fileName); - const range = new vscode.Range( - definition.range.startLine, definition.range.startColumn, - definition.range.endLine, definition.range.endColumn); - return new vscode.Location(definitionResource, range); - } - } - @captureTelemetry(EventName.DEFINITION) - public async provideDefinition(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise { - const filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return; - } - if (position.character <= 0) { - return; - } - - const range = document.getWordRangeAtPosition(position); - if (!range) { - return; - } - const columnIndex = range.isEmpty ? position.character : range.end.character; - const cmd: proxy.ICommand = { - command: proxy.CommandType.Definitions, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line - }; - if (document.isDirty) { - cmd.source = document.getText(); - } - const possibleWord = document.getText(range); - const data = await this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); - return data ? PythonDefinitionProvider.parseData(data, possibleWord) : undefined; - } -} diff --git a/src/client/providers/docStringFoldingProvider.ts b/src/client/providers/docStringFoldingProvider.ts deleted file mode 100644 index 4e3e3f783a4a..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 { - 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 2e09f9080318..000000000000 --- a/src/client/providers/formatProvider.ts +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as vscode from 'vscode'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; -import { IConfigurationService } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { AutoPep8Formatter } from './../formatters/autoPep8Formatter'; -import { BaseFormatter } from './../formatters/baseFormatter'; -import { BlackFormatter } from './../formatters/blackFormatter'; -import { DummyFormatter } from './../formatters/dummyFormatter'; -import { YapfFormatter } from './../formatters/yapfFormatter'; - -export class PythonFormattingEditProvider implements vscode.DocumentFormattingEditProvider, vscode.DocumentRangeFormattingEditProvider, vscode.Disposable { - private readonly config: IConfigurationService; - private readonly workspace: IWorkspaceService; - private readonly documentManager: IDocumentManager; - private readonly commands: ICommandManager; - private formatters = new Map(); - private disposables: vscode.Disposable[] = []; - - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - private documentVersionBeforeFormatting = -1; - private formatterMadeChanges = false; - private saving = false; - - public constructor(_context: vscode.ExtensionContext, serviceContainer: IServiceContainer) { - const yapfFormatter = new YapfFormatter(serviceContainer); - const autoPep8 = new AutoPep8Formatter(serviceContainer); - const black = new BlackFormatter(serviceContainer); - const dummy = new DummyFormatter(serviceContainer); - this.formatters.set(yapfFormatter.Id, yapfFormatter); - this.formatters.set(black.Id, black); - this.formatters.set(autoPep8.Id, autoPep8); - this.formatters.set(dummy.Id, dummy); - - this.commands = serviceContainer.get(ICommandManager); - this.workspace = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); - this.config = serviceContainer.get(IConfigurationService); - 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 { - return this.provideDocumentRangeFormattingEdits(document, undefined, options, token); - } - - public async provideDocumentRangeFormattingEdits(document: vscode.TextDocument, range: vscode.Range | undefined, options: vscode.FormattingOptions, token: vscode.CancellationToken): Promise { - // Workaround for https://github.com/Microsoft/vscode/issues/41194 - // VSC rejects 'format on save' promise in 750 ms. Python formatting may take quite a bit longer. - // Workaround is to resolve promise to nothing here, then execute format document and force new save. - // However, we need to know if this is 'format document' or formatting on save. - - if (this.saving) { - // We are saving after formatting (see onSaveDocument below) - // so we do not want to format again. - return []; - } - - // Remember content before formatting so we can detect if - // formatting edits have been really applied - const editorConfig = this.workspace.getConfiguration('editor', document.uri); - if (editorConfig.get('formatOnSave') === true) { - this.documentVersionBeforeFormatting = document.version; - } - - const settings = this.config.getSettings(document.uri); - const formatter = this.formatters.get(settings.formatting.provider)!; - const edits = await formatter.formatDocument(document, options, token, range); - - this.formatterMadeChanges = edits.length > 0; - return edits; - } - - private async onSaveDocument(document: vscode.TextDocument): Promise { - // Promise was rejected = formatting took too long. - // Don't format inside the event handler, do it on timeout - setTimeout(() => { - try { - if (this.formatterMadeChanges - && !document.isDirty - && document.version === this.documentVersionBeforeFormatting) { - // Formatter changes were not actually applied due to the timeout on save. - // Force formatting now and then save the document. - this.commands.executeCommand('editor.action.formatDocument').then(async () => { - this.saving = true; - await document.save(); - this.saving = false; - }); - } - } finally { - this.documentVersionBeforeFormatting = -1; - this.saving = false; - this.formatterMadeChanges = false; - } - }, 50); - } -} diff --git a/src/client/providers/hoverProvider.ts b/src/client/providers/hoverProvider.ts deleted file mode 100644 index a7e07ea6337e..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 { EventName } from '../telemetry/constants'; -import { ItemInfoSource } from './itemInfoSource'; - -export class PythonHoverProvider implements vscode.HoverProvider { - private itemInfoSource: ItemInfoSource; - - constructor(jediFactory: JediFactory) { - this.itemInfoSource = new ItemInfoSource(jediFactory); - } - - @captureTelemetry(EventName.HOVER_DEFINITION) - public async provideHover(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) - : Promise { - const itemInfos = await this.itemInfoSource.getItemInfoFromDocument(document, position, token); - if (itemInfos) { - return new vscode.Hover(itemInfos.map(item => item.tooltip)); - } - } -} diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts deleted file mode 100644 index 3b6316350396..000000000000 --- a/src/client/providers/importSortProvider.ts +++ /dev/null @@ -1,122 +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 { EventName } 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); - this.documentManager = serviceContainer.get(IDocumentManager); - this.configurationService = serviceContainer.get(IConfigurationService); - this.pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); - this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); - this.editorUtils = serviceContainer.get(IEditorUtils); - } - @captureTelemetry(EventName.FORMAT_SORT_IMPORTS) - public async provideDocumentSortImportsEdits(uri: Uri, token?: CancellationToken): Promise { - 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); - 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); - const disposable = cmdManager.registerCommand(Commands.Sort_Imports, this.sortImports, this); - this.serviceContainer.get(IDisposableRegistry).push(disposable); - } - public async sortImports(uri?: Uri): Promise { - if (!uri) { - const activeEditor = this.documentManager.activeTextEditor; - if (!activeEditor || activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.shell.showErrorMessage('Please open a Python file to sort the imports.').then(noop, noop); - return; - } - uri = activeEditor.document.uri; - } - - const document = await this.documentManager.openTextDocument(uri); - if (document.lineCount <= 1) { - return; - } - - // Hack, if the document doesn't contain an empty line at the end, then add it - // Else the library strips off the last line - const lastLine = document.lineAt(document.lineCount - 1); - if (lastLine.text.trim().length > 0) { - const edit = new WorkspaceEdit(); - edit.insert(uri, lastLine.range.end, EOL); - await this.documentManager.applyEdit(edit); - } - - try { - const changes = await this.provideDocumentSortImportsEdits(uri); - if (!changes || changes.entries().length === 0) { - return; - } - await this.documentManager.applyEdit(changes); - } catch (error) { - const message = typeof error === 'string' ? error : (error.message ? error.message : error); - const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - outputChannel.appendLine(error); - const logger = this.serviceContainer.get(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 ef1d2497051f..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; - getItemInfoFromDocument(document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken): Promise; -} - -export class ItemInfoSource implements IItemInfoSource { - private textConverter = new RestTextConverter(); - constructor(private jediFactory: JediFactory) { } - - public async getItemInfoFromText(documentUri: vscode.Uri, fileName: string, range: vscode.Range, sourceText: string, token: vscode.CancellationToken) - : Promise { - const result = await this.getHoverResultFromTextRange(documentUri, fileName, range, sourceText, token); - if (!result || !result.items.length) { - return; - } - return this.getItemInfoFromHoverResult(result, ''); - } - - public async getItemInfoFromDocument(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) - : Promise { - const range = document.getWordRangeAtPosition(position); - if (!range || range.isEmpty) { - return; - } - const result = await this.getHoverResultFromDocument(document, position, token); - if (!result || !result.items.length) { - return; - } - const word = document.getText(range); - return this.getItemInfoFromHoverResult(result, word); - } - - private async getHoverResultFromDocument(document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken) - : Promise { - if (position.character <= 0 || document.lineAt(position.line).text.match(/^\s*\/\//)) { - return; - } - const range = document.getWordRangeAtPosition(position); - if (!range || range.isEmpty) { - return; - } - return this.getHoverResultFromDocumentRange(document, range, token); - } - - private async getHoverResultFromDocumentRange(document: vscode.TextDocument, range: vscode.Range, token: vscode.CancellationToken) - : Promise { - const cmd: proxy.ICommand = { - command: proxy.CommandType.Hover, - fileName: document.fileName, - columnIndex: range.end.character, - lineIndex: range.end.line - }; - if (document.isDirty) { - cmd.source = document.getText(); - } - return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); - } - - private async getHoverResultFromTextRange(documentUri: vscode.Uri, fileName: string, range: vscode.Range, sourceText: string, token: vscode.CancellationToken) - : Promise { - const cmd: proxy.ICommand = { - command: proxy.CommandType.Hover, - fileName: fileName, - columnIndex: range.end.character, - lineIndex: range.end.line, - source: sourceText - }; - return this.jediFactory.getJediProxyHandler(documentUri).sendCommand(cmd, token); - } - - private getItemInfoFromHoverResult(data: proxy.IHoverResult, currentWord: string): LanguageItemInfo[] { - const infos: LanguageItemInfo[] = []; - - data.items.forEach(item => { - const signature = this.getSignature(item, currentWord); - let tooltip = new vscode.MarkdownString(); - if (item.docstring) { - let lines = item.docstring.split(/\r?\n/); - - // If the docstring starts with the signature, then remove those lines from the docstring. - if (lines.length > 0 && item.signature.indexOf(lines[0]) === 0) { - lines.shift(); - const endIndex = lines.findIndex(line => item.signature.endsWith(line)); - if (endIndex >= 0) { - lines = lines.filter((_line, index) => index > endIndex); - } - } - if (lines.length > 0 && currentWord.length > 0 && item.signature.startsWith(currentWord) && lines[0].startsWith(currentWord) && lines[0].endsWith(')')) { - lines.shift(); - } - - if (signature.length > 0) { - tooltip = tooltip.appendMarkdown(['```python', signature, '```', ''].join(EOL)); - } - - const description = this.textConverter.toMarkdown(lines.join(EOL)); - tooltip = tooltip.appendMarkdown(description); - - infos.push(new LanguageItemInfo(tooltip, item.description, new vscode.MarkdownString(signature))); - return; - } - - if (item.description) { - if (signature.length > 0) { - tooltip.appendMarkdown(['```python', signature, '```', ''].join(EOL)); - } - const description = this.textConverter.toMarkdown(item.description); - tooltip.appendMarkdown(description); - infos.push(new LanguageItemInfo(tooltip, item.description, new vscode.MarkdownString(signature))); - return; - } - - if (item.text) { // Most probably variable type - const code = currentWord && currentWord.length > 0 - ? `${currentWord}: ${item.text}` - : item.text; - tooltip.appendMarkdown(['```python', code, '```', ''].join(EOL)); - infos.push(new LanguageItemInfo(tooltip, '', new vscode.MarkdownString())); - } - }); - return infos; - } - - private getSignature(item: proxy.IHoverItem, currentWord: string): string { - let { signature } = item; - switch (item.kind) { - case vscode.SymbolKind.Constructor: - case vscode.SymbolKind.Function: - case vscode.SymbolKind.Method: { - signature = `def ${signature}`; - break; - } - case vscode.SymbolKind.Class: { - signature = `class ${signature}`; - break; - } - case vscode.SymbolKind.Module: { - if (signature.length > 0) { - signature = `module ${signature}`; - } - break; - } - default: { - signature = typeof item.text === 'string' && item.text.length > 0 ? item.text : currentWord; - } - } - return signature; - } -} diff --git a/src/client/providers/jediProxy.ts b/src/client/providers/jediProxy.ts deleted file mode 100644 index 336ab6ff34c2..000000000000 --- a/src/client/providers/jediProxy.ts +++ /dev/null @@ -1,892 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:no-var-requires no-require-imports no-any -import { ChildProcess } from 'child_process'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -// @ts-ignore -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 { 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 { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { Logger } from './../common/logger'; - -const pythonVSCodeTypeMappings = new Map(); -pythonVSCodeTypeMappings.set('none', CompletionItemKind.Value); -pythonVSCodeTypeMappings.set('type', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('tuple', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dict', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dictionary', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('function', CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('lambda', CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('generator', CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('class', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('instance', CompletionItemKind.Reference); -pythonVSCodeTypeMappings.set('method', CompletionItemKind.Method); -pythonVSCodeTypeMappings.set('builtin', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('builtinfunction', CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('module', CompletionItemKind.Module); -pythonVSCodeTypeMappings.set('file', CompletionItemKind.File); -pythonVSCodeTypeMappings.set('xrange', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('slice', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('traceback', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('frame', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('buffer', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('dictproxy', CompletionItemKind.Class); -pythonVSCodeTypeMappings.set('funcdef', CompletionItemKind.Function); -pythonVSCodeTypeMappings.set('property', CompletionItemKind.Property); -pythonVSCodeTypeMappings.set('import', CompletionItemKind.Module); -pythonVSCodeTypeMappings.set('keyword', CompletionItemKind.Keyword); -pythonVSCodeTypeMappings.set('constant', CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('variable', CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('value', CompletionItemKind.Value); -pythonVSCodeTypeMappings.set('param', CompletionItemKind.Variable); -pythonVSCodeTypeMappings.set('statement', CompletionItemKind.Keyword); - -const pythonVSCodeSymbolMappings = new Map(); -pythonVSCodeSymbolMappings.set('none', SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('type', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('tuple', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('dict', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('dictionary', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('function', SymbolKind.Function); -pythonVSCodeSymbolMappings.set('lambda', SymbolKind.Function); -pythonVSCodeSymbolMappings.set('generator', SymbolKind.Function); -pythonVSCodeSymbolMappings.set('class', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('instance', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('method', SymbolKind.Method); -pythonVSCodeSymbolMappings.set('builtin', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('builtinfunction', SymbolKind.Function); -pythonVSCodeSymbolMappings.set('module', SymbolKind.Module); -pythonVSCodeSymbolMappings.set('file', SymbolKind.File); -pythonVSCodeSymbolMappings.set('xrange', SymbolKind.Array); -pythonVSCodeSymbolMappings.set('slice', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('traceback', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('frame', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('buffer', SymbolKind.Array); -pythonVSCodeSymbolMappings.set('dictproxy', SymbolKind.Class); -pythonVSCodeSymbolMappings.set('funcdef', SymbolKind.Function); -pythonVSCodeSymbolMappings.set('property', SymbolKind.Property); -pythonVSCodeSymbolMappings.set('import', SymbolKind.Module); -pythonVSCodeSymbolMappings.set('keyword', SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('constant', SymbolKind.Constant); -pythonVSCodeSymbolMappings.set('variable', SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('value', SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('param', SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('statement', SymbolKind.Variable); -pythonVSCodeSymbolMappings.set('boolean', SymbolKind.Boolean); -pythonVSCodeSymbolMappings.set('int', SymbolKind.Number); -pythonVSCodeSymbolMappings.set('longlean', SymbolKind.Number); -pythonVSCodeSymbolMappings.set('float', SymbolKind.Number); -pythonVSCodeSymbolMappings.set('complex', SymbolKind.Number); -pythonVSCodeSymbolMappings.set('string', SymbolKind.String); -pythonVSCodeSymbolMappings.set('unicode', SymbolKind.String); -pythonVSCodeSymbolMappings.set('list', SymbolKind.Array); - -function getMappedVSCodeType(pythonType: string): CompletionItemKind { - if (pythonVSCodeTypeMappings.has(pythonType)) { - const value = pythonVSCodeTypeMappings.get(pythonType); - if (value) { - return value; - } - } - return CompletionItemKind.Keyword; -} - -function getMappedVSCodeSymbol(pythonType: string): SymbolKind { - if (pythonVSCodeSymbolMappings.has(pythonType)) { - const value = pythonVSCodeSymbolMappings.get(pythonType); - if (value) { - return value; - } - } - return SymbolKind.Variable; -} - -export enum CommandType { - Arguments, - Completions, - Hover, - Usages, - Definitions, - Symbols -} - -const commandNames = new Map(); -commandNames.set(CommandType.Arguments, 'arguments'); -commandNames.set(CommandType.Completions, 'completions'); -commandNames.set(CommandType.Definitions, 'definitions'); -commandNames.set(CommandType.Hover, 'tooltip'); -commandNames.set(CommandType.Usages, 'usages'); -commandNames.set(CommandType.Symbols, 'names'); - -export class JediProxy implements Disposable { - private proc?: ChildProcess; - private pythonSettings: IPythonSettings; - private cmdId: number = 0; - private lastKnownPythonInterpreter: string; - private previousData = ''; - private commands = new Map>(); - private commandQueue: number[] = []; - private spawnRetryAttempts = 0; - private additionalAutoCompletePaths: string[] = []; - private workspacePath: string; - private languageServerStarted!: Deferred; - private initialized: Deferred; - private environmentVariablesProvider!: IEnvironmentVariablesProvider; - private 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[] = []; - private timer?: NodeJS.Timer | number; - - public constructor(private extensionRootDir: string, workspacePath: string, private serviceContainer: IServiceContainer) { - this.workspacePath = workspacePath; - const configurationService = serviceContainer.get(IConfigurationService); - this.pythonSettings = configurationService.getSettings(Uri.file(workspacePath)); - this.lastKnownPythonInterpreter = this.pythonSettings.pythonPath; - this.logger = serviceContainer.get(ILogger); - const interpreterService = serviceContainer.get(IInterpreterService); - const disposable = interpreterService.onDidChangeInterpreter(this.onDidChangeInterpreter.bind(this)); - this.disposables.push(disposable); - this.initialized = createDeferred(); - this.startLanguageServer() - .then(() => this.initialized.resolve()) - .ignoreErrors(); - - this.proposeNewLanguageServerPopup = serviceContainer.get(IPythonExtensionBanner, BANNER_NAME_PROPOSE_LS); - - this.checkJediMemoryFootprint().ignoreErrors(); - } - - private static getProperty(o: object, name: string): T { - return (o as any)[name]; - } - - public dispose() { - while (this.disposables.length > 0) { - const disposable = this.disposables.pop(); - if (disposable) { - disposable.dispose(); - } - } - if (this.timer) { - clearTimeout(this.timer as any); - } - this.killProcess(); - } - - public getNextCommandId(): number { - const result = this.cmdId; - this.cmdId += 1; - return result; - } - - public async sendCommand(cmd: ICommand): Promise { - await this.initialized.promise; - await this.languageServerStarted.promise; - if (!this.proc) { - return Promise.reject(new Error('Python proc not initialized')); - } - - const executionCmd = >cmd; - const payload = this.createPayload(executionCmd); - executionCmd.deferred = createDeferred(); - try { - this.proc.stdin.write(`${JSON.stringify(payload)}\n`); - this.commands.set(executionCmd.id, executionCmd); - this.commandQueue.push(executionCmd.id); - } catch (ex) { - 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 { - 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(); - if (this.timer) { - clearTimeout(this.timer as any); - } - this.timer = setTimeout(() => this.checkJediMemoryFootprint(), 15 * 1000); - } - private async checkJediMemoryFootprintImpl(): Promise { - if (!this.proc || this.proc.killed) { - return; - } - if (!this.shouldCheckJediMemoryFootprint()) { - return; - } - this.lastCmdIdProcessedForPidUsage = this.lastCmdIdProcessed; - - // Do not run pidusage over and over, wait for it to finish. - const deferred = createDeferred(); - (pidusage as any).stat(this.proc.pid, async (err: any, result: any) => { - if (err) { - this.pidUsageFailures.counter += 1; - // If this function fails 2 times in the last 60 seconds, lets not try ever again. - if (this.pidUsageFailures.timer.elapsedTime > 60 * 1000) { - this.ignoreJediMemoryFootprint = this.pidUsageFailures.counter > 2; - this.pidUsageFailures.counter = 0; - this.pidUsageFailures.timer.reset(); - } - console.error('Python Extension: (pidusage)', err); - } else { - const limit = Math.min(Math.max(this.pythonSettings.jediMemoryLimit, 1024), 8192); - let restartJedi = false; - if (result && result.memory) { - restartJedi = result.memory > limit * 1024 * 1024; - const props = { - memory: result.memory, - limit: limit * 1024 * 1024, - isUserDefinedLimit: limit !== 1024, - restart: restartJedi - }; - sendTelemetryEvent(EventName.JEDI_MEMORY, undefined, props); - } - if (restartJedi) { - 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 { - const newAutoComletePaths = await this.buildAutoCompletePaths(); - this.additionalAutoCompletePaths = newAutoComletePaths; - if (!isTestExecution()) { - await this.proposeNewLanguageServerPopup.showBanner(); - } - return this.restartLanguageServer(); - } - private restartLanguageServer(): Promise { - this.killProcess(); - this.clearPendingRequests(); - return this.initialize(); - } - - private clearPendingRequests() { - this.commandQueue = []; - this.commands.forEach(item => { - if (item.deferred !== undefined) { - item.deferred.resolve(); - } - }); - this.commands.clear(); - } - - private killProcess() { - try { - if (this.proc) { - this.proc.kill(); - } - // tslint:disable-next-line:no-empty - } catch (ex) {} - this.proc = undefined; - } - - private handleError(source: string, errorMessage: string) { - 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(); - const pythonProcess = await this.serviceContainer.get(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(response, 'id'); - if (!this.commands.has(responseId)) { - return; - } - const cmd = this.commands.get(responseId); - if (!cmd) { - return; - } - this.lastCmdIdProcessed = cmd.id; - if (JediProxy.getProperty(response, 'arguments')) { - this.commandQueue.splice(this.commandQueue.indexOf(cmd.id), 1); - return; - } - - this.commands.delete(responseId); - const index = this.commandQueue.indexOf(cmd.id); - if (index) { - this.commandQueue.splice(index, 1); - } - - // Check if this command has expired. - if (cmd.token.isCancellationRequested) { - this.safeResolve(cmd, undefined); - return; - } - - const handler = this.getCommandHandler(cmd.command); - if (handler) { - handler.call(this, cmd, response); - } - // Check if too many pending requests. - this.checkQueueLength(); - }); - } - }, - error => this.handleError('subscription.error', `${error}`) - ); - } - private getCommandHandler(command: CommandType): undefined | ((command: IExecutionCommand, response: object) => void) { - switch (command) { - case CommandType.Completions: - return this.onCompletion; - case CommandType.Definitions: - return this.onDefinition; - case CommandType.Hover: - return this.onHover; - case CommandType.Symbols: - return this.onSymbols; - case CommandType.Usages: - return this.onUsages; - case CommandType.Arguments: - return this.onArguments; - default: - return; - } - } - private onCompletion(command: IExecutionCommand, response: object): void { - let results = JediProxy.getProperty(response, 'results'); - results = Array.isArray(results) ? results : []; - results.forEach(item => { - // tslint:disable-next-line:no-any - const originalType = (item.type); - item.type = getMappedVSCodeType(originalType); - item.kind = getMappedVSCodeSymbol(originalType); - item.rawType = getMappedVSCodeType(originalType); - }); - const completionResult: ICompletionResult = { - items: results, - requestId: command.id - }; - this.safeResolve(command, completionResult); - } - - private onDefinition(command: IExecutionCommand, response: object): void { - // tslint:disable-next-line:no-any - const defs = JediProxy.getProperty(response, 'results'); - const defResult: IDefinitionResult = { - requestId: command.id, - definitions: [] - }; - if (defs.length > 0) { - defResult.definitions = defs.map(def => { - const originalType = def.type as string; - return { - fileName: def.fileName, - text: def.text, - rawType: originalType, - type: getMappedVSCodeType(originalType), - kind: getMappedVSCodeSymbol(originalType), - container: def.container, - range: { - startLine: def.range.start_line, - startColumn: def.range.start_column, - endLine: def.range.end_line, - endColumn: def.range.end_column - } - }; - }); - } - this.safeResolve(command, defResult); - } - - private onHover(command: IExecutionCommand, response: object): void { - // tslint:disable-next-line:no-any - const defs = JediProxy.getProperty(response, 'results'); - const defResult: IHoverResult = { - requestId: command.id, - items: defs.map(def => { - return { - kind: getMappedVSCodeSymbol(def.type), - description: def.description, - signature: def.signature, - docstring: def.docstring, - text: def.text - }; - }) - }; - this.safeResolve(command, defResult); - } - - private onSymbols(command: IExecutionCommand, response: object): void { - // tslint:disable-next-line:no-any - let defs = JediProxy.getProperty(response, 'results'); - defs = Array.isArray(defs) ? defs : []; - const defResults: ISymbolResult = { - requestId: command.id, - definitions: [] - }; - defResults.definitions = defs.map(def => { - const originalType = def.type as string; - return { - fileName: def.fileName, - text: def.text, - rawType: originalType, - type: getMappedVSCodeType(originalType), - kind: getMappedVSCodeSymbol(originalType), - container: def.container, - range: { - startLine: def.range.start_line, - startColumn: def.range.start_column, - endLine: def.range.end_line, - endColumn: def.range.end_column - } - }; - }); - this.safeResolve(command, defResults); - } - - private onUsages(command: IExecutionCommand, response: object): void { - // tslint:disable-next-line:no-any - let defs = JediProxy.getProperty(response, 'results'); - defs = Array.isArray(defs) ? defs : []; - const refResult: IReferenceResult = { - requestId: command.id, - references: defs.map(item => { - return { - columnIndex: item.column, - fileName: item.fileName, - lineIndex: item.line - 1, - moduleName: item.moduleName, - name: item.name - }; - }) - }; - this.safeResolve(command, refResult); - } - - private onArguments(command: IExecutionCommand, response: object): void { - // tslint:disable-next-line:no-any - const defs = JediProxy.getProperty(response, 'results'); - // tslint:disable-next-line:no-object-literal-type-assertion - this.safeResolve(command, { - requestId: command.id, - definitions: defs - }); - } - - private checkQueueLength(): void { - if (this.commandQueue.length > 10) { - const items = this.commandQueue.splice(0, this.commandQueue.length - 10); - items.forEach(id => { - if (this.commands.has(id)) { - const cmd1 = this.commands.get(id); - try { - this.safeResolve(cmd1, undefined); - // tslint:disable-next-line:no-empty - } catch (ex) { - } finally { - this.commands.delete(id); - } - } - }); - } - } - - // tslint:disable-next-line:no-any - private createPayload(cmd: IExecutionCommand): any { - const payload = { - id: cmd.id, - prefix: '', - lookup: commandNames.get(cmd.command), - path: cmd.fileName, - source: cmd.source, - line: cmd.lineIndex, - column: cmd.columnIndex, - config: this.getConfig() - }; - - if (cmd.command === CommandType.Symbols) { - delete payload.column; - delete payload.line; - } - - return payload; - } - - private async getPathFromPythonCommand(args: string[]): Promise { - try { - const pythonProcess = await this.serviceContainer.get(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 { - 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(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); - this.environmentVariablesProvider.onDidEnvironmentVariablesChange(this.environmentVariablesChangeHandler.bind(this)); - } - return this.environmentVariablesProvider; - } - private getConfig() { - // Add support for paths relative to workspace. - const extraPaths = this.pythonSettings.autoComplete - ? this.pythonSettings.autoComplete.extraPaths.map(extraPath => { - if (path.isAbsolute(extraPath)) { - return extraPath; - } - if (typeof this.workspacePath !== 'string') { - return ''; - } - return path.join(this.workspacePath, extraPath); - }) - : []; - - // Always add workspace path into extra paths. - if (typeof this.workspacePath === 'string') { - extraPaths.unshift(this.workspacePath); - } - - const distinctExtraPaths = extraPaths - .concat(this.additionalAutoCompletePaths) - .filter(value => value.length > 0) - .filter((value, index, self) => self.indexOf(value) === index); - - return { - extraPaths: distinctExtraPaths, - useSnippets: false, - caseInsensitiveCompletion: true, - showDescriptions: true, - fuzzyMatcher: true - }; - } - - private safeResolve(command: IExecutionCommand | undefined | null, result: ICommandResult | PromiseLike | undefined): void { - if (command && command.deferred) { - command.deferred.resolve(result); - } - } -} - -// tslint:disable-next-line:no-unused-variable -export interface ICommand { - telemetryEvent?: string; - command: CommandType; - source?: string; - fileName: string; - lineIndex: number; - columnIndex: number; -} - -interface IExecutionCommand extends ICommand { - id: number; - deferred?: Deferred; - token: CancellationToken; - delay?: number; -} - -export interface ICommandError { - message: string; -} - -export interface ICommandResult { - requestId: number; -} -export interface ICompletionResult extends ICommandResult { - items: IAutoCompleteItem[]; -} -export interface IHoverResult extends ICommandResult { - items: IHoverItem[]; -} -export interface IDefinitionResult extends ICommandResult { - definitions: IDefinition[]; -} -export interface IReferenceResult extends ICommandResult { - references: IReference[]; -} -export interface ISymbolResult extends ICommandResult { - definitions: IDefinition[]; -} -export interface IArgumentsResult extends ICommandResult { - definitions: ISignature[]; -} - -export interface ISignature { - name: string; - docstring: string; - description: string; - paramindex: number; - params: IArgument[]; -} -export interface IArgument { - name: string; - value: string; - docstring: string; - description: string; -} - -export interface IReference { - name: string; - fileName: string; - columnIndex: number; - lineIndex: number; - moduleName: string; -} - -export interface IAutoCompleteItem { - type: CompletionItemKind; - rawType: CompletionItemKind; - kind: SymbolKind; - text: string; - description: string; - raw_docstring: string; - rightLabel: string; -} -export interface IDefinitionRange { - startLine: number; - startColumn: number; - endLine: number; - endColumn: number; -} -export interface IDefinition { - rawType: string; - type: CompletionItemKind; - kind: SymbolKind; - text: string; - fileName: string; - container: string; - range: IDefinitionRange; -} - -export interface IHoverItem { - kind: SymbolKind; - text: string; - description: string; - docstring: string; - signature: string; -} - -export class JediProxyHandler implements Disposable { - private commandCancellationTokenSources: Map; - - public get JediProxy(): JediProxy { - return this.jediProxy; - } - - public constructor(private jediProxy: JediProxy) { - this.commandCancellationTokenSources = new Map(); - } - - public dispose() { - if (this.jediProxy) { - this.jediProxy.dispose(); - } - } - - public sendCommand(cmd: ICommand, _token?: CancellationToken): Promise { - const executionCmd = >cmd; - executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - - if (this.commandCancellationTokenSources.has(cmd.command)) { - const ct = this.commandCancellationTokenSources.get(cmd.command); - if (ct) { - ct.cancel(); - } - } - - const cancellation = new CancellationTokenSource(); - this.commandCancellationTokenSources.set(cmd.command, cancellation); - executionCmd.token = cancellation.token; - - return this.jediProxy.sendCommand(executionCmd).catch(reason => { - console.error(reason); - return undefined; - }); - } - - public sendCommandNonCancellableCommand(cmd: ICommand, token?: CancellationToken): Promise { - const executionCmd = >cmd; - executionCmd.id = executionCmd.id || this.jediProxy.getNextCommandId(); - if (token) { - executionCmd.token = token; - } - - return this.jediProxy.sendCommand(executionCmd).catch(reason => { - console.error(reason); - return undefined; - }); - } -} diff --git a/src/client/providers/linterProvider.ts b/src/client/providers/linterProvider.ts deleted file mode 100644 index aba767f54ee0..000000000000 --- a/src/client/providers/linterProvider.ts +++ /dev/null @@ -1,107 +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); - this.engine = serviceContainer.get(ILintingEngine); - this.linterManager = serviceContainer.get(ILinterManager); - this.interpreterService = serviceContainer.get(IInterpreterService); - this.documents = serviceContainer.get(IDocumentManager); - this.configuration = serviceContainer.get(IConfigurationService); - this.workspaceService = serviceContainer.get(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()) { - const timer = setTimeout(() => this.engine.lintOpenPythonFiles().ignoreErrors(), 1200); - this.disposables.push({ dispose: () => clearTimeout(timer) }); - } - } - - public dispose() { - this.disposables.forEach(d => d.dispose()); - } - - private isDocumentOpen(uri: Uri): boolean { - return this.documents.textDocuments.some(document => this.fs.arePathsSame(document.uri.fsPath, uri.fsPath)); - } - - private lintSettingsChangedHandler(e: ConfigurationChangeEvent) { - // Look for python files that belong to the specified workspace folder. - workspace.textDocuments.forEach(document => { - if (e.affectsConfiguration('python.linting', document.uri)) { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - }); - } - - private onDocumentOpened(document: TextDocument): void { - this.engine.lintDocument(document, 'auto').ignoreErrors(); - } - - private onDocumentSaved(document: TextDocument): void { - const settings = this.configuration.getSettings(document.uri); - if (document.languageId === 'python' && settings.linting.enabled && settings.linting.lintOnSave) { - this.engine.lintDocument(document, 'save').ignoreErrors(); - return; - } - - this.linterManager.getActiveLinters(false, document.uri) - .then((linters) => { - const fileName = path.basename(document.uri.fsPath).toLowerCase(); - const watchers = linters.filter((info) => info.configFileNames.indexOf(fileName) >= 0); - if (watchers.length > 0) { - setTimeout(() => this.engine.lintOpenPythonFiles(), 1000); - } - }).ignoreErrors(); - } - - private onDocumentClosed(document: TextDocument) { - if (!document || !document.fileName || !document.uri) { - return; - } - // Check if this document is still open as a duplicate editor. - if (!this.isDocumentOpen(document.uri)) { - this.engine.clearDiagnostics(document); - } - } -} diff --git a/src/client/providers/objectDefinitionProvider.ts b/src/client/providers/objectDefinitionProvider.ts deleted file mode 100644 index d284e05ffb5c..000000000000 --- a/src/client/providers/objectDefinitionProvider.ts +++ /dev/null @@ -1,93 +0,0 @@ -'use strict'; - -import * as vscode from 'vscode'; -import { JediFactory } from '../languageServices/jediProxyFactory'; -import { captureTelemetry } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import * as defProvider from './definitionProvider'; - -export class PythonObjectDefinitionProvider { - private readonly _defProvider: defProvider.PythonDefinitionProvider; - public constructor(jediFactory: JediFactory) { - this._defProvider = new defProvider.PythonDefinitionProvider(jediFactory); - } - - @captureTelemetry(EventName.GO_TO_OBJECT_DEFINITION) - public async goToObjectDefinition() { - const pathDef = await this.getObjectDefinition(); - if (typeof pathDef !== 'string' || pathDef.length === 0) { - return; - } - - const parts = pathDef.split('.'); - let source = ''; - let startColumn = 0; - if (parts.length === 1) { - source = `import ${parts[0]}`; - startColumn = 'import '.length; - } else { - const mod = parts.shift(); - source = `from ${mod} import ${parts.join('.')}`; - startColumn = `from ${mod} import `.length; - } - const range = new vscode.Range(0, startColumn, 0, source.length - 1); - // tslint:disable-next-line:no-any - const doc = { - fileName: 'test.py', - lineAt: (_line: number) => { - return { text: source }; - }, - getWordRangeAtPosition: (_position: vscode.Position) => range, - isDirty: true, - getText: () => source - }; - - const tokenSource = new vscode.CancellationTokenSource(); - const defs = await this._defProvider.provideDefinition(doc, range.start, tokenSource.token); - - if (defs === null) { - await vscode.window.showInformationMessage(`Definition not found for '${pathDef}'`); - return; - } - - let uri: vscode.Uri | undefined; - let lineNumber: number; - if (Array.isArray(defs) && defs.length > 0) { - uri = defs[0].uri; - lineNumber = defs[0].range.start.line; - } - if (defs && !Array.isArray(defs) && defs.uri) { - uri = defs.uri; - lineNumber = defs.range.start.line; - } - - if (uri) { - const openedDoc = await vscode.workspace.openTextDocument(uri); - await vscode.window.showTextDocument(openedDoc); - await vscode.commands.executeCommand('revealLine', { lineNumber: lineNumber!, at: 'top' }); - } else { - await vscode.window.showInformationMessage(`Definition not found for '${pathDef}'`); - } - } - - private intputValidation(value: string): string | undefined | null { - if (typeof value !== 'string') { - return ''; - } - value = value.trim(); - if (value.length === 0) { - return ''; - } - - return null; - } - private async getObjectDefinition(): Promise { - return vscode.window.showInputBox({ prompt: 'Enter Object Path', validateInput: this.intputValidation }); - } -} - -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[]; -} 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 { - 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 064067fb4de3..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 { EventName } from '../telemetry/constants'; -import * as proxy from './jediProxy'; - -export class PythonReferenceProvider implements vscode.ReferenceProvider { - public constructor(private jediFactory: JediFactory) { } - private static parseData(data: proxy.IReferenceResult): vscode.Location[] { - if (data && data.references.length > 0) { - // tslint:disable-next-line:no-unnecessary-local-variable - const references = data.references.filter(ref => { - if (!ref || typeof ref.columnIndex !== 'number' || typeof ref.lineIndex !== 'number' - || typeof ref.fileName !== 'string' || ref.columnIndex === -1 || ref.lineIndex === -1 || ref.fileName.length === 0) { - return false; - } - return true; - }).map(ref => { - const definitionResource = vscode.Uri.file(ref.fileName); - const range = new vscode.Range(ref.lineIndex, ref.columnIndex, ref.lineIndex, ref.columnIndex); - - return new vscode.Location(definitionResource, range); - }); - - return references; - } - return []; - } - - @captureTelemetry(EventName.REFERENCE) - public async provideReferences(document: vscode.TextDocument, position: vscode.Position, _context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise { - const filename = document.fileName; - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return; - } - if (position.character <= 0) { - return; - } - - const range = document.getWordRangeAtPosition(position); - if (!range) { - return; - } - const columnIndex = range.isEmpty ? position.character : range.end.character; - const cmd: proxy.ICommand = { - command: proxy.CommandType.Usages, - fileName: filename, - columnIndex: columnIndex, - lineIndex: position.line - }; - - if (document.isDirty) { - cmd.source = document.getText(); - } - - const data = await this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token); - return data ? PythonReferenceProvider.parseData(data) : undefined; - } -} diff --git a/src/client/providers/renameProvider.ts b/src/client/providers/renameProvider.ts deleted file mode 100644 index db278a73e78a..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 { EventName } 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(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.configurationService = serviceContainer.get(IConfigurationService); - } - @captureTelemetry(EventName.REFACTOR_RENAME) - public provideRenameEdits(document: TextDocument, position: Position, newName: string, _token: CancellationToken): ProviderResult { - return workspace.saveAll(false).then(() => { - return this.doRename(document, position, newName); - }); - } - - private doRename(document: TextDocument, position: Position, newName: string): ProviderResult { - if (document.lineAt(position.line).text.match(/^\s*\/\//)) { - return; - } - if (position.character <= 0) { - return; - } - - const range = document.getWordRangeAtPosition(position); - if (!range || range.isEmpty) { - return; - } - const oldName = document.getText(range); - if (oldName === newName) { - return; - } - - let workspaceFolder = workspace.getWorkspaceFolder(document.uri); - if (!workspaceFolder && Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - workspaceFolder = workspace.workspaceFolders[0]; - } - const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; - const pythonSettings = this.configurationService.getSettings(workspaceFolder ? workspaceFolder.uri : undefined); - - const proxy = new RefactorProxy(EXTENSION_ROOT_DIR, pythonSettings, workspaceRoot, this.serviceContainer); - return proxy.rename(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); - 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 b31d274b9015..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 { EventName } from '../telemetry/constants'; import { ICodeExecutionService } from '../terminals/types'; export class ReplProvider implements Disposable { private readonly disposables: Disposable[] = []; + + private activeResourceService: IActiveResourceService; + constructor(private serviceContainer: IServiceContainer) { + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); this.registerCommand(); } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); + + public dispose(): void { + this.disposables.forEach((disposable) => disposable.dispose()); } + private registerCommand() { const commandManager = this.serviceContainer.get(ICommandManager); const disposable = commandManager.registerCommand(Commands.Start_REPL, this.commandHandler, this); this.disposables.push(disposable); } - @captureTelemetry(EventName.REPL) + private async commandHandler() { - const resource = this.getActiveResourceUri(); - const replProvider = this.serviceContainer.get(ICodeExecutionService, 'repl'); - await replProvider.initializeRepl(resource); - } - private getActiveResourceUri(): Uri | undefined { - const documentManager = this.serviceContainer.get(IDocumentManager); - if (documentManager.activeTextEditor && !documentManager.activeTextEditor!.document.isUntitled) { - return documentManager.activeTextEditor!.document.uri; - } - const workspace = this.serviceContainer.get(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); + const interpreter = await interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + this.serviceContainer + .get(ICommandManager) + .executeCommand(Commands.TriggerEnvironmentSelection, resource) + .then(noop, noop); + return; } + const replProvider = this.serviceContainer.get(ICodeExecutionService, 'standard'); + await replProvider.initializeRepl(resource); } } diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts index 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, SortImportsEditingProvider); +export function registerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton( + IExtensionSingleActivationService, + CodeActionProviderService, + ); } diff --git a/src/client/providers/signatureProvider.ts b/src/client/providers/signatureProvider.ts deleted file mode 100644 index 8ec7169ddc15..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 { EventName } from '../telemetry/constants'; -import * as proxy from './jediProxy'; -import { isPositionInsideStringOrComment } from './providerUtilities'; - -const DOCSTRING_PARAM_PATTERNS = [ - '\\s*:type\\s*PARAMNAME:\\s*([^\\n, ]+)', // Sphinx - '\\s*:param\\s*(\\w?)\\s*PARAMNAME:[^\\n]+', // Sphinx param with type - '\\s*@type\\s*PARAMNAME:\\s*([^\\n, ]+)' // Epydoc -]; - -/** - * Extract the documentation for parameters from a given docstring. - * @param {string} paramName Name of the parameter - * @param {string} docString The docstring for the function - * @returns {string} Docstring for the parameter - */ -function extractParamDocString(paramName: string, docString: string): string { - let paramDocString = ''; - // In docstring the '*' is escaped with a backslash - paramName = paramName.replace(new RegExp('\\*', 'g'), '\\\\\\*'); - - DOCSTRING_PARAM_PATTERNS.forEach(pattern => { - if (paramDocString.length > 0) { - return; - } - pattern = pattern.replace('PARAMNAME', paramName); - const regExp = new RegExp(pattern); - const matches = regExp.exec(docString); - if (matches && matches.length > 0) { - paramDocString = matches[0]; - if (paramDocString.indexOf(':') >= 0) { - paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); - } - if (paramDocString.indexOf(':') >= 0) { - paramDocString = paramDocString.substring(paramDocString.indexOf(':') + 1); - } - } - }); - - return paramDocString.trim(); -} -export class PythonSignatureProvider implements SignatureHelpProvider { - public constructor(private jediFactory: JediFactory) { } - private static parseData(data: proxy.IArgumentsResult): SignatureHelp { - if (data && Array.isArray(data.definitions) && data.definitions.length > 0) { - const signature = new SignatureHelp(); - signature.activeSignature = 0; - - data.definitions.forEach(def => { - signature.activeParameter = def.paramindex; - // Don't display the documentation, as vs code doesn't format the documentation. - // i.e. line feeds are not respected, long content is stripped. - - // Some functions do not come with parameter docs - let label: string; - let documentation: string; - const validParamInfo = def.params && def.params.length > 0 && def.docstring && def.docstring.startsWith(`${def.name}(`); - - if (validParamInfo) { - const docLines = def.docstring.splitLines(); - label = docLines.shift()!.trim(); - documentation = docLines.join(EOL).trim(); - } else { - if (def.params && def.params.length > 0) { - label = `${def.name}(${def.params.map(p => p.name).join(', ')})`; - documentation = def.docstring; - } else { - label = def.description; - documentation = def.docstring; - } - } - - // tslint:disable-next-line:no-object-literal-type-assertion - const sig = { - label, - documentation, - parameters: [] - }; - - if (def.params && def.params.length) { - sig.parameters = def.params.map(arg => { - if (arg.docstring.length === 0) { - arg.docstring = extractParamDocString(arg.name, def.docstring); - } - // tslint:disable-next-line:no-object-literal-type-assertion - return { - documentation: arg.docstring.length > 0 ? arg.docstring : arg.description, - label: arg.name.trim() - }; - }); - } - signature.signatures.push(sig); - }); - return signature; - } - - return new SignatureHelp(); - } - @captureTelemetry(EventName.SIGNATURE) - public provideSignatureHelp(document: TextDocument, position: Position, token: CancellationToken): Thenable { - // early exit if we're in a string or comment (or in an undefined position) - if (position.character <= 0 || - isPositionInsideStringOrComment(document, position)) - { - return Promise.resolve(new SignatureHelp()); - } - - const cmd: proxy.ICommand = { - command: proxy.CommandType.Arguments, - fileName: document.fileName, - columnIndex: position.character, - lineIndex: position.line, - source: document.getText() - }; - return this.jediFactory.getJediProxyHandler(document.uri).sendCommand(cmd, token).then(data => { - return data ? PythonSignatureProvider.parseData(data) : new SignatureHelp(); - }); - } -} diff --git a/src/client/providers/simpleRefactorProvider.ts b/src/client/providers/simpleRefactorProvider.ts deleted file mode 100644 index bb94c8777f3c..000000000000 --- a/src/client/providers/simpleRefactorProvider.ts +++ /dev/null @@ -1,168 +0,0 @@ -import * as vscode from 'vscode'; -import { Commands } from '../common/constants'; -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 { EventName } from '../telemetry/constants'; - -type RenameResponse = { - results: [{ diff: string }]; -}; - -let installer: IInstaller; - -export function activateSimplePythonRefactorProvider(context: vscode.ExtensionContext, outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer) { - installer = serviceContainer.get(IInstaller); - let disposable = vscode.commands.registerCommand(Commands.Refactor_Extract_Variable, () => { - const stopWatch = new StopWatch(); - const promise = extractVariable(context.extensionPath, - vscode.window.activeTextEditor!, - vscode.window.activeTextEditor!.selection, - // tslint:disable-next-line:no-empty - outputChannel, serviceContainer).catch(() => { }); - sendTelemetryWhenDone(EventName.REFACTOR_EXTRACT_VAR, promise, stopWatch); - }); - context.subscriptions.push(disposable); - - disposable = vscode.commands.registerCommand(Commands.Refactor_Extract_Method, () => { - const stopWatch = new StopWatch(); - const promise = extractMethod(context.extensionPath, - vscode.window.activeTextEditor!, - vscode.window.activeTextEditor!.selection, - // tslint:disable-next-line:no-empty - outputChannel, serviceContainer).catch(() => { }); - sendTelemetryWhenDone(EventName.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 { - - 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).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(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { - return response.results[0].diff; - }); - - return extractName(textEditor, newName, rename, outputChannel); - }); -} - -// Exported for unit testing -export function extractMethod(extensionDir: string, textEditor: vscode.TextEditor, range: vscode.Range, - // tslint:disable-next-line:no-any - outputChannel: vscode.OutputChannel, serviceContainer: IServiceContainer): Promise { - - let workspaceFolder = vscode.workspace.getWorkspaceFolder(textEditor.document.uri); - if (!workspaceFolder && Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0) { - workspaceFolder = vscode.workspace.workspaceFolders[0]; - } - const workspaceRoot = workspaceFolder ? workspaceFolder.uri.fsPath : __dirname; - const pythonSettings = serviceContainer.get(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(textEditor.document, newName, textEditor.document.uri.fsPath, range, textEditor.options).then(response => { - return response.results[0].diff; - }); - - return extractName(textEditor, newName, rename, outputChannel); - }); -} - -// tslint:disable-next-line:no-any -function validateDocumentForRefactor(textEditor: vscode.TextEditor): Promise { - if (!textEditor.document.isDirty) { - return Promise.resolve(); - } - - // tslint:disable-next-line:no-any - return new Promise((resolve, reject) => { - vscode.window.showInformationMessage('Please save changes before refactoring', 'Save').then(item => { - if (item === 'Save') { - textEditor.document.save().then(resolve, reject); - } else { - return reject(); - } - }); - }); -} - -function extractName(textEditor: vscode.TextEditor, newName: string, - // tslint:disable-next-line:no-any - renameResponse: Promise, outputChannel: vscode.OutputChannel): Promise { - let changeStartsAtLine = -1; - return renameResponse.then(diff => { - if (diff.length === 0) { - return []; - } - return getTextEditsFromPatch(textEditor.document.getText(), diff); - }).then(edits => { - return textEditor.edit(editBuilder => { - edits.forEach(edit => { - if (changeStartsAtLine === -1 || changeStartsAtLine > edit.range.start.line) { - changeStartsAtLine = edit.range.start.line; - } - editBuilder.replace(edit.range, edit.newText); - }); - }); - }).then(done => { - if (done && changeStartsAtLine >= 0) { - let newWordPosition: vscode.Position | undefined; - for (let lineNumber = changeStartsAtLine; lineNumber < textEditor.document.lineCount; lineNumber += 1) { - const line = textEditor.document.lineAt(lineNumber); - const indexOfWord = line.text.indexOf(newName); - if (indexOfWord >= 0) { - newWordPosition = new vscode.Position(line.range.start.line, indexOfWord); - break; - } - } - - if (newWordPosition) { - textEditor.selections = [new vscode.Selection(newWordPosition, new vscode.Position(newWordPosition.line, newWordPosition.character + newName.length))]; - textEditor.revealRange(new vscode.Range(textEditor.selection.start, textEditor.selection.end), vscode.TextEditorRevealType.Default); - } - return newWordPosition; - } - return null; - }).then(newWordPosition => { - if (newWordPosition) { - return textEditor.document.save().then(() => { - // Now that we have selected the new variable, lets invoke the rename command - return vscode.commands.executeCommand('editor.action.rename'); - }); - } - }).catch(error => { - if (error === 'Not installed') { - installer.promptToInstall(Product.rope, textEditor.document.uri) - .catch(ex => 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 c72d8be22e2e..000000000000 --- a/src/client/providers/symbolProvider.ts +++ /dev/null @@ -1,169 +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 { EventName } from '../telemetry/constants'; -import * as proxy from './jediProxy'; - -function flattenSymbolTree(tree: DocumentSymbol, uri: Uri, containerName: string = ''): SymbolInformation[] { - const flattened: SymbolInformation[] = []; - - const range = new Range(tree.range.start.line, tree.range.start.character, tree.range.end.line, tree.range.end.character); - // For whatever reason, the values of VS Code's SymbolKind enum - // are off-by-one relative to the LSP: - // https://microsoft.github.io/language-server-protocol/specification#document-symbols-request-leftwards_arrow_with_hook - const kind: SymbolKind = tree.kind - 1; - const info = new SymbolInformation( - tree.name, - // Type coercion is a bit fuzzy when it comes to enums, so we - // play it safe by explicitly converting. - // tslint:disable-next-line:no-any - (SymbolKind as any)[(SymbolKind as any)[kind]], - containerName, - new Location(uri, range) - ); - flattened.push(info); - - if (tree.children && tree.children.length > 0) { - // FYI: Jedi doesn't fully-qualify the container name so we - // don't bother here either. - //const fullName = `${containerName}.${tree.name}`; - for (const child of tree.children) { - const flattenedChild = flattenSymbolTree(child, uri, tree.name); - flattened.push(...flattenedChild); - } - } - - return flattened; -} - -/** - * Provides Python symbols to VS Code (from the language server). - * - * See: - * https://code.visualstudio.com/docs/extensionAPI/vscode-api#DocumentSymbolProvider - */ -export class LanguageServerSymbolProvider implements DocumentSymbolProvider { - constructor(private readonly languageClient: LanguageClient) {} - - public async provideDocumentSymbols(document: TextDocument, token: CancellationToken): Promise { - const uri = document.uri; - const args = { textDocument: { uri: uri.toString() } }; - const raw = await this.languageClient.sendRequest('textDocument/documentSymbol', args, token); - const symbols: SymbolInformation[] = []; - for (const tree of raw) { - const flattened = flattenSymbolTree(tree, uri); - symbols.push(...flattened); - } - return Promise.resolve(symbols); - } -} - -/** - * Provides Python symbols to VS Code (from Jedi). - * - * See: - * https://code.visualstudio.com/docs/extensionAPI/vscode-api#DocumentSymbolProvider - */ -export class JediSymbolProvider implements DocumentSymbolProvider { - private debounceRequest: Map }>; - private readonly fs: IFileSystem; - - public constructor(serviceContainer: IServiceContainer, private jediFactory: JediFactory, private readonly debounceTimeoutMs = 500) { - this.debounceRequest = new Map }>(); - this.fs = serviceContainer.get(IFileSystem); - } - - @captureTelemetry(EventName.SYMBOL) - public provideDocumentSymbols(document: TextDocument, token: CancellationToken): Thenable { - return this.provideDocumentSymbolsThrottled(document, token); - } - - private provideDocumentSymbolsThrottled(document: TextDocument, token: CancellationToken): Thenable { - const key = `${document.uri.fsPath}`; - if (this.debounceRequest.has(key)) { - const item = this.debounceRequest.get(key)!; - // tslint:disable-next-line: no-any - clearTimeout(item.timer as any); - item.deferred.resolve([]); - } - - const deferred = createDeferred(); - const timer: NodeJS.Timer | number = setTimeout(() => { - if (token.isCancellationRequested) { - return deferred.resolve([]); - } - - const filename = document.fileName; - const cmd: proxy.ICommand = { - command: proxy.CommandType.Symbols, - fileName: filename, - columnIndex: 0, - lineIndex: 0 - }; - - if (document.isDirty) { - cmd.source = document.getText(); - } - - this.jediFactory - .getJediProxyHandler(document.uri) - .sendCommand(cmd, token) - .then(data => this.parseData(document, data)) - .then(items => deferred.resolve(items)) - .catch(ex => deferred.reject(ex)); - }, this.debounceTimeoutMs); - - token.onCancellationRequested(() => { - clearTimeout(timer); - deferred.resolve([]); - this.debounceRequest.delete(key); - }); - - // When a document is not saved on FS, we cannot uniquely identify it, so lets not debounce, but delay the symbol provider. - if (!document.isUntitled) { - this.debounceRequest.set(key, { timer, deferred }); - } - - return deferred.promise; - } - - // This does not appear to be used anywhere currently... - // tslint:disable-next-line:no-unused-variable - // private provideDocumentSymbolsUnthrottled(document: TextDocument, token: CancellationToken): Thenable { - // const filename = document.fileName; - - // const cmd: proxy.ICommand = { - // command: proxy.CommandType.Symbols, - // fileName: filename, - // columnIndex: 0, - // lineIndex: 0 - // }; - - // if (document.isDirty) { - // cmd.source = document.getText(); - // } - - // return this.jediFactory.getJediProxyHandler(document.uri).sendCommandNonCancellableCommand(cmd, token) - // .then(data => this.parseData(document, data)); - // } - - private parseData(document: TextDocument, data?: proxy.ISymbolResult): SymbolInformation[] { - if (data) { - const symbols = data.definitions.filter(sym => this.fs.arePathsSame(sym.fileName, document.fileName)); - return symbols.map(sym => { - const symbol = sym.kind; - const range = new Range(sym.range.startLine, sym.range.startColumn, sym.range.endLine, sym.range.endColumn); - const uri = Uri.file(sym.fileName); - const location = new Location(uri, range); - return new SymbolInformation(sym.text, symbol, sym.container, location); - }); - } - return []; - } -} diff --git a/src/client/providers/terminalProvider.ts b/src/client/providers/terminalProvider.ts index b4bc02e9c3b2..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 { 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); } - public dispose() { - this.disposables.forEach(disposable => disposable.dispose()); + + @swallowExceptions('Failed to initialize terminal provider') + public async initialize(currentTerminal: Terminal | undefined): Promise { + const configuration = this.serviceContainer.get(IConfigurationService); + const experimentService = this.serviceContainer.get(IExperimentService); + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + + if ( + currentTerminal && + pythonSettings.terminal.activateEnvInCurrentTerminal && + !inTerminalEnvVarExperiment(experimentService) && + !shouldEnvExtHandleActivation() + ) { + const hideFromUser = + 'hideFromUser' in currentTerminal.creationOptions && currentTerminal.creationOptions.hideFromUser; + if (!hideFromUser) { + const terminalActivator = this.serviceContainer.get(ITerminalActivator); + await terminalActivator.activateEnvironmentInTerminal(currentTerminal, { preserveFocus: true }); + } + sendTelemetryEvent(EventName.ACTIVATE_ENV_IN_CURRENT_TERMINAL, undefined, { + isTerminalVisible: !hideFromUser, + }); + } + } + + public dispose(): void { + this.disposables.forEach((disposable) => disposable.dispose()); } + private registerCommands() { const commandManager = this.serviceContainer.get(ICommandManager); const disposable = commandManager.registerCommand(Commands.Create_Terminal, this.onCreateTerminal, this); this.disposables.push(disposable); } + @captureTelemetry(EventName.TERMINAL_CREATE, { triggeredBy: 'commandpalette' }) private async onCreateTerminal() { + const activeResource = this.activeResourceService.getActiveResource(); + if (useEnvExtension()) { + const commandManager = this.serviceContainer.get(ICommandManager); + await commandManager.executeCommand('python-envs.createTerminal', activeResource); + } + const terminalService = this.serviceContainer.get(ITerminalServiceFactory); - const activeResource = this.getActiveResource(); await terminalService.createTerminalService(activeResource, 'Python').show(false); } - private getActiveResource(): Uri | undefined { - const documentManager = this.serviceContainer.get(IDocumentManager); - if (documentManager.activeTextEditor && !documentManager.activeTextEditor.document.isUntitled) { - return documentManager.activeTextEditor.document.uri; - } - const workspace = this.serviceContainer.get(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 e0e712e64136..000000000000 --- a/src/client/providers/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { - CancellationToken, Uri, WorkspaceEdit -} from 'vscode'; - -export const ISortImportsEditingProvider = Symbol('ISortImportsEditingProvider'); -export interface ISortImportsEditingProvider { - provideDocumentSortImportsEdits(uri: Uri, token?: CancellationToken): Promise; - sortImports(uri?: Uri): Promise; - registerCommands(): void; -} diff --git a/src/client/providers/updateSparkLibraryProvider.ts b/src/client/providers/updateSparkLibraryProvider.ts deleted file mode 100644 index 2d4da62291d0..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 { EventName } from '../telemetry/constants'; - -export function activateUpdateSparkLibraryProvider(): vscode.Disposable { - return vscode.commands.registerCommand(Commands.Update_SparkLibrary, updateSparkLibrary); -} - -function updateSparkLibrary() { - const pythonConfig = vscode.workspace.getConfiguration('python', null); - 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(EventName.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; + stop(client: BaseLanguageClient): Promise; + getTelemetryReporter(): TelemetryReporter; +} diff --git a/src/client/pythonEnvironments/api.ts b/src/client/pythonEnvironments/api.ts new file mode 100644 index 000000000000..a2065c30b740 --- /dev/null +++ b/src/client/pythonEnvironments/api.ts @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event } from 'vscode'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; + +export type GetLocatorFunc = () => Promise; + +/** + * The public API for the Python environments component. + * + * Note that this is composed of sub-components. + */ +class PythonEnvironments implements IDiscoveryAPI { + private locator!: IDiscoveryAPI; + + constructor( + // These are factories for the sub-components the full component is composed of: + private readonly getLocator: GetLocatorFunc, + ) {} + + public async activate(): Promise { + this.locator = await this.getLocator(); + } + + public get onProgress(): Event { + return this.locator.onProgress; + } + + public get refreshState(): ProgressReportStage { + return this.locator.refreshState; + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions) { + return this.locator.getRefreshPromise(options); + } + + public get onChanged() { + return this.locator.onChanged; + } + + public getEnvs(query?: PythonLocatorQuery) { + return this.locator.getEnvs(query); + } + + public async resolveEnv(env: string) { + return this.locator.resolveEnv(env); + } + + public async triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions) { + return this.locator.triggerRefresh(query, options); + } +} + +export async function createPythonEnvironments(getLocator: GetLocatorFunc): Promise { + const api = new PythonEnvironments(getLocator); + await api.activate(); + return api; +} diff --git a/src/client/pythonEnvironments/base/info/env.ts b/src/client/pythonEnvironments/base/info/env.ts new file mode 100644 index 000000000000..5c5b9317e169 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/env.ts @@ -0,0 +1,365 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep, isEqual } from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { getArchitectureDisplayName } from '../../../common/platform/registry'; +import { Architecture } from '../../../common/utils/platform'; +import { arePathsSame, isParentPath, normCasePath } from '../../common/externalDependencies'; +import { getKindDisplayName } from './envKind'; +import { areIdenticalVersion, areSimilarVersions, getVersionDisplayString, isVersionEmpty } from './pythonVersion'; + +import { + EnvPathType, + globallyInstalledEnvKinds, + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + PythonReleaseLevel, + PythonVersion, + virtualEnvKinds, +} from '.'; +import { BasicEnvInfo } from '../locator'; + +/** + * Create a new info object with all values empty. + * + * @param init - if provided, these values are applied to the new object + */ +export function buildEnvInfo(init?: { + kind?: PythonEnvKind; + executable?: string; + name?: string; + location?: string; + version?: PythonVersion; + org?: string; + arch?: Architecture; + fileInfo?: { ctime: number; mtime: number }; + source?: PythonEnvSource[]; + display?: string; + sysPrefix?: string; + searchLocation?: Uri; + type?: PythonEnvType; + /** + * Command used to run Python in this environment. + * E.g. `conda run -n envName python` or `python.exe` + */ + pythonRunCommand?: string[]; + identifiedUsingNativeLocator?: boolean; +}): PythonEnvInfo { + const env: PythonEnvInfo = { + name: init?.name ?? '', + location: '', + kind: PythonEnvKind.Unknown, + executable: { + filename: '', + sysPrefix: init?.sysPrefix ?? '', + ctime: init?.fileInfo?.ctime ?? -1, + mtime: init?.fileInfo?.mtime ?? -1, + }, + searchLocation: undefined, + display: init?.display, + version: { + major: -1, + minor: -1, + micro: -1, + release: { + level: PythonReleaseLevel.Final, + serial: 0, + }, + }, + arch: init?.arch ?? Architecture.Unknown, + distro: { + org: init?.org ?? '', + }, + source: init?.source ?? [], + pythonRunCommand: init?.pythonRunCommand, + identifiedUsingNativeLocator: init?.identifiedUsingNativeLocator, + }; + if (init !== undefined) { + updateEnv(env, init); + } + env.id = getEnvID(env.executable.filename, env.location); + return env; +} + +export function areEnvsDeepEqual(env1: PythonEnvInfo, env2: PythonEnvInfo): boolean { + const env1Clone = cloneDeep(env1); + const env2Clone = cloneDeep(env2); + // Cannot compare searchLocation as they are Uri objects. + delete env1Clone.searchLocation; + delete env2Clone.searchLocation; + env1Clone.source = env1Clone.source.sort(); + env2Clone.source = env2Clone.source.sort(); + const searchLocation1 = env1.searchLocation?.fsPath ?? ''; + const searchLocation2 = env2.searchLocation?.fsPath ?? ''; + const searchLocation1Scheme = env1.searchLocation?.scheme ?? ''; + const searchLocation2Scheme = env2.searchLocation?.scheme ?? ''; + return ( + isEqual(env1Clone, env2Clone) && + arePathsSame(searchLocation1, searchLocation2) && + searchLocation1Scheme === searchLocation2Scheme + ); +} + +/** + * Return a deep copy of the given env info. + * + * @param updates - if provided, these values are applied to the copy + */ +export function copyEnvInfo( + env: PythonEnvInfo, + updates?: { + kind?: PythonEnvKind; + }, +): PythonEnvInfo { + // We don't care whether or not extra/hidden properties + // get preserved, so we do the easy thing here. + const copied = cloneDeep(env); + if (updates !== undefined) { + updateEnv(copied, updates); + } + return copied; +} + +function updateEnv( + env: PythonEnvInfo, + updates: { + kind?: PythonEnvKind; + executable?: string; + location?: string; + version?: PythonVersion; + searchLocation?: Uri; + type?: PythonEnvType; + }, +): void { + if (updates.kind !== undefined) { + env.kind = updates.kind; + } + if (updates.executable !== undefined) { + env.executable.filename = updates.executable; + } + if (updates.location !== undefined) { + env.location = updates.location; + } + if (updates.version !== undefined) { + env.version = updates.version; + } + if (updates.searchLocation !== undefined) { + env.searchLocation = updates.searchLocation; + } + if (updates.type !== undefined) { + env.type = updates.type; + } +} + +/** + * Convert the env info to a user-facing representation. + * + * The format is `Python (: )` + * E.g. `Python 3.5.1 32-bit (myenv2: virtualenv)` + */ +export function setEnvDisplayString(env: PythonEnvInfo): void { + env.display = buildEnvDisplayString(env); + env.detailedDisplayName = buildEnvDisplayString(env, true); +} + +function buildEnvDisplayString(env: PythonEnvInfo, getAllDetails = false): string { + // main parts + const shouldDisplayKind = getAllDetails || globallyInstalledEnvKinds.includes(env.kind); + const shouldDisplayArch = !virtualEnvKinds.includes(env.kind); + const displayNameParts: string[] = ['Python']; + if (env.version && !isVersionEmpty(env.version)) { + displayNameParts.push(getVersionDisplayString(env.version)); + } + if (shouldDisplayArch) { + const archName = getArchitectureDisplayName(env.arch); + if (archName !== '') { + displayNameParts.push(archName); + } + } + + // Note that currently we do not use env.distro in the display name. + + // "suffix" + const envSuffixParts: string[] = []; + if (env.name && env.name !== '') { + envSuffixParts.push(`'${env.name}'`); + } else if (env.location && env.location !== '') { + if (env.kind === PythonEnvKind.Conda) { + const condaEnvName = path.basename(env.location); + envSuffixParts.push(`'${condaEnvName}'`); + } + } + if (shouldDisplayKind) { + const kindName = getKindDisplayName(env.kind); + if (kindName !== '') { + envSuffixParts.push(kindName); + } + } + const envSuffix = envSuffixParts.length === 0 ? '' : `(${envSuffixParts.join(': ')})`; + + // Pull it all together. + return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); +} + +/** + * For the given data, build a normalized partial info object. + * + * If insufficient data is provided to generate a minimal object, such + * that it is not identifiable, then `undefined` is returned. + */ +function getMinimalPartialInfo(env: string | PythonEnvInfo | BasicEnvInfo): Partial | undefined { + if (typeof env === 'string') { + if (env === '') { + return undefined; + } + return { + id: '', + executable: { + filename: env, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + }; + } + if ('executablePath' in env) { + return { + id: '', + executable: { + filename: env.executablePath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + location: env.envPath, + kind: env.kind, + source: env.source, + }; + } + return env; +} + +/** + * Returns path to environment folder or path to interpreter that uniquely identifies an environment. + */ +export function getEnvPath(interpreterPath: string, envFolderPath?: string): EnvPathType { + let envPath: EnvPathType = { path: interpreterPath, pathType: 'interpreterPath' }; + if (envFolderPath && !isParentPath(interpreterPath, envFolderPath)) { + // Executable is not inside the environment folder, env folder is the ID. + envPath = { path: envFolderPath, pathType: 'envFolderPath' }; + } + return envPath; +} + +/** + * Gets general unique identifier for most environments. + */ +export function getEnvID(interpreterPath: string, envFolderPath?: string): string { + return normCasePath(getEnvPath(interpreterPath, envFolderPath).path); +} + +/** + * Checks if two environments are same. + * @param {string | PythonEnvInfo} left: environment to compare. + * @param {string | PythonEnvInfo} right: environment to compare. + * @param {boolean} allowPartialMatch: allow partial matches of properties when comparing. + * + * Remarks: The current comparison assumes that if the path to the executables are the same + * then it is the same environment. Additionally, if the paths are not same but executables + * are in the same directory and the version of python is the same than we can assume it + * to be same environment. This later case is needed for comparing microsoft store python, + * where multiple versions of python executables are all put in the same directory. + */ +export function areSameEnv( + left: string | PythonEnvInfo | BasicEnvInfo, + right: string | PythonEnvInfo | BasicEnvInfo, + allowPartialMatch = true, +): boolean | undefined { + const leftInfo = getMinimalPartialInfo(left); + const rightInfo = getMinimalPartialInfo(right); + if (leftInfo === undefined || rightInfo === undefined) { + return undefined; + } + if ( + (leftInfo.executable?.filename && !rightInfo.executable?.filename) || + (!leftInfo.executable?.filename && rightInfo.executable?.filename) + ) { + return false; + } + if (leftInfo.id && leftInfo.id === rightInfo.id) { + // In case IDs are available, use it. + return true; + } + + const leftFilename = leftInfo.executable!.filename; + const rightFilename = rightInfo.executable!.filename; + + if (getEnvID(leftFilename, leftInfo.location) === getEnvID(rightFilename, rightInfo.location)) { + // Otherwise use ID function to get the ID. Note ID returned by function may itself change if executable of + // an environment changes, for eg. when conda installs python into the env. So only use it as a fallback if + // ID is not available. + return true; + } + + if (allowPartialMatch) { + const isSameDirectory = + leftFilename !== 'python' && + rightFilename !== 'python' && + arePathsSame(path.dirname(leftFilename), path.dirname(rightFilename)); + if (isSameDirectory) { + const leftVersion = typeof left === 'string' ? undefined : leftInfo.version; + const rightVersion = typeof right === 'string' ? undefined : rightInfo.version; + if (leftVersion && rightVersion) { + if (areIdenticalVersion(leftVersion, rightVersion) || areSimilarVersions(leftVersion, rightVersion)) { + return true; + } + } + } + } + return false; +} + +/** + * Returns a heuristic value on how much information is available in the given version object. + * @param {PythonVersion} version version object to generate heuristic from. + * @returns A heuristic value indicating the amount of info available in the object + * weighted by most important to least important fields. + * Wn > Wn-1 + Wn-2 + ... W0 + */ +function getPythonVersionSpecificity(version: PythonVersion): number { + let infoLevel = 0; + if (version.major > 0) { + infoLevel += 20; // W4 + } + + if (version.minor >= 0) { + infoLevel += 10; // W3 + } + + if (version.micro >= 0) { + infoLevel += 5; // W2 + } + + if (version.release?.level) { + infoLevel += 3; // W1 + } + + if (version.release?.serial || version.sysVersion) { + infoLevel += 1; // W0 + } + + return infoLevel; +} + +/** + * Compares two python versions, based on the amount of data each object has. If versionA has + * less information then the returned value is negative. If it is same then 0. If versionA has + * more information then positive. + */ +export function comparePythonVersionSpecificity(versionA: PythonVersion, versionB: PythonVersion): number { + return Math.sign(getPythonVersionSpecificity(versionA) - getPythonVersionSpecificity(versionB)); +} diff --git a/src/client/pythonEnvironments/base/info/envKind.ts b/src/client/pythonEnvironments/base/info/envKind.ts new file mode 100644 index 000000000000..08f4ce55d464 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/envKind.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '.'; + +/** + * Get the given kind's user-facing representation. + * + * If it doesn't have one then the empty string is returned. + */ +export function getKindDisplayName(kind: PythonEnvKind): string { + for (const [candidate, value] of [ + // Note that Unknown is excluded here. + [PythonEnvKind.System, 'system'], + [PythonEnvKind.MicrosoftStore, 'Microsoft Store'], + [PythonEnvKind.Pyenv, 'pyenv'], + [PythonEnvKind.Poetry, 'Poetry'], + [PythonEnvKind.Hatch, 'Hatch'], + [PythonEnvKind.Pixi, 'Pixi'], + [PythonEnvKind.Custom, 'custom'], + // For now we treat OtherGlobal like Unknown. + [PythonEnvKind.Venv, 'venv'], + [PythonEnvKind.VirtualEnv, 'virtualenv'], + [PythonEnvKind.VirtualEnvWrapper, 'virtualenv'], + [PythonEnvKind.Pipenv, 'Pipenv'], + [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'ActiveState'], + // For now we treat OtherVirtual like Unknown. + ] as [PythonEnvKind, string][]) { + if (kind === candidate) { + return value; + } + } + return ''; +} + +/** + * Gets a prioritized list of environment types for identification. + * @returns {PythonEnvKind[]} : List of environments ordered by identification priority + * + * Remarks: This is the order of detection based on how the various distributions and tools + * configure the environment, and the fall back for identification. + * Top level we have the following environment types, since they leave a unique signature + * in the environment or use a unique path for the environments they create. + * 1. Conda + * 2. Microsoft Store + * 3. PipEnv + * 4. Pyenv + * 5. Poetry + * 6. Hatch + * 7. Pixi + * + * Next level we have the following virtual environment tools. The are here because they + * are consumed by the tools above, and can also be used independently. + * 1. venv + * 2. virtualenvwrapper + * 3. virtualenv + * + * Last category is globally installed python, or system python. + */ +export function getPrioritizedEnvKinds(): PythonEnvKind[] { + return [ + PythonEnvKind.Pyenv, + PythonEnvKind.Pixi, // Placed here since Pixi environments are essentially Conda envs + PythonEnvKind.Conda, + PythonEnvKind.MicrosoftStore, + PythonEnvKind.Pipenv, + PythonEnvKind.Poetry, + PythonEnvKind.Hatch, + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnvWrapper, + PythonEnvKind.VirtualEnv, + PythonEnvKind.ActiveState, + PythonEnvKind.OtherVirtual, + PythonEnvKind.OtherGlobal, + PythonEnvKind.System, + PythonEnvKind.Custom, + PythonEnvKind.Unknown, + ]; +} diff --git a/src/client/pythonEnvironments/base/info/environmentInfoService.ts b/src/client/pythonEnvironments/base/info/environmentInfoService.ts new file mode 100644 index 000000000000..6a981d21b6df --- /dev/null +++ b/src/client/pythonEnvironments/base/info/environmentInfoService.ts @@ -0,0 +1,233 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IDisposableRegistry } from '../../../common/types'; +import { createDeferred, Deferred, sleep } from '../../../common/utils/async'; +import { createRunningWorkerPool, IWorkerPool, QueuePosition } from '../../../common/utils/workerPool'; +import { getInterpreterInfo, InterpreterInformation } from './interpreter'; +import { buildPythonExecInfo } from '../../exec'; +import { traceError, traceVerbose, traceWarn } from '../../../logging'; +import { Conda, CONDA_ACTIVATION_TIMEOUT, isCondaEnvironment } from '../../common/environmentManagers/conda'; +import { PythonEnvInfo, PythonEnvKind } from '.'; +import { normCasePath } from '../../common/externalDependencies'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { Architecture } from '../../../common/utils/platform'; +import { getEmptyVersion } from './pythonVersion'; + +export enum EnvironmentInfoServiceQueuePriority { + Default, + High, +} + +export interface IEnvironmentInfoService { + /** + * Get the interpreter information for the given environment. + * @param env The environment to get the interpreter information for. + * @param priority The priority of the request. + */ + getEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise; + /** + * Reset any stored interpreter information for the given environment. + * @param searchLocation Search location of the environment. + */ + resetInfo(searchLocation: Uri): void; +} + +async function buildEnvironmentInfo( + env: PythonEnvInfo, + useIsolated = true, +): Promise { + const python = [env.executable.filename]; + if (useIsolated) { + python.push(...['-I', OUTPUT_MARKER_SCRIPT]); + } else { + python.push(...[OUTPUT_MARKER_SCRIPT]); + } + const interpreterInfo = await getInterpreterInfo(buildPythonExecInfo(python, undefined, env.executable.filename)); + return interpreterInfo; +} + +async function buildEnvironmentInfoUsingCondaRun(env: PythonEnvInfo): Promise { + const conda = await Conda.getConda(); + const path = env.location.length ? env.location : env.executable.filename; + const condaEnv = await conda?.getCondaEnvironment(path); + if (!condaEnv) { + return undefined; + } + const python = await conda?.getRunPythonArgs(condaEnv, true, true); + if (!python) { + return undefined; + } + const interpreterInfo = await getInterpreterInfo( + buildPythonExecInfo(python, undefined, env.executable.filename), + CONDA_ACTIVATION_TIMEOUT, + ); + return interpreterInfo; +} + +class EnvironmentInfoService implements IEnvironmentInfoService { + // Caching environment here in-memory. This is so that we don't have to run this on the same + // path again and again in a given session. This information will likely not change in a given + // session. There are definitely cases where this will change. But a simple reload should address + // those. + private readonly cache: Map> = new Map< + string, + Deferred + >(); + + private workerPool?: IWorkerPool; + + private condaRunWorkerPool?: IWorkerPool; + + public dispose(): void { + if (this.workerPool !== undefined) { + this.workerPool.stop(); + this.workerPool = undefined; + } + if (this.condaRunWorkerPool !== undefined) { + this.condaRunWorkerPool.stop(); + this.condaRunWorkerPool = undefined; + } + } + + public async getEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + ): Promise { + const interpreterPath = env.executable.filename; + const result = this.cache.get(normCasePath(interpreterPath)); + if (result !== undefined) { + // Another call for this environment has already been made, return its result. + return result.promise; + } + + const deferred = createDeferred(); + this.cache.set(normCasePath(interpreterPath), deferred); + this._getEnvironmentInfo(env, priority) + .then((r) => { + deferred.resolve(r); + }) + .catch((ex) => { + deferred.reject(ex); + }); + return deferred.promise; + } + + public async _getEnvironmentInfo( + env: PythonEnvInfo, + priority?: EnvironmentInfoServiceQueuePriority, + retryOnce = true, + ): Promise { + if (env.kind === PythonEnvKind.Conda && env.executable.filename === 'python') { + const emptyInterpreterInfo: InterpreterInformation = { + arch: Architecture.Unknown, + executable: { + filename: 'python', + ctime: -1, + mtime: -1, + sysPrefix: '', + }, + version: getEmptyVersion(), + }; + + return emptyInterpreterInfo; + } + if (this.workerPool === undefined) { + this.workerPool = createRunningWorkerPool( + buildEnvironmentInfo, + ); + } + + let reason: Error | undefined; + let r = await addToQueue(this.workerPool, env, priority).catch((err) => { + reason = err; + return undefined; + }); + + if (r === undefined) { + // Even though env kind is not conda, it can still be a conda environment + // as complete env info may not be available at this time. + const isCondaEnv = env.kind === PythonEnvKind.Conda || (await isCondaEnvironment(env.executable.filename)); + if (isCondaEnv) { + traceVerbose( + `Validating ${env.executable.filename} normally failed with error, falling back to using conda run: (${reason})`, + ); + if (this.condaRunWorkerPool === undefined) { + // Create a separate queue for validation using conda, so getting environment info for + // other types of environment aren't blocked on conda. + this.condaRunWorkerPool = createRunningWorkerPool< + PythonEnvInfo, + InterpreterInformation | undefined + >(buildEnvironmentInfoUsingCondaRun); + } + r = await addToQueue(this.condaRunWorkerPool, env, priority).catch((err) => { + traceError(err); + return undefined; + }); + } else if (reason) { + if ( + reason.message.includes('Unknown option: -I') || + reason.message.includes("ModuleNotFoundError: No module named 'encodings'") + ) { + traceWarn(reason); + if (reason.message.includes('Unknown option: -I')) { + traceError( + 'Support for Python 2.7 has been dropped by the Python extension so certain features may not work, upgrade to using Python 3.', + ); + } + return buildEnvironmentInfo(env, false).catch((err) => { + traceError(err); + return undefined; + }); + } + traceError(reason); + } + } + if (r === undefined && retryOnce) { + // Retry once, in case the environment was not fully populated. Also observed in CI: + // https://github.com/microsoft/vscode-python/issues/20147 where running environment the first time + // failed due to unknown reasons. + return sleep(2000).then(() => this._getEnvironmentInfo(env, priority, false)); + } + return r; + } + + public resetInfo(searchLocation: Uri): void { + const searchLocationPath = searchLocation.fsPath; + const keys = Array.from(this.cache.keys()); + keys.forEach((key) => { + if (key.startsWith(normCasePath(searchLocationPath))) { + this.cache.delete(key); + } + }); + } +} + +function addToQueue( + workerPool: IWorkerPool, + env: PythonEnvInfo, + priority: EnvironmentInfoServiceQueuePriority | undefined, +) { + return priority === EnvironmentInfoServiceQueuePriority.High + ? workerPool.addToQueue(env, QueuePosition.Front) + : workerPool.addToQueue(env, QueuePosition.Back); +} + +let envInfoService: IEnvironmentInfoService | undefined; +export function getEnvironmentInfoService(disposables?: IDisposableRegistry): IEnvironmentInfoService { + if (envInfoService === undefined) { + const service = new EnvironmentInfoService(); + disposables?.push({ + dispose: () => { + service.dispose(); + envInfoService = undefined; + }, + }); + envInfoService = service; + } + return envInfoService; +} diff --git a/src/client/pythonEnvironments/base/info/executable.ts b/src/client/pythonEnvironments/base/info/executable.ts new file mode 100644 index 000000000000..ab5a67d79315 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/executable.ts @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { getOSType, OSType } from '../../../common/utils/platform'; +import { getEmptyVersion, parseVersion } from './pythonVersion'; + +import { PythonVersion } from '.'; +import { normCasePath } from '../../common/externalDependencies'; + +/** + * Determine a best-effort Python version based on the given filename. + */ +export function parseVersionFromExecutable(filename: string): PythonVersion { + const version = parseBasename(path.basename(filename)); + + if (version.major === 2 && version.minor === -1) { + version.minor = 7; + } + + return version; +} + +function parseBasename(basename: string): PythonVersion { + basename = normCasePath(basename); + if (getOSType() === OSType.Windows) { + if (basename === 'python.exe') { + // On Windows we can't assume it is 2.7. + return getEmptyVersion(); + } + } else if (basename === 'python') { + // We can assume it is 2.7. (See PEP 394.) + return parseVersion('2.7'); + } + if (!basename.startsWith('python')) { + throw Error(`not a Python executable (expected "python..", got "${basename}")`); + } + // If we reach here then we expect it to have a version in the name. + return parseVersion(basename); +} diff --git a/src/client/pythonEnvironments/base/info/index.ts b/src/client/pythonEnvironments/base/info/index.ts new file mode 100644 index 000000000000..4547e7606308 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/index.ts @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { Architecture } from '../../../common/utils/platform'; +import { BasicVersionInfo, VersionInfo } from '../../../common/utils/version'; + +/** + * IDs for the various supported Python environments. + */ +export enum PythonEnvKind { + Unknown = 'unknown', + // "global" + System = 'global-system', + MicrosoftStore = 'global-microsoft-store', + Pyenv = 'global-pyenv', + Poetry = 'poetry', + Hatch = 'hatch', + Pixi = 'pixi', + ActiveState = 'activestate', + Custom = 'global-custom', + OtherGlobal = 'global-other', + // "virtual" + Venv = 'virt-venv', + VirtualEnv = 'virt-virtualenv', + VirtualEnvWrapper = 'virt-virtualenvwrapper', + Pipenv = 'virt-pipenv', + Conda = 'virt-conda', + OtherVirtual = 'virt-other', +} + +export enum PythonEnvType { + Conda = 'Conda', + Virtual = 'Virtual', +} + +export interface EnvPathType { + /** + * Path to environment folder or path to interpreter that uniquely identifies an environment. + * Virtual environments lacking an interpreter are identified by environment folder paths, + * whereas other envs can be identified using interpreter path. + */ + path: string; + pathType: 'envFolderPath' | 'interpreterPath'; +} + +export const virtualEnvKinds = [ + PythonEnvKind.Poetry, + PythonEnvKind.Hatch, + PythonEnvKind.Pixi, + PythonEnvKind.Pipenv, + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnvWrapper, + PythonEnvKind.Conda, + PythonEnvKind.VirtualEnv, +]; + +export const globallyInstalledEnvKinds = [ + PythonEnvKind.OtherGlobal, + PythonEnvKind.Unknown, + PythonEnvKind.MicrosoftStore, + PythonEnvKind.System, + PythonEnvKind.Custom, +]; + +/** + * Information about a file. + */ +export type FileInfo = { + filename: string; + ctime: number; + mtime: number; +}; + +/** + * Information about a Python binary/executable. + */ +export type PythonExecutableInfo = FileInfo & { + sysPrefix: string; +}; + +/** + * Source types indicating how a particular environment was discovered. + * + * Notes: This is used in auto-selection to figure out which python to select. + * We added this field to support the existing mechanism in the extension to + * calculate the auto-select python. + */ +export enum PythonEnvSource { + /** + * Environment was found via PATH env variable + */ + PathEnvVar = 'path env var', + /** + * Environment was found in windows registry + */ + WindowsRegistry = 'windows registry', + // If source turns out to be useful we will expand this enum to contain more details sources. +} + +/** + * The most fundamental information about a Python environment. + * + * You should expect these objects to be complete (no empty props). + * Note that either `name` or `location` must be non-empty, though + * the other *can* be empty. + * + * @prop id - the env's unique ID + * @prop kind - the env's kind + * @prop executable - info about the env's Python binary + * @prop name - the env's distro-specific name, if any + * @prop location - the env's location (on disk), if relevant + * @prop source - the locator[s] which found the environment. + */ +type PythonEnvBaseInfo = { + id?: string; + kind: PythonEnvKind; + type?: PythonEnvType; + executable: PythonExecutableInfo; + // One of (name, location) must be non-empty. + name: string; + location: string; + // Other possible fields: + // * managed: boolean (if the env is "managed") + // * parent: PythonEnvBaseInfo (the env from which this one was created) + // * binDir: string (where env-installed executables are found) + + source: PythonEnvSource[]; +}; + +/** + * The possible Python release levels. + */ +export enum PythonReleaseLevel { + Alpha = 'alpha', + Beta = 'beta', + Candidate = 'candidate', + Final = 'final', +} + +/** + * Release information for a Python version. + */ +export type PythonVersionRelease = { + level: PythonReleaseLevel; + serial: number; +}; + +/** + * Version information for a Python build/installation. + * + * @prop sysVersion - the raw text from `sys.version` + */ +export type PythonVersion = BasicVersionInfo & { + release?: PythonVersionRelease; + sysVersion?: string; +}; + +/** + * Information for a Python build/installation. + */ +type PythonBuildInfo = { + version: PythonVersion; // incl. raw, AKA sys.version + arch: Architecture; +}; + +/** + * Meta information about a Python distribution. + * + * @prop org - the name of the distro's creator/publisher + * @prop defaultDisplayName - the text to use when showing the distro to users + */ +type PythonDistroMetaInfo = { + org: string; + defaultDisplayName?: string; +}; + +/** + * Information about an installed Python distribution. + * + * @prop version - the installed *distro* version (not the Python version) + * @prop binDir - where to look for the distro's executables (i.e. tools) + */ +export type PythonDistroInfo = PythonDistroMetaInfo & { + version?: VersionInfo; + binDir?: string; +}; + +type _PythonEnvInfo = PythonEnvBaseInfo & PythonBuildInfo; + +/** + * All the available information about a Python environment. + * + * Note that not all the information will necessarily be filled in. + * Locators are only required to fill in the "base" info, though + * they will usually be able to provide the version as well. + * + * @prop distro - the installed Python distro that this env is using or belongs to + * @prop display - the text to use when showing the env to users + * @prop detailedDisplayName - display name containing all details + * @prop searchLocation - the project to which this env is related to, if any + */ +export type PythonEnvInfo = _PythonEnvInfo & { + distro: PythonDistroInfo; + display?: string; + detailedDisplayName?: string; + searchLocation?: Uri; + /** + * Command used to run Python in this environment. + * E.g. `conda run -n envName python` or `python.exe` + */ + pythonRunCommand?: string[]; + identifiedUsingNativeLocator?: boolean; +}; + +/** + * A dummy python version object containing default fields. + * + * Note this object is immutable. So if it is assigned to another object, the properties of the other object + * also cannot be modified by reference. For eg. `otherVersionObject.major = 3` won't work. + */ +export const UNKNOWN_PYTHON_VERSION: PythonVersion = { + major: -1, + minor: -1, + micro: -1, + release: { level: PythonReleaseLevel.Final, serial: -1 }, + sysVersion: undefined, +}; +Object.freeze(UNKNOWN_PYTHON_VERSION); diff --git a/src/client/pythonEnvironments/base/info/interpreter.ts b/src/client/pythonEnvironments/base/info/interpreter.ts new file mode 100644 index 000000000000..e19e1f0d45c2 --- /dev/null +++ b/src/client/pythonEnvironments/base/info/interpreter.ts @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonExecutableInfo, PythonVersion } from '.'; +import { isCI } from '../../../common/constants'; +import { + interpreterInfo as getInterpreterInfoCommand, + InterpreterInfoJson, +} from '../../../common/process/internal/scripts'; +import { Architecture } from '../../../common/utils/platform'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { shellExecute } from '../../common/externalDependencies'; +import { copyPythonExecInfo, PythonExecInfo } from '../../exec'; +import { parseVersion } from './pythonVersion'; + +export type InterpreterInformation = { + arch: Architecture; + executable: PythonExecutableInfo; + version: PythonVersion; +}; + +/** + * Compose full interpreter information based on the given data. + * + * The data format corresponds to the output of the `interpreterInfo.py` script. + * + * @param python - the path to the Python executable + * @param raw - the information returned by the `interpreterInfo.py` script + */ +function extractInterpreterInfo(python: string, raw: InterpreterInfoJson): InterpreterInformation { + let rawVersion = `${raw.versionInfo.slice(0, 3).join('.')}`; + + // We only need additional version details if the version is 'alpha', 'beta' or 'candidate'. + // This restriction is needed to avoid sending any PII if this data is used with telemetry. + // With custom builds of python it is possible that release level and values after that can + // contain PII. + if (raw.versionInfo[3] !== undefined && ['final', 'alpha', 'beta', 'candidate'].includes(raw.versionInfo[3])) { + rawVersion = `${rawVersion}-${raw.versionInfo[3]}`; + if (raw.versionInfo[4] !== undefined) { + let serial = -1; + try { + serial = parseInt(`${raw.versionInfo[4]}`, 10); + } catch (ex) { + serial = -1; + } + rawVersion = serial >= 0 ? `${rawVersion}${serial}` : rawVersion; + } + } + return { + arch: raw.is64Bit ? Architecture.x64 : Architecture.x86, + executable: { + filename: python, + sysPrefix: raw.sysPrefix, + mtime: -1, + ctime: -1, + }, + version: { + ...parseVersion(rawVersion), + sysVersion: raw.sysVersion, + }, + }; +} + +/** + * Collect full interpreter information from the given Python executable. + * + * @param python - the information to use when running Python + * @param timeout - any specific timeouts to use for getting info. + */ +export async function getInterpreterInfo( + python: PythonExecInfo, + timeout?: number, +): Promise { + const [args, parse] = getInterpreterInfoCommand(); + const info = copyPythonExecInfo(python, args); + const argv = [info.command, ...info.args]; + + // Concat these together to make a set of quoted strings + const quoted = argv.reduce( + (p, c) => (p ? `${p} ${c.toCommandArgumentForPythonExt()}` : `${c.toCommandArgumentForPythonExt()}`), + '', + ); + + // Sometimes on CI, the python process takes a long time to start up. This is a workaround for that. + let standardTimeout = isCI ? 30000 : 15000; + if (process.env.VSC_PYTHON_INTERPRETER_INFO_TIMEOUT !== undefined) { + // Custom override for setups where the initial Python setup process may take longer than the standard timeout. + standardTimeout = parseInt(process.env.VSC_PYTHON_INTERPRETER_INFO_TIMEOUT, 10); + traceInfo(`Custom interpreter discovery timeout: ${standardTimeout}`); + } + + // Try shell execing the command, followed by the arguments. This will make node kill the process if it + // takes too long. + // Sometimes the python path isn't valid, timeout if that's the case. + // See these two bugs: + // https://github.com/microsoft/vscode-python/issues/7569 + // https://github.com/microsoft/vscode-python/issues/7760 + const result = await shellExecute(quoted, { timeout: timeout ?? standardTimeout }); + if (result.stderr) { + traceError( + `Stderr when executing script with >> ${quoted} << stderr: ${result.stderr}, still attempting to parse output`, + ); + } + let json: InterpreterInfoJson; + try { + json = parse(result.stdout); + } catch (ex) { + traceError(`Failed to parse interpreter information for >> ${quoted} << with ${ex}`); + return undefined; + } + traceVerbose(`Found interpreter for >> ${quoted} <<: ${JSON.stringify(json)}`); + return extractInterpreterInfo(python.pythonExecutable, json); +} diff --git a/src/client/pythonEnvironments/base/info/pythonVersion.ts b/src/client/pythonEnvironments/base/info/pythonVersion.ts new file mode 100644 index 000000000000..589bf4c7b7af --- /dev/null +++ b/src/client/pythonEnvironments/base/info/pythonVersion.ts @@ -0,0 +1,290 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as basic from '../../../common/utils/version'; + +import { PythonReleaseLevel, PythonVersion, PythonVersionRelease, UNKNOWN_PYTHON_VERSION } from '.'; +import { traceError } from '../../../logging'; + +// XXX getPythonVersionFromPath() should go away in favor of parseVersionFromExecutable(). + +export function getPythonVersionFromPath(exe: string): PythonVersion { + let version = UNKNOWN_PYTHON_VERSION; + try { + version = parseVersion(path.basename(exe)); + } catch (ex) { + traceError(`Failed to parse version from path: ${exe}`, ex); + } + return version; +} + +/** + * Convert the given string into the corresponding Python version object. + * + * Example: + * 3.9.0 + * 3.9.0a1 + * 3.9.0b2 + * 3.9.0rc1 + * 3.9.0-beta2 + * 3.9.0.beta.2 + * 3.9.0.final.0 + * 39 + */ +export function parseVersion(versionStr: string): PythonVersion { + const [version, after] = parseBasicVersion(versionStr); + if (version.micro === -1) { + return version; + } + const [release] = parseRelease(after); + version.release = release; + return version; +} + +export function parseRelease(text: string): [PythonVersionRelease | undefined, string] { + let after: string; + + let alpha: string | undefined; + let beta: string | undefined; + let rc: string | undefined; + let fin: string | undefined; + let serialStr: string; + + let match = text.match(/^(?:-?final|\.final(?:\.0)?)(.*)$/); + if (match) { + [, after] = match; + fin = 'final'; + serialStr = '0'; + } else { + for (const regex of [ + /^(?:(a)|(b)|(rc))([1-9]\d*)(.*)$/, + /^-(?:(?:(alpha)|(beta)|(candidate))([1-9]\d*))(.*)$/, + /^\.(?:(?:(alpha)|(beta)|(candidate))\.([1-9]\d*))(.*)$/, + ]) { + match = text.match(regex); + if (match) { + [, alpha, beta, rc, serialStr, after] = match; + break; + } + } + } + + let level: PythonReleaseLevel; + if (fin) { + level = PythonReleaseLevel.Final; + } else if (rc) { + level = PythonReleaseLevel.Candidate; + } else if (beta) { + level = PythonReleaseLevel.Beta; + } else if (alpha) { + level = PythonReleaseLevel.Alpha; + } else { + // We didn't find release info. + return [undefined, text]; + } + const serial = parseInt(serialStr!, 10); + return [{ level, serial }, after!]; +} + +/** + * Convert the given string into the corresponding Python version object. + */ +export function parseBasicVersion(versionStr: string): [PythonVersion, string] { + // We set a prefix (which will be ignored) to make sure "plain" + // versions are fully parsed. + const parsed = basic.parseBasicVersionInfo(`ignored-${versionStr}`); + if (!parsed) { + if (versionStr === '') { + return [getEmptyVersion(), '']; + } + throw Error(`invalid version ${versionStr}`); + } + // We ignore any "before" text. + const { version, after } = parsed; + version.release = undefined; + + if (version.minor === -1) { + // We trust that the major version is always single-digit. + if (version.major > 9) { + const numdigits = version.major.toString().length - 1; + const factor = 10 ** numdigits; + version.minor = version.major % factor; + version.major = Math.floor(version.major / factor); + } + } + + return [version, after]; +} + +/** + * Get a new version object with all properties "zeroed out". + */ +export function getEmptyVersion(): PythonVersion { + return cloneDeep(basic.EMPTY_VERSION); +} + +/** + * Determine if the version is effectively a blank one. + */ +export function isVersionEmpty(version: PythonVersion): boolean { + // We really only care the `version.major` is -1. However, using + // generic util is better in the long run. + return basic.isVersionInfoEmpty(version); +} +/** + * Convert the info to a user-facing representation. + */ +export function getVersionDisplayString(ver: PythonVersion): string { + if (isVersionEmpty(ver)) { + return ''; + } + if (ver.micro !== -1) { + return getShortVersionString(ver); + } + return `${getShortVersionString(ver)}.x`; +} + +/** + * Convert the info to a simple string. + */ +export function getShortVersionString(ver: PythonVersion): string { + let verStr = basic.getVersionString(ver); + if (ver.release === undefined) { + return verStr; + } + if (ver.release.level === PythonReleaseLevel.Final) { + return verStr; + } + if (ver.release.level === PythonReleaseLevel.Candidate) { + verStr = `${verStr}rc${ver.release.serial}`; + } else if (ver.release.level === PythonReleaseLevel.Beta) { + verStr = `${verStr}b${ver.release.serial}`; + } else if (ver.release.level === PythonReleaseLevel.Alpha) { + verStr = `${verStr}a${ver.release.serial}`; + } else { + throw Error(`unsupported release level ${ver.release.level}`); + } + return verStr; +} + +/** + * Checks if all the important properties of the version objects match. + * + * Only major, minor, micro, and release are compared. + */ +export function areIdenticalVersion(left: PythonVersion, right: PythonVersion): boolean { + return basic.areIdenticalVersion(left, right, compareVersionRelease); +} + +/** + * Checks if the versions are identical or one is more complete than other (and otherwise the same). + * + * A `true` result means the Python executables are strictly compatible. + * For Python 3+, at least the minor version must be set. `(2, -1, -1)` + * implies 2.7, so in that case only the major version must be set (to 2). + */ +export function areSimilarVersions(left: PythonVersion, right: PythonVersion): boolean { + if (!basic.areSimilarVersions(left, right, compareVersionRelease)) { + return false; + } + if (left.major === 2) { + return true; + } + return left.minor > -1 && right.minor > -1; +} + +function compareVersionRelease(left: PythonVersion, right: PythonVersion): [number, string] { + if (left.release === undefined) { + if (right.release === undefined) { + return [0, '']; + } + return [1, 'level']; + } + if (right.release === undefined) { + return [-1, 'level']; + } + + // Compare the level. + if (left.release.level < right.release.level) { + return [1, 'level']; + } + if (left.release.level > right.release.level) { + return [-1, 'level']; + } + if (left.release.level === PythonReleaseLevel.Final) { + // We ignore "serial". + return [0, '']; + } + + // Compare the serial. + if (left.release.serial < right.release.serial) { + return [1, 'serial']; + } + if (left.release.serial > right.release.serial) { + return [-1, 'serial']; + } + + return [0, '']; +} + +/** + * Convert Python version to semver like version object. + * + * Remarks: primarily used to convert to old type of environment info. + * @deprecated + */ +export function toSemverLikeVersion( + version: PythonVersion, +): { + raw: string; + major: number; + minor: number; + patch: number; + build: string[]; + prerelease: string[]; +} { + const versionPrefix = basic.getVersionString(version); + let preRelease: string[] = []; + if (version.release) { + preRelease = + version.release.serial < 0 + ? [`${version.release.level}`] + : [`${version.release.level}`, `${version.release.serial}`]; + } + return { + raw: versionPrefix, + major: version.major, + minor: version.minor, + patch: version.micro, + build: [], + prerelease: preRelease, + }; +} + +/** + * Compares major, minor, patch for two versions of python + * @param v1 : semVer like version object + * @param v2 : semVer like version object + * @returns {1 | 0 | -1} : 0 if v1 === v2, + * 1 if v1 > v2, + * -1 if v1 < v2 + * Remarks: primarily used compare to old type of version info. + * @deprecated + */ +export function compareSemVerLikeVersions( + v1: { major: number; minor: number; patch: number }, + v2: { major: number; minor: number; patch: number }, +): 1 | 0 | -1 { + if (v1.major === v2.major) { + if (v1.minor === v2.minor) { + if (v1.patch === v2.patch) { + return 0; + } + return v1.patch > v2.patch ? 1 : -1; + } + return v1.minor > v2.minor ? 1 : -1; + } + return v1.major > v2.major ? 1 : -1; +} diff --git a/src/client/pythonEnvironments/base/locator.ts b/src/client/pythonEnvironments/base/locator.ts new file mode 100644 index 000000000000..0c15f8b27e5f --- /dev/null +++ b/src/client/pythonEnvironments/base/locator.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable max-classes-per-file */ + +import { Event, Uri } from 'vscode'; +import { IAsyncIterableIterator, iterEmpty } from '../../common/utils/async'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvSource, PythonVersion } from './info'; +import { + IPythonEnvsWatcher, + PythonEnvCollectionChangedEvent, + PythonEnvsChangedEvent, + PythonEnvsWatcher, +} from './watcher'; +import type { Architecture } from '../../common/utils/platform'; + +/** + * A single update to a previously provided Python env object. + */ +export type PythonEnvUpdatedEvent = { + /** + * The iteration index of The env info that was previously provided. + */ + index: number; + /** + * The env info that was previously provided. + */ + old?: I; + /** + * The env info that replaces the old info. + * Update is sent as `undefined` if we find out that the environment is no longer valid. + */ + update: I | undefined; +}; + +/** + * A fast async iterator of Python envs, which may have incomplete info. + * + * Each object yielded by the iterator represents a unique Python + * environment. + * + * The iterator is not required to have provide all info about + * an environment. However, each yielded item will at least + * include all the `PythonEnvBaseInfo` data. + * + * During iteration the information for an already + * yielded object may be updated. Rather than updating the yielded + * object or yielding it again with updated info, the update is + * emitted by the iterator's `onUpdated` (event) property. Once there are no more updates, the event emits + * `null`. + * + * If the iterator does not have `onUpdated` then it means the + * provider does not support updates. + * + * Callers can usually ignore the update event entirely and rely on + * the locator to provide sufficiently complete information. + */ +export interface IPythonEnvsIterator extends IAsyncIterableIterator { + /** + * Provides possible updates for already-iterated envs. + * + * Once there are no more updates, `null` is emitted. + * + * If this property is not provided then it means the iterator does + * not support updates. + */ + onUpdated?: Event | ProgressNotificationEvent>; +} + +export enum ProgressReportStage { + idle = 'idle', + discoveryStarted = 'discoveryStarted', + allPathsDiscovered = 'allPathsDiscovered', + discoveryFinished = 'discoveryFinished', +} + +export type ProgressNotificationEvent = { + stage: ProgressReportStage; +}; + +export function isProgressEvent( + event: PythonEnvUpdatedEvent | ProgressNotificationEvent, +): event is ProgressNotificationEvent { + return 'stage' in event; +} + +/** + * An empty Python envs iterator. + */ +export const NOOP_ITERATOR: IPythonEnvsIterator = iterEmpty(); + +/** + * The most basic info to send to a locator when requesting environments. + * + * This is directly correlated with the `BasicPythonEnvsChangedEvent` + * emitted by watchers. + */ +type BasicPythonLocatorQuery = { + /** + * If provided, results should be limited to these env + * kinds; if not provided, the kind of each environment + * is not considered when filtering + */ + kinds?: PythonEnvKind[]; +}; + +/** + * The portion of a query related to env search locations. + */ +type SearchLocations = { + /** + * The locations under which to look for environments. + */ + roots: Uri[]; + /** + * If true, only query for workspace related envs, i.e do not look for environments that do not have a search location. + */ + doNotIncludeNonRooted?: boolean; +}; + +/** + * The full set of possible info to send to a locator when requesting environments. + * + * This is directly correlated with the `PythonEnvsChangedEvent` + * emitted by watchers. + */ +export type PythonLocatorQuery = BasicPythonLocatorQuery & { + /** + * If provided, results should be limited to within these locations. + */ + searchLocations?: SearchLocations; + /** + * If provided, results should be limited envs provided by these locators. + */ + providerId?: string; + /** + * If provided, results are limited to this env. + */ + envPath?: string; +}; + +type QueryForEvent = E extends PythonEnvsChangedEvent ? PythonLocatorQuery : BasicPythonLocatorQuery; + +export type BasicEnvInfo = { + kind: PythonEnvKind; + executablePath: string; + source?: PythonEnvSource[]; + envPath?: string; + /** + * The project to which this env is related to, if any + * E.g. the project directory when dealing with pipenv virtual environments. + */ + searchLocation?: Uri; + version?: PythonVersion; + name?: string; + /** + * Display name provided by locators, not generated by us. + * E.g. display name as provided by Windows Registry or Windows Store, etc + */ + displayName?: string; + identifiedUsingNativeLocator?: boolean; + arch?: Architecture; + ctime?: number; + mtime?: number; +}; + +/** + * A single Python environment locator. + * + * Each locator object is responsible for identifying the Python + * environments in a single location, whether a directory, a directory + * tree, or otherwise. That location is identified when the locator + * is instantiated. + * + * Based on the narrow focus of each locator, the assumption is that + * calling iterEnvs() to pick up a changed env is effectively no more + * expensive than tracking down that env specifically. Consequently, + * events emitted via `onChanged` do not need to provide information + * for the specific environments that changed. + */ +export interface ILocator extends IPythonEnvsWatcher { + readonly providerId: string; + /** + * Iterate over the enviroments known tos this locator. + * + * Locators are not required to have provide all info about + * an environment. However, each yielded item will at least + * include all the `PythonEnvBaseInfo` data. To ensure all + * possible information is filled in, call `ILocator.resolveEnv()`. + * + * Updates to yielded objects may be provided via the optional + * `onUpdated` property of the iterator. However, callers can + * usually ignore the update event entirely and rely on the + * locator to provide sufficiently complete information. + * + * @param query - if provided, the locator will limit results to match + * @returns - the fast async iterator of Python envs, which may have incomplete info + */ + iterEnvs(query?: QueryForEvent): IPythonEnvsIterator; +} + +export type ICompositeLocator = Omit, 'providerId'>; + +interface IResolver { + /** + * Find as much info about the given Python environment as possible. + * If path passed is invalid, then `undefined` is returned. + * + * @param path - Python executable path or environment path to resolve more information about + */ + resolveEnv(path: string): Promise; +} + +export interface IResolvingLocator extends IResolver, ICompositeLocator {} + +export interface GetRefreshEnvironmentsOptions { + /** + * Get refresh promise which resolves once the following stage has been reached for the list of known environments. + */ + stage?: ProgressReportStage; +} + +export type TriggerRefreshOptions = { + /** + * Only trigger a refresh if it hasn't already been triggered for this session. + */ + ifNotTriggerredAlready?: boolean; +}; + +export interface IDiscoveryAPI { + readonly refreshState: ProgressReportStage; + /** + * Tracks discovery progress for current list of known environments, i.e when it starts, finishes or any other relevant + * stage. Note the progress for a particular query is currently not tracked or reported, this only indicates progress of + * the entire collection. + */ + readonly onProgress: Event; + /** + * Fires with details if the known list changes. + */ + readonly onChanged: Event; + /** + * Resolves once environment list has finished refreshing, i.e all environments are + * discovered. Carries `undefined` if there is no refresh currently going on. + */ + getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined; + /** + * Triggers a new refresh for query if there isn't any already running. + */ + triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise; + /** + * Get current list of known environments. + */ + getEnvs(query?: PythonLocatorQuery): PythonEnvInfo[]; + /** + * Find as much info about the given Python environment as possible. + * If path passed is invalid, then `undefined` is returned. + * + * @param path - Full path of Python executable or environment folder to resolve more information about + */ + resolveEnv(path: string): Promise; +} + +export interface IEmitter { + fire(e: E): void; +} + +/** + * The generic base for Python envs locators. + * + * By default `resolveEnv()` returns undefined. Subclasses may override + * the method to provide an implementation. + * + * Subclasses will call `this.emitter.fire()` to emit events. + * + * Also, in most cases the default event type (`PythonEnvsChangedEvent`) + * should be used. Only in low-level cases should you consider using + * `BasicPythonEnvsChangedEvent`. + */ +abstract class LocatorBase implements ILocator { + public readonly onChanged: Event; + + public abstract readonly providerId: string; + + protected readonly emitter: IEmitter; + + constructor(watcher: IPythonEnvsWatcher & IEmitter) { + this.emitter = watcher; + this.onChanged = watcher.onChanged; + } + + // eslint-disable-next-line class-methods-use-this + public abstract iterEnvs(query?: QueryForEvent): IPythonEnvsIterator; +} + +/** + * The base for most Python envs locators. + * + * By default `resolveEnv()` returns undefined. Subclasses may override + * the method to provide an implementation. + * + * Subclasses will call `this.emitter.fire()` * to emit events. + * + * In most cases this is the class you will want to subclass. + * Only in low-level cases should you consider subclassing `LocatorBase` + * using `BasicPythonEnvsChangedEvent. + */ +export abstract class Locator extends LocatorBase { + constructor() { + super(new PythonEnvsWatcher()); + } +} diff --git a/src/client/pythonEnvironments/base/locatorUtils.ts b/src/client/pythonEnvironments/base/locatorUtils.ts new file mode 100644 index 000000000000..6af8c0ee1b69 --- /dev/null +++ b/src/client/pythonEnvironments/base/locatorUtils.ts @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { createDeferred } from '../../common/utils/async'; +import { getURIFilter } from '../../common/utils/misc'; +import { traceVerbose } from '../../logging'; +import { PythonEnvInfo } from './info'; +import { + IPythonEnvsIterator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from './locator'; + +/** + * Create a filter function to match the given query. + */ +export function getQueryFilter(query: PythonLocatorQuery): (env: PythonEnvInfo) => boolean { + const kinds = query.kinds !== undefined && query.kinds.length > 0 ? query.kinds : undefined; + const includeNonRooted = !query.searchLocations?.doNotIncludeNonRooted; // We default to `true`. + const locationFilters = getSearchLocationFilters(query); + function checkKind(env: PythonEnvInfo): boolean { + if (kinds === undefined) { + return true; + } + return kinds.includes(env.kind); + } + function checkSearchLocation(env: PythonEnvInfo): boolean { + if (env.searchLocation === undefined) { + // It is not a "rooted" env. + return includeNonRooted; + } + // It is a "rooted" env. + const loc = env.searchLocation; + if (locationFilters !== undefined) { + // Check against the requested roots. (There may be none.) + return locationFilters.some((filter) => filter(loc)); + } + return true; + } + return (env) => { + if (!checkKind(env)) { + return false; + } + if (!checkSearchLocation(env)) { + return false; + } + return true; + }; +} + +function getSearchLocationFilters(query: PythonLocatorQuery): ((u: Uri) => boolean)[] | undefined { + if (query.searchLocations === undefined) { + return undefined; + } + if (query.searchLocations.roots.length === 0) { + return []; + } + return query.searchLocations.roots.map((loc) => + getURIFilter(loc, { + checkParent: true, + }), + ); +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvs(iterator: IPythonEnvsIterator): Promise { + const envs: (I | undefined)[] = []; + + const updatesDone = createDeferred(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + const listener = iterator.onUpdated((event: PythonEnvUpdatedEvent | ProgressNotificationEvent) => { + if (isProgressEvent(event)) { + if (event.stage !== ProgressReportStage.discoveryFinished) { + return; + } + updatesDone.resolve(); + listener.dispose(); + } else if (event.index !== undefined) { + const { index, update } = event; + if (envs[index] === undefined) { + const json = JSON.stringify(update); + traceVerbose( + `Updates sent for an env which was classified as invalid earlier, currently not expected, ${json}`, + ); + } + // We don't worry about if envs[index] is set already. + envs[index] = update; + } + }); + } + + let itemIndex = 0; + for await (const env of iterator) { + // We can't just push because updates might get emitted early. + if (envs[itemIndex] === undefined) { + envs[itemIndex] = env; + } + itemIndex += 1; + } + await updatesDone.promise; + + // Do not return invalid environments + return envs.filter((e) => e !== undefined).map((e) => e!); +} diff --git a/src/client/pythonEnvironments/base/locators.ts b/src/client/pythonEnvironments/base/locators.ts new file mode 100644 index 000000000000..10be15c27bf1 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { chain } from '../../common/utils/async'; +import { Disposables } from '../../common/utils/resourceLifecycle'; +import { PythonEnvInfo } from './info'; +import { + ICompositeLocator, + ILocator, + IPythonEnvsIterator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from './locator'; +import { PythonEnvsWatchers } from './watchers'; + +/** + * Combine the `onUpdated` event of the given iterators into a single event. + */ +export function combineIterators(iterators: IPythonEnvsIterator[]): IPythonEnvsIterator { + const result: IPythonEnvsIterator = chain(iterators); + const events = iterators.map((it) => it.onUpdated).filter((v) => v); + if (!events || events.length === 0) { + // There are no sub-events, so we leave `onUpdated` undefined. + return result; + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result.onUpdated = (handleEvent: (e: PythonEnvUpdatedEvent | ProgressNotificationEvent) => any) => { + const disposables = new Disposables(); + let numActive = events.length; + events.forEach((event) => { + const disposable = event!((e: PythonEnvUpdatedEvent | ProgressNotificationEvent) => { + // NOSONAR + if (isProgressEvent(e)) { + if (e.stage === ProgressReportStage.discoveryFinished) { + numActive -= 1; + if (numActive === 0) { + // All the sub-events are done so we're done. + handleEvent({ stage: ProgressReportStage.discoveryFinished }); + } + } else { + handleEvent({ stage: e.stage }); + } + } else { + handleEvent(e); + } + }); + disposables.push(disposable); + }); + return disposables; + }; + return result; +} + +/** + * A wrapper around a set of locators, exposing them as a single locator. + * + * Events and iterator results are combined. + */ +export class Locators extends PythonEnvsWatchers implements ICompositeLocator { + public readonly providerId: string; + + constructor( + // The locators will be watched as well as iterated. + private readonly locators: ReadonlyArray>, + ) { + super(locators); + this.providerId = locators.map((loc) => loc.providerId).join('+'); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators = this.locators.map((loc) => loc.iterEnvs(query)); + return combineIterators(iterators); + } +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts new file mode 100644 index 000000000000..ea0d63cd7552 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonFinder.ts @@ -0,0 +1,541 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, EventEmitter, Event, Uri } from 'vscode'; +import * as ch from 'child_process'; +import * as path from 'path'; +import * as rpc from 'vscode-jsonrpc/node'; +import { PassThrough } from 'stream'; +import * as fs from '../../../../common/platform/fs-paths'; +import { isWindows, getUserHomeDir } from '../../../../common/utils/platform'; +import { EXTENSION_ROOT_DIR } from '../../../../constants'; +import { createDeferred, createDeferredFrom } from '../../../../common/utils/async'; +import { DisposableBase, DisposableStore } from '../../../../common/utils/resourceLifecycle'; +import { noop } from '../../../../common/utils/misc'; +import { getConfiguration, getWorkspaceFolderPaths, isTrusted } from '../../../../common/vscodeApis/workspaceApis'; +import { CONDAPATH_SETTING_KEY } from '../../../common/environmentManagers/conda'; +import { VENVFOLDERS_SETTING_KEY, VENVPATH_SETTING_KEY } from '../lowLevel/customVirtualEnvLocator'; +import { createLogOutputChannel, showWarningMessage } from '../../../../common/vscodeApis/windowApis'; +import { sendNativeTelemetry, NativePythonTelemetry } from './nativePythonTelemetry'; +import { NativePythonEnvironmentKind } from './nativePythonUtils'; +import type { IExtensionContext } from '../../../../common/types'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; +import { traceError } from '../../../../logging'; +import { Common, PythonLocator } from '../../../../common/utils/localize'; +import { Commands } from '../../../../common/constants'; +import { executeCommand } from '../../../../common/vscodeApis/commandApis'; +import { getGlobalStorage, IPersistentStorage } from '../../../../common/persistentState'; + +const PYTHON_ENV_TOOLS_PATH = isWindows() + ? path.join(EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet.exe') + : path.join(EXTENSION_ROOT_DIR, 'python-env-tools', 'bin', 'pet'); + +const DONT_SHOW_SPAWN_ERROR_AGAIN = 'DONT_SHOW_NATIVE_FINDER_SPAWN_ERROR_AGAIN'; + +export interface NativeEnvInfo { + displayName?: string; + name?: string; + executable?: string; + kind?: NativePythonEnvironmentKind; + version?: string; + prefix?: string; + manager?: NativeEnvManagerInfo; + /** + * Path to the project directory when dealing with pipenv virtual environments. + */ + project?: string; + arch?: 'x64' | 'x86'; + symlinks?: string[]; +} + +export interface NativeEnvManagerInfo { + tool: string; + executable: string; + version?: string; +} + +export function isNativeEnvInfo(info: NativeEnvInfo | NativeEnvManagerInfo): info is NativeEnvInfo { + if ((info as NativeEnvManagerInfo).tool) { + return false; + } + return true; +} + +export type NativeCondaInfo = { + canSpawnConda: boolean; + userProvidedEnvFound?: boolean; + condaRcs: string[]; + envDirs: string[]; + environmentsTxt?: string; + environmentsTxtExists?: boolean; + environmentsFromTxt: string[]; +}; + +export interface NativePythonFinder extends Disposable { + /** + * Refresh the list of python environments. + * Returns an async iterable that can be used to iterate over the list of python environments. + * Internally this will take all of the current workspace folders and search for python environments. + * + * If a Uri is provided, then it will search for python environments in that location (ignoring workspaces). + * Uri can be a file or a folder. + * If a NativePythonEnvironmentKind is provided, then it will search for python environments of that kind (ignoring workspaces). + */ + refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable; + /** + * Will spawn the provided Python executable and return information about the environment. + * @param executable + */ + resolve(executable: string): Promise; + /** + * Used only for telemetry. + */ + getCondaInfo(): Promise; +} + +interface NativeLog { + level: string; + message: string; +} + +class NativePythonFinderImpl extends DisposableBase implements NativePythonFinder { + private readonly connection: rpc.MessageConnection; + + private firstRefreshResults: undefined | (() => AsyncGenerator); + + private readonly outputChannel = this._register(createLogOutputChannel('Python Locator', { log: true })); + + private initialRefreshMetrics = { + timeToSpawn: 0, + timeToConfigure: 0, + timeToRefresh: 0, + }; + + private readonly suppressErrorNotification: IPersistentStorage; + + constructor(private readonly cacheDirectory?: Uri, private readonly context?: IExtensionContext) { + super(); + this.suppressErrorNotification = this.context + ? getGlobalStorage(this.context, DONT_SHOW_SPAWN_ERROR_AGAIN, false) + : ({ get: () => false, set: async () => {} } as IPersistentStorage); + this.connection = this.start(); + void this.configure(); + this.firstRefreshResults = this.refreshFirstTime(); + } + + public async resolve(executable: string): Promise { + await this.configure(); + const environment = await this.connection.sendRequest('resolve', { + executable, + }); + + this.outputChannel.info(`Resolved Python Environment ${environment.executable}`); + return environment; + } + + async *refresh(options?: NativePythonEnvironmentKind | Uri[]): AsyncIterable { + if (this.firstRefreshResults) { + // If this is the first time we are refreshing, + // Then get the results from the first refresh. + // Those would have started earlier and cached in memory. + const results = this.firstRefreshResults(); + this.firstRefreshResults = undefined; + yield* results; + } else { + const result = this.doRefresh(options); + let completed = false; + void result.completed.finally(() => { + completed = true; + }); + const envs: (NativeEnvInfo | NativeEnvManagerInfo)[] = []; + let discovered = createDeferred(); + const disposable = result.discovered((data) => { + envs.push(data); + discovered.resolve(); + }); + do { + if (!envs.length) { + await Promise.race([result.completed, discovered.promise]); + } + if (envs.length) { + const dataToSend = [...envs]; + envs.length = 0; + for (const data of dataToSend) { + yield data; + } + } + if (!completed) { + discovered = createDeferred(); + } + } while (!completed); + disposable.dispose(); + } + } + + refreshFirstTime() { + const result = this.doRefresh(); + const completed = createDeferredFrom(result.completed); + const envs: NativeEnvInfo[] = []; + let discovered = createDeferred(); + const disposable = result.discovered((data) => { + envs.push(data); + discovered.resolve(); + }); + + const iterable = async function* () { + do { + if (!envs.length) { + await Promise.race([completed.promise, discovered.promise]); + } + if (envs.length) { + const dataToSend = [...envs]; + envs.length = 0; + for (const data of dataToSend) { + yield data; + } + } + if (!completed.completed) { + discovered = createDeferred(); + } + } while (!completed.completed); + disposable.dispose(); + }; + + return iterable.bind(this); + } + + // eslint-disable-next-line class-methods-use-this + private start(): rpc.MessageConnection { + this.outputChannel.info(`Starting Python Locator ${PYTHON_ENV_TOOLS_PATH} server`); + + // jsonrpc package cannot handle messages coming through too quickly. + // Lets handle the messages and close the stream only when + // we have got the exit event. + const readable = new PassThrough(); + const writable = new PassThrough(); + const disposables: Disposable[] = []; + try { + const stopWatch = new StopWatch(); + const proc = ch.spawn(PYTHON_ENV_TOOLS_PATH, ['server'], { env: process.env }); + this.initialRefreshMetrics.timeToSpawn = stopWatch.elapsedTime; + proc.stdout.pipe(readable, { end: false }); + proc.stderr.on('data', (data) => this.outputChannel.error(data.toString())); + writable.pipe(proc.stdin, { end: false }); + + // Handle spawn errors (e.g., missing DLLs on Windows) + proc.on('error', (error) => { + this.outputChannel.error(`Python Locator process error: ${error.message}`); + this.outputChannel.error(`Error details: ${JSON.stringify(error)}`); + this.handleSpawnError(error.message); + }); + + // Handle immediate exits with error codes + let hasStarted = false; + setTimeout(() => { + hasStarted = true; + }, 1000); + + proc.on('exit', (code, signal) => { + if (!hasStarted && code !== null && code !== 0) { + const errorMessage = `Python Locator process exited immediately with code ${code}`; + this.outputChannel.error(errorMessage); + if (signal) { + this.outputChannel.error(`Exit signal: ${signal}`); + } + this.handleSpawnError(errorMessage); + } + }); + + disposables.push({ + dispose: () => { + try { + if (proc.exitCode === null) { + proc.kill(); + } + } catch (ex) { + this.outputChannel.error('Error disposing finder', ex); + } + }, + }); + } catch (ex) { + this.outputChannel.error(`Error starting Python Finder ${PYTHON_ENV_TOOLS_PATH} server`, ex); + } + const disposeStreams = new Disposable(() => { + readable.end(); + writable.end(); + }); + const connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(readable), + new rpc.StreamMessageWriter(writable), + ); + disposables.push( + connection, + disposeStreams, + connection.onError((ex) => { + disposeStreams.dispose(); + this.outputChannel.error('Connection Error:', ex); + }), + connection.onNotification('log', (data: NativeLog) => { + switch (data.level) { + case 'info': + this.outputChannel.info(data.message); + break; + case 'warning': + this.outputChannel.warn(data.message); + break; + case 'error': + this.outputChannel.error(data.message); + break; + case 'debug': + this.outputChannel.debug(data.message); + break; + default: + this.outputChannel.trace(data.message); + } + }), + connection.onNotification('telemetry', (data: NativePythonTelemetry) => + sendNativeTelemetry(data, this.initialRefreshMetrics), + ), + connection.onClose(() => { + disposables.forEach((d) => d.dispose()); + }), + ); + + connection.listen(); + this._register(Disposable.from(...disposables)); + return connection; + } + + private doRefresh( + options?: NativePythonEnvironmentKind | Uri[], + ): { completed: Promise; discovered: Event } { + const disposable = this._register(new DisposableStore()); + const discovered = disposable.add(new EventEmitter()); + const completed = createDeferred(); + const pendingPromises: Promise[] = []; + const stopWatch = new StopWatch(); + + const notifyUponCompletion = () => { + const initialCount = pendingPromises.length; + Promise.all(pendingPromises) + .then(() => { + if (initialCount === pendingPromises.length) { + completed.resolve(); + } else { + setTimeout(notifyUponCompletion, 0); + } + }) + .catch(noop); + }; + const trackPromiseAndNotifyOnCompletion = (promise: Promise) => { + pendingPromises.push(promise); + notifyUponCompletion(); + }; + + // Assumption is server will ensure there's only one refresh at a time. + // Perhaps we should have a request Id or the like to map the results back to the `refresh` request. + disposable.add( + this.connection.onNotification('environment', (data: NativeEnvInfo) => { + this.outputChannel.info(`Discovered env: ${data.executable || data.prefix}`); + // We know that in the Python extension if either Version of Prefix is not provided by locator + // Then we end up resolving the information. + // Lets do that here, + // This is a hack, as the other part of the code that resolves the version information + // doesn't work as expected, as its still a WIP. + if (data.executable && (!data.version || !data.prefix)) { + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + // HACK = TEMPORARY WORK AROUND, TO GET STUFF WORKING + const promise = this.connection + .sendRequest('resolve', { + executable: data.executable, + }) + .then((environment) => { + this.outputChannel.info(`Resolved ${environment.executable}`); + discovered.fire(environment); + }) + .catch((ex) => this.outputChannel.error(`Error in Resolving ${JSON.stringify(data)}`, ex)); + trackPromiseAndNotifyOnCompletion(promise); + } else { + discovered.fire(data); + } + }), + ); + disposable.add( + this.connection.onNotification('manager', (data: NativeEnvManagerInfo) => { + this.outputChannel.info(`Discovered manager: (${data.tool}) ${data.executable}`); + discovered.fire(data); + }), + ); + + type RefreshOptions = { + searchKind?: NativePythonEnvironmentKind; + searchPaths?: string[]; + }; + + const refreshOptions: RefreshOptions = {}; + if (options && Array.isArray(options) && options.length > 0) { + refreshOptions.searchPaths = options.map((item) => item.fsPath); + } else if (options && typeof options === 'string') { + refreshOptions.searchKind = options; + } + trackPromiseAndNotifyOnCompletion( + this.configure().then(() => + this.connection + .sendRequest<{ duration: number }>('refresh', refreshOptions) + .then(({ duration }) => { + this.outputChannel.info(`Refresh completed in ${duration}ms`); + this.initialRefreshMetrics.timeToRefresh = stopWatch.elapsedTime; + }) + .catch((ex) => this.outputChannel.error('Refresh error', ex)), + ), + ); + + completed.promise.finally(() => disposable.dispose()); + return { + completed: completed.promise, + discovered: discovered.event, + }; + } + + private lastConfiguration?: ConfigurationOptions; + + /** + * Configuration request, this must always be invoked before any other request. + * Must be invoked when ever there are changes to any data related to the configuration details. + */ + private async configure() { + const options: ConfigurationOptions = { + workspaceDirectories: getWorkspaceFolderPaths(), + // We do not want to mix this with `search_paths` + environmentDirectories: getCustomVirtualEnvDirs(), + condaExecutable: getPythonSettingAndUntildify(CONDAPATH_SETTING_KEY), + poetryExecutable: getPythonSettingAndUntildify('poetryPath'), + cacheDirectory: this.cacheDirectory?.fsPath, + }; + // No need to send a configuration request, is there are no changes. + if (JSON.stringify(options) === JSON.stringify(this.lastConfiguration || {})) { + return; + } + try { + const stopWatch = new StopWatch(); + this.lastConfiguration = options; + await this.connection.sendRequest('configure', options); + this.initialRefreshMetrics.timeToConfigure = stopWatch.elapsedTime; + } catch (ex) { + this.outputChannel.error('Refresh error', ex); + } + } + + async getCondaInfo(): Promise { + return this.connection.sendRequest('condaInfo'); + } + + private async handleSpawnError(errorMessage: string): Promise { + // Check if user has chosen to not see this error again + if (this.suppressErrorNotification.get()) { + return; + } + + // Check for Windows runtime DLL issues + if (isWindows() && errorMessage.toLowerCase().includes('vcruntime')) { + this.outputChannel.error(PythonLocator.windowsRuntimeMissing); + } else if (isWindows()) { + this.outputChannel.error(PythonLocator.windowsStartupFailed); + } + + // Show notification to user + const selection = await showWarningMessage( + PythonLocator.startupFailedNotification, + Common.openOutputPanel, + Common.doNotShowAgain, + ); + + if (selection === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } else if (selection === Common.doNotShowAgain) { + await this.suppressErrorNotification.set(true); + } + } +} + +type ConfigurationOptions = { + workspaceDirectories: string[]; + /** + * Place where virtual envs and the like are stored + * Should not contain workspace folders. + */ + environmentDirectories: string[]; + condaExecutable: string | undefined; + poetryExecutable: string | undefined; + cacheDirectory?: string; +}; +/** + * Gets all custom virtual environment locations to look for environments. + */ +function getCustomVirtualEnvDirs(): string[] { + const venvDirs: string[] = []; + const venvPath = getPythonSettingAndUntildify(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSettingAndUntildify(VENVFOLDERS_SETTING_KEY) ?? []; + const homeDir = getUserHomeDir(); + if (homeDir) { + venvFolders + .map((item) => (item.startsWith(homeDir) ? item : path.join(homeDir, item))) + .forEach((d) => venvDirs.push(d)); + venvFolders.forEach((item) => venvDirs.push(untildify(item))); + } + return Array.from(new Set(venvDirs)); +} + +function getPythonSettingAndUntildify(name: string, scope?: Uri): T | undefined { + const value = getConfiguration('python', scope).get(name); + if (typeof value === 'string') { + return value ? ((untildify(value as string) as unknown) as T) : undefined; + } + return value; +} + +let _finder: NativePythonFinder | undefined; +export function getNativePythonFinder(context?: IExtensionContext): NativePythonFinder { + if (!isTrusted()) { + return { + async *refresh() { + traceError('Python discovery not supported in untrusted workspace'); + yield* []; + }, + async resolve() { + traceError('Python discovery not supported in untrusted workspace'); + return {}; + }, + async getCondaInfo() { + traceError('Python discovery not supported in untrusted workspace'); + return ({} as unknown) as NativeCondaInfo; + }, + dispose() { + // do nothing + }, + }; + } + if (!_finder) { + const cacheDirectory = context ? getCacheDirectory(context) : undefined; + _finder = new NativePythonFinderImpl(cacheDirectory, context); + if (context) { + context.subscriptions.push(_finder); + } + } + return _finder; +} + +export function getCacheDirectory(context: IExtensionContext): Uri { + return Uri.joinPath(context.globalStorageUri, 'pythonLocator'); +} + +export async function clearCacheDirectory(context: IExtensionContext): Promise { + const cacheDirectory = getCacheDirectory(context); + await fs.emptyDir(cacheDirectory.fsPath).catch(noop); +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts new file mode 100644 index 000000000000..703fdfca01c3 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonTelemetry.ts @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceError } from '../../../../logging'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; + +export type NativePythonTelemetry = MissingCondaEnvironments | MissingPoetryEnvironments | RefreshPerformance; + +export type MissingCondaEnvironments = { + event: 'MissingCondaEnvironments'; + data: { + missingCondaEnvironments: { + missing: number; + envDirsNotFound?: number; + userProvidedCondaExe?: boolean; + rootPrefixNotFound?: boolean; + condaPrefixNotFound?: boolean; + condaManagerNotFound?: boolean; + sysRcNotFound?: boolean; + userRcNotFound?: boolean; + otherRcNotFound?: boolean; + missingEnvDirsFromSysRc?: number; + missingEnvDirsFromUserRc?: number; + missingEnvDirsFromOtherRc?: number; + missingFromSysRcEnvDirs?: number; + missingFromUserRcEnvDirs?: number; + missingFromOtherRcEnvDirs?: number; + }; + }; +}; + +export type MissingPoetryEnvironments = { + event: 'MissingPoetryEnvironments'; + data: { + missingPoetryEnvironments: { + missing: number; + missingInPath: number; + userProvidedPoetryExe?: boolean; + poetryExeNotFound?: boolean; + globalConfigNotFound?: boolean; + cacheDirNotFound?: boolean; + cacheDirIsDifferent?: boolean; + virtualenvsPathNotFound?: boolean; + virtualenvsPathIsDifferent?: boolean; + inProjectIsDifferent?: boolean; + }; + }; +}; + +export type RefreshPerformance = { + event: 'RefreshPerformance'; + data: { + refreshPerformance: { + total: number; + breakdown: { + Locators: number; + Path: number; + GlobalVirtualEnvs: number; + Workspaces: number; + }; + locators: { + Conda?: number; + Homebrew?: number; + LinuxGlobalPython?: number; + MacCmdLineTools?: number; + MacPythonOrg?: number; + MacXCode?: number; + PipEnv?: number; + PixiEnv?: number; + Poetry?: number; + PyEnv?: number; + Venv?: number; + VirtualEnv?: number; + VirtualEnvWrapper?: number; + WindowsRegistry?: number; + WindowsStore?: number; + }; + }; + }; +}; + +let refreshTelemetrySent = false; + +export function sendNativeTelemetry( + data: NativePythonTelemetry, + initialRefreshMetrics: { + timeToSpawn: number; + timeToConfigure: number; + timeToRefresh: number; + }, +): void { + switch (data.event) { + case 'MissingCondaEnvironments': { + sendTelemetryEvent( + EventName.NATIVE_FINDER_MISSING_CONDA_ENVS, + undefined, + data.data.missingCondaEnvironments, + ); + break; + } + case 'MissingPoetryEnvironments': { + sendTelemetryEvent( + EventName.NATIVE_FINDER_MISSING_POETRY_ENVS, + undefined, + data.data.missingPoetryEnvironments, + ); + break; + } + case 'RefreshPerformance': { + if (refreshTelemetrySent) { + break; + } + refreshTelemetrySent = true; + sendTelemetryEvent(EventName.NATIVE_FINDER_PERF, { + duration: data.data.refreshPerformance.total, + totalDuration: data.data.refreshPerformance.total, + breakdownGlobalVirtualEnvs: data.data.refreshPerformance.breakdown.GlobalVirtualEnvs, + breakdownLocators: data.data.refreshPerformance.breakdown.Locators, + breakdownPath: data.data.refreshPerformance.breakdown.Path, + breakdownWorkspaces: data.data.refreshPerformance.breakdown.Workspaces, + locatorConda: data.data.refreshPerformance.locators.Conda || 0, + locatorHomebrew: data.data.refreshPerformance.locators.Homebrew || 0, + locatorLinuxGlobalPython: data.data.refreshPerformance.locators.LinuxGlobalPython || 0, + locatorMacCmdLineTools: data.data.refreshPerformance.locators.MacCmdLineTools || 0, + locatorMacPythonOrg: data.data.refreshPerformance.locators.MacPythonOrg || 0, + locatorMacXCode: data.data.refreshPerformance.locators.MacXCode || 0, + locatorPipEnv: data.data.refreshPerformance.locators.PipEnv || 0, + locatorPixiEnv: data.data.refreshPerformance.locators.PixiEnv || 0, + locatorPoetry: data.data.refreshPerformance.locators.Poetry || 0, + locatorPyEnv: data.data.refreshPerformance.locators.PyEnv || 0, + locatorVenv: data.data.refreshPerformance.locators.Venv || 0, + locatorVirtualEnv: data.data.refreshPerformance.locators.VirtualEnv || 0, + locatorVirtualEnvWrapper: data.data.refreshPerformance.locators.VirtualEnvWrapper || 0, + locatorWindowsRegistry: data.data.refreshPerformance.locators.WindowsRegistry || 0, + locatorWindowsStore: data.data.refreshPerformance.locators.WindowsStore || 0, + timeToSpawn: initialRefreshMetrics.timeToSpawn, + timeToConfigure: initialRefreshMetrics.timeToConfigure, + timeToRefresh: initialRefreshMetrics.timeToRefresh, + }); + break; + } + default: { + traceError(`Unhandled Telemetry Event type ${JSON.stringify(data)}`); + } + } +} diff --git a/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts new file mode 100644 index 000000000000..716bdd444633 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/nativePythonUtils.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { LogOutputChannel } from 'vscode'; +import { PythonEnvKind } from '../../info'; +import { traceError } from '../../../../logging'; + +export enum NativePythonEnvironmentKind { + Conda = 'Conda', + Pixi = 'Pixi', + Homebrew = 'Homebrew', + Pyenv = 'Pyenv', + GlobalPaths = 'GlobalPaths', + PyenvVirtualEnv = 'PyenvVirtualEnv', + Pipenv = 'Pipenv', + Poetry = 'Poetry', + MacPythonOrg = 'MacPythonOrg', + MacCommandLineTools = 'MacCommandLineTools', + LinuxGlobal = 'LinuxGlobal', + MacXCode = 'MacXCode', + Venv = 'Venv', + VirtualEnv = 'VirtualEnv', + VirtualEnvWrapper = 'VirtualEnvWrapper', + WindowsStore = 'WindowsStore', + WindowsRegistry = 'WindowsRegistry', + VenvUv = 'Uv', +} + +const mapping = new Map([ + [NativePythonEnvironmentKind.Conda, PythonEnvKind.Conda], + [NativePythonEnvironmentKind.Pixi, PythonEnvKind.Pixi], + [NativePythonEnvironmentKind.GlobalPaths, PythonEnvKind.OtherGlobal], + [NativePythonEnvironmentKind.Pyenv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.PyenvVirtualEnv, PythonEnvKind.Pyenv], + [NativePythonEnvironmentKind.Pipenv, PythonEnvKind.Pipenv], + [NativePythonEnvironmentKind.Poetry, PythonEnvKind.Poetry], + [NativePythonEnvironmentKind.VirtualEnv, PythonEnvKind.VirtualEnv], + [NativePythonEnvironmentKind.VirtualEnvWrapper, PythonEnvKind.VirtualEnvWrapper], + [NativePythonEnvironmentKind.Venv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.VenvUv, PythonEnvKind.Venv], + [NativePythonEnvironmentKind.WindowsRegistry, PythonEnvKind.System], + [NativePythonEnvironmentKind.WindowsStore, PythonEnvKind.MicrosoftStore], + [NativePythonEnvironmentKind.Homebrew, PythonEnvKind.System], + [NativePythonEnvironmentKind.LinuxGlobal, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacCommandLineTools, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacPythonOrg, PythonEnvKind.System], + [NativePythonEnvironmentKind.MacXCode, PythonEnvKind.System], +]); + +export function categoryToKind(category?: NativePythonEnvironmentKind, logger?: LogOutputChannel): PythonEnvKind { + if (!category) { + return PythonEnvKind.Unknown; + } + const kind = mapping.get(category); + if (kind) { + return kind; + } + + if (logger) { + logger.error(`Unknown Python Environment category '${category}' from Native Locator.`); + } else { + traceError(`Unknown Python Environment category '${category}' from Native Locator.`); + } + return PythonEnvKind.Unknown; +} diff --git a/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts b/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts new file mode 100644 index 000000000000..378a0d6c521e --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/pythonWatcher.ts @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Event, EventEmitter, GlobPattern, RelativePattern, Uri, WorkspaceFolder } from 'vscode'; +import { createFileSystemWatcher, getWorkspaceFolder } from '../../../../common/vscodeApis/workspaceApis'; +import { isWindows } from '../../../../common/utils/platform'; +import { arePathsSame } from '../../../common/externalDependencies'; +import { FileChangeType } from '../../../../common/platform/fileSystemWatcher'; + +export interface PythonWorkspaceEnvEvent { + type: FileChangeType; + workspaceFolder: WorkspaceFolder; + executable: string; +} + +export interface PythonGlobalEnvEvent { + type: FileChangeType; + uri: Uri; +} + +export interface PythonWatcher extends Disposable { + watchWorkspace(wf: WorkspaceFolder): void; + unwatchWorkspace(wf: WorkspaceFolder): void; + onDidWorkspaceEnvChanged: Event; + + watchPath(uri: Uri, pattern?: string): void; + unwatchPath(uri: Uri): void; + onDidGlobalEnvChanged: Event; +} + +/* + * The pattern to search for python executables in the workspace. + * project + * ├── python or python.exe <--- This is what we are looking for. + * ├── .conda + * │ └── python or python.exe <--- This is what we are looking for. + * └── .venv + * │ └── Scripts or bin + * │ └── python or python.exe <--- This is what we are looking for. + */ +const WORKSPACE_PATTERN = isWindows() ? '**/python.exe' : '**/python'; + +class PythonWatcherImpl implements PythonWatcher { + private disposables: Disposable[] = []; + + private readonly _onDidWorkspaceEnvChanged = new EventEmitter(); + + private readonly _onDidGlobalEnvChanged = new EventEmitter(); + + private readonly _disposeMap: Map = new Map(); + + constructor() { + this.disposables.push(this._onDidWorkspaceEnvChanged, this._onDidGlobalEnvChanged); + } + + onDidGlobalEnvChanged: Event = this._onDidGlobalEnvChanged.event; + + onDidWorkspaceEnvChanged: Event = this._onDidWorkspaceEnvChanged.event; + + watchWorkspace(wf: WorkspaceFolder): void { + if (this._disposeMap.has(wf.uri.fsPath)) { + const disposer = this._disposeMap.get(wf.uri.fsPath); + disposer?.dispose(); + } + + const disposables: Disposable[] = []; + const watcher = createFileSystemWatcher(new RelativePattern(wf, WORKSPACE_PATTERN)); + disposables.push( + watcher, + watcher.onDidChange((uri) => { + this.fireWorkspaceEvent(FileChangeType.Changed, wf, uri); + }), + watcher.onDidCreate((uri) => { + this.fireWorkspaceEvent(FileChangeType.Created, wf, uri); + }), + watcher.onDidDelete((uri) => { + this.fireWorkspaceEvent(FileChangeType.Deleted, wf, uri); + }), + ); + + const disposable = { + dispose: () => { + disposables.forEach((d) => d.dispose()); + this._disposeMap.delete(wf.uri.fsPath); + }, + }; + this._disposeMap.set(wf.uri.fsPath, disposable); + } + + unwatchWorkspace(wf: WorkspaceFolder): void { + const disposable = this._disposeMap.get(wf.uri.fsPath); + disposable?.dispose(); + } + + private fireWorkspaceEvent(type: FileChangeType, wf: WorkspaceFolder, uri: Uri) { + const uriWorkspace = getWorkspaceFolder(uri); + if (uriWorkspace && arePathsSame(uriWorkspace.uri.fsPath, wf.uri.fsPath)) { + this._onDidWorkspaceEnvChanged.fire({ type, workspaceFolder: wf, executable: uri.fsPath }); + } + } + + watchPath(uri: Uri, pattern?: string): void { + if (this._disposeMap.has(uri.fsPath)) { + const disposer = this._disposeMap.get(uri.fsPath); + disposer?.dispose(); + } + + const glob: GlobPattern = pattern ? new RelativePattern(uri, pattern) : uri.fsPath; + const disposables: Disposable[] = []; + const watcher = createFileSystemWatcher(glob); + disposables.push( + watcher, + watcher.onDidChange(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Changed, uri }); + }), + watcher.onDidCreate(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Created, uri }); + }), + watcher.onDidDelete(() => { + this._onDidGlobalEnvChanged.fire({ type: FileChangeType.Deleted, uri }); + }), + ); + + const disposable = { + dispose: () => { + disposables.forEach((d) => d.dispose()); + this._disposeMap.delete(uri.fsPath); + }, + }; + this._disposeMap.set(uri.fsPath, disposable); + } + + unwatchPath(uri: Uri): void { + const disposable = this._disposeMap.get(uri.fsPath); + disposable?.dispose(); + } + + dispose() { + this.disposables.forEach((d) => d.dispose()); + this._disposeMap.forEach((d) => d.dispose()); + } +} + +export function createPythonWatcher(): PythonWatcher { + return new PythonWatcherImpl(); +} diff --git a/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts new file mode 100644 index 000000000000..8b56b4c7b8c1 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/common/resourceBasedLocator.ts @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposable } from '../../../../common/types'; +import { createDeferred, Deferred } from '../../../../common/utils/async'; +import { Disposables } from '../../../../common/utils/resourceLifecycle'; +import { traceError, traceWarn } from '../../../../logging'; +import { arePathsSame, isVirtualWorkspace } from '../../../common/externalDependencies'; +import { getEnvPath } from '../../info/env'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator, PythonLocatorQuery } from '../../locator'; + +/** + * A base locator class that manages the lifecycle of resources. + * + * The resources are not initialized until needed. + * + * It is critical that each subclass properly add its resources + * to the list: + * + * this.disposables.push(someResource); + * + * Otherwise it will leak (and we have no leak detection). + */ +export abstract class LazyResourceBasedLocator extends Locator implements IDisposable { + protected readonly disposables = new Disposables(); + + // This will be set only once we have to create necessary resources + // and resolves once those resources are ready. + private resourcesReady?: Deferred; + + private watchersReady?: Deferred; + + /** + * This can be used to initialize resources when subclasses are created. + */ + protected async activate(): Promise { + await this.ensureResourcesReady(); + // There is not need to wait for the watchers to get started. + try { + this.ensureWatchersReady(); + } catch (ex) { + traceWarn(`Failed to ensure watchers are ready for locator ${this.constructor.name}`, ex); + } + } + + public async dispose(): Promise { + await this.disposables.dispose(); + } + + public async *iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + await this.activate(); + const iterator = this.doIterEnvs(query); + if (query?.envPath) { + let result = await iterator.next(); + while (!result.done) { + const currEnv = result.value; + const { path } = getEnvPath(currEnv.executablePath, currEnv.envPath); + if (arePathsSame(path, query.envPath)) { + yield currEnv; + break; + } + result = await iterator.next(); + } + } else { + yield* iterator; + } + } + + /** + * The subclass implementation of iterEnvs(). + */ + protected abstract doIterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator; + + /** + * This is where subclasses get their resources ready. + * + * It is only called once resources are needed. + * + * Each subclass is responsible to add its resources to the list + * (otherwise it leaks): + * + * this.disposables.push(someResource); + * + * Not all locators have resources other than watchers so a default + * implementation is provided. + */ + // eslint-disable-next-line class-methods-use-this + protected async initResources(): Promise { + // No resources! + } + + /** + * This is where subclasses get their watchers ready. + * + * It is only called with the first `iterEnvs()` call, + * after `initResources()` has been called. + * + * Each subclass is responsible to add its resources to the list + * (otherwise it leaks): + * + * this.disposables.push(someResource); + * + * Not all locators have watchers to init so a default + * implementation is provided. + */ + // eslint-disable-next-line class-methods-use-this + protected async initWatchers(): Promise { + // No watchers! + } + + protected async ensureResourcesReady(): Promise { + if (this.resourcesReady !== undefined) { + await this.resourcesReady.promise; + return; + } + this.resourcesReady = createDeferred(); + await this.initResources().catch((ex) => { + traceError(ex); + this.resourcesReady?.reject(ex); + }); + this.resourcesReady.resolve(); + } + + private async ensureWatchersReady(): Promise { + if (this.watchersReady !== undefined) { + await this.watchersReady.promise; + return; + } + this.watchersReady = createDeferred(); + + // Don't create any file watchers in a virtual workspace. + if (!isVirtualWorkspace()) { + await this.initWatchers().catch((ex) => { + traceError(ex); + this.watchersReady?.reject(ex); + }); + } + this.watchersReady.resolve(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts new file mode 100644 index 000000000000..456e8adfa9a4 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionCache.ts @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event } from 'vscode'; +import { isTestExecution } from '../../../../common/constants'; +import { traceVerbose } from '../../../../logging'; +import { arePathsSame, getFileInfo, pathExists } from '../../../common/externalDependencies'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { areEnvsDeepEqual, areSameEnv, getEnvPath } from '../../info/env'; +import { + BasicPythonEnvCollectionChangedEvent, + PythonEnvCollectionChangedEvent, + PythonEnvsWatcher, +} from '../../watcher'; +import { getCondaInterpreterPath } from '../../../common/environmentManagers/conda'; + +export interface IEnvsCollectionCache { + /** + * Return all environment info currently in memory for this session. + */ + getAllEnvs(): PythonEnvInfo[]; + + /** + * Updates environment in cache using the value provided. + * If no new value is provided, remove the existing value from cache. + */ + updateEnv(oldValue: PythonEnvInfo, newValue: PythonEnvInfo | undefined): void; + + /** + * Fires with details if the cache changes. + */ + onChanged: Event; + + /** + * Adds environment to cache. + */ + addEnv(env: PythonEnvInfo, hasLatestInfo?: boolean): void; + + /** + * Return cached environment information for a given path if it exists and + * is up to date, otherwise return `undefined`. + * + * @param path - Python executable path or path to environment + */ + getLatestInfo(path: string): Promise; + + /** + * Writes the content of the in-memory cache to persistent storage. It is assumed + * all envs have upto date info when this is called. + */ + flush(): Promise; + + /** + * Removes invalid envs from cache. Note this does not check for outdated info when + * validating cache. + * @param envs Carries list of envs for the latest refresh. + * @param isCompleteList Carries whether the list of envs is complete or not. + */ + validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise; +} + +interface IPersistentStorage { + get(): PythonEnvInfo[]; + store(envs: PythonEnvInfo[]): Promise; +} + +/** + * Environment info cache using persistent storage to save and retrieve pre-cached env info. + */ +export class PythonEnvInfoCache extends PythonEnvsWatcher + implements IEnvsCollectionCache { + private envs: PythonEnvInfo[] = []; + + /** + * Carries the list of envs which have been validated to have latest info. + */ + private validatedEnvs = new Set(); + + /** + * Carries the list of envs which have been flushed to persistent storage. + * It signifies that the env info is likely up-to-date. + */ + private flushedEnvs = new Set(); + + constructor(private readonly persistentStorage: IPersistentStorage) { + super(); + } + + public async validateCache(envs?: PythonEnvInfo[], isCompleteList?: boolean): Promise { + /** + * We do check if an env has updated as we already run discovery in background + * which means env cache will have up-to-date envs eventually. This also means + * we avoid the cost of running lstat. So simply remove envs which are no longer + * valid. + */ + const areEnvsValid = await Promise.all( + this.envs.map(async (cachedEnv) => { + const { path } = getEnvPath(cachedEnv.executable.filename, cachedEnv.location); + if (await pathExists(path)) { + if (envs && isCompleteList) { + /** + * Only consider a cached env to be valid if it's relevant. That means: + * * It is relevant for some other workspace folder which is not opened currently. + * * It is either reported in the latest complete discovery for this session. + * * It is provided by the consumer themselves. + */ + if (cachedEnv.searchLocation) { + return true; + } + if (envs.some((env) => cachedEnv.id === env.id)) { + return true; + } + if (Array.from(this.validatedEnvs.keys()).some((envId) => cachedEnv.id === envId)) { + // These envs are provided by the consumer themselves, consider them valid. + return true; + } + } else { + return true; + } + } + return false; + }), + ); + const invalidIndexes = areEnvsValid + .map((isValid, index) => (isValid ? -1 : index)) + .filter((i) => i !== -1) + .reverse(); // Reversed so indexes do not change when deleting + invalidIndexes.forEach((index) => { + const env = this.envs.splice(index, 1)[0]; + traceVerbose(`Removing invalid env from cache ${env.id}`); + this.fire({ old: env, new: undefined }); + }); + if (envs) { + // See if any env has updated after the last refresh and fire events. + envs.forEach((env) => { + const cachedEnv = this.envs.find((e) => e.id === env.id); + if (cachedEnv && !areEnvsDeepEqual(cachedEnv, env)) { + this.updateEnv(cachedEnv, env, true); + } + }); + } + } + + public getAllEnvs(): PythonEnvInfo[] { + return this.envs; + } + + public addEnv(env: PythonEnvInfo, hasLatestInfo?: boolean): void { + const found = this.envs.find((e) => areSameEnv(e, env)); + if (!found) { + this.envs.push(env); + this.fire({ new: env }); + } else if (hasLatestInfo && !this.validatedEnvs.has(env.id!)) { + // Update cache if we have latest info and the env is not already validated. + this.updateEnv(found, env, true); + } + if (hasLatestInfo) { + traceVerbose(`Flushing env to cache ${env.id}`); + this.validatedEnvs.add(env.id!); + this.flush(env).ignoreErrors(); // If we have latest info, flush it so it can be saved. + } + } + + public updateEnv(oldValue: PythonEnvInfo, newValue: PythonEnvInfo | undefined, forceUpdate = false): void { + if (this.flushedEnvs.has(oldValue.id!) && !forceUpdate) { + // We have already flushed this env to persistent storage, so it likely has upto date info. + // If we have latest info, then we do not need to update the cache. + return; + } + const index = this.envs.findIndex((e) => areSameEnv(e, oldValue)); + if (index !== -1) { + if (newValue === undefined) { + this.envs.splice(index, 1); + } else { + this.envs[index] = newValue; + } + this.fire({ old: oldValue, new: newValue }); + } + } + + public async getLatestInfo(path: string): Promise { + // `path` can either be path to environment or executable path + const env = this.envs.find((e) => arePathsSame(e.location, path)) ?? this.envs.find((e) => areSameEnv(e, path)); + if ( + env?.kind === PythonEnvKind.Conda && + getEnvPath(env.executable.filename, env.location).pathType === 'envFolderPath' + ) { + if (await pathExists(getCondaInterpreterPath(env.location))) { + // This is a conda env without python in cache which actually now has a valid python, so return + // `undefined` and delete value from cache as cached value is not the latest anymore. + this.validatedEnvs.delete(env.id!); + return undefined; + } + // Do not attempt to validate these envs as they lack an executable, and consider them as validated by default. + this.validatedEnvs.add(env.id!); + return env; + } + if (env) { + if (this.validatedEnvs.has(env.id!)) { + traceVerbose(`Found cached env for ${path}`); + return env; + } + if (await this.validateInfo(env)) { + traceVerbose(`Needed to validate ${path} with latest info`); + this.validatedEnvs.add(env.id!); + return env; + } + } + traceVerbose(`No cached env found for ${path}`); + return undefined; + } + + public clearAndReloadFromStorage(): void { + this.envs = this.persistentStorage.get(); + this.markAllEnvsAsFlushed(); + } + + public async flush(env?: PythonEnvInfo): Promise { + if (env) { + // Flush only the given env. + const envs = this.persistentStorage.get(); + const index = envs.findIndex((e) => e.id === env.id); + envs[index] = env; + this.flushedEnvs.add(env.id!); + await this.persistentStorage.store(envs); + return; + } + traceVerbose('Environments added to cache', JSON.stringify(this.envs)); + this.markAllEnvsAsFlushed(); + await this.persistentStorage.store(this.envs); + } + + private markAllEnvsAsFlushed(): void { + this.envs.forEach((e) => { + this.flushedEnvs.add(e.id!); + }); + } + + /** + * Ensure environment has complete and latest information. + */ + private async validateInfo(env: PythonEnvInfo) { + // Make sure any previously flushed information is upto date by ensuring environment did not change. + if (!this.flushedEnvs.has(env.id!)) { + // Any environment with complete information is flushed, so this env does not contain complete info. + return false; + } + if (env.version.micro === -1 || env.version.major === -1 || env.version.minor === -1) { + // Env should not contain incomplete versions. + return false; + } + const { ctime, mtime } = await getFileInfo(env.executable.filename); + if (ctime !== -1 && mtime !== -1 && ctime === env.executable.ctime && mtime === env.executable.mtime) { + return true; + } + env.executable.ctime = ctime; + env.executable.mtime = mtime; + return false; + } +} + +/** + * Build a cache of PythonEnvInfo that is ready to use. + */ +export async function createCollectionCache(storage: IPersistentStorage): Promise { + const cache = new PythonEnvInfoCache(storage); + cache.clearAndReloadFromStorage(); + await validateCache(cache); + return cache; +} + +async function validateCache(cache: PythonEnvInfoCache) { + if (isTestExecution()) { + // For purposes for test execution, block on validation so that we can determinally know when it finishes. + return cache.validateCache(); + } + // Validate in background so it doesn't block on returning the API object. + return cache.validateCache().ignoreErrors(); +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts new file mode 100644 index 000000000000..25ceb267da85 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsCollectionService.ts @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, EventEmitter } from 'vscode'; +import '../../../../common/extensions'; +import { createDeferred, Deferred } from '../../../../common/utils/async'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { sendTelemetryEvent } from '../../../../telemetry'; +import { EventName } from '../../../../telemetry/constants'; +import { normalizePath } from '../../../common/externalDependencies'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { getEnvPath } from '../../info/env'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + IResolvingLocator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from '../../locator'; +import { getQueryFilter } from '../../locatorUtils'; +import { PythonEnvCollectionChangedEvent, PythonEnvsWatcher } from '../../watcher'; +import { IEnvsCollectionCache } from './envsCollectionCache'; + +/** + * A service which maintains the collection of known environments. + */ +export class EnvsCollectionService extends PythonEnvsWatcher implements IDiscoveryAPI { + /** Keeps track of ongoing refreshes for various queries. */ + private refreshesPerQuery = new Map>(); + + /** Keeps track of scheduled refreshes other than the ongoing one for various queries. */ + private scheduledRefreshesPerQuery = new Map>(); + + /** Keeps track of promises which resolves when a stage has been reached */ + private progressPromises = new Map>(); + + /** Keeps track of whether a refresh has been triggered for various queries. */ + private hasRefreshFinishedForQuery = new Map(); + + private readonly progress = new EventEmitter(); + + public refreshState = ProgressReportStage.discoveryFinished; + + public get onProgress(): Event { + return this.progress.event; + } + + public getRefreshPromise(options?: GetRefreshEnvironmentsOptions): Promise | undefined { + const stage = options?.stage ?? ProgressReportStage.discoveryFinished; + return this.progressPromises.get(stage)?.promise; + } + + constructor( + private readonly cache: IEnvsCollectionCache, + private readonly locator: IResolvingLocator, + private readonly usingNativeLocator: boolean, + ) { + super(); + this.locator.onChanged((event) => { + const query: PythonLocatorQuery | undefined = event.providerId + ? { providerId: event.providerId, envPath: event.envPath } + : undefined; // We can also form a query based on the event, but skip that for simplicity. + let scheduledRefresh = this.scheduledRefreshesPerQuery.get(query); + // If there is no refresh scheduled for the query, start a new one. + if (!scheduledRefresh) { + scheduledRefresh = this.scheduleNewRefresh(query); + } + scheduledRefresh.then(() => { + // Once refresh of cache is complete, notify changes. + this.fire(event); + }); + }); + this.cache.onChanged((e) => { + this.fire(e); + }); + this.onProgress((event) => { + this.refreshState = event.stage; + // Resolve progress promise indicating the stage has been reached. + this.progressPromises.get(event.stage)?.resolve(); + this.progressPromises.delete(event.stage); + }); + } + + public async resolveEnv(path: string): Promise { + path = normalizePath(path); + // Note cache may have incomplete info when a refresh is happening. + // This API is supposed to return complete info by definition, so + // only use cache if it has complete info on an environment. + const cachedEnv = await this.cache.getLatestInfo(path); + if (cachedEnv) { + return cachedEnv; + } + const resolved = await this.locator.resolveEnv(path).catch((ex) => { + traceError(`Failed to resolve ${path}`, ex); + return undefined; + }); + traceVerbose(`Resolved ${path} using downstream locator`); + if (resolved) { + this.cache.addEnv(resolved, true); + } + return resolved; + } + + public getEnvs(query?: PythonLocatorQuery): PythonEnvInfo[] { + const cachedEnvs = this.cache.getAllEnvs(); + return query ? cachedEnvs.filter(getQueryFilter(query)) : cachedEnvs; + } + + public triggerRefresh(query?: PythonLocatorQuery, options?: TriggerRefreshOptions): Promise { + let refreshPromise = this.getRefreshPromiseForQuery(query); + if (!refreshPromise) { + if (options?.ifNotTriggerredAlready && this.hasRefreshFinished(query)) { + // Do not trigger another refresh if a refresh has previously finished. + return Promise.resolve(); + } + const stopWatch = new StopWatch(); + traceInfo(`Starting Environment refresh`); + refreshPromise = this.startRefresh(query).then(() => { + this.sendTelemetry(query, stopWatch); + traceInfo(`Environment refresh took ${stopWatch.elapsedTime} milliseconds`); + }); + } + return refreshPromise; + } + + private startRefresh(query: PythonLocatorQuery | undefined): Promise { + this.createProgressStates(query); + const promise = this.addEnvsToCacheForQuery(query); + return promise + .then(async () => { + this.resolveProgressStates(query); + }) + .catch((ex) => { + this.rejectProgressStates(query, ex); + }); + } + + private async addEnvsToCacheForQuery(query: PythonLocatorQuery | undefined) { + const iterator = this.locator.iterEnvs(query); + const seen: PythonEnvInfo[] = []; + const state = { + done: false, + pending: 0, + }; + const updatesDone = createDeferred(); + const stopWatch = new StopWatch(); + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated(async (event) => { + if (isProgressEvent(event)) { + switch (event.stage) { + case ProgressReportStage.discoveryFinished: + state.done = true; + listener.dispose(); + traceInfo(`Environments refresh finished (event): ${stopWatch.elapsedTime} milliseconds`); + break; + case ProgressReportStage.allPathsDiscovered: + if (!query) { + traceInfo( + `Environments refresh paths discovered (event): ${stopWatch.elapsedTime} milliseconds`, + ); + // Only mark as all paths discovered when querying for all envs. + this.progress.fire(event); + } + break; + default: + this.progress.fire(event); + } + } else if (event.index !== undefined) { + state.pending += 1; + this.cache.updateEnv(seen[event.index], event.update); + if (event.update) { + seen[event.index] = event.update; + } + state.pending -= 1; + } + if (state.done && state.pending === 0) { + updatesDone.resolve(); + } + }); + } else { + this.progress.fire({ stage: ProgressReportStage.discoveryStarted }); + updatesDone.resolve(); + } + + for await (const env of iterator) { + seen.push(env); + this.cache.addEnv(env); + } + traceInfo(`Environments refresh paths discovered: ${stopWatch.elapsedTime} milliseconds`); + await updatesDone.promise; + // If query for all envs is done, `seen` should contain the list of all envs. + await this.cache.validateCache(seen, query === undefined); + this.cache.flush().ignoreErrors(); + } + + /** + * See if we already have a refresh promise for the query going on and return it. + */ + private getRefreshPromiseForQuery(query?: PythonLocatorQuery) { + // Even if no refresh is running for this exact query, there might be other + // refreshes running for a superset of this query. For eg. the `undefined` query + // is a superset for every other query, only consider that for simplicity. + return this.refreshesPerQuery.get(query)?.promise ?? this.refreshesPerQuery.get(undefined)?.promise; + } + + private hasRefreshFinished(query?: PythonLocatorQuery) { + return this.hasRefreshFinishedForQuery.get(query) ?? this.hasRefreshFinishedForQuery.get(undefined); + } + + /** + * Ensure we trigger a fresh refresh for the query after the current refresh (if any) is done. + */ + private async scheduleNewRefresh(query?: PythonLocatorQuery): Promise { + const refreshPromise = this.getRefreshPromiseForQuery(query); + let nextRefreshPromise: Promise; + if (!refreshPromise) { + nextRefreshPromise = this.startRefresh(query); + } else { + nextRefreshPromise = refreshPromise.then(() => { + // No more scheduled refreshes for this query as we're about to start the scheduled one. + this.scheduledRefreshesPerQuery.delete(query); + this.startRefresh(query); + }); + this.scheduledRefreshesPerQuery.set(query, nextRefreshPromise); + } + return nextRefreshPromise; + } + + private createProgressStates(query: PythonLocatorQuery | undefined) { + this.refreshesPerQuery.set(query, createDeferred()); + Object.values(ProgressReportStage).forEach((stage) => { + this.progressPromises.set(stage, createDeferred()); + }); + if (ProgressReportStage.allPathsDiscovered && query) { + // Only mark as all paths discovered when querying for all envs. + this.progressPromises.delete(ProgressReportStage.allPathsDiscovered); + } + } + + private rejectProgressStates(query: PythonLocatorQuery | undefined, ex: Error) { + this.refreshesPerQuery.get(query)?.reject(ex); + this.refreshesPerQuery.delete(query); + Object.values(ProgressReportStage).forEach((stage) => { + this.progressPromises.get(stage)?.reject(ex); + this.progressPromises.delete(stage); + }); + } + + private resolveProgressStates(query: PythonLocatorQuery | undefined) { + this.refreshesPerQuery.get(query)?.resolve(); + this.refreshesPerQuery.delete(query); + // Refreshes per stage are resolved using progress events instead. + const isRefreshComplete = Array.from(this.refreshesPerQuery.values()).every((d) => d.completed); + if (isRefreshComplete) { + this.progress.fire({ stage: ProgressReportStage.discoveryFinished }); + } + } + + private sendTelemetry(query: PythonLocatorQuery | undefined, stopWatch: StopWatch) { + if (!query && !this.hasRefreshFinished(query)) { + const envs = this.cache.getAllEnvs(); + const environmentsWithoutPython = envs.filter( + (e) => getEnvPath(e.executable.filename, e.location).pathType === 'envFolderPath', + ).length; + const activeStateEnvs = envs.filter((e) => e.kind === PythonEnvKind.ActiveState).length; + const condaEnvs = envs.filter((e) => e.kind === PythonEnvKind.Conda).length; + const customEnvs = envs.filter((e) => e.kind === PythonEnvKind.Custom).length; + const hatchEnvs = envs.filter((e) => e.kind === PythonEnvKind.Hatch).length; + const microsoftStoreEnvs = envs.filter((e) => e.kind === PythonEnvKind.MicrosoftStore).length; + const otherGlobalEnvs = envs.filter((e) => e.kind === PythonEnvKind.OtherGlobal).length; + const otherVirtualEnvs = envs.filter((e) => e.kind === PythonEnvKind.OtherVirtual).length; + const pipEnvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Pipenv).length; + const poetryEnvs = envs.filter((e) => e.kind === PythonEnvKind.Poetry).length; + const pyenvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Pyenv).length; + const systemEnvs = envs.filter((e) => e.kind === PythonEnvKind.System).length; + const unknownEnvs = envs.filter((e) => e.kind === PythonEnvKind.Unknown).length; + const venvEnvs = envs.filter((e) => e.kind === PythonEnvKind.Venv).length; + const virtualEnvEnvs = envs.filter((e) => e.kind === PythonEnvKind.VirtualEnv).length; + const virtualEnvWrapperEnvs = envs.filter((e) => e.kind === PythonEnvKind.VirtualEnvWrapper).length; + + // Intent is to capture time taken for discovery of all envs to complete the first time. + sendTelemetryEvent(EventName.PYTHON_INTERPRETER_DISCOVERY, stopWatch.elapsedTime, { + interpreters: this.cache.getAllEnvs().length, + usingNativeLocator: this.usingNativeLocator, + environmentsWithoutPython, + activeStateEnvs, + condaEnvs, + customEnvs, + hatchEnvs, + microsoftStoreEnvs, + otherGlobalEnvs, + otherVirtualEnvs, + pipEnvEnvs, + poetryEnvs, + pyenvEnvs, + systemEnvs, + unknownEnvs, + venvEnvs, + virtualEnvEnvs, + virtualEnvWrapperEnvs, + }); + } + this.hasRefreshFinishedForQuery.set(query, true); + } +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts new file mode 100644 index 000000000000..c3a523b2d086 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsReducer.ts @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep, isEqual, uniq } from 'lodash'; +import { Event, EventEmitter, Uri } from 'vscode'; +import { traceVerbose } from '../../../../logging'; +import { isParentPath } from '../../../common/externalDependencies'; +import { PythonEnvKind } from '../../info'; +import { areSameEnv } from '../../info/env'; +import { getPrioritizedEnvKinds } from '../../info/envKind'; +import { + BasicEnvInfo, + ICompositeLocator, + ILocator, + IPythonEnvsIterator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../locator'; +import { PythonEnvsChangedEvent } from '../../watcher'; + +/** + * Combines duplicate environments received from the incoming locator into one and passes on unique environments + */ +export class PythonEnvsReducer implements ICompositeLocator { + public get onChanged(): Event { + return this.parentLocator.onChanged; + } + + constructor(private readonly parentLocator: ILocator) {} + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const didUpdate = new EventEmitter | ProgressNotificationEvent>(); + const incomingIterator = this.parentLocator.iterEnvs(query); + const iterator = iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } +} + +async function* iterEnvsIterator( + iterator: IPythonEnvsIterator, + didUpdate: EventEmitter | ProgressNotificationEvent>, +): IPythonEnvsIterator { + const state = { + done: false, + pending: 0, + }; + const seen: BasicEnvInfo[] = []; + + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated((event) => { + if (isProgressEvent(event)) { + if (event.stage === ProgressReportStage.discoveryFinished) { + state.done = true; + listener.dispose(); + } else { + didUpdate.fire(event); + } + } else if (event.update === undefined) { + throw new Error( + 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in reducer', + ); + } else if (event.index !== undefined && seen[event.index] !== undefined) { + const oldEnv = seen[event.index]; + seen[event.index] = event.update; + didUpdate.fire({ index: event.index, old: oldEnv, update: event.update }); + } else { + // This implies a problem in a downstream locator + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + }); + } else { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + } + + let result = await iterator.next(); + while (!result.done) { + const currEnv = result.value; + const oldIndex = seen.findIndex((s) => areSameEnv(s, currEnv)); + if (oldIndex !== -1) { + resolveDifferencesInBackground(oldIndex, currEnv, state, didUpdate, seen).ignoreErrors(); + } else { + // We haven't yielded a matching env so yield this one as-is. + yield currEnv; + seen.push(currEnv); + } + result = await iterator.next(); + } + if (iterator.onUpdated === undefined) { + state.done = true; + checkIfFinishedAndNotify(state, didUpdate); + } +} + +async function resolveDifferencesInBackground( + oldIndex: number, + newEnv: BasicEnvInfo, + state: { done: boolean; pending: number }, + didUpdate: EventEmitter | ProgressNotificationEvent>, + seen: BasicEnvInfo[], +) { + state.pending += 1; + // It's essential we increment the pending call count before any asynchronus calls in this method. + // We want this to be run even when `resolveInBackground` is called in background. + const oldEnv = seen[oldIndex]; + const merged = resolveEnvCollision(oldEnv, newEnv); + if (!isEqual(oldEnv, merged)) { + seen[oldIndex] = merged; + didUpdate.fire({ index: oldIndex, old: oldEnv, update: merged }); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); +} + +/** + * When all info from incoming iterator has been received and all background calls finishes, notify that we're done + * @param state Carries the current state of progress + * @param didUpdate Used to notify when finished + */ +function checkIfFinishedAndNotify( + state: { done: boolean; pending: number }, + didUpdate: EventEmitter | ProgressNotificationEvent>, +) { + if (state.done && state.pending === 0) { + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + didUpdate.dispose(); + traceVerbose(`Finished with environment reducer`); + } +} + +function resolveEnvCollision(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): BasicEnvInfo { + const [env] = sortEnvInfoByPriority(oldEnv, newEnv); + const merged = cloneDeep(env); + merged.source = uniq((oldEnv.source ?? []).concat(newEnv.source ?? [])); + merged.searchLocation = getMergedSearchLocation(oldEnv, newEnv); + return merged; +} + +function getMergedSearchLocation(oldEnv: BasicEnvInfo, newEnv: BasicEnvInfo): Uri | undefined { + if (oldEnv.searchLocation && newEnv.searchLocation) { + // Choose the deeper project path of the two, as that can be used to signify + // that the environment is related to both the projects. + if (isParentPath(oldEnv.searchLocation.fsPath, newEnv.searchLocation.fsPath)) { + return oldEnv.searchLocation; + } + if (isParentPath(newEnv.searchLocation.fsPath, oldEnv.searchLocation.fsPath)) { + return newEnv.searchLocation; + } + } + return oldEnv.searchLocation ?? newEnv.searchLocation; +} + +/** + * Selects an environment based on the environment selection priority. This should + * match the priority in the environment identifier. + */ +function sortEnvInfoByPriority(...envs: BasicEnvInfo[]): BasicEnvInfo[] { + // TODO: When we consolidate the PythonEnvKind and EnvironmentType we should have + // one location where we define priority. + const envKindByPriority: PythonEnvKind[] = getPrioritizedEnvKinds(); + return envs.sort( + (a: BasicEnvInfo, b: BasicEnvInfo) => envKindByPriority.indexOf(a.kind) - envKindByPriority.indexOf(b.kind), + ); +} diff --git a/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts new file mode 100644 index 000000000000..6bd342d14d9c --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/envsResolver.ts @@ -0,0 +1,238 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { cloneDeep } from 'lodash'; +import { Event, EventEmitter } from 'vscode'; +import { isIdentifierRegistered, identifyEnvironment } from '../../../common/environmentIdentifier'; +import { IEnvironmentInfoService } from '../../info/environmentInfoService'; +import { PythonEnvInfo, PythonEnvKind } from '../../info'; +import { getEnvPath, setEnvDisplayString } from '../../info/env'; +import { InterpreterInformation } from '../../info/interpreter'; +import { + BasicEnvInfo, + ICompositeLocator, + IPythonEnvsIterator, + IResolvingLocator, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../locator'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { resolveBasicEnv } from './resolverUtils'; +import { traceVerbose, traceWarn } from '../../../../logging'; +import { getEnvironmentDirFromPath, getInterpreterPathFromDir, isPythonExecutable } from '../../../common/commonUtils'; +import { getEmptyVersion } from '../../info/pythonVersion'; + +/** + * Calls environment info service which runs `interpreterInfo.py` script on environments received + * from the parent locator. Uses information received to populate environments further and pass it on. + */ +export class PythonEnvsResolver implements IResolvingLocator { + public get onChanged(): Event { + return this.parentLocator.onChanged; + } + + constructor( + private readonly parentLocator: ICompositeLocator, + private readonly environmentInfoService: IEnvironmentInfoService, + ) { + this.parentLocator.onChanged((event) => { + if (event.type && event.searchLocation !== undefined) { + // We detect an environment changed, reset any stored info for it so it can be re-run. + this.environmentInfoService.resetInfo(event.searchLocation); + } + }); + } + + public async resolveEnv(path: string): Promise { + const [executablePath, envPath] = await getExecutablePathAndEnvPath(path); + path = executablePath.length ? executablePath : envPath; + const kind = await identifyEnvironment(path); + const environment = await resolveBasicEnv({ kind, executablePath, envPath }); + const info = await this.environmentInfoService.getEnvironmentInfo(environment); + traceVerbose( + `Environment resolver resolved ${path} for ${JSON.stringify(environment)} to ${JSON.stringify(info)}`, + ); + if (!info) { + return undefined; + } + return getResolvedEnv(info, environment); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const didUpdate = new EventEmitter(); + const incomingIterator = this.parentLocator.iterEnvs(query); + const iterator = this.iterEnvsIterator(incomingIterator, didUpdate); + iterator.onUpdated = didUpdate.event; + return iterator; + } + + private async *iterEnvsIterator( + iterator: IPythonEnvsIterator, + didUpdate: EventEmitter, + ): IPythonEnvsIterator { + const environmentKinds = new Map(); + const state = { + done: false, + pending: 0, + }; + const seen: PythonEnvInfo[] = []; + + if (iterator.onUpdated !== undefined) { + const listener = iterator.onUpdated(async (event) => { + state.pending += 1; + if (isProgressEvent(event)) { + if (event.stage === ProgressReportStage.discoveryFinished) { + didUpdate.fire({ stage: ProgressReportStage.allPathsDiscovered }); + state.done = true; + listener.dispose(); + } else { + didUpdate.fire(event); + } + } else if (event.update === undefined) { + throw new Error( + 'Unsupported behavior: `undefined` environment updates are not supported from downstream locators in resolver', + ); + } else if (event.index !== undefined && seen[event.index] !== undefined) { + const old = seen[event.index]; + await setKind(event.update, environmentKinds); + seen[event.index] = await resolveBasicEnv(event.update); + didUpdate.fire({ old, index: event.index, update: seen[event.index] }); + this.resolveInBackground(event.index, state, didUpdate, seen).ignoreErrors(); + } else { + // This implies a problem in a downstream locator + traceVerbose(`Expected already iterated env, got ${event.old} (#${event.index})`); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + }); + } else { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + } + + let result = await iterator.next(); + while (!result.done) { + // Use cache from the current refresh where possible. + await setKind(result.value, environmentKinds); + const currEnv = await resolveBasicEnv(result.value); + seen.push(currEnv); + yield currEnv; + this.resolveInBackground(seen.indexOf(currEnv), state, didUpdate, seen).ignoreErrors(); + result = await iterator.next(); + } + if (iterator.onUpdated === undefined) { + state.done = true; + checkIfFinishedAndNotify(state, didUpdate); + } + } + + private async resolveInBackground( + envIndex: number, + state: { done: boolean; pending: number }, + didUpdate: EventEmitter, + seen: PythonEnvInfo[], + ) { + state.pending += 1; + // It's essential we increment the pending call count before any asynchronus calls in this method. + // We want this to be run even when `resolveInBackground` is called in background. + const info = await this.environmentInfoService.getEnvironmentInfo(seen[envIndex]); + const old = seen[envIndex]; + if (info) { + const resolvedEnv = getResolvedEnv(info, seen[envIndex], old.identifiedUsingNativeLocator); + seen[envIndex] = resolvedEnv; + didUpdate.fire({ old, index: envIndex, update: resolvedEnv }); + } else { + // Send update that the environment is not valid. + didUpdate.fire({ old, index: envIndex, update: undefined }); + } + state.pending -= 1; + checkIfFinishedAndNotify(state, didUpdate); + } +} + +async function setKind(env: BasicEnvInfo, environmentKinds: Map) { + const { path } = getEnvPath(env.executablePath, env.envPath); + // For native locators, do not try to identify the environment kind. + // its already set by the native locator & thats accurate. + if (env.identifiedUsingNativeLocator) { + environmentKinds.set(path, env.kind); + return; + } + let kind = environmentKinds.get(path); + if (!kind) { + if (!isIdentifierRegistered(env.kind)) { + // If identifier is not registered, skip setting env kind. + return; + } + kind = await identifyEnvironment(path); + environmentKinds.set(path, kind); + } + env.kind = kind; +} + +/** + * When all info from incoming iterator has been received and all background calls finishes, notify that we're done + * @param state Carries the current state of progress + * @param didUpdate Used to notify when finished + */ +function checkIfFinishedAndNotify( + state: { done: boolean; pending: number }, + didUpdate: EventEmitter, +) { + if (state.done && state.pending === 0) { + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + didUpdate.dispose(); + traceVerbose(`Finished with environment resolver`); + } +} + +function getResolvedEnv( + interpreterInfo: InterpreterInformation, + environment: PythonEnvInfo, + identifiedUsingNativeLocator = false, +) { + // Deep copy into a new object + const resolvedEnv = cloneDeep(environment); + resolvedEnv.executable.sysPrefix = interpreterInfo.executable.sysPrefix; + const isEnvLackingPython = + getEnvPath(resolvedEnv.executable.filename, resolvedEnv.location).pathType === 'envFolderPath'; + // TODO: Shouldn't this only apply to conda, how else can we have an environment and not have Python in it? + // If thats the case, then this should be gated on environment.kind === PythonEnvKind.Conda + // For non-native do not blow away the versions returned by native locator. + // Windows Store and Home brew have exe and sysprefix in different locations, + // Thus above check is not valid for these envs. + if (isEnvLackingPython && environment.kind !== PythonEnvKind.MicrosoftStore && !identifiedUsingNativeLocator) { + // Install python later into these envs might change the version, which can be confusing for users. + // So avoid displaying any version until it is installed. + resolvedEnv.version = getEmptyVersion(); + } else { + resolvedEnv.version = interpreterInfo.version; + } + resolvedEnv.arch = interpreterInfo.arch; + // Display name should be set after all the properties as we need other properties to build display name. + setEnvDisplayString(resolvedEnv); + return resolvedEnv; +} + +async function getExecutablePathAndEnvPath(path: string) { + let executablePath: string; + let envPath: string; + const isPathAnExecutable = await isPythonExecutable(path).catch((ex) => { + traceWarn('Failed to check if', path, 'is an executable', ex); + // This could happen if the path doesn't exist on a file system, but + // it still maybe the case that it's a valid file when run using a + // shell, as shells may resolve the file extensions before running it, + // so assume it to be an executable. + return true; + }); + if (isPathAnExecutable) { + executablePath = path; + envPath = getEnvironmentDirFromPath(executablePath); + } else { + envPath = path; + executablePath = (await getInterpreterPathFromDir(envPath)) ?? ''; + } + return [executablePath, envPath]; +} diff --git a/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts new file mode 100644 index 000000000000..088ae9cc97c1 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/composite/resolverUtils.ts @@ -0,0 +1,377 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { uniq } from 'lodash'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + UNKNOWN_PYTHON_VERSION, + virtualEnvKinds, +} from '../../info'; +import { buildEnvInfo, comparePythonVersionSpecificity, setEnvDisplayString, getEnvID } from '../../info/env'; +import { getEnvironmentDirFromPath, getPythonVersionFromPath } from '../../../common/commonUtils'; +import { arePathsSame, getFileInfo, isParentPath } from '../../../common/externalDependencies'; +import { + AnacondaCompanyName, + Conda, + getCondaInterpreterPath, + getPythonVersionFromConda, + isCondaEnvironment, +} from '../../../common/environmentManagers/conda'; +import { getPyenvVersionsDir, parsePyenvVersion } from '../../../common/environmentManagers/pyenv'; +import { Architecture, getOSType, OSType } from '../../../../common/utils/platform'; +import { getPythonVersionFromPath as parsePythonVersionFromPath, parseVersion } from '../../info/pythonVersion'; +import { getRegistryInterpreters, getRegistryInterpretersSync } from '../../../common/windowsUtils'; +import { BasicEnvInfo } from '../../locator'; +import { parseVersionFromExecutable } from '../../info/executable'; +import { traceError, traceWarn } from '../../../../logging'; +import { isVirtualEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; +import { getWorkspaceFolderPaths } from '../../../../common/vscodeApis/workspaceApis'; +import { ActiveState } from '../../../common/environmentManagers/activestate'; + +function getResolvers(): Map Promise> { + const resolvers = new Map Promise>(); + Object.values(PythonEnvKind).forEach((k) => { + resolvers.set(k, resolveGloballyInstalledEnv); + }); + virtualEnvKinds.forEach((k) => { + resolvers.set(k, resolveSimpleEnv); + }); + resolvers.set(PythonEnvKind.Conda, resolveCondaEnv); + resolvers.set(PythonEnvKind.MicrosoftStore, resolveMicrosoftStoreEnv); + resolvers.set(PythonEnvKind.Pyenv, resolvePyenvEnv); + resolvers.set(PythonEnvKind.ActiveState, resolveActiveStateEnv); + return resolvers; +} + +/** + * Find as much info about the given Basic Python env as possible without running the + * executable and returns it. Notice `undefined` is never returned, so environment + * returned could still be invalid. + */ +export async function resolveBasicEnv(env: BasicEnvInfo): Promise { + const { kind, source, searchLocation } = env; + const resolvers = getResolvers(); + const resolverForKind = resolvers.get(kind)!; + const resolvedEnv = await resolverForKind(env); + resolvedEnv.searchLocation = getSearchLocation(resolvedEnv, searchLocation); + resolvedEnv.source = uniq(resolvedEnv.source.concat(source ?? [])); + if ( + !env.identifiedUsingNativeLocator && + getOSType() === OSType.Windows && + resolvedEnv.source?.includes(PythonEnvSource.WindowsRegistry) + ) { + // We can update env further using information we can get from the Windows registry. + await updateEnvUsingRegistry(resolvedEnv); + } + setEnvDisplayString(resolvedEnv); + if (env.arch && !resolvedEnv.arch) { + resolvedEnv.arch = env.arch; + } + if (env.ctime && env.mtime) { + resolvedEnv.executable.ctime = env.ctime; + resolvedEnv.executable.mtime = env.mtime; + } else { + const { ctime, mtime } = await getFileInfo(resolvedEnv.executable.filename); + resolvedEnv.executable.ctime = ctime; + resolvedEnv.executable.mtime = mtime; + } + if (!env.identifiedUsingNativeLocator) { + const type = await getEnvType(resolvedEnv); + if (type) { + resolvedEnv.type = type; + } + } + return resolvedEnv; +} + +async function getEnvType(env: PythonEnvInfo) { + if (env.type) { + return env.type; + } + if (await isVirtualEnvironment(env.executable.filename)) { + return PythonEnvType.Virtual; + } + if (await isCondaEnvironment(env.executable.filename)) { + return PythonEnvType.Conda; + } + return undefined; +} + +function getSearchLocation(env: PythonEnvInfo, searchLocation: Uri | undefined): Uri | undefined { + if (searchLocation) { + // A search location has already been established by the downstream locators, simply use that. + return searchLocation; + } + const folders = getWorkspaceFolderPaths(); + const isRootedEnv = folders.some((f) => isParentPath(env.executable.filename, f) || isParentPath(env.location, f)); + if (isRootedEnv) { + // For environments inside roots, we need to set search location so they can be queried accordingly. + // In certain usecases environment directory can itself be a root, for eg. `python -m venv .`. + // So choose folder to environment path to search for this env. + // + // |__ env <--- Default search location directory + // |__ bin or Scripts + // |__ python <--- executable + return Uri.file(env.location); + } + return undefined; +} + +async function updateEnvUsingRegistry(env: PythonEnvInfo): Promise { + // Environment source has already been identified as windows registry, so we expect windows registry + // cache to already be populated. Call sync function which relies on cache. + let interpreters = getRegistryInterpretersSync(); + if (!interpreters) { + traceError('Expected registry interpreter cache to be initialized already'); + interpreters = await getRegistryInterpreters(); + } + const data = interpreters.find((i) => arePathsSame(i.interpreterPath, env.executable.filename)); + if (data) { + const versionStr = data.versionStr ?? data.sysVersionStr ?? data.interpreterPath; + let version; + try { + version = parseVersion(versionStr); + } catch (ex) { + version = UNKNOWN_PYTHON_VERSION; + } + env.kind = env.kind === PythonEnvKind.Unknown ? PythonEnvKind.OtherGlobal : env.kind; + env.version = comparePythonVersionSpecificity(version, env.version) > 0 ? version : env.version; + env.distro.defaultDisplayName = data.companyDisplayName; + env.arch = data.bitnessStr === '32bit' ? Architecture.x86 : Architecture.x64; + env.distro.org = data.distroOrgName ?? env.distro.org; + env.source = uniq(env.source.concat(PythonEnvSource.WindowsRegistry)); + } else { + traceWarn('Expected registry to find the interpreter as source was set'); + } +} + +async function resolveGloballyInstalledEnv(env: BasicEnvInfo): Promise { + const { executablePath } = env; + let version; + try { + version = env.identifiedUsingNativeLocator ? env.version : parseVersionFromExecutable(executablePath); + } catch { + version = UNKNOWN_PYTHON_VERSION; + } + const envInfo = buildEnvInfo({ + kind: env.kind, + name: env.name, + display: env.displayName, + sysPrefix: env.envPath, + location: env.envPath, + searchLocation: env.searchLocation, + version, + executable: executablePath, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + }); + return envInfo; +} + +async function resolveSimpleEnv(env: BasicEnvInfo): Promise { + const { executablePath, kind } = env; + const envInfo = buildEnvInfo({ + kind, + version: env.identifiedUsingNativeLocator ? env.version : await getPythonVersionFromPath(executablePath), + executable: executablePath, + sysPrefix: env.envPath, + location: env.envPath, + display: env.displayName, + searchLocation: env.searchLocation, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + name: env.name, + type: PythonEnvType.Virtual, + }); + const location = env.envPath ?? getEnvironmentDirFromPath(executablePath); + envInfo.location = location; + envInfo.name = path.basename(location); + return envInfo; +} + +async function resolveCondaEnv(env: BasicEnvInfo): Promise { + if (env.identifiedUsingNativeLocator) { + // New approach using native locator. + const executable = env.executablePath; + const envPath = env.envPath ?? getEnvironmentDirFromPath(executable); + // TODO: Hacky, `executable` is never undefined in the typedef, + // However, in reality with native locator this can be undefined. + const version = env.version ?? (executable ? await getPythonVersionFromPath(executable) : undefined); + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location: envPath, + sysPrefix: envPath, + display: env.displayName, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + searchLocation: env.searchLocation, + source: [], + version, + type: PythonEnvType.Conda, + name: env.name, + }); + + if (env.envPath && executable && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } + return info; + } + + // Old approach (without native locator). + // In this approach we need to find conda. + const { executablePath } = env; + const conda = await Conda.getConda(); + if (conda === undefined) { + traceWarn(`${executablePath} identified as Conda environment even though Conda is not found`); + // Environment could still be valid, resolve as a simple env. + env.kind = PythonEnvKind.Unknown; + const envInfo = await resolveSimpleEnv(env); + envInfo.type = PythonEnvType.Conda; + // Assume it's a prefixed env by default because prefixed CLIs work even for named environments. + envInfo.name = ''; + return envInfo; + } + + const envPath = env.envPath ?? getEnvironmentDirFromPath(env.executablePath); + let executable: string; + if (env.executablePath.length > 0) { + executable = env.executablePath; + } else { + executable = await conda.getInterpreterPathForEnvironment({ prefix: envPath }); + } + const version = executable ? await getPythonVersionFromConda(executable) : undefined; + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location: envPath, + source: [], + version, + type: PythonEnvType.Conda, + name: env.name ?? (await conda?.getName(envPath)), + }); + + if (env.envPath && path.basename(executable) === executable) { + // For environments without python, set ID using the predicted executable path after python is installed. + // Another alternative could've been to set ID of all conda environments to the environment path, as that + // remains constant even after python installation. + const predictedExecutable = getCondaInterpreterPath(env.envPath); + info.id = getEnvID(predictedExecutable, env.envPath); + } + return info; +} + +async function resolvePyenvEnv(env: BasicEnvInfo): Promise { + const { executablePath } = env; + const location = env.envPath ?? getEnvironmentDirFromPath(executablePath); + const name = path.basename(location); + + // The sub-directory name sometimes can contain distro and python versions. + // here we attempt to extract the texts out of the name. + const versionStrings = parsePyenvVersion(name); + + const envInfo = buildEnvInfo({ + // If using native resolver, then we can get the kind from the native resolver. + // E.g. pyenv can have conda environments as well. + kind: env.identifiedUsingNativeLocator && env.kind ? env.kind : PythonEnvKind.Pyenv, + executable: executablePath, + source: [], + location, + searchLocation: env.searchLocation, + sysPrefix: env.envPath, + display: env.displayName, + name: env.name, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + // Pyenv environments can fall in to these three categories: + // 1. Global Installs : These are environments that are created when you install + // a supported python distribution using `pyenv install ` command. + // These behave similar to globally installed version of python or distribution. + // + // 2. Virtual Envs : These are environments that are created when you use + // `pyenv virtualenv `. These are similar to environments + // created using `python -m venv `. + // + // 3. Conda Envs : These are environments that are created when you use + // `pyenv virtualenv `. These are similar to + // environments created using `conda create -n . + // + // All these environments are fully handled by `pyenv` and should be activated using + // `pyenv local|global ` or `pyenv shell ` + // + // Here we look for near by files, or config files to see if we can get python version info + // without running python itself. + version: env.version ?? (await getPythonVersionFromPath(executablePath, versionStrings?.pythonVer)), + org: versionStrings && versionStrings.distro ? versionStrings.distro : '', + }); + + // Do this only for the old approach, when not using native locators. + if (!env.identifiedUsingNativeLocator) { + if (await isBaseCondaPyenvEnvironment(executablePath)) { + envInfo.name = 'base'; + } else { + envInfo.name = name; + } + } + return envInfo; +} + +async function resolveActiveStateEnv(env: BasicEnvInfo): Promise { + const info = buildEnvInfo({ + kind: env.kind, + executable: env.executablePath, + display: env.displayName, + version: env.version, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + location: env.envPath, + name: env.name, + searchLocation: env.searchLocation, + sysPrefix: env.envPath, + }); + const projects = await ActiveState.getState().then((v) => v?.getProjects()); + if (projects) { + for (const project of projects) { + for (const dir of project.executables) { + if (arePathsSame(dir, path.dirname(env.executablePath))) { + info.name = `${project.organization}/${project.name}`; + return info; + } + } + } + } + return info; +} + +async function isBaseCondaPyenvEnvironment(executablePath: string) { + if (!(await isCondaEnvironment(executablePath))) { + return false; + } + const location = getEnvironmentDirFromPath(executablePath); + const pyenvVersionDir = getPyenvVersionsDir(); + return arePathsSame(path.dirname(location), pyenvVersionDir); +} + +async function resolveMicrosoftStoreEnv(env: BasicEnvInfo): Promise { + const { executablePath } = env; + return buildEnvInfo({ + kind: PythonEnvKind.MicrosoftStore, + executable: executablePath, + version: env.version ?? parsePythonVersionFromPath(executablePath), + org: 'Microsoft', + display: env.displayName, + location: env.envPath, + sysPrefix: env.envPath, + searchLocation: env.searchLocation, + name: env.name, + identifiedUsingNativeLocator: env.identifiedUsingNativeLocator, + arch: Architecture.x64, + source: [PythonEnvSource.PathEnvVar], + }); +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts new file mode 100644 index 000000000000..3fbdacc639a5 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/activeStateLocator.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ActiveState } from '../../../common/environmentManagers/activestate'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { findInterpretersInDir } from '../../../common/commonUtils'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export class ActiveStateLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'activestate'; + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + const state = await ActiveState.getState(); + if (state === undefined) { + traceVerbose(`Couldn't locate the state binary.`); + return; + } + traceInfo(`Searching for active state environments`); + const projects = await state.getProjects(); + if (projects === undefined) { + traceVerbose(`Couldn't fetch State Tool projects.`); + return; + } + for (const project of projects) { + if (project.executables) { + for (const dir of project.executables) { + try { + traceVerbose(`Looking for Python in: ${project.name}`); + for await (const exe of findInterpretersInDir(dir)) { + traceVerbose(`Found Python executable: ${exe.filename}`); + yield { kind: PythonEnvKind.ActiveState, executablePath: exe.filename }; + } + } catch (ex) { + traceError(`Failed to process State Tool project: ${JSON.stringify(project)}`, ex); + } + } + } + } + traceInfo(`Finished searching for active state environments: ${stopWatch.elapsedTime} milliseconds`); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts new file mode 100644 index 000000000000..bb48ba75b9dd --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/condaLocator.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import '../../../../common/extensions'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { Conda, getCondaEnvironmentsTxt } from '../../../common/environmentManagers/conda'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export class CondaEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'conda-envs'; + + public constructor() { + super( + () => getCondaEnvironmentsTxt(), + async () => PythonEnvKind.Conda, + { isFile: true }, + ); + } + + // eslint-disable-next-line class-methods-use-this + public async *doIterEnvs(_: unknown): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for conda environments'); + const conda = await Conda.getConda(); + if (conda === undefined) { + traceVerbose(`Couldn't locate the conda binary.`); + return; + } + traceVerbose(`Searching for conda environments using ${conda.command}`); + + const envs = await conda.getEnvList(); + for (const env of envs) { + try { + traceVerbose(`Looking into conda env for executable: ${JSON.stringify(env)}`); + const executablePath = await conda.getInterpreterPathForEnvironment(env); + traceVerbose(`Found conda executable: ${executablePath}`); + yield { kind: PythonEnvKind.Conda, executablePath, envPath: env.prefix }; + } catch (ex) { + traceError(`Failed to process conda env: ${JSON.stringify(env)}`, ex); + } + } + traceInfo(`Finished searching for conda environments: ${stopWatch.elapsedTime} milliseconds`); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts new file mode 100644 index 000000000000..6aa83bbc376b --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { uniq } from 'lodash'; +import * as path from 'path'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getUserHomeDir } from '../../../../common/utils/platform'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; +import { getPythonSetting, onDidChangePythonSetting, pathExists } from '../../../common/externalDependencies'; +import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { + isVenvEnvironment, + isVirtualenvEnvironment, + isVirtualenvwrapperEnvironment, +} from '../../../common/environmentManagers/simplevirtualenvs'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; +/** + * Default number of levels of sub-directories to recurse when looking for interpreters. + */ +const DEFAULT_SEARCH_DEPTH = 2; + +export const VENVPATH_SETTING_KEY = 'venvPath'; +export const VENVFOLDERS_SETTING_KEY = 'venvFolders'; + +/** + * Gets all custom virtual environment locations to look for environments. + */ +async function getCustomVirtualEnvDirs(): Promise { + const venvDirs: string[] = []; + const venvPath = getPythonSetting(VENVPATH_SETTING_KEY); + if (venvPath) { + venvDirs.push(untildify(venvPath)); + } + const venvFolders = getPythonSetting(VENVFOLDERS_SETTING_KEY) ?? []; + const homeDir = getUserHomeDir(); + if (homeDir && (await pathExists(homeDir))) { + venvFolders + .map((item) => (item.startsWith(homeDir) ? item : path.join(homeDir, item))) + .forEach((d) => venvDirs.push(d)); + venvFolders.forEach((item) => venvDirs.push(untildify(item))); + } + return asyncFilter(uniq(venvDirs), pathExists); +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPipenvEnvironment(interpreterPath)) { + return PythonEnvKind.Pipenv; + } + + if (await isVirtualenvwrapperEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnvWrapper; + } + + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves custom virtual environments that users have provided. + */ +export class CustomVirtualEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'custom-virtual-envs'; + + constructor() { + super(getCustomVirtualEnvDirs, getVirtualEnvKind, { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. However even + // if the type detected is incorrect, it doesn't do any practical harm as kinds + // in this locator are used in the same way (same activation commands etc.) + delayOnCreated: 1000, + }); + } + + protected async initResources(): Promise { + this.disposables.push(onDidChangePythonSetting(VENVPATH_SETTING_KEY, () => this.fire())); + this.disposables.push(onDidChangePythonSetting(VENVFOLDERS_SETTING_KEY, () => this.fire())); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator() { + const stopWatch = new StopWatch(); + traceInfo('Searching for custom virtual environments'); + const envRootDirs = await getCustomVirtualEnvDirs(); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for custom virtual envs in: ${envRootDir}`); + + const executables = findInterpretersInDir(envRootDir, DEFAULT_SEARCH_DEPTH); + + for await (const entry of executables) { + const { filename } = entry; + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + if (await looksLikeBasicVirtualPython(entry)) { + try { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(filename); + yield { kind, executablePath: filename }; + traceVerbose(`Custom Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } else { + traceVerbose(`Custom Virtual Environment: [skipped] ${filename}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceInfo(`Finished searching for custom virtual envs: ${stopWatch.elapsedTime} milliseconds`); + } + + return iterator(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts new file mode 100644 index 000000000000..8a2b857d496a --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/customWorkspaceLocator.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { getPythonSetting, onDidChangePythonSetting } from '../../../common/externalDependencies'; +import '../../../../common/extensions'; +import { traceVerbose } from '../../../../logging'; +import { DEFAULT_INTERPRETER_SETTING } from '../../../../common/constants'; + +export const DEFAULT_INTERPRETER_PATH_SETTING_KEY = 'defaultInterpreterPath'; + +/** + * Finds and resolves custom virtual environments that users have provided. + */ +export class CustomWorkspaceLocator extends FSWatchingLocator { + public readonly providerId: string = 'custom-workspace-locator'; + + constructor(private readonly root: string) { + super( + () => [], + async () => PythonEnvKind.Unknown, + ); + } + + protected async initResources(): Promise { + this.disposables.push( + onDidChangePythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, () => this.fire(), this.root), + ); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator { + const iterator = async function* (root: string) { + traceVerbose('Searching for custom workspace envs'); + const filename = getPythonSetting(DEFAULT_INTERPRETER_PATH_SETTING_KEY, root); + if (!filename || filename === DEFAULT_INTERPRETER_SETTING) { + // If the user has not set a custom interpreter, our job is done. + return; + } + yield { kind: PythonEnvKind.Unknown, executablePath: filename }; + traceVerbose(`Finished searching for custom workspace envs`); + }; + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/filesLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/filesLocator.ts new file mode 100644 index 000000000000..e5ed206650ca --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/filesLocator.ts @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable max-classes-per-file */ + +import { Event } from 'vscode'; +import { iterPythonExecutablesInDir } from '../../../common/commonUtils'; +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../../locator'; +import { PythonEnvsChangedEvent, PythonEnvsWatcher } from '../../watcher'; + +type GetExecutablesFunc = () => AsyncIterableIterator; + +/** + * A naive locator the wraps a function that finds Python executables. + */ +abstract class FoundFilesLocator implements ILocator { + public abstract readonly providerId: string; + + public readonly onChanged: Event; + + protected readonly watcher = new PythonEnvsWatcher(); + + constructor( + private readonly kind: PythonEnvKind, + private readonly getExecutables: GetExecutablesFunc, + private readonly source?: PythonEnvSource[], + ) { + this.onChanged = this.watcher.onChanged; + } + + public iterEnvs(_query?: PythonLocatorQuery): IPythonEnvsIterator { + const executables = this.getExecutables(); + async function* generator(kind: PythonEnvKind, source?: PythonEnvSource[]): IPythonEnvsIterator { + for await (const executablePath of executables) { + yield { executablePath, kind, source }; + } + } + const iterator = generator(this.kind, this.source); + return iterator; + } +} + +type GetDirExecutablesFunc = (dir: string) => AsyncIterableIterator; + +/** + * A locator for executables in a single directory. + */ +export class DirFilesLocator extends FoundFilesLocator { + public readonly providerId: string; + + constructor( + dirname: string, + defaultKind: PythonEnvKind, + // This is put in a closure and otherwise passed through as-is. + getExecutables: GetDirExecutablesFunc = getExecutablesDefault, + source?: PythonEnvSource[], + ) { + super(defaultKind, () => getExecutables(dirname), source); + this.providerId = `dir-files-${dirname}`; + } +} + +// For now we do not have a DirFilesWatchingLocator. It would be +// a subclass of FSWatchingLocator that wraps a DirFilesLocator +// instance. + +async function* getExecutablesDefault(dirname: string): AsyncIterableIterator { + for await (const entry of iterPythonExecutablesInDir(dirname)) { + yield entry.filename; + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts new file mode 100644 index 000000000000..dd7db5538565 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.ts @@ -0,0 +1,182 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { FileChangeType, watchLocationForPattern } from '../../../../common/platform/fileSystemWatcher'; +import { sleep } from '../../../../common/utils/async'; +import { traceVerbose, traceWarn } from '../../../../logging'; +import { getEnvironmentDirFromPath } from '../../../common/commonUtils'; +import { + PythonEnvStructure, + resolvePythonExeGlobs, + watchLocationForPythonBinaries, +} from '../../../common/pythonBinariesWatcher'; +import { PythonEnvKind } from '../../info'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; + +export enum FSWatcherKind { + Global, // Watcher observes a global location such as ~/.envs, %LOCALAPPDATA%/Microsoft/WindowsApps. + Workspace, // Watchers observes directory in the user's currently open workspace. +} + +type DirUnwatchableReason = 'directory does not exist' | 'too many files' | undefined; + +/** + * Determine if the directory is watchable. + */ +function checkDirWatchable(dirname: string): DirUnwatchableReason { + let names: string[]; + try { + names = fs.readdirSync(dirname); + } catch (err) { + const exception = err as NodeJS.ErrnoException; + traceVerbose('Reading directory failed', exception); + if (exception.code === 'ENOENT') { + // Treat a missing directory as unwatchable since it can lead to CPU load issues: + // https://github.com/microsoft/vscode-python/issues/18459 + return 'directory does not exist'; + } + return undefined; + } + // The limit here is an educated guess. + if (names.length > 200) { + return 'too many files'; + } + return undefined; +} + +type LocationWatchOptions = { + /** + * Glob which represents basename of the executable or directory to watch. + */ + baseGlob?: string; + /** + * Time to wait before handling an environment-created event. + */ + delayOnCreated?: number; // milliseconds + /** + * Location affected by the event. If not provided, a default search location is used. + */ + searchLocation?: string; + /** + * The Python env structure to watch. + */ + envStructure?: PythonEnvStructure; +}; + +type FileWatchOptions = { + /** + * If the provided root is a file instead. In this case the file is directly watched instead for + * looking for python binaries inside a root. + */ + isFile: boolean; +}; + +/** + * The base for Python envs locators who watch the file system. + * Most low-level locators should be using this. + * + * Subclasses can call `this.emitter.fire()` * to emit events. + */ +export abstract class FSWatchingLocator extends LazyResourceBasedLocator { + constructor( + /** + * Location(s) to watch for python binaries. + */ + private readonly getRoots: () => Promise | string | string[], + /** + * Returns the kind of environment specific to locator given the path to executable. + */ + private readonly getKind: (executable: string) => Promise, + private readonly creationOptions: LocationWatchOptions | FileWatchOptions = {}, + private readonly watcherKind: FSWatcherKind = FSWatcherKind.Global, + ) { + super(); + this.activate().ignoreErrors(); + } + + protected async initWatchers(): Promise { + // Enable all workspace watchers. + if (this.watcherKind === FSWatcherKind.Global && !isWatchingAFile(this.creationOptions)) { + // Do not allow global location watchers for now. + return; + } + + // Start the FS watchers. + let roots = await this.getRoots(); + if (typeof roots === 'string') { + roots = [roots]; + } + const promises = roots.map(async (root) => { + if (isWatchingAFile(this.creationOptions)) { + return root; + } + // Note that we only check the root dir. Any directories + // that might be watched due to a glob are not checked. + const unwatchable = await checkDirWatchable(root); + if (unwatchable) { + traceWarn(`Dir "${root}" is not watchable (${unwatchable})`); + return undefined; + } + return root; + }); + const watchableRoots = (await Promise.all(promises)).filter((root) => !!root) as string[]; + watchableRoots.forEach((root) => this.startWatchers(root)); + } + + protected fire(args = {}): void { + this.emitter.fire({ ...args, providerId: this.providerId }); + } + + private startWatchers(root: string): void { + const opts = this.creationOptions; + if (isWatchingAFile(opts)) { + traceVerbose('Start watching file for changes', root); + this.disposables.push( + watchLocationForPattern(path.dirname(root), path.basename(root), () => { + traceVerbose('Detected change in file: ', root, 'initiating a refresh'); + this.emitter.fire({ providerId: this.providerId }); + }), + ); + return; + } + const callback = async (type: FileChangeType, executable: string) => { + if (type === FileChangeType.Created) { + if (opts.delayOnCreated !== undefined) { + // Note detecting kind of env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + await sleep(opts.delayOnCreated); + } + } + // Fetching kind after deletion normally fails because the file structure around the + // executable is no longer available, so ignore the errors. + const kind = await this.getKind(executable).catch(() => undefined); + // By default, search location particularly for virtual environments is intended as the + // directory in which the environment was found in. For eg. the default search location + // for an env containing 'bin' or 'Scripts' directory is: + // + // searchLocation <--- Default search location directory + // |__ env + // |__ bin or Scripts + // |__ python <--- executable + const searchLocation = Uri.file(opts.searchLocation ?? path.dirname(getEnvironmentDirFromPath(executable))); + traceVerbose('Fired event ', JSON.stringify({ type, kind, searchLocation }), 'from locator'); + this.emitter.fire({ type, kind, searchLocation, providerId: this.providerId, envPath: executable }); + }; + + const globs = resolvePythonExeGlobs( + opts.baseGlob, + // The structure determines which globs are returned. + opts.envStructure, + ); + traceVerbose('Start watching root', root, 'for globs', JSON.stringify(globs)); + const watchers = globs.map((g) => watchLocationForPythonBinaries(root, callback, g)); + this.disposables.push(...watchers); + } +} + +function isWatchingAFile(options: LocationWatchOptions | FileWatchOptions): options is FileWatchOptions { + return 'isFile' in options && options.isFile; +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts new file mode 100644 index 000000000000..86fbbed55043 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator.ts @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { toLower, uniq, uniqBy } from 'lodash'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../../common/utils/platform'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { getProjectDir, isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { + isVenvEnvironment, + isVirtualenvEnvironment, + isVirtualenvwrapperEnvironment, +} from '../../../common/environmentManagers/simplevirtualenvs'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; +import { untildify } from '../../../../common/helpers'; + +const DEFAULT_SEARCH_DEPTH = 2; +/** + * Gets all default virtual environment locations. This uses WORKON_HOME, + * and user home directory to find some known locations where global virtual + * environments are often created. + */ +async function getGlobalVirtualEnvDirs(): Promise { + const venvDirs: string[] = []; + + let workOnHome = getEnvironmentVariable('WORKON_HOME'); + if (workOnHome) { + workOnHome = untildify(workOnHome); + if (await pathExists(workOnHome)) { + venvDirs.push(workOnHome); + } + } + + const homeDir = getUserHomeDir(); + if (homeDir && (await pathExists(homeDir))) { + const subDirs = [ + 'envs', + 'Envs', + '.direnv', + '.venvs', + '.virtualenvs', + path.join('.local', 'share', 'virtualenvs'), + ]; + const filtered = await asyncFilter( + subDirs.map((d) => path.join(homeDir, d)), + pathExists, + ); + filtered.forEach((d) => venvDirs.push(d)); + } + + return [OSType.Windows, OSType.OSX].includes(getOSType()) ? uniqBy(venvDirs, toLower) : uniq(venvDirs); +} + +async function getSearchLocation(env: BasicEnvInfo): Promise { + if (env.kind === PythonEnvKind.Pipenv) { + // Pipenv environments are created only for a specific project, so they must only + // appear if that particular project is being queried. + const project = await getProjectDir(path.dirname(path.dirname(env.executablePath))); + if (project) { + return Uri.file(project); + } + } + return undefined; +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPipenvEnvironment(interpreterPath)) { + return PythonEnvKind.Pipenv; + } + + if (await isVirtualenvwrapperEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnvWrapper; + } + + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves virtual environments created in known global locations. + */ +export class GlobalVirtualEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'global-virtual-env'; + + constructor(private readonly searchDepth?: number) { + super(getGlobalVirtualEnvDirs, getVirtualEnvKind, { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. However even + // if the type detected is incorrect, it doesn't do any practical harm as kinds + // in this locator are used in the same way (same activation commands etc.) + delayOnCreated: 1000, + }); + } + + protected doIterEnvs(): IPythonEnvsIterator { + // Number of levels of sub-directories to recurse when looking for + // interpreters + const searchDepth = this.searchDepth ?? DEFAULT_SEARCH_DEPTH; + + async function* iterator() { + const stopWatch = new StopWatch(); + traceInfo('Searching for global virtual environments'); + const envRootDirs = await getGlobalVirtualEnvDirs(); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for global virtual envs in: ${envRootDir}`); + + const executables = findInterpretersInDir(envRootDir, searchDepth); + + for await (const entry of executables) { + const { filename } = entry; + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + if (await looksLikeBasicVirtualPython(entry)) { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(filename); + const searchLocation = await getSearchLocation({ kind, executablePath: filename }); + try { + yield { kind, executablePath: filename, searchLocation }; + traceVerbose(`Global Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } else { + traceVerbose(`Global Virtual Environment: [skipped] ${filename}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceInfo(`Finished searching for global virtual envs: ${stopWatch.elapsedTime} milliseconds`); + } + + return iterator(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts new file mode 100644 index 000000000000..f7746a8c5a2e --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/hatchLocator.ts @@ -0,0 +1,57 @@ +'use strict'; + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; +import { Hatch } from '../../../common/environmentManagers/hatch'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { traceError, traceVerbose } from '../../../../logging'; +import { chain, iterable } from '../../../../common/utils/async'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const hatch = await Hatch.getHatch(root); + const envDirs = (await hatch?.getEnvList()) ?? []; + return asyncFilter(envDirs, pathExists); +} + +/** + * Finds and resolves virtual environments created using Hatch. + */ +export class HatchLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'hatch'; + + public constructor(private readonly root: string) { + super(); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for Hatch virtual envs in: ${envDir}`); + const filename = await getInterpreterPathFromDir(envDir); + if (filename !== undefined) { + try { + yield { executablePath: filename, kind: PythonEnvKind.Hatch }; + traceVerbose(`Hatch Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for Hatch envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts new file mode 100644 index 000000000000..2068a05f3a69 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as minimatch from 'minimatch'; +import * as path from 'path'; +import * as fsapi from '../../../../common/platform/fs-paths'; +import { PythonEnvKind } from '../../info'; +import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { PythonEnvStructure } from '../../../common/pythonBinariesWatcher'; +import { + isStorePythonInstalled, + getMicrosoftStoreAppsRoot, +} from '../../../common/environmentManagers/microsoftStoreEnv'; +import { traceInfo } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +/** + * This is a glob pattern which matches following file names: + * python3.8.exe + * python3.9.exe + * python3.10.exe + * This pattern does not match: + * python.exe + * python2.7.exe + * python3.exe + * python38.exe + */ +const pythonExeGlob = 'python3.{[0-9],[0-9][0-9]}.exe'; + +/** + * Checks if a given path ends with python3.*.exe. Not all python executables are matched as + * we do not want to return duplicate executables. + * @param {string} interpreterPath : Path to python interpreter. + * @returns {boolean} : Returns true if the path matches pattern for windows python executable. + */ +function isMicrosoftStorePythonExePattern(interpreterPath: string): boolean { + return minimatch.default(path.basename(interpreterPath), pythonExeGlob, { nocase: true }); +} + +/** + * Gets paths to the Python executable under Microsoft Store apps. + * @returns: Returns python*.exe for the microsoft store app root directory. + * + * Remarks: We don't need to find the path to the interpreter under the specific application + * directory. Such as: + * `%LOCALAPPDATA%/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0` + * The same python executable is also available at: + * `%LOCALAPPDATA%/Microsoft/WindowsApps` + * It would be a duplicate. + * + * All python executable under `%LOCALAPPDATA%/Microsoft/WindowsApps` or the sub-directories + * are 'reparse points' that point to the real executable at `%PROGRAMFILES%/WindowsApps`. + * However, that directory is off limits to users. So no need to populate interpreters from + * that location. + */ +export async function getMicrosoftStorePythonExes(): Promise { + if (await isStorePythonInstalled()) { + const windowsAppsRoot = getMicrosoftStoreAppsRoot(); + + // Collect python*.exe directly under %LOCALAPPDATA%/Microsoft/WindowsApps + const files = await fsapi.readdir(windowsAppsRoot); + return files + .map((filename: string) => path.join(windowsAppsRoot, filename)) + .filter(isMicrosoftStorePythonExePattern); + } + return []; +} + +export class MicrosoftStoreLocator extends FSWatchingLocator { + public readonly providerId: string = 'microsoft-store'; + + private readonly kind: PythonEnvKind = PythonEnvKind.MicrosoftStore; + + constructor() { + // We have to watch the directory instead of the executable here because + // FS events are not triggered for `*.exe` in the WindowsApps folder. The + // .exe files here are reparse points and not real files. Watching the + // PythonSoftwareFoundation directory will trigger both for new install + // and update case. Update is handled by deleting and recreating the + // PythonSoftwareFoundation directory. + super(getMicrosoftStoreAppsRoot, async () => this.kind, { + baseGlob: pythonExeGlob, + searchLocation: getMicrosoftStoreAppsRoot(), + envStructure: PythonEnvStructure.Flat, + }); + } + + protected doIterEnvs(): IPythonEnvsIterator { + const iterator = async function* (kind: PythonEnvKind) { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows store envs'); + const exes = await getMicrosoftStorePythonExes(); + yield* exes.map(async (executablePath: string) => ({ + kind, + executablePath, + })); + traceInfo(`Finished searching for windows store envs: ${stopWatch.elapsedTime} milliseconds`); + }; + return iterator(this.kind); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts new file mode 100644 index 000000000000..f4a3886a2120 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pixiLocator.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { chain, iterable } from '../../../../common/utils/async'; +import { traceError, traceVerbose } from '../../../../logging'; +import { getCondaInterpreterPath } from '../../../common/environmentManagers/conda'; +import { pathExists } from '../../../common/externalDependencies'; +import { PythonEnvKind } from '../../info'; +import { IPythonEnvsIterator, BasicEnvInfo } from '../../locator'; +import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; +import { getPixi } from '../../../common/environmentManagers/pixi'; + +/** + * Returns all virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const pixi = await getPixi(); + const envDirs = (await pixi?.getEnvList(root)) ?? []; + return asyncFilter(envDirs, pathExists); +} + +/** + * Returns all virtual environment locations to look for in a workspace. + */ +function getVirtualEnvRootDirs(root: string): string[] { + return [path.join(path.join(root, '.pixi'), 'envs')]; +} + +export class PixiLocator extends FSWatchingLocator { + public readonly providerId: string = 'pixi'; + + public constructor(private readonly root: string) { + super( + async () => getVirtualEnvRootDirs(this.root), + async () => PythonEnvKind.Pixi, + { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + delayOnCreated: 1000, + }, + FSWatcherKind.Workspace, + ); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for Pixi virtual envs in: ${envDir}`); + const filename = await getCondaInterpreterPath(envDir); + if (filename !== undefined) { + try { + yield { + executablePath: filename, + kind: PythonEnvKind.Pixi, + envPath: envDir, + }; + + traceVerbose(`Pixi Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for Pixi envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts new file mode 100644 index 000000000000..ab1a8cf77444 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/poetryLocator.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { Uri } from 'vscode'; +import { chain, iterable } from '../../../../common/utils/async'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { isPoetryEnvironment, localPoetryEnvDirName, Poetry } from '../../../common/environmentManagers/poetry'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceError, traceVerbose } from '../../../../logging'; +import { LazyResourceBasedLocator } from '../common/resourceBasedLocator'; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +async function getVirtualEnvDirs(root: string): Promise { + const envDirs = [path.join(root, localPoetryEnvDirName)]; + const poetry = await Poetry.getPoetry(root); + const virtualenvs = await poetry?.getEnvList(); + if (virtualenvs) { + envDirs.push(...virtualenvs); + } + return asyncFilter(envDirs, pathExists); +} + +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPoetryEnvironment(interpreterPath)) { + return PythonEnvKind.Poetry; + } + + return PythonEnvKind.Unknown; +} + +/** + * Finds and resolves virtual environments created using poetry. + */ +export class PoetryLocator extends LazyResourceBasedLocator { + public readonly providerId: string = 'poetry'; + + public constructor(private readonly root: string) { + super(); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envDirs = await getVirtualEnvDirs(root); + const envGenerators = envDirs.map((envDir) => { + async function* generator() { + traceVerbose(`Searching for poetry virtual envs in: ${envDir}`); + const filename = await getInterpreterPathFromDir(envDir); + if (filename !== undefined) { + const kind = await getVirtualEnvKind(filename); + try { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + yield { executablePath: filename, kind, searchLocation: Uri.file(root) }; + traceVerbose(`Poetry Virtual Environment: [added] ${filename}`); + } catch (ex) { + traceError(`Failed to process environment: ${filename}`, ex); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for poetry envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts new file mode 100644 index 000000000000..daca4b860907 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.ts @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as os from 'os'; +import { gte } from 'semver'; +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator } from '../../locator'; +import { commonPosixBinPaths, getPythonBinFromPosixPaths } from '../../../common/posixUtils'; +import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv'; +import { getOSType, OSType } from '../../../../common/utils/platform'; +import { isMacDefaultPythonPath } from '../../../common/environmentManagers/macDefault'; +import { traceError, traceInfo, traceVerbose } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export class PosixKnownPathsLocator extends Locator { + public readonly providerId = 'posixKnownPaths'; + + private kind: PythonEnvKind = PythonEnvKind.OtherGlobal; + + public iterEnvs(): IPythonEnvsIterator { + // Flag to remove system installs of Python 2 from the list of discovered interpreters + // If on macOS Monterey or later. + // See https://github.com/microsoft/vscode-python/issues/17870. + let isMacPython2Deprecated = false; + if (getOSType() === OSType.OSX && gte(os.release(), '21.0.0')) { + isMacPython2Deprecated = true; + } + + const iterator = async function* (kind: PythonEnvKind) { + const stopWatch = new StopWatch(); + traceInfo('Searching for interpreters in posix paths locator'); + try { + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + const knownDirs = (await commonPosixBinPaths()).filter((dirname) => !isPyenvShimDir(dirname)); + let pythonBinaries = await getPythonBinFromPosixPaths(knownDirs); + traceVerbose(`Found ${pythonBinaries.length} python binaries in posix paths`); + + // Filter out MacOS system installs of Python 2 if necessary. + if (isMacPython2Deprecated) { + pythonBinaries = pythonBinaries.filter((binary) => !isMacDefaultPythonPath(binary)); + } + + for (const bin of pythonBinaries) { + try { + yield { executablePath: bin, kind, source: [PythonEnvSource.PathEnvVar] }; + } catch (ex) { + traceError(`Failed to process environment: ${bin}`, ex); + } + } + } catch (ex) { + traceError('Failed to process posix paths', ex); + } + traceInfo( + `Finished searching for interpreters in posix paths locator: ${stopWatch.elapsedTime} milliseconds`, + ); + }; + return iterator(this.kind); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts new file mode 100644 index 000000000000..e97b69c6b882 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/pyenvLocator.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatchingLocator } from './fsWatchingLocator'; +import { getInterpreterPathFromDir } from '../../../common/commonUtils'; +import { getSubDirs } from '../../../common/externalDependencies'; +import { getPyenvVersionsDir } from '../../../common/environmentManagers/pyenv'; +import { traceError, traceInfo } from '../../../../logging'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +/** + * Gets all the pyenv environments. + * + * Remarks: This function looks at the /versions directory and gets + * all the environments (global or virtual) in that directory. + */ +async function* getPyenvEnvironments(): AsyncIterableIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for pyenv environments'); + try { + const pyenvVersionDir = getPyenvVersionsDir(); + + const subDirs = getSubDirs(pyenvVersionDir, { resolveSymlinks: true }); + for await (const subDirPath of subDirs) { + const interpreterPath = await getInterpreterPathFromDir(subDirPath); + + if (interpreterPath) { + try { + yield { + kind: PythonEnvKind.Pyenv, + executablePath: interpreterPath, + }; + } catch (ex) { + traceError(`Failed to process environment: ${interpreterPath}`, ex); + } + } + } + } catch (ex) { + // This is expected when pyenv is not installed + traceInfo(`pyenv is not installed`); + } + traceInfo(`Finished searching for pyenv environments: ${stopWatch.elapsedTime} milliseconds`); +} + +export class PyenvLocator extends FSWatchingLocator { + public readonly providerId: string = 'pyenv'; + + constructor() { + super(getPyenvVersionsDir, async () => PythonEnvKind.Pyenv); + } + + // eslint-disable-next-line class-methods-use-this + public doIterEnvs(): IPythonEnvsIterator { + return getPyenvEnvironments(); + } +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts new file mode 100644 index 000000000000..440d075b4071 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsKnownPathsLocator.ts @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable max-classes-per-file */ + +import { Event } from 'vscode'; +import * as path from 'path'; +import { IDisposable } from '../../../../common/types'; +import { getSearchPathEntries } from '../../../../common/utils/exec'; +import { Disposables } from '../../../../common/utils/resourceLifecycle'; +import { isPyenvShimDir } from '../../../common/environmentManagers/pyenv'; +import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../../locator'; +import { Locators } from '../../locators'; +import { getEnvs } from '../../locatorUtils'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { DirFilesLocator } from './filesLocator'; +import { traceInfo } from '../../../../logging'; +import { inExperiment, pathExists } from '../../../common/externalDependencies'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { iterPythonExecutablesInDir, looksLikeBasicGlobalPython } from '../../../common/commonUtils'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +/** + * A locator for Windows locators found under the $PATH env var. + * + * Note that we assume $PATH won't change, so we don't need to watch + * it for changes. + */ +export class WindowsPathEnvVarLocator implements ILocator, IDisposable { + public readonly providerId: string = 'windows-path-env-var-locator'; + + public readonly onChanged: Event; + + private readonly locators: Locators; + + private readonly disposables = new Disposables(); + + constructor() { + const inExp = inExperiment(DiscoveryUsingWorkers.experiment); + const dirLocators: (ILocator & IDisposable)[] = getSearchPathEntries() + .filter( + (dirname) => + // Filter out following directories: + // 1. Microsoft Store app directories: We have a store app locator that handles this. The + // python.exe available in these directories might not be python. It can be a store + // install shortcut that takes you to microsoft store. + // + // 2. Filter out pyenv shims: They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + !isMicrosoftStoreDir(dirname) && !isPyenvShimDir(dirname), + ) + // Build a locator for each directory. + .map((dirname) => getDirFilesLocator(dirname, PythonEnvKind.System, [PythonEnvSource.PathEnvVar], inExp)); + this.disposables.push(...dirLocators); + this.locators = new Locators(dirLocators); + this.onChanged = this.locators.onChanged; + } + + public async dispose(): Promise { + this.locators.dispose(); + await this.disposables.dispose(); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + // Note that we do no filtering here, including to check if files + // are valid executables. That is left to callers (e.g. composite + // locators). + async function* iterator(it: IPythonEnvsIterator) { + const stopWatch = new StopWatch(); + traceInfo(`Searching windows known paths locator`); + for await (const env of it) { + yield env; + } + traceInfo(`Finished searching windows known paths locator: ${stopWatch.elapsedTime} milliseconds`); + } + return iterator(this.locators.iterEnvs(query)); + } +} + +async function* oldGetExecutables(dirname: string): AsyncIterableIterator { + for await (const entry of iterPythonExecutablesInDir(dirname)) { + if (await looksLikeBasicGlobalPython(entry)) { + yield entry.filename; + } + } +} + +async function* getExecutables(dirname: string): AsyncIterableIterator { + const executable = path.join(dirname, 'python.exe'); + if (await pathExists(executable)) { + yield executable; + } +} + +function getDirFilesLocator( + // These are passed through to DirFilesLocator. + dirname: string, + kind: PythonEnvKind, + source?: PythonEnvSource[], + inExp?: boolean, +): ILocator & IDisposable { + // For now we do not bother using a locator that watches for changes + // in the directory. If we did then we would use + // `DirFilesWatchingLocator`, but only if not \\windows\system32 and + // the `isDirWatchable()` (from fsWatchingLocator.ts) returns true. + const executableFunc = inExp ? getExecutables : oldGetExecutables; + const locator = new DirFilesLocator(dirname, kind, executableFunc, source); + const dispose = async () => undefined; + + // Really we should be checking for symlinks or something more + // sophisticated. Also, this should be done in ReducingLocator + // rather than in each low-level locator. In the meantime we + // take a naive approach. + async function* iterEnvs(query: PythonLocatorQuery): IPythonEnvsIterator { + yield* await getEnvs(locator.iterEnvs(query)).then((res) => res); + } + return { + providerId: locator.providerId, + iterEnvs, + dispose, + onChanged: locator.onChanged, + }; +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts new file mode 100644 index 000000000000..1447c2a90767 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.ts @@ -0,0 +1,75 @@ +/* eslint-disable require-yield */ +/* eslint-disable no-continue */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { PythonEnvKind, PythonEnvSource } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator, Locator, PythonLocatorQuery, IEmitter } from '../../locator'; +import { getRegistryInterpreters } from '../../../common/windowsUtils'; +import { traceError, traceInfo } from '../../../../logging'; +import { isMicrosoftStoreDir } from '../../../common/environmentManagers/microsoftStoreEnv'; +import { PythonEnvsChangedEvent } from '../../watcher'; +import { DiscoveryUsingWorkers } from '../../../../common/experiments/groups'; +import { inExperiment } from '../../../common/externalDependencies'; +import { StopWatch } from '../../../../common/utils/stopWatch'; + +export const WINDOWS_REG_PROVIDER_ID = 'windows-registry'; + +export class WindowsRegistryLocator extends Locator { + public readonly providerId: string = WINDOWS_REG_PROVIDER_ID; + + // eslint-disable-next-line class-methods-use-this + public iterEnvs( + query?: PythonLocatorQuery, + useWorkerThreads = inExperiment(DiscoveryUsingWorkers.experiment), + ): IPythonEnvsIterator { + if (useWorkerThreads) { + /** + * Windows registry is slow and often not necessary, so notify completion immediately, but use watcher + * change events to signal for any new envs which are found. + */ + if (query?.providerId === this.providerId) { + // Query via change event, so iterate all envs. + return iterateEnvs(); + } + return iterateEnvsLazily(this.emitter); + } + return iterateEnvs(); + } +} + +async function* iterateEnvsLazily(changed: IEmitter): IPythonEnvsIterator { + loadAllEnvs(changed).ignoreErrors(); +} + +async function loadAllEnvs(changed: IEmitter) { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows registry interpreters'); + changed.fire({ providerId: WINDOWS_REG_PROVIDER_ID }); + traceInfo(`Finished searching for windows registry interpreters: ${stopWatch.elapsedTime} milliseconds`); +} + +async function* iterateEnvs(): IPythonEnvsIterator { + const stopWatch = new StopWatch(); + traceInfo('Searching for windows registry interpreters'); + const interpreters = await getRegistryInterpreters(); // Value should already be loaded at this point, so this returns immediately. + for (const interpreter of interpreters) { + try { + // Filter out Microsoft Store app directories. We have a store app locator that handles this. + // The python.exe available in these directories might not be python. It can be a store install + // shortcut that takes you to microsoft store. + if (isMicrosoftStoreDir(interpreter.interpreterPath)) { + continue; + } + const env: BasicEnvInfo = { + kind: PythonEnvKind.OtherGlobal, + executablePath: interpreter.interpreterPath, + source: [PythonEnvSource.WindowsRegistry], + }; + yield env; + } catch (ex) { + traceError(`Failed to process environment: ${interpreter}`, ex); + } + } + traceInfo(`Finished searching for windows registry interpreters: ${stopWatch.elapsedTime} milliseconds`); +} diff --git a/src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts b/src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts new file mode 100644 index 000000000000..b815e1d30a89 --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { chain, iterable } from '../../../../common/utils/async'; +import { findInterpretersInDir, looksLikeBasicVirtualPython } from '../../../common/commonUtils'; +import { pathExists } from '../../../common/externalDependencies'; +import { isPipenvEnvironment } from '../../../common/environmentManagers/pipenv'; +import { isVenvEnvironment, isVirtualenvEnvironment } from '../../../common/environmentManagers/simplevirtualenvs'; +import { PythonEnvKind } from '../../info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../locator'; +import { FSWatcherKind, FSWatchingLocator } from './fsWatchingLocator'; +import '../../../../common/extensions'; +import { asyncFilter } from '../../../../common/utils/arrayUtils'; +import { traceVerbose } from '../../../../logging'; + +/** + * Default number of levels of sub-directories to recurse when looking for interpreters. + */ +const DEFAULT_SEARCH_DEPTH = 2; + +/** + * Gets all default virtual environment locations to look for in a workspace. + */ +function getWorkspaceVirtualEnvDirs(root: string): Promise { + return asyncFilter([root, path.join(root, '.direnv')], pathExists); +} + +/** + * Gets the virtual environment kind for a given interpreter path. + * This only checks for environments created using venv, virtualenv, + * and virtualenvwrapper based environments. + * @param interpreterPath: Absolute path to the interpreter paths. + */ +async function getVirtualEnvKind(interpreterPath: string): Promise { + if (await isPipenvEnvironment(interpreterPath)) { + return PythonEnvKind.Pipenv; + } + + if (await isVenvEnvironment(interpreterPath)) { + return PythonEnvKind.Venv; + } + + if (await isVirtualenvEnvironment(interpreterPath)) { + return PythonEnvKind.VirtualEnv; + } + + return PythonEnvKind.Unknown; +} +/** + * Finds and resolves virtual environments created in workspace roots. + */ +export class WorkspaceVirtualEnvironmentLocator extends FSWatchingLocator { + public readonly providerId: string = 'workspaceVirtualEnvLocator'; + + public constructor(private readonly root: string) { + super( + () => getWorkspaceVirtualEnvDirs(this.root), + getVirtualEnvKind, + { + // Note detecting kind of virtual env depends on the file structure around the + // executable, so we need to wait before attempting to detect it. + delayOnCreated: 1000, + }, + FSWatcherKind.Workspace, + ); + } + + protected doIterEnvs(): IPythonEnvsIterator { + async function* iterator(root: string) { + const envRootDirs = await getWorkspaceVirtualEnvDirs(root); + const envGenerators = envRootDirs.map((envRootDir) => { + async function* generator() { + traceVerbose(`Searching for workspace virtual envs in: ${envRootDir}`); + + const executables = findInterpretersInDir(envRootDir, DEFAULT_SEARCH_DEPTH); + + for await (const entry of executables) { + const { filename } = entry; + // We only care about python.exe (on windows) and python (on linux/mac) + // Other version like python3.exe or python3.8 are often symlinks to + // python.exe or python in the same directory in the case of virtual + // environments. + if (await looksLikeBasicVirtualPython(entry)) { + // We should extract the kind here to avoid doing is*Environment() + // check multiple times. Those checks are file system heavy and + // we can use the kind to determine this anyway. + const kind = await getVirtualEnvKind(filename); + yield { kind, executablePath: filename }; + traceVerbose(`Workspace Virtual Environment: [added] ${filename}`); + } else { + traceVerbose(`Workspace Virtual Environment: [skipped] ${filename}`); + } + } + } + return generator(); + }); + + yield* iterable(chain(envGenerators)); + traceVerbose(`Finished searching for workspace virtual envs`); + } + + return iterator(this.root); + } +} diff --git a/src/client/pythonEnvironments/base/locators/wrappers.ts b/src/client/pythonEnvironments/base/locators/wrappers.ts new file mode 100644 index 000000000000..bfaede584f6f --- /dev/null +++ b/src/client/pythonEnvironments/base/locators/wrappers.ts @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// eslint-disable-next-line max-classes-per-file +import { Uri } from 'vscode'; +import { IDisposable } from '../../../common/types'; +import { iterEmpty } from '../../../common/utils/async'; +import { getURIFilter } from '../../../common/utils/misc'; +import { Disposables } from '../../../common/utils/resourceLifecycle'; +import { PythonEnvInfo } from '../info'; +import { BasicEnvInfo, ILocator, IPythonEnvsIterator, PythonLocatorQuery } from '../locator'; +import { combineIterators, Locators } from '../locators'; +import { LazyResourceBasedLocator } from './common/resourceBasedLocator'; + +/** + * A wrapper around all locators used by the extension. + */ + +export class ExtensionLocators extends Locators { + constructor( + // These are expected to be low-level locators (e.g. system). + private readonly nonWorkspace: ILocator[], + // This is expected to be a locator wrapping any found in + // the workspace (i.e. WorkspaceLocators). + private readonly workspace: ILocator, + ) { + super([...nonWorkspace, workspace]); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators: IPythonEnvsIterator[] = [this.workspace.iterEnvs(query)]; + if (!query?.searchLocations?.doNotIncludeNonRooted) { + const nonWorkspace = query?.providerId + ? this.nonWorkspace.filter((locator) => query.providerId === locator.providerId) + : this.nonWorkspace; + iterators.push(...nonWorkspace.map((loc) => loc.iterEnvs(query))); + } + return combineIterators(iterators); + } +} +type WorkspaceLocatorFactoryResult = ILocator & Partial; +type WorkspaceLocatorFactory = (root: Uri) => WorkspaceLocatorFactoryResult[]; +type RootURI = string; + +export type WatchRootsArgs = { + initRoot(root: Uri): void; + addRoot(root: Uri): void; + removeRoot(root: Uri): void; +}; +type WatchRootsFunc = (args: WatchRootsArgs) => IDisposable; +// XXX Factor out RootedLocators and MultiRootedLocators. +/** + * The collection of all workspace-specific locators used by the extension. + * + * The factories are used to produce the locators for each workspace folder. + */ + +export class WorkspaceLocators extends LazyResourceBasedLocator { + public readonly providerId: string = 'workspace-locators'; + + private readonly locators: Record, IDisposable]> = {}; + + private readonly roots: Record = {}; + + constructor(private readonly watchRoots: WatchRootsFunc, private readonly factories: WorkspaceLocatorFactory[]) { + super(); + this.activate().ignoreErrors(); + } + + public async dispose(): Promise { + await super.dispose(); + + // Clear all the roots. + const roots = Object.keys(this.roots).map((key) => this.roots[key]); + roots.forEach((root) => this.removeRoot(root)); + } + + protected doIterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator { + const iterators = Object.keys(this.locators).map((key) => { + if (query?.searchLocations !== undefined) { + const root = this.roots[key]; + // Match any related search location. + const filter = getURIFilter(root, { checkParent: true, checkChild: true }); + // Ignore any requests for global envs. + if (!query.searchLocations.roots.some(filter)) { + // This workspace folder did not match the query, so skip it! + return iterEmpty(); + } + if (query.providerId && query.providerId !== this.providerId) { + // This is a request for a specific provider, so skip it. + return iterEmpty(); + } + } + // The query matches or was not location-specific. + const [locator] = this.locators[key]; + return locator.iterEnvs(query); + }); + return combineIterators(iterators); + } + + protected async initResources(): Promise { + const disposable = this.watchRoots({ + initRoot: (root: Uri) => this.addRoot(root), + addRoot: (root: Uri) => { + // Drop the old one, if necessary. + this.removeRoot(root); + this.addRoot(root); + this.emitter.fire({ searchLocation: root }); + }, + removeRoot: (root: Uri) => { + this.removeRoot(root); + this.emitter.fire({ searchLocation: root }); + }, + }); + this.disposables.push(disposable); + } + + private addRoot(root: Uri): void { + // Create the root's locator, wrapping each factory-generated locator. + const locators: ILocator[] = []; + const disposables = new Disposables(); + this.factories.forEach((create) => { + create(root).forEach((loc) => { + locators.push(loc); + if (loc.dispose !== undefined) { + disposables.push(loc as IDisposable); + } + }); + }); + const locator = new Locators(locators); + // Cache it. + const key = root.toString(); + this.locators[key] = [locator, disposables]; + this.roots[key] = root; + // Hook up the watchers. + disposables.push( + locator.onChanged((e) => { + if (e.searchLocation === undefined) { + e.searchLocation = root; + } + this.emitter.fire(e); + }), + ); + } + + private removeRoot(root: Uri): void { + const key = root.toString(); + const found = this.locators[key]; + if (found === undefined) { + return; + } + const [, disposables] = found; + delete this.locators[key]; + delete this.roots[key]; + disposables.dispose(); + } +} diff --git a/src/client/pythonEnvironments/base/watcher.ts b/src/client/pythonEnvironments/base/watcher.ts new file mode 100644 index 000000000000..a9d0ef65595e --- /dev/null +++ b/src/client/pythonEnvironments/base/watcher.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event, EventEmitter, Uri } from 'vscode'; +import { FileChangeType } from '../../common/platform/fileSystemWatcher'; +import { PythonEnvInfo, PythonEnvKind } from './info'; + +// The use cases for `BasicPythonEnvsChangedEvent` are currently +// hypothetical. However, there's a real chance they may prove +// useful for the concrete low-level locators. So for now we are +// keeping the separate "basic" type. + +/** + * The most basic info for a Python environments event. + * + * @prop kind - the env kind, if any, affected by the event + */ +export type BasicPythonEnvsChangedEvent = { + kind?: PythonEnvKind; + type?: FileChangeType; +}; + +/** + * The full set of possible info for a Python environments event. + */ +export type PythonEnvsChangedEvent = BasicPythonEnvsChangedEvent & { + /** + * The location, if any, affected by the event. + */ + searchLocation?: Uri; + /** + * A specific provider, if any, affected by the event. + */ + providerId?: string; + /** + * The env, if any, affected by the event. + */ + envPath?: string; +}; + +export type PythonEnvCollectionChangedEvent = BasicPythonEnvCollectionChangedEvent & { + type?: FileChangeType; + searchLocation?: Uri; +}; + +export type BasicPythonEnvCollectionChangedEvent = { + old?: PythonEnvInfo; + new?: PythonEnvInfo | undefined; +}; + +/** + * A "watcher" for events related to changes to Python environemts. + * + * The watcher will notify listeners (callbacks registered through + * `onChanged`) of events at undetermined times. The actual emitted + * events, their source, and the timing is entirely up to the watcher + * implementation. + */ +export interface IPythonEnvsWatcher { + /** + * The hook for registering event listeners (callbacks). + */ + readonly onChanged: Event; +} + +/** + * This provides the fundamental functionality of a Python envs watcher. + * + * Consumers register listeners (callbacks) using `onChanged`. Each + * listener is invoked when `fire()` is called. + * + * Note that in most cases classes will not inherit from this class, + * but instead keep a private watcher property. The rule of thumb + * is to follow whether or not consumers of *that* class should be able + * to trigger events (via `fire()`). + * + * Also, in most cases the default event type (`PythonEnvsChangedEvent`) + * should be used. Only in low-level cases should you consider using + * `BasicPythonEnvsChangedEvent`. + */ +export class PythonEnvsWatcher implements IPythonEnvsWatcher { + /** + * The hook for registering event listeners (callbacks). + */ + public readonly onChanged: Event; + + private readonly didChange = new EventEmitter(); + + constructor() { + this.onChanged = this.didChange.event; + } + + /** + * Send the event to all registered listeners. + */ + public fire(event: T): void { + this.didChange.fire(event); + } +} diff --git a/src/client/pythonEnvironments/base/watchers.ts b/src/client/pythonEnvironments/base/watchers.ts new file mode 100644 index 000000000000..60bf5f7516da --- /dev/null +++ b/src/client/pythonEnvironments/base/watchers.ts @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Event } from 'vscode'; +import { IDisposable } from '../../common/types'; +import { Disposables } from '../../common/utils/resourceLifecycle'; +import { IPythonEnvsWatcher, PythonEnvsChangedEvent, PythonEnvsWatcher } from './watcher'; + +/** + * A wrapper around a set of watchers, exposing them as a single watcher. + * + * If any of the wrapped watchers emits an event then this wrapper + * emits that event. + */ +export class PythonEnvsWatchers implements IPythonEnvsWatcher, IDisposable { + public readonly onChanged: Event; + + private readonly watcher = new PythonEnvsWatcher(); + + private readonly disposables = new Disposables(); + + constructor(watchers: ReadonlyArray) { + this.onChanged = this.watcher.onChanged; + watchers.forEach((w) => { + const disposable = w.onChanged((e) => this.watcher.fire(e)); + this.disposables.push(disposable); + }); + } + + public async dispose(): Promise { + await this.disposables.dispose(); + } +} diff --git a/src/client/pythonEnvironments/common/commonUtils.ts b/src/client/pythonEnvironments/common/commonUtils.ts new file mode 100644 index 000000000000..4bd94e0402ab --- /dev/null +++ b/src/client/pythonEnvironments/common/commonUtils.ts @@ -0,0 +1,395 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import { convertFileType, DirEntry, FileType, getFileFilter, getFileType } from '../../common/utils/filesystem'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { traceError, traceVerbose } from '../../logging'; +import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../base/info'; +import { comparePythonVersionSpecificity } from '../base/info/env'; +import { parseVersion } from '../base/info/pythonVersion'; +import { getPythonVersionFromConda } from './environmentManagers/conda'; +import { getPythonVersionFromPyvenvCfg } from './environmentManagers/simplevirtualenvs'; +import { isFile, normCasePath } from './externalDependencies'; +import * as posix from './posixUtils'; +import * as windows from './windowsUtils'; + +const matchStandardPythonBinFilename = + getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename; +type FileFilterFunc = (filename: string) => boolean; + +/** + * Returns `true` if path provided is likely a python executable than a folder path. + */ +export async function isPythonExecutable(filePath: string): Promise { + const isMatch = matchStandardPythonBinFilename(filePath); + if (isMatch && getOSType() === OSType.Windows) { + // On Windows it's fair to assume a path ending with `.exe` denotes a file. + return true; + } + if (await isFile(filePath)) { + return true; + } + return false; +} + +/** + * Searches recursively under the given `root` directory for python interpreters. + * @param root : Directory where the search begins. + * @param recurseLevels : Number of levels to search for from the root directory. + * @param filter : Callback that identifies directories to ignore. + */ +export async function* findInterpretersInDir( + root: string, + recurseLevel?: number, + filterSubDir?: FileFilterFunc, + ignoreErrors = true, +): AsyncIterableIterator { + // "checkBin" is a local variable rather than global + // so we can stub out getOSType() during unit testing. + const checkBin = getOSType() === OSType.Windows ? windows.matchPythonBinFilename : posix.matchPythonBinFilename; + const cfg = { + ignoreErrors, + filterSubDir, + filterFile: checkBin, + // Make no-recursion the default for backward compatibility. + maxDepth: recurseLevel || 0, + }; + // We use an initial depth of 1. + for await (const entry of walkSubTree(root, 1, cfg)) { + const { filename, filetype } = entry; + if (filetype === FileType.File || filetype === FileType.SymbolicLink) { + if (matchFile(filename, checkBin, ignoreErrors)) { + yield entry; + } + } + // We ignore all other file types. + } +} + +/** + * Find all Python executables in the given directory. + */ +export async function* iterPythonExecutablesInDir( + dirname: string, + opts: { + ignoreErrors: boolean; + } = { ignoreErrors: true }, +): AsyncIterableIterator { + const readDirOpts = { + ...opts, + filterFile: matchStandardPythonBinFilename, + }; + const entries = await readDirEntries(dirname, readDirOpts); + for (const entry of entries) { + const { filetype } = entry; + if (filetype === FileType.File || filetype === FileType.SymbolicLink) { + yield entry; + } + // We ignore all other file types. + } +} + +// This function helps simplify the recursion case. +async function* walkSubTree( + subRoot: string, + // "currentDepth" is the depth of the current level of recursion. + currentDepth: number, + cfg: { + filterSubDir: FileFilterFunc | undefined; + maxDepth: number; + ignoreErrors: boolean; + }, +): AsyncIterableIterator { + const entries = await readDirEntries(subRoot, cfg); + for (const entry of entries) { + yield entry; + + const { filename, filetype } = entry; + if (filetype === FileType.Directory) { + if (cfg.maxDepth < 0 || currentDepth <= cfg.maxDepth) { + if (matchFile(filename, cfg.filterSubDir, cfg.ignoreErrors)) { + yield* walkSubTree(filename, currentDepth + 1, cfg); + } + } + } + } +} + +async function readDirEntries( + dirname: string, + opts: { + filterFilename?: FileFilterFunc; + ignoreErrors: boolean; + } = { ignoreErrors: true }, +): Promise { + const ignoreErrors = opts.ignoreErrors || false; + if (opts.filterFilename && getOSType() === OSType.Windows) { + // Since `readdir()` using "withFileTypes" is not efficient + // on Windows, we take advantage of the filter. + let basenames: string[]; + try { + basenames = await fs.promises.readdir(dirname); + } catch (err) { + const exception = err as NodeJS.ErrnoException; + // Treat a missing directory as empty. + if (exception.code === 'ENOENT') { + return []; + } + if (ignoreErrors) { + traceError(`readdir() failed for "${dirname}" (${err})`); + return []; + } + throw err; // re-throw + } + const filenames = basenames + .map((b) => path.join(dirname, b)) + .filter((f) => matchFile(f, opts.filterFilename, ignoreErrors)); + return Promise.all( + filenames.map(async (filename) => { + const filetype = (await getFileType(filename, opts)) || FileType.Unknown; + return { filename, filetype }; + }), + ); + } + + let raw: fs.Dirent[]; + try { + raw = await fs.promises.readdir(dirname, { withFileTypes: true }); + } catch (err) { + const exception = err as NodeJS.ErrnoException; + // Treat a missing directory as empty. + if (exception.code === 'ENOENT') { + return []; + } + if (ignoreErrors) { + traceError(`readdir() failed for "${dirname}" (${err})`); + return []; + } + throw err; // re-throw + } + // (FYI) + // Normally we would have to do an extra (expensive) `fs.lstat()` + // here for each file to determine its file type. However, we + // avoid this by using the "withFileTypes" option to `readdir()` + // above. On non-Windows the file type of each entry is preserved + // for free. Unfortunately, on Windows it actually does an + // `lstat()` under the hood, so it isn't a win. Regardless, + // if we needed more information than just the file type + // then we would be forced to incur the extra cost + // of `lstat()` anyway. + const entries = raw.map((entry) => { + const filename = path.join(dirname, entry.name); + const filetype = convertFileType(entry); + return { filename, filetype }; + }); + if (opts.filterFilename) { + return entries.filter((e) => matchFile(e.filename, opts.filterFilename, ignoreErrors)); + } + return entries; +} + +function matchFile( + filename: string, + filterFile: FileFilterFunc | undefined, + // If "ignoreErrors" is true then we treat a failed filter + // as though it returned `false`. + ignoreErrors = true, +): boolean { + if (filterFile === undefined) { + return true; + } + try { + return filterFile(filename); + } catch (err) { + if (ignoreErrors) { + traceError(`filter failed for "${filename}" (${err})`); + return false; + } + throw err; // re-throw + } +} + +/** + * Looks for files in the same directory which might have version in their name. + * @param interpreterPath + */ +async function getPythonVersionFromNearByFiles(interpreterPath: string): Promise { + const root = path.dirname(interpreterPath); + let version = UNKNOWN_PYTHON_VERSION; + for await (const entry of findInterpretersInDir(root)) { + const { filename } = entry; + try { + const curVersion = parseVersion(path.basename(filename)); + if (comparePythonVersionSpecificity(curVersion, version) > 0) { + version = curVersion; + } + } catch (ex) { + // Ignore any parse errors + } + } + return version; +} + +/** + * This function does the best effort of finding version of python without running the + * python binary. + * @param interpreterPath Absolute path to the interpreter. + * @param hint Any string that might contain version info. + */ +export async function getPythonVersionFromPath(interpreterPath: string, hint?: string): Promise { + let versionA; + try { + versionA = hint ? parseVersion(hint) : UNKNOWN_PYTHON_VERSION; + } catch (ex) { + versionA = UNKNOWN_PYTHON_VERSION; + } + const versionB = interpreterPath ? await getPythonVersionFromNearByFiles(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version B for', interpreterPath, JSON.stringify(versionB)); + const versionC = interpreterPath ? await getPythonVersionFromPyvenvCfg(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version C for', interpreterPath, JSON.stringify(versionC)); + const versionD = interpreterPath ? await getPythonVersionFromConda(interpreterPath) : UNKNOWN_PYTHON_VERSION; + traceVerbose('Best effort version D for', interpreterPath, JSON.stringify(versionD)); + + let version = UNKNOWN_PYTHON_VERSION; + for (const v of [versionA, versionB, versionC, versionD]) { + version = comparePythonVersionSpecificity(version, v) > 0 ? version : v; + } + return version; +} + +/** + * Decide if the file is meets the given criteria for a Python executable. + */ +async function checkPythonExecutable( + executable: string | DirEntry, + opts: { + matchFilename?: (f: string) => boolean; + filterFile?: (f: string | DirEntry) => Promise; + }, +): Promise { + const matchFilename = opts.matchFilename || matchStandardPythonBinFilename; + const filename = typeof executable === 'string' ? executable : executable.filename; + + if (!matchFilename(filename)) { + return false; + } + + // This should occur after we match file names. This is to avoid doing potential + // `lstat` calls on too many files which can slow things down. + if (opts.filterFile && !(await opts.filterFile(executable))) { + return false; + } + + // For some use cases it would also be a good idea to verify that + // the file is executable. That is a relatively expensive operation + // (a stat on linux and actually executing the file on Windows), so + // at best it should be an optional check. If we went down this + // route then it would be worth supporting `fs.Stats` as a type + // for the "executable" arg. + // + // Regardless, currently there is no code that would use such + // an option, so for now we don't bother supporting it. + + return true; +} + +const filterGlobalExecutable = getFileFilter({ ignoreFileType: FileType.SymbolicLink })!; + +/** + * Decide if the file is a typical Python executable. + * + * This is a best effort operation with a focus on the common cases + * and on efficiency. The filename must be basic (python/python.exe). + * For global envs, symlinks are ignored. + */ +export async function looksLikeBasicGlobalPython(executable: string | DirEntry): Promise { + // "matchBasic" is a local variable rather than global + // so we can stub out getOSType() during unit testing. + const matchBasic = + getOSType() === OSType.Windows ? windows.matchBasicPythonBinFilename : posix.matchBasicPythonBinFilename; + + // We could be more permissive here by using matchPythonBinFilename(). + // Originally one key motivation for the "basic" check was to avoid + // symlinks (which often look like python3.exe, etc., particularly + // on Windows). However, the symbolic link check here eliminates + // that rationale to an extent. + // (See: https://github.com/microsoft/vscode-python/issues/15447) + const matchFilename = matchBasic; + const filterFile = filterGlobalExecutable; + return checkPythonExecutable(executable, { matchFilename, filterFile }); +} + +/** + * Decide if the file is a typical Python executable. + * + * This is a best effort operation with a focus on the common cases + * and on efficiency. The filename must be basic (python/python.exe). + * For global envs, symlinks are ignored. + */ +export async function looksLikeBasicVirtualPython(executable: string | DirEntry): Promise { + // "matchBasic" is a local variable rather than global + // so we can stub out getOSType() during unit testing. + const matchBasic = + getOSType() === OSType.Windows ? windows.matchBasicPythonBinFilename : posix.matchBasicPythonBinFilename; + + // With virtual environments, we match only the simplest name + // (e.g. `python`) and we do not ignore symlinks. + const matchFilename = matchBasic; + const filterFile = undefined; + return checkPythonExecutable(executable, { matchFilename, filterFile }); +} + +/** + * This function looks specifically for 'python' or 'python.exe' binary in the sub folders of a given + * environment directory. + * @param envDir Absolute path to the environment directory + */ +export async function getInterpreterPathFromDir( + envDir: string, + opts: { + global?: boolean; + ignoreErrors?: boolean; + } = {}, +): Promise { + const recurseLevel = 2; + + // Ignore any folders or files that not directly python binary related. + function filterDir(dirname: string): boolean { + const lower = path.basename(dirname).toLowerCase(); + return ['bin', 'scripts'].includes(lower); + } + + // Search in the sub-directories for python binary + const matchExecutable = opts.global ? looksLikeBasicGlobalPython : looksLikeBasicVirtualPython; + const executables = findInterpretersInDir(envDir, recurseLevel, filterDir, opts.ignoreErrors); + for await (const entry of executables) { + if (await matchExecutable(entry)) { + return entry.filename; + } + } + return undefined; +} + +/** + * Gets the root environment directory based on the absolute path to the python + * interpreter binary. + * @param interpreterPath Absolute path to the python interpreter + */ +export function getEnvironmentDirFromPath(interpreterPath: string): string { + const skipDirs = ['bin', 'scripts']; + + // env <--- Return this directory if it is not 'bin' or 'scripts' + // |__ python <--- interpreterPath + const dir = path.basename(path.dirname(interpreterPath)); + if (!skipDirs.map((e) => normCasePath(e)).includes(normCasePath(dir))) { + return path.dirname(interpreterPath); + } + + // This is the best next guess. + // env <--- Return this directory if it is not 'bin' or 'scripts' + // |__ bin or Scripts + // |__ python <--- interpreterPath + return path.dirname(path.dirname(interpreterPath)); +} diff --git a/src/client/pythonEnvironments/common/environmentIdentifier.ts b/src/client/pythonEnvironments/common/environmentIdentifier.ts new file mode 100644 index 000000000000..89ff84823673 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentIdentifier.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceWarn } from '../../logging'; +import { PythonEnvKind } from '../base/info'; +import { getPrioritizedEnvKinds } from '../base/info/envKind'; +import { isCondaEnvironment } from './environmentManagers/conda'; +import { isGloballyInstalledEnv } from './environmentManagers/globalInstalledEnvs'; +import { isPipenvEnvironment } from './environmentManagers/pipenv'; +import { isPoetryEnvironment } from './environmentManagers/poetry'; +import { isPyenvEnvironment } from './environmentManagers/pyenv'; +import { + isVenvEnvironment, + isVirtualenvEnvironment as isVirtualEnvEnvironment, + isVirtualenvwrapperEnvironment as isVirtualEnvWrapperEnvironment, +} from './environmentManagers/simplevirtualenvs'; +import { isMicrosoftStoreEnvironment } from './environmentManagers/microsoftStoreEnv'; +import { isActiveStateEnvironment } from './environmentManagers/activestate'; +import { isPixiEnvironment } from './environmentManagers/pixi'; + +const notImplemented = () => Promise.resolve(false); + +function getIdentifiers(): Map Promise> { + const defaultTrue = () => Promise.resolve(true); + const identifier: Map Promise> = new Map(); + Object.values(PythonEnvKind).forEach((k) => { + identifier.set(k, notImplemented); + }); + + identifier.set(PythonEnvKind.Conda, isCondaEnvironment); + identifier.set(PythonEnvKind.MicrosoftStore, isMicrosoftStoreEnvironment); + identifier.set(PythonEnvKind.Pipenv, isPipenvEnvironment); + identifier.set(PythonEnvKind.Pyenv, isPyenvEnvironment); + identifier.set(PythonEnvKind.Poetry, isPoetryEnvironment); + identifier.set(PythonEnvKind.Pixi, isPixiEnvironment); + identifier.set(PythonEnvKind.Venv, isVenvEnvironment); + identifier.set(PythonEnvKind.VirtualEnvWrapper, isVirtualEnvWrapperEnvironment); + identifier.set(PythonEnvKind.VirtualEnv, isVirtualEnvEnvironment); + identifier.set(PythonEnvKind.ActiveState, isActiveStateEnvironment); + identifier.set(PythonEnvKind.Unknown, defaultTrue); + identifier.set(PythonEnvKind.OtherGlobal, isGloballyInstalledEnv); + return identifier; +} + +export function isIdentifierRegistered(kind: PythonEnvKind): boolean { + const identifiers = getIdentifiers(); + const identifier = identifiers.get(kind); + if (identifier === notImplemented) { + return false; + } + return true; +} + +/** + * Returns environment type. + * @param {string} path : Absolute path to the python interpreter binary or path to environment. + * @returns {PythonEnvKind} + */ +export async function identifyEnvironment(path: string): Promise { + const identifiers = getIdentifiers(); + const prioritizedEnvTypes = getPrioritizedEnvKinds(); + for (const e of prioritizedEnvTypes) { + const identifier = identifiers.get(e); + if ( + identifier && + (await identifier(path).catch((ex) => { + traceWarn(`Identifier for ${e} failed to identify ${path}`, ex); + return false; + })) + ) { + return e; + } + } + return PythonEnvKind.Unknown; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/activestate.ts b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts new file mode 100644 index 000000000000..5f22a96e4f83 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/activestate.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { dirname } from 'path'; +import { + arePathsSame, + getPythonSetting, + onDidChangePythonSetting, + pathExists, + shellExecute, +} from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceError, traceVerbose } from '../../../logging'; +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; + +export const ACTIVESTATETOOLPATH_SETTING_KEY = 'activeStateToolPath'; + +const STATE_GENERAL_TIMEOUT = 5000; + +export type ProjectInfo = { + name: string; + organization: string; + local_checkouts: string[]; // eslint-disable-line camelcase + executables: string[]; +}; + +export async function isActiveStateEnvironment(interpreterPath: string): Promise { + const execDir = path.dirname(interpreterPath); + const runtimeDir = path.dirname(execDir); + return pathExists(path.join(runtimeDir, '_runtime_store')); +} + +export class ActiveState { + private static statePromise: Promise | undefined; + + public static async getState(): Promise { + if (ActiveState.statePromise === undefined) { + ActiveState.statePromise = ActiveState.locate(); + } + return ActiveState.statePromise; + } + + constructor() { + onDidChangePythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY, () => { + ActiveState.statePromise = undefined; + }); + } + + public static getStateToolDir(): string | undefined { + const home = getUserHomeDir(); + if (!home) { + return undefined; + } + return getOSType() === OSType.Windows + ? path.join(home, 'AppData', 'Local', 'ActiveState', 'StateTool') + : path.join(home, '.local', 'ActiveState', 'StateTool'); + } + + private static async locate(): Promise { + const stateToolDir = this.getStateToolDir(); + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + if (stateToolDir && ((await pathExists(stateToolDir)) || stateCommand !== this.defaultStateCommand)) { + return new ActiveState(); + } + return undefined; + } + + public async getProjects(): Promise { + return this.getProjectsCached(); + } + + private static readonly defaultStateCommand: string = 'state'; + + // eslint-disable-next-line class-methods-use-this + @cache(30_000, true, 10_000) + private async getProjectsCached(): Promise { + try { + const stateCommand = + getPythonSetting(ACTIVESTATETOOLPATH_SETTING_KEY) ?? ActiveState.defaultStateCommand; + const result = await shellExecute(`${stateCommand} projects -o editor`, { + timeout: STATE_GENERAL_TIMEOUT, + }); + if (!result) { + return undefined; + } + let output = result.stdout.trimEnd(); + if (output[output.length - 1] === '\0') { + // '\0' is a record separator. + output = output.substring(0, output.length - 1); + } + traceVerbose(`${stateCommand} projects -o editor: ${output}`); + const projects = JSON.parse(output); + ActiveState.setCachedProjectInfo(projects); + return projects; + } catch (ex) { + traceError(ex); + return undefined; + } + } + + // Stored copy of known projects. isActiveStateEnvironmentForWorkspace() is + // not async, so getProjects() cannot be used. ActiveStateLocator sets this + // when it resolves project info. + private static cachedProjectInfo: ProjectInfo[] = []; + + public static getCachedProjectInfo(): ProjectInfo[] { + return this.cachedProjectInfo; + } + + private static setCachedProjectInfo(projects: ProjectInfo[]): void { + this.cachedProjectInfo = projects; + } +} + +export function isActiveStateEnvironmentForWorkspace(interpreterPath: string, workspacePath: string): boolean { + const interpreterDir = dirname(interpreterPath); + for (const project of ActiveState.getCachedProjectInfo()) { + if (project.executables) { + for (const [i, dir] of project.executables.entries()) { + // Note multiple checkouts for the same interpreter may exist. + // Check them all. + if (arePathsSame(dir, interpreterDir) && arePathsSame(workspacePath, project.local_checkouts[i])) { + return true; + } + } + } + } + return false; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/conda.ts b/src/client/pythonEnvironments/common/environmentManagers/conda.ts new file mode 100644 index 000000000000..c1bfd7d68bc2 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/conda.ts @@ -0,0 +1,647 @@ +import * as path from 'path'; +import { lt, SemVer } from 'semver'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { + arePathsSame, + getPythonSetting, + isParentPath, + pathExists, + readFile, + onDidChangePythonSetting, + exec, +} from '../externalDependencies'; + +import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info'; +import { parseVersion } from '../../base/info/pythonVersion'; + +import { getRegistryInterpreters } from '../windowsUtils'; +import { EnvironmentType, PythonEnvironment } from '../../info'; +import { cache } from '../../../common/utils/decorators'; +import { isTestExecution } from '../../../common/constants'; +import { traceError, traceVerbose } from '../../../logging'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { splitLines } from '../../../common/stringUtils'; +import { SpawnOptions } from '../../../common/process/types'; +import { sleep } from '../../../common/utils/async'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; + +export const AnacondaCompanyName = 'Anaconda, Inc.'; +export const CONDAPATH_SETTING_KEY = 'condaPath'; +export type CondaEnvironmentInfo = { + name: string; + path: string; +}; + +// This type corresponds to the output of "conda info --json", and property +// names must be spelled exactly as they are in order to match the schema. +export type CondaInfo = { + envs?: string[]; + envs_dirs?: string[]; // eslint-disable-line camelcase + 'sys.version'?: string; + 'sys.prefix'?: string; + python_version?: string; // eslint-disable-line camelcase + default_prefix?: string; // eslint-disable-line camelcase + root_prefix?: string; // eslint-disable-line camelcase + conda_version?: string; // eslint-disable-line camelcase + conda_shlvl?: number; // eslint-disable-line camelcase + config_files?: string[]; // eslint-disable-line camelcase + rc_path?: string; // eslint-disable-line camelcase + sys_rc_path?: string; // eslint-disable-line camelcase + user_rc_path?: string; // eslint-disable-line camelcase +}; + +type CondaEnvInfo = { + prefix: string; + name?: string; +}; + +/** + * Return the list of conda env interpreters. + */ +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types +export async function parseCondaInfo( + info: CondaInfo, + getPythonPath: (condaEnv: string) => string, + fileExists: (filename: string) => Promise, + getPythonInfo: (python: string) => Promise | undefined>, +) { + // The root of the conda environment is itself a Python interpreter + // envs reported as e.g.: /Users/bob/miniconda3/envs/someEnv. + const envs = Array.isArray(info.envs) ? info.envs : []; + if (info.default_prefix && info.default_prefix.length > 0) { + envs.push(info.default_prefix); + } + + const promises = envs.map(async (envPath) => { + const pythonPath = getPythonPath(envPath); + + if (!(await fileExists(pythonPath))) { + return undefined; + } + const details = await getPythonInfo(pythonPath); + if (!details) { + return undefined; + } + + return { + ...(details as PythonEnvironment), + path: pythonPath, + companyDisplayName: AnacondaCompanyName, + envType: EnvironmentType.Conda, + envPath, + }; + }); + + return Promise.all(promises) + .then((interpreters) => interpreters.filter((interpreter) => interpreter !== null && interpreter !== undefined)) + + .then((interpreters) => interpreters.map((interpreter) => interpreter!)); +} + +export function getCondaMetaPaths(interpreterPathOrEnvPath: string): string[] { + const condaMetaDir = 'conda-meta'; + + // Check if the conda-meta directory is in the same directory as the interpreter. + // This layout is common in Windows. + // env + // |__ conda-meta <--- check if this directory exists + // |__ python.exe <--- interpreterPath + const condaEnvDir1 = path.join(path.dirname(interpreterPathOrEnvPath), condaMetaDir); + + // Check if the conda-meta directory is in the parent directory relative to the interpreter. + // This layout is common on linux/Mac. + // env + // |__ conda-meta <--- check if this directory exists + // |__ bin + // |__ python <--- interpreterPath + const condaEnvDir2 = path.join(path.dirname(path.dirname(interpreterPathOrEnvPath)), condaMetaDir); + + const condaEnvDir3 = path.join(interpreterPathOrEnvPath, condaMetaDir); + + // The paths are ordered in the most common to least common + return [condaEnvDir1, condaEnvDir2, condaEnvDir3]; +} + +/** + * Checks if the given interpreter path belongs to a conda environment. Using + * known folder layout, and presence of 'conda-meta' directory. + * @param {string} interpreterPathOrEnvPath: Absolute path to any python interpreter. + * + * Remarks: This is what we will use to begin with. Another approach we can take + * here is to parse ~/.conda/environments.txt. This file will have list of conda + * environments. We can compare the interpreter path against the paths in that file. + * We don't want to rely on this file because it is an implementation detail of + * conda. If it turns out that the layout based identification is not sufficient + * that is the next alternative that is cheap. + * + * sample content of the ~/.conda/environments.txt: + * C:\envs\myenv + * C:\ProgramData\Miniconda3 + * + * Yet another approach is to use `conda env list --json` and compare the returned env + * list to see if the given interpreter path belongs to any of the returned environments. + * This approach is heavy, and involves running a binary. For now we decided not to + * take this approach, since it does not look like we need it. + * + * sample output from `conda env list --json`: + * conda env list --json + * { + * "envs": [ + * "C:\\envs\\myenv", + * "C:\\ProgramData\\Miniconda3" + * ] + * } + */ +export async function isCondaEnvironment(interpreterPathOrEnvPath: string): Promise { + const condaMetaPaths = getCondaMetaPaths(interpreterPathOrEnvPath); + // We don't need to test all at once, testing each one here + for (const condaMeta of condaMetaPaths) { + if (await pathExists(condaMeta)) { + return true; + } + } + return false; +} + +/** + * Gets path to conda's `environments.txt` file. More info https://github.com/conda/conda/issues/11845. + */ +export async function getCondaEnvironmentsTxt(): Promise { + const homeDir = getUserHomeDir(); + if (!homeDir) { + return []; + } + const environmentsTxt = path.join(homeDir, '.conda', 'environments.txt'); + return [environmentsTxt]; +} + +/** + * Extracts version information from `conda-meta/history` near a given interpreter. + * @param interpreterPath Absolute path to the interpreter + * + * Remarks: This function looks for `conda-meta/history` usually in the same or parent directory. + * Reads the `conda-meta/history` and finds the line that contains 'python-3.9.0`. Gets the + * version string from that lines and parses it. + */ +export async function getPythonVersionFromConda(interpreterPath: string): Promise { + const configPaths = getCondaMetaPaths(interpreterPath).map((p) => path.join(p, 'history')); + const pattern = /\:python-(([\d\.a-z]?)+)/; + + // We want to check each of those locations in the order. There is no need to look at + // all of them in parallel. + for (const configPath of configPaths) { + if (await pathExists(configPath)) { + try { + const lines = splitLines(await readFile(configPath)); + + // Sample data: + // +defaults/linux-64::pip-20.2.4-py38_0 + // +defaults/linux-64::python-3.8.5-h7579374_1 + // +defaults/linux-64::readline-8.0-h7b6447c_0 + const pythonVersionStrings = lines + .map((line) => { + // Here we should have only lines with 'python-' in it. + // +defaults/linux-64::python-3.8.5-h7579374_1 + + const matches = pattern.exec(line); + // Typically there will be 3 matches + // 0: "python-3.8.5" + // 1: "3.8.5" + // 2: "5" + + // we only need the second one + return matches ? matches[1] : ''; + }) + .filter((v) => v.length > 0); + + if (pythonVersionStrings.length > 0) { + const last = pythonVersionStrings.length - 1; + return parseVersion(pythonVersionStrings[last].trim()); + } + } catch (ex) { + // There is usually only one `conda-meta/history`. If we found, it but + // failed to parse it, then just return here. No need to look for versions + // any further. + return UNKNOWN_PYTHON_VERSION; + } + } + } + + return UNKNOWN_PYTHON_VERSION; +} + +/** + * Return the interpreter's filename for the given environment. + */ +export function getCondaInterpreterPath(condaEnvironmentPath: string): string { + // where to find the Python binary within a conda env. + const relativePath = getOSType() === OSType.Windows ? 'python.exe' : path.join('bin', 'python'); + const filePath = path.join(condaEnvironmentPath, relativePath); + return filePath; +} + +// Minimum version number of conda required to be able to use 'conda run' with '--no-capture-output' flag. +export const CONDA_RUN_VERSION = '4.9.0'; +export const CONDA_ACTIVATION_TIMEOUT = 45000; +const CONDA_GENERAL_TIMEOUT = 45000; + +/** Wraps the "conda" utility, and exposes its functionality. + */ +export class Conda { + /** + * Locating conda binary is expensive, since it potentially involves spawning or + * trying to spawn processes; so it's done lazily and asynchronously. Methods that + * need a Conda instance should use getConda() to obtain it, and should never access + * this property directly. + */ + private static condaPromise = new Map>(); + + private condaInfoCached = new Map | undefined>(); + + /** + * Carries path to conda binary to be used for shell execution. + */ + public readonly shellCommand: string; + + /** + * Creates a Conda service corresponding to the corresponding "conda" command. + * + * @param command - Command used to spawn conda. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + */ + constructor( + readonly command: string, + shellCommand?: string, + private readonly shellPath?: string, + private readonly useWorkerThreads?: boolean, + ) { + if (this.useWorkerThreads === undefined) { + this.useWorkerThreads = false; + } + this.shellCommand = shellCommand ?? command; + onDidChangePythonSetting(CONDAPATH_SETTING_KEY, () => { + Conda.condaPromise = new Map>(); + }); + } + + public static async getConda(shellPath?: string): Promise { + if (Conda.condaPromise.get(shellPath) === undefined || isTestExecution()) { + Conda.condaPromise.set(shellPath, Conda.locate(shellPath)); + } + return Conda.condaPromise.get(shellPath); + } + + public static setConda(condaPath: string): void { + Conda.condaPromise.set(undefined, Promise.resolve(new Conda(condaPath))); + } + + /** + * Locates the preferred "conda" utility on this system by considering user settings, + * binaries on PATH, Python interpreters in the registry, and known install locations. + * + * @return A Conda instance corresponding to the binary, if successful; otherwise, undefined. + */ + private static async locate(shellPath?: string): Promise { + traceVerbose(`Searching for conda.`); + const home = getUserHomeDir(); + let customCondaPath: string | undefined = 'conda'; + try { + customCondaPath = getPythonSetting(CONDAPATH_SETTING_KEY); + } catch (ex) { + traceError(`Failed to get conda path setting, ${ex}`); + } + const suffix = getOSType() === OSType.Windows ? 'Scripts\\conda.exe' : 'bin/conda'; + + // Produce a list of candidate binaries to be probed by exec'ing them. + async function* getCandidates() { + if (customCondaPath && customCondaPath !== 'conda') { + // If user has specified a custom conda path, use it first. + yield customCondaPath; + } + // Check unqualified filename first, in case it's on PATH. + yield 'conda'; + if (getOSType() === OSType.Windows) { + yield* getCandidatesFromRegistry(); + } + yield* getCandidatesFromKnownPaths(); + yield* getCandidatesFromEnvironmentsTxt(); + } + + async function* getCandidatesFromRegistry() { + const interps = await getRegistryInterpreters(); + const candidates = interps + .filter((interp) => interp.interpreterPath && interp.distroOrgName === 'ContinuumAnalytics') + .map((interp) => path.join(path.win32.dirname(interp.interpreterPath), suffix)); + yield* candidates; + } + + async function* getCandidatesFromKnownPaths() { + // Check common locations. We want to look up "/*conda*/", where prefix and suffix + // depend on the platform, to account for both Anaconda and Miniconda, and all possible variations. + // The check cannot use globs, because on Windows, prefixes are absolute paths with a drive letter, + // and the glob module doesn't understand globs with drive letters in them, producing wrong results + // for "C:/*" etc. + const prefixes: string[] = []; + if (getOSType() === OSType.Windows) { + const programData = getEnvironmentVariable('PROGRAMDATA') || 'C:\\ProgramData'; + prefixes.push(programData); + if (home) { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || path.join(home, 'AppData', 'Local'); + prefixes.push(home, path.join(localAppData, 'Continuum')); + } + } else { + prefixes.push('/usr/share', '/usr/local/share', '/opt', '/opt/homebrew/bin'); + if (home) { + prefixes.push(home, path.join(home, 'opt')); + } + } + + for (const prefix of prefixes) { + let items: string[] | undefined; + try { + items = await fsapi.readdir(prefix); + } catch (ex) { + // Directory doesn't exist or is not readable - not an error. + items = undefined; + } + if (items !== undefined) { + yield* items + .filter((fileName) => fileName.toLowerCase().includes('conda')) + .map((fileName) => path.join(prefix, fileName, suffix)); + } + } + } + + async function* getCandidatesFromEnvironmentsTxt() { + if (!home) { + return; + } + + let contents: string; + try { + contents = await fsapi.readFile(path.join(home, '.conda', 'environments.txt'), 'utf8'); + } catch (ex) { + // File doesn't exist or is not readable - not an error. + contents = ''; + } + + // Match conda behavior; see conda.gateways.disk.read.yield_lines(). + // Note that this precludes otherwise legal paths with trailing spaces. + yield* contents + .split(/\r?\n/g) + .map((line) => line.trim()) + .filter((line) => line !== '' && !line.startsWith('#')) + .map((line) => path.join(line, suffix)); + } + + async function getCondaBatFile(file: string) { + const fileDir = path.dirname(file); + const possibleBatch = path.join(fileDir, '..', 'condabin', 'conda.bat'); + if (await pathExists(possibleBatch)) { + return possibleBatch; + } + return undefined; + } + + // Probe the candidates, and pick the first one that exists and does what we need. + for await (const condaPath of getCandidates()) { + traceVerbose(`Probing conda binary: ${condaPath}`); + let conda = new Conda(condaPath, undefined, shellPath); + try { + await conda.getInfo(); + if (getOSType() === OSType.Windows && (isTestExecution() || condaPath !== customCondaPath)) { + // Prefer to use .bat files over .exe on windows as that is what cmd works best on. + // Do not translate to `.bat` file if the setting explicitly sets the executable. + const condaBatFile = await getCondaBatFile(condaPath); + try { + if (condaBatFile) { + const condaBat = new Conda(condaBatFile, undefined, shellPath); + await condaBat.getInfo(); + conda = new Conda(condaPath, condaBatFile, shellPath); + } + } catch (ex) { + traceVerbose('Failed to spawn conda bat file', condaBatFile, ex); + } + } + traceVerbose(`Found conda via filesystem probing: ${condaPath}`); + return conda; + } catch (ex) { + // Failed to spawn because the binary doesn't exist or isn't on PATH, or the current + // user doesn't have execute permissions for it, or this conda couldn't handle command + // line arguments that we passed (indicating an old version that we do not support). + traceVerbose('Failed to spawn conda binary', condaPath, ex); + } + } + + // Didn't find anything. + traceVerbose("Couldn't locate the conda binary."); + return undefined; + } + + /** + * Retrieves global information about this conda. + * Corresponds to "conda info --json". + */ + public async getInfo(useCache?: boolean): Promise { + let condaInfoCached = this.condaInfoCached.get(this.shellPath); + if (!useCache || !condaInfoCached) { + condaInfoCached = this.getInfoImpl(this.command, this.shellPath); + this.condaInfoCached.set(this.shellPath, condaInfoCached); + } + return condaInfoCached; + } + + /** + * Temporarily cache result for this particular command. + */ + @cache(30_000, true, 10_000) + // eslint-disable-next-line class-methods-use-this + private async getInfoImpl(command: string, shellPath: string | undefined): Promise { + const options: SpawnOptions = { timeout: CONDA_GENERAL_TIMEOUT }; + if (shellPath) { + options.shell = shellPath; + } + const resultPromise = exec(command, ['info', '--json'], options, this.useWorkerThreads); + // It has been observed that specifying a timeout is still not reliable to terminate the Conda process, see #27915. + // Hence explicitly continue execution after timeout has been reached. + const success = await Promise.race([ + resultPromise.then(() => true), + sleep(CONDA_GENERAL_TIMEOUT + 3000).then(() => false), + ]); + if (success) { + const result = await resultPromise; + traceVerbose(`${command} info --json: ${result.stdout}`); + return JSON.parse(result.stdout); + } + throw new Error(`Launching '${command} info --json' timed out`); + } + + /** + * Retrieves list of Python environments known to this conda. + * Corresponds to "conda env list --json", but also computes environment names. + */ + @cache(30_000, true, 10_000) + public async getEnvList(): Promise { + const info = await this.getInfo(); + const { envs } = info; + if (envs === undefined) { + return []; + } + return Promise.all( + envs.map(async (prefix) => ({ + prefix, + name: await this.getName(prefix, info), + })), + ); + } + + /** + * Retrieves list of directories where conda environments are stored. + */ + @cache(30_000, true, 10_000) + public async getEnvDirs(): Promise { + const info = await this.getInfo(); + return info.envs_dirs ?? []; + } + + public async getName(prefix: string, info?: CondaInfo): Promise { + info = info ?? (await this.getInfo(true)); + if (info.root_prefix && arePathsSame(prefix, info.root_prefix)) { + return 'base'; + } + const parentDir = path.dirname(prefix); + if (info.envs_dirs !== undefined) { + for (const envsDir of info.envs_dirs) { + if (arePathsSame(parentDir, envsDir)) { + return path.basename(prefix); + } + } + } + return undefined; + } + + /** + * Returns conda environment related to path provided. + * @param executableOrEnvPath Path to environment folder or path to interpreter that uniquely identifies an environment. + */ + public async getCondaEnvironment(executableOrEnvPath: string): Promise { + const envList = await this.getEnvList(); + // Assuming `executableOrEnvPath` is path to env. + const condaEnv = envList.find((e) => arePathsSame(executableOrEnvPath, e.prefix)); + if (condaEnv) { + return condaEnv; + } + // Assuming `executableOrEnvPath` is an executable. + return envList.find((e) => isParentPath(executableOrEnvPath, e.prefix)); + } + + /** + * Returns executable associated with the conda env, swallows exceptions. + */ + // eslint-disable-next-line class-methods-use-this + public async getInterpreterPathForEnvironment(condaEnv: CondaEnvInfo | { prefix: string }): Promise { + const executablePath = getCondaInterpreterPath(condaEnv.prefix); + if (await pathExists(executablePath)) { + traceVerbose('Found executable within conda env', JSON.stringify(condaEnv)); + return executablePath; + } + traceVerbose( + 'Executable does not exist within conda env, assume the executable to be `python`', + JSON.stringify(condaEnv), + ); + return 'python'; + } + + public async getRunPythonArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + isolatedFlag = false, + ): Promise { + const condaVersion = await this.getCondaVersion(); + if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) { + traceError('`conda run` is not supported for conda version', condaVersion.raw); + return undefined; + } + const args = []; + args.push('-p', env.prefix); + + const python = [ + forShellExecution ? this.shellCommand : this.command, + 'run', + ...args, + '--no-capture-output', + 'python', + ]; + if (isolatedFlag) { + python.push('-I'); + } + return [...python, OUTPUT_MARKER_SCRIPT]; + } + + public async getListPythonPackagesArgs( + env: CondaEnvInfo, + forShellExecution?: boolean, + ): Promise { + const args = ['-p', env.prefix]; + + return [forShellExecution ? this.shellCommand : this.command, 'list', ...args]; + } + + /** + * Return the conda version. The version info is cached. + */ + @cache(-1, true) + public async getCondaVersion(): Promise { + const info = await this.getInfo(true).catch(() => undefined); + let versionString: string | undefined; + if (info && info.conda_version) { + versionString = info.conda_version; + } else { + const stdOut = await exec(this.command, ['--version'], { timeout: CONDA_GENERAL_TIMEOUT }) + .then((result) => result.stdout.trim()) + .catch(() => undefined); + + versionString = stdOut && stdOut.startsWith('conda ') ? stdOut.substring('conda '.length).trim() : stdOut; + } + if (!versionString) { + return undefined; + } + const pattern = /(?\d+)\.(?\d+)\.(?\d+)(?:.*)?/; + const match = versionString.match(pattern); + if (match && match.groups) { + const versionStringParsed = match.groups.major.concat('.', match.groups.minor, '.', match.groups.micro); + + const semVarVersion: SemVer = new SemVer(versionStringParsed); + if (semVarVersion) { + return semVarVersion; + } + } + // Use a bogus version, at least to indicate the fact that a version was returned. + // This ensures we still use conda for activation, installation etc. + traceError(`Unable to parse version of Conda, ${versionString}`); + return new SemVer('0.0.1'); + } + + public async isCondaRunSupported(): Promise { + const condaVersion = await this.getCondaVersion(); + if (condaVersion && lt(condaVersion, CONDA_RUN_VERSION)) { + return false; + } + return true; + } +} + +export function setCondaBinary(executable: string): void { + Conda.setConda(executable); +} + +export async function getCondaEnvDirs(): Promise { + const conda = await Conda.getConda(); + return conda?.getEnvDirs(); +} + +export function getCondaPathSetting(): string | undefined { + const config = getConfiguration('python'); + return config.get(CONDAPATH_SETTING_KEY, ''); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/condaService.ts b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts new file mode 100644 index 000000000000..0aa91bdbfb45 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/condaService.ts @@ -0,0 +1,158 @@ +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { SemVer } from 'semver'; +import { IFileSystem, IPlatformService } from '../../../common/platform/types'; +import { traceVerbose } from '../../../logging'; +import { cache } from '../../../common/utils/decorators'; +import { ICondaService } from '../../../interpreter/contracts'; +import { traceDecoratorVerbose } from '../../../logging'; +import { Conda, CondaEnvironmentInfo, CondaInfo } from './conda'; + +/** + * Injectable version of Conda utility. + */ +@injectable() +export class CondaService implements ICondaService { + private isAvailable: boolean | undefined; + + constructor( + @inject(IPlatformService) private platform: IPlatformService, + @inject(IFileSystem) private fileSystem: IFileSystem, + ) {} + + public async getActivationScriptFromInterpreter( + interpreterPath?: string, + envName?: string, + ): Promise<{ path: string | undefined; type: 'local' | 'global' } | undefined> { + traceVerbose(`Getting activation script for interpreter ${interpreterPath}, env ${envName}`); + const condaPath = await this.getCondaFileFromInterpreter(interpreterPath, envName); + traceVerbose(`Found conda path: ${condaPath}`); + + const activatePath = (condaPath + ? path.join(path.dirname(condaPath), 'activate') + : 'activate' + ).fileToCommandArgumentForPythonExt(); // maybe global activate? + traceVerbose(`Using activate path: ${activatePath}`); + + // try to find the activate script in the global conda root prefix. + if (this.platform.isLinux || this.platform.isMac) { + const condaInfo = await this.getCondaInfo(); + // eslint-disable-next-line camelcase + if (condaInfo?.root_prefix) { + const globalActivatePath = path + // eslint-disable-next-line camelcase + .join(condaInfo.root_prefix, this.platform.virtualEnvBinName, 'activate') + .fileToCommandArgumentForPythonExt(); + + if (activatePath === globalActivatePath || !(await this.fileSystem.fileExists(activatePath))) { + traceVerbose(`Using global activate path: ${globalActivatePath}`); + return { + path: globalActivatePath, + type: 'global', + }; + } + } + } + + return { path: activatePath, type: 'local' }; // return the default activate script wether it exists or not. + } + + /** + * Return the path to the "conda file". + */ + + // eslint-disable-next-line class-methods-use-this + public async getCondaFile(forShellExecution?: boolean): Promise { + return Conda.getConda().then((conda) => { + const command = forShellExecution ? conda?.shellCommand : conda?.command; + return command ?? 'conda'; + }); + } + + // eslint-disable-next-line class-methods-use-this + public async getInterpreterPathForEnvironment(condaEnv: CondaEnvironmentInfo): Promise { + const conda = await Conda.getConda(); + return conda?.getInterpreterPathForEnvironment({ name: condaEnv.name, prefix: condaEnv.path }); + } + + /** + * Is there a conda install to use? + */ + public async isCondaAvailable(): Promise { + if (typeof this.isAvailable === 'boolean') { + return this.isAvailable; + } + return this.getCondaVersion() + + .then((version) => (this.isAvailable = version !== undefined)) // eslint-disable-line no-return-assign + .catch(() => (this.isAvailable = false)); // eslint-disable-line no-return-assign + } + + /** + * Return the conda version. + */ + // eslint-disable-next-line class-methods-use-this + public async getCondaVersion(): Promise { + return Conda.getConda().then((conda) => conda?.getCondaVersion()); + } + + /** + * Get the conda exe from the path to an interpreter's python. This might be different than the + * globally registered conda.exe. + * + * The value is cached for a while. + * The only way this can change is if user installs conda into this same environment. + * Generally we expect that to happen the other way, the user creates a conda environment with conda in it. + */ + @traceDecoratorVerbose('Get Conda File from interpreter') + @cache(120_000) + public async getCondaFileFromInterpreter(interpreterPath?: string, envName?: string): Promise { + const condaExe = this.platform.isWindows ? 'conda.exe' : 'conda'; + const scriptsDir = this.platform.isWindows ? 'Scripts' : 'bin'; + const interpreterDir = interpreterPath ? path.dirname(interpreterPath) : ''; + + // Might be in a situation where this is not the default python env, but rather one running + // from a virtualenv + const envsPos = envName ? interpreterDir.indexOf(path.join('envs', envName)) : -1; + if (envsPos > 0) { + // This should be where the original python was run from when the environment was created. + const originalPath = interpreterDir.slice(0, envsPos); + let condaPath1 = path.join(originalPath, condaExe); + + if (await this.fileSystem.fileExists(condaPath1)) { + return condaPath1; + } + + // Also look in the scripts directory here too. + condaPath1 = path.join(originalPath, scriptsDir, condaExe); + if (await this.fileSystem.fileExists(condaPath1)) { + return condaPath1; + } + } + + let condaPath2 = path.join(interpreterDir, condaExe); + if (await this.fileSystem.fileExists(condaPath2)) { + return condaPath2; + } + // Conda path has changed locations, check the new location in the scripts directory after checking + // the old location + condaPath2 = path.join(interpreterDir, scriptsDir, condaExe); + if (await this.fileSystem.fileExists(condaPath2)) { + return condaPath2; + } + + return this.getCondaFile(); + } + + /** + * Return the info reported by the conda install. + * The result is cached for 30s. + */ + + // eslint-disable-next-line class-methods-use-this + @cache(60_000) + public async getCondaInfo(): Promise { + const conda = await Conda.getConda(); + return conda?.getInfo(); + } +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/globalInstalledEnvs.ts b/src/client/pythonEnvironments/common/environmentManagers/globalInstalledEnvs.ts new file mode 100644 index 000000000000..eb52668a0c65 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/globalInstalledEnvs.ts @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getSearchPathEntries } from '../../../common/utils/exec'; +import { getOSType, OSType } from '../../../common/utils/platform'; +import { isParentPath } from '../externalDependencies'; +import { commonPosixBinPaths } from '../posixUtils'; +import { isPyenvShimDir } from './pyenv'; + +/** + * Checks if the given interpreter belongs to known globally installed types. If an global + * executable is discoverable, we consider it as global type. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isGloballyInstalledEnv(executablePath: string): Promise { + // Identifying this type is not important, as the extension treats `Global` and `Unknown` + // types the same way. This is only required for telemetry. As windows registry is known + // to be slow, we do not want to unnecessarily block on that by default, hence skip this + // step. + // if (getOSType() === OSType.Windows) { + // if (await isFoundInWindowsRegistry(executablePath)) { + // return true; + // } + // } + return isFoundInPathEnvVar(executablePath); +} + +async function isFoundInPathEnvVar(executablePath: string): Promise { + let searchPathEntries: string[] = []; + if (getOSType() === OSType.Windows) { + searchPathEntries = getSearchPathEntries(); + } else { + searchPathEntries = await commonPosixBinPaths(); + } + // Filter out pyenv shims. They are not actual python binaries, they are used to launch + // the binaries specified in .python-version file in the cwd. We should not be reporting + // those binaries as environments. + searchPathEntries = searchPathEntries.filter((dirname) => !isPyenvShimDir(dirname)); + for (const searchPath of searchPathEntries) { + if (isParentPath(executablePath, searchPath)) { + return true; + } + } + return false; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/hatch.ts b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts new file mode 100644 index 000000000000..6d7a13ea1557 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/hatch.ts @@ -0,0 +1,116 @@ +import { isTestExecution } from '../../../common/constants'; +import { exec, pathExists } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; +import { cache } from '../../../common/utils/decorators'; +import { getOSType, OSType } from '../../../common/utils/platform'; + +/** Wraps the "Hatch" utility, and exposes its functionality. + */ +export class Hatch { + /** + * Locating Hatch binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ + private static hatchPromise: Map> = new Map< + string, + Promise + >(); + + /** + * Creates a Hatch service corresponding to the corresponding "hatch" command. + * + * @param command - Command used to run hatch. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + * @param cwd - The working directory to use as cwd when running hatch. + */ + constructor(public readonly command: string, private cwd: string) { + this.fixCwd(); + } + + /** + * Returns a Hatch instance corresponding to the binary which can be used to run commands for the cwd. + * + * Every directory is a valid Hatch project, so this should always return a Hatch instance. + */ + public static async getHatch(cwd: string): Promise { + if (Hatch.hatchPromise.get(cwd) === undefined || isTestExecution()) { + Hatch.hatchPromise.set(cwd, Hatch.locate(cwd)); + } + return Hatch.hatchPromise.get(cwd); + } + + private static async locate(cwd: string): Promise { + // First thing this method awaits on should be hatch command execution, + // hence perform all operations before that synchronously. + const hatchPath = 'hatch'; + traceVerbose(`Probing Hatch binary ${hatchPath}`); + const hatch = new Hatch(hatchPath, cwd); + const virtualenvs = await hatch.getEnvList(); + if (virtualenvs !== undefined) { + traceVerbose(`Found hatch binary ${hatchPath}`); + return hatch; + } + traceVerbose(`Failed to find Hatch binary ${hatchPath}`); + + // Didn't find anything. + traceVerbose(`No Hatch binary found`); + return undefined; + } + + /** + * Retrieves list of Python environments known to Hatch for this working directory. + * Returns `undefined` if we failed to spawn in some way. + * + * Corresponds to "hatch env show --json". Swallows errors if any. + */ + public async getEnvList(): Promise { + return this.getEnvListCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(30_000, true, 10_000) + private async getEnvListCached(_cwd: string): Promise { + const envInfoOutput = await exec(this.command, ['env', 'show', '--json'], { + cwd: this.cwd, + throwOnStdErr: true, + }).catch(traceVerbose); + if (!envInfoOutput) { + return undefined; + } + const envPaths = await Promise.all( + Object.keys(JSON.parse(envInfoOutput.stdout)).map(async (name) => { + const envPathOutput = await exec(this.command, ['env', 'find', name], { + cwd: this.cwd, + throwOnStdErr: true, + }).catch(traceVerbose); + if (!envPathOutput) return undefined; + const dir = envPathOutput.stdout.trim(); + return (await pathExists(dir)) ? dir : undefined; + }), + ); + return envPaths.flatMap((r) => (r ? [r] : [])); + } + + /** + * Due to an upstream hatch issue on Windows https://github.com/pypa/hatch/issues/1350, + * 'hatch env find default' does not handle case-insensitive paths as cwd, which are valid on Windows. + * So we need to pass the case-exact path as cwd. + * It has been observed that only the drive letter in `cwd` is lowercased here. Unfortunately, + * there's no good way to get case of the drive letter correctly without using Win32 APIs: + * https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows + * So we do it manually. + */ + private fixCwd(): void { + if (getOSType() === OSType.Windows) { + if (/^[a-z]:/.test(this.cwd)) { + // Replace first character by the upper case version of the character. + const a = this.cwd.split(':'); + a[0] = a[0].toUpperCase(); + this.cwd = a.join(':'); + } + } + } +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/macDefault.ts b/src/client/pythonEnvironments/common/environmentManagers/macDefault.ts new file mode 100644 index 000000000000..931fbbba9eac --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/macDefault.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { getOSType, OSType } from '../../../common/utils/platform'; + +/** + * Decide if the given Python executable looks like the MacOS default Python. + */ +export function isMacDefaultPythonPath(pythonPath: string): boolean { + if (getOSType() !== OSType.OSX) { + return false; + } + + const defaultPaths = ['/usr/bin/python']; + + return defaultPaths.includes(pythonPath) || pythonPath.startsWith('/usr/bin/python2'); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.ts b/src/client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.ts new file mode 100644 index 000000000000..2b8675d0bc0b --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { getEnvironmentVariable } from '../../../common/utils/platform'; +import { traceWarn } from '../../../logging'; +import { pathExists } from '../externalDependencies'; + +/** + * Gets path to the Windows Apps directory. + * @returns {string} : Returns path to the Windows Apps directory under + * `%LOCALAPPDATA%/Microsoft/WindowsApps`. + */ +export function getMicrosoftStoreAppsRoot(): string { + const localAppData = getEnvironmentVariable('LOCALAPPDATA') || ''; + return path.join(localAppData, 'Microsoft', 'WindowsApps'); +} +/** + * Checks if a given path is under the forbidden microsoft store directory. + * @param {string} absPath : Absolute path to a file or directory. + * @returns {boolean} : Returns true if `interpreterPath` is under + * `%ProgramFiles%/WindowsApps`. + */ +function isForbiddenStorePath(absPath: string): boolean { + const programFilesStorePath = path + .join(getEnvironmentVariable('ProgramFiles') || 'Program Files', 'WindowsApps') + .normalize() + .toUpperCase(); + return path.normalize(absPath).toUpperCase().includes(programFilesStorePath); +} +/** + * Checks if a given directory is any one of the possible microsoft store directories, or + * its sub-directory. + * @param {string} dirPath : Absolute path to a directory. + * + * Remarks: + * These locations are tested: + * 1. %LOCALAPPDATA%/Microsoft/WindowsApps + * 2. %ProgramFiles%/WindowsApps + */ + +export function isMicrosoftStoreDir(dirPath: string): boolean { + const storeRootPath = path.normalize(getMicrosoftStoreAppsRoot()).toUpperCase(); + return path.normalize(dirPath).toUpperCase().includes(storeRootPath) || isForbiddenStorePath(dirPath); +} +/** + * Checks if store python is installed. + * @param {string} interpreterPath : Absolute path to a interpreter. + * Remarks: + * If store python was never installed then the store apps directory will not + * have idle.exe or pip.exe. We can use this as a way to identify the python.exe + * found in the store apps directory is a real python or a store install shortcut. + */ +export async function isStorePythonInstalled(interpreterPath?: string): Promise { + let results = await Promise.all([ + pathExists(path.join(getMicrosoftStoreAppsRoot(), 'idle.exe')), + pathExists(path.join(getMicrosoftStoreAppsRoot(), 'pip.exe')), + ]); + + if (results.includes(true)) { + return true; + } + + if (interpreterPath) { + results = await Promise.all([ + pathExists(path.join(path.dirname(interpreterPath), 'idle.exe')), + pathExists(path.join(path.dirname(interpreterPath), 'pip.exe')), + ]); + return results.includes(true); + } + return false; +} +/** + * Checks if the given interpreter belongs to Microsoft Store Python environment. + * @param interpreterPath: Absolute path to any python interpreter. + * + * Remarks: + * 1. Checking if the path includes `Microsoft\WindowsApps`, `Program Files\WindowsApps`, is + * NOT enough. In WSL, `/mnt/c/users/user/AppData/Local/Microsoft/WindowsApps` is available as a search + * path. It is possible to get a false positive for that path. So the comparison should check if the + * absolute path to 'WindowsApps' directory is present in the given interpreter path. The WSL path to + * 'WindowsApps' is not a valid path to access, Microsoft Store Python. + * + * 2. 'startsWith' comparison may not be right, user can provide '\\?\C:\users\' style long paths in windows. + * + * 3. A limitation of the checks here is that they don't handle 8.3 style windows paths. + * For example, + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * is the shortened form of + * `C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe` + * + * The correct way to compare these would be to always convert given paths to long path (or to short path). + * For either approach to work correctly you need actual file to exist, and accessible from the user's + * account. + * + * To convert to short path without using N-API in node would be to use this command. This is very expensive: + * `> cmd /c for %A in ("C:\Users\USER\AppData\Local\Microsoft\WindowsApps\python3.7.exe") do @echo %~sA` + * The above command will print out this: + * `C:\Users\USER\AppData\Local\MICROS~1\WINDOW~1\PYTHON~2.EXE` + * + * If we go down the N-API route, use node-ffi and either call GetShortPathNameW or GetLongPathNameW from, + * Kernel32 to convert between the two path variants. + * + */ + +export async function isMicrosoftStoreEnvironment(interpreterPath: string): Promise { + if (await isStorePythonInstalled(interpreterPath)) { + const pythonPathToCompare = path.normalize(interpreterPath).toUpperCase(); + const localAppDataStorePath = path.normalize(getMicrosoftStoreAppsRoot()).toUpperCase(); + if (pythonPathToCompare.includes(localAppDataStorePath)) { + return true; + } + + // Program Files store path is a forbidden path. Only admins and system has access this path. + // We should never have to look at this path or even execute python from this path. + if (isForbiddenStorePath(pythonPathToCompare)) { + traceWarn('isMicrosoftStoreEnvironment called with Program Files store path.'); + return true; + } + } + return false; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts b/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts new file mode 100644 index 000000000000..c8651533ed4c --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pipenv.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { getEnvironmentVariable } from '../../../common/utils/platform'; +import { traceError, traceVerbose } from '../../../logging'; +import { arePathsSame, normCasePath, pathExists, readFile } from '../externalDependencies'; + +function getSearchHeight() { + // PIPENV_MAX_DEPTH tells pipenv the maximum number of directories to recursively search for + // a Pipfile, defaults to 3: https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_MAX_DEPTH + const maxDepthStr = getEnvironmentVariable('PIPENV_MAX_DEPTH'); + if (maxDepthStr === undefined) { + return 3; + } + const maxDepth = parseInt(maxDepthStr, 10); + // eslint-disable-next-line no-restricted-globals + if (isNaN(maxDepth)) { + traceError(`PIPENV_MAX_DEPTH is incorrectly set. Converting value '${maxDepthStr}' to number results in NaN`); + return 1; + } + return maxDepth; +} + +/** + * Returns the path to Pipfile associated with the provided directory. + * @param searchDir the directory to look into + * @param lookIntoParentDirectories set to true if we should also search for Pipfile in parent directory + */ +export async function _getAssociatedPipfile( + searchDir: string, + options: { lookIntoParentDirectories: boolean }, +): Promise { + const pipFileName = getEnvironmentVariable('PIPENV_PIPFILE') || 'Pipfile'; + let heightToSearch = options.lookIntoParentDirectories ? getSearchHeight() : 1; + while (heightToSearch > 0 && !arePathsSame(searchDir, path.dirname(searchDir))) { + const pipFile = path.join(searchDir, pipFileName); + if (await pathExists(pipFile)) { + return pipFile; + } + searchDir = path.dirname(searchDir); + heightToSearch -= 1; + } + return undefined; +} + +/** + * If interpreter path belongs to a pipenv environment which is located inside a project, return associated Pipfile, + * otherwise return `undefined`. + * @param interpreterPath Absolute path to any python interpreter. + */ +async function getPipfileIfLocal(interpreterPath: string): Promise { + // Local pipenv environments are created by setting PIPENV_VENV_IN_PROJECT to 1, which always names the environment + // folder '.venv': https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_VENV_IN_PROJECT + // This is the layout we wish to verify. + // project + // |__ Pipfile <--- check if Pipfile exists here + // |__ .venv <--- check if name of the folder is '.venv' + // |__ Scripts/bin + // |__ python <--- interpreterPath + const venvFolder = path.dirname(path.dirname(interpreterPath)); + if (path.basename(venvFolder) !== '.venv') { + return undefined; + } + const directoryWhereVenvResides = path.dirname(venvFolder); + return _getAssociatedPipfile(directoryWhereVenvResides, { lookIntoParentDirectories: false }); +} + +/** + * Returns the project directory for pipenv environments given the environment folder + * @param envFolder Path to the environment folder + */ +export async function getProjectDir(envFolder: string): Promise { + // Global pipenv environments have a .project file with the absolute path to the project + // See https://github.com/pypa/pipenv/blob/v2018.6.25/CHANGELOG.rst#features--improvements + // This is the layout we expect + // + // |__ .project <--- check if .project exists here + // |__ Scripts/bin + // |__ python <--- interpreterPath + // We get the project by reading the .project file + const dotProjectFile = path.join(envFolder, '.project'); + if (!(await pathExists(dotProjectFile))) { + return undefined; + } + const projectDir = (await readFile(dotProjectFile)).trim(); + if (!(await pathExists(projectDir))) { + traceVerbose( + `The .project file inside environment folder: ${envFolder} doesn't contain a valid path to the project`, + ); + return undefined; + } + return projectDir; +} + +/** + * If interpreter path belongs to a global pipenv environment, return associated Pipfile, otherwise return `undefined`. + * @param interpreterPath Absolute path to any python interpreter. + */ +async function getPipfileIfGlobal(interpreterPath: string): Promise { + const envFolder = path.dirname(path.dirname(interpreterPath)); + const projectDir = await getProjectDir(envFolder); + if (projectDir === undefined) { + return undefined; + } + + // This is the layout we expect to see. + // project + // |__ Pipfile <--- check if Pipfile exists here and return it + // The name of the project (directory where Pipfile resides) is used as a prefix in the environment folder + const envFolderName = path.basename(normCasePath(envFolder)); + if (!envFolderName.startsWith(`${path.basename(normCasePath(projectDir))}-`)) { + return undefined; + } + + return _getAssociatedPipfile(projectDir, { lookIntoParentDirectories: false }); +} + +/** + * Checks if the given interpreter path belongs to a pipenv environment, by locating the Pipfile which was used to + * create the environment. + * @param interpreterPath: Absolute path to any python interpreter. + */ +export async function isPipenvEnvironment(interpreterPath: string): Promise { + if (await getPipfileIfLocal(interpreterPath)) { + return true; + } + if (await getPipfileIfGlobal(interpreterPath)) { + return true; + } + return false; +} + +/** + * Returns true if interpreter path belongs to a global pipenv environment which is associated with a particular folder, + * false otherwise. + * @param interpreterPath Absolute path to any python interpreter. + */ +export async function isPipenvEnvironmentRelatedToFolder(interpreterPath: string, folder: string): Promise { + const pipFileAssociatedWithEnvironment = await getPipfileIfGlobal(interpreterPath); + if (!pipFileAssociatedWithEnvironment) { + return false; + } + + // PIPENV_NO_INHERIT is used to tell pipenv not to look for Pipfile in parent directories + // https://pipenv.pypa.io/en/latest/advanced/#pipenv.environments.PIPENV_NO_INHERIT + const lookIntoParentDirectories = getEnvironmentVariable('PIPENV_NO_INHERIT') === undefined; + const pipFileAssociatedWithFolder = await _getAssociatedPipfile(folder, { lookIntoParentDirectories }); + if (!pipFileAssociatedWithFolder) { + return false; + } + return arePathsSame(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder); +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pixi.ts b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts new file mode 100644 index 000000000000..6443e64f9ae8 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pixi.ts @@ -0,0 +1,386 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { readJSON } from 'fs-extra'; +import which from 'which'; +import { getUserHomeDir, isWindows } from '../../../common/utils/platform'; +import { exec, getPythonSetting, onDidChangePythonSetting, pathExists } from '../externalDependencies'; +import { cache } from '../../../common/utils/decorators'; +import { traceVerbose, traceWarn } from '../../../logging'; +import { OUTPUT_MARKER_SCRIPT } from '../../../common/process/internal/scripts'; +import { IDisposableRegistry } from '../../../common/types'; +import { getWorkspaceFolderPaths } from '../../../common/vscodeApis/workspaceApis'; +import { isTestExecution } from '../../../common/constants'; +import { TerminalShellType } from '../../../common/terminal/types'; + +export const PIXITOOLPATH_SETTING_KEY = 'pixiToolPath'; + +// This type corresponds to the output of 'pixi info --json', and property +// names must be spelled exactly as they are in order to match the schema. +export type PixiInfo = { + platform: string; + virtual_packages: string[]; // eslint-disable-line camelcase + version: string; + cache_dir: string; // eslint-disable-line camelcase + cache_size?: number; // eslint-disable-line camelcase + auth_dir: string; // eslint-disable-line camelcase + + project_info?: PixiProjectInfo /* eslint-disable-line camelcase */; + + environments_info: /* eslint-disable-line camelcase */ { + name: string; + features: string[]; + solve_group: string; // eslint-disable-line camelcase + environment_size: number; // eslint-disable-line camelcase + dependencies: string[]; + tasks: string[]; + channels: string[]; + prefix: string; + }[]; +}; + +export type PixiProjectInfo = { + manifest_path: string; // eslint-disable-line camelcase + last_updated: string; // eslint-disable-line camelcase + pixi_folder_size?: number; // eslint-disable-line camelcase + version: string; +}; + +export type PixiEnvMetadata = { + manifest_path: string; // eslint-disable-line camelcase + pixi_version: string; // eslint-disable-line camelcase + environment_name: string; // eslint-disable-line camelcase +}; + +export async function isPixiEnvironment(interpreterPath: string): Promise { + const prefix = getPrefixFromInterpreterPath(interpreterPath); + return ( + pathExists(path.join(prefix, 'conda-meta/pixi')) || pathExists(path.join(prefix, 'conda-meta/pixi_env_prefix')) + ); +} + +/** + * Returns the path to the environment directory based on the interpreter path. + */ +export function getPrefixFromInterpreterPath(interpreterPath: string): string { + const interpreterDir = path.dirname(interpreterPath); + if (!interpreterDir.endsWith('bin') && !interpreterDir.endsWith('Scripts')) { + return interpreterDir; + } + return path.dirname(interpreterDir); +} + +async function findPixiOnPath(): Promise { + try { + return await which('pixi', { all: true }); + } catch { + // Ignore errors + } + return []; +} + +/** Wraps the "pixi" utility, and exposes its functionality. + */ +export class Pixi { + /** + * Creates a Pixi service corresponding to the corresponding "pixi" command. + * + * @param command - Command used to run pixi. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + */ + constructor(public readonly command: string) {} + + /** + * Retrieves list of Python environments known to this pixi for the specified directory. + * + * Corresponds to "pixi info --json" and extracting the environments. Swallows errors if any. + */ + public async getEnvList(cwd: string): Promise { + const pixiInfo = await this.getPixiInfo(cwd); + // eslint-disable-next-line camelcase + return pixiInfo?.environments_info.map((env) => env.prefix); + } + + /** + * Method that runs `pixi info` and returns the result. The value is cached for "only" 1 second + * because the output changes if the project manifest is modified. + */ + @cache(1_000, true, 1_000) + public async getPixiInfo(cwd: string): Promise { + try { + const infoOutput = await exec(this.command, ['info', '--json'], { + cwd, + throwOnStdErr: false, + }); + + if (!infoOutput || !infoOutput.stdout) { + return undefined; + } + + const pixiInfo: PixiInfo = JSON.parse(infoOutput.stdout); + return pixiInfo; + } catch (error) { + traceWarn(`Failed to get pixi info for ${cwd}`, error); + return undefined; + } + } + + /** + * Returns the command line arguments to run `python` within a specific pixi environment. + * @param manifestPath The path to the manifest file used by pixi. + * @param envName The name of the environment in the pixi project + * @param isolatedFlag Whether to add `-I` to the python invocation. + * @returns A list of arguments that can be passed to exec. + */ + public getRunPythonArgs(manifestPath: string, envName?: string, isolatedFlag = false): string[] { + let python = [this.command, 'run', '--manifest-path', manifestPath]; + if (isNonDefaultPixiEnvironmentName(envName)) { + python = python.concat(['--environment', envName]); + } + + python.push('python'); + if (isolatedFlag) { + python.push('-I'); + } + return [...python, OUTPUT_MARKER_SCRIPT]; + } + + /** + * Starting from Pixi 0.24.0, each environment has a special file that records some information + * about which manifest created the environment. + * + * @param envDir The root directory (or prefix) of a conda environment + */ + + // eslint-disable-next-line class-methods-use-this + @cache(5_000, true, 10_000) + async getPixiEnvironmentMetadata(envDir: string): Promise { + const pixiPath = path.join(envDir, 'conda-meta/pixi'); + try { + const result: PixiEnvMetadata | undefined = await readJSON(pixiPath); + return result; + } catch (e) { + traceVerbose(`Failed to get pixi environment metadata for ${envDir}`, e); + } + return undefined; + } +} + +async function getPixiTool(): Promise { + let pixi = getPythonSetting(PIXITOOLPATH_SETTING_KEY); + + if (!pixi || pixi === 'pixi' || !(await pathExists(pixi))) { + pixi = undefined; + const paths = await findPixiOnPath(); + for (const p of paths) { + if (await pathExists(p)) { + pixi = p; + break; + } + } + } + + if (!pixi) { + // Check the default installation location + const home = getUserHomeDir(); + if (home) { + const pixiToolPath = path.join(home, '.pixi', 'bin', isWindows() ? 'pixi.exe' : 'pixi'); + if (await pathExists(pixiToolPath)) { + pixi = pixiToolPath; + } + } + } + + return pixi ? new Pixi(pixi) : undefined; +} + +/** + * Locating pixi binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ +let _pixi: Promise | undefined; + +/** + * Returns a Pixi instance corresponding to the binary which can be used to run commands for the cwd. + * + * Pixi commands can be slow and so can be bottleneck to overall discovery time. So trigger command + * execution as soon as possible. To do that we need to ensure the operations before the command are + * performed synchronously. + */ +export function getPixi(): Promise { + if (_pixi === undefined || isTestExecution()) { + _pixi = getPixiTool(); + } + return _pixi; +} + +export type PixiEnvironmentInfo = { + interpreterPath: string; + pixi: Pixi; + pixiVersion: string; + manifestPath: string; + envName?: string; +}; + +function isPixiProjectDir(pixiProjectDir: string): boolean { + const paths = getWorkspaceFolderPaths().map((f) => path.normalize(f)); + const normalized = path.normalize(pixiProjectDir); + return paths.some((p) => p === normalized); +} + +/** + * Given the location of an interpreter, try to deduce information about the environment in which it + * resides. + * @param interpreterPath The full path to the interpreter. + * @param pixi Optionally a pixi instance. If this is not specified it will be located. + * @returns Information about the pixi environment. + */ +export async function getPixiEnvironmentFromInterpreter( + interpreterPath: string, +): Promise { + if (!interpreterPath) { + return undefined; + } + + const prefix = getPrefixFromInterpreterPath(interpreterPath); + const pixi = await getPixi(); + if (!pixi) { + traceVerbose(`could not find a pixi interpreter for the interpreter at ${interpreterPath}`); + return undefined; + } + + // Check if the environment has pixi metadata that we can source. + const metadata = await pixi.getPixiEnvironmentMetadata(prefix); + if (metadata !== undefined) { + return { + interpreterPath, + pixi, + pixiVersion: metadata.pixi_version, + manifestPath: metadata.manifest_path, + envName: metadata.environment_name, + }; + } + + // Otherwise, we'll have to try to deduce this information. + + // Usually the pixi environments are stored under `/.pixi/envs//`. So, + // we walk backwards to determine the project directory. + let envName: string | undefined; + let envsDir: string; + let dotPixiDir: string; + let pixiProjectDir: string; + let pixiInfo: PixiInfo | undefined; + + try { + envName = path.basename(prefix); + envsDir = path.dirname(prefix); + dotPixiDir = path.dirname(envsDir); + pixiProjectDir = path.dirname(dotPixiDir); + if (!isPixiProjectDir(pixiProjectDir)) { + traceVerbose(`could not determine the pixi project directory for the interpreter at ${interpreterPath}`); + return undefined; + } + + // Invoke pixi to get information about the pixi project + pixiInfo = await pixi.getPixiInfo(pixiProjectDir); + + if (!pixiInfo || !pixiInfo.project_info) { + traceWarn(`failed to determine pixi project information for the interpreter at ${interpreterPath}`); + return undefined; + } + + return { + interpreterPath, + pixi, + pixiVersion: pixiInfo.version, + manifestPath: pixiInfo.project_info.manifest_path, + envName, + }; + } catch (error) { + traceWarn('Error processing paths or getting Pixi Info:', error); + } + + return undefined; +} + +/** + * Returns true if the given environment name is *not* the default environment. + */ +export function isNonDefaultPixiEnvironmentName(envName?: string): envName is string { + return envName !== 'default'; +} + +export function registerPixiFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangePythonSetting(PIXITOOLPATH_SETTING_KEY, () => { + _pixi = getPixiTool(); + }), + ); +} + +/** + * Returns the `pixi run` command + */ +export async function getRunPixiPythonCommand(pythonPath: string): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'run', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + args.push('python'); + return args; +} + +export async function getPixiActivationCommands( + pythonPath: string, + _targetShell?: TerminalShellType, +): Promise { + const pixiEnv = await getPixiEnvironmentFromInterpreter(pythonPath); + if (!pixiEnv) { + return undefined; + } + + const args = [ + pixiEnv.pixi.command.toCommandArgumentForPythonExt(), + 'shell', + '--manifest-path', + pixiEnv.manifestPath.toCommandArgumentForPythonExt(), + ]; + if (isNonDefaultPixiEnvironmentName(pixiEnv.envName)) { + args.push('--environment'); + args.push(pixiEnv.envName.toCommandArgumentForPythonExt()); + } + + // const pixiTargetShell = shellTypeToPixiShell(targetShell); + // if (pixiTargetShell) { + // args.push('--shell'); + // args.push(pixiTargetShell); + // } + + // const shellHookOutput = await exec(pixiEnv.pixi.command, args, { + // throwOnStdErr: false, + // }).catch(traceError); + // if (!shellHookOutput) { + // return undefined; + // } + + // return splitLines(shellHookOutput.stdout, { + // removeEmptyEntries: true, + // trim: true, + // }); + return [args.join(' ')]; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/poetry.ts b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts new file mode 100644 index 000000000000..5e5fa2416208 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/poetry.ts @@ -0,0 +1,343 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import { getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { + getPythonSetting, + isParentPath, + pathExists, + pathExistsSync, + readFile, + shellExecute, +} from '../externalDependencies'; +import { getEnvironmentDirFromPath } from '../commonUtils'; +import { isVirtualenvEnvironment } from './simplevirtualenvs'; +import { StopWatch } from '../../../common/utils/stopWatch'; +import { cache } from '../../../common/utils/decorators'; +import { isTestExecution } from '../../../common/constants'; +import { traceError, traceVerbose } from '../../../logging'; +import { splitLines } from '../../../common/stringUtils'; + +/** + * Global virtual env dir for a project is named as: + * + * --py. + * + * Implementation details behind and are too + * much to rely upon, so for our purposes the best we can do is the following regex. + */ +const globalPoetryEnvDirRegex = /^(.+)-(.+)-py(\d).(\d){1,2}$/; + +/** + * Checks if the given interpreter belongs to a global poetry environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +async function isGlobalPoetryEnvironment(interpreterPath: string): Promise { + const envDir = getEnvironmentDirFromPath(interpreterPath); + return globalPoetryEnvDirRegex.test(path.basename(envDir)) ? isVirtualenvEnvironment(interpreterPath) : false; +} +/** + * Local poetry environments are created by the `virtualenvs.in-project` setting , which always names the environment + * folder '.venv': https://python-poetry.org/docs/configuration/#virtualenvsin-project-boolean + */ +export const localPoetryEnvDirName = '.venv'; + +/** + * Checks if the given interpreter belongs to a local poetry environment, i.e environment is located inside the project. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +async function isLocalPoetryEnvironment(interpreterPath: string): Promise { + // This is the layout we wish to verify. + // project + // |__ pyproject.toml <--- check if this exists + // |__ .venv <--- check if name of the folder is '.venv' + // |__ Scripts/bin + // |__ python <--- interpreterPath + const envDir = getEnvironmentDirFromPath(interpreterPath); + if (path.basename(envDir) !== localPoetryEnvDirName) { + return false; + } + const project = path.dirname(envDir); + if (!(await hasValidPyprojectToml(project))) { + return false; + } + // The assumption is that we need to be able to run poetry CLI for an environment in order to mark it as poetry. + // For that we can either further verify, + // - 'pyproject.toml' is valid toml + // - 'pyproject.toml' has a poetry section which contains the necessary fields + // - Poetry configuration allows local virtual environments + // ... possibly more + // Or we can try running poetry to find the related environment instead. Launching poetry binaries although + // reliable, can be expensive. So report the best effort type instead, i.e this is likely a poetry env. + return true; +} + +/** + * Checks if the given interpreter belongs to a poetry environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isPoetryEnvironment(interpreterPath: string): Promise { + if (await isGlobalPoetryEnvironment(interpreterPath)) { + return true; + } + if (await isLocalPoetryEnvironment(interpreterPath)) { + return true; + } + return false; +} + +const POETRY_TIMEOUT = 50000; + +/** Wraps the "poetry" utility, and exposes its functionality. + */ +export class Poetry { + /** + * Locating poetry binary can be expensive, since it potentially involves spawning or + * trying to spawn processes; so we only do it once per session. + */ + private static poetryPromise: Map> = new Map< + string, + Promise + >(); + + /** + * Creates a Poetry service corresponding to the corresponding "poetry" command. + * + * @param command - Command used to run poetry. This has the same meaning as the + * first argument of spawn() - i.e. it can be a full path, or just a binary name. + * @param cwd - The working directory to use as cwd when running poetry. + */ + constructor(public readonly command: string, private cwd: string) { + this.fixCwd(); + } + + /** + * Returns a Poetry instance corresponding to the binary which can be used to run commands for the cwd. + * + * Poetry commands can be slow and so can be bottleneck to overall discovery time. So trigger command + * execution as soon as possible. To do that we need to ensure the operations before the command are + * performed synchronously. + */ + public static async getPoetry(cwd: string): Promise { + // Following check should be performed synchronously so we trigger poetry execution as soon as possible. + if (!(await hasValidPyprojectToml(cwd))) { + // This check is not expensive and may change during a session, so we need not cache it. + return undefined; + } + if (Poetry.poetryPromise.get(cwd) === undefined || isTestExecution()) { + Poetry.poetryPromise.set(cwd, Poetry.locate(cwd)); + } + return Poetry.poetryPromise.get(cwd); + } + + private static async locate(cwd: string): Promise { + // First thing this method awaits on should be poetry command execution, hence perform all operations + // before that synchronously. + + traceVerbose(`Getting poetry for cwd ${cwd}`); + // Produce a list of candidate binaries to be probed by exec'ing them. + function* getCandidates() { + try { + const customPoetryPath = getPythonSetting('poetryPath'); + if (customPoetryPath && customPoetryPath !== 'poetry') { + // If user has specified a custom poetry path, use it first. + yield customPoetryPath; + } + } catch (ex) { + traceError(`Failed to get poetry setting`, ex); + } + // Check unqualified filename, in case it's on PATH. + yield 'poetry'; + const home = getUserHomeDir(); + if (home) { + const defaultPoetryPath = path.join(home, '.poetry', 'bin', 'poetry'); + if (pathExistsSync(defaultPoetryPath)) { + yield defaultPoetryPath; + } + } + } + + // Probe the candidates, and pick the first one that exists and does what we need. + for (const poetryPath of getCandidates()) { + traceVerbose(`Probing poetry binary for ${cwd}: ${poetryPath}`); + const poetry = new Poetry(poetryPath, cwd); + const virtualenvs = await poetry.getEnvList(); + if (virtualenvs !== undefined) { + traceVerbose(`Found poetry via filesystem probing for ${cwd}: ${poetryPath}`); + return poetry; + } + traceVerbose(`Failed to find poetry for ${cwd}: ${poetryPath}`); + } + + // Didn't find anything. + traceVerbose(`No poetry binary found for ${cwd}`); + return undefined; + } + + /** + * Retrieves list of Python environments known to this poetry for this working directory. + * Returns `undefined` if we failed to spawn because the binary doesn't exist or isn't on PATH, + * or the current user doesn't have execute permissions for it, or this poetry couldn't handle + * command line arguments that we passed (indicating an old version that we do not support, or + * poetry has not been setup properly for the cwd). + * + * Corresponds to "poetry env list --full-path". Swallows errors if any. + */ + public async getEnvList(): Promise { + return this.getEnvListCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(30_000, true, 10_000) + private async getEnvListCached(_cwd: string): Promise { + const result = await this.safeShellExecute(`${this.command} env list --full-path`); + if (!result) { + return undefined; + } + /** + * We expect stdout to contain something like: + * + * \poetry_2-tutorial-project-6hnqYwvD-py3.7 + * \poetry_2-tutorial-project-6hnqYwvD-py3.8 + * \poetry_2-tutorial-project-6hnqYwvD-py3.9 (Activated) + * + * So we'll need to remove the string "(Activated)" after splitting lines to get the full path. + */ + const activated = '(Activated)'; + const res = await Promise.all( + splitLines(result.stdout).map(async (line) => { + if (line.endsWith(activated)) { + line = line.slice(0, -activated.length); + } + const folder = line.trim(); + return (await pathExists(folder)) ? folder : undefined; + }), + ); + return res.filter((r) => r !== undefined).map((r) => r!); + } + + /** + * Retrieves interpreter path of the currently activated virtual environment for this working directory. + * Corresponds to "poetry env info -p". Swallows errors if any. + */ + public async getActiveEnvPath(): Promise { + return this.getActiveEnvPathCached(this.cwd); + } + + /** + * Method created to facilitate caching. The caching decorator uses function arguments as cache key, + * so pass in cwd on which we need to cache. + */ + @cache(20_000, true, 10_000) + private async getActiveEnvPathCached(_cwd: string): Promise { + const result = await this.safeShellExecute(`${this.command} env info -p`, true); + if (!result) { + return undefined; + } + return result.stdout.trim(); + } + + /** + * Retrieves `virtualenvs.path` setting for this working directory. `virtualenvs.path` setting defines where virtual + * environments are created for the directory. Corresponds to "poetry config virtualenvs.path". Swallows errors if any. + */ + public async getVirtualenvsPathSetting(): Promise { + const result = await this.safeShellExecute(`${this.command} config virtualenvs.path`); + if (!result) { + return undefined; + } + return result.stdout.trim(); + } + + /** + * Due to an upstream poetry issue on Windows https://github.com/python-poetry/poetry/issues/3829, + * 'poetry env list' does not handle case-insensitive paths as cwd, which are valid on Windows. + * So we need to pass the case-exact path as cwd. + * It has been observed that only the drive letter in `cwd` is lowercased here. Unfortunately, + * there's no good way to get case of the drive letter correctly without using Win32 APIs: + * https://stackoverflow.com/questions/33086985/how-to-obtain-case-exact-path-of-a-file-in-node-js-on-windows + * So we do it manually. + */ + private fixCwd(): void { + if (getOSType() === OSType.Windows) { + if (/^[a-z]:/.test(this.cwd)) { + // Replace first character by the upper case version of the character. + const a = this.cwd.split(':'); + a[0] = a[0].toUpperCase(); + this.cwd = a.join(':'); + } + } + } + + private async safeShellExecute(command: string, logVerbose = false) { + // It has been observed that commands related to conda or poetry binary take upto 10-15 seconds unlike + // python binaries. So have a large timeout. + const stopWatch = new StopWatch(); + const result = await shellExecute(command, { + cwd: this.cwd, + throwOnStdErr: true, + timeout: POETRY_TIMEOUT, + }).catch((ex) => { + if (logVerbose) { + traceVerbose(ex); + } else { + traceError(ex); + } + return undefined; + }); + traceVerbose(`Time taken to run ${command} in ms`, stopWatch.elapsedTime); + return result; + } +} + +/** + * Returns true if interpreter path belongs to a poetry environment which is associated with a particular folder, + * false otherwise. + * @param interpreterPath Absolute path to any python interpreter. + * @param folder Absolute path to the folder. + * @param poetryPath Poetry command to use to calculate the result. + */ +export async function isPoetryEnvironmentRelatedToFolder( + interpreterPath: string, + folder: string, + poetryPath?: string, +): Promise { + const poetry = poetryPath ? new Poetry(poetryPath, folder) : await Poetry.getPoetry(folder); + const pathToEnv = await poetry?.getActiveEnvPath(); + if (!pathToEnv) { + return false; + } + return isParentPath(interpreterPath, pathToEnv); +} + +/** + * Does best effort to verify whether a folder has been setup for poetry, by looking for "valid" pyproject.toml file. + * Note "valid" is best effort here, i.e we only verify the minimal features. + * + * @param folder Folder to look for pyproject.toml file in. + */ +async function hasValidPyprojectToml(folder: string): Promise { + const pyprojectToml = path.join(folder, 'pyproject.toml'); + if (!pathExistsSync(pyprojectToml)) { + return false; + } + const content = await readFile(pyprojectToml); + if (!content.includes('[tool.poetry]')) { + return false; + } + // It may still be the case that. + // - pyproject.toml is not a valid toml file + // - Some fields are not setup properly for poetry or are missing + // ... possibly more + // But we only wish to verify the minimal features. + return true; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts b/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts new file mode 100644 index 000000000000..8556e6f19f90 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/pyenv.ts @@ -0,0 +1,264 @@ +import * as path from 'path'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { arePathsSame, isParentPath, pathExists, shellExecute } from '../externalDependencies'; +import { traceVerbose } from '../../../logging'; + +export function getPyenvDir(): string { + // Check if the pyenv environment variables exist: PYENV on Windows, PYENV_ROOT on Unix. + // They contain the path to pyenv's installation folder. + // If they don't exist, use the default path: ~/.pyenv/pyenv-win on Windows, ~/.pyenv on Unix. + // If the interpreter path starts with the path to the pyenv folder, then it is a pyenv environment. + // See https://github.com/pyenv/pyenv#locating-the-python-installation for general usage, + // And https://github.com/pyenv-win/pyenv-win for Windows specifics. + let pyenvDir = getEnvironmentVariable('PYENV_ROOT') ?? getEnvironmentVariable('PYENV'); + + if (!pyenvDir) { + const homeDir = getUserHomeDir() || ''; + pyenvDir = + getOSType() === OSType.Windows ? path.join(homeDir, '.pyenv', 'pyenv-win') : path.join(homeDir, '.pyenv'); + } + + return pyenvDir; +} + +let pyenvBinary: string | undefined; + +export function setPyEnvBinary(pyenvBin: string): void { + pyenvBinary = pyenvBin; +} + +async function getPyenvBinary(): Promise { + if (pyenvBinary && (await pathExists(pyenvBinary))) { + return pyenvBinary; + } + + const pyenvDir = getPyenvDir(); + const pyenvBin = path.join(pyenvDir, 'bin', 'pyenv'); + if (await pathExists(pyenvBin)) { + return pyenvBin; + } + return 'pyenv'; +} + +export async function getActivePyenvForDirectory(cwd: string): Promise { + const pyenvBin = await getPyenvBinary(); + try { + const pyenvInterpreterPath = await shellExecute(`${pyenvBin} which python`, { cwd }); + return pyenvInterpreterPath.stdout.trim(); + } catch (ex) { + traceVerbose(ex); + return undefined; + } +} + +export function getPyenvVersionsDir(): string { + return path.join(getPyenvDir(), 'versions'); +} + +/** + * Checks if a given directory path is same as `pyenv` shims path. This checks + * `~/.pyenv/shims` on posix and `~/.pyenv/pyenv-win/shims` on windows. + * @param {string} dirPath: Absolute path to any directory + * @returns {boolean}: Returns true if the patch is same as `pyenv` shims directory. + */ + +export function isPyenvShimDir(dirPath: string): boolean { + const shimPath = path.join(getPyenvDir(), 'shims'); + return arePathsSame(shimPath, dirPath) || arePathsSame(`${shimPath}${path.sep}`, dirPath); +} +/** + * Checks if the given interpreter belongs to a pyenv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean}: Returns true if the interpreter belongs to a pyenv environment. + */ + +export async function isPyenvEnvironment(interpreterPath: string): Promise { + const pathToCheck = interpreterPath; + const pyenvDir = getPyenvDir(); + + if (!(await pathExists(pyenvDir))) { + return false; + } + + return isParentPath(pathToCheck, pyenvDir); +} + +export interface IPyenvVersionStrings { + pythonVer?: string; + distro?: string; + distroVer?: string; +} +/** + * This function provides parsers for some of the common and known distributions + * supported by pyenv. To get the list of supported pyenv distributions, run + * `pyenv install --list` + * + * The parsers below were written based on the list obtained from pyenv version 1.2.21 + */ +function getKnownPyenvVersionParsers(): Map IPyenvVersionStrings | undefined> { + /** + * This function parses versions that are plain python versions. + * @param str string to parse + * + * Parses : + * 2.7.18 + * 3.9.0 + */ + function pythonOnly(str: string): IPyenvVersionStrings { + return { + pythonVer: str, + distro: undefined, + distroVer: undefined, + }; + } + + /** + * This function parses versions that are distro versions. + * @param str string to parse + * + * Examples: + * miniconda3-4.7.12 + * anaconda3-2020.07 + */ + function distroOnly(str: string): IPyenvVersionStrings | undefined { + const parts = str.split('-'); + if (parts.length === 3) { + return { + pythonVer: undefined, + distroVer: `${parts[1]}-${parts[2]}`, + distro: parts[0], + }; + } + + if (parts.length === 2) { + return { + pythonVer: undefined, + distroVer: parts[1], + distro: parts[0], + }; + } + + return { + pythonVer: undefined, + distroVer: undefined, + distro: str, + }; + } + + /** + * This function parser pypy environments supported by the pyenv install command + * @param str string to parse + * + * Examples: + * pypy-c-jit-latest + * pypy-c-nojit-latest + * pypy-dev + * pypy-stm-2.3 + * pypy-stm-2.5.1 + * pypy-1.5-src + * pypy-1.5 + * pypy3.5-5.7.1-beta-src + * pypy3.5-5.7.1-beta + * pypy3.5-5.8.0-src + * pypy3.5-5.8.0 + */ + function pypyParser(str: string): IPyenvVersionStrings | undefined { + const pattern = /[0-9\.]+/; + + const parts = str.split('-'); + const pythonVer = parts[0].search(pattern) > 0 ? parts[0].substr('pypy'.length) : undefined; + if (parts.length === 2) { + return { + pythonVer, + distroVer: parts[1], + distro: 'pypy', + }; + } + + if ( + parts.length === 3 && + (parts[2].startsWith('src') || + parts[2].startsWith('beta') || + parts[2].startsWith('alpha') || + parts[2].startsWith('win64')) + ) { + const part1 = parts[1].startsWith('v') ? parts[1].substr(1) : parts[1]; + return { + pythonVer, + distroVer: `${part1}-${parts[2]}`, + distro: 'pypy', + }; + } + + if (parts.length === 3 && parts[1] === 'stm') { + return { + pythonVer, + distroVer: parts[2], + distro: `${parts[0]}-${parts[1]}`, + }; + } + + if (parts.length === 4 && parts[1] === 'c') { + return { + pythonVer, + distroVer: parts[3], + distro: `pypy-${parts[1]}-${parts[2]}`, + }; + } + + if (parts.length === 4 && parts[3].startsWith('src')) { + return { + pythonVer, + distroVer: `${parts[1]}-${parts[2]}-${parts[3]}`, + distro: 'pypy', + }; + } + + return { + pythonVer, + distroVer: undefined, + distro: 'pypy', + }; + } + + const parsers: Map IPyenvVersionStrings | undefined> = new Map(); + parsers.set('activepython', distroOnly); + parsers.set('anaconda', distroOnly); + parsers.set('graalpython', distroOnly); + parsers.set('ironpython', distroOnly); + parsers.set('jython', distroOnly); + parsers.set('micropython', distroOnly); + parsers.set('miniconda', distroOnly); + parsers.set('miniforge', distroOnly); + parsers.set('pypy', pypyParser); + parsers.set('pyston', distroOnly); + parsers.set('stackless', distroOnly); + parsers.set('3', pythonOnly); + parsers.set('2', pythonOnly); + + return parsers; +} +/** + * This function parses the name of the commonly installed versions of pyenv based environments. + * @param str string to parse. + * + * Remarks: Depending on the environment, the name itself can contain distribution info like + * name and version. Sometimes it may also have python version as a part of the name. This function + * extracts the various strings. + */ + +export function parsePyenvVersion(str: string): IPyenvVersionStrings | undefined { + const allParsers = getKnownPyenvVersionParsers(); + const knownPrefixes = Array.from(allParsers.keys()); + + const parsers = knownPrefixes + .filter((k) => str.startsWith(k)) + .map((p) => allParsers.get(p)) + .filter((p) => p !== undefined); + + if (parsers.length > 0 && parsers[0]) { + return parsers[0](str); + } + + return undefined; +} diff --git a/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts new file mode 100644 index 000000000000..0ad24252f341 --- /dev/null +++ b/src/client/pythonEnvironments/common/environmentManagers/simplevirtualenvs.ts @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as fsapi from '../../../common/platform/fs-paths'; +import '../../../common/extensions'; +import { splitLines } from '../../../common/stringUtils'; +import { getEnvironmentVariable, getOSType, getUserHomeDir, OSType } from '../../../common/utils/platform'; +import { PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../base/info'; +import { comparePythonVersionSpecificity } from '../../base/info/env'; +import { parseBasicVersion, parseRelease, parseVersion } from '../../base/info/pythonVersion'; +import { isParentPath, pathExists, readFile } from '../externalDependencies'; + +function getPyvenvConfigPathsFrom(interpreterPath: string): string[] { + const pyvenvConfigFile = 'pyvenv.cfg'; + + // Check if the pyvenv.cfg file is in the parent directory relative to the interpreter. + // env + // |__ pyvenv.cfg <--- check if this file exists + // |__ bin or Scripts + // |__ python <--- interpreterPath + const venvPath1 = path.join(path.dirname(path.dirname(interpreterPath)), pyvenvConfigFile); + + // Check if the pyvenv.cfg file is in the directory as the interpreter. + // env + // |__ pyvenv.cfg <--- check if this file exists + // |__ python <--- interpreterPath + const venvPath2 = path.join(path.dirname(interpreterPath), pyvenvConfigFile); + + // The paths are ordered in the most common to least common + return [venvPath1, venvPath2]; +} + +/** + * Checks if the given interpreter is a virtual environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isVirtualEnvironment(interpreterPath: string): Promise { + return isVenvEnvironment(interpreterPath); +} + +/** + * Checks if the given interpreter belongs to a venv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a venv environment. + */ +export async function isVenvEnvironment(interpreterPath: string): Promise { + const venvPaths = getPyvenvConfigPathsFrom(interpreterPath); + + // We don't need to test all at once, testing each one here + for (const venvPath of venvPaths) { + if (await pathExists(venvPath)) { + return true; + } + } + return false; +} + +/** + * Checks if the given interpreter belongs to a virtualenv based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean} : Returns true if the interpreter belongs to a virtualenv environment. + */ +export async function isVirtualenvEnvironment(interpreterPath: string): Promise { + // Check if there are any activate.* files in the same directory as the interpreter. + // + // env + // |__ activate, activate.* <--- check if any of these files exist + // |__ python <--- interpreterPath + const directory = path.dirname(interpreterPath); + const files = await fsapi.readdir(directory); + const regex = /^activate(\.([A-z]|\d)+)?$/i; + + return files.find((file) => regex.test(file)) !== undefined; +} + +async function getDefaultVirtualenvwrapperDir(): Promise { + const homeDir = getUserHomeDir() || ''; + + // In Windows, the default path for WORKON_HOME is %USERPROFILE%\Envs. + // If 'Envs' is not available we should default to '.virtualenvs'. Since that + // is also valid for windows. + if (getOSType() === OSType.Windows) { + // ~/Envs with uppercase 'E' is the default home dir for + // virtualEnvWrapper. + const envs = path.join(homeDir, 'Envs'); + if (await pathExists(envs)) { + return envs; + } + } + return path.join(homeDir, '.virtualenvs'); +} + +function getWorkOnHome(): Promise { + // The WORKON_HOME variable contains the path to the root directory of all virtualenvwrapper environments. + // If the interpreter path belongs to one of them then it is a virtualenvwrapper type of environment. + const workOnHome = getEnvironmentVariable('WORKON_HOME'); + if (workOnHome) { + return Promise.resolve(workOnHome); + } + return getDefaultVirtualenvwrapperDir(); +} + +/** + * Checks if the given interpreter belongs to a virtualenvWrapper based environment. + * @param {string} interpreterPath: Absolute path to the python interpreter. + * @returns {boolean}: Returns true if the interpreter belongs to a virtualenvWrapper environment. + */ +export async function isVirtualenvwrapperEnvironment(interpreterPath: string): Promise { + const workOnHomeDir = await getWorkOnHome(); + + // For environment to be a virtualenvwrapper based it has to follow these two rules: + // 1. It should be in a sub-directory under the WORKON_HOME + // 2. It should be a valid virtualenv environment + return ( + (await pathExists(workOnHomeDir)) && + isParentPath(interpreterPath, workOnHomeDir) && + isVirtualenvEnvironment(interpreterPath) + ); +} + +/** + * Extracts version information from pyvenv.cfg near a given interpreter. + * @param interpreterPath Absolute path to the interpreter + * + * Remarks: This function looks for pyvenv.cfg usually in the same or parent directory. + * Reads the pyvenv.cfg and finds the line that looks like 'version = 3.9.0`. Gets the + * version string from that lines and parses it. + */ +export async function getPythonVersionFromPyvenvCfg(interpreterPath: string): Promise { + const configPaths = getPyvenvConfigPathsFrom(interpreterPath); + let version = UNKNOWN_PYTHON_VERSION; + + // We want to check each of those locations in the order. There is no need to look at + // all of them in parallel. + for (const configPath of configPaths) { + if (await pathExists(configPath)) { + try { + const lines = splitLines(await readFile(configPath)); + + const pythonVersions = lines + .map((line) => { + const parts = line.split('='); + if (parts.length === 2) { + const name = parts[0].toLowerCase().trim(); + const value = parts[1].trim(); + if (name === 'version') { + try { + return parseVersion(value); + } catch (ex) { + return undefined; + } + } else if (name === 'version_info') { + try { + return parseVersionInfo(value); + } catch (ex) { + return undefined; + } + } + } + return undefined; + }) + .filter((v) => v !== undefined) + .map((v) => v!); + + if (pythonVersions.length > 0) { + for (const v of pythonVersions) { + if (comparePythonVersionSpecificity(v, version) > 0) { + version = v; + } + } + } + } catch (ex) { + // There is only ome pyvenv.cfg. If we found it but failed to parse it + // then just return here. No need to look for versions any further. + return UNKNOWN_PYTHON_VERSION; + } + } + } + + return version; +} + +/** + * Convert the given string into the corresponding Python version object. + * Example: + * 3.9.0.final.0 + * 3.9.0.alpha.1 + * 3.9.0.beta.2 + * 3.9.0.candidate.1 + * + * Does not parse: + * 3.9.0 + * 3.9.0a1 + * 3.9.0b2 + * 3.9.0rc1 + */ +function parseVersionInfo(versionInfoStr: string): PythonVersion { + let version: PythonVersion; + let after: string; + try { + [version, after] = parseBasicVersion(versionInfoStr); + } catch { + // XXX Use getEmptyVersion(). + return UNKNOWN_PYTHON_VERSION; + } + if (version.micro !== -1 && after.startsWith('.')) { + [version.release] = parseRelease(after); + } + return version; +} diff --git a/src/client/pythonEnvironments/common/externalDependencies.ts b/src/client/pythonEnvironments/common/externalDependencies.ts new file mode 100644 index 000000000000..b0922f8bab06 --- /dev/null +++ b/src/client/pythonEnvironments/common/externalDependencies.ts @@ -0,0 +1,207 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as fsapi from '../../common/platform/fs-paths'; +import { IWorkspaceService } from '../../common/application/types'; +import { ExecutionResult, IProcessServiceFactory, ShellOptions, SpawnOptions } from '../../common/process/types'; +import { IDisposable, IConfigurationService, IExperimentService } from '../../common/types'; +import { chain, iterable } from '../../common/utils/async'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { IServiceContainer } from '../../ioc/types'; +import { traceError, traceVerbose } from '../../logging'; + +let internalServiceContainer: IServiceContainer; +export function initializeExternalDependencies(serviceContainer: IServiceContainer): void { + internalServiceContainer = serviceContainer; +} + +// processes + +export async function shellExecute(command: string, options: ShellOptions = {}): Promise> { + const useWorker = false; + const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; + return service.shellExec(command, options); +} + +export async function exec( + file: string, + args: string[], + options: SpawnOptions = {}, + useWorker = false, +): Promise> { + const service = await internalServiceContainer.get(IProcessServiceFactory).create(); + options = { ...options, useWorker }; + return service.exec(file, args, options); +} + +export function inExperiment(experimentName: string): boolean { + const service = internalServiceContainer.get(IExperimentService); + return service.inExperimentSync(experimentName); +} + +// Workspace + +export function isVirtualWorkspace(): boolean { + const service = internalServiceContainer.get(IWorkspaceService); + return service.isVirtualWorkspace; +} + +// filesystem + +export function pathExists(absPath: string): Promise { + return fsapi.pathExists(absPath); +} + +export function pathExistsSync(absPath: string): boolean { + return fsapi.pathExistsSync(absPath); +} + +export function readFile(filePath: string): Promise { + return fsapi.readFile(filePath, 'utf-8'); +} + +export function readFileSync(filePath: string): string { + return fsapi.readFileSync(filePath, 'utf-8'); +} + +/** + * Returns true if given file path exists within the given parent directory, false otherwise. + * @param filePath File path to check for + * @param parentPath The potential parent path to check for + */ +export function isParentPath(filePath: string, parentPath: string): boolean { + if (!parentPath.endsWith(path.sep)) { + parentPath += path.sep; + } + if (!filePath.endsWith(path.sep)) { + filePath += path.sep; + } + return normCasePath(filePath).startsWith(normCasePath(parentPath)); +} + +export async function isDirectory(filename: string): Promise { + const stat = await fsapi.lstat(filename); + return stat.isDirectory(); +} + +export function normalizePath(filename: string): string { + return path.normalize(filename); +} + +export function resolvePath(filename: string): string { + return path.resolve(filename); +} + +export function normCasePath(filePath: string): string { + return getOSType() === OSType.Windows ? path.normalize(filePath).toUpperCase() : path.normalize(filePath); +} + +export function arePathsSame(path1: string, path2: string): boolean { + return normCasePath(path1) === normCasePath(path2); +} + +export async function resolveSymbolicLink(absPath: string, stats?: fsapi.Stats, count?: number): Promise { + stats = stats ?? (await fsapi.lstat(absPath)); + if (stats.isSymbolicLink()) { + if (count && count > 5) { + traceError(`Detected a potential symbolic link loop at ${absPath}, terminating resolution.`); + return absPath; + } + const link = await fsapi.readlink(absPath); + // Result from readlink is not guaranteed to be an absolute path. For eg. on Mac it resolves + // /usr/local/bin/python3.9 -> ../../../Library/Frameworks/Python.framework/Versions/3.9/bin/python3.9 + // + // The resultant path is reported relative to the symlink directory we resolve. Convert that to absolute path. + const absLinkPath = path.isAbsolute(link) ? link : path.resolve(path.dirname(absPath), link); + count = count ? count + 1 : 1; + return resolveSymbolicLink(absLinkPath, undefined, count); + } + return absPath; +} + +export async function getFileInfo(filePath: string): Promise<{ ctime: number; mtime: number }> { + try { + const data = await fsapi.lstat(filePath); + return { + ctime: data.ctime.valueOf(), + mtime: data.mtime.valueOf(), + }; + } catch (ex) { + // This can fail on some cases, such as, `reparse points` on windows. So, return the + // time as -1. Which we treat as not set in the extension. + traceVerbose(`Failed to get file info for ${filePath}`, ex); + return { ctime: -1, mtime: -1 }; + } +} + +export async function isFile(filePath: string): Promise { + const stats = await fsapi.lstat(filePath); + if (stats.isSymbolicLink()) { + const resolvedPath = await resolveSymbolicLink(filePath, stats); + const resolvedStats = await fsapi.lstat(resolvedPath); + return resolvedStats.isFile(); + } + return stats.isFile(); +} + +/** + * Returns full path to sub directories of a given directory. + * @param {string} root : path to get sub-directories from. + * @param options : If called with `resolveSymlinks: true`, then symlinks found in + * the directory are resolved and if they resolve to directories + * then resolved values are returned. + */ +export async function* getSubDirs( + root: string, + options?: { resolveSymlinks?: boolean }, +): AsyncIterableIterator { + const dirContents = await fsapi.readdir(root, { withFileTypes: true }); + const generators = dirContents.map((item) => { + async function* generator() { + const fullPath = path.join(root, item.name); + if (item.isDirectory()) { + yield fullPath; + } else if (options?.resolveSymlinks && item.isSymbolicLink()) { + // The current FS item is a symlink. It can potentially be a file + // or a directory. Resolve it first and then check if it is a directory. + const resolvedPath = await resolveSymbolicLink(fullPath); + const resolvedPathStat = await fsapi.lstat(resolvedPath); + if (resolvedPathStat.isDirectory()) { + yield resolvedPath; + } + } + } + + return generator(); + }); + + yield* iterable(chain(generators)); +} + +/** + * Returns the value for setting `python.`. + * @param name The name of the setting. + */ +export function getPythonSetting(name: string, root?: string): T | undefined { + const resource = root ? vscode.Uri.file(root) : undefined; + const settings = internalServiceContainer.get(IConfigurationService).getSettings(resource); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (settings as any)[name]; +} + +/** + * Registers the listener to be called when a particular setting changes. + * @param name The name of the setting. + * @param callback The listener function to be called when the setting changes. + */ +export function onDidChangePythonSetting(name: string, callback: () => void, root?: string): IDisposable { + return vscode.workspace.onDidChangeConfiguration((event: vscode.ConfigurationChangeEvent) => { + const scope = root ? vscode.Uri.file(root) : undefined; + if (event.affectsConfiguration(`python.${name}`, scope)) { + callback(); + } + }); +} diff --git a/src/client/pythonEnvironments/common/posixUtils.ts b/src/client/pythonEnvironments/common/posixUtils.ts new file mode 100644 index 000000000000..8149706a5707 --- /dev/null +++ b/src/client/pythonEnvironments/common/posixUtils.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as fs from 'fs'; +import * as path from 'path'; +import { uniq } from 'lodash'; +import * as fsapi from '../../common/platform/fs-paths'; +import { getSearchPathEntries } from '../../common/utils/exec'; +import { resolveSymbolicLink } from './externalDependencies'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../logging'; + +/** + * Determine if the given filename looks like the simplest Python executable. + */ +export function matchBasicPythonBinFilename(filename: string): boolean { + return path.basename(filename) === 'python'; +} + +/** + * Checks if a given path matches pattern for standard non-windows python binary. + * @param {string} interpreterPath : Path to python interpreter. + * @returns {boolean} : Returns true if the path matches pattern for non-windows python binary. + */ +export function matchPythonBinFilename(filename: string): boolean { + /** + * This Reg-ex matches following file names: + * python + * python3 + * python38 + * python3.8 + */ + const posixPythonBinPattern = /^python(\d+(\.\d+)?)?$/; + + return posixPythonBinPattern.test(path.basename(filename)); +} + +export async function commonPosixBinPaths(): Promise { + const searchPaths = getSearchPathEntries(); + + const paths: string[] = Array.from( + new Set( + [ + '/bin', + '/etc', + '/lib', + '/lib/x86_64-linux-gnu', + '/lib64', + '/sbin', + '/snap/bin', + '/usr/bin', + '/usr/games', + '/usr/include', + '/usr/lib', + '/usr/lib/x86_64-linux-gnu', + '/usr/lib64', + '/usr/libexec', + '/usr/local', + '/usr/local/bin', + '/usr/local/etc', + '/usr/local/games', + '/usr/local/lib', + '/usr/local/sbin', + '/usr/sbin', + '/usr/share', + '~/.local/bin', + ].concat(searchPaths), + ), + ); + + const exists = await Promise.all(paths.map((p) => fsapi.pathExists(p))); + return paths.filter((_, index) => exists[index]); +} + +/** + * Finds python interpreter binaries or symlinks in a given directory. + * @param searchDir : Directory to search in + * @returns : Paths to python binaries found in the search directory. + */ +async function findPythonBinariesInDir(searchDir: string) { + return (await fs.promises.readdir(searchDir, { withFileTypes: true })) + .filter((dirent: fs.Dirent) => !dirent.isDirectory()) + .map((dirent: fs.Dirent) => path.join(searchDir, dirent.name)) + .filter(matchPythonBinFilename); +} + +/** + * Pick the shortest versions of the paths. The paths could be + * the binary itself or its symlink, whichever path is shorter. + * + * E.g: + * /usr/bin/python -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + * /usr/bin/python3 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + * /usr/bin/python3.7 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + * + * Of the 4 possible paths to same binary (3 symlinks and 1 binary path), + * the code below will pick '/usr/bin/python'. + */ +function pickShortestPath(pythonPaths: string[]) { + let shortestLen = pythonPaths[0].length; + let shortestPath = pythonPaths[0]; + for (const p of pythonPaths) { + if (p.length <= shortestLen) { + shortestLen = p.length; + shortestPath = p; + } + } + return shortestPath; +} + +/** + * Finds python binaries in given directories. This function additionally reduces the + * found binaries to unique set be resolving symlinks, and returns the shortest paths + * to the said unique binaries. + * @param searchDirs : Directories to search for python binaries + * @returns : Unique paths to python interpreters found in the search dirs. + */ +export async function getPythonBinFromPosixPaths(searchDirs: string[]): Promise { + const binToLinkMap = new Map(); + for (const searchDir of searchDirs) { + const paths = await findPythonBinariesInDir(searchDir).catch((ex) => { + traceWarn('Looking for python binaries within', searchDir, 'failed with', ex); + return []; + }); + + for (const filepath of paths) { + // Ensure that we have a collection of unique global binaries by + // resolving all symlinks to the target binaries. + try { + traceVerbose(`Attempting to resolve symbolic link: ${filepath}`); + const resolvedBin = await resolveSymbolicLink(filepath); + if (binToLinkMap.has(resolvedBin)) { + binToLinkMap.get(resolvedBin)?.push(filepath); + } else { + binToLinkMap.set(resolvedBin, [filepath]); + } + traceInfo(`Found: ${filepath} --> ${resolvedBin}`); + } catch (ex) { + traceError('Failed to resolve symbolic link: ', ex); + } + } + } + + // Pick the shortest versions of the paths. The paths could be + // the binary itself or its symlink, whichever path is shorter. + // + // E.g: + // /usr/bin/python -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + // /usr/bin/python3 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + // /usr/bin/python3.7 -> /System/Library/Frameworks/Python.framework/Versions/3.7/lib/python3.7 + // + // Of the 4 possible paths to same binary (3 symlinks and 1 binary path), + // the code below will pick '/usr/bin/python'. + const keys = Array.from(binToLinkMap.keys()); + const pythonPaths = keys.map((key) => pickShortestPath([key, ...(binToLinkMap.get(key) ?? [])])); + return uniq(pythonPaths); +} diff --git a/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts b/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts new file mode 100644 index 000000000000..efc7d56409c8 --- /dev/null +++ b/src/client/pythonEnvironments/common/pythonBinariesWatcher.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as minimatch from 'minimatch'; +import * as path from 'path'; +import { FileChangeType, watchLocationForPattern } from '../../common/platform/fileSystemWatcher'; +import { IDisposable } from '../../common/types'; +import { getOSType, OSType } from '../../common/utils/platform'; +import { traceVerbose } from '../../logging'; + +const [executable, binName] = getOSType() === OSType.Windows ? ['python.exe', 'Scripts'] : ['python', 'bin']; + +/** + * Start watching the given directory for changes to files matching the glob. + * + * @param baseDir - the root to which the glob is applied while watching + * @param callback - called when the event happens + * @param executableGlob - matches the executable under the directory + */ +export function watchLocationForPythonBinaries( + baseDir: string, + callback: (type: FileChangeType, absPath: string) => void, + executableGlob: string = executable, +): IDisposable { + const resolvedGlob = path.posix.normalize(executableGlob); + const [baseGlob] = resolvedGlob.split('/').slice(-1); + function callbackClosure(type: FileChangeType, e: string) { + traceVerbose('Received event', type, JSON.stringify(e), 'for baseglob', baseGlob); + const isMatch = minimatch.default(path.basename(e), baseGlob, { nocase: getOSType() === OSType.Windows }); + if (!isMatch) { + // When deleting the file for some reason path to all directories leading up to python are reported + // Skip those events + return; + } + callback(type, e); + } + return watchLocationForPattern(baseDir, resolvedGlob, callbackClosure); +} + +// eslint-disable-next-line no-shadow +export enum PythonEnvStructure { + Standard = 'standard', + Flat = 'flat', +} + +/** + * Generate the globs to use when watching a directory for Python executables. + */ +export function resolvePythonExeGlobs( + basenameGlob = executable, + // Be default we always expect a "standard" structure. + structure = PythonEnvStructure.Standard, +): string[] { + if (path.posix.normalize(basenameGlob).includes('/')) { + throw Error(`invalid basename glob "${basenameGlob}"`); + } + const globs: string[] = []; + if (structure === PythonEnvStructure.Standard) { + globs.push( + // Check the directory. + basenameGlob, + // Check in all subdirectories. + `*/${basenameGlob}`, + // Check in the "bin" directory of all subdirectories. + `*/${binName}/${basenameGlob}`, + ); + } else if (structure === PythonEnvStructure.Flat) { + // Check only the directory. + globs.push(basenameGlob); + } + return globs; +} diff --git a/src/client/pythonEnvironments/common/registryKeys.worker.ts b/src/client/pythonEnvironments/common/registryKeys.worker.ts new file mode 100644 index 000000000000..05996d057f11 --- /dev/null +++ b/src/client/pythonEnvironments/common/registryKeys.worker.ts @@ -0,0 +1,24 @@ +import { Registry } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryKey } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryKeys(keys: IRegistryKey[]): IRegistryKey[] { + // Use the map function to create a new array with copies of the specified properties. + return keys.map((key) => ({ + hive: key.hive, + arch: key.arch, + key: key.key, + })); +} + +regKey.keys((err: Error, res: Registry[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryKeys(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/src/client/pythonEnvironments/common/registryValues.worker.ts b/src/client/pythonEnvironments/common/registryValues.worker.ts new file mode 100644 index 000000000000..eaef7cbd58a7 --- /dev/null +++ b/src/client/pythonEnvironments/common/registryValues.worker.ts @@ -0,0 +1,27 @@ +import { RegistryItem } from 'winreg'; +import { parentPort, workerData } from 'worker_threads'; +import { IRegistryValue } from './windowsRegistry'; + +const WinReg = require('winreg'); + +const regKey = new WinReg(workerData); + +function copyRegistryValues(values: IRegistryValue[]): IRegistryValue[] { + // Use the map function to create a new array with copies of the specified properties. + return values.map((value) => ({ + hive: value.hive, + arch: value.arch, + key: value.key, + name: value.name, + type: value.type, + value: value.value, + })); +} + +regKey.values((err: Error, res: RegistryItem[]) => { + if (!parentPort) { + throw new Error('Not in a worker thread'); + } + const messageRes = copyRegistryValues(res); + parentPort.postMessage({ err, res: messageRes }); +}); diff --git a/src/client/pythonEnvironments/common/windowsRegistry.ts b/src/client/pythonEnvironments/common/windowsRegistry.ts new file mode 100644 index 000000000000..801ef0c907b1 --- /dev/null +++ b/src/client/pythonEnvironments/common/windowsRegistry.ts @@ -0,0 +1,60 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { HKCU, HKLM, Options, REG_SZ, Registry, RegistryItem } from 'winreg'; +import * as path from 'path'; +import { createDeferred } from '../../common/utils/async'; +import { executeWorkerFile } from '../../common/process/worker/main'; + +export { HKCU, HKLM, REG_SZ, Options }; + +export interface IRegistryKey { + hive: string; + arch: string; + key: string; + parentKey?: IRegistryKey; +} + +export interface IRegistryValue { + hive: string; + arch: string; + key: string; + name: string; + type: string; + value: string; +} + +export async function readRegistryValues(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.values((err: Error, res: RegistryItem[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryValues.worker.js'), options); +} + +export async function readRegistryKeys(options: Options, useWorkerThreads: boolean): Promise { + if (!useWorkerThreads) { + // eslint-disable-next-line global-require + const WinReg = require('winreg'); + const regKey = new WinReg(options); + const deferred = createDeferred(); + regKey.keys((err: Error, res: Registry[]) => { + if (err) { + deferred.reject(err); + } + deferred.resolve(res); + }); + return deferred.promise; + } + return executeWorkerFile(path.join(__dirname, 'registryKeys.worker.js'), options); +} diff --git a/src/client/pythonEnvironments/common/windowsUtils.ts b/src/client/pythonEnvironments/common/windowsUtils.ts new file mode 100644 index 000000000000..fe15f71522a5 --- /dev/null +++ b/src/client/pythonEnvironments/common/windowsUtils.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { uniqBy } from 'lodash'; +import * as path from 'path'; +import { isTestExecution } from '../../common/constants'; +import { traceError, traceVerbose } from '../../logging'; +import { + HKCU, + HKLM, + IRegistryKey, + IRegistryValue, + readRegistryKeys, + readRegistryValues, + REG_SZ, +} from './windowsRegistry'; + +/* eslint-disable global-require */ + +/** + * Determine if the given filename looks like the simplest Python executable. + */ +export function matchBasicPythonBinFilename(filename: string): boolean { + return path.basename(filename).toLowerCase() === 'python.exe'; +} + +/** + * Checks if a given path ends with python*.exe + * @param {string} interpreterPath : Path to python interpreter. + * @returns {boolean} : Returns true if the path matches pattern for windows python executable. + */ +export function matchPythonBinFilename(filename: string): boolean { + /** + * This Reg-ex matches following file names: + * python.exe + * python3.exe + * python38.exe + * python3.8.exe + */ + const windowsPythonExes = /^python(\d+(.\d+)?)?\.exe$/; + + return windowsPythonExes.test(path.basename(filename)); +} + +export interface IRegistryInterpreterData { + interpreterPath: string; + versionStr?: string; + sysVersionStr?: string; + bitnessStr?: string; + companyDisplayName?: string; + distroOrgName?: string; +} + +async function getInterpreterDataFromKey( + { arch, hive, key }: IRegistryKey, + distroOrgName: string, + useWorkerThreads: boolean, +): Promise { + const result: IRegistryInterpreterData = { + interpreterPath: '', + distroOrgName, + }; + + const values: IRegistryValue[] = await readRegistryValues({ arch, hive, key }, useWorkerThreads); + for (const value of values) { + switch (value.name) { + case 'SysArchitecture': + result.bitnessStr = value.value; + break; + case 'SysVersion': + result.sysVersionStr = value.value; + break; + case 'Version': + result.versionStr = value.value; + break; + case 'DisplayName': + result.companyDisplayName = value.value; + break; + default: + break; + } + } + + const subKeys: IRegistryKey[] = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); + const subKey = subKeys.map((s) => s.key).find((s) => s.endsWith('InstallPath')); + if (subKey) { + const subKeyValues: IRegistryValue[] = await readRegistryValues({ arch, hive, key: subKey }, useWorkerThreads); + const value = subKeyValues.find((v) => v.name === 'ExecutablePath'); + if (value) { + result.interpreterPath = value.value; + if (value.type !== REG_SZ) { + traceVerbose(`Registry interpreter path type [${value.type}]: ${value.value}`); + } + } + } + + if (result.interpreterPath.length > 0) { + return result; + } + return undefined; +} + +export async function getInterpreterDataFromRegistry( + arch: string, + hive: string, + key: string, + useWorkerThreads: boolean, +): Promise { + const subKeys = await readRegistryKeys({ arch, hive, key }, useWorkerThreads); + const distroOrgName = key.substr(key.lastIndexOf('\\') + 1); + const allData = await Promise.all( + subKeys.map((subKey) => getInterpreterDataFromKey(subKey, distroOrgName, useWorkerThreads)), + ); + return (allData.filter((data) => data !== undefined) || []) as IRegistryInterpreterData[]; +} + +let registryInterpretersCache: IRegistryInterpreterData[] | undefined; + +/** + * Returns windows registry interpreters from memory, returns undefined if memory is empty. + * getRegistryInterpreters() must be called prior to this to populate memory. + */ +export function getRegistryInterpretersSync(): IRegistryInterpreterData[] | undefined { + return !isTestExecution() ? registryInterpretersCache : undefined; +} + +let registryInterpretersPromise: Promise | undefined; + +export async function getRegistryInterpreters(): Promise { + if (!isTestExecution() && registryInterpretersPromise !== undefined) { + return registryInterpretersPromise; + } + registryInterpretersPromise = getRegistryInterpretersImpl(); + return registryInterpretersPromise; +} + +async function getRegistryInterpretersImpl(useWorkerThreads = false): Promise { + let registryData: IRegistryInterpreterData[] = []; + + for (const arch of ['x64', 'x86']) { + for (const hive of [HKLM, HKCU]) { + const root = '\\SOFTWARE\\Python'; + let keys: string[] = []; + try { + keys = (await readRegistryKeys({ arch, hive, key: root }, useWorkerThreads)).map((k) => k.key); + } catch (ex) { + traceError(`Failed to access Registry: ${arch}\\${hive}\\${root}`, ex); + } + + for (const key of keys) { + registryData = registryData.concat( + await getInterpreterDataFromRegistry(arch, hive, key, useWorkerThreads), + ); + } + } + } + registryInterpretersCache = uniqBy(registryData, (r: IRegistryInterpreterData) => r.interpreterPath); + return registryInterpretersCache; +} diff --git a/src/client/pythonEnvironments/creation/common/commonUtils.ts b/src/client/pythonEnvironments/creation/common/commonUtils.ts new file mode 100644 index 000000000000..8b6ffe1af450 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/commonUtils.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { Commands } from '../../../common/constants'; +import { Common } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; +import { showErrorMessage } from '../../../common/vscodeApis/windowApis'; +import { isWindows } from '../../../common/utils/platform'; + +export async function showErrorMessageWithLogs(message: string): Promise { + const result = await showErrorMessage(message, Common.openOutputPanel, Common.selectPythonInterpreter); + if (result === Common.openOutputPanel) { + await executeCommand(Commands.ViewOutput); + } else if (result === Common.selectPythonInterpreter) { + await executeCommand(Commands.Set_Interpreter); + } +} + +export function getVenvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.venv'); +} + +export async function hasVenv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(path.join(getVenvPath(workspaceFolder), 'pyvenv.cfg')); +} + +export function getVenvExecutable(workspaceFolder: WorkspaceFolder): string { + if (isWindows()) { + return path.join(getVenvPath(workspaceFolder), 'Scripts', 'python.exe'); + } + return path.join(getVenvPath(workspaceFolder), 'bin', 'python'); +} + +export function getPrefixCondaEnvPath(workspaceFolder: WorkspaceFolder): string { + return path.join(workspaceFolder.uri.fsPath, '.conda'); +} + +export async function hasPrefixCondaEnv(workspaceFolder: WorkspaceFolder): Promise { + return fs.pathExists(getPrefixCondaEnvPath(workspaceFolder)); +} diff --git a/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts new file mode 100644 index 000000000000..eccbf64a7866 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/createEnvTriggerUtils.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { getPipRequirementsFiles } from '../provider/venvUtils'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID } from '../../../common/constants'; +import { PythonExtension } from '../../../api/types'; +import { traceVerbose } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { getWorkspaceStateValue } from '../../../common/persistentState'; + +export const CREATE_ENV_TRIGGER_SETTING_PART = 'createEnvironment.trigger'; +export const CREATE_ENV_TRIGGER_SETTING = `python.${CREATE_ENV_TRIGGER_SETTING_PART}`; + +export async function fileContainsInlineDependencies(_uri: Uri): Promise { + // This is a placeholder for the real implementation of inline dependencies support + // For now we don't detect anything. Once PEP-722/PEP-723 are accepted we can implement + // this properly. + return false; +} + +export async function hasRequirementFiles(workspace: WorkspaceFolder): Promise { + const files = await getPipRequirementsFiles(workspace); + const found = (files?.length ?? 0) > 0; + if (found) { + traceVerbose(`Found requirement files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function hasKnownFiles(workspace: WorkspaceFolder): Promise { + const filePaths: string[] = [ + 'poetry.lock', + 'conda.yaml', + 'environment.yaml', + 'conda.yml', + 'environment.yml', + 'Pipfile', + 'Pipfile.lock', + ].map((fileName) => path.join(workspace.uri.fsPath, fileName)); + const result = await Promise.all(filePaths.map((f) => fsapi.pathExists(f))); + const found = result.some((r) => r); + if (found) { + traceVerbose(`Found known files: ${workspace.uri.fsPath}`); + } + return found; +} + +export async function isGlobalPythonSelected(workspace: WorkspaceFolder): Promise { + const extension = getExtension(PVSC_EXTENSION_ID); + if (!extension) { + return false; + } + const extensionApi: PythonExtension = extension.exports as PythonExtension; + const interpreter = extensionApi.environments.getActiveEnvironmentPath(workspace.uri); + const details = await extensionApi.environments.resolveEnvironment(interpreter); + const isGlobal = details?.environment === undefined; + if (isGlobal) { + traceVerbose(`Selected python for [${workspace.uri.fsPath}] is [global] type: ${interpreter.path}`); + } + return isGlobal; +} + +/** + * Checks the setting `python.createEnvironment.trigger` to see if we should perform the checks + * to prompt to create an environment. + * Returns True if we should prompt to create an environment. + */ +export function shouldPromptToCreateEnv(): boolean { + const config = getConfiguration('python'); + if (config) { + const value = config.get(CREATE_ENV_TRIGGER_SETTING_PART, 'off'); + return value !== 'off'; + } + + return getWorkspaceStateValue(CREATE_ENV_TRIGGER_SETTING, 'off') !== 'off'; +} + +/** + * Sets `python.createEnvironment.trigger` to 'off' in the user settings. + */ +export function disableCreateEnvironmentTrigger(): void { + const config = getConfiguration('python'); + if (config) { + config.update('createEnvironment.trigger', 'off', ConfigurationTarget.Global); + } +} + +let _alreadyCreateEnvCriteriaCheck = false; +/** + * Run-once wrapper function for the workspace check to prompt to create an environment. + * @returns : True if we should prompt to c environment. + */ +export function isCreateEnvWorkspaceCheckNotRun(): boolean { + if (_alreadyCreateEnvCriteriaCheck) { + return false; + } + _alreadyCreateEnvCriteriaCheck = true; + return true; +} diff --git a/src/client/pythonEnvironments/creation/common/installCheckUtils.ts b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts new file mode 100644 index 000000000000..2d8925cc05f6 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/installCheckUtils.ts @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticSeverity, l10n, Range, TextDocument, Uri } from 'vscode'; +import { installedCheckScript } from '../../../common/process/internal/scripts'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { traceInfo, traceVerbose, traceError } from '../../../logging'; +import { getConfiguration } from '../../../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../../../interpreter/contracts'; + +interface PackageDiagnostic { + package: string; + line: number; + character: number; + endLine: number; + endCharacter: number; + code: string; + severity: DiagnosticSeverity; +} + +export const INSTALL_CHECKER_SOURCE = 'Python-InstalledPackagesChecker'; + +function parseDiagnostics(data: string): Diagnostic[] { + let diagnostics: Diagnostic[] = []; + try { + const raw = JSON.parse(data) as PackageDiagnostic[]; + diagnostics = raw.map((item) => { + const d = new Diagnostic( + new Range(item.line, item.character, item.endLine, item.endCharacter), + l10n.t('Package `{0}` is not installed in the selected environment.', item.package), + item.severity, + ); + d.code = { value: item.code, target: Uri.parse(`https://pypi.org/p/${item.package}`) }; + d.source = INSTALL_CHECKER_SOURCE; + return d; + }); + } catch { + diagnostics = []; + } + return diagnostics; +} + +function getMissingPackageSeverity(doc: TextDocument): number { + const config = getConfiguration('python', doc.uri); + const severity: string = config.get('missingPackage.severity', 'Hint'); + if (severity === 'Error') { + return DiagnosticSeverity.Error; + } + if (severity === 'Warning') { + return DiagnosticSeverity.Warning; + } + if (severity === 'Information') { + return DiagnosticSeverity.Information; + } + return DiagnosticSeverity.Hint; +} + +export async function getInstalledPackagesDiagnostics( + interpreterService: IInterpreterService, + doc: TextDocument, +): Promise { + const interpreter = await interpreterService.getActiveInterpreter(doc.uri); + if (!interpreter) { + return []; + } + const scriptPath = installedCheckScript(); + try { + traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath); + const envCopy = { ...process.env, VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}` }; + const result = await plainExec(interpreter.path, [scriptPath, doc.uri.fsPath], { + env: envCopy, + }); + traceVerbose('Installed packages check result:\n', result.stdout); + if (result.stderr) { + traceError('Installed packages check error:\n', result.stderr); + } + return parseDiagnostics(result.stdout); + } catch (ex) { + traceError('Error while getting installed packages check result:\n', ex); + } + return []; +} diff --git a/src/client/pythonEnvironments/creation/common/workspaceSelection.ts b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts new file mode 100644 index 000000000000..3ebab1c67fb4 --- /dev/null +++ b/src/client/pythonEnvironments/creation/common/workspaceSelection.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { CancellationToken, QuickPickItem, WorkspaceFolder } from 'vscode'; +import * as fsapi from '../../../common/platform/fs-paths'; +import { MultiStepAction, showErrorMessage, showQuickPickWithBack } from '../../../common/vscodeApis/windowApis'; +import { getWorkspaceFolders } from '../../../common/vscodeApis/workspaceApis'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; + +function hasVirtualEnv(workspace: WorkspaceFolder): Promise { + return Promise.race([ + fsapi.pathExists(path.join(workspace.uri.fsPath, '.venv')), + fsapi.pathExists(path.join(workspace.uri.fsPath, '.conda')), + ]); +} + +async function getWorkspacesForQuickPick(workspaces: readonly WorkspaceFolder[]): Promise { + const items: QuickPickItem[] = []; + for (const workspace of workspaces) { + items.push({ + label: workspace.name, + detail: workspace.uri.fsPath, + description: (await hasVirtualEnv(workspace)) ? CreateEnv.hasVirtualEnv : undefined, + }); + } + + return items; +} + +export interface PickWorkspaceFolderOptions { + allowMultiSelect?: boolean; + token?: CancellationToken; + preSelectedWorkspace?: WorkspaceFolder; +} + +export async function pickWorkspaceFolder( + options?: PickWorkspaceFolderOptions, + context?: MultiStepAction, +): Promise { + const workspaces = getWorkspaceFolders(); + + if (!workspaces || workspaces.length === 0) { + if (context === MultiStepAction.Back) { + // No workspaces and nothing to show, should just go to previous + throw MultiStepAction.Back; + } + const result = await showErrorMessage(CreateEnv.noWorkspace, Common.openFolder); + if (result === Common.openFolder) { + await executeCommand('vscode.openFolder'); + } + return undefined; + } + + if (options?.preSelectedWorkspace) { + if (context === MultiStepAction.Back) { + // In this case there is no Quick Pick shown, should just go to previous + throw MultiStepAction.Back; + } + + return options.preSelectedWorkspace; + } + + if (workspaces.length === 1) { + if (context === MultiStepAction.Back) { + // In this case there is no Quick Pick shown, should just go to previous + throw MultiStepAction.Back; + } + + return workspaces[0]; + } + + // This is multi-root scenario. + const selected = await showQuickPickWithBack( + await getWorkspacesForQuickPick(workspaces), + { + placeHolder: CreateEnv.pickWorkspacePlaceholder, + ignoreFocusOut: true, + canPickMany: options?.allowMultiSelect, + matchOnDescription: true, + matchOnDetail: true, + }, + options?.token, + ); + + if (selected) { + if (Array.isArray(selected)) { + const details = selected.map((s: QuickPickItem) => s.detail).filter((s) => s !== undefined); + return workspaces.filter((w) => details.includes(w.uri.fsPath)); + } + return workspaces.filter((w) => w.uri.fsPath === (selected as QuickPickItem).detail)[0]; + } + + return undefined; +} diff --git a/src/client/pythonEnvironments/creation/createEnvApi.ts b/src/client/pythonEnvironments/creation/createEnvApi.ts new file mode 100644 index 000000000000..899f57728804 --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvApi.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ConfigurationTarget, Disposable, QuickInputButtons } from 'vscode'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; +import { getCreationEvents, handleCreateEnvironmentCommand } from './createEnvironment'; +import { condaCreationProvider } from './provider/condaCreationProvider'; +import { VenvCreationProvider, VenvCreationProviderId } from './provider/venvCreationProvider'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { CreateEnv } from '../../common/utils/localize'; +import { + CreateEnvironmentProvider, + CreateEnvironmentOptions, + CreateEnvironmentResult, + ProposedCreateEnvironmentAPI, + EnvironmentDidCreateEvent, +} from './proposed.createEnvApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { CreateEnvironmentOptionsInternal } from './types'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { PythonEnvironment } from '../../envExt/types'; + +class CreateEnvironmentProviders { + private _createEnvProviders: CreateEnvironmentProvider[] = []; + + constructor() { + this._createEnvProviders = []; + } + + public add(provider: CreateEnvironmentProvider) { + if (this._createEnvProviders.filter((p) => p.id === provider.id).length > 0) { + throw new Error(`Create Environment provider with id ${provider.id} already registered`); + } + this._createEnvProviders.push(provider); + } + + public remove(provider: CreateEnvironmentProvider) { + this._createEnvProviders = this._createEnvProviders.filter((p) => p !== provider); + } + + public getAll(): readonly CreateEnvironmentProvider[] { + return this._createEnvProviders; + } +} + +const _createEnvironmentProviders: CreateEnvironmentProviders = new CreateEnvironmentProviders(); + +export function registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable { + _createEnvironmentProviders.add(provider); + return new Disposable(() => { + _createEnvironmentProviders.remove(provider); + }); +} + +export const { onCreateEnvironmentStarted, onCreateEnvironmentExited, isCreatingEnvironment } = getCreationEvents(); + +export function registerCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + pythonPathUpdater: IPythonPathUpdaterServiceManager, + pathUtils: IPathUtils, +): void { + disposables.push( + registerCommand( + Commands.Create_Environment, + async ( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, + ): Promise => { + if (useEnvExtension()) { + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: undefined, + pythonVersion: undefined, + }); + const result = await executeCommand( + 'python-envs.createAny', + options, + ); + if (result) { + const managerId = result.envId.managerId; + if (managerId === 'ms-python.python:venv') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + } + if (managerId === 'ms-python.python:conda') { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } + return { path: result.environmentPath.path }; + } + } catch (err) { + if (err === QuickInputButtons.Back) { + return { workspaceFolder: undefined, action: 'Back' }; + } + throw err; + } + } else { + const providers = _createEnvironmentProviders.getAll(); + return handleCreateEnvironmentCommand(providers, options); + } + return undefined; + }, + ), + registerCommand( + Commands.Create_Environment_Button, + async (): Promise => { + sendTelemetryEvent(EventName.ENVIRONMENT_BUTTON, undefined, undefined); + await executeCommand(Commands.Create_Environment); + }, + ), + registerCreateEnvironmentProvider(new VenvCreationProvider(interpreterQuickPick)), + registerCreateEnvironmentProvider(condaCreationProvider()), + onCreateEnvironmentExited(async (e: EnvironmentDidCreateEvent) => { + if (e.path && e.options?.selectEnvironment) { + await pythonPathUpdater.updatePythonPath( + e.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + e.workspaceFolder?.uri, + ); + showInformationMessage(`${CreateEnv.informEnvCreation} ${pathUtils.getDisplayName(e.path)}`); + } + }), + ); +} + +export function buildEnvironmentCreationApi(): ProposedCreateEnvironmentAPI { + return { + onWillCreateEnvironment: onCreateEnvironmentStarted, + onDidCreateEnvironment: onCreateEnvironmentExited, + createEnvironment: async ( + options?: CreateEnvironmentOptions | undefined, + ): Promise => { + const providers = _createEnvironmentProviders.getAll(); + try { + return await handleCreateEnvironmentCommand(providers, options); + } catch (err) { + return { path: undefined, workspaceFolder: undefined, action: undefined, error: err as Error }; + } + }, + registerCreateEnvironmentProvider: (provider: CreateEnvironmentProvider) => + registerCreateEnvironmentProvider(provider), + }; +} + +export async function createVirtualEnvironment(options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal) { + const provider = _createEnvironmentProviders.getAll().find((p) => p.id === VenvCreationProviderId); + if (!provider) { + return; + } + return handleCreateEnvironmentCommand([provider], { ...options, providerId: provider.id }); +} diff --git a/src/client/pythonEnvironments/creation/createEnvButtonContext.ts b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts new file mode 100644 index 000000000000..4ce7d07ad69d --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvButtonContext.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { getConfiguration, onDidChangeConfiguration } from '../../common/vscodeApis/workspaceApis'; + +async function setShowCreateEnvButtonContextKey(): Promise { + const config = getConfiguration('python'); + const showCreateEnvButton = config.get('createEnvironment.contentButton', 'show') === 'show'; + await executeCommand('setContext', 'showCreateEnvButton', showCreateEnvButton); +} + +export function registerCreateEnvironmentButtonFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidChangeConfiguration(async () => { + await setShowCreateEnvButtonContextKey(); + }), + ); + + setShowCreateEnvButtonContextKey(); +} diff --git a/src/client/pythonEnvironments/creation/createEnvironment.ts b/src/client/pythonEnvironments/creation/createEnvironment.ts new file mode 100644 index 000000000000..c7c4e84f445c --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironment.ts @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Event, EventEmitter, QuickInputButtons, QuickPickItem } from 'vscode'; +import { CreateEnv } from '../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPick, + showQuickPickWithBack, +} from '../../common/vscodeApis/windowApis'; +import { traceError, traceVerbose } from '../../logging'; +import { + CreateEnvironmentOptions, + CreateEnvironmentResult, + CreateEnvironmentProvider, + EnvironmentWillCreateEvent, + EnvironmentDidCreateEvent, +} from './proposed.createEnvApis'; +import { CreateEnvironmentOptionsInternal } from './types'; + +const onCreateEnvironmentStartedEvent = new EventEmitter(); +const onCreateEnvironmentExitedEvent = new EventEmitter(); + +let startedEventCount = 0; + +function isBusyCreatingEnvironment(): boolean { + return startedEventCount > 0; +} + +function fireStartedEvent(options?: CreateEnvironmentOptions): void { + onCreateEnvironmentStartedEvent.fire({ options }); + startedEventCount += 1; +} + +function fireExitedEvent(result?: CreateEnvironmentResult, options?: CreateEnvironmentOptions, error?: Error): void { + startedEventCount -= 1; + if (result) { + onCreateEnvironmentExitedEvent.fire({ options, ...result }); + } else if (error) { + onCreateEnvironmentExitedEvent.fire({ options, error }); + } +} + +export function getCreationEvents(): { + onCreateEnvironmentStarted: Event; + onCreateEnvironmentExited: Event; + isCreatingEnvironment: () => boolean; +} { + return { + onCreateEnvironmentStarted: onCreateEnvironmentStartedEvent.event, + onCreateEnvironmentExited: onCreateEnvironmentExitedEvent.event, + isCreatingEnvironment: isBusyCreatingEnvironment, + }; +} + +async function createEnvironment( + provider: CreateEnvironmentProvider, + options: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise { + let result: CreateEnvironmentResult | undefined; + let err: Error | undefined; + try { + fireStartedEvent(options); + result = await provider.createEnvironment(options); + } catch (ex) { + if (ex === QuickInputButtons.Back) { + traceVerbose('Create Env: User clicked back button during environment creation'); + if (!options.showBackButton) { + return undefined; + } + } + err = ex as Error; + throw err; + } finally { + fireExitedEvent(result, options, err); + } + return result; +} + +interface CreateEnvironmentProviderQuickPickItem extends QuickPickItem { + id: string; +} + +async function showCreateEnvironmentQuickPick( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise { + const items: CreateEnvironmentProviderQuickPickItem[] = providers.map((p) => ({ + label: p.name, + description: p.description, + id: p.id, + })); + + if (options?.providerId) { + const provider = providers.find((p) => p.id === options.providerId); + if (provider) { + return provider; + } + } + + let selectedItem: CreateEnvironmentProviderQuickPickItem | CreateEnvironmentProviderQuickPickItem[] | undefined; + + if (options?.showBackButton) { + selectedItem = await showQuickPickWithBack(items, { + placeHolder: CreateEnv.providersQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }); + } else { + selectedItem = await showQuickPick(items, { + placeHolder: CreateEnv.providersQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }); + } + + if (selectedItem) { + const selected = Array.isArray(selectedItem) ? selectedItem[0] : selectedItem; + if (selected) { + const selections = providers.filter((p) => p.id === selected.id); + if (selections.length > 0) { + return selections[0]; + } + } + } + return undefined; +} + +function getOptionsWithDefaults( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): CreateEnvironmentOptions & CreateEnvironmentOptionsInternal { + return { + installPackages: true, + ignoreSourceControl: true, + showBackButton: false, + selectEnvironment: true, + ...options, + }; +} + +export async function handleCreateEnvironmentCommand( + providers: readonly CreateEnvironmentProvider[], + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, +): Promise { + const optionsWithDefaults = getOptionsWithDefaults(options); + let selectedProvider: CreateEnvironmentProvider | undefined; + const envTypeStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + if (providers.length > 0) { + try { + selectedProvider = await showCreateEnvironmentQuickPick(providers, optionsWithDefaults); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (!selectedProvider) { + return MultiStepAction.Cancel; + } + } else { + traceError('No Environment Creation providers were registered.'); + if (context === MultiStepAction.Back) { + // There are no providers to select, so just step back. + return MultiStepAction.Back; + } + } + return MultiStepAction.Continue; + }, + undefined, + ); + + let result: CreateEnvironmentResult | undefined; + const createStep = new MultiStepNode( + envTypeStep, + async (context?: MultiStepAction) => { + if (context === MultiStepAction.Back) { + // This step is to trigger creation, which can go into other extension. + return MultiStepAction.Back; + } + if (selectedProvider) { + try { + result = await createEnvironment(selectedProvider, optionsWithDefaults); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } + return MultiStepAction.Continue; + }, + undefined, + ); + envTypeStep.next = createStep; + + const action = await MultiStepNode.run(envTypeStep); + if (options?.showBackButton) { + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + result = { action, workspaceFolder: undefined, path: undefined, error: undefined }; + } + } + + if (result) { + return Object.freeze(result); + } + return undefined; +} diff --git a/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts new file mode 100644 index 000000000000..5119290a0c2d --- /dev/null +++ b/src/client/pythonEnvironments/creation/createEnvironmentTrigger.ts @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { + fileContainsInlineDependencies, + hasKnownFiles, + hasRequirementFiles, + isGlobalPythonSelected, + shouldPromptToCreateEnv, + isCreateEnvWorkspaceCheckNotRun, + disableCreateEnvironmentTrigger, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder } from '../../common/vscodeApis/workspaceApis'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { hasPrefixCondaEnv, hasVenv } from './common/commonUtils'; +import { showInformationMessage } from '../../common/vscodeApis/windowApis'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { executeCommand, registerCommand } from '../../common/vscodeApis/commandApis'; +import { Commands } from '../../common/constants'; +import { Resource } from '../../common/types'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +export enum CreateEnvironmentCheckKind { + /** + * Checks if environment creation is needed based on file location and content. + */ + File = 'file', + + /** + * Checks if environment creation is needed based on workspace contents. + */ + Workspace = 'workspace', +} + +export interface CreateEnvironmentTriggerOptions { + force?: boolean; +} + +async function createEnvironmentCheckForWorkspace(uri: Uri): Promise { + const workspace = getWorkspaceFolder(uri); + if (!workspace) { + traceInfo(`CreateEnv Trigger - Workspace not found for ${uri.fsPath}`); + return; + } + + const missingRequirements = async (workspaceFolder: WorkspaceFolder) => + !(await hasRequirementFiles(workspaceFolder)); + + const isNonGlobalPythonSelected = async (workspaceFolder: WorkspaceFolder) => + !(await isGlobalPythonSelected(workspaceFolder)); + + // Skip showing the Create Environment prompt if one of the following is True: + // 1. The workspace already has a ".venv" or ".conda" env + // 2. The workspace does NOT have "requirements.txt" or "requirements/*.txt" files + // 3. The workspace has known files for other environment types like environment.yml, conda.yml, poetry.lock, etc. + // 4. The selected python is NOT classified as a global python interpreter + const skipPrompt: boolean = ( + await Promise.all([ + hasVenv(workspace), + hasPrefixCondaEnv(workspace), + missingRequirements(workspace), + hasKnownFiles(workspace), + isNonGlobalPythonSelected(workspace), + ]) + ).some((r) => r); + + if (skipPrompt) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-not-met' }); + traceInfo(`CreateEnv Trigger - Skipping for ${uri.fsPath}`); + return; + } + + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'criteria-met' }); + const selection = await showInformationMessage( + CreateEnv.Trigger.workspaceTriggerMessage, + CreateEnv.Trigger.createEnvironment, + Common.doNotShowAgain, + ); + + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + await executeCommand(Commands.Create_Environment); + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === Common.doNotShowAgain) { + disableCreateEnvironmentTrigger(); + } +} + +function runOnceWorkspaceCheck(uri: Uri, options: CreateEnvironmentTriggerOptions = {}): Promise { + if (isCreateEnvWorkspaceCheckNotRun() || options?.force) { + return createEnvironmentCheckForWorkspace(uri); + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'already-ran' }); + traceVerbose('CreateEnv Trigger - skipping this because it was already run'); + return Promise.resolve(); +} + +async function createEnvironmentCheckForFile(uri: Uri, options?: CreateEnvironmentTriggerOptions): Promise { + if (await fileContainsInlineDependencies(uri)) { + // TODO: Handle create environment for each file here. + // pending acceptance of PEP-722/PEP-723 + + // For now we do the same thing as for workspace. + await runOnceWorkspaceCheck(uri, options); + } + + // If the file does not have any inline dependencies, then we do the same thing + // as for workspace. + await runOnceWorkspaceCheck(uri, options); +} + +export async function triggerCreateEnvironmentCheck( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): Promise { + if (!uri) { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'no-uri' }); + traceVerbose('CreateEnv Trigger - Skipping No URI provided'); + return; + } + + if (shouldPromptToCreateEnv()) { + if (kind === CreateEnvironmentCheckKind.File) { + await createEnvironmentCheckForFile(uri, options); + } else { + await runOnceWorkspaceCheck(uri, options); + } + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_RESULT, undefined, { result: 'turned-off' }); + traceVerbose('CreateEnv Trigger - turned off in settings'); + } +} + +export function triggerCreateEnvironmentCheckNonBlocking( + kind: CreateEnvironmentCheckKind, + uri: Resource, + options?: CreateEnvironmentTriggerOptions, +): void { + // The Event loop for Node.js runs functions with setTimeout() with lower priority than setImmediate. + // This is done to intentionally avoid blocking anything that the user wants to do. + setTimeout(() => triggerCreateEnvironmentCheck(kind, uri, options).ignoreErrors(), 0); +} + +export function registerCreateEnvironmentTriggers(disposables: Disposable[]): void { + disposables.push( + registerCommand(Commands.Create_Environment_Check, (file: Resource) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'as-command' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file, { force: true }); + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts b/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts new file mode 100644 index 000000000000..76a55bea19a0 --- /dev/null +++ b/src/client/pythonEnvironments/creation/globalPipInTerminalTrigger.ts @@ -0,0 +1,85 @@ +import { Disposable, TerminalShellExecutionStartEvent } from 'vscode'; +import { + disableCreateEnvironmentTrigger, + isGlobalPythonSelected, + shouldPromptToCreateEnv, +} from './common/createEnvTriggerUtils'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { Common, CreateEnv } from '../../common/utils/localize'; +import { traceError, traceInfo } from '../../logging'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { Commands, PVSC_EXTENSION_ID } from '../../common/constants'; +import { CreateEnvironmentResult } from './proposed.createEnvApis'; +import { onDidStartTerminalShellExecution, showWarningMessage } from '../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; + +function checkCommand(command: string): boolean { + const lower = command.toLowerCase(); + return ( + lower.startsWith('pip install') || + lower.startsWith('pip3 install') || + lower.startsWith('python -m pip install') || + lower.startsWith('python3 -m pip install') + ); +} + +export function registerTriggerForPipInTerminal(disposables: Disposable[]): void { + if (!shouldPromptToCreateEnv()) { + return; + } + + const folders = getWorkspaceFolders(); + if (!folders || folders.length === 0) { + return; + } + + const createEnvironmentTriggered: Map = new Map(); + folders.forEach((workspaceFolder) => { + createEnvironmentTriggered.set(workspaceFolder.uri.fsPath, false); + }); + + disposables.push( + onDidStartTerminalShellExecution(async (e: TerminalShellExecutionStartEvent) => { + const workspaceFolder = getWorkspaceFolder(e.shellIntegration.cwd); + if ( + workspaceFolder && + !createEnvironmentTriggered.get(workspaceFolder.uri.fsPath) && + (await isGlobalPythonSelected(workspaceFolder)) + ) { + if (e.execution.commandLine.isTrusted && checkCommand(e.execution.commandLine.value)) { + createEnvironmentTriggered.set(workspaceFolder.uri.fsPath, true); + sendTelemetryEvent(EventName.ENVIRONMENT_TERMINAL_GLOBAL_PIP); + const selection = await showWarningMessage( + CreateEnv.Trigger.globalPipInstallTriggerMessage, + CreateEnv.Trigger.createEnvironment, + Common.doNotShowAgain, + ); + if (selection === CreateEnv.Trigger.createEnvironment) { + try { + const result: CreateEnvironmentResult = await executeCommand(Commands.Create_Environment, { + workspaceFolder, + providerId: `${PVSC_EXTENSION_ID}:venv`, + }); + if (result.path) { + traceInfo('CreateEnv Trigger - Environment created: ', result.path); + traceInfo( + `CreateEnv Trigger - Running: ${ + result.path + } -m ${e.execution.commandLine.value.trim()}`, + ); + e.shellIntegration.executeCommand( + `${result.path} -m ${e.execution.commandLine.value}`.trim(), + ); + } + } catch (error) { + traceError('CreateEnv Trigger - Error while creating environment: ', error); + } + } else if (selection === Common.doNotShowAgain) { + disableCreateEnvironmentTrigger(); + } + } + } + }), + ); +} diff --git a/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts new file mode 100644 index 000000000000..0b55e1ec5ce1 --- /dev/null +++ b/src/client/pythonEnvironments/creation/installedPackagesDiagnostic.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Diagnostic, DiagnosticCollection, TextDocument, Uri } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { createDiagnosticCollection, onDidChangeDiagnostics } from '../../common/vscodeApis/languageApis'; +import { getActiveTextEditor, onDidChangeActiveTextEditor } from '../../common/vscodeApis/windowApis'; +import { + getOpenTextDocuments, + onDidCloseTextDocument, + onDidOpenTextDocument, + onDidSaveTextDocument, +} from '../../common/vscodeApis/workspaceApis'; +import { traceVerbose } from '../../logging'; +import { getInstalledPackagesDiagnostics, INSTALL_CHECKER_SOURCE } from './common/installCheckUtils'; +import { IInterpreterService } from '../../interpreter/contracts'; + +export const DEPS_NOT_INSTALLED_KEY = 'pythonDepsNotInstalled'; + +async function setContextForActiveEditor(diagnosticCollection: DiagnosticCollection): Promise { + const doc = getActiveTextEditor()?.document; + if (doc && (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml'))) { + const diagnostics = diagnosticCollection.get(doc.uri); + if (diagnostics && diagnostics.length > 0) { + traceVerbose(`Setting context for python dependencies not installed: ${doc.uri.fsPath}`); + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, true); + return; + } + } + + // undefined here in the logs means no file was selected + await executeCommand('setContext', DEPS_NOT_INSTALLED_KEY, false); +} + +export function registerInstalledPackagesDiagnosticsProvider( + disposables: IDisposableRegistry, + interpreterService: IInterpreterService, +): void { + const diagnosticCollection = createDiagnosticCollection(INSTALL_CHECKER_SOURCE); + const updateDiagnostics = (uri: Uri, diagnostics: Diagnostic[]) => { + if (diagnostics.length > 0) { + diagnosticCollection.set(uri, diagnostics); + } else if (diagnosticCollection.has(uri)) { + diagnosticCollection.delete(uri); + } + }; + + disposables.push(diagnosticCollection); + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }), + onDidCloseTextDocument((e: TextDocument) => { + updateDiagnostics(e.uri, []); + }), + onDidChangeDiagnostics(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + onDidChangeActiveTextEditor(async () => { + await setContextForActiveEditor(diagnosticCollection); + }), + interpreterService.onDidChangeInterpreter(() => { + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); + }), + ); + + getOpenTextDocuments().forEach(async (doc: TextDocument) => { + if (doc.languageId === 'pip-requirements' || doc.fileName.endsWith('pyproject.toml')) { + const diagnostics = await getInstalledPackagesDiagnostics(interpreterService, doc); + updateDiagnostics(doc.uri, diagnostics); + } + }); +} diff --git a/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts new file mode 100644 index 000000000000..ea520fdd27e2 --- /dev/null +++ b/src/client/pythonEnvironments/creation/proposed.createEnvApis.ts @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Event, Disposable, WorkspaceFolder } from 'vscode'; +import { EnvironmentTools } from '../../api/types'; + +export type CreateEnvironmentUserActions = 'Back' | 'Cancel'; +export type EnvironmentProviderId = string; + +/** + * Options used when creating a Python environment. + */ +export interface CreateEnvironmentOptions { + /** + * Default `true`. If `true`, the environment creation handler is expected to install packages. + */ + installPackages?: boolean; + + /** + * Default `true`. If `true`, the environment creation provider is expected to add the environment to ignore list + * for the source control. + */ + ignoreSourceControl?: boolean; + + /** + * Default `false`. If `true` the creation provider should show back button when showing QuickPick or QuickInput. + */ + showBackButton?: boolean; + + /** + * Default `true`. If `true`, the environment after creation will be selected. + */ + selectEnvironment?: boolean; +} + +/** + * Params passed on `onWillCreateEnvironment` event handler. + */ +export interface EnvironmentWillCreateEvent { + /** + * Options used to create a Python environment. + */ + readonly options: CreateEnvironmentOptions | undefined; +} + +export type CreateEnvironmentResult = + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error?: Error; + } + | { + /** + * Workspace folder associated with the environment. + */ + readonly workspaceFolder?: WorkspaceFolder; + + /** + * Path to the executable python in the environment + */ + readonly path?: string; + + /** + * User action that resulted in exit from the create environment flow. + */ + readonly action?: CreateEnvironmentUserActions; + + /** + * Error if any occurred during environment creation. + */ + readonly error: Error; + }; + +/** + * Params passed on `onDidCreateEnvironment` event handler. + */ +export type EnvironmentDidCreateEvent = CreateEnvironmentResult & { + /** + * Options used to create the Python environment. + */ + readonly options: CreateEnvironmentOptions | undefined; +}; + +/** + * Extensions that want to contribute their own environment creation can do that by registering an object + * that implements this interface. + */ +export interface CreateEnvironmentProvider { + /** + * This API is called when user selects this provider from a QuickPick to select the type of environment + * user wants. This API is expected to show a QuickPick or QuickInput to get the user input and return + * the path to the Python executable in the environment. + * + * @param {CreateEnvironmentOptions} [options] Options used to create a Python environment. + * + * @returns a promise that resolves to the path to the + * Python executable in the environment. Or any action taken by the user, such as back or cancel. + */ + createEnvironment(options?: CreateEnvironmentOptions): Promise; + + /** + * Unique ID for the creation provider, typically : + */ + id: EnvironmentProviderId; + + /** + * Display name for the creation provider. + */ + name: string; + + /** + * Description displayed to the user in the QuickPick to select environment provider. + */ + description: string; + + /** + * Tools used to manage this environment. e.g., ['conda']. In the most to least priority order + * for resolving and working with the environment. + */ + tools: EnvironmentTools[]; +} + +export interface ProposedCreateEnvironmentAPI { + /** + * This API can be used to detect when the environment creation starts for any registered + * provider (including internal providers). This will also receive any options passed in + * or defaults used to create environment. + */ + readonly onWillCreateEnvironment: Event; + + /** + * This API can be used to detect when the environment provider exits for any registered + * provider (including internal providers). This will also receive created environment path, + * any errors, or user actions taken from the provider. + */ + readonly onDidCreateEnvironment: Event; + + /** + * This API will show a QuickPick to select an environment provider from available list of + * providers. Based on the selection the `createEnvironment` will be called on the provider. + */ + createEnvironment(options?: CreateEnvironmentOptions): Promise; + + /** + * This API should be called to register an environment creation provider. It returns + * a (@link Disposable} which can be used to remove the registration. + */ + registerCreateEnvironmentProvider(provider: CreateEnvironmentProvider): Disposable; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts new file mode 100644 index 000000000000..a7e4e9a21cd1 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaCreationProvider.ts @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; +import * as path from 'path'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; +import { traceError, traceInfo, traceLog } from '../../../logging'; +import { CreateEnvironmentProgress } from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { execObservable } from '../../../common/process/rawProcessApis'; +import { createDeferred } from '../../../common/utils/async'; +import { getOSType, OSType } from '../../../common/utils/platform'; +import { createCondaScript } from '../../../common/process/internal/scripts'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + ExistingCondaAction, + deleteEnvironment, + getCondaBaseEnv, + getPathEnvVariableForConda, + pickExistingCondaAction, + pickPythonVersion, +} from './condaUtils'; +import { getPrefixCondaEnvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; +import { EventName } from '../../../telemetry/constants'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { + CondaProgressAndTelemetry, + CONDA_ENV_CREATED_MARKER, + CONDA_ENV_EXISTING_MARKER, +} from './condaProgressAndTelemetry'; +import { splitLines } from '../../../common/stringUtils'; +import { + CreateEnvironmentOptions, + CreateEnvironmentResult, + CreateEnvironmentProvider, +} from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; + +function generateCommandArgs(version?: string, options?: CreateEnvironmentOptions): string[] { + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + + const command: string[] = [createCondaScript()]; + + if (addGitIgnore) { + command.push('--git-ignore'); + } + + if (installPackages) { + command.push('--install'); + } + + if (version) { + command.push('--python'); + command.push(version); + } + + return command; +} + +function getCondaEnvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(CONDA_ENV_CREATED_MARKER) || s.startsWith(CONDA_ENV_EXISTING_MARKER))[0]; + if (envPath.includes(CONDA_ENV_CREATED_MARKER)) { + return envPath.substring(CONDA_ENV_CREATED_MARKER.length); + } + return envPath.substring(CONDA_ENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + +async function createCondaEnv( + workspace: WorkspaceFolder, + command: string, + args: string[], + progress: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + progress.report({ + message: CreateEnv.Conda.creating, + }); + + const deferred = createDeferred(); + const pathEnv = getPathEnvVariableForConda(command); + traceLog('Running Conda Env creation script: ', [command, ...args]); + const { proc, out, dispose } = execObservable(command, args, { + mergeStdOutErr: true, + token, + cwd: workspace.uri.fsPath, + env: { + PATH: pathEnv, + }, + }); + + const progressAndTelemetry = new CondaProgressAndTelemetry(progress); + let condaEnvPath: string | undefined; + out.subscribe( + (value) => { + const output = splitLines(value.out).join('\r\n'); + traceLog(output.trimEnd()); + if (output.includes(CONDA_ENV_CREATED_MARKER) || output.includes(CONDA_ENV_EXISTING_MARKER)) { + condaEnvPath = getCondaEnvFromOutput(output); + } + progressAndTelemetry.process(output); + }, + async (error) => { + traceError('Error while running conda env creation script: ', error); + deferred.reject(error); + }, + () => { + dispose(); + if (proc?.exitCode !== 0) { + traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || `Conda env creation failed with exitCode: ${proc?.exitCode}`, + ); + } else { + deferred.resolve(condaEnvPath); + } + }, + ); + return deferred.promise; +} + +function getExecutableCommand(condaBaseEnvPath: string): string { + if (getOSType() === OSType.Windows) { + // Both Miniconda3 and Anaconda3 have the following structure: + // Miniconda3 (or Anaconda3) + // |- python.exe <--- this is the python that we want. + return path.join(condaBaseEnvPath, 'python.exe'); + } + // On non-windows machines: + // miniconda (or miniforge or anaconda3) + // |- bin + // |- python <--- this is the python that we want. + return path.join(condaBaseEnvPath, 'bin', 'python'); +} + +async function createEnvironment(options?: CreateEnvironmentOptions): Promise { + const conda = await getCondaBaseEnv(); + if (!conda) { + return undefined; + } + + let workspace: WorkspaceFolder | undefined; + const workspaceStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + try { + workspace = (await pickWorkspaceFolder(undefined, context)) as WorkspaceFolder | undefined; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + + if (workspace === undefined) { + traceError('Workspace was not selected or found for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating conda environment.`); + return MultiStepAction.Continue; + }, + undefined, + ); + + let existingCondaAction: ExistingCondaAction | undefined; + const existingEnvStep = new MultiStepNode( + workspaceStep, + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { + try { + existingCondaAction = await pickExistingCondaAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let version: string | undefined; + const versionStep = new MultiStepNode( + workspaceStep, + async (context) => { + if ( + existingCondaAction === ExistingCondaAction.Recreate || + existingCondaAction === ExistingCondaAction.Create + ) { + try { + version = await pickPythonVersion(); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (version === undefined) { + traceError('Python version was not selected for creating conda environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected Python version ${version} for creating conda environment.`); + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + } + + return MultiStepAction.Continue; + }, + undefined, + ); + existingEnvStep.next = versionStep; + + const action = await MultiStepNode.run(workspaceStep); + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + throw action; + } + + if (workspace) { + if (existingCondaAction === ExistingCondaAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, getExecutableCommand(conda))) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'conda', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingCondaAction === ExistingCondaAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'conda', + }); + return { path: getPrefixCondaEnvPath(workspace), workspaceFolder: workspace }; + } + } + + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'conda', + pythonVersion: version, + }); + if (workspace) { + envPath = await createCondaEnv( + workspace, + getExecutableCommand(conda), + generateCommandArgs(version, options), + progress, + token, + ); + + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + + throw new Error('Failed to create conda environment. See Output > Python for more info.'); + } else { + throw new Error('A workspace is needed to create conda environment'); + } + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Conda.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } + + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => createEnvInternal(progress, token), + ); +} + +export function condaCreationProvider(): CreateEnvironmentProvider { + return { + createEnvironment, + name: 'Conda', + + description: CreateEnv.Conda.providerDescription, + + id: `${PVSC_EXTENSION_ID}:conda`, + + tools: ['Conda'], + }; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts new file mode 100644 index 000000000000..e4f4784f15c8 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaDeleteUtils.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { WorkspaceFolder } from 'vscode'; +import { plainExec } from '../../../common/process/rawProcessApis'; +import { CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceInfo } from '../../../logging'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv, showErrorMessageWithLogs } from '../common/commonUtils'; + +export async function deleteCondaEnvironment( + workspace: WorkspaceFolder, + interpreter: string, + pathEnvVar: string, +): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspace); + const command = interpreter; + const args = ['-m', 'conda', 'env', 'remove', '--prefix', condaEnvPath, '--yes']; + try { + traceInfo(`Deleting conda environment: ${condaEnvPath}`); + traceInfo(`Running command: ${command} ${args.join(' ')}`); + const result = await plainExec(command, args, { mergeStdOutErr: true }, { ...process.env, PATH: pathEnvVar }); + traceInfo(result.stdout); + if (await hasPrefixCondaEnv(workspace)) { + // If conda cannot delete files it will name the files as .conda_trash. + // These need to be deleted manually. + traceError(`Conda environment ${condaEnvPath} could not be deleted.`); + traceError(`Please delete the environment manually: ${condaEnvPath}`); + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + return false; + } + } catch (err) { + showErrorMessageWithLogs(CreateEnv.Conda.errorDeletingEnvironment); + traceError(`Deleting conda environment ${condaEnvPath} Failed with error: `, err); + return false; + } + return true; +} diff --git a/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts new file mode 100644 index 000000000000..304e90aec84f --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaProgressAndTelemetry.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const CONDA_ENV_CREATED_MARKER = 'CREATED_CONDA_ENV:'; +export const CONDA_ENV_EXISTING_MARKER = 'EXISTING_CONDA_ENV:'; +export const CONDA_INSTALLING_YML = 'CONDA_INSTALLING_YML:'; +export const CREATE_CONDA_FAILED_MARKER = 'CREATE_CONDA.ENV_FAILED_CREATION'; +export const CREATE_CONDA_INSTALLED_YML = 'CREATE_CONDA.INSTALLED_YML'; +export const CREATE_FAILED_INSTALL_YML = 'CREATE_CONDA.FAILED_INSTALL_YML'; + +export class CondaProgressAndTelemetry { + private condaCreatedReported = false; + + private condaFailedReported = false; + + private condaInstallingPackagesReported = false; + + private condaInstallingPackagesFailedReported = false; + + private condaInstalledPackagesReported = false; + + private lastError: string | undefined = undefined; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public process(output: string): void { + if (!this.condaCreatedReported && output.includes(CONDA_ENV_CREATED_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'created', + }); + } else if (!this.condaCreatedReported && output.includes(CONDA_ENV_EXISTING_MARKER)) { + this.condaCreatedReported = true; + this.progress.report({ + message: CreateEnv.Conda.created, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'conda', + reason: 'existing', + }); + } else if (!this.condaFailedReported && output.includes(CREATE_CONDA_FAILED_MARKER)) { + this.condaFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'conda', + reason: 'other', + }); + this.lastError = CREATE_CONDA_FAILED_MARKER; + } else if (!this.condaInstallingPackagesReported && output.includes(CONDA_INSTALLING_YML)) { + this.condaInstallingPackagesReported = true; + this.progress.report({ + message: CreateEnv.Conda.installingPackages, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } else if (!this.condaInstallingPackagesFailedReported && output.includes(CREATE_FAILED_INSTALL_YML)) { + this.condaInstallingPackagesFailedReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + this.lastError = CREATE_FAILED_INSTALL_YML; + } else if (!this.condaInstalledPackagesReported && output.includes(CREATE_CONDA_INSTALLED_YML)) { + this.condaInstalledPackagesReported = true; + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'conda', + using: 'environment.yml', + }); + } + } + + public getLastError(): string | undefined { + return this.lastError; + } +} diff --git a/src/client/pythonEnvironments/creation/provider/condaUtils.ts b/src/client/pythonEnvironments/creation/provider/condaUtils.ts new file mode 100644 index 000000000000..617a2996801e --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/condaUtils.ts @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { CancellationToken, ProgressLocation, QuickPickItem, Uri, WorkspaceFolder } from 'vscode'; +import { Commands, Octicons } from '../../../common/constants'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { executeCommand } from '../../../common/vscodeApis/commandApis'; +import { + MultiStepAction, + showErrorMessage, + showQuickPickWithBack, + withProgress, +} from '../../../common/vscodeApis/windowApis'; +import { traceLog } from '../../../logging'; +import { Conda } from '../../common/environmentManagers/conda'; +import { getPrefixCondaEnvPath, hasPrefixCondaEnv } from '../common/commonUtils'; +import { OSType, getEnvironmentVariable, getOSType } from '../../../common/utils/platform'; +import { deleteCondaEnvironment } from './condaDeleteUtils'; + +const RECOMMENDED_CONDA_PYTHON = '3.11'; + +export async function getCondaBaseEnv(): Promise { + const conda = await Conda.getConda(); + + if (!conda) { + const response = await showErrorMessage(CreateEnv.Conda.condaMissing, Common.learnMore); + if (response === Common.learnMore) { + await executeCommand('vscode.open', Uri.parse('https://docs.anaconda.com/anaconda/install/')); + } + return undefined; + } + + const envs = (await conda.getEnvList()).filter((e) => e.name === 'base'); + if (envs.length === 1) { + return envs[0].prefix; + } + if (envs.length > 1) { + traceLog( + 'Multiple conda base envs detected: ', + envs.map((e) => e.prefix), + ); + return undefined; + } + + return undefined; +} + +export async function pickPythonVersion(token?: CancellationToken): Promise { + const items: QuickPickItem[] = ['3.11', '3.12', '3.10', '3.9', '3.8'].map((v) => ({ + label: v === RECOMMENDED_CONDA_PYTHON ? `${Octicons.Star} Python` : 'Python', + description: v, + })); + const selection = await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.selectPythonQuickPickPlaceholder, + matchOnDescription: true, + ignoreFocusOut: true, + }, + token, + ); + + if (selection) { + return (selection as QuickPickItem).description; + } + + return undefined; +} + +export function getPathEnvVariableForConda(condaBasePythonPath: string): string { + const pathEnv = getEnvironmentVariable('PATH') || getEnvironmentVariable('Path') || ''; + if (getOSType() === OSType.Windows) { + // On windows `conda.bat` is used, which adds the following bin directories to PATH + // then launches `conda.exe` which is a stub to `python.exe -m conda`. Here, we are + // instead using the `python.exe` that ships with conda to run a python script that + // handles conda env creation and package installation. + // See conda issue: https://github.com/conda/conda/issues/11399 + const root = path.dirname(condaBasePythonPath); + const libPath1 = path.join(root, 'Library', 'bin'); + const libPath2 = path.join(root, 'Library', 'mingw-w64', 'bin'); + const libPath3 = path.join(root, 'Library', 'usr', 'bin'); + const libPath4 = path.join(root, 'bin'); + const libPath5 = path.join(root, 'Scripts'); + const libPath = [libPath1, libPath2, libPath3, libPath4, libPath5].join(path.delimiter); + return `${libPath}${path.delimiter}${pathEnv}`; + } + return pathEnv; +} + +export async function deleteEnvironment(workspaceFolder: WorkspaceFolder, interpreter: string): Promise { + const condaEnvPath = getPrefixCondaEnvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Conda.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${condaEnvPath}`, + cancellable: false, + }, + async () => deleteCondaEnvironment(workspaceFolder, interpreter, getPathEnvVariableForConda(interpreter)), + ); +} + +export enum ExistingCondaAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingCondaAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasPrefixCondaEnv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { label: CreateEnv.Conda.recreate, description: CreateEnv.Conda.recreateDescription }, + { + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Conda.existingCondaQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Conda.recreate) { + return ExistingCondaAction.Recreate; + } + + if (selection?.label === CreateEnv.Conda.useExisting) { + return ExistingCondaAction.UseExisting; + } + } else { + return ExistingCondaAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts new file mode 100644 index 000000000000..5c29a8d7128d --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/hideEnvCreation.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Disposable } from 'vscode'; + +const envCreationTracker: Disposable[] = []; + +export function hideEnvCreation(): Disposable { + const disposable = new Disposable(() => { + const index = envCreationTracker.indexOf(disposable); + if (index > -1) { + envCreationTracker.splice(index, 1); + } + }); + envCreationTracker.push(disposable); + return disposable; +} + +export function shouldDisplayEnvCreationProgress(): boolean { + return envCreationTracker.length === 0; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts new file mode 100644 index 000000000000..c5c82b85357f --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvCreationProvider.ts @@ -0,0 +1,389 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as os from 'os'; +import { CancellationToken, CancellationTokenSource, ProgressLocation, WorkspaceFolder } from 'vscode'; +import { Commands, PVSC_EXTENSION_ID } from '../../../common/constants'; +import { createVenvScript } from '../../../common/process/internal/scripts'; +import { execObservable } from '../../../common/process/rawProcessApis'; +import { createDeferred } from '../../../common/utils/async'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { CreateEnvironmentOptionsInternal, CreateEnvironmentProgress } from '../types'; +import { pickWorkspaceFolder } from '../common/workspaceSelection'; +import { IInterpreterQuickPick } from '../../../interpreter/configuration/types'; +import { EnvironmentType, PythonEnvironment } from '../../info'; +import { MultiStepAction, MultiStepNode, withProgress } from '../../../common/vscodeApis/windowApis'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { VenvProgressAndTelemetry, VENV_CREATED_MARKER, VENV_EXISTING_MARKER } from './venvProgressAndTelemetry'; +import { getVenvExecutable, showErrorMessageWithLogs } from '../common/commonUtils'; +import { + ExistingVenvAction, + IPackageInstallSelection, + deleteEnvironment, + pickExistingVenvAction, + pickPackagesToInstall, +} from './venvUtils'; +import { InputFlowAction } from '../../../common/utils/multiStepInput'; +import { + CreateEnvironmentProvider, + CreateEnvironmentOptions, + CreateEnvironmentResult, +} from '../proposed.createEnvApis'; +import { shouldDisplayEnvCreationProgress } from './hideEnvCreation'; +import { noop } from '../../../common/utils/misc'; + +interface IVenvCommandArgs { + argv: string[]; + stdin: string | undefined; +} + +function generateCommandArgs(installInfo?: IPackageInstallSelection[], addGitIgnore?: boolean): IVenvCommandArgs { + const command: string[] = [createVenvScript()]; + let stdin: string | undefined; + + if (addGitIgnore) { + command.push('--git-ignore'); + } + + if (installInfo) { + if (installInfo.some((i) => i.installType === 'toml')) { + const source = installInfo.find((i) => i.installType === 'toml')?.source; + command.push('--toml', source?.fileToCommandArgumentForPythonExt() || 'pyproject.toml'); + } + const extras = installInfo.filter((i) => i.installType === 'toml').map((i) => i.installItem); + extras.forEach((r) => { + if (r) { + command.push('--extras', r); + } + }); + + const requirements = installInfo.filter((i) => i.installType === 'requirements').map((i) => i.installItem); + + if (requirements.length < 10) { + requirements.forEach((r) => { + if (r) { + command.push('--requirements', r); + } + }); + } else { + command.push('--stdin'); + // Too many requirements can cause the command line to be too long error. + stdin = JSON.stringify({ requirements }); + } + } + + return { argv: command, stdin }; +} + +function getVenvFromOutput(output: string): string | undefined { + try { + const envPath = output + .split(/\r?\n/g) + .map((s) => s.trim()) + .filter((s) => s.startsWith(VENV_CREATED_MARKER) || s.startsWith(VENV_EXISTING_MARKER))[0]; + if (envPath.includes(VENV_CREATED_MARKER)) { + return envPath.substring(VENV_CREATED_MARKER.length); + } + return envPath.substring(VENV_EXISTING_MARKER.length); + } catch (ex) { + traceError('Parsing out environment path failed.'); + return undefined; + } +} + +async function createVenv( + workspace: WorkspaceFolder, + command: string, + args: IVenvCommandArgs, + progress: CreateEnvironmentProgress, + token?: CancellationToken, +): Promise { + progress.report({ + message: CreateEnv.Venv.creating, + }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'venv', + pythonVersion: undefined, + }); + + const deferred = createDeferred(); + traceLog('Running Env creation script: ', [command, ...args.argv]); + if (args.stdin) { + traceLog('Requirements passed in via stdin: ', args.stdin); + } + const { proc, out, dispose } = execObservable(command, args.argv, { + mergeStdOutErr: true, + token, + cwd: workspace.uri.fsPath, + stdinStr: args.stdin, + }); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progress); + let venvPath: string | undefined; + out.subscribe( + (value) => { + const output = value.out.split(/\r?\n/g).join(os.EOL); + traceLog(output.trimEnd()); + if (output.includes(VENV_CREATED_MARKER) || output.includes(VENV_EXISTING_MARKER)) { + venvPath = getVenvFromOutput(output); + } + progressAndTelemetry.process(output); + }, + (error) => { + traceError('Error while running venv creation script: ', error); + deferred.reject(error); + }, + () => { + dispose(); + if (proc?.exitCode !== 0) { + traceError('Error while running venv creation script: ', progressAndTelemetry.getLastError()); + deferred.reject( + progressAndTelemetry.getLastError() || + `Failed to create virtual environment with exitCode: ${proc?.exitCode}`, + ); + } else { + deferred.resolve(venvPath); + } + }, + ); + return deferred.promise; +} + +export const VenvCreationProviderId = `${PVSC_EXTENSION_ID}:venv`; +export class VenvCreationProvider implements CreateEnvironmentProvider { + constructor(private readonly interpreterQuickPick: IInterpreterQuickPick) {} + + public async createEnvironment( + options?: CreateEnvironmentOptions & CreateEnvironmentOptionsInternal, + ): Promise { + let workspace = options?.workspaceFolder; + const bypassQuickPicks = options?.workspaceFolder && options.interpreter && options.providerId ? true : false; + const workspaceStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + try { + workspace = + workspace && bypassQuickPicks + ? workspace + : ((await pickWorkspaceFolder( + { preSelectedWorkspace: options?.workspaceFolder }, + context, + )) as WorkspaceFolder | undefined); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + + if (workspace === undefined) { + traceError('Workspace was not selected or found for creating virtual environment.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected workspace ${workspace.uri.fsPath} for creating virtual environment.`); + return MultiStepAction.Continue; + }, + undefined, + ); + + let existingVenvAction: ExistingVenvAction | undefined; + if (bypassQuickPicks) { + existingVenvAction = ExistingVenvAction.Create; + } + const existingEnvStep = new MultiStepNode( + workspaceStep, + async (context?: MultiStepAction) => { + if (workspace && context === MultiStepAction.Continue) { + try { + existingVenvAction = await pickExistingVenvAction(workspace); + return MultiStepAction.Continue; + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + return MultiStepAction.Continue; + }, + undefined, + ); + workspaceStep.next = existingEnvStep; + + let interpreter = options?.interpreter; + const interpreterStep = new MultiStepNode( + existingEnvStep, + async (context?: MultiStepAction) => { + if (workspace) { + if ( + existingVenvAction === ExistingVenvAction.Recreate || + existingVenvAction === ExistingVenvAction.Create + ) { + try { + interpreter = + interpreter && bypassQuickPicks + ? interpreter + : await this.interpreterQuickPick.getInterpreterViaQuickPick( + workspace.uri, + (i: PythonEnvironment) => + [ + EnvironmentType.System, + EnvironmentType.MicrosoftStore, + EnvironmentType.Global, + EnvironmentType.Pyenv, + ].includes(i.envType) && i.type === undefined, // only global intepreters + { + skipRecommended: true, + showBackButton: true, + placeholder: CreateEnv.Venv.selectPythonPlaceHolder, + title: null, + }, + ); + } catch (ex) { + if (ex === InputFlowAction.back) { + return MultiStepAction.Back; + } + interpreter = undefined; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + interpreter = getVenvExecutable(workspace); + } + } + + if (!interpreter) { + traceError('Virtual env creation requires an interpreter.'); + return MultiStepAction.Cancel; + } + traceInfo(`Selected interpreter ${interpreter} for creating virtual environment.`); + return MultiStepAction.Continue; + }, + undefined, + ); + existingEnvStep.next = interpreterStep; + + let addGitIgnore = true; + let installPackages = true; + if (options) { + addGitIgnore = options?.ignoreSourceControl !== undefined ? options.ignoreSourceControl : true; + installPackages = options?.installPackages !== undefined ? options.installPackages : true; + } + let installInfo: IPackageInstallSelection[] | undefined; + const packagesStep = new MultiStepNode( + interpreterStep, + async (context?: MultiStepAction) => { + if (workspace && installPackages) { + if (existingVenvAction !== ExistingVenvAction.UseExisting) { + try { + installInfo = await pickPackagesToInstall(workspace); + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + if (!installInfo) { + traceVerbose('Virtual env creation exited during dependencies selection.'); + return MultiStepAction.Cancel; + } + } else if (context === MultiStepAction.Back) { + return MultiStepAction.Back; + } + } + + return MultiStepAction.Continue; + }, + undefined, + ); + interpreterStep.next = packagesStep; + + const action = await MultiStepNode.run(workspaceStep); + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + throw action; + } + + if (workspace) { + if (existingVenvAction === ExistingVenvAction.Recreate) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'triggered', + }); + if (await deleteEnvironment(workspace, interpreter)) { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'deleted', + }); + } else { + sendTelemetryEvent(EventName.ENVIRONMENT_DELETE, undefined, { + environmentType: 'venv', + status: 'failed', + }); + throw MultiStepAction.Cancel; + } + } else if (existingVenvAction === ExistingVenvAction.UseExisting) { + sendTelemetryEvent(EventName.ENVIRONMENT_REUSE, undefined, { + environmentType: 'venv', + }); + return { path: getVenvExecutable(workspace), workspaceFolder: workspace }; + } + } + + const args = generateCommandArgs(installInfo, addGitIgnore); + const createEnvInternal = async (progress: CreateEnvironmentProgress, token: CancellationToken) => { + progress.report({ + message: CreateEnv.statusStarting, + }); + + let envPath: string | undefined; + try { + if (interpreter && workspace) { + envPath = await createVenv(workspace, interpreter, args, progress, token); + if (envPath) { + return { path: envPath, workspaceFolder: workspace }; + } + throw new Error('Failed to create virtual environment. See Output > Python for more info.'); + } + throw new Error('Failed to create virtual environment. Either interpreter or workspace is undefined.'); + } catch (ex) { + traceError(ex); + showErrorMessageWithLogs(CreateEnv.Venv.errorCreatingEnvironment); + return { error: ex as Error }; + } + }; + + if (!shouldDisplayEnvCreationProgress()) { + const token = new CancellationTokenSource(); + try { + return await createEnvInternal({ report: noop }, token.token); + } finally { + token.dispose(); + } + } + + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.statusTitle} ([${Common.showLogs}](command:${Commands.ViewOutput}))`, + cancellable: true, + }, + async ( + progress: CreateEnvironmentProgress, + token: CancellationToken, + ): Promise => createEnvInternal(progress, token), + ); + } + + name = 'Venv'; + + description: string = CreateEnv.Venv.providerDescription; + + id = VenvCreationProviderId; + + tools = ['Venv']; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts new file mode 100644 index 000000000000..9bd410c09f51 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvDeleteUtils.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { WorkspaceFolder } from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { traceError, traceInfo } from '../../../logging'; +import { getVenvPath, showErrorMessageWithLogs } from '../common/commonUtils'; +import { CreateEnv } from '../../../common/utils/localize'; +import { sleep } from '../../../common/utils/async'; +import { switchSelectedPython } from './venvSwitchPython'; + +async function tryDeleteFile(file: string): Promise { + try { + if (!(await fs.pathExists(file))) { + return true; + } + await fs.unlink(file); + return true; + } catch (err) { + traceError(`Failed to delete file [${file}]:`, err); + return false; + } +} + +async function tryDeleteDir(dir: string): Promise { + try { + if (!(await fs.pathExists(dir))) { + return true; + } + await fs.rmdir(dir, { + recursive: true, + maxRetries: 10, + retryDelay: 200, + }); + return true; + } catch (err) { + traceError(`Failed to delete directory [${dir}]:`, err); + return false; + } +} + +export async function deleteEnvironmentNonWindows(workspaceFolder: WorkspaceFolder): Promise { + const venvPath = getVenvPath(workspaceFolder); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted venv dir: ${venvPath}`); + return true; + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} + +export async function deleteEnvironmentWindows( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + const venvPythonPath = path.join(venvPath, 'Scripts', 'python.exe'); + + if (await tryDeleteFile(venvPythonPath)) { + traceInfo(`Deleted python executable: ${venvPythonPath}`); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + + traceError(`Failed to delete ".venv" dir: ${venvPath}`); + traceError( + 'This happens if the virtual environment is still in use, or some binary in the venv is still running.', + ); + traceError(`Please delete the ".venv" manually: [${venvPath}]`); + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; + } + traceError(`Failed to delete python executable: ${venvPythonPath}`); + traceError('This happens if the virtual environment is still in use.'); + + if (interpreter) { + traceError('We will attempt to switch python temporarily to delete the ".venv"'); + + await switchSelectedPython(interpreter, workspaceFolder.uri, 'temporarily to delete the ".venv"'); + + traceInfo(`Attempting to delete ".venv" again: ${venvPath}`); + const ms = 500; + for (let i = 0; i < 5; i = i + 1) { + traceInfo(`Waiting for ${ms}ms to let processes exit, before a delete attempt.`); + await sleep(ms); + if (await tryDeleteDir(venvPath)) { + traceInfo(`Deleted ".venv" dir: ${venvPath}`); + return true; + } + traceError(`Failed to delete ".venv" dir [${venvPath}] (attempt ${i + 1}/5).`); + } + } else { + traceError(`Please delete the ".venv" dir manually: [${venvPath}]`); + } + showErrorMessageWithLogs(CreateEnv.Venv.errorDeletingEnvironment); + return false; +} diff --git a/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts new file mode 100644 index 000000000000..e092c40c3fe0 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvProgressAndTelemetry.ts @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CreateEnv } from '../../../common/utils/localize'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { CreateEnvironmentProgress } from '../types'; + +export const VENV_CREATED_MARKER = 'CREATED_VENV:'; +export const VENV_EXISTING_MARKER = 'EXISTING_VENV:'; +const INSTALLING_REQUIREMENTS = 'VENV_INSTALLING_REQUIREMENTS:'; +const INSTALLING_PYPROJECT = 'VENV_INSTALLING_PYPROJECT:'; +const PIP_NOT_INSTALLED_MARKER = 'CREATE_VENV.PIP_NOT_FOUND'; +const VENV_NOT_INSTALLED_MARKER = 'CREATE_VENV.VENV_NOT_FOUND'; +const INSTALL_REQUIREMENTS_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_REQUIREMENTS'; +const INSTALL_PYPROJECT_FAILED_MARKER = 'CREATE_VENV.PIP_FAILED_INSTALL_PYPROJECT'; +const CREATE_VENV_FAILED_MARKER = 'CREATE_VENV.VENV_FAILED_CREATION'; +const VENV_ALREADY_EXISTS_MARKER = 'CREATE_VENV.VENV_ALREADY_EXISTS'; +const INSTALLED_REQUIREMENTS_MARKER = 'CREATE_VENV.PIP_INSTALLED_REQUIREMENTS'; +const INSTALLED_PYPROJECT_MARKER = 'CREATE_VENV.PIP_INSTALLED_PYPROJECT'; +const UPGRADE_PIP_FAILED_MARKER = 'CREATE_VENV.UPGRADE_PIP_FAILED'; +const UPGRADING_PIP_MARKER = 'CREATE_VENV.UPGRADING_PIP'; +const UPGRADED_PIP_MARKER = 'CREATE_VENV.UPGRADED_PIP'; +const CREATING_MICROVENV_MARKER = 'CREATE_MICROVENV.CREATING_MICROVENV'; +const CREATE_MICROVENV_FAILED_MARKER = 'CREATE_VENV.MICROVENV_FAILED_CREATION'; +const CREATE_MICROVENV_FAILED_MARKER2 = 'CREATE_MICROVENV.MICROVENV_FAILED_CREATION'; +const MICROVENV_CREATED_MARKER = 'CREATE_MICROVENV.CREATED_MICROVENV'; +const INSTALLING_PIP_MARKER = 'CREATE_VENV.INSTALLING_PIP'; +const INSTALL_PIP_FAILED_MARKER = 'CREATE_VENV.INSTALL_PIP_FAILED'; +const DOWNLOADING_PIP_MARKER = 'CREATE_VENV.DOWNLOADING_PIP'; +const DOWNLOAD_PIP_FAILED_MARKER = 'CREATE_VENV.DOWNLOAD_PIP_FAILED'; +const DISTUTILS_NOT_INSTALLED_MARKER = 'CREATE_VENV.DISTUTILS_NOT_INSTALLED'; + +export class VenvProgressAndTelemetry { + private readonly processed = new Set(); + + private readonly reportActions = new Map string | undefined>([ + [ + VENV_CREATED_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.created }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'created', + }); + return undefined; + }, + ], + [ + VENV_EXISTING_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.existing }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLING_REQUIREMENTS, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLING_PYPROJECT, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPackages }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + PIP_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noPip', + }); + return PIP_NOT_INSTALLED_MARKER; + }, + ], + [ + DISTUTILS_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noDistUtils', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], + [ + VENV_NOT_INSTALLED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'noVenv', + }); + return VENV_NOT_INSTALLED_MARKER; + }, + ], + [ + INSTALL_REQUIREMENTS_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return INSTALL_REQUIREMENTS_FAILED_MARKER; + }, + ], + [ + INSTALL_PYPROJECT_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return INSTALL_PYPROJECT_FAILED_MARKER; + }, + ], + [ + CREATE_VENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'venv', + reason: 'other', + }); + return CREATE_VENV_FAILED_MARKER; + }, + ], + [ + VENV_ALREADY_EXISTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'venv', + reason: 'existing', + }); + return undefined; + }, + ], + [ + INSTALLED_REQUIREMENTS_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'requirements.txt', + }); + return undefined; + }, + ], + [ + INSTALLED_PYPROJECT_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pyproject.toml', + }); + return undefined; + }, + ], + [ + UPGRADED_PIP_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLED_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + [ + UPGRADE_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return UPGRADE_PIP_FAILED_MARKER; + }, + ], + [ + DOWNLOADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.downloadingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return undefined; + }, + ], + [ + DOWNLOAD_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipDownload', + }); + return DOWNLOAD_PIP_FAILED_MARKER; + }, + ], + [ + INSTALLING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.installingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return undefined; + }, + ], + [ + INSTALL_PIP_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES_FAILED, undefined, { + environmentType: 'venv', + using: 'pipInstall', + }); + return INSTALL_PIP_FAILED_MARKER; + }, + ], + [ + CREATING_MICROVENV_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.creatingMicrovenv }); + sendTelemetryEvent(EventName.ENVIRONMENT_CREATING, undefined, { + environmentType: 'microvenv', + pythonVersion: undefined, + }); + return undefined; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER; + }, + ], + [ + CREATE_MICROVENV_FAILED_MARKER2, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_FAILED, undefined, { + environmentType: 'microvenv', + reason: 'other', + }); + return CREATE_MICROVENV_FAILED_MARKER2; + }, + ], + [ + MICROVENV_CREATED_MARKER, + (_progress: CreateEnvironmentProgress) => { + sendTelemetryEvent(EventName.ENVIRONMENT_CREATED, undefined, { + environmentType: 'microvenv', + reason: 'created', + }); + return undefined; + }, + ], + [ + UPGRADING_PIP_MARKER, + (progress: CreateEnvironmentProgress) => { + progress.report({ message: CreateEnv.Venv.upgradingPip }); + sendTelemetryEvent(EventName.ENVIRONMENT_INSTALLING_PACKAGES, undefined, { + environmentType: 'venv', + using: 'pipUpgrade', + }); + return undefined; + }, + ], + ]); + + private lastError: string | undefined = undefined; + + constructor(private readonly progress: CreateEnvironmentProgress) {} + + public getLastError(): string | undefined { + return this.lastError; + } + + public process(output: string): void { + const keys: string[] = Array.from(this.reportActions.keys()); + + for (const key of keys) { + if (output.includes(key) && !this.processed.has(key)) { + const action = this.reportActions.get(key); + if (action) { + const err = action(this.progress); + if (err) { + this.lastError = err; + } + } + this.processed.add(key); + } + } + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts new file mode 100644 index 000000000000..e2567dfd114b --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvSwitchPython.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { createDeferred } from '../../../common/utils/async'; +import { getExtension } from '../../../common/vscodeApis/extensionsApi'; +import { PVSC_EXTENSION_ID, PythonExtension } from '../../../api/types'; +import { traceInfo } from '../../../logging'; + +export async function switchSelectedPython(interpreter: string, uri: Uri, purpose: string): Promise { + let dispose: Disposable | undefined; + try { + const deferred = createDeferred(); + const api: PythonExtension = getExtension(PVSC_EXTENSION_ID)?.exports as PythonExtension; + dispose = api.environments.onDidChangeActiveEnvironmentPath(async (e) => { + if (path.normalize(e.path) === path.normalize(interpreter)) { + traceInfo(`Switched to interpreter ${purpose}: ${interpreter}`); + deferred.resolve(); + } + }); + api.environments.updateActiveEnvironmentPath(interpreter, uri); + traceInfo(`Switching interpreter ${purpose}: ${interpreter}`); + await deferred.promise; + } finally { + dispose?.dispose(); + } +} diff --git a/src/client/pythonEnvironments/creation/provider/venvUtils.ts b/src/client/pythonEnvironments/creation/provider/venvUtils.ts new file mode 100644 index 000000000000..1bfb2c96f224 --- /dev/null +++ b/src/client/pythonEnvironments/creation/provider/venvUtils.ts @@ -0,0 +1,336 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as tomljs from '@iarna/toml'; +import { flatten, isArray } from 'lodash'; +import * as path from 'path'; +import { + CancellationToken, + ProgressLocation, + QuickPickItem, + QuickPickItemButtonEvent, + RelativePattern, + ThemeIcon, + Uri, + WorkspaceFolder, +} from 'vscode'; +import * as fs from '../../../common/platform/fs-paths'; +import { Common, CreateEnv } from '../../../common/utils/localize'; +import { + MultiStepAction, + MultiStepNode, + showQuickPickWithBack, + showTextDocument, + withProgress, +} from '../../../common/vscodeApis/windowApis'; +import { findFiles } from '../../../common/vscodeApis/workspaceApis'; +import { traceError, traceVerbose } from '../../../logging'; +import { Commands } from '../../../common/constants'; +import { isWindows } from '../../../common/utils/platform'; +import { getVenvPath, hasVenv } from '../common/commonUtils'; +import { deleteEnvironmentNonWindows, deleteEnvironmentWindows } from './venvDeleteUtils'; + +export const OPEN_REQUIREMENTS_BUTTON = { + iconPath: new ThemeIcon('go-to-file'), + tooltip: CreateEnv.Venv.openRequirementsFile, +}; +const exclude = '**/{.venv*,.git,.nox,.tox,.conda,site-packages,__pypackages__}/**'; +export async function getPipRequirementsFiles( + workspaceFolder: WorkspaceFolder, + token?: CancellationToken, +): Promise { + const files = flatten( + await Promise.all([ + findFiles(new RelativePattern(workspaceFolder, '**/*requirement*.txt'), exclude, undefined, token), + findFiles(new RelativePattern(workspaceFolder, '**/requirements/*.txt'), exclude, undefined, token), + ]), + ).map((u) => u.fsPath); + return files; +} + +function tomlParse(content: string): tomljs.JsonMap { + try { + return tomljs.parse(content); + } catch (err) { + traceError('Failed to parse `pyproject.toml`:', err); + } + return {}; +} + +function tomlHasBuildSystem(toml: tomljs.JsonMap): boolean { + return toml['build-system'] !== undefined; +} + +function tomlHasProject(toml: tomljs.JsonMap): boolean { + return toml.project !== undefined; +} + +function getTomlOptionalDeps(toml: tomljs.JsonMap): string[] { + const extras: string[] = []; + if (toml.project && (toml.project as tomljs.JsonMap)['optional-dependencies']) { + const deps = (toml.project as tomljs.JsonMap)['optional-dependencies']; + for (const key of Object.keys(deps)) { + extras.push(key); + } + } + return extras; +} + +async function pickTomlExtras(extras: string[], token?: CancellationToken): Promise { + const items: QuickPickItem[] = extras.map((e) => ({ label: e })); + + const selection = await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + canPickMany: true, + ignoreFocusOut: true, + }, + token, + ); + + if (selection && isArray(selection)) { + return selection.map((s) => s.label); + } + + return undefined; +} + +async function pickRequirementsFiles( + files: string[], + root: string, + token?: CancellationToken, +): Promise { + const items: QuickPickItem[] = files + .map((p) => path.relative(root, p)) + .sort((a, b) => { + const al: number = a.split(/[\\\/]/).length; + const bl: number = b.split(/[\\\/]/).length; + if (al === bl) { + if (a.length === b.length) { + return a.localeCompare(b); + } + return a.length - b.length; + } + return al - bl; + }) + .map((e) => ({ + label: e, + buttons: [OPEN_REQUIREMENTS_BUTTON], + })); + + const selection = await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + token, + async (e: QuickPickItemButtonEvent) => { + if (e.item.label) { + await showTextDocument(Uri.file(path.join(root, e.item.label))); + } + }, + ); + + if (selection && isArray(selection)) { + return selection.map((s) => s.label); + } + + return undefined; +} + +export function isPipInstallableToml(tomlContent: string): boolean { + const toml = tomlParse(tomlContent); + return tomlHasBuildSystem(toml) && tomlHasProject(toml); +} + +export interface IPackageInstallSelection { + installType: 'toml' | 'requirements' | 'none'; + installItem?: string; + source?: string; +} + +export async function pickPackagesToInstall( + workspaceFolder: WorkspaceFolder, + token?: CancellationToken, +): Promise { + const tomlPath = path.join(workspaceFolder.uri.fsPath, 'pyproject.toml'); + const packages: IPackageInstallSelection[] = []; + + const tomlStep = new MultiStepNode( + undefined, + async (context?: MultiStepAction) => { + traceVerbose(`Looking for toml pyproject.toml with optional dependencies at: ${tomlPath}`); + + let extras: string[] = []; + let hasBuildSystem = false; + let hasProject = false; + + if (await fs.pathExists(tomlPath)) { + const toml = tomlParse(await fs.readFile(tomlPath, 'utf-8')); + extras = getTomlOptionalDeps(toml); + hasBuildSystem = tomlHasBuildSystem(toml); + hasProject = tomlHasProject(toml); + + if (!hasProject) { + traceVerbose('Create env: Found toml without project. So we will not use editable install.'); + } + if (!hasBuildSystem) { + traceVerbose('Create env: Found toml without build system. So we will not use editable install.'); + } + if (extras.length === 0) { + traceVerbose('Create env: Found toml without optional dependencies.'); + } + } else if (context === MultiStepAction.Back) { + // This step is not really used so just go back + return MultiStepAction.Back; + } + + if (hasBuildSystem && hasProject) { + if (extras.length > 0) { + traceVerbose('Create Env: Found toml with optional dependencies.'); + + try { + const installList = await pickTomlExtras(extras, token); + if (installList) { + if (installList.length > 0) { + installList.forEach((i) => { + packages.push({ installType: 'toml', installItem: i, source: tomlPath }); + }); + } + packages.push({ installType: 'toml', source: tomlPath }); + } else { + return MultiStepAction.Cancel; + } + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + // This step is not really used so just go back + return MultiStepAction.Back; + } else { + // There are no extras to install and the context is to go to next step + packages.push({ installType: 'toml', source: tomlPath }); + } + } else if (context === MultiStepAction.Back) { + // This step is not really used because there is no build system in toml, so just go back + return MultiStepAction.Back; + } + + return MultiStepAction.Continue; + }, + undefined, + ); + + const requirementsStep = new MultiStepNode( + tomlStep, + async (context?: MultiStepAction) => { + traceVerbose('Looking for pip requirements.'); + const requirementFiles = await getPipRequirementsFiles(workspaceFolder, token); + if (requirementFiles && requirementFiles.length > 0) { + traceVerbose('Found pip requirements.'); + try { + const result = await pickRequirementsFiles(requirementFiles, workspaceFolder.uri.fsPath, token); + const installList = result?.map((p) => path.join(workspaceFolder.uri.fsPath, p)); + if (installList) { + installList.forEach((i) => { + packages.push({ installType: 'requirements', installItem: i }); + }); + } else { + return MultiStepAction.Cancel; + } + } catch (ex) { + if (ex === MultiStepAction.Back || ex === MultiStepAction.Cancel) { + return ex; + } + throw ex; + } + } else if (context === MultiStepAction.Back) { + // This step is not really used, because there were no requirement files, so just go back + return MultiStepAction.Back; + } + + return MultiStepAction.Continue; + }, + undefined, + ); + tomlStep.next = requirementsStep; + + const action = await MultiStepNode.run(tomlStep); + if (action === MultiStepAction.Back || action === MultiStepAction.Cancel) { + throw action; + } + + return packages; +} + +export async function deleteEnvironment( + workspaceFolder: WorkspaceFolder, + interpreter: string | undefined, +): Promise { + const venvPath = getVenvPath(workspaceFolder); + return withProgress( + { + location: ProgressLocation.Notification, + title: `${CreateEnv.Venv.deletingEnvironmentProgress} ([${Common.showLogs}](command:${Commands.ViewOutput})): ${venvPath}`, + cancellable: false, + }, + async () => { + if (isWindows()) { + return deleteEnvironmentWindows(workspaceFolder, interpreter); + } + return deleteEnvironmentNonWindows(workspaceFolder); + }, + ); +} + +export enum ExistingVenvAction { + Recreate, + UseExisting, + Create, +} + +export async function pickExistingVenvAction( + workspaceFolder: WorkspaceFolder | undefined, +): Promise { + if (workspaceFolder) { + if (await hasVenv(workspaceFolder)) { + const items: QuickPickItem[] = [ + { + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }, + { + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }, + ]; + + const selection = (await showQuickPickWithBack( + items, + { + placeHolder: CreateEnv.Venv.existingVenvQuickPickPlaceholder, + ignoreFocusOut: true, + }, + undefined, + )) as QuickPickItem | undefined; + + if (selection?.label === CreateEnv.Venv.recreate) { + return ExistingVenvAction.Recreate; + } + + if (selection?.label === CreateEnv.Venv.useExisting) { + return ExistingVenvAction.UseExisting; + } + } else { + return ExistingVenvAction.Create; + } + } + + throw MultiStepAction.Cancel; +} diff --git a/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts new file mode 100644 index 000000000000..5925b7641f45 --- /dev/null +++ b/src/client/pythonEnvironments/creation/pyProjectTomlContext.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TextDocument } from 'vscode'; +import { IDisposableRegistry } from '../../common/types'; +import { executeCommand } from '../../common/vscodeApis/commandApis'; +import { + onDidOpenTextDocument, + onDidSaveTextDocument, + getOpenTextDocuments, +} from '../../common/vscodeApis/workspaceApis'; +import { isPipInstallableToml } from './provider/venvUtils'; + +async function setPyProjectTomlContextKey(doc: TextDocument): Promise { + if (isPipInstallableToml(doc.getText())) { + await executeCommand('setContext', 'pipInstallableToml', true); + } else { + await executeCommand('setContext', 'pipInstallableToml', false); + } +} + +export function registerPyProjectTomlFeatures(disposables: IDisposableRegistry): void { + disposables.push( + onDidOpenTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + onDidSaveTextDocument(async (doc: TextDocument) => { + if (doc.fileName.endsWith('pyproject.toml')) { + await setPyProjectTomlContextKey(doc); + } + }), + ); + + const docs = getOpenTextDocuments().filter( + (doc) => doc.fileName.endsWith('pyproject.toml') && isPipInstallableToml(doc.getText()), + ); + if (docs.length > 0) { + executeCommand('setContext', 'pipInstallableToml', true); + } else { + executeCommand('setContext', 'pipInstallableToml', false); + } +} diff --git a/src/client/pythonEnvironments/creation/registrations.ts b/src/client/pythonEnvironments/creation/registrations.ts new file mode 100644 index 000000000000..25141cbec5ac --- /dev/null +++ b/src/client/pythonEnvironments/creation/registrations.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IDisposableRegistry, IPathUtils } from '../../common/types'; +import { IInterpreterQuickPick, IPythonPathUpdaterServiceManager } from '../../interpreter/configuration/types'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { registerCreateEnvironmentFeatures } from './createEnvApi'; +import { registerCreateEnvironmentButtonFeatures } from './createEnvButtonContext'; +import { registerTriggerForPipInTerminal } from './globalPipInTerminalTrigger'; +import { registerInstalledPackagesDiagnosticsProvider } from './installedPackagesDiagnostic'; +import { registerPyProjectTomlFeatures } from './pyProjectTomlContext'; + +export function registerAllCreateEnvironmentFeatures( + disposables: IDisposableRegistry, + interpreterQuickPick: IInterpreterQuickPick, + pythonPathUpdater: IPythonPathUpdaterServiceManager, + interpreterService: IInterpreterService, + pathUtils: IPathUtils, +): void { + registerCreateEnvironmentFeatures(disposables, interpreterQuickPick, pythonPathUpdater, pathUtils); + registerCreateEnvironmentButtonFeatures(disposables); + registerPyProjectTomlFeatures(disposables); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService); + registerTriggerForPipInTerminal(disposables); +} diff --git a/src/client/pythonEnvironments/creation/types.ts b/src/client/pythonEnvironments/creation/types.ts new file mode 100644 index 000000000000..0e400c2d90f3 --- /dev/null +++ b/src/client/pythonEnvironments/creation/types.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import { Progress, WorkspaceFolder } from 'vscode'; + +export interface CreateEnvironmentProgress extends Progress<{ message?: string; increment?: number }> {} + +/** + * The interpreter path to use for the environment creation. If not provided, will prompt the user to select one. + * If the value of `interpreter` & `workspaceFolder` & `providerId` are provided we will not prompt the user to select a provider, nor folder, nor an interpreter. + */ +export interface CreateEnvironmentOptionsInternal { + workspaceFolder?: WorkspaceFolder; + providerId?: string; + interpreter?: string; +} diff --git a/src/client/pythonEnvironments/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('locator', 'js') === 'native'; +} + +/** + * Set up the Python environments component (during extension activation).' + */ +export async function initialize(ext: ExtensionState): Promise { + // Set up the legacy IOC container before api is created. + initializeLegacyExternalDependencies(ext.legacyIOC.serviceContainer); + + if (useEnvExtension()) { + const api = await createEnvExtApi(ext.disposables); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; + } + + if (shouldUseNativeLocator()) { + const finder = getNativePythonFinder(ext.context); + const api = createNativeEnvironmentsApi(finder); + ext.disposables.push(api); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; + } + + const api = await createPythonEnvironments(() => createLocator(ext)); + registerNewDiscoveryForIOC( + // These are what get wrapped in the legacy adapter. + ext.legacyIOC.serviceManager, + api, + ); + return api; +} + +/** + * Make use of the component (e.g. register with VS Code). + */ +export async function activate(api: IDiscoveryAPI, ext: ExtensionState): Promise { + /** + * Force an initial background refresh of the environments. + * + * Note API is ready to be queried only after a refresh has been triggered, and extension activation is + * blocked on API being ready. So if discovery was never triggered for a scope, we need to block + * extension activation on the "refresh trigger". + */ + const folders = vscode.workspace.workspaceFolders; + // Trigger discovery if environment cache is empty. + const wasTriggered = getGlobalStorage(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []).get().length > 0; + if (!wasTriggered) { + api.triggerRefresh().ignoreErrors(); + folders?.forEach(async (folder) => { + const wasTriggeredForFolder = getGlobalStorage( + ext.context, + `PYTHON_WAS_DISCOVERY_TRIGGERED_${normCasePath(folder.uri.fsPath)}`, + false, + ); + await wasTriggeredForFolder.set(true); + }); + } else { + // Figure out which workspace folders need to be activated if any. + folders?.forEach(async (folder) => { + const wasTriggeredForFolder = getGlobalStorage( + ext.context, + `PYTHON_WAS_DISCOVERY_TRIGGERED_${normCasePath(folder.uri.fsPath)}`, + false, + ); + if (!wasTriggeredForFolder.get()) { + api.triggerRefresh({ + searchLocations: { roots: [folder.uri], doNotIncludeNonRooted: true }, + }).ignoreErrors(); + await wasTriggeredForFolder.set(true); + } + }); + } + + return { + fullyReady: Promise.resolve(), + }; +} + +/** + * Get the locator to use in the component. + */ +async function createLocator( + ext: ExtensionState, + // This is shared. +): Promise { + // Create the low-level locators. + const locators: ILocator = new ExtensionLocators( + // Here we pull the locators together. + createNonWorkspaceLocators(ext), + createWorkspaceLocator(ext), + ); + + // Create the env info service used by ResolvingLocator and CachingLocator. + const envInfoService = getEnvironmentInfoService(ext.disposables); + + // Build the stack of composite locators. + const reducer = new PythonEnvsReducer(locators); + const resolvingLocator = new PythonEnvsResolver( + reducer, + // These are shared. + envInfoService, + ); + const caching = new EnvsCollectionService( + await createCollectionCache(ext), + // This is shared. + resolvingLocator, + shouldUseNativeLocator(), + ); + return caching; +} + +function createNonWorkspaceLocators(ext: ExtensionState): ILocator[] { + const locators: (ILocator & Partial)[] = []; + locators.push( + // OS-independent locators go here. + new PyenvLocator(), + new CondaEnvironmentLocator(), + new ActiveStateLocator(), + new GlobalVirtualEnvironmentLocator(), + new CustomVirtualEnvironmentLocator(), + ); + + if (getOSType() === OSType.Windows) { + locators.push( + // Windows specific locators go here. + new WindowsRegistryLocator(), + new MicrosoftStoreLocator(), + new WindowsPathEnvVarLocator(), + ); + } else { + locators.push( + // Linux/Mac locators go here. + new PosixKnownPathsLocator(), + ); + } + + const disposables = locators.filter((d) => d.dispose !== undefined) as IDisposable[]; + ext.disposables.push(...disposables); + return locators; +} + +function watchRoots(args: WatchRootsArgs): IDisposable { + const { initRoot, addRoot, removeRoot } = args; + + const folders = vscode.workspace.workspaceFolders; + if (folders) { + folders.map((f) => f.uri).forEach(initRoot); + } + + return vscode.workspace.onDidChangeWorkspaceFolders((event) => { + for (const root of event.removed) { + removeRoot(root.uri); + } + for (const root of event.added) { + addRoot(root.uri); + } + }); +} + +function createWorkspaceLocator(ext: ExtensionState): WorkspaceLocators { + const locators = new WorkspaceLocators(watchRoots, [ + (root: vscode.Uri) => [ + new WorkspaceVirtualEnvironmentLocator(root.fsPath), + new PoetryLocator(root.fsPath), + new HatchLocator(root.fsPath), + new PixiLocator(root.fsPath), + new CustomWorkspaceLocator(root.fsPath), + ], + // Add an ILocator factory func here for each kind of workspace-rooted locator. + ]); + ext.disposables.push(locators); + return locators; +} + +function getFromStorage(storage: IPersistentStorage): PythonEnvInfo[] { + return storage.get().map((e) => { + if (e.searchLocation) { + if (typeof e.searchLocation === 'string') { + e.searchLocation = Uri.parse(e.searchLocation); + } else if ('scheme' in e.searchLocation && 'path' in e.searchLocation) { + e.searchLocation = Uri.parse(`${e.searchLocation.scheme}://${e.searchLocation.path}`); + } else { + traceError('Unexpected search location', JSON.stringify(e.searchLocation)); + } + } + return e; + }); +} + +function putIntoStorage(storage: IPersistentStorage, envs: PythonEnvInfo[]): Promise { + storage.set( + // We have to `cloneDeep()` here so that we don't overwrite the original `PythonEnvInfo` objects. + cloneDeep(envs).map((e) => { + if (e.searchLocation) { + // Make TS believe it is string. This is temporary. We need to serialize this in + // a custom way. + e.searchLocation = (e.searchLocation.toString() as unknown) as Uri; + } + return e; + }), + ); + return Promise.resolve(); +} + +async function createCollectionCache(ext: ExtensionState): Promise { + const storage = getGlobalStorage(ext.context, PYTHON_ENV_INFO_CACHE_KEY, []); + const cache = await createCache({ + get: () => getFromStorage(storage), + store: async (e) => putIntoStorage(storage, e), + }); + return cache; +} diff --git a/src/client/pythonEnvironments/info/executable.ts b/src/client/pythonEnvironments/info/executable.ts 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 { + 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 { + 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(); + + 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 { + 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 | 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 { + // 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 { + 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 { + // 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 { + 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 { + // 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 = async () => true, + ): Promise { + const onAddedToCollection = createDeferred(); + // Watch for collection changed events. + this.api.onChanged(async (e: PythonEnvCollectionChangedEvent) => { + if (e.new) { + if (await filter(convertEnvInfo(e.new))) { + onAddedToCollection.resolve(); + } + } + }); + const initialEnvs = await asyncFilter(this.api.getEnvs(), (e) => filter(convertEnvInfo(e))); + if (initialEnvs.length > 0) { + return true; + } + // Wait for an env to be added to the collection until the refresh has finished. Note although it's not + // guaranteed we have initiated discovery in this session, we do trigger refresh in the very first session, + // when Python is not installed, etc. Assuming list is more or less upto date. + await Promise.race([onAddedToCollection.promise, this.api.getRefreshPromise()]); + const envs = await asyncFilter(this.api.getEnvs(), (e) => filter(convertEnvInfo(e))); + return envs.length > 0; + } + + 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 { + 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, CondaService); + serviceManager.addSingletonInstance(IComponentAdapter, new ComponentAdapter(api)); +} diff --git a/src/client/pythonEnvironments/nativeAPI.ts b/src/client/pythonEnvironments/nativeAPI.ts new file mode 100644 index 000000000000..62695c8dd543 --- /dev/null +++ b/src/client/pythonEnvironments/nativeAPI.ts @@ -0,0 +1,548 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { Disposable, Event, EventEmitter, Uri, WorkspaceFoldersChangeEvent } from 'vscode'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType, PythonVersion } from './base/info'; +import { + GetRefreshEnvironmentsOptions, + IDiscoveryAPI, + ProgressNotificationEvent, + ProgressReportStage, + PythonLocatorQuery, + TriggerRefreshOptions, +} from './base/locator'; +import { PythonEnvCollectionChangedEvent } from './base/watcher'; +import { + isNativeEnvInfo, + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonFinder, +} from './base/locators/common/nativePythonFinder'; +import { createDeferred, Deferred } from '../common/utils/async'; +import { Architecture, getPathEnvVariable, getUserHomeDir } from '../common/utils/platform'; +import { parseVersion } from './base/info/pythonVersion'; +import { cache } from '../common/utils/decorators'; +import { traceError, traceInfo, traceLog, traceWarn } from '../logging'; +import { StopWatch } from '../common/utils/stopWatch'; +import { FileChangeType } from '../common/platform/fileSystemWatcher'; +import { categoryToKind, NativePythonEnvironmentKind } from './base/locators/common/nativePythonUtils'; +import { getCondaEnvDirs, getCondaPathSetting, setCondaBinary } from './common/environmentManagers/conda'; +import { setPyEnvBinary } from './common/environmentManagers/pyenv'; +import { + createPythonWatcher, + PythonGlobalEnvEvent, + PythonWorkspaceEnvEvent, +} from './base/locators/common/pythonWatcher'; +import { getWorkspaceFolders, onDidChangeWorkspaceFolders } from '../common/vscodeApis/workspaceApis'; + +function makeExecutablePath(prefix?: string): string { + if (!prefix) { + return process.platform === 'win32' ? 'python.exe' : 'python'; + } + return process.platform === 'win32' ? path.join(prefix, 'python.exe') : path.join(prefix, 'python'); +} + +function toArch(a: string | undefined): Architecture { + switch (a) { + case 'x86': + return Architecture.x86; + case 'x64': + return Architecture.x64; + default: + return Architecture.Unknown; + } +} + +function getLocation(nativeEnv: NativeEnvInfo, executable: string): string { + if (nativeEnv.kind === NativePythonEnvironmentKind.Conda) { + return nativeEnv.prefix ?? path.dirname(executable); + } + + if (nativeEnv.executable) { + return nativeEnv.executable; + } + + if (nativeEnv.prefix) { + return nativeEnv.prefix; + } + + // This is a path to a generated executable. Needed for backwards compatibility. + return executable; +} + +function kindToShortString(kind: PythonEnvKind): string | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + return 'poetry'; + case PythonEnvKind.Pyenv: + return 'pyenv'; + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + return 'venv'; + case PythonEnvKind.Pipenv: + return 'pipenv'; + case PythonEnvKind.Conda: + return 'conda'; + case PythonEnvKind.ActiveState: + return 'active-state'; + case PythonEnvKind.MicrosoftStore: + return 'Microsoft Store'; + case PythonEnvKind.Hatch: + return 'hatch'; + case PythonEnvKind.Pixi: + return 'pixi'; + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + default: + return undefined; + } +} + +function toShortVersionString(version: PythonVersion): string { + return `${version.major}.${version.minor}.${version.micro}`.trim(); +} + +function getDisplayName(version: PythonVersion, kind: PythonEnvKind, arch: Architecture, name?: string): string { + const versionStr = toShortVersionString(version); + const kindStr = kindToShortString(kind); + if (arch === Architecture.x86) { + if (kindStr) { + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit (${kindStr})`; + } + return name ? `Python ${versionStr} 32-bit (${name})` : `Python ${versionStr} 32-bit`; + } + if (kindStr) { + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr} (${kindStr})`; + } + return name ? `Python ${versionStr} (${name})` : `Python ${versionStr}`; +} + +function validEnv(nativeEnv: NativeEnvInfo): boolean { + if (nativeEnv.prefix === undefined && nativeEnv.executable === undefined) { + traceError(`Invalid environment [native]: ${JSON.stringify(nativeEnv)}`); + return false; + } + return true; +} + +function getEnvType(kind: PythonEnvKind): PythonEnvType | undefined { + switch (kind) { + case PythonEnvKind.Poetry: + case PythonEnvKind.Pyenv: + case PythonEnvKind.VirtualEnv: + case PythonEnvKind.Venv: + case PythonEnvKind.VirtualEnvWrapper: + case PythonEnvKind.OtherVirtual: + case PythonEnvKind.Pipenv: + case PythonEnvKind.ActiveState: + case PythonEnvKind.Hatch: + case PythonEnvKind.Pixi: + return PythonEnvType.Virtual; + + case PythonEnvKind.Conda: + return PythonEnvType.Conda; + + case PythonEnvKind.System: + case PythonEnvKind.Unknown: + case PythonEnvKind.OtherGlobal: + case PythonEnvKind.Custom: + case PythonEnvKind.MicrosoftStore: + default: + return undefined; + } +} + +function isSubDir(pathToCheck: string | undefined, parents: string[]): boolean { + return parents.some((prefix) => { + if (pathToCheck) { + return path.normalize(pathToCheck).startsWith(path.normalize(prefix)); + } + return false; + }); +} + +function foundOnPath(fsPath: string): boolean { + const paths = getPathEnvVariable().map((p) => path.normalize(p).toLowerCase()); + const normalized = path.normalize(fsPath).toLowerCase(); + return paths.some((p) => normalized.includes(p)); +} + +function getName(nativeEnv: NativeEnvInfo, kind: PythonEnvKind, condaEnvDirs: string[]): string { + if (nativeEnv.name) { + return nativeEnv.name; + } + + const envType = getEnvType(kind); + if (nativeEnv.prefix && envType === PythonEnvType.Virtual) { + return path.basename(nativeEnv.prefix); + } + + if (nativeEnv.prefix && envType === PythonEnvType.Conda) { + if (nativeEnv.name === 'base') { + return 'base'; + } + + const workspaces = (getWorkspaceFolders() ?? []).map((wf) => wf.uri.fsPath); + if (isSubDir(nativeEnv.prefix, workspaces)) { + traceInfo(`Conda env is --prefix environment: ${nativeEnv.prefix}`); + return ''; + } + + if (condaEnvDirs.length > 0 && isSubDir(nativeEnv.prefix, condaEnvDirs)) { + traceInfo(`Conda env is --named environment: ${nativeEnv.prefix}`); + return path.basename(nativeEnv.prefix); + } + } + + return ''; +} + +function toPythonEnvInfo(nativeEnv: NativeEnvInfo, condaEnvDirs: string[]): PythonEnvInfo | undefined { + if (!validEnv(nativeEnv)) { + return undefined; + } + const kind = categoryToKind(nativeEnv.kind); + const arch = toArch(nativeEnv.arch); + const version: PythonVersion = parseVersion(nativeEnv.version ?? ''); + const name = getName(nativeEnv, kind, condaEnvDirs); + const displayName = nativeEnv.version + ? getDisplayName(version, kind, arch, name) + : nativeEnv.displayName ?? 'Python'; + + const executable = nativeEnv.executable ?? makeExecutablePath(nativeEnv.prefix); + return { + name, + location: getLocation(nativeEnv, executable), + kind, + id: executable, + executable: { + filename: executable, + sysPrefix: nativeEnv.prefix ?? '', + ctime: -1, + mtime: -1, + }, + version: { + sysVersion: nativeEnv.version, + major: version.major, + minor: version.minor, + micro: version.micro, + }, + arch, + distro: { + org: '', + }, + source: [], + detailedDisplayName: displayName, + display: displayName, + type: getEnvType(kind), + }; +} + +function hasChanged(old: PythonEnvInfo, newEnv: PythonEnvInfo): boolean { + if (old.name !== newEnv.name) { + return true; + } + if (old.executable.filename !== newEnv.executable.filename) { + return true; + } + if (old.version.major !== newEnv.version.major) { + return true; + } + if (old.version.minor !== newEnv.version.minor) { + return true; + } + if (old.version.micro !== newEnv.version.micro) { + return true; + } + if (old.location !== newEnv.location) { + return true; + } + if (old.kind !== newEnv.kind) { + return true; + } + if (old.arch !== newEnv.arch) { + return true; + } + + return false; +} + +class NativePythonEnvironments implements IDiscoveryAPI, Disposable { + private _onProgress: EventEmitter; + + private _onChanged: EventEmitter; + + private _refreshPromise?: Deferred; + + private _envs: PythonEnvInfo[] = []; + + private _disposables: Disposable[] = []; + + private _condaEnvDirs: string[] = []; + + constructor(private readonly finder: NativePythonFinder) { + this._onProgress = new EventEmitter(); + this._onChanged = new EventEmitter(); + + this.onProgress = this._onProgress.event; + this.onChanged = this._onChanged.event; + + this.refreshState = ProgressReportStage.idle; + this._disposables.push(this._onProgress, this._onChanged); + + this.initializeWatcher(); + } + + dispose(): void { + this._disposables.forEach((d) => d.dispose()); + } + + refreshState: ProgressReportStage; + + onProgress: Event; + + onChanged: Event; + + getRefreshPromise(_options?: GetRefreshEnvironmentsOptions): Promise | undefined { + return this._refreshPromise?.promise; + } + + triggerRefresh(_query?: PythonLocatorQuery, _options?: TriggerRefreshOptions): Promise { + const stopwatch = new StopWatch(); + traceLog('Native locator: Refresh started'); + if (this.refreshState === ProgressReportStage.discoveryStarted && this._refreshPromise?.promise) { + return this._refreshPromise?.promise; + } + + this.refreshState = ProgressReportStage.discoveryStarted; + this._onProgress.fire({ stage: this.refreshState }); + this._refreshPromise = createDeferred(); + + setImmediate(async () => { + try { + const before = this._envs.map((env) => env.executable.filename); + const after: string[] = []; + for await (const native of this.finder.refresh()) { + const exe = this.processNative(native); + if (exe) { + after.push(exe); + } + } + const envsToRemove = before.filter((item) => !after.includes(item)); + envsToRemove.forEach((item) => this.removeEnv(item)); + this._refreshPromise?.resolve(); + } catch (error) { + this._refreshPromise?.reject(error); + } finally { + traceLog(`Native locator: Refresh finished in ${stopwatch.elapsedTime} ms`); + this.refreshState = ProgressReportStage.discoveryFinished; + this._refreshPromise = undefined; + this._onProgress.fire({ stage: this.refreshState }); + } + }); + + return this._refreshPromise?.promise; + } + + private processNative(native: NativeEnvInfo | NativeEnvManagerInfo): string | undefined { + if (isNativeEnvInfo(native)) { + return this.processEnv(native); + } + this.processEnvManager(native); + + return undefined; + } + + private processEnv(native: NativeEnvInfo): string | undefined { + if (!validEnv(native)) { + return undefined; + } + + try { + const version = native.version ? parseVersion(native.version) : undefined; + + if (categoryToKind(native.kind) === PythonEnvKind.Conda && !native.executable) { + // This is a conda env without python, no point trying to resolve this. + // There is nothing to resolve + return this.addEnv(native)?.executable.filename; + } + if (native.executable && (!version || version.major < 0 || version.minor < 0 || version.micro < 0)) { + // We have a path, but no version info, try to resolve the environment. + this.finder + .resolve(native.executable) + .then((env) => { + if (env) { + this.addEnv(env); + } + }) + .ignoreErrors(); + return native.executable; + } + if (native.executable && version && version.major >= 0 && version.minor >= 0 && version.micro >= 0) { + return this.addEnv(native)?.executable.filename; + } + traceError(`Failed to process environment: ${JSON.stringify(native)}`); + } catch (err) { + traceError(`Failed to process environment: ${err}`); + } + return undefined; + } + + private condaPathAlreadySet: string | undefined; + + // eslint-disable-next-line class-methods-use-this + private processEnvManager(native: NativeEnvManagerInfo) { + const tool = native.tool.toLowerCase(); + switch (tool) { + case 'conda': + { + traceLog(`Conda environment manager found at: ${native.executable}`); + const settingPath = getCondaPathSetting(); + if (!this.condaPathAlreadySet) { + if (settingPath === '' || settingPath === undefined) { + if (foundOnPath(native.executable)) { + setCondaBinary(native.executable); + this.condaPathAlreadySet = native.executable; + traceInfo(`Using conda: ${native.executable}`); + } else { + traceInfo(`Conda not found on PATH, skipping: ${native.executable}`); + traceInfo( + 'You can set the path to conda using the setting: `python.condaPath` if you want to use a different conda binary', + ); + } + } else { + traceInfo(`Using conda from setting: ${settingPath}`); + this.condaPathAlreadySet = settingPath; + } + } else { + traceInfo(`Conda set to: ${this.condaPathAlreadySet}`); + } + } + break; + case 'pyenv': + traceLog(`Pyenv environment manager found at: ${native.executable}`); + setPyEnvBinary(native.executable); + break; + case 'poetry': + traceLog(`Poetry environment manager found at: ${native.executable}`); + break; + default: + traceWarn(`Unknown environment manager: ${native.tool}`); + break; + } + } + + getEnvs(_query?: PythonLocatorQuery): PythonEnvInfo[] { + return this._envs; + } + + private addEnv(native: NativeEnvInfo, searchLocation?: Uri): PythonEnvInfo | undefined { + const info = toPythonEnvInfo(native, this._condaEnvDirs); + if (info) { + const old = this._envs.find((item) => item.executable.filename === info.executable.filename); + if (old) { + this._envs = this._envs.filter((item) => item.executable.filename !== info.executable.filename); + this._envs.push(info); + if (hasChanged(old, info)) { + this._onChanged.fire({ type: FileChangeType.Changed, old, new: info, searchLocation }); + } + } else { + this._envs.push(info); + this._onChanged.fire({ type: FileChangeType.Created, new: info, searchLocation }); + } + } + + return info; + } + + private removeEnv(env: PythonEnvInfo | string): void { + if (typeof env === 'string') { + const old = this._envs.find((item) => item.executable.filename === env); + this._envs = this._envs.filter((item) => item.executable.filename !== env); + this._onChanged.fire({ type: FileChangeType.Deleted, old }); + return; + } + this._envs = this._envs.filter((item) => item.executable.filename !== env.executable.filename); + this._onChanged.fire({ type: FileChangeType.Deleted, old: env }); + } + + @cache(30_000, true) + async resolveEnv(envPath?: string): Promise { + if (envPath === undefined) { + return undefined; + } + try { + const native = await this.finder.resolve(envPath); + if (native) { + if (native.kind === NativePythonEnvironmentKind.Conda && this._condaEnvDirs.length === 0) { + this._condaEnvDirs = (await getCondaEnvDirs()) ?? []; + } + return this.addEnv(native); + } + return undefined; + } catch { + return undefined; + } + } + + private initializeWatcher(): void { + const watcher = createPythonWatcher(); + this._disposables.push( + watcher.onDidGlobalEnvChanged((e) => this.pathEventHandler(e)), + watcher.onDidWorkspaceEnvChanged(async (e) => { + await this.workspaceEventHandler(e); + }), + onDidChangeWorkspaceFolders((e: WorkspaceFoldersChangeEvent) => { + e.removed.forEach((wf) => watcher.unwatchWorkspace(wf)); + e.added.forEach((wf) => watcher.watchWorkspace(wf)); + }), + watcher, + ); + + getWorkspaceFolders()?.forEach((wf) => watcher.watchWorkspace(wf)); + const home = getUserHomeDir(); + if (home) { + watcher.watchPath(Uri.file(path.join(home, '.conda', 'environments.txt'))); + } + } + + private async pathEventHandler(e: PythonGlobalEnvEvent): Promise { + if (e.type === FileChangeType.Created || e.type === FileChangeType.Changed) { + if (e.uri.fsPath.endsWith('environment.txt')) { + const before = this._envs + .filter((env) => env.kind === PythonEnvKind.Conda) + .map((env) => env.executable.filename); + for await (const native of this.finder.refresh(NativePythonEnvironmentKind.Conda)) { + this.processNative(native); + } + const after = this._envs + .filter((env) => env.kind === PythonEnvKind.Conda) + .map((env) => env.executable.filename); + const envsToRemove = before.filter((item) => !after.includes(item)); + envsToRemove.forEach((item) => this.removeEnv(item)); + } + } + } + + private async workspaceEventHandler(e: PythonWorkspaceEnvEvent): Promise { + if (e.type === FileChangeType.Created || e.type === FileChangeType.Changed) { + const native = await this.finder.resolve(e.executable); + if (native) { + this.addEnv(native, e.workspaceFolder.uri); + } + } else { + this.removeEnv(e.executable); + } + } +} + +export function createNativeEnvironmentsApi(finder: NativePythonFinder): IDiscoveryAPI & Disposable { + const native = new NativePythonEnvironments(finder); + native.triggerRefresh().ignoreErrors(); + return native; +} diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts deleted file mode 100644 index 6f1abe27ac06..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) => void; - private _commandReject!: (reason?: any) => void; - private initialized!: Deferred; - constructor(extensionDir: string, _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(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise { - if (!options) { - options = window.activeTextEditor!.options; - } - const command = { - lookup: 'rename', - file: filePath, - start: this.getOffsetAt(document, range.start).toString(), - id: '1', - name: name, - indent_size: options.tabSize - }; - - return this.sendCommand(JSON.stringify(command)); - } - public extractVariable(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise { - if (!options) { - options = window.activeTextEditor!.options; - } - const command = { - lookup: 'extract_variable', - file: filePath, - start: this.getOffsetAt(document, range.start).toString(), - end: this.getOffsetAt(document, range.end).toString(), - id: '1', - name: name, - indent_size: options.tabSize - }; - return this.sendCommand(JSON.stringify(command)); - } - public extractMethod(document: TextDocument, name: string, filePath: string, range: Range, options?: TextEditorOptions): Promise { - if (!options) { - options = window.activeTextEditor!.options; - } - // Ensure last line is an empty line - if (!document.lineAt(document.lineCount - 1).isEmptyOrWhitespace && range.start.line === document.lineCount - 1) { - return Promise.reject('Missing blank line at the end of document (PEP8).'); - } - const command = { - lookup: 'extract_method', - file: filePath, - start: this.getOffsetAt(document, range.start).toString(), - end: this.getOffsetAt(document, range.end).toString(), - id: '1', - name: name, - indent_size: options.tabSize - }; - return this.sendCommand(JSON.stringify(command)); - } - private sendCommand(command: string): Promise { - return this.initialize().then(() => { - // tslint:disable-next-line:promise-must-complete - return new Promise((resolve, reject) => { - this._commandResolve = resolve; - this._commandReject = reject; - this._process!.stdin.write(command + '\n'); - }); - }); - } - private async initialize(): Promise { - const pythonProc = await this.serviceContainer.get(IPythonExecutionFactory).create({ resource: Uri.file(this.workspaceRoot) }); - this.initialized = createDeferred(); - 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 { + const nativeRepl = new NativeRepl(); + nativeRepl.interpreter = interpreter; + await nativeRepl.setReplDirectory(); + nativeRepl.pythonServer = createPythonServer([interpreter.path as string], nativeRepl.cwd); + nativeRepl.disposables.push(nativeRepl.pythonServer); + nativeRepl.setReplController(); + nativeRepl.registerInterpreterChangeHandler(); + + return nativeRepl; + } + + dispose(): void { + this.disposables.forEach((d) => d.dispose()); + } + + /** + * Function that watches for Notebook Closed event. + * This is for the purposes of correctly updating the notebookEditor and notebookDocument on close. + */ + private watchNotebookClosed(): void { + this.disposables.push( + onDidCloseNotebookDocument(async (nb) => { + if (this.notebookDocument && nb.uri.toString() === this.notebookDocument.uri.toString()) { + this.notebookDocument = undefined; + this.newReplSession = true; + await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([this.interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + nativeRepl = undefined; + } + }), + ); + } + + /** + * Function that set up desired directory for REPL. + * If there is multiple workspaces, prompt the user to choose + * which directory we should set in context of native REPL. + */ + private async setReplDirectory(): Promise { + // Figure out uri via workspaceFolder as uri parameter always + // seem to be undefined from parameter when trying to access from replCommands.ts + const workspaces: readonly WorkspaceFolder[] | undefined = getWorkspaceFolders(); + + if (workspaces) { + // eslint-disable-next-line no-shadow + const workspacesQuickPickItems: QuickPickItem[] = workspaces.map((workspace) => ({ + label: workspace.name, + description: workspace.uri.fsPath, + })); + + if (workspacesQuickPickItems.length === 0) { + this.cwd = process.cwd(); // Yields '/' on no workspace scenario. + } else if (workspacesQuickPickItems.length === 1) { + this.cwd = workspacesQuickPickItems[0].description; + } else { + // Show choices of workspaces for user to choose from. + const selection = (await showQuickPick(workspacesQuickPickItems, { + placeHolder: 'Select current working directory for new REPL', + matchOnDescription: true, + ignoreFocusOut: true, + })) as QuickPickItem; + this.cwd = selection?.description; + } + } + } + + /** + * Function that check if NotebookController for REPL exists, and returns it in Singleton manner. + */ + public setReplController(force: boolean = false): NotebookController { + if (!this.replController || force) { + this.replController = createReplController(this.interpreter!.path, this.disposables, this.cwd); + this.replController.variableProvider = new VariablesProvider( + new VariableRequester(this.pythonServer), + () => this.notebookDocument, + this.pythonServer.onCodeExecuted, + ); + } + return this.replController; + } + + private registerInterpreterChangeHandler(): void { + if (!useEnvExtension() || this.envChangeListenerRegistered) { + return; + } + this.envChangeListenerRegistered = true; + this.disposables.push( + onDidChangeEnvironmentEnvExt((event) => { + this.updateInterpreterForChange(event.uri).catch(() => undefined); + }), + ); + this.disposables.push( + this.pythonServer.onCodeExecuted(() => { + if (this.pendingInterpreterChange) { + const { resource } = this.pendingInterpreterChange; + this.pendingInterpreterChange = undefined; + this.updateInterpreterForChange(resource).catch(() => undefined); + } + }), + ); + } + + private async updateInterpreterForChange(resource?: Uri): Promise { + if (this.pythonServer?.isExecuting) { + this.pendingInterpreterChange = { resource }; + return; + } + if (!this.shouldApplyInterpreterChange(resource)) { + return; + } + const scope = resource ?? (this.cwd ? Uri.file(this.cwd) : undefined); + const interpreter = await getActiveInterpreterLegacy(scope); + if (!interpreter || interpreter.path === this.interpreter?.path) { + return; + } + + this.interpreter = interpreter; + this.pythonServer.dispose(); + this.pythonServer = createPythonServer([interpreter.path as string], this.cwd); + this.disposables.push(this.pythonServer); + if (this.replController) { + this.replController.dispose(); + } + this.setReplController(true); + + if (this.notebookDocument) { + const notebookEditor = await showNotebookDocument(this.notebookDocument, { preserveFocus: true }); + await selectNotebookKernel(notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + } + + private shouldApplyInterpreterChange(resource?: Uri): boolean { + if (!resource || !this.cwd) { + return true; + } + const relative = path.relative(this.cwd, resource.fsPath); + return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative)); + } + + /** + * Function that checks if native REPL's text input box contains complete code. + * @returns Promise - True if complete/Valid code is present, False otherwise. + */ + public async checkUserInputCompleteCode(activeEditor: TextEditor | undefined): Promise { + let completeCode = false; + let userTextInput; + if (activeEditor) { + const { document } = activeEditor; + userTextInput = document.getText(); + } + + // Check if userTextInput is a complete Python command + if (userTextInput) { + completeCode = await this.pythonServer.checkValidCommand(userTextInput); + } + + return completeCode; + } + + /** + * Function that opens interactive repl, selects kernel, and send/execute code to the native repl. + */ + public async sendToNativeRepl(code?: string | undefined, preserveFocus: boolean = true): Promise { + let wsMementoUri: Uri | undefined; + + if (!this.notebookDocument) { + const wsMemento = getWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO); + wsMementoUri = wsMemento ? Uri.parse(wsMemento) : undefined; + + if (!wsMementoUri || getTabNameForUri(wsMementoUri) !== 'Python REPL') { + await updateWorkspaceStateValue(NATIVE_REPL_URI_MEMENTO, undefined); + wsMementoUri = undefined; + } + } + + const result = await openInteractiveREPL(this.notebookDocument ?? wsMementoUri, preserveFocus); + if (result) { + this.notebookDocument = result.notebookEditor.notebook; + await updateWorkspaceStateValue( + NATIVE_REPL_URI_MEMENTO, + this.notebookDocument.uri.toString(), + ); + + if (result.documentCreated) { + await selectNotebookKernel(result.notebookEditor, this.replController.id, PVSC_EXTENSION_ID); + } + if (code) { + await executeNotebookCell(result.notebookEditor, code); + } + } + } +} + +/** + * Get Singleton Native REPL Instance + * @param interpreter + * @returns Native REPL instance + */ +export async function getNativeRepl(interpreter: PythonEnvironment, disposables: Disposable[]): Promise { + if (!nativeRepl) { + nativeRepl = await NativeRepl.create(interpreter); + disposables.push(nativeRepl); + } + if (nativeRepl && nativeRepl.newReplSession) { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); + nativeRepl.newReplSession = false; + } + return nativeRepl; +} diff --git a/src/client/repl/pythonServer.ts b/src/client/repl/pythonServer.ts new file mode 100644 index 000000000000..c4b1722b5079 --- /dev/null +++ b/src/client/repl/pythonServer.ts @@ -0,0 +1,168 @@ +import * as path from 'path'; +import * as ch from 'child_process'; +import * as rpc from 'vscode-jsonrpc/node'; +import { Disposable, Event, EventEmitter, window } from 'vscode'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { traceError, traceLog } from '../logging'; +import { captureTelemetry } from '../telemetry'; +import { EventName } from '../telemetry/constants'; + +const SERVER_PATH = path.join(EXTENSION_ROOT_DIR, 'python_files', 'python_server.py'); +let serverInstance: PythonServer | undefined; +export interface ExecutionResult { + status: boolean; + output: string; +} + +export interface PythonServer extends Disposable { + onCodeExecuted: Event; + readonly isExecuting: boolean; + readonly isDisposed: boolean; + execute(code: string): Promise; + executeSilently(code: string): Promise; + interrupt(): void; + input(): void; + checkValidCommand(code: string): Promise; +} + +class PythonServerImpl implements PythonServer, Disposable { + private readonly disposables: Disposable[] = []; + + private readonly _onCodeExecuted = new EventEmitter(); + + onCodeExecuted = this._onCodeExecuted.event; + + private inFlightRequests = 0; + + private disposed = false; + + public get isExecuting(): boolean { + return this.inFlightRequests > 0; + } + + public get isDisposed(): boolean { + return this.disposed; + } + + constructor(private connection: rpc.MessageConnection, private pythonServer: ch.ChildProcess) { + this.initialize(); + this.input(); + } + + private initialize(): void { + this.disposables.push( + this.connection.onNotification('log', (message: string) => { + traceLog('Log:', message); + }), + ); + this.pythonServer.on('exit', (code) => { + traceError(`Python server exited with code ${code}`); + this.markDisposed(); + }); + this.pythonServer.on('error', (err) => { + traceError(err); + this.markDisposed(); + }); + this.connection.listen(); + } + + public input(): void { + // Register input request handler + this.connection.onRequest('input', async (request) => { + // Ask for user input via popup quick input, send it back to Python + let userPrompt = 'Enter your input here: '; + if (request && request.prompt) { + userPrompt = request.prompt; + } + const input = await window.showInputBox({ + title: 'Input Request', + prompt: userPrompt, + ignoreFocusOut: true, + }); + return { userInput: input }; + }); + } + + @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) + public async execute(code: string): Promise { + const result = await this.executeCode(code); + if (result?.status) { + this._onCodeExecuted.fire(); + } + return result; + } + + public executeSilently(code: string): Promise { + return this.executeCode(code); + } + + private async executeCode(code: string): Promise { + this.inFlightRequests += 1; + try { + const result = await this.connection.sendRequest('execute', code); + return result as ExecutionResult; + } catch (err) { + const error = err as Error; + traceError(`Error getting response from REPL server:`, error); + } finally { + this.inFlightRequests -= 1; + } + return undefined; + } + + public interrupt(): void { + // Passing SIGINT to interrupt only would work for Mac and Linux + if (this.pythonServer.kill('SIGINT')) { + traceLog('Python REPL server interrupted'); + } + } + + public async checkValidCommand(code: string): Promise { + this.inFlightRequests += 1; + try { + const completeCode: ExecutionResult = await this.connection.sendRequest('check_valid_command', code); + return completeCode.output === 'True'; + } finally { + this.inFlightRequests -= 1; + } + } + + public dispose(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.sendNotification('exit'); + this.disposables.forEach((d) => d.dispose()); + this.connection.dispose(); + serverInstance = undefined; + } + + private markDisposed(): void { + if (this.disposed) { + return; + } + this.disposed = true; + this.connection.dispose(); + serverInstance = undefined; + } +} + +export function createPythonServer(interpreter: string[], cwd?: string): PythonServer { + if (serverInstance && !serverInstance.isDisposed) { + return serverInstance; + } + + const pythonServer = ch.spawn(interpreter[0], [...interpreter.slice(1), SERVER_PATH], { + cwd, // Launch with correct workspace directory + }); + pythonServer.stderr.on('data', (data) => { + traceError(data.toString()); + }); + const connection = rpc.createMessageConnection( + new rpc.StreamMessageReader(pythonServer.stdout), + new rpc.StreamMessageWriter(pythonServer.stdin), + ); + serverInstance = new PythonServerImpl(connection, pythonServer); + return serverInstance; +} diff --git a/src/client/repl/replCommandHandler.ts b/src/client/repl/replCommandHandler.ts new file mode 100644 index 000000000000..630eddfdd565 --- /dev/null +++ b/src/client/repl/replCommandHandler.ts @@ -0,0 +1,98 @@ +import { + NotebookEditor, + ViewColumn, + NotebookDocument, + NotebookCellData, + NotebookCellKind, + NotebookEdit, + WorkspaceEdit, + Uri, +} from 'vscode'; +import { getExistingReplViewColumn, getTabNameForUri } from './replUtils'; +import { showNotebookDocument } from '../common/vscodeApis/windowApis'; +import { openNotebookDocument, applyEdit } from '../common/vscodeApis/workspaceApis'; +import { executeCommand } from '../common/vscodeApis/commandApis'; + +/** + * Function that opens/show REPL using IW UI. + */ +export async function openInteractiveREPL( + notebookDocument: NotebookDocument | Uri | undefined, + preserveFocus: boolean = true, +): Promise<{ notebookEditor: NotebookEditor; documentCreated: boolean } | undefined> { + let viewColumn = ViewColumn.Beside; + let alreadyExists = false; + if (notebookDocument instanceof Uri) { + // Case where NotebookDocument is undefined, but workspace mementoURI exists. + notebookDocument = await openNotebookDocument(notebookDocument); + } else if (notebookDocument) { + // Case where NotebookDocument (REPL document already exists in the tab) + const existingReplViewColumn = getExistingReplViewColumn(notebookDocument); + viewColumn = existingReplViewColumn ?? viewColumn; + alreadyExists = true; + } else if (!notebookDocument) { + // Case where NotebookDocument doesnt exist, or + // became outdated (untitled.ipynb created without Python extension knowing, effectively taking over original Python REPL's URI) + notebookDocument = await openNotebookDocument('jupyter-notebook'); + } + + const notebookEditor = await showNotebookDocument(notebookDocument!, { + viewColumn, + asRepl: 'Python REPL', + preserveFocus, + }); + + // Sanity check that we opened a Native REPL from showNotebookDocument. + if ( + !notebookEditor || + !notebookEditor.notebook || + !notebookEditor.notebook.uri || + getTabNameForUri(notebookEditor.notebook.uri) !== 'Python REPL' + ) { + return undefined; + } + + return { notebookEditor, documentCreated: !alreadyExists }; +} + +/** + * Function that selects notebook Kernel. + */ +export async function selectNotebookKernel( + notebookEditor: NotebookEditor, + notebookControllerId: string, + extensionId: string, +): Promise { + await executeCommand('notebook.selectKernel', { + notebookEditor, + id: notebookControllerId, + extension: extensionId, + }); +} + +/** + * Function that executes notebook cell given code. + */ +export async function executeNotebookCell(notebookEditor: NotebookEditor, code: string): Promise { + const { notebook, replOptions } = notebookEditor; + const cellIndex = replOptions?.appendIndex ?? notebook.cellCount; + await addCellToNotebook(notebook, cellIndex, code); + // Execute the cell + executeCommand('notebook.cell.execute', { + ranges: [{ start: cellIndex, end: cellIndex + 1 }], + document: notebook.uri, + }); +} + +/** + * Function that adds cell to notebook. + * This function will only get called when notebook document is defined. + */ +async function addCellToNotebook(notebookDocument: NotebookDocument, index: number, code: string): Promise { + const notebookCellData = new NotebookCellData(NotebookCellKind.Code, code as string, 'python'); + // Add new cell to interactive window document + const notebookEdit = NotebookEdit.insertCells(index, [notebookCellData]); + const workspaceEdit = new WorkspaceEdit(); + workspaceEdit.set(notebookDocument!.uri, [notebookEdit]); + await applyEdit(workspaceEdit); +} diff --git a/src/client/repl/replCommands.ts b/src/client/repl/replCommands.ts new file mode 100644 index 000000000000..1171e9466ee8 --- /dev/null +++ b/src/client/repl/replCommands.ts @@ -0,0 +1,131 @@ +import { commands, Uri, window } from 'vscode'; +import { Disposable } from 'vscode-jsonrpc'; +import { ICommandManager } from '../common/application/types'; +import { Commands } from '../common/constants'; +import { IInterpreterService } from '../interpreter/contracts'; +import { ICodeExecutionHelper } from '../terminals/types'; +import { getNativeRepl } from './nativeRepl'; +import { + executeInTerminal, + getActiveInterpreter, + getSelectedTextToExecute, + getSendToNativeREPLSetting, + insertNewLineToREPLInput, + isMultiLineText, +} from './replUtils'; +import { registerCommand } from '../common/vscodeApis/commandApis'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ReplType } from './types'; + +/** + * Register Start Native REPL command in the command palette + */ +export async function registerStartNativeReplCommand( + disposables: Disposable[], + interpreterService: IInterpreterService, +): Promise { + disposables.push( + registerCommand(Commands.Start_Native_REPL, async (uri: Uri) => { + sendTelemetryEvent(EventName.REPL, undefined, { replType: 'Native' }); + const interpreter = await getActiveInterpreter(uri, interpreterService); + if (interpreter) { + const nativeRepl = await getNativeRepl(interpreter, disposables); + await nativeRepl.sendToNativeRepl(undefined, false); + } + }), + ); +} + +/** + * Registers REPL command for shift+enter if sendToNativeREPL setting is enabled. + */ +export async function registerReplCommands( + disposables: Disposable[], + interpreterService: IInterpreterService, + executionHelper: ICodeExecutionHelper, + commandManager: ICommandManager, +): Promise { + disposables.push( + commandManager.registerCommand(Commands.Exec_In_REPL, async (uri: Uri) => { + const nativeREPLSetting = getSendToNativeREPLSetting(); + + if (!nativeREPLSetting) { + await executeInTerminal(); + return; + } + const interpreter = await getActiveInterpreter(uri, interpreterService); + + if (interpreter) { + const nativeRepl = await getNativeRepl(interpreter, disposables); + const activeEditor = window.activeTextEditor; + if (activeEditor) { + const code = await getSelectedTextToExecute(activeEditor); + if (code) { + // Smart Send + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await executionHelper.normalizeLines( + code!, + ReplType.native, + wholeFileContent, + ); + await nativeRepl.sendToNativeRepl(normalizedCode); + } + } + } + }), + ); +} + +/** + * Command triggered for 'Enter': Conditionally call interactive.execute OR insert \n in text input box. + */ +export async function registerReplExecuteOnEnter( + disposables: Disposable[], + interpreterService: IInterpreterService, + commandManager: ICommandManager, +): Promise { + disposables.push( + commandManager.registerCommand(Commands.Exec_In_REPL_Enter, async (uri: Uri) => { + await onInputEnter(uri, 'repl.execute', interpreterService, disposables); + }), + ); + disposables.push( + commandManager.registerCommand(Commands.Exec_In_IW_Enter, async (uri: Uri) => { + await onInputEnter(uri, 'interactive.execute', interpreterService, disposables); + }), + ); +} + +async function onInputEnter( + uri: Uri | undefined, + commandName: string, + interpreterService: IInterpreterService, + disposables: Disposable[], +): Promise { + const interpreter = await getActiveInterpreter(uri, interpreterService); + if (!interpreter) { + return; + } + + const nativeRepl = await getNativeRepl(interpreter, disposables); + const completeCode = await nativeRepl?.checkUserInputCompleteCode(window.activeTextEditor); + const editor = window.activeTextEditor; + + if (editor) { + // Execute right away when complete code and Not multi-line + if (completeCode && !isMultiLineText(editor)) { + await commands.executeCommand(commandName); + } else { + insertNewLineToREPLInput(editor); + + // Handle case when user enters on blank line, just trigger interactive.execute + if (editor && editor.document.lineAt(editor.selection.active.line).text === '') { + await commands.executeCommand(commandName); + } + } + } +} diff --git a/src/client/repl/replController.ts b/src/client/repl/replController.ts new file mode 100644 index 000000000000..f30b8d9cbf6f --- /dev/null +++ b/src/client/repl/replController.ts @@ -0,0 +1,40 @@ +import * as vscode from 'vscode'; +import { createPythonServer } from './pythonServer'; + +export function createReplController( + interpreterPath: string, + disposables: vscode.Disposable[], + cwd?: string, +): vscode.NotebookController { + const server = createPythonServer([interpreterPath], cwd); + disposables.push(server); + + const controller = vscode.notebooks.createNotebookController('pythonREPL', 'jupyter-notebook', 'Python REPL'); + controller.supportedLanguages = ['python']; + + controller.description = 'Python REPL'; + + controller.interruptHandler = async () => { + server.interrupt(); + }; + + controller.executeHandler = async (cells) => { + for (const cell of cells) { + const exec = controller.createNotebookCellExecution(cell); + exec.start(Date.now()); + + const result = await server.execute(cell.document.getText()); + + if (result?.output) { + exec.replaceOutput([ + new vscode.NotebookCellOutput([vscode.NotebookCellOutputItem.text(result.output, 'text/plain')]), + ]); + // TODO: Properly update via NotebookCellOutputItem.error later. + } + + exec.end(result?.status); + } + }; + disposables.push(controller); + return controller; +} diff --git a/src/client/repl/replUtils.ts b/src/client/repl/replUtils.ts new file mode 100644 index 000000000000..93ae6f2a4573 --- /dev/null +++ b/src/client/repl/replUtils.ts @@ -0,0 +1,135 @@ +import { NotebookDocument, TextEditor, Selection, Uri, commands, window, TabInputNotebook, ViewColumn } from 'vscode'; +import { Commands } from '../common/constants'; +import { noop } from '../common/utils/misc'; +import { getActiveResource } from '../common/vscodeApis/windowApis'; +import { getConfiguration } from '../common/vscodeApis/workspaceApis'; +import { IInterpreterService } from '../interpreter/contracts'; +import { PythonEnvironment } from '../pythonEnvironments/info'; +import { getMultiLineSelectionText, getSingleLineSelectionText } from '../terminals/codeExecution/helper'; + +/** + * Function that executes selected code in the terminal. + */ +export async function executeInTerminal(): Promise { + await commands.executeCommand(Commands.Exec_Selection_In_Terminal); +} + +/** + * Function that returns selected text to execute in the REPL. + * @param textEditor + * @returns code - Code to execute in the REPL. + */ +export async function getSelectedTextToExecute(textEditor: TextEditor): Promise { + const { selection } = textEditor; + let code: string; + + if (selection.isEmpty) { + code = textEditor.document.lineAt(selection.start.line).text; + } else if (selection.isSingleLine) { + code = getSingleLineSelectionText(textEditor); + } else { + code = getMultiLineSelectionText(textEditor); + } + + return code; +} + +/** + * Function that returns user's Native REPL setting. + * @returns boolean - True if sendToNativeREPL setting is enabled, False otherwise. + */ +export function getSendToNativeREPLSetting(): boolean { + const uri = getActiveResource(); + const configuration = getConfiguration('python', uri); + return configuration.get('REPL.sendToNativeREPL', false); +} + +// Function that inserts new line in the given (input) text editor +export function insertNewLineToREPLInput(activeEditor: TextEditor | undefined): void { + if (activeEditor) { + const position = activeEditor.selection.active; + const newPosition = position.with(position.line, activeEditor.document.lineAt(position.line).text.length); + activeEditor.selection = new Selection(newPosition, newPosition); + + activeEditor.edit((editBuilder) => { + editBuilder.insert(newPosition, '\n'); + }); + } +} + +export function isMultiLineText(textEditor: TextEditor): boolean { + return (textEditor?.document?.lineCount ?? 0) > 1; +} + +/** + * Function that trigger interpreter warning if invalid interpreter. + * Function will also return undefined or active interpreter + */ +export async function getActiveInterpreter( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + const resource = uri ?? getActiveResource(); + const interpreter = await interpreterService.getActiveInterpreter(resource); + if (!interpreter) { + commands.executeCommand(Commands.TriggerEnvironmentSelection, resource).then(noop, noop); + return undefined; + } + return interpreter; +} + +/** + * Function that will return ViewColumn for existing Native REPL that belongs to given NotebookDocument. + */ +export function getExistingReplViewColumn(notebookDocument: NotebookDocument): ViewColumn | undefined { + const ourNotebookUri = notebookDocument.uri.toString(); + // Use Tab groups, to locate previously opened Python REPL tab and fetch view column. + const ourTb = window.tabGroups; + for (const tabGroup of ourTb.all) { + for (const tab of tabGroup.tabs) { + if (tab.label === 'Python REPL') { + const tabInput = (tab.input as unknown) as TabInputNotebook; + const tabUri = tabInput.uri.toString(); + if (tab.input && tabUri === ourNotebookUri) { + // This is the tab we are looking for. + const existingReplViewColumn = tab.group.viewColumn; + return existingReplViewColumn; + } + } + } + } + return undefined; +} + +/** + * Function that will return tab name for before reloading VS Code + * This is so we can make sure tab name is still 'Python REPL' after reloading VS Code, + * and make sure Python REPL does not get 'merged' into unaware untitled.ipynb tab. + */ +export function getTabNameForUri(uri: Uri): string | undefined { + const tabGroups = window.tabGroups.all; + + for (const tabGroup of tabGroups) { + for (const tab of tabGroup.tabs) { + if (tab.input instanceof TabInputNotebook && tab.input.uri.toString() === uri.toString()) { + return tab.label; + } + } + } + + return undefined; +} + +/** + * Function that will return the minor version of current active Python interpreter. + */ +export async function getPythonMinorVersion( + uri: Uri | undefined, + interpreterService: IInterpreterService, +): Promise { + if (uri) { + const pythonVersion = await getActiveInterpreter(uri, interpreterService); + return pythonVersion?.version?.minor; + } + return undefined; +} diff --git a/src/client/repl/types.ts b/src/client/repl/types.ts new file mode 100644 index 000000000000..38de9bfe2137 --- /dev/null +++ b/src/client/repl/types.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +export enum ReplType { + terminal = 'terminal', + native = 'native', +} diff --git a/src/client/repl/variables/types.ts b/src/client/repl/variables/types.ts new file mode 100644 index 000000000000..1e3c80d32077 --- /dev/null +++ b/src/client/repl/variables/types.ts @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { CancellationToken, Variable } from 'vscode'; + +export interface IVariableDescription extends Variable { + /** The name of the variable at the root scope */ + root: string; + /** How to look up the specific property of the root variable */ + propertyChain: (string | number)[]; + /** The number of children for collection types */ + count?: number; + /** Names of children */ + hasNamedChildren?: boolean; + /** A method to get the children of this variable */ + getChildren?: (start: number, token: CancellationToken) => Promise; +} diff --git a/src/client/repl/variables/variableRequester.ts b/src/client/repl/variables/variableRequester.ts new file mode 100644 index 000000000000..e66afdcd6616 --- /dev/null +++ b/src/client/repl/variables/variableRequester.ts @@ -0,0 +1,59 @@ +import { CancellationToken } from 'vscode'; +import path from 'path'; +import * as fsapi from '../../common/platform/fs-paths'; +import { IVariableDescription } from './types'; +import { PythonServer } from '../pythonServer'; +import { EXTENSION_ROOT_DIR } from '../../constants'; + +const VARIABLE_SCRIPT_LOCATION = path.join(EXTENSION_ROOT_DIR, 'python_files', 'get_variable_info.py'); + +export class VariableRequester { + public static scriptContents: string | undefined; + + constructor(private pythonServer: PythonServer) {} + + async getAllVariableDescriptions( + parent: IVariableDescription | undefined, + start: number, + token: CancellationToken, + ): Promise { + const scriptLines = (await getContentsOfVariablesScript()).split(/(?:\r\n|\n)/); + if (parent) { + const printCall = `import json;return json.dumps(getAllChildrenDescriptions(\'${ + parent.root + }\', ${JSON.stringify(parent.propertyChain)}, ${start}))`; + scriptLines.push(printCall); + } else { + scriptLines.push('import json;return json.dumps(getVariableDescriptions())'); + } + + if (token.isCancellationRequested) { + return []; + } + + const script = wrapScriptInFunction(scriptLines); + const result = await this.pythonServer.executeSilently(script); + + if (result?.output && !token.isCancellationRequested) { + return JSON.parse(result.output) as IVariableDescription[]; + } + + return []; + } +} + +function wrapScriptInFunction(scriptLines: string[]): string { + const indented = scriptLines.map((line) => ` ${line}`).join('\n'); + // put everything into a function scope and then delete that scope + // TODO: run in a background thread + return `def __VSCODE_run_script():\n${indented}\nprint(__VSCODE_run_script())\ndel __VSCODE_run_script`; +} + +async function getContentsOfVariablesScript(): Promise { + if (VariableRequester.scriptContents) { + return VariableRequester.scriptContents; + } + const contents = await fsapi.readFile(VARIABLE_SCRIPT_LOCATION, 'utf-8'); + VariableRequester.scriptContents = contents; + return VariableRequester.scriptContents; +} diff --git a/src/client/repl/variables/variableResultCache.ts b/src/client/repl/variables/variableResultCache.ts new file mode 100644 index 000000000000..1e19415becb7 --- /dev/null +++ b/src/client/repl/variables/variableResultCache.ts @@ -0,0 +1,28 @@ +import { VariablesResult } from 'vscode'; + +export class VariableResultCache { + private cache = new Map(); + + private executionCount = 0; + + getResults(executionCount: number, cacheKey: string): VariablesResult[] | undefined { + if (this.executionCount !== executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } + + return this.cache.get(cacheKey); + } + + setResults(executionCount: number, cacheKey: string, results: VariablesResult[]): void { + if (this.executionCount < executionCount) { + this.cache.clear(); + this.executionCount = executionCount; + } else if (this.executionCount > executionCount) { + // old results, don't cache + return; + } + + this.cache.set(cacheKey, results); + } +} diff --git a/src/client/repl/variables/variablesProvider.ts b/src/client/repl/variables/variablesProvider.ts new file mode 100644 index 000000000000..f033451dc80e --- /dev/null +++ b/src/client/repl/variables/variablesProvider.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + CancellationToken, + NotebookDocument, + Variable, + NotebookVariablesRequestKind, + VariablesResult, + EventEmitter, + Event, + NotebookVariableProvider, + Uri, +} from 'vscode'; +import { VariableResultCache } from './variableResultCache'; +import { IVariableDescription } from './types'; +import { VariableRequester } from './variableRequester'; +import { getConfiguration } from '../../common/vscodeApis/workspaceApis'; + +export class VariablesProvider implements NotebookVariableProvider { + private readonly variableResultCache = new VariableResultCache(); + + private _onDidChangeVariables = new EventEmitter(); + + onDidChangeVariables = this._onDidChangeVariables.event; + + private executionCount = 0; + + constructor( + private readonly variableRequester: VariableRequester, + private readonly getNotebookDocument: () => NotebookDocument | undefined, + codeExecutedEvent: Event, + ) { + codeExecutedEvent(() => this.onDidExecuteCode()); + } + + onDidExecuteCode(): void { + const notebook = this.getNotebookDocument(); + if (notebook) { + this.executionCount += 1; + if (isEnabled(notebook.uri)) { + this._onDidChangeVariables.fire(notebook); + } + } + } + + async *provideVariables( + notebook: NotebookDocument, + parent: Variable | undefined, + kind: NotebookVariablesRequestKind, + start: number, + token: CancellationToken, + ): AsyncIterable { + const notebookDocument = this.getNotebookDocument(); + if ( + !isEnabled(notebook.uri) || + token.isCancellationRequested || + !notebookDocument || + notebookDocument !== notebook + ) { + return; + } + + const { executionCount } = this; + const cacheKey = getVariableResultCacheKey(notebook.uri.toString(), parent, start); + let results = this.variableResultCache.getResults(executionCount, cacheKey); + + if (parent) { + const parentDescription = parent as IVariableDescription; + if (!results && parentDescription.getChildren) { + const variables = await parentDescription.getChildren(start, token); + if (token.isCancellationRequested) { + return; + } + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } else if (!results) { + // no cached results and no way to get children, so return empty + return; + } + + for (const result of results) { + yield result; + } + + // check if we have more indexed children to return + if ( + kind === 2 && + parentDescription.count && + results.length > 0 && + parentDescription.count > start + results.length + ) { + for await (const result of this.provideVariables( + notebook, + parent, + kind, + start + results.length, + token, + )) { + yield result; + } + } + } else { + if (!results) { + const variables = await this.variableRequester.getAllVariableDescriptions(undefined, start, token); + if (token.isCancellationRequested) { + return; + } + results = variables.map((variable) => this.createVariableResult(variable)); + this.variableResultCache.setResults(executionCount, cacheKey, results); + } + + for (const result of results) { + yield result; + } + } + } + + private createVariableResult(result: IVariableDescription): VariablesResult { + const indexedChildrenCount = result.count ?? 0; + const hasNamedChildren = !!result.hasNamedChildren; + const variable = { + getChildren: (start: number, token: CancellationToken) => this.getChildren(variable, start, token), + expression: createExpression(result.root, result.propertyChain), + ...result, + } as Variable; + return { variable, hasNamedChildren, indexedChildrenCount }; + } + + async getChildren(variable: Variable, start: number, token: CancellationToken): Promise { + const parent = variable as IVariableDescription; + return this.variableRequester.getAllVariableDescriptions(parent, start, token); + } +} + +function createExpression(root: string, propertyChain: (string | number)[]): string { + let expression = root; + for (const property of propertyChain) { + if (typeof property === 'string') { + expression += `.${property}`; + } else { + expression += `[${property}]`; + } + } + return expression; +} + +function getVariableResultCacheKey(uri: string, parent: Variable | undefined, start: number) { + let parentKey = ''; + const parentDescription = parent as IVariableDescription; + if (parentDescription) { + parentKey = `${parentDescription.name}.${parentDescription.propertyChain.join('.')}[[${start}`; + } + return `${uri}:${parentKey}`; +} + +function isEnabled(resource?: Uri) { + return getConfiguration('python', resource).get('REPL.provideVariables'); +} diff --git a/src/client/sourceMapSupport.ts b/src/client/sourceMapSupport.ts deleted file mode 100644 index 8650ed59c50b..000000000000 --- a/src/client/sourceMapSupport.ts +++ /dev/null @@ -1,76 +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 { promisify } from 'util'; -import { WorkspaceConfiguration } from 'vscode'; -import './common/extensions'; -import { EXTENSION_ROOT_DIR } from './constants'; - -type VSCode = typeof import('vscode'); - -// tslint:disable:no-require-imports -const setting = 'sourceMapsEnabled'; - -export class SourceMapSupport { - private readonly config: WorkspaceConfiguration; - constructor(private readonly vscode: VSCode) { - this.config = this.vscode.workspace.getConfiguration('python.diagnostics', null); - } - public async initialize(): Promise { - if (!this.enabled) { - return; - } - await this.enableSourceMaps(true); - const localize = require('./common/utils/localize') as typeof import('./common/utils/localize'); - const disable = localize.Diagnostics.disableSourceMaps(); - this.vscode.window.showWarningMessage(localize.Diagnostics.warnSourceMaps(), disable).then(selection => { - if (selection === disable) { - this.disable().ignoreErrors(); - } - }); - } - public get enabled(): boolean { - return this.config.get(setting, false); - } - public async disable(): Promise { - if (this.enabled) { - await this.config.update(setting, false, this.vscode.ConfigurationTarget.Global); - } - await this.enableSourceMaps(false); - } - protected async enableSourceMaps(enable: boolean) { - const extensionSourceFile = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'extension.js'); - const debuggerSourceFile = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'debugAdapter', 'main.js'); - await Promise.all([this.enableSourceMap(enable, extensionSourceFile), this.enableSourceMap(enable, debuggerSourceFile)]); - } - protected async enableSourceMap(enable: boolean, sourceFile: string) { - const sourceMapFile = `${sourceFile}.map`; - const disabledSourceMapFile = `${sourceFile}.map.disabled`; - if (enable) { - await this.rename(disabledSourceMapFile, sourceMapFile); - } else { - await this.rename(sourceMapFile, disabledSourceMapFile); - } - } - protected async rename(sourceFile: string, targetFile: string) { - const fsExists = promisify(fs.exists); - const fsRename = promisify(fs.rename); - if (await fsExists(targetFile)) { - return; - } - await fsRename(sourceFile, targetFile); - } -} -export function initialize(vscode: VSCode = require('vscode')) { - if (!vscode.workspace.getConfiguration('python.diagnostics', null).get('sourceMapsEnabled', false)) { - new SourceMapSupport(vscode).disable().ignoreErrors(); - return; - } - new SourceMapSupport(vscode).initialize().catch(_ex => { - 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, + 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); + 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); + 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 { + // 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); + const workspaceFolderCount = workspaceService.workspaceFolders?.length || 0; + const terminalHelper = serviceContainer.get(ITerminalHelper); + const terminalShellType = terminalHelper.identifyTerminalShell(); + if (!workspaceService.isTrusted) { + return { workspaceFolderCount, terminal: terminalShellType, isFirstSession }; + } + const interpreterService = serviceContainer.get(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(() => undefined); + } else { + interpreter = await interpreterService + .getActiveInterpreter() + .catch(() => undefined); + } + + const pythonVersion = interpreter && interpreter.version ? interpreter.version.raw : undefined; + const interpreterType = interpreter ? interpreter.envType : undefined; + if (interpreterType === EnvironmentType.Unknown) { + traceError('Active interpreter type is detected as Unknown', JSON.stringify(interpreter)); + } + let condaVersion = undefined; + if (interpreterType === EnvironmentType.Conda) { + const condaLocator = serviceContainer.get(ICondaService); + condaVersion = await condaLocator + .getCondaVersion() + .then((ver) => (ver ? ver.raw : '')) + .catch(() => ''); + } + 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 f33536ced805..eff32a6e3299 100644 --- a/src/client/telemetry/constants.ts +++ b/src/client/telemetry/constants.ts @@ -4,83 +4,104 @@ 'use strict'; export enum EventName { - COMPLETION = 'COMPLETION', - COMPLETION_ADD_BRACKETS = 'COMPLETION.ADD_BRACKETS', - DEFINITION = 'DEFINITION', - HOVER_DEFINITION = 'HOVER_DEFINITION', - REFERENCE = 'REFERENCE', - SIGNATURE = 'SIGNATURE', - SYMBOL = 'SYMBOL', - FORMAT_SORT_IMPORTS = 'FORMAT.SORT_IMPORTS', - FORMAT = 'FORMAT.FORMAT', FORMAT_ON_TYPE = 'FORMAT.FORMAT_ON_TYPE', EDITOR_LOAD = 'EDITOR.LOAD', - LINTING = 'LINTING', - GO_TO_OBJECT_DEFINITION = 'GO_TO_OBJECT_DEFINITION', - UPDATE_PYSPARK_LIBRARY = 'UPDATE_PYSPARK_LIBRARY', - REFACTOR_RENAME = 'REFACTOR_RENAME', - REFACTOR_EXTRACT_VAR = 'REFACTOR_EXTRACT_VAR', - REFACTOR_EXTRACT_FUNCTION = 'REFACTOR_EXTRACT_FUNCTION', REPL = 'REPL', + INVOKE_TOOL = 'INVOKE_TOOL', + CREATE_NEW_FILE_COMMAND = 'CREATE_NEW_FILE_COMMAND', + SELECT_INTERPRETER = 'SELECT_INTERPRETER', + SELECT_INTERPRETER_ENTER_BUTTON = 'SELECT_INTERPRETER_ENTER_BUTTON', + SELECT_INTERPRETER_ENTER_CHOICE = 'SELECT_INTERPRETER_ENTER_CHOICE', + SELECT_INTERPRETER_SELECTED = 'SELECT_INTERPRETER_SELECTED', + SELECT_INTERPRETER_ENTER_OR_FIND = 'SELECT_INTERPRETER_ENTER_OR_FIND', + SELECT_INTERPRETER_ENTERED_EXISTS = 'SELECT_INTERPRETER_ENTERED_EXISTS', PYTHON_INTERPRETER = 'PYTHON_INTERPRETER', PYTHON_INSTALL_PACKAGE = 'PYTHON_INSTALL_PACKAGE', + ENVIRONMENT_WITHOUT_PYTHON_SELECTED = 'ENVIRONMENT_WITHOUT_PYTHON_SELECTED', + PYTHON_ENVIRONMENTS_API = 'PYTHON_ENVIRONMENTS_API', PYTHON_INTERPRETER_DISCOVERY = 'PYTHON_INTERPRETER_DISCOVERY', + NATIVE_FINDER_MISSING_CONDA_ENVS = 'NATIVE_FINDER_MISSING_CONDA_ENVS', + NATIVE_FINDER_MISSING_POETRY_ENVS = 'NATIVE_FINDER_MISSING_POETRY_ENVS', + NATIVE_FINDER_PERF = 'NATIVE_FINDER_PERF', + PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE = 'PYTHON_INTERPRETER_DISCOVERY_INVALID_NATIVE', PYTHON_INTERPRETER_AUTO_SELECTION = 'PYTHON_INTERPRETER_AUTO_SELECTION', - PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES', + PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES = 'PYTHON_INTERPRETER.ACTIVATION_ENVIRONMENT_VARIABLES', PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE = 'PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE', PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL = 'PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL', 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', - WORKSPACE_SYMBOLS_BUILD = 'WORKSPACE_SYMBOLS.BUILD', - WORKSPACE_SYMBOLS_GO_TO = 'WORKSPACE_SYMBOLS.GO_TO', + ENVFILE_WORKSPACE = 'ENVFILE_WORKSPACE', EXECUTION_CODE = 'EXECUTION_CODE', EXECUTION_DJANGO = 'EXECUTION_DJANGO', - DEBUGGER = 'DEBUGGER', - DEBUGGER_ATTACH_TO_CHILD_PROCESS = 'DEBUGGER.ATTACH_TO_CHILD_PROCESS', - DEBUGGER_CONFIGURATION_PROMPTS = 'DEBUGGER.CONFIGURATION.PROMPTS', - DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON = 'DEBUGGER.CONFIGURATION.PROMPTS.IN.LAUNCH.JSON', - UNITTEST_STOP = 'UNITTEST.STOP', - UNITTEST_DISABLE = 'UNITTEST.DISABLE', - UNITTEST_RUN = 'UNITTEST.RUN', - UNITTEST_DISCOVER = 'UNITTEST.DISCOVER', - UNITTEST_DISCOVER_WITH_PYCODE = 'UNITTEST.DISCOVER.WITH.PYTHONCODE', - UNITTEST_CONFIGURE = 'UNITTEST.CONFIGURE', + + // Python testing specific telemetry UNITTEST_CONFIGURING = 'UNITTEST.CONFIGURING', - UNITTEST_VIEW_OUTPUT = 'UNITTEST.VIEW_OUTPUT', - UNITTEST_NAVIGATE_TEST_FILE = 'UNITTEST.NAVIGATE.TEST_FILE', - UNITTEST_NAVIGATE_TEST_FUNCTION = 'UNITTEST.NAVIGATE.TEST_FUNCTION', - UNITTEST_NAVIGATE_TEST_SUITE = 'UNITTEST.NAVIGATE.TEST_SUITE', - UNITTEST_EXPLORER_WORK_SPACE_COUNT = 'UNITTEST.TEST_EXPLORER.WORK_SPACE_COUNT', - PYTHON_LANGUAGE_SERVER_SWITCHED = 'PYTHON_LANGUAGE_SERVER.SWITCHED', - PYTHON_LANGUAGE_SERVER_ANALYSISTIME = 'PYTHON_LANGUAGE_SERVER.ANALYSIS_TIME', - PYTHON_LANGUAGE_SERVER_ENABLED = 'PYTHON_LANGUAGE_SERVER.ENABLED', - PYTHON_LANGUAGE_SERVER_EXTRACTED = 'PYTHON_LANGUAGE_SERVER.EXTRACTED', - PYTHON_LANGUAGE_SERVER_DOWNLOADED = 'PYTHON_LANGUAGE_SERVER.DOWNLOADED', - PYTHON_LANGUAGE_SERVER_ERROR = 'PYTHON_LANGUAGE_SERVER.ERROR', - PYTHON_LANGUAGE_SERVER_STARTUP = 'PYTHON_LANGUAGE_SERVER.STARTUP', - PYTHON_LANGUAGE_SERVER_READY = 'PYTHON_LANGUAGE_SERVER.READY', - PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED = 'PYTHON_LANGUAGE_SERVER.PLATFORM_NOT_SUPPORTED', - PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED = 'PYTHON_LANGUAGE_SERVER.PLATFORM_SUPPORTED', - PYTHON_LANGUAGE_SERVER_TELEMETRY = 'PYTHON_LANGUAGE_SERVER.EVENT', - PYTHON_EXPERIMENTS = 'PYTHON_EXPERIMENTS', + 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', - PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES = 'PYTHON_LANGUAGE_SERVER.LIST_BLOB_PACKAGES', + ACTIVATE_ENV_IN_CURRENT_TERMINAL = 'ACTIVATE_ENV_IN_CURRENT_TERMINAL', + ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED = 'ACTIVATE_ENV_TO_GET_ENV_VARS_FAILED', DIAGNOSTICS_ACTION = 'DIAGNOSTICS.ACTION', DIAGNOSTICS_MESSAGE = 'DIAGNOSTICS.MESSAGE', - PLATFORM_INFO = 'PLATFORM.INFO', - SELECT_LINTER = 'LINTING.SELECT', + USE_REPORT_ISSUE_COMMAND = 'USE_REPORT_ISSUE_COMMAND', - LINTER_NOT_INSTALLED_PROMPT = 'LINTER_NOT_INSTALLED_PROMPT', - CONFIGURE_AVAILABLE_LINTER_PROMPT = 'CONFIGURE_AVAILABLE_LINTER_PROMPT', HASHED_PACKAGE_NAME = 'HASHED_PACKAGE_NAME', - JEDI_MEMORY = 'JEDI_MEMORY' + JEDI_LANGUAGE_SERVER_ENABLED = 'JEDI_LANGUAGE_SERVER.ENABLED', + JEDI_LANGUAGE_SERVER_STARTUP = 'JEDI_LANGUAGE_SERVER.STARTUP', + JEDI_LANGUAGE_SERVER_READY = 'JEDI_LANGUAGE_SERVER.READY', + JEDI_LANGUAGE_SERVER_REQUEST = 'JEDI_LANGUAGE_SERVER.REQUEST', + + TENSORBOARD_INSTALL_PROMPT_SHOWN = 'TENSORBOARD.INSTALL_PROMPT_SHOWN', + TENSORBOARD_INSTALL_PROMPT_SELECTION = 'TENSORBOARD.INSTALL_PROMPT_SELECTION', + TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL = 'TENSORBOARD_DETECTED_IN_INTEGRATED_TERMINAL', + TENSORBOARD_PACKAGE_INSTALL_RESULT = 'TENSORBOARD.PACKAGE_INSTALL_RESULT', + TENSORBOARD_TORCH_PROFILER_IMPORT = 'TENSORBOARD.TORCH_PROFILER_IMPORT', + + ENVIRONMENT_CREATING = 'ENVIRONMENT.CREATING', + ENVIRONMENT_CREATED = 'ENVIRONMENT.CREATED', + ENVIRONMENT_FAILED = 'ENVIRONMENT.FAILED', + ENVIRONMENT_INSTALLING_PACKAGES = 'ENVIRONMENT.INSTALLING_PACKAGES', + ENVIRONMENT_INSTALLED_PACKAGES = 'ENVIRONMENT.INSTALLED_PACKAGES', + ENVIRONMENT_INSTALLING_PACKAGES_FAILED = 'ENVIRONMENT.INSTALLING_PACKAGES_FAILED', + ENVIRONMENT_BUTTON = 'ENVIRONMENT.BUTTON', + ENVIRONMENT_DELETE = 'ENVIRONMENT.DELETE', + ENVIRONMENT_REUSE = 'ENVIRONMENT.REUSE', + + ENVIRONMENT_CHECK_TRIGGER = 'ENVIRONMENT.CHECK.TRIGGER', + ENVIRONMENT_CHECK_RESULT = 'ENVIRONMENT.CHECK.RESULT', + ENVIRONMENT_TERMINAL_GLOBAL_PIP = 'ENVIRONMENT.TERMINAL.GLOBAL_PIP', } export enum PlatformErrors { FailedToParseVersion = 'FailedToParseVersion', - FailedToDetermineOS = 'FailedToDetermineOS' + FailedToDetermineOS = 'FailedToDetermineOS', } diff --git a/src/client/telemetry/envFileTelemetry.ts b/src/client/telemetry/envFileTelemetry.ts 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 { + 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('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 { + // Look for PythonCodingPack file under `%USERPROFILE%/.vscode/extensions` + // folder. If that file exists treat this extension as installed from coding + // pack. + // + // Use parent of EXTENSION_ROOT_DIR to access %USERPROFILE%/.vscode/extensions + // this is because the installer will add PythonCodingPack to %USERPROFILE%/.vscode/extensions + // or %USERPROFILE%/.vscode-insiders/extensions depending on what was installed + // previously by the user. If we always join (, .vscode, extensions), we will + // end up looking at the wrong place, with respect to the extension that was launched. + const fileToCheck = path.join(path.dirname(EXTENSION_ROOT_DIR), 'PythonCodingPack'); + if (await fs.fileExists(fileToCheck)) { + setSharedProperty('installSource', 'pythonCodingPack'); + } else { + // We did not file the `PythonCodingPack` file, assume market place install. + setSharedProperty('installSource', 'marketPlace'); + } +} diff --git a/src/client/telemetry/importTracker.ts b/src/client/telemetry/importTracker.ts index e0fef006604e..cf8e1ed48837 100644 --- a/src/client/telemetry/importTracker.ts +++ b/src/client/telemetry/importTracker.ts @@ -1,145 +1,165 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import '../common/extensions'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { TextDocument } from 'vscode'; - -import { sendTelemetryEvent } from '.'; -import { noop } from '../common/utils/misc'; -import { IDocumentManager } from '../common/application/types'; -import { isTestExecution } from '../common/constants'; -import { EventName } from './constants'; -import { IImportTracker } from './types'; - -/* -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. This leads to us supporting: - -- `from pkg import _` -- `from pkg import _, _` -- `from pkg import _ as _` -- `import pkg` -- `import pkg, pkg` -- `import pkg as _` - -We can rely on the fact that the use of the `from` and `import` keywords from the start of a line are -only usable for imports in valid code (`from` can also be used when raising an exception, but `raise` -would be the first keyword on a line in that instance). We also get to rely on the fact that we only -care about the top-level package, keeping the regex extremely greedy. This should lead to the regex -failing fast and having low performance overhead. - -We can also ignore multi-line/parenthesized imports for simplicity since we don't' need 100% accuracy, -just enough to be able to tell what packages user's rely on to make sure we are covering our bases -in terms of support. This allows us to anchor the start and end of the regexp and not try to handle the -parentheses case which adds a lot more optional parts to the regexp. -*/ -const ImportRegEx = /^\s*(from\s+(?\w+)(?:\.\w+)*\s+import\s+\w+(?:\s+as\s+\w+|(?:\s*,\s*\w+)+(?:\s*,)?)?|import\s+(?(?:\w+(?:\s*,\s*)?)+)(?:\s+as\s+\w+)?)\s*(#.*)?$/; -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 IImportTracker { - private pendingDocs = new Map(); - private sentMatches: Set = new Set(); - // tslint:disable-next-line:no-require-imports - private hashFn = require('hash.js').sha256; - - constructor(@inject(IDocumentManager) private documentManager: IDocumentManager) { - this.documentManager.onDidOpenTextDocument(t => this.onOpenedOrSavedDocument(t)); - this.documentManager.onDidSaveTextDocument(t => this.onOpenedOrSavedDocument(t)); - } - - public async activate(): Promise { - // Act like all of our open documents just opened; our timeout will make sure this is delayed. - this.documentManager.textDocuments.forEach(d => this.onOpenedOrSavedDocument(d)); - } - - private getDocumentLines(document: TextDocument): (string | undefined)[] { - const array = Array(Math.min(document.lineCount, MAX_DOCUMENT_LINES)).fill(''); - return array - .map((_a: string, i: number) => { - const line = document.lineAt(i); - if (line && !line.isEmptyOrWhitespace) { - return line.text; - } - return undefined; - }) - .filter((f: string | undefined) => f); - } - - private onOpenedOrSavedDocument(document: TextDocument) { - // Make sure this is a Python file. - if (path.extname(document.fileName) === '.py') { - this.scheduleDocument(document); - } - } - - private scheduleDocument(document: TextDocument) { - // If already scheduled, cancel. - const currentTimeout = this.pendingDocs.get(document.fileName); - if (currentTimeout) { - // tslint:disable-next-line: no-any - clearTimeout(currentTimeout as any); - this.pendingDocs.delete(document.fileName); - } - - // Now schedule a new one. - if (testExecution) { - // During a test, check right away. It needs to be synchronous. - this.checkDocument(document); - } else { - // Wait five seconds to make sure we don't already have this document pending. - this.pendingDocs.set(document.fileName, setTimeout(() => this.checkDocument(document), 5000)); - } - } - - private checkDocument(document: TextDocument) { - this.pendingDocs.delete(document.fileName); - const lines = this.getDocumentLines(document); - this.lookForImports(lines); - } - - private sendTelemetry(packageName: string) { - // No need to send duplicate telemetry or waste CPU cycles on an unneeded hash. - if (this.sentMatches.has(packageName)) { - return; - } - this.sentMatches.add(packageName); - // Hash the package name so that we will never accidentally see a - // user's private package name. - const hash = this.hashFn() - .update(packageName) - .digest('hex'); - sendTelemetryEvent(EventName.HASHED_PACKAGE_NAME, undefined, { hashedName: hash }); - } - - private lookForImports(lines: (string | undefined)[]) { - try { - for (const s of lines) { - const match = s ? ImportRegEx.exec(s) : null; - if (match !== null && match.groups !== undefined) { - if (match.groups.fromImport !== undefined) { - // `from pkg ...` - this.sendTelemetry(match.groups.fromImport); - } else if (match.groups.importImport !== undefined) { - // `import pkg1, pkg2, ...` - const packageNames = match.groups.importImport.split(',').map(rawPackageName => rawPackageName.trim()); - // Can't pass in `this.sendTelemetry` directly as that rebinds `this`. - packageNames.forEach(p => this.sendTelemetry(p)); - } - } - } - } catch { - // Don't care about failures since this is just telemetry. - noop(); - } - } -} +/* 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 (?\w+)(?:\.\w+)* import \w+(?:, \w+)*(?: as \w+)?|import (?\w+(?:, \w+)*)(?: as \w+)?)$/; +const MAX_DOCUMENT_LINES = 1000; + +// Capture isTestExecution on module load so that a test can turn it off and still +// have this value set. +const testExecution = isTestExecution(); + +@injectable() +export class ImportTracker implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: true }; + + private pendingChecks = new Map(); + + private static sentMatches: Set = new Set(); + + 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 { + // 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(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 e363b733d409..763f7405aa0d 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -1,182 +1,251 @@ +/* eslint-disable global-require */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-reference no-any import-name no-any function-name -/// -import { JSONObject } from '@phosphor/coreutils'; -import { basename as pathBasename, sep as pathSep } from 'path'; -import * as stackTrace from 'stack-trace'; -import TelemetryReporter from 'vscode-extension-telemetry'; - -import { IWorkspaceService } from '../common/application/types'; -import { EXTENSION_ROOT_DIR, isTestExecution, PVSC_EXTENSION_ID } from '../common/constants'; -import { traceInfo } from '../common/logger'; + +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 { Telemetry } from '../datascience/constants'; -import { LinterId } from '../linters/types'; +import { EnvironmentType, PythonEnvironment } from '../pythonEnvironments/info'; +import { TensorBoardPromptSelection } from '../tensorBoard/constants'; import { EventName } from './constants'; -import { - CodeExecutionTelemetry, - DebuggerConfigurationPromtpsTelemetry, - DebuggerTelemetry, - DiagnosticsAction, - DiagnosticsMessages, - EditorLoadTelemetry, - FormatTelemetry, - InterpreterActivation, - InterpreterActivationEnvironmentVariables, - InterpreterAutoSelection, - InterpreterDiscovery, - LanguageServePlatformSupported, - LanguageServerErrorTelemetry, - LanguageServerVersionTelemetry, - LinterInstallPromptTelemetry, - LinterSelectionTelemetry, - LintingTelemetry, - Platform, - PythonInterpreterTelemetry, - TerminalTelemetry, - TestConfiguringTelemetry, - TestDiscoverytTelemetry, - TestRunTelemetry -} from './types'; +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; } } +// 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 = {}; +/** + * Set shared properties for all telemetry events. + */ +export function setSharedProperty

(name: E, value?: P[E]): void { + const propertyName = name as string; + // Ignore such shared telemetry during unit tests. + if (isUnitTestExecution() && propertyName.startsWith('ds_')) { + return; + } + if (value === undefined) { + delete sharedProperties[propertyName]; + } else { + sharedProperties[propertyName] = value; + } +} + /** - * Checks if the telemetry is disabled in user settings - * @returns {boolean} + * Reset shared properties for testing purposes. */ -export function isTelemetryDisabled(workspaceService: IWorkspaceService): boolean { - const settings = workspaceService.getConfiguration('telemetry').inspect('enableTelemetry')!; - return settings.globalValue === false ? true : false; +export function _resetSharedProperties(): void { + for (const key of Object.keys(sharedProperties)) { + delete sharedProperties[key]; + } } let telemetryReporter: TelemetryReporter | undefined; -function getTelemetryReporter() { +export function getTelemetryReporter(): TelemetryReporter { if (!isTestExecution() && telemetryReporter) { return telemetryReporter; } - const extensionId = PVSC_EXTENSION_ID; - // tslint:disable-next-line:no-require-imports - const extensions = (require('vscode') as typeof import('vscode')).extensions; - // 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() { +export function clearTelemetryReporter(): void { telemetryReporter = undefined; } export function sendTelemetryEvent

( eventName: E, - durationMs?: Record | number, + measuresOrDurationMs?: Record | number, properties?: P[E], - ex?: Error -) { - if (isTestExecution() || !isTelemetrySupported()) { + ex?: Error, +): void { + if (isTestExecution() || !isTelemetrySupported() || isTelemetryDisabled()) { return; } const reporter = getTelemetryReporter(); - const measures = typeof durationMs === 'number' ? { duration: durationMs } : durationMs ? durationMs : undefined; - - if (ex && (eventName as any) !== 'ERROR') { - // When sending `ERROR` telemetry event no need to send custom properties. - // Else we have to review all properties everytime as part of GDPR. - // Assume we have 10 events all with their own properties. - // As we have errors for each event, those properties are treated as new data items. - // Hence they need to be classified as part of the GDPR process, and thats unnecessary and onerous. - const props: Record = {}; - props.stackTrace = getStackTrace(ex); - props.originalEventName = eventName as any as string; - reporter.sendTelemetryEvent('ERROR', props, measures); - } + const measures = + typeof measuresOrDurationMs === 'number' + ? { duration: measuresOrDurationMs } + : measuresOrDurationMs || undefined; const customProperties: Record = {}; + const eventNameSent = eventName as 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 as any) as string, customProperties, 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) { - traceInfo(`Telemetry Event : ${eventName} Measures: ${JSON.stringify(measures)} Props: ${JSON.stringify(customProperties)} `); + 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

( +// Type-parameterized form of MethodDecorator in lib.es5.d.ts. +type TypedMethodDescriptor = ( + target: unknown, + propertyKey: string | symbol, + descriptor: TypedPropertyDescriptor, +) => TypedPropertyDescriptor | void; + +// The following code uses "any" in many places, as TS does not have rich support +// for typing decorators. Specifically, while it is possible to write types which +// encode the signature of the wrapped function, TS fails to actually infer the +// type of "this" and the signature at call sites, instead choosing to infer +// based on other hints (like the closure parameters), which ends up making it +// no safer than "any" (and sometimes misleading enough to be more unsafe). + +/** + * Decorates a method, sending a telemetry event with the given properties. + * @param eventName The event name to send. + * @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( eventName: E, properties?: P[E], - captureDuration: boolean = true, - failureEventName?: E -) { - // tslint:disable-next-line:no-function-expression no-any - return function (_target: Object, _propertyKey: string, descriptor: TypedPropertyDescriptor) { - const originalMethod = descriptor.value; - // tslint:disable-next-line:no-function-expression no-any - descriptor.value = function (...args: any[]) { - if (!captureDuration) { + 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, + // 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) - .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 - properties = properties || ({} as any); - (properties as any).failed = true; - sendTelemetryEvent( - failureEventName ? failureEventName : eventName, - stopWatch.elapsedTime, - properties, - ex - ); + .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; @@ -189,232 +258,1977 @@ export function captureTelemetry

(eventName: K, properties?: T[K]); export function sendTelemetryWhenDone

( eventName: E, - promise: Promise | Thenable, + promise: Promise | Thenable, stopWatch?: StopWatch, - properties?: P[E] -) { - stopWatch = stopWatch ? stopWatch : new StopWatch(); + properties?: P[E], +): void { + stopWatch = stopWatch || new StopWatch(); if (typeof promise.then === 'function') { - // tslint:disable-next-line:prefer-type-cast no-any - (promise as Promise).then( - data => { - // tslint:disable-next-line:no-non-null-assertion + (promise as Promise).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 + (ex) => { sendTelemetryEvent(eventName, stopWatch!.elapsedTime, properties, ex); return Promise.reject(ex); - } + }, ); } else { throw new Error('Method is neither a Promise nor a Theneable'); } } -function sanitizeFilename(filename: string): string { - if (filename.startsWith(EXTENSION_ROOT_DIR)) { - filename = `${filename.substring(EXTENSION_ROOT_DIR.length)}`; - } else { - // We don't really care about files outside our extension. - filename = `${pathSep}${pathBasename(filename)}`; - } - return filename; -} +/** + * 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'; -function sanitizeName(name: string): string { - if (name.indexOf('/') === -1 && name.indexOf('\\') === -1) { - return name; - } else { - return ''; - } + /** + * 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'; } -function getStackTrace(ex: Error): string { - // We aren't showing the error message (ex.message) since it might - // contain PII. - let trace = ''; - for (const frame of stackTrace.parse(ex)) { - let filename = frame.getFileName(); - if (filename) { - filename = sanitizeFilename(filename); - const lineno = frame.getLineNumber(); - const colno = frame.getColumnNumber(); - trace += `\n\tat ${getCallsite(frame)} ${filename}:${lineno}:${colno}`; - } else { - trace += '\n\tat '; - } - } - return trace.trim(); -} - -function getCallsite(frame: stackTrace.StackFrame) { - const parts: string[] = []; - if (typeof frame.getTypeName() === 'string' && frame.getTypeName().length > 0) { - parts.push(frame.getTypeName()); - } - if (typeof frame.getMethodName() === 'string' && frame.getMethodName().length > 0) { - parts.push(frame.getMethodName()); - } - if (typeof frame.getFunctionName() === 'string' && frame.getFunctionName().length > 0) { - if (parts.length !== 2 || parts.join('.') !== frame.getFunctionName()) { - parts.push(frame.getFunctionName()); - } - } - return parts.map(sanitizeName).join('.'); -} +type FailedEventType = { failed: true }; // Map all events to their properties export interface IEventNamePropertyMapping { - [EventName.COMPLETION]: never | undefined; - [EventName.COMPLETION_ADD_BRACKETS]: { enabled: boolean }; - [EventName.DEBUGGER]: DebuggerTelemetry; - [EventName.DEBUGGER_ATTACH_TO_CHILD_PROCESS]: never | undefined; - [EventName.DEBUGGER_CONFIGURATION_PROMPTS]: DebuggerConfigurationPromtpsTelemetry; - [EventName.DEBUGGER_CONFIGURATION_PROMPTS_IN_LAUNCH_JSON]: never | undefined; - [EventName.DEFINITION]: never | undefined; - [EventName.DIAGNOSTICS_ACTION]: DiagnosticsAction; - [EventName.DIAGNOSTICS_MESSAGE]: DiagnosticsMessages; - [EventName.EDITOR_LOAD]: EditorLoadTelemetry; + [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; - [EventName.EXECUTION_CODE]: CodeExecutionTelemetry; - [EventName.EXECUTION_DJANGO]: CodeExecutionTelemetry; - [EventName.FORMAT]: FormatTelemetry; - [EventName.FORMAT_ON_TYPE]: { enabled: boolean }; - [EventName.FORMAT_SORT_IMPORTS]: never | undefined; - [EventName.GO_TO_OBJECT_DEFINITION]: never | undefined; - [EventName.HOVER_DEFINITION]: never | undefined; - [EventName.HASHED_PACKAGE_NAME]: { hashedName: string }; - [EventName.LINTER_NOT_INSTALLED_PROMPT]: LinterInstallPromptTelemetry; - [EventName.PYTHON_INSTALL_PACKAGE]: { installer: string }; - [EventName.LINTING]: LintingTelemetry; - [EventName.PLATFORM_INFO]: Platform; - [EventName.PYTHON_INTERPRETER]: PythonInterpreterTelemetry; - [EventName.PYTHON_INTERPRETER_ACTIVATION_ENVIRONMENT_VARIABLES]: InterpreterActivationEnvironmentVariables; - [EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_RUNNING_CODE]: InterpreterActivation; - [EventName.PYTHON_INTERPRETER_ACTIVATION_FOR_TERMINAL]: InterpreterActivation; - [EventName.PYTHON_INTERPRETER_AUTO_SELECTION]: InterpreterAutoSelection; - [EventName.PYTHON_INTERPRETER_DISCOVERY]: InterpreterDiscovery; - [EventName.PYTHON_INTERPRETER_ACTIVATE_ENVIRONMENT_PROMPT]: { selection: 'Yes' | 'No' | 'Ignore' | undefined }; - [EventName.PYTHON_LANGUAGE_SERVER_SWITCHED]: { change: 'Switch to Jedi from LS' | 'Switch to LS from Jedi' }; - [EventName.PYTHON_LANGUAGE_SERVER_ANALYSISTIME]: { success: boolean }; - [EventName.PYTHON_LANGUAGE_SERVER_DOWNLOADED]: LanguageServerVersionTelemetry; - [EventName.PYTHON_LANGUAGE_SERVER_ENABLED]: never | undefined; - [EventName.PYTHON_LANGUAGE_SERVER_ERROR]: LanguageServerErrorTelemetry; - [EventName.PYTHON_LANGUAGE_SERVER_EXTRACTED]: LanguageServerVersionTelemetry; - [EventName.PYTHON_LANGUAGE_SERVER_LIST_BLOB_STORE_PACKAGES]: never | undefined; - [EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_NOT_SUPPORTED]: never | undefined; - [EventName.PYTHON_LANGUAGE_SERVER_PLATFORM_SUPPORTED]: LanguageServePlatformSupported; - [EventName.PYTHON_LANGUAGE_SERVER_READY]: never | undefined; - [EventName.PYTHON_LANGUAGE_SERVER_STARTUP]: never | undefined; - [EventName.PYTHON_LANGUAGE_SERVER_TELEMETRY]: any; - [EventName.PYTHON_EXPERIMENTS]: { error?: string; expName?: string }; - [EventName.REFACTOR_EXTRACT_FUNCTION]: never | undefined; - [EventName.REFACTOR_EXTRACT_VAR]: never | undefined; - [EventName.REFACTOR_RENAME]: never | undefined; - [EventName.REFERENCE]: never | undefined; - [EventName.REPL]: never | undefined; - [EventName.SELECT_LINTER]: LinterSelectionTelemetry; - [EventName.CONFIGURE_AVAILABLE_LINTER_PROMPT]: { tool: LinterId; action: 'enable' | 'ignore' | 'disablePrompt' | undefined }; - [EventName.SIGNATURE]: never | undefined; - [EventName.SYMBOL]: never | undefined; - [EventName.UNITTEST_CONFIGURE]: never | undefined; - [EventName.UNITTEST_CONFIGURING]: TestConfiguringTelemetry; - [EventName.TERMINAL_CREATE]: TerminalTelemetry; - [EventName.UNITTEST_DISCOVER]: TestDiscoverytTelemetry; - [EventName.UNITTEST_DISCOVER_WITH_PYCODE]: never | undefined; - [EventName.UNITTEST_RUN]: TestRunTelemetry; - [EventName.UNITTEST_STOP]: never | undefined; - [EventName.UNITTEST_DISABLE]: never | undefined; - [EventName.UNITTEST_VIEW_OUTPUT]: never | undefined; - [EventName.UPDATE_PYSPARK_LIBRARY]: never | undefined; - [EventName.WORKSPACE_SYMBOLS_BUILD]: never | undefined; - [EventName.WORKSPACE_SYMBOLS_GO_TO]: never | undefined; - // Data Science - [Telemetry.AddCellBelow]: never | undefined; - [Telemetry.ClassConstructionTime] : { class: string }; - [Telemetry.CodeLensAverageAcquisitionTime] : never | undefined; - [Telemetry.CollapseAll]: never | undefined; - [Telemetry.ConnectFailedJupyter]: never | undefined; - [Telemetry.ConnectLocalJupyter]: never | undefined; - [Telemetry.ConnectRemoteJupyter]: never | undefined; - [Telemetry.ConnectRemoteFailedJupyter]: never | undefined; - [Telemetry.ConnectRemoteSelfCertFailedJupyter]: never | undefined; - [Telemetry.CopySourceCode]: never | undefined; - [Telemetry.DataScienceSettings]: JSONObject; - [Telemetry.DataViewerFetchTime]: never | undefined; - [Telemetry.DebugCurrentCell]: never | undefined; - [Telemetry.DeleteAllCells]: never | undefined; - [Telemetry.DeleteCell]: never | undefined; - [Telemetry.FindJupyterCommand]: {command: string}; - [Telemetry.FindJupyterKernelSpec]: never | undefined; - [Telemetry.DisableInteractiveShiftEnter]: never | undefined; - [Telemetry.EnableInteractiveShiftEnter]: never | undefined; - [Telemetry.ExecuteCell]: never | undefined; - [Telemetry.ExecuteCellPerceivedCold]: never | undefined; - [Telemetry.ExecuteCellPerceivedWarm]: never | undefined; - [Telemetry.ExpandAll]: never | undefined; - [Telemetry.ExportNotebook]: never | undefined; - [Telemetry.ExportPythonFile]: never | undefined; - [Telemetry.ExportPythonFileAndOutput]: never | undefined; - [Telemetry.GetPasswordAttempt]: never | undefined; - [Telemetry.GetPasswordFailure]: never | undefined; - [Telemetry.GetPasswordSuccess]: never | undefined; - [Telemetry.GotoSourceCode]: never | undefined; - [Telemetry.HiddenCellTime]: never | undefined; - [Telemetry.ImportNotebook]: { scope: 'command' | 'file' }; - [Telemetry.Interrupt]: never | undefined; - [Telemetry.InterruptJupyterTime]: never | undefined; - [Telemetry.PandasNotInstalled]: never | undefined; - [Telemetry.PandasTooOld]: never | undefined; - [Telemetry.OpenPlotViewer]: never | undefined; - [Telemetry.Redo]: never | undefined; - [Telemetry.RemoteAddCode]: never | undefined; - [Telemetry.RestartJupyterTime]: never | undefined; - [Telemetry.RestartKernel]: never | undefined; - [Telemetry.RunAllCells]: never | undefined; - [Telemetry.RunSelectionOrLine]: never | undefined; - [Telemetry.RunCell]: never | undefined; - [Telemetry.RunCurrentCell]: never | undefined; - [Telemetry.RunAllCellsAbove]: never | undefined; - [Telemetry.RunCellAndAllBelow]: never | undefined; - [Telemetry.RunCurrentCellAndAdvance]: never | undefined; - [Telemetry.RunToLine]: never | undefined; - [Telemetry.RunFileInteractive]: never | undefined; - [Telemetry.RunFromLine]: never | undefined; - [Telemetry.SelfCertsMessageClose]: never | undefined; - [Telemetry.SelfCertsMessageEnabled]: never | undefined; - [Telemetry.SelectJupyterURI]: never | undefined; - [Telemetry.SetJupyterURIToLocal]: never | undefined; - [Telemetry.SetJupyterURIToUserSpecified]: never | undefined; - [Telemetry.ShiftEnterBannerShown]: never | undefined; - [Telemetry.ShowDataViewer]: { rows: number | undefined; columns: number | undefined }; - [Telemetry.ShowHistoryPane]: never | undefined; - [Telemetry.StartJupyter]: never | undefined; - [Telemetry.StartJupyterProcess]: never | undefined; - [Telemetry.SubmitCellThroughInput]: never | undefined; - [Telemetry.Undo]: never | undefined; - [Telemetry.VariableExplorerFetchTime]: never | undefined; - [Telemetry.VariableExplorerToggled]: { open: boolean }; - [Telemetry.VariableExplorerVariableCount]: { variableCount: number }; - [Telemetry.WaitForIdleJupyter]: never | undefined; - [Telemetry.WebviewMonacoStyleUpdate]: never | undefined; - [Telemetry.WebviewStartup]: { type: string }; - [Telemetry.WebviewStyleUpdate]: never | undefined; - [EventName.UNITTEST_NAVIGATE_TEST_FILE]: never | undefined; - [EventName.UNITTEST_NAVIGATE_TEST_FUNCTION]: { focus_code: boolean }; - [EventName.UNITTEST_NAVIGATE_TEST_SUITE]: { focus_code: boolean }; - [EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT]: { count: number }; - /* - Telemetry event sent with details of Jedi Memory usage. - memory - Memory usage of Process in kb. - limit - Upper bound for memory usage of Jedi process. - isUserDefinedLimit - Whether the user has configfured the upper bound limit. - restart - Whether to restart the Jedi Process (i.e. memory > limit). + /** + * 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.JEDI_MEMORY]: { memory: number; limit: number; isUserDefinedLimit: boolean; restart: boolean }; + [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. @@ -436,11 +2250,267 @@ export interface IEventNamePropertyMapping { If true, user has a shell in their environment. If false, user does not have a shell in their environment. */ + /* __GDPR__ + "terminal_shell_identification" : { + "failed" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "owner": "karthiknadig" }, + "terminalprovided" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "shellidentificationsource" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "hascustomshell" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" }, + "hasshellinenv" : { "classification": "SystemMetaData", "purpose": "FeatureInsight", "owner": "karthiknadig" } + } + */ [EventName.TERMINAL_SHELL_IDENTIFICATION]: { failed: boolean; terminalProvided: boolean; - shellIdentificationSource: 'terminalName' | 'settings' | 'environment' | 'default'; + 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 5644e887ba3d..42e51b261129 100644 --- a/src/client/telemetry/types.ts +++ b/src/client/telemetry/types.ts @@ -1,199 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -'use strict'; - -import { DiagnosticCodes } from '../application/diagnostics/constants'; -import { TerminalShellType } from '../common/terminal/types'; -import { DebugConfigurationType } from '../debugger/extension/types'; -import { ConsoleType } from '../debugger/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; - pythonVersion: string | undefined; - interpreterType: InterpreterType | undefined; - terminal: TerminalShellType; - workspaceFolderCount: number; - hasPython3: boolean; - usingUserDefinedInterpreter: boolean; - usingAutoSelectedWorkspaceInterpreter: boolean; - usingGlobalInterpreter: boolean; -}; -export type FormatTelemetry = { - tool: 'autopep8' | 'black' | 'yapf'; - hasCustomArgs: boolean; - formatSelection: boolean; -}; - -export type LanguageServerVersionTelemetry = { - success: boolean; - lsVersion?: string; - usedSSL?: boolean; -}; - -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' | 'test'; - console?: ConsoleType; - 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 TestTool = 'nosetest' | 'pytest' | 'unittest'; -export type TestRunTelemetry = { - tool: TestTool; - scope: 'currentFile' | 'all' | 'file' | 'class' | 'function' | 'failed'; - debugging: boolean; - triggerSource: 'ui' | 'codelens' | 'commandpalette' | 'auto' | 'testExplorer'; - failed: boolean; -}; -export type TestDiscoverytTelemetry = { - tool: TestTool; - trigger: 'ui' | 'commandpalette'; - failed: boolean; -}; -export type TestConfiguringTelemetry = { - tool?: TestTool; - 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: DiagnosticCodes; -}; -export type ImportNotebook = { - scope: 'command'; -}; - -export type Platform = { - failureType?: PlatformErrors; - osVersion?: string; -}; - -export type InterpreterAutoSelection = { - rule?: AutoSelectionRule; - interpreterMissing?: boolean; - identified?: boolean; - updated?: boolean; -}; -export type InterpreterDiscovery = { - locator: string; -}; +'use strict'; -export type InterpreterActivationEnvironmentVariables = { - hasEnvVars?: boolean; - failed?: boolean; -}; +import type { IEventNamePropertyMapping } from './index'; +import { EventName } from './constants'; -export type InterpreterActivation = { - hasCommands?: boolean; - failed?: boolean; - terminal: TerminalShellType; - pythonVersion?: string; - interpreterType: InterpreterType; -}; +export type EditorLoadTelemetry = IEventNamePropertyMapping[EventName.EDITOR_LOAD]; +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 { - activate(): Promise; -} +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); + serviceManager.addSingleton(TensorboardDependencyChecker, TensorboardDependencyChecker); +} diff --git a/src/client/tensorBoard/tensorBoardPrompt.ts b/src/client/tensorBoard/tensorBoardPrompt.ts new file mode 100644 index 000000000000..563419bd4ea6 --- /dev/null +++ b/src/client/tensorBoard/tensorBoardPrompt.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { IPersistentState, IPersistentStateFactory } from '../common/types'; + +enum TensorBoardPromptStateKeys { + ShowNativeTensorBoardPrompt = 'showNativeTensorBoardPrompt', +} + +@injectable() +export class TensorBoardPrompt { + private state: IPersistentState; + + constructor(@inject(IPersistentStateFactory) private persistentStateFactory: IPersistentStateFactory) { + this.state = this.persistentStateFactory.createWorkspacePersistentState( + TensorBoardPromptStateKeys.ShowNativeTensorBoardPrompt, + true, + ); + } + + public isPromptEnabled(): boolean { + return this.state.value; + } +} diff --git a/src/client/tensorBoard/tensorBoardSession.ts b/src/client/tensorBoard/tensorBoardSession.ts new file mode 100644 index 000000000000..b18202810e45 --- /dev/null +++ b/src/client/tensorBoard/tensorBoardSession.ts @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { createPromiseFromCancellation } from '../common/cancellation'; +import { IInstaller, InstallerResponse, ProductInstallStatus, Product } from '../common/types'; +import { Common, TensorBoard } from '../common/utils/localize'; +import { IInterpreterService } from '../interpreter/contracts'; +import { sendTelemetryEvent } from '../telemetry'; +import { EventName } from '../telemetry/constants'; +import { ImportTracker } from '../telemetry/importTracker'; +import { TensorBoardPromptSelection } from './constants'; +import { ModuleInstallFlags } from '../common/installer/types'; +import { traceError, traceVerbose } from '../logging'; + +const TensorBoardSemVerRequirement = '>= 2.4.1'; +const TorchProfilerSemVerRequirement = '>= 0.2.0'; + +/** + * Manages the lifecycle of a TensorBoard session. + * Specifically, it: + * - ensures the TensorBoard Python package is installed, + * - asks the user for a log directory to start TensorBoard with + * - spawns TensorBoard in a background process which must stay running + * to serve the TensorBoard website + * - frames the TensorBoard website in a VSCode webview + * - shuts down the TensorBoard process when the webview is closed + */ +export class TensorBoardSession { + constructor( + private readonly installer: IInstaller, + private readonly interpreterService: IInterpreterService, + private readonly commandManager: ICommandManager, + private readonly applicationShell: IApplicationShell, + ) {} + + private async promptToInstall( + tensorBoardInstallStatus: ProductInstallStatus, + profilerPluginInstallStatus: ProductInstallStatus, + ) { + sendTelemetryEvent(EventName.TENSORBOARD_INSTALL_PROMPT_SHOWN); + const yes = Common.bannerLabelYes; + const no = Common.bannerLabelNo; + const isUpgrade = tensorBoardInstallStatus === ProductInstallStatus.NeedsUpgrade; + let message; + + if ( + tensorBoardInstallStatus === ProductInstallStatus.Installed && + profilerPluginInstallStatus !== ProductInstallStatus.Installed + ) { + // PyTorch user already has TensorBoard, just ask if they want the profiler plugin + message = TensorBoard.installProfilerPluginPrompt; + } else if (profilerPluginInstallStatus !== ProductInstallStatus.Installed) { + // PyTorch user doesn't have compatible TensorBoard or the profiler plugin + message = TensorBoard.installTensorBoardAndProfilerPluginPrompt; + } else if (isUpgrade) { + // Not a PyTorch user and needs upgrade, don't need to mention profiler plugin + message = TensorBoard.upgradePrompt; + } else { + // Not a PyTorch user and needs install, again don't need to mention profiler plugin + message = TensorBoard.installPrompt; + } + const selection = await this.applicationShell.showErrorMessage(message, ...[yes, no]); + let telemetrySelection = TensorBoardPromptSelection.None; + if (selection === yes) { + telemetrySelection = TensorBoardPromptSelection.Yes; + } else if (selection === no) { + telemetrySelection = TensorBoardPromptSelection.No; + } + sendTelemetryEvent(EventName.TENSORBOARD_INSTALL_PROMPT_SELECTION, undefined, { + selection: telemetrySelection, + operationType: isUpgrade ? 'upgrade' : 'install', + }); + return selection; + } + + // Ensure that the TensorBoard package is installed before we attempt + // to start a TensorBoard session. If the user has a torch import in + // any of their open documents, also try to install the torch-tb-plugin + // package, but don't block if installing that fails. + public async ensurePrerequisitesAreInstalled(resource?: Uri): Promise { + traceVerbose('Ensuring TensorBoard package is installed into active interpreter'); + const interpreter = + (await this.interpreterService.getActiveInterpreter(resource)) || + (await this.commandManager.executeCommand('python.setInterpreter')); + if (!interpreter) { + return false; + } + + // First see what dependencies we're missing + let [tensorboardInstallStatus, profilerPluginInstallStatus] = await Promise.all([ + this.installer.isProductVersionCompatible(Product.tensorboard, TensorBoardSemVerRequirement, interpreter), + this.installer.isProductVersionCompatible( + Product.torchProfilerImportName, + TorchProfilerSemVerRequirement, + interpreter, + ), + ]); + const isTorchUser = ImportTracker.hasModuleImport('torch'); + const needsTensorBoardInstall = tensorboardInstallStatus !== ProductInstallStatus.Installed; + const needsProfilerPluginInstall = profilerPluginInstallStatus !== ProductInstallStatus.Installed; + if ( + // PyTorch user, in profiler install experiment, TensorBoard and profiler plugin already installed + (isTorchUser && !needsTensorBoardInstall && !needsProfilerPluginInstall) || + // Not PyTorch user or not in profiler install experiment, so no need for profiler plugin, + // and TensorBoard is already installed + (!isTorchUser && tensorboardInstallStatus === ProductInstallStatus.Installed) + ) { + return true; + } + + // Ask the user if they want to install packages to start a TensorBoard session + const selection = await this.promptToInstall( + tensorboardInstallStatus, + isTorchUser ? profilerPluginInstallStatus : ProductInstallStatus.Installed, + ); + if (selection !== Common.bannerLabelYes && !needsTensorBoardInstall) { + return true; + } + if (selection !== Common.bannerLabelYes) { + return false; + } + + // User opted to install packages. Figure out which ones we need and install them + const tokenSource = new CancellationTokenSource(); + const installerToken = tokenSource.token; + const cancellationPromise = createPromiseFromCancellation({ + cancelAction: 'resolve', + defaultValue: InstallerResponse.Ignore, + token: installerToken, + }); + const installPromises = []; + // If need to install torch.profiler and it's not already installed, add it to our list of promises + if (needsTensorBoardInstall) { + installPromises.push( + this.installer.install( + Product.tensorboard, + interpreter, + installerToken, + tensorboardInstallStatus === ProductInstallStatus.NeedsUpgrade + ? ModuleInstallFlags.upgrade + : undefined, + ), + ); + } + if (isTorchUser && needsProfilerPluginInstall) { + installPromises.push( + this.installer.install( + Product.torchProfilerInstallName, + interpreter, + installerToken, + profilerPluginInstallStatus === ProductInstallStatus.NeedsUpgrade + ? ModuleInstallFlags.upgrade + : undefined, + ), + ); + } + await Promise.race([...installPromises, cancellationPromise]); + + // Check install status again after installing + [tensorboardInstallStatus, profilerPluginInstallStatus] = await Promise.all([ + this.installer.isProductVersionCompatible(Product.tensorboard, TensorBoardSemVerRequirement, interpreter), + this.installer.isProductVersionCompatible( + Product.torchProfilerImportName, + TorchProfilerSemVerRequirement, + interpreter, + ), + ]); + // Send telemetry regarding results of install + sendTelemetryEvent(EventName.TENSORBOARD_PACKAGE_INSTALL_RESULT, undefined, { + wasTensorBoardAttempted: needsTensorBoardInstall, + wasProfilerPluginAttempted: needsProfilerPluginInstall, + wasTensorBoardInstalled: tensorboardInstallStatus === ProductInstallStatus.Installed, + wasProfilerPluginInstalled: profilerPluginInstallStatus === ProductInstallStatus.Installed, + }); + // Profiler plugin is not required to start TensorBoard. If it failed, note that it failed + // in the log, but report success only based on TensorBoard package install status. + if (isTorchUser && profilerPluginInstallStatus !== ProductInstallStatus.Installed) { + traceError(`Failed to install torch-tb-plugin. Profiler plugin will not appear in TensorBoard session.`); + } + return tensorboardInstallStatus === ProductInstallStatus.Installed; + } +} diff --git a/src/client/tensorBoard/tensorboardDependencyChecker.ts b/src/client/tensorBoard/tensorboardDependencyChecker.ts new file mode 100644 index 000000000000..995344284eec --- /dev/null +++ b/src/client/tensorBoard/tensorboardDependencyChecker.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, ICommandManager } from '../common/application/types'; +import { IInstaller } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; +import { TensorBoardSession } from './tensorBoardSession'; + +@injectable() +export class TensorboardDependencyChecker { + constructor( + @inject(IInstaller) private readonly installer: IInstaller, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IApplicationShell) private readonly applicationShell: IApplicationShell, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + ) {} + + public async ensureDependenciesAreInstalled(resource?: Uri): Promise { + const newSession = new TensorBoardSession( + this.installer, + this.interpreterService, + this.commandManager, + this.applicationShell, + ); + const result = await newSession.ensurePrerequisitesAreInstalled(resource); + return result; + } +} diff --git a/src/client/tensorBoard/tensorboardIntegration.ts b/src/client/tensorBoard/tensorboardIntegration.ts new file mode 100644 index 000000000000..f3cbad59977b --- /dev/null +++ b/src/client/tensorBoard/tensorboardIntegration.ts @@ -0,0 +1,88 @@ +/* eslint-disable comma-dangle */ + +/* eslint-disable implicit-arrow-linebreak */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Extension, Uri } from 'vscode'; +import { IWorkspaceService } from '../common/application/types'; +import { TENSORBOARD_EXTENSION_ID } from '../common/constants'; +import { IExtensions, Resource } from '../common/types'; +import { IEnvironmentActivationService } from '../interpreter/activation/types'; +import { TensorBoardPrompt } from './tensorBoardPrompt'; +import { TensorboardDependencyChecker } from './tensorboardDependencyChecker'; + +type PythonApiForTensorboardExtension = { + /** + * Gets activated env vars for the active Python Environment for the given resource. + */ + getActivatedEnvironmentVariables(resource: Resource): Promise; + /** + * Ensures that the dependencies required for TensorBoard are installed in Active Environment for the given resource. + */ + ensureDependenciesAreInstalled(resource?: Uri): Promise; + /** + * Whether to allow displaying tensorboard prompt. + */ + isPromptEnabled(): boolean; +}; + +type TensorboardExtensionApi = { + /** + * Registers python extension specific parts with the tensorboard extension + */ + registerPythonApi(interpreterService: PythonApiForTensorboardExtension): void; +}; + +@injectable() +export class TensorboardExtensionIntegration { + private tensorboardExtension: Extension | undefined; + + constructor( + @inject(IExtensions) private readonly extensions: IExtensions, + @inject(IEnvironmentActivationService) private readonly envActivation: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(TensorboardDependencyChecker) private readonly dependencyChcker: TensorboardDependencyChecker, + @inject(TensorBoardPrompt) private readonly tensorBoardPrompt: TensorBoardPrompt, + ) {} + + public registerApi(tensorboardExtensionApi: TensorboardExtensionApi): TensorboardExtensionApi | undefined { + if (!this.workspaceService.isTrusted) { + this.workspaceService.onDidGrantWorkspaceTrust(() => this.registerApi(tensorboardExtensionApi)); + return undefined; + } + tensorboardExtensionApi.registerPythonApi({ + getActivatedEnvironmentVariables: async (resource: Resource) => + this.envActivation.getActivatedEnvironmentVariables(resource, undefined, true), + ensureDependenciesAreInstalled: async (resource?: Uri): Promise => + this.dependencyChcker.ensureDependenciesAreInstalled(resource), + isPromptEnabled: () => this.tensorBoardPrompt.isPromptEnabled(), + }); + return undefined; + } + + public async integrateWithTensorboardExtension(): Promise { + const api = await this.getExtensionApi(); + if (api) { + this.registerApi(api); + } + } + + private async getExtensionApi(): Promise { + if (!this.tensorboardExtension) { + const extension = this.extensions.getExtension(TENSORBOARD_EXTENSION_ID); + if (!extension) { + return undefined; + } + await extension.activate(); + if (extension.isActive) { + this.tensorboardExtension = extension; + return this.tensorboardExtension.exports; + } + } else { + return this.tensorboardExtension.exports; + } + return undefined; + } +} diff --git a/src/client/terminals/activation.ts b/src/client/terminals/activation.ts index a2b0dc1eff42..ed26916e3eaa 100644 --- a/src/client/terminals/activation.ts +++ b/src/client/terminals/activation.ts @@ -4,42 +4,67 @@ 'use strict'; import { inject, injectable } from 'inversify'; -import { Terminal } from 'vscode'; -import { ITerminalManager, IWorkspaceService } from '../common/application/types'; +import { Terminal, Uri } from 'vscode'; +import { IActiveResourceService, ITerminalManager } from '../common/application/types'; import { ITerminalActivator } from '../common/terminal/types'; import { IDisposable, IDisposableRegistry } from '../common/types'; import { ITerminalAutoActivation } from './types'; +import { shouldEnvExtHandleActivation } from '../envExt/api.internal'; @injectable() export class TerminalAutoActivation implements ITerminalAutoActivation { private handler?: IDisposable; + + private readonly terminalsNotToAutoActivate = new WeakSet(); + constructor( - @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(ITerminalManager) + private readonly terminalManager: ITerminalManager, @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, @inject(ITerminalActivator) private readonly activator: ITerminalActivator, - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService + @inject(IActiveResourceService) + private readonly activeResourceService: IActiveResourceService, ) { disposableRegistry.push(this); } - public dispose() { + + public dispose(): void { if (this.handler) { this.handler.dispose(); this.handler = undefined; } } - public register() { + + public register(): void { if (this.handler) { return; } this.handler = this.terminalManager.onDidOpenTerminal(this.activateTerminal, this); } + + public disableAutoActivation(terminal: Terminal): void { + this.terminalsNotToAutoActivate.add(terminal); + } + private async activateTerminal(terminal: Terminal): Promise { - // If we have just one workspace, then pass that as the resource. - // Until upstream VSC issue is resolved https://github.com/Microsoft/vscode/issues/63052. - const workspaceFolder = - this.workspaceService.hasWorkspaceFolders && this.workspaceService.workspaceFolders!.length > 0 - ? this.workspaceService.workspaceFolders![0].uri - : undefined; - await this.activator.activateEnvironmentInTerminal(terminal, workspaceFolder); + 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 30c65ae83800..48165adcd169 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -3,62 +3,183 @@ 'use strict'; -import { inject, injectable, named } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; - +import { inject, injectable } from 'inversify'; +import { Disposable, EventEmitter, Terminal, Uri } from 'vscode'; +import * as path from 'path'; import { ICommandManager, IDocumentManager } from '../../common/application/types'; import { Commands } from '../../common/constants'; import '../../common/extensions'; -import { IFileSystem } from '../../common/platform/types'; -import { BANNER_NAME_INTERACTIVE_SHIFTENTER, IDisposableRegistry, IPythonExtensionBanner } from '../../common/types'; +import { IDisposableRegistry, IConfigurationService, Resource } from '../../common/types'; import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; +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 { private eventEmitter: EventEmitter = new EventEmitter(); - constructor(@inject(ICommandManager) private commandManager: ICommandManager, + constructor( + @inject(ICommandManager) private commandManager: ICommandManager, @inject(IDocumentManager) private documentManager: IDocumentManager, @inject(IDisposableRegistry) private disposableRegistry: Disposable[], - @inject(IFileSystem) private fileSystem: IFileSystem, - @inject(IPythonExtensionBanner) @named(BANNER_NAME_INTERACTIVE_SHIFTENTER) private readonly shiftEnterBanner: IPythonExtensionBanner, - @inject(IServiceContainer) private serviceContainer: IServiceContainer) { + @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; - public get onExecutedCode() : Event { - return this.eventEmitter.event; - } + 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; + } - public registerCommands() { - this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_In_Terminal, this.executeFileInTerminal.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))); + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager + .executeCommand(Commands.TriggerEnvironmentSelection, file) + .then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { + trigger: 'run-in-terminal', + }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + + await this.executeFileInTerminal(file, trigger, { + newTerminalPerFile, + }) + .then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }) + .catch((ex) => traceError('Failed to execute file in terminal', ex)); + }), + ); + }, + ); + this.disposableRegistry.push( + this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal as any, async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + await this.executeSelectionInTerminal().then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }); + }), + ); + this.disposableRegistry.push( + this.commandManager.registerCommand( + Commands.Exec_Selection_In_Django_Shell as any, + async (file: Resource) => { + const interpreterService = this.serviceContainer.get(IInterpreterService); + const interpreter = await interpreterService.getActiveInterpreter(file); + if (!interpreter) { + this.commandManager.executeCommand(Commands.TriggerEnvironmentSelection, file).then(noop, noop); + return; + } + sendTelemetryEvent(EventName.ENVIRONMENT_CHECK_TRIGGER, undefined, { trigger: 'run-selection' }); + triggerCreateEnvironmentCheckNonBlocking(CreateEnvironmentCheckKind.File, file); + await this.executeSelectionInDjangoShell().then(() => { + if (this.shouldTerminalFocusOnStart(file)) + this.commandManager.executeCommand('workbench.action.terminal.focus'); + }); + }, + ), + ); } - @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'file' }, false) - private async executeFileInTerminal(file?: Uri) { + + private async executeUsingExtension(file: Resource, dedicated: boolean): Promise { const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); file = file instanceof Uri ? file : undefined; - const fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); if (!fileToExecute) { return; } - await codeExecutionHelper.saveFileIfDirty(fileToExecute); - try { - const contents = await this.fileSystem.readFile(fileToExecute.fsPath); - this.eventEmitter.fire(contents); - } catch { - // Ignore any errors that occur for firing this event. It's only used - // for telemetry - noop(); + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; + } + + // Check on setting terminal.executeInFileDir + const pythonSettings = this.configSettings.getSettings(file); + let cwd = pythonSettings.terminal.executeInFileDir ? path.dirname(fileToExecute.fsPath) : undefined; + + // Check on setting terminal.launchArgs + const launchArgs = pythonSettings.terminal.launchArgs; + const totalArgs = [...launchArgs, fileToExecute.fsPath.fileToCommandArgumentForPythonExt()]; + + const show = this.shouldTerminalFocusOnStart(fileToExecute); + let terminal: Terminal | undefined; + if (dedicated) { + terminal = await runInDedicatedTerminal(fileToExecute, totalArgs, cwd, show); + } else { + terminal = await runInTerminal(fileToExecute, totalArgs, cwd, show); + } + + if (terminal) { + terminal.show(); + } + } + + private async executeFileInTerminal( + file: Resource, + trigger: 'command' | 'icon', + options?: { newTerminalPerFile: boolean }, + ): Promise { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { + scope: 'file', + trigger, + newTerminalPerFile: options?.newTerminalPerFile, + }); + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + file = file instanceof Uri ? file : undefined; + let fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + if (!fileToExecute) { + return; + } + const fileAfterSave = await codeExecutionHelper.saveFileIfDirty(fileToExecute); + if (fileAfterSave) { + fileToExecute = fileAfterSave; } const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); - await executionService.executeFile(fileToExecute); + await executionService.executeFile(fileToExecute, options); } @captureTelemetry(EventName.EXECUTION_CODE, { scope: 'selection' }, false) @@ -66,8 +187,6 @@ export class CodeExecutionManager implements ICodeExecutionManager { const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); await this.executeSelection(executionService); - // Prompt one time to ask if they want to send shift-enter to the Interactive Window - this.shiftEnterBanner.showBanner().ignoreErrors(); } @captureTelemetry(EventName.EXECUTION_DJANGO, { scope: 'selection' }, false) @@ -82,8 +201,16 @@ export class CodeExecutionManager implements ICodeExecutionManager { return; } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); - const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); + const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor); + let wholeFileContent = ''; + if (activeEditor && activeEditor.document) { + wholeFileContent = activeEditor.document.getText(); + } + const normalizedCode = await codeExecutionHelper.normalizeLines( + codeToExecute!, + ReplType.terminal, + wholeFileContent, + ); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } @@ -96,6 +223,10 @@ export class CodeExecutionManager implements ICodeExecutionManager { noop(); } - await executionService.execute(normalizedCode, activeEditor!.document.uri); + await executionService.execute(normalizedCode, activeEditor.document.uri); + } + + private shouldTerminalFocusOnStart(uri: Uri | undefined): boolean { + return this.configSettings.getSettings(uri)?.terminal.focusAfterLaunch; } } diff --git a/src/client/terminals/codeExecution/djangoContext.ts b/src/client/terminals/codeExecution/djangoContext.ts index 04b0a6a2592d..74643084db28 100644 --- a/src/client/terminals/codeExecution/djangoContext.ts +++ b/src/client/terminals/codeExecution/djangoContext.ts @@ -7,6 +7,7 @@ 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 { @@ -16,19 +17,21 @@ export class DjangoContextInitializer implements Disposable { 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 { + 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 { + // 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 c584ad5d8e40..4efad5ee174e 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -1,30 +1,64 @@ // 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); this.applicationShell = serviceContainer.get(IApplicationShell); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); - this.configurationService = serviceContainer.get(IConfigurationService); + this.interpreterService = serviceContainer.get(IInterpreterService); + this.configSettings = serviceContainer.get(IConfigurationService); + this.commandManager = serviceContainer.get(ICommandManager); + this.activeResourceService = this.serviceContainer.get(IActiveResourceService); } - public async normalizeLines(code: string, resource?: Uri): Promise { + + public async normalizeLines( + code: string, + _replType: ReplType, + wholeFileContent?: string, + resource?: Uri, + ): Promise { try { if (code.trim().length === 0) { return ''; @@ -32,57 +66,260 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { // On windows cr is not handled well by python when passing in/out via stdin/stdout. // So just remove cr from the input. code = code.replace(new RegExp('\\r', 'g'), ''); - const pythonPath = this.configurationService.getSettings(resource).pythonPath; - const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'normalizeForInterpreter.py'), code]; + + 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(); + + // Read result from the normalization script from stdout, and resolve the promise when done. + let normalized = ''; + observable.out.subscribe({ + next: (output) => { + if (output.source === 'stdout') { + normalized += output.out; + } + }, + complete: () => { + normalizeOutput.resolve(normalized); + }, + }); + // If there is no explicit selection, we are exeucting 'line' or 'block'. + if (activeEditor?.selection?.isEmpty) { + sendTelemetryEvent(EventName.EXECUTION_CODE, undefined, { scope: 'line' }); + } + // The normalization script expects a serialized JSON object, with the selection under the "code" key. + // We're using a JSON object so that we don't have to worry about encoding, or escaping non-ASCII characters. + const startLineVal = activeEditor?.selection?.start.line ?? 0; + const endLineVal = activeEditor?.selection?.end.line ?? 0; + const emptyHighlightVal = activeEditor?.selection?.isEmpty ?? true; + let smartSendSettingsEnabledVal = true; + let shellIntegrationEnabled = false; + const configuration = this.serviceContainer.get(IConfigurationService); + if (configuration) { + const pythonSettings = configuration.getSettings(this.activeResourceService.getActiveResource()); + smartSendSettingsEnabledVal = pythonSettings.REPL.enableREPLSmartSend; + shellIntegrationEnabled = pythonSettings.terminal.shellIntegration.enabled; + } + + const input = JSON.stringify({ + code, + wholeFileContent, + startLine: startLineVal, + endLine: endLineVal, + emptyHighlight: emptyHighlightVal, + smartSendSettingsEnabled: smartSendSettingsEnabledVal, + }); + observable.proc?.stdin?.write(input); + observable.proc?.stdin?.end(); + + // We expect a serialized JSON object back, with the normalized code under the "normalized" key. + const result = await normalizeOutput.promise; + const object = JSON.parse(result); + + if (activeEditor?.selection && smartSendSettingsEnabledVal && object.normalized !== 'deprecated') { + const lineOffset = object.nextBlockLineno - activeEditor!.selection.start.line - 1; + await this.moveToNextBlock(lineOffset, activeEditor); + } + + // For new _pyrepl for Python3.13+ && !shellIntegration, we need to send code via bracketed paste mode. + if (object.attach_bracket_paste && !shellIntegrationEnabled && _replType === ReplType.terminal) { + let trimmedNormalized = object.normalized.replace(/\n$/, ''); + if (trimmedNormalized.endsWith(':\n')) { + // In case where statement is unfinished via :, truncate so auto-indentation lands nicely. + trimmedNormalized = trimmedNormalized.replace(/\n$/, ''); + } + return `\u001b[200~${trimmedNormalized}\u001b[201~`; + } + + return parse(object.normalized); } catch (ex) { - 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 { + if (activeEditor?.selection?.isEmpty) { + await this.commandManager.executeCommand('cursorMove', { + to: 'down', + by: 'line', + value: Number(lineOffset), + }); + await this.commandManager.executeCommand('cursorEnd'); + } + + return Promise.resolve(); + } + public async getFileToExecute(): Promise { - const activeEditor = this.documentManager.activeTextEditor!; + const activeEditor = this.documentManager.activeTextEditor; if (!activeEditor) { - this.applicationShell.showErrorMessage('No open file to run in terminal'); - return; + this.applicationShell.showErrorMessage(l10n.t('No open file to run in terminal')); + return undefined; } if (activeEditor.document.isUntitled) { - this.applicationShell.showErrorMessage('The active file needs to be saved before it can be run'); - return; + this.applicationShell.showErrorMessage(l10n.t('The active file needs to be saved before it can be run')); + return undefined; } if (activeEditor.document.languageId !== PYTHON_LANGUAGE) { - this.applicationShell.showErrorMessage('The active file is not a Python source file'); - return; + this.applicationShell.showErrorMessage(l10n.t('The active file is not a Python source file')); + return undefined; } if (activeEditor.document.isDirty) { await activeEditor.document.save(); } + return activeEditor.document.uri; } + // eslint-disable-next-line class-methods-use-this public async getSelectedTextToExecute(textEditor: TextEditor): Promise { if (!textEditor) { - return; + return undefined; } - const selection = textEditor.selection; + const { selection } = textEditor; let code: string; + if (selection.isEmpty) { code = textEditor.document.lineAt(selection.start.line).text; + } else if (selection.isSingleLine) { + code = getSingleLineSelectionText(textEditor); } else { - const textRange = new Range(selection.start, selection.end); - code = textEditor.document.getText(textRange); + code = getMultiLineSelectionText(textEditor); } + return code; } - public async saveFileIfDirty(file: Uri): Promise { - 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 { + 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); + 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 c40cc3792995..bc9a30af1fac 100644 --- a/src/client/terminals/codeExecution/repl.ts +++ b/src/client/terminals/codeExecution/repl.ts @@ -5,10 +5,11 @@ import { inject, injectable } from 'inversify'; import { Disposable } from 'vscode'; -import { IWorkspaceService } from '../../common/application/types'; +import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../common/application/types'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalServiceFactory } from '../../common/terminal/types'; import { IConfigurationService, IDisposableRegistry } from '../../common/types'; +import { IInterpreterService } from '../../interpreter/contracts'; import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; @injectable() @@ -18,10 +19,21 @@ export class ReplProvider extends TerminalCodeExecutionProvider { @inject(IConfigurationService) configurationService: IConfigurationService, @inject(IWorkspaceService) workspace: IWorkspaceService, @inject(IDisposableRegistry) disposableRegistry: Disposable[], - @inject(IPlatformService) platformService: IPlatformService + @inject(IPlatformService) platformService: IPlatformService, + @inject(IInterpreterService) interpreterService: IInterpreterService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IApplicationShell) applicationShell: IApplicationShell, ) { - - super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + super( + terminalServiceFactory, + configurationService, + workspace, + disposableRegistry, + platformService, + interpreterService, + commandManager, + applicationShell, + ); this.terminalTitle = 'REPL'; } } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index 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; - 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 { 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(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(async (resolve) => { + const replCommandArgs = await this.getExecutableInfo(resource); + let listener: IDisposable; + Promise.race([ + new Promise((resolve) => setTimeout(() => resolve(true), 3000)), + new Promise((resolve) => { + let count = 0; + const terminalDataTimeout = setTimeout(() => { + resolve(true); // Fall back for test case scenarios. + }, 3000); + // Watch TerminalData to see if REPL launched. + listener = this.applicationShell.onDidWriteTerminalData((e) => { + for (let i = 0; i < e.data.length; i++) { + if (e.data[i] === '>') { + count++; + if (count === 3) { + clearTimeout(terminalDataTimeout); + resolve(true); + } + } + } + }); + }), + ]).then(() => { + if (listener) { + listener.dispose(); + } + resolve(true); + }); - // Give python repl time to start before we start sending text. - setTimeout(() => resolve(true), 1000); + await terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); }); + this.disposables.push( + terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + }), + ); await this.replActive; } - public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } { + + public async getExecutableInfo(resource?: Uri, args: string[] = []): Promise { const pythonSettings = this.configurationService.getSettings(resource); - const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; - const 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 { + 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 { + const location = this.getLocation(shell); + if (!location) { + return; + } + const shellType = identifyShellFromShellPath(shell); + const terminal = this.terminalManager.createTerminal({ + name: `Python ${shellType} Deactivate`, + shellPath: shell, + hideFromUser: true, + cwd: location, + }); + const globalInterpreters = this.interpreterService.getInterpreters().filter((i) => !i.type); + const outputFile = path.join(location, `envVars.txt`); + const interpreterPath = + globalInterpreters.length > 0 && globalInterpreters[0] ? globalInterpreters[0].path : 'python'; + const checkIfFileHasBeenCreated = () => pathExists(outputFile); + const stopWatch = new StopWatch(); + const command = this.terminalHelper.buildCommandForTerminal(shellType, interpreterPath, [ + this.envVarScript, + outputFile, + ]); + terminal.sendText(command); + await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); + traceVerbose(`Time taken to get env vars using terminal is ${stopWatch.elapsedTime}ms`); + } + + public async getScriptLocation(shell: string, resource: Resource): Promise { + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.type !== PythonEnvType.Virtual) { + return undefined; + } + return this.getLocation(shell); + } + + private getLocation(shell: string) { + const shellType = identifyShellFromShellPath(shell); + if (!ShellIntegrationShells.includes(shellType)) { + return undefined; + } + return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType)); + } + + private getShellFolderName(shellType: TerminalShellType): string { + switch (shellType) { + case TerminalShellType.powershell: + case TerminalShellType.powershellCore: + return 'powershell'; + case TerminalShellType.fish: + return 'fish'; + case TerminalShellType.zsh: + return 'zsh'; + case TerminalShellType.bash: + return 'bash'; + default: + throw new Error(`Unsupported shell type ${shellType}`); + } + } +} diff --git a/src/client/terminals/envCollectionActivation/indicatorPrompt.ts b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts new file mode 100644 index 000000000000..5701bf78603e --- /dev/null +++ b/src/client/terminals/envCollectionActivation/indicatorPrompt.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../common/application/types'; +import { + IConfigurationService, + IDisposableRegistry, + IExperimentService, + IPersistentStateFactory, + Resource, +} from '../../common/types'; +import { Common, Interpreters } from '../../common/utils/localize'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../types'; +import { sleep } from '../../common/utils/async'; +import { isTestExecution } from '../../common/constants'; +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { useEnvExtension } from '../../envExt/api.internal'; + +export const terminalEnvCollectionPromptKey = 'TERMINAL_ENV_COLLECTION_PROMPT_KEY'; + +@injectable() +export class TerminalIndicatorPrompt implements IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + constructor( + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IDisposableRegistry) private readonly disposableRegistry: IDisposableRegistry, + @inject(IActiveResourceService) private readonly activeResourceService: IActiveResourceService, + @inject(ITerminalEnvVarCollectionService) + private readonly terminalEnvVarCollectionService: ITerminalEnvVarCollectionService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(IExperimentService) private readonly experimentService: IExperimentService, + ) {} + + public async activate(): Promise { + if (!inTerminalEnvVarExperiment(this.experimentService) || useEnvExtension()) { + return; + } + if (!isTestExecution()) { + // Avoid showing prompt until startup completes. + await sleep(6000); + } + this.disposableRegistry.push( + this.terminalManager.onDidOpenTerminal(async (terminal) => { + const hideFromUser = + 'hideFromUser' in terminal.creationOptions && terminal.creationOptions.hideFromUser; + const strictEnv = 'strictEnv' in terminal.creationOptions && terminal.creationOptions.strictEnv; + if (hideFromUser || strictEnv || terminal.creationOptions.name) { + // Only show this notification for basic terminals created using the '+' button. + return; + } + const cwd = + 'cwd' in terminal.creationOptions && terminal.creationOptions.cwd + ? terminal.creationOptions.cwd + : this.activeResourceService.getActiveResource(); + const resource = typeof cwd === 'string' ? Uri.file(cwd) : cwd; + const settings = this.configurationService.getSettings(resource); + if (!settings.terminal.activateEnvironment) { + return; + } + if (this.terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)) { + // No need to show notification if terminal prompt already indicates when env is activated. + return; + } + await this.notifyUsers(resource); + }), + ); + } + + private async notifyUsers(resource: Resource): Promise { + const notificationPromptEnabled = this.persistentStateFactory.createGlobalPersistentState( + terminalEnvCollectionPromptKey, + true, + ); + if (!notificationPromptEnabled.value) { + return; + } + const prompts = [Common.doNotShowAgain]; + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (!interpreter || !interpreter.type) { + return; + } + const terminalPromptName = getPromptName(interpreter); + const environmentType = interpreter.type === PythonEnvType.Conda ? 'Selected conda' : 'Python virtual'; + const selection = await this.appShell.showInformationMessage( + Interpreters.terminalEnvVarCollectionPrompt.format(environmentType, terminalPromptName), + ...prompts, + ); + if (!selection) { + return; + } + if (selection === prompts[0]) { + await notificationPromptEnabled.updateValue(false); + } + } +} + +function getPromptName(interpreter: PythonEnvironment) { + if (interpreter.envName) { + return `"(${interpreter.envName})"`; + } + if (interpreter.envPath) { + return `"(${path.basename(interpreter.envPath)})"`; + } + return 'environment indicator'; +} diff --git a/src/client/terminals/envCollectionActivation/service.ts b/src/client/terminals/envCollectionActivation/service.ts new file mode 100644 index 000000000000..2ce8d5d5d86a --- /dev/null +++ b/src/client/terminals/envCollectionActivation/service.ts @@ -0,0 +1,515 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { inject, injectable } from 'inversify'; +import { + MarkdownString, + WorkspaceFolder, + GlobalEnvironmentVariableCollection, + EnvironmentVariableScope, + EnvironmentVariableMutatorOptions, + ProgressLocation, +} from 'vscode'; +import { pathExists, normCase } from '../../common/platform/fs-paths'; +import { IExtensionActivationService } from '../../activation/types'; +import { IApplicationShell, IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; +import { inTerminalEnvVarExperiment } from '../../common/experiments/helpers'; +import { IPlatformService } from '../../common/platform/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { + IExtensionContext, + IExperimentService, + Resource, + IDisposableRegistry, + IConfigurationService, + IPathUtils, +} from '../../common/types'; +import { Interpreters } from '../../common/utils/localize'; +import { traceError, traceInfo, traceLog, traceVerbose, traceWarn } from '../../logging'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { defaultShells } from '../../interpreter/activation/service'; +import { IEnvironmentActivationService } from '../../interpreter/activation/types'; +import { EnvironmentType, PythonEnvironment } from '../../pythonEnvironments/info'; +import { getSearchPathEnvVarNames } from '../../common/utils/exec'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { TerminalShellType } from '../../common/terminal/types'; +import { OSType } from '../../common/utils/platform'; + +import { PythonEnvType } from '../../pythonEnvironments/base/info'; +import { + IShellIntegrationDetectionService, + ITerminalDeactivateService, + ITerminalEnvVarCollectionService, +} from '../types'; +import { ProgressService } from '../../common/application/progressService'; +import { useEnvExtension } from '../../envExt/api.internal'; +import { registerPythonStartup } from '../pythonStartup'; + +@injectable() +export class TerminalEnvVarCollectionService implements IExtensionActivationService, ITerminalEnvVarCollectionService { + public readonly supportedWorkspaceTypes = { + untrustedWorkspace: false, + virtualWorkspace: false, + }; + + /** + * Prompts for these shells cannot be set reliably using variables + */ + private noPromptVariableShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.fish, + ]; + + private registeredOnce = false; + + /** + * Carries default environment variables for the currently selected shell. + */ + private processEnvVars: EnvironmentVariables | undefined; + + private readonly progressService: ProgressService; + + private separator: string; + + constructor( + @inject(IPlatformService) private readonly platform: IPlatformService, + @inject(IInterpreterService) private interpreterService: IInterpreterService, + @inject(IExtensionContext) private context: IExtensionContext, + @inject(IApplicationShell) private shell: IApplicationShell, + @inject(IExperimentService) private experimentService: IExperimentService, + @inject(IApplicationEnvironment) private applicationEnvironment: IApplicationEnvironment, + @inject(IDisposableRegistry) private disposables: IDisposableRegistry, + @inject(IEnvironmentActivationService) private environmentActivationService: IEnvironmentActivationService, + @inject(IWorkspaceService) private workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configurationService: IConfigurationService, + @inject(ITerminalDeactivateService) private readonly terminalDeactivateService: ITerminalDeactivateService, + @inject(IPathUtils) private readonly pathUtils: IPathUtils, + @inject(IShellIntegrationDetectionService) + private readonly shellIntegrationDetectionService: IShellIntegrationDetectionService, + @inject(IEnvironmentVariablesProvider) + private readonly environmentVariablesProvider: IEnvironmentVariablesProvider, + ) { + this.separator = platform.osType === OSType.Windows ? ';' : ':'; + this.progressService = new ProgressService(this.shell); + } + + public async activate(resource: Resource): Promise { + try { + if (useEnvExtension()) { + traceVerbose('Ignoring environment variable experiment since env extension is being used'); + this.context.environmentVariableCollection.clear(); + // Needed for shell integration + await registerPythonStartup(this.context); + return; + } + + if (!inTerminalEnvVarExperiment(this.experimentService)) { + this.context.environmentVariableCollection.clear(); + await this.handleMicroVenv(resource); + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this.handleMicroVenv(r); + }, + this, + this.disposables, + ); + this.registeredOnce = true; + } + await registerPythonStartup(this.context); + return; + } + if (!this.registeredOnce) { + this.interpreterService.onDidChangeInterpreter( + async (r) => { + await this._applyCollection(r).ignoreErrors(); + }, + this, + this.disposables, + ); + this.shellIntegrationDetectionService.onDidChangeStatus( + async () => { + traceInfo("Shell integration status changed, can confirm it's working."); + await this._applyCollection(undefined).ignoreErrors(); + }, + this, + this.disposables, + ); + this.environmentVariablesProvider.onDidEnvironmentVariablesChange( + async (r: Resource) => { + await this._applyCollection(r).ignoreErrors(); + }, + this, + this.disposables, + ); + this.applicationEnvironment.onDidChangeShell( + async (shell: string) => { + this.processEnvVars = undefined; + // Pass in the shell where known instead of relying on the application environment, because of bug + // on VSCode: https://github.com/microsoft/vscode/issues/160694 + await this._applyCollection(undefined, shell).ignoreErrors(); + }, + this, + this.disposables, + ); + const { shell } = this.applicationEnvironment; + const isActive = await this.shellIntegrationDetectionService.isWorking(); + const shellType = identifyShellFromShellPath(shell); + if (!isActive && shellType !== TerminalShellType.commandPrompt) { + traceWarn( + `Shell integration may not be active, environment activated may be overridden by the shell.`, + ); + } + this.registeredOnce = true; + } + this._applyCollection(resource).ignoreErrors(); + } catch (ex) { + traceError(`Activating terminal env collection failed`, ex); + } + } + + public async _applyCollection(resource: Resource, shell?: string): Promise { + this.progressService.showProgress({ + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }); + await this._applyCollectionImpl(resource, shell).catch((ex) => { + traceError(`Failed to apply terminal env vars`, shell, ex); + return Promise.reject(ex); // Ensures progress indicator does not disappear in case of errors, so we can catch issues faster. + }); + this.progressService.hideProgress(); + } + + private async _applyCollectionImpl(resource: Resource, shell = this.applicationEnvironment.shell): Promise { + const workspaceFolder = this.getWorkspaceFolder(resource); + const settings = this.configurationService.getSettings(resource); + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + if (useEnvExtension()) { + envVarCollection.clear(); + traceVerbose('Do not activate terminal env vars as env extension is being used'); + return; + } + + if (!settings.terminal.activateEnvironment) { + envVarCollection.clear(); + traceVerbose('Activating environments in terminal is disabled for', resource?.fsPath); + return; + } + const activatedEnv = await this.environmentActivationService.getActivatedEnvironmentVariables( + resource, + undefined, + undefined, + shell, + ); + const env = activatedEnv ? normCaseKeys(activatedEnv) : undefined; + traceVerbose(`Activated environment variables for ${resource?.fsPath}`, env); + if (!env) { + const shellType = identifyShellFromShellPath(shell); + const defaultShell = defaultShells[this.platform.osType]; + if (defaultShell?.shellType !== shellType) { + // Commands to fetch env vars may fail in custom shells due to unknown reasons, in that case + // fallback to default shells as they are known to work better. + await this._applyCollectionImpl(resource, defaultShell?.shell); + return; + } + await this.trackTerminalPrompt(shell, resource, env); + envVarCollection.clear(); + this.processEnvVars = undefined; + return; + } + if (!this.processEnvVars) { + this.processEnvVars = await this.environmentActivationService.getProcessEnvironmentVariables( + resource, + shell, + ); + } + const processEnv = normCaseKeys(this.processEnvVars); + + // PS1 in some cases is a shell variable (not an env variable) so "env" might not contain it, calculate it in that case. + env.PS1 = await this.getPS1(shell, resource, env); + const defaultPrependOptions = await this.getPrependOptions(); + + // Clear any previously set env vars from collection + envVarCollection.clear(); + const deactivate = await this.terminalDeactivateService.getScriptLocation(shell, resource); + Object.keys(env).forEach((key) => { + if (shouldSkip(key)) { + return; + } + let value = env[key]; + const prevValue = processEnv[key]; + if (prevValue !== value) { + if (value !== undefined) { + if (key === 'PS1') { + // We cannot have the full PS1 without executing in terminal, which we do not. Hence prepend it. + traceLog( + `Prepending environment variable ${key} in collection with ${value} ${JSON.stringify( + defaultPrependOptions, + )}`, + ); + envVarCollection.prepend(key, value, defaultPrependOptions); + return; + } + if (key === 'PATH') { + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + if (processEnv.PATH && env.PATH?.endsWith(processEnv.PATH)) { + // Prefer prepending to PATH instead of replacing it, as we do not want to replace any + // changes to PATH users might have made it in their init scripts (~/.bashrc etc.) + value = env.PATH.slice(0, -processEnv.PATH.length); + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } + traceLog( + `Prepending environment variable ${key} in collection with ${value} ${JSON.stringify( + options, + )}`, + ); + envVarCollection.prepend(key, value, options); + } else { + if (!value.endsWith(this.separator)) { + value = value.concat(this.separator); + } + if (deactivate) { + value = `${deactivate}${this.separator}${value}`; + } + traceLog( + `Prepending environment variable ${key} in collection to ${value} ${JSON.stringify( + options, + )}`, + ); + envVarCollection.prepend(key, value, options); + } + return; + } + const options = { + applyAtShellIntegration: true, + applyAtProcessCreation: true, + }; + traceLog( + `Setting environment variable ${key} in collection to ${value} ${JSON.stringify(options)}`, + ); + envVarCollection.replace(key, value, options); + } + } + }); + + const displayPath = this.pathUtils.getDisplayName(settings.pythonPath, workspaceFolder?.uri.fsPath); + const description = new MarkdownString(`${Interpreters.activateTerminalDescription} \`${displayPath}\``); + envVarCollection.description = description; + + await this.trackTerminalPrompt(shell, resource, env); + await this.terminalDeactivateService.initializeScriptParams(shell).catch((ex) => { + traceError(`Failed to initialize deactivate script`, shell, ex); + }); + } + + private isPromptSet = new Map(); + + // eslint-disable-next-line class-methods-use-this + public isTerminalPromptSetCorrectly(resource?: Resource): boolean { + const workspaceFolder = this.getWorkspaceFolder(resource); + return !!this.isPromptSet.get(workspaceFolder?.index); + } + + /** + * Call this once we know terminal prompt is set correctly for terminal owned by this resource. + */ + private terminalPromptIsCorrect(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.set(key, true); + } + + private terminalPromptIsUnknown(resource: Resource) { + const key = this.getWorkspaceFolder(resource)?.index; + this.isPromptSet.delete(key); + } + + /** + * Tracks whether prompt for terminal was correctly set. + */ + private async trackTerminalPrompt(shell: string, resource: Resource, env: EnvironmentVariables | undefined) { + this.terminalPromptIsUnknown(resource); + if (!env) { + this.terminalPromptIsCorrect(resource); + return; + } + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1 && !env.PS1) { + // PS1 should be set but no PS1 was set. + return; + } + const config = await this.shellIntegrationDetectionService.isWorking(); + if (!config) { + traceVerbose('PS1 is not set when shell integration is disabled.'); + return; + } + } + this.terminalPromptIsCorrect(resource); + } + + private async getPS1(shell: string, resource: Resource, env: EnvironmentVariables) { + // PS1 returned by shell is not predictable: #22078 + // Hence calculate it ourselves where possible. Should no longer be needed once #22128 is available. + const customShellType = identifyShellFromShellPath(shell); + if (this.noPromptVariableShells.includes(customShellType)) { + return env.PS1; + } + if (this.platform.osType !== OSType.Windows) { + // These shells are expected to set PS1 variable for terminal prompt for virtual/conda environments. + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + const shouldSetPS1 = shouldPS1BeSet(interpreter?.type, env); + if (shouldSetPS1) { + const prompt = getPromptForEnv(interpreter, env); + if (prompt) { + return prompt; + } + } + } + if (env.PS1) { + // Prefer PS1 set by env vars, as env.PS1 may or may not contain the full PS1: #22056. + return env.PS1; + } + return undefined; + } + + private async handleMicroVenv(resource: Resource) { + try { + const settings = this.configurationService.getSettings(resource); + const workspaceFolder = this.getWorkspaceFolder(resource); + if (useEnvExtension()) { + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + traceVerbose('Do not activate microvenv as env extension is being used'); + return; + } + if (!settings.terminal.activateEnvironment) { + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + traceVerbose( + 'Do not activate microvenv as activating environments in terminal is disabled for', + resource?.fsPath, + ); + return; + } + const interpreter = await this.interpreterService.getActiveInterpreter(resource); + if (interpreter?.envType === EnvironmentType.Venv) { + const activatePath = path.join(path.dirname(interpreter.path), 'activate'); + if (!(await pathExists(activatePath))) { + const envVarCollection = this.getEnvironmentVariableCollection({ workspaceFolder }); + const pathVarName = getSearchPathEnvVarNames()[0]; + envVarCollection.replace( + 'PATH', + `${path.dirname(interpreter.path)}${path.delimiter}${process.env[pathVarName]}`, + { applyAtShellIntegration: true, applyAtProcessCreation: true }, + ); + return; + } + this.getEnvironmentVariableCollection({ workspaceFolder }).clear(); + } + } catch (ex) { + traceWarn(`Microvenv failed as it is using proposed API which is constantly changing`, ex); + } + } + + private async getPrependOptions(): Promise { + const isActive = await this.shellIntegrationDetectionService.isWorking(); + // Ideally we would want to prepend exactly once, either at shell integration or process creation. + // TODO: Stop prepending altogether once https://github.com/microsoft/vscode/issues/145234 is available. + return isActive + ? { + applyAtShellIntegration: true, + applyAtProcessCreation: false, + } + : { + applyAtShellIntegration: true, // Takes care of false negatives in case manual integration is being used. + applyAtProcessCreation: true, + }; + } + + private getEnvironmentVariableCollection(scope: EnvironmentVariableScope = {}) { + const envVarCollection = this.context.environmentVariableCollection as GlobalEnvironmentVariableCollection; + return envVarCollection.getScoped(scope); + } + + private getWorkspaceFolder(resource: Resource): WorkspaceFolder | undefined { + let workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if ( + !workspaceFolder && + Array.isArray(this.workspaceService.workspaceFolders) && + this.workspaceService.workspaceFolders.length > 0 + ) { + [workspaceFolder] = this.workspaceService.workspaceFolders; + } + return workspaceFolder; + } +} + +function shouldPS1BeSet(type: PythonEnvType | undefined, env: EnvironmentVariables): boolean { + if (env.PS1) { + // Activated variables contain PS1, meaning it was supposed to be set. + return true; + } + if (type === PythonEnvType.Virtual) { + const promptDisabledVar = env.VIRTUAL_ENV_DISABLE_PROMPT; + const isPromptDisabled = promptDisabledVar && promptDisabledVar !== undefined; + return !isPromptDisabled; + } + if (type === PythonEnvType.Conda) { + // Instead of checking config value using `conda config --get changeps1`, simply check + // `CONDA_PROMPT_MODIFER` to avoid the cost of launching the conda binary. + const promptEnabledVar = env.CONDA_PROMPT_MODIFIER; + const isPromptEnabled = promptEnabledVar && promptEnabledVar !== ''; + return !!isPromptEnabled; + } + return false; +} + +function shouldSkip(env: string) { + return [ + '_', + 'SHLVL', + // Even though this maybe returned, setting it can result in output encoding errors in terminal. + 'PYTHONUTF8', + // We have deactivate service which takes care of setting it. + '_OLD_VIRTUAL_PATH', + 'PWD', + ].includes(env); +} + +function getPromptForEnv(interpreter: PythonEnvironment | undefined, env: EnvironmentVariables) { + if (!interpreter) { + return undefined; + } + if (interpreter.envName) { + if (interpreter.envName === 'base') { + // If conda base environment is selected, it can lead to "(base)" appearing twice if we return the env name. + return undefined; + } + if (interpreter.type === PythonEnvType.Virtual && env.VIRTUAL_ENV_PROMPT) { + return `${env.VIRTUAL_ENV_PROMPT}`; + } + return `(${interpreter.envName}) `; + } + if (interpreter.envPath) { + return `(${path.basename(interpreter.envPath)}) `; + } + return undefined; +} + +function normCaseKeys(env: EnvironmentVariables): EnvironmentVariables { + const result: EnvironmentVariables = {}; + Object.keys(env).forEach((key) => { + result[normCase(key)] = env[key]; + }); + return result; +} diff --git a/src/client/terminals/envCollectionActivation/shellIntegrationService.ts b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts new file mode 100644 index 000000000000..92bb98029892 --- /dev/null +++ b/src/client/terminals/envCollectionActivation/shellIntegrationService.ts @@ -0,0 +1,166 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable, inject } from 'inversify'; +import { EventEmitter } from 'vscode'; +import { + IApplicationEnvironment, + IApplicationShell, + ITerminalManager, + IWorkspaceService, +} from '../../common/application/types'; +import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector'; +import { TerminalShellType } from '../../common/terminal/types'; +import { IDisposableRegistry, IPersistentStateFactory } from '../../common/types'; +import { sleep } from '../../common/utils/async'; +import { traceError, traceVerbose } from '../../logging'; +import { IShellIntegrationDetectionService } from '../types'; +import { isTrusted } from '../../common/vscodeApis/workspaceApis'; + +/** + * This is a list of shells which support shell integration: + * https://code.visualstudio.com/docs/terminal/shell-integration + */ +const ShellIntegrationShells = [ + TerminalShellType.powershell, + TerminalShellType.powershellCore, + TerminalShellType.bash, + TerminalShellType.zsh, + TerminalShellType.fish, +]; + +export enum isShellIntegrationWorking { + key = 'SHELL_INTEGRATION_WORKING_KEY', +} + +@injectable() +export class ShellIntegrationDetectionService implements IShellIntegrationDetectionService { + private isWorkingForShell = new Set(); + + private readonly didChange = new EventEmitter(); + + private isDataWriteEventWorking = true; + + constructor( + @inject(ITerminalManager) private readonly terminalManager: ITerminalManager, + @inject(IApplicationShell) private readonly appShell: IApplicationShell, + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IPersistentStateFactory) private readonly persistentStateFactory: IPersistentStateFactory, + @inject(IApplicationEnvironment) private readonly appEnvironment: IApplicationEnvironment, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + ) { + try { + const activeShellType = identifyShellFromShellPath(this.appEnvironment.shell); + const key = getKeyForShell(activeShellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState(key); + if (persistedResult.value) { + this.isWorkingForShell.add(activeShellType); + } + this.appShell.onDidWriteTerminalData( + (e) => { + if (e.data.includes('\x1b]633;A\x07') || e.data.includes('\x1b]133;A\x07')) { + let { shell } = this.appEnvironment; + if ('shellPath' in e.terminal.creationOptions && e.terminal.creationOptions.shellPath) { + shell = e.terminal.creationOptions.shellPath; + } + const shellType = identifyShellFromShellPath(shell); + traceVerbose('Received shell integration sequence for', shellType); + const wasWorking = this.isWorkingForShell.has(shellType); + this.isWorkingForShell.add(shellType); + if (!wasWorking) { + // If it wasn't working previously, status has changed. + this.didChange.fire(); + } + } + }, + this, + this.disposables, + ); + this.appEnvironment.onDidChangeShell( + async (shell: string) => { + this.createDummyHiddenTerminal(shell); + }, + this, + this.disposables, + ); + this.createDummyHiddenTerminal(this.appEnvironment.shell); + } catch (ex) { + this.isDataWriteEventWorking = false; + traceError('Unable to check if shell integration is active', ex); + } + const isEnabled = !!this.workspaceService + .getConfiguration('terminal') + .get('integrated.shellIntegration.enabled'); + if (!isEnabled) { + traceVerbose('Shell integration is disabled in user settings.'); + } + } + + public readonly onDidChangeStatus = this.didChange.event; + + public async isWorking(): Promise { + const { shell } = this.appEnvironment; + return this._isWorking(shell).catch((ex) => { + traceError(`Failed to determine if shell supports shell integration`, shell, ex); + return false; + }); + } + + public async _isWorking(shell: string): Promise { + const shellType = identifyShellFromShellPath(shell); + const isSupposedToWork = ShellIntegrationShells.includes(shellType); + if (!isSupposedToWork) { + return false; + } + const key = getKeyForShell(shellType); + const persistedResult = this.persistentStateFactory.createGlobalPersistentState(key); + if (persistedResult.value !== undefined) { + return persistedResult.value; + } + const result = await this.useDataWriteApproach(shellType); + if (result) { + // Once we know that shell integration is working for a shell, persist it so we need not do this check every session. + await persistedResult.updateValue(result); + } + return result; + } + + private async useDataWriteApproach(shellType: TerminalShellType) { + // For now, based on problems with using the command approach, use terminal data write event. + if (!this.isDataWriteEventWorking) { + // Assume shell integration is working, if data write event isn't working. + return true; + } + if (shellType === TerminalShellType.powershell || shellType === TerminalShellType.powershellCore) { + // Due to upstream bug: https://github.com/microsoft/vscode/issues/204616, assume shell integration is working for now. + return true; + } + if (!this.isWorkingForShell.has(shellType)) { + // Maybe data write event has not been processed yet, wait a bit. + await sleep(1000); + } + traceVerbose( + 'Did we determine shell integration to be working for', + shellType, + '?', + this.isWorkingForShell.has(shellType), + ); + return this.isWorkingForShell.has(shellType); + } + + /** + * Creates a dummy terminal so that we are guaranteed a data write event for this shell type. + */ + private createDummyHiddenTerminal(shell: string) { + if (isTrusted()) { + this.terminalManager.createTerminal({ + shellPath: shell, + hideFromUser: true, + }); + } + } +} + +function getKeyForShell(shellType: TerminalShellType) { + return `${isShellIntegrationWorking.key}_${shellType}`; +} diff --git a/src/client/terminals/pythonStartup.ts b/src/client/terminals/pythonStartup.ts new file mode 100644 index 000000000000..b6f68c860b46 --- /dev/null +++ b/src/client/terminals/pythonStartup.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { ExtensionContext, MarkdownString, Uri } from 'vscode'; +import * as path from 'path'; +import { copy, createDirectory, getConfiguration, onDidChangeConfiguration } from '../common/vscodeApis/workspaceApis'; +import { EXTENSION_ROOT_DIR } from '../constants'; +import { Interpreters } from '../common/utils/localize'; + +async function applyPythonStartupSetting(context: ExtensionContext): Promise { + const config = getConfiguration('python'); + const pythonrcSetting = config.get('terminal.shellIntegration.enabled'); + + if (pythonrcSetting) { + const storageUri = context.storageUri || context.globalStorageUri; + try { + await createDirectory(storageUri); + } catch { + // already exists, most likely + } + const destPath = Uri.joinPath(storageUri, 'pythonrc.py'); + const sourcePath = path.join(EXTENSION_ROOT_DIR, 'python_files', 'pythonrc.py'); + await copy(Uri.file(sourcePath), destPath, { overwrite: true }); + context.environmentVariableCollection.replace('PYTHONSTARTUP', destPath.fsPath); + // When shell integration is enabled, we disable PyREPL from cpython. + context.environmentVariableCollection.replace('PYTHON_BASIC_REPL', '1'); + context.environmentVariableCollection.description = new MarkdownString( + Interpreters.shellIntegrationEnvVarCollectionDescription, + ); + } else { + context.environmentVariableCollection.delete('PYTHONSTARTUP'); + context.environmentVariableCollection.delete('PYTHON_BASIC_REPL'); + context.environmentVariableCollection.description = new MarkdownString( + Interpreters.shellIntegrationDisabledEnvVarCollectionDescription, + ); + } +} + +export async function registerPythonStartup(context: ExtensionContext): Promise { + await applyPythonStartupSetting(context); + context.subscriptions.push( + onDidChangeConfiguration(async (e) => { + if (e.affectsConfiguration('python.terminal.shellIntegration.enabled')) { + await applyPythonStartupSetting(context); + } + }), + ); +} diff --git a/src/client/terminals/pythonStartupLinkProvider.ts b/src/client/terminals/pythonStartupLinkProvider.ts new file mode 100644 index 000000000000..aba1270f1412 --- /dev/null +++ b/src/client/terminals/pythonStartupLinkProvider.ts @@ -0,0 +1,50 @@ +/* eslint-disable class-methods-use-this */ +import { + CancellationToken, + Disposable, + ProviderResult, + TerminalLink, + TerminalLinkContext, + TerminalLinkProvider, +} from 'vscode'; +import { executeCommand } from '../common/vscodeApis/commandApis'; +import { registerTerminalLinkProvider } from '../common/vscodeApis/windowApis'; +import { Repl } from '../common/utils/localize'; + +interface CustomTerminalLink extends TerminalLink { + command: string; +} + +export class CustomTerminalLinkProvider implements TerminalLinkProvider { + provideTerminalLinks( + context: TerminalLinkContext, + _token: CancellationToken, + ): ProviderResult { + const links: CustomTerminalLink[] = []; + let expectedNativeLink; + + if (process.platform === 'darwin') { + expectedNativeLink = 'Cmd click to launch VS Code Native REPL'; + } else { + expectedNativeLink = 'Ctrl click to launch VS Code Native REPL'; + } + + if (context.line.includes(expectedNativeLink)) { + links.push({ + startIndex: context.line.indexOf(expectedNativeLink), + length: expectedNativeLink.length, + tooltip: Repl.launchNativeRepl, + command: 'python.startNativeREPL', + }); + } + return links; + } + + async handleTerminalLink(link: CustomTerminalLink): Promise { + await executeCommand(link.command); + } +} + +export function registerCustomTerminalLinkProvider(disposables: Disposable[]): void { + disposables.push(registerTerminalLinkProvider(new CustomTerminalLinkProvider())); +} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts index 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, CodeExecutionHelper); + serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); - serviceManager.addSingleton(ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'); - serviceManager.addSingleton(ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'); + + serviceManager.addSingleton( + ICodeExecutionService, + DjangoShellCodeExecutionProvider, + 'djangoShell', + ); + serviceManager.addSingleton( + ICodeExecutionService, + TerminalCodeExecutionProvider, + 'standard', + ); serviceManager.addSingleton(ICodeExecutionService, ReplProvider, 'repl'); + serviceManager.addSingleton(ITerminalAutoActivation, TerminalAutoActivation); + serviceManager.addSingleton( + ITerminalEnvVarCollectionService, + TerminalEnvVarCollectionService, + ); + serviceManager.addSingleton(ITerminalDeactivateService, TerminalDeactivateService); + serviceManager.addSingleton( + IExtensionSingleActivationService, + TerminalIndicatorPrompt, + ); + serviceManager.addSingleton( + IShellIntegrationDetectionService, + ShellIntegrationDetectionService, + ); + + serviceManager.addBinding(ITerminalEnvVarCollectionService, IExtensionActivationService); } diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index b096153a3db8..1384057c3b7c 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -1,34 +1,60 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Event, TextEditor, Uri } from 'vscode'; -import { IDisposable } from '../common/types'; +import { Event, Terminal, TextEditor, Uri } from 'vscode'; +import { IDisposable, Resource } from '../common/types'; +import { ReplType } from '../repl/types'; export const ICodeExecutionService = Symbol('ICodeExecutionService'); export interface ICodeExecutionService { execute(code: string, resource?: Uri): Promise; - executeFile(file: Uri): Promise; + executeFile(file: Uri, options?: { newTerminalPerFile: boolean }): Promise; initializeRepl(resource?: Uri): Promise; } export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): Promise; + normalizeLines(code: string, replType: ReplType, wholeFileContent?: string, resource?: Uri): Promise; getFileToExecute(): Promise; - saveFileIfDirty(file: Uri): Promise; + saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; } export const ICodeExecutionManager = Symbol('ICodeExecutionManager'); export interface ICodeExecutionManager { - onExecutedCode: Event; registerCommands(): void; } export const ITerminalAutoActivation = Symbol('ITerminalAutoActivation'); export interface ITerminalAutoActivation extends IDisposable { register(): void; + disableAutoActivation(terminal: Terminal): void; +} + +export const ITerminalEnvVarCollectionService = Symbol('ITerminalEnvVarCollectionService'); +export interface ITerminalEnvVarCollectionService { + /** + * Returns true if we know with high certainity the terminal prompt is set correctly for a particular resource. + */ + isTerminalPromptSetCorrectly(resource?: Resource): boolean; +} + +export const IShellIntegrationDetectionService = Symbol('IShellIntegrationDetectionService'); +export interface IShellIntegrationDetectionService { + onDidChangeStatus: Event; + isWorking(): Promise; +} + +export const ITerminalDeactivateService = Symbol('ITerminalDeactivateService'); +export interface ITerminalDeactivateService { + initializeScriptParams(shell: string): Promise; + getScriptLocation(shell: string, resource: Resource): Promise; +} + +export const IPythonStartupEnvVarService = Symbol('IPythonStartupEnvVarService'); +export interface IPythonStartupEnvVarService { + register(): void; } diff --git a/src/client/testing/codeLenses/main.ts b/src/client/testing/codeLenses/main.ts deleted file mode 100644 index 9f955d485f34..000000000000 --- a/src/client/testing/codeLenses/main.ts +++ /dev/null @@ -1,20 +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, - symbolProvider: vscode.DocumentSymbolProvider, - testCollectionStorage: ITestCollectionStorageService -): vscode.Disposable { - const disposables: vscode.Disposable[] = []; - const codeLensProvider = new TestFileCodeLensProvider(onDidChange, symbolProvider, testCollectionStorage); - disposables.push(vscode.languages.registerCodeLensProvider(PYTHON, codeLensProvider)); - - return { - dispose: () => { - disposables.forEach(d => d.dispose()); - } - }; -} diff --git a/src/client/testing/codeLenses/testFiles.ts b/src/client/testing/codeLenses/testFiles.ts deleted file mode 100644 index 140d1bddb702..000000000000 --- a/src/client/testing/codeLenses/testFiles.ts +++ /dev/null @@ -1,247 +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, - private symbolProvider: DocumentSymbolProvider, - private testCollectionStorage: ITestCollectionStorageService) { - } - - get onDidChangeCodeLenses(): Event { - 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.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, { testSuite: [cls] }] - }), - new CodeLens(range, { - title: getTestStatusIcon(cls.status) + constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [undefined, CommandSource.codelens, file, { testSuite: [cls] }] - }) - ]; - } - default: { - return []; - } - } - } -} - -function getTestStatusIcon(status?: TestStatus): string { - switch (status) { - case TestStatus.Pass: { - return '✔ '; - } - 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, { testFunction: [fn] }] - }), - new CodeLens(range, { - title: getTestStatusIcon(fn.status) + constants.Text.CodeLensDebugUnitTest, - command: constants.Commands.Tests_Debug, - arguments: [undefined, CommandSource.codelens, file, { testFunction: [fn] }] - }) - ]; - } - - // Ok, possible we're dealing with parameterized unit tests. - // If we have [ in the name, then this is a parameterized function. - const functions = functionsAndSuites.functions.filter(func => func.name.startsWith(`${symbolName}[`) && func.name.endsWith(']')); - if (functions.length === 0) { - return []; - } - - // Find all flattened functions. - return [ - new CodeLens(range, { - title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensRunUnitTest} (Multiple)`, - command: constants.Commands.Tests_Picker_UI, - arguments: [undefined, CommandSource.codelens, file, functions] - }), - new CodeLens(range, { - title: `${getTestStatusIcons(functions)}${constants.Text.CodeLensDebugUnitTest} (Multiple)`, - command: constants.Commands.Tests_Picker_UI_Debug, - arguments: [undefined, CommandSource.codelens, file, functions] - }) - ]; -} - -function getAllTestSuitesAndFunctionsPerFile(testFile: TestFile): FunctionsAndSuites { - // tslint:disable-next-line:prefer-type-cast - const all = { functions: [...testFile.functions], suites: [] as TestSuite[] }; - testFile.suites.forEach(suite => { - all.suites.push(suite); - - const allChildItems = getAllTestSuitesAndFunctions(suite); - all.functions.push(...allChildItems.functions); - all.suites.push(...allChildItems.suites); - }); - return all; -} -function getAllTestSuitesAndFunctions(testSuite: TestSuite): FunctionsAndSuites { - const all: { functions: TestFunction[]; suites: TestSuite[] } = { functions: [], suites: [] }; - testSuite.functions.forEach(fn => { - all.functions.push(fn); - }); - testSuite.suites.forEach(suite => { - all.suites.push(suite); - - const allChildItems = getAllTestSuitesAndFunctions(suite); - all.functions.push(...allChildItems.functions); - all.suites.push(...allChildItems.suites); - }); - return all; -} diff --git a/src/client/testing/common/argumentsHelper.ts b/src/client/testing/common/argumentsHelper.ts deleted file mode 100644 index 3842a9a6874e..000000000000 --- a/src/client/testing/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); - } - 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/testing/common/bufferedTestConfigSettingService.ts b/src/client/testing/common/bufferedTestConfigSettingService.ts new file mode 100644 index 000000000000..35266de38c72 --- /dev/null +++ b/src/client/testing/common/bufferedTestConfigSettingService.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; + +export class BufferedTestConfigSettingsService implements ITestConfigSettingsService { + private ops: [string, string | Uri, UnitTestProduct, string[]][]; + + constructor() { + this.ops = []; + } + + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise { + this.ops.push(['updateTestArgs', testDirectory, product, args]); + return Promise.resolve(); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + this.ops.push(['enable', testDirectory, product, []]); + return Promise.resolve(); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + this.ops.push(['disable', testDirectory, product, []]); + return Promise.resolve(); + } + + public async apply(cfg: ITestConfigSettingsService): Promise { + const { ops } = this; + this.ops = []; + // Note that earlier ops do not get rolled back if a later + // one fails. + for (const [op, testDir, prod, args] of ops) { + switch (op) { + case 'updateTestArgs': + await cfg.updateTestArgs(testDir, prod, args); + break; + case 'enable': + await cfg.enable(testDir, prod); + break; + case 'disable': + await cfg.disable(testDir, prod); + break; + default: + break; + } + } + return Promise.resolve(); + } + + // eslint-disable-next-line class-methods-use-this + public getTestEnablingSetting(_: UnitTestProduct): string { + throw new Error('Method not implemented.'); + } +} diff --git a/src/client/testing/common/configSettingService.ts b/src/client/testing/common/configSettingService.ts new file mode 100644 index 000000000000..f6cfeee773e5 --- /dev/null +++ b/src/client/testing/common/configSettingService.ts @@ -0,0 +1,77 @@ +import { inject, injectable } from 'inversify'; +import { Uri, WorkspaceConfiguration } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { ITestConfigSettingsService, UnitTestProduct } from './types'; + +@injectable() +export class TestConfigSettingsService implements ITestConfigSettingsService { + private readonly workspaceService: IWorkspaceService; + + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + + public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise { + const setting = this.getTestArgSetting(product); + return this.updateSetting(testDirectory, setting, args); + } + + public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, true); + } + + public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { + const setting = this.getTestEnablingSetting(product); + return this.updateSetting(testDirectory, setting, false); + } + + // eslint-disable-next-line class-methods-use-this + public getTestEnablingSetting(product: UnitTestProduct): string { + switch (product) { + case Product.unittest: + return 'testing.unittestEnabled'; + case Product.pytest: + return 'testing.pytestEnabled'; + default: + throw new Error('Invalid Test Product'); + } + } + + // eslint-disable-next-line class-methods-use-this + private getTestArgSetting(product: UnitTestProduct): string { + switch (product) { + case Product.unittest: + return 'testing.unittestArgs'; + case Product.pytest: + return 'testing.pytestArgs'; + default: + throw new Error('Invalid Test Product'); + } + } + + private async updateSetting(testDirectory: string | Uri, setting: string, value: unknown) { + let pythonConfig: WorkspaceConfiguration; + const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; + const hasWorkspaceFolders = (this.workspaceService.workspaceFolders?.length || 0) > 0; + if (!hasWorkspaceFolders) { + pythonConfig = this.workspaceService.getConfiguration('python'); + } else if (this.workspaceService.workspaceFolders!.length === 1) { + pythonConfig = this.workspaceService.getConfiguration( + 'python', + this.workspaceService.workspaceFolders![0].uri, + ); + } else { + const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + if (!workspaceFolder) { + throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); + } + + pythonConfig = this.workspaceService.getConfiguration('python', workspaceFolder.uri); + } + + return pythonConfig.update(setting, value); + } +} diff --git a/src/client/testing/common/constants.ts b/src/client/testing/common/constants.ts index fe5e71044ddd..4f41e60c8806 100644 --- a/src/client/testing/common/constants.ts +++ b/src/client/testing/common/constants.ts @@ -1,28 +1,7 @@ import { Product } from '../../common/types'; -import { TestProvider, UnitTestProduct } from './types'; +import { TestProvider } from '../types'; +import { UnitTestProduct } from './types'; -export const CANCELLATION_REASON = 'cancelled_user_request'; -export enum CommandSource { - auto = 'auto', - ui = 'ui', - codelens = 'codelens', - commandPalette = 'commandpalette', - testExplorer = 'testExplorer' -} -export const TEST_OUTPUT_CHANNEL = 'TEST_OUTPUT_CHANNEL'; - -export const UNIT_TEST_PRODUCTS: UnitTestProduct[] = [ - Product.pytest, - Product.unittest, - Product.nosetest -]; -export const NOSETEST_PROVIDER: TestProvider = 'nosetest'; +export const UNIT_TEST_PRODUCTS: UnitTestProduct[] = [Product.pytest, Product.unittest]; export const PYTEST_PROVIDER: TestProvider = 'pytest'; export const UNITTEST_PROVIDER: TestProvider = 'unittest'; - -export enum Icons { - discovering = 'discovering-tests.svg', - passed = 'status-ok.svg', - failed = 'status-error.svg', - unknown = 'status-unknown.svg' -} diff --git a/src/client/testing/common/debugLauncher.ts b/src/client/testing/common/debugLauncher.ts index 65d8c2c2e17b..037bfb265088 100644 --- a/src/client/testing/common/debugLauncher.ts +++ b/src/client/testing/common/debugLauncher.ts @@ -1,77 +1,158 @@ import { inject, injectable, named } from 'inversify'; -import { parse } from 'jsonc-parser'; import * as path from 'path'; -import { DebugConfiguration, Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IDebugService, IWorkspaceService } from '../../common/application/types'; +import { DebugConfiguration, l10n, Uri, WorkspaceFolder, DebugSession, DebugSessionOptions, Disposable } from 'vscode'; +import { IApplicationShell, IDebugService } from '../../common/application/types'; import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { traceError } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; +import * as internalScripts from '../../common/process/internal/scripts'; import { IConfigurationService, IPythonSettings } from '../../common/types'; -import { noop } from '../../common/utils/misc'; -import { DebuggerTypeName } from '../../debugger/constants'; +import { DebuggerTypeName, PythonDebuggerTypeName } from '../../debugger/constants'; import { IDebugConfigurationResolver } from '../../debugger/extension/configuration/types'; -import { LaunchRequestArguments } from '../../debugger/types'; +import { DebugPurpose, LaunchRequestArguments } from '../../debugger/types'; import { IServiceContainer } from '../../ioc/types'; -import { ITestDebugConfig, ITestDebugLauncher, LaunchOptions, TestProvider } from './types'; +import { traceError, traceVerbose } from '../../logging'; +import { TestProvider } from '../types'; +import { ITestDebugLauncher, LaunchOptions } from './types'; +import { getConfigurationsForWorkspace } from '../../debugger/extension/configuration/launch.json/launchJsonReader'; +import { getWorkspaceFolder, getWorkspaceFolders } from '../../common/vscodeApis/workspaceApis'; +import { showErrorMessage } from '../../common/vscodeApis/windowApis'; +import { createDeferred } from '../../common/utils/async'; +import { addPathToPythonpath } from './helpers'; +import * as envExtApi from '../../envExt/api.internal'; + +/** + * Key used to mark debug configurations with a unique session identifier. + * This allows us to track which debug session belongs to which launchDebugger() call + * when multiple debug sessions are launched in parallel. + */ +const TEST_SESSION_MARKER_KEY = '__vscodeTestSessionMarker'; @injectable() export class DebugLauncher implements ITestDebugLauncher { private readonly configService: IConfigurationService; - private readonly workspaceService: IWorkspaceService; - private readonly fs: IFileSystem; + constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer, - @inject(IDebugConfigurationResolver) @named('launch') private readonly launchResolver: IDebugConfigurationResolver + @inject(IDebugConfigurationResolver) + @named('launch') + private readonly launchResolver: IDebugConfigurationResolver, ) { this.configService = this.serviceContainer.get(IConfigurationService); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.fs = this.serviceContainer.get(IFileSystem); } - public async launchDebugger(options: LaunchOptions) { - if (options.token && options.token!.isCancellationRequested) { - return; + /** + * Launches a debug session for test execution. + * Handles cancellation, multi-session support via unique markers, and cleanup. + */ + public async launchDebugger( + options: LaunchOptions, + callback?: () => void, + sessionOptions?: DebugSessionOptions, + ): Promise { + const deferred = createDeferred(); + let hasCallbackBeenCalled = false; + + // Collect disposables for cleanup when debugging completes + const disposables: Disposable[] = []; + + // Ensure callback is only invoked once, even if multiple termination paths fire + const callCallbackOnce = () => { + if (!hasCallbackBeenCalled) { + hasCallbackBeenCalled = true; + callback?.(); + } + }; + + // Early exit if already cancelled before we start + if (options.token && options.token.isCancellationRequested) { + callCallbackOnce(); + deferred.resolve(); + return deferred.promise; + } + + // Listen for cancellation from the test run (e.g., user clicks stop in Test Explorer) + // This allows the caller to clean up resources even if the debug session is still running + if (options.token) { + disposables.push( + options.token.onCancellationRequested(() => { + deferred.resolve(); + callCallbackOnce(); + }), + ); } - const workspaceFolder = this.resolveWorkspaceFolder(options.cwd); + const workspaceFolder = DebugLauncher.resolveWorkspaceFolder(options.cwd); const launchArgs = await this.getLaunchArgs( options, workspaceFolder, - this.configService.getSettings(workspaceFolder.uri) + this.configService.getSettings(workspaceFolder.uri), ); const debugManager = this.serviceContainer.get(IDebugService); - return debugManager.startDebugging(workspaceFolder, launchArgs) - .then(noop, ex => traceError('Failed to start debugging tests', ex)); - } - public async readAllDebugConfigs(workspaceFolder: WorkspaceFolder): Promise { - const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); - if (!(await this.fs.fileExists(filename))) { - return []; - } + + // Unique marker to identify this session among concurrent debug sessions + const sessionMarker = `test-${Date.now()}-${Math.random().toString(36).slice(2)}`; + launchArgs[TEST_SESSION_MARKER_KEY] = sessionMarker; + + let ourSession: DebugSession | undefined; + + // Capture our specific debug session when it starts by matching the marker. + // This fires for ALL debug sessions, so we filter to only our marker. + disposables.push( + debugManager.onDidStartDebugSession((session) => { + if (session.configuration[TEST_SESSION_MARKER_KEY] === sessionMarker) { + ourSession = session; + traceVerbose(`[test-debug] Debug session started: ${session.name} (${session.id})`); + } + }), + ); + + // Handle debug session termination (user stops debugging, or tests complete). + // Only react to OUR session terminating - other parallel sessions should + // continue running independently. + disposables.push( + debugManager.onDidTerminateDebugSession((session) => { + if (ourSession && session.id === ourSession.id) { + traceVerbose(`[test-debug] Debug session terminated: ${session.name} (${session.id})`); + deferred.resolve(); + callCallbackOnce(); + } + }), + ); + + // Clean up event subscriptions when debugging completes (success, failure, or cancellation) + deferred.promise.finally(() => { + disposables.forEach((d) => d.dispose()); + }); + + // Start the debug session + let started = false; try { - const text = await this.fs.readFile(filename); - const parsed = parse(text, [], { allowTrailingComma: true, disallowComments: false }); - if (!parsed.version || !parsed.configurations || !Array.isArray(parsed.configurations)) { - throw Error('malformed launch.json'); - } - // We do not bother ensuring each item is a DebugConfiguration... - return parsed.configurations; - } catch (exc) { - traceError('could not get debug config', exc); - const appShell = this.serviceContainer.get(IApplicationShell); - await appShell.showErrorMessage('Could not load unit test config from launch.json'); - return []; + started = await debugManager.startDebugging(workspaceFolder, launchArgs, sessionOptions); + } catch (error) { + traceError('Error starting debug session', error); + deferred.reject(error); + callCallbackOnce(); + return deferred.promise; } + if (!started) { + traceError('Failed to start debug session'); + deferred.resolve(); + callCallbackOnce(); + } + + return deferred.promise; } - private resolveWorkspaceFolder(cwd: string): WorkspaceFolder { - if (!this.workspaceService.hasWorkspaceFolders) { + + private static resolveWorkspaceFolder(cwd: string): WorkspaceFolder { + const hasWorkspaceFolders = (getWorkspaceFolders()?.length || 0) > 0; + if (!hasWorkspaceFolders) { throw new Error('Please open a workspace'); } const cwdUri = cwd ? Uri.file(cwd) : undefined; - let workspaceFolder = this.workspaceService.getWorkspaceFolder(cwdUri); + let workspaceFolder = getWorkspaceFolder(cwdUri); if (!workspaceFolder) { - workspaceFolder = this.workspaceService.workspaceFolders![0]; + const [first] = getWorkspaceFolders()!; + workspaceFolder = first; } return workspaceFolder; } @@ -79,54 +160,89 @@ export class DebugLauncher implements ITestDebugLauncher { private async getLaunchArgs( options: LaunchOptions, workspaceFolder: WorkspaceFolder, - configSettings: IPythonSettings + configSettings: IPythonSettings, ): Promise { - let debugConfig = await this.readDebugConfig(workspaceFolder); + let debugConfig = await DebugLauncher.readDebugConfig(workspaceFolder); if (!debugConfig) { debugConfig = { name: 'Debug Unit Test', - type: 'python', + type: 'debugpy', request: 'test', - subProcess: true + subProcess: true, }; } + + // Use project name in debug session name if provided + if (options.project) { + debugConfig.name = `Debug Tests: ${options.project.name}`; + } + if (!debugConfig.rules) { debugConfig.rules = []; } debugConfig.rules.push({ - path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), - include: false + path: path.join(EXTENSION_ROOT_DIR, 'python_files'), + include: false, }); - this.applyDefaults(debugConfig!, workspaceFolder, configSettings); + + DebugLauncher.applyDefaults(debugConfig!, workspaceFolder, configSettings, options.cwd); return this.convertConfigToArgs(debugConfig!, workspaceFolder, options); } - private async readDebugConfig(workspaceFolder: WorkspaceFolder): Promise { - const configs = await this.readAllDebugConfigs(workspaceFolder); - for (const cfg of configs) { - if (!cfg.name || cfg.type !== DebuggerTypeName || cfg.request !== 'test') { - continue; + public async readAllDebugConfigs(workspace: WorkspaceFolder): Promise { + try { + const configs = await getConfigurationsForWorkspace(workspace); + return configs; + } catch (exc) { + traceError('could not get debug config', exc); + const appShell = this.serviceContainer.get(IApplicationShell); + await appShell.showErrorMessage( + l10n.t('Could not load unit test config from launch.json as it is missing a field'), + ); + return []; + } + } + + private static async readDebugConfig( + workspaceFolder: WorkspaceFolder, + ): Promise { + try { + const configs = await getConfigurationsForWorkspace(workspaceFolder); + for (const cfg of configs) { + if ( + cfg.name && + (cfg.type === DebuggerTypeName || cfg.type === PythonDebuggerTypeName) && + (cfg.request === 'test' || + (cfg as LaunchRequestArguments).purpose?.includes(DebugPurpose.DebugTest)) + ) { + // Return the first one. + return cfg as LaunchRequestArguments; + } } - // Return the first one. - return cfg as ITestDebugConfig; + return undefined; + } catch (exc) { + traceError('could not get debug config', exc); + await showErrorMessage(l10n.t('Could not load unit test config from launch.json as it is missing a field')); + return undefined; } - return undefined; } - private applyDefaults( - cfg: ITestDebugConfig, + + private static applyDefaults( + cfg: LaunchRequestArguments, workspaceFolder: WorkspaceFolder, - configSettings: IPythonSettings + configSettings: IPythonSettings, + optionsCwd?: string, ) { // cfg.pythonPath is handled by LaunchConfigurationResolver. - // Default value of justMyCode is not provided intentionally, for now we derive its value required for launchArgs using debugStdLib - // Have to provide it if and when we remove complete support for debugStdLib if (!cfg.console) { cfg.console = 'internalConsole'; } if (!cfg.cwd) { - cfg.cwd = workspaceFolder.uri.fsPath; + // For project-based testing, use the project's cwd (optionsCwd) if provided. + // Otherwise fall back to settings.testing.cwd or the workspace folder. + cfg.cwd = optionsCwd || configSettings.testing.cwd || workspaceFolder.uri.fsPath; } if (!cfg.env) { cfg.env = {}; @@ -134,7 +250,6 @@ export class DebugLauncher implements ITestDebugLauncher { if (!cfg.envFile) { cfg.envFile = configSettings.envFile; } - if (cfg.stopOnEntry === undefined) { cfg.stopOnEntry = false; } @@ -151,45 +266,96 @@ export class DebugLauncher implements ITestDebugLauncher { } private async convertConfigToArgs( - debugConfig: ITestDebugConfig, + debugConfig: LaunchRequestArguments, workspaceFolder: WorkspaceFolder, - options: LaunchOptions + options: LaunchOptions, ): Promise { const configArgs = debugConfig as LaunchRequestArguments; + const testArgs = + 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.program = this.getTestLauncherScript(options.testProvider); - configArgs.args = this.fixArgs(options.args, options.testProvider); + configArgs.args = args.slice(1); // We leave configArgs.request as "test" so it will be sent in telemetry. - const launchArgs = await this.launchResolver.resolveDebugConfiguration( + let launchArgs = await this.launchResolver.resolveDebugConfiguration( workspaceFolder, configArgs, - options.token + options.token, + ); + if (!launchArgs) { + throw Error(`Invalid debug config "${debugConfig.name}"`); + } + launchArgs = await this.launchResolver.resolveDebugConfigurationWithSubstitutedVariables( + workspaceFolder, + launchArgs, + options.token, ); if (!launchArgs) { throw Error(`Invalid debug config "${debugConfig.name}"`); } launchArgs.request = 'launch'; - return launchArgs!; - } - - private fixArgs(args: string[], testProvider: TestProvider): string[] { - if (testProvider === 'unittest') { - return args.filter(item => item !== '--debug'); + if (options.pytestPort && options.runTestIdsPort) { + launchArgs.env = { + ...launchArgs.env, + TEST_RUN_PIPE: options.pytestPort, + RUN_TEST_IDS_PIPE: options.runTestIdsPort, + }; } else { - return args; + throw Error( + `Missing value for debug setup, both port and uuid need to be defined. port: "${options.pytestPort}" uuid: "${options.pytestUUID}"`, + ); } + + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + // check if PYTHONPATH is already set in the environment variables + if (launchArgs.env) { + const additionalPythonPath = [pluginPath]; + if (launchArgs.cwd) { + additionalPythonPath.push(launchArgs.cwd); + } else if (options.cwd) { + additionalPythonPath.push(options.cwd); + } + // add the plugin path or cwd to PYTHONPATH if it is not already there using the following function + // this function will handle if PYTHONPATH is undefined + addPathToPythonpath(additionalPythonPath, launchArgs.env.PYTHONPATH); + } + + // Clear out purpose so we can detect if the configuration was used to + // run via F5 style debugging. + launchArgs.purpose = []; + + // For project-based execution, get the Python path from the project's environment. + // Fallback: if env API unavailable or fails, LaunchConfigurationResolver already set + // launchArgs.python from the active interpreter, so debugging still works. + if (options.project && envExtApi.useEnvExtension()) { + try { + const pythonEnv = await envExtApi.getEnvironment(options.project.uri); + if (pythonEnv?.execInfo?.run?.executable) { + launchArgs.python = pythonEnv.execInfo.run.executable; + traceVerbose( + `[test-by-project] Debug session using Python path from project: ${launchArgs.python}`, + ); + } + } catch (error) { + traceVerbose(`[test-by-project] Could not get environment for project, using default: ${error}`); + } + } + + return launchArgs; } - private getTestLauncherScript(testProvider: TestProvider) { + private static getTestLauncherScript(testProvider: TestProvider) { switch (testProvider) { case 'unittest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); + return internalScripts.execution_py_testlauncher; // this is the new way to run unittest execution, debugger } - case 'pytest': - case 'nosetest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); + 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/managers/baseTestManager.ts b/src/client/testing/common/managers/baseTestManager.ts deleted file mode 100644 index 872da7577d78..000000000000 --- a/src/client/testing/common/managers/baseTestManager.ts +++ /dev/null @@ -1,441 +0,0 @@ -import { - CancellationToken, - CancellationTokenSource, - Diagnostic, - DiagnosticCollection, - DiagnosticRelatedInformation, - Disposable, - Event, - EventEmitter, - languages, - OutputChannel, - Uri -} from 'vscode'; -import { ICommandManager, IWorkspaceService } from '../../../common/application/types'; -import '../../../common/extensions'; -import { isNotInstalledError } from '../../../common/helpers'; -import { traceError } from '../../../common/logger'; -import { IFileSystem } from '../../../common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IInstaller, IOutputChannel, IPythonSettings, Product } from '../../../common/types'; -import { getNamesAndValues } from '../../../common/utils/enum'; -import { noop } from '../../../common/utils/misc'; -import { IServiceContainer } from '../../../ioc/types'; -import { EventName } from '../../../telemetry/constants'; -import { sendTelemetryEvent } from '../../../telemetry/index'; -import { TestDiscoverytTelemetry, TestRunTelemetry } from '../../../telemetry/types'; -import { IPythonTestMessage, ITestDiagnosticService, WorkspaceTestStatus } from '../../types'; -import { copyDesiredTestResults } from '../testUtils'; -import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './../constants'; -import { - ITestCollectionStorageService, - ITestDiscoveryService, - ITestManager, - ITestResultsService, - ITestsHelper, - ITestsStatusUpdaterService, - TestDiscoveryOptions, - TestProvider, - Tests, - TestStatus, - TestsToRun -} from './../types'; - -enum CancellationTokenType { - testDiscovery, - testRunner -} - -// tslint:disable: member-ordering max-func-body-length - -export abstract class BaseTestManager implements ITestManager { - public diagnosticCollection: DiagnosticCollection; - protected readonly settings: IPythonSettings; - private readonly unitTestDiagnosticService: ITestDiagnosticService; - public abstract get enabled(): boolean; - protected get outputChannel() { - return this._outputChannel; - } - protected get testResultsService() { - return this._testResultsService; - } - private readonly testCollectionStorage: ITestCollectionStorageService; - private readonly _testResultsService: ITestResultsService; - private readonly commandManager: ICommandManager; - private readonly workspaceService: IWorkspaceService; - private readonly _outputChannel: OutputChannel; - protected tests?: Tests; - private _status: TestStatus = TestStatus.Unknown; - private testDiscoveryCancellationTokenSource?: CancellationTokenSource; - private testRunnerCancellationTokenSource?: CancellationTokenSource; - private _installer!: IInstaller; - private readonly testsStatusUpdaterService: ITestsStatusUpdaterService; - private discoverTestsPromise?: Promise; - private readonly _onDidStatusChange = new EventEmitter(); - private get installer(): IInstaller { - if (!this._installer) { - this._installer = this.serviceContainer.get(IInstaller); - } - return this._installer; - } - constructor( - public readonly testProvider: TestProvider, - private readonly product: Product, - public readonly workspaceFolder: Uri, - protected rootDirectory: string, - protected serviceContainer: IServiceContainer - ) { - this.updateStatus(TestStatus.Unknown); - const configService = serviceContainer.get(IConfigurationService); - this.settings = configService.getSettings(this.rootDirectory ? Uri.file(this.rootDirectory) : undefined); - const disposables = serviceContainer.get(IDisposableRegistry); - this._outputChannel = this.serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); - this._testResultsService = this.serviceContainer.get(ITestResultsService); - this.workspaceService = this.serviceContainer.get(IWorkspaceService); - this.diagnosticCollection = languages.createDiagnosticCollection(this.testProvider); - this.unitTestDiagnosticService = serviceContainer.get(ITestDiagnosticService); - this.testsStatusUpdaterService = serviceContainer.get(ITestsStatusUpdaterService); - this.commandManager = serviceContainer.get(ICommandManager); - disposables.push(this); - } - protected get testDiscoveryCancellationToken(): CancellationToken | undefined { - return this.testDiscoveryCancellationTokenSource ? this.testDiscoveryCancellationTokenSource.token : undefined; - } - protected get testRunnerCancellationToken(): CancellationToken | undefined { - return this.testRunnerCancellationTokenSource ? this.testRunnerCancellationTokenSource.token : undefined; - } - public dispose() { - this.stop(); - } - public get status(): TestStatus { - return this._status; - } - public get onDidStatusChange(): Event { - return this._onDidStatusChange.event; - } - public get workingDirectory(): string { - return this.settings.testing.cwd && this.settings.testing.cwd.length > 0 ? this.settings.testing.cwd : this.rootDirectory; - } - public stop() { - if (this.testDiscoveryCancellationTokenSource) { - this.testDiscoveryCancellationTokenSource.cancel(); - } - if (this.testRunnerCancellationTokenSource) { - this.testRunnerCancellationTokenSource.cancel(); - } - } - public reset() { - this.tests = undefined; - this.updateStatus(TestStatus.Unknown); - } - public resetTestResults() { - if (!this.tests) { - return; - } - - this.testResultsService.resetResults(this.tests!); - } - public async discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false, clearTestStatus: boolean = false): Promise { - if (this.discoverTestsPromise) { - return this.discoverTestsPromise; - } - this.discoverTestsPromise = this._discoverTests(cmdSource, ignoreCache, quietMode, userInitiated, clearTestStatus); - this.discoverTestsPromise.catch(noop).then(() => this.discoverTestsPromise = undefined).ignoreErrors(); - return this.discoverTestsPromise; - } - private async _discoverTests(cmdSource: CommandSource, ignoreCache: boolean = false, quietMode: boolean = false, userInitiated: boolean = false, clearTestStatus: boolean = false): Promise { - if (!ignoreCache && this.tests! && this.tests!.testFunctions.length > 0) { - this.updateStatus(TestStatus.Idle); - return Promise.resolve(this.tests!); - } - if (userInitiated) { - this.testsStatusUpdaterService.updateStatusAsDiscovering(this.workspaceFolder, this.tests); - } - this.updateStatus(TestStatus.Discovering); - // If ignoreCache is true, its an indication of the fact that its a user invoked operation. - // Hence we can stop the debugger. - if (userInitiated) { - this.stop(); - } - const telementryProperties: TestDiscoverytTelemetry = { - tool: this.testProvider, - // tslint:disable-next-line:no-any prefer-type-cast - trigger: cmdSource as any, - failed: false - }; - this.commandManager.executeCommand('setContext', 'testsDiscovered', true).then(noop, noop); - this.createCancellationToken(CancellationTokenType.testDiscovery); - const discoveryOptions = this.getDiscoveryOptions(ignoreCache); - const discoveryService = this.serviceContainer.get(ITestDiscoveryService, this.testProvider); - return discoveryService - .discoverTests(discoveryOptions) - .then(tests => { - const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; - const existingTests = this.testCollectionStorage.getTests(wkspace)!; - if (clearTestStatus) { - this.resetTestResults(); - } else if (existingTests) { - copyDesiredTestResults(existingTests, tests); - this._testResultsService.updateResults(tests); - } - this.testCollectionStorage.storeTests(wkspace, tests); - this.tests = tests; - this.updateStatus(TestStatus.Idle); - this.discoverTestsPromise = undefined; - - // have errors in Discovering - let haveErrorsInDiscovering = false; - tests.testFiles.forEach(file => { - if (file.errorsWhenDiscovering && file.errorsWhenDiscovering.length > 0) { - haveErrorsInDiscovering = true; - this.outputChannel.append('_'.repeat(10)); - this.outputChannel.append(`There was an error in identifying unit tests in ${file.nameToRun}`); - this.outputChannel.appendLine('_'.repeat(10)); - this.outputChannel.appendLine(file.errorsWhenDiscovering); - } - }); - if (haveErrorsInDiscovering && !quietMode) { - const testsHelper = this.serviceContainer.get(ITestsHelper); - testsHelper.displayTestErrorMessage('There were some errors in discovering unit tests'); - } - this.disposeCancellationToken(CancellationTokenType.testDiscovery); - sendTelemetryEvent(EventName.UNITTEST_DISCOVER, undefined, telementryProperties); - return tests; - }) - .catch((reason: {}) => { - if (userInitiated) { - this.testsStatusUpdaterService.updateStatusAsUnknown(this.workspaceFolder, this.tests); - } - if (isNotInstalledError(reason as Error) && !quietMode) { - this.installer.promptToInstall(this.product, this.workspaceFolder) - .catch(ex => traceError('isNotInstalledError', ex)); - } - - this.tests = undefined; - this.discoverTestsPromise = undefined; - if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { - reason = CANCELLATION_REASON; - this.updateStatus(TestStatus.Idle); - } else { - telementryProperties.failed = true; - sendTelemetryEvent(EventName.UNITTEST_DISCOVER, undefined, telementryProperties); - this.updateStatus(TestStatus.Error); - this.outputChannel.appendLine('Test Discovery failed: '); - this.outputChannel.appendLine(reason.toString()); - } - const wkspace = this.workspaceService.getWorkspaceFolder(Uri.file(this.rootDirectory))!.uri; - this.testCollectionStorage.storeTests(wkspace, undefined); - this.disposeCancellationToken(CancellationTokenType.testDiscovery); - return Promise.reject(reason); - }); - } - public async runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - const moreInfo = { - Test_Provider: this.testProvider, - Run_Failed_Tests: 'false', - Run_Specific_File: 'false', - Run_Specific_Class: 'false', - Run_Specific_Function: 'false' - }; - //Ensure valid values are sent. - const validCmdSourceValues = getNamesAndValues(CommandSource).map(item => item.value); - const telementryProperties: TestRunTelemetry = { - tool: this.testProvider, - scope: 'all', - debugging: debug === true, - triggerSource: validCmdSourceValues.indexOf(cmdSource) === -1 ? 'commandpalette' : cmdSource, - failed: false - }; - - if (!runFailedTests && !testsToRun) { - this.testsStatusUpdaterService.updateStatusAsRunning(this.workspaceFolder, this.tests); - } - - this.updateStatus(TestStatus.Running); - if (this.testRunnerCancellationTokenSource) { - this.testRunnerCancellationTokenSource.cancel(); - } - - if (runFailedTests === true) { - moreInfo.Run_Failed_Tests = runFailedTests.toString(); - telementryProperties.scope = 'failed'; - this.testsStatusUpdaterService.updateStatusAsRunningFailedTests(this.workspaceFolder, this.tests); - } - if (testsToRun && typeof testsToRun === 'object') { - if (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) { - telementryProperties.scope = 'file'; - moreInfo.Run_Specific_File = 'true'; - } - if (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) { - telementryProperties.scope = 'class'; - moreInfo.Run_Specific_Class = 'true'; - } - if (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) { - telementryProperties.scope = 'function'; - moreInfo.Run_Specific_Function = 'true'; - } - this.testsStatusUpdaterService.updateStatusAsRunningSpecificTests(this.workspaceFolder, testsToRun, this.tests); - } - - this.testsStatusUpdaterService.triggerUpdatesToTests(this.workspaceFolder, this.tests); - // If running failed tests, then don't clear the previously build UnitTests - // If we do so, then we end up re-discovering the unit tests and clearing previously cached list of failed tests - // Similarly, if running a specific test or test file, don't clear the cache (possible tests have some state information retained) - const clearDiscoveredTestCache = runFailedTests || moreInfo.Run_Specific_File || moreInfo.Run_Specific_Class || moreInfo.Run_Specific_Function ? false : true; - return this.discoverTests(cmdSource, clearDiscoveredTestCache, true, true) - .catch(reason => { - if (this.testDiscoveryCancellationToken && this.testDiscoveryCancellationToken.isCancellationRequested) { - return Promise.reject(reason); - } - const testsHelper = this.serviceContainer.get(ITestsHelper); - testsHelper.displayTestErrorMessage('Errors in discovering tests, continuing with tests'); - return { - rootTestFolders: [], - testFiles: [], - testFolders: [], - testFunctions: [], - testSuites: [], - summary: { errors: 0, failures: 0, passed: 0, skipped: 0 } - }; - }) - .then(tests => { - this.updateStatus(TestStatus.Running); - this.createCancellationToken(CancellationTokenType.testRunner); - return this.runTestImpl(tests, testsToRun, runFailedTests, debug); - }) - .then(() => { - this.updateStatus(TestStatus.Idle); - this.disposeCancellationToken(CancellationTokenType.testRunner); - sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, telementryProperties); - this.testsStatusUpdaterService.updateStatusOfRunningTestsAsIdle(this.workspaceFolder, this.tests); - this.testsStatusUpdaterService.triggerUpdatesToTests(this.workspaceFolder, this.tests); - return this.tests!; - }) - .catch(reason => { - this.testsStatusUpdaterService.updateStatusOfRunningTestsAsIdle(this.workspaceFolder, this.tests); - this.testsStatusUpdaterService.triggerUpdatesToTests(this.workspaceFolder, this.tests); - if (this.testRunnerCancellationToken && this.testRunnerCancellationToken.isCancellationRequested) { - reason = CANCELLATION_REASON; - this.updateStatus(TestStatus.Idle); - } else { - this.updateStatus(TestStatus.Error); - telementryProperties.failed = true; - sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, telementryProperties); - } - this.disposeCancellationToken(CancellationTokenType.testRunner); - return Promise.reject(reason); - }); - } - public async updateDiagnostics(tests: Tests, messages: IPythonTestMessage[]): Promise { - await this.stripStaleDiagnostics(tests, messages); - - // Update relevant file diagnostics for tests that have problems. - const uniqueMsgFiles = messages.reduce((filtered, msg) => { - if (filtered.indexOf(msg.testFilePath) === -1 && msg.testFilePath !== undefined) { - filtered.push(msg.testFilePath); - } - return filtered; - }, []); - const fs = this.serviceContainer.get(IFileSystem); - for (const msgFile of uniqueMsgFiles) { - // Check all messages against each test file. - const fileUri = Uri.file(msgFile); - if (!this.diagnosticCollection.has(fileUri)) { - // Create empty diagnostic for file URI so the rest of the logic can assume one already exists. - const diagnostics: Diagnostic[] = []; - this.diagnosticCollection.set(fileUri, diagnostics); - } - // Get the diagnostics for this file's URI before updating it so old tests that weren't run can still show problems. - const oldDiagnostics = this.diagnosticCollection.get(fileUri)!; - const newDiagnostics: Diagnostic[] = []; - for (const diagnostic of oldDiagnostics) { - newDiagnostics.push(diagnostic); - } - for (const msg of messages) { - if (fs.arePathsSame(fileUri.fsPath, Uri.file(msg.testFilePath).fsPath) && msg.status !== TestStatus.Pass) { - const diagnostic = this.createDiagnostics(msg); - newDiagnostics.push(diagnostic); - } - } - - // Set the diagnostics for the file. - this.diagnosticCollection.set(fileUri, newDiagnostics); - } - } - protected abstract runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; - protected abstract getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions; - private updateStatus(status: TestStatus): void { - this._status = status; - // Fire after 1ms, let existing code run to completion, - // We need to allow for code to get into a consistent state. - setTimeout(() => this._onDidStatusChange.fire({ workspace: this.workspaceFolder, status }), 1); - } - private createCancellationToken(tokenType: CancellationTokenType) { - this.disposeCancellationToken(tokenType); - if (tokenType === CancellationTokenType.testDiscovery) { - this.testDiscoveryCancellationTokenSource = new CancellationTokenSource(); - } else { - this.testRunnerCancellationTokenSource = new CancellationTokenSource(); - } - } - private disposeCancellationToken(tokenType: CancellationTokenType) { - if (tokenType === CancellationTokenType.testDiscovery) { - if (this.testDiscoveryCancellationTokenSource) { - this.testDiscoveryCancellationTokenSource.dispose(); - } - this.testDiscoveryCancellationTokenSource = undefined; - } else { - if (this.testRunnerCancellationTokenSource) { - this.testRunnerCancellationTokenSource.dispose(); - } - this.testRunnerCancellationTokenSource = undefined; - } - } - /** - * Whenever a test is run, any previous problems it had should be removed. This runs through - * every already existing set of diagnostics for any that match the tests that were just run - * so they can be stripped out (as they are now no longer relevant). If the tests pass, then - * there is no need to have a diagnostic for it. If they fail, the stale diagnostic will be - * replaced by an up-to-date diagnostic showing the most recent problem with that test. - * - * In order to identify diagnostics associated with the tests that were run, the `nameToRun` - * property of each messages is compared to the `code` property of each diagnostic. - * - * @param messages Details about the tests that were just run. - */ - private async stripStaleDiagnostics(tests: Tests, messages: IPythonTestMessage[]): Promise { - this.diagnosticCollection.forEach((diagnosticUri, oldDiagnostics, collection) => { - const newDiagnostics: Diagnostic[] = []; - for (const diagnostic of oldDiagnostics) { - const matchingMsg = messages.find(msg => msg.code === diagnostic.code); - if (matchingMsg === undefined) { - // No matching message was found, so this test was not included in the test run. - const matchingTest = tests.testFunctions.find(tf => tf.testFunction.nameToRun === diagnostic.code); - if (matchingTest !== undefined) { - // Matching test was found, so the diagnostic is still relevant. - newDiagnostics.push(diagnostic); - } - } - } - // Set the diagnostics for the file. - collection.set(diagnosticUri, newDiagnostics); - }); - } - - private createDiagnostics(message: IPythonTestMessage): Diagnostic { - const stackStart = message.locationStack![0]; - const diagPrefix = this.unitTestDiagnosticService.getMessagePrefix(message.status!); - const severity = this.unitTestDiagnosticService.getSeverity(message.severity)!; - const diagMsg = message.message ? message.message.split('\n')[0] : ''; - const diagnostic = new Diagnostic(stackStart.location.range, `${diagPrefix ? `${diagPrefix}: ` : ''}${diagMsg}`, severity); - diagnostic.code = message.code; - diagnostic.source = message.provider; - const relatedInfoArr: DiagnosticRelatedInformation[] = []; - for (const frameDetails of message.locationStack!) { - const relatedInfo = new DiagnosticRelatedInformation(frameDetails.location, frameDetails.lineText); - relatedInfoArr.push(relatedInfo); - } - diagnostic.relatedInformation = relatedInfoArr; - return diagnostic; - } -} diff --git a/src/client/testing/common/managers/testConfigurationManager.ts b/src/client/testing/common/managers/testConfigurationManager.ts deleted file mode 100644 index 95983316f902..000000000000 --- a/src/client/testing/common/managers/testConfigurationManager.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as path from 'path'; -import { OutputChannel, QuickPickItem, Uri } from 'vscode'; -import { IApplicationShell } from '../../../common/application/types'; -import { IInstaller, ILogger, IOutputChannel } from '../../../common/types'; -import { createDeferred } from '../../../common/utils/async'; -import { getSubDirectories } from '../../../common/utils/fs'; -import { IServiceContainer } from '../../../ioc/types'; -import { ITestConfigSettingsService, ITestConfigurationManager } from '../../types'; -import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../constants'; -import { UnitTestProduct } from '../types'; - -export abstract class TestConfigurationManager implements ITestConfigurationManager { - protected readonly outputChannel: OutputChannel; - protected readonly installer: IInstaller; - protected readonly testConfigSettingsService: ITestConfigSettingsService; - constructor(protected workspace: Uri, - protected product: UnitTestProduct, - protected readonly serviceContainer: IServiceContainer, - cfg?: ITestConfigSettingsService - ) { - this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.installer = serviceContainer.get(IInstaller); - this.testConfigSettingsService = cfg ? cfg : serviceContainer.get(ITestConfigSettingsService); - } - public abstract configure(wkspace: Uri): Promise; - public abstract requiresUserToConfigure(wkspace: Uri): Promise; - public async enable() { - // Disable other test frameworks. - await Promise.all(UNIT_TEST_PRODUCTS - .filter(prod => prod !== this.product) - .map(prod => this.testConfigSettingsService.disable(this.workspace, prod))); - await this.testConfigSettingsService.enable(this.workspace, this.product); - } - // tslint:disable-next-line:no-any - public async disable() { - return this.testConfigSettingsService.enable(this.workspace, this.product); - } - protected selectTestDir(rootDir: string, subDirs: string[], customOptions: QuickPickItem[] = []): Promise { - const options = { - ignoreFocusOut: true, - matchOnDescription: true, - matchOnDetail: true, - placeHolder: 'Select the directory containing the tests' - }; - let items: QuickPickItem[] = subDirs - .map(dir => { - const dirName = path.relative(rootDir, dir); - if (dirName.indexOf('.') === 0) { - return; - } - return { - label: dirName, - description: '' - }; - }) - .filter(item => item !== undefined) - .map(item => item!); - - items = [{ label: '.', description: 'Root directory' }, ...items]; - items = customOptions.concat(items); - const def = createDeferred(); - const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showQuickPick(items, options).then(item => { - if (!item) { - this.handleCancelled(); // This will throw an exception. - return; - } - - def.resolve(item.label); - }); - - return def.promise; - } - - protected selectTestFilePattern(): Promise { - const options = { - ignoreFocusOut: true, - matchOnDescription: true, - matchOnDetail: true, - placeHolder: 'Select the pattern to identify test files' - }; - const items: QuickPickItem[] = [ - { label: '*test.py', description: 'Python Files ending with \'test\'' }, - { label: '*_test.py', description: 'Python Files ending with \'_test\'' }, - { label: 'test*.py', description: 'Python Files 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(); - const appShell = this.serviceContainer.get(IApplicationShell); - appShell.showQuickPick(items, options).then(item => { - if (!item) { - this.handleCancelled(); // This will throw an exception. - return; - } - - def.resolve(item.label); - }); - - return def.promise; - } - protected getTestDirs(rootDir: string): Promise { - 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; - }); - } - - private handleCancelled() { - const logger = this.serviceContainer.get(ILogger); - logger.logInformation('testing configuration (in UI) cancelled'); - throw Error('cancelled'); - } -} diff --git a/src/client/testing/common/runner.ts b/src/client/testing/common/runner.ts deleted file mode 100644 index df4d5262e200..000000000000 --- a/src/client/testing/common/runner.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { ErrorUtils } from '../../common/errors/errorUtils'; -import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - IPythonToolExecutionService, - ObservableExecutionResult, - SpawnOptions -} from '../../common/process/types'; -import { ExecutionInfo, IConfigurationService, IPythonSettings } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; -import { ITestRunner, ITestsHelper, Options, TestProvider } from './types'; -export { Options } from './types'; - -@injectable() -export class TestRunner implements ITestRunner { - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public run(testProvider: TestProvider, options: Options): Promise { - return run(this.serviceContainer, testProvider, options); - } -} - -export async function run(serviceContainer: IServiceContainer, testProvider: TestProvider, options: Options): Promise { - const testExecutablePath = getExecutablePath(testProvider, serviceContainer.get(IConfigurationService).getSettings(options.workspaceFolder)); - const moduleName = getTestModuleName(testProvider); - const spawnOptions = options as SpawnOptions; - let pythonExecutionServicePromise: Promise; - spawnOptions.mergeStdOutErr = typeof spawnOptions.mergeStdOutErr === 'boolean' ? spawnOptions.mergeStdOutErr : true; - - let promise: Promise>; - - // Since conda 4.4.0 we have found that running python code needs the environment activated. - // So if running an executable, there's no way we can activate, if its a module, then activate and run the module. - const testHelper = serviceContainer.get(ITestsHelper); - const executionInfo: ExecutionInfo = { - execPath: testExecutablePath, - args: options.args, - moduleName: testExecutablePath && testExecutablePath.length > 0 ? undefined : moduleName, - product: testHelper.parseProduct(testProvider) - }; - - if (testProvider === UNITTEST_PROVIDER) { - promise = serviceContainer.get(IPythonExecutionFactory).createActivatedEnvironment({ resource: options.workspaceFolder }) - .then(executionService => executionService.execObservable(options.args, { ...spawnOptions })); - } else if (typeof executionInfo.moduleName === 'string' && executionInfo.moduleName.length > 0) { - pythonExecutionServicePromise = serviceContainer.get(IPythonExecutionFactory).createActivatedEnvironment({ resource: options.workspaceFolder }); - promise = pythonExecutionServicePromise.then(executionService => executionService.execModuleObservable(executionInfo.moduleName!, executionInfo.args, options)); - } else { - const pythonToolsExecutionService = serviceContainer.get(IPythonToolExecutionService); - promise = pythonToolsExecutionService.execObservable(executionInfo, spawnOptions, options.workspaceFolder); - } - - return promise.then(result => { - return new Promise((resolve, reject) => { - let stdOut = ''; - let stdErr = ''; - result.out.subscribe(output => { - stdOut += output.out; - // If the test runner python module is not installed we'll have something in stderr. - // Hence track that separately and check at the end. - if (output.source === 'stderr') { - stdErr += output.out; - } - if (options.outChannel) { - options.outChannel.append(output.out); - } - }, reject, async () => { - // If the test runner python module is not installed we'll have something in stderr. - if (moduleName && pythonExecutionServicePromise && ErrorUtils.outputHasModuleNotInstalledError(moduleName, stdErr)) { - const pythonExecutionService = await pythonExecutionServicePromise; - const isInstalled = await pythonExecutionService.isModuleInstalled(moduleName); - if (!isInstalled) { - return reject(new ModuleNotInstalledError(moduleName)); - } - } - resolve(stdOut); - }); - }); - }); -} - -function getExecutablePath(testProvider: TestProvider, settings: IPythonSettings): string | undefined { - let testRunnerExecutablePath: string | undefined; - switch (testProvider) { - case NOSETEST_PROVIDER: { - testRunnerExecutablePath = settings.testing.nosetestPath; - break; - } - case PYTEST_PROVIDER: { - testRunnerExecutablePath = settings.testing.pytestPath; - break; - } - default: { - return undefined; - } - } - return path.basename(testRunnerExecutablePath) === testRunnerExecutablePath ? undefined : testRunnerExecutablePath; -} -function getTestModuleName(testProvider: TestProvider) { - switch (testProvider) { - case NOSETEST_PROVIDER: { - return 'nose'; - } - case PYTEST_PROVIDER: { - return 'pytest'; - } - case UNITTEST_PROVIDER: { - return 'unittest'; - } - default: { - throw new Error(`Test provider '${testProvider}' not supported`); - } - } -} diff --git a/src/client/testing/common/services/configSettingService.ts b/src/client/testing/common/services/configSettingService.ts deleted file mode 100644 index 6a765d737d26..000000000000 --- a/src/client/testing/common/services/configSettingService.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { Uri, WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { Product } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { ITestConfigSettingsService } from '../../types'; -import { UnitTestProduct } from './../types'; - -@injectable() -export class TestConfigSettingsService implements ITestConfigSettingsService { - private readonly workspaceService: IWorkspaceService; - constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { - this.workspaceService = serviceContainer.get(IWorkspaceService); - } - public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { - const setting = this.getTestArgSetting(product); - return this.updateSetting(testDirectory, setting, args); - } - - public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { - const setting = this.getTestEnablingSetting(product); - return this.updateSetting(testDirectory, setting, true); - } - - public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { - const setting = this.getTestEnablingSetting(product); - return this.updateSetting(testDirectory, setting, false); - } - private getTestArgSetting(product: UnitTestProduct) { - switch (product) { - case Product.unittest: - return 'testing.unittestArgs'; - case Product.pytest: - return 'testing.pytestArgs'; - case Product.nosetest: - return 'testing.nosetestArgs'; - default: - throw new Error('Invalid Test Product'); - } - } - private getTestEnablingSetting(product: UnitTestProduct) { - switch (product) { - case Product.unittest: - return 'testing.unittestEnabled'; - case Product.pytest: - return 'testing.pytestEnabled'; - case Product.nosetest: - return 'testing.nosetestsEnabled'; - default: - throw new Error('Invalid Test Product'); - } - } - // tslint:disable-next-line:no-any - private async updateSetting(testDirectory: string | Uri, setting: string, value: any) { - let pythonConfig: WorkspaceConfiguration; - const resource = typeof testDirectory === 'string' ? Uri.file(testDirectory) : testDirectory; - if (!this.workspaceService.hasWorkspaceFolders) { - pythonConfig = this.workspaceService.getConfiguration('python'); - } else if (this.workspaceService.workspaceFolders!.length === 1) { - pythonConfig = this.workspaceService.getConfiguration('python', this.workspaceService.workspaceFolders![0].uri); - } else { - const workspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - if (!workspaceFolder) { - throw new Error(`Test directory does not belong to any workspace (${testDirectory})`); - } - // tslint:disable-next-line:no-non-null-assertion - pythonConfig = this.workspaceService.getConfiguration('python', workspaceFolder!.uri); - } - - return pythonConfig.update(setting, value); - } -} - -export class BufferedTestConfigSettingsService implements ITestConfigSettingsService { - private ops: [string, string | Uri, UnitTestProduct, string[]][]; - constructor() { - this.ops = []; - } - - public async updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]) { - this.ops.push(['updateTestArgs', testDirectory, product, args]); - } - - public async enable(testDirectory: string | Uri, product: UnitTestProduct): Promise { - this.ops.push(['enable', testDirectory, product, []]); - } - - public async disable(testDirectory: string | Uri, product: UnitTestProduct): Promise { - this.ops.push(['disable', testDirectory, product, []]); - } - - public async apply(cfg: ITestConfigSettingsService) { - const ops = this.ops; - this.ops = []; - // Note that earlier ops do not get rolled back if a later - // one fails. - for (const [op, testDir, prod, args] of ops) { - switch (op) { - case 'updateTestArgs': - await cfg.updateTestArgs(testDir, prod, args); - break; - case 'enable': - await cfg.enable(testDir, prod); - break; - case 'disable': - await cfg.disable(testDir, prod); - break; - default: - break; - } - } - } -} diff --git a/src/client/testing/common/services/contextService.ts b/src/client/testing/common/services/contextService.ts deleted file mode 100644 index 87d8d057b386..000000000000 --- a/src/client/testing/common/services/contextService.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 { ICommandManager } from '../../../common/application/types'; -import { ContextKey } from '../../../common/contextKey'; -import { IDisposable } from '../../../common/types'; -import { swallowExceptions } from '../../../common/utils/decorators'; -import { ITestManagementService, WorkspaceTestStatus } from '../../types'; -import { ITestCollectionStorageService, ITestContextService, TestStatus } from '../types'; - -@injectable() -export class TestContextService implements ITestContextService { - private readonly hasFailedTests: ContextKey; - private readonly runningTests: ContextKey; - private readonly discoveringTests: ContextKey; - private readonly busyTests: ContextKey; - private readonly disposables: IDisposable[] = []; - constructor( - @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService, - @inject(ITestManagementService) private readonly testManager: ITestManagementService, - @inject(ICommandManager) cmdManager: ICommandManager - ) { - this.hasFailedTests = new ContextKey('hasFailedTests', cmdManager); - this.runningTests = new ContextKey('runningTests', cmdManager); - this.discoveringTests = new ContextKey('discoveringTests', cmdManager); - this.busyTests = new ContextKey('busyTests', cmdManager); - } - public dispose(): void { - this.disposables.forEach(d => d.dispose()); - } - public register(): void { - this.testManager.onDidStatusChange(this.onStatusChange, this, this.disposables); - } - @swallowExceptions('Handle status change of tests') - protected async onStatusChange(status: WorkspaceTestStatus): Promise { - const tests = this.storage.getTests(status.workspace); - const promises: Promise[] = []; - if (tests && tests.summary) { - promises.push(this.hasFailedTests.set(tests.summary.failures > 0)); - } - promises.push(...[ - this.runningTests.set(status.status === TestStatus.Running), - this.discoveringTests.set(status.status === TestStatus.Discovering), - this.busyTests.set(status.status === TestStatus.Running || status.status === TestStatus.Discovering) - ]); - - await Promise.all(promises); - } -} diff --git a/src/client/testing/common/services/discoveredTestParser.ts b/src/client/testing/common/services/discoveredTestParser.ts deleted file mode 100644 index ed123270b12e..000000000000 --- a/src/client/testing/common/services/discoveredTestParser.ts +++ /dev/null @@ -1,274 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { traceError } from '../../../common/logger'; -import { TestDataItem } from '../../types'; -import { getParentFile, getParentSuite, getTestType } from '../testUtils'; -import { FlattenedTestFunction, FlattenedTestSuite, SubtestParent, TestFile, TestFolder, TestFunction, Tests, TestSuite, TestType } from '../types'; -import { DiscoveredTests, ITestDiscoveredTestParser, TestContainer, TestItem } from './types'; - -@injectable() -export class TestDiscoveredTestParser implements ITestDiscoveredTestParser { - constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) { } - public parse(resource: Uri, discoveredTests: DiscoveredTests[]): Tests { - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, failures: 0, passed: 0, skipped: 0 }, - testFiles: [], - testFolders: [], - testFunctions: [], - testSuites: [] - }; - - const workspace = this.workspaceService.getWorkspaceFolder(resource); - if (!workspace) { - traceError('Resource does not belong to any workspace folder'); - return tests; - } - - // If the root is the workspace folder, then ignore that. - for (const data of discoveredTests) { - const rootFolder = { - name: data.root, folders: [], time: 0, - testFiles: [], resource: resource, nameToRun: data.rootid - }; - tests.rootTestFolders.push(rootFolder); - tests.testFolders.push(rootFolder); - this.buildChildren(rootFolder, rootFolder, data, tests); - } - - return tests; - } - /** - * Not the best solution to use `case statements`, but it keeps the code simple and easy to read in one place. - * Could go with separate classes for each type and use stratergies, but that just ends up a class for - * 10 lines of code. Hopefully this is more readable and maintainable than having multiple classes for - * the simple processing of the children. - * - * @protected - * @param {TestFolder} rootFolder - * @param {TestDataItem} parent - * @param {DiscoveredTests} discoveredTests - * @param {Tests} tests - * @memberof TestsDiscovery - */ - protected buildChildren(rootFolder: TestFolder, parent: TestDataItem, discoveredTests: DiscoveredTests, tests: Tests) { - const parentType = getTestType(parent); - switch (parentType) { - case TestType.testFolder: { - this.processFolder(rootFolder, parent as TestFolder, discoveredTests, tests); - break; - } - case TestType.testFile: { - this.processFile(rootFolder, parent as TestFile, discoveredTests, tests); - break; - } - case TestType.testSuite: { - this.processSuite(rootFolder, parent as TestSuite, discoveredTests, tests); - break; - } - default: - break; - } - } - /** - * Process the children of a folder. - * A folder can only contain other folders and files. - * Hence limit processing to those items. - * - * @protected - * @param {TestFolder} rootFolder - * @param {TestFolder} parentFolder - * @param {DiscoveredTests} discoveredTests - * @param {Tests} tests - * @memberof TestDiscoveredTestParser - */ - protected processFolder(rootFolder: TestFolder, parentFolder: TestFolder, discoveredTests: DiscoveredTests, tests: Tests) { - const folders = discoveredTests.parents - .filter(child => child.kind === 'folder' && child.parentid === parentFolder.nameToRun) - .map(folder => createTestFolder(rootFolder, folder)); - - const files = discoveredTests.parents - .filter(child => child.kind === 'file' && child.parentid === parentFolder.nameToRun) - .map(file => createTestFile(rootFolder, file)); - - parentFolder.folders.push(...folders); - parentFolder.testFiles.push(...files); - tests.testFolders.push(...folders); - tests.testFiles.push(...files); - [...folders, ...files].forEach(item => this.buildChildren(rootFolder, item, discoveredTests, tests)); - } - /** - * Process the children of a file. - * A file can only contain suites, functions and paramerterized functions. - * Hence limit processing just to those items. - * - * @protected - * @param {TestFolder} rootFolder - * @param {TestFile} parentFile - * @param {DiscoveredTests} discoveredTests - * @param {Tests} tests - * @memberof TestDiscoveredTestParser - */ - protected processFile(rootFolder: TestFolder, parentFile: TestFile, discoveredTests: DiscoveredTests, tests: Tests) { - const suites = discoveredTests.parents - .filter(child => child.kind === 'suite' && child.parentid === parentFile.nameToRun) - .map(suite => createTestSuite(parentFile, rootFolder.resource, suite)); - - const functions = discoveredTests.tests - .filter(func => func.parentid === parentFile.nameToRun) - .map(func => createTestFunction(rootFolder, func)); - - parentFile.suites.push(...suites); - parentFile.functions.push(...functions); - tests.testSuites.push(...suites.map(suite => createFlattenedSuite(tests, suite))); - tests.testFunctions.push(...functions.map(func => createFlattenedFunction(tests, func))); - suites.forEach(item => this.buildChildren(rootFolder, item, discoveredTests, tests)); - - const parameterizedFunctions = discoveredTests.parents - .filter(child => child.kind === 'function' && child.parentid === parentFile.nameToRun) - .map(func => createParameterizedTestFunction(rootFolder, func)); - parameterizedFunctions.forEach(func => this.processParameterizedFunction(rootFolder, parentFile, func, discoveredTests, tests)); - } - /** - * Process the children of a suite. - * A suite can only contain suites, functions and paramerterized functions. - * Hence limit processing just to those items. - * - * @protected - * @param {TestFolder} rootFolder - * @param {TestSuite} parentSuite - * @param {DiscoveredTests} discoveredTests - * @param {Tests} tests - * @memberof TestDiscoveredTestParser - */ - protected processSuite(rootFolder: TestFolder, parentSuite: TestSuite, discoveredTests: DiscoveredTests, tests: Tests) { - const suites = discoveredTests.parents - .filter(child => child.kind === 'suite' && child.parentid === parentSuite.nameToRun) - .map(suite => createTestSuite(parentSuite, rootFolder.resource, suite)); - - const functions = discoveredTests.tests - .filter(func => func.parentid === parentSuite.nameToRun) - .map(func => createTestFunction(rootFolder, func)); - - parentSuite.suites.push(...suites); - parentSuite.functions.push(...functions); - tests.testSuites.push(...suites.map(suite => createFlattenedSuite(tests, suite))); - tests.testFunctions.push(...functions.map(func => createFlattenedFunction(tests, func))); - suites.forEach(item => this.buildChildren(rootFolder, item, discoveredTests, tests)); - - const parameterizedFunctions = discoveredTests.parents - .filter(child => child.kind === 'function' && child.parentid === parentSuite.nameToRun) - .map(func => createParameterizedTestFunction(rootFolder, func)); - parameterizedFunctions.forEach(func => this.processParameterizedFunction(rootFolder, parentSuite, func, discoveredTests, tests)); - } - /** - * Process the children of a parameterized function. - * A parameterized function can only contain functions (in tests). - * Hence limit processing just to those items. - * - * @protected - * @param {TestFolder} rootFolder - * @param {TestFunction} parentFunction - * @param {DiscoveredTests} discoveredTests - * @param {Tests} tests - * @returns - * @memberof TestDiscoveredTestParser - */ - protected processParameterizedFunction(rootFolder: TestFolder, parent: TestFile | TestSuite, parentFunction: SubtestParent, discoveredTests: DiscoveredTests, tests: Tests) { - if (!parentFunction.asSuite) { - return; - } - const functions = discoveredTests.tests - .filter(func => func.parentid === parentFunction.nameToRun) - .map(func => createTestFunction(rootFolder, func)); - functions.map(func => func.subtestParent = parentFunction); - parentFunction.asSuite.functions.push(...functions); - parent.functions.push(...functions); - tests.testFunctions.push(...functions.map(func => createFlattenedParameterizedFunction(tests, func, parent))); - } -} - -function createTestFolder(root: TestFolder, item: TestContainer): TestFolder { - return { - name: item.name, nameToRun: item.id, resource: root.resource, time: 0, folders: [], testFiles: [] - }; -} -function createTestFile(root: TestFolder, item: TestContainer): TestFile { - const fullyQualifiedName = path.isAbsolute(item.id) ? item.id : path.resolve(root.name, item.id); - return { - fullPath: fullyQualifiedName, functions: [], name: item.name, - nameToRun: item.id, resource: root.resource, suites: [], time: 0, xmlName: createXmlName(item.id) - }; -} -function createTestSuite(parentSuiteFile: TestFile | TestSuite, resource: Uri, item: TestContainer): TestSuite { - const suite = { - functions: [], name: item.name, nameToRun: item.id, resource: resource, - suites: [], time: 0, xmlName: '', isInstance: false, isUnitTest: false - }; - suite.xmlName = `${parentSuiteFile.xmlName}.${item.name}`; - return suite; -} -function createFlattenedSuite(tests: Tests, suite: TestSuite): FlattenedTestSuite { - const parentFile = getParentFile(tests, suite); - return { - parentTestFile: parentFile, testSuite: suite, xmlClassName: parentFile.xmlName - }; -} -function createFlattenedParameterizedFunction(tests: Tests, func: TestFunction, parent: TestFile | TestSuite): FlattenedTestFunction { - const type = getTestType(parent); - const parentFile = (type && type === TestType.testSuite) ? getParentFile(tests, func) : parent as TestFile; - const parentSuite = (type && type === TestType.testSuite) ? parent as TestSuite : undefined; - return { - parentTestFile: parentFile, parentTestSuite: parentSuite, - xmlClassName: parentSuite ? parentSuite.xmlName : parentFile.xmlName, testFunction: func - }; -} -function createFlattenedFunction(tests: Tests, func: TestFunction): FlattenedTestFunction { - const parent = getParentFile(tests, func); - const type = parent ? getTestType(parent) : undefined; - const parentFile = (type && type === TestType.testSuite) ? getParentFile(tests, func) : parent as TestFile; - const parentSuite = getParentSuite(tests, func); - return { - parentTestFile: parentFile, parentTestSuite: parentSuite, - xmlClassName: parentSuite ? parentSuite.xmlName : parentFile.xmlName, testFunction: func - }; -} -function createParameterizedTestFunction(root: TestFolder, item: TestContainer): SubtestParent { - const suite: TestSuite = { - functions: [], isInstance: false, isUnitTest: false, - name: item.name, nameToRun: item.id, resource: root.resource, - time: 0, suites: [], xmlName: '' - }; - return { - asSuite: suite, name: item.name, nameToRun: item.id, time: 0 - }; -} -function createTestFunction(root: TestFolder, item: TestItem): TestFunction { - return { - name: item.name, nameToRun: item.id, resource: root.resource, - time: 0, file: item.source.substr(0, item.source.lastIndexOf(':')) - }; -} -/** - * Creates something known as an Xml Name, used to identify items - * from an xunit test result. - * Once we have the test runner done in Python, this can be discarded. - * @param {string} fileId - * @returns - */ -function createXmlName(fileId: string) { - let name = path.join(path.dirname(fileId), path.basename(fileId, path.extname(fileId))); - name = name.replace(/\\/g, '.').replace(/\//g, '.'); - // Remove leading . & / & \ - while (name.startsWith('.') || name.startsWith('/') || name.startsWith('\\')) { - name = name.substring(1); - } - return name; -} diff --git a/src/client/testing/common/services/discovery.ts b/src/client/testing/common/services/discovery.ts deleted file mode 100644 index 41512ff3311e..000000000000 --- a/src/client/testing/common/services/discovery.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; -import { OutputChannel } from 'vscode'; -import { traceError } from '../../../common/logger'; -import { ExecutionFactoryCreateWithEnvironmentOptions, ExecutionResult, IPythonExecutionFactory, SpawnOptions } from '../../../common/process/types'; -import { IOutputChannel } from '../../../common/types'; -import { EXTENSION_ROOT_DIR } from '../../../constants'; -import { captureTelemetry } from '../../../telemetry'; -import { EventName } from '../../../telemetry/constants'; -import { TEST_OUTPUT_CHANNEL } from '../constants'; -import { ITestDiscoveryService, TestDiscoveryOptions, Tests } from '../types'; -import { DiscoveredTests, ITestDiscoveredTestParser } from './types'; - -const DISCOVERY_FILE = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testing_tools', 'run_adapter.py'); - -@injectable() -export class TestsDiscoveryService implements ITestDiscoveryService { - constructor( - @inject(IPythonExecutionFactory) private readonly execFactory: IPythonExecutionFactory, - @inject(ITestDiscoveredTestParser) private readonly parser: ITestDiscoveredTestParser, - @inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private readonly outChannel: OutputChannel - ) { } - @captureTelemetry(EventName.UNITTEST_DISCOVER_WITH_PYCODE, undefined, true) - public async discoverTests(options: TestDiscoveryOptions): Promise { - let output: ExecutionResult | undefined; - try { - output = await this.exec(options); - const discoveredTests = JSON.parse(output.stdout) as DiscoveredTests[]; - return this.parser.parse(options.workspaceFolder, discoveredTests); - } catch (ex) { - if (output) { - traceError('Failed to parse discovered Test', new Error(output.stdout)); - } - traceError('Failed to parse discovered Test', ex); - throw ex; - } - } - public async exec(options: TestDiscoveryOptions): Promise> { - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: options.workspaceFolder - }; - const execService = await this.execFactory.createActivatedEnvironment(creationOptions); - const spawnOptions: SpawnOptions = { - token: options.token, - cwd: options.cwd, - throwOnStdErr: true - }; - const argv = [DISCOVERY_FILE, ...options.args]; - this.outChannel.appendLine(`python ${argv.join(' ')}`); - return execService.exec(argv, spawnOptions); - } -} diff --git a/src/client/testing/common/services/storageService.ts b/src/client/testing/common/services/storageService.ts deleted file mode 100644 index bcb8de052a69..000000000000 --- a/src/client/testing/common/services/storageService.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { inject, injectable } from 'inversify'; -import { Disposable, Event, EventEmitter, Uri } from 'vscode'; -import { IWorkspaceService } from '../../../common/application/types'; -import { IDisposableRegistry } from '../../../common/types'; -import { TestDataItem } from '../../types'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestCollectionStorageService, TestFunction, Tests, TestSuite } from './../types'; - -@injectable() -export class TestCollectionStorageService implements ITestCollectionStorageService { - private readonly _onDidChange = new EventEmitter<{ uri: Uri; data?: TestDataItem }>(); - private readonly testsIndexedByWorkspaceUri = new Map(); - - constructor(@inject(IDisposableRegistry) disposables: Disposable[], - @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) { - disposables.push(this); - } - public get onDidChange(): Event<{ uri: Uri; data?: TestDataItem }> { - return this._onDidChange.event; - } - public getTests(resource: Uri): Tests | undefined { - const workspaceFolder = this.workspaceService.getWorkspaceFolderIdentifier(resource); - return this.testsIndexedByWorkspaceUri.has(workspaceFolder) ? this.testsIndexedByWorkspaceUri.get(workspaceFolder) : undefined; - } - public storeTests(resource: Uri, tests: Tests | undefined): void { - const workspaceFolder = this.workspaceService.getWorkspaceFolderIdentifier(resource); - this.testsIndexedByWorkspaceUri.set(workspaceFolder, tests); - this._onDidChange.fire({ uri: resource }); - } - public findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined { - const tests = this.getTests(resource); - if (!tests) { - return; - } - return tests.testFunctions.find(f => f.testFunction === func); - } - public findFlattendTestSuite(resource: Uri, suite: TestSuite): FlattenedTestSuite | undefined { - const tests = this.getTests(resource); - if (!tests) { - return; - } - return tests.testSuites.find(f => f.testSuite === suite); - } - public dispose() { - this.testsIndexedByWorkspaceUri.clear(); - } - public update(resource: Uri, item: TestDataItem): void { - this._onDidChange.fire({ uri: resource, data: item }); - } -} diff --git a/src/client/testing/common/services/testManagerService.ts b/src/client/testing/common/services/testManagerService.ts deleted file mode 100644 index 33557985832b..000000000000 --- a/src/client/testing/common/services/testManagerService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { Disposable, Uri } from 'vscode'; -import { IConfigurationService, IDisposableRegistry, Product } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { ITestManager, ITestManagerFactory, ITestManagerService, ITestsHelper, UnitTestProduct } from './../types'; - -export class TestManagerService implements ITestManagerService { - private cachedTestManagers = new Map(); - private readonly configurationService: IConfigurationService; - constructor(private wkspace: Uri, private testsHelper: ITestsHelper, private serviceContainer: IServiceContainer) { - const disposables = serviceContainer.get(IDisposableRegistry); - this.configurationService = serviceContainer.get(IConfigurationService); - disposables.push(this); - } - public dispose() { - this.cachedTestManagers.forEach(info => { - info.dispose(); - }); - } - public getTestManager(): ITestManager | undefined { - const preferredTestManager = this.getPreferredTestManager(); - if (typeof preferredTestManager !== 'number') { - return; - } - - // tslint:disable-next-line:no-non-null-assertion - if (!this.cachedTestManagers.has(preferredTestManager)) { - const testDirectory = this.getTestWorkingDirectory(); - const testProvider = this.testsHelper.parseProviderName(preferredTestManager); - const factory = this.serviceContainer.get(ITestManagerFactory); - this.cachedTestManagers.set(preferredTestManager, factory(testProvider, this.wkspace, testDirectory)); - } - const testManager = this.cachedTestManagers.get(preferredTestManager)!; - return testManager.enabled ? testManager : undefined; - } - public getTestWorkingDirectory() { - const settings = this.configurationService.getSettings(this.wkspace); - return settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : this.wkspace.fsPath; - } - public getPreferredTestManager(): UnitTestProduct | undefined { - const settings = this.configurationService.getSettings(this.wkspace); - if (settings.testing.nosetestsEnabled) { - return Product.nosetest; - } else if (settings.testing.pytestEnabled) { - return Product.pytest; - } else if (settings.testing.unittestEnabled) { - return Product.unittest; - } - return undefined; - } -} diff --git a/src/client/testing/common/services/testResultsService.ts b/src/client/testing/common/services/testResultsService.ts deleted file mode 100644 index caf9fa586009..000000000000 --- a/src/client/testing/common/services/testResultsService.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { inject, injectable, named } from 'inversify'; -import { TestDataItem } from '../../types'; -import { getChildren, getTestType } from '../testUtils'; -import { ITestResultsService, ITestVisitor, Tests, TestStatus, TestType } from './../types'; - -@injectable() -export class TestResultsService implements ITestResultsService { - constructor(@inject(ITestVisitor) @named('TestResultResetVisitor') private resultResetVisitor: ITestVisitor) { } - public resetResults(tests: Tests): void { - tests.testFolders.forEach(f => this.resultResetVisitor.visitTestFolder(f)); - tests.testFunctions.forEach(fn => this.resultResetVisitor.visitTestFunction(fn.testFunction)); - tests.testSuites.forEach(suite => this.resultResetVisitor.visitTestSuite(suite.testSuite)); - tests.testFiles.forEach(testFile => this.resultResetVisitor.visitTestFile(testFile)); - } - public updateResults(tests: Tests): void { - // Update Test tree bottom to top - const testQueue: TestDataItem[] = []; - const testStack: TestDataItem[] = []; - tests.rootTestFolders.forEach(folder => testQueue.push(folder)); - - while (testQueue.length > 0) { - const item = testQueue.shift(); - if (!item) { - continue; - } - testStack.push(item); - const children = getChildren(item); - children.forEach(child => testQueue.push(child)); - } - while (testStack.length > 0) { - const item = testStack.pop(); - this.updateTestItem(item!); - } - } - private updateTestItem(test: TestDataItem): void { - if (getTestType(test) === TestType.testFunction) { - return; - } - let allChildrenPassed = true; - let noChildrenRan = true; - test.functionsPassed = test.functionsFailed = test.functionsDidNotRun = 0; - - const children = getChildren(test); - children.forEach(child => { - if (getTestType(child) === TestType.testFunction) { - if (typeof child.passed === 'boolean') { - noChildrenRan = false; - if (child.passed) { - test.functionsPassed! += 1; - } else { - test.functionsFailed! += 1; - allChildrenPassed = false; - } - } else { - test.functionsDidNotRun! += 1; - } - } else { - if (typeof child.passed === 'boolean') { - noChildrenRan = false; - if (!child.passed) { - allChildrenPassed = false; - } - } - test.functionsFailed! += child.functionsFailed!; - test.functionsPassed! += child.functionsPassed!; - test.functionsDidNotRun! += child.functionsDidNotRun!; - } - }); - if (noChildrenRan) { - test.passed = undefined; - test.status = TestStatus.Unknown; - } else { - test.passed = allChildrenPassed; - test.status = test.passed ? TestStatus.Pass : TestStatus.Fail; - } - } -} diff --git a/src/client/testing/common/services/testsStatusService.ts b/src/client/testing/common/services/testsStatusService.ts deleted file mode 100644 index 08bd1316e3aa..000000000000 --- a/src/client/testing/common/services/testsStatusService.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { TestDataItem } from '../../types'; -import { visitRecursive } from '../testVisitors/visitor'; -import { ITestCollectionStorageService, ITestsStatusUpdaterService, Tests, TestStatus, TestsToRun } from '../types'; - -@injectable() -export class TestsStatusUpdaterService implements ITestsStatusUpdaterService { - constructor(@inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService) { } - public updateStatusAsDiscovering(resource: Uri, tests?: Tests): void { - if (!tests) { - return; - } - const visitor = (item: TestDataItem) => { - item.status = TestStatus.Discovering; - this.storage.update(resource, item); - }; - tests.rootTestFolders.forEach(item => visitRecursive(tests, item, visitor)); - } - public updateStatusAsUnknown(resource: Uri, tests?: Tests): void { - if (!tests) { - return; - } - const visitor = (item: TestDataItem) => { - item.status = TestStatus.Unknown; - this.storage.update(resource, item); - }; - tests.rootTestFolders.forEach(item => visitRecursive(tests, item, visitor)); - } - public updateStatusAsRunning(resource: Uri, tests?: Tests): void { - if (!tests) { - return; - } - const visitor = (item: TestDataItem) => { - item.status = TestStatus.Running; - this.storage.update(resource, item); - }; - tests.rootTestFolders.forEach(item => visitRecursive(tests, item, visitor)); - } - public updateStatusAsRunningFailedTests(resource: Uri, tests?: Tests): void { - if (!tests) { - return; - } - const predicate = (item: TestDataItem) => item.status === TestStatus.Fail || item.status === TestStatus.Error; - const visitor = (item: TestDataItem) => { - if (item.status && predicate(item)) { - item.status = TestStatus.Running; - this.storage.update(resource, item); - } - }; - const failedItems = [ - ...tests.testFunctions.map(f => f.testFunction).filter(predicate), - ...tests.testSuites.map(f => f.testSuite).filter(predicate) - ]; - failedItems.forEach(failedItem => visitRecursive(tests, failedItem, visitor)); - } - public updateStatusAsRunningSpecificTests(resource: Uri, testsToRun: TestsToRun, tests?: Tests): void { - if (!tests) { - return; - } - const itemsRunning = [ - ...(testsToRun.testFile || []), - ...(testsToRun.testSuite || []), - ...(testsToRun.testFunction || []) - ]; - const visitor = (item: TestDataItem) => { - item.status = TestStatus.Running; - this.storage.update(resource, item); - }; - itemsRunning.forEach(item => visitRecursive(tests, item, visitor)); - } - public updateStatusOfRunningTestsAsIdle(resource: Uri, tests?: Tests): void { - if (!tests) { - return; - } - const visitor = (item: TestDataItem) => { - if (item.status === TestStatus.Running) { - item.status = TestStatus.Idle; - this.storage.update(resource, item); - } - }; - tests.rootTestFolders.forEach(item => visitRecursive(tests, item, visitor)); - } - public triggerUpdatesToTests(resource: Uri, tests?: Tests): void { - if (!tests) { - return; - } - const visitor = (item: TestDataItem) => this.storage.update(resource, item); - tests.rootTestFolders.forEach(item => visitRecursive(tests, item, visitor)); - } -} diff --git a/src/client/testing/common/services/types.ts b/src/client/testing/common/services/types.ts deleted file mode 100644 index 4bf14344eae8..000000000000 --- a/src/client/testing/common/services/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { Uri } from 'vscode'; -import { Tests } from '../types'; - -export type TestContainer = { - id: string; - kind: 'file' | 'folder' | 'suite' | 'function'; - name: string; - parentid: string; -}; -export type TestItem = { - id: string; - name: string; - source: string; - parentid: string; -}; -export type DiscoveredTests = { - rootid: string; - root: string; - parents: TestContainer[]; - tests: TestItem[]; -}; - -export const ITestDiscoveredTestParser = Symbol('ITestDiscoveredTestParser'); -export interface ITestDiscoveredTestParser { - parse(resource: Uri, discoveredTests: DiscoveredTests[]): Tests; -} diff --git a/src/client/testing/common/services/unitTestDiagnosticService.ts b/src/client/testing/common/services/unitTestDiagnosticService.ts deleted file mode 100644 index 5e88a6df615b..000000000000 --- a/src/client/testing/common/services/unitTestDiagnosticService.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { injectable } from 'inversify'; -import { DiagnosticSeverity } from 'vscode'; -import * as localize from '../../../common/utils/localize'; -import { DiagnosticMessageType, ITestDiagnosticService, PythonTestMessageSeverity } from '../../types'; -import { TestStatus } from '../types'; - -@injectable() -export class UnitTestDiagnosticService implements ITestDiagnosticService { - private MessageTypes = new Map(); - private MessageSeverities = new Map(); - private MessagePrefixes = new Map(); - - constructor() { - this.MessageTypes.set(TestStatus.Error, DiagnosticMessageType.Error); - this.MessageTypes.set(TestStatus.Fail, DiagnosticMessageType.Fail); - this.MessageTypes.set(TestStatus.Skipped, DiagnosticMessageType.Skipped); - this.MessageTypes.set(TestStatus.Pass, DiagnosticMessageType.Pass); - this.MessageSeverities.set(PythonTestMessageSeverity.Error, DiagnosticSeverity.Error); - this.MessageSeverities.set(PythonTestMessageSeverity.Failure, DiagnosticSeverity.Error); - this.MessageSeverities.set(PythonTestMessageSeverity.Skip, DiagnosticSeverity.Information); - this.MessageSeverities.set(PythonTestMessageSeverity.Pass, undefined); - this.MessagePrefixes.set(DiagnosticMessageType.Error, localize.Testing.testErrorDiagnosticMessage()); - this.MessagePrefixes.set(DiagnosticMessageType.Fail, localize.Testing.testFailDiagnosticMessage()); - this.MessagePrefixes.set(DiagnosticMessageType.Skipped, localize.Testing.testSkippedDiagnosticMessage()); - this.MessagePrefixes.set(DiagnosticMessageType.Pass, ''); - } - public getMessagePrefix(status: TestStatus): string | undefined { - const msgType = this.MessageTypes.get(status); - return msgType !== undefined ? this.MessagePrefixes.get(msgType!) : undefined; - } - public getSeverity(unitTestSeverity: PythonTestMessageSeverity): DiagnosticSeverity | undefined { - return this.MessageSeverities.get(unitTestSeverity); - } -} diff --git a/src/client/testing/common/services/workspaceTestManagerService.ts b/src/client/testing/common/services/workspaceTestManagerService.ts deleted file mode 100644 index 779ef9351f53..000000000000 --- a/src/client/testing/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(); - constructor(@inject(IOutputChannel) @named(TEST_OUTPUT_CHANNEL) private outChannel: OutputChannel, - @inject(ITestManagerServiceFactory) private testManagerServiceFactory: ITestManagerServiceFactory, - @inject(IDisposableRegistry) disposables: Disposable[]) { - disposables.push(this); - } - public dispose() { - this.workspaceTestManagers.forEach(info => info.dispose()); - } - public getTestManager(resource: Uri): ITestManager | undefined { - const wkspace = this.getWorkspace(resource); - this.ensureTestManagerService(wkspace); - return this.workspaceTestManagers.get(wkspace.fsPath)!.getTestManager(); - } - public getTestWorkingDirectory(resource: Uri) { - const wkspace = this.getWorkspace(resource); - this.ensureTestManagerService(wkspace); - return this.workspaceTestManagers.get(wkspace.fsPath)!.getTestWorkingDirectory(); - } - public getPreferredTestManager(resource: Uri): UnitTestProduct | undefined { - const wkspace = this.getWorkspace(resource); - this.ensureTestManagerService(wkspace); - return this.workspaceTestManagers.get(wkspace.fsPath)!.getPreferredTestManager(); - } - private getWorkspace(resource: Uri): Uri { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - const noWkspaceMessage = 'Please open a workspace'; - this.outChannel.appendLine(noWkspaceMessage); - throw new Error(noWkspaceMessage); - } - if (!resource || workspace.workspaceFolders.length === 1) { - return workspace.workspaceFolders[0].uri; - } - const workspaceFolder = workspace.getWorkspaceFolder(resource); - if (workspaceFolder) { - return workspaceFolder.uri; - } - const message = `Resource '${resource.fsPath}' does not belong to any workspace`; - this.outChannel.appendLine(message); - throw new Error(message); - } - private ensureTestManagerService(wkspace: Uri) { - if (!this.workspaceTestManagers.has(wkspace.fsPath)) { - this.workspaceTestManagers.set(wkspace.fsPath, this.testManagerServiceFactory(wkspace)); - } - } -} diff --git a/src/client/testing/common/testConfigurationManager.ts b/src/client/testing/common/testConfigurationManager.ts new file mode 100644 index 000000000000..be3f0109da02 --- /dev/null +++ b/src/client/testing/common/testConfigurationManager.ts @@ -0,0 +1,125 @@ +import * as path from 'path'; +import { QuickPickItem, QuickPickOptions, Uri } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { IFileSystem } from '../../common/platform/types'; +import { IInstaller } from '../../common/types'; +import { createDeferred } from '../../common/utils/async'; +import { IServiceContainer } from '../../ioc/types'; +import { traceVerbose } from '../../logging'; +import { UNIT_TEST_PRODUCTS } from './constants'; +import { ITestConfigSettingsService, ITestConfigurationManager, UnitTestProduct } from './types'; + +function handleCancelled(): void { + traceVerbose('testing configuration (in UI) cancelled'); + throw Error('cancelled'); +} + +export abstract class TestConfigurationManager implements ITestConfigurationManager { + protected readonly installer: IInstaller; + + protected readonly testConfigSettingsService: ITestConfigSettingsService; + + private readonly handleCancelled = handleCancelled; + + constructor( + protected workspace: Uri, + protected product: UnitTestProduct, + protected readonly serviceContainer: IServiceContainer, + cfg?: ITestConfigSettingsService, + ) { + this.installer = serviceContainer.get(IInstaller); + this.testConfigSettingsService = + cfg || serviceContainer.get(ITestConfigSettingsService); + } + + public abstract configure(wkspace: Uri): Promise; + + public abstract requiresUserToConfigure(wkspace: Uri): Promise; + + public async enable(): Promise { + // Disable other test frameworks. + await Promise.all( + UNIT_TEST_PRODUCTS.filter((prod) => prod !== this.product).map((prod) => + this.testConfigSettingsService.disable(this.workspace, prod), + ), + ); + await this.testConfigSettingsService.enable(this.workspace, this.product); + } + + public async disable(): Promise { + return this.testConfigSettingsService.enable(this.workspace, this.product); + } + + protected selectTestDir(rootDir: string, subDirs: string[], customOptions: QuickPickItem[] = []): Promise { + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select the directory containing the tests', + }; + let items: QuickPickItem[] = subDirs + .map((dir) => { + const dirName = path.relative(rootDir, dir); + if (dirName.indexOf('.') === 0) { + return undefined; + } + return { + label: dirName, + description: '', + }; + }) + .filter((item) => item !== undefined) + .map((item) => item!); + + items = [{ label: '.', description: 'Root directory' }, ...items]; + items = customOptions.concat(items); + return this.showQuickPick(items, options); + } + + protected selectTestFilePattern(): Promise { + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: 'Select the pattern to identify test files', + }; + const items: QuickPickItem[] = [ + { label: '*test.py', description: "Python files ending with 'test'" }, + { label: '*_test.py', description: "Python files ending with '_test'" }, + { label: 'test*.py', description: "Python files beginning with 'test'" }, + { label: 'test_*.py', description: "Python files beginning with 'test_'" }, + { label: '*test*.py', description: "Python files containing the word 'test'" }, + ]; + + return this.showQuickPick(items, options); + } + + protected getTestDirs(rootDir: string): Promise { + const fs = this.serviceContainer.get(IFileSystem); + return fs.getSubDirectories(rootDir).then((subDirs) => { + subDirs.sort(); + + // Find out if there are any dirs with the name test and place them on the top. + const possibleTestDirs = subDirs.filter((dir) => dir.match(/test/i)); + const nonTestDirs = subDirs.filter((dir) => possibleTestDirs.indexOf(dir) === -1); + possibleTestDirs.push(...nonTestDirs); + + // The test dirs are now on top. + return possibleTestDirs; + }); + } + + private showQuickPick(items: QuickPickItem[], options: QuickPickOptions): Promise { + const def = createDeferred(); + const appShell = this.serviceContainer.get(IApplicationShell); + appShell.showQuickPick(items, options).then((item) => { + if (!item) { + this.handleCancelled(); // This will throw an exception. + return; + } + + def.resolve(item.label); + }); + return def.promise; + } +} diff --git a/src/client/testing/common/testUtils.ts b/src/client/testing/common/testUtils.ts index ed5b1cc403af..04e82e1caa52 100644 --- a/src/client/testing/common/testUtils.ts +++ b/src/client/testing/common/testUtils.ts @@ -1,29 +1,10 @@ -import { inject, injectable, named } from 'inversify'; -import * as path from 'path'; +import { injectable } from 'inversify'; import { Uri, workspace } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import * as constants from '../../common/constants'; -import { ITestingSettings, Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { TestDataItem, TestWorkspaceFolder } from '../types'; -import { CommandSource } from './constants'; -import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; -import { - FlattenedTestFunction, - FlattenedTestSuite, - ITestsHelper, - ITestVisitor, - TestFile, - TestFolder, - TestFunction, - TestProvider, - Tests, - TestSettingsPropertyNames, - TestsToRun, - TestSuite, - TestType, - UnitTestProduct -} from './types'; +import { IApplicationShell } from '../../common/application/types'; +import { Product } from '../../common/types'; +import { ITestingSettings, TestSettingsPropertyNames } from '../configuration/types'; +import { TestProvider } from '../types'; +import { ITestsHelper, UnitTestProduct } from './types'; export async function selectTestWorkspace(appShell: IApplicationShell): Promise { if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { @@ -36,34 +17,10 @@ export async function selectTestWorkspace(appShell: IApplicationShell): Promise< } } -export function extractBetweenDelimiters(content: string, startDelimiter: string, endDelimiter: string): string { - content = content.substring(content.indexOf(startDelimiter) + startDelimiter.length); - return content.substring(0, content.lastIndexOf(endDelimiter)); -} - -export function convertFileToPackage(filePath: string): string { - const lastIndex = filePath.lastIndexOf('.'); - return filePath - .substring(0, lastIndex) - .replace(/\//g, '.') - .replace(/\\/g, '.'); -} - @injectable() export class TestsHelper implements ITestsHelper { - private readonly appShell: IApplicationShell; - private readonly commandManager: ICommandManager; - constructor( - @inject(ITestVisitor) @named('TestFlatteningVisitor') private readonly flatteningVisitor: TestFlatteningVisitor, - @inject(IServiceContainer) serviceContainer: IServiceContainer - ) { - this.appShell = serviceContainer.get(IApplicationShell); - this.commandManager = serviceContainer.get(ICommandManager); - } public parseProviderName(product: UnitTestProduct): TestProvider { switch (product) { - case Product.nosetest: - return 'nosetest'; case Product.pytest: return 'pytest'; case Product.unittest: @@ -75,8 +32,6 @@ export class TestsHelper implements ITestsHelper { } public parseProduct(provider: TestProvider): UnitTestProduct { switch (provider) { - case 'nosetest': - return Product.nosetest; case 'pytest': return Product.pytest; case 'unittest': @@ -93,20 +48,13 @@ export class TestsHelper implements ITestsHelper { return { argsName: 'pytestArgs' as keyof ITestingSettings, pathName: 'pytestPath' as keyof ITestingSettings, - enabledName: 'pytestEnabled' as keyof ITestingSettings - }; - } - case 'nosetest': { - return { - argsName: 'nosetestArgs' as keyof ITestingSettings, - pathName: 'nosetestPath' as keyof ITestingSettings, - enabledName: 'nosetestsEnabled' as keyof ITestingSettings + enabledName: 'pytestEnabled' as keyof ITestingSettings, }; } case 'unittest': { return { argsName: 'unittestArgs' as keyof ITestingSettings, - enabledName: 'unittestEnabled' as keyof ITestingSettings + enabledName: 'unittestEnabled' as keyof ITestingSettings, }; } default: { @@ -114,472 +62,4 @@ export class TestsHelper implements ITestsHelper { } } } - public flattenTestFiles(testFiles: TestFile[], workspaceFolder: string): Tests { - testFiles.forEach(testFile => this.flatteningVisitor.visitTestFile(testFile)); - - // tslint:disable-next-line:no-object-literal-type-assertion - const tests = { - testFiles: testFiles, - testFunctions: this.flatteningVisitor.flattenedTestFunctions, - testSuites: this.flatteningVisitor.flattenedTestSuites, - testFolders: [], - rootTestFolders: [], - summary: { passed: 0, failures: 0, errors: 0, skipped: 0 } - }; - - this.placeTestFilesIntoFolders(tests, workspaceFolder); - - return tests; - } - public placeTestFilesIntoFolders(tests: Tests, workspaceFolder: string): void { - // First get all the unique folders - const folders: string[] = []; - tests.testFiles.forEach(file => { - const relativePath = path.relative(workspaceFolder, file.fullPath); - const dir = path.dirname(relativePath); - if (folders.indexOf(dir) === -1) { - folders.push(dir); - } - }); - - tests.testFolders = []; - const folderMap = new Map(); - folders.sort(); - const resource = Uri.file(workspaceFolder); - folders.forEach(dir => { - dir.split(path.sep).reduce((parentPath, currentName, _index, _values) => { - let newPath = currentName; - let parentFolder: TestFolder | undefined; - if (parentPath.length > 0) { - parentFolder = folderMap.get(parentPath); - newPath = path.join(parentPath, currentName); - } - if (!folderMap.has(newPath)) { - const testFolder: TestFolder = { resource, name: newPath, testFiles: [], folders: [], nameToRun: newPath, time: 0, functionsPassed: 0, functionsFailed: 0, functionsDidNotRun: 0 }; - folderMap.set(newPath, testFolder); - if (parentFolder) { - parentFolder!.folders.push(testFolder); - } else { - tests.rootTestFolders.push(testFolder); - } - tests.testFiles - .filter(fl => path.dirname(path.relative(workspaceFolder, fl.fullPath)) === newPath) - .forEach(testFile => { - testFolder.testFiles.push(testFile); - }); - tests.testFolders.push(testFolder); - } - return newPath; - }, ''); - }); - } - public parseTestName(name: string, rootDirectory: string, tests: Tests): TestsToRun | undefined { - // tslint:disable-next-line:no-suspicious-comment - // TODO: We need a better way to match (currently we have raw name, name, xmlname, etc = which one do we. - // Use to identify a file given the full file name, similarly for a folder and function. - // Perhaps something like a parser or methods like TestFunction.fromString()... something). - if (!tests) { - return undefined; - } - const absolutePath = path.isAbsolute(name) ? name : path.resolve(rootDirectory, name); - const testFolders = tests.testFolders.filter(folder => folder.nameToRun === name || folder.name === name || folder.name === absolutePath); - if (testFolders.length > 0) { - return { testFolder: testFolders }; - } - - const testFiles = tests.testFiles.filter(file => file.nameToRun === name || file.name === name || file.fullPath === absolutePath); - if (testFiles.length > 0) { - return { testFile: testFiles }; - } - - const testFns = tests.testFunctions.filter(fn => fn.testFunction.nameToRun === name || fn.testFunction.name === name).map(fn => fn.testFunction); - if (testFns.length > 0) { - return { testFunction: testFns }; - } - - // Just return this as a test file. - return { testFile: [{ resource: Uri.file(rootDirectory), name: name, nameToRun: name, functions: [], suites: [], xmlName: name, fullPath: '', time: 0, functionsPassed: 0, functionsFailed: 0, functionsDidNotRun: 0 }] }; - } - public displayTestErrorMessage(message: string) { - this.appShell.showErrorMessage(message, constants.Button_Text_Tests_View_Output).then(action => { - if (action === constants.Button_Text_Tests_View_Output) { - this.commandManager.executeCommand(constants.Commands.Tests_ViewOutput, undefined, CommandSource.ui); - } - }); - } - public mergeTests(items: Tests[]): Tests { - return items.reduce((tests, otherTests, index) => { - if (index === 0) { - return tests; - } - - tests.summary.errors += otherTests.summary.errors; - tests.summary.failures += otherTests.summary.failures; - tests.summary.passed += otherTests.summary.passed; - tests.summary.skipped += otherTests.summary.skipped; - tests.rootTestFolders.push(...otherTests.rootTestFolders); - tests.testFiles.push(...otherTests.testFiles); - tests.testFolders.push(...otherTests.testFolders); - tests.testFunctions.push(...otherTests.testFunctions); - tests.testSuites.push(...otherTests.testSuites); - - return tests; - }, items[0]); - } - - public shouldRunAllTests(testsToRun?: TestsToRun) { - if (!testsToRun) { - return true; - } - if ( - (Array.isArray(testsToRun.testFile) && testsToRun.testFile.length > 0) || - (Array.isArray(testsToRun.testFolder) && testsToRun.testFolder.length > 0) || - (Array.isArray(testsToRun.testFunction) && testsToRun.testFunction.length > 0) || - (Array.isArray(testsToRun.testSuite) && testsToRun.testSuite.length > 0) - ) { - return false; - } - - return true; - } -} - -export function getTestType(test: TestDataItem): TestType { - if (test instanceof TestWorkspaceFolder) { - return TestType.testWorkspaceFolder; - } - if (getTestFile(test)) { - return TestType.testFile; - } - if (getTestFolder(test)) { - return TestType.testFolder; - } - if (getTestSuite(test)) { - return TestType.testSuite; - } - if (getTestFunction(test)) { - return TestType.testFunction; - } - throw new Error('Unknown test type'); -} -export function getTestFile(test: TestDataItem): TestFile | undefined { - if (!test) { - return; - } - // Only TestFile has a `fullPath` property. - return typeof (test as TestFile).fullPath === 'string' ? (test as TestFile) : undefined; -} -export function getTestSuite(test: TestDataItem): TestSuite | undefined { - if (!test) { - return; - } - // Only TestSuite has a `suites` property. - return Array.isArray((test as TestSuite).suites) && !getTestFile(test) ? (test as TestSuite) : undefined; -} -export function getTestFolder(test: TestDataItem): TestFolder | undefined { - if (!test) { - return; - } - // Only TestFolder has a `folders` property. - return Array.isArray((test as TestFolder).folders) ? (test as TestFolder) : undefined; -} -export function getTestFunction(test: TestDataItem): TestFunction | undefined { - if (!test) { - return; - } - if (test instanceof TestWorkspaceFolder || getTestFile(test) || getTestFolder(test) || getTestSuite(test)) { - return; - } - return test as TestFunction; -} - -/** - * Gets the parent for a given test item. - * For test functions, this will return either a test suite or a test file. - * For test suites, this will return either a test suite or a test file. - * For test files, this will return a test folder. - * For a test folder, this will return either a test folder or `undefined`. - * @export - * @param {Tests} tests - * @param {TestDataItem} data - * @returns {(TestDataItem | undefined)} - */ -export function getParent(tests: Tests, data: TestDataItem): TestDataItem | undefined { - switch (getTestType(data)) { - case TestType.testFile: { - return getParentTestFolderForFile(tests, data as TestFile); - } - case TestType.testFolder: { - return getParentTestFolder(tests, data as TestFolder); - } - case TestType.testSuite: { - const suite = data as TestSuite; - if (isSubtestsParent(suite)) { - const fn = suite.functions[0]; - const parent = tests.testSuites.find(item => item.testSuite.functions.indexOf(fn) >= 0); - if (parent) { - return parent.testSuite; - } - return tests.testFiles.find(item => item.functions.indexOf(fn) >= 0); - } - const parentSuite = tests.testSuites.find(item => item.testSuite.suites.indexOf(suite) >= 0); - if (parentSuite) { - return parentSuite.testSuite; - } - return tests.testFiles.find(item => item.suites.indexOf(suite) >= 0); - } - case TestType.testFunction: { - const fn = data as TestFunction; - if (fn.subtestParent) { - return fn.subtestParent.asSuite; - } - const parentSuite = tests.testSuites.find(item => item.testSuite.functions.indexOf(fn) >= 0); - if (parentSuite) { - return parentSuite.testSuite; - } - return tests.testFiles.find(item => item.functions.indexOf(fn) >= 0); - } - default: { - throw new Error('Unknown test type'); - } - } -} - -/** - * Returns the parent test folder give a given test file or folder. - * - * @export - * @param {Tests} tests - * @param {(TestFolder | TestFile)} item - * @returns {(TestFolder | undefined)} - */ -function getParentTestFolder(tests: Tests, item: TestFolder | TestFile): TestFolder | undefined { - if (getTestType(item) === TestType.testFolder) { - return getParentTestFolderForFolder(tests, item as TestFolder); - } - return getParentTestFolderForFile(tests, item as TestFile); -} - -/** - * Gets the parent test file for a test item. - * - * @param {Tests} tests - * @param {(TestSuite | TestFunction)} suite - * @returns {TestFile} - */ -export function getParentFile(tests: Tests, suite: TestSuite | TestFunction): TestFile { - let parent = getParent(tests, suite); - while (parent) { - if (getTestType(parent) === TestType.testFile) { - return parent as TestFile; - } - parent = getParent(tests, parent); - } - throw new Error('No parent file for provided test item'); -} -/** - * Gets the parent test suite for a suite/function. - * - * @param {Tests} tests - * @param {(TestSuite | TestFunction)} suite - * @returns {(TestSuite | undefined)} - */ -export function getParentSuite(tests: Tests, suite: TestSuite | TestFunction): TestSuite | undefined { - let parent = getParent(tests, suite); - while (parent) { - if (getTestType(parent) === TestType.testSuite) { - return parent as TestSuite; - } - parent = getParent(tests, parent); - } - return; -} - -/** - * Returns the parent test folder give a given test file. - * - * @param {Tests} tests - * @param {TestFile} file - * @returns {(TestFolder | undefined)} - */ -function getParentTestFolderForFile(tests: Tests, file: TestFile): TestFolder | undefined { - return tests.testFolders.find(folder => folder.testFiles.some(item => item === file)); -} - -/** - * Returns the parent test folder for a given test folder. - * - * @param {Tests} tests - * @param {TestFolder} folder - * @returns {(TestFolder | undefined)} - */ -function getParentTestFolderForFolder(tests: Tests, folder: TestFolder): TestFolder | undefined { - if (tests.rootTestFolders.indexOf(folder) >= 0) { - return; - } - return tests.testFolders.find(item => item.folders.some(child => child === folder)); -} - -/** - * Given a test function will return the corresponding flattened test function. - * - * @export - * @param {Tests} tests - * @param {TestFunction} func - * @returns {(FlattenedTestFunction | undefined)} - */ -export function findFlattendTestFunction(tests: Tests, func: TestFunction): FlattenedTestFunction | undefined { - return tests.testFunctions.find(f => f.testFunction === func); -} - -/** - * Given a test suite, will return the corresponding flattened test suite. - * - * @export - * @param {Tests} tests - * @param {TestSuite} suite - * @returns {(FlattenedTestSuite | undefined)} - */ -export function findFlattendTestSuite(tests: Tests, suite: TestSuite): FlattenedTestSuite | undefined { - return tests.testSuites.find(f => f.testSuite === suite); -} - -/** - * Returns the children of a given test data item. - * - * @export - * @param {Tests} tests - * @param {TestDataItem} item - * @returns {TestDataItem[]} - */ -export function getChildren(item: TestDataItem): TestDataItem[] { - switch (getTestType(item)) { - case TestType.testFolder: { - return [ - ...(item as TestFolder).folders, - ...(item as TestFolder).testFiles - ]; - } - case TestType.testFile: { - const [subSuites, functions] = divideSubtests((item as TestFile).functions); - return [ - ...functions, - ...(item as TestFile).suites, - ...subSuites - ]; - } - case TestType.testSuite: { - let subSuites: TestSuite[] = []; - let functions = (item as TestSuite).functions; - if (!isSubtestsParent((item as TestSuite))) { - [subSuites, functions] = divideSubtests((item as TestSuite).functions); - } - return [ - ...functions, - ...(item as TestSuite).suites, - ...subSuites - ]; - } - case TestType.testFunction: { - return []; - } - default: { - throw new Error('Unknown Test Type'); - } - } -} - -function divideSubtests(mixed: TestFunction[]): [TestSuite[], TestFunction[]] { - const suites: TestSuite[] = []; - const functions: TestFunction[] = []; - mixed.forEach(func => { - if (!func.subtestParent) { - functions.push(func); - return; - } - const parent = func.subtestParent.asSuite; - if (suites.indexOf(parent) < 0) { - suites.push(parent); - } - }); - return [suites, functions]; -} - -export function isSubtestsParent(suite: TestSuite): boolean { - const functions = suite.functions; - if (functions.length === 0) { - return false; - } - const subtestParent = functions[0].subtestParent; - if (subtestParent === undefined) { - return false; - } - return subtestParent.asSuite === suite; -} - -export function copyDesiredTestResults(source: Tests, target: Tests): void { - copyResultsForFolders(source.testFolders, target.testFolders); -} - -function copyResultsForFolders(source: TestFolder[], target: TestFolder[]): void { - source.forEach(sourceFolder => { - const targetFolder = target.find(folder => folder.name === sourceFolder.name && folder.nameToRun === sourceFolder.nameToRun); - if (!targetFolder) { - return; - } - copyValueTypes(sourceFolder, targetFolder); - copyResultsForFiles(sourceFolder.testFiles, targetFolder.testFiles); - // These should be reinitialized - targetFolder.functionsPassed = targetFolder.functionsDidNotRun = targetFolder.functionsFailed = 0; - }); -} -function copyResultsForFiles(source: TestFile[], target: TestFile[]): void { - source.forEach(sourceFile => { - const targetFile = target.find(file => file.name === sourceFile.name); - if (!targetFile) { - return; - } - copyValueTypes(sourceFile, targetFile); - copyResultsForFunctions(sourceFile.functions, targetFile.functions); - copyResultsForSuites(sourceFile.suites, targetFile.suites); - // These should be reinitialized - targetFile.functionsPassed = targetFile.functionsDidNotRun = targetFile.functionsFailed = 0; - }); -} - -function copyResultsForFunctions(source: TestFunction[], target: TestFunction[]): void { - source.forEach(sourceFn => { - const targetFn = target.find(fn => fn.name === sourceFn.name && fn.nameToRun === sourceFn.nameToRun); - if (!targetFn) { - return; - } - copyValueTypes(sourceFn, targetFn); - }); -} - -function copyResultsForSuites(source: TestSuite[], target: TestSuite[]): void { - source.forEach(sourceSuite => { - const targetSuite = target.find(suite => suite.name === sourceSuite.name && - suite.nameToRun === sourceSuite.nameToRun && - suite.xmlName === sourceSuite.xmlName); - if (!targetSuite) { - return; - } - copyValueTypes(sourceSuite, targetSuite); - copyResultsForFunctions(sourceSuite.functions, targetSuite.functions); - copyResultsForSuites(sourceSuite.suites, targetSuite.suites); - // These should be reinitialized - targetSuite.functionsPassed = targetSuite.functionsDidNotRun = targetSuite.functionsFailed = 0; - }); -} - -function copyValueTypes(source: T, target: T): void { - Object.keys(source).forEach(key => { - // tslint:disable-next-line:no-any - const value = (source as any)[key]; - if (['boolean', 'number', 'string', 'undefined'].indexOf(typeof value) >= 0) { - // tslint:disable-next-line:no-any - (target as any)[key] = value; - } - }); } diff --git a/src/client/testing/common/testVisitors/flatteningVisitor.ts b/src/client/testing/common/testVisitors/flatteningVisitor.ts deleted file mode 100644 index 13bdd454ea36..000000000000 --- a/src/client/testing/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(); - // tslint:disable-next-line:variable-name - private _flattenedTestSuites = new Map(); - public get flattenedTestFunctions(): FlattenedTestFunction[] { - return [...this._flattedTestFunctions.values()]; - } - public get flattenedTestSuites(): FlattenedTestSuite[] { - return [...this._flattenedTestSuites.values()]; - } - // tslint:disable-next-line:no-empty - public visitTestFunction(_testFunction: TestFunction): void { } - // tslint:disable-next-line:no-empty - public visitTestSuite(_testSuite: TestSuite): void { } - public visitTestFile(testFile: TestFile): void { - // sample test_three (file name without extension and all / replaced with ., meaning this is the package) - const packageName = convertFileToPackage(testFile.name); - - testFile.functions.forEach(fn => this.addTestFunction(fn, testFile, packageName)); - testFile.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, testFile)); - } - // tslint:disable-next-line:no-empty - public visitTestFolder(_testFile: TestFolder) { } - private visitTestSuiteOfAFile(testSuite: TestSuite, parentTestFile: TestFile): void { - testSuite.functions.forEach(fn => this.visitTestFunctionOfASuite(fn, testSuite, parentTestFile)); - testSuite.suites.forEach(suite => this.visitTestSuiteOfAFile(suite, parentTestFile)); - this.addTestSuite(testSuite, parentTestFile); - } - private visitTestFunctionOfASuite(testFunction: TestFunction, parentTestSuite: TestSuite, parentTestFile: TestFile) { - const key = `Function:${testFunction.name},Suite:${parentTestSuite.name},SuiteXmlName:${parentTestSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; - if (this._flattenedTestSuites.has(key)) { - return; - } - const flattenedFunction = { testFunction, xmlClassName: parentTestSuite.xmlName, parentTestFile, parentTestSuite }; - this._flattedTestFunctions.set(key, flattenedFunction); - } - private addTestSuite(testSuite: TestSuite, parentTestFile: TestFile) { - const key = `Suite:${testSuite.name},SuiteXmlName:${testSuite.xmlName},ParentFile:${parentTestFile.fullPath}`; - if (this._flattenedTestSuites.has(key)) { - return; - } - const flattenedSuite = { parentTestFile, testSuite, xmlClassName: testSuite.xmlName }; - this._flattenedTestSuites.set(key, flattenedSuite); - } - private addTestFunction(testFunction: TestFunction, parentTestFile: TestFile, parentTestPackage: string) { - const key = `Function:${testFunction.name},ParentFile:${parentTestFile.fullPath}`; - if (this._flattedTestFunctions.has(key)) { - return; - } - const flattendFunction = { testFunction, xmlClassName: parentTestPackage, parentTestFile }; - this._flattedTestFunctions.set(key, flattendFunction); - } -} diff --git a/src/client/testing/common/testVisitors/resultResetVisitor.ts b/src/client/testing/common/testVisitors/resultResetVisitor.ts deleted file mode 100644 index 6929d9386fa9..000000000000 --- a/src/client/testing/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/testing/common/testVisitors/visitor.ts b/src/client/testing/common/testVisitors/visitor.ts deleted file mode 100644 index 5056d61e3743..000000000000 --- a/src/client/testing/common/testVisitors/visitor.ts +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { TestDataItem } from '../../types'; -import { getChildren, getParent } from '../testUtils'; -import { Tests } from '../types'; - -export type Visitor = (item: TestDataItem) => void; - -/** - * Vists tests recursively. - * - * @export - * @param {Tests} tests - * @param {Visitor} visitor - */ -export function visitRecursive(tests: Tests, visitor: Visitor): void; - -/** - * Vists tests recursively. - * - * @export - * @param {Tests} tests - * @param {TestDataItem} start - * @param {Visitor} visitor - */ -export function visitRecursive(tests: Tests, start: TestDataItem, visitor: Visitor): void; -export function visitRecursive(tests: Tests, arg1: TestDataItem | Visitor, arg2?: Visitor): void { - const startItem = typeof arg1 === 'function' ? undefined : (arg1 as TestDataItem); - const visitor = startItem ? arg2! : (arg1 as Visitor); - let children: TestDataItem[] = []; - if (startItem) { - visitor(startItem); - children = getChildren(startItem); - } else { - children = tests.rootTestFolders; - } - children.forEach(folder => visitRecursive(tests, folder, visitor)); -} - -/** - * Visits parents recursively. - * - * @export - * @param {Tests} tests - * @param {TestDataItem} startItem - * @param {Visitor} visitor - * @returns {void} - */ -export function visitParentsRecursive(tests: Tests, startItem: TestDataItem, visitor: Visitor): void { - visitor(startItem); - const parent = getParent(tests, startItem); - if (!parent) { - return; - } - visitor(parent); - visitParentsRecursive(tests, parent, visitor); -} diff --git a/src/client/testing/common/types.ts b/src/client/testing/common/types.ts index 4723c60ff4ce..e2fa2d6d2e5a 100644 --- a/src/client/testing/common/types.ts +++ b/src/client/testing/common/types.ts @@ -1,338 +1,83 @@ -import { - CancellationToken, DebugConfiguration, DiagnosticCollection, - Disposable, Event, OutputChannel, Uri -} from 'vscode'; -import { ITestingSettings, Product } from '../../common/types'; -import { DebuggerTypeName } from '../../debugger/constants'; -import { ConsoleType } from '../../debugger/types'; -import { IPythonTestMessage, TestDataItem, WorkspaceTestStatus } from '../types'; -import { CommandSource } from './constants'; +import { CancellationToken, DebugSessionOptions, OutputChannel, Uri } from 'vscode'; +import { Product } from '../../common/types'; +import { TestSettingsPropertyNames } from '../configuration/types'; +import { TestProvider } from '../types'; +import { PythonProject } from '../../envExt/types'; -export type TestProvider = 'nosetest' | 'pytest' | 'unittest'; +export type UnitTestProduct = Product.pytest | Product.unittest; + +// **************** +// test args/options export type TestDiscoveryOptions = { workspaceFolder: Uri; cwd: string; args: string[]; - token: CancellationToken; + token?: CancellationToken; ignoreCache: boolean; - outChannel: OutputChannel; + outChannel?: OutputChannel; }; -export type TestRunOptions = { - workspaceFolder: Uri; +export type LaunchOptions = { cwd: string; - tests: Tests; args: string[]; - testsToRun?: TestsToRun; - token: CancellationToken; + testProvider: TestProvider; + token?: CancellationToken; outChannel?: OutputChannel; - debug?: boolean; -}; - -export type UnitTestParserOptions = TestDiscoveryOptions & { startDirectory: string }; - -export type TestFolder = TestResult & { - resource: Uri; - name: string; - testFiles: TestFile[]; - nameToRun: string; - folders: TestFolder[]; -}; -export enum TestType { - testFile = 'testFile', - testFolder = 'testFolder', - testSuite = 'testSuite', - testFunction = 'testFunction', - testWorkspaceFolder = 'testWorkspaceFolder' -} -export type TestFile = TestResult & { - resource: Uri; - name: string; - fullPath: string; - functions: TestFunction[]; - suites: TestSuite[]; - nameToRun: string; - xmlName: string; - errorsWhenDiscovering?: string; -}; - -export type TestSuite = TestResult & { - resource: Uri; - name: string; - functions: TestFunction[]; - suites: TestSuite[]; - isUnitTest: Boolean; - isInstance: Boolean; - nameToRun: string; - xmlName: string; -}; - -export type TestFunction = TestResult & { - resource: Uri; - name: string; - nameToRun: string; - subtestParent?: SubtestParent; -}; - -export type SubtestParent = TestResult & { - name: string; - nameToRun: string; - asSuite: TestSuite; -}; - -export type TestResult = Node & { - status?: TestStatus; - 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; - 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 = 'Unknown', - Discovering = 'Discovering', - Idle = 'Idle', - Running = 'Running', - Fail = 'Fail', - Error = 'Error', - Skipped = 'Skipped', - Pass = 'Pass' -} - -export type TestsToRun = { - testFolder?: TestFolder[]; - testFile?: TestFile[]; - testSuite?: TestSuite[]; - testFunction?: TestFunction[]; + pytestPort?: string; + pytestUUID?: string; + runTestIdsPort?: string; + /** Optional Python project for project-based execution. */ + project?: PythonProject; }; -export type UnitTestProduct = Product.nosetest | Product.pytest | Product.unittest; - -export interface ITestManagerService extends Disposable { - getTestManager(): ITestManager | undefined; - getTestWorkingDirectory(): string; - getPreferredTestManager(): UnitTestProduct | undefined; +export enum TestFilter { + removeTests = 'removeTests', + discovery = 'discovery', + runAll = 'runAll', + runSpecific = 'runSpecific', + debugAll = 'debugAll', + debugSpecific = 'debugSpecific', } -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 ITestingSettings; - argsName: keyof ITestingSettings; - pathName?: keyof ITestingSettings; -}; +// **************** +// interfaces export const ITestsHelper = Symbol('ITestsHelper'); - export interface ITestsHelper { parseProviderName(product: UnitTestProduct): TestProvider; parseProduct(provider: TestProvider): UnitTestProduct; getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; - flattenTestFiles(testFiles: TestFile[], workspaceFolder: string): Tests; - placeTestFilesIntoFolders(tests: Tests, workspaceFolder: string): void; - displayTestErrorMessage(message: string): void; - shouldRunAllTests(testsToRun?: TestsToRun): boolean; - mergeTests(items: Tests[]): Tests; } -export const ITestVisitor = Symbol('ITestVisitor'); - -export interface ITestVisitor { - visitTestFunction(testFunction: TestFunction): void; - visitTestSuite(testSuite: TestSuite): void; - visitTestFile(testFile: TestFile): void; - visitTestFolder(testFile: TestFolder): void; +export const ITestConfigurationService = Symbol('ITestConfigurationService'); +export interface ITestConfigurationService { + hasConfiguredTests(wkspace: Uri): boolean; + selectTestRunner(placeHolderMessage: string): Promise; + enableTest(wkspace: Uri, product: UnitTestProduct): Promise; + promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise; } -export const ITestCollectionStorageService = Symbol('ITestCollectionStorageService'); - -export interface ITestCollectionStorageService extends Disposable { - onDidChange: Event<{ uri: Uri; data?: TestDataItem }>; - getTests(wkspace: Uri): Tests | undefined; - storeTests(wkspace: Uri, tests: Tests | null | undefined): void; - findFlattendTestFunction(resource: Uri, func: TestFunction): FlattenedTestFunction | undefined; - findFlattendTestSuite(resource: Uri, suite: TestSuite): FlattenedTestSuite | undefined; - update(resource: Uri, item: TestDataItem): void; +export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); +export interface ITestConfigSettingsService { + updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise; + enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; + getTestEnablingSetting(product: UnitTestProduct): string; } -export const ITestResultsService = Symbol('ITestResultsService'); - -export interface ITestResultsService { - resetResults(tests: Tests): void; - updateResults(tests: Tests): void; +export interface ITestConfigurationManager { + requiresUserToConfigure(wkspace: Uri): Promise; + configure(wkspace: Uri): Promise; + enable(): Promise; + disable(): Promise; } -export type LaunchOptions = { - cwd: string; - args: string[]; - testProvider: TestProvider; - token?: CancellationToken; - outChannel?: OutputChannel; -}; - +export const ITestConfigurationManagerFactory = Symbol('ITestConfigurationManagerFactory'); +export interface ITestConfigurationManagerFactory { + create(wkspace: Uri, product: Product, cfg?: ITestConfigSettingsService): ITestConfigurationManager; +} export const ITestDebugLauncher = Symbol('ITestDebugLauncher'); - export interface ITestDebugLauncher { - launchDebugger(options: LaunchOptions): Promise; -} - -export const ITestManagerFactory = Symbol('ITestManagerFactory'); - -export interface ITestManagerFactory extends Function { - // tslint:disable-next-line:callable-types - (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string): ITestManager; -} -export const ITestManagerServiceFactory = Symbol('TestManagerServiceFactory'); - -export interface ITestManagerServiceFactory extends Function { - // tslint:disable-next-line:callable-types - (workspaceFolder: Uri): ITestManagerService; -} - -export const ITestManager = Symbol('ITestManager'); -export interface ITestManager extends Disposable { - readonly status: TestStatus; - readonly enabled: boolean; - readonly workingDirectory: string; - readonly workspaceFolder: Uri; - diagnosticCollection: DiagnosticCollection; - readonly onDidStatusChange: Event; - stop(): void; - resetTestResults(): void; - discoverTests(cmdSource: CommandSource, ignoreCache?: boolean, quietMode?: boolean, userInitiated?: boolean, clearTestStatus?: boolean): Promise; - runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; -} - -export const ITestDiscoveryService = Symbol('ITestDiscoveryService'); - -export interface ITestDiscoveryService { - discoverTests(options: TestDiscoveryOptions): Promise; -} - -export const ITestsParser = Symbol('ITestsParser'); -export interface ITestsParser { - parse(content: string, options: ParserOptions): Tests; -} - -export 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; - 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; -} - -export enum PassCalculationFormulae { - pytest, - nosetests -} - -export const IXUnitParser = Symbol('IXUnitParser'); -export interface IXUnitParser { - updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise; -} - -export type PythonVersionInformation = { - major: number; - minor: number; -}; - -export const ITestMessageService = Symbol('ITestMessageService'); -export interface ITestMessageService { - getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise; -} - -export interface ITestDebugConfig extends DebugConfiguration { - type: typeof DebuggerTypeName; - request: 'test'; - - pythonPath?: string; - console?: ConsoleType; - cwd?: string; - env?: Record; - envFile?: string; - - // converted to DebugOptions: - stopOnEntry?: boolean; - showReturnValue?: boolean; - redirectOutput?: boolean; // default: true - debugStdLib?: boolean; - justMyCode?: boolean; - subProcess?: boolean; -} -export const ITestContextService = Symbol('ITestContextService'); -export interface ITestContextService extends Disposable { - register(): void; -} - -export const ITestsStatusUpdaterService = Symbol('ITestsStatusUpdaterService'); -export interface ITestsStatusUpdaterService { - updateStatusAsDiscovering(resource: Uri, tests?: Tests): void; - updateStatusAsUnknown(resource: Uri, tests?: Tests): void; - updateStatusAsRunning(resource: Uri, tests?: Tests): void; - updateStatusAsRunningFailedTests(resource: Uri, tests?: Tests): void; - updateStatusAsRunningSpecificTests(resource: Uri, testsToRun: TestsToRun, tests?: Tests): void; - updateStatusOfRunningTestsAsIdle(resource: Uri, tests?: Tests): void; - triggerUpdatesToTests(resource: Uri, tests?: Tests): void; + launchDebugger(options: LaunchOptions, callback?: () => void, sessionOptions?: DebugSessionOptions): Promise; } diff --git a/src/client/testing/common/updateTestSettings.ts b/src/client/testing/common/updateTestSettings.ts deleted file mode 100644 index 3217a5edff64..000000000000 --- a/src/client/testing/common/updateTestSettings.ts +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { IExtensionActivationService } from '../../activation/types'; -import { IApplicationEnvironment, IWorkspaceService } from '../../common/application/types'; -import '../../common/extensions'; -import { traceDecorators, traceError } from '../../common/logger'; -import { IFileSystem } from '../../common/platform/types'; -import { Resource } from '../../common/types'; -import { swallowExceptions } from '../../common/utils/decorators'; - -@injectable() -export class UpdateTestSettingService implements IExtensionActivationService { - constructor(@inject(IFileSystem) private readonly fs: IFileSystem, - @inject(IApplicationEnvironment) private readonly application: IApplicationEnvironment, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService) { - } - public async activate(resource: Resource): Promise { - this.updateTestSettings(resource).ignoreErrors(); - } - @traceDecorators.error('Failed to update test settings') - public async updateTestSettings(resource: Resource): Promise { - const filesToBeFixed = await this.getFilesToBeFixed(resource); - await Promise.all(filesToBeFixed.map(file => this.fixSettingInFile(file))); - } - public getSettingsFiles(resource: Resource) { - const settingsFiles: string[] = []; - if (this.application.userSettingsFile) { - settingsFiles.push(this.application.userSettingsFile); - } - const workspaceFolder = this.workspace.getWorkspaceFolder(resource); - if (workspaceFolder) { - settingsFiles.push(path.join(workspaceFolder.uri.fsPath, '.vscode', 'settings.json')); - } - return settingsFiles; - } - public async getFilesToBeFixed(resource: Resource) { - const files = this.getSettingsFiles(resource); - const result = await Promise.all(files.map(async file => { - const needsFixing = await this.doesFileNeedToBeFixed(file); - return { file, needsFixing }; - })); - return result.filter(item => item.needsFixing).map(item => item.file); - } - @swallowExceptions('Failed to update settings.json') - public async fixSettingInFile(filePath: string) { - let fileContents = await this.fs.readFile(filePath); - const setting = new RegExp('"python.unitTest', 'g'); - const setting_pytest_enabled = new RegExp('.pyTestEnabled"', 'g'); - const setting_pytest_args = new RegExp('.pyTestArgs"', 'g'); - const setting_pytest_path = new RegExp('.pyTestPath"', 'g'); - - fileContents = fileContents.replace(setting, '"python.testing'); - fileContents = fileContents.replace(setting_pytest_enabled, '.pytestEnabled"'); - fileContents = fileContents.replace(setting_pytest_args, '.pytestArgs"'); - fileContents = fileContents.replace(setting_pytest_path, '.pytestPath"'); - await this.fs.writeFile(filePath, fileContents); - } - public async doesFileNeedToBeFixed(filePath: string) { - try { - const contents = await this.fs.readFile(filePath); - return contents.indexOf('python.unitTest.') > 0 || contents.indexOf('.pyTest') > 0; - } catch (ex) { - traceError('Failed to check if file needs to be fixed', ex); - return false; - } - } -} diff --git a/src/client/testing/common/xUnitParser.ts b/src/client/testing/common/xUnitParser.ts deleted file mode 100644 index e315ae9b8341..000000000000 --- a/src/client/testing/common/xUnitParser.ts +++ /dev/null @@ -1,147 +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 { - return updateResultsFromXmlLogFile(tests, outputXmlFile, passCalculationFormulae); - } -} -export function updateResultsFromXmlLogFile(tests: Tests, outputXmlFile: string, passCalculationFormulae: PassCalculationFormulae): Promise { - // tslint:disable-next-line:no-any - return new Promise((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: Error, parserResult: { testsuite: TestSuiteResult }) => { - if (error) { - return reject(error); - } - try { - 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 = ''; - } - }); - } catch (ex) { - return reject(ex); - } - - resolve(); - }); - }); - }); -} diff --git a/src/client/testing/configuration.ts b/src/client/testing/configuration.ts deleted file mode 100644 index 68abfe264449..000000000000 --- a/src/client/testing/configuration.ts +++ /dev/null @@ -1,146 +0,0 @@ -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../common/application/types'; -import { traceError } from '../common/logger'; -import { IConfigurationService, Product } from '../common/types'; -import { IServiceContainer } from '../ioc/types'; -import { sendTelemetryEvent } from '../telemetry'; -import { EventName } from '../telemetry/constants'; -import { TestConfiguringTelemetry, TestTool } from '../telemetry/types'; -import { BufferedTestConfigSettingsService } from './common/services/configSettingService'; -import { ITestsHelper, UnitTestProduct } from './common/types'; -import { - ITestConfigSettingsService, ITestConfigurationManager, - ITestConfigurationManagerFactory, ITestConfigurationService -} from './types'; - -@injectable() -export class UnitTestConfigurationService implements ITestConfigurationService { - private readonly configurationService: IConfigurationService; - private readonly appShell: IApplicationShell; - private readonly workspaceService: IWorkspaceService; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.configurationService = serviceContainer.get(IConfigurationService); - this.appShell = serviceContainer.get(IApplicationShell); - this.workspaceService = serviceContainer.get(IWorkspaceService); - } - public async displayTestFrameworkError(wkspace: Uri): Promise { - const settings = this.configurationService.getSettings(wkspace); - let enabledCount = settings.testing.pytestEnabled ? 1 : 0; - enabledCount += settings.testing.nosetestsEnabled ? 1 : 0; - enabledCount += settings.testing.unittestEnabled ? 1 : 0; - if (enabledCount > 1) { - return this._promptToEnableAndConfigureTestFramework(wkspace, 'Enable only one of the test frameworks (unittest, pytest or nosetest).', true); - } else { - const option = 'Enable and configure a Test Framework'; - const item = await this.appShell.showInformationMessage('No test framework configured (unittest, pytest or nosetest)', option); - if (item === option) { - return this._promptToEnableAndConfigureTestFramework(wkspace); - } - return Promise.reject(null); - } - } - public async selectTestRunner(placeHolderMessage: string): Promise { - const items = [{ - label: 'unittest', - product: Product.unittest, - description: 'Standard Python test framework', - detail: 'https://docs.python.org/3/library/unittest.html' - }, - { - label: 'pytest', - product: Product.pytest, - description: 'pytest framework', - // tslint:disable-next-line:no-http-string - detail: 'http://docs.pytest.org/' - }, - { - label: 'nose', - product: Product.nosetest, - description: 'nose framework', - detail: 'https://nose.readthedocs.io/' - }]; - const options = { - ignoreFocusOut: true, - matchOnDescription: true, - matchOnDetail: true, - placeHolder: placeHolderMessage - }; - const selectedTestRunner = await this.appShell.showQuickPick(items, options); - // tslint:disable-next-line:prefer-type-cast - return selectedTestRunner ? selectedTestRunner.product as UnitTestProduct : undefined; - } - public async enableTest(wkspace: Uri, product: UnitTestProduct): Promise { - const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); - const configMgr = factory.create(wkspace, product); - return this._enableTest(wkspace, configMgr); - } - - public async promptToEnableAndConfigureTestFramework(wkspace: Uri) { - await this._promptToEnableAndConfigureTestFramework( - wkspace, - undefined, - false, - 'commandpalette' - ); - } - - private _enableTest(wkspace: Uri, configMgr: ITestConfigurationManager) { - const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); - if (pythonConfig.get('testing.promptToConfigure')) { - return configMgr.enable(); - } - return pythonConfig.update('testing.promptToConfigure', undefined).then(() => { - return configMgr.enable(); - }, reason => { - return configMgr.enable() - .then(() => Promise.reject(reason)); - }); - } - - private async _promptToEnableAndConfigureTestFramework( - wkspace: Uri, - messageToDisplay: string = 'Select a test framework/tool to enable', - enableOnly: boolean = false, - trigger: 'ui' | 'commandpalette' = 'ui' - ) { - const telemetryProps: TestConfiguringTelemetry = { - trigger: trigger, - failed: false - }; - try { - const selectedTestRunner = await this.selectTestRunner(messageToDisplay); - if (typeof selectedTestRunner !== 'number') { - return Promise.reject(null); - } - const helper = this.serviceContainer.get(ITestsHelper); - telemetryProps.tool = helper.parseProviderName(selectedTestRunner) as TestTool; - const delayed = new BufferedTestConfigSettingsService(); - const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); - const configMgr = factory.create(wkspace, selectedTestRunner, delayed); - if (enableOnly) { - await configMgr.enable(); - } else { - // Configure everything before enabling. - // Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) - // to start discovering tests when tests haven't been configured properly. - await configMgr.configure(wkspace) - .then(() => this._enableTest(wkspace, configMgr)) - .catch(reason => { - return this._enableTest(wkspace, configMgr).then(() => Promise.reject(reason)); - }); - } - const cfg = this.serviceContainer.get(ITestConfigSettingsService); - try { - await delayed.apply(cfg); - } catch (exc) { - traceError('Python Extension: applying unit test config updates', exc); - telemetryProps.failed = true; - } - } finally { - sendTelemetryEvent(EventName.UNITTEST_CONFIGURING, undefined, telemetryProps); - } - } -} diff --git a/src/client/testing/configuration/index.ts b/src/client/testing/configuration/index.ts new file mode 100644 index 000000000000..b78475293594 --- /dev/null +++ b/src/client/testing/configuration/index.ts @@ -0,0 +1,135 @@ +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../common/application/types'; +import { IConfigurationService, Product } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { traceError } from '../../logging'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { TestConfiguringTelemetry } from '../../telemetry/types'; +import { BufferedTestConfigSettingsService } from '../common/bufferedTestConfigSettingService'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + ITestConfigurationManagerFactory, + ITestConfigurationService, + ITestsHelper, + UnitTestProduct, +} from '../common/types'; + +export const NONE_SELECTED = Error('none selected'); + +@injectable() +export class UnitTestConfigurationService implements ITestConfigurationService { + private readonly configurationService: IConfigurationService; + + private readonly appShell: IApplicationShell; + + private readonly workspaceService: IWorkspaceService; + + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.configurationService = serviceContainer.get(IConfigurationService); + this.appShell = serviceContainer.get(IApplicationShell); + this.workspaceService = serviceContainer.get(IWorkspaceService); + } + + public hasConfiguredTests(wkspace: Uri): boolean { + const settings = this.configurationService.getSettings(wkspace); + return settings.testing.pytestEnabled || settings.testing.unittestEnabled || false; + } + + public async selectTestRunner(placeHolderMessage: string): Promise { + const items = [ + { + label: 'unittest', + product: Product.unittest, + description: 'Standard Python test framework', + detail: 'https://docs.python.org/3/library/unittest.html', + }, + { + label: 'pytest', + product: Product.pytest, + description: 'pytest framework', + + detail: 'http://docs.pytest.org/', + }, + ]; + const options = { + ignoreFocusOut: true, + matchOnDescription: true, + matchOnDetail: true, + placeHolder: placeHolderMessage, + }; + const selectedTestRunner = await this.appShell.showQuickPick(items, options); + + return selectedTestRunner ? (selectedTestRunner.product as UnitTestProduct) : undefined; + } + + public async enableTest(wkspace: Uri, product: UnitTestProduct): Promise { + const factory = this.serviceContainer.get(ITestConfigurationManagerFactory); + const configMgr = factory.create(wkspace, product); + return this._enableTest(wkspace, configMgr); + } + + public async promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise { + await this._promptToEnableAndConfigureTestFramework(wkspace, undefined, false, 'commandpalette'); + } + + private _enableTest(wkspace: Uri, configMgr: ITestConfigurationManager) { + const pythonConfig = this.workspaceService.getConfiguration('python', wkspace); + if (pythonConfig.get('testing.promptToConfigure')) { + return configMgr.enable(); + } + return pythonConfig.update('testing.promptToConfigure', undefined).then( + () => configMgr.enable(), + (reason) => configMgr.enable().then(() => Promise.reject(reason)), + ); + } + + private async _promptToEnableAndConfigureTestFramework( + wkspace: Uri, + messageToDisplay = 'Select a test framework/tool to enable', + enableOnly = false, + trigger: 'ui' | 'commandpalette' = 'ui', + ): Promise { + const telemetryProps: TestConfiguringTelemetry = { + trigger, + failed: false, + }; + try { + const selectedTestRunner = await this.selectTestRunner(messageToDisplay); + if (typeof selectedTestRunner !== 'number') { + throw NONE_SELECTED; + } + const helper = this.serviceContainer.get(ITestsHelper); + telemetryProps.tool = helper.parseProviderName(selectedTestRunner); + const delayed = new BufferedTestConfigSettingsService(); + const factory = this.serviceContainer.get( + ITestConfigurationManagerFactory, + ); + const configMgr = factory.create(wkspace, selectedTestRunner, delayed); + if (enableOnly) { + await configMgr.enable(); + } else { + // Configure everything before enabling. + // Cuz we don't want the test engine (in main.ts file - tests get discovered when config changes are detected) + // to start discovering tests when tests haven't been configured properly. + await configMgr + .configure(wkspace) + .then(() => this._enableTest(wkspace, configMgr)) + .catch((reason) => this._enableTest(wkspace, configMgr).then(() => Promise.reject(reason))); + } + const cfg = this.serviceContainer.get(ITestConfigSettingsService); + try { + await delayed.apply(cfg); + } catch (exc) { + traceError('Python Extension: applying unit test config updates', exc); + telemetryProps.failed = true; + } + } finally { + sendTelemetryEvent(EventName.UNITTEST_CONFIGURING, undefined, telemetryProps); + } + } +} diff --git a/src/client/testing/configuration/pytest/testConfigurationManager.ts b/src/client/testing/configuration/pytest/testConfigurationManager.ts new file mode 100644 index 000000000000..08f88f8564c7 --- /dev/null +++ b/src/client/testing/configuration/pytest/testConfigurationManager.ts @@ -0,0 +1,78 @@ +import * as path from 'path'; +import { QuickPickItem, Uri } from 'vscode'; +import { IFileSystem } from '../../../common/platform/types'; +import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { IApplicationShell } from '../../../common/application/types'; +import { TestConfigurationManager } from '../../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../../common/types'; +import { PytestInstallationHelper } from '../pytestInstallationHelper'; +import { traceInfo } from '../../../logging'; + +export class ConfigurationManager extends TestConfigurationManager { + private readonly pytestInstallationHelper: PytestInstallationHelper; + + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { + super(workspace, Product.pytest, serviceContainer, cfg); + const appShell = serviceContainer.get(IApplicationShell); + this.pytestInstallationHelper = new PytestInstallationHelper(appShell); + } + + public async requiresUserToConfigure(wkspace: Uri): Promise { + const configFiles = await this.getConfigFiles(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return false; + } + return true; + } + + public async configure(wkspace: Uri): Promise { + const args: string[] = []; + const configFileOptionLabel = 'Use existing config file'; + const options: QuickPickItem[] = []; + const configFiles = await this.getConfigFiles(wkspace.fsPath); + // If a config file exits, there's nothing to be configured. + if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { + return; + } + + if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { + options.push({ + label: configFileOptionLabel, + description: 'setup.cfg', + }); + } + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs, options); + if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { + args.push(testDir); + } + const installed = await this.installer.isInstalled(Product.pytest); + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); + if (!installed) { + // Check if Python Environments extension is available for enhanced installation flow + if (this.pytestInstallationHelper.isEnvExtensionAvailable()) { + traceInfo('pytest not installed, prompting user with environment extension integration'); + const installAttempted = await this.pytestInstallationHelper.promptToInstallPytest(wkspace); + if (!installAttempted) { + // User chose to ignore or installation failed + return; + } + } else { + // Fall back to traditional installer + traceInfo('pytest not installed, falling back to traditional installer'); + await this.installer.install(Product.pytest); + } + } + } + + private async getConfigFiles(rootDir: string): Promise { + const fs = this.serviceContainer.get(IFileSystem); + const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'].map(async (cfg) => + (await fs.fileExists(path.join(rootDir, cfg))) ? cfg : '', + ); + const values = await Promise.all(promises); + return values.filter((exists) => exists.length > 0); + } +} diff --git a/src/client/testing/configuration/pytestInstallationHelper.ts b/src/client/testing/configuration/pytestInstallationHelper.ts new file mode 100644 index 000000000000..bd5fbcd5bb37 --- /dev/null +++ b/src/client/testing/configuration/pytestInstallationHelper.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri, l10n } from 'vscode'; +import { IApplicationShell } from '../../common/application/types'; +import { traceInfo, traceError } from '../../logging'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { getEnvironment } from '../../envExt/api.internal'; + +/** + * Helper class to handle pytest installation using the appropriate method + * based on whether the Python Environments extension is available. + */ +export class PytestInstallationHelper { + constructor(private readonly appShell: IApplicationShell) {} + + /** + * Prompts the user to install pytest with appropriate installation method. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was attempted, false otherwise + */ + async promptToInstallPytest(workspaceUri: Uri): Promise { + const message = l10n.t('pytest selected but not installed. Would you like to install pytest?'); + const installOption = l10n.t('Install pytest'); + + const selection = await this.appShell.showInformationMessage(message, { modal: true }, installOption); + + if (selection === installOption) { + return this.installPytest(workspaceUri); + } + + return false; + } + + /** + * Installs pytest using the appropriate method based on available extensions. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytest(workspaceUri: Uri): Promise { + try { + if (useEnvExtension()) { + return this.installPytestWithEnvExtension(workspaceUri); + } else { + // Fall back to traditional installer if environments extension is not available + traceInfo( + 'Python Environments extension not available, installation cannot proceed via environment extension', + ); + return false; + } + } catch (error) { + traceError('Error installing pytest:', error); + return false; + } + } + + /** + * Installs pytest using the Python Environments extension. + * @param workspaceUri The workspace URI where pytest should be installed + * @returns Promise that resolves to true if installation was successful, false otherwise + */ + private async installPytestWithEnvExtension(workspaceUri: Uri): Promise { + try { + const envExtApi = await getEnvExtApi(); + const environment = await getEnvironment(workspaceUri); + + if (!environment) { + traceError('No Python environment found for workspace:', workspaceUri.fsPath); + await this.appShell.showErrorMessage( + l10n.t('No Python environment found. Please set up a Python environment first.'), + ); + return false; + } + + traceInfo('Installing pytest using Python Environments extension...'); + await envExtApi.managePackages(environment, { + install: ['pytest'], + }); + + traceInfo('pytest installation completed successfully'); + return true; + } catch (error) { + traceError('Failed to install pytest using Python Environments extension:', error); + return false; + } + } + + /** + * Checks if the Python Environments extension is available for package management. + * @returns True if the extension is available, false otherwise + */ + isEnvExtensionAvailable(): boolean { + return useEnvExtension(); + } +} diff --git a/src/client/testing/configuration/types.ts b/src/client/testing/configuration/types.ts new file mode 100644 index 000000000000..3b759bcb39e8 --- /dev/null +++ b/src/client/testing/configuration/types.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +export interface ITestingSettings { + readonly promptToConfigure: boolean; + readonly debugPort: number; + readonly pytestEnabled: boolean; + pytestPath: string; + pytestArgs: string[]; + readonly unittestEnabled: boolean; + unittestArgs: string[]; + cwd?: string; + readonly autoTestDiscoverOnSaveEnabled: boolean; + readonly autoTestDiscoverOnSavePattern: string; +} + +export type TestSettingsPropertyNames = { + enabledName: keyof ITestingSettings; + argsName: keyof ITestingSettings; + pathName?: keyof ITestingSettings; +}; diff --git a/src/client/testing/configuration/unittest/testConfigurationManager.ts b/src/client/testing/configuration/unittest/testConfigurationManager.ts new file mode 100644 index 000000000000..b1482c2a42bc --- /dev/null +++ b/src/client/testing/configuration/unittest/testConfigurationManager.ts @@ -0,0 +1,37 @@ +import { Uri } from 'vscode'; +import { Product } from '../../../common/types'; +import { IServiceContainer } from '../../../ioc/types'; +import { TestConfigurationManager } from '../../common/testConfigurationManager'; +import { ITestConfigSettingsService } from '../../common/types'; + +export class ConfigurationManager extends TestConfigurationManager { + constructor(workspace: Uri, serviceContainer: IServiceContainer, cfg?: ITestConfigSettingsService) { + super(workspace, Product.unittest, serviceContainer, cfg); + } + + // eslint-disable-next-line class-methods-use-this + public async requiresUserToConfigure(_wkspace: Uri): Promise { + return true; + } + + public async configure(wkspace: Uri): Promise { + const args = ['-v']; + const subDirs = await this.getTestDirs(wkspace.fsPath); + const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); + args.push('-s'); + if (typeof testDir === 'string' && testDir !== '.') { + args.push(`./${testDir}`); + } else { + args.push('.'); + } + + const testfilePattern = await this.selectTestFilePattern(); + args.push('-p'); + if (typeof testfilePattern === 'string') { + args.push(testfilePattern); + } else { + args.push('test*.py'); + } + await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.unittest, args); + } +} diff --git a/src/client/testing/configurationFactory.ts b/src/client/testing/configurationFactory.ts index 967c3a82b9a9..b661a38d5779 100644 --- a/src/client/testing/configurationFactory.ts +++ b/src/client/testing/configurationFactory.ts @@ -7,17 +7,17 @@ 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 * as pytest from './configuration/pytest/testConfigurationManager'; import { - ITestConfigSettingsService, ITestConfigurationManager, - ITestConfigurationManagerFactory -} from './types'; -import * as unittest from './unittest/testConfigurationManager'; + 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) { } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} public create(wkspace: Uri, product: Product, cfg?: ITestConfigSettingsService): ITestConfigurationManager { switch (product) { case Product.unittest: { @@ -26,13 +26,9 @@ export class TestConfigurationManagerFactory implements ITestConfigurationManage case Product.pytest: { return new pytest.ConfigurationManager(wkspace, this.serviceContainer, cfg); } - case Product.nosetest: { - return new nose.ConfigurationManager(wkspace, this.serviceContainer, cfg); - } default: { throw new Error('Invalid test configuration'); } } } - } diff --git a/src/client/testing/display/main.ts b/src/client/testing/display/main.ts deleted file mode 100644 index e33487afff02..000000000000 --- a/src/client/testing/display/main.ts +++ /dev/null @@ -1,209 +0,0 @@ -'use strict'; -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter, StatusBarAlignment, StatusBarItem } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import * as constants from '../../common/constants'; -import { isNotInstalledError } from '../../common/helpers'; -import { IConfigurationService } from '../../common/types'; -import { Testing } from '../../common/utils/localize'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -import { captureTelemetry } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { CANCELLATION_REASON } from '../common/constants'; -import { ITestsHelper, Tests } from '../common/types'; -import { ITestResultDisplay } from '../types'; - -@injectable() -export class TestResultDisplay implements ITestResultDisplay { - private statusBar: StatusBarItem; - private discoverCounter = 0; - private ticker = ['|', '/', '-', '|', '/', '-', '\\']; - private progressTimeout: NodeJS.Timer | number | null = null; - private _enabled: boolean = false; - private progressPrefix!: string; - private readonly didChange = new EventEmitter(); - private readonly appShell: IApplicationShell; - private readonly testsHelper: ITestsHelper; - private readonly cmdManager: ICommandManager; - public get onDidChange(): Event { - return this.didChange.event; - } - - // tslint:disable-next-line:no-any - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.appShell = serviceContainer.get(IApplicationShell); - this.statusBar = this.appShell.createStatusBarItem(StatusBarAlignment.Left); - this.testsHelper = serviceContainer.get(ITestsHelper); - this.cmdManager = serviceContainer.get(ICommandManager); - } - public dispose() { - this.clearProgressTicker(); - this.statusBar.dispose(); - } - public get enabled() { - return this._enabled; - } - public set enabled(enable: boolean) { - this._enabled = enable; - if (enable) { - this.statusBar.show(); - } else { - this.statusBar.hide(); - } - } - public displayProgressStatus(testRunResult: Promise, debug: boolean = false) { - this.displayProgress('Running Tests', 'Running Tests (Click to Stop)', constants.Commands.Tests_Ask_To_Stop_Test); - testRunResult - .then(tests => this.updateTestRunWithSuccess(tests, debug)) - .catch(this.updateTestRunWithFailure.bind(this)) - // We don't care about any other exceptions returned by updateTestRunWithFailure - .catch(noop); - } - public displayDiscoverStatus(testDiscovery: Promise, quietMode: boolean = false) { - this.displayProgress('Discovering Tests', 'Discovering tests (click to stop)', constants.Commands.Tests_Ask_To_Stop_Discovery); - return testDiscovery - .then(tests => { - this.updateWithDiscoverSuccess(tests, quietMode); - return tests; - }) - .catch(reason => { - this.updateWithDiscoverFailure(reason); - return Promise.reject(reason); - }); - } - - private updateTestRunWithSuccess(tests: Tests, debug: boolean = false): Tests { - this.clearProgressTicker(); - - // Treat errors as a special case, as we generally wouldn't have any errors - const statusText: string[] = []; - const toolTip: string[] = []; - 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 { - 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) { - // tslint:disable-next-line: no-any - clearInterval(this.progressTimeout as any); - } - this.progressTimeout = null; - this.discoverCounter = 0; - } - - @captureTelemetry(EventName.UNITTEST_DISABLE) - // tslint:disable-next-line:no-any - private async disableTests(): Promise { - const configurationService = this.serviceContainer.get(IConfigurationService); - const settingsToDisable = ['testing.promptToConfigure', 'testing.pytestEnabled', 'testing.unittestEnabled', 'testing.nosetestsEnabled']; - - for (const setting of settingsToDisable) { - await configurationService.updateSetting(setting, false).catch(noop); - } - this.cmdManager.executeCommand('setContext', 'testsDiscovered', false); - } - - private updateWithDiscoverSuccess(tests: Tests, quietMode: boolean = false) { - this.clearProgressTicker(); - const haveTests = tests && tests.testFunctions.length > 0; - this.statusBar.text = '$(zap) Run Tests'; - this.statusBar.tooltip = 'Run Tests'; - this.statusBar.command = constants.Commands.Tests_View_UI; - this.statusBar.show(); - if (this.didChange) { - this.didChange.fire(); - } - - if (!haveTests && !quietMode) { - this.appShell - .showInformationMessage('No tests discovered, please check the configuration settings for the tests.', Testing.disableTests(), Testing.configureTests()) - .then(item => { - if (item === Testing.disableTests()) { - this.disableTests().catch(ex => console.error('Python Extension: disableTests', ex)); - } else if (item === Testing.configureTests()) { - this.cmdManager.executeCommand(constants.Commands.Tests_Configure, undefined, undefined, undefined).then(noop); - } - }); - } - } - - // tslint:disable-next-line:no-any - private updateWithDiscoverFailure(reason: any) { - this.clearProgressTicker(); - this.statusBar.text = '$(zap) Discover Tests'; - this.statusBar.tooltip = 'Discover Tests'; - this.statusBar.command = constants.Commands.Tests_Discover; - this.statusBar.show(); - 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/testing/display/picker.ts b/src/client/testing/display/picker.ts deleted file mode 100644 index 606057c08a39..000000000000 --- a/src/client/testing/display/picker.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { QuickPickItem, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import * as constants from '../../common/constants'; -import { 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, - @inject(ICommandManager) private readonly commandManager: ICommandManager) { - this.testCollectionStorage = serviceRegistry.get(ITestCollectionStorageService); - this.appShell = serviceRegistry.get(IApplicationShell); - } - public displayStopTestUI(workspace: Uri, message: string) { - this.appShell.showQuickPick([message]).then(item => { - if (item === message) { - this.commandManager.executeCommand(constants.Commands.Tests_Stop, undefined, workspace); - } - }); - } - public displayTestUI(cmdSource: CommandSource, wkspace: Uri) { - const tests = this.testCollectionStorage.getTests(wkspace); - this.appShell.showQuickPick(buildItems(tests), { matchOnDescription: true, matchOnDetail: true }) - .then(item => item ? onItemSelected(this.commandManager, cmdSource, wkspace, item, false) : Promise.resolve()); - } - public selectTestFunction(rootDirectory: string, tests: Tests): Promise { - return new Promise((resolve, reject) => { - this.appShell.showQuickPick(buildItemsForFunctions(rootDirectory, tests.testFunctions), { matchOnDescription: true, matchOnDetail: true }) - .then(item => { - if (item && item.fn) { - return resolve(item.fn); - } - return reject(); - }, reject); - }); - } - public selectTestFile(rootDirectory: string, tests: Tests): Promise { - return new Promise((resolve, reject) => { - this.appShell.showQuickPick(buildItemsForTestFiles(rootDirectory, tests.testFiles), { matchOnDescription: true, matchOnDetail: true }) - .then(item => { - if (item && item.testFile) { - return resolve(item.testFile); - } - return reject(); - }, reject); - }); - } - public displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean) { - const tests = this.testCollectionStorage.getTests(wkspace); - if (!tests) { - return; - } - const fileName = file.fsPath; - const 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(this.commandManager, cmdSource, wkspace, testItem, debug) : Promise.resolve()); - } -} - -export enum Type { - RunAll = 0, - ReDiscover = 1, - RunFailed = 2, - RunFolder = 3, - RunFile = 4, - RunClass = 5, - RunMethod = 6, - ViewTestOutput = 7, - Null = 8, - SelectAndRunMethod = 9, - DebugMethod = 10, - Configure = 11 -} -const statusIconMapping = new Map(); -statusIconMapping.set(TestStatus.Pass, constants.Octicons.Test_Pass); -statusIconMapping.set(TestStatus.Fail, constants.Octicons.Test_Fail); -statusIconMapping.set(TestStatus.Error, constants.Octicons.Test_Error); -statusIconMapping.set(TestStatus.Skipped, constants.Octicons.Test_Skip); - -type TestItem = QuickPickItem & { - type: Type; - fn?: FlattenedTestFunction; -}; - -type TestFileItem = QuickPickItem & { - type: Type; - testFile?: TestFile; -}; - -function getSummary(tests?: Tests) { - if (!tests || !tests.summary) { - return ''; - } - const statusText: string[] = []; - if (tests.summary.passed > 0) { - statusText.push(`${constants.Octicons.Test_Pass} ${tests.summary.passed} Passed`); - } - if (tests.summary.failures > 0) { - statusText.push(`${constants.Octicons.Test_Fail} ${tests.summary.failures} Failed`); - } - if (tests.summary.errors > 0) { - const plural = tests.summary.errors === 1 ? '' : 's'; - statusText.push(`${constants.Octicons.Test_Error} ${tests.summary.errors} Error${plural}`); - } - if (tests.summary.skipped > 0) { - statusText.push(`${constants.Octicons.Test_Skip} ${tests.summary.skipped} Skipped`); - } - return statusText.join(', ').trim(); -} -function buildItems(tests?: Tests): TestItem[] { - const items: TestItem[] = []; - items.push({ description: '', label: 'Run All Tests', type: Type.RunAll }); - items.push({ description: '', label: 'Discover Tests', type: Type.ReDiscover }); - items.push({ description: '', label: 'Run Test Method ...', type: Type.SelectAndRunMethod }); - items.push({ description: '', label: 'Configure Tests', type: Type.Configure }); - - const summary = getSummary(tests); - items.push({ description: '', label: 'View Test Output', type: Type.ViewTestOutput, detail: summary }); - - if (tests && tests.summary.failures > 0) { - items.push({ description: '', label: 'Run Failed Tests', type: Type.RunFailed, detail: `${constants.Octicons.Test_Fail} ${tests.summary.failures} Failed` }); - } - - return items; -} - -const statusSortPrefix = { - [TestStatus.Error]: '1', - [TestStatus.Fail]: '2', - [TestStatus.Skipped]: '3', - [TestStatus.Pass]: '4', - [TestStatus.Discovering]: undefined, - [TestStatus.Idle]: undefined, - [TestStatus.Running]: undefined, - [TestStatus.Unknown]: undefined -}; - -function buildItemsForFunctions(rootDirectory: string, tests: FlattenedTestFunction[], sortBasedOnResults: boolean = false, displayStatusIcons: boolean = false, debug: boolean = false): TestItem[] { - const functionItems: TestItem[] = []; - tests.forEach(fn => { - let icon = ''; - if (displayStatusIcons && fn.testFunction.status && statusIconMapping.has(fn.testFunction.status)) { - icon = `${statusIconMapping.get(fn.testFunction.status)} `; - } - - functionItems.push({ - description: '', - detail: path.relative(rootDirectory, fn.parentTestFile.fullPath), - label: icon + fn.testFunction.name, - type: debug === true ? Type.DebugMethod : Type.RunMethod, - fn: fn - }); - }); - functionItems.sort((a, b) => { - let sortAPrefix = '5-'; - let sortBPrefix = '5-'; - if (sortBasedOnResults && a.fn && a.fn.testFunction.status && b.fn && b.fn.testFunction.status) { - sortAPrefix = statusSortPrefix[a.fn.testFunction.status] ? statusSortPrefix[a.fn.testFunction.status]! : sortAPrefix; - sortBPrefix = statusSortPrefix[b.fn.testFunction.status] ? statusSortPrefix[b.fn.testFunction.status]! : sortBPrefix; - } - if (`${sortAPrefix}${a.detail}${a.label}` < `${sortBPrefix}${b.detail}${b.label}`) { - return -1; - } - if (`${sortAPrefix}${a.detail}${a.label}` > `${sortBPrefix}${b.detail}${b.label}`) { - return 1; - } - return 0; - }); - return functionItems; -} -function buildItemsForTestFiles(rootDirectory: string, testFiles: TestFile[]): TestFileItem[] { - const fileItems: TestFileItem[] = testFiles.map(testFile => { - return { - description: '', - detail: path.relative(rootDirectory, testFile.fullPath), - type: Type.RunFile, - label: path.basename(testFile.fullPath), - testFile: testFile - }; - }); - fileItems.sort((a, b) => { - if (!a.detail && !b.detail) { - return 0; - } - if (!a.detail || a.detail < b.detail!) { - return -1; - } - if (!b.detail || a.detail! > b.detail) { - return 1; - } - return 0; - }); - return fileItems; -} -export function onItemSelected(commandManager: ICommandManager, cmdSource: CommandSource, wkspace: Uri, selection: TestItem, debug?: boolean) { - if (!selection || typeof selection.type !== 'number') { - return; - } - switch (selection.type) { - case Type.Null: { - return; - } - case Type.RunAll: { - return commandManager.executeCommand(constants.Commands.Tests_Run, undefined, cmdSource, wkspace, undefined); - } - case Type.ReDiscover: { - return commandManager.executeCommand(constants.Commands.Tests_Discover, undefined, cmdSource, wkspace); - } - case Type.ViewTestOutput: { - return commandManager.executeCommand(constants.Commands.Tests_ViewOutput, undefined, cmdSource); - } - case Type.RunFailed: { - return commandManager.executeCommand(constants.Commands.Tests_Run_Failed, undefined, cmdSource, wkspace); - } - case Type.SelectAndRunMethod: { - const cmd = debug ? constants.Commands.Tests_Select_And_Debug_Method : constants.Commands.Tests_Select_And_Run_Method; - return commandManager.executeCommand(cmd, undefined, cmdSource, wkspace); - } - case Type.RunMethod: { - const testsToRun: TestsToRun = { testFunction: [selection.fn!.testFunction] }; - return commandManager.executeCommand(constants.Commands.Tests_Run, undefined, cmdSource, wkspace, testsToRun); - } - case Type.DebugMethod: { - const testsToRun: TestsToRun = { testFunction: [selection.fn!.testFunction] }; - return commandManager.executeCommand(constants.Commands.Tests_Debug, undefined, cmdSource, wkspace, testsToRun); - } - case Type.Configure: { - return commandManager.executeCommand(constants.Commands.Tests_Configure, undefined, cmdSource, wkspace); - } - default: { - return; - } - } -} diff --git a/src/client/testing/explorer/commandHandlers.ts b/src/client/testing/explorer/commandHandlers.ts deleted file mode 100644 index 66b5ee58d225..000000000000 --- a/src/client/testing/explorer/commandHandlers.ts +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { ICommandManager } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { traceDecorators } from '../../common/logger'; -import { IDisposable } from '../../common/types'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { CommandSource } from '../common/constants'; -import { getTestType } from '../common/testUtils'; -import { - TestFile, TestFolder, TestFunction, - TestsToRun, TestSuite, TestType -} from '../common/types'; -import { ITestExplorerCommandHandler } from '../navigation/types'; -import { ITestDataItemResource, TestDataItem } from '../types'; - -type NavigationCommands = typeof Commands.navigateToTestFile | typeof Commands.navigateToTestFunction | typeof Commands.navigateToTestSuite; -const testNavigationCommandMapping: { [key: string]: NavigationCommands } = { - [TestType.testFile]: Commands.navigateToTestFile, - [TestType.testFunction]: Commands.navigateToTestFunction, - [TestType.testSuite]: Commands.navigateToTestSuite -}; - -@injectable() -export class TestExplorerCommandHandler implements ITestExplorerCommandHandler { - private readonly disposables: IDisposable[] = []; - constructor( - @inject(ICommandManager) private readonly cmdManager: ICommandManager, - @inject(ITestDataItemResource) private readonly testResource: ITestDataItemResource - ) { } - public register(): void { - this.disposables.push(this.cmdManager.registerCommand(Commands.runTestNode, this.onRunTestNode, this)); - this.disposables.push(this.cmdManager.registerCommand(Commands.debugTestNode, this.onDebugTestNode, this)); - this.disposables.push(this.cmdManager.registerCommand(Commands.openTestNodeInEditor, this.onOpenTestNodeInEditor, this)); - } - public dispose(): void { - this.disposables.forEach(item => item.dispose()); - } - @swallowExceptions('Run test node') - @traceDecorators.error('Run test node failed') - protected async onRunTestNode(item: TestDataItem): Promise { - await this.runDebugTestNode(item, 'run'); - } - @swallowExceptions('Debug test node') - @traceDecorators.error('Debug test node failed') - protected async onDebugTestNode(item: TestDataItem): Promise { - await this.runDebugTestNode(item, 'debug'); - } - @swallowExceptions('Open test node in Editor') - @traceDecorators.error('Open test node in editor failed') - protected async onOpenTestNodeInEditor(item: TestDataItem): Promise { - const testType = getTestType(item); - if (testType === TestType.testFolder) { - throw new Error('Unknown Test Type'); - } - const command = testNavigationCommandMapping[testType]; - const testUri = this.testResource.getResource(item); - if (!command) { - throw new Error('Unknown Test Type'); - } - this.cmdManager.executeCommand(command, testUri, item, true); - } - - protected async runDebugTestNode(item: TestDataItem, runType: 'run' | 'debug'): Promise { - let testToRun: TestsToRun; - - switch (getTestType(item)) { - case TestType.testFile: { - testToRun = { testFile: [item as TestFile] }; - break; - } - case TestType.testFolder: { - testToRun = { testFolder: [item as TestFolder] }; - break; - } - case TestType.testSuite: { - testToRun = { testSuite: [item as TestSuite] }; - break; - } - case TestType.testFunction: { - testToRun = { testFunction: [item as TestFunction] }; - break; - } - default: - throw new Error('Unknown Test Type'); - } - const testUri = this.testResource.getResource(item); - const cmd = runType === 'run' ? Commands.Tests_Run : Commands.Tests_Debug; - this.cmdManager.executeCommand(cmd, undefined, CommandSource.testExplorer, testUri, testToRun); - } -} diff --git a/src/client/testing/explorer/failedTestHandler.ts b/src/client/testing/explorer/failedTestHandler.ts deleted file mode 100644 index cbdb6bab34ed..000000000000 --- a/src/client/testing/explorer/failedTestHandler.ts +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { IExtensionActivationService } from '../../activation/types'; -import { ICommandManager } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import '../../common/extensions'; -import { IDisposable, IDisposableRegistry, Resource } from '../../common/types'; -import { debounceAsync } from '../../common/utils/decorators'; -import { getTestType } from '../common/testUtils'; -import { ITestCollectionStorageService, TestStatus, TestType } from '../common/types'; -import { TestDataItem } from '../types'; - -@injectable() -export class FailedTestHandler implements IExtensionActivationService, IDisposable { - private readonly disposables: IDisposable[] = []; - private readonly failedItems: TestDataItem[] = []; - private activated: boolean = false; - constructor(@inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService) { - disposableRegistry.push(this); - } - public dispose() { - this.disposables.forEach(d => d.dispose()); - } - public async activate(_resource: Resource): Promise { - if (this.activated) { - return; - } - this.activated = true; - this.storage.onDidChange(this.onDidChangeTestData, this, this.disposables); - } - public onDidChangeTestData(args: { uri: Uri; data?: TestDataItem }): void { - if (args.data && (args.data.status === TestStatus.Error || args.data.status === TestStatus.Fail) && - getTestType(args.data) === TestType.testFunction) { - this.failedItems.push(args.data); - this.revealFailedNodes().ignoreErrors(); - } - } - - @debounceAsync(500) - private async revealFailedNodes(): Promise { - while (this.failedItems.length > 0) { - const item = this.failedItems.pop()!; - await this.commandManager.executeCommand(Commands.Test_Reveal_Test_Item, item); - } - } -} diff --git a/src/client/testing/explorer/testTreeViewItem.ts b/src/client/testing/explorer/testTreeViewItem.ts deleted file mode 100644 index dd4d614a47e5..000000000000 --- a/src/client/testing/explorer/testTreeViewItem.ts +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-classes-per-file - -import { ThemeIcon, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; -import { Commands } from '../../common/constants'; -import { getIcon } from '../../common/utils/icons'; -import { noop } from '../../common/utils/misc'; -import { Icons } from '../common/constants'; -import { getTestType, isSubtestsParent } from '../common/testUtils'; -import { TestResult, TestStatus, TestSuite, TestType } from '../common/types'; -import { TestDataItem } from '../types'; - -function getDefaultCollapsibleState(data: TestDataItem): TreeItemCollapsibleState { - return getTestType(data) === TestType.testFunction ? TreeItemCollapsibleState.None : TreeItemCollapsibleState.Collapsed; -} - -/** - * Class that represents a visual node on the - * Test Explorer tree view. Is essentially a wrapper for the underlying - * TestDataItem. - */ -export class TestTreeItem extends TreeItem { - public readonly testType: TestType; - - constructor( - public readonly resource: Uri, - public readonly data: Readonly, - collapsibleStatue: TreeItemCollapsibleState = getDefaultCollapsibleState(data) - ) { - super(data.name, collapsibleStatue); - this.testType = getTestType(this.data); - this.setCommand(); - } - public get contextValue(): string { - return this.testType; - } - - public get iconPath(): string | Uri | { light: string | Uri; dark: string | Uri } | ThemeIcon { - if (this.testType === TestType.testWorkspaceFolder) { - return ThemeIcon.Folder; - } - if (!this.data) { - return ''; - } - const status = this.data.status; - switch (status) { - case TestStatus.Error: - case TestStatus.Fail: { - return getIcon(Icons.failed); - } - case TestStatus.Pass: { - return getIcon(Icons.passed); - } - case TestStatus.Discovering: - case TestStatus.Running: { - return getIcon(Icons.discovering); - } - case TestStatus.Idle: - case TestStatus.Unknown: { - return getIcon(Icons.unknown); - } - default: { - return getIcon(Icons.unknown); - } - } - } - - public get tooltip(): string { - if (!this.data || this.testType === TestType.testWorkspaceFolder) { - return ''; - } - const result = this.data as TestResult; - if (!result.status || result.status === TestStatus.Idle || result.status === TestStatus.Unknown || result.status === TestStatus.Skipped) { - return ''; - } - if (this.testType !== TestType.testFunction) { - if (result.functionsPassed === undefined) { - return ''; - } - if (result.functionsDidNotRun) { - return `${result.functionsFailed} failed, ${result.functionsDidNotRun} not run and ${result.functionsPassed} passed`; - } - return `${result.functionsFailed} failed, ${result.functionsPassed} passed`; - } - switch (this.data.status) { - case TestStatus.Error: - case TestStatus.Fail: { - return `Failed in ${+result.time.toFixed(3)} seconds`; - } - case TestStatus.Pass: { - return `Passed in ${+result.time.toFixed(3)} seconds`; - } - case TestStatus.Discovering: - case TestStatus.Running: { - return 'Loading...'; - } - default: { - return ''; - } - } - } - - /** - * Tooltip for our tree nodes is the test status - */ - public get testStatus(): string { - return this.data.status ? this.data.status : TestStatus.Unknown; - } - - private setCommand() { - switch (this.testType) { - case TestType.testFile: { - this.command = { command: Commands.navigateToTestFile, title: 'Open', arguments: [this.resource, this.data] }; - break; - } - case TestType.testFunction: { - this.command = { command: Commands.navigateToTestFunction, title: 'Open', arguments: [this.resource, this.data, false] }; - break; - } - case TestType.testSuite: { - if (isSubtestsParent(this.data as TestSuite)) { - this.command = { command: Commands.navigateToTestFunction, title: 'Open', arguments: [this.resource, this.data, false] }; - break; - } - this.command = { command: Commands.navigateToTestSuite, title: 'Open', arguments: [this.resource, this.data, false] }; - break; - } - default: { - noop(); - } - } - } -} diff --git a/src/client/testing/explorer/testTreeViewProvider.ts b/src/client/testing/explorer/testTreeViewProvider.ts deleted file mode 100644 index d9735086c6ec..000000000000 --- a/src/client/testing/explorer/testTreeViewProvider.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Event, EventEmitter, TreeItem, TreeItemCollapsibleState, Uri } from 'vscode'; -import { ICommandManager, IWorkspaceService } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../common/types'; -import { sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { CommandSource } from '../common/constants'; -import { getChildren, getParent, getTestType } from '../common/testUtils'; -import { ITestCollectionStorageService, Tests, TestStatus, TestType } from '../common/types'; -import { ITestDataItemResource, ITestManagementService, ITestTreeViewProvider, TestDataItem, TestWorkspaceFolder, WorkspaceTestStatus } from '../types'; -import { TestTreeItem } from './testTreeViewItem'; - -@injectable() -export class TestTreeViewProvider implements ITestTreeViewProvider, ITestDataItemResource, IDisposable { - public readonly onDidChangeTreeData: Event; - public readonly discovered = new Set(); - public readonly testsAreBeingDiscovered: Map; - - private _onDidChangeTreeData = new EventEmitter(); - private disposables: IDisposable[] = []; - - constructor( - @inject(ITestCollectionStorageService) private testStore: ITestCollectionStorageService, - @inject(ITestManagementService) private testService: ITestManagementService, - @inject(IWorkspaceService) private readonly workspace: IWorkspaceService, - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry - ) { - this.onDidChangeTreeData = this._onDidChangeTreeData.event; - - disposableRegistry.push(this); - this.testsAreBeingDiscovered = new Map(); - this.disposables.push(this.testService.onDidStatusChange(this.onTestStatusChanged, this)); - this.testStore.onDidChange(e => this._onDidChangeTreeData.fire(e.data), this, this.disposables); - this.workspace.onDidChangeWorkspaceFolders(() => this._onDidChangeTreeData.fire(), this, this.disposables); - - if (Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0) { - this.refresh(workspace.workspaceFolders[0].uri); - } - } - - /** - * We need a way to map a given TestDataItem to a Uri, so that other consumers (such - * as the commandHandler for the Test Explorer) have a way of accessing the Uri outside - * the purview off the TestTreeView. - * - * @param testData Test data item to map to a Uri - * @returns A Uri representing the workspace that the test data item exists within - */ - public getResource(testData: Readonly): Uri { - return testData.resource; - } - - /** - * As the TreeViewProvider itself is getting disposed, ensure all registered listeners are disposed - * from our internal emitter. - */ - public dispose() { - this.disposables.forEach(d => d.dispose()); - this._onDidChangeTreeData.dispose(); - } - - /** - * Get [TreeItem](#TreeItem) representation of the `element` - * - * @param element The element for which [TreeItem](#TreeItem) representation is asked for. - * @return [TreeItem](#TreeItem) representation of the element - */ - public async getTreeItem(element: TestDataItem): Promise { - const defaultCollapsibleState = await this.shouldElementBeExpandedByDefault(element) ? TreeItemCollapsibleState.Expanded : undefined; - return new TestTreeItem(element.resource, element, defaultCollapsibleState); - } - - /** - * Get the children of `element` or root if no element is passed. - * - * @param element The element from which the provider gets children. Can be `undefined`. - * @return Children of `element` or root if no element is passed. - */ - public async getChildren(element?: TestDataItem): Promise { - if (element) { - if (element instanceof TestWorkspaceFolder) { - let tests = this.testStore.getTests(element.workspaceFolder.uri); - if (!tests && !this.discovered.has(element.workspaceFolder.uri.fsPath)) { - this.discovered.add(element.workspaceFolder.uri.fsPath); - await this.commandManager.executeCommand(Commands.Tests_Discover, element, CommandSource.testExplorer, undefined); - tests = this.testStore.getTests(element.workspaceFolder.uri); - } - return this.getRootNodes(tests); - } - return getChildren(element!); - } - - if (!Array.isArray(this.workspace.workspaceFolders) || this.workspace.workspaceFolders.length === 0) { - return []; - } - - sendTelemetryEvent(EventName.UNITTEST_EXPLORER_WORK_SPACE_COUNT, undefined, { count: this.workspace.workspaceFolders.length }); - - // If we are in a single workspace - if (this.workspace.workspaceFolders.length === 1) { - const tests = this.testStore.getTests(this.workspace.workspaceFolders[0].uri); - return this.getRootNodes(tests); - } - - // If we are in a mult-root workspace, then nest the test data within a - // virtual node, represending the workspace folder. - return this.workspace.workspaceFolders - .map(workspaceFolder => new TestWorkspaceFolder(workspaceFolder)); - } - - /** - * Optional method to return the parent of `element`. - * Return `null` or `undefined` if `element` is a child of root. - * - * **NOTE:** This method should be implemented in order to access [reveal](#TreeView.reveal) API. - * - * @param element The element for which the parent has to be returned. - * @return Parent of `element`. - */ - public async getParent(element: TestDataItem): Promise { - if (element instanceof TestWorkspaceFolder) { - return; - } - const tests = this.testStore.getTests(element.resource); - return tests ? getParent(tests, element) : undefined; - } - /** - * If we have test files directly in root directory, return those. - * If we have test folders and no test files under the root directory, then just return the test directories. - * The goal is not avoid returning an empty root node, when all it contains are child nodes for folders. - * - * @param {Tests} [tests] - * @returns - * @memberof TestTreeViewProvider - */ - public getRootNodes(tests?: Tests) { - if (tests && tests.rootTestFolders && tests.rootTestFolders.length === 1) { - return [...tests.rootTestFolders[0].testFiles, ...tests.rootTestFolders[0].folders]; - } - return tests ? tests.rootTestFolders : []; - } - /** - * Refresh the view by rebuilding the model and signaling the tree view to update itself. - * - * @param resource The resource 'root' for this refresh to occur under. - */ - public refresh(resource: Uri): void { - const workspaceFolder = this.workspace.getWorkspaceFolder(resource); - if (!workspaceFolder) { - return; - } - const tests = this.testStore.getTests(resource); - if (tests && tests.testFolders) { - this._onDidChangeTreeData.fire(new TestWorkspaceFolder(workspaceFolder)); - } - } - - /** - * Event handler for TestStatusChanged (coming from the ITestManagementService). - * ThisThe TreeView needs to know when we begin discovery and when discovery completes. - * - * @param e The event payload containing context for the status change - */ - private onTestStatusChanged(e: WorkspaceTestStatus) { - if (e.status === TestStatus.Discovering) { - this.testsAreBeingDiscovered.set(e.workspace.fsPath, true); - return; - } - if (!this.testsAreBeingDiscovered.get(e.workspace.fsPath)) { - return; - } - this.testsAreBeingDiscovered.set(e.workspace.fsPath, false); - this.refresh(e.workspace); - } - - private async shouldElementBeExpandedByDefault(element: TestDataItem) { - const parent = await this.getParent(element); - if (!parent || getTestType(parent) === TestType.testWorkspaceFolder) { - return true; - } - return false; - } -} diff --git a/src/client/testing/explorer/treeView.ts b/src/client/testing/explorer/treeView.ts deleted file mode 100644 index 3b03ede46f0c..000000000000 --- a/src/client/testing/explorer/treeView.ts +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { TreeView } from 'vscode'; -import { IExtensionActivationService } from '../../activation/types'; -import { IApplicationShell, ICommandManager } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IDisposable, IDisposableRegistry, Resource } from '../../common/types'; -import { ITestTreeViewProvider, TestDataItem } from '../types'; - -@injectable() -export class TreeViewService implements IExtensionActivationService, IDisposable { - private _treeView!: TreeView; - private readonly disposables: IDisposable[] = []; - private activated: boolean = false; - public get treeView(): TreeView { - return this._treeView; - } - constructor(@inject(ITestTreeViewProvider) private readonly treeViewProvider: ITestTreeViewProvider, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry, - @inject(IApplicationShell) private readonly appShell: IApplicationShell, - @inject(ICommandManager) private readonly commandManager: ICommandManager) { - disposableRegistry.push(this); - } - public dispose() { - this.disposables.forEach(d => d.dispose()); - } - public async activate(_resource: Resource): Promise { - if (this.activated) { - return; - } - this.activated = true; - this._treeView = this.appShell.createTreeView('python_tests', { showCollapseAll: true, treeDataProvider: this.treeViewProvider }); - this.disposables.push(this._treeView); - this.disposables.push(this.commandManager.registerCommand(Commands.Test_Reveal_Test_Item, this.onRevealTestItem, this)); - } - public async onRevealTestItem(testItem: TestDataItem): Promise { - await this.treeView.reveal(testItem); - } -} diff --git a/src/client/testing/main.ts b/src/client/testing/main.ts index fc71ecaa22a1..eed4d70e852c 100644 --- a/src/client/testing/main.ts +++ b/src/client/testing/main.ts @@ -1,441 +1,230 @@ 'use strict'; -// tslint:disable:no-duplicate-imports no-unnecessary-callback-wrapper - import { inject, injectable } from 'inversify'; -import { ConfigurationChangeEvent, Disposable, DocumentSymbolProvider, Event, EventEmitter, OutputChannel, TextDocument, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types'; +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 { AlwaysDisplayTestExplorerGroups } from '../common/experimentGroups'; import '../common/extensions'; -import { IConfigurationService, IDisposableRegistry, IExperimentsManager, ILogger, IOutputChannel, Resource } from '../common/types'; -import { noop } from '../common/utils/misc'; +import { IDisposableRegistry, Product } from '../common/types'; +import { IInterpreterService } from '../interpreter/contracts'; import { IServiceContainer } from '../ioc/types'; import { EventName } from '../telemetry/constants'; -import { captureTelemetry, sendTelemetryEvent } from '../telemetry/index'; -import { activateCodeLenses } from './codeLenses/main'; -import { CANCELLATION_REASON, CommandSource, TEST_OUTPUT_CHANNEL } from './common/constants'; +import { sendTelemetryEvent } from '../telemetry/index'; import { selectTestWorkspace } from './common/testUtils'; -import { ITestCollectionStorageService, ITestManager, IWorkspaceTestManagerService, TestFile, TestFunction, TestStatus, TestsToRun } from './common/types'; -import { ITestConfigurationService, ITestDisplay, ITestManagementService, ITestResultDisplay, TestWorkspaceFolder, WorkspaceTestStatus } from './types'; - -// tslint:disable:no-any +import { TestSettingsPropertyNames } from './configuration/types'; +import { ITestConfigurationService, ITestsHelper } from './common/types'; +import { ITestingService } from './types'; +import { IExtensionActivationService } from '../activation/types'; +import { ITestController } from './testController/common/types'; +import { DelayedTrigger, IDelayedTrigger } from '../common/utils/delayTrigger'; +import { ExtensionContextKey } from '../common/application/contextKeys'; +import { checkForFailedTests, updateTestResultMap } from './testController/common/testItemUtilities'; +import { Testing } from '../common/utils/localize'; +import { traceVerbose, traceWarn } from '../logging'; +import { writeTestIdToClipboard } from './utils'; @injectable() -export class UnitTestManagementService implements ITestManagementService, Disposable { - private readonly outputChannel: OutputChannel; - private activatedOnce: boolean = false; - private readonly disposableRegistry: Disposable[]; - private workspaceTestManagerService?: IWorkspaceTestManagerService; - private documentManager: IDocumentManager; - private workspaceService: IWorkspaceService; - private testResultDisplay?: ITestResultDisplay; - private autoDiscoverTimer?: NodeJS.Timer | number; - private configChangedTimer?: NodeJS.Timer | number; - private testManagers = new Set(); - private readonly _onDidStatusChange: EventEmitter = new EventEmitter(); - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.disposableRegistry = serviceContainer.get(IDisposableRegistry); - this.outputChannel = serviceContainer.get(IOutputChannel, TEST_OUTPUT_CHANNEL); - this.workspaceService = serviceContainer.get(IWorkspaceService); - this.documentManager = serviceContainer.get(IDocumentManager); +export class TestingService implements ITestingService { + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {} - this.disposableRegistry.push(this); + public getSettingsPropertyNames(product: Product): TestSettingsPropertyNames { + const helper = this.serviceContainer.get(ITestsHelper); + return helper.getSettingsPropertyNames(product); } - public dispose() { - if (this.workspaceTestManagerService) { - this.workspaceTestManagerService.dispose(); - } - if (this.configChangedTimer) { - clearTimeout(this.configChangedTimer as any); - this.configChangedTimer = undefined; - } - if (this.autoDiscoverTimer) { - clearTimeout(this.autoDiscoverTimer as any); - this.autoDiscoverTimer = undefined; - } - } - public get onDidStatusChange(): Event { - return this._onDidStatusChange.event; - } - public async activate(symbolProvider: DocumentSymbolProvider): Promise { - if (this.activatedOnce) { - return; - } - this.activatedOnce = true; - this.workspaceTestManagerService = this.serviceContainer.get(IWorkspaceTestManagerService); +} + +/** + * Registers command handlers but defers service resolution until the commands are actually invoked, + * allowing registration to happen before all services are fully initialized. + */ +export function registerTestCommands(serviceContainer: IServiceContainer): void { + // Resolve only the essential services needed for command registration itself + const disposableRegistry = serviceContainer.get(IDisposableRegistry); + const commandManager = serviceContainer.get(ICommandManager); + + // Helper function to configure tests - services are resolved when invoked, not at registration time + const configureTestsHandler = async (resource?: Uri) => { + sendTelemetryEvent(EventName.UNITTEST_CONFIGURE); + + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); - this.registerHandlers(); - this.registerCommands(); - this.checkExperiments(); - this.autoDiscoverTests(undefined).catch(ex => this.serviceContainer.get(ILogger).logError('Failed to auto discover tests upon activation', ex)); - await this.registerSymbolProvider(symbolProvider); - } - public checkExperiments() { - const experiments = this.serviceContainer.get(IExperimentsManager); - if (experiments.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)) { - const commandManager = this.serviceContainer.get(ICommandManager); - commandManager.executeCommand('setContext', 'testsDiscovered', true).then(noop, noop); - } else { - experiments.sendTelemetryIfInExperiment(AlwaysDisplayTestExplorerGroups.control); - } - } - public async getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise { let wkspace: Uri | undefined; if (resource) { - const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); + const wkspaceFolder = workspaceService.getWorkspaceFolder(resource); wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; } else { - const appShell = this.serviceContainer.get(IApplicationShell); + const appShell = serviceContainer.get(IApplicationShell); wkspace = await selectTestWorkspace(appShell); } if (!wkspace) { return; } - const testManager = this.workspaceTestManagerService!.getTestManager(wkspace); - if (testManager) { - if (!this.testManagers.has(testManager)) { - this.testManagers.add(testManager); - const handler = testManager.onDidStatusChange(e => this._onDidStatusChange.fire(e)); - this.disposableRegistry.push(handler); - } - return testManager; - } - if (displayTestNotConfiguredMessage) { - const configurationService = this.serviceContainer.get(ITestConfigurationService); - await configurationService.displayTestFrameworkError(wkspace); - } - } - public async configurationChangeHandler(eventArgs: ConfigurationChangeEvent) { - // If there's one workspace, then stop the tests and restart, - // else let the user do this manually. - if (!this.workspaceService.hasWorkspaceFolders || this.workspaceService.workspaceFolders!.length > 1) { - return; - } - if (!Array.isArray(this.workspaceService.workspaceFolders)) { - return; - } - const workspaceFolderUri = this.workspaceService.workspaceFolders.find(w => eventArgs.affectsConfiguration('python.testing', w.uri)); - if (!workspaceFolderUri) { - return; - } - const workspaceUri = workspaceFolderUri.uri; - const settings = this.serviceContainer.get(IConfigurationService).getSettings(workspaceUri); - if (!settings.testing.nosetestsEnabled && !settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { - if (this.testResultDisplay) { - this.testResultDisplay.enabled = false; - } - // tslint:disable-next-line:no-suspicious-comment - // TODO: Why are we disposing, what happens when tests are enabled. - if (this.workspaceTestManagerService) { - this.workspaceTestManagerService.dispose(); + const interpreterService = serviceContainer.get(IInterpreterService); + const cmdManager = serviceContainer.get(ICommandManager); + if (!(await interpreterService.getActiveInterpreter(wkspace))) { + cmdManager.executeCommand(constants.Commands.TriggerEnvironmentSelection, wkspace); + return; + } + const configurationService = serviceContainer.get(ITestConfigurationService); + await configurationService.promptToEnableAndConfigureTestFramework(wkspace); + }; + + disposableRegistry.push( + // Command: python.configureTests - prompts user to configure test framework + commandManager.registerCommand( + constants.Commands.Tests_Configure, + (_, _cmdSource: constants.CommandSource = constants.CommandSource.commandPalette, resource?: Uri) => { + // Invoke configuration handler (errors are ignored as this can be called from multiple places) + configureTestsHandler(resource).ignoreErrors(); + traceVerbose('Testing: Trigger refresh after config change'); + // Refresh test data if test controller is available (resolved lazily) + if (tests && !!tests.createTestController) { + const testController = serviceContainer.get(ITestController); + testController?.refreshTestData(resource, { forceRefresh: true }); + } + }, + ), + // Command: python.tests.copilotSetup - Copilot integration for test setup + commandManager.registerCommand(constants.Commands.Tests_CopilotSetup, (resource?: Uri): + | { message: string; command: Command } + | undefined => { + // Resolve services lazily when the command is invoked + const workspaceService = serviceContainer.get(IWorkspaceService); + const wkspaceFolder = + workspaceService.getWorkspaceFolder(resource) || workspaceService.workspaceFolders?.at(0); + if (!wkspaceFolder) { + return undefined; } - return; - } - if (this.testResultDisplay) { - this.testResultDisplay.enabled = true; - } - this.autoDiscoverTests(workspaceUri).catch(ex => this.serviceContainer.get(ILogger).logError('Failed to auto discover tests upon activation', ex)); - } - - public async discoverTestsForDocument(doc: TextDocument): Promise { - const testManager = await this.getTestManager(false, doc.uri); - if (!testManager) { - return; - } - const tests = await testManager.discoverTests(CommandSource.auto, false, true); - if (!tests || !Array.isArray(tests.testFiles) || tests.testFiles.length === 0) { - return; - } - if (tests.testFiles.findIndex((f: TestFile) => f.fullPath === doc.uri.fsPath) === -1) { - return; - } - - if (this.autoDiscoverTimer) { - clearTimeout(this.autoDiscoverTimer as any); - } - this.autoDiscoverTimer = setTimeout(() => this.discoverTests(CommandSource.auto, doc.uri, true, false, true), 1000); - } - public async autoDiscoverTests(resource: Resource) { - if (!this.workspaceService.hasWorkspaceFolders) { - return; - } - // Default to discovering tests in first folder if none specified. - if (!resource) { - resource = this.workspaceService.workspaceFolders![0].uri; - } - const configurationService = this.serviceContainer.get(IConfigurationService); - const settings = configurationService.getSettings(resource); - if (!settings.testing.nosetestsEnabled && !settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { - return; - } - this.discoverTests(CommandSource.auto, resource, true).ignoreErrors(); - } - public async discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean, clearTestStatus?: boolean) { - const testManager = await this.getTestManager(true, resource); - if (!testManager) { - return; - } + const configurationService = serviceContainer.get(ITestConfigurationService); + if (configurationService.hasConfiguredTests(wkspaceFolder.uri)) { + return undefined; + } - if (testManager.status === TestStatus.Discovering || testManager.status === TestStatus.Running) { - return; - } + return { + message: Testing.copilotSetupMessage, + command: { + title: Testing.configureTests, + command: constants.Commands.Tests_Configure, + arguments: [undefined, constants.CommandSource.ui, resource], + }, + }; + }), + // Command: python.copyTestId - copies test ID to clipboard + commandManager.registerCommand(constants.Commands.CopyTestId, async (testItem: TestItem) => { + writeTestIdToClipboard(testItem); + }), + ); +} - if (!this.testResultDisplay) { - this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); - } - const discoveryPromise = testManager.discoverTests(cmdSource, ignoreCache, quietMode, userInitiated, clearTestStatus); - this.testResultDisplay.displayDiscoverStatus(discoveryPromise, quietMode).catch(ex => console.error('Python Extension: displayDiscoverStatus', ex)); - await discoveryPromise; - } - public async stopTests(resource: Uri) { - sendTelemetryEvent(EventName.UNITTEST_STOP); - const testManager = await this.getTestManager(true, resource); - if (testManager) { - testManager.stop(); - } - } - public async displayStopUI(message: string): Promise { - const testManager = await this.getTestManager(true); - if (!testManager) { - return; - } +@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; - const testDisplay = this.serviceContainer.get(ITestDisplay); - testDisplay.displayStopTestUI(testManager.workspaceFolder, message); - } - public async displayUI(cmdSource: CommandSource) { - const testManager = await this.getTestManager(true); - if (!testManager) { - return; - } + // This is temporarily needed until the proposed API settles for this part + private testStateMap: Map = new Map(); - const testDisplay = this.serviceContainer.get(ITestDisplay); - testDisplay.displayTestUI(cmdSource, testManager.workspaceFolder); - } - public async displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean) { - const testManager = await this.getTestManager(true, file); - if (!testManager) { - return; - } + constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.disposableRegistry = serviceContainer.get(IDisposableRegistry); + this.workspaceService = serviceContainer.get(IWorkspaceService); + this.context = this.serviceContainer.get(IContextKeyManager); - const testDisplay = this.serviceContainer.get(ITestDisplay); - testDisplay.displayFunctionTestPickerUI(cmdSource, testManager.workspaceFolder, testManager.workingDirectory, file, testFunctions, debug); - } - public viewOutput(_cmdSource: CommandSource) { - sendTelemetryEvent(EventName.UNITTEST_VIEW_OUTPUT); - this.outputChannel.show(); - } - public async selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean) { - const testManager = await this.getTestManager(true, resource); - if (!testManager) { - return; - } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; + if (tests && !!tests.createTestController) { + this.testController = serviceContainer.get(ITestController); } - const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testDisplay = this.serviceContainer.get(ITestDisplay); - const selectedTestFn = await testDisplay.selectTestFunction(testManager.workspaceFolder.fsPath, tests); - if (!selectedTestFn) { - return; - } - // tslint:disable-next-line:prefer-type-cast no-object-literal-type-assertion - await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFunction: [selectedTestFn.testFunction] } as TestsToRun, false, debug); + const configChangeTrigger = new DelayedTrigger( + this.configurationChangeHandler.bind(this), + 500, + 'Test Configuration Change', + ); + this.configChangeTrigger = configChangeTrigger; + this.disposableRegistry.push(configChangeTrigger); } - public async selectAndRunTestFile(cmdSource: CommandSource) { - const testManager = await this.getTestManager(true); - if (!testManager) { - return; - } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } - const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testDisplay = this.serviceContainer.get(ITestDisplay); - const selectedFile = await testDisplay.selectTestFile(testManager.workspaceFolder.fsPath, tests); - if (!selectedFile) { - return; - } - await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [selectedFile] }); - } - public async runCurrentTestFile(cmdSource: CommandSource) { - if (!this.documentManager.activeTextEditor) { - return; - } - const testManager = await this.getTestManager(true, this.documentManager.activeTextEditor.document.uri); - if (!testManager) { - return; - } - try { - await testManager.discoverTests(cmdSource, true, true, true); - } catch (ex) { - return; - } - const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); - const tests = testCollectionStorage.getTests(testManager.workspaceFolder)!; - const testFiles = tests.testFiles.filter(testFile => { - return testFile.fullPath === this.documentManager.activeTextEditor!.document.uri.fsPath; - }); - if (testFiles.length < 1) { + public async activate(): Promise { + if (this.activatedOnce) { return; } - await this.runTestsImpl(cmdSource, testManager.workspaceFolder, { testFile: [testFiles[0]] }); - } + this.activatedOnce = true; - public async runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug: boolean = false) { - const testManager = await this.getTestManager(true, resource); - if (!testManager) { - return; - } + this.registerHandlers(); - if (!this.testResultDisplay) { - this.testResultDisplay = this.serviceContainer.get(ITestResultDisplay); + 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); + await commandManager.executeCommand('workbench.view.testing.focus'); + traceWarn( + 'Testing: Run attempted but no test configurations found for any workspace, use command palette to configure tests for python if desired.', + ); + } + }); } - - const promise = testManager.runTest(cmdSource, testsToRun, runFailedTests, debug).catch(reason => { - if (reason !== CANCELLATION_REASON) { - this.outputChannel.appendLine(`Error: ${reason}`); - } - return Promise.reject(reason); - }); - - this.testResultDisplay.displayProgressStatus(promise, debug); - await promise; } - public async registerSymbolProvider(symbolProvider: DocumentSymbolProvider): Promise { - const testCollectionStorage = this.serviceContainer.get(ITestCollectionStorageService); - const event = new EventEmitter(); - this.disposableRegistry.push(event); - const handler = this._onDidStatusChange.event(e => { - if (e.status !== TestStatus.Discovering && e.status !== TestStatus.Running) { - event.fire(); - } - }); - this.disposableRegistry.push(handler); - this.disposableRegistry.push(activateCodeLenses(event, symbolProvider, testCollectionStorage)); - } + private async updateTestUIButtons() { + // See if we already have stored tests results from previous runs. + // The tests results currently has a historical test status based on runs. To get a + // full picture of the tests state these need to be reduced by test id. + updateTestResultMap(this.testStateMap, tests.testResults); - @captureTelemetry(EventName.UNITTEST_CONFIGURE, undefined, false) - public async configureTests(resource?: Uri) { - let wkspace: Uri | undefined; - if (resource) { - const wkspaceFolder = this.workspaceService.getWorkspaceFolder(resource); - wkspace = wkspaceFolder ? wkspaceFolder.uri : undefined; - } else { - const appShell = this.serviceContainer.get(IApplicationShell); - wkspace = await selectTestWorkspace(appShell); - } - if (!wkspace) { - return; - } - const configurationService = this.serviceContainer.get(ITestConfigurationService); - await configurationService.promptToEnableAndConfigureTestFramework(wkspace!); + const hasFailedTests = checkForFailedTests(this.testStateMap); + await this.context.setContext(ExtensionContextKey.HasFailedTests, hasFailedTests); } - public registerCommands(): void { - const disposablesRegistry = this.serviceContainer.get(IDisposableRegistry); - const commandManager = this.serviceContainer.get(ICommandManager); - const disposables = [ - commandManager.registerCommand( - constants.Commands.Tests_Discover, - (treeNode?: TestWorkspaceFolder, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { - if (treeNode && treeNode instanceof TestWorkspaceFolder) { - resource = treeNode.resource; - cmdSource = CommandSource.testExplorer; - } - // Ignore the exceptions returned. - // This command will be invoked from other places of the extension. - return this.discoverTests(cmdSource, resource, true, true, false, true).ignoreErrors(); - } - ), - commandManager.registerCommand(constants.Commands.Tests_Configure, (_, _cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri) => { - // Ignore the exceptions returned. - // This command will be invoked from other places of the extension. - this.configureTests(resource).ignoreErrors(); - }), - commandManager.registerCommand(constants.Commands.Tests_Run_Failed, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => - this.runTestsImpl(cmdSource, resource, undefined, true) - ), - commandManager.registerCommand( - constants.Commands.Tests_Run, - (treeNode?: TestWorkspaceFolder, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri, testToRun?: TestsToRun) => { - if (treeNode && treeNode instanceof TestWorkspaceFolder) { - resource = treeNode.resource; - cmdSource = CommandSource.testExplorer; - } - return this.runTestsImpl(cmdSource, resource, testToRun); - } - ), - commandManager.registerCommand( - constants.Commands.Tests_Debug, - (treeNode?: TestWorkspaceFolder, cmdSource: CommandSource = CommandSource.commandPalette, resource?: Uri, testToRun?: TestsToRun) => { - if (treeNode && treeNode instanceof TestWorkspaceFolder) { - resource = treeNode.resource; - cmdSource = CommandSource.testExplorer; - } - return this.runTestsImpl(cmdSource, resource, testToRun, false, true); - } - ), - commandManager.registerCommand(constants.Commands.Tests_View_UI, () => this.displayUI(CommandSource.commandPalette)), - commandManager.registerCommand( - constants.Commands.Tests_Picker_UI, - (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions) - ), - commandManager.registerCommand( - constants.Commands.Tests_Picker_UI_Debug, - (_, cmdSource: CommandSource = CommandSource.commandPalette, file: Uri, testFunctions: TestFunction[]) => this.displayPickerUI(cmdSource, file, testFunctions, true) - ), - commandManager.registerCommand(constants.Commands.Tests_Stop, (_, resource: Uri) => this.stopTests(resource)), - commandManager.registerCommand(constants.Commands.Tests_ViewOutput, (_, cmdSource: CommandSource = CommandSource.commandPalette) => this.viewOutput(cmdSource)), - commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Discovery, () => this.displayStopUI('Stop discovering tests')), - commandManager.registerCommand(constants.Commands.Tests_Ask_To_Stop_Test, () => this.displayStopUI('Stop running tests')), - commandManager.registerCommand(constants.Commands.Tests_Select_And_Run_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => - this.selectAndRunTestMethod(cmdSource, resource) - ), - commandManager.registerCommand(constants.Commands.Tests_Select_And_Debug_Method, (_, cmdSource: CommandSource = CommandSource.commandPalette, resource: Uri) => - this.selectAndRunTestMethod(cmdSource, resource, true) - ), - commandManager.registerCommand(constants.Commands.Tests_Select_And_Run_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => - this.selectAndRunTestFile(cmdSource) - ), - commandManager.registerCommand(constants.Commands.Tests_Run_Current_File, (_, cmdSource: CommandSource = CommandSource.commandPalette) => - this.runCurrentTestFile(cmdSource) - ), - commandManager.registerCommand(constants.Commands.Tests_Discovering, noop) - ]; + private async configurationChangeHandler(eventArgs: ConfigurationChangeEvent) { + const workspaces = this.workspaceService.workspaceFolders ?? []; + const changedWorkspaces: Uri[] = workspaces + .filter((w) => eventArgs.affectsConfiguration('python.testing', w.uri)) + .map((w) => w.uri); - disposablesRegistry.push(...disposables); - } - public onDocumentSaved(doc: TextDocument) { - const settings = this.serviceContainer.get(IConfigurationService).getSettings(doc.uri); - if (!settings.testing.autoTestDiscoverOnSaveEnabled) { - return; - } - this.discoverTestsForDocument(doc).ignoreErrors(); + await Promise.all(changedWorkspaces.map((u) => this.testController?.refreshTestData(u))); } - public registerHandlers() { - const documentManager = this.serviceContainer.get(IDocumentManager); - this.disposableRegistry.push(documentManager.onDidSaveTextDocument(this.onDocumentSaved.bind(this))); + private registerHandlers() { + const interpreterService = this.serviceContainer.get(IInterpreterService); this.disposableRegistry.push( - this.workspaceService.onDidChangeConfiguration(e => { - if (this.configChangedTimer) { - clearTimeout(this.configChangedTimer as any); - } - this.configChangedTimer = setTimeout(() => this.configurationChangeHandler(e), 1000); - }) + this.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/navigation/commandHandler.ts b/src/client/testing/navigation/commandHandler.ts deleted file mode 100644 index 0c036ccee652..000000000000 --- a/src/client/testing/navigation/commandHandler.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable, named } from 'inversify'; -import { ICommandManager } from '../../common/application/types'; -import { Commands } from '../../common/constants'; -import { IDisposable, IDisposableRegistry } from '../../common/types'; -import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler, NavigableItemType } from './types'; - -@injectable() -export class TestCodeNavigatorCommandHandler implements ITestCodeNavigatorCommandHandler { - private disposables: IDisposable[] = []; - constructor( - @inject(ICommandManager) private readonly commandManager: ICommandManager, - @inject(ITestCodeNavigator) @named(NavigableItemType.testFile) private readonly testFileNavigator: ITestCodeNavigator, - @inject(ITestCodeNavigator) @named(NavigableItemType.testFunction) private readonly testFunctionNavigator: ITestCodeNavigator, - @inject(ITestCodeNavigator) @named(NavigableItemType.testSuite) private readonly testSuiteNavigator: ITestCodeNavigator, - @inject(IDisposableRegistry) disposableRegistry: IDisposableRegistry - ) { - disposableRegistry.push(this); - } - public dispose() { - this.disposables.forEach(item => item.dispose()); - } - public register(): void { - if (this.disposables.length > 0) { - return; - } - let disposable = this.commandManager.registerCommand(Commands.navigateToTestFile, this.testFileNavigator.navigateTo, this.testFileNavigator); - this.disposables.push(disposable); - disposable = this.commandManager.registerCommand(Commands.navigateToTestFunction, this.testFunctionNavigator.navigateTo, this.testFunctionNavigator); - this.disposables.push(disposable); - disposable = this.commandManager.registerCommand(Commands.navigateToTestSuite, this.testSuiteNavigator.navigateTo, this.testSuiteNavigator); - this.disposables.push(disposable); - } -} diff --git a/src/client/testing/navigation/fileNavigator.ts b/src/client/testing/navigation/fileNavigator.ts deleted file mode 100644 index 186f1b1b98b0..000000000000 --- a/src/client/testing/navigation/fileNavigator.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { captureTelemetry } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { TestFile } from '../common/types'; -import { ITestCodeNavigator, ITestNavigatorHelper } from './types'; - -@injectable() -export class TestFileCodeNavigator implements ITestCodeNavigator { - constructor(@inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper) { } - @swallowExceptions('Navigate to test file') - @captureTelemetry(EventName.UNITTEST_NAVIGATE_TEST_FILE, undefined, true) - public async navigateTo(_: Uri, item: TestFile, __: boolean): Promise { - await this.helper.openFile(Uri.file(item.fullPath)); - } -} diff --git a/src/client/testing/navigation/functionNavigator.ts b/src/client/testing/navigation/functionNavigator.ts deleted file mode 100644 index b7ee29cfe533..000000000000 --- a/src/client/testing/navigation/functionNavigator.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextEditorRevealType, Uri } from 'vscode'; -import { IDocumentManager } from '../../common/application/types'; -import { traceError } from '../../common/logger'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { ITestCollectionStorageService, TestFunction } from '../common/types'; -import { ITestCodeNavigator, ITestNavigatorHelper } from './types'; - -@injectable() -export class TestFunctionCodeNavigator implements ITestCodeNavigator { - private cancellationToken?: CancellationTokenSource; - constructor( - @inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper, - @inject(IDocumentManager) private readonly docManager: IDocumentManager, - @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService - ) { } - @swallowExceptions('Navigate to test function') - @captureTelemetry(EventName.UNITTEST_NAVIGATE_TEST_FUNCTION, undefined, true) - public async navigateTo(resource: Uri, fn: TestFunction, focus: boolean = true): Promise { - sendTelemetryEvent(EventName.UNITTEST_NAVIGATE_TEST_FUNCTION, undefined, { focus_code: focus }); - if (this.cancellationToken) { - this.cancellationToken.cancel(); - } - const item = this.storage.findFlattendTestFunction(resource, fn); - if (!item) { - throw new Error('Flattend test function not found'); - } - this.cancellationToken = new CancellationTokenSource(); - const [doc, editor] = await this.helper.openFile(Uri.file(item.parentTestFile.fullPath)); - let range: Range | undefined; - if (item.testFunction.line) { - range = new Range(item.testFunction.line, 0, item.testFunction.line, 0); - } else { - const predicate = (s: SymbolInformation) => s.name === item.testFunction.name && (s.kind === SymbolKind.Method || s.kind === SymbolKind.Function); - const symbol = await this.helper.findSymbol(doc, predicate, this.cancellationToken.token); - range = symbol ? symbol.location.range : undefined; - } - if (!range) { - traceError('Unable to navigate to test function', new Error('Test Function not found')); - return; - } - if (focus) { - range = new Range(range.start.line, range.start.character, range.start.line, range.start.character); - await this.docManager.showTextDocument(doc, { preserveFocus: false, selection: range }); - } else { - editor.revealRange(range, TextEditorRevealType.Default); - } - } -} diff --git a/src/client/testing/navigation/helper.ts b/src/client/testing/navigation/helper.ts deleted file mode 100644 index 5d6022396bda..000000000000 --- a/src/client/testing/navigation/helper.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 { CancellationToken, SymbolInformation, TextDocument, TextEditor, Uri } from 'vscode'; -import { IDocumentManager } from '../../common/application/types'; -import { traceError } from '../../common/logger'; -import { IDocumentSymbolProvider } from '../../common/types'; -import { ITestNavigatorHelper, SymbolSearch } from './types'; - -@injectable() -export class TestNavigatorHelper implements ITestNavigatorHelper { - constructor( - @inject(IDocumentManager) private readonly documentManager: IDocumentManager, - @inject(IDocumentSymbolProvider) @named('test') private readonly symbolProvider: IDocumentSymbolProvider - ) { } - public async openFile(file?: Uri): Promise<[TextDocument, TextEditor]> { - if (!file) { - throw new Error('Unable to navigate to an undefined test file'); - } - const doc = await this.documentManager.openTextDocument(file); - const editor = await this.documentManager.showTextDocument(doc); - return [doc, editor]; - } - public async findSymbol(doc: TextDocument, search: SymbolSearch, token: CancellationToken): Promise { - const symbols = (await this.symbolProvider.provideDocumentSymbols(doc, token)) as SymbolInformation[]; - if (!Array.isArray(symbols) || symbols.length === 0) { - traceError('Symbol information not found', new Error('Symbol information not found')); - return; - } - return symbols.find(search); - } -} diff --git a/src/client/testing/navigation/serviceRegistry.ts b/src/client/testing/navigation/serviceRegistry.ts deleted file mode 100644 index 116a3296193d..000000000000 --- a/src/client/testing/navigation/serviceRegistry.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IDocumentSymbolProvider } from '../../common/types'; -import { IServiceManager } from '../../ioc/types'; -import { TestCodeNavigatorCommandHandler } from './commandHandler'; -import { TestFileCodeNavigator } from './fileNavigator'; -import { TestFunctionCodeNavigator } from './functionNavigator'; -import { TestNavigatorHelper } from './helper'; -import { TestSuiteCodeNavigator } from './suiteNavigator'; -import { TestFileSymbolProvider } from './symbolProvider'; -import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler, ITestNavigatorHelper, NavigableItemType } from './types'; - -export function registerTypes(serviceManager: IServiceManager) { - serviceManager.addSingleton(ITestNavigatorHelper, TestNavigatorHelper); - serviceManager.addSingleton(ITestCodeNavigatorCommandHandler, TestCodeNavigatorCommandHandler); - serviceManager.addSingleton(ITestCodeNavigator, TestFileCodeNavigator, NavigableItemType.testFile); - serviceManager.addSingleton(ITestCodeNavigator, TestFunctionCodeNavigator, NavigableItemType.testFunction); - serviceManager.addSingleton(ITestCodeNavigator, TestSuiteCodeNavigator, NavigableItemType.testSuite); - serviceManager.addSingleton(IDocumentSymbolProvider, TestFileSymbolProvider, 'test'); -} diff --git a/src/client/testing/navigation/suiteNavigator.ts b/src/client/testing/navigation/suiteNavigator.ts deleted file mode 100644 index 5a92ac096e74..000000000000 --- a/src/client/testing/navigation/suiteNavigator.ts +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import { CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextEditorRevealType, Uri } from 'vscode'; -import { IDocumentManager } from '../../common/application/types'; -import { traceError } from '../../common/logger'; -import { swallowExceptions } from '../../common/utils/decorators'; -import { captureTelemetry, sendTelemetryEvent } from '../../telemetry'; -import { EventName } from '../../telemetry/constants'; -import { ITestCollectionStorageService, TestSuite } from '../common/types'; -import { ITestCodeNavigator, ITestNavigatorHelper } from './types'; - -@injectable() -export class TestSuiteCodeNavigator implements ITestCodeNavigator { - private cancellationToken?: CancellationTokenSource; - constructor( - @inject(ITestNavigatorHelper) private readonly helper: ITestNavigatorHelper, - @inject(IDocumentManager) private readonly docManager: IDocumentManager, - @inject(ITestCollectionStorageService) private readonly storage: ITestCollectionStorageService - ) { } - @swallowExceptions('Navigate to test suite') - @captureTelemetry(EventName.UNITTEST_NAVIGATE_TEST_SUITE, undefined, true) - public async navigateTo(resource: Uri, suite: TestSuite, focus: boolean = true): Promise { - sendTelemetryEvent(EventName.UNITTEST_NAVIGATE_TEST_SUITE, undefined, { focus_code: focus }); - if (this.cancellationToken) { - this.cancellationToken.cancel(); - } - const item = this.storage.findFlattendTestSuite(resource, suite); - if (!item) { - throw new Error('Flattened test suite not found'); - } - this.cancellationToken = new CancellationTokenSource(); - const [doc, editor] = await this.helper.openFile(Uri.file(item.parentTestFile.fullPath)); - let range: Range | undefined; - if (item.testSuite.line) { - range = new Range(item.testSuite.line, 0, item.testSuite.line, 0); - } else { - const predicate = (s: SymbolInformation) => s.name === item.testSuite.name && s.kind === SymbolKind.Class; - const symbol = await this.helper.findSymbol(doc, predicate, this.cancellationToken.token); - range = symbol ? symbol.location.range : undefined; - } - if (!range) { - traceError('Unable to navigate to test suite', new Error('Test Suite not found')); - return; - } - if (focus) { - range = new Range(range.start.line, range.start.character, range.start.line, range.start.character); - await this.docManager.showTextDocument(doc, { preserveFocus: false, selection: range }); - } else { - editor.revealRange(range, TextEditorRevealType.Default); - } - } -} diff --git a/src/client/testing/navigation/symbolProvider.ts b/src/client/testing/navigation/symbolProvider.ts deleted file mode 100644 index f854cc75548c..000000000000 --- a/src/client/testing/navigation/symbolProvider.ts +++ /dev/null @@ -1,67 +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, DocumentSymbolProvider, Location, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode'; -import { traceError } from '../../common/logger'; -import { IProcessServiceFactory } from '../../common/process/types'; -import { IConfigurationService } from '../../common/types'; -import { EXTENSION_ROOT_DIR } from '../../constants'; - -type RawSymbol = { namespace: string; name: string; range: Range }; -type Symbols = { - classes: RawSymbol[]; - methods: RawSymbol[]; - functions: RawSymbol[]; -}; - -@injectable() -export class TestFileSymbolProvider implements DocumentSymbolProvider { - constructor( - @inject(IConfigurationService) private readonly configurationService: IConfigurationService, - @inject(IProcessServiceFactory) private readonly processServiceFactory: IProcessServiceFactory - ) {} - public async provideDocumentSymbols(document: TextDocument, token: CancellationToken): Promise { - const rawSymbols = await this.getSymbols(document, token); - if (!rawSymbols) { - return []; - } - return [ - ...rawSymbols.classes.map(item => this.parseRawSymbol(document.uri, item, SymbolKind.Class)), - ...rawSymbols.methods.map(item => this.parseRawSymbol(document.uri, item, SymbolKind.Method)), - ...rawSymbols.functions.map(item => this.parseRawSymbol(document.uri, item, SymbolKind.Function)) - ]; - } - private parseRawSymbol(uri: Uri, symbol: RawSymbol, kind: SymbolKind): SymbolInformation { - const range = new Range(symbol.range.start.line, symbol.range.start.character, symbol.range.end.line, symbol.range.end.character); - return { - containerName: symbol.namespace, - kind, - name: symbol.name, - location: new Location(uri, range) - }; - } - private async getSymbols(document: TextDocument, token: CancellationToken): Promise { - try { - if (document.isUntitled) { - return; - } - const scriptArgs: string[] = [document.uri.fsPath]; - if (document.isDirty) { - scriptArgs.push(document.getText()); - } - const pythonPath = this.configurationService.getSettings(document.uri).pythonPath; - const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), ...scriptArgs]; - const processService = await this.processServiceFactory.create(document.uri); - const proc = await processService.exec(pythonPath, args, { throwOnStdErr: true, token }); - - return JSON.parse(proc.stdout); - } catch (ex) { - traceError('Python: Failed to get symbols', ex); - return; - } - } -} diff --git a/src/client/testing/navigation/types.ts b/src/client/testing/navigation/types.ts deleted file mode 100644 index 5d57b0c068f1..000000000000 --- a/src/client/testing/navigation/types.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { CancellationToken, SymbolInformation, TextDocument, TextEditor, Uri } from 'vscode'; -import { IDisposable } from '../../common/types'; -import { TestFile, TestFunction, TestSuite } from '../common/types'; - -export const ITestCodeNavigatorCommandHandler = Symbol('ITestCodeNavigatorCommandHandler'); -export interface ITestCodeNavigatorCommandHandler extends IDisposable { - register(): void; -} -export type NavigableItem = TestFile | TestFunction | TestSuite; -export enum NavigableItemType { - testFile = 'testFile', - testFunction = 'testFunction', - testSuite = 'testSuite' -} - -export const ITestCodeNavigator = Symbol('ITestCodeNavigator'); -export interface ITestCodeNavigator { - navigateTo(resource: Uri, item: NavigableItem, focus: boolean): Promise; -} - -export const ITestNavigatorHelper = Symbol('ITestNavigatorHelper'); -export interface ITestNavigatorHelper { - openFile(file?: Uri): Promise<[TextDocument, TextEditor]>; - findSymbol(doc: TextDocument, predicate: SymbolSearch, token: CancellationToken): Promise; -} -export type SymbolSearch = (item: SymbolInformation) => boolean; - -export const ITestExplorerCommandHandler = Symbol('ITestExplorerCommandHandler'); -export interface ITestExplorerCommandHandler extends IDisposable { - register(): void; -} diff --git a/src/client/testing/nosetest/main.ts b/src/client/testing/nosetest/main.ts deleted file mode 100644 index e7978f7ae720..000000000000 --- a/src/client/testing/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.testing.nosetestsEnabled; - } - constructor(workspaceFolder: Uri, rootDirectory: string, - @inject(IServiceContainer) serviceContainer: IServiceContainer) { - super(NOSETEST_PROVIDER, Product.nosetest, workspaceFolder, rootDirectory, serviceContainer); - this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); - this.helper = this.serviceContainer.get(ITestsHelper); - this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); - } - public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { - const args = this.settings.testing.nosetestArgs.slice(0); - return { - workspaceFolder: this.workspaceFolder, - cwd: this.rootDirectory, args, - token: this.testDiscoveryCancellationToken!, ignoreCache, - outChannel: this.outputChannel - }; - } - public runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args: string[]; - - const runAllTests = this.helper.shouldRunAllTests(testsToRun); - if (debug) { - args = this.argsService.filterArguments(this.settings.testing.nosetestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); - } else { - args = this.argsService.filterArguments(this.settings.testing.nosetestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); - } - - if (runFailedTests === true && args.indexOf('--failed') === -1) { - args.splice(0, 0, '--failed'); - } - if (!runFailedTests && args.indexOf('--with-id') === -1) { - args.splice(0, 0, '--with-id'); - } - const options: TestRunOptions = { - workspaceFolder: Uri.file(this.rootDirectory), - cwd: this.rootDirectory, - tests, args, testsToRun, - token: this.testRunnerCancellationToken!, - outChannel: this.outputChannel, - debug - }; - return this.runner.runTest(this.testResultsService, options, this); - } -} diff --git a/src/client/testing/nosetest/runner.ts b/src/client/testing/nosetest/runner.ts deleted file mode 100644 index 692e038aca8b..000000000000 --- a/src/client/testing/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, NOSETEST_PROVIDER); - this.argsHelper = serviceContainer.get(IArgumentsHelper); - this.testRunner = serviceContainer.get(ITestRunner); - this.xUnitParser = this.serviceContainer.get(IXUnitParser); - this.fs = this.serviceContainer.get(IFileSystem); - } - public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, _: ITestManager): Promise { - let testPaths: string[] = []; - if (options.testsToRun && options.testsToRun.testFolder) { - testPaths = testPaths.concat(options.testsToRun.testFolder.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFile) { - testPaths = testPaths.concat(options.testsToRun.testFile.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testSuite) { - testPaths = testPaths.concat(options.testsToRun.testSuite.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFunction) { - testPaths = testPaths.concat(options.testsToRun.testFunction.map(f => f.nameToRun)); - } - - let deleteJUnitXmlFile: Function = noop; - const args = options.args; - // Check if '--with-xunit' is in args list - if (args.indexOf(WITH_XUNIT) === -1) { - args.splice(0, 0, WITH_XUNIT); - } - - try { - const xmlLogResult = await this.getUnitXmlFile(args); - const xmlLogFile = xmlLogResult.filePath; - deleteJUnitXmlFile = xmlLogResult.dispose; - // Remove the '--unixml' if it exists, and add it with our path. - const testArgs = this.argsService.filterArguments(args, [XUNIT_FILE]); - testArgs.splice(0, 0, `${XUNIT_FILE}=${xmlLogFile}`); - - // Positional arguments control the tests to be run. - testArgs.push(...testPaths); - - if (options.debug === true) { - const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); - const debuggerArgs = [options.cwd, 'nose'].concat(testArgs); - const launchOptions: LaunchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel, testProvider: NOSETEST_PROVIDER }; - await debugLauncher.launchDebugger(launchOptions); - } else { - const runOptions: Options = { - args: testArgs.concat(testPaths), - cwd: options.cwd, - outChannel: options.outChannel, - token: options.token, - workspaceFolder: options.workspaceFolder - }; - await this.testRunner.run(NOSETEST_PROVIDER, runOptions); - } - - return options.debug ? options.tests : await this.updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); - } catch (ex) { - return Promise.reject(ex); - } finally { - deleteJUnitXmlFile(); - } - } - - private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { - await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.nosetests); - testResultsService.updateResults(tests); - return tests; - } - private async getUnitXmlFile(args: string[]): Promise { - const xmlFile = this.argsHelper.getOptionValues(args, XUNIT_FILE); - if (typeof xmlFile === 'string') { - return { filePath: xmlFile, dispose: noop }; - } - - return this.fs.createTemporaryFile('.xml'); - } -} diff --git a/src/client/testing/nosetest/services/argsService.ts b/src/client/testing/nosetest/services/argsService.ts deleted file mode 100644 index 8fbe92f41006..000000000000 --- a/src/client/testing/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); - } - public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { - return { - withArgs: OptionsWithArguments, - withoutArgs: OptionsWithoutArguments - }; - } - public getOptionValue(args: string[], option: string): string | string[] | undefined { - return this.helper.getOptionValues(args, option); - } - // tslint:disable-next-line:max-func-body-length - public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { - const optionsWithoutArgsToRemove: string[] = []; - const optionsWithArgsToRemove: string[] = []; - // Positional arguments in nosetest are test directories and files. - // So if we want to run a specific test, then remove positional args. - let removePositionalArgs = false; - if (Array.isArray(argumentToRemoveOrFilter)) { - argumentToRemoveOrFilter.forEach(item => { - if (OptionsWithArguments.indexOf(item) >= 0) { - optionsWithArgsToRemove.push(item); - } - if (OptionsWithoutArguments.indexOf(item) >= 0) { - optionsWithoutArgsToRemove.push(item); - } - }); - } else { - switch (argumentToRemoveOrFilter) { - case TestFilter.removeTests: { - removePositionalArgs = true; - break; - } - case TestFilter.discovery: { - optionsWithoutArgsToRemove.push(...[ - '-v', '--verbose', '-q', '--quiet', - '-x', '--stop', - '--with-coverage', - ...OptionsWithoutArguments.filter(item => item.startsWith('--cover')), - ...OptionsWithoutArguments.filter(item => item.startsWith('--logging')), - ...OptionsWithoutArguments.filter(item => item.startsWith('--pdb')), - ...OptionsWithoutArguments.filter(item => item.indexOf('xunit') >= 0) - ]); - optionsWithArgsToRemove.push(...[ - '--verbosity', '-l', '--debug', '--cover-package', - ...OptionsWithoutArguments.filter(item => item.startsWith('--cover')), - ...OptionsWithArguments.filter(item => item.startsWith('--logging')), - ...OptionsWithoutArguments.filter(item => item.indexOf('xunit') >= 0) - ]); - break; - } - case TestFilter.debugAll: - case TestFilter.runAll: { - break; - } - case TestFilter.debugSpecific: - case TestFilter.runSpecific: { - removePositionalArgs = true; - break; - } - default: { - throw new Error(`Unsupported Filter '${argumentToRemoveOrFilter}'`); - } - } - } - - let filteredArgs = args.slice(); - if (removePositionalArgs) { - const positionalArgs = this.helper.getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); - filteredArgs = filteredArgs.filter(item => positionalArgs.indexOf(item) === -1); - } - return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); - } - public getTestFolders(args: string[]): string[] { - return this.helper.getPositionalArguments(args, OptionsWithArguments, OptionsWithoutArguments); - } -} diff --git a/src/client/testing/nosetest/services/discoveryService.ts b/src/client/testing/nosetest/services/discoveryService.ts deleted file mode 100644 index 157b24d11257..000000000000 --- a/src/client/testing/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, NOSETEST_PROVIDER); - this.runner = this.serviceContainer.get(ITestRunner); - } - public async discoverTests(options: TestDiscoveryOptions): Promise { - // Remove unwanted arguments. - const args = this.argsService.filterArguments(options.args, TestFilter.discovery); - - const token = options.token ? options.token : new CancellationTokenSource().token; - const runOptions: Options = { - args: ['--collect-only', '-vvv'].concat(args), - cwd: options.cwd, - workspaceFolder: options.workspaceFolder, - token, - outChannel: options.outChannel - }; - - const data = await this.runner.run(NOSETEST_PROVIDER, runOptions); - if (options.token && options.token.isCancellationRequested) { - return Promise.reject('cancelled'); - } - - return this.testParser.parse(data, options); - } -} diff --git a/src/client/testing/nosetest/services/parserService.ts b/src/client/testing/nosetest/services/parserService.ts deleted file mode 100644 index d9c08b135463..000000000000 --- a/src/client/testing/nosetest/services/parserService.ts +++ /dev/null @@ -1,148 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { convertFileToPackage, extractBetweenDelimiters } from '../../common/testUtils'; -import { ITestsHelper, ITestsParser, ParserOptions, TestFile, TestFunction, Tests, TestSuite } from '../../common/types'; - -const NOSE_WANT_FILE_PREFIX = 'nose.selector: DEBUG: wantFile '; -const NOSE_WANT_FILE_SUFFIX = '.py? True'; -const NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT = '? True'; - -@injectable() -export class TestsParser implements ITestsParser { - constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) { } - public parse(content: string, options: ParserOptions): Tests { - let testFiles = this.getTestFiles(content, options); - // Exclude tests that don't have any functions or test suites. - testFiles = testFiles.filter(testFile => testFile.suites.length > 0 || testFile.functions.length > 0); - return this.testsHelper.flattenTestFiles(testFiles, options.cwd); - } - - private getTestFiles(content: string, options: ParserOptions) { - let logOutputLines: string[] = ['']; - const testFiles: TestFile[] = []; - content.split(/\r?\n/g).forEach((line, index, lines) => { - if ((line.startsWith(NOSE_WANT_FILE_PREFIX) && line.endsWith(NOSE_WANT_FILE_SUFFIX)) || - index === lines.length - 1) { - // process the previous lines. - this.parseNoseTestModuleCollectionResult(options.cwd, logOutputLines, testFiles); - logOutputLines = ['']; - } - - if (index === 0) { - if (content.startsWith(os.EOL) || lines.length > 1) { - this.appendLine(line, logOutputLines); - return; - } - logOutputLines[logOutputLines.length - 1] += line; - return; - } - if (index === lines.length - 1) { - logOutputLines[logOutputLines.length - 1] += line; - return; - } - this.appendLine(line, logOutputLines); - return; - }); - - return testFiles; - } - private appendLine(line: string, logOutputLines: string[]) { - const lastLineIndex = logOutputLines.length - 1; - logOutputLines[lastLineIndex] += line; - - // Check whether the previous line is something that we need. - // What we need is a line that ends with ? True, - // and starts with nose.selector: DEBUG: want. - if (logOutputLines[lastLineIndex].endsWith('? True')) { - logOutputLines.push(''); - } else { - // We don't need this line - logOutputLines[lastLineIndex] = ''; - } - } - - private parseNoseTestModuleCollectionResult(rootDirectory: string, lines: string[], testFiles: TestFile[]) { - let currentPackage: string = ''; - let fileName = ''; - let testFile: TestFile; - const resource = Uri.file(rootDirectory); - lines.forEach(line => { - if (line.startsWith(NOSE_WANT_FILE_PREFIX) && line.endsWith(NOSE_WANT_FILE_SUFFIX)) { - fileName = line.substring(NOSE_WANT_FILE_PREFIX.length); - fileName = fileName.substring(0, fileName.lastIndexOf(NOSE_WANT_FILE_SUFFIX_WITHOUT_EXT)); - - // We need to display the path relative to the current directory. - fileName = fileName.substring(rootDirectory.length + 1); - // we don't care about the compiled file. - if (path.extname(fileName) === '.pyc' || path.extname(fileName) === '.pyo') { - fileName = fileName.substring(0, fileName.length - 1); - } - currentPackage = convertFileToPackage(fileName); - const fullyQualifiedName = path.isAbsolute(fileName) ? fileName : path.resolve(rootDirectory, fileName); - testFile = { - resource, - functions: [], suites: [], name: fileName, nameToRun: fileName, - xmlName: currentPackage, time: 0, functionsFailed: 0, functionsPassed: 0, - fullPath: fullyQualifiedName - }; - testFiles.push(testFile); - return; - } - - if (line.startsWith('nose.selector: DEBUG: wantClass ? True'); - const clsName = path.extname(name).substring(1); - const testSuite: TestSuite = { - resource, - name: clsName, nameToRun: `${fileName}:${clsName}`, - functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, - isInstance: false, functionsFailed: 0, functionsPassed: 0 - }; - testFile.suites.push(testSuite); - return; - } - if (line.startsWith('nose.selector: DEBUG: wantClass ')) { - const name = extractBetweenDelimiters(line, 'nose.selector: DEBUG: wantClass ', '? True'); - const testSuite: TestSuite = { - resource, - name: path.extname(name).substring(1), nameToRun: `${fileName}:.${name}`, - functions: [], suites: [], xmlName: name, time: 0, isUnitTest: false, - isInstance: false, functionsFailed: 0, functionsPassed: 0 - }; - testFile.suites.push(testSuite); - return; - } - if (line.startsWith('nose.selector: DEBUG: wantMethod ? True'); - const fnName = path.extname(name).substring(1); - const clsName = path.basename(name, path.extname(name)); - const fn: TestFunction = { - resource, - name: fnName, nameToRun: `${fileName}:${clsName}.${fnName}`, - time: 0, functionsFailed: 0, functionsPassed: 0 - }; - - const cls = testFile.suites.find(suite => suite.name === clsName); - if (cls) { - cls.functions.push(fn); - } - return; - } - if (line.startsWith('nose.selector: DEBUG: wantFunction { - const fs = this.serviceContainer.get(IFileSystem); - for (const cfg of ['.noserc', 'nose.cfg']) { - if (await fs.fileExists(path.join(wkspace.fsPath, cfg))) { - return true; - } - } - return false; - } - public async configure(wkspace: Uri): Promise { - const args: string[] = []; - const configFileOptionLabel = 'Use existing config file'; - // If a config file exits, there's nothing to be configured. - if (await this.requiresUserToConfigure(wkspace)) { - return; - } - const subDirs = await this.getTestDirs(wkspace.fsPath); - const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } - const installed = await this.installer.isInstalled(Product.nosetest); - if (!installed) { - await this.installer.install(Product.nosetest); - } - await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.nosetest, args); - } -} diff --git a/src/client/testing/pytest/main.ts b/src/client/testing/pytest/main.ts deleted file mode 100644 index d9cb8e981c3a..000000000000 --- a/src/client/testing/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, IPythonTestMessage, ITestManagerRunner, TestFilter } from '../types'; - -export class TestManager extends BaseTestManager { - private readonly argsService: IArgumentsService; - private readonly helper: ITestsHelper; - private readonly runner: ITestManagerRunner; - private readonly testMessageService: ITestMessageService; - public get enabled() { - return this.settings.testing.pytestEnabled; - } - constructor(workspaceFolder: Uri, rootDirectory: string, - serviceContainer: IServiceContainer) { - super(PYTEST_PROVIDER, Product.pytest, workspaceFolder, rootDirectory, serviceContainer); - this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); - this.helper = this.serviceContainer.get(ITestsHelper); - this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); - this.testMessageService = this.serviceContainer.get(ITestMessageService, this.testProvider); - } - public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { - const args = this.settings.testing.pytestArgs.slice(0); - return { - workspaceFolder: this.workspaceFolder, - cwd: this.rootDirectory, args, - token: this.testDiscoveryCancellationToken!, ignoreCache, - outChannel: this.outputChannel - }; - } - public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - let args: string[]; - - const runAllTests = this.helper.shouldRunAllTests(testsToRun); - if (debug) { - args = this.argsService.filterArguments(this.settings.testing.pytestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); - } else { - args = this.argsService.filterArguments(this.settings.testing.pytestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); - } - - if (runFailedTests === true && args.indexOf('--lf') === -1 && args.indexOf('--last-failed') === -1) { - args.splice(0, 0, '--last-failed'); - } - const options: TestRunOptions = { - workspaceFolder: this.workspaceFolder, - cwd: this.rootDirectory, - tests, args, testsToRun, debug, - token: this.testRunnerCancellationToken!, - outChannel: this.outputChannel - }; - const testResults = await this.runner.runTest(this.testResultsService, options, this); - const messages: IPythonTestMessage[] = await this.testMessageService.getFilteredTestMessages(this.rootDirectory, testResults); - await this.updateDiagnostics(tests, messages); - return testResults; - } -} diff --git a/src/client/testing/pytest/runner.ts b/src/client/testing/pytest/runner.ts deleted file mode 100644 index f10784011273..000000000000 --- a/src/client/testing/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, PYTEST_PROVIDER); - this.argsHelper = serviceContainer.get(IArgumentsHelper); - this.testRunner = serviceContainer.get(ITestRunner); - this.xUnitParser = this.serviceContainer.get(IXUnitParser); - this.fs = this.serviceContainer.get(IFileSystem); - } - public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, _: ITestManager): Promise { - let testPaths: string[] = []; - if (options.testsToRun && options.testsToRun.testFolder) { - testPaths = testPaths.concat(options.testsToRun.testFolder.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFile) { - testPaths = testPaths.concat(options.testsToRun.testFile.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testSuite) { - testPaths = testPaths.concat(options.testsToRun.testSuite.map(f => f.nameToRun)); - } - if (options.testsToRun && options.testsToRun.testFunction) { - testPaths = testPaths.concat(options.testsToRun.testFunction.map(f => f.nameToRun)); - } - - let deleteJUnitXmlFile: Function = noop; - const args = options.args; - try { - const xmlLogResult = await this.getJUnitXmlFile(args); - const xmlLogFile = xmlLogResult.filePath; - deleteJUnitXmlFile = xmlLogResult.dispose; - // Remove the '--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); - const debuggerArgs = [options.cwd, 'pytest'].concat(testArgs); - const launchOptions: LaunchOptions = { cwd: options.cwd, args: debuggerArgs, token: options.token, outChannel: options.outChannel, testProvider: PYTEST_PROVIDER }; - await debugLauncher.launchDebugger(launchOptions); - } else { - const runOptions: Options = { - args: testArgs, - cwd: options.cwd, - outChannel: options.outChannel, - token: options.token, - workspaceFolder: options.workspaceFolder - }; - await this.testRunner.run(PYTEST_PROVIDER, runOptions); - } - - return options.debug ? options.tests : await this.updateResultsFromLogFiles(options.tests, xmlLogFile, testResultsService); - } catch (ex) { - return Promise.reject(ex); - } finally { - deleteJUnitXmlFile(); - } - } - - private async updateResultsFromLogFiles(tests: Tests, outputXmlFile: string, testResultsService: ITestResultsService): Promise { - await this.xUnitParser.updateResultsFromXmlLogFile(tests, outputXmlFile, PassCalculationFormulae.pytest); - testResultsService.updateResults(tests); - return tests; - } - - private async getJUnitXmlFile(args: string[]): Promise { - const xmlFile = this.argsHelper.getOptionValues(args, JunitXmlArg); - if (typeof xmlFile === 'string') { - return { filePath: xmlFile, dispose: noop }; - } - return this.fs.createTemporaryFile('.xml'); - } - -} diff --git a/src/client/testing/pytest/services/argsService.ts b/src/client/testing/pytest/services/argsService.ts deleted file mode 100644 index 17775ec50d1c..000000000000 --- a/src/client/testing/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); - } - 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/testing/pytest/services/discoveryService.ts b/src/client/testing/pytest/services/discoveryService.ts deleted file mode 100644 index 70f7a35b5289..000000000000 --- a/src/client/testing/pytest/services/discoveryService.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import { CancellationTokenSource } from 'vscode'; -import { IServiceContainer } from '../../../ioc/types'; -import { PYTEST_PROVIDER } from '../../common/constants'; -import { ITestDiscoveryService, ITestsHelper, TestDiscoveryOptions, Tests } from '../../common/types'; -import { IArgumentsService, TestFilter } from '../../types'; - -@injectable() -export class TestDiscoveryService implements ITestDiscoveryService { - private argsService: IArgumentsService; - private helper: ITestsHelper; - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.argsService = this.serviceContainer.get(IArgumentsService, PYTEST_PROVIDER); - this.helper = this.serviceContainer.get(ITestsHelper); - } - public async discoverTests(options: TestDiscoveryOptions): Promise { - const args = this.buildTestCollectionArgs(options); - - // Collect tests for each test directory separately and merge. - const testDirectories = this.argsService.getTestFolders(options.args); - if (testDirectories.length === 0) { - const opts = { - ...options, - args - }; - return this.discoverTestsInTestDirectory(opts); - } - const results = await Promise.all(testDirectories.map(testDir => { - // Add test directory as a positional argument. - const opts = { - ...options, - args: [...args, testDir] - }; - return this.discoverTestsInTestDirectory(opts); - })); - - return this.helper.mergeTests(results); - } - protected buildTestCollectionArgs(options: TestDiscoveryOptions) { - // Remove unwnted arguments (which happen to be test directories & test specific args). - const args = this.argsService.filterArguments(options.args, TestFilter.discovery); - if (options.ignoreCache && args.indexOf('--cache-clear') === -1) { - args.splice(0, 0, '--cache-clear'); - } - if (args.indexOf('-s') === -1) { - args.splice(0, 0, '-s'); - } - return args; - } - protected async discoverTestsInTestDirectory(options: TestDiscoveryOptions): Promise { - const token = options.token ? options.token : new CancellationTokenSource().token; - const discoveryOptions = { ...options }; - discoveryOptions.args = ['discover', 'pytest', '--', ...options.args]; - discoveryOptions.token = token; - - const discoveryService = this.serviceContainer.get(ITestDiscoveryService, 'common'); - if (discoveryOptions.token && discoveryOptions.token.isCancellationRequested) { - return Promise.reject('cancelled'); - } - - return discoveryService.discoverTests(discoveryOptions); - } -} diff --git a/src/client/testing/pytest/services/testMessageService.ts b/src/client/testing/pytest/services/testMessageService.ts deleted file mode 100644 index 6c716457678b..000000000000 --- a/src/client/testing/pytest/services/testMessageService.ts +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Location, Position, Range, TextLine, Uri, workspace } from 'vscode'; -import '../../../common/extensions'; -import { ProductNames } from '../../../common/installer/productNames'; -import { IFileSystem } from '../../../common/platform/types'; -import { Product } from '../../../common/types'; -import { IServiceContainer } from '../../../ioc/types'; -import { FlattenedTestFunction, ITestMessageService, Tests, TestStatus } from '../../common/types'; -import { ILocationStackFrameDetails, IPythonTestMessage, PythonTestMessageSeverity } from '../../types'; - -@injectable() -export class TestMessageService implements ITestMessageService { - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - /** - * Condense the test details down to just the potentially relevant information. Messages - * should only be created for tests that were actually run. - * - * @param testResults Details about all known tests. - */ - public async getFilteredTestMessages(rootDirectory: string, testResults: Tests): Promise { - const testFuncs = testResults.testFunctions.reduce((filtered, test) => { - if (test.testFunction.passed !== undefined || test.testFunction.status === TestStatus.Skipped) { - filtered.push(test); - } - return filtered; - }, []); - const messages: IPythonTestMessage[] = []; - for (const tf of testFuncs) { - const nameToRun = tf.testFunction.nameToRun; - const provider = ProductNames.get(Product.pytest)!; - const status = tf.testFunction.status!; - if (status === TestStatus.Pass) { - // If the test passed, there's not much to do with it. - const msg: IPythonTestMessage = { - code: nameToRun, - severity: PythonTestMessageSeverity.Pass, - provider: provider, - testTime: tf.testFunction.time, - status: status, - testFilePath: tf.parentTestFile.fullPath - }; - messages.push(msg); - } else { - // If the test did not pass, we need to parse the traceback to find each line in - // their respective files so they can be included as related information for the - // diagnostic. - const locationStack = await this.getLocationStack(rootDirectory, tf); - const message = tf.testFunction.message; - const testFilePath = tf.parentTestFile.fullPath; - let severity = PythonTestMessageSeverity.Error; - if (tf.testFunction.status === TestStatus.Skipped) { - severity = PythonTestMessageSeverity.Skip; - } - - const msg: IPythonTestMessage = { - code: nameToRun, - message: message, - severity: severity, - provider: provider, - traceback: tf.testFunction.traceback, - testTime: tf.testFunction.time, - testFilePath: testFilePath, - status: status, - locationStack: locationStack - }; - messages.push(msg); - } - } - return messages; - } - /** - * Given a FlattenedTestFunction, parse its traceback to piece together where each line in the - * traceback was in its respective file and grab the entire text of each line so they can be - * included in the Diagnostic as related information. - * - * @param testFunction The FlattenedTestFunction with the traceback that we need to parse. - */ - private async getLocationStack(rootDirectory: string, testFunction: FlattenedTestFunction): Promise { - const locationStack: ILocationStackFrameDetails[] = []; - if (testFunction.testFunction.traceback) { - const fileMatches = testFunction.testFunction.traceback.match(/^((\.\.[\\\/])*.+\.py)\:(\d+)\:.*$/gim) || []; - for (const fileDetailsMatch of fileMatches) { - const fileDetails = fileDetailsMatch.split(':'); - let filePath = fileDetails[0]; - filePath = path.isAbsolute(filePath) ? filePath : path.resolve(rootDirectory, filePath); - const fileUri = Uri.file(filePath); - const file = await workspace.openTextDocument(fileUri); - const fileLineNum = parseInt(fileDetails[1], 10); - const line = file.lineAt(fileLineNum - 1); - const location = new Location(fileUri, new Range( - new Position((fileLineNum - 1), line.firstNonWhitespaceCharacterIndex), - new Position((fileLineNum - 1), line.text.length) - )); - const stackFrame: ILocationStackFrameDetails = { location: location, lineText: file.getText(location.range) }; - locationStack.push(stackFrame); - } - } - // Find where the file the test was defined. - let testSourceFilePath = testFunction.testFunction.file!; - testSourceFilePath = path.isAbsolute(testSourceFilePath) ? testSourceFilePath : path.resolve(rootDirectory, testSourceFilePath); - const testSourceFileUri = Uri.file(testSourceFilePath); - const testSourceFile = await workspace.openTextDocument(testSourceFileUri); - let testDefLine: TextLine | null = null; - let lineNum = testFunction.testFunction.line!; - let lineText: string = ''; - let trimmedLineText: string = ''; - const testDefPrefix = 'def '; - const testAsyncDefPrefix = 'async def '; - let prefix = ''; - - while (testDefLine === null) { - const possibleTestDefLine = testSourceFile.lineAt(lineNum); - lineText = possibleTestDefLine.text; - trimmedLineText = lineText.trimLeft()!; - if (trimmedLineText.toLowerCase().startsWith(testDefPrefix)) { - testDefLine = possibleTestDefLine; - prefix = testDefPrefix; - } else if (trimmedLineText.toLowerCase().startsWith(testAsyncDefPrefix)) { - testDefLine = possibleTestDefLine; - prefix = testAsyncDefPrefix; - } else { - // The test definition may have been decorated, and there may be multiple - // decorations, so move to the next line and check it. - lineNum += 1; - } - } - const matches = trimmedLineText!.slice(prefix.length).match(/[^ \(:]+/); - const testSimpleName = matches ? matches[0] : ''; - const testDefStartCharNum = (lineText.length - trimmedLineText.length) + prefix.length; - const testDefEndCharNum = testDefStartCharNum + testSimpleName.length; - const lineStart = new Position(testDefLine!.lineNumber, testDefStartCharNum); - const lineEnd = new Position(testDefLine!.lineNumber, testDefEndCharNum); - const lineRange = new Range(lineStart, lineEnd); - const testDefLocation = new Location(testSourceFileUri, lineRange); - const testSourceLocationDetails = { location: testDefLocation, lineText: testSourceFile.getText(lineRange) }; - locationStack.unshift(testSourceLocationDetails); - - // Put the class declaration at the top of the stack if the test was imported. - if (testFunction.parentTestSuite !== undefined) { - // This could be an imported test method - const fs = this.serviceContainer.get(IFileSystem); - if (!fs.arePathsSame(Uri.file(testFunction.parentTestFile.fullPath).fsPath, locationStack[0].location.uri.fsPath)) { - // test method was imported, so reference class declaration line. - // this should be the first thing in the stack to show where the failure/error originated. - locationStack.unshift(await this.getParentSuiteLocation(testFunction)); - } - } - return locationStack; - } - /** - * The test that's associated with the FlattenedtestFunction was imported from another file, as the file - * location found in the traceback that shows what file the test was actually defined in is different than - * the file that the test was executed in. This must also mean that the test was part of a class that was - * imported and then inherited by the class that was actually run in the file. - * - * Test classes can be defined inside of other test classes, and even nested test classes of those that were - * imported will be discovered and ran. Luckily, for pytest, the entire chain of classes is preserved in the - * test's ID. However, in order to keep the Diagnostic as relevant as possible, it should point only at the - * most-nested test class that exists in the file that the test was actually run in, in order to provide the - * most context. This method attempts to go as far down the chain as it can, and resolves to the - * LocationStackFrameDetails for that test class. - * - * @param testFunction The FlattenedTestFunction that was executed. - */ - private async getParentSuiteLocation(testFunction: FlattenedTestFunction): Promise { - const suiteStackWithFileAndTest = testFunction.testFunction.nameToRun.replace('::()', '').split('::'); - // Don't need the file location or the test's name. - const suiteStack = suiteStackWithFileAndTest.slice(1, (suiteStackWithFileAndTest.length - 1)); - const testFileUri = Uri.file(testFunction.parentTestFile.fullPath); - const testFile = await workspace.openTextDocument(testFileUri); - const testFileLines = testFile.getText().splitLines({ trim: false, removeEmptyEntries: false }); - const reversedTestFileLines = testFileLines.slice().reverse(); - // Track the end of the parent scope. - let parentScopeEndIndex = 0; - let parentScopeStartIndex = testFileLines.length; - let parentIndentation: number | undefined; - const suiteLocationStackFrameDetails: ILocationStackFrameDetails[] = []; - - const classPrefix = 'class '; - while (suiteStack.length > 0) { - let indentation: number = 0; - let prevLowestIndentation: number | undefined; - // Get the name of the suite on top of the stack so it can be located. - const suiteName = suiteStack.shift()!; - let suiteDefLineIndex: number | undefined; - for (let index = parentScopeEndIndex; index < parentScopeStartIndex; index += 1) { - const lineText = reversedTestFileLines[index]; - if (lineText.trim().length === 0) { - // This line is just whitespace. - continue; - } - const trimmedLineText = lineText.trimLeft()!; - if (!trimmedLineText.toLowerCase().startsWith(classPrefix)) { - // line is not a class declaration - continue; - } - const matches = trimmedLineText.slice(classPrefix.length).match(/[^ \(:]+/); - const lineClassName = matches ? matches[0] : undefined; - - // Check if the indentation is proper. - if (parentIndentation === undefined) { - // The parentIndentation hasn't been set yet, so we are looking for a class that was - // defined in the global scope of the module. - if (trimmedLineText.length === lineText.length) { - // This line doesn't start with whitespace. - if (lineClassName === suiteName) { - // This is the line that we want. - suiteDefLineIndex = index; - indentation = 0; - // We have our line for the root suite declaration, so move on to processing the Location. - break; - } else { - // This is not the line we want, but may be the line that ends the scope of the class we want. - parentScopeEndIndex = index + 1; - } - } - } else { - indentation = lineText.length - trimmedLineText.length; - if (indentation <= parentIndentation) { - // This is not the line we want, but may be the line that ends the scope of the parent class. - parentScopeEndIndex = index + 1; - continue; - } - if (prevLowestIndentation === undefined || indentation < prevLowestIndentation) { - if (lineClassName === suiteName) { - // This might be the line that we want. - suiteDefLineIndex = index; - prevLowestIndentation = indentation; - } else { - // This is not the line we want, but may be the line that ends the scope of the class we want. - parentScopeEndIndex = index + 1; - } - } - } - } - if (suiteDefLineIndex === undefined) { - // Could not find the suite declaration line, so give up and move on with the latest one that we found. - break; - } - // Found the line to process. - parentScopeStartIndex = suiteDefLineIndex; - parentIndentation = indentation!; - - // Invert the index to get the unreversed equivalent. - const realIndex = (reversedTestFileLines.length - 1) - suiteDefLineIndex; - const startChar = indentation! + classPrefix.length; - const suiteStartPos = new Position(realIndex, startChar); - const suiteEndPos = new Position(realIndex, (startChar + suiteName!.length)); - const suiteRange = new Range(suiteStartPos, suiteEndPos); - const suiteLocation = new Location(testFileUri, suiteRange); - suiteLocationStackFrameDetails.push({ location: suiteLocation, lineText: testFile.getText(suiteRange) }); - } - return suiteLocationStackFrameDetails[suiteLocationStackFrameDetails.length - 1]; - } -} diff --git a/src/client/testing/pytest/testConfigurationManager.ts b/src/client/testing/pytest/testConfigurationManager.ts deleted file mode 100644 index 32e8a47d3daf..000000000000 --- a/src/client/testing/pytest/testConfigurationManager.ts +++ /dev/null @@ -1,59 +0,0 @@ -import * as path from 'path'; -import { QuickPickItem, Uri } from 'vscode'; -import { IFileSystem } from '../../common/platform/types'; -import { Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../types'; - -export class ConfigurationManager extends TestConfigurationManager { - constructor( - workspace: Uri, - serviceContainer: IServiceContainer, - cfg?: ITestConfigSettingsService - ) { - super(workspace, Product.pytest, serviceContainer, cfg); - } - public async requiresUserToConfigure(wkspace: Uri): Promise { - const configFiles = await this.getConfigFiles(wkspace.fsPath); - // If a config file exits, there's nothing to be configured. - if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { - return false; - } - return true; - } - public async configure(wkspace: Uri) { - const args: string[] = []; - const configFileOptionLabel = 'Use existing config file'; - const options: QuickPickItem[] = []; - const configFiles = await this.getConfigFiles(wkspace.fsPath); - // If a config file exits, there's nothing to be configured. - if (configFiles.length > 0 && configFiles.length !== 1 && configFiles[0] !== 'setup.cfg') { - return; - } - - if (configFiles.length === 1 && configFiles[0] === 'setup.cfg') { - options.push({ - label: configFileOptionLabel, - description: 'setup.cfg' - }); - } - const subDirs = await this.getTestDirs(wkspace.fsPath); - const testDir = await this.selectTestDir(wkspace.fsPath, subDirs, options); - if (typeof testDir === 'string' && testDir !== configFileOptionLabel) { - args.push(testDir); - } - const installed = await this.installer.isInstalled(Product.pytest); - if (!installed) { - await this.installer.install(Product.pytest); - } - await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.pytest, args); - } - private async getConfigFiles(rootDir: string): Promise { - const fs = this.serviceContainer.get(IFileSystem); - const promises = ['pytest.ini', 'tox.ini', 'setup.cfg'] - .map(async cfg => await fs.fileExists(path.join(rootDir, cfg)) ? cfg : ''); - const values = await Promise.all(promises); - return values.filter(exists => exists.length > 0); - } -} diff --git a/src/client/testing/serviceRegistry.ts b/src/client/testing/serviceRegistry.ts index fde5cc92e0e4..d36fab7686f8 100644 --- a/src/client/testing/serviceRegistry.ts +++ b/src/client/testing/serviceRegistry.ts @@ -1,151 +1,38 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; import { IExtensionActivationService } from '../activation/types'; -import { IServiceContainer, IServiceManager } from '../ioc/types'; -import { ArgumentsHelper } from './common/argumentsHelper'; -import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './common/constants'; +import { IServiceManager } from '../ioc/types'; import { DebugLauncher } from './common/debugLauncher'; -import { TestRunner } from './common/runner'; -import { TestConfigSettingsService } from './common/services/configSettingService'; -import { TestContextService } from './common/services/contextService'; -import { TestDiscoveredTestParser } from './common/services/discoveredTestParser'; -import { TestsDiscoveryService } from './common/services/discovery'; -import { TestCollectionStorageService } from './common/services/storageService'; -import { TestManagerService } from './common/services/testManagerService'; -import { TestResultsService } from './common/services/testResultsService'; -import { TestsStatusUpdaterService } from './common/services/testsStatusService'; -import { ITestDiscoveredTestParser } from './common/services/types'; -import { UnitTestDiagnosticService } from './common/services/unitTestDiagnosticService'; -import { WorkspaceTestManagerService } from './common/services/workspaceTestManagerService'; +import { TestConfigSettingsService } from './common/configSettingService'; import { TestsHelper } from './common/testUtils'; -import { TestFlatteningVisitor } from './common/testVisitors/flatteningVisitor'; -import { TestResultResetVisitor } from './common/testVisitors/resultResetVisitor'; import { - ITestCollectionStorageService, ITestContextService, - ITestDebugLauncher, ITestDiscoveryService, ITestManager, ITestManagerFactory, - ITestManagerService, ITestManagerServiceFactory, ITestMessageService, ITestResultsService, - ITestRunner, ITestsHelper, ITestsParser, ITestsStatusUpdaterService, ITestVisitor, - IUnitTestSocketServer, IWorkspaceTestManagerService, IXUnitParser, TestProvider + ITestConfigSettingsService, + ITestConfigurationManagerFactory, + ITestConfigurationService, + ITestDebugLauncher, + ITestsHelper, } from './common/types'; -import { UpdateTestSettingService } from './common/updateTestSettings'; -import { XUnitParser } from './common/xUnitParser'; import { UnitTestConfigurationService } from './configuration'; import { TestConfigurationManagerFactory } from './configurationFactory'; -import { TestResultDisplay } from './display/main'; -import { TestDisplay } from './display/picker'; -import { TestExplorerCommandHandler } from './explorer/commandHandlers'; -import { FailedTestHandler } from './explorer/failedTestHandler'; -import { TestTreeViewProvider } from './explorer/testTreeViewProvider'; -import { TreeViewService } from './explorer/treeView'; -import { UnitTestManagementService } from './main'; -import { registerTypes as registerNavigationTypes } from './navigation/serviceRegistry'; -import { ITestExplorerCommandHandler } from './navigation/types'; -import { TestManager as NoseTestManager } from './nosetest/main'; -import { TestManagerRunner as NoseTestManagerRunner } from './nosetest/runner'; -import { ArgumentsService as NoseTestArgumentsService } from './nosetest/services/argsService'; -import { TestDiscoveryService as NoseTestDiscoveryService } from './nosetest/services/discoveryService'; -import { TestsParser as NoseTestTestsParser } from './nosetest/services/parserService'; -import { TestManager as PyTestTestManager } from './pytest/main'; -import { TestManagerRunner as PytestManagerRunner } from './pytest/runner'; -import { ArgumentsService as PyTestArgumentsService } from './pytest/services/argsService'; -import { TestDiscoveryService as PytestTestDiscoveryService } from './pytest/services/discoveryService'; -import { TestMessageService } from './pytest/services/testMessageService'; -import { - IArgumentsHelper, IArgumentsService, ITestConfigSettingsService, - ITestConfigurationManagerFactory, ITestConfigurationService, ITestDataItemResource, - ITestDiagnosticService, ITestDisplay, ITestManagementService, - ITestManagerRunner, - ITestResultDisplay, ITestTreeViewProvider, IUnitTestHelper -} from './types'; -import { UnitTestHelper } from './unittest/helper'; -import { TestManager as UnitTestTestManager } from './unittest/main'; -import { TestManagerRunner as UnitTestTestManagerRunner } from './unittest/runner'; -import { ArgumentsService as UnitTestArgumentsService } from './unittest/services/argsService'; -import { TestDiscoveryService as UnitTestTestDiscoveryService } from './unittest/services/discoveryService'; -import { TestsParser as UnitTestTestsParser } from './unittest/services/parserService'; -import { UnitTestSocketServer } from './unittest/socketServer'; +import { TestingService, UnitTestManagementService } from './main'; +import { ITestingService } from './types'; +import { registerTestControllerTypes } from './testController/serviceRegistry'; export function registerTypes(serviceManager: IServiceManager) { - registerNavigationTypes(serviceManager); serviceManager.addSingleton(ITestDebugLauncher, DebugLauncher); - serviceManager.addSingleton(ITestCollectionStorageService, TestCollectionStorageService); - serviceManager.addSingleton(IWorkspaceTestManagerService, WorkspaceTestManagerService); serviceManager.add(ITestsHelper, TestsHelper); - serviceManager.add(ITestDiscoveredTestParser, TestDiscoveredTestParser); - serviceManager.add(ITestDiscoveryService, TestsDiscoveryService, 'common'); - serviceManager.add(IUnitTestSocketServer, UnitTestSocketServer); - serviceManager.addSingleton(ITestContextService, TestContextService); - serviceManager.addSingleton(ITestsStatusUpdaterService, TestsStatusUpdaterService); - - serviceManager.add(ITestResultsService, TestResultsService); - - serviceManager.add(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); - serviceManager.add(ITestVisitor, TestResultResetVisitor, 'TestResultResetVisitor'); - - serviceManager.add(ITestsParser, UnitTestTestsParser, UNITTEST_PROVIDER); - serviceManager.add(ITestsParser, NoseTestTestsParser, NOSETEST_PROVIDER); - - serviceManager.add(ITestDiscoveryService, UnitTestTestDiscoveryService, UNITTEST_PROVIDER); - serviceManager.add(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); - serviceManager.add(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); - - serviceManager.add(IArgumentsHelper, ArgumentsHelper); - serviceManager.add(ITestRunner, TestRunner); - serviceManager.add(IXUnitParser, XUnitParser); - serviceManager.add(IUnitTestHelper, UnitTestHelper); - - serviceManager.add(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); - serviceManager.add(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); - serviceManager.add(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); - serviceManager.add(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); - serviceManager.add(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); - serviceManager.add(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); serviceManager.addSingleton(ITestConfigurationService, UnitTestConfigurationService); - serviceManager.addSingleton(ITestManagementService, UnitTestManagementService); - serviceManager.addSingleton(ITestResultDisplay, TestResultDisplay); - serviceManager.addSingleton(ITestDisplay, TestDisplay); - serviceManager.addSingleton(ITestConfigSettingsService, TestConfigSettingsService); - serviceManager.addSingleton(ITestConfigurationManagerFactory, TestConfigurationManagerFactory); - - serviceManager.addSingleton(ITestDiagnosticService, UnitTestDiagnosticService); - serviceManager.addSingleton(ITestMessageService, TestMessageService, PYTEST_PROVIDER); - serviceManager.addSingleton(ITestTreeViewProvider, TestTreeViewProvider); - serviceManager.addSingleton(ITestDataItemResource, TestTreeViewProvider); - serviceManager.addSingleton(ITestExplorerCommandHandler, TestExplorerCommandHandler); - serviceManager.addSingleton(IExtensionActivationService, TreeViewService); - serviceManager.addSingleton(IExtensionActivationService, FailedTestHandler); - serviceManager.addSingleton(IExtensionActivationService, UpdateTestSettingService); + serviceManager.addSingleton(ITestingService, TestingService); - serviceManager.addFactory(ITestManagerFactory, (context) => { - return (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string) => { - const serviceContainer = context.container.get(IServiceContainer); - - switch (testProvider) { - case NOSETEST_PROVIDER: { - return new NoseTestManager(workspaceFolder, rootDirectory, serviceContainer); - } - case PYTEST_PROVIDER: { - return new PyTestTestManager(workspaceFolder, rootDirectory, serviceContainer); - } - case UNITTEST_PROVIDER: { - return new UnitTestTestManager(workspaceFolder, rootDirectory, serviceContainer); - } - default: { - throw new Error(`Unrecognized test provider '${testProvider}'`); - } - } - }; - }); + serviceManager.addSingleton(ITestConfigSettingsService, TestConfigSettingsService); + serviceManager.addSingleton( + ITestConfigurationManagerFactory, + TestConfigurationManagerFactory, + ); + serviceManager.addSingleton(IExtensionActivationService, UnitTestManagementService); - serviceManager.addFactory(ITestManagerServiceFactory, (context) => { - return (workspaceFolder: Uri) => { - const serviceContainer = context.container.get(IServiceContainer); - const testsHelper = context.container.get(ITestsHelper); - return new TestManagerService(workspaceFolder, testsHelper, serviceContainer); - }; - }); + registerTestControllerTypes(serviceManager); } diff --git a/src/client/testing/testController/common/argumentsHelper.ts b/src/client/testing/testController/common/argumentsHelper.ts new file mode 100644 index 000000000000..c155d0197da7 --- /dev/null +++ b/src/client/testing/testController/common/argumentsHelper.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { traceWarn } from '../../../logging'; + +export function getPositionalArguments( + args: string[], + optionsWithArguments: string[] = [], + optionsWithoutArguments: string[] = [], +): string[] { + const nonPositionalIndexes: number[] = []; + args.forEach((arg, index) => { + if (optionsWithoutArguments.indexOf(arg) !== -1) { + nonPositionalIndexes.push(index); + } else if (optionsWithArguments.indexOf(arg) !== -1) { + nonPositionalIndexes.push(index); + // Cuz the next item is the value. + nonPositionalIndexes.push(index + 1); + } else if (optionsWithArguments.findIndex((item) => arg.startsWith(`${item}=`)) !== -1) { + nonPositionalIndexes.push(index); + } else if (arg.startsWith('-')) { + // Ok this is an unknown option, lets treat this as one without values. + traceWarn( + `Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new`, + ); + nonPositionalIndexes.push(index); + } else if (arg.indexOf('=') > 0) { + // Ok this is an unknown option with a value + traceWarn( + `Unknown command line option passed into args parser for tests '${arg}'. Please report on https://github.com/Microsoft/vscode-python/issues/new`, + ); + nonPositionalIndexes.push(index); + } + }); + return args.filter((_, index) => nonPositionalIndexes.indexOf(index) === -1); +} + +export function filterArguments( + args: string[], + optionsWithArguments: string[] = [], + optionsWithoutArguments: string[] = [], +): string[] { + let ignoreIndex = -1; + return args.filter((arg, index) => { + if (ignoreIndex === index) { + return false; + } + // Options can use wild cards (with trailing '*') + if ( + optionsWithoutArguments.indexOf(arg) >= 0 || + optionsWithoutArguments.filter((option) => option.endsWith('*') && arg.startsWith(option.slice(0, -1))) + .length > 0 + ) { + return false; + } + // Ignore args that match exactly. + if (optionsWithArguments.indexOf(arg) >= 0) { + ignoreIndex = index + 1; + return false; + } + // Ignore args that match exactly with wild cards & do not have inline values. + if (optionsWithArguments.filter((option) => arg.startsWith(`${option}=`)).length > 0) { + return false; + } + // Ignore args that match a wild card (ending with *) and no inline values. + // Eg. arg='--log-cli-level' and optionsArguments=['--log-*'] + if ( + arg.indexOf('=') === -1 && + optionsWithoutArguments.filter((option) => option.endsWith('*') && arg.startsWith(option.slice(0, -1))) + .length > 0 + ) { + ignoreIndex = index + 1; + return false; + } + // Ignore args that match a wild card (ending with *) and have inline values. + // Eg. arg='--log-cli-level=XYZ' and optionsArguments=['--log-*'] + if ( + arg.indexOf('=') >= 0 && + optionsWithoutArguments.filter((option) => option.endsWith('*') && arg.startsWith(option.slice(0, -1))) + .length > 0 + ) { + return false; + } + return true; + }); +} diff --git a/src/client/testing/testController/common/discoveryHelpers.ts b/src/client/testing/testController/common/discoveryHelpers.ts new file mode 100644 index 000000000000..e170ad576ae8 --- /dev/null +++ b/src/client/testing/testController/common/discoveryHelpers.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationToken, CancellationTokenSource, Disposable, Uri } from 'vscode'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { createDiscoveryErrorPayload, fixLogLinesNoTrailing, startDiscoveryNamedPipe } from './utils'; +import { DiscoveredTestPayload, ITestResultResolver } from './types'; + +/** + * Test provider type for logging purposes. + */ +export type TestProvider = 'pytest' | 'unittest'; + +/** + * Sets up the discovery named pipe and wires up cancellation. + * @param resultResolver The resolver to handle discovered test data + * @param token Optional cancellation token from the caller + * @param uri Workspace URI for logging + * @returns Object containing the pipe name, cancellation source, and disposable for the external token handler + */ +export async function setupDiscoveryPipe( + resultResolver: ITestResultResolver | undefined, + token: CancellationToken | undefined, + uri: Uri, +): Promise<{ pipeName: string; cancellation: CancellationTokenSource; tokenDisposable: Disposable | undefined }> { + const discoveryPipeCancellation = new CancellationTokenSource(); + + // Wire up cancellation from external token and store the disposable + const tokenDisposable = token?.onCancellationRequested(() => { + traceInfo(`Test discovery cancelled.`); + discoveryPipeCancellation.cancel(); + }); + + // Start the named pipe with the discovery listener + const discoveryPipeName = await startDiscoveryNamedPipe((data: DiscoveredTestPayload) => { + if (!token?.isCancellationRequested) { + resultResolver?.resolveDiscovery(data); + } + }, discoveryPipeCancellation.token); + + traceVerbose(`Created discovery pipe: ${discoveryPipeName} for workspace ${uri.fsPath}`); + + return { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + }; +} + +/** + * Creates standard process event handlers for test discovery subprocess. + * Handles stdout/stderr logging and error reporting on process exit. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param uri - The workspace URI + * @param cwd - The current working directory + * @param resultResolver - Resolver for test discovery results + * @param deferredTillExecClose - Deferred to resolve when process closes + * @param allowedSuccessCodes - Additional exit codes to treat as success (e.g., pytest exit code 5 for no tests found) + */ +export function createProcessHandlers( + testProvider: TestProvider, + uri: Uri, + cwd: string, + resultResolver: ITestResultResolver | undefined, + deferredTillExecClose: Deferred, + allowedSuccessCodes: number[] = [], +): { + onStdout: (data: any) => void; + onStderr: (data: any) => void; + onExit: (code: number | null, signal: NodeJS.Signals | null) => void; + onClose: (code: number | null, signal: NodeJS.Signals | null) => void; +} { + const isSuccessCode = (code: number | null): boolean => { + return code === 0 || (code !== null && allowedSuccessCodes.includes(code)); + }; + + return { + onStdout: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceInfo(out); + }, + onStderr: (data: any) => { + const out = fixLogLinesNoTrailing(data.toString()); + traceError(out); + }, + onExit: (code: number | null, _signal: NodeJS.Signals | null) => { + // The 'exit' event fires when the process terminates, but streams may still be open. + // Only log verbose success message here; error handling happens in onClose. + if (isSuccessCode(code)) { + traceVerbose(`${testProvider} discovery subprocess exited successfully for workspace ${uri.fsPath}`); + } + }, + onClose: (code: number | null, signal: NodeJS.Signals | null) => { + // We resolve the deferred here to ensure all output has been captured. + if (!isSuccessCode(code)) { + traceError( + `${testProvider} discovery failed with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating error payload.`, + ); + resultResolver?.resolveDiscovery(createDiscoveryErrorPayload(code, signal, cwd)); + } else { + traceVerbose(`${testProvider} discovery subprocess streams closed for workspace ${uri.fsPath}`); + } + deferredTillExecClose?.resolve(); + }, + }; +} + +/** + * Handles cleanup when test discovery is cancelled. + * Kills the subprocess (if running), resolves the completion deferred, and cancels the discovery pipe. + * + * @param testProvider - The test framework being used ('pytest' or 'unittest') + * @param proc - The process to kill + * @param processCompletion - Deferred to resolve + * @param pipeCancellation - Cancellation token source to cancel + * @param uri - The workspace URI + */ +export function cleanupOnCancellation( + testProvider: TestProvider, + proc: { kill: () => void } | undefined, + processCompletion: Deferred, + pipeCancellation: CancellationTokenSource, + uri: Uri, +): void { + traceInfo(`Test discovery cancelled, killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + if (proc) { + traceVerbose(`Killing ${testProvider} subprocess for workspace ${uri.fsPath}`); + proc.kill(); + } else { + traceVerbose(`No ${testProvider} subprocess to kill for workspace ${uri.fsPath} (proc is undefined)`); + } + traceVerbose(`Resolving process completion deferred for ${testProvider} discovery in workspace ${uri.fsPath}`); + processCompletion.resolve(); + traceVerbose(`Cancelling discovery pipe for ${testProvider} discovery in workspace ${uri.fsPath}`); + pipeCancellation.cancel(); +} diff --git a/src/client/testing/testController/common/projectAdapter.ts b/src/client/testing/testController/common/projectAdapter.ts new file mode 100644 index 000000000000..cfffbf439ca6 --- /dev/null +++ b/src/client/testing/testController/common/projectAdapter.ts @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestItem, Uri } from 'vscode'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { PythonEnvironment, PythonProject } from '../../../envExt/types'; + +/** + * Represents a single Python project with its own test infrastructure. + * A project is defined as a combination of a Python executable + URI (folder/file). + * Projects are uniquely identified by their projectUri (use projectUri.toString() for map keys). + */ +export interface ProjectAdapter { + // === IDENTITY === + /** + * Display name for the project (e.g., "alice (Python 3.11)"). + */ + projectName: string; + + /** + * URI of the project root folder or file. + * This is the unique identifier for the project. + */ + projectUri: Uri; + + /** + * Parent workspace URI containing this project. + */ + workspaceUri: Uri; + + // === API OBJECTS (from vscode-python-environments extension) === + /** + * The PythonProject object from the environment API. + */ + pythonProject: PythonProject; + + /** + * The resolved PythonEnvironment with execution details. + * Contains execInfo.run.executable for running tests. + */ + pythonEnvironment: PythonEnvironment; + + // === TEST INFRASTRUCTURE === + /** + * Test framework provider ('pytest' | 'unittest'). + */ + testProvider: TestProvider; + + /** + * Adapter for test discovery. + */ + discoveryAdapter: ITestDiscoveryAdapter; + + /** + * Adapter for test execution. + */ + executionAdapter: ITestExecutionAdapter; + + /** + * Result resolver for this project (maps test IDs and handles results). + */ + resultResolver: ITestResultResolver; + + /** + * Absolute paths of nested projects to ignore during discovery. + * Used to pass --ignore flags to pytest or exclusion filters to unittest. + * Only populated for parent projects that contain nested child projects. + */ + nestedProjectPathsToIgnore?: string[]; + + // === LIFECYCLE === + /** + * Whether discovery is currently running for this project. + */ + isDiscovering: boolean; + + /** + * Whether tests are currently executing for this project. + */ + isExecuting: boolean; + + /** + * Root TestItem for this project in the VS Code test tree. + * All project tests are children of this item. + */ + projectRootTestItem?: TestItem; +} diff --git a/src/client/testing/testController/common/projectTestExecution.ts b/src/client/testing/testController/common/projectTestExecution.ts new file mode 100644 index 000000000000..fe3b4f91491a --- /dev/null +++ b/src/client/testing/testController/common/projectTestExecution.ts @@ -0,0 +1,296 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, FileCoverageDetail, TestItem, TestRun, TestRunProfileKind, TestRunRequest } from 'vscode'; +import { traceError, traceInfo, traceVerbose, traceWarn } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { ITestDebugLauncher } from '../../common/types'; +import { ProjectAdapter } from './projectAdapter'; +import { TestProjectRegistry } from './testProjectRegistry'; +import { getProjectId } from './projectUtils'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { isParentPath } from '../../../pythonEnvironments/common/externalDependencies'; + +/** Dependencies for project-based test execution. */ +export interface ProjectExecutionDependencies { + projectRegistry: TestProjectRegistry; + pythonExecFactory: IPythonExecutionFactory; + debugLauncher: ITestDebugLauncher; +} + +/** Executes tests for multiple projects, grouping by project and using each project's Python environment. */ +export async function executeTestsForProjects( + projects: ProjectAdapter[], + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + deps: ProjectExecutionDependencies, +): Promise { + if (projects.length === 0) { + traceError(`[test-by-project] No projects provided for execution`); + return; + } + + // Early exit if already cancelled + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Execution cancelled before starting`); + return; + } + + // Group test items by project + const testsByProject = await groupTestItemsByProject(testItems, projects); + + const isDebugMode = request.profile?.kind === TestRunProfileKind.Debug; + traceInfo(`[test-by-project] Executing tests across ${testsByProject.size} project(s), debug=${isDebugMode}`); + + // Setup coverage once for all projects (single callback that routes by file path) + if (request.profile?.kind === TestRunProfileKind.Coverage) { + setupCoverageForProjects(request, projects); + } + + // Execute tests for each project in parallel + // For debug mode, multiple debug sessions will be launched in parallel + // Each execution respects cancellation via runInstance.token + const executions = Array.from(testsByProject.entries()).map(async ([_projectId, { project, items }]) => { + // Check for cancellation before starting each project + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Skipping ${project.projectName} - cancellation requested`); + return; + } + + if (items.length === 0) return; + + traceInfo(`[test-by-project] Executing ${items.length} test item(s) for project: ${project.projectName}`); + + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: project.testProvider, + debugging: isDebugMode, + }); + + try { + await executeTestsForProject(project, items, runInstance, request, deps); + } catch (error) { + // Don't log cancellation as an error + if (!token.isCancellationRequested) { + traceError(`[test-by-project] Execution failed for project ${project.projectName}:`, error); + } + } + }); + + await Promise.all(executions); + + if (token.isCancellationRequested) { + traceInfo(`[test-by-project] Project executions cancelled`); + } else { + traceInfo(`[test-by-project] All project executions completed`); + } +} + +/** Lookup context for caching project lookups within a single test run. */ +interface ProjectLookupContext { + uriToAdapter: Map; + projectPathToAdapter: Map; +} + +/** Groups test items by owning project using env API or path-based matching as fallback. */ +export async function groupTestItemsByProject( + testItems: TestItem[], + projects: ProjectAdapter[], +): Promise> { + const result = new Map(); + + // Initialize entries for all projects + for (const project of projects) { + result.set(getProjectId(project.projectUri), { project, items: [] }); + } + + // Build lookup context for this run - O(p) one-time setup, enables O(1) lookups per item. + // When tests are from a single project, most lookups hit the cache after the first item. + const lookupContext: ProjectLookupContext = { + uriToAdapter: new Map(), + projectPathToAdapter: new Map(projects.map((p) => [p.projectUri.fsPath, p])), + }; + + // Assign each test item to its project + for (const item of testItems) { + const project = await findProjectForTestItem(item, projects, lookupContext); + if (project) { + const entry = result.get(getProjectId(project.projectUri)); + if (entry) { + entry.items.push(item); + } + } else { + // If no project matches, log it + traceWarn(`[test-by-project] Could not match test item ${item.id} to a project`); + } + } + + // Remove projects with no test items + for (const [projectId, entry] of result.entries()) { + if (entry.items.length === 0) { + result.delete(projectId); + } + } + + return result; +} + +/** Finds the project that owns a test item. */ +export async function findProjectForTestItem( + item: TestItem, + projects: ProjectAdapter[], + lookupContext?: ProjectLookupContext, +): Promise { + if (!item.uri) return undefined; + + const uriPath = item.uri.fsPath; + + // Check lookup context first - O(1) + if (lookupContext?.uriToAdapter.has(uriPath)) { + return lookupContext.uriToAdapter.get(uriPath); + } + + let result: ProjectAdapter | undefined; + + // Try using the Python Environment extension API first. + // Legacy path: when useEnvExtension() is false, this block is skipped and we go + // directly to findProjectByPath() below (path-based matching). + if (useEnvExtension()) { + try { + const envExtApi = await getEnvExtApi(); + const pythonProject = envExtApi.getPythonProject(item.uri); + if (pythonProject) { + // Use lookup context for O(1) adapter lookup instead of O(p) linear search + result = lookupContext?.projectPathToAdapter.get(pythonProject.uri.fsPath); + if (!result) { + // Fallback to linear search if lookup context not available + result = projects.find((p) => p.projectUri.fsPath === pythonProject.uri.fsPath); + } + } + } catch (error) { + traceVerbose(`[test-by-project] Failed to use env extension API, falling back to path matching: ${error}`); + } + } + + // Fallback: path-based matching when env API unavailable or didn't find a match. + // O(p) time complexity where p = number of projects. + if (!result) { + result = findProjectByPath(item, projects); + } + + // Store result for future lookups of same file within this run - O(1) + if (lookupContext) { + lookupContext.uriToAdapter.set(uriPath, result); + } + + return result; +} + +/** Fallback: finds project using path-based matching. */ +function findProjectByPath(item: TestItem, projects: ProjectAdapter[]): ProjectAdapter | undefined { + if (!item.uri) return undefined; + + const itemPath = item.uri.fsPath; + let bestMatch: ProjectAdapter | undefined; + let bestMatchLength = 0; + + for (const project of projects) { + const projectPath = project.projectUri.fsPath; + // Use isParentPath for safe path-boundary matching (handles separators and case normalization) + if (isParentPath(itemPath, projectPath) && projectPath.length > bestMatchLength) { + bestMatch = project; + bestMatchLength = projectPath.length; + } + } + + return bestMatch; +} + +/** Executes tests for a single project using the project's Python environment. */ +export async function executeTestsForProject( + project: ProjectAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + deps: ProjectExecutionDependencies, +): Promise { + const processedTestItemIds = new Set(); + const uniqueTestCaseIds = new Set(); + + // Mark items as started and collect test IDs (deduplicated to handle overlapping selections) + for (const item of testItems) { + const testCaseNodes = getTestCaseNodesRecursive(item); + for (const node of testCaseNodes) { + if (processedTestItemIds.has(node.id)) { + continue; + } + processedTestItemIds.add(node.id); + runInstance.started(node); + const runId = project.resultResolver.vsIdToRunId.get(node.id); + if (runId) { + uniqueTestCaseIds.add(runId); + } + } + } + + const testCaseIds = Array.from(uniqueTestCaseIds); + + if (testCaseIds.length === 0) { + traceVerbose(`[test-by-project] No test IDs found for project ${project.projectName}`); + return; + } + + traceInfo(`[test-by-project] Running ${testCaseIds.length} test(s) for project: ${project.projectName}`); + + // Execute tests using the project's execution adapter + await project.executionAdapter.runTests( + project.projectUri, + testCaseIds, + request.profile?.kind, + runInstance, + deps.pythonExecFactory, + deps.debugLauncher, + undefined, // interpreter not needed, project has its own environment + project, + ); +} + +/** Recursively gets all leaf test case nodes from a test item tree. */ +export function getTestCaseNodesRecursive(item: TestItem): TestItem[] { + const results: TestItem[] = []; + if (item.children.size === 0) { + // This is a leaf node (test case) + results.push(item); + } else { + // Recursively get children + item.children.forEach((child) => { + results.push(...getTestCaseNodesRecursive(child)); + }); + } + return results; +} + +/** Sets up detailed coverage loading that routes to the correct project by file path. */ +export function setupCoverageForProjects(request: TestRunRequest, projects: ProjectAdapter[]): void { + if (request.profile?.kind === TestRunProfileKind.Coverage) { + // Create a single callback that routes to the correct project's coverage map by file path + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const filePath = fileCoverage.uri.fsPath; + // Find the project that has coverage data for this file + for (const project of projects) { + const details = project.resultResolver.detailedCoverageMap.get(filePath); + if (details) { + return Promise.resolve(details); + } + } + return Promise.resolve([]); + }; + } +} diff --git a/src/client/testing/testController/common/projectUtils.ts b/src/client/testing/testController/common/projectUtils.ts new file mode 100644 index 000000000000..b104b7f6842d --- /dev/null +++ b/src/client/testing/testController/common/projectUtils.ts @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { Uri } from 'vscode'; +import { IConfigurationService } from '../../../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './types'; +import { UnittestTestDiscoveryAdapter } from '../unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../unittest/testExecutionAdapter'; +import { PytestTestDiscoveryAdapter } from '../pytest/pytestDiscoveryAdapter'; +import { PytestTestExecutionAdapter } from '../pytest/pytestExecutionAdapter'; + +/** + * Separator used to scope test IDs to a specific project. + * Format: {projectId}{SEPARATOR}{testPath} + * Example: "file:///workspace/project@@PROJECT@@test_file.py::test_name" + */ +export const PROJECT_ID_SEPARATOR = '@@vsc@@'; + +/** + * Gets the project ID from a project URI. + * The project ID is simply the string representation of the URI, matching how + * the Python Environments extension stores projects in Map. + * + * @param projectUri The project URI + * @returns The project ID (URI as string) + */ +export function getProjectId(projectUri: Uri): string { + return projectUri.toString(); +} + +/** + * Parses a project-scoped vsId back into its components. + * + * @param vsId The VS Code test item ID to parse + * @returns A tuple of [projectId, runId]. If the ID is not project-scoped, + * returns [undefined, vsId] (legacy format) + */ +export function parseVsId(vsId: string): [string | undefined, string] { + const separatorIndex = vsId.indexOf(PROJECT_ID_SEPARATOR); + if (separatorIndex === -1) { + return [undefined, vsId]; // Legacy ID without project scope + } + return [vsId.substring(0, separatorIndex), vsId.substring(separatorIndex + PROJECT_ID_SEPARATOR.length)]; +} + +/** + * Creates a display name for a project including Python version. + * Format: "{projectName} (Python {version})" + * + * @param projectName The name of the project + * @param pythonVersion The Python version string (e.g., "3.11.2") + * @returns Formatted display name + */ +export function createProjectDisplayName(projectName: string, pythonVersion: string): string { + // Extract major.minor version if full version provided + const versionMatch = pythonVersion.match(/^(\d+\.\d+)/); + const shortVersion = versionMatch ? versionMatch[1] : pythonVersion; + + return `${projectName} (Python ${shortVersion})`; +} + +/** + * Creates test adapters (discovery and execution) for a given test provider. + * + * @param testProvider The test framework provider ('pytest' | 'unittest') + * @param resultResolver The result resolver to use for test results + * @param configSettings The configuration service + * @param envVarsService The environment variables provider + * @returns An object containing the discovery and execution adapters + */ +export function createTestAdapters( + testProvider: TestProvider, + resultResolver: ITestResultResolver, + configSettings: IConfigurationService, + envVarsService: IEnvironmentVariablesProvider, +): { discoveryAdapter: ITestDiscoveryAdapter; executionAdapter: ITestExecutionAdapter } { + if (testProvider === UNITTEST_PROVIDER) { + return { + discoveryAdapter: new UnittestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new UnittestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; + } + + return { + discoveryAdapter: new PytestTestDiscoveryAdapter(configSettings, resultResolver, envVarsService), + executionAdapter: new PytestTestExecutionAdapter(configSettings, resultResolver, envVarsService), + }; +} diff --git a/src/client/testing/testController/common/resultResolver.ts b/src/client/testing/testController/common/resultResolver.ts new file mode 100644 index 000000000000..c126d233de1b --- /dev/null +++ b/src/client/testing/testController/common/resultResolver.ts @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, TestItem, Uri, TestRun, FileCoverageDetail } from 'vscode'; +import { CoveragePayload, DiscoveredTestPayload, ExecutionTestPayload, ITestResultResolver } from './types'; +import { TestProvider } from '../../types'; +import { traceInfo } from '../../../logging'; +import { sendTelemetryEvent } from '../../../telemetry'; +import { EventName } from '../../../telemetry/constants'; +import { TestItemIndex } from './testItemIndex'; +import { TestDiscoveryHandler } from './testDiscoveryHandler'; +import { TestExecutionHandler } from './testExecutionHandler'; +import { TestCoverageHandler } from './testCoverageHandler'; + +export class PythonResultResolver implements ITestResultResolver { + testController: TestController; + + testProvider: TestProvider; + + private testItemIndex: TestItemIndex; + + // Shared singleton handlers + private static discoveryHandler: TestDiscoveryHandler = new TestDiscoveryHandler(); + private static executionHandler: TestExecutionHandler = new TestExecutionHandler(); + private static coverageHandler: TestCoverageHandler = new TestCoverageHandler(); + + public detailedCoverageMap = new Map(); + + /** + * Optional project ID for scoping test IDs. + * When set, all test IDs are prefixed with `{projectId}@@vsc@@` for project-based testing. + * When undefined, uses legacy workspace-level IDs for backward compatibility. + */ + private projectId?: string; + + /** + * Optional project display name for labeling the test tree root. + * When set, the root node label will be "project: {projectName}" instead of the folder name. + */ + private projectName?: string; + + constructor( + testController: TestController, + testProvider: TestProvider, + private workspaceUri: Uri, + projectId?: string, + projectName?: string, + ) { + this.testController = testController; + this.testProvider = testProvider; + this.projectId = projectId; + this.projectName = projectName; + // Initialize a new TestItemIndex which will be used to track test items in this workspace/project + this.testItemIndex = new TestItemIndex(); + } + + // Expose for backward compatibility (WorkspaceTestAdapter accesses these) + public get runIdToTestItem(): Map { + return this.testItemIndex.runIdToTestItemMap; + } + + public get runIdToVSid(): Map { + return this.testItemIndex.runIdToVSidMap; + } + + public get vsIdToRunId(): Map { + return this.testItemIndex.vsIdToRunIdMap; + } + + /** + * Gets the project ID for this resolver (if any). + * Used for project-scoped test ID generation. + */ + public getProjectId(): string | undefined { + return this.projectId; + } + + public resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + PythonResultResolver.discoveryHandler.processDiscovery( + payload, + this.testController, + this.testItemIndex, + this.workspaceUri, + this.testProvider, + token, + this.projectId, + this.projectName, + ); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { + tool: this.testProvider, + failed: false, + }); + } + + public _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void { + // Delegate to the public method for backward compatibility + this.resolveDiscovery(payload, token); + } + + public resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void { + if ('coverage' in payload) { + // coverage data is sent once per connection + traceInfo('Coverage data received, processing...'); + this.detailedCoverageMap = PythonResultResolver.coverageHandler.processCoverage( + payload as CoveragePayload, + runInstance, + ); + traceInfo('Coverage data processing complete.'); + } else { + PythonResultResolver.executionHandler.processExecution( + payload as ExecutionTestPayload, + runInstance, + this.testItemIndex, + this.testController, + ); + } + } + + public _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); + } + + public _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void { + // Delegate to the public method for backward compatibility + this.resolveExecution(payload, runInstance); + } + + /** + * Clean up stale test item references from the cache maps. + * Validates cached items and removes any that are no longer in the test tree. + * Delegates to TestItemIndex. + */ + public cleanupStaleReferences(): void { + this.testItemIndex.cleanupStaleReferences(this.testController); + } +} diff --git a/src/client/testing/testController/common/testCoverageHandler.ts b/src/client/testing/testController/common/testCoverageHandler.ts new file mode 100644 index 000000000000..81ec80579730 --- /dev/null +++ b/src/client/testing/testController/common/testCoverageHandler.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, TestCoverageCount, FileCoverage, FileCoverageDetail, StatementCoverage, Range } from 'vscode'; +import { CoveragePayload, FileCoverageMetrics } from './types'; + +/** + * Stateless handler for processing coverage payloads and creating coverage objects. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestCoverageHandler { + /** + * Process coverage payload + * Pure function - returns coverage data without storing it + */ + public processCoverage(payload: CoveragePayload, runInstance: TestRun): Map { + const detailedCoverageMap = new Map(); + + if (payload.result === undefined) { + return detailedCoverageMap; + } + + for (const [key, value] of Object.entries(payload.result)) { + const fileNameStr = key; + const fileCoverageMetrics: FileCoverageMetrics = value; + + // Create FileCoverage object and add to run instance + const fileCoverage = this.createFileCoverage(Uri.file(fileNameStr), fileCoverageMetrics); + runInstance.addCoverage(fileCoverage); + + // Create detailed coverage array for this file + const detailedCoverage = this.createDetailedCoverage( + fileCoverageMetrics.lines_covered ?? [], + fileCoverageMetrics.lines_missed ?? [], + ); + detailedCoverageMap.set(Uri.file(fileNameStr).fsPath, detailedCoverage); + } + + return detailedCoverageMap; + } + + /** + * Create FileCoverage object from metrics + */ + private createFileCoverage(uri: Uri, metrics: FileCoverageMetrics): FileCoverage { + const linesCovered = metrics.lines_covered ?? []; + const linesMissed = metrics.lines_missed ?? []; + const executedBranches = metrics.executed_branches; + const totalBranches = metrics.total_branches; + + const lineCoverageCount = new TestCoverageCount(linesCovered.length, linesCovered.length + linesMissed.length); + + if (totalBranches === -1) { + // branch coverage was not enabled and should not be displayed + return new FileCoverage(uri, lineCoverageCount); + } else { + const branchCoverageCount = new TestCoverageCount(executedBranches, totalBranches); + return new FileCoverage(uri, lineCoverageCount, branchCoverageCount); + } + } + + /** + * Create detailed coverage array for a file + * Only line coverage on detailed, not branch coverage + */ + private createDetailedCoverage(linesCovered: number[], linesMissed: number[]): FileCoverageDetail[] { + const detailedCoverageArray: FileCoverageDetail[] = []; + + // Add covered lines + for (const line of linesCovered) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // true value means line is covered + const statementCoverage = new StatementCoverage( + true, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + // Add missed lines + for (const line of linesMissed) { + // line is 1-indexed, so we need to subtract 1 to get the 0-indexed line number + // false value means line is NOT covered + const statementCoverage = new StatementCoverage( + false, + new Range(line - 1, 0, line - 1, Number.MAX_SAFE_INTEGER), + ); + detailedCoverageArray.push(statementCoverage); + } + + return detailedCoverageArray; + } +} diff --git a/src/client/testing/testController/common/testDiscoveryHandler.ts b/src/client/testing/testController/common/testDiscoveryHandler.ts new file mode 100644 index 000000000000..3f70e6b68594 --- /dev/null +++ b/src/client/testing/testController/common/testDiscoveryHandler.ts @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, TestController, Uri, MarkdownString } from 'vscode'; +import * as util from 'util'; +import { DiscoveredTestPayload } from './types'; +import { TestProvider } from '../../types'; +import { traceError, traceWarn } from '../../../logging'; +import { Testing } from '../../../common/utils/localize'; +import { createErrorTestItem } from './testItemUtilities'; +import { buildErrorNodeOptions, populateTestTree } from './utils'; +import { TestItemIndex } from './testItemIndex'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; + +/** + * Stateless handler for processing discovery payloads and building/updating the TestItem tree. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestDiscoveryHandler { + /** + * Process discovery payload and update test tree + * Pure function - no instance state used + */ + public processDiscovery( + payload: DiscoveredTestPayload, + testController: TestController, + testItemIndex: TestItemIndex, + workspaceUri: Uri, + testProvider: TestProvider, + token?: CancellationToken, + projectId?: string, + projectName?: string, + ): void { + if (!payload) { + // No test data is available + return; + } + + const workspacePath = workspaceUri.fsPath; + const rawTestData = payload as DiscoveredTestPayload; + + // Check if there were any errors in the discovery process. + if (rawTestData.status === 'error') { + this.createErrorNode(testController, workspaceUri, rawTestData.error, testProvider, projectId, projectName); + } else { + // remove error node only if no errors exist. + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + testController.items.delete(errorNodeId); + } + + if (rawTestData.tests || rawTestData.tests === null) { + // if any tests exist, they should be populated in the test tree, regardless of whether there were errors or not. + // parse and insert test data. + + // Clear existing mappings before rebuilding test tree + testItemIndex.clear(); + + // If the test root for this folder exists: Workspace refresh, update its children. + // Otherwise, it is a freshly discovered workspace, and we need to create a new test root and populate the test tree. + // Note: populateTestTree will call testItemIndex.registerTestItem() for each discovered test + populateTestTree( + testController, + rawTestData.tests, + undefined, + { + runIdToTestItem: testItemIndex.runIdToTestItemMap, + runIdToVSid: testItemIndex.runIdToVSidMap, + vsIdToRunId: testItemIndex.vsIdToRunIdMap, + }, + token, + projectId, + projectName, + ); + } + } + + /** + * Create an error node for discovery failures + */ + public createErrorNode( + testController: TestController, + workspaceUri: Uri, + error: string[] | undefined, + testProvider: TestProvider, + projectId?: string, + projectName?: string, + ): void { + const workspacePath = workspaceUri.fsPath; + const testingErrorConst = + testProvider === 'pytest' ? Testing.errorPytestDiscovery : Testing.errorUnittestDiscovery; + + traceError(testingErrorConst, 'for workspace: ', workspacePath, '\r\n', error?.join('\r\n\r\n') ?? ''); + + // For unittest in project-based mode, check if the error might be caused by nested project imports + // This helps users understand that import errors from nested projects can be safely ignored + // if those tests are covered by a different project with the correct environment. + if (testProvider === 'unittest' && projectId) { + const errorText = error?.join(' ') ?? ''; + const isImportError = + errorText.includes('ModuleNotFoundError') || + errorText.includes('ImportError') || + errorText.includes('No module named'); + + if (isImportError) { + const warningMessage = + '--- ' + + `[test-by-project] Import error during unittest discovery for project at ${workspacePath}. ` + + 'This may be caused by test files in nested project directories that require different dependencies. ' + + 'If these tests are discovered successfully by their own project (with the correct Python environment), ' + + 'this error can be safely ignored. To avoid this, consider excluding nested project paths from parent project discovery. ' + + '---'; + traceWarn(warningMessage); + } + } + + const errorNodeId = projectId + ? `${projectId}${PROJECT_ID_SEPARATOR}DiscoveryError:${workspacePath}` + : `DiscoveryError:${workspacePath}`; + let errorNode = testController.items.get(errorNodeId); + const message = util.format( + `${testingErrorConst} ${Testing.seePythonOutput}\r\n`, + error?.join('\r\n\r\n') ?? '', + ); + + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider, projectName); + // Update the error node ID to include project scope if applicable + options.id = errorNodeId; + errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + } + + const errorNodeLabel: MarkdownString = new MarkdownString( + `[Show output](command:python.viewOutput) to view error logs`, + ); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } +} diff --git a/src/client/testing/testController/common/testExecutionHandler.ts b/src/client/testing/testController/common/testExecutionHandler.ts new file mode 100644 index 000000000000..127e6980ae46 --- /dev/null +++ b/src/client/testing/testController/common/testExecutionHandler.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestRun, TestMessage, Location } from 'vscode'; +import { ExecutionTestPayload } from './types'; +import { TestItemIndex } from './testItemIndex'; +import { splitLines } from '../../../common/stringUtils'; +import { splitTestNameWithRegex } from './utils'; +import { clearAllChildren } from './testItemUtilities'; + +/** + * Stateless handler for processing execution payloads and updating TestRun instances. + * This handler is shared across all workspaces and contains no instance state. + */ +export class TestExecutionHandler { + /** + * Process execution payload and update test run + * Pure function - no instance state used + */ + public processExecution( + payload: ExecutionTestPayload, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTestExecData = payload as ExecutionTestPayload; + + if (rawTestExecData !== undefined && rawTestExecData.result !== undefined) { + for (const keyTemp of Object.keys(rawTestExecData.result)) { + const testItem = rawTestExecData.result[keyTemp]; + + // Delegate to specific outcome handlers + this.handleTestOutcome(keyTemp, testItem, runInstance, testItemIndex, testController); + } + } + } + + /** + * Handle a single test result based on outcome + */ + private handleTestOutcome( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + if (testItem.outcome === 'error') { + this.handleTestError(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'failure' || testItem.outcome === 'passed-unexpected') { + this.handleTestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'success' || testItem.outcome === 'expected-failure') { + this.handleTestSuccess(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'skipped') { + this.handleTestSkipped(runId, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-failure') { + this.handleSubtestFailure(runId, testItem, runInstance, testItemIndex, testController); + } else if (testItem.outcome === 'subtest-success') { + this.handleSubtestSuccess(runId, runInstance, testItemIndex, testController); + } + } + + /** + * Handle test items that errored during execution + */ + private handleTestError( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + const text = `${testItem.test} failed with error: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.errored(foundItem, message); + } + } + + /** + * Handle test items that failed during execution + */ + private handleTestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const rawTraceback = testItem.traceback ?? ''; + const traceback = splitLines(rawTraceback, { + trim: false, + removeEmptyEntries: true, + }).join('\r\n'); + + const text = `${testItem.test} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + const message = new TestMessage(text); + + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem?.uri) { + if (foundItem.range) { + message.location = new Location(foundItem.uri, foundItem.range); + } + runInstance.failed(foundItem, message); + } + } + + /** + * Handle test items that passed during execution + */ + private handleTestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.passed(foundItem); + } + } + + /** + * Handle test items that were skipped during execution + */ + private handleTestSkipped( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const foundItem = testItemIndex.getTestItem(runId, testController); + + if (foundItem !== undefined && foundItem.uri) { + runInstance.skipped(foundItem); + } + } + + /** + * Handle subtest failures + */ + private handleSubtestFailure( + runId: string, + testItem: any, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.failed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { + failed: 1, + passed: 0, + }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + const traceback = testItem.traceback ?? ''; + const text = `${testItem.subtest} failed: ${testItem.message ?? testItem.outcome}\r\n${traceback}`; + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + const message = new TestMessage(text); + if (parentTestItem.uri && parentTestItem.range) { + message.location = new Location(parentTestItem.uri, parentTestItem.range); + } + runInstance.failed(subTestItem, message); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } + + /** + * Handle subtest successes + */ + private handleSubtestSuccess( + runId: string, + runInstance: TestRun, + testItemIndex: TestItemIndex, + testController: TestController, + ): void { + const [parentTestCaseId, subtestId] = splitTestNameWithRegex(runId); + const parentTestItem = testItemIndex.getTestItem(parentTestCaseId, testController); + + if (parentTestItem) { + const stats = testItemIndex.getSubtestStats(parentTestCaseId); + if (stats) { + stats.passed += 1; + } else { + testItemIndex.setSubtestStats(parentTestCaseId, { failed: 0, passed: 1 }); + clearAllChildren(parentTestItem); + } + + const subTestItem = testController?.createTestItem(subtestId, subtestId, parentTestItem.uri); + + if (subTestItem) { + parentTestItem.children.add(subTestItem); + runInstance.started(subTestItem); + runInstance.passed(subTestItem); + } else { + throw new Error('Unable to create new child node for subtest'); + } + } else { + throw new Error('Parent test item not found'); + } + } +} diff --git a/src/client/testing/testController/common/testItemIndex.ts b/src/client/testing/testController/common/testItemIndex.ts new file mode 100644 index 000000000000..448903eae7d5 --- /dev/null +++ b/src/client/testing/testController/common/testItemIndex.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem } from 'vscode'; +import { traceError, traceVerbose } from '../../../logging'; +import { getTestCaseNodes } from './testItemUtilities'; + +export interface SubtestStats { + passed: number; + failed: number; +} + +/** + * Maintains persistent ID mappings between Python test IDs and VS Code TestItems. + * This is a stateful component that bridges discovery and execution phases. + * + * Lifecycle: + * - Created: When PythonResultResolver is instantiated (during workspace activation) + * - Populated: During discovery - each discovered test registers its mappings + * - Queried: During execution - to look up TestItems by Python run ID + * - Cleared: When discovery runs again (fresh start) or workspace is disposed + * - Cleaned: Periodically to remove stale references to deleted tests + */ +export class TestItemIndex { + // THE STATE - these maps persist across discovery and execution + private runIdToTestItem: Map; + private runIdToVSid: Map; + private vsIdToRunId: Map; + private subtestStatsMap: Map; + + constructor() { + this.runIdToTestItem = new Map(); + this.runIdToVSid = new Map(); + this.vsIdToRunId = new Map(); + this.subtestStatsMap = new Map(); + } + + /** + * Register a test item with its Python run ID and VS Code ID + * Called during DISCOVERY to populate the index + */ + public registerTestItem(runId: string, vsId: string, testItem: TestItem): void { + this.runIdToTestItem.set(runId, testItem); + this.runIdToVSid.set(runId, vsId); + this.vsIdToRunId.set(vsId, runId); + } + + /** + * Get TestItem by Python run ID (with validation and fallback strategies) + * Called during EXECUTION to look up tests + * + * Uses a three-tier approach: + * 1. Direct O(1) lookup in runIdToTestItem map + * 2. If stale, try vsId mapping and search by VS Code ID + * 3. Last resort: full tree search + */ + public getTestItem(runId: string, testController: TestController): TestItem | undefined { + // Try direct O(1) lookup first + const directItem = this.runIdToTestItem.get(runId); + if (directItem) { + // Validate the item is still in the test tree + if (this.isTestItemValid(directItem, testController)) { + return directItem; + } else { + // Clean up stale reference + this.runIdToTestItem.delete(runId); + } + } + + // Try vsId mapping as fallback + const vsId = this.runIdToVSid.get(runId); + if (vsId) { + // Search by VS Code ID in the controller + let foundItem: TestItem | undefined; + testController.items.forEach((item) => { + if (item.id === vsId) { + foundItem = item; + return; + } + if (!foundItem) { + item.children.forEach((child) => { + if (child.id === vsId) { + foundItem = child; + } + }); + } + }); + + if (foundItem) { + // Cache for future lookups + this.runIdToTestItem.set(runId, foundItem); + return foundItem; + } else { + // Clean up stale mapping + this.runIdToVSid.delete(runId); + this.vsIdToRunId.delete(vsId); + } + } + + // Last resort: full tree search + traceError(`Falling back to tree search for test: ${runId}`); + const testCases = this.collectAllTestCases(testController); + return testCases.find((item) => item.id === vsId); + } + + /** + * Get Python run ID from VS Code ID + * Called by WorkspaceTestAdapter.executeTests() to convert selected tests to Python IDs + */ + public getRunId(vsId: string): string | undefined { + return this.vsIdToRunId.get(vsId); + } + + /** + * Get VS Code ID from Python run ID + */ + public getVSId(runId: string): string | undefined { + return this.runIdToVSid.get(runId); + } + + /** + * Check if a TestItem reference is still valid in the tree + * + * Time Complexity: O(depth) where depth is the maximum nesting level of the test tree. + * In most cases this is O(1) to O(3) since test trees are typically shallow. + */ + public isTestItemValid(testItem: TestItem, testController: TestController): boolean { + // Simple validation: check if the item's parent chain leads back to the controller + let current: TestItem | undefined = testItem; + while (current?.parent) { + current = current.parent; + } + + // If we reached a root item, check if it's in the controller + if (current) { + return testController.items.get(current.id) === current; + } + + // If no parent chain, check if it's directly in the controller + return testController.items.get(testItem.id) === testItem; + } + + /** + * Get subtest statistics for a parent test case + * Returns undefined if no stats exist yet for this parent + */ + public getSubtestStats(parentId: string): SubtestStats | undefined { + return this.subtestStatsMap.get(parentId); + } + + /** + * Set subtest statistics for a parent test case + */ + public setSubtestStats(parentId: string, stats: SubtestStats): void { + this.subtestStatsMap.set(parentId, stats); + } + + /** + * Remove all mappings + * Called at the start of discovery to ensure clean state + */ + public clear(): void { + this.runIdToTestItem.clear(); + this.runIdToVSid.clear(); + this.vsIdToRunId.clear(); + this.subtestStatsMap.clear(); + } + + /** + * Clean up stale references that no longer exist in the test tree + * Called after test tree modifications + */ + public cleanupStaleReferences(testController: TestController): void { + const staleRunIds: string[] = []; + + // Check all runId->TestItem mappings + this.runIdToTestItem.forEach((testItem, runId) => { + if (!this.isTestItemValid(testItem, testController)) { + staleRunIds.push(runId); + } + }); + + // Remove stale entries + staleRunIds.forEach((runId) => { + const vsId = this.runIdToVSid.get(runId); + this.runIdToTestItem.delete(runId); + this.runIdToVSid.delete(runId); + if (vsId) { + this.vsIdToRunId.delete(vsId); + } + }); + + if (staleRunIds.length > 0) { + traceVerbose(`Cleaned up ${staleRunIds.length} stale test item references`); + } + } + + /** + * Collect all test case items from the test controller tree. + * Note: This performs full tree traversal - use cached lookups when possible. + */ + private collectAllTestCases(testController: TestController): TestItem[] { + const testCases: TestItem[] = []; + + testController.items.forEach((i) => { + const tempArr: TestItem[] = getTestCaseNodes(i); + testCases.push(...tempArr); + }); + + return testCases; + } + + // Expose maps for backward compatibility (read-only access) + public get runIdToTestItemMap(): Map { + return this.runIdToTestItem; + } + + public get runIdToVSidMap(): Map { + return this.runIdToVSid; + } + + public get vsIdToRunIdMap(): Map { + return this.vsIdToRunId; + } +} diff --git a/src/client/testing/testController/common/testItemUtilities.ts b/src/client/testing/testController/common/testItemUtilities.ts new file mode 100644 index 000000000000..43624bba2527 --- /dev/null +++ b/src/client/testing/testController/common/testItemUtilities.ts @@ -0,0 +1,583 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { + TestItem, + Uri, + Range, + Position, + TestController, + TestRunResult, + TestResultState, + TestResultSnapshot, + TestItemCollection, +} from 'vscode'; +import { CancellationToken } from 'vscode-jsonrpc'; +import { asyncForEach } from '../../../common/utils/arrayUtils'; +import { traceError, traceVerbose } from '../../../logging'; +import { + RawDiscoveredTests, + RawTest, + RawTestFile, + RawTestFolder, + RawTestFunction, + RawTestSuite, + TestData, + TestDataKinds, +} from './types'; + +// Todo: Use `TestTag` when the proposed API gets into stable. +export const RunTestTag = { id: 'python-run' }; +export const DebugTestTag = { id: 'python-debug' }; + +function testItemCollectionToArray(collection: TestItemCollection): TestItem[] { + const items: TestItem[] = []; + collection.forEach((c) => { + items.push(c); + }); + return items; +} + +export function removeItemByIdFromChildren( + idToRawData: Map, + item: TestItem, + childNodeIdsToRemove: string[], +): void { + childNodeIdsToRemove.forEach((id) => { + item.children.delete(id); + idToRawData.delete(id); + }); +} + +export type ErrorTestItemOptions = { id: string; label: string; error: string }; + +export function createErrorTestItem(testController: TestController, options: ErrorTestItemOptions): TestItem { + const testItem = testController.createTestItem(options.id, options.label); + testItem.canResolveChildren = false; + testItem.error = options.error; + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +export function createWorkspaceRootTestItem( + testController: TestController, + idToRawData: Map, + options: { id: string; label: string; uri: Uri; runId: string; parentId?: string; rawId?: string }, +): TestItem { + const testItem = testController.createTestItem(options.id, options.label, options.uri); + testItem.canResolveChildren = true; + idToRawData.set(options.id, { + ...options, + rawId: options.rawId ?? options.id, + kind: TestDataKinds.Workspace, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function getParentIdFromRawParentId( + idToRawData: Map, + testRoot: string, + raw: { parentid: string }, +): string | undefined { + const parent = idToRawData.get(path.join(testRoot, raw.parentid)); + let parentId; + if (parent) { + parentId = parent.id === '.' ? testRoot : parent.id; + } + return parentId; +} + +function getRangeFromRawSource(raw: { source: string }): Range | undefined { + // We have to extract the line number from the source data. If it is available it + // saves us from running symbol script or querying language server for this info. + try { + const sourceLine = raw.source.substr(raw.source.indexOf(':') + 1); + const line = Number.parseInt(sourceLine, 10); + // Lines in raw data start at 1, vscode lines start at 0 + return new Range(new Position(line - 1, 0), new Position(line, 0)); + } catch (ex) { + // ignore + } + return undefined; +} + +export function getRunIdFromRawData(id: string): string { + // TODO: This is a temporary solution to normalize test ids. + // The current method is error prone and easy to break. When we + // re-write the test adapters we should make sure we consider this. + // This is the id that will be used to compare with the results. + const runId = id + .replace(/\.py[^\w\-]/g, '') // we want to get rid of the `.py` in file names + .replace(/[\\\:\/]/g, '.') + .replace(/\:\:/g, '.') + .replace(/\.\./g, '.'); + return runId.startsWith('.') ? runId.substr(1) : runId; +} + +function createFolderOrFileTestItem( + testController: TestController, + idToRawData: Map, + testRoot: string, + rawData: RawTestFolder | RawTestFile, +): TestItem { + const fullPath = path.join(testRoot, rawData.relpath); + const uri = Uri.file(fullPath); + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + + const label = path.basename(fullPath); + const testItem = testController.createTestItem(fullPath, label, uri); + + testItem.canResolveChildren = true; + + idToRawData.set(testItem.id, { + id: testItem.id, + rawId: rawData.id, + runId: rawData.relpath, + uri, + kind: TestDataKinds.FolderOrFile, + parentId, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function updateFolderOrFileTestItem( + item: TestItem, + idToRawData: Map, + testRoot: string, + rawData: RawTestFolder | RawTestFile, +): void { + const fullPath = path.join(testRoot, rawData.relpath); + const uri = Uri.file(fullPath); + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + + item.label = path.basename(fullPath); + + item.canResolveChildren = true; + + idToRawData.set(item.id, { + id: item.id, + rawId: rawData.id, + runId: rawData.relpath, + uri, + kind: TestDataKinds.FolderOrFile, + parentId, + }); + item.tags = [RunTestTag, DebugTestTag]; +} + +function createCollectionTestItem( + testController: TestController, + idToRawData: Map, + testRoot: string, + rawData: RawTestSuite | RawTestFunction, +): TestItem { + // id can look like test_something.py::SomeClass + const id = path.join(testRoot, rawData.id); + + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.id.substr(0, rawData.id.indexOf(':'))); + const uri = Uri.file(documentPath); + + const label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + const testItem = testController.createTestItem(id, label, uri); + + testItem.canResolveChildren = true; + + idToRawData.set(testItem.id, { + id: testItem.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Collection, + parentId, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function updateCollectionTestItem( + item: TestItem, + idToRawData: Map, + testRoot: string, + rawData: RawTestSuite | RawTestFunction, +): void { + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.id.substr(0, rawData.id.indexOf(':'))); + const uri = Uri.file(documentPath); + + item.label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + item.canResolveChildren = true; + + idToRawData.set(item.id, { + id: item.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Collection, + parentId, + }); + item.tags = [RunTestTag, DebugTestTag]; +} + +function createTestCaseItem( + testController: TestController, + idToRawData: Map, + testRoot: string, + rawData: RawTest, +): TestItem { + // id can look like: + // test_something.py::SomeClass::someTest + // test_something.py::SomeClass::someTest[x1] + const id = path.join(testRoot, rawData.id); + + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.source.substr(0, rawData.source.indexOf(':'))); + const uri = Uri.file(documentPath); + + const label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + const testItem = testController.createTestItem(id, label, uri); + + testItem.canResolveChildren = false; + testItem.range = getRangeFromRawSource(rawData); + + idToRawData.set(testItem.id, { + id: testItem.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Case, + parentId, + }); + testItem.tags = [RunTestTag, DebugTestTag]; + return testItem; +} + +function updateTestCaseItem( + item: TestItem, + idToRawData: Map, + testRoot: string, + rawData: RawTest, +): void { + // We need the actual document path so we can set the location for the tests. This will be + // used to provide test result status next to the tests. + const documentPath = path.join(testRoot, rawData.source.substr(0, rawData.source.indexOf(':'))); + const uri = Uri.file(documentPath); + + item.label = rawData.name; + + const parentId = getParentIdFromRawParentId(idToRawData, testRoot, rawData); + const runId = getRunIdFromRawData(rawData.id); + + item.canResolveChildren = false; + item.range = getRangeFromRawSource(rawData); + + idToRawData.set(item.id, { + id: item.id, + rawId: rawData.id, + runId, + uri, + kind: TestDataKinds.Case, + parentId, + }); + item.tags = [RunTestTag, DebugTestTag]; +} + +async function updateTestItemFromRawDataInternal( + item: TestItem, + testController: TestController, + idToRawData: Map, + testRoot: string, + rawDataSet: RawDiscoveredTests[], + token?: CancellationToken, +): Promise { + if (token?.isCancellationRequested) { + return; + } + + const rawId = idToRawData.get(item.id)?.rawId; + if (!rawId) { + traceError(`Unknown node id: ${item.id}`); + return; + } + + const nodeRawData = rawDataSet.filter( + (r) => + r.root === rawId || + r.rootid === rawId || + r.parents.find((p) => p.id === rawId) || + r.tests.find((t) => t.id === rawId), + ); + + if (nodeRawData.length === 0 && item.parent) { + removeItemByIdFromChildren(idToRawData, item.parent, [item.id]); + traceVerbose(`Following test item was removed Reason: No-Raw-Data ${item.id}`); + return; + } + + if (nodeRawData.length > 1) { + // Something is wrong, there can only be one test node with that id + traceError(`Multiple (${nodeRawData.length}) raw data nodes had the same id: ${rawId}`); + return; + } + + if (rawId === nodeRawData[0].root || rawId === nodeRawData[0].rootid) { + // This is a test root node, we need to update the entire tree + // The update children and remove any child that does not have raw data. + + await asyncForEach(testItemCollectionToArray(item.children), async (c) => { + await updateTestItemFromRawData(c, testController, idToRawData, testRoot, nodeRawData, token); + }); + + // Create child nodes that are new. + // We only need to look at rawData.parents. Since at this level we either have folder or file. + const rawChildNodes = nodeRawData[0].parents.filter((p) => p.parentid === '.' || p.parentid === rawId); + const existingNodes: string[] = []; + item.children.forEach((c) => existingNodes.push(idToRawData.get(c.id)?.rawId ?? '')); + + await asyncForEach( + rawChildNodes.filter((r) => !existingNodes.includes(r.id)), + async (r) => { + const childItem = + r.kind === 'file' + ? createFolderOrFileTestItem(testController, idToRawData, testRoot, r as RawTestFile) + : createFolderOrFileTestItem(testController, idToRawData, testRoot, r as RawTestFolder); + item.children.add(childItem); + await updateTestItemFromRawData(childItem, testController, idToRawData, testRoot, nodeRawData, token); + }, + ); + + return; + } + + // First check if this is a parent node + const rawData = nodeRawData[0].parents.filter((r) => r.id === rawId); + if (rawData.length === 1) { + // This is either a File/Folder/Collection node + + // Update the node data + switch (rawData[0].kind) { + case 'file': + updateFolderOrFileTestItem(item, idToRawData, testRoot, rawData[0] as RawTestFile); + break; + case 'folder': + updateFolderOrFileTestItem(item, idToRawData, testRoot, rawData[0] as RawTestFolder); + break; + case 'suite': + updateCollectionTestItem(item, idToRawData, testRoot, rawData[0] as RawTestSuite); + break; + case 'function': + updateCollectionTestItem(item, idToRawData, testRoot, rawData[0] as RawTestFunction); + break; + default: + break; + } + + // The update children and remove any child that does not have raw data. + await asyncForEach(testItemCollectionToArray(item.children), async (c) => { + await updateTestItemFromRawData(c, testController, idToRawData, testRoot, nodeRawData, token); + }); + + // Create child nodes that are new. + // Get the existing child node ids so we can skip them + const existingNodes: string[] = []; + item.children.forEach((c) => existingNodes.push(idToRawData.get(c.id)?.rawId ?? '')); + + // We first look at rawData.parents. Since at this level we either have folder or file. + // The current node is potentially a parent of one of these "parent" nodes or it is a parent + // of test case nodes. We will handle Test case nodes after handling parents. + const rawChildNodes = nodeRawData[0].parents.filter((p) => p.parentid === rawId); + await asyncForEach( + rawChildNodes.filter((r) => !existingNodes.includes(r.id)), + async (r) => { + let childItem; + switch (r.kind) { + case 'file': + childItem = createFolderOrFileTestItem(testController, idToRawData, testRoot, r as RawTestFile); + break; + case 'folder': + childItem = createFolderOrFileTestItem( + testController, + idToRawData, + testRoot, + r as RawTestFolder, + ); + break; + case 'suite': + childItem = createCollectionTestItem(testController, idToRawData, testRoot, r as RawTestSuite); + break; + case 'function': + childItem = createCollectionTestItem( + testController, + idToRawData, + testRoot, + r as RawTestFunction, + ); + break; + default: + break; + } + if (childItem) { + item.children.add(childItem); + // This node can potentially have children. So treat it like a new node and update it. + await updateTestItemFromRawData( + childItem, + testController, + idToRawData, + testRoot, + nodeRawData, + token, + ); + } + }, + ); + + // Now we will look at test case nodes. Create any test case node that does not already exist. + const rawTestCaseNodes = nodeRawData[0].tests.filter((p) => p.parentid === rawId); + rawTestCaseNodes + .filter((r) => !existingNodes.includes(r.id)) + .forEach((r) => { + const childItem = createTestCaseItem(testController, idToRawData, testRoot, r); + item.children.add(childItem); + }); + + return; + } + + if (rawData.length > 1) { + // Something is wrong, there can only be one test node with that id + traceError(`Multiple (${rawData.length}) raw data nodes had the same id: ${rawId}`); + return; + } + + // We are here this means rawData.length === 0 + // The node is probably is test case node. Try and find it. + const rawCaseData = nodeRawData[0].tests.filter((r) => r.id === rawId); + + if (rawCaseData.length === 1) { + // This is a test case node + updateTestCaseItem(item, idToRawData, testRoot, rawCaseData[0]); + return; + } + + if (rawCaseData.length > 1) { + // Something is wrong, there can only be one test node with that id + traceError(`Multiple (${rawCaseData.length}) raw data nodes had the same id: ${rawId}`); + } +} + +export async function updateTestItemFromRawData( + item: TestItem, + testController: TestController, + idToRawData: Map, + testRoot: string, + rawDataSet: RawDiscoveredTests[], + token?: CancellationToken, +): Promise { + item.busy = true; + await updateTestItemFromRawDataInternal(item, testController, idToRawData, testRoot, rawDataSet, token); + item.busy = false; +} + +export function getTestCaseNodes(testNode: TestItem, collection: TestItem[] = []): TestItem[] { + if (!testNode.canResolveChildren && testNode.tags.length > 0) { + collection.push(testNode); + } + + testNode.children.forEach((c) => { + if (testNode.canResolveChildren) { + getTestCaseNodes(c, collection); + } else { + collection.push(testNode); + } + }); + return collection; +} + +export function getWorkspaceNode(testNode: TestItem, idToRawData: Map): TestItem | undefined { + const raw = idToRawData.get(testNode.id); + if (raw) { + if (raw.kind === TestDataKinds.Workspace) { + return testNode; + } + if (testNode.parent) { + return getWorkspaceNode(testNode.parent, idToRawData); + } + } + return undefined; +} + +export function getNodeByUri(root: TestItem, uri: Uri): TestItem | undefined { + if (root.uri?.fsPath === uri.fsPath) { + return root; + } + + const nodes: TestItem[] = []; + root.children.forEach((c) => nodes.push(c)); + + // Search at the current level + for (const node of nodes) { + if (node.uri?.fsPath === uri.fsPath) { + return node; + } + } + + // Search the children of the current level + for (const node of nodes) { + const found = getNodeByUri(node, uri); + if (found) { + return found; + } + } + return undefined; +} + +function updateTestResultMapForSnapshot(resultMap: Map, snapshot: TestResultSnapshot) { + for (const taskState of snapshot.taskStates) { + resultMap.set(snapshot.id, taskState.state); + } + snapshot.children.forEach((child) => updateTestResultMapForSnapshot(resultMap, child)); +} + +export function updateTestResultMap( + resultMap: Map, + testResults: readonly TestRunResult[], +): void { + const ordered = new Array(...testResults).sort((a, b) => a.completedAt - b.completedAt); + ordered.forEach((testResult) => { + testResult.results.forEach((snapshot) => updateTestResultMapForSnapshot(resultMap, snapshot)); + }); +} + +export function checkForFailedTests(resultMap: Map): boolean { + return ( + Array.from(resultMap.values()).find( + (state) => state === TestResultState.Failed || state === TestResultState.Errored, + ) !== undefined + ); +} + +export function clearAllChildren(testNode: TestItem): void { + const ids: string[] = []; + testNode.children.forEach((c) => ids.push(c.id)); + ids.forEach(testNode.children.delete); +} diff --git a/src/client/testing/testController/common/testProjectRegistry.ts b/src/client/testing/testController/common/testProjectRegistry.ts new file mode 100644 index 000000000000..4f0702ad584c --- /dev/null +++ b/src/client/testing/testController/common/testProjectRegistry.ts @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { TestController, Uri } from 'vscode'; +import { isParentPath } from '../../../common/platform/fs-paths'; +import { IConfigurationService } from '../../../common/types'; +import { IInterpreterService } from '../../../interpreter/contracts'; +import { traceError, traceInfo } from '../../../logging'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import { TestProvider } from '../../types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonProject, PythonEnvironment } from '../../../envExt/types'; +import { getEnvExtApi, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from './projectAdapter'; +import { getProjectId, createProjectDisplayName, createTestAdapters } from './projectUtils'; +import { PythonResultResolver } from './resultResolver'; + +/** + * Registry for Python test projects within workspaces. + * + * Manages the lifecycle of test projects including: + * - Discovering Python projects via Python Environments API + * - Creating and storing ProjectAdapter instances per workspace + * - Computing nested project relationships for ignore lists + * - Fallback to default "legacy" project when API unavailable + * + * **Key concepts:** + * - **Workspace:** A VS Code workspace folder (may contain multiple projects) + * - **Project:** A Python project within a workspace (identified by pyproject.toml, setup.py, etc.) + * - **ProjectUri:** The unique identifier for a project (the URI of the project root directory) + * - Each project gets its own test tree root, Python environment, and test adapters + * + * **Project identification:** + * Projects are identified and tracked by their URI (projectUri.toString()). This matches + * how the Python Environments extension stores projects in its Map. + */ +export class TestProjectRegistry { + /** + * Map of workspace URI -> Map of project URI string -> ProjectAdapter + * + * Projects are keyed by their URI string (projectUri.toString()) which matches how + * the Python Environments extension identifies projects. This enables O(1) lookups + * when given a project URI. + */ + private readonly workspaceProjects: Map> = new Map(); + + constructor( + private readonly testController: TestController, + private readonly configSettings: IConfigurationService, + private readonly interpreterService: IInterpreterService, + private readonly envVarsService: IEnvironmentVariablesProvider, + ) {} + + /** + * Gets the projects map for a workspace, if it exists. + */ + public getWorkspaceProjects(workspaceUri: Uri): Map | undefined { + return this.workspaceProjects.get(workspaceUri); + } + + /** + * Checks if a workspace has been initialized with projects. + */ + public hasProjects(workspaceUri: Uri): boolean { + return this.workspaceProjects.has(workspaceUri); + } + + /** + * Gets all projects for a workspace as an array. + */ + public getProjectsArray(workspaceUri: Uri): ProjectAdapter[] { + const projectsMap = this.workspaceProjects.get(workspaceUri); + return projectsMap ? Array.from(projectsMap.values()) : []; + } + + /** + * Discovers and registers all Python projects for a workspace. + * Returns the discovered projects for the caller to use. + */ + public async discoverAndRegisterProjects(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Discovering projects for workspace: ${workspaceUri.fsPath}`); + + const projects = await this.discoverProjects(workspaceUri); + + // Create map for this workspace, keyed by project URI + const projectsMap = new Map(); + projects.forEach((project) => { + projectsMap.set(getProjectId(project.projectUri), project); + }); + + this.workspaceProjects.set(workspaceUri, projectsMap); + traceInfo(`[test-by-project] Registered ${projects.length} project(s) for ${workspaceUri.fsPath}`); + + return projects; + } + + /** + * Computes and populates nested project ignore lists for all projects in a workspace. + * Must be called before discovery to ensure parent projects ignore nested children. + */ + public configureNestedProjectIgnores(workspaceUri: Uri): void { + const projectIgnores = this.computeNestedProjectIgnores(workspaceUri); + const projects = this.getProjectsArray(workspaceUri); + + for (const project of projects) { + const ignorePaths = projectIgnores.get(getProjectId(project.projectUri)); + if (ignorePaths && ignorePaths.length > 0) { + project.nestedProjectPathsToIgnore = ignorePaths; + traceInfo(`[test-by-project] ${project.projectName} will ignore nested: ${ignorePaths.join(', ')}`); + } + } + } + + /** + * Clears all projects for a workspace. + */ + public clearWorkspace(workspaceUri: Uri): void { + this.workspaceProjects.delete(workspaceUri); + } + + // ====== Private Methods ====== + + /** + * Discovers Python projects in a workspace using the Python Environment API. + * Falls back to creating a single default project if API is unavailable. + */ + private async discoverProjects(workspaceUri: Uri): Promise { + try { + if (!useEnvExtension()) { + traceInfo('[test-by-project] Python Environments API not available, using default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + const envExtApi = await getEnvExtApi(); + const allProjects = envExtApi.getPythonProjects(); + traceInfo(`[test-by-project] Found ${allProjects.length} total Python projects from API`); + + // Filter to projects within this workspace + const workspaceProjects = allProjects.filter((project) => + isParentPath(project.uri.fsPath, workspaceUri.fsPath), + ); + traceInfo(`[test-by-project] Filtered to ${workspaceProjects.length} projects in workspace`); + + if (workspaceProjects.length === 0) { + traceInfo('[test-by-project] No projects found, creating default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + // Create ProjectAdapter for each discovered project + const adapters: ProjectAdapter[] = []; + for (const pythonProject of workspaceProjects) { + try { + const adapter = await this.createProjectAdapter(pythonProject, workspaceUri); + adapters.push(adapter); + } catch (error) { + traceError(`[test-by-project] Failed to create adapter for ${pythonProject.uri.fsPath}:`, error); + } + } + + if (adapters.length === 0) { + traceInfo('[test-by-project] All adapters failed, falling back to default project'); + return [await this.createDefaultProject(workspaceUri)]; + } + + return adapters; + } catch (error) { + traceError('[test-by-project] Discovery failed, using default project:', error); + return [await this.createDefaultProject(workspaceUri)]; + } + } + + /** + * Creates a ProjectAdapter from a PythonProject. + * + * Each project gets its own isolated test infrastructure: + * - **ResultResolver:** Handles mapping test IDs and processing results for this project + * - **DiscoveryAdapter:** Discovers tests scoped to this project's root directory + * - **ExecutionAdapter:** Runs tests for this project using its Python environment + * + */ + private async createProjectAdapter(pythonProject: PythonProject, workspaceUri: Uri): Promise { + const projectId = getProjectId(pythonProject.uri); + traceInfo(`[test-by-project] Creating adapter for: ${pythonProject.name} at ${projectId}`); + + // Resolve Python environment + const envExtApi = await getEnvExtApi(); + const pythonEnvironment = await envExtApi.getEnvironment(pythonProject.uri); + if (!pythonEnvironment) { + throw new Error(`No Python environment found for project ${projectId}`); + } + + // Create test infrastructure + const testProvider = this.getTestProvider(workspaceUri); + const projectDisplayName = createProjectDisplayName(pythonProject.name, pythonEnvironment.version); + const resultResolver = new PythonResultResolver( + this.testController, + testProvider, + workspaceUri, + projectId, + pythonProject.name, // Use simple project name for test tree label (without version) + ); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + return { + projectName: projectDisplayName, + projectUri: pythonProject.uri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Creates a default project for legacy/fallback mode. + */ + private async createDefaultProject(workspaceUri: Uri): Promise { + traceInfo(`[test-by-project] Creating default project for: ${workspaceUri.fsPath}`); + + const testProvider = this.getTestProvider(workspaceUri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspaceUri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const interpreter = await this.interpreterService.getActiveInterpreter(workspaceUri); + + const pythonEnvironment: PythonEnvironment = { + name: 'default', + displayName: interpreter?.displayName || 'Python', + shortDisplayName: interpreter?.displayName || 'Python', + displayPath: interpreter?.path || 'python', + version: interpreter?.version?.raw || '3.x', + environmentPath: Uri.file(interpreter?.path || 'python'), + sysPrefix: interpreter?.sysPrefix || '', + execInfo: { run: { executable: interpreter?.path || 'python' } }, + envId: { id: 'default', managerId: 'default' }, + }; + + const pythonProject: PythonProject = { + name: path.basename(workspaceUri.fsPath) || 'workspace', + uri: workspaceUri, + }; + + return { + projectName: pythonProject.name, + projectUri: workspaceUri, + workspaceUri, + pythonProject, + pythonEnvironment, + testProvider, + discoveryAdapter, + executionAdapter, + resultResolver, + isDiscovering: false, + isExecuting: false, + }; + } + + /** + * Identifies nested projects and returns ignore paths for parent projects. + * + * **Time complexity:** O(n²) where n is the number of projects in the workspace. + * For each project, checks all other projects to find nested relationships. + * + * Note: Uses path.normalize() to handle Windows path separator inconsistencies + * (e.g., paths from URI.fsPath may have mixed separators). + */ + private computeNestedProjectIgnores(workspaceUri: Uri): Map { + const ignoreMap = new Map(); + const projects = this.getProjectsArray(workspaceUri); + + if (projects.length === 0) return ignoreMap; + + for (const parent of projects) { + const nestedPaths: string[] = []; + + for (const child of projects) { + // Skip self-comparison using URI + if (parent.projectUri.toString() === child.projectUri.toString()) continue; + + // Normalize paths to handle Windows path separator inconsistencies + const parentNormalized = path.normalize(parent.projectUri.fsPath); + const childNormalized = path.normalize(child.projectUri.fsPath); + + // Add trailing separator to ensure we match directory boundaries + const parentWithSep = parentNormalized.endsWith(path.sep) + ? parentNormalized + : parentNormalized + path.sep; + const childWithSep = childNormalized.endsWith(path.sep) ? childNormalized : childNormalized + path.sep; + + // Check if child is inside parent (case-insensitive for Windows) + const childIsInsideParent = childWithSep.toLowerCase().startsWith(parentWithSep.toLowerCase()); + + if (childIsInsideParent) { + nestedPaths.push(child.projectUri.fsPath); + traceInfo(`[test-by-project] Nested: ${child.projectName} is inside ${parent.projectName}`); + } + } + + if (nestedPaths.length > 0) { + ignoreMap.set(getProjectId(parent.projectUri), nestedPaths); + } + } + + return ignoreMap; + } + + /** + * Determines the test provider based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : 'pytest'; + } +} diff --git a/src/client/testing/testController/common/types.ts b/src/client/testing/testController/common/types.ts new file mode 100644 index 000000000000..017c41cf3d97 --- /dev/null +++ b/src/client/testing/testController/common/types.ts @@ -0,0 +1,251 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { + CancellationToken, + Event, + FileCoverageDetail, + OutputChannel, + TestController, + TestItem, + TestRun, + TestRunProfileKind, + Uri, + WorkspaceFolder, +} from 'vscode'; +import { ITestDebugLauncher } from '../../common/types'; +import { IPythonExecutionFactory } from '../../../common/process/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from './projectAdapter'; + +export enum TestDataKinds { + Workspace, + FolderOrFile, + Collection, + Case, +} + +export interface TestData { + rawId: string; + runId: string; + id: string; + uri: Uri; + parentId?: string; + kind: TestDataKinds; +} + +export type TestRefreshOptions = { forceRefresh: boolean }; + +export const ITestController = Symbol('ITestController'); +export interface ITestController { + refreshTestData(resource?: Uri, options?: TestRefreshOptions): Promise; + stopRefreshing(): void; + onRefreshingCompleted: Event; + onRefreshingStarted: Event; + onRunWithoutConfiguration: Event; +} + +export const ITestFrameworkController = Symbol('ITestFrameworkController'); +export interface ITestFrameworkController { + resolveChildren(testController: TestController, item: TestItem, token?: CancellationToken): Promise; +} + +export const ITestsRunner = Symbol('ITestsRunner'); +export interface ITestsRunner {} + +// We expose these here as a convenience and to cut down on churn +// elsewhere in the code. +type RawTestNode = { + id: string; + name: string; + parentid: string; +}; +export type RawTestParent = RawTestNode & { + kind: 'folder' | 'file' | 'suite' | 'function' | 'workspace'; +}; +type RawTestFSNode = RawTestParent & { + kind: 'folder' | 'file'; + relpath: string; +}; +export type RawTestFolder = RawTestFSNode & { + kind: 'folder'; +}; +export type RawTestFile = RawTestFSNode & { + kind: 'file'; +}; +export type RawTestSuite = RawTestParent & { + kind: 'suite'; +}; +// function-as-a-container is for parameterized ("sub") tests. +export type RawTestFunction = RawTestParent & { + kind: 'function'; +}; +export type RawTest = RawTestNode & { + source: string; +}; +export type RawDiscoveredTests = { + rootid: string; + root: string; + parents: RawTestParent[]; + tests: RawTest[]; +}; + +// New test discovery adapter types + +export type DataReceivedEvent = { + uuid: string; + data: string; +}; + +export type TestDiscoveryCommand = { + script: string; + args: string[]; +}; + +export type TestExecutionCommand = { + script: string; + args: string[]; +}; + +export type TestCommandOptions = { + workspaceFolder: Uri; + cwd: string; + command: TestDiscoveryCommand | TestExecutionCommand; + token?: CancellationToken; + outChannel?: OutputChannel; + profileKind?: TestRunProfileKind; + testIds?: string[]; +}; + +// /** +// * Interface describing the server that will send test commands to the Python side, and process responses. +// * +// * Consumers will call sendCommand in order to execute Python-related code, +// * and will subscribe to the onDataReceived event to wait for the results. +// */ +// export interface ITestServer { +// readonly onDataReceived: Event; +// readonly onRunDataReceived: Event; +// readonly onDiscoveryDataReceived: Event; +// sendCommand( +// options: TestCommandOptions, +// env: EnvironmentVariables, +// runTestIdsPort?: string, +// runInstance?: TestRun, +// testIds?: string[], +// callback?: () => void, +// executionFactory?: IPythonExecutionFactory, +// ): Promise; +// serverReady(): Promise; +// getPort(): number; +// createUUID(cwd: string): string; +// deleteUUID(uuid: string): void; +// triggerRunDataReceivedEvent(data: DataReceivedEvent): void; +// triggerDiscoveryDataReceivedEvent(data: DataReceivedEvent): void; +// } + +/** + * Test item mapping interface used by populateTestTree. + * Contains only the maps needed for building the test tree. + */ +export interface ITestItemMappings { + runIdToVSid: Map; + runIdToTestItem: Map; + vsIdToRunId: Map; +} + +export interface ITestResultResolver extends ITestItemMappings { + detailedCoverageMap: Map; + + resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; + resolveExecution(payload: ExecutionTestPayload | CoveragePayload, runInstance: TestRun): void; + _resolveDiscovery(payload: DiscoveredTestPayload, token?: CancellationToken): void; + _resolveExecution(payload: ExecutionTestPayload, runInstance: TestRun): void; + _resolveCoverage(payload: CoveragePayload, runInstance: TestRun): void; +} +export interface ITestDiscoveryAdapter { + discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise; +} + +// interface for execution/runner adapter +export interface ITestExecutionAdapter { + runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise; +} + +// Same types as in python_files/unittestadapter/utils.py +export type DiscoveredTestType = 'folder' | 'file' | 'class' | 'function' | 'test'; + +export type DiscoveredTestCommon = { + path: string; + name: string; + // Trailing underscore to avoid collision with the 'type' Python keyword. + type_: DiscoveredTestType; + id_: string; +}; + +export type DiscoveredTestItem = DiscoveredTestCommon & { + lineno: number | string; + runID: string; +}; + +export type DiscoveredTestNode = DiscoveredTestCommon & { + children: (DiscoveredTestNode | DiscoveredTestItem)[]; + lineno?: number | string; +}; + +export type DiscoveredTestPayload = { + cwd: string; + tests?: DiscoveredTestNode; + status: 'success' | 'error'; + error?: string[]; +}; + +export type CoveragePayload = { + coverage: boolean; + cwd: string; + result?: { + [filePathStr: string]: FileCoverageMetrics; + }; + error: string; +}; + +// using camel-case for these types to match the python side +export type FileCoverageMetrics = { + // eslint-disable-next-line camelcase + lines_covered: number[]; + // eslint-disable-next-line camelcase + lines_missed: number[]; + executed_branches: number; + total_branches: number; +}; + +export type ExecutionTestPayload = { + cwd: string; + status: 'success' | 'error'; + result?: { + [testRunID: string]: { + test?: string; + outcome?: string; + message?: string; + traceback?: string; + subtest?: string; + }; + }; + notFound?: string[]; + error: string; +}; diff --git a/src/client/testing/testController/common/utils.ts b/src/client/testing/testController/common/utils.ts new file mode 100644 index 000000000000..9782487d940b --- /dev/null +++ b/src/client/testing/testController/common/utils.ts @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as crypto from 'crypto'; +import { CancellationToken, Position, TestController, TestItem, Uri, Range, Disposable } from 'vscode'; +import { Message } from 'vscode-jsonrpc'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { DebugTestTag, ErrorTestItemOptions, RunTestTag } from './testItemUtilities'; +import { + DiscoveredTestItem, + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, + ITestItemMappings, +} from './types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { createReaderPipe, generateRandomPipeName } from '../../../common/pipes/namedPipes'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { PROJECT_ID_SEPARATOR } from './projectUtils'; + +export function fixLogLinesNoTrailing(content: string): string { + const lines = content.split(/\r?\n/g); + return `${lines.join('\r\n')}`; +} +export function createTestingDeferred(): Deferred { + return createDeferred(); +} + +interface ExecutionResultMessage extends Message { + params: ExecutionTestPayload; +} + +/** + * Retrieves the path to the temporary directory. + * + * On Windows, it returns the default temporary directory. + * On macOS/Linux, it prefers the `XDG_RUNTIME_DIR` environment variable if set, + * otherwise, it falls back to the default temporary directory. + * + * @returns {string} The path to the temporary directory. + */ +function getTempDir(): string { + if (process.platform === 'win32') { + return os.tmpdir(); // Default Windows behavior + } + return process.env.XDG_RUNTIME_DIR || os.tmpdir(); // Prefer XDG_RUNTIME_DIR on macOS/Linux +} + +/** + * Writes an array of test IDs to a temporary file. + * + * @param testIds - The array of test IDs to write. + * @returns A promise that resolves to the file name of the temporary file. + */ +export async function writeTestIdsFile(testIds: string[]): Promise { + // temp file name in format of test-ids-.txt + const randomSuffix = crypto.randomBytes(10).toString('hex'); + const tempName = `test-ids-${randomSuffix}.txt`; + // create temp file + let tempFileName: string; + const tempDir: string = getTempDir(); + try { + traceLog('Attempting to use temp directory for test ids file, file name:', tempName); + tempFileName = path.join(tempDir, tempName); + // attempt access to written file to check permissions + await fs.promises.access(tempDir); + } catch (error) { + // Handle the error when accessing the temp directory + traceError('Error accessing temp directory:', error, ' Attempt to use extension root dir instead'); + // Make new temp directory in extension root dir + const tempDir = path.join(EXTENSION_ROOT_DIR, '.temp'); + await fs.promises.mkdir(tempDir, { recursive: true }); + tempFileName = path.join(EXTENSION_ROOT_DIR, '.temp', tempName); + traceLog('New temp file:', tempFileName); + } + // write test ids to file + await fs.promises.writeFile(tempFileName, testIds.join('\n')); + // return file name + return tempFileName; +} + +export async function startRunResultNamedPipe( + dataReceivedCallback: (payload: ExecutionTestPayload) => void, + deferredTillServerClose: Deferred, + cancellationToken?: CancellationToken, +): Promise { + traceVerbose('Starting Test Result named pipe'); + const pipeName: string = generateRandomPipeName('python-test-results'); + + const reader = await createReaderPipe(pipeName, cancellationToken); + traceVerbose(`Test Results named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Results named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + deferredTillServerClose.resolve(); + }); + + if (cancellationToken) { + disposables.push( + cancellationToken?.onCancellationRequested(() => { + traceLog(`Test Result named pipe ${pipeName} cancelled`); + disposable.dispose(); + }), + ); + } + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Result named pipe ${pipeName} received data`); + // if EOT, call decrement connection count (callback) + dataReceivedCallback((data as ExecutionResultMessage).params as ExecutionTestPayload); + }), + reader.onClose(() => { + // this is called once the server close, once per run instance + traceVerbose(`Test Result named pipe ${pipeName} closed. Disposing of listener/s.`); + // dispose of all data listeners and cancelation listeners + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Results named pipe ${pipeName} error:`, error); + }), + ); + + return pipeName; +} + +interface DiscoveryResultMessage extends Message { + params: DiscoveredTestPayload; +} + +export async function startDiscoveryNamedPipe( + callback: (payload: DiscoveredTestPayload) => void, + cancellationToken?: CancellationToken, +): Promise { + traceVerbose('Starting Test Discovery named pipe'); + // const pipeName: string = '/Users/eleanorboyd/testingFiles/inc_dec_example/temp33.txt'; + const pipeName: string = generateRandomPipeName('python-test-discovery'); + const reader = await createReaderPipe(pipeName, cancellationToken); + + traceVerbose(`Test Discovery named pipe ${pipeName} connected`); + let disposables: Disposable[] = []; + const disposable = new Disposable(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} disposed`); + disposables.forEach((d) => d.dispose()); + disposables = []; + }); + + if (cancellationToken) { + disposables.push( + cancellationToken.onCancellationRequested(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} cancelled`); + disposable.dispose(); + }), + ); + } + + disposables.push( + reader, + reader.listen((data: Message) => { + traceVerbose(`Test Discovery named pipe ${pipeName} received data`); + callback((data as DiscoveryResultMessage).params as DiscoveredTestPayload); + }), + reader.onClose(() => { + traceVerbose(`Test Discovery named pipe ${pipeName} closed`); + disposable.dispose(); + }), + reader.onError((error) => { + traceError(`Test Discovery named pipe ${pipeName} error:`, error); + }), + ); + return pipeName; +} + +/** + * Extracts the missing module name from a ModuleNotFoundError or ImportError message. + * @param message The error message to parse + * @returns The module name if found, undefined otherwise + */ +function extractMissingModuleName(message: string): string | undefined { + // Match patterns like: + // - No module named 'requests' + // - No module named "requests" + // - ModuleNotFoundError: No module named 'requests' + // - ImportError: No module named requests + const patterns = [/No module named ['"]([^'"]+)['"]/, /No module named (\S+)/]; + + for (const pattern of patterns) { + const match = message.match(pattern); + if (match) { + return match[1]; + } + } + return undefined; +} + +export function buildErrorNodeOptions( + uri: Uri, + message: string, + testType: string, + projectName?: string, +): ErrorTestItemOptions { + let labelText = testType === 'pytest' ? 'pytest Discovery Error' : 'Unittest Discovery Error'; + let errorMessage = message; + + // Check for missing module errors and provide specific messaging + const missingModule = extractMissingModuleName(message); + if (missingModule) { + labelText = `Missing Module: ${missingModule}`; + errorMessage = `The module '${missingModule}' is not installed in the selected Python environment. Please install it to enable test discovery.`; + } + + // Use project name for label if available (project-based testing), otherwise use folder name + const displayName = projectName ?? path.basename(uri.fsPath); + + return { + id: `DiscoveryError:${uri.fsPath}`, + label: `${labelText} [${displayName}]`, + error: errorMessage, + }; +} + +export function populateTestTree( + testController: TestController, + testTreeData: DiscoveredTestNode, + testRoot: TestItem | undefined, + testItemMappings: ITestItemMappings, + token?: CancellationToken, + projectId?: string, + projectName?: string, +): void { + // If testRoot is undefined, use the info of the root item of testTreeData to create a test item, and append it to the test controller. + if (!testRoot) { + // Create project-scoped ID if projectId is provided + const rootId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${testTreeData.path}` : testTreeData.path; + // Use "Project: {name}" label for project-based testing, otherwise use folder name + const rootLabel = projectName ? `Project: ${projectName}` : testTreeData.name; + testRoot = testController.createTestItem(rootId, rootLabel, Uri.file(testTreeData.path)); + + testRoot.canResolveChildren = true; + testRoot.tags = [RunTestTag, DebugTestTag]; + + testController.items.add(testRoot); + } + + // Recursively populate the tree with test data. + testTreeData.children.forEach((child) => { + if (!token?.isCancellationRequested) { + if (isTestItem(child)) { + // Create project-scoped vsId + const vsId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + const testItem = testController.createTestItem(vsId, child.name, Uri.file(child.path)); + testItem.tags = [RunTestTag, DebugTestTag]; + + let range: Range | undefined; + if (child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + } + testItem.canResolveChildren = false; + testItem.range = range; + testItem.tags = [RunTestTag, DebugTestTag]; + + testRoot!.children.add(testItem); + // add to our map - use runID as key, vsId as value + testItemMappings.runIdToTestItem.set(child.runID, testItem); + testItemMappings.runIdToVSid.set(child.runID, vsId); + testItemMappings.vsIdToRunId.set(vsId, child.runID); + } else { + // Use project-scoped ID for non-test nodes and look up within the current root + const nodeId = projectId ? `${projectId}${PROJECT_ID_SEPARATOR}${child.id_}` : child.id_; + let node = testRoot!.children.get(nodeId); + + if (!node) { + node = testController.createTestItem(nodeId, child.name, Uri.file(child.path)); + + node.canResolveChildren = true; + node.tags = [RunTestTag, DebugTestTag]; + + // Set range for class nodes (and other nodes) if lineno is available + let range: Range | undefined; + if ('lineno' in child && child.lineno) { + if (Number(child.lineno) === 0) { + range = new Range(new Position(0, 0), new Position(0, 0)); + } else { + range = new Range( + new Position(Number(child.lineno) - 1, 0), + new Position(Number(child.lineno), 0), + ); + } + node.range = range; + } + + testRoot!.children.add(node); + } + populateTestTree(testController, child, node, testItemMappings, token, projectId, projectName); + } + } + }); +} + +function isTestItem(test: DiscoveredTestNode | DiscoveredTestItem): test is DiscoveredTestItem { + return test.type_ === 'test'; +} + +export function createExecutionErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + testIds: string[], + cwd: string, +): ExecutionTestPayload { + const etp: ExecutionTestPayload = { + cwd, + status: 'error', + error: `Test run failed, the python test process was terminated before it could exit on its own for workspace ${cwd}`, + result: {}, + }; + // add error result for each attempted test. + for (let i = 0; i < testIds.length; i = i + 1) { + const test = testIds[i]; + etp.result![test] = { + test, + outcome: 'error', + message: ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal}`, + }; + } + return etp; +} + +export function createDiscoveryErrorPayload( + code: number | null, + signal: NodeJS.Signals | null, + cwd: string, +): DiscoveredTestPayload { + return { + cwd, + status: 'error', + error: [ + ` \n The python test process was terminated before it could exit on its own, the process errored with: Code: ${code}, Signal: ${signal} for workspace ${cwd}`, + ], + }; +} + +/** + * Splits a test name into its parent test name and subtest unique section. + * + * @param testName The full test name string. + * @returns A tuple where the first item is the parent test name and the second item is the subtest section or `testName` if no subtest section exists. + */ +export function splitTestNameWithRegex(testName: string): [string, string] { + // If a match is found, return the parent test name and the subtest (whichever was captured between parenthesis or square brackets). + // Otherwise, return the entire testName for the parent and entire testName for the subtest. + const regex = /^(.*?) ([\[(].*[\])])$/; + const match = testName.match(regex); + if (match) { + return [match[1].trim(), match[2] || match[3] || testName]; + } + return [testName, testName]; +} + +/** + * Takes a list of arguments and adds an key-value pair to the list if the key doesn't already exist. Searches each element + * in the array for the key to see if it is contained within the element. + * @param args list of arguments to search + * @param argToAdd argument to add if it doesn't already exist + * @returns the list of arguments with the key-value pair added if it didn't already exist + */ +export function addValueIfKeyNotExist(args: string[], key: string, value: string | null): string[] { + for (const arg of args) { + if (arg.includes(key)) { + traceInfo(`arg: ${key} already exists in args, not adding.`); + return args; + } + } + if (value) { + args.push(`${key}=${value}`); + } else { + args.push(`${key}`); + } + return args; +} + +/** + * Checks if a key exists in a list of arguments. Searches each element in the array + * for the key to see if it is contained within the element. + * @param args list of arguments to search + * @param key string to search for + * @returns true if the key exists in the list of arguments, false otherwise + */ +export function argKeyExists(args: string[], key: string): boolean { + for (const arg of args) { + if (arg.includes(key)) { + return true; + } + } + return false; +} + +/** + * Checks recursively if any parent directories of the given path are symbolic links. + * @param {string} currentPath - The path to start checking from. + * @returns {Promise} - Returns true if any parent directory is a symlink, otherwise false. + */ +export async function hasSymlinkParent(currentPath: string): Promise { + try { + // Resolve the path to an absolute path + const absolutePath = path.resolve(currentPath); + // Get the parent directory + const parentDirectory = path.dirname(absolutePath); + // Check if the current directory is the root directory + if (parentDirectory === absolutePath) { + return false; + } + // Check if the parent directory is a symlink + const stats = await fs.promises.lstat(parentDirectory); + if (stats.isSymbolicLink()) { + traceLog(`Symlink found at: ${parentDirectory}`); + return true; + } + // Recurse up the directory tree + return await hasSymlinkParent(parentDirectory); + } catch (error) { + traceError('Error checking symlinks:', error); + return false; + } +} diff --git a/src/client/testing/testController/controller.ts b/src/client/testing/testController/controller.ts new file mode 100644 index 000000000000..04de209c171d --- /dev/null +++ b/src/client/testing/testController/controller.ts @@ -0,0 +1,1004 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable, named } from 'inversify'; +import { uniq } from 'lodash'; +import * as minimatch from 'minimatch'; +import { + CancellationToken, + TestController, + TestItem, + TestRunRequest, + tests, + WorkspaceFolder, + RelativePattern, + TestRunProfileKind, + CancellationTokenSource, + Uri, + EventEmitter, + TextDocument, + FileCoverageDetail, + TestRun, + MarkdownString, +} from 'vscode'; +import { IExtensionSingleActivationService } from '../../activation/types'; +import { ICommandManager, IWorkspaceService } from '../../common/application/types'; +import * as constants from '../../common/constants'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { IConfigurationService, IDisposableRegistry, Resource } from '../../common/types'; +import { DelayedTrigger, IDelayedTrigger } from '../../common/utils/delayTrigger'; +import { noop } from '../../common/utils/misc'; +import { IInterpreterService } from '../../interpreter/contracts'; +import { traceError, traceInfo, traceVerbose } from '../../logging'; +import { IEventNamePropertyMapping, sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; +import { TestProvider } from '../types'; +import { createErrorTestItem, DebugTestTag, getNodeByUri, RunTestTag } from './common/testItemUtilities'; +import { buildErrorNodeOptions } from './common/utils'; +import { ITestController, ITestFrameworkController, TestRefreshOptions } from './common/types'; +import { WorkspaceTestAdapter } from './workspaceTestAdapter'; +import { ITestDebugLauncher } from '../common/types'; +import { PythonResultResolver } from './common/resultResolver'; +import { onDidSaveTextDocument } from '../../common/vscodeApis/workspaceApis'; +import { IEnvironmentVariablesProvider } from '../../common/variables/types'; +import { ProjectAdapter } from './common/projectAdapter'; +import { TestProjectRegistry } from './common/testProjectRegistry'; +import { createTestAdapters, getProjectId } from './common/projectUtils'; +import { executeTestsForProjects } from './common/projectTestExecution'; +import { useEnvExtension, getEnvExtApi } from '../../envExt/api.internal'; +import { DidChangePythonProjectsEventArgs, PythonProject } from '../../envExt/types'; + +// Types gymnastics to make sure that sendTriggerTelemetry only accepts the correct types. +type EventPropertyType = IEventNamePropertyMapping[EventName.UNITTEST_DISCOVERY_TRIGGER]; +type TriggerKeyType = keyof EventPropertyType; +type TriggerType = EventPropertyType[TriggerKeyType]; + +@injectable() +export class PythonTestController implements ITestController, IExtensionSingleActivationService { + public readonly supportedWorkspaceTypes = { untrustedWorkspace: false, virtualWorkspace: false }; + + // Legacy: Single workspace test adapter per workspace (backward compatibility) + private readonly testAdapters: Map = new Map(); + + // Registry for multi-project testing (one registry instance manages all projects across workspaces) + private readonly projectRegistry: TestProjectRegistry; + + private readonly triggerTypes: TriggerType[] = []; + + private readonly testController: TestController; + + private readonly refreshData: IDelayedTrigger; + + private refreshCancellation: CancellationTokenSource; + + private readonly refreshingCompletedEvent: EventEmitter = new EventEmitter(); + + private readonly refreshingStartedEvent: EventEmitter = new EventEmitter(); + + private readonly runWithoutConfigurationEvent: EventEmitter = new EventEmitter< + WorkspaceFolder[] + >(); + + public readonly onRefreshingCompleted = this.refreshingCompletedEvent.event; + + public readonly onRefreshingStarted = this.refreshingStartedEvent.event; + + public readonly onRunWithoutConfiguration = this.runWithoutConfigurationEvent.event; + + private sendTestDisabledTelemetry = true; + + constructor( + @inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService, + @inject(IConfigurationService) private readonly configSettings: IConfigurationService, + @inject(ITestFrameworkController) @named(PYTEST_PROVIDER) private readonly pytest: ITestFrameworkController, + @inject(ITestFrameworkController) @named(UNITTEST_PROVIDER) private readonly unittest: ITestFrameworkController, + @inject(IDisposableRegistry) private readonly disposables: IDisposableRegistry, + @inject(IInterpreterService) private readonly interpreterService: IInterpreterService, + @inject(ICommandManager) private readonly commandManager: ICommandManager, + @inject(IPythonExecutionFactory) private readonly pythonExecFactory: IPythonExecutionFactory, + @inject(ITestDebugLauncher) private readonly debugLauncher: ITestDebugLauncher, + @inject(IEnvironmentVariablesProvider) private readonly envVarsService: IEnvironmentVariablesProvider, + ) { + this.refreshCancellation = new CancellationTokenSource(); + + this.testController = tests.createTestController('python-tests', 'Python Tests'); + this.disposables.push(this.testController); + + // Initialize project registry for multi-project testing support + this.projectRegistry = new TestProjectRegistry( + this.testController, + this.configSettings, + this.interpreterService, + this.envVarsService, + ); + + const delayTrigger = new DelayedTrigger( + (uri: Uri, invalidate: boolean) => { + this.refreshTestDataInternal(uri); + if (invalidate) { + this.invalidateTests(uri); + } + }, + 250, // Delay running the refresh by 250 ms + 'Refresh Test Data', + ); + this.disposables.push(delayTrigger); + this.refreshData = delayTrigger; + + this.disposables.push( + this.testController.createRunProfile( + 'Run Tests', + TestRunProfileKind.Run, + this.runTests.bind(this), + true, + RunTestTag, + ), + this.testController.createRunProfile( + 'Debug Tests', + TestRunProfileKind.Debug, + this.runTests.bind(this), + true, + DebugTestTag, + ), + this.testController.createRunProfile( + 'Coverage Tests', + TestRunProfileKind.Coverage, + this.runTests.bind(this), + true, + RunTestTag, + ), + ); + + this.testController.resolveHandler = this.resolveChildren.bind(this); + this.testController.refreshHandler = (token: CancellationToken) => { + this.disposables.push( + token.onCancellationRequested(() => { + traceVerbose('Testing: Stop refreshing triggered'); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERING_STOP); + this.stopRefreshing(); + }), + ); + + traceVerbose('Testing: Manually triggered test refresh'); + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { + trigger: constants.CommandSource.commandPalette, + }); + return this.refreshTestData(undefined, { forceRefresh: true }); + }; + } + + /** + * Determines the test provider (pytest or unittest) based on workspace settings. + */ + private getTestProvider(workspaceUri: Uri): TestProvider { + const settings = this.configSettings.getSettings(workspaceUri); + return settings.testing.unittestEnabled ? UNITTEST_PROVIDER : PYTEST_PROVIDER; + } + + /** + * Sets up file watchers for test discovery triggers. + */ + private setupFileWatchers(workspace: WorkspaceFolder): void { + const settings = this.configSettings.getSettings(workspace.uri); + if (settings.testing.autoTestDiscoverOnSaveEnabled) { + traceVerbose(`Testing: Setting up watcher for ${workspace.uri.fsPath}`); + this.watchForSettingsChanges(workspace); + this.watchForTestContentChangeOnSave(); + } + } + + /** + * Activates the test controller for all workspaces. + * + * Two activation modes: + * 1. **Project-based mode** (when Python Environments API available): + * 2. **Legacy mode** (fallback): + * + * Uses `Promise.allSettled` for resilient multi-workspace activation: + */ + public async activate(): Promise { + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + // PROJECT-BASED MODE: Uses Python Environments API to discover projects + // Each project becomes its own test tree root with its own Python environment + if (useEnvExtension()) { + traceInfo('[test-by-project] Activating project-based testing mode'); + + // Discover projects in parallel across all workspaces + // Promise.allSettled ensures one workspace failure doesn't block others + const results = await Promise.allSettled( + Array.from(workspaces).map(async (workspace) => { + // Queries Python Environments API and creates ProjectAdapter instances + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspace.uri); + return { workspace, projectCount: projects.length }; + }), + ); + + // Process results: successful workspaces get file watchers, failed ones fall back to legacy + results.forEach((result, index) => { + const workspace = workspaces[index]; + if (result.status === 'fulfilled') { + traceInfo( + `[test-by-project] Activated ${result.value.projectCount} project(s) for ${workspace.uri.fsPath}`, + ); + this.setupFileWatchers(workspace); + } else { + // Graceful degradation: if project discovery fails, use legacy single-adapter mode + traceError(`[test-by-project] Failed for ${workspace.uri.fsPath}:`, result.reason); + this.activateLegacyWorkspace(workspace); + } + }); + // Subscribe to project changes to update test tree when projects are added/removed + await this.subscribeToProjectChanges(); + return; + } + + // LEGACY MODE: Single WorkspaceTestAdapter per workspace (backward compatibility) + workspaces.forEach((workspace) => { + this.activateLegacyWorkspace(workspace); + }); + } + + /** + * Subscribes to Python project changes from the Python Environments API. + * When projects are added or removed, updates the test tree accordingly. + */ + private async subscribeToProjectChanges(): Promise { + try { + const envExtApi = await getEnvExtApi(); + this.disposables.push( + envExtApi.onDidChangePythonProjects((event: DidChangePythonProjectsEventArgs) => { + this.handleProjectChanges(event).catch((error) => { + traceError('[test-by-project] Error handling project changes:', error); + }); + }), + ); + traceInfo('[test-by-project] Subscribed to Python project changes'); + } catch (error) { + traceError('[test-by-project] Failed to subscribe to project changes:', error); + } + } + + /** + * Handles changes to Python projects (added or removed). + * Cleans up stale test items and re-discovers projects and tests for affected workspaces. + */ + private async handleProjectChanges(event: DidChangePythonProjectsEventArgs): Promise { + const { added, removed } = event; + + if (added.length === 0 && removed.length === 0) { + return; + } + + traceInfo(`[test-by-project] Project changes detected: ${added.length} added, ${removed.length} removed`); + + // Find all affected workspaces + const affectedWorkspaces = new Set(); + + const findWorkspace = (project: PythonProject): WorkspaceFolder | undefined => { + return this.workspaceService.getWorkspaceFolder(project.uri); + }; + + for (const project of [...added, ...removed]) { + const workspace = findWorkspace(project); + if (workspace) { + affectedWorkspaces.add(workspace); + } + } + + // For each affected workspace, clean up and re-discover + for (const workspace of affectedWorkspaces) { + traceInfo(`[test-by-project] Re-discovering projects for workspace: ${workspace.uri.fsPath}`); + + // Get the current projects before clearing to know what to clean up + const existingProjects = this.projectRegistry.getProjectsArray(workspace.uri); + + // Remove ALL test items for the affected workspace's projects + // This ensures no stale items remain from deleted/changed projects + this.removeWorkspaceProjectTestItems(workspace.uri, existingProjects); + + // Also explicitly remove test items for removed projects (in case they weren't tracked) + for (const project of removed) { + const projectWorkspace = findWorkspace(project); + if (projectWorkspace?.uri.toString() === workspace.uri.toString()) { + this.removeProjectTestItems(project); + } + } + + // Re-discover all projects and tests for the workspace in a single pass. + // discoverAllProjectsInWorkspace is responsible for clearing/re-registering + // projects and performing test discovery for the workspace. + await this.discoverAllProjectsInWorkspace(workspace.uri); + } + } + + /** + * Removes all test items associated with projects in a workspace. + * Used to clean up stale items before re-discovery. + */ + private removeWorkspaceProjectTestItems(workspaceUri: Uri, projects: ProjectAdapter[]): void { + const idsToRemove: string[] = []; + + // Collect IDs of test items belonging to any project in this workspace + for (const project of projects) { + const projectIdPrefix = getProjectId(project.projectUri); + const projectFsPath = project.projectUri.fsPath; + + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectIdPrefix)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (legacy items might use path directly) + else if (item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + } + + // Also remove any items whose URI is within the workspace (catch-all for edge cases) + this.testController.items.forEach((item) => { + if ( + item.uri && + this.workspaceService.getWorkspaceFolder(item.uri)?.uri.toString() === workspaceUri.toString() + ) { + if (!idsToRemove.includes(item.id)) { + idsToRemove.push(item.id); + } + } + }); + + // Remove all collected items + for (const id of idsToRemove) { + this.testController.items.delete(id); + } + + traceInfo( + `[test-by-project] Cleaned up ${idsToRemove.length} test items for workspace: ${workspaceUri.fsPath}`, + ); + } + + /** + * Removes test items associated with a specific project from the test controller. + * Matches items by project ID prefix, fsPath, or URI. + */ + private removeProjectTestItems(project: PythonProject): void { + const projectId = getProjectId(project.uri); + const projectFsPath = project.uri.fsPath; + const idsToRemove: string[] = []; + + // Find all root items that belong to this project + this.testController.items.forEach((item) => { + // Match by project ID prefix (e.g., "file:///path@@vsc@@...") + if (item.id.startsWith(projectId)) { + idsToRemove.push(item.id); + } + // Match by fsPath in ID (items might use path directly without URI prefix) + else if (item.id.startsWith(projectFsPath) || item.id.includes(projectFsPath)) { + idsToRemove.push(item.id); + } + // Match by item URI being within project directory + else if (item.uri && item.uri.fsPath.startsWith(projectFsPath)) { + idsToRemove.push(item.id); + } + }); + + for (const id of idsToRemove) { + this.testController.items.delete(id); + traceVerbose(`[test-by-project] Removed test item: ${id}`); + } + + if (idsToRemove.length > 0) { + traceInfo(`[test-by-project] Removed ${idsToRemove.length} test items for project: ${project.name}`); + } + } + + /** + * Activates testing for a workspace using the legacy single-adapter approach. + * Used for backward compatibility when project-based testing is disabled or unavailable. + */ + private activateLegacyWorkspace(workspace: WorkspaceFolder): void { + const testProvider = this.getTestProvider(workspace.uri); + const resultResolver = new PythonResultResolver(this.testController, testProvider, workspace.uri); + const { discoveryAdapter, executionAdapter } = createTestAdapters( + testProvider, + resultResolver, + this.configSettings, + this.envVarsService, + ); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + testProvider, + discoveryAdapter, + executionAdapter, + workspace.uri, + resultResolver, + ); + + this.testAdapters.set(workspace.uri, workspaceTestAdapter); + this.setupFileWatchers(workspace); + } + + public refreshTestData(uri?: Resource, options?: TestRefreshOptions): Promise { + if (options?.forceRefresh) { + if (uri === undefined) { + // This is a special case where we want everything to be re-discovered. + traceVerbose('Testing: Clearing all discovered tests'); + this.testController.items.forEach((item) => { + const ids: string[] = []; + item.children.forEach((child) => ids.push(child.id)); + ids.forEach((id) => item.children.delete(id)); + }); + + traceVerbose('Testing: Forcing test data refresh'); + return this.refreshTestDataInternal(undefined); + } + + traceVerbose('Testing: Forcing test data refresh'); + return this.refreshTestDataInternal(uri); + } + + this.refreshData.trigger(uri, false); + return Promise.resolve(); + } + + public stopRefreshing(): void { + this.refreshCancellation.cancel(); + this.refreshCancellation.dispose(); + this.refreshCancellation = new CancellationTokenSource(); + } + + public clearTestController(): void { + const ids: string[] = []; + this.testController.items.forEach((item) => ids.push(item.id)); + ids.forEach((id) => this.testController.items.delete(id)); + } + + private async refreshTestDataInternal(uri?: Resource): Promise { + this.refreshingStartedEvent.fire(); + try { + if (uri) { + await this.discoverTestsInWorkspace(uri); + } else { + await this.discoverTestsInAllWorkspaces(); + } + } finally { + this.refreshingCompletedEvent.fire(); + } + } + + /** + * Discovers tests for a single workspace. + * + * **Discovery flow:** + * 1. If the workspace has registered projects (via Python Environments API), + * uses project-based discovery: each project is discovered independently + * with its own Python environment and test adapters. + * 2. Otherwise, falls back to legacy mode: a single WorkspaceTestAdapter + * discovers all tests in the workspace using the active interpreter. + * + * In project-based mode, the test tree will have separate roots for each project. + * In legacy mode, the workspace folder is the single test tree root. + */ + private async discoverTestsInWorkspace(uri: Uri): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(uri); + if (!workspace?.uri) { + traceError('Unable to find workspace for given file'); + return; + } + + const settings = this.configSettings.getSettings(uri); + traceVerbose(`Discover tests for workspace name: ${workspace.name} - uri: ${uri.fsPath}`); + + // Ensure we send test telemetry if it gets disabled again + this.sendTestDisabledTelemetry = true; + + // Check if any test framework is enabled BEFORE project-based discovery + // This ensures the config screen stays visible when testing is disabled + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + await this.handleNoTestProviderEnabled(workspace); + return; + } + + // Use project-based discovery if applicable (only reached if testing is enabled) + if (this.projectRegistry.hasProjects(workspace.uri)) { + await this.discoverAllProjectsInWorkspace(workspace.uri); + return; + } + + // Legacy mode: Single workspace adapter + if (settings.testing.pytestEnabled) { + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'pytest'); + } else if (settings.testing.unittestEnabled) { + await this.discoverWorkspaceTestsLegacy(workspace.uri, 'unittest'); + } + } + + /** + * Discovers tests for all projects within a workspace (project-based mode). + * Re-discovers projects from the Python Environments API before running test discovery. + * This ensures the test tree stays in sync with project changes. + */ + private async discoverAllProjectsInWorkspace(workspaceUri: Uri): Promise { + // Defensive check: ensure testing is enabled (should be checked by caller, but be safe) + const settings = this.configSettings.getSettings(workspaceUri); + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + traceVerbose('[test-by-project] Skipping discovery - no test framework enabled'); + return; + } + + // Get existing projects before re-discovery for cleanup + const existingProjects = this.projectRegistry.getProjectsArray(workspaceUri); + + // Clean up all existing test items for this workspace + // This ensures stale items from deleted/changed projects are removed + this.removeWorkspaceProjectTestItems(workspaceUri, existingProjects); + + // Re-discover projects from Python Environments API + // This picks up any added/removed projects since last discovery + this.projectRegistry.clearWorkspace(workspaceUri); + const projects = await this.projectRegistry.discoverAndRegisterProjects(workspaceUri); + + if (projects.length === 0) { + traceError(`[test-by-project] No projects found for workspace: ${workspaceUri.fsPath}`); + return; + } + + traceInfo(`[test-by-project] Starting discovery for ${projects.length} project(s) in workspace`); + + try { + // Configure nested project exclusions before discovery + this.projectRegistry.configureNestedProjectIgnores(workspaceUri); + + // Track completion for progress logging + const projectsCompleted = new Set(); + + // Run discovery for all projects in parallel + await Promise.all(projects.map((project) => this.discoverTestsForProject(project, projectsCompleted))); + + traceInfo( + `[test-by-project] Discovery complete: ${projectsCompleted.size}/${projects.length} projects completed`, + ); + } catch (error) { + traceError(`[test-by-project] Discovery failed for workspace ${workspaceUri.fsPath}:`, error); + } + } + + /** + * Discovers tests for a single project (project-based mode). + * Creates test tree items rooted at the project's directory. + */ + private async discoverTestsForProject(project: ProjectAdapter, projectsCompleted: Set): Promise { + try { + traceInfo(`[test-by-project] Discovering tests for project: ${project.projectName}`); + project.isDiscovering = true; + + // In project-based mode, the discovery adapter uses the Python Environments API + // to get the environment directly, so we don't need to pass the interpreter + await project.discoveryAdapter.discoverTests( + project.projectUri, + this.pythonExecFactory, + this.refreshCancellation.token, + undefined, // Interpreter not needed; adapter uses Python Environments API + project, + ); + + // Mark project as completed (use URI string as unique key) + projectsCompleted.add(project.projectUri.toString()); + traceInfo(`[test-by-project] Project ${project.projectName} discovery completed`); + } catch (error) { + traceError(`[test-by-project] Discovery failed for project ${project.projectName}:`, error); + // Individual project failures don't block others + projectsCompleted.add(project.projectUri.toString()); // Still mark as completed + } finally { + project.isDiscovering = false; + } + } + + /** + * Discovers tests across all workspace folders. + * Iterates each workspace and triggers discovery. + */ + private async discoverTestsInAllWorkspaces(): Promise { + traceVerbose('Testing: Refreshing all test data'); + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + + await Promise.all( + workspaces.map(async (workspace) => { + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + } + await this.discoverTestsInWorkspace(workspace.uri); + }), + ); + } + + /** + * Discovers tests for a workspace using legacy single-adapter mode. + */ + private async discoverWorkspaceTestsLegacy(workspaceUri: Uri, expectedProvider: TestProvider): Promise { + const testAdapter = this.testAdapters.get(workspaceUri); + + if (!testAdapter) { + traceError('Unable to find test adapter for workspace.'); + return; + } + + const actualProvider = testAdapter.getTestProvider(); + if (actualProvider !== expectedProvider) { + traceError(`Test provider in adapter is not ${expectedProvider}. Please reload window.`); + this.surfaceErrorNode( + workspaceUri, + 'Test provider types are not aligned, please reload your VS Code window.', + expectedProvider, + ); + return; + } + + await testAdapter.discoverTests( + this.testController, + this.pythonExecFactory, + this.refreshCancellation.token, + await this.interpreterService.getActiveInterpreter(workspaceUri), + ); + } + + /** + * Handles the case when no test provider is enabled. + * Sends telemetry and removes test items for the workspace from the tree. + */ + private async handleNoTestProviderEnabled(workspace: WorkspaceFolder): Promise { + if (this.sendTestDisabledTelemetry) { + this.sendTestDisabledTelemetry = false; + sendTelemetryEvent(EventName.UNITTEST_DISABLED); + } + + this.removeTestItemsForWorkspace(workspace); + } + + /** + * Removes all test items belonging to a specific workspace from the test controller. + * This is used when test discovery is disabled for a workspace. + */ + private removeTestItemsForWorkspace(workspace: WorkspaceFolder): void { + const itemsToDelete: string[] = []; + + this.testController.items.forEach((testItem: TestItem) => { + const itemWorkspace = this.workspaceService.getWorkspaceFolder(testItem.uri); + if (itemWorkspace?.uri.fsPath === workspace.uri.fsPath) { + itemsToDelete.push(testItem.id); + } + }); + + itemsToDelete.forEach((id) => this.testController.items.delete(id)); + } + + private async resolveChildren(item: TestItem | undefined): Promise { + if (item) { + traceVerbose(`Testing: Resolving item ${item.id}`); + const settings = this.configSettings.getSettings(item.uri); + if (settings.testing.pytestEnabled) { + return this.pytest.resolveChildren(this.testController, item, this.refreshCancellation.token); + } + if (settings.testing.unittestEnabled) { + return this.unittest.resolveChildren(this.testController, item, this.refreshCancellation.token); + } + } else { + traceVerbose('Testing: Refreshing all test data'); + this.sendTriggerTelemetry('auto'); + const workspaces: readonly WorkspaceFolder[] = this.workspaceService.workspaceFolders || []; + await Promise.all( + workspaces.map(async (workspace) => { + // In project-based mode, each project has its own environment, + // so we don't require a global active interpreter + if (!useEnvExtension()) { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + traceError('Cannot trigger test discovery as a valid interpreter is not selected'); + return; + } + } + await this.refreshTestDataInternal(workspace.uri); + }), + ); + } + return Promise.resolve(); + } + + private async runTests(request: TestRunRequest, token: CancellationToken): Promise { + const workspaces = this.getWorkspacesForTestRun(request); + const runInstance = this.testController.createTestRun( + request, + `Running Tests for Workspace(s): ${workspaces.map((w) => w.uri.fsPath).join(';')}`, + true, + ); + + const dispose = token.onCancellationRequested(() => { + runInstance.appendOutput(`\nRun instance cancelled.\r\n`); + runInstance.end(); + }); + + const unconfiguredWorkspaces: WorkspaceFolder[] = []; + + try { + await Promise.all( + workspaces.map((workspace) => + this.runTestsForWorkspace(workspace, request, runInstance, token, unconfiguredWorkspaces), + ), + ); + } finally { + traceVerbose('Finished running tests, ending runInstance.'); + runInstance.appendOutput(`Finished running tests!\r\n`); + runInstance.end(); + dispose.dispose(); + if (unconfiguredWorkspaces.length > 0) { + this.runWithoutConfigurationEvent.fire(unconfiguredWorkspaces); + } + } + } + + /** + * Gets the list of workspaces to run tests for based on the test run request. + */ + private getWorkspacesForTestRun(request: TestRunRequest): WorkspaceFolder[] { + if (request.include) { + const workspaces: WorkspaceFolder[] = []; + uniq(request.include.map((r) => this.workspaceService.getWorkspaceFolder(r.uri))).forEach((w) => { + if (w) { + workspaces.push(w); + } + }); + return workspaces; + } + return Array.from(this.workspaceService.workspaceFolders || []); + } + + /** + * Runs tests for a single workspace. + */ + private async runTestsForWorkspace( + workspace: WorkspaceFolder, + request: TestRunRequest, + runInstance: TestRun, + token: CancellationToken, + unconfiguredWorkspaces: WorkspaceFolder[], + ): Promise { + if (!(await this.interpreterService.getActiveInterpreter(workspace.uri))) { + this.commandManager + .executeCommand(constants.Commands.TriggerEnvironmentSelection, workspace.uri) + .then(noop, noop); + return; + } + + const testItems = this.getTestItemsForWorkspace(workspace, request); + const settings = this.configSettings.getSettings(workspace.uri); + + if (testItems.length === 0) { + if (!settings.testing.pytestEnabled && !settings.testing.unittestEnabled) { + unconfiguredWorkspaces.push(workspace); + } + return; + } + + // Check if we're in project-based mode and should use project-specific execution + if (this.projectRegistry.hasProjects(workspace.uri)) { + const projects = this.projectRegistry.getProjectsArray(workspace.uri); + await executeTestsForProjects(projects, testItems, runInstance, request, token, { + projectRegistry: this.projectRegistry, + pythonExecFactory: this.pythonExecFactory, + debugLauncher: this.debugLauncher, + }); + return; + } + + // For unittest (or pytest when not in project mode), use the legacy WorkspaceTestAdapter. + // In project mode, legacy adapters may not be initialized, so create one on demand. + let testAdapter = this.testAdapters.get(workspace.uri); + if (!testAdapter) { + // Initialize legacy adapter on demand (needed for unittest in project mode) + this.activateLegacyWorkspace(workspace); + testAdapter = this.testAdapters.get(workspace.uri); + } + + if (!testAdapter) { + traceError(`[test] No test adapter available for workspace: ${workspace.uri.fsPath}`); + return; + } + + this.setupCoverageIfNeeded(request, testAdapter); + + if (settings.testing.pytestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'pytest', + ); + } else if (settings.testing.unittestEnabled) { + await this.executeTestsForProvider( + workspace, + testAdapter, + testItems, + runInstance, + request, + token, + 'unittest', + ); + } else { + unconfiguredWorkspaces.push(workspace); + } + } + + /** + * Gets test items that belong to a specific workspace from the run request. + */ + private getTestItemsForWorkspace(workspace: WorkspaceFolder, request: TestRunRequest): TestItem[] { + const testItems: TestItem[] = []; + // If the run request includes test items then collect only items that belong to + // `workspace`. If there are no items in the run request then just run the `workspace` + // root test node. Include will be `undefined` in the "run all" scenario. + (request.include ?? this.testController.items).forEach((i: TestItem) => { + const w = this.workspaceService.getWorkspaceFolder(i.uri); + if (w?.uri.fsPath === workspace.uri.fsPath) { + testItems.push(i); + } + }); + return testItems; + } + + /** + * Sets up detailed coverage loading if the run profile is for coverage. + */ + private setupCoverageIfNeeded(request: TestRunRequest, testAdapter: WorkspaceTestAdapter): void { + // no profile will have TestRunProfileKind.Coverage if rewrite isn't enabled + if (request.profile?.kind && request.profile?.kind === TestRunProfileKind.Coverage) { + request.profile.loadDetailedCoverage = ( + _testRun: TestRun, + fileCoverage, + _token, + ): Thenable => { + const details = testAdapter.resultResolver.detailedCoverageMap.get(fileCoverage.uri.fsPath); + if (details === undefined) { + // given file has no detailed coverage data + return Promise.resolve([]); + } + return Promise.resolve(details); + }; + } + } + + /** + * Executes tests using the test adapter for a specific test provider. + */ + private async executeTestsForProvider( + workspace: WorkspaceFolder, + testAdapter: WorkspaceTestAdapter, + testItems: TestItem[], + runInstance: TestRun, + request: TestRunRequest, + token: CancellationToken, + provider: TestProvider, + ): Promise { + sendTelemetryEvent(EventName.UNITTEST_RUN, undefined, { + tool: provider, + debugging: request.profile?.kind === TestRunProfileKind.Debug, + }); + + await testAdapter.executeTests( + this.testController, + runInstance, + testItems, + this.pythonExecFactory, + token, + request.profile?.kind, + this.debugLauncher, + await this.interpreterService.getActiveInterpreter(workspace.uri), + ); + } + + private invalidateTests(uri: Uri) { + this.testController.items.forEach((root) => { + const item = getNodeByUri(root, uri); + if (item && !!item.invalidateResults) { + // Minimize invalidating to test case nodes for the test file where + // the change occurred + item.invalidateResults(); + } + }); + } + + private watchForSettingsChanges(workspace: WorkspaceFolder): void { + const pattern = new RelativePattern(workspace, '**/{settings.json,pytest.ini,pyproject.toml,setup.cfg}'); + const watcher = this.workspaceService.createFileSystemWatcher(pattern); + this.disposables.push(watcher); + + this.disposables.push( + onDidSaveTextDocument(async (doc: TextDocument) => { + const file = doc.fileName; + // refresh on any settings file save + if ( + file.includes('settings.json') || + file.includes('pytest.ini') || + file.includes('setup.cfg') || + file.includes('pyproject.toml') + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } + }), + ); + /* Keep both watchers for create and delete since config files can change test behavior without content + due to their impact on pythonPath. */ + this.disposables.push( + watcher.onDidCreate((uri) => { + traceVerbose(`Testing: Trigger refresh after creating ${uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(uri, false); + }), + ); + this.disposables.push( + watcher.onDidDelete((uri) => { + traceVerbose(`Testing: Trigger refresh after deleting in ${uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(uri, false); + }), + ); + } + + private watchForTestContentChangeOnSave(): void { + this.disposables.push( + onDidSaveTextDocument(async (doc: TextDocument) => { + const settings = this.configSettings.getSettings(doc.uri); + if ( + settings.testing.autoTestDiscoverOnSaveEnabled && + minimatch.default(doc.uri.fsPath, settings.testing.autoTestDiscoverOnSavePattern) + ) { + traceVerbose(`Testing: Trigger refresh after saving ${doc.uri.fsPath}`); + this.sendTriggerTelemetry('watching'); + this.refreshData.trigger(doc.uri, false); + } + }), + ); + } + + /** + * Send UNITTEST_DISCOVERY_TRIGGER telemetry event only once per trigger type. + * + * @param triggerType The trigger type to send telemetry for. + */ + private sendTriggerTelemetry(trigger: TriggerType): void { + if (!this.triggerTypes.includes(trigger)) { + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_TRIGGER, undefined, { + trigger, + }); + this.triggerTypes.push(trigger); + } + } + + private surfaceErrorNode(workspaceUri: Uri, message: string, testProvider: TestProvider): void { + let errorNode = this.testController.items.get(`DiscoveryError:${workspaceUri.fsPath}`); + if (errorNode === undefined) { + const options = buildErrorNodeOptions(workspaceUri, message, testProvider); + errorNode = createErrorTestItem(this.testController, options); + this.testController.items.add(errorNode); + } + const errorNodeLabel: MarkdownString = new MarkdownString(message); + errorNodeLabel.isTrusted = true; + errorNode.error = errorNodeLabel; + } +} diff --git a/src/client/testing/testController/pytest/arguments.ts b/src/client/testing/testController/pytest/arguments.ts new file mode 100644 index 000000000000..2b4efbd56f42 --- /dev/null +++ b/src/client/testing/testController/pytest/arguments.ts @@ -0,0 +1,255 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestFilter } from '../../common/types'; +import { getPositionalArguments, filterArguments } from '../common/argumentsHelper'; + +const OptionsWithArguments = [ + '-c', + '-k', + '-m', + '-o', + '-p', + '-r', + '-W', + '-n', // -n is a pytest-xdist option + '--assert', + '--basetemp', + '--cache-show', + '--capture', + '--code-highlight', + '--color', + '--confcutdir', + '--cov', + '--cov-config', + '--cov-fail-under', + '--cov-report', + '--deselect', + '--dist', + '--doctest-glob', + '--doctest-report', + '--durations', + '--durations-min', + '--ignore', + '--ignore-glob', + '--import-mode', + '--junit-prefix', + '--junit-xml', + '--last-failed-no-failures', + '--lfnf', + '--log-auto-indent', + '--log-cli-date-format', + '--log-cli-format', + '--log-cli-level', + '--log-date-format', + '--log-file', + '--log-file-date-format', + '--log-file-format', + '--log-file-level', + '--log-format', + '--log-level', + '--maxfail', + '--override-ini', + '--pastebin', + '--pdbcls', + '--pythonwarnings', + '--result-log', + '--rootdir', + '--show-capture', + '--tb', + '--verbosity', + '--max-slave-restart', + '--numprocesses', + '--rsyncdir', + '--rsyncignore', + '--tx', +]; + +const OptionsWithoutArguments = [ + '--cache-clear', + '--collect-in-virtualenv', + '--collect-only', + '--co', + '--continue-on-collection-errors', + '--cov-append', + '--cov-branch', + '--debug', + '--disable-pytest-warnings', + '--disable-warnings', + '--doctest-continue-on-failure', + '--doctest-ignore-import-errors', + '--doctest-modules', + '--exitfirst', + '--failed-first', + '--ff', + '--fixtures', + '--fixtures-per-test', + '--force-sugar', + '--full-trace', + '--funcargs', + '--help', + '--keep-duplicates', + '--last-failed', + '--lf', + '--markers', + '--new-first', + '--nf', + '--no-cov', + '--no-cov-on-fail', + '--no-header', + '--no-print-logs', + '--no-summary', + '--noconftest', + '--old-summary', + '--pdb', + '--pyargs', + '-PyTest, Unittest-pyargs', + '--quiet', + '--runxfail', + '--setup-only', + '--setup-plan', + '--setup-show', + '--showlocals', + '--stepwise', + '--sw', + '--stepwise-skip', + '--strict', + '--strict-config', + '--strict-markers', + '--trace-config', + '--verbose', + '--version', + '-V', + '-h', + '-l', + '-q', + '-s', + '-v', + '-x', + '--boxed', + '--forked', + '--looponfail', + '--trace', + '--tx', + '-d', +]; + +export function removePositionalFoldersAndFiles(args: string[]): string[] { + return pytestFilterArguments(args, TestFilter.removeTests); +} + +function pytestFilterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { + const optionsWithoutArgsToRemove: string[] = []; + const optionsWithArgsToRemove: string[] = []; + // Positional arguments in pytest are test directories and files. + // So if we want to run a specific test, then remove positional args. + let removePositionalArgs = false; + if (Array.isArray(argumentToRemoveOrFilter)) { + argumentToRemoveOrFilter.forEach((item) => { + if (OptionsWithArguments.indexOf(item) >= 0) { + optionsWithArgsToRemove.push(item); + } + if (OptionsWithoutArguments.indexOf(item) >= 0) { + optionsWithoutArgsToRemove.push(item); + } + }); + } else { + switch (argumentToRemoveOrFilter) { + case TestFilter.removeTests: { + optionsWithoutArgsToRemove.push( + ...['--lf', '--last-failed', '--ff', '--failed-first', '--nf', '--new-first'], + ); + optionsWithArgsToRemove.push(...['-k', '-m', '--lfnf', '--last-failed-no-failures']); + removePositionalArgs = true; + break; + } + case TestFilter.discovery: { + optionsWithoutArgsToRemove.push( + ...[ + '-x', + '--exitfirst', + '--fixtures', + '--funcargs', + '--fixtures-per-test', + '--pdb', + '--lf', + '--last-failed', + '--ff', + '--failed-first', + '--nf', + '--new-first', + '--cache-show', + '-v', + '--verbose', + '-q', + '-quiet', + '-l', + '--showlocals', + '--no-print-logs', + '--debug', + '--setup-only', + '--setup-show', + '--setup-plan', + '--trace', + ], + ); + optionsWithArgsToRemove.push( + ...[ + '-m', + '--maxfail', + '--pdbcls', + '--capture', + '--lfnf', + '--last-failed-no-failures', + '--verbosity', + '-r', + '--tb', + '--show-capture', + '--durations', + '--junit-xml', + '--junit-prefix', + '--result-log', + '-W', + '--pythonwarnings', + '--log-*', + ], + ); + removePositionalArgs = true; + break; + } + case TestFilter.debugAll: + case TestFilter.runAll: { + optionsWithoutArgsToRemove.push(...['--collect-only', '--trace']); + break; + } + case TestFilter.debugSpecific: + case TestFilter.runSpecific: { + optionsWithoutArgsToRemove.push( + ...[ + '--collect-only', + '--lf', + '--last-failed', + '--ff', + '--failed-first', + '--nf', + '--new-first', + '--trace', + ], + ); + optionsWithArgsToRemove.push(...['-k', '-m', '--lfnf', '--last-failed-no-failures']); + removePositionalArgs = true; + break; + } + default: { + throw new Error(`Unsupported Filter '${argumentToRemoveOrFilter}'`); + } + } + } + + let filteredArgs = args.slice(); + if (removePositionalArgs) { + const positionalArgs = getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); + filteredArgs = filteredArgs.filter((item) => positionalArgs.indexOf(item) === -1); + } + return filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); +} diff --git a/src/client/testing/testController/pytest/pytestController.ts b/src/client/testing/testController/pytest/pytestController.ts new file mode 100644 index 000000000000..f75580c11236 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestController.ts @@ -0,0 +1,142 @@ +/* eslint-disable class-methods-use-this */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { CancellationToken, TestItem, Uri, TestController } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { asyncForEach } from '../../../common/utils/arrayUtils'; +import { Deferred } from '../../../common/utils/async'; +import { + createWorkspaceRootTestItem, + getWorkspaceNode, + removeItemByIdFromChildren, + updateTestItemFromRawData, +} from '../common/testItemUtilities'; +import { ITestFrameworkController, TestData, RawDiscoveredTests } from '../common/types'; + +@injectable() +export class PytestController implements ITestFrameworkController { + private readonly testData: Map = new Map(); + + private discovering: Map> = new Map(); + + private idToRawData: Map = new Map(); + + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public async resolveChildren( + testController: TestController, + item: TestItem, + token?: CancellationToken, + ): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(item.uri); + if (workspace) { + // if we are still discovering then wait + const discovery = this.discovering.get(workspace.uri.fsPath); + if (discovery) { + await discovery.promise; + } + + // see if we have raw test data + const rawTestData = this.testData.get(workspace.uri.fsPath); + if (rawTestData) { + // Refresh each node with new data + if (rawTestData.length === 0) { + const items: TestItem[] = []; + testController.items.forEach((i) => items.push(i)); + items.forEach((i) => testController.items.delete(i.id)); + return Promise.resolve(); + } + + const root = rawTestData.length === 1 ? rawTestData[0].root : workspace.uri.fsPath; + if (root === item.id) { + // This is the workspace root node + if (rawTestData.length === 1) { + if (rawTestData[0].tests.length > 0) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + item.id, + rawTestData, + token, + ); + } else { + this.idToRawData.delete(item.id); + testController.items.delete(item.id); + return Promise.resolve(); + } + } else { + // To figure out which top level nodes have to removed. First we get all the + // existing nodes. Then if they have data we keep those nodes, Nodes without + // data will be removed after we check the raw data. + let subRootWithNoData: string[] = []; + item.children.forEach((c) => subRootWithNoData.push(c.id)); + + await asyncForEach(rawTestData, async (data) => { + let subRootId = data.root; + let rawId; + if (data.root === root) { + const subRoot = data.parents.filter((p) => p.parentid === '.' || p.parentid === root); + subRootId = path.join(data.root, subRoot.length > 0 ? subRoot[0].id : ''); + rawId = subRoot.length > 0 ? subRoot[0].id : undefined; + } + + if (data.tests.length > 0) { + let subRootItem = item.children.get(subRootId); + if (!subRootItem) { + subRootItem = createWorkspaceRootTestItem(testController, this.idToRawData, { + id: subRootId, + label: path.basename(subRootId), + uri: Uri.file(subRootId), + runId: subRootId, + parentId: item.id, + rawId, + }); + item.children.add(subRootItem); + } + + // We found data for a node. Remove its id from the no-data list. + subRootWithNoData = subRootWithNoData.filter((s) => s !== subRootId); + await updateTestItemFromRawData( + subRootItem, + testController, + this.idToRawData, + root, // All the file paths are based on workspace root. + [data], + token, + ); + } else { + // This means there are no tests under this node + removeItemByIdFromChildren(this.idToRawData, item, [subRootId]); + } + }); + + // We did not find any data for these nodes, delete them. + removeItemByIdFromChildren(this.idToRawData, item, subRootWithNoData); + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + workspaceNode.id, + rawTestData, + token, + ); + } + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + testController.items.delete(workspaceNode.id); + } + } + } + return Promise.resolve(); + } +} diff --git a/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts new file mode 100644 index 000000000000..16e27635e66c --- /dev/null +++ b/src/client/testing/testController/pytest/pytestDiscoveryAdapter.ts @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { createTestingDeferred } from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { useEnvExtension, getEnvironment, runInBackground } from '../../../envExt/api.internal'; +import { buildPytestEnv as configureSubprocessEnv, handleSymlinkAndRootDir } from './pytestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; + +/** + * Configures the subprocess environment for pytest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess + */ +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, fullPluginPath, discoveryPipeName); + return mutableEnv; +} + +/** + * Wrapper class for pytest test discovery. This is where we call the pytest subprocess. + */ +export class PytestTestDiscoveryAdapter implements ITestDiscoveryAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); + + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose: Deferred = createTestingDeferred(); + + // Collect all disposables related to discovery to handle cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); + } + + try { + // Build pytest command and arguments + const settings = this.configSettings.getSettings(uri); + let { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + pytestArgs = await handleSymlinkAndRootDir(cwd, pytestArgs); + + // Add --ignore flags for nested projects to prevent duplicate discovery + if (project?.nestedProjectPathsToIgnore?.length) { + const ignoreArgs = project.nestedProjectPathsToIgnore.map((nestedPath) => `--ignore=${nestedPath}`); + pytestArgs = [...pytestArgs, ...ignoreArgs]; + traceInfo( + `[test-by-project] Project ${project.projectName} ignoring nested project(s): ${ignoreArgs.join( + ' ', + )}`, + ); + } + + const commandArgs = ['-m', 'pytest', '-p', 'vscode_pytest', '--collect-only'].concat(pytestArgs); + traceVerbose( + `Running pytest discovery with command: ${commandArgs.join(' ')} for workspace ${uri.fsPath}.`, + ); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + } + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('pytest', uri, cwd, this.resultResolver, deferredTillExecClose, [5]); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for pytest discovery in workspace ${uri.fsPath}`); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: commandArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + traceInfo(`Started pytest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('pytest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); + proc.onExit((code, signal) => { + handlers.onExit(code, signal); + handlers.onClose(code, signal); + }); + + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + return; + } + + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for pytest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Pytest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; + + let resultProc: ChildProcess | undefined; + + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during pytest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('pytest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } + + try { + const result = execService.execObservable(commandArgs, spawnOptions); + resultProc = result?.proc; + + if (!resultProc) { + traceError(`Failed to spawn pytest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started pytest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning pytest discovery subprocess for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for pytest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Pytest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during pytest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } + } +} diff --git a/src/client/testing/testController/pytest/pytestExecutionAdapter.ts b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts new file mode 100644 index 000000000000..102841c2e2dd --- /dev/null +++ b/src/client/testing/testController/pytest/pytestExecutionAdapter.ts @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as path from 'path'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred } from '../../../common/utils/async'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { ExecutionTestPayload, ITestExecutionAdapter, ITestResultResolver } from '../common/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { removePositionalFoldersAndFiles } from './arguments'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { PYTEST_PROVIDER } from '../../common/constants'; +import { EXTENSION_ROOT_DIR } from '../../../common/constants'; +import * as utils from '../common/utils'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { ProjectAdapter } from '../common/projectAdapter'; + +export class PytestTestExecutionAdapter implements ITestExecutionAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + const deferredTillServerClose: Deferred = utils.createTestingDeferred(); + + // create callback to handle data received on the named pipe + const dataReceivedCallback = (data: ExecutionTestPayload) => { + if (runInstance && !runInstance.token.isCancellationRequested) { + this.resultResolver?.resolveExecution(data, runInstance); + } else { + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); + } + }; + const cSource = new CancellationTokenSource(); + runInstance.token.onCancellationRequested(() => cSource.cancel()); + + const name = await utils.startRunResultNamedPipe( + dataReceivedCallback, // callback to handle data received + deferredTillServerClose, // deferred to resolve when server closes + cSource.token, // token to cancel + ); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, resolving 'TillServerClose' deferred for ${uri.fsPath}.`); + }); + + try { + await this.runTestsNew( + uri, + testIds, + name, + cSource, + runInstance, + profileKind, + executionFactory, + debugLauncher, + interpreter, + project, + ); + } finally { + await deferredTillServerClose.promise; + } + } + + private async runTestsNew( + uri: Uri, + testIds: string[], + resultNamedPipeName: string, + serverCancel: CancellationTokenSource, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + const relativePathToPytest = 'python_files'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + const settings = this.configSettings.getSettings(uri); + const { pytestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + // get and edit env vars + const mutableEnv = { + ...(await this.envVarsService?.getEnvironmentVariables(uri)), + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo(`[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for pytest execution`); + } + + if (profileKind && profileKind === TestRunProfileKind.Coverage) { + mutableEnv.COVERAGE_ENABLED = 'True'; + } + + const debugBool = profileKind && profileKind === TestRunProfileKind.Debug; + + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + // need to check what will happen in the exec service is NOT defined and is null + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for pytest execution: ${execInfo}.`); + + try { + // Remove positional test folders and files, we will add as needed per node + let testArgs = removePositionalFoldersAndFiles(pytestArgs); + + // if user has provided `--rootdir` then use that, otherwise add `cwd` + // root dir is required so pytest can find the relative paths and for symlinks + utils.addValueIfKeyNotExist(testArgs, '--rootdir', cwd); + + // -s and --capture are both command line options that control how pytest captures output. + // if neither are set, then set --capture=no to prevent pytest from capturing output. + if (debugBool && !utils.argKeyExists(testArgs, '-s')) { + testArgs = utils.addValueIfKeyNotExist(testArgs, '--capture', 'no'); + } + + // create a file with the test ids and set the environment variable to the file name + const testIdsFileName = await utils.writeTestIdsFile(testIds); + mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; + traceInfo( + `Environment variables set for pytest execution: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}, RUN_TEST_IDS_PIPE=${mutableEnv.RUN_TEST_IDS_PIPE}`, + ); + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token: runInstance.token, + }; + + if (debugBool) { + const launchOptions: LaunchOptions = { + cwd, + args: testArgs, + token: runInstance.token, + testProvider: PYTEST_PROVIDER, + runTestIdsPort: testIdsFileName, + pytestPort: resultNamedPipeName, + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, + }; + const sessionOptions: DebugSessionOptions = { + testRun: runInstance, + }; + traceInfo(`Running DEBUG pytest with arguments: ${testArgs} for workspace ${uri.fsPath} \r\n`); + await debugLauncher!.launchDebugger( + launchOptions, + () => { + serverCancel.cancel(); + }, + sessionOptions, + ); + } else if (useEnvExtension()) { + // For project-based execution, use the project's Python environment + // Otherwise, fall back to getting the environment from the URI + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (pythonEnv) { + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); + + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: runArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + proc.stdout.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.stderr.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.onExit((code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } else { + traceError(`Python Environment not found for: ${uri.fsPath}`); + } + } else { + // deferredTillExecClose is resolved when all stdout and stderr is read + const deferredTillExecClose: Deferred = utils.createTestingDeferred(); + // combine path to run script with run args + const scriptPath = path.join(fullPluginPath, 'vscode_pytest', 'run_pytest_script.py'); + const runArgs = [scriptPath, ...testArgs]; + traceInfo(`Running pytest with arguments: ${runArgs.join(' ')} for workspace ${uri.fsPath} \r\n`); + + let resultProc: ChildProcess | undefined; + + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing pytest subprocess for workspace ${uri.fsPath}`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose.resolve(); + serverCancel.cancel(); + } + }); + + const result = execService?.execObservable(runArgs, spawnOptions); + + // Take all output from the subprocess and add it to the test output channel. This will be the pytest output. + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + result?.proc?.stdout?.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + result?.proc?.on('exit', (code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + }); + + result?.proc?.on('close', (code, signal) => { + traceVerbose('Test run finished, subprocess closed.'); + // if the child has testIds then this is a run request + // if the child process exited with a non-zero exit code, then we need to send the error payload. + if (code !== 0) { + traceError( + `Subprocess closed unsuccessfully with exit code ${code} and signal ${signal} for workspace ${uri.fsPath}. Creating and sending error execution payload \n`, + ); + + if (runInstance) { + this.resultResolver?.resolveExecution( + utils.createExecutionErrorPayload(code, signal, testIds, cwd), + runInstance, + ); + } + } + + // deferredTillEOT is resolved when all data sent on stdout and stderr is received, close event is only called when this occurs + // due to the sync reading of the output. + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } + } catch (ex) { + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + } + + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; + return executionPayload; + } +} diff --git a/src/client/testing/testController/pytest/pytestHelpers.ts b/src/client/testing/testController/pytest/pytestHelpers.ts new file mode 100644 index 000000000000..c6e748fb85a7 --- /dev/null +++ b/src/client/testing/testController/pytest/pytestHelpers.ts @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import * as fs from 'fs'; +import { traceInfo, traceWarn } from '../../../logging'; +import { addValueIfKeyNotExist, hasSymlinkParent } from '../common/utils'; + +/** + * Checks if the current working directory contains a symlink and ensures --rootdir is set in pytest args. + * This is required for pytest to correctly resolve relative paths in symlinked directories. + */ +export async function handleSymlinkAndRootDir(cwd: string, pytestArgs: string[]): Promise { + const stats = await fs.promises.lstat(cwd); + const resolvedPath = await fs.promises.realpath(cwd); + let isSymbolicLink = false; + if (stats.isSymbolicLink()) { + isSymbolicLink = true; + traceWarn(`Working directory is a symbolic link: ${cwd} -> ${resolvedPath}`); + } else if (resolvedPath !== cwd) { + traceWarn( + `Working directory resolves to different path: ${cwd} -> ${resolvedPath}. Checking for symlinks in parent directories.`, + ); + isSymbolicLink = await hasSymlinkParent(cwd); + } + if (isSymbolicLink) { + traceWarn( + `Symlink detected in path. Adding '--rootdir=${cwd}' to pytest args to ensure correct path resolution.`, + ); + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + } + // if user has provided `--rootdir` then use that, otherwise add `cwd` + // root dir is required so pytest can find the relative paths and for symlinks + pytestArgs = addValueIfKeyNotExist(pytestArgs, '--rootdir', cwd); + return pytestArgs; +} + +/** + * Builds the environment variables required for pytest discovery. + * Sets PYTHONPATH to include the plugin path and TEST_RUN_PIPE for communication. + */ +export function buildPytestEnv( + envVars: { [key: string]: string | undefined } | undefined, + fullPluginPath: string, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + // get python path from mutable env, it contains process.env as well + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [fullPluginPath, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo( + `Environment variables set for pytest discovery: PYTHONPATH=${mutableEnv.PYTHONPATH}, TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`, + ); + return mutableEnv; +} diff --git a/src/client/testing/testController/serviceRegistry.ts b/src/client/testing/testController/serviceRegistry.ts new file mode 100644 index 000000000000..03bf883e8eb1 --- /dev/null +++ b/src/client/testing/testController/serviceRegistry.ts @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IExtensionSingleActivationService } from '../../activation/types'; +import { IServiceManager } from '../../ioc/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../common/constants'; +import { ITestFrameworkController, ITestController } from './common/types'; +import { PythonTestController } from './controller'; +import { PytestController } from './pytest/pytestController'; +import { UnittestController } from './unittest/unittestController'; + +export function registerTestControllerTypes(serviceManager: IServiceManager): void { + serviceManager.addSingleton(ITestFrameworkController, PytestController, PYTEST_PROVIDER); + + serviceManager.addSingleton( + ITestFrameworkController, + UnittestController, + UNITTEST_PROVIDER, + ); + serviceManager.addSingleton(ITestController, PythonTestController); + serviceManager.addBinding(ITestController, IExtensionSingleActivationService); +} diff --git a/src/client/testing/testController/unittest/testDiscoveryAdapter.ts b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts new file mode 100644 index 000000000000..558e01f3514d --- /dev/null +++ b/src/client/testing/testController/unittest/testDiscoveryAdapter.ts @@ -0,0 +1,211 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { CancellationToken, Disposable, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { ITestDiscoveryAdapter, ITestResultResolver } from '../common/types'; +import { IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { traceError, traceInfo, traceVerbose } from '../../../logging'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { createTestingDeferred } from '../common/utils'; +import { buildDiscoveryCommand, buildUnittestEnv as configureSubprocessEnv } from './unittestHelpers'; +import { cleanupOnCancellation, createProcessHandlers, setupDiscoveryPipe } from '../common/discoveryHelpers'; +import { ProjectAdapter } from '../common/projectAdapter'; + +/** + * Configures the subprocess environment for unittest discovery. + * @param envVarsService Service to retrieve environment variables + * @param uri Workspace URI + * @param discoveryPipeName Name of the discovery pipe to pass to the subprocess + * @returns Configured environment variables for the subprocess + */ +async function configureDiscoveryEnv( + envVarsService: IEnvironmentVariablesProvider | undefined, + uri: Uri, + discoveryPipeName: string, +): Promise { + const envVars = await envVarsService?.getEnvironmentVariables(uri); + const mutableEnv = configureSubprocessEnv(envVars, discoveryPipeName); + return mutableEnv; +} + +/** + * Wrapper class for unittest test discovery. + */ +export class UnittestTestDiscoveryAdapter implements ITestDiscoveryAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + async discoverTests( + uri: Uri, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // Setup discovery pipe and cancellation + const { + pipeName: discoveryPipeName, + cancellation: discoveryPipeCancellation, + tokenDisposable, + } = await setupDiscoveryPipe(this.resultResolver, token, uri); + + // Setup process handlers deferred (used by both execution paths) + const deferredTillExecClose = createTestingDeferred(); + + // Collect all disposables for cleanup in finally block + const disposables: Disposable[] = []; + if (tokenDisposable) { + disposables.push(tokenDisposable); + } + try { + // Build unittest command and arguments + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + const execArgs = buildDiscoveryCommand(unittestArgs, EXTENSION_ROOT_DIR); + traceVerbose(`Running unittest discovery with command: ${execArgs.join(' ')} for workspace ${uri.fsPath}.`); + + // Configure subprocess environment + const mutableEnv = await configureDiscoveryEnv(this.envVarsService, uri, discoveryPipeName); + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest discovery`, + ); + } + + // Setup process handlers (shared by both execution paths) + const handlers = createProcessHandlers('unittest', uri, cwd, this.resultResolver, deferredTillExecClose); + + // Execute using environment extension if available + if (useEnvExtension()) { + traceInfo(`Using environment extension for unittest discovery in workspace ${uri.fsPath}`); + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (!pythonEnv) { + traceError( + `Python environment not found for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + traceVerbose(`Using Python environment: ${JSON.stringify(pythonEnv)}`); + + const proc = await runInBackground(pythonEnv, { + cwd, + args: execArgs, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + traceInfo(`Started unittest discovery subprocess (environment extension) for workspace ${uri.fsPath}`); + + // Wire up cancellation and process events + const envExtCancellationHandler = token?.onCancellationRequested(() => { + cleanupOnCancellation('unittest', proc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (envExtCancellationHandler) { + disposables.push(envExtCancellationHandler); + } + proc.stdout.on('data', handlers.onStdout); + proc.stderr.on('data', handlers.onStderr); + proc.onExit((code, signal) => { + handlers.onExit(code, signal); + handlers.onClose(code, signal); + }); + + await deferredTillExecClose.promise; + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + return; + } + + // Execute using execution factory (fallback path) + traceInfo(`Using execution factory for unittest discovery in workspace ${uri.fsPath}`); + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: uri, + interpreter, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + if (!execService) { + traceError( + `Failed to create execution service for workspace ${uri.fsPath}. Cannot proceed with test discovery.`, + ); + deferredTillExecClose.resolve(); + return; + } + const execInfo = await execService.getExecutablePath(); + traceVerbose(`Using Python executable: ${execInfo} for workspace ${uri.fsPath}`); + + // Check for cancellation before spawning process + if (token?.isCancellationRequested) { + traceInfo(`Unittest discovery cancelled before spawning process for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + + const spawnOptions: SpawnOptions = { + cwd, + throwOnStdErr: true, + env: mutableEnv, + token, + }; + + let resultProc: ChildProcess | undefined; + + // Set up cancellation handler after all early return checks + const cancellationHandler = token?.onCancellationRequested(() => { + traceInfo(`Cancellation requested during unittest discovery for workspace ${uri.fsPath}`); + cleanupOnCancellation('unittest', resultProc, deferredTillExecClose, discoveryPipeCancellation, uri); + }); + if (cancellationHandler) { + disposables.push(cancellationHandler); + } + + try { + const result = execService.execObservable(execArgs, spawnOptions); + resultProc = result?.proc; + + if (!resultProc) { + traceError(`Failed to spawn unittest discovery subprocess for workspace ${uri.fsPath}`); + deferredTillExecClose.resolve(); + return; + } + traceInfo(`Started unittest discovery subprocess (execution factory) for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error spawning unittest discovery subprocess for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } + resultProc.stdout?.on('data', handlers.onStdout); + resultProc.stderr?.on('data', handlers.onStderr); + resultProc.on('exit', handlers.onExit); + resultProc.on('close', handlers.onClose); + + traceVerbose(`Waiting for unittest discovery subprocess to complete for workspace ${uri.fsPath}`); + await deferredTillExecClose.promise; + traceInfo(`Unittest discovery completed for workspace ${uri.fsPath}`); + } catch (error) { + traceError(`Error during unittest discovery for workspace ${uri.fsPath}: ${error}`); + deferredTillExecClose.resolve(); + throw error; + } finally { + traceVerbose(`Cleaning up unittest discovery resources for workspace ${uri.fsPath}`); + // Dispose all cancellation handlers and event subscriptions + disposables.forEach((d) => d.dispose()); + // Dispose the discovery pipe cancellation token + discoveryPipeCancellation.dispose(); + } + } +} diff --git a/src/client/testing/testController/unittest/testExecutionAdapter.ts b/src/client/testing/testController/unittest/testExecutionAdapter.ts new file mode 100644 index 000000000000..c7d21b768c5b --- /dev/null +++ b/src/client/testing/testController/unittest/testExecutionAdapter.ts @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { CancellationTokenSource, DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { ChildProcess } from 'child_process'; +import { IConfigurationService } from '../../../common/types'; +import { Deferred, createDeferred } from '../../../common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../constants'; +import { + ExecutionTestPayload, + ITestExecutionAdapter, + ITestResultResolver, + TestCommandOptions, + TestExecutionCommand, +} from '../common/types'; +import { traceError, traceInfo, traceLog, traceVerbose } from '../../../logging'; +import { fixLogLinesNoTrailing } from '../common/utils'; +import { EnvironmentVariables, IEnvironmentVariablesProvider } from '../../../common/variables/types'; +import { + ExecutionFactoryCreateWithEnvironmentOptions, + ExecutionResult, + IPythonExecutionFactory, + SpawnOptions, +} from '../../../common/process/types'; +import { ITestDebugLauncher, LaunchOptions } from '../../common/types'; +import { UNITTEST_PROVIDER } from '../../common/constants'; +import * as utils from '../common/utils'; +import { getEnvironment, runInBackground, useEnvExtension } from '../../../envExt/api.internal'; +import { PythonEnvironment } from '../../../pythonEnvironments/info'; +import { ProjectAdapter } from '../common/projectAdapter'; + +/** + * Wrapper Class for unittest test execution. This is where we call `runTestCommand`? + */ + +export class UnittestTestExecutionAdapter implements ITestExecutionAdapter { + constructor( + public configSettings: IConfigurationService, + private readonly resultResolver?: ITestResultResolver, + private readonly envVarsService?: IEnvironmentVariablesProvider, + ) {} + + public async runTests( + uri: Uri, + testIds: string[], + profileKind: boolean | TestRunProfileKind | undefined, + runInstance: TestRun, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + _interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + // deferredTillServerClose awaits named pipe server close + const deferredTillServerClose: Deferred = utils.createTestingDeferred(); + + // create callback to handle data received on the named pipe + const dataReceivedCallback = (data: ExecutionTestPayload) => { + if (runInstance && !runInstance.token.isCancellationRequested) { + this.resultResolver?.resolveExecution(data, runInstance); + } else { + traceError(`No run instance found, cannot resolve execution, for workspace ${uri.fsPath}.`); + } + }; + const cSource = new CancellationTokenSource(); + runInstance.token.onCancellationRequested(() => cSource.cancel()); + const name = await utils.startRunResultNamedPipe( + dataReceivedCallback, // callback to handle data received + deferredTillServerClose, // deferred to resolve when server closes + cSource.token, // token to cancel + ); + runInstance.token.onCancellationRequested(() => { + console.log(`Test run cancelled, resolving 'till TillAllServerClose' deferred for ${uri.fsPath}.`); + // if canceled, stop listening for results + deferredTillServerClose.resolve(); + }); + try { + await this.runTestsNew( + uri, + testIds, + name, + cSource, + runInstance, + profileKind, + executionFactory, + debugLauncher, + project, + ); + } catch (error) { + traceError(`Error in running unittest tests: ${error}`); + } finally { + await deferredTillServerClose.promise; + } + } + + private async runTestsNew( + uri: Uri, + testIds: string[], + resultNamedPipeName: string, + serverCancel: CancellationTokenSource, + runInstance: TestRun, + profileKind: boolean | TestRunProfileKind | undefined, + executionFactory: IPythonExecutionFactory, + debugLauncher?: ITestDebugLauncher, + project?: ProjectAdapter, + ): Promise { + const settings = this.configSettings.getSettings(uri); + const { unittestArgs } = settings.testing; + const cwd = settings.testing.cwd && settings.testing.cwd.length > 0 ? settings.testing.cwd : uri.fsPath; + + const command = buildExecutionCommand(unittestArgs); + let mutableEnv: EnvironmentVariables | undefined = await this.envVarsService?.getEnvironmentVariables(uri); + if (mutableEnv === undefined) { + mutableEnv = {} as EnvironmentVariables; + } + const pythonPathParts: string[] = mutableEnv.PYTHONPATH?.split(path.delimiter) ?? []; + const pythonPathCommand = [cwd, ...pythonPathParts].join(path.delimiter); + mutableEnv.PYTHONPATH = pythonPathCommand; + mutableEnv.TEST_RUN_PIPE = resultNamedPipeName; + + // Set PROJECT_ROOT_PATH for project-based testing (tells Python where to root the test tree) + if (project) { + mutableEnv.PROJECT_ROOT_PATH = project.projectUri.fsPath; + traceInfo( + `[test-by-project] Setting PROJECT_ROOT_PATH=${project.projectUri.fsPath} for unittest execution`, + ); + } + + if (profileKind && profileKind === TestRunProfileKind.Coverage) { + mutableEnv.COVERAGE_ENABLED = cwd; + } + + const options: TestCommandOptions = { + workspaceFolder: uri, + command, + cwd, + profileKind: typeof profileKind === 'boolean' ? undefined : profileKind, + testIds, + token: runInstance.token, + }; + traceLog(`Running UNITTEST execution for the following test ids: ${testIds}`); + + // create named pipe server to send test ids + const testIdsFileName = await utils.writeTestIdsFile(testIds); + mutableEnv.RUN_TEST_IDS_PIPE = testIdsFileName; + traceInfo( + `All environment variables set for unittest execution, PYTHONPATH: ${JSON.stringify( + mutableEnv.PYTHONPATH, + )}`, + ); + + const spawnOptions: SpawnOptions = { + token: options.token, + cwd: options.cwd, + throwOnStdErr: true, + env: mutableEnv, + }; + // Create the Python environment in which to execute the command. + const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { + allowEnvironmentFetchExceptions: false, + resource: options.workspaceFolder, + }; + const execService = await executionFactory.createActivatedEnvironment(creationOptions); + + const execInfo = await execService?.getExecutablePath(); + traceVerbose(`Executable path for unittest execution: ${execInfo}.`); + + const args = [options.command.script].concat(options.command.args); + + if (options.outChannel) { + options.outChannel.appendLine(`python ${args.join(' ')}`); + } + + try { + if (options.profileKind && options.profileKind === TestRunProfileKind.Debug) { + const launchOptions: LaunchOptions = { + cwd: options.cwd, + args, + token: options.token, + testProvider: UNITTEST_PROVIDER, + runTestIdsPort: testIdsFileName, + pytestPort: resultNamedPipeName, // change this from pytest + // Pass project for project-based debugging (Python path and session name derived from this) + project: project?.pythonProject, + }; + const sessionOptions: DebugSessionOptions = { + testRun: runInstance, + }; + traceInfo(`Running DEBUG unittest for workspace ${options.cwd} with arguments: ${args}\r\n`); + + if (debugLauncher === undefined) { + traceError('Debug launcher is not defined'); + throw new Error('Debug launcher is not defined'); + } + await debugLauncher.launchDebugger( + launchOptions, + () => { + serverCancel.cancel(); + }, + sessionOptions, + ); + } else if (useEnvExtension()) { + const pythonEnv = project?.pythonEnvironment ?? (await getEnvironment(uri)); + if (pythonEnv) { + traceInfo(`Running unittest with arguments: ${args.join(' ')} for workspace ${uri.fsPath} \r\n`); + const deferredTillExecClose = createDeferred(); + + const proc = await runInBackground(pythonEnv, { + cwd, + args, + env: (mutableEnv as unknown) as { [key: string]: string }, + }); + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${uri.fsPath}`); + proc.kill(); + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + proc.stdout.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.stderr.on('data', (data) => { + const out = utils.fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(out); + }); + proc.onExit((code, signal) => { + if (code !== 0) { + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} on workspace ${uri.fsPath}`, + ); + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } else { + traceError(`Python Environment not found for: ${uri.fsPath}`); + } + } else { + // This means it is running the test + traceInfo(`Running unittests for workspace ${cwd} with arguments: ${args}\r\n`); + + const deferredTillExecClose = createDeferred>(); + + let resultProc: ChildProcess | undefined; + + runInstance.token.onCancellationRequested(() => { + traceInfo(`Test run cancelled, killing unittest subprocess for workspace ${cwd}.`); + // if the resultProc exists just call kill on it which will handle resolving the ExecClose deferred, otherwise resolve the deferred here. + if (resultProc) { + resultProc?.kill(); + } else { + deferredTillExecClose?.resolve(); + serverCancel.cancel(); + } + }); + + const result = execService?.execObservable(args, spawnOptions); + resultProc = result?.proc; + + // Displays output to user and ensure the subprocess doesn't run into buffer overflow. + + result?.proc?.stdout?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(`${out}`); + }); + result?.proc?.stderr?.on('data', (data) => { + const out = fixLogLinesNoTrailing(data.toString()); + runInstance.appendOutput(`${out}`); + }); + + result?.proc?.on('exit', (code, signal) => { + // if the child has testIds then this is a run request + if (code !== 0 && testIds) { + // This occurs when we are running the test and there is an error which occurs. + + traceError( + `Subprocess exited unsuccessfully with exit code ${code} and signal ${signal} for workspace ${options.cwd}. Creating and sending error execution payload \n`, + ); + if (runInstance) { + this.resultResolver?.resolveExecution( + utils.createExecutionErrorPayload(code, signal, testIds, cwd), + runInstance, + ); + } + } + deferredTillExecClose.resolve(); + serverCancel.cancel(); + }); + await deferredTillExecClose.promise; + } + } catch (ex) { + traceError(`Error while running tests for workspace ${uri}: ${testIds}\r\n${ex}\r\n\r\n`); + return Promise.reject(ex); + } + // placeholder until after the rewrite is adopted + // TODO: remove after adoption. + const executionPayload: ExecutionTestPayload = { + cwd, + status: 'success', + error: '', + }; + return executionPayload; + } +} + +function buildExecutionCommand(args: string[]): TestExecutionCommand { + const executionScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'execution.py'); + + return { + script: executionScript, + args: ['--udiscovery', ...args], + }; +} diff --git a/src/client/testing/testController/unittest/unittestController.ts b/src/client/testing/testController/unittest/unittestController.ts new file mode 100644 index 000000000000..863f34abd514 --- /dev/null +++ b/src/client/testing/testController/unittest/unittestController.ts @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { CancellationToken, TestController, TestItem } from 'vscode'; +import { IWorkspaceService } from '../../../common/application/types'; +import { Deferred } from '../../../common/utils/async'; +import { ITestFrameworkController, RawDiscoveredTests, TestData } from '../common/types'; +import { getWorkspaceNode, updateTestItemFromRawData } from '../common/testItemUtilities'; + +@injectable() +export class UnittestController implements ITestFrameworkController { + private readonly testData: Map = new Map(); + + private discovering: Map> = new Map(); + + private idToRawData: Map = new Map(); + + constructor(@inject(IWorkspaceService) private readonly workspaceService: IWorkspaceService) {} + + public async resolveChildren( + testController: TestController, + item: TestItem, + token?: CancellationToken, + ): Promise { + const workspace = this.workspaceService.getWorkspaceFolder(item.uri); + if (workspace) { + // if we are still discovering then wait + const discovery = this.discovering.get(workspace.uri.fsPath); + if (discovery) { + await discovery.promise; + } + + // see if we have raw test data + const rawTestData = this.testData.get(workspace.uri.fsPath); + if (rawTestData) { + if (rawTestData.root === item.id) { + if (rawTestData.tests.length === 0) { + testController.items.delete(item.id); + return Promise.resolve(); + } + + if (rawTestData.tests.length > 0) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + item.id, + [rawTestData], + token, + ); + } else { + this.idToRawData.delete(item.id); + testController.items.delete(item.id); + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + await updateTestItemFromRawData( + item, + testController, + this.idToRawData, + workspaceNode.id, + [rawTestData], + token, + ); + } + } + } else { + const workspaceNode = getWorkspaceNode(item, this.idToRawData); + if (workspaceNode) { + testController.items.delete(workspaceNode.id); + } + } + } + return Promise.resolve(); + } +} diff --git a/src/client/testing/testController/unittest/unittestHelpers.ts b/src/client/testing/testController/unittest/unittestHelpers.ts new file mode 100644 index 000000000000..249a78dda7b7 --- /dev/null +++ b/src/client/testing/testController/unittest/unittestHelpers.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as path from 'path'; +import { traceInfo } from '../../../logging'; + +/** + * Builds the environment variables required for unittest discovery. + * Sets TEST_RUN_PIPE for communication. + */ +export function buildUnittestEnv( + envVars: { [key: string]: string | undefined } | undefined, + discoveryPipeName: string, +): { [key: string]: string | undefined } { + const mutableEnv = { + ...envVars, + }; + mutableEnv.TEST_RUN_PIPE = discoveryPipeName; + traceInfo(`Environment variables set for unittest discovery: TEST_RUN_PIPE=${mutableEnv.TEST_RUN_PIPE}`); + return mutableEnv; +} + +/** + * Builds the unittest discovery command. + */ +export function buildDiscoveryCommand(args: string[], extensionRootDir: string): string[] { + const discoveryScript = path.join(extensionRootDir, 'python_files', 'unittestadapter', 'discovery.py'); + return [discoveryScript, '--udiscovery', ...args]; +} diff --git a/src/client/testing/testController/workspaceTestAdapter.ts b/src/client/testing/testController/workspaceTestAdapter.ts new file mode 100644 index 000000000000..f17687732f57 --- /dev/null +++ b/src/client/testing/testController/workspaceTestAdapter.ts @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as util from 'util'; +import { CancellationToken, TestController, TestItem, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import { createDeferred, Deferred } from '../../common/utils/async'; +import { Testing } from '../../common/utils/localize'; +import { traceError } from '../../logging'; +import { sendTelemetryEvent } from '../../telemetry'; +import { EventName } from '../../telemetry/constants'; +import { TestProvider } from '../types'; +import { createErrorTestItem, getTestCaseNodes } from './common/testItemUtilities'; +import { ITestDiscoveryAdapter, ITestExecutionAdapter, ITestResultResolver } from './common/types'; +import { IPythonExecutionFactory } from '../../common/process/types'; +import { ITestDebugLauncher } from '../common/types'; +import { buildErrorNodeOptions } from './common/utils'; +import { PythonEnvironment } from '../../pythonEnvironments/info'; +import { ProjectAdapter } from './common/projectAdapter'; + +/** + * This class exposes a test-provider-agnostic way of discovering tests. + * + * It gets instantiated by the `PythonTestController` class in charge of reflecting test data in the UI, + * and then instantiates provider-specific adapters under the hood depending on settings. + * + * This class formats the JSON test data returned by the `[Unittest|Pytest]TestDiscoveryAdapter` into test UI elements, + * and uses them to insert/update/remove items in the `TestController` instance behind the testing UI whenever the `PythonTestController` requests a refresh. + */ +export class WorkspaceTestAdapter { + private discovering: Deferred | undefined; + + private executing: Deferred | undefined; + + constructor( + private testProvider: TestProvider, + private discoveryAdapter: ITestDiscoveryAdapter, + private executionAdapter: ITestExecutionAdapter, + private workspaceUri: Uri, + public resultResolver: ITestResultResolver, + ) {} + + public async executeTests( + testController: TestController, + runInstance: TestRun, + includes: TestItem[], + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + profileKind?: boolean | TestRunProfileKind, + debugLauncher?: ITestDebugLauncher, + interpreter?: PythonEnvironment, + project?: ProjectAdapter, + ): Promise { + if (this.executing) { + traceError('Test execution already in progress, not starting a new one.'); + return this.executing.promise; + } + + const deferred = createDeferred(); + this.executing = deferred; + + const testCaseNodes: TestItem[] = []; + const testCaseIdsSet = new Set(); + try { + // first fetch all the individual test Items that we necessarily want + includes.forEach((t) => { + const nodes = getTestCaseNodes(t); + testCaseNodes.push(...nodes); + }); + // iterate through testItems nodes and fetch their unittest runID to pass in as argument + testCaseNodes.forEach((node) => { + runInstance.started(node); // do the vscode ui test item start here before runtest + const runId = this.resultResolver.vsIdToRunId.get(node.id); + if (runId) { + testCaseIdsSet.add(runId); + } + }); + const testCaseIds = Array.from(testCaseIdsSet); + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test execution'); + } + await this.executionAdapter.runTests( + this.workspaceUri, + testCaseIds, + profileKind, + runInstance, + executionFactory, + debugLauncher, + interpreter, + project, + ); + deferred.resolve(); + } catch (ex) { + // handle token and telemetry here + sendTelemetryEvent(EventName.UNITTEST_RUN_ALL_FAILED, undefined); + + let cancel = token?.isCancellationRequested + ? Testing.cancelUnittestExecution + : Testing.errorUnittestExecution; + if (this.testProvider === 'pytest') { + cancel = token?.isCancellationRequested ? Testing.cancelPytestExecution : Testing.errorPytestExecution; + } + traceError(`${cancel}\r\n`, ex); + + // Also report on the test view + const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); + const errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + + deferred.reject(ex as Error); + } finally { + this.executing = undefined; + } + + return Promise.resolve(); + } + + public async discoverTests( + testController: TestController, + executionFactory: IPythonExecutionFactory, + token?: CancellationToken, + interpreter?: PythonEnvironment, + ): Promise { + sendTelemetryEvent(EventName.UNITTEST_DISCOVERING, undefined, { tool: this.testProvider }); + + // Discovery is expensive. If it is already running, use the existing promise. + if (this.discovering) { + traceError('Test discovery already in progress, not starting a new one.'); + return this.discovering.promise; + } + + const deferred = createDeferred(); + this.discovering = deferred; + + try { + if (executionFactory === undefined) { + throw new Error('Execution factory is required for test discovery'); + } + await this.discoveryAdapter.discoverTests(this.workspaceUri, executionFactory, token, interpreter); + deferred.resolve(); + } catch (ex) { + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: true }); + + let cancel = token?.isCancellationRequested + ? Testing.cancelUnittestDiscovery + : Testing.errorUnittestDiscovery; + if (this.testProvider === 'pytest') { + cancel = token?.isCancellationRequested ? Testing.cancelPytestDiscovery : Testing.errorPytestDiscovery; + } + + traceError(`${cancel} for workspace: ${this.workspaceUri} \r\n`, ex); + + // Report also on the test view. + const message = util.format(`${cancel} ${Testing.seePythonOutput}\r\n`, ex); + const options = buildErrorNodeOptions(this.workspaceUri, message, this.testProvider); + const errorNode = createErrorTestItem(testController, options); + testController.items.add(errorNode); + + return deferred.reject(ex as Error); + } finally { + // Discovery has finished running, we have the data, + // we don't need the deferred promise anymore. + this.discovering = undefined; + } + + sendTelemetryEvent(EventName.UNITTEST_DISCOVERY_DONE, undefined, { tool: this.testProvider, failed: false }); + return Promise.resolve(); + } + + /** + * Retrieves the current test provider instance. + * + * @returns {TestProvider} The instance of the test provider. + */ + public getTestProvider(): TestProvider { + return this.testProvider; + } +} diff --git a/src/client/testing/types.ts b/src/client/testing/types.ts index 442ff4d04be6..da308ee6998b 100644 --- a/src/client/testing/types.ts +++ b/src/client/testing/types.ts @@ -3,187 +3,15 @@ 'use strict'; -// tslint:disable-next-line:ordered-imports -import { - DiagnosticSeverity, Disposable, DocumentSymbolProvider, - Event, Location, ProviderResult, TextDocument, - TreeDataProvider, TreeItem, Uri, WorkspaceFolder -} from 'vscode'; -import { Product, Resource } from '../common/types'; -import { CommandSource } from './common/constants'; -import { - FlattenedTestFunction, ITestManager, ITestResultsService, - TestFile, TestFolder, TestFunction, TestRunOptions, Tests, - TestStatus, TestsToRun, TestSuite, UnitTestProduct -} from './common/types'; +import { Product } from '../common/types'; +import { TestSettingsPropertyNames } from './configuration/types'; -export const ITestConfigurationService = Symbol('ITestConfigurationService'); -export interface ITestConfigurationService { - displayTestFrameworkError(wkspace: Uri): Promise; - selectTestRunner(placeHolderMessage: string): Promise; - enableTest(wkspace: Uri, product: UnitTestProduct): Promise; - promptToEnableAndConfigureTestFramework(wkspace: Uri): Promise; -} - -export const ITestResultDisplay = Symbol('ITestResultDisplay'); - -export interface ITestResultDisplay extends Disposable { - enabled: boolean; - readonly onDidChange: Event; - displayProgressStatus(testRunResult: Promise, debug?: boolean): void; - displayDiscoverStatus(testDiscovery: Promise, quietMode?: boolean): Promise; -} - -export const ITestDisplay = Symbol('ITestDisplay'); -export interface ITestDisplay { - displayStopTestUI(workspace: Uri, message: string): void; - displayTestUI(cmdSource: CommandSource, wkspace: Uri): void; - selectTestFunction(rootDirectory: string, tests: Tests): Promise; - selectTestFile(rootDirectory: string, tests: Tests): Promise; - displayFunctionTestPickerUI(cmdSource: CommandSource, wkspace: Uri, rootDirectory: string, file: Uri, testFunctions: TestFunction[], debug?: boolean): void; -} - -export const ITestManagementService = Symbol('ITestManagementService'); -export interface ITestManagementService { - readonly onDidStatusChange: Event; - activate(symbolProvider: DocumentSymbolProvider): Promise; - getTestManager(displayTestNotConfiguredMessage: boolean, resource?: Uri): Promise; - discoverTestsForDocument(doc: TextDocument): Promise; - autoDiscoverTests(resource: Resource): Promise; - discoverTests(cmdSource: CommandSource, resource?: Uri, ignoreCache?: boolean, userInitiated?: boolean, quietMode?: boolean): Promise; - stopTests(resource: Uri): Promise; - displayStopUI(message: string): Promise; - displayUI(cmdSource: CommandSource): Promise; - displayPickerUI(cmdSource: CommandSource, file: Uri, testFunctions: TestFunction[], debug?: boolean): Promise; - runTestsImpl(cmdSource: CommandSource, resource?: Uri, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise; - runCurrentTestFile(cmdSource: CommandSource): Promise; - - selectAndRunTestFile(cmdSource: CommandSource): Promise; - - selectAndRunTestMethod(cmdSource: CommandSource, resource: Uri, debug?: boolean): Promise; - - viewOutput(cmdSource: CommandSource): void; -} - -export const ITestConfigSettingsService = Symbol('ITestConfigSettingsService'); -export interface ITestConfigSettingsService { - updateTestArgs(testDirectory: string | Uri, product: UnitTestProduct, args: string[]): Promise; - enable(testDirectory: string | Uri, product: UnitTestProduct): Promise; - disable(testDirectory: string | Uri, product: UnitTestProduct): Promise; -} - -export interface ITestConfigurationManager { - requiresUserToConfigure(wkspace: Uri): Promise; - configure(wkspace: Uri): Promise; - enable(): Promise; - disable(): Promise; -} - -export const ITestConfigurationManagerFactory = Symbol('ITestConfigurationManagerFactory'); -export interface ITestConfigurationManagerFactory { - create(wkspace: Uri, product: Product, cfg?: ITestConfigSettingsService): ITestConfigurationManager; -} - -export enum TestFilter { - removeTests = 'removeTests', - discovery = 'discovery', - runAll = 'runAll', - runSpecific = 'runSpecific', - debugAll = 'debugAll', - debugSpecific = 'debugSpecific' -} -export const IArgumentsService = Symbol('IArgumentsService'); -export interface IArgumentsService { - getKnownOptions(): { withArgs: string[]; withoutArgs: string[] }; - getOptionValue(args: string[], option: string): string | string[] | undefined; - filterArguments(args: string[], argumentToRemove: string[]): string[]; - // tslint:disable-next-line:unified-signatures - filterArguments(args: string[], filter: TestFilter): string[]; - getTestFolders(args: string[]): string[]; -} -export const IArgumentsHelper = Symbol('IArgumentsHelper'); -export interface IArgumentsHelper { - getOptionValues(args: string[], option: string): string | string[] | undefined; - filterArguments(args: string[], optionsWithArguments?: string[], optionsWithoutArguments?: string[]): string[]; - getPositionalArguments(args: string[], optionsWithArguments?: string[], optionsWithoutArguments?: string[]): string[]; -} - -export const ITestManagerRunner = Symbol('ITestManagerRunner'); -export interface ITestManagerRunner { - runTest(testResultsService: ITestResultsService, options: TestRunOptions, testManager: ITestManager): Promise; -} - -export const IUnitTestHelper = Symbol('IUnitTestHelper'); -export interface IUnitTestHelper { - getStartDirectory(args: string[]): string; - getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[]; -} - -export const ITestDiagnosticService = Symbol('ITestDiagnosticService'); -export interface ITestDiagnosticService { - getMessagePrefix(status: TestStatus): string | undefined; - getSeverity(unitTestSeverity: PythonTestMessageSeverity): DiagnosticSeverity | undefined; -} - -export interface IPythonTestMessage { - code: string | undefined; - message?: string; - severity: PythonTestMessageSeverity; - provider: string | undefined; - traceback?: string; - testTime: number; - status?: TestStatus; - locationStack?: ILocationStackFrameDetails[]; - testFilePath: string; -} -export enum PythonTestMessageSeverity { - Error, - Failure, - Skip, - Pass -} -export enum DiagnosticMessageType { - Error, - Fail, - Skipped, - Pass -} - -export interface ILocationStackFrameDetails { - location: Location; - lineText: string; -} - -export type WorkspaceTestStatus = { workspace: Uri; status: TestStatus }; - -export type TestDataItem = TestWorkspaceFolder | TestFolder | TestFile | TestSuite | TestFunction; - -export class TestWorkspaceFolder { - public status?: TestStatus; - public time?: number; - public functionsPassed?: number; - public functionsFailed?: number; - public functionsDidNotRun?: number; - public passed?: boolean; - constructor(public readonly workspaceFolder: WorkspaceFolder) { } - public get resource(): Uri { - return this.workspaceFolder.uri; - } - public get name(): string { - return this.workspaceFolder.name; - } -} - -export const ITestTreeViewProvider = Symbol('ITestTreeViewProvider'); -export interface ITestTreeViewProvider extends TreeDataProvider { - onDidChangeTreeData: Event; - getTreeItem(element: TestDataItem): Promise; - getChildren(element?: TestDataItem): ProviderResult; - refresh(resource: Uri): void; -} +export type TestProvider = 'pytest' | 'unittest'; -export const ITestDataItemResource = Symbol('ITestDataItemResource'); +// **************** +// interfaces -export interface ITestDataItemResource { - getResource(testData: Readonly): Uri; +export const ITestingService = Symbol('ITestingService'); +export interface ITestingService { + getSettingsPropertyNames(product: Product): TestSettingsPropertyNames; } diff --git a/src/client/testing/unittest/helper.ts b/src/client/testing/unittest/helper.ts deleted file mode 100644 index f89c4c287b39..000000000000 --- a/src/client/testing/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); - } - public getStartDirectory(args: string[]): string { - const shortValue = this.argsHelper.getOptionValues(args, '-s'); - if (typeof shortValue === 'string') { - return shortValue; - } - const longValue = this.argsHelper.getOptionValues(args, '--start-directory'); - if (typeof longValue === 'string') { - return longValue; - } - return '.'; - } - public getIdsOfTestsToRun(tests: Tests, testsToRun: TestsToRun): string[] { - const testIds: string[] = []; - if (testsToRun && testsToRun.testFolder) { - // Get test ids of files in these folders. - testsToRun.testFolder.forEach(folder => { - tests.testFiles.forEach(f => { - if (f.fullPath.startsWith(folder.name)) { - testIds.push(f.nameToRun); - } - }); - }); - } - if (testsToRun && testsToRun.testFile) { - testIds.push(...testsToRun.testFile.map(f => f.nameToRun)); - } - if (testsToRun && testsToRun.testSuite) { - testIds.push(...testsToRun.testSuite.map(f => f.nameToRun)); - } - if (testsToRun && testsToRun.testFunction) { - testIds.push(...testsToRun.testFunction.map(f => f.nameToRun)); - } - return testIds; - } -} diff --git a/src/client/testing/unittest/main.ts b/src/client/testing/unittest/main.ts deleted file mode 100644 index 71d74380b455..000000000000 --- a/src/client/testing/unittest/main.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Uri } from 'vscode'; -import { Product } from '../../common/types'; -import { noop } from '../../common/utils/misc'; -import { IServiceContainer } from '../../ioc/types'; -import { CommandSource, UNITTEST_PROVIDER } from '../common/constants'; -import { BaseTestManager } from '../common/managers/baseTestManager'; -import { ITestsHelper, TestDiscoveryOptions, TestRunOptions, Tests, TestStatus, TestsToRun } from '../common/types'; -import { IArgumentsService, ITestManagerRunner, TestFilter } from '../types'; - -export class TestManager extends BaseTestManager { - private readonly argsService: IArgumentsService; - private readonly helper: ITestsHelper; - private readonly runner: ITestManagerRunner; - public get enabled() { - return this.settings.testing.unittestEnabled; - } - constructor(workspaceFolder: Uri, rootDirectory: string, serviceContainer: IServiceContainer) { - super(UNITTEST_PROVIDER, Product.unittest, workspaceFolder, rootDirectory, serviceContainer); - this.argsService = this.serviceContainer.get(IArgumentsService, this.testProvider); - this.helper = this.serviceContainer.get(ITestsHelper); - this.runner = this.serviceContainer.get(ITestManagerRunner, this.testProvider); - } - public configure() { - noop(); - } - public getDiscoveryOptions(ignoreCache: boolean): TestDiscoveryOptions { - const args = this.settings.testing.unittestArgs.slice(0); - return { - workspaceFolder: this.workspaceFolder, - cwd: this.rootDirectory, args, - token: this.testDiscoveryCancellationToken!, ignoreCache, - outChannel: this.outputChannel - }; - } - public async runTest(cmdSource: CommandSource, testsToRun?: TestsToRun, runFailedTests?: boolean, debug?: boolean): Promise { - if (runFailedTests === true && this.tests) { - testsToRun = { testFile: [], testFolder: [], testSuite: [], testFunction: [] }; - testsToRun.testFunction = this.tests.testFunctions.filter(fn => { - return fn.testFunction.status === TestStatus.Error || fn.testFunction.status === TestStatus.Fail; - }).map(fn => fn.testFunction); - } - return super.runTest(cmdSource, testsToRun, runFailedTests, debug); - } - public async runTestImpl(tests: Tests, testsToRun?: TestsToRun, _runFailedTests?: boolean, debug?: boolean): Promise { - let args: string[]; - - const runAllTests = this.helper.shouldRunAllTests(testsToRun); - if (debug) { - args = this.argsService.filterArguments(this.settings.testing.unittestArgs, runAllTests ? TestFilter.debugAll : TestFilter.debugSpecific); - } else { - args = this.argsService.filterArguments(this.settings.testing.unittestArgs, runAllTests ? TestFilter.runAll : TestFilter.runSpecific); - } - - const options: TestRunOptions = { - workspaceFolder: this.workspaceFolder, - cwd: this.rootDirectory, - tests, args, testsToRun, debug, - token: this.testRunnerCancellationToken!, - outChannel: this.outputChannel - }; - return this.runner.runTest(this.testResultsService, options, this); - } -} diff --git a/src/client/testing/unittest/runner.ts b/src/client/testing/unittest/runner.ts deleted file mode 100644 index 684e7d095dfb..000000000000 --- a/src/client/testing/unittest/runner.ts +++ /dev/null @@ -1,211 +0,0 @@ -'use strict'; - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../common/constants'; -import { IDisposableRegistry, 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(); -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; - - constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { - this.argsHelper = serviceContainer.get(IArgumentsHelper); - this.testRunner = serviceContainer.get(ITestRunner); - this.server = this.serviceContainer.get(IUnitTestSocketServer); - this.logger = this.serviceContainer.get(ILogger); - this.helper = this.serviceContainer.get(IUnitTestHelper); - this.serviceContainer.get(IDisposableRegistry).push(this.server); - } - - // tslint:disable-next-line:max-func-body-length - public async runTest(testResultsService: ITestResultsService, options: TestRunOptions, testManager: ITestManager): Promise { - if (this.busy && !this.busy.completed) { - return this.busy.promise; - } - this.busy = createDeferred(); - - options.tests.summary.errors = 0; - options.tests.summary.failures = 0; - options.tests.summary.passed = 0; - options.tests.summary.skipped = 0; - let failFast = false; - 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; - switch (test.testFunction.status) { - case TestStatus.Error: - case TestStatus.Fail: { - test.testFunction.passed = false; - break; - } - case TestStatus.Pass: { - test.testFunction.passed = true; - break; - } - default: { - test.testFunction.passed = undefined; - } - } - test.testFunction.message = data.message; - test.testFunction.traceback = data.traceback; - options.tests.summary[statusDetails.summaryProperty] += 1; - - if (failFast && (statusDetails.summaryProperty === 'failures' || statusDetails.summaryProperty === 'errors')) { - testManager.stop(); - } - } else { - if (statusDetails) { - options.tests.summary[statusDetails.summaryProperty] += 1; - } - } - }); - - const port = await this.server.start(); - const testPaths: string[] = this.helper.getIdsOfTestsToRun(options.tests, options.testsToRun!); - for (let counter = 0; counter < testPaths.length; counter += 1) { - testPaths[counter] = `-t${testPaths[counter].trim()}`; - } - - const runTestInternal = async (testFile: string = '', testId: string = '') => { - let testArgs = this.buildTestArgs(options.args); - failFast = testArgs.indexOf('--uf') >= 0; - testArgs = testArgs.filter(arg => arg !== '--uf'); - - testArgs.push(`--result-port=${port}`); - if (testId.length > 0) { - testArgs.push(`-t${testId}`); - } - if (testFile.length > 0) { - testArgs.push(`--testFile=${testFile}`); - } - if (options.debug === true) { - const debugLauncher = this.serviceContainer.get(ITestDebugLauncher); - testArgs.push('--debug'); - const launchOptions: LaunchOptions = { cwd: options.cwd, args: testArgs, token: options.token, outChannel: options.outChannel, testProvider: UNITTEST_PROVIDER }; - return debugLauncher.launchDebugger(launchOptions); - } else { - const 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 { - // Ok, the test runner can only work with one test at a time. - if (options.testsToRun) { - if (Array.isArray(options.testsToRun.testFile)) { - for (const testFile of options.testsToRun.testFile) { - await runTestInternal(testFile.fullPath, testFile.nameToRun); - } - } - if (Array.isArray(options.testsToRun.testSuite)) { - for (const testSuite of options.testsToRun.testSuite) { - const item = options.tests.testSuites.find(t => t.testSuite === testSuite); - if (item) { - const testFileName = item.parentTestFile.fullPath; - await runTestInternal(testFileName, testSuite.nameToRun); - } - } - } - if (Array.isArray(options.testsToRun.testFunction)) { - for (const testFn of options.testsToRun.testFunction) { - const item = options.tests.testFunctions.find(t => t.testFunction === testFn); - if (item) { - const testFileName = item.parentTestFile.fullPath; - await runTestInternal(testFileName, testFn.nameToRun); - } - } - } - - await this.removeListenersAfter(Promise.resolve()); - } - - } - - testResultsService.updateResults(options.tests); - this.busy.resolve(options.tests); - return options.tests; - } - - // remove all the listeners from the server after all tests are complete, - // and just pass the promise `after` through as we do not want to get in - // the way here. - // tslint:disable-next-line:no-any - private async removeListenersAfter(after: Promise): Promise { - return after - .then(() => this.server.removeAllListeners()) - .catch((err) => { - this.server.removeAllListeners(); - throw err; // keep propagating this downward - }); - } - - private buildTestArgs(args: string[]): string[] { - const startTestDiscoveryDirectory = this.helper.getStartDirectory(args); - let pattern = 'test*.py'; - const shortValue = this.argsHelper.getOptionValues(args, '-p'); - const longValueValue = this.argsHelper.getOptionValues(args, '-pattern'); - if (typeof shortValue === 'string') { - pattern = shortValue; - } else if (typeof longValueValue === 'string') { - pattern = longValueValue; - } - const failFast = args.some(arg => arg.trim() === '-f' || arg.trim() === '--failfast'); - const verbosity = args.some(arg => arg.trim().indexOf('-v') === 0) ? 2 : 1; - const testArgs = [`--us=${startTestDiscoveryDirectory}`, `--up=${pattern}`, `--uvInt=${verbosity}`]; - if (failFast) { - testArgs.push('--uf'); - } - return testArgs; - } -} diff --git a/src/client/testing/unittest/services/argsService.ts b/src/client/testing/unittest/services/argsService.ts deleted file mode 100644 index 26b530da23d7..000000000000 --- a/src/client/testing/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); - } - public getKnownOptions(): { withArgs: string[]; withoutArgs: string[] } { - return { - withArgs: OptionsWithArguments, - withoutArgs: OptionsWithoutArguments - }; - } - public getOptionValue(args: string[], option: string): string | string[] | undefined { - return this.helper.getOptionValues(args, option); - } - public filterArguments(args: string[], argumentToRemoveOrFilter: string[] | TestFilter): string[] { - const optionsWithoutArgsToRemove: string[] = []; - const optionsWithArgsToRemove: string[] = []; - // Positional arguments in pytest positional args are test directories and files. - // So if we want to run a specific test, then remove positional args. - let removePositionalArgs = false; - if (Array.isArray(argumentToRemoveOrFilter)) { - argumentToRemoveOrFilter.forEach(item => { - if (OptionsWithArguments.indexOf(item) >= 0) { - optionsWithArgsToRemove.push(item); - } - if (OptionsWithoutArguments.indexOf(item) >= 0) { - optionsWithoutArgsToRemove.push(item); - } - }); - } else { - removePositionalArgs = true; - } - - let filteredArgs = args.slice(); - if (removePositionalArgs) { - const positionalArgs = this.helper.getPositionalArguments(filteredArgs, OptionsWithArguments, OptionsWithoutArguments); - filteredArgs = filteredArgs.filter(item => positionalArgs.indexOf(item) === -1); - } - return this.helper.filterArguments(filteredArgs, optionsWithArgsToRemove, optionsWithoutArgsToRemove); - } - public getTestFolders(args: string[]): string[] { - const shortValue = this.helper.getOptionValues(args, '-s'); - if (typeof shortValue === 'string') { - return [shortValue]; - } - const longValue = this.helper.getOptionValues(args, '--start-directory'); - if (typeof longValue === 'string') { - return [longValue]; - } - return ['.']; - } -} diff --git a/src/client/testing/unittest/services/discoveryService.ts b/src/client/testing/unittest/services/discoveryService.ts deleted file mode 100644 index fdf28cff3c93..000000000000 --- a/src/client/testing/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); - this.runner = serviceContainer.get(ITestRunner); - } - public async discoverTests(options: TestDiscoveryOptions): Promise { - 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('cancelled'); - } - - return this.testParser.parse(data, unitTestOptions); - } - public getDiscoveryScript(options: TestDiscoveryOptions): string { - const unitTestOptions = this.translateOptions(options); - return ` -import unittest -loader = unittest.TestLoader() -suites = loader.discover("${unitTestOptions.startDirectory}", pattern="${unitTestOptions.pattern}") -print("start") #Don't remove this line -for suite in suites._tests: - for cls in suite._tests: - try: - for m in cls._tests: - print(m.id()) - except: - pass`; - } - public translateOptions(options: TestDiscoveryOptions): UnitTestDiscoveryOptions { - return { - ...options, - startDirectory: this.getStartDirectory(options), - pattern: this.getTestPattern(options) - }; - } - private getStartDirectory(options: TestDiscoveryOptions) { - const shortValue = this.argsHelper.getOptionValues(options.args, '-s'); - if (typeof shortValue === 'string') { - return shortValue; - } - const longValue = this.argsHelper.getOptionValues(options.args, '--start-directory'); - if (typeof longValue === 'string') { - return longValue; - } - return '.'; - } - private getTestPattern(options: TestDiscoveryOptions) { - const shortValue = this.argsHelper.getOptionValues(options.args, '-p'); - if (typeof shortValue === 'string') { - return shortValue; - } - const longValue = this.argsHelper.getOptionValues(options.args, '--pattern'); - if (typeof longValue === 'string') { - return longValue; - } - return 'test*.py'; - } -} diff --git a/src/client/testing/unittest/services/parserService.ts b/src/client/testing/unittest/services/parserService.ts deleted file mode 100644 index 14ec5831cbe2..000000000000 --- a/src/client/testing/unittest/services/parserService.ts +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { - ITestsHelper, ITestsParser, TestFile, - TestFunction, Tests, TestStatus, - UnitTestParserOptions -} from '../../common/types'; - -@injectable() -export class TestsParser implements ITestsParser { - constructor(@inject(ITestsHelper) private testsHelper: ITestsHelper) { } - public parse(content: string, options: UnitTestParserOptions): Tests { - const testIds = this.getTestIds(content); - let testsDirectory = options.cwd; - if (options.startDirectory.length > 1) { - testsDirectory = path.isAbsolute(options.startDirectory) ? options.startDirectory : path.resolve(options.cwd, options.startDirectory); - } - return this.parseTestIds(options.cwd, testsDirectory, testIds); - } - private getTestIds(content: string): string[] { - let startedCollecting = false; - return content.split(/\r?\n/g) - .map(line => { - if (!startedCollecting) { - if (line === 'start') { - startedCollecting = true; - } - return ''; - } - return line.trim(); - }) - .filter(line => line.length > 0); - } - private parseTestIds(workspaceDirectory: string, testsDirectory: string, testIds: string[]): Tests { - const testFiles: TestFile[] = []; - testIds.forEach(testId => this.addTestId(testsDirectory, testId, testFiles)); - - return this.testsHelper.flattenTestFiles(testFiles, workspaceDirectory); - } - - /** - * Add the test Ids into the array provided. - * TestIds are fully qualified including the method names. - * E.g. tone_test.Failing2Tests.test_failure - * Where tone_test = folder, Failing2Tests = class/suite, test_failure = method. - * @private - * @param {string} rootDirectory - * @param {string[]} 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()!; - const resource = Uri.file(rootDirectory); - - // Check if we already have this test file - let testFile = testFiles.find(test => test.fullPath === filePath); - if (!testFile) { - testFile = { - resource, - name: path.basename(filePath), - fullPath: filePath, - functions: [], - suites: [], - nameToRun: `${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 = { - resource, - name: className, - functions: [], - suites: [], - isUnitTest: true, - isInstance: false, - nameToRun: suiteToRun, - xmlName: '', - status: TestStatus.Idle, - time: 0 - }; - testFile.suites.push(testSuite!); - } - - const testFunction: TestFunction = { - resource, - name: functionName, - nameToRun: testId, - status: TestStatus.Idle, - time: 0 - }; - - testSuite!.functions.push(testFunction); - } -} diff --git a/src/client/testing/unittest/socketServer.ts b/src/client/testing/unittest/socketServer.ts deleted file mode 100644 index 916643e1867f..000000000000 --- a/src/client/testing/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:variable-name no-any -const MaxConnections = 100; - -@injectable() -export class UnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { - private server?: net.Server; - private startedDef?: Deferred; - private sockets: net.Socket[] = []; - private ipcBuffer: string = ''; - constructor() { - super(); - } - public get clientsConnected(): boolean { - return this.sockets.length > 0; - } - public dispose() { - this.stop(); - } - public stop() { - if (this.server) { - this.server!.close(); - this.server = undefined; - } - } - public start(options: { port?: number; host?: string } = { port: 0, host: 'localhost' }): Promise { - this.ipcBuffer = ''; - this.startedDef = createDeferred(); - this.server = net.createServer(this.connectionListener.bind(this)); - this.server!.maxConnections = MaxConnections; - this.server!.on('error', (err) => { - if (this.startedDef) { - this.startedDef.reject(err); - this.startedDef = undefined; - } - this.emit('error', err); - }); - this.log('starting server as', 'TCP'); - options.port = typeof options.port === 'number' ? options.port! : 0; - options.host = typeof options.host === 'string' && options.host!.trim().length > 0 ? options.host!.trim() : 'localhost'; - this.server!.listen(options, (socket: net.Socket) => { - this.startedDef!.resolve(this.server!.address().port); - this.startedDef = undefined; - this.emit('start', socket); - }); - return this.startedDef!.promise; - } - - private connectionListener(socket: net.Socket) { - this.sockets.push(socket); - socket.setEncoding('utf8'); - this.log('## socket connection to server detected ##'); - socket.on('close', () => { - this.ipcBuffer = ''; - this.onCloseSocket(); - }); - socket.on('error', (err) => { - this.log('server socket error', err); - this.emit('error', err); - }); - socket.on('data', (data) => { - const sock = socket; - // Assume we have just one client socket connection - let dataStr = this.ipcBuffer += data; - - // tslint:disable-next-line:no-constant-condition - while (true) { - const startIndex = dataStr.indexOf('{'); - if (startIndex === -1) { - return; - } - const lengthOfMessage = parseInt(dataStr.slice(dataStr.indexOf(':') + 1, dataStr.indexOf('{')).trim(), 10); - if (dataStr.length < startIndex + lengthOfMessage) { - return; - } - // tslint:disable-next-line:no-any - let message: any; - try { - message = JSON.parse(dataStr.substring(startIndex, lengthOfMessage + startIndex)); - } catch (jsonErr) { - this.emit('error', jsonErr); - return; - } - dataStr = this.ipcBuffer = dataStr.substring(startIndex + lengthOfMessage); - this.emit(message.event, message.body, sock); - } - }); - this.emit('connect', socket); - } - private log(message: string, ...data: any[]) { - this.emit('log', message, ...data); - } - private onCloseSocket() { - // tslint:disable-next-line:one-variable-per-declaration - for (let i = 0, count = this.sockets.length; i < count; i += 1) { - const socket = this.sockets[i]; - let destroyedSocketId = false; - if (socket && socket.readable) { - continue; - } - // tslint:disable-next-line:no-any prefer-type-cast - if ((socket as any).id) { - // tslint:disable-next-line:no-any prefer-type-cast - destroyedSocketId = (socket as any).id; - } - this.log('socket disconnected', destroyedSocketId.toString()); - if (socket && socket.destroy) { - socket.destroy(); - } - this.sockets.splice(i, 1); - this.emit('socket.disconnected', socket, destroyedSocketId); - return; - } - } -} diff --git a/src/client/testing/unittest/testConfigurationManager.ts b/src/client/testing/unittest/testConfigurationManager.ts deleted file mode 100644 index 9f0792ffbc43..000000000000 --- a/src/client/testing/unittest/testConfigurationManager.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Uri } from 'vscode'; -import { Product } from '../../common/types'; -import { IServiceContainer } from '../../ioc/types'; -import { TestConfigurationManager } from '../common/managers/testConfigurationManager'; -import { ITestConfigSettingsService } from '../types'; - -export class ConfigurationManager extends TestConfigurationManager { - constructor( - workspace: Uri, - serviceContainer: IServiceContainer, - cfg?: ITestConfigSettingsService - ) { - super(workspace, Product.unittest, serviceContainer, cfg); - } - public async requiresUserToConfigure(_wkspace: Uri): Promise { - return true; - } - public async configure(wkspace: Uri) { - const args = ['-v']; - const subDirs = await this.getTestDirs(wkspace.fsPath); - const testDir = await this.selectTestDir(wkspace.fsPath, subDirs); - args.push('-s'); - if (typeof testDir === 'string' && testDir !== '.') { - args.push(`./${testDir}`); - } else { - args.push('.'); - } - - const testfilePattern = await this.selectTestFilePattern(); - args.push('-p'); - if (typeof testfilePattern === 'string') { - args.push(testfilePattern); - } else { - args.push('test*.py'); - } - await this.testConfigSettingsService.updateTestArgs(wkspace.fsPath, Product.unittest, args); - } -} diff --git a/src/client/testing/utils.ts b/src/client/testing/utils.ts new file mode 100644 index 000000000000..c1027d4a8dc1 --- /dev/null +++ b/src/client/testing/utils.ts @@ -0,0 +1,49 @@ +import { TestItem, env } from 'vscode'; +import { traceLog } from '../logging'; + +export async function writeTestIdToClipboard(testItem: TestItem): Promise { + if (testItem && typeof testItem.id === 'string') { + if (testItem.id.includes('\\') && testItem.id.indexOf('::') === -1) { + // Convert the id to a module.class.method format as this is a unittest + const moduleClassMethod = idToModuleClassMethod(testItem.id); + if (moduleClassMethod) { + await env.clipboard.writeText(moduleClassMethod); + traceLog('Testing: Copied test id to clipboard, id: ' + moduleClassMethod); + return; + } + } + // Otherwise use the id as is for pytest + await clipboardWriteText(testItem.id); + traceLog('Testing: Copied test id to clipboard, id: ' + testItem.id); + } +} + +export function idToModuleClassMethod(id: string): string | undefined { + // Split by backslash + const parts = id.split('\\'); + if (parts.length === 1) { + // Only one part, likely a parent folder or file + return parts[0]; + } + if (parts.length === 2) { + // Two parts: filePath and className + const [filePath, className] = parts.slice(-2); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}`; + } + // Three or more parts: filePath, className, methodName + const [filePath, className, methodName] = parts.slice(-3); + const fileName = filePath.split(/[\\/]/).pop(); + if (!fileName) { + return undefined; + } + const module = fileName.replace(/\.py$/, ''); + return `${module}.${className}.${methodName}`; +} +export function clipboardWriteText(text: string): Thenable { + return env.clipboard.writeText(text); +} diff --git a/src/client/typeFormatters/blockFormatProvider.ts b/src/client/typeFormatters/blockFormatProvider.ts deleted file mode 100644 index 8f1d9fb7bbf3..000000000000 --- a/src/client/typeFormatters/blockFormatProvider.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { - CancellationToken, FormattingOptions, OnTypeFormattingEditProvider, - Position, TextDocument, TextEdit -} from 'vscode'; -import { CodeBlockFormatProvider } from './codeBlockFormatProvider'; -import { - ASYNC_DEF_REGEX, ASYNC_FOR_IN_REGEX, CLASS_REGEX, DEF_REGEX, - ELIF_REGEX, ELSE_REGEX, EXCEPT_REGEX, FINALLY_REGEX, - FOR_IN_REGEX, IF_REGEX, TRY_REGEX, WHILE_REGEX - } from './contracts'; - -export class BlockFormatProviders implements OnTypeFormattingEditProvider { - private providers: CodeBlockFormatProvider[]; - constructor() { - this.providers = []; - const boundaryBlocks = [ - DEF_REGEX, - ASYNC_DEF_REGEX, - CLASS_REGEX - ]; - - const elseParentBlocks = [ - IF_REGEX, - ELIF_REGEX, - FOR_IN_REGEX, - ASYNC_FOR_IN_REGEX, - WHILE_REGEX, - TRY_REGEX, - EXCEPT_REGEX - ]; - this.providers.push(new CodeBlockFormatProvider(ELSE_REGEX, elseParentBlocks, boundaryBlocks)); - - const elifParentBlocks = [ - IF_REGEX, - ELIF_REGEX - ]; - this.providers.push(new CodeBlockFormatProvider(ELIF_REGEX, elifParentBlocks, boundaryBlocks)); - - const exceptParentBlocks = [ - TRY_REGEX, - EXCEPT_REGEX - ]; - this.providers.push(new CodeBlockFormatProvider(EXCEPT_REGEX, exceptParentBlocks, boundaryBlocks)); - - const finallyParentBlocks = [ - TRY_REGEX, - EXCEPT_REGEX - ]; - this.providers.push(new CodeBlockFormatProvider(FINALLY_REGEX, finallyParentBlocks, boundaryBlocks)); - } - - public provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, _token: CancellationToken): TextEdit[] { - if (position.line === 0) { - return []; - } - - const currentLine = document.lineAt(position.line); - const prevousLine = document.lineAt(position.line - 1); - - // We're only interested in cases where the current block is at the same indentation level as the previous line - // E.g. if we have an if..else block, generally the else statement would be at the same level as the code in the if... - if (currentLine.firstNonWhitespaceCharacterIndex !== prevousLine.firstNonWhitespaceCharacterIndex) { - return []; - } - - const currentLineText = currentLine.text; - const provider = this.providers.find(p => p.canProvideEdits(currentLineText)); - if (provider) { - return provider.provideEdits(document, position, ch, options, currentLine); - } - - return []; - } -} diff --git a/src/client/typeFormatters/codeBlockFormatProvider.ts b/src/client/typeFormatters/codeBlockFormatProvider.ts deleted file mode 100644 index 63c302796ed6..000000000000 --- a/src/client/typeFormatters/codeBlockFormatProvider.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - FormattingOptions, Position, Range, TextDocument, TextEdit, TextLine -} from 'vscode'; -import { BlockRegEx } from './contracts'; - -export class CodeBlockFormatProvider { - constructor(private blockRegExp: BlockRegEx, private previousBlockRegExps: BlockRegEx[], private boundaryRegExps: BlockRegEx[]) { - } - public canProvideEdits(line: string): boolean { - return this.blockRegExp.test(line); - } - - public provideEdits(document: TextDocument, position: Position, _ch: string, options: FormattingOptions, line: TextLine): TextEdit[] { - // We can have else for the following blocks: - // if: - // elif x: - // for x in y: - // while x: - - // We need to find a block statement that is less than or equal to this statement block (but not greater) - for (let lineNumber = position.line - 1; lineNumber >= 0; lineNumber -= 1) { - const prevLine = document.lineAt(lineNumber); - const prevLineText = prevLine.text; - - // Oops, we've reached a boundary (like the function or class definition) - // Get out of here - if (this.boundaryRegExps.some(value => value.test(prevLineText))) { - return []; - } - - const blockRegEx = this.previousBlockRegExps.find(value => value.test(prevLineText)); - if (!blockRegEx) { - continue; - } - - const startOfBlockInLine = prevLine.firstNonWhitespaceCharacterIndex; - if (startOfBlockInLine > line.firstNonWhitespaceCharacterIndex) { - continue; - } - - const startPosition = new Position(position.line, 0); - const endPosition = new Position(position.line, line.firstNonWhitespaceCharacterIndex - startOfBlockInLine); - - if (startPosition.isEqual(endPosition)) { - // current block cannot be at the same level as a preivous block - continue; - } - - if (options.insertSpaces) { - return [ - TextEdit.delete(new Range(startPosition, endPosition)) - ]; - } else { - // Delete everything before the block and insert the same characters we have in the previous block - const prefixOfPreviousBlock = prevLineText.substring(0, startOfBlockInLine); - - const startDeletePosition = new Position(position.line, 0); - const endDeletePosition = new Position(position.line, line.firstNonWhitespaceCharacterIndex); - - return [ - TextEdit.delete(new Range(startDeletePosition, endDeletePosition)), - TextEdit.insert(startDeletePosition, prefixOfPreviousBlock) - ]; - } - } - - return []; - } -} diff --git a/src/client/typeFormatters/contracts.ts b/src/client/typeFormatters/contracts.ts deleted file mode 100644 index ed7ba30b41d0..000000000000 --- a/src/client/typeFormatters/contracts.ts +++ /dev/null @@ -1,23 +0,0 @@ -export class BlockRegEx { - constructor(private regEx: RegExp, public startWord: String) { - - } - public test(value: string): boolean { - // Clear the cache - this.regEx.lastIndex = -1; - return this.regEx.test(value); - } -} - -export const IF_REGEX = new BlockRegEx(/^( |\t)*if +.*: *$/g, 'if'); -export const ELIF_REGEX = new BlockRegEx(/^( |\t)*elif +.*: *$/g, 'elif'); -export const ELSE_REGEX = new BlockRegEx(/^( |\t)*else *: *$/g, 'else'); -export const FOR_IN_REGEX = new BlockRegEx(/^( |\t)*for \w in .*: *$/g, 'for'); -export const ASYNC_FOR_IN_REGEX = new BlockRegEx(/^( |\t)*async *for \w in .*: *$/g, 'for'); -export const WHILE_REGEX = new BlockRegEx(/^( |\t)*while .*: *$/g, 'while'); -export const TRY_REGEX = new BlockRegEx(/^( |\t)*try *: *$/g, 'try'); -export const FINALLY_REGEX = new BlockRegEx(/^( |\t)*finally *: *$/g, 'finally'); -export const EXCEPT_REGEX = new BlockRegEx(/^( |\t)*except *\w* *(as)? *\w* *: *$/g, 'except'); -export const DEF_REGEX = new BlockRegEx(/^( |\t)*def \w *\(.*$/g, 'def'); -export const ASYNC_DEF_REGEX = new BlockRegEx(/^( |\t)*async *def \w *\(.*$/g, 'async'); -export const CLASS_REGEX = new BlockRegEx(/^( |\t)*class *\w* *.*: *$/g, 'class'); diff --git a/src/client/typeFormatters/dispatcher.ts b/src/client/typeFormatters/dispatcher.ts deleted file mode 100644 index 65fa2ac692cc..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: Record; - - constructor(providers: Record) { - this.providers = providers; - } - - public provideOnTypeFormattingEdits(document: TextDocument, position: Position, ch: string, options: FormattingOptions, cancellationToken: CancellationToken): ProviderResult { - const provider = this.providers[ch]; - - if (provider) { - return provider.provideOnTypeFormattingEdits(document, position, ch, options, cancellationToken); - } - - return []; - } - - public getTriggerCharacters(): { first: string; more: string[] } | undefined { - const keys = Object.keys(this.providers); - keys.sort(); // Make output deterministic - - const first = keys.shift(); - - if (first) { - return { - first: first, - more: keys - }; - } - - return undefined; - } -} diff --git a/src/client/typeFormatters/onEnterFormatter.ts b/src/client/typeFormatters/onEnterFormatter.ts deleted file mode 100644 index 3f61f4827bbe..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/workspaceSymbols/contracts.ts b/src/client/workspaceSymbols/contracts.ts deleted file mode 100644 index ae447a4e2fbb..000000000000 --- a/src/client/workspaceSymbols/contracts.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { Position, SymbolKind } from 'vscode'; - -export interface ITag { - fileName: string; - symbolName: string; - symbolKind: SymbolKind; - position: Position; - code: string; -} diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts deleted file mode 100644 index 4b682cfcd4c7..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 { EventName } from '../telemetry/constants'; - -export class Generator implements Disposable { - private optionsFile: string; - private disposables: Disposable[]; - private pythonSettings: IPythonSettings; - public get tagFilePath(): string { - return this.pythonSettings.workspaceSymbols.tagFilePath; - } - public get enabled(): boolean { - return this.pythonSettings.workspaceSymbols.enabled; - } - constructor(public readonly workspaceFolder: Uri, - private readonly output: OutputChannel, - private readonly appShell: IApplicationShell, - private readonly fs: IFileSystem, - private readonly processServiceFactory: IProcessServiceFactory, - configurationService: IConfigurationService) { - this.disposables = []; - this.optionsFile = path.join(EXTENSION_ROOT_DIR, 'resources', 'ctagOptions'); - this.pythonSettings = configurationService.getSettings(workspaceFolder); - } - - public dispose() { - this.disposables.forEach(d => d.dispose()); - } - public async generateWorkspaceTags(): Promise { - if (!this.pythonSettings.workspaceSymbols.enabled) { - return; - } - return this.generateTags({ directory: this.workspaceFolder.fsPath }); - } - private buildCmdArgs(): string[] { - const exclusions = this.pythonSettings.workspaceSymbols.exclusionPatterns; - const excludes = exclusions.length === 0 ? [] : exclusions.map(pattern => `--exclude=${pattern}`); - - return [`--options=${this.optionsFile}`, '--languages=Python'].concat(excludes); - } - @captureTelemetry(EventName.WORKSPACE_SYMBOLS_BUILD) - private async generateTags(source: { directory?: string; file?: string }): Promise { - const tagFile = path.normalize(this.pythonSettings.workspaceSymbols.tagFilePath); - const cmd = this.pythonSettings.workspaceSymbols.ctagsPath; - const args = this.buildCmdArgs(); - let outputFile = tagFile; - if (source.file && source.file.length > 0) { - source.directory = path.dirname(source.file); - } - - if (path.dirname(outputFile) === source.directory) { - outputFile = path.basename(outputFile); - } - const outputDir = path.dirname(outputFile); - if (!await this.fs.directoryExists(outputDir)) { - await this.fs.createDirectory(outputDir); - } - args.push('-o', outputFile, '.'); - this.output.appendLine(`${'-'.repeat(10)}Generating Tags${'-'.repeat(10)}`); - this.output.appendLine(`${cmd} ${args.join(' ')}`); - const promise = new Promise(async (resolve, reject) => { - try { - const processService = await this.processServiceFactory.create(); - const result = processService.execObservable(cmd, args, { cwd: source.directory }); - let errorMsg = ''; - result.out.subscribe(output => { - if (output.source === 'stderr') { - errorMsg += output.out; - } - this.output.append(output.out); - }, - reject, - () => { - if (errorMsg.length > 0) { - reject(new Error(errorMsg)); - } else { - resolve(); - } - }); - } catch (ex) { - reject(ex); - } - }); - - this.appShell.setStatusBarMessage('Generating Tags', promise); - - await promise; - } -} diff --git a/src/client/workspaceSymbols/main.ts b/src/client/workspaceSymbols/main.ts deleted file mode 100644 index 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(IOutputChannel, STANDARD_OUTPUT_CHANNEL); - this.commandMgr = this.serviceContainer.get(ICommandManager); - this.fs = this.serviceContainer.get(IFileSystem); - this.workspace = this.serviceContainer.get(IWorkspaceService); - this.processFactory = this.serviceContainer.get(IProcessServiceFactory); - this.appShell = this.serviceContainer.get(IApplicationShell); - this.configurationService = this.serviceContainer.get(IConfigurationService); - this.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[] { - if (token && token.isCancellationRequested) { - return []; - } - if (this.generators.length === 0) { - return []; - } - - let promptPromise: Promise; - let promptResponse: InstallerResponse; - return this.generators.map(async generator => { - if (!generator.enabled) { - return; - } - const exists = await this.fs.fileExists(generator.tagFilePath); - // If file doesn't exist, then run the ctag generator, - // or check if required to rebuild. - if (!rebuild && exists) { - return; - } - for (let counter = 0; counter < MAX_NUMBER_OF_ATTEMPTS_TO_INSTALL_AND_BUILD; counter += 1) { - try { - await generator.generateWorkspaceTags(); - return; - } catch (error) { - if (!isNotInstalledError(error)) { - 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); - 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 54bd50571360..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 { ITag } from './contracts'; - -// tslint:disable:no-require-imports no-var-requires no-suspicious-comment -// tslint:disable:no-any -// TODO: Turn these into imports. -const LineByLineReader = require('line-by-line'); -const NamedRegexp = require('named-js-regexp'); -const fuzzy = require('fuzzy'); - -const IsFileRegEx = /\tkind:file\tline:\d+$/g; -const LINE_REGEX = '(?\\w+)\\t(?.*)\\t\\/\\^(?.*)\\$\\/;"\\tkind:(?\\w+)\\tline:(?\\d+)$'; - -export interface IRegexGroup { - name: string; - file: string; - code: string; - type: string; - line: number; -} - -export function matchNamedRegEx(data: String, regex: String): IRegexGroup | null { - const compiledRegexp = NamedRegexp(regex, 'g'); - const rawMatch = compiledRegexp.exec(data); - if (rawMatch !== null) { - return rawMatch.groups(); - } - - return null; -} - -const CTagKinMapping = new Map(); -CTagKinMapping.set('_array', vscode.SymbolKind.Array); -CTagKinMapping.set('_boolean', vscode.SymbolKind.Boolean); -CTagKinMapping.set('_class', vscode.SymbolKind.Class); -CTagKinMapping.set('_classes', vscode.SymbolKind.Class); -CTagKinMapping.set('_constant', vscode.SymbolKind.Constant); -CTagKinMapping.set('_constants', vscode.SymbolKind.Constant); -CTagKinMapping.set('_constructor', vscode.SymbolKind.Constructor); -CTagKinMapping.set('_enum', vscode.SymbolKind.Enum); -CTagKinMapping.set('_enums', vscode.SymbolKind.Enum); -CTagKinMapping.set('_enumeration', vscode.SymbolKind.Enum); -CTagKinMapping.set('_enumerations', vscode.SymbolKind.Enum); -CTagKinMapping.set('_field', vscode.SymbolKind.Field); -CTagKinMapping.set('_fields', vscode.SymbolKind.Field); -CTagKinMapping.set('_file', vscode.SymbolKind.File); -CTagKinMapping.set('_files', vscode.SymbolKind.File); -CTagKinMapping.set('_function', vscode.SymbolKind.Function); -CTagKinMapping.set('_functions', vscode.SymbolKind.Function); -CTagKinMapping.set('_member', vscode.SymbolKind.Function); -CTagKinMapping.set('_interface', vscode.SymbolKind.Interface); -CTagKinMapping.set('_interfaces', vscode.SymbolKind.Interface); -CTagKinMapping.set('_key', vscode.SymbolKind.Key); -CTagKinMapping.set('_keys', vscode.SymbolKind.Key); -CTagKinMapping.set('_method', vscode.SymbolKind.Method); -CTagKinMapping.set('_methods', vscode.SymbolKind.Method); -CTagKinMapping.set('_module', vscode.SymbolKind.Module); -CTagKinMapping.set('_modules', vscode.SymbolKind.Module); -CTagKinMapping.set('_namespace', vscode.SymbolKind.Namespace); -CTagKinMapping.set('_namespaces', vscode.SymbolKind.Namespace); -CTagKinMapping.set('_number', vscode.SymbolKind.Number); -CTagKinMapping.set('_numbers', vscode.SymbolKind.Number); -CTagKinMapping.set('_null', vscode.SymbolKind.Null); -CTagKinMapping.set('_object', vscode.SymbolKind.Object); -CTagKinMapping.set('_package', vscode.SymbolKind.Package); -CTagKinMapping.set('_packages', vscode.SymbolKind.Package); -CTagKinMapping.set('_property', vscode.SymbolKind.Property); -CTagKinMapping.set('_properties', vscode.SymbolKind.Property); -CTagKinMapping.set('_objects', vscode.SymbolKind.Object); -CTagKinMapping.set('_string', vscode.SymbolKind.String); -CTagKinMapping.set('_variable', vscode.SymbolKind.Variable); -CTagKinMapping.set('_variables', vscode.SymbolKind.Variable); -CTagKinMapping.set('_projects', vscode.SymbolKind.Package); -CTagKinMapping.set('_defines', vscode.SymbolKind.Module); -CTagKinMapping.set('_labels', vscode.SymbolKind.Interface); -CTagKinMapping.set('_macros', vscode.SymbolKind.Function); -CTagKinMapping.set('_types (structs and records)', vscode.SymbolKind.Class); -CTagKinMapping.set('_subroutine', vscode.SymbolKind.Method); -CTagKinMapping.set('_subroutines', vscode.SymbolKind.Method); -CTagKinMapping.set('_types', vscode.SymbolKind.Class); -CTagKinMapping.set('_programs', vscode.SymbolKind.Class); -CTagKinMapping.set('_Object\'s method', vscode.SymbolKind.Method); -CTagKinMapping.set('_Module or functor', vscode.SymbolKind.Module); -CTagKinMapping.set('_Global variable', vscode.SymbolKind.Variable); -CTagKinMapping.set('_Type name', vscode.SymbolKind.Class); -CTagKinMapping.set('_A function', vscode.SymbolKind.Function); -CTagKinMapping.set('_A constructor', vscode.SymbolKind.Constructor); -CTagKinMapping.set('_An exception', vscode.SymbolKind.Class); -CTagKinMapping.set('_A \'structure\' field', vscode.SymbolKind.Field); -CTagKinMapping.set('_procedure', vscode.SymbolKind.Function); -CTagKinMapping.set('_procedures', vscode.SymbolKind.Function); -CTagKinMapping.set('_constant definitions', vscode.SymbolKind.Constant); -CTagKinMapping.set('_javascript functions', vscode.SymbolKind.Function); -CTagKinMapping.set('_singleton methods', vscode.SymbolKind.Method); - -const newValuesAndKeys = {}; -CTagKinMapping.forEach((value, key) => { - (newValuesAndKeys as any)[key.substring(1)] = value; -}); -Object.keys(newValuesAndKeys).forEach(key => { - CTagKinMapping.set(key, (newValuesAndKeys as any)[key]); -}); - -export function parseTags( - workspaceFolder: string, - tagFile: string, - query: string, - token: vscode.CancellationToken -): Promise { - return fsExistsAsync(tagFile).then(exists => { - if (!exists) { - return Promise.resolve([]); - } - - return new Promise((resolve, reject) => { - const lr = new LineByLineReader(tagFile); - let lineNumber = 0; - const tags: ITag[] = []; - - lr.on('error', (err: Error) => { - reject(err); - }); - - lr.on('line', (line: string) => { - lineNumber = lineNumber + 1; - if (token.isCancellationRequested) { - lr.close(); - return; - } - const tag = parseTagsLine(workspaceFolder, line, query); - if (tag) { - tags.push(tag); - } - if (tags.length >= 100) { - lr.close(); - } - }); - - lr.on('end', () => { - resolve(tags); - }); - }); - }); -} -function parseTagsLine(workspaceFolder: string, line: string, searchPattern: string): ITag | undefined { - if (IsFileRegEx.test(line)) { - return; - } - const match = matchNamedRegEx(line, LINE_REGEX); - if (!match) { - return; - } - if (!fuzzy.test(searchPattern, match.name)) { - return; - } - let file = match.file; - if (!path.isAbsolute(file)) { - file = path.resolve(workspaceFolder, '.vscode', file); - } - - const symbolKind = CTagKinMapping.get(match.type) || vscode.SymbolKind.Null; - return { - fileName: file, - code: match.code, - position: new vscode.Position(Number(match.line) - 1, 0), - symbolName: match.name, - symbolKind: symbolKind - }; -} diff --git a/src/client/workspaceSymbols/provider.ts b/src/client/workspaceSymbols/provider.ts deleted file mode 100644 index 019bcd25592d..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 { EventName } from '../telemetry/constants'; -import { Generator } from './generator'; -import { parseTags } from './parser'; - -export class WorkspaceSymbolProvider implements IWorspaceSymbolProvider { - public constructor( - private fs: IFileSystem, - private commands: ICommandManager, - private tagGenerators: Generator[] - ) { - } - - @captureTelemetry(EventName.WORKSPACE_SYMBOLS_GO_TO) - public async provideWorkspaceSymbols(query: string, token: CancellationToken): Promise { - if (this.tagGenerators.length === 0) { - return []; - } - const generatorsWithTagFiles = await Promise.all(this.tagGenerators.map(generator => this.fs.fileExists(generator.tagFilePath))); - if (generatorsWithTagFiles.filter(exists => exists).length !== this.tagGenerators.length) { - await this.commands.executeCommand(Commands.Build_Workspace_Symbols, true, token); - } - - const generators: Generator[] = []; - await Promise.all(this.tagGenerators.map(async generator => { - if (await this.fs.fileExists(generator.tagFilePath)) { - generators.push(generator); - } - })); - - const promises = generators - .filter(generator => generator !== undefined && generator.enabled) - .map(async generator => { - // load tags - const items = await parseTags(generator!.workspaceFolder.fsPath, generator!.tagFilePath, query, token); - 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/data-explorer/cellFormatter.css b/src/datascience-ui/data-explorer/cellFormatter.css deleted file mode 100644 index 10c63a79c8ac..000000000000 --- a/src/datascience-ui/data-explorer/cellFormatter.css +++ /dev/null @@ -1,8 +0,0 @@ -.number-formatter { - text-align: right; -} - -.cell-formatter { - /* Note: This is impacted by the RowHeightAdjustment in reactSlickGrid.tsx */ - margin: 0px 0px 0px 0px; -} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/cellFormatter.tsx b/src/datascience-ui/data-explorer/cellFormatter.tsx deleted file mode 100644 index a6f0e5afa594..000000000000 --- a/src/datascience-ui/data-explorer/cellFormatter.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './cellFormatter.css'; - -import * as React from 'react'; -import * as ReactDOMServer from 'react-dom/server'; -import { ISlickRow } from './reactSlickGrid'; - -interface ICellFormatterProps { - value: string | number | object | boolean; - columnDef: Slick.Column; -} - -class CellFormatter extends React.Component { - - constructor(props: ICellFormatterProps) { - super(props); - } - - public render() { - // Render based on type - if (this.props.value !== null && this.props.columnDef && this.props.columnDef.hasOwnProperty('type')) { - // tslint:disable-next-line: no-any - const columnType = (this.props.columnDef as any).type; - switch (columnType) { - case 'bool': - return this.renderBool(this.props.value as boolean); - break; - - case 'integer': - case 'float': - case 'int64': - case 'float64': - case 'number': - return this.renderNumber(this.props.value as number); - break; - - default: - break; - } - } - - // Otherwise an unknown type or a string - const val = this.props.value !== null ? this.props.value.toString() : ''; - return (

{val}
); - } - - private renderBool(value: boolean) { - return
{value.toString()}
; - } - - private renderNumber(value: number) { - const val = value.toString(); - return
{val}
; - } - -} - -// tslint:disable-next-line: no-any -export function cellFormatterFunc(_row: number, _cell: number, value: any, columnDef: Slick.Column, _dataContext: Slick.SlickData) : string { - return ReactDOMServer.renderToString(); -} diff --git a/src/datascience-ui/data-explorer/dataGridRow.tsx b/src/datascience-ui/data-explorer/dataGridRow.tsx deleted file mode 100644 index 151e259ccec7..000000000000 --- a/src/datascience-ui/data-explorer/dataGridRow.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as AdazzleReactDataGrid from 'react-data-grid'; - -export class DataGridRowRenderer extends AdazzleReactDataGrid.Row { - - // tslint:disable:no-any - constructor(props: any) { - super(props); - } - - public render = () => { - return super.render(); - // if (this.props.idx) { - // const style: React.CSSProperties = { - // color: this.props.idx % 2 ? 'red' : 'blue' - // }; - // return
{parent}
; - // } - } -} diff --git a/src/datascience-ui/data-explorer/emptyRowsView.tsx b/src/datascience-ui/data-explorer/emptyRowsView.tsx deleted file mode 100644 index e47a68fefcdd..000000000000 --- a/src/datascience-ui/data-explorer/emptyRowsView.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './emptyRowsView.css'; - -import * as React from 'react'; -import { getLocString } from '../react-common/locReactSide'; - -export interface IEmptyRowsProps { -} - -export const EmptyRows = (_props: IEmptyRowsProps) => { - const message = getLocString('DataScience.noRowsInDataViewer', 'No rows match current filter'); - - return ( -
- {message} -
- ); -}; diff --git a/src/datascience-ui/data-explorer/index.css b/src/datascience-ui/data-explorer/index.css deleted file mode 100644 index b4cc7250b98c..000000000000 --- a/src/datascience-ui/data-explorer/index.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: sans-serif; -} diff --git a/src/datascience-ui/data-explorer/index.html b/src/datascience-ui/data-explorer/index.html deleted file mode 100644 index b94a027725e1..000000000000 --- a/src/datascience-ui/data-explorer/index.html +++ /dev/null @@ -1,356 +0,0 @@ - - - - - - - React App - - - - - -
- - - diff --git a/src/datascience-ui/data-explorer/index.tsx b/src/datascience-ui/data-explorer/index.tsx deleted file mode 100644 index 704a5737bbd2..000000000000 --- a/src/datascience-ui/data-explorer/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './index.css'; - -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -import { IVsCodeApi } from '../react-common/postOffice'; -import { detectBaseTheme } from '../react-common/themeDetector'; -import { MainPanel } from './mainPanel'; - -// This special function talks to vscode from a web panel -export declare function acquireVsCodeApi(): IVsCodeApi; - -const baseTheme = detectBaseTheme(); - -// tslint:disable:no-typeof-undefined -ReactDOM.render( - , // Turn this back off when we have real variable explorer data - document.getElementById('root') as HTMLElement -); diff --git a/src/datascience-ui/data-explorer/mainPanel.css b/src/datascience-ui/data-explorer/mainPanel.css deleted file mode 100644 index 0616067bf249..000000000000 --- a/src/datascience-ui/data-explorer/mainPanel.css +++ /dev/null @@ -1,13 +0,0 @@ - -.main-panel { - position: absolute; - bottom: 0; - top: 0px; - left: 0px; - right: 0; - font-size: var(--code-font-size); - font-family: var(--code-font-family); - background-color: var(--vscode-editor-background); - overflow: hidden; -} - diff --git a/src/datascience-ui/data-explorer/mainPanel.tsx b/src/datascience-ui/data-explorer/mainPanel.tsx deleted file mode 100644 index 76d80022a794..000000000000 --- a/src/datascience-ui/data-explorer/mainPanel.tsx +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { JSONArray, JSONObject } from '@phosphor/coreutils'; -import * as React from 'react'; -import * as uuid from 'uuid/v4'; - -import { - CellFetchAllLimit, - CellFetchSizeFirst, - CellFetchSizeSubsequent, - DataViewerMessages, - IDataViewerMapping, - IGetRowsResponse -} from '../../client/datascience/data-viewing/types'; -import { IJupyterVariable } from '../../client/datascience/types'; -import { getLocString } from '../react-common/locReactSide'; -import { IMessageHandler, PostOffice } from '../react-common/postOffice'; -import { Progress } from '../react-common/progress'; -import { StyleInjector } from '../react-common/styleInjector'; -import { cellFormatterFunc } from './cellFormatter'; -import { ISlickGridAdd, ISlickRow, ReactSlickGrid } from './reactSlickGrid'; -import { generateTestData } from './testData'; - -// Our css has to come after in order to override body styles -import './mainPanel.css'; - -export interface IMainPanelProps { - skipDefault?: boolean; - baseTheme: string; - testMode?: boolean; -} - -//tslint:disable:no-any -interface IMainPanelState { - gridColumns: Slick.Column[]; - gridRows: ISlickRow[]; - fetchedRowCount: number; - totalRowCount: number; - filters: {}; - indexColumn: string; -} - -export class MainPanel extends React.Component implements IMessageHandler { - private container: React.Ref = React.createRef(); - private sentDone = false; - private postOffice: PostOffice = new PostOffice(); - private gridAddEvent: Slick.Event = new Slick.Event(); - private rowFetchSizeFirst: number = 0; - private rowFetchSizeSubsequent: number = 0; - private rowFetchSizeAll: number = 0; - // Just used for testing. - private grid: React.Ref = React.createRef(); - - // tslint:disable-next-line:max-func-body-length - constructor(props: IMainPanelProps, _state: IMainPanelState) { - super(props); - - if (!this.props.skipDefault) { - const data = generateTestData(5000); - this.state = { - gridColumns: data.columns.map(c => { return {...c, formatter: cellFormatterFunc }; }), - gridRows: [], - totalRowCount: data.rows.length, - fetchedRowCount: 0, - filters: {}, - indexColumn: data.primaryKeys[0] - }; - - // Fire off a timer to mimic dynamic loading - setTimeout(() => { - this.handleGetAllRowsResponse({data: data.rows}); - }, 1000); - } else { - this.state = { - gridColumns: [], - gridRows: [], - totalRowCount: 0, - fetchedRowCount: 0, - filters: {}, - indexColumn: 'index' - }; - } - } - - public componentWillMount() { - // Add ourselves as a handler for the post office - this.postOffice.addHandler(this); - - // Tell the dataviewer code we have started. - this.postOffice.sendMessage(DataViewerMessages.Started); - } - - public componentWillUnmount() { - this.postOffice.removeHandler(this); - this.postOffice.dispose(); - } - - public render = () => { - // Send our done message if we haven't yet and we just reached full capacity. Do it here so we - // can guarantee our render will run before somebody checks our rendered output. - if (this.state.totalRowCount && this.state.totalRowCount === this.state.fetchedRowCount && !this.sentDone) { - this.sentDone = true; - this.sendMessage(DataViewerMessages.CompletedData); - } - - const progressBar = this.state.totalRowCount > this.state.fetchedRowCount ? : undefined; - - return ( -
- - {progressBar} - {this.state.totalRowCount > 0 && this.renderGrid()} -
- ); - } - - // tslint:disable-next-line:no-any - public handleMessage = (msg: string, payload?: any) => { - switch (msg) { - case DataViewerMessages.InitializeData: - this.initializeData(payload); - break; - - case DataViewerMessages.GetAllRowsResponse: - this.handleGetAllRowsResponse(payload as JSONObject); - break; - - case DataViewerMessages.GetRowsResponse: - this.handleGetRowChunkResponse(payload as IGetRowsResponse); - break; - - default: - break; - } - - return false; - } - - private renderGrid() { - const filterRowsText = getLocString('DataScience.filterRowsButton', 'Filter Rows'); - const filterRowsTooltip = getLocString('DataScience.filterRowsTooltip', 'Click to filter.'); - - return ( - - ); - } - - // tslint:disable-next-line:no-any - private initializeData(payload: any) { - // Payload should be an IJupyterVariable with the first 100 rows filled out - if (payload) { - const variable = payload as IJupyterVariable; - if (variable) { - const columns = this.generateColumns(variable); - const totalRowCount = variable.rowCount ? variable.rowCount : 0; - const initialRows: ISlickRow[] = []; - const indexColumn = variable.indexColumn ? variable.indexColumn : 'index'; - - this.setState( - { - gridColumns: columns, - gridRows: initialRows, - totalRowCount, - fetchedRowCount: initialRows.length, - indexColumn: indexColumn - } - ); - - // Compute our row fetch sizes based on the number of columns - this.rowFetchSizeAll = Math.round(CellFetchAllLimit / columns.length); - this.rowFetchSizeFirst = Math.round(Math.max(2, CellFetchSizeFirst / columns.length)); - this.rowFetchSizeSubsequent = Math.round(Math.max(2, CellFetchSizeSubsequent / columns.length)); - - // Request the rest of the data if necessary - if (initialRows.length !== totalRowCount) { - // Get all at once if less than 1000 - if (totalRowCount < this.rowFetchSizeAll) { - this.getAllRows(); - } else { - this.getRowsInChunks(initialRows.length, totalRowCount); - } - } - } - } - } - - private getAllRows() { - this.sendMessage(DataViewerMessages.GetAllRowsRequest); - } - - private getRowsInChunks(startIndex: number, endIndex: number) { - // Ask for our first chunk. Don't spam jupyter though with all requests at once - // Instead, do them one at a time. - const chunkEnd = startIndex + Math.min(this.rowFetchSizeFirst, endIndex); - const chunkStart = startIndex; - this.sendMessage(DataViewerMessages.GetRowsRequest, {start: chunkStart, end: chunkEnd}); - } - - private handleGetAllRowsResponse(response: JSONObject) { - const rows = response.data ? response.data as JSONArray : []; - const normalized = this.normalizeRows(rows); - - // Update our fetched count and actual rows - this.setState( - { - gridRows: this.state.gridRows.concat(normalized), - fetchedRowCount: this.state.totalRowCount - }); - - // Add all of these rows to the grid - this.gridAddEvent.notify({newRows: normalized}); - } - - private handleGetRowChunkResponse(response: IGetRowsResponse) { - // We have a new fetched row count - const rows = response.rows.data ? response.rows.data as JSONArray : []; - const normalized = this.normalizeRows(rows); - const newFetched = this.state.fetchedRowCount + (response.end - response.start); - - // gridRows should have our entire list. We need to replace our part with our new results - const before = this.state.gridRows.slice(0, response.start); - const after = response.end < this.state.gridRows.length ? this.state.gridRows.slice(response.end) : []; - const newActual = before.concat(normalized.concat(after)); - - // Apply this to our state - this.setState({ - fetchedRowCount: newFetched, - gridRows: newActual - }); - - // Tell our grid about the new ros - this.gridAddEvent.notify({newRows: normalized}); - - // Get the next chunk - if (newFetched < this.state.totalRowCount) { - const chunkStart = response.end; - const chunkEnd = Math.min(chunkStart + this.rowFetchSizeSubsequent, this.state.totalRowCount); - this.sendMessage(DataViewerMessages.GetRowsRequest, {start: chunkStart, end: chunkEnd}); - } - } - - private generateColumns(variable: IJupyterVariable): Slick.Column[] { - if (variable.columns) { - return variable.columns.map((c: {key: string; type: string}, i: number) => { - return { - type: c.type, - field: c.key.toString(), - id: `${i}`, - name: c.key.toString(), - sortable: true, - formatter: cellFormatterFunc - }; - }); - } - return []; - } - - private normalizeRows(rows: JSONArray): ISlickRow[] { - // Make sure we have an index field and all rows have an item - return rows.map((r: any | undefined) => { - if (!r) { - r = {}; - } - if (!r.hasOwnProperty(this.state.indexColumn)) { - r[this.state.indexColumn] = uuid(); - } - return r; - }); - } - - private sendMessage(type: T, payload?: M[T]) { - this.postOffice.sendMessage(type, payload); - } - -} diff --git a/src/datascience-ui/data-explorer/progressBar.css b/src/datascience-ui/data-explorer/progressBar.css deleted file mode 100644 index 8249651f7029..000000000000 --- a/src/datascience-ui/data-explorer/progressBar.css +++ /dev/null @@ -1,9 +0,0 @@ -.progress-bar { - margin:2px; - text-align: center; -} - -.progress-container { - padding: 20px; - text-align:center; -} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/progressBar.tsx b/src/datascience-ui/data-explorer/progressBar.tsx deleted file mode 100644 index e24304ca0b3e..000000000000 --- a/src/datascience-ui/data-explorer/progressBar.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './progressBar.css'; - -import * as React from 'react'; -import { getLocString } from '../react-common/locReactSide'; - -export interface IEmptyRowsProps { - total: number; - current: number; -} - -export const ProgressBar = (props: IEmptyRowsProps) => { - const percent = props.current / props.total * 100; - const percentText = `${Math.round(percent)}%`; - const style: React.CSSProperties = { - width: percentText - }; - const message = getLocString('DataScience.fetchingDataViewer', 'Fetching data ...'); - - return ( -
- {message} -
{percentText}
-
- ); -}; diff --git a/src/datascience-ui/data-explorer/reactSlickGrid.css b/src/datascience-ui/data-explorer/reactSlickGrid.css deleted file mode 100644 index 273c96b8e810..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGrid.css +++ /dev/null @@ -1,86 +0,0 @@ -.outer-container { - width:auto; - height:100%; -} - -.react-grid-container { - border-color: var(--vscode-editor-inactiveSelectionBackground); - border-style: solid; - border-width: 1px; -} - -.react-grid-measure { - position: absolute; - bottom: 5px; -} - -.react-grid-filter-button { - background: var(--vscode-button-background); - color: var(--vscode-button-foreground); - padding: 10px; - border: none; - border-radius: 5px; - cursor: pointer; - margin:8px 4px; -} - -.react-grid-filter-button:focus { - outline: none; -} - -.react-grid-header-cell { - padding: 0px 4px; - background-color: var(--vscode-debugToolBar-background); - color: var(--vscode-editor-foreground); - text-align: left; - font-weight: bold; - border-right-color: var(--vscode-editor-inactiveSelectionBackground); -} - -.react-grid-cell { - padding: 0px 4px; - background-color: var(--vscode-editor-background); - color: var(--vscode-editor-foreground); - border-bottom-color: var(--vscode-editor-inactiveSelectionBackground); - border-right-color: var(--vscode-editor-inactiveSelectionBackground); - border-right-style: solid; - box-sizing: border-box; -} - -/* Some overrides necessary to get the colors we want */ -.slick-headerrow-column { - background-color: var(--vscode-debugToolBar-background); - border-right-color: var(--vscode-editor-inactiveSelectionBackground); - border-right-style: solid; -} - -.slick-header-column.ui-state-default, .slick-group-header-column.ui-state-default { - border-right-color: var(--vscode-editor-inactiveSelectionBackground); -} - -.slick-sort-indicator { - float: right; - width: 0px; - margin-right: 12px; - margin-top: 1px; -} - -.react-grid-header-cell:hover { - background-color: var(--vscode-editor-inactiveSelectionBackground); -} - -.react-grid-header-cell > .slick-sort-indicator-asc::before { - background: none; - content: '▲'; - align-items: center; -} - -.react-grid-header-cell > .slick-sort-indicator-desc::before { - background: none; - content: '▼'; - align-items: center; -} - -.slick-row:hover > .react-grid-cell { - background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); -} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/reactSlickGrid.tsx b/src/datascience-ui/data-explorer/reactSlickGrid.tsx deleted file mode 100644 index 01a849399697..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGrid.tsx +++ /dev/null @@ -1,415 +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 { MaxStringCompare } from '../../client/datascience/data-viewing/types'; -import { measureText } from '../react-common/textMeasure'; -import { ReactSlickGridFilterBox } from './reactSlickGridFilterBox'; - -// Slickgrid requires jquery to be defined. Globally. So we do some hacks here. -// tslint:disable-next-line: no-var-requires no-require-imports -require('expose-loader?jQuery!slickgrid/lib/jquery-1.11.2.min'); -// tslint:disable-next-line: no-var-requires no-require-imports -require('expose-loader?jQuery.fn.drag!slickgrid/lib/jquery.event.drag-2.3.0'); - -import 'slickgrid/slick.core'; -import 'slickgrid/slick.dataview'; -import 'slickgrid/slick.grid'; - -import 'slickgrid/plugins/slick.autotooltips'; - -import 'slickgrid/slick.grid.css'; - -// Make sure our css comes after the slick grid css. We override some of its styles. -import './reactSlickGrid.css'; - -const MinColumnWidth = 70; -const MaxColumnWidth = 500; -const RowHeightAdjustment = 4; - -export interface ISlickRow extends Slick.SlickData { - id: string; -} - -export interface ISlickGridAdd { - newRows: ISlickRow[]; -} - -// tslint:disable:no-any -export interface ISlickGridProps { - idProperty: string; - columns: Slick.Column[]; - rowsAdded: Slick.Event; - filterRowsText: string; - filterRowsTooltip: string; - forceHeight?: number; -} - -interface ISlickGridState { - grid?: Slick.Grid; - showingFilters?: boolean; - fontSize: number; -} - -class ColumnFilter { - private matchFunc : (v: any) => boolean; - private lessThanRegEx = /^\s*<\s*(\d+.*)/; - private lessThanEqualRegEx = /^\s*<=\s*(\d+.*).*/; - private greaterThanRegEx = /^\s*>\s*(\d+.*).*/; - private greaterThanEqualRegEx = /^\s*>=\s*(\d+.*).*/; - private equalThanRegEx = /^\s*=\s*(\d+.*).*/; - - constructor(text: string, column: Slick.Column) { - if (text && text.length > 0) { - const columnType = (column as any).type; - switch (columnType) { - case 'string': - default: - this.matchFunc = (v: any) => !v || v.toString().includes(text); - break; - - case 'integer': - case 'float': - case 'int64': - case 'float64': - case 'number': - this.matchFunc = this.generateNumericOperation(text); - break; - } - } else { - this.matchFunc = (_v: any) => true; - } - } - - public matches(value: any) : boolean { - return this.matchFunc(value); - } - - private extractDigits(text: string, regex: RegExp) : number { - const match = regex.exec(text); - if (match && match.length > 1) { - return parseFloat(match[1]); - } - return 0; - } - - private generateNumericOperation(text: string) : (v: any) => boolean { - if (this.lessThanRegEx.test(text)) { - const n1 = this.extractDigits(text, this.lessThanRegEx); - return (v: any) => v && v < n1; - } else if (this.lessThanEqualRegEx.test(text)) { - const n2 = this.extractDigits(text, this.lessThanEqualRegEx); - return (v: any) => v && v <= n2; - } else if (this.greaterThanRegEx.test(text)) { - const n3 = this.extractDigits(text, this.greaterThanRegEx); - return (v: any) => v && v > n3; - } else if (this.greaterThanEqualRegEx.test(text)) { - const n4 = this.extractDigits(text, this.greaterThanEqualRegEx); - return (v: any) => v && v >= n4; - } else if (this.equalThanRegEx.test(text)) { - const n5 = this.extractDigits(text, this.equalThanRegEx); - return (v: any) => v && v === n5; - } else { - const n6 = parseFloat(text); - return (v: any) => v && v === n6; - } - } -} - -export class ReactSlickGrid extends React.Component { - private containerRef: React.RefObject; - private measureRef: React.RefObject; - private dataView: Slick.Data.DataView = new Slick.Data.DataView(); - private columnFilters: Map = new Map(); - private resizeTimer?: number; - private autoResizedColumns: boolean = false; - - constructor(props: ISlickGridProps) { - super(props); - this.state = { fontSize: 15 }; - this.containerRef = React.createRef(); - this.measureRef = React.createRef(); - this.props.rowsAdded.subscribe(this.addedRows); - } - - public componentDidMount = () => { - window.addEventListener('resize', this.windowResized); - - if (this.containerRef.current) { - // Compute font size. Default to 15 if not found. - let fontSize = parseInt(getComputedStyle(this.containerRef.current).getPropertyValue('--vscode-font-size'), 10); - if (isNaN(fontSize)) { - fontSize = 15; - } - - // Setup options for the grid - const options : Slick.GridOptions = { - asyncEditorLoading: true, - editable: false, - enableCellNavigation: true, - showHeaderRow: true, - enableColumnReorder: false, - explicitInitialization: true, - viewportClass: 'react-grid', - rowHeight: fontSize + RowHeightAdjustment - }; - - // Transform columns so they are sortable and stylable - const columns = this.props.columns.map(c => { - c.sortable = true; - c.headerCssClass = 'react-grid-header-cell'; - c.cssClass = 'react-grid-cell'; - return c; - }); - - // Create the grid - const grid = new Slick.Grid( - this.containerRef.current, - this.dataView, - columns, - options - ); - grid.registerPlugin(new Slick.AutoTooltips({ enableForCells: true, enableForHeaderCells: true})); - - // Setup our dataview - this.dataView.beginUpdate(); - this.dataView.setFilter(this.filter.bind(this)); - this.dataView.setItems([], this.props.idProperty); - this.dataView.endUpdate(); - - this.dataView.onRowCountChanged.subscribe((_e, _args) => { - grid.updateRowCount(); - grid.render(); - }); - - this.dataView.onRowsChanged.subscribe((_e, args) => { - grid.invalidateRows(args.rows); - grid.render(); - }); - - // Setup the filter render - grid.onHeaderRowCellRendered.subscribe(this.renderFilterCell); - - // Setup the sorting - grid.onSort.subscribe(this.sort); - - // Init to force the actual render. - grid.init(); - - // Set the initial sort column to our index column - const indexColumn = columns.find(c => c.field === this.props.idProperty); - if (indexColumn && indexColumn.id) { - grid.setSortColumn(indexColumn.id, true); - } - - // Save in our state - this.setState({ grid, fontSize }); - } - - // Act like a resize happened to refresh the layout. - this.windowResized(); - } - - public componentWillUnmount = () => { - if (this.resizeTimer) { - window.clearTimeout(this.resizeTimer); - } - window.removeEventListener('resize', this.windowResized); - if (this.state.grid) { - this.state.grid.destroy(); - } - } - - public componentDidUpdate = (_prevProps: ISlickGridProps, prevState: ISlickGridState) => { - if (this.state.showingFilters && this.state.grid) { - this.state.grid.setHeaderRowVisibility(true); - } else if (this.state.showingFilters === false && this.state.grid) { - this.state.grid.setHeaderRowVisibility(false); - } - - // If this is our first time setting the grid, we need to dynanically modify the styles - // that the slickGrid generates for the rows. It's eliminating some of the height - if (!prevState.grid && this.state.grid && this.containerRef.current) { - this.updateCssStyles(); - } - } - - public render() { - const style : React.CSSProperties = this.props.forceHeight ? { - height: `${this.props.forceHeight}px`, - width: `${this.props.forceHeight}px` - } : { - }; - - return ( -
- -
-
-
-
- ); - } - - // public for testing - public sort = (_e: Slick.EventData, args: Slick.OnSortEventArgs) => { - // Note: dataView.fastSort is an IE workaround. Not necessary. - this.dataView.sort((l: any, r: any) => this.compareElements(l, r, args.sortCol), args.sortAsc); - args.grid.invalidateAllRows(); - args.grid.render(); - } - - private updateCssStyles = () => { - if (this.state.grid && this.containerRef.current) { - const gridName = (this.state.grid as any).getUID() as string; - const document = this.containerRef.current.ownerDocument; - if (document) { - const cssOverrideNode = document.createElement('style'); - const rule = `.${gridName} .slick-cell {height: ${this.state.fontSize + RowHeightAdjustment}px;}`; - cssOverrideNode.setAttribute('type', 'text/css'); - cssOverrideNode.setAttribute('rel', 'stylesheet'); - cssOverrideNode.appendChild(document.createTextNode(rule)); - document.head.appendChild(cssOverrideNode); - } - } - } - - private windowResized = () => { - if (this.resizeTimer) { - clearTimeout(this.resizeTimer); - } - this.resizeTimer = window.setTimeout(this.updateGridSize, 10); - } - - private updateGridSize = () => { - if (this.state.grid && this.containerRef.current && this.measureRef.current) { - // We use a div at the bottom to figure out our expected height. Slickgrid isn't - // so good without a specific height set in the style. - const height = this.measureRef.current.offsetTop - this.containerRef.current.offsetTop; - this.containerRef.current.style.height = `${this.props.forceHeight ? this.props.forceHeight : height}px`; - this.state.grid.resizeCanvas(); - } - } - - private autoResizeColumns(rows: ISlickRow[]) { - if (this.state.grid) { - const fontString = this.computeFont(); - const columns = this.state.grid.getColumns(); - columns.forEach(c => { - let colWidth = MinColumnWidth; - rows.forEach((r: any) => { - const field = c.field ? r[c.field] : ''; - const fieldWidth = field ? measureText(field.toString(), fontString) : 0; - colWidth = Math.min(MaxColumnWidth, Math.max(colWidth, fieldWidth)); - }); - c.width = colWidth; - }); - this.state.grid.setColumns(columns); - - // We also need to update the styles as slickgrid will mess up the height of rows - // again - setTimeout(() => { - this.updateCssStyles(); - - // Hide the header row after we finally resize our columns - this.state.grid!.setHeaderRowVisibility(false); - } - , 0); - } - } - - private computeFont() : string | null { - if (this.containerRef.current) { - const style = getComputedStyle(this.containerRef.current); - return style ? style.font : null; - } - return null; - } - - private addedRows = (_e: Slick.EventData, data: ISlickGridAdd) => { - // Add all of these new rows into our data. - this.dataView.beginUpdate(); - for (const row of data.newRows) { - this.dataView.addItem(row); - } - - // Update columns if we haven't already - if (!this.autoResizedColumns) { - this.autoResizedColumns = true; - this.autoResizeColumns(data.newRows); - } - - this.dataView.endUpdate(); - - // This should cause a rowsChanged event in the dataview that will - // refresh the grid. - } - - // tslint:disable-next-line: no-any - private filter(item: any, _args: any): boolean { - const fields = Array.from(this.columnFilters.keys()); - for (const field of fields) { - if (field) { - const filter = this.columnFilters.get(field); - if (filter) { - if (!filter.matches(item[field])) { - return false; - } - } - } - } - return true; - } - - private clickFilterButton = (e: React.SyntheticEvent) => { - e.preventDefault(); - this.setState({showingFilters: !this.state.showingFilters}); - } - - private renderFilterCell = (_e: Slick.EventData, args: Slick.OnHeaderRowCellRenderedEventArgs) => { - ReactDOM.render(, args.node); - } - - private filterChanged = (text: string, column: Slick.Column) => { - if (column && column.field) { - this.columnFilters.set(column.field, new ColumnFilter(text, column)); - this.dataView.refresh(); - } - } - - private compareElements(a: any, b: any, col?: Slick.Column) : number { - if (col) { - const sortColumn = col.field; - if (sortColumn && col.hasOwnProperty('type')) { - const columnType = (col as any).type; - const isStringColumn = columnType === 'string' || columnType === 'object'; - if (isStringColumn) { - const aVal = a[sortColumn] ? a[sortColumn].toString() : ''; - const bVal = b[sortColumn] ? b[sortColumn].toString() : ''; - const aStr = aVal ? aVal.substring(0, Math.min(aVal.length, MaxStringCompare)) : aVal; - const bStr = bVal ? bVal.substring(0, Math.min(bVal.length, MaxStringCompare)) : bVal; - return aStr.localeCompare(bStr); - } else { - const aVal = a[sortColumn]; - const bVal = b[sortColumn]; - return aVal === bVal ? 0 : aVal > bVal ? 1 : -1; - } - } - } - - // No sort column, try index column - if (a.hasOwnProperty(this.props.idProperty) && b.hasOwnProperty(this.props.idProperty)) { - const sortColumn = this.props.idProperty; - const aVal = a[sortColumn]; - const bVal = b[sortColumn]; - return aVal === bVal ? 0 : aVal > bVal ? 1 : -1; - } - - return -1; - } -} diff --git a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css deleted file mode 100644 index 0d5670650776..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.css +++ /dev/null @@ -1,17 +0,0 @@ -.filter-box { - border-color: var(--vscode-editor-inactiveSelectionBackground); - border-style: solid; - border-width: 1px; - display: block; - position: relative; - left: -2px; - top: -3px; - width: 98%; - padding: 1px; - margin: 0px; -} - -.filter-box:focus { - border-color: var(--vscode-editor-selectionBackground); - outline: none; -} \ No newline at end of file diff --git a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx b/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx deleted file mode 100644 index f3348a78ecab..000000000000 --- a/src/datascience-ui/data-explorer/reactSlickGridFilterBox.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as React from 'react'; - -import './reactSlickGridFilterBox.css'; - -interface IFilterProps { - column: Slick.Column; - onChange(val: string, column: Slick.Column): void; -} - -export class ReactSlickGridFilterBox extends React.Component { - - constructor(props: IFilterProps) { - super(props); - } - - public render() { - return ; - } - - private updateInputValue = (evt: React.SyntheticEvent) => { - const element = evt.currentTarget as HTMLInputElement; - if (element) { - this.props.onChange(element.value, this.props.column); - } - } - -} diff --git a/src/datascience-ui/data-explorer/testData.ts b/src/datascience-ui/data-explorer/testData.ts deleted file mode 100644 index 56b10855a537..000000000000 --- a/src/datascience-ui/data-explorer/testData.ts +++ /dev/null @@ -1,12517 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export interface ITestData { - columns: {id: string; name: string; type: string }[]; - primaryKeys: string[]; - rows: {}[]; - loadingRows: {}[]; -} - -// tslint:disable -export function generateTestData(_numberOfRows: number) : ITestData { - const columns = [ - { id: 'PassengerId', name: 'PassengerId', field: 'PassengerId', type: 'integer'}, - { id: 'SibSp', name: 'SibSp', field: 'SibSp',type: 'integer'}, - { id: 'Ticket', name: 'Ticket', field: 'Ticket',type: 'string'}, - { id: 'Parch', name: 'Parch', field: 'Parch', type: 'integer'}, - { id: 'Cabin', name: 'Cabin', field: 'Cabin',type: 'string'}, - { id: 'Age', name: 'Age', field: 'Age',type: 'integer'}, - { id: 'Fare', name: 'Fare', field: 'Fare',type: 'number'}, - { id: 'Name', name: 'Name', field: 'Name',type: 'string'}, - { id: 'Survived', name: 'Survived', field: 'Survived',type: 'bool'}, - { id: 'Pclass', name: 'Pclass', field: 'Pclass',type: 'integer'}, - { id: 'Embarked', name: 'Embarked', field: 'Embarked',type: 'string'}, - { id: 'Sex', name: 'Sex', field: 'Sex',type: 'string'} - ] - - const keys = ['PassengerId']; - - const rows : {}[] = titanicData; - - return { - columns, - primaryKeys: keys, - rows, - loadingRows: titanicData.map(_t => { return {};}) - }; -} - -const titanicData = -[ - { - "SibSp": 1, - "Ticket": "A/5 21171", - "Parch": 0, - "Cabin": null, - "PassengerId": 1, - "Age": 22, - "Fare": 7.25, - "Name": "Braund, Mr. Owen Harris", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17599", - "Parch": 0, - "Cabin": "C85", - "PassengerId": 2, - "Age": 38, - "Fare": 71.2833, - "Name": "Cumings, Mrs. John Bradley (Florence Briggs Thayer)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O2. 3101282", - "Parch": 0, - "Cabin": null, - "PassengerId": 3, - "Age": 26, - "Fare": 7.925, - "Name": "Heikkinen, Miss. Laina", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "113803", - "Parch": 0, - "Cabin": "C123", - "PassengerId": 4, - "Age": 35, - "Fare": 53.1, - "Name": "Futrelle, Mrs. Jacques Heath (Lily May Peel)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "373450", - "Parch": 0, - "Cabin": null, - "PassengerId": 5, - "Age": 35, - "Fare": 8.05, - "Name": "Allen, Mr. William Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330877", - "Parch": 0, - "Cabin": null, - "PassengerId": 6, - "Age": null, - "Fare": 8.4583, - "Name": "Moran, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "17463", - "Parch": 0, - "Cabin": "E46", - "PassengerId": 7, - "Age": 54, - "Fare": 51.8625, - "Name": "McCarthy, Mr. Timothy J", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "349909", - "Parch": 1, - "Cabin": null, - "PassengerId": 8, - "Age": 2, - "Fare": 21.075, - "Name": "Palsson, Master. Gosta Leonard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347742", - "Parch": 2, - "Cabin": null, - "PassengerId": 9, - "Age": 27, - "Fare": 11.1333, - "Name": "Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "237736", - "Parch": 0, - "Cabin": null, - "PassengerId": 10, - "Age": 14, - "Fare": 30.0708, - "Name": "Nasser, Mrs. Nicholas (Adele Achem)", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "PP 9549", - "Parch": 1, - "Cabin": "G6", - "PassengerId": 11, - "Age": 4, - "Fare": 16.7, - "Name": "Sandstrom, Miss. Marguerite Rut", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113783", - "Parch": 0, - "Cabin": "C103", - "PassengerId": 12, - "Age": 58, - "Fare": 26.55, - "Name": "Bonnell, Miss. Elizabeth", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A/5. 2151", - "Parch": 0, - "Cabin": null, - "PassengerId": 13, - "Age": 20, - "Fare": 8.05, - "Name": "Saundercock, Mr. William Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347082", - "Parch": 5, - "Cabin": null, - "PassengerId": 14, - "Age": 39, - "Fare": 31.275, - "Name": "Andersson, Mr. Anders Johan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350406", - "Parch": 0, - "Cabin": null, - "PassengerId": 15, - "Age": 14, - "Fare": 7.8542, - "Name": "Vestrom, Miss. Hulda Amanda Adolfina", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "248706", - "Parch": 0, - "Cabin": null, - "PassengerId": 16, - "Age": 55, - "Fare": 16, - "Name": "Hewlett, Mrs. (Mary D Kingcome) ", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "382652", - "Parch": 1, - "Cabin": null, - "PassengerId": 17, - "Age": 2, - "Fare": 29.125, - "Name": "Rice, Master. Eugene", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "244373", - "Parch": 0, - "Cabin": null, - "PassengerId": 18, - "Age": null, - "Fare": 13, - "Name": "Williams, Mr. Charles Eugene", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "345763", - "Parch": 0, - "Cabin": null, - "PassengerId": 19, - "Age": 31, - "Fare": 18, - "Name": "Vander Planke, Mrs. Julius (Emelia Maria Vandemoortele)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2649", - "Parch": 0, - "Cabin": null, - "PassengerId": 20, - "Age": null, - "Fare": 7.225, - "Name": "Masselmani, Mrs. Fatima", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "239865", - "Parch": 0, - "Cabin": null, - "PassengerId": 21, - "Age": 35, - "Fare": 26, - "Name": "Fynney, Mr. Joseph J", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248698", - "Parch": 0, - "Cabin": "D56", - "PassengerId": 22, - "Age": 34, - "Fare": 13, - "Name": "Beesley, Mr. Lawrence", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330923", - "Parch": 0, - "Cabin": null, - "PassengerId": 23, - "Age": 15, - "Fare": 8.0292, - "Name": "McGowan, Miss. Anna \"Annie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113788", - "Parch": 0, - "Cabin": "A6", - "PassengerId": 24, - "Age": 28, - "Fare": 35.5, - "Name": "Sloper, Mr. William Thompson", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "349909", - "Parch": 1, - "Cabin": null, - "PassengerId": 25, - "Age": 8, - "Fare": 21.075, - "Name": "Palsson, Miss. Torborg Danira", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "347077", - "Parch": 5, - "Cabin": null, - "PassengerId": 26, - "Age": 38, - "Fare": 31.3875, - "Name": "Asplund, Mrs. Carl Oscar (Selma Augusta Emilia Johansson)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2631", - "Parch": 0, - "Cabin": null, - "PassengerId": 27, - "Age": null, - "Fare": 7.225, - "Name": "Emir, Mr. Farred Chehab", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "19950", - "Parch": 2, - "Cabin": "C23 C25 C27", - "PassengerId": 28, - "Age": 19, - "Fare": 263, - "Name": "Fortune, Mr. Charles Alexander", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330959", - "Parch": 0, - "Cabin": null, - "PassengerId": 29, - "Age": null, - "Fare": 7.8792, - "Name": "O'Dwyer, Miss. Ellen \"Nellie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349216", - "Parch": 0, - "Cabin": null, - "PassengerId": 30, - "Age": null, - "Fare": 7.8958, - "Name": "Todoroff, Mr. Lalio", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17601", - "Parch": 0, - "Cabin": null, - "PassengerId": 31, - "Age": 40, - "Fare": 27.7208, - "Name": "Uruchurtu, Don. Manuel E", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17569", - "Parch": 0, - "Cabin": "B78", - "PassengerId": 32, - "Age": null, - "Fare": 146.5208, - "Name": "Spencer, Mrs. William Augustus (Marie Eugenie)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "335677", - "Parch": 0, - "Cabin": null, - "PassengerId": 33, - "Age": null, - "Fare": 7.75, - "Name": "Glynn, Miss. Mary Agatha", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C.A. 24579", - "Parch": 0, - "Cabin": null, - "PassengerId": 34, - "Age": 66, - "Fare": 10.5, - "Name": "Wheadon, Mr. Edward H", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17604", - "Parch": 0, - "Cabin": null, - "PassengerId": 35, - "Age": 28, - "Fare": 82.1708, - "Name": "Meyer, Mr. Edgar Joseph", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113789", - "Parch": 0, - "Cabin": null, - "PassengerId": 36, - "Age": 42, - "Fare": 52, - "Name": "Holverson, Mr. Alexander Oskar", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2677", - "Parch": 0, - "Cabin": null, - "PassengerId": 37, - "Age": null, - "Fare": 7.2292, - "Name": "Mamee, Mr. Hanna", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A./5. 2152", - "Parch": 0, - "Cabin": null, - "PassengerId": 38, - "Age": 21, - "Fare": 8.05, - "Name": "Cann, Mr. Ernest Charles", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "345764", - "Parch": 0, - "Cabin": null, - "PassengerId": 39, - "Age": 18, - "Fare": 18, - "Name": "Vander Planke, Miss. Augusta Maria", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "2651", - "Parch": 0, - "Cabin": null, - "PassengerId": 40, - "Age": 14, - "Fare": 11.2417, - "Name": "Nicola-Yarred, Miss. Jamila", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "7546", - "Parch": 0, - "Cabin": null, - "PassengerId": 41, - "Age": 40, - "Fare": 9.475, - "Name": "Ahlin, Mrs. Johan (Johanna Persdotter Larsson)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "11668", - "Parch": 0, - "Cabin": null, - "PassengerId": 42, - "Age": 27, - "Fare": 21, - "Name": "Turpin, Mrs. William John Robert (Dorothy Ann Wonnacott)", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349253", - "Parch": 0, - "Cabin": null, - "PassengerId": 43, - "Age": null, - "Fare": 7.8958, - "Name": "Kraeff, Mr. Theodor", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "SC/Paris 2123", - "Parch": 2, - "Cabin": null, - "PassengerId": 44, - "Age": 3, - "Fare": 41.5792, - "Name": "Laroche, Miss. Simonne Marie Anne Andree", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "330958", - "Parch": 0, - "Cabin": null, - "PassengerId": 45, - "Age": 19, - "Fare": 7.8792, - "Name": "Devaney, Miss. Margaret Delia", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "S.C./A.4. 23567", - "Parch": 0, - "Cabin": null, - "PassengerId": 46, - "Age": null, - "Fare": 8.05, - "Name": "Rogers, Mr. William John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "370371", - "Parch": 0, - "Cabin": null, - "PassengerId": 47, - "Age": null, - "Fare": 15.5, - "Name": "Lennon, Mr. Denis", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "14311", - "Parch": 0, - "Cabin": null, - "PassengerId": 48, - "Age": null, - "Fare": 7.75, - "Name": "O'Driscoll, Miss. Bridget", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "2662", - "Parch": 0, - "Cabin": null, - "PassengerId": 49, - "Age": null, - "Fare": 21.6792, - "Name": "Samaan, Mr. Youssef", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "349237", - "Parch": 0, - "Cabin": null, - "PassengerId": 50, - "Age": 18, - "Fare": 17.8, - "Name": "Arnold-Franchi, Mrs. Josef (Josefine Franchi)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "3101295", - "Parch": 1, - "Cabin": null, - "PassengerId": 51, - "Age": 7, - "Fare": 39.6875, - "Name": "Panula, Master. Juha Niilo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/4. 39886", - "Parch": 0, - "Cabin": null, - "PassengerId": 52, - "Age": 21, - "Fare": 7.8, - "Name": "Nosworthy, Mr. Richard Cater", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17572", - "Parch": 0, - "Cabin": "D33", - "PassengerId": 53, - "Age": 49, - "Fare": 76.7292, - "Name": "Harper, Mrs. Henry Sleeper (Myna Haxtun)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "2926", - "Parch": 0, - "Cabin": null, - "PassengerId": 54, - "Age": 29, - "Fare": 26, - "Name": "Faunthorpe, Mrs. Lizzie (Elizabeth Anne Wilkinson)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113509", - "Parch": 1, - "Cabin": "B30", - "PassengerId": 55, - "Age": 65, - "Fare": 61.9792, - "Name": "Ostby, Mr. Engelhart Cornelius", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "19947", - "Parch": 0, - "Cabin": "C52", - "PassengerId": 56, - "Age": null, - "Fare": 35.5, - "Name": "Woolner, Mr. Hugh", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 31026", - "Parch": 0, - "Cabin": null, - "PassengerId": 57, - "Age": 21, - "Fare": 10.5, - "Name": "Rugg, Miss. Emily", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2697", - "Parch": 0, - "Cabin": null, - "PassengerId": 58, - "Age": 28.5, - "Fare": 7.2292, - "Name": "Novel, Mr. Mansouer", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 34651", - "Parch": 2, - "Cabin": null, - "PassengerId": 59, - "Age": 5, - "Fare": 27.75, - "Name": "West, Miss. Constance Mirium", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 5, - "Ticket": "CA 2144", - "Parch": 2, - "Cabin": null, - "PassengerId": 60, - "Age": 11, - "Fare": 46.9, - "Name": "Goodwin, Master. William Frederick", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2669", - "Parch": 0, - "Cabin": null, - "PassengerId": 61, - "Age": 22, - "Fare": 7.2292, - "Name": "Sirayanian, Mr. Orsen", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113572", - "Parch": 0, - "Cabin": "B28", - "PassengerId": 62, - "Age": 38, - "Fare": 80, - "Name": "Icard, Miss. Amelie", - "Survived": true, - "Pclass": 1, - "Embarked": null, - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "36973", - "Parch": 0, - "Cabin": "C83", - "PassengerId": 63, - "Age": 45, - "Fare": 83.475, - "Name": "Harris, Mr. Henry Birkhardt", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "347088", - "Parch": 2, - "Cabin": null, - "PassengerId": 64, - "Age": 4, - "Fare": 27.9, - "Name": "Skoog, Master. Harald", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17605", - "Parch": 0, - "Cabin": null, - "PassengerId": 65, - "Age": null, - "Fare": 27.7208, - "Name": "Stewart, Mr. Albert A", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2661", - "Parch": 1, - "Cabin": null, - "PassengerId": 66, - "Age": null, - "Fare": 15.2458, - "Name": "Moubarek, Master. Gerios", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 29395", - "Parch": 0, - "Cabin": "F33", - "PassengerId": 67, - "Age": 29, - "Fare": 10.5, - "Name": "Nye, Mrs. (Elizabeth Ramell)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "S.P. 3464", - "Parch": 0, - "Cabin": null, - "PassengerId": 68, - "Age": 19, - "Fare": 8.1583, - "Name": "Crease, Mr. Ernest James", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "3101281", - "Parch": 2, - "Cabin": null, - "PassengerId": 69, - "Age": 17, - "Fare": 7.925, - "Name": "Andersson, Miss. Erna Alexandra", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "315151", - "Parch": 0, - "Cabin": null, - "PassengerId": 70, - "Age": 26, - "Fare": 8.6625, - "Name": "Kink, Mr. Vincenz", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 33111", - "Parch": 0, - "Cabin": null, - "PassengerId": 71, - "Age": 32, - "Fare": 10.5, - "Name": "Jenkin, Mr. Stephen Curnow", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 5, - "Ticket": "CA 2144", - "Parch": 2, - "Cabin": null, - "PassengerId": 72, - "Age": 16, - "Fare": 46.9, - "Name": "Goodwin, Miss. Lillian Amy", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "S.O.C. 14879", - "Parch": 0, - "Cabin": null, - "PassengerId": 73, - "Age": 21, - "Fare": 73.5, - "Name": "Hood, Mr. Ambrose Jr", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2680", - "Parch": 0, - "Cabin": null, - "PassengerId": 74, - "Age": 26, - "Fare": 14.4542, - "Name": "Chronopoulos, Mr. Apostolos", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 75, - "Age": 32, - "Fare": 56.4958, - "Name": "Bing, Mr. Lee", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "348123", - "Parch": 0, - "Cabin": "F G73", - "PassengerId": 76, - "Age": 25, - "Fare": 7.65, - "Name": "Moen, Mr. Sigurd Hansen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349208", - "Parch": 0, - "Cabin": null, - "PassengerId": 77, - "Age": null, - "Fare": 7.8958, - "Name": "Staneff, Mr. Ivan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "374746", - "Parch": 0, - "Cabin": null, - "PassengerId": 78, - "Age": null, - "Fare": 8.05, - "Name": "Moutal, Mr. Rahamin Haim", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248738", - "Parch": 2, - "Cabin": null, - "PassengerId": 79, - "Age": 0.83, - "Fare": 29, - "Name": "Caldwell, Master. Alden Gates", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364516", - "Parch": 0, - "Cabin": null, - "PassengerId": 80, - "Age": 30, - "Fare": 12.475, - "Name": "Dowdell, Miss. Elizabeth", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "345767", - "Parch": 0, - "Cabin": null, - "PassengerId": 81, - "Age": 22, - "Fare": 9, - "Name": "Waelens, Mr. Achille", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345779", - "Parch": 0, - "Cabin": null, - "PassengerId": 82, - "Age": 29, - "Fare": 9.5, - "Name": "Sheerlinck, Mr. Jan Baptist", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330932", - "Parch": 0, - "Cabin": null, - "PassengerId": 83, - "Age": null, - "Fare": 7.7875, - "Name": "McDermott, Miss. Brigdet Delia", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113059", - "Parch": 0, - "Cabin": null, - "PassengerId": 84, - "Age": 28, - "Fare": 47.1, - "Name": "Carrau, Mr. Francisco M", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SO/C 14885", - "Parch": 0, - "Cabin": null, - "PassengerId": 85, - "Age": 17, - "Fare": 10.5, - "Name": "Ilett, Miss. Bertha", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 3, - "Ticket": "3101278", - "Parch": 0, - "Cabin": null, - "PassengerId": 86, - "Age": 33, - "Fare": 15.85, - "Name": "Backstrom, Mrs. Karl Alfred (Maria Mathilda Gustafsson)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "W./C. 6608", - "Parch": 3, - "Cabin": null, - "PassengerId": 87, - "Age": 16, - "Fare": 34.375, - "Name": "Ford, Mr. William Neal", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 392086", - "Parch": 0, - "Cabin": null, - "PassengerId": 88, - "Age": null, - "Fare": 8.05, - "Name": "Slocovski, Mr. Selman Francis", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "19950", - "Parch": 2, - "Cabin": "C23 C25 C27", - "PassengerId": 89, - "Age": 23, - "Fare": 263, - "Name": "Fortune, Miss. Mabel Helen", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "343275", - "Parch": 0, - "Cabin": null, - "PassengerId": 90, - "Age": 24, - "Fare": 8.05, - "Name": "Celotti, Mr. Francesco", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "343276", - "Parch": 0, - "Cabin": null, - "PassengerId": 91, - "Age": 29, - "Fare": 8.05, - "Name": "Christmann, Mr. Emil", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347466", - "Parch": 0, - "Cabin": null, - "PassengerId": 92, - "Age": 20, - "Fare": 7.8542, - "Name": "Andreasson, Mr. Paul Edvin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "W.E.P. 5734", - "Parch": 0, - "Cabin": "E31", - "PassengerId": 93, - "Age": 46, - "Fare": 61.175, - "Name": "Chaffee, Mr. Herbert Fuller", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 2315", - "Parch": 2, - "Cabin": null, - "PassengerId": 94, - "Age": 26, - "Fare": 20.575, - "Name": "Dean, Mr. Bertram Frank", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364500", - "Parch": 0, - "Cabin": null, - "PassengerId": 95, - "Age": 59, - "Fare": 7.25, - "Name": "Coxon, Mr. Daniel", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "374910", - "Parch": 0, - "Cabin": null, - "PassengerId": 96, - "Age": null, - "Fare": 8.05, - "Name": "Shorney, Mr. Charles Joseph", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17754", - "Parch": 0, - "Cabin": "A5", - "PassengerId": 97, - "Age": 71, - "Fare": 34.6542, - "Name": "Goldschmidt, Mr. George B", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17759", - "Parch": 1, - "Cabin": "D10 D12", - "PassengerId": 98, - "Age": 23, - "Fare": 63.3583, - "Name": "Greenfield, Mr. William Bertram", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "231919", - "Parch": 1, - "Cabin": null, - "PassengerId": 99, - "Age": 34, - "Fare": 23, - "Name": "Doling, Mrs. John T (Ada Julia Bone)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "244367", - "Parch": 0, - "Cabin": null, - "PassengerId": 100, - "Age": 34, - "Fare": 26, - "Name": "Kantor, Mr. Sinai", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349245", - "Parch": 0, - "Cabin": null, - "PassengerId": 101, - "Age": 28, - "Fare": 7.8958, - "Name": "Petranec, Miss. Matilda", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349215", - "Parch": 0, - "Cabin": null, - "PassengerId": 102, - "Age": null, - "Fare": 7.8958, - "Name": "Petroff, Mr. Pastcho (\"Pentcho\")", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "35281", - "Parch": 1, - "Cabin": "D26", - "PassengerId": 103, - "Age": 21, - "Fare": 77.2875, - "Name": "White, Mr. Richard Frasar", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "7540", - "Parch": 0, - "Cabin": null, - "PassengerId": 104, - "Age": 33, - "Fare": 8.6542, - "Name": "Johansson, Mr. Gustaf Joel", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "3101276", - "Parch": 0, - "Cabin": null, - "PassengerId": 105, - "Age": 37, - "Fare": 7.925, - "Name": "Gustafsson, Mr. Anders Vilhelm", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349207", - "Parch": 0, - "Cabin": null, - "PassengerId": 106, - "Age": 28, - "Fare": 7.8958, - "Name": "Mionoff, Mr. Stoytcho", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "343120", - "Parch": 0, - "Cabin": null, - "PassengerId": 107, - "Age": 21, - "Fare": 7.65, - "Name": "Salkjelsvik, Miss. Anna Kristine", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "312991", - "Parch": 0, - "Cabin": null, - "PassengerId": 108, - "Age": null, - "Fare": 7.775, - "Name": "Moss, Mr. Albert Johan", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349249", - "Parch": 0, - "Cabin": null, - "PassengerId": 109, - "Age": 38, - "Fare": 7.8958, - "Name": "Rekic, Mr. Tido", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "371110", - "Parch": 0, - "Cabin": null, - "PassengerId": 110, - "Age": null, - "Fare": 24.15, - "Name": "Moran, Miss. Bertha", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "110465", - "Parch": 0, - "Cabin": "C110", - "PassengerId": 111, - "Age": 47, - "Fare": 52, - "Name": "Porter, Mr. Walter Chamberlain", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2665", - "Parch": 0, - "Cabin": null, - "PassengerId": 112, - "Age": 14.5, - "Fare": 14.4542, - "Name": "Zabour, Miss. Hileni", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "324669", - "Parch": 0, - "Cabin": null, - "PassengerId": 113, - "Age": 22, - "Fare": 8.05, - "Name": "Barton, Mr. David John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "4136", - "Parch": 0, - "Cabin": null, - "PassengerId": 114, - "Age": 20, - "Fare": 9.825, - "Name": "Jussila, Miss. Katriina", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2627", - "Parch": 0, - "Cabin": null, - "PassengerId": 115, - "Age": 17, - "Fare": 14.4583, - "Name": "Attalah, Miss. Malake", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101294", - "Parch": 0, - "Cabin": null, - "PassengerId": 116, - "Age": 21, - "Fare": 7.925, - "Name": "Pekoniemi, Mr. Edvard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370369", - "Parch": 0, - "Cabin": null, - "PassengerId": 117, - "Age": 70.5, - "Fare": 7.75, - "Name": "Connors, Mr. Patrick", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "11668", - "Parch": 0, - "Cabin": null, - "PassengerId": 118, - "Age": 29, - "Fare": 21, - "Name": "Turpin, Mr. William John Robert", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17558", - "Parch": 1, - "Cabin": "B58 B60", - "PassengerId": 119, - "Age": 24, - "Fare": 247.5208, - "Name": "Baxter, Mr. Quigg Edmond", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "347082", - "Parch": 2, - "Cabin": null, - "PassengerId": 120, - "Age": 2, - "Fare": 31.275, - "Name": "Andersson, Miss. Ellis Anna Maria", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "S.O.C. 14879", - "Parch": 0, - "Cabin": null, - "PassengerId": 121, - "Age": 21, - "Fare": 73.5, - "Name": "Hickman, Mr. Stanley George", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A4. 54510", - "Parch": 0, - "Cabin": null, - "PassengerId": 122, - "Age": null, - "Fare": 8.05, - "Name": "Moore, Mr. Leonard Charles", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "237736", - "Parch": 0, - "Cabin": null, - "PassengerId": 123, - "Age": 32.5, - "Fare": 30.0708, - "Name": "Nasser, Mr. Nicholas", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "27267", - "Parch": 0, - "Cabin": "E101", - "PassengerId": 124, - "Age": 32.5, - "Fare": 13, - "Name": "Webber, Miss. Susan", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "35281", - "Parch": 1, - "Cabin": "D26", - "PassengerId": 125, - "Age": 54, - "Fare": 77.2875, - "Name": "White, Mr. Percival Wayland", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2651", - "Parch": 0, - "Cabin": null, - "PassengerId": 126, - "Age": 12, - "Fare": 11.2417, - "Name": "Nicola-Yarred, Master. Elias", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370372", - "Parch": 0, - "Cabin": null, - "PassengerId": 127, - "Age": null, - "Fare": 7.75, - "Name": "McMahon, Mr. Martin", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C 17369", - "Parch": 0, - "Cabin": null, - "PassengerId": 128, - "Age": 24, - "Fare": 7.1417, - "Name": "Madsen, Mr. Fridtjof Arne", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2668", - "Parch": 1, - "Cabin": "F E69", - "PassengerId": 129, - "Age": null, - "Fare": 22.3583, - "Name": "Peter, Miss. Anna", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347061", - "Parch": 0, - "Cabin": null, - "PassengerId": 130, - "Age": 45, - "Fare": 6.975, - "Name": "Ekstrom, Mr. Johan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349241", - "Parch": 0, - "Cabin": null, - "PassengerId": 131, - "Age": 33, - "Fare": 7.8958, - "Name": "Drazenoic, Mr. Jozef", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 3101307", - "Parch": 0, - "Cabin": null, - "PassengerId": 132, - "Age": 20, - "Fare": 7.05, - "Name": "Coelho, Mr. Domingos Fernandeo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "A/5. 3337", - "Parch": 0, - "Cabin": null, - "PassengerId": 133, - "Age": 47, - "Fare": 14.5, - "Name": "Robins, Mrs. Alexander A (Grace Charity Laury)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "228414", - "Parch": 0, - "Cabin": null, - "PassengerId": 134, - "Age": 29, - "Fare": 26, - "Name": "Weisz, Mrs. Leopold (Mathilde Francoise Pede)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C.A. 29178", - "Parch": 0, - "Cabin": null, - "PassengerId": 135, - "Age": 25, - "Fare": 13, - "Name": "Sobey, Mr. Samuel James Hayden", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SC/PARIS 2133", - "Parch": 0, - "Cabin": null, - "PassengerId": 136, - "Age": 23, - "Fare": 15.0458, - "Name": "Richard, Mr. Emile", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "11752", - "Parch": 2, - "Cabin": "D47", - "PassengerId": 137, - "Age": 19, - "Fare": 26.2833, - "Name": "Newsom, Miss. Helen Monypeny", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "113803", - "Parch": 0, - "Cabin": "C123", - "PassengerId": 138, - "Age": 37, - "Fare": 53.1, - "Name": "Futrelle, Mr. Jacques Heath", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "7534", - "Parch": 0, - "Cabin": null, - "PassengerId": 139, - "Age": 16, - "Fare": 9.2167, - "Name": "Osen, Mr. Olaf Elon", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17593", - "Parch": 0, - "Cabin": "B86", - "PassengerId": 140, - "Age": 24, - "Fare": 79.2, - "Name": "Giglio, Mr. Victor", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2678", - "Parch": 2, - "Cabin": null, - "PassengerId": 141, - "Age": null, - "Fare": 15.2458, - "Name": "Boulos, Mrs. Joseph (Sultana)", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347081", - "Parch": 0, - "Cabin": null, - "PassengerId": 142, - "Age": 22, - "Fare": 7.75, - "Name": "Nysten, Miss. Anna Sofia", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "STON/O2. 3101279", - "Parch": 0, - "Cabin": null, - "PassengerId": 143, - "Age": 24, - "Fare": 15.85, - "Name": "Hakkarainen, Mrs. Pekka Pietari (Elin Matilda Dolck)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "365222", - "Parch": 0, - "Cabin": null, - "PassengerId": 144, - "Age": 19, - "Fare": 6.75, - "Name": "Burke, Mr. Jeremiah", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "231945", - "Parch": 0, - "Cabin": null, - "PassengerId": 145, - "Age": 18, - "Fare": 11.5, - "Name": "Andrew, Mr. Edgardo Samuel", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 33112", - "Parch": 1, - "Cabin": null, - "PassengerId": 146, - "Age": 19, - "Fare": 36.75, - "Name": "Nicholls, Mr. Joseph Charles", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350043", - "Parch": 0, - "Cabin": null, - "PassengerId": 147, - "Age": 27, - "Fare": 7.7958, - "Name": "Andersson, Mr. August Edvard (\"Wennerstrom\")", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "W./C. 6608", - "Parch": 2, - "Cabin": null, - "PassengerId": 148, - "Age": 9, - "Fare": 34.375, - "Name": "Ford, Miss. Robina Maggie \"Ruby\"", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "230080", - "Parch": 2, - "Cabin": "F2", - "PassengerId": 149, - "Age": 36.5, - "Fare": 26, - "Name": "Navratil, Mr. Michel (\"Louis M Hoffman\")", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "244310", - "Parch": 0, - "Cabin": null, - "PassengerId": 150, - "Age": 42, - "Fare": 13, - "Name": "Byles, Rev. Thomas Roussel Davids", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.O.P. 1166", - "Parch": 0, - "Cabin": null, - "PassengerId": 151, - "Age": 51, - "Fare": 12.525, - "Name": "Bateman, Rev. Robert James", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113776", - "Parch": 0, - "Cabin": "C2", - "PassengerId": 152, - "Age": 22, - "Fare": 66.6, - "Name": "Pears, Mrs. Thomas (Edith Wearne)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A.5. 11206", - "Parch": 0, - "Cabin": null, - "PassengerId": 153, - "Age": 55.5, - "Fare": 8.05, - "Name": "Meo, Mr. Alfonzo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5. 851", - "Parch": 2, - "Cabin": null, - "PassengerId": 154, - "Age": 40.5, - "Fare": 14.5, - "Name": "van Billiard, Mr. Austin Blyler", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "Fa 265302", - "Parch": 0, - "Cabin": null, - "PassengerId": 155, - "Age": null, - "Fare": 7.3125, - "Name": "Olsen, Mr. Ole Martin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17597", - "Parch": 1, - "Cabin": null, - "PassengerId": 156, - "Age": 51, - "Fare": 61.3792, - "Name": "Williams, Mr. Charles Duane", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "35851", - "Parch": 0, - "Cabin": null, - "PassengerId": 157, - "Age": 16, - "Fare": 7.7333, - "Name": "Gilnagh, Miss. Katherine \"Katie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 392090", - "Parch": 0, - "Cabin": null, - "PassengerId": 158, - "Age": 30, - "Fare": 8.05, - "Name": "Corn, Mr. Harry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315037", - "Parch": 0, - "Cabin": null, - "PassengerId": 159, - "Age": null, - "Fare": 8.6625, - "Name": "Smiljanic, Mr. Mile", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 160, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Master. Thomas Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "371362", - "Parch": 1, - "Cabin": null, - "PassengerId": 161, - "Age": 44, - "Fare": 16.1, - "Name": "Cribb, Mr. John Hatfield", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 33595", - "Parch": 0, - "Cabin": null, - "PassengerId": 162, - "Age": 40, - "Fare": 15.75, - "Name": "Watt, Mrs. James (Elizabeth \"Bessie\" Inglis Milne)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347068", - "Parch": 0, - "Cabin": null, - "PassengerId": 163, - "Age": 26, - "Fare": 7.775, - "Name": "Bengtsson, Mr. John Viktor", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315093", - "Parch": 0, - "Cabin": null, - "PassengerId": 164, - "Age": 17, - "Fare": 8.6625, - "Name": "Calic, Mr. Jovo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "3101295", - "Parch": 1, - "Cabin": null, - "PassengerId": 165, - "Age": 1, - "Fare": 39.6875, - "Name": "Panula, Master. Eino Viljami", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "363291", - "Parch": 2, - "Cabin": null, - "PassengerId": 166, - "Age": 9, - "Fare": 20.525, - "Name": "Goldsmith, Master. Frank John William \"Frankie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113505", - "Parch": 1, - "Cabin": "E33", - "PassengerId": 167, - "Age": null, - "Fare": 55, - "Name": "Chibnall, Mrs. (Edith Martha Bowerman)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "347088", - "Parch": 4, - "Cabin": null, - "PassengerId": 168, - "Age": 45, - "Fare": 27.9, - "Name": "Skoog, Mrs. William (Anna Bernhardina Karlsson)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17318", - "Parch": 0, - "Cabin": null, - "PassengerId": 169, - "Age": null, - "Fare": 25.925, - "Name": "Baumann, Mr. John D", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 170, - "Age": 28, - "Fare": 56.4958, - "Name": "Ling, Mr. Lee", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "111240", - "Parch": 0, - "Cabin": "B19", - "PassengerId": 171, - "Age": 61, - "Fare": 33.5, - "Name": "Van der hoef, Mr. Wyckoff", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "382652", - "Parch": 1, - "Cabin": null, - "PassengerId": 172, - "Age": 4, - "Fare": 29.125, - "Name": "Rice, Master. Arthur", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347742", - "Parch": 1, - "Cabin": null, - "PassengerId": 173, - "Age": 1, - "Fare": 11.1333, - "Name": "Johnson, Miss. Eleanor Ileen", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101280", - "Parch": 0, - "Cabin": null, - "PassengerId": 174, - "Age": 21, - "Fare": 7.925, - "Name": "Sivola, Mr. Antti Wilhelm", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "17764", - "Parch": 0, - "Cabin": "A7", - "PassengerId": 175, - "Age": 56, - "Fare": 30.6958, - "Name": "Smith, Mr. James Clinch", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "350404", - "Parch": 1, - "Cabin": null, - "PassengerId": 176, - "Age": 18, - "Fare": 7.8542, - "Name": "Klasen, Mr. Klas Albin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "4133", - "Parch": 1, - "Cabin": null, - "PassengerId": 177, - "Age": null, - "Fare": 25.4667, - "Name": "Lefebre, Master. Henry Forbes", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17595", - "Parch": 0, - "Cabin": "C49", - "PassengerId": 178, - "Age": 50, - "Fare": 28.7125, - "Name": "Isham, Miss. Ann Elizabeth", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "250653", - "Parch": 0, - "Cabin": null, - "PassengerId": 179, - "Age": 30, - "Fare": 13, - "Name": "Hale, Mr. Reginald", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "LINE", - "Parch": 0, - "Cabin": null, - "PassengerId": 180, - "Age": 36, - "Fare": 0, - "Name": "Leonard, Mr. Lionel", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 181, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Miss. Constance Gladys", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SC/PARIS 2131", - "Parch": 0, - "Cabin": null, - "PassengerId": 182, - "Age": null, - "Fare": 15.05, - "Name": "Pernot, Mr. Rene", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "347077", - "Parch": 2, - "Cabin": null, - "PassengerId": 183, - "Age": 9, - "Fare": 31.3875, - "Name": "Asplund, Master. Clarence Gustaf Hugo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "230136", - "Parch": 1, - "Cabin": "F4", - "PassengerId": 184, - "Age": 1, - "Fare": 39, - "Name": "Becker, Master. Richard F", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315153", - "Parch": 2, - "Cabin": null, - "PassengerId": 185, - "Age": 4, - "Fare": 22.025, - "Name": "Kink-Heilmann, Miss. Luise Gretchen", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113767", - "Parch": 0, - "Cabin": "A32", - "PassengerId": 186, - "Age": null, - "Fare": 50, - "Name": "Rood, Mr. Hugh Roscoe", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "370365", - "Parch": 0, - "Cabin": null, - "PassengerId": 187, - "Age": null, - "Fare": 15.5, - "Name": "O'Brien, Mrs. Thomas (Johanna \"Hannah\" Godfrey)", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "111428", - "Parch": 0, - "Cabin": null, - "PassengerId": 188, - "Age": 45, - "Fare": 26.55, - "Name": "Romaine, Mr. Charles Hallace (\"Mr C Rolmane\")", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "364849", - "Parch": 1, - "Cabin": null, - "PassengerId": 189, - "Age": 40, - "Fare": 15.5, - "Name": "Bourke, Mr. John", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349247", - "Parch": 0, - "Cabin": null, - "PassengerId": 190, - "Age": 36, - "Fare": 7.8958, - "Name": "Turcin, Mr. Stjepan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "234604", - "Parch": 0, - "Cabin": null, - "PassengerId": 191, - "Age": 32, - "Fare": 13, - "Name": "Pinsky, Mrs. (Rosa)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "28424", - "Parch": 0, - "Cabin": null, - "PassengerId": 192, - "Age": 19, - "Fare": 13, - "Name": "Carbines, Mr. William", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "350046", - "Parch": 0, - "Cabin": null, - "PassengerId": 193, - "Age": 19, - "Fare": 7.8542, - "Name": "Andersen-Jensen, Miss. Carla Christine Nielsine", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "230080", - "Parch": 1, - "Cabin": "F2", - "PassengerId": 194, - "Age": 3, - "Fare": 26, - "Name": "Navratil, Master. Michel M", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17610", - "Parch": 0, - "Cabin": "B4", - "PassengerId": 195, - "Age": 44, - "Fare": 27.7208, - "Name": "Brown, Mrs. James Joseph (Margaret Tobin)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17569", - "Parch": 0, - "Cabin": "B80", - "PassengerId": 196, - "Age": 58, - "Fare": 146.5208, - "Name": "Lurette, Miss. Elise", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "368703", - "Parch": 0, - "Cabin": null, - "PassengerId": 197, - "Age": null, - "Fare": 7.75, - "Name": "Mernagh, Mr. Robert", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "4579", - "Parch": 1, - "Cabin": null, - "PassengerId": 198, - "Age": 42, - "Fare": 8.4042, - "Name": "Olsen, Mr. Karl Siegwart Andreas", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370370", - "Parch": 0, - "Cabin": null, - "PassengerId": 199, - "Age": null, - "Fare": 7.75, - "Name": "Madigan, Miss. Margaret \"Maggie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "248747", - "Parch": 0, - "Cabin": null, - "PassengerId": 200, - "Age": 24, - "Fare": 13, - "Name": "Yrois, Miss. Henriette (\"Mrs Harbeck\")", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "345770", - "Parch": 0, - "Cabin": null, - "PassengerId": 201, - "Age": 28, - "Fare": 9.5, - "Name": "Vande Walle, Mr. Nestor Cyriel", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 202, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Mr. Frederick", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "3101264", - "Parch": 0, - "Cabin": null, - "PassengerId": 203, - "Age": 34, - "Fare": 6.4958, - "Name": "Johanson, Mr. Jakob Alfred", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2628", - "Parch": 0, - "Cabin": null, - "PassengerId": 204, - "Age": 45.5, - "Fare": 7.225, - "Name": "Youseff, Mr. Gerious", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5 3540", - "Parch": 0, - "Cabin": null, - "PassengerId": 205, - "Age": 18, - "Fare": 8.05, - "Name": "Cohen, Mr. Gurshon \"Gus\"", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347054", - "Parch": 1, - "Cabin": "G6", - "PassengerId": 206, - "Age": 2, - "Fare": 10.4625, - "Name": "Strom, Miss. Telma Matilda", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "3101278", - "Parch": 0, - "Cabin": null, - "PassengerId": 207, - "Age": 32, - "Fare": 15.85, - "Name": "Backstrom, Mr. Karl Alfred", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2699", - "Parch": 0, - "Cabin": null, - "PassengerId": 208, - "Age": 26, - "Fare": 18.7875, - "Name": "Albimona, Mr. Nassef Cassem", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "367231", - "Parch": 0, - "Cabin": null, - "PassengerId": 209, - "Age": 16, - "Fare": 7.75, - "Name": "Carr, Miss. Helen \"Ellen\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "112277", - "Parch": 0, - "Cabin": "A31", - "PassengerId": 210, - "Age": 40, - "Fare": 31, - "Name": "Blank, Mr. Henry", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 3101311", - "Parch": 0, - "Cabin": null, - "PassengerId": 211, - "Age": 24, - "Fare": 7.05, - "Name": "Ali, Mr. Ahmed", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "F.C.C. 13528", - "Parch": 0, - "Cabin": null, - "PassengerId": 212, - "Age": 35, - "Fare": 21, - "Name": "Cameron, Miss. Clear Annie", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A/5 21174", - "Parch": 0, - "Cabin": null, - "PassengerId": 213, - "Age": 22, - "Fare": 7.25, - "Name": "Perkin, Mr. John Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250646", - "Parch": 0, - "Cabin": null, - "PassengerId": 214, - "Age": 30, - "Fare": 13, - "Name": "Givard, Mr. Hans Kristensen", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "367229", - "Parch": 0, - "Cabin": null, - "PassengerId": 215, - "Age": null, - "Fare": 7.75, - "Name": "Kiernan, Mr. Philip", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "35273", - "Parch": 0, - "Cabin": "D36", - "PassengerId": 216, - "Age": 31, - "Fare": 113.275, - "Name": "Newell, Miss. Madeleine", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O2. 3101283", - "Parch": 0, - "Cabin": null, - "PassengerId": 217, - "Age": 27, - "Fare": 7.925, - "Name": "Honkanen, Miss. Eliina", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "243847", - "Parch": 0, - "Cabin": null, - "PassengerId": 218, - "Age": 42, - "Fare": 27, - "Name": "Jacobsohn, Mr. Sidney Samuel", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "11813", - "Parch": 0, - "Cabin": "D15", - "PassengerId": 219, - "Age": 32, - "Fare": 76.2917, - "Name": "Bazzani, Miss. Albina", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "W/C 14208", - "Parch": 0, - "Cabin": null, - "PassengerId": 220, - "Age": 30, - "Fare": 10.5, - "Name": "Harris, Mr. Walter", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 392089", - "Parch": 0, - "Cabin": null, - "PassengerId": 221, - "Age": 16, - "Fare": 8.05, - "Name": "Sunderland, Mr. Victor Francis", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "220367", - "Parch": 0, - "Cabin": null, - "PassengerId": 222, - "Age": 27, - "Fare": 13, - "Name": "Bracken, Mr. James H", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "21440", - "Parch": 0, - "Cabin": null, - "PassengerId": 223, - "Age": 51, - "Fare": 8.05, - "Name": "Green, Mr. George Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349234", - "Parch": 0, - "Cabin": null, - "PassengerId": 224, - "Age": null, - "Fare": 7.8958, - "Name": "Nenkoff, Mr. Christo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "19943", - "Parch": 0, - "Cabin": "C93", - "PassengerId": 225, - "Age": 38, - "Fare": 90, - "Name": "Hoyt, Mr. Frederick Maxfield", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PP 4348", - "Parch": 0, - "Cabin": null, - "PassengerId": 226, - "Age": 22, - "Fare": 9.35, - "Name": "Berglund, Mr. Karl Ivar Sven", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SW/PP 751", - "Parch": 0, - "Cabin": null, - "PassengerId": 227, - "Age": 19, - "Fare": 10.5, - "Name": "Mellors, Mr. William John", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5 21173", - "Parch": 0, - "Cabin": null, - "PassengerId": 228, - "Age": 20.5, - "Fare": 7.25, - "Name": "Lovell, Mr. John Hall (\"Henry\")", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "236171", - "Parch": 0, - "Cabin": null, - "PassengerId": 229, - "Age": 18, - "Fare": 13, - "Name": "Fahlstrom, Mr. Arne Jonas", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "4133", - "Parch": 1, - "Cabin": null, - "PassengerId": 230, - "Age": null, - "Fare": 25.4667, - "Name": "Lefebre, Miss. Mathilde", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "36973", - "Parch": 0, - "Cabin": "C83", - "PassengerId": 231, - "Age": 35, - "Fare": 83.475, - "Name": "Harris, Mrs. Henry Birkhardt (Irene Wallach)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347067", - "Parch": 0, - "Cabin": null, - "PassengerId": 232, - "Age": 29, - "Fare": 7.775, - "Name": "Larsson, Mr. Bengt Edvin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "237442", - "Parch": 0, - "Cabin": null, - "PassengerId": 233, - "Age": 59, - "Fare": 13.5, - "Name": "Sjostedt, Mr. Ernst Adolf", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "347077", - "Parch": 2, - "Cabin": null, - "PassengerId": 234, - "Age": 5, - "Fare": 31.3875, - "Name": "Asplund, Miss. Lillian Gertrud", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C.A. 29566", - "Parch": 0, - "Cabin": null, - "PassengerId": 235, - "Age": 24, - "Fare": 10.5, - "Name": "Leyson, Mr. Robert William Norman", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "W./C. 6609", - "Parch": 0, - "Cabin": null, - "PassengerId": 236, - "Age": null, - "Fare": 7.55, - "Name": "Harknett, Miss. Alice Phoebe", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "26707", - "Parch": 0, - "Cabin": null, - "PassengerId": 237, - "Age": 44, - "Fare": 26, - "Name": "Hold, Mr. Stephen", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 31921", - "Parch": 2, - "Cabin": null, - "PassengerId": 238, - "Age": 8, - "Fare": 26.25, - "Name": "Collyer, Miss. Marjorie \"Lottie\"", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "28665", - "Parch": 0, - "Cabin": null, - "PassengerId": 239, - "Age": 19, - "Fare": 10.5, - "Name": "Pengelly, Mr. Frederick William", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SCO/W 1585", - "Parch": 0, - "Cabin": null, - "PassengerId": 240, - "Age": 33, - "Fare": 12.275, - "Name": "Hunt, Mr. George Henry", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2665", - "Parch": 0, - "Cabin": null, - "PassengerId": 241, - "Age": null, - "Fare": 14.4542, - "Name": "Zabour, Miss. Thamine", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "367230", - "Parch": 0, - "Cabin": null, - "PassengerId": 242, - "Age": null, - "Fare": 15.5, - "Name": "Murphy, Miss. Katherine \"Kate\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "W./C. 14263", - "Parch": 0, - "Cabin": null, - "PassengerId": 243, - "Age": 29, - "Fare": 10.5, - "Name": "Coleridge, Mr. Reginald Charles", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101275", - "Parch": 0, - "Cabin": null, - "PassengerId": 244, - "Age": 22, - "Fare": 7.125, - "Name": "Maenpaa, Mr. Matti Alexanteri", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2694", - "Parch": 0, - "Cabin": null, - "PassengerId": 245, - "Age": 30, - "Fare": 7.225, - "Name": "Attalah, Mr. Sleiman", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "19928", - "Parch": 0, - "Cabin": "C78", - "PassengerId": 246, - "Age": 44, - "Fare": 90, - "Name": "Minahan, Dr. William Edward", - "Survived": false, - "Pclass": 1, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347071", - "Parch": 0, - "Cabin": null, - "PassengerId": 247, - "Age": 25, - "Fare": 7.775, - "Name": "Lindahl, Miss. Agda Thorilda Viktoria", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "250649", - "Parch": 2, - "Cabin": null, - "PassengerId": 248, - "Age": 24, - "Fare": 14.5, - "Name": "Hamalainen, Mrs. William (Anna)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "11751", - "Parch": 1, - "Cabin": "D35", - "PassengerId": 249, - "Age": 37, - "Fare": 52.5542, - "Name": "Beckwith, Mr. Richard Leonard", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "244252", - "Parch": 0, - "Cabin": null, - "PassengerId": 250, - "Age": 54, - "Fare": 26, - "Name": "Carter, Rev. Ernest Courtenay", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "362316", - "Parch": 0, - "Cabin": null, - "PassengerId": 251, - "Age": null, - "Fare": 7.25, - "Name": "Reed, Mr. James George", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347054", - "Parch": 1, - "Cabin": "G6", - "PassengerId": 252, - "Age": 29, - "Fare": 10.4625, - "Name": "Strom, Mrs. Wilhelm (Elna Matilda Persson)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113514", - "Parch": 0, - "Cabin": "C87", - "PassengerId": 253, - "Age": 62, - "Fare": 26.55, - "Name": "Stead, Mr. William Thomas", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "A/5. 3336", - "Parch": 0, - "Cabin": null, - "PassengerId": 254, - "Age": 30, - "Fare": 16.1, - "Name": "Lobb, Mr. William Arthur", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370129", - "Parch": 2, - "Cabin": null, - "PassengerId": 255, - "Age": 41, - "Fare": 20.2125, - "Name": "Rosblom, Mrs. Viktor (Helena Wilhelmina)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2650", - "Parch": 2, - "Cabin": null, - "PassengerId": 256, - "Age": 29, - "Fare": 15.2458, - "Name": "Touma, Mrs. Darwis (Hanne Youssef Razi)", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17585", - "Parch": 0, - "Cabin": null, - "PassengerId": 257, - "Age": null, - "Fare": 79.2, - "Name": "Thorne, Mrs. Gertrude Maybelle", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "110152", - "Parch": 0, - "Cabin": "B77", - "PassengerId": 258, - "Age": 30, - "Fare": 86.5, - "Name": "Cherry, Miss. Gladys", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17755", - "Parch": 0, - "Cabin": null, - "PassengerId": 259, - "Age": 35, - "Fare": 512.3292, - "Name": "Ward, Miss. Anna", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "230433", - "Parch": 1, - "Cabin": null, - "PassengerId": 260, - "Age": 50, - "Fare": 26, - "Name": "Parrish, Mrs. (Lutie Davis)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "384461", - "Parch": 0, - "Cabin": null, - "PassengerId": 261, - "Age": null, - "Fare": 7.75, - "Name": "Smith, Mr. Thomas", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "347077", - "Parch": 2, - "Cabin": null, - "PassengerId": 262, - "Age": 3, - "Fare": 31.3875, - "Name": "Asplund, Master. Edvin Rojj Felix", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "110413", - "Parch": 1, - "Cabin": "E67", - "PassengerId": 263, - "Age": 52, - "Fare": 79.65, - "Name": "Taussig, Mr. Emil", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "112059", - "Parch": 0, - "Cabin": "B94", - "PassengerId": 264, - "Age": 40, - "Fare": 0, - "Name": "Harrison, Mr. William", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "382649", - "Parch": 0, - "Cabin": null, - "PassengerId": 265, - "Age": null, - "Fare": 7.75, - "Name": "Henry, Miss. Delia", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C.A. 17248", - "Parch": 0, - "Cabin": null, - "PassengerId": 266, - "Age": 36, - "Fare": 10.5, - "Name": "Reeves, Mr. David", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "3101295", - "Parch": 1, - "Cabin": null, - "PassengerId": 267, - "Age": 16, - "Fare": 39.6875, - "Name": "Panula, Mr. Ernesti Arvid", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347083", - "Parch": 0, - "Cabin": null, - "PassengerId": 268, - "Age": 25, - "Fare": 7.775, - "Name": "Persson, Mr. Ernst Ulrik", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17582", - "Parch": 1, - "Cabin": "C125", - "PassengerId": 269, - "Age": 58, - "Fare": 153.4625, - "Name": "Graham, Mrs. William Thompson (Edith Junkins)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17760", - "Parch": 0, - "Cabin": "C99", - "PassengerId": 270, - "Age": 35, - "Fare": 135.6333, - "Name": "Bissette, Miss. Amelia", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113798", - "Parch": 0, - "Cabin": null, - "PassengerId": 271, - "Age": null, - "Fare": 31, - "Name": "Cairns, Mr. Alexander", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "LINE", - "Parch": 0, - "Cabin": null, - "PassengerId": 272, - "Age": 25, - "Fare": 0, - "Name": "Tornquist, Mr. William Henry", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250644", - "Parch": 1, - "Cabin": null, - "PassengerId": 273, - "Age": 41, - "Fare": 19.5, - "Name": "Mellinger, Mrs. (Elizabeth Anne Maidment)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17596", - "Parch": 1, - "Cabin": "C118", - "PassengerId": 274, - "Age": 37, - "Fare": 29.7, - "Name": "Natsch, Mr. Charles H", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370375", - "Parch": 0, - "Cabin": null, - "PassengerId": 275, - "Age": null, - "Fare": 7.75, - "Name": "Healy, Miss. Hanora \"Nora\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "13502", - "Parch": 0, - "Cabin": "D7", - "PassengerId": 276, - "Age": 63, - "Fare": 77.9583, - "Name": "Andrews, Miss. Kornelia Theodosia", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347073", - "Parch": 0, - "Cabin": null, - "PassengerId": 277, - "Age": 45, - "Fare": 7.75, - "Name": "Lindblom, Miss. Augusta Charlotta", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "239853", - "Parch": 0, - "Cabin": null, - "PassengerId": 278, - "Age": null, - "Fare": 0, - "Name": "Parkes, Mr. Francis \"Frank\"", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "382652", - "Parch": 1, - "Cabin": null, - "PassengerId": 279, - "Age": 7, - "Fare": 29.125, - "Name": "Rice, Master. Eric", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 2673", - "Parch": 1, - "Cabin": null, - "PassengerId": 280, - "Age": 35, - "Fare": 20.25, - "Name": "Abbott, Mrs. Stanton (Rosa Hunt)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "336439", - "Parch": 0, - "Cabin": null, - "PassengerId": 281, - "Age": 65, - "Fare": 7.75, - "Name": "Duane, Mr. Frank", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347464", - "Parch": 0, - "Cabin": null, - "PassengerId": 282, - "Age": 28, - "Fare": 7.8542, - "Name": "Olsson, Mr. Nils Johan Goransson", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345778", - "Parch": 0, - "Cabin": null, - "PassengerId": 283, - "Age": 16, - "Fare": 9.5, - "Name": "de Pelsmaeker, Mr. Alfons", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5. 10482", - "Parch": 0, - "Cabin": null, - "PassengerId": 284, - "Age": 19, - "Fare": 8.05, - "Name": "Dorking, Mr. Edward Arthur", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113056", - "Parch": 0, - "Cabin": "A19", - "PassengerId": 285, - "Age": null, - "Fare": 26, - "Name": "Smith, Mr. Richard William", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349239", - "Parch": 0, - "Cabin": null, - "PassengerId": 286, - "Age": 33, - "Fare": 8.6625, - "Name": "Stankovic, Mr. Ivan", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345774", - "Parch": 0, - "Cabin": null, - "PassengerId": 287, - "Age": 30, - "Fare": 9.5, - "Name": "de Mulder, Mr. Theodore", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349206", - "Parch": 0, - "Cabin": null, - "PassengerId": 288, - "Age": 22, - "Fare": 7.8958, - "Name": "Naidenoff, Mr. Penko", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "237798", - "Parch": 0, - "Cabin": null, - "PassengerId": 289, - "Age": 42, - "Fare": 13, - "Name": "Hosono, Mr. Masabumi", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370373", - "Parch": 0, - "Cabin": null, - "PassengerId": 290, - "Age": 22, - "Fare": 7.75, - "Name": "Connolly, Miss. Kate", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "19877", - "Parch": 0, - "Cabin": null, - "PassengerId": 291, - "Age": 26, - "Fare": 78.85, - "Name": "Barber, Miss. Ellen \"Nellie\"", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "11967", - "Parch": 0, - "Cabin": "B49", - "PassengerId": 292, - "Age": 19, - "Fare": 91.0792, - "Name": "Bishop, Mrs. Dickinson H (Helen Walton)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SC/Paris 2163", - "Parch": 0, - "Cabin": "D", - "PassengerId": 293, - "Age": 36, - "Fare": 12.875, - "Name": "Levy, Mr. Rene Jacques", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349236", - "Parch": 0, - "Cabin": null, - "PassengerId": 294, - "Age": 24, - "Fare": 8.85, - "Name": "Haas, Miss. Aloisia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349233", - "Parch": 0, - "Cabin": null, - "PassengerId": 295, - "Age": 24, - "Fare": 7.8958, - "Name": "Mineff, Mr. Ivan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17612", - "Parch": 0, - "Cabin": null, - "PassengerId": 296, - "Age": null, - "Fare": 27.7208, - "Name": "Lewy, Mr. Ervin G", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2693", - "Parch": 0, - "Cabin": null, - "PassengerId": 297, - "Age": 23.5, - "Fare": 7.2292, - "Name": "Hanna, Mr. Mansour", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113781", - "Parch": 2, - "Cabin": "C22 C26", - "PassengerId": 298, - "Age": 2, - "Fare": 151.55, - "Name": "Allison, Miss. Helen Loraine", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "19988", - "Parch": 0, - "Cabin": "C106", - "PassengerId": 299, - "Age": null, - "Fare": 30.5, - "Name": "Saalfeld, Mr. Adolphe", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17558", - "Parch": 1, - "Cabin": "B58 B60", - "PassengerId": 300, - "Age": 50, - "Fare": 247.5208, - "Name": "Baxter, Mrs. James (Helene DeLaudeniere Chaput)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "9234", - "Parch": 0, - "Cabin": null, - "PassengerId": 301, - "Age": null, - "Fare": 7.75, - "Name": "Kelly, Miss. Anna Katherine \"Annie Kate\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "367226", - "Parch": 0, - "Cabin": null, - "PassengerId": 302, - "Age": null, - "Fare": 23.25, - "Name": "McCoy, Mr. Bernard", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "LINE", - "Parch": 0, - "Cabin": null, - "PassengerId": 303, - "Age": 19, - "Fare": 0, - "Name": "Johnson, Mr. William Cahoone Jr", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "226593", - "Parch": 0, - "Cabin": "E101", - "PassengerId": 304, - "Age": null, - "Fare": 12.35, - "Name": "Keane, Miss. Nora A", - "Survived": true, - "Pclass": 2, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A/5 2466", - "Parch": 0, - "Cabin": null, - "PassengerId": 305, - "Age": null, - "Fare": 8.05, - "Name": "Williams, Mr. Howard Hugh \"Harry\"", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113781", - "Parch": 2, - "Cabin": "C22 C26", - "PassengerId": 306, - "Age": 0.92, - "Fare": 151.55, - "Name": "Allison, Master. Hudson Trevor", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "17421", - "Parch": 0, - "Cabin": null, - "PassengerId": 307, - "Age": null, - "Fare": 110.8833, - "Name": "Fleming, Miss. Margaret", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "PC 17758", - "Parch": 0, - "Cabin": "C65", - "PassengerId": 308, - "Age": 17, - "Fare": 108.9, - "Name": "Penasco y Castellana, Mrs. Victor de Satode (Maria Josefa Perez de Soto y Vallejo)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "P/PP 3381", - "Parch": 0, - "Cabin": null, - "PassengerId": 309, - "Age": 30, - "Fare": 24, - "Name": "Abelson, Mr. Samuel", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17485", - "Parch": 0, - "Cabin": "E36", - "PassengerId": 310, - "Age": 30, - "Fare": 56.9292, - "Name": "Francatelli, Miss. Laura Mabel", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "11767", - "Parch": 0, - "Cabin": "C54", - "PassengerId": 311, - "Age": 24, - "Fare": 83.1583, - "Name": "Hays, Miss. Margaret Bechstein", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "PC 17608", - "Parch": 2, - "Cabin": "B57 B59 B63 B66", - "PassengerId": 312, - "Age": 18, - "Fare": 262.375, - "Name": "Ryerson, Miss. Emily Borie", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "250651", - "Parch": 1, - "Cabin": null, - "PassengerId": 313, - "Age": 26, - "Fare": 26, - "Name": "Lahtinen, Mrs. William (Anna Sylfven)", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349243", - "Parch": 0, - "Cabin": null, - "PassengerId": 314, - "Age": 28, - "Fare": 7.8958, - "Name": "Hendekovic, Mr. Ignjac", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "F.C.C. 13529", - "Parch": 1, - "Cabin": null, - "PassengerId": 315, - "Age": 43, - "Fare": 26.25, - "Name": "Hart, Mr. Benjamin", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347470", - "Parch": 0, - "Cabin": null, - "PassengerId": 316, - "Age": 26, - "Fare": 7.8542, - "Name": "Nilsson, Miss. Helmina Josefina", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "244367", - "Parch": 0, - "Cabin": null, - "PassengerId": 317, - "Age": 24, - "Fare": 26, - "Name": "Kantor, Mrs. Sinai (Miriam Sternin)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "29011", - "Parch": 0, - "Cabin": null, - "PassengerId": 318, - "Age": 54, - "Fare": 14, - "Name": "Moraweck, Dr. Ernest", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "36928", - "Parch": 2, - "Cabin": "C7", - "PassengerId": 319, - "Age": 31, - "Fare": 164.8667, - "Name": "Wick, Miss. Mary Natalie", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "16966", - "Parch": 1, - "Cabin": "E34", - "PassengerId": 320, - "Age": 40, - "Fare": 134.5, - "Name": "Spedden, Mrs. Frederic Oakley (Margaretta Corning Stone)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A/5 21172", - "Parch": 0, - "Cabin": null, - "PassengerId": 321, - "Age": 22, - "Fare": 7.25, - "Name": "Dennis, Mr. Samuel", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349219", - "Parch": 0, - "Cabin": null, - "PassengerId": 322, - "Age": 27, - "Fare": 7.8958, - "Name": "Danoff, Mr. Yoto", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "234818", - "Parch": 0, - "Cabin": null, - "PassengerId": 323, - "Age": 30, - "Fare": 12.35, - "Name": "Slayter, Miss. Hilda Mary", - "Survived": true, - "Pclass": 2, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "248738", - "Parch": 1, - "Cabin": null, - "PassengerId": 324, - "Age": 22, - "Fare": 29, - "Name": "Caldwell, Mrs. Albert Francis (Sylvia Mae Harbaugh)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 325, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Mr. George John Jr", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17760", - "Parch": 0, - "Cabin": "C32", - "PassengerId": 326, - "Age": 36, - "Fare": 135.6333, - "Name": "Young, Miss. Marie Grice", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "345364", - "Parch": 0, - "Cabin": null, - "PassengerId": 327, - "Age": 61, - "Fare": 6.2375, - "Name": "Nysveen, Mr. Johan Hansen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "28551", - "Parch": 0, - "Cabin": "D", - "PassengerId": 328, - "Age": 36, - "Fare": 13, - "Name": "Ball, Mrs. (Ada E Hall)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "363291", - "Parch": 1, - "Cabin": null, - "PassengerId": 329, - "Age": 31, - "Fare": 20.525, - "Name": "Goldsmith, Mrs. Frank John (Emily Alice Brown)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "111361", - "Parch": 1, - "Cabin": "B18", - "PassengerId": 330, - "Age": 16, - "Fare": 57.9792, - "Name": "Hippach, Miss. Jean Gertrude", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "367226", - "Parch": 0, - "Cabin": null, - "PassengerId": 331, - "Age": null, - "Fare": 23.25, - "Name": "McCoy, Miss. Agnes", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113043", - "Parch": 0, - "Cabin": "C124", - "PassengerId": 332, - "Age": 45.5, - "Fare": 28.5, - "Name": "Partner, Mr. Austen", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17582", - "Parch": 1, - "Cabin": "C91", - "PassengerId": 333, - "Age": 38, - "Fare": 153.4625, - "Name": "Graham, Mr. George Edward", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "345764", - "Parch": 0, - "Cabin": null, - "PassengerId": 334, - "Age": 16, - "Fare": 18, - "Name": "Vander Planke, Mr. Leo Edmondus", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17611", - "Parch": 0, - "Cabin": null, - "PassengerId": 335, - "Age": null, - "Fare": 133.65, - "Name": "Frauenthal, Mrs. Henry William (Clara Heinsheimer)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349225", - "Parch": 0, - "Cabin": null, - "PassengerId": 336, - "Age": null, - "Fare": 7.8958, - "Name": "Denkoff, Mr. Mitto", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113776", - "Parch": 0, - "Cabin": "C2", - "PassengerId": 337, - "Age": 29, - "Fare": 66.6, - "Name": "Pears, Mr. Thomas Clinton", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "16966", - "Parch": 0, - "Cabin": "E40", - "PassengerId": 338, - "Age": 41, - "Fare": 134.5, - "Name": "Burns, Miss. Elizabeth Margaret", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "7598", - "Parch": 0, - "Cabin": null, - "PassengerId": 339, - "Age": 45, - "Fare": 8.05, - "Name": "Dahl, Mr. Karl Edwart", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113784", - "Parch": 0, - "Cabin": "T", - "PassengerId": 340, - "Age": 45, - "Fare": 35.5, - "Name": "Blackwell, Mr. Stephen Weart", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "230080", - "Parch": 1, - "Cabin": "F2", - "PassengerId": 341, - "Age": 2, - "Fare": 26, - "Name": "Navratil, Master. Edmond Roger", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "19950", - "Parch": 2, - "Cabin": "C23 C25 C27", - "PassengerId": 342, - "Age": 24, - "Fare": 263, - "Name": "Fortune, Miss. Alice Elizabeth", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "248740", - "Parch": 0, - "Cabin": null, - "PassengerId": 343, - "Age": 28, - "Fare": 13, - "Name": "Collander, Mr. Erik Gustaf", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "244361", - "Parch": 0, - "Cabin": null, - "PassengerId": 344, - "Age": 25, - "Fare": 13, - "Name": "Sedgwick, Mr. Charles Frederick Waddington", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "229236", - "Parch": 0, - "Cabin": null, - "PassengerId": 345, - "Age": 36, - "Fare": 13, - "Name": "Fox, Mr. Stanley Hubert", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248733", - "Parch": 0, - "Cabin": "F33", - "PassengerId": 346, - "Age": 24, - "Fare": 13, - "Name": "Brown, Miss. Amelia \"Mildred\"", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "31418", - "Parch": 0, - "Cabin": null, - "PassengerId": 347, - "Age": 40, - "Fare": 13, - "Name": "Smith, Miss. Marion Elsie", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "386525", - "Parch": 0, - "Cabin": null, - "PassengerId": 348, - "Age": null, - "Fare": 16.1, - "Name": "Davison, Mrs. Thomas Henry (Mary E Finck)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "C.A. 37671", - "Parch": 1, - "Cabin": null, - "PassengerId": 349, - "Age": 3, - "Fare": 15.9, - "Name": "Coutts, Master. William Loch \"William\"", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315088", - "Parch": 0, - "Cabin": null, - "PassengerId": 350, - "Age": 42, - "Fare": 8.6625, - "Name": "Dimic, Mr. Jovan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "7267", - "Parch": 0, - "Cabin": null, - "PassengerId": 351, - "Age": 23, - "Fare": 9.225, - "Name": "Odahl, Mr. Nils Martin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113510", - "Parch": 0, - "Cabin": "C128", - "PassengerId": 352, - "Age": null, - "Fare": 35, - "Name": "Williams-Lambert, Mr. Fletcher Fellows", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2695", - "Parch": 1, - "Cabin": null, - "PassengerId": 353, - "Age": 15, - "Fare": 7.2292, - "Name": "Elias, Mr. Tannous", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "349237", - "Parch": 0, - "Cabin": null, - "PassengerId": 354, - "Age": 25, - "Fare": 17.8, - "Name": "Arnold-Franchi, Mr. Josef", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2647", - "Parch": 0, - "Cabin": null, - "PassengerId": 355, - "Age": null, - "Fare": 7.225, - "Name": "Yousif, Mr. Wazli", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345783", - "Parch": 0, - "Cabin": null, - "PassengerId": 356, - "Age": 28, - "Fare": 9.5, - "Name": "Vanden Steen, Mr. Leo Peter", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113505", - "Parch": 1, - "Cabin": "E33", - "PassengerId": 357, - "Age": 22, - "Fare": 55, - "Name": "Bowerman, Miss. Elsie Edith", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "237671", - "Parch": 0, - "Cabin": null, - "PassengerId": 358, - "Age": 38, - "Fare": 13, - "Name": "Funk, Miss. Annie Clemmer", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "330931", - "Parch": 0, - "Cabin": null, - "PassengerId": 359, - "Age": null, - "Fare": 7.8792, - "Name": "McGovern, Miss. Mary", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "330980", - "Parch": 0, - "Cabin": null, - "PassengerId": 360, - "Age": null, - "Fare": 7.8792, - "Name": "Mockler, Miss. Helen Mary \"Ellie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "347088", - "Parch": 4, - "Cabin": null, - "PassengerId": 361, - "Age": 40, - "Fare": 27.9, - "Name": "Skoog, Mr. Wilhelm", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "SC/PARIS 2167", - "Parch": 0, - "Cabin": null, - "PassengerId": 362, - "Age": 29, - "Fare": 27.7208, - "Name": "del Carlo, Mr. Sebastiano", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2691", - "Parch": 1, - "Cabin": null, - "PassengerId": 363, - "Age": 45, - "Fare": 14.4542, - "Name": "Barbara, Mrs. (Catherine David)", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 3101310", - "Parch": 0, - "Cabin": null, - "PassengerId": 364, - "Age": 35, - "Fare": 7.05, - "Name": "Asim, Mr. Adola", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "370365", - "Parch": 0, - "Cabin": null, - "PassengerId": 365, - "Age": null, - "Fare": 15.5, - "Name": "O'Brien, Mr. Thomas", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C 7076", - "Parch": 0, - "Cabin": null, - "PassengerId": 366, - "Age": 30, - "Fare": 7.25, - "Name": "Adahl, Mr. Mauritz Nils Martin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "110813", - "Parch": 0, - "Cabin": "D37", - "PassengerId": 367, - "Age": 60, - "Fare": 75.25, - "Name": "Warren, Mrs. Frank Manley (Anna Sophia Atkinson)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2626", - "Parch": 0, - "Cabin": null, - "PassengerId": 368, - "Age": null, - "Fare": 7.2292, - "Name": "Moussa, Mrs. (Mantoura Boulos)", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "14313", - "Parch": 0, - "Cabin": null, - "PassengerId": 369, - "Age": null, - "Fare": 7.75, - "Name": "Jermyn, Miss. Annie", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17477", - "Parch": 0, - "Cabin": "B35", - "PassengerId": 370, - "Age": 24, - "Fare": 69.3, - "Name": "Aubart, Mme. Leontine Pauline", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "11765", - "Parch": 0, - "Cabin": "E50", - "PassengerId": 371, - "Age": 25, - "Fare": 55.4417, - "Name": "Harder, Mr. George Achilles", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "3101267", - "Parch": 0, - "Cabin": null, - "PassengerId": 372, - "Age": 18, - "Fare": 6.4958, - "Name": "Wiklund, Mr. Jakob Alfred", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "323951", - "Parch": 0, - "Cabin": null, - "PassengerId": 373, - "Age": 19, - "Fare": 8.05, - "Name": "Beavan, Mr. William Thomas", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17760", - "Parch": 0, - "Cabin": null, - "PassengerId": 374, - "Age": 22, - "Fare": 135.6333, - "Name": "Ringhini, Mr. Sante", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "349909", - "Parch": 1, - "Cabin": null, - "PassengerId": 375, - "Age": 3, - "Fare": 21.075, - "Name": "Palsson, Miss. Stina Viola", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "PC 17604", - "Parch": 0, - "Cabin": null, - "PassengerId": 376, - "Age": null, - "Fare": 82.1708, - "Name": "Meyer, Mrs. Edgar Joseph (Leila Saks)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C 7077", - "Parch": 0, - "Cabin": null, - "PassengerId": 377, - "Age": 22, - "Fare": 7.25, - "Name": "Landergren, Miss. Aurora Adelia", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113503", - "Parch": 2, - "Cabin": "C82", - "PassengerId": 378, - "Age": 27, - "Fare": 211.5, - "Name": "Widener, Mr. Harry Elkins", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2648", - "Parch": 0, - "Cabin": null, - "PassengerId": 379, - "Age": 20, - "Fare": 4.0125, - "Name": "Betros, Mr. Tannous", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347069", - "Parch": 0, - "Cabin": null, - "PassengerId": 380, - "Age": 19, - "Fare": 7.775, - "Name": "Gustafsson, Mr. Karl Gideon", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17757", - "Parch": 0, - "Cabin": null, - "PassengerId": 381, - "Age": 42, - "Fare": 227.525, - "Name": "Bidois, Miss. Rosalie", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2653", - "Parch": 2, - "Cabin": null, - "PassengerId": 382, - "Age": 1, - "Fare": 15.7417, - "Name": "Nakid, Miss. Maria (\"Mary\")", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101293", - "Parch": 0, - "Cabin": null, - "PassengerId": 383, - "Age": 32, - "Fare": 7.925, - "Name": "Tikkanen, Mr. Juho", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113789", - "Parch": 0, - "Cabin": null, - "PassengerId": 384, - "Age": 35, - "Fare": 52, - "Name": "Holverson, Mrs. Alexander Oskar (Mary Aline Towner)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349227", - "Parch": 0, - "Cabin": null, - "PassengerId": 385, - "Age": null, - "Fare": 7.8958, - "Name": "Plotcharsky, Mr. Vasil", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.O.C. 14879", - "Parch": 0, - "Cabin": null, - "PassengerId": 386, - "Age": 18, - "Fare": 73.5, - "Name": "Davies, Mr. Charles Henry", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 5, - "Ticket": "CA 2144", - "Parch": 2, - "Cabin": null, - "PassengerId": 387, - "Age": 1, - "Fare": 46.9, - "Name": "Goodwin, Master. Sidney Leonard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "27849", - "Parch": 0, - "Cabin": null, - "PassengerId": 388, - "Age": 36, - "Fare": 13, - "Name": "Buss, Miss. Kate", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "367655", - "Parch": 0, - "Cabin": null, - "PassengerId": 389, - "Age": null, - "Fare": 7.7292, - "Name": "Sadlier, Mr. Matthew", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SC 1748", - "Parch": 0, - "Cabin": null, - "PassengerId": 390, - "Age": 17, - "Fare": 12, - "Name": "Lehmann, Miss. Bertha", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "113760", - "Parch": 2, - "Cabin": "B96 B98", - "PassengerId": 391, - "Age": 36, - "Fare": 120, - "Name": "Carter, Mr. William Ernest", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350034", - "Parch": 0, - "Cabin": null, - "PassengerId": 392, - "Age": 21, - "Fare": 7.7958, - "Name": "Jansson, Mr. Carl Olof", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "3101277", - "Parch": 0, - "Cabin": null, - "PassengerId": 393, - "Age": 28, - "Fare": 7.925, - "Name": "Gustafsson, Mr. Johan Birger", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "35273", - "Parch": 0, - "Cabin": "D36", - "PassengerId": 394, - "Age": 23, - "Fare": 113.275, - "Name": "Newell, Miss. Marjorie", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PP 9549", - "Parch": 2, - "Cabin": "G6", - "PassengerId": 395, - "Age": 24, - "Fare": 16.7, - "Name": "Sandstrom, Mrs. Hjalmar (Agnes Charlotta Bengtsson)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "350052", - "Parch": 0, - "Cabin": null, - "PassengerId": 396, - "Age": 22, - "Fare": 7.7958, - "Name": "Johansson, Mr. Erik", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350407", - "Parch": 0, - "Cabin": null, - "PassengerId": 397, - "Age": 31, - "Fare": 7.8542, - "Name": "Olsson, Miss. Elina", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "28403", - "Parch": 0, - "Cabin": null, - "PassengerId": 398, - "Age": 46, - "Fare": 26, - "Name": "McKane, Mr. Peter David", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "244278", - "Parch": 0, - "Cabin": null, - "PassengerId": 399, - "Age": 23, - "Fare": 10.5, - "Name": "Pain, Dr. Alfred", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "240929", - "Parch": 0, - "Cabin": null, - "PassengerId": 400, - "Age": 28, - "Fare": 12.65, - "Name": "Trout, Mrs. William H (Jessie L)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101289", - "Parch": 0, - "Cabin": null, - "PassengerId": 401, - "Age": 39, - "Fare": 7.925, - "Name": "Niskanen, Mr. Juha", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "341826", - "Parch": 0, - "Cabin": null, - "PassengerId": 402, - "Age": 26, - "Fare": 8.05, - "Name": "Adams, Mr. John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "4137", - "Parch": 0, - "Cabin": null, - "PassengerId": 403, - "Age": 21, - "Fare": 9.825, - "Name": "Jussila, Miss. Mari Aina", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "STON/O2. 3101279", - "Parch": 0, - "Cabin": null, - "PassengerId": 404, - "Age": 28, - "Fare": 15.85, - "Name": "Hakkarainen, Mr. Pekka Pietari", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315096", - "Parch": 0, - "Cabin": null, - "PassengerId": 405, - "Age": 20, - "Fare": 8.6625, - "Name": "Oreskovic, Miss. Marija", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "28664", - "Parch": 0, - "Cabin": null, - "PassengerId": 406, - "Age": 34, - "Fare": 21, - "Name": "Gale, Mr. Shadrach", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347064", - "Parch": 0, - "Cabin": null, - "PassengerId": 407, - "Age": 51, - "Fare": 7.75, - "Name": "Widegren, Mr. Carl/Charles Peter", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "29106", - "Parch": 1, - "Cabin": null, - "PassengerId": 408, - "Age": 3, - "Fare": 18.75, - "Name": "Richards, Master. William Rowe", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "312992", - "Parch": 0, - "Cabin": null, - "PassengerId": 409, - "Age": 21, - "Fare": 7.775, - "Name": "Birkeland, Mr. Hans Martin Monsen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "4133", - "Parch": 1, - "Cabin": null, - "PassengerId": 410, - "Age": null, - "Fare": 25.4667, - "Name": "Lefebre, Miss. Ida", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349222", - "Parch": 0, - "Cabin": null, - "PassengerId": 411, - "Age": null, - "Fare": 7.8958, - "Name": "Sdycoff, Mr. Todor", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "394140", - "Parch": 0, - "Cabin": null, - "PassengerId": 412, - "Age": null, - "Fare": 6.8583, - "Name": "Hart, Mr. Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "19928", - "Parch": 0, - "Cabin": "C78", - "PassengerId": 413, - "Age": 33, - "Fare": 90, - "Name": "Minahan, Miss. Daisy E", - "Survived": true, - "Pclass": 1, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "239853", - "Parch": 0, - "Cabin": null, - "PassengerId": 414, - "Age": null, - "Fare": 0, - "Name": "Cunningham, Mr. Alfred Fleming", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101269", - "Parch": 0, - "Cabin": null, - "PassengerId": 415, - "Age": 44, - "Fare": 7.925, - "Name": "Sundman, Mr. Johan Julian", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "343095", - "Parch": 0, - "Cabin": null, - "PassengerId": 416, - "Age": null, - "Fare": 8.05, - "Name": "Meek, Mrs. Thomas (Annie Louise Rowley)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "28220", - "Parch": 1, - "Cabin": null, - "PassengerId": 417, - "Age": 34, - "Fare": 32.5, - "Name": "Drew, Mrs. James Vivian (Lulu Thorne Christian)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "250652", - "Parch": 2, - "Cabin": null, - "PassengerId": 418, - "Age": 18, - "Fare": 13, - "Name": "Silven, Miss. Lyyli Karoliina", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "28228", - "Parch": 0, - "Cabin": null, - "PassengerId": 419, - "Age": 30, - "Fare": 13, - "Name": "Matthews, Mr. William John", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345773", - "Parch": 2, - "Cabin": null, - "PassengerId": 420, - "Age": 10, - "Fare": 24.15, - "Name": "Van Impe, Miss. Catharina", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349254", - "Parch": 0, - "Cabin": null, - "PassengerId": 421, - "Age": null, - "Fare": 7.8958, - "Name": "Gheorgheff, Mr. Stanio", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5. 13032", - "Parch": 0, - "Cabin": null, - "PassengerId": 422, - "Age": 21, - "Fare": 7.7333, - "Name": "Charters, Mr. David", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315082", - "Parch": 0, - "Cabin": null, - "PassengerId": 423, - "Age": 29, - "Fare": 7.875, - "Name": "Zimmerman, Mr. Leo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347080", - "Parch": 1, - "Cabin": null, - "PassengerId": 424, - "Age": 28, - "Fare": 14.4, - "Name": "Danbom, Mrs. Ernst Gilbert (Anna Sigrid Maria Brogren)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "370129", - "Parch": 1, - "Cabin": null, - "PassengerId": 425, - "Age": 18, - "Fare": 20.2125, - "Name": "Rosblom, Mr. Viktor Richard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/4. 34244", - "Parch": 0, - "Cabin": null, - "PassengerId": 426, - "Age": null, - "Fare": 7.25, - "Name": "Wiseman, Mr. Phillippe", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2003", - "Parch": 0, - "Cabin": null, - "PassengerId": 427, - "Age": 28, - "Fare": 26, - "Name": "Clarke, Mrs. Charles V (Ada Maria Winfield)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "250655", - "Parch": 0, - "Cabin": null, - "PassengerId": 428, - "Age": 19, - "Fare": 26, - "Name": "Phillips, Miss. Kate Florence (\"Mrs Kate Louise Phillips Marshall\")", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "364851", - "Parch": 0, - "Cabin": null, - "PassengerId": 429, - "Age": null, - "Fare": 7.75, - "Name": "Flynn, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 392078", - "Parch": 0, - "Cabin": "E10", - "PassengerId": 430, - "Age": 32, - "Fare": 8.05, - "Name": "Pickard, Mr. Berk (Berk Trembisky)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "110564", - "Parch": 0, - "Cabin": "C52", - "PassengerId": 431, - "Age": 28, - "Fare": 26.55, - "Name": "Bjornstrom-Steffansson, Mr. Mauritz Hakan", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "376564", - "Parch": 0, - "Cabin": null, - "PassengerId": 432, - "Age": null, - "Fare": 16.1, - "Name": "Thorneycroft, Mrs. Percival (Florence Kate White)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "SC/AH 3085", - "Parch": 0, - "Cabin": null, - "PassengerId": 433, - "Age": 42, - "Fare": 26, - "Name": "Louch, Mrs. Charles Alexander (Alice Adelaide Slow)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101274", - "Parch": 0, - "Cabin": null, - "PassengerId": 434, - "Age": 17, - "Fare": 7.125, - "Name": "Kallio, Mr. Nikolai Erland", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "13507", - "Parch": 0, - "Cabin": "E44", - "PassengerId": 435, - "Age": 50, - "Fare": 55.9, - "Name": "Silvey, Mr. William Baird", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113760", - "Parch": 2, - "Cabin": "B96 B98", - "PassengerId": 436, - "Age": 14, - "Fare": 120, - "Name": "Carter, Miss. Lucile Polk", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "W./C. 6608", - "Parch": 2, - "Cabin": null, - "PassengerId": 437, - "Age": 21, - "Fare": 34.375, - "Name": "Ford, Miss. Doolina Margaret \"Daisy\"", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "29106", - "Parch": 3, - "Cabin": null, - "PassengerId": 438, - "Age": 24, - "Fare": 18.75, - "Name": "Richards, Mrs. Sidney (Emily Hocking)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "19950", - "Parch": 4, - "Cabin": "C23 C25 C27", - "PassengerId": 439, - "Age": 64, - "Fare": 263, - "Name": "Fortune, Mr. Mark", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 18723", - "Parch": 0, - "Cabin": null, - "PassengerId": 440, - "Age": 31, - "Fare": 10.5, - "Name": "Kvillner, Mr. Johan Henrik Johannesson", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "F.C.C. 13529", - "Parch": 1, - "Cabin": null, - "PassengerId": 441, - "Age": 45, - "Fare": 26.25, - "Name": "Hart, Mrs. Benjamin (Esther Ada Bloomfield)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "345769", - "Parch": 0, - "Cabin": null, - "PassengerId": 442, - "Age": 20, - "Fare": 9.5, - "Name": "Hampe, Mr. Leon", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347076", - "Parch": 0, - "Cabin": null, - "PassengerId": 443, - "Age": 25, - "Fare": 7.775, - "Name": "Petterson, Mr. Johan Emil", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "230434", - "Parch": 0, - "Cabin": null, - "PassengerId": 444, - "Age": 28, - "Fare": 13, - "Name": "Reynaldo, Ms. Encarnacion", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "65306", - "Parch": 0, - "Cabin": null, - "PassengerId": 445, - "Age": null, - "Fare": 8.1125, - "Name": "Johannesen-Bratthammer, Mr. Bernt", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "33638", - "Parch": 2, - "Cabin": "A34", - "PassengerId": 446, - "Age": 4, - "Fare": 81.8583, - "Name": "Dodge, Master. Washington", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250644", - "Parch": 1, - "Cabin": null, - "PassengerId": 447, - "Age": 13, - "Fare": 19.5, - "Name": "Mellinger, Miss. Madeleine Violet", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113794", - "Parch": 0, - "Cabin": null, - "PassengerId": 448, - "Age": 34, - "Fare": 26.55, - "Name": "Seward, Mr. Frederic Kimber", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "2666", - "Parch": 1, - "Cabin": null, - "PassengerId": 449, - "Age": 5, - "Fare": 19.2583, - "Name": "Baclini, Miss. Marie Catherine", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113786", - "Parch": 0, - "Cabin": "C104", - "PassengerId": 450, - "Age": 52, - "Fare": 30.5, - "Name": "Peuchen, Major. Arthur Godfrey", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 34651", - "Parch": 2, - "Cabin": null, - "PassengerId": 451, - "Age": 36, - "Fare": 27.75, - "Name": "West, Mr. Edwy Arthur", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "65303", - "Parch": 0, - "Cabin": null, - "PassengerId": 452, - "Age": null, - "Fare": 19.9667, - "Name": "Hagland, Mr. Ingvald Olai Olsen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113051", - "Parch": 0, - "Cabin": "C111", - "PassengerId": 453, - "Age": 30, - "Fare": 27.75, - "Name": "Foreman, Mr. Benjamin Laventall", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "17453", - "Parch": 0, - "Cabin": "C92", - "PassengerId": 454, - "Age": 49, - "Fare": 89.1042, - "Name": "Goldenberg, Mr. Samuel L", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5 2817", - "Parch": 0, - "Cabin": null, - "PassengerId": 455, - "Age": null, - "Fare": 8.05, - "Name": "Peduzzi, Mr. Joseph", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349240", - "Parch": 0, - "Cabin": null, - "PassengerId": 456, - "Age": 29, - "Fare": 7.8958, - "Name": "Jalsevac, Mr. Ivan", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "13509", - "Parch": 0, - "Cabin": "E38", - "PassengerId": 457, - "Age": 65, - "Fare": 26.55, - "Name": "Millet, Mr. Francis Davis", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "17464", - "Parch": 0, - "Cabin": "D21", - "PassengerId": 458, - "Age": null, - "Fare": 51.8625, - "Name": "Kenyon, Mrs. Frederick R (Marion)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "F.C.C. 13531", - "Parch": 0, - "Cabin": null, - "PassengerId": 459, - "Age": 50, - "Fare": 10.5, - "Name": "Toomey, Miss. Ellen", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "371060", - "Parch": 0, - "Cabin": null, - "PassengerId": 460, - "Age": null, - "Fare": 7.75, - "Name": "O'Connor, Mr. Maurice", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "19952", - "Parch": 0, - "Cabin": "E12", - "PassengerId": 461, - "Age": 48, - "Fare": 26.55, - "Name": "Anderson, Mr. Harry", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364506", - "Parch": 0, - "Cabin": null, - "PassengerId": 462, - "Age": 34, - "Fare": 8.05, - "Name": "Morley, Mr. William", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "111320", - "Parch": 0, - "Cabin": "E63", - "PassengerId": 463, - "Age": 47, - "Fare": 38.5, - "Name": "Gee, Mr. Arthur H", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "234360", - "Parch": 0, - "Cabin": null, - "PassengerId": 464, - "Age": 48, - "Fare": 13, - "Name": "Milling, Mr. Jacob Christian", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/S 2816", - "Parch": 0, - "Cabin": null, - "PassengerId": 465, - "Age": null, - "Fare": 8.05, - "Name": "Maisner, Mr. Simon", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 3101306", - "Parch": 0, - "Cabin": null, - "PassengerId": 466, - "Age": 38, - "Fare": 7.05, - "Name": "Goncalves, Mr. Manuel Estanslas", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "239853", - "Parch": 0, - "Cabin": null, - "PassengerId": 467, - "Age": null, - "Fare": 0, - "Name": "Campbell, Mr. William", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113792", - "Parch": 0, - "Cabin": null, - "PassengerId": 468, - "Age": 56, - "Fare": 26.55, - "Name": "Smart, Mr. John Montgomery", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "36209", - "Parch": 0, - "Cabin": null, - "PassengerId": 469, - "Age": null, - "Fare": 7.725, - "Name": "Scanlan, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "2666", - "Parch": 1, - "Cabin": null, - "PassengerId": 470, - "Age": 0.75, - "Fare": 19.2583, - "Name": "Baclini, Miss. Helene Barbara", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "323592", - "Parch": 0, - "Cabin": null, - "PassengerId": 471, - "Age": null, - "Fare": 7.25, - "Name": "Keefe, Mr. Arthur", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315089", - "Parch": 0, - "Cabin": null, - "PassengerId": 472, - "Age": 38, - "Fare": 8.6625, - "Name": "Cacic, Mr. Luka", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 34651", - "Parch": 2, - "Cabin": null, - "PassengerId": 473, - "Age": 33, - "Fare": 27.75, - "Name": "West, Mrs. Edwy Arthur (Ada Mary Worth)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SC/AH Basle 541", - "Parch": 0, - "Cabin": "D", - "PassengerId": 474, - "Age": 23, - "Fare": 13.7917, - "Name": "Jerwan, Mrs. Amin S (Marie Marthe Thuillard)", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "7553", - "Parch": 0, - "Cabin": null, - "PassengerId": 475, - "Age": 22, - "Fare": 9.8375, - "Name": "Strandberg, Miss. Ida Sofia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "110465", - "Parch": 0, - "Cabin": "A14", - "PassengerId": 476, - "Age": null, - "Fare": 52, - "Name": "Clifford, Mr. George Quincy", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "31027", - "Parch": 0, - "Cabin": null, - "PassengerId": 477, - "Age": 34, - "Fare": 21, - "Name": "Renouf, Mr. Peter Henry", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "3460", - "Parch": 0, - "Cabin": null, - "PassengerId": 478, - "Age": 29, - "Fare": 7.0458, - "Name": "Braund, Mr. Lewis Richard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350060", - "Parch": 0, - "Cabin": null, - "PassengerId": 479, - "Age": 22, - "Fare": 7.5208, - "Name": "Karlsson, Mr. Nils August", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "3101298", - "Parch": 1, - "Cabin": null, - "PassengerId": 480, - "Age": 2, - "Fare": 12.2875, - "Name": "Hirvonen, Miss. Hildur E", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 5, - "Ticket": "CA 2144", - "Parch": 2, - "Cabin": null, - "PassengerId": 481, - "Age": 9, - "Fare": 46.9, - "Name": "Goodwin, Master. Harold Victor", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "239854", - "Parch": 0, - "Cabin": null, - "PassengerId": 482, - "Age": null, - "Fare": 0, - "Name": "Frost, Mr. Anthony Wood \"Archie\"", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5 3594", - "Parch": 0, - "Cabin": null, - "PassengerId": 483, - "Age": 50, - "Fare": 8.05, - "Name": "Rouse, Mr. Richard Henry", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "4134", - "Parch": 0, - "Cabin": null, - "PassengerId": 484, - "Age": 63, - "Fare": 9.5875, - "Name": "Turkula, Mrs. (Hedwig)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "11967", - "Parch": 0, - "Cabin": "B49", - "PassengerId": 485, - "Age": 25, - "Fare": 91.0792, - "Name": "Bishop, Mr. Dickinson H", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "4133", - "Parch": 1, - "Cabin": null, - "PassengerId": 486, - "Age": null, - "Fare": 25.4667, - "Name": "Lefebre, Miss. Jeannie", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "19943", - "Parch": 0, - "Cabin": "C93", - "PassengerId": 487, - "Age": 35, - "Fare": 90, - "Name": "Hoyt, Mrs. Frederick Maxfield (Jane Anne Forby)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "11771", - "Parch": 0, - "Cabin": "B37", - "PassengerId": 488, - "Age": 58, - "Fare": 29.7, - "Name": "Kent, Mr. Edward Austin", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A.5. 18509", - "Parch": 0, - "Cabin": null, - "PassengerId": 489, - "Age": 30, - "Fare": 8.05, - "Name": "Somerton, Mr. Francis William", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 37671", - "Parch": 1, - "Cabin": null, - "PassengerId": 490, - "Age": 9, - "Fare": 15.9, - "Name": "Coutts, Master. Eden Leslie \"Neville\"", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "65304", - "Parch": 0, - "Cabin": null, - "PassengerId": 491, - "Age": null, - "Fare": 19.9667, - "Name": "Hagland, Mr. Konrad Mathias Reiersen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 3101317", - "Parch": 0, - "Cabin": null, - "PassengerId": 492, - "Age": 21, - "Fare": 7.25, - "Name": "Windelov, Mr. Einar", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113787", - "Parch": 0, - "Cabin": "C30", - "PassengerId": 493, - "Age": 55, - "Fare": 30.5, - "Name": "Molson, Mr. Harry Markland", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17609", - "Parch": 0, - "Cabin": null, - "PassengerId": 494, - "Age": 71, - "Fare": 49.5042, - "Name": "Artagaveytia, Mr. Ramon", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/4 45380", - "Parch": 0, - "Cabin": null, - "PassengerId": 495, - "Age": 21, - "Fare": 8.05, - "Name": "Stanley, Mr. Edward Roland", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2627", - "Parch": 0, - "Cabin": null, - "PassengerId": 496, - "Age": null, - "Fare": 14.4583, - "Name": "Yousseff, Mr. Gerious", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "36947", - "Parch": 0, - "Cabin": "D20", - "PassengerId": 497, - "Age": 54, - "Fare": 78.2667, - "Name": "Eustis, Miss. Elizabeth Mussey", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C.A. 6212", - "Parch": 0, - "Cabin": null, - "PassengerId": 498, - "Age": null, - "Fare": 15.1, - "Name": "Shellard, Mr. Frederick William", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113781", - "Parch": 2, - "Cabin": "C22 C26", - "PassengerId": 499, - "Age": 25, - "Fare": 151.55, - "Name": "Allison, Mrs. Hudson J C (Bessie Waldo Daniels)", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "350035", - "Parch": 0, - "Cabin": null, - "PassengerId": 500, - "Age": 24, - "Fare": 7.7958, - "Name": "Svensson, Mr. Olof", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315086", - "Parch": 0, - "Cabin": null, - "PassengerId": 501, - "Age": 17, - "Fare": 8.6625, - "Name": "Calic, Mr. Petar", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364846", - "Parch": 0, - "Cabin": null, - "PassengerId": 502, - "Age": 21, - "Fare": 7.75, - "Name": "Canavan, Miss. Mary", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "330909", - "Parch": 0, - "Cabin": null, - "PassengerId": 503, - "Age": null, - "Fare": 7.6292, - "Name": "O'Sullivan, Miss. Bridget Mary", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "4135", - "Parch": 0, - "Cabin": null, - "PassengerId": 504, - "Age": 37, - "Fare": 9.5875, - "Name": "Laitinen, Miss. Kristina Sofia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "110152", - "Parch": 0, - "Cabin": "B79", - "PassengerId": 505, - "Age": 16, - "Fare": 86.5, - "Name": "Maioni, Miss. Roberta", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "PC 17758", - "Parch": 0, - "Cabin": "C65", - "PassengerId": 506, - "Age": 18, - "Fare": 108.9, - "Name": "Penasco y Castellana, Mr. Victor de Satode", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "26360", - "Parch": 2, - "Cabin": null, - "PassengerId": 507, - "Age": 33, - "Fare": 26, - "Name": "Quick, Mrs. Frederick Charles (Jane Richards)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "111427", - "Parch": 0, - "Cabin": null, - "PassengerId": 508, - "Age": null, - "Fare": 26.55, - "Name": "Bradley, Mr. George (\"George Arthur Brayton\")", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C 4001", - "Parch": 0, - "Cabin": null, - "PassengerId": 509, - "Age": 28, - "Fare": 22.525, - "Name": "Olsen, Mr. Henry Margido", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 510, - "Age": 26, - "Fare": 56.4958, - "Name": "Lang, Mr. Fang", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "382651", - "Parch": 0, - "Cabin": null, - "PassengerId": 511, - "Age": 29, - "Fare": 7.75, - "Name": "Daly, Mr. Eugene Patrick", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 3101316", - "Parch": 0, - "Cabin": null, - "PassengerId": 512, - "Age": null, - "Fare": 8.05, - "Name": "Webber, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17473", - "Parch": 0, - "Cabin": "E25", - "PassengerId": 513, - "Age": 36, - "Fare": 26.2875, - "Name": "McGough, Mr. James Robert", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17603", - "Parch": 0, - "Cabin": null, - "PassengerId": 514, - "Age": 54, - "Fare": 59.4, - "Name": "Rothschild, Mrs. Martin (Elizabeth L. Barrett)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349209", - "Parch": 0, - "Cabin": null, - "PassengerId": 515, - "Age": 24, - "Fare": 7.4958, - "Name": "Coleff, Mr. Satio", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "36967", - "Parch": 0, - "Cabin": "D46", - "PassengerId": 516, - "Age": 47, - "Fare": 34.0208, - "Name": "Walker, Mr. William Anderson", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 34260", - "Parch": 0, - "Cabin": "F33", - "PassengerId": 517, - "Age": 34, - "Fare": 10.5, - "Name": "Lemore, Mrs. (Amelia Milley)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "371110", - "Parch": 0, - "Cabin": null, - "PassengerId": 518, - "Age": null, - "Fare": 24.15, - "Name": "Ryan, Mr. Patrick", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "226875", - "Parch": 0, - "Cabin": null, - "PassengerId": 519, - "Age": 36, - "Fare": 26, - "Name": "Angle, Mrs. William A (Florence \"Mary\" Agnes Hughes)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349242", - "Parch": 0, - "Cabin": null, - "PassengerId": 520, - "Age": 32, - "Fare": 7.8958, - "Name": "Pavlovic, Mr. Stefo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "12749", - "Parch": 0, - "Cabin": "B73", - "PassengerId": 521, - "Age": 30, - "Fare": 93.5, - "Name": "Perreault, Miss. Anne", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349252", - "Parch": 0, - "Cabin": null, - "PassengerId": 522, - "Age": 22, - "Fare": 7.8958, - "Name": "Vovk, Mr. Janko", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2624", - "Parch": 0, - "Cabin": null, - "PassengerId": 523, - "Age": null, - "Fare": 7.225, - "Name": "Lahoud, Mr. Sarkis", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "111361", - "Parch": 1, - "Cabin": "B18", - "PassengerId": 524, - "Age": 44, - "Fare": 57.9792, - "Name": "Hippach, Mrs. Louis Albert (Ida Sophia Fischer)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2700", - "Parch": 0, - "Cabin": null, - "PassengerId": 525, - "Age": null, - "Fare": 7.2292, - "Name": "Kassem, Mr. Fared", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "367232", - "Parch": 0, - "Cabin": null, - "PassengerId": 526, - "Age": 40.5, - "Fare": 7.75, - "Name": "Farrell, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "W./C. 14258", - "Parch": 0, - "Cabin": null, - "PassengerId": 527, - "Age": 50, - "Fare": 10.5, - "Name": "Ridsdale, Miss. Lucy", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17483", - "Parch": 0, - "Cabin": "C95", - "PassengerId": 528, - "Age": null, - "Fare": 221.7792, - "Name": "Farthing, Mr. John", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "3101296", - "Parch": 0, - "Cabin": null, - "PassengerId": 529, - "Age": 39, - "Fare": 7.925, - "Name": "Salonen, Mr. Johan Werner", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "29104", - "Parch": 1, - "Cabin": null, - "PassengerId": 530, - "Age": 23, - "Fare": 11.5, - "Name": "Hocking, Mr. Richard George", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "26360", - "Parch": 1, - "Cabin": null, - "PassengerId": 531, - "Age": 2, - "Fare": 26, - "Name": "Quick, Miss. Phyllis May", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2641", - "Parch": 0, - "Cabin": null, - "PassengerId": 532, - "Age": null, - "Fare": 7.2292, - "Name": "Toufik, Mr. Nakli", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2690", - "Parch": 1, - "Cabin": null, - "PassengerId": 533, - "Age": 17, - "Fare": 7.2292, - "Name": "Elias, Mr. Joseph Jr", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2668", - "Parch": 2, - "Cabin": null, - "PassengerId": 534, - "Age": null, - "Fare": 22.3583, - "Name": "Peter, Mrs. Catherine (Catherine Rizk)", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "315084", - "Parch": 0, - "Cabin": null, - "PassengerId": 535, - "Age": 30, - "Fare": 8.6625, - "Name": "Cacic, Miss. Marija", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "F.C.C. 13529", - "Parch": 2, - "Cabin": null, - "PassengerId": 536, - "Age": 7, - "Fare": 26.25, - "Name": "Hart, Miss. Eva Miriam", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113050", - "Parch": 0, - "Cabin": "B38", - "PassengerId": 537, - "Age": 45, - "Fare": 26.55, - "Name": "Butt, Major. Archibald Willingham", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17761", - "Parch": 0, - "Cabin": null, - "PassengerId": 538, - "Age": 30, - "Fare": 106.425, - "Name": "LeRoy, Miss. Bertha", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "364498", - "Parch": 0, - "Cabin": null, - "PassengerId": 539, - "Age": null, - "Fare": 14.5, - "Name": "Risien, Mr. Samuel Beard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "13568", - "Parch": 2, - "Cabin": "B39", - "PassengerId": 540, - "Age": 22, - "Fare": 49.5, - "Name": "Frolicher, Miss. Hedwig Margaritha", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "WE/P 5735", - "Parch": 2, - "Cabin": "B22", - "PassengerId": 541, - "Age": 36, - "Fare": 71, - "Name": "Crosby, Miss. Harriet R", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "347082", - "Parch": 2, - "Cabin": null, - "PassengerId": 542, - "Age": 9, - "Fare": 31.275, - "Name": "Andersson, Miss. Ingeborg Constanzia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "347082", - "Parch": 2, - "Cabin": null, - "PassengerId": 543, - "Age": 11, - "Fare": 31.275, - "Name": "Andersson, Miss. Sigrid Elisabeth", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "2908", - "Parch": 0, - "Cabin": null, - "PassengerId": 544, - "Age": 32, - "Fare": 26, - "Name": "Beane, Mr. Edward", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17761", - "Parch": 0, - "Cabin": "C86", - "PassengerId": 545, - "Age": 50, - "Fare": 106.425, - "Name": "Douglas, Mr. Walter Donald", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "693", - "Parch": 0, - "Cabin": null, - "PassengerId": 546, - "Age": 64, - "Fare": 26, - "Name": "Nicholson, Mr. Arthur Ernest", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2908", - "Parch": 0, - "Cabin": null, - "PassengerId": 547, - "Age": 19, - "Fare": 26, - "Name": "Beane, Mrs. Edward (Ethel Clarke)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SC/PARIS 2146", - "Parch": 0, - "Cabin": null, - "PassengerId": 548, - "Age": null, - "Fare": 13.8625, - "Name": "Padro y Manent, Mr. Julian", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "363291", - "Parch": 1, - "Cabin": null, - "PassengerId": 549, - "Age": 33, - "Fare": 20.525, - "Name": "Goldsmith, Mr. Frank John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 33112", - "Parch": 1, - "Cabin": null, - "PassengerId": 550, - "Age": 8, - "Fare": 36.75, - "Name": "Davies, Master. John Morgan Jr", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "17421", - "Parch": 2, - "Cabin": "C70", - "PassengerId": 551, - "Age": 17, - "Fare": 110.8833, - "Name": "Thayer, Mr. John Borland Jr", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "244358", - "Parch": 0, - "Cabin": null, - "PassengerId": 552, - "Age": 27, - "Fare": 26, - "Name": "Sharp, Mr. Percival James R", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330979", - "Parch": 0, - "Cabin": null, - "PassengerId": 553, - "Age": null, - "Fare": 7.8292, - "Name": "O'Brien, Mr. Timothy", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2620", - "Parch": 0, - "Cabin": null, - "PassengerId": 554, - "Age": 22, - "Fare": 7.225, - "Name": "Leeni, Mr. Fahim (\"Philip Zenni\")", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347085", - "Parch": 0, - "Cabin": null, - "PassengerId": 555, - "Age": 22, - "Fare": 7.775, - "Name": "Ohman, Miss. Velin", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113807", - "Parch": 0, - "Cabin": null, - "PassengerId": 556, - "Age": 62, - "Fare": 26.55, - "Name": "Wright, Mr. George", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "11755", - "Parch": 0, - "Cabin": "A16", - "PassengerId": 557, - "Age": 48, - "Fare": 39.6, - "Name": "Duff Gordon, Lady. (Lucille Christiana Sutherland) (\"Mrs Morgan\")", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17757", - "Parch": 0, - "Cabin": null, - "PassengerId": 558, - "Age": null, - "Fare": 227.525, - "Name": "Robbins, Mr. Victor", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "110413", - "Parch": 1, - "Cabin": "E67", - "PassengerId": 559, - "Age": 39, - "Fare": 79.65, - "Name": "Taussig, Mrs. Emil (Tillie Mandelbaum)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "345572", - "Parch": 0, - "Cabin": null, - "PassengerId": 560, - "Age": 36, - "Fare": 17.4, - "Name": "de Messemaeker, Mrs. Guillaume Joseph (Emma)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "372622", - "Parch": 0, - "Cabin": null, - "PassengerId": 561, - "Age": null, - "Fare": 7.75, - "Name": "Morrow, Mr. Thomas Rowan", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349251", - "Parch": 0, - "Cabin": null, - "PassengerId": 562, - "Age": 40, - "Fare": 7.8958, - "Name": "Sivic, Mr. Husein", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "218629", - "Parch": 0, - "Cabin": null, - "PassengerId": 563, - "Age": 28, - "Fare": 13.5, - "Name": "Norman, Mr. Robert Douglas", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 392082", - "Parch": 0, - "Cabin": null, - "PassengerId": 564, - "Age": null, - "Fare": 8.05, - "Name": "Simmons, Mr. John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 392087", - "Parch": 0, - "Cabin": null, - "PassengerId": 565, - "Age": null, - "Fare": 8.05, - "Name": "Meanwell, Miss. (Marion Ogden)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "A/4 48871", - "Parch": 0, - "Cabin": null, - "PassengerId": 566, - "Age": 24, - "Fare": 24.15, - "Name": "Davies, Mr. Alfred J", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349205", - "Parch": 0, - "Cabin": null, - "PassengerId": 567, - "Age": 19, - "Fare": 7.8958, - "Name": "Stoytcheff, Mr. Ilia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349909", - "Parch": 4, - "Cabin": null, - "PassengerId": 568, - "Age": 29, - "Fare": 21.075, - "Name": "Palsson, Mrs. Nils (Alma Cornelia Berglund)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2686", - "Parch": 0, - "Cabin": null, - "PassengerId": 569, - "Age": null, - "Fare": 7.2292, - "Name": "Doharr, Mr. Tannous", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350417", - "Parch": 0, - "Cabin": null, - "PassengerId": 570, - "Age": 32, - "Fare": 7.8542, - "Name": "Jonsson, Mr. Carl", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.W./PP 752", - "Parch": 0, - "Cabin": null, - "PassengerId": 571, - "Age": 62, - "Fare": 10.5, - "Name": "Harris, Mr. George", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "11769", - "Parch": 0, - "Cabin": "C101", - "PassengerId": 572, - "Age": 53, - "Fare": 51.4792, - "Name": "Appleton, Mrs. Edward Dale (Charlotte Lamson)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17474", - "Parch": 0, - "Cabin": "E25", - "PassengerId": 573, - "Age": 36, - "Fare": 26.3875, - "Name": "Flynn, Mr. John Irwin (\"Irving\")", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "14312", - "Parch": 0, - "Cabin": null, - "PassengerId": 574, - "Age": null, - "Fare": 7.75, - "Name": "Kelly, Miss. Mary", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A/4. 20589", - "Parch": 0, - "Cabin": null, - "PassengerId": 575, - "Age": 16, - "Fare": 8.05, - "Name": "Rush, Mr. Alfred George John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "358585", - "Parch": 0, - "Cabin": null, - "PassengerId": 576, - "Age": 19, - "Fare": 14.5, - "Name": "Patchett, Mr. George", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "243880", - "Parch": 0, - "Cabin": null, - "PassengerId": 577, - "Age": 34, - "Fare": 13, - "Name": "Garside, Miss. Ethel", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "13507", - "Parch": 0, - "Cabin": "E44", - "PassengerId": 578, - "Age": 39, - "Fare": 55.9, - "Name": "Silvey, Mrs. William Baird (Alice Munger)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "2689", - "Parch": 0, - "Cabin": null, - "PassengerId": 579, - "Age": null, - "Fare": 14.4583, - "Name": "Caram, Mrs. Joseph (Maria Elias)", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101286", - "Parch": 0, - "Cabin": null, - "PassengerId": 580, - "Age": 32, - "Fare": 7.925, - "Name": "Jussila, Mr. Eiriik", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "237789", - "Parch": 1, - "Cabin": null, - "PassengerId": 581, - "Age": 25, - "Fare": 30, - "Name": "Christy, Miss. Julie Rachel", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "17421", - "Parch": 1, - "Cabin": "C68", - "PassengerId": 582, - "Age": 39, - "Fare": 110.8833, - "Name": "Thayer, Mrs. John Borland (Marian Longstreth Morris)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "28403", - "Parch": 0, - "Cabin": null, - "PassengerId": 583, - "Age": 54, - "Fare": 26, - "Name": "Downton, Mr. William James", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "13049", - "Parch": 0, - "Cabin": "A10", - "PassengerId": 584, - "Age": 36, - "Fare": 40.125, - "Name": "Ross, Mr. John Hugo", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "3411", - "Parch": 0, - "Cabin": null, - "PassengerId": 585, - "Age": null, - "Fare": 8.7125, - "Name": "Paulner, Mr. Uscher", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "110413", - "Parch": 2, - "Cabin": "E68", - "PassengerId": 586, - "Age": 18, - "Fare": 79.65, - "Name": "Taussig, Miss. Ruth", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "237565", - "Parch": 0, - "Cabin": null, - "PassengerId": 587, - "Age": 47, - "Fare": 15, - "Name": "Jarvis, Mr. John Denzil", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "13567", - "Parch": 1, - "Cabin": "B41", - "PassengerId": 588, - "Age": 60, - "Fare": 79.2, - "Name": "Frolicher-Stehli, Mr. Maxmillian", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "14973", - "Parch": 0, - "Cabin": null, - "PassengerId": 589, - "Age": 22, - "Fare": 8.05, - "Name": "Gilinski, Mr. Eliezer", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A./5. 3235", - "Parch": 0, - "Cabin": null, - "PassengerId": 590, - "Age": null, - "Fare": 8.05, - "Name": "Murdlin, Mr. Joseph", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101273", - "Parch": 0, - "Cabin": null, - "PassengerId": 591, - "Age": 35, - "Fare": 7.125, - "Name": "Rintamaki, Mr. Matti", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "36947", - "Parch": 0, - "Cabin": "D20", - "PassengerId": 592, - "Age": 52, - "Fare": 78.2667, - "Name": "Stephenson, Mrs. Walter Bertram (Martha Eustis)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "A/5 3902", - "Parch": 0, - "Cabin": null, - "PassengerId": 593, - "Age": 47, - "Fare": 7.25, - "Name": "Elsbury, Mr. William James", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364848", - "Parch": 2, - "Cabin": null, - "PassengerId": 594, - "Age": null, - "Fare": 7.75, - "Name": "Bourke, Miss. Mary", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "SC/AH 29037", - "Parch": 0, - "Cabin": null, - "PassengerId": 595, - "Age": 37, - "Fare": 26, - "Name": "Chapman, Mr. John Henry", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "345773", - "Parch": 1, - "Cabin": null, - "PassengerId": 596, - "Age": 36, - "Fare": 24.15, - "Name": "Van Impe, Mr. Jean Baptiste", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248727", - "Parch": 0, - "Cabin": null, - "PassengerId": 597, - "Age": null, - "Fare": 33, - "Name": "Leitch, Miss. Jessie Wills", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "LINE", - "Parch": 0, - "Cabin": null, - "PassengerId": 598, - "Age": 49, - "Fare": 0, - "Name": "Johnson, Mr. Alfred", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2664", - "Parch": 0, - "Cabin": null, - "PassengerId": 599, - "Age": null, - "Fare": 7.225, - "Name": "Boulos, Mr. Hanna", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17485", - "Parch": 0, - "Cabin": "A20", - "PassengerId": 600, - "Age": 49, - "Fare": 56.9292, - "Name": "Duff Gordon, Sir. Cosmo Edmund (\"Mr Morgan\")", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "243847", - "Parch": 1, - "Cabin": null, - "PassengerId": 601, - "Age": 24, - "Fare": 27, - "Name": "Jacobsohn, Mrs. Sidney Samuel (Amy Frances Christy)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349214", - "Parch": 0, - "Cabin": null, - "PassengerId": 602, - "Age": null, - "Fare": 7.8958, - "Name": "Slabenoff, Mr. Petco", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113796", - "Parch": 0, - "Cabin": null, - "PassengerId": 603, - "Age": null, - "Fare": 42.4, - "Name": "Harrington, Mr. Charles H", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364511", - "Parch": 0, - "Cabin": null, - "PassengerId": 604, - "Age": 44, - "Fare": 8.05, - "Name": "Torber, Mr. Ernst William", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "111426", - "Parch": 0, - "Cabin": null, - "PassengerId": 605, - "Age": 35, - "Fare": 26.55, - "Name": "Homer, Mr. Harry (\"Mr E Haven\")", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "349910", - "Parch": 0, - "Cabin": null, - "PassengerId": 606, - "Age": 36, - "Fare": 15.55, - "Name": "Lindell, Mr. Edvard Bengtsson", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349246", - "Parch": 0, - "Cabin": null, - "PassengerId": 607, - "Age": 30, - "Fare": 7.8958, - "Name": "Karaic, Mr. Milan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113804", - "Parch": 0, - "Cabin": null, - "PassengerId": 608, - "Age": 27, - "Fare": 30.5, - "Name": "Daniel, Mr. Robert Williams", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "SC/Paris 2123", - "Parch": 2, - "Cabin": null, - "PassengerId": 609, - "Age": 22, - "Fare": 41.5792, - "Name": "Laroche, Mrs. Joseph (Juliette Marie Louise Lafargue)", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17582", - "Parch": 0, - "Cabin": "C125", - "PassengerId": 610, - "Age": 40, - "Fare": 153.4625, - "Name": "Shutes, Miss. Elizabeth W", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "347082", - "Parch": 5, - "Cabin": null, - "PassengerId": 611, - "Age": 39, - "Fare": 31.275, - "Name": "Andersson, Mrs. Anders Johan (Alfrida Konstantia Brogren)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 3101305", - "Parch": 0, - "Cabin": null, - "PassengerId": 612, - "Age": null, - "Fare": 7.05, - "Name": "Jardin, Mr. Jose Neto", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "367230", - "Parch": 0, - "Cabin": null, - "PassengerId": 613, - "Age": null, - "Fare": 15.5, - "Name": "Murphy, Miss. Margaret Jane", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "370377", - "Parch": 0, - "Cabin": null, - "PassengerId": 614, - "Age": null, - "Fare": 7.75, - "Name": "Horgan, Mr. John", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364512", - "Parch": 0, - "Cabin": null, - "PassengerId": 615, - "Age": 35, - "Fare": 8.05, - "Name": "Brocklebank, Mr. William Alfred", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "220845", - "Parch": 2, - "Cabin": null, - "PassengerId": 616, - "Age": 24, - "Fare": 65, - "Name": "Herman, Miss. Alice", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "347080", - "Parch": 1, - "Cabin": null, - "PassengerId": 617, - "Age": 34, - "Fare": 14.4, - "Name": "Danbom, Mr. Ernst Gilbert", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "A/5. 3336", - "Parch": 0, - "Cabin": null, - "PassengerId": 618, - "Age": 26, - "Fare": 16.1, - "Name": "Lobb, Mrs. William Arthur (Cordelia K Stanlick)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "230136", - "Parch": 1, - "Cabin": "F4", - "PassengerId": 619, - "Age": 4, - "Fare": 39, - "Name": "Becker, Miss. Marion Louise", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "31028", - "Parch": 0, - "Cabin": null, - "PassengerId": 620, - "Age": 26, - "Fare": 10.5, - "Name": "Gavey, Mr. Lawrence", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2659", - "Parch": 0, - "Cabin": null, - "PassengerId": 621, - "Age": 27, - "Fare": 14.4542, - "Name": "Yasbeck, Mr. Antoni", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "11753", - "Parch": 0, - "Cabin": "D19", - "PassengerId": 622, - "Age": 42, - "Fare": 52.5542, - "Name": "Kimball, Mr. Edwin Nelson Jr", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2653", - "Parch": 1, - "Cabin": null, - "PassengerId": 623, - "Age": 20, - "Fare": 15.7417, - "Name": "Nakid, Mr. Sahid", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350029", - "Parch": 0, - "Cabin": null, - "PassengerId": 624, - "Age": 21, - "Fare": 7.8542, - "Name": "Hansen, Mr. Henry Damsgaard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "54636", - "Parch": 0, - "Cabin": null, - "PassengerId": 625, - "Age": 21, - "Fare": 16.1, - "Name": "Bowen, Mr. David John \"Dai\"", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "36963", - "Parch": 0, - "Cabin": "D50", - "PassengerId": 626, - "Age": 61, - "Fare": 32.3208, - "Name": "Sutton, Mr. Frederick", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "219533", - "Parch": 0, - "Cabin": null, - "PassengerId": 627, - "Age": 57, - "Fare": 12.35, - "Name": "Kirkland, Rev. Charles Leonard", - "Survived": false, - "Pclass": 2, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "13502", - "Parch": 0, - "Cabin": "D9", - "PassengerId": 628, - "Age": 21, - "Fare": 77.9583, - "Name": "Longley, Miss. Gretchen Fiske", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349224", - "Parch": 0, - "Cabin": null, - "PassengerId": 629, - "Age": 26, - "Fare": 7.8958, - "Name": "Bostandyeff, Mr. Guentcho", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "334912", - "Parch": 0, - "Cabin": null, - "PassengerId": 630, - "Age": null, - "Fare": 7.7333, - "Name": "O'Connell, Mr. Patrick D", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "27042", - "Parch": 0, - "Cabin": "A23", - "PassengerId": 631, - "Age": 80, - "Fare": 30, - "Name": "Barkworth, Mr. Algernon Henry Wilson", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347743", - "Parch": 0, - "Cabin": null, - "PassengerId": 632, - "Age": 51, - "Fare": 7.0542, - "Name": "Lundahl, Mr. Johan Svensson", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "13214", - "Parch": 0, - "Cabin": "B50", - "PassengerId": 633, - "Age": 32, - "Fare": 30.5, - "Name": "Stahelin-Maeglin, Dr. Max", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "112052", - "Parch": 0, - "Cabin": null, - "PassengerId": 634, - "Age": null, - "Fare": 0, - "Name": "Parr, Mr. William Henry Marsh", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "347088", - "Parch": 2, - "Cabin": null, - "PassengerId": 635, - "Age": 9, - "Fare": 27.9, - "Name": "Skoog, Miss. Mabel", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "237668", - "Parch": 0, - "Cabin": null, - "PassengerId": 636, - "Age": 28, - "Fare": 13, - "Name": "Davis, Miss. Mary", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101292", - "Parch": 0, - "Cabin": null, - "PassengerId": 637, - "Age": 32, - "Fare": 7.925, - "Name": "Leinonen, Mr. Antti Gustaf", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 31921", - "Parch": 1, - "Cabin": null, - "PassengerId": 638, - "Age": 31, - "Fare": 26.25, - "Name": "Collyer, Mr. Harvey", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "3101295", - "Parch": 5, - "Cabin": null, - "PassengerId": 639, - "Age": 41, - "Fare": 39.6875, - "Name": "Panula, Mrs. Juha (Maria Emilia Ojala)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "376564", - "Parch": 0, - "Cabin": null, - "PassengerId": 640, - "Age": null, - "Fare": 16.1, - "Name": "Thorneycroft, Mr. Percival", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350050", - "Parch": 0, - "Cabin": null, - "PassengerId": 641, - "Age": 20, - "Fare": 7.8542, - "Name": "Jensen, Mr. Hans Peder", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17477", - "Parch": 0, - "Cabin": "B35", - "PassengerId": 642, - "Age": 24, - "Fare": 69.3, - "Name": "Sagesser, Mlle. Emma", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 3, - "Ticket": "347088", - "Parch": 2, - "Cabin": null, - "PassengerId": 643, - "Age": 2, - "Fare": 27.9, - "Name": "Skoog, Miss. Margit Elizabeth", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 644, - "Age": null, - "Fare": 56.4958, - "Name": "Foo, Mr. Choong", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "2666", - "Parch": 1, - "Cabin": null, - "PassengerId": 645, - "Age": 0.75, - "Fare": 19.2583, - "Name": "Baclini, Miss. Eugenie", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "PC 17572", - "Parch": 0, - "Cabin": "D33", - "PassengerId": 646, - "Age": 48, - "Fare": 76.7292, - "Name": "Harper, Mr. Henry Sleeper", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349231", - "Parch": 0, - "Cabin": null, - "PassengerId": 647, - "Age": 19, - "Fare": 7.8958, - "Name": "Cor, Mr. Liudevit", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "13213", - "Parch": 0, - "Cabin": "A26", - "PassengerId": 648, - "Age": 56, - "Fare": 35.5, - "Name": "Simonius-Blumer, Col. Oberst Alfons", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.O./P.P. 751", - "Parch": 0, - "Cabin": null, - "PassengerId": 649, - "Age": null, - "Fare": 7.55, - "Name": "Willey, Mr. Edward", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "CA. 2314", - "Parch": 0, - "Cabin": null, - "PassengerId": 650, - "Age": 23, - "Fare": 7.55, - "Name": "Stanley, Miss. Amy Zillah Elsie", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349221", - "Parch": 0, - "Cabin": null, - "PassengerId": 651, - "Age": null, - "Fare": 7.8958, - "Name": "Mitkoff, Mr. Mito", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "231919", - "Parch": 1, - "Cabin": null, - "PassengerId": 652, - "Age": 18, - "Fare": 23, - "Name": "Doling, Miss. Elsie", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "8475", - "Parch": 0, - "Cabin": null, - "PassengerId": 653, - "Age": 21, - "Fare": 8.4333, - "Name": "Kalvik, Mr. Johannes Halvorsen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330919", - "Parch": 0, - "Cabin": null, - "PassengerId": 654, - "Age": null, - "Fare": 7.8292, - "Name": "O'Leary, Miss. Hanora \"Norah\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "365226", - "Parch": 0, - "Cabin": null, - "PassengerId": 655, - "Age": 18, - "Fare": 6.75, - "Name": "Hegarty, Miss. Hanora \"Nora\"", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 2, - "Ticket": "S.O.C. 14879", - "Parch": 0, - "Cabin": null, - "PassengerId": 656, - "Age": 24, - "Fare": 73.5, - "Name": "Hickman, Mr. Leonard Mark", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349223", - "Parch": 0, - "Cabin": null, - "PassengerId": 657, - "Age": null, - "Fare": 7.8958, - "Name": "Radeff, Mr. Alexander", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "364849", - "Parch": 1, - "Cabin": null, - "PassengerId": 658, - "Age": 32, - "Fare": 15.5, - "Name": "Bourke, Mrs. John (Catherine)", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "29751", - "Parch": 0, - "Cabin": null, - "PassengerId": 659, - "Age": 23, - "Fare": 13, - "Name": "Eitemiller, Mr. George Floyd", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "35273", - "Parch": 2, - "Cabin": "D48", - "PassengerId": 660, - "Age": 58, - "Fare": 113.275, - "Name": "Newell, Mr. Arthur Webster", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "PC 17611", - "Parch": 0, - "Cabin": null, - "PassengerId": 661, - "Age": 50, - "Fare": 133.65, - "Name": "Frauenthal, Dr. Henry William", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2623", - "Parch": 0, - "Cabin": null, - "PassengerId": 662, - "Age": 40, - "Fare": 7.225, - "Name": "Badt, Mr. Mohamed", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "5727", - "Parch": 0, - "Cabin": "E58", - "PassengerId": 663, - "Age": 47, - "Fare": 25.5875, - "Name": "Colley, Mr. Edward Pomeroy", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349210", - "Parch": 0, - "Cabin": null, - "PassengerId": 664, - "Age": 36, - "Fare": 7.4958, - "Name": "Coleff, Mr. Peju", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "STON/O 2. 3101285", - "Parch": 0, - "Cabin": null, - "PassengerId": 665, - "Age": 20, - "Fare": 7.925, - "Name": "Lindqvist, Mr. Eino William", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "S.O.C. 14879", - "Parch": 0, - "Cabin": null, - "PassengerId": 666, - "Age": 32, - "Fare": 73.5, - "Name": "Hickman, Mr. Lewis", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "234686", - "Parch": 0, - "Cabin": null, - "PassengerId": 667, - "Age": 25, - "Fare": 13, - "Name": "Butler, Mr. Reginald Fenton", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "312993", - "Parch": 0, - "Cabin": null, - "PassengerId": 668, - "Age": null, - "Fare": 7.775, - "Name": "Rommetvedt, Mr. Knud Paust", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/5 3536", - "Parch": 0, - "Cabin": null, - "PassengerId": 669, - "Age": 43, - "Fare": 8.05, - "Name": "Cook, Mr. Jacob", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "19996", - "Parch": 0, - "Cabin": "C126", - "PassengerId": 670, - "Age": null, - "Fare": 52, - "Name": "Taylor, Mrs. Elmer Zebley (Juliet Cummins Wright)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "29750", - "Parch": 1, - "Cabin": null, - "PassengerId": 671, - "Age": 40, - "Fare": 39, - "Name": "Brown, Mrs. Thomas William Solomon (Elizabeth Catherine Ford)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "F.C. 12750", - "Parch": 0, - "Cabin": "B71", - "PassengerId": 672, - "Age": 31, - "Fare": 52, - "Name": "Davidson, Mr. Thornton", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 24580", - "Parch": 0, - "Cabin": null, - "PassengerId": 673, - "Age": 70, - "Fare": 10.5, - "Name": "Mitchell, Mr. Henry Michael", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "244270", - "Parch": 0, - "Cabin": null, - "PassengerId": 674, - "Age": 31, - "Fare": 13, - "Name": "Wilhelms, Mr. Charles", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "239856", - "Parch": 0, - "Cabin": null, - "PassengerId": 675, - "Age": null, - "Fare": 0, - "Name": "Watson, Mr. Ennis Hastings", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349912", - "Parch": 0, - "Cabin": null, - "PassengerId": 676, - "Age": 18, - "Fare": 7.775, - "Name": "Edvardsson, Mr. Gustaf Hjalmar", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "342826", - "Parch": 0, - "Cabin": null, - "PassengerId": 677, - "Age": 24.5, - "Fare": 8.05, - "Name": "Sawyer, Mr. Frederick Charles", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "4138", - "Parch": 0, - "Cabin": null, - "PassengerId": 678, - "Age": 18, - "Fare": 9.8417, - "Name": "Turja, Miss. Anna Sofia", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "CA 2144", - "Parch": 6, - "Cabin": null, - "PassengerId": 679, - "Age": 43, - "Fare": 46.9, - "Name": "Goodwin, Mrs. Frederick (Augusta Tyler)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17755", - "Parch": 1, - "Cabin": "B51 B53 B55", - "PassengerId": 680, - "Age": 36, - "Fare": 512.3292, - "Name": "Cardeza, Mr. Thomas Drake Martinez", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "330935", - "Parch": 0, - "Cabin": null, - "PassengerId": 681, - "Age": null, - "Fare": 8.1375, - "Name": "Peters, Miss. Katie", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17572", - "Parch": 0, - "Cabin": "D49", - "PassengerId": 682, - "Age": 27, - "Fare": 76.7292, - "Name": "Hassab, Mr. Hammad", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "6563", - "Parch": 0, - "Cabin": null, - "PassengerId": 683, - "Age": 20, - "Fare": 9.225, - "Name": "Olsvigen, Mr. Thor Anderson", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 5, - "Ticket": "CA 2144", - "Parch": 2, - "Cabin": null, - "PassengerId": 684, - "Age": 14, - "Fare": 46.9, - "Name": "Goodwin, Mr. Charles Edward", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "29750", - "Parch": 1, - "Cabin": null, - "PassengerId": 685, - "Age": 60, - "Fare": 39, - "Name": "Brown, Mr. Thomas William Solomon", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "SC/Paris 2123", - "Parch": 2, - "Cabin": null, - "PassengerId": 686, - "Age": 25, - "Fare": 41.5792, - "Name": "Laroche, Mr. Joseph Philippe Lemercier", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "3101295", - "Parch": 1, - "Cabin": null, - "PassengerId": 687, - "Age": 14, - "Fare": 39.6875, - "Name": "Panula, Mr. Jaako Arnold", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349228", - "Parch": 0, - "Cabin": null, - "PassengerId": 688, - "Age": 19, - "Fare": 10.1708, - "Name": "Dakic, Mr. Branko", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350036", - "Parch": 0, - "Cabin": null, - "PassengerId": 689, - "Age": 18, - "Fare": 7.7958, - "Name": "Fischer, Mr. Eberhard Thelander", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "24160", - "Parch": 1, - "Cabin": "B5", - "PassengerId": 690, - "Age": 15, - "Fare": 211.3375, - "Name": "Madill, Miss. Georgette Alexandra", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "17474", - "Parch": 0, - "Cabin": "B20", - "PassengerId": 691, - "Age": 31, - "Fare": 57, - "Name": "Dick, Mr. Albert Adrian", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349256", - "Parch": 1, - "Cabin": null, - "PassengerId": 692, - "Age": 4, - "Fare": 13.4167, - "Name": "Karun, Miss. Manca", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 693, - "Age": null, - "Fare": 56.4958, - "Name": "Lam, Mr. Ali", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2672", - "Parch": 0, - "Cabin": null, - "PassengerId": 694, - "Age": 25, - "Fare": 7.225, - "Name": "Saad, Mr. Khalil", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113800", - "Parch": 0, - "Cabin": null, - "PassengerId": 695, - "Age": 60, - "Fare": 26.55, - "Name": "Weir, Col. John", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248731", - "Parch": 0, - "Cabin": null, - "PassengerId": 696, - "Age": 52, - "Fare": 13.5, - "Name": "Chapman, Mr. Charles Henry", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "363592", - "Parch": 0, - "Cabin": null, - "PassengerId": 697, - "Age": 44, - "Fare": 8.05, - "Name": "Kelly, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "35852", - "Parch": 0, - "Cabin": null, - "PassengerId": 698, - "Age": null, - "Fare": 7.7333, - "Name": "Mullens, Miss. Katherine \"Katie\"", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "17421", - "Parch": 1, - "Cabin": "C68", - "PassengerId": 699, - "Age": 49, - "Fare": 110.8833, - "Name": "Thayer, Mr. John Borland", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "348121", - "Parch": 0, - "Cabin": "F G63", - "PassengerId": 700, - "Age": 42, - "Fare": 7.65, - "Name": "Humblen, Mr. Adolf Mathias Nicolai Olsen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17757", - "Parch": 0, - "Cabin": "C62 C64", - "PassengerId": 701, - "Age": 18, - "Fare": 227.525, - "Name": "Astor, Mrs. John Jacob (Madeleine Talmadge Force)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17475", - "Parch": 0, - "Cabin": "E24", - "PassengerId": 702, - "Age": 35, - "Fare": 26.2875, - "Name": "Silverthorne, Mr. Spencer Victor", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2691", - "Parch": 1, - "Cabin": null, - "PassengerId": 703, - "Age": 18, - "Fare": 14.4542, - "Name": "Barbara, Miss. Saiide", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "36864", - "Parch": 0, - "Cabin": null, - "PassengerId": 704, - "Age": 25, - "Fare": 7.7417, - "Name": "Gallagher, Mr. Martin", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "350025", - "Parch": 0, - "Cabin": null, - "PassengerId": 705, - "Age": 26, - "Fare": 7.8542, - "Name": "Hansen, Mr. Henrik Juul", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250655", - "Parch": 0, - "Cabin": null, - "PassengerId": 706, - "Age": 39, - "Fare": 26, - "Name": "Morley, Mr. Henry Samuel (\"Mr Henry Marshall\")", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "223596", - "Parch": 0, - "Cabin": null, - "PassengerId": 707, - "Age": 45, - "Fare": 13.5, - "Name": "Kelly, Mrs. Florence \"Fannie\"", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17476", - "Parch": 0, - "Cabin": "E24", - "PassengerId": 708, - "Age": 42, - "Fare": 26.2875, - "Name": "Calderhead, Mr. Edward Pennington", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113781", - "Parch": 0, - "Cabin": null, - "PassengerId": 709, - "Age": 22, - "Fare": 151.55, - "Name": "Cleaver, Miss. Alice", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "2661", - "Parch": 1, - "Cabin": null, - "PassengerId": 710, - "Age": null, - "Fare": 15.2458, - "Name": "Moubarek, Master. Halim Gonios (\"William George\")", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17482", - "Parch": 0, - "Cabin": "C90", - "PassengerId": 711, - "Age": 24, - "Fare": 49.5042, - "Name": "Mayne, Mlle. Berthe Antonine (\"Mrs de Villiers\")", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113028", - "Parch": 0, - "Cabin": "C124", - "PassengerId": 712, - "Age": null, - "Fare": 26.55, - "Name": "Klaber, Mr. Herman", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "19996", - "Parch": 0, - "Cabin": "C126", - "PassengerId": 713, - "Age": 48, - "Fare": 52, - "Name": "Taylor, Mr. Elmer Zebley", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "7545", - "Parch": 0, - "Cabin": null, - "PassengerId": 714, - "Age": 29, - "Fare": 9.4833, - "Name": "Larsson, Mr. August Viktor", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250647", - "Parch": 0, - "Cabin": null, - "PassengerId": 715, - "Age": 52, - "Fare": 13, - "Name": "Greenberg, Mr. Samuel", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "348124", - "Parch": 0, - "Cabin": "F G73", - "PassengerId": 716, - "Age": 19, - "Fare": 7.65, - "Name": "Soholt, Mr. Peter Andreas Lauritz Andersen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17757", - "Parch": 0, - "Cabin": "C45", - "PassengerId": 717, - "Age": 38, - "Fare": 227.525, - "Name": "Endres, Miss. Caroline Louise", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "34218", - "Parch": 0, - "Cabin": "E101", - "PassengerId": 718, - "Age": 27, - "Fare": 10.5, - "Name": "Troutt, Miss. Edwina Celia \"Winnie\"", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "36568", - "Parch": 0, - "Cabin": null, - "PassengerId": 719, - "Age": null, - "Fare": 15.5, - "Name": "McEvoy, Mr. Michael", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347062", - "Parch": 0, - "Cabin": null, - "PassengerId": 720, - "Age": 33, - "Fare": 7.775, - "Name": "Johnson, Mr. Malkolm Joackim", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248727", - "Parch": 1, - "Cabin": null, - "PassengerId": 721, - "Age": 6, - "Fare": 33, - "Name": "Harper, Miss. Annie Jessie \"Nina\"", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "350048", - "Parch": 0, - "Cabin": null, - "PassengerId": 722, - "Age": 17, - "Fare": 7.0542, - "Name": "Jensen, Mr. Svend Lauritz", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "12233", - "Parch": 0, - "Cabin": null, - "PassengerId": 723, - "Age": 34, - "Fare": 13, - "Name": "Gillespie, Mr. William Henry", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250643", - "Parch": 0, - "Cabin": null, - "PassengerId": 724, - "Age": 50, - "Fare": 13, - "Name": "Hodges, Mr. Henry Price", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113806", - "Parch": 0, - "Cabin": "E8", - "PassengerId": 725, - "Age": 27, - "Fare": 53.1, - "Name": "Chambers, Mr. Norman Campbell", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315094", - "Parch": 0, - "Cabin": null, - "PassengerId": 726, - "Age": 20, - "Fare": 8.6625, - "Name": "Oreskovic, Mr. Luka", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "31027", - "Parch": 0, - "Cabin": null, - "PassengerId": 727, - "Age": 30, - "Fare": 21, - "Name": "Renouf, Mrs. Peter Henry (Lillian Jefferys)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "36866", - "Parch": 0, - "Cabin": null, - "PassengerId": 728, - "Age": null, - "Fare": 7.7375, - "Name": "Mannion, Miss. Margareth", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "236853", - "Parch": 0, - "Cabin": null, - "PassengerId": 729, - "Age": 25, - "Fare": 26, - "Name": "Bryhl, Mr. Kurt Arnold Gottfrid", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "STON/O2. 3101271", - "Parch": 0, - "Cabin": null, - "PassengerId": 730, - "Age": 25, - "Fare": 7.925, - "Name": "Ilmakangas, Miss. Pieta Sofia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "24160", - "Parch": 0, - "Cabin": "B5", - "PassengerId": 731, - "Age": 29, - "Fare": 211.3375, - "Name": "Allen, Miss. Elisabeth Walton", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2699", - "Parch": 0, - "Cabin": null, - "PassengerId": 732, - "Age": 11, - "Fare": 18.7875, - "Name": "Hassan, Mr. Houssein G N", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "239855", - "Parch": 0, - "Cabin": null, - "PassengerId": 733, - "Age": null, - "Fare": 0, - "Name": "Knight, Mr. Robert J", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "28425", - "Parch": 0, - "Cabin": null, - "PassengerId": 734, - "Age": 23, - "Fare": 13, - "Name": "Berriman, Mr. William John", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "233639", - "Parch": 0, - "Cabin": null, - "PassengerId": 735, - "Age": 23, - "Fare": 13, - "Name": "Troupiansky, Mr. Moses Aaron", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "54636", - "Parch": 0, - "Cabin": null, - "PassengerId": 736, - "Age": 28.5, - "Fare": 16.1, - "Name": "Williams, Mr. Leslie", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "W./C. 6608", - "Parch": 3, - "Cabin": null, - "PassengerId": 737, - "Age": 48, - "Fare": 34.375, - "Name": "Ford, Mrs. Edward (Margaret Ann Watson)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17755", - "Parch": 0, - "Cabin": "B101", - "PassengerId": 738, - "Age": 35, - "Fare": 512.3292, - "Name": "Lesurer, Mr. Gustave J", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349201", - "Parch": 0, - "Cabin": null, - "PassengerId": 739, - "Age": null, - "Fare": 7.8958, - "Name": "Ivanoff, Mr. Kanio", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349218", - "Parch": 0, - "Cabin": null, - "PassengerId": 740, - "Age": null, - "Fare": 7.8958, - "Name": "Nankoff, Mr. Minko", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "16988", - "Parch": 0, - "Cabin": "D45", - "PassengerId": 741, - "Age": null, - "Fare": 30, - "Name": "Hawksford, Mr. Walter James", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "19877", - "Parch": 0, - "Cabin": "C46", - "PassengerId": 742, - "Age": 36, - "Fare": 78.85, - "Name": "Cavendish, Mr. Tyrell William", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "PC 17608", - "Parch": 2, - "Cabin": "B57 B59 B63 B66", - "PassengerId": 743, - "Age": 21, - "Fare": 262.375, - "Name": "Ryerson, Miss. Susan Parker \"Suzette\"", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "376566", - "Parch": 0, - "Cabin": null, - "PassengerId": 744, - "Age": 24, - "Fare": 16.1, - "Name": "McNamee, Mr. Neal", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "STON/O 2. 3101288", - "Parch": 0, - "Cabin": null, - "PassengerId": 745, - "Age": 31, - "Fare": 7.925, - "Name": "Stranden, Mr. Juho", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "WE/P 5735", - "Parch": 1, - "Cabin": "B22", - "PassengerId": 746, - "Age": 70, - "Fare": 71, - "Name": "Crosby, Capt. Edward Gifford", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 2673", - "Parch": 1, - "Cabin": null, - "PassengerId": 747, - "Age": 16, - "Fare": 20.25, - "Name": "Abbott, Mr. Rossmore Edward", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "250648", - "Parch": 0, - "Cabin": null, - "PassengerId": 748, - "Age": 30, - "Fare": 13, - "Name": "Sinkkonen, Miss. Anna", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "113773", - "Parch": 0, - "Cabin": "D30", - "PassengerId": 749, - "Age": 19, - "Fare": 53.1, - "Name": "Marvin, Mr. Daniel Warner", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "335097", - "Parch": 0, - "Cabin": null, - "PassengerId": 750, - "Age": 31, - "Fare": 7.75, - "Name": "Connaghton, Mr. Michael", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "29103", - "Parch": 1, - "Cabin": null, - "PassengerId": 751, - "Age": 4, - "Fare": 23, - "Name": "Wells, Miss. Joan", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "392096", - "Parch": 1, - "Cabin": "E121", - "PassengerId": 752, - "Age": 6, - "Fare": 12.475, - "Name": "Moor, Master. Meier", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345780", - "Parch": 0, - "Cabin": null, - "PassengerId": 753, - "Age": 33, - "Fare": 9.5, - "Name": "Vande Velde, Mr. Johannes Joseph", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349204", - "Parch": 0, - "Cabin": null, - "PassengerId": 754, - "Age": 23, - "Fare": 7.8958, - "Name": "Jonkoff, Mr. Lalio", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "220845", - "Parch": 2, - "Cabin": null, - "PassengerId": 755, - "Age": 48, - "Fare": 65, - "Name": "Herman, Mrs. Samuel (Jane Laver)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "250649", - "Parch": 1, - "Cabin": null, - "PassengerId": 756, - "Age": 0.67, - "Fare": 14.5, - "Name": "Hamalainen, Master. Viljo", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350042", - "Parch": 0, - "Cabin": null, - "PassengerId": 757, - "Age": 28, - "Fare": 7.7958, - "Name": "Carlsson, Mr. August Sigfrid", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "29108", - "Parch": 0, - "Cabin": null, - "PassengerId": 758, - "Age": 18, - "Fare": 11.5, - "Name": "Bailey, Mr. Percy Andrew", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "363294", - "Parch": 0, - "Cabin": null, - "PassengerId": 759, - "Age": 34, - "Fare": 8.05, - "Name": "Theobald, Mr. Thomas Leonard", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "110152", - "Parch": 0, - "Cabin": "B77", - "PassengerId": 760, - "Age": 33, - "Fare": 86.5, - "Name": "Rothes, the Countess. of (Lucy Noel Martha Dyer-Edwards)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "358585", - "Parch": 0, - "Cabin": null, - "PassengerId": 761, - "Age": null, - "Fare": 14.5, - "Name": "Garfirth, Mr. John", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O2 3101272", - "Parch": 0, - "Cabin": null, - "PassengerId": 762, - "Age": 41, - "Fare": 7.125, - "Name": "Nirva, Mr. Iisakki Antino Aijo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2663", - "Parch": 0, - "Cabin": null, - "PassengerId": 763, - "Age": 20, - "Fare": 7.2292, - "Name": "Barah, Mr. Hanna Assi", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113760", - "Parch": 2, - "Cabin": "B96 B98", - "PassengerId": 764, - "Age": 36, - "Fare": 120, - "Name": "Carter, Mrs. William Ernest (Lucile Polk)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347074", - "Parch": 0, - "Cabin": null, - "PassengerId": 765, - "Age": 16, - "Fare": 7.775, - "Name": "Eklund, Mr. Hans Linus", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "13502", - "Parch": 0, - "Cabin": "D11", - "PassengerId": 766, - "Age": 51, - "Fare": 77.9583, - "Name": "Hogeboom, Mrs. John C (Anna Andrews)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "112379", - "Parch": 0, - "Cabin": null, - "PassengerId": 767, - "Age": null, - "Fare": 39.6, - "Name": "Brewe, Dr. Arthur Jackson", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364850", - "Parch": 0, - "Cabin": null, - "PassengerId": 768, - "Age": 30.5, - "Fare": 7.75, - "Name": "Mangan, Miss. Mary", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "371110", - "Parch": 0, - "Cabin": null, - "PassengerId": 769, - "Age": null, - "Fare": 24.15, - "Name": "Moran, Mr. Daniel J", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "8471", - "Parch": 0, - "Cabin": null, - "PassengerId": 770, - "Age": 32, - "Fare": 8.3625, - "Name": "Gronnestad, Mr. Daniel Danielsen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345781", - "Parch": 0, - "Cabin": null, - "PassengerId": 771, - "Age": 24, - "Fare": 9.5, - "Name": "Lievens, Mr. Rene Aime", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "350047", - "Parch": 0, - "Cabin": null, - "PassengerId": 772, - "Age": 48, - "Fare": 7.8542, - "Name": "Jensen, Mr. Niels Peder", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.O./P.P. 3", - "Parch": 0, - "Cabin": "E77", - "PassengerId": 773, - "Age": 57, - "Fare": 10.5, - "Name": "Mack, Mrs. (Mary)", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2674", - "Parch": 0, - "Cabin": null, - "PassengerId": 774, - "Age": null, - "Fare": 7.225, - "Name": "Elias, Mr. Dibo", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "29105", - "Parch": 3, - "Cabin": null, - "PassengerId": 775, - "Age": 54, - "Fare": 23, - "Name": "Hocking, Mrs. Elizabeth (Eliza Needs)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "347078", - "Parch": 0, - "Cabin": null, - "PassengerId": 776, - "Age": 18, - "Fare": 7.75, - "Name": "Myhrman, Mr. Pehr Fabian Oliver Malkolm", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "383121", - "Parch": 0, - "Cabin": "F38", - "PassengerId": 777, - "Age": null, - "Fare": 7.75, - "Name": "Tobin, Mr. Roger", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "364516", - "Parch": 0, - "Cabin": null, - "PassengerId": 778, - "Age": 5, - "Fare": 12.475, - "Name": "Emanuel, Miss. Virginia Ethel", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "36865", - "Parch": 0, - "Cabin": null, - "PassengerId": 779, - "Age": null, - "Fare": 7.7375, - "Name": "Kilgannon, Mr. Thomas J", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "24160", - "Parch": 1, - "Cabin": "B3", - "PassengerId": 780, - "Age": 43, - "Fare": 211.3375, - "Name": "Robert, Mrs. Edward Scott (Elisabeth Walton McMillan)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2687", - "Parch": 0, - "Cabin": null, - "PassengerId": 781, - "Age": 13, - "Fare": 7.2292, - "Name": "Ayoub, Miss. Banoura", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "17474", - "Parch": 0, - "Cabin": "B20", - "PassengerId": 782, - "Age": 17, - "Fare": 57, - "Name": "Dick, Mrs. Albert Adrian (Vera Gillespie)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113501", - "Parch": 0, - "Cabin": "D6", - "PassengerId": 783, - "Age": 29, - "Fare": 30, - "Name": "Long, Mr. Milton Clyde", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "W./C. 6607", - "Parch": 2, - "Cabin": null, - "PassengerId": 784, - "Age": null, - "Fare": 23.45, - "Name": "Johnston, Mr. Andrew G", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O.Q. 3101312", - "Parch": 0, - "Cabin": null, - "PassengerId": 785, - "Age": 25, - "Fare": 7.05, - "Name": "Ali, Mr. William", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "374887", - "Parch": 0, - "Cabin": null, - "PassengerId": 786, - "Age": 25, - "Fare": 7.25, - "Name": "Harmer, Mr. Abraham (David Lishin)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "3101265", - "Parch": 0, - "Cabin": null, - "PassengerId": 787, - "Age": 18, - "Fare": 7.4958, - "Name": "Sjoblom, Miss. Anna Sofia", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "382652", - "Parch": 1, - "Cabin": null, - "PassengerId": 788, - "Age": 8, - "Fare": 29.125, - "Name": "Rice, Master. George Hugh", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 2315", - "Parch": 2, - "Cabin": null, - "PassengerId": 789, - "Age": 1, - "Fare": 20.575, - "Name": "Dean, Master. Bertram Vere", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "PC 17593", - "Parch": 0, - "Cabin": "B82 B84", - "PassengerId": 790, - "Age": 46, - "Fare": 79.2, - "Name": "Guggenheim, Mr. Benjamin", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "12460", - "Parch": 0, - "Cabin": null, - "PassengerId": 791, - "Age": null, - "Fare": 7.75, - "Name": "Keane, Mr. Andrew \"Andy\"", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "239865", - "Parch": 0, - "Cabin": null, - "PassengerId": 792, - "Age": 16, - "Fare": 26, - "Name": "Gaskell, Mr. Alfred", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 793, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Miss. Stella Anna", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17600", - "Parch": 0, - "Cabin": null, - "PassengerId": 794, - "Age": null, - "Fare": 30.6958, - "Name": "Hoyt, Mr. William Fisher", - "Survived": false, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349203", - "Parch": 0, - "Cabin": null, - "PassengerId": 795, - "Age": 25, - "Fare": 7.8958, - "Name": "Dantcheff, Mr. Ristiu", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "28213", - "Parch": 0, - "Cabin": null, - "PassengerId": 796, - "Age": 39, - "Fare": 13, - "Name": "Otter, Mr. Richard", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "17465", - "Parch": 0, - "Cabin": "D17", - "PassengerId": 797, - "Age": 49, - "Fare": 25.9292, - "Name": "Leader, Dr. Alice (Farnham)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349244", - "Parch": 0, - "Cabin": null, - "PassengerId": 798, - "Age": 31, - "Fare": 8.6833, - "Name": "Osman, Mrs. Mara", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2685", - "Parch": 0, - "Cabin": null, - "PassengerId": 799, - "Age": 30, - "Fare": 7.2292, - "Name": "Ibrahim Shawah, Mr. Yousseff", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "345773", - "Parch": 1, - "Cabin": null, - "PassengerId": 800, - "Age": 30, - "Fare": 24.15, - "Name": "Van Impe, Mrs. Jean Baptiste (Rosalie Paula Govaert)", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "250647", - "Parch": 0, - "Cabin": null, - "PassengerId": 801, - "Age": 34, - "Fare": 13, - "Name": "Ponesell, Mr. Martin", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "C.A. 31921", - "Parch": 1, - "Cabin": null, - "PassengerId": 802, - "Age": 31, - "Fare": 26.25, - "Name": "Collyer, Mrs. Harvey (Charlotte Annie Tate)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "113760", - "Parch": 2, - "Cabin": "B96 B98", - "PassengerId": 803, - "Age": 11, - "Fare": 120, - "Name": "Carter, Master. William Thornton II", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2625", - "Parch": 1, - "Cabin": null, - "PassengerId": 804, - "Age": 0.42, - "Fare": 8.5167, - "Name": "Thomas, Master. Assad Alexander", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347089", - "Parch": 0, - "Cabin": null, - "PassengerId": 805, - "Age": 27, - "Fare": 6.975, - "Name": "Hedman, Mr. Oskar Arvid", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347063", - "Parch": 0, - "Cabin": null, - "PassengerId": 806, - "Age": 31, - "Fare": 7.775, - "Name": "Johansson, Mr. Karl Johan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "112050", - "Parch": 0, - "Cabin": "A36", - "PassengerId": 807, - "Age": 39, - "Fare": 0, - "Name": "Andrews, Mr. Thomas Jr", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347087", - "Parch": 0, - "Cabin": null, - "PassengerId": 808, - "Age": 18, - "Fare": 7.775, - "Name": "Pettersson, Miss. Ellen Natalia", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "248723", - "Parch": 0, - "Cabin": null, - "PassengerId": 809, - "Age": 39, - "Fare": 13, - "Name": "Meyer, Mr. August", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "113806", - "Parch": 0, - "Cabin": "E8", - "PassengerId": 810, - "Age": 33, - "Fare": 53.1, - "Name": "Chambers, Mrs. Norman Campbell (Bertha Griggs)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "3474", - "Parch": 0, - "Cabin": null, - "PassengerId": 811, - "Age": 26, - "Fare": 7.8875, - "Name": "Alexander, Mr. William", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "A/4 48871", - "Parch": 0, - "Cabin": null, - "PassengerId": 812, - "Age": 39, - "Fare": 24.15, - "Name": "Lester, Mr. James", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "28206", - "Parch": 0, - "Cabin": null, - "PassengerId": 813, - "Age": 35, - "Fare": 10.5, - "Name": "Slemen, Mr. Richard James", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 4, - "Ticket": "347082", - "Parch": 2, - "Cabin": null, - "PassengerId": 814, - "Age": 6, - "Fare": 31.275, - "Name": "Andersson, Miss. Ebba Iris Alfrida", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "364499", - "Parch": 0, - "Cabin": null, - "PassengerId": 815, - "Age": 30.5, - "Fare": 8.05, - "Name": "Tomlin, Mr. Ernest Portage", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "112058", - "Parch": 0, - "Cabin": "B102", - "PassengerId": 816, - "Age": null, - "Fare": 0, - "Name": "Fry, Mr. Richard", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "STON/O2. 3101290", - "Parch": 0, - "Cabin": null, - "PassengerId": 817, - "Age": 23, - "Fare": 7.925, - "Name": "Heininen, Miss. Wendla Maria", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "S.C./PARIS 2079", - "Parch": 1, - "Cabin": null, - "PassengerId": 818, - "Age": 31, - "Fare": 37.0042, - "Name": "Mallet, Mr. Albert", - "Survived": false, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C 7075", - "Parch": 0, - "Cabin": null, - "PassengerId": 819, - "Age": 43, - "Fare": 6.45, - "Name": "Holm, Mr. John Fredrik Alexander", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 3, - "Ticket": "347088", - "Parch": 2, - "Cabin": null, - "PassengerId": 820, - "Age": 10, - "Fare": 27.9, - "Name": "Skoog, Master. Karl Thorsten", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "12749", - "Parch": 1, - "Cabin": "B69", - "PassengerId": 821, - "Age": 52, - "Fare": 93.5, - "Name": "Hays, Mrs. Charles Melville (Clara Jennings Gregg)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "315098", - "Parch": 0, - "Cabin": null, - "PassengerId": 822, - "Age": 27, - "Fare": 8.6625, - "Name": "Lulic, Mr. Nikola", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "19972", - "Parch": 0, - "Cabin": null, - "PassengerId": 823, - "Age": 38, - "Fare": 0, - "Name": "Reuchlin, Jonkheer. John George", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "392096", - "Parch": 1, - "Cabin": "E121", - "PassengerId": 824, - "Age": 27, - "Fare": 12.475, - "Name": "Moor, Mrs. (Beila)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "3101295", - "Parch": 1, - "Cabin": null, - "PassengerId": 825, - "Age": 2, - "Fare": 39.6875, - "Name": "Panula, Master. Urho Abraham", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "368323", - "Parch": 0, - "Cabin": null, - "PassengerId": 826, - "Age": null, - "Fare": 6.95, - "Name": "Flynn, Mr. John", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 827, - "Age": null, - "Fare": 56.4958, - "Name": "Lam, Mr. Len", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.C./PARIS 2079", - "Parch": 2, - "Cabin": null, - "PassengerId": 828, - "Age": 1, - "Fare": 37.0042, - "Name": "Mallet, Master. Andre", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "367228", - "Parch": 0, - "Cabin": null, - "PassengerId": 829, - "Age": null, - "Fare": 7.75, - "Name": "McCormack, Mr. Thomas Joseph", - "Survived": true, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113572", - "Parch": 0, - "Cabin": "B28", - "PassengerId": 830, - "Age": 62, - "Fare": 80, - "Name": "Stone, Mrs. George Nelson (Martha Evelyn)", - "Survived": true, - "Pclass": 1, - "Embarked": null, - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "2659", - "Parch": 0, - "Cabin": null, - "PassengerId": 831, - "Age": 15, - "Fare": 14.4542, - "Name": "Yasbeck, Mrs. Antoni (Selini Alexander)", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "29106", - "Parch": 1, - "Cabin": null, - "PassengerId": 832, - "Age": 0.83, - "Fare": 18.75, - "Name": "Richards, Master. George Sibley", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2671", - "Parch": 0, - "Cabin": null, - "PassengerId": 833, - "Age": null, - "Fare": 7.2292, - "Name": "Saad, Mr. Amin", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347468", - "Parch": 0, - "Cabin": null, - "PassengerId": 834, - "Age": 23, - "Fare": 7.8542, - "Name": "Augustsson, Mr. Albert", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2223", - "Parch": 0, - "Cabin": null, - "PassengerId": 835, - "Age": 18, - "Fare": 8.3, - "Name": "Allum, Mr. Owen George", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "PC 17756", - "Parch": 1, - "Cabin": "E49", - "PassengerId": 836, - "Age": 39, - "Fare": 83.1583, - "Name": "Compton, Miss. Sara Rebecca", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "315097", - "Parch": 0, - "Cabin": null, - "PassengerId": 837, - "Age": 21, - "Fare": 8.6625, - "Name": "Pasic, Mr. Jakob", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "392092", - "Parch": 0, - "Cabin": null, - "PassengerId": 838, - "Age": null, - "Fare": 8.05, - "Name": "Sirota, Mr. Maurice", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "1601", - "Parch": 0, - "Cabin": null, - "PassengerId": 839, - "Age": 32, - "Fare": 56.4958, - "Name": "Chip, Mr. Chang", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "11774", - "Parch": 0, - "Cabin": "C47", - "PassengerId": 840, - "Age": null, - "Fare": 29.7, - "Name": "Marechal, Mr. Pierre", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/O2 3101287", - "Parch": 0, - "Cabin": null, - "PassengerId": 841, - "Age": 20, - "Fare": 7.925, - "Name": "Alhomaki, Mr. Ilmari Rudolf", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "S.O./P.P. 3", - "Parch": 0, - "Cabin": null, - "PassengerId": 842, - "Age": 16, - "Fare": 10.5, - "Name": "Mudd, Mr. Thomas Charles", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "113798", - "Parch": 0, - "Cabin": null, - "PassengerId": 843, - "Age": 30, - "Fare": 31, - "Name": "Serepeca, Miss. Augusta", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2683", - "Parch": 0, - "Cabin": null, - "PassengerId": 844, - "Age": 34.5, - "Fare": 6.4375, - "Name": "Lemberopolous, Mr. Peter L", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "315090", - "Parch": 0, - "Cabin": null, - "PassengerId": 845, - "Age": 17, - "Fare": 8.6625, - "Name": "Culumovic, Mr. Jeso", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "C.A. 5547", - "Parch": 0, - "Cabin": null, - "PassengerId": 846, - "Age": 42, - "Fare": 7.55, - "Name": "Abbing, Mr. Anthony", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 847, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Mr. Douglas Bullen", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349213", - "Parch": 0, - "Cabin": null, - "PassengerId": 848, - "Age": 35, - "Fare": 7.8958, - "Name": "Markoff, Mr. Marin", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "248727", - "Parch": 1, - "Cabin": null, - "PassengerId": 849, - "Age": 28, - "Fare": 33, - "Name": "Harper, Rev. John", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "17453", - "Parch": 0, - "Cabin": "C92", - "PassengerId": 850, - "Age": null, - "Fare": 89.1042, - "Name": "Goldenberg, Mrs. Samuel L (Edwiga Grabowska)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 4, - "Ticket": "347082", - "Parch": 2, - "Cabin": null, - "PassengerId": 851, - "Age": 4, - "Fare": 31.275, - "Name": "Andersson, Master. Sigvard Harald Elias", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "347060", - "Parch": 0, - "Cabin": null, - "PassengerId": 852, - "Age": 74, - "Fare": 7.775, - "Name": "Svensson, Mr. Johan", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "2678", - "Parch": 1, - "Cabin": null, - "PassengerId": 853, - "Age": 9, - "Fare": 15.2458, - "Name": "Boulos, Miss. Nourelain", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17592", - "Parch": 1, - "Cabin": "D28", - "PassengerId": 854, - "Age": 16, - "Fare": 39.4, - "Name": "Lines, Miss. Mary Conover", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "244252", - "Parch": 0, - "Cabin": null, - "PassengerId": 855, - "Age": 44, - "Fare": 26, - "Name": "Carter, Mrs. Ernest Courtenay (Lilian Hughes)", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "392091", - "Parch": 1, - "Cabin": null, - "PassengerId": 856, - "Age": 18, - "Fare": 9.35, - "Name": "Aks, Mrs. Sam (Leah Rosen)", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "36928", - "Parch": 1, - "Cabin": null, - "PassengerId": 857, - "Age": 45, - "Fare": 164.8667, - "Name": "Wick, Mrs. George Dennick (Mary Hitchcock)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "113055", - "Parch": 0, - "Cabin": "E17", - "PassengerId": 858, - "Age": 51, - "Fare": 26.55, - "Name": "Daly, Mr. Peter Denis ", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "2666", - "Parch": 3, - "Cabin": null, - "PassengerId": 859, - "Age": 24, - "Fare": 19.2583, - "Name": "Baclini, Mrs. Solomon (Latifa Qurban)", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2629", - "Parch": 0, - "Cabin": null, - "PassengerId": 860, - "Age": null, - "Fare": 7.2292, - "Name": "Razi, Mr. Raihed", - "Survived": false, - "Pclass": 3, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 2, - "Ticket": "350026", - "Parch": 0, - "Cabin": null, - "PassengerId": 861, - "Age": 41, - "Fare": 14.1083, - "Name": "Hansen, Mr. Claus Peter", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "28134", - "Parch": 0, - "Cabin": null, - "PassengerId": 862, - "Age": 21, - "Fare": 11.5, - "Name": "Giles, Mr. Frederick Edward", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "17466", - "Parch": 0, - "Cabin": "D17", - "PassengerId": 863, - "Age": 48, - "Fare": 25.9292, - "Name": "Swift, Mrs. Frederick Joel (Margaret Welles Barron)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 8, - "Ticket": "CA. 2343", - "Parch": 2, - "Cabin": null, - "PassengerId": 864, - "Age": null, - "Fare": 69.55, - "Name": "Sage, Miss. Dorothy Edith \"Dolly\"", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "233866", - "Parch": 0, - "Cabin": null, - "PassengerId": 865, - "Age": 24, - "Fare": 13, - "Name": "Gill, Mr. John William", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "236852", - "Parch": 0, - "Cabin": null, - "PassengerId": 866, - "Age": 42, - "Fare": 13, - "Name": "Bystrom, Mrs. (Karolina)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "SC/PARIS 2149", - "Parch": 0, - "Cabin": null, - "PassengerId": 867, - "Age": 27, - "Fare": 13.8583, - "Name": "Duran y More, Miss. Asuncion", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "PC 17590", - "Parch": 0, - "Cabin": "A24", - "PassengerId": 868, - "Age": 31, - "Fare": 50.4958, - "Name": "Roebling, Mr. Washington Augustus II", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345777", - "Parch": 0, - "Cabin": null, - "PassengerId": 869, - "Age": null, - "Fare": 9.5, - "Name": "van Melkebeke, Mr. Philemon", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "347742", - "Parch": 1, - "Cabin": null, - "PassengerId": 870, - "Age": 4, - "Fare": 11.1333, - "Name": "Johnson, Master. Harold Theodor", - "Survived": true, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349248", - "Parch": 0, - "Cabin": null, - "PassengerId": 871, - "Age": 26, - "Fare": 7.8958, - "Name": "Balkic, Mr. Cerin", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "11751", - "Parch": 1, - "Cabin": "D35", - "PassengerId": 872, - "Age": 47, - "Fare": 52.5542, - "Name": "Beckwith, Mrs. Richard Leonard (Sallie Monypeny)", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "695", - "Parch": 0, - "Cabin": "B51 B53 B55", - "PassengerId": 873, - "Age": 33, - "Fare": 5, - "Name": "Carlsson, Mr. Frans Olof", - "Survived": false, - "Pclass": 1, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "345765", - "Parch": 0, - "Cabin": null, - "PassengerId": 874, - "Age": 47, - "Fare": 9, - "Name": "Vander Cruyssen, Mr. Victor", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 1, - "Ticket": "P/PP 3381", - "Parch": 0, - "Cabin": null, - "PassengerId": 875, - "Age": 28, - "Fare": 24, - "Name": "Abelson, Mrs. Samuel (Hannah Wizosky)", - "Survived": true, - "Pclass": 2, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "2667", - "Parch": 0, - "Cabin": null, - "PassengerId": 876, - "Age": 15, - "Fare": 7.225, - "Name": "Najib, Miss. Adele Kiamie \"Jane\"", - "Survived": true, - "Pclass": 3, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "7534", - "Parch": 0, - "Cabin": null, - "PassengerId": 877, - "Age": 20, - "Fare": 9.8458, - "Name": "Gustafsson, Mr. Alfred Ossian", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349212", - "Parch": 0, - "Cabin": null, - "PassengerId": 878, - "Age": 19, - "Fare": 7.8958, - "Name": "Petroff, Mr. Nedelio", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "349217", - "Parch": 0, - "Cabin": null, - "PassengerId": 879, - "Age": null, - "Fare": 7.8958, - "Name": "Laleff, Mr. Kristo", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "11767", - "Parch": 1, - "Cabin": "C50", - "PassengerId": 880, - "Age": 56, - "Fare": 83.1583, - "Name": "Potter, Mrs. Thomas Jr (Lily Alexenia Wilson)", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "230433", - "Parch": 1, - "Cabin": null, - "PassengerId": 881, - "Age": 25, - "Fare": 26, - "Name": "Shelley, Mrs. William (Imanita Parrish Hall)", - "Survived": true, - "Pclass": 2, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "349257", - "Parch": 0, - "Cabin": null, - "PassengerId": 882, - "Age": 33, - "Fare": 7.8958, - "Name": "Markun, Mr. Johann", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "7552", - "Parch": 0, - "Cabin": null, - "PassengerId": 883, - "Age": 22, - "Fare": 10.5167, - "Name": "Dahlberg, Miss. Gerda Ulrika", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "C.A./SOTON 34068", - "Parch": 0, - "Cabin": null, - "PassengerId": 884, - "Age": 28, - "Fare": 10.5, - "Name": "Banfield, Mr. Frederick James", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "SOTON/OQ 392076", - "Parch": 0, - "Cabin": null, - "PassengerId": 885, - "Age": 25, - "Fare": 7.05, - "Name": "Sutehall, Mr. Henry Jr", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "382652", - "Parch": 5, - "Cabin": null, - "PassengerId": 886, - "Age": 39, - "Fare": 29.125, - "Name": "Rice, Mrs. William (Margaret Norton)", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "211536", - "Parch": 0, - "Cabin": null, - "PassengerId": 887, - "Age": 27, - "Fare": 13, - "Name": "Montvila, Rev. Juozas", - "Survived": false, - "Pclass": 2, - "Embarked": "S", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "112053", - "Parch": 0, - "Cabin": "B42", - "PassengerId": 888, - "Age": 19, - "Fare": 30, - "Name": "Graham, Miss. Margaret Edith", - "Survived": true, - "Pclass": 1, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 1, - "Ticket": "W./C. 6607", - "Parch": 2, - "Cabin": null, - "PassengerId": 889, - "Age": null, - "Fare": 23.45, - "Name": "Johnston, Miss. Catherine Helen \"Carrie\"", - "Survived": false, - "Pclass": 3, - "Embarked": "S", - "Sex": "female" - }, - { - "SibSp": 0, - "Ticket": "111369", - "Parch": 0, - "Cabin": "C148", - "PassengerId": 890, - "Age": 26, - "Fare": 30, - "Name": "Behr, Mr. Karl Howell", - "Survived": true, - "Pclass": 1, - "Embarked": "C", - "Sex": "male" - }, - { - "SibSp": 0, - "Ticket": "370376", - "Parch": 0, - "Cabin": null, - "PassengerId": 891, - "Age": 32, - "Fare": 7.75, - "Name": "Dooley, Mr. Patrick", - "Survived": false, - "Pclass": 3, - "Embarked": "Q", - "Sex": "male" - } - ]; diff --git a/src/datascience-ui/history-react/MainPanel.tsx b/src/datascience-ui/history-react/MainPanel.tsx deleted file mode 100644 index 635e84371e42..000000000000 --- a/src/datascience-ui/history-react/MainPanel.tsx +++ /dev/null @@ -1,1085 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { min } from 'lodash'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as React from 'react'; -import * as uuid from 'uuid/v4'; - -import { createDeferred, Deferred } from '../../client/common/utils/async'; -import { noop } from '../../client/common/utils/misc'; -import { CellMatcher } from '../../client/datascience/cellMatcher'; -import { generateMarkdownFromCodeLines } from '../../client/datascience/common'; -import { Identifiers } from '../../client/datascience/constants'; -import { IInteractiveWindowMapping, InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { CellState, ICell, IInteractiveWindowInfo, IJupyterVariable, IJupyterVariablesResponse } from '../../client/datascience/types'; -import { ErrorBoundary } from '../react-common/errorBoundary'; -import { getLocString } from '../react-common/locReactSide'; -import { IMessageHandler, PostOffice } from '../react-common/postOffice'; -import { getSettings, updateSettings } from '../react-common/settingsReactSide'; -import { StyleInjector } from '../react-common/styleInjector'; -import { Cell, ICellViewModel } from './cell'; -import { ContentPanel, IContentPanelProps } from './contentPanel'; -import { InputHistory } from './inputHistory'; -import { IntellisenseProvider } from './intellisenseProvider'; -import { createCellVM, createEditableCellVM, extractInputText, generateTestState, IMainPanelState } from './mainPanelState'; -import { initializeTokenizer, registerMonacoLanguage } from './tokenizer'; -import { IToolbarPanelProps, ToolbarPanel } from './toolbarPanel'; -import { VariableExplorer } from './variableExplorer'; -import { IVariablePanelProps, VariablePanel } from './variablePanel'; - -import './mainPanel.css'; - -export interface IMainPanelProps { - skipDefault?: boolean; - testMode?: boolean; - baseTheme: string; - codeTheme: string; -} - -export class MainPanel extends React.Component implements IMessageHandler { - private stackLimit = 10; - private updateCount = 0; - private renderCount = 0; - private editCellRef: Cell | null = null; - private mainPanel: HTMLDivElement | null = null; - private variableExplorerRef: React.RefObject; - private styleInjectorRef: React.RefObject; - private postOffice: PostOffice = new PostOffice(); - private intellisenseProvider: IntellisenseProvider; - private onigasmPromise: Deferred | undefined; - private tmlangugePromise: Deferred | undefined; - private monacoIdToCellId: Map = new Map(); - - // 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 : [], - submittedText: false, - history: new InputHistory(), - editCellVM: getSettings && getSettings().allowInput ? createEditableCellVM(1) : undefined, - editorOptions: this.computeEditorOptions(), - currentExecutionCount: 0, - debugging: false - }; - - // Add test state if necessary - if (!this.props.skipDefault) { - this.state = generateTestState(this.inputBlockToggled); - } - - // Create the ref to hold our variable explorer - this.variableExplorerRef = React.createRef(); - - // Create the ref to hold our style injector - this.styleInjectorRef = React.createRef(); - - // Setup the completion provider for monaco. We only need one - this.intellisenseProvider = new IntellisenseProvider(this.postOffice, this.getCellId); - - // Setup the tokenizer for monaco if running inside of vscode - if (this.props.skipDefault) { - if (this.props.testMode) { - // Running a test, skip the tokenizer. We want the UI to display synchronously - this.state = {tokenizerLoaded: true, ...this.state}; - - // However we still need to register python as a language - registerMonacoLanguage(); - } else { - initializeTokenizer(this.loadOnigasm, this.loadTmlanguage, this.tokenizerLoaded).ignoreErrors(); - } - } - } - - public componentWillMount() { - // Add ourselves as a handler for the post office - this.postOffice.addHandler(this); - - // Tell the interactive window code we have started. - this.postOffice.sendMessage(InteractiveWindowMessages.Started); - } - - public componentDidUpdate(_prevProps: Readonly, _prevState: Readonly, _snapshot?: {}) { - // If in test mode, update our outputs - if (this.props.testMode) { - this.updateCount = this.updateCount + 1; - } - } - - public componentWillUnmount() { - // Remove ourselves as a handler for the post office - this.postOffice.removeHandler(this); - - // Get rid of our completion provider - this.intellisenseProvider.dispose(); - - // Get rid of our post office - this.postOffice.dispose(); - } - - public render() { - - // If in test mode, update our outputs - if (this.props.testMode) { - this.renderCount = this.renderCount + 1; - } - - const baseTheme = this.computeBaseTheme(); - - return ( -
- -
- {this.renderToolbarPanel(baseTheme)} -
-
- {this.renderVariablePanel(baseTheme)} -
-
- {this.renderContentPanel(baseTheme)} -
- -
- ); - } - - // tslint:disable-next-line:no-any cyclomatic-complexity - public handleMessage = (msg: string, payload?: any) => { - switch (msg) { - case InteractiveWindowMessages.StartCell: - this.startCell(payload); - return true; - - case InteractiveWindowMessages.FinishCell: - this.finishCell(payload); - return true; - - case InteractiveWindowMessages.UpdateCell: - this.updateCell(payload); - return true; - - case InteractiveWindowMessages.GetAllCells: - this.getAllCells(); - return true; - - case InteractiveWindowMessages.ExpandAll: - this.expandAllSilent(); - return true; - - case InteractiveWindowMessages.CollapseAll: - this.collapseAllSilent(); - return true; - - case InteractiveWindowMessages.DeleteAllCells: - this.clearAllSilent(); - return true; - - case InteractiveWindowMessages.Redo: - this.redo(); - return true; - - case InteractiveWindowMessages.Undo: - this.undo(); - return true; - - case InteractiveWindowMessages.StartProgress: - if (!this.props.testMode) { - this.setState({busy: true}); - } - break; - - case InteractiveWindowMessages.StopProgress: - if (!this.props.testMode) { - this.setState({busy: false}); - } - break; - - case InteractiveWindowMessages.UpdateSettings: - this.updateSettings(payload); - break; - - case InteractiveWindowMessages.Activate: - this.activate(); - break; - - case InteractiveWindowMessages.GetVariablesResponse: - this.getVariablesResponse(payload); - break; - - case InteractiveWindowMessages.GetVariableValueResponse: - this.getVariableValueResponse(payload); - break; - - case InteractiveWindowMessages.LoadOnigasmAssemblyResponse: - this.handleOnigasmResponse(payload); - break; - - case InteractiveWindowMessages.LoadTmLanguageResponse: - this.handleTmLanguageResponse(payload); - break; - - case InteractiveWindowMessages.RestartKernel: - // this should be the response from a restart. - this.setState({currentExecutionCount: 0}); - if (this.variableExplorerRef.current && this.variableExplorerRef.current.state.open) { - this.refreshVariables(); - } - break; - - case InteractiveWindowMessages.StartDebugging: - this.setState({debugging: true}); - break; - - case InteractiveWindowMessages.StopDebugging: - this.setState({debugging: false}); - break; - - default: - break; - } - - return false; - } - - // Uncomment this to use for debugging messages. Add a call to this to stick in dummy sys info messages. - // private addDebugMessageCell(message: string) { - // const cell: ICell = { - // id: '0', - // file: '', - // line: 0, - // state: CellState.finished, - // data: { - // cell_type: 'sys_info', - // version: '0.0.0.0', - // notebook_version: '0', - // path: '', - // message: message, - // connection: '', - // source: '', - // metadata: {} - // } - // }; - // this.addCell(cell); - // } - - private renderToolbarPanel(baseTheme: string) { - const toolbarProps = this.getToolbarProps(baseTheme); - return ; - } - - private renderVariablePanel(baseTheme: string) { - const variableProps = this.getVariableProps(baseTheme); - return ; - } - - private renderContentPanel(baseTheme: string) { - // Skip if the tokenizer isn't finished yet. It needs - // to finish loading so our code editors work. - if (!this.state.tokenizerLoaded && !this.props.testMode) { - return null; - } - - // Otherwise render our cells. - const contentProps = this.getContentProps(baseTheme); - return ; - } - - private renderFooterPanel(baseTheme: string) { - // Skip if the tokenizer isn't finished yet. It needs - // to finish loading so our code editors work. - // We also skip rendering if we're in debug mode (for now). We can't run other cells when debugging - if (!this.state.tokenizerLoaded || !this.state.editCellVM || this.state.debugging) { - return null; - } - - const maxOutputSize = getSettings().maxOutputSize; - const maxTextSize = maxOutputSize && maxOutputSize < 10000 && maxOutputSize > 0 ? maxOutputSize : undefined; - const executionCount = this.getInputExecutionCount(); - - return ( -
- - - -
- ); - } - - private computeEditorOptions() : monacoEditor.editor.IEditorOptions { - const intellisenseOptions = getSettings().intellisenseOptions; - const extraSettings = getSettings().extraSettings; - if (intellisenseOptions && extraSettings) { - return { - quickSuggestions: { - other: intellisenseOptions.quickSuggestions.other, - comments: intellisenseOptions.quickSuggestions.comments, - strings: intellisenseOptions.quickSuggestions.strings - }, - acceptSuggestionOnEnter: intellisenseOptions.acceptSuggestionOnEnter, - quickSuggestionsDelay: intellisenseOptions.quickSuggestionsDelay, - suggestOnTriggerCharacters: intellisenseOptions.suggestOnTriggerCharacters, - tabCompletion: intellisenseOptions.tabCompletion, - suggest: { - localityBonus: intellisenseOptions.suggestLocalityBonus - }, - suggestSelection: intellisenseOptions.suggestSelection, - wordBasedSuggestions: intellisenseOptions.wordBasedSuggestions, - parameterHints: { - enabled: intellisenseOptions.parameterHintsEnabled - }, - cursorStyle: extraSettings.editorCursor, - cursorBlinking: extraSettings.editorCursorBlink - }; - } - - return {}; - } - - private darkChanged = (newDark: boolean) => { - // update our base theme if allowed. Don't do this - // during testing as it will mess up the expected render count. - if (!this.props.testMode) { - this.setState( - { - forceDark: newDark - } - ); - } - } - - private monacoThemeChanged = (theme: string) => { - // update our base theme if allowed. Don't do this - // during testing as it will mess up the expected render count. - if (!this.props.testMode) { - this.setState( - { - monacoTheme: theme - } - ); - } - } - - private computeBaseTheme(): string { - // If we're ignoring, always light - if (getSettings && getSettings().ignoreVscodeTheme) { - return 'vscode-light'; - } - - // Otherwise see if the style injector has figured out - // the theme is dark or not - if (this.state.forceDark !== undefined) { - return this.state.forceDark ? 'vscode-dark' : 'vscode-light'; - } - - return this.props.baseTheme; - } - - private showPlot = (imageHtml: string) => { - this.sendMessage(InteractiveWindowMessages.ShowPlot, imageHtml); - } - - private getContentProps = (baseTheme: string): IContentPanelProps => { - return { - editorOptions: this.state.editorOptions, - baseTheme: baseTheme, - cellVMs: this.state.cellVMs, - history: this.state.history, - testMode: this.props.testMode, - codeTheme: this.props.codeTheme, - submittedText: this.state.submittedText, - gotoCellCode: this.gotoCellCode, - copyCellCode: this.copyCellCode, - deleteCell: this.deleteCell, - skipNextScroll: this.state.skipNextScroll ? true : false, - monacoTheme: this.state.monacoTheme, - onCodeCreated: this.readOnlyCodeCreated, - onCodeChange: this.codeChange, - openLink: this.openLink, - expandImage: this.showPlot - }; - } - private getToolbarProps = (baseTheme: string): IToolbarPanelProps => { - return { - addMarkdown: this.addMarkdown, - collapseAll: this.collapseAll, - expandAll: this.expandAll, - export: this.export, - restartKernel: this.restartKernel, - interruptKernel: this.interruptKernel, - undo: this.undo, - redo: this.redo, - clearAll: this.clearAll, - skipDefault: this.props.skipDefault, - canCollapseAll: this.canCollapseAll(), - canExpandAll: this.canExpandAll(), - canExport: this.canExport(), - canUndo: this.canUndo(), - canRedo: this.canRedo(), - baseTheme: baseTheme - }; - } - - private getVariableProps = (baseTheme: string): IVariablePanelProps => { - return { - debugging: this.state.debugging, - busy: this.state.busy, - showDataExplorer: this.showDataViewer, - skipDefault: this.props.skipDefault, - testMode: this.props.testMode, - variableExplorerRef: this.variableExplorerRef, - refreshVariables: this.refreshVariables, - variableExplorerToggled: this.variableExplorerToggled, - baseTheme: baseTheme - }; - } - - private activate() { - // Make sure the input cell gets focus - if (getSettings && getSettings().allowInput) { - // Delay this so that we make sure the outer frame has focus first. - setTimeout(() => { - // First we have to give ourselves focus (so that focus actually ends up in the code cell) - if (this.mainPanel) { - this.mainPanel.focus({preventScroll: true}); - } - - if (this.editCellRef) { - this.editCellRef.giveFocus(); - } - }, 100); - } - } - - // tslint:disable-next-line:no-any - private updateSettings = (payload?: any) => { - if (payload) { - const prevShowInputs = getSettings().showCellInputCode; - updateSettings(payload as string); - - // If our settings change updated show inputs we need to fix up our cells - const showInputs = getSettings().showCellInputCode; - - // Also save the editor options. Intellisense options may have changed. - this.setState({ - editorOptions: this.computeEditorOptions() - }); - - if (prevShowInputs !== showInputs) { - this.toggleCellInputVisibility(showInputs, getSettings().collapseCellInputCodeByDefault); - } - } - } - - private showDataViewer = (targetVariable: string, numberOfColumns: number) => { - this.sendMessage(InteractiveWindowMessages.ShowDataViewer, { variableName: targetVariable, columnSize: numberOfColumns }); - } - - private sendMessage(type: T, payload?: M[T]) { - this.postOffice.sendMessage(type, payload); - } - - private openLink = (uri: monacoEditor.Uri) => { - this.sendMessage(InteractiveWindowMessages.OpenLink, uri.toString()); - } - - private getAllCells = () => { - // Send all of our cells back to the other side - const cells = this.state.cellVMs.map((cellVM : ICellViewModel) => { - return cellVM.cell; - }); - - this.sendMessage(InteractiveWindowMessages.ReturnAllCells, cells); - } - - private saveEditCellRef = (ref: Cell | null) => { - this.editCellRef = ref; - } - - 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 getNonEditCellVMs() : ICellViewModel [] { - return this.state.cellVMs.filter(c => !c.editable); - } - - private canCollapseAll = () => { - return this.getNonEditCellVMs().length > 0; - } - - private canExpandAll = () => { - return this.getNonEditCellVMs().length > 0; - } - - private canExport = () => { - return this.getNonEditCellVMs().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 - this.sendMessage(InteractiveWindowMessages.GotoCodeCell, { file : cellVM.cell.file, line: cellVM.cell.line }); - } - - private copyCellCode = (index: number) => { - // Find our cell - const cellVM = this.state.cellVMs[index]; - - // Send a message to the other side to jump to a particular cell - this.sendMessage(InteractiveWindowMessages.CopyCodeCell, { source: extractInputText(cellVM.cell, getSettings()) }); - } - - private deleteCell = (index: number) => { - this.sendMessage(InteractiveWindowMessages.DeleteCell); - const cellVM = this.state.cellVMs[index]; - if (cellVM) { - this.sendMessage(InteractiveWindowMessages.RemoveCell, {id: cellVM.cell.id}); - } - - // 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 collapseAll = () => { - this.sendMessage(InteractiveWindowMessages.CollapseAll); - this.collapseAllSilent(); - } - - private expandAll = () => { - this.sendMessage(InteractiveWindowMessages.ExpandAll); - this.expandAllSilent(); - } - - private clearAll = () => { - this.sendMessage(InteractiveWindowMessages.DeleteAllCells); - 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); - this.sendMessage(InteractiveWindowMessages.Redo); - 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); - this.sendMessage(InteractiveWindowMessages.Undo); - 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 - this.sendMessage(InteractiveWindowMessages.RestartKernel); - } - - private interruptKernel = () => { - // Send a message to the other side to restart the kernel - this.sendMessage(InteractiveWindowMessages.Interrupt); - } - - 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; }); - this.sendMessage(InteractiveWindowMessages.Export, cellContents); - } - - private updateSelf = (r: HTMLDivElement) => { - this.mainPanel = r; - } - - // tslint:disable-next-line:no-any - private addCell = (payload?: any) => { - // Get our settings for if we should display input code and if we should collapse by default - const showInputs = getSettings().showCellInputCode; - const collapseInputs = getSettings().collapseCellInputCodeByDefault; - - if (payload) { - const cell = payload as ICell; - let cellVM: ICellViewModel = createCellVM(cell, getSettings(), this.inputBlockToggled); - - // Set initial cell visibility and collapse - cellVM = this.alterCellVM(cellVM, showInputs, !collapseInputs); - - if (cellVM) { - const newList = [...this.state.cellVMs, cellVM]; - this.setState({ - cellVMs: newList, - 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 getEditCell() : ICellViewModel | undefined { - return this.state.editCellVM; - } - - 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.alterCellVM(targetCellVM, true, !targetCellVM.inputBlockOpen); - - this.setState({ - skipNextScroll: true, - cellVMs: cellVMArray - }); - } - } - - private toggleCellInputVisibility = (visible: boolean, collapse: boolean) => { - this.alterAllCellVMs(visible, !collapse); - } - - private collapseAllSilent = () => { - if (getSettings().showCellInputCode) { - this.alterAllCellVMs(true, false); - } - } - - private expandAllSilent = () => { - if (getSettings().showCellInputCode) { - this.alterAllCellVMs(true, true); - } - } - - private alterAllCellVMs = (visible: boolean, expanded: boolean) => { - const newCells = this.state.cellVMs.map((value: ICellViewModel) => { - return this.alterCellVM(value, visible, expanded); - }); - - this.setState({ - skipNextScroll: true, - cellVMs: newCells - }); - } - - // Adjust the visibility or collapsed state of a cell - private alterCellVM = (cellVM: ICellViewModel, visible: boolean, expanded: boolean) => { - if (cellVM.cell.data.cell_type === 'code') { - // If we are already in the correct state, return back our initial cell vm - if (cellVM.inputBlockShow === visible && cellVM.inputBlockOpen === expanded) { - return cellVM; - } - - const newCellVM = {...cellVM}; - if (cellVM.inputBlockShow !== visible) { - if (visible) { - // Show the cell, the rest of the function will add on correct collapse state - newCellVM.inputBlockShow = true; - } else { - // Hide this cell - newCellVM.inputBlockShow = false; - } - } - - // No elseif as we want newly visible cells to pick up the correct expand / collapse state - if (cellVM.inputBlockOpen !== expanded && cellVM.inputBlockCollapseNeeded && cellVM.inputBlockShow) { - if (expanded) { - // Expand the cell - const newText = extractInputText(cellVM.cell, getSettings()); - - newCellVM.inputBlockOpen = true; - newCellVM.inputBlockText = newText; - } else { - // Collapse the cell - let newText = extractInputText(cellVM.cell, getSettings()); - if (newText.length > 0) { - newText = newText.split('\n', 1)[0]; - newText = newText.slice(0, 255); // Slice to limit length, slicing past length is fine - newText = newText.concat('...'); - } - - newCellVM.inputBlockOpen = false; - newCellVM.inputBlockText = newText; - } - } - - return newCellVM; - } - - return cellVM; - } - - private sendInfo = () => { - const info : IInteractiveWindowInfo = { - cellCount: this.getNonEditCellVMs().length, - undoCount: this.state.undoStack.length, - redoCount: this.state.redoStack.length - }; - this.sendMessage(InteractiveWindowMessages.SendInfo, info); - } - - private updateOrAdd = (cell: ICell, allowAdd? : boolean) => { - const index = this.state.cellVMs.findIndex((c : ICellViewModel) => { - return c.cell.id === cell.id && - c.cell.line === cell.line && - c.cell.file === cell.file; - }); - if (index >= 0) { - // Update this cell - this.state.cellVMs[index].cell = cell; - - // This means the cell existed already so it was actual executed code. - // Use its execution count to update our execution count. - const newExecutionCount = cell.data.execution_count ? - Math.max(this.state.currentExecutionCount, parseInt(cell.data.execution_count.toString(), 10)) : - this.state.currentExecutionCount; - if (newExecutionCount !== this.state.currentExecutionCount) { - this.setState({ currentExecutionCount: newExecutionCount }); - - // We also need to update our variable explorer when the execution count changes - // Use the ref here to maintain var explorer independence - if (this.variableExplorerRef.current && this.variableExplorerRef.current.state.open) { - this.refreshVariables(); - } - } else { - // Force an update anyway as we did change something - 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.testMode || cell.data.cell_type !== 'messages'; - } - - // 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 startCell = (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); - } - } - } - - private getInputExecutionCount = () : number => { - return this.state.currentExecutionCount + 1; - } - - private submitInput = (code: string) => { - // This should be from our last entry. Switch this entry to read only, and add a new item to our list - let editCell = this.getEditCell(); - if (editCell) { - // Change this editable cell to not editable. - editCell.cell.state = CellState.executing; - editCell.cell.data.source = code; - - // Change type to markdown if necessary - const split = code.splitLines({trim: false}); - const firstLine = split[0]; - const matcher = new CellMatcher(getSettings()); - if (matcher.isMarkdown(firstLine)) { - editCell.cell.data.cell_type = 'markdown'; - editCell.cell.data.source = generateMarkdownFromCodeLines(split); - editCell.cell.state = CellState.finished; - } - - // Update input controls (always show expanded since we just edited it.) - editCell = createCellVM(editCell.cell, getSettings(), this.inputBlockToggled); - const collapseInputs = getSettings().collapseCellInputCodeByDefault; - editCell = this.alterCellVM(editCell, true, !collapseInputs); - - // Generate a new id (as the edit cell always has the same one) - editCell.cell.id = uuid(); - - // Indicate this is direct input so that we don't hide it if the user has - // hide all inputs turned on. - editCell.directInput = true; - - // Stick in a new cell at the bottom that's editable and update our state - // so that the last cell becomes busy - this.setState({ - cellVMs: [...this.state.cellVMs, editCell], - editCellVM: createEditableCellVM(this.getInputExecutionCount()), - undoStack : this.pushStack(this.state.undoStack, this.state.cellVMs), - redoStack: this.state.redoStack, - skipNextScroll: false, - submittedText: true - }); - - // Send a message to execute this code if necessary. - if (editCell.cell.state !== CellState.finished) { - this.sendMessage(InteractiveWindowMessages.SubmitNewCell, { code, id: editCell.cell.id }); - } - } - } - - private variableExplorerToggled = (open: boolean) => { - this.sendMessage(InteractiveWindowMessages.VariableExplorerToggle, open); - } - - // When the variable explorer wants to refresh state (say if it was expanded) - private refreshVariables = () => { - this.sendMessage(InteractiveWindowMessages.GetVariablesRequest, this.state.currentExecutionCount); - } - - // Find the display value for one specific variable - private refreshVariable = (targetVar: IJupyterVariable) => { - this.sendMessage(InteractiveWindowMessages.GetVariableValueRequest, targetVar); - } - - // When we get a variable value back use the ref to pass to the variable explorer - // tslint:disable-next-line:no-any - private getVariableValueResponse = (payload?: any) => { - if (payload) { - const variable = payload as IJupyterVariable; - - // Only send the updated variable data if we are on the same execution count as when we requsted it - if (variable && variable.executionCount !== undefined && variable.executionCount === this.state.currentExecutionCount) { - if (this.variableExplorerRef.current) { - this.variableExplorerRef.current.newVariableData(variable); - } - } - } - } - - // When we get our new set of variables back use the ref to pass to the variable explorer - // tslint:disable-next-line:no-any - private getVariablesResponse = (payload?: any) => { - if (payload) { - const variablesResponse = payload as IJupyterVariablesResponse; - - // Check to see if we have moved to a new execution count only send our update if we are on the same count as the request - if (variablesResponse.executionCount === this.state.currentExecutionCount) { - if (this.variableExplorerRef.current) { - this.variableExplorerRef.current.newVariablesData(variablesResponse.variables); - } - - // Now put out a request for all of the sub values for the variables - variablesResponse.variables.forEach(this.refreshVariable); - } - } - } - - private codeChange = (changes: monacoEditor.editor.IModelContentChange[], id: string, modelId: string) => { - // If the model id doesn't match, skip sending this edit. This happens - // when a cell is reused after deleting another - const expectedCellId = this.monacoIdToCellId.get(modelId); - if (expectedCellId !== id) { - // A cell has been reused. Update our mapping - this.monacoIdToCellId.set(modelId, id); - } else { - // Just a normal edit. Pass this onto the completion provider running in the extension - this.sendMessage(InteractiveWindowMessages.EditCell, { changes, id }); - } - } - - private readOnlyCodeCreated = (_text: string, file: string, id: string, monacoId: string) => { - const cell = this.state.cellVMs.find(c => c.cell.id === id); - if (cell) { - // Pass this onto the completion provider running in the extension - this.sendMessage(InteractiveWindowMessages.AddCell, { - fullText: extractInputText(cell.cell, getSettings()), - currentText: cell.inputBlockText, - file, - id - }); - } - - // Save in our map of monaco id to cell id - this.monacoIdToCellId.set(monacoId, id); - } - - private editableCodeCreated = (_text: string, _file: string, id: string, monacoId: string) => { - // Save in our map of monaco id to cell id - this.monacoIdToCellId.set(monacoId, id); - } - - private getCellId = (monacoId: string) : string => { - const result = this.monacoIdToCellId.get(monacoId); - if (result) { - return result; - } - - // Just assume it's the edit cell if not found. - return Identifiers.EditCellId; - } - - // tslint:disable-next-line: no-any - private tokenizerLoaded = (_e?: any) => { - this.setState({ tokenizerLoaded: true }); - } - - private loadOnigasm = () : Promise => { - if (!this.onigasmPromise) { - this.onigasmPromise = createDeferred(); - // Send our load onigasm request - this.sendMessage(InteractiveWindowMessages.LoadOnigasmAssemblyRequest); - } - return this.onigasmPromise.promise; - } - - private loadTmlanguage = () : Promise => { - if (!this.tmlangugePromise) { - this.tmlangugePromise = createDeferred(); - // Send our load onigasm request - this.sendMessage(InteractiveWindowMessages.LoadTmLanguageRequest); - } - return this.tmlangugePromise.promise; - } - - // tslint:disable-next-line: no-any - private handleOnigasmResponse(payload: any) { - if (payload && this.onigasmPromise) { - const typedArray = new Uint8Array(payload.data); - this.onigasmPromise.resolve(typedArray.buffer); - } else if (this.onigasmPromise) { - this.onigasmPromise.resolve(undefined); - } - } - - // tslint:disable-next-line: no-any - private handleTmLanguageResponse(payload: any) { - if (payload && this.tmlangugePromise) { - this.tmlangugePromise.resolve(payload.toString()); - } else if (this.tmlangugePromise) { - this.tmlangugePromise.resolve(undefined); - } - } -} diff --git a/src/datascience-ui/history-react/cell.css b/src/datascience-ui/history-react/cell.css deleted file mode 100644 index c8fae107218b..000000000000 --- a/src/datascience-ui/history-react/cell.css +++ /dev/null @@ -1,143 +0,0 @@ -.cell-wrapper { - margin: 0px; - padding: 2px; - display: block; -} - -.cell-wrapper-preview { - background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); -} - -.cell-wrapper-noneditable { - border-bottom-color: var(--override-widget-background, var(--vscode-editorGroupHeader-tabsBackground)); - border-bottom-style: solid; - border-bottom-width: 1px; -} - -.cell-wrapper:after { - content: ""; - clear: both; - display: block; -} - -.cell-outer { - display:grid; - grid-template-columns: auto 1fr; - grid-column-gap: 3px; - width: 100%; -} - -.cell-outer-editable { - display:grid; - grid-template-columns: auto 1fr; - grid-column-gap: 3px; - width: 100%; - margin-top:16px; -} - -.content-div { - grid-column: 2; - width: 100%; -} - -.controls-div { - grid-column: 1; - grid-template-columns: auto; - display: grid; -} - -.cell-result-container { - width: 100%; -} - -.cell-input { - margin: 0; -} - -.cell-input pre{ - margin: 0px; - padding: 0px; -} - -.cell-output { - margin-top: 5px; - background: var(--override-widget-background, var(--vscode-notifications-background)); -} - -.cell-output-text { - white-space: pre-wrap; - font-size: var(--code-font-size); - font-family: var(--code-font-family); -} -.cell-output-text pre { - white-space: pre-wrap; - font-size: var(--code-font-size); - font-family: var(--code-font-family); -} - -.cell-output-html { - white-space: unset; - position: relative; -} - -.cell-output table { - background-color: transparent; - border: none; - border-collapse: collapse; - border-spacing: 0px; - font-size: 12px; - table-layout: fixed; -} - -.cell-output thead { - border-bottom-color: var(--override-foreground, var(--vscode-editor-foreground)); - border-bottom-style: solid; - border-bottom-width: 1px; - vertical-align: bottom; -} - -.cell-output tr, -.cell-output th, -.cell-output td { - text-align: right; - vertical-align: middle; - padding: 0.5em 0.5em; - line-height: normal; - white-space: normal; - max-width: none; - border: none; -} -.cell-output th { - font-weight: bold; -} -.cell-output tbody tr:nth-child(even) { - background: var(--override-background, var(--vscode-editor-background)); /* Force to white because the default color for output is gray */ -} -.cell-output tbody tr:hover { - background: var(--override-selection-background, var(--vscode-editor-selectionBackground)); -} -.cell-output * + table { - margin-top: 1em; -} - -.center-img { - display: block; - margin: 0 auto; -} - -.cell-output-html .plot-open-button { - z-index: 10; - position: absolute; - left: 2px; - visibility: hidden; -} - -.cell-output-html:hover .plot-open-button { - visibility: visible; -} - -.cell-output-error { - background: var(--override-background, var(--vscode-editor-background)); -} - - diff --git a/src/datascience-ui/history-react/cell.tsx b/src/datascience-ui/history-react/cell.tsx deleted file mode 100644 index 6dbf3fc34c5a..000000000000 --- a/src/datascience-ui/history-react/cell.tsx +++ /dev/null @@ -1,568 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { nbformat } from '@jupyterlab/coreutils'; -import { JSONObject } from '@phosphor/coreutils'; -import ansiToHtml from 'ansi-to-html'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as React from 'react'; -// tslint:disable-next-line:match-default-export-name import-name -import JSONTree from 'react-json-tree'; - -import '../../client/common/extensions'; -import { concatMultilineString, formatStreamText } from '../../client/datascience/common'; -import { Identifiers, RegExpValues } from '../../client/datascience/constants'; -import { CellState, ICell } from '../../client/datascience/types'; -import { noop } from '../../test/core'; -import { Image, ImageName } from '../react-common/image'; -import { ImageButton } from '../react-common/imageButton'; -import { getLocString } from '../react-common/locReactSide'; -import { getSettings } from '../react-common/settingsReactSide'; -import { Code } from './code'; -import { CollapseButton } from './collapseButton'; -import { ExecutionCount } from './executionCount'; -import { InformationMessages } from './informationMessages'; -import { InputHistory } from './inputHistory'; -import { MenuBar } from './menuBar'; -import { displayOrder, richestMimetype, transforms } from './transforms'; - -import './cell.css'; - -interface ICellProps { - role?: string; - cellVM: ICellViewModel; - baseTheme: string; - codeTheme: string; - testMode?: boolean; - autoFocus: boolean; - maxTextSize?: number; - history: InputHistory | undefined; - showWatermark: boolean; - monacoTheme: string | undefined; - editorOptions: monacoEditor.editor.IEditorOptions; - editExecutionCount: number; - gotoCode(): void; - copyCode(): void; - delete(): void; - submitNewCode(code: string): void; - onCodeChange(changes: monacoEditor.editor.IModelContentChange[], cellId: string, modelId: string): void; - onCodeCreated(code: string, file: string, cellId: string, modelId: string): void; - openLink(uri: monacoEditor.Uri): void; - expandImage(imageHtml: string): void; -} - -export interface ICellViewModel { - cell: ICell; - inputBlockShow: boolean; - inputBlockOpen: boolean; - inputBlockText: string; - inputBlockCollapseNeeded: boolean; - editable: boolean; - directInput?: boolean; - inputBlockToggled(id: string): void; -} - -export class Cell extends React.Component { - private code: Code | undefined; - - constructor(prop: ICellProps) { - super(prop); - this.state = {focused: this.props.autoFocus}; - } - - private static getAnsiToHtmlOptions() : { fg: string; bg: string; colors: string [] } { - // Here's the default colors for ansiToHtml. We need to use the - // colors from our current theme. - // const colors = { - // 0: '#000', - // 1: '#A00', - // 2: '#0A0', - // 3: '#A50', - // 4: '#00A', - // 5: '#A0A', - // 6: '#0AA', - // 7: '#AAA', - // 8: '#555', - // 9: '#F55', - // 10: '#5F5', - // 11: '#FF5', - // 12: '#55F', - // 13: '#F5F', - // 14: '#5FF', - // 15: '#FFF' - // }; - return { - fg: 'var(--vscode-terminal-foreground)', - bg: 'var(--vscode-terminal-background)', - colors: [ - 'var(--vscode-terminal-ansiBlack)', // 0 - 'var(--vscode-terminal-ansiBrightRed)', // 1 - 'var(--vscode-terminal-ansiGreen)', // 2 - 'var(--vscode-terminal-ansiYellow)', // 3 - 'var(--vscode-terminal-ansiBrightBlue)', // 4 - 'var(--vscode-terminal-ansiMagenta)', // 5 - 'var(--vscode-terminal-ansiCyan)', // 6 - 'var(--vscode-terminal-ansiBrightBlack)', // 7 - 'var(--vscode-terminal-ansiWhite)', // 8 - 'var(--vscode-terminal-ansiRed)', // 9 - 'var(--vscode-terminal-ansiBrightGreen)', // 10 - 'var(--vscode-terminal-ansiBrightYellow)', // 11 - 'var(--vscode-terminal-ansiBlue)', // 12 - 'var(--vscode-terminal-ansiBrightMagenta)', // 13 - 'var(--vscode-terminal-ansiBrightCyan)', // 14 - 'var(--vscode-terminal-ansiBrightWhite)' // 15 - ] - }; - } - public render() { - if (this.props.cellVM.cell.data.cell_type === 'messages') { - return ; - } else { - return this.renderNormalCell(); - } - } - - public giveFocus() { - if (this.code) { - this.code.giveFocus(); - } - } - - // 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 getCopyBackToSourceString = () => { - return getLocString('DataScience.copyBackToSourceButtonTooltip', 'Paste code into file'); - } - - 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() { - const hasNoSource = this.props.cellVM.cell.file === Identifiers.EmptyFileName; - const results: JSX.Element[] = this.renderResults(); - const allowsPlainInput = getSettings().showCellInputCode || this.props.cellVM.directInput || this.props.cellVM.editable; - const shouldRender = allowsPlainInput || (results && results.length > 0); - const cellOuterClass = this.props.cellVM.editable ? 'cell-outer-editable' : 'cell-outer'; - let cellWrapperClass = this.props.cellVM.editable ? 'cell-wrapper' : 'cell-wrapper cell-wrapper-noneditable'; - if (this.props.cellVM.cell.type === 'preview') { - cellWrapperClass += ' cell-wrapper-preview'; - } - - // Only render if we are allowed to. - if (shouldRender) { - return ( -
- - - - - -
- {this.renderControls()} -
-
- {this.renderInputs()} - {this.renderResultsDiv(results)} -
-
-
-
- ); - } - - // Shouldn't be rendered because not allowing empty input and not a direct input cell - return null; - } - - private onMouseClick = (ev: React.MouseEvent) => { - // When we receive a click, tell the code element. - if (this.code) { - this.code.onParentClick(ev); - } - } - - private showInputs = () : boolean => { - return (this.isCodeCell() && (this.props.cellVM.inputBlockShow || this.props.cellVM.editable)); - } - - private getRenderableInputCode = () : string => { - if (this.props.cellVM.editable) { - return ''; - } - - return this.props.cellVM.inputBlockText; - } - - private renderControls = () => { - const busy = this.props.cellVM.cell.state === CellState.init || this.props.cellVM.cell.state === CellState.executing; - const collapseVisible = (this.props.cellVM.inputBlockCollapseNeeded && this.props.cellVM.inputBlockShow && !this.props.cellVM.editable); - const executionCount = this.props.cellVM && this.props.cellVM.cell && this.props.cellVM.cell.data && this.props.cellVM.cell.data.execution_count ? - this.props.cellVM.cell.data.execution_count.toString() : '-'; - - // Only code cells have controls. Markdown should be empty - if (this.isCodeCell()) { - - return this.props.cellVM.editable ? - ( -
- -
- ) : ( -
- - -
- ); - } else { - return null; - } - } - - private updateCodeRef = (ref: Code) => { - this.code = ref; - } - - private renderInputs = () => { - if (this.showInputs()) { - const backgroundColor = this.props.cellVM.cell.type === 'preview' ? - 'var(--override-peek-background, var(--vscode-peekViewEditor-background))' - : undefined; - - return ( -
- -
- ); - } else { - return null; - } - } - - private onCodeChange = (changes: monacoEditor.editor.IModelContentChange[], modelId: string) => { - this.props.onCodeChange(changes, this.props.cellVM.cell.id, modelId); - } - - private onCodeCreated = (code: string, modelId: string) => { - this.props.onCodeCreated(code, this.props.cellVM.cell.file, this.props.cellVM.cell.id, modelId); - } - - private renderResultsDiv = (results: JSX.Element[]) => { - - // Only render results if the user can't edit. For now. Might allow editing of code later? - if (!this.props.cellVM.editable) { - const outputClassNames = this.isCodeCell() ? - `cell-output cell-output-${this.props.baseTheme}` : - ''; - - // Then combine them inside a div - return
{results}
; - } - return null; - } - - private renderResults = (): JSX.Element[] => { - // Results depend upon the type of cell - return this.isCodeCell() ? - this.renderCodeOutputs() : - this.renderMarkdown(this.getMarkdownCell()); - } - - 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); - }); - } - - return []; - } - - 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 []; - } - - private renderWithTransform = (mimetype: string, output : nbformat.IOutput, index : number, renderWithScrollbars: boolean, isText: boolean, isError: boolean) => { - - // 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
{this.getUnknownMimeTypeFormatString().format(mimetype)}
; - } - - try { - // Massage our data to make sure it displays well - if (output.data) { - let extraButton = null; - const mimeBundle = output.data as nbformat.IMimeBundle; - let data: nbformat.MultilineString | JSONObject = mimeBundle[mimetype]; - switch (mimetype) { - case 'text/plain': - // Data needs to be contiguous for us to display it. - data = concatMultilineString(data as nbformat.MultilineString); - renderWithScrollbars = true; - isText = true; - break; - - case 'image/svg+xml': - // Jupyter adds a universal selector style that messes - // up all of our other styles. Remove it. - const html = concatMultilineString(data as nbformat.MultilineString); - data = html.replace(RegExpValues.StyleTagRegex, ''); - - // Also change the width to 100% so it scales correctly. We need to save the - // width/height for the plot window though - let sizeTag = ''; - const widthMatch = RegExpValues.SvgWidthRegex.exec(data); - const heightMatch = RegExpValues.SvgHeightRegex.exec(data); - if (widthMatch && heightMatch && widthMatch.length > 2 && heightMatch.length > 2) { - // SvgHeightRegex and SvgWidthRegex match both the - - - -
- ); - break; - - default: - break; - } - - // Create a default set of properties - const style: React.CSSProperties = { - }; - - // Create a scrollbar style if necessary - if (renderWithScrollbars && this.props.maxTextSize) { - style.overflowX = 'auto'; - style.overflowY = 'auto'; - style.maxHeight = `${this.props.maxTextSize}px`; - } - - let className = isText ? 'cell-output-text' : 'cell-output-html'; - className = isError ? `${className} cell-output-error` : className; - - return ( -
- {extraButton} - -
- ); - } - } catch (ex) { - window.console.log('Error in rendering'); - window.console.log(ex); - return
; - } - } - - return
; - } - - private doubleClick = (event: React.MouseEvent) => { - // Extract the svg image from whatever was clicked - // tslint:disable-next-line: no-any - const svgChild = event.target as any; - if (svgChild && svgChild.ownerSVGElement) { - const svg = svgChild.ownerSVGElement as SVGElement; - this.props.expandImage(svg.outerHTML); - } - } - - private plotOpenClick = (event?: React.MouseEvent) => { - const divChild = event && event.currentTarget; - if (divChild && divChild.parentElement && divChild.parentElement.parentElement) { - const svgs = divChild.parentElement.parentElement.getElementsByTagName('svg'); - if (svgs && svgs.length > 1) { // First svg should be the button itself. See the code above where we bind to this function. - this.props.expandImage(svgs[1].outerHTML); - } - } - } - - private click = (event: React.MouseEvent) => { - // If this is an anchor element, forward the click as Jupyter does. - let anchor = event.target as HTMLAnchorElement; - if (anchor && anchor.href) { - // Href may be redirected to an inner anchor - if (anchor.href.startsWith('vscode')) { - const inner = anchor.getElementsByTagName('a'); - if (inner && inner.length > 0) { - anchor = inner[0]; - } - } - if (anchor && anchor.href && !anchor.href.startsWith('vscode')) { - this.props.openLink(monacoEditor.Uri.parse(anchor.href)); - } - } - } - - // tslint:disable-next-line: max-func-body-length - 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
; - } - - // 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.hasOwnProperty('application/json')) { - return ; - } - - // Only for text and error ouptut do we add scrollbars - let addScrollbars = false; - let isText = false; - let isError = false; - - // Stream and error output need to be converted - if (copy.output_type === 'stream') { - addScrollbars = true; - isText = true; - - // 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' : `${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(Cell.getAnsiToHtmlOptions()); - const html = converter.toHtml(formatted); - copy.data = { - 'text/html': html - }; - } - } catch { - noop(); - } - - } else if (copy.output_type === 'error') { - addScrollbars = true; - isText = true; - isError = true; - const error = copy as nbformat.IError; - try { - const converter = new ansiToHtml(Cell.getAnsiToHtmlOptions()); - 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 - let mimetype: string = richestMimetype(copy.data, displayOrder, transforms); - - // If that worked, use the transform - if (mimetype) { - return this.renderWithTransform(mimetype, copy, index, addScrollbars, isText, isError); - } - - if (copy.data) { - const keys = Object.keys(copy.data); - mimetype = keys.length > 0 ? keys[0] : 'unknown'; - } else { - mimetype = 'unknown'; - } - const str : string = this.getUnknownMimeTypeFormatString().format(mimetype); - return <div key={index}>{str}</div>; - } -} diff --git a/src/datascience-ui/history-react/code.css b/src/datascience-ui/history-react/code.css deleted file mode 100644 index d45022eaf89d..000000000000 --- a/src/datascience-ui/history-react/code.css +++ /dev/null @@ -1,21 +0,0 @@ - -.code-area { - position: relative; - width:100%; - margin-bottom:16px; - top: -2px; /* Account for spacing removed from the monaco editor */ -} - -.code-area-editable { - margin-bottom: 10px; -} - -.code-watermark { - position: absolute; - top: 3px; - left: 30px; - z-index: 500; - font-style: italic; - color: var(--override-watermark-color, var(--vscode-panelTitle-inactiveForeground)); -} - diff --git a/src/datascience-ui/history-react/code.tsx b/src/datascience-ui/history-react/code.tsx deleted file mode 100644 index 8cb117a06ef1..000000000000 --- a/src/datascience-ui/history-react/code.tsx +++ /dev/null @@ -1,246 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as React from 'react'; - -import { getLocString } from '../react-common/locReactSide'; -import { MonacoEditor } from '../react-common/monacoEditor'; -import { InputHistory } from './inputHistory'; - -import './code.css'; - -export interface ICodeProps { - autoFocus: boolean; - code : string; - codeTheme: string; - testMode: boolean; - readOnly: boolean; - history: InputHistory | undefined; - showWatermark: boolean; - monacoTheme: string | undefined; - outermostParentClass: string; - editorOptions: monacoEditor.editor.IEditorOptions; - forceBackgroundColor?: string; - onSubmit(code: string): void; - onCreated(code: string, modelId: string): void; - onChange(changes: monacoEditor.editor.IModelContentChange[], modelId: string): void; - openLink(uri: monacoEditor.Uri): void; -} - -interface ICodeState { - focused: boolean; - cursorLeft: number; - cursorTop: number; - cursorBottom: number; - charUnderCursor: string; - allowWatermark: boolean; - editor: monacoEditor.editor.IStandaloneCodeEditor | undefined; - model: monacoEditor.editor.ITextModel | null; -} - -export class Code extends React.Component<ICodeProps, ICodeState> { - private subscriptions: monacoEditor.IDisposable[] = []; - private lastCleanVersionId: number = 0; - private editorRef: React.RefObject<MonacoEditor> = React.createRef<MonacoEditor>(); - - constructor(prop: ICodeProps) { - super(prop); - this.state = {focused: false, cursorLeft: 0, cursorTop: 0, cursorBottom: 0, charUnderCursor: '', allowWatermark: true, editor: undefined, model: null}; - } - - public componentWillUnmount = () => { - this.subscriptions.forEach(d => d.dispose()); - } - - public render() { - const readOnly = this.props.readOnly; - const waterMarkClass = this.props.showWatermark && this.state.allowWatermark && !readOnly ? 'code-watermark' : 'hide'; - const classes = readOnly ? 'code-area' : 'code-area code-area-editable'; - const options: monacoEditor.editor.IEditorConstructionOptions = { - minimap: { - enabled: false - }, - glyphMargin: false, - wordWrap: 'on', - scrollBeyondLastLine: false, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden' - }, - lineNumbers: 'off', - renderLineHighlight: 'none', - highlightActiveIndentGuide: false, - autoIndent: true, - autoClosingBrackets: this.props.testMode ? 'never' : 'languageDefined', - autoClosingQuotes: this.props.testMode ? 'never' : 'languageDefined', - renderIndentGuides: false, - overviewRulerBorder: false, - overviewRulerLanes: 0, - hideCursorInOverviewRuler: true, - folding: false, - readOnly: readOnly, - lineDecorationsWidth: 0, - contextmenu: false, - matchBrackets: false, - ...this.props.editorOptions - }; - - return ( - <div className={classes}> - <MonacoEditor - testMode={this.props.testMode} - value={this.props.code} - outermostParentClass={this.props.outermostParentClass} - theme={this.props.monacoTheme ? this.props.monacoTheme : 'vs'} - language='python' - editorMounted={this.editorDidMount} - options={options} - openLink={this.props.openLink} - ref={this.editorRef} - forceBackground={this.props.forceBackgroundColor} - /> - <div className={waterMarkClass}>{this.getWatermarkString()}</div> - </div> - ); - } - - public onParentClick(ev: React.MouseEvent<HTMLDivElement>) { - const readOnly = this.props.testMode || this.props.readOnly; - if (this.state.editor && !readOnly) { - ev.stopPropagation(); - this.state.editor.focus(); - } - } - - public giveFocus() { - const readOnly = this.props.testMode || this.props.readOnly; - if (this.state.editor && !readOnly) { - this.state.editor.focus(); - } - } - - private getWatermarkString = () : string => { - return getLocString('DataScience.inputWatermark', 'Shift-enter to run'); - } - - private editorDidMount = (editor: monacoEditor.editor.IStandaloneCodeEditor) => { - // Update our state - const model = editor.getModel(); - this.setState({ editor, model: editor.getModel() }); - - // Listen for model changes - this.subscriptions.push(editor.onDidChangeModelContent(this.modelChanged)); - - // List for key up/down events if not read only - if (!this.props.readOnly) { - this.subscriptions.push(editor.onKeyDown(this.onKeyDown)); - this.subscriptions.push(editor.onKeyUp(this.onKeyUp)); - } - - // Indicate we're ready - this.props.onCreated(this.props.code, model!.id); - } - - private modelChanged = (e: monacoEditor.editor.IModelContentChangedEvent) => { - if (this.state.model) { - this.props.onChange(e.changes, this.state.model.id); - } - if (!this.props.readOnly) { - this.setState({allowWatermark: false}); - } - } - - private onKeyDown = (e: monacoEditor.IKeyboardEvent) => { - if (e.shiftKey && e.keyCode === monacoEditor.KeyCode.Enter && this.state.model && this.state.editor) { - // Shift enter was hit - e.stopPropagation(); - e.preventDefault(); - window.setTimeout(this.submitContent, 0); - } else if (e.keyCode === monacoEditor.KeyCode.UpArrow) { - this.arrowUp(e); - } else if (e.keyCode === monacoEditor.KeyCode.DownArrow) { - this.arrowDown(e); - } - } - - private onKeyUp = (e: monacoEditor.IKeyboardEvent) => { - if (e.shiftKey && e.keyCode === monacoEditor.KeyCode.Enter) { - // Shift enter was hit - e.stopPropagation(); - e.preventDefault(); - } - } - - private submitContent = () => { - let content = this.getContents(); - if (content) { - // Remove empty lines off the end - let endPos = content.length - 1; - while (endPos >= 0 && content[endPos] === '\n') { - endPos -= 1; - } - content = content.slice(0, endPos + 1); - - // Send to the input history too if necessary - if (this.props.history) { - this.props.history.add(content, this.state.model!.getVersionId() > this.lastCleanVersionId); - } - - // Clear our current contents since we submitted - this.state.model!.setValue(''); - - // Send to jupyter - this.props.onSubmit(content); - } - } - - private getContents() : string { - if (this.state.model) { - return this.state.model.getValue().replace(/\r/g, ''); - } - return ''; - } - - private isAutoCompleteOpen() : boolean { - if (this.editorRef.current) { - return this.editorRef.current.isSuggesting(); - } - return false; - } - - private arrowUp(e: monacoEditor.IKeyboardEvent) { - if (this.state.editor && this.state.model && !this.isAutoCompleteOpen()) { - const cursor = this.state.editor.getPosition(); - if (cursor && cursor.lineNumber === 1 && this.props.history) { - const currentValue = this.getContents(); - const newValue = this.props.history.completeUp(currentValue); - if (newValue !== currentValue) { - this.state.model.setValue(newValue); - this.lastCleanVersionId = this.state.model.getVersionId(); - this.state.editor.setPosition({lineNumber: 1, column: 1}); - e.stopPropagation(); - } - } - } - } - - private arrowDown(e: monacoEditor.IKeyboardEvent) { - if (this.state.editor && this.state.model && !this.isAutoCompleteOpen()) { - const cursor = this.state.editor.getPosition(); - if (cursor && cursor.lineNumber === this.state.model.getLineCount() && this.props.history) { - const currentValue = this.getContents(); - const newValue = this.props.history.completeDown(currentValue); - if (newValue !== currentValue) { - this.state.model.setValue(newValue); - this.lastCleanVersionId = this.state.model.getVersionId(); - const lastLine = this.state.model.getLineCount(); - this.state.editor.setPosition({lineNumber: lastLine, column: this.state.model.getLineLength(lastLine) + 1}); - e.stopPropagation(); - } - } - } - } - -} diff --git a/src/datascience-ui/history-react/collapseButton.css b/src/datascience-ui/history-react/collapseButton.css deleted file mode 100644 index d9a0d59aa363..000000000000 --- a/src/datascience-ui/history-react/collapseButton.css +++ /dev/null @@ -1,33 +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; -} - -.collapse-input { - grid-column: 2; - padding: 2px; - margin-top: 2px; - height: min-content; -} - -.remove-style { - background-color:transparent; - border:transparent; - font:inherit; -} - -.collapseInputLabel { - background-color: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - font-family: var(--vscode-editor-font-family); - font-weight: var(--vscode-editor-font-weight); - margin: 3px; -} diff --git a/src/datascience-ui/history-react/collapseButton.tsx b/src/datascience-ui/history-react/collapseButton.tsx deleted file mode 100644 index 4a585add87c1..000000000000 --- a/src/datascience-ui/history-react/collapseButton.tsx +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as React from 'react'; -import { getLocString } from '../react-common/locReactSide'; -import './collapseButton.css'; - -interface ICollapseButtonProps { - theme: string; - tooltip: string; - visible: boolean; - open: boolean; - label?: string; - onClick(): void; -} - -export class CollapseButton extends React.Component<ICollapseButtonProps> { - constructor(props: ICollapseButtonProps) { - super(props); - } - - public render() { - const collapseInputPolygonClassNames = `collapse-input-svg ${this.props.open ? ' collapse-input-svg-rotate' : ''} collapse-input-svg-${this.props.theme}`; - const collapseInputClassNames = `collapse-input remove-style ${this.props.visible ? '' : ' invisible'}`; - const tooltip = this.props.open ? getLocString('DataScience.collapseSingle', 'Collapse') : getLocString('DataScience.expandSingle', 'Expand'); - const ariaExpanded = this.props.open ? 'true' : 'false'; - // https://reactjs.org/docs/conditional-rendering.html#inline-if-with-logical--operator - // Comment here just because the (boolean && statement) was new to me - return ( - <button className={collapseInputClassNames} title={tooltip} onClick={this.props.onClick} aria-expanded={ariaExpanded}> - <svg version='1.1' baseProfile='full' width='8px' height='11px'> - <polygon points='0,0 0,10 5,5' className={collapseInputPolygonClassNames} fill='black' /> - </svg> - {this.props.label && - <label className='collapseInputLabel'>{this.props.label}</label> - } - </button> - ); - } -} diff --git a/src/datascience-ui/history-react/commandPrompt.css b/src/datascience-ui/history-react/commandPrompt.css deleted file mode 100644 index f0fb122a98c9..000000000000 --- a/src/datascience-ui/history-react/commandPrompt.css +++ /dev/null @@ -1,11 +0,0 @@ -.command-prompt { - background-color: transparent; - border: transparent; - color: var(--code-comment-color); - font-weight: bold; - margin-left: 2px; - margin-right: 2px; - vertical-align: middle; - text-align: center; - font-family: var(--code-font-family); -} diff --git a/src/datascience-ui/history-react/commandPrompt.tsx b/src/datascience-ui/history-react/commandPrompt.tsx deleted file mode 100644 index 45692e32605d..000000000000 --- a/src/datascience-ui/history-react/commandPrompt.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as React from 'react'; -import './commandPrompt.css'; - -export class CommandPrompt extends React.Component { - constructor(props: {}) { - super(props); - } - - public render() { - return <div className='command-prompt'>{'>>>'}</div>; - } - -} diff --git a/src/datascience-ui/history-react/contentPanel.css b/src/datascience-ui/history-react/contentPanel.css deleted file mode 100644 index 1b93b248a7ca..000000000000 --- a/src/datascience-ui/history-react/contentPanel.css +++ /dev/null @@ -1,8 +0,0 @@ -#cell-table { - display: table; - width: 100%; -} - -#cell-table-body { - display: table-row-group; -} \ No newline at end of file diff --git a/src/datascience-ui/history-react/contentPanel.tsx b/src/datascience-ui/history-react/contentPanel.tsx deleted file mode 100644 index 25497edf5112..000000000000 --- a/src/datascience-ui/history-react/contentPanel.tsx +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './contentPanel.css'; - -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as React from 'react'; - -import { noop } from '../../test/core'; -import { ErrorBoundary } from '../react-common/errorBoundary'; -import { getSettings } from '../react-common/settingsReactSide'; -import { Cell, ICellViewModel } from './cell'; -import { InputHistory } from './inputHistory'; - -export interface IContentPanelProps { - baseTheme: string; - cellVMs: ICellViewModel[]; - history: InputHistory; - testMode?: boolean; - codeTheme: string; - submittedText: boolean; - skipNextScroll: boolean; - monacoTheme: string | undefined; - editorOptions: monacoEditor.editor.IEditorOptions; - gotoCellCode(index: number): void; - copyCellCode(index: number): void; - deleteCell(index: number): void; - onCodeChange(changes: monacoEditor.editor.IModelContentChange[], cellId: string, modelId: string): void; - onCodeCreated(code: string, file: string, cellId: string, modelId: string): void; - openLink(uri: monacoEditor.Uri): void; - expandImage(imageHtml: string): void; -} - -export class ContentPanel extends React.Component<IContentPanelProps> { - private bottomRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); - private containerRef: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); - constructor(prop: IContentPanelProps) { - super(prop); - } - - public componentDidMount() { - this.scrollToBottom(); - } - - public componentDidUpdate() { - this.scrollToBottom(); - } - - public render() { - return( - <div id='content-panel-div' ref={this.containerRef}> - <div id='cell-table'> - <div id='cell-table-body' role='list'> - {this.renderCells()} - </div> - </div> - <div ref={this.bottomRef}/> - </div> - ); - } - - private renderCells = () => { - const maxOutputSize = getSettings().maxOutputSize; - const maxTextSize = maxOutputSize && maxOutputSize < 10000 && maxOutputSize > 0 ? maxOutputSize : undefined; - const baseTheme = getSettings().ignoreVscodeTheme ? 'vscode-light' : this.props.baseTheme; - return this.props.cellVMs.map((cellVM: ICellViewModel, index: number) => - <ErrorBoundary key={index}> - <Cell - role='listitem' - editorOptions={this.props.editorOptions} - history={undefined} - maxTextSize={maxTextSize} - autoFocus={false} - testMode={this.props.testMode} - cellVM={cellVM} - submitNewCode={noop} - baseTheme={baseTheme} - codeTheme={this.props.codeTheme} - showWatermark={false} - editExecutionCount={0} - gotoCode={() => this.props.gotoCellCode(index)} - copyCode={() => this.props.copyCellCode(index)} - delete={() => this.props.deleteCell(index)} - onCodeChange={this.props.onCodeChange} - onCodeCreated={this.props.onCodeCreated} - monacoTheme={this.props.monacoTheme} - openLink={this.props.openLink} - expandImage={this.props.expandImage} - /> - </ErrorBoundary> - ); - } - - private scrollToBottom = () => { - if (this.bottomRef.current && !this.props.skipNextScroll && !this.props.testMode && this.containerRef.current) { - // Force auto here as smooth scrolling can be canceled by updates to the window - // from elsewhere (and keeping track of these would make this hard to maintain) - setTimeout(() => { - if (this.bottomRef.current) { - this.bottomRef.current!.scrollIntoView({behavior: 'auto', block: 'start', inline: 'nearest'}); - } - }, 100); - } - } - -} diff --git a/src/datascience-ui/history-react/cursor.css b/src/datascience-ui/history-react/cursor.css deleted file mode 100644 index e1c29ef1f33e..000000000000 --- a/src/datascience-ui/history-react/cursor.css +++ /dev/null @@ -1,64 +0,0 @@ -.cursor-top { - position: absolute; - z-index:1005; - font-family: var(--code-font-family); - pointer-events: none; -} - -.cursor-block { - border: .05px solid var(--override-foreground, var(--vscode-editor-foreground)); - min-width: 5px; - margin-left: -1px; - margin-right: -1px; -} - -.cursor-line { - border-left: 1px solid var(--override-foreground, var(--vscode-editor-foreground)); -} - -.cursor-underline { - border-bottom: 1px solid var(--override-foreground, var(--vscode-editor-foreground)); - min-width: 5px; -} - -.cursor-measure { - visibility: hidden; -} - -.cursor-line-overlay { - border-left-width: 1px; - border-left-style: solid; - border-left-color: transparent; - min-width: 5px; - animation: blinkCursorLine 750ms infinite; -} - -.cursor-underline-overlay { - border-bottom-width: 1px; - border-bottom-style: solid; - border-bottom-color: transparent; - min-width: 5px; - animation: blinkCursorUnderline 750ms infinite; -} - -.cursor-block-overlay { - font-family: var(--code-font-family); - background-color: transparent; - color: transparent; - animation: blinkCursorBlock 750ms infinite; -} - -@keyframes blinkCursorLine { - 0%, 49% {border-left-color: transparent;} - 50%, 100% {border-left-color: var(--override-foreground, var(--vscode-editor-foreground));} -} - -@keyframes blinkCursorUnderline { - 0%, 49% {border-bottom-color: transparent;} - 50%, 100% {border-bottom-color: var(--override-foreground, var(--vscode-editor-foreground)); } -} - -@keyframes blinkCursorBlock { - 0%, 49% {background-color: transparent; color: transparent;} - 50%, 100% {background-color: var(--override-foreground, var(--vscode-editor-foreground)); color: var(--override-background, var(--vscode-editor-background));} -} diff --git a/src/datascience-ui/history-react/cursor.tsx b/src/datascience-ui/history-react/cursor.tsx deleted file mode 100644 index 9615a270969f..000000000000 --- a/src/datascience-ui/history-react/cursor.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as React from 'react'; -import './cursor.css'; - -export interface ICursorProps { - codeInFocus: boolean; - hidden: boolean; - left: number; - top: number; - bottom: number; - text: string; - cursorType: string; -} - -export class Cursor extends React.Component<ICursorProps> { - - constructor(props: ICursorProps) { - super(props); - } - - public render() { - const style : React.CSSProperties = this.props.bottom > 0 ? { - left : `${this.props.left}px`, - top: `${this.props.top}px`, - height: `${this.props.bottom - this.props.top}px` - } : { - left : `${this.props.left}px`, - top: `${this.props.top}px` - }; - - if (this.props.hidden) { - return null; - } else if (this.props.codeInFocus) { - return this.renderInFocus(style); - } else { - return this.renderOutOfFocus(style); - } - } - - private getRenderText() : string { - // Verify that we have some non-whitespace letter. slice(0,1) is legal on empty string - let renderText = this.props.text.slice(0, 1).trim(); - if (renderText.length === 0) { - renderText = 'A'; - } - - return renderText; - } - - private renderInFocus = (style: React.CSSProperties) => { - const cursorClass = `cursor-top cursor-${this.props.cursorType}-overlay`; - const textClass = this.props.cursorType !== 'block' || this.props.text.slice(0, 1).trim().length === 0 ? 'cursor-measure' : 'cursor-text'; - return <div className={cursorClass} style={style}><div className={textClass}>{this.getRenderText()}</div></div>; - } - - private renderOutOfFocus = (style: React.CSSProperties) => { - const cursorClass = `cursor-top cursor-${this.props.cursorType}`; - return <div className={cursorClass} style={style}><div className='cursor-measure'>{this.getRenderText()}</div></div>; - } -} diff --git a/src/datascience-ui/history-react/executionCount.css b/src/datascience-ui/history-react/executionCount.css deleted file mode 100644 index 861c10dd984c..000000000000 --- a/src/datascience-ui/history-react/executionCount.css +++ /dev/null @@ -1,41 +0,0 @@ -.execution-count { - grid-column: 1; - font-weight: bold; - display:flex; - color: var(--code-comment-color); - font-family: var(--code-font-family); - } - - .execution-count-busy-outer { - grid-column: 1; - font-weight: bold; - color: var(--code-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(--code-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 38b6b939713e..000000000000 --- a/src/datascience-ui/history-react/executionCount.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 './executionCount.css'; - -interface IExecutionCountProps { - isBusy: boolean; - count: string; - visible: boolean; -} - -export class ExecutionCount extends React.Component<IExecutionCountProps> { - constructor(props: IExecutionCountProps) { - super(props); - } - - public render() { - if (this.props.visible) { - - return this.props.isBusy ? - ( - <div className='execution-count-busy-outer'>[<svg className='execution-count-busy-svg' viewBox='0 0 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.count}]`}</div> - ); - } else { - return null; - } - } - -} 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/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 9e9b10e5a0f5..000000000000 --- a/src/datascience-ui/history-react/index.html +++ /dev/null @@ -1,355 +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; - } - function getInitialSettings() { - return { allowInput: true, - showJupyterVariableExplorer: true, - showCellInputCode: true, - extraSettings: { editorCursor: 'block', editorCursorBlink: 'blink'} - }; - } - </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 ad5a2230c1cc..000000000000 --- a/src/datascience-ui/history-react/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './index.css'; - -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -import { Identifiers } from '../../client/datascience/constants'; -import { IVsCodeApi } from '../react-common/postOffice'; -import { detectBaseTheme } from '../react-common/themeDetector'; -import { MainPanel } from './MainPanel'; - -// This special function talks to vscode from a web panel -export declare function acquireVsCodeApi(): IVsCodeApi; -const baseTheme = detectBaseTheme(); - -// tslint:disable:no-typeof-undefined -ReactDOM.render( - <MainPanel baseTheme={baseTheme} codeTheme={Identifiers.GeneratedThemeName} skipDefault={typeof acquireVsCodeApi !== 'undefined'} />, - document.getElementById('root') as HTMLElement -); diff --git a/src/datascience-ui/history-react/informationMessages.css b/src/datascience-ui/history-react/informationMessages.css deleted file mode 100644 index fdc23937e568..000000000000 --- a/src/datascience-ui/history-react/informationMessages.css +++ /dev/null @@ -1,33 +0,0 @@ -.messages-wrapper { - padding: 12px; - display: block; - border-bottom-color: var(--override-tabs-background, var(--vscode-editorGroupHeader-tabsBackground)); - border-bottom-style: solid; - border-bottom-width: 1px; -} - -.messages-outer { - background: var(--override-widget-background, var(--vscode-notifications-background)); - white-space: pre-wrap; - font-family: monospace; - width: 100%; -} - -.messages-outer-preview { - font-weight: bold; - background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); - font-family: var(--code-font-family); -} - -.messages-wrapper-preview { - background-color: var(--override-peek-background, var(--vscode-peekViewEditor-background)); -} - -.messages-result-container pre { - white-space: pre-wrap; - font-family: monospace; - margin: 0px; -} - - - diff --git a/src/datascience-ui/history-react/informationMessages.tsx b/src/datascience-ui/history-react/informationMessages.tsx deleted file mode 100644 index dbb60733e5d3..000000000000 --- a/src/datascience-ui/history-react/informationMessages.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './informationMessages.css'; - -import * as React from 'react'; - -// tslint:disable-next-line:match-default-export-name import-name -interface IInformationMessagesProps -{ - messages: string[]; - type: 'execute' | 'preview'; -} - -export class InformationMessages extends React.Component<IInformationMessagesProps> { - constructor(prop: IInformationMessagesProps) { - super(prop); - } - - public render() { - const output = this.props.messages.join('\n'); - const wrapperClassName = this.props.type === 'preview' ? 'messages-wrapper messages-wrapper-preview' : 'messages-wrapper'; - const outerClassName = this.props.type === 'preview' ? 'messages-outer messages-outer-preview' : 'messages-outer'; - - return ( - <div className={wrapperClassName}> - <div className={outerClassName}> - <div className='messages-result-container'> - <pre><span>{output}</span></pre> - </div> - </div> - </div> - ); - } -} diff --git a/src/datascience-ui/history-react/inputHistory.ts b/src/datascience-ui/history-react/inputHistory.ts deleted file mode 100644 index 766e84137b6a..000000000000 --- a/src/datascience-ui/history-react/inputHistory.ts +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -export class InputHistory { - - private historyStack: string [] = []; - private up: number | undefined; - private down: number | undefined; - private last: number | undefined; - - public completeUp(code: string) : string { - // If going up, only move if anything in the history - if (this.historyStack.length > 0) { - if (this.up === undefined) { - this.up = 0; - } - - const result = this.up < this.historyStack.length ? this.historyStack[this.up] : code; - this.adjustCursors(this.up); - return result; - } - - return code; - } - - public completeDown(code: string) : string { - // If going down, move and then return something if we have a position - if (this.historyStack.length > 0 && this.down !== undefined) { - const result = this.historyStack[this.down]; - this.adjustCursors(this.down); - return result; - } - - return code; - } - - public add(code: string, typed: boolean) { - // Compute our new history. Behavior depends upon if the user typed it in or - // just used the arrows - - // Only skip adding a dupe if it's the same as the top item. Otherwise - // add it as normal. - this.historyStack = this.last === 0 && this.historyStack.length > 0 && this.historyStack[this.last] === code ? - this.historyStack : [code, ...this.historyStack]; - - // Position is more complicated. If we typed something start over - if (typed) { - this.reset(); - } else { - // We want our next up push to match the index of the item that was - // actually entered. - if (this.last === 0) { - this.up = undefined; - this.down = undefined; - } else if (this.last) { - this.up = this.last + 1; - this.down = this.last - 1; - } - } - } - - private reset() { - this.up = undefined; - this.down = undefined; - } - - private adjustCursors(currentPos: number) { - // Save last position we entered. - this.last = currentPos; - - // For a single item, ony up works. But never modify it. - if (this.historyStack.length > 1) { - if (currentPos < this.historyStack.length) { - this.up = currentPos + 1; - } else { - this.up = this.historyStack.length; - - // If we go off the end, don't make the down go up to the last. - // CMD prompt behaves this way. Down is always one off. - currentPos = this.historyStack.length - 1; - } - if (currentPos > 0) { - this.down = currentPos - 1; - } else { - this.down = undefined; - } - } - } -} diff --git a/src/datascience-ui/history-react/intellisenseProvider.ts b/src/datascience-ui/history-react/intellisenseProvider.ts deleted file mode 100644 index f7778ddeca7c..000000000000 --- a/src/datascience-ui/history-react/intellisenseProvider.ts +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as uuid from 'uuid/v4'; -import { IDisposable } from '../../client/common/types'; -import { createDeferred, Deferred } from '../../client/common/utils/async'; -import { - IInteractiveWindowMapping, - InteractiveWindowMessages, - IProvideCompletionItemsResponse, - IProvideHoverResponse, - IProvideSignatureHelpResponse -} from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IMessageHandler, PostOffice } from '../react-common/postOffice'; - -interface IRequestData<T> { - promise: Deferred<T>; - cancelDisposable: monacoEditor.IDisposable; -} - -export class IntellisenseProvider implements monacoEditor.languages.CompletionItemProvider, monacoEditor.languages.HoverProvider, monacoEditor.languages.SignatureHelpProvider, IDisposable, IMessageHandler { - public triggerCharacters?: string[] | undefined = ['.']; - public readonly signatureHelpTriggerCharacters?: ReadonlyArray<string> = ['(', ',', '<']; - public readonly signatureHelpRetriggerCharacters?: ReadonlyArray<string> = [')']; - private completionRequests: Map<string, IRequestData<monacoEditor.languages.CompletionList>> = new Map<string, IRequestData<monacoEditor.languages.CompletionList>>(); - private hoverRequests: Map<string, IRequestData<monacoEditor.languages.Hover>> = new Map<string, IRequestData<monacoEditor.languages.Hover>>(); - private signatureHelpRequests: Map<string, IRequestData<monacoEditor.languages.SignatureHelp>> = new Map<string, IRequestData<monacoEditor.languages.SignatureHelp>>(); - private registerDisposables: monacoEditor.IDisposable[] = []; - constructor(private postOffice: PostOffice, private getCellId: (modelId: string) => string) { - // Register a completion provider - this.registerDisposables.push(monacoEditor.languages.registerCompletionItemProvider('python', this)); - this.registerDisposables.push(monacoEditor.languages.registerHoverProvider('python', this)); - this.registerDisposables.push(monacoEditor.languages.registerSignatureHelpProvider('python', this)); - this.postOffice.addHandler(this); - } - - public provideCompletionItems( - model: monacoEditor.editor.ITextModel, - position: monacoEditor.Position, - context: monacoEditor.languages.CompletionContext, - token: monacoEditor.CancellationToken): monacoEditor.languages.ProviderResult<monacoEditor.languages.CompletionList> { - - // Emit a new request - const requestId = uuid(); - const promise = createDeferred<monacoEditor.languages.CompletionList>(); - - const cancelDisposable = token.onCancellationRequested(() => { - promise.resolve(); - this.sendMessage(InteractiveWindowMessages.CancelCompletionItemsRequest, { requestId }); - }); - - this.completionRequests.set(requestId, { promise, cancelDisposable }); - this.sendMessage(InteractiveWindowMessages.ProvideCompletionItemsRequest, { position, context, requestId, cellId: this.getCellId(model.id) }); - - return promise.promise; - } - - public provideHover( - model: monacoEditor.editor.ITextModel, - position: monacoEditor.Position, - token: monacoEditor.CancellationToken) : monacoEditor.languages.ProviderResult<monacoEditor.languages.Hover> { - // Emit a new request - const requestId = uuid(); - const promise = createDeferred<monacoEditor.languages.Hover>(); - - const cancelDisposable = token.onCancellationRequested(() => { - promise.resolve(); - this.sendMessage(InteractiveWindowMessages.CancelCompletionItemsRequest, { requestId }); - }); - - this.hoverRequests.set(requestId, { promise, cancelDisposable }); - this.sendMessage(InteractiveWindowMessages.ProvideHoverRequest, { position, requestId, cellId: this.getCellId(model.id) }); - - return promise.promise; - } - - public provideSignatureHelp( - model: monacoEditor.editor.ITextModel, - position: monacoEditor.Position, - token: monacoEditor.CancellationToken, - context: monacoEditor.languages.SignatureHelpContext): monacoEditor.languages.ProviderResult<monacoEditor.languages.SignatureHelp> { - // Emit a new request - const requestId = uuid(); - const promise = createDeferred<monacoEditor.languages.SignatureHelp>(); - - const cancelDisposable = token.onCancellationRequested(() => { - promise.resolve(); - this.sendMessage(InteractiveWindowMessages.CancelSignatureHelpRequest, { requestId }); - }); - - this.signatureHelpRequests.set(requestId, { promise, cancelDisposable }); - this.sendMessage(InteractiveWindowMessages.ProvideSignatureHelpRequest, { position, context, requestId, cellId: this.getCellId(model.id) }); - - return promise.promise; - } - - public dispose() { - this.registerDisposables.forEach(r => r.dispose()); - this.completionRequests.forEach(r => r.promise.resolve()); - this.hoverRequests.forEach(r => r.promise.resolve()); - - this.registerDisposables = []; - this.completionRequests.clear(); - this.hoverRequests.clear(); - - this.postOffice.removeHandler(this); - } - - // tslint:disable-next-line: no-any - public handleMessage(type: string, payload?: any): boolean { - switch (type) { - case InteractiveWindowMessages.ProvideCompletionItemsResponse: - this.handleCompletionResponse(payload); - return true; - - case InteractiveWindowMessages.ProvideHoverResponse: - this.handleHoverResponse(payload); - return true; - - case InteractiveWindowMessages.ProvideSignatureHelpResponse: - this.handleSignatureHelpResponse(payload); - return true; - - default: - break; - } - - return false; - } - - // Handle completion response - // tslint:disable-next-line:no-any - private handleCompletionResponse = (payload?: any) => { - if (payload) { - const response = payload as IProvideCompletionItemsResponse; - - // Resolve our waiting promise if we have one - const waiting = this.completionRequests.get(response.requestId); - if (waiting) { - waiting.promise.resolve(response.list); - } - } - } - // Handle hover response - // tslint:disable-next-line:no-any - private handleHoverResponse = (payload?: any) => { - if (payload) { - const response = payload as IProvideHoverResponse; - - // Resolve our waiting promise if we have one - const waiting = this.hoverRequests.get(response.requestId); - if (waiting) { - waiting.promise.resolve(response.hover); - } - } - } - - // Handle hover response - // tslint:disable-next-line:no-any - private handleSignatureHelpResponse = (payload?: any) => { - if (payload) { - const response = payload as IProvideSignatureHelpResponse; - - // Resolve our waiting promise if we have one - const waiting = this.signatureHelpRequests.get(response.requestId); - if (waiting) { - waiting.promise.resolve(response.signatureHelp); - } - } - } - - private sendMessage<M extends IInteractiveWindowMapping, T extends keyof M>(type: T, payload?: M[T]) { - this.postOffice.sendMessage<M, T>(type, payload); - } -} diff --git a/src/datascience-ui/history-react/mainPanel.css b/src/datascience-ui/history-react/mainPanel.css deleted file mode 100644 index 6657611f76dc..000000000000 --- a/src/datascience-ui/history-react/mainPanel.css +++ /dev/null @@ -1,63 +0,0 @@ -body, html { - height: 100%; - margin: 0; -} - -#root { - height: 100%; - overflow: hidden; -} - -#main-panel { - background: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - display: grid; - grid-template-rows: auto auto 1fr auto; - grid-template-columns: 1fr; - grid-template-areas: - "toolbar" - "variable" - "content" - "footer"; - height: 100%; - width: 100%; - position: absolute; - overflow: hidden; -} - -#main-panel-toolbar { - grid-area: toolbar; - justify-self: end; - overflow: hidden; -} - -#main-panel-variable { - grid-area: variable; - overflow: auto; -} -#main-panel-content { - grid-area: content; - max-height: 100%; - overflow: auto; -} -#main-panel-footer { - grid-area: footer; - overflow: hidden; -} - -.hide { - display: none; -} - -.invisible { - visibility: hidden; -} - -.edit-panel { - min-height:50px; - padding: 10px 0px 10px 0px; - width: 100%; - border-top-color: var(--override-widget-background, var(--vscode-editorGroupHeader-tabsBackground)); - border-top-style: solid; - border-top-width: 1px; -} \ No newline at end of file diff --git a/src/datascience-ui/history-react/mainPanelState.ts b/src/datascience-ui/history-react/mainPanelState.ts deleted file mode 100644 index 9ffcbee7b3ac..000000000000 --- a/src/datascience-ui/history-react/mainPanelState.ts +++ /dev/null @@ -1,437 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as path from 'path'; - -import { IDataScienceSettings } from '../../client/common/types'; -import { CellMatcher } from '../../client/datascience/cellMatcher'; -import { concatMultilineString } from '../../client/datascience/common'; -import { Identifiers } from '../../client/datascience/constants'; -import { CellState, ICell, IMessageCell } from '../../client/datascience/types'; -import { noop } from '../../test/core'; -import { ICellViewModel } from './cell'; -import { InputHistory } from './inputHistory'; - -export interface IMainPanelState { - cellVMs: ICellViewModel[]; - editCellVM?: ICellViewModel; - busy: boolean; - skipNextScroll? : boolean; - undoStack : ICellViewModel[][]; - redoStack : ICellViewModel[][]; - submittedText: boolean; - history: InputHistory; - rootStyle?: string; - theme?: string; - forceDark?: boolean; - monacoTheme?: string; - tokenizerLoaded?: boolean; - editorOptions: monacoEditor.editor.IEditorOptions; - currentExecutionCount: number; - debugging: boolean; -} - -// tslint:disable-next-line: no-multiline-string -const darkStyle = ` - :root { - --code-comment-color: #6A9955; - --code-numeric-color: #b5cea8; - --code-string-color: #ce9178; - --code-variable-color: #9CDCFE; - --code-type-color: #4EC9B0; - --code-font-family: Consolas, 'Courier New', monospace; - --code-font-size: 14px; - } -`; - -// This function generates test state when running under a browser instead of inside of -export function generateTestState(inputBlockToggled : (id: string) => void, filePath: string = '') : IMainPanelState { - return { - cellVMs : generateVMs(inputBlockToggled, filePath), - editCellVM: createEditableCellVM(1), - busy: true, - skipNextScroll : false, - undoStack : [], - redoStack : [], - submittedText: false, - history: new InputHistory(), - rootStyle: darkStyle, - tokenizerLoaded: true, - editorOptions: {}, - currentExecutionCount: 0, - debugging: false - }; -} - -export function createEditableCellVM(executionCount: number) : ICellViewModel { - return { - cell: - { - data: - { - cell_type: 'code', // We should eventually allow this to change to entering of markdown? - execution_count: executionCount, - metadata: {}, - outputs: [], - source: '' - }, - id: Identifiers.EditCellId, - file: Identifiers.EmptyFileName, - line: 0, - state: CellState.editing, - type: 'execute' - }, - editable: true, - inputBlockOpen: true, - inputBlockShow: true, - inputBlockText: '', - inputBlockCollapseNeeded: false, - inputBlockToggled: noop - }; -} - -export function extractInputText(inputCell: ICell, settings: IDataScienceSettings | undefined) : string { - let source = inputCell.data.cell_type === 'code' ? inputCell.data.source : []; - const matcher = new CellMatcher(settings); - - // Eliminate the #%% on the front if it has nothing else on the line - if (source.length > 0) { - const title = matcher.exec(source[0].trim()); - if (title !== undefined && title.length <= 0) { - source = source.slice(1); - } - } - - return concatMultilineString(source); -} - -export function createCellVM(inputCell: ICell, settings: IDataScienceSettings | undefined, inputBlockToggled : (id: string) => void) : ICellViewModel { - let inputLinesCount = 0; - const inputText = inputCell.data.cell_type === 'code' ? extractInputText(inputCell, settings) : ''; - if (inputText) { - inputLinesCount = inputText.split('\n').length; - } - - return { - cell: inputCell, - editable: false, - inputBlockOpen: true, - inputBlockShow: 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, undefined, inputBlockToggled); - }); -} - -function generateCells(filePath: string) : ICell[] { - const cellData = generateCellData(); - return cellData.map((data : nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | IMessageCell, key : number) => { - return { - id : key.toString(), - file : path.join(filePath, 'foo.py'), - line : 1, - state: key === cellData.length - 1 ? CellState.executing : CellState.finished, - type: key === 3 ? 'preview' : 'execute', - data : data - }; - }); -} - -//tslint:disable:max-func-body-length -function generateCellData() : (nbformat.ICodeCell | nbformat.IMarkdownCell | nbformat.IRawCell | IMessageCell)[] { - - // Hopefully new entries here can just be copied out of a jupyter notebook (ipynb) - return [ - { - // These are special. Sys_info is our own custom cell - cell_type: 'messages', - messages: [ - 'You have this python data:', - 'c:\\data\\python.exe', - '3.9.9.9 The Uber Version', - '(5, 9, 9)', - 'https:\\localhost\\token?=9343p0843084039483084308430984038403840938409384098304983094803948093848034809384' - ], - source: [], - metadata: {} - }, - { - cell_type: 'code', - execution_count: 467, - metadata: { - slideshow: { - slide_type: '-' - } - }, - outputs: [ - { - data: { -// tslint:disable-next-line: no-multiline-string - 'text/html': [` - <div style=" - overflow: auto; - "> - <style scoped=""> - .dataframe tbody tr th:only-of-type { - vertical-align: middle; - } - .dataframe tbody tr th { - vertical-align: top; - } - .dataframe thead th { - text-align: right; - } - </style> - <table border="1" class="dataframe"> - <thead> - <tr style="text-align: right;"> - <th></th> - <th>0</th> - <th>1</th> - <th>2</th> - <th>3</th> - <th>4</th> - <th>5</th> - <th>6</th> - <th>7</th> - <th>8</th> - <th>9</th> - <th>...</th> - <th>2990</th> - <th>2991</th> - <th>2992</th> - <th>2993</th> - <th>2994</th> - <th>2995</th> - <th>2996</th> - <th>2997</th> - <th>2998</th> - <th>2999</th> - </tr> - <tr> - <th>idx</th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - <th></th> - </tr> - </thead> - <tbody> - <tr> - <th>2007-01-31</th> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>...</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - <td>37.060604</td> - </tr> - <tr> - <th>2007-02-28</th> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>...</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - <td>20.603407</td> - </tr> - <tr> - <th>2007-03-31</th> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>...</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - <td>6.142031</td> - </tr> - <tr> - <th>2007-04-30</th> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>...</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - <td>6.931635</td> - </tr> - <tr> - <th>2007-05-31</th> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>...</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - <td>52.642243</td> - </tr> - </tbody> - </table> - <p>5 rows × 3000 columns</p> - </div>` - ] - }, - execution_count: 4, - metadata: {}, - output_type: 'execute_result' - } - ], - source: [ - '# 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 745e3803e714..000000000000 --- a/src/datascience-ui/history-react/menuBar.css +++ /dev/null @@ -1,7 +0,0 @@ -.menuBar { - margin-top: 2px; - margin-bottom: 2px; - display: flex; - flex-direction: row-reverse; - justify-content: right; -} diff --git a/src/datascience-ui/history-react/menuBar.tsx b/src/datascience-ui/history-react/menuBar.tsx deleted file mode 100644 index ec1635dc3491..000000000000 --- a/src/datascience-ui/history-react/menuBar.tsx +++ /dev/null @@ -1,29 +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 { - baseTheme: 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: IMenuBarProps) { - super(props); - } - - public render() { - return ( - <div className='menuBar'> - <div className='menuBar-childContainer'> - {this.props.children} - </div> - </div> - ); - } -} diff --git a/src/datascience-ui/history-react/tokenizer.ts b/src/datascience-ui/history-react/tokenizer.ts deleted file mode 100644 index f35d39b138a3..000000000000 --- a/src/datascience-ui/history-react/tokenizer.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { wireTmGrammars } from 'monaco-editor-textmate'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import { Registry } from 'monaco-textmate'; -import { loadWASM } from 'onigasm'; -import { PYTHON_LANGUAGE } from '../../client/common/constants'; - -export function registerMonacoLanguage() { - // Tell monaco about our language - monacoEditor.languages.register({ - id: PYTHON_LANGUAGE, - extensions: ['.py'] - }); - - // Setup the configuration so that auto indent and other things work. Onigasm is just going to setup the tokenizer - monacoEditor.languages.setLanguageConfiguration( - PYTHON_LANGUAGE, - { - comments: { - lineComment: '#', - blockComment: ['\'\'\'', '\"\"\"'] - }, - brackets: [ - ['{', '}'], - ['[', ']'], - ['(', ')'] - ], - autoClosingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '"', close: '"', notIn: ['string'] }, - { open: '\'', close: '\'', notIn: ['string', 'comment'] } - ], - surroundingPairs: [ - { open: '{', close: '}' }, - { open: '[', close: ']' }, - { open: '(', close: ')' }, - { open: '"', close: '"' }, - { open: '\'', close: '\'' } - ], - onEnterRules: [ - { - beforeText: new RegExp('^\\s*(?:def|class|for|if|elif|else|while|try|with|finally|except|async).*?:\\s*$'), - action: { indentAction: monacoEditor.languages.IndentAction.Indent } - } - ], - folding: { - offSide: true, - markers: { - start: new RegExp('^\\s*#region\\b'), - end: new RegExp('^\\s*#endregion\\b') - } - } - } - ); -} - -// tslint:disable: no-any -export async function initializeTokenizer( - getOnigasm: () => Promise<ArrayBuffer>, - getTmlanguageJSON: () => Promise<string>, - loadingFinished: (e?: any) => void): Promise<void> { - try { - // Register the language first - registerMonacoLanguage(); - - // Load the web assembly - const blob = await getOnigasm(); - await loadWASM(blob); - - // Setup our registry of different - const registry = new Registry({ - getGrammarDefinition: async (_scopeName) => { - return { - format: 'json', - content: await getTmlanguageJSON() - }; - } - }); - - // map of monaco "language id's" to TextMate scopeNames - const grammars = new Map(); - grammars.set('python', 'source.python'); - - // Wire everything together. - await wireTmGrammars(monacoEditor, registry, grammars); - - // Indicate to the callback that we're done. - loadingFinished(); - } catch (e) { - loadingFinished(e); - } -} diff --git a/src/datascience-ui/history-react/toolbarPanel.tsx b/src/datascience-ui/history-react/toolbarPanel.tsx deleted file mode 100644 index 59ae4178c525..000000000000 --- a/src/datascience-ui/history-react/toolbarPanel.tsx +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './toolbarPanel.css'; - -import * as React from 'react'; - -import { Image, ImageName } from '../react-common/image'; -import { ImageButton } from '../react-common/imageButton'; -import { getLocString } from '../react-common/locReactSide'; -import { getSettings } from '../react-common/settingsReactSide'; -import { MenuBar } from './menuBar'; - -export interface IToolbarPanelProps { - baseTheme: string; - canCollapseAll: boolean; - canExpandAll: boolean; - canExport: boolean; - canUndo: boolean; - canRedo: boolean; - skipDefault?: boolean; - addMarkdown(): void; - collapseAll(): void; - expandAll(): void; - export(): void; - restartKernel(): void; - interruptKernel(): void; - undo(): void; - redo(): void; - clearAll(): void; -} - -export class ToolbarPanel extends React.Component<IToolbarPanelProps> { - constructor(prop: IToolbarPanelProps) { - super(prop); - } - - public render() { - // note to self - tabIndex should not be provided as that's global to the whole page. Instead order of elements matters - return( - <div id='toolbar-panel'> - <MenuBar baseTheme={this.props.baseTheme}> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.clearAll} tooltip={getLocString('DataScience.clearAll', 'Remove All Cells')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Cancel}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.redo} disabled={!this.props.canRedo} tooltip={getLocString('DataScience.redo', 'Redo')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Redo}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.undo} disabled={!this.props.canUndo} tooltip={getLocString('DataScience.undo', 'Undo')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Undo}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.interruptKernel} tooltip={getLocString('DataScience.interruptKernel', 'Interrupt iPython Kernel')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Interrupt}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.restartKernel} tooltip={getLocString('DataScience.restartServer', 'Restart iPython Kernel')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Restart}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.export} disabled={!this.props.canExport} tooltip={getLocString('DataScience.export', 'Export as Jupyter Notebook')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.SaveAs}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.expandAll} disabled={!this.props.canExpandAll} tooltip={getLocString('DataScience.expandAll', 'Expand all cell inputs')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.ExpandAll}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.collapseAll} disabled={!this.props.canCollapseAll} tooltip={getLocString('DataScience.collapseAll', 'Collapse all cell inputs')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.CollapseAll}/> - </ImageButton> - {this.renderExtraButtons()} - </MenuBar> - </div> - ); - } - - private renderExtraButtons = () => { - if (!this.props.skipDefault) { - const baseTheme = getSettings().ignoreVscodeTheme ? 'vscode-light' : this.props.baseTheme; - return <ImageButton baseTheme={baseTheme} onClick={this.props.addMarkdown} tooltip='Add Markdown Test'>M</ImageButton>; - } - - return null; - } -} 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/history-react/variableExplorer.css b/src/datascience-ui/history-react/variableExplorer.css deleted file mode 100644 index 6f7363a8c3ec..000000000000 --- a/src/datascience-ui/history-react/variableExplorer.css +++ /dev/null @@ -1,13 +0,0 @@ -.variable-explorer { - margin: 5px; - background: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); -} - -#variable-explorer-data-grid { - margin: 4px; -} - -.span-debug-message { - margin: 4px; -} diff --git a/src/datascience-ui/history-react/variableExplorer.tsx b/src/datascience-ui/history-react/variableExplorer.tsx deleted file mode 100644 index d0bb37bd5a77..000000000000 --- a/src/datascience-ui/history-react/variableExplorer.tsx +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './variableExplorer.css'; - -import * as React from 'react'; - -import { RegExpValues } from '../../client/datascience/constants'; -import { IJupyterVariable } from '../../client/datascience/types'; -import { getLocString } from '../react-common/locReactSide'; -import { getSettings } from '../react-common/settingsReactSide'; -import { CollapseButton } from './collapseButton'; -import { IButtonCellValue, VariableExplorerButtonCellFormatter } from './variableExplorerButtonCellFormatter'; -import { CellStyle, VariableExplorerCellFormatter } from './variableExplorerCellFormatter'; -import { VariableExplorerEmptyRowsView } from './variableExplorerEmptyRows'; - -import * as AdazzleReactDataGrid from 'react-data-grid'; -import { VariableExplorerHeaderCellFormatter } from './variableExplorerHeaderCellFormatter'; -import { VariableExplorerRowRenderer } from './variableExplorerRowRenderer'; - -import './variableExplorerGrid.less'; - -interface IVariableExplorerProps { - baseTheme: string; - skipDefault?: boolean; - debugging: boolean; - refreshVariables(): void; - showDataExplorer(targetVariable: string, numberOfColumns: number): void; - variableExplorerToggled(open: boolean): void; -} - -interface IVariableExplorerState { - open: boolean; - gridColumns: {key: string; name: string}[]; - gridRows: IGridRow[]; - gridHeight: number; - height: number; - fontSize: number; - sortDirection: string; - sortColumn: string | number; -} - -const defaultColumnProperties = { - filterable: false, - sortable: true, - resizable: true -}; - -// Sanity check on our string comparisons -const MaxStringCompare = 400; - -interface IGridRow { - // tslint:disable-next-line:no-any - name: string; - type: string; - size: string; - value: string | undefined; - buttons: IButtonCellValue; -} - -export class VariableExplorer extends React.Component<IVariableExplorerProps, IVariableExplorerState> { - private divRef: React.RefObject<HTMLDivElement>; - private variableFetchCount: number; - - constructor(prop: IVariableExplorerProps) { - super(prop); - const columns = [ - { - key: 'name', - name: getLocString('DataScience.variableExplorerNameColumn', 'Name'), - type: 'string', - width: 120, - formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.variable} />, - headerRenderer: <VariableExplorerHeaderCellFormatter/> - }, - { - key: 'type', - name: getLocString('DataScience.variableExplorerTypeColumn', 'Type'), - type: 'string', - width: 120, - formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.string} />, - headerRenderer: <VariableExplorerHeaderCellFormatter/> - }, - { - key: 'size', - name: getLocString('DataScience.variableExplorerSizeColumn', 'Count'), - type: 'string', - width: 120, - formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.numeric} />, - headerRenderer: <VariableExplorerHeaderCellFormatter/> - }, - { - key: 'value', - name: getLocString('DataScience.variableExplorerValueColumn', 'Value'), - type: 'string', - width: 300, - formatter: <VariableExplorerCellFormatter cellStyle={CellStyle.string} />, - headerRenderer: <VariableExplorerHeaderCellFormatter/> - }, - { - key: 'buttons', - name: '', - type: 'boolean', - width: 34, - sortable: false, - resizable: false, - formatter: <VariableExplorerButtonCellFormatter showDataExplorer={this.props.showDataExplorer} baseTheme={this.props.baseTheme} /> - } - ]; - this.state = { open: false, - gridColumns: columns, - gridRows: !this.props.skipDefault ? this.generateDummyVariables() : [], - gridHeight: 200, - height: 0, - fontSize: 14, - sortColumn: 'name', - sortDirection: 'NONE'}; - - this.divRef = React.createRef<HTMLDivElement>(); - this.variableFetchCount = 0; - } - - public render() { - if (getSettings && getSettings().showJupyterVariableExplorer) { - const contentClassName = `variable-explorer-content ${this.state.open ? '' : ' hide'}`; - - const fontSizeStyle: React.CSSProperties = { - fontSize: `${this.state.fontSize.toString()}px` - }; - - return( - <div className='variable-explorer' ref={this.divRef} style={fontSizeStyle}> - <CollapseButton theme={this.props.baseTheme} - visible={true} - open={this.state.open} - onClick={this.toggleInputBlock} - tooltip={getLocString('DataScience.collapseVariableExplorerTooltip', 'Collapse variable explorer')} - label={getLocString('DataScience.collapseVariableExplorerLabel', 'Variables')} /> - <div className={contentClassName}> - {this.renderGrid()} - </div> - </div> - ); - } - - return null; - } - - public componentDidMount = () => { - // After mounting, check our computed style to see if the font size is changed - if (this.divRef.current) { - const newFontSize = parseInt(getComputedStyle(this.divRef.current).getPropertyValue('--code-font-size'), 10); - - // Make sure to check for update here so we don't update loop - // tslint:disable-next-line: use-isnan - if (newFontSize && newFontSize !== NaN && this.state.fontSize !== newFontSize) { - this.setState({fontSize: newFontSize}); - } - } - } - - // New variable data passed in via a ref - // Help to keep us independent of main interactive window state if we choose to break out the variable explorer - public newVariablesData(newVariables: IJupyterVariable[]) { - const newGridRows = newVariables.map(newVar => { - return { - buttons: { - name: newVar.name, - supportsDataExplorer: newVar.supportsDataExplorer, - numberOfColumns: this.getColumnCountFromShape(newVar.shape) - }, - name: newVar.name, - type: newVar.type, - size: '', - value: getLocString('DataScience.variableLoadingValue', 'Loading...') - }; - }); - - this.setState({ gridRows: newGridRows}); - this.variableFetchCount = newGridRows.length; - } - - // Update the value of a single variable already in our list - public newVariableData(newVariable: IJupyterVariable) { - const newGridRows = this.state.gridRows.slice(); - for (let i = 0; i < newGridRows.length; i = i + 1) { - if (newGridRows[i].name === newVariable.name) { - - // For object with shape, use that for size - // for object with length use that for size - // If it doesn't have either, then just leave it out - let newSize = ''; - if (newVariable.shape && newVariable.shape !== '') { - newSize = newVariable.shape; - } else if (newVariable.count) { - newSize = newVariable.count.toString(); - } - - // Also use the shape to compute the number of columns. Necessary - // when showing a data viewer - const numberOfColumns = this.getColumnCountFromShape(newVariable.shape); - - const newGridRow: IGridRow = {...newGridRows[i], - buttons: { - ...newGridRows[i].buttons, - numberOfColumns - }, - value: newVariable.value, - size: newSize}; - - newGridRows[i] = newGridRow; - } - } - - // Update that we have retreived a new variable - // When we hit zero we have all the vars and can sort our values - this.variableFetchCount = this.variableFetchCount - 1; - if (this.variableFetchCount === 0) { - this.setState({ gridRows: this.internalSortRows(newGridRows, this.state.sortColumn, this.state.sortDirection) }); - } else { - this.setState({ gridRows: newGridRows }); - } - } - - public toggleInputBlock = () => { - this.setState({open: !this.state.open}); - - // If we toggle open request a data refresh - if (!this.state.open) { - this.props.refreshVariables(); - } - - // Notify of the toggle, reverse it as the state is not updated yet - this.props.variableExplorerToggled(!this.state.open); - } - - public sortRows = (sortColumn: string | number, sortDirection: string) => { - this.setState({ - sortColumn, - sortDirection, - gridRows: this.internalSortRows(this.state.gridRows, sortColumn, sortDirection) - }); - } - - private renderGrid() { - if (this.props.debugging) { - return ( - <span className='span-debug-message'>{getLocString('DataScience.variableExplorerDisabledDuringDebugging', 'Variables are not available while debugging.')}</span> - ); - } else { - return ( - <div id='variable-explorer-data-grid' role='table' aria-label={getLocString('DataScience.collapseVariableExplorerLabel', 'Variables')}> - <AdazzleReactDataGrid - columns = {this.state.gridColumns.map(c => { return {...defaultColumnProperties, ...c }; })} - rowGetter = {this.getRow} - rowsCount = {this.state.gridRows.length} - minHeight = {this.state.gridHeight} - headerRowHeight = {this.state.fontSize + 9} - rowHeight = {this.state.fontSize + 9} - onRowDoubleClick = {this.rowDoubleClick} - onGridSort = {this.sortRows} - emptyRowsView = {VariableExplorerEmptyRowsView} - rowRenderer = {VariableExplorerRowRenderer} - /> - </div> - ); - } - } - - private generateDummyVariables() : IGridRow[] { - return [ - { - name: 'foo', - value: 'bar', - type: 'DataFrame', - size: '(100, 100)', - buttons: { - supportsDataExplorer: true, - name: 'foo', - numberOfColumns: 100 - } - } - ]; - } - - private getColumnType(key: string | number) : string | undefined { - let column; - if (typeof key === 'string') { - //tslint:disable-next-line:no-any - column = this.state.gridColumns.find(c => c.key === key) as any; - } else { - // This is the index lookup - column = this.state.gridColumns[key]; - } - - // Special case our size column, it's displayed as a string - // but we will sort it like a number - if (column && column.key === 'size') { - return 'number'; - } else if (column && column.type) { - return column.type; - } - } - - private getColumnCountFromShape(shape: string | undefined) : number { - if (shape) { - // Try to match on the second value if there is one - const matches = RegExpValues.ShapeSplitterRegEx.exec(shape); - if (matches && matches.length > 1) { - return parseInt(matches[1], 10); - } - } - return 0; - } - - private internalSortRows = (gridRows: IGridRow[], sortColumn: string | number, sortDirection: string): IGridRow[] => { - // Default to the name column - if (sortDirection === 'NONE') { - sortColumn = 'name'; - sortDirection = 'ASC'; - } - - const columnType = this.getColumnType(sortColumn); - const isStringColumn = columnType === 'string' || columnType === 'object'; - const invert = sortDirection !== 'DESC'; - - // Use a special comparer for string columns as we can't compare too much of a string - // or it will take too long - const comparer = isStringColumn ? - //tslint:disable-next-line:no-any - (a: any, b: any): number => { - const aVal = a[sortColumn] as string; - const bVal = b[sortColumn] as string; - const aStr = aVal ? aVal.substring(0, Math.min(aVal.length, MaxStringCompare)).toUpperCase() : aVal; - const bStr = bVal ? bVal.substring(0, Math.min(bVal.length, MaxStringCompare)).toUpperCase() : bVal; - const result = aStr > bStr ? -1 : 1; - return invert ? -1 * result : result; - } : - //tslint:disable-next-line:no-any - (a: any, b: any): number => { - const aVal = this.getComparisonValue(a, sortColumn); - const bVal = this.getComparisonValue(b, sortColumn); - const result = aVal > bVal ? -1 : 1; - return invert ? -1 * result : result; - }; - - return gridRows.sort(comparer); - } - - // Get the numerical comparison value for a column - private getComparisonValue(gridRow: IGridRow, sortColumn: string | number): number { - // tslint:disable-next-line: no-any - return (sortColumn === 'size') ? this.sizeColumnComparisonValue(gridRow) : (gridRow as any)[sortColumn]; - } - - // The size column needs special casing - private sizeColumnComparisonValue(gridRow: IGridRow): number { - const sizeStr: string = gridRow.size as string; - - if (!sizeStr) { - return -1; - } - - let sizeNumber = -1; - const commaIndex = sizeStr.indexOf(','); - // First check the shape case like so (5000,1000) in this case we want the 5000 to compare with - if (sizeStr[0] === '(' && commaIndex > 0) { - sizeNumber = parseInt(sizeStr.substring(1, commaIndex), 10); - } else { - // If not in the shape format, assume a to i conversion - sizeNumber = parseInt(sizeStr, 10); - } - - // If our parse fails we get NaN for any case that like return -1 - return isNaN(sizeNumber) ? -1 : sizeNumber; - } - - private rowDoubleClick = (_rowIndex: number, row: IGridRow) => { - // On row double click, see if data explorer is supported and open it if it is - if (row.buttons && row.buttons.supportsDataExplorer !== undefined - && row.buttons.name && row.buttons.supportsDataExplorer) { - this.props.showDataExplorer(row.buttons.name, row.buttons.numberOfColumns); - } - } - - private getRow = (index: number) : IGridRow => { - if (index >= 0 && index < this.state.gridRows.length) { - return this.state.gridRows[index]; - } - return {buttons: { supportsDataExplorer: false, name: '', numberOfColumns: 0}, name: '', type: '', size: '', value: ''}; - } -} diff --git a/src/datascience-ui/history-react/variableExplorerButtonCellFormatter.css b/src/datascience-ui/history-react/variableExplorerButtonCellFormatter.css deleted file mode 100644 index 8ad219b7558c..000000000000 --- a/src/datascience-ui/history-react/variableExplorerButtonCellFormatter.css +++ /dev/null @@ -1,4 +0,0 @@ -.variable-explorer-button-cell { - height: 18px; - width: 18px; -} diff --git a/src/datascience-ui/history-react/variableExplorerButtonCellFormatter.tsx b/src/datascience-ui/history-react/variableExplorerButtonCellFormatter.tsx deleted file mode 100644 index 6116b8cce8d3..000000000000 --- a/src/datascience-ui/history-react/variableExplorerButtonCellFormatter.tsx +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as React from 'react'; - -import { Image, ImageName } from '../react-common/image'; -import { ImageButton } from '../react-common/imageButton'; -import { getLocString } from '../react-common/locReactSide'; - -import './variableExplorerButtonCellFormatter.css'; - -export interface IButtonCellValue { - supportsDataExplorer: boolean; - name: string; - numberOfColumns: number; -} - -interface IVariableExplorerButtonCellFormatterProps { - baseTheme: string; - value?: IButtonCellValue; - showDataExplorer(targetVariable: string, numberOfColumns: number): void; -} - -export class VariableExplorerButtonCellFormatter extends React.Component<IVariableExplorerButtonCellFormatterProps> { - public shouldComponentUpdate(nextProps: IVariableExplorerButtonCellFormatterProps) { - return nextProps.value !== this.props.value; - } - - public render() { - const className = 'variable-explorer-button-cell'; - if (this.props.value !== null && this.props.value !== undefined) { - if (this.props.value.supportsDataExplorer) { - return( - <div className={className}> - <ImageButton baseTheme={this.props.baseTheme} tooltip={getLocString('DataScience.showDataExplorerTooltip', 'Show variable in data viewer.')} onClick={this.onDataExplorerClick}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.OpenInNewWindow}/> - </ImageButton> - </div> - ); - } else { - return(null); - } - } - return []; - } - - private onDataExplorerClick = () => { - if (this.props.value !== null && this.props.value !== undefined) { - this.props.showDataExplorer(this.props.value.name, this.props.value.numberOfColumns); - } - } -} diff --git a/src/datascience-ui/history-react/variableExplorerCellFormatter.css b/src/datascience-ui/history-react/variableExplorerCellFormatter.css deleted file mode 100644 index 1996345f9b1b..000000000000 --- a/src/datascience-ui/history-react/variableExplorerCellFormatter.css +++ /dev/null @@ -1,15 +0,0 @@ -.react-grid-variable-explorer-cell-variable { - color: var(--code-variable-color, var(--vscode-editor-foreground)); -} - -.react-grid-variable-explorer-cell-type { - color: var(--code-type-color, var(--vscode-editor-foreground)); -} - -.react-grid-variable-explorer-cell-string { - color: var(--code-string-color, var(--vscode-editor-foreground)); -} - -.react-grid-variable-explorer-cell-numeric { - color: var(--code-numeric-color, var(--vscode-editor-foreground)); -} \ No newline at end of file diff --git a/src/datascience-ui/history-react/variableExplorerCellFormatter.tsx b/src/datascience-ui/history-react/variableExplorerCellFormatter.tsx deleted file mode 100644 index 1eabfb00849e..000000000000 --- a/src/datascience-ui/history-react/variableExplorerCellFormatter.tsx +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './variableExplorerCellFormatter.css'; - -import * as React from 'react'; - -export enum CellStyle { - variable = 'variable', - type = 'type', - string = 'string', - numeric = 'numeric' -} - -interface IVariableExplorerCellFormatterProps { - cellStyle: CellStyle; - // value gets populated by the default cell formatter props - value?: string | number | object | boolean; - role?: string; -} - -// Our formatter for cells in the variable explorer. Allow for different styles per column type -export class VariableExplorerCellFormatter extends React.Component<IVariableExplorerCellFormatterProps> { - public shouldComponentUpdate(nextProps: IVariableExplorerCellFormatterProps) { - return nextProps.value !== this.props.value; - } - - public render() { - const className = `react-grid-variable-explorer-cell-${this.props.cellStyle.toString()}`; - if (this.props.value !== null && this.props.value !== undefined) { - return(<div className={className} role={this.props.role ? this.props.role : 'cell'} title={this.props.value.toString()}>{this.props.value}</div>); - } - return []; - } -} diff --git a/src/datascience-ui/history-react/variableExplorerEmptyRows.css b/src/datascience-ui/history-react/variableExplorerEmptyRows.css deleted file mode 100644 index 23fe2407049e..000000000000 --- a/src/datascience-ui/history-react/variableExplorerEmptyRows.css +++ /dev/null @@ -1,4 +0,0 @@ -#variable-explorer-empty-rows { - margin: 5px; - font-family: var(--code-font-family); -} \ No newline at end of file diff --git a/src/datascience-ui/history-react/variableExplorerEmptyRows.tsx b/src/datascience-ui/history-react/variableExplorerEmptyRows.tsx deleted file mode 100644 index 32ee81dc3d3b..000000000000 --- a/src/datascience-ui/history-react/variableExplorerEmptyRows.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './variableExplorerEmptyRows.css'; - -import * as React from 'react'; -import { getLocString } from '../react-common/locReactSide'; - -export const VariableExplorerEmptyRowsView = () => { - // IANHU: Change - const message = getLocString('DataScience.noRowsInVariableExplorer', 'No variables defined'); - - return ( - <div id='variable-explorer-empty-rows'> - {message} - </div> - ); -}; diff --git a/src/datascience-ui/history-react/variableExplorerGrid.less b/src/datascience-ui/history-react/variableExplorerGrid.less deleted file mode 100644 index 77b2a379b656..000000000000 --- a/src/datascience-ui/history-react/variableExplorerGrid.less +++ /dev/null @@ -1,80 +0,0 @@ -/* Import bootstrap, but prefix it all with our grid div so we don't clobber our interactive windows styles */ -#variable-explorer-data-grid { - @import "~bootstrap-less/bootstrap/bootstrap.less"; -} - -#variable-explorer-data-grid .form-control { - height: auto; - padding: 0px; - font-size: inherit; - font-weight: inherit; - line-height: inherit; - border-radius: 0px; -} - -#variable-explorer-data-grid .react-grid-Main { - font-family: var(--code-font-family); - background-color: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - outline: none; -} - -#variable-explorer-data-grid .react-grid-Grid { - background-color: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - border-style: none; -} - -#variable-explorer-data-grid .react-grid-Canvas { - background-color: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); -} - -#variable-explorer-data-grid .react-grid-Header { - background-color: var(--override-tabs-background, var(--vscode-notifications-background)); -} - -#variable-explorer-data-grid .react-grid-HeaderCell { - background-color: var(--override-lineHighlightBorder, var(--vscode-editor-lineHighlightBorder)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - border-style: solid; - border-color: var(--override-badge-background, var(--vscode-badge-background)); - padding: 2px; -} - - -#variable-explorer-data-grid .react-grid-Row--even { - background-color: var(--override-background, var(--vscode-editor-background)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - border-right-color: var(--override-badge-background, var(--vscode-badge-background)); - border-bottom-color: var(--override-badge-background, var(--vscode-badge-background)); -} - -#variable-explorer-data-grid .react-grid-Row--odd { - background-color: var(--override-lineHighlightBorder, var(--vscode-editor-lineHighlightBorder)); - color: var(--override-foreground, var(--vscode-editor-foreground)); - border-right-color: var(--override-badge-background, var(--vscode-badge-background)); - border-bottom-color: var(--override-badge-background, var(--vscode-badge-background)); -} - -#variable-explorer-data-grid .react-grid-Cell { - background-color: transparent; - color: var(--override-foreground, var(--vscode-editor-foreground)); - border-style: none; -} - -#variable-explorer-data-grid .react-grid-Cell:hover { - background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); -} - -#variable-explorer-data-grid .react-grid-Row:hover { - background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); -} - -#variable-explorer-data-grid .react-grid-Row:hover .react-grid-Cell { - background-color: var(--override-selection-background, var(--vscode-editor-selectionBackground)); -} - -#variable-explorer-data-grid .rdg-selected { - visibility: hidden; -} diff --git a/src/datascience-ui/history-react/variableExplorerHeaderCellFormatter.tsx b/src/datascience-ui/history-react/variableExplorerHeaderCellFormatter.tsx deleted file mode 100644 index 2b3ad1f0dc4e..000000000000 --- a/src/datascience-ui/history-react/variableExplorerHeaderCellFormatter.tsx +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as React from 'react'; - -interface IVariableExplorerHeaderCellFormatterProps { - // value gets populated by the default cell formatter props - column?: { - name: string; - }; -} - -// Our formatter for cells in the variable explorer. Allow for different styles per column type -export class VariableExplorerHeaderCellFormatter extends React.Component<IVariableExplorerHeaderCellFormatterProps> { - public render() { - if (this.props.column) { - return(<div role='columnheader'><span>{this.props.column.name}</span></div>); - } - } -} diff --git a/src/datascience-ui/history-react/variableExplorerRowRenderer.tsx b/src/datascience-ui/history-react/variableExplorerRowRenderer.tsx deleted file mode 100644 index bbde2cc53bf8..000000000000 --- a/src/datascience-ui/history-react/variableExplorerRowRenderer.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as React from 'react'; - -// tslint:disable:no-any -interface IVariableExplorerRowProps { - renderBaseRow(props: any) : JSX.Element; -} - -export const VariableExplorerRowRenderer: React.SFC<IVariableExplorerRowProps & any> = (props) => { - return <div role='row'>{props.renderBaseRow(props)}</div>; -}; diff --git a/src/datascience-ui/history-react/variablePanel.css b/src/datascience-ui/history-react/variablePanel.css deleted file mode 100644 index 4ccb8f2e72af..000000000000 --- a/src/datascience-ui/history-react/variablePanel.css +++ /dev/null @@ -1,6 +0,0 @@ -#variable-divider { - width: 100%; - border-top-color: var(--override-badge-background, var(--vscode-badge-background)); - border-top-style: solid; - border-top-width: 2px; -} \ No newline at end of file diff --git a/src/datascience-ui/history-react/variablePanel.tsx b/src/datascience-ui/history-react/variablePanel.tsx deleted file mode 100644 index c1d7906ef6b1..000000000000 --- a/src/datascience-ui/history-react/variablePanel.tsx +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './variablePanel.css'; - -import * as React from 'react'; - -import { Progress } from '../react-common/progress'; -import { VariableExplorer } from './variableExplorer'; - -export interface IVariablePanelProps { - baseTheme: string; - busy: boolean; - skipDefault?: boolean; - testMode?: boolean; - variableExplorerRef: React.RefObject<VariableExplorer>; - debugging: boolean; - showDataExplorer(targetVariable: string, numberOfColumns: number): void; - refreshVariables(): void; - variableExplorerToggled(open: boolean): void; -} - -export class VariablePanel extends React.Component<IVariablePanelProps> { - constructor(prop: IVariablePanelProps) { - super(prop); - } - - public render() { - const progressBar = this.props.busy && !this.props.testMode ? <Progress /> : undefined; - return( - <div id='variable-panel'> - {progressBar} - <VariableExplorer - debugging={this.props.debugging} - baseTheme={this.props.baseTheme} - skipDefault={this.props.skipDefault} - showDataExplorer={this.props.showDataExplorer} - refreshVariables={this.props.refreshVariables} - variableExplorerToggled={this.props.variableExplorerToggled} - ref={this.props.variableExplorerRef} - /> - <div id='variable-divider'/> - </div> - ); - } -} diff --git a/src/datascience-ui/plot/index.css b/src/datascience-ui/plot/index.css deleted file mode 100644 index b4cc7250b98c..000000000000 --- a/src/datascience-ui/plot/index.css +++ /dev/null @@ -1,5 +0,0 @@ -body { - margin: 0; - padding: 0; - font-family: sans-serif; -} diff --git a/src/datascience-ui/plot/index.html b/src/datascience-ui/plot/index.html deleted file mode 100644 index 9dd3ffa71749..000000000000 --- a/src/datascience-ui/plot/index.html +++ /dev/null @@ -1,356 +0,0 @@ -<!doctype html> -<html lang="en"> - <head> - <meta charset="utf-8"> - <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> - <meta name="theme-color" content="#000000"> - <title>Python Extension Plot Viewer</title> - <base href="<%= htmlWebpackPlugin.options.indexUrl %>"> - <style id='default-styles'> -:root { --background-color: #ffffff; - --comment-color: green; ---color: #000000; ---font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; ---font-size: 13px; ---font-weight: normal; ---link-active-color: #006ab1; ---link-color: #006ab1; ---vscode-activityBar-background: #2c2c2c; ---vscode-activityBar-dropBackground: rgba(255, 255, 255, 0.12); ---vscode-activityBar-foreground: #ffffff; ---vscode-activityBar-inactiveForeground: rgba(255, 255, 255, 0.6); ---vscode-activityBarBadge-background: #007acc; ---vscode-activityBarBadge-foreground: #ffffff; ---vscode-badge-background: #c4c4c4; ---vscode-badge-foreground: #333333; ---vscode-breadcrumb-activeSelectionForeground: #4e4e4e; ---vscode-breadcrumb-background: #ffffff; ---vscode-breadcrumb-focusForeground: #4e4e4e; ---vscode-breadcrumb-foreground: rgba(97, 97, 97, 0.8); ---vscode-breadcrumbPicker-background: #f3f3f3; ---vscode-button-background: #007acc; ---vscode-button-foreground: #ffffff; ---vscode-button-hoverBackground: #0062a3; ---vscode-debugExceptionWidget-background: #f1dfde; ---vscode-debugExceptionWidget-border: #a31515; ---vscode-debugToolBar-background: #f3f3f3; ---vscode-descriptionForeground: #717171; ---vscode-diffEditor-insertedTextBackground: rgba(155, 185, 85, 0.2); ---vscode-diffEditor-removedTextBackground: rgba(255, 0, 0, 0.2); ---vscode-dropdown-background: #ffffff; ---vscode-dropdown-border: #cecece; ---vscode-editor-background: #ffffff; ---vscode-editor-findMatchBackground: #a8ac94; ---vscode-editor-findMatchHighlightBackground: rgba(234, 92, 0, 0.33); ---vscode-editor-findRangeHighlightBackground: rgba(180, 180, 180, 0.3); ---vscode-editor-font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", HelveticaNeue-Light, Ubuntu, "Droid Sans", sans-serif; ---vscode-editor-font-size: 13px; ---vscode-editor-font-weight: normal; ---vscode-editor-foreground: #000000; ---vscode-editor-hoverHighlightBackground: rgba(173, 214, 255, 0.15); ---vscode-editor-inactiveSelectionBackground: #e5ebf1; ---vscode-editor-lineHighlightBorder: #eeeeee; ---vscode-editor-rangeHighlightBackground: rgba(253, 255, 0, 0.2); ---vscode-editor-selectionBackground: #add6ff; ---vscode-editor-selectionHighlightBackground: rgba(173, 214, 255, 0.3); ---vscode-editor-snippetFinalTabstopHighlightBorder: rgba(10, 50, 100, 0.5); ---vscode-editor-snippetTabstopHighlightBackground: rgba(10, 50, 100, 0.2); ---vscode-editor-wordHighlightBackground: rgba(87, 87, 87, 0.25); ---vscode-editor-wordHighlightStrongBackground: rgba(14, 99, 156, 0.25); ---vscode-editorActiveLineNumber-foreground: #0b216f; ---vscode-editorBracketMatch-background: rgba(0, 100, 0, 0.1); ---vscode-editorBracketMatch-border: #b9b9b9; ---vscode-editorCodeLens-foreground: #999999; ---vscode-editorCursor-foreground: #000000; ---vscode-editorError-foreground: #d60a0a; ---vscode-editorGroup-border: #e7e7e7; ---vscode-editorGroup-dropBackground: rgba(38, 119, 203, 0.18); ---vscode-editorGroupHeader-noTabsBackground: #ffffff; ---vscode-editorGroupHeader-tabsBackground: #f3f3f3; ---vscode-editorGutter-addedBackground: #81b88b; ---vscode-editorGutter-background: #ffffff; ---vscode-editorGutter-commentRangeForeground: #c5c5c5; ---vscode-editorGutter-deletedBackground: #ca4b51; ---vscode-editorGutter-modifiedBackground: #66afe0; ---vscode-editorHint-foreground: #6c6c6c; ---vscode-editorHoverWidget-background: #f3f3f3; ---vscode-editorHoverWidget-border: #c8c8c8; ---vscode-editorIndentGuide-activeBackground: #939393; ---vscode-editorIndentGuide-background: #d3d3d3; ---vscode-editorInfo-foreground: #008000; ---vscode-editorLineNumber-activeForeground: #0b216f; ---vscode-editorLineNumber-foreground: #237893; ---vscode-editorLink-activeForeground: #0000ff; ---vscode-editorMarkerNavigation-background: #ffffff; ---vscode-editorMarkerNavigationError-background: #d60a0a; ---vscode-editorMarkerNavigationInfo-background: #008000; ---vscode-editorMarkerNavigationWarning-background: #117711; ---vscode-editorOverviewRuler-addedForeground: rgba(0, 122, 204, 0.6); ---vscode-editorOverviewRuler-border: rgba(127, 127, 127, 0.3); ---vscode-editorOverviewRuler-bracketMatchForeground: #a0a0a0; ---vscode-editorOverviewRuler-commonContentForeground: rgba(96, 96, 96, 0.4); ---vscode-editorOverviewRuler-currentContentForeground: rgba(64, 200, 174, 0.5); ---vscode-editorOverviewRuler-deletedForeground: rgba(0, 122, 204, 0.6); ---vscode-editorOverviewRuler-errorForeground: rgba(255, 18, 18, 0.7); ---vscode-editorOverviewRuler-findMatchForeground: rgba(246, 185, 77, 0.7); ---vscode-editorOverviewRuler-incomingContentForeground: rgba(64, 166, 255, 0.5); ---vscode-editorOverviewRuler-infoForeground: rgba(18, 18, 136, 0.7); ---vscode-editorOverviewRuler-modifiedForeground: rgba(0, 122, 204, 0.6); ---vscode-editorOverviewRuler-rangeHighlightForeground: rgba(0, 122, 204, 0.6); ---vscode-editorOverviewRuler-selectionHighlightForeground: rgba(160, 160, 160, 0.8); ---vscode-editorOverviewRuler-warningForeground: rgba(18, 136, 18, 0.7); ---vscode-editorOverviewRuler-wordHighlightForeground: rgba(160, 160, 160, 0.8); ---vscode-editorOverviewRuler-wordHighlightStrongForeground: rgba(192, 160, 192, 0.8); ---vscode-editorPane-background: #ffffff; ---vscode-editorRuler-foreground: #d3d3d3; ---vscode-editorSuggestWidget-background: #f3f3f3; ---vscode-editorSuggestWidget-border: #c8c8c8; ---vscode-editorSuggestWidget-foreground: #000000; ---vscode-editorSuggestWidget-highlightForeground: #0066bf; ---vscode-editorSuggestWidget-selectedBackground: #d6ebff; ---vscode-editorUnnecessaryCode-opacity: rgba(0, 0, 0, 0.47); ---vscode-editorWarning-foreground: #117711; ---vscode-editorWhitespace-foreground: rgba(51, 51, 51, 0.2); ---vscode-editorWidget-background: #f3f3f3; ---vscode-editorWidget-border: #c8c8c8; ---vscode-errorForeground: #a1260d; ---vscode-extensionButton-prominentBackground: #327e36; ---vscode-extensionButton-prominentForeground: #ffffff; ---vscode-extensionButton-prominentHoverBackground: #28632b; ---vscode-focusBorder: rgba(0, 122, 204, 0.4); ---vscode-foreground: #616161; ---vscode-gitDecoration-addedResourceForeground: #587c0c; ---vscode-gitDecoration-conflictingResourceForeground: #6c6cc4; ---vscode-gitDecoration-deletedResourceForeground: #ad0707; ---vscode-gitDecoration-ignoredResourceForeground: #8e8e90; ---vscode-gitDecoration-modifiedResourceForeground: #895503; ---vscode-gitDecoration-submoduleResourceForeground: #1258a7; ---vscode-gitDecoration-untrackedResourceForeground: #007100; ---vscode-input-background: #ffffff; ---vscode-input-foreground: #616161; ---vscode-input-placeholderForeground: #767676; ---vscode-inputOption-activeBorder: #007acc; ---vscode-inputValidation-errorBackground: #f2dede; ---vscode-inputValidation-errorBorder: #be1100; ---vscode-inputValidation-infoBackground: #d6ecf2; ---vscode-inputValidation-infoBorder: #007acc; ---vscode-inputValidation-warningBackground: #f6f5d2; ---vscode-inputValidation-warningBorder: #b89500; ---vscode-list-activeSelectionBackground: #2477ce; ---vscode-list-activeSelectionForeground: #ffffff; ---vscode-list-dropBackground: #d6ebff; ---vscode-list-errorForeground: #b01011; ---vscode-list-focusBackground: #d6ebff; ---vscode-list-highlightForeground: #0066bf; ---vscode-list-hoverBackground: #e8e8e8; ---vscode-list-inactiveFocusBackground: #d8dae6; ---vscode-list-inactiveSelectionBackground: #e4e6f1; ---vscode-list-invalidItemForeground: #b89500; ---vscode-list-warningForeground: #117711; ---vscode-menu-background: #ffffff; ---vscode-menu-selectionBackground: #2477ce; ---vscode-menu-selectionForeground: #ffffff; ---vscode-menu-separatorBackground: #888888; ---vscode-menubar-selectionBackground: rgba(0, 0, 0, 0.1); ---vscode-menubar-selectionForeground: #333333; ---vscode-merge-commonContentBackground: rgba(96, 96, 96, 0.16); ---vscode-merge-commonHeaderBackground: rgba(96, 96, 96, 0.4); ---vscode-merge-currentContentBackground: rgba(64, 200, 174, 0.2); ---vscode-merge-currentHeaderBackground: rgba(64, 200, 174, 0.5); ---vscode-merge-incomingContentBackground: rgba(64, 166, 255, 0.2); ---vscode-merge-incomingHeaderBackground: rgba(64, 166, 255, 0.5); ---vscode-notificationCenterHeader-background: #e7e7e7; ---vscode-notificationLink-foreground: #006ab1; ---vscode-notifications-background: #f3f3f3; ---vscode-notifications-border: #e7e7e7; ---vscode-panel-background: #ffffff; ---vscode-panel-border: rgba(128, 128, 128, 0.35); ---vscode-panel-dropBackground: rgba(38, 119, 203, 0.18); ---vscode-panelTitle-activeBorder: rgba(128, 128, 128, 0.35); ---vscode-panelTitle-activeForeground: #424242; ---vscode-panelTitle-inactiveForeground: rgba(66, 66, 66, 0.75); ---vscode-peekView-border: #007acc; ---vscode-peekViewEditor-background: #f2f8fc; ---vscode-peekViewEditor-matchHighlightBackground: rgba(245, 216, 2, 0.87); ---vscode-peekViewEditorGutter-background: #f2f8fc; ---vscode-peekViewResult-background: #f3f3f3; ---vscode-peekViewResult-fileForeground: #1e1e1e; ---vscode-peekViewResult-lineForeground: #646465; ---vscode-peekViewResult-matchHighlightBackground: rgba(234, 92, 0, 0.3); ---vscode-peekViewResult-selectionBackground: rgba(51, 153, 255, 0.2); ---vscode-peekViewResult-selectionForeground: #6c6c6c; ---vscode-peekViewTitle-background: #ffffff; ---vscode-peekViewTitleDescription-foreground: rgba(108, 108, 108, 0.7); ---vscode-peekViewTitleLabel-foreground: #333333; ---vscode-pickerGroup-border: #cccedb; ---vscode-pickerGroup-foreground: #0066bf; ---vscode-progressBar-background: #0e70c0; ---vscode-scrollbar-shadow: #dddddd; ---vscode-scrollbarSlider-activeBackground: rgba(0, 0, 0, 0.6); ---vscode-scrollbarSlider-background: rgba(100, 100, 100, 0.4); ---vscode-scrollbarSlider-hoverBackground: rgba(100, 100, 100, 0.7); ---vscode-settings-checkboxBackground: #ffffff; ---vscode-settings-checkboxBorder: #cecece; ---vscode-settings-dropdownBackground: #ffffff; ---vscode-settings-dropdownBorder: #cecece; ---vscode-settings-dropdownListBorder: #c8c8c8; ---vscode-settings-headerForeground: #444444; ---vscode-settings-modifiedItemIndicator: #66afe0; ---vscode-settings-numberInputBackground: #ffffff; ---vscode-settings-numberInputBorder: #cecece; ---vscode-settings-numberInputForeground: #616161; ---vscode-settings-textInputBackground: #ffffff; ---vscode-settings-textInputBorder: #cecece; ---vscode-settings-textInputForeground: #616161; ---vscode-sideBar-background: #f3f3f3; ---vscode-sideBar-dropBackground: rgba(255, 255, 255, 0.12); ---vscode-sideBarSectionHeader-background: rgba(128, 128, 128, 0.2); ---vscode-sideBarTitle-foreground: #6f6f6f; ---vscode-statusBar-background: #007acc; ---vscode-statusBar-debuggingBackground: #cc6633; ---vscode-statusBar-debuggingForeground: #ffffff; ---vscode-statusBar-foreground: #ffffff; ---vscode-statusBar-noFolderBackground: #68217a; ---vscode-statusBar-noFolderForeground: #ffffff; ---vscode-statusBarItem-activeBackground: rgba(255, 255, 255, 0.18); ---vscode-statusBarItem-hoverBackground: rgba(255, 255, 255, 0.12); ---vscode-statusBarItem-prominentBackground: #388a34; ---vscode-statusBarItem-prominentHoverBackground: #369432; ---vscode-tab-activeBackground: #ffffff; ---vscode-tab-activeForeground: #333333; ---vscode-tab-border: #f3f3f3; ---vscode-tab-inactiveBackground: #ececec; ---vscode-tab-inactiveForeground: rgba(51, 51, 51, 0.5); ---vscode-tab-unfocusedActiveForeground: rgba(51, 51, 51, 0.7); ---vscode-tab-unfocusedInactiveForeground: rgba(51, 51, 51, 0.25); ---vscode-terminal-ansiBlack: #000000; ---vscode-terminal-ansiBlue: #0451a5; ---vscode-terminal-ansiBrightBlack: #666666; ---vscode-terminal-ansiBrightBlue: #0451a5; ---vscode-terminal-ansiBrightCyan: #0598bc; ---vscode-terminal-ansiBrightGreen: #14ce14; ---vscode-terminal-ansiBrightMagenta: #bc05bc; ---vscode-terminal-ansiBrightRed: #cd3131; ---vscode-terminal-ansiBrightWhite: #a5a5a5; ---vscode-terminal-ansiBrightYellow: #b5ba00; ---vscode-terminal-ansiCyan: #0598bc; ---vscode-terminal-ansiGreen: #00bc00; ---vscode-terminal-ansiMagenta: #bc05bc; ---vscode-terminal-ansiRed: #cd3131; ---vscode-terminal-ansiWhite: #555555; ---vscode-terminal-ansiYellow: #949800; ---vscode-terminal-background: #ffffff; ---vscode-terminal-border: rgba(128, 128, 128, 0.35); ---vscode-terminal-foreground: #333333; ---vscode-terminal-selectionBackground: rgba(0, 0, 0, 0.25); ---vscode-textBlockQuote-background: rgba(127, 127, 127, 0.1); ---vscode-textBlockQuote-border: rgba(0, 122, 204, 0.5); ---vscode-textCodeBlock-background: rgba(220, 220, 220, 0.4); ---vscode-textLink-activeForeground: #006ab1; ---vscode-textLink-foreground: #006ab1; ---vscode-textPreformat-foreground: #a31515; ---vscode-textSeparator-foreground: rgba(0, 0, 0, 0.18); ---vscode-titleBar-activeBackground: #dddddd; ---vscode-titleBar-activeForeground: #333333; ---vscode-titleBar-inactiveBackground: rgba(221, 221, 221, 0.6); ---vscode-titleBar-inactiveForeground: rgba(51, 51, 51, 0.6); ---vscode-widget-shadow: #a8a8a8; ---code-font-family: 'Comic-Sans'; ---code-font-size: 15px; -} - - body { - background-color: var(--vscode-editor-background); - color: var(--vscode-editor-foreground); - font-family: var(--vscode-editor-font-family); - font-weight: var(--vscode-editor-font-weight); - font-size: var(--vscode-editor-font-size); - margin: 0; - padding: 0 20px; - } - - img { - max-width: 100%; - max-height: 100%; - } - - a { - color: var(--vscode-textLink-foreground); - } - - a:hover { - color: var(--vscode-textLink-activeForeground); - } - - a:focus, - input:focus, - select:focus, - textarea:focus { - outline: 1px solid -webkit-focus-ring-color; - outline-offset: -1px; - } - - code { - color: var(--vscode-textPreformat-foreground); - } - - blockquote { - background: var(--vscode-textBlockQuote-background); - border-color: var(--vscode-textBlockQuote-border); - } - - ::-webkit-scrollbar { - width: 10px; - height: 10px; - } - - ::-webkit-scrollbar-thumb { - background-color: rgba(121, 121, 121, 0.4); - } - body.vscode-light::-webkit-scrollbar-thumb { - background-color: rgba(100, 100, 100, 0.4); - } - body.vscode-high-contrast::-webkit-scrollbar-thumb { - background-color: rgba(111, 195, 223, 0.3); - } - - ::-webkit-scrollbar-thumb:hover { - background-color: rgba(100, 100, 100, 0.7); - } - body.vscode-light::-webkit-scrollbar-thumb:hover { - background-color: rgba(100, 100, 100, 0.7); - } - body.vscode-high-contrast::-webkit-scrollbar-thumb:hover { - background-color: rgba(111, 195, 223, 0.8); - } - - ::-webkit-scrollbar-thumb:active { - background-color: rgba(85, 85, 85, 0.8); - } - body.vscode-light::-webkit-scrollbar-thumb:active { - background-color: rgba(0, 0, 0, 0.6); - } - body.vscode-high-contrast::-webkit-scrollbar-thumb:active { - background-color: rgba(111, 195, 223, 0.8); - } - </style> - - </head> - <body> - <div id="root"></div> - <script type="text/javascript"> - function resolvePath(relativePath) { - if (relativePath && relativePath[0] == '.' && relativePath[1] != '.') { - return "<%= htmlWebpackPlugin.options.imageBaseUrl %>" + relativePath.substring(1); - } - - return "<%= htmlWebpackPlugin.options.imageBaseUrl %>" + relativePath; - } - function getInitialSettings() { - return { allowInput: true, - extraSettings: { editorCursor: 'block', editorCursorBlink: 'blink' } - }; - } - </script> - </body> -</html> diff --git a/src/datascience-ui/plot/index.tsx b/src/datascience-ui/plot/index.tsx deleted file mode 100644 index 6b8efe9a2592..000000000000 --- a/src/datascience-ui/plot/index.tsx +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import './index.css'; - -import * as React from 'react'; -import * as ReactDOM from 'react-dom'; - -import { IVsCodeApi } from '../react-common/postOffice'; -import { detectBaseTheme } from '../react-common/themeDetector'; -import { MainPanel } from './mainPanel'; - -// This special function talks to vscode from a web panel -export declare function acquireVsCodeApi(): IVsCodeApi; - -const baseTheme = detectBaseTheme(); - -// tslint:disable:no-typeof-undefined -ReactDOM.render( - <MainPanel baseTheme={baseTheme} skipDefault={typeof acquireVsCodeApi !== 'undefined'}/>, // Turn this back off when we have real variable explorer data - document.getElementById('root') as HTMLElement -); diff --git a/src/datascience-ui/plot/mainPanel.css b/src/datascience-ui/plot/mainPanel.css deleted file mode 100644 index aa507ba5a6b9..000000000000 --- a/src/datascience-ui/plot/mainPanel.css +++ /dev/null @@ -1,13 +0,0 @@ - -.main-panel { - position: absolute; - bottom: 0; - top: 0; - left: 0; - right: 0; - font-size: var(--code-font-size); - font-family: var(--code-font-family); - background-color: var(--vscode-editor-background); - overflow: hidden; -} - diff --git a/src/datascience-ui/plot/mainPanel.tsx b/src/datascience-ui/plot/mainPanel.tsx deleted file mode 100644 index 5605a62f992b..000000000000 --- a/src/datascience-ui/plot/mainPanel.tsx +++ /dev/null @@ -1,382 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -//import copy from 'copy-to-clipboard'; -import * as React from 'react'; -import { Tool, Value } from 'react-svg-pan-zoom'; -import * as uuid from 'uuid/v4'; - -import { createDeferred } from '../../client/common/utils/async'; -import { RegExpValues } from '../../client/datascience/constants'; -import { IPlotViewerMapping, PlotViewerMessages } from '../../client/datascience/plotting/types'; -import { IMessageHandler, PostOffice } from '../react-common/postOffice'; -import { getSettings } from '../react-common/settingsReactSide'; -import { StyleInjector } from '../react-common/styleInjector'; -import { SvgList } from '../react-common/svgList'; -import { SvgViewer } from '../react-common/svgViewer'; - -// Our css has to come after in order to override body styles -import './mainPanel.css'; -import { TestSvg } from './testSvg'; -import { Toolbar } from './toolbar'; - -export interface IMainPanelProps { - skipDefault?: boolean; - baseTheme: string; - testMode?: boolean; -} - -interface ISize { - width: string; - height: string; -} - -//tslint:disable:no-any -interface IMainPanelState { - images: string[]; - thumbnails: string[]; - sizes: ISize[]; - values: (Value | undefined)[]; - ids: string[]; - currentImage: number; - tool: Tool; - forceDark?: boolean; -} - -const PanKeyboardSize = 10; - -export class MainPanel extends React.Component<IMainPanelProps, IMainPanelState> implements IMessageHandler { - private container: React.RefObject<HTMLDivElement> = React.createRef<HTMLDivElement>(); - private viewer: React.RefObject<SvgViewer> = React.createRef<SvgViewer>(); - private postOffice: PostOffice = new PostOffice(); - private currentValue: Value | undefined; - - // tslint:disable-next-line:max-func-body-length - constructor(props: IMainPanelProps, _state: IMainPanelState) { - super(props); - const images = !props.skipDefault ? - [TestSvg, TestSvg, TestSvg] : - []; - const thumbnails = images.map(this.generateThumbnail); - const sizes = images.map(this.extractSize); - const values = images.map(_i => undefined); - const ids = images.map(_i => uuid()); - - this.state = {images, thumbnails, sizes, values, ids, tool: 'pan', currentImage: images.length > 0 ? 0 : -1}; - } - - public componentWillMount() { - // Add ourselves as a handler for the post office - this.postOffice.addHandler(this); - - // Tell the plot viewer code we have started. - this.postOffice.sendMessage<IPlotViewerMapping, 'started'>(PlotViewerMessages.Started); - - // Listen to key events - window.addEventListener('keydown', this.onKeyDown); - } - - public componentWillUnmount() { - this.postOffice.removeHandler(this); - this.postOffice.dispose(); - // Stop listening to key events - window.removeEventListener('keydown', this.onKeyDown); - } - - public render = () => { - const baseTheme = this.computeBaseTheme(); - return ( - <div className='main-panel' role='group' ref={this.container}> - <StyleInjector - expectingDark={this.props.baseTheme !== 'vscode-light'} - darkChanged={this.darkChanged} - postOffice={this.postOffice} /> - {this.renderToolbar(baseTheme)} - {this.renderThumbnails(baseTheme)} - {this.renderPlot(baseTheme)} - </div> - ); - } - - // tslint:disable-next-line:no-any - public handleMessage = (msg: string, payload?: any) => { - switch (msg) { - case PlotViewerMessages.SendPlot: - this.addPlot(payload); - break; - - default: - break; - } - - return false; - } - - private darkChanged = (newDark: boolean) => { - // update our base theme if allowed. Don't do this - // during testing as it will mess up the expected render count. - if (!this.props.testMode) { - this.setState( - { - forceDark: newDark - } - ); - } - } - - private computeBaseTheme() : string { - // If we're ignoring, always light - if (getSettings && getSettings().ignoreVscodeTheme) { - return 'vscode-light'; - } - - // Otherwise see if the style injector has figured out - // the theme is dark or not - if (this.state.forceDark !== undefined) { - return this.state.forceDark ? 'vscode-dark' : 'vscode-light'; - } - - return this.props.baseTheme; - } - - private onKeyDown = (event: KeyboardEvent) => { - if (!event.ctrlKey) { - switch (event.key) { - case 'ArrowRight': - if (this.state.currentImage < this.state.images.length - 1) { - this.setState({currentImage: this.state.currentImage + 1}); - } - break; - - case 'ArrowLeft': - if (this.state.currentImage > 0) { - this.setState({currentImage: this.state.currentImage - 1}); - } - break; - - default: - break; - } - } else if (event.ctrlKey && !event.altKey && this.viewer && this.viewer.current) { - switch (event.key) { - case 'ArrowRight': - this.viewer.current.move(PanKeyboardSize, 0); - break; - - case 'ArrowLeft': - this.viewer.current.move(-PanKeyboardSize, 0); - break; - - case 'ArrowUp': - this.viewer.current.move(0, -PanKeyboardSize); - break; - - case 'ArrowDown': - this.viewer.current.move(0, PanKeyboardSize); - break; - - default: - break; - } - } else if (event.ctrlKey && event.altKey && this.viewer && this.viewer.current) { - switch (event.key) { - case '+': - this.viewer.current.zoom(1.5); - break; - - case '-': - this.viewer.current.zoom(0.66666); - break; - - default: - break; - } - } - } - - private addPlot(payload: any) { - this.setState({ - images: [...this.state.images, payload as string], - thumbnails: [...this.state.thumbnails, this.generateThumbnail(payload)], - sizes: [...this.state.sizes, this.extractSize(payload)], - values: [...this.state.values, undefined], - ids: [...this.state.ids, uuid()], - currentImage: this.state.images.length - }); - } - - private renderThumbnails(_baseTheme: string) { - return ( - <SvgList images={this.state.thumbnails} currentImage={this.state.currentImage} imageClicked={this.imageClicked}/> - ); - } - - private renderToolbar(baseTheme: string) { - const prev = this.state.currentImage > 0 ? this.prevClicked : undefined; - const next = this.state.currentImage < this.state.images.length - 1 ? this.nextClicked : undefined; - const deleteClickHandler = this.state.currentImage !== -1 ? this.deleteClicked : undefined; - return ( - <Toolbar - baseTheme={baseTheme} - changeTool={this.changeTool} - exportButtonClicked={this.exportCurrent} - copyButtonClicked={this.copyCurrent} - prevButtonClicked={prev} - nextButtonClicked={next} - deleteButtonClicked={deleteClickHandler} /> - ); - } - private renderPlot(baseTheme: string) { - // Render current plot - const currentPlot = this.state.currentImage >= 0 ? this.state.images[this.state.currentImage] : undefined; - const currentSize = this.state.currentImage >= 0 ? this.state.sizes[this.state.currentImage] : undefined; - const currentId = this.state.currentImage >= 0 ? this.state.ids[this.state.currentImage] : undefined; - const value = this.state.currentImage >= 0 ? this.state.values[this.state.currentImage] : undefined; - if (currentPlot && currentSize && currentId) { - return ( - <SvgViewer - baseTheme={baseTheme} - svg={currentPlot} - id={currentId} - size={currentSize} - defaultValue={value} - tool={this.state.tool} - changeValue={this.changeCurrentValue} - ref={this.viewer} - /> - ); - } - - return null; - } - - private generateThumbnail(image: string): string { - // A 'thumbnail' is really just an svg image with - // the width and height forced to 100% - const h = image.replace(RegExpValues.SvgHeightRegex, '$1100%\"'); - return h.replace(RegExpValues.SvgWidthRegex, '$1100%\"'); - } - - private changeCurrentValue = (value: Value) => { - this.currentValue = {...value}; - } - - private changeTool = (tool: Tool) => { - this.setState({tool}); - } - - private extractSize(image: string): ISize { - let height = '100px'; - let width = '100px'; - - // Try the tags that might have been added by the cell formatter - const sizeTagMatch = RegExpValues.SvgSizeTagRegex.exec(image); - if (sizeTagMatch && sizeTagMatch.length > 2) { - width = sizeTagMatch[1]; - height = sizeTagMatch[2]; - } else { - // Otherwise just parse the height/width directly - const heightMatch = RegExpValues.SvgHeightRegex.exec(image); - if (heightMatch && heightMatch.length > 2) { - height = heightMatch[2]; - } - const widthMatch = RegExpValues.SvgHeightRegex.exec(image); - if (widthMatch && widthMatch.length > 2) { - width = widthMatch[2]; - } - } - - return { - height, - width - }; - } - - private changeCurrentImage(index: number) { - // Update our state for our current image and our current value - if (index !== this.state.currentImage) { - const newValues = [...this.state.values]; - newValues[this.state.currentImage] = this.currentValue; - this.setState({ - currentImage: index, - values: newValues - }); - - // Reassign the current value to the new index so we track it. - this.currentValue = newValues[index]; - } - } - - private imageClicked = (index: number) => { - this.changeCurrentImage(index); - } - - private sendMessage<M extends IPlotViewerMapping, T extends keyof M>(type: T, payload?: M[T]) { - this.postOffice.sendMessage<M, T>(type, payload); - } - - private exportCurrent = async () => { - // In order to export, we need the png and the svg. Generate - // a png by drawing to a canvas and then turning the canvas into a dataurl. - if (this.container && this.container.current) { - const doc = this.container.current.ownerDocument; - if (doc) { - const canvas = doc.createElement('canvas'); - if (canvas) { - const ctx = canvas.getContext('2d'); - if (ctx) { - const waitable = createDeferred(); - const svgBlob = new Blob([this.state.images[this.state.currentImage]], { type: 'image/svg+xml;charset=utf-8' }); - const img = new Image(); - const url = window.URL.createObjectURL(svgBlob); - img.onload = () => { - canvas.width = img.width; - canvas.height = img.height; - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); - waitable.resolve(); - }; - img.src = url; - await waitable.promise; - const png = canvas.toDataURL('png'); - canvas.remove(); - - // Send both our image and the png. - this.sendMessage(PlotViewerMessages.ExportPlot, { svg: this.state.images[this.state.currentImage], png }); - } - } - } - } - } - - private copyCurrent = async () => { - // Not supported at the moment. - } - - private prevClicked = () => { - this.changeCurrentImage(this.state.currentImage - 1); - } - - private nextClicked = () => { - this.changeCurrentImage(this.state.currentImage + 1); - } - - private deleteClicked = () => { - if (this.state.currentImage >= 0) { - const oldCurrent = this.state.currentImage; - const newCurrent = this.state.images.length > 1 ? this.state.currentImage : -1; - - this.setState({ - images: this.state.images.filter((_v, i) => i !== oldCurrent), - sizes: this.state.sizes.filter((_v, i) => i !== oldCurrent), - values: this.state.values.filter((_v, i) => i !== oldCurrent), - thumbnails: this.state.thumbnails.filter((_v, i) => i !== oldCurrent), - currentImage : newCurrent - }); - - // Tell the other side too as we don't want it sending this image again - this.sendMessage(PlotViewerMessages.RemovePlot, oldCurrent); - } - } -} diff --git a/src/datascience-ui/plot/testSvg.ts b/src/datascience-ui/plot/testSvg.ts deleted file mode 100644 index 64bfd3e54221..000000000000 --- a/src/datascience-ui/plot/testSvg.ts +++ /dev/null @@ -1,571 +0,0 @@ -// tslint:disable: no-multiline-string no-trailing-whitespace -export const TestSvg = ` -<svg height="574.678125pt" version="1.1" viewBox="0 0 331.045312 574.678125" width="331.045312pt" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> - <defs> - - </defs> - <g> - <g> - <path d="M 0 574.678125 -L 331.045312 574.678125 -L 331.045312 0 -L 0 0 -z -" style="fill:none;"></path> - </g> - <g> - <g> - <path d="M 44.845313 550.8 -L 323.845312 550.8 -L 323.845312 7.2 -L 44.845313 7.2 -z -" style="fill:#ffffff;"></path> - </g> - <g> - <g> - <g> - <defs> - <path d="M 0 0 -L 0 3.5 -" style="stroke:#000000;stroke-width:0.8;"></path> - </defs> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="57.527131" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 0.0 --> - <defs> - <path d="M 31.78125 66.40625 -Q 24.171875 66.40625 20.328125 58.90625 -Q 16.5 51.421875 16.5 36.375 -Q 16.5 21.390625 20.328125 13.890625 -Q 24.171875 6.390625 31.78125 6.390625 -Q 39.453125 6.390625 43.28125 13.890625 -Q 47.125 21.390625 47.125 36.375 -Q 47.125 51.421875 43.28125 58.90625 -Q 39.453125 66.40625 31.78125 66.40625 -z -M 31.78125 74.21875 -Q 44.046875 74.21875 50.515625 64.515625 -Q 56.984375 54.828125 56.984375 36.375 -Q 56.984375 17.96875 50.515625 8.265625 -Q 44.046875 -1.421875 31.78125 -1.421875 -Q 19.53125 -1.421875 13.0625 8.265625 -Q 6.59375 17.96875 6.59375 36.375 -Q 6.59375 54.828125 13.0625 64.515625 -Q 19.53125 74.21875 31.78125 74.21875 -z -"></path> - <path d="M 10.6875 12.40625 -L 21 12.40625 -L 21 0 -L 10.6875 0 -z -"></path> - </defs> - <g transform="translate(49.575568 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-48"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="89.231676" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 2.5 --> - <defs> - <path d="M 19.1875 8.296875 -L 53.609375 8.296875 -L 53.609375 0 -L 7.328125 0 -L 7.328125 8.296875 -Q 12.9375 14.109375 22.625 23.890625 -Q 32.328125 33.6875 34.8125 36.53125 -Q 39.546875 41.84375 41.421875 45.53125 -Q 43.3125 49.21875 43.3125 52.78125 -Q 43.3125 58.59375 39.234375 62.25 -Q 35.15625 65.921875 28.609375 65.921875 -Q 23.96875 65.921875 18.8125 64.3125 -Q 13.671875 62.703125 7.8125 59.421875 -L 7.8125 69.390625 -Q 13.765625 71.78125 18.9375 73 -Q 24.125 74.21875 28.421875 74.21875 -Q 39.75 74.21875 46.484375 68.546875 -Q 53.21875 62.890625 53.21875 53.421875 -Q 53.21875 48.921875 51.53125 44.890625 -Q 49.859375 40.875 45.40625 35.40625 -Q 44.1875 33.984375 37.640625 27.21875 -Q 31.109375 20.453125 19.1875 8.296875 -z -"></path> - <path d="M 10.796875 72.90625 -L 49.515625 72.90625 -L 49.515625 64.59375 -L 19.828125 64.59375 -L 19.828125 46.734375 -Q 21.96875 47.46875 24.109375 47.828125 -Q 26.265625 48.1875 28.421875 48.1875 -Q 40.625 48.1875 47.75 41.5 -Q 54.890625 34.8125 54.890625 23.390625 -Q 54.890625 11.625 47.5625 5.09375 -Q 40.234375 -1.421875 26.90625 -1.421875 -Q 22.3125 -1.421875 17.546875 -0.640625 -Q 12.796875 0.140625 7.71875 1.703125 -L 7.71875 11.625 -Q 12.109375 9.234375 16.796875 8.0625 -Q 21.484375 6.890625 26.703125 6.890625 -Q 35.15625 6.890625 40.078125 11.328125 -Q 45.015625 15.765625 45.015625 23.390625 -Q 45.015625 31 40.078125 35.4375 -Q 35.15625 39.890625 26.703125 39.890625 -Q 22.75 39.890625 18.8125 39.015625 -Q 14.890625 38.140625 10.796875 36.28125 -z -"></path> - </defs> - <g transform="translate(81.280114 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-50"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="120.936222" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 5.0 --> - <g transform="translate(112.984659 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-53"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="152.640767" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 7.5 --> - <defs> - <path d="M 8.203125 72.90625 -L 55.078125 72.90625 -L 55.078125 68.703125 -L 28.609375 0 -L 18.3125 0 -L 43.21875 64.59375 -L 8.203125 64.59375 -z -"></path> - </defs> - <g transform="translate(144.689205 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-55"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="184.345313" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 10.0 --> - <defs> - <path d="M 12.40625 8.296875 -L 28.515625 8.296875 -L 28.515625 63.921875 -L 10.984375 60.40625 -L 10.984375 69.390625 -L 28.421875 72.90625 -L 38.28125 72.90625 -L 38.28125 8.296875 -L 54.390625 8.296875 -L 54.390625 0 -L 12.40625 0 -z -"></path> - </defs> - <g transform="translate(173.2125 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-49"></use> - <use x="63.623047" xlink:href="#DejaVuSans-48"></use> - <use x="127.246094" xlink:href="#DejaVuSans-46"></use> - <use x="159.033203" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="216.049858" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 12.5 --> - <g transform="translate(204.917045 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-49"></use> - <use x="63.623047" xlink:href="#DejaVuSans-50"></use> - <use x="127.246094" xlink:href="#DejaVuSans-46"></use> - <use x="159.033203" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="247.754403" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 15.0 --> - <g transform="translate(236.621591 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-49"></use> - <use x="63.623047" xlink:href="#DejaVuSans-53"></use> - <use x="127.246094" xlink:href="#DejaVuSans-46"></use> - <use x="159.033203" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="279.458949" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 17.5 --> - <g transform="translate(268.326136 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-49"></use> - <use x="63.623047" xlink:href="#DejaVuSans-55"></use> - <use x="127.246094" xlink:href="#DejaVuSans-46"></use> - <use x="159.033203" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="311.163494" xlink:href="#m539de8c21e" y="550.8"></use> - </g> - </g> - <g> - <!-- 20.0 --> - <g transform="translate(300.030682 565.398438)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-50"></use> - <use x="63.623047" xlink:href="#DejaVuSans-48"></use> - <use x="127.246094" xlink:href="#DejaVuSans-46"></use> - <use x="159.033203" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - </g> - <g> - <g> - <g> - <defs> - <path d="M 0 0 -L -3.5 0 -" style="stroke:#000000;stroke-width:0.8;"></path> - </defs> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="526.628231"></use> - </g> - </g> - <g> - <!-- −1.00 --> - <defs> - <path d="M 10.59375 35.5 -L 73.1875 35.5 -L 73.1875 27.203125 -L 10.59375 27.203125 -z -"></path> - </defs> - <g transform="translate(7.2 530.42745)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-8722"></use> - <use x="83.789062" xlink:href="#DejaVuSans-49"></use> - <use x="147.412109" xlink:href="#DejaVuSans-46"></use> - <use x="179.199219" xlink:href="#DejaVuSans-48"></use> - <use x="242.822266" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="464.78806"></use> - </g> - </g> - <g> - <!-- −0.75 --> - <g transform="translate(7.2 468.587279)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-8722"></use> - <use x="83.789062" xlink:href="#DejaVuSans-48"></use> - <use x="147.412109" xlink:href="#DejaVuSans-46"></use> - <use x="179.199219" xlink:href="#DejaVuSans-55"></use> - <use x="242.822266" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="402.947889"></use> - </g> - </g> - <g> - <!-- −0.50 --> - <g transform="translate(7.2 406.747107)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-8722"></use> - <use x="83.789062" xlink:href="#DejaVuSans-48"></use> - <use x="147.412109" xlink:href="#DejaVuSans-46"></use> - <use x="179.199219" xlink:href="#DejaVuSans-53"></use> - <use x="242.822266" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="341.107717"></use> - </g> - </g> - <g> - <!-- −0.25 --> - <g transform="translate(7.2 344.906936)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-8722"></use> - <use x="83.789062" xlink:href="#DejaVuSans-48"></use> - <use x="147.412109" xlink:href="#DejaVuSans-46"></use> - <use x="179.199219" xlink:href="#DejaVuSans-50"></use> - <use x="242.822266" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="279.267546"></use> - </g> - </g> - <g> - <!-- 0.00 --> - <g transform="translate(15.579688 283.066764)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-48"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-48"></use> - <use x="159.033203" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="217.427374"></use> - </g> - </g> - <g> - <!-- 0.25 --> - <g transform="translate(15.579688 221.226593)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-48"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-50"></use> - <use x="159.033203" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="155.587203"></use> - </g> - </g> - <g> - <!-- 0.50 --> - <g transform="translate(15.579688 159.386422)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-48"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-53"></use> - <use x="159.033203" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="93.747031"></use> - </g> - </g> - <g> - <!-- 0.75 --> - <g transform="translate(15.579688 97.54625)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-48"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-55"></use> - <use x="159.033203" xlink:href="#DejaVuSans-53"></use> - </g> - </g> - </g> - <g> - <g> - <g> - <use style="stroke:#000000;stroke-width:0.8;" x="44.845313" xlink:href="#me19ac63e8b" y="31.90686"></use> - </g> - </g> - <g> - <!-- 1.00 --> - <g transform="translate(15.579688 35.706079)scale(0.1 -0.1)"> - <use xlink:href="#DejaVuSans-49"></use> - <use x="63.623047" xlink:href="#DejaVuSans-46"></use> - <use x="95.410156" xlink:href="#DejaVuSans-48"></use> - <use x="159.033203" xlink:href="#DejaVuSans-48"></use> - </g> - </g> - </g> - </g> - <g> - <path clip-path="url(#pffcc3726a6)" d="M 57.527131 279.267546 -L 60.089114 229.634907 -L 62.651098 182.021004 -L 65.213081 138.362462 -L 67.775065 100.435031 -L 70.337048 69.781352 -L 72.899032 47.64822 -L 75.461015 34.935868 -L 78.022998 32.161352 -L 80.584982 39.437521 -L 83.146965 56.468429 -L 85.708949 82.561367 -L 88.270932 116.655043 -L 90.832916 157.362747 -L 93.394899 203.028752 -L 95.956883 251.795658 -L 98.518866 301.679944 -L 101.08085 350.652638 -L 103.642833 396.721847 -L 106.204817 438.013773 -L 108.7668 472.848927 -L 111.328784 499.810439 -L 113.890767 517.801689 -L 116.452751 526.090909 -L 119.014734 524.340947 -L 121.576717 512.622981 -L 124.138701 491.413621 -L 126.700684 461.575527 -L 129.262668 424.322321 -L 131.824651 381.169222 -L 134.386635 333.871421 -L 136.948618 284.352687 -L 139.510602 234.627122 -L 142.072585 186.717241 -L 144.634569 142.571709 -L 147.196552 103.986082 -L 149.758536 72.529774 -L 152.320519 49.482225 -L 154.882503 35.78086 -L 157.444486 31.982963 -L 160.00647 38.243006 -L 162.568453 54.306373 -L 165.130436 79.519709 -L 167.69242 112.857499 -L 170.254403 152.963775 -L 172.816387 198.207274 -L 175.37837 246.747781 -L 177.940354 296.610983 -L 180.502337 345.768766 -L 183.064321 392.221708 -L 185.626304 434.080403 -L 188.188288 469.642311 -L 190.750271 497.461001 -L 193.312255 516.404989 -L 195.874238 525.703756 -L 198.436222 524.979088 -L 200.998205 514.26046 -L 203.560189 493.983836 -L 206.122172 464.973939 -L 208.684155 428.410704 -L 211.246139 385.781288 -L 213.808122 338.819579 -L 216.370106 289.435679 -L 218.932089 239.638204 -L 221.494073 191.452595 -L 224.056056 146.838732 -L 226.61804 107.611218 -L 229.180023 75.365577 -L 231.742007 51.413351 -L 234.30399 36.728765 -L 236.865974 31.909091 -L 239.427957 37.150363 -L 241.989941 52.2394 -L 244.551924 76.562477 -L 247.113908 109.130289 -L 249.675891 148.618186 -L 252.237874 193.420056 -L 254.799858 241.713649 -L 257.361841 291.534691 -L 259.923825 340.856786 -L 262.485808 387.673827 -L 265.047792 430.0816 -L 267.609775 466.355231 -L 270.171759 495.019341 -L 272.733742 514.908061 -L 275.295726 525.212445 -L 277.857709 525.513377 -L 280.419693 515.798617 -L 282.981676 496.4633 -L 285.54366 468.293861 -L 288.105643 432.43605 -L 290.667627 390.348334 -L 293.22961 343.742567 -L 295.791593 294.514373 -L 298.353577 244.666036 -L 300.91556 196.225065 -L 303.477544 151.161727 -L 306.039527 111.308906 -L 308.601511 78.28756 -L 311.163494 53.440782 -" style="fill:none;stroke:#1f77b4;stroke-linecap:square;stroke-width:1.5;"></path> - </g> - <g> - <path d="M 44.845313 550.8 -L 44.845313 7.2 -" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> - </g> - <g> - <path d="M 323.845312 550.8 -L 323.845312 7.2 -" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> - </g> - <g> - <path d="M 44.845313 550.8 -L 323.845312 550.8 -" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> - </g> - <g> - <path d="M 44.845313 7.2 -L 323.845312 7.2 -" style="fill:none;stroke:#000000;stroke-linecap:square;stroke-linejoin:miter;stroke-width:0.8;"></path> - </g> - </g> - </g> - <defs> - <clipPath> - <rect height="543.6" width="279" x="44.845313" y="7.2"></rect> - </clipPath> - </defs> -</svg> -`; diff --git a/src/datascience-ui/plot/toolbar.css b/src/datascience-ui/plot/toolbar.css deleted file mode 100644 index 82df52ea1834..000000000000 --- a/src/datascience-ui/plot/toolbar.css +++ /dev/null @@ -1,7 +0,0 @@ -#plot-toolbar-panel { - position: absolute; - top: 0; - left: 0; - background-color: var(--vscode-editor-background); - border: 1px solid black; -} \ No newline at end of file diff --git a/src/datascience-ui/plot/toolbar.tsx b/src/datascience-ui/plot/toolbar.tsx deleted file mode 100644 index b9a79730cfee..000000000000 --- a/src/datascience-ui/plot/toolbar.tsx +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as React from 'react'; -import { Tool } from 'react-svg-pan-zoom'; -import { Image, ImageName } from '../react-common/image'; -import { ImageButton } from '../react-common/imageButton'; -import { getLocString } from '../react-common/locReactSide'; - -interface IToolbarProps { - baseTheme: string; - changeTool(tool: Tool): void; - prevButtonClicked?(): void; - nextButtonClicked?(): void; - exportButtonClicked(): void; - copyButtonClicked(): void; - deleteButtonClicked?(): void; -} - -export class Toolbar extends React.Component<IToolbarProps> { - constructor(props: IToolbarProps) { - super(props); - } - - public render() { - return ( - <div id='plot-toolbar-panel'> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.prevButtonClicked} disabled={!this.props.prevButtonClicked} tooltip={getLocString('DataScience.previousPlot', 'Previous')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Prev}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.nextButtonClicked} disabled={!this.props.nextButtonClicked} tooltip={getLocString('DataScience.nextPlot', 'Next')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Next}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.pan} tooltip={getLocString('DataScience.panPlot', 'Pan')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Pan}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.zoomIn} tooltip={getLocString('DataScience.zoomInPlot', 'Zoom in')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Zoom}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.zoomOut} tooltip={getLocString('DataScience.zoomOutPlot', 'Zoom out')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.ZoomOut}/> - </ImageButton> - {/* This isn't possible until VS Code supports copying images to the clipboard. See https://github.com/microsoft/vscode/issues/217 - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.copyButtonClicked} tooltip={getLocString('DataScience.copyPlot', 'Copy image to clipboard')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Copy}/> - </ImageButton> */} - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.exportButtonClicked} tooltip={getLocString('DataScience.exportPlot', 'Export to different formats.')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.SaveAs}/> - </ImageButton> - <ImageButton baseTheme={this.props.baseTheme} onClick={this.props.deleteButtonClicked} disabled={!this.props.deleteButtonClicked} tooltip={getLocString('DataScience.deletePlot', 'Remove')}> - <Image baseTheme={this.props.baseTheme} class='image-button-image' image={ImageName.Cancel}/> - </ImageButton> - </div> - ); - } - - private pan = () => { - this.props.changeTool('pan'); - } - - private zoomIn = () => { - this.props.changeTool('zoom-in'); - } - - private zoomOut = () => { - this.props.changeTool('zoom-out'); - } -} diff --git a/src/datascience-ui/react-common/errorBoundary.tsx b/src/datascience-ui/react-common/errorBoundary.tsx deleted file mode 100644 index 8b96ecc33e0e..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: Error, errorInfo: React.ErrorInfo) { - const stack = errorInfo.componentStack; - - // Display fallback UI - this.setState({ hasError: true, errorMessage: `${error} at \n ${stack}`}); - } - - public render() { - if (this.state.hasError) { - // Render our error message; - const style: React.CSSProperties = {}; - // tslint:disable-next-line:no-string-literal - style['whiteSpace'] = 'pre'; - - return <h1 style={style}>{this.state.errorMessage}</h1>; - } - return this.props.children; - } - -} diff --git a/src/datascience-ui/react-common/image.tsx b/src/datascience-ui/react-common/image.tsx deleted file mode 100644 index 847ec60bdc96..000000000000 --- a/src/datascience-ui/react-common/image.tsx +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as React from 'react'; -// tslint:disable-next-line:import-name match-default-export-name -import InlineSVG from 'svg-inline-react'; - -// This react component loads our svg files inline so that we can load them in vscode as it no longer -// supports loading svgs from disk. Please put new images in this list as appropriate. -export enum ImageName { - Cancel, - CollapseAll, - ExpandAll, - GoToSourceCode, - Interrupt, - OpenInNewWindow, - PopIn, - PopOut, - Redo, - Restart, - SaveAs, - Undo, - Pan, - Zoom, - ZoomOut, - Next, - Prev, - Copy -} - -// All of the images must be 'require' so that webpack doesn't rewrite the import as requiring a .default. -// tslint:disable:no-require-imports -const images: { [key: string] : { light: string; dark: string } } = { - Cancel: - { - light: require('./images/Cancel/Cancel_16xMD_vscode.svg'), - dark : require('./images/Cancel/Cancel_16xMD_vscode_dark.svg') - }, - CollapseAll: - { - light: require('./images/CollapseAll/CollapseAll_16x_vscode.svg'), - dark : require('./images/CollapseAll/CollapseAll_16x_vscode_dark.svg') - }, - ExpandAll: - { - light: require('./images/ExpandAll/ExpandAll_16x_vscode.svg'), - dark : require('./images/ExpandAll/ExpandAll_16x_vscode_dark.svg') - }, - GoToSourceCode: - { - light: require('./images/GoToSourceCode/GoToSourceCode_16x_vscode.svg'), - dark : require('./images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg') - }, - Interrupt: - { - light: require('./images/Interrupt/Interrupt_16x_vscode.svg'), - dark : require('./images/Interrupt/Interrupt_16x_vscode_dark.svg') - }, - OpenInNewWindow: - { - light: require('./images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg'), - dark : require('./images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg') - }, - PopIn: - { - light: require('./images/PopIn/PopIn_16x_vscode.svg'), - dark : require('./images/PopIn/PopIn_16x_vscode_dark.svg') - }, - PopOut: - { - light: require('./images/PopOut/PopOut_16x_vscode.svg'), - dark : require('./images/PopOut/PopOut_16x_vscode_dark.svg') - }, - Redo: - { - light: require('./images/Redo/Redo_16x_vscode.svg'), - dark : require('./images/Redo/Redo_16x_vscode_dark.svg') - }, - Restart: - { - light: require('./images/Restart/Restart_grey_16x_vscode.svg'), - dark : require('./images/Restart/Restart_grey_16x_vscode_dark.svg') - }, - SaveAs: - { - light: require('./images/SaveAs/SaveAs_16x_vscode.svg'), - dark : require('./images/SaveAs/SaveAs_16x_vscode_dark.svg') - }, - Undo: - { - light: require('./images/Undo/Undo_16x_vscode.svg'), - dark : require('./images/Undo/Undo_16x_vscode_dark.svg') - }, - Next: - { - light: require('./images/Next/next.svg'), - dark : require('./images/Next/next-inverse.svg') - }, - Prev: - { - light: require('./images/Prev/previous.svg'), - dark : require('./images/Prev/previous-inverse.svg') - }, - // tslint:disable-next-line: no-suspicious-comment - // Todo: Get new images from a designer. These are all temporary. - Pan: - { - light: require('./images/Pan/pan.svg'), - dark : require('./images/Pan/pan_inverse.svg') - }, - Zoom: - { - light: require('./images/Zoom/zoom.svg'), - dark : require('./images/Zoom/zoom_inverse.svg') - }, - ZoomOut: - { - light: require('./images/ZoomOut/zoomout.svg'), - dark : require('./images/ZoomOut/zoomout_inverse.svg') - }, - Copy: - { - light: require('./images/Copy/copy.svg'), - dark : require('./images/Copy/copy_inverse.svg') - } -}; - -interface IImageProps { - baseTheme: string; - image: ImageName; - class: string; -} - -export class Image extends React.Component<IImageProps> { - constructor(props: IImageProps) { - super(props); - } - - public render() { - const key = (ImageName[this.props.image]).toString(); - const image = images.hasOwnProperty(key) ? - images[key] : images.Cancel; // Default is cancel. - const source = this.props.baseTheme.includes('dark') ? image.dark : image.light; - return ( - <InlineSVG className={this.props.class} src={source}/> - ); - } - -} diff --git a/src/datascience-ui/react-common/imageButton.css b/src/datascience-ui/react-common/imageButton.css deleted file mode 100644 index 20840af81121..000000000000 --- a/src/datascience-ui/react-common/imageButton.css +++ /dev/null @@ -1,47 +0,0 @@ -:root { - --button-size: 18px; -} - -.image-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; -} - -.image-button-inner-disabled-filter { - opacity: 0.5; -} - -.image-button-child { - max-width: 100%; - max-height: 100%; -} - -.image-button-child img{ - max-width: 100%; - max-height: 100%; -} - -.image-button-image svg{ - pointer-events: none; -} - -.image-button-vscode-light:disabled { - border-color: gray; - filter: grayscale(100%); -} - -.image-button-vscode-dark:disabled { - border-color: gray; - filter: grayscale(100%); -} - diff --git a/src/datascience-ui/react-common/imageButton.tsx b/src/datascience-ui/react-common/imageButton.tsx deleted file mode 100644 index ac8ea1b73bb1..000000000000 --- a/src/datascience-ui/react-common/imageButton.tsx +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; -import * as React from 'react'; -import './imageButton.css'; - -interface IImageButtonProps { - baseTheme: string; - tooltip : string; - disabled?: boolean; - hidden?: boolean; - onClick?(event?: React.MouseEvent<HTMLButtonElement>) : void; -} - -export class ImageButton extends React.Component<IImageButtonProps> { - constructor(props: IImageButtonProps) { - super(props); - } - - public render() { - const classNames = `image-button image-button-${this.props.baseTheme} ${this.props.hidden ? 'hide' : ''}`; - const innerFilter = this.props.disabled ? 'image-button-inner-disabled-filter' : ''; - const ariaDisabled = this.props.disabled ? 'true' : 'false'; - - return ( - <button role='button' aria-pressed='false' disabled={this.props.disabled} aria-disabled={ariaDisabled} title={this.props.tooltip} aria-label={this.props.tooltip} className={classNames} onClick={this.props.onClick}> - <span className={innerFilter} > - <span className='image-button-child'> - {this.props.children} - </span> - </span> - </button> - ); - } - -} diff --git a/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode.svg b/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode.svg deleted file mode 100644 index e3fe708c0510..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Cancel/Cancel_16xMD_vscode_dark.svg b/src/datascience-ui/react-common/images/Cancel/Cancel_16xMD_vscode_dark.svg deleted file mode 100644 index 7bcae30b65c7..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/CollapseAll/CollapseAll_16x_vscode.svg b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode.svg deleted file mode 100644 index 9ca22c78a573..000000000000 --- a/src/datascience-ui/react-common/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 ><path data-name="&lt;Compound Path&gt;" class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g style="display: none;"><path data-name="&lt;Compound Path&gt;" class="icon-vs-out" d="M15,10H13v2H11v2H2V5H4V3H6V1h9Z" style="display: none;"/></g><g ><path 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/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/CollapseAll/CollapseAll_16x_vscode_dark.svg deleted file mode 100644 index 0bffae69dd19..000000000000 --- a/src/datascience-ui/react-common/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 ><path data-name="&lt;Compound Path&gt;" class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g style="display: none;"><path data-name="&lt;Compound Path&gt;" class="icon-vs-out" d="M15,10H13v2H11v2H2V5H4V3H6V1h9Z" style="display: none;"/></g><g ><path 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/react-common/images/Copy/copy.svg b/src/datascience-ui/react-common/images/Copy/copy.svg deleted file mode 100644 index 6071f048ab34..000000000000 --- a/src/datascience-ui/react-common/images/Copy/copy.svg +++ /dev/null @@ -1,3 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" width="16" height="16"><defs><path d="M10.18 7.56L7.35 7.56" id="aF66deqzp"></path><path d="M9.14 4.67L6.28 4.67L6.28 13.06L11.25 13.06L11.25 6.93L11.99 6.93L11.99 14.03L5.54 14.03L5.54 3.7L9.14 3.7L9.14 4.67ZM9.64 6.36L9.64 3.7L11.99 6.36L9.64 6.36Z" id="b2GO1kijJL"></path><path d="M10.18 9.11L7.35 9.11" id="c2crTfzSSx"></path><path d="M10.18 10.85L7.35 10.85" id="g8tf3xbETq"></path><path d="M5.54 11.67L3.04 11.67L3.04 2.38L5.93 2.38" id="e3zDOD27e"></path><path d="M6.6 1.79L9.43 1.79L6.6 1.79L6.6 1.79Z" id="d2A6Y4wUQ"></path><path d="M12.29 1.11L15.12 1.11L12.29 1.11L12.29 1.11Z" id="fQ13ghGMB"></path><path d="M5.54 7.56L4.48 7.56" id="a3L9P7uuuc"></path><path d="M5.54 5.95L4.48 5.95" id="a1IWLF0rEt"></path><path d="M5.54 9.3L4.48 9.3" id="egAXAJqJd"></path></defs><g><g><g><g><use xlink:href="#aF66deqzp" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g><g><use xlink:href="#b2GO1kijJL" opacity="1" fill="#000000" fill-opacity="1"></use></g><g><g><use xlink:href="#c2crTfzSSx" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#g8tf3xbETq" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#e3zDOD27e" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g><g><use xlink:href="#d2A6Y4wUQ" opacity="1" fill="#bd8afc" fill-opacity="1"></use></g><g><use xlink:href="#fQ13ghGMB" opacity="1" fill="#bd8afc" fill-opacity="1"></use></g><g><g><use xlink:href="#a3L9P7uuuc" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#a1IWLF0rEt" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#egAXAJqJd" opacity="1" fill-opacity="0" stroke="#000000" stroke-width="1" stroke-opacity="1"></use></g></g></g></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/Copy/copy_inverse.svg b/src/datascience-ui/react-common/images/Copy/copy_inverse.svg deleted file mode 100644 index fba45b820769..000000000000 --- a/src/datascience-ui/react-common/images/Copy/copy_inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"> -<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid meet" viewBox="0 0 16 16" width="16" height="16"><defs><path d="M10.18 7.56L7.35 7.56" id="aF66deqzp"></path><path d="M9.14 4.67L6.28 4.67L6.28 13.06L11.25 13.06L11.25 6.93L11.99 6.93L11.99 14.03L5.54 14.03L5.54 3.7L9.14 3.7L9.14 4.67ZM9.64 6.36L9.64 3.7L11.99 6.36L9.64 6.36Z" id="b2GO1kijJL"></path><path d="M10.18 9.11L7.35 9.11" id="c2crTfzSSx"></path><path d="M10.18 10.85L7.35 10.85" id="g8tf3xbETq"></path><path d="M5.54 11.67L3.04 11.67L3.04 2.38L5.93 2.38" id="e3zDOD27e"></path><path d="M6.6 1.79L9.43 1.79L6.6 1.79L6.6 1.79Z" id="d2A6Y4wUQ"></path><path d="M12.29 1.11L15.12 1.11L12.29 1.11L12.29 1.11Z" id="fQ13ghGMB"></path><path d="M5.54 7.56L4.48 7.56" id="a3L9P7uuuc"></path><path d="M5.54 5.95L4.48 5.95" id="a1IWLF0rEt"></path><path d="M5.54 9.3L4.48 9.3" id="egAXAJqJd"></path></defs><g><g><g><g><use xlink:href="#aF66deqzp" opacity="1" fill-opacity="0" stroke="#C5C5C5" stroke-width="1" stroke-opacity="1"></use></g></g><g><use xlink:href="#b2GO1kijJL" opacity="1" fill="#C5C5C5" fill-opacity="1"></use></g><g><g><use xlink:href="#c2crTfzSSx" opacity="1" fill-opacity="0" stroke="#C5C5C5" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#g8tf3xbETq" opacity="1" fill-opacity="0" stroke="#C5C5C5" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#e3zDOD27e" opacity="1" fill-opacity="0" stroke="#C5C5C5" stroke-width="1" stroke-opacity="1"></use></g></g><g><use xlink:href="#d2A6Y4wUQ" opacity="1" fill="#c5c5c5" fill-opacity="1"></use></g><g><use xlink:href="#fQ13ghGMB" opacity="1" fill="#c5c5c5" fill-opacity="1"></use></g><g><g><use xlink:href="#a3L9P7uuuc" opacity="1" fill-opacity="0" stroke="#c5c5c5" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#a1IWLF0rEt" opacity="1" fill-opacity="0" stroke="#C5C5C5" stroke-width="1" stroke-opacity="1"></use></g></g><g><g><use xlink:href="#egAXAJqJd" opacity="1" fill-opacity="0" stroke="#C5C5C5" stroke-width="1" stroke-opacity="1"></use></g></g></g></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode.svg b/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode.svg deleted file mode 100644 index fb7844320255..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M15,1v9H13v2H11v2H2V5H4V3H6V1Z" style="display: none;"/></g><g ><path data-name="&lt;Compound Path&gt;" class="icon-vs-bg" d="M14,2V9H13V3H7V2ZM5,4V5h6v6h1V4Zm5,2v7H3V6ZM9,7H4v5H9Z"/><path 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/react-common/images/ExpandAll/ExpandAll_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/ExpandAll/ExpandAll_16x_vscode_dark.svg deleted file mode 100644 index 06b7ce1cc77c..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M15,1v9H13v2H11v2H2V5H4V3H6V1Z" style="display: none;"/></g><g ><path data-name="&lt;Compound Path&gt;" class="icon-vs-bg" d="M14,2V9H13V3H7V2ZM5,4V5h6v6h1V4Zm5,2v7H3V6ZM9,7H4v5H9Z"/><path 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/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg b/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode.svg deleted file mode 100644 index 7e7bc0cd11af..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><path class="icon-vs-bg" d="M13,11H3V10H13ZM7,14h7V13H7ZM11,4V5h4V4ZM10,8h4V7H10Z"/></g><g ><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/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/GoToSourceCode/GoToSourceCode_16x_vscode_dark.svg deleted file mode 100644 index 2e01d8d191cd..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><path class="icon-vs-bg" d="M13,11H3V10H13ZM7,14h7V13H7ZM11,4V5h4V4ZM10,8h4V7H10Z"/></g><g ><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/react-common/images/Interrupt/Interrupt_16x_vscode.svg b/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode.svg deleted file mode 100644 index e0060f4302cf..000000000000 --- a/src/datascience-ui/react-common/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 > - <path class="icon_x002D_canvas_x002D_transparent" d="M16,16H0V0h16V16z"/> -</g> -<g style="display: none;"> - <path class="icon_x002D_vs_x002D_out" d="M13,13H3V3h10V13z"/> -</g> -<g > - <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/react-common/images/Interrupt/Interrupt_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Interrupt/Interrupt_16x_vscode_dark.svg deleted file mode 100644 index 7ec2258e94ed..000000000000 --- a/src/datascience-ui/react-common/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 > - <path class="icon_x002D_canvas_x002D_transparent" d="M16,16H0V0h16V16z"/> -</g> -<g style="display: none;"> - <path class="icon_x002D_vs_x002D_out" d="M13,13H3V3h10V13z"/> -</g> -<g > - <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/react-common/images/Next/next-inverse.svg b/src/datascience-ui/react-common/images/Next/next-inverse.svg deleted file mode 100644 index 3edc83485d05..000000000000 --- a/src/datascience-ui/react-common/images/Next/next-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.5 12L2.44428 10.9453L7.38955 6L2.44428 1.05473L3.5 0L9.5 6L3.5 12Z" fill="#CCCCCC"/> -</svg> diff --git a/src/datascience-ui/react-common/images/Next/next.svg b/src/datascience-ui/react-common/images/Next/next.svg deleted file mode 100644 index eeb0d05cad61..000000000000 --- a/src/datascience-ui/react-common/images/Next/next.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M3.5 12L2.44428 10.9453L7.38955 6L2.44428 1.05473L3.5 0L9.5 6L3.5 12Z" fill="#424242"/> -</svg> diff --git a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg b/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg deleted file mode 100644 index 7eab14a55c69..000000000000 --- a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12 0V9.75H9.75V12H0V2.25H2.25V0H12ZM11.25 9V0.75H3V2.25H4.5V3H0.75V11.25H9V7.5H9.75V9H11.25ZM5.51367 7.01367L4.98633 6.48633L8.4668 3H6V2.25H9.75V6H9V3.5332L5.51367 7.01367Z" fill="#424242"/> -</svg> diff --git a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg deleted file mode 100644 index 344b42f15f54..000000000000 --- a/src/datascience-ui/react-common/images/OpenInNewWindow/OpenInNewWindow_16x_vscode_dark.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M12 0V9.75H9.75V12H0V2.25H2.25V0H12ZM11.25 9V0.75H3V2.25H4.5V3H0.75V11.25H9V7.5H9.75V9H11.25ZM5.51367 7.01367L4.98633 6.48633L8.4668 3H6V2.25H9.75V6H9V3.5332L5.51367 7.01367Z" fill="#D4D4D4"/> -</svg> diff --git a/src/datascience-ui/react-common/images/Pan/pan.svg b/src/datascience-ui/react-common/images/Pan/pan.svg deleted file mode 100644 index b38db9544b50..000000000000 --- a/src/datascience-ui/react-common/images/Pan/pan.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.3905 3.45001L3.8595 2.92501L6 0.782257L8.1405 2.92501L7.6095 3.45601L6.375 2.21776V4.50001H5.625V2.21776L4.3905 3.45001ZM4.5 6.37501V5.62501H2.21775L3.45 4.39051L2.925 3.85951L0.782249 6.00001L2.925 8.14051L3.456 7.60951L2.21775 6.37501H4.5ZM6.375 9.78226V7.50001H5.625V9.78226L4.3905 8.55001L3.8595 9.08101L6 11.2178L8.1405 9.07501L7.6095 8.54401L6.375 9.78226ZM9.075 3.85726L8.544 4.38826L9.78225 5.62501H7.5V6.37501H9.78225L8.55 7.60951L9.081 8.14051L11.2177 6.00001L9.075 3.85726Z" fill="#424242"/> -</svg> diff --git a/src/datascience-ui/react-common/images/Pan/pan_inverse.svg b/src/datascience-ui/react-common/images/Pan/pan_inverse.svg deleted file mode 100644 index f7a23b31abf2..000000000000 --- a/src/datascience-ui/react-common/images/Pan/pan_inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M4.3905 3.45001L3.8595 2.92501L6 0.782257L8.1405 2.92501L7.6095 3.45601L6.375 2.21776V4.50001H5.625V2.21776L4.3905 3.45001ZM4.5 6.37501V5.62501H2.21775L3.45 4.39051L2.925 3.85951L0.782249 6.00001L2.925 8.14051L3.456 7.60951L2.21775 6.37501H4.5ZM6.375 9.78226V7.50001H5.625V9.78226L4.3905 8.55001L3.8595 9.08101L6 11.2178L8.1405 9.07501L7.6095 8.54401L6.375 9.78226ZM9.075 3.85726L8.544 4.38826L9.78225 5.62501H7.5V6.37501H9.78225L8.55 7.60951L9.081 8.14051L11.2177 6.00001L9.075 3.85726Z" fill="#C5C5C5"/> -</svg> diff --git a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg b/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg deleted file mode 100644 index 906f8244eb8b..000000000000 --- a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>PopIn_16x</title><g ><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M16,0V15H13V5.121L7.121,11H12v3H2V4H5V8.879L10.879,3H1V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM11.146,4.146,4,11.293V5H3v8h8V12H4.707l7.147-7.146Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg deleted file mode 100644 index 041b6e015305..000000000000 --- a/src/datascience-ui/react-common/images/PopIn/PopIn_16x_vscode_dark.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>PopIn_16x</title><g ><path class="icon-canvas-transparent" d="M16,16H0V0H16Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M16,0V15H13V5.121L7.121,11H12v3H2V4H5V8.879L10.879,3H1V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM11.146,4.146,4,11.293V5H3v8h8V12H4.707l7.147-7.146Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg b/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg deleted file mode 100644 index e979568ea576..000000000000 --- a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>PopOut_16x</title><g ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M2,4H12V14H9V9.121L2.854,15.268.732,13.146,6.879,7H2ZM1,0V3H13V15h3V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM3,6H9.293L2.146,13.146l.708.708L10,6.707V13h1V5H3Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg deleted file mode 100644 index a72e56c2dc70..000000000000 --- a/src/datascience-ui/react-common/images/PopOut/PopOut_16x_vscode_dark.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#252526;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#c5c5c5;}</style></defs><title>PopOut_16x</title><g ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g style="display: none;"><path class="icon-vs-out" d="M2,4H12V14H9V9.121L2.854,15.268.732,13.146,6.879,7H2ZM1,0V3H13V15h3V0Z" style="display: none;"/></g><g ><path class="icon-vs-bg" d="M15,1V14H14V2H2V1ZM3,6H9.293L2.146,13.146l.708.708L10,6.707V13h1V5H3Z"/></g></svg> \ No newline at end of file diff --git a/src/datascience-ui/react-common/images/Prev/previous-inverse.svg b/src/datascience-ui/react-common/images/Prev/previous-inverse.svg deleted file mode 100644 index f43fba8ed52e..000000000000 --- a/src/datascience-ui/react-common/images/Prev/previous-inverse.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.5 0L9.55572 1.05473L4.61045 6L9.55572 10.9453L8.5 12L2.5 6L8.5 0Z" fill="#CCCCCC"/> -</svg> diff --git a/src/datascience-ui/react-common/images/Prev/previous.svg b/src/datascience-ui/react-common/images/Prev/previous.svg deleted file mode 100644 index 01c558dee9d9..000000000000 --- a/src/datascience-ui/react-common/images/Prev/previous.svg +++ /dev/null @@ -1,3 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<path d="M8.5 -8.41007e-08L9.55572 1.05473L4.61045 6L9.55572 10.9453L8.5 12L2.5 6L8.5 -8.41007e-08Z" fill="#424242"/> -</svg> diff --git a/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode.svg b/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode.svg deleted file mode 100644 index 2541acf74b99..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Redo/Redo_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Redo/Redo_16x_vscode_dark.svg deleted file mode 100644 index 8ed72f5b2b1f..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Restart/Restart_grey_16x_vscode.svg b/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode.svg deleted file mode 100644 index f8aa67f86ec1..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Restart/Restart_grey_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Restart/Restart_grey_16x_vscode_dark.svg deleted file mode 100644 index beb0607466aa..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/SaveAs/SaveAs_16x_vscode.svg b/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode.svg deleted file mode 100644 index b056b5799b92..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/SaveAs/SaveAs_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/SaveAs/SaveAs_16x_vscode_dark.svg deleted file mode 100644 index f5d68d93cd9f..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Undo/Undo_16x_vscode.svg b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode.svg deleted file mode 100644 index b956d054fdfd..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Undo/Undo_16x_vscode_dark.svg b/src/datascience-ui/react-common/images/Undo/Undo_16x_vscode_dark.svg deleted file mode 100644 index b61f05a6faf7..000000000000 --- a/src/datascience-ui/react-common/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 ><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g 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 ><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/react-common/images/Zoom/zoom.svg b/src/datascience-ui/react-common/images/Zoom/zoom.svg deleted file mode 100644 index 49fb5254f04f..000000000000 --- a/src/datascience-ui/react-common/images/Zoom/zoom.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0)"> -<path d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z" fill="#424242"/> -<path d="M6.75 4.5V5.25H5.25V6.75H4.5V5.25H3V4.5H4.5V3H5.25V4.5H6.75Z" fill="#00539C"/> -</g> -<defs> -<clipPath id="clip0"> -<rect width="12" height="12" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg b/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg deleted file mode 100644 index c7e06120a875..000000000000 --- a/src/datascience-ui/react-common/images/Zoom/zoom_inverse.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0)"> -<path d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z" fill="#C5C5C5"/> -<path d="M6.75 4.5V5.25H5.25V6.75H4.5V5.25H3V4.5H4.5V3H5.25V4.5H6.75Z" fill="#75BEFF"/> -</g> -<defs> -<clipPath id="clip0"> -<rect width="12" height="12" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg b/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg deleted file mode 100644 index 3eee2733f99f..000000000000 --- a/src/datascience-ui/react-common/images/ZoomOut/zoomout.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0)"> -<path d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z" fill="#424242"/> -<path d="M6.75 4.5V5.25H3V4.5H6.75Z" fill="#00539C"/> -</g> -<defs> -<clipPath id="clip0"> -<rect width="12" height="12" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg b/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg deleted file mode 100644 index bea47b6b24d2..000000000000 --- a/src/datascience-ui/react-common/images/ZoomOut/zoomout_inverse.svg +++ /dev/null @@ -1,11 +0,0 @@ -<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg"> -<g clip-path="url(#clip0)"> -<path d="M11.1405 10.6095L8.043 7.51276C8.72354 6.69576 9.06291 5.64783 8.9905 4.58698C8.91808 3.52613 8.43947 2.53403 7.65421 1.81708C6.86896 1.10013 5.83753 0.713516 4.77448 0.73767C3.71144 0.761824 2.69863 1.19489 1.94675 1.94677C1.19487 2.69865 0.761809 3.71145 0.737655 4.7745C0.7135 5.83754 1.10011 6.86897 1.81707 7.65423C2.53402 8.43948 3.52611 8.9181 4.58696 8.99051C5.64781 9.06293 6.69574 8.72356 7.51275 8.04301L10.6095 11.1405C10.6444 11.1754 10.6858 11.203 10.7313 11.2219C10.7769 11.2408 10.8257 11.2505 10.875 11.2505C10.9243 11.2505 10.9731 11.2408 11.0187 11.2219C11.0642 11.203 11.1056 11.1754 11.1405 11.1405C11.1754 11.1056 11.203 11.0643 11.2219 11.0187C11.2408 10.9731 11.2505 10.9243 11.2505 10.875C11.2505 10.8257 11.2408 10.7769 11.2219 10.7313C11.203 10.6858 11.1754 10.6444 11.1405 10.6095ZM4.875 8.25001C4.20749 8.25001 3.55496 8.05207 2.99995 7.68122C2.44493 7.31037 2.01235 6.78327 1.7569 6.16657C1.50146 5.54987 1.43462 4.87127 1.56485 4.21658C1.69507 3.5619 2.01651 2.96053 2.48851 2.48853C2.96051 2.01653 3.56188 1.69509 4.21657 1.56486C4.87125 1.43464 5.54985 1.50147 6.16655 1.75692C6.78325 2.01237 7.31036 2.44495 7.68121 2.99996C8.05206 3.55498 8.25 4.2075 8.25 4.87501C8.24901 5.76981 7.89311 6.62768 7.26039 7.2604C6.62767 7.89312 5.7698 8.24902 4.875 8.25001Z" fill="#C5C5C5"/> -<path d="M6.75 4.5V5.25H3V4.5H6.75Z" fill="#75BEFF"/> -</g> -<defs> -<clipPath id="clip0"> -<rect width="12" height="12" fill="white"/> -</clipPath> -</defs> -</svg> diff --git a/src/datascience-ui/react-common/locReactSide.ts b/src/datascience-ui/react-common/locReactSide.ts deleted file mode 100644 index 8c0689ed903c..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() : Record<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: Record<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/monacoEditor.css b/src/datascience-ui/react-common/monacoEditor.css deleted file mode 100644 index 9e1b8451699a..000000000000 --- a/src/datascience-ui/react-common/monacoEditor.css +++ /dev/null @@ -1,108 +0,0 @@ -.measure-width-div { - width: 100vw; - visibility: hidden; - position: absolute; -} - -.monaco-editor-outer-container .mtk1 { - /* For some reason the monaco editor refuses to update this style no matter the theme. It's always black */ - color: var(--override-foreground, var(--vscode-editor-foreground)); -} - -.monaco-editor .mtk1 { - /* For some reason the monaco editor refuses to update this style no matter the theme. It's always black */ - color: var(--override-foreground, var(--vscode-editor-foreground)); -} - -/* Bunch of styles copied from vscode. Handles the hover window */ - - .monaco-editor-hover { - cursor: default; - position: absolute; - overflow: hidden; - z-index: 50; - -webkit-user-select: text; - -ms-user-select: text; - -khtml-user-select: text; - -moz-user-select: text; - -o-user-select: text; - user-select: text; - box-sizing: initial; - animation: fadein 100ms linear; - line-height: 1.5em; -} - -.monaco-editor-hover.hidden { - display: none; -} - -.monaco-editor-hover .hover-contents { - padding: 4px 8px; -} - -.monaco-editor-hover .markdown-hover > .hover-contents:not(.code-hover-contents) { - max-width: 500px; -} - -.monaco-editor-hover p, -.monaco-editor-hover ul { - margin: 8px 0; -} - -.monaco-editor-hover hr { - margin-top: 4px; - margin-bottom: -6px; - margin-left: -10px; - margin-right: -10px; - height: 1px; -} - -.monaco-editor-hover p:first-child, -.monaco-editor-hover ul:first-child { - margin-top: 0; -} - -.monaco-editor-hover p:last-child, -.monaco-editor-hover ul:last-child { - margin-bottom: 0; -} - -.monaco-editor-hover ul { - padding-left: 20px; -} - -.monaco-editor-hover li > p { - margin-bottom: 0; -} - -.monaco-editor-hover li > ul { - margin-top: 0; -} - -.monaco-editor-hover code { - border-radius: 3px; - padding: 0 0.4em; -} - -.monaco-editor-hover .monaco-tokenized-source { - white-space: pre-wrap; - word-break: break-all; -} - -.monaco-editor-hover .hover-row.status-bar { - font-size: 12px; - line-height: 22px; -} - -.monaco-editor-hover .hover-row.status-bar .actions { - display: flex; -} - -.monaco-editor-hover .hover-row.status-bar .actions .action-container { - margin: 0px 8px; - cursor: pointer; -} - -.monaco-editor-hover .hover-row.status-bar .actions .action-container .action .icon { - padding-right: 4px; -} diff --git a/src/datascience-ui/react-common/monacoEditor.tsx b/src/datascience-ui/react-common/monacoEditor.tsx deleted file mode 100644 index 8e48304b8eaa..000000000000 --- a/src/datascience-ui/react-common/monacoEditor.tsx +++ /dev/null @@ -1,428 +0,0 @@ - -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as React from 'react'; -import { IDisposable } from '../../client/common/types'; - -import './monacoEditor.css'; - -const LINE_HEIGHT = 18; - -export interface IMonacoEditorProps { - language: string; - value: string; - theme?: string; - outermostParentClass: string; - options: monacoEditor.editor.IEditorConstructionOptions; - testMode?: boolean; - forceBackground?: string; - editorMounted(editor: monacoEditor.editor.IStandaloneCodeEditor): void; - openLink(uri: monacoEditor.Uri): void; -} - -interface IMonacoEditorState { - editor?: monacoEditor.editor.IStandaloneCodeEditor; - model: monacoEditor.editor.ITextModel | null; -} - -// Need this to prevent wiping of the current value on a componentUpdate. react-monaco-editor has that problem. - -export class MonacoEditor extends React.Component<IMonacoEditorProps, IMonacoEditorState> { - private containerRef: React.RefObject<HTMLDivElement>; - private measureWidthRef: React.RefObject<HTMLDivElement>; - private resizeTimer?: number; - private leaveTimer?: number; - private subscriptions: monacoEditor.IDisposable[] = []; - private widgetParent: HTMLDivElement | undefined; - private outermostParent: HTMLElement | null = null; - private enteredHover: boolean = false; - private lastOffsetLeft: number | undefined; - private lastOffsetTop: number | undefined; - constructor(props: IMonacoEditorProps) { - super(props); - this.state = { editor: undefined, model: null }; - this.containerRef = React.createRef<HTMLDivElement>(); - this.measureWidthRef = React.createRef<HTMLDivElement>(); - } - - public componentDidMount = () => { - if (window) { - window.addEventListener('resize', this.windowResized); - } - if (this.containerRef.current) { - // Compute our outermost parent - let outerParent = this.containerRef.current.parentElement; - while (outerParent && !outerParent.classList.contains(this.props.outermostParentClass)) { - outerParent = outerParent.parentElement; - } - this.outermostParent = outerParent; - if (this.outermostParent) { - this.outermostParent.addEventListener('mouseleave', this.outermostParentLeave); - } - - // Create the editor - const editor = monacoEditor.editor.create(this.containerRef.current, - { - value: this.props.value, - language: this.props.language, - ...this.props.options - }); - - // Force the editor to behave like a unix editor as - // all of our code is assuming that. - const model = editor.getModel(); - if (model) { - model.setEOL(monacoEditor.editor.EndOfLineSequence.LF); - } - - // Register a link opener so when a user clicks on a link we can navigate to it. - // tslint:disable-next-line: no-any - const openerService = (editor.getContribution('editor.linkDetector') as any).openerService; - if (openerService && openerService.open) { - openerService.open = this.props.openLink; - } - - // Save the editor and the model in our state. - this.setState({ editor, model }); - if (this.props.theme) { - monacoEditor.editor.setTheme(this.props.theme); - } - - // do the initial set of the height (wait a bit) - this.windowResized(); - - // on each edit recompute height (wait a bit) - this.subscriptions.push(editor.onDidChangeModelDecorations(() => { - this.windowResized(); - })); - - // List for key down events - this.subscriptions.push(editor.onKeyDown(this.onKeyDown)); - - // Setup our context menu to show up outside. Autocomplete doesn't have this problem so it just works - this.subscriptions.push(editor.onContextMenu((e) => { - if (this.state.editor) { - const domNode = this.state.editor.getDomNode(); - const contextMenuElement = domNode ? domNode.querySelector('.monaco-menu-container') as HTMLElement : null; - if (contextMenuElement) { - const posY = (e.event.posy + contextMenuElement.clientHeight) > window.outerHeight - ? e.event.posy - contextMenuElement.clientHeight - : e.event.posy; - const posX = (e.event.posx + contextMenuElement.clientWidth) > window.outerWidth - ? e.event.posx - contextMenuElement.clientWidth - : e.event.posx; - contextMenuElement.style.position = 'fixed'; - contextMenuElement.style.top = `${Math.max(0, Math.floor(posY))}px`; - contextMenuElement.style.left = `${Math.max(0, Math.floor(posX))}px`; - } - } - })); - - // Make sure our suggest and hover windows show up on top of other stuff - this.updateWidgetParent(editor); - - // If we're readonly, monaco is not putting the aria-readonly property on the textarea - // We should do that - if (this.props.options.readOnly) { - this.setAriaReadOnly(editor); - } - - // Eliminate the find action if possible - // tslint:disable-next-line: no-any - const editorAny = editor as any; - if (editorAny._standaloneKeybindingService) { - editorAny._standaloneKeybindingService.addDynamicKeybinding('-actions.find'); - } - - // Tell our parent the editor is ready to use - this.props.editorMounted(editor); - } - } - - public componentWillUnmount = () => { - if (this.resizeTimer) { - window.clearTimeout(this.resizeTimer); - } - - if (window) { - window.removeEventListener('resize', this.windowResized); - } - - if (this.outermostParent) { - this.outermostParent.removeEventListener('mouseleave', this.outermostParentLeave); - this.outermostParent = null; - } - if (this.widgetParent) { - this.widgetParent.remove(); - } - - this.subscriptions.forEach(d => d.dispose()); - if (this.state.editor) { - this.state.editor.dispose(); - } - } - - public componentDidUpdate(prevProps: IMonacoEditorProps, prevState: IMonacoEditorState) { - if (this.state.editor) { - if (prevProps.language !== this.props.language && this.state.model) { - monacoEditor.editor.setModelLanguage(this.state.model, this.props.language); - } - if (prevProps.theme !== this.props.theme && this.props.theme) { - monacoEditor.editor.setTheme(this.props.theme); - } - if (prevProps.options !== this.props.options) { - this.state.editor.updateOptions(this.props.options); - } - if (prevProps.value !== this.props.value && this.state.model) { - this.state.model.setValue(this.props.value); - } - } - - this.updateEditorSize(); - - // If this is our first time setting the editor, we might need to dynanically modify the styles - // that the editor generates for the background colors. - if (!prevState.editor && this.state.editor && this.containerRef.current && this.props.forceBackground) { - this.updateBackgroundStyle(); - } - } - - public render() { - return ( - <div className='monaco-editor-outer-container' ref={this.containerRef}> - <div className='monaco-editor-container' /> - <div className='measure-width-div' ref={this.measureWidthRef} /> - </div> - ); - } - - public isSuggesting() : boolean { - // This should mean our widgetParent has some height - if (this.widgetParent && this.widgetParent.firstChild && this.widgetParent.firstChild.childNodes.length >= 2) { - const suggestWidget = this.widgetParent.firstChild.childNodes.item(1) as HTMLDivElement; - const signatureHelpWidget = this.widgetParent.firstChild.childNodes.length > 2 ? this.widgetParent.firstChild.childNodes.item(2) as HTMLDivElement : undefined; - const suggestVisible = suggestWidget ? suggestWidget.className.includes('visible') : false; - const signatureVisible = signatureHelpWidget ? signatureHelpWidget.className.includes('visible') : false; - return suggestVisible || signatureVisible; - } - return false; - } - - private setAriaReadOnly(editor: monacoEditor.editor.IStandaloneCodeEditor) { - const editorDomNode = editor.getDomNode(); - if (editorDomNode) { - const textArea = editorDomNode.getElementsByTagName('textarea'); - if (textArea && textArea.length > 0) { - const item = textArea.item(0); - if (item) { - item.setAttribute('aria-readonly', 'true'); - } - } - } - } - - private windowResized = () => { - if (this.resizeTimer) { - clearTimeout(this.resizeTimer); - } - this.resizeTimer = window.setTimeout(this.updateEditorSize, 0); - } - - private startUpdateWidgetPosition = () => { - this.updateWidgetPosition(); - } - - private onKeyDown = (e: monacoEditor.IKeyboardEvent) => { - if (e.keyCode === monacoEditor.KeyCode.Escape) { - // Shift Escape is special, so it doesn't work as going backwards. - // For now just support escape to get out of a cell (like Jupyter does) - const nextElement = this.findTabStop(1); - if (nextElement) { - nextElement.focus(); - } - } - } - - private findTabStop(direction: number) : HTMLElement | undefined { - if (this.state.editor) { - const editorDomNode = this.state.editor.getDomNode(); - if (editorDomNode) { - const textArea = editorDomNode.getElementsByTagName('textarea'); - const allFocusable = document.querySelectorAll('input, button, select, textarea, a[href]'); - if (allFocusable && textArea && textArea.length > 0) { - const tabable = Array.prototype.filter.call(allFocusable, (i: HTMLElement) => i.tabIndex >= 0); - const self = tabable.indexOf(textArea.item(0)); - return direction >= 0 ? tabable[self + 1] || tabable[0] : tabable[self - 1] || tabable[0]; - } - } - } - } - - private updateBackgroundStyle = () => { - if (this.state.editor && this.containerRef.current && this.props.forceBackground) { - const nodes = this.containerRef.current.getElementsByClassName('monaco-editor-background'); - if (nodes && nodes.length > 0) { - const backgroundNode = nodes[0] as HTMLDivElement; - if (backgroundNode && backgroundNode.style) { - backgroundNode.style.backgroundColor = this.props.forceBackground; - } - } - } - } - - private updateWidgetPosition(width?: number) { - if (this.state.editor && this.widgetParent) { - // Position should be at the top of the editor. - const editorDomNode = this.state.editor.getDomNode(); - if (editorDomNode) { - const rect = editorDomNode.getBoundingClientRect(); - if (rect && - (rect.left !== this.lastOffsetLeft || rect.top !== this.lastOffsetTop)) { - this.lastOffsetLeft = rect.left; - this.lastOffsetTop = rect.top; - - this.widgetParent.setAttribute( - 'style', - `position: absolute; left: ${rect.left}px; top: ${rect.top}px; width:${width ? width : rect.width}px`); - } - } - } - } - - private updateEditorSize = () => { - if (this.measureWidthRef.current && - this.measureWidthRef.current.clientWidth && - this.containerRef.current && - this.containerRef.current.parentElement && - this.state.editor && - this.state.model) { - const editorDomNode = this.state.editor.getDomNode(); - if (!editorDomNode) { return; } - const container = editorDomNode.getElementsByClassName('view-lines')[0] as HTMLElement; - const lineHeight = container.firstChild - ? (container.firstChild as HTMLElement).offsetHeight - : LINE_HEIGHT; - const currLineCount = this.state.model.getLineCount(); - const height = (currLineCount * lineHeight) + 3; // Fudge factor - const width = this.measureWidthRef.current.clientWidth - this.containerRef.current.parentElement.offsetLeft - 15; // Leave room for the scroll bar in regular cell table - - // For some reason this is flashing. Need to debug the editor code to see if - // it draws more than once. Or if we can have React turn off DOM updates - this.state.editor.layout({ width: width, height: height }); - - // Also need to update our widget positions - this.updateWidgetPosition(width); - } - } - - private onHoverLeave = () => { - // If the hover is active, make sure to hide it. - if (this.state.editor && this.widgetParent) { - this.enteredHover = false; - // tslint:disable-next-line: no-any - const hover = this.state.editor.getContribution('editor.contrib.hover') as any; - if (hover._hideWidgets) { - hover._hideWidgets(); - } - } - } - - private onHoverEnter = () => { - if (this.state.editor && this.widgetParent) { - // If we enter the hover, indicate it so we don't leave - this.enteredHover = true; - } - } - - private outermostParentLeave = () => { - // Have to bounce this because the leave for the cell is the - // enter for the hover - if (this.leaveTimer) { - clearTimeout(this.leaveTimer); - } - this.leaveTimer = window.setTimeout(this.outermostParentLeaveBounced, 0); - } - - private outermostParentLeaveBounced = () => { - if (this.state.editor && !this.enteredHover) { - // If we haven't already entered hover, then act like it shuts down - this.onHoverLeave(); - } - } - - private updateWidgetParent(editor: monacoEditor.editor.IStandaloneCodeEditor) { - // Reparent the hover widgets. They cannot be inside anything that has overflow hidden or scrolling or they won't show - // up overtop of anything. Warning, this is a big hack. If the class name changes or the logic - // for figuring out the position of hover widgets changes, this won't work anymore. - // appendChild on a DOM node moves it, but doesn't clone it. - // https://developer.mozilla.org/en-US/docs/Web/API/Node/appendChild - const editorNode = editor.getDomNode(); - if (editorNode) { - try { - const elements = editorNode.getElementsByClassName('overflowingContentWidgets'); - if (elements && elements.length) { - const contentWidgets = elements[0] as HTMLDivElement; - if (contentWidgets) { - // Go up to the document. - const document = contentWidgets.getRootNode() as HTMLDocument; - - // His first child with the id 'root' should be where we want to parent our overflow widgets - if (document && document.getElementById) { - const root = document.getElementById('root'); - if (root) { - // We need to create a dummy 'monaco-editor' div so that the content widgets get the same styles. - this.widgetParent = document.createElement('div', {}); - this.widgetParent.setAttribute('class', `${editorNode.className} monaco-editor-pretend-parent`); - - // We also need to make sure its position follows the editor around on the screen. - const rect = editorNode.getBoundingClientRect(); - if (rect) { - this.lastOffsetLeft = rect.left; - this.lastOffsetTop = rect.top; - this.widgetParent.setAttribute( - 'style', - `position: absolute; left: ${rect.left}px; top: ${rect.top}px`); - } - - root.appendChild(this.widgetParent); - this.widgetParent.appendChild(contentWidgets); - - // Listen for changes so we can update the position dynamically - editorNode.addEventListener('mouseenter', this.startUpdateWidgetPosition); - - // We also need to trick the editor into thinking mousing over the hover does not - // mean the mouse has left the editor. - // tslint:disable-next-line: no-any - const hover = editor.getContribution('editor.contrib.hover') as any; - if (hover._toUnhook && hover._toUnhook.length === 8 && hover.contentWidget) { - // This should mean our 5th element is the event handler for mouse leave. Remove it. - const array = hover._toUnhook as IDisposable[]; - array[5].dispose(); - array.splice(5, 1); - - // Instead listen to mouse leave for our hover widget - const hoverWidget = this.widgetParent.getElementsByClassName('monaco-editor-hover')[0] as HTMLElement; - if (hoverWidget) { - hoverWidget.addEventListener('mouseenter', this.onHoverEnter); - hoverWidget.addEventListener('mouseleave', this.onHoverLeave); - } - } - } - } - } - } - } catch (e) { - // If something fails, then the hover will just work inside the main frame - if (!this.props.testMode) { - window.console.warn(`Error moving editor widgets: ${e}`); - } - - // Make sure we don't try moving it around. - this.widgetParent = undefined; - } - } - } -} diff --git a/src/datascience-ui/react-common/postOffice.ts b/src/datascience-ui/react-common/postOffice.ts deleted file mode 100644 index 017c7def8c0b..000000000000 --- a/src/datascience-ui/react-common/postOffice.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { WebPanelMessage } from '../../client/common/application/types'; -import { IDisposable } from '../../client/common/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; -} - -// This special function talks to vscode from a web panel -export declare function acquireVsCodeApi(): IVsCodeApi; - -// tslint:disable-next-line: no-unnecessary-class -export class PostOffice implements IDisposable { - - private registered: boolean = false; - private vscodeApi : IVsCodeApi | undefined; - private handlers: IMessageHandler[] = []; - private baseHandler = this.handleMessages.bind(this); - - public dispose() { - if (this.registered) { - this.registered = false; - window.removeEventListener('message', this.baseHandler); - } - } - - public sendMessage<M, T extends keyof M>(type: T, payload?: M[T]) { - const api = this.acquireApi(); - if (api) { - api.postMessage({ type: type.toString(), payload }); - } - } - - // tslint:disable-next-line:no-any - public sendUnsafeMessage(type: string, payload?: any) { - const api = this.acquireApi(); - if (api) { - api.postMessage({ type: type, payload }); - } - } - - public addHandler(handler: IMessageHandler) { - // Acquire here too so that the message handlers are setup during tests. - this.acquireApi(); - this.handlers.push(handler); - } - - public removeHandler(handler: IMessageHandler) { - this.handlers = this.handlers.filter(f => f !== handler); - } - - private acquireApi() : IVsCodeApi | undefined { - // Only do this once as it crashes if we ask more than once - // tslint:disable-next-line:no-typeof-undefined - if (!this.vscodeApi && typeof acquireVsCodeApi !== 'undefined') { - this.vscodeApi = acquireVsCodeApi(); - } - if (!this.registered) { - this.registered = true; - window.addEventListener('message', this.baseHandler); - } - - return this.vscodeApi; - } - - private async handleMessages(ev: MessageEvent) { - if (this.handlers) { - const msg = ev.data as WebPanelMessage; - if (msg) { - this.handlers.forEach((h : IMessageHandler | null) => { - if (h) { - h.handleMessage(msg.type, msg.payload); - } - }); - } - } - } -} diff --git a/src/datascience-ui/react-common/progress.css b/src/datascience-ui/react-common/progress.css deleted file mode 100644 index c5802e7ee863..000000000000 --- a/src/datascience-ui/react-common/progress.css +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - - .monaco-progress-container { - width: 100%; - height: 5px; - overflow: hidden; /* keep progress bit in bounds */ - position: fixed; - z-index: 10; -} - -.monaco-progress-container .progress-bit { - width: 2%; - height: 5px; - position: absolute; - left: 0; - display: none; - background-color:var(--vscode-editorSuggestWidget-highlightForeground); -} - -.monaco-progress-container.active .progress-bit { - display: inherit; -} - -.monaco-progress-container.discrete .progress-bit { - left: 0; - transition: width 100ms linear; - -webkit-transition: width 100ms linear; - -o-transition: width 100ms linear; - -moz-transition: width 100ms linear; - -ms-transition: width 100ms linear; -} - -.monaco-progress-container.discrete.done .progress-bit { - width: 100%; -} - -.monaco-progress-container.infinite .progress-bit { - animation-name: progress; - animation-duration: 4s; - animation-iteration-count: infinite; - animation-timing-function: linear; - -ms-animation-name: progress; - -ms-animation-duration: 4s; - -ms-animation-iteration-count: infinite; - -ms-animation-timing-function: linear; - -webkit-animation-name: progress; - -webkit-animation-duration: 4s; - -webkit-animation-iteration-count: infinite; - -webkit-animation-timing-function: linear; - -moz-animation-name: progress; - -moz-animation-duration: 4s; - -moz-animation-iteration-count: infinite; - -moz-animation-timing-function: linear; - will-change: transform; -} - -/** - * The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of - * that container. Since translateX is relative to the progress bit size, we have to multiple it with - * its relative size to the parent container: - * 50%: 50 * 50 = 2500% - * 100%: 50 * 100 - 50 (do not overflow): 4950% - */ -@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } -@-ms-keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } -@-webkit-keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } -@-moz-keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } } diff --git a/src/datascience-ui/react-common/progress.tsx b/src/datascience-ui/react-common/progress.tsx deleted file mode 100644 index d545adf43cc6..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/settingsReactSide.ts b/src/datascience-ui/react-common/settingsReactSide.ts deleted file mode 100644 index fcf6ca1b8679..000000000000 --- a/src/datascience-ui/react-common/settingsReactSide.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IDataScienceExtraSettings } from '../../client/datascience/types'; - -// The WebPanel constructed by the extension should inject a getInitialSettings function into -// the script. This should return a dictionary of key value pairs for settings -// tslint:disable-next-line:no-any -export declare function getInitialSettings(): any; - -let loadedSettings: IDataScienceExtraSettings; - -export function getSettings() : IDataScienceExtraSettings { - if (loadedSettings === undefined) { - load(); - } - - return loadedSettings; -} - -export function updateSettings(jsonSettingsString: string) { - const newSettings = JSON.parse(jsonSettingsString); - loadedSettings = <IDataScienceExtraSettings>newSettings; -} - -function load() { - // tslint:disable-next-line:no-typeof-undefined - if (typeof getInitialSettings !== 'undefined') { - loadedSettings = <IDataScienceExtraSettings>getInitialSettings(); - } else { - // Default settings for tests - loadedSettings = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - allowInput: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - enablePlotViewer: true, - extraSettings: { - editorCursor: 'line', - editorCursorBlink: 'blink', - theme: 'Default Dark+' - }, - intellisenseOptions: { - quickSuggestions: { - other: true, - comments: false, - strings: false - }, - acceptSuggestionOnEnter: 'on', - quickSuggestionsDelay: 10, - suggestOnTriggerCharacters: true, - tabCompletion: 'on', - suggestLocalityBonus: true, - suggestSelection: 'recentlyUsed', - wordBasedSuggestions: true, - parameterHintsEnabled: true - } - }; - } -} diff --git a/src/datascience-ui/react-common/styleInjector.tsx b/src/datascience-ui/react-common/styleInjector.tsx deleted file mode 100644 index c71bb48bfe42..000000000000 --- a/src/datascience-ui/react-common/styleInjector.tsx +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as React from 'react'; - -import { CssMessages, IGetCssResponse, SharedMessages } from '../../client/datascience/messages'; -import { IGetMonacoThemeResponse } from '../../client/datascience/monacoMessages'; -import { IDataScienceExtraSettings } from '../../client/datascience/types'; -import { IMessageHandler, PostOffice } from './postOffice'; -import { getSettings } from './settingsReactSide'; -import { detectBaseTheme } from './themeDetector'; - -export interface IStyleInjectorProps { - expectingDark: boolean; - postOffice: PostOffice; - darkChanged?(newDark: boolean): void; - monacoThemeChanged?(theme: string): void; -} - -interface IStyleInjectorState { - rootCss?: string; - theme?: string; - knownDark?: boolean; -} - -export class StyleInjector extends React.Component<IStyleInjectorProps, IStyleInjectorState> implements IMessageHandler { - - constructor(props: IStyleInjectorProps) { - super(props); - this.state = { rootCss: undefined, theme: undefined }; - } - - public componentWillMount() { - // Add ourselves as a handler for the post office - this.props.postOffice.addHandler(this); - } - - public componentWillUnmount() { - // Remove ourselves as a handler for the post office - this.props.postOffice.removeHandler(this); - } - - public componentDidMount() { - if (!this.state.rootCss) { - // Set to a temporary value. - this.setState({rootCss: ' '}); - this.props.postOffice.sendUnsafeMessage(CssMessages.GetCssRequest, { isDark: this.props.expectingDark }); - this.props.postOffice.sendUnsafeMessage(CssMessages.GetMonacoThemeRequest, { isDark: this.props.expectingDark }); - } - } - - public render() { - return ( - <div className='styleSetter'> - <style> - {this.state.rootCss} - </style> - {this.props.children} - </div> - ); - } - - // tslint:disable-next-line:no-any - public handleMessage = (msg: string, payload?: any) : boolean => { - switch (msg) { - case CssMessages.GetCssResponse: - this.handleCssResponse(payload); - break; - - case CssMessages.GetMonacoThemeResponse: - this.handleMonacoThemeResponse(payload); - break; - - case SharedMessages.UpdateSettings: - this.updateSettings(payload); - break; - - default: - break; - } - - return true; - } - - // tslint:disable-next-line:no-any - private handleCssResponse(payload?: any) { - const response = payload as IGetCssResponse; - if (response && response.css) { - - // Recompute our known dark value from the class name in the body - // VS code should update this dynamically when the theme changes - const computedKnownDark = this.computeKnownDark(); - - // We also get this in our response, but computing is more reliable - // than searching for it. - - if (this.state.knownDark !== computedKnownDark && - this.props.darkChanged) { - this.props.darkChanged(computedKnownDark); - } - - this.setState({ - rootCss: response.css, - theme: response.theme, - knownDark: computedKnownDark - }); - } - } - - // tslint:disable-next-line: no-any - private handleMonacoThemeResponse(payload?: any) { - const response = payload as IGetMonacoThemeResponse; - if (response && response.theme) { - - // Tell monaco we have a new theme. THis is like a state update for monaco - monacoEditor.editor.defineTheme('interactiveWindow', response.theme); - - // Tell the main panel we have a theme now - if (this.props.monacoThemeChanged) { - this.props.monacoThemeChanged('interactiveWindow'); - } - } - } - - // tslint:disable-next-line:no-any - private updateSettings(payload: any) { - if (payload) { - const newSettings = JSON.parse(payload as string); - const dsSettings = newSettings as IDataScienceExtraSettings; - if (dsSettings && dsSettings.extraSettings && dsSettings.extraSettings.theme !== this.state.theme) { - // User changed the current theme. Rerender - this.props.postOffice.sendUnsafeMessage(CssMessages.GetCssRequest, { isDark: this.computeKnownDark() }); - this.props.postOffice.sendUnsafeMessage(CssMessages.GetMonacoThemeRequest, { isDark: this.computeKnownDark() }); - } - } - } - - private computeKnownDark() : boolean { - const ignore = getSettings && getSettings().ignoreVscodeTheme ? true : false; - const baseTheme = ignore ? 'vscode-light' : detectBaseTheme(); - return baseTheme !== 'vscode-light'; - } -} diff --git a/src/datascience-ui/react-common/svgList.css b/src/datascience-ui/react-common/svgList.css deleted file mode 100644 index 1c7761842c46..000000000000 --- a/src/datascience-ui/react-common/svgList.css +++ /dev/null @@ -1,32 +0,0 @@ -.svg-list-container { - width: 100%; - height: 100px; - margin: 10px; -} - -.svg-list { - list-style-type: none; - overflow-x: scroll; - overflow-y: hidden; - white-space: nowrap; - padding-inline-start: 0px; -} - -.svg-list-item { - width: 100px; - height: 100%; - border: 1px solid transparent; - display: inline-block; - overflow: hidden; -} - -.svg-list-item-selected { - border-color: var(--vscode-list-highlightForeground); - border-style: solid; - border-width: 1px; -} - -.svg-list-item-image { - width: 100px; - height: 100px; -} \ No newline at end of file diff --git a/src/datascience-ui/react-common/svgList.tsx b/src/datascience-ui/react-common/svgList.tsx deleted file mode 100644 index d2c0bacd3136..000000000000 --- a/src/datascience-ui/react-common/svgList.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as React from 'react'; -import { SvgLoader } from 'react-svgmt'; -import { getLocString } from '../react-common/locReactSide'; - -import './svgList.css'; - -interface ISvgListProps { - images: string[]; - currentImage: number; - imageClicked(index: number): void; -} - -export class SvgList extends React.Component<ISvgListProps> { - constructor(props: ISvgListProps) { - super(props); - } - - public render() { - return ( - <div className='svg-list-container'> - <div className='svg-list'> - {this.renderImages()} - </div> - </div> - ); - } - - private renderImages() { - return this.props.images.map((image, index) => { - const className = index === this.props.currentImage ? 'svg-list-item svg-list-item-selected' : 'svg-list-item'; - const ariaLabel = index === this.props.currentImage ? getLocString('DataScience.selectedImageListLabel', 'Selected Image') : getLocString('DataScience.selectedImageLabel', 'Image'); - const ariaPressed = index === this.props.currentImage ? 'true' : 'false'; - const clickHandler = () => this.props.imageClicked(index); - const keyDownHandler = (e: React.KeyboardEvent<HTMLDivElement>) => this.onKeyDown(e, index); - return ( - // See the comments here: https://github.com/Microsoft/tslint-microsoft-contrib/issues/676 - // tslint:disable-next-line: react-this-binding-issue - <div className={className} tabIndex={0} role='button' aria-label={ariaLabel} aria-pressed={ariaPressed} onClick={clickHandler} onKeyDown={keyDownHandler} key={index}> - <div className='svg-list-item-image'> - <SvgLoader svgXML={image}> - </SvgLoader> - </div> - </div> - ); - }); - } - - private onKeyDown = (event: React.KeyboardEvent<HTMLDivElement>, index: number) => { - // Enter and Space commit an action the same as a click does - if (event.key === 'Enter' || event.key === ' ') { - this.props.imageClicked(index); - } - } -} diff --git a/src/datascience-ui/react-common/svgViewer.tsx b/src/datascience-ui/react-common/svgViewer.tsx deleted file mode 100644 index 89e7063d1e11..000000000000 --- a/src/datascience-ui/react-common/svgViewer.tsx +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as React from 'react'; -import { POSITION_TOP, ReactSVGPanZoom, Tool, Value } from 'react-svg-pan-zoom'; -import { SvgLoader } from 'react-svgmt'; -import { AutoSizer } from 'react-virtualized'; -import './svgViewer.css'; - -interface ISvgViewerProps { - svg: string; - id: string; // Unique identified for this svg (in case they are the same) - baseTheme: string; - size: {width: string; height: string}; - defaultValue: Value | undefined; - tool: Tool; - changeValue(value: Value): void; -} - -interface ISvgViewerState { - value: Value; - tool: Tool; -} - -export class SvgViewer extends React.Component<ISvgViewerProps, ISvgViewerState> { - private svgPanZoomRef : React.RefObject<ReactSVGPanZoom> = React.createRef<ReactSVGPanZoom>(); - constructor(props: ISvgViewerProps) { - super(props); - // tslint:disable-next-line: no-object-literal-type-assertion - this.state = { value: props.defaultValue ? props.defaultValue : {} as Value, tool: props.tool}; - } - - public componentDidUpdate(prevProps: ISvgViewerProps) { - // May need to update state if props changed - if (prevProps.defaultValue !== this.props.defaultValue || - this.props.id !== prevProps.id) { - this.setState({ - // tslint:disable-next-line: no-object-literal-type-assertion - value: this.props.defaultValue ? this.props.defaultValue : {} as Value, - tool: this.props.tool - }); - } else if (this.props.tool !== this.state.tool) { - this.setState({tool: this.props.tool}); - } - } - - public move(offsetX: number, offsetY: number) { - if (this.svgPanZoomRef && this.svgPanZoomRef.current) { - this.svgPanZoomRef.current.pan(offsetX, offsetY); - } - } - - public zoom(amount: number) { - if (this.svgPanZoomRef && this.svgPanZoomRef.current) { - this.svgPanZoomRef.current.zoomOnViewerCenter(amount); - } - } - - public render() { - return ( - <AutoSizer> - {({ height, width }) => ( - width === 0 || height === 0 ? null : - <ReactSVGPanZoom - ref={this.svgPanZoomRef} - width={width} - height={height} - toolbarProps={{position: POSITION_TOP}} - detectAutoPan={true} - tool={this.state.tool} - value={this.state.value} - onChangeTool={this.changeTool} - onChangeValue={this.changeValue} - customToolbar={this.renderToolbar} - customMiniature={this.renderMiniature} - SVGBackground={'transparent'} - background={'var(--override-widget-background, var(--vscode-notifications-background))'} - detectWheel={true}> - <svg width={this.props.size.width} height={this.props.size.height}> - <SvgLoader svgXML={this.props.svg}/> - </svg> - </ReactSVGPanZoom> - )} - </AutoSizer> - ); - } - - private changeTool = (tool: Tool) => { - this.setState({tool}); - } - - private changeValue = (value: Value) => { - this.setState({value}); - this.props.changeValue(value); - } - - private renderToolbar = () => { - // Hide toolbar too - return ( - <div/> - ); - } - - private renderMiniature = () => { - return ( - <div /> // Hide miniature - ); - } -} diff --git a/src/datascience-ui/react-common/textMeasure.ts b/src/datascience-ui/react-common/textMeasure.ts deleted file mode 100644 index a86cf799b175..000000000000 --- a/src/datascience-ui/react-common/textMeasure.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -let canvas : HTMLCanvasElement | undefined; - -function getCanvas() : HTMLCanvasElement { - if (!canvas) { - canvas = document.createElement('canvas'); - } - return canvas; -} - -export function measureText(text: string, font: string | null) : number { - const context = getCanvas().getContext('2d'); - if (context) { - if (font) { - context.font = font; - } - const metrics = context.measureText(text); - return metrics.width; - } - return 0; -} \ No newline at end of file diff --git a/src/datascience-ui/react-common/themeDetector.ts b/src/datascience-ui/react-common/themeDetector.ts deleted file mode 100644 index 7bb967b15599..000000000000 --- a/src/datascience-ui/react-common/themeDetector.ts +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// From here: -// https://stackoverflow.com/questions/37257911/detect-light-dark-theme-programatically-in-visual-studio-code -// Detect vscode-light, vscode-dark, and vscode-high-contrast class name on the body element. -export function detectBaseTheme() : 'vscode-light' | 'vscode-dark' | 'vscode-high-contrast' { - const body = document.body; - if (body) { - switch (body.className) { - default: - case 'vscode-light': - return 'vscode-light'; - case 'vscode-dark': - return 'vscode-dark'; - case 'vscode-high-contrast': - return 'vscode-high-contrast'; - } - } - - return 'vscode-light'; -} diff --git a/src/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 a20077c2b735..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.testing.nosetestArgs": [], "python.testing.pytestArgs": [], - "python.testing.unittestArgs": [ - "-s=./tests", - "-p=test_*.py" - ], - "python.sortImports.args": [], + "python.testing.unittestArgs": ["-s=./tests", "-p=test_*.py", "-v", "-s", ".", "-p", "*test*.py"], "python.linting.lintOnSave": false, "python.linting.enabled": true, - "python.linting.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/activation/activationManager.unit.test.ts b/src/test/activation/activationManager.unit.test.ts index f884619151f8..6ee2572214b8 100644 --- a/src/test/activation/activationManager.unit.test.ts +++ b/src/test/activation/activationManager.unit.test.ts @@ -3,244 +3,417 @@ 'use strict'; -import { expect } from 'chai'; +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 } from 'vscode'; +import { TextDocument, Uri, WorkspaceFolder } from 'vscode'; import { ExtensionActivationManager } from '../../client/activation/activationManager'; -import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; -import { IExtensionActivationService } from '../../client/activation/types'; import { IApplicationDiagnostics } from '../../client/application/types'; -import { IDocumentManager, IWorkspaceService } from '../../client/common/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 { IDisposable } from '../../client/common/types'; +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 { IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; +import * as EnvFileTelemetry from '../../client/telemetry/envFileTelemetry'; import { sleep } from '../core'; -// tslint:disable:max-func-body-length no-any -suite('Activation - ActivationManager', () => { - class ExtensionActivationManagerTest extends ExtensionActivationManager { - // tslint:disable-next-line:no-unnecessary-override - public addHandlers() { - return super.addHandlers(); - } - // tslint:disable-next-line:no-unnecessary-override - public async initialize() { - return super.initialize(); - } - // tslint:disable-next-line:no-unnecessary-override - public addRemoveDocOpenedHandlers() { - super.addRemoveDocOpenedHandlers(); +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 interpreterService: IInterpreterService; - let documentManager: typemoq.IMock<IDocumentManager>; - let activationService1: IExtensionActivationService; - let activationService2: IExtensionActivationService; - setup(() => { - workspaceService = mock(WorkspaceService); - appDiagnostics = typemoq.Mock.ofType<IApplicationDiagnostics>(); - autoSelection = typemoq.Mock.ofType<IInterpreterAutoSelectionService>(); - interpreterService = mock(InterpreterService); - documentManager = typemoq.Mock.ofType<IDocumentManager>(); - activationService1 = mock(LanguageServerExtensionActivationService); - activationService2 = mock(LanguageServerExtensionActivationService); - managerTest = new ExtensionActivationManagerTest( - [instance(activationService1), instance(activationService2)], - documentManager.object, - instance(interpreterService), - autoSelection.object, - appDiagnostics.object, - instance(workspaceService) - ); - }); - test('Initialize will add event handlers and will dispose them when running dispose', async () => { - const disposable = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); - when(workspaceService.workspaceFolders).thenReturn([1 as any, 2 as any]); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const eventDef = () => disposable2.object; - documentManager.setup(d => d.onDidOpenTextDocument).returns(() => eventDef).verifiable(typemoq.Times.once()); + 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, + ); - await managerTest.initialize(); + sinon.stub(EnvFileTelemetry, 'sendActivationTelemetry').resolves(); + }); - verify(workspaceService.workspaceFolders).once(); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(workspaceService.onDidChangeWorkspaceFolders).once(); + teardown(() => { + sinon.restore(); + }); - documentManager.verifyAll(); + 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); - disposable.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); + autoSelection + .setup((a) => a.autoSelectInterpreter(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + appDiagnostics + .setup((a) => a.performPreStartupHealthCheck(resource)) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); - managerTest.dispose(); + managerTest = new ExtensionActivationManagerTest( + [], + [], + documentManager.object, + autoSelection.object, + appDiagnostics.object, + instance(workspaceService), + instance(fileSystem), + instance(activeResourceService), + interpreterPathService.object, + ); + await managerTest.activateWorkspace(resource); - disposable.verifyAll(); - disposable2.verifyAll(); - }); - test('Remove text document opened handler if there is only one workspace', async () => { - const disposable = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(() => disposable.object); - when(workspaceService.workspaceFolders).thenReturn([1 as any, 2 as any]); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const eventDef = () => disposable2.object; - documentManager.setup(d => d.onDidOpenTextDocument).returns(() => eventDef).verifiable(typemoq.Times.once()); - disposable.setup(d => d.dispose()); - disposable2.setup(d => d.dispose()); - - await managerTest.initialize(); - - verify(workspaceService.workspaceFolders).once(); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(workspaceService.onDidChangeWorkspaceFolders).once(); - documentManager.verifyAll(); - disposable.verify(d => d.dispose(), typemoq.Times.never()); - disposable2.verify(d => d.dispose(), typemoq.Times.never()); - - when(workspaceService.workspaceFolders).thenReturn([]); - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - - await managerTest.initialize(); - - verify(workspaceService.hasWorkspaceFolders).twice(); - disposable.verify(d => d.dispose(), typemoq.Times.never()); - disposable2.verify(d => d.dispose(), typemoq.Times.once()); - - managerTest.dispose(); - - disposable.verify(d => d.dispose(), typemoq.Times.atLeast(1)); - disposable2.verify(d => d.dispose(), typemoq.Times.once()); - }); - test('Activate workspace specific to the resource in case of Multiple workspaces when a file is opened', async () => { - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - let fileOpenedHandler!: (e: TextDocument) => Promise<void>; - let workspaceFoldersChangedHandler!: Function; - const documentUri = Uri.file('a'); - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(d => d.uri).returns(() => documentUri); - document.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE); - - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { - workspaceFoldersChangedHandler = cb; - return disposable1.object; + 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']); }); - documentManager - .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback(cb => (fileOpenedHandler = cb)) - .returns(() => disposable2.object) - .verifiable(typemoq.Times.once()); - - const resource = Uri.parse('two'); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: resource, index: 2 }; - when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn('one'); - when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.getWorkspaceFolder(document.object.uri)).thenReturn(folder2); - - when(workspaceService.getWorkspaceFolder(resource)).thenReturn(folder2); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); - when(interpreterService.getInterpreters(anything())).thenResolve(); - autoSelection - .setup(a => a.autoSelectInterpreter(resource)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - appDiagnostics - .setup(a => a.performPreStartupHealthCheck(resource)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - // Add workspaceFoldersChangedHandler - managerTest.addHandlers(); - expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); - - // Add fileOpenedHandler - workspaceFoldersChangedHandler.call(managerTest); - expect(fileOpenedHandler).not.to.be.equal(undefined, 'Handler not set'); - - // Check if activate workspace is called on opening a file - await fileOpenedHandler.call(managerTest, document.object); - await sleep(1); - - documentManager.verifyAll(); - verify(workspaceService.onDidChangeWorkspaceFolders).once(); - verify(workspaceService.workspaceFolders).atLeast(1); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(workspaceService.getWorkspaceFolder(anything())).atLeast(1); - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); - }); - test('Function activateWorkspace() will be filtered to current resource', async () => { - const resource = Uri.parse('two'); - when(activationService1.activate(resource)).thenResolve(); - when(activationService2.activate(resource)).thenResolve(); - when(interpreterService.getInterpreters(anything())).thenResolve(); - autoSelection - .setup(a => a.autoSelectInterpreter(resource)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - appDiagnostics - .setup(a => a.performPreStartupHealthCheck(resource)) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - - await managerTest.activateWorkspace(resource); - - verify(activationService1.activate(resource)).once(); - verify(activationService2.activate(resource)).once(); - }); - test('Handler docOpenedHandler is disposed in case no. of workspace folders decreases to one', async () => { - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - let docOpenedHandler!: (e: TextDocument) => Promise<void>; - let workspaceFoldersChangedHandler!: Function; - const documentUri = Uri.file('a'); - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(d => d.uri).returns(() => documentUri); - - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(cb => { workspaceFoldersChangedHandler = cb; return disposable1.object; }); - documentManager - .setup(w => w.onDidOpenTextDocument(typemoq.It.isAny(), typemoq.It.isAny())) - .callback(cb => (docOpenedHandler = cb)) - .returns(() => disposable2.object) - .verifiable(typemoq.Times.once()); - - const resource = Uri.parse('two'); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: resource, index: 2 }; - when(workspaceService.workspaceFolders).thenReturn([folder1, folder2]); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - // Add workspaceFoldersChangedHandler - managerTest.addHandlers(); - expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); - - // Add docOpenedHandler - workspaceFoldersChangedHandler.call(managerTest); - expect(docOpenedHandler).not.to.be.equal(undefined, 'Handler not set'); - - documentManager.verifyAll(); - verify(workspaceService.onDidChangeWorkspaceFolders).once(); - verify(workspaceService.workspaceFolders).atLeast(1); - verify(workspaceService.hasWorkspaceFolders).once(); - - //Removed no. of folders to one - when(workspaceService.workspaceFolders).thenReturn([folder1]); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - - workspaceFoldersChangedHandler.call(managerTest); - - verify(workspaceService.workspaceFolders).atLeast(1); - verify(workspaceService.hasWorkspaceFolders).twice(); }); }); diff --git a/src/test/activation/activationService.unit.test.ts b/src/test/activation/activationService.unit.test.ts deleted file mode 100644 index 6b5ebeabe11c..000000000000 --- a/src/test/activation/activationService.unit.test.ts +++ /dev/null @@ -1,896 +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 { ConfigurationChangeEvent, Disposable, Uri, WorkspaceConfiguration } from 'vscode'; -import { LanguageServerExtensionActivationService } from '../../client/activation/activationService'; -import { - FolderVersionPair, - IExtensionActivationService, - ILanguageServerActivator, - ILanguageServerFolderService, - LanguageServerActivator -} from '../../client/activation/types'; -import { LSNotSupportedDiagnosticServiceId } from '../../client/application/diagnostics/checks/lsNotSupported'; -import { IDiagnostic, IDiagnosticsService } from '../../client/application/diagnostics/types'; -import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { LSControl, LSEnabled } from '../../client/common/experimentGroups'; -import { IPlatformService } from '../../client/common/platform/types'; -import { IConfigurationService, IDisposable, IDisposableRegistry, IExperimentsManager, IOutputChannel, IPersistentState, IPersistentStateFactory, IPythonSettings, Resource } from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; - -// tslint:disable:no-any - -suite('Activation - ActivationService', () => { - [true, false].forEach(jediIsEnabled => { - suite(`Test activation - ${jediIsEnabled ? 'Jedi is enabled' : 'Jedi is disabled'}`, () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let cmdManager: TypeMoq.IMock<ICommandManager>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let platformService: TypeMoq.IMock<IPlatformService>; - let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; - let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; - let state: TypeMoq.IMock<IPersistentState<boolean | undefined>>; - let experiments: TypeMoq.IMock<IExperimentsManager>; - let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - state = TypeMoq.Mock.ofType<IPersistentState<boolean | undefined>>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); - const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3') - }; - lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - workspaceService.setup(w => w.workspaceFolders).returns(() => []); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - langFolderServiceMock - .setup(l => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory.setup(f => f.createGlobalPersistentState(TypeMoq.It.isValue('SWITCH_LS'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => state.object); - state.setup(s => s.value).returns(() => undefined); - state.setup(s => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - const setting = { workspaceFolderValue: jediIsEnabled }; - workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(ws => ws.getConfiguration('python', TypeMoq.It.isAny())).returns(() => workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<boolean>('jediEnabled')) - .returns(() => setting as any); - 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<ILanguageServerActivator>, - lsSupported: boolean = true - ) { - activator - .setup(a => a.activate(undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - let activatorName = LanguageServerActivator.Jedi; - if (lsSupported && !jediIsEnabled) { - activatorName = LanguageServerActivator.DotNet; - } - let diagnostics: IDiagnostic[]; - if (!lsSupported && !jediIsEnabled) { - diagnostics = [TypeMoq.It.isAny()]; - } else { - diagnostics = []; - } - lsNotSupportedDiagnosticService - .setup(l => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup(l => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(activatorName))) - .returns(() => activator.object) - .verifiable(TypeMoq.Times.once()); - - experiments - .setup(ex => ex.inExperiment(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - - await activationService.activate(undefined); - - activator.verifyAll(); - serviceContainer.verifyAll(); - experiments.verifyAll(); - } - - test('LS is supported', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - - await testActivation(activationService, activator, true); - }); - test('LS is not supported', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - - await testActivation(activationService, activator, false); - }); - - test('Activatory must be activated', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - - await testActivation(activationService, activator); - }); - test('Activatory must be deactivated', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - - await testActivation(activationService, activator); - - activator - .setup(a => a.dispose()) - .verifiable(TypeMoq.Times.once()); - - activationService.dispose(); - activator.verifyAll(); - }); - test('Prompt user to reload VS Code and reload, when setting is toggled', async () => { - 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<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.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 () => { - 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<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.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 () => { - 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<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.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 () => { - 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<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.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(); - }); - if (!jediIsEnabled) { - test('Revert to jedi when LS activation fails', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activatorDotNet = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activatorJedi = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const diagnostics: IDiagnostic[] = []; - lsNotSupportedDiagnosticService - .setup(l => l.diagnose(undefined)) - .returns(() => Promise.resolve(diagnostics)); - lsNotSupportedDiagnosticService - .setup(l => l.handle(TypeMoq.It.isValue(diagnostics))) - .returns(() => Promise.resolve()); - serviceContainer - .setup(c => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerActivator.DotNet) - ) - ) - .returns(() => activatorDotNet.object) - .verifiable(TypeMoq.Times.once()); - activatorDotNet - .setup(a => a.activate(undefined)) - .returns(() => Promise.reject(new Error(''))) - .verifiable(TypeMoq.Times.once()); - serviceContainer - .setup(c => - c.get( - TypeMoq.It.isValue(ILanguageServerActivator), - TypeMoq.It.isValue(LanguageServerActivator.Jedi) - ) - ) - .returns(() => activatorJedi.object) - .verifiable(TypeMoq.Times.once()); - activatorJedi - .setup(a => a.activate(undefined)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - - await activationService.activate(undefined); - - activatorDotNet.verifyAll(); - activatorJedi.verifyAll(); - serviceContainer.verifyAll(); - }); - async function testActivationOfResource( - activationService: IExtensionActivationService, - activator: TypeMoq.IMock<ILanguageServerActivator>, - resource: Resource - ) { - activator - .setup(a => a.activate(resource)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - lsNotSupportedDiagnosticService - .setup(l => l.diagnose(undefined)) - .returns(() => Promise.resolve([])); - lsNotSupportedDiagnosticService - .setup(l => l.handle(TypeMoq.It.isValue([]))) - .returns(() => Promise.resolve()); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(LanguageServerActivator.DotNet))) - .returns(() => activator.object) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup(w => w.getWorkspaceFolderIdentifier(resource, '')) - .returns(() => resource!.fsPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - experiments - .setup(ex => ex.inExperiment(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - - await activationService.activate(resource); - - activator.verifyAll(); - serviceContainer.verifyAll(); - workspaceService.verifyAll(); - experiments.verifyAll(); - } - test('Activator is disposed if activated workspace is removed', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - let workspaceFoldersChangedHandler!: Function; - workspaceService - .setup(w => w.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback(cb => (workspaceFoldersChangedHandler = cb)) - .returns(() => TypeMoq.Mock.ofType<IDisposable>().object) - .verifiable(TypeMoq.Times.once()); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - workspaceService.verifyAll(); - expect(workspaceFoldersChangedHandler).not.to.be.equal(undefined, 'Handler not set'); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - const folder3 = { name: 'three', uri: Uri.parse('three'), index: 3 }; - - const activator1 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - await testActivationOfResource(activationService, activator1, folder1.uri); - const activator2 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - await testActivationOfResource(activationService, activator2, folder2.uri); - const activator3 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - await testActivationOfResource(activationService, activator3, folder3.uri); - - //Now remove folder3 - workspaceService.reset(); - workspaceService.setup(w => w.workspaceFolders).returns(() => [folder1, folder2]); - workspaceService - .setup(w => w.getWorkspaceFolderIdentifier(folder1.uri, '')) - .returns(() => folder1.uri.fsPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService - .setup(w => w.getWorkspaceFolderIdentifier(folder2.uri, '')) - .returns(() => folder2.uri.fsPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - activator1 - .setup(d => d.dispose()) - .verifiable(TypeMoq.Times.never()); - activator2 - .setup(d => d.dispose()) - .verifiable(TypeMoq.Times.never()); - activator3 - .setup(d => d.dispose()) - .verifiable(TypeMoq.Times.once()); - workspaceFoldersChangedHandler.call(activationService); - workspaceService.verifyAll(); - activator3.verifyAll(); - }); - } else { - test('Jedi is only activated once', async () => { - pythonSettings.setup(p => p.jediEnabled).returns(() => jediIsEnabled); - const activator1 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const folder1 = { name: 'one', uri: Uri.parse('one'), index: 1 }; - const folder2 = { name: 'two', uri: Uri.parse('two'), index: 2 }; - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(LanguageServerActivator.Jedi))) - .returns(() => activator1.object) - .verifiable(TypeMoq.Times.once()); - activator1 - .setup(a => a.activate(folder1.uri)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - experiments - .setup(ex => ex.inExperiment(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - await activationService.activate(folder1.uri); - activator1.verifyAll(); - serviceContainer.verifyAll(); - experiments.verifyAll(); - - const activator2 = TypeMoq.Mock.ofType<ILanguageServerActivator>(); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerActivator), TypeMoq.It.isValue(LanguageServerActivator.Jedi))) - .returns(() => activator2.object) - .verifiable(TypeMoq.Times.once()); - activator2 - .setup(a => a.activate(folder2.uri)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - experiments - .setup(ex => ex.inExperiment(TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - await activationService.activate(folder2.uri); - serviceContainer.verifyAll(); - activator1.verifyAll(); - activator2.verifyAll(); - experiments.verifyAll(); - }); - } - }); - }); - - suite('Test trackLangaugeServerSwitch()', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let cmdManager: TypeMoq.IMock<ICommandManager>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let platformService: TypeMoq.IMock<IPlatformService>; - let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; - let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; - let state: TypeMoq.IMock<IPersistentState<boolean | undefined>>; - let experiments: TypeMoq.IMock<IExperimentsManager>; - let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - state = TypeMoq.Mock.ofType<IPersistentState<boolean | undefined>>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); - const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3') - }; - lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - workspaceService.setup(w => w.workspaceFolders).returns(() => []); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - langFolderServiceMock - .setup(l => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory.setup(f => f.createGlobalPersistentState(TypeMoq.It.isValue('SWITCH_LS'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => state.object); - state.setup(s => s.value).returns(() => undefined); - state.setup(s => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(ws => ws.getConfiguration('python', TypeMoq.It.isAny())).returns(() => workspaceConfig.object); - const output = TypeMoq.Mock.ofType<IOutputChannel>(); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) - .returns(() => output.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))) - .returns(() => appShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ICommandManager))) - .returns(() => cmdManager.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer - .setup(s => - s.get( - TypeMoq.It.isValue(IDiagnosticsService), - TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) - ) - ) - .returns(() => lsNotSupportedDiagnosticService.object); - }); - - test('Track current LS usage for first usage', async () => { - state.reset(); - state.setup(s => s.value).returns(() => undefined).verifiable(TypeMoq.Times.once()); - state.setup(s => s.updateValue(TypeMoq.It.isValue(true))).returns(() => Promise.resolve()).verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - await activationService.trackLangaugeServerSwitch(true); - - state.verifyAll(); - }); - test('Track switch to LS', async () => { - state.reset(); - state.setup(s => s.value).returns(() => true).verifiable(TypeMoq.Times.once()); - state.setup(s => s.updateValue(TypeMoq.It.isValue(false))).returns(() => Promise.resolve()).verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - await activationService.trackLangaugeServerSwitch(false); - - state.verify(s => s.updateValue(TypeMoq.It.isValue(false)), TypeMoq.Times.once()); - }); - test('Track switch to Jedi', async () => { - state.reset(); - state.setup(s => s.value).returns(() => false).verifiable(TypeMoq.Times.once()); - state.setup(s => s.updateValue(TypeMoq.It.isValue(true))).returns(() => Promise.resolve()).verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - await activationService.trackLangaugeServerSwitch(true); - - state.verify(s => s.updateValue(TypeMoq.It.isValue(true)), TypeMoq.Times.once()); - }); - }); - - suite('Test useJedi()', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let cmdManager: TypeMoq.IMock<ICommandManager>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let platformService: TypeMoq.IMock<IPlatformService>; - let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; - let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; - let state: TypeMoq.IMock<IPersistentState<boolean | undefined>>; - let experiments: TypeMoq.IMock<IExperimentsManager>; - let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - state = TypeMoq.Mock.ofType<IPersistentState<boolean | undefined>>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); - const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3') - }; - lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - workspaceService.setup(w => w.workspaceFolders).returns(() => []); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - langFolderServiceMock - .setup(l => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory.setup(f => f.createGlobalPersistentState(TypeMoq.It.isValue('SWITCH_LS'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => state.object); - state.setup(s => s.value).returns(() => undefined); - state.setup(s => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(ws => ws.getConfiguration('python', TypeMoq.It.isAny())).returns(() => workspaceConfig.object); - const output = TypeMoq.Mock.ofType<IOutputChannel>(); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) - .returns(() => output.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))) - .returns(() => appShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ICommandManager))) - .returns(() => cmdManager.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer - .setup(s => - s.get( - TypeMoq.It.isValue(IDiagnosticsService), - TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) - ) - ) - .returns(() => lsNotSupportedDiagnosticService.object); - }); - - test('If default value of jedi is being used, and LSEnabled experiment is enabled, then return false', async () => { - const settings = {}; - experiments - .setup(ex => ex.inExperiment(LSEnabled)) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - experiments - .setup(ex => ex.sendTelemetryIfInExperiment(TypeMoq.It.isAny())) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - workspaceConfig.setup(c => c.inspect<boolean>('jediEnabled')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const result = activationService.useJedi(); - expect(result).to.equal(false, 'LS should be enabled'); - - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - experiments.verifyAll(); - }); - - test('If default value of jedi is being used, and LSEnabled experiment is disabled, then send telemetry if user is in Experiment LSControl and return python settings value (which will always be true as default value is true)', async () => { - const settings = {}; - experiments - .setup(ex => ex.inExperiment(LSEnabled)) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - experiments - .setup(ex => ex.sendTelemetryIfInExperiment(LSControl)) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - workspaceConfig.setup(c => c.inspect<boolean>('jediEnabled')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - pythonSettings - .setup(p => p.jediEnabled) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const result = activationService.useJedi(); - expect(result).to.equal(true, 'Return value should be true'); - - pythonSettings.verifyAll(); - experiments.verifyAll(); - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - - suite('If default value of jedi is not being used, then no experiments are used, and python settings value is returned', async () => { - [ - { - testName: 'Returns false when python settings value is false', - pythonSettingsValue: false, - expectedResult: false - }, - { - testName: 'Returns true when python settings value is true', - pythonSettingsValue: true, - expectedResult: true - } - ].forEach(testParams => { - test(testParams.testName, async () => { - const settings = { workspaceFolderValue: true }; - experiments - .setup(ex => ex.inExperiment(LSEnabled)) - .returns(() => false) - .verifiable(TypeMoq.Times.never()); - experiments - .setup(ex => ex.sendTelemetryIfInExperiment(LSControl)) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - workspaceConfig.setup(c => c.inspect<boolean>('jediEnabled')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - pythonSettings - .setup(p => p.jediEnabled) - .returns(() => testParams.pythonSettingsValue) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const result = activationService.useJedi(); - expect(result).to.equal(testParams.pythonSettingsValue, `Return value should be ${testParams.pythonSettingsValue}`); - - pythonSettings.verifyAll(); - experiments.verifyAll(); - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - }); - }); - }); - - suite('Function isJediUsingDefaultConfiguration()', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let cmdManager: TypeMoq.IMock<ICommandManager>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let platformService: TypeMoq.IMock<IPlatformService>; - let lsNotSupportedDiagnosticService: TypeMoq.IMock<IDiagnosticsService>; - let stateFactory: TypeMoq.IMock<IPersistentStateFactory>; - let state: TypeMoq.IMock<IPersistentState<boolean | undefined>>; - let experiments: TypeMoq.IMock<IExperimentsManager>; - let workspaceConfig: TypeMoq.IMock<WorkspaceConfiguration>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - cmdManager = TypeMoq.Mock.ofType<ICommandManager>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - state = TypeMoq.Mock.ofType<IPersistentState<boolean | undefined>>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - experiments = TypeMoq.Mock.ofType<IExperimentsManager>(); - const langFolderServiceMock = TypeMoq.Mock.ofType<ILanguageServerFolderService>(); - const folderVer: FolderVersionPair = { - path: '', - version: new SemVer('1.2.3') - }; - lsNotSupportedDiagnosticService = TypeMoq.Mock.ofType<IDiagnosticsService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - workspaceService.setup(w => w.workspaceFolders).returns(() => []); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - langFolderServiceMock - .setup(l => l.getCurrentLanguageServerDirectory()) - .returns(() => Promise.resolve(folderVer)); - stateFactory.setup(f => f.createGlobalPersistentState(TypeMoq.It.isValue('SWITCH_LS'), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => state.object); - state.setup(s => s.value).returns(() => undefined); - state.setup(s => s.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(ws => ws.getConfiguration('python', TypeMoq.It.isAny())).returns(() => workspaceConfig.object); - const output = TypeMoq.Mock.ofType<IOutputChannel>(); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) - .returns(() => output.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))) - .returns(() => appShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ICommandManager))) - .returns(() => cmdManager.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(ILanguageServerFolderService))) - .returns(() => langFolderServiceMock.object); - serviceContainer - .setup(s => - s.get( - TypeMoq.It.isValue(IDiagnosticsService), - TypeMoq.It.isValue(LSNotSupportedDiagnosticServiceId) - ) - ) - .returns(() => lsNotSupportedDiagnosticService.object); - }); - const value = [undefined, true, false]; // Possible values of settings - const index = [0, 1, 2]; // Index associated with each value - const expectedResults: boolean[][][] = // Initializing a 3D array with default value `false` - Array(3).fill(false) - .map(() => Array(3).fill(false) - .map(() => Array(3).fill(false))); - expectedResults[0][0][0] = true; - for (const globalIndex of index) { - for (const workspaceIndex of index) { - for (const workspaceFolderIndex of index) { - const expectedResult = expectedResults[globalIndex][workspaceIndex][workspaceFolderIndex]; - const settings = { globalValue: value[globalIndex], workspaceValue: value[workspaceIndex], workspaceFolderValue: value[workspaceFolderIndex] }; - const testName = `Returns ${expectedResult} for setting = ${JSON.stringify(settings)}`; - test(testName, async () => { - workspaceConfig.reset(); - workspaceConfig.setup(c => c.inspect<boolean>('jediEnabled')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a')); - expect(result).to.equal(expectedResult); - - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - } - } - } - test('Returns false for settings = undefined', async () => { - workspaceConfig.reset(); - workspaceConfig.setup(c => c.inspect<boolean>('jediEnabled')) - .returns(() => undefined as any) - .verifiable(TypeMoq.Times.once()); - - const activationService = new LanguageServerExtensionActivationService(serviceContainer.object, stateFactory.object, experiments.object); - const result = activationService.isJediUsingDefaultConfiguration(Uri.parse('a')); - expect(result).to.equal(false, 'Return value should be false'); - - workspaceService.verifyAll(); - workspaceConfig.verifyAll(); - }); - }); -}); diff --git a/src/test/activation/activeResource.unit.test.ts b/src/test/activation/activeResource.unit.test.ts new file mode 100644 index 000000000000..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/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/activator.unit.test.ts b/src/test/activation/languageServer/activator.unit.test.ts deleted file mode 100644 index 7f035270076b..000000000000 --- a/src/test/activation/languageServer/activator.unit.test.ts +++ /dev/null @@ -1,228 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { LanguageServerExtensionActivator } from '../../../client/activation/languageServer/activator'; -import { LanguageServerDownloader } from '../../../client/activation/languageServer/downloader'; -import { LanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; -import { LanguageServerManager } from '../../../client/activation/languageServer/manager'; -import { - ILanguageServerDownloader, - ILanguageServerFolderService, - ILanguageServerManager -} from '../../../client/activation/types'; -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 { FileSystem } from '../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; -import { EXTENSION_ROOT_DIR } from '../../../client/constants'; -import { sleep } from '../../core'; - -// tslint:disable:max-func-body-length - -suite('Language Server - Activator', () => { - let activator: LanguageServerExtensionActivator; - let workspaceService: IWorkspaceService; - let manager: ILanguageServerManager; - let fs: IFileSystem; - let lsDownloader: ILanguageServerDownloader; - let lsFolderService: ILanguageServerFolderService; - let configuration: IConfigurationService; - let settings: IPythonSettings; - setup(() => { - manager = mock(LanguageServerManager); - workspaceService = mock(WorkspaceService); - fs = mock(FileSystem); - lsDownloader = mock(LanguageServerDownloader); - lsFolderService = mock(LanguageServerFolderService); - configuration = mock(ConfigurationService); - settings = mock(PythonSettings); - when(configuration.getSettings(anything())).thenReturn(instance(settings)); - activator = new LanguageServerExtensionActivator( - instance(manager), - instance(workspaceService), - instance(fs), - instance(lsDownloader), - instance(lsFolderService), - instance(configuration) - ); - }); - test('Manager must be started without any workspace', async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); - when(settings.downloadLanguageServer).thenReturn(false); - - await activator.activate(undefined); - - verify(manager.start(undefined)).once(); - verify(workspaceService.hasWorkspaceFolders).once(); - }); - test('Manager must be disposed', async () => { - activator.dispose(); - - verify(manager.dispose()).once(); - }); - test('Do not download LS if not required', async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); - when(settings.downloadLanguageServer).thenReturn(false); - - await activator.activate(undefined); - - verify(manager.start(undefined)).once(); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(lsFolderService.getLanguageServerFolderName(anything())).never(); - verify(lsDownloader.downloadLanguageServer(anything(), anything())).never(); - }); - test('Do not download LS if not required', async () => { - const languageServerFolder = 'Some folder name'; - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); - - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); - when(settings.downloadLanguageServer).thenReturn(true); - when(lsFolderService.getLanguageServerFolderName(anything())) - .thenResolve(languageServerFolder); - when(fs.fileExists(mscorlib)).thenResolve(true); - - await activator.activate(undefined); - - verify(manager.start(undefined)).once(); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(lsFolderService.getLanguageServerFolderName(anything())).once(); - verify(lsDownloader.downloadLanguageServer(anything(), anything())).never(); - }); - test('Start language server after downloading', async () => { - const deferred = createDeferred<void>(); - const languageServerFolder = 'Some folder name'; - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); - - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - when(manager.start(undefined)).thenResolve(); - when(settings.downloadLanguageServer).thenReturn(true); - when(lsFolderService.getLanguageServerFolderName(anything())) - .thenResolve(languageServerFolder); - when(fs.fileExists(mscorlib)).thenResolve(false); - when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)) - .thenReturn(deferred.promise); - - const promise = activator.activate(undefined); - await sleep(1); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(lsFolderService.getLanguageServerFolderName(anything())).once(); - verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); - - verify(manager.start(undefined)).never(); - - deferred.resolve(); - await sleep(1); - verify(manager.start(undefined)).once(); - - await promise; - }); - test('Manager must be started with resource for first available workspace', async () => { - const uri = Uri.file(__filename); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri }]); - when(manager.start(uri)).thenResolve(); - when(settings.downloadLanguageServer).thenReturn(false); - - await activator.activate(undefined); - - verify(manager.start(uri)).once(); - verify(workspaceService.hasWorkspaceFolders).once(); - verify(workspaceService.workspaceFolders).once(); - }); - - test('Manager must be disposed', async () => { - activator.dispose(); - - verify(manager.dispose()).once(); - }); - test('Download and check if ICU config exists', async () => { - const languageServerFolder = 'Some folder name'; - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); - const targetJsonFile = path.join(languageServerFolderPath, 'Microsoft.Python.LanguageServer.runtimeconfig.json'); - - when(settings.downloadLanguageServer).thenReturn(true); - when(lsFolderService.getLanguageServerFolderName(undefined)).thenResolve(languageServerFolder); - when(fs.fileExists(mscorlib)).thenResolve(false); - when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)).thenResolve(); - when(fs.fileExists(targetJsonFile)).thenResolve(false); - - await activator.ensureLanguageServerIsAvailable(undefined); - - verify(lsFolderService.getLanguageServerFolderName(undefined)).once(); - verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); - verify(fs.fileExists(targetJsonFile)).once(); - }); - test('Download if contents of ICU config is not as expected', async () => { - const languageServerFolder = 'Some folder name'; - const languageServerFolderPath = path.join(EXTENSION_ROOT_DIR, languageServerFolder); - const mscorlib = path.join(languageServerFolderPath, 'mscorlib.dll'); - const targetJsonFile = path.join(languageServerFolderPath, 'Microsoft.Python.LanguageServer.runtimeconfig.json'); - const jsonContents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': false } } }; - - when(settings.downloadLanguageServer).thenReturn(true); - when(lsFolderService.getLanguageServerFolderName(undefined)).thenResolve(languageServerFolder); - when(fs.fileExists(mscorlib)).thenResolve(false); - when(lsDownloader.downloadLanguageServer(languageServerFolderPath, undefined)).thenResolve(); - when(fs.fileExists(targetJsonFile)).thenResolve(true); - when(fs.readFile(targetJsonFile)).thenResolve(JSON.stringify(jsonContents)); - - await activator.ensureLanguageServerIsAvailable(undefined); - - verify(lsFolderService.getLanguageServerFolderName(undefined)).once(); - verify(lsDownloader.downloadLanguageServer(anything(), undefined)).once(); - verify(fs.fileExists(targetJsonFile)).once(); - verify(fs.readFile(targetJsonFile)).once(); - }); - test('JSON file is created to ensure LS can start without ICU', async () => { - const targetJsonFile = path.join('some folder', 'Microsoft.Python.LanguageServer.runtimeconfig.json'); - const contents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; - when(fs.fileExists(targetJsonFile)).thenResolve(false); - when(fs.writeFile(targetJsonFile, JSON.stringify(contents))).thenResolve(); - - await activator.prepareLanguageServerForNoICU('some folder'); - - verify(fs.fileExists(targetJsonFile)).atLeast(1); - verify(fs.writeFile(targetJsonFile, JSON.stringify(contents))).once(); - }); - test('JSON file is not created if it already exists with the right content', async () => { - const targetJsonFile = path.join('some folder', 'Microsoft.Python.LanguageServer.runtimeconfig.json'); - const contents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; - const existingContents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; - when(fs.fileExists(targetJsonFile)).thenResolve(true); - when(fs.readFile(targetJsonFile)).thenResolve(JSON.stringify(existingContents)); - - await activator.prepareLanguageServerForNoICU('some folder'); - - verify(fs.fileExists(targetJsonFile)).atLeast(1); - verify(fs.writeFile(targetJsonFile, JSON.stringify(contents))).never(); - verify(fs.readFile(targetJsonFile)).once(); - }); - test('JSON file is created if it already exists but with the wrong file content', async () => { - const targetJsonFile = path.join('some folder', 'Microsoft.Python.LanguageServer.runtimeconfig.json'); - const contents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': true } } }; - const existingContents = { runtimeOptions: { configProperties: { 'System.Globalization.Invariant': false } } }; - when(fs.fileExists(targetJsonFile)).thenResolve(true); - when(fs.readFile(targetJsonFile)).thenResolve(JSON.stringify(existingContents)); - - await activator.prepareLanguageServerForNoICU('some folder'); - - verify(fs.fileExists(targetJsonFile)).atLeast(1); - verify(fs.writeFile(targetJsonFile, JSON.stringify(contents))).once(); - verify(fs.readFile(targetJsonFile)).once(); - }); -}); diff --git a/src/test/activation/languageServer/analysisOptions.unit.test.ts b/src/test/activation/languageServer/analysisOptions.unit.test.ts deleted file mode 100644 index 5a6fb078f30c..000000000000 --- a/src/test/activation/languageServer/analysisOptions.unit.test.ts +++ /dev/null @@ -1,246 +0,0 @@ -// 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 * as typemoq from 'typemoq'; -import { ConfigurationChangeEvent, Uri, WorkspaceFolder } from 'vscode'; -import { DocumentSelector } from 'vscode-languageclient'; -import { LanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; -import { LanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; -import { ILanguageServerFolderService } from '../../../client/activation/types'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { PYTHON_LANGUAGE } from '../../../client/common/constants'; -import { PathUtils } from '../../../client/common/platform/pathUtils'; -import { IConfigurationService, IDisposable, IExtensionContext, IOutputChannel, IPathUtils, IPythonExtensionBanner } from '../../../client/common/types'; -import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; -import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; -import { IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { ProposeLanguageServerBanner } from '../../../client/languageServices/proposeLanguageServerBanner'; -import { sleep } from '../../core'; - -// tslint:disable:no-unnecessary-override no-any chai-vague-errors no-unused-expression max-func-body-length - -suite('Language Server - Analysis Options', () => { - class TestClass extends LanguageServerAnalysisOptions { - public getDocumentSelector(workspaceFolder?: WorkspaceFolder): DocumentSelector { - return super.getDocumentSelector(workspaceFolder); - } - public getExcludedFiles(): string[] { - return super.getExcludedFiles(); - } - public getVsCodeExcludeSection(setting: string, list: string[]): void { - return super.getVsCodeExcludeSection(setting, list); - } - public getPythonExcludeSection(list: string[]): void { - return super.getPythonExcludeSection(list); - } - public getTypeshedPaths(): string[] { - return super.getTypeshedPaths(); - } - public onSettingsChanged(): void { - return super.onSettingsChanged(); - } - public async notifyIfValuesHaveChanged(oldArray: string[], newArray: string[]): Promise<void> { - return super.notifyIfValuesHaveChanged(oldArray, newArray); - } - } - let analysisOptions: TestClass; - let context: typemoq.IMock<IExtensionContext>; - let envVarsProvider: IEnvironmentVariablesProvider; - let configurationService: IConfigurationService; - let workspace: IWorkspaceService; - let surveyBanner: IPythonExtensionBanner; - let interpreterService: IInterpreterService; - let outputChannel: IOutputChannel; - let pathUtils: IPathUtils; - let lsFolderService: ILanguageServerFolderService; - setup(() => { - context = typemoq.Mock.ofType<IExtensionContext>(); - envVarsProvider = mock(EnvironmentVariablesProvider); - configurationService = mock(ConfigurationService); - workspace = mock(WorkspaceService); - surveyBanner = mock(ProposeLanguageServerBanner); - interpreterService = mock(InterpreterService); - outputChannel = typemoq.Mock.ofType<IOutputChannel>().object; - pathUtils = mock(PathUtils); - lsFolderService = mock(LanguageServerFolderService); - analysisOptions = new TestClass(context.object, instance(envVarsProvider), - instance(configurationService), - instance(workspace), instance(surveyBanner), - instance(interpreterService), outputChannel, - instance(pathUtils), instance(lsFolderService)); - }); - test('Initialize will add event handlers and will dispose them when running dispose', async () => { - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - const disposable3 = typemoq.Mock.ofType<IDisposable>(); - when(workspace.onDidChangeConfiguration).thenReturn(() => disposable1.object); - when(interpreterService.onDidChangeInterpreter).thenReturn(() => disposable2.object); - when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); - - await analysisOptions.initialize(undefined); - - verify(workspace.onDidChangeConfiguration).once(); - verify(interpreterService.onDidChangeInterpreter).once(); - verify(envVarsProvider.onDidEnvironmentVariablesChange).once(); - - disposable1.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - disposable2.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - disposable3.setup(d => d.dispose()).verifiable(typemoq.Times.once()); - - analysisOptions.dispose(); - - disposable1.verifyAll(); - disposable2.verifyAll(); - disposable3.verifyAll(); - }); - test('Changes to settings or interpreter will be debounced', async () => { - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - const disposable3 = typemoq.Mock.ofType<IDisposable>(); - let configChangedHandler!: Function; - let interpreterChangedHandler!: Function; - when(workspace.onDidChangeConfiguration).thenReturn(cb => { configChangedHandler = cb; return disposable1.object; }); - when(interpreterService.onDidChangeInterpreter).thenReturn(cb => { interpreterChangedHandler = cb; return disposable2.object; }); - when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(() => disposable3.object); - let settingsChangedInvokedCount = 0; - analysisOptions.onDidChange(() => settingsChangedInvokedCount += 1); - - await analysisOptions.initialize(undefined); - expect(configChangedHandler).to.not.be.undefined; - expect(interpreterChangedHandler).to.not.be.undefined; - - for (let i = 0; i < 100; i += 1) { - configChangedHandler.call(analysisOptions); - } - expect(settingsChangedInvokedCount).to.be.equal(0); - - await sleep(10); - - expect(settingsChangedInvokedCount).to.be.equal(1); - }); - test('If there are no changes then no events will be fired', async () => { - analysisOptions.getExcludedFiles = () => []; - analysisOptions.getTypeshedPaths = () => []; - - let eventFired = false; - analysisOptions.onDidChange(() => eventFired = true); - - analysisOptions.onSettingsChanged(); - await sleep(10); - - expect(eventFired).to.be.equal(false); - }); - test('Event must be fired if excluded files are different', async () => { - analysisOptions.getExcludedFiles = () => ['1']; - analysisOptions.getTypeshedPaths = () => []; - - let eventFired = false; - analysisOptions.onDidChange(() => eventFired = true); - - analysisOptions.onSettingsChanged(); - await sleep(10); - - expect(eventFired).to.be.equal(true); - }); - test('Event must be fired if typeshed files are different', async () => { - analysisOptions.getExcludedFiles = () => []; - analysisOptions.getTypeshedPaths = () => ['1']; - - let eventFired = false; - analysisOptions.onDidChange(() => eventFired = true); - - analysisOptions.onSettingsChanged(); - await sleep(10); - - expect(eventFired).to.be.equal(true); - }); - test('Event must be fired if interpreter info is different', async () => { - let eventFired = false; - analysisOptions.onDidChange(() => eventFired = true); - - analysisOptions.onSettingsChanged(); - await sleep(10); - - expect(eventFired).to.be.equal(true); - }); - test('Changes to settings will be filtered to current resource', async () => { - const uri = Uri.file(__filename); - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - const disposable3 = typemoq.Mock.ofType<IDisposable>(); - let configChangedHandler!: Function; - let interpreterChangedHandler!: Function; - let envVarChangedHandler!: Function; - when(workspace.onDidChangeConfiguration).thenReturn(cb => { configChangedHandler = cb; return disposable1.object; }); - when(interpreterService.onDidChangeInterpreter).thenReturn(cb => { interpreterChangedHandler = cb; return disposable2.object; }); - when(envVarsProvider.onDidEnvironmentVariablesChange).thenReturn(cb => { envVarChangedHandler = cb; return disposable3.object; }); - let settingsChangedInvokedCount = 0; - - analysisOptions.onDidChange(() => settingsChangedInvokedCount += 1); - await analysisOptions.initialize(uri); - expect(configChangedHandler).to.not.be.undefined; - expect(interpreterChangedHandler).to.not.be.undefined; - expect(envVarChangedHandler).to.not.be.undefined; - - for (let i = 0; i < 100; i += 1) { - const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); - event.setup(e => e.affectsConfiguration(typemoq.It.isValue('python'), typemoq.It.isValue(uri))) - .returns(() => true) - .verifiable(typemoq.Times.once()); - configChangedHandler.call(analysisOptions, event.object); - - event.verifyAll(); - } - expect(settingsChangedInvokedCount).to.be.equal(0); - - await sleep(10); - - expect(settingsChangedInvokedCount).to.be.equal(1); - }); - test('Ensure search pattern is not provided when there are no workspaces', () => { - when(workspace.workspaceFolders).thenReturn([]); - - const expectedSelector = [ - { scheme: 'file', language: PYTHON_LANGUAGE }, - { scheme: 'untitled', language: PYTHON_LANGUAGE } - ]; - - const selector = analysisOptions.getDocumentSelector(); - - expect(selector).to.deep.equal(expectedSelector); - }); - test('Ensure search pattern is not provided in single-root workspaces', () => { - const workspaceFolder: WorkspaceFolder = { name: '', index: 0, uri: Uri.file(__dirname) }; - when(workspace.workspaceFolders).thenReturn([workspaceFolder]); - - const expectedSelector = [ - { scheme: 'file', language: PYTHON_LANGUAGE }, - { scheme: 'untitled', language: PYTHON_LANGUAGE } - ]; - - const selector = analysisOptions.getDocumentSelector(workspaceFolder); - - expect(selector).to.deep.equal(expectedSelector); - }); - test('Ensure search pattern is provided in a multi-root workspace', () => { - const workspaceFolder1 = { name: '1', index: 0, uri: Uri.file(__dirname) }; - const workspaceFolder2 = { name: '2', index: 1, uri: Uri.file(__dirname) }; - when(workspace.workspaceFolders).thenReturn([workspaceFolder1, workspaceFolder2]); - - const expectedSelector = [ - { scheme: 'file', language: PYTHON_LANGUAGE, pattern: `${workspaceFolder1.uri.fsPath}/**/*` }, - { scheme: 'untitled', language: PYTHON_LANGUAGE } - ]; - - const selector = analysisOptions.getDocumentSelector(workspaceFolder1); - - expect(selector).to.deep.equal(expectedSelector); - }); -}); diff --git a/src/test/activation/languageServer/downloadChannelRules.unit.test.ts b/src/test/activation/languageServer/downloadChannelRules.unit.test.ts deleted file mode 100644 index b95867bbf050..000000000000 --- a/src/test/activation/languageServer/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/languageServer/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/languageServer/downloader.unit.test.ts b/src/test/activation/languageServer/downloader.unit.test.ts deleted file mode 100644 index 1b18ae51a2e9..000000000000 --- a/src/test/activation/languageServer/downloader.unit.test.ts +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import { SemVer } from 'semver'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceConfiguration } from 'vscode'; -import { LanguageServerDownloader } from '../../../client/activation/languageServer/downloader'; -import { ILanguageServerFolderService, IPlatformData } from '../../../client/activation/types'; -import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IOutputChannel, Resource, IFileDownloader } from '../../../client/common/types'; -import { Common, LanguageService } from '../../../client/common/utils/localize'; -import { mock, instance, verify, when, anything, deepEqual } from 'ts-mockito'; -import { PlatformData } from '../../../client/activation/languageServer/platformData'; -import { FileDownloader } from '../../../client/common/net/fileDownloader'; -import { LanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { MockOutputChannel } from '../../mockClasses'; -import { noop } from '../../core'; - -use(chaiAsPromised); - -// tslint:disable-next-line:max-func-body-length -suite('Activation - Downloader', () => { - let languageServerDownloader: LanguageServerDownloader; - let folderService: TypeMoq.IMock<ILanguageServerFolderService>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let resource: Resource; - setup(() => { - folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(undefined, TypeMoq.MockBehavior.Strict); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(undefined, TypeMoq.MockBehavior.Strict); - resource = Uri.file(__dirname); - languageServerDownloader = new LanguageServerDownloader( - undefined as any, - undefined as any, - undefined as any, - folderService.object, - undefined as any, - undefined as any, - workspaceService.object - ); - }); - - test('Get download info - HTTPS with resource', async () => { - const cfg = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); - cfg - .setup(c => c.get('proxyStrictSSL', true)) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('http'), TypeMoq.It.isValue(resource))) - .returns(() => cfg.object) - .verifiable(TypeMoq.Times.once()); - - const pkg = makePkgInfo('ls', 'https://a.b.com/x/y/z/ls.nupkg'); - folderService - .setup(f => f.getLatestLanguageServerVersion(resource)) - .returns(() => Promise.resolve(pkg)) - .verifiable(TypeMoq.Times.once()); - - const [uri, version] = await languageServerDownloader.getDownloadInfo(resource); - - folderService.verifyAll(); - workspaceService.verifyAll(); - expect(uri).to.equal(pkg.uri); - expect(version).to.equal(pkg.version.raw); - }); - - test('Get download info - HTTPS without resource', async () => { - const cfg = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); - cfg - .setup(c => c.get('proxyStrictSSL', true)) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('http'), undefined)) - .returns(() => cfg.object) - .verifiable(TypeMoq.Times.once()); - - const pkg = makePkgInfo('ls', 'https://a.b.com/x/y/z/ls.nupkg'); - folderService - .setup(f => f.getLatestLanguageServerVersion(undefined)) - .returns(() => Promise.resolve(pkg)) - .verifiable(TypeMoq.Times.once()); - - const [uri, version] = await languageServerDownloader.getDownloadInfo(undefined); - - folderService.verifyAll(); - workspaceService.verifyAll(); - expect(uri).to.equal(pkg.uri); - expect(version).to.equal(pkg.version.raw); - }); - - test('Get download info - HTTPS disabled', async () => { - const cfg = TypeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, TypeMoq.MockBehavior.Strict); - cfg - .setup(c => c.get('proxyStrictSSL', true)) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - workspaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('http'), TypeMoq.It.isValue(resource))) - .returns(() => cfg.object) - .verifiable(TypeMoq.Times.once()); - - const pkg = makePkgInfo('ls', 'https://a.b.com/x/y/z/ls.nupkg'); - folderService - .setup(f => f.getLatestLanguageServerVersion(resource)) - .returns(() => Promise.resolve(pkg)) - .verifiable(TypeMoq.Times.once()); - - const [uri, version] = await languageServerDownloader.getDownloadInfo(resource); - - folderService.verifyAll(); - workspaceService.verifyAll(); - // tslint:disable-next-line:no-http-string - expect(uri).to.deep.equal('http://a.b.com/x/y/z/ls.nupkg'); - expect(version).to.equal(pkg.version.raw); - }); - - test('Get download info - HTTP', async () => { - // tslint:disable-next-line:no-http-string - const pkg = makePkgInfo('ls', 'http://a.b.com/x/y/z/ls.nupkg'); - folderService - .setup(f => f.getLatestLanguageServerVersion(resource)) - .returns(() => Promise.resolve(pkg)) - .verifiable(TypeMoq.Times.once()); - - const [uri, version] = await languageServerDownloader.getDownloadInfo(resource); - - folderService.verifyAll(); - workspaceService.verifyAll(); - expect(uri).to.equal(pkg.uri); - expect(version).to.equal(pkg.version.raw); - }); - - test('Get download info - bogus URL', async () => { - const pkg = makePkgInfo('ls', 'xyz'); - folderService - .setup(f => f.getLatestLanguageServerVersion(resource)) - .returns(() => Promise.resolve(pkg)) - .verifiable(TypeMoq.Times.once()); - - const [uri, version] = await languageServerDownloader.getDownloadInfo(resource); - - folderService.verifyAll(); - workspaceService.verifyAll(); - expect(uri).to.equal(pkg.uri); - expect(version).to.equal(pkg.version.raw); - }); - - suite('Test LanguageServerDownloader.downloadFile', () => { - let lsDownloader: LanguageServerDownloader; - let outputChannel: IOutputChannel; - let fileDownloader: IFileDownloader; - const downloadUri = 'http://wow.com/file.txt'; - const downloadTitle = 'Downloadimg file.txt'; - setup(() => { - const platformData = mock(PlatformData); - outputChannel = mock(MockOutputChannel); - fileDownloader = mock(FileDownloader); - const lsFolderService = mock(LanguageServerFolderService); - const appShell = mock(ApplicationShell); - const fs = mock(FileSystem); - const workspaceService = mock(WorkspaceService); - - lsDownloader = new LanguageServerDownloader(instance(platformData), - instance(outputChannel), instance(fileDownloader), - instance(lsFolderService), instance(appShell), - instance(fs), instance(workspaceService)); - }); - - test('Downloaded file name must be returned from file downloader and right args passed', async () => { - const downloadedFile = 'This is the downloaded file'; - when(fileDownloader.downloadFile(anything(), anything())).thenResolve(downloadedFile); - const expectedDownloadOptions = { - extension: '.nupkg', - outputChannel: instance(outputChannel), - progressMessagePrefix: downloadTitle - }; - - const file = await lsDownloader.downloadFile(downloadUri, downloadTitle); - - expect(file).to.be.equal(downloadedFile); - verify(fileDownloader.downloadFile(anything(), anything())).once(); - verify(fileDownloader.downloadFile(downloadUri, deepEqual(expectedDownloadOptions))).once(); - }); - test('If download succeeds then log completion message', async () => { - when(fileDownloader.downloadFile(anything(), anything())).thenResolve(); - - await lsDownloader.downloadFile(downloadUri, downloadTitle); - - verify(fileDownloader.downloadFile(anything(), anything())).once(); - verify(outputChannel.appendLine(LanguageService.extractionCompletedOutputMessage())).once(); - }); - test('If download fails do not log completion message', async () => { - const ex = new Error('kaboom'); - when(fileDownloader.downloadFile(anything(), anything())).thenReject(ex); - - const promise = lsDownloader.downloadFile(downloadUri, downloadTitle); - await promise.catch(noop); - - verify(outputChannel.appendLine(LanguageService.extractionCompletedOutputMessage())).never(); - expect(promise).to.eventually.be.rejectedWith('kaboom'); - }); - }); - - // tslint:disable-next-line:max-func-body-length - suite('Test LanguageServerDownloader.downloadLanguageServer', () => { - const failure = new Error('kaboom'); - - class LanguageServerDownloaderTest extends LanguageServerDownloader { - // tslint:disable-next-line:no-unnecessary-override - public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> { - return super.downloadLanguageServer(destinationFolder, res); - } - public async downloadFile(_uri: string, _title: string): Promise<string> { - throw failure; - } - } - class LanguageServerExtractorTest extends LanguageServerDownloader { - // tslint:disable-next-line:no-unnecessary-override - public async downloadLanguageServer(destinationFolder: string, res?: Resource): Promise<void> { - return super.downloadLanguageServer(destinationFolder, res); - } - // tslint:disable-next-line:no-unnecessary-override - public async getDownloadInfo(res?: Resource) { - return super.getDownloadInfo(res); - } - public async downloadFile() { - return 'random'; - } - protected async unpackArchive(_extensionPath: string, _tempFilePath: string): Promise<void> { - throw failure; - } - } - let output: TypeMoq.IMock<IOutputChannel>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let fs: TypeMoq.IMock<IFileSystem>; - let platformData: TypeMoq.IMock<IPlatformData>; - let languageServerDownloaderTest: LanguageServerDownloaderTest; - let languageServerExtractorTest: LanguageServerExtractorTest; - setup(() => { - appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); - folderService = TypeMoq.Mock.ofType<ILanguageServerFolderService>(undefined, TypeMoq.MockBehavior.Strict); - output = TypeMoq.Mock.ofType<IOutputChannel>(undefined, TypeMoq.MockBehavior.Strict); - fs = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); - platformData = TypeMoq.Mock.ofType<IPlatformData>(undefined, TypeMoq.MockBehavior.Strict); - - languageServerDownloaderTest = new LanguageServerDownloaderTest( - platformData.object, - output.object, - undefined as any, - folderService.object, - appShell.object, - fs.object, - workspaceService.object - ); - languageServerExtractorTest = new LanguageServerExtractorTest( - platformData.object, - output.object, - undefined as any, - folderService.object, - appShell.object, - fs.object, - workspaceService.object - ); - }); - test('Display error message if LS downloading fails', async () => { - const pkg = makePkgInfo('ls', 'xyz'); - folderService - .setup(f => f.getLatestLanguageServerVersion(resource)) - .returns(() => Promise.resolve(pkg)); - output - .setup(o => o.appendLine(LanguageService.downloadFailedOutputMessage())); - output - .setup(o => o.appendLine((failure as unknown) as string)); - appShell - .setup(a => a.showErrorMessage(LanguageService.lsFailedToDownload(), Common.openOutputPanel())) - .returns(() => Promise.resolve(undefined)); - - let actualFailure: Error | undefined; - try { - await languageServerDownloaderTest.downloadLanguageServer('', resource); - } catch (err) { - actualFailure = err; - } - - expect(actualFailure).to.not.equal(undefined, 'error not thrown'); - folderService.verifyAll(); - output.verifyAll(); - appShell.verifyAll(); - fs.verifyAll(); - platformData.verifyAll(); - }); - test('Display error message if LS extraction fails', async () => { - const pkg = makePkgInfo('ls', 'xyz'); - folderService - .setup(f => f.getLatestLanguageServerVersion(resource)) - .returns(() => Promise.resolve(pkg)); - output - .setup(o => o.appendLine(LanguageService.extractionFailedOutputMessage())); - output - .setup(o => o.appendLine((failure as unknown) as string)); - appShell - .setup(a => a.showErrorMessage(LanguageService.lsFailedToExtract(), Common.openOutputPanel())) - .returns(() => Promise.resolve(undefined)); - - let actualFailure: Error | undefined; - try { - await languageServerExtractorTest.downloadLanguageServer('', resource); - } catch (err) { - actualFailure = err; - } - - expect(actualFailure).to.not.equal(undefined, 'error not thrown'); - folderService.verifyAll(); - output.verifyAll(); - appShell.verifyAll(); - fs.verifyAll(); - platformData.verifyAll(); - }); - }); -}); - -function makePkgInfo(name: string, uri: string, version: string = '0.0.0') { - return { - package: name, - uri: uri, - version: new SemVer(version) - } as any; -} diff --git a/src/test/activation/languageServer/languageClientFactory.unit.test.ts b/src/test/activation/languageServer/languageClientFactory.unit.test.ts deleted file mode 100644 index 0e34a6b09104..000000000000 --- a/src/test/activation/languageServer/languageClientFactory.unit.test.ts +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -//tslint:disable:no-require-imports no-require-imports no-var-requires no-any no-unnecessary-class max-func-body-length match-default-export-name - -import { expect } from 'chai'; -import * as path from 'path'; -import rewiremock from 'rewiremock'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { LanguageClientOptions, ServerOptions } from 'vscode-languageclient'; -import { BaseLanguageClientFactory, DownloadedLanguageClientFactory, SimpleLanguageClientFactory } from '../../../client/activation/languageServer/languageClientFactory'; -import { LanguageServerFolderService } from '../../../client/activation/languageServer/languageServerFolderService'; -import { PlatformData } from '../../../client/activation/languageServer/platformData'; -import { PythonSettings } from '../../../client/common/configSettings'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; -import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; - -const dotNetCommand = 'dotnet'; -const languageClientName = 'Python Tools'; - -suite('Language Server - LanguageClient Factory', () => { - let configurationService: IConfigurationService; - let settings: IPythonSettings; - setup(() => { - configurationService = mock(ConfigurationService); - settings = mock(PythonSettings); - when(configurationService.getSettings(anything())).thenReturn(instance(settings)); - }); - teardown(() => { - rewiremock.disable(); - }); - - test('Download factory is used when required to download the LS', async () => { - const downloadFactory = mock(DownloadedLanguageClientFactory); - const simpleFactory = mock(SimpleLanguageClientFactory); - const envVarProvider = mock(EnvironmentVariablesProvider); - const activationService = mock(EnvironmentActivationService); - const factory = new BaseLanguageClientFactory(instance(downloadFactory), instance(simpleFactory), instance(configurationService), instance(envVarProvider), instance(activationService)); - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - const env = { FOO: 'bar' }; - when(settings.downloadLanguageServer).thenReturn(true); - when(envVarProvider.getEnvironmentVariables(uri)).thenReturn(Promise.resolve(env)); - when(activationService.getActivatedEnvironmentVariables(uri)).thenReturn(); - - await factory.createLanguageClient(uri, options); - - verify(configurationService.getSettings(uri)).once(); - verify(downloadFactory.createLanguageClient(uri, options, env)).once(); - verify(simpleFactory.createLanguageClient(uri, options, env)).never(); - }); - test('Simple factory is used when not required to download the LS', async () => { - const downloadFactory = mock(DownloadedLanguageClientFactory); - const simpleFactory = mock(SimpleLanguageClientFactory); - const envVarProvider = mock(EnvironmentVariablesProvider); - const activationService = mock(EnvironmentActivationService); - const factory = new BaseLanguageClientFactory(instance(downloadFactory), instance(simpleFactory), instance(configurationService), instance(envVarProvider), instance(activationService)); - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - const env = { FOO: 'bar' }; - when(settings.downloadLanguageServer).thenReturn(false); - when(envVarProvider.getEnvironmentVariables(uri)).thenReturn(Promise.resolve(env)); - when(activationService.getActivatedEnvironmentVariables(uri)).thenReturn(); - - await factory.createLanguageClient(uri, options); - - verify(configurationService.getSettings(uri)).once(); - verify(downloadFactory.createLanguageClient(uri, options, env)).never(); - verify(simpleFactory.createLanguageClient(uri, options, env)).once(); - }); - test('Download factory will make use of the language server folder name and client will be created', async () => { - const platformData = mock(PlatformData); - const lsFolderService = mock(LanguageServerFolderService); - const factory = new DownloadedLanguageClientFactory(instance(platformData), instance(lsFolderService)); - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - const languageServerFolder = 'some folder name'; - const engineDllName = 'xyz.dll'; - when(lsFolderService.getLanguageServerFolderName(anything())) - .thenResolve(languageServerFolder); - when(platformData.engineExecutableName).thenReturn(engineDllName); - - const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, engineDllName); - const expectedServerOptions = { - run: { command: serverModule, args: [], options: { stdio: 'pipe', env: { FOO: 'bar' } } }, - debug: { command: serverModule, args: ['--debug'], options: { stdio: 'pipe', env: { FOO: 'bar' } } } - }; - rewiremock.enable(); - - class MockClass { - constructor(language: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions) { - expect(language).to.be.equal('python'); - expect(name).to.be.equal(languageClientName); - expect(clientOptions).to.be.deep.equal(options); - expect(serverOptions).to.be.deep.equal(expectedServerOptions); - } - } - rewiremock('vscode-languageclient').with({ LanguageClient: MockClass }); - - const client = await factory.createLanguageClient(uri, options, { FOO: 'bar' }); - - verify(lsFolderService.getLanguageServerFolderName(anything())).once(); - verify(platformData.engineExecutableName).atLeast(1); - verify(platformData.engineDllName).never(); - verify(platformData.platformName).never(); - expect(client).to.be.instanceOf(MockClass); - }); - test('Simple factory will make use of the language server folder name and client will be created', async () => { - const platformData = mock(PlatformData); - const lsFolderService = mock(LanguageServerFolderService); - const factory = new SimpleLanguageClientFactory(instance(platformData), instance(lsFolderService)); - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - const languageServerFolder = 'some folder name'; - const engineDllName = 'xyz.dll'; - when(lsFolderService.getLanguageServerFolderName(anything())).thenResolve(languageServerFolder); - when(platformData.engineDllName).thenReturn(engineDllName); - - const serverModule = path.join(EXTENSION_ROOT_DIR, languageServerFolder, engineDllName); - const expectedServerOptions = { - run: { command: dotNetCommand, args: [serverModule], options: { stdio: 'pipe', env: { FOO: 'bar' } } }, - debug: { command: dotNetCommand, args: [serverModule, '--debug'], options: { stdio: 'pipe', env: { FOO: 'bar' } } } - }; - rewiremock.enable(); - - class MockClass { - constructor(language: string, name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions) { - expect(language).to.be.equal('python'); - expect(name).to.be.equal(languageClientName); - expect(clientOptions).to.be.deep.equal(options); - expect(serverOptions).to.be.deep.equal(expectedServerOptions); - } - } - rewiremock('vscode-languageclient').with({ LanguageClient: MockClass }); - - const client = await factory.createLanguageClient(uri, options, { FOO: 'bar' }); - - verify(lsFolderService.getLanguageServerFolderName(anything())).once(); - verify(platformData.engineExecutableName).never(); - verify(platformData.engineDllName).once(); - verify(platformData.platformName).never(); - expect(client).to.be.instanceOf(MockClass); - }); -}); diff --git a/src/test/activation/languageServer/languageServer.unit.test.ts b/src/test/activation/languageServer/languageServer.unit.test.ts deleted file mode 100644 index 30ea9f33bc76..000000000000 --- a/src/test/activation/languageServer/languageServer.unit.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; -import { BaseLanguageClientFactory } from '../../../client/activation/languageServer/languageClientFactory'; -import { LanguageServer } from '../../../client/activation/languageServer/languageServer'; -import { ILanguageClientFactory } from '../../../client/activation/types'; -import '../../../client/common/extensions'; -import { IConfigurationService, IDisposable, IPythonSettings } from '../../../client/common/types'; -import { sleep } from '../../../client/common/utils/async'; -import { UnitTestManagementService } from '../../../client/testing/main'; -import { ITestManagementService } from '../../../client/testing/types'; - -//tslint:disable:no-require-imports no-require-imports no-var-requires no-any no-unnecessary-class max-func-body-length - -suite('Language Server - LanguageServer', () => { - class LanguageServerTest extends LanguageServer { - // tslint:disable-next-line:no-unnecessary-override - public async registerTestServices() { - return super.registerTestServices(); - } - } - let clientFactory: ILanguageClientFactory; - let server: LanguageServerTest; - let client: typemoq.IMock<LanguageClient>; - let testManager: ITestManagementService; - let configService: typemoq.IMock<IConfigurationService>; - setup(() => { - client = typemoq.Mock.ofType<LanguageClient>(); - clientFactory = mock(BaseLanguageClientFactory); - testManager = mock(UnitTestManagementService); - configService = typemoq.Mock.ofType<IConfigurationService>(); - server = new LanguageServerTest(instance(clientFactory), instance(testManager), configService.object); - }); - teardown(() => { - client.setup(c => c.stop()).returns(() => Promise.resolve()); - server.dispose(); - }); - test('Loading extension will not throw an error if not activated', () => { - expect(() => server.loadExtension()).not.throw(); - }); - test('Loading extension will not throw an error if not activated but after it loads message will be sent', async () => { - const loadExtensionArgs = { x: 1 }; - - expect(() => server.loadExtension({ a: '2' })).not.throw(); - - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - - const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - pythonSettings - .setup(p => p.downloadLanguageServer) - .returns(() => true); - configService - .setup(c => c.getSettings(uri)) - .returns(() => pythonSettings.object); - - const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); - client - .setup(c => c.onTelemetry(typemoq.It.isAny())) - .returns(() => onTelemetryDisposable.object); - - client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); - const startDisposable = typemoq.Mock.ofType<IDisposable>(); - client.setup(c => c.stop()).returns(() => Promise.resolve()); - client - .setup(c => c.start()) - .returns(() => startDisposable.object) - .verifiable(typemoq.Times.once()); - client - .setup(c => - c.sendRequest(typemoq.It.isValue('python/loadExtension'), typemoq.It.isValue(loadExtensionArgs)) - ) - .returns(() => Promise.resolve(undefined) as any); - - expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - client - .setup(c => c.initializeResult) - .returns(() => false as any) - .verifiable(typemoq.Times.once()); - - server.start(uri, options).ignoreErrors(); - - // Even though server has started request should not yet be sent out. - // Not untill language client has initialized. - expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - - // // Initialize language client and verify that the request was sent out. - client - .setup(c => c.initializeResult) - .returns(() => true as any) - .verifiable(typemoq.Times.once()); - await sleep(120); - - verify(testManager.activate(anything())).once(); - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.atLeast(2)); - }); - test('Send telemetry when LS has started and disposes appropriately', async () => { - const loadExtensionArgs = { x: 1 }; - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - - const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - pythonSettings - .setup(p => p.downloadLanguageServer) - .returns(() => true); - configService - .setup(c => c.getSettings(uri)) - .returns(() => pythonSettings.object); - - const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); - client - .setup(c => c.onTelemetry(typemoq.It.isAny())) - .returns(() => onTelemetryDisposable.object); - - client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); - const startDisposable = typemoq.Mock.ofType<IDisposable>(); - client.setup(c => c.stop()).returns(() => Promise.resolve()); - client - .setup(c => c.start()) - .returns(() => startDisposable.object) - .verifiable(typemoq.Times.once()); - client - .setup(c => - c.sendRequest(typemoq.It.isValue('python/loadExtension'), typemoq.It.isValue(loadExtensionArgs)) - ) - .returns(() => Promise.resolve(undefined) as any); - - expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - client - .setup(c => c.initializeResult) - .returns(() => false as any) - .verifiable(typemoq.Times.once()); - - const promise = server.start(uri, options); - - // Even though server has started request should not yet be sent out. - // Not untill language client has initialized. - expect(() => server.loadExtension(loadExtensionArgs)).not.throw(); - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - - // // Initialize language client and verify that the request was sent out. - client - .setup(c => c.initializeResult) - .returns(() => true as any) - .verifiable(typemoq.Times.once()); - await sleep(120); - - verify(testManager.activate(anything())).once(); - expect(() => server.loadExtension(loadExtensionArgs)).to.not.throw(); - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); - client.verify(c => c.stop(), typemoq.Times.never()); - - await promise; - server.dispose(); - - client.verify(c => c.stop(), typemoq.Times.once()); - startDisposable.verify(d => d.dispose(), typemoq.Times.once()); - }); - test('Ensure Errors raised when starting test manager are not bubbled up', async () => { - await server.registerTestServices(); - }); - test('Register telemetry handler if LS was downloadeded', async () => { - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - - const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - pythonSettings - .setup(p => p.downloadLanguageServer) - .returns(() => true) - .verifiable(typemoq.Times.once()); - configService - .setup(c => c.getSettings(uri)) - .returns(() => pythonSettings.object) - .verifiable(typemoq.Times.once()); - - const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); - client - .setup(c => c.onTelemetry(typemoq.It.isAny())) - .returns(() => onTelemetryDisposable.object) - .verifiable(typemoq.Times.once()); - - client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); - const startDisposable = typemoq.Mock.ofType<IDisposable>(); - client.setup(c => c.stop()).returns(() => Promise.resolve()); - client - .setup(c => c.start()) - .returns(() => startDisposable.object) - .verifiable(typemoq.Times.once()); - - server.start(uri, options).ignoreErrors(); - - // Initialize language client and verify that the request was sent out. - client - .setup(c => c.initializeResult) - .returns(() => true as any) - .verifiable(typemoq.Times.once()); - await sleep(120); - - verify(testManager.activate(anything())).once(); - - client.verify(c => c.onTelemetry(typemoq.It.isAny()), typemoq.Times.once()); - pythonSettings.verifyAll(); - configService.verifyAll(); - }); - test('Do not register telemetry handler if LS was not downloadeded', async () => { - client.verify(c => c.sendRequest(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - - const uri = Uri.file(__filename); - const options = typemoq.Mock.ofType<LanguageClientOptions>().object; - - const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - pythonSettings - .setup(p => p.downloadLanguageServer) - .returns(() => false) - .verifiable(typemoq.Times.once()); - configService - .setup(c => c.getSettings(uri)) - .returns(() => pythonSettings.object) - .verifiable(typemoq.Times.once()); - - const onTelemetryDisposable = typemoq.Mock.ofType<IDisposable>(); - client - .setup(c => c.onTelemetry(typemoq.It.isAny())) - .returns(() => onTelemetryDisposable.object) - .verifiable(typemoq.Times.once()); - - client.setup(c => (c as any).then).returns(() => undefined); - when(clientFactory.createLanguageClient(uri, options)).thenResolve(client.object); - const startDisposable = typemoq.Mock.ofType<IDisposable>(); - client.setup(c => c.stop()).returns(() => Promise.resolve()); - client - .setup(c => c.start()) - .returns(() => startDisposable.object) - .verifiable(typemoq.Times.once()); - - server.start(uri, options).ignoreErrors(); - - // Initialize language client and verify that the request was sent out. - client - .setup(c => c.initializeResult) - .returns(() => true as any) - .verifiable(typemoq.Times.once()); - await sleep(120); - - verify(testManager.activate(anything())).once(); - - client.verify(c => c.onTelemetry(typemoq.It.isAny()), typemoq.Times.never()); - pythonSettings.verifyAll(); - configService.verifyAll(); - }); -}); 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/languageServerExtension.unit.test.ts b/src/test/activation/languageServer/languageServerExtension.unit.test.ts deleted file mode 100644 index 3c7739a690dd..000000000000 --- a/src/test/activation/languageServer/languageServerExtension.unit.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../client/common/application/types'; -import { IDisposable } from '../../../client/common/types'; - -use(chaiAsPromised); - -// tslint:disable:max-func-body-length no-any chai-vague-errors no-unused-expression - -const loadExtensionCommand = 'python._loadLanguageServerExtension'; - -suite('Language Server - Language Server Extension', () => { - class LanguageServerExtensionTest extends LanguageServerExtension { - // tslint:disable-next-line:no-unnecessary-override - public async register(): Promise<void> { - return super.register(); - } - public clearLoadExtensionArgs() { - super.loadExtensionArgs = undefined; - } - } - let extension: LanguageServerExtensionTest; - let cmdManager: ICommandManager; - let commandRegistrationDisposable: typemoq.IMock<IDisposable>; - setup(() => { - cmdManager = mock(CommandManager); - commandRegistrationDisposable = typemoq.Mock.ofType<IDisposable>(); - extension = new LanguageServerExtensionTest(instance(cmdManager)); - extension.clearLoadExtensionArgs(); - }); - test('Must register command handler', async () => { - when(cmdManager.registerCommand(loadExtensionCommand, anything())).thenReturn( - commandRegistrationDisposable.object - ); - await extension.register(); - verify(cmdManager.registerCommand(loadExtensionCommand, anything())).once(); - extension.dispose(); - commandRegistrationDisposable.verify(d => d.dispose(), typemoq.Times.once()); - }); -}); diff --git a/src/test/activation/languageServer/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 5e32bf6253dd..000000000000 --- a/src/test/activation/languageServer/languageServerPackageService.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any no-invalid-this max-func-body-length - -import { expect } from 'chai'; -import * as typeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { LanguageServerPackageStorageContainers } from '../../../client/activation/languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; -import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; -import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository'; -import { NugetService } from '../../../client/common/nuget/nugetService'; -import { INugetRepository, INugetService } from '../../../client/common/nuget/types'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { IPlatformService } from '../../../client/common/platform/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; -const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; - -suite('Language Server Package Service', () => { - let serviceContainer: typeMoq.IMock<IServiceContainer>; - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - }); - test('Ensure new Major versions of Language Server is accounted for (azure blob)', async () => { - const nugetService = new NugetService(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService); - const platformService = new PlatformService(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IPlatformService))).returns(() => platformService); - const workspace = typeMoq.Mock.ofType<IWorkspaceService>(); - const cfg = typeMoq.Mock.ofType<WorkspaceConfiguration>(); - cfg.setup(c => c.get('proxyStrictSSL', true)) - .returns(() => true); - workspace.setup(w => w.getConfiguration('http', undefined)) - .returns(() => cfg.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspace.object); - const defaultStorageChannel = 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, undefined); - - const latestReleases = packages - .filter(item => nugetService.isReleaseVersion(item.version)) - .sort((a, b) => a.version.compare(b.version)); - const latestRelease = latestReleases[latestReleases.length - 1]; - - expect(packages).to.be.length.greaterThan(0, 'No packages returned.'); - expect(latestReleases).to.be.length.greaterThan(0, 'No release packages returned.'); - expect(latestRelease.version.major).to.be.equal(lsPackageService.maxMajorVersion, 'New Major version of Language server has been released, we need to update it at our end.'); - }); -}); diff --git a/src/test/activation/languageServer/languageServerPackageService.unit.test.ts b/src/test/activation/languageServer/languageServerPackageService.unit.test.ts deleted file mode 100644 index dd6b47c87650..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/types'; -import { IApplicationEnvironment } from '../../../client/common/application/types'; -import { NugetService } from '../../../client/common/nuget/nugetService'; -import { INugetRepository, INugetService, NugetPackage } from '../../../client/common/nuget/types'; -import { IPlatformService } from '../../../client/common/platform/types'; -import { OSType } from '../../../client/common/utils/platform'; -import { IServiceContainer } from '../../../client/ioc/types'; - -const downloadBaseFileName = 'Python-Language-Server'; - -suite('Language', () => { - 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), typeMoq.It.isAny())) - .returns(() => Promise.resolve(packages)) - .verifiable(typeMoq.Times.once()); - nuget - .setup(n => n.isReleaseVersion(typeMoq.It.isAny())) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - - const info = await lsPackageService.getLatestNugetPackageVersion(undefined); - - repo.verifyAll(); - nuget.verifyAll(); - expect(info).to.deep.equal(expectedPackage); - }); - test('Get latest nuget package version (excluding non-release)', async () => { - setMinVersionOfLs('0.0.1'); - const packageName = 'packageName'; - lsPackageService.getNugetPackageName = () => packageName; - lsPackageService.maxMajorVersion = 1; - const packages: NugetPackage[] = [ - { package: '', uri: '', version: new SemVer('1.1.1') }, - { package: '', uri: '', version: new SemVer('1.3.1-alpha') }, - { package: '', uri: '', version: new SemVer('1.4.1-preview') }, - { package: '', uri: '', version: new SemVer('1.2.1-internal') } - ]; - const expectedPackage = packages[0]; - const repo = typeMoq.Mock.ofType<INugetRepository>(); - const nuget = new NugetService(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetRepository), typeMoq.It.isAny())).returns(() => repo.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetService))).returns(() => nuget); - - repo - .setup(n => n.getPackages(typeMoq.It.isValue(packageName), typeMoq.It.isAny())) - .returns(() => Promise.resolve(packages)) - .verifiable(typeMoq.Times.once()); - - const info = await lsPackageService.getLatestNugetPackageVersion(undefined); - - repo.verifyAll(); - expect(info).to.deep.equal(expectedPackage); - }); - test('Ensure minimum version of package is used', async () => { - const minimumVersion = '0.1.50'; - setMinVersionOfLs(minimumVersion); - const packageName = 'packageName'; - lsPackageService.getNugetPackageName = () => packageName; - lsPackageService.maxMajorVersion = 0; - const packages: NugetPackage[] = [ - { package: '', uri: '', version: new SemVer('0.1.48') }, - { package: '', uri: '', version: new SemVer('0.1.49') } - ]; - const repo = typeMoq.Mock.ofType<INugetRepository>(); - const nuget = new NugetService(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetRepository), typeMoq.It.isAny())).returns(() => repo.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetService))).returns(() => nuget); - - repo - .setup(n => n.getPackages(typeMoq.It.isValue(packageName), typeMoq.It.isAny())) - .returns(() => Promise.resolve(packages)) - .verifiable(typeMoq.Times.once()); - - const info = await lsPackageService.getLatestNugetPackageVersion(undefined); - - 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/languageServer/manager.unit.test.ts b/src/test/activation/languageServer/manager.unit.test.ts deleted file mode 100644 index c1fdf216b75c..000000000000 --- a/src/test/activation/languageServer/manager.unit.test.ts +++ /dev/null @@ -1,166 +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 { instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; -import { LanguageServerAnalysisOptions } from '../../../client/activation/languageServer/analysisOptions'; -import { LanguageServer } from '../../../client/activation/languageServer/languageServer'; -import { LanguageServerExtension } from '../../../client/activation/languageServer/languageServerExtension'; -import { LanguageServerManager } from '../../../client/activation/languageServer/manager'; -import { ILanguageServer, ILanguageServerAnalysisOptions, ILanguageServerExtension } from '../../../client/activation/types'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { sleep } from '../../core'; - -use(chaiAsPromised); - -// tslint:disable:max-func-body-length no-any chai-vague-errors no-unused-expression - -suite('Language Server - Manager', () => { - let manager: LanguageServerManager; - let serviceContainer: IServiceContainer; - let analysisOptions: ILanguageServerAnalysisOptions; - let languageServer: ILanguageServer; - let lsExtension: ILanguageServerExtension; - let onChangeAnalysisHandler: Function; - const languageClientOptions = ({ x: 1 } as any) as LanguageClientOptions; - setup(() => { - serviceContainer = mock(ServiceContainer); - analysisOptions = mock(LanguageServerAnalysisOptions); - languageServer = mock(LanguageServer); - lsExtension = mock(LanguageServerExtension); - manager = new LanguageServerManager( - instance(serviceContainer), - instance(analysisOptions), - instance(lsExtension) - ); - }); - - [undefined, Uri.file(__filename)].forEach(resource => { - async function startLanguageServer() { - let invoked = false; - const lsExtensionChangeFn = (_handler: Function) => { - invoked = true; - }; - when(lsExtension.invoked).thenReturn(lsExtensionChangeFn as any); - - let analysisHandlerRegistered = false; - const analysisChangeFn = (handler: Function) => { - analysisHandlerRegistered = true; - onChangeAnalysisHandler = handler; - }; - when(analysisOptions.initialize(resource)).thenResolve(); - when(analysisOptions.getAnalysisOptions()).thenResolve(languageClientOptions); - when(analysisOptions.onDidChange).thenReturn(analysisChangeFn as any); - when(serviceContainer.get<ILanguageServer>(ILanguageServer)).thenReturn(instance(languageServer)); - when(languageServer.start(resource, languageClientOptions)).thenResolve(); - - await manager.start(resource); - - verify(analysisOptions.initialize(resource)).once(); - verify(analysisOptions.getAnalysisOptions()).once(); - verify(serviceContainer.get<ILanguageServer>(ILanguageServer)).once(); - verify(languageServer.start(resource, languageClientOptions)).once(); - expect(invoked).to.be.true; - expect(analysisHandlerRegistered).to.be.true; - verify(languageServer.dispose()).never(); - } - test('Start must register handlers and initialize analysis options', async () => { - await startLanguageServer(); - - manager.dispose(); - - verify(languageServer.dispose()).once(); - }); - test('Attempting to start LS will throw an exception', async () => { - await startLanguageServer(); - - await expect(manager.start(resource)).to.eventually.be.rejectedWith('Language Server already started'); - }); - test('Changes in analysis options must restart LS', async () => { - await startLanguageServer(); - - await onChangeAnalysisHandler.call(manager); - await sleep(1); - - verify(languageServer.dispose()).once(); - - verify(analysisOptions.getAnalysisOptions()).twice(); - verify(serviceContainer.get<ILanguageServer>(ILanguageServer)).twice(); - verify(languageServer.start(resource, languageClientOptions)).twice(); - }); - test('Changes in analysis options must throttled when restarting LS', async () => { - await startLanguageServer(); - - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await Promise.all([ - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager) - ]); - await sleep(1); - - verify(languageServer.dispose()).once(); - - verify(analysisOptions.getAnalysisOptions()).twice(); - verify(serviceContainer.get<ILanguageServer>(ILanguageServer)).twice(); - verify(languageServer.start(resource, languageClientOptions)).twice(); - }); - test('Multiple changes in analysis options must restart LS twice', async () => { - await startLanguageServer(); - - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await Promise.all([ - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager) - ]); - await sleep(1); - - verify(languageServer.dispose()).once(); - - verify(analysisOptions.getAnalysisOptions()).twice(); - verify(serviceContainer.get<ILanguageServer>(ILanguageServer)).twice(); - verify(languageServer.start(resource, languageClientOptions)).twice(); - - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await onChangeAnalysisHandler.call(manager); - await Promise.all([ - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager), - onChangeAnalysisHandler.call(manager) - ]); - await sleep(1); - - verify(languageServer.dispose()).twice(); - - verify(analysisOptions.getAnalysisOptions()).thrice(); - verify(serviceContainer.get<ILanguageServer>(ILanguageServer)).thrice(); - verify(languageServer.start(resource, languageClientOptions)).thrice(); - }); - test('Must load extension when command was been sent before starting LS', async () => { - const args = { x: 1 }; - when(lsExtension.loadExtensionArgs).thenReturn(args as any); - - await startLanguageServer(); - - verify(languageServer.loadExtension(args)).once(); - }); - }); -}); diff --git a/src/test/activation/languageServer/platformData.unit.test.ts b/src/test/activation/languageServer/platformData.unit.test.ts deleted file mode 100644 index c813d8b965be..000000000000 --- a/src/test/activation/languageServer/platformData.unit.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-unused-variable -import * as assert from 'assert'; -import * as TypeMoq from 'typemoq'; -import { PlatformData, PlatformLSExecutables } from '../../../client/activation/languageServer/platformData'; -import { IPlatformService } from '../../../client/common/platform/types'; - -const testDataWinMac = [ - { isWindows: true, is64Bit: true, expectedName: 'win-x64' }, - { isWindows: true, is64Bit: false, expectedName: 'win-x86' }, - { isWindows: false, is64Bit: true, expectedName: 'osx-x64' } -]; - -const testDataLinux = [ - { name: 'centos', expectedName: 'linux-x64' }, - { name: 'debian', expectedName: 'linux-x64' }, - { name: 'fedora', expectedName: 'linux-x64' }, - { name: 'ol', expectedName: 'linux-x64' }, - { name: 'opensuse', expectedName: 'linux-x64' }, - { name: 'rhel', expectedName: 'linux-x64' }, - { name: 'ubuntu', expectedName: 'linux-x64' } -]; - -const testDataModuleName = [ - { isWindows: true, isMac: false, isLinux: false, expectedName: PlatformLSExecutables.Windows }, - { isWindows: false, isMac: true, isLinux: false, expectedName: PlatformLSExecutables.MacOS }, - { isWindows: false, isMac: false, isLinux: true, expectedName: PlatformLSExecutables.Linux } -]; - -// tslint:disable-next-line:max-func-body-length -suite('Activation - platform data', () => { - test('Name and hash (Windows/Mac)', async () => { - for (const t of testDataWinMac) { - const platformService = TypeMoq.Mock.ofType<IPlatformService>(); - platformService.setup(x => x.isWindows).returns(() => t.isWindows); - platformService.setup(x => x.isMac).returns(() => !t.isWindows); - platformService.setup(x => x.is64bit).returns(() => t.is64Bit); - - const pd = new PlatformData(platformService.object); - - const actual = pd.platformName; - assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); - } - }); - test('Name and hash (Linux)', async () => { - for (const t of testDataLinux) { - const platformService = TypeMoq.Mock.ofType<IPlatformService>(); - platformService.setup(x => x.isWindows).returns(() => false); - platformService.setup(x => x.isMac).returns(() => false); - platformService.setup(x => x.isLinux).returns(() => true); - platformService.setup(x => x.is64bit).returns(() => true); - - const pd = new PlatformData(platformService.object); - - const actual = pd.platformName; - assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); - } - }); - test('Module name', async () => { - for (const t of testDataModuleName) { - const platformService = TypeMoq.Mock.ofType<IPlatformService>(); - platformService.setup(x => x.isWindows).returns(() => t.isWindows); - platformService.setup(x => x.isLinux).returns(() => t.isLinux); - platformService.setup(x => x.isMac).returns(() => t.isMac); - - const pd = new PlatformData(platformService.object); - - const actual = pd.engineExecutableName; - assert.equal(actual, t.expectedName, `${actual} does not match ${t.expectedName}`); - } - }); -}); diff --git a/src/test/activation/node/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/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 53044c724c5c..90e433f91647 100644 --- a/src/test/analysisEngineTest.ts +++ b/src/test/analysisEngineTest.ts @@ -3,7 +3,6 @@ 'use strict'; -// tslint:disable:no-console no-require-imports no-var-requires import * as path from 'path'; process.env.CODE_TESTS_WORKSPACE = path.join(__dirname, '..', '..', 'src', 'test'); @@ -13,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 dba79fc92019..3a2b9c2f62dd 100644 --- a/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts +++ b/src/test/application/diagnostics/applicationDiagnostics.unit.test.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:insecure-random no-any - import * as assert from 'assert'; import { anything, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; @@ -12,48 +10,46 @@ import { DiagnosticSeverity } from 'vscode'; import { ApplicationDiagnostics } from '../../../client/application/diagnostics/applicationDiagnostics'; import { EnvironmentPathVariableDiagnosticsService } from '../../../client/application/diagnostics/checks/envPathVariable'; import { InvalidPythonInterpreterService } from '../../../client/application/diagnostics/checks/pythonInterpreter'; -import { DiagnosticScope, IDiagnostic, IDiagnosticsService, ISourceMapSupportService } from '../../../client/application/diagnostics/types'; +import { DiagnosticScope, IDiagnostic, IDiagnosticsService } from '../../../client/application/diagnostics/types'; import { IApplicationDiagnostics } from '../../../client/application/types'; -import { STANDARD_OUTPUT_CHANNEL } from '../../../client/common/constants'; -import { 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 lsNotSupportedCheck: typemoq.IMock<IDiagnosticsService>; let pythonInterpreterCheck: typemoq.IMock<IDiagnosticsService>; - let outputChannel: typemoq.IMock<IOutputChannel>; - let logger: typemoq.IMock<ILogger>; + 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(() => { - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; + delete process.env.VSC_PYTHON_UNIT_TEST; + delete process.env.VSC_PYTHON_CI_TEST; serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); envHealthCheck = typemoq.Mock.ofType<IDiagnosticsService>(); - envHealthCheck.setup(service => service.runInBackground).returns(() => true); + envHealthCheck.setup((service) => service.runInBackground).returns(() => true); lsNotSupportedCheck = typemoq.Mock.ofType<IDiagnosticsService>(); - lsNotSupportedCheck.setup(service => service.runInBackground).returns(() => false); + lsNotSupportedCheck.setup((service) => service.runInBackground).returns(() => false); pythonInterpreterCheck = typemoq.Mock.ofType<IDiagnosticsService>(); - pythonInterpreterCheck.setup(service => service.runInBackground).returns(() => false); - outputChannel = typemoq.Mock.ofType<IOutputChannel>(); - logger = typemoq.Mock.ofType<ILogger>(); + 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))) + serviceContainer + .setup((d) => d.getAll(typemoq.It.isValue(IDiagnosticsService))) .returns(() => [envHealthCheck.object, lsNotSupportedCheck.object, pythonInterpreterCheck.object]); - serviceContainer.setup(d => d.get(typemoq.It.isValue(IOutputChannel), typemoq.It.isValue(STANDARD_OUTPUT_CHANNEL))) - .returns(() => outputChannel.object); - serviceContainer.setup(d => d.get(typemoq.It.isValue(ILogger))) - .returns(() => logger.object); + serviceContainer + .setup((d) => d.get(typemoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); - appDiagnostics = new ApplicationDiagnostics(serviceContainer.object, outputChannel.object); + appDiagnostics = new ApplicationDiagnostics(serviceContainer.object); }); teardown(() => { @@ -61,28 +57,42 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; }); - test('Register should register source maps', () => { - const sourceMapService = typemoq.Mock.ofType<ISourceMapSupportService>(); - sourceMapService.setup(s => s.register()).verifiable(typemoq.Times.once()); - - serviceContainer.setup(d => d.get(typemoq.It.isValue(ISourceMapSupportService), typemoq.It.isAny())) - .returns(() => sourceMapService.object); + test('Performing Pre Startup Health Check must diagnose all validation checks', async () => { + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) + .returns(() => Promise.resolve([])) + .verifiable(typemoq.Times.once()); - appDiagnostics.register(); + await appDiagnostics.performPreStartupHealthCheck(undefined); - sourceMapService.verifyAll(); + envHealthCheck.verifyAll(); + lsNotSupportedCheck.verifyAll(); + pythonInterpreterCheck.verifyAll(); }); - test('Performing Pre Startup Health Check must diagnose all validation checks', async () => { - envHealthCheck.setup(e => e.diagnose(typemoq.It.isAny())) + 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())) + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); - pythonInterpreterCheck.setup(p => p.diagnose(typemoq.It.isAny())) + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) - .verifiable(typemoq.Times.once()); + .verifiable(typemoq.Times.never()); await appDiagnostics.performPreStartupHealthCheck(undefined); @@ -98,24 +108,30 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { scope: undefined, severity: undefined, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', } as any; - envHealthCheck.setup(e => e.diagnose(typemoq.It.isAny())) + 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]))) + envHealthCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - lsNotSupportedCheck.setup(p => p.diagnose(typemoq.It.isAny())) + 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]))) + lsNotSupportedCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); - pythonInterpreterCheck.setup(p => p.diagnose(typemoq.It.isAny())) + 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]))) + pythonInterpreterCheck + .setup((p) => p.handle(typemoq.It.isValue([diagnostic]))) .returns(() => Promise.resolve()) .verifiable(typemoq.Times.once()); @@ -129,72 +145,50 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { 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}` as any, message: `Error${i}`, scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, severity: DiagnosticSeverity.Error, resource: undefined, - invokeHandler: 'default' + 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}` as any, message: `Warning${i}`, scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, severity: DiagnosticSeverity.Warning, resource: undefined, - invokeHandler: 'default' + 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}` as any, message: `Info${i}`, scope: i % 2 === 0 ? DiagnosticScope.Global : DiagnosticScope.WorkspaceFolder, severity: DiagnosticSeverity.Information, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; diagnostics.push(diagnostic); } - for (const diagnostic of diagnostics) { - const message = `Diagnostic Code: ${diagnostic.code}, Message: ${diagnostic.message}`; - switch (diagnostic.severity) { - case DiagnosticSeverity.Error: { - 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(typemoq.It.isAny())) + envHealthCheck + .setup((e) => e.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve(diagnostics)) .verifiable(typemoq.Times.once()); - lsNotSupportedCheck.setup(p => p.diagnose(typemoq.It.isAny())) + lsNotSupportedCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); - pythonInterpreterCheck.setup(p => p.diagnose(typemoq.It.isAny())) + pythonInterpreterCheck + .setup((p) => p.diagnose(typemoq.It.isAny())) .returns(() => Promise.resolve([])) .verifiable(typemoq.Times.once()); @@ -204,25 +198,28 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { envHealthCheck.verifyAll(); lsNotSupportedCheck.verifyAll(); pythonInterpreterCheck.verifyAll(); - outputChannel.verifyAll(); - logger.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.getAll<IDiagnosticsService>(IDiagnosticsService)) - .thenReturn([instance(foreGroundService), instance(backGroundService)]); + 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), outputChannel.object); + const service = new ApplicationDiagnostics(instance(svcContainer)); const promise = service.performPreStartupHealthCheck(undefined); const deferred = createDeferredFromPromise(promise); @@ -231,16 +228,15 @@ suite('Application Diagnostics - ApplicationDiagnostics', () => { verify(foreGroundService.runInBackground).atLeast(1); verify(backGroundService.runInBackground).atLeast(1); - assert.equal(deferred.completed, false); + assert.strictEqual(deferred.completed, false); foreGroundDeferred.resolve([]); await sleep(1); - assert.equal(deferred.completed, true); + assert.strictEqual(deferred.completed, true); backgroundGroundDeferred.resolve([]); await sleep(1); 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 07df13ce32e7..c6c4ff06ee74 100644 --- a/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts +++ b/src/test/application/diagnostics/checks/envPathVariable.unit.test.ts @@ -11,15 +11,24 @@ import { BaseDiagnosticsService } from '../../../../client/application/diagnosti 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 { + DiagnosticCommandPromptHandlerServiceId, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + DiagnosticScope, + IDiagnostic, + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, + IDiagnosticsService, +} from '../../../../client/application/diagnostics/types'; import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; import { EnvironmentVariables } from '../../../../client/common/variables/types'; import { IServiceContainer } from '../../../../client/ioc/types'; -// tslint:disable:max-func-body-length no-any suite('Application Diagnostics - Checks Env Path Variable', () => { let diagnosticService: IDiagnosticsService; let platformService: typemoq.IMock<IPlatformService>; @@ -34,56 +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); + 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))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); - workspaceService.setup(w => w.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())).returns(() => undefined); - diagnosticService = new class extends EnvironmentPathVariableDiagnosticsService { + diagnosticService = new (class extends EnvironmentPathVariableDiagnosticsService { public _clear() { while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); } } - }(serviceContainer.object, []); + })(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()); @@ -93,7 +109,8 @@ 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) + diagnostic + .setup((d) => d.code) .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); @@ -102,39 +119,33 @@ 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); + 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); + 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(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(undefined); @@ -146,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(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]); @@ -183,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/invalidLaunchJsonDebugger.unit.test.ts b/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts deleted file mode 100644 index eeba414f9846..000000000000 --- a/src/test/application/diagnostics/checks/invalidLaunchJsonDebugger.unit.test.ts +++ /dev/null @@ -1,376 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidLaunchJsonDebuggerDiagnostic, InvalidLaunchJsonDebuggerService } from '../../../../client/application/diagnostics/checks/invalidLaunchJsonDebugger'; -import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { IDiagnostic, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IFileSystem } from '../../../../client/common/platform/types'; -import { Diagnostics } from '../../../../client/common/utils/localize'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -// tslint:disable:max-func-body-length no-any -suite('Application Diagnostics - Checks if launch.json is invalid', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let diagnosticService: IDiagnosticsService; - let commandFactory: TypeMoq.IMock<IDiagnosticsCommandFactory>; - let fs: TypeMoq.IMock<IFileSystem>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let baseWorkspaceService: TypeMoq.IMock<IWorkspaceService>; - let messageHandler: TypeMoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; - let workspaceFolder: WorkspaceFolder; - setup(() => { - workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - commandFactory = TypeMoq.Mock.ofType<IDiagnosticsCommandFactory>(); - fs = TypeMoq.Mock.ofType<IFileSystem>(); - messageHandler = TypeMoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - baseWorkspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer - .setup(s => s.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => baseWorkspaceService.object); - - diagnosticService = new class extends InvalidLaunchJsonDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - public async fixLaunchJson(code: DiagnosticCodes) { - await super.fixLaunchJson(code); - } - }(serviceContainer.object, fs.object, [], workspaceService.object, messageHandler.object); - (diagnosticService as any)._clear(); - }); - - test('Can handle all InvalidLaunchJsonDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic - ]) { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - - test('Can not handle non-InvalidLaunchJsonDebugger diagnostics', async () => { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => 'Something Else' as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - - test('Should return empty diagnostics if there are no workspace folders', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not exist', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.getWorkspaceFolder(undefined)) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return empty diagnostics if file launch.json does not contain strings "pythonExperimental" and "debugStdLib" ', async () => { - const fileContents = 'Hello I am launch.json, although I am not very jsony'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return InvalidDebuggerTypeDiagnostic if file launch.json contains string "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "pythonExperimental"'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([ - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined) - ], 'not the same'); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return JustMyCodeDiagnostic if file launch.json contains string "debugStdLib"', async () => { - const fileContents = 'Hello I am launch.json, I contain string "debugStdLib"'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([ - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined) - ], 'not the same'); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('Should return both diagnostics if file launch.json contains string "debugStdLib" and "pythonExperimental"', async () => { - const fileContents = 'Hello I am launch.json, I contain both "debugStdLib" and "pythonExperimental"'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(fileContents)) - .verifiable(TypeMoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([ - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.InvalidDebuggerTypeDiagnostic, undefined), - new InvalidLaunchJsonDebuggerDiagnostic(DiagnosticCodes.JustMyCodeDiagnostic, undefined) - ], 'not the same'); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('All InvalidLaunchJsonDebugger diagnostics should display 2 options to with one command', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic - ]) { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup(d => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler - .setup(m => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(TypeMoq.Times.atLeastOnce()); - baseWorkspaceService - .setup(c => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.atLeastOnce()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(2); - expect(options!.commandPrompts[0].prompt).to.be.equal(Diagnostics.yesUpdateLaunch()); - expect(options!.commandPrompts[0].command).not.to.be.equal(undefined, 'Command not set'); - } - }); - - test('All InvalidLaunchJsonDebugger diagnostics should display message twice if invoked twice', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic - ]) { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => code) - .verifiable(TypeMoq.Times.atLeastOnce()); - diagnostic - .setup(d => d.invokeHandler) - .returns(() => 'always') - .verifiable(TypeMoq.Times.atLeastOnce()); - messageHandler.reset(); - messageHandler - .setup(m => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.exactly(2)); - baseWorkspaceService - .setup(c => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder) - .verifiable(TypeMoq.Times.never()); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - baseWorkspaceService.verifyAll(); - } - }); - - test('Function fixLaunchJson() returns if there are no workspace folders', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic - ]) { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.never()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - } - }); - - test('Function fixLaunchJson() returns if file launch.json does not exist', async () => { - for (const code of [ - DiagnosticCodes.InvalidDebuggerTypeDiagnostic, - DiagnosticCodes.JustMyCodeDiagnostic, - DiagnosticCodes.ConsoleTypeDiagnostic - ]) { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve('')) - .verifiable(TypeMoq.Times.never()); - await (diagnosticService as any).fixLaunchJson(code); - workspaceService.verifyAll(); - fs.verifyAll(); - } - }); - - test('File launch.json is fixed correctly when code equals JustMyCodeDiagnostic ', async () => { - const launchJson = '{"debugStdLib": true, "debugStdLib": false}'; - const correctedlaunchJson = '{"justMyCode": false, "justMyCode": true}'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup(w => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.JustMyCodeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals InvalidDebuggerTypeDiagnostic ', async () => { - const launchJson = '{"Python Experimental: task" "pythonExperimental"}'; - const correctedlaunchJson = '{"Python: task" "python"}'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup(w => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.InvalidDebuggerTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); - - test('File launch.json is fixed correctly when code equals ConsoleTypeDiagnostic ', async () => { - const launchJson = '{"console": "none"}'; - const correctedlaunchJson = '{"console": "internalConsole"}'; - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceService.setup(w => w.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - fs.setup(w => w.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(launchJson)) - .verifiable(TypeMoq.Times.atLeastOnce()); - fs.setup(w => w.writeFile(TypeMoq.It.isAnyString(), correctedlaunchJson)) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - await (diagnosticService as any).fixLaunchJson(DiagnosticCodes.ConsoleTypeDiagnostic); - workspaceService.verifyAll(); - fs.verifyAll(); - }); -}); 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 b3e483327669..000000000000 --- a/src/test/application/diagnostics/checks/invalidPythonPathInDebugger.unit.test.ts +++ /dev/null @@ -1,409 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-invalid-template-strings max-func-body-length no-any - -import { expect } from 'chai'; -import * as path from 'path'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidPythonPathInDebuggerService } from '../../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; -import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { - DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt -} from '../../../../client/application/diagnostics/promptHandler'; -import { - IDiagnostic, - IDiagnosticCommand, - IDiagnosticHandlerService, - IInvalidPythonPathInDebuggerService -} from '../../../../client/application/diagnostics/types'; -import { CommandsWithoutArgs } from '../../../../client/common/application/commands'; -import { IDocumentManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; -import { PythonPathSource } from '../../../../client/debugger/extension/types'; -import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -suite('Application Diagnostics - Checks Python Path in debugger', () => { - let diagnosticService: IInvalidPythonPathInDebuggerService; - let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; - let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; - let configService: typemoq.IMock<IConfigurationService>; - let helper: typemoq.IMock<IInterpreterHelper>; - let workspaceService: typemoq.IMock<IWorkspaceService>; - let docMgr: typemoq.IMock<IDocumentManager>; - setup(() => { - const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); - messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); - serviceContainer - .setup(s => - s.get( - typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) - ) - ) - .returns(() => messageHandler.object); - commandFactory = typemoq.Mock.ofType<IDiagnosticsCommandFactory>(); - docMgr = typemoq.Mock.ofType<IDocumentManager>(); - serviceContainer - .setup(s => s.get(typemoq.It.isValue(IDiagnosticsCommandFactory))) - .returns(() => commandFactory.object); - configService = typemoq.Mock.ofType<IConfigurationService>(); - serviceContainer - .setup(s => s.get(typemoq.It.isValue(IConfigurationService))) - .returns(() => configService.object); - helper = typemoq.Mock.ofType<IInterpreterHelper>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterHelper))).returns(() => helper.object); - workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); - serviceContainer - .setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - - diagnosticService = new class extends InvalidPythonPathInDebuggerService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - }( - serviceContainer.object, - workspaceService.object, - commandFactory.object, - helper.object, - docMgr.object, - configService.object, - [], - messageHandler.object - ); - (diagnosticService as any)._clear(); - }); - - test('Can handle InvalidPythonPathInDebugger diagnostics', async () => { - for (const code of [ - DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic, - DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic - ]) { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => code) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, `Should be able to handle ${code}`); - diagnostic.verifyAll(); - } - }); - test('Can not handle non-InvalidPythonPathInDebugger diagnostics', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => 'Something Else' as any) - .verifiable(typemoq.Times.atLeastOnce()); - - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('Should return empty diagnostics', async () => { - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory - .setup(f => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ type: 'executeVSCCommand' }) - ) - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.once()); - messageHandler.setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message once if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup(d => d.invokeHandler) - .returns(() => 'default') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory - .setup(f => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ type: 'executeVSCCommand' }) - ) - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(1)); - messageHandler - .setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(1)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerSettings diagnostic should display message twice if invoked twice', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic - .setup(d => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - diagnostic - .setup(d => d.invokeHandler) - .returns(() => 'always') - .verifiable(typemoq.Times.atLeastOnce()); - const interpreterSelectionCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory - .setup(f => - f.createCommand( - typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ type: 'executeVSCCommand' }) - ) - ) - .returns(() => interpreterSelectionCommand.object) - .verifiable(typemoq.Times.exactly(2)); - messageHandler - .setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .verifiable(typemoq.Times.exactly(2)); - - await diagnosticService.handle([diagnostic.object]); - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - test('InvalidPythonPathInDebuggerLaunch diagnostic should display one option to with a command', async () => { - const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - let options: MessageCommandPrompt | undefined; - diagnostic - .setup(d => d.code) - .returns(() => DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic) - .verifiable(typemoq.Times.atLeastOnce()); - messageHandler - .setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => (options = opts)) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(1); - expect(options!.commandPrompts[0].prompt).to.be.equal('Open launch.json'); - }); - test('Ensure we get python path from config when path = ${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, PythonPathSource.settingsJson, Uri.parse('something')); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure ${env:XYZ123} is expanded', async () => { - const pythonPath = '${env:XYZ123}/venv/bin/python'; - - process.env.XYZ123 = 'something/else'; - const expectedPath = `${process.env.XYZ123}/venv/bin/python`; - workspaceService - .setup(c => c.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined) - .verifiable(typemoq.Times.once()); - helper - .setup(h => h.getInterpreterInformation(typemoq.It.isValue(expectedPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we get python path from config when path = undefined', async () => { - const pythonPath = undefined; - - const settings = typemoq.Mock.ofType<IPythonSettings>(); - settings - .setup(s => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup(c => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - helper - .setup(h => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - settings.verifyAll(); - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure we do not get python path from config when path is provided', async () => { - const pythonPath = path.join('a', 'b'); - - const settings = typemoq.Mock.ofType<IPythonSettings>(); - configService - .setup(c => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - helper - .setup(h => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve({})) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath); - - configService.verifyAll(); - helper.verifyAll(); - expect(valid).to.be.equal(true, 'not valid'); - }); - test('Ensure InvalidPythonPathInDebuggerLaunch diagnostic is handled when path is invalid in launch.json', async () => { - const pythonPath = path.join('a', 'b'); - const settings = typemoq.Mock.ofType<IPythonSettings>(); - configService - .setup(c => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.never()); - let handleInvoked = false; - diagnosticService.handle = diagnostics => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerLaunchDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup(h => h.getInterpreterInformation(typemoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.launchJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); - test('Ensure InvalidPythonPathInDebuggerSettings diagnostic is handled when path is invalid in settings.json', async () => { - const pythonPath = undefined; - const settings = typemoq.Mock.ofType<IPythonSettings>(); - settings - .setup(s => s.pythonPath) - .returns(() => 'p') - .verifiable(typemoq.Times.once()); - configService - .setup(c => c.getSettings(typemoq.It.isAny())) - .returns(() => settings.object) - .verifiable(typemoq.Times.once()); - let handleInvoked = false; - diagnosticService.handle = diagnostics => { - if ( - diagnostics.length !== 0 && - diagnostics[0].code === DiagnosticCodes.InvalidPythonPathInDebuggerSettingsDiagnostic - ) { - handleInvoked = true; - } - return Promise.resolve(); - }; - helper - .setup(h => h.getInterpreterInformation(typemoq.It.isValue('p'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - - const valid = await diagnosticService.validatePythonPath(pythonPath, PythonPathSource.settingsJson); - - helper.verifyAll(); - expect(valid).to.be.equal(false, 'should be invalid'); - expect(handleInvoked).to.be.equal(true, 'should be invoked'); - }); -}); diff --git a/src/test/application/diagnostics/checks/jediPython27NotSupported.unit.test.ts b/src/test/application/diagnostics/checks/jediPython27NotSupported.unit.test.ts new file mode 100644 index 000000000000..d4af2e5ca901 --- /dev/null +++ b/src/test/application/diagnostics/checks/jediPython27NotSupported.unit.test.ts @@ -0,0 +1,510 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { LanguageServerType } from '../../../../client/activation/types'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + JediPython27NotSupportedDiagnostic, + JediPython27NotSupportedDiagnosticService, +} from '../../../../client/application/diagnostics/checks/jediPython27NotSupported'; +import { IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; +import { + DiagnosticCommandPromptHandlerService, + MessageCommandPrompt, +} from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnosticCommand, + IDiagnosticFilterService, + IDiagnosticHandlerService, +} from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { WorkspaceService } from '../../../../client/common/application/workspace'; +import { ConfigurationService } from '../../../../client/common/configuration/service'; +import { IConfigurationService, IPythonSettings } from '../../../../client/common/types'; +import { Python27Support } from '../../../../client/common/utils/localize'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Jedi with Python 2.7 deprecated', () => { + suite('Diagnostics', () => { + const resource = Uri.file('test.py'); + + function createConfigurationAndWorkspaceServices( + languageServer: LanguageServerType, + ): { configurationService: IConfigurationService; workspaceService: IWorkspaceService } { + const configurationService = ({ + getSettings: () => ({ languageServer }), + updateSetting: () => Promise.resolve(), + } as unknown) as IConfigurationService; + + const workspaceService = ({ + getConfiguration: () => ({ + inspect: () => ({ + workspaceValue: languageServer, + }), + }), + } as unknown) as IWorkspaceService; + + return { configurationService, workspaceService }; + } + + test('Should return an empty diagnostics array if the active interpreter version is Python 3', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 3, + minor: 8, + patch: 0, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + + test('Should return an empty diagnostics array if the active interpreter is undefined', async () => { + const interpreterService = { + getActiveInterpreter: () => Promise.resolve(undefined), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + + test('Should return a diagnostics array with one diagnostic if the active interpreter version is Python 2.7', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + const diagnostic = result[0]; + + assert.strictEqual(result.length, 1); + assert.strictEqual(diagnostic.message, Python27Support.jediMessage); + }); + + test('Should return a diagnostics array with one diagnostic if the language server is Jedi', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Jedi, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + const diagnostic = result[0]; + + assert.strictEqual(result.length, 1); + assert.strictEqual(diagnostic.message, Python27Support.jediMessage); + }); + + test('Should return an empty diagnostics array if the language server is Pylance', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.Node, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + + test('Should return an empty diagnostics array if there is no language server', async () => { + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + const { configurationService, workspaceService } = createConfigurationAndWorkspaceServices( + LanguageServerType.None, + ); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + const result = await service.diagnose(resource); + + assert.strictEqual(result.length, 0); + }); + }); + + suite('Setting update', () => { + const resource = Uri.file('test.py'); + let workspaceService: IWorkspaceService; + let getConfigurationStub: sinon.SinonStub; + let updateSettingStub: sinon.SinonStub; + let serviceContainer: IServiceContainer; + let services: { + [key: string]: IWorkspaceService; + }; + + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + setup(() => { + serviceContainer = ({ + get: (serviceIdentifier: symbol) => services[serviceIdentifier.toString()] as IWorkspaceService, + tryGet: () => ({}), + } as unknown) as IServiceContainer; + + workspaceService = new WorkspaceService(); + services = { + 'Symbol(IWorkspaceService)': workspaceService, + }; + + getConfigurationStub = sinon.stub(WorkspaceService.prototype, 'getConfiguration'); + updateSettingStub = sinon.stub(ConfigurationService.prototype, 'updateSetting'); + + const getSettingsStub = sinon.stub(ConfigurationService.prototype, 'getSettings'); + getSettingsStub.returns(({ + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as unknown) as IPythonSettings); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Running the diagnostic should update the workspace setting if set', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + workspaceValue: LanguageServerType.JediLSP, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.calledWith( + updateSettingStub, + 'languageServer', + LanguageServerType.Jedi, + resource, + ConfigurationTarget.Workspace, + ); + }); + + test('Running the diagnostic should update the global setting if set', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + globalValue: LanguageServerType.JediLSP, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.calledWith( + updateSettingStub, + 'languageServer', + LanguageServerType.Jedi, + resource, + ConfigurationTarget.Global, + ); + }); + + test('Running the diagnostic should not update the setting if not set in workspace or global scopes', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + workspaceFolderValue: LanguageServerType.JediLSP, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.notCalled(updateSettingStub); + }); + + test('Running the diagnostic should not update the setting if not set to Jedi LSP', async () => { + getConfigurationStub.returns({ + inspect: () => ({ + workspaceValue: LanguageServerType.Node, + }), + }); + const configurationService = new ConfigurationService(serviceContainer); + + const service = new JediPython27NotSupportedDiagnosticService( + ({ + get: () => ({}), + } as unknown) as IServiceContainer, + interpreterService, + workspaceService, + configurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.diagnose(resource); + + sinon.assert.calledOnce(getConfigurationStub); + sinon.assert.notCalled(updateSettingStub); + }); + }); + + suite('Handler', () => { + class TestJediPython27NotSupportedDiagnosticService extends JediPython27NotSupportedDiagnosticService { + // eslint-disable-next-line class-methods-use-this + public static clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + } + + let services: { + [key: string]: IWorkspaceService | IDiagnosticFilterService | IDiagnosticsCommandFactory; + }; + let serviceContainer: IServiceContainer; + let handleMessageStub: sinon.SinonStub; + + const interpreterService = { + getActiveInterpreter: () => + Promise.resolve({ + version: { + major: 2, + minor: 7, + patch: 10, + }, + }), + } as IInterpreterService; + + setup(() => { + services = { + 'Symbol(IDiagnosticsCommandFactory)': { + createCommand: () => ({} as IDiagnosticCommand), + }, + }; + serviceContainer = { + get: (serviceIdentifier: symbol) => + services[serviceIdentifier.toString()] as IDiagnosticFilterService | IDiagnosticsCommandFactory, + } as IServiceContainer; + + handleMessageStub = sinon.stub(DiagnosticCommandPromptHandlerService.prototype, 'handle'); + }); + + teardown(() => { + sinon.restore(); + TestJediPython27NotSupportedDiagnosticService.clear(); + }); + + test('Handling an empty diagnostics array does not display a prompt', async () => { + const service = new TestJediPython27NotSupportedDiagnosticService( + serviceContainer, + interpreterService, + {} as IWorkspaceService, + {} as IConfigurationService, + {} as IDiagnosticHandlerService<MessageCommandPrompt>, + [], + ); + + await service.handle([]); + + sinon.assert.notCalled(handleMessageStub); + }); + + test('Handling a diagnostic that should be ignored does not display a prompt', async () => { + const diagnosticHandlerService = new DiagnosticCommandPromptHandlerService(serviceContainer); + + services['Symbol(IDiagnosticFilterService)'] = ({ + shouldIgnoreDiagnostic: async () => Promise.resolve(true), + } as unknown) as IDiagnosticFilterService; + + const service = new TestJediPython27NotSupportedDiagnosticService( + serviceContainer, + interpreterService, + {} as IWorkspaceService, + {} as IConfigurationService, + diagnosticHandlerService, + [], + ); + + await service.handle([new JediPython27NotSupportedDiagnostic('ignored', undefined)]); + + sinon.assert.notCalled(handleMessageStub); + }); + + test('Handling a diagnostic should show a prompt', async () => { + const diagnosticHandlerService = new DiagnosticCommandPromptHandlerService(serviceContainer); + const configurationService = new ConfigurationService(serviceContainer); + + services['Symbol(IDiagnosticFilterService)'] = ({ + shouldIgnoreDiagnostic: () => Promise.resolve(false), + } as unknown) as IDiagnosticFilterService; + + const service = new TestJediPython27NotSupportedDiagnosticService( + serviceContainer, + interpreterService, + {} as IWorkspaceService, + configurationService, + diagnosticHandlerService, + [], + ); + + const diagnostic = new JediPython27NotSupportedDiagnostic('diagnostic', undefined); + + await service.handle([diagnostic]); + + sinon.assert.calledOnce(handleMessageStub); + }); + }); +}); diff --git a/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts b/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts deleted file mode 100644 index fc82fe965154..000000000000 --- a/src/test/application/diagnostics/checks/lsNotSupported.unit.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { ILanguageServerCompatibilityService } from '../../../../client/activation/types'; -import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { LSNotSupportedDiagnosticService } from '../../../../client/application/diagnostics/checks/lsNotSupported'; -import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/application/diagnostics/commands/types'; -import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; -import { DiagnosticCommandPromptHandlerServiceId, MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, IDiagnosticsService } from '../../../../client/application/diagnostics/types'; -import { IWorkspaceService } from '../../../../client/common/application/types'; -import { IServiceContainer } from '../../../../client/ioc/types'; - -// tslint:disable:max-func-body-length no-any -suite('Application Diagnostics - Checks LS not supported', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let diagnosticService: IDiagnosticsService; - let filterService: TypeMoq.IMock<IDiagnosticFilterService>; - let commandFactory: TypeMoq.IMock<IDiagnosticsCommandFactory>; - let messageHandler: TypeMoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; - let lsCompatibility: TypeMoq.IMock<ILanguageServerCompatibilityService>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - filterService = TypeMoq.Mock.ofType<IDiagnosticFilterService>(); - commandFactory = TypeMoq.Mock.ofType<IDiagnosticsCommandFactory>(); - messageHandler = TypeMoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); - lsCompatibility = TypeMoq.Mock.ofType<ILanguageServerCompatibilityService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IDiagnosticFilterService))).returns(() => filterService.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IDiagnosticsCommandFactory))).returns(() => commandFactory.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IDiagnosticHandlerService), TypeMoq.It.isValue(DiagnosticCommandPromptHandlerServiceId))).returns(() => messageHandler.object); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => undefined); - - diagnosticService = new class extends LSNotSupportedDiagnosticService { - public _clear() { - while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { - BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); - } - } - }(serviceContainer.object, lsCompatibility.object, messageHandler.object, []); - (diagnosticService as any)._clear(); - }); - - test('Should display two options in message displayed with 2 commands', async () => { - let options: MessageCommandPrompt | undefined; - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => DiagnosticCodes.LSNotSupportedDiagnostic) - .verifiable(TypeMoq.Times.atLeastOnce()); - const launchBrowserCommand = TypeMoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(TypeMoq.It.isAny(), - TypeMoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }))) - .returns(() => launchBrowserCommand.object) - .verifiable(TypeMoq.Times.once()); - const alwaysIgnoreCommand = TypeMoq.Mock.ofType<IDiagnosticCommand>(); - commandFactory.setup(f => f.createCommand(TypeMoq.It.isAny(), - TypeMoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', options: DiagnosticScope.Global }))) - .returns(() => alwaysIgnoreCommand.object) - .verifiable(TypeMoq.Times.once()); - messageHandler.setup(m => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((_, opts: MessageCommandPrompt) => options = opts) - .verifiable(TypeMoq.Times.once()); - - await diagnosticService.handle([diagnostic.object]); - - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - expect(options!.commandPrompts).to.be.lengthOf(2); - expect(options!.commandPrompts[0].prompt).to.be.equal('More Info'); - }); - test('Should not display a message if the diagnostic code has been ignored', async () => { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - - filterService.setup(f => f.shouldIgnoreDiagnostic(TypeMoq.It.isValue(DiagnosticCodes.LSNotSupportedDiagnostic))) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - diagnostic.setup(d => d.code) - .returns(() => DiagnosticCodes.LSNotSupportedDiagnostic) - .verifiable(TypeMoq.Times.atLeastOnce()); - commandFactory.setup(f => f.createCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.never()); - messageHandler.setup(m => m.handle(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .verifiable(TypeMoq.Times.never()); - - await diagnosticService.handle([diagnostic.object]); - - filterService.verifyAll(); - diagnostic.verifyAll(); - commandFactory.verifyAll(); - messageHandler.verifyAll(); - }); - - test('LSNotSupportedDiagnosticService can handle LSNotSupported diagnostics', async () => { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => DiagnosticCodes.LSNotSupportedDiagnostic) - .verifiable(TypeMoq.Times.atLeastOnce()); - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(true, 'Invalid value'); - diagnostic.verifyAll(); - }); - test('LSNotSupportedDiagnosticService can not handle non-LSNotSupported diagnostics', async () => { - const diagnostic = TypeMoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) - .returns(() => 'Something Else' as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); - }); -}); diff --git a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts index 00f0997f9f65..ba2436d0ffeb 100644 --- a/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/macPythonInterpreter.unit.test.ts @@ -3,92 +3,117 @@ '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 { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidMacPythonInterpreterDiagnostic, InvalidMacPythonInterpreterService } from '../../../../client/application/diagnostics/checks/macPythonInterpreter'; +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 { + 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, IPythonSettings } from '../../../../client/common/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, IInterpreterService, InterpreterType } from '../../../../client/interpreter/contracts'; +import { IInterpreterHelper } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; -suite('Application Diagnostics - Checks Python Interpreter', () => { +suite('Application Diagnostics - Checks Mac Python Interpreter', () => { let diagnosticService: IDiagnosticsService; let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; let commandFactory: typemoq.IMock<IDiagnosticsCommandFactory>; let settings: typemoq.IMock<IPythonSettings>; - let interpreterService: typemoq.IMock<IInterpreterService>; let platformService: typemoq.IMock<IPlatformService>; let helper: typemoq.IMock<IInterpreterHelper>; let filterService: typemoq.IMock<IDiagnosticFilterService>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; const pythonPath = 'My Python Path in Settings'; let serviceContainer: typemoq.IMock<IServiceContainer>; function createContainer() { serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); 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); + 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))) + configService.setup((c) => c.getSettings(typemoq.It.isAny())).returns(() => settings.object); + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IConfigurationService))) .returns(() => configService.object); - interpreterService = typemoq.Mock.ofType<IInterpreterService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IInterpreterService))) - .returns(() => interpreterService.object); platformService = typemoq.Mock.ofType<IPlatformService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPlatformService))) + 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(() => []); + 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))) + 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) + .setup((p) => p.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); return serviceContainer.object; } suite('Diagnostics', () => { setup(() => { - diagnosticService = new class extends InvalidMacPythonInterpreterService { + diagnosticService = new (class extends InvalidMacPythonInterpreterService { public _clear() { while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); } } - protected addPythonPathChangedHandler() { noop(); } - }(createContainer(), interpreterService.object, [], platformService.object, helper.object); + protected addPythonPathChangedHandler() { + noop(); + } + })(createContainer(), [], platformService.object, helper.object); (diagnosticService as any)._clear(); }); test('Can handle InvalidPythonPathInterpreter diagnostics', async () => { - for (const code of [ - DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, - DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic - ]) { + for (const code of [DiagnosticCodes.MacInterpreterSelected]) { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => code) .verifiable(typemoq.Times.atLeastOnce()); @@ -99,7 +124,8 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { }); test('Can not handle non-InvalidPythonPathInterpreter diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); @@ -110,172 +136,80 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { test('Should return empty diagnostics if not a Mac', async () => { platformService.reset(); platformService - .setup(p => p.isMac) - .returns(() => true) - .verifiable(typemoq.Times.once()); - - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - platformService.verifyAll(); - }); - test('Should return empty diagnostics if installer check is disabled', async () => { - settings - .setup(s => s.disableInstallationChecks) + .setup((p) => p.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); const diagnostics = await diagnosticService.diagnose(undefined); expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); - platformService.verifyAll(); - }); - test('Should return empty diagnostics if there are interpreters, one is selected, and platform is not mac', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.hasInterpreters) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([{} as any])) - .verifiable(typemoq.Times.never()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => false) - .verifiable(typemoq.Times.once()); - - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); - interpreterService.verifyAll(); platformService.verifyAll(); }); - test('Should return empty diagnostics if there are interpreters, platform is mac and selected interpreter is not default mac interpreter', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.hasInterpreters) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([{} as any])) - .verifiable(typemoq.Times.never()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); + test('Should return empty diagnostics if platform is mac and selected interpreter is not default mac interpreter', async () => { platformService - .setup(i => i.isMac) + .setup((i) => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isAny())) - .returns(() => false) + .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(); - interpreterService.verifyAll(); platformService.verifyAll(); helper.verifyAll(); }); - test('Should return diagnostic if there are no other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([ - { path: pythonPath } as any, - { path: pythonPath } as any - ])) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); + test('Should return diagnostic if platform is mac and selected interpreter is default mac interpreter', async () => { platformService - .setup(i => i.isMac) + .setup((i) => i.isMac) .returns(() => true) .verifiable(typemoq.Times.once()); helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, undefined)], 'not the same'); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); - }); - test('Should return diagnostic if there are other interpreters, platform is mac and selected interpreter is default mac interpreter', async () => { - const nonMacStandardInterpreter = 'Non Mac Std Interpreter'; - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) - .verifiable(typemoq.Times.once()); - interpreterService - .setup(i => i.getInterpreters(typemoq.It.isAny())) - .returns(() => Promise.resolve([ - { path: pythonPath } as any, - { path: pythonPath } as any, - { path: nonMacStandardInterpreter } as any - ])) - .verifiable(typemoq.Times.once()); - platformService - .setup(i => i.isMac) - .returns(() => true) - .verifiable(typemoq.Times.once()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - helper - .setup(i => i.isMacDefaultPythonPath(typemoq.It.isValue(nonMacStandardInterpreter))) - .returns(() => false) + .setup((i) => i.isMacDefaultPythonPath(typemoq.It.isValue(pythonPath))) + .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.atLeastOnce()); - interpreterService - .setup(i => i.getActiveInterpreter(typemoq.It.isAny())) - .returns(() => { return Promise.resolve({ type: InterpreterType.Unknown } as any); }) - .verifiable(typemoq.Times.once()); const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, undefined)], 'not the same'); - settings.verifyAll(); - interpreterService.verifyAll(); - platformService.verifyAll(); - helper.verifyAll(); + 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.MacInterpreterSelectedAndHaveOtherInterpretersDiagnostic, undefined); - const cmd = {} as any as IDiagnosticCommand; - const cmdIgnore = {} as any as IDiagnosticCommand; + 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) + .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' }))) + 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 }))) + 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()); @@ -286,53 +220,24 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); expect(messagePrompt!.commandPrompts).to.be.deep.equal([ { prompt: 'Select Python Interpreter', command: cmd }, - { prompt: 'Do not show again', command: cmdIgnore } - ]); - }); - test('Handling no interpreters diagnostisc should return 3 commands', async () => { - const diagnostic = new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, undefined); - const cmdDownload = {} as any as IDiagnosticCommand; - const cmdLearn = {} as any as IDiagnosticCommand; - const cmdIgnore = {} as any as IDiagnosticCommand; - let messagePrompt: MessageCommandPrompt | undefined; - messageHandler - .setup(i => i.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) - .callback((_d, p: MessageCommandPrompt) => messagePrompt = p) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch', options: 'https://code.visualstudio.com/docs/python/python-tutorial#_prerequisites' }))) - .returns(() => cmdLearn) - .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch', options: 'https://www.python.org/downloads' }))) - .returns(() => cmdDownload) - .verifiable(typemoq.Times.once()); - commandFactory.setup(f => f.createCommand(typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', options: DiagnosticScope.Global }))) - .returns(() => cmdIgnore) - .verifiable(typemoq.Times.once()); - - await diagnosticService.handle([diagnostic]); - - messageHandler.verifyAll(); - commandFactory.verifyAll(); - expect(messagePrompt).not.be.equal(undefined, 'Message prompt not set'); - expect(messagePrompt!.commandPrompts).to.be.deep.equal([ - { prompt: 'Learn more', command: cmdLearn }, - { prompt: 'Download', command: cmdDownload }, - { prompt: 'Do not show again', command: cmdIgnore } + { prompt: "Don't show again", command: cmdIgnore }, ]); }); test('Should not display a message if No Interpreters diagnostic has been ignored', async () => { - const diagnostic = new InvalidMacPythonInterpreterDiagnostic(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic, undefined); + const diagnostic = new InvalidMacPythonInterpreterDiagnostic( + DiagnosticCodes.MacInterpreterSelected, + undefined, + ); - filterService.setup(f => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.MacInterpreterSelectedAndNoOtherInterpretersDiagnostic))) + 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())) + 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())) + messageHandler + .setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())) .verifiable(typemoq.Times.never()); await diagnosticService.handle([diagnostic]); @@ -346,69 +251,43 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { suite('Change Handlers.', () => { test('Add PythonPath handler is invoked', async () => { let invoked = false; - diagnosticService = new class extends InvalidMacPythonInterpreterService { - protected addPythonPathChangedHandler() { invoked = true; } - }(createContainer(), interpreterService.object, [], platformService.object, helper.object); - - expect(invoked).to.be.equal(true, 'Not invoked'); - }); - test('Event Handler is registered and invoked', async () => { - let invoked = false; - let callbackHandler!: (e: ConfigurationChangeEvent) => Promise<void>; - const workspaceService = { onDidChangeConfiguration: (cb: (e: ConfigurationChangeEvent) => Promise<void>) => callbackHandler = cb } as any; - const serviceContainerObject = createContainer(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService); - diagnosticService = new class extends InvalidMacPythonInterpreterService { - protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } - }(serviceContainerObject, undefined as any, [], undefined as any, undefined as any); + diagnosticService = new (class extends InvalidMacPythonInterpreterService { + protected addPythonPathChangedHandler() { + invoked = true; + } + })(createContainer(), [], platformService.object, helper.object); - 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 InvalidMacPythonInterpreterService { - protected async onDidChangeConfiguration(_event: ConfigurationChangeEvent) { invoked = true; } - }(serviceContainerObject, undefined as any, [], undefined as any, undefined as any); - - expect(invoked).to.be.equal(false, 'Not invoked'); - }); - test('Diagnostics are checked when path changes', async () => { - const event = typemoq.Mock.ofType<ConfigurationChangeEvent>(); + test('Diagnostics are checked with correct interpreter config uri when path changes', async () => { + const event = typemoq.Mock.ofType<InterpreterConfigurationScope>(); const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typemoq.Times.once()); - workspaceService - .setup(w => w.workspaceFolders) + .setup((w) => w.workspaceFolders) .returns(() => [{ uri: '' }] as any) .verifiable(typemoq.Times.once()); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); - const diagnosticSvc = new class extends InvalidMacPythonInterpreterService { - constructor(arg1: IServiceContainer, arg2: IInterpreterService, arg3: IPlatformService, arg4: IInterpreterHelper) { - super(arg1, arg2, [], arg3, arg4); + + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor(arg1: IServiceContainer, arg3: IPlatformService, arg4: IInterpreterHelper) { + super(arg1, [], arg3, arg4); this.changeThrottleTimeout = 1; } - public onDidChangeConfigurationEx = (e: ConfigurationChangeEvent) => super.onDidChangeConfiguration(e); + public onDidChangeConfigurationEx = (e: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e); public diagnose(): Promise<any> { diagnoseInvocationCount += 1; return Promise.resolve(); } - }(serviceContainerObject, typemoq.Mock.ofType<IInterpreterService>().object, typemoq.Mock.ofType<IPlatformService>().object, typemoq.Mock.ofType<IInterpreterHelper>().object); - - event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); + })( + serviceContainerObject, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object, + ); await diagnosticSvc.onDidChangeConfigurationEx(event.object); event.verifyAll(); @@ -419,37 +298,36 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { 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 event = typemoq.Mock.ofType<InterpreterConfigurationScope>(); const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); const serviceContainerObject = createContainer(); let diagnoseInvocationCount = 0; workspaceService - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typemoq.Times.once()); - workspaceService - .setup(w => w.workspaceFolders) + .setup((w) => w.workspaceFolders) .returns(() => [{ uri: '' }] as any) .verifiable(typemoq.Times.once()); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); - const diagnosticSvc = new class extends InvalidMacPythonInterpreterService { - constructor(arg1: IServiceContainer, arg2: IInterpreterService, arg3: IPlatformService, arg4: IInterpreterHelper) { - super(arg1, arg2, [], arg3, arg4); + + const diagnosticSvc = new (class extends InvalidMacPythonInterpreterService { + constructor(arg1: IServiceContainer, arg3: IPlatformService, arg4: IInterpreterHelper) { + super(arg1, [], arg3, arg4); this.changeThrottleTimeout = 100; } - public onDidChangeConfigurationEx = (e: ConfigurationChangeEvent) => super.onDidChangeConfiguration(e); + public onDidChangeConfigurationEx = (e: InterpreterConfigurationScope) => + super.onDidChangeConfiguration(e); public diagnose(): Promise<any> { diagnoseInvocationCount += 1; return Promise.resolve(); } - }(serviceContainerObject, typemoq.Mock.ofType<IInterpreterService>().object, typemoq.Mock.ofType<IPlatformService>().object, typemoq.Mock.ofType<IInterpreterHelper>().object); - - event - .setup(e => e.affectsConfiguration(typemoq.It.isValue('python.pythonPath'), typemoq.It.isAny())) - .returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); + })( + serviceContainerObject, + typemoq.Mock.ofType<IPlatformService>().object, + typemoq.Mock.ofType<IInterpreterHelper>().object, + ); await diagnosticSvc.onDidChangeConfigurationEx(event.object); await diagnosticSvc.onDidChangeConfigurationEx(event.object); @@ -457,8 +335,33 @@ suite('Application Diagnostics - Checks Python Interpreter', () => { await diagnosticSvc.onDidChangeConfigurationEx(event.object); await diagnosticSvc.onDidChangeConfigurationEx(event.object); await sleep(500); - event.verifyAll(); expect(diagnoseInvocationCount).to.be.equal(1, 'Not invoked'); }); + + test('Ensure event Handler is registered correctly', 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 288d40697a73..29a6c6eb3aff 100644 --- a/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts +++ b/src/test/application/diagnostics/checks/powerShellActivation.unit.test.ts @@ -11,7 +11,7 @@ import { CommandOption, IDiagnosticsCommandFactory } from '../../../../client/ap import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; import { DiagnosticCommandPromptHandlerServiceId, - MessageCommandPrompt + MessageCommandPrompt, } from '../../../../client/application/diagnostics/promptHandler'; import { DiagnosticScope, @@ -19,7 +19,7 @@ import { IDiagnosticCommand, IDiagnosticFilterService, IDiagnosticHandlerService, - IDiagnosticsService + IDiagnosticsService, } from '../../../../client/application/diagnostics/types'; import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; import { IPlatformService } from '../../../../client/common/platform/types'; @@ -27,7 +27,6 @@ import { ICurrentProcess, IPathUtils } from '../../../../client/common/types'; import { EnvironmentVariables } from '../../../../client/common/variables/types'; import { IServiceContainer } from '../../../../client/ioc/types'; -// tslint:disable:max-func-body-length no-any suite('Application Diagnostics - PowerShell Activation', () => { let diagnosticService: IDiagnosticsService; let platformService: typemoq.IMock<IPlatformService>; @@ -42,62 +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))).returns(() => platformService.object); + 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 => + .setup((s) => s.get( typemoq.It.isValue(IDiagnosticHandlerService), - typemoq.It.isValue(DiagnosticCommandPromptHandlerServiceId) - ) + 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))) + .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))) + .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); const workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IWorkspaceService))) + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IWorkspaceService))) .returns(() => workspaceService.object); - workspaceService.setup(w => w.getWorkspaceFolder(typemoq.It.isAny())) - .returns(() => undefined); + workspaceService.setup((w) => w.getWorkspaceFolder(typemoq.It.isAny())).returns(() => undefined); - diagnosticService = new class extends PowerShellActivationHackDiagnosticsService { + diagnosticService = new (class extends PowerShellActivationHackDiagnosticsService { public _clear() { while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); } } - }(serviceContainer.object, []); + })(serviceContainer.object, []); (diagnosticService as any)._clear(); }); test('Can handle PowerShell diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); diagnostic - .setup(d => d.code) + .setup((d) => d.code) .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); @@ -107,7 +108,8 @@ suite('Application Diagnostics - PowerShell Activation', () => { }); test('Can not handle non-EnvPathVariable diagnostics', async () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); - diagnostic.setup(d => d.code) + diagnostic + .setup((d) => d.code) .returns(() => 'Something Else' as any) .verifiable(typemoq.Times.atLeastOnce()); @@ -123,34 +125,34 @@ suite('Application Diagnostics - PowerShell Activation', () => { const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); let options: MessageCommandPrompt | undefined; diagnostic - .setup(d => d.code) + .setup((d) => d.code) .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) .verifiable(typemoq.Times.atLeastOnce()); const alwaysIgnoreCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), typemoq.It.isObjectWith<CommandOption<'ignore', DiagnosticScope>>({ type: 'ignore', - options: DiagnosticScope.Global - }) - ) + options: DiagnosticScope.Global, + }), + ), ) .returns(() => alwaysIgnoreCommand.object) .verifiable(typemoq.Times.once()); const launchBrowserCommand = typemoq.Mock.ofType<IDiagnosticCommand>(); commandFactory - .setup(f => + .setup((f) => f.createCommand( typemoq.It.isAny(), - typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }) - ) + typemoq.It.isObjectWith<CommandOption<'launch', string>>({ type: 'launch' }), + ), ) .returns(() => launchBrowserCommand.object) .verifiable(typemoq.Times.once()); messageHandler - .setup(m => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .setup((m) => m.handle(typemoq.It.isAny(), typemoq.It.isAny())) .callback((_, opts: MessageCommandPrompt) => (options = opts)) .verifiable(typemoq.Times.once()); diff --git a/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts b/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts new file mode 100644 index 000000000000..85dc5a4fb8af --- /dev/null +++ b/src/test/application/diagnostics/checks/pylanceDefault.unit.test.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { ExtensionContext } from 'vscode'; +import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; +import { + PylanceDefaultDiagnostic, + PylanceDefaultDiagnosticService, + PYLANCE_PROMPT_MEMENTO, +} from '../../../../client/application/diagnostics/checks/pylanceDefault'; +import { DiagnosticCodes } from '../../../../client/application/diagnostics/constants'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { + IDiagnostic, + IDiagnosticFilterService, + IDiagnosticHandlerService, +} from '../../../../client/application/diagnostics/types'; +import { IExtensionContext } from '../../../../client/common/types'; +import { Common, Diagnostics } from '../../../../client/common/utils/localize'; +import { IServiceContainer } from '../../../../client/ioc/types'; + +suite('Application Diagnostics - Pylance informational prompt', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let diagnosticService: PylanceDefaultDiagnosticService; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let context: typemoq.IMock<IExtensionContext>; + let memento: typemoq.IMock<ExtensionContext['globalState']>; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + context = typemoq.Mock.ofType<IExtensionContext>(); + memento = typemoq.Mock.ofType<ExtensionContext['globalState']>(); + + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + context.setup((c) => c.globalState).returns(() => memento.object); + + diagnosticService = new (class extends PylanceDefaultDiagnosticService { + // eslint-disable-next-line class-methods-use-this + public _clear() { + while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { + BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); + } + } + })(serviceContainer.object, context.object, messageHandler.object, []); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (diagnosticService as any)._clear(); + }); + + teardown(() => { + context.reset(); + memento.reset(); + }); + + function setupMementos(version?: string, promptShown?: boolean) { + diagnosticService.initialMementoValue = version; + memento.setup((m) => m.get(PYLANCE_PROMPT_MEMENTO)).returns(() => promptShown); + } + + test("Should display message if it's an existing installation of the extension and the prompt has not been shown yet", async () => { + setupMementos('1.0.0', undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, [ + new PylanceDefaultDiagnostic(Diagnostics.pylanceDefaultMessage, undefined), + ]); + }); + + test("Should return empty diagnostics if it's an existing installation of the extension and the prompt has been shown before", async () => { + setupMementos('1.0.0', true); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, []); + }); + + test("Should return empty diagnostics if it's a fresh installation of the extension", async () => { + setupMementos(undefined, undefined); + + const diagnostics = await diagnosticService.diagnose(undefined); + + assert.deepStrictEqual(diagnostics, []); + }); + + test('Should display a prompt when handling the diagnostic code', async () => { + const diagnostic = new PylanceDefaultDiagnostic(DiagnosticCodes.PylanceDefaultDiagnostic, undefined); + let messagePrompt: MessageCommandPrompt | undefined; + + messageHandler + .setup((f) => f.handle(typemoq.It.isValue(diagnostic), typemoq.It.isAny())) + .callback((_d, prompt: MessageCommandPrompt) => { + messagePrompt = prompt; + }) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + + assert.notDeepStrictEqual(messagePrompt, undefined); + assert.notDeepStrictEqual(messagePrompt!.onClose, undefined); + assert.deepStrictEqual(messagePrompt!.commandPrompts, [{ prompt: Common.ok }]); + }); + + test('Should return empty diagnostics if the diagnostic code has been ignored', async () => { + const diagnostic = new PylanceDefaultDiagnostic(DiagnosticCodes.PylanceDefaultDiagnostic, undefined); + + filterService + .setup((f) => f.shouldIgnoreDiagnostic(typemoq.It.isValue(DiagnosticCodes.PylanceDefaultDiagnostic))) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + messageHandler.setup((f) => f.handle(typemoq.It.isAny(), typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await diagnosticService.handle([diagnostic]); + + filterService.verifyAll(); + messageHandler.verifyAll(); + }); + + test('PylanceDefaultDiagnosticService can handle PylanceDefaultDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.PylanceDefaultDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(true, 'Invalid value'); + diagnostic.verifyAll(); + }); + + test('PylanceDefaultDiagnosticService cannot handle non-PylanceDefaultDiagnostic diagnostics', async () => { + const diagnostic = typemoq.Mock.ofType<IDiagnostic>(); + diagnostic + .setup((d) => d.code) + .returns(() => DiagnosticCodes.EnvironmentActivationInPowerShellWithBatchFilesNotSupportedDiagnostic) + .verifiable(typemoq.Times.atLeastOnce()); + + const canHandle = await diagnosticService.canHandle(diagnostic.object); + + expect(canHandle).to.be.equal(false, 'Invalid value'); + diagnostic.verifyAll(); + }); +}); diff --git a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts index 8eb517c6c973..2eecf052e433 100644 --- a/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts +++ b/src/test/application/diagnostics/checks/pythonInterpreter.unit.test.ts @@ -3,80 +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 { EventEmitter, Uri } from 'vscode'; import { BaseDiagnosticsService } from '../../../../client/application/diagnostics/base'; -import { InvalidPythonInterpreterDiagnostic, InvalidPythonInterpreterService } from '../../../../client/application/diagnostics/checks/pythonInterpreter'; +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 { + 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 { IPlatformService } from '../../../../client/common/platform/types'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings } from '../../../../client/common/types'; +import { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; +import { Commands } from '../../../../client/common/constants'; +import { IFileSystem, IPlatformService } from '../../../../client/common/platform/types'; +import { IProcessService, IProcessServiceFactory } from '../../../../client/common/process/types'; +import { + IConfigurationService, + IDisposable, + IDisposableRegistry, + IInterpreterPathService, + Resource, +} from '../../../../client/common/types'; +import { Common } from '../../../../client/common/utils/localize'; import { noop } from '../../../../client/common/utils/misc'; -import { IInterpreterHelper, IInterpreterService } from '../../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../../client/ioc/types'; +import { 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 { + diagnosticService = new (class extends InvalidPythonInterpreterService { public _clear() { while (BaseDiagnosticsService.handledDiagnosticCodeKeys.length > 0) { BaseDiagnosticsService.handledDiagnosticCodeKeys.shift(); } } - protected addPythonPathChangedHandler() { noop(); } - }(createContainer(), []); + 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.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()); @@ -85,52 +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' as any) - .verifiable(typemoq.Times.atLeastOnce()); - const canHandle = await diagnosticService.canHandle(diagnostic.object); - expect(canHandle).to.be.equal(false, 'Invalid value'); - diagnostic.verifyAll(); + test('Should return empty diagnostics', async () => { + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics).to.be.deep.equal([], 'not the same'); }); - test('Should return empty diagnostics if installer check is disabled', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => true) + + 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()) + .returns(() => Promise.resolve(false)) + .verifiable(typemoq.Times.once()); + interpreterService + .setup((i) => i.getInterpreters(undefined)) + .returns(() => []) .verifiable(typemoq.Times.once()); - const diagnostics = await diagnosticService.diagnose(undefined); - expect(diagnostics).to.be.deep.equal([]); - settings.verifyAll(); + 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.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._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [new DefaultShellDiagnostic(DiagnosticCodes.InvalidComspecDiagnostic, undefined)], + 'not the same', + ); + }); + test('Should return incomplete path diagnostics if `Path` variable is incomplete and execution fails', async function () { + if (getOSType() !== OSType.Windows) { + return this.skip(); + } + // No interpreter should exist if execution is failing. + interpreterService + .setup((i) => i.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 diagnostics if there are no interpreters', async () => { - settings - .setup(s => s.disableInstallationChecks) - .returns(() => false) + 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.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(undefined); + }); + processService + .setup((p) => p.shellExec(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.reject({ errno: -4058 })); + const diagnostics = await diagnosticService._manualDiagnose(undefined); + expect(diagnostics).to.be.deep.equal( + [ + new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ), + ], + 'not the same', + ); + }); + test('Should return invalid interpreter diagnostics if there are interpreters but no current interpreter', async () => { + interpreterService + .setup((i) => i.hasInterpreters()) + .returns(() => Promise.resolve(true)) .verifiable(typemoq.Times.once()); interpreterService - .setup(i => i.hasInterpreters) - .returns(() => Promise.resolve(false)) + .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 empty diagnostics if there are interpreters and a current interpreter', async () => { + interpreterService.setup((i) => i.hasInterpreters()).returns(() => Promise.resolve(true)); + interpreterService + .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()); + 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(undefined); - expect(diagnostics).to.be.deep.equal([new InvalidPythonInterpreterDiagnostic(DiagnosticCodes.NoPythonInterpretersDiagnostic, undefined)], 'not the same'); - settings.verifyAll(); - interpreterService.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, undefined); - 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()); @@ -139,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, undefined - ); - 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', CommandsWithoutArgs>>({ 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()); @@ -162,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.NoCurrentlySelectedPythonInterpreterDiagnostic, undefined); - 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', CommandsWithoutArgs>>({ 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()); @@ -183,7 +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 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)) + .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.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: 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; + + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); + + await diagnosticService.handle(diagnostics); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); + }); + test('Handling an unsupported diagnostic code should not show a message nor return a command', async () => { + const diagnostic = new InvalidPythonInterpreterDiagnostic( + DiagnosticCodes.InvalidPythonInterpreterDiagnostic, + undefined, + workspaceService.object, + ); + const cmd = ({} as any) as IDiagnosticCommand; + const diagnosticServiceMock = (typemoq.Mock.ofInstance(diagnosticService) as any) as typemoq.IMock< + InvalidPythonInterpreterService + >; + + diagnosticServiceMock.setup((f) => f.canHandle(typemoq.It.isAny())).returns(() => Promise.resolve(false)); + messageHandler + .setup((i) => i.handle(typemoq.It.isAny(), typemoq.It.isAny())) + .callback((_d, p: MessageCommandPrompt) => p) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.never()); + commandFactory + .setup((f) => + f.createCommand( + typemoq.It.isAny(), + typemoq.It.isObjectWith<CommandOption<'executeVSCCommand', CommandsWithoutArgs>>({ + type: 'executeVSCCommand', + }), + ), + ) + .returns(() => cmd) + .verifiable(typemoq.Times.never()); + + await diagnosticServiceMock.object.handle([diagnostic]); + + messageHandler.verifyAll(); + commandFactory.verifyAll(); }); }); }); diff --git a/src/test/application/diagnostics/checks/switchToDefaultLSDiagnostic.unit.test.ts b/src/test/application/diagnostics/checks/switchToDefaultLSDiagnostic.unit.test.ts new file mode 100644 index 000000000000..c3d1c9e18fec --- /dev/null +++ b/src/test/application/diagnostics/checks/switchToDefaultLSDiagnostic.unit.test.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as typemoq from 'typemoq'; +import { SwitchToDefaultLanguageServerDiagnosticService } from '../../../../client/application/diagnostics/checks/switchToDefaultLS'; +import { MessageCommandPrompt } from '../../../../client/application/diagnostics/promptHandler'; +import { IDiagnosticFilterService, IDiagnosticHandlerService } from '../../../../client/application/diagnostics/types'; +import { IWorkspaceService } from '../../../../client/common/application/types'; +import { IServiceContainer } from '../../../../client/ioc/types'; +import { MockWorkspaceConfiguration } from '../../../mocks/mockWorkspaceConfig'; + +suite('Application Diagnostics - Switch to default LS', () => { + let serviceContainer: typemoq.IMock<IServiceContainer>; + let diagnosticService: SwitchToDefaultLanguageServerDiagnosticService; + let filterService: typemoq.IMock<IDiagnosticFilterService>; + let messageHandler: typemoq.IMock<IDiagnosticHandlerService<MessageCommandPrompt>>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + filterService = typemoq.Mock.ofType<IDiagnosticFilterService>(); + messageHandler = typemoq.Mock.ofType<IDiagnosticHandlerService<MessageCommandPrompt>>(); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + + serviceContainer + .setup((s) => s.get(typemoq.It.isValue(IDiagnosticFilterService))) + .returns(() => filterService.object); + + diagnosticService = new SwitchToDefaultLanguageServerDiagnosticService( + serviceContainer.object, + workspaceService.object, + messageHandler.object, + [], + ); + }); + + test('When global language server is NOT Microsoft do Nothing', async () => { + workspaceService + .setup((w) => w.getConfiguration('python')) + .returns( + () => + new MockWorkspaceConfiguration({ + languageServer: { + globalValue: 'Default', + workspaceValue: undefined, + }, + }), + ); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(0, 'Diagnostics should not be returned for this case'); + }); + test('When global language server is Microsoft', async () => { + const config = new MockWorkspaceConfiguration({ + languageServer: { + globalValue: 'Microsoft', + workspaceValue: undefined, + }, + }); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => config); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(1, 'Diagnostics should be returned for this case'); + const value = config.inspect<string>('languageServer'); + expect(value).to.be.equals('Default', 'Global language server value should be Default'); + }); + + test('When workspace language server is NOT Microsoft do Nothing', async () => { + workspaceService + .setup((w) => w.getConfiguration('python')) + .returns( + () => + new MockWorkspaceConfiguration({ + languageServer: { + globalValue: undefined, + workspaceValue: 'Default', + }, + }), + ); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(0, 'Diagnostics should not be returned for this case'); + }); + test('When workspace language server is Microsoft', async () => { + const config = new MockWorkspaceConfiguration({ + languageServer: { + globalValue: undefined, + workspaceValue: 'Microsoft', + }, + }); + workspaceService.setup((w) => w.getConfiguration('python')).returns(() => config); + + const diagnostics = await diagnosticService.diagnose(undefined); + expect(diagnostics.length).to.be.equals(1, 'Diagnostics should be returned for this case'); + const value = config.inspect<string>('languageServer'); + expect(value).to.be.equals('Default', 'Workspace language server value should be Default'); + }); +}); diff --git a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts b/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts deleted file mode 100644 index d2420161510b..000000000000 --- a/src/test/application/diagnostics/checks/updateTestSettings.unit.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as path from 'path'; -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { ApplicationEnvironment } from '../../../../client/common/application/applicationEnvironment'; -import { IApplicationEnvironment, IWorkspaceService } from '../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../client/common/application/workspace'; -import { PersistentState, PersistentStateFactory } from '../../../../client/common/persistentState'; -import { FileSystem } from '../../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../../client/common/platform/types'; -import { IPersistentState } from '../../../../client/common/types'; -import { UpdateTestSettingService } from '../../../../client/testing/common/updateTestSettings'; - -// tslint:disable:max-func-body-length no-invalid-this no-any -suite('Application Diagnostics - Check Test Settings', () => { - let diagnosticService: UpdateTestSettingService; - let fs: IFileSystem; - let appEnv: IApplicationEnvironment; - let storage: IPersistentState<string[]>; - let workspace: IWorkspaceService; - const sandbox = sinon.sandbox.create(); - setup(() => { - fs = mock(FileSystem); - appEnv = mock(ApplicationEnvironment); - storage = mock(PersistentState); - workspace = mock(WorkspaceService); - const stateFactory = mock(PersistentStateFactory); - - when(stateFactory.createGlobalPersistentState('python.unitTest.Settings', anything())).thenReturn(instance(storage)); - - diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); - }); - teardown(() => { - sandbox.restore(); - }); - [Uri.file(__filename), undefined].forEach(resource => { - const resourceTitle = resource ? '(with a resource)' : '(without a resource)'; - - test(`activate method invokes UpdateTestSettings ${resourceTitle}`, async () => { - const updateTestSettings = sandbox.stub(UpdateTestSettingService.prototype, 'updateTestSettings'); - updateTestSettings.resolves(); - diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); - - await diagnosticService.activate(resource); - - assert.ok(updateTestSettings.calledOnce); - }); - - test(`activate method invokes UpdateTestSettings and ignores errors raised by UpdateTestSettings ${resourceTitle}`, async () => { - const updateTestSettings = sandbox.stub(UpdateTestSettingService.prototype, 'updateTestSettings'); - updateTestSettings.rejects(new Error('Kaboom')); - diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); - - await diagnosticService.activate(resource); - - assert.ok(updateTestSettings.calledOnce); - }); - - test(`When there are no workspaces, then return just the user settings file ${resourceTitle}`, async () => { - when(workspace.getWorkspaceFolder(anything())).thenReturn(); - when(appEnv.userSettingsFile).thenReturn('user.json'); - - const files = diagnosticService.getSettingsFiles(resource); - - assert.deepEqual(files, ['user.json']); - verify(workspace.getWorkspaceFolder(resource)).once(); - }); - test(`When there are no workspaces & no user file, then return an empty array ${resourceTitle}`, async () => { - when(workspace.getWorkspaceFolder(anything())).thenReturn(); - when(appEnv.userSettingsFile).thenReturn(); - - const files = diagnosticService.getSettingsFiles(resource); - - assert.deepEqual(files, []); - verify(workspace.getWorkspaceFolder(resource)).once(); - }); - test(`When there is a workspace folder, then return the user settings file & workspace file ${resourceTitle}`, async function () { - if (!resource) { - return this.skip(); - } - when(workspace.getWorkspaceFolder(resource)).thenReturn({ name: '1', uri: Uri.file('folder1'), index: 0 }); - when(appEnv.userSettingsFile).thenReturn('user.json'); - - const files = diagnosticService.getSettingsFiles(resource); - - assert.deepEqual(files, ['user.json', path.join(Uri.file('folder1').fsPath, '.vscode', 'settings.json')]); - verify(workspace.getWorkspaceFolder(resource)).once(); - }); - test(`When there is a workspace folder & no user file, then workspace file ${resourceTitle}`, async function () { - if (!resource) { - return this.skip(); - } - when(workspace.getWorkspaceFolder(resource)).thenReturn({ name: '1', uri: Uri.file('folder1'), index: 0 }); - when(appEnv.userSettingsFile).thenReturn(); - - const files = diagnosticService.getSettingsFiles(resource); - - assert.deepEqual(files, [path.join(Uri.file('folder1').fsPath, '.vscode', 'settings.json')]); - verify(workspace.getWorkspaceFolder(resource)).once(); - }); - test(`Return an empty array if there are no files ${resourceTitle}`, async () => { - const getSettingsFiles = sandbox.stub(UpdateTestSettingService.prototype, 'getSettingsFiles'); - getSettingsFiles.returns([]); - diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); - - const files = await diagnosticService.getFilesToBeFixed(resource); - - expect(files).to.deep.equal([]); - }); - test(`Filter files based on whether they need to be fixed ${resourceTitle}`, async () => { - const getSettingsFiles = sandbox.stub(UpdateTestSettingService.prototype, 'getSettingsFiles'); - const filterFiles = sandbox.stub(UpdateTestSettingService.prototype, 'doesFileNeedToBeFixed'); - filterFiles.callsFake(file => Promise.resolve(file === 'file_a' || file === 'file_c')); - getSettingsFiles.returns(['file_a', 'file_b', 'file_c', 'file_d']); - - diagnosticService = new UpdateTestSettingService(instance(fs), instance(appEnv), instance(workspace)); - - const files = await diagnosticService.getFilesToBeFixed(resource); - - expect(files).to.deep.equal(['file_a', 'file_c']); - }); - }); - [ - { - testTitle: 'Should fix file if contents contains python.unitTest.', - expectedValue: true, - contents: '{"python.pythonPath":"1234", "python.unitTest.unitTestArgs":[]}' - }, - { - testTitle: 'Should fix file if contents contains python.pyTest.', - expectedValue: true, - contents: '{"python.pythonPath":"1234", "python.pyTestArgs":[]}' - }, - { - testTitle: 'Should fix file if contents contains python.pyTest. & python.unitTest.', - expectedValue: true, - contents: '{"python.pythonPath":"1234", "python.testing.pyTestArgs":[], "python.unitTest.unitTestArgs":[]}' - }, - { - testTitle: 'Should not fix file if contents does not contain python.unitTest. and python.pyTest', - expectedValue: false, - contents: '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[]}' - } - ].forEach(item => { - test(item.testTitle, async () => { - when(fs.readFile(__filename)).thenResolve(item.contents); - - const needsToBeFixed = await diagnosticService.doesFileNeedToBeFixed(__filename); - - expect(needsToBeFixed).to.equal(item.expectedValue); - verify(fs.readFile(__filename)).once(); - }); - }); - test('File should not be fixed if there\'s an error in reading the file', async () => { - when(fs.readFile(__filename)).thenReject(new Error('Kaboom')); - - const needsToBeFixed = await diagnosticService.doesFileNeedToBeFixed(__filename); - - assert.ok(!needsToBeFixed); - verify(fs.readFile(__filename)).once(); - }); - - [ - { - testTitle: 'Should replace python.unitTest.', - contents: '{"python.pythonPath":"1234", "python.unitTest.unitTestArgs":[]}', - expectedContents: '{"python.pythonPath":"1234", "python.testing.unitTestArgs":[]}' - }, - { - testTitle: 'Should replace python.unitTest.pyTest.', - contents: '{"python.pythonPath":"1234", "python.unitTest.pyTestArgs":[], "python.unitTest.pyTestArgs":[], "python.unitTest.pyTestPath":[]}', - expectedContents: '{"python.pythonPath":"1234", "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}', - }, - { - testTitle: 'Should replace python.testing.pyTest.', - contents: '{"python.pythonPath":"1234", "python.testing.pyTestArgs":[], "python.testing.pyTestArgs":[], "python.testing.pyTestPath":[]}', - expectedContents: '{"python.pythonPath":"1234", "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}', - }, - { - testTitle: 'Should not make any changes to the file', - contents: '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[], "python.unitTest.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}', - expectedContents: '{"python.pythonPath":"1234", "python.unittest.unitTestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestArgs":[], "python.testing.pytestPath":[]}' - } - ].forEach(item => { - test(item.testTitle, async () => { - when(fs.readFile(__filename)).thenResolve(item.contents); - when(fs.writeFile(__filename, anything())).thenResolve(); - - await diagnosticService.fixSettingInFile(__filename); - - verify(fs.readFile(__filename)).once(); - verify(fs.writeFile(__filename, item.expectedContents)).once(); - }); - }); -}); diff --git a/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts index 56b81f60893d..24881c71833b 100644 --- a/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts +++ b/src/test/application/diagnostics/commands/execVSCCommands.unit.test.ts @@ -19,7 +19,7 @@ suite('Application Diagnostics - Exec VSC Commands', () => { const serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); commandManager = typemoq.Mock.ofType<ICommandManager>(); serviceContainer - .setup(svc => svc.get<ICommandManager>(typemoq.It.isValue(ICommandManager), typemoq.It.isAny())) + .setup((svc) => svc.get<ICommandManager>(typemoq.It.isValue(ICommandManager), typemoq.It.isAny())) .returns(() => commandManager.object); commandFactory = new DiagnosticsCommandFactory(serviceContainer.object); }); @@ -27,18 +27,24 @@ suite('Application Diagnostics - Exec VSC Commands', () => { 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' }); + 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')) + .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' }); + const command = commandFactory.createCommand(diagnostic.object, { + type: 'executeVSCCommand', + options: 'editor.action.formatDocument', + }); await command.invoke(); expect(command).to.be.instanceOf(ExecuteVSCCommand); 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 0a68368cbd78..90c8e38f8470 100644 --- a/src/test/application/diagnostics/commands/ignore.unit.test.ts +++ b/src/test/application/diagnostics/commands/ignore.unit.test.ts @@ -5,11 +5,14 @@ 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'; -// tslint:disable:no-any - suite('Application Diagnostics - Commands Ignore', () => { let ignoreCommand: IDiagnosticCommand; let serviceContainer: typemoq.IMock<IServiceContainer>; @@ -23,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' as any) + 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 6b01ebe81520..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 no-any - +import { expect } from 'chai'; import * as typemoq from 'typemoq'; import { DiagnosticSeverity } from 'vscode'; -import { DiagnosticCommandPromptHandlerService, MessageCommandPrompt } from '../../../client/application/diagnostics/promptHandler'; -import { DiagnosticScope, IDiagnostic, IDiagnosticCommand, IDiagnosticHandlerService } from '../../../client/application/diagnostics/types'; +import { + 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,13 +29,12 @@ 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' as any, @@ -36,21 +42,24 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + 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; } @@ -59,6 +68,70 @@ 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' as any, @@ -66,32 +139,47 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; const options: MessageCommandPrompt = { - commandPrompts: [ - { prompt: 'Yes' }, - { prompt: 'No' } - ], - message: 'Custom Message' + 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; } @@ -107,37 +195,54 @@ suite('Application Diagnostics - PromptHandler', () => { scope: DiagnosticScope.Global, severity: severity.value, resource: undefined, - invokeHandler: 'default' + invokeHandler: 'default', }; const command = typemoq.Mock.ofType<IDiagnosticCommand>(); const options: MessageCommandPrompt = { commandPrompts: [ { prompt: 'Yes', command: command.object }, - { prompt: 'No', command: command.object } + { prompt: 'No', command: command.object }, ], - message: 'Custom Message' + message: 'Custom Message', }; - command.setup(c => c.invoke()) - .verifiable(typemoq.Times.once()); + 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 index 0bae981033d4..dcff47b2b7e7 100644 --- a/src/test/application/diagnostics/serviceRegistry.unit.test.ts +++ b/src/test/application/diagnostics/serviceRegistry.unit.test.ts @@ -4,20 +4,50 @@ '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 { InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId } from '../../../client/application/diagnostics/checks/invalidPythonPathInDebugger'; -import { LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId } from '../../../client/application/diagnostics/checks/lsNotSupported'; -import { InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId } from '../../../client/application/diagnostics/checks/macPythonInterpreter'; -import { PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId } from '../../../client/application/diagnostics/checks/powerShellActivation'; -import { InvalidPythonInterpreterService, InvalidPythonInterpreterServiceId } from '../../../client/application/diagnostics/checks/pythonInterpreter'; +import { + 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 { + 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 { + 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'; @@ -30,16 +60,84 @@ suite('Application Diagnostics - Register classes in IOC Container', () => { 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, InvalidPythonPathInDebuggerService, InvalidPythonPathInDebuggerServiceId)); - verify(serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, LSNotSupportedDiagnosticService, LSNotSupportedDiagnosticServiceId)); - verify(serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, PowerShellActivationHackDiagnosticsService, PowerShellActivationHackDiagnosticsServiceId)); - verify(serviceManager.addSingleton<IDiagnosticsService>(IDiagnosticsService, InvalidMacPythonInterpreterService, InvalidMacPythonInterpreterServiceId)); - verify(serviceManager.addSingleton<IDiagnosticsCommandFactory>(IDiagnosticsCommandFactory, DiagnosticsCommandFactory)); + 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 ed4a6413fa42..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; diff --git a/src/test/common.ts b/src/test/common.ts index 1a32f4128104..886323e815a5 100644 --- a/src/test/common.ts +++ b/src/test/common.ts @@ -2,31 +2,27 @@ // Licensed under the MIT License. 'use strict'; -// tslint:disable:no-console no-require-imports no-var-requires +// IMPORTANT: Do not import anything from the 'client' folder in this file as that folder is not available during smoke tests. import * as assert from 'assert'; -import * as fs from 'fs-extra'; +import * as fs from '../client/common/platform/fs-paths'; import * as glob from 'glob'; import * as path from 'path'; import { coerce, SemVer } from 'semver'; import { ConfigurationTarget, Event, TextDocument, Uri } from 'vscode'; -import { IExtensionApi } from '../client/api'; +import type { PythonExtension } from '../client/api/types'; import { IProcessService } from '../client/common/process/types'; -import { 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(); @@ -38,33 +34,43 @@ 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' | - 'testing.nosetestArgs' | 'testing.pytestArgs' | 'testing.unittestArgs' | - 'formatting.provider' | 'sortImports.args' | - 'testing.nosetestsEnabled' | 'testing.pytestEnabled' | 'testing.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 || null); + 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(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(this: any, 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 || null); - const value = settings.inspect<string>('pythonPath'); - const prop: 'workspaceFolderValue' | 'workspaceValue' = config === vscode.ConfigurationTarget.Workspace ? 'workspaceValue' : 'workspaceFolderValue'; + 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'; } @@ -256,14 +292,14 @@ export function correctPathForOsType(pathToCorrect: string, os?: OSType): string * @return `SemVer` version of the Python interpreter, or `undefined` if an error occurs. */ export async function getPythonSemVer(procService?: IProcessService): Promise<SemVer | undefined> { - const decoder = await import('../client/common/process/decoder'); - const proc = await import('../client/common/process/proc'); + const proc = await import('../client/common/process/proc.js'); - const pythonProcRunner = procService ? procService : new proc.ProcessService(new decoder.BufferDecoder()); + const pythonProcRunner = procService ? procService : new proc.ProcessService(); const pyVerArgs = ['-c', 'import sys;print("{0}.{1}.{2}".format(*sys.version_info[:3]))']; - return pythonProcRunner.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); @@ -291,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) { @@ -350,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; } } @@ -381,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; } @@ -396,7 +436,7 @@ export async function unzip(zipFile: string, targetFolder: string): Promise<void return new Promise<void>((resolve, reject) => { const zip = new StreamZip({ file: zipFile, - storeEntries: true + storeEntries: true, }); zip.on('ready', async () => { zip.extract('extension', targetFolder, (err: any) => { @@ -410,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(); + const timer = setInterval(async () => { + if (!(await condition().catch(() => false))) { return; } - await sleep(500); - if (completed) { - 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/asyncDump.ts b/src/test/common/asyncDump.ts deleted file mode 100644 index 7236c16e4258..000000000000 --- a/src/test/common/asyncDump.ts +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -//tslint:disable:no-require-imports no-var-requires -const log = require('why-is-node-running'); - -// Call this function to debug async hangs. It should print out stack traces of still running promises. -export function asyncDump() { - log(); -} diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 066ed1613a56..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 as any)[key] as string); - if (key.endsWith('Path') && IS_WINDOWS) { - assert.equal(settingValue.toUpperCase(), pythonSettingValue.toUpperCase(), `Setting ${key} not the same`); - } else if (key === 'workspaceSymbols' && IS_WINDOWS) { - const workspaceSettings = (pythonSettingValue as {} as IWorkspaceSymbolSettings); - const workspaceSttings = (settingValue as {} as IWorkspaceSymbolSettings); - assert.equal(workspaceSettings.tagFilePath.toUpperCase(), workspaceSttings.tagFilePath.toUpperCase(), `Setting ${key} not the same`); - const workspaceSettingsWithoutPath = { ...workspaceSettings }; - 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 327e6ef91361..65afc782d7bb 100644 --- a/src/test/common/configSettings/configSettings.unit.test.ts +++ b/src/test/common/configSettings/configSettings.unit.test.ts @@ -5,111 +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, - ITestingSettings, - IWorkspaceSymbolSettings } from '../../../client/common/types'; import { noop } from '../../../client/common/utils/misc'; +import * as EnvFileTelemetry from '../../../client/telemetry/envFileTelemetry'; +import { ITestingSettings } from '../../../client/testing/configuration/types'; import { MockAutoSelectionService } from '../../mocks/autoSelector'; +import { MockMemento } from '../../mocks/mementos'; +import { untildify } from '../../../client/common/helpers'; +import { MockExtensions } from '../../mocks/extensions'; -// tslint:disable-next-line:max-func-body-length -suite('Python Settings', () => { +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', 'pipenvPath', 'envFile', 'poetryPath']) { - config.setup(c => c.get<string>(name)) - // tslint:disable-next-line:no-any + 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]); } - if (sourceSettings.jediEnabled) { - config.setup(c => c.get<string>('jediPath')) - .returns(() => sourceSettings.jediPath); - } for (const name of ['venvFolders']) { - config.setup(c => c.get<string[]>(name)) - // tslint:disable-next-line:no-any + 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)) - // tslint:disable-next-line:no-any - .returns(() => (sourceSettings as any)[name]); - } - for (const name of ['disableInstallationCheck', 'globalModuleInstallation']) { - config.setup(c => c.get<boolean>(name)) - // tslint:disable-next-line:no-any + for (const name of ['globalModuleInstallation']) { + config + .setup((c) => c.get<boolean>(name)) + .returns(() => (sourceSettings as any)[name]); } - // number settings - if (sourceSettings.jediEnabled) { - config.setup(c => c.get<number>('jediMemoryLimit')) - .returns(() => sourceSettings.jediMemoryLimit); - } + // Language server type settings + config.setup((c) => c.get<LanguageServerType>('languageServer')).returns(() => sourceSettings.languageServer); // "any" settings - // tslint:disable-next-line:no-any - config.setup(c => c.get<any[]>('devOptions')) - .returns(() => sourceSettings.devOptions); + + 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<ITestingSettings>('testing')) - .returns(() => sourceSettings.testing); - config.setup(c => c.get<ITerminalSettings>('terminal')) - .returns(() => sourceSettings.terminal); - config.setup(c => c.get<IDataScienceSettings>('dataScience')) - .returns(() => sourceSettings.datascience); + config.setup((c) => c.get<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()); @@ -119,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()); @@ -133,55 +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)) { - // tslint:disable-next-line:no-any - expect((settings.formatting as any)[key]).to.be.deep.equal((expected.formatting as any)[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); }); - test('Formatter Paths (paths relative to home)', () => { + + function testPyreflySettings(pyreflyInstalled: boolean, pyreflyDisabled: boolean, languageServerDisabled: boolean) { + test(`pyrefly ${pyreflyInstalled ? 'installed' : 'not installed'} and ${ + pyreflyDisabled ? 'disabled' : 'enabled' + }`, () => { + if (pyreflyInstalled) { + extensions.extensionIdsToFind = ['meta.pyrefly']; + } else { + extensions.extensionIdsToFind = []; + } + config.setup((c) => c.get<boolean>('pyrefly.disableLanguageServices')).returns(() => pyreflyDisabled); + + config + .setup((c) => c.get<string>('languageServer')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + + settings.update(config.object); + + if (languageServerDisabled) { + expect(settings.languageServer).to.equal(LanguageServerType.None); + } else { + expect(settings.languageServer).not.to.equal(LanguageServerType.None); + } + expect(settings.languageServerIsDefault).to.equal(true); + config.verifyAll(); + }); + } + + suite('pyrefly languageServer settings', async () => { + const values = [ + { pyreflyInstalled: true, pyreflyDisabled: false, languageServerDisabled: true }, + { pyreflyInstalled: true, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: true, languageServerDisabled: false }, + { pyreflyInstalled: false, pyreflyDisabled: false, languageServerDisabled: false }, + ]; + + values.forEach((v) => { + testPyreflySettings(v.pyreflyInstalled, v.pyreflyDisabled, v.languageServerDisabled); + }); + }); + + function testExperiments(enabled: boolean) { expected.pythonPath = 'python3'; - // tslint:disable-next-line:no-any - expected.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; - } - // tslint:disable-next-line:no-any - const expectedPath = untildify((expected.formatting as any)[key]); - // tslint:disable-next-line:no-any - expect((settings.formatting as any)[key]).to.be.equal(expectedPath); + 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 466f67a221ca..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 } 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 '../../testing/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', () => { 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 asyncRegistry = serviceContainer.get<IDisposableRegistry>(IDisposableRegistry); + let subs = serviceContainer.get<IExtensionContext>(IExtensionContext).subscriptions; + const oldLength = subs.length; const disposable = { dispose(): Promise<void> { - disposed = true; return Promise.resolve(); - } + }, }; asyncRegistry.push(disposable); - await asyncRegistry.dispose(); - expect(disposed).to.be.equal(true, 'Didn\'t dispose during async registry cleanup'); + subs = serviceContainer.get<IExtensionContext>(IExtensionContext).subscriptions; + const newLength = subs.length; + expect(newLength).to.be.equal(oldLength + 1, 'Subscription not added'); + // serviceContainer subscriptions are not disposed of as this breaks other tests that use the service container. }); }); diff --git a/src/test/common/configuration/service.unit.test.ts b/src/test/common/configuration/service.unit.test.ts 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/crypto.unit.test.ts b/src/test/common/crypto.unit.test.ts deleted file mode 100644 index a7274f3900af..000000000000 --- a/src/test/common/crypto.unit.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert } from 'chai'; -import { CryptoUtils } from '../../client/common/crypto'; - -suite('Crypto Utils', async () => { - let crypto: CryptoUtils; - setup(() => { - crypto = new CryptoUtils(); - }); - test('If hashFormat equals `number`, method createHash() returns a number', async () => { - const hash = crypto.createHash('blabla', 'hex', 'number'); - assert.typeOf(hash, 'number', 'Type should be a number'); - }); - test('If hashFormat equals `string`, method createHash() returns a string', async () => { - const hash = crypto.createHash('blabla', 'hex', 'string'); - assert.typeOf(hash, 'string', 'Type should be a string'); - }); -}); 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 index fa844b43a856..cb04d3a90b38 100644 --- a/src/test/common/exitCIAfterTestReporter.ts +++ b/src/test/common/exitCIAfterTestReporter.ts @@ -7,8 +7,8 @@ // This is a hack, however for some reason the process running the tests do not exit. // The hack is to force it to die when tests are done, if this doesn't work we've got a bigger problem on our hands. -// tslint:disable:no-var-requires no-require-imports no-any no-console no-unnecessary-class no-default-export -import * as fs from 'fs-extra'; +import * as fs from '../../client/common/platform/fs-paths'; + import * as net from 'net'; import * as path from 'path'; import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../constants'; @@ -25,7 +25,7 @@ async function connectToServer() { } const port = parseInt(await fs.readFile(portFile, 'utf-8'), 10); console.log(`Need to connect to port ${port}`); - return new Promise(resolve => { + return new Promise<void>((resolve) => { try { client = new net.Socket(); client.connect({ port }, () => { diff --git a/src/test/common/experiments.unit.test.ts b/src/test/common/experiments.unit.test.ts deleted file mode 100644 index 870f47f665fb..000000000000 --- a/src/test/common/experiments.unit.test.ts +++ /dev/null @@ -1,687 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any - -import { assert, expect } from 'chai'; -import { anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { ApplicationEnvironment } from '../../client/common/application/applicationEnvironment'; -import { IApplicationEnvironment, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { CryptoUtils } from '../../client/common/crypto'; -import { configUri, downloadedExperimentStorageKey, ExperimentsManager, experimentStorageKey, isDownloadedStorageValidKey } from '../../client/common/experiments'; -import { HttpClient } from '../../client/common/net/httpClient'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../client/common/platform/types'; -import { ABExperiments, ICryptoUtils, IHttpClient, IOutputChannel, IPersistentState, IPersistentStateFactory } from '../../client/common/types'; -import { createDeferred, createDeferredFromPromise } from '../../client/common/utils/async'; -import { sleep } from '../common'; - -// tslint:disable-next-line: max-func-body-length -suite('A/B experiments', () => { - let workspaceService: IWorkspaceService; - let httpClient: IHttpClient; - let crypto: ICryptoUtils; - let appEnvironment: IApplicationEnvironment; - let persistentStateFactory: IPersistentStateFactory; - let isDownloadedStorageValid: TypeMoq.IMock<IPersistentState<boolean>>; - let experimentStorage: TypeMoq.IMock<IPersistentState<any>>; - let downloadedExperimentsStorage: TypeMoq.IMock<IPersistentState<any>>; - let output: TypeMoq.IMock<IOutputChannel>; - let fs: IFileSystem; - let expManager: ExperimentsManager; - setup(() => { - workspaceService = mock(WorkspaceService); - httpClient = mock(HttpClient); - crypto = mock(CryptoUtils); - appEnvironment = mock(ApplicationEnvironment); - persistentStateFactory = mock(PersistentStateFactory); - isDownloadedStorageValid = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - experimentStorage = TypeMoq.Mock.ofType<IPersistentState<any>>(); - downloadedExperimentsStorage = TypeMoq.Mock.ofType<IPersistentState<any>>(); - output = TypeMoq.Mock.ofType<IOutputChannel>(); - fs = mock(FileSystem); - when(persistentStateFactory.createGlobalPersistentState(isDownloadedStorageValidKey, false, anything())).thenReturn(isDownloadedStorageValid.object); - when(persistentStateFactory.createGlobalPersistentState(experimentStorageKey, undefined as any)).thenReturn(experimentStorage.object); - when(persistentStateFactory.createGlobalPersistentState(downloadedExperimentStorageKey, undefined as any)).thenReturn(downloadedExperimentsStorage.object); - expManager = new ExperimentsManager(instance(persistentStateFactory), instance(workspaceService), instance(httpClient), instance(crypto), instance(appEnvironment), output.object, instance(fs)); - }); - - async function testInitialization( - downloadError: boolean = false, - experimentsDownloaded?: any - ) { - if (downloadError) { - when(httpClient.getJSON(configUri, false)).thenReject(new Error('Kaboom')); - } else { - if (experimentsDownloaded) { - when(httpClient.getJSON(configUri, false)).thenResolve(experimentsDownloaded); - } else { - when(httpClient.getJSON(configUri, false)).thenResolve([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]); - } - } - - try { - await expManager.initializeInBackground(); - // tslint:disable-next-line:no-empty - } catch { } - - isDownloadedStorageValid.verifyAll(); - experimentStorage.verifyAll(); - } - - test('Initializing experiments does not download experiments if storage is valid and contains experiments', async () => { - isDownloadedStorageValid.setup(n => n.value).returns(() => true).verifiable(TypeMoq.Times.once()); - - await testInitialization(); - - verify(httpClient.getJSON(configUri, false)).never(); - }); - - test('If storage has expired, initializing experiments downloads the experiments, but does not store them if they are invalid or incomplete', async () => { - const experiments = [{ name: 'experiment1', salt: 'salt', max: 100 }]; - isDownloadedStorageValid - .setup(n => n.value) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - isDownloadedStorageValid - .setup(n => n.updateValue(true)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - downloadedExperimentsStorage - .setup(n => n.updateValue(experiments)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - // downloadError = false, experimentsDownloaded = experiments - await testInitialization(false, experiments); - - verify(httpClient.getJSON(configUri, false)).once(); - }); - - test('If storage has expired, initializing experiments downloads the experiments, and stores them if they are valid', async () => { - isDownloadedStorageValid.setup(n => n.value).returns(() => false).verifiable(TypeMoq.Times.once()); - isDownloadedStorageValid.setup(n => n.updateValue(true)).returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage.setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])).returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.once()); - - await testInitialization(); - - verify(httpClient.getJSON(configUri, false)).once(); - }); - - test('If downloading experiments fails with error, the storage is left as it is', async () => { - isDownloadedStorageValid.setup(n => n.value).returns(() => false).verifiable(TypeMoq.Times.once()); - isDownloadedStorageValid.setup(n => n.updateValue(true)).returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.never()); - downloadedExperimentsStorage.setup(n => n.updateValue(anything())).returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.never()); - - // downloadError = true - await testInitialization(true); - - verify(httpClient.getJSON(configUri, false)).once(); - }); - - test('If the users have opted out of telemetry, then they are opted out of AB testing ', async () => { - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - const settings = { globalValue: false }; - - when( - workspaceService.getConfiguration('telemetry') - ).thenReturn(workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<boolean>('enableTelemetry')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.never()); - - await expManager.activate(); - - verify(workspaceService.getConfiguration('telemetry')).once(); - workspaceConfig.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('Ensure experiments can only be activated once', async () => { - // Activate it twice and check - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - const settings = {}; - - when( - workspaceService.getConfiguration('telemetry') - ).thenReturn(workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<boolean>('enableTelemetry')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - when( - fs.fileExists(anything()) - ).thenResolve(false); - experimentStorage.setup(n => n.value).returns(() => undefined) - .verifiable(TypeMoq.Times.exactly(2)); - isDownloadedStorageValid - .setup(n => n.value) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - - // First activation - await expManager.activate(); - - resetCalls(workspaceService); - - // Second activation - await expManager.activate(); - - verify(workspaceService.getConfiguration(anything())).never(); - - workspaceConfig.verifyAll(); - verify(fs.fileExists(anything())).once(); - isDownloadedStorageValid.verifyAll(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('Ensure experiments are reliably initialized in the background', async () => { - const experimentsDeferred = createDeferred<ABExperiments>(); - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - const settings = {}; - - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - isDownloadedStorageValid - .setup(n => n.value) - .returns(() => false) - .verifiable(TypeMoq.Times.once()); - isDownloadedStorageValid - .setup(n => n.updateValue(true)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - when( - workspaceService.getConfiguration('telemetry') - ) - .thenReturn(workspaceConfig.object); - workspaceConfig - .setup(c => c.inspect<boolean>('enableTelemetry')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - when( - httpClient.getJSON(configUri, false) - ) - .thenReturn(experimentsDeferred.promise); - - const promise = expManager.activate(); - const deferred = createDeferredFromPromise(promise); - await sleep(1); - - // Ensure activate() function has completed while initialization is still running - assert.equal(deferred.completed, true); - - experimentsDeferred.resolve([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]); - await sleep(1); - - verify( - workspaceService.getConfiguration('telemetry') - ).once(); - workspaceConfig.verifyAll(); - isDownloadedStorageValid.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - verify( - httpClient.getJSON(configUri, false) - ).once(); - }); - - test('Ensure experiment storage is updated to contain the latest downloaded experiments', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) - .verifiable(TypeMoq.Times.atLeastOnce()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) - .returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.once()); - - await expManager.updateExperimentStorage(); - - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('When no downloaded experiments are available, and experiment storage contains experiments, then experiment storage is not updated', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - // tslint:disable-next-line:no-multiline-string - const fileContent = ` - [{ "name": "experiment1", "salt": "salt", "min": 90, "max": 100 }] - `; - - when( - fs.fileExists(anything()) - ).thenResolve(true); - when( - fs.readFile(anything()) - ).thenResolve(fileContent); - - experimentStorage - .setup(n => n.value) - .returns(() => [{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }]) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).never(); - verify(fs.readFile(anything())).never(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('When no downloaded experiments are available, experiment storage is empty, but if local experiments file is not valid, experiment storage is not updated', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - // tslint:disable-next-line:no-multiline-string - const fileContent = ` - // Yo! I am a JSON file with comments as well as trailing commas! - - [{ "name": "experiment1", "salt": "salt", "min": 90, },] - `; - - when( - fs.fileExists(anything()) - ).thenResolve(true); - when( - fs.readFile(anything()) - ).thenResolve(fileContent); - - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.never()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).once(); - verify(fs.readFile(anything())).once(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('When no downloaded experiments are available, and experiment storage is empty, then experiment storage is updated using local experiments file given experiments are valid', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - // tslint:disable-next-line:no-multiline-string - const fileContent = ` - // Yo! I am a JSON file with comments as well as trailing commas! - - [{ "name": "experiment1", "salt": "salt", "min": 90, "max": 100, },] - `; - - when( - fs.fileExists(anything()) - ).thenResolve(true); - when( - fs.readFile(anything()) - ).thenResolve(fileContent); - - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue([{ name: 'experiment1', salt: 'salt', min: 90, max: 100 }])) - .returns(() => Promise.resolve(undefined)).verifiable(TypeMoq.Times.once()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).once(); - verify(fs.readFile(anything())).once(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - // tslint:disable-next-line:max-func-body-length - suite('When no downloaded experiments are available, and experiment storage is empty, then function updateExperimentStorage() stops execution and returns', () => { - test('If checking the existence of config file fails', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - const error = new Error('Kaboom'); - when( - fs.fileExists(anything()) - ).thenThrow(error); - when( - fs.readFile(anything()) - ).thenResolve('fileContent'); - - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).once(); - verify(fs.readFile(anything())).never(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('If reading config file fails', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - const error = new Error('Kaboom'); - when( - fs.fileExists(anything()) - ).thenResolve(true); - when( - fs.readFile(anything()) - ).thenThrow(error); - - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).once(); - verify(fs.readFile(anything())).once(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('If config file does not exist', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - when( - fs.fileExists(anything()) - ).thenResolve(false); - when( - fs.readFile(anything()) - ).thenResolve('fileContent'); - - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).once(); - verify(fs.readFile(anything())).never(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - - test('If parsing file or updating storage fails', async () => { - downloadedExperimentsStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - downloadedExperimentsStorage - .setup(n => n.updateValue(undefined)) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - // tslint:disable-next-line:no-multiline-string - const fileContent = ` - // Yo! I am a JSON file with comments as well as trailing commas! - - [{ "name": "experiment1", "salt": "salt", "min": 90, "max": 100 },] - `; - const error = new Error('Kaboom'); - when( - fs.fileExists(anything()) - ).thenResolve(true); - when( - fs.readFile(anything()) - ).thenResolve(fileContent); - - experimentStorage - .setup(n => n.value) - .returns(() => undefined) - .verifiable(TypeMoq.Times.once()); - experimentStorage - .setup(n => n.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.reject(error)) - .verifiable(TypeMoq.Times.once()); - - await expManager.updateExperimentStorage(); - - verify(fs.fileExists(anything())).once(); - verify(fs.readFile(anything())).once(); - experimentStorage.verifyAll(); - downloadedExperimentsStorage.verifyAll(); - }); - }); - - const testsForInExperiment = - [ - { - testName: 'If experiment\'s name is not in user experiment list, user is not in experiment', - experimentName: 'imaginary experiment', - userExperiments: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: false - }, - { - testName: 'If experiment\'s name is in user experiment list and hash modulo output is in range, user is in experiment', - experimentName: 'experiment1', - userExperiments: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: true - } - ]; - - testsForInExperiment.forEach(testParams => { - test(testParams.testName, async () => { - expManager.userExperiments = testParams.userExperiments; - expect(expManager.inExperiment(testParams.experimentName)).to.equal(testParams.expectedResult, 'Incorrectly identified'); - }); - }); - - const testsForIsUserInRange = - [ - // Note min equals 79 and max equals 94 - { - testName: 'Returns true if hash modulo output is in range', - hash: 1181, - expectedResult: true - }, - { - testName: 'Returns false if hash modulo is less than min', - hash: 967, - expectedResult: false - }, - { - testName: 'Returns false if hash modulo is more than max', - hash: 3297, - expectedResult: false - }, - { - testName: 'If checking if user is in range fails with error, throw error', - hash: 3297, - error: true, - expectedResult: false - }, - { - testName: 'If machine ID is bogus, throw error', - hash: 3297, - machineIdError: true, - expectedResult: false - } - ]; - - suite('Function IsUserInRange()', () => { - testsForIsUserInRange.forEach(testParams => { - test(testParams.testName, async () => { - when(appEnvironment.machineId).thenReturn('101'); - if (testParams.machineIdError) { - when(appEnvironment.machineId).thenReturn(undefined as any); - expect(() => expManager.isUserInRange(79, 94, 'salt')).to.throw(); - } else if (testParams.error) { - const error = new Error('Kaboom'); - when(crypto.createHash(anything(), 'hex', 'number')).thenThrow(error); - expect(() => expManager.isUserInRange(79, 94, 'salt')).to.throw(error); - } else { - when(crypto.createHash(anything(), 'hex', 'number')).thenReturn(testParams.hash); - expect(expManager.isUserInRange(79, 94, 'salt')).to.equal(testParams.expectedResult, 'Incorrectly identified'); - } - }); - }); - }); - - const testsForPopulateUserExperiments = - [ - { - testName: 'User experiments list is empty if experiment storage value is not an array', - experimentStorageValue: undefined, - expectedResult: [] - }, - { - testName: 'User experiments list is empty if experiment storage value is an empty array', - experimentStorageValue: [], - expectedResult: [] - }, - { - testName: 'User experiments list contains the experiment if and only if user is in experiment range', - experimentStorageValue: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - hash: 8187, - expectedResult: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }] - } - ]; - - testsForPopulateUserExperiments.forEach(testParams => { - test(testParams.testName, async () => { - experimentStorage - .setup(n => n.value) - .returns(() => testParams.experimentStorageValue); - when(appEnvironment.machineId).thenReturn('101'); - if (testParams.hash) { - when(crypto.createHash(anything(), 'hex', 'number')).thenReturn(testParams.hash); - } - expManager.populateUserExperiments(); - assert.deepEqual(expManager.userExperiments, testParams.expectedResult); - }); - }); - - const testsForAreExperimentsValid = - [ - { - testName: 'If experiments are not an array, return false', - experiments: undefined, - expectedResult: false - }, - { - testName: 'If any experiment have `min` field missing, return false', - experiments: [{ name: 'experiment1', salt: 'salt', max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: false - }, - { - testName: 'If any experiment have `max` field missing, return false', - experiments: [{ name: 'experiment1', salt: 'salt', min: 79 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: false - }, - { - testName: 'If any experiment have `salt` field missing, return false', - experiments: [{ name: 'experiment1', min: 79, max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: false - }, - { - testName: 'If any experiment have `name` field missing, return false', - experiments: [{ salt: 'salt', min: 79, max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: false - }, - { - testName: 'If all experiments contain all the fields in type `ABExperiment`, return true', - experiments: [{ name: 'experiment1', salt: 'salt', min: 79, max: 94 }, { name: 'experiment2', salt: 'salt', min: 19, max: 30 }], - expectedResult: true - } - ]; - - suite('Function areExperimentsValid()', () => { - testsForAreExperimentsValid.forEach(testParams => { - test(testParams.testName, () => { - expect(expManager.areExperimentsValid(testParams.experiments as any)).to.equal(testParams.expectedResult); - }); - }); - }); -}); 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 e1b1d03ab08d..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 8eb649d073c3..000000000000 --- a/src/test/common/installer.test.ts +++ /dev/null @@ -1,134 +0,0 @@ -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -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 { TerminalHelper } from '../../client/common/terminal/helper'; -import { ITerminalHelper } from '../../client/common/terminal/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 '../testing/serviceRegistry'; -import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; - -// tslint:disable-next-line:max-func-body-length -suite('Installer', () => { - let ioc: UnitTestIocContainer; - const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); - const resource = IS_MULTI_ROOT_TEST ? workspaceUri : undefined; - suiteSetup(initializeTest); - setup(async () => { - await initializeTest(); - await resetSettings(); - initializeDI(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await resetSettings(); - }); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerUnitTestTypes(); - ioc.registerFileSystemTypes(); - ioc.registerVariableTypes(); - ioc.registerLinterTypes(); - ioc.registerFormatterTypes(); - - ioc.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)); - ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); - 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)); - ioc.serviceManager.addSingletonInstance<ITerminalHelper>(ITerminalHelper, instance(mock(TerminalHelper))); - 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 index 798ee0fa8f6a..64a4a35539e4 100644 --- a/src/test/common/installer/condaInstaller.unit.test.ts +++ b/src/test/common/installer/condaInstaller.unit.test.ts @@ -9,28 +9,47 @@ 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 { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { ICondaService } from '../../../client/interpreter/contracts'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; +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: CondaInstaller; + 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 CondaInstaller(instance(serviceContainer)); + installer = new CondaInstallerTest(instance(serviceContainer)); }); test('Name and priority', async () => { - assert.equal(installer.displayName, 'Conda'); - assert.equal(installer.priority, 0); + assert.strictEqual(installer.displayName, 'Conda'); + assert.strictEqual(installer.name, 'Conda'); + assert.strictEqual(installer.priority, 10); + }); + test('Installer is not supported when conda is available variable is set to false', async () => { + const uri = Uri.file(__filename); + 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); @@ -38,7 +57,7 @@ suite('Common - Conda Installer', () => { const supported = await installer.isSupported(uri); - assert.equal(supported, false); + assert.strictEqual(supported, false); }); test('Installer is not supported when current env is not a conda env', async () => { const uri = Uri.file(__filename); @@ -48,11 +67,11 @@ suite('Common - Conda Installer', () => { when(settings.pythonPath).thenReturn(pythonPath); when(condaService.isCondaAvailable()).thenResolve(true); when(configService.getSettings(uri)).thenReturn(instance(settings)); - when(condaService.isCondaEnvironment(pythonPath)).thenResolve(false); + when(condaLocatorService.isCondaEnvironment(pythonPath)).thenResolve(false); const supported = await installer.isSupported(uri); - assert.equal(supported, false); + assert.strictEqual(supported, false); }); test('Installer is supported when current env is a conda env', async () => { const uri = Uri.file(__filename); @@ -62,10 +81,56 @@ suite('Common - Conda Installer', () => { when(settings.pythonPath).thenReturn(pythonPath); when(condaService.isCondaAvailable()).thenResolve(true); when(configService.getSettings(uri)).thenReturn(instance(settings)); - when(condaService.isCondaEnvironment(pythonPath)).thenResolve(true); + when(condaLocatorService.isCondaEnvironment(pythonPath)).thenResolve(true); const supported = await installer.isSupported(uri); - assert.equal(supported, true); + assert.strictEqual(supported, true); + }); + test('Include name of environment', async () => { + const uri = Uri.file(__filename); + 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 a95b55d81421..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<boolean>(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) - ).returns(() => persistValue.object); - await installer.promptToInstall(product.value, resource); - productPathService.verifyAll(); - }); - } - } - }); - }); - }); -}); diff --git a/src/test/common/installer/installer.unit.test.ts b/src/test/common/installer/installer.unit.test.ts deleted file mode 100644 index 31a230e43bc6..000000000000 --- a/src/test/common/installer/installer.unit.test.ts +++ /dev/null @@ -1,346 +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); - }); - break; - } - default: { - test(`Ensure the prompt is displayed only once, until the prompt is closed, ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) - .verifiable(TypeMoq.Times.exactly(resource ? 5 : 0)); - app.setup(a => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns( - () => { - return promptDeferred.promise; - }) - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - persistVal.setup(p => p.value).returns(() => false); - persistVal.setup(p => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore.setup(ps => - ps.createGlobalPersistentState<boolean>(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) - ).returns(() => persistVal.object); - - // Display first prompt. - installer.promptToInstall(product.value, resource).ignoreErrors(); - - // 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(); - }); - test(`Ensure the prompt is displayed again when previous prompt has been closed, ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) - .verifiable(TypeMoq.Times.exactly(resource ? 3 : 0)); - app.setup(a => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.exactly(3)); - const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - persistVal.setup(p => p.value).returns(() => false); - persistVal.setup(p => p.updateValue(TypeMoq.It.isValue(true))); - persistentStore.setup(ps => - ps.createGlobalPersistentState<boolean>(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) - ).returns(() => persistVal.object); - - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - await installer.promptToInstall(product.value, resource); - - app.verifyAll(); - workspaceService.verifyAll(); - }); - - if (product.value === Product.pylint) { - test(`Ensure the install prompt is not displayed when the user requests it not be shown again, ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object) - .verifiable(TypeMoq.Times.exactly(resource ? 2 : 0)); - app.setup(a => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'))) - .returns( - async () => { - return 'Do not show again'; - }) - .verifiable(TypeMoq.Times.once()); - const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - let mockPersistVal = false; - persistVal.setup(p => p.value).returns(() => { - return mockPersistVal; - }); - persistVal.setup(p => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }).verifiable(TypeMoq.Times.once()); - persistentStore.setup(ps => - ps.createGlobalPersistentState<boolean>(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) - ).returns(() => { - return persistVal.object; - }).verifiable(TypeMoq.Times.exactly(3)); - - // Display first prompt. - const initialResponse = await installer.promptToInstall(product.value, resource); - - // Display a second prompt. - const secondResponse = await installer.promptToInstall(product.value, resource); - - expect(initialResponse).to.be.equal(InstallerResponse.Ignore); - expect(secondResponse).to.be.equal(InstallerResponse.Ignore); - - app.verifyAll(); - workspaceService.verifyAll(); - persistentStore.verifyAll(); - persistVal.verifyAll(); - }); - } else if (productService.getProductType(product.value) === ProductType.Linter) { - test(`Ensure the 'do not show again' prompt isn't shown for non-pylint linters, ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource!))) - .returns(() => TypeMoq.Mock.ofType<WorkspaceFolder>().object); - app.setup(a => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'))) - .returns( - async () => { - return undefined; - }) - .verifiable(TypeMoq.Times.once()); - app.setup(a => - a.showErrorMessage( - TypeMoq.It.isAnyString(), - TypeMoq.It.isValue('Install'), - TypeMoq.It.isValue('Select Linter'), - TypeMoq.It.isValue('Do not show again'))) - .returns( - async () => { - return undefined; - }) - .verifiable(TypeMoq.Times.never()); - const persistVal = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - let mockPersistVal = false; - persistVal.setup(p => p.value).returns(() => { - return mockPersistVal; - }); - persistVal.setup(p => p.updateValue(TypeMoq.It.isValue(true))) - .returns(() => { - mockPersistVal = true; - return Promise.resolve(); - }); - persistentStore.setup(ps => - ps.createGlobalPersistentState<boolean>(TypeMoq.It.isAnyString(), TypeMoq.It.isValue(undefined)) - ).returns(() => { - return persistVal.object; - }); - - // Display the prompt. - await installer.promptToInstall(product.value, resource); - - // we're just ensuring the 'disable pylint' prompt never appears... - app.verifyAll(); - }); - } - } - - test(`Ensure resource info is passed into the module installer ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const moduleName = installer.translateProductToModuleName(product.value, ModuleNamePurpose.install); - 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()); - } - }); - } - }); - - 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 a86cd69bbedd..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,209 +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 getActiveInterpreter is used in PipInstaller (${product.name})`, async () => { - setActiveInterpreter(); - try { - await installer.installModule(product.name, resource); - } catch { - noop(); - } - interpreterService.verifyAll(); - }); - } - if (installerClass === PipInstaller) { - test(`Test Args (${product.name})`, async () => { - setActiveInterpreter(); - const proxyArgs = proxyServer.length === 0 ? [] : ['--proxy', proxyServer]; - const expectedArgs = ['-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']; - if (moduleName === 'black') { - expectedArgs.push('--pre') - } - await installModuleAndVerifyCommand(pipenvName, expectedArgs); - }); - } - if (installerClass === CondaInstaller) { - test(`Test args (${product.name})`, async () => { - setActiveInterpreter(); - const expectedArgs = ['install']; - if (condaEnvInfo && condaEnvInfo.name) { - expectedArgs.push('--name'); - expectedArgs.push(condaEnvInfo.name.toCommandArgument()); - } else if (condaEnvInfo && condaEnvInfo.path) { - expectedArgs.push('--prefix'); - expectedArgs.push(condaEnvInfo.path.fileToCommandArgument()); - } - expectedArgs.push(moduleName); - 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, + ); + }); + }); + } + }); }); }); }); @@ -246,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 index e4bca8b3420b..07d60159138e 100644 --- a/src/test/common/installer/poetryInstaller.unit.test.ts +++ b/src/test/common/installer/poetryInstaller.unit.test.ts @@ -3,7 +3,10 @@ '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'; @@ -11,126 +14,178 @@ import { WorkspaceService } from '../../../client/common/application/workspace'; import { PythonSettings } from '../../../client/common/configSettings'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { PoetryInstaller } from '../../../client/common/installer/poetryInstaller'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { ProcessService } from '../../../client/common/process/proc'; -import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; +import { ExecutionResult, ShellOptions } from '../../../client/common/process/types'; import { ExecutionInfo, IConfigurationService } from '../../../client/common/types'; import { ServiceContainer } from '../../../client/ioc/container'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { TEST_LAYOUT_ROOT } from '../../pythonEnvironments/common/commonTestConstants'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; +import { EnvironmentType } from '../../../client/pythonEnvironments/info'; -// tslint:disable-next-line:max-func-body-length suite('Module Installer - Poetry', () => { class TestInstaller extends PoetryInstaller { - // tslint:disable-next-line:no-unnecessary-override public getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> { return super.getExecutionInfo(moduleName, resource); } } + const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); + const project1 = path.join(testPoetryDir, 'project1'); let poetryInstaller: TestInstaller; let workspaceService: IWorkspaceService; let configurationService: IConfigurationService; - let fileSystem: IFileSystem; - let processServiceFactory: IProcessServiceFactory; + let interpreterService: IInterpreterService; + let serviceContainer: ServiceContainer; + let shellExecute: sinon.SinonStub; + setup(() => { - const serviceContainer = mock(ServiceContainer); + serviceContainer = mock(ServiceContainer); + interpreterService = mock<IInterpreterService>(); + when(serviceContainer.get<IInterpreterService>(IInterpreterService)).thenReturn(instance(interpreterService)); workspaceService = mock(WorkspaceService); configurationService = mock(ConfigurationService); - fileSystem = mock(FileSystem); - processServiceFactory = mock(ProcessServiceFactory); - poetryInstaller = new TestInstaller(instance(serviceContainer), instance(workspaceService), - instance(configurationService), instance(fileSystem), - instance(processServiceFactory)); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + shellExecute.callsFake((command: string, options: ShellOptions) => { + // eslint-disable-next-line default-case + switch (command) { + case 'poetry env list --full-path': + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + case 'poetry env info -p': { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project1)) { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `${path.join(project1, '.venv')} \n`, + }); + } + } + } + return Promise.reject(new Error('Command failed')); + }); + + poetryInstaller = new TestInstaller( + instance(serviceContainer), + instance(workspaceService), + instance(configurationService), + ); + }); + + 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.equal(supported, false); + assert.strictEqual(supported, false); }); test('Is not supported when there is no workspace', async () => { when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.equal(supported, false); + assert.strictEqual(supported, false); }); - test('Is not supported when the poetry file does not exists', async () => { + test('Get Executable info', async () => { const uri = Uri.file(__dirname); - when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - when(fileSystem.fileExists(anything())).thenResolve(false); + const settings = mock(PythonSettings); - const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); + + const info = await poetryInstaller.getExecutionInfo('something', uri); - assert.equal(supported, false); + assert.deepEqual(info, { args: ['add', '--group', 'dev', 'something'], execPath: 'poetry path' }); }); - test('Is not supported when the poetry is not available (with stderr)', async () => { + test('Get executable info when installing black', async () => { const uri = Uri.file(__dirname); - const processService = mock(ProcessService); const settings = mock(PythonSettings); - when(configurationService.getSettings(anything())).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry'); - when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - when(fileSystem.fileExists(anything())).thenResolve(true); - when(processServiceFactory.create(anything())).thenResolve(instance(processService)); - when(processService.exec(anything(), anything(), anything())).thenResolve({ stderr: 'Kaboom', stdout: '' }); + when(configurationService.getSettings(uri)).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry path'); - const supported = await poetryInstaller.isSupported(Uri.file(__filename)); + const info = await poetryInstaller.getExecutionInfo('black', uri); - assert.equal(supported, false); + assert.deepEqual(info, { + args: ['add', '--group', 'dev', 'black'], + execPath: 'poetry path', + }); }); - test('Is not supported when the poetry is not available (with error running poetry)', async () => { - const uri = Uri.file(__dirname); - const processService = mock(ProcessService); + test('Is supported returns true if selected interpreter is related to the workspace', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: path.join(project1, '.venv', 'Scripts', 'python.exe'), + envType: EnvironmentType.Poetry, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); when(configurationService.getSettings(anything())).thenReturn(instance(settings)); when(settings.poetryPath).thenReturn('poetry'); when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - when(fileSystem.fileExists(anything())).thenResolve(true); - when(processServiceFactory.create(anything())).thenResolve(instance(processService)); - when(processService.exec(anything(), anything(), anything())).thenReject(new Error('Kaboom')); const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.equal(supported, false); + assert.strictEqual(supported, true); }); - test('Is supported', async () => { - const uri = Uri.file(__dirname); - const processService = mock(ProcessService); + + test('Is supported returns true if no interpreter is selected', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); - when(configurationService.getSettings(uri)).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry path'); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(undefined); + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - when(fileSystem.fileExists(anything())).thenResolve(true); - when(processServiceFactory.create(uri)).thenResolve(instance(processService)); - when(processService.exec('poetry path', anything(), anything())).thenResolve({ stderr: '', stdout: '' }); const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.equal(supported, true); + assert.strictEqual(supported, false); }); - test('Get Executable info', async () => { - const uri = Uri.file(__dirname); + + test('Is supported returns false if selected interpreter is not related to the workspace', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); - when(configurationService.getSettings(uri)).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry path'); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: path.join(project1, '.random', 'Scripts', 'python.exe'), + envType: EnvironmentType.Poetry, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - const info = await poetryInstaller.getExecutionInfo('something', uri); + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.deepEqual(info, { args: ['add', '--dev', 'something'], execPath: 'poetry path' }); + assert.strictEqual(supported, false); }); - test('Get executable info when installing black', async () => { - const uri = Uri.file(__dirname); + + test('Is supported returns false if selected interpreter is not of Poetry type', async () => { + const uri = Uri.file(project1); const settings = mock(PythonSettings); - when(configurationService.getSettings(uri)).thenReturn(instance(settings)); - when(settings.poetryPath).thenReturn('poetry path'); + when(interpreterService.getActiveInterpreter(anything())).thenResolve({ + path: path.join(project1, '.venv', 'Scripts', 'python.exe'), + envType: EnvironmentType.Pipenv, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + when(configurationService.getSettings(anything())).thenReturn(instance(settings)); + when(settings.poetryPath).thenReturn('poetry'); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ uri, name: '', index: 0 }); - const info = await poetryInstaller.getExecutionInfo('black', uri); + const supported = await poetryInstaller.isSupported(Uri.file(__filename)); - assert.deepEqual(info, { args: ['add', '--dev', 'black', '--allow-prereleases'], execPath: 'poetry path' }); + assert.strictEqual(supported, false); }); }); diff --git a/src/test/common/installer/productInstaller.unit.test.ts b/src/test/common/installer/productInstaller.unit.test.ts new file mode 100644 index 000000000000..2934d613f88f --- /dev/null +++ b/src/test/common/installer/productInstaller.unit.test.ts @@ -0,0 +1,80 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { DataScienceInstaller } from '../../../client/common/installer/productInstaller'; +import { IInstallationChannelManager, IModuleInstaller, InterpreterUri } from '../../../client/common/installer/types'; +import { InstallerResponse, Product } from '../../../client/common/types'; +import { Architecture } from '../../../client/common/utils/platform'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, ModuleInstallerType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; + +class AlwaysInstalledDataScienceInstaller extends DataScienceInstaller { + // eslint-disable-next-line @typescript-eslint/no-unused-vars, class-methods-use-this + public async isInstalled(_product: Product, _resource?: InterpreterUri): Promise<boolean> { + return true; + } +} + +suite('DataScienceInstaller install', async () => { + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let installationChannelManager: TypeMoq.IMock<IInstallationChannelManager>; + let dataScienceInstaller: DataScienceInstaller; + let appShell: TypeMoq.IMock<IApplicationShell>; + + const interpreterPath = 'path/to/interpreter'; + + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + installationChannelManager = TypeMoq.Mock.ofType<IInstallationChannelManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(undefined)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInstallationChannelManager))) + .returns(() => installationChannelManager.object); + + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); + + dataScienceInstaller = new AlwaysInstalledDataScienceInstaller(serviceContainer.object); + }); + + teardown(() => { + // noop + }); + + test('Will invoke pip for pytorch with conda environment', async () => { + // See https://github.com/microsoft/vscode-jupyter/issues/5034 + const testEnvironment: PythonEnvironment = { + envType: EnvironmentType.Conda, + envName: 'test', + envPath: interpreterPath, + path: interpreterPath, + architecture: Architecture.x64, + sysPrefix: '', + }; + const testInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + + testInstaller.setup((c) => c.type).returns(() => ModuleInstallerType.Pip); + testInstaller + .setup((c) => + c.installModule( + TypeMoq.It.isValue(Product.torchProfilerInstallName), + TypeMoq.It.isValue(testEnvironment), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve()); + + installationChannelManager + .setup((c) => c.getInstallationChannels(TypeMoq.It.isAny())) + .returns(() => Promise.resolve([testInstaller.object])); + + const result = await dataScienceInstaller.install(Product.torchProfilerInstallName, testEnvironment); + expect(result).to.equal(InstallerResponse.Installed, 'Should be Installed'); + }); +}); diff --git a/src/test/common/installer/productPath.unit.test.ts b/src/test/common/installer/productPath.unit.test.ts deleted file mode 100644 index 4ba0017a7cb4..000000000000 --- a/src/test/common/installer/productPath.unit.test.ts +++ /dev/null @@ -1,190 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length 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, ITestingSettings, IWorkspaceSymbolSettings, ModuleNamePurpose, Product, ProductType } from '../../../client/common/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { IFormatterHelper } from '../../../client/formatters/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ILinterInfo, ILinterManager } from '../../../client/linters/types'; -import { ITestsHelper } from '../../../client/testing/common/types'; - -use(chaiAsPromised); - -suite('Product Path', () => { - [undefined, Uri.file('resource')].forEach(resource => { - getNamesAndValues<Product>(Product).forEach(product => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let formattingSettings: TypeMoq.IMock<IFormattingSettings>; - let unitTestSettings: TypeMoq.IMock<ITestingSettings>; - let workspaceSymnbolSettings: TypeMoq.IMock<IWorkspaceSymbolSettings>; - let configService: TypeMoq.IMock<IConfigurationService>; - let productInstaller: ProductInstaller; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - formattingSettings = TypeMoq.Mock.ofType<IFormattingSettings>(); - unitTestSettings = TypeMoq.Mock.ofType<ITestingSettings>(); - workspaceSymnbolSettings = TypeMoq.Mock.ofType<IWorkspaceSymbolSettings>(); - - productInstaller = new ProductInstaller(serviceContainer.object, TypeMoq.Mock.ofType<OutputChannel>().object); - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.formatting).returns(() => formattingSettings.object); - pythonSettings.setup(p => p.testing).returns(() => unitTestSettings.object); - pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymnbolSettings.object); - configService.setup(s => s.getSettings(TypeMoq.It.isValue(resource))) - .returns(() => pythonSettings.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => productInstaller); - - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProductService), TypeMoq.It.isAny())).returns(() => new ProductService()); - }); - - if (product.value === Product.isort) { - return; - } - const productType = new ProductService().getProductType(product.value); - switch (productType) { - case ProductType.Formatter: { - test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const productPathService = new FormatterProductPathService(serviceContainer.object); - const formatterHelper = TypeMoq.Mock.ofType<IFormatterHelper>(); - const expectedPath = 'Some Path'; - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IFormatterHelper), TypeMoq.It.isAny())) - .returns(() => formatterHelper.object); - formattingSettings.setup(f => f.autopep8Path) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - formatterHelper.setup(f => f.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - pathName: 'autopep8Path', - argsName: 'autopep8Args' - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - formattingSettings.verifyAll(); - formatterHelper.verifyAll(); - }); - break; - } - case ProductType.Linter: { - test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const productPathService = new LinterProductPathService(serviceContainer.object); - const linterManager = TypeMoq.Mock.ofType<ILinterManager>(); - const linterInfo = TypeMoq.Mock.ofType<ILinterInfo>(); - const expectedPath = 'Some Path'; - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => linterManager.object); - linterInfo.setup(l => l.pathName(TypeMoq.It.isValue(resource))) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.once()); - linterManager.setup(l => l.getLinterInfo(TypeMoq.It.isValue(product.value))) - .returns(() => linterInfo.object) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - linterInfo.verifyAll(); - linterManager.verifyAll(); - }); - break; - } - case ProductType.RefactoringLibrary: { - test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const productPathService = new RefactoringLibraryProductPathService(serviceContainer.object); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - const moduleName = productInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.run); - expect(value).to.be.equal(moduleName); - }); - break; - } - case ProductType.WorkspaceSymbols: { - test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const productPathService = new CTagsProductPathService(serviceContainer.object); - const expectedPath = 'Some Path'; - workspaceSymnbolSettings.setup(w => w.ctagsPath) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - workspaceSymnbolSettings.verifyAll(); - }); - break; - } - case ProductType.TestFramework: { - test(`Ensure path is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType<ITestsHelper>(); - const expectedPath = 'Some Path'; - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper.setup(t => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: 'nosetestPath' - }; - }) - .verifiable(TypeMoq.Times.once()); - unitTestSettings.setup(u => u.nosetestPath) - .returns(() => expectedPath) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - expect(value).to.be.equal(expectedPath); - testHelper.verifyAll(); - unitTestSettings.verifyAll(); - }); - test(`Ensure module name is returned for ${product.name} (${resource ? 'With a resource' : 'without a resource'})`, async () => { - const productPathService = new TestFrameworkProductPathService(serviceContainer.object); - const testHelper = TypeMoq.Mock.ofType<ITestsHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestsHelper), TypeMoq.It.isAny())) - .returns(() => testHelper.object); - testHelper.setup(t => t.getSettingsPropertyNames(TypeMoq.It.isValue(product.value))) - .returns(() => { - return { - argsName: 'autoTestDiscoverOnSaveEnabled', - enabledName: 'autoTestDiscoverOnSaveEnabled', - pathName: undefined - }; - }) - .verifiable(TypeMoq.Times.once()); - - const value = productPathService.getExecutableNameFromSettings(product.value, resource); - const moduleName = productInstaller.translateProductToModuleName(product.value, ModuleNamePurpose.run); - expect(value).to.be.equal(moduleName); - testHelper.verifyAll(); - }); - break; - } - 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/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 c8e884fe2d0b..0cdb6f270c54 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -1,108 +1,176 @@ -// tslint:disable:max-func-body-length - -import { expect, should as chai_should, use as chai_use } from 'chai'; +import { expect, should as chaiShould, use as chaiUse } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; import { SemVer } from 'semver'; import { instance, mock } from 'ts-mockito'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Uri, 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 { 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 { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from '../../client/common/terminal/types'; -import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IPythonSettings, IsWindows } from '../../client/common/types'; +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 '../testing/serviceRegistry'; -import { closeActiveWindows, initializeTest } from './../initialize'; +import { closeActiveWindows, initializeTest } from '../initialize'; +import { createTypeMoq } from '../mocks/helper'; -chai_use(chaiAsPromised); +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 () => { - chai_should(); - 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); @@ -110,37 +178,95 @@ 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); + 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; + 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: '' }); @@ -152,31 +278,29 @@ 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); + 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; + 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: '' }); @@ -188,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'); @@ -227,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/fileDownloader.unit.test.ts b/src/test/common/net/fileDownloader.unit.test.ts deleted file mode 100644 index 08f7fd354645..000000000000 --- a/src/test/common/net/fileDownloader.unit.test.ts +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable: no-var-requires no-require-imports max-func-body-length no-any match-default-export-name -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as fsExtra from 'fs-extra'; -import * as nock from 'nock'; -import * as path from 'path'; -import rewiremock from 'rewiremock'; -import * as sinon from 'sinon'; -import { Readable, Writable } from 'stream'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Progress } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { IApplicationShell } from '../../../client/common/application/types'; -import { FileDownloader } from '../../../client/common/net/fileDownloader'; -import { HttpClient } from '../../../client/common/net/httpClient'; -import { FileSystem } from '../../../client/common/platform/fileSystem'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IHttpClient } from '../../../client/common/types'; -import { Http } from '../../../client/common/utils/localize'; -import { EXTENSION_ROOT_DIR } from '../../../client/constants'; -import { noop } from '../../core'; -import { MockOutputChannel } from '../../mockClasses'; -const requestProgress = require('request-progress'); -const request = require('request'); - -type ProgressReporterData = { message?: string; increment?: number }; - -/** - * Writable stream that'll throw an error when written to. - * (used to mimick errors thrown when writing to a file). - * - * @class ErroringMemoryStream - * @extends {Writable} - */ -class ErroringMemoryStream extends Writable { - constructor(private readonly errorMessage: string) { - super(); - } - public _write(_chunk: any, _encoding: any, callback: any) { - super.emit('error', new Error(this.errorMessage)); - return callback(); - } -} -/** - * Readable stream that's slow to return data. - * (used to mimic slow file downloads). - * - * @class DelayedReadMemoryStream - * @extends {Readable} - */ -class DelayedReadMemoryStream extends Readable { - public get readableLength() { - return 1024 * 10; - } - private readCounter = 0; - constructor(private readonly totalKb: number, - private readonly delayMs: number, - private readonly kbPerIteration: number) { - super(); - } - public _read() { - // Delay reading data, mimicking slow file downloads. - setTimeout(() => this.sendMesage(), this.delayMs); - } - public sendMesage() { - const i = this.readCounter += 1; - if (i > (this.totalKb / this.kbPerIteration)) { - this.push(null); - } else { - this.push(Buffer.from('a'.repeat(this.kbPerIteration), 'ascii')); - } - } -} - -suite('File Downloader', () => { - let fileDownloader: FileDownloader; - let httpClient: IHttpClient; - let fs: IFileSystem; - let appShell: IApplicationShell; - suiteTeardown(() => { - rewiremock.disable(); - sinon.restore(); - }); - suite('File Downloader (real)', () => { - const uri = 'https://python.extension/package.json'; - const packageJsonFile = path.join(EXTENSION_ROOT_DIR, 'package.json'); - setup(() => { - rewiremock.disable(); - httpClient = mock(HttpClient); - appShell = mock(ApplicationShell); - when(httpClient.downloadFile(anything())).thenCall(request); - fs = new FileSystem(new PlatformService()); - }); - teardown(() => { - rewiremock.disable(); - sinon.restore(); - }); - test('File gets downloaded', async () => { - // When downloading a uri, point it to package.json file. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => fsExtra.createReadStream(packageJsonFile)); - const progressReportStub = sinon.stub(); - const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; - const tmpFilePath = await fs.createTemporaryFile('.json'); - when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - await fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath); - - // Confirm the package.json file gets downloaded - const expectedFileContents = fsExtra.readFileSync(packageJsonFile).toString(); - assert.equal(fsExtra.readFileSync(tmpFilePath.filePath).toString(), expectedFileContents); - }); - test('Error is throw for http Status !== 200', async () => { - // When downloading a uri, throw status 500 error. - nock('https://python.extension') - .get('/package.json') - .reply(500); - const progressReportStub = sinon.stub(); - const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; - when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - const tmpFilePath = await fs.createTemporaryFile('.json'); - - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath); - - await expect(promise).to.eventually.be.rejectedWith('Failed with status 500, null, Uri https://python.extension/package.json'); - }); - test('Error is throw if unable to write to the file stream', async () => { - // When downloading a uri, point it to package.json file. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => fsExtra.createReadStream(packageJsonFile)); - const progressReportStub = sinon.stub(); - const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; - when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - - // Use bogus files that cannot be created (on windows, invalid drives, on mac & linux use invalid home directories). - const invalidFileName = new PlatformService().isWindows ? 'abcd:/bogusFile/one.txt' : '/bogus file path/.txt'; - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', invalidFileName); - - // Things should fall over. - await expect(promise).to.eventually.be.rejected; - }); - test('Error is throw if file stream throws an error', async () => { - // When downloading a uri, point it to package.json file. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => fsExtra.createReadStream(packageJsonFile)); - const progressReportStub = sinon.stub(); - const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; - when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - // Create a file stream that will throw an error when written to (use ErroringMemoryStream). - const tmpFilePath = 'bogus file'; - const fileSystem = mock(FileSystem); - const fileStream = new ErroringMemoryStream('kaboom from fs'); - when(fileSystem.createWriteStream(tmpFilePath)).thenReturn(fileStream as any); - - fileDownloader = new FileDownloader(instance(httpClient), instance(fileSystem), instance(appShell)); - const promise = fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath); - - // Confirm error from FS is bubbled up. - await expect(promise).to.eventually.be.rejectedWith('kaboom from fs'); - }); - test('Report progress as file gets downloaded', async () => { - const totalKb = 50; - // When downloading a uri, point it to stream that's slow. - // We'll return data from this stream slowly, mimicking a slow download. - // When the download is slow, we can test progress. - nock('https://python.extension') - .get('/package.json') - .reply(200, () => [200, new DelayedReadMemoryStream(1024 * totalKb, 5, 1024 * 10), { 'content-length': 1024 * totalKb }]); - const progressReportStub = sinon.stub(); - const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub }; - when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter)); - const tmpFilePath = await fs.createTemporaryFile('.json'); - // Mock request-progress to throttle 1ms, so we can get progress messages. - // I.e. report progress every 1ms. (however since download is delayed to 10ms, - // we'll get progress reported every 10ms. We use 1ms, to ensure its guaranteed - // to be reported. Else changing it to 10ms could result in it being reported in 12ms - rewiremock.enable(); - rewiremock('request-progress').with((reqUri: string) => requestProgress(reqUri, { throttle: 1 })); - - fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell)); - await fileDownloader.downloadFileWithStatusBarProgress(uri, 'Downloading-something', tmpFilePath.filePath); - - // Since we are throttling the progress notifications for ever 1ms, - // and we're delaying downloading by every 10ms, we'll have progress reported for every 10ms. - // So we'll have progress reported for every 10kb of data downloaded, for a total of 5 times. - expect(progressReportStub.callCount).to.equal(5); - expect(progressReportStub.args[0][0].message).to.equal(getProgressMessage(10, 20)); - expect(progressReportStub.args[1][0].message).to.equal(getProgressMessage(20, 40)); - expect(progressReportStub.args[2][0].message).to.equal(getProgressMessage(30, 60)); - expect(progressReportStub.args[3][0].message).to.equal(getProgressMessage(40, 80)); - expect(progressReportStub.args[4][0].message).to.equal(getProgressMessage(50, 100)); - - function getProgressMessage(downloadedKb: number, percentage: number) { - return Http.downloadingFileProgress().format('Downloading-something', - downloadedKb.toFixed(), totalKb.toFixed(), percentage.toString()); - } - }); - }); - suite('File Downloader (mocks)', () => { - let downloadWithProgressStub: sinon.SinonStub<any>; - setup(() => { - httpClient = mock(HttpClient); - fs = mock(FileSystem); - appShell = mock(ApplicationShell); - downloadWithProgressStub = sinon.stub(FileDownloader.prototype, 'displayDownloadProgress'); - downloadWithProgressStub.callsFake(() => Promise.resolve()); - }); - teardown(() => { - sinon.restore(); - }); - test('Create temporary file and return path to that file', async () => { - const tmpFile = { filePath: 'my temp file', dispose: noop }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - const file = await fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); - - verify(fs.createTemporaryFile('.pdf')).once(); - assert.equal(file, 'my temp file'); - }); - test('Display progress message in output channel', async () => { - const outputChannel = mock(MockOutputChannel); - const tmpFile = { filePath: 'my temp file', dispose: noop }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - await fileDownloader.downloadFile('file to download', { progressMessagePrefix: '', extension: '.pdf', outputChannel: outputChannel }); - - verify(outputChannel.append(Http.downloadingFile().format('file to download'))); - }); - test('Display progress when downloading', async () => { - const tmpFile = { filePath: 'my temp file', dispose: noop }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - const statusBarProgressStub = sinon.stub(FileDownloader.prototype, 'downloadFileWithStatusBarProgress'); - statusBarProgressStub.callsFake(() => Promise.resolve()); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - await fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); - - assert.ok(statusBarProgressStub.calledOnce); - }); - test('Dispose temp file and bubble error thrown by status progress', async () => { - const disposeStub = sinon.stub(); - const tmpFile = { filePath: 'my temp file', dispose: disposeStub }; - when(fs.createTemporaryFile('.pdf')).thenResolve(tmpFile); - const statusBarProgressStub = sinon.stub(FileDownloader.prototype, 'downloadFileWithStatusBarProgress'); - statusBarProgressStub.callsFake(() => Promise.reject(new Error('kaboom'))); - fileDownloader = new FileDownloader(instance(httpClient), instance(fs), instance(appShell)); - - const promise = fileDownloader.downloadFile('file', { progressMessagePrefix: '', extension: '.pdf' }); - - await expect(promise).to.eventually.be.rejectedWith('kaboom'); - assert.ok(statusBarProgressStub.calledOnce); - assert.ok(disposeStub.calledOnce); - }); - }); -}); diff --git a/src/test/common/net/httpClient.unit.test.ts b/src/test/common/net/httpClient.unit.test.ts deleted file mode 100644 index e93314bd7196..000000000000 --- a/src/test/common/net/httpClient.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-any - -import * as assert from 'assert'; -import { expect } from 'chai'; -// tslint:disable-next-line: match-default-export-name -import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { HttpClient } from '../../../client/common/net/httpClient'; -import { IServiceContainer } from '../../../client/ioc/types'; - -// tslint:disable-next-line: max-func-body-length -suite('Http Client', () => { - const proxy = 'https://myproxy.net:4242'; - let config: TypeMoq.IMock<WorkspaceConfiguration>; - let workSpaceService: TypeMoq.IMock<IWorkspaceService>; - let httpClient: HttpClient; - setup(() => { - const container = TypeMoq.Mock.ofType<IServiceContainer>(); - workSpaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - config - .setup(c => c.get(TypeMoq.It.isValue('proxy'), TypeMoq.It.isValue(''))) - .returns(() => proxy) - .verifiable(TypeMoq.Times.once()); - workSpaceService - .setup(w => w.getConfiguration(TypeMoq.It.isValue('http'))) - .returns(() => config.object) - .verifiable(TypeMoq.Times.once()); - container.setup(a => a.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workSpaceService.object); - - httpClient = new HttpClient(container.object); - }); - test('Get proxy info', async () => { - expect(httpClient.requestOptions).to.deep.equal({ proxy: proxy }); - config.verifyAll(); - workSpaceService.verifyAll(); - }); - suite('Test getJSON()', async () => { - teardown(() => { - rewiremock.disable(); - }); - [ - { - name: 'Throw error if request returns with download error', - returnedArgs: ['downloadError', { statusCode: 201 }, undefined], - expectedErrorMessage: 'downloadError' - }, - { - name: 'Throw error if request does not return with status code 200', - returnedArgs: [undefined, { statusCode: 201, statusMessage: 'wrongStatus' }, undefined], - expectedErrorMessage: 'Failed with status 201, wrongStatus, Uri downloadUri' - }, - { - name: 'If strict is set to true, and parsing fails, throw error', - returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : true,, }]'], - strict: true - } - ].forEach(async testParams => { - test(testParams.name, async () => { - const requestMock = (_uri: any, _requestOptions: any, callBackFn: Function) => callBackFn(...testParams.returnedArgs); - rewiremock.enable(); - rewiremock('request').with(requestMock); - let rejected = true; - try { - await httpClient.getJSON('downloadUri', testParams.strict); - rejected = false; - } catch (ex) { - if (testParams.expectedErrorMessage) { - // Compare error messages - if (ex.message) { - ex = ex.message; - } - expect(ex).to.equal(testParams.expectedErrorMessage, 'Promise rejected with the wrong error message'); - } - } - assert(rejected === true, 'Promise should be rejected'); - }); - }); - - [ - { - name: 'If strict is set to false, and jsonc parsing returns error codes, then log errors and don\'t throw, return json', - returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : false,, }]'], - strict: false, - expectedJSON: [{ strictJSON: false }] - }, - { - name: 'Return expected json if strict is set to true and parsing is successful', - returnedArgs: [undefined, { statusCode: 200 }, '[{ "strictJSON" : true }]'], - strict: true, - expectedJSON: [{ strictJSON: true }] - }, - { - name: 'Return expected json if strict is set to false and parsing is successful', - returnedArgs: [undefined, { statusCode: 200 }, '[{ //Comment \n "strictJSON" : false }]'], - strict: false, - expectedJSON: [{ strictJSON: false }] - } - ].forEach(async testParams => { - test(testParams.name, async () => { - const requestMock = (_uri: any, _requestOptions: any, callBackFn: Function) => callBackFn(...testParams.returnedArgs); - rewiremock.enable(); - rewiremock('request').with(requestMock); - let json; - try { - json = await httpClient.getJSON('downloadUri', testParams.strict); - } catch (ex) { - assert(false, 'Promise should not be rejected'); - } - assert.deepEqual(json, testParams.expectedJSON, 'Unexpected JSON returned'); - }); - }); - }); -}); diff --git a/src/test/common/nuget/azureBobStoreRepository.functional.test.ts b/src/test/common/nuget/azureBobStoreRepository.functional.test.ts deleted file mode 100644 index f076c85749b8..000000000000 --- a/src/test/common/nuget/azureBobStoreRepository.functional.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { SemVer } from 'semver'; -import * as typeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { LanguageServerPackageStorageContainers } from '../../../client/activation/languageServer/languageServerPackageRepository'; -import { LanguageServerPackageService } from '../../../client/activation/languageServer/languageServerPackageService'; -import { IApplicationEnvironment, IWorkspaceService } from '../../../client/common/application/types'; -import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository'; -import { INugetService } from '../../../client/common/nuget/types'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { IHttpClient } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -const azureBlobStorageAccount = 'https://pvsc.blob.core.windows.net'; -const azureCDNBlobStorageAccount = 'https://pvsc.azureedge.net'; - -suite('Nuget Azure Storage Repository', () => { - let serviceContainer: typeMoq.IMock<IServiceContainer>; - let httpClient: typeMoq.IMock<IHttpClient>; - let workspace: typeMoq.IMock<IWorkspaceService>; - let cfg: typeMoq.IMock<WorkspaceConfiguration>; - let repo: AzureBlobStoreNugetRepository; - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - httpClient = typeMoq.Mock.ofType<IHttpClient>(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IHttpClient))).returns(() => httpClient.object); - cfg = typeMoq.Mock.ofType<WorkspaceConfiguration>(); - cfg.setup(c => c.get('proxyStrictSSL', true)) - .returns(() => true); - workspace = typeMoq.Mock.ofType<IWorkspaceService>(); - workspace.setup(w => w.getConfiguration('http', undefined)) - .returns(() => cfg.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspace.object); - - const nugetService = typeMoq.Mock.ofType<INugetService>(); - nugetService.setup(n => n.getVersionFromPackageFileName(typeMoq.It.isAny())).returns(() => new SemVer('1.1.1')); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetService))).returns(() => nugetService.object); - const defaultStorageChannel = 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, undefined); - - expect(packages).to.be.length.greaterThan(0); - }); -}); diff --git a/src/test/common/nuget/azureBobStoreRepository.unit.test.ts b/src/test/common/nuget/azureBobStoreRepository.unit.test.ts deleted file mode 100644 index ac06b125485d..000000000000 --- a/src/test/common/nuget/azureBobStoreRepository.unit.test.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-http-string - -import { BlobService, ErrorOrResult } from 'azure-storage'; -import { expect } from 'chai'; -import { SemVer } from 'semver'; -import * as typeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { AzureBlobStoreNugetRepository } from '../../../client/common/nuget/azureBlobStoreNugetRepository'; -import { INugetService } from '../../../client/common/nuget/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Nuget Azure Storage Repository', () => { - const packageName = 'Python-Language-Server-???'; - - let serviceContainer: typeMoq.IMock<IServiceContainer>; - let workspace: typeMoq.IMock<IWorkspaceService>; - let nugetService: typeMoq.IMock<INugetService>; - let cfg: typeMoq.IMock<WorkspaceConfiguration>; - - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(undefined, typeMoq.MockBehavior.Strict); - workspace = typeMoq.Mock.ofType<IWorkspaceService>(undefined, typeMoq.MockBehavior.Strict); - nugetService = typeMoq.Mock.ofType<INugetService>(undefined, typeMoq.MockBehavior.Strict); - cfg = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); - - serviceContainer.setup(c => c.get(typeMoq.It.isValue(INugetService))) - .returns(() => nugetService.object); - }); - - class FakeBlobStore { - // tslint:disable-next-line:no-any - public calls: [string, string, any][] = []; - public results?: BlobService.BlobResult[]; - public error?: Error; - public contructor() { - this.calls = []; - } - // tslint:disable-next-line:no-any - public listBlobsSegmentedWithPrefix(c: string, p: string, t: any, cb: ErrorOrResult<BlobService.ListBlobsResult>) { - this.calls.push([c, p, t]); - const result: BlobService.ListBlobsResult = { entries: this.results! }; - // tslint:disable-next-line:no-any - cb(this.error as Error, result, undefined as any); - } - } - - const tests: [string, boolean, string][] = [ - ['https://az', true, 'https://az'], - ['https://az', false, 'http://az'], - ['http://az', true, 'http://az'], - ['http://az', false, 'http://az'] - ]; - for (const [uri, setting, expected] of tests) { - test(`Get all packages ("${uri}" / ${setting})`, async () => { - if (uri.startsWith('https://')) { - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspace.object); - workspace.setup(w => w.getConfiguration('http', undefined)) - .returns(() => cfg.object); - cfg.setup(c => c.get('proxyStrictSSL', true)) - .returns(() => setting); - } - const blobstore = new FakeBlobStore(); - // tslint:disable:no-object-literal-type-assertion - blobstore.results = [ - { name: 'Azarath' } as BlobService.BlobResult, - { name: 'Metrion' } as BlobService.BlobResult, - { name: 'Zinthos' } as BlobService.BlobResult - ]; - // tslint:enable:no-object-literal-type-assertion - const version = new SemVer('1.1.1'); - blobstore.results.forEach(r => { - nugetService.setup(n => n.getVersionFromPackageFileName(r.name)) - .returns(() => version); - }); - let actualURI = ''; - const repo = new AzureBlobStoreNugetRepository( - serviceContainer.object, - uri, - 'spam', - 'eggs', - async (uriArg) => { - actualURI = uriArg; - return blobstore; - } - ); - - const packages = await repo.getPackages(packageName, undefined); - - expect(packages).to.deep.equal([ - { package: 'Azarath', uri: 'eggs/spam/Azarath', version: version }, - { package: 'Metrion', uri: 'eggs/spam/Metrion', version: version }, - { package: 'Zinthos', uri: 'eggs/spam/Zinthos', version: version } - ]); - expect(actualURI).to.equal(expected); - expect(blobstore.calls).to.deep.equal([ - ['spam', packageName, undefined] - ], 'failed'); - serviceContainer.verifyAll(); - workspace.verifyAll(); - cfg.verifyAll(); - }); - } -}); diff --git a/src/test/common/nuget/nugetRepository.unit.test.ts b/src/test/common/nuget/nugetRepository.unit.test.ts deleted file mode 100644 index 6e9214293771..000000000000 --- a/src/test/common/nuget/nugetRepository.unit.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { SemVer } from 'semver'; -import * as typeMoq from 'typemoq'; -import { NugetRepository } from '../../../client/common/nuget/nugetRepository'; -import { IHttpClient } from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Nuget on Nuget Repo', () => { - let serviceContainer: typeMoq.IMock<IServiceContainer>; - let httpClient: typeMoq.IMock<IHttpClient>; - let nugetRepo: NugetRepository; - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - httpClient = typeMoq.Mock.ofType<IHttpClient>(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IHttpClient))).returns(() => httpClient.object); - - nugetRepo = new NugetRepository(serviceContainer.object); - }); - - test('Get all package versions', async () => { - const packageBaseAddress = 'a'; - const packageName = 'b'; - const resp = { versions: ['1.1.1', '1.2.1'] }; - const expectedUri = `${packageBaseAddress}/${packageName.toLowerCase().trim()}/index.json`; - - httpClient - .setup(h => h.getJSON(typeMoq.It.isValue(expectedUri))) - .returns(() => Promise.resolve(resp)) - .verifiable(typeMoq.Times.once()); - - const versions = await nugetRepo.getVersions(packageBaseAddress, packageName); - - httpClient.verifyAll(); - expect(versions).to.be.lengthOf(2); - expect(versions.map(item => item.raw)).to.deep.equal(resp.versions); - }); - - test('Get package uri', async () => { - const packageBaseAddress = 'a'; - const packageName = 'b'; - const version = '1.1.3'; - const expectedUri = `${packageBaseAddress}/${packageName}/${version}/${packageName}.${version}.nupkg`; - - const packageUri = nugetRepo.getNugetPackageUri(packageBaseAddress, packageName, new SemVer(version)); - - httpClient.verifyAll(); - expect(packageUri).to.equal(expectedUri); - }); - - test('Get packages', async () => { - const versions = ['1.1.1', '1.2.1', '2.2.2', '2.5.4', '2.9.5-release', '2.7.4-beta', '2.0.2', '3.5.4']; - nugetRepo.getVersions = () => Promise.resolve(versions.map(v => new SemVer(v))); - nugetRepo.getNugetPackageUri = () => 'uri'; - - const packages = await nugetRepo.getPackages('packageName'); - - expect(packages).to.be.lengthOf(versions.length); - expect(packages.map(item => item.version.raw)).to.be.deep.equal(versions); - expect(packages.map(item => item.uri)).to.be.deep.equal(versions.map(() => 'uri')); - expect(packages.map(item => item.package)).to.be.deep.equal(versions.map(() => 'packageName')); - }); -}); diff --git a/src/test/common/nuget/nugetService.unit.test.ts b/src/test/common/nuget/nugetService.unit.test.ts deleted file mode 100644 index 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 04fd6f62a01a..f012cb9fb27e 100644 --- a/src/test/common/platform/filesystem.unit.test.ts +++ b/src/test/common/platform/filesystem.unit.test.ts @@ -1,119 +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 searchPattern = `${path.basename(__filename, __filename.substring(__filename.length - 3))}.*`; - const files = await fileSystem.search(path.join(__dirname, searchPattern)); - 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: Error) => { - 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 5a8c379aa0d8..000000000000 --- a/src/test/common/platform/platformService.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as os from 'os'; -import { parse } from 'semver'; -import { PlatformService } from '../../../client/common/platform/platformService'; -import { OSType } from '../../../client/common/utils/platform'; - -use(chaiAsPromised); - -// tslint:disable-next-line:max-func-body-length -suite('PlatformService', () => { - const osType = getOSType(); - test('pathVariableName', async () => { - const expected = osType === OSType.Windows ? 'Path' : 'PATH'; - const svc = new PlatformService(); - const result = svc.pathVariableName; - - expect(result).to.be.equal(expected, 'invalid value'); - }); - - test('virtualEnvBinName - Windows', async () => { - const expected = osType === OSType.Windows ? 'Scripts' : 'bin'; - const svc = new PlatformService(); - const result = svc.virtualEnvBinName; - - expect(result).to.be.equal(expected, 'invalid value'); - }); - - test('isWindows', async () => { - const expected = osType === OSType.Windows; - const svc = new PlatformService(); - const result = svc.isWindows; - - expect(result).to.be.equal(expected, 'invalid value'); - }); - - test('isMac', async () => { - const expected = osType === OSType.OSX; - const svc = new PlatformService(); - const result = svc.isMac; - - expect(result).to.be.equal(expected, 'invalid value'); - }); - - test('isLinux', async () => { - const expected = osType === OSType.Linux; - const svc = new PlatformService(); - const result = svc.isLinux; - - expect(result).to.be.equal(expected, 'invalid value'); - }); - - test('osRelease', async () => { - const expected = os.release(); - const svc = new PlatformService(); - const result = svc.osRelease; - - expect(result).to.be.equal(expected, 'invalid value'); - }); - - test('is64bit', async () => { - // tslint:disable-next-line:no-require-imports - const arch = require('arch'); - - const hostReports64Bit = arch() === 'x64'; - const svc = new PlatformService(); - const result = svc.is64bit; - - expect(result).to.be.equal(hostReports64Bit, `arch() reports '${arch()}', PlatformService.is64bit reports ${result}.`); - }); - - test('getVersion on Mac/Windows', async function () { - if (osType === OSType.Linux) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - const expectedVersion = parse(os.release())!; - const svc = new PlatformService(); - const result = await svc.getVersion(); - - expect(result.compare(expectedVersion)).to.be.equal(0, 'invalid value'); - }); - test('getVersion on Linux shoud throw an exception', async function () { - if (osType !== OSType.Linux) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - const svc = new PlatformService(); - - await expect(svc.getVersion()).to.eventually.be.rejectedWith('Not Supported'); - }); -}); - -function getOSType(platform: string = process.platform): OSType { - if (/^win/.test(platform)) { - return OSType.Windows; - } else if (/^darwin/.test(platform)) { - return OSType.OSX; - } else if (/^linux/.test(platform)) { - return OSType.Linux; - } else { - return OSType.Unknown; - } -} diff --git a/src/test/common/platform/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 f7c1c525a161..000000000000 --- a/src/test/common/process/execFactory.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -// tslint:disable:max-func-body-length no-any - -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Uri } from 'vscode'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; -import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; -import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { InterpreterVersionService } from '../../../client/interpreter/interpreterVersion'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('PythonExecutableService', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let configService: TypeMoq.IMock<IConfigurationService>; - let procService: TypeMoq.IMock<IProcessService>; - let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const envVarsProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); - procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - procService = TypeMoq.Mock.ofType<IProcessService>(); - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - const fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - fileSystem.setup(f => f.fileExists(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => procServiceFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configService.object); - procService.setup((x: any) => x.then).returns(() => undefined); - procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(procService.object)); - envVarsProvider.setup(v => v.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - - const envActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(); - envActivationService.setup(e => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IEnvironmentActivationService), TypeMoq.It.isAny())) - .returns(() => envActivationService.object); - }); - test('Ensure resource is used when getting configuration service settings (undefined resource)', async () => { - const pythonPath = `Python_Path_${new Date().toString()}`; - const pythonVersion = `Python_Version_${new Date().toString()}`; - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => pythonSettings.object); - procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); - - const versionService = new InterpreterVersionService(procServiceFactory.object); - const version = await versionService.getVersion(pythonPath, ''); - - expect(version).to.be.equal(pythonVersion); - }); - test('Ensure resource is used when getting configuration service settings (defined resource)', async () => { - const resource = Uri.file('abc'); - const pythonPath = `Python_Path_${new Date().toString()}`; - const pythonVersion = `Python_Version_${new Date().toString()}`; - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => pythonPath); - configService.setup(c => c.getSettings(TypeMoq.It.isValue(resource))).returns(() => pythonSettings.object); - procService.setup(p => p.exec(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: pythonVersion })); - - const versionService = new InterpreterVersionService(procServiceFactory.object); - const version = await versionService.getVersion(pythonPath, ''); - - expect(version).to.be.equal(pythonVersion); - }); -}); diff --git a/src/test/common/process/logger.unit.test.ts b/src/test/common/process/logger.unit.test.ts new file mode 100644 index 000000000000..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 b6f597ed054e..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, isOs, OSType } from '../../common'; +import { isOs, OSType } from '../../common'; +import { getExtensionSettings } from '../../extensionSettings'; import { initialize } from './../../initialize'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// tslint:disable-next-line:max-func-body-length suite('ProcessService', () => { let pythonPath: string; suiteSetup(() => { @@ -23,202 +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); - }); + result.out.subscribe( + (output) => { + const value = output.out.trim(); + // Ignore line breaks. + if (value.length === 0) { + return; + } + const expectedOutput = outputs.shift()!; + expect(value).to.be.equal(expectedOutput.out, 'Expected output is incorrect'); + expect(output.source).to.be.equal(expectedOutput.source, 'Expected sopurce is incorrect'); + }, + done, + done, + ); + }); test('execObservable should send stdout and stderr streams separately', async function () { // This test is failing on Windows. Tracked by GH #4755. if (isOs(OSType.Windows)) { - // tslint:disable-next-line:no-invalid-this return this.skip(); } }); test('execObservable should throw an error with stderr output', (done) => { - const procService = new ProcessService(new BufferDecoder()); + const 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 index 449d67257aa6..0981c59e78bb 100644 --- a/src/test/common/process/pythonExecutionFactory.unit.test.ts +++ b/src/test/common/process/pythonExecutionFactory.unit.test.ts @@ -1,48 +1,62 @@ // 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 { anything, instance, mock, verify, when } from 'ts-mockito'; +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 { BufferDecoder } from '../../../client/common/process/decoder'; -import { ProcessService } from '../../../client/common/process/proc'; +import { ProcessLogger } from '../../../client/common/process/logger'; import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; -import { PythonExecutionService } from '../../../client/common/process/pythonProcess'; import { - ExecutionFactoryCreationOptions, - IBufferDecoder, + IProcessLogger, + IProcessService, IProcessServiceFactory, - IPythonExecutionService + IPythonExecutionService, } from '../../../client/common/process/types'; -import { IConfigurationService, IDisposableRegistry } from '../../../client/common/types'; +import { IConfigurationService, IDisposableRegistry, IInterpreterPathService } from '../../../client/common/types'; import { Architecture } from '../../../client/common/utils/platform'; import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +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'; -// tslint:disable:no-any max-func-body-length - -const pythonInterpreter: PythonInterpreter = { +const pythonInterpreter: PythonEnvironment = { path: '/foo/bar/python.exe', version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; -function title(resource?: Uri, interpreter?: PythonInterpreter) { +function title(resource?: Uri, interpreter?: PythonEnvironment) { return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; } -async function verifyCreateActivated(factory: PythonExecutionFactory, activationHelper: IEnvironmentActivationService, resource?: Uri, interpreter?: PythonInterpreter): Promise<IPythonExecutionService> { +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 }); @@ -57,86 +71,352 @@ suite('Process - PythonExecutionFactory', () => { { resource: undefined, interpreter: undefined }, { resource: undefined, interpreter: pythonInterpreter }, { resource: Uri.parse('x'), interpreter: undefined }, - { resource: Uri.parse('x'), interpreter: pythonInterpreter } - ].forEach(item => { - const resource = item.resource; - const interpreter = item.interpreter; + { 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 bufferDecoder: IBufferDecoder; - let procecssFactory: IProcessServiceFactory; + 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(() => { - bufferDecoder = mock(BufferDecoder); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + sinon.stub(Conda.prototype, 'getInterpreterPathForEnvironment').resolves(pythonPath); + + getPixiEnvironmentFromInterpreterStub = sinon.stub(pixi, 'getPixiEnvironmentFromInterpreter'); + getPixiEnvironmentFromInterpreterStub.resolves(undefined); + + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + activationHelper = mock(EnvironmentActivationService); - procecssFactory = mock(ProcessServiceFactory); + 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([]); - factory = new PythonExecutionFactory(instance(serviceContainer), - instance(activationHelper), instance(procecssFactory), - instance(configService), instance(bufferDecoder)); + 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(procecssFactory.create(resource)).thenResolve(instance(mock(ProcessService))); + 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 }); - verify(procecssFactory.create(resource)).once(); + expect(service).to.not.equal(undefined); + verify(processFactory.create(resource)).once(); verify(pythonSettings.pythonPath).once(); - expect(service).instanceOf(PythonExecutionService); }); + + 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 (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; - return Promise.resolve(mockExecService as any as IPythonExecutionService); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Promise.resolve((mockExecService as any) as IPythonExecutionService); }; const service = await verifyCreateActivated(factory, activationHelper, resource, interpreter); assert.deepEqual(service, mockExecService); - assert.equal(createInvoked, true); + assert.strictEqual(createInvoked, true); }); test('Ensure we use an existing `create` method if there are no environment variables (0 length) for the activated env', async () => { + const pythonSettings = mock(PythonSettings); + + when(processFactory.create(resource)).thenResolve(processService.object); + when(pythonSettings.pythonPath).thenReturn(pythonPath); + when(configService.getSettings(resource)).thenReturn(instance(pythonSettings)); + let createInvoked = false; const mockExecService = 'something'; - factory.create = async (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; - return Promise.resolve(mockExecService as any as IPythonExecutionService); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return Promise.resolve((mockExecService as any) as IPythonExecutionService); }; const service = await verifyCreateActivated(factory, activationHelper, resource, interpreter); assert.deepEqual(service, mockExecService); - assert.equal(createInvoked, true); + assert.strictEqual(createInvoked, true); }); test('PythonExecutionService is created', async () => { let createInvoked = false; const mockExecService = 'something'; - factory.create = async (_options: ExecutionFactoryCreationOptions) => { + factory.create = async () => { createInvoked = true; - return Promise.resolve(mockExecService as any as IPythonExecutionService); + // 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(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(); } - expect(service).instanceOf(PythonExecutionService); - assert.equal(createInvoked, false); + 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 4b7a93fbc79b..fc4fbf5328a9 100644 --- a/src/test/common/process/pythonProc.simple.multiroot.test.ts +++ b/src/test/common/process/pythonProc.simple.multiroot.test.ts @@ -6,94 +6,39 @@ 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 { anything, instance, mock, when } from 'ts-mockito'; -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 { IConfigurationService } from '../../../client/common/types'; import { clearCache } from '../../../client/common/utils/cacheUtils'; -import { OSType } from '../../../client/common/utils/platform'; -import { - registerTypes as variablesRegisterTypes -} from '../../../client/common/variables/serviceRegistry'; -import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../../client/interpreter/autoSelection/types'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { ServiceManager } from '../../../client/ioc/serviceManager'; 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); - - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - serviceManager.addSingletonInstance<IEnvironmentActivationService>(IEnvironmentActivationService, instance(mockEnvironmentActivationService)); - - 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); @@ -102,8 +47,6 @@ suite('PythonExecutableService', () => { }); suiteTeardown(closeActiveWindows); teardown(async () => { - cont.unbindAll(); - cont.unload(); await closeActiveWindows(); await clearPythonPathInWorkspaceFolder(workspace4Path); await configService.updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); @@ -112,51 +55,58 @@ suite('PythonExecutableService', () => { }); 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()); }); @@ -165,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 index 4e6108ee93d6..9a82681625d4 100644 --- a/src/test/common/serviceRegistry.unit.test.ts +++ b/src/test/common/serviceRegistry.unit.test.ts @@ -3,10 +3,9 @@ 'use strict'; -// tslint:disable: no-any - 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'; @@ -15,21 +14,23 @@ 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 { IApplicationEnvironment, IApplicationShell, ICommandManager, IDebugService, IDocumentManager, ILanguageService, ILiveShareApi, ITerminalManager, IWorkspaceService } from '../../client/common/application/types'; +import { + IActiveResourceService, + IApplicationEnvironment, + IApplicationShell, + ICommandManager, + IDebugService, + IDocumentManager, + ILanguageService, + ITerminalManager, + IWorkspaceService, +} from '../../client/common/application/types'; import { WorkspaceService } from '../../client/common/application/workspace'; -import { AsyncDisposableRegistry } from '../../client/common/asyncDisposableRegistry'; import { ConfigurationService } from '../../client/common/configuration/service'; -import { CryptoUtils } from '../../client/common/crypto'; -import { EditorUtils } from '../../client/common/editor'; -import { ExperimentsManager } from '../../client/common/experiments'; -import { FeatureDeprecationManager } from '../../client/common/featureDeprecationManager'; +import { PipEnvExecutionPath } from '../../client/common/configuration/executionSettings/pipEnvExecution'; import { ProductInstaller } from '../../client/common/installer/productInstaller'; -import { LiveShareApi } from '../../client/common/liveshare/liveshare'; -import { Logger } from '../../client/common/logger'; +import { InterpreterPathService } from '../../client/common/interpreterPathService'; import { BrowserService } from '../../client/common/net/browser'; -import { HttpClient } from '../../client/common/net/httpClient'; -import { NugetService } from '../../client/common/nuget/nugetService'; -import { INugetService } from '../../client/common/nuget/types'; import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; @@ -38,13 +39,38 @@ 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 { ITerminalActivationCommandProvider, ITerminalActivationHandler, ITerminalActivator, ITerminalHelper, ITerminalServiceFactory, TerminalActivationProviders } from '../../client/common/terminal/types'; -import { IAsyncDisposableRegistry, IBrowserService, IConfigurationService, ICryptoUtils, ICurrentProcess, IEditorUtils, IExperimentsManager, IExtensions, IFeatureDeprecationManager, IHttpClient, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IRandom } from '../../client/common/types'; +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'; @@ -56,10 +82,11 @@ suite('Common - Service Registry', () => { const serviceManager = typemoq.Mock.ofType<IServiceManager>(); [ + [IActiveResourceService, ActiveResourceService], + [IInterpreterPathService, InterpreterPathService], [IExtensions, Extensions], [IRandom, Random], [IPersistentStateFactory, PersistentStateFactory], - [ILogger, Logger], [ITerminalServiceFactory, TerminalServiceFactory], [IPathUtils, PathUtils], [IApplicationShell, ApplicationShell], @@ -74,33 +101,45 @@ suite('Common - Service Registry', () => { [IApplicationEnvironment, ApplicationEnvironment], [ILanguageService, LanguageService], [IBrowserService, BrowserService], - [IHttpClient, HttpClient], - [IEditorUtils, EditorUtils], - [INugetService, NugetService], [ITerminalActivator, TerminalActivator], [ITerminalActivationHandler, PowershellTerminalActivationFailedHandler], - [ILiveShareApi, LiveShareApi], - [ICryptoUtils, CryptoUtils], - [IExperimentsManager, ExperimentsManager], [ITerminalHelper, TerminalHelper], [ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv], [ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish], - [ITerminalActivationCommandProvider, CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell], + [ + ITerminalActivationCommandProvider, + CommandPromptAndPowerShell, + TerminalActivationProviders.commandPromptAndPowerShell, + ], + [ITerminalActivationCommandProvider, Nushell, TerminalActivationProviders.nushell], + [IToolExecutionPath, PipEnvExecutionPath, ToolExecutionPath.pipenv], [ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda], [ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv], - [IFeatureDeprecationManager, FeatureDeprecationManager], - [IAsyncDisposableRegistry, AsyncDisposableRegistry], [IMultiStepInputFactory, MultiStepInputFactory], - [IImportTracker, ImportTracker] - ].forEach(mapping => { + [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.isAny())) - .callback((_, cls) => expect(cls).to.equal(mapping[1])) - .verifiable(typemoq.Times.once()); + .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))) + .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()); } diff --git a/src/test/common/socketCallbackHandler.test.ts b/src/test/common/socketCallbackHandler.test.ts index d299daa24776..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 { @@ -102,7 +102,7 @@ class MockSocketClient { if (this.socket === undefined || this.def === undefined) { throw Error('not started'); } - this.socketStream = new SocketStream(this.socket, new Buffer('')); + this.socketStream = new SocketStream(this.socket, Buffer.from('')); this.def.resolve(); this.socket.on('error', () => {}); this.socket.on('data', (data: Buffer) => { @@ -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); } }); @@ -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; @@ -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 03692cfe6e41..35420e4a614c 100644 --- a/src/test/common/socketStream.test.ts +++ b/src/test/common/socketStream.test.ts @@ -11,12 +11,12 @@ import * as assert from 'assert'; // as well as import your extension to test it import * as net from 'net'; import { SocketStream } from '../../client/common/net/socket/SocketStream'; -// tslint:disable:no-require-imports no-var-requires + const uint64be = require('uint64be'); class MockSocket { private _data: string; - // tslint:disable-next-line:no-any + private _rawDataWritten: any; constructor() { this._data = ''; @@ -24,162 +24,169 @@ class MockSocket { public get dataWritten(): string { return this._data; } - // tslint:disable-next-line:no-any + public get rawDataWritten(): any { return this._rawDataWritten; } - // tslint:disable-next-line:no-any + public write(data: any) { this._data = `${data}` + ''; this._rawDataWritten = data; } } // Defines a Mocha test suite to group tests of similar kind together -// tslint:disable-next-line:max-func-body-length + suite('SocketStream', () => { - test('Read Byte', done => { - const buffer = new Buffer('X'); + test('Read Byte', (done) => { + const buffer = Buffer.from('X'); const byteValue = buffer[0]; const socket = new MockSocket(); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); - assert.equal(stream.ReadByte(), byteValue); + assert.strictEqual(stream.ReadByte(), byteValue); done(); }); - test('Read Int32', done => { + test('Read Int32', (done) => { const num = 1234; const socket = new MockSocket(); const buffer = uint64be.encode(num); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); - assert.equal(stream.ReadInt32(), num); + assert.strictEqual(stream.ReadInt32(), num); done(); }); - test('Read Int64', done => { + test('Read Int64', (done) => { const num = 9007199254740993; const socket = new MockSocket(); const buffer = uint64be.encode(num); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); - assert.equal(stream.ReadInt64(), num); + assert.strictEqual(stream.ReadInt64(), num); done(); }); - test('Read Ascii String', done => { + test('Read Ascii String', (done) => { const message = 'Hello World'; const socket = new MockSocket(); - const buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); - // tslint:disable-next-line:no-any + const buffer = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length), Buffer.from(message)]); + const stream = new SocketStream((socket as any) as net.Socket, buffer); - assert.equal(stream.ReadString(), message); + assert.strictEqual(stream.ReadString(), message); done(); }); - test('Read Unicode String', done => { + test('Read Unicode String', (done) => { const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; const socket = new MockSocket(); - const stringBuffer = new Buffer(message); - const buffer = Buffer.concat([Buffer.concat([new Buffer('U'), uint64be.encode(stringBuffer.byteLength)]), stringBuffer]); - // tslint:disable-next-line:no-any + const stringBuffer = Buffer.from(message); + const buffer = Buffer.concat([ + Buffer.concat([Buffer.from('U'), uint64be.encode(stringBuffer.byteLength)]), + stringBuffer, + ]); + const stream = new SocketStream((socket as any) as net.Socket, buffer); - assert.equal(stream.ReadString(), message); + assert.strictEqual(stream.ReadString(), message); done(); }); - test('Read RollBackTransaction', done => { + test('Read RollBackTransaction', (done) => { const message = 'Hello World'; const socket = new MockSocket(); - let buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); + let buffer = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length), Buffer.from(message)]); // Write part of a second message - const partOfSecondMessage = Buffer.concat([new Buffer('A'), uint64be.encode(message.length)]); + const partOfSecondMessage = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length)]); buffer = Buffer.concat([buffer, partOfSecondMessage]); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.BeginTransaction(); - assert.equal(stream.ReadString(), message, 'First message not read properly'); + assert.strictEqual(stream.ReadString(), message, 'First message not read properly'); stream.ReadString(); - assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + assert.strictEqual(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); stream.RollBackTransaction(); - assert.equal(stream.ReadString(), message, 'First message not read properly after rolling back transaction'); + assert.strictEqual( + stream.ReadString(), + message, + 'First message not read properly after rolling back transaction', + ); done(); }); - test('Read EndTransaction', done => { + test('Read EndTransaction', (done) => { const message = 'Hello World'; const socket = new MockSocket(); - let buffer = Buffer.concat([new Buffer('A'), uint64be.encode(message.length), new Buffer(message)]); + let buffer = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length), Buffer.from(message)]); // Write part of a second message - const partOfSecondMessage = Buffer.concat([new Buffer('A'), uint64be.encode(message.length)]); + const partOfSecondMessage = Buffer.concat([Buffer.from('A'), uint64be.encode(message.length)]); buffer = Buffer.concat([buffer, partOfSecondMessage]); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.BeginTransaction(); - assert.equal(stream.ReadString(), message, 'First message not read properly'); + assert.strictEqual(stream.ReadString(), message, 'First message not read properly'); stream.ReadString(); - assert.equal(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); + assert.strictEqual(stream.HasInsufficientDataForReading, true, 'Should not have sufficient data for reading'); stream.EndTransaction(); stream.RollBackTransaction(); - assert.notEqual(stream.ReadString(), message, 'First message cannot be read after commit transaction'); + assert.notStrictEqual(stream.ReadString(), message, 'First message cannot be read after commit transaction'); done(); }); - test('Write Buffer', done => { + test('Write Buffer', (done) => { const message = 'Hello World'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); - stream.Write(new Buffer(message)); + stream.Write(Buffer.from(message)); - assert.equal(socket.dataWritten, message); + assert.strictEqual(socket.dataWritten, message); done(); }); - test('Write Int32', done => { + test('Write Int32', (done) => { const num = 1234; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.WriteInt32(num); - assert.equal(uint64be.decode(socket.rawDataWritten), num); + assert.strictEqual(uint64be.decode(socket.rawDataWritten), num); done(); }); - test('Write Int64', done => { + test('Write Int64', (done) => { const num = 9007199254740993; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.WriteInt64(num); - assert.equal(uint64be.decode(socket.rawDataWritten), num); + assert.strictEqual(uint64be.decode(socket.rawDataWritten), num); done(); }); - test('Write Ascii String', done => { + test('Write Ascii String', (done) => { const message = 'Hello World'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.WriteString(message); - assert.equal(socket.dataWritten, message); + assert.strictEqual(socket.dataWritten, message); done(); }); - test('Write Unicode String', done => { + test('Write Unicode String', (done) => { const message = 'Hello World - Функция проверки ИНН и КПП - 说明'; - const buffer = new Buffer(''); + const buffer = Buffer.from(''); const socket = new MockSocket(); - // tslint:disable-next-line:no-any + const stream = new SocketStream((socket as any) as net.Socket, buffer); stream.WriteString(message); - assert.equal(socket.dataWritten, message); + assert.strictEqual(socket.dataWritten, message); done(); }); }); diff --git a/src/test/common/stringUtils.unit.test.ts b/src/test/common/stringUtils.unit.test.ts new file mode 100644 index 000000000000..f8b5f2947631 --- /dev/null +++ b/src/test/common/stringUtils.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import '../../client/common/extensions'; +import { replaceAll } from '../../client/common/stringUtils'; + +suite('String Extensions', () => { + test('String should replace all substrings with new substring', () => { + const oldString = `foo \\ foo \\ foo`; + const expectedString = `foo \\\\ foo \\\\ foo`; + const oldString2 = `\\ foo \\ foo`; + const expectedString2 = `\\\\ foo \\\\ foo`; + const oldString3 = `\\ foo \\`; + const expectedString3 = `\\\\ foo \\\\`; + const oldString4 = `foo foo`; + const expectedString4 = `foo foo`; + expect(replaceAll(oldString, '\\', '\\\\')).to.be.equal(expectedString); + expect(replaceAll(oldString2, '\\', '\\\\')).to.be.equal(expectedString2); + expect(replaceAll(oldString3, '\\', '\\\\')).to.be.equal(expectedString3); + expect(replaceAll(oldString4, '\\', '\\\\')).to.be.equal(expectedString4); + }); +}); diff --git a/src/test/common/terminals/activation.bash.unit.test.ts b/src/test/common/terminals/activation.bash.unit.test.ts index 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 951d2ffebb02..39bf58a9a36b 100644 --- a/src/test/common/terminals/activation.conda.unit.test.ts +++ b/src/test/common/terminals/activation.conda.unit.test.ts @@ -1,32 +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 { WorkspaceService } from '../../../client/common/application/workspace'; import '../../../client/common/extensions'; import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; -import { CurrentProcess } from '../../../client/common/process/currentProcess'; import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { CondaActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; +import { Nushell } from '../../../client/common/terminal/environmentActivationProviders/nushell'; +import { + CondaActivationCommandProvider, + _getPowershellCommands, +} from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; import { PipEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { PyEnvActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; import { TerminalHelper } from '../../../client/common/terminal/helper'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../client/common/terminal/types'; -import { IConfigurationService, IDisposableRegistry, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { + 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; @@ -39,6 +44,7 @@ suite('Terminal Environment Activation conda', () => { let processService: TypeMoq.IMock<IProcessService>; let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; let condaService: TypeMoq.IMock<ICondaService>; + let componentAdapter: TypeMoq.IMock<IComponentAdapter>; let configService: TypeMoq.IMock<IConfigurationService>; let conda: string; let bash: ITerminalActivationCommandProvider; @@ -47,119 +53,126 @@ suite('Terminal Environment Activation conda', () => { 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); + 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); + 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); + pythonSettings.setup((s) => s.terminal).returns(() => terminalSettings.object); terminalHelper = new TerminalHelper( platformService.object, instance(mock(TerminalManager)), - condaService.object, + serviceContainer.object, instance(mock(InterpreterService)), configService.object, - new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object), + new CondaActivationCommandProvider( + condaService.object, + platformService.object, + configService.object, + componentAdapter.object, + ), instance(bash), mock(CommandPromptAndPowerShell), + mock(Nushell), mock(PyEnvActivationCommandProvider), mock(PipEnvActivationCommandProvider), - instance(mock(CurrentProcess)), - instance(mock(WorkspaceService)) + 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(condaService.object, platformService.object, configService.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 () => { - const envName = 'EnvA'; - const pythonPath = 'python3'; - const condaPath = path.join('a', 'b', 'c', 'conda'); - platformService.setup(p => p.isWindows).returns(() => false); - condaService.reset(); - condaService - .setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())) - .returns(() => - Promise.resolve({ - name: envName, - path: path.dirname(pythonPath) - }) - ); - condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve(condaPath)); - condaService.setup(c => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.3.1', true)!)); - const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; - - const provider = new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object); - const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); - - expect(activationCommands).to.deep.equal(expected, 'Incorrect Activation command'); - }); - test('Conda activation on bash uses "conda" after 4.4.0', async () => { const envName = 'EnvA'; const pythonPath = 'python3'; const condaPath = path.join('a', 'b', 'c', 'conda'); - platformService.setup(p => p.isWindows).returns(() => false); + platformService.setup((p) => p.isWindows).returns(() => false); condaService.reset(); - condaService - .setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())) + componentAdapter + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) .returns(() => Promise.resolve({ name: envName, - path: path.dirname(pythonPath) - }) + path: path.dirname(pythonPath), + }), ); - condaService.setup(c => c.getCondaFile()).returns(() => Promise.resolve(condaPath)); - condaService.setup(c => c.getCondaVersion()).returns(() => Promise.resolve(parse('4.4.0', true)!)); - const expected = [`source ${path.join(path.dirname(condaPath), 'activate').fileToCommandArgument()} EnvA`]; + 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); + 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'); @@ -168,51 +181,131 @@ suite('Terminal Environment Activation conda', () => { const interpreterPath = path.join('path', 'to', 'interpreter'); const environmentName = 'Env'; const environmentNameHasSpaces = 'Env with spaces'; - const testsForActivationUsingInterpreterPath = [ + const testsForActivationUsingInterpreterPath: { + testName: string; + envName: string; + condaScope?: 'global' | 'local'; + condaInfo?: { + // eslint-disable-next-line camelcase + conda_shlvl?: number; + }; + expectedResult: string[]; + isWindows: boolean; + }[] = [ { - testName: 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with no spaces in env name', + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with no spaces in env name', envName: environmentName, expectedResult: ['path/to/activate', 'conda activate Env'], - isWindows: true + isWindows: true, }, { - testName: 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with no spaces in env name', + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with no spaces in env name', envName: environmentName, - expectedResult: ['source path/to/activate', 'conda activate Env'], - isWindows: false + expectedResult: ['source path/to/activate Env'], + isWindows: false, }, { - testName: 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with spaces in env name', + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, with spaces in env name', envName: environmentNameHasSpaces, expectedResult: ['path/to/activate', 'conda activate "Env with spaces"'], - isWindows: true + isWindows: true, }, { - testName: 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with spaces in env name', + testName: + 'Activation provides correct activation commands (non-windows) after 4.4.0 given interpreter path is provided, with spaces in env name', envName: environmentNameHasSpaces, - expectedResult: ['source path/to/activate', 'conda activate "Env with spaces"'], - isWindows: false - } + expectedResult: ['source path/to/activate "Env with spaces"'], + isWindows: false, + }, + { + testName: + 'Activation provides correct activation commands (windows) after 4.4.0 given interpreter path is provided, and no env name', + envName: '', + expectedResult: ['path/to/activate', `conda activate .`], + isWindows: true, + }, + { + 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, + }, ]; - testsForActivationUsingInterpreterPath.forEach(testParams => { + testsForActivationUsingInterpreterPath.forEach((testParams) => { test(testParams.testName, async () => { const pythonPath = 'python3'; - platformService.setup(p => p.isWindows).returns(() => testParams.isWindows); + platformService.setup((p) => p.isWindows).returns(() => testParams.isWindows); condaService.reset(); - condaService - .setup(c => c.getCondaEnvironment(TypeMoq.It.isAny())) + componentAdapter + .setup((c) => c.getCondaEnvironment(TypeMoq.It.isAny())) .returns(() => Promise.resolve({ name: testParams.envName, - path: path.dirname(pythonPath) - }) + path: path.dirname(pythonPath), + }), + ); + condaService + .setup((c) => c.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.getCondaVersion()).returns(() => Promise.resolve(parse('4.4.0', true)!)); - condaService.setup(c => c.getCondaFileFromInterpreter(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(interpreterPath)); - const provider = new CondaActivationCommandProvider(condaService.object, platformService.object, configService.object); - const activationCommands = await provider.getActivationCommands(undefined, TerminalShellType.bash); + 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'); }); @@ -224,175 +317,251 @@ suite('Terminal Environment Activation conda', () => { isLinux: boolean, pythonPath: string, shellType: TerminalShellType, - hasSpaceInEnvironmentName = false + envName: 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); - 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(condaService.object, platformService.object, configService.object).getActivationCommands( - undefined, - shellType - ); - let expectedActivationCommamnd: string[] | undefined; + 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 = [`conda activate ${envName.toCommandArgument()}`]; - 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 testCondaActivationCommands(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 testCondaActivationCommands(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 testCondaActivationCommands(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 testCondaActivationCommands(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 testCondaActivationCommands(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 testCondaActivationCommands(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']; when(bash.isShellSupported(anything())).thenReturn(true); when(bash.getActivationCommands(anything(), TerminalShellType.bash)).thenResolve(expectedActivationCommand); - 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'); }); - async function expectActivationCommandIfCondaDetectionFails(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)); - condaService.setup(c => c.isCondaEnvironment(TypeMoq.It.isAny())).returns(() => Promise.resolve(false)); - pythonSettings.setup(s => s.pythonPath).returns(() => pythonPath); + 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'); - 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 expectActivationCommandIfCondaDetectionFails(true, false, false, pythonPath); }); test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (osx)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + fileSystem + .setup((f) => + f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta'))), + ) + .returns(() => Promise.resolve(true)); await expectActivationCommandIfCondaDetectionFails(false, true, false, pythonPath); }); test('If environment is a conda environment and environment detection fails, ensure activatino of script is sent (linux)', async () => { const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'python'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); + 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'); }); @@ -415,7 +584,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershell + terminalKind: TerminalShellType.powershell, }, { testName: 'Activation uses full path with spaces on windows for powershell', @@ -423,7 +592,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.powershell + terminalKind: TerminalShellType.powershell, }, { testName: 'Activation uses full path on windows under powershell, environment name has spaces', @@ -431,7 +600,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'The Tester Environment', expectedResult: ['conda activate "The Tester Environment"'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershell + terminalKind: TerminalShellType.powershell, }, { testName: 'Activation uses full path on windows for powershell-core', @@ -439,7 +608,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershellCore + terminalKind: TerminalShellType.powershellCore, }, { testName: 'Activation uses full path with spaces on windows for powershell-core', @@ -447,7 +616,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: ['conda activate TesterEnv'], expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.powershellCore + terminalKind: TerminalShellType.powershellCore, }, { testName: 'Activation uses full path on windows for powershell-core, environment name has spaces', @@ -455,7 +624,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'The Tester Environment', expectedResult: ['conda activate "The Tester Environment"'], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.powershellCore + terminalKind: TerminalShellType.powershellCore, }, { testName: 'Activation uses full path on windows for cmd.exe', @@ -463,7 +632,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: [`${path.join(windowsTestPath, 'activate')} TesterEnv`], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.commandPrompt + terminalKind: TerminalShellType.commandPrompt, }, { testName: 'Activation uses full path with spaces on windows for cmd.exe', @@ -471,7 +640,7 @@ suite('Terminal Environment Activation conda', () => { envName: 'TesterEnv', expectedResult: [`"${path.join(windowsTestPathSpaces, 'activate')}" TesterEnv`], expectedRawCmd: `"${path.join(windowsTestPathSpaces, 'activate')}"`, - terminalKind: TerminalShellType.commandPrompt + terminalKind: TerminalShellType.commandPrompt, }, { testName: 'Activation uses full path on windows for cmd.exe, environment name has spaces', @@ -479,31 +648,30 @@ suite('Terminal Environment Activation conda', () => { envName: 'The Tester Environment', expectedResult: [`${path.join(windowsTestPath, 'activate')} "The Tester Environment"`], expectedRawCmd: `${path.join(windowsTestPath, 'activate')}`, - terminalKind: TerminalShellType.commandPrompt - } + terminalKind: TerminalShellType.commandPrompt, + }, ]; testsForWindowsActivation.forEach((testParams: WindowsActivationTestParams) => { test(testParams.testName, async () => { // each test simply tests the base windows activate command, // and then the specific result from the terminal selected. - const servCnt = TypeMoq.Mock.ofType<IServiceContainer>(); const condaSrv = TypeMoq.Mock.ofType<ICondaService>(); - condaSrv - .setup(c => c.getCondaFile()) - .returns(async () => { - return path.join(testParams.basePath, 'conda.exe'); - }); - servCnt.setup(s => s.get(TypeMoq.It.isValue(ICondaService), TypeMoq.It.isAny())).returns(() => condaSrv.object); + condaSrv.setup((c) => c.getCondaFile()).returns(async () => path.join(testParams.basePath, 'conda.exe')); - const tstCmdProvider = new CondaActivationCommandProvider(condaSrv.object, platformService.object, configService.object); + 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); + 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 6e98a699ad90..d87d33ea03e6 100644 --- a/src/test/common/terminals/activation.unit.test.ts +++ b/src/test/common/terminals/activation.unit.test.ts @@ -3,105 +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, Uri } from 'vscode'; +import { ActiveResourceService } from '../../../client/common/application/activeResource'; import { TerminalManager } from '../../../client/common/application/terminalManager'; -import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; +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: ITerminalActivator; let terminalManager: ITerminalManager; let terminalAutoActivation: ITerminalAutoActivation; - let workspaceService: IWorkspaceService; + let activeResourceService: IActiveResourceService; + const resource = Uri.parse('a'); + let terminal: Terminal; setup(() => { + 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); - workspaceService = mock(WorkspaceService); + activeResourceService = mock(ActiveResourceService); terminalAutoActivation = new TerminalAutoActivation( instance(terminalManager), [], instance(activator), - instance(workspaceService) + instance(activeResourceService), ); }); + teardown(() => { + sinon.restore(); + }); test('New Terminals should be activated', async () => { type EventHandler = (e: Terminal) => void; let handler: undefined | EventHandler; const handlerDisposable = TypeMoq.Mock.ofType<IDisposable>(); - const terminal = TypeMoq.Mock.ofType<Terminal>(); 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(), anything())).thenResolve(); - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); terminalAutoActivation.register(); expect(handler).not.to.be.an('undefined', 'event handler not initialized'); - handler!.bind(terminalAutoActivation)(terminal.object); + handler!.bind(terminalAutoActivation)(terminal); - verify(activator.activateEnvironmentInTerminal(terminal.object, undefined)).once(); + 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 terminal = TypeMoq.Mock.ofType<Terminal>(); const onDidOpenTerminal = (cb: EventHandler) => { handler = cb; return handlerDisposable.object; }; - const resource = Uri.file(__filename); + when(activeResourceService.getActiveResource()).thenReturn(resource); when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); - when(activator.activateEnvironmentInTerminal(anything(), anything(), anything())).thenResolve(); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([{ index: 0, name: '', uri: resource }]); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); terminalAutoActivation.register(); expect(handler).not.to.be.an('undefined', 'event handler not initialized'); - handler!.bind(terminalAutoActivation)(terminal.object); + handler!.bind(terminalAutoActivation)(terminal); - verify(activator.activateEnvironmentInTerminal(terminal.object, resource)).once(); + 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 terminal = TypeMoq.Mock.ofType<Terminal>(); const onDidOpenTerminal = (cb: EventHandler) => { handler = cb; return handlerDisposable.object; }; - const resource = Uri.file(__filename); + when(activeResourceService.getActiveResource()).thenReturn(resource); when(terminalManager.onDidOpenTerminal).thenReturn(onDidOpenTerminal); - when(activator.activateEnvironmentInTerminal(anything(), anything(), anything())).thenResolve(); - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([ - { index: 0, name: '', uri: resource }, - { index: 2, name: '2', uri: Uri.file('1234') } - ]); - + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); terminalAutoActivation.register(); expect(handler).not.to.be.an('undefined', 'event handler not initialized'); - handler!.bind(terminalAutoActivation)(terminal.object); + handler!.bind(terminalAutoActivation)(terminal); - verify(activator.activateEnvironmentInTerminal(terminal.object, resource)).once(); + 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 54d68bfc6cf8..fdfe9dcee579 100644 --- a/src/test/common/terminals/activator/base.unit.test.ts +++ b/src/test/common/terminals/activator/base.unit.test.ts @@ -10,84 +10,98 @@ import { BaseTerminalActivator } from '../../../../client/common/terminal/activa import { ITerminalActivator, ITerminalHelper } from '../../../../client/common/terminal/types'; import { noop } from '../../../../client/common/utils/misc'; -// tslint:disable:max-func-body-length no-any suite('Terminal Base Activator', () => { let activator: ITerminalActivator; let helper: TypeMoq.IMock<ITerminalHelper>; 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.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.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.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 eb2384483090..9bf1afbbad03 100644 --- a/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts +++ b/src/test/common/terminals/activator/powerShellFailedHandler.unit.test.ts @@ -8,57 +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.identifyTerminalShell(TypeMoq.It.isAny())) - .returns(() => shellType); + 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)); 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 index cb6d177713fc..5d963b8aa2c2 100644 --- a/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts +++ b/src/test/common/terminals/environmentActivationProviders/pipEnvActivationProvider.unit.test.ts @@ -9,39 +9,34 @@ import * as TypeMoq from 'typemoq'; import { Uri } from 'vscode'; import { IWorkspaceService } from '../../../../client/common/application/types'; import { WorkspaceService } from '../../../../client/common/application/workspace'; -import { FileSystem } from '../../../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../../../client/common/platform/types'; import { PipEnvActivationCommandProvider } from '../../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; import { ITerminalActivationCommandProvider, TerminalShellType } from '../../../../client/common/terminal/types'; +import { IToolExecutionPath } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; -import { IInterpreterService, InterpreterType, IPipEnvService } from '../../../../client/interpreter/contracts'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; import { InterpreterService } from '../../../../client/interpreter/interpreterService'; - -// tslint:disable:no-any +import { EnvironmentType } from '../../../../client/pythonEnvironments/info'; suite('Terminals Activation - Pipenv', () => { - [undefined, Uri.parse('x')].forEach(resource => { + [undefined, Uri.parse('x')].forEach((resource) => { suite(resource ? 'With a resource' : 'Without a resource', () => { let pipenvExecFile = 'pipenv'; let activationProvider: ITerminalActivationCommandProvider; let interpreterService: IInterpreterService; - let pipenvService: TypeMoq.IMock<IPipEnvService>; + let pipEnvExecution: TypeMoq.IMock<IToolExecutionPath>; let workspaceService: IWorkspaceService; - let fs: IFileSystem; setup(() => { interpreterService = mock(InterpreterService); - fs = mock(FileSystem); workspaceService = mock(WorkspaceService); interpreterService = mock(InterpreterService); - pipenvService = TypeMoq.Mock.ofType<IPipEnvService>(); + pipEnvExecution = TypeMoq.Mock.ofType<IToolExecutionPath>(); activationProvider = new PipEnvActivationCommandProvider( instance(interpreterService), - pipenvService.object, + pipEnvExecution.object, instance(workspaceService), - instance(fs) ); - pipenvService.setup(p => p.executable).returns(() => pipenvExecFile); + pipEnvExecution.setup((p) => p.executable).returns(() => pipenvExecFile); }); test('No commands for no interpreter', async () => { @@ -50,24 +45,31 @@ suite('Terminals Activation - Pipenv', () => { for (const shell of getNamesAndValues<TerminalShellType>(TerminalShellType)) { const cmd = await activationProvider.getActivationCommands(resource, shell.value); - assert.equal(cmd, undefined); + assert.strictEqual(cmd, undefined); } }); test('No commands for an interpreter that is not Pipenv', async () => { - const nonPipInterpreterTypes = getNamesAndValues<InterpreterType>(InterpreterType) - .filter(t => t.value !== InterpreterType.Pipenv); + const nonPipInterpreterTypes = getNamesAndValues<EnvironmentType>(EnvironmentType).filter( + (t) => t.value !== EnvironmentType.Pipenv, + ); for (const interpreterType of nonPipInterpreterTypes) { - when(interpreterService.getActiveInterpreter(resource)).thenResolve({ type: interpreterType } as any); + 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.equal(cmd, undefined); + assert.strictEqual(cmd, undefined); } } }); test('pipenv shell is returned for pipenv interpeter', async () => { - when(interpreterService.getActiveInterpreter(resource)).thenResolve({ type: InterpreterType.Pipenv } as any); + 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); @@ -77,7 +79,10 @@ suite('Terminals Activation - Pipenv', () => { }); test('pipenv is properly escaped', async () => { pipenvExecFile = 'my pipenv'; - when(interpreterService.getActiveInterpreter(resource)).thenResolve({ type: InterpreterType.Pipenv } as any); + 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); diff --git a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts index f09314612262..5a5e65a9c0f2 100644 --- a/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts +++ b/src/test/common/terminals/environmentActivationProviders/terminalActivation.testvirtualenvs.ts @@ -4,26 +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 { PlatformService } from '../../../../client/common/platform/platformService'; 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:max-func-body-length no-any suite('Activation of Environments in Terminal', () => { - const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', 'testExecInTerminal.py'); + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'testExecInTerminal.py', + ); let outputFile = ''; let outputFileCounter = 0; - const fileSystem = new FileSystem(new PlatformService()); + 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 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; @@ -36,28 +49,37 @@ suite('Activation of Environments in Terminal', () => { const defaultShell = { Windows: '', Linux: '', - MacOS: '' + MacOS: '', }; 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(); + defaultShell.Windows = terminalSettings.inspect('integrated.defaultProfile.windows').globalValue; + defaultShell.Linux = terminalSettings.inspect('integrated.defaultProfile.linux').globalValue; + await terminalSettings.update('integrated.defaultProfile.linux', 'bash', vscode.ConfigurationTarget.Global); }); - setup(async () => { + setup(async function () { + this.skip(); // https://github.com/microsoft/vscode-python/issues/22264 await initializeTest(); - outputFile = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', `testExecInTerminal_${outputFileCounter}.log`); + outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + `testExecInTerminal_${outputFileCounter}.log`, + ); outputFileCounter += 1; outputFilesCreated.push(outputFile); }); - suiteTeardown(async () => { + suiteTeardown(async function () { + sandbox.restore(); + this.timeout(TEST_TIMEOUT * 2); await revertSettings(); // remove all created log files. @@ -69,10 +91,23 @@ suite('Activation of Environments in Terminal', () => { }); 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(); } @@ -89,11 +124,14 @@ suite('Activation of Environments in Terminal', () => { consoleInitWaitMs: number, pythonFile: string, logFile: string, - logFileCreationWaitMs: number + logFileCreationWaitMs: number, ): Promise<string> { const terminal = vscode.window.createTerminal(); await sleep(consoleInitWaitMs); - terminal.sendText(`python ${pythonFile} ${logFile}`, true); + terminal.sendText( + `python ${pythonFile.toCommandArgumentForPythonExt()} ${logFile.toCommandArgumentForPythonExt()}`, + true, + ); await waitForCondition(() => fs.pathExists(logFile), logFileCreationWaitMs, `${logFile} file not created.`); return fs.readFile(logFile, 'utf-8'); @@ -108,30 +146,51 @@ suite('Activation of Environments in Terminal', () => { * @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 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 updateSetting('terminal.activateEnvironment', false, vscode.workspace.workspaceFolders![0].uri, vscode.ConfigurationTarget.WorkspaceFolder); + 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.unit.test.ts b/src/test/common/terminals/helper.unit.test.ts index f6cbba997709..0d130b573408 100644 --- a/src/test/common/terminals/helper.unit.test.ts +++ b/src/test/common/terminals/helper.unit.test.ts @@ -4,93 +4,122 @@ import { expect } from 'chai'; import { SemVer } from 'semver'; import * as sinon from 'sinon'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; +import { Terminal, Uri } from 'vscode'; import { TerminalManager } from '../../../client/common/application/terminalManager'; import { ITerminalManager } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; import { PythonSettings } from '../../../client/common/configSettings'; import { ConfigurationService } from '../../../client/common/configuration/service'; import { PlatformService } from '../../../client/common/platform/platformService'; import { IPlatformService } from '../../../client/common/platform/types'; -import { CurrentProcess } from '../../../client/common/process/currentProcess'; import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash'; import { CommandPromptAndPowerShell } from '../../../client/common/terminal/environmentActivationProviders/commandPrompt'; -import { - CondaActivationCommandProvider -} from '../../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; -import { - PipEnvActivationCommandProvider -} from '../../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; -import { - PyEnvActivationCommandProvider -} from '../../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; +import { 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 { ShellDetector } from '../../../client/common/terminal/shellDetector'; +import { TerminalNameShellDetector } from '../../../client/common/terminal/shellDetectors/terminalNameShellDetector'; import { + IShellDetector, ITerminalActivationCommandProvider, - TerminalShellType + TerminalShellType, } from '../../../client/common/terminal/types'; import { IConfigurationService } from '../../../client/common/types'; import { getNamesAndValues } from '../../../client/common/utils/enum'; import { Architecture, OSType } from '../../../client/common/utils/platform'; -import { ICondaService, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { IComponentAdapter } from '../../../client/interpreter/contracts'; import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; - -// tslint:disable:max-func-body-length no-any +import { IServiceContainer } from '../../../client/ioc/types'; +import { EnvironmentType, PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { PixiActivationCommandProvider } from '../../../client/common/terminal/environmentActivationProviders/pixiActivationProvider'; suite('Terminal Service helpers', () => { let helper: TerminalHelper; let terminalManager: ITerminalManager; let platformService: IPlatformService; - let condaService: ICondaService; + let condaService: IComponentAdapter; + let serviceContainer: IServiceContainer; let configurationService: IConfigurationService; let condaActivationProvider: ITerminalActivationCommandProvider; let bashActivationProvider: ITerminalActivationCommandProvider; let cmdActivationProvider: ITerminalActivationCommandProvider; + let nushellActivationProvider: ITerminalActivationCommandProvider; let pyenvActivationProvider: ITerminalActivationCommandProvider; let pipenvActivationProvider: ITerminalActivationCommandProvider; + let pixiActivationProvider: ITerminalActivationCommandProvider; let pythonSettings: PythonSettings; - let currentProcess: CurrentProcess; - let shellDetectorIdentifyTerminalShell: sinon.SinonStub; - - const pythonInterpreter: PythonInterpreter = { + 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', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; function doSetup() { + mockDetector = mock(TerminalNameShellDetector); terminalManager = mock(TerminalManager); platformService = mock(PlatformService); - condaService = mock(CondaService); + serviceContainer = mock<IServiceContainer>(); + condaService = mock<IComponentAdapter>(); + when(serviceContainer.get<IComponentAdapter>(IComponentAdapter)).thenReturn(instance(condaService)); configurationService = mock(ConfigurationService); condaActivationProvider = mock(CondaActivationCommandProvider); bashActivationProvider = mock(Bash); cmdActivationProvider = mock(CommandPromptAndPowerShell); + nushellActivationProvider = mock(Nushell); pyenvActivationProvider = mock(PyEnvActivationCommandProvider); pipenvActivationProvider = mock(PipEnvActivationCommandProvider); + pixiActivationProvider = mock(PixiActivationCommandProvider); pythonSettings = mock(PythonSettings); - currentProcess = mock(CurrentProcess); shellDetectorIdentifyTerminalShell = sinon.stub(ShellDetector.prototype, 'identifyTerminalShell'); - helper = new TerminalHelper(instance(platformService), instance(terminalManager), - instance(condaService), + 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(currentProcess), - instance(mock(WorkspaceService))); + instance(pixiActivationProvider), + [instance(mockDetector)], + ); } teardown(() => shellDetectorIdentifyTerminalShell.restore()); suite('Misc', () => { setup(doSetup); + test('Creating terminal should not automatically contain PYTHONSTARTUP', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + const term = helper.createTerminal(theTitle); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + const terminalOptions = args.env; + const safeTerminalOptions = terminalOptions || {}; + expect(safeTerminalOptions).to.not.have.property('PYTHONSTARTUP'); + }); + + test('Env should be undefined if not explicitly passed in ', () => { + const theTitle = 'Hello'; + const terminal = 'Terminal Created'; + when(terminalManager.createTerminal(anything())).thenReturn(terminal as any); + + const term = helper.createTerminal(theTitle); + + verify(terminalManager.createTerminal(anything())).once(); + const args = capture(terminalManager.createTerminal).first()[0]; + expect(term).to.be.deep.equal(terminal); + expect(args.env).to.be.deep.equal(undefined); + }); test('Create terminal without a title', () => { const terminal = 'Terminal Created'; @@ -116,11 +145,28 @@ suite('Terminal Service helpers', () => { expect(args.name).to.be.deep.equal(theTitle); }); test('Ensure spaces in command is quoted', async () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(item => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { const command = 'c:\\python 3.7.exe'; const args = ['1', '2']; - const commandPrefix = (item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore) ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()} 1 2`; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; + const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgumentForPythonExt()} 1 2`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + 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}`); @@ -128,10 +174,13 @@ suite('Terminal Service helpers', () => { }); test('Ensure empty args are ignored', async () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(item => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { const command = 'python3.7.exe'; const args: string[] = []; - const commandPrefix = (item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore) ? '& ' : ''; + const commandPrefix = + item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore + ? '& ' + : ''; const expectedTerminalCommand = `${commandPrefix}${command}`; const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); @@ -140,11 +189,14 @@ suite('Terminal Service helpers', () => { }); test('Ensure empty args are ignored with s in command', async () => { - getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(item => { + getNamesAndValues<TerminalShellType>(TerminalShellType).forEach((item) => { const command = 'c:\\python 3.7.exe'; const args: string[] = []; - const commandPrefix = (item.value === TerminalShellType.powershell || item.value === TerminalShellType.powershellCore) ? '& ' : ''; - const expectedTerminalCommand = `${commandPrefix}${command.fileToCommandArgument()}`; + const 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}`); @@ -152,31 +204,28 @@ suite('Terminal Service helpers', () => { }); }); - - function title(resource?: Uri, interpreter?: PythonInterpreter) { + function title(resource?: Uri, interpreter?: PythonEnvironment) { return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; } suite('Activation', () => { - [undefined, Uri.parse('a')].forEach(resource => { + [undefined, Uri.parse('a')].forEach((resource) => { suite(title(resource), () => { setup(() => { doSetup(); when(configurationService.getSettings(resource)).thenReturn(instance(pythonSettings)); }); - test('Activation command must be empty if activation of terminals is disabled', async () => { - when(pythonSettings.terminal).thenReturn({ activateEnvironment: false } as any); - - const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); - - expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.terminal).once(); - }); - function ensureCondaIsSupported(isSupported: boolean, pythonPath: string, condaActivationCommands: string[]) { + 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); + 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'; @@ -186,9 +235,8 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.equal(condaActivationCommands); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(condaActivationProvider.getActivationCommands(resource, anything())).once(); }); test('Activation command must return undefined if none of the proivders support the shell', async () => { @@ -197,16 +245,20 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(false); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); - const cmd = await helper.getEnvironmentActivationCommands('someShell' as any as TerminalShellType, resource); + const cmd = await helper.getEnvironmentActivationCommands( + ('someShell' as any) as TerminalShellType, + resource, + ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); @@ -220,17 +272,18 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(false); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(false); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + verify(pythonSettings.pythonPath).atLeast(1); + verify(condaService.isCondaEnvironment(pythonPath)).atLeast(1); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); @@ -240,10 +293,17 @@ suite('Terminal Service helpers', () => { const expectCommand = ['one', 'two']; ensureCondaIsSupported(false, pythonPath, []); - when(pipenvActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); + when(pipenvActivationProvider.getActivationCommands(resource, anything())).thenResolve( + expectCommand, + ); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(true); - [bashActivationProvider, cmdActivationProvider, pyenvActivationProvider].forEach(provider => { + [ + bashActivationProvider, + cmdActivationProvider, + nushellActivationProvider, + pyenvActivationProvider, + ].forEach((provider) => { when(provider.getActivationCommands(resource, anything())).thenResolve(['Something']); when(provider.isShellSupported(anything())).thenReturn(true); }); @@ -251,8 +311,7 @@ suite('Terminal Service helpers', () => { const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).never(); @@ -260,6 +319,7 @@ suite('Terminal Service helpers', () => { verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.getActivationCommands(resource, anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); }); test('Activation command must return command from Command Prompt if that is supported and others are not', async () => { const pythonPath = 'some python Path value'; @@ -270,83 +330,129 @@ suite('Terminal Service helpers', () => { when(bashActivationProvider.isShellSupported(anything())).thenReturn(false); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(false); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); }); - test('Activation command must return command from Command Prompt if that is supported, and so is bash but no commands are returned', async () => { + test('Activation command must return command from Command Prompt if that is supported, and so is bash and nushell but no commands are returned', async () => { const pythonPath = 'some python Path value'; const expectCommand = ['one', 'two']; ensureCondaIsSupported(false, pythonPath, []); when(cmdActivationProvider.getActivationCommands(resource, anything())).thenResolve(expectCommand); when(bashActivationProvider.getActivationCommands(resource, anything())).thenResolve([]); + when(nushellActivationProvider.getActivationCommands(resource, anything())).thenResolve([]); when(bashActivationProvider.isShellSupported(anything())).thenReturn(true); when(cmdActivationProvider.isShellSupported(anything())).thenReturn(true); + when(nushellActivationProvider.isShellSupported(anything())).thenReturn(true); when(pyenvActivationProvider.isShellSupported(anything())).thenReturn(false); when(pipenvActivationProvider.isShellSupported(anything())).thenReturn(false); const cmd = await helper.getEnvironmentActivationCommands(anything(), resource); expect(cmd).to.deep.equal(expectCommand); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); + verify(pythonSettings.pythonPath).atLeast(1); verify(condaService.isCondaEnvironment(pythonPath)).once(); - verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(bashActivationProvider.getActivationCommands(resource, anything())).once(); verify(cmdActivationProvider.getActivationCommands(resource, anything())).once(); + // It should not be called as command prompt already returns the activation commands and is higher priority. + verify(nushellActivationProvider.getActivationCommands(resource, anything())).never(); verify(pyenvActivationProvider.isShellSupported(anything())).atLeast(1); verify(pipenvActivationProvider.isShellSupported(anything())).atLeast(1); + verify(bashActivationProvider.isShellSupported(anything())).atLeast(1); verify(cmdActivationProvider.isShellSupported(anything())).atLeast(1); + verify(nushellActivationProvider.isShellSupported(anything())).atLeast(1); }); - [undefined, pythonInterpreter].forEach(interpreter => { + [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); + 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 => { + [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; + 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); + const cmd = await helper.getEnvironmentActivationShellCommands( + resource, + shellToExpect, + interpreter, + ); expect(cmd).to.equal(undefined, 'Command must be undefined'); - verify(pythonSettings.terminal).once(); - verify(pythonSettings.pythonPath).once(); - verify(condaService.isCondaEnvironment(pythonPath)).once(); + 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); }); }); }); }); }); }); + + suite('Identify Terminal Shell', () => { + setup(doSetup); + test('Use shell detector to identify terminal shells', () => { + const terminal = {} as any; + const expectedShell = TerminalShellType.ksh; + shellDetectorIdentifyTerminalShell.returns(expectedShell); + + const shell = helper.identifyTerminalShell(terminal); + expect(shell).to.be.equal(expectedShell); + expect(shellDetectorIdentifyTerminalShell.callCount).to.equal(1); + expect(shellDetectorIdentifyTerminalShell.args[0]).deep.equal([terminal]); + }); + test('Detector passed throught constructor is used by shell detector class', () => { + const terminal = {} as any; + const expectedShell = TerminalShellType.ksh; + shellDetectorIdentifyTerminalShell.callThrough(); + when(mockDetector.identify(anything(), terminal)).thenReturn(expectedShell); + + const shell = helper.identifyTerminalShell(terminal); + + expect(shell).to.be.equal(expectedShell); + expect(shellDetectorIdentifyTerminalShell.callCount).to.equal(1); + verify(mockDetector.identify(anything(), terminal)).once(); + }); + }); }); diff --git a/src/test/common/terminals/pyenvActivationProvider.unit.test.ts b/src/test/common/terminals/pyenvActivationProvider.unit.test.ts index 9b3357bb1050..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 9afc97d5eb53..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,115 +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.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.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 sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python < 3.13', async () => { + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + service = new TerminalService(mockServiceContainer.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure sendText is called when Python shell integration and terminal shell integration are both enabled - Mac, Linux && Python >= 3.13', async () => { + interpreterService.reset(); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => + Promise.resolve({ path: 'yo', version: { major: 3, minor: 13, patch: 0 } } as PythonEnvironment), + ); + + isWindowsStub.returns(false); + pythonConfig + .setup((p) => p.get('terminal.shellIntegration.enabled')) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + + terminalHelper + .setup((helper) => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + service = new TerminalService(mockServiceContainer.object, options.object); + const textToSend = 'Some Text'; + terminalHelper.setup((h) => h.identifyTerminalShell(TypeMoq.It.isAny())).returns(() => TerminalShellType.bash); + terminalManager.setup((t) => t.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + + await service.ensureTerminal(); + const executePromise = service.executeCommand(textToSend, true); + onDidWriteTerminalDataEmitter.fire({ terminal: terminal.object, data: '>>> ' }); + await executePromise; + + terminal.verify((t) => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.once()); }); - test('Ensure terminal shown', async () => { - terminalHelper.setup(helper => helper.getEnvironmentActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + 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); - 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 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.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 PYTHONSTARTUP is injected', async () => { + service = new TerminalService(mockServiceContainer.object); + terminalActivator + .setup((h) => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.once()); + terminalManager + .setup((t) => t.createTerminal(TypeMoq.It.isAny())) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + const envVarScript = path.join(EXTENSION_ROOT_DIR, 'python_files', 'pythonrc.py'); + terminalManager + .setup((t) => + t.createTerminal({ + name: TypeMoq.It.isAny(), + env: TypeMoq.It.isObjectWith({ PYTHONSTARTUP: envVarScript }), + hideFromUser: TypeMoq.It.isAny(), + }), + ) + .returns(() => terminal.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + await service.show(); + await service.show(); + await service.show(); + await service.show(); + + terminalHelper.verifyAll(); + terminalActivator.verifyAll(); + terminal.verify((t) => t.show(TypeMoq.It.isValue(true)), TypeMoq.Times.atLeastOnce()); }); test('Ensure terminal is activated once after creation', async () => { service = new TerminalService(mockServiceContainer.object); terminalActivator - .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(); @@ -140,18 +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'; 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); @@ -161,22 +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.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'); @@ -187,19 +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.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'); @@ -208,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 index 44c246c09760..c09560a3ea37 100644 --- a/src/test/common/terminals/shellDetector.unit.test.ts +++ b/src/test/common/terminals/shellDetector.unit.test.ts @@ -1,225 +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 { instance, mock, when } from 'ts-mockito'; -import { Terminal } from 'vscode'; +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 { CurrentProcess } from '../../../client/common/process/currentProcess'; 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 { OSType } from '../../common'; - -// tslint:disable:max-func-body-length no-any +import { getNamesAndValues } from '../../../client/common/utils/enum'; +import { OSType } from '../../../client/common/utils/platform'; +import { MockProcess } from '../../../test/mocks/process'; suite('Shell Detector', () => { - let shellDetector: ShellDetector; let platformService: IPlatformService; - let currentProcess: CurrentProcess; - - // Dummy data for testing. - const shellPathsAndIdentification = new Map<string, TerminalShellType>(); - shellPathsAndIdentification.set('c:\\windows\\system32\\cmd.exe', TerminalShellType.commandPrompt); - shellPathsAndIdentification.set('c:\\windows\\system32\\bash.exe', TerminalShellType.bash); - shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.wsl); - shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.gitbash); - shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); - shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.zsh); - shellPathsAndIdentification.set('/usr/bin/ksh', TerminalShellType.ksh); - shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); - shellPathsAndIdentification.set('c:\\windows\\system32\\pwsh.exe', TerminalShellType.powershellCore); - shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); - shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/pwsh', TerminalShellType.powershellCore); - shellPathsAndIdentification.set('/usr/bin/fish', TerminalShellType.fish); - shellPathsAndIdentification.set('c:\\windows\\system32\\shell.exe', TerminalShellType.other); - shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); - shellPathsAndIdentification.set('/usr/bin/csh', TerminalShellType.cshell); - shellPathsAndIdentification.set('/usr/bin/tcsh', TerminalShellType.tcshell); - shellPathsAndIdentification.set('/usr/bin/xonsh', TerminalShellType.xonsh); - shellPathsAndIdentification.set('/usr/bin/xonshx', TerminalShellType.other); - - - setup(() => { - platformService = mock(PlatformService); - currentProcess = mock(CurrentProcess); - shellDetector = new ShellDetector(instance(platformService), - instance(currentProcess), - instance(mock(WorkspaceService))); - }); - test('Test identification of Terminal Shells', async () => { - shellPathsAndIdentification.forEach((shellType, shellPath) => { - expect(shellDetector.identifyShellByTerminalName(shellPath, {} as any)).to.equal(shellType, `Incorrect Shell Type from identifyShellByTerminalName, for path '${shellPath}'`); - expect(shellDetector.identifyShellFromShellPath(shellPath)).to.equal(shellType, `Incorrect Shell Type for path from identifyTerminalFromShellPath, '${shellPath}'`); - - // Assume the same paths are stored in user settings, we should still be able to identify the shell. - shellDetector.getTerminalShellPath = () => shellPath; - expect(shellDetector.identifyShellFromSettings({} as any)).to.equal(shellType, `Incorrect Shell Type from identifyTerminalFromSettings, for path '${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.identifyShellFromUserEnv({} as any)).to.equal(shellType, `Incorrect Shell Type from identifyTerminalFromEnv, for path '${shellPath}'`); + 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('Default shell on Windows < 10 is cmd.exe', () => { - 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', () => { - 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', () => { - 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', () => { - 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`, () => { - when(platformService.osType).thenReturn(OSType.OSX); - when(currentProcess.env).thenReturn({}); + 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 shellPath = shellDetector.getDefaultPlatformShell(); + const shell = shellDetector.identifyTerminalShell(); - expect(shellPath).to.equal('/bin/bash'); + expect(shell).to.be.equal(defaultOSShells[os.value]); }); - test(`Default shell on ${osType} is what ever is in env.SHELL`, () => { - when(platformService.osType).thenReturn(OSType.OSX); - when(currentProcess.env).thenReturn({ SHELL: 'hello terminal.app' }); + 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 shellPath = shellDetector.getDefaultPlatformShell(); + const shell = shellDetector.identifyTerminalShell({ name: 'bash' } as any); - expect(shellPath).to.equal('hello terminal.app'); + expect(shell).to.be.equal(defaultOSShells[os.value]); }); - test(`Default shell on ${osType} is what ever is /bin/bash if env.SHELL == /bin/false`, () => { - when(platformService.osType).thenReturn(OSType.OSX); - when(currentProcess.env).thenReturn({ SHELL: '/bin/false' }); + 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 shellPath = shellDetector.getDefaultPlatformShell(); + const shell = shellDetector.identifyTerminalShell(); - expect(shellPath).to.equal('/bin/bash'); + expect(shell).to.be.equal(detectedShell); + verify(detector.identify(anything(), undefined)).once(); }); - }); - shellPathsAndIdentification.forEach((expectedShell, shellPath) => { - if (expectedShell === TerminalShellType.other) { - return; - } - const testSuffix = `(${shellPath})`; - test(`Try identifying the shell based on the terminal name ${testSuffix}`, () => { - const terminal: Terminal = { name: shellPath } as any; - - const identifyShellByTerminalName = sinon.stub(shellDetector, 'identifyShellByTerminalName'); - const getTerminalShellPath = sinon.stub(shellDetector, 'getTerminalShellPath'); - const getDefaultPlatformShell = sinon.stub(shellDetector, 'getDefaultPlatformShell'); - - identifyShellByTerminalName.callsFake(() => expectedShell); + 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(identifyShellByTerminalName.calledOnce).to.equal(true, 'identifyShellByTerminalName should be invoked to identify the shell'); - expect(identifyShellByTerminalName.args[0][0]).to.equal(terminal.name); - expect(getTerminalShellPath.notCalled).to.equal(true, 'We should not be checking the shell path'); - expect(getDefaultPlatformShell.notCalled).to.equal(true, 'We should not be identifying the default OS shell'); - expect(shell).to.equal(expectedShell); + expect(shell).to.be.equal(detectedShell); + verify(detector.identify(anything(), terminal)).once(); }); - test(`Try identifying the shell based on VSC Settings ${testSuffix}`, () => { - // As the terminal is 'some unknown value' we don't know the shell. - // We should identify the shell based on VSC settings. - // We should not check user environment for shell. - const terminal: Terminal = { name: 'some unknown name' } as any; - - const identifyShellByTerminalName = sinon.stub(shellDetector, 'identifyShellByTerminalName'); - const getTerminalShellPath = sinon.stub(shellDetector, 'getTerminalShellPath'); - const getDefaultPlatformShell = sinon.stub(shellDetector, 'getDefaultPlatformShell'); - - // We cannot identify shell by the name of the terminal, hence other will be returned. - identifyShellByTerminalName.callsFake(() => TerminalShellType.other); - getTerminalShellPath.returns(shellPath); - - const shell = shellDetector.identifyTerminalShell(terminal); - - expect(getTerminalShellPath.calledOnce).to.equal(true, 'We should be checking the shell path'); - expect(identifyShellByTerminalName.args[0][0]).to.equal(terminal.name); - expect(getTerminalShellPath.calledAfter(identifyShellByTerminalName)).to.equal(true, 'We should be checking the shell path after checking terminal name'); - expect(getDefaultPlatformShell.calledOnce).to.equal(false, 'We should not be identifying the default OS shell'); - expect(identifyShellByTerminalName.calledOnce).to.equal(true, 'identifyShellByTerminalName should be invoked'); - expect(shell).to.equal(expectedShell); + 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(`Try identifying the shell based on user environment ${testSuffix}`, () => { - // As the terminal is 'some unknown value' we don't know the shell. - // We should try try identify the shell based on VSC settings. - // We should check user environment for shell. - const terminal: Terminal = { name: 'some unknown name' } as any; - - const identifyShellByTerminalName = sinon.stub(shellDetector, 'identifyShellByTerminalName'); - const getTerminalShellPath = sinon.stub(shellDetector, 'getTerminalShellPath'); - const getDefaultPlatformShell = sinon.stub(shellDetector, 'getDefaultPlatformShell'); - - // We cannot identify shell by the name of the terminal, hence other will be returned. - identifyShellByTerminalName.callsFake(() => TerminalShellType.other); - getTerminalShellPath.returns('some bogus terminal app.app'); - getDefaultPlatformShell.returns(shellPath); - - const shell = shellDetector.identifyTerminalShell(terminal); - - expect(getTerminalShellPath.calledOnce).to.equal(true, 'We should be checking the shell path'); - expect(identifyShellByTerminalName.args[0][0]).to.equal(terminal.name); - expect(getTerminalShellPath.calledAfter(identifyShellByTerminalName)).to.equal(true, 'We should be checking the shell path after checking terminal name'); - expect(getDefaultPlatformShell.calledOnce).to.equal(true, 'We should be identifying the default OS shell'); - expect(getDefaultPlatformShell.calledAfter(getTerminalShellPath)).to.equal(true, 'We should be checking the platform shell path after checking settings'); - expect(identifyShellByTerminalName.calledOnce).to.equal(true, 'identifyShellByTerminalName should be invoked'); - expect(shell).to.equal(expectedShell); + 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(); }); - }); - [OSType.Windows, OSType.Linux, OSType.OSX].forEach(osType => { - test(`Use os defaults if all 3 stratergies fail (${osType})`, () => { - // All three approaches should fail. - // We should try try identify the shell based on VSC settings. - // We should check user environment for shell. - const terminal: Terminal = { name: 'some unknown name' } as any; - const expectedDefault = osType === OSType.Windows ? TerminalShellType.commandPrompt : TerminalShellType.bash; - - const identifyShellByTerminalName = sinon.stub(shellDetector, 'identifyShellByTerminalName'); - const getTerminalShellPath = sinon.stub(shellDetector, 'getTerminalShellPath'); - const getDefaultPlatformShell = sinon.stub(shellDetector, 'getDefaultPlatformShell'); - - // Remember, none of the methods should return a valid terminal. - when(platformService.osType).thenReturn(osType); - identifyShellByTerminalName.callsFake(() => TerminalShellType.other); - getTerminalShellPath.returns('some bogus terminal app.app'); - getDefaultPlatformShell.returns('nothing here as well'); - - const shell = shellDetector.identifyTerminalShell(terminal); - - expect(getTerminalShellPath.calledOnce).to.equal(true, 'We should be checking the shell path'); - expect(getDefaultPlatformShell.calledOnce).to.equal(true, 'We should be identifying the default OS shell'); - expect(identifyShellByTerminalName.calledOnce).to.equal(true, 'identifyShellByTerminalName should be invoked'); - expect(identifyShellByTerminalName.args[0][0]).to.equal(terminal.name); - expect(shell).to.equal(expectedDefault); + 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 index 386c569fa24a..01a11f4b4585 100644 --- a/src/test/common/utils/cacheUtils.unit.test.ts +++ b/src/test/common/utils/cacheUtils.unit.test.ts @@ -3,228 +3,54 @@ 'use strict'; -import { expect } from 'chai'; -import { Uri } from 'vscode'; -import { clearCache, InMemoryInterpreterSpecificCache } from '../../../client/common/utils/cacheUtils'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import { InMemoryCache } from '../../../client/common/utils/cacheUtils'; -type CacheUtilsTestScenario = { - scenarioDesc: string; - // tslint:disable-next-line:no-any - dataToStore: any; -}; - -const scenariosToTest: CacheUtilsTestScenario[] = [ - { - scenarioDesc: 'simple string', - dataToStore: 'hello' - }, - { - scenarioDesc: 'undefined', - dataToStore: undefined - }, - { - scenarioDesc: 'object', - dataToStore: { date: new Date(), hello: 1234 } - } -]; - -class TestInMemoryInterpreterSpecificCache extends InMemoryInterpreterSpecificCache<string | undefined | { date: number; hello: number }> { - public elapsed: number = 0; - - public set simulatedElapsedMs(value: number) { - this.elapsed = value; - } - - protected calculateExpiry(): number { - return this.expiryDurationMs; - } - - protected hasExpired(expiry: number): boolean { - return expiry < this.elapsed; - } -} - -// tslint:disable:no-any max-func-body-length suite('Common Utils - CacheUtils', () => { - teardown(() => { - clearCache(); - }); - function createMockVSC(pythonPath: string): typeof import('vscode') { - return { - workspace: { - getConfiguration: () => { - return { - get: () => { - return pythonPath; - }, - inspect: () => { - return { globalValue: pythonPath }; - } - }; - }, - getWorkspaceFolder: () => { - return; - } - }, - Uri: Uri - } as any; - } - scenariosToTest.forEach((scenario: CacheUtilsTestScenario) => { - test(`Data is stored in cache (without workspaces): ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - - cache.data = scenario.dataToStore; - - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - }); - test(`Data is stored in cache must be cleared when clearing globally: ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - - cache.data = scenario.dataToStore; - - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - - clearCache(); - expect(cache.hasData).to.be.equal(false, 'Must not have data'); - expect(cache.data).to.be.deep.equal(undefined, 'Must not have data'); + suite('InMemory Cache', () => { + let clock: sinon.SinonFakeTimers; + setup(() => { + clock = sinon.useFakeTimers(); }); - test(`Data is stored in cache must be cleared: ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - - cache.data = scenario.dataToStore; - - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - - cache.clear(); - expect(cache.hasData).to.be.equal(false, 'Must not have data'); - expect(cache.data).to.be.deep.equal(undefined, 'Must not have data'); - }); - test(`Data is stored in cache and expired data is not returned: ${scenario.scenarioDesc}`, async () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const cache = new TestInMemoryInterpreterSpecificCache('Something', 100, [resource], vsc); - - expect(cache.hasData).to.be.equal(false, 'Must not have any data before caching.'); - cache.data = scenario.dataToStore; - expect(cache.hasData).to.be.equal(true, 'Must have data after setting the first time.'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - - cache.simulatedElapsedMs = 10; - expect(cache.hasData).to.be.equal(true, 'Must have data after waiting for 10ms'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore, 'Data should be intact and unchanged in cache after 10ms'); - - cache.simulatedElapsedMs = 50; - expect(cache.hasData).to.be.equal(true, 'Must have data after waiting 50ms'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore, 'Data should be intact and unchanged in cache after 50ms'); - - cache.simulatedElapsedMs = 110; - expect(cache.hasData).to.be.equal(false, 'Must not have data after waiting 110ms'); - expect(cache.data).to.be.deep.equal(undefined, 'Must not have data stored after 100ms timeout expires.'); - }); - test(`Data is stored in cache (with workspaces): ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - (vsc.workspace as any).workspaceFolders = [{ index: 0, name: '1', uri: Uri.parse('wkfolder') }]; - vsc.workspace.getWorkspaceFolder = () => vsc.workspace.workspaceFolders![0]; - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - - cache.data = scenario.dataToStore; - - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - }); - test(`Data is stored in cache and different resources point to same storage location (without workspaces): ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const anotherResource = Uri.parse('b'); - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - const cache2 = new InMemoryInterpreterSpecificCache('Something', 10000, [anotherResource], vsc); - - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); - - cache.data = scenario.dataToStore; + teardown(() => clock.restore()); + test('Cached item should exist', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache2.hasData).to.be.equal(true, 'Must have data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - expect(cache2.data).to.be.deep.equal(scenario.dataToStore); + assert.strictEqual(cache.data, 'Hello World'); + assert.isOk(cache.hasData); }); - test(`Data is stored in cache and different resources point to same storage location (with workspaces): ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const anotherResource = Uri.parse('b'); - (vsc.workspace as any).workspaceFolders = [{ index: 0, name: '1', uri: Uri.parse('wkfolder') }]; - vsc.workspace.getWorkspaceFolder = () => vsc.workspace.workspaceFolders![0]; - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - const cache2 = new InMemoryInterpreterSpecificCache('Something', 10000, [anotherResource], vsc); + test('Cached item can be updated and should exist', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); + assert.strictEqual(cache.data, 'Hello World'); + assert.isOk(cache.hasData); - cache.data = scenario.dataToStore; + cache.data = 'Bye'; - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache2.hasData).to.be.equal(true, 'Must have data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - expect(cache2.data).to.be.deep.equal(scenario.dataToStore); + assert.strictEqual(cache.data, 'Bye'); + assert.isOk(cache.hasData); }); - test(`Data is stored in cache and different resources do not point to same storage location (with multiple workspaces): ${scenario.scenarioDesc}`, () => { - const pythonPath = 'Some Python Path'; - const vsc = createMockVSC(pythonPath); - const resource = Uri.parse('a'); - const anotherResource = Uri.parse('b'); - (vsc.workspace as any).workspaceFolders = [ - { index: 0, name: '1', uri: Uri.parse('wkfolder1') }, - { index: 1, name: '2', uri: Uri.parse('wkfolder2') } - ]; - vsc.workspace.getWorkspaceFolder = (res) => { - const index = res.fsPath === resource.fsPath ? 0 : 1; - return vsc.workspace.workspaceFolders![index]; - }; - const cache = new InMemoryInterpreterSpecificCache('Something', 10000, [resource], vsc); - const cache2 = new InMemoryInterpreterSpecificCache('Something', 10000, [anotherResource], vsc); + test('Cached item should not exist after time expires', () => { + const cache = new InMemoryCache(5_000); + cache.data = 'Hello World'; - expect(cache.hasData).to.be.equal(false, 'Must not have any data'); - expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); + assert.strictEqual(cache.data, 'Hello World'); + assert.isTrue(cache.hasData); - cache.data = scenario.dataToStore; + // Should not expire after 4.999s. + clock.tick(4_999); - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache2.hasData).to.be.equal(false, 'Must not have any data'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - expect(cache2.data).to.be.deep.equal(undefined, 'Must not have any data'); + assert.strictEqual(cache.data, 'Hello World'); + assert.isTrue(cache.hasData); - cache2.data = 'Store some other data'; + // Should expire after 5s (previous 4999ms + 1ms). + clock.tick(1); - expect(cache.hasData).to.be.equal(true, 'Must have data'); - expect(cache2.hasData).to.be.equal(true, 'Must have'); - expect(cache.data).to.be.deep.equal(scenario.dataToStore); - expect(cache2.data).to.be.deep.equal('Store some other data', 'Must have data'); + 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 index 0115e39fa47a..b1e86c4e2013 100644 --- a/src/test/common/utils/decorators.unit.test.ts +++ b/src/test/common/utils/decorators.unit.test.ts @@ -3,372 +3,328 @@ 'use strict'; -import { expect } from 'chai'; -import { Uri } from 'vscode'; -import { Resource } from '../../../client/common/types'; +import { expect, use } from 'chai'; +import * as chaiPromise from 'chai-as-promised'; import { clearCache } from '../../../client/common/utils/cacheUtils'; -import { - cacheResourceSpecificInterpreterData, makeDebounceAsyncDecorator, makeDebounceDecorator -} from '../../../client/common/utils/decorators'; +import { cache, makeDebounceAsyncDecorator, makeDebounceDecorator } from '../../../client/common/utils/decorators'; import { sleep } from '../../core'; +use(chaiPromise.default); -// tslint:disable:no-any max-func-body-length no-unnecessary-class suite('Common Utils - Decorators', function () { // For some reason, sometimes we have timeouts on CI. // Note: setTimeout and similar functions are not guaranteed to execute // at the precise time prescribed. - // tslint:disable-next-line: no-invalid-this - this.retries(3); - teardown(() => { - clearCache(); - }); - /* - * Time in milliseconds (from some arbitrary point in time for current process). - * Don't use new Date().getTime() to calculate differences in times. - * Similarly setTimeout doesn't always trigger at prescribed time (accuracy isn't guaranteed). - * This has an accuracy of around 2-20ms. - * However we're dealing with tests that need accuracy of 1ms. - * Use API that'll give us better accuracy when dealing with elapsed times. - * - * @returns {number} - */ - function getHighPrecisionTime(): number { - const currentTime = process.hrtime(); - // Convert seconds to ms and nanoseconds to ms. - return (currentTime[0] * 1000) + (currentTime[1] / 1000_000); - } - - /** - * setTimeout doesn't always trigger at prescribed time (accuracy isn't guaranteed). - * Allow a discrepancy of +-5%. - * Here's a simple test to prove this (this has been reported by others too): - * ```js - * // Execute the following around 100 times, you'll see at least one where elapsed time is < 100. - * const startTime = .... - * await new Promise(resolve = setTimeout(resolve, 100)) - * console.log(currentTime - startTijme) - * ``` - * - * @param {number} actualDelay - * @param {number} expectedDelay - */ - function assertElapsedTimeWithinRange(actualDelay: number, expectedDelay: number) { - const difference = actualDelay - expectedDelay; - if (difference >= 0) { - return; - } - expect(Math.abs(difference)).to.be.lessThan(expectedDelay * 0.05, `Actual delay ${actualDelay}, expected delay ${expectedDelay}, not within 5% of accuracy`); - } - function createMockVSC(pythonPath: string): typeof import('vscode') { - return { - workspace: { - getConfiguration: () => { - return { - get: () => { - return pythonPath; - }, - inspect: () => { - return { globalValue: pythonPath }; - } - }; - }, - getWorkspaceFolder: () => { - return; - } - }, - Uri: Uri - } as any; - } - test('Result must be cached when using cache decorator', async () => { - const vsc = createMockVSC(''); - class TestClass { - public invoked = false; - @cacheResourceSpecificInterpreterData('Something', 100000, vsc) - public async doSomething(_resource: Resource, a: number, b: number): Promise<number> { - this.invoked = true; - return a + b; - } - } - const cls = new TestClass(); - const uri = Uri.parse('a'); - const uri2 = Uri.parse('b'); - - let result = await cls.doSomething(uri, 1, 2); - expect(result).to.equal(3); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - - cls.invoked = false; - let result2 = await cls.doSomething(uri2, 2, 3); - expect(result2).to.equal(5); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - - cls.invoked = false; - result = await cls.doSomething(uri, 1, 2); - result2 = await cls.doSomething(uri2, 2, 3); - expect(result).to.equal(3); - expect(result2).to.equal(5); - expect(cls.invoked).to.equal(false, 'Must not be invoked'); - }); - test('Cache result must be cleared when cache expires', async () => { - const vsc = createMockVSC(''); + 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; - @cacheResourceSpecificInterpreterData('Something', 100, vsc) - public async doSomething(_resource: Resource, a: number, b: number): Promise<number> { + @cache(1000) + public async doSomething(a: number, b: number): Promise<number> { this.invoked = true; return a + b; } } - const cls = new TestClass(); - const uri = Uri.parse('a'); - let result = await cls.doSomething(uri, 1, 2); - - expect(result).to.equal(3); - expect(cls.invoked).to.equal(true, 'Must be invoked'); - - cls.invoked = false; - result = await cls.doSomething(uri, 1, 2); - - expect(result).to.equal(3); - expect(cls.invoked).to.equal(false, 'Must not be invoked'); - - await sleep(110); - - cls.invoked = false; - result = await cls.doSomething(uri, 1, 2); - - expect(result).to.equal(3); - expect(cls.invoked).to.equal(true, 'Must be invoked'); + 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); }); - // debounce() - // tslint:disable-next-line: max-classes-per-file - class Base { - public created: number; - public calls: string[]; - public timestamps: number[]; - constructor() { - this.created = getHighPrecisionTime(); - this.calls = []; - this.timestamps = []; + 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; } - 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) { + + /** + * 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; } - await sleep(delay); - } - if (timestamps.length < count) { - throw Error(`timed out after ${timeout}ms`); + expect(Math.abs(difference)).to.be.lessThan( + expectedDelay * 0.05, + `Actual delay ${actualDelay}, expected delay ${expectedDelay}, not within 5% of accuracy`, + ); } - } - test('Debounce: one sync call', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceDecorator(wait) - public run(): void { - this._addCall('run'); - } - } - const one = new One(); - const start = getHighPrecisionTime(); - one.run(); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - }); - test('Debounce: one async call & no wait', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceAsyncDecorator(wait) - public async run(): Promise<void> { - this._addCall('run'); + 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); } } - const one = new One(); - - const start = getHighPrecisionTime(); - let errored = false; - one.run().catch(() => errored = true); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - expect(errored).to.be.equal(false, 'Exception raised when there shouldn\'t have been any'); - }); - test('Debounce: one async call', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceAsyncDecorator(wait) - public async run(): Promise<void> { - this._addCall('run'); + 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`); } } - const one = new One(); - - const start = getHighPrecisionTime(); - await one.run(); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; + test('Debounce: one sync call', async () => { + const wait = 100; - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - }); - test('Debounce: one async call and ensure exceptions are re-thrown', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceAsyncDecorator(wait) - public async run(): Promise<void> { - this._addCall('run'); - throw new Error('Kaboom'); + class One extends Base { + @makeDebounceDecorator(wait) + public run(): void { + this._addCall('run'); + } } - } - const one = new One(); - - const start = getHighPrecisionTime(); - let capturedEx: Error | undefined; - await one.run().catch(ex => capturedEx = ex); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - expect(capturedEx).to.not.be.equal(undefined, 'Exception not re-thrown'); - }); - test('Debounce: multiple async calls', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceAsyncDecorator(wait) - public async run(): Promise<void> { - this._addCall('run'); + const one = new One(); + + const start = getHighPrecisionTime(); + 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); - one.run().catch(() => errored = true); - one.run().catch(() => errored = true); - one.run().catch(() => errored = true); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - expect(errored).to.be.equal(false, 'Exception raised when there shouldn\'t have been any'); - }); - test('Debounce: multiple async calls when awaiting on all', async function () { - - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceAsyncDecorator(wait) - public async run(): Promise<void> { - this._addCall('run'); + const one = new One(); + + const start = getHighPrecisionTime(); + 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 Promise.all([one.run(), one.run(), one.run(), one.run()]); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - }); - test('Debounce: multiple async calls & wait on some', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceAsyncDecorator(wait) - public async run(): Promise<void> { - this._addCall('run'); + const one = new One(); + + const start = getHighPrecisionTime(); + 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 errored = false; - one.run().catch(() => errored = true); - await one.run(); - one.run().catch(() => errored = true); - one.run().catch(() => errored = true); - await waitForCalls(one.timestamps, 2); - const delay = one.timestamps[1] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run', 'run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - expect(errored).to.be.equal(false, 'Exception raised when there shouldn\'t have been any'); - }); - test('Debounce: multiple calls grouped', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceDecorator(wait) - public run(): void { - this._addCall('run'); + const one = new One(); + + const start = getHighPrecisionTime(); + 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(); - one.run(); - one.run(); - one.run(); - await waitForCalls(one.timestamps, 1); - const delay = one.timestamps[0] - start; - - assertElapsedTimeWithinRange(delay, wait); - expect(one.calls).to.deep.equal(['run']); - expect(one.timestamps).to.have.lengthOf(one.calls.length); - }); - test('Debounce: multiple calls spread', async () => { - const wait = 100; - // tslint:disable-next-line:max-classes-per-file - class One extends Base { - @makeDebounceDecorator(wait) - public run(): void { - this._addCall('run'); + const one = new One(); + + 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 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); + 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); + 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/localize.functional.test.ts b/src/test/common/utils/localize.functional.test.ts deleted file mode 100644 index 0ce1d568b8ad..000000000000 --- a/src/test/common/utils/localize.functional.test.ts +++ /dev/null @@ -1,223 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - -import * as assert from 'assert'; -import * as fs from 'fs'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import * as localize from '../../../client/common/utils/localize'; - -const defaultNLSFile = path.join(EXTENSION_ROOT_DIR, 'package.nls.json'); - -// Defines a Mocha test suite to group tests of similar kind together -suite('Localization', () => { - // Note: We use package.nls.json by default for tests. Use the - // setLocale() helper to switch to a different locale. - - let localeFiles: string[]; - let nls_orig: string | undefined; - - setup(() => { - localeFiles = []; - - nls_orig = process.env.VSCODE_NLS_CONFIG; - setLocale('en-us'); - - // Ensure each test starts fresh. - localize._resetCollections(); - }); - - teardown(() => { - if (nls_orig) { - process.env.VSCODE_NLS_CONFIG = nls_orig; - } else { - delete process.env.VSCODE_NLS_CONFIG; - } - - const filenames = localeFiles; - localeFiles = []; - for (const filename of filenames) { - fs.unlinkSync(filename); - } - }); - - function addLocale(locale: string, nls: Record<string, string>) { - const filename = addLocaleFile(locale, nls); - localeFiles.push(filename); - } - - test('keys', done => { - const val = localize.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 - setLocale('it'); - - const val = localize.LanguageService.bannerLabelYes(); - assert.equal(val, 'Sì, prenderò il sondaggio ora', 'bannerLabelYes is not being translated'); - done(); - }); - - test('key found for locale', done => { - addLocale('spam', { - 'debug.selectConfigurationTitle': '???', - 'Common.gotIt': '!!!' - }); - setLocale('spam'); - - const title = localize.DebugConfigStrings.selectConfiguration.title(); - const gotIt = localize.Common.gotIt(); - - assert.equal(title, '???', 'not used'); - assert.equal(gotIt, '!!!', 'not used'); - done(); - }); - - test('key not found for locale (default used)', done => { - addLocale('spam', { - 'debug.selectConfigurationTitle': '???' - }); - setLocale('spam'); - - const gotIt = localize.Common.gotIt(); - - assert.equal(gotIt, 'Got it!', `default not used (got ${gotIt})`); - done(); - }); - - test('keys exist', done => { - // Read in the JSON object for the package.nls.json - const nlsCollection = getDefaultCollection(); - - // Now match all of our namespace entries to our nls entries - useEveryLocalization(localize); - - // Now verify all of the asked for keys exist - const askedFor = localize._getAskedForCollection(); - const missing: Record<string, string> = {}; - Object.keys(askedFor).forEach((key: string) => { - // Now check that this key exists somewhere in the nls collection - if (!nlsCollection[key]) { - missing[key] = askedFor[key]; - } - }); - - // If any missing keys, output an error - const missingKeys = Object.keys(missing); - if (missingKeys && missingKeys.length > 0) { - let message = 'Missing keys. Add the following to package.nls.json:\n'; - missingKeys.forEach((k: string) => { - message = message.concat(`\t"${k}" : "${missing[k]}",\n`); - }); - assert.fail(message); - } - - done(); - }); - - test('all keys used', function(done) { - // tslint:disable-next-line:no-suspicious-comment - // TODO: Unused keys need to be cleaned up. - // tslint:disable-next-line:no-invalid-this - this.skip(); - //test('all keys used', done => { - const nlsCollection = getDefaultCollection(); - useEveryLocalization(localize); - - // Now verify all of the asked for keys exist - const askedFor = localize._getAskedForCollection(); - const extra: Record<string, string> = {}; - Object.keys(nlsCollection).forEach((key: string) => { - // Now check that this key exists somewhere in the nls collection - if (askedFor[key]) { - return; - } - if (key.toLowerCase().indexOf('datascience') >= 0) { - return; - } - extra[key] = nlsCollection[key]; - }); - - // If any missing keys, output an error - const extraKeys = Object.keys(extra); - if (extraKeys && extraKeys.length > 0) { - let message = 'Unused keys. Remove the following from package.nls.json:\n'; - extraKeys.forEach((k: string) => { - message = message.concat(`\t"${k}" : "${extra[k]}",\n`); - }); - assert.fail(message); - } - - done(); - }); -}); - -function addLocaleFile(locale: string, nls: Record<string, string>) { - const filename = path.join(EXTENSION_ROOT_DIR, `package.nls.${locale}.json`); - if (fs.existsSync(filename)) { - throw Error('NLS file already exists'); - } - const contents = JSON.stringify(nls); - fs.writeFileSync(filename, contents); - return filename; -} - -function setLocale(locale: string) { - let nls: Record<string, string>; - if (process.env.VSCODE_NLS_CONFIG) { - nls = JSON.parse(process.env.VSCODE_NLS_CONFIG); - nls.locale = locale; - } else { - nls = { locale: locale }; - } - process.env.VSCODE_NLS_CONFIG = JSON.stringify(nls); -} - -function getDefaultCollection() { - if (!fs.existsSync(defaultNLSFile)) { - throw Error('package.nls.json is missing'); - } - const contents = fs.readFileSync(defaultNLSFile, 'utf8'); - return JSON.parse(contents); -} - -// tslint:disable-next-line:no-any -function useEveryLocalization(topns: any) { - // Read all of the namespaces from the localize import. - const entries = Object.keys(topns); - - // Now match all of our namespace entries to our nls entries. - entries.forEach((e: string) => { - // @ts-ignore - if (typeof topns[e] === 'function') { - return; - } - // It must be a namespace. - useEveryLocalizationInNS(topns[e]); - }); -} - -// tslint:disable-next-line:no-any -function useEveryLocalizationInNS(ns: any) { - // The namespace should have functions inside of it. - // @ts-ignore - const props = Object.keys(ns); - - // Run every function and cover every sub-namespace. - // This should fill up our "asked-for keys" collection. - props.forEach((key: string) => { - if (typeof ns[key] === 'function') { - const func = ns[key]; - func(); - } else { - useEveryLocalizationInNS(ns[key]); - } - }); -} diff --git a/src/test/common/utils/platform.unit.test.ts b/src/test/common/utils/platform.unit.test.ts new file mode 100644 index 000000000000..b27708978fc1 --- /dev/null +++ b/src/test/common/utils/platform.unit.test.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import { OSType, getOSType } from '../../../client/common/utils/platform'; + +suite('Utils for platform - getOSType function', () => { + const testsData = [ + { platform: 'linux', expected: OSType.Linux }, + { platform: 'darwin', expected: OSType.OSX }, + { platform: 'anunknownplatform', expected: OSType.Unknown }, + { platform: 'windows', expected: OSType.Windows }, + ]; + + testsData.forEach((testData) => { + test(`getOSType when platform is ${testData.platform}`, () => { + const osType = getOSType(testData.platform); + expect(osType).equal(testData.expected); + }); + }); +}); diff --git a/src/test/common/utils/regexp.unit.test.ts b/src/test/common/utils/regexp.unit.test.ts 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/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 2fce35335a72..3ba073d71474 100644 --- a/src/test/common/variables/envVarsProvider.multiroot.test.ts +++ b/src/test/common/variables/envVarsProvider.multiroot.test.ts @@ -4,18 +4,16 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; +import { anything } from 'ts-mockito'; import { ConfigurationTarget, Disposable, Uri, workspace } from 'vscode'; import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { IS_WINDOWS, NON_WINDOWS_PATH_VARIABLE_NAME, WINDOWS_PATH_VARIABLE_NAME } from '../../../client/common/platform/constants'; import { PlatformService } from '../../../client/common/platform/platformService'; +import { IFileSystem } from '../../../client/common/platform/types'; import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; -import { clearCache } from '../../../client/common/utils/cacheUtils'; +import { getSearchPathEnvVarNames } from '../../../client/common/utils/exec'; import { EnvironmentVariablesService } from '../../../client/common/variables/environment'; import { EnvironmentVariablesProvider } from '../../../client/common/variables/environmentVariablesProvider'; import { EnvironmentVariables } from '../../../client/common/variables/types'; -import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; import { IInterpreterAutoSelectionService } from '../../../client/interpreter/autoSelection/types'; import { clearPythonPathInWorkspaceFolder, isOs, OSType, updateSetting } from '../../common'; @@ -23,35 +21,47 @@ import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } fr import { MockAutoSelectionService } from '../../mocks/autoSelector'; import { MockProcess } from '../../mocks/process'; import { UnitTestIocContainer } from '../../testing/serviceRegistry'; +import { createTypeMoq } from '../../mocks/helper'; -use(chaiAsPromised); +use(chaiAsPromised.default); const multirootPath = path.join(__dirname, '..', '..', '..', '..', 'src', 'testMultiRootWkspc'); const workspace4Path = Uri.file(path.join(multirootPath, 'workspace4')); const workspace4PyFile = Uri.file(path.join(workspace4Path.fsPath, 'one.py')); -// tslint:disable-next-line:max-func-body-length suite('Multiroot Environment Variables Provider', () => { let ioc: UnitTestIocContainer; - const pathVariableName = IS_WINDOWS ? WINDOWS_PATH_VARIABLE_NAME : NON_WINDOWS_PATH_VARIABLE_NAME; + const pathVariableName = getSearchPathEnvVarNames()[0]; suiteSetup(async function () { if (!IS_MULTI_ROOT_TEST) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); + this.skip(); } await clearPythonPathInWorkspaceFolder(workspace4Path); await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initialize(); }); - setup(() => { + setup(async () => { ioc = new UnitTestIocContainer(); ioc.registerCommonTypes(); ioc.registerVariableTypes(); ioc.registerProcessTypes(); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - ioc.serviceManager.rebindInstance<IEnvironmentActivationService>(IEnvironmentActivationService, instance(mockEnvironmentActivationService)); - clearCache(); + 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); @@ -61,19 +71,23 @@ suite('Multiroot Environment Variables Provider', () => { await clearPythonPathInWorkspaceFolder(workspace4Path); await updateSetting('envFile', undefined, workspace4PyFile, ConfigurationTarget.WorkspaceFolder); await initializeTest(); - clearCache(); }); function getVariablesProvider(mockVariables: EnvironmentVariables = { ...process.env }) { const pathUtils = ioc.serviceContainer.get<IPathUtils>(IPathUtils); + const fs = ioc.serviceContainer.get<IFileSystem>(IFileSystem); const mockProcess = new MockProcess(mockVariables); - const variablesService = new EnvironmentVariablesService(pathUtils); + const variablesService = new EnvironmentVariablesService(pathUtils, fs); const disposables = ioc.serviceContainer.get<Disposable[]>(IDisposableRegistry); ioc.serviceManager.addSingletonInstance(IInterpreterAutoSelectionService, new MockAutoSelectionService()); - const cfgService = new ConfigurationService(ioc.serviceContainer); const workspaceService = new WorkspaceService(); - return new EnvironmentVariablesProvider(variablesService, disposables, - new PlatformService(), workspaceService, cfgService, mockProcess); + return new EnvironmentVariablesProvider( + variablesService, + disposables, + new PlatformService(), + workspaceService, + mockProcess, + ); } test('Custom variables should not be undefined without an env file', async () => { @@ -84,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) { @@ -99,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) { @@ -112,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) { @@ -136,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'; @@ -150,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'; @@ -169,10 +183,9 @@ suite('Multiroot Environment Variables Provider', () => { // this test is flaky on windows (likely the value of the path property // has incorrect path separator chars). Tracked by GH #4756 if (isOs(OSType.Windows)) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); + this.skip(); } - // tslint:disable-next-line:no-invalid-template-strings + await updateSetting('envFile', '${workspaceRoot}/.env', workspace4PyFile, ConfigurationTarget.WorkspaceFolder); const processVariables = { ...process.env }; processVariables.PYTHONPATH = '/usr/one/TWO'; @@ -187,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'; @@ -205,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) { @@ -227,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'; @@ -242,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) { @@ -265,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); @@ -283,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); @@ -294,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) { @@ -308,11 +320,11 @@ 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'); 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 54e0c61f9ac6..3709d97b9f62 100644 --- a/src/test/common/variables/envVarsService.unit.test.ts +++ b/src/test/common/variables/envVarsService.unit.test.ts @@ -6,231 +6,374 @@ 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, parseEnvFile } from '../../../client/common/variables/environment'; -import { IEnvironmentVariablesService } from '../../../client/common/variables/types'; -import { getOSType } from '../../common'; +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('Simple variable substitution is supported', async () => { - const vars = await variablesService.parseFile( - path.join(envFilesFolderPath, '.env6'), - { BINDIR: '/usr/bin' } - ); + expect(vars).to.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); - 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'); - }); + test('Custom variables should be not undefined with a valid environment file', async () => { + setFile(filename, '...'); - 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'); - }); + const vars = await variablesService.parseFile(filename); - test('Ensure path variabnles variables are not merged into target', async () => { - const pathVariable = pathUtils.getPathVariableName(); - const vars1 = { ONE: '1', TWO: 'TWO', PYTHONPATH: 'PYTHONPATH' }; - // tslint:disable-next-line:no-any - (vars1 as any)[pathVariable] = 'PATH'; - const vars2 = { ONE: 'ONE', THREE: '3' }; - variablesService.mergeVariables(vars1, vars2); - expect(Object.keys(vars1)).lengthOf(4, 'Source variables modified'); - expect(Object.keys(vars2)).lengthOf(3, 'Variables not merged'); - expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); - expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); - expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); - }); + expect(vars).to.not.equal(undefined, 'Variables should be undefined'); + verifyAll(); + }); - 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' }; - // tslint:disable-next-line:no-any - (vars2 as any)[pathVariable] = 'PATH'; - variablesService.mergeVariables(vars1, vars2); - expect(Object.keys(vars1)).lengthOf(2, 'Source variables modified'); - expect(Object.keys(vars2)).lengthOf(5, 'Variables not merged'); - expect(vars2).to.have.property('ONE', 'ONE', 'Variable overwritten'); - expect(vars2).to.have.property('TWO', 'TWO', 'Incorrect value'); - expect(vars2).to.have.property('THREE', '3', 'Variable not merged'); - expect(vars2).to.have.property('PYTHONPATH', 'PYTHONPATH', 'Incorrect value'); - expect(vars2).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - }); + test('Custom variables should be parsed from env file', async () => { + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + ` +X1234PYEXTUNITTESTVAR=1234 +PYTHONPATH=../workspace5 + `, + ); + + const vars = await variablesService.parseFile(filename); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + verifyAll(); + }); + + test('PATH and PYTHONPATH from env file should be returned as is', async () => { + const expectedPythonPath = '/usr/one/three:/usr/one/four'; + const expectedPath = '/usr/x:/usr/y'; + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + ` +X=1 +Y=2 +PYTHONPATH=/usr/one/three:/usr/one/four +# Unix PATH variable +PATH=/usr/x:/usr/y +# Windows Path variable +Path=/usr/x:/usr/y + `, + ); + + const vars = await variablesService.parseFile(filename); + + expect(vars).to.not.equal(undefined, 'Variables is is undefiend'); + expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); + expect(vars).to.have.property('X', '1', 'X value is invalid'); + expect(vars).to.have.property('Y', '2', 'Y value is invalid'); + expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); + expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); + verifyAll(); + }); + + test('Simple variable substitution is supported', async () => { + // src/testMultiRootWkspc/workspace4/.env + setFile( + filename, + + '\ +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(); + }); + }); + + 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 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'); + test('Ensure variables are merged', async () => { + const vars1 = { ONE: '1', TWO: 'TWO' }; + const vars2 = { ONE: 'ONE', THREE: '3' }; - variablesService.appendPath(vars, ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + variablesService.mergeVariables(vars1, vars2); - variablesService.appendPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - }); + 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 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'); + test('Ensure path variabnles variables are not merged into target', async () => { + const vars1 = { ONE: '1', TWO: 'TWO', PYTHONPATH: 'PYTHONPATH' }; - variablesService.appendPythonPath(vars, ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); + (vars1 as any)[pathVariable] = 'PATH'; + const vars2 = { ONE: 'ONE', THREE: '3' }; - variablesService.appendPythonPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - }); + variablesService.mergeVariables(vars1, vars2); - 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' }; - // tslint:disable-next-line:no-any - (vars as any)[pathVariable] = 'PATH'; - variablesService.appendPath(vars); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - - variablesService.appendPath(vars, ''); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - - variablesService.appendPath(vars, ' ', ''); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, 'PATH', 'Incorrect value'); - }); + 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 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'); + 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 PATH is appeneded', async () => { - const pathVariable = pathUtils.getPathVariableName(); - const vars = { ONE: '1' }; - // tslint:disable-next-line:no-any - (vars as any)[pathVariable] = 'PATH'; - const pathToAppend = `/usr/one${path.delimiter}/usr/three`; - variablesService.appendPath(vars, pathToAppend); - expect(Object.keys(vars)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('ONE', '1', 'Incorrect value'); - expect(vars).to.have.property(pathVariable, `PATH${path.delimiter}${pathToAppend}`, 'Incorrect value'); + 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 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'); + 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(); + }); }); }); -// tslint:disable-next-line:max-func-body-length suite('Parsing Environment Variables Files', () => { - - test('Custom variables should be parsed from env file', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + 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'); - }); + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('X1234PYEXTUNITTESTVAR', '1234', 'X1234PYEXTUNITTESTVAR value is invalid'); + expect(vars).to.have.property('PYTHONPATH', '../workspace5', 'PYTHONPATH value is invalid'); + }); - test('PATH and PYTHONPATH from env file should be returned as is', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + 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 @@ -240,23 +383,21 @@ PATH=/usr/x:/usr/y Path=/usr/x:/usr/y `); - const expectedPythonPath = '/usr/one/three:/usr/one/four'; - const expectedPath = '/usr/x:/usr/y'; - expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); - expect(vars).to.have.property('X', '1', 'X value is invalid'); - expect(vars).to.have.property('Y', '2', 'Y value is invalid'); - expect(vars).to.have.property('PYTHONPATH', expectedPythonPath, 'PYTHONPATH value is invalid'); - expect(vars).to.have.property('PATH', expectedPath, 'PATH value is invalid'); - }); - - test('Variable names must be alpha + alnum/underscore', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + 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 -_bogus1=... 1bogus2=... bogus 3=... bogus.4=... @@ -264,63 +405,61 @@ bogus-5=... bogus~6=... VAR1=3456 VAR_2=7890 +_VAR_3=1234 `); - expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(5, 'Incorrect number of variables'); - expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); - expect(vars).to.have.property('ham', '5678', 'value is invalid'); - expect(vars).to.have.property('Eggs', '9012', 'value is invalid'); - expect(vars).to.have.property('VAR1', '3456', 'value is invalid'); - expect(vars).to.have.property('VAR_2', '7890', 'value is invalid'); - }); - - test('Empty values become empty string', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + 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', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` -SPAM=1234 -HAM='5678' -EGGS="9012" -FOO='"3456"' -BAR="'7890'" -BAZ="\"ABCD" -VAR1="EFGH -VAR2=IJKL" + 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', '1234', 'value is invalid'); - expect(vars).to.have.property('HAM', '5678', 'value is invalid'); - expect(vars).to.have.property('EGGS', '9012', 'value is invalid'); - expect(vars).to.have.property('FOO', '"3456"', 'value is invalid'); - expect(vars).to.have.property('BAR', '\'7890\'', 'value is invalid'); - expect(vars).to.have.property('BAZ', '"ABCD', 'value is invalid'); - expect(vars).to.have.property('VAR1', '"EFGH', 'value is invalid'); - expect(vars).to.have.property('VAR2', 'IJKL"', 'value is invalid'); - // tslint:disable-next-line:no-suspicious-comment - // TODO: Should the outer marks be left? - expect(vars).to.have.property('VAR3', 'MN\'OP', 'value is invalid'); - expect(vars).to.have.property('VAR4', 'QR"ST', 'value is invalid'); - }); - - test('Whitespace is ignored', () => { - // tslint:disable:no-trailing-whitespace - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + 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 @@ -331,25 +470,22 @@ VAR1=EFGH ... VAR2=IJKL VAR3=' MNOP ' `); - // tslint:enable:no-trailing-whitespace - - expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(9, 'Incorrect number of variables'); - expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); - expect(vars).to.have.property('HAM', '5678', 'value is invalid'); - expect(vars).to.have.property('EGGS', '9012', 'value is invalid'); - expect(vars).to.have.property('FOO', '3456', 'value is invalid'); - expect(vars).to.have.property('BAR', '7890', 'value is invalid'); - expect(vars).to.have.property('BAZ', 'ABCD', 'value is invalid'); - expect(vars).to.have.property('VAR1', 'EFGH ...', 'value is invalid'); - expect(vars).to.have.property('VAR2', 'IJKL', 'value is invalid'); - expect(vars).to.have.property('VAR3', ' MNOP ', 'value is invalid'); - }); - test('Blank lines are ignored', () => { - // tslint:disable:no-trailing-whitespace - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + 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 @@ -357,17 +493,15 @@ HAM=5678 `); - // tslint:enable:no-trailing-whitespace - expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); - expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); - expect(vars).to.have.property('HAM', '5678', 'value is invalid'); - }); + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('HAM', '5678', 'value is invalid'); + }); - test('Comments are ignored', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile(` + test('Comments are ignored', () => { + const vars = parseEnvFile(` # step 1 SPAM=1234 # step 2 @@ -377,45 +511,63 @@ 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'); - }); - - // Substitution - // tslint:disable:no-invalid-template-strings - - test('Basic substitution syntax', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + 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', + ); + }); - 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', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + 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'); - }); + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); + expect(vars).to.have.property('EGGS', '$SPAM', 'value is invalid'); + }); - test('Nested substitution is not supported', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + test('Nested substitution is not supported', () => { + const vars = parseEnvFile( + '\ SPAM=EGGS \n\ EGGS=??? \n\ HAM1="-- ${${SPAM}} --"\n\ @@ -423,81 +575,94 @@ 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'); - }); + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(7, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', 'EGGS', 'value is invalid'); + expect(vars).to.have.property('EGGS', '???', 'value is invalid'); + expect(vars).to.have.property('HAM1', '-- ${${SPAM}} --', 'value is invalid'); + expect(vars).to.have.property('abcEGGSxyz', '!!!', 'value is invalid'); + expect(vars).to.have.property('HAM2', '-- ${abc${SPAM}xyz} --', 'value is invalid'); + expect(vars).to.have.property('HAM3', '-- ${${SPAM} --', 'value is invalid'); + expect(vars).to.have.property('HAM4', '-- ${${SPAM}} ${EGGS} --', 'value is invalid'); + }); - test('Other bad substitution syntax', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + 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'); - }); + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(6, 'Incorrect number of variables'); + expect(vars).to.have.property('SPAM', 'EGGS', 'value is invalid'); + expect(vars).to.have.property('EGGS', '???', 'value is invalid'); + expect(vars).to.have.property('HAM1', '${}', 'value is invalid'); + expect(vars).to.have.property('HAM2', '${', 'value is invalid'); + expect(vars).to.have.property('HAM3', '${SPAM+EGGS}', 'value is invalid'); + expect(vars).to.have.property('HAM4', '$SPAM', 'value is invalid'); + }); - test('Recursive substitution is allowed', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + 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'); - }); + ', + ); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + expect(Object.keys(vars!)).lengthOf(2, 'Incorrect number of variables'); + expect(vars).to.have.property('REPO', '/home/user/git/foobar', 'value is invalid'); + expect(vars).to.have.property( + 'PYTHONPATH', + '/home/user/git/foobar/foo:/home/user/git/foobar/bar', + 'value is invalid', + ); + }); - test('Substitution may be escaped', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + test('"$" may be escaped', () => { + const vars = parseEnvFile( + '\ SPAM=1234 \n\ EGGS=\\${SPAM}/foo:\\${SPAM}/bar \n\ -HAM=\$ ... $$ \n\ - '); - - expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(3, 'Incorrect number of variables'); - expect(vars).to.have.property('SPAM', '1234', 'value is invalid'); - expect(vars).to.have.property('EGGS', '${SPAM}/foo:${SPAM}/bar', 'value is invalid'); - expect(vars).to.have.property('HAM', '$ ... $$', 'value is invalid'); - }); +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', () => { - // tslint:disable-next-line:no-multiline-string - const vars = parseEnvFile('\ + test('base substitution variables', () => { + const vars = parseEnvFile('\ PYTHONPATH=${REPO}/foo:${REPO}/bar \n\ - ', { - REPO: '/home/user/git/foobar' + ', { + REPO: '/home/user/git/foobar', + }); + + expect(vars).to.not.equal(undefined, 'Variables is undefiend'); + 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', + ); }); - - expect(vars).to.not.equal(undefined, 'Variables is undefiend'); - expect(Object.keys(vars!)).lengthOf(1, 'Incorrect number of variables'); - expect(vars).to.have.property('PYTHONPATH', '/home/user/git/foobar/foo:/home/user/git/foobar/bar', 'value is invalid'); + }); }); - - // tslint:enable:no-invalid-template-strings }); diff --git a/src/test/common/variables/environmentVariablesProvider.unit.test.ts b/src/test/common/variables/environmentVariablesProvider.unit.test.ts index 0a79bbc6b522..bf02f5f867d7 100644 --- a/src/test/common/variables/environmentVariablesProvider.unit.test.ts +++ b/src/test/common/variables/environmentVariablesProvider.unit.test.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -5,47 +6,55 @@ import * as assert from 'assert'; import * as path from 'path'; +import * as sinon from 'sinon'; import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; import * as typemoq from 'typemoq'; import { ConfigurationChangeEvent, FileSystemWatcher, Uri } from 'vscode'; import { IWorkspaceService } from '../../../client/common/application/types'; import { WorkspaceService } from '../../../client/common/application/workspace'; -import { PythonSettings } from '../../../client/common/configSettings'; -import { ConfigurationService } from '../../../client/common/configuration/service'; import { PlatformService } from '../../../client/common/platform/platformService'; import { IPlatformService } from '../../../client/common/platform/types'; import { CurrentProcess } from '../../../client/common/process/currentProcess'; -import { IConfigurationService, ICurrentProcess, IPythonSettings } from '../../../client/common/types'; -import { clearCache } from '../../../client/common/utils/cacheUtils'; +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'; -// tslint:disable:no-any max-func-body-length suite('Multiroot Environment Variables Provider', () => { let provider: EnvironmentVariablesProvider; let envVarsService: IEnvironmentVariablesService; let platform: IPlatformService; let workspace: IWorkspaceService; - let configuration: IConfigurationService; let currentProcess: ICurrentProcess; - let settings: IPythonSettings; + let envFile: string; setup(() => { + envFile = ''; envVarsService = mock(EnvironmentVariablesService); platform = mock(PlatformService); workspace = mock(WorkspaceService); - configuration = mock(ConfigurationService); currentProcess = mock(CurrentProcess); - settings = mock(PythonSettings); - when(configuration.getSettings(anything())).thenReturn(instance(settings)); when(workspace.onDidChangeConfiguration).thenReturn(noop as any); - provider = new EnvironmentVariablesProvider(instance(envVarsService), [], instance(platform), - instance(workspace), instance(configuration), instance(currentProcess)); + 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(); + }); - clearCache(); + teardown(() => { + sinon.restore(); }); test('Event is fired when there are changes to settings', () => { @@ -55,62 +64,73 @@ suite('Multiroot Environment Variables Provider', () => { provider.trackedWorkspaceFolders.add(workspaceFolder1Uri.fsPath); provider.trackedWorkspaceFolders.add(workspaceFolder2Uri.fsPath); - provider.onDidEnvironmentVariablesChange(uri => affectedWorkspace = uri); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); const changedEvent: ConfigurationChangeEvent = { affectsConfiguration(setting: string, uri?: Uri) { return setting === 'python.envFile' && uri!.fsPath === workspaceFolder1Uri.fsPath; - } + }, }; provider.configurationChanged(changedEvent); assert.ok(affectedWorkspace); - assert.equal(affectedWorkspace!.fsPath, workspaceFolder1Uri.fsPath); + assert.strictEqual(affectedWorkspace!.fsPath, workspaceFolder1Uri.fsPath); }); test('Event is not fired when there are not changes to settings', () => { let affectedWorkspace: Uri | undefined; const workspaceFolderUri = Uri.file('workspace1'); provider.trackedWorkspaceFolders.add(workspaceFolderUri.fsPath); - provider.onDidEnvironmentVariablesChange(uri => affectedWorkspace = uri); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); const changedEvent: ConfigurationChangeEvent = { - affectsConfiguration(_setting: string, _uri?: Uri) { + affectsConfiguration() { return false; - } + }, }; provider.configurationChanged(changedEvent); - assert.equal(affectedWorkspace, undefined); + assert.strictEqual(affectedWorkspace, undefined); }); test('Event is not fired when workspace is not tracked', () => { let affectedWorkspace: Uri | undefined; - provider.onDidEnvironmentVariablesChange(uri => affectedWorkspace = uri); + provider.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); const changedEvent: ConfigurationChangeEvent = { - affectsConfiguration(_setting: string, _uri?: Uri) { + affectsConfiguration() { return true; - } + }, }; provider.configurationChanged(changedEvent); - assert.equal(affectedWorkspace, undefined); + assert.strictEqual(affectedWorkspace, undefined); }); - [undefined, Uri.file('workspace')].forEach(workspaceUri => { + [undefined, Uri.file('workspace')].forEach((workspaceUri) => { const workspaceTitle = workspaceUri ? '(with a workspace)' : '(without a workspace)'; test(`Event is fired when the environment file is modified ${workspaceTitle}`, () => { let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + // eslint-disable-next-line @typescript-eslint/ban-types let onChangeHandler: undefined | ((resource?: Uri) => Function); fileSystemWatcher - .setup(fs => fs.onDidChange(typemoq.It.isAny())) - .callback(cb => onChangeHandler = cb) + .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.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); provider.createFileWatcher(envFile, workspaceUri); @@ -119,21 +139,26 @@ suite('Multiroot Environment Variables Provider', () => { onChangeHandler!(); - assert.equal(affectedWorkspace, workspaceUri); + assert.strictEqual(affectedWorkspace, workspaceUri); }); test(`Event is fired when the environment file is deleted ${workspaceTitle}`, () => { let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + // eslint-disable-next-line @typescript-eslint/ban-types let onDeleted: undefined | ((resource?: Uri) => Function); fileSystemWatcher - .setup(fs => fs.onDidDelete(typemoq.It.isAny())) - .callback(cb => onDeleted = cb) + .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.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); provider.createFileWatcher(envFile, workspaceUri); @@ -142,21 +167,26 @@ suite('Multiroot Environment Variables Provider', () => { onDeleted!(); - assert.equal(affectedWorkspace, workspaceUri); + assert.strictEqual(affectedWorkspace, workspaceUri); }); test(`Event is fired when the environment file is created ${workspaceTitle}`, () => { let affectedWorkspace: Uri | undefined = Uri.file('dummy value'); - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); + // eslint-disable-next-line @typescript-eslint/ban-types let onCreated: undefined | ((resource?: Uri) => Function); fileSystemWatcher - .setup(fs => fs.onDidCreate(typemoq.It.isAny())) - .callback(cb => onCreated = cb) + .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.onDidEnvironmentVariablesChange((uri) => { + affectedWorkspace = uri; + }); provider.createFileWatcher(envFile, workspaceUri); @@ -165,21 +195,15 @@ suite('Multiroot Environment Variables Provider', () => { onCreated!(); - assert.equal(affectedWorkspace, workspaceUri); + assert.strictEqual(affectedWorkspace, workspaceUri); }); test(`File system watcher event handlers are added once ${workspaceTitle}`, () => { - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const fileSystemWatcher = typemoq.Mock.ofType<FileSystemWatcher>(); - fileSystemWatcher - .setup(fs => fs.onDidChange(typemoq.It.isAny())) - .verifiable(typemoq.Times.once()); - fileSystemWatcher - .setup(fs => fs.onDidCreate(typemoq.It.isAny())) - .verifiable(typemoq.Times.once()); - fileSystemWatcher - .setup(fs => fs.onDidDelete(typemoq.It.isAny())) - .verifiable(typemoq.Times.once()); + 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); @@ -193,12 +217,11 @@ suite('Multiroot Environment Variables Provider', () => { }); test(`Getting environment variables (without an envfile, without PATH in current env, without PYTHONPATH in current env) & ${workspaceTitle}`, async () => { - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; const currentProcEnv = { SOMETHING: 'wow' }; when(currentProcess.env).thenReturn(currentProcEnv); - when(settings.envFile).thenReturn(envFile); when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(undefined); when(platform.pathVariableName).thenReturn('PATH'); @@ -206,49 +229,44 @@ suite('Multiroot Environment Variables Provider', () => { const vars = await provider.getEnvironmentVariables(workspaceUri); verify(currentProcess.env).atLeast(1); - verify(settings.envFile).atLeast(1); verify(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual({}))).once(); verify(platform.pathVariableName).atLeast(1); assert.deepEqual(vars, {}); }); test(`Getting environment variables (with an envfile, without PATH in current env, without PYTHONPATH in current env) & ${workspaceTitle}`, async () => { - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; const currentProcEnv = { SOMETHING: 'wow' }; const envFileVars = { MY_FILE: '1234' }; when(currentProcess.env).thenReturn(currentProcEnv); - when(settings.envFile).thenReturn(envFile); when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); - when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(envFileVars); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenCall(async () => ({ ...envFileVars })); when(platform.pathVariableName).thenReturn('PATH'); const vars = await provider.getEnvironmentVariables(workspaceUri); verify(currentProcess.env).atLeast(1); - verify(settings.envFile).atLeast(1); verify(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual(envFileVars))).once(); verify(platform.pathVariableName).atLeast(1); assert.deepEqual(vars, envFileVars); }); test(`Getting environment variables (with an envfile, with PATH in current env, with PYTHONPATH in current env) & ${workspaceTitle}`, async () => { - const envFile = path.join('a', 'b', 'env.file'); + envFile = path.join('a', 'b', 'env.file'); const workspaceFolder = workspaceUri ? { name: '', index: 0, uri: workspaceUri } : undefined; const currentProcEnv = { SOMETHING: 'wow', PATH: 'some path value', PYTHONPATH: 'some python path value' }; const envFileVars = { MY_FILE: '1234' }; when(currentProcess.env).thenReturn(currentProcEnv); - when(settings.envFile).thenReturn(envFile); when(workspace.getWorkspaceFolder(workspaceUri)).thenReturn(workspaceFolder); - when(envVarsService.parseFile(envFile, currentProcEnv)).thenResolve(envFileVars); + when(envVarsService.parseFile(envFile, currentProcEnv)).thenCall(async () => ({ ...envFileVars })); when(platform.pathVariableName).thenReturn('PATH'); const vars = await provider.getEnvironmentVariables(workspaceUri); verify(currentProcess.env).atLeast(1); - verify(settings.envFile).atLeast(1); verify(envVarsService.parseFile(envFile, currentProcEnv)).atLeast(1); verify(envVarsService.mergeVariables(deepEqual(currentProcEnv), deepEqual(envFileVars))).once(); verify(envVarsService.appendPath(deepEqual(envFileVars), currentProcEnv.PATH)).once(); @@ -256,5 +274,112 @@ suite('Multiroot Environment Variables Provider', () => { 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/color.test.ts b/src/test/datascience/color.test.ts deleted file mode 100644 index 7e78e84df854..000000000000 --- a/src/test/datascience/color.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { assert } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { WorkspaceConfiguration } from 'vscode'; - -import { Extensions } from '../../client/common/application/extensions'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { Logger } from '../../client/common/logger'; -import { CurrentProcess } from '../../client/common/process/currentProcess'; -import { IConfigurationService } from '../../client/common/types'; -import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; -import { ThemeFinder } from '../../client/datascience/themeFinder'; -import { IThemeFinder } from '../../client/datascience/types'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; - -// tslint:disable:max-func-body-length -suite('Theme colors', () => { - let themeFinder: ThemeFinder; - let extensions : Extensions; - let currentProcess : CurrentProcess; - let logger : Logger; - let workspaceService : TypeMoq.IMock<IWorkspaceService>; - let workspaceConfig : TypeMoq.IMock<WorkspaceConfiguration>; - let cssGenerator: CodeCssGenerator; - let configService : TypeMoq.IMock<IConfigurationService>; - const settings : PythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); - - setup(() => { - extensions = new Extensions(); - currentProcess = new CurrentProcess(); - logger = new Logger(); - themeFinder = new ThemeFinder(extensions, currentProcess, logger); - - workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceConfig.setup(ws => ws.has(TypeMoq.It.isAnyString())) - .returns(() => { - return false; - }); - workspaceConfig.setup(ws => ws.get(TypeMoq.It.isAnyString())) - .returns(() => { - return undefined; - }); - workspaceConfig.setup(ws => ws.get(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((_s, d) => { - return d; - }); - - settings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 20000, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - enablePlotViewer: true - }; - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings); - - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - workspaceService.setup(c => c.getConfiguration(TypeMoq.It.isAny())).returns(() => workspaceConfig.object); - workspaceService.setup(c => c.getConfiguration(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => workspaceConfig.object); - - cssGenerator = new CodeCssGenerator(workspaceService.object, themeFinder, configService.object, logger); - }); - - function runTest(themeName: string, isDark: boolean, shouldExist: boolean) { - test(themeName, async () => { - const json = await themeFinder.findThemeRootJson(themeName); - if (shouldExist) { - assert.ok(json, `Cannot find theme ${themeName}`); - const actuallyDark = await themeFinder.isThemeDark(themeName); - assert.equal(actuallyDark, isDark, `Theme ${themeName} darkness is not ${isDark}`); - workspaceConfig.reset(); - workspaceConfig.setup(ws => ws.get<string>(TypeMoq.It.isValue('colorTheme'))).returns(() => { - return themeName; - }); - workspaceConfig.setup(ws => ws.get<string>(TypeMoq.It.isValue('fontFamily'))).returns(() => { - return 'Arial'; - }); - workspaceConfig.setup(ws => ws.get<number>(TypeMoq.It.isValue('fontSize'))).returns(() => { - return 16; - }); - workspaceConfig.setup(ws => ws.get(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) - .returns((_s, d) => { - return d; - }); - const theme = await cssGenerator.generateMonacoTheme(isDark, themeName); - assert.ok(theme, `Cannot find monaco theme for ${themeName}`); - const colors = await cssGenerator.generateThemeCss(isDark, themeName); - assert.ok(colors, 'Cannot find theme colors for Kimbie Dark'); - - // Make sure we have a string value that is not set to a variable - // (that would be the default and all themes have a string color) - assert.ok(theme.rules, 'No rules found in monaco theme'); - // tslint:disable-next-line: no-any - const commentPunctuation = (theme.rules as any[]).findIndex(r => r.token === 'punctuation.definition.comment'); - assert.ok(commentPunctuation >= 0, 'No punctuation.comment found'); - } else { - assert.notOk(json, `Found ${themeName} when not expected`); - } - }); - } - - // One test per known theme - runTest('Light (Visual Studio)', false, true); - runTest('Light+ (default light)', false, true); - runTest('Quiet Light', false, true); - runTest('Solarized Light', false, true); - runTest('Abyss', true, true); - runTest('Dark (Visual Studio)', true, true); - runTest('Dark+ (default dark)', true, true); - runTest('Kimbie Dark', true, true); - runTest('Monokai', true, true); - runTest('Monokai Dimmed', true, true); - runTest('Red', true, true); - runTest('Solarized Dark', true, true); - runTest('Tomorrow Night Blue', true, true); - - // One test to make sure unknown themes don't return a value. - runTest('Knight Rider', true, false); - - // Test for when theme's json can't be found. - test('Missing json theme', async () => { - const mockThemeFinder = TypeMoq.Mock.ofType<IThemeFinder>(); - mockThemeFinder.setup(m => m.isThemeDark(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(false)); - mockThemeFinder.setup(m => m.findThemeRootJson(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(undefined)); - - cssGenerator = new CodeCssGenerator(workspaceService.object, mockThemeFinder.object, configService.object, logger); - - const colors = await cssGenerator.generateThemeCss(false, 'Kimbie Dark'); - assert.ok(colors, 'Cannot find theme colors for Kimbie Dark'); - - // Make sure we have a string value that is not set to a variable - // (that would be the default and all themes have a string color) - const matches = /--code-string-color\:\s(.*?);/gm.exec(colors); - assert.ok(matches, 'No matches found for string color'); - assert.equal(matches!.length, 2, 'Wrong number of matches for for string color'); - assert.ok(matches![1].includes('#'), 'String color not found'); - }); - -}); diff --git a/src/test/datascience/dataScienceIocContainer.ts b/src/test/datascience/dataScienceIocContainer.ts deleted file mode 100644 index 25d6d59d4ce7..000000000000 --- a/src/test/datascience/dataScienceIocContainer.ts +++ /dev/null @@ -1,698 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -//tslint:disable:trailing-comma no-any -import * as child_process from 'child_process'; -import { ReactWrapper } from 'enzyme'; -import { interfaces } from 'inversify'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import * as TypeMoq from 'typemoq'; -import { - ConfigurationChangeEvent, - Disposable, - Event, - EventEmitter, - FileSystemWatcher, - Uri, - ViewColumn, - WorkspaceConfiguration, - WorkspaceFolder -} from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../client/activation/types'; -import { DebugService } from '../../client/common/application/debugService'; -import { TerminalManager } from '../../client/common/application/terminalManager'; -import { - IApplicationShell, - ICommandManager, - IDebugService, - IDocumentManager, - ILiveShareApi, - ILiveShareTestingApi, - ITerminalManager, - IWebPanel, - IWebPanelMessageListener, - IWebPanelProvider, - IWorkspaceService, - WebPanelMessage -} 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 { Logger } from '../../client/common/logger'; -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 { - CondaActivationCommandProvider -} from '../../client/common/terminal/environmentActivationProviders/condaActivationProvider'; -import { - PipEnvActivationCommandProvider -} from '../../client/common/terminal/environmentActivationProviders/pipEnvActivationProvider'; -import { - PyEnvActivationCommandProvider -} from '../../client/common/terminal/environmentActivationProviders/pyenvActivationProvider'; -import { TerminalHelper } from '../../client/common/terminal/helper'; -import { - ITerminalActivationCommandProvider, - ITerminalHelper, - TerminalActivationProviders -} from '../../client/common/terminal/types'; -import { - IAsyncDisposableRegistry, - IConfigurationService, - ICurrentProcess, - IExtensions, - ILogger, - IPathUtils, - IPersistentStateFactory, - IsWindows -} from '../../client/common/types'; -import { Deferred, sleep } from '../../client/common/utils/async'; -import { noop } from '../../client/common/utils/misc'; -import { Architecture } from '../../client/common/utils/platform'; -import { EnvironmentVariablesService } from '../../client/common/variables/environment'; -import { EnvironmentVariablesProvider } from '../../client/common/variables/environmentVariablesProvider'; -import { IEnvironmentVariablesProvider, IEnvironmentVariablesService } from '../../client/common/variables/types'; -import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; -import { DataViewer } from '../../client/datascience/data-viewing/dataViewer'; -import { DataViewerProvider } from '../../client/datascience/data-viewing/dataViewerProvider'; -import { CellHashProvider } from '../../client/datascience/editor-integration/cellhashprovider'; -import { CodeLensFactory } from '../../client/datascience/editor-integration/codeLensFactory'; -import { CodeWatcher } from '../../client/datascience/editor-integration/codewatcher'; -import { - DotNetIntellisenseProvider -} from '../../client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider'; -import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; -import { - InteractiveWindowCommandListener -} from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; -import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; -import { JupyterCommandFactory } from '../../client/datascience/jupyter/jupyterCommand'; -import { JupyterDebugger } from '../../client/datascience/jupyter/jupyterDebugger'; -import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; -import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; -import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; -import { JupyterPasswordConnect } from '../../client/datascience/jupyter/jupyterPasswordConnect'; -import { JupyterServerFactory } from '../../client/datascience/jupyter/jupyterServerFactory'; -import { JupyterSessionManager } from '../../client/datascience/jupyter/jupyterSessionManager'; -import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables'; -import { PlotViewer } from '../../client/datascience/plotting/plotViewer'; -import { PlotViewerProvider } from '../../client/datascience/plotting/plotViewerProvider'; -import { StatusProvider } from '../../client/datascience/statusProvider'; -import { ThemeFinder } from '../../client/datascience/themeFinder'; -import { - ICellHashProvider, - ICodeCssGenerator, - ICodeLensFactory, - ICodeWatcher, - IDataScience, - IDataScienceCommandListener, - IDataViewer, - IDataViewerProvider, - IInteractiveWindow, - IInteractiveWindowListener, - IInteractiveWindowProvider, - IJupyterCommandFactory, - IJupyterDebugger, - IJupyterExecution, - IJupyterPasswordConnect, - IJupyterSessionManager, - IJupyterVariables, - INotebookExporter, - INotebookImporter, - INotebookServer, - IPlotViewer, - IPlotViewerProvider, - IStatusProvider, - IThemeFinder -} from '../../client/datascience/types'; -import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; -import { IEnvironmentActivationService } from '../../client/interpreter/activation/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, - InterpreterType, - IPipEnvService, - IVirtualEnvironmentsSearchPathProvider, - KNOWN_PATH_SERVICE, - PIPENV_SERVICE, - PythonInterpreter, - 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 { PipEnvServiceHelper } from '../../client/interpreter/locators/services/pipEnvServiceHelper'; -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 { IPipEnvServiceHelper, IPythonInPathCommandProvider } from '../../client/interpreter/locators/types'; -import { VirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs'; -import { IVirtualEnvironmentManager } from '../../client/interpreter/virtualEnvs/types'; -import { CodeExecutionHelper } from '../../client/terminals/codeExecution/helper'; -import { ICodeExecutionHelper } from '../../client/terminals/types'; -import { IVsCodeApi } from '../../datascience-ui/react-common/postOffice'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; -import { MockCommandManager } from './mockCommandManager'; -import { MockDocumentManager } from './mockDocumentManager'; -import { MockExtensions } from './mockExtensions'; -import { MockJupyterManager, SupportedCommands } from './mockJupyterManager'; -import { MockLanguageServer } from './mockLanguageServer'; -import { MockLanguageServerAnalysisOptions } from './mockLanguageServerAnalysisOptions'; -import { MockLiveShareApi } from './mockLiveShare'; -import { blurWindow, createMessageEvent } from './reactHelpers'; - -export class DataScienceIocContainer extends UnitTestIocContainer { - - public webPanelListener: IWebPanelMessageListener | undefined; - public wrapper: ReactWrapper<any, Readonly<{}>, React.Component> | undefined; - public wrapperCreatedPromise: Deferred<boolean> | undefined; - public postMessage: ((ev: MessageEvent) => void) | undefined; - // tslint:disable-next-line:no-any - private missedMessages : any[] = []; - private pythonSettings = new class extends PythonSettings { - public fireChangeEvent() { - this.changed.fire(); - } - }(undefined, new MockAutoSelectionService()); - private commandManager: MockCommandManager = new MockCommandManager(); - private setContexts: Record<string, boolean> = {}; - private contextSetEvent: EventEmitter<{ name: string; value: boolean }> = new EventEmitter<{ name: string; value: boolean }>(); - private jupyterMock: MockJupyterManager | undefined; - private shouldMockJupyter: boolean; - private asyncRegistry: AsyncDisposableRegistry; - private configChangeEvent = new EventEmitter<ConfigurationChangeEvent>(); - private documentManager = new MockDocumentManager(); - private workingPython: PythonInterpreter = { - path: '/foo/bar/python.exe', - version: new SemVer('3.6.6-final'), - sysVersion: '1.0.0.0', - sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64, - }; - private extraListeners: ((m: string, p: any) => void)[] = []; - 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(); - - // Blur window focus so we don't have editors polling - blurWindow(); - - if (this.wrapper && this.wrapper.length) { - this.wrapper.unmount(); - this.wrapper = undefined; - } - - // Bounce this so that our editor has time to shutdown - await sleep(10); - - // Clear out the monaco global services. Some of these services are preventing shutdown. - // tslint:disable: no-require-imports - const services = require('monaco-editor/esm/vs/editor/standalone/browser/standaloneServices') as any; - if (services.StaticServices) { - const keys = Object.keys(services.StaticServices); - keys.forEach(k => { - const service = services.StaticServices[k] as any; - if (service && service._value && service._value.dispose) { - if (typeof service._value.dispose === 'function') { - service._value.dispose(); - } - } - }); - } - - // This file doesn't have an export so we can't force a dispose. Instead it has a 5 second timeout - const config = require('monaco-editor/esm/vs/editor/browser/config/configuration') as any; - if (config.getCSSBasedConfiguration) { - config.getCSSBasedConfiguration().dispose(); - } - } - - //tslint:disable:max-func-body-length - public registerDataScienceTypes() { - this.registerFileSystemTypes(); - this.serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecutionFactory); - this.serviceManager.addSingleton<IInteractiveWindowProvider>(IInteractiveWindowProvider, InteractiveWindowProvider); - this.serviceManager.addSingleton<IDataViewerProvider>(IDataViewerProvider, DataViewerProvider); - this.serviceManager.addSingleton<IPlotViewerProvider>(IPlotViewerProvider, PlotViewerProvider); - this.serviceManager.addSingleton<ILogger>(ILogger, Logger); - this.serviceManager.add<IInteractiveWindow>(IInteractiveWindow, InteractiveWindow); - this.serviceManager.add<IDataViewer>(IDataViewer, DataViewer); - this.serviceManager.add<IPlotViewer>(IPlotViewer, PlotViewer); - this.serviceManager.add<INotebookImporter>(INotebookImporter, JupyterImporter); - this.serviceManager.add<INotebookExporter>(INotebookExporter, JupyterExporter); - this.serviceManager.addSingleton<ILiveShareApi>(ILiveShareApi, MockLiveShareApi); - this.serviceManager.addSingleton<IExtensions>(IExtensions, MockExtensions); - this.serviceManager.add<INotebookServer>(INotebookServer, JupyterServerFactory); - this.serviceManager.add<IJupyterCommandFactory>(IJupyterCommandFactory, JupyterCommandFactory); - this.serviceManager.addSingleton<IThemeFinder>(IThemeFinder, ThemeFinder); - 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); - this.serviceManager.addSingleton<IEnvironmentActivationService>(IEnvironmentActivationService, EnvironmentActivationService); - this.serviceManager.add<ICodeWatcher>(ICodeWatcher, CodeWatcher); - this.serviceManager.add<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper); - this.serviceManager.add<IDataScienceCommandListener>(IDataScienceCommandListener, InteractiveWindowCommandListener); - this.serviceManager.addSingleton<IJupyterVariables>(IJupyterVariables, JupyterVariables); - this.serviceManager.addSingleton<IJupyterDebugger>(IJupyterDebugger, JupyterDebugger); - - this.serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, Bash, TerminalActivationProviders.bashCShellFish); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, CommandPromptAndPowerShell, TerminalActivationProviders.commandPromptAndPowerShell); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, PyEnvActivationCommandProvider, TerminalActivationProviders.pyenv); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, CondaActivationCommandProvider, TerminalActivationProviders.conda); - this.serviceManager.addSingleton<ITerminalActivationCommandProvider>( - ITerminalActivationCommandProvider, PipEnvActivationCommandProvider, TerminalActivationProviders.pipenv); - this.serviceManager.addSingleton<ITerminalManager>(ITerminalManager, TerminalManager); - this.serviceManager.addSingleton<IPipEnvServiceHelper>(IPipEnvServiceHelper, PipEnvServiceHelper); - this.serviceManager.addSingleton<ILanguageServer>(ILanguageServer, MockLanguageServer); - this.serviceManager.addSingleton<ILanguageServerAnalysisOptions>(ILanguageServerAnalysisOptions, MockLanguageServerAnalysisOptions); - this.serviceManager.add<IInteractiveWindowListener>(IInteractiveWindowListener, DotNetIntellisenseProvider); - this.serviceManager.addSingleton<IDebugService>(IDebugService, DebugService); - this.serviceManager.addSingleton<ICellHashProvider>(ICellHashProvider, CellHashProvider); - this.serviceManager.addBinding<ICellHashProvider, IInteractiveWindowListener>(ICellHashProvider, IInteractiveWindowListener); - this.serviceManager.addSingleton<ICodeLensFactory>(ICodeLensFactory, CodeLensFactory); - - // 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 condaService = TypeMoq.Mock.ofType<ICondaService>(); - const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - const configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - const interpreterDisplay = TypeMoq.Mock.ofType<IInterpreterDisplay>(); - const datascience = TypeMoq.Mock.ofType<IDataScience>(); - - // Setup default settings - this.pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 20000, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - liveShareConnectionTimeout: 100, - autoPreviewNotebooksInInteractivePane: true, - enablePlotViewer: 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); - workspaceService.setup(w => w.onDidChangeConfiguration).returns(() => this.configChangeEvent.event); - interpreterDisplay.setup(i => i.refresh(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - const startTime = Date.now(); - datascience.setup(d => d.activationStartTime).returns(() => startTime); - - 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(() => '~'); - - // 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'); - this.pythonSettings.terminal = { - executeInFileDir: false, - launchArgs: [], - activateEnvironment: true - }; - - 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); - - this.serviceManager.addSingleton<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider, EnvironmentVariablesProvider); - this.serviceManager.addSingleton<IVirtualEnvironmentsSearchPathProvider>(IVirtualEnvironmentsSearchPathProvider, GlobalVirtualEnvironmentsSearchPathProvider, 'global'); - this.serviceManager.addSingleton<IVirtualEnvironmentsSearchPathProvider>(IVirtualEnvironmentsSearchPathProvider, WorkspaceVirtualEnvironmentsSearchPathProvider, 'workspace'); - this.serviceManager.addSingleton<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, VirtualEnvironmentManager); - - this.serviceManager.addSingletonInstance<ICondaService>(ICondaService, condaService.object); - this.serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - this.serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, this.documentManager); - this.serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspaceService.object); - this.serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configurationService.object); - this.serviceManager.addSingletonInstance<IDataScience>(IDataScience, datascience.object); - this.serviceManager.addSingleton<IBufferDecoder>(IBufferDecoder, BufferDecoder); - this.serviceManager.addSingleton<IEnvironmentVariablesService>(IEnvironmentVariablesService, EnvironmentVariablesService); - this.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); - this.serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); - - 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); - - 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); - this.serviceManager.addSingleton<IJupyterPasswordConnect>(IJupyterPasswordConnect, JupyterPasswordConnect); - } - - if (this.serviceManager.get<IPlatformService>(IPlatformService).isWindows) { - this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); - } - - const dummyDisposable = { - dispose: () => { return; } - }; - - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => { throw e; }); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); - appShell.setup(a => a.showSaveDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(Uri.file('test.ipynb'))); - appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - appShell.setup(a => a.showInputBox(TypeMoq.It.isAny())).returns(() => Promise.resolve('Python')); - - const interpreterManager = this.serviceContainer.get<IInterpreterService>(IInterpreterService); - interpreterManager.initialize(); - - if (this.mockJupyter) { - this.mockJupyter.addInterpreter(this.workingPython, SupportedCommands.all); - } - } - - // tslint:disable:any - public createWebView(mount: () => ReactWrapper<any, Readonly<{}>, React.Component>, role: vsls.Role = vsls.Role.None) { - - // Force the container to mock actual live share if necessary - if (role !== vsls.Role.None) { - const liveShareTest = this.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; - liveShareTest.forceRole(role); - } - - const webPanelProvider = TypeMoq.Mock.ofType<IWebPanelProvider>(); - const webPanel = TypeMoq.Mock.ofType<IWebPanel>(); - - this.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.isAny(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString(), TypeMoq.It.isAny())).returns( - (_viewColumn: ViewColumn, listener: IWebPanelMessageListener, _title: string, _script: string, _css: string) => { - // Keep track of the current listener. It listens to messages through the vscode api - this.webPanelListener = listener; - - // Send messages that were already posted but were missed. - // During normal operation, the react control will not be created before - // the webPanelListener - if (this.missedMessages.length && this.webPanelListener) { - this.missedMessages.forEach(m => this.webPanelListener ? this.webPanelListener.onMessage(m.type, m.payload) : noop()); - - // Note, you might think we should clean up the messages. However since the mount only occurs once, we might - // create multiple webpanels with the same mount. We need to resend these messages to - // other webpanels that get created with the same mount. - } - - // Return our dummy web panel - return webPanel.object; - }); - webPanel.setup(p => p.postMessage(TypeMoq.It.isAny())).callback((m: WebPanelMessage) => { - const message = createMessageEvent(m); - if (this.postMessage) { - this.postMessage(message); - } else { - throw new Error('postMessage callback not defined'); - } - }); - webPanel.setup(p => p.show(true)); - - // We need to mount the react control before we even create an interactive window object. Otherwise the mount will miss rendering some parts - this.mountReactControl(mount); - } - - 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 getSettings() { - return this.pythonSettings; - } - - public forceSettingsChanged(newPath: string) { - this.pythonSettings.pythonPath = newPath; - this.pythonSettings.fireChangeEvent(); - this.configChangeEvent.fire({ - affectsConfiguration(_s: string, _r?: Uri) : boolean { - return true; - } - }); - } - - public get mockJupyter(): MockJupyterManager | undefined { - return this.jupyterMock; - } - - public get<T>(serviceIdentifier: interfaces.ServiceIdentifier<T>, name?: string | number | symbol) : T { - return this.serviceManager.get<T>(serviceIdentifier, name); - } - - public addDocument(code: string, file: string) { - this.documentManager.addDocument(code, file); - } - - public addMessageListener(callback: (m: string, p: any) => void) { - this.extraListeners.push(callback); - } - - public changeJediEnabled(enabled: boolean) { - this.pythonSettings.jediEnabled = enabled; - } - - 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'; - } - } - - private postMessageToWebPanel(msg: any) { - if (this.webPanelListener) { - this.webPanelListener.onMessage(msg.type, msg.payload); - } else { - this.missedMessages.push(msg); - } - - if (this.extraListeners.length) { - this.extraListeners.forEach(e => e(msg.type, msg.payload)); - } - if (this.wrapperCreatedPromise && !this.wrapperCreatedPromise.resolved) { - this.wrapperCreatedPromise.resolve(); - } - } - - private mountReactControl(mount: () => ReactWrapper<any, Readonly<{}>, React.Component>) { - // This is a remount (or first time). Clear out messages that were sent - // by the last mount - this.missedMessages = []; - - // Setup the acquireVsCodeApi. The react control will cache this value when it's mounted. - const globalAcquireVsCodeApi = (): IVsCodeApi => { - return { - // tslint:disable-next-line:no-any - postMessage: (msg: any) => { - this.postMessageToWebPanel(msg); - }, - // tslint:disable-next-line:no-any no-empty - setState: (_msg: any) => { - - }, - // tslint:disable-next-line:no-any no-empty - getState: () => { - return {}; - } - }; - }; - // tslint:disable-next-line:no-string-literal - (global as any)['acquireVsCodeApi'] = globalAcquireVsCodeApi; - - // Remap event handlers to point to the container. - const oldListener = window.addEventListener; - window.addEventListener = (event: string, cb: any) => { - if (event === 'message') { - this.postMessage = cb; - } - }; - - // Mount our main panel. This will make the global api be cached and have the event handler registered - this.wrapper = mount(); - - // We can remove the global api and event listener now. - delete (global as any).acquireVsCodeApi; - window.addEventListener = oldListener; - } -} diff --git a/src/test/datascience/datascience.unit.test.ts b/src/test/datascience/datascience.unit.test.ts deleted file mode 100644 index 07bfb0e849cf..000000000000 --- a/src/test/datascience/datascience.unit.test.ts +++ /dev/null @@ -1,232 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { assert } from 'chai'; - -import { generateCells } from '../../client/datascience/cellFactory'; -import { formatStreamText, stripComments } from '../../client/datascience/common'; -import { InputHistory } from '../../datascience-ui/history-react/inputHistory'; - -// tslint:disable: max-func-body-length -suite('Data Science Tests', () => { - - test('formatting stream text', async () => { - assert.equal(formatStreamText('\rExecute\rExecute 1'), 'Execute 1'); - assert.equal(formatStreamText('\rExecute\r\nExecute 2'), 'Execute\nExecute 2'); - assert.equal(formatStreamText('\rExecute\rExecute\r\nExecute 3'), 'Execute\nExecute 3'); - assert.equal(formatStreamText('\rExecute\rExecute\nExecute 4'), 'Execute\nExecute 4'); - assert.equal(formatStreamText('\rExecute\r\r \r\rExecute\nExecute 5'), 'Execute\nExecute 5'); - assert.equal(formatStreamText('\rExecute\rExecute\nExecute 6\rExecute 7'), 'Execute\nExecute 7'); - assert.equal(formatStreamText('\rExecute\rExecute\nExecute 8\rExecute 9\r\r'), 'Execute\n'); - assert.equal(formatStreamText('\rExecute\rExecute\nExecute 10\rExecute 11\r\n'), 'Execute\nExecute 11\n'); - }); - - test('input history', async () => { - let history = new InputHistory(); - history.add('1', true); - history.add('2', true); - history.add('3', true); - history.add('4', true); - assert.equal(history.completeDown('5'), '5'); - history.add('5', true); - assert.equal(history.completeUp(''), '5'); - history.add('5', false); - assert.equal(history.completeUp('5'), '5'); - assert.equal(history.completeUp('4'), '4'); - assert.equal(history.completeUp('2'), '3'); - assert.equal(history.completeUp('1'), '2'); - assert.equal(history.completeUp(''), '1'); - - // Add should reset position. - history.add('6', true); - assert.equal(history.completeUp(''), '6'); - assert.equal(history.completeUp(''), '5'); - assert.equal(history.completeUp(''), '4'); - assert.equal(history.completeUp(''), '3'); - assert.equal(history.completeUp(''), '2'); - assert.equal(history.completeUp(''), '1'); - history = new InputHistory(); - history.add('1', true); - history.add('2', true); - history.add('3', true); - history.add('4', true); - assert.equal(history.completeDown('5'), '5'); - assert.equal(history.completeDown(''), ''); - assert.equal(history.completeUp('1'), '4'); - assert.equal(history.completeDown('4'), '4'); - assert.equal(history.completeDown('4'), '4'); - assert.equal(history.completeUp('1'), '3'); - assert.equal(history.completeUp('4'), '2'); - assert.equal(history.completeDown('3'), '3'); - assert.equal(history.completeDown(''), '4'); - assert.equal(history.completeUp(''), '3'); - assert.equal(history.completeUp(''), '2'); - assert.equal(history.completeUp(''), '1'); - assert.equal(history.completeUp(''), ''); - assert.equal(history.completeUp('1'), '1'); - assert.equal(history.completeDown('1'), '2'); - assert.equal(history.completeDown('2'), '3'); - assert.equal(history.completeDown('3'), '4'); - assert.equal(history.completeDown(''), ''); - history.add('5', true); - assert.equal(history.completeUp('1'), '5'); - assert.equal(history.completeUp('1'), '4'); - assert.equal(history.completeUp('1'), '3'); - history.add('3', false); - assert.equal(history.completeUp('1'), '3'); - assert.equal(history.completeUp('1'), '2'); - assert.equal(history.completeUp('1'), '1'); - assert.equal(history.completeDown('1'), '2'); - assert.equal(history.completeUp('1'), '1'); - assert.equal(history.completeDown('1'), '2'); - assert.equal(history.completeDown('1'), '3'); - assert.equal(history.completeDown('1'), '4'); - assert.equal(history.completeDown('1'), '5'); - assert.equal(history.completeDown('1'), '3'); - }); - - test('parsing cells', () => { - let cells = generateCells(undefined, '#%%\na=1\na', 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'Simple cell, not right number found'); - cells = generateCells(undefined, '#%% [markdown]\na=1\na', 'foo', 0, true, '1'); - assert.equal(cells.length, 2, 'Split cell, not right number found'); - cells = generateCells(undefined, '#%% [markdown]\n# #a=1\n#a', 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'Markdown split wrong'); - assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); - cells = generateCells(undefined, '#%% [markdown]\n\'\'\'\n# a\nb\n\'\'\'', 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'Markdown cell multline failed'); - assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); - assert.equal(cells[0].data.source.length, 2, 'Lines for markdown not emitted'); - cells = generateCells(undefined, '#%% [markdown]\n\"\"\"\n# a\nb\n\"\"\"', 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'Markdown cell multline failed'); - assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); - assert.equal(cells[0].data.source.length, 2, 'Lines for markdown not emitted'); - cells = generateCells(undefined, '#%% \n\"\"\"\n# a\nb\n\"\"\"', 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'Code cell multline failed'); - assert.equal(cells[0].data.cell_type, 'code', 'Code cell not generated'); - assert.equal(cells[0].data.source.length, 5, 'Lines for cell not emitted'); - cells = generateCells(undefined, '#%% [markdown] \n\"\"\"# a\nb\n\"\"\"', 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'Markdown cell multline failed'); - assert.equal(cells[0].data.cell_type, 'markdown', 'Markdown cell not generated'); - assert.equal(cells[0].data.source.length, 2, 'Lines for cell not emitted'); - -// tslint:disable-next-line: no-multiline-string -const multilineCode = `#%% -myvar = """ # Lorem Ipsum -Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Nullam eget varius ligula, eget fermentum mauris. -Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. -Nunc quis orci ante. Vivamus vel blandit velit. -Sed mattis dui diam, et blandit augue mattis vestibulum. -Suspendisse ornare interdum velit. Suspendisse potenti. -Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. -"""`; -// tslint:disable-next-line: no-multiline-string -const multilineTwo = `#%% -""" # Lorem Ipsum -Lorem ipsum dolor sit amet, consectetur adipiscing elit. -Nullam eget varius ligula, eget fermentum mauris. -Cras ultrices, enim sit amet iaculis ornare, nisl nibh aliquet elit, sed ultrices velit ipsum dignissim nisl. -Nunc quis orci ante. Vivamus vel blandit velit. -Sed mattis dui diam, et blandit augue mattis vestibulum. -Suspendisse ornare interdum velit. Suspendisse potenti. -Morbi molestie lacinia sapien nec porttitor. Nam at vestibulum nisi. -""" print('bob')`; - - cells = generateCells(undefined, multilineCode, 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'code cell multline failed'); - assert.equal(cells[0].data.cell_type, 'code', 'Code cell not generated'); - assert.equal(cells[0].data.source.length, 10, 'Lines for cell not emitted'); - cells = generateCells(undefined, multilineTwo, 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'code cell multline failed'); - assert.equal(cells[0].data.cell_type, 'code', 'Code cell not generated'); - assert.equal(cells[0].data.source.length, 10, 'Lines for cell not emitted'); -// tslint:disable-next-line: no-multiline-string - assert.equal(cells[0].data.source[9], `""" print('bob')`, 'Lines for cell not emitted'); -// tslint:disable-next-line: no-multiline-string - const multilineMarkdown = `#%% [markdown] -# ## Block of Interest -# -# ### Take a look -# -# -# 1. Item 1 -# -# - Item 1-a -# 1. Item 1-a-1 -# - Item 1-a-1-a -# - Item 1-a-1-b -# 2. Item 1-a-2 -# - Item 1-a-2-a -# - Item 1-a-2-b -# 3. Item 1-a-3 -# - Item 1-a-3-a -# - Item 1-a-3-b -# - Item 1-a-3-c -# -# 2. Item 2`; - cells = generateCells(undefined, multilineMarkdown, 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'markdown cell multline failed'); - assert.equal(cells[0].data.cell_type, 'markdown', 'markdown cell not generated'); - assert.equal(cells[0].data.source.length, 20, 'Lines for cell not emitted'); - assert.equal(cells[0].data.source[17], ' - Item 1-a-3-c\n', 'Lines for markdown not emitted'); - -// tslint:disable-next-line: no-multiline-string -const multilineQuoteWithOtherDelimiter = `#%% [markdown] -''' -### Take a look - 2. Item 2 -""" Not a comment delimiter -''' -`; - cells = generateCells(undefined, multilineQuoteWithOtherDelimiter, 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'markdown cell multline failed'); - assert.equal(cells[0].data.cell_type, 'markdown', 'markdown cell not generated'); - assert.equal(cells[0].data.source.length, 3, 'Lines for cell not emitted'); - assert.equal(cells[0].data.source[2], '""" Not a comment delimiter', 'Lines for markdown not emitted'); - - // tslint:disable-next-line: no-multiline-string -const multilineQuoteInFunc = `#%% -import requests -def download(url, filename): - """ utility function to download a file """ - response = requests.get(url, stream=True) - with open(filename, "wb") as handle: - for data in response.iter_content(): - handle.write(data) -`; - cells = generateCells(undefined, multilineQuoteInFunc, 'foo', 0, true, '1'); - assert.equal(cells.length, 1, 'cell multline failed'); - assert.equal(cells[0].data.cell_type, 'code', 'code cell not generated'); - assert.equal(cells[0].data.source.length, 9, 'Lines for cell not emitted'); - assert.equal(cells[0].data.source[3], ' """ utility function to download a file """\n', 'Lines for cell not emitted'); - -// tslint:disable-next-line: no-multiline-string -const multilineMarkdownWithCell = `#%% [markdown] -# # Define a simple class -class Pizza(object): - def __init__(self, size, toppings, price, rating): - self.size = size - self.toppings = toppings - self.price = price - self.rating = rating - `; - - cells = generateCells(undefined, multilineMarkdownWithCell, 'foo', 0, true, '1'); - assert.equal(cells.length, 2, 'cell split failed'); - assert.equal(cells[0].data.cell_type, 'markdown', 'markdown cell not generated'); - assert.equal(cells[0].data.source.length, 1, 'Lines for markdown not emitted'); - assert.equal(cells[1].data.cell_type, 'code', 'code cell not generated'); - assert.equal(cells[1].data.source.length, 7, 'Lines for code not emitted'); - assert.equal(cells[1].data.source[3], ' self.toppings = toppings\n', 'Lines for cell not emitted'); - - // Non comments tests - let nonComments = stripComments(multilineCode); - assert.ok(nonComments.startsWith('myvar = """ # Lorem Ipsum'), 'Variable set to multiline string not working'); - nonComments = stripComments(multilineTwo); - assert.equal(nonComments, '', 'Multline comment is not being stripped'); - nonComments = stripComments(multilineQuoteInFunc); - assert.equal(nonComments.splitLines().length, 6, 'Splitting quote in func wrong number of lines'); - }); - -}); 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/dataviewer.functional.test.tsx b/src/test/datascience/dataviewer.functional.test.tsx deleted file mode 100644 index 7223e634514d..000000000000 --- a/src/test/datascience/dataviewer.functional.test.tsx +++ /dev/null @@ -1,235 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string -import '../../client/common/extensions'; - -import { nbformat } from '@jupyterlab/coreutils'; -import * as assert from 'assert'; -import { mount, ReactWrapper } from 'enzyme'; -import { parse } from 'node-html-parser'; -import * as React from 'react'; -import * as uuid from 'uuid/v4'; -import { Disposable } from 'vscode'; - -import { createDeferred } from '../../client/common/utils/async'; -import { Identifiers } from '../../client/datascience/constants'; -import { DataViewerMessages } from '../../client/datascience/data-viewing/types'; -import { IDataViewer, IDataViewerProvider, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; -import { MainPanel } from '../../datascience-ui/data-explorer/mainPanel'; -import { ReactSlickGrid } from '../../datascience-ui/data-explorer/reactSlickGrid'; -import { noop } from '../core'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; - -// import { asyncDump } from '../common/asyncDump'; -suite('DataScience DataViewer tests', () => { - const disposables: Disposable[] = []; - let dataProvider: IDataViewerProvider; - let ioc: DataScienceIocContainer; - let messageWrapper: ((m: string, payload: any) => void) | undefined; - - suiteSetup(function () { - // DataViewer tests require jupyter to run. Othewrise can't - // run any of our variable execution code - const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; - if (!isRollingBuild) { - // tslint:disable-next-line:no-console - console.log('Skipping DataViewer tests. Requires python environment'); - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - }); - - setup(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - - // Add a listener for our ioc that lets the test - // forward messages on - ioc.addMessageListener((m, p) => { - if (messageWrapper) { - messageWrapper(m, p); - } - }); - - }); - - function mountWebView(): ReactWrapper<any, Readonly<{}>, React.Component> { - // Setup our webview panel - ioc.createWebView(() => mount(<MainPanel skipDefault={true} baseTheme={'vscode-light'} testMode={true}/>)); - - // Make sure the data explorer provider and execution factory in the container is created (the extension does this on startup in the extension) - dataProvider = ioc.get<IDataViewerProvider>(IDataViewerProvider); - - return ioc.wrapper!; - } - - teardown(async () => { - for (const disposable of disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - await ioc.dispose(); - delete (global as any).ascquireVsCodeApi; - }); - - suiteTeardown(() => { - // asyncDump(); - }); - - async function createDataViewer(variable: string): Promise<IDataViewer> { - return dataProvider.create(variable); - } - - async function injectCode(code: string) : Promise<void> { - const exec = ioc.get<IJupyterExecution>(IJupyterExecution); - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const server = await exec.connectToNotebookServer(await interactiveWindowProvider.getNotebookOptions()); - if (server) { - const cells = await server.execute(code, Identifiers.EmptyFileName, 0, uuid()); - assert.equal(cells.length, 1, `Wrong number of cells returned`); - assert.equal(cells[0].data.cell_type, 'code', `Wrong type of cell returned`); - const cell = cells[0].data as nbformat.ICodeCell; - if (cell.outputs.length > 0) { - const error = cell.outputs[0].evalue; - if (error) { - assert.fail(`Unexpected error: ${error}`); - } - } - } - } - - function waitForMessage(message: string) : Promise<void> { - // Wait for the mounted web panel to send a message back to the data explorer - const promise = createDeferred<void>(); - messageWrapper = (m: string, _p: any) => { - if (m === message) { - promise.resolve(); - } - }; - return promise.promise; - } - - function getCompletedPromise() : Promise<void> { - return waitForMessage(DataViewerMessages.CompletedData); - } - - // tslint:disable-next-line:no-any - function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>) { - test(name, async () => { - const wrapper = mountWebView(); - try { - await testFunc(wrapper); - } finally { - // Make sure to unmount the wrapper or it will interfere with other tests - if (wrapper && wrapper.length) { - wrapper.unmount(); - } - } - }); - } - - function sortRows(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, sortCol: string, sortAsc: boolean) : void { - // Cause our sort - const mainPanelWrapper = wrapper.find(MainPanel); - assert.ok(mainPanelWrapper && mainPanelWrapper.length > 0, 'Grid not found to sort on'); - const mainPanel = mainPanelWrapper.instance() as MainPanel; - assert.ok(mainPanel, 'Main panel instance not found'); - const reactGrid = (mainPanel as any).grid.current as ReactSlickGrid; - assert.ok(reactGrid, 'Grid control not found'); - if (reactGrid.state.grid) { - const cols = reactGrid.state.grid.getColumns(); - const col = cols.find(c => c.field === sortCol); - assert.ok(col, `${sortCol} is not a column of the grid`); - reactGrid.sort(new Slick.EventData(), { sortCol: col, sortAsc, multiColumnSort: false, grid: reactGrid.state.grid }); - } - } - - function verifyRows(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, rows: (string | number)[]) { - const mainPanel = wrapper.find('.main-panel'); - assert.ok(mainPanel.length >= 1, 'Didn\'t find any cells being rendered'); - - // Force the main panel to actually render. - const html = mainPanel.html(); - const root = parse(html) as any; - const cells = root.querySelectorAll('.react-grid-cell') as HTMLElement[]; - assert.ok(cells, 'No cells found'); - assert.ok(cells.length >= rows.length, 'Not enough cells found'); - // Cells should be an array that matches up to the values we expect. - for (let i = 0; i < rows.length; i += 1) { - // Span should have our value (based on the CellFormatter's output) - const span = cells[i].querySelector('div.cell-formatter span') as HTMLSpanElement; - assert.ok(span, `Span ${i} not found`); - const val = rows[i].toString(); - assert.equal(val, span.innerHTML, `Row ${i} not matching. ${span.innerHTML} !== ${val}`); - } - } - - runMountedTest('Data Frame', async (wrapper) => { - await injectCode('import pandas as pd\r\ndf = pd.DataFrame([0, 1, 2, 3])'); - const gotAllRows = getCompletedPromise(); - const dv = await createDataViewer('df'); - assert.ok(dv, 'DataViewer not created'); - await gotAllRows; - - verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); - }); - - runMountedTest('List', async (wrapper) => { - await injectCode('ls = [0, 1, 2, 3]'); - const gotAllRows = getCompletedPromise(); - const dv = await createDataViewer('ls'); - assert.ok(dv, 'DataViewer not created'); - await gotAllRows; - - verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); - }); - - runMountedTest('Series', async (wrapper) => { - await injectCode('import pandas as pd\r\ns = pd.Series([0, 1, 2, 3])'); - const gotAllRows = getCompletedPromise(); - const dv = await createDataViewer('s'); - assert.ok(dv, 'DataViewer not created'); - await gotAllRows; - - verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); - }); - - runMountedTest('np.array', async (wrapper) => { - await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); - const gotAllRows = getCompletedPromise(); - const dv = await createDataViewer('x'); - assert.ok(dv, 'DataViewer not created'); - await gotAllRows; - - verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); - }); - - runMountedTest('Failure', async (_wrapper) => { - await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); - try { - await createDataViewer('unknown variable'); - assert.fail('Exception should have been thrown'); - } catch { - noop(); - } - }); - - runMountedTest('Sorting', async (wrapper) => { - await injectCode('import numpy as np\r\nx = np.array([0, 1, 2, 3])'); - const gotAllRows = getCompletedPromise(); - const dv = await createDataViewer('x'); - assert.ok(dv, 'DataViewer not created'); - await gotAllRows; - - verifyRows(wrapper, [0, 0, 1, 1, 2, 2, 3, 3]); - sortRows(wrapper, '0', false); - verifyRows(wrapper, [3, 3, 2, 2, 1, 1, 0, 0]); - }); -}); diff --git a/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts b/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts deleted file mode 100644 index 442eb99f2320..000000000000 --- a/src/test/datascience/editor-integration/cellhashprovider.unit.test.ts +++ /dev/null @@ -1,517 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { assert } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { Position, Range } from 'vscode'; - -import { IConfigurationService, IDataScienceSettings, IPythonSettings } from '../../../client/common/types'; -import { CellHashProvider } from '../../../client/datascience/editor-integration/cellhashprovider'; -import { InteractiveWindowMessages, SysInfoReason } from '../../../client/datascience/interactive-window/interactiveWindowTypes'; -import { MockDocumentManager } from '../mockDocumentManager'; - -// tslint:disable-next-line: max-func-body-length -suite('CellHashProvider Unit Tests', () => { - let hashProvider: CellHashProvider; - let documentManager: MockDocumentManager; - let configurationService: TypeMoq.IMock<IConfigurationService>; - let dataScienceSettings: TypeMoq.IMock<IDataScienceSettings>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - - setup(() => { - 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); - documentManager = new MockDocumentManager(); - hashProvider = new CellHashProvider(documentManager, configurationService.object); - }); - - function addSingleChange(file: string, range: Range, newText: string) { - documentManager.changeDocument(file, [{ range, newText }]); - } - - test('Add a cell and edit it', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Edit the first cell, removing it - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(1, 14)), ''); - - // Get our hashes again. The line number should change - // We should have a single hash - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - }); - - test('Add a cell, delete it, and recreate it', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Change the second cell - addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 0)), 'print ("bob")\r\n'); - - // Should be no hashes now - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 0, 'Hash should be gone'); - - // Undo the last change - addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 15)), ''); - - // Hash should reappear - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - }); - - test('Delete code below', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Change the third cell - addSingleChange('foo.py', new Range(new Position(5, 0), new Position(5, 0)), 'print ("bob")\r\n'); - - // Should be the same hashes - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Delete the first cell - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(1, 14)), ''); - - // Hash should move - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - }); - - test('Modify code after sending twice', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; - const code = '#%%\r\nprint("bar")'; - const thirdCell = '#%%\r\nprint ("bob")\r\nprint("baz")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Change the third cell - addSingleChange('foo.py', new Range(new Position(5, 0), new Position(5, 0)), 'print ("bob")\r\n'); - - // Send the third cell - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code: thirdCell, file: 'foo.py', line: 4 }); - - // Should be two hashes - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 2, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - assert.equal(hashes[0].hashes[1].line, 6, 'Wrong start line'); - assert.equal(hashes[0].hashes[1].endLine, 7, 'Wrong end line'); - assert.equal(hashes[0].hashes[1].executionCount, 2, 'Wrong execution count'); - - // Delete the first cell - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(1, 14)), ''); - - // Hashes should move - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 2, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - assert.equal(hashes[0].hashes[1].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[1].endLine, 5, 'Wrong end line'); - assert.equal(hashes[0].hashes[1].executionCount, 2, 'Wrong execution count'); - }); - - test('Run same cell twice', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; - const code = '#%%\r\nprint("bar")'; - const thirdCell = '#%%\r\nprint ("bob")\r\nprint("baz")'; - - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // Add a second cell - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code: thirdCell, file: 'foo.py', line: 4 }); - - // Add this code a second time - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // Execution count should go up, but still only have two cells. - const hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 2, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 3, 'Wrong execution count'); - assert.equal(hashes[0].hashes[1].line, 6, 'Wrong start line'); - assert.equal(hashes[0].hashes[1].endLine, 6, 'Wrong end line'); - assert.equal(hashes[0].hashes[1].executionCount, 2, 'Wrong execution count'); - }); - - test('Two files with same cells', () => { - const file1 = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; - const file2 = file1; - const code = '#%%\r\nprint("bar")'; - const thirdCell = '#%%\r\nprint ("bob")\r\nprint("baz")'; - - // Create our documents - documentManager.addDocument(file1, 'foo.py'); - documentManager.addDocument(file2, 'bar.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'bar.py', line: 2 }); - - // Add a second cell - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code: thirdCell, file: 'foo.py', line: 4 }); - - // Add this code a second time - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // Execution count should go up, but still only have two cells. - const hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 2, 'Wrong number of hashes'); - const fooHash = hashes.find(h => h.file === 'foo.py'); - const barHash = hashes.find(h => h.file === 'bar.py'); - assert.ok(fooHash, 'No hash for foo.py'); - assert.ok(barHash, 'No hash for bar.py'); - assert.equal(fooHash!.hashes.length, 2, 'Not enough hashes found'); - assert.equal(fooHash!.hashes[0].line, 4, 'Wrong start line'); - assert.equal(fooHash!.hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(fooHash!.hashes[0].executionCount, 4, 'Wrong execution count'); - assert.equal(fooHash!.hashes[1].line, 6, 'Wrong start line'); - assert.equal(fooHash!.hashes[1].endLine, 6, 'Wrong end line'); - assert.equal(fooHash!.hashes[1].executionCount, 3, 'Wrong execution count'); - assert.equal(barHash!.hashes.length, 1, 'Not enough hashes found'); - assert.equal(barHash!.hashes[0].line, 4, 'Wrong start line'); - assert.equal(barHash!.hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(barHash!.hashes[0].executionCount, 2, 'Wrong execution count'); - }); - - test('Delete cell with dupes in code, put cell back', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")\r\n#%%\r\nprint("baz")'; - const code = '#%%\r\nprint("foo")'; - - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Modify the code - addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 1)), ''); - - // Should have zero hashes - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 0, 'Too many hashes found'); - - // Put back the original cell - addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 0)), 'p'); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Modify the code - addSingleChange('foo.py', new Range(new Position(3, 0), new Position(3, 1)), ''); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 0, 'Too many hashes found'); - - // Remove the first cell - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(1, 14)), ''); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 0, 'Too many hashes found'); - - // Put back the original cell - addSingleChange('foo.py', new Range(new Position(1, 0), new Position(1, 0)), 'p'); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 2, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - }); - - test('Add a cell and edit different parts of it', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - const hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Edit the cell we added - addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 0)), '#'); - assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); - addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 1)), ''); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 1)), ''); - assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); - addSingleChange('foo.py', new Range(new Position(2, 0), new Position(2, 0)), '#'); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - addSingleChange('foo.py', new Range(new Position(2, 1), new Position(2, 2)), ''); - assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); - addSingleChange('foo.py', new Range(new Position(2, 1), new Position(2, 1)), '%'); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - addSingleChange('foo.py', new Range(new Position(2, 2), new Position(2, 3)), ''); - assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); - addSingleChange('foo.py', new Range(new Position(2, 2), new Position(2, 2)), '%'); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - addSingleChange('foo.py', new Range(new Position(2, 3), new Position(2, 4)), ''); - assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); - addSingleChange('foo.py', new Range(new Position(2, 3), new Position(2, 3)), '\r'); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - addSingleChange('foo.py', new Range(new Position(2, 4), new Position(2, 5)), ''); - assert.equal(hashProvider.getHashes().length, 0, 'Cell should be destroyed'); - addSingleChange('foo.py', new Range(new Position(2, 4), new Position(2, 4)), '\n'); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - }); - - test('Add a cell and edit it to be exactly the same', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Replace with the same cell - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(3, 14)), file); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - assert.equal(hashProvider.getHashes().length, 1, 'Cell should be back'); - }); - - test('Add a cell and edit it to not be exactly the same', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const file2 = '#%%\r\nprint("fooze")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Replace with the new code - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(3, 14)), file2); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 0, 'Hashes should be gone'); - - // Put back old code - addSingleChange('foo.py', new Range(new Position(0, 0), new Position(3, 14)), file); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - }); - - test('Apply multiple edits at once', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Apply a couple of edits at once - documentManager.changeDocument('foo.py', - [ - { - range: new Range(new Position(0, 0), new Position(0, 0)), - newText: '#%%\r\nprint("new cell")\r\n' - }, - { - range: new Range(new Position(0, 0), new Position(0, 0)), - newText: '#%%\r\nprint("new cell")\r\n' - } - ]); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 8, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 8, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - documentManager.changeDocument('foo.py', - [ - { - range: new Range(new Position(0, 0), new Position(0, 0)), - newText: '#%%\r\nprint("new cell")\r\n' - }, - { - range: new Range(new Position(0, 0), new Position(1, 19)), - newText: '' - } - ]); - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 8, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 8, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - }); - - test('Restart kernel', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - const code = '#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code, file: 'foo.py', line: 2 }); - - // We should have a single hash - let hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 4, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 4, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - - // Restart the kernel - hashProvider.onMessage(InteractiveWindowMessages.AddedSysInfo, { type: SysInfoReason.Restart }); - - hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 0, 'Restart should have cleared'); - }); - - test('More than one cell in range', () => { - const file = '#%%\r\nprint("foo")\r\n#%%\r\nprint("bar")'; - // Create our document - documentManager.addDocument(file, 'foo.py'); - - // Add this code - hashProvider.onMessage(InteractiveWindowMessages.RemoteAddCode, { code: file, file: 'foo.py', line: 0 }); - - // We should have a single hash - const hashes = hashProvider.getHashes(); - assert.equal(hashes.length, 1, 'No hashes found'); - assert.equal(hashes[0].hashes.length, 1, 'Not enough hashes found'); - assert.equal(hashes[0].hashes[0].line, 2, 'Wrong start line'); - assert.equal(hashes[0].hashes[0].endLine, 3, 'Wrong end line'); - assert.equal(hashes[0].hashes[0].executionCount, 1, 'Wrong execution count'); - }); -}); diff --git a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts b/src/test/datascience/editor-integration/codelensprovider.unit.test.ts deleted file mode 100644 index db6884e507d6..000000000000 --- a/src/test/datascience/editor-integration/codelensprovider.unit.test.ts +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, Disposable, TextDocument } from 'vscode'; - -import { ICommandManager, IDebugService, IDocumentManager } from '../../../client/common/application/types'; -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'; - -// tslint:disable-next-line: max-func-body-length -suite('DataScienceCodeLensProvider Unit Tests', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let configurationService: TypeMoq.IMock<IConfigurationService>; - let codeLensProvider: IDataScienceCodeLensProvider; - let dataScienceSettings: TypeMoq.IMock<IDataScienceSettings>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let commandManager: TypeMoq.IMock<ICommandManager>; - let debugService: TypeMoq.IMock<IDebugService>; - let tokenSource : CancellationTokenSource; - const disposables: Disposable[] = []; - - setup(() => { - tokenSource = new CancellationTokenSource(); - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - debugService = TypeMoq.Mock.ofType<IDebugService>(); - - 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); - commandManager.setup(c => c.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve()); - debugService.setup(d => d.activeDebugSession).returns(() => undefined); - - codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, documentManager.object, configurationService.object, commandManager.object, disposables, debugService.object); - }); - - test('Initialize Code Lenses one document', () => { - // Create our document - const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(d => d.fileName).returns(() => 'test.py'); - document.setup(d => d.version).returns(() => 1); - - const targetCodeWatcher = TypeMoq.Mock.ofType<ICodeWatcher>(); - targetCodeWatcher.setup(tc => tc.getCodeLenses()).returns(() => []).verifiable(TypeMoq.Times.once()); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => targetCodeWatcher.object).verifiable(TypeMoq.Times.once()); - documentManager.setup(d => d.textDocuments).returns(() => [document.object]); - - codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - - targetCodeWatcher.verifyAll(); - serviceContainer.verifyAll(); - }); - - test('Initialize Code Lenses same doc called', () => { - // Create our document - const document = TypeMoq.Mock.ofType<TextDocument>(); - 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()); - documentManager.setup(d => d.textDocuments).returns(() => [document.object]); - - codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - - // getCodeLenses should be called twice, but getting the code watcher only once due to same doc - targetCodeWatcher.verifyAll(); - serviceContainer.verifyAll(); - }); - - test('Initialize Code Lenses new name / version', () => { - // Create our document - const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(d => d.fileName).returns(() => 'test.py'); - document.setup(d => d.version).returns(() => 1); - - const document2 = TypeMoq.Mock.ofType<TextDocument>(); - document2.setup(d => d.fileName).returns(() => 'test2.py'); - document2.setup(d => d.version).returns(() => 1); - - const document3 = TypeMoq.Mock.ofType<TextDocument>(); - document3.setup(d => d.fileName).returns(() => 'test.py'); - document3.setup(d => d.version).returns(() => 2); - - const targetCodeWatcher = TypeMoq.Mock.ofType<ICodeWatcher>(); - targetCodeWatcher.setup(tc => tc.getCodeLenses()).returns(() => []).verifiable(TypeMoq.Times.exactly(3)); - targetCodeWatcher.setup(tc => tc.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)); - documentManager.setup(d => d.textDocuments).returns(() => [document.object, document2.object, document3.object]); - - codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - codeLensProvider.provideCodeLenses(document2.object, tokenSource.token); - codeLensProvider.provideCodeLenses(document3.object, tokenSource.token); - - // service container get should be called three times as the names and versions don't match - targetCodeWatcher.verifyAll(); - serviceContainer.verifyAll(); - }); -}); diff --git a/src/test/datascience/editor-integration/codewatcher.unit.test.ts b/src/test/datascience/editor-integration/codewatcher.unit.test.ts deleted file mode 100644 index 483de93a8cbb..000000000000 --- a/src/test/datascience/editor-integration/codewatcher.unit.test.ts +++ /dev/null @@ -1,740 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -// tslint:disable:max-func-body-length no-trailing-whitespace no-multiline-string chai-vague-errors no-unused-expression -// Disable whitespace / multiline as we use that to pass in our fake file strings -import { expect } from 'chai'; -import * as TypeMoq from 'typemoq'; -import { CancellationTokenSource, CodeLens, Disposable, Range, Selection, TextEditor } from 'vscode'; - -import { - IApplicationShell, - ICommandManager, - IDebugService, - IDocumentManager -} from '../../../client/common/application/types'; -import { PythonSettings } from '../../../client/common/configSettings'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IConfigurationService, ILogger } from '../../../client/common/types'; -import { Commands, EditorContexts } from '../../../client/datascience/constants'; -import { CodeLensFactory } from '../../../client/datascience/editor-integration/codeLensFactory'; -import { DataScienceCodeLensProvider } from '../../../client/datascience/editor-integration/codelensprovider'; -import { CodeWatcher } from '../../../client/datascience/editor-integration/codewatcher'; -import { ICodeWatcher, IInteractiveWindow, IInteractiveWindowProvider } from '../../../client/datascience/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ICodeExecutionHelper } from '../../../client/terminals/types'; -import { MockAutoSelectionService } from '../../mocks/autoSelector'; -import { createDocument } from './helpers'; - -//tslint:disable:no-any - -suite('DataScience Code Watcher Unit Tests', () => { - let codeWatcher: CodeWatcher; - let appShell: TypeMoq.IMock<IApplicationShell>; - let logger: TypeMoq.IMock<ILogger>; - let interactiveWindowProvider: TypeMoq.IMock<IInteractiveWindowProvider>; - let activeInteractiveWindow: TypeMoq.IMock<IInteractiveWindow>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let commandManager: TypeMoq.IMock<ICommandManager>; - let textEditor: TypeMoq.IMock<TextEditor>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let configService: TypeMoq.IMock<IConfigurationService>; - let serviceContainer : TypeMoq.IMock<IServiceContainer>; - let helper: TypeMoq.IMock<ICodeExecutionHelper>; - let tokenSource : CancellationTokenSource; - let debugService: TypeMoq.IMock<IDebugService>; - const contexts : Map<string, boolean> = new Map<string, boolean>(); - const pythonSettings = new class extends PythonSettings { - public fireChangeEvent() { - this.changed.fire(); - } - }(undefined, new MockAutoSelectionService()); - const disposables: Disposable[] = []; - - setup(() => { - tokenSource = new CancellationTokenSource(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - logger = TypeMoq.Mock.ofType<ILogger>(); - interactiveWindowProvider = TypeMoq.Mock.ofType<IInteractiveWindowProvider>(); - activeInteractiveWindow = createTypeMoq<IInteractiveWindow>('history'); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - textEditor = TypeMoq.Mock.ofType<TextEditor>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - debugService = TypeMoq.Mock.ofType<IDebugService>(); - - // Setup default settings - pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 20000, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - enableCellCodeLens: true, - enablePlotViewer: true - }; - debugService.setup(d => d.activeDebugSession).returns(() => undefined); - - // Setup the service container to return code watchers - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const codeLensFactory = new CodeLensFactory(configService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICodeWatcher))).returns(() => new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, codeLensFactory)); - - // Setup our active history instance - interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)); - - // Setup our active text editor - documentManager.setup(dm => dm.activeTextEditor).returns(() => textEditor.object); - - // Setup the file system - fileSystem.setup(f => f.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - - // Setup config service - configService.setup(c => c.getSettings()).returns(() => pythonSettings); - - commandManager.setup(c => c.executeCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((c, n, v) => { - if (c === 'setContext') { - contexts.set(n, v); - } - return Promise.resolve(); - }); - - const codeLens = new CodeLensFactory(configService.object); - - codeWatcher = new CodeWatcher(appShell.object, logger.object, interactiveWindowProvider.object, fileSystem.object, configService.object, documentManager.object, helper.object, codeLens); - }); - - function createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { - // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class - // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 - const result: TypeMoq.IMock<T> = TypeMoq.Mock.ofType<T>(); - (result as any).tag = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; - } - - function verifyCodeLensesAtPosition(codeLenses: CodeLens[], startLensIndex: number, targetRange: Range, firstCell: boolean = false) { - if (codeLenses[startLensIndex].command) { - expect(codeLenses[startLensIndex].command!.command).to.be.equal(Commands.RunCell, 'Run Cell code lens command incorrect'); - } - expect(codeLenses[startLensIndex].range).to.be.deep.equal(targetRange, 'Run Cell code lens range incorrect'); - - if (!firstCell) { - if (codeLenses[startLensIndex + 1].command) { - expect(codeLenses[startLensIndex + 1].command!.command).to.be.equal(Commands.RunAllCellsAbove, 'Run Above code lens command incorrect'); - } - expect(codeLenses[startLensIndex + 1].range).to.be.deep.equal(targetRange, 'Run Above code lens range incorrect'); - } - - const indexAdd = firstCell ? 1 : 2; - if (codeLenses[startLensIndex + indexAdd].command) { - expect(codeLenses[startLensIndex + indexAdd].command!.command).to.be.equal(Commands.DebugCell, 'Debug command incorrect'); - } - expect(codeLenses[startLensIndex + indexAdd].range).to.be.deep.equal(targetRange, 'Debug code lens range incorrect'); - } - - 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.setDocument(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'); - verifyCodeLensesAtPosition(codeLenses, 0, new Range(0, 0, 0, 3), true); - - // Verify function calls - document.verifyAll(); - }); - - test('Add a file without a mark to a code watcher', () => { - const fileName = 'test.py'; - const version = 1; - const inputText = `dummy`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.setDocument(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.setDocument(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(5, 'Incorrect count of code lenses'); - - verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); - verifyCodeLensesAtPosition(codeLenses, 2, new Range(6, 0, 7, 11)); - - // Verify function calls - document.verifyAll(); - }); - - test('Add a file with custom marks to a code watcher', () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`first line -second line - -# <foobar> -third line - -# <baz> -fourth line - -# <mymarkdown> -# fifth line`; - pythonSettings.datascience.codeRegularExpression = '(#\\s*\\<foobar\\>|#\\s*\\<baz\\>)'; - pythonSettings.datascience.markdownRegularExpression = '(#\\s*\\<markdowncell\\>|#\\s*\\<mymarkdown\\>)'; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.setDocument(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(8, 'Incorrect count of code lenses'); - - verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); - verifyCodeLensesAtPosition(codeLenses, 2, new Range(6, 0, 8, 0)); - verifyCodeLensesAtPosition(codeLenses, 5, new Range(9, 0, 10, 12)); - - // Verify function calls - document.verifyAll(); - }); - - test('Make sure invalid regex from a user still work', () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`first line -second line - -# <codecell> -third line - -# <codecell> -fourth line - -# <mymarkdown> -# fifth line`; - pythonSettings.datascience.codeRegularExpression = '# * code cell)'; - pythonSettings.datascience.markdownRegularExpression = '(#\\s*\\<markdowncell\\>|#\\s*\\<mymarkdown\\>)'; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.setDocument(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(8, 'Incorrect count of code lenses'); - - verifyCodeLensesAtPosition(codeLenses, 0, new Range(3, 0, 5, 0), true); - verifyCodeLensesAtPosition(codeLenses, 2, new Range(6, 0, 8, 0)); - verifyCodeLensesAtPosition(codeLenses, 5, new Range(9, 0, 10, 12)); - - // Verify function calls - document.verifyAll(); - }); - - test('Test the RunCell command', async () => { - const fileName = 'test.py'; - const version = 1; - const testString = '#%%\ntesting'; - const document = createDocument(testString, fileName, version, TypeMoq.Times.atLeastOnce(), true); - const testRange = new Range(0, 0, 1, 7); - - codeWatcher.setDocument(document.object); - - // Set up our expected call to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny())).verifiable(TypeMoq.Times.once()); - - // Try our RunCell command - await codeWatcher.runCell(testRange); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunFileInteractive command', async () => { - const fileName = '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(), true); - - codeWatcher.setDocument(document.object); - - // Set up our expected calls to add code - // RunFileInteractive should run the entire file in one block, not cell by cell like RunAllCells - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(inputText), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - await codeWatcher.runFileInteractive(); - - // Verify function calls - activeInteractiveWindow.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.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString1), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(testString2), - TypeMoq.It.isValue('test.py'), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - await codeWatcher.runAllCells(); - - // Verify function calls - activeInteractiveWindow.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.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.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; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - // For this test we need to set up a document selection point - textEditor.setup(te => te.selection).returns(() => new Selection(2, 0, 2, 0)); - - await codeWatcher.runCurrentCell(); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunCellAndAllBelow command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2 -#%% -testing3`; - const targetText1 = -`#%% -testing2`; - - const targetText2 = -`#%% -testing3`; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText1), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText2), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(4), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - await codeWatcher.runCellAndAllBelow(2, 0); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunAllCellsAbove command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2 -#%% -testing3`; - const targetText1 = -`#%% -testing1`; - - const targetText2 = -`#%% -testing2`; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText1), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText2), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - await codeWatcher.runAllCellsAbove(4, 0); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunToLine command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2 -#%% -testing3`; - const targetText = -`#%% -testing1`; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(0), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - await codeWatcher.runToLine(2); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunToLine command with nothing on the lines', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -` - -print('testing')`; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); - - // If adding empty lines nothing should be added and history should not be started - interactiveWindowProvider.setup(h => h.getOrCreateActive()).returns(() => Promise.resolve(activeInteractiveWindow.object)).verifiable(TypeMoq.Times.never()); - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isAny(), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isAnyNumber(), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.never()); - - await codeWatcher.runToLine(2); - - // Verify function calls - interactiveWindowProvider.verifyAll(); - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunFromLine command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2 -#%% -testing3`; - const targetText = -`#%% -testing2 -#%% -testing3`; - - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce(), true); - - codeWatcher.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue(targetText), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(2), - TypeMoq.It.isAny(), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - // Try our RunCell command with the first selection point - await codeWatcher.runFromLine(2); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunSelection command', async () => { - const fileName = 'test.py'; - const version = 1; - const inputText = -`#%% -testing1 -#%% -testing2`; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - - codeWatcher.setDocument(document.object); - helper.setup(h => h.getSelectedTextToExecute(TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }))).returns(() => Promise.resolve('testing2')); - helper.setup(h => h.normalizeLines(TypeMoq.It.isAny())).returns(() => Promise.resolve('testing2')); - - // Set up our expected calls to add code - activeInteractiveWindow.setup(h => h.addCode(TypeMoq.It.isValue('testing2'), - TypeMoq.It.isValue(fileName), - TypeMoq.It.isValue(3), - TypeMoq.It.is((ed: TextEditor) => { - return textEditor.object === ed; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - // For this test we need to set up a document selection point - textEditor.setup(te => te.document).returns(() => document.object); - textEditor.setup(te => te.selection).returns(() => new Selection(3, 0, 3, 0)); - - // Try our RunCell command with the first selection point - await codeWatcher.runSelectionOrLine(textEditor.object); - - // Verify function calls - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('Test the RunCellAndAdvance command with next cell', async () => { - const fileName = '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.setDocument(document.object); - - // Set up our expected calls to add code - activeInteractiveWindow.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; - }), - TypeMoq.It.isAny() - )).verifiable(TypeMoq.Times.once()); - - // For this test we need to set up a document selection point - const selection = new Selection(0, 0, 0, 0); - textEditor.setup(te => te.selection).returns(() => selection); - - //textEditor.setup(te => te.selection = TypeMoq.It.isAny()).verifiable(TypeMoq.Times.once()); - //textEditor.setup(te => te.selection = TypeMoq.It.isAnyObject<Selection>(Selection)); - // Would be good to check that selection was set, but TypeMoq doesn't seem to like - // both getting and setting an object property. isAnyObject is not valid for this class - // and is or isAny overwrite the previous property getter if used. Will verify selection set - // in functional test - // https://github.com/florinn/typemoq/issues/107 - - // To get around this, override the advanceToRange function called from within runCurrentCellAndAdvance - // this will tell us if we are calling the correct range - (codeWatcher as any).advanceToRange = (targetRange: Range) => { - expect(targetRange.start.line).is.equal(2, 'Incorrect range in run cell and advance'); - expect(targetRange.start.character).is.equal(0, 'Incorrect range in run cell and advance'); - expect(targetRange.end.line).is.equal(3, 'Incorrect range in run cell and advance'); - expect(targetRange.end.character).is.equal(8, 'Incorrect range in run cell and advance'); - }; - - await codeWatcher.runCurrentCellAndAdvance(); - - // Verify function calls - textEditor.verifyAll(); - activeInteractiveWindow.verifyAll(); - document.verifyAll(); - }); - - test('CodeLens returned after settings changed is different', () => { - // Create our document - const fileName = 'test.py'; - const version = 1; - const inputText = '#%% foobar'; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - documentManager.setup(d => d.textDocuments).returns(() => [document.object]); - const codeLensProvider = new DataScienceCodeLensProvider(serviceContainer.object, documentManager.object, configService.object, commandManager.object, disposables, debugService.object); - - let result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - expect(result, 'result not okay').to.be.ok; - let codeLens = result as CodeLens[]; - expect(codeLens.length).to.equal(2, 'Code lens wrong length'); - - expect(contexts.get(EditorContexts.HasCodeCells)).to.be.equal(true, 'Code cells context not set'); - - // Change settings - pythonSettings.datascience.codeRegularExpression = '#%%%.*dude'; - result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - expect(result, 'result not okay').to.be.ok; - codeLens = result as CodeLens[]; - expect(codeLens.length).to.equal(0, 'Code lens wrong length'); - - expect(contexts.get(EditorContexts.HasCodeCells)).to.be.equal(false, 'Code cells context not set'); - - // Change settings to empty - pythonSettings.datascience.codeRegularExpression = ''; - result = codeLensProvider.provideCodeLenses(document.object, tokenSource.token); - expect(result, 'result not okay').to.be.ok; - codeLens = result as CodeLens[]; - expect(codeLens.length).to.equal(2, 'Code lens wrong length'); - }); -}); diff --git a/src/test/datascience/editor-integration/helpers.ts b/src/test/datascience/editor-integration/helpers.ts deleted file mode 100644 index 384e185514a3..000000000000 --- a/src/test/datascience/editor-integration/helpers.ts +++ /dev/null @@ -1,59 +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); - - const textLines = inputLines.map((line, index) => { - const textLine = TypeMoq.Mock.ofType<TextLine>(); - const testRange = new Range(index, 0, index, line.length); - textLine.setup(l => l.text).returns(() => line); - textLine.setup(l => l.range).returns(() => testRange); - textLine.setup(l => l.isEmptyOrWhitespace).returns(() => line.trim().length === 0); - return textLine; - }); - document.setup(d => d.lineAt(TypeMoq.It.isAnyNumber())).returns((index: number) => textLines[index].object); - - // Get text is a bit trickier - if (implementGetText) { - document.setup(d => d.getText()).returns(() => inputText); - document.setup(d => d.getText(TypeMoq.It.isAny())).returns((r: Range) => { - let results = ''; - if (r) { - for (let line = r.start.line; line <= r.end.line && line < inputLines.length; line += 1) { - const startIndex = line === r.start.line ? r.start.character : 0; - const endIndex = line === r.end.line ? r.end.character : inputLines[line].length - 1; - results += inputLines[line].slice(startIndex, endIndex + 1); - if (line !== r.end.line) { - results += '\n'; - } - } - } else { - results = inputText; - } - return results; - }); - } - - return document; -} diff --git a/src/test/datascience/execution.unit.test.ts b/src/test/datascience/execution.unit.test.ts deleted file mode 100644 index 47d7a5fe8737..000000000000 --- a/src/test/datascience/execution.unit.test.ts +++ /dev/null @@ -1,700 +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 { CancellationToken, 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 { LiveShareApi } from '../../client/common/liveshare/liveshare'; -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 { JupyterCommandFactory } from '../../client/datascience/jupyter/jupyterCommand'; -import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; -import { - ICell, - IConnection, - IJupyterKernelSpec, - INotebookCompletion, - INotebookServer, - INotebookServerLaunchInfo, - InterruptResult -} from '../../client/datascience/types'; -import { EnvironmentActivationService } from '../../client/interpreter/activation/service'; -import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -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 launchInfo: INotebookServerLaunchInfo | undefined; - private kernelSpec: IJupyterKernelSpec | undefined; - private notebookFile: TemporaryFile | undefined; - public connect(launchInfo: INotebookServerLaunchInfo): Promise<void> { - if (launchInfo && launchInfo.connectionInfo && launchInfo.kernelSpec) { - this.launchInfo = launchInfo; - this.kernelSpec = launchInfo.kernelSpec; - - // Validate connection info and kernel spec - if (launchInfo.connectionInfo.baseUrl && launchInfo.kernelSpec.name && /[a-z,A-Z,0-9,-,.,_]+/.test(launchInfo.kernelSpec.name)) { - return Promise.resolve(); - } - } - return Promise.reject('invalid server startup'); - } - public 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 async getCompletion(_cellCode: string, _offsetInCode: number, _cancelToken?: CancellationToken) : Promise<INotebookCompletion> { - throw new Error('Method not implemented'); - } - public execute(_code: string, _f: string, _line: number): Promise<ICell[]> { - throw new Error('Method not implemented'); - } - public restartKernel(): Promise<void> { - throw new Error('Method not implemented'); - } - public translateToNotebook(_cells: ICell[]): Promise<JSONObject> { - throw new Error('Method not implemented'); - } - public waitForIdle(): Promise<void> { - throw new Error('Method not implemented'); - } - public setInitialDirectory(_directory: string): Promise<void> { - throw new Error('Method not implemented'); - } - - public async setMatplotLibStyle(_useDark: boolean): Promise<void> { - noop(); - } - public getConnectionInfo(): IConnection | undefined { - return this.launchInfo ? this.launchInfo.connectionInfo : undefined; - } - public waitForConnect(): Promise<INotebookServerLaunchInfo | undefined> { - throw new Error('Method not implemented'); - } - public async shutdown() { - return Promise.resolve(); - } - - public getSysInfo() : Promise<ICell | undefined> { - return Promise.resolve(undefined); - } - - public interruptKernel(_timeout: number) : Promise<InterruptResult> { - throw new Error('Method not implemented'); - } - - public async dispose() : Promise<void> { - if (this.launchInfo) { - this.launchInfo.connectionInfo.dispose(); // This should kill the process that's running - this.launchInfo = 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 (const disposable of this.disposables) { - if (!disposable) { - continue; - } - const val = disposable.dispose(); - if (val instanceof Promise) { - const promise = val as Promise<void>; - await promise; - } - } - this.disposables = []; - } - -} - -suite('Jupyter Execution', async () => { - const interpreterService = mock(InterpreterService); - const executionFactory = mock(PythonExecutionFactory); - const liveShare = mock(LiveShareApi); - const configService = mock(ConfigurationService); - const processServiceFactory = mock(ProcessServiceFactory); - const knownSearchPaths = mock(KnownSearchPathsForInterpreters); - const activationHelper = mock(EnvironmentActivationService); - 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.IMock<T> = TypeMoq.Mock.ofType<T>(); - (result as any).tag = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; - } - - function argsMatch(matchers: (string | RegExp)[], args: string[]): boolean { - if (matchers.length === args.length) { - return args.every((s, i) => { - const r = matchers[i] as RegExp; - return r && r.test ? r.test(s) : s === matchers[i]; - }); - } - return false; - } - - function setupPythonService(service: TypeMoq.IMock<IPythonExecutionService>, module: string | undefined, args: (string | RegExp)[], result: Promise<ExecutionResult<string>>) { - - if (module) { - service.setup(x => x.execModule( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(() => result); - const withModuleArgs = ['-m', module, ...args]; - service.setup(x => x.exec( - TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), - TypeMoq.It.isAny())) - .returns(() => result); - } else { - service.setup(x => x.exec( - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(() => result); - - } - } - - function setupPythonServiceWithFunc(service: TypeMoq.IMock<IPythonExecutionService>, module: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - service.setup(x => x.execModule( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(result); - const withModuleArgs = ['-m', module, ...args]; - service.setup(x => x.exec( - TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), - TypeMoq.It.isAny())) - .returns(result); - } - - function setupPythonServiceExecObservable(service: TypeMoq.IMock<IPythonExecutionService>, module: string, args: (string | RegExp)[], stderr: string[], stdout: string[]) { - const result: ObservableExecutionResult<string> = { - proc: undefined, - out: new Observable<Output<string>>(subscriber => { - stderr.forEach(s => subscriber.next({ source: 'stderr', out: s })); - stdout.forEach(s => subscriber.next({ source: 'stderr', out: s })); - }), - dispose: () => { - noop(); - } - }; - - service.setup(x => x.execModuleObservable( - TypeMoq.It.isValue(module), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(() => result); - const withModuleArgs = ['-m', module, ...args]; - service.setup(x => x.execObservable( - TypeMoq.It.is(a => argsMatch(withModuleArgs, a)), - TypeMoq.It.isAny())) - .returns(() => result); - } - - function setupProcessServiceExec(service: TypeMoq.IMock<IProcessService>, file: string, args: (string | RegExp)[], result: Promise<ExecutionResult<string>>) { - service.setup(x => x.exec( - TypeMoq.It.isValue(file), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(() => result); - } - - function setupProcessServiceExecWithFunc(service: TypeMoq.IMock<IProcessService>, file: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - service.setup(x => x.exec( - TypeMoq.It.isValue(file), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(result); - } - - function setupProcessServiceExecObservable(service: TypeMoq.IMock<IProcessService>, file: string, args: (string | RegExp)[], stderr: string[], stdout: string[]) { - const result: ObservableExecutionResult<string> = { - proc: undefined, - out: new Observable<Output<string>>(subscriber => { - stderr.forEach(s => subscriber.next({ source: 'stderr', out: s })); - stdout.forEach(s => subscriber.next({ source: 'stderr', out: s })); - }), - dispose: () => { - noop(); - } - }; - - service.setup(x => x.execObservable( - TypeMoq.It.isValue(file), - TypeMoq.It.is(a => argsMatch(args, a)), - TypeMoq.It.isAny())) - .returns(() => result); - } - - function setupWorkingPythonService(service: TypeMoq.IMock<IPythonExecutionService>, notebookStdErr?: string[]) { - setupPythonService(service, 'ipykernel', ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupPythonService(service, 'jupyter', ['nbconvert', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupPythonService(service, 'jupyter', ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupPythonService(service, 'jupyter', ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - service.setup(x => x.getInterpreterInformation()).returns(() => Promise.resolve(workingPython)); - - // Don't mind the goofy path here. It's supposed to not find the item. It's just testing the internal regex works - setupPythonServiceWithFunc(service, 'jupyter', ['kernelspec', 'list'], () => { - // 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` }); - }); - setupPythonService(service, '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` })); - setupPythonServiceWithFunc(service, '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'); - setupPythonService(service, undefined, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); - setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list'], [], []); - setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - - } - - function setupMissingKernelPythonService(service: TypeMoq.IMock<IPythonExecutionService>, notebookStdErr?: string[]) { - setupPythonService(service, 'jupyter', ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupPythonService(service, 'jupyter', ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - service.setup(x => x.getInterpreterInformation()).returns(() => Promise.resolve(missingKernelPython)); - setupPythonService(service, 'jupyter', ['kernelspec', 'list'], Promise.resolve({ stdout: `working ${path.dirname(workingKernelSpec)}` })); - const getServerInfoPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'datascience', 'getServerInfo.py'); - setupPythonService(service, undefined, [getServerInfoPath], Promise.resolve({ stdout: 'failure to get server infos' })); - setupPythonServiceExecObservable(service, 'jupyter', ['kernelspec', 'list'], [], []); - setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - } - - function setupMissingNotebookPythonService(service: TypeMoq.IMock<IPythonExecutionService>) { - service.setup(x => x.execModule(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((_v) => { - return Promise.reject('cant exec'); - }); - service.setup(x => x.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=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], 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=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], 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=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - - // WE also check for existence with just the key jupyter - setupProcessServiceExec(service, 'jupyter', ['--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupProcessServiceExec(service, 'jupyter', ['notebook', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - setupProcessServiceExec(service, 'jupyter', ['kernelspec', '--version'], Promise.resolve({ stdout: '1.1.1.1' })); - } - - function createExecution(activeInterpreter: PythonInterpreter, notebookStdErr?: string[], skipSearch?: boolean): JupyterExecutionFactory { - // 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, notebookStdErr); - const missingKernelService = createTypeMoq<IPythonExecutionService>('missingKernel'); - setupMissingKernelPythonService(missingKernelService, notebookStdErr); - const missingNotebookService = createTypeMoq<IPythonExecutionService>('missingNotebook'); - setupMissingNotebookPythonService(missingNotebookService); - const missingNotebookService2 = createTypeMoq<IPythonExecutionService>('missingNotebook2'); - setupMissingNotebookPythonService(missingNotebookService2); - const processService = createTypeMoq<IProcessService>('working process'); - setupWorkingProcessService(processService, notebookStdErr); - setupMissingKernelProcessService(processService, notebookStdErr); - setupPathProcessService(jupyterOnPath, processService, notebookStdErr); - when(executionFactory.create(argThat(o => o.pythonPath && o.pythonPath === workingPython.path))).thenResolve(workingService.object); - when(executionFactory.create(argThat(o => o.pythonPath && o.pythonPath === missingKernelPython.path))).thenResolve(missingKernelService.object); - when(executionFactory.create(argThat(o => o.pythonPath && o.pythonPath === missingNotebookPython.path))).thenResolve(missingNotebookService.object); - when(executionFactory.create(argThat(o => o.pythonPath && o.pythonPath === missingNotebookPython2.path))).thenResolve(missingNotebookService2.object); - - // 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(executionFactory.createActivatedEnvironment(argThat(o => !o || o.interpreter === activeInterpreter))).thenResolve(activeService); - when(processServiceFactory.create()).thenResolve(processService.object); - - when(liveShare.getApi()).thenResolve(null); - - // Service container needs logger, file system, and config service - when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); - when(serviceContainer.get<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, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: !skipSearch, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - allowLiveShare: false, - enablePlotViewer: 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 JupyterExecutionFactory( - instance(liveShare), - instance(executionFactory), - instance(interpreterService), - instance(processServiceFactory), - instance(knownSearchPaths), - instance(logger), - disposableRegistry, - disposableRegistry, - instance(fileSystem), - mockSessionManager, - instance(workspaceService), - instance(configService), - new JupyterCommandFactory( - instance(executionFactory), - instance(activationHelper), - instance(processServiceFactory), - instance(interpreterService)), - 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(), '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(), '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(), '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(), '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(), '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(), 'Should be able to start a server'); - }).timeout(10000); - - test('Jupyter found on the path skipped', async () => { - // Make sure we can find jupyter on the path if we - // can't find it in a python module. - const execution = createExecution(missingNotebookPython, undefined, true); - when(interpreterService.getInterpreters()).thenResolve([missingNotebookPython]); - when(fileSystem.getFiles(anyString())).thenResolve([jupyterOnPath]); - await assert.isRejected(execution.connectToNotebookServer(), 'Running cells requires Jupyter notebooks to be installed.'); - }).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/intellisense.functional.test.tsx b/src/test/datascience/intellisense.functional.test.tsx deleted file mode 100644 index 63f731b15148..000000000000 --- a/src/test/datascience/intellisense.functional.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as assert from 'assert'; -import { ReactWrapper } from 'enzyme'; -import { IDisposable } from 'monaco-editor'; -import { Disposable } from 'vscode'; - -import { createDeferred } from '../../client/common/utils/async'; -import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-window/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IInteractiveWindow, IInteractiveWindowProvider } from '../../client/datascience/types'; -import { MonacoEditor } from '../../datascience-ui/react-common/monacoEditor'; -import { noop } from '../core'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { getEditor, runMountedTest, typeCode } from './interactiveWindowTestHelpers'; - -// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string -suite('DataScience Intellisense tests', () => { - const disposables: Disposable[] = []; - let ioc: DataScienceIocContainer; - - setup(() => { - ioc = new DataScienceIocContainer(); - // For this test, jedi is turned off so we use our mock language server - ioc.changeJediEnabled(false); - ioc.registerDataScienceTypes(); - }); - - teardown(async () => { - for (const disposable of disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - await ioc.dispose(); - }); - - // suiteTeardown(() => { - // asyncDump(); - // }); - - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - - function getIntellisenseTextLines(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) : string[] { - assert.ok(wrapper); - const editor = getEditor(wrapper); - assert.ok(editor); - const domNode = editor.getDOMNode(); - assert.ok(domNode); - const nodes = domNode!.getElementsByClassName('monaco-list-row'); - assert.ok(nodes && nodes.length); - const innerTexts: string[] = []; - for (let i = 0; i < nodes.length; i += 1) { - const node = nodes.item(i) as HTMLElement; - const content = node.textContent; - if (content) { - innerTexts.push(content); - } - } - return innerTexts; - } - - function verifyIntellisenseVisible(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, expectedSpan: string) { - const innerTexts = getIntellisenseTextLines(wrapper); - assert.ok(innerTexts.includes(expectedSpan), 'Intellisense row not matching'); - } - - function verifyIntellisenseMissing(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, expectedSpan: string) { - const innerTexts = getIntellisenseTextLines(wrapper); - assert.ok(!innerTexts.includes(expectedSpan), 'Intellisense row was found when not expected'); - } - - function waitForSuggestion(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) : { disposable: IDisposable; promise: Promise<void>} { - const editorEnzyme = getEditor(wrapper); - const reactEditor = editorEnzyme.instance() as MonacoEditor; - const editor = reactEditor.state.editor; - if (editor) { - // The suggest controller has a suggest model on it. It has an event - // that fires when the suggest controller is opened. - const suggest = editor.getContribution('editor.contrib.suggestController') as any; - if (suggest && suggest._model) { - const promise = createDeferred<void>(); - const disposable = suggest._model.onDidSuggest(() => { - promise.resolve(); - }); - return { - disposable, - promise: promise.promise - }; - } - } - - return { - disposable: { - dispose: noop - }, - promise: Promise.resolve() - }; - } - - runMountedTest('Simple autocomplete', async (wrapper) => { - // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); - - // Then enter some code. Don't submit, we're just testing that autocomplete appears - const suggestion = waitForSuggestion(wrapper); - typeCode(wrapper, 'print'); - await suggestion.promise; - suggestion.disposable.dispose(); - verifyIntellisenseVisible(wrapper, 'print'); - }, () => { return ioc; }); - - runMountedTest('Jupyter autocomplete', async (wrapper) => { - if (ioc.mockJupyter) { - // This test only works when mocking. - - // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); - - // Then enter some code. Don't submit, we're just testing that autocomplete appears - const suggestion = waitForSuggestion(wrapper); - typeCode(wrapper, 'print'); - await suggestion.promise; - suggestion.disposable.dispose(); - verifyIntellisenseVisible(wrapper, 'printly'); - } - }, () => { return ioc; }); - - runMountedTest('Jupyter autocomplete timeout', async (wrapper) => { - if (ioc.mockJupyter) { - // This test only works when mocking. - - // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); - - // Force a timeout on the jupyter completions - ioc.mockJupyter.getCurrentSession()!.setCompletionTimeout(1000); - - // Then enter some code. Don't submit, we're just testing that autocomplete appears - const suggestion = waitForSuggestion(wrapper); - typeCode(wrapper, 'print'); - await suggestion.promise; - suggestion.disposable.dispose(); - verifyIntellisenseMissing(wrapper, 'printly'); - } - }, () => { return ioc; }); -}); diff --git a/src/test/datascience/intellisense.unit.test.ts b/src/test/datascience/intellisense.unit.test.ts deleted file mode 100644 index cc723bfd6269..000000000000 --- a/src/test/datascience/intellisense.unit.test.ts +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { expect } from 'chai'; -import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; -import * as TypeMoq from 'typemoq'; - -import { ILanguageServer, ILanguageServerAnalysisOptions } from '../../client/activation/types'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { PythonSettings } from '../../client/common/configSettings'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService } from '../../client/common/types'; -import { Identifiers } from '../../client/datascience/constants'; -import { DotNetIntellisenseProvider } from '../../client/datascience/interactive-window/intellisense/dotNetIntellisenseProvider'; -import { IInteractiveWindowMapping, InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IInteractiveWindowListener, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockLanguageClient } from './mockLanguageClient'; - -// tslint:disable:no-any unified-signatures - -// tslint:disable-next-line: max-func-body-length -suite('DataScience Intellisense Unit Tests', () => { - let intellisenseProvider: IInteractiveWindowListener; - let languageServer: TypeMoq.IMock<ILanguageServer>; - let analysisOptions: TypeMoq.IMock<ILanguageServerAnalysisOptions>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let configService: TypeMoq.IMock<IConfigurationService>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let jupyterExecution: TypeMoq.IMock<IJupyterExecution>; - let interactiveWindowProvider: TypeMoq.IMock<IInteractiveWindowProvider>; - const pythonSettings = new class extends PythonSettings { - public fireChangeEvent() { - this.changed.fire(); - } - }(undefined, new MockAutoSelectionService()); - - const languageClient = new MockLanguageClient( - 'mockLanguageClient', { module: 'dummy' }, {}); - - setup(() => { - languageServer = TypeMoq.Mock.ofType<ILanguageServer>(); - analysisOptions = TypeMoq.Mock.ofType<ILanguageServerAnalysisOptions>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - jupyterExecution = TypeMoq.Mock.ofType<IJupyterExecution>(); - interactiveWindowProvider = TypeMoq.Mock.ofType<IInteractiveWindowProvider>(); - - pythonSettings.jediEnabled = false; - languageServer.setup(l => l.start(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve()); - analysisOptions.setup(a => a.getAnalysisOptions()).returns(() => Promise.resolve({})); - languageServer.setup(l => l.languageClient).returns(() => languageClient); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings); - workspaceService.setup(w => w.rootPath).returns(() => '/foo/bar'); - - intellisenseProvider = new DotNetIntellisenseProvider( - languageServer.object, - analysisOptions.object, - workspaceService.object, - configService.object, - fileSystem.object, - jupyterExecution.object, - interactiveWindowProvider.object); - }); - - function sendMessage<M extends IInteractiveWindowMapping, T extends keyof M>(type: T, payload?: M[T]) : Promise<void> { - const result = languageClient.waitForNotification(); - intellisenseProvider.onMessage(type.toString(), payload); - return result; - } - - function addCell(code: string, id: string) : Promise<void> { - return sendMessage(InteractiveWindowMessages.AddCell, { fullText: code, currentText: code, file: 'foo.py', id }); - } - - function updateCell(newCode: string, oldCode: string, id: string) : Promise<void> { - const oldSplit = oldCode.split('\n'); - const change: monacoEditor.editor.IModelContentChange = { - range: { - startLineNumber: 1, - startColumn: 1, - endLineNumber: oldSplit.length, - endColumn: oldSplit[oldSplit.length - 1].length + 1 - }, - rangeOffset: 0, - rangeLength: oldCode.length, - text: newCode - }; - return sendMessage(InteractiveWindowMessages.EditCell, { changes: [change], id}); - } - - function addCode(code: string, line: number, pos: number, offset: number) : Promise<void> { - if (!line || !pos) { - throw new Error('Invalid line or position data'); - } - const change: monacoEditor.editor.IModelContentChange = { - range: { - startLineNumber: line, - startColumn: pos, - endLineNumber: line, - endColumn: pos - }, - rangeOffset: offset, - rangeLength: 0, - text: code - }; - return sendMessage(InteractiveWindowMessages.EditCell, { changes: [change], id: Identifiers.EditCellId}); - } - - function removeCode(line: number, startPos: number, endPos: number, length: number) : Promise<void> { - if (!line || !startPos || !endPos) { - throw new Error('Invalid line or position data'); - } - const change: monacoEditor.editor.IModelContentChange = { - range: { - startLineNumber: line, - startColumn: startPos, - endLineNumber: line, - endColumn: endPos - }, - rangeOffset: startPos, - rangeLength: length, - text: '' - }; - return sendMessage(InteractiveWindowMessages.EditCell, { changes: [change], id: Identifiers.EditCellId}); - } - - function removeCell(id: string) : Promise<void> { - sendMessage(InteractiveWindowMessages.RemoveCell, { id }).ignoreErrors(); - return Promise.resolve(); - } - - function removeAllCells() : Promise<void> { - sendMessage(InteractiveWindowMessages.DeleteAllCells).ignoreErrors(); - return Promise.resolve(); - } - - test('Add a single cell', async () => { - await addCell('import sys\n\n', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n\n\n', 'Document not set'); - }); - - test('Add two cells', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCell('import sys', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\n', 'Document not set after double'); - }); - - test('Add a cell and edit', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('i', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); - await addCode('m', 1, 2, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nim', 'Document not set after edit'); - await addCode('\n', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nim\n', 'Document not set after edit'); - }); - - test('Add a cell and remove', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('i', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\ni', 'Document not set after edit'); - await removeCode(1, 1, 2, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); - await addCode('\n', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n\n', 'Document not set after edit'); - }); - - test('Remove a section in the middle', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('import os', 1, 1, 0); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport os', 'Document not set after edit'); - await removeCode(1, 4, 7, 4); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimp os', 'Document not set after edit'); - }); - - test('Remove a bunch in a row', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('p', 1, 1, 0); - await addCode('r', 1, 2, 1); - await addCode('i', 1, 3, 2); - await addCode('n', 1, 4, 3); - await addCode('t', 1, 5, 4); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nprint', 'Document not set after edit'); - await removeCode(1, 5, 6, 1); - await removeCode(1, 4, 5, 1); - await removeCode(1, 3, 4, 1); - await removeCode(1, 2, 3, 1); - await removeCode(1, 1, 2, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set after edit'); - }); - test('Remove from a line', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('s', 1, 1, 0); - await addCode('y', 1, 2, 1); - await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); - await addCode('\n', 1, 4, 3); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys\n', 'Document not set after edit'); - await addCode('s', 2, 1, 3); - await addCode('y', 2, 2, 4); - await addCode('s', 2, 3, 5); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys\nsys', 'Document not set after edit'); - await removeCode(1, 3, 4, 1); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsy\nsys', 'Document not set after edit'); - }); - - test('Add cell after adding code', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('s', 1, 1, 0); - await addCode('y', 1, 2, 1); - await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); - await addCell('import sys', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a second cell broken'); - }); - - test('Collapse expand cell', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await updateCell('import sys\nsys.version_info', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Readding a cell broken'); - await updateCell('import sys', 'import sys\nsys.version_info', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Collapsing a cell broken'); - await updateCell('import sys', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Updating a cell broken'); - }); - - test('Collapse expand cell after adding code', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('s', 1, 1, 0); - await addCode('y', 1, 2, 1); - await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); - await updateCell('import sys\nsys.version_info', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Readding a cell broken'); - await updateCell('import sys', 'import sys\nsys.version_info', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Collapsing a cell broken'); - await updateCell('import sys', 'import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Updating a cell broken'); - }); - - test('Add a cell and remove it', async () => { - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\n', 'Document not set'); - await addCode('s', 1, 1, 0); - await addCode('y', 1, 2, 1); - await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set after edit'); - await removeCell('1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Removing a cell broken'); - await addCell('import sys', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nsys', 'Adding a cell broken'); - await addCell('import bar', '3'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Adding a cell broken'); - await removeCell('1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport sys\nimport bar\nsys', 'Removing a cell broken'); - }); - - test('Add a bunch of cells and remove them', async () => { - await addCode('s', 1, 1, 0); - await addCode('y', 1, 2, 1); - await addCode('s', 1, 3, 2); - expect(languageClient.getDocumentContents()).to.be.eq('sys', 'Document not set after edit'); - await addCell('import sys', '1'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nsys', 'Document not set'); - await addCell('import foo', '2'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nsys', 'Document not set'); - await addCell('import bar', '3'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Document not set'); - await removeAllCells(); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nsys', 'Removing all cells broken'); - await addCell('import baz', '3'); - expect(languageClient.getDocumentContents()).to.be.eq('import sys\nimport foo\nimport bar\nimport baz\nsys', 'Document not set'); - }); -}); diff --git a/src/test/datascience/interactiveWindow.functional.test.tsx b/src/test/datascience/interactiveWindow.functional.test.tsx deleted file mode 100644 index 5641b7fb42b7..000000000000 --- a/src/test/datascience/interactiveWindow.functional.test.tsx +++ /dev/null @@ -1,622 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import { parse } from 'node-html-parser'; -import * as os from 'os'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { Disposable, Selection, TextDocument, TextEditor } from 'vscode'; - -import { IApplicationShell, IDocumentManager } from '../../client/common/application/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { noop } from '../../client/common/utils/misc'; -import { generateCellsFromDocument } from '../../client/datascience/cellFactory'; -import { concatMultilineString } from '../../client/datascience/common'; -import { EditorContexts } from '../../client/datascience/constants'; -import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; -import { - InteractiveWindowMessageListener -} from '../../client/datascience/interactive-window/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IInteractiveWindow, IInteractiveWindowProvider } from '../../client/datascience/types'; -import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; -import { ImageButton } from '../../datascience-ui/react-common/imageButton'; -import { sleep } from '../core'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { createDocument } from './editor-integration/helpers'; -import { - addCode, - addContinuousMockData, - addMockData, - CellInputState, - CellPosition, - defaultDataScienceSettings, - enterInput, - escapePath, - findButton, - getCellResults, - getLastOutputCell, - initialDataScienceSettings, - runMountedTest, - srcDirectory, - toggleCellExpansion, - updateDataScienceSettings, - verifyHtmlOnCell, - verifyLastCellInputState -} from './interactiveWindowTestHelpers'; -import { MockEditor } from './mockTextEditor'; -import { waitForUpdate } from './reactHelpers'; - -//import { asyncDump } from '../common/asyncDump'; -import { MockDocumentManager } from './mockDocumentManager'; -// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string -suite('DataScience Interactive Window output tests', () => { - const disposables: Disposable[] = []; - let ioc: DataScienceIocContainer; - - setup(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - }); - - teardown(async () => { - for (const disposable of disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - await ioc.dispose(); - }); - - // Uncomment this to debug hangs on exit - // suiteTeardown(() => { - // asyncDump(); - // }); - - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - - async function waitForMessageResponse(action: () => void): Promise<void> { - ioc.wrapperCreatedPromise = createDeferred<boolean>(); - action(); - await ioc.wrapperCreatedPromise.promise; - ioc.wrapperCreatedPromise = undefined; - } - - runMountedTest('Simple text', async (wrapper) => { - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - }, () => { return ioc; }); - - runMountedTest('Hide inputs', async (wrapper) => { - initialDataScienceSettings({ ...defaultDataScienceSettings(), showCellInputCode: false }); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - verifyLastCellInputState(wrapper, CellInputState.Hidden); - - // Add a cell without output, this cell should not show up at all - addMockData(ioc, 'a=1', undefined, 'text/plain'); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1', 4); - - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.First); - verifyHtmlOnCell(wrapper, undefined, CellPosition.Last); - }, () => { return ioc; }); - - runMountedTest('Show inputs', async (wrapper) => { - initialDataScienceSettings({ ...defaultDataScienceSettings() }); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); - }, () => { return ioc; }); - - runMountedTest('Expand inputs', async (wrapper) => { - initialDataScienceSettings({ ...defaultDataScienceSettings(), collapseCellInputCodeByDefault: false }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - verifyLastCellInputState(wrapper, CellInputState.Expanded); - }, () => { return ioc; }); - - runMountedTest('Collapse / expand cell', async (wrapper) => { - initialDataScienceSettings({ ...defaultDataScienceSettings() }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); - - toggleCellExpansion(wrapper); - - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Expanded); - - toggleCellExpansion(wrapper); - - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); - }, () => { return ioc; }); - - runMountedTest('Hide / show cell', async (wrapper) => { - initialDataScienceSettings({ ...defaultDataScienceSettings() }); - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); - - // Hide the inputs and verify - updateDataScienceSettings(wrapper, { ...defaultDataScienceSettings(), showCellInputCode: false }); - - verifyLastCellInputState(wrapper, CellInputState.Hidden); - - // Show the inputs and verify - updateDataScienceSettings(wrapper, { ...defaultDataScienceSettings(), showCellInputCode: true }); - - verifyLastCellInputState(wrapper, CellInputState.Visible); - verifyLastCellInputState(wrapper, CellInputState.Collapsed); - }, () => { return ioc; }); - - 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 = 'svg'; - const spinningCursor = `import sys -import time - -def spinning_cursor(): - while True: - for cursor in '|/-\\\\': - yield cursor - -spinner = spinning_cursor() -for _ in range(50): - sys.stdout.write(next(spinner)) - sys.stdout.flush() - time.sleep(0.1) - sys.stdout.write('\\r')`; - - addMockData(ioc, badPanda, `pandas has no attribute 'read'`, 'text/html', 'error'); - addMockData(ioc, goodPanda, `<td>A table</td>`, 'text/html'); - addMockData(ioc, matPlotLib, matPlotLibResults, 'text/html'); - const cursors = ['|', '/', '-', '\\']; - let cursorPos = 0; - let loops = 3; - addContinuousMockData(ioc, spinningCursor, async (_c) => { - const result = `${cursors[cursorPos]}\r`; - cursorPos += 1; - if (cursorPos >= cursors.length) { - cursorPos = 0; - loops -= 1; - } - return Promise.resolve({ result: result, haveMore: loops > 0 }); - }); - - await addCode(getOrCreateInteractiveWindow, wrapper, badPanda, 4); - verifyHtmlOnCell(wrapper, `has no attribute 'read'`, CellPosition.Last); - - await addCode(getOrCreateInteractiveWindow, wrapper, goodPanda); - verifyHtmlOnCell(wrapper, `<td>`, CellPosition.Last); - - await addCode(getOrCreateInteractiveWindow, wrapper, matPlotLib); - verifyHtmlOnCell(wrapper, matPlotLibResults, CellPosition.Last); - - await addCode(getOrCreateInteractiveWindow, wrapper, spinningCursor, 4 + (ioc.mockJupyter ? (cursors.length * 3) : 0)); - verifyHtmlOnCell(wrapper, '<xmp>', CellPosition.Last); - }, () => { return ioc; }); - - runMountedTest('Undo/redo commands', async (wrapper) => { - const interactiveWindow = await getOrCreateInteractiveWindow(); - - // Get a cell into the list - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - // Now verify if we undo, we have no cells - let afterUndo = await getCellResults(wrapper, 1, () => { - interactiveWindow.undoCells(); - return Promise.resolve(); - }); - - assert.equal(afterUndo.length, 1, `Undo should remove cells + ${afterUndo.debug()}`); - - // Redo should put the cells back - const afterRedo = await getCellResults(wrapper, 1, () => { - interactiveWindow.redoCells(); - return Promise.resolve(); - }); - assert.equal(afterRedo.length, 2, 'Redo should put cells back'); - - // Get another cell into the list - const afterAdd = await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - assert.equal(afterAdd.length, 3, 'Second cell did not get added'); - - // Clear everything - const afterClear = await getCellResults(wrapper, 1, () => { - interactiveWindow.removeAllCells(); - return Promise.resolve(); - }); - assert.equal(afterClear.length, 1, 'Clear didn\'t work'); - - // Undo should put them back - afterUndo = await getCellResults(wrapper, 1, () => { - interactiveWindow.undoCells(); - return Promise.resolve(); - }); - - assert.equal(afterUndo.length, 3, `Undo should put cells back`); - }, () => { return ioc; }); - - 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<IDocumentManager>(); - const visibleEditor = TypeMoq.Mock.ofType<TextEditor>(); - const dummyDocument = TypeMoq.Mock.ofType<TextDocument>(); - 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>(IDocumentManager, docManager.object); - - // Get a cell into the list - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - - // 'Click' the buttons in the react control - const undo = findButton(wrapper, 2); - const redo = findButton(wrapper, 1); - const clear = findButton(wrapper, 0); - - // 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, 1, `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, 2, 'Redo should put cells back'); - - // Get another cell into the list - const afterAdd = await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - assert.equal(afterAdd.length, 3, '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, 1, '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, 3, `Undo should put cells back`); - - // find the buttons on the cell itself - const ImageButtons = afterUndo.at(afterUndo.length - 2).find(ImageButton); - assert.equal(ImageButtons.length, 3, 'Cell buttons not found'); - const goto = ImageButtons.at(0); - const deleteButton = ImageButtons.at(2); - - // Make sure goto works - await waitForMessageResponse(() => goto.simulate('click')); - await Promise.race([sleep(1000), 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, 2, `Delete should remove a cell`); - }, () => { return ioc; }); - - 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<IApplicationShell>(); - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => { throw e; }); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); - appShell.setup(a => a.showSaveDialog(TypeMoq.It.isAny())).returns(() => { - exportCalled = true; - return Promise.resolve(undefined); - }); - appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - ioc.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); - - // Make sure to create the interactive window after the rebind or it gets the wrong application shell. - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - const interactiveWindow = await getOrCreateInteractiveWindow(); - - // Export should cause exportCalled to change to true - await waitForMessageResponse(() => interactiveWindow.exportCells()); - assert.equal(exportCalled, true, 'Export is not being called during export'); - - // Remove the cell - const exportButton = findButton(wrapper, 5); - const undo = findButton(wrapper, 2); - - // 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, 1, `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'); - - }, () => { return ioc; }); - - runMountedTest('Dispose test', async () => { - // tslint:disable-next-line:no-any - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); // Have to wait for the load to finish - await interactiveWindow.dispose(); - // tslint:disable-next-line:no-any - const h2 = await getOrCreateInteractiveWindow(); - // Check equal and then dispose so the test goes away - const equal = Object.is(interactiveWindow, h2); - await h2.show(); - assert.ok(!equal, 'Disposing is not removing the active interactive window'); - }, () => { return ioc; }); - - runMountedTest('Editor Context', async (wrapper) => { - // Verify we can send different commands to the UI and it will respond - const interactiveWindow = await getOrCreateInteractiveWindow(); - - // 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 interactive window - await interactiveWindow.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<boolean>(); - const eventDispose = ioc.onContextSet(_a => { - count += 1; - if (count >= 3) { - deferred.resolve(); - } - }); - disposables.push(eventDispose); - - // Create a method that resets the waiting - const resetWaiting = () => { - count = 0; - deferred = createDeferred<boolean>(); - }; - - // Now send an undo command. This should change the state, so use our waitForInfo promise instead - resetWaiting(); - interactiveWindow.undoCells(); - 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(); - interactiveWindow.redoCells(); - 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(); - interactiveWindow.removeAllCells(); - 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'); - }, () => { return ioc; }); - - runMountedTest('Simple input', async (wrapper) => { - // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); - - // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - }, () => { return ioc; }); - - runMountedTest('Copy to source input', async (wrapper) => { - const showedEditor = createDeferred(); - ioc.addDocument('# No cells here', 'foo.py'); - const docManager = ioc.get<IDocumentManager>(IDocumentManager) as MockDocumentManager; - const editor = await docManager.showTextDocument(docManager.textDocuments[0]) as MockEditor; - editor.setRevealCallback(() => showedEditor.resolve()); - - // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); - - // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - const ImageButtons = getLastOutputCell(wrapper).find(ImageButton); - assert.equal(ImageButtons.length, 3, 'Cell buttons not found'); - const copyToSource = ImageButtons.at(1); - - // Then click the copy to source button - await waitForMessageResponse(() => copyToSource.simulate('click')); - await Promise.race([sleep(100), showedEditor.promise]); - assert.ok(showedEditor.resolved, 'Copy to source is not adding code to the editor'); - - }, () => { return ioc; }); - - runMountedTest('Multiple input', async (wrapper) => { - // Create an interactive window so that it listens to the results. - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.show(); - - // Then enter some code. - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Then delete the node - const lastCell = getLastOutputCell(wrapper); - const ImageButtons = lastCell.find(ImageButton); - assert.equal(ImageButtons.length, 3, 'Cell buttons not found'); - const deleteButton = ImageButtons.at(2); - - // 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`); - - // Should be able to enter again - await enterInput(wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Try a 3rd time with some new input - addMockData(ioc, 'print("hello")', 'hello'); - await enterInput(wrapper, 'print("hello")'); - verifyHtmlOnCell(wrapper, '>hello</', CellPosition.Last); - }, () => { return ioc; }); - - runMountedTest('Restart with session failure', async (wrapper) => { - // Prime the pump - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Then something that could possibly timeout - addContinuousMockData(ioc, 'import time\r\ntime.sleep(1000)', (_c) => { - return Promise.resolve({ result: '', haveMore: true}); - }); - - // Then get our mock session and force it to not restart ever. - if (ioc.mockJupyter) { - const currentSession = ioc.mockJupyter.getCurrentSession(); - if (currentSession) { - currentSession.prolongRestarts(); - } - } - - // Then try executing our long running cell and restarting in the middle - const interactiveWindow = await getOrCreateInteractiveWindow(); - const executed = createDeferred(); - // We have to wait until the execute goes through before we reset. - interactiveWindow.onExecutedCode(() => executed.resolve()); - const added = interactiveWindow.addCode('import time\r\ntime.sleep(1000)', 'foo', 0); - await executed.promise; - await interactiveWindow.restartKernel(); - await added; - - // Now see if our wrapper still works. Interactive window should have forced a restart - await interactiveWindow.addCode('a=1\na', 'foo', 0); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - }, () => { return ioc; }); - - runMountedTest('Preview', async (wrapper) => { - - const testFile = path.join(srcDirectory(), 'sub', 'test.ipynb'); - - // Preview is much fewer renders than an add code since the data is already there. - await getCellResults(wrapper, 2, async () => { - const interactiveWindow = await getOrCreateInteractiveWindow(); - await interactiveWindow.previewNotebook(testFile); - }); - - verifyHtmlOnCell(wrapper, '<img', CellPosition.Last); - }, () => { return ioc; }); - - runMountedTest('LiveLossPlot', async (wrapper) => { - // Only run this test when not mocking. Too complicated to mimic otherwise - if (!ioc.mockJupyter) { - // Load all of our cells - const testFile = path.join(srcDirectory(), 'liveloss.py'); - const version = 1; - const inputText = await fs.readFile(testFile, 'utf-8'); - const document = createDocument(inputText, testFile, version, TypeMoq.Times.atLeastOnce(), true); - const cells = generateCellsFromDocument(document.object); - assert.ok(cells, 'No cells generated'); - assert.equal(cells.length, 2, 'Not enough cells generated'); - - // Run the first cell - await addCode(getOrCreateInteractiveWindow, wrapper, concatMultilineString(cells[0].data.source), 4); - - // Last cell should generate a series of updates. Verify we end up with a single image - await addCode(getOrCreateInteractiveWindow, wrapper, concatMultilineString(cells[1].data.source), 10); - const cell = getLastOutputCell(wrapper); - - const output = cell!.find('div.cell-output'); - assert.ok(output.length > 0, 'No output cell found'); - const outHtml = output.html(); - - const root = parse(outHtml) as any; - const svgs = root.querySelectorAll('svg') as HTMLElement[]; - assert.ok(svgs, 'No svgs found'); - assert.equal(svgs.length, 1, 'Wrong number of svgs'); - } - - }, () => { return ioc; }); - - runMountedTest('Copy back to source', async (_wrapper) => { - ioc.addDocument(`#%%${os.EOL}print("bar")`, 'foo.py'); - const docManager = ioc.get<IDocumentManager>(IDocumentManager); - docManager.showTextDocument(docManager.textDocuments[0]); - const window = await getOrCreateInteractiveWindow() as InteractiveWindow; - window.copyCode({source: 'print("baz")'}); - assert.equal(docManager.textDocuments[0].getText(), `#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("bar")`, 'Text not inserted'); - const activeEditor = docManager.activeTextEditor as MockEditor; - activeEditor.selection = new Selection(1, 2, 1, 2); - window.copyCode({source: 'print("baz")'}); - assert.equal(docManager.textDocuments[0].getText(), `#%%${os.EOL}#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("baz")${os.EOL}#%%${os.EOL}print("bar")`, 'Text not inserted'); - }, () => { return ioc; }); -}); diff --git a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts b/src/test/datascience/interactiveWindowCommandListener.unit.test.ts deleted file mode 100644 index 0f9eef3ab9b4..000000000000 --- a/src/test/datascience/interactiveWindowCommandListener.unit.test.ts +++ /dev/null @@ -1,275 +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 * as uuid from 'uuid/v4'; -import { Disposable, EventEmitter, Uri } from 'vscode'; - -import { ApplicationShell } from '../../client/common/application/applicationShell'; -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, IDisposable, ILogger } from '../../client/common/types'; -import { generateCells } from '../../client/datascience/cellFactory'; -import { Commands } from '../../client/datascience/constants'; -import { InteractiveWindowCommandListener } from '../../client/datascience/interactive-window/interactiveWindowCommandListener'; -import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; -import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; -import { JupyterExporter } from '../../client/datascience/jupyter/jupyterExporter'; -import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; -import { IInteractiveWindow, 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 { MockCommandManager } from './mockCommandManager'; -import { MockDocumentManager } from './mockDocumentManager'; - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length - -function createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { - // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class - // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 - const result = TypeMoq.Mock.ofType<T>(); - (result as any).tag = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; -} - -class MockStatusProvider implements IStatusProvider { - public set(_message: string, _timeout?: number): Disposable { - return { - dispose: noop - }; - } - - public waitWithStatus<T>(promise: () => Promise<T>, _message: string, _timeout?: number, _canceled?: () => void): Promise<T> { - return promise(); - } - -} - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length -suite('Interactive window command listener', async () => { - const interpreterService = mock(InterpreterService); - const configService = mock(ConfigurationService); - const knownSearchPaths = mock(KnownSearchPathsForInterpreters); - const logger = mock(Logger); - const fileSystem = mock(FileSystem); - const serviceContainer = mock(ServiceContainer); - const dummyEvent = new EventEmitter<void>(); - const pythonSettings = new PythonSettings(undefined, new MockAutoSelectionService()); - const disposableRegistry: IDisposable[] = []; - const interactiveWindowProvider = mock(InteractiveWindowProvider); - const notebookImporter = mock(JupyterImporter); - const notebookExporter = mock(JupyterExporter); - const applicationShell = mock(ApplicationShell); - const jupyterExecution = mock(JupyterExecutionFactory); - const interactiveWindow = createTypeMoq<IInteractiveWindow>('Interactive Window'); - const documentManager = new MockDocumentManager(); - const statusProvider = new MockStatusProvider(); - const commandManager = new MockCommandManager(); - const server = createTypeMoq<INotebookServer>('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(): InteractiveWindowCommandListener { - // 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>(IConfigurationService)).thenReturn(instance(configService)); - when(serviceContainer.get<IFileSystem>(IFileSystem)).thenReturn(instance(fileSystem)); - when(serviceContainer.get<ILogger>(ILogger)).thenReturn(instance(logger)); - when(configService.getSettings()).thenReturn(pythonSettings); - - // Setup default settings - pythonSettings.datascience = { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: '', - changeDirOnImportExport: true, - notebookFileRoot: 'WORKSPACE', - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - autoPreviewNotebooksInInteractivePane: true, - enablePlotViewer: 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); - - // mocks doesn't work with resolving things that also have promises, so use typemoq instead. - interactiveWindow.setup(s => s.previewNotebook(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - - when(interactiveWindowProvider.getActive()).thenReturn(interactiveWindow.object); - when(interactiveWindowProvider.getOrCreateActive()).thenResolve(interactiveWindow.object); - when(notebookImporter.importFromFile(anything())).thenResolve('imported'); - const metadata: nbformat.INotebookMetadata = { - language_info: { - name: 'python', - codemirror_mode: { - name: 'ipython', - version: 3 - } - }, - orig_nbformat: 2, - file_extension: '.py', - mimetype: 'text/x-python', - name: 'python', - npconvert_exporter: 'python', - pygments_lexer: `ipython${3}`, - version: 3 - }; - when(notebookExporter.translateToNotebook(anything())).thenResolve( - { - cells: [], - nbformat: 4, - nbformat_minor: 2, - metadata: metadata - } - ); - - if (jupyterExecution.isNotebookSupported) { - when(jupyterExecution.isNotebookSupported()).thenResolve(true); - } - - documentManager.addDocument('#%%\r\nprint("code")', 'bar.ipynb'); - - when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); - when(applicationShell.showInformationMessage(anything())).thenReturn(Promise.resolve('moo')); - - const result = new InteractiveWindowCommandListener( - disposableRegistry, - instance(interactiveWindowProvider), - instance(notebookExporter), - instance(jupyterExecution), - documentManager, - instance(applicationShell), - instance(fileSystem), - instance(logger), - instance(configService), - statusProvider, - instance(notebookImporter)); - result.register(commandManager); - - return result; - } - - test('Import', async () => { - createCommandListener(); - when(applicationShell.showOpenDialog(argThat(o => o.openLabel && o.openLabel.includes('Import')))).thenReturn(Promise.resolve([Uri.file('foo')])); - await commandManager.executeCommand(Commands.ImportNotebook, undefined, undefined); - assert.ok(documentManager.activeTextEditor, 'Imported file was not opened'); - }); - test('Import File', async () => { - createCommandListener(); - await commandManager.executeCommand(Commands.ImportNotebook, Uri.file('bar.ipynb'), undefined); - assert.ok(documentManager.activeTextEditor, 'Imported file was not opened'); - }); - test('Export File', async () => { - createCommandListener(); - const doc = await documentManager.openTextDocument('bar.ipynb'); - await documentManager.showTextDocument(doc); - when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo'))); - - await commandManager.executeCommand(Commands.ExportFileAsNotebook, Uri.file('bar.ipynb'), undefined); - assert.ok(lastFileContents, 'Export file was not written to'); - }); - test('Export File and output', async () => { - createCommandListener(); - const doc = await documentManager.openTextDocument('bar.ipynb'); - await documentManager.showTextDocument(doc); - when(jupyterExecution.connectToNotebookServer(anything(), anything())).thenResolve(server.object); - server.setup(s => s.execute(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAnyNumber(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return Promise.resolve(generateCells(undefined, 'a=1', 'bar.py', 0, false, uuid())); - }); - - when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo'))); - when(applicationShell.showInformationMessage(anything(), anything())).thenReturn(Promise.resolve('moo')); - - 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(); - when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo'))); - await commandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('bar.ipynb')); - assert.notExists(lastFileContents, 'Export file was written to'); - }); - test('Export happens on no file', async () => { - createCommandListener(); - const doc = await documentManager.openTextDocument('bar.ipynb'); - await documentManager.showTextDocument(doc); - when(applicationShell.showSaveDialog(argThat(o => o.saveLabel && o.saveLabel.includes('Export')))).thenReturn(Promise.resolve(Uri.file('foo'))); - await commandManager.executeCommand(Commands.ExportFileAsNotebook, undefined, undefined); - assert.ok(lastFileContents, 'Export file was not written to'); - }); - -}); diff --git a/src/test/datascience/interactiveWindowTestHelpers.tsx b/src/test/datascience/interactiveWindowTestHelpers.tsx deleted file mode 100644 index d37758162726..000000000000 --- a/src/test/datascience/interactiveWindowTestHelpers.tsx +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as assert from 'assert'; -import { mount, ReactWrapper } from 'enzyme'; -import { min } from 'lodash'; -import * as path from 'path'; -import * as React from 'react'; -import { CancellationToken } from 'vscode'; - -import { EXTENSION_ROOT_DIR } from '../../client/common/constants'; -import { IDataScienceSettings } from '../../client/common/types'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IInteractiveWindow, IJupyterExecution } from '../../client/datascience/types'; -import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; -import { ImageButton } from '../../datascience-ui/react-common/imageButton'; -import { updateSettings } from '../../datascience-ui/react-common/settingsReactSide'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { createInputEvent, createKeyboardEvent, waitForUpdate } from './reactHelpers'; - -//tslint:disable:trailing-comma no-any no-multiline-string -export enum CellInputState { - Hidden, - Visible, - Collapsed, - Expanded -} - -export enum CellPosition { - First = 'first', - Last = 'last' -} - -// tslint:disable-next-line:no-any -export function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>, getIOC: () => DataScienceIocContainer) { - test(name, async () => { - const ioc = getIOC(); - const jupyterExecution = ioc.get<IJupyterExecution>(IJupyterExecution); - if (await jupyterExecution.isNotebookSupported()) { - addMockData(ioc, 'a=1\na', 1); - const wrapper = mountWebView(ioc, <MainPanel baseTheme='vscode-light' codeTheme='light_vs' testMode={true} skipDefault={true} />); - await testFunc(wrapper); - } else { - // tslint:disable-next-line:no-console - console.log(`${name} skipped, no Jupyter installed.`); - } - }); -} - -//export async function getOrCreateHistory(ioc: DataScienceIocContainer): Promise<IInteractiveWindow> { - //const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - //const result = await interactiveWindowProvider.getOrCreateActive(); - - //// During testing the MainPanel sends the init message before our history is created. - //// Pretend like it's happening now - //const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - //listener.onMessage(InteractiveWindowMessages.Started, {}); - - //return result; -//} - -export function mountWebView(ioc: DataScienceIocContainer, node: React.ReactElement<any>): ReactWrapper<any, Readonly<{}>, React.Component> { - // Setup our webview panel - ioc.createWebView(() => mount(node)); - return ioc.wrapper!; -} - -export function addMockData(ioc: DataScienceIocContainer, code: string, result: string | number | undefined, mimeType?: string, cellType?: string) { - if (ioc.mockJupyter) { - if (cellType && cellType === 'error') { - ioc.mockJupyter.addError(code, result ? result.toString() : ''); - } else { - if (result) { - ioc.mockJupyter.addCell(code, result, mimeType); - } else { - ioc.mockJupyter.addCell(code); - } - } - } -} - -export function addContinuousMockData(ioc: DataScienceIocContainer, code: string, resultGenerator: (c: CancellationToken) => Promise<{ result: string; haveMore: boolean }>) { - if (ioc.mockJupyter) { - ioc.mockJupyter.addContinuousOutputCell(code, resultGenerator); - } -} - -export function getLastOutputCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>): ReactWrapper<any, Readonly<{}>, React.Component> { - // Skip the edit cell - const foundResult = wrapper.find('Cell'); - assert.ok(foundResult.length >= 2, 'Didn\'t find any cells being rendered'); - return foundResult.at(foundResult.length - 2); -} - -export function verifyHtmlOnCell(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, html: string | undefined, cellIndex: number | CellPosition) { - const foundResult = wrapper.find('Cell'); - assert.ok(foundResult.length >= 1, 'Didn\'t find any cells being rendered'); - - let targetCell: ReactWrapper; - // Get the correct result that we are dealing with - if (typeof cellIndex === 'number') { - if (cellIndex >= 0 && cellIndex <= (foundResult.length - 1)) { - targetCell = foundResult.at(cellIndex); - } - } else if (typeof cellIndex === 'string') { - switch (cellIndex) { - case CellPosition.First: - targetCell = foundResult.first(); - break; - - case CellPosition.Last: - // Skip the input cell on these checks. - targetCell = getLastOutputCell(wrapper); - break; - - default: - // Fall through, targetCell check will fail out - break; - } - } - - // ! is ok here to get rid of undefined type check as we want a fail here if we have not initialized targetCell - assert.ok(targetCell!, 'Target cell doesn\'t exist'); - - // If html is specified, check it - if (html) { - // Extract only the first 100 chars from the input string - const sliced = html.substr(0, min([html.length, 100])); - const output = targetCell!.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}`); - } else { - // html not specified, look for an empty render - assert.ok(targetCell!.isEmptyRender(), 'Target cell is not empty render'); - } -} - -export function verifyLastCellInputState(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, state: CellInputState) { - - const lastCell = getLastOutputCell(wrapper); - assert.ok(lastCell, 'Last call doesn\'t exist'); - - const inputBlock = lastCell.find('div.cell-input'); - const toggleButton = lastCell.find('polygon.collapse-input-svg'); - - switch (state) { - case CellInputState.Hidden: - assert.ok(inputBlock.length === 0, 'Cell input not hidden'); - break; - - case CellInputState.Visible: - assert.ok(inputBlock.length === 1, 'Cell input not visible'); - break; - - case CellInputState.Expanded: - assert.ok(toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not expanded'); - break; - - case CellInputState.Collapsed: - assert.ok(!toggleButton.html().includes('collapse-input-svg-rotate'), 'Cell input toggle not collapsed'); - break; - - default: - assert.fail('Unknown cellInputStat'); - break; - } -} - -export async function getCellResults(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'); -} - -export async function addCode(interactiveWindowProvider: () => Promise<IInteractiveWindow>, 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 = await interactiveWindowProvider(); - await history.addCode(code, 'foo.py', 2); - }); -} - -function simulateKey(domNode: HTMLTextAreaElement, key: string, shiftDown?: boolean) { - // Submit a keypress into the textarea. Simulate doesn't work here because the keydown - // handler is not registered in any react code. It's being handled with DOM events - - // According to this: - // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent#Usage_notes - // The normal events are - // 1) keydown - // 2) keypress - // 3) keyup - let event = createKeyboardEvent('keydown', { key, code: key, shiftKey: shiftDown }); - - // Dispatch. Result can be swallowed. If so skip the next event. - let result = domNode.dispatchEvent(event); - if (result) { - event = createKeyboardEvent('keypress', { key, code: key, shiftKey: shiftDown }); - result = domNode.dispatchEvent(event); - if (result) { - event = createKeyboardEvent('keyup', { key, code: key, shiftKey: shiftDown }); - domNode.dispatchEvent(event); - - // Update our value. This will reset selection to zero. - domNode.value = domNode.value + key; - - // Tell the dom node its selection start has changed. Monaco - // reads this to determine where the character went. - domNode.selectionEnd = domNode.value.length; - domNode.selectionStart = domNode.value.length; - - // Dispatch an input event so we update the textarea - domNode.dispatchEvent(createInputEvent()); - } - } - -} - -async function submitInput(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, textArea: HTMLTextAreaElement): Promise<void> { - // Get a render promise with the expected number of renders (how many updates a the shift + enter will cause) - // Should be 6 - 1 for the shift+enter and 5 for the new cell. - const renderPromise = waitForUpdate(wrapper, MainPanel, 6); - - // Submit a keypress into the textarea - simulateKey(textArea, '\n', true); - - return renderPromise; -} - -function enterKey(_wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, textArea: HTMLTextAreaElement, key: string) { - // Simulate a key press - simulateKey(textArea, key); -} - -export function getEditor(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) : ReactWrapper<any, Readonly<{}>, React.Component> { - // Find the last cell. It should have a monacoEditor object - const cells = wrapper.find('Cell'); - const lastCell = cells.last(); - return lastCell.find('MonacoEditor'); -} - -export function typeCode(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string) : HTMLTextAreaElement | null { - - // Find the last cell. It should have a monacoEditor object. We need to search - // through its DOM to find the actual textarea to send input to - // (we can't actually find it with the enzyme wrappers because they only search - // React accessible nodes and the monaco html is not react) - const editorControl = getEditor(wrapper); - const ecDom = editorControl.getDOMNode(); - assert.ok(ecDom, 'ec DOM object not found'); - const textArea = ecDom!.querySelector('.overflow-guard')!.querySelector('textarea'); - assert.ok(textArea!, 'Cannot find the textarea inside the monaco editor'); - textArea!.focus(); - - // Now simulate entering all of the keys - for (let i = 0; i < code.length; i += 1) { - enterKey(wrapper, textArea!, code.charAt(i)); - } - - return textArea; -} - -export async function enterInput(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, code: string): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - - // First we have to type the code into the input box - const textArea = typeCode(wrapper, code); - - // Now simulate a shift enter. This should cause a new cell to be added - await submitInput(wrapper, textArea!); - - // Return the result - return wrapper.find('Cell'); -} - -export function findButton(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, index: number): ReactWrapper<any, Readonly<{}>, React.Component> | undefined { - const mainObj = wrapper.find(MainPanel); - if (mainObj) { - const buttons = mainObj.find(ImageButton); - if (buttons) { - return buttons.at(index); - } - } -} - -// The default base set of data science settings to use -export function defaultDataScienceSettings(): IDataScienceSettings { - return { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - maxOutputSize: 400, - errorBackgroundColor: '#FFFFFF', - sendSelectionToInteractiveWindow: false, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method', - codeRegularExpression: '^(#\\s*%%|#\\s*\\<codecell\\>|#\\s*In\\[\\d*?\\]|#\\s*In\\[ \\])', - markdownRegularExpression: '^(#\\s*%%\\s*\\[markdown\\]|#\\s*\\<markdowncell\\>)', - enablePlotViewer: true - }; -} - -// Set initial data science settings to use for a test (initially loaded via settingsReactSide.ts) -export function initialDataScienceSettings(newSettings: IDataScienceSettings) { - const settingsString = JSON.stringify(newSettings); - updateSettings(settingsString); -} - -export function getMainPanel(wrapper: ReactWrapper<any, Readonly<{}>>): MainPanel | undefined { - const mainObj = wrapper.find(MainPanel); - if (mainObj) { - return mainObj.instance() as MainPanel; - } - - return undefined; -} - -// Update data science settings while running (goes through the UpdateSettings channel) -export function updateDataScienceSettings(wrapper: ReactWrapper<any, Readonly<{}>>, newSettings: IDataScienceSettings) { - const settingsString = JSON.stringify(newSettings); - const mainPanel = getMainPanel(wrapper); - if (mainPanel) { - mainPanel.handleMessage(InteractiveWindowMessages.UpdateSettings, settingsString); - } - wrapper.update(); -} - -export function toggleCellExpansion(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) { - // Find the last cell added - const lastCell = getLastOutputCell(wrapper); - assert.ok(lastCell, 'Last call doesn\'t exist'); - - const toggleButton = lastCell.find('button.collapse-input'); - assert.ok(toggleButton); - toggleButton.simulate('click'); -} - -export function escapePath(p: string) { - return p.replace(/\\/g, '\\\\'); -} - -export function srcDirectory() { - return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); -} diff --git a/src/test/datascience/jupyterPasswordConnect.unit.test.ts b/src/test/datascience/jupyterPasswordConnect.unit.test.ts deleted file mode 100644 index 43766ef86ff3..000000000000 --- a/src/test/datascience/jupyterPasswordConnect.unit.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as assert from 'assert'; -import * as nodeFetch from 'node-fetch'; -import * as typemoq from 'typemoq'; - -import { IApplicationShell } from '../../client/common/application/types'; -import { JupyterPasswordConnect } from '../../client/datascience/jupyter/jupyterPasswordConnect'; - -// tslint:disable:no-any max-func-body-length -suite('JupyterPasswordConnect', () => { - let jupyterPasswordConnect: JupyterPasswordConnect; - let appShell: typemoq.IMock<IApplicationShell>; - - const xsrfValue: string = '12341234'; - const sessionName: string = 'sessionName'; - const sessionValue: string = 'sessionValue'; - - setup(() => { - appShell = typemoq.Mock.ofType<IApplicationShell>(); - appShell.setup(a => a.showInputBox(typemoq.It.isAny())).returns(() => Promise.resolve('Python')); - jupyterPasswordConnect = new JupyterPasswordConnect(appShell.object); - }); - - test('getPasswordConnectionInfo', async() => { - // Set up our fake node fetch - const fetchMock: typemoq.IMock<typeof nodeFetch.default> = typemoq.Mock.ofInstance(nodeFetch.default); - - // Mock our first call to get xsrf cookie - const mockXsrfResponse = typemoq.Mock.ofType(nodeFetch.Response); - const mockXsrfHeaders = typemoq.Mock.ofType(nodeFetch.Headers); - mockXsrfHeaders.setup(mh => mh.get('set-cookie')).returns(() => `_xsrf=${xsrfValue}`).verifiable(typemoq.Times.once()); - mockXsrfResponse.setup(mr => mr.ok).returns(() => true).verifiable(typemoq.Times.once()); - mockXsrfResponse.setup(mr => mr.headers).returns(() => mockXsrfHeaders.object).verifiable(typemoq.Times.once()); - - //tslint:disable-next-line:no-http-string - fetchMock.setup(fm => fm('http://TESTNAME:8888/login?', { - method: 'get', - redirect: 'manual', - headers: { Connection: 'keep-alive' } - })).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once()); - - // Mock our second call to get session cookie - const mockSessionResponse = typemoq.Mock.ofType(nodeFetch.Response); - const mockSessionHeaders = typemoq.Mock.ofType(nodeFetch.Headers); - mockSessionHeaders.setup(mh => mh.get('set-cookie')).returns(() => `${sessionName}=${sessionValue}`).verifiable(typemoq.Times.once()); - mockSessionResponse.setup(mr => mr.status).returns(() => 302).verifiable(typemoq.Times.once()); - mockSessionResponse.setup(mr => mr.headers).returns(() => mockSessionHeaders.object).verifiable(typemoq.Times.once()); - - // typemoq doesn't love this comparison, so generalize it a bit - //tslint:disable-next-line:no-http-string - fetchMock.setup(fm => fm('http://TESTNAME:8888/login?', typemoq.It.isObjectWith({ - method: 'post', - headers: { Cookie: `_xsrf=${xsrfValue}`, Connection: 'keep-alive', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' } - }))).returns(() => Promise.resolve(mockSessionResponse.object)).verifiable(typemoq.Times.once()); - - //tslint:disable-next-line:no-http-string - const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', false, fetchMock.object); - assert(result, 'Failed to get password'); - if (result) { - assert(result.xsrfCookie === xsrfValue, 'Incorrect xsrf value'); - assert(result.sessionCookieName === sessionName, 'Incorrect session name'); - assert(result.sessionCookieValue === sessionValue, 'Incorrect session value'); - } - - // Verfiy calls - mockXsrfHeaders.verifyAll(); - mockSessionHeaders.verifyAll(); - mockXsrfResponse.verifyAll(); - mockSessionResponse.verifyAll(); - fetchMock.verifyAll(); - }); - - test('getPasswordConnectionInfo allowUnauthorized', async() => { - // Set up our fake node fetch - const fetchMock: typemoq.IMock<typeof nodeFetch.default> = typemoq.Mock.ofInstance(nodeFetch.default); - - // Mock our first call to get xsrf cookie - const mockXsrfResponse = typemoq.Mock.ofType(nodeFetch.Response); - const mockXsrfHeaders = typemoq.Mock.ofType(nodeFetch.Headers); - mockXsrfHeaders.setup(mh => mh.get('set-cookie')).returns(() => `_xsrf=${xsrfValue}`).verifiable(typemoq.Times.once()); - mockXsrfResponse.setup(mr => mr.ok).returns(() => true).verifiable(typemoq.Times.once()); - mockXsrfResponse.setup(mr => mr.headers).returns(() => mockXsrfHeaders.object).verifiable(typemoq.Times.once()); - - //tslint:disable-next-line:no-http-string - fetchMock.setup(fm => fm('https://TESTNAME:8888/login?', typemoq.It.isObjectWith({ - method: 'get', - headers: { Connection: 'keep-alive' } - }))).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once()); - - // Mock our second call to get session cookie - const mockSessionResponse = typemoq.Mock.ofType(nodeFetch.Response); - const mockSessionHeaders = typemoq.Mock.ofType(nodeFetch.Headers); - mockSessionHeaders.setup(mh => mh.get('set-cookie')).returns(() => `${sessionName}=${sessionValue}`).verifiable(typemoq.Times.once()); - mockSessionResponse.setup(mr => mr.status).returns(() => 302).verifiable(typemoq.Times.once()); - mockSessionResponse.setup(mr => mr.headers).returns(() => mockSessionHeaders.object).verifiable(typemoq.Times.once()); - - // typemoq doesn't love this comparison, so generalize it a bit - //tslint:disable-next-line:no-http-string - fetchMock.setup(fm => fm('https://TESTNAME:8888/login?', typemoq.It.isObjectWith({ - method: 'post', - headers: { Cookie: `_xsrf=${xsrfValue}`, Connection: 'keep-alive', 'content-type': 'application/x-www-form-urlencoded;charset=UTF-8' } - }))).returns(() => Promise.resolve(mockSessionResponse.object)).verifiable(typemoq.Times.once()); - - //tslint:disable-next-line:no-http-string - const result = await jupyterPasswordConnect.getPasswordConnectionInfo('https://TESTNAME:8888/', true, fetchMock.object); - assert(result, 'Failed to get password'); - if (result) { - assert(result.xsrfCookie === xsrfValue, 'Incorrect xsrf value'); - assert(result.sessionCookieName === sessionName, 'Incorrect session name'); - assert(result.sessionCookieValue === sessionValue, 'Incorrect session value'); - } - - // Verfiy calls - mockXsrfHeaders.verifyAll(); - mockSessionHeaders.verifyAll(); - mockXsrfResponse.verifyAll(); - mockSessionResponse.verifyAll(); - fetchMock.verifyAll(); - }); - - test('getPasswordConnectionInfo failure', async() => { - // Set up our fake node fetch - const fetchMock: typemoq.IMock<typeof nodeFetch.default> = typemoq.Mock.ofInstance(nodeFetch.default); - - // Mock our first call to get xsrf cookie - const mockXsrfResponse = typemoq.Mock.ofType(nodeFetch.Response); - const mockXsrfHeaders = typemoq.Mock.ofType(nodeFetch.Headers); - mockXsrfHeaders.setup(mh => mh.get('set-cookie')).returns(() => `_xsrf=${xsrfValue}`).verifiable(typemoq.Times.never()); - // Status set to not ok and header fetch should not be hit - mockXsrfResponse.setup(mr => mr.ok).returns(() => false).verifiable(typemoq.Times.once()); - mockXsrfResponse.setup(mr => mr.headers).returns(() => mockXsrfHeaders.object).verifiable(typemoq.Times.never()); - - //tslint:disable-next-line:no-http-string - fetchMock.setup(fm => fm('http://TESTNAME:8888/login?', { - method: 'get', - redirect: 'manual', - headers: { Connection: 'keep-alive' } - })).returns(() => Promise.resolve(mockXsrfResponse.object)).verifiable(typemoq.Times.once()); - - //tslint:disable-next-line:no-http-string - const result = await jupyterPasswordConnect.getPasswordConnectionInfo('http://TESTNAME:8888/', false, fetchMock.object); - assert(!result); - - // Verfiy calls - mockXsrfHeaders.verifyAll(); - mockXsrfResponse.verifyAll(); - fetchMock.verifyAll(); - }); - -}); diff --git a/src/test/datascience/jupyterVariables.unit.test.ts b/src/test/datascience/jupyterVariables.unit.test.ts deleted file mode 100644 index 8354b9d59345..000000000000 --- a/src/test/datascience/jupyterVariables.unit.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import { nbformat } from '@jupyterlab/coreutils'; -import * as assert from 'assert'; -import * as typemoq from 'typemoq'; - -import { IFileSystem } from '../../client/common/platform/types'; -import { Identifiers } from '../../client/datascience/constants'; -import { JupyterVariables } from '../../client/datascience/jupyter/jupyterVariables'; -import { CellState, ICell, IInteractiveWindowProvider, IJupyterExecution, IJupyterVariable, INotebookServer } from '../../client/datascience/types'; - -// tslint:disable:no-any max-func-body-length -suite('JupyterVariables', () => { - let execution: typemoq.IMock<IJupyterExecution>; - let interactiveWindowProvider: typemoq.IMock<IInteractiveWindowProvider>; - let fakeServer: typemoq.IMock<INotebookServer>; - let jupyterVariables: JupyterVariables; - let fileSystem: typemoq.IMock<IFileSystem>; - - function generateVariableOutput(outputData: string, outputType: string): nbformat.IOutput { - return { - output_type: outputType, - text: outputData - }; - } - - function generateCell(outputData: string, outputType: string, hasOutput: boolean): ICell { - return { - data: { - cell_type: 'code', - execution_count: 0, - metadata: {}, - outputs: hasOutput ? [generateVariableOutput(outputData, outputType)] : [], - source: '' - }, - id: '0', - file: '', - line: 0, - state: CellState.finished, - type: 'execute' - }; - } - - function generateCells(outputData: string, outputType: string, hasOutput: boolean = true): ICell[] { - return [generateCell(outputData, outputType, hasOutput)]; - } - - function createTypeMoq<T>(tag: string): typemoq.IMock<T> { - // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class - // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 - const result: typemoq.IMock<T> = typemoq.Mock.ofType<T>(); - (result as any).tag = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; - } - - setup(() => { - execution = typemoq.Mock.ofType<IJupyterExecution>(); - interactiveWindowProvider = typemoq.Mock.ofType<IInteractiveWindowProvider>(); - // Create our fake notebook server - fakeServer = createTypeMoq<INotebookServer>('Fake Server'); - - fileSystem = typemoq.Mock.ofType<IFileSystem>(); - fileSystem.setup(fs => fs.readFile(typemoq.It.isAnyString())) - .returns(() => Promise.resolve('test')); - - jupyterVariables = new JupyterVariables(fileSystem.object, execution.object, interactiveWindowProvider.object); - }); - - test('getVariables no server', async() => { - execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { - return Promise.resolve(undefined); - }); - - fakeServer.setup(fs => fs.execute(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny(), undefined, typemoq.It.isAny())) - .returns(() => Promise.resolve(generateCells( - '[{"name": "big_dataframe", "type": "DataFrame", "size": 62}, {"name": "big_dict", "type": "dict", "size": 57}, {"name": "big_float", "type": "float", "size": 58}, {"name": "big_int", "type": "int", "size": 56}, {"name": "big_list", "type": "list", "size": 57}, {"name": "big_nparray", "type": "ndarray", "size": 60}, {"name": "big_series", "type": "Series", "size": 59}, {"name": "big_string", "type": "str", "size": 59}, {"name": "big_tuple", "type": "tuple", "size": 58]', - 'stream' - ))) - .verifiable(typemoq.Times.never()); - - const results = await jupyterVariables.getVariables(); - assert.equal(results.length, 0); - - fakeServer.verifyAll(); - }); - - // No cells, no output, no text/plain - test('getVariables no cells', async() => { - execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }); - - fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) - .returns(() => Promise.resolve([])) - .verifiable(typemoq.Times.once()); - - let exceptionThrown = false; - try { - await jupyterVariables.getVariables(); - } catch (exc) { - exceptionThrown = true; - } - - assert.equal(exceptionThrown, true); - fakeServer.verifyAll(); - }); - - test('getVariables no output', async() => { - execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }); - - fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) - .returns(() => Promise.resolve(generateCells('', 'stream', false))) - .verifiable(typemoq.Times.once()); - - let exceptionThrown = false; - try { - await jupyterVariables.getVariables(); - } catch (exc) { - exceptionThrown = true; - } - - assert.equal(exceptionThrown, true); - fakeServer.verifyAll(); - }); - - test('getVariables bad output type', async() => { - execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }); - - fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) - .returns(() => Promise.resolve(generateCells( - 'bogus string', - 'bogus output type' - ))) - .verifiable(typemoq.Times.once()); - - let exceptionThrown = false; - try { - await jupyterVariables.getVariables(); - } catch (exc) { - exceptionThrown = true; - } - - assert.equal(exceptionThrown, true); - fakeServer.verifyAll(); - }); - - test('getVariables fake data', async() => { - execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }); - - fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) - .returns(() => Promise.resolve(generateCells( - '[{"name": "big_dataframe", "type": "DataFrame", "size": 62}, {"name": "big_dict", "type": "dict", "size": 57}, {"name": "big_int", "type": "int", "size": 56}, {"name": "big_list", "type": "list", "size": 57}, {"name": "big_nparray", "type": "ndarray", "size": 60}, {"name": "big_string", "type": "str", "size": 59}]', - 'stream' - ))) - .verifiable(typemoq.Times.once()); - - const results = await jupyterVariables.getVariables(); - - // Check the results that we get back - assert.equal(results.length, 6); - - // Check our items (just the first few real items, no need to check all 19) - assert.deepEqual(results[0], {name: 'big_dataframe', size: 62, type: 'DataFrame'}); - assert.deepEqual(results[1], {name: 'big_dict', size: 57, type: 'dict'}); - assert.deepEqual(results[2], {name: 'big_int', size: 56, type: 'int'}); - assert.deepEqual(results[3], {name: 'big_list', size: 57, type: 'list'}); - assert.deepEqual(results[4], {name: 'big_nparray', size: 60, type: 'ndarray'}); - assert.deepEqual(results[5], {name: 'big_string', size: 59, type: 'str'}); - - fakeServer.verifyAll(); - }); - - // getValue failure paths are shared with getVariables, so no need to test them here - test('getValue fake data', async() => { - execution.setup(sm => sm.getServer(typemoq.It.isAny())).returns(() => { - return Promise.resolve(fakeServer.object); - }); - - fakeServer.setup(fs => fs.execute(typemoq.It.isValue('test'), typemoq.It.isValue(Identifiers.EmptyFileName), typemoq.It.isValue(0), typemoq.It.isAnyString(), undefined, typemoq.It.isValue(true))) - .returns(() => Promise.resolve(generateCells( - '{"name": "big_complex", "type": "complex", "size": 60, "value": "(1+1j)"}', - 'stream' - ))) - .verifiable(typemoq.Times.once()); - - const testVariable: IJupyterVariable = { name: 'big_complex', type: 'complex', size: 60, truncated: false, count: 0, shape: '', value: '', supportsDataExplorer: false }; - - const resultVariable = await jupyterVariables.getValue(testVariable); - - // Verify the result value should be filled out from fake server result - assert.deepEqual(resultVariable, {name: 'big_complex', size: 60, type: 'complex', value: '(1+1j)'}); - fakeServer.verifyAll(); - }); -}); diff --git a/src/test/datascience/liveloss.py b/src/test/datascience/liveloss.py deleted file mode 100644 index a8187e717761..000000000000 --- a/src/test/datascience/liveloss.py +++ /dev/null @@ -1,18 +0,0 @@ -#%% -from time import sleep -import numpy as np - -from livelossplot import PlotLosses - -#%% -liveplot = PlotLosses() - -for i in range(10): - liveplot.update({ - 'accuracy': 1 - np.random.rand() / (i + 2.), - 'val_accuracy': 1 - np.random.rand() / (i + 0.5), - 'mse': 1. / (i + 2.), - 'val_mse': 1. / (i + 0.5) - }) - liveplot.draw() - sleep(1.) diff --git a/src/test/datascience/liveshare.functional.test.tsx b/src/test/datascience/liveshare.functional.test.tsx deleted file mode 100644 index 7dbda32e380b..000000000000 --- a/src/test/datascience/liveshare.functional.test.tsx +++ /dev/null @@ -1,329 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as assert from 'assert'; -import { mount, ReactWrapper } from 'enzyme'; -import * as React from 'react'; -import * as TypeMoq from 'typemoq'; -import { Disposable, Uri } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { - IApplicationShell, - ICommandManager, - IDocumentManager, - ILiveShareApi, - ILiveShareTestingApi -} from '../../client/common/application/types'; -import { IFileSystem } from '../../client/common/platform/types'; -import { Commands } from '../../client/datascience/constants'; -import { ICodeWatcher, IDataScienceCommandListener, IInteractiveWindow, IInteractiveWindowProvider, IJupyterExecution } from '../../client/datascience/types'; -import { MainPanel } from '../../datascience-ui/history-react/MainPanel'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { createDocument } from './editor-integration/helpers'; -import { addMockData, CellPosition, verifyHtmlOnCell } from './interactiveWindowTestHelpers'; -import { waitForUpdate } from './reactHelpers'; - -//tslint:disable:trailing-comma no-any no-multiline-string - -// tslint:disable-next-line:max-func-body-length no-any -suite('DataScience LiveShare tests', () => { - const disposables: Disposable[] = []; - let hostContainer: DataScienceIocContainer; - let guestContainer: DataScienceIocContainer; - let lastErrorMessage : string | undefined; - - setup(() => { - hostContainer = createContainer(vsls.Role.Host); - guestContainer = createContainer(vsls.Role.Guest); - }); - - teardown(async () => { - for (const disposable of disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - await hostContainer.dispose(); - await guestContainer.dispose(); - lastErrorMessage = undefined; - }); - - function createContainer(role: vsls.Role): DataScienceIocContainer { - const result = new DataScienceIocContainer(); - result.registerDataScienceTypes(); - - // Rebind the appshell so we can change what happens on an error - const dummyDisposable = { - dispose: () => { return; } - }; - const appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAnyString())).returns((e) => lastErrorMessage = e); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve('')); - appShell.setup(a => a.showInformationMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_a1: string, a2: string, _a3: string) => Promise.resolve(a2)); - appShell.setup(a => a.showSaveDialog(TypeMoq.It.isAny())).returns(() => Promise.resolve(Uri.file('test.ipynb'))); - appShell.setup(a => a.setStatusBarMessage(TypeMoq.It.isAny())).returns(() => dummyDisposable); - - result.serviceManager.rebindInstance<IApplicationShell>(IApplicationShell, appShell.object); - - // Setup our webview panel - result.createWebView(() => mount(<MainPanel baseTheme='vscode-light' codeTheme='light_vs' testMode={true} skipDefault={true} />), role); - - // Make sure the history provider and execution factory in the container is created (the extension does this on startup in the extension) - // This is necessary to get the appropriate live share services up and running. - result.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - result.get<IJupyterExecution>(IJupyterExecution); - return result; - } - - function getOrCreateInteractiveWindow(role: vsls.Role): Promise<IInteractiveWindow> { - // Get the container to use based on the role. - const container = role === vsls.Role.Host ? hostContainer : guestContainer; - return container!.get<IInteractiveWindowProvider>(IInteractiveWindowProvider).getOrCreateActive(); - } - - function isSessionStarted(role: vsls.Role): boolean { - const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; - return api.isSessionStarted; - } - - async function waitForResults(role: vsls.Role, resultGenerator: (both: boolean) => Promise<void>, expectedRenderCount: number = 5): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - const container = role === vsls.Role.Host ? hostContainer : guestContainer; - - // If just the host session has started or nobody, just run the host. - const guestStarted = isSessionStarted(vsls.Role.Guest); - if (!guestStarted) { - const hostRenderPromise = waitForUpdate(hostContainer.wrapper!, MainPanel, expectedRenderCount); - - // Generate our results - await resultGenerator(false); - - // Wait for all of the renders to go through - await hostRenderPromise; - } else { - // Otherwise more complicated. We have to wait for renders on both - - // Get a render promise with the expected number of renders for both wrappers - const hostRenderPromise = waitForUpdate(hostContainer.wrapper!, MainPanel, expectedRenderCount); - const guestRenderPromise = waitForUpdate(guestContainer.wrapper!, MainPanel, expectedRenderCount); - - // Generate our results - await resultGenerator(true); - - // Wait for all of the renders to go through. Guest may have been shutdown by now. - await Promise.all([hostRenderPromise, isSessionStarted(vsls.Role.Guest) ? guestRenderPromise : Promise.resolve()]); - } - return container.wrapper!; - } - - async function addCodeToRole(role: vsls.Role, code: string, expectedRenderCount: number = 5): Promise<ReactWrapper<any, Readonly<{}>, React.Component>> { - return waitForResults(role, async (both: boolean) => { - if (!both) { - const history = await getOrCreateInteractiveWindow(role); - return history.addCode(code, 'foo.py', 2); - } else { - // Add code to the apropriate container - const host = await getOrCreateInteractiveWindow(vsls.Role.Host); - - // Make sure guest is still creatable - if (isSessionStarted(vsls.Role.Guest)) { - const guest = await getOrCreateInteractiveWindow(vsls.Role.Guest); - return (role === vsls.Role.Host ? host.addCode(code, 'foo.py', 2) : guest.addCode(code, 'foo.py', 2)); - } else { - return host.addCode(code, 'foo.py', 2); - } - } - }, expectedRenderCount); - } - - function startSession(role: vsls.Role): Promise<void> { - const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; - return api.startSession(); - } - - function stopSession(role: vsls.Role): Promise<void> { - const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; - return api.stopSession(); - } - - function disableGuestChecker(role: vsls.Role) { - const container = role === vsls.Role.Host ? hostContainer : guestContainer; - const api = container!.get<ILiveShareApi>(ILiveShareApi) as ILiveShareTestingApi; - api.disableGuestChecker(); - } - - test('Host alone', async () => { - // Should only need mock data in host - addMockData(hostContainer!, 'a=1\na', 1); - - // Start the host session first - await startSession(vsls.Role.Host); - - // Just run some code in the host - const wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - }); - - test('Host & Guest Simple', async () => { - // Should only need mock data in host - addMockData(hostContainer!, 'a=1\na', 1); - - // Create the host history and then the guest history - await getOrCreateInteractiveWindow(vsls.Role.Host); - await startSession(vsls.Role.Host); - await getOrCreateInteractiveWindow(vsls.Role.Guest); - await startSession(vsls.Role.Guest); - - // Send code through the host - const wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Verify it ended up on the guest too - assert.ok(guestContainer.wrapper, 'Guest wrapper not created'); - verifyHtmlOnCell(guestContainer.wrapper!, '<span>1</span>', CellPosition.Last); - }); - - test('Host Shutdown and Run', async () => { - // Should only need mock data in host - addMockData(hostContainer!, 'a=1\na', 1); - - // Create the host history and then the guest history - await getOrCreateInteractiveWindow(vsls.Role.Host); - await startSession(vsls.Role.Host); - - // Send code through the host - let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Stop the session - await stopSession(vsls.Role.Host); - - // Send code again. It should still work. - wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - }); - - test('Host startup and guest restart', async () => { - // Should only need mock data in host - addMockData(hostContainer!, 'a=1\na', 1); - - // Start the host, and add some data - const host = await getOrCreateInteractiveWindow(vsls.Role.Host); - await startSession(vsls.Role.Host); - - // Send code through the host - let wrapper = await addCodeToRole(vsls.Role.Host, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Shutdown the host - await host.dispose(); - - // Startup a guest and run some code. - await startSession(vsls.Role.Guest); - wrapper = await addCodeToRole(vsls.Role.Guest, 'a=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - assert.ok(hostContainer.wrapper, 'Host wrapper not created'); - verifyHtmlOnCell(hostContainer.wrapper!, '<span>1</span>', CellPosition.Last); - }); - - test('Going through codewatcher', async () => { - // Should only need mock data in host - addMockData(hostContainer!, '#%%\na=1\na', 1); - - // Start both the host and the guest - await startSession(vsls.Role.Host); - await startSession(vsls.Role.Guest); - - // Setup a document and text - const fileName = 'test.py'; - const version = 1; - const inputText = '#%%\na=1\na'; - const document = createDocument(inputText, fileName, version, TypeMoq.Times.atLeastOnce()); - document.setup(doc => doc.getText(TypeMoq.It.isAny())).returns(() => inputText); - - const codeWatcher = guestContainer!.get<ICodeWatcher>(ICodeWatcher); - codeWatcher.setDocument(document.object); - - // Send code using a codewatcher instead (we're sending it through the guest) - const wrapper = await waitForResults(vsls.Role.Guest, async (both: boolean) => { - // Should always be both - assert.ok(both, 'Expected both guest and host to be used'); - await codeWatcher.runAllCells(); - }); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - assert.ok(hostContainer.wrapper, 'Host wrapper not created for some reason'); - verifyHtmlOnCell(hostContainer.wrapper!, '<span>1</span>', CellPosition.Last); - }); - - test('Export from guest', async () => { - // Should only need mock data in host - addMockData(hostContainer!, '#%%\na=1\na', 1); - - // Remap the fileSystem so we control the write for the notebook. Have to do this - // before the listener is created so that it uses this file system. - let outputContents: string | undefined; - const fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - guestContainer!.serviceManager.rebindInstance<IFileSystem>(IFileSystem, fileSystem.object); - fileSystem.setup(f => f.writeFile(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_f, c) => { - outputContents = c.toString(); - return Promise.resolve(); - }); - fileSystem.setup(f => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true); - fileSystem.setup(f => f.getSubDirectories(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - - // Need to register commands as our extension isn't actually loading. - const listener = guestContainer!.get<IDataScienceCommandListener>(IDataScienceCommandListener); - const guestCommandManager = guestContainer!.get<ICommandManager>(ICommandManager); - listener.register(guestCommandManager); - - // Start both the host and the guest - await startSession(vsls.Role.Host); - await startSession(vsls.Role.Guest); - - // Create a document on the guest - guestContainer!.addDocument('#%%\na=1\na', 'foo.py'); - guestContainer!.get<IDocumentManager>(IDocumentManager).showTextDocument(Uri.file('foo.py')); - - // Attempt to export a file from the guest by running an ExportFileAndOutputAsNotebook - const executePromise = guestCommandManager.executeCommand(Commands.ExportFileAndOutputAsNotebook, Uri.file('foo.py')) as Promise<Uri>; - assert.ok(executePromise, 'Export file did not return a promise'); - const savedUri = await executePromise; - assert.ok(savedUri, 'Uri not returned from export'); - assert.equal(savedUri.fsPath, 'test.ipynb', 'Export did not work'); - assert.ok(outputContents, 'Output not exported'); - assert.ok(outputContents!.includes('data'), 'Output is empty'); - }); - - test('Guest does not have extension', async () => { - // Should only need mock data in host - addMockData(hostContainer!, '#%%\na=1\na', 1); - - // Start just the host and verify it works - await startSession(vsls.Role.Host); - let wrapper = await addCodeToRole(vsls.Role.Host, '#%%\na=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - - // Disable guest checking on the guest (same as if the guest doesn't have the python extension) - await startSession(vsls.Role.Guest); - disableGuestChecker(vsls.Role.Guest); - - // Host should now be in a state that if any code runs, the session should end. However - // the code should still run - wrapper = await addCodeToRole(vsls.Role.Host, '#%%\na=1\na'); - verifyHtmlOnCell(wrapper, '<span>1</span>', CellPosition.Last); - assert.equal(isSessionStarted(vsls.Role.Host), false, 'Host should have exited session'); - assert.equal(isSessionStarted(vsls.Role.Guest), false, 'Guest should have exited session'); - assert.ok(lastErrorMessage, 'Error was not set during session shutdown'); - }); - -}); diff --git a/src/test/datascience/manualTestFiles/manualTestFile.py b/src/test/datascience/manualTestFiles/manualTestFile.py deleted file mode 100644 index bd7165cf21d5..000000000000 --- a/src/test/datascience/manualTestFiles/manualTestFile.py +++ /dev/null @@ -1,50 +0,0 @@ -# To run this file either conda or pip install the following: jupyter, numpy, matplotlib, pandas, tqdm, bokeh - -#%% Basic Imports -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -#%% Matplotlib Plot -x = np.linspace(0, 20, 100) -plt.plot(x, np.sin(x)) -plt.show() - -#%% Bokeh Plot -from bokeh.io import output_notebook, show -from bokeh.plotting import figure -output_notebook() -p = figure(plot_width=400, plot_height=400) -p.circle([1,2,3,4,5], [6,7,2,4,5], size=15, line_color="navy", fill_color="orange", fill_alpha=0.5) -show(p) - -#%% Progress bar -from tqdm import trange -import time -for i in trange(100): - time.sleep(0.01) - -#%% [markdown] -# # Heading -# ## Sub-heading -# *bold*,_italic_,`monospace` -# Horizontal rule -# --- -# Bullet List -# * Apples -# * Pears -# Numbered List -# 1. ??? -# 2. Profit -# -# [Link](http://www.microsoft.com) - -#%% Magics -%whos - -#%% Some extra variable types for the variable explorer -myNparray = np.array([['Bob', 1, 2, 3], ['Alice', 4, 5, 6], ['Gina', 7, 8, 9]]) -myDataFrame = pd.DataFrame(myNparray, columns=['name', 'b', 'c', 'd']) -mySeries = myDataFrame['name'] -myList = [x ** 2 for x in range(0, 100000)] -myString = 'testing testing testing' \ No newline at end of file diff --git a/src/test/datascience/manualTestFiles/manualTestFileNoCells.py b/src/test/datascience/manualTestFiles/manualTestFileNoCells.py deleted file mode 100644 index 87de1fd22327..000000000000 --- a/src/test/datascience/manualTestFiles/manualTestFileNoCells.py +++ /dev/null @@ -1,22 +0,0 @@ -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt - -# Matplotlib Plot -x = np.linspace(0, 20, 100) -plt.plot(x, np.sin(x)) -plt.show() - -# Bokeh Plot -from bokeh.io import output_notebook, show -from bokeh.plotting import figure -output_notebook() -p = figure(plot_width=400, plot_height=400) -p.circle([1,2,3,4,5], [6,7,2,4,5], size=15, line_color="navy", fill_color="orange", fill_alpha=0.5) -show(p) - -# Progress bar -from tqdm import trange -import time -for i in trange(100): - time.sleep(0.01) \ No newline at end of file diff --git a/src/test/datascience/matplotlib.txt b/src/test/datascience/matplotlib.txt deleted file mode 100644 index 9b106e836b8e..000000000000 --- a/src/test/datascience/matplotlib.txt +++ /dev/null @@ -1 +0,0 @@ -<img alt="" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAYYAAAD8CAYAAABzTgP2AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4xLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvDW2N/gAAIABJREFUeJztvXl4ZNV16PtbVZpVmqeW1FKrNfRI0w2IBmzDxRgwnoAkHiCDSeKElxf75SV+N9f45l7Hz0m+2PHNJTf5nNgkHvCNHwY7tsEOMQYMHhi7oedBraHV3WpNpblKs6r2++Oco64WUmuo4Qy1f99Xn6rOUGdp1zl77TXstUUphUaj0Wg0Fj67BdBoNBqNs9CKQaPRaDSXoRWDRqPRaC5DKwaNRqPRXIZWDBqNRqO5DK0YNBqNRnMZWjFoNBqN5jK0YtBoNBrNZWjFoNFoNJrLyLBbgI1QXl6uGhoa7BZDo9FoXMUbb7wxpJSqWO04VyqGhoYGDh48aLcYGo1G4ypE5NxajtOuJI1Go9FchlYMGo1Go7kMrRg0Go1GcxlaMWg0Go3mMrRi0Gg0Gs1lJEQxiMjXRGRQRI6vsF9E5O9FpENEjorItTH7HhCRdvP1QCLk0Wg0Gs3GSZTF8A3grivsfw/QYr4eBP4JQERKgT8HbgD2A38uIiUJkkmj0Wg0GyAhikEp9XNg5AqH3AN8Uxm8ChSLSDXwbuBZpdSIUmoUeJYrKxhHMzY1x4+O9jIxM2+3KGlHJKr48fE+3jh3pdtQkyxO9k7w2OvnmY9E7RZFkwBSNcGtFrgQ87nH3LbS9rcgIg9iWBvU19cnR8oN8tzJAZ44eIEX2gaZjyh2VRfyzY/tpzyQbbdonmchEuWHR3v5h5920BWcxO8T/vv7dvLA2xoQEbvF8zyTswv83XNn+NpL3USiisdeP8/ffmgvLVUFdoumiYNUBZ+Xe0LVFba/daNSjyilWpVSrRUVq87oThlPHenl9755kEMXxnjgpga+8Gt76BoK85GvvELf+LTd4nmaSFRx3yOv8iePHyHL7+N/3bePd26v5LM/PMmn/u0oswsRu0X0NIfOj3Lnwz/nn39xlg+31vHwR/bSMzrN+/7hl3z1l2ftFk8TB6myGHqAupjPm4Fec/utS7a/mCKZ4iY0M89f/ugke2qL+P4fvo0Mv6Fnt5YH+N1vHOBDX36Fbz94I5tL8myW1Jt85+AFDp4b5bMf2MVHb2rA5xM+cHUNf/fcGf7+px2MT8/zld9qtVtMTxKJKv70u0dRSvGdP7iJ6xtKAXhHcwUP/dtR/uJHJ9lVXchNTWU2S6rZCKmyGJ4CPmpmJ90IjCul+oBngDtFpMQMOt9pbnMF/+u5doLhWf7i3qsWlQLA/q2l/H+/fwOjk3N88Zk2GyX0LuHZBf7HT85w3ZYSHniboRQAfD7hk3du549vb+GZEwMc6xm3WVJv8oNDF+kYDPPf379rUSkAVBRk86XfuJbKgmwefu4MSi3rANA4nESlqz4GvAJsF5EeEfmYiPyBiPyBecjTQBfQAfwz8IcASqkR4C+AA+brc+Y2x3O6f4Kvv9zNfdfXs6+u+C37r95czP376/nR0T56x7RLKdF85WedDIVn+W/v27lsLOF337GVgpwM/vHFDhuk8zZzC1Eefu4Me2qLuOuqTW/Zn5Pp5+PvbOb1syO81DFsg4SaeElUVtL9SqlqpVSmUmqzUuqrSqkvK6W+bO5XSqmPK6WalFJ7lFIHY879mlKq2Xx9PRHyJBulFJ/5wQkKczL4L+/evuJxv/OOrQB8/SXtb00kvWPTPPLzLu7eW8M19ctnNxfmZPLATQ38+EQ/HYOhFEvobR4/cJ6e0Wn+87u3rxjgv29/HdVFOfzPZ9u01eBC9MznDfDimSCvd4/wqbt2UJKfteJxtcW5vG9PNY+9fkGnsCaQ//FMGwr4L3etrJTBsBpyMvz844udqREsDZiei/D3P+1g/9ZSbmkpX/G47Aw/n7itmTfPj/HimWAKJdQkAq0YNsCThy5SlJvJr167edVjf//mRsKzCzz++oVVj9WsTjA0yw8OX+SjN25ZNahfmp/F/fvrefJwLxdGplIkobf55ivdBEOz/OkVrAWLD11Xx+aSXB5+VscaEkE0qohGU9OOWjGsk+m5CD85OcB792wiK2P15tuzuYgbG0v5+ktn9eSfBPDjE/1EFXywdXWlDPDgLY34BL78M201JILvvNHD/obSywLOK5GV4ePj72zmaM84b54fS4F03uaXHUPc+NfPc7p/IunX0ophnTx/eoCpuQgf2Fuz5nMevKWR3vEZnj7Wl0TJ0oOnj/bRWJHP9jVOoNpUlMPde2t58nAvcwtaMcdDx2CIjsEw77u6es3nvO/qajL9wk9O9CdRsvTg2ZMDhGYWaCjLT/q1tGJYJ08d7qWyIJsbtq49P/vWbZXUFufywyO9SZTM+wRDs7x2dpj376le16zm91y1ifDsAq+d1Rky8fDMiQEA7txdteZzCnMyuampnGdO9Gt3UhwopXju1AA3t5STk+lP+vW0YlgH49PzvNgW5P1X1+D3rb1j8vmE23dW8suOIWbm9WzcjWK5kd67jhErwNuby8nJ9PHcyYEkSZYe/Ph4P/vqiqkuyl3Xee/eXUX38BRnBsJJksz7HL84Qd/4DHfsWrtSjgetGNbBMyf6mYtEuXvf2t1IFu/cUcnMfJRXuvSodaM8fbSPpnW4kSxys/zc3FLBc6cG9ah1g/SMTnHs4jjvWWbewmrcsasKEeP50WyMZ0/24xN4106tGBzHD4/0sqUsj72bi9Z97o2NZeRm+nnh9GASJPM+lhvpfet0I1ncsbOKi2PTnOrTcxo2guVGevfu9SuGyoIcrqkr1oohDn5ycoDWLaWUXiE9PpFoxbBGhsKzvNw5zAeurtlQx5ST6eftzeU8r0etG2KjbiSLd+6oRASeO6XdSRvhmeP97NhUQEP5xgKf7969iRO9EzpteANcGJnidH9oXbGdeNGKYY08f2qASFTx/r0b65gAbttRycWxadoHta91vWzUjWRRUZDNNXXFWjFsgGBolgPnRpYtf7FWLEvjWR3nWTc/MdssVfEF0IphzbzaNUJ5IHvDHRMYigHgp9qdtC7Gp+Z57eww792gG8ni9l1VHO0Zp398JoHSeZ+fnOxHKeJSDA3lhlLX7qT18+zJfrZVBdiSgjRVC60Y1oBSilc6h7mxsTSujmlTUQ67qgv56SmtGNbDge4RosrILoqHO8zA3fOn9ah1PTx7coAtZXlxDYrAyE460D3CcHg2QZJ5n7GpOQ50j6bUWgCtGNbEueEp+idmuLEx/try79pZyRvnRxmf0rWT1srr3SNk+X3LVrFdD82VAbaU5em01XUQiSoOdo/yjubyuFfEu21nFVGFzsxbBy+2BYlEFXfs2ri1thG0YlgDr5o3ciIUwzt3VBKJKn7WrguLrZXXuobZV1cc98QeEeGd2yt5uXNYz4JeI6f6JgjPLrB/6+olMFZjd00huZl+DnaPJkCy9OC1s8MU5mRwde36MyHjQSuGNfBq1zDlgWyaKuL38e3dXExpfhY/a9OKYS2EZxc43jvBDY3xd0xgLKI0uxDlRK9ewGctvH7WWB5lLbWRViPT7+PaLcWL36lZnQPdo1y3pWRxIapUkaiFeu4SkTYR6RCRh5bZ/7CIHDZfZ0RkLGZfJGbfU4mQJ5EopXi1ayTu+IKF3ye0binhzfN61LQW3jg3SiSqEjJiBWjdUrL4vZrVOdA9Qm1xLjXF65vtvBKtW0o53T+hy9CvgdHJOToGw7QmQCmvl7gVg4j4gS8B7wF2AfeLyK7YY5RSf6KU2qeU2gf8A/C9mN3T1j6l1N3xypNoEhlfsLhuSwlnhyZ1EG4NvNY1TIZPuG7L8gvyrJfKwhzqSnO1O2MNKKU40D2SMKUMhsUWVXBIV1tdFWvw0pqge389JMJi2A90KKW6lFJzwLeBe65w/P3AYwm4bkqwAmWJXNT8WvOH1qWIV+f1syNcVVtEXlZGwr6zdUspB8+N6omGq3B2aJKh8FxC3EgW++qK8fuEA9qdtCoHzo2Q6Rf2xpl0sRESoRhqgdhVaHrMbW9BRLYAW4GfxmzOEZGDIvKqiNybAHkSyqtdw1QUZNO4wRmfy7GntohMv2h30ipMz0U40jOWsPiCxXVbShgKz3Jez8K9Ige6jc57/9bEjVjzszPYXVO4+N2alTnYPcqe2qKUVFNdSiIUw3KO95WGYvcB31VKxZYYrVdKtQK/DvydiDQtexGRB00FcjAYTE3g1ogvDHNjY1lC4gsWOZl+dtcUaT/3Khy6MMp8RHHjOkqcr4XWBqOj0+6kK/P62VFK87Noqggk9Huvbyjl8IUxZhd0peGVmJmPcKxn3Jb4AiRGMfQAdTGfNwMrLTxwH0vcSEqpXvNvF/AicM1yJyqlHlFKtSqlWisqKuKVeU10D08xMDHLjQkesYIxaj1yYUyv6nYFXusawSdwXUNifazbKgsoyMngoFbMV+RA9wjXN5QkdFAEcH1DCbMLUY5fTP5KZG7l2MVx5iJRW+ILkBjFcABoEZGtIpKF0fm/JbtIRLYDJcArMdtKRCTbfF8OvB04mQCZEsJrZnxhPYvyrJVr642H42SvfjhW4rWzw+yqKaQwJzOh3+vzCdfWl/DGOe3OWImBiRnOj0wlNL5gYY2CtTtpZSxrNlFJF+slbsWglFoAPgE8A5wCnlBKnRCRz4lIbJbR/cC31eURv53AQRE5ArwAfF4p5RjFcKRnnMKcjITMX1jKtVuMgJKOMyzP7EKEQ+fH2N+QeKUMRqbHmYEwY1NzSfl+t2PNNUhkRpJFecCI2R3UimFFDnaP0FiRT1kg25brJyTVQyn1NPD0km2fWfL5s8uc9zKwJxEyJINjF8e4enNxwk1pgOqiXGqLc3nj3Ci/8/atCf9+t3O6L8TsQnQxHpBorFHrm+dHuW1HauvQuIED3SPkZ/nZVV2YlO9vbSjhmRMDRKMq5ZO3nE40qjh4bpS7NrD2RaLQM59XYGY+Qlt/iD0bWJRnrVxTX8yb2s+9LMcuGjOT9ySpFMC+umIyfKID0CtwsHuUa+pLyPAnp4u4vqGU8el5XYJ+GTqDYcan5xMeW1sPWjGsQFt/iPmISlrHBIb/sHd8ht6x6aRdw60cvzhOUW4mm0sSM+N2KblZfnbXFOoA9DLMLkQ4MxDi6iQOiqy5PEcu6Lk8SzlgDlaSEd9ZK1oxrECyR6xwKbCk4wxv5djFcfbUFiXFjWdx3ZZSnRm2DG39IRaiiquSeO9vLcsnP8vPcV2z6i0c7RmjOC+ThrI822TQimEFjvWMU5KXvBErwM7qQnIyfXo+wxKsEWsyOyaAPZsLmV2I0hnU7oxYrDTSZA6KfD5hd00RJ3RW3ls40TvB7prCpA6KVkMrhhU4enGcPUkKPFtk+n3sqS3iWI8eNcWSCjcewFU1xvef0Pn0l3G818jGS+agCGBXTSEneyeIRHVpEov5SJS2/tDivWkXWjEsw8y86WNNQQ303TVFnOqbIKofjkVS4cYDaKwIkJPp06PWJZy4OM5VSXbjAVxVW8T0fISzQ9pis2gfCDMXibKrJjnZYGtFK4ZlONlnjGKSmZFksau6kMm5COd03Z5FrMBzXWlyR6x+n7BjU6FemyGG+UiUU/3Jd+MBXFVrdH56BvQlrHtxt7YYnIfl2klmVoaFNTLQM6AvkYrAs8XumkJO9k3oSqsmHYNh5hai7E7BiLW5IkB2ho/jF7VitjjRO0Felp+tCSzauRG0YliGoz3jlAey2VSYk/RrtVQFyPCJHrWazC4Y80dSMWIFY2QWmlngwohOGQYWO+lUtH+G38eO6kKdmRTDid5xdlYX4rd50p9WDMtw/OI4V29OzYg1O8NPc2WAk33aYgA40x9OSeDZwhoZa8VscPziOPlZfraWpWbEelVNISd6tcUGxoznk70TXGVzfAG0YngLU3MLtA+GUtYxwaXsDE3qAs8W2zcV4PeJDkCbHO+dYFdNYcrKVGiL7RLdw5NMzkVsjy+AVgxv4WTvBFGVuo4JjIdjMDRLMKSX+jyWosCzRU6mn+aKgLYYgIg5Yk1lx7QYgNbtvzg4sTsjCbRieAtHzcBzKjKSLKxCZdqdZLgyrqpN7eSe3aY7I905OxRmej6SsvgOwLaqAjJ8ogPQGMox0y9sqyqwWxStGJZyqm+C8kAWVSkIPFssKoY075zmFqIpDTxb7Kop1BYbqZnxvJScTD8tVQUcT/N7H4znf1tVAVkZ9nfL9kvgMNoGQmzflFqNXWSW3kh3d0b7YIi5SDTlsz4t10m6t//xi+NkZ/iSsv7IlbiqppATF8fTOgCtlOJE74TtM54ttGKIIRJVnBkIsb0q9T6+XdWFae9KausPAbCzOrWKeddiZlJ6t/+xi0aqZLJKba/EVbVFDE/OMTCRvhZb3/gMI5Nz7K61P74ACVIMInKXiLSJSIeIPLTM/t8WkaCIHDZfvxez7wERaTdfDyRCno1yfmSKmfkoO1JsMYDROZ0dmmRqbiHl13YKbf0hsvw+GlKUKmlhBbvT2ZWnlOJU34QtgU8rZfhYGscZrEFJKiYWroW4FYOI+IEvAe8BdgH3i8iuZQ59XCm1z3z9i3luKfDnwA3AfuDPRcS21Sna+o0fZ0eKR6xguDOUglN9oZRf2ym0DYRoqgykfMQKsLu6KK1dSQMTs0zMLNgyKNphxtis5y8dOdE7johRcdkJJOIJ3A90KKW6lFJzwLeBe9Z47ruBZ5VSI0qpUeBZ4K4EyLQhTveHEIGWSnssBkjvzKS2/pAtHRMYI7Xu4SlCM/O2XN9u2gaMAYkdGTGBbKOSa9tA+hbTO90XoqEsn7yshKy2HDeJUAy1wIWYzz3mtqX8mogcFZHvikjdOs9NCW39xo+Tm+VP+bVrinIoys1MW3fG+PQ8feMzKQ/8W1ij1nRdatIarW+3KVVye1UB7QPpay2fGQyxrSpgtxiLJEIxLJdwvjS94IdAg1LqauA54NF1nGscKPKgiBwUkYPBYHDDwl6Jtv6QbQ+GiKR1APqM2SnY1f7WQ5munVNbf5jKgmxK8rNsuf62TQV0BsNpuZrezHyE7qFJ2+795UiEYugB6mI+bwZ6Yw9QSg0rpayUg38GrlvruTHf8YhSqlUp1VpRUZEAsS9nZj5C9/CkbSNWMMozdAyE0jJt77SZkWRX+9eV5JGT6aOtPz0thjM2pGnHsr2qgPmIonto0jYZ7KIzGCaqDOXoFBKhGA4ALSKyVUSygPuAp2IPEJHqmI93A6fM988Ad4pIiRl0vtPclnLaB4wfxy4fNxiVVifnIlwcS7+6MWf6QxTkZFBdlLqJhbH4fEJLZQHtg+lnMUSiivbBkK0zbltMi60tDS22djO24imLQSm1AHwCo0M/BTyhlDohIp8TkbvNw/5IRE6IyBHgj4DfNs8dAf4CQ7kcAD5nbks5pywfq42KwXow29MwCGe58exc53ZbVcHiXIp04oKZpm1nx9RUEcAnxgAh3WgbCJHpFxpsXoMhloSEwJVSTwNPL9n2mZj3nwY+vcK5XwO+lgg54qGtP0ROpo8tKc6hj2WbmQ11ZiDEO3dU2iZHqlFK0TYQ4n1XV69+cBLZVhXg397sYWxqjuI8e3ztdrCYkWTjoCgn009DeX5aWgxn+kM0lgfItCFNeyWcI4nNtPWHaKkssHWBjKK8TCoLsjmTZhbDwMQs49Pztrrx4JLFlm7tb43SWyrtzYrZXlWQdm0PZkaSg+ILoBXDIqdtzKGPZVtV+vm5T5tuPLurSloP55k0G7W2DYSoK80lP9veHPptVQV0D08yMx+xVY5UMjlrrEWxzWalvBStGIDh8CxD4Vlb4wsWLVUBIxAeTZ/MJLtTVS1qinIIZGekn2KwMU07lu2bClDKWHc6XbD+V20xOBAr4Lhjk/3T0bdVFTA9n16ZSaf7Q7bm0FuICC1VgbRSDLMLEc4O2ZumbWFZjOmUANDmkEHRUrRiwP4c+lisiVbp1DnZnUMfy7bKgrTKCjs7NMlCVNnuxgNoKMsjy+9Lr3u/P0R2ho+60jy7RbkMrRgwOqbS/CwqCrLtFoXmyvQKgEaiivaBsCPiO2CY9MOTcwyF06MEdJuDBkUZfh9NlYG0ykw6MximpSpga9LLcmjFgOHna3ZI8KcoN5NNhTlpM2rqHp5kdiHqiBErpJ/FdmYgRIZPaCx3xv2/vSqQVnMZzvTbO7FwJdJeMSilaHeQYgDSys/dbmNVz+WwfL3p0jm19YfZWp7viOUkwbDYesdnmEiDKrfj0/P0T8w45t6PxRl3g40MhecYn563PYc7lm1VBXQMhomkQWaSlZXhFMVcUZBNUW4mZ9IkM+bMgLNy6Lcvzv73vmJud2jgGbRicFzHBIY7Y3YhyoWRKbtFSTodg2FqinJsz6G3EBFjolUaWAzTcxEujE4tzrh3Apcyk7yvmJ0w43wltGIIOk8xtFSlz0SrjmCYJge1PVxy5Xm9ym1nMIxSzrr3a4tzyc30p8VchjP9IQLZGdTYVDjySmjFMGD8OJsKnfPjWG4try8aE40qOgcnHdUxgZGhMzGz4PnF6TsdOCjy+YSmyvzFAZuXsQZFdhaOXAmtGBz44xTkZFJbnOt5i6F3fJrp+YijOiaA5gpDHq+PWjsHw/gEGsqdlUPfVBGg0+NtD2Y2ZIWz7n2LtFcM7QPO/HEMd4a3H47F+I7D2t9SVJ0eH7V2BMPUl+aRnZH6pWyvRHNFgItj00zOLtgtStKYmJlnYGLWcYMii7RWDBMz8wyGnPnjNFcE6Ap6u2aSEwP/YGQmFWRneN5icNL8nVgsmc56eDW3rqDxvzVVOGcNhljSWjFYD76TUlUtmiqNzCQv10zqDIYpycukLGD/jPNYRISmyoCnLYaFSJSzQ5OOC/zDJcXgZcXs1EGRRUIUg4jcJSJtItIhIg8ts/+TInJSRI6KyPMisiVmX0REDpuvp5aem0w6Bpz74yw+HB7unJw6YgXDz+3ljun8yBTzEeU4Nx7AlrJ8/D7xdPt3DIbJ9Av1DquRZBG3YhARP/Al4D3ALuB+Edm15LBDQKtS6mrgu8DfxOybVkrtM193k0I6gmGyHFjACoyOCfB0EM7JiqG5MsBgaNazM3A7TVeGE9s/K8PHltI8T1tsncEwDWX5ZDho1bZYEiHVfqBDKdWllJoDvg3cE3uAUuoFpZQ1W+tVYHMCrhs3HYNhGsvzHVfACqA0P4vS/CzPPhzD4VlGp+YXiwY6jcUAtEcVszUad6IrCQy5vGwxdDp4UASJUQy1wIWYzz3mtpX4GPAfMZ9zROSgiLwqIveudJKIPGgedzAYDMYnsUn7YMjRP05TRT6dg94MwDndx2oFBb3aOXUMhqksyKYwJ9NuUZaluTJA9/AkC5Go3aIknLmFKOdGpha9Ak4kEYphueH2sqk0IvKbQCvwxZjN9UqpVuDXgb8TkablzlVKPaKUalVKtVZUVMQrMzPzEXpGpx3bMYGZz+1Ri8GJM85jqS/NI9Mviy4Xr9ERdPaItakiwHxEcc6DZWHODU8SiSpHt38iFEMPUBfzeTPQu/QgEbkd+DPgbqXU4pRSpVSv+bcLeBG4JgEyrYoTywEspbkywPDkHKOTc3aLknA6BsPkZfkdWQ4AjLUBGsryPWkxKKUc78rwsivP6dYyJEYxHABaRGSriGQB9wGXZReJyDXAVzCUwmDM9hIRyTbflwNvB04mQKZVuZSq6kwfN8QEoD1oNXQMhmmqcNaM86U0VxpzSbzGYGiW8OyCozumRVeeB9vf6nsaHTqHARKgGJRSC8AngGeAU8ATSqkTIvI5EbGyjL4IBIDvLElL3QkcFJEjwAvA55VSKVMMTiwHEIvXFYOTOyYw2v/cyBRzC97yczt1xnksBTnGglVetNg6g2Fqi3PJy3JGReHlSIhkSqmngaeXbPtMzPvbVzjvZWBPImRYLx2DYbaU5TuuHEAstSW5ZGf4PPdwhGcX6BufcbxiaK4MEIkquocnHbmYykZxgysDoKky35uupGDY0dYCpPHM567gpGOno1v4fcLW8nzPBUCth93JWRng3bkkHYNhCrIzHLHG+ZVorgjQGZz0VPlzp1YUXkpaKoZIVBnlABzeMQGeLM3gxHLPy9Ho0ZTVjkHnVRRejubKAOFZb5U/75uYYXo+4vi+Jy0VQ8/oFHORqOPNOTBGTRdGppiZj9gtSsLoDIbJ8Albypwb3wHINxdR8ZpidnqqqoU1+c5Litktbry0VAyXKhs6+8cB4+GIKuge9o47qSs4ac4TcP7t11QZ8FRmzMTMPMHQrCvu/WYPJl90asXgXKwbzQ0PhxUH8dIM6M5gmEYXtD0YD3Dn4KRnyp93ObhG0lIqCrIpyPFW+fOOYJii3EzK8rPsFuWKpK1iKMnLpMThPw5AY3kAEe+Y05GoontoiqZK57vxwBg8TM9H6JuYsVuUhNDpghx6CxGhsSJA15A37n0w2r+pIt/x8Z00VQzuCDwD5Gb5qS3O9Yw5bcV3msrd0f5ey0zqGjLiO04t97yUpor8RSvHC7il70lLxdAVDLvix7Fo9lBm0mJ8xy0WgymnV2ZAdw5OUl/mjvgOGIq5b3zGE8t8jk/PMxSedWxF21jccXckkPGpeYbCc64wpS2sYnpe8HNbCq7RJRZDRcBY5tMrc0m6htw1KLJibF5Y5rNr8d53ft+Tdoqhc8g9gWeLxop8ZuajnvBzdwbDlOZnuSK+A6afu9Ibfm4rvuOmQVGjhzKTLlnLzu970k8xOHyBkuWwlJgX3BmdwUlXjJhiaSr3xroYi/EdFw2KtpTl4RM8YbFZ83fcEN9JO8XQNTRJpl+oK8m1W5Q107iYsup+xeC2+A4Yg4j+iRnCLvdzX0rTdo9izs7wU+eRZT67gu6J7zhfwgTTaRbPc+paq8th+bm7XO5nteI7bgk8W1gWzlmXj1otV4Zb4jsWTRUBT2QmdQbDrml79/SOCaIzGHbViAli/Nwufzis+I5bHg4Ly+3o9jiD2+I7Fo3l+ZwdcnfyxUIkyrnhKdf0PWmlGOYjUc6K/HxsAAAgAElEQVSPTLlm1m0sTeX5rjen3RR8i2XRz+1yV16nCyoKL0dTZYCZ+Si949N2i7JhekanXRXfSSvFcGFkivmIcs2PE0tTpfvzuTuDYdfFdyDGz+1yV16Xi1wZsViuPDcHoC1r0y0ZYQlRDCJyl4i0iUiHiDy0zP5sEXnc3P+aiDTE7Pu0ub1NRN6dCHlW4lLxPHf8OLEs+rld3Dl1Bd0X37FoLHf3ojFuje9AjCvPxRazldXmlkFp3E+oiPiBLwHvAXYB94vIriWHfQwYVUo1Aw8DXzDP3YWxRvRu4C7gH83vSwqLk6tc8uPEYj0cbnYnuTFV1aKxIsDZIfcW03NrfAegLD+LwpwMV9/7XUPuqc8GibEY9gMdSqkupdQc8G3gniXH3AM8ar7/LvAuMapI3QN8Wyk1q5Q6C3SY35cUOoNhygPZFOVmJusSScPt+dxG8G3SdfEFi6aKALMLUS6OudPP7db4DhjJF00uT75wS40ki0QohlrgQsznHnPbsscopRaAcaBsjecCICIPishBETkYDAY3JOjkXIRtVe75cWLJzvCzuSTPteb0hdFp18Z34JJv2K0pw26N71g0lru7XliXC9Z5jiURimG5+rFL7e2VjlnLucZGpR5RSrUqpVorKirWKaLBl379Wv71Yzds6Fwn0FTh3vWf3VTueTncPvvczfEdMIoZDkzMunKS4WJ8x0WDokTcJT1AXcznzUDvSseISAZQBIys8dyE4vM5uw76lTD83O7M57ayMtxSbnsp5YEsClzs53ZzfAcuxUbcqJgX4ztpphgOAC0islVEsjCCyU8tOeYp4AHz/QeBnyqllLn9PjNraSvQAryeAJk8SVNFwLXF9DoHJ434Tp774jtg+rldOgPX7fEdgObF8ufua383ZkPGrRjMmMEngGeAU8ATSqkTIvI5EbnbPOyrQJmIdACfBB4yzz0BPAGcBH4MfFwp5Z1V7xOMm2smdbrMx7ocjRXunGRoxXfcbDHUl+bj94kr298qnlfnguJ5FhmJ+BKl1NPA00u2fSbm/QzwoRXO/SvgrxIhh9eJ9XPfsm1jcRa76AyGueuqarvFiIumigDfe/Mi4dkFAtkJeXRSghsrCi8lK8NHfWmeSy2GMFtcUjzPwj2SamL83O56OEYm5xidmneVKb0clvxu83MvVlV1aXzHosmlFltncNJV8QXQisFVLPq5XVbMzepI3TxihZj1n13WOXUF3R3fsTDu/UkiLkq+mLfiO1oxaJJJY4X7Fo3xyoi1viwPv09c585wY0Xh5WiqCDC3EOXiqHsmGV6qz+au9teKwWU0Vbhv0Ziu4CRZGT5qXTq5yiI7w0+9CxeN6QyGXW+tAYt1ntzU/p0unXGuFYPLsExSNy0aYyxQYmSVuJ0ml1lsl+I77uqYlsOay+AuxeBOa1krBpdh5XN3BEM2S7J2jOCbu0zplWgyi+m5xc99qXCk+9u/JD+L0vwsdymGwbAr4ztaMbiMxXxul4xa5xaMxZG8MGIF088didIzOmW3KGvCSlVt9kz7u8ti6xpy5+JIWjG4jKwMH1tc5Oc+P2KMrj2jGFzm5+4MhsnO8FFT7O74jkVThXuK6Sml6Bh0Z3xHKwYX0lTpnoejw2ULlKzGop/bJaPWruAkWz0S3wHjPhqenGNsas5uUVZlZHKO8Wl3xne0YnAhTRUBuoemWIhE7RZlVSwFttWF5vRylORnUeYiP7dXMpIsLllszlfMnS6skWShFYMLaarIZy4S5YIL8rk7g2E2Fea4qoTEarjFnTG7EPFUfAfcNclwMSPJhe2vFYMLWVzm0wXF9LqCk65cZ/hKNFXm0+GCtj83PEVUuXPEuhKbS/LI8vvcoRgGw+Rk+qh1YXxHKwYX0uSSfG6llDnr1n0jpivRVBFgdGqekUln+7kXi+d5qP39PmFruTsyk4z5OwFXrgGjFYMLKcrLpDyQ7XjFEAzPEppZcHW55+VwiztjMb7jsfZvrMh3RSHDzqB718DQisGlNFc6f5lPa1Tn1odjJRYVg8PdSV3BSWqKcsj3UHwHjPY/NzLF3IJzky9m5iNcGJ1yrRsvLsUgIqUi8qyItJt/S5Y5Zp+IvCIiJ0TkqIh8JGbfN0TkrIgcNl/74pEnnWiqCNAxGMZYCM+ZLC7n6SFXBkBtSS5ZGc73c3stI8miqTKfSFRxfsS5A6Pu4UmUcu+9H6/F8BDwvFKqBXje/LyUKeCjSqndwF3A34lIccz+P1VK7TNfh+OUJ21oqggwPj3PsIP93B2DYfKy/GwqzLFblITi9wmN5c622Iz4jvvKPa8F63/qcHCcodPl83fiVQz3AI+a7x8F7l16gFLqjFKq3XzfCwwC7lp+zIG4ITOpYzBMc6U7g2+r4fSU1cHQLOHZBde6Mq5EowtiPJ3BMCLuje/EqxiqlFJ9AObfyisdLCL7gSygM2bzX5kupodFJDtOedIG64F38qi1YzDsmRo9S2mqyOfCyBQz885cotxKp3XbymFrIZCdwabCHMcrhtriXHKz/HaLsiFWVQwi8pyIHF/mdc96LiQi1cD/Bn5HKWVFjT4N7ACuB0qBT13h/AdF5KCIHAwGg+u5tCepKcolN9Pv2IcjNDNP3/gMzVXe65jAsNiiypgr4ETaB4zquy0ebf/myoCjrWW3p2mvqhiUUrcrpa5a5vUkMGB2+FbHP7jcd4hIIfDvwH9TSr0a8919ymAW+Dqw/wpyPKKUalVKtVZUaE+UzyfGam4OVQwdHqvquZRLfm5ntn/7YJii3EwqAt40wpsrA7Q7NPkiGlV0Dro7vhOvK+kp4AHz/QPAk0sPEJEs4PvAN5VS31myz1IqghGfOB6nPGmFlZnkRCy5WqoKbJYkOVjrGzi1/dsHw7RUBjAeLe/RUhVgai5C7/iM3aK8hYtj00zPR9jmYmstXsXweeAOEWkH7jA/IyKtIvIv5jEfBm4BfnuZtNRvicgx4BhQDvxlnPKkFU0VAeMmnHOen7tjMExWho86ly/nuRJ5WRnUlebSPujMBZM6BsOedSMBtFQaAw7LZeYkrHvCze0f18wXpdQw8K5lth8Efs98/6/Av65w/m3xXD/daakKoJThz7yqtshucS6jfdBYzjPD7905lC2VBbQPOM9iGA7PMjI5R3OlN601gJbKS668W7dfMecl5Vj3RHOFe9vfu09tGmA9HE4ctVqpql6mpSpA11DYceXP2634jofbvyQ/i/JAliMVc/tgmMoC9y3nGYtWDC6moTyfTL9wxmEPh1UOwMsdExgWw3xEcW7EWZlJlmJo8Xj7GwFo5w2K2gdCrnYjgVYMribT72Nreb7j/KydwTBKXfIDe5VFi81p7T8YJj/LT3WRt2acL6WlssBxmUlKKTPw7+57XysGl2M9HE6iIw1cGXDp/3OaO6N9MERzVYFnM5IsWqoChGYWGAzN2i3KIr3jM0zNRbTFoLGXlqoA50emHJWZ1D4Qxu8TGsrz7BYlqeRnZ1BbnOs4xdw+EPa8GwmcqZgXJxZqi0FjJ9uqChYzk5xCx2CYLWV5ZGe4sxzAethWFXCUYhifmmcwNJsWimExZdVBcYYOj8R3tGJwOdYkmjMO8nO3D4Y8O+N5KS1VBXQGw0SizvBzdwTdn0O/VsoDWRTnZToq+aJ9IEx5IIuS/Cy7RYkLrRhczpYyIzPJKaPWuYUo3cNTadExgeHOmFuIct4hmUmWW8Xtroy1ICK0VAbocJDFcGYw5Im214rB5TgtM+nc8CSRqPJ84NliW5WzZuC2u3gB+o3QXFnAmQFnZCYppegY8MaMc60YPEBLVYFjzOlLOfTuHzWthcUAqEMstvZBo6qnF9fAWI6WSmPBqqGw/QtWDUzMEppdcH18AbRi8ATbKgu4MOqMzKRL6wC4c4GS9RKwMpMcYjF0DIQ80TGtFWt07oQAtCWDF0qRaMXgAWJrJtlNW3+IutJc8rK8tQD9lbBKQNtNaGae3vEZz1a0XQ7LMnVCldvF+I52JWmcgJMyk073T7BjU6HdYqQUIwBqf2aStZqfm9cBWC9VhdkUZGc4Yi5D+2CI0vwsyj2wBoZWDB7AykyyO84wMx/h7NAkOzelz4gVjAD07EKUnlF7M5Pa+icA2JFG7S8iNFcFHDEoah/wTuFIrRg8gJWZZHfaXsdgmKiCHdXpZTFYy5faPWo91RciL8tPfam3Z5wvZcemQk73h2zNTLJqJGnFoHEUTshMOtVnjFi3p9GIFS7Ncm2zedR6un+C7ZsK0iYjyWJndQHj0/P0T9i3mlvf+Azj0/OesZbjUgwiUioiz4pIu/m3ZIXjIjGrtz0Vs32riLxmnv+4uQyoZgM4ITPpdH+I7AwfDWXpkZFkUZCTyeaS3EXFaAdKKU71hdIuvgMs/s+n++xTzNZvv9Mj1nK8FsNDwPNKqRbgefPzckwrpfaZr7tjtn8BeNg8fxT4WJzypC3bNxmZSXb6Wtv6Q2yrKsCfZiNWMDoEOxVD/4Q5Yq32xoh1Peww/+eTNra/9dt7xY0ar2K4B3jUfP8ocO9aTxSjJvBtwHc3cr7mcqyRip2dk5GRlH4dExjtf3Zokpl5eyw2a7ScjhZDYU4mtcW5nO630WLoD1Ffmkcg2xtp2vEqhiqlVB+A+XelxVdzROSgiLwqIlbnXwaMKaUWzM89QG2c8qQtdSXGTWnXqCkYmmUoPOeZEdN62VVdQFQZVpMdnOpPz/iOhd0W26k+bw2KVlVvIvIcsGmZXX+2juvUK6V6RaQR+KmIHAOW+xVXTCsQkQeBBwHq6+vXcen0wOcTdlYXcLLXnofD6hC99HCsh1iLbW9dccqvf7ovRG1xLkW57l1nOB52Vhfw09MDzMxHyMlMbbn36bkI3UOTfODqmpReN5msajEopW5XSl21zOtJYEBEqgHMv4MrfEev+bcLeBG4BhgCikXEUk6bgd4ryPGIUqpVKdVaUVGxjn8xfdhZbaTtRW2YaHU6DXPoY6krySM/y2/bqPV0/0RaxhcsdmwqJKrsmQHdNhAiqrwTeIb4XUlPAQ+Y7x8Anlx6gIiUiEi2+b4ceDtwUhlJxy8AH7zS+Zq1s6u6kPDsAhdsmGh1uj9EeSCbMg/M+twIPp+wo7qQUzZkxswuROgMTqZlfMFip40BaGswsEsrhkU+D9whIu3AHeZnRKRVRP7FPGYncFBEjmAogs8rpU6a+z4FfFJEOjBiDl+NU560ZleNcWPa4U5K9xErGJ3Tqf6JlE+0ah8wynHsSOP231KWT06mz5aU1dN9E+Rn+dlc4p1S53GF0JVSw8C7ltl+EPg98/3LwJ4Vzu8C9scjg+YS26oK8IkxgnnPnuqUXXchEqV9IMxHb9qSsms6kZ3Vhfzrq+fpGZ2mLoWzj0/3p29GkoXfJ2zfZE8A+lRfiB3VhZ6aWKhnPnuInEw/TRWBlJvT3cNTzC5E2Z7GHRPYlzJ8um/CnFiYXqUwlrJzUwGnU2yxKaU45UFrWSsGj7GrJvV+7nTPSLLYsakAEVLe/qfNiYUZ/vR+nHdsKmB0ap7B0GzKrtkzOk1oZsFTgWfQisFz7Kwu5OLYNGNTqVvR6nT/BH6feKaA2EbJy8qgoSw/9RaDB0esG8HqnFNpMS/OePaYtawVg8fYZcPDcaJ3gsby/JTnjzsRKwCdKhYnFnqsY9oIdtRMOt0fQsR71rJWDB7jkp87NQ+HUoqjPWNcvTn1k7qcyM5NhZwbniI8u7D6wQng0ojVWx3TRijKs0pjpNZi2FKaR75HSmFYaMXgMSoKsqkoyE5Zymrv+AxD4Tn21hWl5HpOx1LMbSnqnI5dHAdgd41ufzAU5IkUpmuf6pvwXHwBtGLwJLuqC1PmSjrWMwbAnlrdMQHstOaSpMhiO3JhjMbyfIry0rMUxlKu3lxMZzBMaGY+6dcKzcxzbmTKk248rRg8yK6aQjoGQ8wtRJN+rSM942T4xJOjpo1QU5RDSV4mx3vGU3K9Iz1jttRmcip764pQ6pIllUyO9YyjFJ60lrVi8CC7qguZj6iUrM1wtGeMHdUFOvBsIiLsqyvm0IXRpF+rf3yGgYlZrt7svY5po+wzleSRC8lXDIcujF12TS+hFYMH2WsGgo+Ybp5kEY0qjvaM68DzEvbVldA+mHx3xmGzY9IWwyWK87JoKMvjcAoU8+ELY2wtz6c4z3sLT2rF4EHqSnMpy8/izXPJVQzdw5OEZhbYq0esl3FNfTFKwdEku5OO9IyR4RNPFW9LBHvripNuMSilOHxhzJPWAmjF4ElEhGvqS5LuzrA6Pm0xXI41grdG9MniaM8YO6sLtRtvCXs3F9M/MUP/+EzSrtE7PkMwNKsVg8ZdXFNfTFdwMqkzoI/2jJOT6aMlzWc8L6UoN5PGinwOnU+eYohGFUcvjHsy8Bkv++qT70o9fN678QXQisGzXFtfAlwKkCWDoz1j7K4pSvsaPctxTV0Jhy+MJq2gW9fQJKHZhcV4kuYSu6oLyfAJR5J47x/pGSMrw+fZbDz9RHuUqzcX4RM4dC457qSFSJTjveM6I2YF9tUXMxSeo2d0Oinff0QHnlckJ9PPzurCpLryDp8fY3dNIVkZ3uxCvflfacjPzmDHpsKkWQztg2Fm5qN6xLoC15gddrLa/2jPGPlZRpl1zVvZV1fM0Z7xpCxzuxCJcuziuKfv/bgUg4iUisizItJu/i1Z5ph3isjhmNeMiNxr7vuGiJyN2bcvHnk0l3NNfTGHz48l5eE4avpvtcWwPNs3FZCT6Vv0RSeawz3j7NlchN9Di8Mkkr11xYRnF+gaSvwa0G0DIabnI1xTrxXDSjwEPK+UagGeNz9fhlLqBaXUPqXUPuA2YAr4Scwhf2rtV0odjlMeTQzX1pcQml2gI5j4h+NIzzgFOUaZac1byfT72FNblJR8+tmFCKd6J7Qb6QrsM4Pyh5OQtnrYwxPbLOJVDPcAj5rvHwXuXeX4DwL/oZRK/Wr1aYg1ojl0PvGd0xvdo+yrK/bUcoaJZl9dMcd7JxJemuR0X4i5SJR9HnZlxEtjeYCC7IykKObD58cozc+iPoXLt6aaeBVDlVKqD8D8W7nK8fcBjy3Z9lciclREHhaR7JVOFJEHReSgiBwMBoPxSZ0mGLMyMxM+0S0YmqVtIMTbmsoT+r1e45r6EuYWoglfuOdNU9Fri2FlfD7h6rqipASgj/SMsXdzESLeHRStqhhE5DkROb7M6571XEhEqoE9wDMxmz8N7ACuB0qBT610vlLqEaVUq1KqtaKiYj2XTltEhGuSULfn1a5hAN7WVJbQ7/Ua+5I00e2ljmG2lOVRU5yb0O/1GtdtKeVk7wTj04krTTIxM0/7YJh9dW8Jp3qKVRWDUup2pdRVy7yeBAbMDt/q+Aev8FUfBr6vlFr8lZRSfcpgFvg6sD++f0ezlGvrjbo9Ewms2/Ny5zAFORnsrvFmDneiqC7KYVNhDq+fHUnYdy5EorzaNczbm7W1thrvaC4nqi4NZBLBq53DKAU3NJYm7DudSLyupKeAB8z3DwBPXuHY+1niRopRKoIRnzgepzyaJVxTX4JS8EYC5zO80jnEDVvL9MS2VRAR3tFSzi87hogkKDPsSM844dkF3qEVw6rsqysmL8vPL9uHEvadv2gfIi/LvziB1KvE+2R/HrhDRNqBO8zPiEiriPyLdZCINAB1wM+WnP8tETkGHAPKgb+MUx7NElobSsjO8PHzM4mJy1wcm6Z7eEq7kdbIzS3ljE/PJ2x9gJc6hhCBmxp1+69GVoaPGxvL+GVHIhVDkJsayzw7sc0irv9OKTWslHqXUqrF/Dtibj+olPq9mOO6lVK1SqnokvNvU0rtMV1Tv6mUSnxeZZqTk+nnxsYyftaWGMXwSqdhlt+kFcOauLmlAhESpph/2THEVTVFlOR7r9RzMnh7czlnhybpGY0/EfLCyBTdw1O8o8X71pq31Z4GgFu3V9A1NMn54fgfjpc7hyjNz2J7lV58fi2U5mexp7YoIYphcnaBQ+dHeVuzVspr5WazE38pAVbDL0yX1M0t3k9+0YohDbh1u5FF/LMzV8oNWB2lFK90DnNTY5mev7AObm4p59CFsbgTAF7vHmE+onR8YR20VAaoLMjmlx3xB6B/0R6kpiiHpgrvT+rUiiENaCjLo740jxfjdCd1D0/RNz6j3Ujr5JaWCiJRxctxdk4vtQ+RleHj+gZvZ8QkEhHhHc3lvNQxFFdpmIVIlJc6hkzXoPcHRVoxpAEiwq3bK3i5c5iZ+ciGv+flTsOU1oHn9XHtlhIC2Rn8vD0+xfxS5zCtW0r0wjzr5B0t5YxMznEyjomGRy+OMzGzwM3b0sNa04ohTbh1ewXT8xEOdG88p/7ljmE2Feawtdz7pnQiyfT7uKmpjJ+fCW54fYah8Cyn+ib0/IUNYLVZPHGGX5wxssHeniaz/bViSBNuaiwnK8O3YXfS1NwCL7QN8p+2pYcpnWhuaSmnZ9RI9d0IVqemFcP6qSrMYVtVIK601V+0B9lTmz7ZYFoxpAm5WX5u2FrKi20bC0A/e3KAqbkI915Tm2DJ0oNbthmZLBvNTvrR0T7KA9nsqdVlzjfCzS0VvHZ2ZEPlMSZm5jl0YWwxwykd0Iohjbh1eyWdwUkujKx/1Pq9Ny9SW5zLDVt14HMjbCnLp6Esj2dO9K/73KHwLC+cHuRXr63V6y9skHv21TC3EOWpI73rPvffj/YRiSretbMqCZI5E60Y0ojbdhhpqz862reu8wZDM/yiPcg9+2p0mmocfPC6zbzcOczZocl1nfeDQxdZiCo+dN3mJEnmffbUFrFjUwFPHLiw7nMfe/0826sKFlflSwe0Ykgjtpbnc2NjKf/66rl11e556nAvUQW/ot1IcfHh1joyfMJjr59f8zlKKb77Rg9764pp0ZMKN4yI8JHr6zh2cZyTvWvPTjp+cZyjPePcv78urWJrWjGkGb/9tgYujk3z3KmBNZ/zg8MXuaq2UHdMcVJZmMMdu6r4zsELa04bPn5xgtP9IW0tJIB799WS5ffxxMG1Ww3fPnCe7Awfv3JNerW/Vgxpxu07q6gtzuUbL3Wv6fj2gRDHL06k3YORLH7jhi2MTs3z4+NrizV8540LZGf4+MDemiRL5n1K8rO4c3cV3z90cU2KeWpugR8c6uV9e6opystMgYTOQSuGNCPD7+M3b9zCK13DtPWHVj3+e4cu4vcJd+uOKSG8ramMhrI8vvXauVWPnZmP8OThXt69exNFuenVMSWLj1xfx/j0PD85ubrF/KOjfYRnF7j/hvoUSOYstGJIQ+67vo7sDB+PvtJ9xeNGJ+d44sAFbm4pp6JgxVVXNevA5xN+/YZ6DnSPrqqYnz05wPj0PB9q1dZaonh7Uzm1xblrCkI/9vp5misDtG7x9toLy6EVQxpSkp/Fvftq+f6bFxmfWjmv+y9+dJLx6Xn+9N3bUyid9/ngdXVk+X184+WzKx4zPjXPXz99isbyfL22dgLx+Ywg9C87hvjZFeaUvNY1zKHzY9y/vz6tgs4WcSkGEfmQiJwQkaiItF7huLtEpE1EOkTkoZjtW0XkNRFpF5HHRSQ9phU6gAfe1sD0fIS/evrksmUafnp6gO8dusgf3trE7ho9qSqRlOZn8es31PPY6xf44TJ59Uop/usPjjEYmuXhj+zTcxcSzO/f3Mj2qgI++fhhBiZm3rJ/cGKGTzx2iIayPD6cptZavBbDceBXgZ+vdICI+IEvAe8BdgH3i8guc/cXgIeVUi3AKPCxOOXRrJFdNYX8X7c188TBHr7w47bL9k3MzPNfv3ecbVUBPn5bs00SeptPv3cH1zeU8J+/c4SjPWOX7fvOGz38+9E+PnnnNvamUe58qsjN8vOl37iGqbkIf/TYIRYil9YPm1uI8n9+600mZxf4ym+1UpCTnrGdeFdwO6WUalvlsP1Ah1KqSyk1B3wbuMdc5/k24LvmcY9irPusSRGfvGMbv3XjFr78s06+/LNOpuYWeLlziP/niSMMhmb4mw/uJTtDV/JMBtkZfv7pN6+jPJDN73/zIOeHp7gwMsXPzwT57FMnuLGxlP/jlia7xfQszZUF/OW9V/Ha2RG++EwbZ4cm6R+f4XM/OsEb50b5mw9ezfZN6ZuenZGCa9QCsZGeHuAGoAwYU0otxGzXM6hSiIjw/969m7HpeT7/H6f54jNtixPf/uT2bezTo9WkUh7I5p8/2soHv/wyt3zxhcXtxXmZ2oWUAn7tus280jXMV37exVd+3rW4/cFbGnn/1emdhbeqYhCR54BNy+z6M6XUk2u4xnJ3t7rC9pXkeBB4EKC+Pv3Sx5KFzyf87Yf2srU8n2hUcd2WEq6tL0m7vG272FVTyGO/fyMvdQ5RHsimsiCb3TVFOgssRfz1r+7hvXs2MTG9wPR8hLwsP+/bU223WLazqmJQSt0e5zV6gLqYz5uBXmAIKBaRDNNqsLavJMcjwCMAra2tG1+KSfMWsjJ8fPKObXaLkbbsrSvWsQSbyPT7uG1H+hTHWyupSFc9ALSYGUhZwH3AU8pIhXkB+KB53APAWiwQjUaj0SSReNNVf0VEeoCbgH8XkWfM7TUi8jSAaQ18AngGOAU8oZQ6YX7Fp4BPikgHRszhq/HIo9FoNJr4kY0uNWgnra2t6uDBg3aLodFoNK5CRN5QSq0458xCz3zWaDQazWVoxaDRaDSay9CKQaPRaDSXoRWDRqPRaC5DKwaNRqPRXIYrs5JEJAisvtLJ8pRjTK5zGlqu9aHlWh9arvXhVbm2KKUqVjvIlYohHkTk4FrStVKNlmt9aLnWh5ZrfaS7XNqVpNFoNJrL0IpBo9FoNJeRjorhEbsFWAEt1/rQcq0PLdf6SGu50i7GoNFoNJork44Wg0aj0WiugGcVg4jcJSJtItIhIg8tsz9bRB43978mIg0pkKlORF4QkVMickJE/u9ljrlVRPybzMsAAAS3SURBVMZF5LD5+kyy5TKv2y0ix8xrvqVCoRj8vdleR0Xk2hTItD2mHQ6LyISI/PGSY1LSXiLyNREZFJHjMdtKReRZEWk3/5ascO4D5jHtIvJACuT6ooicNn+n74vIsos9rPabJ0Guz4rIxZjf6r0rnHvFZzcJcj0eI1O3iBxe4dxktteyfYNt95hSynMvwA90Ao1AFnAE2LXkmD8Evmy+vw94PAVyVQPXmu8LgDPLyHUr8CMb2qwbKL/C/vcC/4Gx8t6NwGs2/Kb9GHnYKW8v4BbgWuB4zLa/AR4y3z8EfGGZ80qBLvNvifm+JMly3QlkmO+/sJxca/nNkyDXZ4H/vIbf+YrPbqLlWrL/b4HP2NBey/YNdt1jXrUY9gMdSqkupdQc8G3gniXH3AM8ar7/LvAuEUnqIrtKqT6l1Jvm+xDG+hRuWef6HuCbyuBVjNX3UrkG4ruATqXURic2xoVS6ufAyJLNsffQo8C9y5z6buBZpdSIUmoUeBa4K5lyKaV+oi6tpf4qxuqIKWWF9loLa3l2kyKX+fx/GHgsUddbK1foG2y5x7yqGGqBCzGfe3hrB7x4jPkQjWMsFpQSTNfVNcBry+y+SUSOiMh/iMjuFImkgJ+IyBtirK+9lLW0aTK5j5UfWDvaC6BKKdUHxoMNVC5zjN3t9rsYlt5yrPabJ4NPmC6ur63gFrGzvW4GBpRS7SvsT0l7LekbbLnHvKoYlhv5L02/WssxSUFEAsC/AX+slJpYsvtNDHfJXuAfgB+kQibg7Uqpa4H3AB8XkVuW7LezvbKAu4HvLLPbrvZaK3a2258BC8C3Vjhktd880fwT0ATsA/ow3DZLsa29gPu5srWQ9PZapW9Y8bRltsXVZl5VDD1AXcznzUDvSseISAZQxMZM33UhIpkYP/y3lFLfW7pfKTWhlAqb758GMkWkPNlyKaV6zb+DwPcxTPpY1tKmyeI9wJtKqYGlO+xqL5MBy51m/h1c5hhb2s0MQL4f+A1lOqKXsobfPKEopQaUUhGlVBT45xWuZ1d7ZQC/Cjy+0jHJbq8V+gZb7jGvKoYDQIuIbDVHm/cBTy055inAit5/EPjpSg9QojB9mF8FTiml/ucKx2yyYh0ish/jNxpOslz5IlJgvccIXh5fcthTwEfF4EZg3DJxU8CKIzk72iuG2HvoAeDJZY55BrhTREpM18md5rakISJ3YaynfrdSamqFY9bymydartiY1K+scL21PLvJ4HbgtFKqZ7mdyW6vK/QN9txjyYiwO+GFkUVzBiPD4c/MbZ/DeFgAcjBcEx3A60BjCmR6B4aJdxQ4bL7eC/wB8AfmMZ8ATmBkY7wKvC0FcjWa1ztiXttqr1i5BPiS2Z7HgNYU/Y55GB19Ucy2lLcXhmLqA+YxRmgfw4hJPQ+0m39LzWNbgX+JOfd3zfusA/idFMjVgeFztu4xK/uuBnj6Sr95kuX63+a9cxSjw6teKpf5+S3PbjLlMrd/w7qnYo5NZXut1DfYco/pmc8ajUajuQyvupI0Go1Gs0G0YtBoNBrNZWjFoNFoNJrL0IpBo9FoNJehFYNGo9FoLkMrBo1Go9FchlYMGo1Go7kMrRg0Go1Gcxn/Px8G3YTg49XbAAAAAElFTkSuQmCC&#10;"> diff --git a/src/test/datascience/mockCommandManager.ts b/src/test/datascience/mockCommandManager.ts deleted file mode 100644 index 19cb62d1129d..000000000000 --- a/src/test/datascience/mockCommandManager.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { noop } from 'lodash'; -import { Disposable, TextEditor, TextEditorEdit } from 'vscode'; - -import { ICommandNameArgumentTypeMapping } from '../../client/common/application/commands'; -import { ICommandManager } from '../../client/common/application/types'; - -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length - -export class MockCommandManager implements ICommandManager { - private commands: Map<string, (...args: any[]) => any> = new Map<string, (...args: any[]) => any>(); - - public registerCommand<E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E]>(command: E, callback: (...args: U) => any, thisArg?: any): Disposable { - this.commands.set(command, thisArg ? callback.bind(thisArg) as any : callback as any); - return { - dispose: () => { - noop(); - } - }; - } - - public registerTextEditorCommand(_command: string, _callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, _thisArg?: any): Disposable { - throw new Error('Method not implemented.'); - } - public executeCommand<T, E extends keyof ICommandNameArgumentTypeMapping, U extends ICommandNameArgumentTypeMapping[E]>(command: E, ...rest: U): Thenable<T | undefined> { - const func = this.commands.get(command); - if (func) { - const result = func(...rest); - const tPromise = result as Promise<T>; - if (tPromise) { - return tPromise; - } - return Promise.resolve(result); - } - return Promise.resolve(undefined); - } - - public getCommands(_filterInternal?: boolean): Thenable<string[]> { - const keys = Object.keys(this.commands); - return Promise.resolve(keys); - } -} diff --git a/src/test/datascience/mockDocument.ts b/src/test/datascience/mockDocument.ts deleted file mode 100644 index e1ced53db4b3..000000000000 --- a/src/test/datascience/mockDocument.ts +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { EndOfLine, Position, Range, TextDocument, TextDocumentContentChangeEvent, TextLine, Uri } from 'vscode'; - -import { - DefaultWordPattern, - ensureValidWordDefinition, - getWordAtText, - regExpLeadsToEndlessLoop -} from '../../client/datascience/interactive-window/intellisense/wordHelper'; - -class MockLine implements TextLine { - private _range: Range; - private _rangeWithLineBreak: Range; - private _firstNonWhitespaceIndex: number | undefined; - private _isEmpty: boolean | undefined; - - constructor(private _contents: string, private _line: number, private _offset: number) { - this._range = new Range(new Position(_line, 0), new Position(_line, _contents.length)); - this._rangeWithLineBreak = new Range(this.range.start, new Position(_line, _contents.length + 1)); - } - - public get offset(): number { - return this._offset; - } - public get lineNumber(): number { - return this._line; - } - public get text(): string { - return this._contents; - } - public get range(): Range { - return this._range; - } - public get rangeIncludingLineBreak(): Range { - return this._rangeWithLineBreak; - } - public get firstNonWhitespaceCharacterIndex(): number { - if (this._firstNonWhitespaceIndex === undefined) { - this._firstNonWhitespaceIndex = this._contents.trimLeft().length - this._contents.length; - } - return this._firstNonWhitespaceIndex; - } - public get isEmptyOrWhitespace(): boolean { - if (this._isEmpty === undefined) { - this._isEmpty = this._contents.length === 0 || this._contents.trim().length === 0; - } - return this._isEmpty; - } -} - -export class MockDocument implements TextDocument { - private _uri: Uri; - private _version: number = 0; - private _lines: MockLine[] = []; - private _contents: string = ''; - - constructor(contents: string, fileName: string) { - this._uri = Uri.file(fileName); - this._contents = contents; - this._lines = this.createLines(); - } - - public get uri(): Uri { - return this._uri; - } - public get fileName(): string { - return this._uri.fsPath; - } - - public get isUntitled(): boolean { - return true; - } - public get languageId(): string { - return 'python'; - } - public get version(): number { - return this._version; - } - public get isDirty(): boolean { - return true; - } - public get isClosed(): boolean { - return false; - } - public save(): Thenable<boolean> { - return Promise.resolve(true); - } - public get eol(): EndOfLine { - return EndOfLine.LF; - } - public get lineCount(): number { - return this._lines.length; - } - public lineAt(position: Position | number): TextLine { - if (typeof position === 'number') { - return this._lines[position as number]; - } else { - return this._lines[position.line]; - } - } - public offsetAt(position: Position): number { - return this.convertToOffset(position); - } - public positionAt(offset: number): Position { - let line = 0; - let ch = 0; - while (line + 1 < this._lines.length && this._lines[line + 1].offset <= offset) { - line += 1; - } - if (line < this._lines.length) { - ch = offset - this._lines[line].offset; - } - return new Position(line, ch); - } - public getText(range?: Range | undefined): string { - if (!range) { - return this._contents; - } else { - const startOffset = this.convertToOffset(range.start); - const endOffset = this.convertToOffset(range.end); - return this._contents.substr(startOffset, endOffset - startOffset); - } - } - public getWordRangeAtPosition(position: Position, regexp?: RegExp | undefined): Range | undefined { - if (!regexp) { - // use default when custom-regexp isn't provided - regexp = DefaultWordPattern; - - } else if (regExpLeadsToEndlessLoop(regexp)) { - // use default when custom-regexp is bad - console.warn(`[getWordRangeAtPosition]: ignoring custom regexp '${regexp.source}' because it matches the empty string.`); - regexp = DefaultWordPattern; - } - - const wordAtText = getWordAtText( - position.character + 1, - ensureValidWordDefinition(regexp), - this._lines[position.line].text, - 0 - ); - - if (wordAtText) { - return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1); - } - return undefined; - } - public validateRange(range: Range): Range { - return range; - } - public validatePosition(position: Position): Position { - return position; - } - - public edit(c: TextDocumentContentChangeEvent): void { - this._version += 1; - const before = this._contents.substr(0, c.rangeOffset); - const after = this._contents.substr(c.rangeOffset + c.rangeLength); - this._contents = `${before}${c.text}${after}`; - this._lines = this.createLines(); - } - - private createLines(): MockLine[] { - const split = this._contents.split('\n'); - let prevLine: MockLine | undefined; - return split.map((s, i) => { - const nextLine = this.createTextLine(s, i, prevLine); - prevLine = nextLine; - return nextLine; - }); - } - - private createTextLine(line: string, index: number, prevLine: MockLine | undefined): MockLine { - return new MockLine(line, index, prevLine ? prevLine.offset + prevLine.rangeIncludingLineBreak.end.character : 0); - } - - private convertToOffset(pos: Position): number { - if (pos.line < this._lines.length) { - return this._lines[pos.line].offset + Math.min(this._lines[pos.line].rangeIncludingLineBreak.end.character, pos.character); - } - return this._contents.length; - } -} diff --git a/src/test/datascience/mockDocumentManager.ts b/src/test/datascience/mockDocumentManager.ts deleted file mode 100644 index dbc07c12e02b..000000000000 --- a/src/test/datascience/mockDocumentManager.ts +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { - DecorationRenderOptions, - Event, - EventEmitter, - Range, - TextDocument, - TextDocumentChangeEvent, - TextDocumentShowOptions, - TextEditor, - TextEditorDecorationType, - TextEditorOptionsChangeEvent, - TextEditorSelectionChangeEvent, - TextEditorViewColumnChangeEvent, - Uri, - ViewColumn, - WorkspaceEdit -} from 'vscode'; - -import { IDocumentManager } from '../../client/common/application/types'; -import { MockDocument } from './mockDocument'; -import { MockEditor } from './mockTextEditor'; -// tslint:disable:no-any no-http-string no-multiline-string max-func-body-length - -export class MockDocumentManager implements IDocumentManager { - public textDocuments: TextDocument[] = []; - public activeTextEditor: TextEditor | undefined; - public visibleTextEditors: TextEditor[] = []; - private didChangeEmitter = new EventEmitter<TextEditor>(); - private didOpenEmitter = new EventEmitter<TextDocument>(); - private didChangeVisibleEmitter = new EventEmitter<TextEditor[]>(); - private didChangeTextEditorSelectionEmitter = new EventEmitter<TextEditorSelectionChangeEvent>(); - private didChangeTextEditorOptionsEmitter = new EventEmitter<TextEditorOptionsChangeEvent>(); - private didChangeTextEditorViewColumnEmitter = new EventEmitter<TextEditorViewColumnChangeEvent>(); - private didCloseEmitter = new EventEmitter<TextDocument>(); - private didSaveEmitter = new EventEmitter<TextDocument>(); - private didChangeTextDocumentEmitter = new EventEmitter<TextDocumentChangeEvent>(); - public get onDidChangeActiveTextEditor(): Event<TextEditor> { - return this.didChangeEmitter.event; - } - public get onDidChangeTextDocument(): Event<TextDocumentChangeEvent> { - return this.didChangeTextDocumentEmitter.event; - } - public get onDidOpenTextDocument(): Event<TextDocument> { - return this.didOpenEmitter.event; - } - public get onDidChangeVisibleTextEditors(): Event<TextEditor[]> { - return this.didChangeVisibleEmitter.event; - } - public get onDidChangeTextEditorSelection(): Event<TextEditorSelectionChangeEvent> { - return this.didChangeTextEditorSelectionEmitter.event; - } - public get onDidChangeTextEditorOptions(): Event<TextEditorOptionsChangeEvent> { - return this.didChangeTextEditorOptionsEmitter.event; - } - public get onDidChangeTextEditorViewColumn(): Event<TextEditorViewColumnChangeEvent> { - return this.didChangeTextEditorViewColumnEmitter.event; - } - public get onDidCloseTextDocument(): Event<TextDocument> { - return this.didCloseEmitter.event; - } - public get onDidSaveTextDocument(): Event<TextDocument> { - return this.didSaveEmitter.event; - } - public showTextDocument(_document: TextDocument, _column?: ViewColumn, _preserveFocus?: boolean): Thenable<TextEditor>; - public showTextDocument(_document: TextDocument | Uri, _options?: TextDocumentShowOptions): Thenable<TextEditor>; - public showTextDocument(_document: any, _column?: any, _preserveFocus?: any): Thenable<TextEditor> { - const mockEditor = new MockEditor(this, this.lastDocument as MockDocument); - this.activeTextEditor = mockEditor; - return Promise.resolve(mockEditor); - } - public openTextDocument(_fileName: string | Uri): Thenable<TextDocument>; - public openTextDocument(_options?: { language?: string; content?: string }): Thenable<TextDocument>; - public openTextDocument(_options?: any): Thenable<TextDocument> { - return Promise.resolve(this.lastDocument); - } - public applyEdit(_edit: WorkspaceEdit): Thenable<boolean> { - throw new Error('Method not implemented.'); - } - - public addDocument(code: string, file: string) { - const mockDoc = new MockDocument(code, file); - this.textDocuments.push(mockDoc); - } - - public changeDocument(file: string, changes: {range: Range; newText: string}[]) { - const doc = this.textDocuments.find(d => d.fileName === file) 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 - }; - this.didChangeTextDocumentEmitter.fire(ev); - ev.contentChanges.forEach(doc.edit.bind(doc)); - } - } - - 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'); - } -} diff --git a/src/test/datascience/mockExtensions.ts b/src/test/datascience/mockExtensions.ts deleted file mode 100644 index 3fe85666712f..000000000000 --- a/src/test/datascience/mockExtensions.ts +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { injectable } from 'inversify'; -import { Extension } from 'vscode'; - -import { IExtensions } from '../../client/common/types'; - -// tslint:disable:no-any unified-signatures - -@injectable() -export class MockExtensions implements IExtensions { - public all: Extension<any>[] = []; - public getExtension<T>(_extensionId: string) : Extension<T> | undefined { - return undefined; - } -} diff --git a/src/test/datascience/mockJupyterManager.ts b/src/test/datascience/mockJupyterManager.ts deleted file mode 100644 index 7482d3deb6bd..000000000000 --- a/src/test/datascience/mockJupyterManager.ts +++ /dev/null @@ -1,472 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils'; -import { ChildProcess } from 'child_process'; -import * as fs from 'fs-extra'; -import * as os from 'os'; -import * as path from 'path'; -import { Observable } from 'rxjs/Observable'; -import * as 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 { 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 { CellMatcher } from '../../client/datascience/cellMatcher'; -import { concatMultilineString } from '../../client/datascience/common'; -import { CodeSnippits, Identifiers } from '../../client/datascience/constants'; -import { - ICell, - 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<IPythonExecutionFactory>('Python Exec Factory'); - private processServiceFactory = this.createTypeMoq<IProcessServiceFactory>('Process Exec Factory'); - private processService: MockProcessService = new MockProcessService(); - private interpreterService = this.createTypeMoq<IInterpreterService>('Interpreter Service'); - private asyncRegistry : IAsyncDisposableRegistry; - private changedInterpreterEvent: EventEmitter<void> = new EventEmitter<void>(); - private installedInterpreters : PythonInterpreter[] = []; - private pythonServices: MockPythonService[] = []; - private activeInterpreter: PythonInterpreter | undefined; - private sessionTimeout: number | undefined; - private cellDictionary: Record<string, ICell> = {}; - private kernelSpecs : {name: string; dir: string}[] = []; - private currentSession: MockJupyterSession | undefined; - - constructor(serviceManager: IServiceManager) { - // Save async registry. Need to stick servers created into it - this.asyncRegistry = serviceManager.get<IAsyncDisposableRegistry>(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>(IConfigurationService); - if (configService && configService !== null) { - configService.getSettings().onDidChange(this.onConfigChanged.bind(this)); - } - - // Stick our services into the service manager - serviceManager.addSingletonInstance<IJupyterSessionManager>(IJupyterSessionManager, this); - serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, this.interpreterService.object); - serviceManager.addSingletonInstance<IPythonExecutionFactory>(IPythonExecutionFactory, this.pythonExecutionFactory.object); - serviceManager.addSingletonInstance<IProcessServiceFactory>(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(CodeSnippits.MatplotLibInitSvg); - this.addCell(CodeSnippits.MatplotLibInitPng); - this.addCell(`import sys\r\nsys.path.append('undefined')`); - this.addCell(`import ptvsd\r\nptvsd.enable_attach(('localhost', 0))`); - this.addCell('matplotlib.style.use(\'dark_background\')'); - this.addCell(`matplotlib.rcParams.update(${Identifiers.MatplotLibDefaultParams})`); - this.addCell(`%cd "${path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience')}"`); - 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 getCurrentSession() : MockJupyterSession | undefined { - return this.currentSession; - } - - public setProcessDelay(timeout: number | undefined) { - this.processService.setDelay(timeout); - this.pythonServices.forEach(p => p.setDelay(timeout)); - } - - public addInterpreter(interpreter: PythonInterpreter, supportedCommands: SupportedCommands, notebookStdErr?: string[], notebookProc?: ChildProcess) { - this.installedInterpreters.push(interpreter); - - // Add the python calls first. - const pythonService = new MockPythonService(interpreter); - this.pythonServices.push(pythonService); - this.pythonExecutionFactory.setup(f => f.create(TypeMoq.It.is(o => { - return o && o.pythonPath ? o.pythonPath === interpreter.path : false; - }))).returns(() => Promise.resolve(pythonService)); - this.pythonExecutionFactory.setup(f => f.createActivatedEnvironment(TypeMoq.It.is(o => { - return !o || JSON.stringify(o.interpreter) === JSON.stringify(interpreter); - }))).returns(() => Promise.resolve(pythonService)); - this.setupSupportedPythonService(pythonService, interpreter, supportedCommands, notebookStdErr, notebookProc); - - // Then the process calls - this.setupSupportedProcessService(interpreter, supportedCommands, notebookStdErr); - - // Default to being the new active - this.makeActive(interpreter); - } - - public 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(undefined, code, 'foo.py', 1, true, uuid()); - 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 as any).resultGenerator = async (t: CancellationToken) => { - const result = await resultGenerator(t); - return { - result: this.createStreamResult(result.result), - haveMore: result.haveMore - }; - }; - - // Save in the cell. - c.data = data; - } - - // Save each in our dictionary for future use. - // Note: Our entire setup is recreated each test so this dictionary - // should be unique per test - this.cellDictionary[key] = c; - }); - } - - public addCell(code: string, result?: undefined | string | number | nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, mimeType?: string) { - const cells = generateCells(undefined, code, 'foo.py', 1, true, uuid()); - cells.forEach(c => { - const cellMatcher = new CellMatcher(); - const key = cellMatcher.stripMarkers(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; - if (result) { - data.outputs = [...data.outputs, massagedResult]; - } else { - data.outputs = [...data.outputs]; - } - 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<IJupyterSession> { - 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 this.createNewSession(); - }, cancelToken); - } else { - return Promise.resolve(this.createNewSession()); - } - } - - public getActiveKernelSpecs(_connection: IConnection) : Promise<IJupyterKernelSpec[]> { - return Promise.resolve([]); - } - - private onConfigChanged = () => { - this.changedInterpreterEvent.fire(); - } - - private createNewSession() : MockJupyterSession { - this.currentSession = new MockJupyterSession(this.cellDictionary, MockJupyterTimeDelay); - return this.currentSession; - } - - private createStreamResult(str: string) : nbformat.IStream { - return { - output_type: 'stream', - name: 'stdout', - text: str - }; - } - - private massageCellResult( - result: undefined | string | number | nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError, - mimeType?: string) : - nbformat.IUnrecognizedOutput | nbformat.IExecuteResult | nbformat.IDisplayData | nbformat.IStream | nbformat.IError { - - // See if undefined or string or number - if (!result) { - // This is an empty execute result - return { - output_type: 'execute_result', - execution_count: 1, - data: {}, - metadata : {} - }; - } else if (typeof result === 'string') { - const data = {}; - (data as any)[mimeType ? mimeType : 'text/plain'] = result; - return { - output_type: 'execute_result', - execution_count: 1, - data: data, - metadata: {} - }; - } else if (typeof result === 'number') { - return { - output_type: 'execute_result', - execution_count: 1, - data: { 'text/plain' : result.toString() }, - metadata : {} - }; - } else { - return result; - } - } - - private createTempSpec(pythonPath: string): string { - const tempDir = os.tmpdir(); - const subDir = uuid(); - const filePath = path.join(tempDir, subDir, 'kernel.json'); - fs.ensureDirSync(path.dirname(filePath)); - fs.writeJSONSync(filePath, - { - display_name: 'Python 3', - language: 'python', - argv: [ - pythonPath, - '-m', - 'ipykernel_launcher', - '-f', - '{connection_file}' - ] - }); - return filePath; - } - - private createTypeMoq<T>(tag: string): TypeMoq.IMock<T> { - // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class - // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 - const result = TypeMoq.Mock.ofType<T>(); - (result as any).tag = tag; - result.setup((x: any) => x.then).returns(() => undefined); - return result; - } - - private setupPythonServiceExec(service: MockPythonService, module: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - service.addExecResult(['-m', module, ...args], result); - service.addExecModuleResult(module, args, result); - } - - private setupPythonServiceExecObservable(service: MockPythonService, module: string, args: (string | RegExp)[], stderr: string[], stdout: string[], proc?: ChildProcess) { - const result = { - proc, - out: new Observable<Output<string>>(subscriber => { - stderr.forEach(s => subscriber.next({ source: 'stderr', out: s })); - stdout.forEach(s => subscriber.next({ source: 'stderr', out: s })); - }), - dispose: () => { - noop(); - } - }; - - service.addExecObservableResult(['-m', module, ...args], () => result); - service.addExecModuleObservableResult(module, args, () => result); - } - - private setupProcessServiceExec(service: MockProcessService, file: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - service.addExecResult(file, args, result); - } - - private setupProcessServiceExecObservable(service: MockProcessService, file: string, args: (string | RegExp)[], stderr: string[], stdout: string[]) { - service.addExecObservableResult(file, args, () => { - return { - proc: undefined, - out: new Observable<Output<string>>(subscriber => { - stderr.forEach(s => subscriber.next({ source: 'stderr', out: s })); - stdout.forEach(s => subscriber.next({ source: 'stderr', out: s })); - }), - dispose: () => { - noop(); - } - }; - }); - } - - private setupSupportedPythonService(service: MockPythonService, workingPython: PythonInterpreter, supportedCommands: SupportedCommands, notebookStdErr?: string[], notebookProc?: ChildProcess) { - 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' })); - this.setupPythonServiceExec(service, 'jupyter', ['nbconvert', /.*/, '--to', 'python', '--stdout', '--template', /.*/], () => { - return Promise.resolve({ - stdout: '#%%\r\nimport os\r\nos.chdir()' - }); - }); - } - - if ((supportedCommands & SupportedCommands.notebook) === SupportedCommands.notebook) { - this.setupPythonServiceExec(service, 'jupyter', ['notebook', '--version'], () => Promise.resolve({ stdout: '1.1.1.1' })); - this.setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'], notebookProc); - this.setupPythonServiceExecObservable(service, 'jupyter', ['notebook', '--no-browser', /--notebook-dir=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198'], notebookProc); - } - 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=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - this.setupProcessServiceExecObservable(this.processService, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - } else if ((supportedCommands & SupportedCommands.notebook) === SupportedCommands.notebook) { - this.setupProcessServiceExec(this.processService, workingPython.path, ['-m', 'jupyter', 'kernelspec', 'list'], () => { - 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=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - this.setupProcessServiceExecObservable(this.processService, workingPython.path, ['-m', 'jupyter', 'notebook', '--no-browser', /--notebook-dir=.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], notebookStdErr ? notebookStdErr : ['http://localhost:8888/?token=198']); - } - if ((supportedCommands & SupportedCommands.nbconvert) === SupportedCommands.nbconvert) { - this.setupProcessServiceExec(this.processService, workingPython.path, ['-m', 'jupyter', 'nbconvert', /.*/, '--to', 'python', '--stdout', '--template', /.*/], () => { - return Promise.resolve({ - stdout: '#%%\r\nimport os\r\nos.chdir()' - }); - }); - } - } - - 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=.*/, /.*/, '--NotebookApp.iopub_data_rate_limit=10000000000.0'], [], 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 83e94a3c7a95..000000000000 --- a/src/test/datascience/mockJupyterRequest.ts +++ /dev/null @@ -1,211 +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<IMessageResult>; -} - -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<IMessageResult> { - return new Promise<IMessageResult>((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<IMessageResult> { - // Special case the 'generator' cell that returns a function - // to generate output. - if (this.output.output_type === 'generator') { - const resultEntry = <any>this.output.resultGenerator; - const resultGenerator = resultEntry as (t: CancellationToken) => Promise<{result: nbformat.IStream; haveMore: boolean}>; - if (resultGenerator) { - const streamResult = await resultGenerator(this.cancelToken); - return { - message: this.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<void>; - public onStdin: (msg: KernelMessage.IStdinMessage) => void | PromiseLike<void>; - public onIOPub: (msg: KernelMessage.IIOPubMessage) => void | PromiseLike<void>; - public isDisposed: boolean = false; - - private deferred: Deferred<KernelMessage.IShellMessage> = createDeferred<KernelMessage.IShellMessage>(); - private executionCount: number; - private cell: ICell; - private cancelToken: CancellationToken; - - 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<KernelMessage.IShellMessage> { - return this.deferred.promise; - } - public registerMessageHook(_hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void { - noop(); - } - public removeMessageHook(_hook: (msg: KernelMessage.IIOPubMessage) => boolean | PromiseLike<boolean>): void { - noop(); - } - public sendInputReply(_content: KernelMessage.IInputReply): void { - 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 (!this.cancelToken.isCancellationRequested) { - 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(<any>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 89bed747b00a..000000000000 --- a/src/test/datascience/mockJupyterSession.ts +++ /dev/null @@ -1,128 +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 { JupyterKernelPromiseFailedError } from '../../client/datascience/jupyter/jupyterKernelPromiseFailedError'; -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: Record<string, ICell>; - private restartedEvent: EventEmitter<void> = new EventEmitter<void>(); - private timedelay: number; - private executionCount: number = 0; - private outstandingRequestTokenSources: CancellationTokenSource[] = []; - private executes: string[] = []; - private forceRestartTimeout : boolean = false; - private completionTimeout: number = 1; - - constructor(cellDictionary: Record<string, ICell>, timedelay: number) { - this.dict = cellDictionary; - this.timedelay = timedelay; - } - - public get onRestarted() : Event<void> { - return this.restartedEvent.event; - } - - public async restart(_timeout: number): Promise<void> { - // For every outstanding request, switch them to fail mode - const requests = [...this.outstandingRequestTokenSources]; - requests.forEach(r => r.cancel()); - - if (this.forceRestartTimeout) { - throw new JupyterKernelPromiseFailedError('Forcing restart timeout'); - } - - return sleep(this.timedelay); - } - public interrupt(_timeout: number): Promise<void> { - const requests = [...this.outstandingRequestTokenSources]; - requests.forEach(r => r.cancel()); - return sleep(this.timedelay); - } - public waitForIdle(_timeout: number): Promise<void> { - return sleep(this.timedelay); - } - - public prolongRestarts() { - this.forceRestartTimeout = true; - } - public requestExecute(content: KernelMessage.IExecuteRequest, _disposeOnDone?: boolean, _metadata?: JSONObject): Kernel.IFuture { - // Content should have the code - const cell = this.findCell(content.code); - if (cell) { - this.executes.push(content.code); - } - - // Create a new dummy request - this.executionCount += 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 requestComplete(_content: KernelMessage.ICompleteRequest): Promise<KernelMessage.ICompleteReplyMsg | undefined> { - await sleep(this.completionTimeout); - - return { - content: { - matches: ['printly'], // This keeps this in the intellisense when the editor pairs down results - cursor_start: 0, - cursor_end: 7, - status: 'ok', - metadata: {} - }, - channel: 'shell', - header: { - username: 'foo', - version: '1', - session: '1', - msg_id: '1', - msg_type: 'complete' - }, - parent_header: { - }, - metadata: { - } - }; - } - - public dispose(): Promise<void> { - return sleep(10); - } - - public getExecutes() : string [] { - return this.executes; - } - - public setCompletionTimeout(timeout: number) { - this.completionTimeout = timeout; - } - - 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()[1]} not found in mock`); - throw new Error(`Cell ${code.splitLines()[1]} not found in mock`); - } -} diff --git a/src/test/datascience/mockLanguageClient.ts b/src/test/datascience/mockLanguageClient.ts deleted file mode 100644 index b417d347765f..000000000000 --- a/src/test/datascience/mockLanguageClient.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { - CancellationToken, - DiagnosticCollection, - Disposable, - Event, - OutputChannel, - TextDocumentContentChangeEvent -} from 'vscode'; -import { - Code2ProtocolConverter, - CompletionItem, - DynamicFeature, - ErrorHandler, - GenericNotificationHandler, - GenericRequestHandler, - InitializeResult, - LanguageClient, - LanguageClientOptions, - MessageTransports, - NotificationHandler, - NotificationHandler0, - NotificationType, - NotificationType0, - Protocol2CodeConverter, - RequestHandler, - RequestHandler0, - RequestType, - RequestType0, - RPCMessageType, - ServerOptions, - StateChangeEvent, - StaticFeature, - TextDocumentItem, - Trace, - VersionedTextDocumentIdentifier -} from 'vscode-languageclient'; - -import { createDeferred, Deferred } from '../../client/common/utils/async'; -import { noop } from '../core'; -import { MockProtocolConverter } from './mockProtocolConverter'; - -// tslint:disable:no-any unified-signatures -export class MockLanguageClient extends LanguageClient { - private notificationPromise : Deferred<void> | undefined; - private contents : string; - private versionId: number | null; - private converter: MockProtocolConverter; - - public constructor(name: string, serverOptions: ServerOptions, clientOptions: LanguageClientOptions, forceDebug?: boolean) { - (LanguageClient.prototype as any).checkVersion = noop; - super(name, serverOptions, clientOptions, forceDebug); - this.contents = ''; - this.versionId = 0; - this.converter = new MockProtocolConverter(); - } - public waitForNotification() : Promise<void> { - this.notificationPromise = createDeferred(); - return this.notificationPromise.promise; - } - - // Returns the current contents of the document being built by the completion provider calls - public getDocumentContents() : string { - return this.contents; - } - - public getVersionId() : number | null { - return this.versionId; - } - - public stop(): Thenable<void> { - throw new Error('Method not implemented.'); - } - public registerProposedFeatures(): void { - throw new Error('Method not implemented.'); - } - public get initializeResult(): InitializeResult | undefined { - throw new Error('Method not implemented.'); - } - public sendRequest<R, E, RO>(type: RequestType0<R, E, RO>, token?: CancellationToken | undefined): Thenable<R>; - public sendRequest<P, R, E, RO>(type: RequestType<P, R, E, RO>, params: P, token?: CancellationToken | undefined): Thenable<R>; - public sendRequest<R>(method: string, token?: CancellationToken | undefined): Thenable<R>; - public sendRequest<R>(method: string, param: any, token?: CancellationToken | undefined): Thenable<R>; - public sendRequest(_method: any, _param?: any, _token?: any) : Thenable<any> { - switch (_method.method) { - case 'textDocument/completion': - // Just return one for each line of our contents - return Promise.resolve(this.getDocumentCompletions()); - break; - - default: - break; - } - return Promise.resolve(); - } - public onRequest<R, E, RO>(type: RequestType0<R, E, RO>, handler: RequestHandler0<R, E>): void; - public onRequest<P, R, E, RO>(type: RequestType<P, R, E, RO>, handler: RequestHandler<P, R, E>): void; - public onRequest<R, E>(method: string, handler: GenericRequestHandler<R, E>): void; - public onRequest(_method: any, _handler: any) { - throw new Error('Method not implemented.'); - } - public sendNotification<RO>(type: NotificationType0<RO>): void; - public sendNotification<P, RO>(type: NotificationType<P, RO>, params?: P | undefined): void; - public sendNotification(method: string): void; - public sendNotification(method: string, params: any): void; - public sendNotification(method: any, params?: any) { - switch (method.method) { - case 'textDocument/didOpen': - const item = params.textDocument as TextDocumentItem; - if (item) { - this.contents = item.text; - this.versionId = item.version; - } - break; - - case 'textDocument/didChange': - const id = params.textDocument as VersionedTextDocumentIdentifier; - const changes = params.contentChanges as TextDocumentContentChangeEvent[]; - if (id && changes) { - this.applyChanges(changes); - this.versionId = id.version; - } - break; - - default: - if (this.notificationPromise) { - this.notificationPromise.reject(new Error(`Unknown notification ${method.method}`)); - } - break; - } - if (this.notificationPromise && !this.notificationPromise.resolved) { - this.notificationPromise.resolve(); - } - } - public onNotification<RO>(type: NotificationType0<RO>, handler: NotificationHandler0): void; - public onNotification<P, RO>(type: NotificationType<P, RO>, handler: NotificationHandler<P>): void; - public onNotification(method: string, handler: GenericNotificationHandler): void; - public onNotification(_method: any, _handler: any) { - throw new Error('Method not implemented.'); - } - public get clientOptions(): LanguageClientOptions { - throw new Error('Method not implemented.'); - } - public get protocol2CodeConverter(): Protocol2CodeConverter { - throw new Error('Method not implemented.'); - } - public get code2ProtocolConverter(): Code2ProtocolConverter { - return this.converter; - } - public get onTelemetry(): Event<any> { - throw new Error('Method not implemented.'); - } - public get onDidChangeState(): Event<StateChangeEvent> { - throw new Error('Method not implemented.'); - } - public get outputChannel(): OutputChannel { - throw new Error('Method not implemented.'); - } - public get diagnostics(): DiagnosticCollection | undefined { - throw new Error('Method not implemented.'); - } - public createDefaultErrorHandler(): ErrorHandler { - throw new Error('Method not implemented.'); - } - public get trace(): Trace { - throw new Error('Method not implemented.'); - } - public info(_message: string, _data?: any): void { - throw new Error('Method not implemented.'); - } - public warn(_message: string, _data?: any): void { - throw new Error('Method not implemented.'); - } - public error(_message: string, _data?: any): void { - throw new Error('Method not implemented.'); - } - public needsStart(): boolean { - throw new Error('Method not implemented.'); - } - public needsStop(): boolean { - throw new Error('Method not implemented.'); - } - public onReady(): Promise<void> { - throw new Error('Method not implemented.'); - } - public start(): Disposable { - throw new Error('Method not implemented.'); - } - public registerFeatures(_features: (StaticFeature | DynamicFeature<any>)[]): void { - throw new Error('Method not implemented.'); - } - public registerFeature(_feature: StaticFeature | DynamicFeature<any>): void { - throw new Error('Method not implemented.'); - } - public logFailedRequest(_type: RPCMessageType, _error: any): void { - throw new Error('Method not implemented.'); - } - - protected handleConnectionClosed(): void { - throw new Error('Method not implemented.'); - } - protected createMessageTransports(_encoding: string): Thenable<MessageTransports> { - throw new Error('Method not implemented.'); - } - protected registerBuiltinFeatures(): void { - noop(); - } - - private applyChanges(changes: TextDocumentContentChangeEvent[]) { - changes.forEach(c => { - const before = this.contents.substr(0, c.rangeOffset); - const after = this.contents.substr(c.rangeOffset + c.rangeLength); - this.contents = `${before}${c.text}${after}`; - }); - } - - private getDocumentCompletions() : CompletionItem[] { - const lines = this.contents.splitLines(); - return lines.map(l => { - return { - label: l, - insertText: l, - sortText: l - }; - }); - } -} diff --git a/src/test/datascience/mockLanguageServer.ts b/src/test/datascience/mockLanguageServer.ts deleted file mode 100644 index f9daf07ed2ff..000000000000 --- a/src/test/datascience/mockLanguageServer.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { injectable } from 'inversify'; -import { Uri } from 'vscode'; -import { LanguageClient, LanguageClientOptions } from 'vscode-languageclient'; - -import { ILanguageServer } from '../../client/activation/types'; -import { MockLanguageClient } from './mockLanguageClient'; - -// tslint:disable:no-any unified-signatures -@injectable() -export class MockLanguageServer implements ILanguageServer { - private mockLanguageClient: MockLanguageClient | undefined; - - public get languageClient(): LanguageClient | undefined { - if (!this.mockLanguageClient) { - this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); - } - return this.mockLanguageClient; - } - - public start(_resource: Uri | undefined, _options: LanguageClientOptions): Promise<void> { - if (!this.mockLanguageClient) { - this.mockLanguageClient = new MockLanguageClient('mockLanguageClient', { module: 'dummy' }, {}); - } - return Promise.resolve(); - } - public loadExtension(_args?: {} | undefined): void { - throw new Error('Method not implemented.'); - } - public dispose(): void | undefined { - this.mockLanguageClient = undefined; - } - -} diff --git a/src/test/datascience/mockLanguageServerAnalysisOptions.ts b/src/test/datascience/mockLanguageServerAnalysisOptions.ts deleted file mode 100644 index f31c494f58df..000000000000 --- a/src/test/datascience/mockLanguageServerAnalysisOptions.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { injectable } from 'inversify'; -import { Event, EventEmitter } from 'vscode'; -import { LanguageClientOptions } from 'vscode-languageclient'; - -import { ILanguageServerAnalysisOptions } from '../../client/activation/types'; -import { Resource } from '../../client/common/types'; -import { noop } from '../core'; - -// tslint:disable:no-any unified-signatures -@injectable() -export class MockLanguageServerAnalysisOptions implements ILanguageServerAnalysisOptions { - private onDidChangeEmitter: EventEmitter<void> = new EventEmitter<void>(); - - public get onDidChange(): Event<void> { - return this.onDidChangeEmitter.event; - } - - public initialize(_resource: Resource): Promise<void> { - return Promise.resolve(); - } - public getAnalysisOptions(): Promise<LanguageClientOptions> { - return Promise.resolve({ - }); - } - public dispose(): void | undefined { - noop(); - } -} diff --git a/src/test/datascience/mockLiveShare.ts b/src/test/datascience/mockLiveShare.ts deleted file mode 100644 index 031826718886..000000000000 --- a/src/test/datascience/mockLiveShare.ts +++ /dev/null @@ -1,435 +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 uuid from 'uuid/v4'; -import { CancellationToken, CancellationTokenSource, Disposable, Event, EventEmitter, TreeDataProvider, Uri } from 'vscode'; -import * as vsls from 'vsls/vscode'; - -import { IApplicationShell, ILiveShareTestingApi } from '../../client/common/application/types'; -import { LiveShareProxy } from '../../client/common/liveshare/liveshareProxy'; -import { IConfigurationService, IDisposable, IDisposableRegistry } from '../../client/common/types'; -import { noop } from '../../client/common/utils/misc'; -import { LiveShare } from '../../client/datascience/constants'; - -// tslint:disable:no-any unified-signatures max-classes-per-file - -class MockLiveService implements vsls.SharedService, vsls.SharedServiceProxy { - public isServiceAvailable: boolean = true; - private changeIsServiceAvailableEmitter: EventEmitter<boolean> = new EventEmitter<boolean>(); - private requestHandlers: Map<string, vsls.RequestHandler> = new Map<string, vsls.RequestHandler>(); - private notifyHandlers: Map<string, vsls.NotifyHandler> = new Map<string, vsls.NotifyHandler>(); - private defaultCancellationSource = new CancellationTokenSource(); - private sibling: MockLiveService | undefined; - - public setSibling(sibling: MockLiveService) { - this.sibling = sibling; - } - - public get onDidChangeIsServiceAvailable(): Event<boolean> { - return this.changeIsServiceAvailableEmitter.event; - } - public request(name: string, args: any[], cancellation?: CancellationToken): Promise<any> { - // See if any handlers. - const handler = this.sibling ? this.sibling.requestHandlers.get(name) : undefined; - if (handler) { - return handler(args, cancellation ? cancellation : this.defaultCancellationSource.token); - } - return Promise.resolve(); - } - public onRequest(name: string, handler: vsls.RequestHandler): void { - this.requestHandlers.set(name, handler); - } - public onNotify(name: string, handler: vsls.NotifyHandler): void { - this.notifyHandlers.set(name, handler); - } - public notify(name: string, args: object): void { - // See if any handlers. - const handler = this.sibling ? this.sibling.notifyHandlers.get(name) : undefined; - if (handler) { - handler(args); - } - } - - public clearHandlers(): void { - this.requestHandlers.clear(); - this.notifyHandlers.clear(); - } -} - -type ArgumentType = 'boolean' | 'number' | 'string' | 'object' | 'function' | 'array' | 'uri'; - -function checkArg(value: any, name: string, type?: ArgumentType) { - if (!value) { - throw new Error(`Argument \'${name}\' is required.`); - } else if (type) { - if (type === 'array') { - if (!Array.isArray(value)) { - throw new Error(`Argument \'${name}\' must be an array.`); - } - } else if (type === 'uri') { - if (!(value instanceof Uri)) { - throw new Error(`Argument \'${name}\' must be a Uri object.`); - } - } else if (type === 'object' && Array.isArray(value)) { - throw new Error(`Argument \'${name}\' must be a a non-array object.`); - } else if (typeof value !== type) { - throw new Error(`Argument \'${name}\' must be type \'' + type + '\'.`); - } - } -} - -type Listener = [Function, any] | Function; - -class Emitter<T> { - - private _event: Event<T> | undefined; - private _disposed: boolean = false; - private _deliveryQueue: { listener: Listener; event?: T }[] = []; - private _listeners: Listener[] = []; - - get event(): Event<T> { - if (!this._event) { - this._event = (listener: (e: T) => any, thisArgs?: any, disposables?: IDisposable[]) => { - this._listeners.push(!thisArgs ? listener : [listener, thisArgs]); - let result: IDisposable; - result = { - dispose: () => { - result.dispose = noop; - if (!this._disposed) { - this._listeners = this._listeners.filter(l => l !== listener); - } - } - }; - if (Array.isArray(disposables)) { - disposables.push(result); - } - - return result; - }; - } - return this._event; - } - - public async fire(event?: T): Promise<void> { - if (this._listeners) { - // put all [listener,event]-pairs into delivery queue - // then emit all event. an inner/nested event might be - // the driver of this - - if (!this._deliveryQueue) { - this._deliveryQueue = []; - } - - for (const l of this._listeners) { - this._deliveryQueue.push({ listener: l, event }); - } - - while (this._deliveryQueue.length > 0) { - const item = this._deliveryQueue.shift(); - let result: any; - try { - if (item && item.listener) { - if (typeof item.listener === 'function') { - result = item.listener.call(undefined, item.event); - } else { - const func = item.listener[0]; - if (func) { - result = func.call(item.listener[1], item.event); - } - } - } - } catch (e) { - // Do nothinga - } - if (result) { - const promise = result as Promise<void>; - if (promise) { - await promise; - } - } - } - } - } - - public dispose() { - if (this._listeners) { - this._listeners = []; - } - if (this._deliveryQueue) { - this._deliveryQueue = []; - } - this._disposed = true; - } -} - -class MockLiveShare implements vsls.LiveShare, vsls.Session, vsls.Peer, IDisposable { - private static others: MockLiveShare[] = []; - private static services: Map<string, MockLiveService[]> = new Map<string, MockLiveService[]>(); - private changeSessionEmitter = new Emitter<vsls.SessionChangeEvent>(); - private changePeersEmitter = new EventEmitter<vsls.PeersChangeEvent>(); - private currentPeers: vsls.Peer[] = []; - private _id = uuid(); - private _peerNumber = 0; - private _visibleRole = vsls.Role.None; - constructor(private _role: vsls.Role) { - this._peerNumber = _role === vsls.Role.Host ? 0 : 1; - MockLiveShare.others.push(this); - } - - public onPeerConnected(peer: MockLiveShare) { - if (peer.role !== this.role) { - this.currentPeers.push(peer); - this.changePeersEmitter.fire({ added: [peer], removed: [] }); - } - } - - public dispose() { - MockLiveShare.others = MockLiveShare.others.filter(o => o._id !== this._id); - } - - public get session(): vsls.Session { - return this; - } - - public async start(): Promise<void> { - this._visibleRole = this._role; - - // Special case, we need to wait for the fire to finish. This means - // the real product can have a race condition between starting the session and registering commands? - // Nope, because the guest side can't do anything until the session starts up. - await this.changeSessionEmitter.fire({ session: this }); - if (this._role === vsls.Role.Guest) { - for (const o of MockLiveShare.others) { - if (o._id !== this._id) { - o.onPeerConnected(this); - } - } - } - } - - public async stop(): Promise<void> { - this._visibleRole = vsls.Role.None; - const existingPeers = this.currentPeers; - this.currentPeers = []; - this.changePeersEmitter.fire({ added: [], removed: existingPeers }); - await this.changeSessionEmitter.fire({ session: this }); - } - - public removeHandlers(serviceName: string) { - const services = MockLiveShare.services.get(serviceName); - if (!services) { - throw new Error(`${serviceName} failure to add service to map`); - } - - // Remove just the one corresponding to the role of this api - if (this.role === vsls.Role.Guest) { - services[1].clearHandlers(); - } else { - services[0].clearHandlers(); - } - } - - public getContacts(_emails: string[]): Promise<vsls.ContactsCollection> { - throw new Error('Method not implemented.'); - } - - public get role(): vsls.Role { - return this._visibleRole; - } - public get id(): string { - return this._id; - } - public get peerNumber(): number { - return this._peerNumber; - } - public get user(): vsls.UserInfo { - return { - displayName: 'Test', - emailAddress: 'Test@Microsoft.Com', - userName: 'Test', - id: '0' - }; - } - public get access(): vsls.Access { - return vsls.Access.None; - } - - public get onDidChangeSession(): Event<vsls.SessionChangeEvent> { - return this.changeSessionEmitter.event; - } - public get peers(): vsls.Peer[] { - return this.currentPeers; - } - public get onDidChangePeers(): Event<vsls.PeersChangeEvent> { - return this.changePeersEmitter.event; - } - public share(_options?: vsls.ShareOptions): Promise<Uri> { - throw new Error('Method not implemented.'); - } - public join(_link: Uri, _options?: vsls.JoinOptions): Promise<void> { - throw new Error('Method not implemented.'); - } - public async end(): Promise<void> { - // If we're the guest, just stop ourselves. If we're the host, stop everybody - if (this._role === vsls.Role.Guest) { - await this.stop(); - } else { - await Promise.all(MockLiveShare.others.map(p => p.stop())); - } - } - public shareService(name: string): Promise<vsls.SharedService> { - if (!MockLiveShare.services.has(name)) { - MockLiveShare.services.set(name, this.generateServicePair()); - } - const services = MockLiveShare.services.get(name); - if (!services) { - throw new Error(`${name} failure to add service to map`); - } - - // Host is always the first - return Promise.resolve(services[0]); - } - public unshareService(name: string): Promise<void> { - MockLiveShare.services.delete(name); - return Promise.resolve(); - } - public getSharedService(name: string): Promise<vsls.SharedServiceProxy> { - if (!MockLiveShare.services.has(name)) { - // Don't wait for the host to start. It shouldn't be necessary anyway. - MockLiveShare.services.set(name, this.generateServicePair()); - } - const services = MockLiveShare.services.get(name); - if (!services) { - throw new Error(`${name} failure to add service to map`); - } - - // Guest is always the second one - return Promise.resolve(services[1]); - } - public convertLocalUriToShared(localUri: Uri): Uri { - // Do the same checking that liveshare does - checkArg(localUri, 'localUri', 'uri'); - - if (this.session.role !== vsls.Role.Host) { - throw new Error('Only the host role can convert shared URIs.'); - } - - const scheme = 'vsls'; - if (localUri.scheme === scheme) { - throw new Error(`URI is already a ${scheme} URI: ${localUri}`); - } - - if (localUri.scheme !== 'file') { - throw new Error(`Not a workspace file URI: ${localUri}`); - } - - const file = localUri.fsPath.includes('/') ? path.basename(localUri.fsPath) : localUri.fsPath; - return Uri.parse(`vsls:${file}`); - } - public convertSharedUriToLocal(sharedUri: Uri): Uri { - checkArg(sharedUri, 'sharedUri', 'uri'); - - if (this.session.role !== vsls.Role.Host) { - throw new Error('Only the host role can convert shared URIs.'); - } - - const scheme = 'vsls'; - if (sharedUri.scheme !== scheme) { - throw new Error( - `Not a shared URI: ${sharedUri}`); - } - - return Uri.file(sharedUri.fsPath); - } - public registerCommand(_command: string, _isEnabled?: () => boolean, _thisArg?: any): Disposable { - throw new Error('Method not implemented.'); - } - public registerTreeDataProvider<T>(_viewId: vsls.View, _treeDataProvider: TreeDataProvider<T>): Disposable { - throw new Error('Method not implemented.'); - } - public registerContactServiceProvider(_name: string, _contactServiceProvider: vsls.ContactServiceProvider): Disposable { - throw new Error('Method not implemented.'); - } - public shareServer(_server: vsls.Server): Promise<Disposable> { - // Ignore for now. We don't need to port forward during a test - return Promise.resolve({ dispose: noop }); - } - - private generateServicePair() : MockLiveService[] { - const hostService = new MockLiveService(); - const guestService = new MockLiveService(); - hostService.setSibling(guestService); - guestService.setSibling(hostService); - // Host is always first - return [hostService, guestService]; - } -} - -@injectable() -export class MockLiveShareApi implements ILiveShareTestingApi { - - private currentRole: vsls.Role = vsls.Role.None; - private internalApi: MockLiveShare | null = null; - private externalProxy: vsls.LiveShare | null = null; - private sessionStarted = false; - - constructor( - @inject(IDisposableRegistry) private disposables: IDisposableRegistry, - @inject(IApplicationShell) private appShell : IApplicationShell, - @inject(IConfigurationService) private config: IConfigurationService - ) { - } - - public getApi(): Promise<vsls.LiveShare | null> { - return Promise.resolve(this.externalProxy); - } - - public forceRole(role: vsls.Role) { - // Force a role on our live share api - if (role !== this.currentRole) { - this.internalApi = new MockLiveShare(role); - this.externalProxy = new LiveShareProxy(this.appShell, this.config.getSettings().datascience.liveShareConnectionTimeout, this.internalApi); - this.internalApi.onDidChangeSession(this.onInternalSessionChanged, this); - this.currentRole = role; - this.disposables.push(this.internalApi); - } - } - - public async startSession(): Promise<void> { - if (this.internalApi) { - await this.internalApi.start(); - this.sessionStarted = true; - } else { - throw Error('Cannot start session without a role.'); - } - } - - public async stopSession(): Promise<void> { - if (this.internalApi) { - await this.internalApi.stop(); - this.sessionStarted = false; - } else { - throw Error('Cannot start session without a role.'); - } - } - - public disableGuestChecker() { - // Remove the handlers for the guest checker notification - if (this.internalApi) { - this.internalApi.removeHandlers(LiveShare.GuestCheckerService); - } - this.externalProxy = null; - } - - public get isSessionStarted(): boolean { - return this.sessionStarted; - } - - private onInternalSessionChanged(_ev: vsls.SessionChangeEvent) { - if (this.internalApi) { - this.sessionStarted = this.internalApi.role !== vsls.Role.None; - } - } -} diff --git a/src/test/datascience/mockProcessService.ts b/src/test/datascience/mockProcessService.ts deleted file mode 100644 index 94f41afe3748..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<ExecutionResult<string>> }[] = []; - private execObservableResults: {file: string; args: (string | RegExp)[]; result(): ObservableExecutionResult<string> }[] = []; - private timeDelay: number | undefined; - - public execObservable(file: string, args: string[], _options: SpawnOptions): ObservableExecutionResult<string> { - const match = this.execObservableResults.find(f => this.argsMatch(f.args, args) && f.file === file); - if (match) { - return match.result(); - } - - return this.defaultObservable([file, ...args]); - } - - public async exec(file: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { - const match = this.execResults.find(f => this.argsMatch(f.args, args) && f.file === file); - if (match) { - // Might need a delay before executing to mimic it taking a while. - if (this.timeDelay) { - try { - const localTime = this.timeDelay; - await Cancellation.race((_t) => sleep(localTime), options.token); - } catch (exc) { - if (exc instanceof CancellationError) { - return this.defaultExecutionResult([file, ...args]); - } - } - } - return match.result(); - } - - return this.defaultExecutionResult([file, ...args]); - } - - public shellExec(command: string, _options: ShellOptions) : Promise<ExecutionResult<string>> { - // Not supported - return this.defaultExecutionResult([command]); - } - - public addExecResult(file: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - this.execResults.push({file: file, args: args, result: result}); - } - - public addExecObservableResult(file: string, args: (string | RegExp)[], result: () => ObservableExecutionResult<string>) { - 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<string> { - const output = new Observable<Output<string>>(subscriber => { subscriber.next({out: `Invalid call to ${args.join(' ')}`, source: 'stderr'}); }); - return { - proc: undefined, - out: output, - dispose: () => noop - }; - } - - private defaultExecutionResult(args: string[]) : Promise<ExecutionResult<string>> { - return Promise.resolve({stderr: `Invalid call to ${args.join(' ')}`, stdout: ''}); - } - -} diff --git a/src/test/datascience/mockProtocolConverter.ts b/src/test/datascience/mockProtocolConverter.ts deleted file mode 100644 index bf8143ab8b25..000000000000 --- a/src/test/datascience/mockProtocolConverter.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as code from 'vscode'; -import { Code2ProtocolConverter } from 'vscode-languageclient'; -import * as proto from 'vscode-languageserver-protocol'; - -// tslint:disable:no-any unified-signatures -export class MockProtocolConverter implements Code2ProtocolConverter { - public asUri(_uri: code.Uri): string { - throw new Error('Method not implemented.'); - } - public asTextDocumentIdentifier(_textDocument: code.TextDocument): proto.TextDocumentIdentifier { - throw new Error('Method not implemented.'); - } - public asVersionedTextDocumentIdentifier(_textDocument: code.TextDocument): proto.VersionedTextDocumentIdentifier { - throw new Error('Method not implemented.'); - } - public asOpenTextDocumentParams(_textDocument: code.TextDocument): proto.DidOpenTextDocumentParams { - throw new Error('Method not implemented.'); - } - public asChangeTextDocumentParams(textDocument: code.TextDocument): proto.DidChangeTextDocumentParams; - public asChangeTextDocumentParams(event: code.TextDocumentChangeEvent): proto.DidChangeTextDocumentParams; - public asChangeTextDocumentParams(_event: any): proto.DidChangeTextDocumentParams { - throw new Error('Method not implemented.'); - } - public asCloseTextDocumentParams(_textDocument: code.TextDocument): proto.DidCloseTextDocumentParams { - throw new Error('Method not implemented.'); - } - public asSaveTextDocumentParams(_textDocument: code.TextDocument, _includeContent?: boolean | undefined): proto.DidSaveTextDocumentParams { - throw new Error('Method not implemented.'); - } - public asWillSaveTextDocumentParams(_event: code.TextDocumentWillSaveEvent): proto.WillSaveTextDocumentParams { - throw new Error('Method not implemented.'); - } - public asTextDocumentPositionParams(_textDocument: code.TextDocument, _position: code.Position): proto.TextDocumentPositionParams { - return { - textDocument: { - uri: _textDocument.uri.fsPath - }, - position: { - line: _position.line, - character: _position.character - } - }; - } - public asCompletionParams(_textDocument: code.TextDocument, _position: code.Position, _context: code.CompletionContext): proto.CompletionParams { - const triggerKind = _context.triggerKind as number; - return { - textDocument: { - uri: _textDocument.uri.fsPath - }, - position: { - line: _position.line, - character: _position.character - }, - context: { - triggerCharacter: _context.triggerCharacter, - triggerKind: triggerKind as proto.CompletionTriggerKind - } - }; - } - public asWorkerPosition(_position: code.Position): proto.Position { - throw new Error('Method not implemented.'); - } - public asPosition(value: code.Position): proto.Position; - public asPosition(value: undefined): undefined; - public asPosition(value: null): null; - public asPosition(value: code.Position | null | undefined): proto.Position | null | undefined; - public asPosition(_value: any): any { - throw new Error('Method not implemented.'); - } - public asRange(value: code.Range): proto.Range; - public asRange(value: undefined): undefined; - public asRange(value: null): null; - public asRange(value: code.Range | null | undefined): proto.Range | null | undefined; - public asRange(_value: any): any { - throw new Error('Method not implemented.'); - } - public asDiagnosticSeverity(_value: code.DiagnosticSeverity): number { - throw new Error('Method not implemented.'); - } - public asDiagnostic(_item: code.Diagnostic): proto.Diagnostic { - throw new Error('Method not implemented.'); - } - public asDiagnostics(_items: code.Diagnostic[]): proto.Diagnostic[] { - throw new Error('Method not implemented.'); - } - public asCompletionItem(_item: code.CompletionItem): proto.CompletionItem { - throw new Error('Method not implemented.'); - } - public asTextEdit(_edit: code.TextEdit): proto.TextEdit { - throw new Error('Method not implemented.'); - } - public asReferenceParams(_textDocument: code.TextDocument, _position: code.Position, _options: { includeDeclaration: boolean }): proto.ReferenceParams { - throw new Error('Method not implemented.'); - } - public asCodeActionContext(_context: code.CodeActionContext): proto.CodeActionContext { - throw new Error('Method not implemented.'); - } - public asCommand(_item: code.Command): proto.Command { - throw new Error('Method not implemented.'); - } - public asCodeLens(_item: code.CodeLens): proto.CodeLens { - throw new Error('Method not implemented.'); - } - public asFormattingOptions(_item: code.FormattingOptions): proto.FormattingOptions { - throw new Error('Method not implemented.'); - } - public asDocumentSymbolParams(_textDocument: code.TextDocument): proto.DocumentSymbolParams { - throw new Error('Method not implemented.'); - } - public asCodeLensParams(_textDocument: code.TextDocument): proto.CodeLensParams { - throw new Error('Method not implemented.'); - } - public asDocumentLink(_item: code.DocumentLink): proto.DocumentLink { - throw new Error('Method not implemented.'); - } - public asDocumentLinkParams(_textDocument: code.TextDocument): proto.DocumentLinkParams { - throw new Error('Method not implemented.'); - } -} diff --git a/src/test/datascience/mockPythonService.ts b/src/test/datascience/mockPythonService.ts deleted file mode 100644 index 39293dce94a1..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<InterpreterInfomation> { - return Promise.resolve(this.interpreter); - } - - public getExecutablePath(): Promise<string> { - return Promise.resolve(this.interpreter.path); - } - - public isModuleInstalled(_moduleName: string): Promise<boolean> { - return Promise.resolve(false); - } - - public execObservable(args: string[], options: SpawnOptions): ObservableExecutionResult<string> { - return this.procService.execObservable(this.interpreter.path, args, options); - } - public execModuleObservable(moduleName: string, args: string[], options: SpawnOptions): ObservableExecutionResult<string> { - return this.procService.execObservable(this.interpreter.path, ['-m', moduleName, ...args], options); - } - public exec(args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { - return this.procService.exec(this.interpreter.path, args, options); - } - - public execModule(moduleName: string, args: string[], options: SpawnOptions): Promise<ExecutionResult<string>> { - return this.procService.exec(this.interpreter.path, ['-m', moduleName, ...args], options); - } - - public addExecResult(args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - this.procService.addExecResult(this.interpreter.path, args, result); - } - - public addExecModuleResult(moduleName: string, args: (string | RegExp)[], result: () => Promise<ExecutionResult<string>>) { - this.procService.addExecResult(this.interpreter.path, ['-m', moduleName, ...args], result); - } - - public addExecObservableResult(args: (string | RegExp)[], result: () => ObservableExecutionResult<string>) { - this.procService.addExecObservableResult(this.interpreter.path, args, result); - } - - public addExecModuleObservableResult(moduleName: string, args: (string | RegExp)[], result: () => ObservableExecutionResult<string>) { - this.procService.addExecObservableResult(this.interpreter.path, ['-m', moduleName, ...args], result); - } - - public setDelay(timeout: number | undefined) { - this.procService.setDelay(timeout); - } -} diff --git a/src/test/datascience/mockTextEditor.ts b/src/test/datascience/mockTextEditor.ts deleted file mode 100644 index 0a66e0598c6c..000000000000 --- a/src/test/datascience/mockTextEditor.ts +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { - DecorationOptions, - EndOfLine, - Position, - Range, - Selection, - SnippetString, - TextDocument, - TextEditor, - TextEditorDecorationType, - TextEditorEdit, - TextEditorOptions, - TextEditorRevealType, - ViewColumn -} from 'vscode'; - -import { noop } from '../../client/common/utils/misc'; -import { MockDocument } from './mockDocument'; -import { MockDocumentManager } from './mockDocumentManager'; - -class MockEditorEdit implements TextEditorEdit { - - constructor(private _documentManager: MockDocumentManager, private _document: MockDocument) { - } - - public replace(location: Selection | Range | Position, value: string): void { - this._documentManager.changeDocument(this._document.fileName, [{ - range: location as Range, - newText: value - }]); - } - - public insert(location: Position, value: string): void { - this._documentManager.changeDocument(this._document.fileName, [{ - range: new Range(location, location), - newText: value - }]); - } - public delete(_location: Selection | Range): void { - throw new Error('Method not implemented.'); - } - public setEndOfLine(_endOfLine: EndOfLine): void { - throw new Error('Method not implemented.'); - } -} - -export class MockEditor implements TextEditor { - public selection: Selection; - public selections: Selection[] = []; - private _revealCallback: () => void; - - constructor(private _documentManager: MockDocumentManager, private _document: MockDocument) { - this.selection = new Selection(0, 0, 0, 0); - this._revealCallback = noop; - } - - public get document(): TextDocument { - return this._document; - } - public get visibleRanges(): Range[] { - return []; - } - public get options(): TextEditorOptions { - return { - }; - } - public get viewColumn(): ViewColumn | undefined { - return undefined; - } - public edit(callback: (editBuilder: TextEditorEdit) => void, _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined): Thenable<boolean> { - return new Promise(r => { - const editor = new MockEditorEdit(this._documentManager, this._document); - callback(editor); - r(true); - }); - } - public insertSnippet(_snippet: SnippetString, _location?: Range | Position | Range[] | Position[] | undefined, _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined): Thenable<boolean> { - throw new Error('Method not implemented.'); - } - public setDecorations(_decorationType: TextEditorDecorationType, _rangesOrOptions: Range[] | DecorationOptions[]): void { - throw new Error('Method not implemented.'); - } - public revealRange(_range: Range, _revealType?: TextEditorRevealType | undefined): void { - this._revealCallback(); - } - public show(_column?: ViewColumn | undefined): void { - throw new Error('Method not implemented.'); - } - public hide(): void { - throw new Error('Method not implemented.'); - } - - public setRevealCallback(callback: () => void) { - this._revealCallback = callback; - } -} diff --git a/src/test/datascience/notebook.functional.test.ts b/src/test/datascience/notebook.functional.test.ts deleted file mode 100644 index c49892ae5eac..000000000000 --- a/src/test/datascience/notebook.functional.test.ts +++ /dev/null @@ -1,1039 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import { nbformat } from '@jupyterlab/coreutils'; -import { assert } from 'chai'; -import { ChildProcess } from 'child_process'; -import * as fs from 'fs-extra'; -import { injectable } from 'inversify'; -import * as os from 'os'; -import * as path from 'path'; -import { Readable, Writable } from 'stream'; -import * as uuid from 'uuid/v4'; -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 { traceError, traceInfo } from '../../client/common/logger'; -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 { concatMultilineString } from '../../client/datascience/common'; -import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; -import { JupyterKernelPromiseFailedError } from '../../client/datascience/jupyter/jupyterKernelPromiseFailedError'; -import { - CellState, - ICell, - IConnection, - IJupyterExecution, - IJupyterKernelSpec, - INotebookExecutionLogger, - INotebookExporter, - INotebookImporter, - INotebookServer, - InterruptResult -} from '../../client/datascience/types'; -import { - IInterpreterService, - IKnownSearchPathsForInterpreters, - PythonInterpreter -} from '../../client/interpreter/contracts'; -import { ICellViewModel } from '../../datascience-ui/history-react/cell'; -import { generateTestState } from '../../datascience-ui/history-react/mainPanelState'; -import { asyncDump } from '../common/asyncDump'; -import { sleep } from '../core'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; - -// tslint:disable:no-any no-multiline-string max-func-body-length no-console max-classes-per-file trailing-comma -suite('DataScience notebook tests', () => { - const disposables: Disposable[] = []; - let jupyterExecution: IJupyterExecution; - let processFactory: IProcessServiceFactory; - let ioc: DataScienceIocContainer; - let modifiedConfig = false; - - setup(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(IJupyterExecution); - processFactory = ioc.serviceManager.get<IProcessServiceFactory>(IProcessServiceFactory); - }); - - teardown(async () => { - try { - if (modifiedConfig) { - traceInfo('Attempting to put jupyter default config back'); - 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 }); - } - } - traceInfo('Shutting down after test.'); - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < disposables.length; i += 1) { - const disposable = disposables[i]; - if (disposable) { - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - } - await ioc.dispose(); - traceInfo('Shutdown after test complete.'); - } catch (e) { - traceError(e); - } - }); - - suiteTeardown(() => { - asyncDump(); - }); - - function escapePath(p: string) { - return p.replace(/\\/g, '\\\\'); - } - - function srcDirectory() { - return path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience'); - } - - function extractDataOutput(cell: ICell): any { - assert.equal(cell.data.cell_type, 'code', `Wrong type of cell returned`); - const codeCell = cell.data as nbformat.ICodeCell; - if (codeCell.outputs.length > 0) { - assert.equal(codeCell.outputs.length, 1, 'Cell length not correct'); - const data = codeCell.outputs[0].data; - const error = codeCell.outputs[0].evalue; - if (error) { - assert.fail(`Unexpected error: ${error}`); - } - assert.ok(data, `No data object on the cell`); - if (data) { // For linter - assert.ok(data.hasOwnProperty('text/plain'), `Cell mime type not correct`); - assert.ok((data as any)['text/plain'], `Cell mime type not correct`); - return (data as any)['text/plain']; - } - } - } - - async function verifySimple(jupyterServer: INotebookServer | undefined, code: string, expectedValue: any): Promise<void> { - const cells = await jupyterServer!.execute(code, path.join(srcDirectory(), 'foo.py'), 2, uuid()); - assert.equal(cells.length, 1, `Wrong number of cells returned`); - const data = extractDataOutput(cells[0]); - assert.equal(data, expectedValue, 'Cell value does not match'); - } - - async function verifyError(jupyterServer: INotebookServer | undefined, code: string, errorString: string): Promise<void> { - const cells = await jupyterServer!.execute(code, path.join(srcDirectory(), 'foo.py'), 2, uuid()); - assert.equal(cells.length, 1, `Wrong number of cells returned`); - assert.equal(cells[0].data.cell_type, 'code', `Wrong type of cell returned`); - const cell = cells[0].data as nbformat.ICodeCell; - assert.equal(cell.outputs.length, 1, `Cell length not correct`); - const error = cell.outputs[0].evalue; - if (error) { - assert.ok(error, 'Error not found when expected'); - assert.equal(error, errorString, 'Unexpected error found'); - } - } - - async function verifyCell(jupyterServer: INotebookServer | undefined, index: number, code: string, mimeType: string, cellType: string, verifyValue: (data: any) => void): Promise<void> { - // Verify results of an execute - const cells = await jupyterServer!.execute(code, path.join(srcDirectory(), 'foo.py'), 2, uuid()); - assert.equal(cells.length, 1, `${index}: Wrong number of cells returned`); - if (cellType === 'code') { - assert.equal(cells[0].data.cell_type, cellType, `${index}: Wrong type of cell returned`); - const cell = cells[0].data as nbformat.ICodeCell; - assert.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; - const text = cell.outputs[0].text; - assert.ok(data || text, `${index}: No data object on the cell for ${code}`); - if (data) { // For linter - assert.ok(data.hasOwnProperty(mimeType), `${index}: Cell mime type not correct for ${JSON.stringify(data)}`); - assert.ok((data as any)[mimeType], `${index}: Cell mime type not correct`); - verifyValue((data as any)[mimeType]); - } - if (text) { - verifyValue(text); - } - } else if (cellType === 'markdown') { - assert.equal(cells[0].data.cell_type, cellType, `${index}: Wrong type of cell returned`); - const cell = cells[0].data as nbformat.IMarkdownCell; - const outputSource = concatMultilineString(cell.source); - verifyValue(outputSource); - } else if (cellType === 'error') { - const cell = cells[0].data as nbformat.ICodeCell; - assert.equal(cell.outputs.length, 1, `${index}: Cell length not correct`); - const error = cell.outputs[0].evalue; - assert.ok(error, 'Error not found when expected'); - verifyValue(error); - } - } - - function testMimeTypes(types: { markdownRegEx: string | undefined; code: string; mimeType: string; result: any; cellType: string; verifyValue(data: any): void }[]) { - runTest('MimeTypes', async () => { - // Prefill with the output (This is only necessary for mocking) - types.forEach(t => { - addMockData(t.code, t.result, t.mimeType, t.cellType); - }); - - // Test all mime types together so we don't have to startup and shutdown between - // each - const server = await createNotebookServer(true); - if (server) { - for (let i = 0; i < types.length; i += 1) { - const markdownRegex = types[i].markdownRegEx ? types[i].markdownRegEx : ''; - ioc.getSettings().datascience.markdownRegularExpression = markdownRegex!; - await verifyCell(server, i, types[i].code, types[i].mimeType, types[i].cellType, types[i].verifyValue); - } - } - }); - } - - function runTest(name: string, func: () => Promise<void>, _notebookProc?: ChildProcess) { - 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, usingDarkTheme?: boolean, purpose?: string): Promise<INotebookServer | undefined> { - // 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({ usingDarkTheme, useDefaultConfig, workingDir: testDir, purpose: purpose ? purpose : '1' }); - 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 Self Certs', async () => { - const python = await getNotebookCapableInterpreter(); - const procService = await processFactory.create(); - - // We will only connect if we allow for self signed cert connections - ioc.getSettings().datascience.allowUnauthorizedRemoteConnection = true; - - if (procService && python) { - const connectionFound = createDeferred(); - const configFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'serverConfigFiles', 'selfCert.py'); - const pemFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'serverConfigFiles', 'jcert.pem'); - const keyFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'serverConfigFiles', 'jkey.key'); - - const exeResult = procService.execObservable(python.path, ['-m', 'jupyter', 'notebook', `--config=${configFile}`, `--certfile=${pemFile}`, `--keyfile=${keyFile}`], { env: process.env, throwOnStdErr: false }); - disposables.push(exeResult); - - exeResult.out.subscribe((output: Output<string>) => { - const connectionURL = getIPConnectionInfo(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, useDefaultConfig: true, purpose: '' }); - if (!server) { - assert.fail('Failed to connect to remote self cert server'); - } else { - await verifySimple(server, `a=1${os.EOL}a`, 1); - } - // Have to dispose here otherwise the process may exit before hand and mess up cleanup. - await server!.dispose(); - } - }); - - runTest('Remote Password', async () => { - const python = await getNotebookCapableInterpreter(); - const procService = await processFactory.create(); - - if (procService && python) { - const connectionFound = createDeferred(); - const configFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'serverConfigFiles', 'remotePassword.py'); - const exeResult = procService.execObservable(python.path, ['-m', 'jupyter', 'notebook', `--config=${configFile}`], { env: process.env, throwOnStdErr: false }); - disposables.push(exeResult); - - exeResult.out.subscribe((output: Output<string>) => { - const connectionURL = getIPConnectionInfo(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, useDefaultConfig: true, purpose: '' }); - if (!server) { - assert.fail('Failed to connect to remote password server'); - } else { - await verifySimple(server, `a=1${os.EOL}a`, 1); - } - // Have to dispose here otherwise the process may exit before hand and mess up cleanup. - await server!.dispose(); - } - }); - - runTest('Remote', async () => { - const python = await getNotebookCapableInterpreter(); - const procService = await processFactory.create(); - - if (procService && python) { - const connectionFound = createDeferred(); - const configFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'datascience', 'serverConfigFiles', 'remoteToken.py'); - const exeResult = procService.execObservable(python.path, ['-m', 'jupyter', 'notebook', `--config=${configFile}`], { env: process.env, throwOnStdErr: false }); - disposables.push(exeResult); - - exeResult.out.subscribe((output: Output<string>) => { - 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, useDefaultConfig: true, purpose: '' }); - if (!server) { - assert.fail('Failed to connect to remote server'); - } else { - await verifySimple(server, `a=1${os.EOL}a`, 1); - } - - // Have to dispose here otherwise the process may exit before hand and mess up cleanup. - await server!.dispose(); - } - }); - - runTest('Creation', async () => { - await createNotebookServer(true); - }); - - // IP = * format is a bit different from localhost format - function getIPConnectionInfo(output: string): string | undefined { - // String format: http://(NAME or IP):PORT/ - const nameAndPortRegEx = /(https?):\/\/\(([^\s]*) or [0-9.]*\):([0-9]*)\/(?:\?token=)?([a-zA-Z0-9]*)?/; - - const urlMatch = nameAndPortRegEx.exec(output); - if (urlMatch && !urlMatch[4]) { - return `${urlMatch[1]}://${urlMatch[2]}:${urlMatch[3]}/`; - } else if (urlMatch && urlMatch.length === 5) { - return `${urlMatch[1]}://${urlMatch[2]}:${urlMatch[3]}/?token=${urlMatch[4]}`; - } - - return undefined; - } - - 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 JupyterExecutionFactory { - public isNotebookSupported = (): Promise<boolean> => { - return Promise.resolve(false); - } - } - ioc.serviceManager.rebind<IJupyterExecution>(IJupyterExecution, FailedProcess); - jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(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<boolean> { - return Promise.resolve(true); - } - public onDidChangeInterpreter(_listener: (e: void) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - public onDidChangeInterpreterInformation(_listener: (e: PythonInterpreter) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - public getInterpreters(_resource?: Uri): Promise<PythonInterpreter[]> { - return Promise.resolve([]); - } - public autoSetInterpreter(): Promise<void> { - throw new Error('Method not implemented'); - } - public getActiveInterpreter(_resource?: Uri): Promise<PythonInterpreter | undefined> { - return Promise.resolve(undefined); - } - public getInterpreterDetails(_pythonPath: string, _resoure?: Uri): Promise<PythonInterpreter> { - throw new Error('Method not implemented'); - } - public refresh(_resource: Uri): Promise<void> { - throw new Error('Method not implemented'); - } - public initialize(): void { - throw new Error('Method not implemented'); - } - public getDisplayName(_interpreter: Partial<PythonInterpreter>): Promise<string> { - throw new Error('Method not implemented'); - } - public shouldAutoSetInterpreter(): Promise<boolean> { - throw new Error('Method not implemented'); - } - } - class EmptyPathService implements IKnownSearchPathsForInterpreters { - public getSearchPaths(): string[] { - return []; - } - } - ioc.serviceManager.rebind<IInterpreterService>(IInterpreterService, EmptyInterpreterService); - ioc.serviceManager.rebind<IKnownSearchPathsForInterpreters>(IKnownSearchPathsForInterpreters, EmptyPathService); - jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(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>(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) { - // tslint:disable-next-line:no-string-literal - const nbcells = notebook['cells']; - if (nbcells) { - // tslint:disable-next-line:no-string-literal - const firstCellText: string = (nbcells as any)[0]['source'] as string; - assert.ok(firstCellText.includes('os.chdir')); - } - } - - // Save to a temp file - const fileSystem = ioc.serviceManager.get<IFileSystem>(IFileSystem); - const importer = ioc.serviceManager.get<INotebookImporter>(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 have a single chdir in our results - const first = results.indexOf('os.chdir'); - assert.ok(first >= 0, 'No os.chdir in import'); - const second = results.indexOf('os.chdir', first + 1); - assert.equal(second, -1, 'More than one chdir in the import. It should be skipped'); - - // Make sure we have a cell in our results - assert.ok(/#\s*%%/.test(results), 'No cells in returned import'); - } finally { - importer.dispose(); - temp.dispose(); - } - }); - - 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(10000); - - console.log('Restarting kernel'); - try { - await server!.restartKernel(10000); - - console.log('Waiting for idle'); - await server!.waitForIdle(10000); - - console.log('Verifying restart'); - await verifyError(server, 'a', `name 'a' is not defined`); - - } catch (exc) { - assert.ok(exc instanceof JupyterKernelPromiseFailedError, 'Restarting did not timeout correctly'); - } - - }); - - class TaggedCancellationTokenSource extends CancellationTokenSource { - public tag: string; - constructor(tag: string) { - super(); - this.tag = tag; - } - } - - async function testCancelableCall<T>(method: (t: CancellationToken) => Promise<T>, messageFormat: string, timeout: number): Promise<boolean> { - const tokenSource = new TaggedCancellationTokenSource(messageFormat.format(timeout.toString())); - let canceled = false; - const disp = setTimeout((_s) => { - canceled = true; - tokenSource.cancel(); - }, timeout, tokenSource.tag); - - try { - // tslint:disable-next-line:no-string-literal - (tokenSource.token as any)['tag'] = messageFormat.format(timeout.toString()); - await method(tokenSource.token); - // We might get here before the cancel finishes - assert.ok(!canceled, 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<T>(method: (t: CancellationToken) => Promise<T>, messageFormat: string, short?: boolean): Promise<boolean> { - const timeouts = short ? [10, 20, 30, 100] : [100, 200, 300, 1000]; - // tslint:disable-next-line:prefer-for-of - for (let i = 0; i < timeouts.length; i += 1) { - await testCancelableCall(method, messageFormat, timeouts[i]); - } - - return true; - } - - runTest('Cancel execution', async () => { - 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, t), 'Cancel did not cancel start after {0}ms')); - - if (ioc.mockJupyter) { - ioc.mockJupyter.setProcessDelay(undefined); - } - - // Make sure doing normal start still works - const nonCancelSource = new CancellationTokenSource(); - const server = await jupyterExecution.connectToNotebookServer(undefined, nonCancelSource.token); - 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('/usr/bin/test3/python'); - - assert.ok(await testCancelableMethod((t: CancellationToken) => jupyterExecution.getUsableJupyterPython(t), 'Cancel did not cancel getusable after {0}ms', true)); - assert.ok(await testCancelableMethod((t: CancellationToken) => jupyterExecution.isNotebookSupported(t), 'Cancel did not cancel isNotebook after {0}ms', true)); - assert.ok(await testCancelableMethod((t: CancellationToken) => jupyterExecution.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<InterruptResult> { - let interrupted = false; - let finishedBefore = false; - const finishedPromise = createDeferred(); - let error; - const observable = server!.executeObservable(code, 'foo.py', 0, uuid(), false); - observable.subscribe(c => { - if (c.length > 0 && c[0].state === CellState.error) { - finishedBefore = !interrupted; - finishedPromise.resolve(); - } - if (c.length > 0 && c[0].state === CellState.finished) { - finishedBefore = !interrupted; - finishedPromise.resolve(); - } - }, (err) => { error = err; finishedPromise.resolve(); }, () => finishedPromise.resolve()); - - // Then interrupt - interrupted = true; - const result = await 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 - await interruptExecute(server, returnable, 1000, 1000); - - // Try again with something that doesn't return. However it should finish before - // we get to our own sleep. Note: We need the print so that the test knows something happened. - await interruptExecute(server, fourSecondSleep, 7000, 7000); - - // Try again with something that doesn't return. Make sure it times out - await interruptExecute(server, fourSecondSleep, 100, 7000); - - // The tough one, somethign that causes a kernel reset. - await interruptExecute(server, kill, 1000, 1000); - }); - - testMimeTypes( - [ - { - markdownRegEx: undefined, - code: - `a=1 -a`, - mimeType: 'text/plain', - cellType: 'code', - result: 1, - verifyValue: (d) => assert.equal(d, 1, 'Plain text invalid') - }, - { - markdownRegEx: undefined, - code: - `import pandas as pd -df = pd.read("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") -df.head()`, - mimeType: 'text/html', - result: `pd has no attribute 'read'`, - cellType: 'error', - // tslint:disable-next-line:quotemark - verifyValue: (d) => assert.ok((d as string).includes("has no attribute 'read'"), 'Unexpected error result') - }, - { - markdownRegEx: undefined, - code: - `import pandas as pd -df = pd.read_csv("${escapePath(path.join(srcDirectory(), 'DefaultSalesReport.csv'))}") -df.head()`, - mimeType: 'text/html', - result: `<td>A table</td>`, - cellType: 'code', - verifyValue: (d) => assert.ok(d.toString().includes('</td>'), 'Table not found') - }, - { - markdownRegEx: undefined, - code: - `#%% [markdown]# -# #HEADER`, - mimeType: 'text/plain', - cellType: 'markdown', - result: '#HEADER', - verifyValue: (d) => assert.equal(d, '#HEADER', 'Markdown incorrect') - }, - { - markdownRegEx: '\\s*#\\s*<markdowncell>', - code: - `# <markdowncell> -# #HEADER`, - mimeType: 'text/plain', - cellType: 'markdown', - result: '#HEADER', - verifyValue: (d) => assert.equal(d, '#HEADER', 'Markdown incorrect') - }, - { - // Test relative directories too. - markdownRegEx: undefined, - code: - `import pandas as pd -df = pd.read_csv("./DefaultSalesReport.csv") -df.head()`, - mimeType: 'text/html', - cellType: 'code', - result: `<td>A table</td>`, - verifyValue: (d) => assert.ok(d.toString().includes('</td>'), 'Table not found') - }, - { - // Important to test as multiline cell magics only work if they are the first item in the cell - markdownRegEx: undefined, - code: - `#%% -%%bash -echo 'hello'`, - mimeType: 'text/plain', - cellType: 'code', - result: 'hello', - verifyValue: (d) => assert.ok(d.includes('hello') || d.includes('bash'), `Multiline cell magic incorrect - ${d}`) - }, - { - // Test shell command should work on PC / Mac / Linux - markdownRegEx: undefined, - code: - `!echo world`, - mimeType: 'text/plain', - cellType: 'code', - result: 'world', - verifyValue: (d) => assert.ok(d.includes('world'), 'Cell command incorrect') - }, - { - // Plotly - markdownRegEx: undefined, - code: - `import matplotlib.pyplot as plt -import matplotlib as mpl -import numpy as np -import pandas as pd -x = np.linspace(0, 20, 100) -plt.plot(x, np.sin(x)) -plt.show()`, - result: `00000`, - mimeType: 'image/svg+xml', - cellType: 'code', - verifyValue: (_d) => { return; } - } - ] - ); - - async function getNotebookCapableInterpreter(): Promise<PythonInterpreter | undefined> { - const is = ioc.serviceContainer.get<IInterpreterService>(IInterpreterService); - const list = await is.getInterpreters(); - const procService = await processFactory.create(); - if (procService) { - // tslint:disable-next-line:prefer-for-of - 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>(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 JupyterExecutionFactory { - protected async getMatchingKernelSpec(_connection?: IConnection, _cancelToken?: CancellationToken): Promise<IJupyterKernelSpec | undefined> { - return Promise.resolve(undefined); - } - } - ioc.serviceManager.rebind<IJupyterExecution>(IJupyterExecution, FailedKernelSpec); - jupyterExecution = ioc.serviceManager.get<IJupyterExecution>(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); - } - }); - - runTest('Server cache working', async () => { - console.log('Staring server cache test'); - const s1 = await createNotebookServer(true, false, false, 'same'); - console.log('Creating s2'); - const s2 = await createNotebookServer(true, false, false, 'same'); - console.log('Testing s1 and s2, creating s3'); - assert.ok(s1 === s2, 'Two servers not the same when they should be'); - const s3 = await createNotebookServer(false, false, false, 'same'); - console.log('Testing s1 and s3, creating s4'); - assert.ok(s1 !== s3, 'Different config should create different server'); - const s4 = await createNotebookServer(true, false, false, 'different'); - console.log('Testing s1 and s4, creating s5'); - assert.ok(s1 !== s4, 'Different purpose should create different server'); - const s5 = await createNotebookServer(true, false, true, 'different'); - assert.ok(s4 === s5, 'Dark theme should be same server'); - console.log('Disposing of all'); - await s1!.dispose(); - await s3!.dispose(); - await s4!.dispose(); - }); - - class DyingProcess implements ChildProcess { - public stdin: Writable; - public stdout: Readable; - public stderr: Readable; - public stdio: [Writable, Readable, Readable]; - public killed: boolean = false; - public pid: number = 1; - public connected: boolean = true; - constructor(private timeout: number) { - noop(); - this.stderr = this.stdout = new Readable(); - this.stdin = new Writable(); - this.stdio = [this.stdin, this.stdout, this.stderr]; - } - public kill(_signal?: string): void { - throw new Error('Method not implemented.'); - } - public send(_message: any, _sendHandle?: any, _options?: any, _callback?: any): any { - throw new Error('Method not implemented.'); - } - public disconnect(): void { - throw new Error('Method not implemented.'); - } - public unref(): void { - throw new Error('Method not implemented.'); - } - public ref(): void { - throw new Error('Method not implemented.'); - } - public addListener(_event: any, _listener: any): this { - throw new Error('Method not implemented.'); - } - public emit(_event: any, _message?: any, _sendHandle?: any, ..._rest: any[]): any { - throw new Error('Method not implemented.'); - } - public on(event: any, listener: any): this { - if (event === 'exit') { - setTimeout(() => listener(2), this.timeout); - } - return this; - } - public once(_event: any, _listener: any): this { - throw new Error('Method not implemented.'); - } - public prependListener(_event: any, _listener: any): this { - throw new Error('Method not implemented.'); - } - public prependOnceListener(_event: any, _listener: any): this { - throw new Error('Method not implemented.'); - } - public removeListener(_event: string | symbol, _listener: (...args: any[]) => void): this { - return this; - } - public removeAllListeners(_event?: string | symbol): this { - throw new Error('Method not implemented.'); - } - public setMaxListeners(_n: number): this { - throw new Error('Method not implemented.'); - } - public getMaxListeners(): number { - throw new Error('Method not implemented.'); - } - public listeners(_event: string | symbol): Function[] { - throw new Error('Method not implemented.'); - } - public rawListeners(_event: string | symbol): Function[] { - throw new Error('Method not implemented.'); - } - public eventNames(): (string | symbol)[] { - throw new Error('Method not implemented.'); - } - public listenerCount(_type: string | symbol): number { - throw new Error('Method not implemented.'); - } - } - - runTest('Server death', async () => { - if (ioc.mockJupyter) { - // Only run this test for mocks. We need to mock the server dying. - addMockData(`a=1${os.EOL}a`, 1); - const server = await createNotebookServer(true); - assert.ok(server, 'Server died before running'); - - // Sleep for 100 ms so it crashes - await sleep(100); - - try { - await verifySimple(server, `a=1${os.EOL}a`, 1); - assert.ok(false, 'Exception should have been thrown'); - } catch { - noop(); - } - - } - }, new DyingProcess(100)); - - runTest('Execution logging', async () => { - const cellInputs: string[] = []; - const outputs: string[] = []; - @injectable() - class Logger implements INotebookExecutionLogger { - public preExecute(cell: ICell, _silent: boolean): void { - cellInputs.push(concatMultilineString(cell.data.source)); - } - public postExecute(cellOrError: ICell | Error, _silent: boolean): void { - if (!(cellOrError instanceof Error)) { - const cell = cellOrError as ICell; - outputs.push(extractDataOutput(cell)); - } - } - } - ioc.serviceManager.add<INotebookExecutionLogger>(INotebookExecutionLogger, Logger); - addMockData(`a=1${os.EOL}a`, 1); - const server = await createNotebookServer(true); - assert.ok(server, 'Server not created in logging case'); - await server!.execute(`a=1${os.EOL}a`, path.join(srcDirectory(), 'foo.py'), 2, uuid()); - assert.equal(cellInputs.length, 3, 'Not enough cell inputs'); - assert.equal(outputs.length, 3, 'Not enough cell inputs'); - assert.equal(cellInputs[2], 'a=1\na', 'Cell inputs not captured'); - assert.equal(outputs[2], '1', 'Cell outputs not captured'); - }); - -}); diff --git a/src/test/datascience/plotViewer.functional.test.tsx b/src/test/datascience/plotViewer.functional.test.tsx deleted file mode 100644 index e14ae5205fa7..000000000000 --- a/src/test/datascience/plotViewer.functional.test.tsx +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string -import '../../client/common/extensions'; - -import * as assert from 'assert'; -import { mount, ReactWrapper } from 'enzyme'; -import { parse } from 'node-html-parser'; -import * as React from 'react'; -import { Disposable } from 'vscode'; - -import { IPlotViewerProvider } from '../../client/datascience/types'; -import { MainPanel } from '../../datascience-ui/plot/mainPanel'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { waitForUpdate } from './reactHelpers'; - -// import { asyncDump } from '../common/asyncDump'; -suite('DataScience PlotViewer tests', () => { - const disposables: Disposable[] = []; - let plotViewerProvider: IPlotViewerProvider; - let ioc: DataScienceIocContainer; - - setup(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - }); - - function mountWebView(): ReactWrapper<any, Readonly<{}>, React.Component> { - // Setup our webview panel - ioc.createWebView(() => mount(<MainPanel skipDefault={true} baseTheme={'vscode-light'} testMode={true}/>)); - - // Make sure the plot viewer provider and execution factory in the container is created (the extension does this on startup in the extension) - plotViewerProvider = ioc.get<IPlotViewerProvider>(IPlotViewerProvider); - - return ioc.wrapper!; - } - - teardown(async () => { - for (const disposable of disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - await ioc.dispose(); - delete (global as any).ascquireVsCodeApi; - }); - - suiteTeardown(() => { - // asyncDump(); - }); - - async function waitForPlot(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, svg: string): Promise<void> { - - // Get a render promise with the expected number of renders - const renderPromise = waitForUpdate(wrapper, MainPanel, 1); - - // Call our function to add a plot - await plotViewerProvider.showPlot(svg); - - // Wait for all of the renders to go through - await renderPromise; - } - - // tslint:disable-next-line:no-any - function runMountedTest(name: string, testFunc: (wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) => Promise<void>) { - test(name, async () => { - const wrapper = mountWebView(); - try { - await testFunc(wrapper); - } finally { - // Make sure to unmount the wrapper or it will interfere with other tests - if (wrapper && wrapper.length) { - wrapper.unmount(); - } - } - }); - } - - function verifySvgValue(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, svg: string) { - const html = wrapper.html(); - const root = parse(html) as any; - const drawnSvgs = root.querySelectorAll('.injected-svg') as SVGSVGElement[]; - assert.equal(drawnSvgs.length, 1, 'Injected svg not found'); - const expectedSvg = (parse(svg) as any) as SVGSVGElement; - const drawnSvg = drawnSvgs[0] as SVGSVGElement; - const drawnPaths = drawnSvg.querySelectorAll('path'); - const expectedPaths = expectedSvg.querySelectorAll('path'); - assert.equal(drawnPaths.length, expectedPaths.length, 'Paths do not match'); - assert.equal(drawnPaths[0].innerHTML, expectedPaths[0].innerHTML, 'Path values do not match'); - } - - const cancelSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>Cancel_16xMD</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M10.475,8l3.469,3.47L11.47,13.944,8,10.475,4.53,13.944,2.056,11.47,5.525,8,2.056,4.53,4.53,2.056,8,5.525l3.47-3.469L13.944,4.53Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M9.061,8l3.469,3.47-1.06,1.06L8,9.061,4.53,12.53,3.47,11.47,6.939,8,3.47,4.53,4.53,3.47,8,6.939,11.47,3.47l1.06,1.06Z"/></g></svg>`; - - runMountedTest('Simple SVG', async (wrapper) => { - await waitForPlot(wrapper, cancelSvg); - verifySvgValue(wrapper, cancelSvg); - }); - - runMountedTest('Export', async (_wrapper) => { - // Export isn't runnable inside of JSDOM. So this test does nothing. - }); - -}); diff --git a/src/test/datascience/reactHelpers.ts b/src/test/datascience/reactHelpers.ts deleted file mode 100644 index 4efae57173d2..000000000000 --- a/src/test/datascience/reactHelpers.ts +++ /dev/null @@ -1,585 +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 { noop } from '../../client/common/utils/misc'; - -// Custom module loader so we can skip loading the 'canvas' module which won't load -// inside of vscode -// tslint:disable:no-var-requires no-require-imports no-any no-function-expression -const Module = require('module'); - -(function () { - const origRequire = Module.prototype.require; - const _require = (context: any, filepath: any) => { - return origRequire.call(context, filepath); - }; - Module.prototype.require = function (filepath: string) { - if (filepath === 'canvas') { - try { - // Make sure we aren't inside of vscode. The nodejs version of Canvas won't match. At least sometimes. - if (require('vscode')) { - return ''; - } - } catch { - // This should happen when not inside vscode. - noop(); - } - } - // tslint:disable-next-line:no-invalid-this - return _require(this, filepath); - }; -})(); - -// tslint:disable:no-string-literal no-any object-literal-key-quotes max-func-body-length member-ordering -// tslint:disable: no-require-imports no-var-requires - -// Monkey patch the stylesheet impl from jsdom before loading jsdom. -// This is necessary to get slickgrid to work. -const utils = require('jsdom/lib/jsdom/living/generated/utils'); -const ssExports = require('jsdom/lib/jsdom/living/helpers/stylesheets'); -if (ssExports && ssExports.createStylesheet) { - const orig = ssExports.createStylesheet; - ssExports.createStylesheet = (sheetText: any, elementImpl: any, baseURL: any) => { - // Call the original. - orig(sheetText, elementImpl, baseURL); - - // Then pull out the style sheet and add some properties. See the discussion here - // https://github.com/jsdom/jsdom/issues/992 - if (elementImpl.sheet) { - elementImpl.sheet.href = baseURL; - elementImpl.sheet.ownerNode = utils.wrapperForImpl(elementImpl); - } - }; -} - -import { DOMWindow, JSDOM } from 'jsdom'; -import * as React from 'react'; - -class MockCanvas implements CanvasRenderingContext2D { - public canvas!: HTMLCanvasElement; - public restore(): void { - throw new Error('Method not implemented.'); - } - public save(): void { - throw new Error('Method not implemented.'); - } - public getTransform(): DOMMatrix { - throw new Error('Method not implemented.'); - } - public resetTransform(): void { - throw new Error('Method not implemented.'); - } - public rotate(_angle: number): void { - throw new Error('Method not implemented.'); - } - public scale(_x: number, _y: number): void { - throw new Error('Method not implemented.'); - } - public setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void; - public setTransform(transform?: DOMMatrix2DInit | undefined): void; - public setTransform(_a?: any, _b?: any, _c?: any, _d?: any, _e?: any, _f?: any) { - throw new Error('Method not implemented.'); - } - public transform(_a: number, _b: number, _c: number, _d: number, _e: number, _f: number): void { - throw new Error('Method not implemented.'); - } - public translate(_x: number, _y: number): void { - throw new Error('Method not implemented.'); - } - public globalAlpha!: number; - public globalCompositeOperation!: string; - public imageSmoothingEnabled!: boolean; - public imageSmoothingQuality!: ImageSmoothingQuality; - public fillStyle!: string | CanvasGradient | CanvasPattern; - public strokeStyle!: string | CanvasGradient | CanvasPattern; - public createLinearGradient(_x0: number, _y0: number, _x1: number, _y1: number): CanvasGradient { - throw new Error('Method not implemented.'); - } - public createPattern(_image: CanvasImageSource, _repetition: string): CanvasPattern | null { - throw new Error('Method not implemented.'); - } - public createRadialGradient(_x0: number, _y0: number, _r0: number, _x1: number, _y1: number, _r1: number): CanvasGradient { - throw new Error('Method not implemented.'); - } - public shadowBlur!: number; - public shadowColor!: string; - public shadowOffsetX!: number; - public shadowOffsetY!: number; - public filter!: string; - public clearRect(_x: number, _y: number, _w: number, _h: number): void { - throw new Error('Method not implemented.'); - } - public fillRect(_x: number, _y: number, _w: number, _h: number): void { - throw new Error('Method not implemented.'); - } - public strokeRect(_x: number, _y: number, _w: number, _h: number): void { - throw new Error('Method not implemented.'); - } - public beginPath(): void { - throw new Error('Method not implemented.'); - } - public clip(fillRule?: 'nonzero' | 'evenodd' | undefined): void; - public clip(path: Path2D, fillRule?: 'nonzero' | 'evenodd' | undefined): void; - public clip(_path?: any, _fillRule?: any) { - throw new Error('Method not implemented.'); - } - public fill(fillRule?: 'nonzero' | 'evenodd' | undefined): void; - public fill(path: Path2D, fillRule?: 'nonzero' | 'evenodd' | undefined): void; - public fill(_path?: any, _fillRule?: any) { - throw new Error('Method not implemented.'); - } - public isPointInPath(x: number, y: number, fillRule?: 'nonzero' | 'evenodd' | undefined): boolean; - public isPointInPath(path: Path2D, x: number, y: number, fillRule?: 'nonzero' | 'evenodd' | undefined): boolean; - public isPointInPath(_path: any, _x: any, _y?: any, _fillRule?: any): boolean { - throw new Error('Method not implemented.'); - } - public isPointInStroke(x: number, y: number): boolean; - public isPointInStroke(path: Path2D, x: number, y: number): boolean; - public isPointInStroke(_path: any, _x: any, _y?: any): boolean { - throw new Error('Method not implemented.'); - } - public stroke(): void; - // tslint:disable-next-line: unified-signatures - public stroke(path: Path2D): void; - public stroke(_path?: any) { - throw new Error('Method not implemented.'); - } - public drawFocusIfNeeded(element: Element): void; - public drawFocusIfNeeded(path: Path2D, element: Element): void; - public drawFocusIfNeeded(_path: any, _element?: any) { - throw new Error('Method not implemented.'); - } - public scrollPathIntoView(): void; - // tslint:disable-next-line: unified-signatures - public scrollPathIntoView(path: Path2D): void; - public scrollPathIntoView(_path?: any) { - throw new Error('Method not implemented.'); - } - public fillText(_text: string, _x: number, _y: number, _maxWidth?: number | undefined): void { - throw new Error('Method not implemented.'); - } - public measureText(_text: string): TextMetrics { - throw new Error('Method not implemented.'); - } - public strokeText(_text: string, _x: number, _y: number, _maxWidth?: number | undefined): void { - throw new Error('Method not implemented.'); - } - public drawImage(image: CanvasImageSource, dx: number, dy: number): void; - public drawImage(image: CanvasImageSource, dx: number, dy: number, dw: number, dh: number): void; - public drawImage(image: CanvasImageSource, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void; - public drawImage(_image: any, _sx: any, _sy: any, _sw?: any, _sh?: any, _dx?: any, _dy?: any, _dw?: any, _dh?: any) { - throw new Error('Method not implemented.'); - } - public createImageData(sw: number, sh: number): ImageData; - public createImageData(imagedata: ImageData): ImageData; - public createImageData(_sw: any, _sh?: any): ImageData { - throw new Error('Method not implemented.'); - } - public getImageData(_sx: number, _sy: number, _sw: number, _sh: number): ImageData { - throw new Error('Method not implemented.'); - } - public putImageData(imagedata: ImageData, dx: number, dy: number): void; - public putImageData(imagedata: ImageData, dx: number, dy: number, dirtyX: number, dirtyY: number, dirtyWidth: number, dirtyHeight: number): void; - public putImageData(_imagedata: any, _dx: any, _dy: any, _dirtyX?: any, _dirtyY?: any, _dirtyWidth?: any, _dirtyHeight?: any) { - throw new Error('Method not implemented.'); - } - public lineCap!: CanvasLineCap; - public lineDashOffset!: number; - public lineJoin!: CanvasLineJoin; - public lineWidth!: number; - public miterLimit!: number; - public getLineDash(): number[] { - throw new Error('Method not implemented.'); - } - public setLineDash(_segments: number[]): void { - throw new Error('Method not implemented.'); - } - public direction!: CanvasDirection; - public font!: string; - public textAlign!: CanvasTextAlign; - public textBaseline!: CanvasTextBaseline; - public arc(_x: number, _y: number, _radius: number, _startAngle: number, _endAngle: number, _anticlockwise?: boolean | undefined): void { - throw new Error('Method not implemented.'); - } - public arcTo(_x1: number, _y1: number, _x2: number, _y2: number, _radius: number): void { - throw new Error('Method not implemented.'); - } - public bezierCurveTo(_cp1x: number, _cp1y: number, _cp2x: number, _cp2y: number, _x: number, _y: number): void { - throw new Error('Method not implemented.'); - } - public closePath(): void { - throw new Error('Method not implemented.'); - } - public ellipse(_x: number, _y: number, _radiusX: number, _radiusY: number, _rotation: number, _startAngle: number, _endAngle: number, _anticlockwise?: boolean | undefined): void { - throw new Error('Method not implemented.'); - } - public lineTo(_x: number, _y: number): void { - throw new Error('Method not implemented.'); - } - public moveTo(_x: number, _y: number): void { - throw new Error('Method not implemented.'); - } - public quadraticCurveTo(_cpx: number, _cpy: number, _x: number, _y: number): void { - throw new Error('Method not implemented.'); - } - public rect(_x: number, _y: number, _w: number, _h: number): void { - throw new Error('Method not implemented.'); - } -} - -const mockCanvas = new MockCanvas(); - -export function setUpDomEnvironment() { - // tslint:disable-next-line:no-http-string - const dom = new JSDOM('<!doctype html><html><body><div id="root"></div></body></html>', { pretendToBeVisual: true, url: 'http://localhost'}); - const { window } = dom; - - // tslint:disable: no-function-expression no-empty - try { - // If running inside of vscode, we need to mock the canvas because the real canvas is not - // returned. - if (require('vscode')) { - window.HTMLCanvasElement.prototype.getContext = (contextId: string, _contextAttributes?: {}): any => { - if (contextId === '2d') { - return mockCanvas; - } - return null; - }; - } - } catch { - noop(); - } - - // tslint:disable-next-line: no-function-expression - window.HTMLCanvasElement.prototype.toDataURL = function () { - return ''; - }; - - // tslist:disable-next-line:no-string-literal no-any - (global as any)['Element'] = window.Element; - // tslist:disable-next-line:no-string-literal no-any - (global as any)['location'] = window.location; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['window'] = window; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['document'] = window.document; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['navigator'] = { - userAgent: 'node.js', - platform: 'node' - }; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['getComputedStyle'] = window.getComputedStyle; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['self'] = window; - copyProps(window, global); - - // Special case. Monaco needs queryCommandSupported - (global as any)['document'].queryCommandSupported = () => (false); - - // Special case. Transform needs createRange - (global as any)['document'].createRange = () => ({ - createContextualFragment: (str: string) => JSDOM.fragment(str), - setEnd : (_endNode: any, _endOffset: any) => noop(), - setStart : (_startNode: any, _startOffset: any) => noop(), - getBoundingClientRect : () => null, - getClientRects: () => [] - }); - - // Another special case. CodeMirror needs selection - // tslint:disable-next-line:no-string-literal no-any - (global as any)['document'].selection = { - anchorNode: null, - anchorOffset: 0, - baseNode: null, - baseOffset: 0, - extentNode: null, - extentOffset: 0, - focusNode: null, - focusOffset: 0, - isCollapsed: false, - rangeCount: 0, - type: '', - addRange: (_range: Range) => noop(), - createRange: () => null, - collapse: (_parentNode: Node, _offset: number) => noop(), - collapseToEnd: noop, - collapseToStart: noop, - containsNode: (_node: Node, _partlyContained: boolean) => false, - deleteFromDocument: noop, - empty: noop, - extend: (_newNode: Node, _offset: number) => noop(), - getRangeAt: (_index: number) => null, - removeAllRanges: noop, - removeRange: (_range: Range) => noop(), - selectAllChildren: (_parentNode: Node) => noop(), - setBaseAndExtent: (_baseNode: Node, _baseOffset: number, _extentNode: Node, _extentOffset: number) => noop(), - setPosition: (_parentNode: Node, _offset: number) => noop(), - toString: () => '{Selection}' - }; - - // For Jupyter server to load correctly. It expects the window object to not be defined - // tslint:disable-next-line:no-eval no-any - const fetchMod = eval('require')('node-fetch'); - // tslint:disable-next-line:no-string-literal no-any - (global as any)['fetch'] = fetchMod; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['Request'] = fetchMod.Request; - // tslint:disable-next-line:no-string-literal no-any - (global as any)['Headers'] = fetchMod.Headers; - // tslint:disable-next-line:no-string-literal no-eval no-any - (global as any)['WebSocket'] = eval('require')('ws'); - - // 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 no-any - (global as any)['getLocStrings'] = () => { - return { 'DataScience.unknownMimeType' : 'Unknown mime type from helper' }; - }; - - // tslint:disable-next-line:no-string-literal no-eval no-any - (global as any)['getInitialSettings'] = () => { - return { - allowImportFromNotebook: true, - jupyterLaunchTimeout: 10, - jupyterLaunchRetries: 3, - enabled: true, - jupyterServerURI: 'local', - notebookFileRoot: 'WORKSPACE', - changeDirOnImportExport: true, - useDefaultConfigForJupyter: true, - jupyterInterruptTimeout: 10000, - searchForJupyter: true, - showCellInputCode: true, - collapseCellInputCodeByDefault: true, - allowInput: true, - showJupyterVariableExplorer: true, - variableExplorerExclude: 'module;builtin_function_or_method' - }; - }; - - (global as any)['DOMParser'] = dom.window.DOMParser; - (global as any)['Blob'] = dom.window.Blob; - - configure({ adapter: new Adapter() }); - - // Special case for the node_modules\monaco-editor\esm\vs\editor\browser\config\configuration.js. It doesn't - // export the function we need to dispose of the timer it's set. So force it to. - const configurationRegex = /.*(\\|\/)node_modules(\\|\/)monaco-editor(\\|\/)esm(\\|\/)vs(\\|\/)editor(\\|\/)browser(\\|\/)config(\\|\/)configuration\.js/g; - const _oldLoader = require.extensions['.js']; - // tslint:disable-next-line:no-function-expression - require.extensions['.js'] = function (mod: any, filename) { - if (configurationRegex.test(filename)) { - let content = require('fs').readFileSync(filename, 'utf8'); - content += 'export function getCSSBasedConfiguration() { return CSSBasedConfiguration.INSTANCE; };\n'; - mod._compile(content, filename); - } else { - _oldLoader(mod, filename); - } - }; -} - -export function setupTranspile() { - // Some special work for getting the monaco editor to work. - // We need to babel transpile some modules. Monaco-editor is not in commonJS format so imports - // can't be loaded. - require('@babel/register')({ plugins: ['@babel/transform-modules-commonjs'], only: [ /monaco-editor/ ] }); - - // Special case for editor api. Webpack bundles editor.all.js as well. Tests don't. - require('monaco-editor/esm/vs/editor/editor.api'); - require('monaco-editor/esm/vs/editor/editor.all'); -} - -function copyProps(src: any, target: any) { - const props = Object.getOwnPropertyNames(src) - .filter(prop => typeof target[prop] === undefined); - props.forEach((p : string) => { - target[p] = src[p]; - }); -} - -function waitForComponentDidUpdate<P, S, C>(component: React.Component<P, S, C>) : Promise<void> { - return new Promise((resolve, reject) => { - if (component) { - let originalUpdateFunc = component.componentDidUpdate; - if (originalUpdateFunc) { - originalUpdateFunc = originalUpdateFunc.bind(component); - } - - // tslint:disable-next-line:no-any - component.componentDidUpdate = (prevProps: Readonly<P>, prevState: Readonly<S>, snapshot?: any) => { - // When the component updates, call the original function and resolve our promise - if (originalUpdateFunc) { - originalUpdateFunc(prevProps, prevState, snapshot); - } - - // Reset our update function - component.componentDidUpdate = originalUpdateFunc; - - // Finish the promise - resolve(); - }; - } else { - reject('Cannot find the component for waitForComponentDidUpdate'); - } - }); -} - -export function waitForRender<P, S, C>(component: React.Component<P, S, C>, numberOfRenders: number = 1) : Promise<void> { - // tslint:disable-next-line:promise-must-complete - return new Promise((resolve, reject) => { - if (component) { - let originalRenderFunc = component.render; - if (originalRenderFunc) { - originalRenderFunc = originalRenderFunc.bind(component); - } - let renderCount = 0; - component.render = () => { - let result : React.ReactNode = null; - - // When the render occurs, call the original function and resolve our promise - if (originalRenderFunc) { - result = originalRenderFunc(); - } - renderCount += 1; - - if (renderCount === numberOfRenders) { - // Reset our render function - component.render = originalRenderFunc; - resolve(); - } - - return result; - }; - } else { - reject('Cannot find the component for waitForRender'); - } - }); -} - -export async function waitForUpdate<P, S, C>(wrapper: ReactWrapper<P, S, C>, mainClass: ComponentClass<P>, numberOfRenders: number = 1) : Promise<void> { - const mainObj = wrapper.find(mainClass).instance(); - if (mainObj) { - // Hook the render first. - const renderPromise = waitForRender(mainObj, numberOfRenders); - - // First wait for the update - await waitForComponentDidUpdate(mainObj); - - // Force a render - wrapper.update(); - - // Wait for the render - await renderPromise; - } -} - -// map of string chars to keycodes and whether or not shift has to be hit -// this is necessary to generate keypress/keydown events. -// There doesn't seem to be an official way to do this (according to stack overflow) -// so just hardcoding it here. -const keyMap : { [key: string] : { code: number; shift: boolean }} = { - 'A' : { code: 65, shift: false }, - 'B' : { code: 66, shift: false }, - 'C' : { code: 67, shift: false }, - 'D' : { code: 68, shift: false }, - 'E' : { code: 69, shift: false }, - 'F' : { code: 70, shift: false }, - 'G' : { code: 71, shift: false }, - 'H' : { code: 72, shift: false }, - 'I' : { code: 73, shift: false }, - 'J' : { code: 74, shift: false }, - 'K' : { code: 75, shift: false }, - 'L' : { code: 76, shift: false }, - 'M' : { code: 77, shift: false }, - 'N' : { code: 78, shift: false }, - 'O' : { code: 79, shift: false }, - 'P' : { code: 80, shift: false }, - 'Q' : { code: 81, shift: false }, - 'R' : { code: 82, shift: false }, - 'S' : { code: 83, shift: false }, - 'T' : { code: 84, shift: false }, - 'U' : { code: 85, shift: false }, - 'V' : { code: 86, shift: false }, - 'W' : { code: 87, shift: false }, - 'X' : { code: 88, shift: false }, - 'Y' : { code: 89, shift: false }, - 'Z' : { code: 90, shift: false }, - '0' : { code: 48, shift: false }, - '1' : { code: 49, shift: false }, - '2' : { code: 50, shift: false }, - '3' : { code: 51, shift: false }, - '4' : { code: 52, shift: false }, - '5' : { code: 53, shift: false }, - '6' : { code: 54, shift: false }, - '7' : { code: 55, shift: false }, - '8' : { code: 56, shift: false }, - '9' : { code: 57, shift: false }, - ')' : { code: 48, shift: true }, - '!' : { code: 49, shift: true }, - '@' : { code: 50, shift: true }, - '#' : { code: 51, shift: true }, - '$' : { code: 52, shift: true }, - '%' : { code: 53, shift: true }, - '^' : { code: 54, shift: true }, - '&' : { code: 55, shift: true }, - '*' : { code: 56, shift: true }, - '(' : { code: 57, shift: true }, - '[' : { code: 219, shift: false }, - '\\' : { code: 209, shift: false }, - ']' : { code: 221, shift: false }, - '{' : { code: 219, shift: true }, - '|' : { code: 209, shift: true }, - '}' : { code: 221, shift: true }, - ';' : { code: 186, shift: false }, - '\'' : { code: 222, shift: false }, - ':' : { code: 186, shift: true }, - '"' : { code: 222, shift: true }, - ',' : { code: 188, shift: false }, - '.' : { code: 190, shift: false }, - '/' : { code: 191, shift: false }, - '<' : { code: 188, shift: true }, - '>' : { code: 190, shift: true }, - '?' : { code: 191, shift: true }, - '`' : { code: 192, shift: false }, - '~' : { code: 192, shift: true }, - ' ' : { code: 32, shift: false }, - '\n' : { code: 13, shift: false }, - '\r' : { code: 0, shift: false } // remove \r from the text. -}; - -export function createMessageEvent(data: any) : MessageEvent { - const domWindow = window as DOMWindow; - return new domWindow.MessageEvent('message', { data }); -} - -export function createKeyboardEvent(type: string, options: KeyboardEventInit) : KeyboardEvent { - const domWindow = window as DOMWindow; - options.bubbles = true; - options.cancelable = true; - - // charCodes and keyCodes are different things. Compute the keycode for cm to work. - // This is the key (on an english qwerty keyboard) that would have to be hit to generate the key - // This site was a great help with the mapping: - // https://www.cambiaresearch.com/articles/15/javascript-char-codes-key-codes - const upper = options.key!.toUpperCase(); - const keyCode = keyMap.hasOwnProperty(upper) ? keyMap[upper].code : options.key!.charCodeAt(0); - const shift = keyMap.hasOwnProperty(upper) ? keyMap[upper].shift || options.shiftKey : options.shiftKey; - - // JSDOM doesn't support typescript so well. The options are supposed to be flexible to support just about anything, but - // the type KeyboardEventInit only supports the minimum. Stick in extras with some typecasting hacks - return new domWindow.KeyboardEvent(type, (({ ...options, keyCode, shiftKey: shift } as any) as KeyboardEventInit)); -} - -export function createInputEvent() : Event { - const domWindow = window as DOMWindow; - return new domWindow.Event('input', {bubbles: true, cancelable: false}); -} - -export function blurWindow() { - // blur isn't implemented. We just need to dispatch the blur event - const domWindow = window as DOMWindow; - const blurEvent = new domWindow.Event('blur', {bubbles: true}); - domWindow.dispatchEvent(blurEvent); -} diff --git a/src/test/datascience/serverConfigFiles/jcert.pem b/src/test/datascience/serverConfigFiles/jcert.pem deleted file mode 100644 index 01a4008fdd5b..000000000000 --- a/src/test/datascience/serverConfigFiles/jcert.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDeTCCAmGgAwIBAgIJAKa7Vk5Yxq+5MA0GCSqGSIb3DQEBCwUAMFMxCzAJBgNV -BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdSZWRtb2RuMQ0w -CwYDVQQKDAROb25lMQ4wDAYDVQQDDAVpYW5odTAeFw0xOTA1MTQyMjMzMDZaFw0y -MDA1MTMyMjMzMDZaMFMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9u -MRAwDgYDVQQHDAdSZWRtb2RuMQ0wCwYDVQQKDAROb25lMQ4wDAYDVQQDDAVpYW5o -dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKpEcEMQvPwSw3Xl4cqt -2jwakzDBbRB5dxW+SVUhGnyPc6Ime4HioP1ch3+OqjVnstne2WKk3bDnpTSp2wX8 -yAY64CY/eLexGYVZed7hKu79rFKaEe+W7fjUER+36DsIo5JZ81LpRLCusrUrd2/A -12rAJm8EmPgqdmSZo51PHbuFOLKT+95pNxEkxrsmDCv/jUVU0iGkkAAsizne7gqa -FBMwo+MHd+1OBkxyXdG/0yrFuFV6AZ3KXZBMTZmtrW+eUSHJ/DrVPAdXOvPoO1d3 -9VJndD2UpZgkYX2povBxVdslHctDaNnuhGb463ldQXED4M0Fq6Ojnu/rn3GzmUJ1 -tskCAwEAAaNQME4wHQYDVR0OBBYEFJIGYn57WFv1QzrPbPHzJolZMGJUMB8GA1Ud -IwQYMBaAFJIGYn57WFv1QzrPbPHzJolZMGJUMAwGA1UdEwQFMAMBAf8wDQYJKoZI -hvcNAQELBQADggEBAIc+IlU5ri52OUDBiPWHGQSOMjcCJ9SDjV6Z0GlGRvWlp9Pp -MJraFl7umZa8sZmRHfjX2dm4x3JHw1pcCdicyYy/Hig1A2MiGZLzUjQcO94ZBj2E -pddOria2KgDiXbULprQMyGCR4aNQKs3ycaNufFr53QrLFa8OGpeYS5nILHQkVq4N -pa9sjlsVoafQXlngSYOUt4VUWm2RUZ61jgnITqIause9wlxY7kW/YUXUCMUW/QEE -wCQplIMrHMBzLK7piNZrGMSfB/pXvnM+9zy2lLclI7ipAZCOW44340s/8w5Zc4fm -cryiN3LEGkuFv3XTSgflLk5f0At77yb+lpCoPG8= ------END CERTIFICATE----- diff --git a/src/test/datascience/serverConfigFiles/jkey.key b/src/test/datascience/serverConfigFiles/jkey.key deleted file mode 100644 index 370a3c5da279..000000000000 --- a/src/test/datascience/serverConfigFiles/jkey.key +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCqRHBDELz8EsN1 -5eHKrdo8GpMwwW0QeXcVvklVIRp8j3OiJnuB4qD9XId/jqo1Z7LZ3tlipN2w56U0 -qdsF/MgGOuAmP3i3sRmFWXne4Sru/axSmhHvlu341BEft+g7CKOSWfNS6USwrrK1 -K3dvwNdqwCZvBJj4KnZkmaOdTx27hTiyk/veaTcRJMa7Jgwr/41FVNIhpJAALIs5 -3u4KmhQTMKPjB3ftTgZMcl3Rv9MqxbhVegGdyl2QTE2Zra1vnlEhyfw61TwHVzrz -6DtXd/VSZ3Q9lKWYJGF9qaLwcVXbJR3LQ2jZ7oRm+Ot5XUFxA+DNBaujo57v659x -s5lCdbbJAgMBAAECggEAf5ri96AnwlLdohIy8g7xK3JPNY8BCgO+N9FwbBhvHUL1 -SmTE00bhmIAsHHDzJyscYyQcj003yEkTCzDxML+NuP2O15tiAWj802+HYn4mCw6a -gx1sq77VglKMstTFetiynhBDx7ODA1cqH5T/4gUIbLytES7E5dgM+sAaWt7cTZgF -M4c48/Rrb/7O11Kkout+6Zv/FVTt6Fqe3HPxuIW/SF0xgSJIH/lWoGlUaG6DxvEl -wZbNhSYg+jDrf/gnhwQR17H/MmchovKiYCPhDKZnzCao+hzC6mDLshKP3gmo0RA0 -zYRFYGOlEph3TKYcqooRub2HRckMBcOBXjrpxIpsAQKBgQDUv2zfTFJwfE4RE5Xv -1NzPhMRMEW5HCx90v6CjY6oqLJK9U/uLH14hdfr2Mb5SPieBEHBuE971hmXqK1Bo -8H7tXbxiZR9P5k1Yz6UhWnHxZeJxsFYT+JVNFjXHRMQEnhHihN/AtZAYmvwp0Mwd -13OJSBAmeXZr5anpU5InyQ4IqQKBgQDM4hiujo7zQf7ZZhcwY3o2YF/xXToKrP7f -hoIvJn3fcdde19SGElVvE2Lir2XTdNEv5VbjOOywHFH6rt8oMEEjnavzq1rgVrjr -3Q1pAHdjQFFyC8lPmakGgQWkay8wc0wIxUoEz4fRnBcOut1azbZ4+3y9Kuzp6MwR -N5CA97RxIQKBgQCXFR4u8Zd19IDIFb2b5PGumV2Bm7tRzm9XTKK6hZOZga/vrg1r -vint30gKwEalRyhsuoztT0U93WTQyFPBQlERJkkbIy76YdW55TQinIVgZfdKv2xR -oG3+oXAthAMkOFEBKVVxGD8tihrbY0EhTBjre/akLAvSEfX5EfUwNdK2iQKBgGoj -awPq6FVOyBaZk8PGlQZccPeaAzqKmlLz3LdOaoD5+cexafC2yLmNQnoKwWaFKuV0 -GsoFsGAfm7yRIRwxu10XDoBiMebsJkpSLuNJkY/CPy8kufpZsT2kU2b0+/JOmIIm -qozJciP9h9hip8+lqDUOm3VoKmmW5zi4H00ghcLhAoGAD4x1nIq0Wthk5BrBmoe/ -NDEjOxIrEsRDLiQ7BsQ3MEqJYaA0h+riesLPnbh65NUGfOpaVqGQ+8+vifDC71ye -qAj3HBiTLwCa88QqRG5h9vybD8LpfoeNA1bVR0VFaFvHsn5CdenxMU2UYTROo589 -Bg0MdwY0jPPPdEjM7GgjxyM= ------END PRIVATE KEY----- diff --git a/src/test/datascience/serverConfigFiles/remotePassword.py b/src/test/datascience/serverConfigFiles/remotePassword.py deleted file mode 100644 index ed9112faa09b..000000000000 --- a/src/test/datascience/serverConfigFiles/remotePassword.py +++ /dev/null @@ -1,4 +0,0 @@ -c.NotebookApp.ip = '0.0.0.0' -c.NotebookApp.open_browser = False -# Python -c.NotebookApp.password = 'sha1:74182e119a7b:e1b98bbba98f9ada3fd714eda9652437e80082e2' \ No newline at end of file diff --git a/src/test/datascience/serverConfigFiles/remoteToken.py b/src/test/datascience/serverConfigFiles/remoteToken.py deleted file mode 100644 index 395e1e34daec..000000000000 --- a/src/test/datascience/serverConfigFiles/remoteToken.py +++ /dev/null @@ -1 +0,0 @@ -c.NotebookApp.open_browser = False \ No newline at end of file diff --git a/src/test/datascience/serverConfigFiles/selfCert.py b/src/test/datascience/serverConfigFiles/selfCert.py deleted file mode 100644 index 93d144e75abd..000000000000 --- a/src/test/datascience/serverConfigFiles/selfCert.py +++ /dev/null @@ -1,2 +0,0 @@ -c.NotebookApp.ip = '0.0.0.0' -c.NotebookApp.open_browser = False \ No newline at end of file diff --git a/src/test/datascience/shiftEnterBanner.unit.test.ts b/src/test/datascience/shiftEnterBanner.unit.test.ts deleted file mode 100644 index 1dc3569f0457..000000000000 --- a/src/test/datascience/shiftEnterBanner.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:max-func-body-length match-default-export-name no-any no-multiline-string no-trailing-whitespace -import { expect } from 'chai'; -import rewiremock from 'rewiremock'; -import * as typemoq from 'typemoq'; - -import { IApplicationShell } from '../../client/common/application/types'; -import { - IConfigurationService, - IDataScienceSettings, - IPersistentState, - IPersistentStateFactory, - IPythonSettings -} from '../../client/common/types'; -import { Telemetry } from '../../client/datascience/constants'; -import { InteractiveShiftEnterBanner, InteractiveShiftEnterStateKeys } from '../../client/datascience/shiftEnterBanner'; -import { IJupyterExecution } from '../../client/datascience/types'; -import { clearTelemetryReporter } from '../../client/telemetry'; - -suite('Interactive Shift Enter Banner', () => { - const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; - const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; - let appShell: typemoq.IMock<IApplicationShell>; - let jupyterExecution: typemoq.IMock<IJupyterExecution>; - let config: typemoq.IMock<IConfigurationService>; - - class Reporter { - public static eventNames: string[] = []; - public static properties: Record<string, string>[] = []; - public static measures: {}[] = []; - public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { - Reporter.eventNames.push(eventName); - Reporter.properties.push(properties!); - Reporter.measures.push(measures!); - } - } - - setup(() => { - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; - appShell = typemoq.Mock.ofType<IApplicationShell>(); - jupyterExecution = typemoq.Mock.ofType<IJupyterExecution>(); - config = typemoq.Mock.ofType<IConfigurationService>(); - rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); - }); - - teardown(() => { - process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; - process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; - Reporter.properties = []; - Reporter.eventNames = []; - Reporter.measures = []; - rewiremock.disable(); - clearTelemetryReporter(); - }); - - test('Shift Enter Banner with Jupyter available', async() => { - const shiftBanner = loadBanner(appShell, jupyterExecution, config, true, true, true, true, true, 'Yes'); - await shiftBanner.showBanner(); - - appShell.verifyAll(); - jupyterExecution.verifyAll(); - config.verifyAll(); - - expect(Reporter.eventNames).to.deep.equal([Telemetry.ShiftEnterBannerShown, Telemetry.EnableInteractiveShiftEnter]); - }); - - test('Shift Enter Banner without Jupyter available', async() => { - const shiftBanner = loadBanner(appShell, jupyterExecution, config, true, false, false, true, false, 'Yes'); - await shiftBanner.showBanner(); - - appShell.verifyAll(); - jupyterExecution.verifyAll(); - config.verifyAll(); - - expect(Reporter.eventNames).to.deep.equal([]); - }); - - test('Shift Enter Banner don\'t check Jupyter when disabled', async() => { - const shiftBanner = loadBanner(appShell, jupyterExecution, config, false, false, false, false, false, 'Yes'); - await shiftBanner.showBanner(); - - appShell.verifyAll(); - jupyterExecution.verifyAll(); - config.verifyAll(); - - expect(Reporter.eventNames).to.deep.equal([]); - }); - - test('Shift Enter Banner changes setting', async() => { - const shiftBanner = loadBanner(appShell, jupyterExecution, config, false, false, false, false, true, 'Yes'); - await shiftBanner.enableInteractiveShiftEnter(); - - appShell.verifyAll(); - jupyterExecution.verifyAll(); - config.verifyAll(); - }); - - test('Shift Enter Banner say no', async() => { - const shiftBanner = loadBanner(appShell, jupyterExecution, config, true, true, true, true, true, 'No'); - await shiftBanner.showBanner(); - - appShell.verifyAll(); - jupyterExecution.verifyAll(); - config.verifyAll(); - - expect(Reporter.eventNames).to.deep.equal([Telemetry.ShiftEnterBannerShown, Telemetry.DisableInteractiveShiftEnter]); - }); -}); - -// Create a test banner with the given settings -function loadBanner( - appShell: typemoq.IMock<IApplicationShell>, - jupyterExecution: typemoq.IMock<IJupyterExecution>, - config: typemoq.IMock<IConfigurationService>, - stateEnabled: boolean, - jupyterFound: boolean, - bannerShown: boolean, - executionCalled: boolean, - configCalled: boolean, - questionResponse: string -): InteractiveShiftEnterBanner { - // Config persist state - const persistService: typemoq.IMock<IPersistentStateFactory> = typemoq.Mock.ofType<IPersistentStateFactory>(); - const enabledState: typemoq.IMock<IPersistentState<boolean>> = typemoq.Mock.ofType<IPersistentState<boolean>>(); - enabledState.setup(a => a.value).returns(() => stateEnabled); - persistService.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(InteractiveShiftEnterStateKeys.ShowBanner), - typemoq.It.isValue(true))).returns(() => { - return enabledState.object; - }); - persistService.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(InteractiveShiftEnterStateKeys.ShowBanner), - typemoq.It.isValue(false))).returns(() => { - return enabledState.object; - }); - - // Config settings - const pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - const dataScienceSettings = typemoq.Mock.ofType<IDataScienceSettings>(); - dataScienceSettings.setup(d => d.enabled).returns(() => true); - dataScienceSettings.setup(d => d.sendSelectionToInteractiveWindow).returns(() => false); - pythonSettings.setup(p => p.datascience).returns(() => dataScienceSettings.object); - config.setup(c => c.getSettings(typemoq.It.isAny())).returns(() => pythonSettings.object); - - // Config Jupyter - jupyterExecution.setup(j => j.isNotebookSupported()).returns(() => { - return Promise.resolve(jupyterFound); - }).verifiable(executionCalled ? typemoq.Times.once() : typemoq.Times.never()); - - const yes = 'Yes'; - const no = 'No'; - - // Config AppShell - appShell.setup(a => a.showInformationMessage(typemoq.It.isAny(), - typemoq.It.isValue(yes), - typemoq.It.isValue(no))) - .returns(() => Promise.resolve(questionResponse)) - .verifiable(bannerShown ? typemoq.Times.once() : typemoq.Times.never()); - - // Config settings - config.setup(c => c.updateSetting(typemoq.It.isValue('dataScience.sendSelectionToInteractiveWindow'), typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve()) - .verifiable(configCalled ? typemoq.Times.once() : typemoq.Times.never()); - - return new InteractiveShiftEnterBanner(appShell.object, persistService.object, jupyterExecution.object, config.object); -} diff --git a/src/test/datascience/sub/test.ipynb b/src/test/datascience/sub/test.ipynb deleted file mode 100644 index 1cd123a4c5cd..000000000000 --- a/src/test/datascience/sub/test.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"source":"# Change directory to VSCode workspace root so that relative path loads work correctly. Turn this addition off with the DataScience.changeDirOnImportExport setting\r\n# ms-python.python added\r\nimport os\r\ntry:\r\n\tos.chdir(os.path.join(os.getcwd(), '..'))\r\n\tprint(os.getcwd())\r\nexcept:\r\n\tpass\r\n","cell_type":"code","outputs":[],"metadata":{},"execution_count":0},{"cell_type":"code","execution_count":1,"metadata":{},"outputs":[{"name":"stdout","output_type":"stream","text":"hello\n"},{"data":{"text/plain":"'d:\\\\Training\\\\SnakePython'"},"execution_count":1,"metadata":{},"output_type":"execute_result"}],"source":["print('hello')\n","\n","import os\n","os.getcwd()\n",""]},{"cell_type":"code","execution_count":2,"metadata":{},"outputs":[{"data":{"text/plain":"[<matplotlib.lines.Line2D at 0x18bf15abf28>]"},"execution_count":2,"metadata":{},"output_type":"execute_result"},{"data":{"image/png":"iVBORw0KGgoAAAANSUhEUgAAAU4AAAJCCAYAAACveS6PAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADl0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uIDMuMC4zLCBodHRwOi8vbWF0cGxvdGxpYi5vcmcvnQurowAAIABJREFUeJzsvXuQZNd93/f59Xt6uue1M7tYALt4kCBNkJRIewlKls1UJFGkXJbAOJJNOomhlFSIKpZdZZUdUZFLSmirIsepMJWKEguWKFGWYkqi7RJsU6H5kGTJFEksRZAgQOFNAPvenWd3z/T75I97T3fvoGemH/dxzr3nU7WFnZ6e6Yuz937P731EKYXD4XA4JicT9wU4HA6HbTjhdDgcjilxwulwOBxT4oTT4XA4psQJp8PhcEyJE06Hw+GYEiecDofDMSVOOB0Oh2NKnHA6HA7HlOTivoBZWF9fV/fee2/cl+FwOBLGV77ylVtKqY2T3melcN57771cvHgx7stwOBwJQ0RemeR9zlV3OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFPihNPhcDimJBDhFJGPicgNEfnGEd8XEfk/ReQFEfm6iPz5ke89IiLP+38eCeJ6HA6HI0yCsjh/DXj/Md//fuAB/8+jwP8DICJrwM8B7wYeAn5ORFYDuiaHw+EIhUCEUyn1H4GtY97yMPDryuOLwIqInAXeB3xGKbWllNoGPsPxAuxwOByxE1WM8y7gtZGvL/mvHfW60Xzum9f5o+dvxn0ZVvL89Rr/wye/xu5BJ+5LsQ6lFD/520/y20+8dvKbHaESlXDKmNfUMa+//heIPCoiF0Xk4s2b8YnW41+7wo/9+kX+3m99jV5/7KU6jqDd7fN3P/Ekv33xEr9z0T380/Lpp6/xr//0Mv/L732TRqsb9+WkmqiE8xJwbuTru4Erx7z+OpRSjymlLiilLmxsbIR2ocfxB8/e4Cd/60lOV4vcqrf4wou3YrkOW/m/Pv8837y6x0a1yG9+6VX6buOZmG6vzz/99LNsVIts73f4jS++EvclpZqohPNx4G/52fXvAHaVUleBTwPfJyKrflLo+/zXjOOlm3V+/De+wpvvqPLv/s5fplrM8btPjtV4xxieurTLL/7Bi/y1d97F//hX/hwv32rwhRc3474sa/jXf3qZF282+EcPv42//MA6//yPXuKg3Yv7slJLUOVI/xL4E+DNInJJRH5URH5cRH7cf8ungJeAF4B/Dvz3AEqpLeAfAU/4fz7iv2Ycv//sTZqdPr/03/wFNqpF3ve2O/j/vnGNZsfdvJPwD3/3G6xXCvzcD7yV73/bWVbLeX7zS85qmoRmp8dHP/sc335uhfe99Qx/57sf4Fa9zf/75VfjvrTUElRW/UNKqbNKqbxS6m6l1K8opf6ZUuqf+d9XSqm/rZR6g1Lq7UqpiyM/+zGl1Bv9P78axPWEwVOXdji7XOLu1TIAD7/jTuqtLr//ZzdivjLzabS6fP3SDn/zoXtYLucp5bP88IVz/IdnrnN9rxn35RnPv/7Ty1zdbfJT73szIsJD963xHfev8Ut/+CKtrtu448B1Dk3I1y/v8ra7lgdff+f9p1ivFJ27PgF/dq2GUvDWO5cGr/3Nh87T6ys+8WWXJDqJr766zXqlwF984/rgtR/5i/dyo9biqUu7MV5ZenHCOQG1ZoeXbzX4thHhzGUz/NVvO8vn/+yGK605gWeu7gHw4Ihw3ru+yLvuXeXzzzqL/SSeu17jTWeqt7321juX/e/V47ik1OOEcwKevrKHUvC2u5dve/0Hvv0s7V6f//SCy64fxzNX9lgp5zm7XLrt9becXeLFG3WUctn1o+j3Fc9dr/PmO24XzrtWFigXsjx3vRbTlaUbJ5wToN2ht991u3C+5axnQb100+36x/HM1T3ecscSIreX7b7xdIV6q8v1vVZMV2Y+r27tc9Dp8ecOCWcmIzxwpuqEMyaccE7AU5d3uXO5xHqleNvr5UKOs8slXrrZiOnKzKfb6/NnV/duc9M1b9yoAPDCDbfxHMWzvjAedtUB3nS64lz1mHDCOQFPXd7l7YfcdM1964u8dMsJ51F8a7NBq9vnwbNjhPO0Fk5nNR3Fs9eOFs4331HlVr3FVqMd9WWlHiecJ7DnJ4YOu+ma+zcWeemmi9MdxdNXXp8Y0mxUi1RLOV50FvuRPHutxvm1MovF3Ou+94Avps5djx4nnCfwjct+fPPulbHfv2+9wl6z63b9I/jm1RqFbIY3+G75KCLCGzYqzlU/hmfHZNQ1b3bCGRtOOE/gqMSQ5v6NRQDnrh/BM1f3eOBMhUJu/K32xtMVXnDJtbG0uj1evtV4XWJIc2bJs9idcEaPE84TeOryLnetLLC2WBj7/fvXPeF82bmbY3nmyt6g+mAcbzxd4Wat5Wphx/DijQa9vnpdKZJGRHjTmSrPXXMbT9Q44TyBp6/s8ba7jn7w714tk88KL95yN+9hbtSa3Kq3xiaGNC6zfjTPXvfiw0cJJ3hJo+du1FyMPWKccB5Dv6+4vH3Avb5VOY5sRrjn1KKzOMfwzDGJIY3OrL/ohPN1/Nm1GvmscN8x99+bzlTY2e9ws+ZqYaPECecx3Gq0aPf63LWycOz77nclSWPRsbe33HG0cJ5bK1PIZVyccwzPXavxho0K+ezRj+kwQeTWL0qccB7DlR1vcs/Z5ROEc6PCK5sNNxH+EFd2mlSLOZbL+SPfk80I968vOotzDM9eqx2ZGNLokqRnXYIoUpxwHsPVnQMA7lwpHfu++9cX6fQUl7b3o7gsa7i6e8DZE9YO8EqSnMV5G/vtLld2mwNhPIr1SoG1xQLPO+GMFCecx3DZF84TXXVXkjSWq7tN7jjBWgd4w+kKr23tu6HQI1zb9bydkzZtrxbWhYoO89SlXf74+fCG7zjhPIYrO00W8lmWF452NYFB8N71rN/O1d0mZ5dOtjjfeLpCX8HL7uEfoIXzzATrd2ap5AZCH+Ljf/It/v7vfC203++E8xiu7h5w50rpdVN9DrO2WGB5Ie+mJI3Q7va5VW9N5KrrWthXNp1waq7tTRZfB7hjqcS13aYrSRrh+l6TM8sn33uz4oTzGK7sHHDnCW46eO7SfeuLzmIa4fpeE6V43QzOcdzhv8eNlxty1bc475jA4rxjuUSr23dNBCNc32typlo8+Y0z4oTzGK7sNrlzgh0fPKvplU2XHNLoB38Si2mtXCCXEedujnB9r8nyQp6FQvbE92p33m08Q67vtQYbchg44TyCVrfHzVprIosT4MxyiRs15y5pru56ibVJLM5MRjhdLboHf4Rru82JrE0YWuzX3MYDeKeC7h50JooPz4oTziOYNKupOV0t0ukptveduwQjFueEG8/pJW/jcXhcmyJGpwX2+q5bP2DguZx2rnr06OL3SS3O01XtLrmbF7yNp1rMURkzR3IcZ5aKbu1GuDZhRQLA6SVPIJzF6aE9F+eqx8CVQfH7pBaTd/PecD3DwOTF7xqvpMatHUCn1+dmvTWxxVnMZVlbLDjh9NHr4Fz1GNDCOUmMDoZuwQ138wKTF79rziyV2D3ouCJ44GatNXFFgubMUsm56j43nHDGx5XdJqcWC5TyJ2c1YeiqO4vT4+pukzunePCHG49bP20xTZoc8t5bdBanz/W9JqV8hqXSZGGiWXDCeQST1nBqFgpZqsWcG+/FsPh9mhjToKTGJYim6hrS3LHsuoc01/ZanFk6uXFlHpxwHoHuGpqG00tFlxlmWPw+aQ0sjNYiuvW7NqiBnW7juVVv0+72w7osa7i+1wzVTQcnnGNRyhtgPEnx9iinqyXnajLS9TLVg++56i5B5D34hVyGlWPG8R1Gu/Vu4/ZinE44Y2Cv2aXR7p04Fekwp5eKztVkWPw+jcW+vJCnkMu45Br+cJTl6VzNM8vOYgfP6LkWcrslOOEcy6DrZVpXvVrkxl4r9d1DQ4tz8o1HRFwtp8+1GSwmbXFe2023xb7X7NLs9EOt4QQnnGOZtoZTc7rqDVvYa3bDuCxrmLb4XXOm6mo5Ybp2S81AOFO+8WiP5bRz1aNn0DU0bYzTj9PdTLm7fmVnuuJ3zZmlUupDHdrVnCYxBLBS9kIdabfYZynlmgUnnGPQJUXrlfFnqR/FhqtFBLybdxo3XXN6qZj6tdvZ79Du9qd21UVkMJczzWiPRScbw8IJ5xi2Gm1Wynlyx5wuOA5XBO8xbfG75sxSiXqrS72V3lDHLBUJmjuWSql31a9H0DUETjjHstlocWpxOmsTRktq0nvzdnpe8fssN65evzRn1vW9M4twnlkupXrtYDjHdNKOv1lxwjmGzXqbU4vTm/qVYo6FfDbVFud2o41SsD5DOciZwYSp9K7fNJPfD6PbLtNc1eEVv4frpoMTzrFsNdqszWBxiojfPZTeB3+z0QaYyWI/7Yq4ubbXJCPDePk0nFkq0ez02TtIb6hDt1uGjRPOMWw22pyaMjGk8Wo50/vgb/vCuVp2oY5ZuLHX5FSlSH7K+DoM43ppjnNG0TUETjhfR6+v2N5vz2QxgZcgSvOgj4HFOcPGUynmKBeyqXbVtxqz33tpP0Kj31fcqLWcqx4HO/tejO5UZbbF36im21Xf8oVz1lBH2s8I395vz2StwzA8oq3+tHGr0aLXV6HXcIITztexOceDD14tYr3VpZHSkprNRhuR2Vx1GLatppXNRpu1GcNE+p7dSqlwak9vlvjwtDjhPMRmffbkBgwzw2m1OrcaLVYW8mQzs81CPJ3y7qHtRpu1GTedpVKejHhWaxrZbngHJa7NUBEzLU44D7HZ8ARvVlf9dMprEWetSNCcWiyk1tXs9RU7Bx1WZ1y/TEZYLRdSa3Fu7WtvcfJxfLPihPMQ88TowHUPzSucq+UCe80unV76BvIO4uvzrN9iIcUW5+wVHdPihPMQm3Udo5tt1xqcneOEcya0tbCTwvPpteDNanGCt+HrcFPa2PLj68sLzuKMnE0/Rjdtn7pmecGLM+2kdNf3hHP2GNOKby2kcf22dIxuDotprZxii3O/zfIcz+40OOE8xLwWUyYjLC/kU3nz9vuK7f3OXK5mmjPDW358fa5Qx2JhIMBpY3u/M9emMw1OOA9xq96eOTGkWS0XBhm+NLF70KHXV3PHOCGdmeGBxTlnqGN7v53KfvVtf6pZFDjhPMQ8nRuatAbo5+ka0qz6Mc7tVMc4Z3/4V8sFen2VylMI5vUWp8EJ5yGCWPzVcj7dD/4c7pL+2TS66pv1NpVijmJu9pFoaynuHpqn62panHCOMOhTn9NVXykXUpnc0NnceTaeUj5LuZBN74M/Zw2izshvpez+U0o5izMutgOoowPP4kyjxbQVgKsOntWZtgcffG9nTotJ/3zaNp6DTo9Wtz9XKdc0BCKcIvJ+EXlWRF4QkQ+P+f5HReRJ/89zIrIz8r3eyPceD+J6ZmXQbjnvg79YoNXtc9DuBXFZ1hBEVhi8GF8a6ziDsJj0z2+mTDgHjSsRuerTnd86BhHJAr8IvBe4BDwhIo8rpZ7R71FK/b2R9/8d4J0jv+JAKfWOea8jCDaDevBHMsMLhekPLbOVzcb8MTogtW2DW402D5ypzPU70hrj1FUsNlmcDwEvKKVeUkq1gU8ADx/z/g8B/zKAzw2cgas555AA3XWUtsx6UDGmtZRWJWzvz++qlwtZCrlM6kIdUfapQzDCeRfw2sjXl/zXXoeI3APcB3x+5OWSiFwUkS+KyAcCuJ6ZCcpVXxnEmdLlbm412oHs+F4dbLoe/Ganx367N/f6iYjXPZSy9dPJ2BVbXHVg3Pywo6pvPwh8Uik1Gvw7r5S6IiL3A58XkaeUUi++7kNEHgUeBTh//vy81zyWeWdJagbuUtp2/UY7kGMLRgd9zHKEhI0MvZ0ANp4Udg9FHeMM4q68BJwb+fpu4MoR7/0gh9x0pdQV/78vAX/A7fHP0fc9ppS6oJS6sLGxMe81j2XeWZIa3b2QtpKk4Fz19A360A9+EBa77h5KE9uNNhmBpQgGfEAwwvkE8ICI3CciBTxxfF12XETeDKwCfzLy2qqIFP2/rwPfBTxz+GejYjOAdkuAlQVtcabnwVdKeYfcBfDgp3HQx7zjDEdJY6hja7/NSrkwt9EzKXO76kqproj8BPBpIAt8TCn1tIh8BLiolNIi+iHgE+r2Jtq3AL8kIn08Ef+F0Wx81GwGZDEVchkqxVyqdv1Gu0e72w8sOQTp6h7a3g9OONcW01cHu93ozDwKchaCiHGilPoU8KlDr/3soa//pzE/9wXg7UFcQxBs1lu8+Y5qIL9rdTGfql1/K4CuIU0aB30EGaNbLRfY2e/Q7fUjGbFmAlF2DYHrHLqNIBd/tVxIlaseVA0sDIdcpCnBseXH6IIYwqurQnYO0rN+UfapgxPOAfq8l6CycmnrVw/S1UyrxblaLpAJIEa3msK2S2dxxkSt2UGp4OrA0jYhaXg66PzJtTQO+vAGfARz76UtRqyUN5wnqhpOcMI5QJe+BHVeSdoym4MY3ZzNA5q0DfrYrM/fNaRJm8XeaPfo9FRkXUPghHPA7kHwwllrpee0xq1Gm0Iuw2Jhvj51TdoGfWzvB+dqDi3OdKxflKdbapxw+gyEM6CShtWUFXF7wfk8IsHU0aVt0MdWY/bz1A+zkrJZCUHWwE6KE04fLZwrAVmcaSvi3j3oBHos62qKTmvs+wO0g3I1S/ksi4Vsao4J1iGdqCYjgRPOATsBu+o6XpUWqylo4VxbTE+MuNbs+ofczZ9Y06Tp3KvtiPvUwQnngD1fOIPqdR26S+lw1XcPuoFbnHrQR9IZWEwBdr6cWkxPqCPIPv9JccLps3vQoZTPUMoHldxIl6u+d9AJdMBCmgZ9DEeiBbjxpMni3G+TzQhLpUAaISfCCafPzn47YIspbRZnsK76SopKavRRvoGu30J6JiRt+X3qQSUmJ8EJp0/QD/5CPksxl0nFzdvt9am3gnXV03QERNClcPp37R2k42z17Ua07ZbghHPA7kFnMA4uCEQkNUXwYVhMaSri1sK5VApYOJsd+v2jZoonh6j71MEJ54Ddg27gQ1BXUtJ2GYbFlKZBH0EnJvXvUgpqreRbnXvN4J/dk3DC6bMbcIwT9Hiv9FhMwcboCrf97iSzd9ChmAsuMQlDEd5LyfotLUSXGAInnAOCjnFCek5rDEM4S/kM+ayw10z+g78bcEUCDP8t0rLxBP3snoQTTqDT69No9wItBwHnqs+DiLC8kE/Fgx/Gpq3jpUnfeHp9Ra3VDTQ+PAlOOAnnwYehq570AH1Y67dUyqfD1WwGL5zLKXHVa81w7r2TcMJJeA/+8kKevoJGO9kB+jCSGwDVhfwgY59kQrE4/Zhf0i323ZDuvZNwwknwk5E0+uZN+sO/G0JyA0iVqx5018vQ4kz2vaf//5zFGQNhupqQfHdpdz+c4PxSKUct4WsH4axfpZgjI8m3OHUMN8p2S3DCCXg3LoQgnCmJM4XhaoK3fkl/8Pt+ciPo9RORVKyfc9VjJOhZnJqBu5QCVz0M4dTdL0olN7lWa3VRKpwHX69fktkLyVs8CSechLdraVc9Dbt+OK56nk5P0ewkd7RcWIk18NYvDfceOIszFnb2OywWsuSzwS7HIDmUgps3HFc9+ZnhsOLr+ncm/d7ba3bIZiSws64mxQkn4T34laLOqif85g2h8wVGQx3JXb8wXc00VCXoioQoR8qBE07AF84QpqvkshkqxVyiS0J6ISU3IB1VCWFMRtIsLeTYTfC9B145UtTxTXDCCcDuQZvlkIYELJVyzmKaEW3FJtlqCquGGLz1S/K9B+H0+U+CE07Cc9XBv3nT8OA7V30m9kJsGVxeyNPu9ml2eoH/blMIo111EpxwEvwQ41GWSsne9cMUTl3UnORQx+5BeMmNtIQ6oh7wAU44AR3jDMviTHaMM2xXc/QzkkiYyY00jJbbO+hGPosTnHDS7PRodvrhuerO4pyZfDZDuZBNtMUUZnIjDRvPXtPFOGMh7M6DpLe9hSmckI6NJ6y1S3qMuNnp0e72naseB6E/+At56q1uYmdyhr9+ucRvPGFZTEl31eNqtwQnnOyEbjHlEn1o1t5Bh0III+U0ST/mNqzmAUh+ci2udktwwjmYjBT0sRmapE9ICtPVhOS76mGW0yQ9xhlmKddJOOGMIEYHyY0zhS6cCY4RK6VCXT+dXEvq+g27rlxWPXKiiNFBst2lMIUzyYMqDjo9Oj0VanIjyesX1/R3cMI5iHFWQ7p5ncU5H0ulHLWEJteiePCTPOjDxThjZO+gQ7WUI5sJZ7pK0k8bjMJVT2pyLWxvB5IdIx7MMnXlSNGz1wy3ZWtocSbvwYdohBOSufFEIpwL+cROSNo96LCQz1LIRS9jqRfOWrNLNcTgcqWU3GG8vb6i1uyG6ioleYr+0NUM7/5LdIyz2Yml3RKccFJrdkIVzmxGqBZzibx5axGUgwyPWE7e+kVRwO3NSkje2kF8szjBCadvcYa7+EmdixhVcmP0s5JEFK768kKeWqtLL4HJtbgmI4ETTs/VDLkObCmh3S96MwjTYk/yaLTdkCs6INnrF9csTnDC6bvqIVucCZ0CX/MTXqEKZ4IHVewedKgWw6vogGQP+ohr+jukXDiVUqEnhyC5U+B1jDNMd6lazCGSXIsp7Ac/yYM+9vxZpnGQauFsdvp0+yoCizM/sM6SRBQWZ8ZPriX2wQ9ZOJcSGiPuh3hI4CSkWjhrEcToILmZzeH6RZFcS9aDD15tb9gWU1Itzlqri1LxdA1ByoVzLwKLCXyLM4GZTW1x6vPjwyKptYjRVHQks4540DXkhDN6oojRwfAft5awAH2t1aWYy4TeubFUSma/da0ZfoxOC3O9laz1C/M8+klIuXBGZXEmc0JSFBYT+KGOhG06APVWd9BZFhaLhSwZIXEx9jinv4MTTiCaGB0kryQkCosJkjkFPqqKDhGhUswlTzgjMnqOIhDhFJH3i8izIvKCiHx4zPd/RERuisiT/p8fG/neIyLyvP/nkSCuZ1IiSw4ltAg5igcfkumqH3R69CKo6ADPMEiacEYVZjuKue96EckCvwi8F7gEPCEijyulnjn01t9SSv3EoZ9dA34OuAAo4Cv+z27Pe12TEJWrntQi5CiaB8B78L2hv33y2WQ4SfWIEmvg3d9Ji6/XW/ZbnA8BLyilXlJKtYFPAA9P+LPvAz6jlNryxfIzwPsDuKaJqDU7iMBiIfxyJEhqjDOaBx+GYpMEonQ1PeFMztrBSEWHxcJ5F/DayNeX/NcO81+KyNdF5JMicm7Knw2FvWaXSjFHJsSWN0hyjDMa4dQPRz1Bw4yjdDUrxVyi1g689SvlM7F5IEF86jjVOVyw+G+Be5VS3wZ8Fvj4FD/rvVHkURG5KCIXb968OfPFjuIN+Ijgxi0ks22w1uxQKUaQVddVCQnaeKIKE3mfkU+kqx7FvXcUQQjnJeDcyNd3A1dG36CU2lRKtfwv/znwFyb92ZHf8ZhS6oJS6sLGxkYAlx3+LE5NJuNlNpOU4Oj1FY12LxqL039AkuSqawswClczia56FF1XxxGEcD4BPCAi94lIAfgg8PjoG0Tk7MiXPwh80//7p4HvE5FVEVkFvs9/LRKicjVh2D2UFKIMzuvPSNLDH1W7KnjinKR7D7xNNK74JgSQVVdKdUXkJ/AELwt8TCn1tIh8BLiolHoc+Lsi8oNAF9gCfsT/2S0R+Ud44gvwEaXU1rzXNCm1VofT1VIkn1Up5hJlMUUao0tkjDO6jWeplKfd7dPq9ijmsqF/XhRE5S0eRSCfrJT6FPCpQ6/97Mjffxr46SN+9mPAx4K4jmmpNbu8YSOaxa+WkhWgjzZGpy3O5IQ69PqFXdEBt1clFCvJEM56qxuZ0TOOZBTFzUiUrnolYXGmqLquAKp+jDNJ7mbNr+gIc4ixRteKJu3+i9NVT61wei1v0RRwQ/JKQqLqugIo5TPkMpKwBz86V1Pf40lav3qERs84UiucrW6fTk9FevMm6caNsgBZRKiUkhUjrrci9Ha0xZmQCUn9vqLe7lKNoOvqKFIrnHsRZjW9z0lW21st4pa3xK2f76pHQdKqEuptb4hxVM/uOFIrnPomiqoWrFrM0er2aXf7kXxe2EQ9ZKFSzCcu1BHVg7+UMFe9HqG3cxSpF84ok0OQnJKaWrNLPisUQx5irKmWcok6PiPqxCRAPSEWe9TP7jhSLJzRuuraLUtKnE5bTCLhZ4XBs9iTsnbghTqiDHNAgixOP1YbVahjHCkWzqhjdLqkJjm7fpQ7frWUS8zaQbSuej6boZTPJKacay/CUrijSLFwRp8c8j43GTdv1MKZpKx6p9en2elHmhWuFJNT1VGPOD8xjtQKp56NGXVJSFIefm8yUpQWp/fgK2X/SaFxJDeWElSVEPcsTkixcOohxpUIWt5gpO0tIe5SVAe1aSrFHN2+opWAqoQou640SWr51TFO56rHwF6zS6UQ/hBjTWXgqidn14/SVU/STM69CLuuNElq+a01u4hAOR9f331qhTP6Bz9Z/dbeCZcRWpwJOj4jjnKaajE5w4x180BURs84Uiyc0WU1AYo5r986CQ++UirSlkEYGfSRgPUbzDKNcIJ5NUHJtVoz3nZLSLVwRvvg637rJDz4jXaPvore1YRkxIijHJCiScq9B9EbPeNIr3C2oh+EmpQAfdSlXN5nJSdGHIurXspTb3fp9xNQldCKd6QcpFk4I84Kg66lS86DH2k5UgJd9Sgf/moxh1LQaNu/flF7i+NIuXBGbHEWk+EuxWMxJaeBYK/ZoZDLRHqMRZLWz4uvO1c9cvQQY33eeVQ4V312khXjjP6ExiQNM466+WIcqRTOqIcYa5ISoI96JB+M9FsnINRRjyNMNNh47F+/ODaew6RSOKMeYqxJyvEZcXS+QHJmcsZhMWkjwfbRfN5pnX1nccZr+WqiAAAgAElEQVSBrmeLuhasWsonopYujnIa8Cxc2x98iCe+vpSQGGdc995h0imcreizwuD9Y7d7fZqdXqSfGzS1ZpdsRigXom15S8qEpDiEs+JXJdi+fsOKBJccipw4ykEgOYM+tKsZ1RBjTVLOHaq3ugMhi4qk1MGaMP0d0iqcMdQhjn6e7bt+rRXdQWOjJCVGvBfh0cCaciFLRpKwaccTZjtMOoUz4hMaNYNjWm0XzpgKkJNwxHK/7/X5R50VFhEqCagjjqMUbhypFs7FGJJDYP/xGfVmN9LJSJpKAs4d2u/0UCqeIbzVUt76sXxxGT2HSbVwxlUSYvuuX291WSxGPwtxqZSzvt86TospCROSTJj+DmkVzoiPttUkJcbZaHVjyWpWSvb3W8eZ3KgmoAHDWZwxUveTG3FkhfXn20xcyaEktA3G1TygP9P2MNFes0MhG22f/zjSKZzNbuTxTUjO8Rn1ZpdKDK76wGK3eOPR//axVSVYvOmAGZORIKXCGZfFVMxlKWTtPt+611ccdHqR1yFCMmoR43Q1E+GqN+OfxQkpFc5GxMc+jGJ7gD6u5gFIRnKtEVNiEpJRzlWLoQZ2HKkUznpMFifYPyFpWJEQvauepBhnLKGiYpZ2r0+ra2/Lb73VjfSspqNIp3A248kKg/0zOYcWUzx1nGB3jLPR8kQrrhjn6DXYSM256vHhxTjjycrZHqCPs44uGTHODgv5LNkYjrbVxkLD4o3HhBMuIaXC2YjTVS/a3b0Rp6u+WMghYncdbJwHjel/M5tDHSYc1AYpFM5eX7HfjicrDH73i8U7/nBASvTrl8kIlYLdMznrrV5sFpP+N7O1gUApFavRM0rqhDPOrLD+XJt3/EbM67dYzFntatabnVgSQ8CgTdZWi73V7dPtq9jWb5T0CmecMc5WF6Xs7LeuxVhOA55g22oxgZeYiWvtbO9cM6XdEtIonDG6muCV1PT6imanH8vnz4tev8WIp79rFi0fjVaLMUa3aHlVwvDec8IZOSa46mBvZrjR7rKQz5LLxnPrVG131VvxHW1r+5CZuJ/dUdIrnHG5S3qYsaUPf9x1dIvFrLUWE8TrqmtLzdb1i/vZHSV9whnzmSWLgyJke2/eOG/cSjFvdQF3XANmwKtKWCzYu/HE2a56mPQJpz9WK66b13Z3Ke5ykEoxa22Yo9Xt0e71Y01uVEr2hjriOrlhHCkUzvha3kY/19Zd3xspF/OD3+5ZWZUQZ7ulZrGYszZM5LLqMRLXCZcaHR+0taSm1orP1QTvwbe1KqEe44APTdXill8T1k+TPuFsdSgX4ukVBvuLkOMcyQfD5JqNFrsJyQ2bXfVGq4sIlPPxTn+HVApnvBaTHolVtzTBEXdyyOZaRBOEc7Fgb8tvrdVlsZAjE5PRM0rqhDPu6SqlfIaMDJNUthFnVhhGR6PZ9/Drf/M4y7kqFs9KiDsxOUrqhLMR83QVEaFSzFlZUmNEVljXwVoY6og7Mak/21bhNGUyEqRQOON2NcG7eW188E3ICg+SaxY+/HEnJvVnNyydlVBv9YxIDEEKhbMWczkN2BugN+HBtznGGfdkKfDWr9NTtLo2ViV0jBhiDAEJp4i8X0SeFZEXROTDY77/kyLyjIh8XUQ+JyL3jHyvJyJP+n8eD+J6jsMEi3PRUnepFnPzANidVa8ZkBW2eUJSo9UbVKXEzdzCKSJZ4BeB7wceBD4kIg8eettXgQtKqW8DPgn8ryPfO1BKvcP/84PzXs9JxB3jBHvjTNpVjzPGabPFWW/GnxW2O7nWjW2q2WGCsDgfAl5QSr2klGoDnwAeHn2DUur3lVL7/pdfBO4O4HOnRillhMVpq3AOssIxrl+5kEXEzgffhKzwotXJtfjOCjtMEMJ5F/DayNeX/NeO4keB3xv5uiQiF0XkiyLygaN+SEQe9d938ebNmzNdaKvbp9NTsVuctk4xj/NoW42Id3yGrQ9+3K5m1VKLc2D0GJJVD+IqxvkdY1N2IvJfAxeA/2zk5fNKqSsicj/weRF5Sin14ut+oVKPAY8BXLhwYaaUoAkFyPrzbbQ4TXDVweLkWiu+Y6k1toY6Wt0+PUOOzYBgLM5LwLmRr+8Grhx+k4h8L/AzwA8qpVr6daXUFf+/LwF/ALwzgGsaiwlZYf35NpaEmOCqg73JtXor/qNtK5Ymh7SHEff6aYIQzieAB0TkPhEpAB8EbsuOi8g7gV/CE80bI6+vikjR//s68F3AMwFc01iMsThLOfoKDjp2FcHXm35WOKZjMzS2Wuxe15UZrrpt62fSSDkIwFVXSnVF5CeATwNZ4GNKqadF5CPARaXU48A/BSrA74gIwKt+Bv0twC+JSB9PxH9BKRW+cBoQ4wTvQSobcH7KpNRbPSqFHP6/YWxULW0bNCErPHrv2YRJQ4whmBgnSqlPAZ869NrPjvz9e4/4uS8Abw/iGiZhMP095pt3dNc/HeuVTEe91Yl90wFvUMX1vWbclzE1JmSFba1K0K66CfcfpKxzaGjux3vz2hqgj3uylKZSsm+mpClZ4UFVgmX3nmkWZyqFM+6b19Yp8Ca0q4KdMU6dFY7bVQc7qxJMyU9oUimccbvqtp47FPcQY40WTpuqEgaupgEF3DZuPE44Y6Te7JLNCKV8vP/bOlRg2/EZJnRdgRfq6CusOj7DhAEfmkULp3OZ4i1q0iWcrS6LhWzsWeFhLZ195UimxDhhOHTEBoYWU/yuetVCV73R6pIRWDDg2AxImXDWml2qMXdugL2uuikWp3Z3bRoGPWxXjf/Bt/H4jJq/acdt9GhSJZwNA3qFwds1M5aVhOissBkxTv/cJos2noYh8XXQySF7Nh0wY0DKKKkSTlMsJhGxrm3woNOjr8zo3BicFGrR+plSCgf6BAJ7whxgzrOrSZ1wmvDgg3++tYUPvgk37/CkUAvXzwiLPUej3bOqKsGkZxdSJpwmmfuLRbuKuAddVwY8+IOqBAuF0xRXvddXVlUlmBIm0qRKOE0y9yulnFXlSANX04De+mFW3aL1a3pZ4bhL4WBkmLFFVQkmGT2QQuE0xdy37aRLk6bT2Hj8g960TcgKD4cZ25MgMqUUTpMa4VRKGbVrLRbsqqUzyVXXVQlWhTpMuvcsLIczaf0gRcLZ7PSNyQqDP6jCIuHUYQUT1s/GqoR6M/4BHxrbZiWYclbYKKkRzmFWOP5yELCvX1h3OZlQTgP2VSU02uY8+LYdEayNHlM2HkijcBqy+LYdn2HKLFONbQfe1QyK0Q3HGtqRHNJJLFPWD1IknA2DssIwHFRhy/EZulfYhKwwWBjqMMjVHLrqttx7/iGBhqwfpEg4TSrgBvsOzdIVCSZkhcHGUIeBwmlJcqjeNCe+rkmNcDYMKqeBYazVlpvXJIsJhqEOWzCpFK6Uz5DNiDWuumlGD6RIOE2qQ4ThoApbaukabXMefLCr88q0UjgRYbGQtebec8IZI4OWN0OSQzo7bUv3Rr3VM+rGtclVN60UDuxaP5OGQGtSI5ymuepVyyzOerNjpHDaUJVgWkUH2FWVUBs8u2aUwkGKhFNnEMuGTJAejkazw+JstHpG3biVkj3HZ5hWQwxY1UBg0ixTTWqEs+Efm5HJmJMVBntKQkxKboBdgypMK4UDu47PMGlAisacKwkZ04YEDMqRLElwmNT5AnYdn2FicsOm4zNMK4WDNAln25xeYbDr+AzTssJg1/EZpsXXQcc4zd90wLxSOEiRcJq2+DYNqmh1+3R6yrAH357jM0xMDlWKWSvWDswrhYOUCadJMSawpySkYaCradNMTiNddYtmJdRbPSeccWHi4lcsKeJuDCYjmbN++lpsmKJvqqve7StaXfOrEjxv0ZyKBEiVcHaMW/zFoh3HZ5hYTmPTTEnTSuFg2AhihcXeNCvMBikSzkarZ1SMCSxy1dtaOM2po1u0yFU3rRQOhqVRNtx/ppXCQYqE08TFXyxmrXjwh9NpzLGYyvksInbUwTaMvPfsEU7TSuEgJcLZ7vZpd/tUDEsO2VISYmJyI5MRrxbRghhxvWVWKRyMJtfMv/9M3HhSIZwmBufBIlfd0PWzxmI3rBQO7DmbvtXt0ekp49YvFcJposUE9hyfYdpIPs1iMUfdguSaqaVwYL6rPqjoKJgTJoKUCOcguWGYu2RLSYi+eU3deEzHyFI4S7Lqw5Fy5iQmIS3CaajFZMuuX291WMhnyRqUFQZ7zqY3sQ7RluRQrWleKRykRDjrA4vJrMW3paTGRIsJ9IFtLrkxC7aUI2lv0bT1S4dwGnjYE4ycO2T6zWugxQQ6uWb+WDkTs+rZjLCQNz+5Zmp8PRXCaeI8RBi1OM22mky0mEBn1c1eu06vT8vAUjjQw4zNXj8T5yRASoTTtPOGNPa46qYKp/nlXKbG18HzeEy/90xdv1QIp6mLX7UkQF9vdQfXahKVQo52t0+nZ25VgqmlcODFiE0XzrqhFR2pEM56q0shlyGfNet/1xaL01xX3fz1M3GylGaxkBschGYqg/yEq+OMHhM7N8CekhBjs+oWrN8wuWHWgw921ME22l1K+Qw5w4wes64mJDyLybwbV++iJj/4YHBWvWR+cq1haHwd7Dgi2FSjJxXCWW/1jBqJpsllM5TyGaNv3l5fcdAx0+IcWuzmliSZWk4D9mTVTVy7VAinqRYT6FpEc2/e4SxO827eYR2sues3EE4Dy5FsyaqbuHapEE5Ty2nA/DhTvWmucNqRHDJ3/SrFPAedHr2+uUNmTGwegJQIp6nmPpgfZzK1lAvsaBs0ev0s6FxzMc4Yqbe6RnZugPlF3EbXIVpgcdZbPQrZDIWceY+aDevXMLSiw7x/zRBoGGrug++qGzxT0ug6RCsefHPvPRvWr25ofiLxwtnvKxptM3ct8C1Og49/MNniLOQ8S87kIu66oaVwYEcdrEsOxcQwK2zqzZu1IitsonCCBck1Qx98MH/ITL+v2DfU6AlEOEXk/SLyrIi8ICIfHvP9ooj8lv/9L4nIvSPf+2n/9WdF5H1BXM8oJruaYP4w3mFyw8yNx/QJSQ1DkxtgvsVpcinc3MIpIlngF4HvBx4EPiQiDx56248C20qpNwIfBf6J/7MPAh8E3gq8H/i//d8XGMZbTKWc0SUhJhdwg7fxmPrgg9kVHaYL5+DZNTBGHITF+RDwglLqJaVUG/gE8PCh9zwMfNz/+yeB7xER8V//hFKqpZR6GXjB/32BYXIdHYxkNg1NEDVaXXIZoWhgVhjscNVNfPDB/JMuTS7lCuJpuAt4beTrS/5rY9+jlOoCu8CpCX8WABF5VEQuisjFmzdvTnxxpo7e15ie2dRZYW+fMw/T62BNL4UDky1OM4+8gWCEc9wTddjvPOo9k/ys96JSjymlLiilLmxsbEx8cX/xDes8//Pfz7vuXZv4Z6LEdOGsGZzcAM+NMzmrbmodIkAxlyGXEWPvPVNPboBghPMScG7k67uBK0e9R0RywDKwNeHPzk0+mzHuhEaN6f3WJic3wBtmbOqDr5Si0TazDhFARIy22E2OrwchnE8AD4jIfSJSwEv2PH7oPY8Dj/h//yHg80op5b/+QT/rfh/wAPDlAK7JGgZtg4bWcnoWk5kPPmhX3cxNZ7/dQykzH3yNyUNmTM5PzH1FSqmuiPwE8GkgC3xMKfW0iHwEuKiUehz4FeBfiMgLeJbmB/2ffVpEfht4BugCf1spZea/YkiYH2fqGjlLUlMpZmm0uyiljIvDmpzc0Jh8UmjD4Kx6IFeklPoU8KlDr/3syN+bwA8f8bM/D/x8ENdhI9WS2THORqvL2eVS3JdxJIvFHEphZKG0qYcEjmJyHWzNYIvTzBqTFLFoeDmSqdNpNCYn10yexakxechMo9Ula2gpnHlXlDJsKEI2zZIbRVtzJq6fyckNjcl1sI1Wj8VC1rgQDDjhjJ1izsv4m3jzKqWMz6qbPJOzYejRtqOYnlU3de2ccMaMiLBYMDPO1Oz06RueFTY5uWZ6nz/o5JB5awdmt6s64TSASjFHzcBypJqfbTUxq6kZDuM1b+MxfU4CDIXTqw40C5PbVZ1wGkClZKa71DC45U1jcr+1yeU0msVijr7yvAvTcK6641gWDZ0Cb3LLm8bk5Fq91SUjsJA3d+OpGHzukKlDjMEJpxGYGmeywtU0uA5WDzE2MSusMbmcy+Q+fyecBmDqMOPB0cAGu5oL+SwZMddiMnntwHyL3dQwkRNOAzC139r0kXygqxLMtdhNXjswVzh1KZyp6+eE0wC8c4fMunHBDlcdzK1FrBvsampMddVb3T7dvjLWYnfCaQD6wTetJMSGIRVgbr91w2BXU1MxtPPK9E3bCacBVEo5un1Fq2tWSUi92UUEygZnhQEqpbxxDz6YP8sUzHXVTa/ocMJpAKbevPVWj8VCjoyhQ6A1poY6ak1zY3QaU111kw9qAyecRqB3VdNuXi84b7a1CeZWJXjT38188DXlfBYR804gGFR0GLp+TjgNwNR+63rbfIsJPKvEtLWzYUAKQCbjVyUY1vJrekWHE04DMLXfut40/8EHMxsIWt0+nZ4y9sEfxUuumbV+dcMnSznhNABTu19ssJjAzHIkk8/LOcxiMUfdsJZf56o7TkSXrJh2zK0NBdzgPVydnqLVNcdi196DDetXLRroqhs+ks8JpwGYmtm0IbkBI1UJBj38ptchjmKixW76sSNOOA2gYqhw1puWZNUNjBHbJpymxYi9ASlZY0vhnHAagN5VTRtm3Gj1qBTzcV/GiZhYB2u6qzlK1cCxhib3qYMTTiPwSkLMymy2u33avb7xLYNgpnDacDSwZtHAGKfJ09/BCacxmOYu2dKnDmZOgbfhhEuNidO5TJ7+Dk44jcG0WkSbHnwTjwi2aeOplnK0e33DqhLMnf4OTjiNwbRzh2xLboBZwml6VniUxYK22M0Rznqr51x1x8mYNozXtgJuMMtVb7S6lAtZsoZmhUcxcf3qrY7R954TTkPw+q1N2vHtcTW1VWfSxmNL8wCYmVzzzhsyNzHphNMQvBhnJ+7LGGCTq57NCOVC1qjMcL3Vs2LtwMxhxl5yyNxSOCechmDaFHOb6hDBvCOWbenzB/NixO1un3bX7FI4J5yGUCmaNcVchw2qBu/6o1SLhoU6LOm6AvM612yoSHDCaQiVYnaw05qAjRZnvWlWqMMWi9M04bQhTOSE0xBMvHmLuQy5rB23iHGhDkuGQMPQsjOl5dcJp2NiTIsz2WQxgXmhDqtinIbVcTpX3TExppWENAzvFT6MaQe21SyZng+Qy2ZYyGeNSa6ZflAbOOE0BtOmwJve8nYYk2ZKdnt9Wt2+0RbTYRaLOeeqT4ETTkMYxJkMefhtspjArAPbGoaflzOOikHnDjlX3TExVcOSQ15yw46MOkClkPMPSIu/KqHmNzLYJJwmWeymH9QGTjiNYZAcMsRd8lrezL1xD2NSv7VN5w1pKsWcMd6OfgZ00spEnHAagolZdRuG8Gp0jNiEOF3dshpY8ITThE0HPG+nlDe7FM7cK0sZpp2tbltyaLB+BmSGtQDZtPGY5aqb3acOTjiNIZsRFvJZIwZ99PqK/bZdrrpJDQQ2TZbSmDSdq97sGt2nDk44jcKUm1dbbSYH5w9jUveLTUOMNSZN5zL9oDZwwmkUphyf0bCgAPkwJoU6rHTVCzmanT5dA6oSbOhac8JpEIuG1NLZUEd3GJMaCAZZYRvXrx3/xuOE0zEVplic2t01Pc40SkWfTW/A+tXbXQq5DHmDs8KH0f/WJtx/zlV3TEXFkPOtB3WIFsXoTDoi2KYBHxqT6mBNP6gNnHAaRcWQKeY2ZoVz2QylfMaIB79h0bEZGpPqiE0/qA2ccBrFojEWp31ZdTCn+6XWNN/VPIxu+Y37/uv2+jQ7feO9HSecBmFKjLNuYVYYzCni9lx1e+LDYI6rrpNTpnddOeE0iErRjEEVNrrqYE7bYKNtX4zTlHmwtmzaTjgNwpRdv9bsks8KxZxdt4cpMyXrFrrqpnRe2VIKZ9eTkXBM2fV1VlhEYr2OaTEpuWabxWlKcsgWb2cu4RSRNRH5jIg87/93dcx73iEifyIiT4vI10Xkb4x879dE5GURedL/8455rsd2dAmGCTev6eUg4/Bc9fgLuG2oQzxMIZehkM3E3vI76LoyfP3mtTg/DHxOKfUA8Dn/68PsA39LKfVW4P3A/yEiKyPf/wdKqXf4f56c83qsxiRX3fTpNOMwwVXv9RWNtn3lSOBt3HHfe7Z0Xc0rnA8DH/f//nHgA4ffoJR6Tin1vP/3K8ANYGPOz00kQ1c93l2/3uoYv+OPw4TjH3SowPTkxjgWDTjwzobzhmB+4TyjlLoK4P/39HFvFpGHgALw4sjLP++78B8VkeIxP/uoiFwUkYs3b96c87LNpGJILZ29rnqeg06PXl/Fdg221sCCGUcs27J+JwqniHxWRL4x5s/D03yQiJwF/gXw3yqldL3NTwN/DngXsAb81FE/r5R6TCl1QSl1YWMjmQarKW2DdcsOatMsGtBvrTc9GzeeqgENGLYkh068OqXU9x71PRG5LiJnlVJXfWG8ccT7loB/D/xDpdQXR373Vf+vLRH5VeDvT3X1CaPqxxXj7n6pW5jcgNtLapYX4onR1iyxmMZRKeW4WWvFeg31Vo9CNkPB8FK4ea/uceAR/++PAL97+A0iUgD+DfDrSqnfOfS9s/5/BS8++o05r8dqTLE4a027zhvSmDBaTltsVq6fAZ1r9VbHirWbVzh/AXiviDwPvNf/GhG5ICK/7L/nrwPvAX5kTNnRb4rIU8BTwDrwj+e8HqsxYVBFp9en1e1baTGZcDb9MLlhX1VCpRR/VUK9aUd8fa4rVEptAt8z5vWLwI/5f/8N4DeO+Pnvnufzk0jcgypsCc6Pw4Sz6a2PccZ8fIYtzQNmBxJSSNyDKmoWP/gmHBFsdYyz6B2fEeeshJoliUknnIYR96CKuiWdG+MwoZxrYHHauH4mxIhbdsTXnXAaRtzdL4MYnQU372GqpfirEuqtDuVClmzGrj5/GIp93PefDZuOE07DqMac2bTaYjLB4rTkwR9H1YBZCbYkh5xwGkbc/cI2x+iyGaFcyMaa4KhZ8uCPQ1cCxCmctZYdcxKccBpG3LV0NmeFwYD1a3WtjA/DyHSumCz2VrdHu9t3MU7H9FRKOfZidDVtLkeC+GsRbXE1x1GJuQ5WjwS04d5zwmkYS6U87W6fVjeeCUn6oTH9sKyjiD1GnIQYZ0wbT63phVhsWD8nnIYx7LeORzj1gI+MhVlhiN/itHWWKYyONYwnRmxTDbETTsOIOzNsw5nWx1GJecKPLXWI4ygXsojEee/ZU0PshNMw9G6714xn17d1FqemWopvpqRSympXXURibfkdDkgx32J3wmkYcdfS1Sw8oXGUSjE3iJVFTbPTp9dXdm88MVrsNjVfOOE0DD2TM86b1wZX6SiqJS85pFT0U+BrLXuSG0dRKcWXXLOphtgJp2HEfdJlw2JXE7yHrq/goBN9cs3mWZyaOOtgbVo/J5yGMewXjinGaXEdIsRbxG3LQWPHUSnlY6tKqLc65DJC0fDp7+CE0zj0bhtXgL6WAIsT4lk/m/v8NXHWwepN2zsQwmyccBpGMZchn5VYLCadFbbBVTqKwcYTw/rVLEpuHEWc5Vw2bdpOOA1Dl4TEsevvt3soZbfFVIkxuTaI0VlaAA/xJodsOl3VCaeBxNX9YlM5yFHE2f2SlPWrt7r0Yzib3iZvxwmngVSK8QToa0mI0cXoqg/PBM9G/tlBodev0Y5n/Wy595xwGohXixijxWTJzTuOOBsIas0uhVyGYs5e4Rxa7PGsX8WCriFwwmkkcWU2bR8pB8MjguMpR+pY3TwA8ZZz2XJQGzjhNJK4Ypw2Tac5irx/Nn0cG0/d8nZViLmcq9VxMU7H7MRVEjKcTmOHu3QUlWI+pgffHovpKOKaydnp9Wl2+tZY7E44DaRaiunB14NkLdn1j6Iao8Vu+9rFde5Qw7KKBCecBlIt5WKZAp+ErDBoiz2e5JotFtNRVAZVCdGun20VHU44DSSuYca1VpdC1u6sMMQ3qML2WaYQ39nqgzCRJevnhNNA4ioJsX3AhyYuV92mzpejiO3eG1R02BFfd8JpIHEVcSchuQHxtQ0mweIcnE0f9b1nWUWHE04DiWsmp+2zODVx1MF6MWl7ssLHEUeow6YhxuCE00jimgKfhKww+BZnM9op8EloHtBUStGfO2TTEGNwwmkkg8xmxG2XScgKgxcn6/YVrW4/ss8cDviwI0Z3HHGcO1S37NgRJ5wGElcRchJidDBaUhPd+tlWTnMcccSI680uIt4RxTbghNNA4mp7S0JWGIbnckdZi2hbOc1xxNG5tuffezZMfwcnnEYS1xR4m4YsHEccJTW2uZrHsRhDcsi2MJETTgPRU+CjdDWbnR7tXp+lhQTE6GIIdSRhQIqmGsPZ9LbVEDvhNJRqKR/prl+zLKt5HMPkWpQWpx6Qkoz1i/psettqiJ1wGkrUFqe2MJIgnHGUc9lWwH0clWI+8rPpa60uVYsqEpxwGkol4inwA4vTkpa344ijgaDe6pIRWMjbkRU+jjiGGdebHas2HSechlKN2OLcS5DFqac7RSmcewcdqqW8NVnh46jGUNXhkkOOQIi6lk6LdBKSQ8VclkIuE3kdZxI2HYhnQpJtpXBOOA2lWoq2li5JMU6IPjO817QrRnccUVcl9PqKRrvnXHXH/ER9/MMwq56Mhz9qi32v2WHJogf/OLTXsRfRxqOPInYWp2Nuop4Cv5eglkGIw2JPnsUZlcVuYymcE05DiXoK/N5Bh0oxRzZjf3ID/HKuSC32BFqcB9GsnxboJYs2HiechlKNuKSm1uwm5sEHP9QRZVXCQScRiTWASiGHSHQWpxZomyx2J5yGEnVms9bsWHXjngLRYoUAACAASURBVMTSQnTJoX5feeU0Cdl4Mhmv5XcvQm8HvH8zW3DCaShRj0ZLUjkNeG6ffiDDptHu0ld2xehOYqmUjyw5pOfO2rRxO+E0lGrE51vXWp2EPfhejLPfD7/felADa9GDfxLVUi6yGKf+HJtCRU44DSXqzObeQXKywuAlOJQalrqESdJKucDbBKLLqjuL0xEQUZ90WWt2rIoxnYRevyjidNqlTdL6eTHiiCzOZpdSPkMhZ48c2XOlKUPvvlHs+kqpRNUhwtBtjiLOaaPFdBLVKGOczY51YY65hFNE1kTkMyLyvP/f1SPe1xORJ/0/j4+8fp+IfMn/+d8SkcI815MkCrkMC/lsJBZTs9On21fJinEu6I0nAovzwL4C7pOoliK0OA/sS0zOa3F+GPicUuoB4HP+1+M4UEq9w//zgyOv/xPgo/7PbwM/Ouf1JIqlhZyzmGZk4KpHuH62WU3HoWOcUQwz3mvaVwM7r3A+DHzc//vHgQ9M+oPizd/6buCTs/x8GoiqJGQQo7Ns1z+Ogaseyfol0+LsK2i0w2/5tXFAyrzCeUYpdRXA/+/pI95XEpGLIvJFEdHieArYUUppf+AScNec15MolhbykZSE7CWwnCZSV73ZoZDNUErAEGPNsO0yAov9wL521ROvVkQ+C9wx5ls/M8XnnFdKXRGR+4HPi8hTwN6Y9x3pF4jIo8CjAOfPn5/io+2lWsqx1WiH/jk2Dlk4iWhd9W6iMuoQbVXHXrNrnat+4r+2Uup7j/qeiFwXkbNKqasicha4ccTvuOL/9yUR+QPgncC/AlZEJOdbnXcDV465jseAxwAuXLgQ3SlSMbJUyvOtW43QPyeJMc58VifXInDVD5LVrgpRhzrsa76Y11V/HHjE//sjwO8efoOIrIpI0f/7OvBdwDPKizr/PvBDx/18mllaiKZfOIkWJ0RXi5i0dlWIrgGj2enR7vatCxPNK5y/ALxXRJ4H3ut/jYhcEJFf9t/zFuCiiHwNTyh/QSn1jP+9nwJ+UkRewIt5/sqc15ModL912JlN7c4m7+GPJrlmYx3iSUQ1Wm7YrmrXvTfX1SqlNoHvGfP6ReDH/L9/AXj7ET//EvDQPNeQZJYW8nT7imanz0IhvMRDremd0LhYsOvmPYmliPqt95pdziyVQv+cKInK4hx2Xdm18bjOIYOJKs5Ua3pDjDMJGWKsWVqIpt86kRbn4N6LyuK0a/2ccBqMztSGnRlOWrulxnPVXYxzFkr5LIVsJvRN29YwkRNOg4nK4txL4IMP2lUPd+06vT777V4iNx6vcy3cjce56o7AGdYihn/z2uYqTYLnqndDTa7pM6GSVscJnsUedqjD1ooOJ5wGE9UxrUks4Ab/pNBen1a3H9pn7CWwBlazVAq/HG5wbIZl6+eE02CiGo2WtPOGNFGsn60W0yREZXFmM0I5xKqRMHDCaTBRDeNNYnIDorHY9xI4GUkTxWg53TXkzfyxByecBlPKZynmMqFaTN4QY/ta3iYhio0nibM4NVEceOcdS23fpuOE03CWFsItqWm0e/4JjfbdvCcRjavuLM558Pr87dt0nHAajhegdw/+LCwvRGBxJjirvrSQ56DTo9MLN7lm473nhNNwvJmcLrkxC1Gc26R/d6WYxPULf7ScrfF1J5yGsxRy98twpJx9N+9JDF31cB/8xUKWXDZ5j1IUoY69A/uOzQAnnMZTLeWohXrjJu9McE0pnyGflXCz6gmcxamJyuJ0rrojcLzkUBTlNMmzOEUk9FpEW13NSQi7nKvXV9Radq6fE07D8UpCwmsbHEynsdBdmoSwR8vZeELjpIQ9Wq5u8b3nhNNwlhbCbRtMcnIIwrfYE21xhhwj3rM4vu6E03DCDtDvNTvkMsJCgk5oHCXsWsSktqtC+NO5bO66csJpOMM4UzgP/+5Bh+WFvHUtb5MSdvfLXrObyPgwQCXkzitbj80AJ5zGszS4ecN5+Hf3OyyX7dvxJ2UpxHOHhu2qyVy/bEaoFHOhxTgHk5FcjNMRNMNDs0ISTt/iTCphuurNTp9OT1kZo5uUMJNrg64rCzceJ5yGsxSyu7Rz0GYlwcK5tJBnvx1O2+CgXTXB6xdmOZfNzRdOOA0n7OTQzn6HlXIhlN9tAkshFnHv+P8myd54wpuVYPNkKSechhN2EXLyXfXw+tV39n3hTHiMeDckV73W7FC2tF3VvitOGcVcxjttMISbt9dX1JrdRAvnMEYcgsW53wZgZSG5FvtyObyqBFsnI4ETTuMRkdDcJf1AJFo4Q6xKGLjqCbY4VxYKgw0iaPYO7G0ecMJpAWHVIqbhwQ/TVd/1XfUkl3OtlPM02j3aIXSu7Ry0WbU0vu6E0wKqIU2BH7iaCX7wtajpeGSQ7By0yWaEagJncWpW/fXbDWPjtriG2AmnBSyVwilC3k2Bq64f/J0QHvykd10BLPsW4e5B8O76zn5n8O9jG044LSCsKfBD4bTTXZqEhXyWQjYTjsW530l0KRIMS63CsthtLYVzwmkBS6VcKCUhabA4RYTlcj6UBMfugb2u5qSshBTqaHZ6NDt9a+89J5wWoC3OoGdy6ofB1pt3UlbLeWdxzogutQo61LFreWLSCacFrJYLtHt99tu9QH/v7kGHxUKWQi7Zt8HKQoGdMGJ0FruakzJMrgW7ftv+73NZdUdo6AD6dsA3785+sruGNCshWpxJX79qMUdGgnfVB11Xlq6fE04L0FZN0Dfv7kF7kDVNMmEIZ7fXp9bsWutqTkomIywv5AO32Hcsr4F1wmkBq6EJZ4flheTWIGpWy4XArXVdV2urxTQNK+VCKJs2OFfdESIrIbrqSe6z1iyX87S6fZqd4GLEw+aB5K/fSjkfeAH8tuUDUpxwWsBKSAH63YOOtTfuNGirJsiNR2eZbXU1p2FlIfhQx85+h0I2Y+1ZV044LUBbhdsB3rxKKXYSPlJOE0YR967lyY1pWCkHX5Xgxdft7bpywmkBhVyGSjEX6IPf7PRpd/upsJjC6FfXQpIGV305BItzu2FvuyU44bSGlYC7X9LQNaQZJtcCXL9UWZx5as0u3QCPH/GObLF303HCaQlBZ4YHFpPFN++krIQw6GPH4hMap2VlcApBcG2/Nk9GAiec1rBSzgca49y1PKs5DaEkh/Y7VEs5shk7Y3TTsBKGxX7gXHVHBHi1dCFkhVNgMZXyWYq5zGCzCIK0VCTASIw4QIt9e9/udlUnnJawGpLFmQbhhBBCHft2x+imQbvqQW08tk9GAiec1rBSLrDX7NDrBzMhyfbpNNMSdNvlTooszkFyLaCSpCTce044LWG1nEep4M5X18c+VBJ87MMoXr91sBa7zRbTNAQ9k1P/HlvbLcEJpzUE3XaZhmMfRlkNIUZss8U0DdVSHglwQtL24Fhle9fPCaclrJSD7R5Kw0i0UYJ01ft9laoYZzYjLJWCqyO2fTISOOG0hqCLuHdT0m6p0RN+gpiiX2936Su7Y3TTslIOLtRh+2QkcMJpDasBx5nSVE4D3oPf7vU5CGBCUtoqEiDYQR87CaghdsJpCSsBF3GnzVUfTtGf/+EfPvj2WkzTslwuBGZxbls+GQmccFrDkt+lEqjFmSLh1EcgBxHqGA74SM/6rSzk2Q0sTGT3ZCRwwmkNIt4RBkFYnP2+Yq+ZTosziCLuQR1iitYvyBjnzr7d7ZYwp3CKyJqIfEZEnvf/uzrmPf+5iDw58qcpIh/wv/drIvLyyPfeMc/1JJ2gMsN7zQ5KkYrzhjRBViUkISs8LSsL3hT4fgANGNsJqEiY1+L8MPA5pdQDwOf8r29DKfX7Sql3KKXeAXw3sA/8h5G3/AP9faXUk3NeT6JZDWig7GbD+x3rFbtv3mkYTkiaf/3SNJJPs1IuoBTUApiQZPtkJJhfOB8GPu7//ePAB054/w8Bv6eU2p/zc1PJajnPdmN+i2mz7onHqcXi3L/LFpYDnAK/s9+mXMhSzNmb3JiWoDeeVLvqwBml1FUA/7+nT3j/B4F/eei1nxeRr4vIR0UkPU/yDAQ1IWmz3gLgVIoszlI+y0I+G0xyaD9diTUItu1yZ79jfUXCiY3KIvJZ4I4x3/qZaT5IRM4Cbwc+PfLyTwPXgALwGPBTwEeO+PlHgUcBzp8/P81HJ4agJiTd8l31NAknBLd+Ww27R6LNwqAqYc4EUbPT46DTsz7McaJwKqW+96jvich1ETmrlLrqC+ONY37VXwf+jVJqsPLaWgVaIvKrwN8/5joewxNXLly4EMyIIMtYKRc46PRodnqU5qiB0xbnWtoe/oDOB79Vb7FeTZdzNKiDbcxnsSdhMhLM76o/Djzi//0R4HePee+HOOSm+2KLeAVdHwC+Mef1JBp9s817xvVmvc1qOU8um65qtNVyftDuNw+36u1UJdYATlW8jeKWv+nOShImI8H8wvkLwHtF5Hngvf7XiMgFEfll/SYRuRc4B/zhoZ//TRF5CngKWAf+8ZzXk2iCOgJis9EaPAhpIojjR5RS3Kq32EjZ+i2VchSyGW7V57v3thr2T0aCCVz141BKbQLfM+b1i8CPjXz9LeCuMe/77nk+P20MRsvNmVm/VW9zatHuHX8Wgkiu1VtdWt1+6uLDIsJ6pTC3xal/fsPyUEe6fDXLCWpC0ma9xXrKLCaA9cUCW432XFP0dSlXKtevWpxbOG/WvJ+3ff2ccFrEakDdL5uNduosJvCsnL4auouzcGtQymX3gz8LpxaDsThzGbE+q+6E0yKCmALf6fXZ2e+kqvhdo60cbfXMwq16+rquNOuVIrdq83k7t3xvJ2P5scpOOC2ilM9SKeYG7uIsbKe0hhOGcbV5rKZBjC6FFud6tchmozXXMOibtRbrVfvvPSeclrFRLXJzrgc/3RYTzGtxej+7msLk2nqlSKen5iqH80q57N90nHBaxkalyI295sw/v9lIb4wuCItT18DmU1YDC8PNdl6L3QmnI3I2luazOIcDPtJnMS0Wcyzks3NbnGncdGAYnrg5Y5xzUANreSkSOOG0jo1KMRBXM7UP/9yhjlYqwxzAoM10Votz96BDp6ecxemIno1qkVqzS3PGQ8c2G23yWWGpNFfvg7VszFmLuJmQGN0saC9lc8b10+uehI3HCadlnK7Ol+DYrLc4tVi0+ryXeVivFOay2G8mJEY3C6vlAtmMzNx2qV38JFQkOOG0DB0fulGbLUG0WU9n8bvGszhne/CbnR61ZjcRFtMsZDLC2hxF8DcT0m4JTjitY2NOi/NWo53a+CZ4JTVbjTadXn/qn90a1MCme/1mFc5bCWm3BCec1jGvcG7WW6ynMKOu0es3SxPBMEZn/4M/K+uVAjdntNiT0m4JTjit49RikYzAjZmFM92u+voccyU3U9w8oNmoFAeW47TcrCWj3RKccFpHNiOcmrEkab/d5aDTS7WrOY/FftNZnIMJSbO0XXqT85Ox6TjhtJBZaznTXPyuGRRxz2BxDmtg07t+65UCrW6femv6Y4KT0m4JTjit5PRScSZX3cXo5utX36x7xwKXC+msgYXhkdKzVCZoVz0JOOG0kLktzhRbTAuFLNVibqb1S0qf9TzM2j2klGKzkYx2S3DCaSW6+6U/5STzNA/4GGXWSeZpbrfUDAZ9TLnxJKndEpxwWsnpapFuX0090PiWi3EC81nsad90dIz41pRT9IdHZiTj3nPCaSEb1RIwfYJjs96mUszNdSZ7Elivztb94lx1WFssIDK9xTnoGkrI+jnhtJBZS2pu1lupjm9qZrE4e33FViN956kfJpfNsFqefuPR3o6LcTpiQw/6uLE33c17eXufO5cXwrgkq1ivFNmbcsLU9n6bvkp3RYJmlmOCk9RuCU44rWRgcU55817eOeCuVSecg7bLKeJ0roZzyKnF6Qel3ExQuyU44bSSxWKOcmG6Sebtbp8btRZ3rTjhnKWW8+quN43qjqVSKNdkE7NUJdxKULslOOG0lo3qdEXwV3cPUApncTJy9tAU63dp+wCAc2vlUK7JJu5YKnJ1tzlVOVyS2i3BCae1nK4WuTnFTM7L/oN/t7M4Zwp1XNrap5DNJCYrPA/n1sq0u/2p1u/6XitRa+eE01KmtTgv73jC6SzOoat+fYrTQi9te/HhpLia83Bu1bO6L23vT/R+pRSvbe0nylp3wmkp05bUXN45QATOuqw6hVyGO5ZKvLo12YMPnkjc7TYdAM6teevw2tbBRO/fPehQa3U574TTETenl0rUml0O2pOV1FzePuB0tUgh5/7JAc6fKvPaFML52vYBd68m58GfB70Ok66fFlhncTpiR2fHJ3WXLu8ccKeLbw44v1bmlc3J1q7R6rLVaDuL06eUz7JRLfLahPeetuzPJWjjccJpKfeuLwLw8q3GRO+/vHPgSpFGuGetzI1aayKLXceHk2Qxzcu51YWJXfWBcK4l5/5zwmkp957yHuJJrKZ+X3F1p+kSQyOc99dvEqtJu6TO4hxybq08lcW5tligWkpG8Ts44bSWlXKBlXKeb22ebHHerLdo9/quFGkEnaiYZOPRNZxOOIecWy1zdbdJd4LTQpOWUQcnnFZzz6nFiYRTP/jO4hxyzykv1DFJZv3S9j7FnKvhHOXc2gK9vhp0VB3Hq1v7icqogxNOq7nvVJlv3Tr5wR/UcK4k6+adh9Vynkoxx6sTbDyvbR1w9+oCIq6GU3Nuwsx6t9fn8s4B5xMU3wQnnFZzz6lFruwenDjl57KzOF+HiHB+rTyZxbmz70qRDqFd75PinFd3m/T6ylmcDnO4b30RpU4uSbq8s8/ygmdhOYbcc6rMKxO56geJyggHwdnlEtmMnJhZH2bUnXA6DOEePzP88gnu+pWdpitFGsP5tTKXtg7oHTOsotbssLPfcRbnIXLZDGeXSydanFo4ncXpMIb7/FrOV06I013ednM4x3H+VJl2r39sz7rLqB/NudWTu69e3donl5HEtfo64bSYlXKB5YX8sUXwSilX/H4Ek5QkDcbJOYvzdZxbW+C17ZNd9btWF8gmbDiKE07LuXd98dgHf++gS73VdcI5hnvWPIv9OKvJFb8fzbnVMjdrrWOTk68lsBQJnHBaz72nysfWcr7qHvwjuXPFS3C8snX0+l3aPmAhn2Ut5Ucqj+PutZPnJbyawOJ3cMJpPfecWuTKzgGt7vhd/2uXdgB4213LUV6WFeSyGe5aWTjBVd/n3Jqr4RzHsJZzvLu+5yfWnMXpMI771sv01dE371df3WG9UnAW5xHcc8J4ueeu17jX7zJy3I62JI+qhX0toRl1cMJpPbp18FtHJIi++to27zi36iymIzi3dnQt541ak29t7nPh3tWIr8oOTleLnFos8LXXdsZ+X1vySUysOeG0nPu0cI6Jc+7st3npZoN3nl+J+rKs4f71RXb2O2NLkp54eRuAh+47FfVlWYGI8O771/jiS5so9fpa2C+/vEUpn+GBM5UYri5cnHBazko5z1Ipx4s3Xy+cT/qWgBPOo/nON3ii+EfP33rd95741hYL+SxvvXMp6suyhu+4/xRXdpuDsq1RvvDiLd517xqlfDaGKwsXJ5yWIyJcuHeNP3r+5ut2/a++ukNG4NvudsJ5FG+5Y4n1SpE/ev7m67735Ze3+PP3rJDPusfkKN7tW+N/8tLmba/f2Gvy3PU63/XG9TguK3TcHZEA3vvgGS5tH/Bn12q3vf7V13Z405mq61E/hkxG+MsPrPNHz9+67Zzw3YMO37y2x7vuXYvx6szngdMV1hYLfPGQcH7hRe/r73qDE06HoXzPW04jAp955vrgtX5f8eSr27zzvEtsnMR73rTOVqPNM1f3Bq/96SvbKAUPOeE8lkxGePd9a3zppa3bXv/jF26xUs7zYELDHE44E8Dpaol3nFu5TThfutVgr9l18c0J+Etv3ADgD58buutf/tYWuYy4jWcC3n3fGpd3DgblR0opvvDCLb7z/lOJa7XUOOFMCO998AxPXd7l6q4XpP/qq15G+M874TyRjWqRB88u3RbnfOLlLd5+9zILheQlNoLmO/wEm3bXX77V4MpuM7HxTZhTOEXkh0XkaRHpi8iFY973fhF5VkReEJEPj7x+n4h8SUSeF5HfEhHX1zYj3/fgGQA+61udf/rqNtVSjvvXk1cKEgbvedMGX3llm0arS7PT4+uXdp2bPiFvOl1ltZznSy977vp/0vFNJ5xH8g3grwH/8ag3iEgW+EXg+4EHgQ+JyIP+t/8J8FGl1APANvCjc15PannDRoX71xf5909d5R//u2f4xBOv8ZfeuE4moa5S0LzngXU6PcWv/PHL/M//9mnavb5LDE1IJiM8dN8af/jcTX7zS6/wb792hbtWFgYnsSaRuYRTKfVNpdSzJ7ztIeAFpdRLSqk28AngYfFaWb4b+KT/vo8DH5jnetKMiPDeB8/wxZe2+OU/fpn/6t3n+d9++Nvjvixr+Av3rlIuZPnfP/Mc/+orl3nfW88k2mIKmr/6bXeys9/mZ/7NN/jyy1u8500bie5Wi6JO5S7gtZGvLwHvBk4BO0qp7sjrd0VwPYnlb7zrHN+4sst/95438J43bcR9OVZRzGX5lUfeRaPV5TvfcIpFV8I1FT/w7XfyV95+lhu1Jld3m7zpTDXuSwqVE+8OEfkscMeYb/2MUup3J/iMcduOOub1o67jUeBRgPPnz0/wsenj/o0Kv/lj3xH3ZViL7iJyzEbWn/SetGnv4zhROJVS3zvnZ1wCzo18fTdwBbgFrIhIzrc69etHXcdjwGMAFy5cOPqQGIfD4QiZKMqRngAe8DPoBeCDwOPK6w/8feCH/Pc9AkxiwTocDkeszFuO9F+IyCXgO4F/LyKf9l+/U0Q+BeBbkz8BfBr4JvDbSqmn/V/xU8BPisgLeDHPX5nnehwOhyMKZNw4KNO5cOGCunjxYtyX4XA4EoaIfEUpdWRNusZ1DjkcDseUOOF0OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFPihNPhcDimxAmnw+FwTIkTTofD4ZgSJ5wOh8MxJU44HQ6HY0qccDocDseUOOF0OByOKXHC6XA4HFNi5ZlDInITeGXKH1vHO5I4DuL87LR/fpr/3+P+fBv/3+9RSv3/7Z1dqBVVFMd/f1KLSvSafZgKZURQD9VFxL5EMEwltCLECJIMQkrIhyBBEOnNoh6KKPqQLKQufVgSSkoFPWmZXL/QvFcRMm9XyNCih7JWD7NPDOPMuTPn7HOsc9YPDrPP3mvPf9as2Yu995zLvXwko/9l4mwESbvK/BOmTtPudv1u9v1863ey775UdxzHqYgnTsdxnIp0U+J8vUu1u12/m30/3/od63vX7HE6juPEoptmnI7jOFHoqMQpaZ6k7yUNSlqV036hpL7QvlPSNRG1p0r6StJBSQckPZVjM1vSaUn94bMmln44/zFJ+8K5d+W0S9JLwf+9knoj6d6Q8qlf0hlJKzM2UX2XtF7SSUn7U3UTJG2XNBCOPQV9lwabAUlLI+o/L+lQuLebJI0v6Fs3Tk3or5X0Y+oeLyjoW3ecNKjdl9I9Jqm/oG8M33PHWjvjj5l1xAe4ADgCTAPGAHuAGzM2TwCvhfISoC+i/iSgN5THAodz9GcDn7XwHhwDJtZpXwBsBQTMBHa2KA4/kfwermW+A7OAXmB/qu45YFUorwLW5fSbABwNx55Q7omkPxcYFcrr8vTLxKkJ/bXA0yXiU3ecNKKdaX8BWNNC33PHWjvj30kzzhnAoJkdNbM/gPeBRRmbRcCGUP4QmCNJMcTNbMjMdofyr8BBYHKMc0dkEfCOJewAxkuaFFljDnDEzKr+gUIlzOxr4FSmOh3fDcB9OV3vAbab2Skz+wXYDsyLoW9m28zsbPi6A5hS9bzN6JekzDhpWDuMp8XAew1cW1n9orHWtvh3UuKcDPyQ+n6ccxPXvzbhAT8NXBb7QsIWwK3Azpzm2yTtkbRV0k2RpQ3YJuk7SY/ntJe5R82yhOJB00rfAa40syFIBhdwRY5NO+4BwDKS2X0eI8WpGVaErYL1BUvVVvt/FzBsZgMF7VF9z4y1tsW/kxJn3swx+5OBMjbNXYR0KfARsNLMzmSad5MsYW8GXgY+iakN3GFmvcB84ElJs7KXl9Mnmv+SxgALgQ9ymlvte1na8QysBs4CGwtMRopTo7wKXAfcAgyRLJnPubycupj+P0T92WY030cYa4Xdcuoq+99JifM4MDX1fQpwoshG0ihgHI0td3KRNJokkBvN7ONsu5mdMbPfQnkLMFrSxFj6ZnYiHE8Cm0iWZWnK3KNmmA/sNrPhnGtrqe+B4drWQziezLFp6T0ILxvuBR62sKmWpUScGsLMhs3sLzP7G3ij4Lwt8z+MqQeAvjrXGMX3grHWtvh3UuL8Frhe0rVh5rME2Jyx2QzU3qI9CHxZ9HBXJeztvAUcNLMXC2yuqu2pSppBcv9/jqR/iaSxtTLJi4r9GbPNwCNKmAmcri1tIlE422il7ynS8V0KfJpj8zkwV1JPWMrODXVNI2ke8Ayw0Mx+L7ApE6dG9dP71fcXnLfMOGmUu4FDZna84Pqi+F5nrLUv/s283fqvfUjeGh8meWu4OtQ9S/IgA1xEsowcBL4BpkXUvpNkyr8X6A+fBcByYHmwWQEcIHmTuQO4PaL+tHDePUGj5n9aX8Ar4f7sA6ZH1L+YJBGOS9W1zHeSBD0E/Ekyi3iMZL/6C2AgHCcE2+nAm6m+y8IzMAg8GlF/kGT/rBb/2i84rga21ItTJP13Q1z3kiSRSVn9onHSrHaof7sW75RtK3wvGmtti7//5ZDjOE5FOmmp7jiO0xY8cTqO41TEE6fjOE5FPHE6juNUxBOn4zhORTxxOo7jVMQTp+M4TkU8cTqO41TkpxNePAAAAAVJREFUH+ktJxYbz4WvAAAAAElFTkSuQmCC\n","text/plain":"<Figure size 360x720 with 1 Axes>"},"metadata":{"needs_background":"light"},"output_type":"display_data"}],"source":["import matplotlib.pyplot as plt\n","import matplotlib as mpl\n","import numpy as np\n","import pandas as pd\n","\n","x = np.linspace(0, 20, 100)\n","fig, ax = plt.subplots(figsize=(5,10))\n","plt.plot(x, np.sin(x))\n",""]}],"nbformat":4,"nbformat_minor":2,"metadata":{"language_info":{"name":"python","codemirror_mode":{"name":"ipython","version":3}},"orig_nbformat":2,"file_extension":".py","mimetype":"text/x-python","name":"python","npconvert_exporter":"python","pygments_lexer":"ipython3","version":3}} \ No newline at end of file diff --git a/src/test/datascience/test.ipynb b/src/test/datascience/test.ipynb deleted file mode 100644 index 412b9c008e74..000000000000 --- a/src/test/datascience/test.ipynb +++ /dev/null @@ -1 +0,0 @@ -{"cells":[{"source":["a=1\n","a"],"cell_type":"code","outputs":[{"output_type":"execute_result","data":{"text/plain":"1"},"metadata":{},"execution_count":1}],"metadata":{},"execution_count":1}],"nbformat":4,"nbformat_minor":2,"metadata":{"language_info":{"name":"python","codemirror_mode":{"name":"ipython","version":3}},"orig_nbformat":2,"file_extension":".py","mimetype":"text/x-python","name":"python","npconvert_exporter":"python","pygments_lexer":"ipython3","version":3}} \ No newline at end of file diff --git a/src/test/datascience/variableexplorer.functional.test.tsx b/src/test/datascience/variableexplorer.functional.test.tsx deleted file mode 100644 index e4e2ce8d1dc4..000000000000 --- a/src/test/datascience/variableexplorer.functional.test.tsx +++ /dev/null @@ -1,465 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -import * as assert from 'assert'; -import { expect } from 'chai'; -import { ReactWrapper } from 'enzyme'; -import { parse } from 'node-html-parser'; -import * as React from 'react'; -import { Disposable } from 'vscode'; -import { InteractiveWindowMessageListener } from '../../client/datascience/interactive-window/interactiveWindowMessageListener'; -import { InteractiveWindowMessages } from '../../client/datascience/interactive-window/interactiveWindowTypes'; -import { IInteractiveWindow, IInteractiveWindowProvider, IJupyterVariable } from '../../client/datascience/types'; -import { VariableExplorer } from '../../datascience-ui/history-react/variableExplorer'; -import { DataScienceIocContainer } from './dataScienceIocContainer'; -import { addCode, runMountedTest } from './interactiveWindowTestHelpers'; -import { waitForUpdate } from './reactHelpers'; - -// tslint:disable:max-func-body-length trailing-comma no-any no-multiline-string -suite('DataScience Interactive Window variable explorer tests', () => { - const disposables: Disposable[] = []; - let ioc: DataScienceIocContainer; - - suiteSetup(function () { - // These test require python, so only run with a non-mocked jupyter - const isRollingBuild = process.env ? process.env.VSCODE_PYTHON_ROLLING !== undefined : false; - if (!isRollingBuild) { - // tslint:disable-next-line:no-console - console.log('Skipping Variable Explorer tests. Requires python environment'); - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - }); - - setup(() => { - ioc = new DataScienceIocContainer(); - ioc.registerDataScienceTypes(); - }); - - teardown(async () => { - for (const disposable of disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; - } - } - await ioc.dispose(); - }); - - async function getOrCreateInteractiveWindow(): Promise<IInteractiveWindow> { - const interactiveWindowProvider = ioc.get<IInteractiveWindowProvider>(IInteractiveWindowProvider); - const result = await interactiveWindowProvider.getOrCreateActive(); - - // During testing the MainPanel sends the init message before our interactive window is created. - // Pretend like it's happening now - const listener = ((result as any).messageListener) as InteractiveWindowMessageListener; - listener.onMessage(InteractiveWindowMessages.Started, {}); - - return result; - } - - runMountedTest('Variable explorer - Exclude', async (wrapper) => { - const basicCode: string = `import numpy as np -import pandas as pd -value = 'hello world'`; - const basicCode2: string = `value2 = 'hello world 2'`; - - openVariableExplorer(wrapper); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); - await waitForUpdate(wrapper, VariableExplorer, 3); - - // We should show a string and show an int, the modules should be hidden - let targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'value', value: "'hello world'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - - // Update our exclude list to only exlude strings - ioc.getSettings().datascience.variableExplorerExclude = 'str'; - - // Add another string and check our vars, modules should be shown and str should be hidden - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode2, 4); - await waitForUpdate(wrapper, VariableExplorer, 7); - - targetVariables = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'matplotlib', value: '"<module', supportsDataExplorer: false, type: 'module', size: 54, shape: '', count: 0, truncated: false}, - {name: 'notebook', value: '"<module', supportsDataExplorer: false, type: 'module', size: 54, shape: '', count: 0, truncated: false}, - {name: 'np', value: '"<module', supportsDataExplorer: false, type: 'module', size: 54, shape: '', count: 0, truncated: false}, - {name: 'pd', value: '"<module', supportsDataExplorer: false, type: 'module', size: 54, shape: '', count: 0, truncated: false}, - {name: 'sys', value: '"<module', supportsDataExplorer: false, type: 'module', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - }, () => { return ioc; }); - - runMountedTest('Variable explorer - Update', async (wrapper) => { - const basicCode: string = `value = 'hello world'`; - const basicCode2: string = `value2 = 'hello world 2'`; - - openVariableExplorer(wrapper); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await waitForUpdate(wrapper, VariableExplorer, 2); - - // Check that we have just the 'a' variable - let targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - ]; - verifyVariables(wrapper, targetVariables); - - // Add another variable and check it - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); - await waitForUpdate(wrapper, VariableExplorer, 3); - - targetVariables = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'value', value: "'hello world'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - - // Add a second variable and check it - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode2, 4); - await waitForUpdate(wrapper, VariableExplorer, 4); - - targetVariables = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'value', value: "'hello world'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'value2', value: "'hello world 2'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - }, () => { return ioc; }); - - runMountedTest('Variable explorer - Loading', async (wrapper) => { - const basicCode: string = `value = 'hello world'`; - - openVariableExplorer(wrapper); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); - - // Here we are only going to wait for two renders instead of the needed three - // a should have the value updated, but value should still be loading - await waitForUpdate(wrapper, VariableExplorer, 2); - - let targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'value', value: 'Loading...', supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - - // Now wait for one more update and then check the variables, we should have loaded the value var - await waitForUpdate(wrapper, VariableExplorer, 1); - - targetVariables = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'value', value: "'hello world'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - }, () => { return ioc; }); - - // Test our display of basic types. We render 8 rows by default so only 8 values per test - runMountedTest('Variable explorer - Types A', async (wrapper) => { - const basicCode: string = `myList = [1, 2, 3] -mySet = set([42]) -myDict = {'a': 1}`; - - openVariableExplorer(wrapper); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); - - // Verify that we actually update the variable explorer - // Count here is our main render + a render for each variable row as they come in - await waitForUpdate(wrapper, VariableExplorer, 5); - - const targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'myDict', value: "{'a': 1}", supportsDataExplorer: true, type: 'dict', size: 54, shape: '', count: 0, truncated: false}, - {name: 'myList', value: '[1, 2, 3]', supportsDataExplorer: true, type: 'list', size: 54, shape: '', count: 0, truncated: false}, - // Set can vary between python versions, so just don't both to check the value, just see that we got it - {name: 'mySet', value: undefined, supportsDataExplorer: true, type: 'set', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - }, () => { return ioc; }); - - runMountedTest('Variable explorer - Basic B', async (wrapper) => { - const basicCode: string = `import numpy as np -import pandas as pd -myComplex = complex(1, 1) -myInt = 99999999 -myFloat = 9999.9999 -mynpArray = np.linspace(0, 100000, 50000,endpoint=True) -myDataframe = pd.DataFrame(mynpArray) -mySeries = myDataframe[0] -myTuple = 1,2,3,4,5,6,7,8,9 -`; - - openVariableExplorer(wrapper); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); - - // Verify that we actually update the variable explorer - // Count here is our main render + a render for each variable row as they come in - await waitForUpdate(wrapper, VariableExplorer, 9); - - const targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'myComplex', value: '(1+1j)', supportsDataExplorer: false, type: 'complex', size: 54, shape: '', count: 0, truncated: false}, - {name: 'myDataframe', value: ` 0 -0 0.00000 -1 2.00004 -2 4.00008 -3 6.00012 -4 8.00016 -5 10.00020 -6 12.00024 -7 14.00028 -8 16.00032 -9 18.00036 -10 20.00040 -11 22.00044 -12 24.00048 -13 26.00052 -14 28.00056 -15 30.00060 -16 32.00064 -17 34.00068 -18 36.00072 -19 38.00076 -20 40.00080 -21 42.00084 -22 44.00088 -23 46.00092 -24 48.00096 -25 50.00100 -26 52.00104 -27 54.00108 -28 56.00112 -29 58.00116 -... ... -49970 99941.99884 -49971 99943.99888 -49972 99945.99892 -49973 99947.99896 -49974 99949.99900 -49975 99951.99904 -49976 99953.99908 -49977 99955.99912 -49978 99957.99916 -49979 99959.99920 -49980 99961.99924 -49981 99963.99928 -49982 99965.99932 -49983 99967.99936 -49984 99969.99940 -49985 99971.99944 -49986 99973.99948 -49987 99975.99952 -49988 99977.99956 -49989 99979.99960 -49990 99981.99964 -49991 99983.99968 -49992 99985.99972 -49993 99987.99976 -49994 99989.99980 -49995 99991.99984 -49996 99993.99988 -49997 99995.99992 -49998 99997.99996 -49999 100000.00000 - -[50000 rows x 1 columns]`, supportsDataExplorer: true, type: 'DataFrame', size: 54, shape: '', count: 0, truncated: false}, - {name: 'myFloat', value: '9999.9999', supportsDataExplorer: false, type: 'float', size: 58, shape: '', count: 0, truncated: false}, - {name: 'myInt', value: '99999999', supportsDataExplorer: false, type: 'int', size: 56, shape: '', count: 0, truncated: false}, - {name: 'mynpArray', value: `array([0.00000000e+00, 2.00004000e+00, 4.00008000e+00, ..., - 9.99959999e+04, 9.99980000e+04, 1.00000000e+05])`, supportsDataExplorer: true, type: 'ndarray', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable:no-trailing-whitespace - {name: 'mySeries', value: `0 0.00000 -1 2.00004 -2 4.00008 -3 6.00012 -4 8.00016 -5 10.00020 -6 12.00024 -7 14.00028 -8 16.00032 -9 18.00036 -10 20.00040 -11 22.00044 -12 24.00048 -13 26.00052 -14 28.00056 -15 30.00060 -16 32.00064 -17 34.00068 -18 36.00072 -19 38.00076 -20 40.00080 -21 42.00084 -22 44.00088 -23 46.00092 -24 48.00096 -25 50.00100 -26 52.00104 -27 54.00108 -28 56.00112 -29 58.00116 - ... -49970 99941.99884 -49971 99943.99888 -49972 99945.99892 -49973 99947.99896 -49974 99949.99900 -49975 99951.99904 -49976 99953.99908 -49977 99955.99912 -49978 99957.99916 -49979 99959.99920 -49980 99961.99924 -49981 99963.99928 -49982 99965.99932 -49983 99967.99936 -49984 99969.99940 -49985 99971.99944 -49986 99973.99948 -49987 99975.99952 -49988 99977.99956 -49989 99979.99960 -49990 99981.99964 -49991 99983.99968 -49992 99985.99972 -49993 99987.99976 -49994 99989.99980 -49995 99991.99984 -49996 99993.99988 -49997 99995.99992 -49998 99997.99996 -49999 100000.00000 -Name: 0, Length: 50000, dtype: float64`, supportsDataExplorer: true, type: 'Series', size: 54, shape: '', count: 0, truncated: false}, - {name: 'myTuple', value: '(1, 2, 3, 4, 5, 6, 7, 8, 9)', supportsDataExplorer: false, type: 'tuple', size: 54, shape: '', count: 0, truncated: false} - ]; - verifyVariables(wrapper, targetVariables); - }, () => { return ioc; }); - - runMountedTest('Variable explorer - Sorting', async (wrapper) => { - const basicCode: string = `b = 2 -c = 3 -stra = 'a' -strb = 'b' -strc = 'c'`; - - openVariableExplorer(wrapper); - - await addCode(getOrCreateInteractiveWindow, wrapper, 'a=1\na'); - await addCode(getOrCreateInteractiveWindow, wrapper, basicCode, 4); - - await waitForUpdate(wrapper, VariableExplorer, 7); - - let targetVariables: IJupyterVariable[] = [ - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'b', value: '2', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'c', value: '3', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'stra', value: "'a'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'strb', value: "'b'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'strc', value: "'c'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - ]; - verifyVariables(wrapper, targetVariables); - - sortVariableExplorer(wrapper, 'value', 'DESC'); - - targetVariables = [ - {name: 'c', value: '3', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'b', value: '2', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - {name: 'a', value: '1', supportsDataExplorer: false, type: 'int', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'strc', value: "'c'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'strb', value: "'b'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - // tslint:disable-next-line:quotemark - {name: 'stra', value: "'a'", supportsDataExplorer: false, type: 'str', size: 54, shape: '', count: 0, truncated: false}, - ]; - verifyVariables(wrapper, targetVariables); - }, () => { return ioc; }); -}); - -// Open up our variable explorer which also triggers a data fetch -function openVariableExplorer(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>) { - const varExp: VariableExplorer = wrapper.find('VariableExplorer').instance() as VariableExplorer; - - assert(varExp); - - if (varExp) { - varExp.setState({open: true}); - } -} - -function sortVariableExplorer(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, sortColumn: string, sortDirection: string) { - const varExp: VariableExplorer = wrapper.find('VariableExplorer').instance() as VariableExplorer; - - assert(varExp); - - if (varExp) { - varExp.sortRows(sortColumn, sortDirection); - } -} - -// Verify a set of rows versus a set of expected variables -function verifyVariables(wrapper: ReactWrapper<any, Readonly<{}>, React.Component>, targetVariables: IJupyterVariable[]) { - const foundRows = wrapper.find('div.react-grid-Row'); - - expect(foundRows.length).to.be.equal(targetVariables.length, 'Different number of variable explorer rows and target variables'); - - foundRows.forEach((row, index) => { - verifyRow(row, targetVariables[index]); - }); -} - -// Verify a single row versus a single expected variable -function verifyRow(rowWrapper: ReactWrapper<any, Readonly<{}>, React.Component>, targetVariable: IJupyterVariable) { - const rowCells = rowWrapper.find('div.react-grid-Cell'); - - expect(rowCells.length).to.be.equal(5, 'Unexpected number of cells in variable explorer row'); - - verifyCell(rowCells.at(0), targetVariable.name, targetVariable.name); - verifyCell(rowCells.at(1), targetVariable.type, targetVariable.name); - - if (targetVariable.shape && targetVariable.shape !== '') { - verifyCell(rowCells.at(2), targetVariable.shape, targetVariable.name); - } else if (targetVariable.count) { - verifyCell(rowCells.at(2), targetVariable.count.toString(), targetVariable.name); - } - - if (targetVariable.value) { - verifyCell(rowCells.at(3), targetVariable.value, targetVariable.name); - } -} - -// Verify a single cell value against a specific target value -function verifyCell(cellWrapper: ReactWrapper<any, Readonly<{}>, React.Component>, value: string, targetName: string) { - const cellHTML = parse(cellWrapper.html()) as any; - // tslint:disable-next-line:no-string-literal - const rawValue = cellHTML.firstChild.rawAttributes['value'] as string; - - // Eliminate whitespace differences - const actualValueNormalized = rawValue.replace(/^\s*|\s(?=\s)|\s*$/g, ''); - const expectedValueNormalized = value.replace(/^\s*|\s(?=\s)|\s*$/g, ''); - - expect(actualValueNormalized).to.be.equal(expectedValueNormalized, `${targetName} has an unexpected value in variable explorer cell`); -} diff --git a/src/test/debugger/attach.ptvsd.test.ts b/src/test/debugger/attach.ptvsd.test.ts deleted file mode 100644 index 5ace57371215..000000000000 --- a/src/test/debugger/attach.ptvsd.test.ts +++ /dev/null @@ -1,145 +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 { MultiStepInputFactory } 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); - debugClient = await createDebugAdapter(); - }); - 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}`, 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<IPlatformService>(); - platformService.setup(p => p.isWindows).returns(() => isLocalHostWindows); - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(c => c.get(IPlatformService, TypeMoq.It.isAny())).returns(() => platformService.object); - - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - const configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - - const launchResolver = TypeMoq.Mock.ofType<IDebugConfigurationResolver<LaunchRequestArguments>>(); - const attachResolver = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); - const providerFactory = TypeMoq.Mock.ofType<IDebugConfigurationProviderFactory>().object; - const fs = mock(FileSystem); - const multistepFactory = mock(MultiStepInputFactory); - const configProvider = new PythonDebugConfigurationService(attachResolver, launchResolver.object, providerFactory, - instance(multistepFactory), 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); - }) - // Retry as tests can timeout on server due to connectivity issues. - .retries(3); -}); diff --git a/src/test/debugger/capabilities.test.ts b/src/test/debugger/capabilities.test.ts deleted file mode 100644 index 3e45655c22d7..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}`, 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<DebugProtocol.InitializeResponse>(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<IServiceContainer>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - 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<net.Socket>(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 a2cde4dd0aae..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<Logger.Logger>(); - 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<Logger.Logger>(); - 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<Logger.Logger>(); - 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<boolean>(resolve => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - const responseDetected = new Promise<boolean>(resolve => { - protocolParser.on('response_initialize', () => resolve(true)); - }); - const eventDetected = new Promise<boolean>(resolve => { - protocolParser.on('event_initialized', () => resolve(true)); - }); - - stream.write('Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}'); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - stream.write('Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}'); - await expect(responseDetected).to.eventually.equal(true, 'response not parsed'); - - stream.write('Content-Length: 63\r\n\r\n{"type": "event", "seq": 1, "event": "initialized", "body": {}}'); - await expect(eventDetected).to.eventually.equal(true, 'event not parsed'); - - expect(messagesDetected).to.be.equal(3, 'incorrect number of protocol messages'); - }); - test('Ensure messages are not received after disposing the parser', async () => { - const stream = new PassThrough(); - - const protocolParser = new ProtocolParser(); - protocolParser.connect(stream); - let messagesDetected = 0; - protocolParser.on('data', () => messagesDetected += 1); - const requestDetected = new Promise<boolean>(resolve => { - protocolParser.on('request_initialize', () => resolve(true)); - }); - stream.write('Content-Length: 289\r\n\r\n{"command":"initialize","arguments":{"clientID":"vscode","adapterID":"pythonExperiment","pathFormat":"path","linesStartAt1":true,"columnsStartAt1":true,"supportsVariableType":true,"supportsVariablePaging":true,"supportsRunInTerminalRequest":true,"locale":"en-us"},"type":"request","seq":1}'); - await expect(requestDetected).to.eventually.equal(true, 'request not parsed'); - - protocolParser.dispose(); - - const responseDetected = createDeferred<boolean>(); - protocolParser.on('response_initialize', () => responseDetected.resolve(true)); - - stream.write('Content-Length: 265\r\n\r\n{"seq":1,"type":"response","request_seq":1,"command":"initialize","success":true,"body":{"supportsEvaluateForHovers":false,"supportsConditionalBreakpoints":true,"supportsConfigurationDoneRequest":true,"supportsFunctionBreakpoints":false,"supportsSetVariable":true}}'); - // Wait for messages to go through and get parsed (unnecenssary, but add for testing edge cases). - await sleep(1000); - expect(responseDetected.completed).to.be.equal(false, 'Promise should not have resolved'); - }); -}); diff --git a/src/test/debugger/debugAdapter/debugClients/launcherProvider.unit.test.ts b/src/test/debugger/debugAdapter/debugClients/launcherProvider.unit.test.ts deleted file mode 100644 index 8aeeed217b0a..000000000000 --- a/src/test/debugger/debugAdapter/debugClients/launcherProvider.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 fs from 'fs-extra'; -import * as path from 'path'; -import { EXTENSION_ROOT_DIR } from '../../../../client/common/constants'; -import { DebuggerLauncherScriptProvider, NoDebugLauncherScriptProvider, RemoteDebuggerExternalLauncherScriptProvider } from '../../../../client/debugger/debugAdapter/DebugClients/launcherProvider'; - -const expectedPath = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'ptvsd_launcher.py'); - -// tslint:disable-next-line:max-func-body-length -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'); - }); - const testsForLaunchProvider = - [ - { - testName: 'When path to ptvsd launcher does not contains spaces', - path: path.join('path', 'to', 'ptvsd_launcher'), - expectedPath: path.join('path', 'to', 'ptvsd_launcher') - }, - { - testName: 'When path to ptvsd launcher contains spaces', - path: path.join('path', 'to', 'ptvsd_launcher', 'with spaces'), - expectedPath: path.join('path', 'to', 'ptvsd_launcher', 'with spaces') - } - ]; - - testsForLaunchProvider.forEach(testParams => { - suite(testParams.testName, async () => { - test('Test debug launcher args', async () => { - const args = new DebuggerLauncherScriptProvider(testParams.path).getLauncherArgs({ host: 'something', port: 1234 }); - const expectedArgs = [testParams.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(testParams.path).getLauncherArgs({ host: 'something', port: 1234 }); - const expectedArgs = [testParams.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(testParams.path).getLauncherArgs({ host: 'something', port: 1234, customDebugger: true }); - const expectedArgs = [testParams.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(testParams.path).getLauncherArgs({ host: 'something', port: 1234, customDebugger: true }); - const expectedArgs = [testParams.expectedPath, '--custom', '--nodebug', '--client', '--host', 'something', '--port', '1234']; - expect(args).to.be.deep.equal(expectedArgs); - }); - }); - }); - - suite('External Debug Launcher', () => { - [ - { - testName: 'When path to ptvsd launcher does not contains spaces', - path: path.join('path', 'to', 'ptvsd_launcher'), - expectedPath: 'path/to/ptvsd_launcher' - }, - { - testName: 'When path to ptvsd launcher contains spaces', - path: path.join('path', 'to', 'ptvsd_launcher', 'with spaces'), - expectedPath: '"path/to/ptvsd_launcher/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 = new RemoteDebuggerExternalLauncherScriptProvider(testParams.path).getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: false }); - const expectedArgs = [testParams.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 RemoteDebuggerExternalLauncherScriptProvider(testParams.path).getLauncherArgs({ host: 'something', port: 1234, waitUntilDebuggerAttaches: true }); - const expectedArgs = [testParams.expectedPath, '--default', '--host', 'something', '--port', '1234', '--wait']; - expect(args).to.be.deep.equal(expectedArgs); - }); - }); - }); - }); -}); diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts index a679a7a69b8c..8b0f55986281 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -1,43 +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 { + 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(async function () { if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - // tslint:disable-next-line:no-invalid-this return this.skip(); } await initialize(); }); setup(async () => { - initializeDI(); + await initializeDI(); await initializeTest(); const envParser = ioc.serviceContainer.get<IEnvironmentVariablesService>(IEnvironmentVariablesService); const pathUtils = ioc.serviceContainer.get<IPathUtils>(IPathUtils); mockProcess = ioc.serviceContainer.get<ICurrentProcess>(ICurrentProcess); - helper = new DebugClientHelper(envParser, pathUtils, mockProcess); + debugEnvParser = new DebugEnvironmentVariablesHelper(envParser, mockProcess); pathVariableName = pathUtils.getPathVariableName(); }); suiteTeardown(closeActiveWindows); @@ -46,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>( + IRecommendedEnvironmentService, + RecommendedEnvironmentService, + ); } async function testBasicProperties(console: ConsoleType, expectedNumberOfVariables: number) { - const args = { - program: '', pythonPath: '', args: [], envFile: '', - console - // tslint:disable-next-line:no-any - } as any as LaunchRequestArguments; - - const envVars = await helper.getEnvironmentVariables(args); + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + } as any) as LaunchRequestArguments; + + const envVars = await debugEnvParser.getEnvironmentVariables(args); expect(envVars).not.be.undefined; expect(Object.keys(envVars)).lengthOf(expectedNumberOfVariables, 'Incorrect number of variables'); expect(envVars).to.have.property('PYTHONUNBUFFERED', '1', 'Property not found'); expect(envVars).to.have.property('PYTHONIOENCODING', 'UTF-8', 'Property not found'); } - test('Confirm basic environment variables exist when launched in external terminal', () => testBasicProperties('externalTerminal', 2)); - - test('Confirm basic environment variables exist when launched in intergrated terminal', () => testBasicProperties('integratedTerminal', 2)); + test('Confirm basic environment variables exist when launched in external terminal', () => + testBasicProperties('externalTerminal', 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<string, string> = { DO_NOT_OVERWRITE: '1' }; + const args = ({ + program: '', + pythonPath: '', + args: [], + envFile: '', + console, + env, + } as any) as LaunchRequestArguments; + + const baseEnvVars = { CONDA_PREFIX: 'path/to/conda/env', DO_NOT_OVERWRITE: '0' }; + const envVars = await debugEnvParser.getEnvironmentVariables(args, baseEnvVars); + expect(envVars).not.be.undefined; + expect(Object.keys(envVars)).lengthOf(4, 'Incorrect number of variables'); + expect(envVars).to.have.property('PYTHONUNBUFFERED', '1', 'Property not found'); + expect(envVars).to.have.property('PYTHONIOENCODING', 'UTF-8', 'Property not found'); + expect(envVars).to.have.property('CONDA_PREFIX', 'path/to/conda/env', 'Property not found'); + expect(envVars).to.have.property('DO_NOT_OVERWRITE', '1', 'Property not found'); + }); test('Confirm basic environment variables exist when launched in debug console', async () => { let expectedNumberOfVariables = Object.keys(mockProcess.env).length; @@ -83,23 +116,25 @@ suite('Resolving Environment Variables when Debugging', () => { }); async function testJsonEnvVariables(console: ConsoleType, expectedNumberOfVariables: number) { - const prop1 = shortid.generate(); - const prop2 = shortid.generate(); - const prop3 = shortid.generate(); + const prop1 = normCase(shortid.generate()); + const prop2 = normCase(shortid.generate()); + const prop3 = normCase(shortid.generate()); const env: Record<string, string> = {}; env[prop1] = prop1; env[prop2] = prop2; 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'); @@ -114,9 +149,11 @@ suite('Resolving Environment Variables when Debugging', () => { } } - 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 @@ -130,8 +167,11 @@ suite('Resolving Environment Variables when Debugging', () => { await testJsonEnvVariables('internalConsole', expectedNumberOfVariables); }); - async function testAppendingOfPaths(console: ConsoleType, - expectedNumberOfVariables: number, removePythonPath: boolean) { + async function testAppendingOfPaths( + console: ConsoleType, + expectedNumberOfVariables: number, + removePythonPath: boolean, + ) { if (removePythonPath && mockProcess.env.PYTHONPATH !== undefined) { delete mockProcess.env.PYTHONPATH; } @@ -149,12 +189,16 @@ suite('Resolving Environment Variables when Debugging', () => { 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'); @@ -183,12 +227,18 @@ suite('Resolving Environment Variables when Debugging', () => { 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.`, + ); }); } } @@ -196,7 +246,6 @@ suite('Resolving Environment Variables when Debugging', () => { test('Confirm paths get appended correctly when using json variables and launched in external terminal', async function () { // test is flakey on windows, path separator problems. GH issue #4758 if (isOs(OSType.Windows)) { - // tslint:disable-next-line:no-invalid-this return this.skip(); } await testAppendingOfPaths('externalTerminal', 6, false); @@ -205,7 +254,6 @@ suite('Resolving Environment Variables when Debugging', () => { test('Confirm paths get appended correctly when using json variables and launched in integrated terminal', async function () { // test is flakey on windows, path separator problems. GH issue #4758 if (isOs(OSType.Windows)) { - // tslint:disable-next-line:no-invalid-this return this.skip(); } await testAppendingOfPaths('integratedTerminal', 6, false); @@ -214,7 +262,6 @@ suite('Resolving Environment Variables when Debugging', () => { test('Confirm paths get appended correctly when using json variables and launched in debug console', async function () { // test is flakey on windows, path separator problems. GH issue #4758 if (isOs(OSType.Windows)) { - // tslint:disable-next-line:no-invalid-this return this.skip(); } diff --git a/src/test/debugger/extension/adapter/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<boolean | undefined>; + 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<string, string>[] = []; + public static measures: {}[] = []; + public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { + Reporter.eventNames.push(eventName); + Reporter.properties.push(properties!); + Reporter.measures.push(measures!); + } + } + + setup(() => { + process.env.VSC_PYTHON_UNIT_TEST = undefined; + process.env.VSC_PYTHON_CI_TEST = undefined; + 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<boolean | undefined>; + commandManager = mock(CommandManager); + getDebugpyPathStub = sinon.stub(pythonDebugger, 'getDebugpyPath'); + getDebugpyPathStub.resolves(debugpyPath); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + + when( + stateFactory.createGlobalPersistentState<boolean | undefined>(debugStateKeys.doNotShowAgain, false), + ).thenReturn(instance(state)); + + const configurationService = mock(ConfigurationService); + when(configurationService.getSettings(undefined)).thenReturn(({ + experiments: { enabled: true }, + } 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<DebugConfiguration>, 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<IServiceContainer>; - let browser: typemoq.IMock<IBrowserService>; - let launchCounterState: typemoq.IMock<IPersistentState<number>>; - let launchThresholdCounterState: typemoq.IMock<IPersistentState<number | undefined>>; - let showBannerState: typemoq.IMock<IPersistentState<boolean>>; - let userSelected: boolean | undefined; - let userSelectedState: typemoq.IMock<IPersistentState<boolean | undefined>>; - let debugService: typemoq.IMock<IDebugService>; - let appShell: typemoq.IMock<IApplicationShell>; - let runtime: typemoq.IMock<IRandom>; - let banner: DebuggerBanner; - const message = 'Can you please take 2 minutes to tell us how the debugger is working for you?'; - const yes = 'Yes, take survey now'; - const no = 'No thanks'; - const later = 'Remind me later'; - - setup(() => { - serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); - browser = typemoq.Mock.ofType<IBrowserService>(); - debugService = typemoq.Mock.ofType<IDebugService>(); - const logger = typemoq.Mock.ofType<ILogger>(); - - launchCounterState = typemoq.Mock.ofType<IPersistentState<number>>(); - showBannerState = typemoq.Mock.ofType<IPersistentState<boolean>>(); - appShell = typemoq.Mock.ofType<IApplicationShell>(); - runtime = typemoq.Mock.ofType<IRandom>(); - launchThresholdCounterState = typemoq.Mock.ofType<IPersistentState<number | undefined>>(); - userSelected = true; - userSelectedState = typemoq.Mock.ofType<IPersistentState<boolean | undefined>>(); - const factory = typemoq.Mock.ofType<IPersistentStateFactory>(); - factory - .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.DebuggerLaunchCounter), typemoq.It.isAny())) - .returns(() => launchCounterState.object); - factory - .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.ShowBanner), typemoq.It.isAny())) - .returns(() => showBannerState.object); - factory - .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.DebuggerLaunchThresholdCounter), typemoq.It.isAny())) - .returns(() => launchThresholdCounterState.object); - factory - .setup(f => f.createGlobalPersistentState(typemoq.It.isValue(PersistentStateKeys.UserSelected), typemoq.It.isAny())) - .returns(() => userSelectedState.object); - - serviceContainer.setup(s => s.get(typemoq.It.isValue(IBrowserService))).returns(() => browser.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IPersistentStateFactory))).returns(() => factory.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(IDebugService))).returns(() => debugService.object); - serviceContainer.setup(s => s.get(typemoq.It.isValue(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<void>; - debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) - .callback(cb => onDidTerminateDebugSessionCb = cb) - .verifiable(typemoq.Times.once()); - - const debuggerLaunchCounter = 1234; - launchCounterState.setup(l => l.value).returns(() => debuggerLaunchCounter) - .verifiable(typemoq.Times.atLeastOnce()); - launchCounterState.setup(l => l.updateValue(typemoq.It.isValue(debuggerLaunchCounter + 1))) - .verifiable(typemoq.Times.once()); - showBannerState.setup(s => s.value).returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - - banner.initialize(); - await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); - - launchCounterState.verifyAll(); - browser.verifyAll(); - debugService.verifyAll(); - showBannerState.verifyAll(); - }); - test('Do not Increment Debugger Launch Counter when debug session starts and Banner is disabled', async () => { - debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) - .verifiable(typemoq.Times.never()); - - const debuggerLaunchCounter = 1234; - launchCounterState.setup(l => l.value).returns(() => debuggerLaunchCounter) - .verifiable(typemoq.Times.never()); - launchCounterState.setup(l => l.updateValue(typemoq.It.isValue(debuggerLaunchCounter + 1))) - .verifiable(typemoq.Times.never()); - showBannerState.setup(s => s.value).returns(() => false) - .verifiable(typemoq.Times.atLeastOnce()); - - banner.initialize(); - - launchCounterState.verifyAll(); - browser.verifyAll(); - debugService.verifyAll(); - showBannerState.verifyAll(); - }); - test('shouldShow must return false when Banner is disabled', async () => { - showBannerState.setup(s => s.value).returns(() => false) - .verifiable(typemoq.Times.once()); - - expect(await banner.shouldShow()).to.be.equal(false, 'Incorrect value'); - - showBannerState.verifyAll(); - }); - test('shouldShow must return false when Banner is enabled and debug counter is not same as threshold', async () => { - showBannerState.setup(s => s.value).returns(() => true) - .verifiable(typemoq.Times.once()); - launchCounterState.setup(l => l.value).returns(() => 1) - .verifiable(typemoq.Times.once()); - launchThresholdCounterState.setup(t => t.value).returns(() => 10) - .verifiable(typemoq.Times.atLeastOnce()); - - expect(await banner.shouldShow()).to.be.equal(false, 'Incorrect value'); - - showBannerState.verifyAll(); - launchCounterState.verifyAll(); - launchThresholdCounterState.verifyAll(); - }); - test('shouldShow must return true when Banner is enabled and debug counter is same as threshold', async () => { - showBannerState.setup(s => s.value).returns(() => true) - .verifiable(typemoq.Times.once()); - launchCounterState.setup(l => l.value).returns(() => 10) - .verifiable(typemoq.Times.once()); - launchThresholdCounterState.setup(t => t.value).returns(() => 10) - .verifiable(typemoq.Times.atLeastOnce()); - - expect(await banner.shouldShow()).to.be.equal(true, 'Incorrect value'); - - showBannerState.verifyAll(); - launchCounterState.verifyAll(); - launchThresholdCounterState.verifyAll(); - }); - test('show must be invoked when shouldShow returns true', async () => { - let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise<void>; - const currentLaunchCounter = 50; - - debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) - .callback(cb => onDidTerminateDebugSessionCb = cb) - .verifiable(typemoq.Times.atLeastOnce()); - showBannerState.setup(s => s.value).returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - launchCounterState.setup(l => l.value).returns(() => currentLaunchCounter) - .verifiable(typemoq.Times.atLeastOnce()); - launchThresholdCounterState.setup(t => t.value).returns(() => 10) - .verifiable(typemoq.Times.atLeastOnce()); - launchCounterState.setup(l => l.updateValue(typemoq.It.isValue(currentLaunchCounter + 1))) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.atLeastOnce()); - - appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), typemoq.It.isValue(yes), typemoq.It.isValue(no), typemoq.It.isValue(later))) - .verifiable(typemoq.Times.once()); - banner.initialize(); - await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); - - appShell.verifyAll(); - showBannerState.verifyAll(); - launchCounterState.verifyAll(); - launchThresholdCounterState.verifyAll(); - }); - test('show must not be invoked the second time after dismissing the message', async () => { - let onDidTerminateDebugSessionCb: (e: DebugSession) => Promise<void>; - let currentLaunchCounter = 50; - - debugService.setup(d => d.onDidTerminateDebugSession(typemoq.It.isAny())) - .callback(cb => onDidTerminateDebugSessionCb = cb) - .verifiable(typemoq.Times.atLeastOnce()); - showBannerState.setup(s => s.value).returns(() => true) - .verifiable(typemoq.Times.atLeastOnce()); - launchCounterState.setup(l => l.value).returns(() => currentLaunchCounter) - .verifiable(typemoq.Times.atLeastOnce()); - launchThresholdCounterState.setup(t => t.value).returns(() => 10) - .verifiable(typemoq.Times.atLeastOnce()); - launchCounterState.setup(l => l.updateValue(typemoq.It.isAny())) - .callback(() => currentLaunchCounter = currentLaunchCounter + 1); - - appShell.setup(a => a.showInformationMessage(typemoq.It.isValue(message), typemoq.It.isValue(yes), typemoq.It.isValue(no), typemoq.It.isValue(later))) - .returns(() => Promise.resolve(undefined)) - .verifiable(typemoq.Times.once()); - banner.initialize(); - await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); - await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); - await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); - await onDidTerminateDebugSessionCb!({ type: DebuggerTypeName } as any); - - appShell.verifyAll(); - showBannerState.verifyAll(); - launchCounterState.verifyAll(); - launchThresholdCounterState.verifyAll(); - expect(currentLaunchCounter).to.be.equal(54); - }); - test('Disabling banner must store value of \'false\' in global store', async () => { - showBannerState.setup(s => s.updateValue(typemoq.It.isValue(false))) - .verifiable(typemoq.Times.once()); - - await banner.disable(); - - showBannerState.verifyAll(); - }); -}); diff --git a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts index 611b240b39cc..ae13ad375371 100644 --- a/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts +++ b/src/test/debugger/extension/configuration/debugConfigurationService.unit.test.ts @@ -3,153 +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<IDebugConfigurationResolver<AttachRequestArguments>>; let launchResolver: typemoq.IMock<IDebugConfigurationResolver<LaunchRequestArguments>>; let configService: TestPythonDebugConfigurationService; - let multiStepFactory: typemoq.IMock<IMultiStepInputFactory>; - let providerFactory: DebugConfigurationProviderFactory; - let fs: IFileSystem; - class TestPythonDebugConfigurationService extends PythonDebugConfigurationService { - // tslint:disable-next-line:no-unnecessary-override - public async pickDebugConfiguration(input: IMultiStepInput<DebugConfigurationState>, state: DebugConfigurationState) { - return super.pickDebugConfiguration(input, state); - } - } + class TestPythonDebugConfigurationService extends PythonDebugConfigurationService {} setup(() => { attachResolver = typemoq.Mock.ofType<IDebugConfigurationResolver<AttachRequestArguments>>(); launchResolver = typemoq.Mock.ofType<IDebugConfigurationResolver<LaunchRequestArguments>>(); - multiStepFactory = typemoq.Mock.ofType<IMultiStepInputFactory>(); - providerFactory = mock(DebugConfigurationProviderFactory); - 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<IMultiStepInput<DebugConfigurationState>>(); - multiStepInput - .setup(i => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) - .verifiable(typemoq.Times.once()); - - await configService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - }); - test('Existing Configuration items must be removed before displaying picker', async () => { - // tslint:disable-next-line:no-object-literal-type-assertion - const state = { configs: [1, 2, 3], folder: {}, token: undefined } as any as DebugConfigurationState; - const multiStepInput = typemoq.Mock.ofType<IMultiStepInput<DebugConfigurationState>>(); - multiStepInput - .setup(i => i.showQuickPick(typemoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) - .verifiable(typemoq.Times.once()); - - await configService.pickDebugConfiguration(multiStepInput.object, state); - - multiStepInput.verifyAll(); - expect(Object.keys(state.config)).to.be.lengthOf(0); - }); - test('Ensure generated config is returned', async () => { - const expectedConfig = { yes: 'Updated' }; - const multiStepInput = { - run: (_: any, state: any) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - } - }; - multiStepFactory - .setup(f => f.create()) - .returns(() => multiStepInput as any) - .verifiable(typemoq.Times.once()); - configService.pickDebugConfiguration = (_, state) => { - Object.assign(state.config, expectedConfig); - return Promise.resolve(); - }; - const config = await configService.provideDebugConfigurations!({} as any); - - multiStepFactory.verifyAll(); - expect(config).to.deep.equal([expectedConfig]); - }); - test('Ensure 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/completionProvider.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts deleted file mode 100644 index d4a6c570d80b..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/completionProvider.unit.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { deepEqual, instance, mock, verify } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, CompletionItem, CompletionItemKind, Position, SnippetString, TextDocument, Uri } from 'vscode'; -import { LanguageService } from '../../../../../client/common/application/languageService'; -import { ILanguageService } from '../../../../../client/common/application/types'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { LaunchJsonCompletionProvider } from '../../../../../client/debugger/extension/configuration/launch.json/completionProvider'; - -// tslint:disable:no-any no-multiline-string max-func-body-length -suite('Debugging - launch.json Completion Provider', () => { - let completionProvider: LaunchJsonCompletionProvider; - let languageService: ILanguageService; - - setup(() => { - languageService = mock(LanguageService); - completionProvider = new LaunchJsonCompletionProvider(instance(languageService), []); - }); - test('Activation will register the completion provider', async () => { - await completionProvider.activate(undefined); - verify(languageService.registerCompletionItemProvider(deepEqual({ language: 'json' }), completionProvider)).once(); - verify(languageService.registerCompletionItemProvider(deepEqual({ language: 'jsonc' }), completionProvider)).once(); - }); - test('Cannot provide completions for non launch.json files', () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const position = new Position(0, 0); - document.setup(doc => doc.uri).returns(() => Uri.file(__filename)); - assert.equal(completionProvider.canProvideCompletions(document.object, position), false); - - document.reset(); - document.setup(doc => doc.uri).returns(() => Uri.file('settings.json')); - assert.equal(completionProvider.canProvideCompletions(document.object, position), false); - }); - function testCanProvideCompletions(position: Position, offset: number, json: string, expectedValue: boolean) { - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.uri).returns(() => Uri.file('launch.json')); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => offset); - const canProvideCompletions = completionProvider.canProvideCompletions(document.object, position); - assert.equal(canProvideCompletions, expectedValue); - } - test('Cannot provide completions when there is no configurations section in json', () => { - const position = new Position(0, 0); - const config = `{ - "version": "0.1.0" -}`; - testCanProvideCompletions(position, 1, config as any, false); - }); - test('Cannot provide completions when cursor position is not in configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [] -}`; - testCanProvideCompletions(position, 10, json, false); - }); - test('Cannot provide completions when cursor position is in an empty configurations array', () => { - const position = new Position(0, 0); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - testCanProvideCompletions(position, json.indexOf('# Cursor Position'), json, true); - }); - test('No Completions for non launch.json', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(doc => doc.uri).returns(() => Uri.file('settings.json')); - const token = new CancellationTokenSource().token; - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.equal(completions.length, 0); - }); - test('No Completions for files ending with launch.json', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(doc => doc.uri).returns(() => Uri.file('x-launch.json')); - const token = new CancellationTokenSource().token; - const position = new Position(0, 0); - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.equal(completions.length, 0); - }); - test('Get Completions', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] -}`; - - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.uri).returns(() => Uri.file('launch.json')); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('# Cursor Position')); - const position = new Position(0, 0); - const token = new CancellationTokenSource().token; - - const completions = await completionProvider.provideCompletionItems(document.object, position, token); - - assert.equal(completions.length, 1); - - const expectedCompletionItem: CompletionItem = { - command: { - command: 'python.SelectAndInsertDebugConfiguration', - title: DebugConfigStrings.launchJsonCompletions.description(), - arguments: [document.object, position, token] - }, - documentation: DebugConfigStrings.launchJsonCompletions.description(), - sortText: 'AAAA', - preselect: true, - kind: CompletionItemKind.Enum, - label: DebugConfigStrings.launchJsonCompletions.label(), - insertText: new SnippetString() - }; - - assert.deepEqual(completions[0], expectedCompletionItem); - }); -}); diff --git a/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts new file mode 100644 index 000000000000..4241f3526f1a --- /dev/null +++ b/src/test/debugger/extension/configuration/launch.json/launchJsonReader.unit.test.ts @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import { assert } from 'chai'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { getConfigurationsForWorkspace } from '../../../../../client/debugger/extension/configuration/launch.json/launchJsonReader'; +import * as vscodeApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Launch Json Reader', () => { + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + const workspacePath = 'path/to/workspace'; + const workspaceFolder = { + name: 'workspace', + uri: Uri.file(workspacePath), + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + getConfigurationStub = sinon.stub(vscodeApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return the config in the launch.json file', async () => { + const launchPath = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); + pathExistsStub.withArgs(launchPath).resolves(true); + const launchJson = `{ + "version": "0.1.0", + "configurations": [ + { + "name": "Python: Launch.json", + "type": "python", + "request": "launch", + "purpose": ["debug-test"], + }, + ] + }`; + readFileStub.withArgs(launchPath, 'utf-8').returns(launchJson); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Launch.json', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); + + test('If there is no launch.json return the config in the workspace file', async () => { + getConfigurationStub.withArgs('launch').returns({ + configurations: [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ], + }); + + const config = await getConfigurationsForWorkspace(workspaceFolder); + + assert.deepStrictEqual(config, [ + { + name: 'Python: Workspace File', + type: 'python', + request: 'launch', + purpose: ['debug-test'], + }, + ]); + }); +}); diff --git a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts b/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts deleted file mode 100644 index 91067453765f..000000000000 --- a/src/test/debugger/extension/configuration/launch.json/updaterServer.unit.test.ts +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, DebugConfiguration, Position, TextDocument, TextEditor, Uri } from 'vscode'; -import { CommandManager } from '../../../../../client/common/application/commandManager'; -import { DocumentManager } from '../../../../../client/common/application/documentManager'; -import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../../client/common/application/workspace'; -import { PythonDebugConfigurationService } from '../../../../../client/debugger/extension/configuration/debugConfigurationService'; -import { LaunchJsonUpdaterService, LaunchJsonUpdaterServiceHelper } from '../../../../../client/debugger/extension/configuration/launch.json/updaterService'; -import { IDebugConfigurationService } from '../../../../../client/debugger/extension/types'; - -type LaunchJsonSchema = { - version: string; - configurations: DebugConfiguration[]; -}; - -// tslint:disable:no-any no-multiline-string max-func-body-length -suite('Debugging - launch.json Updater Service', () => { - let helper: LaunchJsonUpdaterServiceHelper; - let commandManager: ICommandManager; - let workspace: IWorkspaceService; - let documentManager: IDocumentManager; - let debugConfigService: IDebugConfigurationService; - - setup(() => { - commandManager = mock(CommandManager); - workspace = mock(WorkspaceService); - documentManager = mock(DocumentManager); - debugConfigService = mock(PythonDebugConfigurationService); - helper = new LaunchJsonUpdaterServiceHelper(instance(commandManager), - instance(workspace), instance(documentManager), instance(debugConfigService)); - }); - test('Activation will register the required commands', async () => { - const service = new LaunchJsonUpdaterService(instance(commandManager), [], instance(workspace), instance(documentManager), instance(debugConfigService)); - await service.activate(undefined); - verify(commandManager.registerCommand('python.SelectAndInsertDebugConfiguration', helper.selectAndInsertDebugConfig, helper)); - }); - - test('Configuration Array is detected as being empty', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const config: LaunchJsonSchema = { - version: '', - configurations: [] - }; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = helper.isConfigurationArrayEmpty(document.object); - assert.equal(isEmpty, true); - }); - test('Configuration Array is not empty', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python' - } - ] - }; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - - const isEmpty = helper.isConfigurationArrayEmpty(document.object); - assert.equal(isEmpty, false); - }); - test('Cursor is not positioned in the configurations array', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const config: LaunchJsonSchema = { - version: '', - configurations: [ - { - name: '', - request: 'launch', - type: 'python' - } - ] - }; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => JSON.stringify(config)); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => 10); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.equal(cursorPosition, undefined); - }); - test('Cursor is positioned in the empty configurations array', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const json = `{ - "version": "0.1.0", - "configurations": [ - # Cursor Position - ] - }`; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('#')); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.equal(cursorPosition, 'InsideEmptyArray'); - }); - test('Cursor is positioned before an item in the configurations array', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - } - ] -}`; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('{') - 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.equal(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned before an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf(',{') + 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.equal(cursorPosition, 'BeforeItem'); - }); - test('Cursor is positioned after an item in the configurations array', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - }] -}`; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.lastIndexOf('}]') + 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.equal(cursorPosition, 'AfterItem'); - }); - test('Cursor is positioned after an item in the middle of the configurations array', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - - const cursorPosition = helper.getCursorPositionInConfigurationsArray(document.object, new Position(0, 0)); - assert.equal(cursorPosition, 'AfterItem'); - }); - test('Text to be inserted must be prefixed with a comma', async () => { - const config = {} as any; - const expectedText = `,${JSON.stringify(config)}`; - - const textToInsert = helper.getTextForInsertion(config, 'AfterItem'); - - assert.equal(textToInsert, expectedText); - }); - test('Text to be inserted must be suffixed with a comma', async () => { - const config = {} as any; - const expectedText = `${JSON.stringify(config)},`; - - const textToInsert = helper.getTextForInsertion(config, 'BeforeItem'); - - assert.equal(textToInsert, expectedText); - }); - test('Text to be inserted must not be prefixed nor suffixed with commas', async () => { - const config = {} as any; - const expectedText = JSON.stringify(config); - - const textToInsert = helper.getTextForInsertion(config, 'InsideEmptyArray'); - - assert.equal(textToInsert, expectedText); - }); - test('When inserting the debug config into the json file format the document', async () => { - const json = `{ - "version": "0.1.0", - "configurations": [ - { - "name":"wow" - },{ - "name":"wow" - } - ] -}`; - const config = {} as any; - const document = typemoq.Mock.ofType<TextDocument>(); - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => json); - document.setup(doc => doc.offsetAt(typemoq.It.isAny())).returns(() => json.indexOf('},') + 1); - when(documentManager.applyEdit(anything())).thenResolve(); - when(commandManager.executeCommand('editor.action.formatDocument')).thenResolve(); - - await helper.insertDebugConfiguration(document.object, new Position(0, 0), config); - - verify(documentManager.applyEdit(anything())).once(); - verify(commandManager.executeCommand('editor.action.formatDocument')).once(); - }); - test('No changes to configuration if there is not active document', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const position = new Position(0, 0); - const token = new CancellationTokenSource().token; - when(documentManager.activeTextEditor).thenReturn(); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(anything())).never(); - assert.equal(debugConfigInserted, false); - }); - test('No changes to configuration if the active document is not same as the document passed in', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const position = new Position(0, 0); - const token = new CancellationTokenSource().token; - const textEditor = typemoq.Mock.ofType<TextEditor>(); - textEditor.setup(t => t.document).returns(() => 'x' as any).verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(anything())).never(); - textEditor.verifyAll(); - assert.equal(debugConfigInserted, false); - }); - test('No changes to configuration if cancellation token has been cancelled', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - const token = tokenSource.token; - const textEditor = typemoq.Mock.ofType<TextEditor>(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document.setup(doc => doc.uri).returns(() => docUri).verifiable(typemoq.Times.atLeastOnce()); - textEditor.setup(t => t.document).returns(() => document.object).verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([''] as any); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); - textEditor.verifyAll(); - document.verifyAll(); - assert.equal(debugConfigInserted, false); - }); - test('No changes to configuration if no configuration items are returned', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const token = tokenSource.token; - const textEditor = typemoq.Mock.ofType<TextEditor>(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document.setup(doc => doc.uri).returns(() => docUri).verifiable(typemoq.Times.atLeastOnce()); - textEditor.setup(t => t.document).returns(() => document.object).verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve([] as any); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); - textEditor.verifyAll(); - document.verifyAll(); - assert.equal(debugConfigInserted, false); - }); - test('Changes are made to the configuration', async () => { - const document = typemoq.Mock.ofType<TextDocument>(); - const position = new Position(0, 0); - const tokenSource = new CancellationTokenSource(); - const token = tokenSource.token; - const textEditor = typemoq.Mock.ofType<TextEditor>(); - const docUri = Uri.file(__filename); - const folderUri = Uri.file('Folder Uri'); - const folder = { name: '', index: 0, uri: folderUri }; - document.setup(doc => doc.uri).returns(() => docUri).verifiable(typemoq.Times.atLeastOnce()); - textEditor.setup(t => t.document).returns(() => document.object).verifiable(typemoq.Times.atLeastOnce()); - when(documentManager.activeTextEditor).thenReturn(textEditor.object); - when(workspace.getWorkspaceFolder(docUri)).thenReturn(folder); - when(debugConfigService.provideDebugConfigurations!(folder, token)).thenResolve(['config'] as any); - let debugConfigInserted = false; - helper.insertDebugConfiguration = async () => { - debugConfigInserted = true; - }; - - await helper.selectAndInsertDebugConfig(document.object, position, token); - - verify(documentManager.activeTextEditor).atLeast(1); - verify(documentManager.activeTextEditor).atLeast(1); - verify(workspace.getWorkspaceFolder(docUri)).atLeast(1); - textEditor.verifyAll(); - document.verifyAll(); - assert.equal(debugConfigInserted, true); - }); -}); 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 9e22a057bcc1..000000000000 --- a/src/test/debugger/extension/configuration/providers/djangoLaunch.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:no-any no-invalid-template-strings max-func-body-length - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../../client/common/application/workspace'; -import { FileSystem } from '../../../../../client/common/platform/fileSystem'; -import { PathUtils } from '../../../../../client/common/platform/pathUtils'; -import { IFileSystem } from '../../../../../client/common/platform/types'; -import { IPathUtils } from '../../../../../client/common/types'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { DjangoLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/djangoLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Django', () => { - let fs: IFileSystem; - let workspaceService: IWorkspaceService; - let pathUtils: IPathUtils; - let provider: TestDjangoLaunchDebugConfigurationProvider; - let input: MultiStepInput<DebugConfigurationState>; - class TestDjangoLaunchDebugConfigurationProvider extends DjangoLaunchDebugConfigurationProvider { - // tslint:disable-next-line:no-unnecessary-override - public resolveVariables(pythonPath: string, resource: Uri | undefined): string { - return super.resolveVariables(pythonPath, resource); - } - // tslint:disable-next-line:no-unnecessary-override - public async getManagePyPath(folder: WorkspaceFolder): Promise<string | undefined> { - return super.getManagePyPath(folder); - } - } - setup(() => { - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - pathUtils = mock(PathUtils); - input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); - provider = new TestDjangoLaunchDebugConfigurationProvider(instance(fs), instance(workspaceService), instance(pathUtils)); - }); - test('getManagePyPath should return undefined if file doesn\'t exist', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - when(fs.fileExists(managePyPath)).thenResolve(false); - - const file = await provider.getManagePyPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getManagePyPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'manage.py'); - - when(pathUtils.separator).thenReturn('-'); - when(fs.fileExists(managePyPath)).thenResolve(true); - - const file = await provider.getManagePyPath(folder); - - // tslint:disable-next-line:no-invalid-template-strings - expect(file).to.be.equal('${workspaceFolder}-manage.py'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); - - const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateManagePy(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateManagePy(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => ''; - - const error = await provider.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path doesn\'t exist', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz'; - - when(fs.fileExists('xyz')).thenResolve(false); - const error = await provider.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.txt'; - - when(fs.fileExists('xyz.txt')).thenResolve(true); - const error = await provider.validateManagePy(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is python', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.py'; - - when(fs.fileExists('xyz.py')).thenResolve(true); - const error = await provider.validateManagePy(folder, '', 'x'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getManagePyPath = () => Promise.resolve('xyz.py'); - when(pathUtils.separator).thenReturn('-'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - program: 'xyz.py', - args: [ - 'runserver', - '--noreload' - ], - django: true - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getManagePyPath = () => Promise.resolve(undefined); - when(pathUtils.separator).thenReturn('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - program: 'hello', - args: [ - 'runserver', - '--noreload' - ], - django: true - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getManagePyPath = () => Promise.resolve(undefined); - const workspaceFolderToken = '${workspaceFolder}'; - const defaultProgram = `${workspaceFolderToken}-manage.py`; - - when(pathUtils.separator).thenReturn('-'); - when(input.showInputBox(anything())).thenResolve(); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.django.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - program: defaultProgram, - args: [ - 'runserver', - '--noreload' - ], - django: true - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts deleted file mode 100644 index cb06defdeb9c..000000000000 --- a/src/test/debugger/extension/configuration/providers/fileLaunch.unit.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any no-invalid-template-strings max-func-body-length - -import { expect } from 'chai'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { FileLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/fileLaunch'; - -suite('Debugging - Configuration Provider File', () => { - let provider: FileLaunchDebugConfigurationProvider; - setup(() => { - provider = new FileLaunchDebugConfigurationProvider(); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - - await provider.buildConfiguration(undefined as any, state); - - const config = { - name: DebugConfigStrings.file.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - // tslint:disable-next-line:no-invalid-template-strings - program: '${file}', - console: 'integratedTerminal' - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/flaskLaunch.unit.test.ts deleted file mode 100644 index 79cc78310903..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 { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { FlaskLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/flaskLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Flask', () => { - let fs: IFileSystem; - let provider: TestFlaskLaunchDebugConfigurationProvider; - let input: MultiStepInput<DebugConfigurationState>; - class TestFlaskLaunchDebugConfigurationProvider extends FlaskLaunchDebugConfigurationProvider { - // tslint:disable-next-line:no-unnecessary-override - public async getApplicationPath(folder: WorkspaceFolder): Promise<string | undefined> { - return super.getApplicationPath(folder); - } - } - setup(() => { - fs = mock(FileSystem); - input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); - provider = new TestFlaskLaunchDebugConfigurationProvider(instance(fs)); - }); - test('getApplicationPath should return undefined if file doesn\'t exist', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - when(fs.fileExists(appPyPath)).thenResolve(false); - - const file = await provider.getApplicationPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getApplicationPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const appPyPath = path.join(folder.uri.fsPath, 'app.py'); - - when(fs.fileExists(appPyPath)).thenResolve(true); - - const file = await provider.getApplicationPath(folder); - - // tslint:disable-next-line:no-invalid-template-strings - expect(file).to.be.equal('app.py'); - }); - test('Launch JSON with valid python path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve('xyz.py'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'xyz.py', - FLASK_ENV: 'development', - FLASK_DEBUG: '0' - }, - args: [ - 'run', - '--no-debugger', - '--no-reload' - ], - jinja: true - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected app path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve(undefined); - - when(input.showInputBox(anything())).thenResolve('hello'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'hello', - FLASK_ENV: 'development', - FLASK_DEBUG: '0' - }, - args: [ - 'run', - '--no-debugger', - '--no-reload' - ], - jinja: true - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default managepy path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getApplicationPath = () => Promise.resolve(undefined); - - when(input.showInputBox(anything())).thenResolve(); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.flask.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: 'flask', - env: { - FLASK_APP: 'app.py', - FLASK_ENV: 'development', - FLASK_DEBUG: '0' - }, - args: [ - 'run', - '--no-debugger', - '--no-reload' - ], - jinja: true - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts b/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts deleted file mode 100644 index cf1248c9da26..000000000000 --- a/src/test/debugger/extension/configuration/providers/moduleLaunch.unit.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any no-invalid-template-strings max-func-body-length - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { ModuleLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/moduleLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Module', () => { - let provider: ModuleLaunchDebugConfigurationProvider; - setup(() => { - provider = new ModuleLaunchDebugConfigurationProvider(); - }); - test('Launch JSON with default module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve(); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: DebugConfigStrings.module.snippet.default(), - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected module name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - const input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); - - when(input.showInputBox(anything())).thenResolve('hello'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.module.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - module: 'hello' - }; - - expect(state.config).to.be.deep.equal(config); - }); -}); diff --git a/src/test/debugger/extension/configuration/providers/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<DebugConfigurationType, IDebugConfigurationProvider>; - let factory: IDebugConfigurationProviderFactory; - setup(() => { - mappedProviders = new Map<DebugConfigurationType, IDebugConfigurationProvider>(); - getNamesAndValues<DebugConfigurationType>(DebugConfigurationType).forEach(item => { - mappedProviders.set(item.value, item.value as any as IDebugConfigurationProvider); - }); - factory = new DebugConfigurationProviderFactory( - mappedProviders.get(DebugConfigurationType.launchFlask)!, - mappedProviders.get(DebugConfigurationType.launchDjango)!, - mappedProviders.get(DebugConfigurationType.launchModule)!, - mappedProviders.get(DebugConfigurationType.launchFile)!, - mappedProviders.get(DebugConfigurationType.launchPyramid)!, - mappedProviders.get(DebugConfigurationType.remoteAttach)! - ); - }); - getNamesAndValues<DebugConfigurationType>(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 3fa6385cfc70..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 { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { PyramidLaunchDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/pyramidLaunch'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; - -suite('Debugging - Configuration Provider Pyramid', () => { - let fs: IFileSystem; - let workspaceService: IWorkspaceService; - let pathUtils: IPathUtils; - let provider: TestPyramidLaunchDebugConfigurationProvider; - let input: MultiStepInput<DebugConfigurationState>; - class TestPyramidLaunchDebugConfigurationProvider extends PyramidLaunchDebugConfigurationProvider { - // tslint:disable-next-line:no-unnecessary-override - public resolveVariables(pythonPath: string, resource: Uri | undefined): string { - return super.resolveVariables(pythonPath, resource); - } - // tslint:disable-next-line:no-unnecessary-override - public async getDevelopmentIniPath(folder: WorkspaceFolder): Promise<string | undefined> { - return super.getDevelopmentIniPath(folder); - } - } - setup(() => { - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - pathUtils = mock(PathUtils); - input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); - provider = new TestPyramidLaunchDebugConfigurationProvider(instance(fs), instance(workspaceService), instance(pathUtils)); - }); - test('getDevelopmentIniPath should return undefined if file doesn\'t exist', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - when(fs.fileExists(managePyPath)).thenResolve(false); - - const file = await provider.getDevelopmentIniPath(folder); - - expect(file).to.be.equal(undefined, 'Should return undefined'); - }); - test('getDevelopmentIniPath should file path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const managePyPath = path.join(folder.uri.fsPath, 'development.ini'); - - when(pathUtils.separator).thenReturn('-'); - when(fs.fileExists(managePyPath)).thenResolve(true); - - const file = await provider.getDevelopmentIniPath(folder); - - // tslint:disable-next-line:no-invalid-template-strings - expect(file).to.be.equal('${workspaceFolder}-development.ini'); - }); - test('Resolve variables (with resource)', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(folder); - - const resolvedPath = provider.resolveVariables('${workspaceFolder}/one.py', Uri.file('')); - - expect(resolvedPath).to.be.equal(`${folder.uri.fsPath}/one.py`); - }); - test('Validation of path should return errors if path is undefined', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateIniPath(folder, ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - - const error = await provider.validateIniPath(folder, '', ''); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is empty', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => ''; - - const error = await provider.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path doesn\'t exist', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz'; - - when(fs.fileExists('xyz')).thenResolve(false); - const error = await provider.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is non-ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.txt'; - - when(fs.fileExists('xyz.txt')).thenResolve(true); - const error = await provider.validateIniPath(folder, '', 'x'); - - expect(error).to.be.length.greaterThan(1); - }); - test('Validation of path should return errors if resolved path is ini', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - provider.resolveVariables = () => 'xyz.ini'; - - when(fs.fileExists('xyz.ini')).thenResolve(true); - const error = await provider.validateIniPath(folder, '', 'x'); - - expect(error).to.be.equal(undefined, 'should not have errors'); - }); - test('Launch JSON with valid ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getDevelopmentIniPath = () => Promise.resolve('xyz.ini'); - when(pathUtils.separator).thenReturn('-'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - args: [ - 'xyz.ini' - ], - pyramid: true, - jinja: true - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with selected ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getDevelopmentIniPath = () => Promise.resolve(undefined); - when(pathUtils.separator).thenReturn('-'); - when(input.showInputBox(anything())).thenResolve('hello'); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - args: [ - 'hello' - ], - pyramid: true, - jinja: true - }; - - expect(state.config).to.be.deep.equal(config); - }); - test('Launch JSON with default ini path', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - provider.getDevelopmentIniPath = () => Promise.resolve(undefined); - const workspaceFolderToken = '${workspaceFolder}'; - const defaultIni = `${workspaceFolderToken}-development.ini`; - - when(pathUtils.separator).thenReturn('-'); - when(input.showInputBox(anything())).thenResolve(); - - await provider.buildConfiguration(instance(input), state); - - const config = { - name: DebugConfigStrings.pyramid.snippet.name(), - type: DebuggerTypeName, - request: 'launch', - 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 1de9805d11de..000000000000 --- a/src/test/debugger/extension/configuration/providers/remoteAttach.unit.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any no-invalid-template-strings max-func-body-length - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { DebugConfigStrings } from '../../../../../client/common/utils/localize'; -import { MultiStepInput } from '../../../../../client/common/utils/multiStepInput'; -import { DebuggerTypeName } from '../../../../../client/debugger/constants'; -import { RemoteAttachDebugConfigurationProvider } from '../../../../../client/debugger/extension/configuration/providers/remoteAttach'; -import { DebugConfigurationState } from '../../../../../client/debugger/extension/types'; -import { AttachRequestArguments } from '../../../../../client/debugger/types'; - -suite('Debugging - Configuration Provider Remote Attach', () => { - let provider: TestRemoteAttachDebugConfigurationProvider; - let input: MultiStepInput<DebugConfigurationState>; - class TestRemoteAttachDebugConfigurationProvider extends RemoteAttachDebugConfigurationProvider { - // tslint:disable-next-line:no-unnecessary-override - public async configurePort(i: MultiStepInput<DebugConfigurationState>, config: Partial<AttachRequestArguments>) { - return super.configurePort(i, config); - } - } - setup(() => { - input = mock<MultiStepInput<DebugConfigurationState>>(MultiStepInput); - provider = new TestRemoteAttachDebugConfigurationProvider(); - }); - test('Configure port will display prompt', async () => { - when(input.showInputBox(anything())).thenResolve(); - - await provider.configurePort(instance(input), {}); - - verify(input.showInputBox(anything())).once(); - }); - test('Configure port will default to 5678 if entered value is not a number', async () => { - const config: { 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: DebugConfigStrings.attach.snippet.name(), - type: DebuggerTypeName, - request: 'attach', - port: 5678, - host: 'localhost', - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.' - } - ] - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); - test('Launch JSON with user defined host name', async () => { - const folder = { uri: Uri.parse(path.join('one', 'two')), name: '1', index: 0 }; - const state = { config: {}, folder }; - let portConfigured = false; - when(input.showInputBox(anything())).thenResolve('Hello'); - provider.configurePort = (_, cfg) => { - portConfigured = true; - cfg.port = 9999; - return Promise.resolve(); - }; - - const configurePort = await provider.buildConfiguration(instance(input), state); - if (configurePort) { - await configurePort(input, state); - } - - const config = { - name: DebugConfigStrings.attach.snippet.name(), - type: DebuggerTypeName, - request: 'attach', - port: 9999, - host: 'Hello', - pathMappings: [ - { - localRoot: '${workspaceFolder}', - remoteRoot: '.' - } - ] - }; - - expect(state.config).to.be.deep.equal(config); - expect(portConfigured).to.be.equal(true, 'Port not configured'); - }); -}); diff --git a/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/attach.unit.test.ts index bcb3294fa306..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,143 +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<IServiceContainer>; + + 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<IPlatformService>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let documentManager: TypeMoq.IMock<IDocumentManager>; let configurationService: TypeMoq.IMock<IConfigurationService>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - const debugOptionsAvailable = [DebugOptions.RedirectOutput]; - if (os.value === OSType.Windows) { - debugOptionsAvailable.push(DebugOptions.FixFilePathCase); - debugOptionsAvailable.push(DebugOptions.WindowsClient); - } else { - debugOptionsAvailable.push(DebugOptions.UnixClient); - } - debugOptionsAvailable.push(DebugOptions.ShowReturnValue); + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let getActiveTextEditorStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + const debugOptionsAvailable = getAvailableOptions(); + setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - 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<IDocumentManager>(); - debugProvider = new AttachConfigurationResolver(workspaceService.object, documentManager.object, platformService.object, configurationService.object); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + debugProvider = new AttachConfigurationResolver(configurationService.object, interpreterService.object); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + getOSTypeStub = sinon.stub(platform, 'getOSType'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getOSTypeStub.returns(osType); + }); + + teardown(() => { + sinon.restore(); }); + function createMoqWorkspaceFolder(folderPath: string) { const folder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); + folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); return folder.object; } + function setupActiveEditor(fileName: string | undefined, languageId: string) { if (fileName) { const textEditor = TypeMoq.Mock.ofType<TextEditor>(); const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(d => d.languageId).returns(() => languageId); - document.setup(d => d.fileName).returns(() => fileName); - textEditor.setup(t => t.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); + 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<AttachRequestArguments> = { + name: 'Python attach', + type: 'python', + request: 'attach', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + attachConfig: Partial<AttachRequestArguments>, + ) { + let config = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + attachConfig as DebugConfiguration, + ); + if (config === undefined || config === null) { + return config; + } + + config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); + if (config === undefined || config === null) { + return config; + } + + return config as AttachRequestArguments; + } + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { const workspaceFolder = createMoqWorkspaceFolder(__dirname); const pythonFile = 'xyz.py'; setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { request: 'attach' } as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(workspaceFolder, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { const pythonFile = 'xyz.py'; setupActiveEditor(pythonFile, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { setupActiveEditor(undefined, PYTHON_LANGUAGE); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { const activeFile = 'xyz.js'; setupActiveEditor(activeFile, 'javascript'); setupWorkspaces([]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.not.have.property('localRoot'); expect(debugConfig).to.have.property('host', 'localhost'); }); + test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { const activeFile = 'xyz.py'; setupActiveEditor(activeFile, PYTHON_LANGUAGE); const defaultWorkspace = path.join('usr', 'desktop'); setupWorkspaces([defaultWorkspace]); - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, { request: 'attach' } as DebugConfiguration); + const debugConfig = await resolveDebugConfiguration(undefined, { + request: 'attach', + }); expect(Object.keys(debugConfig!)).to.have.lengthOf.least(3); expect(debugConfig).to.have.property('request', 'attach'); + expect(debugConfig).to.have.property('clientOS', getClientOS()); expect(debugConfig).to.have.property('debugOptions').deep.equal(debugOptionsAvailable); expect(debugConfig).to.have.property('host', 'localhost'); }); - test('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); @@ -147,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); @@ -160,19 +245,23 @@ 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 (os.name !== 'Windows') { + 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); @@ -180,17 +269,49 @@ 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 pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - const lowercasedLocalRoot = path.join('c:', 'Debug', 'Python_Path'); + 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); - expect(pathMappings![0].localRoot).to.be.equal(lowercasedLocalRoot); + 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 (os.name !== 'Windows') { + + 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); @@ -198,13 +319,54 @@ getNamesAndValues(OSType).forEach(os => { setupWorkspaces([defaultWorkspace]); const localRoot = `Debug_PythonPath_${new Date().toString()}`; - const debugPathMappings = [ { localRoot: path.join('${workspaceFolder}', localRoot), remoteRoot: '/app/' }]; - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { localRoot, pathMappings: debugPathMappings, host, request: 'attach' } as any as DebugConfiguration); - const pathMappings = (debugConfig as AttachRequestArguments).pathMappings; - const lowercasedLocalRoot = path.join('c:', 'Debug', 'Python_Path', localRoot); + 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]); - expect(pathMappings![0].localRoot).to.be.equal(lowercasedLocalRoot); + 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')); @@ -213,13 +375,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 pathMappings = (debugConfig as AttachRequestArguments).pathMappings; + 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); @@ -228,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); @@ -244,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); @@ -258,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); @@ -271,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); @@ -283,85 +470,31 @@ 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); }); - - const testsForJustMyCode = - [ - { - justMyCode: false, - debugStdLib: true, - expectedResult: false - }, - { - justMyCode: false, - debugStdLib: false, - expectedResult: false - }, - { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false - }, - { - justMyCode: true, - debugStdLib: false, - expectedResult: true - }, - { - justMyCode: true, - debugStdLib: true, - expectedResult: true - }, - { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true - }, - { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true - }, - { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false - }, - { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true - } - ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { - const activeFile = 'xyz.py'; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - const defaultWorkspace = path.join('usr', 'desktop'); - setupWorkspaces([defaultWorkspace]); - - const debugOptions = debugOptionsAvailable.slice().concat(DebugOptions.Jinja, DebugOptions.Sudo); - - testsForJustMyCode.forEach(async testParams => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugOptions, request: 'attach', justMyCode: testParams.justMyCode, debugStdLib: testParams.debugStdLib } as any as DebugConfiguration); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); - }); - }); }); }); diff --git a/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/base.unit.test.ts index 8e2a4e70bdf8..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<AttachRequestArguments | LaunchRequestArguments> { - public resolveDebugConfiguration(_folder: WorkspaceFolder | undefined, _debugConfiguration: DebugConfiguration, _token?: CancellationToken): Promise<AttachRequestArguments | LaunchRequestArguments | undefined> { + public resolveDebugConfiguration( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise<AttachRequestArguments | LaunchRequestArguments | undefined> { throw new Error('Not Implemented'); } - public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { - return super.getWorkspaceFolder(folder); + + public resolveDebugConfigurationWithSubstitutedVariables( + _folder: WorkspaceFolder | undefined, + _debugConfiguration: DebugConfiguration, + _token?: CancellationToken, + ): Promise<AttachRequestArguments | LaunchRequestArguments | undefined> { + throw new Error('Not Implemented'); } - public getProgram(): string | undefined { - return super.getProgram(); + + public getWorkspaceFolder(folder: WorkspaceFolder | undefined): Uri | undefined { + return BaseConfigurationResolver.getWorkspaceFolder(folder); } - public resolveAndUpdatePythonPath(workspaceFolder: Uri | undefined, debugConfiguration: LaunchRequestArguments): void { - return super.resolveAndUpdatePythonPath(workspaceFolder, debugConfiguration); + + 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<LaunchRequestArguments & AttachRequestArguments>) { + return BaseConfigurationResolver.isDebuggingFastAPI(debugConfiguration); + } + public isDebuggingFlask(debugConfiguration: Partial<LaunchRequestArguments & AttachRequestArguments>) { - return super.isDebuggingFlask(debugConfiguration); + return BaseConfigurationResolver.isDebuggingFlask(debugConfiguration); } } let resolver: BaseResolver; - let workspaceService: IWorkspaceService; - let 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<IInterpreterService>(); + resolver = new BaseResolver(instance(configurationService), instance(interpreterService)); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getProgramStub = sinon.stub(helper, 'getProgram'); }); - - test('Program should return filepath of active editor if file is python', () => { - const expectedFileName = 'my.py'; - const editor = typemoq.Mock.ofType<TextEditor>(); - const doc = typemoq.Mock.ofType<TextDocument>(); - - editor.setup(e => e.document).returns(() => doc.object).verifiable(typemoq.Times.once()); - doc.setup(d => d.languageId).returns(() => PYTHON_LANGUAGE).verifiable(typemoq.Times.once()); - doc.setup(d => d.fileName).returns(() => expectedFileName).verifiable(typemoq.Times.once()); - when(documentManager.activeTextEditor).thenReturn(editor.object); - - const program = resolver.getProgram(); - - expect(program).to.be.equal(expectedFileName); + teardown(() => { + sinon.restore(); }); - test('Program should return undefined if active file is not python', () => { - const editor = typemoq.Mock.ofType<TextEditor>(); - const doc = typemoq.Mock.ofType<TextDocument>(); - - editor.setup(e => e.document).returns(() => doc.object).verifiable(typemoq.Times.once()); - doc.setup(d => d.languageId).returns(() => 'C#').verifiable(typemoq.Times.once()); - when(documentManager.activeTextEditor).thenReturn(editor.object); - - const program = resolver.getProgram(); - expect(program).to.be.equal(undefined, 'Not undefined'); - }); - test('Program should return undefined if there is no active editor', () => { - when(documentManager.activeTextEditor).thenReturn(undefined); - - const program = resolver.getProgram(); - - expect(program).to.be.equal(undefined, 'Not undefined'); - }); test('Should get workspace folder when workspace folder is provided', () => { const expectedUri = Uri.parse('mock'); const folder: WorkspaceFolder = { index: 0, uri: expectedUri, name: 'mock' }; @@ -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: Record<string, boolean> = { localhost: true, '127.0.0.1': true, '::1': true, '127.0.0.2': false, '156.1.2.3': false, '::2': false }; - Object.keys(localHostTestMatrix) - .forEach(key => { - test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { - const isLocalHost = resolver.isLocalHost(key); - expect(isLocalHost).to.equal(localHostTestMatrix[key]); - }); + test('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<string, boolean> = { + localhost: true, + '127.0.0.1': true, + '::1': true, + '127.0.0.2': false, + '156.1.2.3': false, + '::2': false, + }; + Object.keys(localHostTestMatrix).forEach((key) => { + test(`Local host = ${localHostTestMatrix[key]} for ${key}`, () => { + const isLocalHost = resolver.isLocalHost(key); + + expect(isLocalHost).to.equal(localHostTestMatrix[key]); }); + }); + test('Is debugging 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<TextEditor>(); + const doc = typemoq.Mock.ofType<TextDocument>(); + + editor + .setup((e) => e.document) + .returns(() => doc.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.languageId) + .returns(() => PYTHON_LANGUAGE) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.fileName) + .returns(() => expectedFileName) + .verifiable(typemoq.Times.once()); + + getActiveTextEditorStub.returns(editor.object); + + const program = getProgram(); + + expect(program).to.be.equal(expectedFileName); + }); + test('Program should return undefined if active file is not python', () => { + const editor = typemoq.Mock.ofType<TextEditor>(); + const doc = typemoq.Mock.ofType<TextDocument>(); + + editor + .setup((e) => e.document) + .returns(() => doc.object) + .verifiable(typemoq.Times.once()); + doc.setup((d) => d.languageId) + .returns(() => 'C#') + .verifiable(typemoq.Times.once()); + getActiveTextEditorStub.returns(editor.object); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); + test('Program should return undefined if there is no active editor', () => { + getActiveTextEditorStub.returns(undefined); + + const program = getProgram(); + + expect(program).to.be.equal(undefined, 'Not undefined'); + }); +}); diff --git a/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts b/src/test/debugger/extension/configuration/resolvers/launch.unit.test.ts index d56264d86b55..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,541 +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<IServiceContainer>; - let debugProvider: DebugConfigurationProvider; - let platformService: TypeMoq.IMock<IPlatformService>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let pythonExecutionService: TypeMoq.IMock<IPythonExecutionService>; - let logger: TypeMoq.IMock<ILogger>; - let helper: TypeMoq.IMock<IInterpreterHelper>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let diagnosticsService: TypeMoq.IMock<IInvalidPythonPathInDebuggerService>; - function createMoqWorkspaceFolder(folderPath: string) { - const folder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - folder.setup(f => f.uri).returns(() => Uri.file(folderPath)); - return folder.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 setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder, isWindows: boolean = false, isMac: boolean = false, isLinux: boolean = false) { - const confgService = TypeMoq.Mock.ofType<IConfigurationService>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - logger = TypeMoq.Mock.ofType<ILogger>(); - diagnosticsService = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>(); - - pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); - helper = TypeMoq.Mock.ofType<IInterpreterHelper>(); - pythonExecutionService.setup((x: any) => x.then).returns(() => undefined); - const factory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); - factory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(pythonExecutionService.object)); - helper.setup(h => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - const 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<IPythonSettings>(); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - if (workspaceFolder) { - settings.setup(s => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); + + suite(`Debugging - Config Resolver Launch, OS = ${osName}`, () => { + let debugProvider: DebugConfigurationProvider; + let pythonExecutionService: TypeMoq.IMock<IPythonExecutionService>; + let helper: TypeMoq.IMock<IInterpreterHelper>; + const envVars = { FOO: 'BAR' }; + + let diagnosticsService: TypeMoq.IMock<IInvalidPythonPathInDebuggerService>; + let configService: TypeMoq.IMock<IConfigurationService>; + let debugEnvHelper: TypeMoq.IMock<IDebugEnvironmentVariablesService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let environmentActivationService: TypeMoq.IMock<IEnvironmentActivationService>; + let getActiveTextEditorStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + getOSTypeStub = sinon.stub(platform, 'getOSType'); + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getOSTypeStub.returns(osType); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + function createMoqWorkspaceFolder(folderPath: string) { + const folder = TypeMoq.Mock.ofType<WorkspaceFolder>(); + folder.setup((f) => f.uri).returns(() => Uri.file(folderPath)); + return folder.object; } - 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); - } - function setupActiveEditor(fileName: string | undefined, languageId: string) { - if (fileName) { - const textEditor = TypeMoq.Mock.ofType<TextEditor>(); - const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(d => d.languageId).returns(() => languageId); - document.setup(d => d.fileName).returns(() => fileName); - textEditor.setup(t => t.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - } else { - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + function getClientOS() { + return osType === platform.OSType.Windows ? 'windows' : 'unix'; } - 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, workspaceFolder); - - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an object with \'noDebug\' property is passed with a Workspace Folder and active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, workspaceFolder); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { noDebug: true } as any as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(__dirname.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(__dirname, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const pythonFile = 'xyz.py'; - setupIoc(pythonPath, createMoqWorkspaceFolder(path.dirname(pythonFile))); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - const filePath = Uri.file(path.dirname('')).fsPath; - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', pythonFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and no active file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - setupIoc(pythonPath); - setupActiveEditor(undefined, PYTHON_LANGUAGE); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, no workspaces and non python file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.js'; - setupIoc(pythonPath); - setupActiveEditor(activeFile, 'javascript'); - setupWorkspaces([]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', ''); - expect(debugConfig).not.to.have.property('cwd'); - expect(debugConfig).not.to.have.property('envFile'); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Defaults should be returned when an empty object is passed without Workspace Folder, with a workspace and an active python file', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const activeFile = 'xyz.py'; - const defaultWorkspace = path.join('usr', 'desktop'); - setupIoc(pythonPath, createMoqWorkspaceFolder(defaultWorkspace)); - setupActiveEditor(activeFile, PYTHON_LANGUAGE); - setupWorkspaces([defaultWorkspace]); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(undefined, {} as DebugConfiguration); - const filePath = Uri.file(defaultWorkspace).fsPath; - - expect(Object.keys(debugConfig!)).to.have.lengthOf.above(3); - expect(debugConfig).to.have.property('pythonPath', pythonPath); - expect(debugConfig).to.have.property('type', 'python'); - expect(debugConfig).to.have.property('request', 'launch'); - expect(debugConfig).to.have.property('program', activeFile); - expect(debugConfig).to.have.property('cwd'); - expect(debugConfig!.cwd!.toLowerCase()).to.be.equal(filePath.toLowerCase()); - expect(debugConfig).to.have.property('envFile'); - expect(debugConfig!.envFile!.toLowerCase()).to.be.equal(path.join(filePath, '.env2').toLowerCase()); - expect(debugConfig).to.have.property('env'); - // tslint:disable-next-line:no-any - expect(Object.keys((debugConfig as any).env)).to.have.lengthOf(0); - }); - test('Ensure `${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', true); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([DebugOptions.ShowReturnValue, DebugOptions.RedirectOutput]); - }); - test('Test defaults of python debugger', async () => { - if ('python' === DebuggerTypeName) { - return; + + function setupIoc(pythonPath: string, workspaceFolder?: WorkspaceFolder) { + environmentActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + diagnosticsService = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>(); + debugEnvHelper = TypeMoq.Mock.ofType<IDebugEnvironmentVariablesService>(); + pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); + helper = TypeMoq.Mock.ofType<IInterpreterHelper>(); + const factory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + factory + .setup((f) => f.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(pythonExecutionService.object)); + helper.setup((h) => h.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({})); + diagnosticsService + .setup((h) => h.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(true)); + + const settings = TypeMoq.Mock.ofType<IPythonSettings>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + // interpreterService + // .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + // .returns(() => Promise.resolve({ path: pythonPath } as any)); + settings.setup((s) => s.pythonPath).returns(() => pythonPath); + if (workspaceFolder) { + settings.setup((s) => s.envFile).returns(() => path.join(workspaceFolder!.uri.fsPath, '.env2')); + } + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + 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, + ); } - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, {} as DebugConfiguration); - - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', true); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([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, justMyCode: false } as LaunchRequestArguments); - - expect(debugConfig).to.have.property('console', 'integratedTerminal'); - expect(debugConfig).to.have.property('stopOnEntry', false); - expect(debugConfig).to.have.property('showReturnValue', true); - expect(debugConfig).to.have.property('justMyCode', false); - expect(debugConfig).to.have.property('debugOptions'); - expect((debugConfig as any).debugOptions).to.be.deep.equal([DebugOptions.DebugStdLib, DebugOptions.ShowReturnValue]); - }); - const testsForJustMyCode = - [ - { + + function setupActiveEditor(fileName: string | undefined, languageId: string) { + if (fileName) { + const textEditor = TypeMoq.Mock.ofType<TextEditor>(); + const document = TypeMoq.Mock.ofType<TextDocument>(); + document.setup((d) => d.languageId).returns(() => languageId); + document.setup((d) => d.fileName).returns(() => fileName); + textEditor.setup((t) => t.document).returns(() => document.object); + getActiveTextEditorStub.returns(textEditor.object); + } else { + getActiveTextEditorStub.returns(undefined); + } + } + + function setupWorkspaces(folders: string[]) { + const workspaceFolders = folders.map(createMoqWorkspaceFolder); + getWorkspaceFolderStub.returns(workspaceFolders); + } + + const launch: LaunchRequestArguments = { + name: 'Python launch', + type: 'python', + request: 'launch', + }; + + async function resolveDebugConfiguration( + workspaceFolder: WorkspaceFolder | undefined, + launchConfig: Partial<LaunchRequestArguments>, + ) { + let config = await debugProvider.resolveDebugConfiguration!( + workspaceFolder, + launchConfig as DebugConfiguration, + ); + if (config === undefined || config === null) { + return config; + } + + const interpreterPath = configService.object.getSettings(workspaceFolder ? workspaceFolder.uri : undefined) + .pythonPath; + for (const key of Object.keys(config)) { + const value = config[key]; + if (typeof value === 'string') { + config[key] = value.replace('${command:python.interpreterPath}', interpreterPath); + } + } + + config = await debugProvider.resolveDebugConfigurationWithSubstitutedVariables!(workspaceFolder, config); + if (config === undefined || config === null) { + return config; + } + + return config as LaunchRequestArguments; + } + + test('Defaults should be returned when an empty object is passed with a Workspace Folder and active file', async () => { + const pythonPath = `PythonPath_${new Date().toString()}`; + const workspaceFolder = createMoqWorkspaceFolder(__dirname); + const pythonFile = 'xyz.py'; + setupIoc(pythonPath, workspaceFolder); + setupActiveEditor(pythonFile, PYTHON_LANGUAGE); + + const debugConfig = await 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('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("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, - debugStdLib: true, - expectedResult: 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, }, { - justMyCode: false, - debugStdLib: false, - expectedResult: false + console: 'integratedTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined, }, { - justMyCode: false, - debugStdLib: undefined, - expectedResult: false + console: 'externalTerminal', + redirectOutput: undefined, + expectedRedirectOutput: undefined, }, { - justMyCode: true, - debugStdLib: false, - expectedResult: true + console: 'internalConsole', + redirectOutput: false, + expectedRedirectOutput: false, }, { - justMyCode: true, - debugStdLib: true, - expectedResult: true + console: 'integratedTerminal', + redirectOutput: false, + expectedRedirectOutput: false, }, { - justMyCode: true, - debugStdLib: undefined, - expectedResult: true + console: 'externalTerminal', + redirectOutput: false, + expectedRedirectOutput: false, }, { - justMyCode: undefined, - debugStdLib: false, - expectedResult: true + console: 'internalConsole', + redirectOutput: true, + expectedRedirectOutput: true, }, { - justMyCode: undefined, - debugStdLib: true, - expectedResult: false + console: 'integratedTerminal', + redirectOutput: true, + expectedRedirectOutput: true, }, { - justMyCode: undefined, - debugStdLib: undefined, - expectedResult: true - } + console: 'externalTerminal', + redirectOutput: true, + expectedRedirectOutput: true, + }, ]; - test('Ensure justMyCode property is correctly derived from debugStdLib', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - testsForJustMyCode.forEach(async testParams => { - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { debugStdLib: testParams.debugStdLib, justMyCode: testParams.justMyCode } as LaunchRequestArguments); - expect(debugConfig).to.have.property('justMyCode', testParams.expectedResult); + 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); + } + }); }); - }); - 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, undefined, 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); - } - } - 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, undefined, 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)); - } - 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); + + 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 any).debugOptions).contains(DebugOptions.Jinja); - } else { - expect(debugConfig!.program).to.be.not.equal(pserveFilePath); - } - 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(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.be.equal(undefined, 'Not undefined'); - }); - test('Test validation of Python Path when launching debugger (with valid python path)', async () => { - const pythonPath = `PythonPath_${new Date().toString()}`; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - const pythonFile = 'xyz.py'; - setupIoc(pythonPath); - setupActiveEditor(pythonFile, PYTHON_LANGUAGE); - - diagnosticsService.reset(); - diagnosticsService - .setup(h => h.validatePythonPath(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, { redirectOutput: false, pythonPath } as LaunchRequestArguments); - - diagnosticsService.verifyAll(); - expect(debugConfig).to.not.be.equal(undefined, 'is undefined'); - }); - async function testSetting(requestType: 'launch' | 'attach', settings: Record<string, boolean>, debugOptionName: DebugOptions, mustHaveDebugOption: boolean) { - setupIoc('pythonPath'); - const debugConfiguration: DebugConfiguration = { request: requestType, type: 'python', name: '', ...settings }; - const workspaceFolder = createMoqWorkspaceFolder(__dirname); - - const debugConfig = await debugProvider.resolveDebugConfiguration!(workspaceFolder, debugConfiguration); - if (mustHaveDebugOption) { - expect((debugConfig as any).debugOptions).contains(debugOptionName); - } else { - expect((debugConfig as any).debugOptions).not.contains(debugOptionName); - } - } - type LaunchOrAttach = 'launch' | 'attach'; - const items: LaunchOrAttach[] = ['launch', 'attach']; - items.forEach(requestType => { - test(`Must not contain Sub Process when not specified (${requestType})`, async () => { - await testSetting(requestType, {}, DebugOptions.SubProcess, false); + 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(`Must not contain Sub Process setting=false (${requestType})`, async () => { - await testSetting(requestType, { subProcess: false }, DebugOptions.SubProcess, false); + + 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(`Must not contain Sub Process setting=true (${requestType})`, async () => { - await testSetting(requestType, { subProcess: true }, DebugOptions.SubProcess, true); + + 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<string, boolean>, + 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<ICommandManager>; + let debugService: typemoq.IMock<IDebugService>; + let disposables: typemoq.IMock<IDisposableRegistry>; + let interpreterService: typemoq.IMock<IInterpreterService>; + let debugCommands: IExtensionSingleActivationService; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + + setup(() => { + commandManager = typemoq.Mock.ofType<ICommandManager>(); + commandManager + .setup((c) => c.executeCommand(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve()); + debugService = typemoq.Mock.ofType<IDebugService>(); + disposables = typemoq.Mock.ofType<IDisposableRegistry>(); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + sinon.stub(telemetry, 'sendTelemetryEvent').callsFake(() => { + /** noop */ + }); + triggerCreateEnvironmentCheckNonBlockingStub = sinon.stub( + triggerApis, + 'triggerCreateEnvironmentCheckNonBlocking', + ); + triggerCreateEnvironmentCheckNonBlockingStub.returns(undefined); + }); + teardown(() => { + sinon.restore(); + }); + test('Test registering debug file command', async () => { + commandManager + .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) + .returns(() => ({ + dispose: () => { + /* noop */ + }, + })) + .verifiable(typemoq.Times.once()); + + debugCommands = new DebugCommands( + commandManager.object, + debugService.object, + disposables.object, + interpreterService.object, + ); + await debugCommands.activate(); + commandManager.verifyAll(); + }); + test('Test running debug file command', async () => { + let callback: (f: Uri) => Promise<void> = (_f: Uri) => Promise.resolve(); + commandManager + .setup((c) => c.registerCommand(Commands.Debug_In_Terminal, typemoq.It.isAny())) + .callback((_name, cb) => { + callback = cb; + }); + debugService + .setup((d) => d.startDebugging(undefined, typemoq.It.isAny())) + .returns(() => Promise.resolve(true)) + .verifiable(typemoq.Times.once()); + + debugCommands = new DebugCommands( + commandManager.object, + debugService.object, + disposables.object, + interpreterService.object, + ); + await debugCommands.activate(); + + await callback(Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'test.py'))); + commandManager.verifyAll(); + debugService.verifyAll(); + }); +}); diff --git a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts index 2f195fc09639..b1053def2eba 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachHandler.unit.test.ts @@ -3,43 +3,68 @@ 'use strict'; -// tslint:disable:no-any - import { expect } from 'chai'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; import { ChildProcessAttachEventHandler } from '../../../../client/debugger/extension/hooks/childProcessAttachHandler'; import { ChildProcessAttachService } from '../../../../client/debugger/extension/hooks/childProcessAttachService'; -import { 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 = {}; - const session: any = {}; + const session: any = { configuration: { type: DebuggerTypeName } }; await handler.handleCustomEvent({ event: 'abc', body, session }); verify(attachService.attach(body, session)).never(); }); - test('Do not attach to child process if 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 = {}; - const session: any = {}; - await handler.handleCustomEvent({ event: PTVSDEvents.ChildProcessLaunched, body, session }); - verify(attachService.attach(body, session)).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 = {}; - const session: any = {}; + 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: PTVSDEvents.ChildProcessLaunched, body, session: {} as any }); + await handler.handleCustomEvent({ event: DebuggerEvents.DebugpyAttachToSubprocess, body, session }); verify(attachService.attach(body, anything())).once(); const [, secondArg] = capture(attachService.attach).last(); expect(secondArg).to.deep.equal(session); diff --git a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts index 0e9e07c3073a..118efe416e94 100644 --- a/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts +++ b/src/test/debugger/extension/hooks/childProcessAttachService.unit.test.ts @@ -3,343 +3,193 @@ 'use strict'; -// tslint:disable:no-any max-func-body-length - import { expect } from 'chai'; -import * as path from 'path'; +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 = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(true as any); - await service.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + showErrorMessageStub.returns(undefined); + + 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, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(anything(), anything(), anything())).thenResolve(false as any); - when(shell.showErrorMessage(anything())).thenResolve(); + showErrorMessageStub.resolves(() => {}); - await service.attach(data, session); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(anything(), anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).once(); + sinon.assert.calledOnce(showErrorMessageStub); }); test('Use correct workspace folder', async () => { - const 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, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([wkspace1, rightWorkspaceFolder, wkspace2]); + getWorkspaceFoldersStub.returns([wkspace1, rightWorkspaceFolder, wkspace2]); when(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).thenResolve(true as any); - await service.attach(data, session); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.called(getWorkspaceFoldersStub); verify(debugService.startDebugging(rightWorkspaceFolder, anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); test('Use empty workspace folder if right one is not found', async () => { - const 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, }; const session: any = {}; - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - when(workspaceService.workspaceFolders).thenReturn([wkspace1, wkspace2]); + getWorkspaceFoldersStub.returns([wkspace1, wkspace2]); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - await service.attach(data, session); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.called(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); - verify(shell.showErrorMessage(anything())).never(); + sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config 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); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - await service.attach(data, session); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); - verify(shell.showErrorMessage(anything())).never(); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); }); - test('Validate debug config when parent/root parent was attached', async () => { - const 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, - port: 1234, - processId: 2, - rootStartRequest: { - seq: 1, - type: 'python', - arguments: args, - command: 'request' - } - }; - - const debugConfig = JSON.parse(JSON.stringify(args)); - debugConfig.host = args.host!; - debugConfig.port = data.port; - debugConfig.name = `Child Process ${data.processId}`; - debugConfig.request = 'attach'; const session: any = {}; + const debugConfig = JSON.parse(JSON.stringify(data)); - when(workspaceService.hasWorkspaceFolders).thenReturn(false); + getWorkspaceFoldersStub.returns(undefined); when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - await service.attach(data, session); + await attachService.attach(data, session); - verify(workspaceService.hasWorkspaceFolders).once(); + sinon.assert.calledOnce(getWorkspaceFoldersStub); verify(debugService.startDebugging(undefined, anything(), anything())).once(); const [, secondArg, thirdArg] = capture(debugService.startDebugging).last(); expect(secondArg).to.deep.equal(debugConfig); - expect(thirdArg).to.deep.equal(session); - verify(shell.showErrorMessage(anything())).never(); - }); - test('Path mappings are not set when there is no 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 args: LaunchRequestArguments & AttachRequestArguments = { - request: 'launch', - type: 'python', - name: '', - pythonPath: '', args: [], envFile: '' - }; - - service.fixPathMappings(args); - - expect(args.pathMappings).to.equal(undefined, 'Not undefined'); - }); - test('Path mappings are left untouched when they are provided', async () => { - const shell = mock(ApplicationShell); - const debugService = mock(DebugService); - const workspaceService = mock(WorkspaceService); - const service = new ChildProcessAttachService(instance(shell), instance(debugService), instance(workspaceService)); - - const pathMappings = [{ localRoot: '1', remoteRoot: '2' }]; - const args: LaunchRequestArguments & AttachRequestArguments = { - request: 'launch', - type: 'python', - name: '', - workspaceFolder: __dirname, - pythonPath: '', args: [], envFile: '', - pathMappings: pathMappings - }; - - service.fixPathMappings(args); - - expect(args.pathMappings).to.deep.equal(pathMappings); - }); - test('Path mappings default to 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 expectedPathMappings = [{ localRoot: __dirname, remoteRoot: '.' }]; - const args: LaunchRequestArguments & AttachRequestArguments = { - request: 'launch', - type: 'python', - name: '', - workspaceFolder: __dirname, - pythonPath: '', args: [], envFile: '' - }; - - service.fixPathMappings(args); - - expect(args.pathMappings).to.deep.equal(expectedPathMappings); + expect(thirdArg).to.deep.equal({ parentSession: session, lifecycleManagedByParent: true }); + sinon.assert.notCalled(showErrorMessageStub); }); - test('Path mappings default to cwd 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 expectedPathMappings = [{ localRoot: path.join('hello', 'world'), remoteRoot: '.' }]; - const args: LaunchRequestArguments & AttachRequestArguments = { - request: 'launch', + test('Validate debug config when parent/root parent was attached', async () => { + const data: AttachRequestArguments = { + request: 'attach', type: 'python', - name: '', - cwd: path.join('hello', 'world'), - workspaceFolder: __dirname, - pythonPath: '', args: [], envFile: '' + name: 'Attach', + host: '123.123.123.123', + port: 1234, + subProcessId: 2, }; - service.fixPathMappings(args); - - expect(args.pathMappings).to.deep.equal(expectedPathMappings); - }); - test('Path mappings default to cwd folder relative to 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 debugConfig = JSON.parse(JSON.stringify(data)); + debugConfig.host = data.host; + debugConfig.port = data.port; + debugConfig.request = 'attach'; + const session: any = {}; - const expectedPathMappings = [{ localRoot: path.join(__dirname, 'hello', 'world'), remoteRoot: '.' }]; - const args: LaunchRequestArguments & AttachRequestArguments = { - request: 'launch', - type: 'python', - name: '', - // tslint:disable-next-line: no-invalid-template-strings - cwd: path.join('${workspaceFolder}', 'hello', 'world'), - workspaceFolder: __dirname, - pythonPath: '', args: [], envFile: '' - }; + getWorkspaceFoldersStub.returns(undefined); + when(debugService.startDebugging(undefined, anything(), anything())).thenResolve(true as any); - service.fixPathMappings(args); + await attachService.attach(data, session); - expect(args.pathMappings).to.deep.equal(expectedPathMappings); + 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); }); }); 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<IServiceManager>(); - - [ - [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>( + IChildProcessAttachService, + ChildProcessAttachService, + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugAdapterActivator, + ), + ).once(); + verify( + serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ), + ).once(); + verify( + serviceManager.addSingleton<IDebugSessionEventHandlers>( + IDebugSessionEventHandlers, + ChildProcessAttachEventHandler, + ), + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationResolver<LaunchRequestArguments>>( + IDebugConfigurationResolver, + LaunchConfigurationResolver, + 'launch', + ), + ).once(); + verify( + serviceManager.addSingleton<IDebugConfigurationResolver<AttachRequestArguments>>( + IDebugConfigurationResolver, + AttachConfigurationResolver, + 'attach', + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugAdapterActivator, + ), + ).once(); + verify( + serviceManager.addSingleton<IDebugAdapterDescriptorFactory>( + IDebugAdapterDescriptorFactory, + DebugAdapterDescriptorFactory, + ), + ).once(); + verify( + serviceManager.addSingleton<IDebugSessionLoggingFactory>( + IDebugSessionLoggingFactory, + DebugSessionLoggingFactory, + ), + ).once(); + verify( + serviceManager.addSingleton<IOutdatedDebuggerPromptFactory>( + IOutdatedDebuggerPromptFactory, + OutdatedDebuggerPromptFactory, + ), + ).once(); + verify( + serviceManager.addSingleton<IAttachProcessProviderFactory>( + IAttachProcessProviderFactory, + AttachProcessProviderFactory, + ), + ).once(); + verify( + serviceManager.addSingleton<IExtensionSingleActivationService>( + IExtensionSingleActivationService, + DebugCommands, + ), + ).once(); }); }); diff --git a/src/test/debugger/misc.test.ts b/src/test/debugger/misc.test.ts deleted file mode 100644 index 9e34a8bd691a..000000000000 --- a/src/test/debugger/misc.test.ts +++ /dev/null @@ -1,105 +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 { 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'; - -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; -suite(`Standard Debugging - Misc tests: ${debuggerType}`, () => { - - let debugClient: DebugClient; - // All tests in this suite are failed - // Check https://github.com/Microsoft/vscode-python/issues/4067 - setup(async function () { - return this.skip(); - - if (!IS_MULTI_ROOT_TEST || !TEST_DEBUGGER) { - // tslint:disable-next-line:no-invalid-this - return 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. - * @returns {DebugClient} - */ - function createDebugAdapter(): DebugClient { - return new DebugClient(process.env.NODE_PATH || 'node', testAdapterFilePath, debuggerType); - } - function buildLaunchArgs(pythonFile: string, stopOnEntry: boolean = false, showReturnValue: boolean = true): 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; - } - - // Check https://github.com/Microsoft/vscode-python/issues/4067 - test('Should run program to the end', async function () { - return this.skip(); - await Promise.all([ - debugClient.configurationSequence(), - debugClient.launch(buildLaunchArgs('simplePrint.py', false)), - debugClient.waitForEvent('initialized'), - debugClient.waitForEvent('terminated') - ]); - }); - // Check https://github.com/Microsoft/vscode-python/issues/4067 - test('test stderr output for Python', async function () { - return this.skip(); - 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 e29866dae606..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(process.env.NODE_PATH || '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 = true): LaunchRequestArguments { - return { - program: path.join(debugFilesPath, pythonFile), - cwd: debugFilesPath, - stopOnEntry, - showReturnValue, - logToFile: false, - 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<number>(resolve => 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 ddbaf9e3c800..000000000000 --- a/src/test/debugger/run.test.ts +++ /dev/null @@ -1,83 +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 { 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)); - debugClient = await createDebugAdapter(); - }); - 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 = true): 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 29a7d2d8067e..9ccb8958b660 100644 --- a/src/test/debugger/utils.ts +++ b/src/test/debugger/utils.ts @@ -3,86 +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 { DebuggerTypeName } from '../../client/debugger/constants'; -import { DEBUGGER_TIMEOUT } from './common/constants'; - -const testAdapterFilePath = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'debugAdapter', 'main.js'); -const debuggerType = DebuggerTypeName; - -/** - * Creates the debug adapter. - * @returns {DebugClient} - */ -export async function createDebugAdapter(): Promise<DebugClient> { - await new Promise(resolve => setTimeout(resolve, 1000)); - const debugClient = new DebugClient(process.env.NODE_PATH || 'node', testAdapterFilePath, debuggerType); - 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<ProcResult> { + const cachedResult = this.results[id]; + if (cachedResult !== undefined) { + return cachedResult; + } + const tracked = this.tracked[id]; + if (tracked === undefined) { + throw Error(`untracked debugger ${id}`); + } else { + while (tracked.exitCode === undefined) { + await sleep(10); // milliseconds + } + const result = { + exitCode: tracked.exitCode, + stdout: tracked.output.stdout, + }; + this.results[id] = result; + return result; + } + } - 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<vscode.DebugAdapterTracker> { + const id = session.configuration._test_session_id; + const tracked = this.tracked[id]; + if (tracked !== undefined) { + tracked.session = session; + return new DebugAdapterTracker(tracked); + } else if (id !== undefined) { + // This should not have happened, but we don't worry about + // it for now. + } + return undefined; } } -export function makeHttpRequest(uri: string): Promise<string> { - return new Promise<string>((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<ProcResult> { + if (this.proc) { + return this.proc.waitUntilDone(); + } else { + return debuggers.waitUntilDone(this.id); + } + } + + public addBreakpoint(filename: string, line: number, ch?: number): vscode.Breakpoint { + // The arguments are 1-indexed. + const loc = new vscode.Location( + vscode.Uri.file(filename), + // VS Code wants 0-indexed line and column numbers. + // We default to the beginning of the line. + new vscode.Position(line - 1, ch ? ch - 1 : 0), + ); + const bp = new vscode.SourceBreakpoint(loc); + vscode.debug.addBreakpoints([bp]); + return bp; + } + + public async waitForBreakpoint(bp: vscode.Breakpoint, opts: { clear: boolean } = { clear: true }) { + while (!this.stopped || !this.stopped.breakpoint) { + await sleep(10); // milliseconds + } + if (opts.clear) { + vscode.debug.removeBreakpoints([bp]); + await this.raw!.customRequest('continue', { threadId: this.stopped.threadId }); + this.stopped = undefined; + } + } + + public handleDAPMessage(_src: DAPSource, baseMsg: DebugProtocol.ProtocolMessage) { + if (baseMsg.type === 'event') { + const event = ((baseMsg as unknown) as DebugProtocol.Event).event; + if (event === 'stopped') { + const msg = (baseMsg as unknown) as DebugProtocol.StoppedEvent; + this.stopped = { + breakpoint: msg.body.reason === 'breakpoint', + threadId: (msg.body.threadId as unknown) as number, + }; + } else { + // For now there aren't any other events we care about. } - 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<DebuggerSession> { + const config = getConfig(configName); + let proc: Proc | undefined; + if (config.request === 'launch') { + config.program = file; + config.args = scriptArgs; + config.redirectOutput = false; + // XXX set the file in the current vscode editor? + } else if (config.request === 'attach') { + if (config.port) { + proc = 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<string>]> { - 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<IServiceContainer>; + let discoverAPI: typemoq.IMock<IDiscoveryAPI>; + let interpreterPathService: typemoq.IMock<IInterpreterPathService>; + let configService: typemoq.IMock<IConfigurationService>; + let extensions: typemoq.IMock<IExtensions>; + let workspaceService: typemoq.IMock<IWorkspaceService>; + let envVarsProvider: typemoq.IMock<IEnvironmentVariablesProvider>; + let onDidChangeRefreshState: EventEmitter<ProgressNotificationEvent>; + let onDidChangeEnvironments: EventEmitter<PythonEnvCollectionChangedEvent>; + let onDidChangeEnvironmentVariables: EventEmitter<Uri | undefined>; + + let environmentApi: PythonExtension['environments']; + + setup(() => { + serviceContainer = typemoq.Mock.ofType<IServiceContainer>(); + sinon.stub(workspaceApis, 'getWorkspaceFolders').returns([workspaceFolder]); + sinon.stub(workspaceApis, 'getWorkspaceFolder').callsFake((resource: Resource) => { + if (resource?.fsPath === workspaceFolder.uri.fsPath) { + return workspaceFolder; + } + return undefined; + }); + discoverAPI = typemoq.Mock.ofType<IDiscoveryAPI>(); + extensions = typemoq.Mock.ofType<IExtensions>(); + workspaceService = typemoq.Mock.ofType<IWorkspaceService>(); + envVarsProvider = typemoq.Mock.ofType<IEnvironmentVariablesProvider>(); + extensions + .setup((e) => e.determineExtensionFromCallStack()) + .returns(() => Promise.resolve({ extensionId: 'id', displayName: 'displayName', apiName: 'apiName' })); + interpreterPathService = typemoq.Mock.ofType<IInterpreterPathService>(); + configService = typemoq.Mock.ofType<IConfigurationService>(); + onDidChangeRefreshState = new EventEmitter(); + onDidChangeEnvironments = new EventEmitter(); + onDidChangeEnvironmentVariables = new EventEmitter(); + serviceContainer.setup((s) => s.get(IExtensions)).returns(() => extensions.object); + serviceContainer.setup((s) => s.get(IInterpreterPathService)).returns(() => interpreterPathService.object); + serviceContainer.setup((s) => s.get(IConfigurationService)).returns(() => configService.object); + serviceContainer.setup((s) => s.get(IWorkspaceService)).returns(() => workspaceService.object); + serviceContainer.setup((s) => s.get(IEnvironmentVariablesProvider)).returns(() => envVarsProvider.object); + envVarsProvider + .setup((e) => e.onDidEnvironmentVariablesChange) + .returns(() => onDidChangeEnvironmentVariables.event); + serviceContainer.setup((s) => s.get(IDisposableRegistry)).returns(() => []); + + discoverAPI.setup((d) => d.onProgress).returns(() => onDidChangeRefreshState.event); + discoverAPI.setup((d) => d.onChanged).returns(() => onDidChangeEnvironments.event); + discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const onDidChangePythonEnvironment = new EventEmitter<Uri>(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Provide an event to track when environment variables change', async () => { + const resource = workspaceFolder.uri; + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const events: EnvironmentVariablesChangeEvent[] = []; + environmentApi.onDidEnvironmentVariablesChange((e) => { + events.push(e); + }); + onDidChangeEnvironmentVariables.fire(resource); + await sleep(1); + assert.deepEqual(events, [{ env: envVars, resource: workspaceFolder }]); + }); + + test('getEnvironmentVariables: No resource', async () => { + const resource = undefined; + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const vars = environmentApi.getEnvironmentVariables(resource); + assert.deepEqual(vars, envVars); + }); + + test('getEnvironmentVariables: With Uri resource', async () => { + const resource = Uri.file('x'); + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const vars = environmentApi.getEnvironmentVariables(resource); + assert.deepEqual(vars, envVars); + }); + + test('getEnvironmentVariables: With WorkspaceFolder resource', async () => { + const resource = Uri.file('x'); + const folder = ({ uri: resource } as unknown) as WorkspaceFolder; + const envVars = { PATH: 'path' }; + envVarsProvider.setup((e) => e.getEnvironmentVariablesSync(resource)).returns(() => envVars); + const vars = environmentApi.getEnvironmentVariables(folder); + assert.deepEqual(vars, envVars); + }); + + test('Provide an event to track when active environment details change', async () => { + const events: ActiveEnvironmentPathChangeEvent[] = []; + environmentApi.onDidChangeActiveEnvironmentPath((e) => { + events.push(e); + }); + reportActiveInterpreterChanged({ path: 'path/to/environment', resource: undefined }); + await sleep(1); + assert.deepEqual(events, [ + { id: normCasePath('path/to/environment'), path: 'path/to/environment', resource: undefined }, + ]); + }); + + test('getActiveEnvironmentPath: No resource', () => { + const pythonPath = 'this/is/a/test/path'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = environmentApi.getActiveEnvironmentPath(); + assert.deepEqual(actual, { + id: normCasePath(pythonPath), + path: pythonPath, + }); + }); + + test('getActiveEnvironmentPath: default python', () => { + const pythonPath = 'python'; + configService + .setup((c) => c.getSettings(undefined)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = environmentApi.getActiveEnvironmentPath(); + assert.deepEqual(actual, { + id: 'DEFAULT_PYTHON', + path: pythonPath, + }); + }); + + test('getActiveEnvironmentPath: With resource', () => { + const pythonPath = 'this/is/a/test/path'; + const resource = Uri.file(__filename); + configService + .setup((c) => c.getSettings(resource)) + .returns(() => (({ pythonPath } as unknown) as IPythonSettings)); + const actual = environmentApi.getActiveEnvironmentPath(resource); + assert.deepEqual(actual, { + id: normCasePath(pythonPath), + path: pythonPath, + }); + }); + + test('resolveEnvironment: invalid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(undefined)); + + const actual = await environmentApi.resolveEnvironment(pythonPath); + expect(actual).to.be.equal(undefined); + }); + + test('resolveEnvironment: valid environment (when passed as string)', async () => { + const pythonPath = 'this/is/a/test/path'; + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + + const actual = await environmentApi.resolveEnvironment(pythonPath); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); + + test('resolveEnvironment: valid environment (when passed as environment)', async () => { + const pythonPath = 'this/is/a/test/path'; + const env = buildEnvInfo({ + executable: pythonPath, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }); + const partialEnv = buildEnvInfo({ + executable: pythonPath, + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }); + discoverAPI.setup((p) => p.resolveEnv(pythonPath)).returns(() => Promise.resolve(env)); + + const actual = await environmentApi.resolveEnvironment(convertCompleteEnvInfo(partialEnv)); + assert.deepEqual((actual as EnvironmentReference).internal, convertCompleteEnvInfo(env)); + }); + + test('environments: no pythons found', () => { + discoverAPI.setup((d) => d.getEnvs()).returns(() => []); + const actual = environmentApi.known; + expect(actual).to.be.deep.equal([]); + }); + + test('environments: python found', async () => { + const expectedEnvs = [ + { + id: normCasePath('this/is/a/test/python/path1'), + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + id: normCasePath('this/is/a/test/python/path2'), + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: -1, + micro: -1, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + const envs = [ + ...expectedEnvs, + { + id: normCasePath('this/is/a/test/python/path3'), + executable: { + filename: 'this/is/a/test/python/path3', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: -1, + micro: -1, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + searchLocation: Uri.file('path/outside/workspace'), + }, + ]; + discoverAPI.setup((d) => d.getEnvs()).returns(() => envs); + const onDidChangePythonEnvironment = new EventEmitter<Uri>(); + const jupyterApi: JupyterPythonEnvironmentApi = { + onDidChangePythonEnvironment: onDidChangePythonEnvironment.event, + getPythonEnvironment: (_uri: Uri) => undefined, + }; + environmentApi = buildEnvironmentApi(discoverAPI.object, serviceContainer.object, jupyterApi); + const actual = environmentApi.known; + const actualEnvs = actual?.map((a) => (a as EnvironmentReference).internal); + assert.deepEqual( + actualEnvs?.sort((a, b) => a.id.localeCompare(b.id)), + expectedEnvs.map((e) => convertEnvInfo(e)).sort((a, b) => a.id.localeCompare(b.id)), + ); + }); + + test('Provide an event to track when list of environments change', async () => { + let events: EnvironmentsChangeEvent[] = []; + let eventValues: EnvironmentsChangeEvent[] = []; + let expectedEvents: EnvironmentsChangeEvent[] = []; + environmentApi.onDidChangeEnvironments((e) => { + events.push(e); + }); + const envs = [ + buildEnvInfo({ + executable: 'pythonPath', + kind: PythonEnvKind.System, + sysPrefix: 'prefix/path', + searchLocation: Uri.file(workspacePath), + }), + { + id: normCasePath('this/is/a/test/python/path1'), + executable: { + filename: 'this/is/a/test/python/path1', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 9, + micro: 0, + }, + kind: PythonEnvKind.System, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + { + id: normCasePath('this/is/a/test/python/path2'), + executable: { + filename: 'this/is/a/test/python/path2', + ctime: 1, + mtime: 2, + sysPrefix: 'prefix/path', + }, + version: { + major: 3, + minor: 10, + micro: 0, + }, + kind: PythonEnvKind.Venv, + arch: Architecture.x64, + name: '', + location: '', + source: [PythonEnvSource.PathEnvVar], + distro: { + org: '', + }, + }, + ]; + + // Now fire and verify events. Note the event value holds the reference to an environment, so may itself + // change when the environment is altered. So it's important to verify them as soon as they're received. + + // Add events + onDidChangeEnvironments.fire({ old: undefined, new: envs[0] }); + expectedEvents.push({ env: convertEnvInfo(envs[0]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[1] }); + expectedEvents.push({ env: convertEnvInfo(envs[1]), type: 'add' }); + onDidChangeEnvironments.fire({ old: undefined, new: envs[2] }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'add' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Update events + events = []; + expectedEvents = []; + const updatedEnv0 = cloneDeep(envs[0]); + updatedEnv0.arch = Architecture.x86; + onDidChangeEnvironments.fire({ old: envs[0], new: updatedEnv0 }); + expectedEvents.push({ env: convertEnvInfo(updatedEnv0), type: 'update' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + // Remove events + events = []; + expectedEvents = []; + onDidChangeEnvironments.fire({ old: envs[2], new: undefined }); + expectedEvents.push({ env: convertEnvInfo(envs[2]), type: 'remove' }); + eventValues = events.map((e) => ({ env: (e.env as EnvironmentReference).internal, type: e.type })); + assert.deepEqual(eventValues, expectedEvents); + + const expectedEnvs = [convertEnvInfo(updatedEnv0), convertEnvInfo(envs[1])].sort(); + const knownEnvs = environmentApi.known.map((e) => (e as EnvironmentReference).internal).sort(); + + assert.deepEqual(expectedEnvs, knownEnvs); + }); + + test('updateActiveEnvironmentPath: no resource', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.updateActiveEnvironmentPath('this/is/a/test/python/path'); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentPath: passed as Environment', async () => { + interpreterPathService + .setup((i) => i.update(undefined, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.updateActiveEnvironmentPath({ + id: normCasePath('this/is/a/test/python/path'), + path: 'this/is/a/test/python/path', + }); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentPath: with uri', async () => { + const uri = Uri.parse('a'); + interpreterPathService + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.updateActiveEnvironmentPath('this/is/a/test/python/path', uri); + + interpreterPathService.verifyAll(); + }); + + test('updateActiveEnvironmentPath: with workspace folder', async () => { + const uri = Uri.parse('a'); + interpreterPathService + .setup((i) => i.update(uri, ConfigurationTarget.WorkspaceFolder, 'this/is/a/test/python/path')) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + const workspace: WorkspaceFolder = { + uri, + name: '', + index: 0, + }; + + await environmentApi.updateActiveEnvironmentPath('this/is/a/test/python/path', workspace); + + interpreterPathService.verifyAll(); + }); + + test('refreshInterpreters: default', async () => { + discoverAPI + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: true }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.refreshEnvironments(); + + discoverAPI.verifyAll(); + }); + + test('refreshInterpreters: when forcing a refresh', async () => { + discoverAPI + .setup((d) => d.triggerRefresh(undefined, typemoq.It.isValue({ ifNotTriggerredAlready: false }))) + .returns(() => Promise.resolve()) + .verifiable(typemoq.Times.once()); + + await environmentApi.refreshEnvironments({ forceRefresh: true }); + + discoverAPI.verifyAll(); + }); +}); diff --git a/src/test/extension-version.functional.test.ts b/src/test/extension-version.functional.test.ts 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 553be3f7121e..000000000000 --- a/src/test/extension.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:no-any - -import { expect } from 'chai'; -import { buildApi } from '../client/api'; -import { EXTENSION_ROOT_DIR } from '../client/common/constants'; - -const expectedPath = `${EXTENSION_ROOT_DIR.fileToCommandArgument()}/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<void> { + return new vscode.EventEmitter<void>().event; + } + + public autoSelectInterpreter(_resource: Resource): Promise<void> { + return Promise.resolve(); + } + + public getAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + return undefined; + } + + public async setWorkspaceInterpreter( + _resource: Uri, + _interpreter: PythonEnvironment | undefined, + ): Promise<void> { + return undefined; + } + } + const pythonSettings = require('../client/common/configSettings') as typeof import('../client/common/configSettings'); + const workspaceService = new WorkspaceService(); + const workspaceMemento = new MockMemento(); + const globalMemento = new MockMemento(); + const persistentStateFactory = new PersistentStateFactory(globalMemento, workspaceMemento); + const extensions = new MockExtensions(); + return pythonSettings.PythonSettings.getInstance( + resource, + new AutoSelectionService(), + workspaceService, + new InterpreterPathService(persistentStateFactory, workspaceService, [], { + remoteName: undefined, + } as IApplicationEnvironment), + undefined, + extensions, + ); +} diff --git a/src/test/fakeVSCFileSystemAPI.ts b/src/test/fakeVSCFileSystemAPI.ts new file mode 100644 index 000000000000..1811f51dcd04 --- /dev/null +++ b/src/test/fakeVSCFileSystemAPI.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { FileStat, FileType, Uri } from 'vscode'; +import * as fsextra from '../client/common/platform/fs-paths'; +import { convertStat } from '../client/common/platform/fileSystem'; +import { createDeferred } from '../client/common/utils/async'; + +/* eslint-disable class-methods-use-this */ + +// This is necessary for unit tests and functional tests, since they +// do not run under VS Code so they do not have access to the actual +// "vscode" namespace. +export class FakeVSCodeFileSystemAPI { + public async readFile(uri: Uri): Promise<Uint8Array> { + return fsextra.readFile(uri.fsPath); + } + + public async writeFile(uri: Uri, content: Uint8Array): Promise<void> { + return fsextra.writeFile(uri.fsPath, Buffer.from(content)); + } + + public async delete(uri: Uri): Promise<void> { + return ( + fsextra + // Make sure the file exists before deleting. + .stat(uri.fsPath) + .then(() => fsextra.remove(uri.fsPath)) + ); + } + + public async stat(uri: Uri): Promise<FileStat> { + const filename = uri.fsPath; + + let filetype = FileType.Unknown; + let stat = await fsextra.lstat(filename); + if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = await fsextra.stat(filename); + } + if (stat.isFile()) { + filetype |= FileType.File; + } else if (stat.isDirectory()) { + filetype |= FileType.Directory; + } + return convertStat(stat, filetype); + } + + public async readDirectory(uri: Uri): Promise<[string, FileType][]> { + const names: string[] = await fsextra.readdir(uri.fsPath); + const promises = names.map((name) => { + const filename = path.join(uri.fsPath, name); + return ( + fsextra + // Get the lstat info and deal with symlinks if necessary. + .lstat(filename) + .then(async (stat) => { + let filetype = FileType.Unknown; + if (stat.isFile()) { + filetype = FileType.File; + } else if (stat.isDirectory()) { + filetype = FileType.Directory; + } else if (stat.isSymbolicLink()) { + filetype = FileType.SymbolicLink; + stat = await fsextra.stat(filename); + if (stat.isFile()) { + filetype |= FileType.File; + } else if (stat.isDirectory()) { + filetype |= FileType.Directory; + } + } + return [name, filetype] as [string, FileType]; + }) + .catch(() => [name, FileType.Unknown] as [string, FileType]) + ); + }); + return Promise.all(promises); + } + + public async createDirectory(uri: Uri): Promise<void> { + return fsextra.mkdirp(uri.fsPath); + } + + public async copy(src: Uri, dest: Uri): Promise<void> { + const deferred = createDeferred<void>(); + const rs = fsextra + // Set an error handler on the stream. + .createReadStream(src.fsPath) + .on('error', (err) => { + deferred.reject(err); + }); + const ws = fsextra + .createWriteStream(dest.fsPath) + // Set an error & close handler on the stream. + .on('error', (err) => { + deferred.reject(err); + }) + .on('close', () => { + deferred.resolve(); + }); + rs.pipe(ws); + return deferred.promise; + } + + public async rename(src: Uri, dest: Uri): Promise<void> { + return fsextra.rename(src.fsPath, dest.fsPath); + } +} diff --git a/src/test/fixtures.ts b/src/test/fixtures.ts 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<void>); + +export class CleanupFixture { + private cleanups: CleanupFunc[]; + constructor() { + this.cleanups = []; + } + + public addCleanup(cleanup: CleanupFunc) { + this.cleanups.push(cleanup); + } + public addFSCleanup(filename: string) { + this.addCleanup(async () => { + try { + await fs.unlink(filename); + } catch { + // The file is already gone. + } + }); + } + + public async cleanUp() { + const cleanups = this.cleanups; + this.cleanups = []; + + return Promise.all( + cleanups.map(async (cleanup, i) => { + try { + const res = cleanup(); + if (res) { + await res; + } + } catch (err) { + 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<TextDocument>(); - const pos = TypeMoq.Mock.ofType<Position>(); - const opt = TypeMoq.Mock.ofType<FormattingOptions>(); - const token = TypeMoq.Mock.ofType<CancellationToken>(); - const edits = TypeMoq.Mock.ofType<ProviderResult<TextEdit[]>>(); - - test('No providers', async () => { - const dispatcher = new OnTypeFormattingDispatcher({}); - - const triggers = dispatcher.getTriggerCharacters(); - assert.equal(triggers, undefined, 'Trigger was not undefined'); - - const result = await dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, '\n', opt.object, token.object); - assert.deepStrictEqual(result, [], 'Did not return an empty list of edits'); - }); - - test('Single provider', () => { - const provider = setupProvider(doc.object, pos.object, ':', opt.object, token.object, edits.object); - - const dispatcher = new OnTypeFormattingDispatcher({ - ':': provider.object - }); - - const triggers = dispatcher.getTriggerCharacters(); - assert.deepStrictEqual(triggers, { first: ':', more: [] }, 'Did not return correct triggers'); - - const result = dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, ':', opt.object, token.object); - assert.equal(result, edits.object, 'Did not return correct edits'); - - provider.verifyAll(); - }); - - test('Two providers', () => { - const colonProvider = setupProvider(doc.object, pos.object, ':', opt.object, token.object, edits.object); - - const doc2 = TypeMoq.Mock.ofType<TextDocument>(); - const pos2 = TypeMoq.Mock.ofType<Position>(); - const opt2 = TypeMoq.Mock.ofType<FormattingOptions>(); - const token2 = TypeMoq.Mock.ofType<CancellationToken>(); - const edits2 = TypeMoq.Mock.ofType<ProviderResult<TextEdit[]>>(); - - const newlineProvider = setupProvider(doc2.object, pos2.object, '\n', opt2.object, token2.object, edits2.object); - - const dispatcher = new OnTypeFormattingDispatcher({ - ':': colonProvider.object, - '\n': newlineProvider.object - }); - - const triggers = dispatcher.getTriggerCharacters(); - assert.deepStrictEqual(triggers, { first: '\n', more: [':'] }, 'Did not return correct triggers'); - - const result = dispatcher.provideOnTypeFormattingEdits(doc.object, pos.object, ':', opt.object, token.object); - assert.equal(result, edits.object, 'Did not return correct editsfor colon provider'); - - const result2 = dispatcher.provideOnTypeFormattingEdits(doc2.object, pos2.object, '\n', opt2.object, token2.object); - assert.equal(result2, edits2.object, 'Did not return correct edits for newline provider'); - - colonProvider.verifyAll(); - newlineProvider.verifyAll(); - }); - - function setupProvider(document: TextDocument, position: Position, ch: string, options: FormattingOptions, cancellationToken: CancellationToken, - result: ProviderResult<TextEdit[]>): TypeMoq.IMock<OnTypeFormattingEditProvider> { - const provider = TypeMoq.Mock.ofType<OnTypeFormattingEditProvider>(); - provider.setup(p => p.provideOnTypeFormattingEdits(document, position, ch, options, cancellationToken)) - .returns(() => result) - .verifiable(TypeMoq.Times.once()); - return provider; - } -}); diff --git a/src/test/format/extension.format.test.ts b/src/test/format/extension.format.test.ts deleted file mode 100644 index b8da5f609703..000000000000 --- a/src/test/format/extension.format.test.ts +++ /dev/null @@ -1,194 +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 { ICondaService } from '../../client/interpreter/contracts'; -import { CondaService } from '../../client/interpreter/locators/services/condaService'; -import { isPythonVersionInProcess } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { MockProcessService } from '../mocks/proc'; -import { compareFiles } from '../textUtils'; -import { UnitTestIocContainer } from '../testing/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>(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<boolean> { - const processService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory) - .create(Uri.file(workspaceRootPath)); - return !(await isPythonVersionInProcess(processService, '2', '3.0', '3.1', '3.2', '3.3', '3.4', '3.5')); - } - - setup(async () => { - await initializeTest(); - initializeDI(); - }); - suiteTeardown(async () => { - [autoPep8FileToFormat, blackFileToFormat, 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(); - ioc.serviceManager.addSingleton<ICondaService>(ICondaService, CondaService); - } - - async function injectFormatOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--diff') >= 0) { - callback({ - out: fs.readFileSync(path.join(formatFilesPath, outputFileName), 'utf8'), - source: 'stdout' - }); - } - }); - } - - async function testFormatting(formatter: AutoPep8Formatter | BlackFormatter | YapfFormatter, formattedContents: string, fileToFormat: string, outputFileName: string) { - const textDocument = await workspace.openTextDocument(fileToFormat); - const textEditor = await window.showTextDocument(textDocument); - const options = { insertSpaces: textEditor.options.insertSpaces! as boolean, tabSize: textEditor.options.tabSize! as number }; - - await injectFormatOutput(outputFileName); - - const edits = await formatter.formatDocument(textDocument, options, new CancellationTokenSource().token); - await textEditor.edit(editBuilder => { - edits.forEach(edit => editBuilder.replace(edit.range, edit.newText)); - }); - compareFiles(formattedContents, textEditor.document.getText()); - } - - test('AutoPep8', async () => { - 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.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<TextDocument>(); - document.setup(x => x.lineAt(TypeMoq.It.isAnyNumber())).returns(n => { - const line = TypeMoq.Mock.ofType<TextLine>(); - line.setup(x => x.text).returns(() => lines[n]); - line.setup(x => x.range).returns(() => new Range(new Position(n, 0), new Position(n, lines[n].length))); - return line.object; - }); - document.setup(x => x.getText(TypeMoq.It.isAny())).returns(o => { - const r = o as Range; - const bits: string[] = []; - - if (r.start.line === r.end.line) { - return lines[r.start.line].substring(r.start.character, r.end.character); - } - - bits.push(lines[r.start.line].substr(r.start.character)); - for (let i = r.start.line + 1; i < r.end.line; i += 1) { - bits.push(lines[i]); - } - bits.push(lines[r.end.line].substring(0, r.end.character)); - return bits.join('\n'); - }); - document.setup(x => x.offsetAt(TypeMoq.It.isAny())).returns(o => { - const p = o as Position; - let offset = 0; - for (let i = 0; i < p.line; i += 1) { - offset += lines[i].length + 1; // Accounting for the line break - } - return offset + p.character; - }); - - return formatter.formatLine(document.object, lineNumber); - } - - function formatLine(text: string): string { - const line = TypeMoq.Mock.ofType<TextLine>(); - line.setup(x => x.text).returns(() => text); - - const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(x => x.lineAt(TypeMoq.It.isAnyNumber())).returns(() => line.object); - - return formatter.formatLine(document.object, 0); - } -}); diff --git a/src/test/format/extension.onEnterFormat.test.ts b/src/test/format/extension.onEnterFormat.test.ts deleted file mode 100644 index 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 211cc041ce93..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<void> { - let textDocument: vscode.TextDocument; - return vscode.workspace.openTextDocument(fileToFormat).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(_editor => { - return provider.provideOnTypeFormattingEdits( - textDocument, position, ':', formatOptions, new vscode.CancellationTokenSource().token); - }).then(edits => { - assert.equal(edits.length, expectedEdits.length, 'Number of edits not the same'); - edits.forEach((edit, index) => { - const expectedEdit = expectedEdits[index]; - assert.equal(edit.newText, expectedEdit.newText, `newText for edit is not the same for index = ${index}`); - const providedRange = `${edit.range.start.line},${edit.range.start.character},${edit.range.end.line},${edit.range.end.character}`; - const expectedRange = `${expectedEdit.range.start.line},${expectedEdit.range.start.character},${expectedEdit.range.end.line},${expectedEdit.range.end.character}`; - assert.ok(edit.range.isEqual(expectedEdit.range), `range for edit is not the same for index = ${index}, provided ${providedRange}, expected ${expectedRange}`); - }); - }, reason => { - assert.fail(reason, undefined, 'Type Formatting failed', ''); - }); -} - -suite('Else block with if in first line of file', () => { - suiteSetup(async () => { - await initialize(); - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['elseBlocksFirstLine2.py', 'elseBlocksFirstLine4.py', 'elseBlocksFirstLineTab.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - interface ITestCase { - title: string; - line: number; - column: number; - expectedEdits: vscode.TextEdit[]; - formatOptions: vscode.FormattingOptions; - filePath: string; - } - const testCases: ITestCase[] = [ - { - title: 'else block with 2 spaces', - line: 3, column: 7, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(3, 0, 3, 2)) - ], - formatOptions: { insertSpaces: true, tabSize: 2 }, - filePath: elseBlockFirstLine2OutFilePath - }, - { - title: 'else block with 4 spaces', - line: 3, column: 9, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(3, 0, 3, 4)) - ], - formatOptions: { insertSpaces: true, tabSize: 4 }, - filePath: elseBlockFirstLine4OutFilePath - }, - { - title: 'else block with Tab', - line: 3, column: 6, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(3, 0, 3, 1)), - vscode.TextEdit.insert(new vscode.Position(3, 0), '') - ], - formatOptions: { insertSpaces: false, tabSize: 4 }, - filePath: elseBlockFirstLineTabOutFilePath - } - ]; - - testCases.forEach((testCase, index) => { - test(`${index + 1}. ${testCase.title}`, done => { - const pos = new vscode.Position(testCase.line, testCase.column); - testFormatting(testCase.filePath, pos, testCase.expectedEdits, testCase.formatOptions).then(done, done); - }); - }); -}); - -suite('Try blocks with indentation of 2 spaces', () => { - suiteSetup(async () => { - await initialize(); - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['tryBlocks2.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - interface ITestCase { - title: string; - line: number; - column: number; - expectedEdits: vscode.TextEdit[]; - } - const testCases: ITestCase[] = [ - { - title: 'except off by tab', - line: 6, column: 22, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(6, 0, 6, 2)) - ] - }, - { - title: 'except off by one should not be formatted', - line: 15, column: 21, - expectedEdits: [] - }, - { - title: 'except off by tab inside a for loop', - line: 35, column: 13, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(35, 0, 35, 2)) - ] - }, - { - title: 'except off by one inside a for loop should not be formatted', - line: 47, column: 12, - expectedEdits: [ - ] - }, - { - title: 'except IOError: off by tab inside a for loop', - line: 54, column: 19, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(54, 0, 54, 2)) - ] - }, - { - title: 'else: off by tab inside a for loop', - line: 76, column: 9, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(76, 0, 76, 2)) - ] - }, - { - title: 'except ValueError:: off by tab inside a function', - line: 143, column: 22, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(143, 0, 143, 2)) - ] - }, - { - title: 'except ValueError as err: off by one inside a function should not be formatted', - line: 157, column: 25, - expectedEdits: [ - ] - }, - { - title: 'else: off by tab inside function', - line: 172, column: 11, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(172, 0, 172, 2)) - ] - }, - { - title: 'finally: off by tab inside function', - line: 195, column: 12, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(195, 0, 195, 2)) - ] - } - ]; - - const formatOptions: vscode.FormattingOptions = { - insertSpaces: true, tabSize: 2 - }; - - testCases.forEach((testCase, index) => { - test(`${index + 1}. ${testCase.title}`, done => { - const pos = new vscode.Position(testCase.line, testCase.column); - testFormatting(tryBlock2OutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); - }); - }); -}); - -suite('Try blocks with indentation of 4 spaces', () => { - suiteSetup(async () => { - await initialize(); - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['tryBlocks4.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - interface ITestCase { - title: string; - line: number; - column: number; - expectedEdits: vscode.TextEdit[]; - } - const testCases: ITestCase[] = [ - { - title: 'except off by tab', - line: 6, column: 22, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(6, 0, 6, 4)) - ] - }, - { - title: 'except off by one should not be formatted', - line: 15, column: 21, - expectedEdits: [] - }, - { - title: 'except off by tab inside a for loop', - line: 35, column: 13, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(35, 0, 35, 4)) - ] - }, - { - title: 'except off by one inside a for loop should not be formatted', - line: 47, column: 12, - expectedEdits: [ - ] - }, - { - title: 'except IOError: off by tab inside a for loop', - line: 54, column: 19, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(54, 0, 54, 4)) - ] - }, - { - title: 'else: off by tab inside a for loop', - line: 76, column: 9, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(76, 0, 76, 4)) - ] - }, - { - title: 'except ValueError:: off by tab inside a function', - line: 143, column: 22, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(143, 0, 143, 4)) - ] - }, - { - title: 'except ValueError as err: off by one inside a function should not be formatted', - line: 157, column: 25, - expectedEdits: [ - ] - }, - { - title: 'else: off by tab inside function', - line: 172, column: 11, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(172, 0, 172, 4)) - ] - }, - { - title: 'finally: off by tab inside function', - line: 195, column: 12, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(195, 0, 195, 4)) - ] - } - ]; - - const formatOptions: vscode.FormattingOptions = { - insertSpaces: true, tabSize: 4 - }; - - testCases.forEach((testCase, index) => { - test(`${index + 1}. ${testCase.title}`, done => { - const pos = new vscode.Position(testCase.line, testCase.column); - testFormatting(tryBlock4OutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); - }); - }); -}); - -suite('Try blocks with indentation of Tab', () => { - suiteSetup(async () => { - await initialize(); - fs.ensureDirSync(path.dirname(outPythoFilesPath)); - - ['tryBlocksTab.py'].forEach(file => { - const targetFile = path.join(outPythoFilesPath, file); - if (fs.existsSync(targetFile)) { fs.unlinkSync(targetFile); } - fs.copySync(path.join(srcPythoFilesPath, file), targetFile); - }); - }); - suiteTeardown(closeActiveWindows); - teardown(closeActiveWindows); - - interface ITestCase { - title: string; - line: number; - column: number; - expectedEdits: vscode.TextEdit[]; - } - const TAB = ' '; - const testCases: ITestCase[] = [ - { - title: 'except off by tab', - line: 6, column: 22, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(6, 0, 6, 2)), - vscode.TextEdit.insert(new vscode.Position(6, 0), TAB) - ] - }, - { - title: 'except off by tab inside a for loop', - line: 35, column: 13, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(35, 0, 35, 2)), - vscode.TextEdit.insert(new vscode.Position(35, 0), TAB) - ] - }, - { - title: 'except IOError: off by tab inside a for loop', - line: 54, column: 19, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(54, 0, 54, 2)), - vscode.TextEdit.insert(new vscode.Position(54, 0), TAB) - ] - }, - { - title: 'else: off by tab inside a for loop', - line: 76, column: 9, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(76, 0, 76, 2)), - vscode.TextEdit.insert(new vscode.Position(76, 0), TAB) - ] - }, - { - title: 'except ValueError:: off by tab inside a function', - line: 143, column: 22, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(143, 0, 143, 2)), - vscode.TextEdit.insert(new vscode.Position(143, 0), TAB) - ] - }, - { - title: 'else: off by tab inside function', - line: 172, column: 11, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(172, 0, 172, 3)), - vscode.TextEdit.insert(new vscode.Position(172, 0), TAB + TAB) - ] - }, - { - title: 'finally: off by tab inside function', - line: 195, column: 12, - expectedEdits: [ - vscode.TextEdit.delete(new vscode.Range(195, 0, 195, 2)), - vscode.TextEdit.insert(new vscode.Position(195, 0), TAB) - ] - } - ]; - - const formatOptions: vscode.FormattingOptions = { - insertSpaces: false, tabSize: 4 - }; - - testCases.forEach((testCase, index) => { - test(`${index + 1}. ${testCase.title}`, done => { - const pos = new vscode.Position(testCase.line, testCase.column); - testFormatting(tryBlockTabOutFilePath, pos, testCase.expectedEdits, formatOptions).then(done, done); - }); - }); -}); - -// 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 8b8bb027c258..000000000000 --- a/src/test/format/extension.sort.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as fs from 'fs'; -import { EOL } from 'os'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; -import { Commands } from '../../client/common/constants'; -import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { CondaService } from '../../client/interpreter/locators/services/condaService'; -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 '../testing/serviceRegistry'; - -const sortingPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'sorting'); -const fileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'before.py'); -const originalFileToFormatWithoutConfig = path.join(sortingPath, 'noconfig', 'original.py'); -const fileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'before.py'); -const originalFileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'original.py'); -const fileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'before.1.py'); -const originalFileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'original.1.py'); - -// tslint:disable-next-line:max-func-body-length -suite('Sorting', () => { - let ioc: UnitTestIocContainer; - let sorter: ISortImportsEditingProvider; - const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - suiteSetup(initialize); - suiteTeardown(async () => { - fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); - fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); - fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); - await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); - await closeActiveWindows(); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - fs.writeFileSync(fileToFormatWithConfig, fs.readFileSync(originalFileToFormatWithConfig)); - fs.writeFileSync(fileToFormatWithoutConfig, fs.readFileSync(originalFileToFormatWithoutConfig)); - fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); - await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); - await closeActiveWindows(); - sorter = new SortImportsEditingProvider(ioc.serviceContainer); - }); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - test('Without Config', async () => { - const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); - await window.showTextDocument(textDocument); - const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; - expect(edit.entries()).to.be.lengthOf(1); - const edits = edit.entries()[0][1]; - assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new Range(2, 0, 2, 0))).length, 1, 'EOL not found'); - assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(3, 0, 4, 0))).length, 1, '"" not found'); - assert.equal(edits.filter(value => value.newText === `from rope.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && value.range.isEqual(new Range(6, 0, 6, 0))).length, 1, 'Text not found'); - assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(13, 0, 18, 0))).length, 1, '"" not found'); - }); - - test('Without Config (via Command)', async () => { - const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); - const originalContent = textDocument.getText(); - await window.showTextDocument(textDocument); - await commands.executeCommand(Commands.Sort_Imports); - assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); - }); - - test('With Config', async () => { - const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); - await window.showTextDocument(textDocument); - const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; - expect(edit.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 9e442d7b8db2..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 '../testing/serviceRegistry'; - -// tslint:disable-next-line:max-func-body-length -suite('Formatting - Helper', () => { - let ioc: UnitTestIocContainer; - let formatHelper: FormatterHelper; - - suiteSetup(initialize); - setup(() => { - ioc = new UnitTestIocContainer(); - - const config = TypeMoq.Mock.ofType<IConfigurationService>(); - config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => getExtensionSettings(undefined)); - - ioc.serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object); - formatHelper = new FormatterHelper(ioc.serviceManager); - }); - - test('Ensure product is set in Execution Info', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { - const info = formatHelper.getExecutionInfo(formatter, []); - assert.equal(info.product, formatter, `Incorrect products for ${formatHelper.translateToId(formatter)}`); - }); - }); - - test('Ensure executable is set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - - [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { - const info = formatHelper.getExecutionInfo(formatter, []); - const names = formatHelper.getSettingsPropertyNames(formatter); - const execPath = settings.formatting[names.pathName] as string; - - assert.equal(info.execPath, execPath, `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}`); - }); - }); - - test('Ensure arguments are set in Execution Info', async () => { - const settings = getExtensionSettings(undefined); - const customArgs = ['1', '2', '3']; - - [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { - const names = formatHelper.getSettingsPropertyNames(formatter); - const args: string[] = Array.isArray(settings.formatting[names.argsName]) ? settings.formatting[names.argsName] as string[] : []; - const expectedArgs = args.concat(customArgs).join(','); - - assert.equal(expectedArgs.endsWith(customArgs.join(',')), true, `Incorrect custom arguments for product ${formatHelper.translateToId(formatter)}`); - - }); - }); - - test('Ensure correct setting names are returned', async () => { - [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { - const translatedId = formatHelper.translateToId(formatter)!; - const settings = { - argsName: `${translatedId}Args` as keyof IFormattingSettings, - pathName: `${translatedId}Path` as keyof IFormattingSettings - }; - - assert.deepEqual(formatHelper.getSettingsPropertyNames(formatter), settings, `Incorrect settings for product ${formatHelper.translateToId(formatter)}`); - }); - }); - - test('Ensure translation of ids works', async () => { - const formatterMapping = new Map<Product, FormatterId>(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - - [Product.autopep8, Product.black, Product.yapf].forEach(formatter => { - const translatedId = formatHelper.translateToId(formatter); - assert.equal(translatedId, formatterMapping.get(formatter)!, `Incorrect translation for product ${formatHelper.translateToId(formatter)}`); - }); - }); - - EnumEx.getValues<Product>(Product).forEach(product => { - const formatterMapping = new Map<Product, FormatterId>(); - formatterMapping.set(Product.autopep8, 'autopep8'); - formatterMapping.set(Product.black, 'black'); - formatterMapping.set(Product.yapf, 'yapf'); - if (formatterMapping.has(product)) { - return; - } - - test(`Ensure translation of ids throws exceptions for unknown formatters (${product})`, async () => { - assert.throws(() => formatHelper.translateToId(product)); - }); - }); -}); diff --git a/src/test/format/formatter.unit.test.ts b/src/test/format/formatter.unit.test.ts deleted file mode 100644 index d121a9dc07ea..000000000000 --- a/src/test/format/formatter.unit.test.ts +++ /dev/null @@ -1,155 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { anything, capture, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, FormattingOptions, TextDocument, Uri } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { PythonSettings } from '../../client/common/configSettings'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { STANDARD_OUTPUT_CHANNEL } from '../../client/common/constants'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { IPythonToolExecutionService } from '../../client/common/process/types'; -import { ExecutionInfo, IConfigurationService, IDisposableRegistry, IFormattingSettings, IOutputChannel, IPythonSettings } from '../../client/common/types'; -import { AutoPep8Formatter } from '../../client/formatters/autoPep8Formatter'; -import { BaseFormatter } from '../../client/formatters/baseFormatter'; -import { BlackFormatter } from '../../client/formatters/blackFormatter'; -import { FormatterHelper } from '../../client/formatters/helper'; -import { IFormatterHelper } from '../../client/formatters/types'; -import { YapfFormatter } from '../../client/formatters/yapfFormatter'; -import { ServiceContainer } from '../../client/ioc/container'; -import { IServiceContainer } from '../../client/ioc/types'; -import { noop } from '../core'; -import { MockOutputChannel } from '../mockClasses'; - -// tslint:disable-next-line: max-func-body-length -suite('Formatting - Test Arguments', () => { - let container: IServiceContainer; - let outputChannel: IOutputChannel; - let workspace: IWorkspaceService; - let settings: IPythonSettings; - const workspaceUri = Uri.file(__dirname); - let document: typemoq.IMock<TextDocument>; - const docUri = Uri.file(__filename); - let pythonToolExecutionService: IPythonToolExecutionService; - const options: FormattingOptions = { insertSpaces: false, tabSize: 1 }; - const formattingSettingsWithPath: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: path.join('a', 'exe'), - blackArgs: ['1', '2'], - blackPath: path.join('a', 'exe'), - provider: '', - yapfArgs: ['1', '2'], - yapfPath: path.join('a', 'exe') - }; - - const formattingSettingsWithModuleName: IFormattingSettings = { - autopep8Args: ['1', '2'], - autopep8Path: 'module_name', - blackArgs: ['1', '2'], - blackPath: 'module_name', - provider: '', - yapfArgs: ['1', '2'], - yapfPath: 'module_name', - }; - - setup(() => { - container = mock(ServiceContainer); - outputChannel = mock(MockOutputChannel); - workspace = mock(WorkspaceService); - settings = mock(PythonSettings); - document = typemoq.Mock.ofType<TextDocument>(); - document.setup(doc => doc.getText(typemoq.It.isAny())).returns(() => ''); - document.setup(doc => doc.isDirty).returns(() => false); - document.setup(doc => doc.fileName).returns(() => docUri.fsPath); - document.setup(doc => doc.uri).returns(() => docUri); - pythonToolExecutionService = mock(PythonToolExecutionService); - - const configService = mock(ConfigurationService); - const formatterHelper = new FormatterHelper(instance(container)); - - const appShell = mock(ApplicationShell); - when(appShell.setStatusBarMessage(anything(), anything())).thenReturn({ dispose: noop }); - - when(configService.getSettings(anything())).thenReturn(instance(settings)); - when(workspace.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceUri }); - when(container.get<IOutputChannel>(IOutputChannel, STANDARD_OUTPUT_CHANNEL)).thenReturn(instance(outputChannel)); - when(container.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(appShell)); - when(container.get<IFormatterHelper>(IFormatterHelper)).thenReturn(formatterHelper); - when(container.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspace)); - when(container.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); - when(container.get<IPythonToolExecutionService>(IPythonToolExecutionService)).thenReturn(instance(pythonToolExecutionService)); - when(container.get<IDisposableRegistry>(IDisposableRegistry)).thenReturn([]); - }); - - async function setupFormatter(formatter: BaseFormatter, formattingSettings: IFormattingSettings): Promise<ExecutionInfo> { - const token = new CancellationTokenSource().token; - when(settings.formatting).thenReturn(formattingSettings); - when(pythonToolExecutionService.exec(anything(), anything(), anything())).thenResolve({ stdout: '' }); - - await formatter.formatDocument(document.object, options, token); - - const args = capture(pythonToolExecutionService.exec).first(); - return args[0]; - } - test('Ensure blackPath and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.equal(execInfo.execPath, formattingSettingsWithPath.blackPath); - assert.equal(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath])); - }); - test('Ensure black modulename and args used to launch the formatter', async () => { - const formatter = new BlackFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.equal(execInfo.execPath, formattingSettingsWithModuleName.blackPath); - assert.equal(execInfo.moduleName, formattingSettingsWithModuleName.blackPath); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.blackArgs.concat(['--diff', '--quiet', docUri.fsPath])); - }); - test('Ensure autopep8path and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.equal(execInfo.execPath, formattingSettingsWithPath.autopep8Path); - assert.equal(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure autpep8 modulename and args used to launch the formatter', async () => { - const formatter = new AutoPep8Formatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.equal(execInfo.execPath, formattingSettingsWithModuleName.autopep8Path); - assert.equal(execInfo.moduleName, formattingSettingsWithModuleName.autopep8Path); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.autopep8Args.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapfpath and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithPath); - - assert.equal(execInfo.execPath, formattingSettingsWithPath.yapfPath); - assert.equal(execInfo.moduleName, undefined); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); - test('Ensure yapf modulename and args used to launch the formatter', async () => { - const formatter = new YapfFormatter(instance(container)); - - const execInfo = await setupFormatter(formatter, formattingSettingsWithModuleName); - - assert.equal(execInfo.execPath, formattingSettingsWithModuleName.yapfPath); - assert.equal(execInfo.moduleName, formattingSettingsWithModuleName.yapfPath); - assert.deepEqual(execInfo.args, formattingSettingsWithPath.yapfArgs.concat(['--diff', docUri.fsPath])); - }); -}); diff --git a/src/test/index.ts b/src/test/index.ts index b3a5a58c82fe..a4c69a2a9ac6 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,7 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; -// tslint:disable:no-require-imports no-var-requires no-any + // Always place at the top, to ensure other modules are imported first. require('./common/exitCIAfterTestReporter'); @@ -9,51 +10,26 @@ if ((Reflect as any).metadata === undefined) { require('reflect-metadata'); } +import * as glob from 'glob'; +import * as Mocha from 'mocha'; import * as path from 'path'; -import { - IS_CI_SERVER_TEST_DEBUGGER, MOCHA_REPORTER_JUNIT -} from './ciConstants'; -import { IS_MULTI_ROOT_TEST } 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(); - -// Check for a grep setting. Might be running a subset of the tests -const defaultGrep = process.env.VSC_PYTHON_CI_TEST_GREP; - -// 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; - -// 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 -}; -(options as any).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}` +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'; + +initializeLogger(); + +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); -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<void> { + 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<string[]>((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<void>((resolve, reject) => { + mocha.run((failures) => { + if (failures > 0) { + return reject(new Error(`${failures} total failures`)); + } + resolve(); + }); + }); +} diff --git a/src/test/initialize.ts b/src/test/initialize.ts index a3a2e843b4be..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,51 +29,85 @@ export async function initializePython() { await setPythonPathInWorkspaceRoot(PYTHON_PATH); } -// tslint:disable-next-line:no-any export async function initialize(): Promise<IExtensionTestApi> { await initializePython(); + + const pythonConfig = vscode.workspace.getConfiguration('python'); + await pythonConfig.update('experiments.optInto', ['All'], vscode.ConfigurationTarget.Global); + await pythonConfig.update('experiments.optOutFrom', [], vscode.ConfigurationTarget.Global); const api = await activateExtension(); if (!IS_SMOKE_TEST) { // When running smoke tests, we won't have access to these. - const configSettings = await import('../client/common/configSettings'); + const configSettings = await import('../client/common/configSettings.js'); // Dispose any cached python settings (used only in test env). configSettings.PythonSettings.dispose(); } - // tslint:disable-next-line:no-any - return api as any as IExtensionTestApi; + + return (api as any) as IExtensionTestApi; } export async function activateExtension() { - const extension = vscode.extensions.getExtension<IExtensionApi>(PVSC_EXTENSION_ID_FOR_TESTS)!; + const extension = vscode.extensions.getExtension<PythonExtension>(PVSC_EXTENSION_ID_FOR_TESTS)!; const api = await extension.activate(); - // Wait 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<any> { await initializePython(); await closeActiveWindows(); if (!IS_SMOKE_TEST) { // When running smoke tests, we won't have access to these. - const configSettings = await import('../client/common/configSettings'); + const configSettings = await import('../client/common/configSettings.js'); // Dispose any cached python settings (used only in test env). configSettings.PythonSettings.dispose(); } } export async function closeActiveWindows(): Promise<void> { + await closeActiveNotebooks(); + await closeWindowsInteral(); +} +export async function closeActiveNotebooks(): Promise<void> { + if (!vscode.env.appName.toLowerCase().includes('insiders') || !isANotebookOpen()) { + return; + } + // We could have untitled notebooks, close them by reverting changes. + + 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<void>((resolve, reject) => { // Attempt to fix #1301. // Lets not waste too much time. const timer = setTimeout(() => { - reject(new Error('Command \'workbench.action.closeAllEditors\' timed out')); + reject(new Error("Command 'workbench.action.closeAllEditors' timed out")); }, 15000); - vscode.commands.executeCommand('workbench.action.closeAllEditors') - .then(() => { + vscode.commands.executeCommand('workbench.action.closeAllEditors').then( + () => { clearTimeout(timer); resolve(); - }, ex => { + }, + (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 774bda95a43b..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<IInterpreterLocatorService>; setup(() => { const cont = new Container(); serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - pipEnv = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); - serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, pipEnv.object, PIPENV_SERVICE); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + 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<IApplicationShell>(); + const appShell = createTypeMoq<IApplicationShell>(); serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - // tslint:disable-next-line:no-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any let items: any[] | undefined; appShell - .setup(x => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((i: string[], _o: QuickPickOptions) => { + .setup((x) => x.showQuickPick(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((i: string[]) => { items = i; }) - .returns(() => new Promise<string | undefined>((resolve, _reject) => resolve(undefined))); + .returns( + () => new Promise<string | undefined>((resolve, _reject) => resolve(undefined)), + ); - installer1.setup(x => x.displayName).returns(() => 'Name 1'); - installer2.setup(x => x.displayName).returns(() => 'Name 2'); + installer1.setup((x) => x.displayName).returns(() => 'Name 1'); + installer2.setup((x) => x.displayName).returns(() => 'Name 2'); const cm = new InstallationChannelManager(serviceContainer); - await cm.getInstallationChannel(Product.pylint); + await cm.getInstallationChannel(Product.pytest); - assert.notEqual(items, undefined, 'showQuickPick not called'); - assert.equal(items!.length, 2, 'Incorrect number of installer shown'); - assert.notEqual(items![0]!.label!.indexOf('Name 1'), -1, 'Incorrect first installer name'); - assert.notEqual(items![1]!.label!.indexOf('Name 2'), -1, 'Incorrect second installer name'); + assert.notStrictEqual(items, undefined, 'showQuickPick not called'); + assert.strictEqual(items!.length, 2, 'Incorrect number of installer shown'); + assert.notStrictEqual(items![0]!.label!.indexOf('Name 1'), -1, 'Incorrect first installer name'); + assert.notStrictEqual(items![1]!.label!.indexOf('Name 2'), -1, 'Incorrect second installer name'); }); function mockInstaller(supported: boolean, name: string, priority?: number): TypeMoq.IMock<IModuleInstaller> { - const installer = TypeMoq.Mock.ofType<IModuleInstaller>(); + const installer = createTypeMoq<IModuleInstaller>(); installer - .setup(x => x.isSupported(TypeMoq.It.isAny())) - .returns(() => new Promise<boolean>((resolve) => resolve(supported))); - installer.setup(x => x.priority).returns(() => priority ? priority : 0); + .setup((x) => x.isSupported(TypeMoq.It.isAny())) + .returns( + () => new Promise<boolean>((resolve) => resolve(supported)), + ); + installer.setup((x) => x.priority).returns(() => priority || 0); serviceManager.addSingletonInstance<IModuleInstaller>(IModuleInstaller, installer.object, name); return installer; } diff --git a/src/test/install/channelManager.messages.test.ts b/src/test/install/channelManager.messages.test.ts index 65b3cbd3cc1f..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<IPlatformService>; @@ -42,138 +46,147 @@ suite('Installation - channel messages', () => { const serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - platform = TypeMoq.Mock.ofType<IPlatformService>(); + platform = createTypeMoq<IPlatformService>(); serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platform.object); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + appShell = createTypeMoq<IApplicationShell>(); serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - interpreters = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreters = createTypeMoq<IInterpreterService>(); serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreters.object); - const moduleInstaller = TypeMoq.Mock.ofType<IModuleInstaller>(); + const moduleInstaller = createTypeMoq<IModuleInstaller>(); serviceManager.addSingletonInstance<IModuleInstaller>(IModuleInstaller, moduleInstaller.object); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, + ); }); test('No installers message: Unknown/Windows', async () => { - platform.setup(x => x.isWindows).returns(() => true); - await testInstallerMissingMessage(InterpreterType.Unknown, - async (message: string, url: string) => { - 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<void>, - methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage'): Promise<void> { - - const activeInterpreter: PythonInterpreter = { + methodType: 'showNoInstallersMessage' | 'getInstallationChannel' = 'showNoInstallersMessage', + ): Promise<void> { + const activeInterpreter: PythonEnvironment = { ...info, - type: interpreterType, - path: '' + envType: interpreterType, + path: '', }; interpreters - .setup(x => x.getActiveInterpreter(TypeMoq.It.isAny())) - .returns(() => new Promise<PythonInterpreter>((resolve, _reject) => resolve(activeInterpreter))); + .setup((x) => x.getActiveInterpreter(TypeMoq.It.isAny())) + .returns( + () => new Promise<PythonEnvironment>((resolve, _reject) => resolve(activeInterpreter)), + ); const channels = new InstallationChannelManager(serviceContainer); - let url: string = ''; - let message: string = ''; - let search: string = ''; + 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<string>((resolve, _reject) => resolve(search))); - appShell.setup(x => x.openUrl(TypeMoq.It.isAnyString())).callback((s: string) => { - url = s; - }); + .returns( + () => new Promise<string>((resolve, _reject) => resolve(search)), + ); + appShell + .setup((x) => x.openUrl(TypeMoq.It.isAnyString())) + .callback((s: string) => { + url = s; + }); if (methodType === 'showNoInstallersMessage') { await channels.showNoInstallersMessage(); } else { - await channels.getInstallationChannel(Product.pylint); + await channels.getInstallationChannel(Product.pytest); } await verify(message, url); } diff --git a/src/test/interpreters/activation/indicatorPrompt.unit.test.ts b/src/test/interpreters/activation/indicatorPrompt.unit.test.ts new file mode 100644 index 000000000000..b15cd84dc01a --- /dev/null +++ b/src/test/interpreters/activation/indicatorPrompt.unit.test.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as sinon from 'sinon'; +import { mock, when, anything, instance, verify, reset } from 'ts-mockito'; +import { EventEmitter, Terminal, Uri } from 'vscode'; +import { IActiveResourceService, IApplicationShell, ITerminalManager } from '../../../client/common/application/types'; +import { + IConfigurationService, + IExperimentService, + IPersistentState, + IPersistentStateFactory, + IPythonSettings, +} from '../../../client/common/types'; +import { TerminalIndicatorPrompt } from '../../../client/terminals/envCollectionActivation/indicatorPrompt'; +import { Common, Interpreters } from '../../../client/common/utils/localize'; +import { TerminalEnvVarActivation } from '../../../client/common/experiments/groups'; +import { sleep } from '../../core'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { ITerminalEnvVarCollectionService } from '../../../client/terminals/types'; +import { PythonEnvType } from '../../../client/pythonEnvironments/base/info'; +import * as extapi from '../../../client/envExt/api.internal'; + +suite('Terminal Activation Indicator Prompt', () => { + let shell: IApplicationShell; + let terminalManager: ITerminalManager; + let experimentService: IExperimentService; + let activeResourceService: IActiveResourceService; + let terminalEnvVarCollectionService: ITerminalEnvVarCollectionService; + let persistentStateFactory: IPersistentStateFactory; + let terminalEnvVarCollectionPrompt: TerminalIndicatorPrompt; + let terminalEventEmitter: EventEmitter<Terminal>; + let notificationEnabled: IPersistentState<boolean>; + let configurationService: IConfigurationService; + let interpreterService: IInterpreterService; + let useEnvExtensionStub: sinon.SinonStub; + const prompts = [Common.doNotShowAgain]; + const envName = 'env'; + const type = PythonEnvType.Virtual; + const expectedMessage = Interpreters.terminalEnvVarCollectionPrompt.format('Python virtual', `"(${envName})"`); + + setup(async () => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + shell = mock<IApplicationShell>(); + terminalManager = mock<ITerminalManager>(); + interpreterService = mock<IInterpreterService>(); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName, + type, + } as unknown) as PythonEnvironment); + experimentService = mock<IExperimentService>(); + activeResourceService = mock<IActiveResourceService>(); + persistentStateFactory = mock<IPersistentStateFactory>(); + terminalEnvVarCollectionService = mock<ITerminalEnvVarCollectionService>(); + configurationService = mock<IConfigurationService>(); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: true, + }, + } as unknown) as IPythonSettings); + notificationEnabled = mock<IPersistentState<boolean>>(); + terminalEventEmitter = new EventEmitter<Terminal>(); + when(persistentStateFactory.createGlobalPersistentState(anything(), true)).thenReturn( + instance(notificationEnabled), + ); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + when(terminalManager.onDidOpenTerminal).thenReturn(terminalEventEmitter.event); + terminalEnvVarCollectionPrompt = new TerminalIndicatorPrompt( + instance(shell), + instance(persistentStateFactory), + instance(terminalManager), + [], + instance(activeResourceService), + instance(terminalEnvVarCollectionService), + instance(configurationService), + instance(interpreterService), + instance(experimentService), + ); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Show notification when a new terminal is opened for which there is no prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).once(); + }); + + test('Do not show notification if automatic terminal activation is turned off', async () => { + reset(configurationService); + when(configurationService.getSettings(anything())).thenReturn(({ + terminal: { + activateEnvironment: false, + }, + } as unknown) as IPythonSettings); + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('When not in experiment, do not show notification for the same', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification if notification is disabled', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(false); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test('Do not show notification when a new terminal is opened for which there is prompt set', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(true); + when(notificationEnabled.value).thenReturn(true); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenResolve(undefined); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(shell.showInformationMessage(expectedMessage, ...prompts)).never(); + }); + + test("Disable notification if `Don't show again` is clicked", async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn( + Promise.resolve(Common.doNotShowAgain), + ); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).once(); + }); + + test('Do not disable notification if prompt is closed', async () => { + const resource = Uri.file('a'); + const terminal = ({ + creationOptions: { + cwd: resource, + }, + } as unknown) as Terminal; + when(terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource)).thenReturn(false); + when(notificationEnabled.value).thenReturn(true); + when(notificationEnabled.updateValue(false)).thenResolve(); + when(shell.showInformationMessage(expectedMessage, ...prompts)).thenReturn(Promise.resolve(undefined)); + + await terminalEnvVarCollectionPrompt.activate(); + terminalEventEmitter.fire(terminal); + await sleep(1); + + verify(notificationEnabled.updateValue(false)).never(); + }); +}); diff --git a/src/test/interpreters/activation/service.unit.test.ts b/src/test/interpreters/activation/service.unit.test.ts index 35e31b9d86b8..a0f9b3bd6915 100644 --- a/src/test/interpreters/activation/service.unit.test.ts +++ b/src/test/interpreters/activation/service.unit.test.ts @@ -7,8 +7,9 @@ import { EOL } from 'os'; import * as path from 'path'; import { SemVer } from 'semver'; import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Uri, workspace as workspaceType, WorkspaceConfiguration } from 'vscode'; +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'; @@ -17,28 +18,27 @@ import { ProcessServiceFactory } from '../../../client/common/process/processFac import { IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; import { TerminalHelper } from '../../../client/common/terminal/helper'; import { ITerminalHelper } from '../../../client/common/terminal/types'; -import { ICurrentProcess } from '../../../client/common/types'; -import { clearCache } from '../../../client/common/utils/cacheUtils'; +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 { InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { noop } from '../../core'; -import { mockedVSCodeNamespaces } from '../../vscode-mock'; +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 + [OSType.Unknown]: undefined, }; -// tslint:disable:no-unnecessary-override no-any max-func-body-length -suite('Interprters Activation - Python Environment Variables', () => { +suite('Interpreters Activation - Python Environment Variables', () => { let service: EnvironmentActivationService; let helper: ITerminalHelper; let platform: IPlatformService; @@ -46,188 +46,312 @@ suite('Interprters Activation - Python Environment Variables', () => { let processService: IProcessService; let currentProcess: ICurrentProcess; let envVarsService: IEnvironmentVariablesProvider; - let workspace: typemoq.IMock<typeof workspaceType>; - - const pythonInterpreter: PythonInterpreter = { + let workspace: IWorkspaceService; + let interpreterService: IInterpreterService; + let onDidChangeEnvVariables: EventEmitter<Uri | undefined>; + let onDidChangeInterpreter: EventEmitter<Resource>; + const pythonInterpreter: PythonEnvironment = { path: '/foo/bar/python.exe', version: new SemVer('3.6.6-final'), sysVersion: '1.0.0.0', sysPrefix: 'Python', - type: InterpreterType.Unknown, - architecture: Architecture.x64 + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, }; - function initSetup() { + function initSetup(interpreter: PythonEnvironment | undefined) { helper = mock(TerminalHelper); platform = mock(PlatformService); processServiceFactory = mock(ProcessServiceFactory); processService = mock(ProcessService); currentProcess = mock(CurrentProcess); envVarsService = mock(EnvironmentVariablesProvider); - workspace = mockedVSCodeNamespaces.workspace!; - when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(noop as any); + interpreterService = mock(InterpreterService); + workspace = mock(WorkspaceService); + onDidChangeEnvVariables = new EventEmitter<Uri | undefined>(); + onDidChangeInterpreter = new EventEmitter<Resource>(); + when(envVarsService.onDidEnvironmentVariablesChange).thenReturn(onDidChangeEnvVariables.event); + when(interpreterService.onDidChangeInterpreter).thenReturn(onDidChangeInterpreter.event); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(interpreter); service = new EnvironmentActivationService( - instance(helper), instance(platform), - instance(processServiceFactory), instance(currentProcess), - instance(envVarsService) + instance(helper), + instance(platform), + instance(processServiceFactory), + instance(currentProcess), + instance(workspace), + instance(interpreterService), + instance(envVarsService), ); - - const cfg = typemoq.Mock.ofType<WorkspaceConfiguration>(); - workspace.setup(w => w.getConfiguration(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => cfg.object); - workspace.setup(w => w.workspaceFolders).returns(() => []); - cfg.setup(c => c.inspect(typemoq.It.isValue('pythonPath'))) - .returns(() => { return { globalValue: 'GlobalValuepython' } as any; }); - clearCache(); - - verify(envVarsService.onDidEnvironmentVariablesChange).once(); } - teardown(() => { - mockedVSCodeNamespaces.workspace!.reset(); - }); - function title(resource?: Uri, interpreter?: PythonInterpreter) { + function title(resource?: Uri, interpreter?: PythonEnvironment) { return `${resource ? 'With a resource' : 'Without a resource'}${interpreter ? ' and an interpreter' : ''}`; } - [undefined, Uri.parse('a')].forEach(resource => - [undefined, pythonInterpreter].forEach(interpreter => { - suite(title(resource, interpreter), () => { - setup(initSetup); - test('Unknown os will return empty variables', async () => { - when(platform.osType).thenReturn(OSType.Unknown); - const env = await service.getActivatedEnvironmentVariables(resource); + [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'); - }); + verify(platform.osType).once(); + expect(env).to.equal(undefined, 'Should not have any variables'); + }); - const osTypes = getNamesAndValues<OSType>(OSType) - .filter(osType => osType.value !== OSType.Unknown); + const osTypes = getNamesAndValues<OSType>(OSType).filter((osType) => osType.value !== OSType.Unknown); - osTypes.forEach(osType => { - suite(osType.name, () => { - setup(initSetup); - test('getEnvironmentActivationShellCommands will be invoked', async () => { - when(platform.osType).thenReturn(osType.value); - when(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).thenResolve(); + 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); + const env = await service.getActivatedEnvironmentVariables(resource, interpreter); - verify(platform.osType).once(); - expect(env).to.equal(undefined, 'Should not have any variables'); - verify(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).once(); - }); - test('Validate command used to activation and printing env vars', async () => { - const cmd = ['1', '2']; - const envVars = { one: '1', two: '2' }; - when(platform.osType).thenReturn(osType.value); - when(helper.getEnvironmentActivationShellCommands(resource, anything(), interpreter)).thenResolve(cmd); - when(processServiceFactory.create(resource)).thenResolve(instance(processService)); - when(envVarsService.getEnvironmentVariables(resource)).thenResolve(envVars); + 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 env = await service.getActivatedEnvironmentVariables(resource, interpreter); + const microVenv = { ...pythonInterpreter, envType: EnvironmentType.Venv }; + const key = getSearchPathEnvVarNames()[0]; + const varsFromEnv = { [key]: '/foo/bar' }; - 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(); + when( + helper.getEnvironmentActivationShellCommands(resource, anything(), microVenv), + ).thenResolve(); - const shellCmd = capture(processService.shellExec).first()[0]; + const env = await service.getActivatedEnvironmentVariables(resource, microVenv); - const printEnvPyFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'printEnvVariables.py'); - const expectedCommand = `${cmd.join(' && ')} && echo '${getEnvironmentPrefix}' && python ${printEnvPyFile.fileToCommandArgument()}`; + 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); - 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 }); - }); - test('Use current process variables if there are no custom variables', 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({}); - 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({ shell: expectedShell, env: envVars, timeout: 30000, maxBuffer: 1000 * 1000 }); - }); - 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(); + 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<WorkspaceConfiguration>; + const progressOptions = { + location: ProgressLocation.Window, + title: Interpreters.activatingTerminals, + }; + let configService: IConfigurationService; + let shellIntegrationService: IShellIntegrationDetectionService; + const displayPath = 'display/path'; + const customShell = 'powershell'; + const defaultShell = defaultShells[getOSType()]; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + workspaceService = mock<IWorkspaceService>(); + terminalDeactivateService = mock<ITerminalDeactivateService>(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve(undefined); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); + when(workspaceService.getWorkspaceFolder(anything())).thenReturn(undefined); + when(workspaceService.workspaceFolders).thenReturn(undefined); + platform = mock<IPlatformService>(); + when(platform.osType).thenReturn(getOSType()); + interpreterService = mock<IInterpreterService>(); + context = mock<IExtensionContext>(); + shell = mock<IApplicationShell>(); + const envVarProvider = mock<IEnvironmentVariablesProvider>(); + shellIntegrationService = mock<IShellIntegrationDetectionService>(); + when(shellIntegrationService.isWorking()).thenResolve(true); + globalCollection = mock<GlobalEnvironmentVariableCollection>(); + collection = mock<EnvironmentVariableCollection>(); + when(context.environmentVariableCollection).thenReturn(instance(globalCollection)); + when(globalCollection.getScoped(anything())).thenReturn(instance(collection)); + experimentService = mock<IExperimentService>(); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(true); + applicationEnvironment = mock<IApplicationEnvironment>(); + when(applicationEnvironment.shell).thenReturn(customShell); + when(shell.withProgress(anything(), anything())) + .thenCall((options, _) => { + expect(options).to.deep.equal(progressOptions); + }) + .thenResolve(); + environmentActivationService = mock<IEnvironmentActivationService>(); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + process.env, + ); + configService = mock<IConfigurationService>(); + when(configService.getSettings(anything())).thenReturn(({ + terminal: { activateEnvironment: true }, + pythonPath: displayPath, + } as unknown) as IPythonSettings); + when(collection.clear()).thenResolve(); + terminalEnvVarCollectionService = new TerminalEnvVarCollectionService( + instance(platform), + instance(interpreterService), + instance(context), + instance(shell), + instance(experimentService), + instance(applicationEnvironment), + [], + instance(environmentActivationService), + instance(workspaceService), + instance(configService), + instance(terminalDeactivateService), + new PathUtils(getOSType() === OSType.Windows), + instance(shellIntegrationService), + instance(envVarProvider), + ); + pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Apply activated variables to the collection on activation', async () => { + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); + await terminalEnvVarCollectionService.activate(undefined); + assert(applyCollectionStub.calledOnce, 'Collection not applied on activation'); + }); + + test('When not in experiment, do not apply activated variables to the collection and clear it instead', async () => { + reset(experimentService); + when(experimentService.inExperimentSync(TerminalEnvVarActivation.experiment)).thenReturn(false); + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService.activate(undefined); + + verify(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).once(); + verify(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).never(); + assert(applyCollectionStub.notCalled, 'Collection should not be applied on activation'); + + verify(globalCollection.clear()).atLeast(1); + }); + + test('When interpreter changes, apply new activated variables to the collection', async () => { + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + const resource = Uri.file('x'); + let callback: (resource: Resource) => Promise<void>; + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenCall((cb) => { + callback = cb; + }); + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenReturn(); + await terminalEnvVarCollectionService.activate(undefined); + + await callback!(resource); + assert(applyCollectionStub.calledWithExactly(resource)); + }); + + test('When selected shell changes, apply new activated variables to the collection', async () => { + const applyCollectionStub = sinon.stub(terminalEnvVarCollectionService, '_applyCollection'); + applyCollectionStub.resolves(); + let callback: (shell: string) => Promise<void>; + when(applicationEnvironment.onDidChangeShell(anything(), anything(), anything())).thenCall((cb) => { + callback = cb; + }); + when(interpreterService.onDidChangeInterpreter(anything(), anything(), anything())).thenReturn(); + await terminalEnvVarCollectionService.activate(undefined); + + await callback!(customShell); + assert(applyCollectionStub.calledWithExactly(undefined, customShell)); + }); + + test('If activated variables are returned for custom shell, apply it correctly to the collection', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + // eslint-disable-next-line consistent-return + test('If activated variables contain PS1, prefix it using shell integration', async function () { + if (getOSType() === OSType.Windows) { + return this.skip(); + } + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + PS1: '(envName) extra prompt', // Should not use this + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + envName: 'envName', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + + test('Respect VIRTUAL_ENV_DISABLE_PROMPT when setting PS1 for venv', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + VIRTUAL_ENV_DISABLE_PROMPT: '1', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', anything(), anything())).never(); + }); + + test('Otherwise set PS1 for venv even if PS1 is not returned', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + VIRTUAL_BIN: 'prefix/to/conda', + ...process.env, + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Virtual, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + when(collection.prepend('PS1', '(envName) ', anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.prepend('PS1', '(envName) ', anything())).once(); + }); + + test('Respect CONDA_PROMPT_MODIFIER when setting PS1 for conda', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(envName)', + }; + when( + environmentActivationService.getActivatedEnvironmentVariables(anything(), undefined, undefined, 'bash'), + ).thenResolve(envVars); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PS1', '(envName) ', anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, 'bash'); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + assert.deepEqual(opts, { applyAtProcessCreation: false, applyAtShellIntegration: true }); + }); + + test('Prepend only "prepend portion of PATH" where applicable', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', prependedPart, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Also prepend deactivate script location if available', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenReject(); // Verify we swallow errors from here + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const prependedPart = 'path/to/activate/dir:'; + const envVars: NodeJS.ProcessEnv = { PATH: `${prependedPart}${processEnv.PATH}` }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + verify(collection.prepend('PATH', `scriptLocation${separator}${prependedPart}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH with separator otherwise', async () => { + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Prepend full PATH with separator otherwise', async () => { + reset(terminalDeactivateService); + when(terminalDeactivateService.initializeScriptParams(anything())).thenResolve(); + when(terminalDeactivateService.getScriptLocation(anything(), anything())).thenResolve('scriptLocation'); + const processEnv = { PATH: 'hello/1/2/3' }; + reset(environmentActivationService); + when(environmentActivationService.getProcessEnvironmentVariables(anything(), anything())).thenResolve( + processEnv, + ); + const separator = getOSType() === OSType.Windows ? ';' : ':'; + const finalPath = 'hello/3/2/1'; + const envVars: NodeJS.ProcessEnv = { PATH: finalPath }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + let opts: EnvironmentVariableMutatorOptions | undefined; + when(collection.prepend('PATH', anything(), anything())).thenCall((_, _v, o) => { + opts = o; + }); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.prepend('PATH', `scriptLocation${separator}${finalPath}${separator}`, anything())).once(); + verify(collection.replace('PATH', anything(), anything())).never(); + assert.deepEqual(opts, { applyAtProcessCreation: true, applyAtShellIntegration: true }); + }); + + test('Verify envs are not applied if env activation is disabled', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + reset(configService); + when(configService.getSettings(anything())).thenReturn(({ + terminal: { activateEnvironment: false }, + pythonPath: displayPath, + } as unknown) as IPythonSettings); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).never(); + }); + + test('Verify correct options are used when applying envs and setting description', async () => { + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, customShell), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenCall( + (_e, _v, options: EnvironmentVariableMutatorOptions) => { + assert.deepEqual(options, { applyAtShellIntegration: true, applyAtProcessCreation: true }); + return Promise.resolve(); + }, + ); + + await terminalEnvVarCollectionService._applyCollection(resource, customShell); + + verify(collection.clear()).once(); + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + }); + + test('Correct track that prompt was set for non-Windows bash where PS1 is set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for PS1 if shell integration is disabled', async () => { + reset(shellIntegrationService); + when(shellIntegrationService.isWorking()).thenResolve(false); + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', PS1: '(.venv)', ...process.env }; + const ps1Shell = 'bash'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set for non-Windows where PS1 is not set but should be set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for non-Windows where PS1 is not set but env name is base', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { + CONDA_PREFIX: 'prefix/to/conda', + ...process.env, + CONDA_PROMPT_MODIFIER: '(base)', + }; + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'base', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was not set for non-Windows fish where PS1 is not set', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + const ps1Shell = 'fish'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Conda, + envName: 'envName', + envPath: 'prefix/to/conda', + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('Correct track that prompt was set correctly for global interpreters', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const ps1Shell = 'zsh'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: undefined, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, ps1Shell), + ).thenResolve(undefined); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, ps1Shell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was set for Windows when not using powershell', async () => { + when(platform.osType).thenReturn(OSType.Windows); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'cmd'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(true); + }); + + test('Correct track that prompt was not set for Windows when using powershell', async () => { + when(platform.osType).thenReturn(OSType.Linux); + const envVars: NodeJS.ProcessEnv = { VIRTUAL_ENV: 'prefix/to/venv', ...process.env }; + const windowsShell = 'powershell'; + const resource = Uri.file('a'); + const workspaceFolder: WorkspaceFolder = { + uri: Uri.file('workspacePath'), + name: 'workspace1', + index: 0, + }; + when(interpreterService.getActiveInterpreter(resource)).thenResolve(({ + type: PythonEnvType.Virtual, + } as unknown) as PythonEnvironment); + when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); + when( + environmentActivationService.getActivatedEnvironmentVariables(resource, undefined, undefined, windowsShell), + ).thenResolve(envVars); + when(collection.replace(anything(), anything(), anything())).thenReturn(); + + await terminalEnvVarCollectionService._applyCollection(resource, windowsShell); + + const result = terminalEnvVarCollectionService.isTerminalPromptSetCorrectly(resource); + + expect(result).to.equal(false); + }); + + test('If no activated variables are returned for custom shell, fallback to using default shell', async () => { + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + customShell, + ), + ).thenResolve(undefined); + const envVars = { CONDA_PREFIX: 'prefix/to/conda', ...process.env }; + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + defaultShell?.shell, + ), + ).thenResolve(envVars); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, customShell); + + verify(collection.replace('CONDA_PREFIX', 'prefix/to/conda', anything())).once(); + verify(collection.clear()).once(); + }); + + test('If no activated variables are returned for default shell, clear collection', async () => { + when( + environmentActivationService.getActivatedEnvironmentVariables( + anything(), + undefined, + undefined, + defaultShell?.shell, + ), + ).thenResolve(undefined); + + when(collection.replace(anything(), anything(), anything())).thenResolve(); + when(collection.delete(anything())).thenResolve(); + + await terminalEnvVarCollectionService._applyCollection(undefined, defaultShell?.shell); + + verify(collection.clear()).once(); + }); +}); diff --git a/src/test/interpreters/autoSelection/index.unit.test.ts b/src/test/interpreters/autoSelection/index.unit.test.ts index 54ed54b5b13c..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 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,22 +37,22 @@ suite('Interpreters - Auto Selection', () => { let workspaceService: IWorkspaceService; let stateFactory: IPersistentStateFactory; let fs: IFileSystem; - let systemInterpreter: IInterpreterAutoSelectionRule; - let currentPathInterpreter: IInterpreterAutoSelectionRule; - let winRegInterpreter: IInterpreterAutoSelectionRule; - let cachedPaths: IInterpreterAutoSelectionRule; - let userDefinedInterpreter: IInterpreterAutoSelectionRule; - let workspaceInterpreter: IInterpreterAutoSelectionRule; - let state: PersistentState<PythonInterpreter | undefined>; + let state: PersistentState<PythonEnvironment | undefined>; let helper: IInterpreterHelper; - let proxy: IInterpreterAutoSeletionProxyService; + let proxy: IInterpreterAutoSelectionProxyService; + let interpreterService: IInterpreterService; + let experimentService: IExperimentService; + let sendTelemetryEventStub: sinon.SinonStub; + let telemetryEvents: { eventName: string; properties: Record<string, unknown> }[] = []; class InterpreterAutoSelectionServiceTest extends InterpreterAutoSelectionService { public initializeStore(resource: Resource): Promise<void> { return super.initializeStore(resource); } - public storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { + + public storeAutoSelectedInterpreter(resource: Resource, interpreter: PythonEnvironment | undefined) { return super.storeAutoSelectedInterpreter(resource, interpreter); } + public getAutoSelectedWorkspacePromises() { return this.autoSelectedWorkspacePromises; } @@ -58,119 +60,447 @@ suite('Interpreters - Auto Selection', () => { setup(() => { workspaceService = mock(WorkspaceService); stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); + state = mock(PersistentState) as PersistentState<PythonEnvironment | undefined>; fs = mock(FileSystem); - systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); - currentPathInterpreter = mock(CurrentPathInterpretersAutoSelectionRule); - winRegInterpreter = mock(WindowsRegistryInterpretersAutoSelectionRule); - cachedPaths = mock(CachedInterpretersAutoSelectionRule); - userDefinedInterpreter = mock(SettingsInterpretersAutoSelectionRule); - workspaceInterpreter = mock(WorkspaceVirtualEnvInterpretersAutoSelectionRule); helper = mock(InterpreterHelper); - proxy = mock(InterpreterAutoSeletionProxyService); + proxy = mock(InterpreterAutoSelectionProxyService); + interpreterService = mock(InterpreterService); + experimentService = mock<IExperimentService>(); + when(experimentService.inExperimentSync(anything())).thenReturn(false); + + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); autoSelectionService = new InterpreterAutoSelectionServiceTest( - instance(workspaceService), instance(stateFactory), instance(fs), - instance(systemInterpreter), instance(currentPathInterpreter), - instance(winRegInterpreter), instance(cachedPaths), - instance(userDefinedInterpreter), instance(workspaceInterpreter), - instance(proxy), instance(helper) + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), ); + + when(interpreterService.refreshPromise).thenReturn(undefined); + when(interpreterService.getInterpreters(anything())).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pyenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 5, patch: 0 }, + } as PythonEnvironment, + ]); + + sendTelemetryEventStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake((( + eventName: string, + _, + properties: Record<string, unknown>, + ) => { + const telemetry = { eventName, properties }; + telemetryEvents.push(telemetry); + }) as typeof Telemetry.sendTelemetryEvent); + }); + + teardown(() => { + sinon.restore(); + Telemetry._resetSharedProperties(); + telemetryEvents = []; }); test('Instance is registered in proxy', () => { verify(proxy.registerInstance!(autoSelectionService)).once(); }); - test('Rules are chained in order of preference', () => { - verify(userDefinedInterpreter.setNextRule(instance(workspaceInterpreter))).once(); - verify(workspaceInterpreter.setNextRule(instance(cachedPaths))).once(); - verify(cachedPaths.setNextRule(instance(currentPathInterpreter))).once(); - verify(currentPathInterpreter.setNextRule(instance(winRegInterpreter))).once(); - verify(winRegInterpreter.setNextRule(instance(systemInterpreter))).once(); - verify(systemInterpreter.setNextRule(anything())).never(); - }); - test('Run rules in background', async () => { - let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); - expect(eventFired).to.deep.equal(true, 'event not fired'); - - const allRules = [userDefinedInterpreter, winRegInterpreter, currentPathInterpreter, systemInterpreter, workspaceInterpreter, cachedPaths]; - for (const service of allRules) { - verify(service.autoSelectInterpreter(undefined)).once(); - if (service !== userDefinedInterpreter) { - verify(service.autoSelectInterpreter(anything(), autoSelectionService)).never(); - } - } - verify(userDefinedInterpreter.autoSelectInterpreter(anything(), autoSelectionService)).once(); + suite('Test locator-based auto-selection method', () => { + let workspacePath: string; + let resource: Uri; + let eventFired: boolean; + + setup(() => { + workspacePath = path.join('path', 'to', 'workspace'); + resource = Uri.parse('resource'); + eventFired = false; + + const folderUri = { fsPath: workspacePath }; + + when(helper.getActiveWorkspaceUri(anything())).thenReturn({ + folderUri, + } as WorkspacePythonPath); + when( + stateFactory.createWorkspacePersistentState<PythonEnvironment | undefined>(anyString(), undefined), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + 'autoSelectionInterpretersQueriedOnce', + undefined, + ), + ).thenReturn(instance(state)); + when(workspaceService.getWorkspaceFolderIdentifier(anything(), '')).thenReturn('workspaceIdentifier'); + + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = () => Promise.resolve(); + }); + + test('If there is a local environment select it', async () => { + const localEnv = { + envType: EnvironmentType.Venv, + type: PythonEnvType.Virtual, + envPath: path.join(workspacePath, '.venv'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + type: PythonEnvType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment, + localEnv, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(localEnv)).once(); + }); + + test('If there are no local environments, return a globally-installed interpreter', async () => { + const systemEnv = { + envType: EnvironmentType.System, + envPath: path.join('/', 'usr', 'bin'), + version: { major: 3, minor: 9, patch: 1 }, + } as PythonEnvironment; + + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + systemEnv, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + await autoSelectionService.autoSelectInterpreter(resource); + + expect(eventFired).to.deep.equal(true, 'event not fired'); + verify(interpreterService.getInterpreters(resource)).once(); + verify(state.updateValue(systemEnv)).once(); + }); + + test('getInterpreters is called with ignoreCache at true if there is no value set in the workspace persistent state', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + const globalQueriedState = mock(PersistentState) as PersistentState<boolean | undefined>; + when(globalQueriedState.value).thenReturn(true); + when(stateFactory.createGlobalPersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(globalQueriedState), + ); + + const queryState = mock(PersistentState) as PersistentState<boolean | undefined>; + + when(queryState.value).thenReturn(undefined); + when(stateFactory.createWorkspacePersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(interpreterService.triggerRefresh(anything())).thenResolve(); + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.triggerRefresh(anything())).once(); + }); + + test('getInterpreters is called with ignoreCache at false if there is a value set in the workspace persistent state', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + const queryState = mock(PersistentState) as PersistentState<boolean | undefined>; + + when(queryState.value).thenReturn(true); + when(stateFactory.createWorkspacePersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(interpreterService.triggerRefresh(anything())).thenResolve(); + when(interpreterService.getInterpreters(resource)).thenCall((_) => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.getInterpreters(resource)).once(); + verify(interpreterService.triggerRefresh(anything())).never(); + }); + + test('Telemetry event is sent with useCachedInterpreter set to false if auto-selection has not been run before', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + when(interpreterService.getInterpreters(resource)).thenCall(() => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.getInterpreters(resource)).once(); + sinon.assert.calledOnce(sendTelemetryEventStub); + expect(telemetryEvents).to.deep.equal( + [ + { + eventName: EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + properties: { useCachedInterpreter: false }, + }, + ], + 'Telemetry event properties are different', + ); + }); + + test('Telemetry event is sent with useCachedInterpreter set to true if auto-selection has been run before', async () => { + const interpreterComparer = new EnvironmentTypeComparer(instance(helper)); + + when(interpreterService.getInterpreters(resource)).thenCall(() => [ + { + envType: EnvironmentType.Conda, + envPath: path.join('some', 'conda', 'env'), + version: { major: 3, minor: 7, patch: 2 }, + } as PythonEnvironment, + { + envType: EnvironmentType.Pipenv, + envPath: path.join('some', 'pipenv', 'env'), + version: { major: 3, minor: 10, patch: 0 }, + } as PythonEnvironment, + ]); + + autoSelectionService = new InterpreterAutoSelectionServiceTest( + instance(workspaceService), + instance(stateFactory), + instance(fs), + instance(interpreterService), + interpreterComparer, + instance(proxy), + instance(helper), + instance(experimentService), + ); + + autoSelectionService.initializeStore = () => Promise.resolve(); + + await autoSelectionService.autoSelectInterpreter(resource); + + await autoSelectionService.autoSelectInterpreter(resource); + + verify(interpreterService.getInterpreters(resource)).once(); + sinon.assert.calledTwice(sendTelemetryEventStub); + expect(telemetryEvents).to.deep.equal( + [ + { + eventName: EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + properties: { useCachedInterpreter: false }, + }, + { + eventName: EventName.PYTHON_INTERPRETER_AUTO_SELECTION, + properties: { useCachedInterpreter: true }, + }, + ], + 'Telemetry event properties are different', + ); + }); }); - test('Run userDefineInterpreter as the first rule', async () => { - let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); - autoSelectionService.initializeStore = () => Promise.resolve(); - await autoSelectionService.autoSelectInterpreter(undefined); - - expect(eventFired).to.deep.equal(true, 'event not fired'); - verify(userDefinedInterpreter.autoSelectInterpreter(undefined, autoSelectionService)).once(); - }); test('Initialize the store', async () => { + const queryState = mock(PersistentState) as PersistentState<boolean | undefined>; + + when(queryState.value).thenReturn(undefined); + when(stateFactory.createWorkspacePersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + when(queryState.value).thenReturn(undefined); + when(stateFactory.createGlobalPersistentState<boolean | undefined>(anyString(), undefined)).thenReturn( + instance(queryState), + ); + let initialize = false; let eventFired = false; - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); - autoSelectionService.initializeStore = async () => initialize = true as any; + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); + autoSelectionService.initializeStore = async () => { + initialize = true; + }; await autoSelectionService.autoSelectInterpreter(undefined); expect(eventFired).to.deep.equal(true, 'event not fired'); expect(initialize).to.be.equal(true, 'Not invoked'); }); + test('Initializing the store would be executed once', async () => { - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); await autoSelectionService.initializeStore(undefined); await autoSelectionService.initializeStore(undefined); await autoSelectionService.initializeStore(undefined); - verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + 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<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(false); await autoSelectionService.initializeStore(undefined); - verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).twice(); verify(state.value).atLeast(1); verify(fs.fileExists(pythonPath)).once(); verify(state.updateValue(undefined)).once(); }); + test('Should not clear file stored in cache if it does exist', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(state.value).thenReturn(interpreterInfo); when(fs.fileExists(pythonPath)).thenResolve(true); await autoSelectionService.initializeStore(undefined); - verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).once(); + verify(stateFactory.createGlobalPersistentState(preferredGlobalInterpreter, undefined)).twice(); verify(state.value).atLeast(1); verify(fs.fileExists(pythonPath)).once(); verify(state.updateValue(undefined)).never(); }); + test('Store interpreter info in state store when resource is undefined', async () => { let eventFired = false; const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); await autoSelectionService.initializeStore(undefined); await autoSelectionService.storeAutoSelectedInterpreter(undefined, interpreterInfo); @@ -180,14 +510,22 @@ suite('Interpreters - Auto Selection', () => { expect(selectedInterpreter).to.deep.equal(interpreterInfo); 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<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(state.value).thenReturn(interpreterInfoInState); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -199,14 +537,22 @@ suite('Interpreters - Auto Selection', () => { expect(selectedInterpreter).to.deep.equal(interpreterInfoInState); expect(eventFired).to.deep.equal(false, 'event fired'); }); + test('Store global interpreter info in state store when resource is undefined and version is higher than one already in state', async () => { let eventFired = false; const pythonPath = 'Hello World'; 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<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - autoSelectionService.onDidChangeAutoSelectedInterpreter(() => eventFired = true); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); + autoSelectionService.onDidChangeAutoSelectedInterpreter(() => { + eventFired = true; + }); when(state.value).thenReturn(interpreterInfoInState); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -218,10 +564,16 @@ suite('Interpreters - Auto Selection', () => { expect(selectedInterpreter).to.deep.equal(interpreterInfo); expect(eventFired).to.deep.equal(false, 'event fired'); }); + test('Store global interpreter info in state store', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); await autoSelectionService.initializeStore(undefined); @@ -231,14 +583,22 @@ suite('Interpreters - Auto Selection', () => { verify(state.updateValue(interpreterInfo)).once(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); }); + test('Store interpreter info in state store when resource is defined', async () => { let eventFired = false; const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + 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(undefined); @@ -249,26 +609,17 @@ suite('Interpreters - Auto Selection', () => { expect(selectedInterpreter).to.deep.equal(interpreterInfo); expect(eventFired).to.deep.equal(false, 'event fired'); }); - test('Storing workspace interpreter info in state store should fail', async () => { - const pythonPath = 'Hello World'; - const interpreterInfo = { path: pythonPath } as any; - const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); - when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); - when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); - - await autoSelectionService.initializeStore(undefined); - await autoSelectionService.setWorkspaceInterpreter(resource, interpreterInfo); - const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(resource); - verify(state.updateValue(interpreterInfo)).never(); - expect(selectedInterpreter ? selectedInterpreter : undefined).to.deep.equal(undefined, 'not undefined'); - }); test('Store workspace interpreter info in state store', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); when(workspaceService.getWorkspaceFolderIdentifier(anything(), anything())).thenReturn(''); const deferred = createDeferred<void>(); @@ -282,11 +633,17 @@ suite('Interpreters - Auto Selection', () => { verify(state.updateValue(interpreterInfo)).once(); expect(selectedInterpreter).to.deep.equal(interpreterInfo); }); + test('Return undefined when we do not have a global value', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); when(workspaceService.getWorkspaceFolder(resource)).thenReturn({ name: '', index: 0, uri: resource }); when(workspaceService.getWorkspaceFolderIdentifier(undefined, anything())).thenReturn(''); @@ -297,11 +654,17 @@ suite('Interpreters - Auto Selection', () => { verify(state.updateValue(interpreterInfo)).never(); expect(selectedInterpreter === null || selectedInterpreter === undefined).to.equal(true, 'Should be undefined'); }); + test('Return global value if we do not have a matching value for the resource', async () => { const pythonPath = 'Hello World'; const interpreterInfo = { path: pythonPath } as any; const resource = Uri.parse('one'); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(preferredGlobalInterpreter, undefined)).thenReturn(instance(state)); + when( + stateFactory.createGlobalPersistentState<PythonEnvironment | undefined>( + preferredGlobalInterpreter, + undefined, + ), + ).thenReturn(instance(state)); const globalInterpreterInfo = { path: 'global Value' }; when(state.value).thenReturn(globalInterpreterInfo as any); when(workspaceService.getWorkspaceFolderIdentifier(resource, anything())).thenReturn('1'); @@ -313,7 +676,9 @@ suite('Interpreters - Auto Selection', () => { await autoSelectionService.storeAutoSelectedInterpreter(resource, interpreterInfo); const anotherResourceOfAnotherWorkspace = Uri.parse('Some other workspace'); - when(workspaceService.getWorkspaceFolderIdentifier(anotherResourceOfAnotherWorkspace, anything())).thenReturn('2'); + when(workspaceService.getWorkspaceFolderIdentifier(anotherResourceOfAnotherWorkspace, anything())).thenReturn( + '2', + ); const selectedInterpreter = autoSelectionService.getAutoSelectedInterpreter(anotherResourceOfAnotherWorkspace); 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<void>(); - constructor(private readonly pythonPath: string = '') { } + + constructor(private readonly pythonPath: string = '') {} + public get onDidChangeAutoSelectedInterpreter(): Event<void> { return this.eventEmitter.event; } - public getAutoSelectedInterpreter(_resource: Uri): PythonInterpreter { + + public getAutoSelectedInterpreter(): PythonEnvironment { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return { path: this.pythonPath } as any; } - public async setWorkspaceInterpreter(_resource: Uri, _interpreter: PythonInterpreter | undefined): Promise<void>{ - return; + + // eslint-disable-next-line class-methods-use-this + public async setWorkspaceInterpreter(): Promise<void> { + return Promise.resolve(); } } - let proxy: InterpreterAutoSeletionProxyService; + let proxy: InterpreterAutoSelectionProxyService; setup(() => { - proxy = new InterpreterAutoSeletionProxyService([] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + proxy = new InterpreterAutoSelectionProxyService([] as any); }); - test('Change evnet is fired', () => { + test('Change event is fired', () => { const obj = new InstanceClass(); proxy.registerInstance(obj); let eventRaised = false; - proxy.onDidChangeAutoSelectedInterpreter(() => eventRaised = true); + proxy.onDidChangeAutoSelectedInterpreter(() => { + eventRaised = true; + }); proxy.registerInstance(obj); obj.eventEmitter.fire(); @@ -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<PythonInterpreter | undefined>; - class BaseRuleServiceTest extends BaseRuleService { - public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void> { - return super.next(resource, manager); - } - public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { - return super.cacheSelectedInterpreter(resource, interpreter); - } - public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise<boolean> { - return super.setGlobalInterpreter(interpreter, manager); - } - protected async onAutoSelectInterpreter(_resource: Uri, _manager?: IInterpreterAutoSelectionService): Promise<NextAction> { - return NextAction.runNextRule; - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(anything(), undefined)).thenReturn(instance(state)); - rule = new BaseRuleServiceTest(AutoSelectionRule.cachedInterpreters, instance(fs), instance(stateFactory)); - }); - - test('State store is created', () => { - verify(stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, undefined)).once(); - }); - test('Next rule should be invoked', async () => { - const nextRule = mock(CurrentPathInterpretersAutoSelectionRule); - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.parse('x'); - - rule.setNextRule(instance(nextRule)); - await rule.next(resource, manager); - - verify(stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, undefined)).once(); - verify(nextRule.autoSelectInterpreter(resource, manager)).once(); - }); - test('Next rule should not be invoked', async () => { - const nextRule = mock(CurrentPathInterpretersAutoSelectionRule); - const resource = Uri.parse('x'); - - rule.setNextRule(instance(nextRule)); - await rule.next(resource); - - verify(stateFactory.createGlobalPersistentState(`InterpreterAutoSeletionRule-${AutoSelectionRule.cachedInterpreters}`, undefined)).once(); - verify(nextRule.autoSelectInterpreter(anything(), anything())).never(); - }); - test('State store must be updated', async () => { - const resource = Uri.parse('x'); - const interpreterInfo = { x: '1324' } as any; - when(state.updateValue(anything())).thenResolve(); - - await rule.cacheSelectedInterpreter(resource, interpreterInfo); - - verify(state.updateValue(interpreterInfo)).once(); - }); - test('State store must be cleared when file does not exist', async () => { - const resource = Uri.parse('x'); - const interpreterInfo = { path: '1324' } as any; - when(state.value).thenReturn(interpreterInfo); - when(state.updateValue(anything())).thenResolve(); - when(fs.fileExists(interpreterInfo.path)).thenResolve(false); - - await rule.autoSelectInterpreter(resource); - - verify(state.value).atLeast(1); - verify(state.updateValue(undefined)).once(); - verify(fs.fileExists(interpreterInfo.path)).once(); - }); - test('State store must not be cleared when file exists', async () => { - const resource = Uri.parse('x'); - const interpreterInfo = { path: '1324' } as any; - when(state.value).thenReturn(interpreterInfo); - when(state.updateValue(anything())).thenResolve(); - when(fs.fileExists(interpreterInfo.path)).thenResolve(true); - - await rule.autoSelectInterpreter(resource); - - verify(state.value).atLeast(1); - verify(state.updateValue(anything())).never(); - verify(fs.fileExists(interpreterInfo.path)).once(); - }); - test('Get undefined if there\'s nothing in state store', async () => { - when(state.value).thenReturn(undefined); - - expect(rule.getPreviouslyAutoSelectedInterpreter(Uri.parse('x'))).to.be.equal(undefined, 'Must be undefined'); - - verify(state.value).atLeast(1); - }); - test('Get value from state store', async () => { - const stateStoreValue = 'x'; - when(state.value).thenReturn(stateStoreValue as any); - - expect(rule.getPreviouslyAutoSelectedInterpreter(Uri.parse('x'))).to.be.equal(stateStoreValue); - - verify(state.value).atLeast(1); - }); - test('setGlobalInterpreter should do nothing if 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 db2dbb9df1d2..000000000000 --- a/src/test/interpreters/autoSelection/rules/cached.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 { 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<PythonInterpreter | undefined>; - let systemInterpreter: IInterpreterAutoSelectionRule; - let currentPathInterpreter: IInterpreterAutoSelectionRule; - let winRegInterpreter: IInterpreterAutoSelectionRule; - let helper: IInterpreterHelper; - class CachedInterpretersAutoSelectionRuleTest extends CachedInterpretersAutoSelectionRule { - public readonly rules!: IInterpreterAutoSelectionRule[]; - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - manager?: IInterpreterAutoSelectionService - ): Promise<boolean> { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter( - resource: Resource, - manager?: IInterpreterAutoSelectionService - ): Promise<NextAction> { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - systemInterpreter = mock(SystemWideInterpretersAutoSelectionRule); - currentPathInterpreter = mock(SystemWideInterpretersAutoSelectionRule); - winRegInterpreter = mock(SystemWideInterpretersAutoSelectionRule); - - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(anything(), undefined)).thenReturn( - instance(state) - ); - rule = new CachedInterpretersAutoSelectionRuleTest( - instance(fs), - instance(helper), - instance(stateFactory), - instance(systemInterpreter), - instance(currentPathInterpreter), - instance(winRegInterpreter) - ); - }); - - test('Invoke next rule if there are no cached interpreters', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - - when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); - when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); - when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).thenReturn(undefined); - - const nextAction = await rule.onAutoSelectInterpreter(resource, manager); - - verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); - verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); - verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(resource)).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - }); - test('Invoke next rule if fails to update global state', async () => { - const manager = mock(InterpreterAutoSelectionService); - const winRegInterpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - const resource = Uri.file('x'); - - when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(winRegInterpreterInfo); - when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); - when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); - when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(winRegInterpreterInfo); - - const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); - moq.callBase = true; - moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(typemoq.Times.once()); - - const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); - - verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); - verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); - verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); - moq.verifyAll(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - }); - test('Must not Invoke next rule if updating global state is successful', async () => { - const manager = mock(InterpreterAutoSelectionService); - const winRegInterpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - const resource = Uri.file('x'); - - when(helper.getBestInterpreter(deepEqual(anything()))).thenReturn(winRegInterpreterInfo); - when(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); - when(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(undefined); - when(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).thenReturn(winRegInterpreterInfo); - - const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); - moq.callBase = true; - moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - - const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); - - verify(systemInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); - verify(currentPathInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); - verify(winRegInterpreter.getPreviouslyAutoSelectedInterpreter(anything())).once(); - moq.verifyAll(); - expect(nextAction).to.be.equal(NextAction.exit); - }); -}); diff --git a/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts b/src/test/interpreters/autoSelection/rules/currentPath.unit.test.ts deleted file mode 100644 index 6fb84d62bcfe..000000000000 --- a/src/test/interpreters/autoSelection/rules/currentPath.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-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<PythonInterpreter | undefined>; - let locator: IInterpreterLocatorService; - let helper: IInterpreterHelper; - class CurrentPathInterpretersAutoSelectionRuleTest extends CurrentPathInterpretersAutoSelectionRule { - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - manager?: IInterpreterAutoSelectionService - ): Promise<boolean> { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter( - resource: Resource, - manager?: IInterpreterAutoSelectionService - ): Promise<NextAction> { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - locator = mock(KnownPathsService); - - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(anything(), undefined)).thenReturn( - instance(state) - ); - rule = new CurrentPathInterpretersAutoSelectionRuleTest( - instance(fs), - instance(helper), - instance(stateFactory), - instance(locator) - ); - }); - - test('Invoke next rule if there are no interpreters in the current path', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - - when(locator.getInterpreters(resource)).thenResolve([]); - - const nextAction = await rule.onAutoSelectInterpreter(resource, manager); - - verify(locator.getInterpreters(resource)).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - }); - test('Invoke next rule if fails to update global state', async () => { - const manager = mock(InterpreterAutoSelectionService); - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - const resource = Uri.file('x'); - - when(helper.getBestInterpreter(anything())).thenReturn(interpreterInfo); - when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); - - const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); - moq.callBase = true; - moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(false)) - .verifiable(typemoq.Times.once()); - - const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); - - moq.verifyAll(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - }); - test('Not Invoke next rule if succeeds to update global state', async () => { - const manager = mock(InterpreterAutoSelectionService); - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - const resource = Uri.file('x'); - - when(helper.getBestInterpreter(anything())).thenReturn(interpreterInfo); - when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); - - const moq = typemoq.Mock.ofInstance(rule, typemoq.MockBehavior.Loose, true); - moq.callBase = true; - moq.setup(m => m.setGlobalInterpreter(typemoq.It.isAny(), typemoq.It.isAny())) - .returns(() => Promise.resolve(true)) - .verifiable(typemoq.Times.once()); - - const nextAction = await moq.object.onAutoSelectInterpreter(resource, manager); - - moq.verifyAll(); - expect(nextAction).to.be.equal(NextAction.exit); - }); -}); diff --git a/src/test/interpreters/autoSelection/rules/settings.unit.test.ts b/src/test/interpreters/autoSelection/rules/settings.unit.test.ts deleted file mode 100644 index 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<PythonInterpreter | undefined>; - let workspaceService: IWorkspaceService; - class SettingsInterpretersAutoSelectionRuleTest extends SettingsInterpretersAutoSelectionRule { - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<NextAction> { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - workspaceService = mock(WorkspaceService); - - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(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 d5ccb4173cd4..000000000000 --- a/src/test/interpreters/autoSelection/rules/system.unit.test.ts +++ /dev/null @@ -1,120 +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<PythonInterpreter | undefined>; - let interpreterService: IInterpreterService; - let helper: IInterpreterHelper; - class SystemWideInterpretersAutoSelectionRuleTest extends SystemWideInterpretersAutoSelectionRule { - public async setGlobalInterpreter( - interpreter?: PythonInterpreter, - manager?: IInterpreterAutoSelectionService - ): Promise<boolean> { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter( - resource: Resource, - manager?: IInterpreterAutoSelectionService - ): Promise<NextAction> { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - interpreterService = mock(InterpreterService); - - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(anything(), undefined)).thenReturn( - instance(state) - ); - rule = new SystemWideInterpretersAutoSelectionRuleTest( - instance(fs), - instance(helper), - instance(stateFactory), - instance(interpreterService) - ); - }); - - test('Invoke next rule if there are no interpreters in the current path', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - let setGlobalInterpreterInvoked = false; - when(interpreterService.getInterpreters(resource)).thenResolve([]); - when(helper.getBestInterpreter(deepEqual([]))).thenReturn(undefined); - rule.setGlobalInterpreter = async (res: any) => { - setGlobalInterpreterInvoked = true; - assert.equal(res, undefined); - return Promise.resolve(false); - }; - - const nextAction = await rule.onAutoSelectInterpreter(resource, manager); - - verify(interpreterService.getInterpreters(resource)).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); - }); - test('Invoke next rule if there interpreters in the current path but update fails', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - let setGlobalInterpreterInvoked = false; - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - when(interpreterService.getInterpreters(resource)).thenResolve([interpreterInfo]); - when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); - rule.setGlobalInterpreter = async (res: any) => { - setGlobalInterpreterInvoked = true; - expect(res).deep.equal(interpreterInfo); - return Promise.resolve(false); - }; - - const nextAction = await rule.onAutoSelectInterpreter(resource, manager); - - verify(interpreterService.getInterpreters(resource)).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); - }); - test('Do not Invoke next rule if there interpreters in the current path and update does not fail', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - let setGlobalInterpreterInvoked = false; - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - when(interpreterService.getInterpreters(resource)).thenResolve([interpreterInfo]); - when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); - rule.setGlobalInterpreter = async (res: any) => { - setGlobalInterpreterInvoked = true; - expect(res).deep.equal(interpreterInfo); - return Promise.resolve(true); - }; - - const nextAction = await rule.onAutoSelectInterpreter(resource, manager); - - verify(interpreterService.getInterpreters(resource)).once(); - expect(nextAction).to.be.equal(NextAction.exit); - expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); - }); -}); diff --git a/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts b/src/test/interpreters/autoSelection/rules/winRegistry.unit.test.ts deleted file mode 100644 index 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<PythonInterpreter | undefined>; - let locator: IInterpreterLocatorService; - let platform: IPlatformService; - let helper: IInterpreterHelper; - class WindowsRegistryInterpretersAutoSelectionRuleTest extends WindowsRegistryInterpretersAutoSelectionRule { - public async setGlobalInterpreter(interpreter?: PythonInterpreter, manager?: IInterpreterAutoSelectionService): Promise<boolean> { - return super.setGlobalInterpreter(interpreter, manager); - } - public async onAutoSelectInterpreter(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<NextAction> { - return super.onAutoSelectInterpreter(resource, manager); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - locator = mock(WindowsRegistryService); - platform = mock(PlatformService); - - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(anything(), undefined)).thenReturn(instance(state)); - rule = new WindowsRegistryInterpretersAutoSelectionRuleTest(instance(fs), instance(helper), - instance(stateFactory), instance(platform), instance(locator)); - }); - - getNamesAndValues<OSType>(OSType).forEach(osType => { - test(`Invoke next rule if platform is not windows (${osType.name})`, async function () { - const manager = mock(InterpreterAutoSelectionService); - if (osType.value === OSType.Windows) { - return this.skip(); - } - const resource = Uri.file('x'); - when(platform.osType).thenReturn(osType.value); - - const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); - - verify(platform.osType).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - }); - }); - test('Invoke next rule if there are no interpreters in the registry', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - let setGlobalInterpreterInvoked = false; - when(platform.osType).thenReturn(OSType.Windows); - when(locator.getInterpreters(resource)).thenResolve([]); - when(helper.getBestInterpreter(deepEqual([]))).thenReturn(undefined); - rule.setGlobalInterpreter = async (res: any) => { - setGlobalInterpreterInvoked = true; - assert.equal(res, undefined); - return Promise.resolve(false); - }; - - const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); - - verify(locator.getInterpreters(resource)).once(); - verify(platform.osType).once(); - verify(helper.getBestInterpreter(deepEqual([]))).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); - }); - test('Invoke next rule if there are interpreters in the registry and update fails', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - let setGlobalInterpreterInvoked = false; - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - when(platform.osType).thenReturn(OSType.Windows); - when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); - when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); - rule.setGlobalInterpreter = async (res: any) => { - setGlobalInterpreterInvoked = true; - expect(res).to.deep.equal(interpreterInfo); - return Promise.resolve(false); - }; - - const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); - - verify(locator.getInterpreters(resource)).once(); - verify(platform.osType).once(); - verify(helper.getBestInterpreter(deepEqual([interpreterInfo]))).once(); - expect(nextAction).to.be.equal(NextAction.runNextRule); - expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); - }); - test('Do not Invoke next rule if there are interpreters in the registry and update does not fail', async () => { - const manager = mock(InterpreterAutoSelectionService); - const resource = Uri.file('x'); - let setGlobalInterpreterInvoked = false; - const interpreterInfo = { path: '1', version: new SemVer('1.0.0') } as any; - when(platform.osType).thenReturn(OSType.Windows); - when(locator.getInterpreters(resource)).thenResolve([interpreterInfo]); - when(helper.getBestInterpreter(deepEqual([interpreterInfo]))).thenReturn(interpreterInfo); - rule.setGlobalInterpreter = async (res: any) => { - setGlobalInterpreterInvoked = true; - expect(res).to.deep.equal(interpreterInfo); - return Promise.resolve(true); - }; - - const nextAction = await rule.onAutoSelectInterpreter(resource, instance(manager)); - - verify(locator.getInterpreters(resource)).once(); - verify(platform.osType).once(); - verify(helper.getBestInterpreter(deepEqual([interpreterInfo]))).once(); - expect(nextAction).to.be.equal(NextAction.exit); - expect(setGlobalInterpreterInvoked).to.be.equal(true, 'setGlobalInterpreter not invoked'); - }); -}); diff --git a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts b/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts deleted file mode 100644 index 76edd0284207..000000000000 --- a/src/test/interpreters/autoSelection/rules/workspaceEnv.unit.test.ts +++ /dev/null @@ -1,412 +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<PythonInterpreter | undefined>; - 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<boolean> { - return super.setGlobalInterpreter(interpreter, manager); - } - public async next(resource: Resource, manager?: IInterpreterAutoSelectionService): Promise<void> { - return super.next(resource, manager); - } - public async cacheSelectedInterpreter(resource: Resource, interpreter: PythonInterpreter | undefined) { - return super.cacheSelectedInterpreter(resource, interpreter); - } - public async getWorkspaceVirtualEnvInterpreters(resource: Resource): Promise<PythonInterpreter[] | undefined> { - return super.getWorkspaceVirtualEnvInterpreters(resource); - } - } - setup(() => { - stateFactory = mock(PersistentStateFactory); - state = mock(PersistentState); - fs = mock(FileSystem); - helper = mock(InterpreterHelper); - platform = mock(PlatformService); - pipEnvLocator = mock(KnownPathsService); - workspaceService = mock(WorkspaceService); - virtualEnvLocator = mock(KnownPathsService); - pythonPathUpdaterService = mock(PythonPathUpdaterService); - - when(stateFactory.createGlobalPersistentState<PythonInterpreter | undefined>(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<PythonPathInConfig>(); - const pythonPathValue = 'Hello there.exe'; - pythonPathInConfig - .setup(p => p.workspaceFolderValue) - .returns(() => pythonPathValue) - .verifiable(typemoq.Times.once()); - - const pythonPath = { inspect: () => pythonPathInConfig.object }; - const folderUri = Uri.parse('Folder'); - const someUri = Uri.parse('somethign'); - - rule.setNextRule(nextRule); - when(platform.osType).thenReturn(OSType.OSX); - when(helper.getActiveWorkspaceUri(anything())).thenReturn({ folderUri } as any); - when(nextRule.autoSelectInterpreter(someUri, manager)).thenResolve(); - when(workspaceService.getConfiguration('python', folderUri)).thenReturn(pythonPath as any); - - rule.setNextRule(instance(nextRule)); - await rule.autoSelectInterpreter(someUri, manager); - - verify(nextRule.autoSelectInterpreter(someUri, manager)).once(); - verify(helper.getActiveWorkspaceUri(anything())).once(); - pythonPathInConfig.verifyAll(); - }); - test('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<PythonPathInConfig>(); - 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<PythonInterpreter[]>(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; - when(pipEnvLocator.getInterpreters(folderUri, true)).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<PythonPathInConfig>(); - 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<PythonInterpreter[]>(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([interpreterInfo]); - when(pipEnvLocator.getInterpreters(folderUri, true)).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 interpreters', async () => { - const folderUri = Uri.parse('Folder'); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType<PythonPathInConfig>(); - 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<PythonInterpreter[]>(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => virtualEnvPromise.promise; - when(pipEnvLocator.getInterpreters(folderUri, true)).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 interpreters', async () => { - const folderUri = Uri.parse('Folder'); - type PythonPathInConfig = { workspaceFolderValue: string }; - const pythonPathInConfig = typemoq.Mock.ofType<PythonPathInConfig>(); - 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<PythonInterpreter[]>(); - const nextInvoked = createDeferred(); - rule.next = () => Promise.resolve(nextInvoked.resolve()); - rule.getWorkspaceVirtualEnvInterpreters = () => Promise.resolve([]); - when(pipEnvLocator.getInterpreters(folderUri, true)).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<ILogger>; - let condaService: TypeMoq.IMock<ICondaService>; - let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; - let condaFileProvider: IInterpreterLocatorService; - let fileSystem: TypeMoq.IMock<IFileSystem>; - setup(() => { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); - const state = new MockState(undefined); - stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); - - condaService = TypeMoq.Mock.ofType<ICondaService>(); - interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - logger = TypeMoq.Mock.ofType<ILogger>(); - 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 ac813b10fedd..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 '../testing/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<ILogger>; - let condaProvider: CondaEnvService; - let condaService: TypeMoq.IMock<ICondaService>; - let interpreterHelper: TypeMoq.IMock<InterpreterHelper>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - setup(() => { - initializeDI(); - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => stateFactory.object); - const state = new MockState(undefined); - stateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => state); - - condaService = TypeMoq.Mock.ofType<ICondaService>(); - interpreterHelper = TypeMoq.Mock.ofType<InterpreterHelper>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - condaProvider = new CondaEnvService(condaService.object, interpreterHelper.object, logger.object, serviceContainer.object, fileSystem.object); - }); - teardown(() => ioc.dispose()); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - logger = TypeMoq.Mock.ofType<ILogger>(); - } - - 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 cfe953fc6e6f..000000000000 --- a/src/test/interpreters/condaService.unit.test.ts +++ /dev/null @@ -1,721 +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 { 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<IProcessService>; - let platformService: TypeMoq.IMock<IPlatformService>; - let condaService: CondaService; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let config: TypeMoq.IMock<IConfigurationService>; - let settings: TypeMoq.IMock<IPythonSettings>; - let registryInterpreterLocatorService: TypeMoq.IMock<IInterpreterLocatorService>; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; - let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; - let logger: TypeMoq.IMock<ILogger>; - let condaPathSetting: string; - let disposableRegistry: Disposable[]; - let interpreterService: TypeMoq.IMock<IInterpreterService>; - let workspaceService : TypeMoq.IMock<IWorkspaceService>; - let mockState: MockState; - let terminalProvider: TypeMoq.IMock<ITerminalActivationCommandProvider>; - setup(async () => { - condaPathSetting = ''; - logger = TypeMoq.Mock.ofType<ILogger>(); - processService = TypeMoq.Mock.ofType<IProcessService>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - registryInterpreterLocatorService = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - config = TypeMoq.Mock.ofType<IConfigurationService>(); - settings = TypeMoq.Mock.ofType<IPythonSettings>(); - procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - processService.setup((x: any) => x.then).returns(() => undefined); - procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - disposableRegistry = []; - const e = new EventEmitter<void>(); - interpreterService.setup(x => x.onDidChangeInterpreter).returns(() => e.event); - resetMockState(undefined); - persistentStateFactory.setup(s => s.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => mockState); - - terminalProvider = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - terminalProvider.setup(p => p.isShellSupported(TypeMoq.It.isAny())).returns(() => true); - terminalProvider.setup(p => p.getActivationCommands(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(['activate'])); - terminalProvider.setup(p => p.getActivationCommandsForInterpreter!(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(['activate'])); - - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(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, - disposableRegistry, - workspaceService.object, - registryInterpreterLocatorService.object); - - }); - - function resetMockState(data: any) { - mockState = new MockState(data); - } - - async function identifyPythonPathAsCondaEnvironment(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string) { - platformService.setup(p => p.isLinux).returns(() => isLinux); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - - const isCondaEnv = await condaService.isCondaEnvironment(pythonPath); - expect(isCondaEnv).to.be.equal(true, 'Path not identified as a conda path'); - } - - test('Correctly identifies a python path as a conda environment (windows)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); - await identifyPythonPathAsCondaEnvironment(true, false, false, pythonPath); - }); - - test('Correctly identifies a python path as a conda environment (linux)', async () => { - const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await identifyPythonPathAsCondaEnvironment(false, false, true, pythonPath); - }); - - test('Correctly identifies a python path as a conda environment (osx)', async () => { - const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await identifyPythonPathAsCondaEnvironment(false, true, false, pythonPath); - }); - - async function identifyPythonPathAsNonCondaEnvironment(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string) { - platformService.setup(p => p.isLinux).returns(() => isLinux); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(false)); - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(false)); - - const isCondaEnv = await condaService.isCondaEnvironment(pythonPath); - expect(isCondaEnv).to.be.equal(false, 'Path incorrectly identified as a conda path'); - } - - test('Correctly identifies a python path as a non-conda environment (windows)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'enva', 'python.exe'); - await identifyPythonPathAsNonCondaEnvironment(true, false, false, pythonPath); - }); - - test('Correctly identifies a python path as a non-conda environment (linux)', async () => { - const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - await identifyPythonPathAsNonCondaEnvironment(false, false, true, pythonPath); - }); - - test('Correctly identifies a python path as a non-conda environment (osx)', async () => { - const pythonPath = path.join('users', 'xyz', '.conda', 'envs', 'enva', 'bin', 'python'); - await identifyPythonPathAsNonCondaEnvironment(false, true, false, pythonPath); - }); - - async function checkCondaNameAndPathForCondaEnvironments(isWindows: boolean, isOsx: boolean, isLinux: boolean, pythonPath: string, condaEnvsPath: string, expectedCondaEnv?: { name: string; path: string }) { - const condaEnvironments = [ - { name: 'One', path: path.join(condaEnvsPath, 'one') }, - { name: 'Three', path: path.join(condaEnvsPath, 'three') }, - { name: 'Seven', path: path.join(condaEnvsPath, 'seven') }, - { name: 'Eight', path: path.join(condaEnvsPath, 'Eight 8') }, - { name: 'nine 9', path: path.join(condaEnvsPath, 'nine 9') } - ]; - - platformService.setup(p => p.isLinux).returns(() => isLinux); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.isMac).returns(() => isOsx); - - resetMockState({ data: condaEnvironments }); - - const condaEnv = await condaService.getCondaEnvironment(pythonPath); - expect(condaEnv).deep.equal(expectedCondaEnv, 'Conda environment not identified'); - } - - test('Correctly retrieves conda environment (windows)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'python.exe'); - const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); - await checkCondaNameAndPathForCondaEnvironments(true, false, false, pythonPath, condaEnvDir, { name: 'One', path: path.dirname(pythonPath) }); - }); - - test('Correctly retrieves conda environment with spaces in env name (windows)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'eight 8', 'python.exe'); - const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); - await checkCondaNameAndPathForCondaEnvironments(true, false, false, pythonPath, condaEnvDir, { name: 'Eight', path: path.dirname(pythonPath) }); - }); - - test('Correctly retrieves conda environment (osx)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'bin', 'python'); - const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await checkCondaNameAndPathForCondaEnvironments(false, true, false, pythonPath, condaEnvDir, { name: 'One', path: path.join(path.dirname(pythonPath), '..') }); - }); - - test('Correctly retrieves conda environment with spaces in env name (osx)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'Eight 8', 'bin', 'python'); - const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await checkCondaNameAndPathForCondaEnvironments(false, true, false, pythonPath, condaEnvDir, { name: 'Eight', path: path.join(path.dirname(pythonPath), '..') }); - }); - - test('Correctly retrieves conda environment (linux)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'one', 'bin', 'python'); - const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await checkCondaNameAndPathForCondaEnvironments(false, false, true, pythonPath, condaEnvDir, { name: 'One', path: path.join(path.dirname(pythonPath), '..') }); - }); - - test('Correctly retrieves conda environment with spaces in env name (linux)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'Eight 8', 'bin', 'python'); - const condaEnvDir = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), '..', 'conda-meta')))).returns(() => Promise.resolve(true)); - await checkCondaNameAndPathForCondaEnvironments(false, false, true, pythonPath, condaEnvDir, { name: 'Eight', path: path.join(path.dirname(pythonPath), '..') }); - }); - - test('Ignore cache if environment is not found in the cache (conda env is detected second time round)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'newEnvironment', 'python.exe'); - const condaEnvsPath = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - const condaEnvironments = [ - { name: 'One', path: path.join(condaEnvsPath, 'one') }, - { name: 'Three', path: path.join(condaEnvsPath, 'three') }, - { name: 'Seven', path: path.join(condaEnvsPath, 'seven') }, - { name: 'Eight', path: path.join(condaEnvsPath, 'Eight 8') }, - { name: 'nine 9', path: path.join(condaEnvsPath, 'nine 9') } - ]; - - platformService.setup(p => p.isLinux).returns(() => false); - platformService.setup(p => p.isWindows).returns(() => true); - platformService.setup(p => p.isMac).returns(() => false); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); - resetMockState({ data: condaEnvironments }); - - const envList = ['# conda environments:', - '#', - 'base * /Users/donjayamanne/anaconda3', - 'one /Users/donjayamanne/anaconda3/envs/one', - 'one two /Users/donjayamanne/anaconda3/envs/one two', - 'py27 /Users/donjayamanne/anaconda3/envs/py27', - 'py36 /Users/donjayamanne/anaconda3/envs/py36', - 'three /Users/donjayamanne/anaconda3/envs/three', - `newEnvironment ${path.join(condaEnvsPath, 'newEnvironment')}` - ]; - - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: envList.join(EOL) })); - - const condaEnv = await condaService.getCondaEnvironment(pythonPath); - expect(condaEnv).deep.equal({ name: 'newEnvironment', path: path.dirname(pythonPath) }, 'Conda environment not identified after ignoring cache'); - expect(mockState.data.data).lengthOf(7, 'Incorrect number of items in the cache'); - }); - - test('Ignore cache if environment is not found in the cache (cond env is not detected in conda env list)', async () => { - const pythonPath = path.join('c', 'users', 'xyz', '.conda', 'envs', 'newEnvironment', 'python.exe'); - const condaEnvsPath = path.join('c', 'users', 'xyz', '.conda', 'envs'); - - const condaEnvironments = [ - { name: 'One', path: path.join(condaEnvsPath, 'one') }, - { name: 'Three', path: path.join(condaEnvsPath, 'three') }, - { name: 'Seven', path: path.join(condaEnvsPath, 'seven') }, - { name: 'Eight', path: path.join(condaEnvsPath, 'Eight 8') }, - { name: 'nine 9', path: path.join(condaEnvsPath, 'nine 9') } - ]; - - platformService.setup(p => p.isLinux).returns(() => false); - platformService.setup(p => p.isWindows).returns(() => true); - platformService.setup(p => p.isMac).returns(() => false); - - fileSystem.setup(f => f.directoryExists(TypeMoq.It.isValue(path.join(path.dirname(pythonPath), 'conda-meta')))).returns(() => Promise.resolve(true)); - resetMockState({ data: condaEnvironments }); - - const envList = ['# conda environments:', - '#', - 'base * /Users/donjayamanne/anaconda3', - 'one /Users/donjayamanne/anaconda3/envs/one', - 'one two /Users/donjayamanne/anaconda3/envs/one two', - 'py27 /Users/donjayamanne/anaconda3/envs/py27', - 'py36 /Users/donjayamanne/anaconda3/envs/py36', - 'three /Users/donjayamanne/anaconda3/envs/three' - ]; - - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: envList.join(EOL) })); - - const condaEnv = await condaService.getCondaEnvironment(pythonPath); - expect(condaEnv).deep.equal(undefined, 'Conda environment incorrectly identified after ignoring cache'); - expect(mockState.data.data).lengthOf(6, 'Incorrect number of items in the cache'); - }); - - test('Must use Conda env from Registry to locate conda.exe', async () => { - const condaPythonExePath = path.join('dumyPath', 'environments', 'conda', 'Scripts', 'python.exe'); - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: new SemVer('1.0.0'), type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: condaPythonExePath, companyDisplayName: 'Two 2', version: new SemVer('1.11.0'), type: InterpreterType.Conda }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: new SemVer('2.10.1'), type: InterpreterType.Unknown }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ].map(item => { - return { ...info, ...item }; - }); - const condaInterpreterIndex = registryInterpreters.findIndex(i => i.displayName === 'Anaconda'); - const expectedCodnaPath = path.join(path.dirname(registryInterpreters[condaInterpreterIndex].path), 'conda.exe'); - platformService.setup(p => p.isWindows).returns(() => true); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); - }); - - test('Must use Conda env from Registry to latest version of locate conda.exe', async () => { - const condaPythonExePath = path.join('dumyPath', 'environments'); - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: new SemVer('1.0.0'), type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), companyDisplayName: 'Two 1', version: new SemVer('1.11.0'), type: InterpreterType.Conda }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.11', version: new SemVer('2.11.0'), type: InterpreterType.Conda }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.31', version: new SemVer('2.31.0'), type: InterpreterType.Conda }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: new SemVer('2.21.0'), type: InterpreterType.Conda }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: new SemVer('2.10.1'), type: InterpreterType.Unknown }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ].map(item => { - return { ...info, ...item }; - }); - const indexOfLatestVersion = 3; - const expectedCodnaPath = path.join(path.dirname(registryInterpreters[indexOfLatestVersion].path), 'conda.exe'); - platformService.setup(p => p.isWindows).returns(() => true); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((file: string) => Promise.resolve(file === expectedCodnaPath)); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, expectedCodnaPath, 'Failed to identify conda.exe'); - }); - - test('Must use \'conda\' if conda.exe cannot be located using registry entries', async () => { - const condaPythonExePath = path.join('dumyPath', 'environments'); - const registryInterpreters: PythonInterpreter[] = [ - { displayName: 'One', path: path.join(environmentsPath, 'path1', 'one.exe'), companyDisplayName: 'One 1', version: new SemVer('1.0.0'), type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda1', 'Scripts', 'python.exe'), companyDisplayName: 'Two 1', version: new SemVer('1.11.0'), type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda211', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.11', version: new SemVer('2.11.0'), type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda231', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.31', version: new SemVer('2.31.0'), type: InterpreterType.Unknown }, - { displayName: 'Anaconda', path: path.join(condaPythonExePath, 'conda221', 'Scripts', 'python.exe'), companyDisplayName: 'Two 2.21', version: new SemVer('2.21.0'), type: InterpreterType.Unknown }, - { displayName: 'Three', path: path.join(environmentsPath, 'path2', 'one.exe'), companyDisplayName: 'Three 3', version: new SemVer('2.10.1'), type: InterpreterType.Unknown }, - { displayName: 'Seven', path: path.join(environmentsPath, 'conda', 'envs', 'numpy'), companyDisplayName: 'Continuum Analytics, Inc.', type: InterpreterType.Unknown } - ].map(item => { return { ...info, ...item }; }); - platformService.setup(p => p.isWindows).returns(() => true); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - registryInterpreterLocatorService.setup(r => r.getInterpreters(TypeMoq.It.isAny())).returns(() => Promise.resolve(registryInterpreters)); - fileSystem.setup(fs => fs.search(TypeMoq.It.isAnyString())).returns(async () => []); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((_file: string) => Promise.resolve(false)); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); - }); - - test('Get conda file from default/known locations', async () => { - - const expected = 'C:/ProgramData/Miniconda2/Scripts/conda.exe'; - - platformService.setup(p => p.isWindows).returns(() => true); - - fileSystem.setup(f => f.search(TypeMoq.It.isAnyString())) - .returns(() => Promise.resolve([expected])); - const CondaServiceForTesting = class extends CondaService { - public async isCondaInCurrentPath() { return false; } - }; - const condaSrv = new CondaServiceForTesting( - procServiceFactory.object, - platformService.object, - fileSystem.object, - persistentStateFactory.object, - config.object, - logger.object, - disposableRegistry, - workspaceService.object); - - const result = await condaSrv.getCondaFile(); - expect(result).is.equal(expected); - }); - - test('Must use \'python.condaPath\' setting if set', async () => { - condaPathSetting = 'spam-spam-conda-spam-spam'; - // We ensure that conda would otherwise be found. - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))) - .returns(() => Promise.resolve({ stdout: 'xyz' })) - .verifiable(TypeMoq.Times.never()); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, 'spam-spam-conda-spam-spam', 'Failed to identify conda.exe'); - - // We should not try to call other unwanted methods. - processService.verifyAll(); - registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - test('Must use \'conda\' if is available in the current path', async () => { - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))).returns(() => Promise.resolve({ stdout: 'xyz' })); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); - - // We should not try to call other unwanted methods. - registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - test('Must invoke process only once to check if conda is in the current path', async () => { - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']))).returns(() => Promise.resolve({ stdout: 'xyz' })); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, 'conda', 'Failed to identify conda.exe'); - processService.verify(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - - // We should not try to call other unwanted methods. - registryInterpreterLocatorService.verify(r => r.getInterpreters(TypeMoq.It.isAny()), TypeMoq.Times.never()); - - await condaService.getCondaFile(); - processService.verify(p => p.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); - }); - - ['~/anaconda/bin/conda', '~/miniconda/bin/conda', '~/anaconda2/bin/conda', - '~/miniconda2/bin/conda', '~/anaconda3/bin/conda', '~/miniconda3/bin/conda'] - .forEach(knownLocation => { - test(`Must return conda path from known location '${knownLocation}' (non windows)`, async () => { - const expectedCondaLocation = untildify(knownLocation); - platformService.setup(p => p.isWindows).returns(() => false); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - fileSystem.setup(fs => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([expectedCondaLocation])); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(expectedCondaLocation))).returns(() => Promise.resolve(true)); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, expectedCondaLocation, 'Failed to identify'); - }); - }); - - test('Must return \'conda\' if conda could not be found in known locations', async () => { - platformService.setup(p => p.isWindows).returns(() => false); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - fileSystem.setup(fs => fs.search(TypeMoq.It.isAny())).returns(() => Promise.resolve([])); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())).returns((_file: string) => Promise.resolve(false)); - - const condaExe = await condaService.getCondaFile(); - assert.equal(condaExe, 'conda', 'Failed to identify'); - }); - - test('Correctly identify interpreter location relative to entironment path (non windows)', async () => { - const environmentPath = path.join('a', 'b', 'c'); - platformService.setup(p => p.isWindows).returns(() => false); - const pythonPath = condaService.getInterpreterPath(environmentPath); - assert.equal(pythonPath, path.join(environmentPath, 'bin', 'python'), 'Incorrect path'); - }); - - test('Correctly identify interpreter location relative to entironment path (windows)', async () => { - const environmentPath = path.join('a', 'b', 'c'); - platformService.setup(p => p.isWindows).returns(() => true); - const pythonPath = condaService.getInterpreterPath(environmentPath); - assert.equal(pythonPath, path.join(environmentPath, 'python.exe'), 'Incorrect path'); - }); - - test('Returns condaInfo when conda exists', async () => { - const expectedInfo = { - envs: [path.join(environmentsPath, 'conda', 'envs', 'numpy'), - path.join(environmentsPath, 'conda', 'envs', 'scipy')], - default_prefix: '', - 'sys.version': '3.6.1 |Anaconda 4.4.0 (64-bit)| (default, May 11 2017, 13:25:24) [MSC v.1900 64 bit (AMD64)]' - }; - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: JSON.stringify(expectedInfo) })); - - const condaInfo = await condaService.getCondaInfo(); - assert.deepEqual(condaInfo, expectedInfo, 'Conda info does not match'); - }); - - test('Returns undefined if there\'s and error in getting the info', async () => { - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['info', '--json']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('unknown'))); - - const condaInfo = await condaService.getCondaInfo(); - assert.equal(condaInfo, undefined, 'Conda info does not match'); - }); - - test('Returns conda environments when conda exists', async () => { - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: 'xyz' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stdout: '' })); - const environments = await condaService.getCondaEnvironments(true); - assert.equal(environments, undefined, 'Conda environments do not match'); - }); - - test('Logs information message when conda does not exist', async () => { - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - processService.setup(p => p.exec(TypeMoq.It.isValue('conda'), TypeMoq.It.isValue(['env', 'list']), TypeMoq.It.isAny())).returns(() => Promise.reject(new Error('Not Found'))); - 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); - }); - - type InterpreterSearchTestParams = { - pythonPath: string; - environmentName: string; - isLinux: boolean; - expectedCondaPath: string; - }; - - const testsForInterpreter: InterpreterSearchTestParams[] = - [ - { - pythonPath: path.join('users', 'foo', 'envs', 'test1', 'python'), - environmentName: 'test1', - isLinux: true, - expectedCondaPath: path.join('users', 'foo', 'bin', 'conda') - }, - { - pythonPath: path.join('users', 'foo', 'envs', 'test2', 'python'), - environmentName: 'test2', - isLinux: true, - expectedCondaPath: path.join('users', 'foo', 'envs', 'test2', 'conda') - }, - { - pythonPath: path.join('users', 'foo', 'envs', 'test3', 'python'), - environmentName: 'test3', - isLinux: false, - expectedCondaPath: path.join('users', 'foo', 'Scripts', 'conda.exe') - }, - { - pythonPath: path.join('users', 'foo', 'envs', 'test4', 'python'), - environmentName: 'test4', - isLinux: false, - expectedCondaPath: path.join('users', 'foo', 'conda.exe') - } - ]; - - testsForInterpreter.forEach(t => { - test(`Finds conda.exe for subenvironment ${t.environmentName}`, async () => { - platformService.setup(p => p.isLinux).returns(() => t.isLinux); - platformService.setup(p => p.isWindows).returns(() => !t.isLinux); - platformService.setup(p => p.isMac).returns(() => false); - fileSystem.setup(f => f.fileExists(TypeMoq.It.is(p => { - if (p === t.expectedCondaPath) { - return true; - } - return false; - }))).returns(() => Promise.resolve(true)); - - const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, t.environmentName); - assert.equal(condaFile, t.expectedCondaPath); - }); - test(`Finds conda.exe for different ${t.environmentName}`, async () => { - platformService.setup(p => p.isLinux).returns(() => t.isLinux); - platformService.setup(p => p.isWindows).returns(() => !t.isLinux); - platformService.setup(p => p.isMac).returns(() => false); - fileSystem.setup(f => f.fileExists(TypeMoq.It.is(p => { - if (p === t.expectedCondaPath) { - return true; - } - return false; - }))).returns(() => Promise.resolve(true)); - - const condaFile = await condaService.getCondaFileFromInterpreter(t.pythonPath, undefined); - - // This should only work if the expectedConda path has the original environment name in it - if (t.expectedCondaPath.includes(t.environmentName)) { - assert.equal(condaFile, t.expectedCondaPath); - } else { - assert.equal(condaFile, undefined); - } - }); - }); -}); diff --git a/src/test/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<IProcessService>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let virtualEnvironmentManager: TypeMoq.IMock<IVirtualEnvironmentManager>; - let interpreterHelper: TypeMoq.IMock<InterpreterHelper>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let currentPathService: CurrentPathService; - let persistentState: TypeMoq.IMock<IPersistentState<PythonInterpreter[]>>; - let platformService: TypeMoq.IMock<IPlatformService>; - let pythonInPathCommandProvider: IPythonInPathCommandProvider; - setup(async () => { - processService = TypeMoq.Mock.ofType<IProcessService>(); - virtualEnvironmentManager = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); - interpreterHelper = TypeMoq.Mock.ofType<InterpreterHelper>(); - const configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - const persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - persistentState = TypeMoq.Mock.ofType<IPersistentState<PythonInterpreter[]>>(); - processService.setup((x: any) => x.then).returns(() => undefined); - persistentState.setup(p => p.value).returns(() => undefined as any); - persistentState.setup(p => p.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - persistentStateFactory.setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); - const procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager), TypeMoq.It.isAny())).returns(() => virtualEnvironmentManager.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterVersionService), TypeMoq.It.isAny())).returns(() => interpreterHelper.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory), TypeMoq.It.isAny())).returns(() => persistentStateFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configurationService.object); - pythonInPathCommandProvider = new PythonInPathCommandProvider(platformService.object); - currentPathService = new CurrentPathService(interpreterHelper.object, procServiceFactory.object, - pythonInPathCommandProvider, serviceContainer.object); - }); - - [true, false].forEach(isWindows => { - test(`Interpreters that do not exist on the file system are not excluded from the list (${isWindows ? 'windows' : 'not windows'})`, async () => { - // Specific test for 1305 - const version = new SemVer('1.0.0'); - platformService.setup(p => p.isWindows).returns(() => isWindows); - platformService.setup(p => p.osType).returns(() => isWindows ? OSType.Windows : OSType.Linux); - interpreterHelper.setup(v => v.getInterpreterInformation(TypeMoq.It.isAny())).returns(() => Promise.resolve({ version })); - - const execArgs = ['-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 45c6236d6a5a..d9be806ff709 100644 --- a/src/test/interpreters/display.unit.test.ts +++ b/src/test/interpreters/display.unit.test.ts @@ -1,32 +1,49 @@ import { expect } from 'chai'; import * as path from 'path'; import { SemVer } from 'semver'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Disposable, 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 { InterpreterAutoSelectionService } from '../../client/interpreter/autoSelection'; -import { IInterpreterAutoSelectionService } from '../../client/interpreter/autoSelection/types'; -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', () => { @@ -34,145 +51,403 @@ suite('Interpreters Display', () => { let workspaceService: TypeMoq.IMock<IWorkspaceService>; let serviceContainer: TypeMoq.IMock<IServiceContainer>; let interpreterService: TypeMoq.IMock<IInterpreterService>; - let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>; let fileSystem: TypeMoq.IMock<IFileSystem>; let disposableRegistry: Disposable[]; let statusBar: TypeMoq.IMock<StatusBarItem>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let configurationService: TypeMoq.IMock<IConfigurationService>; - let interpreterDisplay: IInterpreterDisplay; + let interpreterDisplay: IInterpreterDisplay & IExtensionSingleActivationService; let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; let pathUtils: TypeMoq.IMock<IPathUtils>; - let autoSelection: IInterpreterAutoSelectionService; - setup(() => { + let languageStatusItem: TypeMoq.IMock<LanguageStatusItem>; + let traceLogStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; + async function createInterpreterDisplay(filters: IInterpreterStatusbarVisibilityFilter[] = []) { + interpreterDisplay = new InterpreterDisplay(serviceContainer.object); + try { + await interpreterDisplay.activate(); + } catch {} + filters.forEach((f) => interpreterDisplay.registerVisibilityFilter(f)); + } + + async function setupMocks(useLanguageStatus: boolean) { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); disposableRegistry = []; statusBar = TypeMoq.Mock.ofType<StatusBarItem>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + statusBar.setup((s) => s.name).returns(() => ''); + languageStatusItem = TypeMoq.Mock.ofType<LanguageStatusItem>(); pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); - autoSelection = mock(InterpreterAutoSelectionService); - - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => applicationShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterService))).returns(() => interpreterService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IVirtualEnvironmentManager))).returns(() => virtualEnvMgr.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => disposableRegistry); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterAutoSelectionService))).returns(() => instance(autoSelection)); - - applicationShell.setup(a => a.createStatusBarItem(TypeMoq.It.isValue(StatusBarAlignment.Left), TypeMoq.It.isValue(100))).returns(() => statusBar.object); - pathUtils.setup(p => p.getDisplayName(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(p => p); - interpreterDisplay = new InterpreterDisplay(serviceContainer.object); - }); + traceLogStub = sinon.stub(logging, 'traceLog'); + + 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(); + } + function setupWorkspaceFolder(resource: Uri, workspaceFolder?: Uri) { if (workspaceFolder) { const mockFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - mockFolder.setup(w => w.uri).returns(() => workspaceFolder); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(resource))).returns(() => mockFolder.object); + 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); - when(autoSelection.autoSelectInterpreter(anything())).thenResolve(); - interpreterService.setup(i => i.getInterpreters(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve([])); - interpreterService.setup(i => i.getActiveInterpreter(TypeMoq.It.isValue(workspaceFolder))).returns(() => Promise.resolve(activeInterpreter)); - - await interpreterDisplay.refresh(resource); - - verify(autoSelection.autoSelectInterpreter(anything())).once(); - statusBar.verify(s => s.text = TypeMoq.It.isValue(activeInterpreter.displayName)!, TypeMoq.Times.once()); - statusBar.verify(s => s.tooltip = TypeMoq.It.isValue(activeInterpreter.path)!, TypeMoq.Times.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<void>(); + const filter1: ReadWrite<IInterpreterStatusbarVisibilityFilter> = { + hidden: false, + changed: event1.event, + }; + const event2 = new EventEmitter<void>(); + const filter2: ReadWrite<IInterpreterStatusbarVisibilityFilter> = { + hidden: false, + changed: event2.event, + }; + createInterpreterDisplay([filter1, filter2]); + + await interpreterDisplay.refresh(resource); + + statusBar.verify((s) => s.show(), TypeMoq.Times.once()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + + // Filter one will now want the status bar to get hidden. + statusBar.reset(); + filter1.hidden = true; + event1.fire(); + + statusBar.verify((s) => s.show(), TypeMoq.Times.never()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.once()); + + // Filter two now needs it to be displayed. + statusBar.reset(); + event2.fire(); + + // No changes. + statusBar.verify((s) => s.show(), TypeMoq.Times.never()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.once()); + + // Filter two now needs it to be displayed & filter 1 will allow it to be displayed. + filter1.hidden = false; + statusBar.reset(); + event2.fire(); + + // No changes. + statusBar.verify((s) => s.show(), TypeMoq.Times.once()); + statusBar.verify((s) => s.hide(), TypeMoq.Times.never()); + }); + }); + }); }); }); diff --git a/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts b/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts deleted file mode 100644 index 4332b9ad7ac7..000000000000 --- a/src/test/interpreters/display/interpreterSelectionTip.unit.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { IApplicationShell } from '../../../client/common/application/types'; -import { PersistentState, PersistentStateFactory } from '../../../client/common/persistentState'; -import { IPersistentState } from '../../../client/common/types'; -import { Common, Interpreters } from '../../../client/common/utils/localize'; -import { InterpreterSelectionTip } from '../../../client/interpreter/display/interpreterSelectionTip'; - -// tslint:disable:no-any -suite('Interpreters - Interpreter Selection Tip', () => { - let selectionTip: InterpreterSelectionTip; - let appShell: IApplicationShell; - let storage: IPersistentState<boolean>; - setup(() => { - const factory = mock(PersistentStateFactory); - storage = mock(PersistentState); - appShell = mock(ApplicationShell); - - when(factory.createGlobalPersistentState('InterpreterSelectionTip', false)).thenReturn(instance(storage)); - - selectionTip = new InterpreterSelectionTip(instance(appShell), instance(factory)); - }); - test('Do not show tip', async () => { - when(storage.value).thenReturn(true); - - await selectionTip.activate(undefined); - - verify(appShell.showInformationMessage(anything(), anything())).never(); - }); - test('Show tip and do not track it', async () => { - when(storage.value).thenReturn(false); - when(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).thenResolve(); - - await selectionTip.activate(undefined); - - verify(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).once(); - verify(storage.updateValue(true)).never(); - }); - test('Show tip once per session', async () => { - when(storage.value).thenReturn(false); - when(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).thenResolve(); - - await Promise.all([ - selectionTip.activate(undefined), - selectionTip.activate(undefined), - selectionTip.activate(undefined) - ]); - - verify(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).once(); - verify(storage.updateValue(true)).never(); - }); - test('Show tip and track it', async () => { - when(storage.value).thenReturn(false); - when(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).thenResolve(Common.gotIt() as any); - - await selectionTip.activate(undefined); - - verify(appShell.showInformationMessage(Interpreters.selectInterpreterTip(), Common.gotIt())).once(); - verify(storage.updateValue(true)).once(); - }); -}); diff --git a/src/test/interpreters/display/progressDisplay.unit.test.ts b/src/test/interpreters/display/progressDisplay.unit.test.ts index 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<R> = (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>; +import { Commands } from '../../../client/common/constants'; +import { createDeferred, Deferred } from '../../../client/common/utils/async'; +import { Interpreters } from '../../../client/common/utils/localize'; +import { IComponentAdapter } from '../../../client/interpreter/contracts'; +import { InterpreterLocatorProgressStatusBarHandler } from '../../../client/interpreter/display/progressDisplay'; +import { ProgressNotificationEvent, ProgressReportStage } from '../../../client/pythonEnvironments/base/locator'; +import { noop } from '../../core'; + +type ProgressTask<R> = ( + progress: Progress<{ message?: string; increment?: number }>, + token: CancellationToken, +) => Thenable<R>; suite('Interpreters - Display Progress', () => { - let refreshingCallback: (e: void) => any | undefined; - let refreshedCallback: (e: void) => any | undefined; - const progressService: IInterpreterLocatorProgressService = { - onRefreshing(listener: (e: void) => any): Disposable { - refreshingCallback = listener; - return { dispose: noop }; - }, - onRefreshed(listener: (e: void) => any): Disposable { - refreshedCallback = listener; - return { dispose: noop }; - }, - register(): void { - noop(); - } - }; - - test('Display loading message when refreshing interpreters for the first time', async () => { + let refreshingCallback: (e: ProgressNotificationEvent) => unknown | undefined; + let refreshDeferred: Deferred<void>; + let componentAdapter: IComponentAdapter; + setup(() => { + refreshDeferred = createDeferred<void>(); + componentAdapter = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + onProgress(listener: (e: ProgressNotificationEvent) => any): Disposable { + refreshingCallback = listener; + return { dispose: noop }; + }, + getRefreshPromise: () => refreshDeferred.promise, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any; + }); + teardown(() => { + refreshDeferred.resolve(); + }); + test('Display discovering message when refreshing interpreters for the first time', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); - statusBar.register(); - refreshingCallback(undefined); + await statusBar.activate(); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - expect(options.title).to.be.equal(Common.loadingExtension()); + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(`[${Interpreters.discovering}](command:${Commands.Set_Interpreter})`); }); test('Display refreshing message when refreshing interpreters for the second time', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); - statusBar.register(); - refreshingCallback(undefined); + await statusBar.activate(); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - let options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - expect(options.title).to.be.equal(Common.loadingExtension()); + let options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(`[${Interpreters.discovering}](command:${Commands.Set_Interpreter})`); - refreshingCallback(undefined); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - expect(options.title).to.be.equal(Interpreters.refreshing()); + options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + expect(options.title).to.be.equal(`[${Interpreters.refreshing}](command:${Commands.Set_Interpreter})`); }); test('Progress message is hidden when loading has completed', async () => { const shell = mock(ApplicationShell); - const statusBar = new InterpreterLocatorProgressStatubarHandler(instance(shell), progressService, []); + const statusBar = new InterpreterLocatorProgressStatusBarHandler(instance(shell), [], componentAdapter); when(shell.withProgress(anything(), anything())).thenResolve(); - statusBar.register(); - refreshingCallback(undefined); + await statusBar.activate(); + refreshingCallback({ stage: ProgressReportStage.discoveryStarted }); - const options = capture(shell.withProgress as any).last()[0] as ProgressOptions; - const callback = capture(shell.withProgress as any).last()[1] as ProgressTask<void>; - const promise = callback(undefined as any, undefined as any); + const options = capture(shell.withProgress as never).last()[0] as ProgressOptions; + const callback = capture(shell.withProgress as never).last()[1] as ProgressTask<void>; + const promise = callback(undefined as never, undefined as never); - expect(options.title).to.be.equal(Common.loadingExtension()); + expect(options.title).to.be.equal(`[${Interpreters.discovering}](command:${Commands.Set_Interpreter})`); - refreshedCallback(undefined); + refreshDeferred.resolve(); // Promise must resolve when refreshed callback is invoked. // When promise resolves, the progress message is hidden by VSC. await promise; diff --git a/src/test/interpreters/helpers.unit.test.ts b/src/test/interpreters/helpers.unit.test.ts index 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<IDocumentManager>; let workspaceService: TypeMoq.IMock<IWorkspaceService>; let serviceContainer: TypeMoq.IMock<IServiceContainer>; let helper: InterpreterHelper; + let pyenvs: TypeMoq.IMock<IComponentAdapter>; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + pyenvs = TypeMoq.Mock.ofType<IComponentAdapter>(); - 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<TextEditor>(); const document = TypeMoq.Mock.ofType<TextDocument>(); - textEditor.setup(t => t.document).returns(() => document.object); - document.setup(d => d.uri).returns(() => documentUri); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => undefined); + 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<TextEditor>(); const document = TypeMoq.Mock.ofType<TextDocument>(); - textEditor.setup(t => t.document).returns(() => document.object); - document.setup(d => d.uri).returns(() => documentUri); - documentManager.setup(d => d.activeTextEditor).returns(() => textEditor.object); - // tslint:disable-next-line:no-any - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isValue(documentUri))).returns(() => { return { uri: documentWorkspaceFolderUri } as any; }); + 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<IInterpreterService>(); + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterPathCommand = new InterpreterPathCommand(instance(interpreterService), []); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure command is registered with the correct callback handler', async () => { + let getInterpreterPathHandler = (_param: unknown) => undefined; + registerCommandStub.callsFake((_, cb) => { + getInterpreterPathHandler = cb; + return TypeMoq.Mock.ofType<IDisposable>().object; + }); + await interpreterPathCommand.activate(); + + sinon.assert.calledOnce(registerCommandStub); + const getSelectedInterpreterPath = sinon.stub(InterpreterPathCommand.prototype, '_getSelectedInterpreterPath'); + getInterpreterPathHandler([]); + assert(getSelectedInterpreterPath.calledOnceWith([])); + }); + + test('If `workspaceFolder` property exists in `args`, it is used to retrieve setting from config', async () => { + const args = { workspaceFolder: 'folderPath', type: 'debugpy' }; + when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'settingValue' }) as unknown; + }); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); + + test('If `args[1]` is defined, it is used to retrieve setting from config', async () => { + const args = ['command', 'folderPath']; + when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'settingValue' }) as unknown; + }); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); + + test('If interpreter path contains spaces, double quote it before returning', async () => { + const args = ['command', 'folderPath']; + when(interpreterService.getActiveInterpreter(anything())).thenCall((arg) => { + assert.deepEqual(arg, Uri.file('folderPath')); + + return Promise.resolve({ path: 'setting Value' }) as unknown; + }); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('"setting Value"'); + }); + + test('If neither of these exists, value of workspace folder is `undefined`', async () => { + getConfigurationStub.withArgs('python').returns({ + get: sinon.stub().returns(false), + }); + + const args = ['command']; + + when(interpreterService.getActiveInterpreter(undefined)).thenReturn( + Promise.resolve({ path: 'settingValue' }) as Promise<PythonEnvironment | undefined>, + ); + const setting = await interpreterPathCommand._getSelectedInterpreterPath(args); + expect(setting).to.equal('settingValue'); + }); +}); diff --git a/src/test/interpreters/interpreterService.unit.test.ts b/src/test/interpreters/interpreterService.unit.test.ts index 84eef49031f8..1d521dad8ec8 100644 --- a/src/test/interpreters/interpreterService.unit.test.ts +++ b/src/test/interpreters/interpreterService.unit.test.ts @@ -3,455 +3,306 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any no-unnecessary-override - import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import { Container } from 'inversify'; -import * as md5 from 'md5'; import * as path from 'path'; -import { SemVer } from 'semver'; import * as TypeMoq from 'typemoq'; -import { 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, IPersistentState, 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'; -use(chaiAsPromised); +/* eslint-disable @typescript-eslint/no-explicit-any */ + +use(chaiAsPromised.default); suite('Interpreters service', () => { let serviceManager: ServiceManager; let serviceContainer: ServiceContainer; let updater: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let pyenvs: TypeMoq.IMock<IComponentAdapter>; let helper: TypeMoq.IMock<IInterpreterHelper>; - let locator: TypeMoq.IMock<IInterpreterLocatorService>; let workspace: TypeMoq.IMock<IWorkspaceService>; let config: TypeMoq.IMock<WorkspaceConfiguration>; let fileSystem: TypeMoq.IMock<IFileSystem>; let interpreterDisplay: TypeMoq.IMock<IInterpreterDisplay>; - let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>; let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; let pythonExecutionService: TypeMoq.IMock<IPythonExecutionService>; let configService: TypeMoq.IMock<IConfigurationService>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let experiments: TypeMoq.IMock<IExperimentService>; + let installer: TypeMoq.IMock<IInstaller>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let reportActiveInterpreterChangedStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); - function setupSuite() { const cont = new Container(); serviceManager = new ServiceManager(cont); serviceContainer = new ServiceContainer(cont); - updater = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); - helper = TypeMoq.Mock.ofType<IInterpreterHelper>(); - locator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); - workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - config = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - interpreterDisplay = TypeMoq.Mock.ofType<IInterpreterDisplay>(); - virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); - persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); - pythonExecutionService = TypeMoq.Mock.ofType<IPythonExecutionService>(); - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + interpreterPathService = createTypeMoq<IInterpreterPathService>(); + updater = createTypeMoq<IPythonPathUpdaterServiceManager>(); + pyenvs = createTypeMoq<IComponentAdapter>(); + helper = createTypeMoq<IInterpreterHelper>(); + workspace = createTypeMoq<IWorkspaceService>(); + config = createTypeMoq<WorkspaceConfiguration>(); + fileSystem = createTypeMoq<IFileSystem>(); + interpreterDisplay = createTypeMoq<IInterpreterDisplay>(); + persistentStateFactory = createTypeMoq<IPersistentStateFactory>(); + pythonExecutionFactory = createTypeMoq<IPythonExecutionFactory>(); + pythonExecutionService = createTypeMoq<IPythonExecutionService>(); + configService = createTypeMoq<IConfigurationService>(); + installer = createTypeMoq<IInstaller>(); + appShell = createTypeMoq<IApplicationShell>(); + experiments = createTypeMoq<IExperimentService>(); + + pythonSettings = createTypeMoq<IPythonSettings>(); + pythonSettings.setup((s) => s.pythonPath).returns(() => PYTHON_PATH); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); 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>(IExperimentService, experiments.object); serviceManager.addSingletonInstance<Disposable[]>(IDisposableRegistry, []); serviceManager.addSingletonInstance<IInterpreterHelper>(IInterpreterHelper, helper.object); - serviceManager.addSingletonInstance<IPythonPathUpdaterServiceManager>(IPythonPathUpdaterServiceManager, updater.object); + serviceManager.addSingletonInstance<IPythonPathUpdaterServiceManager>( + IPythonPathUpdaterServiceManager, + updater.object, + ); serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance<IInterpreterLocatorService>(IInterpreterLocatorService, locator.object, INTERPRETER_LOCATOR_SERVICE); serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fileSystem.object); + serviceManager.addSingletonInstance<IInterpreterPathService>( + IInterpreterPathService, + interpreterPathService.object, + ); serviceManager.addSingletonInstance<IInterpreterDisplay>(IInterpreterDisplay, interpreterDisplay.object); - serviceManager.addSingletonInstance<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, virtualEnvMgr.object); - serviceManager.addSingletonInstance<IPersistentStateFactory>(IPersistentStateFactory, persistentStateFactory.object); - serviceManager.addSingletonInstance<IPythonExecutionFactory>(IPythonExecutionFactory, pythonExecutionFactory.object); - serviceManager.addSingletonInstance<IPythonExecutionService>(IPythonExecutionService, pythonExecutionService.object); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + serviceManager.addSingletonInstance<IPersistentStateFactory>( + IPersistentStateFactory, + persistentStateFactory.object, + ); + serviceManager.addSingletonInstance<IPythonExecutionFactory>( + IPythonExecutionFactory, + pythonExecutionFactory.object, + ); + serviceManager.addSingletonInstance<IPythonExecutionService>( + IPythonExecutionService, + pythonExecutionService.object, + ); + serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, + ); + installer.setup((i) => i.isInstalled(TypeMoq.It.isAny())).returns(() => Promise.resolve(true)); + serviceManager.addSingletonInstance<IInstaller>(IInstaller, installer.object); + serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configService.object); - } - suite('Misc', () => { - setup(setupSuite); - [undefined, Uri.file('xyz')] - .forEach(resource => { - const resourceTestSuffix = `(${resource ? 'with' : 'without'} a resource)`; - - test(`Refresh invokes refresh of display ${resourceTestSuffix}`, async () => { - interpreterDisplay - .setup(i => i.refresh(TypeMoq.It.isValue(resource))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - - const service = new InterpreterService(serviceContainer); - 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<IDocumentManager>(); - - let activeTextEditorChangeHandler: Function | undefined; - documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { - activeTextEditorChangeHandler = handler; - return { dispose: noop }; - }); - serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - - // tslint:disable-next-line:no-any - service.initialize(); - const textEditor = TypeMoq.Mock.ofType<TextEditor>(); - const uri = Uri.file(path.join('usr', 'file.py')); - const document = TypeMoq.Mock.ofType<TextDocument>(); - textEditor.setup(t => t.document).returns(() => document.object); - document.setup(d => d.uri).returns(() => uri); - activeTextEditorChangeHandler!(textEditor.object); - - interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); - }); - - test('If there is no active document then interpreter.refresh should not be invoked', async () => { - const service = new InterpreterService(serviceContainer); - const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - - let activeTextEditorChangeHandler: Function | undefined; - documentManager.setup(d => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { - activeTextEditorChangeHandler = handler; - return { dispose: noop }; - }); - serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - - // tslint:disable-next-line:no-any - service.initialize(); - activeTextEditorChangeHandler!(); - - interpreterDisplay.verify(i => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); - }); + reportActiveInterpreterChangedStub = sinon.stub(proposedApi, 'reportActiveInterpreterChanged'); }); - 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'); - }); - }); + teardown(() => { + sinon.restore(); }); - suite('Caching Display name', () => { - setup(() => { - setupSuite(); - fileSystem.reset(); - persistentStateFactory.reset(); - }); - test('Return cached display name', async () => { - const pythonPath = '1234'; - const interpreterInfo: Partial<PythonInterpreter> = { path: pythonPath }; - const fileHash = 'File_Hash'; - const hash = `${fileHash}-${md5(JSON.stringify({ ...interpreterInfo, displayName: '' }))}`; - 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: { hash, displayName: expectedDisplayName } - }; - return state as any; - }) - .verifiable(TypeMoq.Times.once()); - - const service = new InterpreterService(serviceContainer); - const displayName = await service.getDisplayName(interpreterInfo, undefined); + [undefined, Uri.file('xyz')].forEach((resource) => { + const resourceTestSuffix = `(${resource ? 'with' : 'without'} a resource)`; - 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<PythonInterpreter> = { 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; - }) + 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); - const displayName = await service.getDisplayName(interpreterInfo, undefined).catch(() => ''); + const service = new InterpreterService(serviceContainer, pyenvs.object); + await service.refresh(resource); - expect(displayName).to.not.equal(expectedDisplayName); - fileSystem.verifyAll(); - persistentStateFactory.verifyAll(); + interpreterDisplay.verifyAll(); }); }); - // This is kind of a verbose test, but we need to ensure we have covered all permutations. - // Also we have special handling for certain types of interpreters. - suite('Display Format (with all permutations)', () => { - setup(setupSuite); - [undefined, Uri.file('xyz')].forEach(resource => { - [undefined, new SemVer('1.2.3-alpha')].forEach(version => { - // Forced cast to ignore TS warnings. - (EnumEx.getNamesAndValues<Architecture>(Architecture) as ({ name: string; value: Architecture } | undefined)[]).concat(undefined).forEach(arch => { - [undefined, path.join('a', 'b', 'c', 'd', 'bin', 'python')].forEach(pythonPath => { - // Forced cast to ignore TS warnings. - (EnumEx.getNamesAndValues<InterpreterType>(InterpreterType) as ({ name: string; value: InterpreterType } | undefined)[]).concat(undefined).forEach(interpreterType => { - [undefined, 'my env name'].forEach(envName => { - ['', 'my pipenv name'].forEach(pipEnvName => { - const testName = [`${resource ? 'With' : 'Without'} a workspace`, - `${version ? 'with' : 'without'} version information`, - `${arch ? arch.name : 'without'} architecture`, - `${pythonPath ? 'with' : 'without'} python Path`, - `${interpreterType ? `${interpreterType.name} interpreter type` : 'without interpreter type'}`, - `${envName ? 'with' : 'without'} environment name`, - `${pipEnvName ? 'with' : 'without'} pip environment` - ].join(', '); - - test(testName, async () => { - const interpreterInfo: Partial<PythonInterpreter> = { - version, - architecture: arch ? arch.value : undefined, - envName, - type: interpreterType ? interpreterType.value : undefined, - path: pythonPath - }; - - if (interpreterInfo.path && interpreterType && interpreterType.value === InterpreterType.Pipenv) { - virtualEnvMgr - .setup(v => v.getEnvironmentName(TypeMoq.It.isValue(interpreterInfo.path!), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(pipEnvName)); - } - if (interpreterType) { - helper - .setup(h => h.getInterpreterTypeDisplayName(TypeMoq.It.isValue(interpreterType.value))) - .returns(() => `${interpreterType!.name}_display`); - } - - const service = new InterpreterService(serviceContainer); - const expectedDisplayName = buildDisplayName(interpreterInfo); - - const displayName = await service.getDisplayName(interpreterInfo, resource); - expect(displayName).to.equal(expectedDisplayName); - }); - - function buildDisplayName(interpreterInfo: Partial<PythonInterpreter>) { - const displayNameParts: string[] = ['Python']; - const envSuffixParts: string[] = []; - - if (interpreterInfo.version) { - displayNameParts.push(`${interpreterInfo.version.major}.${interpreterInfo.version.minor}.${interpreterInfo.version.patch}`); - } - if (interpreterInfo.architecture) { - displayNameParts.push(getArchitectureDisplayName(interpreterInfo.architecture)); - } - if (!interpreterInfo.envName && interpreterInfo.path && interpreterInfo.type && interpreterInfo.type === InterpreterType.Pipenv && pipEnvName) { - // If we do not have the name of the environment, then try to get it again. - // This can happen based on the context (i.e. resource). - // I.e. we can determine if an environment is PipEnv only when giving it the right workspacec path (i.e. resource). - interpreterInfo.envName = pipEnvName; - } - if (interpreterInfo.envName && interpreterInfo.envName.length > 0) { - envSuffixParts.push(`'${interpreterInfo.envName}'`); - } - if (interpreterInfo.type) { - envSuffixParts.push(`${interpreterType!.name}_display`); - } - - const envSuffix = envSuffixParts.length === 0 ? '' : - `(${envSuffixParts.join(': ')})`; - return `${displayNameParts.join(' ')} ${envSuffix}`.trim(); - } - }); - }); - }); - }); - }); + test('Changes to active document should invoke interpreter.refresh method', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq<IDocumentManager>(); + + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let activeTextEditorChangeHandler: (e: TextEditor | undefined) => any | undefined; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((handler) => { + activeTextEditorChangeHandler = handler; + return { dispose: noop }; }); - }); - }); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); - suite('Interprter Cache', () => { - setup(() => { - setupSuite(); - fileSystem.reset(); - persistentStateFactory.reset(); - }); - test('Ensure cache is returned', async () => { - const fileHash = 'file_hash'; - const pythonPath = 'Some Python Path'; - fileSystem - .setup(fs => fs.getFileHash(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(fileHash)) - .verifiable(TypeMoq.Times.once()); + service.initialize(); + const textEditor = createTypeMoq<TextEditor>(); + const uri = Uri.file(path.join('usr', 'file.py')); + const document = createTypeMoq<TextDocument>(); + textEditor.setup((t) => t.document).returns(() => document.object); + document.setup((d) => d.uri).returns(() => uri); + activeTextEditorChangeHandler!(textEditor.object); - const state = TypeMoq.Mock.ofType<IPersistentState<{ fileHash: string; info?: PythonInterpreter }>>(); - const info = { path: 'hell', type: InterpreterType.Venv }; - state - .setup(s => s.value) - .returns(() => { - return { - fileHash, - info: info as any - }; - }) - .verifiable(TypeMoq.Times.atLeastOnce()); - state - .setup(s => s.updateValue(TypeMoq.It.isAny())) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.never()); - state - .setup(s => (s as any).then) - .returns(() => undefined); - persistentStateFactory - .setup(f => f.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => state.object) - .verifiable(TypeMoq.Times.once()); + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(uri)), TypeMoq.Times.once()); + }); - const service = new InterpreterService(serviceContainer); + test('If there is no active document then interpreter.refresh should not be invoked', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq<IDocumentManager>(); - const store = await service.getInterpreterCache(pythonPath); + 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); - expect(store.value).to.deep.equal({ fileHash, info }); - state.verifyAll(); - persistentStateFactory.verifyAll(); - fileSystem.verifyAll(); - }); - test('Ensure cache is cleared if file hash is different', async () => { - const fileHash = 'file_hash'; - const pythonPath = 'Some Python Path'; - fileSystem - .setup(fs => fs.getFileHash(TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve('different value')) - .verifiable(TypeMoq.Times.once()); + service.initialize(); + activeTextEditorChangeHandler!(); - const state = TypeMoq.Mock.ofType<IPersistentState<{ fileHash: string; info?: PythonInterpreter }>>(); - const info = { path: 'hell', type: InterpreterType.Venv }; - state - .setup(s => s.value) - .returns(() => { - return { - fileHash, - info: info as any - }; - }) - .verifiable(TypeMoq.Times.atLeastOnce()); - state - .setup(s => s.updateValue(TypeMoq.It.isValue({fileHash: 'different value'}))) - .returns(() => Promise.resolve()) - .verifiable(TypeMoq.Times.once()); - state - .setup(s => (s as any).then) - .returns(() => undefined); - persistentStateFactory - .setup(f => f.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => state.object) - .verifiable(TypeMoq.Times.once()); + interpreterDisplay.verify((i) => i.refresh(TypeMoq.It.isValue(undefined)), TypeMoq.Times.never()); + }); - const service = new InterpreterService(serviceContainer); + test('Register the correct handler', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const documentManager = createTypeMoq<IDocumentManager>(); + + workspace.setup((w) => w.workspaceFolders).returns(() => [{ uri: '' }] as any); + let interpreterPathServiceHandler: (e: InterpreterConfigurationScope) => any | undefined = () => 0; + documentManager + .setup((d) => d.onDidChangeActiveTextEditor(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => ({ dispose: noop })); + const i: InterpreterConfigurationScope = { + uri: Uri.parse('a'), + configTarget: ConfigurationTarget.Workspace, + }; + configService.reset(); + configService + .setup((c) => c.getSettings(i.uri)) + .returns(() => pythonSettings.object) + .verifiable(TypeMoq.Times.once()); + interpreterPathService + .setup((d) => d.onDidChange(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((cb) => { + interpreterPathServiceHandler = cb; + }) + .returns(() => ({ dispose: noop })); + serviceManager.addSingletonInstance(IDocumentManager, documentManager.object); + interpreterDisplay.setup((a) => a.refresh()).returns(() => Promise.resolve()); + + service.initialize(); + expect(interpreterPathServiceHandler).to.not.equal(undefined, 'Handler not set'); + + await interpreterPathServiceHandler!(i); + + // Ensure correct handler was invoked + configService.verifyAll(); + }); - const store = await service.getInterpreterCache(pythonPath); + 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, + }); + }); - expect(store.value.info).to.deep.equal(info); - state.verifyAll(); - persistentStateFactory.verifyAll(); - fileSystem.verifyAll(); + test('If stored setting is not equal to current interpreter path setting, refresh the interpreter display', async () => { + const service = new InterpreterService(serviceContainer, pyenvs.object); + const resource = Uri.parse('a'); + const workspaceFolder = { uri: resource, name: '', index: 0 }; + workspace.setup((w) => w.getWorkspaceFolder(resource)).returns(() => workspaceFolder); + service._pythonPathSetting = 'stored setting'; + configService.reset(); + configService.setup((c) => c.getSettings(resource)).returns(() => ({ pythonPath: 'current path' } as any)); + interpreterDisplay + .setup((i) => i.refresh()) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + await service._onConfigChanged(resource); + interpreterDisplay.verifyAll(); + sinon.assert.calledOnceWithExactly(reportActiveInterpreterChangedStub, { + path: 'current path', + resource: workspaceFolder, }); }); + + test('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<IProcessService>; - let interpreterVersionService: IInterpreterVersionService; - - setup(() => { - const processFactory = typeMoq.Mock.ofType<IProcessServiceFactory>(); - processService = typeMoq.Mock.ofType<IProcessService>(); - // tslint:disable-next-line:no-any - processService.setup((p: any) => p.then).returns(() => undefined); - - processFactory.setup(p => p.create()).returns(() => Promise.resolve(processService.object)); - interpreterVersionService = new InterpreterVersionService(processFactory.object); - }); - 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<IServiceContainer>; - let currentProcess: TypeMoq.IMock<ICurrentProcess>; - let platformService: TypeMoq.IMock<IPlatformService>; - let pathUtils: TypeMoq.IMock<IPathUtils>; - let knownSearchPaths: IKnownSearchPathsForInterpreters; - - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - currentProcess = TypeMoq.Mock.ofType<ICurrentProcess>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess), TypeMoq.It.isAny())).returns(() => currentProcess.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils), TypeMoq.It.isAny())).returns(() => pathUtils.object); - - knownSearchPaths = new KnownSearchPathsForInterpreters(serviceContainer.object); - }); - - test('Ensure known list of paths are returned', async () => { - const pathDelimiter = 'X'; - const pathsInPATHVar = [path.join('a', 'b', 'c'), '', path.join('1', '2'), '3']; - pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); - platformService.setup(p => p.isWindows).returns(() => true); - platformService.setup(p => p.pathVariableName).returns(() => 'PATH'); - currentProcess.setup(p => p.env).returns(() => { - return { PATH: pathsInPATHVar.join(pathDelimiter) }; - }); - - const expectedPaths = [...pathsInPATHVar].filter(item => item.length > 0); - - const paths = knownSearchPaths.getSearchPaths(); - - expect(paths).to.deep.equal(expectedPaths); - }); - - test('Ensure known list of paths are returned on non-windows', async () => { - const homeDir = '/users/peter Smith'; - const pathDelimiter = 'X'; - pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); - pathUtils.setup(p => p.home).returns(() => homeDir); - platformService.setup(p => p.isWindows).returns(() => false); - platformService.setup(p => p.pathVariableName).returns(() => 'PATH'); - currentProcess.setup(p => p.env).returns(() => { - return { PATH: '' }; - }); - - const expectedPaths: string[] = []; - ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin'] - .forEach(p => { - expectedPaths.push(p); - expectedPaths.push(path.join(homeDir, p)); - }); - - expectedPaths.push(path.join(homeDir, 'anaconda', 'bin')); - expectedPaths.push(path.join(homeDir, 'python', 'bin')); - - const paths = knownSearchPaths.getSearchPaths(); - - expect(paths).to.deep.equal(expectedPaths); - }); - - test('Ensure PATH variable and known list of paths are merged on non-windows', async () => { - const homeDir = '/users/peter Smith'; - const pathDelimiter = 'X'; - const pathsInPATHVar = [path.join('a', 'b', 'c'), '', path.join('1', '2'), '3']; - pathUtils.setup(p => p.delimiter).returns(() => pathDelimiter); - pathUtils.setup(p => p.home).returns(() => homeDir); - platformService.setup(p => p.isWindows).returns(() => false); - platformService.setup(p => p.pathVariableName).returns(() => 'PATH'); - currentProcess.setup(p => p.env).returns(() => { - return { PATH: pathsInPATHVar.join(pathDelimiter) }; - }); - - const expectedPaths = [...pathsInPATHVar].filter(item => item.length > 0); - ['/usr/local/bin', '/usr/bin', '/bin', '/usr/sbin', '/sbin', '/usr/local/sbin'] - .forEach(p => { - expectedPaths.push(p); - expectedPaths.push(path.join(homeDir, p)); - }); - - expectedPaths.push(path.join(homeDir, 'anaconda', 'bin')); - expectedPaths.push(path.join(homeDir, 'python', 'bin')); - - const paths = knownSearchPaths.getSearchPaths(); - - expect(paths).to.deep.equal(expectedPaths); - }); -}); diff --git a/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts b/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts deleted file mode 100644 index 8d80e8464d53..000000000000 --- a/src/test/interpreters/locators/cacheableLocatorService.unit.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any max-classes-per-file max-func-body-length - -import { expect } from 'chai'; -import * as md5 from 'md5'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Disposable, Uri, WorkspaceFolder } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { Resource } from '../../../client/common/types'; -import { noop } from '../../../client/common/utils/misc'; -import { IInterpreterWatcher, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { CacheableLocatorService } from '../../../client/interpreter/locators/services/cacheableLocatorService'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { IServiceContainer } from '../../../client/ioc/types'; - -suite('Interpreters - Cacheable Locator Service', () => { - suite('Caching', () => { - class Locator extends CacheableLocatorService { - constructor(name: string, serviceCcontainer: IServiceContainer, private readonly mockLocator: MockLocator) { - super(name, serviceCcontainer); - } - public dispose() { - noop(); - } - protected async getInterpretersImplementation(_resource?: Uri): Promise<PythonInterpreter[]> { - return this.mockLocator.getInterpretersImplementation(); - } - protected getCachedInterpreters(_resource?: Uri): PythonInterpreter[] | undefined { - return this.mockLocator.getCachedInterpreters(); - } - protected async cacheInterpreters(_interpreters: PythonInterpreter[], _resource?: Uri) { - return this.mockLocator.cacheInterpreters(); - } - protected getCacheKey(_resource?: Uri) { - return this.mockLocator.getCacheKey(); - } - } - class MockLocator { - public async getInterpretersImplementation(): Promise<PythonInterpreter[]> { - return []; - } - public getCachedInterpreters(): PythonInterpreter[] | undefined { - return; - } - public async cacheInterpreters() { - return; - } - public getCacheKey(): string { - return ''; - } - } - let serviceContainer: ServiceContainer; - setup(() => { - serviceContainer = mock(ServiceContainer); - }); - - test('Interpreters must be retrieved once, then cached', async () => { - const expectedInterpreters = [1, 2] as any; - const mockedLocatorForVerification = mock(MockLocator); - const locator = new class extends Locator { - protected async addHandlersForInterpreterWatchers(_cacheKey: string, _resource: Resource): Promise<void> { - noop(); - } - }('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); - - when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve(expectedInterpreters); - when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); - when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); - - const [items1, items2, items3] = await Promise.all([locator.getInterpreters(), locator.getInterpreters(), locator.getInterpreters()]); - expect(items1).to.be.deep.equal(expectedInterpreters); - expect(items2).to.be.deep.equal(expectedInterpreters); - expect(items3).to.be.deep.equal(expectedInterpreters); - - verify(mockedLocatorForVerification.getInterpretersImplementation()).once(); - verify(mockedLocatorForVerification.getCachedInterpreters()).atLeast(1); - verify(mockedLocatorForVerification.cacheInterpreters()).atLeast(1); - }); - - test('Ensure onDidCreate event handler is attached', async () => { - const mockedLocatorForVerification = mock(MockLocator); - class Watcher implements IInterpreterWatcher { - public onDidCreate(_listener: (e: Resource) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - return { dispose: noop }; - } - } - const watcher: IInterpreterWatcher = mock(Watcher); - - const locator = new class extends Locator { - protected async getInterpreterWatchers(_resource: Resource): Promise<IInterpreterWatcher[]> { - return [instance(watcher)]; - } - }('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); - - await locator.getInterpreters(); - - verify(watcher.onDidCreate(anything(), anything(), anything())).once(); - }); - - test('Ensure cache is cleared when watcher event fires', async () => { - const expectedInterpreters = [1, 2] as any; - const mockedLocatorForVerification = mock(MockLocator); - class Watcher implements IInterpreterWatcher { - private listner?: (e: Resource) => any; - public onDidCreate(listener: (e: Resource) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - this.listner = listener; - return { dispose: noop }; - } - public invokeListeners() { - this.listner!(undefined); - } - } - const watcher = new Watcher(); - - const locator = new class extends Locator { - protected async getInterpreterWatchers(_resource: Resource): Promise<IInterpreterWatcher[]> { - return [watcher]; - } - }('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); - - when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve(expectedInterpreters); - when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); - when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); - - const [items1, items2, items3] = await Promise.all([locator.getInterpreters(), locator.getInterpreters(), locator.getInterpreters()]); - expect(items1).to.be.deep.equal(expectedInterpreters); - expect(items2).to.be.deep.equal(expectedInterpreters); - expect(items3).to.be.deep.equal(expectedInterpreters); - - verify(mockedLocatorForVerification.getInterpretersImplementation()).once(); - verify(mockedLocatorForVerification.getCachedInterpreters()).atLeast(1); - verify(mockedLocatorForVerification.cacheInterpreters()).once(); - - watcher.invokeListeners(); - - const [items4, items5, items6] = await Promise.all([locator.getInterpreters(), locator.getInterpreters(), locator.getInterpreters()]); - expect(items4).to.be.deep.equal(expectedInterpreters); - expect(items5).to.be.deep.equal(expectedInterpreters); - expect(items6).to.be.deep.equal(expectedInterpreters); - - // We must get the list of interperters again and cache the new result again. - verify(mockedLocatorForVerification.getInterpretersImplementation()).twice(); - verify(mockedLocatorForVerification.cacheInterpreters()).twice(); - }); - test('Ensure locating event is raised', async () => { - const mockedLocatorForVerification = mock(MockLocator); - const locator = new class extends Locator { - protected async getInterpreterWatchers(_resource: Resource): Promise<IInterpreterWatcher[]> { - return []; - } - }('dummy', instance(serviceContainer), instance(mockedLocatorForVerification)); - - let locatingEventRaised = false; - locator.onLocating(() => locatingEventRaised = true); - - when(mockedLocatorForVerification.getInterpretersImplementation()).thenResolve([1, 2] as any); - when(mockedLocatorForVerification.getCacheKey()).thenReturn('xyz'); - when(mockedLocatorForVerification.getCachedInterpreters()).thenResolve(); - - await locator.getInterpreters(); - expect(locatingEventRaised).to.be.equal(true, 'Locating Event not raised'); - }); - }); - suite('Cache Key', () => { - class Locator extends CacheableLocatorService { - public dispose() { - noop(); - } - // tslint:disable-next-line:no-unnecessary-override - public getCacheKey(resource?: Uri) { - return super.getCacheKey(resource); - } - protected async getInterpretersImplementation(_resource?: Uri): Promise<PythonInterpreter[]> { - return []; - } - protected getCachedInterpreters(_resource?: Uri): PythonInterpreter[] | undefined { - return []; - } - protected async cacheInterpreters(_interpreters: PythonInterpreter[], _resource?: Uri) { - noop(); - } - } - let serviceContainer: ServiceContainer; - setup(() => { - serviceContainer = mock(ServiceContainer); - }); - - test('Cache Key must contain name of locator', async () => { - const locator = new Locator('hello-World', instance(serviceContainer)); - - const key = locator.getCacheKey(); - - expect(key).contains('hello-World'); - }); - - test('Cache Key must not contain path to workspace', async () => { - const workspace = mock(WorkspaceService); - const workspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file(__dirname) }; - - when(workspace.hasWorkspaceFolders).thenReturn(true); - when(workspace.workspaceFolders).thenReturn([workspaceFolder]); - when(workspace.getWorkspaceFolder(anything())).thenReturn(workspaceFolder); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspace)); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService, anything())).thenReturn(instance(workspace)); - - const locator = new Locator('hello-World', instance(serviceContainer), false); - - const key = locator.getCacheKey(Uri.file('something')); - - expect(key).contains('hello-World'); - expect(key).not.contains(md5(workspaceFolder.uri.fsPath)); - }); - - test('Cache Key must contain path to workspace', async () => { - const workspace = mock(WorkspaceService); - const workspaceFolder: WorkspaceFolder = { name: '1', index: 1, uri: Uri.file(__dirname) }; - const resource = Uri.file('a'); - - when(workspace.hasWorkspaceFolders).thenReturn(true); - when(workspace.workspaceFolders).thenReturn([workspaceFolder]); - when(workspace.getWorkspaceFolder(resource)).thenReturn(workspaceFolder); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspace)); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService, anything())).thenReturn(instance(workspace)); - - const locator = new Locator('hello-World', instance(serviceContainer), true); - - const key = locator.getCacheKey(resource); - - expect(key).contains('hello-World'); - expect(key).contains(md5(workspaceFolder.uri.fsPath)); - }); - }); -}); diff --git a/src/test/interpreters/locators/helpers.unit.test.ts b/src/test/interpreters/locators/helpers.unit.test.ts deleted file mode 100644 index 87d72666d7a1..000000000000 --- a/src/test/interpreters/locators/helpers.unit.test.ts +++ /dev/null @@ -1,202 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - -import { expect } from 'chai'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { Architecture } from '../../../client/common/utils/platform'; -import { IInterpreterHelper, IInterpreterLocatorHelper, InterpreterType, PythonInterpreter } from '../../../client/interpreter/contracts'; -import { InterpreterLocatorHelper } from '../../../client/interpreter/locators/helpers'; -import { PipEnvServiceHelper } from '../../../client/interpreter/locators/services/pipEnvServiceHelper'; -import { IPipEnvServiceHelper } from '../../../client/interpreter/locators/types'; -import { IServiceContainer } from '../../../client/ioc/types'; - -enum OS { - Windows = 'Windows', - Linux = 'Linux', - Mac = 'Mac' -} - -suite('Interpreters - Locators Helper', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let platform: TypeMoq.IMock<IPlatformService>; - let helper: IInterpreterLocatorHelper; - let fs: TypeMoq.IMock<IFileSystem>; - let pipEnvHelper: IPipEnvServiceHelper; - let interpreterServiceHelper: TypeMoq.IMock<IInterpreterHelper>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - platform = TypeMoq.Mock.ofType<IPlatformService>(); - fs = TypeMoq.Mock.ofType<IFileSystem>(); - pipEnvHelper = mock(PipEnvServiceHelper); - interpreterServiceHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platform.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterServiceHelper.object); - - helper = new InterpreterLocatorHelper(fs.object, instance(pipEnvHelper)); - }); - test('Ensure default Mac interpreter is not excluded from the list of interpreters', async () => { - platform.setup(p => p.isWindows).returns(() => false); - platform.setup(p => p.isLinux).returns(() => false); - platform - .setup(p => p.isMac).returns(() => true) - .verifiable(TypeMoq.Times.never()); - fs - .setup(f => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => false) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const interpreters: PythonInterpreter[] = []; - ['conda', 'virtualenv', 'mac', 'pyenv'].forEach(name => { - const interpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', 'python', 'bin', name), - sysPrefix: name, - sysVersion: name, - type: InterpreterType.Unknown, - version: new SemVer('0.0.0-alpha') - }; - interpreters.push(interpreter); - - // Treat 'mac' as as mac interpreter. - interpreterServiceHelper - .setup(i => i.isMacDefaultPythonPath(TypeMoq.It.isValue(interpreter.path))) - .returns(() => name === 'mac') - .verifiable(TypeMoq.Times.never()); - }); - - const expectedInterpreters = interpreters.slice(0); - when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); - - const items = await helper.mergeInterpreters(interpreters); - - interpreterServiceHelper.verifyAll(); - platform.verifyAll(); - fs.verifyAll(); - expect(items).to.be.lengthOf(4); - expect(items).to.be.deep.equal(expectedInterpreters); - }); - getNamesAndValues<OS>(OS).forEach(os => { - test(`Ensure duplicates are removed (same version and same interpreter directory on ${os.name})`, async () => { - interpreterServiceHelper - .setup(i => i.isMacDefaultPythonPath(TypeMoq.It.isAny())) - .returns(() => false); - platform.setup(p => p.isWindows).returns(() => os.value === OS.Windows); - platform.setup(p => p.isLinux).returns(() => os.value === OS.Linux); - platform.setup(p => p.isMac).returns(() => os.value === OS.Mac); - fs - .setup(f => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((a, b) => a === b) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const interpreters: PythonInterpreter[] = []; - const expectedInterpreters: PythonInterpreter[] = []; - // Unique python paths and versions. - ['3.6', '3.6', '2.7', '2.7'].forEach((name, index) => { - const interpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', `python${name}${index}`, 'bin', name + index.toString()), - sysPrefix: name, - sysVersion: name, - type: InterpreterType.Unknown, - version: new SemVer(`3.${parseInt(name.substr(-1), 10)}.0-final`) - }; - interpreters.push(interpreter); - expectedInterpreters.push(interpreter); - }); - // Same versions, but different executables. - ['3.6', '3.6', '3.7', '3.7'].forEach((name, index) => { - const interpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', 'python', 'bin', 'python.exe'), - sysPrefix: name, - sysVersion: name, - type: InterpreterType.Unknown, - version: new SemVer(`3.${parseInt(name.substr(-1), 10)}.0-final`) - }; - - const duplicateInterpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', 'python', 'bin', `python${name}.exe`), - sysPrefix: name, - sysVersion: name, - type: InterpreterType.Unknown, - version: new SemVer(interpreter.version.raw) - }; - - interpreters.push(interpreter); - interpreters.push(duplicateInterpreter); - if (index % 2 === 1) { - expectedInterpreters.push(interpreter); - } - }); - - when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); - const items = await helper.mergeInterpreters(interpreters); - - interpreterServiceHelper.verifyAll(); - platform.verifyAll(); - fs.verifyAll(); - expect(items).to.be.lengthOf(expectedInterpreters.length); - expect(items).to.be.deep.equal(expectedInterpreters); - }); - }); - getNamesAndValues<OS>(OS).forEach(os => { - test(`Ensure interpreter types are identified from other locators (${os.name})`, async () => { - interpreterServiceHelper - .setup(i => i.isMacDefaultPythonPath(TypeMoq.It.isAny())) - .returns(() => false); - platform.setup(p => p.isWindows).returns(() => os.value === OS.Windows); - platform.setup(p => p.isLinux).returns(() => os.value === OS.Linux); - platform.setup(p => p.isMac).returns(() => os.value === OS.Mac); - fs - .setup(f => f.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns((a, b) => a === b && a === path.join('users', 'python', 'bin')) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const interpreters: PythonInterpreter[] = []; - const expectedInterpreters: PythonInterpreter[] = []; - ['3.6', '3.6'].forEach((name, index) => { - // Ensure the type in the first item is 'Unknown', - // and type in second item is known (e.g. Conda). - const type = index === 0 ? InterpreterType.Unknown : InterpreterType.Pipenv; - const interpreter = { - architecture: Architecture.Unknown, - displayName: name, - path: path.join('users', 'python', 'bin', 'python.exe'), - sysPrefix: name, - sysVersion: name, - type, - version: new SemVer(`3.${parseInt(name.substr(-1), 10)}.0-final`) - }; - interpreters.push(interpreter); - - if (index === 1) { - expectedInterpreters.push(interpreter); - } - }); - - when(pipEnvHelper.getPipEnvInfo(anything())).thenResolve(); - const items = await helper.mergeInterpreters(interpreters); - - interpreterServiceHelper.verifyAll(); - platform.verifyAll(); - fs.verifyAll(); - expect(items).to.be.lengthOf(1); - expect(items).to.be.deep.equal(expectedInterpreters); - }); - }); -}); diff --git a/src/test/interpreters/locators/index.unit.test.ts b/src/test/interpreters/locators/index.unit.test.ts deleted file mode 100644 index 00c028caaf77..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<IServiceContainer>; - let platformSvc: TypeMoq.IMock<IPlatformService>; - let helper: TypeMoq.IMock<IInterpreterLocatorHelper>; - let locator: IInterpreterLocatorService; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - platformSvc = TypeMoq.Mock.ofType<IPlatformService>(); - helper = TypeMoq.Mock.ofType<IInterpreterLocatorHelper>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry))).returns(() => []); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformSvc.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterLocatorHelper))).returns(() => helper.object); - - locator = new PythonInterpreterLocatorService(serviceContainer.object); - }); - [undefined, Uri.file('Something')].forEach(resource => { - getNamesAndValues<OSType>(OSType).forEach(osType => { - if (osType.value === OSType.Unknown) { - return; - } - const testSuffix = `(on ${osType.name}, with${resource ? '' : 'out'} a resource)`; - test(`All Interpreter Sources are used ${testSuffix}`, async () => { - const locatorsTypes: string[] = []; - if (osType.value === OSType.Windows) { - locatorsTypes.push(WINDOWS_REGISTRY_SERVICE); - } - platformSvc.setup(p => p.osType).returns(() => osType.value); - platformSvc.setup(p => p.isWindows).returns(() => osType.value === OSType.Windows); - platformSvc.setup(p => p.isLinux).returns(() => osType.value === OSType.Linux); - platformSvc.setup(p => p.isMac).returns(() => osType.value === OSType.OSX); - - locatorsTypes.push(CONDA_ENV_SERVICE); - locatorsTypes.push(CONDA_ENV_FILE_SERVICE); - locatorsTypes.push(PIPENV_SERVICE); - locatorsTypes.push(GLOBAL_VIRTUAL_ENV_SERVICE); - locatorsTypes.push(WORKSPACE_VIRTUAL_ENV_SERVICE); - locatorsTypes.push(KNOWN_PATH_SERVICE); - locatorsTypes.push(CURRENT_PATH_SERVICE); - - const locatorsWithInterpreters = locatorsTypes.map(typeName => { - const interpreter: PythonInterpreter = { - architecture: Architecture.Unknown, - displayName: typeName, - path: typeName, - sysPrefix: typeName, - sysVersion: typeName, - type: InterpreterType.Unknown, - version: new SemVer('0.0.0-alpha') - }; - - const typeLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); - typeLocator - .setup(l => l.hasInterpreters) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - typeLocator - .setup(l => l.getInterpreters(TypeMoq.It.isValue(resource))) - .returns(() => Promise.resolve([interpreter])) - .verifiable(TypeMoq.Times.once()); - - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(typeName))) - .returns(() => typeLocator.object); - - return { - type: typeName, - locator: typeLocator, - interpreters: [interpreter] - }; - }); - - helper - .setup(h => h.mergeInterpreters(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(locatorsWithInterpreters.map(item => item.interpreters[0]))) - .verifiable(TypeMoq.Times.once()); - - await locator.getInterpreters(resource); - - locatorsWithInterpreters.forEach(item => item.locator.verifyAll()); - helper.verifyAll(); - }); - test(`Interpreter Sources are sorted correctly and merged ${testSuffix}`, async () => { - const locatorsTypes: string[] = []; - if (osType.value === OSType.Windows) { - locatorsTypes.push(WINDOWS_REGISTRY_SERVICE); - } - platformSvc.setup(p => p.osType).returns(() => osType.value); - platformSvc.setup(p => p.isWindows).returns(() => osType.value === OSType.Windows); - platformSvc.setup(p => p.isLinux).returns(() => osType.value === OSType.Linux); - platformSvc.setup(p => p.isMac).returns(() => osType.value === OSType.OSX); - - locatorsTypes.push(CONDA_ENV_SERVICE); - locatorsTypes.push(CONDA_ENV_FILE_SERVICE); - locatorsTypes.push(PIPENV_SERVICE); - locatorsTypes.push(GLOBAL_VIRTUAL_ENV_SERVICE); - locatorsTypes.push(WORKSPACE_VIRTUAL_ENV_SERVICE); - locatorsTypes.push(KNOWN_PATH_SERVICE); - locatorsTypes.push(CURRENT_PATH_SERVICE); - - const locatorsWithInterpreters = locatorsTypes.map(typeName => { - const interpreter: PythonInterpreter = { - architecture: Architecture.Unknown, - displayName: typeName, - path: typeName, - sysPrefix: typeName, - sysVersion: typeName, - type: InterpreterType.Unknown, - version: new SemVer('0.0.0-alpha') - }; - - const typeLocator = TypeMoq.Mock.ofType<IInterpreterLocatorService>(); - typeLocator - .setup(l => l.hasInterpreters) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - typeLocator - .setup(l => l.getInterpreters(TypeMoq.It.isValue(resource))) - .returns(() => Promise.resolve([interpreter])) - .verifiable(TypeMoq.Times.once()); - - serviceContainer - .setup(c => c.get(TypeMoq.It.isValue(IInterpreterLocatorService), TypeMoq.It.isValue(typeName))) - .returns(() => typeLocator.object); - - return { - type: typeName, - locator: typeLocator, - interpreters: [interpreter] - }; - }); - - const expectedInterpreters = locatorsWithInterpreters.map(item => item.interpreters[0]); - helper - .setup(h => h.mergeInterpreters(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(expectedInterpreters)) - .verifiable(TypeMoq.Times.once()); - - const interpreters = await locator.getInterpreters(resource); - - locatorsWithInterpreters.forEach(item => item.locator.verifyAll()); - helper.verifyAll(); - expect(interpreters).to.be.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>(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE)).thenReturn(watcher as any as IInterpreterWatcher); - - const item = await builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined); - - expect(item).to.be.equal(watcher, 'invalid'); - }); - test('Ensure we cache Workspace Virtual Env Watcher', async () => { - const workspaceService = mock(WorkspaceService); - const serviceContainer = mock(ServiceContainer); - const builder = new InterpreterWatcherBuilder(instance(workspaceService), instance(serviceContainer)); - const watcher = { register: () => Promise.resolve() }; - - when(workspaceService.getWorkspaceFolder(anything())).thenReturn(); - when(serviceContainer.get<IInterpreterWatcher>(IInterpreterWatcher, WORKSPACE_VIRTUAL_ENV_SERVICE)).thenReturn(watcher as any as IInterpreterWatcher); - - const [item1, item2, item3] = await Promise.all([ - builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), - builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined), - builder.getWorkspaceVirtualEnvInterpreterWatcher(undefined) - ]); - - expect(item1).to.be.equal(watcher, 'invalid'); - expect(item2).to.be.equal(watcher, 'invalid'); - expect(item3).to.be.equal(watcher, 'invalid'); - }); -}); diff --git a/src/test/interpreters/locators/progressService.unit.test.ts b/src/test/interpreters/locators/progressService.unit.test.ts deleted file mode 100644 index 1ce2cd8a9669..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<boolean> { - return Promise.resolve(true); - } - public locatingCallback?: (e: Promise<PythonInterpreter[]>) => any; - public onLocating(listener: (e: Promise<PythonInterpreter[]>) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable { - this.locatingCallback = listener; - return { dispose: noop }; - } - public getInterpreters(_resource?: Uri): Promise<PythonInterpreter[]> { - return Promise.resolve([]); - } - public dispose() { - noop(); - } - } - - test('Must raise refreshing event', async () => { - const serviceContainer = mock(ServiceContainer); - const locator = new Locator(); - when(serviceContainer.getAll(anything())).thenReturn([locator]); - const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); - progress.register(); - - let refreshingInvoked = false; - progress.onRefreshing(() => refreshingInvoked = true); - let refreshedInvoked = false; - progress.onRefreshed(() => refreshedInvoked = true); - - const locatingDeferred = createDeferred<PythonInterpreter[]>(); - locator.locatingCallback!.bind(progress)(locatingDeferred.promise); - expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); - expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); - }); - test('Must raise refreshed event', async () => { - const serviceContainer = mock(ServiceContainer); - const locator = new Locator(); - when(serviceContainer.getAll(anything())).thenReturn([locator]); - const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); - progress.register(); - - let refreshingInvoked = false; - progress.onRefreshing(() => refreshingInvoked = true); - let refreshedInvoked = false; - progress.onRefreshed(() => refreshedInvoked = true); - - const locatingDeferred = createDeferred<PythonInterpreter[]>(); - locator.locatingCallback!.bind(progress)(locatingDeferred.promise); - locatingDeferred.resolve(); - - await sleep(10); - expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); - expect(refreshedInvoked).to.be.equal(true, 'Refreshed not invoked'); - }); - test('Must raise refreshed event only when all locators have completed', async () => { - const serviceContainer = mock(ServiceContainer); - const locator1 = new Locator(); - const locator2 = new Locator(); - const locator3 = new Locator(); - when(serviceContainer.getAll(anything())).thenReturn([locator1, locator2, locator3]); - const progress = new InterpreterLocatorProgressService(instance(serviceContainer), []); - progress.register(); - - let refreshingInvoked = false; - progress.onRefreshing(() => refreshingInvoked = true); - let refreshedInvoked = false; - progress.onRefreshed(() => refreshedInvoked = true); - - const locatingDeferred1 = createDeferred<PythonInterpreter[]>(); - locator1.locatingCallback!.bind(progress)(locatingDeferred1.promise); - - const locatingDeferred2 = createDeferred<PythonInterpreter[]>(); - locator2.locatingCallback!.bind(progress)(locatingDeferred2.promise); - - const locatingDeferred3 = createDeferred<PythonInterpreter[]>(); - locator3.locatingCallback!.bind(progress)(locatingDeferred3.promise); - - locatingDeferred1.resolve(); - - await sleep(10); - expect(refreshingInvoked).to.be.equal(true, 'Refreshing Not invoked'); - expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); - - locatingDeferred2.resolve(); - - await sleep(10); - expect(refreshedInvoked).to.be.equal(false, 'Refreshed invoked'); - - locatingDeferred3.resolve(); - - await sleep(10); - expect(refreshedInvoked).to.be.equal(true, 'Refreshed not invoked'); - }); -}); diff --git a/src/test/interpreters/locators/workspaceVirtualEnvService.test.ts b/src/test/interpreters/locators/workspaceVirtualEnvService.test.ts deleted file mode 100644 index f5f0f9acd36b..000000000000 --- a/src/test/interpreters/locators/workspaceVirtualEnvService.test.ts +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any max-classes-per-file max-func-body-length no-invalid-this -import { expect } from 'chai'; -import { exec } from 'child_process'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import '../../../client/common/extensions'; -import { createDeferredFromPromise, Deferred } from '../../../client/common/utils/async'; -import { StopWatch } from '../../../client/common/utils/stopWatch'; -import { - IInterpreterLocatorService, - IInterpreterWatcherBuilder, - WORKSPACE_VIRTUAL_ENV_SERVICE -} from '../../../client/interpreter/contracts'; -import { WorkspaceVirtualEnvWatcherService } from '../../../client/interpreter/locators/services/workspaceVirtualEnvWatcherService'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { deleteFiles, getOSType, isPythonVersionInProcess, OSType, PYTHON_PATH, rootWorkspaceUri, waitForCondition } from '../../common'; -import { IS_MULTI_ROOT_TEST } from '../../constants'; -import { sleep } from '../../core'; -import { initialize, multirootPath } from '../../initialize'; - -const 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 manuallyTriggerFSWatcher(deferred: Deferred<void>) { - // Monitoring files on virtualized environments can be finicky... - // Lets trigger the fs watcher manually for the tests. - const stopWatch = new StopWatch(); - const builder = serviceContainer.get<IInterpreterWatcherBuilder>(IInterpreterWatcherBuilder); - const watcher = (await builder.getWorkspaceVirtualEnvInterpreterWatcher( - workspaceUri - )) as WorkspaceVirtualEnvWatcherService; - const binDir = getOSType() === OSType.Windows ? 'Scripts' : 'bin'; - const executable = getOSType() === OSType.Windows ? 'python.exe' : 'python'; - while (!deferred.completed && stopWatch.elapsedTime < timeoutMs - 10_000) { - const pythonPath = path.join(workspaceUri.fsPath, binDir, executable); - watcher.createHandler(Uri.file(pythonPath)).ignoreErrors(); - await sleep(1000); - } - } - async function waitForInterpreterToBeDetected(envNameToLookFor: string) { - const predicate = async () => { - const items = await locator.getInterpreters(workspaceUri); - return items.some(item => item.envName === envNameToLookFor); - }; - const promise = waitForCondition( - predicate, - timeoutMs, - `${envNameToLookFor}, Environment not detected in the workspace ${workspaceUri.fsPath}` - ); - const deferred = createDeferredFromPromise(promise); - manuallyTriggerFSWatcher(deferred).ignoreErrors(); - await deferred.promise; - } - async function createVirtualEnvironment(envSuffix: string) { - // Ensure env is random to avoid conflicts in tests (currupting test data). - const envName = `${venvPrefix}${envSuffix}${new Date().getTime().toString()}`; - return new Promise<string>((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 Python < 3, no venv support - if (await isPythonVersionInProcess(undefined, '2')) { - return this.skip(); - } - - serviceContainer = (await initialize()).serviceContainer; - locator = serviceContainer.get<IInterpreterLocatorService>( - IInterpreterLocatorService, - WORKSPACE_VIRTUAL_ENV_SERVICE - ); - // This test is required, we need to wait for interpreter listing completes, - // before proceeding with other tests. - await 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<IInterpreterWatcher[]> { - return super.getInterpreterWatchers(resource); - } - }(undefined as any, instance(serviceContainer), instance(builder)); - - const watchers = 1 as any; - when(builder.getWorkspaceVirtualEnvInterpreterWatcher(anything())).thenResolve(watchers); - - const items = await locator.getInterpreterWatchers(undefined); - - expect(items).to.deep.equal([watchers]); - verify(builder.getWorkspaceVirtualEnvInterpreterWatcher(anything())).once(); - }); -}); diff --git a/src/test/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 23ba558f68c2..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<string[]> { - 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<string | undefined | null> { - const items = this.values.find(item => { + public async getValue( + key: string, + hive: RegistryHive, + arch?: Architecture, + name?: string, + ): Promise<string | undefined | null> { + const items = this.values.find((item) => { if (item.key !== key || item.hive !== hive) { return false; } @@ -37,31 +43,15 @@ export class MockRegistry implements IRegistry { } } -// tslint:disable-next-line:max-classes-per-file -@injectable() -export class MockInterpreterVersionProvider implements IInterpreterVersionService { - constructor(private displayName: string, private useDefaultDisplayName: boolean = false, - private pipVersionPromise?: Promise<string>) { } - public async getVersion(_pythonPath: string, defaultDisplayName: string): Promise<string> { - return this.useDefaultDisplayName ? Promise.resolve(defaultDisplayName) : Promise.resolve(this.displayName); - } - public async getPipVersion(_pythonPath: string): Promise<string> { - // tslint:disable-next-line:no-non-null-assertion - return this.pipVersionPromise!; - } - // tslint:disable-next-line:no-empty - public dispose() { } -} - -// tslint:disable-next-line:no-any max-classes-per-file export class MockState implements IPersistentState<any> { - // tslint:disable-next-line:no-any - constructor(public data: any) { } - // tslint:disable-next-line:no-any + constructor(public data: any) {} + + public readonly storage = new MockMemento(); + get value(): any { return this.data; } - // tslint:disable-next-line:no-any + public async updateValue(data: any): Promise<void> { this.data = data; } diff --git a/src/test/interpreters/pipEnvService.unit.test.ts b/src/test/interpreters/pipEnvService.unit.test.ts deleted file mode 100644 index 7c7fe1a56052..000000000000 --- a/src/test/interpreters/pipEnvService.unit.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length no-any - -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as path from 'path'; -import { SemVer } from 'semver'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { IFileSystem, IPlatformService } from '../../client/common/platform/types'; -import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; -import { - IConfigurationService, - ICurrentProcess, - ILogger, - IPersistentState, - IPersistentStateFactory, - IPythonSettings -} from '../../client/common/types'; -import { getNamesAndValues } from '../../client/common/utils/enum'; -import { IEnvironmentVariablesProvider } from '../../client/common/variables/types'; -import { IInterpreterHelper } from '../../client/interpreter/contracts'; -import { PipEnvService } from '../../client/interpreter/locators/services/pipEnvService'; -import { PipEnvServiceHelper } from '../../client/interpreter/locators/services/pipEnvServiceHelper'; -import { IPipEnvServiceHelper } from '../../client/interpreter/locators/types'; -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: PipEnvService; - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; - let processService: TypeMoq.IMock<IProcessService>; - let currentProcess: TypeMoq.IMock<ICurrentProcess>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let persistentStateFactory: TypeMoq.IMock<IPersistentStateFactory>; - let envVarsProvider: TypeMoq.IMock<IEnvironmentVariablesProvider>; - let procServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; - let logger: TypeMoq.IMock<ILogger>; - let platformService: TypeMoq.IMock<IPlatformService>; - let config: TypeMoq.IMock<IConfigurationService>; - let settings: TypeMoq.IMock<IPythonSettings>; - let pipenvPathSetting: string; - let pipEnvServiceHelper: IPipEnvServiceHelper; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - processService = TypeMoq.Mock.ofType<IProcessService>(); - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - currentProcess = TypeMoq.Mock.ofType<ICurrentProcess>(); - persistentStateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - envVarsProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); - procServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - logger = TypeMoq.Mock.ofType<ILogger>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - pipEnvServiceHelper = mock(PipEnvServiceHelper); - processService.setup((x: any) => x.then).returns(() => undefined); - procServiceFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - - // tslint:disable-next-line:no-any - const persistentState = TypeMoq.Mock.ofType<IPersistentState<any>>(); - persistentStateFactory.setup(p => p.createGlobalPersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); - persistentStateFactory.setup(p => p.createWorkspacePersistentState(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => persistentState.object); - persistentState.setup(p => p.value).returns(() => undefined); - persistentState.setup(p => p.updateValue(TypeMoq.It.isAny())).returns(() => Promise.resolve()); - - const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(rootWorkspace)); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceService.setup(w => w.rootPath).returns(() => rootWorkspace); - - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory), TypeMoq.It.isAny())).returns(() => procServiceFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInterpreterHelper))).returns(() => interpreterHelper.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => currentProcess.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fileSystem.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPersistentStateFactory))).returns(() => persistentStateFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider))).returns(() => envVarsProvider.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger))).returns(() => logger.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))).returns(() => platformService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => config.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPipEnvServiceHelper), TypeMoq.It.isAny())).returns(() => instance(pipEnvServiceHelper)); - - when(pipEnvServiceHelper.trackWorkspaceFolder(anything(), anything())).thenResolve(); - config = TypeMoq.Mock.ofType<IConfigurationService>(); - settings = TypeMoq.Mock.ofType<IPythonSettings>(); - config.setup(c => c.getSettings(TypeMoq.It.isValue(undefined))).returns(() => settings.object); - settings.setup(p => p.pipenvPath).returns(() => pipenvPathSetting); - pipenvPathSetting = 'pipenv'; - - pipEnvService = new PipEnvService(serviceContainer.object); - }); - - test(`Should return an empty list'${testSuffix}`, () => { - const environments = pipEnvService.getInterpreters(resource); - expect(environments).to.be.eventually.deep.equal([]); - }); - test(`Should return an empty list if there is no \'PipFile\'${testSuffix}`, async () => { - const env = {}; - envVarsProvider.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())).returns(() => Promise.resolve({})).verifiable(TypeMoq.Times.once()); - currentProcess.setup(c => c.env).returns(() => env); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(false)).verifiable(TypeMoq.Times.once()); - const environments = await pipEnvService.getInterpreters(resource); - - expect(environments).to.be.deep.equal([]); - fileSystem.verifyAll(); - }); - test(`Should display warning message if there is a \'PipFile\' but \'pipenv --version\' fails ${testSuffix}`, async () => { - const env = {}; - currentProcess.setup(c => c.env).returns(() => env); - processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.reject('')); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)); - const warningMessage = 'Workspace contains Pipfile but \'pipenv\' was not found. Make sure \'pipenv\' is on the PATH.'; - appShell.setup(a => a.showWarningMessage(warningMessage)).returns(() => Promise.resolve('')).verifiable(TypeMoq.Times.once()); - 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\' fails with stderr ${testSuffix}`, async () => { - const env = {}; - currentProcess.setup(c => c.env).returns(() => env); - processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isValue(['--version']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stderr: '', stdout: 'pipenv, version 2018.11.26' })); - processService.setup(p => p.exec(TypeMoq.It.isValue('pipenv'), TypeMoq.It.isValue(['--venv']), TypeMoq.It.isAny())).returns(() => Promise.resolve({ stderr: 'Aborted!', stdout: '' })); - fileSystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(path.join(rootWorkspace, 'Pipfile')))).returns(() => Promise.resolve(true)); - const warningMessage = 'Workspace contains Pipfile but the associated virtual environment has not been setup. Setup the virtual environment manually if needed.'; - appShell.setup(a => a.showWarningMessage(warningMessage)).returns(() => Promise.resolve('')).verifiable(TypeMoq.Times.once()); - 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(); - }); - test('Must use \'python.pipenvPath\' setting', async () => { - pipenvPathSetting = 'spam-spam-pipenv-spam-spam'; - const pipenvExe = pipEnvService.executable; - assert.equal(pipenvExe, 'spam-spam-pipenv-spam-spam', 'Failed to identify pipenv.exe'); - }); - }); - }); -}); 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<IServiceContainer>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let updaterServiceFactory: IPythonPathUpdaterServiceFactory; - function setupMocks() { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - updaterServiceFactory = new PythonPathUpdaterServiceFactory(serviceContainer.object); - } - function setupConfigProvider(resource?: Uri): TypeMoq.IMock<WorkspaceConfiguration> { - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(w => w.getConfiguration(TypeMoq.It.isValue('python'), TypeMoq.It.isValue(resource))).returns(() => workspaceConfig.object); - return workspaceConfig; - } - suite('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<IServiceContainer>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let experimentsManager: TypeMoq.IMock<IExperimentService>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; + let updaterServiceFactory: IPythonPathUpdaterServiceFactory; + function setupMocks() { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(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/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 4c0836fb274d..000000000000 --- a/src/test/interpreters/venv.unit.test.ts +++ /dev/null @@ -1,159 +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'; - -// tslint:disable-next-line:no-require-imports no-var-requires -const untildify: (value: string) => string = require('untildify'); - -// tslint:disable-next-line: max-func-body-length -suite('Virtual environments', () => { - let serviceManager: ServiceManager; - let serviceContainer: ServiceContainer; - let settings: TypeMoq.IMock<IPythonSettings>; - let config: TypeMoq.IMock<IConfigurationService>; - let workspace: TypeMoq.IMock<IWorkspaceService>; - let process: TypeMoq.IMock<ICurrentProcess>; - let virtualEnvMgr: TypeMoq.IMock<IVirtualEnvironmentManager>; - - setup(() => { - const cont = new Container(); - serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - settings = TypeMoq.Mock.ofType<IPythonSettings>(); - config = TypeMoq.Mock.ofType<IConfigurationService>(); - workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - process = TypeMoq.Mock.ofType<ICurrentProcess>(); - virtualEnvMgr = TypeMoq.Mock.ofType<IVirtualEnvironmentManager>(); - - config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object); - serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance<ICurrentProcess>(ICurrentProcess, process.object); - serviceManager.addSingletonInstance<IVirtualEnvironmentManager>(IVirtualEnvironmentManager, virtualEnvMgr.object); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - }); - - test('Global search paths', async () => { - const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); - - const homedir = os.homedir(); - const folders = ['Envs', 'testpath']; - settings.setup(x => x.venvFolders).returns(() => folders); - virtualEnvMgr.setup(v => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - let paths = await pathProvider.getSearchPaths(); - let expected = [ - 'envs', - '.pyenv', - '.direnv', - '.virtualenvs', - ...folders].map(item => path.join(homedir, item)); - - virtualEnvMgr.verifyAll(); - expect(paths).to.deep.equal(expected, 'Global search folder list is incorrect.'); - - virtualEnvMgr.reset(); - virtualEnvMgr.setup(v => v.getPyEnvRoot(TypeMoq.It.isAny())).returns(() => Promise.resolve('pyenv_path')); - paths = await pathProvider.getSearchPaths(); - - virtualEnvMgr.verifyAll(); - expected = expected.concat(['pyenv_path', path.join('pyenv_path', 'versions')]); - expect(paths).to.deep.equal(expected, 'pyenv path not resolved correctly.'); - }); - - test('Global search paths with duplicates', async () => { - const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); - - const folders = ['.virtualenvs', '.direnv']; - settings.setup(x => x.venvFolders).returns(() => folders); - const paths = await pathProvider.getSearchPaths(); - - expect([...new Set(paths)]).to.deep.equal(paths, 'Duplicates are not removed from the list of global search paths'); - }); - - test('Global search paths with tilde path in the WORKON_HOME environment variable', async () => { - const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); - - const homedir = os.homedir(); - const workonFolder = path.join('~', '.workonFolder'); - process.setup(p => p.env).returns(() => { - return { WORKON_HOME: workonFolder }; - }); - settings.setup(x => x.venvFolders).returns(() => []); - - const paths = await pathProvider.getSearchPaths(); - const expected = [ - 'envs', - '.pyenv', - '.direnv', - '.virtualenvs' - ].map(item => path.join(homedir, item)); - expected.push(untildify(workonFolder)); - - expect(paths).to.deep.equal(expected, 'WORKON_HOME environment variable not read.'); - }); - - test('Global search paths with absolute path in the WORKON_HOME environment variable', async () => { - const pathProvider = new GlobalVirtualEnvironmentsSearchPathProvider(serviceContainer); - - const homedir = os.homedir(); - const workonFolder = path.join('path', 'to', '.workonFolder'); - process.setup(p => p.env).returns(() => { - return { WORKON_HOME: workonFolder }; - }); - settings.setup(x => x.venvFolders).returns(() => []); - - const paths = await pathProvider.getSearchPaths(); - const expected = [ - 'envs', - '.pyenv', - '.direnv', - '.virtualenvs' - ].map(item => path.join(homedir, item)); - expected.push(workonFolder); - - expect(paths).to.deep.equal(expected, 'WORKON_HOME environment variable not read.'); - }); - - test('Workspace search paths', async () => { - settings.setup(x => x.venvPath).returns(() => path.join('~', 'foo')); - - const wsRoot = TypeMoq.Mock.ofType<WorkspaceFolder>(); - wsRoot.setup(x => x.uri).returns(() => Uri.file('root')); - - const folder1 = TypeMoq.Mock.ofType<WorkspaceFolder>(); - folder1.setup(x => x.uri).returns(() => Uri.file('dir1')); - - workspace.setup(x => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsRoot.object); - workspace.setup(x => x.workspaceFolders).returns(() => [wsRoot.object, folder1.object]); - - const pathProvider = new WorkspaceVirtualEnvironmentsSearchPathProvider(serviceContainer); - const paths = await pathProvider.getSearchPaths(Uri.file('')); - - const homedir = os.homedir(); - const isWindows = new PlatformService(); - const fixCase = (item: string) => isWindows ? item.toUpperCase() : item; - const expected = [path.join(homedir, 'foo'), 'root', path.join('root', '.direnv')] - .map(item => Uri.file(item).fsPath) - .map(fixCase); - expect(paths.map(fixCase)).to.deep.equal(expected, 'Workspace venv folder search list does not match.'); - }); -}); diff --git a/src/test/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<IServiceContainer>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IPipEnvService))).returns(() => TypeMoq.Mock.ofType<IPipEnvService>().object); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - - const venvManager = new VirtualEnvironmentManager(serviceContainer.object); - const name = await venvManager.getEnvironmentName(pythonPath); - - expect(name).to.be.equal(virtualEnvFolderName); - }); - - test('Use workspacec name as env name', async () => { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const pipEnvService = TypeMoq.Mock.ofType<IPipEnvService>(); - pipEnvService - .setup(p => p.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => TypeMoq.Mock.ofType<IProcessServiceFactory>().object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IPipEnvService))).returns(() => pipEnvService.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IFileSystem))).returns(() => TypeMoq.Mock.ofType<IFileSystem>().object); - const workspaceUri = Uri.file(path.join('root', 'sub', 'wkspace folder')); - const workspaceFolder: WorkspaceFolder = { name: 'wkspace folder', index: 0, uri: workspaceUri }; - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => true); - workspaceService.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - - const venvManager = new VirtualEnvironmentManager(serviceContainer.object); - const name = await venvManager.getEnvironmentName(pythonPath); - - expect(name).to.be.equal(path.basename(workspaceUri.fsPath)); - pipEnvService.verifyAll(); - }); - - async function testSuffix(expectedEnvName: string, isPipEnvironment: boolean = false, resource?: Uri) { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => TypeMoq.Mock.ofType<IProcessServiceFactory>().object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IFileSystem))).returns(() => TypeMoq.Mock.ofType<IFileSystem>().object); - const pipEnvService = TypeMoq.Mock.ofType<IPipEnvService>(); - pipEnvService.setup(w => w.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => Promise.resolve(isPipEnvironment)); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IPipEnvService))).returns(() => pipEnvService.object); - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - workspaceService.setup(w => w.hasWorkspaceFolders).returns(() => false); - if (resource) { - const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolder.setup(w => w.uri).returns(() => resource); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - } - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - - const venvManager = new VirtualEnvironmentManager(serviceContainer.object); - - const name = await venvManager.getEnvironmentName(pythonPath, resource); - expect(name).to.be.equal(expectedEnvName, 'Virtual envrironment name suffix is incorrect.'); - } -}); diff --git a/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts new file mode 100644 index 000000000000..860970bd641e --- /dev/null +++ b/src/test/interpreters/virtualEnvs/activatedEnvLaunch.unit.test.ts @@ -0,0 +1,528 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as TypeMoq from 'typemoq'; +import { ConfigurationTarget, Uri, WorkspaceFolder } from 'vscode'; +import { IApplicationShell, IWorkspaceService } from '../../../client/common/application/types'; +import { ExecutionResult, IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; +import { Common } from '../../../client/common/utils/localize'; +import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { ActivatedEnvironmentLaunch } from '../../../client/interpreter/virtualEnvs/activatedEnvLaunch'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import { Conda } from '../../../client/pythonEnvironments/common/environmentManagers/conda'; + +suite('Activated Env Launch', async () => { + const uri = Uri.file('a'); + const condaPrefix = 'path/to/conda/env'; + const virtualEnvPrefix = 'path/to/virtual/env'; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let pythonPathUpdaterService: TypeMoq.IMock<IPythonPathUpdaterServiceManager>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let processServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let processService: TypeMoq.IMock<IProcessService>; + let activatedEnvLaunch: ActivatedEnvironmentLaunch; + let _promptIfApplicable: sinon.SinonStub; + + suite('Method selectIfLaunchedViaActivatedEnv()', () => { + const oldVSCodeCLI = process.env.VSCODE_CLI; + const oldCondaPrefix = process.env.CONDA_PREFIX; + const oldCondaShlvl = process.env.CONDA_SHLVL; + const oldVirtualEnv = process.env.VIRTUAL_ENV; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + pythonPathUpdaterService = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + _promptIfApplicable = sinon.stub(ActivatedEnvironmentLaunch.prototype, '_promptIfApplicable'); + _promptIfApplicable.returns(Promise.resolve()); + process.env.VSCODE_CLI = '1'; + }); + + teardown(() => { + if (oldCondaPrefix) { + process.env.CONDA_PREFIX = oldCondaPrefix; + } else { + delete process.env.CONDA_PREFIX; + } + if (oldCondaShlvl) { + process.env.CONDA_SHLVL = oldCondaShlvl; + } else { + delete process.env.CONDA_SHLVL; + } + if (oldVirtualEnv) { + process.env.VIRTUAL_ENV = oldVirtualEnv; + } else { + delete process.env.VIRTUAL_ENV; + } + if (oldVSCodeCLI) { + process.env.VSCODE_CLI = oldVSCodeCLI; + } else { + delete process.env.VSCODE_CLI; + } + sinon.restore(); + }); + + test('Updates interpreter path with the non-base conda prefix if activated', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Does not update interpreter path if VSCode is not launched via CLI', async () => { + delete process.env.VSCODE_CLI; + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path with the base conda prefix if activated and environment var is configured to not auto activate it', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + process.env.CONDA_AUTO_ACTIVATE_BASE = 'false'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path with the base conda prefix if activated and environment var is configured to auto activate it', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + process.env.CONDA_AUTO_ACTIVATE_BASE = 'true'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + expect(_promptIfApplicable.calledOnce).to.equal(true, 'Prompt not displayed'); + }); + + test('Updates interpreter path with virtual env prefix if activated', async () => { + process.env.VIRTUAL_ENV = virtualEnvPrefix; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(virtualEnvPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(virtualEnvPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + }); + + test('Updates interpreter path in global scope if no workspace is opened', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'env' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + workspaceService.setup((w) => w.workspaceFolders).returns(() => []); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.Global), + TypeMoq.It.isValue('load'), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(condaPrefix, 'Incorrect value'); + pythonPathUpdaterService.verifyAll(); + expect(_promptIfApplicable.notCalled).to.equal(true, 'Prompt should not be displayed'); + }); + + test('Returns `undefined` if env was already selected', async () => { + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + true, + ); + const result = await activatedEnvLaunch.selectIfLaunchedViaActivatedEnv(); + expect(result).to.be.equal(undefined, 'Incorrect value'); + }); + }); + + suite('Method _promptIfApplicable()', () => { + const oldCondaPrefix = process.env.CONDA_PREFIX; + const oldCondaShlvl = process.env.CONDA_SHLVL; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo]; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + pythonPathUpdaterService = TypeMoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((p) => (p as any).then).returns(() => undefined); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + }); + + teardown(() => { + if (oldCondaPrefix) { + process.env.CONDA_PREFIX = oldCondaPrefix; + } else { + delete process.env.CONDA_PREFIX; + } + if (oldCondaShlvl) { + process.env.CONDA_SHLVL = oldCondaShlvl; + } else { + delete process.env.CONDA_SHLVL; + } + sinon.restore(); + }); + + test('Shows prompt if base conda environment is activated and auto activate configuration is disabled', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.once()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('If user chooses yes, update interpreter path', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + pythonPathUpdaterService.verifyAll(); + }); + + test('If user chooses no, do not update interpreter path', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + pythonPathUpdaterService + .setup((p) => + p.updatePythonPath( + TypeMoq.It.isValue(condaPrefix), + TypeMoq.It.isValue(ConfigurationTarget.WorkspaceFolder), + TypeMoq.It.isValue('load'), + TypeMoq.It.isValue(uri), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelNo)); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + pythonPathUpdaterService.verifyAll(); + }); + + test('Do not show prompt if base conda environment is activated but auto activate configuration is enabled', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base True' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('Do not show prompt if non-base conda environment is activated', async () => { + process.env.CONDA_PREFIX = condaPrefix; + process.env.CONDA_SHLVL = '1'; + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'nonbase' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + + test('Do not show prompt if conda environment is not activated', async () => { + interpreterService + .setup((i) => i.getInterpreterDetails(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ envName: 'base' } as unknown) as PythonEnvironment)); + workspaceService.setup((w) => w.workspaceFile).returns(() => undefined); + const workspaceFolder: WorkspaceFolder = { name: 'one', uri, index: 0 }; + workspaceService.setup((w) => w.workspaceFolders).returns(() => [workspaceFolder]); + appShell + .setup((a) => a.showInformationMessage(TypeMoq.It.isAny(), ...prompts)) + .returns(() => Promise.resolve(Common.bannerLabelYes)) + .verifiable(TypeMoq.Times.never()); + processService + .setup((p) => p.shellExec('conda config --get auto_activate_base')) + .returns(() => + Promise.resolve(({ stdout: '--set auto_activate_base False' } as unknown) as ExecutionResult< + string + >), + ); + activatedEnvLaunch = new ActivatedEnvironmentLaunch( + workspaceService.object, + appShell.object, + pythonPathUpdaterService.object, + interpreterService.object, + processServiceFactory.object, + ); + await activatedEnvLaunch._promptIfApplicable(); + appShell.verifyAll(); + }); + }); +}); diff --git a/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts b/src/test/interpreters/virtualEnvs/condaInheritEnvPrompt.unit.test.ts 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<IWorkspaceService>; + let appShell: TypeMoq.IMock<IApplicationShell>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let platformService: TypeMoq.IMock<IPlatformService>; + let applicationEnvironment: TypeMoq.IMock<IApplicationEnvironment>; + let persistentStateFactory: IPersistentStateFactory; + let notificationPromptEnabled: TypeMoq.IMock<IPersistentState<any>>; + let condaInheritEnvPrompt: CondaInheritEnvPrompt; + function verifyAll() { + workspaceService.verifyAll(); + appShell.verifyAll(); + interpreterService.verifyAll(); + } + + suite('Method shouldShowPrompt()', () => { + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + 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<WorkspaceConfiguration>(); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(undefined) as any) + .verifiable(TypeMoq.Times.never()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); + verifyAll(); + }); + test('Returns false if 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<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if no active interpreter is present', async () => { + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + test('Returns false if settings returned is `undefined`', async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((ws) => ws.inspect<boolean>('integrated.inheritEnv')) + .returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + [ + { + name: 'Returns false if globalValue `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: true, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }, + }, + { + name: 'Returns false if workspaceValue of `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: undefined, + workspaceValue: true, + workspaceFolderValue: undefined, + }, + }, + { + name: 'Returns false if workspaceFolderValue of `terminal.integrated.inheritEnv` setting is set', + settings: { + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: false, + }, + }, + ].forEach((testParams) => { + test(testParams.name, async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig + .setup((ws) => ws.inspect<boolean>('integrated.inheritEnv')) + .returns(() => testParams.settings as any); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(false, 'Prompt should not be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(false, 'Should be false'); + verifyAll(); + }); + }); + test('Returns true otherwise', async () => { + const interpreter = { + envType: EnvironmentType.Conda, + }; + const settings = { + globalValue: undefined, + workspaceValue: undefined, + workspaceFolderValue: undefined, + }; + const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + platformService + .setup((ps) => ps.isWindows) + .returns(() => false) + .verifiable(TypeMoq.Times.once()); + interpreterService + .setup((is) => is.getActiveInterpreter(resource)) + .returns(() => Promise.resolve(interpreter) as any) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal', resource)) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.once()); + workspaceConfig.setup((ws) => ws.inspect<boolean>('integrated.inheritEnv')).returns(() => settings as any); + const result = await condaInheritEnvPrompt.shouldShowPrompt(resource); + expect(result).to.equal(true, 'Prompt should be shown'); + expect(condaInheritEnvPrompt.hasPromptBeenShownInCurrentSession).to.equal(true, 'Should be true'); + verifyAll(); + }); + }); + suite('Method activate()', () => { + let initializeInBackground: sinon.SinonStub<any>; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Invokes initializeInBackground() in the background', async () => { + const initializeInBackgroundDeferred = createDeferred<void>(); + initializeInBackground = sinon.stub(CondaInheritEnvPrompt.prototype, 'initializeInBackground'); + initializeInBackground.callsFake(() => initializeInBackgroundDeferred.promise); + condaInheritEnvPrompt = new CondaInheritEnvPrompt( + interpreterService.object, + workspaceService.object, + 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<any>; + let promptAndUpdate: sinon.SinonStub<any>; + setup(() => { + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + }); + + teardown(() => { + 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<IWorkspaceService>(); + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + persistentStateFactory = mock(PersistentStateFactory); + notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<any>>(); + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + applicationEnvironment = TypeMoq.Mock.ofType<IApplicationEnvironment>(); + applicationEnvironment.setup((a) => a.remoteName).returns(() => undefined); + when(persistentStateFactory.createGlobalPersistentState(condaInheritEnvPromptKey, true)).thenReturn( + notificationPromptEnabled.object, + ); + 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<WorkspaceConfiguration>(); + notificationPromptEnabled + .setup((n) => n.value) + .returns(() => true) + .verifiable(TypeMoq.Times.once()); + appShell + .setup((a) => a.showInformationMessage(Interpreters.condaInheritEnvMessage, ...prompts)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + workspaceService + .setup((ws) => ws.getConfiguration('terminal')) + .returns(() => workspaceConfig.object) + .verifiable(TypeMoq.Times.never()); + workspaceConfig + .setup((wc) => wc.update('integrated.inheritEnv', false, ConfigurationTarget.Global)) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + notificationPromptEnabled + .setup((n) => n.updateValue(false)) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + 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<WorkspaceConfiguration>(); + 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<WorkspaceConfiguration>(); + 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<ICurrentProcess>; - let processService: TypeMoq.IMock<IProcessService>; - let pathUtils: TypeMoq.IMock<IPathUtils>; - let virtualEnvMgr: VirtualEnvironmentManager; - let fs: TypeMoq.IMock<IFileSystem>; - let workspace: TypeMoq.IMock<IWorkspaceService>; - let pipEnvService: TypeMoq.IMock<IPipEnvService>; - let terminalActivation: TypeMoq.IMock<ITerminalActivationCommandProvider>; - let platformService: TypeMoq.IMock<IPlatformService>; - - setup(() => { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - process = TypeMoq.Mock.ofType<ICurrentProcess>(); - processService = TypeMoq.Mock.ofType<IProcessService>(); - const processFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); - fs = TypeMoq.Mock.ofType<IFileSystem>(); - workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - pipEnvService = TypeMoq.Mock.ofType<IPipEnvService>(); - terminalActivation = TypeMoq.Mock.ofType<ITerminalActivationCommandProvider>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - - processService.setup(p => (p as any).then).returns(() => undefined); - processFactory.setup(p => p.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService.object)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IProcessServiceFactory))).returns(() => processFactory.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ICurrentProcess))).returns(() => process.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPathUtils))).returns(() => pathUtils.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))).returns(() => fs.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))).returns(() => workspace.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPipEnvService))).returns(() => pipEnvService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalActivationCommandProvider), TypeMoq.It.isAny())).returns(() => terminalActivation.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())).returns(() => platformService.object); - - virtualEnvMgr = new VirtualEnvironmentManager(serviceContainer.object); - }); - - test('Get PyEnv Root from PYENV_ROOT', async () => { - process - .setup(p => p.env) - .returns(() => { return { PYENV_ROOT: 'yes' }; }) - .verifiable(TypeMoq.Times.once()); - - const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); - - process.verifyAll(); - expect(pyenvRoot).to.equal('yes'); - }); - - test('Get PyEnv Root from current PYENV_ROOT', async () => { - process - .setup(p => p.env) - .returns(() => { return {}; }) - .verifiable(TypeMoq.Times.once()); - processService - .setup(p => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root']))) - .returns(() => Promise.resolve({ stdout: 'PROC' })) - .verifiable(TypeMoq.Times.once()); - - const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); - - process.verifyAll(); - processService.verifyAll(); - expect(pyenvRoot).to.equal('PROC'); - }); - - test('Get default PyEnv Root path', async () => { - process - .setup(p => p.env) - .returns(() => { return {}; }) - .verifiable(TypeMoq.Times.once()); - processService - .setup(p => p.exec(TypeMoq.It.isValue('pyenv'), TypeMoq.It.isValue(['root']))) - .returns(() => Promise.resolve({ stdout: '', stderr: 'err' })) - .verifiable(TypeMoq.Times.once()); - pathUtils - .setup(p => p.home) - .returns(() => 'HOME') - .verifiable(TypeMoq.Times.once()); - const pyenvRoot = await virtualEnvMgr.getPyEnvRoot(); - - process.verifyAll(); - processService.verifyAll(); - expect(pyenvRoot).to.equal(path.join('HOME', '.pyenv')); - }); - - test('Get Environment Type, detects venv', async () => { - const pythonPath = path.join('a', 'b', 'c', 'python'); - const dir = path.dirname(pythonPath); - - fs.setup(f => f.fileExists(TypeMoq.It.isValue(path.join(dir, 'pyvenv.cfg')))) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const isRecognized = await virtualEnvMgr.isVenvEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(true, 'invalid value'); - fs.verifyAll(); - }); - test('Get Environment Type, does not detect venv incorrectly', async () => { - const pythonPath = path.join('a', 'b', 'c', 'python'); - const dir = path.dirname(pythonPath); - - fs.setup(f => f.fileExists(TypeMoq.It.isValue(path.join(dir, 'pyvenv.cfg')))) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const isRecognized = await virtualEnvMgr.isVenvEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(false, 'invalid value'); - fs.verifyAll(); - }); - - test('Get Environment Type, detects pyenv', async () => { - const pythonPath = path.join('py-env-root', 'b', 'c', 'python'); - - process.setup(p => p.env) - .returns(() => { - return { PYENV_ROOT: path.join('py-env-root', 'b') }; - }) - .verifiable(TypeMoq.Times.once()); - - const isRecognized = await virtualEnvMgr.isPyEnvEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(true, 'invalid value'); - process.verifyAll(); - }); - - test('Get Environment Type, does not detect pyenv incorrectly', async () => { - const pythonPath = path.join('a', 'b', 'c', 'python'); - - process.setup(p => p.env) - .returns(() => { - return { PYENV_ROOT: path.join('py-env-root', 'b') }; - }) - .verifiable(TypeMoq.Times.once()); - - const isRecognized = await virtualEnvMgr.isPyEnvEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(false, 'invalid value'); - process.verifyAll(); - }); - - test('Get Environment Type, detects pipenv', async () => { - const pythonPath = path.join('x', 'b', 'c', 'python'); - workspace - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - const ws = [{ uri: Uri.file('x') }]; - workspace - .setup(w => w.workspaceFolders) - .returns(() => ws as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - pipEnvService - .setup(p => p.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.once()); - - const isRecognized = await virtualEnvMgr.isPipEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(true, 'invalid value'); - workspace.verifyAll(); - pipEnvService.verifyAll(); - }); - - test('Get Environment Type, does not detect pipenv incorrectly', async () => { - const pythonPath = path.join('x', 'b', 'c', 'python'); - workspace - .setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.atLeastOnce()); - const ws = [{ uri: Uri.file('x') }]; - workspace - .setup(w => w.workspaceFolders) - .returns(() => ws as any) - .verifiable(TypeMoq.Times.atLeastOnce()); - pipEnvService - .setup(p => p.isRelatedPipEnvironment(TypeMoq.It.isAny(), TypeMoq.It.isValue(pythonPath))) - .returns(() => Promise.resolve(false)) - .verifiable(TypeMoq.Times.once()); - - const isRecognized = await virtualEnvMgr.isPipEnvironment(pythonPath); - - expect(isRecognized).to.be.equal(false, 'invalid value'); - workspace.verifyAll(); - pipEnvService.verifyAll(); - }); - - for (const isWindows of [true, false]) { - const testTitleSuffix = `(${isWindows ? 'On Windows' : 'Non-Windows'}})`; - test(`Get Environment Type, detects virtualenv ${testTitleSuffix}`, async () => { - const pythonPath = path.join('x', 'b', 'c', 'python'); - 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 index d6f64a4dfb5f..2ad67831c455 100644 --- a/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts +++ b/src/test/interpreters/virtualEnvs/virtualEnvPrompt.unit.test.ts @@ -3,166 +3,243 @@ 'use strict'; -import { expect } from 'chai'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { ConfigurationTarget, Disposable, Uri, WorkspaceConfiguration } from 'vscode'; +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, IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; +import { IApplicationShell } from '../../../client/common/application/types'; import { PersistentStateFactory } from '../../../client/common/persistentState'; import { IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { Common, InteractiveShiftEnterBanner } from '../../../client/common/utils/localize'; +import { Common } from '../../../client/common/utils/localize'; import { PythonPathUpdaterService } from '../../../client/interpreter/configuration/pythonPathUpdaterService'; import { IPythonPathUpdaterServiceManager } from '../../../client/interpreter/configuration/types'; -import { IInterpreterHelper, IInterpreterLocatorService, IInterpreterWatcherBuilder, PythonInterpreter } from '../../../client/interpreter/contracts'; +import { IComponentAdapter, IInterpreterHelper, IInterpreterService } from '../../../client/interpreter/contracts'; import { InterpreterHelper } from '../../../client/interpreter/helpers'; -import { CacheableLocatorService } from '../../../client/interpreter/locators/services/cacheableLocatorService'; -import { InterpreterWatcherBuilder } from '../../../client/interpreter/locators/services/interpreterWatcherBuilder'; import { VirtualEnvironmentPrompt } from '../../../client/interpreter/virtualEnvs/virtualEnvPrompt'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as createEnvApi from '../../../client/pythonEnvironments/creation/createEnvApi'; -// tslint:disable-next-line:max-func-body-length suite('Virtual Environment Prompt', () => { class VirtualEnvironmentPromptTest extends VirtualEnvironmentPrompt { - // tslint:disable-next-line:no-unnecessary-override public async handleNewEnvironment(resource: Uri): Promise<void> { await super.handleNewEnvironment(resource); } - // tslint:disable-next-line:no-unnecessary-override - public async notifyUser(interpreter: PythonInterpreter, resource: Uri): Promise<void> { + + public async notifyUser(interpreter: PythonEnvironment, resource: Uri): Promise<void> { await super.notifyUser(interpreter, resource); } - // tslint:disable-next-line:no-unnecessary-override - public hasUserDefinedPythonPath(resource: Uri) { - return super.hasUserDefinedPythonPath(resource); - } } - let builder: IInterpreterWatcherBuilder; - let workspaceService: IWorkspaceService; let persistentStateFactory: IPersistentStateFactory; let helper: IInterpreterHelper; let pythonPathUpdaterService: IPythonPathUpdaterServiceManager; - let locator: IInterpreterLocatorService; let disposable: Disposable; let appShell: IApplicationShell; + let componentAdapter: IComponentAdapter; + let interpreterService: IInterpreterService; let environmentPrompt: VirtualEnvironmentPromptTest; + let isCreatingEnvironmentStub: sinon.SinonStub; setup(() => { - workspaceService = mock(WorkspaceService); - builder = mock(InterpreterWatcherBuilder); persistentStateFactory = mock(PersistentStateFactory); helper = mock(InterpreterHelper); pythonPathUpdaterService = mock(PythonPathUpdaterService); - locator = mock(CacheableLocatorService); + componentAdapter = mock<IComponentAdapter>(); + interpreterService = mock<IInterpreterService>(); + isCreatingEnvironmentStub = sinon.stub(createEnvApi, 'isCreatingEnvironment'); + isCreatingEnvironmentStub.returns(false); + when(interpreterService.getActiveInterpreter(anything())).thenResolve(({ + id: 'selected', + path: 'path/to/selected', + } as unknown) as PythonEnvironment); disposable = mock(Disposable); appShell = mock(ApplicationShell); environmentPrompt = new VirtualEnvironmentPromptTest( - instance(builder), instance(persistentStateFactory), - instance(workspaceService), instance(helper), instance(pythonPathUpdaterService), - instance(locator), [instance(disposable)], - instance(appShell) + instance(appShell), + instance(componentAdapter), + instance(interpreterService), ); }); - test('User is not notified if python path is specified in settings.json', async () => { + + 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 settings = { workspaceFolderValue: 'path/to/interpreter1' }; - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - // tslint:disable:no-any - when(locator.getInterpreters(resource)).thenResolve([interpreter1, interpreter2] as any); - when(helper.getBestInterpreter(anything())).thenReturn(interpreter2 as any); - when(workspaceService.getConfiguration('python', resource)).thenReturn(workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<string>('pythonPath')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + + when(componentAdapter.getWorkspaceVirtualEnvInterpreters(resource)).thenResolve([ + interpreter1, + interpreter2, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(helper.getBestInterpreter(deepEqual([interpreter1, interpreter2] as any))).thenReturn(interpreter2 as any); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object, + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(); await environmentPrompt.handleNewEnvironment(resource); - verify(locator.getInterpreters(resource)).once(); - verify(helper.getBestInterpreter(anything())).once(); - verify(workspaceService.getConfiguration('python', resource)).once(); - workspaceConfig.verifyAll(); + verify(appShell.showInformationMessage(anything(), ...prompts)).once(); }); + test('User is not notified if currently selected interpreter is the same as new interpreter', async () => { + const resource = Uri.file('a'); + const interpreter1 = { path: 'path/to/interpreter1' }; + const interpreter2 = { path: 'path/to/interpreter2' }; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; + const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); + + // Return interpreters using the component adapter instead + when(componentAdapter.getWorkspaceVirtualEnvInterpreters(resource)).thenResolve([ + interpreter1, + interpreter2, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(helper.getBestInterpreter(deepEqual([interpreter1, interpreter2] as any))).thenReturn(interpreter2 as any); + reset(interpreterService); + when(interpreterService.getActiveInterpreter(anything())).thenResolve( + (interpreter2 as unknown) as PythonEnvironment, + ); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object, + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); + when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(); + + await environmentPrompt.handleNewEnvironment(resource); + + verify(appShell.showInformationMessage(anything(), ...prompts)).never(); + }); test('User is notified if interpreter exists and only python path to global interpreter is specified in settings', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; const interpreter2 = { path: 'path/to/interpreter2' }; - const settings = { workspaceFolderValue: 'python', globalValue: 'path/to/globalInterpreter' }; - const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Common.doNotShowAgain()]; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - // tslint:disable:no-any - when(locator.getInterpreters(resource)).thenResolve([interpreter1, interpreter2] as any); - when(helper.getBestInterpreter(anything())).thenReturn(interpreter2 as any); - when(workspaceService.getConfiguration('python', resource)).thenReturn(workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<string>('pythonPath')) - .returns(() => settings as any) - .verifiable(TypeMoq.Times.once()); - when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn(notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => true); + + // Return interpreters using the component adapter instead + when(componentAdapter.getWorkspaceVirtualEnvInterpreters(resource)).thenResolve([ + interpreter1, + interpreter2, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ] as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + when(helper.getBestInterpreter(deepEqual([interpreter1, interpreter2] as any))).thenReturn(interpreter2 as any); + when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn( + notificationPromptEnabled.object, + ); + notificationPromptEnabled.setup((n) => n.value).returns(() => true); when(appShell.showInformationMessage(anything(), ...prompts)).thenResolve(); await environmentPrompt.handleNewEnvironment(resource); - verify(locator.getInterpreters(resource)).once(); - verify(helper.getBestInterpreter(anything())).once(); - verify(workspaceService.getConfiguration('python', resource)).once(); - workspaceConfig.verifyAll(); - verify(persistentStateFactory.createWorkspacePersistentState(anything(), true)).once(); verify(appShell.showInformationMessage(anything(), ...prompts)).once(); }); - test('If user selects \'Yes\', python path is updated', async () => { + test("If user selects 'Yes', python path is updated", async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Common.doNotShowAgain()]; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn(notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => true); + 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(); + 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(); + verify( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource, + ), + ).once(); }); - test('If user selects \'No\', no operation is performed', async () => { + test("If user selects 'No', no operation is performed", async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Common.doNotShowAgain()]; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn(notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => true); + 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()); + 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(); + verify( + pythonPathUpdaterService.updatePythonPath( + interpreter1.path, + ConfigurationTarget.WorkspaceFolder, + 'ui', + resource, + ), + ).never(); notificationPromptEnabled.verifyAll(); }); - test('If user selects \'Do not show again\', prompt is disabled', async () => { + test('If user selects "Don\'t show again", prompt is disabled', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Common.doNotShowAgain()]; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn(notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => true); + 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()); + 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(); @@ -173,72 +250,32 @@ suite('Virtual Environment Prompt', () => { test('If prompt is disabled, no notification is shown', async () => { const resource = Uri.file('a'); const interpreter1 = { path: 'path/to/interpreter1' }; - const prompts = [InteractiveShiftEnterBanner.bannerLabelYes(), InteractiveShiftEnterBanner.bannerLabelNo(), Common.doNotShowAgain()]; + const prompts = [Common.bannerLabelYes, Common.bannerLabelNo, Common.doNotShowAgain]; const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - when(persistentStateFactory.createWorkspacePersistentState(anything(), true)).thenReturn(notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => false); + 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(); }); - const testsForHasUserDefinedPath = - [ - { - testName: 'Returns false when workspace folder setting equals \'python\'', - settings: { workspaceFolderValue: 'python' }, - expectedResult: false - }, - { - testName: 'Returns true when interpreter is provided in workspace folder setting', - settings: { workspaceFolderValue: 'path/to/interpreter' }, - expectedResult: true - }, - { - testName: 'Returns false when workspace setting equals \'python\'', - settings: { workspaceValue: 'python' }, - expectedResult: false - }, - { - testName: 'Returns true when interpreter is provided in workspace setting', - settings: { workspaceValue: 'path/to/interpreter' }, - expectedResult: true - }, - { - testName: 'Returns false when global setting equals \'python\'', - settings: { globalValue: 'python' }, - expectedResult: false - }, - { - testName: 'Returns false when interpreter is provided in global setting', - settings: { globalValue: 'path/to/interpreter' }, - expectedResult: false - }, - { - testName: 'Returns false when no python setting is provided', - settings: {}, - expectedResult: false - } - ]; - - suite('Function hasUserDefinedPythonPath()', () => { - testsForHasUserDefinedPath.forEach(testParams => { - test(testParams.testName, async () => { - const resource = Uri.parse('a'); - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - when(workspaceService.getConfiguration('python', resource)).thenReturn(workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<string>('pythonPath')) - .returns(() => testParams.settings as any) - .verifiable(TypeMoq.Times.once()); - - expect(environmentPrompt.hasUserDefinedPythonPath(resource)).to.equal(testParams.expectedResult); - - verify(workspaceService.getConfiguration('python', resource)).once(); - workspaceConfig.verifyAll(); - }); - }); + 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<IServiceContainer>; - let interpreterHelper: TypeMoq.IMock<IInterpreterHelper>; - let platformService: TypeMoq.IMock<IPlatformService>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const stateFactory = TypeMoq.Mock.ofType<IPersistentStateFactory>(); - interpreterHelper = TypeMoq.Mock.ofType<IInterpreterHelper>(); - const pathUtils = TypeMoq.Mock.ofType<IPathUtils>(); - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - 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: <any>undefined }, - - { key: '\\Software\\Python\\Company Three\\6.0.0\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') }, - - { key: '\\Software\\Python\\Company A\\10.0.0\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'conda', 'envs', 'numpy') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, setup64Bit(false), serviceContainer.object); - 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: <any>undefined }, - - { key: '\\Software\\Python\\Company Three\\Tag !\\InstallPath', hive: RegistryHive.HKCU, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') }, - - { key: '\\Software\\Python\\Company A\\Another Tag\\InstallPath', hive: RegistryHive.HKLM, arch: Architecture.x86, value: path.join(environmentsPath, 'non-existent-path', 'envs', 'numpy') } - ]; - const registry = new MockRegistry(registryKeys, registryValues); - const winRegistry = new WindowsRegistryService(registry, setup64Bit(false), serviceContainer.object); - 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<IApplicationShell>(); + commandManager = mock<ICommandManager>(); + disposables = mock<IDisposableRegistry>(); + + requireJupyterPrompt = new RequireJupyterPrompt( + instance(appShell), + instance(commandManager), + instance(disposables), + ); + }); + + test('Activation registers command', async () => { + await requireJupyterPrompt.activate(); + + verify(commandManager.registerCommand(Commands.InstallJupyter, anything())).once(); + }); + + test('Show prompt with Yes selection installs Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelYes)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).once(); + }); + + test('Show prompt with No selection does not install Jupyter extension', async () => { + when( + appShell.showInformationMessage(Interpreters.requireJupyter, Common.bannerLabelYes, Common.bannerLabelNo), + ).thenReturn(Promise.resolve(Common.bannerLabelNo)); + + await requireJupyterPrompt.activate(); + await requireJupyterPrompt._showPrompt(); + + verify( + commandManager.executeCommand('workbench.extensions.installExtension', JUPYTER_EXTENSION_ID, undefined), + ).never(); + }); +}); diff --git a/src/test/language/characterStream.test.ts b/src/test/language/characterStream.test.ts deleted file mode 100644 index 1dfb1e91e4e2..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 } 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 99e308d2a691..000000000000 --- a/src/test/language/textRange.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as assert from 'assert'; -import { TextRange } from '../../client/language/types'; - -// tslint:disable-next-line:max-func-body-length -suite('Language.TextRange', () => { - test('Empty static', async () => { - const e = TextRange.empty; - assert.equal(e.start, 0); - assert.equal(e.end, 0); - assert.equal(e.length, 0); - }); - test('Construction', async () => { - let r = new TextRange(10, 20); - assert.equal(r.start, 10); - assert.equal(r.end, 30); - assert.equal(r.length, 20); - r = new TextRange(10, 0); - assert.equal(r.start, 10); - assert.equal(r.end, 10); - assert.equal(r.length, 0); - }); - test('From bounds', async () => { - let r = TextRange.fromBounds(7, 9); - assert.equal(r.start, 7); - assert.equal(r.end, 9); - assert.equal(r.length, 2); - - r = TextRange.fromBounds(5, 5); - assert.equal(r.start, 5); - assert.equal(r.end, 5); - assert.equal(r.length, 0); - }); - test('Contains', async () => { - const r = TextRange.fromBounds(7, 9); - assert.equal(r.contains(-1), false); - assert.equal(r.contains(6), false); - assert.equal(r.contains(7), true); - assert.equal(r.contains(8), true); - assert.equal(r.contains(9), false); - assert.equal(r.contains(10), false); - }); - test('Exceptions', async () => { - assert.throws( - () => { - // @ts-ignore - const e = new TextRange(0, -1); - }, - Error - ); - assert.throws( - () => { - // @ts-ignore - const e = TextRange.fromBounds(3, 1); - }, - Error - ); - }); -}); diff --git a/src/test/language/textRangeCollection.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<void> = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise<void>) => { + onDidChangeConfigListener = listener; + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeConfigListener({ affectsConfiguration: () => false }); + + // Check that startLanguageServer was only called once: When we called it above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('When the config settings change, and the python.languageServer setting is affected, the watcher should restart the language server', async () => { + let onDidChangeConfigListener: (event: ConfigurationChangeEvent) => Promise<void> = () => Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: (listener: (event: ConfigurationChangeEvent) => Promise<void>) => { + onDidChangeConfigListener = listener; + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + workspaceFolders: [{ uri: Uri.parse('workspace') }], + } as unknown) as IWorkspaceService; + + const getSettingsStub = sandbox.stub(); + getSettingsStub.onFirstCall().returns({ languageServer: LanguageServerType.None }); + getSettingsStub.onSecondCall().returns({ languageServer: LanguageServerType.Node }); + + const configService = ({ + getSettings: getSettingsStub, + } as unknown) as IConfigurationService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + configService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => 'python', + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + // Use a fake here so we don't actually start up language servers. + const startLanguageServerFake = sandbox.fake.resolves(undefined); + sandbox.replace(watcher, 'startLanguageServer', startLanguageServerFake); + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeConfigListener({ affectsConfiguration: () => true }); + + // Check that startLanguageServer was called twice: When we called it above, and implicitly because of the event. + assert.ok(startLanguageServerFake.calledTwice); + }); + + test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is Jedi, do not instantiate a language server', async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + await watcher.startLanguageServer(LanguageServerType.Jedi); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test('When starting a language server with a Python 2.7 interpreter and the python.languageServer setting is default, use Pylance', async () => { + const startLanguageServerStub = sandbox.stub(PylanceLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + sandbox.stub(PylanceLSExtensionManager.prototype, 'canStartLanguageServer').returns(true); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ + languageServer: LanguageServerType.Jedi, + languageServerIsDefault: true, + }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(LanguageServerType.Node); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + test('When starting a language server in an untrusted workspace with Jedi, do not instantiate a language server', async () => { + const startLanguageServerStub = sandbox.stub(NoneLSExtensionManager.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.Jedi }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 2, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: false, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(LanguageServerType.Jedi); + + assert.ok(startLanguageServerStub.calledOnce); + }); + + [ + { + languageServer: LanguageServerType.Jedi, + multiLS: true, + extensionLSCls: JediLSExtensionManager, + lsManagerCls: JediLanguageServerManager, + }, + { + languageServer: LanguageServerType.Node, + multiLS: false, + extensionLSCls: PylanceLSExtensionManager, + lsManagerCls: NodeLanguageServerManager, + }, + { + languageServer: LanguageServerType.None, + multiLS: false, + extensionLSCls: NoneLSExtensionManager, + lsManagerCls: undefined, + }, + ].forEach(({ languageServer, multiLS, extensionLSCls, lsManagerCls }) => { + test(`When starting language servers with different resources, ${ + multiLS ? 'multiple' : 'a single' + } language server${multiLS ? 's' : ''} should be instantiated when using ${languageServer}`, async () => { + const getActiveInterpreterStub = sandbox.stub(); + getActiveInterpreterStub.onFirstCall().returns({ path: 'folder1/python', version: { major: 3, minor: 9 } }); + getActiveInterpreterStub + .onSecondCall() + .returns({ path: 'folder2/python', version: { major: 3, minor: 10 } }); + const startLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'startLanguageServer'); + startLanguageServerStub.returns(Promise.resolve()); + const stopLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'stopLanguageServer'); + sandbox.stub(extensionLSCls.prototype, 'canStartLanguageServer').returns(true); + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: getActiveInterpreterStub, + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(languageServer, Uri.parse('folder1')); + await watcher.startLanguageServer(languageServer, Uri.parse('folder2')); + + // If multiLS set to true, then we expect to have called startLanguageServer twice. + // If multiLS set to false, then we expect to have called startLanguageServer once. + assert.ok(startLanguageServerStub.calledTwice === multiLS); + assert.ok(startLanguageServerStub.calledOnce === !multiLS); + assert.ok(getActiveInterpreterStub.calledTwice); + assert.ok(stopLanguageServerStub.notCalled); + }); + + test(`${languageServer} language server(s) should ${ + multiLS ? '' : 'not' + } be stopped if a workspace gets removed from the current project`, async () => { + sandbox.stub(extensionLSCls.prototype, 'startLanguageServer').returns(Promise.resolve()); + if (lsManagerCls) { + sandbox.stub(lsManagerCls.prototype, 'dispose').returns(); + } + + const stopLanguageServerStub = sandbox.stub(extensionLSCls.prototype, 'stopLanguageServer'); + stopLanguageServerStub.returns(Promise.resolve()); + + let onDidChangeWorkspaceFoldersListener: (event: WorkspaceFoldersChangeEvent) => Promise<void> = () => + Promise.resolve(); + + const workspaceService = ({ + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: (listener: (event: WorkspaceFoldersChangeEvent) => Promise<void>) => { + onDidChangeWorkspaceFoldersListener = listener; + }, + workspaceFolders: [{ uri: Uri.parse('workspace1') }, { uri: Uri.parse('workspace2') }], + isTrusted: true, + } as unknown) as IWorkspaceService; + + watcher = new LanguageServerWatcher( + {} as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + ({ + getActiveInterpreter: () => ({ version: { major: 3, minor: 7 } }), + onDidChangeInterpreterInformation: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterService, + {} as IEnvironmentVariablesProvider, + workspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + ({ + showWarningMessage: () => Promise.resolve(undefined), + } as unknown) as IApplicationShell, + disposables, + ); + watcher.register(); + + await watcher.startLanguageServer(languageServer, Uri.parse('workspace1')); + await watcher.startLanguageServer(languageServer, Uri.parse('workspace2')); + + await onDidChangeWorkspaceFoldersListener({ + added: [], + removed: [{ uri: Uri.parse('workspace2') } as WorkspaceFolder], + }); + + // If multiLS set to true, then we expect to have stopped a language server. + // If multiLS set to false, then we expect to not have stopped a language server. + assert.ok(stopLanguageServerStub.calledOnce === multiLS); + assert.ok(stopLanguageServerStub.notCalled === !multiLS); + }); + }); + + test('The language server should be restarted if the interpreter info changed', async () => { + const info = ({ + envPath: 'foo', + path: 'path/to/foo/bin/python', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => ({ + envPath: 'foo', + path: 'path/to/foo', + }), + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called twice: Once above, and once after the interpreter info changed. + assert.ok(startLanguageServerSpy.calledTwice); + }); + + test('The language server should not be restarted if the interpreter info did not change', async () => { + const info = ({ + envPath: 'foo', + path: 'path/to/foo', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => info, + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called once: Only when startLanguageServer() was called above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('The language server should not be restarted if the interpreter info changed but the env path is an empty string', async () => { + const info = ({ + envPath: '', + path: 'path/to/foo', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => ({ + envPath: 'foo', + path: 'path/to/foo', + }), + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called once: Only when startLanguageServer() was called above. + assert.ok(startLanguageServerSpy.calledOnce); + }); + + test('The language server should not be restarted if the interpreter info changed but the env path is undefined', async () => { + const info = ({ + envPath: undefined, + path: 'path/to/foo', + } as unknown) as PythonEnvironment; + + let onDidChangeInfoListener: (event: PythonEnvironment) => Promise<void> = () => Promise.resolve(); + + const interpreterService = ({ + onDidChangeInterpreterInformation: ( + listener: (event: PythonEnvironment) => Promise<void>, + thisArg: unknown, + ): void => { + onDidChangeInfoListener = listener.bind(thisArg); + }, + getActiveInterpreter: () => ({ + envPath: 'foo', + path: 'path/to/foo', + }), + } as unknown) as IInterpreterService; + + watcher = new LanguageServerWatcher( + ({ + get: () => { + /* do nothing */ + }, + } as unknown) as IServiceContainer, + {} as ILanguageServerOutputChannel, + { + getSettings: () => ({ languageServer: LanguageServerType.None }), + } as IConfigurationService, + {} as IExperimentService, + ({ + getActiveWorkspaceUri: () => undefined, + } as unknown) as IInterpreterHelper, + ({ + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IInterpreterPathService, + interpreterService, + ({ + onDidEnvironmentVariablesChange: () => { + /* do nothing */ + }, + } as unknown) as IEnvironmentVariablesProvider, + ({ + isTrusted: true, + getWorkspaceFolder: (uri: Uri) => ({ uri }), + onDidChangeConfiguration: () => { + /* do nothing */ + }, + onDidChangeWorkspaceFolders: () => { + /* do nothing */ + }, + } as unknown) as IWorkspaceService, + ({ + registerCommand: () => { + /* do nothing */ + }, + } as unknown) as ICommandManager, + {} as IFileSystem, + ({ + getExtension: () => undefined, + onDidChange: () => { + /* do nothing */ + }, + } as unknown) as IExtensions, + {} as IApplicationShell, + disposables, + ); + watcher.register(); + + const startLanguageServerSpy = sandbox.spy(watcher, 'startLanguageServer'); + + await watcher.startLanguageServer(LanguageServerType.None); + + await onDidChangeInfoListener(info); + + // Check that startLanguageServer was called once: Only when startLanguageServer() was called above. + assert.ok(startLanguageServerSpy.calledOnce); + }); +}); diff --git a/src/test/languageServers/jedi/autocomplete/base.test.ts b/src/test/languageServers/jedi/autocomplete/base.test.ts deleted file mode 100644 index 3b9a60e6159b..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 '../../../testing/serviceRegistry'; - -const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); -const fileOne = path.join(autoCompPath, 'one.py'); -const fileImport = path.join(autoCompPath, 'imp.py'); -const fileDoc = path.join(autoCompPath, 'doc.py'); -const fileLambda = path.join(autoCompPath, 'lamb.py'); -const fileDecorator = path.join(autoCompPath, 'deco.py'); -const fileEncoding = path.join(autoCompPath, 'four.py'); -const fileEncodingUsed = path.join(autoCompPath, 'five.py'); -const fileSuppress = path.join(autoCompPath, 'suppress.py'); - -// tslint:disable-next-line:max-func-body-length -suite('Autocomplete Base Tests', function () { - // Attempt to fix #1301 - // tslint:disable-next-line:no-invalid-this - this.timeout(60000); - let ioc: UnitTestIocContainer; - - 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.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - assert.equal(list!.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); - }).then(done, done); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/975 - test('For "import *" find a specific completion for known lib [fstat]', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileImport); - await vscode.window.showTextDocument(textDocument); - const lineNum = 1; - const colNum = 4; - const position = new vscode.Position(lineNum, colNum); - const list = await vscode.commands.executeCommand<vscode.CompletionList>( - 'vscode.executeCompletionItemProvider', - textDocument.uri, - position); - - const indexOfFstat = list!.items.findIndex((val: vscode.CompletionItem) => val.label === 'fstat'); - - assert( - indexOfFstat !== -1, - `fstat was not found as a completion in ${fileImport} at line ${lineNum}, col ${colNum}`); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/898 - test('For "f.readlines()"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileDoc); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(5, 27); - await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - // These are not known to work, jedi issue - // assert.equal(list.items.filter(item => item.label === 'capitalize').length, 1, 'capitalize not found (known not to work, Jedi issue)'); - // assert.notEqual(list.items.filter(item => item.label === 'upper').length, 1, 'upper not found'); - // assert.notEqual(list.items.filter(item => item.label === 'lower').length, 1, 'lower not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/265 - test('For "lambda"', async function () { - if (await isPythonVersion('2')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - const textDocument = await vscode.workspace.openTextDocument(fileLambda); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(1, 19); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'append').length, 0, 'append not found'); - assert.notEqual(list!.items.filter(item => item.label === 'clear').length, 0, 'clear not found'); - assert.notEqual(list!.items.filter(item => item.label === 'count').length, 0, 'cound not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/630 - test('For "abc.decorators"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileDecorator); - await vscode.window.showTextDocument(textDocument); - let position = new vscode.Position(3, 9); - let list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); - assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); - - position = new vscode.Position(4, 9); - list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); - assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); - - position = new vscode.Position(2, 30); - list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'ABCMeta').length, 0, 'ABCMeta not found'); - assert.notEqual(list!.items.filter(item => item.label === 'abstractmethod').length, 0, 'abstractmethod not found'); - }); - - // https://github.com/DonJayamanne/pythonVSCode/issues/727 - // https://github.com/DonJayamanne/pythonVSCode/issues/746 - // https://github.com/davidhalter/jedi/issues/859 - test('For "time.slee"', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileDoc); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(10, 9); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - - const items = list!.items.filter(item => item.label === 'sleep'); - assert.notEqual(items.length, 0, 'sleep not found'); - - checkDocumentation(items[0], 'Delay execution for a given number of seconds. The argument may be'); - }); - - test('For custom class', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileOne).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(_editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(30, 4); - return vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - assert.notEqual(list!.items.filter(item => item.label === 'method1').length, 0, 'method1 not found'); - assert.notEqual(list!.items.filter(item => item.label === 'method2').length, 0, 'method2 not found'); - }).then(done, done); - }); - - test('With Unicode Characters', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileEncoding).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(_editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(25, 4); - return vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - const items = list!.items.filter(item => item.label === 'bar'); - assert.equal(items.length, 1, 'bar not found'); - - const expected1 = '说明 - keep this line, it works'; - checkDocumentation(items[0], expected1); - - const expected2 = '如果存在需要等待审批或正在执行的任务,将不刷新页面'; - checkDocumentation(items[0], expected2); - }).then(done, done); - }); - - test('Across files With Unicode Characters', 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.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - }).then(list => { - let items = list!.items.filter(item => item.label === 'Foo'); - assert.equal(items.length, 1, 'Foo not found'); - checkDocumentation(items[0], '说明'); - - items = list!.items.filter(item => item.label === 'showMessage'); - assert.equal(items.length, 1, 'showMessage not found'); - - const expected1 = 'Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи.'; - checkDocumentation(items[0], expected1); - - const expected2 = 'Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку.'; - checkDocumentation(items[0], expected2); - }).then(done, done); - }); - - // https://github.com/Microsoft/vscode-python/issues/110 - test('Suppress in strings/comments', async () => { - const positions = [ - new vscode.Position(0, 1), // false - new vscode.Position(0, 9), // true - new vscode.Position(0, 12), // false - new vscode.Position(1, 1), // false - new vscode.Position(1, 3), // false - new vscode.Position(2, 7), // false - new vscode.Position(3, 0), // false - new vscode.Position(4, 2), // false - new vscode.Position(4, 8), // false - new vscode.Position(5, 4), // false - new vscode.Position(5, 10) // false - ]; - const expected = [ - false, true, false, false, false, false, false, false, false, false, false - ]; - const textDocument = await vscode.workspace.openTextDocument(fileSuppress); - await vscode.window.showTextDocument(textDocument); - for (let i = 0; i < positions.length; i += 1) { - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, positions[i]); - const result = list!.items.filter(item => item.label === 'abs').length; - assert.equal(result > 0, expected[i], - `Expected ${expected[i]} at position ${positions[i].line}:${positions[i].character} but got ${result}`); - } - }); -}); - -// tslint:disable-next-line:no-any -function checkDocumentation(item: vscode.CompletionItem, expectedContains: string): void { - let isValidType = false; - let documentation: string; - - if (typeof item.documentation === 'string') { - isValidType = true; - documentation = item.documentation; - } else { - documentation = (item.documentation as vscode.MarkdownString).value; - isValidType = documentation !== undefined && documentation !== null; - } - assert.equal(isValidType, true, 'Documentation is neither string nor vscode.MarkdownString'); - - const inDoc = documentation.indexOf(expectedContains) >= 0; - assert.equal(inDoc, true, 'Documentation incorrect'); -} diff --git a/src/test/languageServers/jedi/autocomplete/pep484.test.ts b/src/test/languageServers/jedi/autocomplete/pep484.test.ts deleted file mode 100644 index f3bfa88f5d04..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 '../../../testing/serviceRegistry'; - -const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); -const filePep484 = path.join(autoCompPath, 'pep484.py'); - -suite('Autocomplete PEP 484', () => { - let isPython2: boolean; - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - await initialize(); - initializeDI(); - isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri!) === 2; - if (isPython2) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - return; - } - }); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await ioc.dispose(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - } - - test('argument', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep484); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(2, 27); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); - assert.notEqual(list!.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); - assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); - }); - - test('return value', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep484); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(8, 6); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'bit_length').length, 0, 'bit_length not found'); - assert.notEqual(list!.items.filter(item => item.label === 'from_bytes').length, 0, 'from_bytes not found'); - }); -}); diff --git a/src/test/languageServers/jedi/autocomplete/pep526.test.ts b/src/test/languageServers/jedi/autocomplete/pep526.test.ts deleted file mode 100644 index 0ce3cc50cb7f..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 '../../../testing/serviceRegistry'; - -const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'autocomp'); -const filePep526 = path.join(autoCompPath, 'pep526.py'); - -// tslint:disable-next-line:max-func-body-length -suite('Autocomplete PEP 526', () => { - let ioc: UnitTestIocContainer; - suiteSetup(async function () { - // Pep526 only valid for 3.6+ (#2545) - if (await isPythonVersion('2', '3.4', '3.5')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - await initialize(); - initializeDI(); - }); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await ioc.dispose(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - } - test('variable (abc:str)', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep526); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(9, 8); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); - assert.notEqual(list!.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); - assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); - }); - - test('variable (abc: str = "")', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep526); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(8, 14); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); - assert.notEqual(list!.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); - assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); - }); - - test('variable (abc = UNKNOWN # type: str)', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep526); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(7, 14); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'capitalize').length, 0, 'capitalize not found'); - assert.notEqual(list!.items.filter(item => item.label === 'upper').length, 0, 'upper not found'); - assert.notEqual(list!.items.filter(item => item.label === 'lower').length, 0, 'lower not found'); - }); - - test('class methods', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep526); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - let position = new vscode.Position(20, 4); - let list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'a').length, 0, 'method a not found'); - - position = new vscode.Position(21, 4); - list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'b').length, 0, 'method b not found'); - }); - - test('class method types', async () => { - const textDocument = await vscode.workspace.openTextDocument(filePep526); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(21, 6); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - assert.notEqual(list!.items.filter(item => item.label === 'bit_length').length, 0, 'bit_length not found'); - }); -}); diff --git a/src/test/languageServers/jedi/completionSource.unit.test.ts b/src/test/languageServers/jedi/completionSource.unit.test.ts deleted file mode 100644 index 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<JediProxyHandler<ICompletionResult>>; - let autoCompleteSettings: TypeMoq.IMock<IAutoCompleteSettings>; - let itemInfoSource: TypeMoq.IMock<IItemInfoSource>; - setup(() => { - const jediFactory = TypeMoq.Mock.ofType(JediFactory); - jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<ICompletionResult>>(); - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - autoCompleteSettings = TypeMoq.Mock.ofType<IAutoCompleteSettings>(); - autoCompleteSettings = TypeMoq.Mock.ofType<IAutoCompleteSettings>(); - - jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) - .returns(() => jediHandler.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => configService.object); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); - pythonSettings.setup(p => p.autoComplete).returns(() => autoCompleteSettings.object); - itemInfoSource = TypeMoq.Mock.ofType<IItemInfoSource>(); - completionSource = new CompletionSource(jediFactory.object, serviceContainer.object, itemInfoSource.object); - }); - - async function testDocumentation(source: string, addBrackets: boolean) { - const doc = TypeMoq.Mock.ofType<TextDocument>(); - const position = new Position(1, 1); - const token = new CancellationTokenSource().token; - const lineText = TypeMoq.Mock.ofType<TextLine>(); - const completionResult = TypeMoq.Mock.ofType<ICompletionResult>(); - - const autoCompleteItems: IAutoCompleteItem[] = [{ - description: 'description', kind: SymbolKind.Function, - raw_docstring: 'raw docstring', - rawType: CompletionItemKind.Function, - rightLabel: 'right label', - text: 'some text', type: CompletionItemKind.Function - }]; - - autoCompleteSettings.setup(a => a.addBrackets).returns(() => addBrackets); - doc.setup(d => d.fileName).returns(() => ''); - doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => source); - doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); - doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => 0); - lineText.setup(l => l.text).returns(() => source); - completionResult.setup(c => c.requestId).returns(() => 1); - completionResult.setup(c => c.items).returns(() => autoCompleteItems); - completionResult.setup((c: any) => c.then).returns(() => undefined); - jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return Promise.resolve(completionResult.object); - }); - - const expectedSource = `${source}${autoCompleteItems[0].text}`; - itemInfoSource.setup(i => i.getItemInfoFromText(TypeMoq.It.isAny(), TypeMoq.It.isAny(), - TypeMoq.It.isAny(), expectedSource, TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - - const [item] = await completionSource.getVsCodeCompletionItems(doc.object, position, token); - await completionSource.getDocumentation(item, token); - itemInfoSource.verifyAll(); - } - - test('Ensure docs are provided when \'addBrackets\' setting is false', async () => { - const source = 'if True:\n print("Hello")\n'; - await testDocumentation(source, false); - }); - test('Ensure docs are provided when \'addBrackets\' setting is true', async () => { - const source = 'if True:\n print("Hello")\n'; - await testDocumentation(source, true); - }); - -}); diff --git a/src/test/languageServers/jedi/definitions/hover.jedi.test.ts b/src/test/languageServers/jedi/definitions/hover.jedi.test.ts deleted file mode 100644 index f5ff09aff682..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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '30,4', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '30,11', 'End position is incorrect'); - assert.equal(def[0].contents.length, 1, 'Invalid content items'); - // tslint:disable-next-line:prefer-template - const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; - assert.equal(normalizeMarkedString(def[0].contents[0]), expectedContent, 'function signature incorrect'); - }).then(done, done); - }); - - test('Across files', 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,9', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,12', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '25,4', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '25,7', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '1,5', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '1,16', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { - assert.equal(def!.length, 0, 'Definition length is incorrect'); - }).then(done, done); - }); - - test('Nothing for keywords (for)', done => { - let textDocument: vscode.TextDocument; - vscode.workspace.openTextDocument(fileHover).then(document => { - textDocument = document; - return vscode.window.showTextDocument(textDocument); - }).then(_editor => { - assert(vscode.window.activeTextEditor, 'No active editor'); - const position = new vscode.Position(3, 1); - return vscode.commands.executeCommand<vscode.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(def => { - assert.equal(def!.length, 0, 'Definition length is incorrect'); - }).then(done, done); - }); - - test('Highlighting Class', 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '11,12', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '11,18', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '12,5', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '12,12', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '8,11', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '8,15', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(`${def[0].range!.start.line},${def[0].range!.start.character}`, '14,9', 'Start position is incorrect'); - assert.equal(`${def[0].range!.end.line},${def[0].range!.end.character}`, '14,15', 'End position is incorrect'); - // 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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - }).then(result => { - const def = result!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(def[0].contents.length, 1, 'Only expected one result'); - const contents = normalizeMarkedString(def[0].contents[0]); - if (contents.indexOf('```python') === -1) { - assert.fail(contents, '', 'First line is incorrect', 'compare'); - } - if (contents.indexOf('rnd: Random') === -1) { - assert.fail(contents, '', 'Variable name or type are missing', 'compare'); - } - }).then(done, done); - }); - - test('Hover over method shows proper text.', async () => { - const textDocument = await vscode.workspace.openTextDocument(fileStringFormat); - await vscode.window.showTextDocument(textDocument); - const position = new vscode.Position(8, 4); - const def = (await vscode.commands.executeCommand<vscode.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position))!; - assert.equal(def.length, 1, 'Definition length is incorrect'); - assert.equal(def[0].contents.length, 1, 'Only expected one result'); - const contents = normalizeMarkedString(def[0].contents[0]); - if (contents.indexOf('def my_func') === -1) { - assert.fail(contents, '', '\'def my_func\' is missing', 'compare'); - } - if (contents.indexOf('This is a test.') === -1 && - contents.indexOf('It also includes this text, too.') === -1) { - assert.fail(contents, '', 'Expected custom function text missing', 'compare'); - } - }); -}); diff --git a/src/test/languageServers/jedi/definitions/navigation.test.ts b/src/test/languageServers/jedi/definitions/navigation.test.ts deleted file mode 100644 index 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.Location[]>('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.Hover[]>('vscode.executeHoverProvider', textDocument.uri, position); - const codeDef = await vscode.commands.executeCommand<vscode.Location[]>('vscode.executeDefinitionProvider', textDocument.uri, position); - position = new vscode.Position(3, 10); - const list = await vscode.commands.executeCommand<vscode.CompletionList>('vscode.executeCompletionItemProvider', textDocument.uri, position); - - assert.equal(list!.items.filter(item => item.label === 'api_version').length, 1, 'api_version not found'); - - assert.equal(codeDef!.length, 1, 'Definition length is incorrect'); - const expectedPath = IS_WINDOWS ? fileOne.toUpperCase() : fileOne; - const actualPath = IS_WINDOWS ? codeDef![0].uri.fsPath.toUpperCase() : codeDef![0].uri.fsPath; - assert.equal(actualPath, expectedPath, 'Incorrect file'); - assert.equal(`${codeDef![0].range!.start.line},${codeDef![0].range!.start.character}`, '17,4', 'Start position is incorrect'); - assert.equal(`${codeDef![0].range!.end.line},${codeDef![0].range!.end.character}`, '21,11', 'End position is incorrect'); - - assert.equal(hoverDef!.length, 1, 'Definition length is incorrect'); - assert.equal(`${hoverDef![0].range!.start.line},${hoverDef![0].range!.start.character}`, '30,4', 'Start position is incorrect'); - assert.equal(`${hoverDef![0].range!.end.line},${hoverDef![0].range!.end.character}`, '30,11', 'End position is incorrect'); - assert.equal(hoverDef![0].contents.length, 1, 'Invalid content items'); - // tslint:disable-next-line:prefer-template - const expectedContent = '```python' + EOL + 'def method1()' + EOL + '```' + EOL + 'This is method1'; - assert.equal(normalizeMarkedString(hoverDef![0].contents[0]), expectedContent, 'function signature incorrect'); - }); -}); diff --git a/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts b/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts deleted file mode 100644 index 00fc9a33492f..000000000000 --- a/src/test/languageServers/jedi/pythonSignatureProvider.unit.test.ts +++ /dev/null @@ -1,251 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - -import { assert, expect, use } from 'chai'; -import * as chaipromise from 'chai-as-promised'; -import * as TypeMoq from 'typemoq'; -import { - CancellationToken, Position, SignatureHelp, - TextDocument, TextLine, Uri -} from 'vscode'; -import { JediFactory } from '../../../client/languageServices/jediProxyFactory'; -import { IArgumentsResult, JediProxyHandler } from '../../../client/providers/jediProxy'; -import { isPositionInsideStringOrComment } from '../../../client/providers/providerUtilities'; -import { PythonSignatureProvider } from '../../../client/providers/signatureProvider'; - -use(chaipromise); - -suite('Signature Provider unit tests', () => { - let pySignatureProvider: PythonSignatureProvider; - let jediHandler: TypeMoq.IMock<JediProxyHandler<IArgumentsResult>>; - let argResultItems: IArgumentsResult; - setup(() => { - const jediFactory = TypeMoq.Mock.ofType(JediFactory); - jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<IArgumentsResult>>(); - jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) - .returns(() => jediHandler.object); - pySignatureProvider = new PythonSignatureProvider(jediFactory.object); - argResultItems = { - definitions: [ - { - description: 'The result', - docstring: 'Some docstring goes here.', - name: 'print', - paramindex: 0, - params: [ - { - description: 'Some parameter', - docstring: 'gimme docs', - name: 'param', - value: 'blah' - } - ] - } - ], - requestId: 1 - }; - }); - - function testSignatureReturns(source: string, pos: number): Thenable<SignatureHelp> { - const doc = TypeMoq.Mock.ofType<TextDocument>(); - const position = new Position(0, pos); - const lineText = TypeMoq.Mock.ofType<TextLine>(); - const argsResult = TypeMoq.Mock.ofType<IArgumentsResult>(); - const cancelToken = TypeMoq.Mock.ofType<CancellationToken>(); - cancelToken.setup(ct => ct.isCancellationRequested).returns(() => false); - - doc.setup(d => d.fileName).returns(() => ''); - doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => source); - doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => lineText.object); - doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => pos - 1); // pos is 1-based - const docUri = TypeMoq.Mock.ofType<Uri>(); - docUri.setup(u => u.scheme).returns(() => 'http'); - doc.setup(d => d.uri).returns(() => docUri.object); - lineText.setup(l => l.text).returns(() => source); - argsResult.setup(c => c.requestId).returns(() => 1); - // tslint:disable-next-line:no-any - argsResult.setup(c => c.definitions).returns(() => (argResultItems as any)[0].definitions); - jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return Promise.resolve(argResultItems); - }); - - return pySignatureProvider.provideSignatureHelp(doc.object, position, cancelToken.object); - } - - function testIsInsideStringOrComment(sourceLine: string, sourcePos: number): boolean { - const textLine: TypeMoq.IMock<TextLine> = TypeMoq.Mock.ofType<TextLine>(); - textLine.setup(t => t.text).returns(() => sourceLine); - const doc: TypeMoq.IMock<TextDocument> = TypeMoq.Mock.ofType<TextDocument>(); - const pos: Position = new Position(1, sourcePos); - - doc.setup(d => d.fileName).returns(() => ''); - doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => sourceLine); - doc.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); - doc.setup(d => d.offsetAt(TypeMoq.It.isAny())).returns(() => sourcePos); - - return isPositionInsideStringOrComment(doc.object, pos); - } - - test('Ensure no signature is given within a string.', async () => { - const source = ' print(\'Python is awesome,\')\n'; - const sigHelp: SignatureHelp = await testSignatureReturns(source, 27); - expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); - expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a string?'); - }); - test('Ensure no signature is given within a line comment.', async () => { - const source = '# print(\'Python is awesome,\')\n'; - const sigHelp: SignatureHelp = await testSignatureReturns(source, 28); - expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); - expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a full-line comment?'); - }); - test('Ensure no signature is given within a comment tailing a command.', async () => { - const source = ' print(\'Python\') # print(\'is awesome,\')\n'; - const sigHelp: SignatureHelp = await testSignatureReturns(source, 38); - expect(sigHelp).to.not.be.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); - expect(sigHelp.signatures.length).to.equal(0, 'Signature provided for symbols within a trailing comment?'); - }); - test('Ensure signature is given for built-in print command.', async () => { - const source = ' print(\'Python\',)\n'; - let sigHelp: SignatureHelp; - try { - sigHelp = await testSignatureReturns(source, 18); - expect(sigHelp).to.not.equal(undefined, 'Expected to get a blank signature item back - did the pattern change here?'); - expect(sigHelp.signatures.length).to.not.equal(0, 'Expected dummy argresult back from testing our print signature.'); - expect(sigHelp.activeParameter).to.be.equal(0, 'Parameter for print should be the first member of the test argresult\'s params object.'); - expect(sigHelp.activeSignature).to.be.equal(0, 'The signature for print should be the first member of the test argresult.'); - expect(sigHelp.signatures[sigHelp.activeSignature].label).to.be.equal('print(param)', `Expected arg result calls for specific returned signature of \'print(param)\' but we got ${sigHelp.signatures[sigHelp.activeSignature].label}`); - } catch (error) { - assert(false, `Caught exception ${error}`); - } - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected.', () => { - const sourceLine: string = ' print(\'Hello world!\')\n'; - const sourcePos: number = sourceLine.length - 1; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.not.be.equal(true, [ - `Position set to the end of ${sourceLine} but `, - 'is reported as being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected at end of source.', () => { - const sourceLine: string = ' print(\'Hello world!\')\n'; - const sourcePos: number = 0; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.not.be.equal(true, [ - `Position set to the end of ${sourceLine} but `, - 'is reported as being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected at beginning of source.', () => { - const sourceLine: string = ' print(\'Hello world!\')\n'; - const sourcePos: number = 0; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.not.be.equal(true, [ - `Position set to the beginning of ${sourceLine} but `, - 'is reported as being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected within a string.', () => { - const sourceLine: string = ' print(\'Hello world!\')\n'; - const sourcePos: number = 16; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within the string in ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected immediately before a string.', () => { - const sourceLine: string = ' print(\'Hello world!\')\n'; - const sourcePos: number = 8; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(false, [ - `Position set to just before the string in ${sourceLine} (position ${sourcePos}) but `, - 'is reported as being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected immediately in a string.', () => { - const sourceLine: string = ' print(\'Hello world!\')\n'; - const sourcePos: number = 9; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set to the start of the string in ${sourceLine} (position ${sourcePos}) but `, - 'is reported as being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected within a comment.', () => { - const sourceLine: string = '# print(\'Hello world!\')\n'; - const sourcePos: number = 16; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a full line comment ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected within a trailing comment.', () => { - const sourceLine: string = ' print(\'Hello world!\') # some comment...\n'; - const sourcePos: number = 34; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected at the very end of a trailing comment.', () => { - const sourceLine: string = ' print(\'Hello world!\') # some comment...\n'; - const sourcePos: number = sourceLine.length - 1; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a trailing line comment ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string.', () => { - const sourceLine: string = ' stringVal = \'\'\'This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!\'\'\'\n'; - const sourcePos: number = 48; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string.', () => { - const sourceLine: string = ' stringVal = \'\'\'This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!\'\'\'\n'; - const sourcePos: number = sourceLine.length - 2; // just at the last ' - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected within a multiline string (double-quoted).', () => { - const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; - const sourcePos: number = 48; - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected at the very last quote on a multiline string (double-quoted).', () => { - const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff out with\neveryday!"""\n'; - const sourcePos: number = sourceLine.length - 2; // just at the last ' - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); - test('Ensure isPositionInsideStringOrComment is behaving as expected during construction of a multiline string (double-quoted).', () => { - const sourceLine: string = ' stringVal = """This is a multiline\nstring that you can use\nto test this stuff'; - const sourcePos: number = sourceLine.length - 1; // just at the last position in the string before it's termination - const isInsideStrComment: boolean = testIsInsideStringOrComment(sourceLine, sourcePos); - - expect(isInsideStrComment).to.be.equal(true, [ - `Position set within a multi-line string ${sourceLine} (position ${sourcePos}) but `, - 'is reported as NOT being within a string or comment.'].join('')); - }); -}); diff --git a/src/test/languageServers/jedi/signature/signature.jedi.test.ts b/src/test/languageServers/jedi/signature/signature.jedi.test.ts deleted file mode 100644 index 273ac9540855..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 '../../../testing/serviceRegistry'; - -const autoCompPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'signature'); - -class SignatureHelpResult { - constructor( - public line: number, - public index: number, - public signaturesCount: number, - public activeParameter: number, - public parameterName: string | null) { } -} - -// tslint:disable-next-line:max-func-body-length -suite('Signatures (Jedi)', () => { - let isPython2: boolean; - let ioc: UnitTestIocContainer; - suiteSetup(async () => { - await initialize(); - initializeDI(); - isPython2 = await ioc.getPythonMajorVersion(rootWorkspaceUri!) === 2; - }); - setup(initializeTest); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await ioc.dispose(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerProcessTypes(); - } - - test('For ctor', async () => { - const expected = [ - new SignatureHelpResult(5, 11, 0, 0, null), - new SignatureHelpResult(5, 12, 1, 0, 'name'), - new SignatureHelpResult(5, 13, 0, 0, null), - new SignatureHelpResult(5, 14, 0, 0, null), - new SignatureHelpResult(5, 15, 0, 0, null), - new SignatureHelpResult(5, 16, 0, 0, null), - new SignatureHelpResult(5, 17, 0, 0, null), - new SignatureHelpResult(5, 18, 1, 1, 'age'), - new SignatureHelpResult(5, 19, 1, 1, 'age'), - new SignatureHelpResult(5, 20, 0, 0, null) - ]; - - const document = await openDocument(path.join(autoCompPath, 'classCtor.py')); - for (let i = 0; i < expected.length; i += 1) { - await checkSignature(expected[i], document!.uri, i); - } - }); - - test('For intrinsic', async () => { - 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<vscode.TextDocument | undefined> { - const document = await vscode.workspace.openTextDocument(documentPath); - await vscode.window.showTextDocument(document!); - return document; -} - -async function checkSignature(expected: SignatureHelpResult, uri: vscode.Uri, caseIndex: number) { - const position = new vscode.Position(expected.line, expected.index); - const actual = await vscode.commands.executeCommand<vscode.SignatureHelp>('vscode.executeSignatureHelpProvider', uri, position); - assert.equal(actual!.signatures.length, expected.signaturesCount, `Signature count does not match, case ${caseIndex}`); - if (expected.signaturesCount > 0) { - assert.equal(actual!.activeParameter, expected.activeParameter, `Parameter index does not match, case ${caseIndex}`); - if (expected.parameterName) { - const parameter = actual!.signatures[0].parameters[expected.activeParameter]; - assert.equal(parameter.label, expected.parameterName, `Parameter name is incorrect, case ${caseIndex}`); - } - } -} diff --git a/src/test/languageServers/jedi/symbolProvider.unit.test.ts b/src/test/languageServers/jedi/symbolProvider.unit.test.ts deleted file mode 100644 index a9608abb1ed3..000000000000 --- a/src/test/languageServers/jedi/symbolProvider.unit.test.ts +++ /dev/null @@ -1,485 +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 { parseRange } from '../../../client/common/utils/text'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { JediFactory } from '../../../client/languageServices/jediProxyFactory'; -import { IDefinition, ISymbolResult, JediProxyHandler } from '../../../client/providers/jediProxy'; -import { JediSymbolProvider, LanguageServerSymbolProvider } from '../../../client/providers/symbolProvider'; - -const assertArrays = require('chai-arrays'); -use(assertArrays); - -suite('Jedi Symbol Provider', () => { - let serviceContainer: TypeMoq.IMock<IServiceContainer>; - let jediHandler: TypeMoq.IMock<JediProxyHandler<ISymbolResult>>; - let jediFactory: TypeMoq.IMock<JediFactory>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let provider: DocumentSymbolProvider; - let uri: Uri; - let doc: TypeMoq.IMock<TextDocument>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - jediFactory = TypeMoq.Mock.ofType(JediFactory); - jediHandler = TypeMoq.Mock.ofType<JediProxyHandler<ISymbolResult>>(); - - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - doc = TypeMoq.Mock.ofType<TextDocument>(); - jediFactory.setup(j => j.getJediProxyHandler(TypeMoq.It.isAny())) - .returns(() => jediHandler.object); - - serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fileSystem.object); - }); - - async function testDocumentation(requestId: number, fileName: string, expectedSize: number, token?: CancellationToken, isUntitled = false) { - fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => true); - token = token ? token : new CancellationTokenSource().token; - const symbolResult = TypeMoq.Mock.ofType<ISymbolResult>(); - - const definitions: IDefinition[] = [ - { - container: '', fileName: fileName, kind: SymbolKind.Array, - range: { endColumn: 0, endLine: 0, startColumn: 0, startLine: 0 }, - rawType: '', text: '', type: CompletionItemKind.Class - } - ]; - - uri = Uri.file(fileName); - doc.setup(d => d.uri).returns(() => uri); - doc.setup(d => d.fileName).returns(() => fileName); - doc.setup(d => d.isUntitled).returns(() => isUntitled); - doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => ''); - symbolResult.setup(c => c.requestId).returns(() => requestId); - symbolResult.setup(c => c.definitions).returns(() => definitions); - symbolResult.setup((c: any) => c.then).returns(() => undefined); - jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(symbolResult.object)); - - const items = await provider.provideDocumentSymbols(doc.object, token); - expect(items).to.be.array(); - expect(items).to.be.ofSize(expectedSize); - } - - test('Ensure symbols are returned', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await testDocumentation(1, __filename, 1); - }); - test('Ensure symbols are returned (for untitled documents)', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await testDocumentation(1, __filename, 1, undefined, true); - }); - test('Ensure symbols are returned with a debounce of 100ms', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await testDocumentation(1, __filename, 1); - }); - test('Ensure symbols are returned with a debounce of 100ms (for untitled documents)', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await testDocumentation(1, __filename, 1, undefined, true); - }); - test('Ensure symbols are not returned when cancelled', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - await testDocumentation(1, __filename, 0, tokenSource.token); - }); - test('Ensure symbols are not returned when cancelled (for untitled documents)', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - const tokenSource = new CancellationTokenSource(); - tokenSource.cancel(); - await testDocumentation(1, __filename, 0, tokenSource.token, true); - }); - test('Ensure symbols are returned only for the last request', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); - await Promise.all([ - testDocumentation(1, __filename, 0), - testDocumentation(2, __filename, 0), - testDocumentation(3, __filename, 1) - ]); - }); - test('Ensure symbols are returned for all the requests when the doc is untitled', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); - await Promise.all([ - testDocumentation(1, __filename, 1, undefined, true), - testDocumentation(2, __filename, 1, undefined, true), - testDocumentation(3, __filename, 1, undefined, true) - ]); - }); - test('Ensure symbols are returned for multiple documents', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await Promise.all([ - testDocumentation(1, 'file1', 1), - testDocumentation(2, 'file2', 1) - ]); - }); - test('Ensure symbols are returned for multiple untitled documents ', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await Promise.all([ - testDocumentation(1, 'file1', 1, undefined, true), - testDocumentation(2, 'file2', 1, undefined, true) - ]); - }); - test('Ensure symbols are returned for multiple documents with a debounce of 100ms', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); - await Promise.all([ - testDocumentation(1, 'file1', 1), - testDocumentation(2, 'file2', 1) - ]); - }); - test('Ensure symbols are returned for multiple untitled documents with a debounce of 100ms', async () => { - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 100); - await Promise.all([ - testDocumentation(1, 'file1', 1, undefined, true), - testDocumentation(2, 'file2', 1, undefined, true) - ]); - }); - test('Ensure IFileSystem.arePathsSame is used', async () => { - doc.setup(d => d.getText()) - .returns(() => '') - .verifiable(TypeMoq.Times.once()); - doc.setup(d => d.isDirty) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - doc.setup(d => d.fileName) - .returns(() => __filename); - - const symbols = TypeMoq.Mock.ofType<ISymbolResult>(); - symbols.setup((s: any) => s.then).returns(() => undefined); - const definitions: IDefinition[] = []; - for (let counter = 0; counter < 3; counter += 1) { - const def = TypeMoq.Mock.ofType<IDefinition>(); - def.setup(d => d.fileName).returns(() => counter.toString()); - definitions.push(def.object); - - fileSystem.setup(fs => fs.arePathsSame(TypeMoq.It.isValue(counter.toString()), TypeMoq.It.isValue(__filename))) - .returns(() => false) - .verifiable(TypeMoq.Times.exactly(1)); - } - symbols.setup(s => s.definitions) - .returns(() => definitions) - .verifiable(TypeMoq.Times.atLeastOnce()); - - jediHandler.setup(j => j.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(symbols.object)) - .verifiable(TypeMoq.Times.once()); - - provider = new JediSymbolProvider(serviceContainer.object, jediFactory.object, 0); - await provider.provideDocumentSymbols(doc.object, new CancellationTokenSource().token); - - doc.verifyAll(); - symbols.verifyAll(); - fileSystem.verifyAll(); - jediHandler.verifyAll(); - }); -}); - -suite('Language Server Symbol Provider', () => { - - function createLanguageClient( - token: CancellationToken, - results: [any, any[]][] - ): TypeMoq.IMock<LanguageClient> { - const langClient = TypeMoq.Mock.ofType<LanguageClient>(undefined, TypeMoq.MockBehavior.Strict); - for (const [doc, symbols] of results) { - langClient.setup(l => l.sendRequest( - TypeMoq.It.isValue('textDocument/documentSymbol'), - TypeMoq.It.isValue(doc), - TypeMoq.It.isValue(token) - )) - .returns(() => Promise.resolve(symbols)) - .verifiable(TypeMoq.Times.once()); - } - return langClient; - } - - function getRawDoc( - uri: Uri - ) { - return { - textDocument: { - uri: uri.toString() - } - }; - } - - test('Ensure symbols are returned - simple', async () => { - const raw = [{ - name: 'spam', - kind: SymbolKind.Array + 1, - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 } - }, - children: [] - }]; - const uri = Uri.file(__filename); - const expected = createSymbols(uri, [ - ['spam', SymbolKind.Array, 0] - ]); - const doc = createDoc(uri); - const token = new CancellationTokenSource().token; - const langClient = createLanguageClient(token, [ - [getRawDoc(uri), raw] - ]); - const provider = new LanguageServerSymbolProvider(langClient.object); - - const items = await provider.provideDocumentSymbols(doc.object, token); - - expect(items).to.deep.equal(expected); - doc.verifyAll(); - langClient.verifyAll(); - }); - test('Ensure symbols are returned - minimal', async () => { - const uri = Uri.file(__filename); - - // The test data is loosely based on the "full" test. - const raw = [{ - name: 'SpamTests', - kind: 5, - range: { - start: { line: 2, character: 6 }, - end: { line: 2, character: 15 } - }, - children: [ - { - name: 'test_all', - kind: 12, - range: { - start: { line: 3, character: 8 }, - end: { line: 3, character: 16 } - }, - children: [{ - name: 'self', - kind: 13, - range: { - start: { line: 3, character: 17 }, - end: { line: 3, character: 21 } - }, - children: [] - }] - }, { - name: 'assertTrue', - kind: 13, - range: { - start: { line: 0, character: 0 }, - end: { line: 0, character: 0 } - }, - children: [] - } - ] - }]; - const expected = [ - new SymbolInformation( - 'SpamTests', - SymbolKind.Class, - '', - new Location( - uri, - new Range(2, 6, 2, 15) - ) - ), - new SymbolInformation( - 'test_all', - SymbolKind.Function, - 'SpamTests', - new Location( - uri, - new Range(3, 8, 3, 16) - ) - ), - new SymbolInformation( - 'self', - SymbolKind.Variable, - 'test_all', - new Location( - uri, - new Range(3, 17, 3, 21) - ) - ), - new SymbolInformation( - 'assertTrue', - SymbolKind.Variable, - 'SpamTests', - new Location( - uri, - new Range(0, 0, 0, 0) - ) - ) - ]; - - const doc = createDoc(uri); - const token = new CancellationTokenSource().token; - const langClient = createLanguageClient(token, [ - [getRawDoc(uri), raw] - ]); - const provider = new LanguageServerSymbolProvider(langClient.object); - - const items = await provider.provideDocumentSymbols(doc.object, token); - - expect(items).to.deep.equal(expected); - }); - test('Ensure symbols are returned - full', async () => { - const uri = Uri.file(__filename); - - // This is the raw symbol data returned by the language server which - // gets converted to SymbolInformation[]. It was captured from an - // actual VS Code session for a file with the following code: - // - // import unittest - // - // class SpamTests(unittest.TestCase): - // def test_all(self): - // self.assertTrue(False) - // - // See: LanguageServerSymbolProvider.provideDocumentSymbols() - // tslint:disable-next-line:no-suspicious-comment - // TODO: Change "raw" once the following issues are resolved: - // * https://github.com/Microsoft/python-language-server/issues/1 - // * https://github.com/Microsoft/python-language-server/issues/2 - const raw = JSON.parse('[{"name":"SpamTests","detail":"SpamTests","kind":5,"deprecated":false,"range":{"start":{"line":2,"character":6},"end":{"line":2,"character":15}},"selectionRange":{"start":{"line":2,"character":6},"end":{"line":2,"character":15}},"children":[{"name":"test_all","detail":"test_all","kind":12,"deprecated":false,"range":{"start":{"line":3,"character":4},"end":{"line":4,"character":30}},"selectionRange":{"start":{"line":3,"character":4},"end":{"line":4,"character":30}},"children":[{"name":"self","detail":"self","kind":13,"deprecated":false,"range":{"start":{"line":3,"character":17},"end":{"line":3,"character":21}},"selectionRange":{"start":{"line":3,"character":17},"end":{"line":3,"character":21}},"children":[],"_functionKind":""}],"_functionKind":"function"},{"name":"assertTrue","detail":"assertTrue","kind":13,"deprecated":false,"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"selectionRange":{"start":{"line":0,"character":0},"end":{"line":0,"character":0}},"children":[],"_functionKind":""}],"_functionKind":"class"}]'); - raw[0].children[0].range.start.character = 8; - raw[0].children[0].range.end.line = 3; - raw[0].children[0].range.end.character = 16; - - // This is the data from Jedi corresponding to same Python code - // for which the raw data above was generated. - // See: JediSymbolProvider.provideDocumentSymbols() - const expectedRaw = JSON.parse('[{"name":"unittest","kind":1,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":0,"character":7},{"line":0,"character":15}]},"containerName":""},{"name":"SpamTests","kind":4,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":2,"character":0},{"line":4,"character":29}]},"containerName":""},{"name":"test_all","kind":11,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":3,"character":4},{"line":4,"character":29}]},"containerName":"SpamTests"},{"name":"self","kind":12,"location":{"uri":{"$mid":1,"path":"<some file>","scheme":"file"},"range":[{"line":3,"character":17},{"line":3,"character":21}]},"containerName":"test_all"}]'); - expectedRaw[1].location.range[0].character = 6; - expectedRaw[1].location.range[1].line = 2; - expectedRaw[1].location.range[1].character = 15; - expectedRaw[2].location.range[0].character = 8; - expectedRaw[2].location.range[1].line = 3; - expectedRaw[2].location.range[1].character = 16; - const expected = normalizeSymbols(uri, expectedRaw); - expected.shift(); // For now, drop the "unittest" symbol. - expected.push(new SymbolInformation( - 'assertTrue', - SymbolKind.Variable, - 'SpamTests', - new Location( - uri, - new Range(0, 0, 0, 0) - ) - )); - - const doc = createDoc(uri); - const token = new CancellationTokenSource().token; - const langClient = createLanguageClient(token, [ - [getRawDoc(uri), raw] - ]); - const provider = new LanguageServerSymbolProvider(langClient.object); - - const items = await provider.provideDocumentSymbols(doc.object, token); - - expect(items).to.deep.equal(expected); - }); -}); - -//################################ -// helpers - -function createDoc( - uri?: Uri, - filename?: string, - isUntitled?: boolean, - text?: string -): TypeMoq.IMock<TextDocument> { - const doc = TypeMoq.Mock.ofType<TextDocument>(undefined, TypeMoq.MockBehavior.Strict); - if (uri !== undefined) { - doc.setup(d => d.uri).returns(() => uri); - } - if (filename !== undefined) { - doc.setup(d => d.fileName).returns(() => filename); - } - if (isUntitled !== undefined) { - doc.setup(d => d.isUntitled).returns(() => isUntitled); - } - if (text !== undefined) { - doc.setup(d => d.getText(TypeMoq.It.isAny())).returns(() => text); - } - return doc; -} - -function createSymbols( - uri: Uri, - info: [string, SymbolKind, string | number][] -): SymbolInformation[] { - const symbols: SymbolInformation[] = []; - for (const [fullName, kind, range] of info) { - const symbol = createSymbol(uri, fullName, kind, range); - symbols.push(symbol); - } - return symbols; -} - -function createSymbol( - uri: Uri, - fullName: string, - kind: SymbolKind, - rawRange: string | number = '' -): SymbolInformation { - const [containerName, name] = splitParent(fullName); - const range = parseRange(rawRange); - const loc = new Location(uri, range); - return new SymbolInformation(name, kind, containerName, loc); -} - -function normalizeSymbols(uri: Uri, raw: any[]): SymbolInformation[] { - const symbols: SymbolInformation[] = []; - for (const item of raw) { - const symbol = new SymbolInformation( - item.name, - // Type coercion is a bit fuzzy when it comes to enums, so we - // play it safe by explicitly converting. - (SymbolKind as any)[(SymbolKind as any)[item.kind]], - item.containerName, - new Location( - uri, - new Range( - item.location.range[0].line, - item.location.range[0].character, - item.location.range[1].line, - item.location.range[1].character - ) - ) - ); - symbols.push(symbol); - } - return symbols; -} - -/** - * Return [parent name, name] for the given qualified (dotted) name. - * - * Examples: - * 'x.y' -> ['x', 'y'] - * 'x' -> ['', 'x'] - * 'x.y.z' -> ['x.y', 'z'] - * '' -> ['', ''] - */ -export function splitParent(fullName: string): [string, string] { - if (fullName.length === 0) { - return ['', '']; - } - const pos = fullName.lastIndexOf('.'); - if (pos < 0) { - return ['', fullName]; - } - const parentName = fullName.slice(0, pos); - const name = fullName.slice(pos + 1); - return [parentName, name]; -} diff --git a/src/test/legacyFileSystem.ts b/src/test/legacyFileSystem.ts new file mode 100644 index 000000000000..7584f9619943 --- /dev/null +++ b/src/test/legacyFileSystem.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { FileSystem, FileSystemUtils, RawFileSystem } from '../client/common/platform/fileSystem'; +import { FakeVSCodeFileSystemAPI } from './fakeVSCFileSystemAPI'; + +export class LegacyFileSystem extends FileSystem { + constructor() { + super(); + const vscfs = new FakeVSCodeFileSystemAPI(); + const raw = RawFileSystem.withDefaults(undefined, vscfs); + this.utils = FileSystemUtils.withDefaults(raw); + } +} diff --git a/src/test/linters/common.ts b/src/test/linters/common.ts deleted file mode 100644 index 995a7b27da78..000000000000 --- a/src/test/linters/common.ts +++ /dev/null @@ -1,366 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as os from 'os'; -import * as TypeMoq from 'typemoq'; -import { - DiagnosticSeverity, - TextDocument, - Uri, - WorkspaceFolder -} from 'vscode'; -import { - IApplicationShell, - IWorkspaceService -} from '../../client/common/application/types'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { - IFileSystem, - IPlatformService -} from '../../client/common/platform/types'; -import { - IPythonExecutionFactory, - IPythonToolExecutionService -} from '../../client/common/process/types'; -import { - Flake8CategorySeverity, - IConfigurationService, - IInstaller, - ILogger, - IMypyCategorySeverity, - IOutputChannel, - IPep8CategorySeverity, - IPylintCategorySeverity, - IPythonSettings -} from '../../client/common/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { - ILinter, - ILinterManager, - ILintMessage, - LinterId -} from '../../client/linters/types'; - -export function newMockDocument(filename: string): TypeMoq.IMock<TextDocument> { - const uri = Uri.file(filename); - const doc = TypeMoq.Mock.ofType<TextDocument>(undefined, TypeMoq.MockBehavior.Strict); - doc.setup(s => s.uri) - .returns(() => uri); - return doc; -} - -export function linterMessageAsLine(msg: ILintMessage): string { - switch (msg.provider) { - case 'pydocstyle': { - return `<filename>:${msg.line} spam:${os.EOL}\t${msg.code}: ${msg.message}`; - } - default: { - return `${msg.line},${msg.column},${msg.type},${msg.code}:${msg.message}`; - } - } -} - -export function getLinterID(product: Product): LinterId { - const linterID = LINTERID_BY_PRODUCT.get(product); - if (!linterID) { - throwUnknownProduct(product); - } - return linterID!; -} - -export function getProductName(product: Product, capitalize = true): string { - let prodName = ProductNames.get(product); - if (!prodName) { - prodName = Product[product]; - } - if (capitalize) { - return prodName.charAt(0).toUpperCase() + prodName.slice(1); - } else { - return prodName; - } -} - -export function throwUnknownProduct(product: Product) { - throw Error(`unsupported product ${Product[product]} (${product})`); -} - -export class LintingSettings { - public enabled: boolean; - public ignorePatterns: string[]; - public prospectorEnabled: boolean; - public prospectorArgs: string[]; - public pylintEnabled: boolean; - public pylintArgs: string[]; - public 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; - - constructor() { - // mostly from configSettings.ts - - this.enabled = true; - this.ignorePatterns = []; - this.lintOnSave = false; - this.maxNumberOfProblems = 100; - - this.flake8Enabled = false; - this.flake8Path = 'flake8'; - this.flake8Args = []; - this.flake8CategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning, - F: DiagnosticSeverity.Warning - }; - - this.mypyEnabled = false; - this.mypyPath = 'mypy'; - this.mypyArgs = []; - this.mypyCategorySeverity = { - error: DiagnosticSeverity.Error, - note: DiagnosticSeverity.Hint - }; - - this.banditEnabled = false; - this.banditPath = 'bandit'; - this.banditArgs = []; - - this.pep8Enabled = false; - this.pep8Path = 'pep8'; - this.pep8Args = []; - this.pep8CategorySeverity = { - E: DiagnosticSeverity.Error, - W: DiagnosticSeverity.Warning - }; - - this.pylamaEnabled = false; - this.pylamaPath = 'pylama'; - this.pylamaArgs = []; - - this.prospectorEnabled = false; - this.prospectorPath = 'prospector'; - this.prospectorArgs = []; - - this.pydocstyleEnabled = false; - this.pydocstylePath = 'pydocstyle'; - this.pydocstyleArgs = []; - - this.pylintEnabled = false; - this.pylintPath = 'pylint'; - this.pylintArgs = []; - this.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning - }; - this.pylintUseMinimalCheckers = false; - } -} - -export class BaseTestFixture { - public serviceContainer: TypeMoq.IMock<IServiceContainer>; - public linterManager: LinterManager; - - // services - public workspaceService: TypeMoq.IMock<IWorkspaceService>; - public logger: TypeMoq.IMock<ILogger>; - public installer: TypeMoq.IMock<IInstaller>; - public appShell: TypeMoq.IMock<IApplicationShell>; - - // config - public configService: TypeMoq.IMock<IConfigurationService>; - public pythonSettings: TypeMoq.IMock<IPythonSettings>; - public lintingSettings: LintingSettings; - - // data - public outputChannel: TypeMoq.IMock<IOutputChannel>; - - // artifacts - public output: string; - public logged: string[]; - - constructor( - platformService: IPlatformService, - filesystem: IFileSystem, - pythonToolExecService: IPythonToolExecutionService, - pythonExecFactory: IPythonExecutionFactory, - configService?: TypeMoq.IMock<IConfigurationService>, - serviceContainer?: TypeMoq.IMock<IServiceContainer>, - ignoreConfigUpdates = false, - public readonly workspaceDir = '.', - protected readonly printLogs = false - ) { - this.serviceContainer = serviceContainer ? serviceContainer : TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); - - // services - - this.workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(undefined, TypeMoq.MockBehavior.Strict); - this.logger = TypeMoq.Mock.ofType<ILogger>(undefined, TypeMoq.MockBehavior.Strict); - this.installer = TypeMoq.Mock.ofType<IInstaller>(undefined, TypeMoq.MockBehavior.Strict); - this.appShell = TypeMoq.Mock.ofType<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())) - .returns(() => filesystem); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())) - .returns(() => this.workspaceService.object); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILogger), TypeMoq.It.isAny())) - .returns(() => this.logger.object); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IInstaller), TypeMoq.It.isAny())) - .returns(() => this.installer.object); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService), TypeMoq.It.isAny())) - .returns(() => platformService); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonToolExecutionService), TypeMoq.It.isAny())) - .returns(() => pythonToolExecService); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPythonExecutionFactory), TypeMoq.It.isAny())) - .returns(() => pythonExecFactory); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())) - .returns(() => this.appShell.object); - this.initServices(); - - // config - - this.configService = configService ? configService : TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); - this.pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(undefined, TypeMoq.MockBehavior.Strict); - this.lintingSettings = new LintingSettings(); - - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())) - .returns(() => this.configService.object); - this.configService.setup(c => c.getSettings(TypeMoq.It.isAny())) - .returns(() => this.pythonSettings.object); - this.pythonSettings.setup(s => s.linting) - .returns(() => this.lintingSettings); - this.initConfig(ignoreConfigUpdates); - - // data - - this.outputChannel = TypeMoq.Mock.ofType<IOutputChannel>(undefined, TypeMoq.MockBehavior.Strict); - - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isAny())) - .returns(() => this.outputChannel.object); - this.initData(); - - // artifacts - - this.output = ''; - this.logged = []; - - // linting - - this.linterManager = new LinterManager( - this.serviceContainer.object, - this.workspaceService.object! - ); - this.serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())) - .returns(() => this.linterManager); - } - - public async getLinter(product: Product, enabled = true): Promise<ILinter> { - const info = this.linterManager.getLinterInfo(product); - // tslint:disable-next-line:no-any - (this.lintingSettings as any)[info.enabledSettingName] = enabled; - - await this.linterManager.setActiveLintersAsync([product]); - await this.linterManager.enableLintingAsync(enabled); - return this.linterManager.createLinter( - product, - this.outputChannel.object, - this.serviceContainer.object - ); - } - - public async getEnabledLinter(product: Product): Promise<ILinter> { - return this.getLinter(product, true); - } - - public async getDisabledLinter(product: Product): Promise<ILinter> { - return this.getLinter(product, false); - } - - protected newMockDocument(filename: string): TypeMoq.IMock<TextDocument> { - return newMockDocument(filename); - } - - private initServices(): void { - const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(undefined, TypeMoq.MockBehavior.Strict); - workspaceFolder.setup(f => f.uri) - .returns(() => Uri.file(this.workspaceDir)); - this.workspaceService.setup(s => s.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder.object); - - this.logger.setup(l => l.logError(TypeMoq.It.isAny())) - .callback(msg => { - this.logged.push(msg); - if (this.printLogs) { - // tslint:disable-next-line:no-console - console.log(msg); - } - }) - .returns(() => undefined); - - this.appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - } - - private initConfig(ignoreUpdates = false): void { - this.configService.setup(c => c.updateSetting(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((setting, value) => { - if (ignoreUpdates) { - return; - } - const prefix = 'linting.'; - if (setting.startsWith(prefix)) { - // tslint:disable-next-line:no-any - (this.lintingSettings as any)[setting.substring(prefix.length)] = value; - } - }) - .returns(() => Promise.resolve(undefined)); - - this.pythonSettings.setup(s => s.jediEnabled) - .returns(() => true); - } - - private initData(): void { - this.outputChannel.setup(o => o.appendLine(TypeMoq.It.isAny())) - .callback(line => { - if (this.output === '') { - this.output = line; - } else { - this.output = `${this.output}${os.EOL}${line}`; - } - }); - this.outputChannel.setup(o => o.append(TypeMoq.It.isAny())) - .callback(data => { - this.output += data; - }); - this.outputChannel.setup(o => o.show()); - } -} diff --git a/src/test/linters/lint.args.test.ts b/src/test/linters/lint.args.test.ts deleted file mode 100644 index c722928338ad..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<IInterpreterService>; - let engine: TypeMoq.IMock<ILintingEngine>; - let configService: TypeMoq.IMock<IConfigurationService>; - let docManager: TypeMoq.IMock<IDocumentManager>; - let settings: TypeMoq.IMock<IPythonSettings>; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let document: TypeMoq.IMock<TextDocument>; - let outputChannel: TypeMoq.IMock<OutputChannel>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - const cancellationToken = new CancellationTokenSource().token; - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); - - const fs = TypeMoq.Mock.ofType<IFileSystem>(); - fs.setup(x => x.fileExists(TypeMoq.It.isAny())).returns(() => new Promise<boolean>((resolve, _reject) => resolve(true))); - fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fs.object); - - serviceManager.addSingletonInstance(IOutputChannel, outputChannel.object); - - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreterService.object); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - engine = TypeMoq.Mock.ofType<ILintingEngine>(); - serviceManager.addSingletonInstance<ILintingEngine>(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType<IDocumentManager>(); - serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType<ILintingSettings>(); - lintSettings.setup(x => x.enabled).returns(() => true); - lintSettings.setup(x => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(x => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configService.object); - - const workspaceFolder: WorkspaceFolder | undefined = workspaceUri ? { uri: Uri.file(workspaceUri), index: 0, name: '' } : undefined; - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - workspaceService.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); - serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspaceService.object); - - const logger = TypeMoq.Mock.ofType<ILogger>(); - serviceManager.addSingletonInstance<ILogger>(ILogger, logger.object); - - const installer = TypeMoq.Mock.ofType<IInstaller>(); - serviceManager.addSingletonInstance<IInstaller>(IInstaller, installer.object); - - const platformService = TypeMoq.Mock.ofType<IPlatformService>(); - serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platformService.object); - - lm = new LinterManager(serviceContainer, workspaceService.object); - serviceManager.addSingletonInstance<ILinterManager>(ILinterManager, lm); - document = TypeMoq.Mock.ofType<TextDocument>(); - }); - - async function testLinter(linter: BaseLinter, expectedArgs: string[]) { - document.setup(d => d.uri).returns(() => fileUri); - - let invoked = false; - (linter as any).run = (args: string[]) => { - expect(args).to.deep.equal(expectedArgs); - invoked = true; - return Promise.resolve([]); - }; - await linter.lint(document.object, cancellationToken); - expect(invoked).to.be.equal(true, 'method not invoked'); - } - test('Flake8', async () => { - const linter = new Flake8(outputChannel.object, serviceContainer); - const expectedArgs = ['--format=%(row)d,%(col)d,%(code).1s,%(code)s:%(text)s', fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - test('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: any[], _doc: any, _token: any) => { - expect(args[args.length - 1]).to.equal(fileUri.fsPath); - invoked = true; - return Promise.resolve([]); - }; - await linter.lint(document.object, cancellationToken); - expect(invoked).to.be.equal(true, 'method not invoked'); - }); - test('Bandit', async () => { - const linter = new Bandit(outputChannel.object, serviceContainer); - const expectedArgs = ['-f', 'custom', '--msg-template', '{line},0,{severity},{test_id}:{msg}', '-n', '-1', fileUri.fsPath]; - await testLinter(linter, expectedArgs); - }); - }); - }); - }); -}); diff --git a/src/test/linters/lint.functional.test.ts b/src/test/linters/lint.functional.test.ts deleted file mode 100644 index cdfb83cf3006..000000000000 --- a/src/test/linters/lint.functional.test.ts +++ /dev/null @@ -1,384 +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 os from 'os'; -import * as path from 'path'; -import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, - TextDocument, - TextLine, - Uri -} from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { PlatformService } from '../../client/common/platform/platformService'; -import { BufferDecoder } from '../../client/common/process/decoder'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { PythonExecutionFactory } from '../../client/common/process/pythonExecutionFactory'; -import { PythonToolExecutionService } from '../../client/common/process/pythonToolService'; -import { - IBufferDecoder, - IPythonExecutionFactory, - IPythonToolExecutionService -} from '../../client/common/process/types'; -import { - IConfigurationService, IDisposableRegistry -} from '../../client/common/types'; -import { - IEnvironmentVariablesProvider -} from '../../client/common/variables/types'; -import { - IEnvironmentActivationService -} from '../../client/interpreter/activation/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { - ILintMessage, - LinterId, - LintMessageSeverity -} from '../../client/linters/types'; -import { deleteFile, PYTHON_PATH } from '../common'; -import { - BaseTestFixture, - getLinterID, - getProductName, - newMockDocument, - throwUnknownProduct -} from './common'; - -const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const workspaceUri = Uri.file(workspaceDir); -const pythonFilesDir = path.join(workspaceDir, 'pythonFiles', 'linting'); -const fileToLint = path.join(pythonFilesDir, 'file.py'); - -const linterConfigDirs = new Map<LinterId, string>([ - ['flake8', path.join(pythonFilesDir, 'flake8config')], - ['pep8', path.join(pythonFilesDir, 'pep8config')], - ['pydocstyle', path.join(pythonFilesDir, 'pydocstyleconfig27')], - ['pylint', path.join(pythonFilesDir, 'pylintconfig')] -]); -const linterConfigRCFiles = new Map<LinterId, string>([ - ['pylint', '.pylintrc'], - ['pydocstyle', '.pydocstyle'] -]); - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { line: 24, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 30, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 34, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 40, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 44, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 55, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 59, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 62, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling undefined-variable (E0602)', provider: '', type: 'warning' }, - { line: 70, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 84, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 87, column: 0, severity: LintMessageSeverity.Hint, code: 'C0304', message: 'Final newline missing', provider: '', type: 'warning' }, - { line: 11, column: 20, severity: LintMessageSeverity.Warning, code: 'W0613', message: 'Unused argument \'arg\'', provider: '', type: 'warning' }, - { line: 26, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', provider: '', type: 'warning' }, - { line: 36, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 46, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 61, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 72, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 75, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 77, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 83, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' } -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { line: 5, column: 1, severity: LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: 'E' }, - { line: 19, column: 15, severity: LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: 'E' }, - { line: 24, column: 23, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 62, column: 30, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 70, column: 22, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 80, column: 5, severity: LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: 'E' }, - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: 'E' } -]; -const pep8MessagesToBeReturned: ILintMessage[] = [ - { line: 5, column: 1, severity: LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: 'E' }, - { line: 19, column: 15, severity: LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: 'E' }, - { line: 24, column: 23, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 62, column: 30, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 70, column: 22, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 80, column: 5, severity: LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: 'E' }, - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: 'E' } -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 0, line: 1, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 0, line: 5, type: '', provider: 'pydocstyle' }, - { code: 'D102', severity: LintMessageSeverity.Information, message: 'Missing docstring in public method', column: 4, line: 8, type: '', provider: 'pydocstyle' }, - { code: 'D401', severity: LintMessageSeverity.Information, message: 'First line should be in imperative mood (\'thi\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'This\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'And\', not \'and\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 80, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 80, type: '', provider: 'pydocstyle' } -]; - -const filteredFlake8MessagesToBeReturned: ILintMessage[] = [ - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } -]; -const filteredPep8MessagesToBeReturned: ILintMessage[] = [ - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: '' } -]; - -function getMessages(product: Product): ILintMessage[] { - switch (product) { - case Product.pylint: { - return pylintMessagesToBeReturned; - } - case Product.flake8: { - return flake8MessagesToBeReturned; - } - case Product.pep8: { - return pep8MessagesToBeReturned; - } - case Product.pydocstyle: { - return pydocstyleMessagesToBeReturned; - } - default: { - throwUnknownProduct(product); - return []; // to quiet tslint - } - } -} - -async function getInfoForConfig(product: Product) { - const prodID = getLinterID(product); - const dirname = linterConfigDirs.get(prodID); - assert.notEqual(dirname, undefined, `tests not set up for ${Product[product]}`); - - const filename = path.join(dirname!, product === Product.pylint ? 'file2.py' : 'file.py'); - let messagesToBeReceived: ILintMessage[] = []; - switch (product) { - case Product.flake8: { - messagesToBeReceived = filteredFlake8MessagesToBeReturned; - break; - } - case Product.pep8: { - messagesToBeReceived = filteredPep8MessagesToBeReturned; - break; - } - default: { break; } - } - const basename = linterConfigRCFiles.get(prodID); - return { - filename, - messagesToBeReceived, - origRCFile: basename ? path.join(dirname!, basename) : '' - }; -} - -class TestFixture extends BaseTestFixture { - constructor( - printLogs = false - ) { - const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); - const configService = TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); - - const platformService = new PlatformService(); - const filesystem = new FileSystem(platformService); - - super( - platformService, - filesystem, - TestFixture.newPythonToolExecService( - serviceContainer.object - ), - TestFixture.newPythonExecFactory( - serviceContainer, - configService.object - ), - configService, - serviceContainer, - false, - workspaceDir, - printLogs - ); - - this.pythonSettings.setup(s => s.pythonPath) - .returns(() => PYTHON_PATH); - } - - private static newPythonToolExecService( - serviceContainer: IServiceContainer - ): IPythonToolExecutionService { - // We do not worry about the IProcessServiceFactory possibly - // needed by PythonToolExecutionService. - return new PythonToolExecutionService( - serviceContainer - ); - } - - private static newPythonExecFactory( - serviceContainer: TypeMoq.IMock<IServiceContainer>, - configService: IConfigurationService - ): IPythonExecutionFactory { - const envVarsService = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(undefined, TypeMoq.MockBehavior.Strict); - envVarsService.setup(e => e.getEnvironmentVariables(TypeMoq.It.isAny())) - .returns(() => Promise.resolve({})); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IEnvironmentVariablesProvider), TypeMoq.It.isAny())) - .returns(() => envVarsService.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny())) - .returns(() => []); - - const envActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(undefined, TypeMoq.MockBehavior.Strict); - - const decoder = new BufferDecoder(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IBufferDecoder), TypeMoq.It.isAny())) - .returns(() => decoder); - - const procServiceFactory = new ProcessServiceFactory(serviceContainer.object); - - return new PythonExecutionFactory( - serviceContainer.object, - envActivationService.object, - procServiceFactory, - configService, - decoder - ); - } - - public makeDocument(filename: string): TextDocument { - const doc = newMockDocument(filename); - doc.setup(d => d.lineAt(TypeMoq.It.isAny())) - .returns(lno => { - const lines = fs.readFileSync(filename) - .toString() - .split(os.EOL); - const textline = TypeMoq.Mock.ofType<TextLine>(undefined, TypeMoq.MockBehavior.Strict); - textline.setup(t => t.text) - .returns(() => lines[lno]); - return textline.object; - }); - return doc.object; - } -} - -// tslint:disable-next-line:max-func-body-length -suite('Linting Functional Tests', () => { - // These are integration tests that mock out everything except - // the filesystem and process execution. - // tslint:disable-next-line:no-any - async function testLinterMessages( - fixture: TestFixture, - product: Product, - pythonFile: string, - messagesToBeReceived: ILintMessage[] - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter( - product, - fixture.outputChannel.object, - fixture.serviceContainer.object - ); - - const messages = await linter.lint( - doc, - (new CancellationTokenSource()).token - ); - - if (messagesToBeReceived.length === 0) { - assert.equal(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else { - if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(getProductName(product), async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some(p => p === product)) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const fixture = new TestFixture(); - const messagesToBeReturned = getMessages(product); - await testLinterMessages(fixture, product, fileToLint, messagesToBeReturned); - }); - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - // tslint:disable-next-line:max-func-body-length - test(`${getProductName(product)} with config in root`, async function () { - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some(p => p === product)) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const fixture = new TestFixture(); - if (product === Product.pydocstyle) { - fixture.lintingSettings.pylintUseMinimalCheckers = false; - } - - const { filename, messagesToBeReceived, origRCFile } = await getInfoForConfig(product); - let rcfile = ''; - async function cleanUp() { - if (rcfile !== '') { - await deleteFile(rcfile); - } - } - if (origRCFile !== '') { - rcfile = path.join(workspaceUri.fsPath, path.basename(origRCFile)); - await fs.copy(origRCFile, rcfile); - } - - try { - await testLinterMessages(fixture, product, filename, messagesToBeReceived); - } finally { - await cleanUp(); - } - }); - } - - async function testLinterMessageCount( - fixture: TestFixture, - product: Product, - pythonFile: string, - messageCountToBeReceived: number - ) { - const doc = fixture.makeDocument(pythonFile); - await fixture.linterManager.setActiveLintersAsync([product], doc.uri); - const linter = await fixture.linterManager.createLinter( - product, - fixture.outputChannel.object, - fixture.serviceContainer.object - ); - - const messages = await linter.lint( - doc, - (new CancellationTokenSource()).token - ); - - assert.equal(messages.length, messageCountToBeReceived, - 'Expected number of lint errors does not match lint error count'); - } - test('Three line output counted as one message', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - await testLinterMessageCount( - fixture, - Product.pylint, - path.join(pythonFilesDir, 'threeLineLints.py'), - maxErrors - ); - }); -}); diff --git a/src/test/linters/lint.manager.unit.test.ts b/src/test/linters/lint.manager.unit.test.ts deleted file mode 100644 index 7b31fc485379..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<void> { - this.enableUnconfiguredLintersCallCount += 1; - } -} - -function getServiceContainerMockForLinterManagerTests(): TypeMoq.IMock<IServiceContainer> { - // setup test mocks - const serviceContainerMock = TypeMoq.Mock.ofType<IServiceContainer>(); - const configMock = TypeMoq.Mock.ofType<IConfigurationService>(); - const pythonSettingsMock = TypeMoq.Mock.ofType<IPythonSettings>(); - 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<IWorkspaceService>(); - 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 b6082fdb5547..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), <anonymous>:1\n1,7,E,E999:SyntaxError: invalid syntax\n'; - public pylintMsg: string = '************* Module print\ns:142:13), <anonymous>:1\n1,0,error,syntax-error:Missing parentheses in call to \'print\'. Did you mean print(x)? (<unknown>, line 1)\n'; - - // Depending on moduleName being exec'd, return the appropriate sample. - public async exec(executionInfo: ExecutionInfo, _options: SpawnOptions, _resource: Uri): Promise<ExecutionResult<string>> { - let msg = this.flake8Msg; - if (executionInfo.moduleName === 'pylint') { - msg = this.pylintMsg; - } - return { stdout: msg }; - } -} - -// tslint:disable-next-line:max-func-body-length -suite('Linting - Multiple Linters Enabled Test', () => { - let api: IExtensionTestApi; - let configService: IConfigurationService; - let linterManager: ILinterManager; - - suiteSetup(async () => { - api = await initialize(); - configService = api.serviceContainer.get<IConfigurationService>(IConfigurationService); - linterManager = api.serviceContainer.get<ILinterManager>(ILinterManager); - }); - setup(async () => { - await initializeTest(); - await resetSettings(); - - // We only want to return some valid strings from linters, we don't care if they - // are being returned by actual linters (we aren't testing linters here, only how - // our code responds to those linters). - api.serviceManager.rebind<IPythonToolExecutionService>(IPythonToolExecutionService, MockPythonToolExecService); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await closeActiveWindows(); - await resetSettings(); - await deleteFile(path.join(workspaceUri.fsPath, '.pylintrc')); - await deleteFile(path.join(workspaceUri.fsPath, '.pydocstyle')); - - // Restore the execution service as it was... - api.serviceManager.rebind<IPythonToolExecutionService>(IPythonToolExecutionService, PythonToolExecutionService); - }); - - async function resetSettings() { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', true, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); - - linterManager.getAllLinterInfos().forEach(async (x) => { - await configService.updateSetting(makeSettingKey(x.product), false, rootWorkspaceUri, target); - }); - } - - function makeSettingKey(product: Product): PythonSettingKeys { - return `linting.${linterManager.getLinterInfo(product).enabledSettingName}` as PythonSettingKeys; - } - - test('Multiple linters', async () => { - await closeActiveWindows(); - const document = await workspace.openTextDocument(path.join(pythoFilesPath, 'print.py')); - await window.showTextDocument(document); - await configService.updateSetting('linting.enabled', true, workspaceUri); - await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); - await configService.updateSetting('linting.pylintEnabled', true, workspaceUri); - await configService.updateSetting('linting.flake8Enabled', true, workspaceUri); - - const commands = api.serviceContainer.get<ICommandManager>(ICommandManager); - - const collection = await commands.executeCommand('python.runLinting') as DiagnosticCollection; - assert.notEqual(collection, undefined, 'python.runLinting did not return valid diagnostics collection.'); - - const messages = collection!.get(document.uri); - assert.notEqual(messages!.length, 0, 'No diagnostic messages.'); - assert.notEqual(messages!.filter(x => x.source === 'pylint').length, 0, 'No pylint messages.'); - assert.notEqual(messages!.filter(x => x.source === 'flake8').length, 0, 'No flake8 messages.'); - }); -}); diff --git a/src/test/linters/lint.multiroot.test.ts b/src/test/linters/lint.multiroot.test.ts deleted file mode 100644 index fb328c00b914..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/testing/common/constants'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -// tslint:disable:max-func-body-length no-invalid-this - -const multirootPath = path.join(__dirname, '..', '..', '..', 'src', 'testMultiRootWkspc'); - -suite('Multiroot Linting', () => { - const pylintSetting = 'linting.pylintEnabled'; - const flake8Setting = 'linting.flake8Enabled'; - - let ioc: UnitTestIocContainer; - suiteSetup(function () { - if (!IS_MULTI_ROOT_TEST) { - this.skip(); - } - return initialize(); - }); - setup(async () => { - initializeDI(); - await initializeTest(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - PythonSettings.dispose(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerPlatformTypes(); - 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 createLinter(product: Product, resource?: Uri): Promise<ILinter> { - const mockOutputChannel = ioc.serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL); - const lm = ioc.serviceContainer.get<ILinterManager>(ILinterManager); - await lm.setActiveLintersAsync([product], resource); - return lm.createLinter(product, mockOutputChannel, ioc.serviceContainer); - } - async function testLinterInWorkspaceFolder(product: Product, workspaceFolderRelativePath: string, mustHaveErrors: boolean): Promise<void> { - const fileToLint = path.join(multirootPath, workspaceFolderRelativePath, 'file.py'); - const cancelToken = new CancellationTokenSource(); - const document = await workspace.openTextDocument(fileToLint); - - const linter = await createLinter(product); - const messages = await linter.lint(document, cancelToken.token); - - const errorMessage = mustHaveErrors ? 'No errors returned by linter' : 'Errors returned by linter'; - assert.equal(messages.length > 0, mustHaveErrors, errorMessage); - } - async function enableDisableSetting(workspaceFolder: string, configTarget: ConfigurationTarget, setting: string, value: boolean): Promise<void> { - const config = ioc.serviceContainer.get<IConfigurationService>(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<void> { - 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 90c7643aca4f..000000000000 --- a/src/test/linters/lint.provider.test.ts +++ /dev/null @@ -1,188 +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 { PersistentStateFactory } from '../../client/common/persistentState'; -import { IFileSystem } from '../../client/common/platform/types'; -import { - GLOBAL_MEMENTO, IConfigurationService, IInstaller, - ILintingSettings, IMemento, IPersistentStateFactory, IPythonSettings, Product, WORKSPACE_MEMENTO -} from '../../client/common/types'; -import { createDeferred } from '../../client/common/utils/async'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../../client/interpreter/autoSelection/types'; -import { IInterpreterService } from '../../client/interpreter/contracts'; -import { ServiceContainer } from '../../client/ioc/container'; -import { ServiceManager } from '../../client/ioc/serviceManager'; -import { AvailableLinterActivator } from '../../client/linters/linterAvailability'; -import { LinterManager } from '../../client/linters/linterManager'; -import { - IAvailableLinterActivator, ILinterManager, ILintingEngine -} from '../../client/linters/types'; -import { LinterProvider } from '../../client/providers/linterProvider'; -import { initialize } from '../initialize'; -import { MockAutoSelectionService } from '../mocks/autoSelector'; -import { MockMemento } from '../mocks/mementos'; - -// tslint:disable-next-line:max-func-body-length -suite('Linting - Provider', () => { - let context: TypeMoq.IMock<vscode.ExtensionContext>; - let interpreterService: TypeMoq.IMock<IInterpreterService>; - let engine: TypeMoq.IMock<ILintingEngine>; - let configService: TypeMoq.IMock<IConfigurationService>; - let docManager: TypeMoq.IMock<IDocumentManager>; - let settings: TypeMoq.IMock<IPythonSettings>; - let lm: ILinterManager; - let serviceContainer: ServiceContainer; - let emitter: vscode.EventEmitter<vscode.TextDocument>; - let document: TypeMoq.IMock<vscode.TextDocument>; - let fs: TypeMoq.IMock<IFileSystem>; - let appShell: TypeMoq.IMock<IApplicationShell>; - let linterInstaller: TypeMoq.IMock<IInstaller>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - - suiteSetup(initialize); - setup(async () => { - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - - serviceContainer = new ServiceContainer(cont); - context = TypeMoq.Mock.ofType<vscode.ExtensionContext>(); - - fs = TypeMoq.Mock.ofType<IFileSystem>(); - fs.setup(x => x.fileExists(TypeMoq.It.isAny())).returns(() => new Promise<boolean>((resolve, _reject) => resolve(true))); - fs.setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())).returns(() => true); - serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fs.object); - - interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); - serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, interpreterService.object); - - engine = TypeMoq.Mock.ofType<ILintingEngine>(); - serviceManager.addSingletonInstance<ILintingEngine>(ILintingEngine, engine.object); - - docManager = TypeMoq.Mock.ofType<IDocumentManager>(); - serviceManager.addSingletonInstance<IDocumentManager>(IDocumentManager, docManager.object); - - const lintSettings = TypeMoq.Mock.ofType<ILintingSettings>(); - lintSettings.setup(x => x.enabled).returns(() => true); - lintSettings.setup(x => x.lintOnSave).returns(() => true); - - settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(x => x.linting).returns(() => lintSettings.object); - - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, configService.object); - - appShell = TypeMoq.Mock.ofType<IApplicationShell>(); - linterInstaller = TypeMoq.Mock.ofType<IInstaller>(); - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceManager.addSingletonInstance<IApplicationShell>(IApplicationShell, appShell.object); - serviceManager.addSingletonInstance<IInstaller>(IInstaller, linterInstaller.object); - serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspaceService.object); - serviceManager.add(IAvailableLinterActivator, AvailableLinterActivator); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory); - serviceManager.addSingleton<vscode.Memento>(IMemento, MockMemento, GLOBAL_MEMENTO); - serviceManager.addSingleton<vscode.Memento>(IMemento, MockMemento, WORKSPACE_MEMENTO); - lm = new LinterManager(serviceContainer, workspaceService.object); - serviceManager.addSingletonInstance<ILinterManager>(ILinterManager, lm); - emitter = new vscode.EventEmitter<vscode.TextDocument>(); - document = TypeMoq.Mock.ofType<vscode.TextDocument>(); - }); - - test('Lint on open file', () => { - 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-expression - 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-expression - 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-expression - 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-expression - 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<void>(); - interpreterService.setup(x => x.onDidChangeInterpreter).returns(() => e.event); - - // tslint:disable-next-line:no-unused-expression - 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-expression - new LinterProvider(context.object, serviceContainer); - emitter.fire(document.object); - - const deferred = createDeferred<void>(); - setTimeout(() => deferred.resolve(), 2000); - await deferred.promise; - engine.verify(x => x.lintOpenPythonFiles(), TypeMoq.Times.once()); - }); - - test('Diagnostic cleared on file close', () => 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 - // tslint:disable-next-line:no-unused-expression - 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 0e09b5604809..000000000000 --- a/src/test/linters/lint.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { ConfigurationTarget, Uri } from 'vscode'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { Product } from '../../client/common/installer/productInstaller'; -import { - CTagsProductPathService, FormatterProductPathService, LinterProductPathService, - RefactoringLibraryProductPathService, TestFrameworkProductPathService -} from '../../client/common/installer/productPath'; -import { ProductService } from '../../client/common/installer/productService'; -import { IProductPathService, IProductService } from '../../client/common/installer/types'; -import { IConfigurationService, ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { LinterManager } from '../../client/linters/linterManager'; -import { ILinterManager } from '../../client/linters/types'; -import { rootWorkspaceUri } from '../common'; -import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; -import { UnitTestIocContainer } from '../testing/serviceRegistry'; - -const workspaceDir = path.join(__dirname, '..', '..', '..', 'src', 'test'); -const workspaceUri = Uri.file(workspaceDir); - -// tslint:disable-next-line:max-func-body-length -suite('Linting Settings', () => { - let ioc: UnitTestIocContainer; - let linterManager: ILinterManager; - let configService: IConfigurationService; - - suiteSetup(async function () { - // These tests are still consistently failing during teardown. - // See gh-4326. - // tslint:disable-next-line:no-invalid-this - this.skip(); - - await initialize(); - }); - setup(async () => { - initializeDI(); - await initializeTest(); - }); - suiteTeardown(closeActiveWindows); - teardown(async () => { - await ioc.dispose(); - await closeActiveWindows(); - await resetSettings(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(false); - ioc.registerProcessTypes(); - ioc.registerLinterTypes(); - ioc.registerVariableTypes(); - ioc.registerPlatformTypes(); - linterManager = new LinterManager(ioc.serviceContainer, new WorkspaceService()); - configService = ioc.serviceContainer.get<IConfigurationService>(IConfigurationService); - ioc.serviceManager.addSingletonInstance<IProductService>(IProductService, new ProductService()); - ioc.serviceManager.addSingleton<IProductPathService>(IProductPathService, CTagsProductPathService, ProductType.WorkspaceSymbols); - ioc.serviceManager.addSingleton<IProductPathService>(IProductPathService, FormatterProductPathService, ProductType.Formatter); - ioc.serviceManager.addSingleton<IProductPathService>(IProductPathService, LinterProductPathService, ProductType.Linter); - ioc.serviceManager.addSingleton<IProductPathService>(IProductPathService, TestFrameworkProductPathService, ProductType.TestFramework); - ioc.serviceManager.addSingleton<IProductPathService>(IProductPathService, RefactoringLibraryProductPathService, ProductType.RefactoringLibrary); - } - - async function resetSettings(lintingEnabled = true) { - // Don't run these updates in parallel, as they are updating the same file. - const target = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - await configService.updateSetting('linting.enabled', lintingEnabled, rootWorkspaceUri, target); - await configService.updateSetting('linting.lintOnSave', false, rootWorkspaceUri, target); - await configService.updateSetting('linting.pylintUseMinimalCheckers', false, workspaceUri); - - linterManager.getAllLinterInfos().forEach(async (x) => { - const settingKey = `linting.${x.enabledSettingName}`; - await configService.updateSetting(settingKey, false, rootWorkspaceUri, target); - }); - } - - test('enable through manager (global)', async () => { - const settings = configService.getSettings(); - await resetSettings(false); - - await linterManager.enableLintingAsync(false); - assert.equal(settings.linting.enabled, false, 'mismatch'); - - await linterManager.enableLintingAsync(true); - assert.equal(settings.linting.enabled, true, 'mismatch'); - }); - - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`enable through manager (${Product[product]})`, async () => { - const settings = configService.getSettings(); - await resetSettings(); - - // tslint:disable-next-line:no-any - assert.equal((settings.linting as any)[`${Product[product]}Enabled`], false, 'mismatch'); - - await linterManager.setActiveLintersAsync([product]); - - // tslint:disable-next-line:no-any - assert.equal((settings.linting as any)[`${Product[product]}Enabled`], true, 'mismatch'); - linterManager.getAllLinterInfos().forEach(async (x) => { - if (x.product !== product) { - // tslint:disable-next-line:no-any - assert.equal((settings.linting as any)[x.enabledSettingName], false, 'mismatch'); - } - }); - }); - } -}); diff --git a/src/test/linters/lint.unit.test.ts b/src/test/linters/lint.unit.test.ts deleted file mode 100644 index 5832764fef50..000000000000 --- a/src/test/linters/lint.unit.test.ts +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; - -import * as assert from 'assert'; -import * as os from 'os'; -import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, - TextDocument, - TextLine -} from 'vscode'; -import { Product } from '../../client/common/installer/productInstaller'; -import { ProductNames } from '../../client/common/installer/productNames'; -import { ProductService } from '../../client/common/installer/productService'; -import { - IFileSystem, - IPlatformService -} from '../../client/common/platform/types'; -import { - IPythonExecutionFactory, - IPythonExecutionService, - IPythonToolExecutionService -} from '../../client/common/process/types'; -import { ProductType } from '../../client/common/types'; -import { LINTERID_BY_PRODUCT } from '../../client/linters/constants'; -import { - ILintMessage, - LintMessageSeverity -} from '../../client/linters/types'; -import { - BaseTestFixture, - getLinterID, - getProductName, - linterMessageAsLine, - throwUnknownProduct -} from './common'; - -const pylintMessagesToBeReturned: ILintMessage[] = [ - { line: 24, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 30, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 34, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 40, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 44, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 55, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 59, column: 0, severity: LintMessageSeverity.Information, code: 'I0012', message: 'Locally enabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 62, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling undefined-variable (E0602)', provider: '', type: 'warning' }, - { line: 70, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 84, column: 0, severity: LintMessageSeverity.Information, code: 'I0011', message: 'Locally disabling no-member (E1101)', provider: '', type: 'warning' }, - { line: 87, column: 0, severity: LintMessageSeverity.Hint, code: 'C0304', message: 'Final newline missing', provider: '', type: 'warning' }, - { line: 11, column: 20, severity: LintMessageSeverity.Warning, code: 'W0613', message: 'Unused argument \'arg\'', provider: '', type: 'warning' }, - { line: 26, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blop\' member', provider: '', type: 'warning' }, - { line: 36, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 46, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 61, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 72, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 75, column: 18, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 77, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' }, - { line: 83, column: 14, severity: LintMessageSeverity.Error, code: 'E1101', message: 'Instance of \'Foo\' has no \'blip\' member', provider: '', type: 'warning' } -]; -const flake8MessagesToBeReturned: ILintMessage[] = [ - { line: 5, column: 1, severity: LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: 'E' }, - { line: 19, column: 15, severity: LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: 'E' }, - { line: 24, column: 23, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 62, column: 30, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 70, column: 22, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 80, column: 5, severity: LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: 'E' }, - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: 'E' } -]; -const pep8MessagesToBeReturned: ILintMessage[] = [ - { line: 5, column: 1, severity: LintMessageSeverity.Error, code: 'E302', message: 'expected 2 blank lines, found 1', provider: '', type: 'E' }, - { line: 19, column: 15, severity: LintMessageSeverity.Error, code: 'E127', message: 'continuation line over-indented for visual indent', provider: '', type: 'E' }, - { line: 24, column: 23, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 62, column: 30, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 70, column: 22, severity: LintMessageSeverity.Error, code: 'E261', message: 'at least two spaces before inline comment', provider: '', type: 'E' }, - { line: 80, column: 5, severity: LintMessageSeverity.Error, code: 'E303', message: 'too many blank lines (2)', provider: '', type: 'E' }, - { line: 87, column: 24, severity: LintMessageSeverity.Warning, code: 'W292', message: 'no newline at end of file', provider: '', type: 'E' } -]; -const pydocstyleMessagesToBeReturned: ILintMessage[] = [ - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 0, line: 1, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 0, line: 5, type: '', provider: 'pydocstyle' }, - { code: 'D102', severity: LintMessageSeverity.Information, message: 'Missing docstring in public method', column: 4, line: 8, type: '', provider: 'pydocstyle' }, - { code: 'D401', severity: LintMessageSeverity.Information, message: 'First line should be in imperative mood (\'thi\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'This\', not \'this\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'e\')', column: 4, line: 11, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'And\', not \'and\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'t\')', column: 4, line: 15, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 21, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 28, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 38, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 53, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 68, type: '', provider: 'pydocstyle' }, - { code: 'D403', severity: LintMessageSeverity.Information, message: 'First word of the first line should be properly capitalized (\'Test\', not \'test\')', column: 4, line: 80, type: '', provider: 'pydocstyle' }, - { code: 'D400', severity: LintMessageSeverity.Information, message: 'First line should end with a period (not \'g\')', column: 4, line: 80, type: '', provider: 'pydocstyle' } -]; - -class TestFixture extends BaseTestFixture { - public platformService: TypeMoq.IMock<IPlatformService>; - public filesystem: TypeMoq.IMock<IFileSystem>; - public pythonToolExecService: TypeMoq.IMock<IPythonToolExecutionService>; - public pythonExecService: TypeMoq.IMock<IPythonExecutionService>; - public pythonExecFactory: TypeMoq.IMock<IPythonExecutionFactory>; - - constructor( - workspaceDir = '.', - printLogs = false - ) { - const platformService = TypeMoq.Mock.ofType<IPlatformService>(undefined, TypeMoq.MockBehavior.Strict); - const filesystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); - const pythonToolExecService = TypeMoq.Mock.ofType<IPythonToolExecutionService>(undefined, TypeMoq.MockBehavior.Strict); - const pythonExecFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(undefined, TypeMoq.MockBehavior.Strict); - super( - platformService.object, - filesystem.object, - pythonToolExecService.object, - pythonExecFactory.object, - undefined, - undefined, - true, - workspaceDir, - printLogs - ); - - this.platformService = platformService; - this.filesystem = filesystem; - this.pythonToolExecService = pythonToolExecService; - this.pythonExecService = TypeMoq.Mock.ofType<IPythonExecutionService>(undefined, TypeMoq.MockBehavior.Strict); - this.pythonExecFactory = pythonExecFactory; - - this.filesystem.setup(f => f.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - // tslint:disable-next-line:no-any - this.pythonExecService.setup((s: any) => s.then) - .returns(() => undefined); - this.pythonExecService.setup(s => s.isModuleInstalled(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); - - this.pythonExecFactory.setup(f => f.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(this.pythonExecService.object)); - } - - public makeDocument(product: Product, filename: string): TextDocument { - const doc = this.newMockDocument(filename); - if (product === Product.pydocstyle) { - const dummyLine = TypeMoq.Mock.ofType<TextLine>(undefined, TypeMoq.MockBehavior.Strict); - dummyLine.setup(d => d.text) - .returns(() => ' ...'); - doc.setup(s => s.lineAt(TypeMoq.It.isAny())) - .returns(() => dummyLine.object); - } - return doc.object; - } - - public setDefaultMessages(product: Product): ILintMessage[] { - let messages: ILintMessage[]; - switch (product) { - case Product.pylint: { - messages = pylintMessagesToBeReturned; - break; - } - case Product.flake8: { - messages = flake8MessagesToBeReturned; - break; - } - case Product.pep8: { - messages = pep8MessagesToBeReturned; - break; - } - case Product.pydocstyle: { - messages = pydocstyleMessagesToBeReturned; - break; - } - default: { - throwUnknownProduct(product); - return []; // to quiet tslint - } - } - this.setMessages(messages, product); - return messages; - } - - public setMessages(messages: ILintMessage[], product?: Product) { - if (messages.length === 0) { - this.setStdout(''); - return; - } - - const lines: string[] = []; - for (const msg of messages) { - if (msg.provider === '' && product) { - msg.provider = getLinterID(product); - } - const line = linterMessageAsLine(msg); - lines.push(line); - } - this.setStdout(lines.join(os.EOL) + os.EOL); - } - - public setStdout(stdout: string) { - this.pythonToolExecService.setup(s => s.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: stdout })); - } -} - -// tslint:disable-next-line:max-func-body-length -suite('Linting Scenarios', () => { - // Note that these aren't actually unit tests. Instead they are - // integration tests with heavy usage of mocks. - - test('No linting with PyLint (enabled) when disabled at top-level', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = false; - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - (new CancellationTokenSource()).token - ); - - assert.equal(messages.length, 0, `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`); - }); - - test('No linting with Pylint disabled (and Flake8 enabled)', async () => { - const product = Product.pylint; - const fixture = new TestFixture(); - fixture.lintingSettings.enabled = true; - fixture.lintingSettings.flake8Enabled = true; - fixture.setDefaultMessages(Product.pylint); - const linter = await fixture.getDisabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - (new CancellationTokenSource()).token - ); - - assert.equal(messages.length, 0, `Unexpected linter errors when linting is disabled, Output - ${fixture.output}`); - }); - - async function testEnablingDisablingOfLinter( - fixture: TestFixture, - product: Product, - enabled: boolean - ) { - fixture.lintingSettings.enabled = true; - fixture.setDefaultMessages(product); - if (enabled) { - fixture.setDefaultMessages(product); - } - const linter = await fixture.getLinter(product, enabled); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - (new CancellationTokenSource()).token - ); - - if (enabled) { - assert.notEqual(messages.length, 0, `Expected linter errors when linter is enabled, Output - ${fixture.output}`); - } else { - assert.equal(messages.length, 0, `Unexpected linter errors when linter is disabled, Output - ${fixture.output}`); - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - for (const enabled of [false, true]) { - test(`${enabled ? 'Enable' : 'Disable'} ${getProductName(product)} and run linter`, async function () { - // tslint:disable-next-line:no-suspicious-comment - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some(p => p === product)) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - - const fixture = new TestFixture(); - await testEnablingDisablingOfLinter(fixture, product, enabled); - }); - } - } - for (const useMinimal of [true, false]) { - for (const enabled of [true, false]) { - test(`PyLint ${enabled ? 'enabled' : 'disabled'} with${useMinimal ? '' : 'out'} minimal checkers`, async () => { - const fixture = new TestFixture(); - fixture.lintingSettings.pylintUseMinimalCheckers = useMinimal; - await testEnablingDisablingOfLinter(fixture, Product.pylint, enabled); - }); - } - } - - async function testLinterMessages( - fixture: TestFixture, - product: Product - ) { - const messagesToBeReceived = fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - (new CancellationTokenSource()).token - ); - - if (messagesToBeReceived.length === 0) { - assert.equal(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } else { - if (fixture.output.indexOf('ENOENT') === -1) { - // Pylint for Python Version 2.7 could return 80 linter messages, where as in 3.5 it might only return 1. - // Looks like pylint stops linting as soon as it comes across any ERRORS. - assert.notEqual(messages.length, 0, `No errors in linter, Output - ${fixture.output}`); - } - } - } - for (const product of LINTERID_BY_PRODUCT.keys()) { - test(`Check ${getProductName(product)} messages`, async function () { - // tslint:disable-next-line:no-suspicious-comment - // TODO: Add coverage for these linters. - if ([Product.bandit, Product.mypy, Product.pylama, Product.prospector].some(p => p === product)) { - // tslint:disable-next-line:no-invalid-this - this.skip(); - } - - const fixture = new TestFixture(); - await testLinterMessages(fixture, product); - }); - } - - async function testLinterMessageCount( - fixture: TestFixture, - product: Product, - messageCountToBeReceived: number - ) { - fixture.setDefaultMessages(product); - const linter = await fixture.getEnabledLinter(product); - - const messages = await linter.lint( - fixture.makeDocument(product, 'spam.py'), - (new CancellationTokenSource()).token - ); - - assert.equal(messages.length, messageCountToBeReceived, `Expected number of lint errors does not match lint error count, Output - ${fixture.output}`); - } - test('Three line output counted as one message (Pylint)', async () => { - const maxErrors = 5; - const fixture = new TestFixture(); - fixture.lintingSettings.maxNumberOfProblems = maxErrors; - - await testLinterMessageCount(fixture, Product.pylint, maxErrors); - }); -}); - -const PRODUCTS = Object.keys(Product) - // tslint:disable-next-line:no-any - .filter(key => !isNaN(Number(Product[key as any]))) - // tslint:disable-next-line:no-any - .map(key => Product[key as any]); - -// tslint:disable-next-line:max-func-body-length -suite('Linting Products', () => { - const prodService = new ProductService(); - - test('All linting products are represented by linters', async () => { - for (const product of PRODUCTS) { - // tslint:disable-next-line:no-any - if (prodService.getProductType(product as any) !== ProductType.Linter) { - continue; - } - // tslint:disable-next-line:no-any - const found = LINTERID_BY_PRODUCT.get(product as any); - // tslint:disable-next-line:no-any - assert.notEqual(found, undefined, `did find linter ${Product[product as any]}`); - } - }); - - test('All linters match linting products', async () => { - for (const product of LINTERID_BY_PRODUCT.keys()) { - const prodType = prodService.getProductType(product); - assert.notEqual(prodType, undefined, `${Product[product]} is not not properly registered`); - assert.equal(prodType, ProductType.Linter, `${Product[product]} is not a linter product`); - } - }); - - test('All linting product names match linter IDs', async () => { - for (const [product, linterID] of LINTERID_BY_PRODUCT) { - const prodName = ProductNames.get(product); - assert.equal(prodName, linterID, 'product name does not match linter ID'); - } - }); -}); diff --git a/src/test/linters/lintengine.test.ts b/src/test/linters/lintengine.test.ts deleted file mode 100644 index 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<IServiceContainer>; - let lintManager: TypeMoq.IMock<ILinterManager>; - let settings: TypeMoq.IMock<IPythonSettings>; - let lintSettings: TypeMoq.IMock<ILintingSettings>; - let fileSystem: TypeMoq.IMock<IFileSystem>; - let lintingEngine: ILintingEngine; - - suiteSetup(initialize); - setup(async () => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - - const docManager = TypeMoq.Mock.ofType<IDocumentManager>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())).returns(() => docManager.object); - - const workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService), TypeMoq.It.isAny())).returns(() => workspaceService.object); - - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem), TypeMoq.It.isAny())).returns(() => fileSystem.object); - - lintSettings = TypeMoq.Mock.ofType<ILintingSettings>(); - settings = TypeMoq.Mock.ofType<IPythonSettings>(); - - const configService = TypeMoq.Mock.ofType<IConfigurationService>(); - configService.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - configService.setup(x => x.isTestExecution()).returns(() => true); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService), TypeMoq.It.isAny())).returns(() => configService.object); - - const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(STANDARD_OUTPUT_CHANNEL))).returns(() => outputChannel.object); - - lintManager = TypeMoq.Mock.ofType<ILinterManager>(); - lintManager.setup(x => x.isLintingEnabled(TypeMoq.It.isAny())).returns(async () => true); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILinterManager), TypeMoq.It.isAny())).returns(() => lintManager.object); - - lintingEngine = new LintingEngine(serviceContainer.object); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ILintingEngine), TypeMoq.It.isAny())).returns(() => lintingEngine); - }); - - test('Ensure document.uri is passed into isLintingEnabled', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify(l => l.isLintingEnabled(TypeMoq.It.isAny(), TypeMoq.It.isValue(doc.uri)), TypeMoq.Times.once()); - } - }); - test('Ensure document.uri is passed into createLinter', () => { - const doc = mockTextDocument('a.py', PYTHON_LANGUAGE, true); - try { - lintingEngine.lintDocument(doc, 'auto').ignoreErrors(); - } catch { - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isValue(doc.uri)), TypeMoq.Times.atLeastOnce()); - } - }); - - test('Verify files that match ignore pattern are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, true, ['a*.py']); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - test('Ensure non-Python files are not linted', async () => { - const doc = mockTextDocument('a.ts', 'typescript', true); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - test('Ensure files with git scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'git'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - test('Ensure files with showModifications scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'showModifications'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - test('Ensure files with svn scheme are not linted', async () => { - const doc = mockTextDocument('a1.py', PYTHON_LANGUAGE, false, [], 'svn'); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - test('Ensure non-existing files are not linted', async () => { - const doc = mockTextDocument('file.py', PYTHON_LANGUAGE, false, []); - await lintingEngine.lintDocument(doc, 'auto'); - lintManager.verify(l => l.createLinter(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); - }); - - function mockTextDocument(fileName: string, language: string, exists: boolean, ignorePattern: string[] = [], scheme?: string): TextDocument { - fileSystem.setup(x => x.fileExists(TypeMoq.It.isAnyString())).returns(() => Promise.resolve(exists)); - - lintSettings.setup(l => l.ignorePatterns).returns(() => ignorePattern); - settings.setup(x => x.linting).returns(() => lintSettings.object); - - const doc = TypeMoq.Mock.ofType<TextDocument>(); - if (scheme) { - doc.setup(d => d.uri).returns(() => Uri.parse(`${scheme}:${fileName}`)); - } else { - doc.setup(d => d.uri).returns(() => Uri.file(fileName)); - } - doc.setup(d => d.fileName).returns(() => fileName); - doc.setup(d => d.languageId).returns(() => language); - return doc.object; - } -}); diff --git a/src/test/linters/linter.availability.unit.test.ts b/src/test/linters/linter.availability.unit.test.ts deleted file mode 100644 index 210794ef9c13..000000000000 --- a/src/test/linters/linter.availability.unit.test.ts +++ /dev/null @@ -1,643 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as TypeMoq from 'typemoq'; -import { Uri, WorkspaceConfiguration, WorkspaceFolder } from 'vscode'; -import { ApplicationShell } from '../../client/common/application/applicationShell'; -import { IApplicationShell, IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PersistentStateFactory } from '../../client/common/persistentState'; -import { FileSystem } from '../../client/common/platform/fileSystem'; -import { IFileSystem } from '../../client/common/platform/types'; -import { IConfigurationService, IPersistentState, IPersistentStateFactory, IPythonSettings, Product } from '../../client/common/types'; -import { Common, Linters } from '../../client/common/utils/localize'; -import { AvailableLinterActivator } from '../../client/linters/linterAvailability'; -import { LinterInfo } from '../../client/linters/linterInfo'; -import { IAvailableLinterActivator, ILinterInfo } from '../../client/linters/types'; - -// tslint:disable:max-func-body-length no-any -suite('Linter Availability Provider tests', () => { - test('Availability feature is disabled when global default for jediEnabled=true.', async () => { - // set expectations - const jediEnabledValue = true; - const expectedResult = false; - - // arrange - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock] = getDependenciesForAvailabilityTests(); - setupConfigurationServiceForJediSettingsTest(jediEnabledValue, configServiceMock); - - // call - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - - // check expectaions - expect(availabilityProvider.isFeatureEnabled).is.equal(expectedResult, 'Avaialability feature should be disabled when python.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, fsMock, workspaceServiceMock, configServiceMock, factoryMock] = getDependenciesForAvailabilityTests(); - setupConfigurationServiceForJediSettingsTest(jediEnabledValue, configServiceMock); - - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - - expect(availabilityProvider.isFeatureEnabled).is.equal(expectedResult, 'Avaialability feature should be enabled when python.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, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - - const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); - - expect(result).to.equal(expectedResult, 'Linter is unconfigured but prompt did not get raised'); - workspaceServiceMock.verifyAll(); - }); - - test('No prompt performed when linter is configured as enabled for the workspace', async () => { - // setup expectations - const pylintUserValue = undefined; - const pylintWorkspaceValue = true; - const pylintWorkspaceFolderValue = undefined; - const expectedResult = false; - - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - - const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); - expect(result).to.equal(expectedResult, 'Available linter prompt should not be shown when linter is configured for workspace.'); - workspaceServiceMock.verifyAll(); - }); - - test('No prompt performed when linter is configured as enabled for the entire user', async () => { - // setup expectations - const pylintUserValue = true; - const pylintWorkspaceValue = undefined; - const pylintWorkspaceFolderValue = undefined; - const expectedResult = false; - - // arrange - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - - const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); - expect(result).to.equal(expectedResult, 'Available linter prompt should not be shown when linter is configured for user.'); - workspaceServiceMock.verifyAll(); - }); - - test('No prompt performed when linter is configured as enabled for the workspace-folder', async () => { - // setup expectations - const pylintUserValue = undefined; - const pylintWorkspaceValue = undefined; - const pylintWorkspaceFolderValue = true; - const expectedResult = false; - - // arrange - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupWorkspaceMockForLinterConfiguredTests(pylintUserValue, pylintWorkspaceValue, pylintWorkspaceFolderValue, workspaceServiceMock); - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - - const result = availabilityProvider.isLinterUsingDefaultConfiguration(linterInfo); - expect(result).to.equal(expectedResult, 'Available linter prompt should not be shown when linter is configured for workspace-folder.'); - workspaceServiceMock.verifyAll(); - }); - - async function testForLinterPromptResponse(promptAction: 'enable' | 'ignore' | 'disablePrompt' | undefined, promptEnabled = true): Promise<boolean> { - // arrange - const [appShellMock, fsMock, workspaceServiceMock, , factoryMock] = getDependenciesForAvailabilityTests(); - const configServiceMock = TypeMoq.Mock.ofType<IConfigurationService>(); - - const linterInfo = new class extends LinterInfo { - public testIsEnabled: boolean = promptAction === 'enable' ? true : false; - - public async enableAsync(enabled: boolean, _resource?: Uri): Promise<void> { - this.testIsEnabled = enabled; - return Promise.resolve(); - } - - }(Product.pylint, 'pylint', configServiceMock.object, ['.pylintrc', 'pylintrc']); - - const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - factoryMock - .setup(f => f.createWorkspacePersistentState(TypeMoq.It.isAny(), true)) - .returns(() => notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => promptEnabled); - const selections: ['enable', 'ignore', 'disablePrompt'] = ['enable', 'ignore', 'disablePrompt']; - const optButtons = [ - Linters.enableLinter().format(linterInfo.id), - Common.notNow(), - Common.doNotShowAgain() - ]; - if (promptEnabled) { - appShellMock.setup(ap => ap.showInformationMessage( - TypeMoq.It.isValue(Linters.enablePylint().format(linterInfo.id)), - TypeMoq.It.isValue(Linters.enableLinter().format(linterInfo.id)), - TypeMoq.It.isAny(), - TypeMoq.It.isAny()) - ) - .returns(() => Promise.resolve(promptAction ? optButtons[selections.indexOf(promptAction)] : undefined)) - .verifiable(TypeMoq.Times.once()); - if (promptAction === 'disablePrompt') { - notificationPromptEnabled.setup(n => n.updateValue(false)).returns(() => Promise.resolve()).verifiable(TypeMoq.Times.once()); - } - } else { - appShellMock.setup(ap => ap.showInformationMessage( - TypeMoq.It.isValue(Linters.enablePylint().format(linterInfo.id)), - TypeMoq.It.isValue(Linters.enableLinter().format(linterInfo.id)), - TypeMoq.It.isAny(), - TypeMoq.It.isAny()) - ) - .returns(() => Promise.resolve(promptAction ? optButtons[selections.indexOf(promptAction)] : undefined)) - .verifiable(TypeMoq.Times.never()); - } - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - const result = await availabilityProvider.promptToConfigureAvailableLinter(linterInfo); - if (promptEnabled && promptAction === 'enable') { - expect(linterInfo.testIsEnabled).to.equal(true, 'LinterInfo test class was not updated as a result of the test.'); - } - - appShellMock.verifyAll(); - notificationPromptEnabled.verifyAll(); - - return result; - } - - test('Linter is enabled after being prompted and "Enable <linter>" is selected', async () => { - // set expectations - const expectedResult = true; - const promptAction = 'enable'; - - // run scenario - const result = await testForLinterPromptResponse(promptAction); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - }); - - test('Linter is left unconfigured and prompt is disabled when "Do not show again" is selected', async () => { - // set expectations - const expectedResult = false; - const promptAction = 'disablePrompt'; - - // run scenario - const result = await testForLinterPromptResponse(promptAction); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); - }); - - test('Linter is left unconfigured and no notification is shown if prompt is disabled', async () => { - // set expectations - const expectedResult = false; - const promptAction = 'disablePrompt'; - - // run scenario - const result = await testForLinterPromptResponse(promptAction, false); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); - }); - - test('Linter is left unconfigured after being prompted and the prompt is disabled without any selection made', async () => { - // set expectation - const promptAction = undefined; - const expectedResult = false; - - // run scenario - const result = await testForLinterPromptResponse(promptAction); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); - }); - - test('Linter is left unconfigured when "Not now" is selected', async () => { - // set expectation - const promptAction = 'ignore'; - const expectedResult = false; - - // run scenario - const result = await testForLinterPromptResponse(promptAction); - - // test results - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return false.'); - }); - - // Options to test the implementation of the IAvailableLinterActivator. - // All options default to values that would otherwise allow the prompt to appear. - class AvailablityTestOverallOptions { - public jediEnabledValue: boolean = false; - public pylintUserEnabled?: boolean; - public pylintWorkspaceEnabled?: boolean; - public pylintWorkspaceFolderEnabled?: boolean; - public linterIsInstalled: boolean = true; - public promptAction?: 'enable' | 'disablePrompt' | 'ignore'; - } - - async function performTestOfOverallImplementation(options: AvailablityTestOverallOptions): Promise<boolean> { - // arrange - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - const selections: ['enable', 'ignore', 'disablePrompt'] = ['enable', 'ignore', 'disablePrompt']; - const optButtons = [ - Linters.enableLinter().format(linterInfo.id), - Common.notNow(), - Common.doNotShowAgain() - ]; - appShellMock.setup(ap => ap.showInformationMessage( - TypeMoq.It.isValue(Linters.enablePylint().format(linterInfo.id)), - TypeMoq.It.isValue(Linters.enableLinter().format(linterInfo.id)), - TypeMoq.It.isAny(), - TypeMoq.It.isAny()) - ) - .returns(() => Promise.resolve(options.promptAction ? optButtons[selections.indexOf(options.promptAction)] : undefined)) - .verifiable(TypeMoq.Times.once()); - - const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - workspaceServiceMock - .setup(c => c.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceServiceMock - .setup(c => c.workspaceFolders) - .returns(() => [workspaceFolder]) - .verifiable(TypeMoq.Times.once()); - fsMock.setup(fs => fs.fileExists(TypeMoq.It.isAny())) - .returns(async () => options.linterIsInstalled) - .verifiable(TypeMoq.Times.atLeastOnce()); - - setupConfigurationServiceForJediSettingsTest(options.jediEnabledValue, configServiceMock); - setupWorkspaceMockForLinterConfiguredTests( - options.pylintUserEnabled, - options.pylintWorkspaceEnabled, - options.pylintWorkspaceFolderEnabled, - workspaceServiceMock - ); - - const notificationPromptEnabled = TypeMoq.Mock.ofType<IPersistentState<boolean>>(); - factoryMock - .setup(f => f.createWorkspacePersistentState(TypeMoq.It.isAny(), true)) - .returns(() => notificationPromptEnabled.object); - notificationPromptEnabled.setup(n => n.value).returns(() => true); - // perform test - const availabilityProvider: IAvailableLinterActivator = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - return availabilityProvider.promptIfLinterAvailable(linterInfo); - } - - test('Overall implementation does not change configuration when feature disabled', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.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.promptAction = undefined; // just being explicit for test readability - this is the default - const expectedResult = false; - - // arrange - const result = await performTestOfOverallImplementation(testOpts); - - expect(expectedResult).to.equal(result, 'Configuration should not change if the user is prompted and they dismiss the prompt.'); - }); - - test('Overall implementation does not change configuration when user is prompted and "Do not show again" is selected', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.promptAction = 'disablePrompt'; - const expectedResult = false; - - // arrange - const result = await performTestOfOverallImplementation(testOpts); - - expect(expectedResult).to.equal(result, 'Configuration should change if the user is prompted and they choose to update the linter config.'); - }); - - test('Overall implementation does not change configuration when user is prompted and "Not now" is selected', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.promptAction = 'ignore'; - const expectedResult = false; - - // arrange - const result = await performTestOfOverallImplementation(testOpts); - - expect(expectedResult).to.equal(result, 'Configuration should change if the user is prompted and they choose to update the linter config.'); - }); - - test('Overall implementation changes configuration when user is prompted and "Enable <linter>" is selected', async () => { - // set expectations - const testOpts = new AvailablityTestOverallOptions(); - testOpts.promptAction = 'enable'; - const expectedResult = true; - - // arrange - const result = await performTestOfOverallImplementation(testOpts); - - expect(expectedResult).to.equal(result, 'Configuration should change if the user is prompted and they choose to update the linter config.'); - }); - - test('Discovery of linter is available in the environment returns true when it succeeds and is present', async () => { - // set expectations - const linterIsInstalled = true; - const expectedResult = true; - - // arrange - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupInstallerForAvailabilityTest(linterInfo, linterIsInstalled, fsMock, workspaceServiceMock); - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - const result = await availabilityProvider.isLinterAvailable(linterInfo, undefined); - - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - fsMock.verifyAll(); - workspaceServiceMock.verifyAll(); - }); - - test('Discovery of linter is available in the environment returns false when it succeeds and is not present', async () => { - // set expectations - const linterIsInstalled = false; - const expectedResult = false; - - // arrange - const [appShellMock, fsMock, workspaceServiceMock, configServiceMock, factoryMock, linterInfo] = getDependenciesForAvailabilityTests(); - setupInstallerForAvailabilityTest(linterInfo, linterIsInstalled, fsMock, workspaceServiceMock); - - // perform test - const availabilityProvider = new AvailableLinterActivator(appShellMock.object, fsMock.object, workspaceServiceMock.object, configServiceMock.object, factoryMock.object); - const result = await availabilityProvider.isLinterAvailable(linterInfo, undefined); - - expect(result).to.equal(expectedResult, 'Expected promptToConfigureAvailableLinter to return true because the configuration was updated.'); - fsMock.verifyAll(); - workspaceServiceMock.verifyAll(); - }); - - suite('Linter Availability', () => { - let availabilityProvider: AvailableLinterActivator; - let workspaceService: IWorkspaceService; - let fs: IFileSystem; - const defaultWorkspace: WorkspaceFolder = { uri: Uri.file(path.join('a', 'b', 'default')), name: 'default', index: 0 }; - const resource = Uri.file(__dirname); - setup(() => { - workspaceService = mock(WorkspaceService); - fs = mock(FileSystem); - - availabilityProvider = new AvailableLinterActivator(instance(mock(ApplicationShell)), - instance(fs), instance(workspaceService), instance(mock(ConfigurationService)), - instance(mock(PersistentStateFactory))); - }); - test('No linters when there are no workspaces', async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(false); - const linterInfo = {} as any as ILinterInfo; - const available = await availabilityProvider.isLinterAvailable(linterInfo, undefined); - - expect(available).to.equal(false, 'Should be false'); - }); - - [ - undefined, { uri: Uri.file(path.join('c', 'd', 'resource')), name: 'another', index: 10 } - ].forEach(workspaceFolderRelatedToResource => { - const testSuffix = workspaceFolderRelatedToResource ? '(has a corresponding workspace)' : '(use default workspace)'; - // If there's a workspace, then access default workspace. - const workspaceFolder = workspaceFolderRelatedToResource || defaultWorkspace; - test(`No linters when there are no config files ${testSuffix}`, async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const linterInfo = { configFileNames: [] } as any as ILinterInfo; - when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); - when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); - const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); - - expect(available).to.equal(false, 'Should be false'); - verify(workspaceService.getWorkspaceFolder(resource)).once(); - verify(fs.fileExists(anything())).never(); - // If there's a workspace, then access default workspace. - if (workspaceFolderRelatedToResource) { - verify(workspaceService.workspaceFolders).never(); - } else { - verify(workspaceService.workspaceFolders).once(); - } - }); - test(`No linters when there none of the config files exist ${testSuffix}`, async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const linterInfo = { configFileNames: ['1', '2'] } as any as ILinterInfo; - when(fs.fileExists(anything())).thenResolve(false); - when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); - when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); - - const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); - - expect(available).to.equal(false, 'Should be false'); - verify(workspaceService.getWorkspaceFolder(resource)).once(); - verify(fs.fileExists(anything())).twice(); - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).once(); - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).once(); - if (workspaceFolderRelatedToResource) { - verify(workspaceService.workspaceFolders).never(); - } else { - verify(workspaceService.workspaceFolders).once(); - } - }); - test(`Linters exist when all of the config files exist ${testSuffix}`, async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const linterInfo = { configFileNames: ['1', '2'] } as any as ILinterInfo; - when(fs.fileExists(anything())).thenResolve(true); - when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); - when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); - - const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); - - expect(available).to.equal(true, 'Should be true'); - verify(workspaceService.getWorkspaceFolder(resource)).once(); - verify(fs.fileExists(anything())).once(); - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).once(); - // Check only the first file, if that exists, no point checking the rest. - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).never(); - if (workspaceFolderRelatedToResource) { - verify(workspaceService.workspaceFolders).never(); - } else { - verify(workspaceService.workspaceFolders).once(); - } - }); - test(`Linters exist when one of the config files exist ${testSuffix}`, async () => { - when(workspaceService.hasWorkspaceFolders).thenReturn(true); - const linterInfo = { configFileNames: ['1', '2', '3'] } as any as ILinterInfo; - when(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).thenResolve(false); - when(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).thenResolve(true); - when(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '3'))).thenResolve(false); - when(workspaceService.getWorkspaceFolder(resource)).thenReturn(workspaceFolderRelatedToResource); - when(workspaceService.workspaceFolders).thenReturn([defaultWorkspace]); - - const available = await availabilityProvider.isLinterAvailable(linterInfo, resource); - - expect(available).to.equal(true, 'Should be true'); - verify(workspaceService.getWorkspaceFolder(resource)).once(); - verify(fs.fileExists(anything())).twice(); - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '1'))).once(); - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '2'))).once(); - // Check only the second file, if that exists, no point checking the rest. - verify(fs.fileExists(path.join(workspaceFolder.uri.fsPath, '3'))).never(); - if (workspaceFolderRelatedToResource) { - verify(workspaceService.workspaceFolders).never(); - } else { - verify(workspaceService.workspaceFolders).once(); - } - }); - }); - }); -}); - -function setupWorkspaceMockForLinterConfiguredTests( - enabledForUser: boolean | undefined, - enabeldForWorkspace: boolean | undefined, - enabledForWorkspaceFolder: boolean | undefined, - workspaceServiceMock?: TypeMoq.IMock<IWorkspaceService>): TypeMoq.IMock<IWorkspaceService> { - - if (!workspaceServiceMock) { - workspaceServiceMock = TypeMoq.Mock.ofType<IWorkspaceService>(); - } - const workspaceConfiguration = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceConfiguration.setup(wc => wc.inspect(TypeMoq.It.isValue('pylintEnabled'))) - .returns(() => { - return { - key: '', - globalValue: enabledForUser, - defaultValue: false, - workspaceFolderValue: enabeldForWorkspace, - workspaceValue: enabledForWorkspaceFolder - }; - }) - .verifiable(TypeMoq.Times.once()); - - workspaceServiceMock.setup(ws => ws.getConfiguration(TypeMoq.It.isValue('python.linting'), TypeMoq.It.isAny())) - .returns(() => workspaceConfiguration.object) - .verifiable(TypeMoq.Times.once()); - - return workspaceServiceMock; -} - -function setupConfigurationServiceForJediSettingsTest( - jediEnabledValue: boolean, - configServiceMock: TypeMoq.IMock<IConfigurationService> -): [ - TypeMoq.IMock<IConfigurationService>, - TypeMoq.IMock<IPythonSettings> - ] { - - if (!configServiceMock) { - configServiceMock = TypeMoq.Mock.ofType<IConfigurationService>(); - } - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(ps => ps.jediEnabled).returns(() => jediEnabledValue); - - configServiceMock.setup(cs => cs.getSettings()).returns(() => pythonSettings.object); - return [configServiceMock, pythonSettings]; -} - -function setupInstallerForAvailabilityTest(_linterInfo: LinterInfo, linterIsInstalled: boolean, fsMock: TypeMoq.IMock<IFileSystem>, workspaceServiceMock: TypeMoq.IMock<IWorkspaceService>): TypeMoq.IMock<IFileSystem> { - if (!fsMock) { - fsMock = TypeMoq.Mock.ofType<IFileSystem>(); - } - const workspaceFolder = { uri: Uri.parse('full/path/to/workspace'), name: '', index: 0 }; - workspaceServiceMock - .setup(c => c.hasWorkspaceFolders) - .returns(() => true) - .verifiable(TypeMoq.Times.once()); - workspaceServiceMock - .setup(c => c.workspaceFolders) - .returns(() => [workspaceFolder]); - workspaceServiceMock - .setup(c => c.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolder); - fsMock.setup(fs => fs.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(linterIsInstalled)) - .verifiable(TypeMoq.Times.atLeastOnce()); - - return fsMock; -} - -function getDependenciesForAvailabilityTests(): [ - TypeMoq.IMock<IApplicationShell>, - TypeMoq.IMock<IFileSystem>, - TypeMoq.IMock<IWorkspaceService>, - TypeMoq.IMock<IConfigurationService>, - TypeMoq.IMock<IPersistentStateFactory>, - LinterInfo -] { - const configServiceMock = TypeMoq.Mock.ofType<IConfigurationService>(); - return [ - TypeMoq.Mock.ofType<IApplicationShell>(), - TypeMoq.Mock.ofType<IFileSystem>(), - TypeMoq.Mock.ofType<IWorkspaceService>(), - TypeMoq.Mock.ofType<IConfigurationService>(), - TypeMoq.Mock.ofType<IPersistentStateFactory>(), - new LinterInfo(Product.pylint, '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>(ILinterManager)).thenReturn(instance(manager)); - when(svcContainer.get<IApplicationShell>(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get<IDocumentManager>(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get<ILintingEngine>(ILintingEngine)).thenReturn(instance(lintingEngine)); - linterCommands = new LinterCommands(instance(svcContainer)); - }); - - test('Commands are registered', () => { - verify(cmdManager.registerCommand(Commands.Set_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Enable_Linter, anything())).once(); - verify(cmdManager.registerCommand(Commands.Run_Linter, anything())).once(); - }); - - test('Run Linting method will lint all open files', async () => { - when(lintingEngine.lintOpenPythonFiles()).thenResolve('Hello' as any); - - const result = await linterCommands.runLinting(); - - expect(result).to.be.equal('Hello'); - }); - - async function testEnableLintingWithCurrentState(currentState: boolean, selectedState: 'on' | 'off' | undefined) { - when(manager.isLintingEnabled(true, anything())).thenResolve(currentState); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${currentState ? 'on' : 'off'}` - }; - when(shell.showQuickPick(anything(), anything())).thenResolve(selectedState as any); - - await linterCommands.enableLintingAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const options = capture(shell.showQuickPick).last()[0]; - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(options).to.deep.equal(['on', 'off']); - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - - if (selectedState) { - verify(manager.enableLintingAsync(selectedState === 'on', anything())).once(); - } else { - verify(manager.enableLintingAsync(anything(), anything())).never(); - } - } - test('Enable linting should check if linting is enabled, and display current state of \'on\' and select nothing', async () => { - await testEnableLintingWithCurrentState(true, undefined); - }); - test('Enable linting should check if linting is enabled, and display current state of \'on\' and select \'on\'', async () => { - await testEnableLintingWithCurrentState(true, 'on'); - }); - test('Enable linting should check if linting is enabled, and display current state of \'on\' and select \'off\'', async () => { - await testEnableLintingWithCurrentState(true, 'off'); - }); - test('Enable linting should check if linting is enabled, and display current state of \'off\' and select \'on\'', async () => { - await testEnableLintingWithCurrentState(true, 'on'); - }); - test('Enable linting should check if linting is enabled, and display current state of \'off\' and select \'off\'', async () => { - await testEnableLintingWithCurrentState(true, 'off'); - }); - - test('Set Linter should display a quickpick', async () => { - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(true, anything())).thenResolve([]); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: none' - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - test('Set Linter should display a quickpick and currently active linter when only one is enabled', async () => { - const linterId = 'Hello World'; - const activeLinters: ILinterInfo[] = [{ id: linterId } as any]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: `current: ${linterId}` - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - test('Set Linter should display a quickpick and with message about multiple linters being enabled', async () => { - const activeLinters: ILinterInfo[] = [{ id: 'linterId' } as any, { id: 'linterId2' } as any]; - when(manager.getAllLinterInfos()).thenReturn([]); - when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve(); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected' - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - }); - test('Selecting a linter should display warning message about multiple linters', async () => { - const linters: ILinterInfo[] = [{ id: '1' }, { id: '2' }, { id: '3', product: 'Three' }] as any; - const activeLinters: ILinterInfo[] = [{ id: '1' }, { id: '3' }] as any; - when(manager.getAllLinterInfos()).thenReturn(linters); - when(manager.getActiveLinters(true, anything())).thenResolve(activeLinters); - when(shell.showQuickPick(anything(), anything())).thenResolve('3' as any); - when(shell.showWarningMessage(anything(), 'Yes', 'No')).thenResolve('Yes' as any); - const expectedQuickPickOptions = { - matchOnDetail: true, - matchOnDescription: true, - placeHolder: 'current: multiple selected' - }; - - await linterCommands.setLinterAsync(); - - verify(shell.showQuickPick(anything(), anything())).once(); - verify(shell.showWarningMessage(anything(), 'Yes', 'No')).once(); - const quickPickOptions = capture(shell.showQuickPick).last()[1]; - expect(quickPickOptions).to.deep.equal(expectedQuickPickOptions); - verify(manager.setActiveLintersAsync(deepEqual(['Three']), 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>(IApplicationShell)).thenReturn(instance(shell)); - when(svcContainer.get<IDocumentManager>(IDocumentManager)).thenReturn(instance(docManager)); - when(svcContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(cmdManager)); - when(svcContainer.get<ILintingEngine>(ILintingEngine)).thenReturn(instance(lintingEngine)); - when(svcContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); - when(svcContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); - linterManager = new LinterManagerTest(instance(svcContainer), instance(workspaceService)); - }); - - test('Get all linters will return a list of all linters', () => { - const linters = linterManager.getAllLinterInfos(); - - expect(linters).to.be.lengthOf(8); - - const productService = new ProductService(); - const linterProducts = getNamesAndValues<Product>(Product) - .filter(product => productService.getProductType(product.value) === ProductType.Linter) - .map(item => ProductNames.get(item.value)); - expect(linters.map(item => item.id).sort()).to.be.deep.equal(linterProducts.sort()); - }); - - test('Get linter info for non-linter product should throw an exception', () => { - const productService = new ProductService(); - getNamesAndValues<Product>(Product).forEach(prod => { - if (productService.getProductType(prod.value) === ProductType.Linter) { - const info = linterManager.getLinterInfo(prod.value); - expect(info.id).to.equal(ProductNames.get(prod.value)); - expect(info).not.to.be.equal(undefined, 'should not be unedfined'); - } else { - expect(() => linterManager.getLinterInfo(prod.value)).to.throw(); - } - }); - }); - test('Pylint configuration file watch', async () => { - const pylint = linterManager.getLinterInfo(Product.pylint); - assert.equal(pylint.configFileNames.length, 2, 'Pylint configuration file count is incorrect.'); - assert.notEqual(pylint.configFileNames.indexOf('pylintrc'), -1, 'Pylint configuration files miss pylintrc.'); - assert.notEqual(pylint.configFileNames.indexOf('.pylintrc'), -1, 'Pylint configuration files miss .pylintrc.'); - }); - - [undefined, Uri.parse('something')].forEach(resource => { - const testResourceSuffix = `(${resource ? 'with a resource' : 'without a resource'})`; - [true, false].forEach(enabled => { - const testSuffix = `(${enabled ? 'enable' : 'disable'}) & ${testResourceSuffix}`; - test(`Enable linting should update config ${testSuffix}`, async () => { - when(configService.updateSetting('linting.enabled', enabled, resource)).thenResolve(); - - await linterManager.enableLintingAsync(enabled, resource); - - verify(configService.updateSetting('linting.enabled', enabled, resource)).once(); - }); - }); - test(`getActiveLinters will check if linter is enabled and in silent mode ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.isEnabled(resource)).thenReturn(true); - - const linters = await linterManager.getActiveLinters(true, resource); - - verify(linterInfo.isEnabled(resource)).once(); - expect(linters[0]).to.deep.equal(instanceOfLinterInfo); - }); - test(`getActiveLinters will check if linter is enabled and not in silent mode ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.isEnabled(resource)).thenReturn(true); - let enableUnconfiguredLintersInvoked = false; - linterManager.enableUnconfiguredLinters = async () => { - enableUnconfiguredLintersInvoked = true; - }; - - const linters = await linterManager.getActiveLinters(false, resource); - - verify(linterInfo.isEnabled(resource)).once(); - expect(linters[0]).to.deep.equal(instanceOfLinterInfo); - expect(enableUnconfiguredLintersInvoked).to.equal(true, 'not invoked'); - }); - - test(`setActiveLintersAsync with invalid products does nothing ${testResourceSuffix}`, async () => { - let getActiveLintersInvoked = false; - linterManager.getActiveLinters = async () => { getActiveLintersInvoked = true; return []; }; - - await linterManager.setActiveLintersAsync([Product.ctags, Product.pytest], resource); - - expect(getActiveLintersInvoked).to.be.equal(false, 'Should not be invoked'); - }); - test(`setActiveLintersAsync with single product will disable it then enable it ${testResourceSuffix}`, async () => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters = [instanceOfLinterInfo]; - when(linterInfo.product).thenReturn(Product.flake8); - when(linterInfo.enableAsync(false, resource)).thenResolve(); - linterManager.getActiveLinters = () => Promise.resolve([instanceOfLinterInfo]); - linterManager.enableLintingAsync = () => Promise.resolve(); - - await linterManager.setActiveLintersAsync([Product.flake8], resource); - - verify(linterInfo.enableAsync(false, resource)).atLeast(1); - verify(linterInfo.enableAsync(true, resource)).atLeast(1); - }); - test(`setActiveLintersAsync with single product will disable all existing then enable the necessary two ${testResourceSuffix}`, async () => { - const linters = new Map<Product, LinterInfo>(); - const linterInstances = new Map<Product, LinterInfo>(); - linterManager.linters = []; - [Product.flake8, Product.mypy, Product.prospector, Product.bandit, Product.pydocstyle].forEach(product => { - const linterInfo = mock(LinterInfo); - const instanceOfLinterInfo = instance(linterInfo); - linterManager.linters.push(instanceOfLinterInfo); - linters.set(product, linterInfo); - linterInstances.set(product, instanceOfLinterInfo); - when(linterInfo.product).thenReturn(product); - when(linterInfo.enableAsync(anything(), resource)).thenResolve(); - }); - - linterManager.getActiveLinters = () => Promise.resolve(Array.from(linterInstances.values())); - linterManager.enableLintingAsync = () => Promise.resolve(); - - const lintersToEnable = [Product.flake8, Product.mypy, Product.pydocstyle]; - await linterManager.setActiveLintersAsync([Product.flake8, Product.mypy, Product.pydocstyle], resource); - - linters.forEach((item, product) => { - verify(item.enableAsync(false, resource)).atLeast(1); - if (lintersToEnable.indexOf(product) >= 0) { - verify(item.enableAsync(true, resource)).atLeast(1); - } - }); - }); - }); -}); diff --git a/src/test/linters/linterinfo.unit.test.ts b/src/test/linters/linterinfo.unit.test.ts deleted file mode 100644 index ed3522ce004f..000000000000 --- a/src/test/linters/linterinfo.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: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; - }); - const testsForisEnabled = - [ - { - testName: 'When workspaceFolder setting is provided', - inspection: { workspaceFolderValue: true } - }, - { - testName: 'When workspace setting is provided', - inspection: { workspaceValue: true } - }, - { - testName: 'When global setting is provided', - inspection: { globalValue: true } - } - ]; - - suite('Test is enabled when using Language Server and Pylint is configured', () => { - testsForisEnabled.forEach(testParams => { - test(testParams.testName, async () => { - const config = mock(ConfigurationService); - const workspaceService = mock(WorkspaceService); - const linterInfo = new PylintLinterInfo(instance(config), instance(workspaceService), []); - - const pythonConfig = { - inspect: () => testParams.inspection - }; - when(config.getSettings(anything())).thenReturn({ linting: { pylintEnabled: true }, 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 9a9413c5c00e..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<IFileSystem>; - let platformService: TypeMoq.IMock<IPlatformService>; - let workspace: TypeMoq.IMock<IWorkspaceService>; - let execService: TypeMoq.IMock<IPythonToolExecutionService>; - let config: TypeMoq.IMock<IConfigurationService>; - let serviceContainer: ServiceContainer; - - setup(() => { - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - fileSystem - .setup(x => x.arePathsSame(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((a, b) => a === b); - - platformService = TypeMoq.Mock.ofType<IPlatformService>(); - platformService.setup(x => x.isWindows).returns(() => false); - - workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - execService = TypeMoq.Mock.ofType<IPythonToolExecutionService>(); - - const cont = new Container(); - const serviceManager = new ServiceManager(cont); - serviceContainer = new ServiceContainer(cont); - - serviceManager.addSingletonInstance<IFileSystem>(IFileSystem, fileSystem.object); - serviceManager.addSingletonInstance<IWorkspaceService>(IWorkspaceService, workspace.object); - serviceManager.addSingletonInstance<IPythonToolExecutionService>(IPythonToolExecutionService, execService.object); - serviceManager.addSingletonInstance<IPlatformService>(IPlatformService, platformService.object); - serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); - config = TypeMoq.Mock.ofType<IConfigurationService>(); - serviceManager.addSingletonInstance<IConfigurationService>(IConfigurationService, config.object); - const linterManager = new LinterManager(serviceContainer, workspace.object); - serviceManager.addSingletonInstance<ILinterManager>(ILinterManager, linterManager); - const logger = TypeMoq.Mock.ofType<ILogger>(); - serviceManager.addSingletonInstance<ILogger>(ILogger, logger.object); - const installer = TypeMoq.Mock.ofType<IInstaller>(); - serviceManager.addSingletonInstance<IInstaller>(IInstaller, installer.object); - }); - - test('pylintrc in the file folder', async () => { - fileSystem.setup(x => x.fileExists(path.join(basePath, pylintrc))).returns(() => Promise.resolve(true)); - let result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${pylintrc}' not detected in the file folder.`); - - fileSystem.setup(x => x.fileExists(path.join(basePath, dotPylintrc))).returns(() => Promise.resolve(true)); - result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the file folder.`); - }); - test('pylintrc up the module tree', async () => { - const module1 = path.join('/user/a/b/c/d', '__init__.py'); - const module2 = path.join('/user/a/b/c', '__init__.py'); - const module3 = path.join('/user/a/b', '__init__.py'); - const rc = path.join('/user/a/b/c', pylintrc); - - fileSystem.setup(x => x.fileExists(module1)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExists(module2)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExists(module3)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); - - const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${pylintrc}' not detected in the module tree.`); - }); - test('.pylintrc up the module tree', async () => { - // Don't use path.join since it will use / on Travis and Mac - const module1 = path.join('/user/a/b/c/d', '__init__.py'); - const module2 = path.join('/user/a/b/c', '__init__.py'); - const module3 = path.join('/user/a/b', '__init__.py'); - const rc = path.join('/user/a/b/c', pylintrc); - - fileSystem.setup(x => x.fileExists(module1)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExists(module2)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExists(module3)).returns(() => Promise.resolve(true)); - fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); - - const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the module tree.`); - }); - test('.pylintrc up the ~ folder', async () => { - const home = os.homedir(); - const rc = path.join(home, dotPylintrc); - fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); - - const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${dotPylintrc}' not detected in the ~ folder.`); - }); - test('pylintrc up the ~/.config folder', async () => { - const home = os.homedir(); - const rc = path.join(home, '.config', pylintrc); - fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); - - const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${pylintrc}' not detected in the ~/.config folder.`); - }); - test('pylintrc in the /etc folder', async () => { - const rc = path.join('/etc', pylintrc); - fileSystem.setup(x => x.fileExists(rc)).returns(() => Promise.resolve(true)); - - const result = await Pylint.hasConfigurationFile(fileSystem.object, basePath, platformService.object); - expect(result).to.be.equal(true, `'${pylintrc}' not detected in the /etc folder.`); - }); - test('pylintrc between file and workspace root', async () => { - const root = '/user/a'; - const midFolder = '/user/a/b'; - fileSystem - .setup(x => x.fileExists(path.join(midFolder, pylintrc))) - .returns(() => Promise.resolve(true)); - - const result = await Pylint.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<void> { - const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); - const pylinter = new Pylint(outputChannel.object, serviceContainer); - - const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(x => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType<WorkspaceFolder>(); - wsf.setup(x => x.uri).returns(() => Uri.file(wsRoot)); - - workspace.setup(x => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); - - let execInfo: ExecutionInfo | undefined; - execService - .setup(x => x.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .callback((e: ExecutionInfo, _b, _c) => { - execInfo = e; - }) - .returns(() => Promise.resolve({ stdout: '', stderr: '' })); - - const lintSettings = new MockLintingSettings(); - lintSettings.pylintUseMinimalCheckers = true; - // tslint:disable-next-line:no-string-literal - lintSettings['pylintPath'] = 'pyLint'; - // tslint:disable-next-line:no-string-literal - lintSettings['pylintEnabled'] = true; - - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(x => x.linting).returns(() => lintSettings); - config.setup(x => x.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - - await pylinter.lint(document.object, new CancellationTokenSource().token); - expect(execInfo!.args.findIndex(x => x.indexOf('--disable=all') >= 0), - 'Minimal args passed to pylint while pylintrc exists.').to.be.eq(expectedMinArgs ? 0 : -1); - } - test('Negative column numbers should be treated 0', async () => { - const fileFolder = '/user/a/b/c'; - const outputChannel = TypeMoq.Mock.ofType<OutputChannel>(); - const pylinter = new Pylint(outputChannel.object, serviceContainer); - - const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(x => x.uri).returns(() => Uri.file(path.join(fileFolder, 'test.py'))); - - const wsf = TypeMoq.Mock.ofType<WorkspaceFolder>(); - wsf.setup(x => x.uri).returns(() => Uri.file(fileFolder)); - - workspace.setup(x => x.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => wsf.object); - - const linterOutput = ['No config file found, using default configuration', - '************* Module test', - '1,1,convention,C0111:Missing module docstring', - '3,-1,error,E1305:Too many arguments for format string'].join(os.EOL); - execService - .setup(x => x.exec(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: linterOutput, stderr: '' })); - - const lintSettings = new MockLintingSettings(); - lintSettings.pylintUseMinimalCheckers = false; - lintSettings.maxNumberOfProblems = 1000; - lintSettings.pylintPath = 'pyLint'; - lintSettings.pylintEnabled = true; - lintSettings.pylintCategorySeverity = { - convention: DiagnosticSeverity.Hint, - error: DiagnosticSeverity.Error, - fatal: DiagnosticSeverity.Error, - refactor: DiagnosticSeverity.Hint, - warning: DiagnosticSeverity.Warning - }; - - const settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(x => x.linting).returns(() => lintSettings); - 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 46a8f9146881..000000000000 --- a/src/test/markdown/restTextConverter.test.ts +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { RestTextConverter } from '../../client/common/markdown/restTextConverter'; -import { compareFiles } from '../textUtils'; - -const srcPythoFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'markdown'); - -async function testConversion(fileName: string): Promise<void> { - const cvt = new RestTextConverter(); - const file = path.join(srcPythoFilesPath, fileName); - const source = await fs.readFile(`${file}.pydoc`, 'utf8'); - const actual = cvt.toMarkdown(source); - const expected = await fs.readFile(`${file}.md`, 'utf8'); - compareFiles(expected, actual); -} - -// tslint:disable-next-line:max-func-body-length -suite('Hover - RestTextConverter', () => { - test('scipy', async () => testConversion('scipy')); - test('scipy.spatial', async () => testConversion('scipy.spatial')); - test('scipy.spatial.distance', async () => testConversion('scipy.spatial.distance')); - test('anydbm', async () => testConversion('anydbm')); - test('aifc', async () => testConversion('aifc')); - test('astroid', async () => testConversion('astroid')); -}); diff --git a/src/test/mockClasses.ts b/src/test/mockClasses.ts index a479a7dc64df..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; + private _eventEmitter = new vscode.EventEmitter<vscode.LogLevel>(); + public onDidChangeLogLevel: vscode.Event<vscode.LogLevel> = this._eventEmitter.event; constructor(name: string) { this.name = name; this.output = ''; + this.logLevel = vscode.LogLevel.Debug; + } + public logLevel: vscode.LogLevel; + trace(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + debug(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + info(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + warn(message: string, ...args: any[]): void { + this.appendLine(util.format(message, ...args)); + } + error(error: string | Error, ...args: any[]): void { + this.appendLine(util.format(error, ...args)); } public append(value: string) { this.output += value; } - 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 { this.isShown = true; } public hide() { this.isShown = false; } - // tslint:disable-next-line:no-empty - public dispose() { } + + public dispose() {} } export class MockStatusBarItem implements vscode.StatusBarItem { + backgroundColor: vscode.ThemeColor | undefined; + accessibilityInformation: vscode.AccessibilityInformation | undefined; public alignment!: vscode.StatusBarAlignment; public priority!: number; public text!: string; public tooltip!: string; public color!: string; public command!: string; - // tslint:disable-next-line:no-empty - public show(): void { - } - // tslint:disable-next-line:no-empty - public hide(): void { - } - // tslint:disable-next-line:no-empty - public dispose(): void { - } -} + 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<void> { +export class MockAutoSelectionService + implements IInterpreterAutoSelectionService, IInterpreterAutoSelectionProxyService { + // eslint-disable-next-line class-methods-use-this + public async setWorkspaceInterpreter(_resource: Resource, _interpreter: PythonEnvironment): Promise<void> { return Promise.resolve(); } - public async setGlobalInterpreter(_interpreter: PythonInterpreter): Promise<void> { - return; - } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public async setGlobalInterpreter(_interpreter: PythonEnvironment): Promise<void> {} + + // eslint-disable-next-line class-methods-use-this get onDidChangeAutoSelectedInterpreter(): Event<void> { return new EventEmitter<void>().event; } + + // eslint-disable-next-line class-methods-use-this public autoSelectInterpreter(_resource: Resource): Promise<void> { return Promise.resolve(); } - public getAutoSelectedInterpreter(_resource: Resource): PythonInterpreter | undefined { - return; - } - public registerInstance(_instance: IInterpreterAutoSeletionProxyService): void { - return; + + // eslint-disable-next-line class-methods-use-this + public getAutoSelectedInterpreter(_resource: Resource): PythonEnvironment | undefined { + return undefined; } + + // eslint-disable-next-line class-methods-use-this, @typescript-eslint/no-empty-function + public registerInstance(_instance: IInterpreterAutoSelectionProxyService): void {} } diff --git a/src/test/mocks/extension.ts b/src/test/mocks/extension.ts new file mode 100644 index 000000000000..61d70eb5ee9e --- /dev/null +++ b/src/test/mocks/extension.ts @@ -0,0 +1,16 @@ +import { injectable } from 'inversify'; +import { Extension, ExtensionKind, Uri } from 'vscode'; + +@injectable() +export class MockExtension<T> implements Extension<T> { + id!: string; + extensionUri!: Uri; + extensionPath!: string; + isActive!: boolean; + packageJSON: any; + extensionKind!: ExtensionKind; + exports!: T; + activate(): Thenable<T> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/extensions.ts b/src/test/mocks/extensions.ts new file mode 100644 index 000000000000..efe9b6b8ca31 --- /dev/null +++ b/src/test/mocks/extensions.ts @@ -0,0 +1,23 @@ +import { injectable } from 'inversify'; +import { IExtensions } from '../../client/common/types'; +import { Extension, Event } from 'vscode'; +import { MockExtension } from './extension'; + +@injectable() +export class MockExtensions implements IExtensions { + extensionIdsToFind: unknown[] = []; + all: readonly Extension<unknown>[] = []; + onDidChange: Event<void> = () => { + throw new Error('Method not implemented'); + }; + getExtension(extensionId: string): Extension<unknown> | undefined; + getExtension<T>(extensionId: string): Extension<T> | undefined; + getExtension(extensionId: unknown): import('vscode').Extension<unknown> | undefined { + if (this.extensionIdsToFind.includes(extensionId)) { + return new MockExtension(); + } + } + determineExtensionFromCallStack(): Promise<{ extensionId: string; displayName: string }> { + throw new Error('Method not implemented.'); + } +} diff --git a/src/test/mocks/helper.ts b/src/test/mocks/helper.ts new file mode 100644 index 000000000000..d61bf728a25c --- /dev/null +++ b/src/test/mocks/helper.ts @@ -0,0 +1,28 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as TypeMoq from 'typemoq'; +import { Readable } from 'stream'; +// eslint-disable-next-line import/no-unresolved +import * as common from 'typemoq/Common/_all'; + +export class FakeReadableStream extends Readable { + _read(_size: unknown): void | null { + // custom reading logic here + this.push(null); // end the stream + } +} + +export function createTypeMoq<T>( + targetCtor?: common.CtorWithArgs<T>, + behavior?: TypeMoq.MockBehavior, + shouldOverrideTarget?: boolean, + ...targetCtorArgs: any[] +): TypeMoq.IMock<T> { + // Use typemoqs for those things that are resolved as promises. mockito doesn't allow nesting of mocks. ES6 Proxy class + // is the problem. We still need to make it thenable though. See this issue: https://github.com/florinn/typemoq/issues/67 + const result = TypeMoq.Mock.ofType<T>(targetCtor, behavior, shouldOverrideTarget, ...targetCtorArgs); + result.setup((x: any) => x.then).returns(() => undefined); + return result; +} diff --git a/src/test/mocks/mementos.ts b/src/test/mocks/mementos.ts index 2ebdaeceba80..1ffa09884262 100644 --- a/src/test/mocks/mementos.ts +++ b/src/test/mocks/mementos.ts @@ -3,18 +3,34 @@ import { Memento } from 'vscode'; @injectable() export class MockMemento implements Memento { - private map: Map<string, {}> = new Map<string, {}>(); - // @ts-ignore - // 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<string, any> = {}; + + public keys(): string[] { + return Object.keys(this._value); + } + + // @ts-ignore Ignore the return value warning + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any public get(key: any, defaultValue?: any); + public get<T>(key: string, defaultValue?: T): T { - const exists = this.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<void> { - 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<string, any>; + + readonly channel?: Pipe | null | undefined; + + readonly stdio: [ + Writable | null, + // stdin + Readable | null, + // stdout + Readable | null, + // stderr + Readable | Writable | null | undefined, + // extra + Readable | Writable | null | undefined, // extra + ]; + + readonly killed: boolean; + + readonly pid?: number | undefined; + + readonly connected: boolean; + + readonly exitCode: number | null; + + readonly signalCode: NodeJS.Signals | null; + + readonly spawnargs: string[]; + + readonly spawnfile: string; + + signal?: NodeJS.Signals | number; + + send(message: Serializable, callback?: (error: Error | null) => void): boolean; + + send(message: Serializable, sendHandle?: SendHandle, callback?: (error: Error | null) => void): boolean; + + send( + message: Serializable, + sendHandle?: SendHandle, + options?: MessageOptions, + callback?: (error: Error | null) => void, + ): boolean; + + send( + message: Serializable, + _sendHandleOrCallback?: SendHandle | ((error: Error | null) => void), + _optionsOrCallback?: MessageOptions | ((error: Error | null) => void), + _callback?: (error: Error | null) => void, + ): boolean { + // Implementation of the send method + // For example, you might want to emit a 'message' event + this.stdout?.push(message.toString()); + return true; + } + + // eslint-disable-next-line class-methods-use-this + disconnect(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + unref(): void { + /* noop */ + } + + // eslint-disable-next-line class-methods-use-this + ref(): void { + /* noop */ + } + + addListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'disconnect', listener: () => void): this; + + addListener(event: 'error', listener: (err: Error) => void): this; + + addListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + addListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + addListener(event: 'spawn', listener: () => void): this; + + addListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + emit(event: 'close', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'disconnect'): boolean; + + emit(event: 'error', err: Error): boolean; + + emit(event: 'exit', code: number | null, signal: NodeJS.Signals | null): boolean; + + emit(event: 'message', message: Serializable, sendHandle: SendHandle): boolean; + + emit(event: 'spawn', listener: () => void): boolean; + + emit(event: string | symbol, ...args: unknown[]): boolean { + if (this.eventMap.has(event.toString())) { + this.eventMap.get(event.toString()).forEach((listener: (...arg0: unknown[]) => void) => { + const argsArray: unknown[] = Array.isArray(args) ? args : [args]; + listener(...argsArray); + }); + } + return true; + } + + on(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'disconnect', listener: () => void): this; + + on(event: 'error', listener: (err: Error) => void): this; + + on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + on(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + on(event: 'spawn', listener: () => void): this; + + on(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + once(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'disconnect', listener: () => void): this; + + once(event: 'error', listener: (err: Error) => void): this; + + once(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + once(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + once(event: 'spawn', listener: () => void): this; + + once(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'disconnect', listener: () => void): this; + + prependListener(event: 'error', listener: (err: Error) => void): this; + + prependListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependListener(event: 'spawn', listener: () => void): this; + + prependListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + prependOnceListener(event: 'close', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'disconnect', listener: () => void): this; + + prependOnceListener(event: 'error', listener: (err: Error) => void): this; + + prependOnceListener(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): this; + + prependOnceListener(event: 'message', listener: (message: Serializable, sendHandle: SendHandle) => void): this; + + prependOnceListener(event: 'spawn', listener: () => void): this; + + prependOnceListener(event: string, listener: (...args: any[]) => void): this { + if (this.eventMap.has(event)) { + this.eventMap.get(event).push(listener); + } else { + this.eventMap.set(event, [listener]); + } + return this; + } + + trigger(event: string): Array<any> { + if (this.eventMap.has(event)) { + return this.eventMap.get(event); + } + return []; + } + + kill(_signal?: NodeJS.Signals | number): boolean { + this.stdout?.destroy(); + return true; + } + + dispose(): void { + this.stdout?.destroy(); + } +} diff --git a/src/test/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<boolean>; + + constructor( + contents: string, + fileName: string, + onSave: (doc: TextDocument) => Promise<boolean>, + language?: string, + ) { + this._uri = Uri.file(fileName); + this._contents = contents; + this._lines = this.createLines(); + this._onSave = onSave; + this._language = language ?? this._language; + } + encoding: string = 'utf8'; + + public setContent(contents: string): 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<boolean> { + return this._onSave(this); + } + + // eslint-disable-next-line class-methods-use-this + public get eol(): EndOfLine { + return EndOfLine.LF; + } + + public get lineCount(): number { + return this._lines.length; + } + + public lineAt(position: Position | number): TextLine { + if (typeof position === 'number') { + return this._lines[position as number]; + } + 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<TextEditor>(); + + private didOpenEmitter = new EventEmitter<TextDocument>(); + + private didChangeVisibleEmitter = new EventEmitter<TextEditor[]>(); + + private didChangeTextEditorSelectionEmitter = new EventEmitter<TextEditorSelectionChangeEvent>(); + + private didChangeTextEditorOptionsEmitter = new EventEmitter<TextEditorOptionsChangeEvent>(); + + private didChangeTextEditorViewColumnEmitter = new EventEmitter<TextEditorViewColumnChangeEvent>(); + + private didCloseEmitter = new EventEmitter<TextDocument>(); + + private didSaveEmitter = new EventEmitter<TextDocument>(); + + private didChangeTextDocumentEmitter = new EventEmitter<TextDocumentChangeEvent>(); + + public get onDidChangeActiveTextEditor(): Event<TextEditor | undefined> { + return this.didChangeActiveTextEditorEmitter.event; + } + + public get onDidChangeTextDocument(): Event<TextDocumentChangeEvent> { + return this.didChangeTextDocumentEmitter.event; + } + + public get onDidOpenTextDocument(): Event<TextDocument> { + return this.didOpenEmitter.event; + } + + public get onDidChangeVisibleTextEditors(): Event<TextEditor[]> { + return this.didChangeVisibleEmitter.event; + } + + public get onDidChangeTextEditorSelection(): Event<TextEditorSelectionChangeEvent> { + return this.didChangeTextEditorSelectionEmitter.event; + } + + public get onDidChangeTextEditorOptions(): Event<TextEditorOptionsChangeEvent> { + return this.didChangeTextEditorOptionsEmitter.event; + } + + public get onDidChangeTextEditorViewColumn(): Event<TextEditorViewColumnChangeEvent> { + return this.didChangeTextEditorViewColumnEmitter.event; + } + + public get onDidCloseTextDocument(): Event<TextDocument> { + return this.didCloseEmitter.event; + } + + public get onDidSaveTextDocument(): Event<TextDocument> { + return this.didSaveEmitter.event; + } + + public showTextDocument( + _document: TextDocument, + _column?: ViewColumn, + _preserveFocus?: boolean, + ): Thenable<TextEditor>; + + public showTextDocument(_document: TextDocument | Uri, _options?: TextDocumentShowOptions): Thenable<TextEditor>; + + public showTextDocument(document: unknown, _column?: unknown, _preserveFocus?: unknown): Thenable<TextEditor> { + this.visibleTextEditors.push(document as TextEditor); + const mockEditor = new MockEditor(this, this.lastDocument as MockDocument); + this.activeTextEditor = mockEditor; + this.didChangeActiveTextEditorEmitter.fire(this.activeTextEditor); + return Promise.resolve(mockEditor); + } + + public openTextDocument(_fileName: string | Uri): Thenable<TextDocument>; + + public openTextDocument(_options?: { language?: string; content?: string }): Thenable<TextDocument>; + + public openTextDocument(_options?: unknown): Thenable<TextDocument> { + const opts = _options as { content?: string }; + if (opts && opts.content) { + const doc = new MockDocument(opts.content, 'Untitled-1', this.saveDocument); + this.textDocuments.push(doc); + } + return Promise.resolve(this.lastDocument); + } + + // eslint-disable-next-line class-methods-use-this + public applyEdit(_edit: WorkspaceEdit): Thenable<boolean> { + throw new Error('Method not implemented.'); + } + + public addDocument(code: string, file: string, 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<boolean> => { + // Create a new document with the contents of the doc passed in + this.addDocument(doc.getText(), path.join(EXTENSION_ROOT_DIR, 'baz.py')); + return Promise.resolve(true); + }; +} diff --git a/src/test/mocks/mockTextEditor.ts b/src/test/mocks/mockTextEditor.ts new file mode 100644 index 000000000000..6c1c91f45577 --- /dev/null +++ b/src/test/mocks/mockTextEditor.ts @@ -0,0 +1,134 @@ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { + DecorationOptions, + EndOfLine, + Position, + Range, + Selection, + SnippetString, + TextDocument, + TextEditorDecorationType, + TextEditorEdit, + TextEditorOptions, + TextEditorRevealType, + ViewColumn, +} from 'vscode'; + +import { noop } from '../../client/common/utils/misc'; +import { MockDocument } from './mockDocument'; +import { IMockDocumentManager, IMockTextEditor } from './mockTypes'; + +class MockEditorEdit implements TextEditorEdit { + constructor(private _documentManager: IMockDocumentManager, private _document: MockDocument) {} + + public replace(location: Selection | Range | Position, value: string): void { + this._documentManager.changeDocument(this._document.fileName, [ + { + range: location as Range, + newText: value, + }, + ]); + } + + public insert(location: Position, value: string): void { + this._documentManager.changeDocument(this._document.fileName, [ + { + range: new Range(location, location), + newText: value, + }, + ]); + } + + // eslint-disable-next-line class-methods-use-this + public delete(_location: Selection | Range): void { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + public setEndOfLine(_endOfLine: EndOfLine): void { + throw new Error('Method not implemented.'); + } +} + +export class MockEditor implements IMockTextEditor { + public selection: Selection; + + public selections: Selection[] = []; + + private _revealCallback: () => void; + + constructor(private _documentManager: IMockDocumentManager, private _document: MockDocument) { + this.selection = new Selection(0, 0, 0, 0); + this._revealCallback = noop; + } + + public get document(): TextDocument { + return this._document; + } + + // eslint-disable-next-line class-methods-use-this + public get visibleRanges(): Range[] { + return []; + } + + // eslint-disable-next-line class-methods-use-this + public get options(): TextEditorOptions { + return {}; + } + + // eslint-disable-next-line class-methods-use-this + public get viewColumn(): ViewColumn | undefined { + return undefined; + } + + public edit( + callback: (editBuilder: TextEditorEdit) => void, + _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined, + ): Thenable<boolean> { + return new Promise((r) => { + const editor = new MockEditorEdit(this._documentManager, this._document); + callback(editor); + r(true); + }); + } + + // eslint-disable-next-line class-methods-use-this + public insertSnippet( + _snippet: SnippetString, + _location?: Range | Position | Range[] | Position[] | undefined, + _options?: { undoStopBefore: boolean; undoStopAfter: boolean } | undefined, + ): Thenable<boolean> { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + public setDecorations( + _decorationType: TextEditorDecorationType, + _rangesOrOptions: Range[] | DecorationOptions[], + ): void { + throw new Error('Method not implemented.'); + } + + public revealRange(_range: Range, _revealType?: TextEditorRevealType | undefined): void { + this._revealCallback(); + } + + // eslint-disable-next-line class-methods-use-this + public show(_column?: ViewColumn | undefined): void { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + public hide(): void { + throw new Error('Method not implemented.'); + } + + public setRevealCallback(callback: () => void): void { + this._revealCallback = callback; + } +} diff --git a/src/test/mocks/mockTypes.ts b/src/test/mocks/mockTypes.ts new file mode 100644 index 000000000000..eb560efcef99 --- /dev/null +++ b/src/test/mocks/mockTypes.ts @@ -0,0 +1,8 @@ +import { Range, TextEditor } from 'vscode'; +import { IDocumentManager } from '../../client/common/application/types'; + +export interface IMockTextEditor extends TextEditor {} + +export interface IMockDocumentManager extends IDocumentManager { + changeDocument(file: string, changes: { range: Range; newText: string }[]): void; +} diff --git a/src/test/mocks/mockWorkspaceConfig.ts b/src/test/mocks/mockWorkspaceConfig.ts new file mode 100644 index 000000000000..8627cd599fba --- /dev/null +++ b/src/test/mocks/mockWorkspaceConfig.ts @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { ConfigurationTarget, WorkspaceConfiguration } from 'vscode'; + +type SectionType<T> = { + key: string; + defaultValue?: T | undefined; + globalValue?: T | undefined; + globalLanguageValue?: T | undefined; + workspaceValue?: T | undefined; + workspaceLanguageValue?: T | undefined; + workspaceFolderValue?: T | undefined; + workspaceFolderLanguageValue?: T | undefined; +}; + +export class MockWorkspaceConfiguration implements WorkspaceConfiguration { + private values = new Map<string, unknown>(); + + constructor(defaultSettings?: { [key: string]: unknown }) { + if (defaultSettings) { + const keys = [...Object.keys(defaultSettings)]; + keys.forEach((k) => this.values.set(k, defaultSettings[k])); + } + } + + public get<T>(key: string, defaultValue?: T): T | undefined { + if (this.values.has(key)) { + return this.values.get(key) as T; + } + + return arguments.length > 1 ? defaultValue : undefined; + } + + public has(section: string): boolean { + return this.values.has(section); + } + + public inspect<T>(section: string): SectionType<T> | undefined { + return this.values.get(section) as SectionType<T>; + } + + public update( + section: string, + value: unknown, + _configurationTarget?: boolean | ConfigurationTarget | undefined, + ): Promise<void> { + this.values.set(section, value); + return Promise.resolve(); + } +} diff --git a/src/test/mocks/moduleInstaller.ts b/src/test/mocks/moduleInstaller.ts index 064e319fc97c..fb183e6ebd99 100644 --- a/src/test/mocks/moduleInstaller.ts +++ b/src/test/mocks/moduleInstaller.ts @@ -1,17 +1,33 @@ import { EventEmitter } from 'events'; import { Uri } from 'vscode'; import { IModuleInstaller } from '../../client/common/installer/types'; +import { Product } from '../../client/common/types'; +import { ModuleInstallerType } from '../../client/pythonEnvironments/info'; export class MockModuleInstaller extends EventEmitter implements IModuleInstaller { constructor(public readonly displayName: string, private supported: boolean) { super(); } + + // eslint-disable-next-line class-methods-use-this + public get name(): string { + return 'mock'; + } + + // eslint-disable-next-line class-methods-use-this + public get type(): ModuleInstallerType { + return ModuleInstallerType.Pip; + } + + // eslint-disable-next-line class-methods-use-this public get priority(): number { return 0; } - public async installModule(name: string, _resource?: Uri): Promise<void> { + + public async installModule(name: Product | string, _resource?: Uri): Promise<void> { this.emit('installModule', name); } + public async isSupported(_resource?: Uri): Promise<boolean> { return this.supported; } diff --git a/src/test/mocks/proc.ts b/src/test/mocks/proc.ts index 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<string> { let value: Observable<Output<string>> | Output<string> | undefined; let valueReturned = false; - this.emit('execObservable', file, args, options, (result: Observable<Output<string>> | Output<string>) => { value = result; valueReturned = true; }); + this.emit('execObservable', file, args, options, (result: Observable<Output<string>> | Output<string>) => { + value = result; + valueReturned = true; + }); if (valueReturned) { const output = value as Output<string>; - if (['stderr', 'stdout'].some(source => source === output.source)) { + if (['stderr', 'stdout'].some((source) => source === output.source)) { return { - // tslint:disable-next-line:no-any - proc: {} as any, + proc: {} as ChildProcess, out: Observable.of(output), - dispose: () => { noop(); } - }; - } else { - return { - // tslint:disable-next-line:no-any - proc: {} as any, - out: value as Observable<Output<string>>, - dispose: () => { noop(); } + dispose: () => { + noop(); + }, }; } - } else { - return this.procService.execObservable(file, args, options); + return { + proc: {} as ChildProcess, + out: value as Observable<Output<string>>, + dispose: () => { + noop(); + }, + }; } + return this.procService.execObservable(file, args, options); } - public onExec(handler: (file: string, args: string[], options: SpawnOptions, callback: ExecCallback) => void) { + + public onExec( + handler: (file: string, args: string[], options: SpawnOptions, callback: ExecCallback) => void, + ): void { this.on('exec', handler); } + public async exec(file: string, args: string[], options: SpawnOptions = {}): Promise<ExecutionResult<string>> { let value: ExecutionResult<string> | undefined; let valueReturned = false; - this.emit('exec', file, args, options, (result: ExecutionResult<string>) => { value = result; valueReturned = true; }); + this.emit('exec', file, args, options, (result: ExecutionResult<string>) => { + 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<ExecutionResult<string>> { let value: ExecutionResult<string> | undefined; let valueReturned = false; - this.emit('shellExec', command, options, (result: ExecutionResult<string>) => { value = result; valueReturned = true; }); + this.emit('shellExec', command, options, (result: ExecutionResult<string>) => { + 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 fbf90f4f1675..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 }) { } + constructor(public env: EnvironmentVariables = { ...process.env }) {} + + // eslint-disable-next-line @typescript-eslint/ban-types public on(_event: string | symbol, _listener: Function): this { return this; } + + // eslint-disable-next-line class-methods-use-this public get argv(): string[] { return []; } + + // eslint-disable-next-line class-methods-use-this public get stdout(): NodeJS.WriteStream { - return TypeMoq.Mock.ofType<NodeJS.WriteStream>().object; + return createTypeMoq<NodeJS.WriteStream>().object; } + + // eslint-disable-next-line class-methods-use-this public get stdin(): NodeJS.ReadStream { - return TypeMoq.Mock.ofType<NodeJS.ReadStream>().object; + return createTypeMoq<NodeJS.ReadStream>().object; } - 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 e2349a5aa1d5..ad2020c57110 100644 --- a/src/test/mocks/vsc/arrays.ts +++ b/src/test/mocks/vsc/arrays.ts @@ -1,405 +1,399 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + 'use strict'; -// tslint:disable:all +/** + * Returns the last element of an array. + * @param array The array. + * @param n Which element from the end (default is zero). + */ +export function tail<T>(array: T[], n = 0): T { + return array[array.length - (1 + n)]; +} -export namespace vscMockArrays { - /** - * Returns the last element of an array. - * @param array The array. - * @param n Which element from the end (default is zero). - */ - export function tail<T>(array: T[], n: number = 0): T { - return array[array.length - (1 + n)]; +export function equals<T>(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; } - export function equals<T>(one: T[], other: T[], itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { - if (one.length !== other.length) { + for (let i = 0, len = one.length; i < len; i += 1) { + if (!itemEquals(one[i], other[i])) { return false; } + } - for (let i = 0, len = one.length; i < len; i++) { - if (!itemEquals(one[i], other[i])) { - return false; - } - } + return true; +} - return true; +export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { + let low = 0; + let high = array.length - 1; + + while (low <= high) { + const mid = ((low + high) / 2) | 0; + const comp = comparator(array[mid], key); + if (comp < 0) { + low = mid + 1; + } else if (comp > 0) { + high = mid - 1; + } else { + return mid; + } } + return -(low + 1); +} - export function binarySearch<T>(array: T[], key: T, comparator: (op1: T, op2: T) => number): number { - let low = 0, - high = array.length - 1; - - while (low <= high) { - let mid = ((low + high) / 2) | 0; - let comp = comparator(array[mid], key); - if (comp < 0) { - low = mid + 1; - } else if (comp > 0) { - high = mid - 1; - } else { - return mid; - } +/** + * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false + * are located before all elements where p(x) is true. + * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. + */ +export function findFirst<T>(array: T[], p: (x: T) => boolean): number { + let low = 0; + let high = array.length; + if (high === 0) { + return 0; // no children + } + while (low < high) { + const mid = Math.floor((low + high) / 2); + if (p(array[mid])) { + high = mid; + } else { + low = mid + 1; } - return -(low + 1); } + return low; +} - /** - * Takes a sorted array and a function p. The array is sorted in such a way that all elements where p(x) is false - * are located before all elements where p(x) is true. - * @returns the least x for which p(x) is true or array.length if no element fullfills the given function. - */ - export function findFirst<T>(array: T[], p: (x: T) => boolean): number { - let low = 0, high = array.length; - if (high === 0) { - return 0; // no children - } - while (low < high) { - let mid = Math.floor((low + high) / 2); - if (p(array[mid])) { - high = mid; - } else { - low = mid + 1; - } +/** + * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` + * so only use this when actually needing stable sort. + */ +export function mergeSort<T>(data: T[], compare: (a: T, b: T) => number): T[] { + _divideAndMerge(data, compare); + return data; +} + +function _divideAndMerge<T>(data: T[], compare: (a: T, b: T) => number): void { + if (data.length <= 1) { + // sorted + return; + } + const p = (data.length / 2) | 0; + const left = data.slice(0, p); + const right = data.slice(p); + + _divideAndMerge(left, compare); + _divideAndMerge(right, compare); + + let leftIdx = 0; + let rightIdx = 0; + let i = 0; + while (leftIdx < left.length && rightIdx < right.length) { + const ret = compare(left[leftIdx], right[rightIdx]); + if (ret <= 0) { + // smaller_equal -> take left to preserve order + data[(i += 1)] = left[(leftIdx += 1)]; + } else { + // greater -> take right + data[(i += 1)] = right[(rightIdx += 1)]; } - return low; } + while (leftIdx < left.length) { + data[(i += 1)] = left[(leftIdx += 1)]; + } + while (rightIdx < right.length) { + data[(i += 1)] = right[(rightIdx += 1)]; + } +} - /** - * Like `Array#sort` but always stable. Usually runs a little slower `than Array#sort` - * so only use this when actually needing stable sort. - */ - export function mergeSort<T>(data: T[], compare: (a: T, b: T) => number): T[] { - _divideAndMerge(data, compare); - return data; +export function groupBy<T>(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<T>(data: T[], compare: (a: T, b: T) => number): void { - if (data.length <= 1) { - // sorted +type IMutableSplice<T> = { + deleteCount: number; + start: number; + toInsert: T[]; +}; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ISplice<T> = Array<T> & any; + +/** + * Diffs two *sorted* arrays and computes the splices which apply the diff. + */ +export function sortedDiff<T>(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice<T>[] { + const result: IMutableSplice<T>[] = []; + + function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { + if (deleteCount === 0 && toInsert.length === 0) { return; } - const p = (data.length / 2) | 0; - const left = data.slice(0, p); - const right = data.slice(p); - - _divideAndMerge(left, compare); - _divideAndMerge(right, compare); - - let leftIdx = 0; - let rightIdx = 0; - let i = 0; - while (leftIdx < left.length && rightIdx < right.length) { - let ret = compare(left[leftIdx], right[rightIdx]); - if (ret <= 0) { - // smaller_equal -> take left to preserve order - data[i++] = left[leftIdx++]; - } else { - // greater -> take right - data[i++] = right[rightIdx++]; - } - } - while (leftIdx < left.length) { - data[i++] = left[leftIdx++]; - } - while (rightIdx < right.length) { - data[i++] = right[rightIdx++]; + + const latest = result[result.length - 1]; + + if (latest && latest.start + latest.deleteCount === start) { + latest.deleteCount += deleteCount; + latest.toInsert.push(...toInsert); + } else { + result.push({ start, deleteCount, toInsert }); } } - export function groupBy<T>(data: T[], compare: (a: T, b: T) => number): T[][] { - const result: T[][] = []; - let currentGroup: T[]; - for (const element of mergeSort(data.slice(0), compare)) { - // @ts-ignore - if (!currentGroup || compare(currentGroup[0], element) !== 0) { - currentGroup = [element]; - result.push(currentGroup); - } else { - currentGroup.push(element); - } + let beforeIdx = 0; + let afterIdx = 0; + + while (beforeIdx !== before.length || afterIdx !== after.length) { + const beforeElement = before[beforeIdx]; + const afterElement = after[afterIdx]; + const n = compare(beforeElement, afterElement); + if (n === 0) { + // equal + beforeIdx += 1; + afterIdx += 1; + } else if (n < 0) { + // beforeElement is smaller -> before element removed + pushSplice(beforeIdx, 1, []); + beforeIdx += 1; + } else if (n > 0) { + // beforeElement is greater -> after element added + pushSplice(beforeIdx, 0, [afterElement]); + afterIdx += 1; } - return result; } - type IMutableSplice<T> = Array<T> & any & { - deleteCount: number; + if (beforeIdx === before.length) { + pushSplice(beforeIdx, 0, after.slice(afterIdx)); + } else if (afterIdx === after.length) { + pushSplice(beforeIdx, before.length - beforeIdx, []); } - type ISplice<T> = Array<T> & any; - - /** - * Diffs two *sorted* arrays and computes the splices which apply the diff. - */ - export function sortedDiff<T>(before: T[], after: T[], compare: (a: T, b: T) => number): ISplice<T>[] { - const result: IMutableSplice<T>[] = []; - - function pushSplice(start: number, deleteCount: number, toInsert: T[]): void { - if (deleteCount === 0 && toInsert.length === 0) { - return; - } - - const latest = result[result.length - 1]; - - if (latest && latest.start + latest.deleteCount === start) { - latest.deleteCount += deleteCount; - latest.toInsert.push(...toInsert); - } else { - result.push({ start, deleteCount, toInsert }); - } - } - let beforeIdx = 0; - let afterIdx = 0; - - while (true) { - if (beforeIdx === before.length) { - pushSplice(beforeIdx, 0, after.slice(afterIdx)); - break; - } - if (afterIdx === after.length) { - pushSplice(beforeIdx, before.length - beforeIdx, []); - break; - } - - const beforeElement = before[beforeIdx]; - const afterElement = after[afterIdx]; - const n = compare(beforeElement, afterElement); - if (n === 0) { - // equal - beforeIdx += 1; - afterIdx += 1; - } else if (n < 0) { - // beforeElement is smaller -> before element removed - pushSplice(beforeIdx, 1, []); - beforeIdx += 1; - } else if (n > 0) { - // beforeElement is greater -> after element added - pushSplice(beforeIdx, 0, [afterElement]); - afterIdx += 1; - } - } + return result; +} - return result; +/** + * Takes two *sorted* arrays and computes their delta (removed, added elements). + * Finishes in `Math.min(before.length, after.length)` steps. + */ +export function delta<T>(before: T[], after: T[], compare: (a: T, b: T) => number): { removed: T[]; added: T[] } { + const splices = sortedDiff(before, after, compare); + const removed: T[] = []; + const added: T[] = []; + + for (const splice of splices) { + removed.push(...before.slice(splice.start, splice.start + splice.deleteCount)); + added.push(...splice.toInsert); } - /** - * Takes two *sorted* arrays and computes their delta (removed, added elements). - * Finishes in `Math.min(before.length, after.length)` steps. - * @param before - * @param after - * @param compare - */ - export function delta<T>(before: T[], after: T[], compare: (a: T, b: T) => number): { removed: T[], added: T[] } { - const splices = sortedDiff(before, after, compare); - const removed: T[] = []; - const added: T[] = []; - - for (const splice of splices) { - removed.push(...before.slice(splice.start, splice.start + splice.deleteCount)); - added.push(...splice.toInsert); - } + return { removed, added }; +} - return { removed, added }; +/** + * Returns the top N elements from the array. + * + * Faster than sorting the entire array when the array is a lot larger than N. + * + * @param array The unsorted array. + * @param compare A sort function for the elements. + * @param n The number of elements to return. + * @return The first n elemnts from array when sorted with compare. + */ +export function top<T>(array: T[], compare: (a: T, b: T) => number, n: number): T[] { + if (n === 0) { + return []; } + const result = array.slice(0, n).sort(compare); + topStep(array, compare, result, n, array.length); + return result; +} - /** - * Returns the top N elements from the array. - * - * Faster than sorting the entire array when the array is a lot larger than N. - * - * @param array The unsorted array. - * @param compare A sort function for the elements. - * @param n The number of elements to return. - * @return The first n elemnts from array when sorted with compare. - */ - export function top<T>(array: T[], compare: (a: T, b: T) => number, n: number): T[] { - if (n === 0) { - return []; +function topStep<T>(array: T[], compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { + for (const n = result.length; i < m; i += 1) { + const element = array[i]; + if (compare(element, result[n - 1]) < 0) { + result.pop(); + const j = findFirst(result, (e) => compare(element, e) < 0); + result.splice(j, 0, element); } - const result = array.slice(0, n).sort(compare); - topStep(array, compare, result, n, array.length); - return result; } +} - function topStep<T>(array: T[], compare: (a: T, b: T) => number, result: T[], i: number, m: number): void { - for (const n = result.length; i < m; i++) { - const element = array[i]; - if (compare(element, result[n - 1]) < 0) { - result.pop(); - const j = findFirst(result, e => compare(element, e) < 0); - result.splice(j, 0, element); - } - } +/** + * @returns a new array with all undefined or null values removed. The original array is not modified at all. + */ +export function coalesce<T>(array: T[]): T[] { + if (!array) { + return array; } - /** - * @returns a new array with all undefined or null values removed. The original array is not modified at all. - */ - export function coalesce<T>(array: T[]): T[] { - if (!array) { - return array; - } + return array.filter((e) => !!e); +} - return array.filter(e => !!e); - } +/** + * Moves the element in the array for the provided positions. + */ +export function move(array: unknown[], from: number, to: number): void { + array.splice(to, 0, array.splice(from, 1)[0]); +} - /** - * Moves the element in the array for the provided positions. - */ - export function move(array: any[], from: number, to: number): void { - array.splice(to, 0, array.splice(from, 1)[0]); - } +/** + * @returns {{false}} if the provided object is an array + * and not empty. + */ +export function isFalsyOrEmpty(obj: unknown): boolean { + return !Array.isArray(obj) || (<Array<unknown>>obj).length === 0; +} - /** - * @returns {{false}} if the provided object is an array - * and not empty. - */ - export function isFalsyOrEmpty(obj: any): boolean { - return !Array.isArray(obj) || (<Array<any>>obj).length === 0; +/** + * Removes duplicates from the given array. The optional keyFn allows to specify + * how elements are checked for equalness by returning a unique string for each. + */ +export function distinct<T>(array: T[], keyFn?: (t: T) => string): T[] { + if (!keyFn) { + return array.filter((element, position) => array.indexOf(element) === position); } - /** - * Removes duplicates from the given array. The optional keyFn allows to specify - * how elements are checked for equalness by returning a unique string for each. - */ - export function distinct<T>(array: T[], keyFn?: (t: T) => string): T[] { - if (!keyFn) { - return array.filter((element, position) => { - return array.indexOf(element) === position; - }); + const seen: Record<string, boolean> = Object.create(null); + return array.filter((elem) => { + const key = keyFn(elem); + if (seen[key]) { + return false; } - const seen: Record<string, boolean> = Object.create(null); - return array.filter((elem) => { - const key = keyFn(elem); - if (seen[key]) { - return false; - } + seen[key] = true; - seen[key] = true; + return true; + }); +} - return true; - }); - } +export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean { + const seen: Record<string, boolean> = Object.create(null); - export function uniqueFilter<T>(keyFn: (t: T) => string): (t: T) => boolean { - const seen: Record<string, boolean> = Object.create(null); + return (element) => { + const key = keyFn(element); - return element => { - const key = keyFn(element); + if (seen[key]) { + return false; + } + + seen[key] = true; + return true; + }; +} - if (seen[key]) { - return false; - } +export function firstIndex<T>(array: T[], fn: (item: T) => boolean): number { + for (let i = 0; i < array.length; i += 1) { + const element = array[i]; - seen[key] = true; - return true; - }; + if (fn(element)) { + return i; + } } - export function firstIndex<T>(array: T[], fn: (item: T) => boolean): number { - for (let i = 0; i < array.length; i++) { - const element = array[i]; + return -1; +} - if (fn(element)) { - return i; - } - } +export function first<T>(array: T[], fn: (item: T) => boolean, notFoundValue: T | null = null): T { + const idx = firstIndex(array, fn); + return idx < 0 && notFoundValue !== null ? notFoundValue : array[idx]; +} - return -1; - } - // @ts-ignore - export function first<T>(array: T[], fn: (item: T) => boolean, notFoundValue: T = null): T { - const index = firstIndex(array, fn); - return index < 0 ? notFoundValue : array[index]; +export function commonPrefixLength<T>(one: T[], other: T[], eqls: (a: T, b: T) => boolean = (a, b) => a === b): number { + let result = 0; + + for (let i = 0, len = Math.min(one.length, other.length); i < len && eqls(one[i], other[i]); i += 1) { + result += 1; } - export function commonPrefixLength<T>(one: T[], other: T[], equals: (a: T, b: T) => boolean = (a, b) => a === b): number { - let result = 0; + return result; +} - for (let i = 0, len = Math.min(one.length, other.length); i < len && equals(one[i], other[i]); i++) { - result++; - } +export function flatten<T>(arr: T[][]): T[] { + return ([] as T[]).concat(...arr); +} - return result; - } +export function range(to: number): number[]; +export function range(from: number, to: number): number[]; +export function range(arg: number, to?: number): number[] { + let from = typeof to === 'number' ? arg : 0; - export function flatten<T>(arr: T[][]): T[] { - // @ts-ignore - return [].concat(...arr); + if (typeof to === 'number') { + from = arg; + } else { + from = 0; + to = arg; } - export function range(to: number): number[]; - export function range(from: number, to: number): number[]; - export function range(arg: number, to?: number): number[] { - let from = typeof to === 'number' ? arg : 0; + const result: number[] = []; - if (typeof to === 'number') { - from = arg; - } else { - from = 0; - to = arg; + if (from <= to) { + for (let i = from; i < to; i += 1) { + result.push(i); } - - const result: number[] = []; - - if (from <= to) { - for (let i = from; i < to; i++) { - result.push(i); - } - } else { - for (let i = from; i > to; i--) { - result.push(i); - } + } else { + for (let i = from; i > to; i -= 1) { + result.push(i); } - - return result; } - export function fill<T>(num: number, valueFn: () => T, arr: T[] = []): T[] { - for (let i = 0; i < num; i++) { - arr[i] = valueFn(); - } + return result; +} - return arr; +export function fill<T>(num: number, valueFn: () => T, arr: T[] = []): T[] { + for (let i = 0; i < num; i += 1) { + arr[i] = valueFn(); } - export function index<T>(array: T[], indexer: (t: T) => string): Record<string, T>; - export function index<T, R>(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): Record<string, R>; - export function index<T, R>(array: T[], indexer: (t: T) => string, merger: (t: T, r: R) => R = t => t as any): Record<string, R> { - return array.reduce((r, t) => { - const key = indexer(t); - r[key] = merger(t, r[key]); - return r; - }, Object.create(null)); - } + return arr; +} - /** - * Inserts an element into an array. Returns a function which, when - * called, will remove that element from the array. - */ - export function insert<T>(array: T[], element: T): () => void { - array.push(element); - - return () => { - const index = array.indexOf(element); - if (index > -1) { - array.splice(index, 1); - } - }; - } +export function index<T>(array: T[], indexer: (t: T) => string): Record<string, T>; +export function index<T, R>(array: T[], indexer: (t: T) => string, merger?: (t: T, r: R) => R): Record<string, R>; +export function index<T, R>( + array: T[], + indexer: (t: T) => string, + merger: (t: T, r: R) => R = (t) => (t as unknown) as R, +): Record<string, R> { + return array.reduce((r, t) => { + const key = indexer(t); + r[key] = merger(t, r[key]); + return r; + }, Object.create(null)); +} - /** - * Insert `insertArr` inside `target` at `insertIndex`. - * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array - */ - export function arrayInsert<T>(target: T[], insertIndex: number, insertArr: T[]): T[] { - const before = target.slice(0, insertIndex); - const after = target.slice(insertIndex); - return before.concat(insertArr, after); - } +/** + * Inserts an element into an array. Returns a function which, when + * called, will remove that element from the array. + */ +export function insert<T>(array: T[], element: T): () => void { + array.push(element); + + return () => { + const idx = array.indexOf(element); + if (idx > -1) { + array.splice(idx, 1); + } + }; +} + +/** + * Insert `insertArr` inside `target` at `insertIndex`. + * Please don't touch unless you understand https://jsperf.com/inserting-an-array-within-an-array + */ +export function arrayInsert<T>(target: T[], insertIndex: number, insertArr: T[]): T[] { + const before = target.slice(0, insertIndex); + const after = target.slice(insertIndex); + return before.concat(insertArr, after); } diff --git a/src/test/mocks/vsc/charCode.ts b/src/test/mocks/vsc/charCode.ts 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 8e595aea523c..c2c1188c3449 100644 --- a/src/test/mocks/vsc/extHostedTypes.ts +++ b/src/test/mocks/vsc/extHostedTypes.ts @@ -1,2017 +1,2352 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -// import * as crypto from 'crypto'; +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable max-classes-per-file */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. -// tslint:disable:all +'use strict'; import { relative } from 'path'; import * as vscode from 'vscode'; -import { vscMockHtmlContent } from './htmlContent'; -import { vscMockStrings } from './strings'; -import { vscUri } from './uri'; +import * as vscMockHtmlContent from './htmlContent'; +import * as vscMockStrings from './strings'; +import * as vscUri from './uri'; import { generateUuid } from './uuid'; -export namespace vscMockExtHostedTypes { +export enum 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; +} + +const illegalArgument = (msg = 'Illegal Argument') => new Error(msg); - export class Disposable { - static from(...disposables: { dispose(): any }[]): Disposable { - return new Disposable(function () { - if (disposables) { - for (let disposable of disposables) { - if (disposable && typeof disposable.dispose === 'function') { - disposable.dispose(); - } +export class Disposable { + static from(...disposables: { dispose(): () => void }[]): Disposable { + return new Disposable(() => { + if (disposables) { + for (const disposable of disposables) { + if (disposable && typeof disposable.dispose === 'function') { + disposable.dispose(); } - // @ts-ignore - disposables = undefined; } - }); - } - private _callOnDispose: Function; + disposables = []; + } + }); + } - constructor(callOnDispose: Function) { - this._callOnDispose = callOnDispose; - } + private _callOnDispose: (() => void) | undefined; - dispose(): any { - if (typeof this._callOnDispose === 'function') { - this._callOnDispose(); - // @ts-ignore - this._callOnDispose = undefined; - } - } + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; } - 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) { - // @ts-ignore - if (p.isBefore(result)) { - result = p; - } +export class Position { + static Min(...positions: Position[]): Position { + let result = positions.pop(); + for (const p of positions) { + if (result && p.isBefore(result)) { + result = p; } - // @ts-ignore - return result; } + return result || new Position(0, 0); + } - static Max(...positions: Position[]): Position { - let result = positions.pop(); - for (let p of positions) { - // @ts-ignore - if (p.isAfter(result)) { - result = p; - } + static Max(...positions: Position[]): Position { + let result = positions.pop(); + for (const p of positions) { + if (result && p.isAfter(result)) { + result = p; } - // @ts-ignore - return result; } + return result || new Position(0, 0); + } - static isPosition(other: any): other is Position { - if (!other) { - return false; - } - if (other instanceof Position) { - return true; - } - let { line, character } = <Position>other; - if (typeof line === 'number' && typeof character === 'number') { - return true; - } + static isPosition(other: unknown): other is Position { + if (!other) { return false; } + if (other instanceof Position) { + return true; + } + const { line, character } = <Position>other; + if (typeof line === 'number' && typeof character === 'number') { + return true; + } + return false; + } - private _line: number; - private _character: number; + private _line: number; - get line(): number { - return this._line; - } + private _character: number; + + get 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; - // @ts-ignore - 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; - // @ts-ignore - 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; + + get start(): Position { + return this._start; + } - let line: number; - if (typeof lineOrChange === 'undefined') { - line = this.line; + get end(): Position { + return this._end; + } - } else if (typeof lineOrChange === 'number') { - line = lineOrChange; + constructor(start: Position, end: Position); - } else { - line = typeof lineOrChange.line === 'number' ? lineOrChange.line : this.line; - character = typeof lineOrChange.character === 'number' ? lineOrChange.character : this.character; - } + constructor(startLine: number, startColumn: number, endLine: number, endColumn: number); - if (line === this.line && character === this.character) { - return this; - } - return new Position(line, character); + 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((<Range>thing).start) - && Position.isPosition((<Range>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; + get isEmpty(): boolean { + return this._start.isEqual(this._end); + } - if (typeof startLineOrStart === 'number' && typeof startColumnOrEnd === 'number' && typeof endLine === 'number' && typeof endColumn === 'number') { - start = new Position(startLineOrStart, startColumnOrEnd); - end = new Position(endLine, endColumn); - } else if (startLineOrStart instanceof Position && startColumnOrEnd instanceof Position) { - start = startLineOrStart; - end = startColumnOrEnd; - } - // @ts-ignore - if (!start || !end) { - throw new Error('Invalid arguments'); - } + 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: - // |-----| - // |----| - // @ts-ignore - 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((<Selection>thing).anchor) && + Position.isPosition((<Selection>thing).active) && + typeof (<Selection>thing).isReversed === 'boolean' + ); + } - with(change: { start?: Position, end?: Position }): Range; - // @ts-ignore - with(start?: Position, end?: Position): Range; - with(startOrChange: Position | { start?: Position, end?: Position }, end: Position = this.end): Range { + 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((<Selection>thing).anchor) - && Position.isPosition((<Selection>thing).active) - && typeof (<Selection>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(<TextEdit>thing) && typeof (<TextEdit>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; - } - // @ts-ignore - if (!anchor || !active) { - throw new Error('Invalid arguments'); - } + static setEndOfLine(eol: EndOfLine): TextEdit { + const ret = new TextEdit(new Range(new Position(0, 0), new Position(0, 0)), ''); + ret.newEol = eol; + return ret; + } - super(anchor, active); + _range: Range = new Range(new Position(0, 0), new Position(0, 0)); - this._anchor = anchor; - this._active = active; - } + newText = ''; + + _newEol: EndOfLine = EndOfLine.LF; + + get range(): Range { + return this._range; + } - get isReversed(): boolean { - return this._anchor === this._end; + 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((<TextEdit>thing)) - && typeof (<TextEdit>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 { - // @ts-ignore - let ret = new TextEdit(undefined, undefined); - ret.newEol = eol; - return ret; - } - // @ts-ignore - protected _range: Range; - // @ts-ignore - protected _newText: string; - // @ts-ignore - protected _newEol: EndOfLine; + private _seqPool = 0; - get range(): Range { - return this._range; - } + private _resourceEdits: { seq: number; from: vscUri.URI; to: vscUri.URI }[] = []; - set range(value: Range) { - if (value && !Range.isRange(value)) { - throw illegalArgument('range'); - } - this._range = value; - } + private _textEdits = new Map<string, { seq: number; uri: vscUri.URI; edits: TextEdit[] }>(); - get newText(): string { - return this._newText || ''; - } + // createResource(uri: vscode.Uri): void { + // this.renameResource(undefined, uri); + // } - set newText(value: string) { - if (value && typeof value !== 'string') { - throw illegalArgument('newText'); - } - this._newText = value; - } + // deleteResource(uri: vscode.Uri): void { + // this.renameResource(uri, undefined); + // } - get newEol(): EndOfLine { - return this._newEol; - } + // renameResource(from: vscode.Uri, to: vscode.Uri): void { + // this._resourceEdits.push({ seq: this._seqPool+= 1, from, to }); + // } - set newEol(value: EndOfLine) { - if (value && typeof value !== 'number') { - throw illegalArgument('newEol'); - } - this._newEol = value; - } + // resourceEdits(): [vscode.Uri, vscode.Uri][] { + // return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to])); + // } - constructor(range: Range, newText: string) { - this.range = range; - this.newText = newText; - } + // eslint-disable-next-line class-methods-use-this + createFile(_uri: vscode.Uri, _options?: { overwrite?: boolean; ignoreIfExists?: 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 + deleteFile(_uri: vscode.Uri, _options?: { recursive?: boolean; ignoreIfNotExists?: boolean }): void { + throw new Error('Method not implemented.'); + } + + // 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<string, { seq: number, uri: vscUri.URI, edits: TextEdit[] }>(); + 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 (<SnippetString>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) { - // @ts-ignore - 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())) { - // @ts-ignore - return undefined; - } - // @ts-ignore - 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 += '|}'; + + return this; + } - get size(): number { - return this._textEdits.size + this._resourceEdits.length; + 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 (<SnippetString>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((<Location>thing).range) && vscUri.URI.isUri((<Location>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 (<DiagnosticRelatedInformation>thing).message === 'string' && + (<DiagnosticRelatedInformation>thing).location && + Range.isRange((<DiagnosticRelatedInformation>thing).location.range) && + vscUri.URI.isUri((<DiagnosticRelatedInformation>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 = <vscode.MarkdownString[]>contents; + } else if (vscMockHtmlContent.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; } + + this.range = range || new Range(new Position(0, 0), new Position(0, 0)); } +} + +export enum DocumentHighlightKind { + Text = 0, + Read = 1, + Write = 2, +} + +export class DocumentHighlight { + range: Range; - export enum DiagnosticTag { - Unnecessary = 1, + 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 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 Location { +export class SymbolInformation { + name: string; - static isLocation(thing: any): thing is Location { - if (thing instanceof Location) { - return true; - } - if (!thing) { - return false; - } - return Range.isRange((<Location>thing).range) - && vscUri.URI.isUri((<Location>thing).uri); - } + location: Location = new Location( + vscUri.URI.parse('testLocation'), + new Range(new Position(0, 0), new Position(0, 0)), + ); - uri: vscUri.URI; - // @ts-ignore - range: Range; + kind: SymbolKind; - constructor(uri: vscUri.URI, rangeOrPosition: Range | Position) { - this.uri = uri; + containerName: string; - 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'); - } + 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, + }; + } +} - static is(thing: any): thing is DiagnosticRelatedInformation { - if (!thing) { - return false; - } - return typeof (<DiagnosticRelatedInformation>thing).message === 'string' - && (<DiagnosticRelatedInformation>thing).location - && Range.isRange((<DiagnosticRelatedInformation>thing).location.range) - && vscUri.URI.isUri((<DiagnosticRelatedInformation>thing).location.uri); - } +export class SymbolInformation2 extends SymbolInformation { + definingRange: Range; - location: Location; - message: string; + children: SymbolInformation2[]; - constructor(location: Location, message: string) { - this.location = location; - this.message = message; - } + constructor(name: string, kind: SymbolKind, containerName: string, location: Location) { + super(name, kind, containerName, location); + + this.children = []; + this.definingRange = location.range; } +} - export class Diagnostic { +export enum CodeActionTrigger { + Automatic = 1, + Manual = 2, +} - range: Range; - message: string; - // @ts-ignore - source: string; - // @ts-ignore - code: string | number; - severity: DiagnosticSeverity; - // @ts-ignore - 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 = <vscode.MarkdownString[] | vscode.MarkedString[]>contents; - } else if (vscMockHtmlContent.isMarkdownString(contents)) { - this.contents = [contents]; - } else { - this.contents = [contents]; - } - // @ts-ignore - 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; - // @ts-ignore - location: Location; - kind: SymbolKind; - containerName: string; - - constructor(name: string, kind: SymbolKind, containerName: string, location: Location); - constructor(name: string, kind: SymbolKind, range: Range, uri?: vscUri.URI, containerName?: string); - constructor(name: string, kind: SymbolKind, rangeOrContainer: string | Range, locationOrUri?: Location | vscUri.URI, containerName?: string) { - this.name = name; - this.kind = kind; - // @ts-ignore - this.containerName = containerName; - - if (typeof rangeOrContainer === 'string') { - this.containerName = rangeOrContainer; - } +export class MarkdownString { + value: string; - if (locationOrUri instanceof Location) { - this.location = locationOrUri; - } else if (rangeOrContainer instanceof Range) { - // @ts-ignore - 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 class CodeActionKind { - private static readonly sep = '.'; +export interface CompletionContext { + triggerKind: CompletionTriggerKind; + triggerCharacter: string; +} - 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 enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + File = 16, + Reference = 17, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26, +} - constructor( - public readonly value: string - ) { } +export enum CompletionItemTag { + Deprecated = 1, +} - public append(parts: string): CodeActionKind { - return new CodeActionKind(this.value ? this.value + CodeActionKind.sep + parts : parts); - } +export interface CompletionItemLabel { + name: string; + signature?: string; + qualifier?: string; + type?: string; +} - public contains(other: CodeActionKind): boolean { - return this.value === other.value || vscMockStrings.startsWith(other.value, this.value + CodeActionKind.sep); - } +export class CompletionItem { + label: string; + + label2?: CompletionItemLabel; + + kind?: CompletionItemKind; + + tags?: CompletionItemTag[]; + + detail?: string; + + documentation?: string | MarkdownString; + + sortText?: string; + + filterText?: string; + + preselect?: boolean; + + insertText?: string | SnippetString; + + keepWhitespace?: boolean; + + range?: Range; + + 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 CompletionList { + isIncomplete?: boolean; - export class CodeLens { + items: vscode.CompletionItem[]; - range: Range; + constructor(items: vscode.CompletionItem[] = [], isIncomplete = false) { + this.items = items; + this.isIncomplete = isIncomplete; + } +} - command: vscode.Command; +export class CallHierarchyItem { + name: string; - constructor(range: Range, command?: vscode.Command) { - this.range = range; - // @ts-ignore - this.command = command; - } + kind: SymbolKind; - get isResolved(): boolean { - return !!this.command; - } + tags?: ReadonlyArray<vscode.SymbolTag>; + + 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 class MarkdownString { +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, +} - value: string; - isTrusted?: boolean; +export enum StatusBarAlignment { + Left = 1, + Right = 2, +} - constructor(value?: string) { - this.value = value || ''; +export enum TextEditorLineNumbersStyle { + Off = 0, + On = 1, + Relative = 2, +} + +export enum TextDocumentSaveReason { + Manual = 1, + AfterDelay = 2, + FocusOut = 3, +} + +export enum TextEditorRevealType { + Default = 0, + InCenter = 1, + InCenterIfOutsideViewport = 2, + AtTop = 3, +} + +// eslint-disable-next-line import/export +export enum TextEditorSelectionChangeKind { + Keyboard = 1, + Mouse = 2, + Command = 3, +} + +/** + * These values match very carefully the values of `TrackedRangeStickiness` + */ +export enum DecorationRangeBehavior { + /** + * 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; } + } +} - appendText(value: string): MarkdownString { - // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash - this.value += value.replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&'); - return this; +export class DocumentLink { + range: Range; + + target: vscUri.URI; + + constructor(range: Range, target: vscUri.URI) { + if (target && !(target instanceof vscUri.URI)) { + throw illegalArgument('target'); + } + if (!Range.isRange(range) || range.isEmpty) { + throw illegalArgument('range'); } + this.range = range; + this.target = target; + } +} - appendMarkdown(value: string): MarkdownString { - this.value += value; - 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; + } +} - appendCodeblock(code: string, language: string = ''): MarkdownString { - this.value += '\n```'; - this.value += language; - this.value += '\n'; - this.value += code; - this.value += '\n```\n'; - return this; +export class ColorPresentation { + label: string; + + textEdit?: TextEdit; + + additionalTextEdits?: TextEdit[]; + + constructor(label: string) { + if (!label || typeof label !== 'string') { + throw illegalArgument('label'); } + this.label = label; } +} + +export enum ColorFormat { + RGB = 0, + HEX = 1, + HSL = 2, +} + +export enum SourceControlInputBoxValidationType { + Error = 0, + Warning = 1, + Information = 2, +} + +export enum TaskRevealKind { + Always = 1, + + Silent = 2, + + Never = 3, +} + +export enum TaskPanelKind { + Shared = 1, - export class ParameterInformation { + Dedicated = 2, - label: string; - documentation?: string | MarkdownString; + New = 3, +} - constructor(label: string, documentation?: string | MarkdownString) { - this.label = label; - this.documentation = documentation; - } - } +export class TaskGroup implements vscode.TaskGroup { + private _id: string; - export class SignatureInformation { + public isDefault = undefined; - label: string; - documentation?: string | MarkdownString; - parameters: ParameterInformation[]; + public static Clean: TaskGroup = new TaskGroup('clean', 'Clean'); - constructor(label: string, documentation?: string | MarkdownString) { - this.label = label; - this.documentation = documentation; - this.parameters = []; - } - } + public static Build: TaskGroup = new TaskGroup('build', 'Build'); - export class SignatureHelp { + public static Rebuild: TaskGroup = new TaskGroup('rebuild', 'Rebuild'); - signatures: SignatureInformation[]; - // @ts-ignore - activeSignature: number; - // @ts-ignore - 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; - // @ts-ignore - detail: string; - // @ts-ignore - documentation: string | MarkdownString; - // @ts-ignore - sortText: string; - // @ts-ignore - filterText: string; - // @ts-ignore - insertText: string | SnippetString; - // @ts-ignore - range: Range; - // @ts-ignore - textEdit: TextEdit; - // @ts-ignore - additionalTextEdits: TextEdit[]; - // @ts-ignore - command: vscode.Command; - - constructor(label: string, kind?: CompletionItemKind) { - this.label = label; - // @ts-ignore - 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 DocumentLink { +export class ShellExecution implements vscode.ShellExecution { + private _commandLine = ''; - range: Range; + private _command: string | vscode.ShellQuotedString = ''; - target: vscUri.URI; + private _args: (string | vscode.ShellQuotedString)[] = []; - constructor(range: Range, target: vscUri.URI) { - if (target && !(target instanceof vscUri.URI)) { - throw illegalArgument('target'); + private _options: vscode.ShellExecutionOptions | undefined; + + 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'); } - if (!Range.isRange(range) || range.isEmpty) { - throw illegalArgument('range'); + 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[]; - // @ts-ignore - 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; - // @ts-ignore - this._options = varg2; - } else { - this._options = varg1; - } - } - // @ts-ignore - 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 { - // @ts-ignore - - private _commandLine: string; - // @ts-ignore - private _command: string | vscode.ShellQuotedString; - // @ts-ignore - 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)[]; - // @ts-ignore - this._options = arg2; - } else { - if (typeof arg0 !== 'string') { - throw illegalArgument('commandLine'); - } - this._commandLine = arg0; - // @ts-ignore - 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 static ProcessType: string = 'process'; - private static ShellType: string = 'shell'; - private static EmptyType: string = '$empty'; - - private __id: string | undefined; - - private _definition!: vscode.TaskDefinition; - private _scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined; - private _name!: string; - private _execution: ProcessExecution | ShellExecution | undefined; - private _problemMatchers: string[]; - private _hasDefinedMatchers: boolean; - private _isBackground: boolean; - private _source!: string; - private _group: TaskGroup | undefined; - private _presentationOptions: vscode.TaskPresentationOptions; - private _runOptions: vscode.RunOptions; - - constructor(definition: vscode.TaskDefinition, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); - constructor(definition: vscode.TaskDefinition, scope: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder, name: string, source: string, execution?: ProcessExecution | ShellExecution, problemMatchers?: string | string[]); - constructor(definition: vscode.TaskDefinition, arg2: string | (vscode.TaskScope.Global | vscode.TaskScope.Workspace) | vscode.WorkspaceFolder, arg3: any, arg4?: any, arg5?: any, arg6?: any) { - this.definition = definition; - let problemMatchers: string | string[]; - if (typeof arg2 === 'string') { - this.name = arg2; - this.source = arg3; - this.execution = arg4; - problemMatchers = arg5; - } else if (arg2 === TaskScope.Global || arg2 === TaskScope.Workspace) { - this.target = arg2; - this.name = arg3; - this.source = arg4; - this.execution = arg5; - problemMatchers = arg6; - } else { - this.target = arg2; - this.name = arg3; - this.source = arg4; - this.execution = arg5; - problemMatchers = arg6; - } - if (typeof problemMatchers === 'string') { - this._problemMatchers = [problemMatchers]; - this._hasDefinedMatchers = true; - } else if (Array.isArray(problemMatchers)) { - this._problemMatchers = problemMatchers; - this._hasDefinedMatchers = true; - } else { - this._problemMatchers = []; - this._hasDefinedMatchers = false; - } - this._isBackground = false; - this._presentationOptions = Object.create(null); - this._runOptions = Object.create(null); - } - - get _id(): string | undefined { - return this.__id; + set isBackground(value: boolean) { + if (value !== true && value !== false) { + value = false; } + this.clear(); + this._isBackground = value; + } - set _id(value: string | undefined) { - this.__id = value; - } + get source(): string { + return this._source; + } - private clear(): void { - if (this.__id === undefined) { - return; - } - this.__id = undefined; - this._scope = undefined; - this.computeDefinitionBasedOnExecution(); + 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; + } - private computeDefinitionBasedOnExecution(): void { - if (this._execution instanceof ProcessExecution) { - this._definition = { - type: Task.ProcessType, - id: this._execution.computeId() - }; - } else if (this._execution instanceof ShellExecution) { - this._definition = { - type: Task.ShellType, - id: this._execution.computeId() - }; - } else { - this._definition = { - type: Task.EmptyType, - id: generateUuid() - }; - } - } + get group(): TaskGroup | undefined { + return this._group; + } - get definition(): vscode.TaskDefinition { - return this._definition; + set group(value: TaskGroup | undefined) { + if (value === null) { + value = undefined; } + this.clear(); + this._group = value; + } - 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 presentationOptions(): vscode.TaskPresentationOptions { + return this._presentationOptions; + } - get scope(): vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder | undefined { - return this._scope; + set presentationOptions(value: vscode.TaskPresentationOptions) { + if (value === null || value === undefined) { + value = Object.create(null); } + this.clear(); + this._presentationOptions = value; + } - set target(value: vscode.TaskScope.Global | vscode.TaskScope.Workspace | vscode.WorkspaceFolder) { - this.clear(); - this._scope = value; - } + get runOptions(): vscode.RunOptions { + return this._runOptions; + } - get name(): string { - return this._name; + set runOptions(value: vscode.RunOptions) { + if (value === null || value === undefined) { + value = Object.create(null); } + this.clear(); + this._runOptions = value; + } +} - set name(value: string) { - if (typeof value !== 'string') { - throw illegalArgument('name'); - } - this.clear(); - this._name = value; - } +export enum ProgressLocation { + SourceControl = 1, + Window = 10, + Notification = 15, +} - get execution(): ProcessExecution | ShellExecution | undefined { - return this._execution; - } +export enum TreeItemCollapsibleState { + None = 0, + Collapsed = 1, + Expanded = 2, +} - set execution(value: ProcessExecution | ShellExecution | undefined) { - if (value === null) { - value = undefined; - } - this.clear(); - this._execution = value; - let type = this._definition.type; - if (Task.EmptyType === type || Task.ProcessType === type || Task.ShellType === type) { - this.computeDefinitionBasedOnExecution(); - } - } +/** + * 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 problemMatchers(): string[] { - return this._problemMatchers; - } +export class TreeItem { + label?: string | vscode.TreeItemLabel; + id?: string; - set problemMatchers(value: string[]) { - if (!Array.isArray(value)) { - this.clear(); - this._problemMatchers = []; - this._hasDefinedMatchers = false; - return; - } else { - this.clear(); - this._problemMatchers = value; - this._hasDefinedMatchers = true; - } - } + resourceUri?: vscUri.URI; - get hasDefinedMatchers(): boolean { - return this._hasDefinedMatchers; - } + iconPath?: string | IconPath; - get isBackground(): boolean { - return this._isBackground; - } + command?: vscode.Command; - set isBackground(value: boolean) { - if (value !== true && value !== false) { - value = false; - } - this.clear(); - this._isBackground = value; - } + contextValue?: string; - get source(): string { - return this._source; - } + tooltip?: string; - set source(value: string) { - if (typeof value !== 'string' || value.length === 0) { - throw illegalArgument('source must be a string of length > 0'); - } - this.clear(); - this._source = value; - } + constructor(label: string, collapsibleState?: vscode.TreeItemCollapsibleState); - get group(): TaskGroup | undefined { - return this._group; - } + constructor(resourceUri: vscUri.URI, collapsibleState?: vscode.TreeItemCollapsibleState); - set group(value: TaskGroup | undefined) { - if (value === null) { - value = undefined; - } - this.clear(); - this._group = 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 presentationOptions(): vscode.TaskPresentationOptions { - return this._presentationOptions; - } +export class ThemeIcon { + static readonly File = new ThemeIcon('file'); - set presentationOptions(value: vscode.TaskPresentationOptions) { - if (value === null || value === undefined) { - value = Object.create(null); - } - this.clear(); - this._presentationOptions = value; - } + static readonly Folder = new ThemeIcon('folder'); - get runOptions(): vscode.RunOptions { - return this._runOptions; - } + readonly id: string; - set runOptions(value: vscode.RunOptions) { - if (value === null || value === undefined) { - value = Object.create(null); - } - this.clear(); - this._runOptions = 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; - // @ts-ignore - 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 (<any>Object).setPrototypeOf === 'function') { - (<any>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<SymbolTag>; - 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 (<IMarkdownString>thing).value === 'string' - && (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || (<IMarkdownString>thing).isTrusted === void 0); - } - return false; +export function isMarkdownString(thing: unknown): thing is IMarkdownString { + if (thing instanceof MarkdownString) { + return true; + } + if (thing && typeof thing === 'object') { + return ( + typeof (<IMarkdownString>thing).value === 'string' && + (typeof (<IMarkdownString>thing).isTrusted === 'boolean' || + (<IMarkdownString>thing).isTrusted === undefined) + ); } + return false; +} - export function markedStringsEquals(a: IMarkdownString | IMarkdownString[], b: IMarkdownString | IMarkdownString[]): boolean { - if (!a && !b) { - return true; - } else if (!a || !b) { - return false; - } else if (Array.isArray(a) && Array.isArray(b)) { - return vscMockArrays.equals(a, b, markdownStringEqual); - } else if (isMarkdownString(a) && isMarkdownString(b)) { - return markdownStringEqual(a, b); - } else { - return false; - } +export function markedStringsEquals( + a: IMarkdownString | IMarkdownString[], + b: IMarkdownString | IMarkdownString[], +): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + if (Array.isArray(a) && Array.isArray(b)) { + return vscMockArrays.equals(a, b, markdownStringEqual); + } + if (isMarkdownString(a) && isMarkdownString(b)) { + return markdownStringEqual(a, b); } + return false; +} - function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { - if (a === b) { - return true; - } else if (!a || !b) { - return false; - } else { - return a.value === b.value && a.isTrusted === b.isTrusted; - } +function markdownStringEqual(a: IMarkdownString, b: IMarkdownString): boolean { + if (a === b) { + return true; + } + if (!a || !b) { + return false; } + return a.value === b.value && a.isTrusted === b.isTrusted; +} - export function removeMarkdownEscapes(text: string): string { - if (!text) { - return text; - } - return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); +export function removeMarkdownEscapes(text: string): string { + if (!text) { + return text; } + return text.replace(/\\([\\`*_{}[\]()#+\-.!])/g, '$1'); } diff --git a/src/test/mocks/vsc/index.ts b/src/test/mocks/vsc/index.ts index d9586c4a4d35..152beb64cdf4 100644 --- a/src/test/mocks/vsc/index.ts +++ b/src/test/mocks/vsc/index.ts @@ -1,210 +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 { +const escapeCodiconsRegex = /(\\)?\$\([a-z0-9\-]+?(?:~[a-z0-9\-]*?)?\)/gi; +export function escapeCodicons(text: string): string { + return text.replace(escapeCodiconsRegex, (match, escaped) => (escaped ? match : `\\${match}`)); +} - private static _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - private static _empty = ''; +export class ThemeIcon { + static readonly File: ThemeIcon; - 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: string) { + static readonly Folder: ThemeIcon; - } - public static file(path: string): Uri { - return new Uri('file', '', path, '', '', path); - } - public static parse(value: string): Uri { - const match = this._regexp.exec(value); - if (!match) { - return new Uri('', '', '', '', '', ''); + 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 = []; } - return new Uri( - match[2] || this._empty, - decodeURIComponent(match[4] || this._empty), - decodeURIComponent(match[5] || this._empty), - decodeURIComponent(match[7] || this._empty), - decodeURIComponent(match[9] || this._empty), - decodeURIComponent(match[5] || this._empty)); - } - 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 Disposable { - constructor(private callOnDispose: Function) { - } - public dispose(): any { - if (this.callOnDispose) { - this.callOnDispose(); - } + private _callOnDispose: (() => void) | undefined; + + constructor(callOnDispose: () => void) { + this._callOnDispose = callOnDispose; + } + + dispose(): void { + if (typeof this._callOnDispose === 'function') { + this._callOnDispose(); + this._callOnDispose = undefined; } } +} - export class EventEmitter<T> implements vscode.EventEmitter<T> { +// eslint-disable-next-line @typescript-eslint/no-namespace +export namespace l10n { + export function t(message: string, ...args: unknown[]): string; + export function t(options: { + message: string; + args?: Array<string | number | boolean> | Record<string, unknown>; + comment: string | string[]; + }): string; - public event: vscode.Event<T>; - public emitter: NodeEventEmitter; - constructor() { - // @ts-ignore - this.event = this.add.bind(this); - this.emitter = new NodeEventEmitter(); - } - public fire(data?: T): void { - this.emitter.emit('evt', data); - } - public dispose(): void { - this.emitter.removeAllListeners(); + export function t( + message: + | string + | { + message: string; + args?: Array<string | number | boolean> | Record<string, unknown>; + comment: string | string[]; + }, + ...args: unknown[] + ): string { + let _message = message; + let _args: unknown[] | Record<string, unknown> | undefined = args; + if (typeof message !== 'string') { + _message = message.message; + _args = message.args ?? args; } - protected add = (listener: (e: T) => any, _thisArgs?: any, _disposables?: Disposable[]): Disposable => { - const bound = _thisArgs ? listener.bind(_thisArgs) : listener; - this.emitter.addListener('evt', bound); - return { - dispose: () => { - this.emitter.removeListener('evt', bound); - } - } as any as Disposable; + if ((_args as Array<string>).length > 0) { + return (_message as string).replace(/{(\d+)}/g, (match, number) => + (_args as Array<string>)[number] === undefined ? match : (_args as Array<string>)[number], + ); } + return _message as string; } + export const bundle: { [key: string]: string } | undefined = undefined; + export const uri: vscode.Uri | undefined = undefined; +} - export class CancellationToken extends EventEmitter<any> implements vscode.CancellationToken { - public isCancellationRequested!: boolean; - public onCancellationRequested: vscode.Event<any>; - constructor() { - super(); - // @ts-ignore - this.onCancellationRequested = this.add.bind(this); - } - public cancel() { - this.isCancellationRequested = true; - this.fire(); - } +export class EventEmitter<T> implements vscode.EventEmitter<T> { + public event: vscode.Event<T>; + + public emitter: NodeEventEmitter; + + constructor() { + this.event = (this.add.bind(this) as unknown) as vscode.Event<T>; + this.emitter = new NodeEventEmitter(); } - export class CancellationTokenSource { - public token: CancellationToken; - constructor() { - this.token = new CancellationToken(); - } - public cancel(): void { - this.token.cancel(); - } - public dispose(): void { - this.token.dispose(); - } + public fire(data?: T): void { + this.emitter.emit('evt', data); + } + + public dispose(): void { + this.emitter.removeAllListeners(); + } + + protected add = ( + listener: (e: T) => void, + _thisArgs?: EventEmitter<T>, + _disposables?: Disposable[], + ): Disposable => { + const bound = _thisArgs ? listener.bind(_thisArgs) : listener; + this.emitter.addListener('evt', bound); + return { + dispose: () => { + this.emitter.removeListener('evt', bound); + }, + } as Disposable; + }; +} + +export class CancellationToken<T> extends EventEmitter<T> implements vscode.CancellationToken { + public isCancellationRequested!: boolean; + + public onCancellationRequested: vscode.Event<T>; + + constructor() { + super(); + this.onCancellationRequested = this.add.bind(this) as vscode.Event<T>; + } + + public cancel(): void { + this.isCancellationRequested = true; + this.fire(); + } +} + +export class CancellationTokenSource { + public token: CancellationToken<unknown>; + + constructor() { + this.token = new CancellationToken(); + } + + public cancel(): void { + this.token.cancel(); + } + + public dispose(): void { + this.token.dispose(); + } +} + +export class CodeAction { + public title: string; + + public edit?: vscode.WorkspaceEdit; + + public diagnostics?: vscode.Diagnostic[]; + + public command?: vscode.Command; + + public kind?: CodeActionKind; + + public isPreferred?: boolean; + + constructor(_title: string, _kind?: CodeActionKind) { + this.title = _title; + this.kind = _kind; + } +} + +export enum CompletionItemKind { + Text = 0, + Method = 1, + Function = 2, + Constructor = 3, + Field = 4, + Variable = 5, + Class = 6, + Interface = 7, + Module = 8, + Property = 9, + Unit = 10, + Value = 11, + Enum = 12, + Keyword = 13, + Snippet = 14, + Color = 15, + Reference = 17, + File = 16, + Folder = 18, + EnumMember = 19, + Constant = 20, + Struct = 21, + Event = 22, + Operator = 23, + TypeParameter = 24, + User = 25, + Issue = 26, +} +export enum SymbolKind { + File = 0, + Module = 1, + Namespace = 2, + Package = 3, + Class = 4, + Method = 5, + Property = 6, + Field = 7, + Constructor = 8, + Enum = 9, + Interface = 10, + Function = 11, + Variable = 12, + Constant = 13, + String = 14, + Number = 15, + Boolean = 16, + Array = 17, + Object = 18, + Key = 19, + Null = 20, + EnumMember = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25, +} +export enum IndentAction { + None = 0, + Indent = 1, + IndentOutdent = 2, + Outdent = 3, +} + +export enum CompletionTriggerKind { + Invoke = 0, + TriggerCharacter = 1, + TriggerForIncompleteCompletions = 2, +} + +export class MarkdownString { + public value: string; + + public isTrusted?: boolean; + + public readonly supportThemeIcons?: boolean; + + constructor(value?: string, supportThemeIcons = false) { + this.value = value ?? ''; + this.supportThemeIcons = supportThemeIcons; } - export 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 - } - - export class CodeActionKind { - public static readonly Empty: CodeActionKind = new CodeActionKind('empty'); - public static readonly QuickFix: CodeActionKind = new CodeActionKind('quick.fix'); - - public static readonly Refactor: CodeActionKind = new CodeActionKind('refactor'); - - public static readonly RefactorExtract: CodeActionKind = new CodeActionKind('refactor.extract'); - - public static readonly RefactorInline: CodeActionKind = new CodeActionKind('refactor.inline'); - - public static readonly RefactorRewrite: CodeActionKind = new CodeActionKind('refactor.rewrite'); - public static readonly Source: CodeActionKind = new CodeActionKind('source'); - public static readonly SourceOrganizeImports: CodeActionKind = new CodeActionKind('source.organize.imports'); - public static readonly SourceFixAll: CodeActionKind = new CodeActionKind('source.fix.all'); - - private constructor(private _value: string) { + public static isMarkdownString(thing?: string | MarkdownString | unknown): thing is vscode.MarkdownString { + if (thing instanceof MarkdownString) { + return true; } + return ( + thing !== undefined && + typeof thing === 'object' && + thing !== null && + thing.hasOwnProperty('appendCodeblock') && + thing.hasOwnProperty('appendMarkdown') && + thing.hasOwnProperty('appendText') && + thing.hasOwnProperty('value') + ); + } + + public appendText(value: string): MarkdownString { + // escape markdown syntax tokens: http://daringfireball.net/projects/markdown/syntax#backslash + this.value += (this.supportThemeIcons ? escapeCodicons(value) : value) + .replace(/[\\`*_{}[\]()#+\-.!]/g, '\\$&') + .replace(/\n/g, '\n\n'); + + return this; + } + + 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; + } +} - public append(parts: string): CodeActionKind { - return new CodeActionKind(`${this._value}.${parts}`); +export class Hover { + public contents: vscode.MarkdownString[] | vscode.MarkedString[]; + + public range: vscode.Range | undefined; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | vscode.MarkdownString[] | vscode.MarkedString[], + range?: vscode.Range, + ) { + if (!contents) { + throw new Error('Illegal argument, contents must be defined'); } - public intersects(other: CodeActionKind): boolean { - return this._value.includes(other._value) || other._value.includes(this._value); + if (Array.isArray(contents)) { + this.contents = <vscode.MarkdownString[] | vscode.MarkedString[]>contents; + } else if (MarkdownString.isMarkdownString(contents)) { + this.contents = [contents]; + } else { + this.contents = [contents]; } + this.range = range; + } +} - public contains(other: CodeActionKind): boolean { - return this._value.startsWith(other._value); - } +export class CodeActionKind { + public static readonly Empty: CodeActionKind = new CodeActionKind('empty'); - public get value(): string { - return this._value; - } + public static readonly QuickFix: CodeActionKind = new CodeActionKind('quick.fix'); + + public static readonly Refactor: CodeActionKind = new CodeActionKind('refactor'); + + public static readonly RefactorExtract: CodeActionKind = new CodeActionKind('refactor.extract'); + + public static readonly RefactorInline: CodeActionKind = new CodeActionKind('refactor.inline'); + + public static readonly RefactorMove: CodeActionKind = new CodeActionKind('refactor.move'); + + public static readonly RefactorRewrite: CodeActionKind = new CodeActionKind('refactor.rewrite'); + + public static readonly Source: CodeActionKind = new CodeActionKind('source'); + + public static readonly SourceOrganizeImports: CodeActionKind = new CodeActionKind('source.organize.imports'); + + public static readonly SourceFixAll: CodeActionKind = new CodeActionKind('source.fix.all'); + + public static readonly Notebook: CodeActionKind = new CodeActionKind('notebook'); + + private constructor(private _value: string) {} + + public append(parts: string): CodeActionKind { + return new CodeActionKind(`${this._value}.${parts}`); } + 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; + } + + /** + * 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 17e9e1e315f2..538e9ec7b9d2 100644 --- a/src/test/mocks/vsc/range.ts +++ b/src/test/mocks/vsc/range.ts @@ -1,387 +1,397 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. 'use strict'; -// tslint:disable:all -import { vscMockPosition } from './position'; - -export namespace vscMockRange { - /** - * A range in the editor. This interface is suitable for serialization. - */ - export interface IRange { - /** - * Line number on which the range starts (starts at 1). - */ - readonly startLineNumber: number; - /** - * Column on which the range starts in line `startLineNumber` (starts at 1). - */ - readonly startColumn: number; - /** - * Line number on which the range ends. - */ - readonly endLineNumber: number; - /** - * Column on which the range ends in line `endLineNumber`. - */ - readonly endColumn: number; - } +import * as vscMockPosition from './position'; + +/** + * A range in the editor. This interface is suitable for serialization. + */ +export interface IRange { /** - * A range in the editor. (startLineNumber,startColumn) is <= (endLineNumber,endColumn) - */ - export class Range { - - /** - * Line number on which the range starts (starts at 1). - */ - public readonly startLineNumber: number; - /** - * Column on which the range starts in line `startLineNumber` (starts at 1). - */ - public readonly startColumn: number; - /** - * Line number on which the range ends. - */ - public readonly endLineNumber: number; - /** - * Column on which the range ends in line `endLineNumber`. - */ - public readonly endColumn: number; - - constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { - if ((startLineNumber > endLineNumber) || (startLineNumber === endLineNumber && startColumn > endColumn)) { - this.startLineNumber = endLineNumber; - this.startColumn = endColumn; - this.endLineNumber = startLineNumber; - this.endColumn = startColumn; - } else { - this.startLineNumber = startLineNumber; - this.startColumn = startColumn; - this.endLineNumber = endLineNumber; - this.endColumn = endColumn; - } - } + * Line number on which the range starts (starts at 1). + */ + readonly startLineNumber: number; + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + readonly startColumn: number; + /** + * Line number on which the range ends. + */ + readonly endLineNumber: number; + /** + * Column on which the range ends in line `endLineNumber`. + */ + readonly endColumn: number; +} - /** - * Test if this range is empty. - */ - public isEmpty(): boolean { - return Range.isEmpty(this); - } +/** + * A range in the editor. (startLineNumber,startColumn) is <= (endLineNumber,endColumn) + */ +export class Range { + /** + * Line number on which the range starts (starts at 1). + */ + public readonly startLineNumber: number; - /** - * Test if `range` is empty. - */ - public static isEmpty(range: IRange): boolean { - return (range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn); - } + /** + * Column on which the range starts in line `startLineNumber` (starts at 1). + */ + public readonly startColumn: number; - /** - * Test if position is in this range. If the position is at the edges, will return true. - */ - public containsPosition(position: vscMockPosition.IPosition): boolean { - return Range.containsPosition(this, position); - } + /** + * Line number on which the range ends. + */ + public readonly endLineNumber: number; - /** - * Test if `position` is in `range`. If the position is at the edges, will return true. - */ - public static containsPosition(range: IRange, position: vscMockPosition.IPosition): boolean { - if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { - return false; - } - if (position.lineNumber === range.startLineNumber && position.column < range.startColumn) { - return false; - } - if (position.lineNumber === range.endLineNumber && position.column > range.endColumn) { - return false; - } - return true; + /** + * Column on which the range ends in line `endLineNumber`. + */ + public readonly endColumn: number; + + constructor(startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number) { + if (startLineNumber > endLineNumber || (startLineNumber === endLineNumber && startColumn > endColumn)) { + this.startLineNumber = endLineNumber; + this.startColumn = endColumn; + this.endLineNumber = startLineNumber; + this.endColumn = startColumn; + } else { + this.startLineNumber = startLineNumber; + this.startColumn = startColumn; + this.endLineNumber = endLineNumber; + this.endColumn = endColumn; } + } - /** - * Test if range is in this range. If the range is equal to this range, will return true. - */ - public containsRange(range: IRange): boolean { - return Range.containsRange(this, range); - } + /** + * Test if this range is empty. + */ + public isEmpty(): boolean { + return Range.isEmpty(this); + } - /** - * Test if `otherRange` is in `range`. If the ranges are equal, will return true. - */ - public static containsRange(range: IRange, otherRange: IRange): boolean { - if (otherRange.startLineNumber < range.startLineNumber || otherRange.endLineNumber < range.startLineNumber) { - return false; - } - if (otherRange.startLineNumber > range.endLineNumber || otherRange.endLineNumber > range.endLineNumber) { - return false; - } - if (otherRange.startLineNumber === range.startLineNumber && otherRange.startColumn < range.startColumn) { - return false; - } - if (otherRange.endLineNumber === range.endLineNumber && otherRange.endColumn > range.endColumn) { - return false; - } - return true; - } + /** + * Test if `range` is empty. + */ + public static isEmpty(range: IRange): boolean { + return range.startLineNumber === range.endLineNumber && range.startColumn === range.endColumn; + } - /** - * A reunion of the two ranges. - * The smallest position will be used as the start point, and the largest one as the end point. - */ - public plusRange(range: IRange): Range { - return Range.plusRange(this, range); + /** + * Test if position is in this range. If the position is at the edges, will return true. + */ + public containsPosition(position: vscMockPosition.IPosition): boolean { + return Range.containsPosition(this, position); + } + + /** + * Test if `position` is in `range`. If the position is at the edges, will return true. + */ + public static containsPosition(range: IRange, position: vscMockPosition.IPosition): boolean { + if (position.lineNumber < range.startLineNumber || position.lineNumber > range.endLineNumber) { + return false; + } + if (position.lineNumber === range.startLineNumber && position.column < range.startColumn) { + return false; + } + if (position.lineNumber === range.endLineNumber && position.column > range.endColumn) { + return false; } + return true; + } - /** - * A reunion of the two ranges. - * The smallest position will be used as the start point, and the largest one as the end point. - */ - public static plusRange(a: IRange, b: IRange): Range { - var startLineNumber: number, startColumn: number, endLineNumber: number, endColumn: number; - if (b.startLineNumber < a.startLineNumber) { - startLineNumber = b.startLineNumber; - startColumn = b.startColumn; - } else if (b.startLineNumber === a.startLineNumber) { - startLineNumber = b.startLineNumber; - startColumn = Math.min(b.startColumn, a.startColumn); - } else { - startLineNumber = a.startLineNumber; - startColumn = a.startColumn; - } + /** + * Test if range is in this range. If the range is equal to this range, will return true. + */ + public containsRange(range: IRange): boolean { + return Range.containsRange(this, range); + } - if (b.endLineNumber > a.endLineNumber) { - endLineNumber = b.endLineNumber; - endColumn = b.endColumn; - } else if (b.endLineNumber === a.endLineNumber) { - endLineNumber = b.endLineNumber; - endColumn = Math.max(b.endColumn, a.endColumn); - } else { - endLineNumber = a.endLineNumber; - endColumn = a.endColumn; - } + /** + * Test if `otherRange` is in `range`. If the ranges are equal, will return true. + */ + public static containsRange(range: IRange, otherRange: IRange): boolean { + if (otherRange.startLineNumber < range.startLineNumber || otherRange.endLineNumber < range.startLineNumber) { + return false; + } + if (otherRange.startLineNumber > range.endLineNumber || otherRange.endLineNumber > range.endLineNumber) { + return false; + } + if (otherRange.startLineNumber === range.startLineNumber && otherRange.startColumn < range.startColumn) { + return false; + } + if (otherRange.endLineNumber === range.endLineNumber && otherRange.endColumn > range.endColumn) { + return false; + } + return true; + } - return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public plusRange(range: IRange): Range { + return Range.plusRange(this, range); + } + + /** + * A reunion of the two ranges. + * The smallest position will be used as the start point, and the largest one as the end point. + */ + public static plusRange(a: IRange, b: IRange): Range { + let startLineNumber: number; + let startColumn: number; + let endLineNumber: number; + let endColumn: number; + if (b.startLineNumber < a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = b.startColumn; + } else if (b.startLineNumber === a.startLineNumber) { + startLineNumber = b.startLineNumber; + startColumn = Math.min(b.startColumn, a.startColumn); + } else { + startLineNumber = a.startLineNumber; + startColumn = a.startColumn; } - /** - * A intersection of the two ranges. - */ - public intersectRanges(range: IRange): Range { - return Range.intersectRanges(this, range); + if (b.endLineNumber > a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = b.endColumn; + } else if (b.endLineNumber === a.endLineNumber) { + endLineNumber = b.endLineNumber; + endColumn = Math.max(b.endColumn, a.endColumn); + } else { + endLineNumber = a.endLineNumber; + endColumn = a.endColumn; } - /** - * A intersection of the two ranges. - */ - public static intersectRanges(a: IRange, b: IRange): Range { - var resultStartLineNumber = a.startLineNumber, - resultStartColumn = a.startColumn, - resultEndLineNumber = a.endLineNumber, - resultEndColumn = a.endColumn, - otherStartLineNumber = b.startLineNumber, - otherStartColumn = b.startColumn, - otherEndLineNumber = b.endLineNumber, - otherEndColumn = b.endColumn; - - if (resultStartLineNumber < otherStartLineNumber) { - resultStartLineNumber = otherStartLineNumber; - resultStartColumn = otherStartColumn; - } else if (resultStartLineNumber === otherStartLineNumber) { - resultStartColumn = Math.max(resultStartColumn, otherStartColumn); - } + return new Range(startLineNumber, startColumn, endLineNumber, endColumn); + } - if (resultEndLineNumber > otherEndLineNumber) { - resultEndLineNumber = otherEndLineNumber; - resultEndColumn = otherEndColumn; - } else if (resultEndLineNumber === otherEndLineNumber) { - resultEndColumn = Math.min(resultEndColumn, otherEndColumn); - } + /** + * A intersection of the two ranges. + */ + public intersectRanges(range: IRange): Range | null { + return Range.intersectRanges(this, range); + } - // Check if selection is now empty - if (resultStartLineNumber > resultEndLineNumber) { - // @ts-ignore - return null; - } - if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { - // @ts-ignore - return null; - } - return new Range(resultStartLineNumber, resultStartColumn, resultEndLineNumber, resultEndColumn); + /** + * A intersection of the two ranges. + */ + public static intersectRanges(a: IRange, b: IRange): Range | null { + let resultStartLineNumber = a.startLineNumber; + let resultStartColumn = a.startColumn; + let resultEndLineNumber = a.endLineNumber; + let resultEndColumn = a.endColumn; + const otherStartLineNumber = b.startLineNumber; + const otherStartColumn = b.startColumn; + const otherEndLineNumber = b.endLineNumber; + const otherEndColumn = b.endColumn; + + if (resultStartLineNumber < otherStartLineNumber) { + resultStartLineNumber = otherStartLineNumber; + resultStartColumn = otherStartColumn; + } else if (resultStartLineNumber === otherStartLineNumber) { + resultStartColumn = Math.max(resultStartColumn, otherStartColumn); } - /** - * Test if this range equals other. - */ - public equalsRange(other: IRange): boolean { - return Range.equalsRange(this, other); + if (resultEndLineNumber > otherEndLineNumber) { + resultEndLineNumber = otherEndLineNumber; + resultEndColumn = otherEndColumn; + } else if (resultEndLineNumber === otherEndLineNumber) { + resultEndColumn = Math.min(resultEndColumn, otherEndColumn); } - /** - * Test if range `a` equals `b`. - */ - public static equalsRange(a: IRange, b: IRange): boolean { - return ( - !!a && - !!b && - a.startLineNumber === b.startLineNumber && - a.startColumn === b.startColumn && - a.endLineNumber === b.endLineNumber && - a.endColumn === b.endColumn - ); + // Check if selection is now empty + if (resultStartLineNumber > resultEndLineNumber) { + return null; } - - /** - * Return the end position (which will be after or equal to the start position) - */ - public getEndPosition(): vscMockPosition.Position { - return new vscMockPosition.Position(this.endLineNumber, this.endColumn); + if (resultStartLineNumber === resultEndLineNumber && resultStartColumn > resultEndColumn) { + return null; } - /** - * Return the start position (which will be before or equal to the end position) - */ - public getStartPosition(): vscMockPosition.Position { - return new vscMockPosition.Position(this.startLineNumber, this.startColumn); - } + return new Range(resultStartLineNumber, resultStartColumn, resultEndLineNumber, resultEndColumn); + } - /** - * Transform to a user presentable string representation. - */ - public toString(): string { - return '[' + this.startLineNumber + ',' + this.startColumn + ' -> ' + this.endLineNumber + ',' + this.endColumn + ']'; - } + /** + * Test if this range equals other. + */ + public equalsRange(other: IRange): boolean { + return Range.equalsRange(this, other); + } - /** - * Create a new range using this range's start position, and using endLineNumber and endColumn as the end position. - */ - public setEndPosition(endLineNumber: number, endColumn: number): Range { - return new Range(this.startLineNumber, this.startColumn, endLineNumber, endColumn); - } + /** + * Test if range `a` equals `b`. + */ + public static equalsRange(a: IRange, b: IRange): boolean { + return ( + !!a && + !!b && + a.startLineNumber === b.startLineNumber && + a.startColumn === b.startColumn && + a.endLineNumber === b.endLineNumber && + a.endColumn === b.endColumn + ); + } - /** - * Create a new range using this range's end position, and using startLineNumber and startColumn as the start position. - */ - public setStartPosition(startLineNumber: number, startColumn: number): Range { - return new Range(startLineNumber, startColumn, this.endLineNumber, this.endColumn); - } + /** + * Return the end position (which will be after or equal to the start position) + */ + public getEndPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.endLineNumber, this.endColumn); + } - /** - * Create a new empty range using this range's start position. - */ - public collapseToStart(): Range { - return Range.collapseToStart(this); - } + /** + * Return the start position (which will be before or equal to the end position) + */ + public getStartPosition(): vscMockPosition.Position { + return new vscMockPosition.Position(this.startLineNumber, this.startColumn); + } - /** - * Create a new empty range using this range's start position. - */ - public static collapseToStart(range: IRange): Range { - return new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn); - } + /** + * Transform to a user presentable string representation. + */ + public toString(): string { + return `[${this.startLineNumber},${this.startColumn} -> ${this.endLineNumber},${this.endColumn}]`; + } - // --- + /** + * Create a new range using this range's start position, and using endLineNumber and endColumn as the end position. + */ + public setEndPosition(endLineNumber: number, endColumn: number): Range { + return new Range(this.startLineNumber, this.startColumn, endLineNumber, endColumn); + } - public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Range { - return new Range(start.lineNumber, start.column, end.lineNumber, end.column); - } + /** + * Create a new range using this range's end position, and using startLineNumber and startColumn as the start position. + */ + public setStartPosition(startLineNumber: number, startColumn: number): Range { + return new Range(startLineNumber, startColumn, this.endLineNumber, this.endColumn); + } - /** - * Create a `Range` from an `IRange`. - */ - public static lift(range: IRange): Range { - if (!range) { - // @ts-ignore - return null; - } - return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); - } + /** + * Create a new empty range using this range's start position. + */ + public collapseToStart(): Range { + return Range.collapseToStart(this); + } + + /** + * Create a new empty range using this range's start position. + */ + public static collapseToStart(range: IRange): Range { + return new Range(range.startLineNumber, range.startColumn, range.startLineNumber, range.startColumn); + } + + // --- - /** - * Test if `obj` is an `IRange`. - */ - public static isIRange(obj: any): obj is IRange { - return ( - obj - && (typeof obj.startLineNumber === 'number') - && (typeof obj.startColumn === 'number') - && (typeof obj.endLineNumber === 'number') - && (typeof obj.endColumn === 'number') - ); + public static fromPositions(start: vscMockPosition.IPosition, end: vscMockPosition.IPosition = start): Range { + return new Range(start.lineNumber, start.column, end.lineNumber, end.column); + } + + /** + * Create a `Range` from an `IRange`. + */ + public static lift(range: IRange): Range | null { + if (!range) { + return null; } + return new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn); + } - /** - * Test if the two ranges are touching in any way. - */ - public static areIntersectingOrTouching(a: IRange, b: IRange): boolean { - // Check if `a` is before `b` - if (a.endLineNumber < b.startLineNumber || (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn)) { - return false; - } + /** + * Test if `obj` is an `IRange`. + */ + public static isIRange(obj?: { + startLineNumber: unknown; + startColumn: unknown; + endLineNumber: unknown; + endColumn: unknown; + }): obj is IRange { + return ( + obj !== undefined && + typeof obj.startLineNumber === 'number' && + typeof obj.startColumn === 'number' && + typeof obj.endLineNumber === 'number' && + typeof obj.endColumn === 'number' + ); + } - // Check if `b` is before `a` - if (b.endLineNumber < a.startLineNumber || (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn)) { - return false; - } + /** + * Test if the two ranges are touching in any way. + */ + public static areIntersectingOrTouching(a: IRange, b: IRange): boolean { + // Check if `a` is before `b` + if ( + a.endLineNumber < b.startLineNumber || + (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) + ) { + return false; + } - // These ranges must intersect - return true; + // Check if `b` is before `a` + if ( + b.endLineNumber < a.startLineNumber || + (b.endLineNumber === a.startLineNumber && b.endColumn < a.startColumn) + ) { + return false; } - /** - * A function that compares ranges, useful for sorting ranges - * It will first compare ranges on the startPosition and then on the endPosition - */ - public static compareRangesUsingStarts(a: IRange, b: IRange): number { - let aStartLineNumber = a.startLineNumber | 0; - let bStartLineNumber = b.startLineNumber | 0; - - if (aStartLineNumber === bStartLineNumber) { - let aStartColumn = a.startColumn | 0; - let bStartColumn = b.startColumn | 0; - - if (aStartColumn === bStartColumn) { - let aEndLineNumber = a.endLineNumber | 0; - let bEndLineNumber = b.endLineNumber | 0; - - if (aEndLineNumber === bEndLineNumber) { - let aEndColumn = a.endColumn | 0; - let bEndColumn = b.endColumn | 0; - return aEndColumn - bEndColumn; - } - return aEndLineNumber - bEndLineNumber; + // These ranges must intersect + return true; + } + + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the startPosition and then on the endPosition + */ + public static compareRangesUsingStarts(a: IRange, b: IRange): number { + const aStartLineNumber = a.startLineNumber | 0; + const bStartLineNumber = b.startLineNumber | 0; + + if (aStartLineNumber === bStartLineNumber) { + const aStartColumn = a.startColumn | 0; + const bStartColumn = b.startColumn | 0; + + if (aStartColumn === bStartColumn) { + const aEndLineNumber = a.endLineNumber | 0; + const bEndLineNumber = b.endLineNumber | 0; + + if (aEndLineNumber === bEndLineNumber) { + const aEndColumn = a.endColumn | 0; + const bEndColumn = b.endColumn | 0; + return aEndColumn - bEndColumn; } - return aStartColumn - bStartColumn; + return aEndLineNumber - bEndLineNumber; } - return aStartLineNumber - bStartLineNumber; + return aStartColumn - bStartColumn; } + return aStartLineNumber - bStartLineNumber; + } - /** - * A function that compares ranges, useful for sorting ranges - * It will first compare ranges on the endPosition and then on the startPosition - */ - public static compareRangesUsingEnds(a: IRange, b: IRange): number { - if (a.endLineNumber === b.endLineNumber) { - if (a.endColumn === b.endColumn) { - if (a.startLineNumber === b.startLineNumber) { - return a.startColumn - b.startColumn; - } - return a.startLineNumber - b.startLineNumber; + /** + * A function that compares ranges, useful for sorting ranges + * It will first compare ranges on the endPosition and then on the startPosition + */ + public static compareRangesUsingEnds(a: IRange, b: IRange): number { + if (a.endLineNumber === b.endLineNumber) { + if (a.endColumn === b.endColumn) { + if (a.startLineNumber === b.startLineNumber) { + return a.startColumn - b.startColumn; } - return a.endColumn - b.endColumn; + return a.startLineNumber - b.startLineNumber; } - return a.endLineNumber - b.endLineNumber; + return a.endColumn - b.endColumn; } + return a.endLineNumber - b.endLineNumber; + } - /** - * Test if the range spans multiple lines. - */ - public static spansMultipleLines(range: IRange): boolean { - return range.endLineNumber > range.startLineNumber; - } + /** + * Test if the range spans multiple lines. + */ + public static spansMultipleLines(range: IRange): boolean { + return range.endLineNumber > range.startLineNumber; } } diff --git a/src/test/mocks/vsc/selection.ts b/src/test/mocks/vsc/selection.ts index 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 0e7613d2bae5..671c60c1ba65 100644 --- a/src/test/mocks/vsc/uri.ts +++ b/src/test/mocks/vsc/uri.ts @@ -1,476 +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 (<URI>thing).authority === 'string' && + typeof (<URI>thing).fragment === 'string' && + typeof (<URI>thing).path === 'string' && + typeof (<URI>thing).query === 'string' && + typeof (<URI>thing).scheme === 'string' && + typeof (<URI>thing).fsPath === 'function' && + typeof (<URI>thing).with === 'function' && + typeof (<URI>thing).toString === 'function' + ); } - const _empty = ''; - const _slash = '/'; - const _regexp = /^(([^:/?#]+?):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?/; - 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 (<URI>thing).authority === 'string' - && typeof (<URI>thing).fragment === 'string' - && typeof (<URI>thing).path === 'string' - && typeof (<URI>thing).query === 'string' - && typeof (<URI>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( - // @ts-ignore - 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 = <UriState>{ - $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 = (<UriState>data).fsPath; - result._formatted = (<UriState>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 = (<UriState>data).external; + result._fsPath = (<UriState>data)._sep === _pathSepMarker ? (<UriState>data).fsPath : null; + return result; } - export interface UriComponents { - scheme: string; - authority: string; - path: string; - query: string; - fragment: string; + static joinPath(uri: URI, ...pathFragment: string[]): URI { + if (!uri.path) { + throw new Error(`[UriError]: cannot call joinPaths on URI without path`); + } + let newPath: string; + if (isWindows && uri.scheme === 'file') { + newPath = URI.file(pathImport.join(uri.fsPath, ...pathFragment)).path; + } else { + newPath = pathImport.join(uri.path, ...pathFragment); + } + return uri.with({ path: newPath }); } +} + +export interface UriComponents { + scheme: string; + authority: string; + path: string; + query: string; + fragment: string; +} - interface UriState extends UriComponents { - $mid: number; - fsPath: string; - external: string; +interface UriState extends UriComponents { + $mid: number; + external: string; + fsPath: string; + _sep: 1 | undefined; +} + +class _URI extends URI { + _formatted: string | null = null; + + _fsPath: string | null = null; + + constructor( + schemeOrData: string | UriComponents, + authority?: string, + path?: string, + query?: string, + fragment?: string, + _strict = false, + ) { + super(schemeOrData as string, authority, path, query, fragment, _strict); + this._fsPath = this.fsPath; } + get fsPath(): string { + if (!this._fsPath) { + this._fsPath = _makeFsPath(this); + } + return this._fsPath; + } - // tslint:disable-next-line:class-name - class _URI extends URI { - // @ts-ignore - _formatted: string = null; - // @ts-ignore - _fsPath: string = null; + toString(skipEncoding = false): string { + if (!skipEncoding) { + if (!this._formatted) { + this._formatted = _asFormatted(this, false); + } + return this._formatted; + } + // we don't cache that + return _asFormatted(this, true); + } - get fsPath(): string { - if (!this._fsPath) { - this._fsPath = _makeFsPath(this); + toJSON(): UriComponents { + const res = <UriState>{ + $mid: 1, + }; + // cached state + if (this._fsPath) { + res.fsPath = this._fsPath; + if (_pathSepMarker) { + res._sep = _pathSepMarker; } - return this._fsPath; } + 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; + } +} - public toString(skipEncoding: boolean = false): string { - if (!skipEncoding) { - if (!this._formatted) { - this._formatted = _asFormatted(this, false); +// 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 { + // 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; } - return this._formatted; - } else { - // we don't cache that - return _asFormatted(this, true); + + // append escaped variant to result + res += escaped; + } else if (nativeEncodePos === -1) { + // use native encode only when needed + nativeEncodePos = pos; } } } + if (nativeEncodePos !== -1) { + res += encodeURIComponent(uriComponent.substring(nativeEncodePos)); + } - /** - * 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); - } else { - // other path - value = uri.path; - } - if (platform.isWindows) { - value = value.replace(/\//g, '\\'); + return res !== undefined ? res : uriComponent; +} + +function encodeURIComponentMinimal(path: string): string { + let res: string | undefined; + for (let pos = 0; pos < path.length; pos += 1) { + const code = path.charCodeAt(pos); + if (code === CharCode.Hash || code === CharCode.QuestionMark) { + if (res === undefined) { + res = path.substr(0, pos); + } + res += encodeTable[code]; + } else if (res !== undefined) { + res += path[pos]; } - return value; } + 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 - ? 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('@'); - } - authority = authority.toLowerCase(); - idx = authority.indexOf(':'); +/** + * Create the external version of a uri + */ +function _asFormatted(uri: URI, skipEncoding: boolean): string { + const encoder = !skipEncoding ? encodeURIComponentFast : encodeURIComponentMinimal; + + let res = ''; + let { authority, path } = uri; + const { scheme, query, fragment } = uri; + if (scheme) { + res += scheme; + res += ':'; + } + if (authority || scheme === 'file') { + res += _slash; + res += _slash; + } + if (authority) { + let idx = authority.indexOf('@'); + if (idx !== -1) { + // <user>@<auth> + const userinfo = authority.substr(0, idx); + authority = authority.substr(idx + 1); + idx = userinfo.indexOf(':'); if (idx === -1) { - parts.push(encoder(authority)); + res += encoder(userinfo, false); } else { - parts.push(encoder(authority.substr(0, idx)), authority.substr(idx)); + // <user>:<pass>@<auth> + 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 { + // <auth>:<port> + res += encoder(authority.substr(0, idx), false); + res += authority.substr(idx); + } + } + if (path) { + // lower-case windows drive letters in /C:/fff or C:/fff + if (path.length >= 3 && path.charCodeAt(0) === CharCode.Slash && path.charCodeAt(2) === CharCode.Colon) { + const code = path.charCodeAt(1); + if (code >= CharCode.A && code <= CharCode.Z) { + path = `/${String.fromCharCode(code + 32)}:${path.substr(3)}`; // "/c:".length === 3 } - - // 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 index 53b6afa3b41c..fd825440ab7d 100644 --- a/src/test/mocks/vsc/uuid.ts +++ b/src/test/mocks/vsc/uuid.ts @@ -1,13 +1,14 @@ +/* eslint-disable max-classes-per-file */ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. 'use strict'; + /** * Represents a UUID as defined by rfc4122. */ -// tslint:disable-next-line: interface-name -export interface UUID { +export interface UUID { /** * @returns the canonical representation in sets of hexadecimal numbers separated by dashes. */ @@ -15,7 +16,6 @@ export interface UUID { } class ValueUUID implements UUID { - constructor(public _value: string) { // empty } @@ -26,13 +26,11 @@ class ValueUUID implements UUID { } class V4UUID extends ValueUUID { - private static readonly _chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f']; private static readonly _timeHighBits = ['8', '9', 'a', 'b']; private static _oneOf(array: string[]): string { - // tslint:disable:insecure-random return array[Math.floor(array.length * Math.random())]; } @@ -40,46 +38,47 @@ class V4UUID extends ValueUUID { return V4UUID._oneOf(V4UUID._chars); } -// tslint:disable-next-line: member-ordering constructor() { - super([ - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - '-', - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - '-', - '4', - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - '-', - V4UUID._oneOf(V4UUID._timeHighBits), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - '-', - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex(), - V4UUID._randomHex() - ].join('')); + 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(''), + ); } } 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 4f0a9616d4b4..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: Record<string, {}> = { ACTIVATION_TIMES_LOG_FILE_PATH: logFile, ACTIVATION_TIMES_EXT_VERSION: version === Version.Release ? releaseVersion : devVersion, - CODE_EXTENSIONS_PATH: version === Version.Release ? publishedExtensionPath : EXTENSION_ROOT_DIR + CODE_EXTENSIONS_PATH: version === Version.Release ? publishedExtensionPath : EXTENSION_ROOT_DIR, }; await this.launchTest(env); } - private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], languageServerLogFiles: string[]) { + private async runPerfTest(devLogFiles: string[], releaseLogFiles: string[], languageServerLogFiles: string[]) { const env: Record<string, {}> = { ACTIVATION_TIMES_DEV_LOG_FILE_PATHS: JSON.stringify(devLogFiles), ACTIVATION_TIMES_RELEASE_LOG_FILE_PATHS: JSON.stringify(releaseLogFiles), - ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS: JSON.stringify(languageServerLogFiles) + ACTIVATION_TIMES_DEV_LANGUAGE_SERVER_LOG_FILE_PATHS: JSON.stringify(languageServerLogFiles), }; await this.launchTest(env); } - private async launchTest(customEnvVars: Record<string, {}>) { - await new Promise((resolve, reject) => { - const env: Record<string, {}> = { + private async launchTest(customEnvVars: Record<string, {}>) { + await new Promise<void>((resolve, reject) => { + const env: Record<string, string> = { TEST_FILES_SUFFIX: 'perf.test', CODE_TESTS_WORKSPACE: path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'performance'), ...process.env, - ...customEnvVars + ...customEnvVars, }; const proc = spawn('node', [path.join(__dirname, 'standardTest.js')], { cwd: EXTENSION_ROOT_DIR, env }); 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<string> { + private async getReleaseVersion(): Promise<string> { const url = `https://marketplace.visualstudio.com/items?itemName=${PVSC_EXTENSION_ID}`; - const content = await new Promise<string>((resolve, reject) => { - request(url, (error, response, body) => { - if (error) { - return reject(error); - } - if (response.statusCode === 200) { - return resolve(body); - } - reject(`Status code of ${response.statusCode} received.`); - }); - }); - const re = NamedRegexp('"version"\S?:\S?"(:<version>\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); + const request = bent.default('string', 'GET', 200); + + const content: string = await request(url); + const re = NamedRegexp('"version"S?:S?"(:<version>\\d{4}\\.\\d{1,2}\\.\\d{1,2})"', 'g'); const matches = re.exec(content); return matches.groups().version; } - private async getDevVersion(): Promise<string> { - // tslint:disable-next-line:non-literal-require + private async getDevVersion(): Promise<string> { 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<ProcResult> { + if (this.result) { + return this.result; + } + while (this.raw.exitCode === null) { + await sleep(10); // milliseconds + } + this.result = { + exitCode: this.raw.exitCode, + stdout: this.output.stdout, + }; + return this.result; + } +} + +export function spawn(executable: string, ...args: string[]) { + // Un-comment this to see the executed command: + //console.log(`|${executable} ${args.join(' ')}|`); + const output = new ProcOutput(); + const raw = cp.spawn(executable, args); + raw.stdout.on('data', (data: Buffer) => output.addStdout(data)); + raw.stderr.on('data', (data: Buffer) => output.addStderr(data)); + return new Proc(raw, output); +} diff --git a/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts new file mode 100644 index 000000000000..136271b3e4e5 --- /dev/null +++ b/src/test/providers/codeActionProvider/launchJsonCodeActionProvider.unit.test.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { assert, expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { CodeActionContext, CodeActionKind, Diagnostic, Range, TextDocument, Uri } from 'vscode'; +import { LaunchJsonCodeActionProvider } from '../../../client/providers/codeActionProvider/launchJsonCodeActionProvider'; + +suite('LaunchJson CodeAction Provider', () => { + const documentUri = Uri.parse('a'); + let document: TypeMoq.IMock<TextDocument>; + let range: TypeMoq.IMock<Range>; + let context: TypeMoq.IMock<CodeActionContext>; + let diagnostic: TypeMoq.IMock<Diagnostic>; + let codeActionsProvider: LaunchJsonCodeActionProvider; + + setup(() => { + codeActionsProvider = new LaunchJsonCodeActionProvider(); + document = TypeMoq.Mock.ofType<TextDocument>(); + range = TypeMoq.Mock.ofType<Range>(); + context = TypeMoq.Mock.ofType<CodeActionContext>(); + diagnostic = TypeMoq.Mock.ofType<Diagnostic>(); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'Diagnostic text'); + document.setup((d) => d.uri).returns(() => documentUri); + context.setup((c) => c.diagnostics).returns(() => [diagnostic.object]); + }); + + test('Ensure correct code action is returned if diagnostic message equals `Incorrect type. Expected "string".`', async () => { + diagnostic.setup((d) => d.message).returns(() => 'Incorrect type. Expected "string".'); + diagnostic.setup((d) => d.range).returns(() => new Range(2, 0, 7, 8)); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + // Now ensure that the code action object is as expected + expect(codeActions).to.have.length(1); + expect(codeActions[0].kind).to.eq(CodeActionKind.QuickFix); + expect(codeActions[0].title).to.equal('Convert to "Diagnostic text"'); + + // Ensure the correct TextEdit is provided + const entries = codeActions[0].edit!.entries(); + // Edits the correct document is edited + assert.deepEqual(entries[0][0], documentUri); + const edit = entries[0][1][0]; + // Final text is as expected + expect(edit.newText).to.equal('"Diagnostic text"'); + // Text edit range is as expected + expect(edit.range.isEqual(new Range(2, 0, 7, 8))).to.equal(true, 'Text edit range not as expected'); + }); + + test('Ensure no code action is returned if diagnostic message does not equal `Incorrect type. Expected "string".`', async () => { + diagnostic.setup((d) => d.message).returns(() => 'Random diagnostic message'); + + const codeActions = codeActionsProvider.provideCodeActions(document.object, range.object, context.object); + + expect(codeActions).to.have.length(0); + }); +}); diff --git a/src/test/providers/codeActionProvider/main.unit.test.ts b/src/test/providers/codeActionProvider/main.unit.test.ts new file mode 100644 index 000000000000..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<IDisposableRegistry>().object); + + await quickFixService.activate(); + + // Ensure QuickFixLaunchJson is registered with correct arguments + assert.deepEqual(selector!, { + scheme: 'file', + language: 'jsonc', + pattern: '**/launch.json', + }); + assert.deepEqual(metadata!, { + 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<TextDocument>; - let range: TypeMoq.IMock<Range>; - let context: TypeMoq.IMock<CodeActionContext>; - let token: TypeMoq.IMock<CancellationToken>; - - setup(() => { - codeActionsProvider = new PythonCodeActionProvider(); - document = TypeMoq.Mock.ofType<TextDocument>(); - range = TypeMoq.Mock.ofType<Range>(); - context = TypeMoq.Mock.ofType<CodeActionContext>(); - token = TypeMoq.Mock.ofType<CancellationToken>(); - }); - - test('Ensure it always returns a source.organizeImports CodeAction', async () => { - 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 fbef1aa47c0f..000000000000 --- a/src/test/providers/foldingProvider.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import { expect } from 'chai'; -import * as path from 'path'; -import { CancellationTokenSource, FoldingRange, FoldingRangeKind, workspace } from 'vscode'; -import { DocStringFoldingProvider } from '../../client/providers/docStringFoldingProvider'; - -type FileFoldingRanges = { file: string; ranges: FoldingRange[] }; -const pythonFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'folding'); - -// tslint:disable-next-line:max-func-body-length -suite('Provider - Folding Provider', () => { - const docStringFileAndExpectedFoldingRanges: FileFoldingRanges[] = [ - { - file: path.join(pythonFilesPath, 'attach_server.py'), ranges: [ - new FoldingRange(0, 14), - new FoldingRange(44, 73, FoldingRangeKind.Comment), - new FoldingRange(98, 146), - new FoldingRange(152, 153, FoldingRangeKind.Comment), - new FoldingRange(312, 320), - new FoldingRange(327, 329) - ] - }, - { - file: path.join(pythonFilesPath, 'visualstudio_ipython_repl.py'), ranges: [ - new FoldingRange(0, 14), new FoldingRange(78, 79, FoldingRangeKind.Comment), - new FoldingRange(81, 82, FoldingRangeKind.Comment), new FoldingRange(92, 93, FoldingRangeKind.Comment), - new FoldingRange(108, 109, FoldingRangeKind.Comment), new FoldingRange(139, 140, FoldingRangeKind.Comment), - new FoldingRange(169, 170, FoldingRangeKind.Comment), new FoldingRange(275, 277, FoldingRangeKind.Comment), - new FoldingRange(319, 320, FoldingRangeKind.Comment) - ] - }, - { - file: path.join(pythonFilesPath, 'visualstudio_py_debugger.py'), ranges: [ - new FoldingRange(0, 15, FoldingRangeKind.Comment), new FoldingRange(22, 25, FoldingRangeKind.Comment), - new FoldingRange(47, 48, FoldingRangeKind.Comment), new FoldingRange(69, 70, FoldingRangeKind.Comment), - new FoldingRange(96, 97, FoldingRangeKind.Comment), new FoldingRange(105, 106, FoldingRangeKind.Comment), - new FoldingRange(141, 142, FoldingRangeKind.Comment), new FoldingRange(149, 162, FoldingRangeKind.Comment), - new FoldingRange(165, 166, FoldingRangeKind.Comment), new FoldingRange(207, 208, FoldingRangeKind.Comment), - new FoldingRange(235, 237, FoldingRangeKind.Comment), new FoldingRange(240, 241, FoldingRangeKind.Comment), - new FoldingRange(300, 301, FoldingRangeKind.Comment), new FoldingRange(334, 335, FoldingRangeKind.Comment), - new FoldingRange(346, 348, FoldingRangeKind.Comment), new FoldingRange(499, 500, FoldingRangeKind.Comment), - new FoldingRange(558, 559, FoldingRangeKind.Comment), new FoldingRange(602, 604, FoldingRangeKind.Comment), - new FoldingRange(608, 609, FoldingRangeKind.Comment), new FoldingRange(612, 614, FoldingRangeKind.Comment), - new FoldingRange(637, 638, FoldingRangeKind.Comment) - ] - }, - { - file: path.join(pythonFilesPath, 'visualstudio_py_repl.py'), ranges: [] - } - ]; - - docStringFileAndExpectedFoldingRanges.forEach(item => { - test(`Test Docstring folding regions '${path.basename(item.file)}'`, async () => { - const document = await workspace.openTextDocument(item.file); - const provider = new DocStringFoldingProvider(); - const ranges = await provider.provideFoldingRanges(document, {}, new CancellationTokenSource().token); - expect(ranges).to.be.lengthOf(item.ranges.length); - ranges!.forEach(range => { - const index = item.ranges - .findIndex(searchItem => searchItem.start === range.start && - searchItem.end === range.end); - expect(index).to.be.greaterThan(-1, `${range.start}, ${range.end} not found`); - }); - }); - }); -}); diff --git a/src/test/providers/importSortProvider.unit.test.ts b/src/test/providers/importSortProvider.unit.test.ts deleted file mode 100644 index 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<IServiceContainer>; - let shell: TypeMoq.IMock<IApplicationShell>; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let configurationService: TypeMoq.IMock<IConfigurationService>; - let pythonExecFactory: TypeMoq.IMock<IPythonExecutionFactory>; - let processServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; - let editorUtils: TypeMoq.IMock<IEditorUtils>; - let commandManager: TypeMoq.IMock<ICommandManager>; - let pythonSettings: TypeMoq.IMock<IPythonSettings>; - let sortProvider: ISortImportsEditingProvider; - let fs: TypeMoq.IMock<IFileSystem>; - setup(() => { - serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - fs = TypeMoq.Mock.ofType<IFileSystem>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - shell = TypeMoq.Mock.ofType<IApplicationShell>(); - configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); - pythonExecFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); - processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - editorUtils = TypeMoq.Mock.ofType<IEditorUtils>(); - fs = TypeMoq.Mock.ofType<IFileSystem>(); - 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<TextEditor>(); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - mockDoc.setup(d => d.languageId) - .returns(() => 'xyz') - .verifiable(TypeMoq.Times.atLeastOnce()); - mockEditor.setup(d => d.document) - .returns(() => mockDoc.object) - .verifiable(TypeMoq.Times.atLeastOnce()); - - documentManager - .setup(d => d.activeTextEditor) - .returns(() => mockEditor.object) - .verifiable(TypeMoq.Times.once()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isValue('Please open a Python file to sort the imports.'))) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.once()); - await sortProvider.sortImports(); - - mockEditor.verifyAll(); - mockDoc.verifyAll(); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure document is opened', async () => { - const uri = Uri.file('TestDoc'); - - documentManager - .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.activeTextEditor) - .verifiable(TypeMoq.Times.never()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - await sortProvider.sortImports(uri).catch(noop); - - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure no edits are provided when there is only one line', async () => { - const uri = Uri.file('TestDoc'); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - // tslint:disable-next-line:no-any - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 1) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) - .returns(() => Promise.resolve(mockDoc.object)) - .verifiable(TypeMoq.Times.atLeastOnce()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const edit = await sortProvider.sortImports(uri); - - expect(edit).to.be.equal(undefined, 'not undefined'); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure no edits are provided when there are no lines', async () => { - const uri = Uri.file('TestDoc'); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - // tslint:disable-next-line:no-any - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 0) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) - .returns(() => Promise.resolve(mockDoc.object)) - .verifiable(TypeMoq.Times.atLeastOnce()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const edit = await sortProvider.sortImports(uri); - - expect(edit).to.be.equal(undefined, 'not undefined'); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure empty line is added when line does not end with an empty line', async () => { - const uri = Uri.file('TestDoc'); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 10) - .verifiable(TypeMoq.Times.atLeastOnce()); - - const lastLine = TypeMoq.Mock.ofType<TextLine>(); - let editApplied: WorkspaceEdit | undefined; - lastLine.setup(l => l.text) - .returns(() => '1234') - .verifiable(TypeMoq.Times.atLeastOnce()); - lastLine.setup(l => l.range) - .returns(() => new Range(1, 0, 10, 1)) - .verifiable(TypeMoq.Times.atLeastOnce()); - mockDoc.setup(d => d.lineAt(TypeMoq.It.isValue(9))) - .returns(() => lastLine.object) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.applyEdit(TypeMoq.It.isAny())) - .callback(e => editApplied = e) - .returns(() => Promise.resolve(true)) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) - .returns(() => Promise.resolve(mockDoc.object)) - .verifiable(TypeMoq.Times.atLeastOnce()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - - sortProvider.provideDocumentSortImportsEdits = () => Promise.resolve(undefined); - await sortProvider.sortImports(uri); - - expect(editApplied).not.to.be.equal(undefined, 'Applied edit is undefined'); - expect(editApplied!.entries()).to.be.lengthOf(1); - expect(editApplied!.entries()[0][1]).to.be.lengthOf(1); - expect(editApplied!.entries()[0][1][0].newText).to.be.equal(EOL); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure no edits are provided when there is only one line (when using provider method)', async () => { - const uri = Uri.file('TestDoc'); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 1) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) - .returns(() => Promise.resolve(mockDoc.object)) - .verifiable(TypeMoq.Times.atLeastOnce()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const edit = await sortProvider.provideDocumentSortImportsEdits(uri); - - expect(edit).to.be.equal(undefined, 'not undefined'); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure no edits are provided when there are no lines (when using provider method)', async () => { - const uri = Uri.file('TestDoc'); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 0) - .verifiable(TypeMoq.Times.atLeastOnce()); - documentManager - .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) - .returns(() => Promise.resolve(mockDoc.object)) - .verifiable(TypeMoq.Times.atLeastOnce()); - shell - .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(TypeMoq.Times.never()); - const edit = await sortProvider.provideDocumentSortImportsEdits(uri); - - expect(edit).to.be.equal(undefined, 'not undefined'); - shell.verifyAll(); - documentManager.verifyAll(); - }); - test('Ensure temporary file is created for sorting when document is dirty', async () => { - const uri = Uri.file('something.py'); - const mockDoc = TypeMoq.Mock.ofType<TextDocument>(); - let tmpFileDisposed = false; - const tmpFile: TemporaryFile = { filePath: 'TmpFile', dispose: () => tmpFileDisposed = true }; - const processService = TypeMoq.Mock.ofType<ProcessService>(); - processService.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 10) - .verifiable(TypeMoq.Times.atLeastOnce()); - mockDoc.setup(d => d.getText(TypeMoq.It.isAny())) - .returns(() => 'Hello') - .verifiable(TypeMoq.Times.atLeastOnce()); - mockDoc.setup(d => d.isDirty) - .returns(() => true) - .verifiable(TypeMoq.Times.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<TextDocument>(); - let tmpFileDisposed = false; - const tmpFile: TemporaryFile = { filePath: 'TmpFile', dispose: () => tmpFileDisposed = true }; - const processService = TypeMoq.Mock.ofType<ProcessService>(); - processService.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup((d: any) => d.then).returns(() => undefined); - mockDoc.setup(d => d.lineCount) - .returns(() => 10) - .verifiable(TypeMoq.Times.atLeastOnce()); - mockDoc.setup(d => d.getText(TypeMoq.It.isAny())) - .returns(() => 'Hello') - .verifiable(TypeMoq.Times.atLeastOnce()); - mockDoc.setup(d => d.isDirty) - .returns(() => true) - .verifiable(TypeMoq.Times.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<IPythonExecutionService>(); - processExeService.setup((p: any) => p.then).returns(() => undefined); - pythonExecFactory.setup(p => p.create(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(processExeService.object)) - .verifiable(TypeMoq.Times.once()); - 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<IServiceContainer>; let commandManager: TypeMoq.IMock<ICommandManager>; let workspace: TypeMoq.IMock<IWorkspaceService>; let codeExecutionService: TypeMoq.IMock<ICodeExecutionService>; let documentManager: TypeMoq.IMock<IDocumentManager>; + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; let replProvider: ReplProvider; + let interpreterService: TypeMoq.IMock<IInterpreterService>; setup(() => { serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); codeExecutionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - 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<IActiveResourceService>(); + 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<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); }); teardown(() => { try { replProvider.dispose(); - // tslint:disable-next-line:no-empty - } catch { } + } catch { + // No catch clause. + } }); test('Ensure command is registered', () => { replProvider = new ReplProvider(serviceContainer.object); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + commandManager.verify( + (c) => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); }); test('Ensure command handler is disposed', () => { const disposable = TypeMoq.Mock.ofType<Disposable>(); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => disposable.object); + 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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + let commandHandler: undefined | (() => Promise<void>); + + commandManager + .setup((c) => + c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + ) + .returns((_cmd, callback) => { + commandHandler = callback; + return disposable.object; + }); + activeResourceService + .setup((a) => a.getActiveResource()) + .returns(() => resource) + .verifiable(TypeMoq.Times.once()); replProvider = new ReplProvider(serviceContainer.object); expect(commandHandler).not.to.be.equal(undefined, 'Handler not set'); - commandHandler!.call(replProvider); + 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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - const documentUri = Uri.file('a'); - const editor = TypeMoq.Mock.ofType<TextEditor>(); - const document = TypeMoq.Mock.ofType<TextDocument>(); - 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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - const editor = TypeMoq.Mock.ofType<TextEditor>(); - const document = TypeMoq.Mock.ofType<TextDocument>(); - 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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Start_REPL), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); - - const workspaceUri = Uri.file('a'); - const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>(); - 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>( + 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 c3f525a1ac8b..000000000000 --- a/src/test/providers/shebangCodeLenseProvider.unit.test.ts +++ /dev/null @@ -1,186 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { anything, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { TextDocument, TextLine, Uri } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { ConfigurationService } from '../../client/common/configuration/service'; -import { PlatformService } from '../../client/common/platform/platformService'; -import { IPlatformService } from '../../client/common/platform/types'; -import { ProcessServiceFactory } from '../../client/common/process/processFactory'; -import { IProcessService, IProcessServiceFactory } from '../../client/common/process/types'; -import { IConfigurationService, IPythonSettings } from '../../client/common/types'; -import { ShebangCodeLensProvider } from '../../client/interpreter/display/shebangCodeLensProvider'; - -// tslint:disable-next-line:max-func-body-length -suite('Shebang detection', () => { - let configurationService: IConfigurationService; - let pythonSettings: typemoq.IMock<IPythonSettings>; - let workspaceService: IWorkspaceService; - let provider: ShebangCodeLensProvider; - let factory: IProcessServiceFactory; - let processService: typemoq.IMock<IProcessService>; - let platformService: typemoq.IMock<PlatformService>; - setup(() => { - pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - configurationService = mock(ConfigurationService); - workspaceService = mock(WorkspaceService); - factory = mock(ProcessServiceFactory); - processService = typemoq.Mock.ofType<IProcessService>(); - platformService = typemoq.Mock.ofType<IPlatformService>(); - // tslint:disable-next-line:no-any - processService.setup(p => (p as any).then).returns(() => undefined); - when(configurationService.getSettings(anything())).thenReturn(pythonSettings.object); - when(factory.create(anything())).thenResolve(processService.object); - provider = new ShebangCodeLensProvider(instance(factory), instance(configurationService), - platformService.object, instance(workspaceService)); - }); - function createDocument(firstLine: string, uri = Uri.parse('xyz.py')): [typemoq.IMock<TextDocument>, typemoq.IMock<TextLine>] { - const doc = typemoq.Mock.ofType<TextDocument>(); - const line = typemoq.Mock.ofType<TextLine>(); - - line.setup(l => l.isEmptyOrWhitespace) - .returns(() => firstLine.length === 0) - .verifiable(typemoq.Times.once()); - line.setup(l => l.text) - .returns(() => firstLine); - - doc.setup(d => d.lineAt(typemoq.It.isValue(0))) - .returns(() => line.object) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.uri) - .returns(() => uri); - - return [doc, line]; - } - test('Shebang should be empty when first line is empty', 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'); - processService.setup(p => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'python' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve(''); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(0); - }); - test('No code lens when shebang is an empty string', async () => { - const [document] = createDocument('#!'); - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); - processService.setup(p => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'python' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve(''); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(0); - }); - test('No code lens when python path in settings is the same as that in shebang', async () => { - const [document] = createDocument('#!python'); - pythonSettings.setup(p => p.pythonPath).returns(() => 'python'); - processService.setup(p => p.exec(typemoq.It.isValue('python'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'python' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve('python'); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(0); - }); - test('Code lens returned when python path in settings is different to one in shebang', async () => { - const [document] = createDocument('#!python'); - pythonSettings.setup(p => p.pythonPath).returns(() => 'different'); - processService.setup(p => p.exec(typemoq.It.isValue('different'), typemoq.It.isAny())) - .returns(() => Promise.resolve({ stdout: 'different' })) - .verifiable(typemoq.Times.once()); - - provider.detectShebang = () => Promise.resolve('python'); - - const codeLenses = await provider.provideCodeLenses(document.object); - - expect(codeLenses).to.be.lengthOf(1); - expect(codeLenses[0].command!.command).to.equal('python.setShebangInterpreter'); - expect(codeLenses[0].command!.title).to.equal('Set as interpreter'); - expect(codeLenses[0].range.start.character).to.equal(0); - expect(codeLenses[0].range.start.line).to.equal(0); - expect(codeLenses[0].range.end.line).to.equal(0); - }); -}); diff --git a/src/test/providers/terminal.unit.test.ts b/src/test/providers/terminal.unit.test.ts index 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<IServiceContainer>; let commandManager: TypeMoq.IMock<ICommandManager>; let workspace: TypeMoq.IMock<IWorkspaceService>; - let documentManager: TypeMoq.IMock<IDocumentManager>; + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; + let experimentService: TypeMoq.IMock<IExperimentService>; let terminalProvider: TerminalProvider; + let useEnvExtensionStub: sinon.SinonStub; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; + const resource = Uri.parse('a'); setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + experimentService = TypeMoq.Mock.ofType<IExperimentService>(); + experimentService.setup((e) => e.inExperimentSync(TerminalEnvVarActivation.experiment)).returns(() => false); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - 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<Disposable>(); - 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<Disposable>(); let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; - }); - 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<ITerminalServiceFactory>(); - 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<TerminalService>(); - 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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; + suite('terminal.activateCurrentTerminal setting', () => { + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let terminalSettings: TypeMoq.IMock<ITerminalSettings>; + let configService: TypeMoq.IMock<IConfigurationService>; + let terminalActivator: TypeMoq.IMock<ITerminalActivator>; + let terminal: TypeMoq.IMock<Terminal>; + + setup(() => { + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configService.object); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + + terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); + pythonSettings.setup((s) => s.terminal).returns(() => terminalSettings.object); + + terminalActivator = TypeMoq.Mock.ofType<ITerminalActivator>(); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(ITerminalActivator))) + .returns(() => terminalActivator.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + + terminal = TypeMoq.Mock.ofType<Terminal>(); + terminal.setup((c) => c.creationOptions).returns(() => ({ hideFromUser: false })); }); - const editor = TypeMoq.Mock.ofType<TextEditor>(); - documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); - const document = TypeMoq.Mock.ofType<TextDocument>(); - 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<ITerminalServiceFactory>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); - const terminalService = TypeMoq.Mock.ofType<TerminalService>(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; + 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<TextEditor>(); - documentManager.setup(d => d.activeTextEditor).returns(() => editor.object); - const document = TypeMoq.Mock.ofType<TextDocument>(); - 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<ITerminalServiceFactory>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); - const terminalService = TypeMoq.Mock.ofType<TerminalService>(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(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<Disposable>(); - let commandHandler: undefined | (() => void); - commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => { - commandHandler = callback; - return disposable.object; + 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>(); - 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<ITerminalServiceFactory>(); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object); - const terminalService = TypeMoq.Mock.ofType<TerminalService>(); - terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(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<I = PythonEnvInfo> extends Locator<I> { + public readonly providerId: string = 'SimpleLocator'; + + private deferred = createDeferred<void>(); + + constructor( + private envs: I[], + public callbacks: { + resolve?: null | ((env: PythonEnvInfo | string) => Promise<PythonEnvInfo | undefined>); + before?(): Promise<void>; + after?(): Promise<void>; + onUpdated?: Event<PythonEnvUpdatedEvent<I> | ProgressNotificationEvent>; + beforeEach?(e: I): Promise<void>; + afterEach?(e: I): Promise<void>; + onQuery?(query: PythonLocatorQuery | undefined, envs: I[]): Promise<I[]>; + } = {}, + private options?: { resolveAsString?: boolean }, + ) { + super(); + } + + public get done(): Promise<void> { + return this.deferred.promise; + } + + public fire(event: PythonEnvsChangedEvent): void { + this.emitter.fire(event); + } + + public iterEnvs(query?: PythonLocatorQuery): IPythonEnvsIterator<I> { + const { deferred } = this; + const { callbacks } = this; + let { envs } = this; + const iterator: IPythonEnvsIterator<I> = (async function* () { + if (callbacks?.onQuery !== undefined) { + envs = await callbacks.onQuery(query, envs); + } + if (callbacks.before !== undefined) { + await callbacks.before(); + } + if (callbacks.beforeEach !== undefined) { + // The results will likely come in a different order. + const mapped = mapToIterator(envs, async (env) => { + await callbacks.beforeEach!(env); + return env; + }); + for await (const env of iterable(mapped)) { + yield env; + if (callbacks.afterEach !== undefined) { + await callbacks.afterEach(env); + } + } + } else { + for (const env of envs) { + yield env; + if (callbacks.afterEach !== undefined) { + await callbacks.afterEach(env); + } + } + } + if (callbacks?.after !== undefined) { + await callbacks.after(); + } + deferred.resolve(); + })(); + iterator.onUpdated = this.callbacks?.onUpdated; + return iterator; + } + + public async resolveEnv(env: string): Promise<PythonEnvInfo | undefined> { + const envInfo: PythonEnvInfo = createLocatedEnv('', '', undefined, env); + if (this.callbacks.resolve === undefined) { + return envInfo; + } + if (this.callbacks?.resolve === null) { + return undefined; + } + return this.callbacks.resolve(this.options?.resolveAsString ? env : envInfo); + } +} + +export async function getEnvs<I = PythonEnvInfo>(iterator: IPythonEnvsIterator<I>): Promise<I[]> { + return flattenIterator(iterator); +} + +/** + * Unroll the given iterator into an array. + * + * This includes applying any received updates. + */ +export async function getEnvsWithUpdates<I = PythonEnvInfo>( + iterator: IPythonEnvsIterator<I>, + iteratorUpdateCallback: () => void = noop, +): Promise<I[]> { + const envs: (I | undefined)[] = []; + + const updatesDone = createDeferred<void>(); + if (iterator.onUpdated === undefined) { + updatesDone.resolve(); + } else { + const listener = iterator.onUpdated((event) => { + if (isProgressEvent(event)) { + if (event.stage !== ProgressReportStage.discoveryFinished) { + return; + } + updatesDone.resolve(); + listener.dispose(); + } else if (event.index !== undefined) { + const { index, update } = event; + // We don't worry about if envs[index] is set already. + envs[index] = update; + } + }); + } + + let itemIndex = 0; + for await (const env of iterator) { + // We can't just push because updates might get emitted early. + if (envs[itemIndex] === undefined) { + envs[itemIndex] = env; + } + itemIndex += 1; + } + iteratorUpdateCallback(); + await updatesDone.promise; + + // Do not return invalid environments + return envs.filter((e) => e !== undefined).map((e) => e!); +} + +export function sortedEnvs(envs: PythonEnvInfo[]): PythonEnvInfo[] { + return envs.sort((env1, env2) => { + const env1str = `${env1.kind}-${env1.executable.filename}-${getVersionString(env1.version)}`; + const env2str = `${env2.kind}-${env2.executable.filename}-${getVersionString(env2.version)}`; + return env1str.localeCompare(env2str); + }); +} + +export function assertSameEnvs(envs: PythonEnvInfo[], expected: PythonEnvInfo[]): void { + expect(sortedEnvs(envs)).to.deep.equal(sortedEnvs(expected)); +} diff --git a/src/test/pythonEnvironments/base/info/env.unit.test.ts b/src/test/pythonEnvironments/base/info/env.unit.test.ts new file mode 100644 index 000000000000..20bff8d71249 --- /dev/null +++ b/src/test/pythonEnvironments/base/info/env.unit.test.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { parseVersionInfo } from '../../../../client/common/utils/version'; +import { PythonEnvInfo, PythonDistroInfo, PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; +import { areEnvsDeepEqual, setEnvDisplayString } from '../../../../client/pythonEnvironments/base/info/env'; +import { createLocatedEnv } from '../common'; + +suite('Environment helpers', () => { + const name = 'my-env'; + const location = 'x/y/z/spam/'; + const searchLocation = 'x/y/z'; + const arch = Architecture.x64; + const version = '3.8.1'; + const kind = PythonEnvKind.Venv; + const distro: PythonDistroInfo = { + org: 'Distro X', + defaultDisplayName: 'distroX 1.2', + version: parseVersionInfo('1.2.3')?.version, + binDir: 'distroX/bin', + }; + const locationConda1 = 'x/y/z/conda1'; + const locationConda2 = 'x/y/z/conda2'; + const kindConda = PythonEnvKind.Conda; + function getEnv(info: { + version?: string; + arch?: Architecture; + name?: string; + kind?: PythonEnvKind; + distro?: PythonDistroInfo; + display?: string; + location?: string; + searchLocation?: string; + }): PythonEnvInfo { + const env = createLocatedEnv( + info.location || '', + info.version || '', + info.kind || PythonEnvKind.Unknown, + 'python', // exec + info.distro, + info.searchLocation ? Uri.file(info.searchLocation) : undefined, + ); + env.name = info.name || ''; + env.arch = info.arch || Architecture.Unknown; + env.display = info.display; + return env; + } + function testGenerator() { + const tests: [PythonEnvInfo, string, string][] = [ + [getEnv({}), 'Python', 'Python'], + [getEnv({ version, arch, name, kind, distro }), "Python 3.8.1 ('my-env')", "Python 3.8.1 ('my-env': venv)"], + // without "suffix" info + [getEnv({ version }), 'Python 3.8.1', 'Python 3.8.1'], + [getEnv({ arch }), 'Python 64-bit', 'Python 64-bit'], + [getEnv({ version, arch }), 'Python 3.8.1 64-bit', 'Python 3.8.1 64-bit'], + // with "suffix" info + [getEnv({ name }), "Python ('my-env')", "Python ('my-env')"], + [getEnv({ kind }), 'Python', 'Python (venv)'], + [getEnv({ name, kind }), "Python ('my-env')", "Python ('my-env': venv)"], + // env.location is ignored. + [getEnv({ location }), 'Python', 'Python'], + [getEnv({ name, location }), "Python ('my-env')", "Python ('my-env')"], + [ + getEnv({ name, location, searchLocation, version, arch }), + "Python 3.8.1 64-bit ('my-env')", + "Python 3.8.1 64-bit ('my-env')", + ], + // conda env.name is empty. + [getEnv({ kind: kindConda }), 'Python', 'Python (conda)'], + [getEnv({ location: locationConda1, kind: kindConda }), "Python ('conda1')", "Python ('conda1': conda)"], + [getEnv({ location: locationConda2, kind: kindConda }), "Python ('conda2')", "Python ('conda2': conda)"], + ]; + return tests; + } + testGenerator().forEach(([env, expectedDisplay, expectedDetailedDisplay]) => { + test(`"${expectedDisplay}"`, () => { + setEnvDisplayString(env); + + assert.equal(env.display, expectedDisplay); + assert.equal(env.detailedDisplayName, expectedDetailedDisplay); + }); + }); + testGenerator().forEach(([env1, _d1, display1], index1) => { + testGenerator().forEach(([env2, _d2, display2], index2) => { + if (index1 === index2) { + test(`"${display1}" === "${display2}"`, () => { + assert.strictEqual(areEnvsDeepEqual(env1, env2), true); + }); + } else { + test(`"${display1}" !== "${display2}"`, () => { + assert.strictEqual(areEnvsDeepEqual(env1, env2), false); + }); + } + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/info/envKind.unit.test.ts b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts new file mode 100644 index 000000000000..6d0866754330 --- /dev/null +++ b/src/test/pythonEnvironments/base/info/envKind.unit.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; + +import { getNamesAndValues } from '../../../../client/common/utils/enum'; +import { PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; +import { getKindDisplayName, getPrioritizedEnvKinds } from '../../../../client/pythonEnvironments/base/info/envKind'; + +const KIND_NAMES: [PythonEnvKind, string][] = [ + // We handle PythonEnvKind.Unknown separately. + [PythonEnvKind.System, 'system'], + [PythonEnvKind.MicrosoftStore, 'winStore'], + [PythonEnvKind.Pyenv, 'pyenv'], + [PythonEnvKind.Poetry, 'poetry'], + [PythonEnvKind.Hatch, 'hatch'], + [PythonEnvKind.Pixi, 'pixi'], + [PythonEnvKind.Custom, 'customGlobal'], + [PythonEnvKind.OtherGlobal, 'otherGlobal'], + [PythonEnvKind.Venv, 'venv'], + [PythonEnvKind.VirtualEnv, 'virtualenv'], + [PythonEnvKind.VirtualEnvWrapper, 'virtualenvWrapper'], + [PythonEnvKind.Pipenv, 'pipenv'], + [PythonEnvKind.Conda, 'conda'], + [PythonEnvKind.ActiveState, 'activestate'], + [PythonEnvKind.OtherVirtual, 'otherVirtual'], +]; + +suite('pyenvs info - PyEnvKind', () => { + test('all Python env kinds are covered', () => { + assert.strictEqual( + KIND_NAMES.length, + // We ignore PythonEnvKind.Unknown. + getNamesAndValues(PythonEnvKind).length - 1, + ); + }); + + suite('getKindDisplayName()', () => { + suite('known', () => { + KIND_NAMES.forEach(([kind]) => { + if (kind === PythonEnvKind.OtherGlobal || kind === PythonEnvKind.OtherVirtual) { + return; + } + test(`check ${kind}`, () => { + const name = getKindDisplayName(kind); + + assert.notStrictEqual(name, ''); + }); + }); + }); + + suite('not known', () => { + [ + PythonEnvKind.Unknown, + PythonEnvKind.OtherGlobal, + PythonEnvKind.OtherVirtual, + // Any other kinds that don't have clear display names go here. + ].forEach((kind) => { + test(`check ${kind}`, () => { + const name = getKindDisplayName(kind); + + assert.strictEqual(name, ''); + }); + }); + }); + }); + + suite('getPrioritizedEnvKinds()', () => { + test('all Python env kinds are covered', () => { + const numPrioritized = getPrioritizedEnvKinds().length; + const numNames = getNamesAndValues(PythonEnvKind).length; + + assert.strictEqual(numPrioritized, numNames); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/info/environmentInfoService.functional.test.ts b/src/test/pythonEnvironments/base/info/environmentInfoService.functional.test.ts new file mode 100644 index 000000000000..785148f8589c --- /dev/null +++ b/src/test/pythonEnvironments/base/info/environmentInfoService.functional.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { SemVer } from 'semver'; +import { ExecutionResult } from '../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../client/common/types'; +import { Architecture } from '../../../../client/common/utils/platform'; +import { InterpreterInformation } from '../../../../client/pythonEnvironments/base/info/interpreter'; +import { parseVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; +import * as ExternalDep from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + EnvironmentInfoServiceQueuePriority, + getEnvironmentInfoService, +} from '../../../../client/pythonEnvironments/base/info/environmentInfoService'; +import { buildEnvInfo } from '../../../../client/pythonEnvironments/base/info/env'; +import { Conda, CONDA_RUN_VERSION } from '../../../../client/pythonEnvironments/common/environmentManagers/conda'; + +suite('Environment Info Service', () => { + let stubShellExec: sinon.SinonStub; + let disposables: IDisposableRegistry; + + function createExpectedEnvInfo(executable: string): InterpreterInformation { + return { + version: { + ...parseVersion('3.8.3-final'), + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + }, + arch: Architecture.x64, + executable: { + filename: executable, + sysPrefix: 'path', + mtime: -1, + ctime: -1, + }, + }; + } + + setup(() => { + disposables = []; + stubShellExec = sinon.stub(ExternalDep, 'shellExecute'); + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + stderr: 'Some std error', // This should be ignored. + }); + }), + ); + sinon.stub(Conda, 'getConda').resolves(new Conda('conda')); + sinon.stub(Conda.prototype, 'getCondaVersion').resolves(new SemVer(CONDA_RUN_VERSION)); + }); + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + test('Add items to queue and get results', async () => { + const envService = getEnvironmentInfoService(disposables); + const promises: Promise<InterpreterInformation | undefined>[] = []; + const expected: InterpreterInformation[] = []; + for (let i = 0; i < 10; i = i + 1) { + const path = `any-path${i}`; + if (i < 5) { + promises.push(envService.getEnvironmentInfo(buildEnvInfo({ executable: path }))); + } else { + promises.push( + envService.getEnvironmentInfo( + buildEnvInfo({ executable: path }), + EnvironmentInfoServiceQueuePriority.High, + ), + ); + } + expected.push(createExpectedEnvInfo(path)); + } + + await Promise.all(promises).then((r) => { + // The processing order is non-deterministic since we don't know + // how long each work item will take. So we compare here with + // results of processing in the same order as we have collected + // the promises. + assert.deepEqual(r, expected); + }); + }); + + test('Add same item to queue', async () => { + const envService = getEnvironmentInfoService(disposables); + const promises: Promise<InterpreterInformation | undefined>[] = []; + const expected: InterpreterInformation[] = []; + + const path = 'any-path'; + // Clear call counts + stubShellExec.resetHistory(); + // Evaluate once so the result is cached. + await envService.getEnvironmentInfo(buildEnvInfo({ executable: path })); + + for (let i = 0; i < 10; i = i + 1) { + promises.push(envService.getEnvironmentInfo(buildEnvInfo({ executable: path }))); + expected.push(createExpectedEnvInfo(path)); + } + + await Promise.all(promises).then((r) => { + assert.deepEqual(r, expected); + }); + assert.ok(stubShellExec.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/base/info/pythonVersion.unit.test.ts b/src/test/pythonEnvironments/base/info/pythonVersion.unit.test.ts new file mode 100644 index 000000000000..620fb15f8614 --- /dev/null +++ b/src/test/pythonEnvironments/base/info/pythonVersion.unit.test.ts @@ -0,0 +1,231 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; + +import { PythonReleaseLevel, PythonVersion } from '../../../../client/pythonEnvironments/base/info'; +import { + compareSemVerLikeVersions, + getEmptyVersion, + getShortVersionString, + parseVersion, +} from '../../../../client/pythonEnvironments/base/info/pythonVersion'; + +export function ver( + major: number, + minor: number | undefined, + micro: number | undefined, + level?: string, + serial?: number, +): PythonVersion { + const version: PythonVersion = { + major, + minor: minor === undefined ? -1 : minor, + micro: micro === undefined ? -1 : micro, + release: undefined, + }; + if (level !== undefined) { + version.release = { + serial: serial!, + level: level as PythonReleaseLevel, + }; + } + return version; +} + +const VERSION_STRINGS: [string, PythonVersion][] = [ + ['0.9.2b2', ver(0, 9, 2, 'beta', 2)], + ['3.3.1', ver(3, 3, 1)], // final + ['3.9.0rc1', ver(3, 9, 0, 'candidate', 1)], + ['2.7.11a3', ver(2, 7, 11, 'alpha', 3)], +]; + +suite('pyenvs info - getShortVersionString', () => { + for (const data of VERSION_STRINGS) { + const [expected, info] = data; + test(`conversion works for '${expected}'`, () => { + const result = getShortVersionString(info); + + assert.strictEqual(result, expected); + }); + } + + test('conversion works for final', () => { + const expected = '3.3.1'; + const info = ver(3, 3, 1, 'final', 0); + + const result = getShortVersionString(info); + + assert.strictEqual(result, expected); + }); +}); + +suite('pyenvs info - parseVersion', () => { + suite('full versions (short)', () => { + VERSION_STRINGS.forEach((data) => { + const [text, expected] = data; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + suite('full versions (long)', () => { + [ + ['0.9.2-beta2', ver(0, 9, 2, 'beta', 2)], + ['3.3.1-final', ver(3, 3, 1, 'final', 0)], + ['3.3.1-final0', ver(3, 3, 1, 'final', 0)], + ['3.9.0-candidate1', ver(3, 9, 0, 'candidate', 1)], + ['2.7.11-alpha3', ver(2, 7, 11, 'alpha', 3)], + ['0.9.2.beta.2', ver(0, 9, 2, 'beta', 2)], + ['3.3.1.final.0', ver(3, 3, 1, 'final', 0)], + ['3.9.0.candidate.1', ver(3, 9, 0, 'candidate', 1)], + ['2.7.11.alpha.3', ver(2, 7, 11, 'alpha', 3)], + ].forEach((data) => { + const [text, expected] = data as [string, PythonVersion]; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + suite('partial versions', () => { + [ + ['3.7.1', ver(3, 7, 1)], + ['3.7', ver(3, 7, -1)], + ['3', ver(3, -1, -1)], + ['37', ver(3, 7, -1)], // not 37 + ['371', ver(3, 71, -1)], // not 3.7.1 + ['3102', ver(3, 102, -1)], // not 3.10.2 + ['2.7', ver(2, 7, -1)], + ['2', ver(2, -1, -1)], // not 2.7 + ['27', ver(2, 7, -1)], + ].forEach((data) => { + const [text, expected] = data as [string, PythonVersion]; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + suite('other forms', () => { + [ + // prefixes + ['python3', ver(3, -1, -1)], + ['python3.8', ver(3, 8, -1)], + ['python3.8.1', ver(3, 8, 1)], + ['python3.8.1b2', ver(3, 8, 1, 'beta', 2)], + ['python-3', ver(3, -1, -1)], + // release ignored (missing micro) + ['python3.8b2', ver(3, 8, -1)], + ['python38b2', ver(3, 8, -1)], + ['python381b2', ver(3, 81, -1)], // not 3.8.1 + // suffixes + ['python3.exe', ver(3, -1, -1)], + ['python3.8.exe', ver(3, 8, -1)], + ['python3.8.1.exe', ver(3, 8, 1)], + ['python3.8.1b2.exe', ver(3, 8, 1, 'beta', 2)], + ['3.8.1.build123.revDEADBEEF', ver(3, 8, 1)], + ['3.8.1b2.build123.revDEADBEEF', ver(3, 8, 1, 'beta', 2)], + // dirnames + ['/x/y/z/python38/bin/python', ver(3, 8, -1)], + ['/x/y/z/python/38/bin/python', ver(3, 8, -1)], + ['/x/y/z/python/38/bin/python', ver(3, 8, -1)], + ].forEach((data) => { + const [text, expected] = data as [string, PythonVersion]; + test(`conversion works for '${text}'`, () => { + const result = parseVersion(text); + + assert.deepEqual(result, expected); + }); + }); + }); + + test('empty string results in empty version', () => { + const expected = getEmptyVersion(); + + const result = parseVersion(''); + + assert.deepEqual(result, expected); + }); + + suite('bogus input', () => { + [ + // errant dots + 'py.3.7', + 'py3.7.', + 'python.3', + // no version + 'spam', + 'python.exe', + 'python', + ].forEach((text) => { + test(`conversion does not work for '${text}'`, () => { + assert.throws(() => parseVersion(text)); + }); + }); + }); +}); + +suite('pyenvs info - compareSemVerLikeVersions', () => { + const testData = [ + { + v1: { major: 2, minor: 7, patch: 19 }, + v2: { major: 3, minor: 7, patch: 4 }, + expected: -1, + }, + { + v1: { major: 2, minor: 7, patch: 19 }, + v2: { major: 2, minor: 7, patch: 19 }, + expected: 0, + }, + { + v1: { major: 3, minor: 7, patch: 4 }, + v2: { major: 2, minor: 7, patch: 19 }, + expected: 1, + }, + { + v1: { major: 3, minor: 8, patch: 1 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: -1, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: 0, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 8, patch: 1 }, + expected: 1, + }, + { + v1: { major: 3, minor: 9, patch: 0 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: -1, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 9, patch: 1 }, + expected: 0, + }, + { + v1: { major: 3, minor: 9, patch: 1 }, + v2: { major: 3, minor: 9, patch: 0 }, + expected: 1, + }, + ]; + + testData.forEach((data) => { + test(`Compare versions ${JSON.stringify(data.v1)} and ${JSON.stringify(data.v2)}`, () => { + const actual = compareSemVerLikeVersions(data.v1, data.v2); + assert.deepStrictEqual(actual, data.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts new file mode 100644 index 000000000000..8e4bc02e4797 --- /dev/null +++ b/src/test/pythonEnvironments/base/locatorUtils.unit.test.ts @@ -0,0 +1,497 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import { EventEmitter, Uri } from 'vscode'; +import { getValues as getEnumValues } from '../../../client/common/utils/enum'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { copyEnvInfo } from '../../../client/pythonEnvironments/base/info/env'; +import { + IPythonEnvsIterator, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, + PythonLocatorQuery, +} from '../../../client/pythonEnvironments/base/locator'; +import { getEnvs, getQueryFilter } from '../../../client/pythonEnvironments/base/locatorUtils'; +import { createLocatedEnv, createNamedEnv } from './common'; + +const homeDir = path.normalize('/home/me'); +const workspaceRoot = Uri.file('workspace-root'); +const doesNotExist = Uri.file(path.normalize('does-not-exist')); + +function setSearchLocation(env: PythonEnvInfo, location?: string): void { + const locationStr = location === undefined ? path.dirname(env.location) : path.normalize(location); + env.searchLocation = Uri.file(locationStr); +} + +const env1 = createNamedEnv('env1', '3.8', PythonEnvKind.System, '/usr/bin/python3.8'); +const env2 = createNamedEnv('env2', '3.8.1rc2', PythonEnvKind.Pyenv, '/pyenv/3.8.1rc2/bin/python'); +const env3 = createNamedEnv('env3', '3.9.1b2', PythonEnvKind.Unknown, 'python3.9'); +const env4 = createNamedEnv('env4', '2.7.11', PythonEnvKind.Pyenv, '/pyenv/2.7.11/bin/python'); +const env5 = createNamedEnv('env5', '2.7', PythonEnvKind.System, 'python2'); +const env6 = createNamedEnv('env6', '3.7.4', PythonEnvKind.Conda, 'python'); +const plainEnvs = [env1, env2, env3, env4, env5, env6]; + +const envL1 = createLocatedEnv('/.venvs/envL1', '3.9.0', PythonEnvKind.Venv); +const envL2 = createLocatedEnv('/conda/envs/envL2', '3.8.3', PythonEnvKind.Conda); +const locatedEnvs = [envL1, envL2]; + +const envS1 = createNamedEnv('env S1', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir/bin/python`); +setSearchLocation(envS1, `${homeDir}/`); // Have a search location ending in '/' +const envS2 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${homeDir}/some-dir2/bin/python`); +setSearchLocation(envS2, homeDir); +const envS3 = createNamedEnv('env S2', '3.9', PythonEnvKind.OtherVirtual, `${workspaceRoot.fsPath}/p/python`); +envS3.searchLocation = workspaceRoot; +const rootedEnvs = [envS1, envS2, envS3]; + +const envSL1 = createLocatedEnv(`${homeDir}/.venvs/envSL1`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL1); +const envSL2 = createLocatedEnv(`${workspaceRoot.fsPath}/.venv`, '3.8.2', PythonEnvKind.Pipenv); +setSearchLocation(envSL2); +const envSL3 = createLocatedEnv(`${homeDir}/.conda-envs/envSL3`, '3.8.2', PythonEnvKind.Conda); +setSearchLocation(envSL3); +const envSL4 = createLocatedEnv('/opt/python3.10', '3.10.0a1', PythonEnvKind.Custom); +setSearchLocation(envSL4); +const envSL5 = createLocatedEnv(`${homeDir}/.venvs/envSL5`, '3.9.0', PythonEnvKind.Venv); +setSearchLocation(envSL5); +const rootedLocatedEnvs = [envSL1, envSL2, envSL3, envSL4, envSL5]; + +const envs = [...plainEnvs, ...locatedEnvs, ...rootedEnvs, ...rootedLocatedEnvs]; + +suite('Python envs locator utils - getQueryFilter', () => { + suite('empty query', () => { + const queries: PythonLocatorQuery[] = [ + {}, + { kinds: [] }, + // Any "defined" value for searchLocations causes filtering... + ]; + queries.forEach((query) => { + test(`all envs kept (query ${query})`, () => { + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + }); + + suite('kinds', () => { + test('match none', () => { + const query: PythonLocatorQuery = { kinds: [PythonEnvKind.Poetry] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + ([ + [PythonEnvKind.Unknown, [env3]], + [PythonEnvKind.System, [env1, env5]], + [PythonEnvKind.MicrosoftStore, []], + [PythonEnvKind.Pyenv, [env2, env4]], + [PythonEnvKind.Venv, [envL1, envSL1, envSL5]], + [PythonEnvKind.Conda, [env6, envL2, envSL3]], + ] as [PythonEnvKind, PythonEnvInfo[]][]).forEach(([kind, expected]) => { + test(`match some (one kind: ${kind})`, () => { + const query: PythonLocatorQuery = { kinds: [kind] }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + test('match some (many kinds)', () => { + const expected = [env6, envL1, envL2, envSL1, envSL2, envSL3, envSL4, envSL5]; + const kinds = [ + PythonEnvKind.Venv, + PythonEnvKind.VirtualEnv, + PythonEnvKind.Pipenv, + PythonEnvKind.Conda, + PythonEnvKind.Custom, + ]; + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const query: PythonLocatorQuery = { kinds }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, envs); + }); + }); + + suite('searchLocations', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + searchLocations: { + roots: [doesNotExist], + doNotIncludeNonRooted: true, + }, + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match one (multiple locations)', () => { + const expected = [envSL4]; + const searchLocations = { + roots: [ + envSL4.searchLocation!, + doesNotExist, + envSL4.searchLocation!, // repeated + ], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (one location)', () => { + const expected = [envS3, envSL2]; + const searchLocations = { + roots: [workspaceRoot], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test("match multiple (one location) uri path ending in '/'", () => { + const expected = [envS3, envSL2]; + const searchLocations = { + roots: [Uri.file(`${workspaceRoot.path}/`)], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (multiple locations)', () => { + const expected = [envS3, ...rootedLocatedEnvs]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match multiple (include non-searched envs)', () => { + const expected = [...plainEnvs, ...locatedEnvs, envS3, ...rootedLocatedEnvs]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + doNotIncludeNonRooted: false, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all (including non-searched)', () => { + const expected = envs; + const searchLocations = { + roots: expected.map((e) => e.searchLocation!).filter((e) => !!e), + doNotIncludeNonRooted: false, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all searched under one root', () => { + const expected = [envS1, envS2, envSL1, envSL3, envSL5]; + const searchLocations = { + roots: [Uri.file(homeDir)], + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (empty roots)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [], + doNotIncludeNonRooted: false, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match only non-searched envs (with unmatched location)', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [doesNotExist], + doNotIncludeNonRooted: false, + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('include non rooted envs by default', () => { + const expected = [...plainEnvs, ...locatedEnvs]; + const searchLocations = { + roots: [doesNotExist], + }; + const query: PythonLocatorQuery = { searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); + + suite('mixed query', () => { + test('match none', () => { + const query: PythonLocatorQuery = { + kinds: [PythonEnvKind.OtherGlobal], + searchLocations: { + roots: [doesNotExist], + }, + }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, []); + }); + + test('match some', () => { + const expected = [envSL1, envSL4, envSL5]; + const kinds = [PythonEnvKind.Venv, PythonEnvKind.Custom]; + const searchLocations = { + roots: rootedLocatedEnvs.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + searchLocations.roots.push(doesNotExist); + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + + test('match all', () => { + const expected = [...rootedEnvs, ...rootedLocatedEnvs]; + const kinds: PythonEnvKind[] = getEnumValues(PythonEnvKind); + const searchLocations = { + roots: expected.map((env) => env.searchLocation!), + doNotIncludeNonRooted: true, + }; + const query: PythonLocatorQuery = { kinds, searchLocations }; + + const filter = getQueryFilter(query); + const filtered = envs.filter(filter); + + assert.deepEqual(filtered, expected); + }); + }); +}); + +suite('Python envs locator utils - getEnvs', () => { + test('empty, no update emitter', async () => { + const iterator = (async function* () { + // Yield nothing. + })() as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('empty, with unused update emitter', async () => { + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + // eslint-disable-next-line require-yield + const iterator = (async function* () { + // Yield nothing. + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, []); + }); + + test('yield one, no update emitter', async () => { + const iterator = (async function* () { + yield env1; + })() as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, no update', async () => { + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + yield env1; + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, [env1]); + }); + + test('yield one, with update', async () => { + const expected = [envSL2]; + const old = copyEnvInfo(envSL2, { kind: PythonEnvKind.Venv }); + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + yield old; + emitter.fire({ index: 0, old, update: envSL2 }); + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, no update emitter', async () => { + const expected = rootedLocatedEnvs; + const iterator = (async function* () { + yield* expected; + })() as IPythonEnvsIterator; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, none updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + yield* expected; + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, some updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + const original = [...expected]; + const updated = [1, 2, 4]; + const kind = PythonEnvKind.Unknown; + updated.forEach((index) => { + original[index] = copyEnvInfo(expected[index], { kind }); + }); + + yield* original; + + updated.forEach((index) => { + emitter.fire({ index, old: original[index], update: expected[index] }); + }); + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); + + test('yield many, all updated', async () => { + const expected = rootedLocatedEnvs; + const emitter = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const iterator = (async function* () { + const kind = PythonEnvKind.Unknown; + const original = expected.map((env) => copyEnvInfo(env, { kind })); + + yield original[0]; + yield original[1]; + emitter.fire({ index: 0, old: original[0], update: expected[0] }); + yield* original.slice(2); + original.forEach((old, index) => { + if (index > 0) { + emitter.fire({ index, old, update: expected[index] }); + } + }); + emitter.fire({ stage: ProgressReportStage.discoveryFinished }); + })() as IPythonEnvsIterator; + iterator.onUpdated = emitter.event; + + const result = await getEnvs(iterator); + + assert.deepEqual(result, expected); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators.unit.test.ts b/src/test/pythonEnvironments/base/locators.unit.test.ts new file mode 100644 index 000000000000..ad17b588c48b --- /dev/null +++ b/src/test/pythonEnvironments/base/locators.unit.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { createDeferred } from '../../../client/common/utils/async'; +import { PythonEnvInfo, PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { PythonLocatorQuery } from '../../../client/pythonEnvironments/base/locator'; +import { Locators } from '../../../client/pythonEnvironments/base/locators'; +import { PythonEnvsChangedEvent } from '../../../client/pythonEnvironments/base/watcher'; +import { createLocatedEnv, createNamedEnv, getEnvs, SimpleLocator } from './common'; + +suite('Python envs locators - Locators', () => { + suite('onChanged consolidates', () => { + test('one', () => { + const event1: PythonEnvsChangedEvent = {}; + const expected = [event1]; + const sub1 = new SimpleLocator([]); + const locators = new Locators([sub1]); + + const events: PythonEnvsChangedEvent[] = []; + locators.onChanged((e) => events.push(e)); + sub1.fire(event1); + + assert.deepEqual(events, expected); + }); + + test('many', () => { + const loc1 = Uri.file('some-dir'); + const event1: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown, searchLocation: loc1 }; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const event3: PythonEnvsChangedEvent = {}; + const event4: PythonEnvsChangedEvent = { searchLocation: loc1 }; + const event5: PythonEnvsChangedEvent = {}; + const expected = [event1, event2, event3, event4, event5]; + const sub1 = new SimpleLocator([]); + const sub2 = new SimpleLocator([]); + const sub3 = new SimpleLocator([]); + const locators = new Locators([sub1, sub2, sub3]); + + const events: PythonEnvsChangedEvent[] = []; + locators.onChanged((e) => events.push(e)); + sub2.fire(event1); + sub3.fire(event2); + sub1.fire(event3); + sub2.fire(event4); + sub1.fire(event5); + + assert.deepEqual(events, expected); + }); + }); + + suite('iterEnvs() consolidates', () => { + test('no envs', async () => { + const expected: PythonEnvInfo[] = []; + const sub1 = new SimpleLocator([]); + const locators = new Locators([sub1]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('one', async () => { + const env1 = createNamedEnv('foo', '3.8', PythonEnvKind.Venv); + const expected: PythonEnvInfo[] = [env1]; + const sub1 = new SimpleLocator(expected); + const locators = new Locators([sub1]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('many', async () => { + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); + const expected = [env1, env2, env3, env4, env5]; + const sub1 = new SimpleLocator([env1]); + const sub2 = new SimpleLocator([], { before: () => sub1.done }); + const sub3 = new SimpleLocator([env2, env3, env4], { before: () => sub2.done }); + const sub4 = new SimpleLocator([env5], { before: () => sub3.done }); + const locators = new Locators([sub1, sub2, sub3, sub4]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('with query', async () => { + const expected: PythonLocatorQuery = { + kinds: [PythonEnvKind.Venv], + searchLocations: { roots: [Uri.file('???')] }, + }; + let query: PythonLocatorQuery | undefined; + async function onQuery(q: PythonLocatorQuery | undefined, e: PythonEnvInfo[]) { + query = q; + return e; + } + const env1 = createNamedEnv('foo', '3.8', PythonEnvKind.Venv); + const sub1 = new SimpleLocator([env1], { onQuery }); + const locators = new Locators([sub1]); + + const iterator = locators.iterEnvs(expected); + await getEnvs(iterator); + + assert.deepEqual(query, expected); + }); + + test('iterate out of order', async () => { + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); + const env6 = createNamedEnv('spam', '3.10.0a0', PythonEnvKind.Custom); + const env7 = createNamedEnv('eggs', '3.9.1a0', PythonEnvKind.Custom); + const expected = [env5, env1, env2, env3, env4, env6, env7]; + const sub4 = new SimpleLocator([env5]); + const sub2 = new SimpleLocator([env1], { before: () => sub4.done }); + const sub1 = new SimpleLocator([]); + const sub3 = new SimpleLocator([env2, env3, env4], { before: () => sub2.done }); + const sub5 = new SimpleLocator([env6, env7], { before: () => sub3.done }); + const locators = new Locators([sub1, sub2, sub3, sub4, sub5]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('iterate intermingled', async () => { + const env1 = createNamedEnv('foo', '3.5.12b1', PythonEnvKind.Venv); + const env2 = createLocatedEnv('some-dir', '3.8.1', PythonEnvKind.Conda); + const env3 = createNamedEnv('python2', '2.7', PythonEnvKind.System); + const env4 = createNamedEnv('42', '3.9.0rc2', PythonEnvKind.Pyenv); + const env5 = createNamedEnv('hello world', '3.8', PythonEnvKind.System); + const expected = [env1, env4, env2, env5, env3]; + const deferred1 = createDeferred<void>(); + const deferred2 = createDeferred<void>(); + const deferred4 = createDeferred<void>(); + const deferred5 = createDeferred<void>(); + const sub1 = new SimpleLocator([env1, env2, env3], { + beforeEach: async (env) => { + if (env === env2) { + await deferred4.promise; + } else if (env === env3) { + await deferred5.promise; + } + }, + afterEach: async (env) => { + if (env === env1) { + deferred1.resolve(); + } else if (env === env2) { + deferred2.resolve(); + } + }, + }); + const sub2 = new SimpleLocator([env4, env5], { + beforeEach: async (env) => { + if (env === env4) { + await deferred1.promise; + } else if (env === env5) { + await deferred2.promise; + } + }, + afterEach: async (env) => { + if (env === env4) { + deferred4.resolve(); + } else if (env === env5) { + deferred5.resolve(); + } + }, + }); + const locators = new Locators([sub1, sub2]); + + const iterator = locators.iterEnvs(); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts new file mode 100644 index 000000000000..9fe481c4da3f --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/envsCollectionService.unit.test.ts @@ -0,0 +1,656 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import { assert, expect } from 'chai'; +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { EventEmitter, Uri } from 'vscode'; +import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; +import { createDeferred, createDeferredFromPromise, sleep } from '../../../../../client/common/utils/async'; +import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { areSameEnv, buildEnvInfo } from '../../../../../client/pythonEnvironments/base/info/env'; +import { + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, +} from '../../../../../client/pythonEnvironments/base/locator'; +import { createCollectionCache } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionCache'; +import { EnvsCollectionService } from '../../../../../client/pythonEnvironments/base/locators/composite/envsCollectionService'; +import { PythonEnvCollectionChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { noop } from '../../../../core'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { SimpleLocator } from '../../common'; +import { assertEnvEqual, assertEnvsEqual, createFile, deleteFile } from '../envTestUtils'; +import { OSType, getOSType } from '../../../../common'; +import * as nativeFinder from '../../../../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; + +class MockNativePythonFinder implements nativeFinder.NativePythonFinder { + find(_searchPath: string): Promise<nativeFinder.NativeEnvInfo[]> { + throw new Error('Method not implemented.'); + } + + getCondaInfo(): Promise<nativeFinder.NativeCondaInfo> { + throw new Error('Method not implemented.'); + } + + resolve(_executable: string): Promise<nativeFinder.NativeEnvInfo> { + throw new Error('Method not implemented.'); + } + + refresh(): AsyncIterable<nativeFinder.NativeEnvInfo> { + const envs: nativeFinder.NativeEnvInfo[] = []; + return (async function* () { + for (const env of envs) { + yield env; + } + })(); + } + + dispose() { + /** noop */ + } +} + +suite('Python envs locator - Environments Collection', async () => { + let getNativePythonFinderStub: sinon.SinonStub; + let collectionService: EnvsCollectionService; + let storage: PythonEnvInfo[]; + + const updatedName = 'updatedName'; + const pathToCondaPython = getOSType() === OSType.Windows ? 'python.exe' : path.join('bin', 'python'); + const condaEnvWithoutPython = createEnv( + 'python', + undefined, + undefined, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython'), + PythonEnvKind.Conda, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython', pathToCondaPython), + ); + const condaEnvWithPython = createEnv( + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython', pathToCondaPython), + undefined, + undefined, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython'), + PythonEnvKind.Conda, + path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython', 'condaLackingPython', pathToCondaPython), + ); + + function applyChangeEventToEnvList(envs: PythonEnvInfo[], event: PythonEnvCollectionChangedEvent) { + const env = event.old ?? event.new; + let envIndex = -1; + if (env) { + envIndex = envs.findIndex((item) => item.executable.filename === env.executable.filename); + } + if (event.new) { + if (envIndex === -1) { + envs.push(event.new); + } else { + envs[envIndex] = event.new; + } + } + if (envIndex !== -1 && event.new === undefined) { + envs.splice(envIndex, 1); + } + return envs; + } + + function createEnv( + executable: string, + searchLocation?: Uri, + name?: string, + location?: string, + kind?: PythonEnvKind, + id?: string, + ) { + const env = buildEnvInfo({ executable, searchLocation, name, location, kind }); + env.id = id ?? env.id; + env.version.major = 3; + env.version.minor = 10; + env.version.micro = 10; + return env; + } + + function getLocatorEnvs() { + const env1 = createEnv(path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe')); + const env2 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), + Uri.file(TEST_LAYOUT_ROOT), + ); + const env3 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe'), + ); + const env4 = createEnv(path.join(TEST_LAYOUT_ROOT, 'virtualhome', '.venvs', 'win1', 'python.exe')); // Path is valid but it's an invalid env + return [env1, env2, env3, env4]; + } + + function getValidCachedEnvs() { + const cachedEnvForWorkspace = createEnv( + path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1', 'win1', 'python.exe'), + Uri.file(path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1')), + ); + const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const envCached1 = createEnv(path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', 'python.exe')); + const envCached2 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), + Uri.file(TEST_LAYOUT_ROOT), + ); + const envCached3 = condaEnvWithoutPython; + return [cachedEnvForWorkspace, envCached1, envCached2, envCached3]; + } + + function getCachedEnvs() { + const envCached3 = createEnv(path.join(TEST_LAYOUT_ROOT, 'doesNotExist')); // Invalid path, should not be reported. + return [...getValidCachedEnvs(), envCached3]; + } + + function getExpectedEnvs() { + const cachedEnvForWorkspace = createEnv( + path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1', 'win1', 'python.exe'), + Uri.file(path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1')), + ); + const env1 = createEnv(path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), undefined, updatedName); + const env2 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project1', '.venv', 'Scripts', 'python.exe'), + Uri.file(TEST_LAYOUT_ROOT), + updatedName, + ); + const env3 = createEnv( + path.join(TEST_LAYOUT_ROOT, 'pyenv2', '.pyenv', 'pyenv-win', 'versions', '3.6.9', 'bin', 'python.exe'), + undefined, + updatedName, + ); + // Do not include cached envs which were not yielded by the locator, unless it belongs to some workspace. + return [cachedEnvForWorkspace, env1, env2, env3]; + } + + setup(async () => { + getNativePythonFinderStub = sinon.stub(nativeFinder, 'getNativePythonFinder'); + getNativePythonFinderStub.returns(new MockNativePythonFinder()); + storage = []; + const parentLocator = new SimpleLocator(getLocatorEnvs()); + const cache = await createCollectionCache({ + get: () => getCachedEnvs(), + store: async (envs) => { + storage = envs; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + }); + + teardown(async () => { + await deleteFile(condaEnvWithPython.executable.filename); // Restore to the original state + sinon.restore(); + }); + + test('getEnvs() returns valid envs from cache', () => { + const envs = collectionService.getEnvs(); + assertEnvsEqual(envs, getValidCachedEnvs()); + }); + + test('getEnvs() uses query to filter envs before returning', () => { + // Only query for environments which are not under any roots + const envs = collectionService.getEnvs({ searchLocations: { roots: [] } }); + assertEnvsEqual( + envs, + getValidCachedEnvs().filter((e) => !e.searchLocation), + ); + }); + + test('If `ifNotTriggerredAlready` option is set and a refresh for query is already triggered, triggerRefresh() does not trigger a refresh', async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + let refreshTriggerCount = 0; + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + refreshTriggerCount += 1; + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => getCachedEnvs(), + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + await collectionService.triggerRefresh(undefined); + await collectionService.triggerRefresh(undefined, { ifNotTriggerredAlready: true }); + expect(refreshTriggerCount).to.equal(1, 'Refresh should not be triggered in case 1'); + await collectionService.triggerRefresh({ searchLocations: { roots: [] } }, { ifNotTriggerredAlready: true }); + expect(refreshTriggerCount).to.equal(1, 'Refresh should not be triggered in case 2'); + await collectionService.triggerRefresh(undefined); + expect(refreshTriggerCount).to.equal(2, 'Refresh should be triggered in case 3'); + }); + + test('Ensure correct events are fired when collection changes on refresh', async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + + let envs = cachedEnvs; + // Ensure when all the events are applied to the original list in sequence, the final list is as expected. + events.forEach((e) => { + envs = applyChangeEventToEnvList(envs, e); + }); + const expected = getExpectedEnvs(); + assertEnvsEqual(envs, expected); + }); + + test("Ensure update events are not fired if an environment isn't actually updated", async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + let events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + expect(events.length).to.not.equal(0, 'Atleast event should be fired'); + const envs = collectionService.getEnvs(); + + // Trigger a refresh again. + events = []; + await collectionService.triggerRefresh(); + // Filter out the events which are related to envs in the cache, we expect no such events to be fired as no + // envs were updated. + events = events.filter((e) => + envs.some((env) => { + const eventEnv = e.old ?? e.new; + if (!eventEnv) { + return true; + } + return areSameEnv(eventEnv, env); + }), + ); + expect(events.length).to.equal(0, 'Do not fire additional events as envs have not updated'); + }); + + test('triggerRefresh() refreshes the collection with any new envs & removes cached envs if not relevant', async () => { + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const parentLocator = new SimpleLocator(locatedEnvs, { + onUpdated: onUpdated.event, + after: async () => { + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await collectionService.triggerRefresh(); + + let envs = cachedEnvs; + // Ensure when all the events are applied to the original list in sequence, the final list is as expected. + events.forEach((e) => { + envs = applyChangeEventToEnvList(envs, e); + }); + const expected = getExpectedEnvs(); + assertEnvsEqual(envs, expected); + const queriedEnvs = collectionService.getEnvs(); + assertEnvsEqual(queriedEnvs, expected); + assertEnvsEqual(storage, expected); + }); + + test('Ensure progress stage updates are emitted correctly and refresh promises correct track promise for each stage', async () => { + // Arrange + const onUpdated = new EventEmitter<PythonEnvUpdatedEvent | ProgressNotificationEvent>(); + const locatedEnvs = getLocatorEnvs(); + const cachedEnvs = getCachedEnvs(); + const waitUntilEventVerified = createDeferred<void>(); + const waitForAllPathsDiscoveredEvent = createDeferred<void>(); + const parentLocator = new SimpleLocator(locatedEnvs, { + before: async () => { + onUpdated.fire({ stage: ProgressReportStage.discoveryStarted }); + }, + onUpdated: onUpdated.event, + after: async () => { + onUpdated.fire({ stage: ProgressReportStage.allPathsDiscovered }); + waitForAllPathsDiscoveredEvent.resolve(); + await waitUntilEventVerified.promise; + locatedEnvs.forEach((env, index) => { + const update = cloneDeep(env); + update.name = updatedName; + onUpdated.fire({ index, update }); + }); + onUpdated.fire({ index: locatedEnvs.length - 1, update: undefined }); + // It turns out the last env is invalid, ensure it does not appear in the final result. + onUpdated.fire({ stage: ProgressReportStage.discoveryFinished }); + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async (e) => { + storage = e; + }, + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + let stage: ProgressReportStage | undefined; + collectionService.onProgress((e) => { + stage = e.stage; + }); + + // Act + const discoveryPromise = collectionService.triggerRefresh(); + + // Verify stages and refresh promises + expect(stage).to.equal(ProgressReportStage.discoveryStarted, 'Discovery should already be started'); + let refreshPromise = collectionService.getRefreshPromise({ + stage: ProgressReportStage.discoveryStarted, + }); + expect(refreshPromise).to.equal(undefined); + refreshPromise = collectionService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + expect(refreshPromise).to.not.equal(undefined); + const allPathsDiscoveredPromise = createDeferredFromPromise(refreshPromise!); + refreshPromise = collectionService.getRefreshPromise({ stage: ProgressReportStage.discoveryFinished }); + expect(refreshPromise).to.not.equal(undefined); + const discoveryFinishedPromise = createDeferredFromPromise(refreshPromise!); + + expect(allPathsDiscoveredPromise.resolved).to.equal(false); + await waitForAllPathsDiscoveredEvent.promise; // Wait for all paths to be discovered. + expect(stage).to.equal(ProgressReportStage.allPathsDiscovered); + expect(allPathsDiscoveredPromise.resolved).to.equal(true); + waitUntilEventVerified.resolve(); + + await discoveryPromise; + expect(stage).to.equal(ProgressReportStage.discoveryFinished); + expect(discoveryFinishedPromise.resolved).to.equal( + true, + 'Any previous refresh promises should be resolved when refresh is over', + ); + expect(collectionService.getRefreshPromise()).to.equal( + undefined, + 'Should be undefined if no refresh is currently going on', + ); + + // Test stage when query is provided. + collectionService.onProgress((e) => { + if (e.stage === ProgressReportStage.allPathsDiscovered) { + assert(false, 'All paths discovered event should not be fired if a query is provided'); + } + }); + collectionService + .triggerRefresh({ searchLocations: { roots: [], doNotIncludeNonRooted: true } }) + .ignoreErrors(); + refreshPromise = collectionService.getRefreshPromise({ stage: ProgressReportStage.allPathsDiscovered }); + expect(refreshPromise).to.equal(undefined, 'All paths discovered stage not applicable if a query is provided'); + }); + + test('resolveEnv() uses cache if complete and up to date info is available', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const cachedEnvs = getCachedEnvs(); + const env = cachedEnvs[0]; + env.executable.ctime = 100; + env.executable.mtime = 100; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + const parentLocator = new SimpleLocator([], { + resolve: async (e: any) => { + if (env.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(env.executable.filename); + assertEnvEqual(resolved, env); + }); + + test('resolveEnv() does not use cache if complete info is not available', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const deferred = createDeferred<void>(); + const waitDeferred = createDeferred<void>(); + const locatedEnvs = getLocatorEnvs(); + const env = locatedEnvs[0]; + env.executable.ctime = 100; + env.executable.mtime = 100; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + const parentLocator = new SimpleLocator(locatedEnvs, { + after: async () => { + waitDeferred.resolve(); + await deferred.promise; + }, + resolve: async (e: any) => { + if (env.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => [], + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + collectionService.triggerRefresh().ignoreErrors(); + await waitDeferred.promise; // Cache should already contain `env` at this point, although it is not complete. + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(env.executable.filename); + assertEnvEqual(resolved, resolvedViaLocator); + }); + + test('resolveEnv() uses underlying locator if cache does not have up to date info for env', async () => { + const cachedEnvs = getCachedEnvs(); + const env = cachedEnvs[0]; + const resolvedViaLocator = buildEnvInfo({ + executable: env.executable.filename, + sysPrefix: 'Resolved via locator', + }); + env.executable.ctime = 101; + env.executable.mtime = 90; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + const parentLocator = new SimpleLocator([], { + resolve: async (e: any) => { + if (env.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(env.executable.filename); + assertEnvEqual(resolved, resolvedViaLocator); + }); + + test('resolveEnv() adds env to cache after resolving using downstream locator', async () => { + const resolvedViaLocator = buildEnvInfo({ executable: 'Resolved via locator' }); + const parentLocator = new SimpleLocator([], { + resolve: async (e: any) => { + if (resolvedViaLocator.executable.filename === e.executable.filename) { + return resolvedViaLocator; + } + return undefined; + }, + }); + const cache = await createCollectionCache({ + get: () => [], + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const resolved = await collectionService.resolveEnv(resolvedViaLocator.executable.filename); + const envs = collectionService.getEnvs(); + assertEnvsEqual(envs, [resolved]); + }); + + test('resolveEnv() uses underlying locator once conda envs without python get a python installed', async () => { + const cachedEnvs = [condaEnvWithoutPython]; + const parentLocator = new SimpleLocator( + [], + { + resolve: async (e) => { + if (condaEnvWithoutPython.location === (e as string)) { + return condaEnvWithPython; + } + return undefined; + }, + }, + { resolveAsString: true }, + ); + const cache = await createCollectionCache({ + get: () => cachedEnvs, + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + let resolved = await collectionService.resolveEnv(condaEnvWithoutPython.location); + assertEnvEqual(resolved, condaEnvWithoutPython); // Ensure cache is used to resolve such envs. + + condaEnvWithPython.executable.ctime = 100; + condaEnvWithPython.executable.mtime = 100; + sinon.stub(externalDependencies, 'getFileInfo').resolves({ ctime: 100, mtime: 100 }); + + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + await createFile(condaEnvWithPython.executable.filename); // Install Python into the env + + resolved = await collectionService.resolveEnv(condaEnvWithoutPython.location); + assertEnvEqual(resolved, condaEnvWithPython); // Ensure it resolves latest info. + + // Verify conda env without python in cache is replaced with updated info. + const envs = collectionService.getEnvs(); + assertEnvsEqual(envs, [condaEnvWithPython]); + + expect(events.length).to.equal(1, 'Update event should be fired'); + }); + + test('Ensure events from downstream locators do not trigger new refreshes if a refresh is already scheduled', async () => { + const refreshDeferred = createDeferred(); + let refreshCount = 0; + const parentLocator = new SimpleLocator([], { + after: () => { + refreshCount += 1; + return refreshDeferred.promise; + }, + }); + const cache = await createCollectionCache({ + get: () => [], + store: async () => noop(), + }); + collectionService = new EnvsCollectionService(cache, parentLocator, false); + const events: PythonEnvCollectionChangedEvent[] = []; + collectionService.onChanged((e) => { + events.push(e); + }); + + const downstreamEvents = [ + { type: FileChangeType.Created, searchLocation: Uri.file('folder1s') }, + { type: FileChangeType.Changed }, + { type: FileChangeType.Deleted, kind: PythonEnvKind.Venv }, + { type: FileChangeType.Deleted, kind: PythonEnvKind.VirtualEnv }, + ]; // Total of 4 events + await Promise.all( + downstreamEvents.map(async (event) => { + parentLocator.fire(event); + await sleep(1); // Wait for refreshes to be initialized via change events + }), + ); + + refreshDeferred.resolve(); + await sleep(1); + + await collectionService.getRefreshPromise(); // Wait for refresh to finish + + /** + * We expect 2 refreshes to be triggered in total, explanation: + * * First event triggers a refresh. + * * Second event schedules a refresh to happen once the first refresh is finished. + * * Third event is received. A fresh refresh is already scheduled to take place so no need to schedule another one. + * * Same with the fourth event. + */ + expect(refreshCount).to.equal(2); + expect(events.length).to.equal(downstreamEvents.length, 'All 4 events should also be fired by the collection'); + assert.deepStrictEqual( + events.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), + downstreamEvents.sort((a, b) => (a.type && b.type ? a.type?.localeCompare(b.type) : 0)), + ); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts new file mode 100644 index 000000000000..a7f44abbbf94 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/envsReducer.unit.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import { PythonEnvsReducer } from '../../../../../client/pythonEnvironments/base/locators/composite/envsReducer'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv, getEnvs, getEnvsWithUpdates, SimpleLocator } from '../../common'; +import { + BasicEnvInfo, + ProgressReportStage, + isProgressEvent, +} from '../../../../../client/pythonEnvironments/base/locator'; +import { createDeferred } from '../../../../../client/common/utils/async'; + +suite('Python envs locator - Environments Reducer', () => { + suite('iterEnvs()', () => { + test('Iterator only yields unique environments', async () => { + const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createBasicEnv(PythonEnvKind.Conda, path.join('path', 'to', 'exec2')); + const env3 = createBasicEnv(PythonEnvKind.System, path.join('path', 'to', 'exec3')); + const env4 = createBasicEnv(PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 + const env5 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); // Same as env1 + const environmentsToBeIterated = [env1, env2, env3, env4, env5]; // Contains 3 unique environments + const parentLocator = new SimpleLocator(environmentsToBeIterated); + const reducer = new PythonEnvsReducer(parentLocator); + + const iterator = reducer.iterEnvs(); + const envs = await getEnvs(iterator); + + const expected = [env1, env2, env3]; + assertBasicEnvsEqual(envs, expected); + }); + + test('Updates are applied correctly', async () => { + const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createBasicEnv(PythonEnvKind.System, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.PathEnvVar, + ]); + const env3 = createBasicEnv(PythonEnvKind.Conda, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.WindowsRegistry, + ]); // Same as env2 + const env4 = createBasicEnv(PythonEnvKind.Unknown, path.join('path', 'to', 'exec2')); // Same as env2 + const env5 = createBasicEnv(PythonEnvKind.Poetry, path.join('path', 'to', 'exec1')); // Same as env1 + const env6 = createBasicEnv(PythonEnvKind.VirtualEnv, path.join('path', 'to', 'exec1')); // Same as env1 + const environmentsToBeIterated = [env1, env2, env3, env4, env5, env6]; // Contains 3 unique environments + const parentLocator = new SimpleLocator(environmentsToBeIterated); + const reducer = new PythonEnvsReducer(parentLocator); + + const iterator = reducer.iterEnvs(); + const envs = await getEnvsWithUpdates(iterator); + + const expected = [ + createBasicEnv(PythonEnvKind.Poetry, path.join('path', 'to', 'exec1')), + createBasicEnv(PythonEnvKind.Conda, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.PathEnvVar, + PythonEnvSource.WindowsRegistry, + ]), + ]; + assertBasicEnvsEqual(envs, expected); + }); + + test('Ensure progress updates are emitted correctly', async () => { + // Arrange + const env1 = createBasicEnv(PythonEnvKind.Venv, path.join('path', 'to', 'exec1')); + const env2 = createBasicEnv(PythonEnvKind.System, path.join('path', 'to', 'exec2'), [ + PythonEnvSource.PathEnvVar, + ]); + const envsReturnedByParentLocator = [env1, env2]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const reducer = new PythonEnvsReducer(parentLocator); + + // Act + const iterator = reducer.iterEnvs(); + let stage: ProgressReportStage | undefined; + let waitForProgressEvent = createDeferred<void>(); + iterator.onUpdated!(async (event) => { + if (isProgressEvent(event)) { + stage = event.stage; + waitForProgressEvent.resolve(); + } + }); + // Act + let result = await iterator.next(); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryStarted); + + // Act + waitForProgressEvent = createDeferred<void>(); + while (!result.done) { + // Once all envs are iterated, discovery should be finished. + result = await iterator.next(); + } + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryFinished); + }); + }); + + test('onChanged fires iff onChanged from locator manager fires', () => { + const parentLocator = new SimpleLocator([]); + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const expected = [event1, event2]; + const reducer = new PythonEnvsReducer(parentLocator); + + const events: PythonEnvsChangedEvent[] = []; + reducer.onChanged((e) => events.push(e)); + + parentLocator.fire(event1); + parentLocator.fire(event2); + + assert.deepEqual(events, expected); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts new file mode 100644 index 000000000000..0d189da35282 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/envsResolver.unit.test.ts @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import { cloneDeep } from 'lodash'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { EventEmitter, Uri } from 'vscode'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { IDisposableRegistry } from '../../../../../client/common/types'; +import { Architecture } from '../../../../../client/common/utils/platform'; +import * as platformApis from '../../../../../client/common/utils/platform'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvType, + PythonVersion, + UNKNOWN_PYTHON_VERSION, +} from '../../../../../client/pythonEnvironments/base/info'; +import { getEmptyVersion, parseVersion } from '../../../../../client/pythonEnvironments/base/info/pythonVersion'; +import { + BasicEnvInfo, + isProgressEvent, + ProgressNotificationEvent, + ProgressReportStage, + PythonEnvUpdatedEvent, +} from '../../../../../client/pythonEnvironments/base/locator'; +import { PythonEnvsResolver } from '../../../../../client/pythonEnvironments/base/locators/composite/envsResolver'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { + getEnvironmentInfoService, + IEnvironmentInfoService, +} from '../../../../../client/pythonEnvironments/base/info/environmentInfoService'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertEnvEqual, assertEnvsEqual } from '../envTestUtils'; +import { createBasicEnv, getEnvs, getEnvsWithUpdates, SimpleLocator } from '../../common'; +import { getOSType, OSType } from '../../../../common'; +import { CondaInfo } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { createDeferred } from '../../../../../client/common/utils/async'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Python envs locator - Environments Resolver', () => { + let envInfoService: IEnvironmentInfoService; + let disposables: IDisposableRegistry; + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + + setup(() => { + disposables = []; + envInfoService = getEnvironmentInfoService(disposables); + }); + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + /** + * Returns the expected environment to be returned by Environment info service + */ + function createExpectedEnvInfo( + env: PythonEnvInfo, + expectedDisplay: string, + expectedDetailedDisplay: string, + ): PythonEnvInfo { + const updatedEnv = cloneDeep(env); + updatedEnv.version = { + ...parseVersion('3.8.3-final'), + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + }; + updatedEnv.executable.filename = env.executable.filename; + updatedEnv.executable.sysPrefix = 'path'; + updatedEnv.arch = Architecture.x64; + updatedEnv.display = expectedDisplay; + updatedEnv.detailedDisplayName = expectedDetailedDisplay; + updatedEnv.identifiedUsingNativeLocator = updatedEnv.identifiedUsingNativeLocator ?? undefined; + updatedEnv.pythonRunCommand = updatedEnv.pythonRunCommand ?? undefined; + if (env.kind === PythonEnvKind.Conda) { + env.type = PythonEnvType.Conda; + } + return updatedEnv; + } + + function createExpectedResolvedEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + display: string | undefined = undefined, + type?: PythonEnvType, + detailedDisplay?: string, + ): PythonEnvInfo { + return { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display, + detailedDisplayName: detailedDisplay ?? display, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: Uri.file(location), + source: [], + type, + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + } + suite('iterEnvs()', () => { + let stubShellExec: sinon.SinonStub; + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + stubShellExec = sinon.stub(externalDependencies, 'shellExecute'); + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Iterator yields environments after resolving basic envs received from parent iterator', async () => { + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const resolvedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + "Python ('win1')", + PythonEnvType.Virtual, + "Python ('win1': venv)", + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + const envs = await getEnvs(iterator); + + assertEnvsEqual(envs, [resolvedEnvReturnedByBasicResolver]); + }); + + test('Updates for environments are sent correctly followed by the null event', async () => { + // Arrange + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const resolvedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + const envs = await getEnvsWithUpdates(iterator); + + assertEnvsEqual(envs, [ + createExpectedEnvInfo( + resolvedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), + ]); + }); + + test('If fetching interpreter info fails, it is not reported in the final list of envs', async () => { + // Arrange + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: '', + }); + }), + ); + // Arrange + const env1 = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const envsReturnedByParentLocator = [env1]; + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + // Act + const iterator = resolver.iterEnvs(); + const envs = await getEnvsWithUpdates(iterator); + + // Assert + assertEnvsEqual(envs, []); + }); + + test('Updates to environments from the incoming iterator are applied properly', async () => { + // Arrange + const env = createBasicEnv( + PythonEnvKind.Unknown, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const updatedEnv = createBasicEnv( + PythonEnvKind.VirtualEnv, // Ensure this type is discarded. + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const resolvedUpdatedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, + ); + const envsReturnedByParentLocator = [env]; + const didUpdate = new EventEmitter<PythonEnvUpdatedEvent<BasicEnvInfo> | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator, { + onUpdated: didUpdate.event, + }); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + // Act + const iterator = resolver.iterEnvs(); + const iteratorUpdateCallback = () => { + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + didUpdate.fire({ index: 0, old: env, update: updatedEnv }); + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); // It is essential for the incoming iterator to fire event signifying it's done + }; + const envs = await getEnvsWithUpdates(iterator, iteratorUpdateCallback); + + // Assert + assertEnvsEqual(envs, [ + createExpectedEnvInfo( + resolvedUpdatedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), + ]); + didUpdate.dispose(); + }); + + test('Ensure progress updates are emitted correctly', async () => { + // Arrange + const shellExecDeferred = createDeferred<void>(); + stubShellExec.reset(); + stubShellExec.returns( + shellExecDeferred.promise.then( + () => + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ), + ); + const env = createBasicEnv( + PythonEnvKind.Venv, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const updatedEnv = createBasicEnv( + PythonEnvKind.Poetry, + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + ); + const envsReturnedByParentLocator = [env]; + const didUpdate = new EventEmitter<PythonEnvUpdatedEvent<BasicEnvInfo> | ProgressNotificationEvent>(); + const parentLocator = new SimpleLocator<BasicEnvInfo>(envsReturnedByParentLocator, { + onUpdated: didUpdate.event, + }); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const iterator = resolver.iterEnvs(); + let stage: ProgressReportStage | undefined; + let waitForProgressEvent = createDeferred<void>(); + iterator.onUpdated!(async (event) => { + if (isProgressEvent(event)) { + stage = event.stage; + waitForProgressEvent.resolve(); + } + }); + // Act + let result = await iterator.next(); + while (!result.done) { + result = await iterator.next(); + } + didUpdate.fire({ stage: ProgressReportStage.discoveryStarted }); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryStarted); + + // Act + waitForProgressEvent = createDeferred<void>(); + didUpdate.fire({ index: 0, old: env, update: updatedEnv }); + didUpdate.fire({ stage: ProgressReportStage.discoveryFinished }); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.allPathsDiscovered); + + // Act + waitForProgressEvent = createDeferred<void>(); + shellExecDeferred.resolve(); + await waitForProgressEvent.promise; + // Assert + expect(stage).to.equal(ProgressReportStage.discoveryFinished); + didUpdate.dispose(); + }); + }); + + test('onChanged fires iff onChanged from resolver fires', () => { + const parentLocator = new SimpleLocator([]); + const event1: PythonEnvsChangedEvent = {}; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown }; + const expected = [event1, event2]; + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const events: PythonEnvsChangedEvent[] = []; + resolver.onChanged((e) => events.push(e)); + + parentLocator.fire(event1); + parentLocator.fire(event2); + + assert.deepEqual(events, expected); + }); + + suite('resolveEnv()', () => { + let stubShellExec: sinon.SinonStub; + const envsWithoutPython = path.join(TEST_LAYOUT_ROOT, 'envsWithoutPython'); + function condaInfo(condaPrefix: string): CondaInfo { + return { + conda_version: '4.8.0', + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + root_prefix: '/some/prefix', + envs: [condaPrefix], + envs_dirs: [path.dirname(condaPrefix)], + }; + } + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + stubShellExec = sinon.stub(externalDependencies, 'shellExecute'); + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stdout: + '{"versionInfo": [3, 8, 3, "final", 0], "sysPrefix": "path", "sysVersion": "3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]", "is64Bit": true}', + }); + }), + ); + sinon.stub(workspaceApis, 'getWorkspaceFolderPaths').returns([testVirtualHomeDir]); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Calls into basic resolver to get environment info, then calls environnment service to resolve environment further and return it', async function () { + if (getOSType() !== OSType.Windows) { + this.skip(); + } + const resolvedEnvReturnedByBasicResolver = createExpectedResolvedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + undefined, + PythonEnvType.Virtual, + ); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')); + + assertEnvEqual( + expected, + createExpectedEnvInfo( + resolvedEnvReturnedByBasicResolver, + "Python 3.8.3 ('win1')", + "Python 3.8.3 ('win1': venv)", + ), + ); + }); + + test('Resolver should return empty version info for envs lacking an interpreter', async function () { + if (getOSType() !== OSType.Windows) { + this.skip(); + } + sinon.stub(externalDependencies, 'getPythonSetting').withArgs('condaPath').returns('conda'); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { + return { stdout: JSON.stringify(condaInfo(path.join(envsWithoutPython, 'condaLackingPython'))) }; + } + throw new Error(`${command} is missing or is not executable`); + }); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(envsWithoutPython, 'condaLackingPython')); + + assert.deepEqual(expected?.version, getEmptyVersion()); + assert.equal(expected?.display, "Python ('condaLackingPython')"); + assert.equal(expected?.detailedDisplayName, "Python ('condaLackingPython': conda)"); + }); + + test('If running interpreter info throws error, return undefined', async () => { + stubShellExec.returns( + new Promise<ExecutionResult<string>>((_resolve, reject) => { + reject(); + }), + ); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')); + + assert.deepEqual(expected, undefined); + }); + + test('If parsing interpreter info fails, return undefined', async () => { + stubShellExec.returns( + new Promise<ExecutionResult<string>>((resolve) => { + resolve({ + stderr: 'Kaboom', + stdout: '', + }); + }), + ); + const parentLocator = new SimpleLocator([]); + const resolver = new PythonEnvsResolver(parentLocator, envInfoService); + + const expected = await resolver.resolveEnv(path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')); + + assert.deepEqual(expected, undefined); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts new file mode 100644 index 000000000000..22b2f0c01304 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/composite/resolverUtils.unit.test.ts @@ -0,0 +1,661 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as winreg from '../../../../../client/pythonEnvironments/common/windowsRegistry'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformApis from '../../../../../client/common/utils/platform'; +import { + PythonEnvInfo, + PythonEnvKind, + PythonEnvSource, + PythonEnvType, + PythonVersion, + UNKNOWN_PYTHON_VERSION, +} from '../../../../../client/pythonEnvironments/base/info'; +import { buildEnvInfo, setEnvDisplayString } from '../../../../../client/pythonEnvironments/base/info/env'; +import { InterpreterInformation } from '../../../../../client/pythonEnvironments/base/info/interpreter'; +import { parseVersion } from '../../../../../client/pythonEnvironments/base/info/pythonVersion'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertEnvEqual } from '../envTestUtils'; +import { Architecture } from '../../../../../client/common/utils/platform'; +import { + AnacondaCompanyName, + CondaInfo, +} from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { resolveBasicEnv } from '../../../../../client/pythonEnvironments/base/locators/composite/resolverUtils'; +import * as workspaceApis from '../../../../../client/common/vscodeApis/workspaceApis'; + +suite('Resolver Utils', () => { + let getWorkspaceFolders: sinon.SinonStub; + setup(() => { + sinon.stub(externalDependencies, 'getPythonSetting').withArgs('condaPath').returns('conda'); + getWorkspaceFolders = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); + getWorkspaceFolders.returns([]); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('Pyenv', () => { + const testPyenvRoot = path.join(TEST_LAYOUT_ROOT, 'pyenvhome', '.pyenv'); + const testPyenvVersionsDir = path.join(testPyenvRoot, 'versions'); + setup(() => { + sinon.stub(platformApis, 'getEnvironmentVariable').withArgs('PYENV_ROOT').returns(testPyenvRoot); + }); + + teardown(() => { + sinon.restore(); + }); + function getExpectedPyenvInfo1(): PythonEnvInfo | undefined { + const envInfo = buildEnvInfo({ + kind: PythonEnvKind.Pyenv, + executable: path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python'), + version: { + major: 3, + minor: 9, + micro: 0, + }, + source: [], + }); + envInfo.location = path.join(testPyenvVersionsDir, '3.9.0'); + envInfo.name = '3.9.0'; + setEnvDisplayString(envInfo); + return envInfo; + } + + function getExpectedPyenvInfo2(): PythonEnvInfo | undefined { + const envInfo = buildEnvInfo({ + kind: PythonEnvKind.Pyenv, + executable: path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python'), + version: { + major: 3, + minor: 7, + micro: -1, + }, + source: [], + org: 'miniconda3', + type: PythonEnvType.Conda, + }); + envInfo.location = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12'); + envInfo.name = 'base'; + setEnvDisplayString(envInfo); + return envInfo; + } + + test('resolveEnv', async () => { + const executablePath = path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python'); + const expected = getExpectedPyenvInfo1(); + + const actual = await resolveBasicEnv({ executablePath, kind: PythonEnvKind.Pyenv }); + assertEnvEqual(actual, expected); + }); + + test('resolveEnv (base conda env)', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + const executablePath = path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python'); + const expected = getExpectedPyenvInfo2(); + + const actual = await resolveBasicEnv({ executablePath, kind: PythonEnvKind.Pyenv }); + assertEnvEqual(actual, expected); + }); + }); + + suite('Microsoft store', () => { + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + sinon.stub(platformApis, 'getEnvironmentVariable').withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + sinon.restore(); + }); + + function createExpectedInterpreterInfo( + executable: string, + sysVersion?: string, + sysPrefix?: string, + versionStr?: string, + ): InterpreterInformation { + let version: PythonVersion; + try { + version = parseVersion(versionStr ?? path.basename(executable)); + if (sysVersion) { + version.sysVersion = sysVersion; + } + } catch (e) { + version = UNKNOWN_PYTHON_VERSION; + } + return { + version, + arch: Architecture.x64, + executable: { + filename: executable, + sysPrefix: sysPrefix ?? '', + ctime: -1, + mtime: -1, + }, + }; + } + + test('resolveEnv', async () => { + const python38path = path.join(testStoreAppRoot, 'python3.8.exe'); + const expected: PythonEnvInfo = { + display: undefined, + searchLocation: undefined, + name: '', + location: '', + kind: PythonEnvKind.MicrosoftStore, + distro: { org: 'Microsoft' }, + source: [PythonEnvSource.PathEnvVar], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + ...createExpectedInterpreterInfo(python38path), + }; + setEnvDisplayString(expected); + + const actual = await resolveBasicEnv({ + executablePath: python38path, + kind: PythonEnvKind.MicrosoftStore, + }); + + assertEnvEqual(actual, expected); + }); + + test('resolveEnv(string): forbidden path', async () => { + const python38path = path.join(testLocalAppData, 'Program Files', 'WindowsApps', 'python3.8.exe'); + const expected: PythonEnvInfo = { + display: undefined, + searchLocation: undefined, + name: '', + location: '', + kind: PythonEnvKind.MicrosoftStore, + distro: { org: 'Microsoft' }, + source: [PythonEnvSource.PathEnvVar], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + ...createExpectedInterpreterInfo(python38path), + }; + setEnvDisplayString(expected); + + const actual = await resolveBasicEnv({ + executablePath: python38path, + kind: PythonEnvKind.MicrosoftStore, + }); + + assertEnvEqual(actual, expected); + }); + }); + + suite('Conda', () => { + const condaPrefixNonWindows = path.join(TEST_LAYOUT_ROOT, 'conda2'); + const condaPrefixWindows = path.join(TEST_LAYOUT_ROOT, 'conda1'); + const condaInfo: CondaInfo = { + conda_version: '4.8.0', + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + root_prefix: path.dirname(TEST_LAYOUT_ROOT), + envs: [], + envs_dirs: [TEST_LAYOUT_ROOT], + }; + + function expectedEnvInfo(executable: string, location: string, name: string) { + const info = buildEnvInfo({ + executable, + kind: PythonEnvKind.Conda, + org: AnacondaCompanyName, + location, + source: [], + version: UNKNOWN_PYTHON_VERSION, + fileInfo: undefined, + name, + type: PythonEnvType.Conda, + }); + setEnvDisplayString(info); + return info; + } + function createSimpleEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + ): PythonEnvInfo { + const info: PythonEnvInfo = { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display: undefined, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: undefined, + source: [], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + info.type = PythonEnvType.Conda; + setEnvDisplayString(info); + return info; + } + + teardown(() => { + sinon.restore(); + }); + + test('resolveEnv (Windows)', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { + return { stdout: JSON.stringify(condaInfo) }; + } + throw new Error(`${command} is missing or is not executable`); + }); + const actual = await resolveBasicEnv({ + executablePath: path.join(condaPrefixWindows, 'python.exe'), + envPath: condaPrefixWindows, + kind: PythonEnvKind.Conda, + }); + assertEnvEqual( + actual, + expectedEnvInfo( + path.join(condaPrefixWindows, 'python.exe'), + condaPrefixWindows, + path.basename(condaPrefixWindows), + ), + ); + }); + + test('resolveEnv (non-Windows)', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + if (command === 'conda' && args[0] === 'info' && args[1] === '--json') { + return { stdout: JSON.stringify(condaInfo) }; + } + throw new Error(`${command} is missing or is not executable`); + }); + const actual = await resolveBasicEnv({ + executablePath: path.join(condaPrefixNonWindows, 'bin', 'python'), + kind: PythonEnvKind.Conda, + envPath: condaPrefixNonWindows, + }); + assertEnvEqual( + actual, + expectedEnvInfo( + path.join(condaPrefixNonWindows, 'bin', 'python'), + condaPrefixNonWindows, + path.basename(condaPrefixNonWindows), + ), + ); + }); + + test('resolveEnv: If no conda binary found, resolve as an unknown environment', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { + throw new Error(`${command} is missing or is not executable`); + }); + const actual = await resolveBasicEnv({ + executablePath: path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), + kind: PythonEnvKind.Conda, + }); + assertEnvEqual( + actual, + createSimpleEnvInfo( + path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'), + PythonEnvKind.Unknown, + undefined, + '', + path.join(TEST_LAYOUT_ROOT, 'conda1'), + ), + ); + }); + }); + + suite('Simple envs', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + setup(() => { + getWorkspaceFolders.returns([testVirtualHomeDir]); + }); + + teardown(() => { + sinon.restore(); + }); + + function createExpectedEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + ): PythonEnvInfo { + const info: PythonEnvInfo = { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display: undefined, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: Uri.file(location), + source: [], + type: PythonEnvType.Virtual, + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + setEnvDisplayString(info); + return info; + } + + test('resolveEnv', async () => { + const expected = createExpectedEnvInfo( + path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + PythonEnvKind.Venv, + undefined, + 'win1', + path.join(testVirtualHomeDir, '.venvs', 'win1'), + ); + const actual = await resolveBasicEnv({ + executablePath: path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe'), + kind: PythonEnvKind.Venv, + }); + assertEnvEqual(actual, expected); + }); + }); + + suite('Globally-installed envs', () => { + const testPosixKnownPathsRoot = path.join(TEST_LAYOUT_ROOT, 'posixroot'); + const testLocation3 = path.join(testPosixKnownPathsRoot, 'location3'); + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + }); + + teardown(() => { + sinon.restore(); + }); + + function createExpectedEnvInfo( + interpreterPath: string, + kind: PythonEnvKind, + version: PythonVersion = UNKNOWN_PYTHON_VERSION, + name = '', + location = '', + ): PythonEnvInfo { + const info: PythonEnvInfo = { + name, + location, + kind, + executable: { + filename: interpreterPath, + sysPrefix: '', + ctime: -1, + mtime: -1, + }, + display: undefined, + version, + arch: Architecture.Unknown, + distro: { org: '' }, + searchLocation: undefined, + source: [], + identifiedUsingNativeLocator: undefined, + pythonRunCommand: undefined, + }; + setEnvDisplayString(info); + return info; + } + + test('resolveEnv', async () => { + const executable = path.join(testLocation3, 'python3.8'); + const expected = createExpectedEnvInfo(executable, PythonEnvKind.OtherGlobal, parseVersion('3.8')); + const actual = await resolveBasicEnv({ + executablePath: executable, + kind: PythonEnvKind.OtherGlobal, + }); + assertEnvEqual(actual, expected); + }); + }); + + suite('Windows registry', () => { + const regTestRoot = path.join(TEST_LAYOUT_ROOT, 'winreg'); + + const registryData = { + x64: { + HKLM: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore', '\\SOFTWARE\\Python\\ContinuumAnalytics'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9', + values: { + '': '', + DisplayName: 'Python 3.9 (64-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '64bit', + SysVersion: '3.9', + Version: '3.9.0rc2', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'py39', 'python.exe'), + }, + subKeys: [] as string[], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics', + values: { + '': '', + }, + subKeys: ['\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64'], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64', + values: { + '': '', + DisplayName: 'Anaconda py38_4.8.3', + SupportUrl: 'github.com/continuumio/anaconda-issues', + SysArchitecture: '64bit', + SysVersion: '3.8', + Version: 'py38_4.8.3', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'conda3', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + HKCU: [], + }, + x86: { + HKLM: [], + HKCU: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8', + values: { + '': '', + DisplayName: 'Python 3.8 (32-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '32bit', + SysVersion: '3.8.5', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'python38', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + }, + }; + + function fakeRegistryValues({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryValue[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const values: winreg.IRegistryValue[] = []; + for (const [name, value] of Object.entries(k.values)) { + values.push({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: k.key, + name, + type: winreg.REG_SZ, + value: value ?? '', + }); + } + return Promise.resolve(values); + } + } + return Promise.resolve([]); + } + + function fakeRegistryKeys({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryKey[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const keys = k.subKeys.map((s) => ({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: s, + })); + return Promise.resolve(keys); + } + } + return Promise.resolve([]); + } + + setup(async () => { + sinon.stub(winreg, 'readRegistryValues').callsFake(fakeRegistryValues); + sinon.stub(winreg, 'readRegistryKeys').callsFake(fakeRegistryKeys); + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + }); + + teardown(() => { + sinon.restore(); + }); + + test('If data provided by registry is more informative than kind resolvers, use it to update environment (64bit)', async () => { + const interpreterPath = path.join(regTestRoot, 'py39', 'python.exe'); + const actual = await resolveBasicEnv({ + executablePath: interpreterPath, + kind: PythonEnvKind.Unknown, + source: [PythonEnvSource.WindowsRegistry], + }); + const expected = buildEnvInfo({ + kind: PythonEnvKind.OtherGlobal, // Environment should be marked as "Global" instead of "Unknown". + executable: interpreterPath, + version: parseVersion('3.9.0rc2'), // Registry provides more complete version info. + arch: Architecture.x64, + org: 'PythonCore', + source: [PythonEnvSource.WindowsRegistry], + }); + setEnvDisplayString(expected); + expected.distro.defaultDisplayName = 'Python 3.9 (64-bit)'; + assertEnvEqual(actual, expected); + }); + + test('If data provided by registry is more informative than kind resolvers, use it to update environment (32bit)', async () => { + const interpreterPath = path.join(regTestRoot, 'python38', 'python.exe'); + const actual = await resolveBasicEnv({ + executablePath: interpreterPath, + kind: PythonEnvKind.Unknown, + source: [PythonEnvSource.WindowsRegistry, PythonEnvSource.PathEnvVar], + }); + const expected = buildEnvInfo({ + kind: PythonEnvKind.OtherGlobal, // Environment should be marked as "Global" instead of "Unknown". + executable: interpreterPath, + version: parseVersion('3.8.5'), // Registry provides more complete version info. + arch: Architecture.x86, // Provided by registry + org: 'PythonCodingPack', // Provided by registry + source: [PythonEnvSource.WindowsRegistry, PythonEnvSource.PathEnvVar], + }); + setEnvDisplayString(expected); + expected.distro.defaultDisplayName = 'Python 3.8 (32-bit)'; + assertEnvEqual(actual, expected); + }); + + test('If data provided by registry is less informative than kind resolvers, do not use it to update environment', async () => { + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string) => { + throw new Error(`${command} is missing or is not executable`); + }); + const interpreterPath = path.join(regTestRoot, 'conda3', 'python.exe'); + const actual = await resolveBasicEnv({ + executablePath: interpreterPath, + kind: PythonEnvKind.Conda, + source: [PythonEnvSource.WindowsRegistry], + }); + const expected = buildEnvInfo({ + location: path.join(regTestRoot, 'conda3'), + // Environment is not marked as Conda, update it to Global. + kind: PythonEnvKind.OtherGlobal, + executable: interpreterPath, + // Registry does not provide the minor version, so keep version provided by Conda resolver instead. + version: parseVersion('3.8.5'), + arch: Architecture.x64, // Provided by registry + org: 'ContinuumAnalytics', // Provided by registry + name: '', + source: [PythonEnvSource.WindowsRegistry], + type: PythonEnvType.Conda, + }); + setEnvDisplayString(expected); + expected.distro.defaultDisplayName = 'Anaconda py38_4.8.3'; + assertEnvEqual(actual, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/envTestUtils.ts b/src/test/pythonEnvironments/base/locators/envTestUtils.ts new file mode 100644 index 000000000000..db29575d29ba --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/envTestUtils.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { exec } from 'child_process'; +import { cloneDeep, zip } from 'lodash'; +import { promisify } from 'util'; +import * as fsapi from '../../../../client/common/platform/fs-paths'; +import { PythonEnvInfo, PythonVersion, UNKNOWN_PYTHON_VERSION } from '../../../../client/pythonEnvironments/base/info'; +import { getEmptyVersion } from '../../../../client/pythonEnvironments/base/info/pythonVersion'; +import { BasicEnvInfo } from '../../../../client/pythonEnvironments/base/locator'; + +const execAsync = promisify(exec); +export async function run(argv: string[], options?: { cwd?: string; env?: NodeJS.ProcessEnv }): Promise<void> { + const cmdline = argv.join(' '); + const { stderr } = await execAsync(cmdline, options ?? {}); + if (stderr && stderr.length > 0) { + throw Error(stderr); + } +} + +function normalizeVersion(version: PythonVersion): PythonVersion { + if (version === UNKNOWN_PYTHON_VERSION) { + version = getEmptyVersion(); + } + // Force `undefined` to be set if nothing set. + // eslint-disable-next-line no-self-assign + version.release = version.release; + // eslint-disable-next-line no-self-assign + version.sysVersion = version.sysVersion; + return version; +} + +export function assertVersionsEqual(actual: PythonVersion | undefined, expected: PythonVersion | undefined): void { + if (actual) { + actual = normalizeVersion(actual); + } + if (expected) { + expected = normalizeVersion(expected); + } + assert.deepStrictEqual(actual, expected); +} + +export async function createFile(filename: string, text = ''): Promise<string> { + await fsapi.writeFile(filename, text); + return filename; +} + +export async function deleteFile(filename: string): Promise<void> { + await fsapi.remove(filename); +} + +export function assertEnvEqual(actual: PythonEnvInfo | undefined, expected: PythonEnvInfo | undefined): void { + assert.notStrictEqual(actual, undefined); + assert.notStrictEqual(expected, undefined); + + if (actual) { + // Make sure to clone so we do not alter the original object + actual = cloneDeep(actual); + expected = cloneDeep(expected); + // No need to match these, so reset them + actual.executable.ctime = -1; + actual.executable.mtime = -1; + actual.version = normalizeVersion(actual.version); + if (expected) { + expected.executable.ctime = -1; + expected.executable.mtime = -1; + expected.version = normalizeVersion(expected.version); + delete expected.id; + } + delete actual.id; + + assert.deepStrictEqual(actual, expected); + } +} + +export function assertEnvsEqual( + actualEnvs: (PythonEnvInfo | undefined)[], + expectedEnvs: (PythonEnvInfo | undefined)[], +): void { + actualEnvs = actualEnvs.sort((a, b) => (a && b ? a.executable.filename.localeCompare(b.executable.filename) : 0)); + expectedEnvs = expectedEnvs.sort((a, b) => + a && b ? a.executable.filename.localeCompare(b.executable.filename) : 0, + ); + assert.deepStrictEqual(actualEnvs.length, expectedEnvs.length, 'Number of envs'); + zip(actualEnvs, expectedEnvs).forEach((value) => { + const [actual, expected] = value; + actual?.source.sort(); + expected?.source.sort(); + assertEnvEqual(actual, expected); + }); +} + +export function assertBasicEnvsEqual(actualEnvs: BasicEnvInfo[], expectedEnvs: BasicEnvInfo[]): void { + actualEnvs = actualEnvs + .sort((a, b) => a.executablePath.localeCompare(b.executablePath)) + .map((c) => ({ ...c, executablePath: c.executablePath.toLowerCase() })); + expectedEnvs = expectedEnvs + .sort((a, b) => a.executablePath.localeCompare(b.executablePath)) + .map((c) => ({ ...c, executablePath: c.executablePath.toLowerCase() })); + assert.deepStrictEqual(actualEnvs.length, expectedEnvs.length, 'Number of envs'); + zip(actualEnvs, expectedEnvs).forEach((value) => { + const [actual, expected] = value; + if (actual) { + actual.source = actual.source ?? []; + actual.searchLocation = actual.searchLocation ?? undefined; + actual.source.sort(); + } + if (expected) { + expected.source = expected.source ?? []; + expected.searchLocation = expected.searchLocation ?? undefined; + expected.source.sort(); + } + assert.deepStrictEqual(actual, expected); + }); +} diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts new file mode 100644 index 000000000000..b0b18fb3827e --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/activestateLocator.unit.test.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsapi from '../../../../../client/common/platform/fs-paths'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { ActiveStateLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/activeStateLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import { createBasicEnv } from '../../common'; +import * as platform from '../../../../../client/common/utils/platform'; +import { ActiveState } from '../../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { replaceAll } from '../../../../../client/common/stringUtils'; + +suite('ActiveState Locator', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + let locator: ActiveStateLocator; + + setup(() => { + locator = new ActiveStateLocator(); + + let homeDir: string; + switch (platform.getOSType()) { + case platform.OSType.Windows: + homeDir = 'C:\\Users\\user'; + break; + case platform.OSType.OSX: + homeDir = '/Users/user'; + break; + default: + homeDir = '/home/user'; + } + sinon.stub(platform, 'getUserHomeDir').returns(homeDir); + + const stateToolDir = ActiveState.getStateToolDir(); + if (stateToolDir) { + sinon.stub(fsapi, 'pathExists').callsFake((dir: string) => Promise.resolve(dir === stateToolDir)); + } + + sinon.stub(externalDependencies, 'getPythonSetting').returns(undefined); + + sinon.stub(externalDependencies, 'shellExecute').callsFake((command: string) => { + if (command === 'state projects -o editor') { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `[{"name":"test","organization":"test-org","local_checkouts":["does-not-matter"],"executables":["${replaceAll( + path.join(testActiveStateDir, 'c09080d1', 'exec'), + '\\', + '\\\\', + )}"]},{"name":"test2","organization":"test-org","local_checkouts":["does-not-matter2"],"executables":["${replaceAll( + path.join(testActiveStateDir, '2af6390a', 'exec'), + '\\', + '\\\\', + )}"]}]\n\0`, + }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + teardown(() => sinon.restore()); + + test('iterEnvs()', async () => { + const actualEnvs = await getEnvs(locator.iterEnvs()); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.ActiveState, + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + platform.getOSType() === platform.OSType.Windows ? 'python3.exe' : 'python3', + ), + ), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..3c7d4348b1c5 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.testvirtualenvs.ts @@ -0,0 +1,132 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { CondaEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/condaLocator'; +import { sleep } from '../../../../core'; +import { createDeferred, Deferred } from '../../../../../client/common/utils/async'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, TEST_TIMEOUT } from '../../../../constants'; +import { traceWarn } from '../../../../../client/logging'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { PYTHON_VIRTUAL_ENVS_LOCATION } from '../../../../ciConstants'; +import { isCI } from '../../../../../client/common/constants'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; + +class CondaEnvs { + private readonly condaEnvironmentsTxt; + + constructor() { + const home = platformUtils.getUserHomeDir(); + if (!home) { + throw new Error('Home directory not found'); + } + this.condaEnvironmentsTxt = path.join(home, '.conda', 'environments.txt'); + } + + public async create(): Promise<void> { + try { + await fs.createFile(this.condaEnvironmentsTxt); + } catch (err) { + throw new Error(`Failed to create environments.txt ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async update(): Promise<void> { + try { + await fs.writeFile(this.condaEnvironmentsTxt, 'path/to/environment'); + } catch (err) { + throw new Error(`Failed to update environments file ${this.condaEnvironmentsTxt}, Error: ${err}`); + } + } + + public async cleanUp() { + try { + await fs.remove(this.condaEnvironmentsTxt); + } catch (err) { + traceWarn(`Failed to clean up ${this.condaEnvironmentsTxt}`); + } + } +} + +suite('Conda Env Locator', async () => { + let locator: CondaEnvironmentLocator; + let condaEnvsTxt: CondaEnvs; + const envsLocation = + PYTHON_VIRTUAL_ENVS_LOCATION !== undefined + ? path.join(EXTENSION_ROOT_DIR_FOR_TESTS, PYTHON_VIRTUAL_ENVS_LOCATION) + : path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'tmp', 'envPaths.json'); + + async function waitForChangeToBeDetected(deferred: Deferred<void>) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + let envPaths: any; + + suiteSetup(async () => { + if (isCI) { + envPaths = await fs.readJson(envsLocation); + } + }); + + setup(async () => { + sinon.stub(platformUtils, 'getUserHomeDir').returns(TEST_LAYOUT_ROOT); + condaEnvsTxt = new CondaEnvs(); + await condaEnvsTxt.cleanUp(); + if (isCI) { + sinon.stub(externalDependencies, 'getPythonSetting').returns(envPaths.condaExecPath); + } + }); + + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { + locator = new CondaEnvironmentLocator(); + // Wait for watchers to get ready + await sleep(1000); + locator.onChanged(onChanged); + } + + teardown(async () => { + await condaEnvsTxt.cleanUp(); + await locator.dispose(); + sinon.restore(); + }); + + test('Fires when conda `environments.txt` file is created', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { providerId: 'conda-envs' }; + await setupLocator(async (e) => { + deferred.resolve(); + actualEvent = e; + }); + + await condaEnvsTxt.create(); + await waitForChangeToBeDetected(deferred); + + assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); + }); + + test('Fires when conda `environments.txt` file is updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { providerId: 'conda-envs' }; + await condaEnvsTxt.create(); + await setupLocator(async (e) => { + deferred.resolve(); + actualEvent = e; + }); + + await condaEnvsTxt.update(); + await waitForChangeToBeDetected(deferred); + + assert.deepEqual(actualEvent!, expectedEvent, 'Unexpected event emitted'); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts new file mode 100644 index 000000000000..605109b7a67e --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/condaLocator.unit.test.ts @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsapi from '../../../../../client/common/platform/fs-paths'; +import { PythonReleaseLevel, PythonVersion } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { getPythonVersionFromConda } from '../../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { TEST_DATA_ROOT } from '../../../common/commonTestConstants'; +import { assertVersionsEqual } from '../envTestUtils'; + +suite('Conda Python Version Parser Tests', () => { + let readFileStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + const testDataRoot = path.join(TEST_DATA_ROOT, 'versiondata', 'conda'); + + setup(() => { + readFileStub = sinon.stub(externalDeps, 'readFile'); + sinon.stub(externalDeps, 'inExperiment').returns(false); + + pathExistsStub = sinon.stub(externalDeps, 'pathExists'); + pathExistsStub.resolves(true); + }); + + teardown(() => { + sinon.restore(); + }); + + interface ICondaPythonVersionTestData { + name: string; + historyFileContents: string; + expected: PythonVersion | undefined; + } + + function getTestData(): ICondaPythonVersionTestData[] { + const data: ICondaPythonVersionTestData[] = []; + + const cases = fsapi.readdirSync(testDataRoot).map((c) => path.join(testDataRoot, c)); + const casesToVersion = new Map<string, PythonVersion>(); + casesToVersion.set('case1', { major: 3, minor: 8, micro: 5 }); + + casesToVersion.set('case2', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Alpha, serial: 1 }, + }); + + casesToVersion.set('case3', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Beta, serial: 2 }, + }); + + casesToVersion.set('case4', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Candidate, serial: 1 }, + }); + + casesToVersion.set('case5', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Candidate, serial: 2 }, + }); + + for (const c of cases) { + const name = path.basename(c); + const expected = casesToVersion.get(name); + if (expected) { + data.push({ + name, + historyFileContents: fsapi.readFileSync(c, 'utf-8'), + expected, + }); + } + } + + return data; + } + + const testData = getTestData(); + testData.forEach((data) => { + test(`Parsing ${data.name}`, async () => { + readFileStub.resolves(data.historyFileContents); + + const actual = await getPythonVersionFromConda('/path/here/does/not/matter'); + + assertVersionsEqual(actual, data.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts new file mode 100644 index 000000000000..e570c3fb72da --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator.unit.test.ts @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as helpers from '../../../../../client/common/helpers'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { + CustomVirtualEnvironmentLocator, + VENVFOLDERS_SETTING_KEY, + VENVPATH_SETTING_KEY, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/customVirtualEnvLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('CustomVirtualEnvironment Locator', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testVenvPathWithTilda = path.join('~', 'customfolder'); + let getUserHomeDirStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let locator: CustomVirtualEnvironmentLocator; + let watchLocationForPatternStub: sinon.SinonStub; + let getPythonSettingStub: sinon.SinonStub; + let onDidChangePythonSettingStub: sinon.SinonStub; + let untildify: sinon.SinonStub; + + setup(async () => { + untildify = sinon.stub(helpers, 'untildify'); + untildify.callsFake((value: string) => value.replace('~', testVirtualHomeDir)); + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + getUserHomeDirStub.returns(testVirtualHomeDir); + getPythonSettingStub = sinon.stub(externalDependencies, 'getPythonSetting'); + + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + onDidChangePythonSettingStub = sinon.stub(externalDependencies, 'onDidChangePythonSetting'); + onDidChangePythonSettingStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + const expectedDotProjectFile = path.join( + testVirtualHomeDir, + '.local', + 'share', + 'virtualenvs', + 'project2-vnNIWe9P', + '.project', + ); + readFileStub = sinon.stub(externalDependencies, 'readFile'); + readFileStub.withArgs(expectedDotProjectFile).returns(path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2')); + readFileStub.callThrough(); + + locator = new CustomVirtualEnvironmentLocator(); + }); + teardown(async () => { + await locator.dispose(); + sinon.restore(); + }); + + test('iterEnvs(): Windows with both settings set', async () => { + getPythonSettingStub.withArgs('venvPath').returns(testVenvPathWithTilda); + getPythonSettingStub.withArgs('venvFolders').returns(['.venvs', '.virtualenvs', 'Envs']); + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win2', 'bin', 'python.exe'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows with both settings set', async () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + + getPythonSettingStub.withArgs('venvPath').returns(path.join(testWorkspaceFolder, 'posix2conda')); + getPythonSettingStub + .withArgs('venvFolders') + .returns(['.venvs', '.virtualenvs', 'envs', path.join('.local', 'share', 'virtualenvs')]); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix2conda', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), + ), + createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No User home dir set', async () => { + getUserHomeDirStub.returns(undefined); + + getPythonSettingStub.withArgs('venvPath').returns(testVenvPathWithTilda); + getPythonSettingStub.withArgs('venvFolders').returns(['.venvs', '.virtualenvs', 'Envs']); + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'customfolder', 'win2', 'bin', 'python.exe'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with only venvFolders set', async () => { + getPythonSettingStub.withArgs('venvFolders').returns(['.venvs', '.virtualenvs', 'Envs']); + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with only venvPath set', async () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + + getPythonSettingStub.withArgs('venvPath').returns(path.join(testWorkspaceFolder, 'posix2conda')); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix2conda', 'python')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('onChanged fires if venvPath setting changes', async () => { + const events: PythonEnvsChangedEvent[] = []; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; + locator.onChanged((e) => events.push(e)); + + await getEnvs(locator.iterEnvs()); + const venvPathCall = onDidChangePythonSettingStub + .getCalls() + .filter((c) => c.args[0] === VENVPATH_SETTING_KEY)[0]; + const callback = venvPathCall.args[1]; + callback(); // Callback is called when venvPath setting changes + + assert.deepEqual(events, expected, 'Unexpected events'); + }); + + test('onChanged fires if venvFolders setting changes', async () => { + const events: PythonEnvsChangedEvent[] = []; + const expected: PythonEnvsChangedEvent[] = [{ providerId: locator.providerId }]; + locator.onChanged((e) => events.push(e)); + + await getEnvs(locator.iterEnvs()); + const venvFoldersCall = onDidChangePythonSettingStub + .getCalls() + .filter((c) => c.args[0] === VENVFOLDERS_SETTING_KEY)[0]; + const callback = venvFoldersCall.args[1]; + callback(); // Callback is called when venvFolders setting changes + + assert.deepEqual(events, expected, 'Unexpected events'); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.unit.test.ts new file mode 100644 index 000000000000..fc1c6927d3fe --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator.unit.test.ts @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; +import { Disposables } from '../../../../../client/common/utils/resourceLifecycle'; +import { PythonEnvInfo, PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo, IPythonEnvsIterator } from '../../../../../client/pythonEnvironments/base/locator'; +import { + FSWatcherKind, + FSWatchingLocator, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/fsWatchingLocator'; +import * as binWatcher from '../../../../../client/pythonEnvironments/common/pythonBinariesWatcher'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; + +suite('File System Watching Locator Tests', () => { + const baseDir = TEST_LAYOUT_ROOT; + const fakeDir = '/this/is/a/fake/path'; + const callback = async () => Promise.resolve(PythonEnvKind.System); + let watchLocationStub: sinon.SinonStub; + + setup(() => { + watchLocationStub = sinon.stub(binWatcher, 'watchLocationForPythonBinaries'); + watchLocationStub.resolves(new Disposables()); + }); + + teardown(() => { + sinon.restore(); + }); + + class TestWatcher extends FSWatchingLocator { + public readonly providerId: string = 'test'; + + constructor( + watcherKind: FSWatcherKind, + opts: { + envStructure?: binWatcher.PythonEnvStructure; + } = {}, + ) { + super(() => [baseDir, fakeDir], callback, opts, watcherKind); + } + + public async initialize() { + await this.initWatchers(); + } + + // eslint-disable-next-line class-methods-use-this + protected doIterEnvs(): IPythonEnvsIterator<BasicEnvInfo> { + throw new Error('Method not implemented.'); + } + + // eslint-disable-next-line class-methods-use-this + protected doResolveEnv(): Promise<PythonEnvInfo | undefined> { + throw new Error('Method not implemented.'); + } + } + + [ + binWatcher.PythonEnvStructure.Standard, + binWatcher.PythonEnvStructure.Flat, + // `undefined` means "use the default". + undefined, + ].forEach((envStructure) => { + suite(`${envStructure || 'default'} structure`, () => { + const expected = + getOSType() === OSType.Windows + ? [ + // The first one is the basename glob. + 'python.exe', + '*/python.exe', + '*/Scripts/python.exe', + ] + : [ + // The first one is the basename glob. + 'python', + '*/python', + '*/bin/python', + ]; + if (envStructure === binWatcher.PythonEnvStructure.Flat) { + while (expected.length > 1) { + expected.pop(); + } + } + + const watcherKinds = [FSWatcherKind.Global, FSWatcherKind.Workspace]; + + const opts = { + envStructure, + }; + + watcherKinds.forEach((watcherKind) => { + test(`watching ${FSWatcherKind[watcherKind]}`, async () => { + const testWatcher = new TestWatcher(watcherKind, opts); + await testWatcher.initialize(); + + // Watcher should be called for all workspace locators. For global locators it should never be called. + if (watcherKind === FSWatcherKind.Workspace) { + assert.strictEqual(watchLocationStub.callCount, expected.length); + expected.forEach((glob) => { + assert.ok(watchLocationStub.calledWithMatch(baseDir, sinon.match.any, glob)); + assert.strictEqual( + // As directory does not exist, it should not be watched. + watchLocationStub.calledWithMatch(fakeDir, sinon.match.any, glob), + false, + ); + }); + } else if (watcherKind === FSWatcherKind.Global) { + assert.ok(watchLocationStub.notCalled); + } + }); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..eb88b2c48d56 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.testvirtualenvs.ts @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { GlobalVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { testLocatorWatcher } from './watcherTestUtils'; + +suite('GlobalVirtualEnvironment Locator', async () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testWorkOnHomePath = path.join(testVirtualHomeDir, 'workonhome'); + let workonHomeOldValue: string | undefined; + suiteSetup(async function () { + // https://github.com/microsoft/vscode-python/issues/17798 + return this.skip(); + workonHomeOldValue = process.env.WORKON_HOME; + process.env.WORKON_HOME = testWorkOnHomePath; + }); + testLocatorWatcher(testWorkOnHomePath, async () => new GlobalVirtualEnvironmentLocator()); + suiteTeardown(() => { + process.env.WORKON_HOME = workonHomeOldValue; + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts new file mode 100644 index 000000000000..ede947073ea2 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvironmentLocator.unit.test.ts @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { GlobalVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/globalVirtualEnvronmentLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('GlobalVirtualEnvironment Locator', () => { + const testVirtualHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualhome'); + const testWorkOnHomePath = path.join(testVirtualHomeDir, 'workonhome'); + let getEnvVariableStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let locator: GlobalVirtualEnvironmentLocator; + let watchLocationForPatternStub: sinon.SinonStub; + const project2 = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2'); + + setup(async () => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('WORKON_HOME').returns(testWorkOnHomePath); + + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + getUserHomeDirStub.returns(testVirtualHomeDir); + + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + const expectedDotProjectFile = path.join( + testVirtualHomeDir, + '.local', + 'share', + 'virtualenvs', + 'project2-vnNIWe9P', + '.project', + ); + readFileStub = sinon.stub(externalDependencies, 'readFile'); + readFileStub.withArgs(expectedDotProjectFile).returns(project2); + readFileStub.callThrough(); + }); + teardown(async () => { + await locator.dispose(); + readFileStub.restore(); + getEnvVariableStub.restore(); + getUserHomeDirStub.restore(); + getOSTypeStub.restore(); + watchLocationForPatternStub.restore(); + }); + + test('iterEnvs(): Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'win2', 'bin', 'python.exe'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Windows (WORKON_HOME NOT set)', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win1', 'python.exe')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'win2', 'bin', 'python.exe')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win1', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'Envs', 'wrapper_win2', 'bin', 'python.exe'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows', async () => { + const pipenv = createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ); + pipenv.searchLocation = Uri.file(project2); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), + createBasicEnv(PythonEnvKind.VirtualEnv, path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), + ), + pipenv, + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): with depth set', async () => { + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.VirtualEnv, path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(1); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows (WORKON_HOME not set)', async () => { + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + const pipenv = createBasicEnv( + PythonEnvKind.Pipenv, + path.join(testVirtualHomeDir, '.local', 'share', 'virtualenvs', 'project2-vnNIWe9P', 'bin', 'python'), + ); + pipenv.searchLocation = Uri.file(project2); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix1', 'python')), + createBasicEnv(PythonEnvKind.Venv, path.join(testVirtualHomeDir, '.venvs', 'posix2', 'bin', 'python')), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, '.virtualenvs', 'posix2', 'bin', 'python'), + ), + pipenv, + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No User home dir set', async () => { + getUserHomeDirStub.returns(undefined); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): No default virtual environment dirs ', async () => { + // We can simulate that by pointing the user home dir to some random directory + getUserHomeDirStub.returns(path.join('some', 'random', 'directory')); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix1', 'python'), + ), + createBasicEnv( + PythonEnvKind.VirtualEnvWrapper, + path.join(testVirtualHomeDir, 'workonhome', 'posix2', 'bin', 'python'), + ), + ]; + + locator = new GlobalVirtualEnvironmentLocator(2); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts new file mode 100644 index 000000000000..9a2a69908f2a --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/hatchLocator.unit.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { HatchLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/hatchLocator'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv } from '../../common'; +import { makeExecHandler, projectDirs, venvDirs } from '../../../common/environmentManagers/hatch.unit.test'; + +suite('Hatch Locator', () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSType: sinon.SinonStub; + let locator: HatchLocator; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('hatch'); + getOSType = sinon.stub(platformUtils, 'getOSType'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + suiteTeardown(() => sinon.restore()); + + suite('iterEnvs()', () => { + setup(() => { + getOSType.returns(platformUtils.OSType.Linux); + }); + + interface TestArgs { + osType?: platformUtils.OSType; + pythonBin?: string; + } + + const testProj1 = async ({ osType, pythonBin = 'bin/python' }: TestArgs = {}) => { + if (osType) { + getOSType.returns(osType); + } + + locator = new HatchLocator(projectDirs.project1); + exec.callsFake(makeExecHandler(venvDirs.project1, { path: true, cwd: projectDirs.project1 })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = [createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project1.default, pythonBin))]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }; + + test('project with only the default env', () => testProj1()); + test('project with only the default env on Windows', () => + testProj1({ + osType: platformUtils.OSType.Windows, + pythonBin: 'Scripts/python.exe', + })); + + test('project with multiple defined envs', async () => { + locator = new HatchLocator(projectDirs.project2); + exec.callsFake(makeExecHandler(venvDirs.project2, { path: true, cwd: projectDirs.project2 })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.default, 'bin/python')), + createBasicEnv(PythonEnvKind.Hatch, path.join(venvDirs.project2.test, 'bin/python')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/macDefaultLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/macDefaultLocator.unit.test.ts new file mode 100644 index 000000000000..62339df7e144 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/macDefaultLocator.unit.test.ts @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as osUtils from '../../../../../client/common/utils/platform'; +import { isMacDefaultPythonPath } from '../../../../../client/pythonEnvironments/common/environmentManagers/macDefault'; + +suite('isMacDefaultPythonPath', () => { + let getOSTypeStub: sinon.SinonStub; + + setup(() => { + getOSTypeStub = sinon.stub(osUtils, 'getOSType'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testCases: { path: string; os: osUtils.OSType; expected: boolean }[] = [ + { path: '/usr/bin/python', os: osUtils.OSType.OSX, expected: true }, + { path: '/usr/bin/python', os: osUtils.OSType.Linux, expected: false }, + { path: '/usr/bin/python2', os: osUtils.OSType.OSX, expected: true }, + { path: '/usr/local/bin/python2', os: osUtils.OSType.OSX, expected: false }, + { path: '/usr/bin/python3', os: osUtils.OSType.OSX, expected: false }, + { path: '/usr/bin/python3', os: osUtils.OSType.Linux, expected: false }, + ]; + + testCases.forEach(({ path, os, expected }) => { + const testName = `If the Python path is ${path} on ${os}, it is${ + expected ? '' : ' not' + } a macOS default Python path`; + + test(testName, () => { + getOSTypeStub.returns(os); + + const result = isMacDefaultPythonPath(path); + + assert.strictEqual(result, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts new file mode 100644 index 000000000000..511597dd28db --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; +import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { MicrosoftStoreLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator'; +import { TEST_TIMEOUT } from '../../../../constants'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { traceWarn } from '../../../../../client/logging'; + +class MicrosoftStoreEnvs { + private executables: string[] = []; + + private dirs: string[] = []; + + constructor(private readonly storeAppRoot: string) {} + + public async create(version: string): Promise<string> { + const dirName = path.join(this.storeAppRoot, `PythonSoftwareFoundation.Python.${version}_qbz5n2kfra8p0`); + const filename = path.join(this.storeAppRoot, `python${version}.exe`); + try { + await fs.createFile(filename); + } catch (err) { + throw new Error(`Failed to create Windows Apps executable ${filename}, Error: ${err}`); + } + try { + await fs.mkdir(dirName); + } catch (err) { + throw new Error(`Failed to create Windows Apps directory ${dirName}, Error: ${err}`); + } + this.executables.push(filename); + this.dirs.push(dirName); + return filename; + } + + public async update(version: string): Promise<void> { + // On update microsoft store removes the directory and re-adds it. + const dirName = path.join(this.storeAppRoot, `PythonSoftwareFoundation.Python.${version}_qbz5n2kfra8p0`); + try { + await fs.rmdir(dirName); + await fs.mkdir(dirName); + } catch (err) { + throw new Error(`Failed to update Windows Apps directory ${dirName}, Error: ${err}`); + } + } + + public async cleanUp() { + await Promise.all( + this.executables.map(async (filename: string) => { + try { + await fs.remove(filename); + } catch (err) { + traceWarn(`Failed to clean up ${filename}`); + } + }), + ); + await Promise.all( + this.dirs.map(async (dir: string) => { + try { + await fs.rmdir(dir); + } catch (err) { + traceWarn(`Failed to clean up ${dir}`); + } + }), + ); + } +} + +suite('Microsoft Store Locator', async () => { + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + const windowsStoreEnvs = new MicrosoftStoreEnvs(testStoreAppRoot); + let locator: MicrosoftStoreLocator; + + const localAppDataOldValue = process.env.LOCALAPPDATA; + + async function waitForChangeToBeDetected(deferred: Deferred<void>) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + async function isLocated(executable: string): Promise<boolean> { + const items = await getEnvs(locator.iterEnvs()); + return items.some((item) => externalDeps.arePathsSame(item.executablePath, executable)); + } + + suiteSetup(async function () { + // Enable once this is done: https://github.com/microsoft/vscode-python/issues/17797 + return this.skip(); + process.env.LOCALAPPDATA = testLocalAppData; + await windowsStoreEnvs.cleanUp(); + }); + + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { + locator = new MicrosoftStoreLocator(); + await getEnvs(locator.iterEnvs()); // Force the watchers to start. + // Wait for watchers to get ready + await sleep(1000); + locator.onChanged(onChanged); + } + + teardown(async () => { + await windowsStoreEnvs.cleanUp(); + await locator.dispose(); + }); + suiteTeardown(async () => { + process.env.LOCALAPPDATA = localAppDataOldValue; + }); + + test('Detect a new environment', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { + kind: PythonEnvKind.MicrosoftStore, + type: FileChangeType.Created, + searchLocation: Uri.file(testStoreAppRoot), + }; + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + const executable = await windowsStoreEnvs.create('3.4'); + await waitForChangeToBeDetected(deferred); + const isFound = await isLocated(executable); + + assert.ok(isFound); + assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); + }); + + test('Detect when an environment has been deleted', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { + kind: PythonEnvKind.MicrosoftStore, + type: FileChangeType.Deleted, + searchLocation: Uri.file(testStoreAppRoot), + }; + const executable = await windowsStoreEnvs.create('3.4'); + // Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. + await sleep(100); + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + await windowsStoreEnvs.cleanUp(); + await waitForChangeToBeDetected(deferred); + const isFound = await isLocated(executable); + + assert.notOk(isFound); + assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); + }); + + test('Detect when an environment has been updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const expectedEvent = { + kind: PythonEnvKind.MicrosoftStore, + type: FileChangeType.Changed, + searchLocation: Uri.file(testStoreAppRoot), + }; + const executable = await windowsStoreEnvs.create('3.4'); + // Wait before the change event has been sent. If both operations occur almost simultaneously no event is sent. + await sleep(100); + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + await windowsStoreEnvs.update('3.4'); + await waitForChangeToBeDetected(deferred); + const isFound = await isLocated(executable); + + assert.ok(isFound); + assert.deepEqual(actualEvent!, expectedEvent, 'Wrong event emitted'); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.unit.test.ts new file mode 100644 index 000000000000..98d9602e9729 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator.unit.test.ts @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import { ExecutionResult } from '../../../../../client/common/process/types'; +import * as platformApis from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; +import * as externalDep from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { + getMicrosoftStorePythonExes, + MicrosoftStoreLocator, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator'; +import { getEnvs } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Microsoft Store', () => { + suite('Utils', () => { + let getEnvVarStub: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVarStub.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + getEnvVarStub.restore(); + }); + + test('Store Python Interpreters', async () => { + const expected = [ + path.join(testStoreAppRoot, 'python3.7.exe'), + path.join(testStoreAppRoot, 'python3.8.exe'), + ]; + + const actual = await getMicrosoftStorePythonExes(); + assert.deepEqual(actual, expected); + }); + }); + + suite('Locator', () => { + let stubShellExec: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + let locator: MicrosoftStoreLocator; + let watchLocationForPatternStub: sinon.SinonStub; + + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + const pathToData = new Map< + string, + { + versionInfo: (string | number)[]; + sysPrefix: string; + sysVersion: string; + is64Bit: boolean; + } + >(); + + const python383data = { + versionInfo: [3, 8, 3, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.8.3 (tags/v3.8.3:6f8c832, May 13 2020, 22:37:02) [MSC v.1924 64 bit (AMD64)]', + is64Bit: true, + }; + + const python379data = { + versionInfo: [3, 7, 9, 'final', 0], + sysPrefix: 'path', + sysVersion: '3.7.9 (tags/v3.7.9:13c94747c7, Aug 17 2020, 16:30:00) [MSC v.1900 64 bit (AMD64)]', + is64Bit: true, + }; + + pathToData.set(path.join(testStoreAppRoot, 'python3.8.exe'), python383data); + pathToData.set(path.join(testStoreAppRoot, 'python3.7.exe'), python379data); + + function createExpectedInfo(executable: string): BasicEnvInfo { + return { + executablePath: executable, + kind: PythonEnvKind.MicrosoftStore, + }; + } + + setup(async () => { + stubShellExec = sinon.stub(externalDep, 'shellExecute'); + stubShellExec.callsFake((command: string) => { + if (command.indexOf('notpython.exe') > 0) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + if (command.indexOf('python3.7.exe') > 0) { + return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python379data) }); + } + return Promise.resolve<ExecutionResult<string>>({ stdout: JSON.stringify(python383data) }); + }); + + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(testLocalAppData); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + locator = new MicrosoftStoreLocator(); + }); + + teardown(async () => { + await locator.dispose(); + sinon.restore(); + }); + + test('iterEnvs()', async () => { + const expectedEnvs = [ + createExpectedInfo(path.join(testStoreAppRoot, 'python3.7.exe')), + createExpectedInfo(path.join(testStoreAppRoot, 'python3.8.exe')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = (await getEnvs(iterator)).sort((a, b) => + a.executablePath.localeCompare(b.executablePath), + ); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts new file mode 100644 index 000000000000..b55f61c3a771 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pixiLocator.unit.test.ts @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as sinon from 'sinon'; +import * as path from 'path'; +import { PixiLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pixiLocator'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { makeExecHandler, projectDirs } from '../../../common/environmentManagers/pixi.unit.test'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { createBasicEnv } from '../../common'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Pixi Locator', () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSType: sinon.SinonStub; + let locator: PixiLocator; + let pathExistsStub: sinon.SinonStub; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('pixi'); + getOSType = sinon.stub(platformUtils, 'getOSType'); + exec = sinon.stub(externalDependencies, 'exec'); + pathExistsStub = sinon.stub(externalDependencies, 'pathExists'); + pathExistsStub.resolves(true); + }); + + suiteTeardown(() => sinon.restore()); + + suite('iterEnvs()', () => { + interface TestArgs { + projectDir: string; + osType: platformUtils.OSType; + pythonBin: string; + } + + const testProject = async ({ projectDir, osType, pythonBin }: TestArgs) => { + getOSType.returns(osType); + + locator = new PixiLocator(projectDir); + exec.callsFake(makeExecHandler({ cwd: projectDir })); + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const envPath = path.join(projectDir, '.pixi', 'envs', 'default'); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Pixi, path.join(envPath, pythonBin), undefined, envPath), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }; + + test('project with only the default env', () => + testProject({ + projectDir: projectDirs.nonWindows.path, + osType: platformUtils.OSType.Linux, + pythonBin: 'bin/python', + })); + test('project with only the default env on Windows', () => + testProject({ + projectDir: projectDirs.windows.path, + osType: platformUtils.OSType.Windows, + pythonBin: 'python.exe', + })); + + test('project with multiple environments', async () => { + getOSType.returns(platformUtils.OSType.Linux); + + exec.callsFake(makeExecHandler({ cwd: projectDirs.multiEnv.path })); + + locator = new PixiLocator(projectDirs.multiEnv.path); + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + const expectedEnvs = projectDirs.multiEnv.info.environments_info.map((info) => + createBasicEnv(PythonEnvKind.Pixi, path.join(info.prefix, 'bin/python'), undefined, info.prefix), + ); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts new file mode 100644 index 000000000000..e7982a4c4e9a --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/poetryLocator.unit.test.ts @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PoetryLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/poetryLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { ExecutionResult, ShellOptions } from '../../../../../client/common/process/types'; +import { createBasicEnv as createBasicEnvCommon } from '../../common'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; + +suite('Poetry Locator', () => { + let shellExecute: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let getOSTypeStub: sinon.SinonStub; + const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); + let locator: PoetryLocator; + + suiteSetup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('poetry'); + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + }); + + suiteTeardown(() => sinon.restore()); + + suite('Windows', () => { + const project1 = path.join(testPoetryDir, 'project1'); + + function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, + ): BasicEnvInfo { + const basicEnv = createBasicEnvCommon(kind, executablePath, source, envPath); + basicEnv.searchLocation = Uri.file(project1); + return basicEnv; + } + setup(() => { + locator = new PoetryLocator(project1); + getOSTypeStub.returns(platformUtils.OSType.Windows); + shellExecute.callsFake((command: string, options: ShellOptions) => { + if (command === 'poetry env list --full-path') { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project1)) { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `${path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8')} \n + ${path.join(testPoetryDir, 'globalwinproject-9hvDnqYw-py3.11')} (Activated)\r\n + ${path.join(testPoetryDir, 'someRandomPathWhichDoesNotExist')} `, + }); + } + } + return Promise.reject(new Error('Command failed')); + }); + }); + + test('iterEnvs()', async () => { + // Act + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + // Assert + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8', 'Scripts', 'python.exe'), + ), + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'globalwinproject-9hvDnqYw-py3.11', 'Scripts', 'python.exe'), + ), + createBasicEnv(PythonEnvKind.Poetry, path.join(project1, '.venv', 'Scripts', 'python.exe')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); + + suite('Non-Windows', () => { + const project2 = path.join(testPoetryDir, 'project2'); + + function createBasicEnv( + kind: PythonEnvKind, + executablePath: string, + source?: PythonEnvSource[], + envPath?: string, + ): BasicEnvInfo { + const basicEnv = createBasicEnvCommon(kind, executablePath, source, envPath); + basicEnv.searchLocation = Uri.file(project2); + return basicEnv; + } + setup(() => { + locator = new PoetryLocator(project2); + getOSTypeStub.returns(platformUtils.OSType.Linux); + shellExecute.callsFake((command: string, options: ShellOptions) => { + if (command === 'poetry env list --full-path') { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (cwd && externalDependencies.arePathsSame(cwd, project2)) { + return Promise.resolve<ExecutionResult<string>>({ + stdout: `${path.join(testPoetryDir, 'posix1project-9hvDnqYw-py3.4')} (Activated)\n + ${path.join(testPoetryDir, 'posix2project-6hnqYwvD-py3.7')}`, + }); + } + } + return Promise.reject(new Error('Command failed')); + }); + }); + + test('iterEnvs()', async () => { + // Act + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + // Assert + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'posix1project-9hvDnqYw-py3.4', 'python'), + ), + createBasicEnv( + PythonEnvKind.Poetry, + path.join(testPoetryDir, 'posix2project-6hnqYwvD-py3.7', 'bin', 'python'), + ), + createBasicEnv(PythonEnvKind.Poetry, path.join(project2, '.venv', 'bin', 'python')), + ]; + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.unit.test.ts new file mode 100644 index 000000000000..7a9a2bc6475d --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator.unit.test.ts @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as semver from 'semver'; +import * as executablesAPI from '../../../../../client/common/utils/exec'; +import * as osUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo } from '../../../../../client/pythonEnvironments/base/locator'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PosixKnownPathsLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/posixKnownPathsLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { isMacDefaultPythonPath } from '../../../../../client/pythonEnvironments/common/environmentManagers/macDefault'; + +suite('Posix Known Path Locator', () => { + let getPathEnvVar: sinon.SinonStub; + let locator: PosixKnownPathsLocator; + + const testPosixKnownPathsRoot = path.join(TEST_LAYOUT_ROOT, 'posixroot'); + + const testLocation1 = path.join(testPosixKnownPathsRoot, 'location1'); + const testLocation2 = path.join(testPosixKnownPathsRoot, 'location2'); + const testLocation3 = path.join(testPosixKnownPathsRoot, 'location3'); + + const testFileData: Map<string, string[]> = new Map(); + + testFileData.set(testLocation1, ['python', 'python3']); + testFileData.set(testLocation2, ['python', 'python37', 'python38']); + testFileData.set(testLocation3, ['python3.7', 'python3.8']); + + setup(async () => { + getPathEnvVar = sinon.stub(executablesAPI, 'getSearchPathEntries'); + locator = new PosixKnownPathsLocator(); + }); + teardown(() => { + sinon.restore(); + }); + + test('iterEnvs(): get python bin from known test roots', async () => { + const testLocations = [testLocation1, testLocation2, testLocation3]; + getPathEnvVar.returns(testLocations); + + const expectedEnvs: BasicEnvInfo[] = []; + testLocations.forEach((location) => { + const binaries = testFileData.get(location); + if (binaries) { + binaries.forEach((binary) => { + expectedEnvs.push({ + source: [PythonEnvSource.PathEnvVar], + ...createBasicEnv(PythonEnvKind.OtherGlobal, path.join(location, binary)), + }); + }); + } + }); + + const actualEnvs = (await getEnvs(locator.iterEnvs())).filter((e) => e.executablePath.indexOf('posixroot') > 0); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Do not return Python 2 installs when on macOS Monterey', async function () { + if (osUtils.getOSType() !== osUtils.OSType.OSX) { + this.skip(); + } + + const getOSTypeStub = sinon.stub(osUtils, 'getOSType'); + const gteStub = sinon.stub(semver, 'gte'); + + getOSTypeStub.returns(osUtils.OSType.OSX); + gteStub.returns(true); + + const actualEnvs = await getEnvs(locator.iterEnvs()); + + const globalPython2Envs = actualEnvs.filter((env) => isMacDefaultPythonPath(env.executablePath)); + + assert.strictEqual(globalPython2Envs.length, 0); + }); + + test('iterEnvs(): Return Python 2 installs when not on macOS Monterey', async function () { + if (osUtils.getOSType() !== osUtils.OSType.OSX) { + this.skip(); + } + + const getOSTypeStub = sinon.stub(osUtils, 'getOSType'); + const gteStub = sinon.stub(semver, 'gte'); + + getOSTypeStub.returns(osUtils.OSType.OSX); + gteStub.returns(false); + + const actualEnvs = await getEnvs(locator.iterEnvs()); + + const globalPython2Envs = actualEnvs.filter((env) => isMacDefaultPythonPath(env.executablePath)); + + assert.notStrictEqual(globalPython2Envs.length, 0); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..c370a8ff6da5 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.testvirtualenvs.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { PyenvLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pyenvLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { testLocatorWatcher } from './watcherTestUtils'; + +suite('Pyenv Locator', async () => { + const testPyenvRoot = path.join(TEST_LAYOUT_ROOT, 'pyenvhome', '.pyenv'); + const testPyenvVersionsDir = path.join(testPyenvRoot, 'versions'); + let pyenvRootOldValue: string | undefined; + suiteSetup(async function () { + // https://github.com/microsoft/vscode-python/issues/17798 + return this.skip(); + pyenvRootOldValue = process.env.PYENV_ROOT; + process.env.PYENV_ROOT = testPyenvRoot; + }); + testLocatorWatcher(testPyenvVersionsDir, async () => new PyenvLocator(), { kind: PythonEnvKind.Pyenv }); + suiteTeardown(() => { + process.env.PYENV_ROOT = pyenvRootOldValue; + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.unit.test.ts new file mode 100644 index 000000000000..19f8088db65f --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/pyenvLocator.unit.test.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PyenvLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/pyenvLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; + +suite('Pyenv Locator Tests', () => { + let getEnvVariableStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let locator: PyenvLocator; + let watchLocationForPatternStub: sinon.SinonStub; + + const testPyenvRoot = path.join(TEST_LAYOUT_ROOT, 'pyenvhome', '.pyenv'); + const testPyenvVersionsDir = path.join(testPyenvRoot, 'versions'); + + setup(async () => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('PYENV_ROOT').returns(testPyenvRoot); + + getOsTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOsTypeStub.returns(platformUtils.OSType.Linux); + + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + + locator = new PyenvLocator(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('iterEnvs()', async () => { + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, '3.9.0', 'bin', 'python')), + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, 'conda1', 'bin', 'python')), + + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, 'miniconda3-4.7.12', 'bin', 'python')), + createBasicEnv(PythonEnvKind.Pyenv, path.join(testPyenvVersionsDir, 'venv1', 'bin', 'python')), + ]; + + const actualEnvs = await getEnvs(locator.iterEnvs()); + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts new file mode 100644 index 000000000000..e9c7be3ec321 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/watcherTestUtils.ts @@ -0,0 +1,261 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import * as fs from '../../../../../client/common/platform/fs-paths'; +import { FileChangeType } from '../../../../../client/common/platform/fileSystemWatcher'; +import { IDisposable } from '../../../../../client/common/types'; +import { createDeferred, Deferred, sleep } from '../../../../../client/common/utils/async'; +import { getOSType, OSType } from '../../../../../client/common/utils/platform'; +import { traceWarn } from '../../../../../client/logging'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { BasicEnvInfo, ILocator } from '../../../../../client/pythonEnvironments/base/locator'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { PythonEnvsChangedEvent } from '../../../../../client/pythonEnvironments/base/watcher'; +import { getInterpreterPathFromDir } from '../../../../../client/pythonEnvironments/common/commonUtils'; +import * as externalDeps from '../../../../../client/pythonEnvironments/common/externalDependencies'; +import { deleteFiles, PYTHON_PATH } from '../../../../common'; +import { TEST_TIMEOUT } from '../../../../constants'; +import { run } from '../envTestUtils'; + +/** + * A utility class used to create, delete, or modify environments. Primarily used for watcher + * tests, where we need to create environments. + */ +class Venvs { + constructor(private readonly root: string, private readonly prefix = '.virtualenv-') {} + + public async create(name: string): Promise<{ executable: string; envDir: string }> { + const envName = this.resolve(name); + const argv = [PYTHON_PATH.fileToCommandArgumentForPythonExt(), '-m', 'virtualenv', envName]; + try { + await run(argv, { cwd: this.root }); + } catch (err) { + throw new Error(`Failed to create Env ${path.basename(envName)} Error: ${err}`); + } + const dirToLookInto = path.join(this.root, envName); + const filename = await getInterpreterPathFromDir(dirToLookInto); + if (!filename) { + throw new Error(`No environment to update exists in ${dirToLookInto}`); + } + return { executable: filename, envDir: path.dirname(path.dirname(filename)) }; + } + + /** + * Creates a dummy environment by creating a fake executable. + * @param name environment suffix name to create + */ + public async createDummyEnv( + name: string, + kind: PythonEnvKind | undefined, + ): Promise<{ executable: string; envDir: string }> { + const envName = this.resolve(name); + const interpreterPath = path.join(this.root, envName, getOSType() === OSType.Windows ? 'python.exe' : 'python'); + const configPath = path.join(this.root, envName, 'pyvenv.cfg'); + try { + await fs.createFile(interpreterPath); + if (kind === PythonEnvKind.Venv) { + await fs.createFile(configPath); + await fs.writeFile(configPath, 'version = 3.9.2'); + } + } catch (err) { + throw new Error(`Failed to create python executable ${interpreterPath}, Error: ${err}`); + } + return { executable: interpreterPath, envDir: path.dirname(interpreterPath) }; + } + + // eslint-disable-next-line class-methods-use-this + public async update(filename: string): Promise<void> { + try { + await fs.writeFile(filename, 'Environment has been updated'); + } catch (err) { + throw new Error(`Failed to update Workspace virtualenv executable ${filename}, Error: ${err}`); + } + } + + // eslint-disable-next-line class-methods-use-this + public async delete(filename: string): Promise<void> { + try { + await fs.remove(filename); + } catch (err) { + traceWarn(`Failed to clean up ${filename}`); + } + } + + public async cleanUp(): Promise<void> { + const globPattern = path.join(this.root, `${this.prefix}*`); + await deleteFiles(globPattern); + } + + private resolve(name: string): string { + // Ensure env is random to avoid conflicts in tests (corrupting test data) + const now = new Date().getTime().toString().substr(-8); + return `${this.prefix}${name}${now}`; + } +} + +type locatorFactoryFuncType1 = () => Promise<ILocator<BasicEnvInfo> & IDisposable>; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type locatorFactoryFuncType2 = (_: any) => Promise<ILocator<BasicEnvInfo> & IDisposable>; + +export type locatorFactoryFuncType = locatorFactoryFuncType1 & locatorFactoryFuncType2; + +/** + * Test if we're able to: + * * Detect a new environment + * * Detect when an environment has been deleted + * * Detect when an environment has been updated + * @param root The root folder where we create, delete, or modify environments. + * @param createLocatorFactoryFunc The factory function used to create the locator. + */ +export function testLocatorWatcher( + root: string, + createLocatorFactoryFunc: locatorFactoryFuncType, + options?: { + /** + * Argument to the locator factory function if any. + */ + arg?: string; + /** + * Environment kind to check for in watcher events. + * If not specified the check is skipped is default. This is because detecting kind of virtual env + * often depends on the file structure around the executable, so we need to wait before attempting + * to verify it. Omitting that check in those cases as we can never deterministically say when it's + * ready to check. + */ + kind?: PythonEnvKind; + /** + * For search based locators it is possible to verify if the environment is now being located, as it + * can be searched for. But for non-search based locators, for eg. which rely on running commands to + * get environments, it's not possible to verify it without executing actual commands, installing tools + * etc, so this option is useful for those locators. + */ + doNotVerifyIfLocated?: boolean; + }, +): void { + let locator: ILocator<BasicEnvInfo> & IDisposable; + const venvs = new Venvs(root); + + async function waitForChangeToBeDetected(deferred: Deferred<void>) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + deferred.reject(new Error('Environment not detected')); + }, TEST_TIMEOUT); + await deferred.promise; + } + + async function isLocated(executable: string): Promise<boolean> { + const items = await getEnvs(locator.iterEnvs()); + return items.some((item) => externalDeps.arePathsSame(item.executablePath, executable)); + } + + suiteSetup(async function () { + if (getOSType() === OSType.Linux) { + this.skip(); + } + await venvs.cleanUp(); + }); + async function setupLocator(onChanged: (e: PythonEnvsChangedEvent) => Promise<void>) { + locator = options?.arg ? await createLocatorFactoryFunc(options.arg) : await createLocatorFactoryFunc(); + locator.onChanged(onChanged); + await getEnvs(locator.iterEnvs()); // Force the FS watcher to start. + // Wait for watchers to get ready + await sleep(2000); + } + + teardown(async () => { + if (locator) { + await locator.dispose(); + } + await venvs.cleanUp(); + }); + + test('Detect a new environment', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + await setupLocator(async (e) => { + actualEvent = e; + deferred.resolve(); + }); + + const { executable, envDir } = await venvs.create('one'); + await waitForChangeToBeDetected(deferred); + if (!options?.doNotVerifyIfLocated) { + const isFound = await isLocated(executable); + assert.ok(isFound); + } + + assert.strictEqual(actualEvent!.type, FileChangeType.Created, 'Wrong event emitted'); + if (options?.kind) { + assert.strictEqual(actualEvent!.kind, options.kind, 'Wrong event emitted'); + } + assert.notStrictEqual(actualEvent!.searchLocation, undefined, 'Wrong event emitted'); + assert.ok( + externalDeps.arePathsSame(actualEvent!.searchLocation!.fsPath, path.dirname(envDir)), + 'Wrong event emitted', + ); + }).timeout(TEST_TIMEOUT * 2); + + test('Detect when an environment has been deleted', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + const { executable, envDir } = await venvs.create('one'); + await setupLocator(async (e) => { + if (e.type === FileChangeType.Deleted) { + actualEvent = e; + deferred.resolve(); + } + }); + + // VSCode API has a limitation where it fails to fire event when environment folder is deleted directly: + // https://github.com/microsoft/vscode/issues/110923 + // Using chokidar directly in tests work, but it has permission issues on Windows that you cannot delete a + // folder if it has a subfolder that is being watched inside: https://github.com/paulmillr/chokidar/issues/422 + // Hence we test directly deleting the executable, and not the whole folder using `workspaceVenvs.cleanUp()`. + await venvs.delete(executable); + await waitForChangeToBeDetected(deferred); + if (!options?.doNotVerifyIfLocated) { + const isFound = await isLocated(executable); + assert.notOk(isFound); + } + + assert.notStrictEqual(actualEvent!, undefined, 'Wrong event emitted'); + if (options?.kind) { + assert.strictEqual(actualEvent!.kind, options.kind, 'Wrong event emitted'); + } + assert.notStrictEqual(actualEvent!.searchLocation, undefined, 'Wrong event emitted'); + assert.ok( + externalDeps.arePathsSame(actualEvent!.searchLocation!.fsPath, path.dirname(envDir)), + 'Wrong event emitted', + ); + }).timeout(TEST_TIMEOUT * 2); + + test('Detect when an environment has been updated', async () => { + let actualEvent: PythonEnvsChangedEvent; + const deferred = createDeferred<void>(); + // Create a dummy environment so we can update its executable later. We can't choose a real environment here. + // Executables inside real environments can be symlinks, so writing on them can result in the real executable + // being updated instead of the symlink. + const { executable, envDir } = await venvs.createDummyEnv('one', options?.kind); + await setupLocator(async (e) => { + if (e.type === FileChangeType.Changed) { + actualEvent = e; + deferred.resolve(); + } + }); + + await venvs.update(executable); + await waitForChangeToBeDetected(deferred); + assert.notStrictEqual(actualEvent!, undefined, 'Event was not emitted'); + if (options?.kind) { + assert.strictEqual(actualEvent!.kind, options.kind, 'Kind is not as expected'); + } + assert.notStrictEqual(actualEvent!.searchLocation, undefined, 'Search location is not set'); + assert.ok( + externalDeps.arePathsSame(actualEvent!.searchLocation!.fsPath, path.dirname(envDir)), + `Paths don't match ${actualEvent!.searchLocation!.fsPath} != ${path.dirname(envDir)}`, + ); + }).timeout(TEST_TIMEOUT * 2); +} diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts new file mode 100644 index 000000000000..07a7a864ef74 --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator.unit.test.ts @@ -0,0 +1,275 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { PythonEnvKind, PythonEnvSource } from '../../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import * as winreg from '../../../../../client/pythonEnvironments/common/windowsRegistry'; +import { + WindowsRegistryLocator, + WINDOWS_REG_PROVIDER_ID, +} from '../../../../../client/pythonEnvironments/base/locators/lowLevel/windowsRegistryLocator'; +import { createBasicEnv } from '../../common'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import * as externalDependencies from '../../../../../client/pythonEnvironments/common/externalDependencies'; + +suite('Windows Registry', () => { + let stubReadRegistryValues: sinon.SinonStub; + let stubReadRegistryKeys: sinon.SinonStub; + let locator: WindowsRegistryLocator; + + const regTestRoot = path.join(TEST_LAYOUT_ROOT, 'winreg'); + + const registryData = { + x64: { + HKLM: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore', '\\SOFTWARE\\Python\\ContinuumAnalytics'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9', + values: { + '': '', + DisplayName: 'Python 3.9 (64-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '64bit', + SysVersion: '3.9', + Version: '3.9.0rc2', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.9\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'py39', 'python.exe'), + }, + subKeys: [] as string[], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics', + values: { + '': '', + }, + subKeys: ['\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64'], + }, + { + key: '\\SOFTWARE\\Python\\ContinuumAnalytics\\Anaconda38-64', + values: { + '': '', + DisplayName: 'Anaconda py38_4.8.3', + SupportUrl: 'github.com/continuumio/anaconda-issues', + SysArchitecture: '64bit', + SysVersion: '3.8', + Version: 'py38_4.8.3', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\Anaconda38-64\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'conda3', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + HKCU: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.7'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.7', + values: { + '': '', + DisplayName: 'Python 3.7 (64-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '64bit', + SysVersion: '3.7', + Version: '3.7.7', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCore\\3.7\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCore\\3.7\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'python37', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + }, + x86: { + HKLM: [], + HKCU: [ + { + key: '\\SOFTWARE\\Python', + values: { '': '' }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack', + values: { + '': '', + DisplayName: 'Python Software Foundation', + SupportUrl: 'www.python.org', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8', + values: { + '': '', + DisplayName: 'Python 3.8 (32-bit)', + SupportUrl: 'www.python.org', + SysArchitecture: '32bit', + SysVersion: '3.8.5', + }, + subKeys: ['\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath'], + }, + { + key: '\\SOFTWARE\\Python\\PythonCodingPack\\3.8\\InstallPath', + values: { + '': '', + ExecutablePath: path.join(regTestRoot, 'python38', 'python.exe'), + }, + subKeys: [] as string[], + }, + ], + }, + }; + + function fakeRegistryValues({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryValue[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const values: winreg.IRegistryValue[] = []; + for (const [name, value] of Object.entries(k.values)) { + values.push({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: k.key, + name, + type: winreg.REG_SZ, + value: value ?? '', + }); + } + return Promise.resolve(values); + } + } + return Promise.resolve([]); + } + + function fakeRegistryKeys({ arch, hive, key }: winreg.Options): Promise<winreg.IRegistryKey[]> { + const regArch = arch === 'x86' ? registryData.x86 : registryData.x64; + const regHive = hive === winreg.HKCU ? regArch.HKCU : regArch.HKLM; + for (const k of regHive) { + if (k.key === key) { + const keys = k.subKeys.map((s) => ({ + arch: arch ?? 'x64', + hive: hive ?? winreg.HKLM, + key: s, + })); + return Promise.resolve(keys); + } + } + return Promise.resolve([]); + } + + setup(async () => { + sinon.stub(externalDependencies, 'inExperiment').returns(true); + stubReadRegistryValues = sinon.stub(winreg, 'readRegistryValues'); + stubReadRegistryKeys = sinon.stub(winreg, 'readRegistryKeys'); + stubReadRegistryValues.callsFake(fakeRegistryValues); + stubReadRegistryKeys.callsFake(fakeRegistryKeys); + + locator = new WindowsRegistryLocator(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('iterEnvs()', async () => { + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'py39', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'conda3', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python37', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python38', 'python.exe')), + ].map((e) => ({ ...e, source: [PythonEnvSource.WindowsRegistry] })); + + const lazyIterator = locator.iterEnvs(undefined, true); + const envs = await getEnvs(lazyIterator); + expect(envs.length).to.equal(0); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): query is undefined', async () => { + // Iterate no envs when query is `undefined`, i.e notify completion immediately. + const lazyIterator = locator.iterEnvs(undefined, true); + const envs = await getEnvs(lazyIterator); + expect(envs.length).to.equal(0); + }); + + test('iterEnvs(): no registry permission', async () => { + stubReadRegistryKeys.callsFake(() => { + throw Error(); + }); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); + const actualEnvs = await getEnvs(iterator); + + assert.deepStrictEqual(actualEnvs, []); + }); + + test('iterEnvs(): partial registry permission', async () => { + stubReadRegistryKeys.callsFake(({ arch, hive, key }: winreg.Options) => { + if (hive === winreg.HKLM) { + throw Error(); + } + return fakeRegistryKeys({ arch, hive, key }); + }); + + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python37', 'python.exe')), + createBasicEnv(PythonEnvKind.OtherGlobal, path.join(regTestRoot, 'python38', 'python.exe')), + ].map((e) => ({ ...e, source: [PythonEnvSource.WindowsRegistry] })); + + const iterator = locator.iterEnvs({ providerId: WINDOWS_REG_PROVIDER_ID }, true); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts new file mode 100644 index 000000000000..60168f4847ca --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.testvirtualenvs.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { WorkspaceVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { testLocatorWatcher } from './watcherTestUtils'; + +suite('WorkspaceVirtualEnvironment Locator', async () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + testLocatorWatcher(testWorkspaceFolder, async (root?: string) => new WorkspaceVirtualEnvironmentLocator(root!), { + arg: testWorkspaceFolder, + kind: PythonEnvKind.Venv, + }); +}); diff --git a/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts new file mode 100644 index 000000000000..3bf93b1eaf5d --- /dev/null +++ b/src/test/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator.unit.test.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsWatcher from '../../../../../client/common/platform/fileSystemWatcher'; +import * as platformUtils from '../../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../../client/pythonEnvironments/base/info'; +import { WorkspaceVirtualEnvironmentLocator } from '../../../../../client/pythonEnvironments/base/locators/lowLevel/workspaceVirtualEnvLocator'; +import { getEnvs } from '../../../../../client/pythonEnvironments/base/locatorUtils'; +import { TEST_LAYOUT_ROOT } from '../../../common/commonTestConstants'; +import { assertBasicEnvsEqual } from '../envTestUtils'; +import { createBasicEnv } from '../../common'; + +suite('WorkspaceVirtualEnvironment Locator', () => { + const testWorkspaceFolder = path.join(TEST_LAYOUT_ROOT, 'workspace', 'folder1'); + let getOSTypeStub: sinon.SinonStub; + let watchLocationForPatternStub: sinon.SinonStub; + let locator: WorkspaceVirtualEnvironmentLocator; + + setup(() => { + getOSTypeStub = sinon.stub(platformUtils, 'getOSType'); + getOSTypeStub.returns(platformUtils.OSType.Linux); + watchLocationForPatternStub = sinon.stub(fsWatcher, 'watchLocationForPattern'); + watchLocationForPatternStub.returns({ + dispose: () => { + /* do nothing */ + }, + }); + locator = new WorkspaceVirtualEnvironmentLocator(testWorkspaceFolder); + }); + teardown(async () => { + await locator.dispose(); + sinon.restore(); + }); + + test('iterEnvs(): Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Windows); + const expectedEnvs = [ + createBasicEnv(PythonEnvKind.Venv, path.join(testWorkspaceFolder, 'win1', 'python.exe')), + createBasicEnv( + PythonEnvKind.Venv, + path.join(testWorkspaceFolder, '.direnv', 'win2', 'Scripts', 'python.exe'), + ), + createBasicEnv(PythonEnvKind.Pipenv, path.join(testWorkspaceFolder, '.venv', 'Scripts', 'python.exe')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); + + test('iterEnvs(): Non-Windows', async () => { + getOSTypeStub.returns(platformUtils.OSType.Linux); + const expectedEnvs = [ + createBasicEnv( + PythonEnvKind.VirtualEnv, + path.join(testWorkspaceFolder, '.direnv', 'posix1virtualenv', 'bin', 'python'), + ), + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix2conda', 'python')), + createBasicEnv(PythonEnvKind.Unknown, path.join(testWorkspaceFolder, 'posix3custom', 'bin', 'python')), + ]; + + const iterator = locator.iterEnvs(); + const actualEnvs = await getEnvs(iterator); + + assertBasicEnvsEqual(actualEnvs, expectedEnvs); + }); +}); diff --git a/src/test/pythonEnvironments/base/watcher.unit.test.ts b/src/test/pythonEnvironments/base/watcher.unit.test.ts new file mode 100644 index 000000000000..bcb3cfbed7f7 --- /dev/null +++ b/src/test/pythonEnvironments/base/watcher.unit.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { + BasicPythonEnvsChangedEvent, + PythonEnvsChangedEvent, + PythonEnvsWatcher, +} from '../../../client/pythonEnvironments/base/watcher'; + +const KINDS_TO_TEST = [ + PythonEnvKind.Unknown, + PythonEnvKind.System, + PythonEnvKind.Custom, + PythonEnvKind.OtherGlobal, + PythonEnvKind.Venv, + PythonEnvKind.Conda, + PythonEnvKind.OtherVirtual, +]; + +suite('Python envs watcher - PythonEnvsWatcher', () => { + const location = Uri.file('some-dir'); + + suite('fire()', () => { + test('empty event', () => { + const expected: PythonEnvsChangedEvent = {}; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(event, expected); + }); + + KINDS_TO_TEST.forEach((kind) => { + test(`non-empty event ("${kind}")`, () => { + const expected: PythonEnvsChangedEvent = { + kind, + searchLocation: location, + }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(event, expected); + }); + }); + + test('kind-only', () => { + const expected: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(event, expected); + }); + + test('searchLocation-only', () => { + const expected: PythonEnvsChangedEvent = { searchLocation: Uri.file('foo') }; + const watcher = new PythonEnvsWatcher(); + let event: PythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(event, expected); + }); + }); + + suite('using BasicPythonEnvsChangedEvent', () => { + test('empty event', () => { + const expected: BasicPythonEnvsChangedEvent = {}; + const watcher = new PythonEnvsWatcher<BasicPythonEnvsChangedEvent>(); + let event: BasicPythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(event, expected); + }); + + KINDS_TO_TEST.forEach((kind) => { + test(`non-empty event ("${kind}")`, () => { + const expected: BasicPythonEnvsChangedEvent = { + kind, + }; + const watcher = new PythonEnvsWatcher<BasicPythonEnvsChangedEvent>(); + let event: BasicPythonEnvsChangedEvent | undefined; + watcher.onChanged((e) => { + event = e; + }); + + watcher.fire(expected); + + assert.strictEqual(event, expected); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/base/watchers.unit.test.ts b/src/test/pythonEnvironments/base/watchers.unit.test.ts new file mode 100644 index 000000000000..6ccc6451fa55 --- /dev/null +++ b/src/test/pythonEnvironments/base/watchers.unit.test.ts @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { Uri } from 'vscode'; +import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { PythonEnvsChangedEvent, PythonEnvsWatcher } from '../../../client/pythonEnvironments/base/watcher'; +import { PythonEnvsWatchers } from '../../../client/pythonEnvironments/base/watchers'; + +suite('Python envs watchers - PythonEnvsWatchers', () => { + suite('onChanged consolidates', () => { + test('empty', () => { + const watcher = new PythonEnvsWatchers([]); + + assert.ok(watcher); + }); + + test('one', () => { + const event1: PythonEnvsChangedEvent = {}; + const expected = [event1]; + const sub1 = new PythonEnvsWatcher(); + const watcher = new PythonEnvsWatchers([sub1]); + + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + sub1.fire(event1); + + assert.deepEqual(events, expected); + }); + + test('many', () => { + const loc1 = Uri.file('some-dir'); + const event1: PythonEnvsChangedEvent = { kind: PythonEnvKind.Unknown, searchLocation: loc1 }; + const event2: PythonEnvsChangedEvent = { kind: PythonEnvKind.Venv }; + const event3: PythonEnvsChangedEvent = {}; + const event4: PythonEnvsChangedEvent = { searchLocation: loc1 }; + const event5: PythonEnvsChangedEvent = {}; + const expected = [event1, event2, event3, event4, event5]; + const sub1 = new PythonEnvsWatcher(); + const sub2 = new PythonEnvsWatcher(); + const sub3 = new PythonEnvsWatcher(); + const watcher = new PythonEnvsWatchers([sub1, sub2, sub3]); + + const events: PythonEnvsChangedEvent[] = []; + watcher.onChanged((e) => events.push(e)); + sub2.fire(event1); + sub3.fire(event2); + sub1.fire(event3); + sub2.fire(event4); + sub1.fire(event5); + + assert.deepEqual(events, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/commonTestConstants.ts b/src/test/pythonEnvironments/common/commonTestConstants.ts new file mode 100644 index 000000000000..361f9b4dd1ea --- /dev/null +++ b/src/test/pythonEnvironments/common/commonTestConstants.ts @@ -0,0 +1,27 @@ +import * as path from 'path'; + +export const TEST_LAYOUT_ROOT = path.join( + __dirname, + '..', + '..', + '..', + '..', + 'src', + 'test', + 'pythonEnvironments', + 'common', + 'envlayouts', +); + +export const TEST_DATA_ROOT = path.join( + __dirname, + '..', + '..', + '..', + '..', + 'src', + 'test', + 'pythonEnvironments', + 'common', + 'testdata', +); diff --git a/src/test/pythonEnvironments/common/commonUtils.functional.test.ts b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts new file mode 100644 index 000000000000..647a17a40a90 --- /dev/null +++ b/src/test/pythonEnvironments/common/commonUtils.functional.test.ts @@ -0,0 +1,543 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as path from 'path'; +import { getOSType, OSType } from '../../../client/common/utils/platform'; +import { findInterpretersInDir } from '../../../client/pythonEnvironments/common/commonUtils'; +import { ensureFSTree as utilEnsureFSTree } from '../../utils/fs'; + +const IS_WINDOWS = getOSType() === OSType.Windows; + +async function ensureFSTree(tree: string): Promise<void> { + await utilEnsureFSTree(tree.trimEnd(), __dirname); +} + +suite('pyenvs common utils - finding Python executables', () => { + const datadir = path.join(__dirname, '.data'); + + function resolveDataFiles(rootName: string, relnames: string[]): string[] { + return relnames.map((relname) => path.normalize(`${datadir}/${rootName}/${relname}`)); + } + + async function find( + rootName: string, + maxDepth?: number, + filterDir?: (x: string) => boolean, + // Errors are helpful when testing, so we don't bother ignoring them. + ): Promise<string[]> { + const results: string[] = []; + const root = path.join(datadir, rootName); + const executables = findInterpretersInDir(root, maxDepth, filterDir); + for await (const entry of executables) { + results.push(entry.filename); + } + return results; + } + + suite('mixed', () => { + const rootName = 'root_mixed'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + sub1/ + spam + sub2/ + sub2.1/ + sub2.1.1/ + <python.exe> + spam.txt + sub2.2/ + <spam.exe> + python3.exe + sub3/ + python.exe + <spam.exe> + spam.txt + <python.exe> + eggs.exe + python2.exe + <python3.8.exe> + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + sub1/ + spam + sub2/ + sub2.1/ + sub2.1.1/ + <python> + spam.txt + sub2.2/ + <spam> + python3 + sub3/ + python + <spam> + spam.txt + <python> + eggs + python2 + <python3.8> + python3 -> sub2/sub2.2/python3 + python3.7 -> sub2/sub2.1/sub2.1.1/python + `); + } + }); + + suite('non-recursive', () => { + test('no filter', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These will match. + 'python.exe', + 'python2.exe', + 'python3.8.exe', + ] + : [ + // These will match. + 'python', + 'python2', + 'python3', + 'python3.7', + 'python3.8', + ], + ); + + const found = await find(rootName); + + assert.deepEqual(found, expected); + }); + }); + + suite('recursive', () => { + test('no filter', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These will match. + 'python.exe', + 'python2.exe', + 'python3.8.exe', + 'sub2/sub2.1/sub2.1.1/python.exe', + 'sub2/sub2.2/python3.exe', + 'sub3/python.exe', + ] + : [ + // These will match. + 'python', + 'python2', + 'python3', + 'python3.7', + 'python3.8', + 'sub2/sub2.1/sub2.1.1/python', + 'sub2/sub2.2/python3', + 'sub3/python', + ], + ); + + const found = await find(rootName, 3); + + assert.deepEqual(found, expected); + }); + + test('filtered', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These will match. + 'python.exe', + 'python2.exe', + 'python3.8.exe', + 'sub3/python.exe', + ] + : [ + // These will match. + 'python', + 'python2', + 'python3', + 'python3.7', + 'python3.8', + 'sub3/python', + ], + ); + function filterDir(dirname: string): boolean { + return dirname.match(/sub\d$/) !== null; + } + + const found = await find(rootName, 3, filterDir); + + assert.deepEqual(found, expected); + }); + }); + }); + + suite('different layouts and patterns', () => { + suite('names', () => { + const rootName = 'root_name_patterns'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + <python.exe> + <python2.exe> + <python2.7.exe> + <python27.exe> + <python3.exe> + <python3.8.exe> + <python3.8.1.exe> # should match but doesn't + <python3.8.1rc1.exe> # should match but doesn't + <python3.8.1rc1.10213.exe> # should match but doesn't + <python3.8.1-candidate1.exe> # should match but doesn't + <python.3.8.exe> # should match but doesn't + <python.3.8.1.candidate.1.exe> # should match but doesn't + <python-3.exe> # should match but doesn't + <python-3.8.exe> # should match but doesn't + <python38.exe> + <python381.exe> + <my-python.exe> # should match but doesn't + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + <python> + <python2> + <python2.7> + <python27> + <python3> + <python3.8> + <python3.8.1> # should match but doesn't + <python3.8.1rc1> # should match but doesn't + <python3.8.1rc1.10213> # should match but doesn't + <python3.8.1-candidate1> # should match but doesn't + <python.3.8> # should match but doesn't + <python.3.8.1.candidate.1> # should match but doesn't + <python-3> # should match but doesn't + <python-3.8> # should match but doesn't + <python38> + <python381> + <my-python> # should match but doesn't + `); + } + }); + + test('non-recursive', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These order here matters. + 'python.exe', + 'python2.7.exe', + 'python2.exe', + 'python27.exe', + 'python3.8.exe', + 'python3.exe', + 'python38.exe', + 'python381.exe', + ] + : [ + // These order here matters. + 'python', + 'python2', + 'python2.7', + 'python27', + 'python3', + 'python3.8', + 'python38', + 'python381', + ], + ); + + const found = await find(rootName); + + assert.deepEqual(found, expected); + }); + }); + + suite('trees', () => { + const rootName = 'root_layouts'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + py/ + 2.7/ + bin/ + <python.exe> + <python2.exe> + <python2.7.exe> + 3.8/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + python/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + 3.8/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + python2/ + <python.exe> + python3/ + <python3.exe> + python3.8/ + bin/ + <python3.exe> + <python3.8.exe> + python38/ + bin/ + <python3.exe> + python.3.8/ + bin/ + <python3.exe> + <python3.8.exe> + python-3.8/ + bin/ + <python3.exe> + <python3.8.exe> + my-python/ + <python3.exe> + 3.8/ + bin/ + <python.exe> + <python3.exe> + <python3.8.exe> + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + py/ + 2.7/ + bin/ + <python> + <python2> + <python2.7> + 3.8/ + bin/ + <python> + <python3> + <python3.8> + python/ + bin/ + <python> + <python3> + <python3.8> + 3.8/ + bin/ + <python> + <python3> + <python3.8> + python2/ + <python> + python3/ + <python3> + python3.8/ + bin/ + <python3> + <python3.8> + python38/ + bin/ + <python3> + python.3.8/ + bin/ + <python3> + <python3.8> + python-3.8/ + bin/ + <python3> + <python3.8> + my-python/ + <python3> + 3.8/ + bin/ + <python> + <python3> + <python3.8> + `); + } + }); + + test('recursive', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These order here matters. + '3.8/bin/python.exe', + '3.8/bin/python3.8.exe', + '3.8/bin/python3.exe', + 'my-python/python3.exe', + 'py/2.7/bin/python.exe', + 'py/2.7/bin/python2.7.exe', + 'py/2.7/bin/python2.exe', + 'py/3.8/bin/python.exe', + 'py/3.8/bin/python3.8.exe', + 'py/3.8/bin/python3.exe', + 'python/3.8/bin/python.exe', + 'python/3.8/bin/python3.8.exe', + 'python/3.8/bin/python3.exe', + 'python/bin/python.exe', + 'python/bin/python3.8.exe', + 'python/bin/python3.exe', + 'python-3.8/bin/python3.8.exe', + 'python-3.8/bin/python3.exe', + 'python.3.8/bin/python3.8.exe', + 'python.3.8/bin/python3.exe', + 'python2/python.exe', + 'python3/python3.exe', + 'python3.8/bin/python3.8.exe', + 'python3.8/bin/python3.exe', + 'python38/bin/python3.exe', + ] + : [ + // These order here matters. + '3.8/bin/python', + '3.8/bin/python3', + '3.8/bin/python3.8', + 'my-python/python3', + 'py/2.7/bin/python', + 'py/2.7/bin/python2', + 'py/2.7/bin/python2.7', + 'py/3.8/bin/python', + 'py/3.8/bin/python3', + 'py/3.8/bin/python3.8', + 'python/3.8/bin/python', + 'python/3.8/bin/python3', + 'python/3.8/bin/python3.8', + 'python/bin/python', + 'python/bin/python3', + 'python/bin/python3.8', + 'python-3.8/bin/python3', + 'python-3.8/bin/python3.8', + 'python.3.8/bin/python3', + 'python.3.8/bin/python3.8', + 'python2/python', + 'python3/python3', + 'python3.8/bin/python3', + 'python3.8/bin/python3.8', + 'python38/bin/python3', + ], + ); + + const found = await find(rootName, 3); + + assert.deepEqual(found, expected); + }); + }); + }); + + suite('tricky cases', () => { + const rootName = 'root_tricky'; + + suiteSetup(async () => { + if (IS_WINDOWS) { + await ensureFSTree(` + ./.data/ + ${rootName}/ + pythons/ + <python.exe> + <python2.exe> + <python2.7.exe> + <python3.exe> + <python3.7.exe> + <python3.8.exe> + <python3.9.2.exe> # should match but doesn't + <python3.10a1.exe> # should match but doesn't + python2.7.exe/ + <spam.exe> + python3.8.exe/ + <python.exe> + <py.exe> # launcher not supported + <py3.exe> # launcher not supported + <Python3.exe> # case-insensitive + <PYTHON.EXE> # case-insensitive + <Python3> + <PYTHON> + <not-python.exe> + <python.txt> + <python> + <python2> + <python3> + <spam.exe> + `); + } else { + await ensureFSTree(` + ./.data/ + ${rootName}/ + pythons/ + <python> + <python2> + <python2.7> + <python3> + <python3.7> + <python3.8> + <python3.9.2> # should match but doesn't + <python3.10a1> # should match but doesn't + <py> # launcher not supported + <py3> # launcher not supported + <Python3> + <PYTHON> + <not-python> + <python.txt> + <python.exe> + <python2.exe> + <python3.exe> + <spam> + `); + } + }); + + test('recursive', async () => { + const expected = resolveDataFiles( + rootName, + IS_WINDOWS + ? [ + // These order here matters. + 'python3.8.exe/python.exe', + 'pythons/python.exe', + 'pythons/python2.7.exe', + 'pythons/python2.exe', + 'pythons/python3.7.exe', + 'pythons/python3.8.exe', + 'pythons/python3.exe', + // 'Python3.exe', + // 'PYTHON.EXE', + ] + : [ + // These order here matters. + 'pythons/python', + 'pythons/python2', + 'pythons/python2.7', + 'pythons/python3', + 'pythons/python3.7', + 'pythons/python3.8', + ], + ); + + const found = await find(rootName, 3); + + assert.deepEqual(found, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts new file mode 100644 index 000000000000..af719c3e40ed --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentIdentifier.unit.test.ts @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../client/pythonEnvironments/base/info'; +import { identifyEnvironment } from '../../../client/pythonEnvironments/common/environmentIdentifier'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; +import { getOSType as getOSTypeForTest, OSType } from '../../common'; +import { TEST_LAYOUT_ROOT } from './commonTestConstants'; + +suite('Environment Identifier', () => { + suite('Conda', () => { + test('Conda layout with conda-meta and python binary in the same directory', async () => { + const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'conda1', 'python.exe'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Conda); + }); + test('Conda layout with conda-meta and python binary in a sub directory', async () => { + const interpreterPath: string = path.join(TEST_LAYOUT_ROOT, 'conda2', 'bin', 'python'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Conda); + }); + }); + + suite('Pipenv', () => { + let getEnvVar: sinon.SinonStub; + let readFile: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + }); + + test('Path to a global pipenv environment', async () => { + const expectedDotProjectFile = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project2-vnNIWe9P', + '.project', + ); + const expectedProjectFile = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project2'); + readFile.withArgs(expectedDotProjectFile).resolves(expectedProjectFile); + const interpreterPath: string = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project2-vnNIWe9P', + 'bin', + 'python', + ); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + + assert.strictEqual(envType, PythonEnvKind.Pipenv); + }); + + test('Path to a local pipenv environment with a custom Pipfile name', async () => { + getEnvVar.withArgs('PIPENV_PIPFILE').returns('CustomPipfileName'); + const interpreterPath: string = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'project1', + '.venv', + 'Scripts', + 'python.exe', + ); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + + assert.strictEqual(envType, PythonEnvKind.Pipenv); + }); + }); + + suite('Microsoft Store', () => { + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + const fakeLocalAppDataPath = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const fakeProgramFilesPath = 'X:\\Program Files'; + const executable = ['python.exe', 'python3.exe', 'python3.8.exe']; + suiteSetup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); + getEnvVar.withArgs('ProgramFiles').returns(fakeProgramFilesPath); + + pathExists = sinon.stub(externalDependencies, 'pathExists'); + pathExists.withArgs(path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', 'idle.exe')).resolves(true); + }); + suiteTeardown(() => { + getEnvVar.restore(); + pathExists.restore(); + }); + executable.forEach((exe) => { + test(`Path to local app data microsoft store interpreter (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); + const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path to local app data microsoft store interpreter app sub-directory (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(fakeLocalAppDataPath); + const interpreterPath = path.join( + fakeLocalAppDataPath, + 'Microsoft', + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path to program files microsoft store interpreter app sub-directory (${exe})`, async () => { + const interpreterPath = path.join( + fakeProgramFilesPath, + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Local app data not set (${exe})`, async () => { + getEnvVar.withArgs('LOCALAPPDATA').returns(undefined); + const interpreterPath = path.join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Program files app data not set (${exe})`, async () => { + const interpreterPath = path.join( + fakeProgramFilesPath, + 'WindowsApps', + 'PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0', + exe, + ); + getEnvVar.withArgs('ProgramFiles').returns(undefined); + pathExists.withArgs(path.join(path.dirname(interpreterPath), 'idle.exe')).resolves(true); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path using forward slashes (${exe})`, async () => { + const interpreterPath = path + .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) + .replace(/\\/g, '/'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + test(`Path using long path style slashes (${exe})`, async () => { + const interpreterPath = path + .join(fakeLocalAppDataPath, 'Microsoft', 'WindowsApps', exe) + .replace('\\', '/'); + pathExists.callsFake((p: string) => { + if (p.endsWith('idle.exe')) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + const envType: PythonEnvKind = await identifyEnvironment(`\\\\?\\${interpreterPath}`); + assert.deepEqual(envType, PythonEnvKind.MicrosoftStore); + }); + }); + }); + + suite('Pyenv', () => { + let getEnvVarStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + + suiteSetup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformApis, 'getOSType'); + getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir'); + }); + + suiteTeardown(() => { + getEnvVarStub.restore(); + getOsTypeStub.restore(); + getUserHomeDirStub.restore(); + }); + + test('PYENV_ROOT is not set on non-Windows, fallback to the default value ~/.pyenv', async function () { + if (getOSTypeForTest() === OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'pyenv1', + '.pyenv', + 'versions', + '3.6.9', + 'bin', + 'python', + ); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv1')); + getEnvVarStub.withArgs('PYENV_ROOT').returns(undefined); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + + test('PYENV is not set on Windows, fallback to the default value %USERPROFILE%\\.pyenv\\pyenv-win', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'pyenv2', + '.pyenv', + 'pyenv-win', + 'versions', + '3.6.9', + 'bin', + 'python.exe', + ); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'pyenv2')); + getEnvVarStub.withArgs('PYENV').returns(undefined); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + + test('PYENV_ROOT is set to a custom value on non-Windows', async function () { + if (getOSTypeForTest() === OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python'); + + getEnvVarStub.withArgs('PYENV_ROOT').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3')); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + + test('PYENV is set to a custom value on Windows', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'pyenv3', 'versions', '3.6.9', 'bin', 'python.exe'); + + getEnvVarStub.withArgs('PYENV').returns(path.join(TEST_LAYOUT_ROOT, 'pyenv3')); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.Pyenv); + + return undefined; + }); + }); + + suite('Venv', () => { + test('Pyvenv.cfg is in the same directory as the interpreter', async () => { + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv1', 'python'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Venv); + }); + test('Pyvenv.cfg is in the same directory as the interpreter', async () => { + const interpreterPath = path.join(TEST_LAYOUT_ROOT, 'venv2', 'bin', 'python'); + const envType: PythonEnvKind = await identifyEnvironment(interpreterPath); + assert.deepEqual(envType, PythonEnvKind.Venv); + }); + }); + + suite('Virtualenvwrapper', () => { + let getEnvVarStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + + suiteSetup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformApis, 'getOSType'); + getUserHomeDirStub = sinon.stub(platformApis, 'getUserHomeDir'); + + getUserHomeDirStub.returns(path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper1')); + }); + + suiteTeardown(() => { + getEnvVarStub.restore(); + getOsTypeStub.restore(); + getUserHomeDirStub.restore(); + }); + + test('WORKON_HOME is set to its default value ~/.virtualenvs on non-Windows', async function () { + if (getOSTypeForTest() === OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'virtualenvwrapper1', + '.virtualenvs', + 'myenv', + 'bin', + 'python', + ); + + getEnvVarStub.withArgs('WORKON_HOME').returns(undefined); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnvWrapper); + + return undefined; + }); + + test('WORKON_HOME is set to its default value %USERPROFILE%\\Envs on Windows', async function () { + if (getOSTypeForTest() !== OSType.Windows) { + return this.skip(); + } + + const interpreterPath = path.join( + TEST_LAYOUT_ROOT, + 'virtualenvwrapper1', + 'Envs', + 'myenv', + 'Scripts', + 'python', + ); + + getEnvVarStub.withArgs('WORKON_HOME').returns(undefined); + getOsTypeStub.returns(platformApis.OSType.Windows); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnvWrapper); + + return undefined; + }); + + test('WORKON_HOME is set to a custom value', async () => { + const workonHomeDir = path.join(TEST_LAYOUT_ROOT, 'virtualenvwrapper2'); + const interpreterPath = path.join(workonHomeDir, 'myenv', 'bin', 'python'); + + getEnvVarStub.withArgs('WORKON_HOME').returns(workonHomeDir); + + const envType = await identifyEnvironment(interpreterPath); + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnvWrapper); + }); + }); + + suite('Virtualenv', () => { + const activateFiles = [ + { folder: 'virtualenv1', file: 'activate' }, + { folder: 'virtualenv2', file: 'activate.sh' }, + { folder: 'virtualenv3', file: 'activate.ps1' }, + ]; + + activateFiles.forEach(({ folder, file }) => { + test(`Folder contains ${file}`, async () => { + const interpreterPath = path.join(TEST_LAYOUT_ROOT, folder, 'bin', 'python'); + const envType = await identifyEnvironment(interpreterPath); + + assert.deepStrictEqual(envType, PythonEnvKind.VirtualEnv); + }); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts new file mode 100644 index 000000000000..23eebc5fee07 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/activestate.unit.test.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import { getOSType, OSType } from '../../../../client/common/utils/platform'; +import { isActiveStateEnvironment } from '../../../../client/pythonEnvironments/common/environmentManagers/activestate'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('isActiveStateEnvironment Tests', () => { + const testActiveStateDir = path.join(TEST_LAYOUT_ROOT, 'activestate'); + + test('Return true if runtime is set up', async () => { + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'c09080d1', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(true); + }); + + test(`Return false if the runtime is not set up`, async () => { + const result = await isActiveStateEnvironment( + path.join( + testActiveStateDir, + 'b6a0705d', + 'exec', + getOSType() === OSType.Windows ? 'python3.exe' : 'python3', + ), + ); + expect(result).to.equal(false); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts new file mode 100644 index 000000000000..9480dffe6a59 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/conda.unit.test.ts @@ -0,0 +1,675 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as util from 'util'; +import { eq } from 'semver'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as platform from '../../../../client/common/utils/platform'; +import { PythonEnvKind } from '../../../../client/pythonEnvironments/base/info'; +import { getEnvs } from '../../../../client/pythonEnvironments/base/locatorUtils'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import * as windowsUtils from '../../../../client/pythonEnvironments/common/windowsUtils'; +import { Conda, CondaInfo } from '../../../../client/pythonEnvironments/common/environmentManagers/conda'; +import { CondaEnvironmentLocator } from '../../../../client/pythonEnvironments/base/locators/lowLevel/condaLocator'; +import { createBasicEnv } from '../../base/common'; +import { assertBasicEnvsEqual } from '../../base/locators/envTestUtils'; +import { OUTPUT_MARKER_SCRIPT } from '../../../../client/common/process/internal/scripts'; + +suite('Conda and its environments are located correctly', () => { + // getOSType() is stubbed to return this. + let osType: platform.OSType; + + // getUserHomeDir() is stubbed to return this. + let homeDir: string | undefined; + + // getRegistryInterpreters() is stubbed to return this. + let registryInterpreters: windowsUtils.IRegistryInterpreterData[]; + + // readdir() and readFile() are stubbed to present a dummy file system based on this + // object graph. Keys are filenames. For each key, if the corresponding value is an + // object, it's considered a subdirectory, otherwise it's a file with that value as + // its contents. + type Directory = { [fileName: string]: string | Directory | undefined }; + let files: Directory; + + function getFile(filePath: string): string | Directory | undefined; + function getFile(filePath: string, throwIfMissing: 'throwIfMissing'): string | Directory; + function getFile(filePath: string, throwIfMissing?: 'throwIfMissing') { + const segments = filePath.split(/[\\/]/); + let dir: Directory | string = files; + let currentPath = ''; + for (const fileName of segments) { + if (typeof dir === 'string') { + throw new Error(`${currentPath} is not a directory`); + } else if (fileName !== '') { + const child: string | Directory | undefined = dir[fileName]; + if (child === undefined) { + if (throwIfMissing) { + const err: NodeJS.ErrnoException = new Error(`${currentPath} does not contain ${fileName}`); + err.code = 'ENOENT'; + throw err; + } else { + return undefined; + } + } + dir = child; + currentPath = `${currentPath}/${fileName}`; + } + } + return dir; + } + + // exec("command") is stubbed such that if either getFile(`${entry}/command`) or + // getFile(`${entry}/command.exe`) returns a non-empty string, it succeeds with + // that string as stdout. Otherwise, the exec stub throws. Empty strings can be + // used to simulate files that are present but not executable. + let execPath: string[]; + + async function expectConda(expectedPath: string) { + const expectedInfo = JSON.parse(getFile(expectedPath) as string); + + const conda = await Conda.getConda(); + expect(conda).to.not.equal(undefined, 'conda should not be missing'); + + const info = await conda!.getInfo(); + expect(info).to.deep.equal(expectedInfo); + } + + function condaInfo(condaVersion?: string): CondaInfo { + return { + conda_version: condaVersion, + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + default_prefix: '/conda/base', + envs: [], + }; + } + + let getPythonSetting: sinon.SinonStub; + let condaVersionOutput: string; + + setup(() => { + osType = platform.OSType.Unknown; + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.withArgs('condaPath').returns('conda'); + homeDir = undefined; + execPath = []; + files = {}; + registryInterpreters = []; + + sinon.stub(windowsUtils, 'getRegistryInterpreters').callsFake(async () => registryInterpreters); + + sinon.stub(platform, 'getOSType').callsFake(() => osType); + + sinon.stub(platform, 'getUserHomeDir').callsFake(() => homeDir); + + sinon.stub(fs, 'lstat').callsFake(async (filePath: fs.PathLike) => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } + const file = getFile(filePath, 'throwIfMissing'); + return { + isDirectory: () => typeof file !== 'string', + } as fs.Stats; + }); + + sinon.stub(fs, 'pathExists').callsFake(async (filePath: string | Buffer) => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } + try { + getFile(filePath, 'throwIfMissing'); + } catch { + return false; + } + return true; + }); + + sinon.stub(fs, 'readdir').callsFake( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async (filePath: fs.PathLike, options?: { withFileTypes?: boolean }): Promise<any> => { + if (typeof filePath !== 'string') { + throw new Error(`expected path to be string, got ${typeof path}`); + } + + const dir = getFile(filePath, 'throwIfMissing'); + if (typeof dir === 'string') { + throw new Error(`${path} is not a directory`); + } + + if (options === undefined) { + return (Object.keys(getFile(filePath, 'throwIfMissing')) as unknown) as fs.Dirent[]; + } + + const names = Object.keys(dir); + if (!options?.withFileTypes) { + return names; + } + + return names.map( + (name): fs.Dirent => { + const isFile = typeof dir[name] === 'string'; + return { + name, + path: dir.name?.toString() ?? '', + isFile: () => isFile, + isDirectory: () => !isFile, + isBlockDevice: () => false, + isCharacterDevice: () => false, + isSymbolicLink: () => false, + isFIFO: () => false, + isSocket: () => false, + parentPath: '', + }; + }, + ); + }, + ); + const readFileStub = async ( + filePath: fs.PathOrFileDescriptor, + options: { encoding: BufferEncoding; flag?: string | undefined } | BufferEncoding, + ): Promise<string> => { + if (typeof filePath !== 'string') { + throw new Error(`expected filePath to be string, got ${typeof filePath}`); + } else if (typeof options === 'string') { + if (options !== 'utf8') { + throw new Error(`Unsupported encoding ${options}`); + } + } else if ((options as any).encoding !== 'utf8') { + throw new Error(`Unsupported encoding ${(options as any).encoding}`); + } + + const contents = getFile(filePath); + if (typeof contents !== 'string') { + throw new Error(`${filePath} is not a file`); + } + + return contents; + }; + sinon.stub(fs, 'readFile' as any).callsFake(readFileStub as any); + + sinon.stub(externalDependencies, 'exec').callsFake(async (command: string, args: string[]) => { + for (const prefix of ['', ...execPath]) { + const contents = getFile(path.join(prefix, command)); + if (args[0] === 'info' && args[1] === '--json') { + if (typeof contents === 'string' && contents !== '') { + return { stdout: contents }; + } + } else if (args[0] === '--version') { + return { stdout: condaVersionOutput }; + } else { + throw new Error(`Invalid arguments: ${util.inspect(args)}`); + } + } + throw new Error(`${command} is missing or is not executable`); + }); + }); + + teardown(() => { + condaVersionOutput = ''; + sinon.restore(); + }); + + suite('Conda binary is located correctly', () => { + test('Must not find conda if it is missing', async () => { + const conda = await Conda.getConda(); + expect(conda).to.equal(undefined, 'conda should be missing'); + }); + + test('Must find conda using `python.condaPath` setting and prefer it', async () => { + getPythonSetting.withArgs('condaPath').returns('condaPath/conda'); + + files = { + condaPath: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + }; + await expectConda('/condaPath/conda'); + }); + + test('Must find conda on PATH, and prefer it', async () => { + osType = platform.OSType.Linux; + execPath = ['/bin']; + + files = { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + opt: { + anaconda: { + bin: { + conda: JSON.stringify(condaInfo('4.8.1')), + }, + }, + }, + }; + + await expectConda('/bin/conda'); + }); + + test('Use conda.bat when possible over conda.exe on windows', async () => { + osType = platform.OSType.Windows; + + getPythonSetting.withArgs('condaPath').returns('bin/conda'); + files = { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + condabin: { + 'conda.bat': JSON.stringify(condaInfo('4.8.0')), + }, + }; + + await expectConda('/condabin/conda.bat'); + }); + + suite('Must find conda in well-known locations', () => { + const condaDirNames = ['Anaconda', 'anaconda', 'Miniconda', 'miniconda']; + + condaDirNames.forEach((condaDirName) => { + suite(`Must find conda in well-known locations on Linux with ${condaDirName} directory name`, () => { + setup(() => { + osType = platform.OSType.Linux; + homeDir = '/home/user'; + + files = { + home: { + user: { + opt: {}, + }, + }, + opt: { + homebrew: { + bin: {}, + }, + }, + usr: { + share: { + doc: {}, + }, + local: { + share: { + doc: {}, + }, + }, + }, + }; + }); + + [ + '/usr/share', + '/usr/local/share', + '/opt', + '/opt/homebrew/bin', + '/home/user', + '/home/user/opt', + ].forEach((prefix) => { + const condaPath = `${prefix}/${condaDirName}`; + + test(`Must find conda in ${condaPath}`, async () => { + const prefixDir = getFile(prefix) as Directory; + prefixDir[condaDirName] = { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + }; + + await expectConda(`${condaPath}/bin/conda`); + }); + }); + }); + + suite(`Must find conda in well-known locations on Windows with ${condaDirName} directory name`, () => { + setup(() => { + osType = platform.OSType.Windows; + homeDir = 'E:\\Users\\user'; + + sinon + .stub(platform, 'getEnvironmentVariable') + .withArgs('PROGRAMDATA') + .returns('D:\\ProgramData') + .withArgs('LOCALAPPDATA') + .returns('F:\\Users\\user\\AppData\\Local'); + + files = { + 'C:': {}, + 'D:': { + ProgramData: {}, + }, + 'E:': { + Users: { + user: {}, + }, + }, + 'F:': { + Users: { + user: { + AppData: { + Local: { + Continuum: {}, + }, + }, + }, + }, + }, + }; + }); + + // Drive letters are intentionally unusual to ascertain that locator doesn't hardcode paths. + ['D:\\ProgramData', 'E:\\Users\\user', 'F:\\Users\\user\\AppData\\Local\\Continuum'].forEach( + (prefix) => { + const condaPath = `${prefix}\\${condaDirName}`; + + test(`Must find conda in ${condaPath}`, async () => { + const prefixDir = getFile(prefix) as Directory; + prefixDir[condaDirName] = { + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.0')), + }, + }; + + await expectConda(`${condaPath}\\Scripts\\conda.exe`); + }); + }, + ); + }); + }); + }); + + suite('Must find conda in environments.txt', () => { + test('Must find conda in environments.txt on Unix', async () => { + osType = platform.OSType.Linux; + homeDir = '/home/user'; + + files = { + home: { + user: { + '.conda': { + 'environments.txt': ['', '/missing', '', '# comment', '', ' /present ', ''].join( + '\n', + ), + }, + }, + }, + present: { + bin: { + conda: JSON.stringify(condaInfo('4.8.0')), + }, + }, + }; + + await expectConda('/present/bin/conda'); + }); + + test('Must find conda in environments.txt on Windows', async () => { + osType = platform.OSType.Windows; + homeDir = 'D:\\Users\\user'; + + files = { + 'D:': { + Users: { + user: { + '.conda': { + 'environments.txt': [ + '', + 'C:\\Missing', + '', + '# comment', + '', + ' E:\\Present ', + '', + ].join('\r\n'), + }, + }, + }, + }, + 'E:': { + Present: { + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.0')), + }, + }, + }, + }; + + await expectConda('E:\\Present\\Scripts\\conda.exe'); + }); + }); + + test('Must find conda in the registry', async () => { + osType = platform.OSType.Windows; + + registryInterpreters = [ + { + interpreterPath: 'C:\\Python2\\python.exe', + }, + { + interpreterPath: 'C:\\Anaconda2\\python.exe', + distroOrgName: 'ContinuumAnalytics', + }, + { + interpreterPath: 'C:\\Python3\\python.exe', + distroOrgName: 'PythonCore', + }, + { + interpreterPath: 'C:\\Anaconda3\\python.exe', + distroOrgName: 'ContinuumAnalytics', + }, + ]; + + files = { + 'C:': { + Python3: { + // Shouldn't be located because it's not a well-known conda path, + // and it's listed under PythonCore in the registry. + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.0')), + }, + }, + Anaconda2: { + // Shouldn't be located because it can't handle "conda info --json". + Scripts: { + 'conda.exe': '', + }, + }, + Anaconda3: { + Scripts: { + 'conda.exe': JSON.stringify(condaInfo('4.8.1')), + }, + }, + }, + }; + + await expectConda('C:\\Anaconda3\\Scripts\\conda.exe'); + }); + }); + + test('Conda version returns version info using `conda info` command if applicable', async () => { + files = { + conda: JSON.stringify(condaInfo('4.8.0')), + }; + const conda = await Conda.getConda(); + const version = await conda?.getCondaVersion(); + expect(version).to.not.equal(undefined); + expect(eq(version!, '4.8.0')).to.equal(true); + }); + + test('Conda version returns version info using `conda --version` command otherwise', async () => { + files = { + conda: JSON.stringify(condaInfo()), + }; + condaVersionOutput = 'conda 4.8.0'; + const conda = await Conda.getConda(); + const version = await conda?.getCondaVersion(); + expect(version).to.not.equal(undefined); + expect(eq(version!, '4.8.0')).to.equal(true); + }); + + test('Conda version works for dev versions of conda', async () => { + files = { + conda: JSON.stringify(condaInfo('23.1.0.post7+d5281f611')), + }; + condaVersionOutput = 'conda 23.1.0.post7+d5281f611'; + const conda = await Conda.getConda(); + const version = await conda?.getCondaVersion(); + expect(version).to.not.equal(undefined); + expect(eq(version!, '23.1.0')).to.equal(true); + }); + + test('Conda run args returns `undefined` for conda version below 4.9.0', async () => { + files = { + conda: JSON.stringify(condaInfo('4.8.0')), + }; + const conda = await Conda.getConda(); + const args = await conda?.getRunPythonArgs({ name: 'envName', prefix: 'envPrefix' }); + expect(args).to.equal(undefined); + }); + + test('Conda run args returns appropriate args for conda version starting with 4.9.0', async () => { + files = { + conda: JSON.stringify(condaInfo('4.9.0')), + }; + const conda = await Conda.getConda(); + let args = await conda?.getRunPythonArgs({ name: 'envName', prefix: 'envPrefix' }); + expect(args).to.not.equal(undefined); + assert.deepStrictEqual( + args, + ['conda', 'run', '-p', 'envPrefix', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + 'Incorrect args for case 1', + ); + + args = await conda?.getRunPythonArgs({ name: '', prefix: 'envPrefix' }); + assert.deepStrictEqual( + args, + ['conda', 'run', '-p', 'envPrefix', '--no-capture-output', 'python', OUTPUT_MARKER_SCRIPT], + 'Incorrect args for case 2', + ); + }); + + suite('Conda env list is parsed correctly', () => { + setup(() => { + homeDir = '/home/user'; + files = { + home: { + user: { + miniconda3: { + bin: { + python: '', + conda: JSON.stringify({ + conda_version: '4.8.0', + python_version: '3.9.0', + 'sys.version': '3.9.0', + 'sys.prefix': '/some/env', + root_prefix: '/home/user/miniconda3', + default_prefix: '/home/user/miniconda3/envs/env1', + envs_dirs: ['/home/user/miniconda3/envs', '/home/user/.conda/envs'], + envs: [ + '/home/user/miniconda3', + '/home/user/miniconda3/envs/env1', + '/home/user/miniconda3/envs/env2', + '/home/user/miniconda3/envs/dir/env3', + '/home/user/.conda/envs/env4', + '/home/user/.conda/envs/env5', + '/env6', + ], + }), + }, + envs: { + env1: { + bin: { + python: '', + }, + }, + dir: { + env3: { + bin: { + python: '', + }, + }, + }, + }, + }, + '.conda': { + envs: { + env4: { + bin: { + python: '', + }, + }, + }, + }, + }, + }, + env6: { + bin: { + python: '', + }, + }, + }; + sinon.stub(externalDependencies, 'inExperiment').returns(false); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Must compute conda environment name from prefix', async () => { + const conda = new Conda('/home/user/miniconda3/bin/conda'); + const envs = await conda.getEnvList(); + + expect(envs).to.have.deep.members([ + { + prefix: '/home/user/miniconda3', + name: 'base', + }, + { + prefix: '/home/user/miniconda3/envs/env1', + name: 'env1', + }, + { + prefix: '/home/user/miniconda3/envs/env2', + name: 'env2', + }, + { + prefix: '/home/user/miniconda3/envs/dir/env3', + name: undefined, // because it's not directly under envsDirs + }, + { + prefix: '/home/user/.conda/envs/env4', + name: 'env4', + }, + { + prefix: '/home/user/.conda/envs/env5', + name: 'env5', + }, + { + prefix: '/env6', + name: undefined, // because it's not directly under envsDirs + }, + ]); + }); + + test('Must iterate conda environments correctly', async () => { + const locator = new CondaEnvironmentLocator(); + const envs = await getEnvs(locator.iterEnvs()); + const expected = [ + '/home/user/miniconda3', + '/home/user/miniconda3/envs/env1', + '/home/user/miniconda3/envs/dir/env3', + '/home/user/.conda/envs/env4', + '/env6', + ].map((envPath) => + createBasicEnv(PythonEnvKind.Conda, path.join(envPath, 'bin', 'python'), undefined, envPath), + ); + expected.push( + ...[ + '/home/user/miniconda3/envs/env2', // Show env2 despite there's no bin/python* under it + '/home/user/.conda/envs/env5', // Show env5 despite there's no bin/python* under it + ].map((envPath) => createBasicEnv(PythonEnvKind.Conda, 'python', undefined, envPath)), + ); + assertBasicEnvsEqual(envs, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts new file mode 100644 index 000000000000..5d348aa2b131 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/hatch.unit.test.ts @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { Hatch } from '../../../../client/pythonEnvironments/common/environmentManagers/hatch'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +export type HatchCommand = { cmd: 'env show --json' } | { cmd: 'env find'; env: string } | { cmd: null }; + +export function hatchCommand(args: string[]): HatchCommand { + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'env' && args[1] === 'show' && args[2] === '--json') { + return { cmd: 'env show --json' }; + } + if (args[0] === 'env' && args[1] === 'find') { + return { cmd: 'env find', env: args[2] }; + } + return { cmd: null }; +} + +interface VerifyOptions { + path?: boolean; + cwd?: string; +} + +export function makeExecHandler(venvDirs: Record<string, string>, verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise<ExecutionResult<string>> => { + if (verify.path && file !== 'hatch') { + throw new Error('Command failed'); + } + if (verify.cwd) { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error('Command failed'); + } + } + const cmd = hatchCommand(args); + if (cmd.cmd === 'env show --json') { + const envs = Object.fromEntries(Object.keys(venvDirs).map((name) => [name, { type: 'virtual' }])); + return { stdout: JSON.stringify(envs) }; + } + if (cmd.cmd === 'env find' && cmd.env in venvDirs) { + return { stdout: venvDirs[cmd.env] }; + } + throw new Error('Command failed'); + }; +} + +const testHatchDir = path.join(TEST_LAYOUT_ROOT, 'hatch'); +// This is usually in <data-dir>/hatch, e.g. `~/.local/share/hatch` +const hatchEnvsDir = path.join(testHatchDir, 'env/virtual/python'); +export const projectDirs = { + project1: path.join(testHatchDir, 'project1'), + project2: path.join(testHatchDir, 'project2'), +}; +export const venvDirs = { + project1: { default: path.join(hatchEnvsDir, 'cK2g6fIm/project1') }, + project2: { + default: path.join(hatchEnvsDir, 'q4In3tK-/project2'), + test: path.join(hatchEnvsDir, 'q4In3tK-/test'), + }, +}; + +suite('Hatch binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (verify = true) => { + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake( + makeExecHandler(venvDirs.project1, verify ? { path: true, cwd: projectDirs.project1 } : undefined), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal('hatch'); + }; + + test('Use Hatch on PATH if available', () => testPath()); + + test('Return undefined if Hatch cannot be found', async () => { + getPythonSetting.returns('hatch'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const hatch = await Hatch.getHatch(projectDirs.project1); + expect(hatch?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.unit.test.ts new file mode 100644 index 000000000000..59bbf5e53167 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/microsoftStoreEnv.unit.test.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../../client/common/utils/platform'; +import { getMicrosoftStorePythonExes } from '../../../../client/pythonEnvironments/base/locators/lowLevel/microsoftStoreLocator'; +import { isMicrosoftStoreDir } from '../../../../client/pythonEnvironments/common/environmentManagers/microsoftStoreEnv'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('Microsoft Store Env', () => { + let getEnvVarStub: sinon.SinonStub; + const testLocalAppData = path.join(TEST_LAYOUT_ROOT, 'storeApps'); + const testStoreAppRoot = path.join(testLocalAppData, 'Microsoft', 'WindowsApps'); + + setup(() => { + getEnvVarStub = sinon.stub(platformApis, 'getEnvironmentVariable'); + getEnvVarStub.withArgs('LOCALAPPDATA').returns(testLocalAppData); + }); + + teardown(() => { + getEnvVarStub.restore(); + }); + + test('Store Python Interpreters', async () => { + const expected = [path.join(testStoreAppRoot, 'python3.7.exe'), path.join(testStoreAppRoot, 'python3.8.exe')]; + + const actual = await getMicrosoftStorePythonExes(); + assert.deepEqual(actual, expected); + }); + + test('isMicrosoftStoreDir: valid case', () => { + assert.deepStrictEqual(isMicrosoftStoreDir(testStoreAppRoot), true); + assert.deepStrictEqual(isMicrosoftStoreDir(testStoreAppRoot + path.sep), true); + }); + + test('isMicrosoftStoreDir: invalid case', () => { + assert.deepStrictEqual(isMicrosoftStoreDir(__dirname), false); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pipenv.functional.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pipenv.functional.test.ts new file mode 100644 index 000000000000..33d2a9eb1fe4 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pipenv.functional.test.ts @@ -0,0 +1,47 @@ +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../../client/common/utils/platform'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { isPipenvEnvironmentRelatedToFolder } from '../../../../client/pythonEnvironments/common/environmentManagers/pipenv'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +suite('Pipenv utils', () => { + let readFile: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + }); + + test('Global pipenv environment is associated with a project whose Pipfile lies at 3 levels above the project', async () => { + getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); + const expectedDotProjectFile = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project3-2s1eXEJ2', + '.project', + ); + const project = path.join(TEST_LAYOUT_ROOT, 'pipenv', 'project3'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + const interpreterPath: string = path.join( + TEST_LAYOUT_ROOT, + 'pipenv', + 'globalEnvironments', + 'project3-2s1eXEJ2', + 'Scripts', + 'python.exe', + ); + const folder = path.join(project, 'parent', 'child', 'folder'); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, true); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pipenv.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pipenv.unit.test.ts new file mode 100644 index 000000000000..8a30ca5153ae --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pipenv.unit.test.ts @@ -0,0 +1,278 @@ +import * as assert from 'assert'; +import * as pathModule from 'path'; +import * as sinon from 'sinon'; +import * as platformApis from '../../../../client/common/utils/platform'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + _getAssociatedPipfile, + isPipenvEnvironment, + isPipenvEnvironmentRelatedToFolder, +} from '../../../../client/pythonEnvironments/common/environmentManagers/pipenv'; + +const path = platformApis.getOSType() === platformApis.OSType.Windows ? pathModule.win32 : pathModule.posix; + +suite('Pipenv helper', () => { + suite('isPipenvEnvironmentRelatedToFolder()', async () => { + let readFile: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + let arePathsSame: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + arePathsSame = sinon.stub(externalDependencies, 'arePathsSame'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + pathExists.restore(); + arePathsSame.restore(); + }); + + test('If no Pipfile is associated with the environment, return false', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + // Dot project file doesn't exist + pathExists.withArgs(expectedDotProjectFile).resolves(false); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, false); + }); + + test('If a Pipfile is associated with the environment but no pipfile is associated with the folder, return false', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + const project = path.join('path', 'to', 'project'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(project).resolves(true); + const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + // Pipfile associated with folder doesn't exist + pathExists.withArgs(pipFileAssociatedWithFolder).resolves(false); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, false); + }); + + test('If a Pipfile is associated with the environment and another is associated with the folder, but the path to both Pipfiles are different, return false', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + const project = path.join('path', 'to', 'project'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(project).resolves(true); + const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + // Pipfile associated with folder exists + pathExists.withArgs(pipFileAssociatedWithFolder).resolves(true); + // But the paths to both Pipfiles aren't the same + arePathsSame.withArgs(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder).resolves(false); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, false); + }); + + test('If a Pipfile is associated with the environment and another is associated with the folder, and the path to both Pipfiles are same, return true', async () => { + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + const project = path.join('path', 'to', 'project'); + readFile.withArgs(expectedDotProjectFile).resolves(project); + pathExists.withArgs(project).resolves(true); + const pipFileAssociatedWithEnvironment = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFileAssociatedWithEnvironment).resolves(true); + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + pathExists.withArgs(interpreterPath).resolves(true); + const folder = path.join('path', 'to', 'folder'); + const pipFileAssociatedWithFolder = path.join(folder, 'Pipfile'); + // Pipfile associated with folder exists + pathExists.withArgs(pipFileAssociatedWithFolder).resolves(true); + // The paths to both Pipfiles are also the same + arePathsSame.withArgs(pipFileAssociatedWithEnvironment, pipFileAssociatedWithFolder).resolves(true); + + const isRelated = await isPipenvEnvironmentRelatedToFolder(interpreterPath, folder); + + assert.strictEqual(isRelated, true); + }); + }); + + suite('isPipenvEnvironment()', async () => { + let readFile: sinon.SinonStub; + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + readFile = sinon.stub(externalDependencies, 'readFile'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + readFile.restore(); + getEnvVar.restore(); + pathExists.restore(); + }); + + test('If the project layout matches that of a local pipenv environment, return true', async () => { + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + // Environment is inside the project + const interpreterPath = path.join(project, '.venv', 'Scripts', 'python.exe'); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + + test('If not local & dotProject file is missing, return false', async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + // dotProject file doesn't exist + pathExists.withArgs(expectedDotProjectFile).resolves(false); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('If not local & dotProject contains invalid path to project, return false', async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const project = path.join('path', 'to', 'project'); + // Project doesn't exist + pathExists.withArgs(project).resolves(false); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + // dotProject file doesn't exist + pathExists.withArgs(expectedDotProjectFile).resolves(false); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + readFile.withArgs(expectedDotProjectFile).resolves(project); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test("If not local & the name of the project isn't used as a prefix in the environment folder, return false", async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + // The project name (someProjectName) isn't used as a prefix in environment folder name (project-2s1eXEJ2) + const project = path.join('path', 'to', 'someProjectName'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + readFile.withArgs(expectedDotProjectFile).resolves(project); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('If the project layout matches that of a global pipenv environment, return true', async () => { + const interpreterPath = path.join('environments', 'project-2s1eXEJ2', 'Scripts', 'python.exe'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile associated with environment exists + pathExists.withArgs(pipFile).resolves(true); + const expectedDotProjectFile = path.join('environments', 'project-2s1eXEJ2', '.project'); + pathExists.withArgs(expectedDotProjectFile).resolves(true); + readFile.withArgs(expectedDotProjectFile).resolves(project); + + const result = await isPipenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + }); + + suite('_getAssociatedPipfile()', async () => { + let getEnvVar: sinon.SinonStub; + let pathExists: sinon.SinonStub; + setup(() => { + getEnvVar = sinon.stub(platformApis, 'getEnvironmentVariable'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + getEnvVar.restore(); + pathExists.restore(); + }); + + test('Correct Pipfile is returned for folder whose Pipfile lies in the folder directory', async () => { + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + pathExists.withArgs(pipFile).resolves(true); + const folder = project; + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: false }); + + assert.strictEqual(result, pipFile); + }); + + test('Correct Pipfile is returned for folder if a custom Pipfile name is being used', async () => { + getEnvVar.withArgs('PIPENV_PIPFILE').returns('CustomPipfile'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'CustomPipfile'); + pathExists.withArgs(pipFile).resolves(true); + const folder = project; + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: false }); + + assert.strictEqual(result, pipFile); + }); + + test('Correct Pipfile is returned for folder whose Pipfile lies 3 levels above the folder', async () => { + getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + pathExists.withArgs(pipFile).resolves(true); + const folder = path.join(project, 'parent', 'child', 'folder'); + pathExists.withArgs(folder).resolves(true); + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: true }); + + assert.strictEqual(result, pipFile); + }); + + test('No Pipfile is returned for folder if no Pipfile exists in the associated directories', async () => { + getEnvVar.withArgs('PIPENV_MAX_DEPTH').returns('5'); + const project = path.join('path', 'to', 'project'); + pathExists.withArgs(project).resolves(true); + const pipFile = path.join(project, 'Pipfile'); + // Pipfile doesn't exist + pathExists.withArgs(pipFile).resolves(false); + const folder = path.join(project, 'parent', 'child', 'folder'); + pathExists.withArgs(folder).resolves(true); + + const result = await _getAssociatedPipfile(folder, { lookIntoParentDirectories: true }); + + assert.strictEqual(result, undefined); + }); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts new file mode 100644 index 000000000000..0cbc6b25145c --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pixi.unit.test.ts @@ -0,0 +1,147 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; +import { getPixi } from '../../../../client/pythonEnvironments/common/environmentManagers/pixi'; + +export type PixiCommand = { cmd: 'info --json' } | { cmd: '--version' } | { cmd: null }; + +const textPixiDir = path.join(TEST_LAYOUT_ROOT, 'pixi'); +export const projectDirs = { + windows: { + path: path.join(textPixiDir, 'windows'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'windows', '.pixi', 'envs', 'default'), + }, + ], + }, + }, + nonWindows: { + path: path.join(textPixiDir, 'non-windows'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'non-windows', '.pixi', 'envs', 'default'), + }, + ], + }, + }, + multiEnv: { + path: path.join(textPixiDir, 'multi-env'), + info: { + environments_info: [ + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'default'), + }, + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'py310'), + }, + { + prefix: path.join(textPixiDir, 'multi-env', '.pixi', 'envs', 'py311'), + }, + ], + }, + }, +}; + +/** + * Convert the command line arguments into a typed command. + */ +export function pixiCommand(args: string[]): PixiCommand { + if (args[0] === '--version') { + return { cmd: '--version' }; + } + + if (args.length < 2) { + return { cmd: null }; + } + if (args[0] === 'info' && args[1] === '--json') { + return { cmd: 'info --json' }; + } + return { cmd: null }; +} +interface VerifyOptions { + pixiPath?: string; + cwd?: string; +} + +export function makeExecHandler(verify: VerifyOptions = {}) { + return async (file: string, args: string[], options: ShellOptions): Promise<ExecutionResult<string>> => { + /// Verify that the executable path is indeed the one we expect it to be + if (verify.pixiPath && file !== verify.pixiPath) { + throw new Error('Command failed: not the correct pixi path'); + } + + const cmd = pixiCommand(args); + if (cmd.cmd === '--version') { + return { stdout: 'pixi 0.24.1' }; + } + + /// Verify that the working directory is the expected one + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (verify.cwd) { + if (!cwd || !externalDependencies.arePathsSame(cwd, verify.cwd)) { + throw new Error(`Command failed: not the correct path, expected: ${verify.cwd}, got: ${cwd}`); + } + } + + /// Convert the command into a single string + if (cmd.cmd === 'info --json') { + const project = Object.values(projectDirs).find((p) => cwd?.startsWith(p.path)); + if (!project) { + throw new Error('Command failed: could not find project'); + } + return { stdout: JSON.stringify(project.info) }; + } + + throw new Error(`Command failed: unknown command ${args}`); + }; +} + +suite('Pixi binary is located correctly', async () => { + let exec: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + let pathExists: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + exec = sinon.stub(externalDependencies, 'exec'); + pathExists = sinon.stub(externalDependencies, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + const testPath = async (pixiPath: string, verify = true) => { + getPythonSetting.returns(pixiPath); + pathExists.returns(pixiPath !== 'pixi'); + // If `verify` is false, don’t verify that the command has been called with that path + exec.callsFake(makeExecHandler(verify ? { pixiPath } : undefined)); + const pixi = await getPixi(); + + if (pixiPath === 'pixi') { + expect(pixi).to.equal(undefined); + } else { + expect(pixi?.command).to.equal(pixiPath); + } + }; + + test('Return a Pixi instance in an empty directory', () => testPath('pixiPath', false)); + test('When user has specified a valid Pixi path, use it', () => testPath('path/to/pixi/binary')); + // 'pixi' is the default value + test('When user hasn’t specified a path, use Pixi on PATH if available', () => testPath('pixi')); + + test('Return undefined if Pixi cannot be found', async () => { + getPythonSetting.returns('pixi'); + exec.callsFake((_file: string, _args: string[], _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + const pixi = await getPixi(); + expect(pixi?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts new file mode 100644 index 000000000000..5e40e3454e2b --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/poetry.unit.test.ts @@ -0,0 +1,195 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert, expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { ExecutionResult, ShellOptions } from '../../../../client/common/process/types'; +import * as platformApis from '../../../../client/common/utils/platform'; +import * as externalDependencies from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { isPoetryEnvironment, Poetry } from '../../../../client/pythonEnvironments/common/environmentManagers/poetry'; +import { TEST_LAYOUT_ROOT } from '../commonTestConstants'; + +const testPoetryDir = path.join(TEST_LAYOUT_ROOT, 'poetry'); +const project1 = path.join(testPoetryDir, 'project1'); +const project4 = path.join(testPoetryDir, 'project4'); +const project3 = path.join(testPoetryDir, 'project3'); + +suite('isPoetryEnvironment Tests', () => { + let shellExecute: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + suite('Global poetry environment', async () => { + setup(() => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + }); + teardown(() => { + sinon.restore(); + }); + test('Return true if environment folder name matches global env pattern and environment is of virtual env type', async () => { + const result = await isPoetryEnvironment( + path.join(testPoetryDir, 'poetry-tutorial-project-6hnqYwvD-py3.8', 'Scripts', 'python.exe'), + ); + expect(result).to.equal(true); + }); + + test('Return false if environment folder name does not matches env pattern', async () => { + const result = await isPoetryEnvironment( + path.join(testPoetryDir, 'wannabeglobalenv', 'Scripts', 'python.exe'), + ); + expect(result).to.equal(false); + }); + + test('Return false if environment folder name matches env pattern but is not of virtual env type', async () => { + const result = await isPoetryEnvironment( + path.join(testPoetryDir, 'project1-haha-py3.8', 'Scripts', 'python.exe'), + ); + expect(result).to.equal(false); + }); + }); + + suite('Local poetry environment', async () => { + setup(() => { + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + getPythonSetting.returns('poetry'); + shellExecute.callsFake((command: string, _options: ShellOptions) => { + if (command === 'poetry env list --full-path') { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Return true if environment folder name matches criteria for local envs', async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + const result = await isPoetryEnvironment(path.join(project1, '.venv', 'Scripts', 'python.exe')); + expect(result).to.equal(true); + }); + + test(`Return false if environment folder name is not named '.venv' for local envs`, async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Windows); + const result = await isPoetryEnvironment(path.join(project1, '.venv2', 'Scripts', 'python.exe')); + expect(result).to.equal(false); + }); + + test(`Return false if running poetry for project dir as cwd fails (pyproject.toml file is invalid)`, async () => { + sinon.stub(platformApis, 'getOSType').callsFake(() => platformApis.OSType.Linux); + const result = await isPoetryEnvironment(path.join(project4, '.venv', 'bin', 'python')); + expect(result).to.equal(false); + }); + }); +}); + +suite('Poetry binary is located correctly', async () => { + let shellExecute: sinon.SinonStub; + let getPythonSetting: sinon.SinonStub; + + setup(() => { + getPythonSetting = sinon.stub(externalDependencies, 'getPythonSetting'); + shellExecute = sinon.stub(externalDependencies, 'shellExecute'); + }); + + teardown(() => { + sinon.restore(); + }); + + test("Return undefined if pyproject.toml doesn't exist in cwd", async () => { + getPythonSetting.returns('poetryPath'); + shellExecute.callsFake((_command: string, _options: ShellOptions) => + Promise.resolve<ExecutionResult<string>>({ stdout: '' }), + ); + + const poetry = await Poetry.getPoetry(testPoetryDir); + + expect(poetry?.command).to.equal(undefined); + }); + + test('Return undefined if cwd contains pyproject.toml which does not contain a poetry section', async () => { + getPythonSetting.returns('poetryPath'); + shellExecute.callsFake((_command: string, _options: ShellOptions) => + Promise.resolve<ExecutionResult<string>>({ stdout: '' }), + ); + + const poetry = await Poetry.getPoetry(project3); + + expect(poetry?.command).to.equal(undefined); + }); + + test('When user has specified a valid poetry path, use it', async () => { + getPythonSetting.returns('poetryPath'); + shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if ( + command === `poetryPath env list --full-path` && + cwd && + externalDependencies.arePathsSame(cwd, project1) + ) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal('poetryPath'); + }); + + test("When user hasn't specified a path, use poetry on PATH if available", async () => { + getPythonSetting.returns('poetry'); // Setting returns the default value + shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if (command === `poetry env list --full-path` && cwd && externalDependencies.arePathsSame(cwd, project1)) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal('poetry'); + }); + + test('When poetry is not available on PATH, try using the default poetry location if valid', async () => { + const home = platformApis.getUserHomeDir(); + if (!home) { + assert(true); + return; + } + const defaultPoetry = path.join(home, '.poetry', 'bin', 'poetry'); + const pathExistsSync = sinon.stub(externalDependencies, 'pathExistsSync'); + pathExistsSync.withArgs(defaultPoetry).returns(true); + pathExistsSync.callThrough(); + getPythonSetting.returns('poetry'); + shellExecute.callsFake((command: string, options: ShellOptions) => { + const cwd = typeof options.cwd === 'string' ? options.cwd : options.cwd?.toString(); + if ( + command === `${defaultPoetry} env list --full-path` && + cwd && + externalDependencies.arePathsSame(cwd, project1) + ) { + return Promise.resolve<ExecutionResult<string>>({ stdout: '' }); + } + return Promise.reject(new Error('Command failed')); + }); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal(defaultPoetry); + }); + + test('Return undefined otherwise', async () => { + getPythonSetting.returns('poetry'); + shellExecute.callsFake((_command: string, _options: ShellOptions) => + Promise.reject(new Error('Command failed')), + ); + + const poetry = await Poetry.getPoetry(project1); + + expect(poetry?.command).to.equal(undefined); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/pyenv.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/pyenv.unit.test.ts new file mode 100644 index 000000000000..e5902ae2b291 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/pyenv.unit.test.ts @@ -0,0 +1,305 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as platformUtils from '../../../../client/common/utils/platform'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + IPyenvVersionStrings, + isPyenvEnvironment, + isPyenvShimDir, + parsePyenvVersion, +} from '../../../../client/pythonEnvironments/common/environmentManagers/pyenv'; + +suite('Pyenv Identifier Tests', () => { + const home = platformUtils.getUserHomeDir() || ''; + let getEnvVariableStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let getOsTypeStub: sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getOsTypeStub = sinon.stub(platformUtils, 'getOSType'); + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + }); + + teardown(() => { + getEnvVariableStub.restore(); + pathExistsStub.restore(); + getOsTypeStub.restore(); + }); + + type PyenvUnitTestData = { + testTitle: string; + interpreterPath: string; + pyenvEnvVar?: string; + osType: platformUtils.OSType; + }; + + const testData: PyenvUnitTestData[] = [ + { + testTitle: 'undefined', + interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'undefined', + interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + osType: platformUtils.OSType.Windows, + }, + { + testTitle: 'its default value', + interpreterPath: path.join(home, '.pyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home, '.pyenv'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'its default value', + interpreterPath: path.join(home, '.pyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join(home, '.pyenv', 'pyenv-win'), + osType: platformUtils.OSType.Windows, + }, + { + testTitle: 'a custom value', + interpreterPath: path.join('path', 'to', 'mypyenv', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join('path', 'to', 'mypyenv'), + osType: platformUtils.OSType.Linux, + }, + { + testTitle: 'a custom value', + interpreterPath: path.join('path', 'to', 'mypyenv', 'pyenv-win', 'versions', '3.8.0', 'bin', 'python'), + pyenvEnvVar: path.join('path', 'to', 'mypyenv', 'pyenv-win'), + osType: platformUtils.OSType.Windows, + }, + ]; + + testData.forEach(({ testTitle, interpreterPath, pyenvEnvVar, osType }) => { + test(`The environment variable is set to ${testTitle} on ${osType}, and the interpreter path is in a subfolder of the pyenv folder`, async () => { + getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvEnvVar); + getEnvVariableStub.withArgs('PYENV').returns(pyenvEnvVar); + getOsTypeStub.returns(osType); + pathExistsStub.resolves(true); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, true); + }); + }); + + test('The pyenv directory does not exist', async () => { + const interpreterPath = path.join('path', 'to', 'python'); + + pathExistsStub.resolves(false); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); + + test('The interpreter path is not in a subfolder of the pyenv folder', async () => { + const interpreterPath = path.join('path', 'to', 'python'); + + pathExistsStub.resolves(true); + + const result = await isPyenvEnvironment(interpreterPath); + + assert.strictEqual(result, false); + }); +}); + +suite('Pyenv Versions Parser Test', () => { + interface IPyenvVersionTestData { + input: string; + expectedOutput?: IPyenvVersionStrings; + } + const testData: IPyenvVersionTestData[] = [ + { input: '2.7.0', expectedOutput: { pythonVer: '2.7.0', distro: undefined, distroVer: undefined } }, + { input: '2.7-dev', expectedOutput: { pythonVer: '2.7-dev', distro: undefined, distroVer: undefined } }, + { input: '2.7.18', expectedOutput: { pythonVer: '2.7.18', distro: undefined, distroVer: undefined } }, + { input: '3.9.0', expectedOutput: { pythonVer: '3.9.0', distro: undefined, distroVer: undefined } }, + { input: '3.9-dev', expectedOutput: { pythonVer: '3.9-dev', distro: undefined, distroVer: undefined } }, + { input: '3.10-dev', expectedOutput: { pythonVer: '3.10-dev', distro: undefined, distroVer: undefined } }, + { + input: 'activepython-2.7.14', + expectedOutput: { pythonVer: undefined, distro: 'activepython', distroVer: '2.7.14' }, + }, + { + input: 'activepython-3.6.0', + expectedOutput: { pythonVer: undefined, distro: 'activepython', distroVer: '3.6.0' }, + }, + { input: 'anaconda-4.0.0', expectedOutput: { pythonVer: undefined, distro: 'anaconda', distroVer: '4.0.0' } }, + { input: 'anaconda2-5.3.1', expectedOutput: { pythonVer: undefined, distro: 'anaconda2', distroVer: '5.3.1' } }, + { + input: 'anaconda2-2019.07', + expectedOutput: { pythonVer: undefined, distro: 'anaconda2', distroVer: '2019.07' }, + }, + { input: 'anaconda3-5.3.1', expectedOutput: { pythonVer: undefined, distro: 'anaconda3', distroVer: '5.3.1' } }, + { + input: 'anaconda3-2020.07', + expectedOutput: { pythonVer: undefined, distro: 'anaconda3', distroVer: '2020.07' }, + }, + { + input: 'graalpython-20.2.0', + expectedOutput: { pythonVer: undefined, distro: 'graalpython', distroVer: '20.2.0' }, + }, + { input: 'ironpython-dev', expectedOutput: { pythonVer: undefined, distro: 'ironpython', distroVer: 'dev' } }, + { + input: 'ironpython-2.7.6.3', + expectedOutput: { pythonVer: undefined, distro: 'ironpython', distroVer: '2.7.6.3' }, + }, + { + input: 'ironpython-2.7.7', + expectedOutput: { pythonVer: undefined, distro: 'ironpython', distroVer: '2.7.7' }, + }, + { input: 'jython-dev', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: 'dev' } }, + { input: 'jython-2.5.0', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.5.0' } }, + { input: 'jython-2.5-dev', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.5-dev' } }, + { + input: 'jython-2.5.4-rc1', + expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.5.4-rc1' }, + }, + { input: 'jython-2.7.2', expectedOutput: { pythonVer: undefined, distro: 'jython', distroVer: '2.7.2' } }, + { input: 'micropython-dev', expectedOutput: { pythonVer: undefined, distro: 'micropython', distroVer: 'dev' } }, + { + input: 'micropython-1.9.3', + expectedOutput: { pythonVer: undefined, distro: 'micropython', distroVer: '1.9.3' }, + }, + { + input: 'micropython-1.13', + expectedOutput: { pythonVer: undefined, distro: 'micropython', distroVer: '1.13' }, + }, + { + input: 'miniconda-latest', + expectedOutput: { pythonVer: undefined, distro: 'miniconda', distroVer: 'latest' }, + }, + { input: 'miniconda-2.2.2', expectedOutput: { pythonVer: undefined, distro: 'miniconda', distroVer: '2.2.2' } }, + { + input: 'miniconda-3.18.3', + expectedOutput: { pythonVer: undefined, distro: 'miniconda', distroVer: '3.18.3' }, + }, + { + input: 'miniconda2-latest', + expectedOutput: { pythonVer: undefined, distro: 'miniconda2', distroVer: 'latest' }, + }, + { + input: 'miniconda2-4.7.12', + expectedOutput: { pythonVer: undefined, distro: 'miniconda2', distroVer: '4.7.12' }, + }, + { + input: 'miniconda3-latest', + expectedOutput: { pythonVer: undefined, distro: 'miniconda3', distroVer: 'latest' }, + }, + { + input: 'miniconda3-4.7.12', + expectedOutput: { pythonVer: undefined, distro: 'miniconda3', distroVer: '4.7.12' }, + }, + { + input: 'miniforge3-4.9.2', + expectedOutput: { pythonVer: undefined, distro: 'miniforge3', distroVer: '4.9.2' }, + }, + { + input: 'pypy-c-jit-latest', + expectedOutput: { pythonVer: undefined, distro: 'pypy-c-jit', distroVer: 'latest' }, + }, + { + input: 'pypy-c-nojit-latest', + expectedOutput: { pythonVer: undefined, distro: 'pypy-c-nojit', distroVer: 'latest' }, + }, + { input: 'pypy-dev', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: 'dev' } }, + { input: 'pypy-stm-2.3', expectedOutput: { pythonVer: undefined, distro: 'pypy-stm', distroVer: '2.3' } }, + { input: 'pypy-stm-2.5.1', expectedOutput: { pythonVer: undefined, distro: 'pypy-stm', distroVer: '2.5.1' } }, + { input: 'pypy-5.4-src', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.4-src' } }, + { input: 'pypy-5.4', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.4' } }, + { input: 'pypy-5.7.1-src', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.7.1-src' } }, + { input: 'pypy-5.7.1', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.7.1' } }, + { input: 'pypy2-5.4-src', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4-src' } }, + { input: 'pypy2-5.4', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4' } }, + { input: 'pypy2-5.4.1-src', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4.1-src' } }, + { input: 'pypy2-5.4.1', expectedOutput: { pythonVer: '2', distro: 'pypy', distroVer: '5.4.1' } }, + { input: 'pypy2.7-7.3.1-src', expectedOutput: { pythonVer: '2.7', distro: 'pypy', distroVer: '7.3.1-src' } }, + { input: 'pypy2.7-7.3.1', expectedOutput: { pythonVer: '2.7', distro: 'pypy', distroVer: '7.3.1' } }, + { input: 'pypy3-2.4.0-src', expectedOutput: { pythonVer: '3', distro: 'pypy', distroVer: '2.4.0-src' } }, + { input: 'pypy3-2.4.0', expectedOutput: { pythonVer: '3', distro: 'pypy', distroVer: '2.4.0' } }, + { + input: 'pypy3.3-5.2-alpha1-src', + expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.2-alpha1-src' }, + }, + { input: 'pypy3.3-5.2-alpha1', expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.2-alpha1' } }, + { + input: 'pypy3.3-5.5-alpha-src', + expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.5-alpha-src' }, + }, + { input: 'pypy3.3-5.5-alpha', expectedOutput: { pythonVer: '3.3', distro: 'pypy', distroVer: '5.5-alpha' } }, + { + input: 'pypy3.5-c-jit-latest', + expectedOutput: { pythonVer: '3.5', distro: 'pypy-c-jit', distroVer: 'latest' }, + }, + { + input: 'pypy3.5-5.7-beta-src', + expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7-beta-src' }, + }, + { input: 'pypy3.5-5.7-beta', expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7-beta' } }, + { + input: 'pypy3.5-5.7.1-beta-src', + expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7.1-beta-src' }, + }, + { input: 'pypy3.5-5.7.1-beta', expectedOutput: { pythonVer: '3.5', distro: 'pypy', distroVer: '5.7.1-beta' } }, + { input: 'pypy3.6-7.3.1-src', expectedOutput: { pythonVer: '3.6', distro: 'pypy', distroVer: '7.3.1-src' } }, + { input: 'pypy3.6-7.3.1', expectedOutput: { pythonVer: '3.6', distro: 'pypy', distroVer: '7.3.1' } }, + { + input: 'pypy3.7-v7.3.5rc3-win64', + expectedOutput: { pythonVer: '3.7', distro: 'pypy', distroVer: '7.3.5rc3-win64' }, + }, + { + input: 'pypy3.7-v7.3.5-win64', + expectedOutput: { pythonVer: '3.7', distro: 'pypy', distroVer: '7.3.5-win64' }, + }, + { + input: 'pypy-5.7.1-beta-src', + expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: '5.7.1-beta-src' }, + }, + { input: 'pypy', expectedOutput: { pythonVer: undefined, distro: 'pypy', distroVer: undefined } }, + { input: 'pyston-0.6.1', expectedOutput: { pythonVer: undefined, distro: 'pyston', distroVer: '0.6.1' } }, + { input: 'stackless-dev', expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: 'dev' } }, + { + input: 'stackless-2.7-dev', + expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: '2.7-dev' }, + }, + { + input: 'stackless-3.4-dev', + expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: '3.4-dev' }, + }, + { input: 'stackless-3.7.5', expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: '3.7.5' } }, + { input: 'stackless', expectedOutput: { pythonVer: undefined, distro: 'stackless', distroVer: undefined } }, + { input: 'unknown', expectedOutput: undefined }, + ]; + + testData.forEach((data) => { + test(`Parse pyenv version [${data.input}]`, async () => { + assert.deepStrictEqual(parsePyenvVersion(data.input), data.expectedOutput); + }); + }); +}); + +suite('Pyenv Shims Dir filter tests', () => { + let getEnvVariableStub: sinon.SinonStub; + const pyenvRoot = path.join('path', 'to', 'pyenv', 'root'); + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getEnvVariableStub.withArgs('PYENV_ROOT').returns(pyenvRoot); + }); + + teardown(() => { + getEnvVariableStub.restore(); + }); + + test('isPyenvShimDir: valid case', () => { + assert.deepStrictEqual(isPyenvShimDir(path.join(pyenvRoot, 'shims')), true); + }); + test('isPyenvShimDir: invalid case', () => { + assert.deepStrictEqual(isPyenvShimDir(__dirname), false); + }); +}); diff --git a/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts b/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts new file mode 100644 index 000000000000..6d75668b8556 --- /dev/null +++ b/src/test/pythonEnvironments/common/environmentManagers/simplevirtualenvs.unit.test.ts @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import * as fsapi from '../../../../client/common/platform/fs-paths'; +import * as platformUtils from '../../../../client/common/utils/platform'; +import { PythonReleaseLevel, PythonVersion } from '../../../../client/pythonEnvironments/base/info'; +import * as fileUtils from '../../../../client/pythonEnvironments/common/externalDependencies'; +import { + getPythonVersionFromPyvenvCfg, + isVenvEnvironment, + isVirtualenvEnvironment, + isVirtualenvwrapperEnvironment, +} from '../../../../client/pythonEnvironments/common/environmentManagers/simplevirtualenvs'; +import { TEST_DATA_ROOT, TEST_LAYOUT_ROOT } from '../commonTestConstants'; +import { assertVersionsEqual } from '../../base/locators/envTestUtils'; + +suite('isVenvEnvironment Tests', () => { + const pyvenvCfg = 'pyvenv.cfg'; + const envRoot = path.join('path', 'to', 'env'); + const configPath = path.join('env', pyvenvCfg); + let fileExistsStub: sinon.SinonStub; + + setup(() => { + fileExistsStub = sinon.stub(fileUtils, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('pyvenv.cfg does not exist', async () => { + const interpreter = path.join(envRoot, 'python'); + fileExistsStub.callsFake(() => Promise.resolve(false)); + assert.ok(!(await isVenvEnvironment(interpreter))); + }); + + test('pyvenv.cfg exists in the current folder', async () => { + const interpreter = path.join(envRoot, 'python'); + + fileExistsStub.callsFake((p: string) => { + if (p.endsWith(configPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + assert.ok(await isVenvEnvironment(interpreter)); + }); + + test('pyvenv.cfg exists in the parent folder', async () => { + const interpreter = path.join(envRoot, 'bin', 'python'); + + fileExistsStub.callsFake((p: string) => { + if (p.endsWith(configPath)) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }); + + assert.ok(await isVenvEnvironment(interpreter)); + }); +}); + +suite('isVirtualenvEnvironment Tests', () => { + const envRoot = path.join('path', 'to', 'env'); + const interpreter = path.join(envRoot, 'python'); + let readDirStub: sinon.SinonStub; + + setup(() => { + readDirStub = sinon.stub(fsapi, 'readdir'); + }); + + teardown(() => { + readDirStub.restore(); + }); + + test('Interpreter folder contains an activate file', async () => { + readDirStub.resolves(['activate', 'python']); + + assert.ok(await isVirtualenvEnvironment(interpreter)); + }); + + test('Interpreter folder does not contain any activate.* files', async () => { + readDirStub.resolves(['mymodule', 'python']); + + assert.strictEqual(await isVirtualenvEnvironment(interpreter), false); + }); +}); + +suite('isVirtualenvwrapperEnvironment Tests', () => { + const homeDir = path.join(TEST_LAYOUT_ROOT, 'virutalhome'); + + let getEnvVariableStub: sinon.SinonStub; + let getUserHomeDirStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readDirStub: sinon.SinonStub; + + setup(() => { + getEnvVariableStub = sinon.stub(platformUtils, 'getEnvironmentVariable'); + getUserHomeDirStub = sinon.stub(platformUtils, 'getUserHomeDir'); + + readDirStub = sinon.stub(fsapi, 'readdir'); + readDirStub.resolves(['activate', 'python']); + + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + pathExistsStub.resolves(true); + // This is windows specific path. For test purposes we will use the common path + // that works on all OS. So, fail the path check for windows specific default route. + pathExistsStub.withArgs(path.join(homeDir, 'Envs')).resolves(false); + }); + + teardown(() => { + getEnvVariableStub.restore(); + getUserHomeDirStub.restore(); + pathExistsStub.restore(); + readDirStub.restore(); + }); + + test('WORKON_HOME is not set, and the interpreter is in a sub-folder of virtualenvwrapper', async () => { + const interpreter = path.join(homeDir, '.virtualenvs', 'win2', 'bin', 'python.exe'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(undefined); + getUserHomeDirStub.returns(homeDir); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('WORKON_HOME is set to a custom value, and the interpreter is is in a sub-folder', async () => { + const workonHomeDirectory = path.join(homeDir, 'workonhome'); + const interpreter = path.join(workonHomeDirectory, 'win2', 'bin', 'python.exe'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + pathExistsStub.withArgs(path.join(workonHomeDirectory)).resolves(true); + + assert.ok(await isVirtualenvwrapperEnvironment(interpreter)); + }); + + test('The interpreter is not in a sub-folder of WORKON_HOME', async () => { + const workonHomeDirectory = path.join('path', 'to', 'workonhome'); + const interpreter = path.join('some', 'path', 'env', 'bin', 'python'); + + getEnvVariableStub.withArgs('WORKON_HOME').returns(workonHomeDirectory); + + assert.deepStrictEqual(await isVirtualenvwrapperEnvironment(interpreter), false); + }); +}); + +suite('Virtual Env Version Parser Tests', () => { + let readFileStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + const testDataRoot = path.join(TEST_DATA_ROOT, 'versiondata', 'venv'); + + setup(() => { + readFileStub = sinon.stub(fileUtils, 'readFile'); + + pathExistsStub = sinon.stub(fileUtils, 'pathExists'); + pathExistsStub.resolves(true); + }); + + teardown(() => { + readFileStub.restore(); + pathExistsStub.restore(); + }); + + interface ICondaPythonVersionTestData { + name: string; + historyFileContents: string; + expected: PythonVersion | undefined; + } + + function getTestData(): ICondaPythonVersionTestData[] { + const data: ICondaPythonVersionTestData[] = []; + + const cases = fsapi.readdirSync(testDataRoot).map((c) => path.join(testDataRoot, c)); + const casesToVersion = new Map<string, PythonVersion>(); + casesToVersion.set('case1', { major: 3, minor: 9, micro: 0 }); + + casesToVersion.set('case2', { + major: 3, + minor: 8, + micro: 2, + release: { level: PythonReleaseLevel.Final, serial: 0 }, + sysVersion: undefined, + }); + casesToVersion.set('case3', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Candidate, serial: 1 }, + }); + casesToVersion.set('case4', { + major: 3, + minor: 9, + micro: 0, + release: { level: PythonReleaseLevel.Alpha, serial: 1 }, + sysVersion: undefined, + }); + + for (const c of cases) { + const name = path.basename(c); + const expected = casesToVersion.get(name); + if (expected) { + data.push({ + name, + historyFileContents: fsapi.readFileSync(c, 'utf-8'), + expected, + }); + } + } + + return data; + } + + const testData = getTestData(); + testData.forEach((data) => { + test(`Parsing ${data.name}`, async () => { + readFileStub.resolves(data.historyFileContents); + + const actual = await getPythonVersionFromPyvenvCfg('/path/here/does/not/matter'); + + assertVersionsEqual(actual, data.expected); + }); + }); +}); diff --git a/pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/complex/tests/x/y/z/b/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/_runtime_store/completed diff --git a/pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/notests/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3 diff --git a/pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/simple/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/2af6390a/exec/not-python3.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3 @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe new file mode 100644 index 000000000000..0800f9b4dfd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/activestate/b6a0705d/exec/python3.exe @@ -0,0 +1 @@ +invalid python interpreter: missing _runtime_store diff --git a/pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed similarity index 100% rename from pythonFiles/tests/testing_tools/adapter/.data/syntax-error/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/_runtime_store/completed diff --git a/src/datascience-ui/data-explorer/emptyRowsView.css b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 similarity index 100% rename from src/datascience-ui/data-explorer/emptyRowsView.css rename to src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3 diff --git a/src/datascience-ui/history-react/toolbarPanel.css b/src/test/pythonEnvironments/common/envlayouts/activestate/c09080d1/exec/python3.exe similarity index 100% rename from src/datascience-ui/history-react/toolbarPanel.css 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/datascience-ui/react-common/svgViewer.css b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/bin/dummy similarity index 100% rename from src/datascience-ui/react-common/svgViewer.css rename to src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/bin/dummy diff --git a/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/conda-meta/history new file mode 100644 index 000000000000..a329d0a79b88 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/condaLackingPython/conda-meta/history @@ -0,0 +1 @@ +Usually contains command that was used to create or update the conda environment with time stamps. diff --git a/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/python.exe b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/envsWithoutPython/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonFiles/environments/conda/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg new file mode 100644 index 000000000000..365d6f5eacee --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/cK2g6fIm/project1/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.11 +include-system-site-packages = false +version = 3.11.1 diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/project2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/bin/python b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/bin/python rename to src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/bin/python diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg new file mode 100644 index 000000000000..a67a28be91b5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/env/virtual/python/q4In3tK-/test/pyvenv.cfg @@ -0,0 +1,3 @@ +home = /usr/bin/python3.10 +include-system-site-packages = false +version = 3.10.3 diff --git a/src/test/pythonFiles/autoimport/one.py b/src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep similarity index 100% rename from src/test/pythonFiles/autoimport/one.py rename to src/test/pythonEnvironments/common/envlayouts/hatch/project1/.gitkeep diff --git a/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml new file mode 100644 index 000000000000..9848374b54fd --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/hatch/project2/hatch.toml @@ -0,0 +1,6 @@ +# this file is not actually used in tests, as all is mocked out + +# The default environment always exists +#[envs.default] + +[envs.test] diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/.project b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/.project new file mode 100644 index 000000000000..f16530171cd4 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/.project @@ -0,0 +1 @@ +Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project2 diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/bin/python b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/bin/python new file mode 100644 index 000000000000..590cf8f553ef --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project2-vnNIWe9P/bin/python @@ -0,0 +1 @@ +Not a real python binary diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/.project b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/.project new file mode 100644 index 000000000000..9b9083816e90 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/.project @@ -0,0 +1 @@ +Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project3 \ No newline at end of file diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/globalEnvironments/project3-2s1eXEJ2/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/.venv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/.venv/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/.venv/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project1/CustomPipfileName @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project2/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonFiles/autoimport/two/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pipenv/project3/parent/child/folder/dummyFile similarity index 100% rename from src/test/pythonFiles/autoimport/two/__init__.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/pythonFiles/autoimport/two/three.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi similarity index 100% rename from src/test/pythonFiles/autoimport/two/three.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py310/conda-meta/pixi diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/bin/python @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonFiles/definition/navigation/__init__.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi similarity index 100% rename from src/test/pythonFiles/definition/navigation/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/conda-meta/pixi diff --git a/src/test/pythonFiles/docstrings/one.py b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python similarity index 100% rename from src/test/pythonFiles/docstrings/one.py rename to src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/.pixi/envs/py311/python diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml new file mode 100644 index 000000000000..9b93e638e9ab --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/multi-env/pixi.toml @@ -0,0 +1,14 @@ +[project] +name = "multi-env" +channels = ["conda-forge"] +platforms = ["win-64"] + +[feature.py310.dependencies] +python = "~=3.10" + +[feature.py311.dependencies] +python = "~=3.11" + +[environments] +py310 = ["py310"] +py311 = ["py311"] diff --git a/src/test/pythonFiles/environments/conda/envs/numpy/python.exe b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/numpy/python.exe rename to src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/bin/python diff --git a/src/test/pythonFiles/environments/conda/envs/scipy/python.exe b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi similarity index 100% rename from src/test/pythonFiles/environments/conda/envs/scipy/python.exe rename to src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/.pixi/envs/default/conda-meta/pixi diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml new file mode 100644 index 000000000000..f11ab3b42360 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/non-windows/pixi.toml @@ -0,0 +1,11 @@ +[project] +name = "non-windows" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra <zalmstra.bas@gmail.com>"] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/.pixi/envs/default/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml new file mode 100644 index 000000000000..1341496c5590 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pixi/windows/pixi.toml @@ -0,0 +1,12 @@ +[project] +name = "windows" +version = "0.1.0" +description = "Add a short description here" +authors = ["Bas Zalmstra <zalmstra.bas@gmail.com>"] +channels = ["conda-forge"] +platforms = ["win-64"] + +[tasks] + +[dependencies] +python = "~=3.8.0" diff --git a/src/test/pythonFiles/environments/path1/one b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/activate similarity index 100% rename from src/test/pythonFiles/environments/path1/one rename to src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/activate diff --git a/src/test/pythonFiles/environments/path1/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/python.exe similarity index 100% rename from src/test/pythonFiles/environments/path1/python.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/Scripts/python.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/pyvenv.cfg new file mode 100644 index 000000000000..8245a8f957a5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/globalwinproject-9hvDnqYw-py3.11/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~\appdata\local\programs\python\python36 +include-system-site-packages = false +version = 3.6.1 diff --git a/src/test/pythonFiles/environments/path1/one.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/activate similarity index 100% rename from src/test/pythonFiles/environments/path1/one.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/activate diff --git a/src/test/pythonFiles/environments/path2/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/python.exe similarity index 100% rename from src/test/pythonFiles/environments/path2/python.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/Scripts/python.exe diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/pyvenv.cfg new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/poetry-tutorial-project-6hnqYwvD-py3.8/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonFiles/environments/path2/one b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/activate similarity index 100% rename from src/test/pythonFiles/environments/path2/one rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/activate diff --git a/src/test/pythonFiles/environments/path2/one.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/python similarity index 100% rename from src/test/pythonFiles/environments/path2/one.exe rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/python diff --git a/src/test/pythonFiles/folding/empty.py b/src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/pyvenv.cfg similarity index 100% rename from src/test/pythonFiles/folding/empty.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix1project-9hvDnqYw-py3.4/pyvenv.cfg diff --git a/src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/activate similarity index 100% rename from src/test/pythonFiles/testFiles/multi/tests/more_tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/activate diff --git a/src/test/pythonFiles/testFiles/standard/tests/__init__.py b/src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/python similarity index 100% rename from src/test/pythonFiles/testFiles/standard/tests/__init__.py rename to src/test/pythonEnvironments/common/envlayouts/poetry/posix2project-6hnqYwvD-py3.7/bin/python 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 <pvscc@microsoft.com>"] + +[tool.poetry.dependencies] +python = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/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 <pvscc@microsoft.com>"] + +[tool.poetry.dependencies] +python = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project3/pyproject.toml b/src/test/pythonEnvironments/common/envlayouts/poetry/project3/pyproject.toml new file mode 100644 index 000000000000..884d85e9903e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project3/pyproject.toml @@ -0,0 +1,10 @@ +# This pyproject.toml has not been setup for poetry + +[tool.pipenv.dependencies] +python = "^3.5" + +[tool.pipenv.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/project4/pyproject.toml b/src/test/pythonEnvironments/common/envlayouts/poetry/project4/pyproject.toml new file mode 100644 index 000000000000..627d86251d86 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/poetry/project4/pyproject.toml @@ -0,0 +1,13 @@ +[tool.poetrzzzzy] +name = "poetry-tutorial-project" +version = "0.1.0" +description = "" + +[tool.poetry.dependencies] +python = "^3.5" + +[tool.poetry.dev-dependencies] + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/poetry/wannabeglobalenv/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python3 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python37 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python37 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python37 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python38 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python38 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location2/python38 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.7 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.7 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.7 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.8 b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.8 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.8 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.9/empty b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.9/empty new file mode 100644 index 000000000000..0c6fe8957e8a --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/posixroot/location3/python3.9/empty @@ -0,0 +1 @@ +this is intentionally empty diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv1/.pyenv/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv2/.pyenv/pyenv-win/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/pyenv3/versions/3.6.9/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/3.9.0/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/3.9.0/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python3.8 b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/bin/python3.8 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/conda-meta/history new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/conda1/conda-meta/history @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3 b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3.7 b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/bin/python3.7 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/miniconda3-4.7.12/conda-meta/history new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/bin/python b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/pyvenv.cfg new file mode 100644 index 000000000000..b8b1d803da5b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/pyenvhome/.pyenv/versions/venv1/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~/.pyenv/versions/3.9.0/bin +include-system-site-packages = false +version = 3.9.0 diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.7_qbz5n2kfra8p0/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/PythonSoftwareFoundation.Python.3.8_qbz5n2kfra8p0/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/idle.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/idle.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/idle.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Microsoft/WindowsApps/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.7.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.8.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe new file mode 100644 index 000000000000..5ef39645e15b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/storeApps/Program Files/WindowsApps/python3.exe @@ -0,0 +1 @@ +Not a real exe. diff --git a/src/test/pythonEnvironments/common/envlayouts/venv1/python b/src/test/pythonEnvironments/common/envlayouts/venv1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/venv1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/venv1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/venv2/bin/python b/src/test/pythonEnvironments/common/envlayouts/venv2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/venv2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/venv2/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenv1/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenv1/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenv2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualenv2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenv3/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualenv3/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/.virtualenvs/myenv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper1/Envs/myenv/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualenvwrapper2/myenv/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/.project b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/.project new file mode 100644 index 000000000000..f16530171cd4 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/.project @@ -0,0 +1 @@ +Absolute path to \src\test\pythonEnvironments\common\envlayouts\pipenv\project2 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/bin/python new file mode 100644 index 000000000000..590cf8f553ef --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/bin/python @@ -0,0 +1 @@ +Not a real python binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/pyvenv.cfg new file mode 100644 index 000000000000..0faad0624a4c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.local/share/virtualenvs/project2-vnNIWe9P/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (folder1) diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/posix2/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win1/pyvenv.cfg new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.venvs/win2/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3.8 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3.8 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix1/python3.8 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/.virtualenvs/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/Envs/wrapper_win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3.5 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3.5 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix1/python3.5 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/customfolder/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3.5 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3.5 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix1/python3.5 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/activate.sh b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/activate.sh new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/python b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/posix2/bin/python new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/activate b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/activate.ps1 b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/activate.ps1 new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/python.exe b/src/test/pythonEnvironments/common/envlayouts/virtualhome/workonhome/win2/bin/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/conda-meta/history new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/conda-meta/history @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/winreg/conda3/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/py39/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/py39/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/python37/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/python37/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/winreg/python38/python.exe b/src/test/pythonEnvironments/common/envlayouts/winreg/python38/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/activate b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/activate new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3 b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3.8 b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3.8 new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/posix1virtualenv/bin/python3.8 @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/Scripts/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/pyvenv.cfg new file mode 100644 index 000000000000..8245a8f957a5 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.direnv/win2/pyvenv.cfg @@ -0,0 +1,3 @@ +home = ~\appdata\local\programs\python\python36 +include-system-site-packages = false +version = 3.6.1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/Scripts/python.exe b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/Scripts/python.exe new file mode 100644 index 000000000000..a37b666d049e --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/Scripts/python.exe @@ -0,0 +1 @@ +Not real python exe diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/pyvenv.cfg new file mode 100644 index 000000000000..0faad0624a4c --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/.venv/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (folder1) diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile new file mode 100644 index 000000000000..b5846df18ca8 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[dev-packages] + +[packages] + +[requires] +python_version = "3.8" diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/conda-meta/history b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/conda-meta/history new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/conda-meta/history @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/python b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix2conda/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix3custom/bin/python b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix3custom/bin/python new file mode 100644 index 000000000000..c7c9e3509282 --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/posix3custom/bin/python @@ -0,0 +1 @@ +Not a real binary diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/python.exe b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/python.exe new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/pyvenv.cfg b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/pyvenv.cfg new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/envlayouts/workspace/folder1/win1/pyvenv.cfg @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/posixUtils.unit.test.ts b/src/test/pythonEnvironments/common/posixUtils.unit.test.ts new file mode 100644 index 000000000000..6ff06f9bacba --- /dev/null +++ b/src/test/pythonEnvironments/common/posixUtils.unit.test.ts @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { promises, Dirent } from 'fs'; +import * as externalDependencies from '../../../client/pythonEnvironments/common/externalDependencies'; +import { getPythonBinFromPosixPaths } from '../../../client/pythonEnvironments/common/posixUtils'; + +suite('Posix Utils tests', () => { + let readDirStub: sinon.SinonStub; + let resolveSymlinkStub: sinon.SinonStub; + + class FakeDirent extends Dirent { + constructor( + public readonly name: string, + private readonly _isFile: boolean, + private readonly _isLink: boolean, + ) { + super(); + } + + public isFile(): boolean { + return this._isFile; + } + + public isDirectory(): boolean { + return !this._isFile && !this._isLink; + } + + // eslint-disable-next-line class-methods-use-this + public isBlockDevice(): boolean { + return false; + } + + // eslint-disable-next-line class-methods-use-this + public isCharacterDevice(): boolean { + return false; + } + + public isSymbolicLink(): boolean { + return this._isLink; + } + + // eslint-disable-next-line class-methods-use-this + public isFIFO(): boolean { + return false; + } + + // eslint-disable-next-line class-methods-use-this + public isSocket(): boolean { + return false; + } + } + + setup(() => { + readDirStub = sinon.stub(promises, 'readdir'); + readDirStub + .withArgs(path.join('usr', 'bin'), { withFileTypes: true }) + .resolves([ + new FakeDirent('python', false, true), + new FakeDirent('python3', false, true), + new FakeDirent('python3.7', false, true), + new FakeDirent('python3.8', false, true), + ]); + readDirStub + .withArgs(path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib'), { + withFileTypes: true, + }) + .resolves([new FakeDirent('python3.9', true, false)]); + + resolveSymlinkStub = sinon.stub(externalDependencies, 'resolveSymbolicLink'); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python3.7')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.7', 'lib', 'python3.7'), + ); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python3')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.7', 'lib', 'python3.7'), + ); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.7', 'lib', 'python3.7'), + ); + resolveSymlinkStub + .withArgs(path.join('usr', 'bin', 'python3.8')) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.8', 'lib', 'python3.8'), + ); + resolveSymlinkStub + .withArgs( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib', 'python3.9'), + ) + .resolves( + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib', 'python3.9'), + ); + }); + + teardown(() => { + readDirStub.restore(); + resolveSymlinkStub.restore(); + }); + test('getPythonBinFromPosixPaths', async () => { + const expectedPaths = [ + path.join('usr', 'bin', 'python'), + path.join('usr', 'bin', 'python3.8'), + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib', 'python3.9'), + ].sort((a, b) => a.length - b.length); + + const actualPaths = await getPythonBinFromPosixPaths([ + path.join('usr', 'bin'), + path.join('System', 'Library', 'Frameworks', 'Python.framework', 'Versions', '3.9', 'lib'), + ]); + actualPaths.sort((a, b) => a.length - b.length); + + assert.deepStrictEqual(actualPaths, expectedPaths); + }); +}); diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case1 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case1 new file mode 100644 index 000000000000..0ff7c173605f --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case1 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.8.5-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case2 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case2 new file mode 100644 index 000000000000..d5bab55214ac --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case2 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0a1-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case3 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case3 new file mode 100644 index 000000000000..4de9c7e72768 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case3 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0b2-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case4 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case4 new file mode 100644 index 000000000000..f6d8f0b9c027 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case4 @@ -0,0 +1,23 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0rc1-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/conda/case5 b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case5 new file mode 100644 index 000000000000..5b5be621a41e --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/conda/case5 @@ -0,0 +1,46 @@ +==> 2020-10-29 17:13:39 <== +# cmd: ~/.pyenv/versions/miniconda3-4.7.12/bin/conda create --name conda1 --yes python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0b2-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] +==> 2020-10-31 17:17:40 <== +# cmd: /home/kanadig/.pyenv/versions/miniconda3-4.7.12/bin/conda update python +# conda version: 4.9.1 ++defaults/linux-64::_libgcc_mutex-0.1-main ++defaults/linux-64::ca-certificates-2020.10.14-0 ++defaults/linux-64::certifi-2020.6.20-py38h06a4308_2 ++defaults/linux-64::ld_impl_linux-64-2.33.1-h53a641e_7 ++defaults/linux-64::libedit-3.1.20191231-h14c3975_1 ++defaults/linux-64::libffi-3.3-he6710b0_2 ++defaults/linux-64::libgcc-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::libstdcxx-ng-9.1.0-hdf63c60_0 ++defaults/linux-64::ncurses-6.2-he6710b0_1 ++defaults/linux-64::openssl-1.1.1h-h7b6447c_0 ++defaults/linux-64::pip-20.2.4-py38_0 ++defaults/linux-64::python-3.9.0rc2-h7579374_1 ++defaults/linux-64::readline-8.0-h7b6447c_0 ++defaults/linux-64::setuptools-50.3.0-py38hb0f4dca_1 ++defaults/linux-64::sqlite-3.33.0-h62c20be_0 ++defaults/linux-64::tk-8.6.10-hbc83047_0 ++defaults/linux-64::xz-5.2.5-h7b6447c_0 ++defaults/linux-64::zlib-1.2.11-h7b6447c_3 ++defaults/noarch::wheel-0.35.1-py_0 +# update specs: ['python'] diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case1 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case1 new file mode 100644 index 000000000000..0f664d82a0d5 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case1 @@ -0,0 +1,3 @@ +home = /home/kanadig/.pyenv/versions/3.9.0/bin +include-system-site-packages = false +version = 3.9.0 diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case2 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case2 new file mode 100644 index 000000000000..706bcf757bd2 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case2 @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.8.2.final.0 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case3 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case3 new file mode 100644 index 000000000000..58c87730bf79 --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case3 @@ -0,0 +1,3 @@ +home = /home/kanadig/.pyenv/versions/3.9.0/bin +include-system-site-packages = false +version = 3.9.0rc1 diff --git a/src/test/pythonEnvironments/common/testdata/versiondata/venv/case4 b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case4 new file mode 100644 index 000000000000..45a1a0c8d51b --- /dev/null +++ b/src/test/pythonEnvironments/common/testdata/versiondata/venv/case4 @@ -0,0 +1,9 @@ +home = ~\appdata\local\programs\python\python38 +implementation = CPython +version_info = 3.9.0.alpha.1 +virtualenv = 20.1.0 +include-system-site-packages = false +base-prefix = ~\appdata\local\programs\python\python38 +base-exec-prefix = ~\appdata\local\programs\python\python38 +base-executable = ~\appdata\local\programs\python\python38\python.exe +prompt = (pythonEnv) diff --git a/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts b/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts new file mode 100644 index 000000000000..96b2f9c68244 --- /dev/null +++ b/src/test/pythonEnvironments/common/windowsUtils.unit.test.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import { matchPythonBinFilename } from '../../../client/pythonEnvironments/common/windowsUtils'; + +suite('Windows Utils tests', () => { + const testParams = [ + { path: 'python.exe', expected: true }, + { path: 'python3.exe', expected: true }, + { path: 'python38.exe', expected: true }, + { path: 'python3.8.exe', expected: true }, + { path: 'python', expected: false }, + { path: 'python3', expected: false }, + { path: 'python38', expected: false }, + { path: 'python3.8', expected: false }, + { path: 'idle.exe', expected: false }, + { path: 'pip.exe', expected: false }, + { path: 'python.dll', expected: false }, + { path: 'python3.dll', expected: false }, + { path: 'python3.8.dll', expected: false }, + ]; + + testParams.forEach((testParam) => { + test(`Python executable check ${testParam.expected ? 'should match' : 'should not match'} this path: ${ + testParam.path + }`, () => { + assert.deepEqual(matchPythonBinFilename(testParam.path), testParam.expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts new file mode 100644 index 000000000000..2900b9b89c8f --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/installCheckUtils.unit.test.ts @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, TextDocument, Range, Uri, WorkspaceConfiguration, ConfigurationScope } from 'vscode'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { getInstalledPackagesDiagnostics } from '../../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { SpawnOptions } from '../../../../client/common/process/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../../client/pythonEnvironments/info'; + +chaiUse(chaiAsPromised.default); + +function getSomeRequirementFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +const MISSING_PACKAGES_STR = + '[{"line": 8, "character": 34, "endLine": 8, "endCharacter": 44, "package": "flake8-csv", "code": "not-installed", "severity": 3}]'; +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +suite('Install check diagnostics tests', () => { + let plainExecStub: sinon.SinonStub; + let interpreterService: typemoq.IMock<IInterpreterService>; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock<WorkspaceConfiguration>; + + setup(() => { + configMock = typemoq.Mock.ofType<WorkspaceConfiguration>(); + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(typemoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Test parse diagnostics', async () => { + configMock + .setup((c) => c.get<string>('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); + plainExecStub.resolves({ stdout: MISSING_PACKAGES_STR, stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, MISSING_PACKAGES); + configMock.verifyAll(); + }); + + test('Test parse empty diagnostics', async () => { + configMock + .setup((c) => c.get<string>('missingPackage.severity', 'Hint')) + .returns(() => 'Error') + .verifiable(typemoq.Times.atLeastOnce()); + plainExecStub.resolves({ stdout: '', stderr: '' }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, []); + configMock.verifyAll(); + }); + + [ + ['Error', '0'], + ['Warning', '1'], + ['Information', '2'], + ['Hint', '3'], + ].forEach((severityType: string[]) => { + const setting = severityType[0]; + const expected = severityType[1]; + test(`Test missing package severity: ${setting}`, async () => { + configMock + .setup((c) => c.get<string>('missingPackage.severity', 'Hint')) + .returns(() => setting) + .verifiable(typemoq.Times.atLeastOnce()); + let severity: string | undefined; + plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => { + severity = options.env?.VSCODE_MISSING_PGK_SEVERITY; + return { stdout: '', stderr: '' }; + }); + const someFile = getSomeRequirementFile(); + const result = await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object); + + assert.deepStrictEqual(result, []); + assert.deepStrictEqual(severity, expected); + configMock.verifyAll(); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts new file mode 100644 index 000000000000..1d3df521fd0a --- /dev/null +++ b/src/test/pythonEnvironments/creation/common/workspaceSelection.unit.test.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import { assert, expect, use as chaiUse } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +// import * as typemoq from 'typemoq'; +import { Uri, WorkspaceFolder } from 'vscode'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { pickWorkspaceFolder } from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; + +chaiUse(chaiAsPromised.default); + +suite('Create environment workspace selection tests', () => { + let showQuickPickWithBackStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let showErrorMessageStub: sinon.SinonStub; + + setup(() => { + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + showErrorMessageStub = sinon.stub(windowApis, 'showErrorMessage'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No workspaces (undefined)', async () => { + getWorkspaceFoldersStub.returns(undefined); + assert.isUndefined(await pickWorkspaceFolder()); + assert.isTrue(showErrorMessageStub.calledOnce); + }); + + test('No workspaces (empty array)', async () => { + getWorkspaceFoldersStub.returns([]); + assert.isUndefined(await pickWorkspaceFolder()); + assert.isTrue(showErrorMessageStub.calledOnce); + }); + + test('User did not select workspace or user hit escape', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file('some_folder'), + name: 'some_folder', + index: 0, + }, + { + uri: Uri.file('some_folder2'), + name: 'some_folder2', + index: 1, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns(undefined); + assert.isUndefined(await pickWorkspaceFolder()); + }); + + test('User clicked on the back button', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file('some_folder'), + name: 'some_folder', + index: 0, + }, + { + uri: Uri.file('some_folder2'), + name: 'some_folder2', + index: 1, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.throws(windowApis.MultiStepAction.Back); + expect(pickWorkspaceFolder()).to.eventually.be.rejectedWith(windowApis.MultiStepAction.Back); + }); + + test('single workspace scenario', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns({ + label: workspaces[0].name, + detail: workspaces[0].uri.fsPath, + description: undefined, + }); + + const workspace = await pickWorkspaceFolder(); + assert.deepEqual(workspace, workspaces[0]); + assert(showQuickPickWithBackStub.notCalled); + }); + + test('Multi-workspace scenario with single workspace selected', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace2')), + name: 'workspace2', + index: 1, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace3')), + name: 'workspace3', + index: 2, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace4')), + name: 'workspace4', + index: 3, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace5')), + name: 'workspace5', + index: 4, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns({ + label: workspaces[1].name, + detail: workspaces[1].uri.fsPath, + description: undefined, + }); + + const workspace = await pickWorkspaceFolder(); + assert.deepEqual(workspace, workspaces[1]); + assert(showQuickPickWithBackStub.calledOnce); + }); + + test('Multi-workspace scenario with multiple workspaces selected', async () => { + const workspaces: WorkspaceFolder[] = [ + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace2')), + name: 'workspace2', + index: 1, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace3')), + name: 'workspace3', + index: 2, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace4')), + name: 'workspace4', + index: 3, + }, + { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace5')), + name: 'workspace5', + index: 4, + }, + ]; + + getWorkspaceFoldersStub.returns(workspaces); + showQuickPickWithBackStub.returns([ + { + label: workspaces[1].name, + detail: workspaces[1].uri.fsPath, + description: undefined, + }, + { + label: workspaces[3].name, + detail: workspaces[3].uri.fsPath, + description: undefined, + }, + ]); + + const workspace = await pickWorkspaceFolder({ allowMultiSelect: true }); + assert.deepEqual(workspace, [workspaces[1], workspaces[3]]); + assert(showQuickPickWithBackStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts new file mode 100644 index 000000000000..dd09203d65cc --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvApi.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { ConfigurationTarget, Uri } from 'vscode'; +import { IDisposableRegistry, IPathUtils } from '../../../client/common/types'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { + IInterpreterQuickPick, + IPythonPathUpdaterServiceManager, +} from '../../../client/interpreter/configuration/types'; +import { registerCreateEnvironmentFeatures } from '../../../client/pythonEnvironments/creation/createEnvApi'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('Create Environment APIs', () => { + let registerCommandStub: sinon.SinonStub; + let showQuickPickStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let interpreterQuickPick: typemoq.IMock<IInterpreterQuickPick>; + let interpreterPathService: typemoq.IMock<IPythonPathUpdaterServiceManager>; + let pathUtils: typemoq.IMock<IPathUtils>; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + registerCommandStub = sinon.stub(commandApis, 'registerCommand'); + interpreterQuickPick = typemoq.Mock.ofType<IInterpreterQuickPick>(); + interpreterPathService = typemoq.Mock.ofType<IPythonPathUpdaterServiceManager>(); + pathUtils = typemoq.Mock.ofType<IPathUtils>(); + + registerCommandStub.callsFake((_command: string, _callback: (...args: any[]) => any) => ({ + dispose: () => { + // Do nothing + }, + })); + + pathUtils.setup((p) => p.getDisplayName(typemoq.It.isAny())).returns(() => 'test'); + + registerCreateEnvironmentFeatures( + disposables, + interpreterQuickPick.object, + interpreterPathService.object, + pathUtils.object, + ); + }); + teardown(() => { + disposables.forEach((d) => d.dispose()); + sinon.restore(); + }); + + [true, false].forEach((selectEnvironment) => { + test(`Set environment selectEnvironment == ${selectEnvironment}`, async () => { + const workspace1 = { + uri: Uri.file('/path/to/env'), + name: 'workspace1', + index: 0, + }; + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider + .setup((p) => p.createEnvironment(typemoq.It.isAny())) + .returns(() => + Promise.resolve({ + path: '/path/to/env', + workspaceFolder: workspace1, + action: undefined, + error: undefined, + }), + ); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + interpreterPathService + .setup((p) => + p.updatePythonPath( + typemoq.It.isValue('/path/to/env'), + ConfigurationTarget.WorkspaceFolder, + 'ui', + typemoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve()) + .verifiable(selectEnvironment ? typemoq.Times.once() : typemoq.Times.never()); + + await handleCreateEnvironmentCommand([provider.object], { selectEnvironment }); + + assert.ok(showQuickPickStub.calledOnce); + assert.ok(selectEnvironment ? showInformationMessageStub.calledOnce : showInformationMessageStub.notCalled); + interpreterPathService.verifyAll(); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts new file mode 100644 index 000000000000..b666191b37bf --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvButtonContext.unit.test.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { WorkspaceConfiguration } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerCreateEnvironmentButtonFeatures } from '../../../client/pythonEnvironments/creation/createEnvButtonContext'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let onDidChangeConfigurationStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock<WorkspaceConfiguration>; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + onDidChangeConfigurationStub = sinon.stub(workspaceApis, 'onDidChangeConfiguration'); + onDidChangeConfigurationStub.returns(new FakeDisposable()); + + configMock = typemoq.Mock.ofType<WorkspaceConfiguration>(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + getConfigurationStub.returns(configMock.object); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('python.createEnvironment.contentButton setting is set to "show", no files open', async () => { + registerCreateEnvironmentButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting is set to "hide", no files open', async () => { + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + + registerCreateEnvironmentButtonFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); + + test('python.createEnvironment.contentButton setting changed from "hide" to "show"', async () => { + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvironmentButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + }); + + test('python.createEnvironment.contentButton setting changed from "show" to "hide"', async () => { + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'show'); + + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeConfigurationStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerCreateEnvironmentButtonFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', true)); + executeCommandStub.reset(); + + configMock.reset(); + configMock.setup((c) => c.get<string>(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => 'hide'); + handler(); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'showCreateEnvButton', false)); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts new file mode 100644 index 000000000000..9aa9a606d22f --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironment.unit.test.ts @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { handleCreateEnvironmentCommand } from '../../../client/pythonEnvironments/creation/createEnvironment'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { onCreateEnvironmentStarted } from '../../../client/pythonEnvironments/creation/createEnvApi'; +import { CreateEnvironmentProvider } from '../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('Create Environments Tests', () => { + let showQuickPickStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let startedEventTriggered = false; + let exitedEventTriggered = false; + + setup(() => { + showQuickPickStub = sinon.stub(windowApis, 'showQuickPick'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + startedEventTriggered = false; + exitedEventTriggered = false; + disposables.push( + onCreateEnvironmentStarted(() => { + startedEventTriggered = true; + }), + ); + disposables.push( + onCreateEnvironmentStarted(() => { + exitedEventTriggered = true; + }), + ); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Successful environment creation', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + assert.isTrue(showQuickPickWithBackStub.notCalled); + provider.verifyAll(); + }); + + test('Successful environment creation with Back', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickWithBackStub.resolves(provider.object); + + await handleCreateEnvironmentCommand([provider.object], { showBackButton: true }); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + assert.isTrue(showQuickPickStub.notCalled); + provider.verifyAll(); + }); + + test('Environment creation error', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.reject(new Error('test'))); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + await assert.isRejected(handleCreateEnvironmentCommand([provider.object])); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + provider.verifyAll(); + }); + + test('Environment creation error with Back', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.reject(new Error('test'))); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickWithBackStub.resolves(provider.object); + await assert.isRejected(handleCreateEnvironmentCommand([provider.object], { showBackButton: true })); + + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + provider.verifyAll(); + }); + + test('No providers registered', async () => { + await handleCreateEnvironmentCommand([]); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.isFalse(startedEventTriggered); + assert.isFalse(exitedEventTriggered); + }); + + test('Single environment creation provider registered', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickStub.resolves(provider.object); + await handleCreateEnvironmentCommand([provider.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Multiple environment creation providers registered', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object]); + + assert.isTrue(showQuickPickStub.calledOnce); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Single environment creation provider registered with Back', async () => { + const provider = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider.setup((p) => p.name).returns(() => 'test'); + provider.setup((p) => p.id).returns(() => 'test-id'); + provider.setup((p) => p.description).returns(() => 'test-description'); + provider.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + provider.setup((p) => (p as any).then).returns(() => undefined); + + showQuickPickWithBackStub.resolves(provider.object); + await handleCreateEnvironmentCommand([provider.object], { showBackButton: true }); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('Multiple environment creation providers registered with Back', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickWithBackStub.resolves({ + id: 'test-id2', + label: 'test2', + description: 'test-description2', + }); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + await handleCreateEnvironmentCommand([provider1.object, provider2.object], { showBackButton: true }); + + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + assert.isTrue(startedEventTriggered); + assert.isTrue(exitedEventTriggered); + }); + + test('User clicked Back', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickWithBackStub.returns(Promise.reject(windowApis.MultiStepAction.Back)); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + const result = await handleCreateEnvironmentCommand([provider1.object, provider2.object], { + showBackButton: true, + }); + + assert.deepStrictEqual(result, { + action: 'Back', + workspaceFolder: undefined, + path: undefined, + error: undefined, + }); + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + }); + + test('User pressed Escape', async () => { + const provider1 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider1.setup((p) => p.name).returns(() => 'test1'); + provider1.setup((p) => p.id).returns(() => 'test-id1'); + provider1.setup((p) => p.description).returns(() => 'test-description1'); + provider1.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + const provider2 = typemoq.Mock.ofType<CreateEnvironmentProvider>(); + provider2.setup((p) => p.name).returns(() => 'test2'); + provider2.setup((p) => p.id).returns(() => 'test-id2'); + provider2.setup((p) => p.description).returns(() => 'test-description2'); + provider2.setup((p) => p.createEnvironment(typemoq.It.isAny())).returns(() => Promise.resolve(undefined)); + + showQuickPickWithBackStub.returns(Promise.reject(windowApis.MultiStepAction.Cancel)); + + provider1.setup((p) => (p as any).then).returns(() => undefined); + provider2.setup((p) => (p as any).then).returns(() => undefined); + const result = await handleCreateEnvironmentCommand([provider1.object, provider2.object], { + showBackButton: true, + }); + + assert.deepStrictEqual(result, { + action: 'Cancel', + workspaceFolder: undefined, + path: undefined, + error: undefined, + }); + assert.isTrue(showQuickPickStub.notCalled); + assert.isTrue(showQuickPickWithBackStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts new file mode 100644 index 000000000000..d4041ef4bb88 --- /dev/null +++ b/src/test/pythonEnvironments/creation/createEnvironmentTrigger.unit.test.ts @@ -0,0 +1,253 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as commonUtils from '../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { + CreateEnvironmentCheckKind, + triggerCreateEnvironmentCheck, +} from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { Commands } from '../../../client/common/constants'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; + +suite('Create Environment Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let hasVenvStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let hasRequirementFilesStub: sinon.SinonStub; + let hasKnownFilesStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showInformationMessageStub: sinon.SinonStub; + let isCreateEnvWorkspaceCheckNotRunStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + hasVenvStub = sinon.stub(commonUtils, 'hasVenv'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + hasRequirementFilesStub = sinon.stub(triggerUtils, 'hasRequirementFiles'); + hasKnownFilesStub = sinon.stub(triggerUtils, 'hasKnownFiles'); + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showInformationMessageStub = sinon.stub(windowApis, 'showInformationMessage'); + + isCreateEnvWorkspaceCheckNotRunStub = sinon.stub(triggerUtils, 'isCreateEnvWorkspaceCheckNotRun'); + isCreateEnvWorkspaceCheckNotRunStub.returns(true); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No Uri', async () => { + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, undefined); + sinon.assert.notCalled(shouldPromptToCreateEnvStub); + }); + + test('Should not perform checks if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not perform checks even if force is true, if user set trigger to "off"', async () => { + shouldPromptToCreateEnvStub.returns(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri, { + force: true, + }); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.notCalled(hasVenvStub); + sinon.assert.notCalled(hasPrefixCondaEnvStub); + sinon.assert.notCalled(hasRequirementFilesStub); + sinon.assert.notCalled(hasKnownFilesStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".venv"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(true); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there is a ".conda"', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(true); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are no requirements', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if there are known files', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(false); + hasKnownFilesStub.resolves(true); + isGlobalPythonSelectedStub.resolves(true); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should not show prompt if selected python is not global', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(false); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showInformationMessageStub); + }); + + test('Should show prompt if all conditions met: User closes prompt', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + showInformationMessageStub.resolves(undefined); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + }); + + test('Should show prompt if all conditions met: User clicks create', async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.calledOnceWithExactly(executeCommandStub, Commands.Create_Environment); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + }); + + test("Should show prompt if all conditions met: User clicks don't show again", async () => { + shouldPromptToCreateEnvStub.returns(true); + hasVenvStub.resolves(false); + hasPrefixCondaEnvStub.resolves(false); + hasRequirementFilesStub.resolves(true); + hasKnownFilesStub.resolves(false); + isGlobalPythonSelectedStub.resolves(true); + + showInformationMessageStub.resolves(Common.doNotShowAgain); + await triggerCreateEnvironmentCheck(CreateEnvironmentCheckKind.Workspace, workspace1.uri); + + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(hasVenvStub); + sinon.assert.calledOnce(hasPrefixCondaEnvStub); + sinon.assert.calledOnce(hasRequirementFilesStub); + sinon.assert.calledOnce(hasKnownFilesStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showInformationMessageStub); + + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts b/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts new file mode 100644 index 000000000000..2b6a8df91d82 --- /dev/null +++ b/src/test/pythonEnvironments/creation/globalPipInTerminalTrigger.unit.test.ts @@ -0,0 +1,278 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as path from 'path'; +import * as sinon from 'sinon'; +import { assert } from 'chai'; +import * as typemoq from 'typemoq'; +import { + Disposable, + Terminal, + TerminalShellExecution, + TerminalShellExecutionStartEvent, + TerminalShellIntegration, + Uri, +} from 'vscode'; +import * as triggerUtils from '../../../client/pythonEnvironments/creation/common/createEnvTriggerUtils'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as commandApis from '../../../client/common/vscodeApis/commandApis'; +import { registerTriggerForPipInTerminal } from '../../../client/pythonEnvironments/creation/globalPipInTerminalTrigger'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../constants'; +import { Common, CreateEnv } from '../../../client/common/utils/localize'; + +suite('Global Pip in Terminal Trigger', () => { + let shouldPromptToCreateEnvStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let getWorkspaceFolderStub: sinon.SinonStub; + let isGlobalPythonSelectedStub: sinon.SinonStub; + let showWarningMessageStub: sinon.SinonStub; + let executeCommandStub: sinon.SinonStub; + let disableCreateEnvironmentTriggerStub: sinon.SinonStub; + let onDidStartTerminalShellExecutionStub: sinon.SinonStub; + let handler: undefined | ((e: TerminalShellExecutionStartEvent) => Promise<void>); + let execEvent: typemoq.IMock<TerminalShellExecutionStartEvent>; + let shellIntegration: typemoq.IMock<TerminalShellIntegration>; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + const outsideWorkspace = Uri.file( + path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'outsideWorkspace'), + ); + + setup(() => { + shouldPromptToCreateEnvStub = sinon.stub(triggerUtils, 'shouldPromptToCreateEnv'); + + getWorkspaceFoldersStub = sinon.stub(workspaceApis, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([workspace1]); + + getWorkspaceFolderStub = sinon.stub(workspaceApis, 'getWorkspaceFolder'); + getWorkspaceFolderStub.returns(workspace1); + + isGlobalPythonSelectedStub = sinon.stub(triggerUtils, 'isGlobalPythonSelected'); + showWarningMessageStub = sinon.stub(windowApis, 'showWarningMessage'); + + executeCommandStub = sinon.stub(commandApis, 'executeCommand'); + executeCommandStub.resolves({ path: 'some/python' }); + + disableCreateEnvironmentTriggerStub = sinon.stub(triggerUtils, 'disableCreateEnvironmentTrigger'); + + onDidStartTerminalShellExecutionStub = sinon.stub(windowApis, 'onDidStartTerminalShellExecution'); + onDidStartTerminalShellExecutionStub.callsFake((cb) => { + handler = cb; + return { + dispose: () => { + handler = undefined; + }, + }; + }); + + shellIntegration = typemoq.Mock.ofType<TerminalShellIntegration>(); + execEvent = typemoq.Mock.ofType<TerminalShellExecutionStartEvent>(); + execEvent.setup((e) => e.shellIntegration).returns(() => shellIntegration.object); + shellIntegration + .setup((s) => s.executeCommand(typemoq.It.isAnyString())) + .returns(() => (({} as unknown) as TerminalShellExecution)); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Should not prompt to create environment if setting is off', async () => { + shouldPromptToCreateEnvStub.returns(false); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + assert.strictEqual(disposables.length, 0); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + }); + + test('Should not prompt to create environment if no workspace folders', async () => { + shouldPromptToCreateEnvStub.returns(true); + getWorkspaceFoldersStub.returns([]); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + assert.strictEqual(disposables.length, 0); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFoldersStub); + }); + + test('Should not prompt to create environment if workspace folder is not found', async () => { + shouldPromptToCreateEnvStub.returns(true); + getWorkspaceFolderStub.returns(undefined); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + shellIntegration.setup((s) => s.cwd).returns(() => outsideWorkspace); + await handler?.(({ shellIntegration: shellIntegration.object } as unknown) as TerminalShellExecutionStartEvent); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.notCalled(isGlobalPythonSelectedStub); + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if global python is not selected', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(false); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.(({ shellIntegration: shellIntegration.object } as unknown) as TerminalShellExecutionStartEvent); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if command is not trusted', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: false, + value: 'pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + test('Should not prompt to create environment if command does not start with pip install', async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: false, + value: 'some command pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + + sinon.assert.notCalled(showWarningMessageStub); + }); + + ['pip install', 'pip3 install', 'python -m pip install', 'python3 -m pip install'].forEach((command) => { + test(`Should prompt to create environment if all conditions are met: ${command}`, async () => { + shouldPromptToCreateEnvStub.returns(true); + isGlobalPythonSelectedStub.returns(true); + showWarningMessageStub.resolves(CreateEnv.Trigger.createEnvironment); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: true, + value: command, + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve(command); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showWarningMessageStub); + sinon.assert.calledOnce(executeCommandStub); + sinon.assert.notCalled(disableCreateEnvironmentTriggerStub); + + shellIntegration.verify((s) => s.executeCommand(typemoq.It.isAnyString()), typemoq.Times.once()); + }); + }); + + test("Should disable create environment trigger if user selects don't show again", async () => { + shouldPromptToCreateEnvStub.returns(true); + + isGlobalPythonSelectedStub.returns(true); + showWarningMessageStub.resolves(Common.doNotShowAgain); + + const disposables: Disposable[] = []; + registerTriggerForPipInTerminal(disposables); + + await handler?.({ + terminal: ({} as unknown) as Terminal, + shellIntegration: shellIntegration.object, + execution: { + cwd: workspace1.uri, + commandLine: { + isTrusted: true, + value: 'pip install', + confidence: 0, + }, + read: () => + (async function* () { + yield Promise.resolve('pip install'); + })(), + }, + }); + + assert.strictEqual(disposables.length, 1); + sinon.assert.calledOnce(shouldPromptToCreateEnvStub); + sinon.assert.calledOnce(getWorkspaceFolderStub); + sinon.assert.calledOnce(isGlobalPythonSelectedStub); + sinon.assert.calledOnce(showWarningMessageStub); + sinon.assert.notCalled(executeCommandStub); + sinon.assert.calledOnce(disableCreateEnvironmentTriggerStub); + }); +}); diff --git a/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts new file mode 100644 index 000000000000..21bddd33c678 --- /dev/null +++ b/src/test/pythonEnvironments/creation/installedPackagesDiagnostics.unit.test.ts @@ -0,0 +1,334 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { Diagnostic, DiagnosticCollection, TextEditor, Range, Uri, TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import * as languageApis from '../../../client/common/vscodeApis/languageApis'; +import * as windowApis from '../../../client/common/vscodeApis/windowApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import * as installUtils from '../../../client/pythonEnvironments/creation/common/installCheckUtils'; +import { + DEPS_NOT_INSTALLED_KEY, + registerInstalledPackagesDiagnosticsProvider, +} from '../../../client/pythonEnvironments/creation/installedPackagesDiagnostic'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +const MISSING_PACKAGES: Diagnostic[] = [ + { + range: new Range(8, 34, 8, 44), + message: 'Package `flake8-csv` is not installed in the selected environment.', + source: 'Python-InstalledPackagesChecker', + code: { value: 'not-installed', target: Uri.parse(`https://pypi.org/p/flake8-csv`) }, + severity: 3, + relatedInformation: [], + }, +]; + +function getSomeFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +function getSomeRequirementFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'requirements.txt'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'pip-requirements'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'flake8-csv'); + return someFile; +} + +function getPyProjectTomlFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'pyproject.toml'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[project]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +function getSomeTomlFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'something.toml'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.languageId).returns(() => 'toml'); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[build-system]\nrequires = ["flit_core >=3.2,<4"]\nbuild-backend = "flit_core.buildapi"\n\n[something]\nname = "something"\nversion = "2023.0.0"\nrequires-python = ">=3.8"\ndependencies = ["attrs>=21.3.0", "flake8-csv"]\n ', + ); + return someFile; +} + +suite('Create Env content button settings tests', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + let onDidCloseTextDocumentStub: sinon.SinonStub; + let onDidChangeDiagnosticsStub: sinon.SinonStub; + let onDidChangeActiveTextEditorStub: sinon.SinonStub; + let createDiagnosticCollectionStub: sinon.SinonStub; + let diagnosticCollection: typemoq.IMock<DiagnosticCollection>; + let getActiveTextEditorStub: sinon.SinonStub; + let textEditor: typemoq.IMock<TextEditor>; + let getInstalledPackagesDiagnosticsStub: sinon.SinonStub; + let interpreterService: typemoq.IMock<IInterpreterService>; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + getOpenTextDocumentsStub.returns([]); + + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + onDidCloseTextDocumentStub = sinon.stub(workspaceApis, 'onDidCloseTextDocument'); + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + onDidCloseTextDocumentStub.returns(new FakeDisposable()); + + onDidChangeDiagnosticsStub = sinon.stub(languageApis, 'onDidChangeDiagnostics'); + onDidChangeDiagnosticsStub.returns(new FakeDisposable()); + createDiagnosticCollectionStub = sinon.stub(languageApis, 'createDiagnosticCollection'); + diagnosticCollection = typemoq.Mock.ofType<DiagnosticCollection>(); + diagnosticCollection.setup((d) => d.set(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.clear()).returns(() => undefined); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).returns(() => undefined); + diagnosticCollection.setup((d) => d.has(typemoq.It.isAny())).returns(() => false); + createDiagnosticCollectionStub.returns(diagnosticCollection.object); + + onDidChangeActiveTextEditorStub = sinon.stub(windowApis, 'onDidChangeActiveTextEditor'); + onDidChangeActiveTextEditorStub.returns(new FakeDisposable()); + getActiveTextEditorStub = sinon.stub(windowApis, 'getActiveTextEditor'); + textEditor = typemoq.Mock.ofType<TextEditor>(); + getActiveTextEditorStub.returns(textEditor.object); + + getInstalledPackagesDiagnosticsStub = sinon.stub(installUtils, 'getInstalledPackagesDiagnostics'); + interpreterService = typemoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.onDidChangeInterpreter(typemoq.It.isAny(), undefined, undefined)) + .returns(() => new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Ensure nothing is run if there are no open documents', () => { + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should not run packages check if opened files are not dep files', () => { + const someFile = getSomeFile(); + const someTomlFile = getSomeTomlFile(); + getOpenTextDocumentsStub.returns([someFile.object, someTomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(executeCommandStub.notCalled); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test('Should run packages check if opened files are dep files', () => { + const reqFile = getSomeRequirementFile(); + const tomlFile = getPyProjectTomlFile(); + getOpenTextDocumentsStub.returns([reqFile.object, tomlFile.object]); + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + assert.ok(getInstalledPackagesDiagnosticsStub.calledTwice); + }); + + [getSomeRequirementFile().object, getPyProjectTomlFile().object].forEach((file) => { + test(`Should run packages check on open of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on save of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.calledOnce); + }); + + test(`Should run packages check on close of a dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidCloseTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + diagnosticCollection.reset(); + diagnosticCollection.setup((d) => d.delete(typemoq.It.isAny())).verifiable(typemoq.Times.once()); + diagnosticCollection + .setup((d) => d.has(typemoq.It.isAny())) + .returns(() => true) + .verifiable(typemoq.Times.once()); + + handler(file); + diagnosticCollection.verifyAll(); + }); + + test(`Should trigger a context update on active editor switch to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + + test(`Should trigger a context update to true on diagnostic change to dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => MISSING_PACKAGES); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, true)); + }); + }); + + [getSomeFile().object, getSomeTomlFile().object].forEach((file) => { + test(`Should not run packages check on open of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should not run packages check on save of a non dep file: ${file.fileName}`, () => { + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + getInstalledPackagesDiagnosticsStub.reset(); + + getInstalledPackagesDiagnosticsStub.returns(Promise.resolve(MISSING_PACKAGES)); + + handler(file); + assert.ok(getInstalledPackagesDiagnosticsStub.notCalled); + }); + + test(`Should trigger a context update on active editor switch to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeActiveTextEditorStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + + test(`Should trigger a context update to false on diagnostic change to non-dep file: ${file.fileName}`, () => { + let handler: () => void = () => { + /* do nothing */ + }; + onDidChangeDiagnosticsStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + registerInstalledPackagesDiagnosticsProvider(disposables, interpreterService.object); + + getActiveTextEditorStub.returns({ document: file }); + diagnosticCollection.setup((d) => d.get(typemoq.It.isAny())).returns(() => []); + + handler(); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', DEPS_NOT_INSTALLED_KEY, false)); + }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts new file mode 100644 index 000000000000..ee177a58c779 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/commonUtils.unit.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import { hasVenv } from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; + +suite('CommonUtils', () => { + let fileExistsStub: sinon.SinonStub; + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + fileExistsStub = sinon.stub(fs, 'pathExists'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Venv exists test', async () => { + fileExistsStub.resolves(true); + const result = await hasVenv(workspace1); + expect(result).to.be.equal(true, 'Incorrect result'); + + fileExistsStub.calledOnceWith(path.join(workspace1.uri.fsPath, '.venv', 'pyvenv.cfg')); + }); + + test('Venv does not exist test', async () => { + fileExistsStub.resolves(false); + const result = await hasVenv(workspace1); + expect(result).to.be.equal(false, 'Incorrect result'); + + fileExistsStub.calledOnceWith(path.join(workspace1.uri.fsPath, '.venv', 'pyvenv.cfg')); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts new file mode 100644 index 000000000000..e2ff9b2ab486 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaCreationProvider.unit.test.ts @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import { assert, use as chaiUse } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import { condaCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/condaCreationProvider'; +import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as condaUtils from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { Output } from '../../../../client/common/process/types'; +import { createDeferred } from '../../../../client/common/utils/async'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { CONDA_ENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/condaProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; +import { + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('Conda Creation provider tests', () => { + let condaProvider: CreateEnvironmentProvider; + let progressMock: typemoq.IMock<CreateEnvironmentProgress>; + let getCondaBaseEnvStub: sinon.SinonStub; + let pickPythonVersionStub: sinon.SinonStub; + let pickWorkspaceFolderStub: sinon.SinonStub; + let execObservableStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickExistingCondaActionStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + + setup(() => { + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + getCondaBaseEnvStub = sinon.stub(condaUtils, 'getCondaBaseEnv'); + pickPythonVersionStub = sinon.stub(condaUtils, 'pickPythonVersion'); + execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + pickExistingCondaActionStub = sinon.stub(condaUtils, 'pickExistingCondaAction'); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.Create); + + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + + progressMock = typemoq.Mock.ofType<CreateEnvironmentProgress>(); + condaProvider = condaCreationProvider(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No conda installed', async () => { + getCondaBaseEnvStub.resolves(undefined); + + assert.isUndefined(await condaProvider.createEnvironment()); + }); + + test('No workspace selected', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves(undefined); + + await assert.isRejected(condaProvider.createEnvironment()); + }); + + test('No python version picked selected', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves(undefined); + + await assert.isRejected(condaProvider.createEnvironment()); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Create conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + assert.deepStrictEqual(await promise, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Create conda environment failed', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + pickWorkspaceFolderStub.resolves({ + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _error: undefined | ((error: unknown) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: undefined, + out: { + subscribe: ( + _next?: (value: Output<string>) => void, + // eslint-disable-next-line no-shadow + error?: (error: unknown) => void, + complete?: () => void, + ) => { + _error = error; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_error); + _error!('bad arguments'); + _complete!(); + const result = await promise; + assert.ok(result?.error); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Create conda environment failed (non-zero exit code)', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickPythonVersionStub.resolves('3.10'); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = condaProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${CONDA_ENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + const result = await promise; + assert.ok(result?.error); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(pickExistingCondaActionStub.calledOnce); + }); + + test('Use existing conda environment', async () => { + getCondaBaseEnvStub.resolves('/usr/bin/conda'); + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + pickWorkspaceFolderStub.resolves(workspace1); + pickExistingCondaActionStub.resolves(condaUtils.ExistingCondaAction.UseExisting); + getPrefixCondaEnvPathStub.returns('existing_environment'); + + const result = await condaProvider.createEnvironment(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(pickPythonVersionStub.notCalled); + assert.isTrue(execObservableStub.notCalled); + assert.isTrue(withProgressStub.notCalled); + + assert.deepStrictEqual(result, { path: 'existing_environment', workspaceFolder: workspace1 }); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..b1acd0678714 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaDeleteUtils.unit.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Uri } from 'vscode'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { deleteCondaEnvironment } from '../../../../client/pythonEnvironments/creation/provider/condaDeleteUtils'; + +suite('Conda Delete test', () => { + let plainExecStub: sinon.SinonStub; + let getPrefixCondaEnvPathStub: sinon.SinonStub; + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + plainExecStub = sinon.stub(rawProcessApis, 'plainExec'); + getPrefixCondaEnvPathStub = sinon.stub(commonUtils, 'getPrefixCondaEnvPath'); + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete conda env ', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isTrue(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete conda env with error', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(true); + plainExecStub.resolves({ stdout: 'stdout' }); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.calledOnce); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete conda env with exception', async () => { + getPrefixCondaEnvPathStub.returns('condaEnvPath'); + hasPrefixCondaEnvStub.resolves(false); + plainExecStub.rejects(new Error('error')); + const result = await deleteCondaEnvironment(workspace1, 'interpreter', 'pathEnvVar'); + assert.isFalse(result); + assert.isTrue(plainExecStub.calledOnce); + assert.isTrue(getPrefixCondaEnvPathStub.calledOnce); + assert.isTrue(hasPrefixCondaEnvStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts new file mode 100644 index 000000000000..a3f4a1abe905 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/condaUtils.unit.test.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { CancellationTokenSource, Uri } from 'vscode'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import { + ExistingCondaAction, + pickExistingCondaAction, + pickPythonVersion, +} from '../../../../client/pythonEnvironments/creation/provider/condaUtils'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Conda Utils test', () => { + let showQuickPickWithBackStub: sinon.SinonStub; + + setup(() => { + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No version selected or user pressed escape', async () => { + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickPythonVersion(); + assert.isUndefined(actual); + }); + + test('User selected a version', async () => { + showQuickPickWithBackStub.resolves({ label: 'Python', description: '3.10' }); + + const actual = await pickPythonVersion(); + assert.equal(actual, '3.10'); + }); + + test('With cancellation', async () => { + const source = new CancellationTokenSource(); + + showQuickPickWithBackStub.callsFake(() => { + source.cancel(); + }); + + const actual = await pickPythonVersion(source.token); + assert.isUndefined(actual); + }); +}); + +suite('Existing .conda env test', () => { + let hasPrefixCondaEnvStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + hasPrefixCondaEnvStub = sinon.stub(commonUtils, 'hasPrefixCondaEnv'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No .conda found', async () => { + hasPrefixCondaEnvStub.resolves(false); + showQuickPickWithBackStub.resolves(undefined); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Create); + assert.isTrue(showQuickPickWithBackStub.notCalled); + }); + + test('User presses escape', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingCondaAction(workspace1)); + }); + + test('.conda found and user selected to re-create', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.recreate, + description: CreateEnv.Conda.recreateDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.Recreate); + }); + + test('.conda found and user selected to re-use', async () => { + hasPrefixCondaEnvStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Conda.useExisting, + description: CreateEnv.Conda.useExistingDescription, + }); + + const actual = await pickExistingCondaAction(workspace1); + assert.deepStrictEqual(actual, ExistingCondaAction.UseExisting); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts new file mode 100644 index 000000000000..aa2d317c405e --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvCreationProvider.unit.test.ts @@ -0,0 +1,551 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as chaiAsPromised from 'chai-as-promised'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import * as sinon from 'sinon'; +import { CancellationToken, ProgressOptions, Uri } from 'vscode'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import { VenvCreationProvider } from '../../../../client/pythonEnvironments/creation/provider/venvCreationProvider'; +import { IInterpreterQuickPick } from '../../../../client/interpreter/configuration/types'; +import * as wsSelect from '../../../../client/pythonEnvironments/creation/common/workspaceSelection'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as rawProcessApis from '../../../../client/common/process/rawProcessApis'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { createDeferred } from '../../../../client/common/utils/async'; +import { Output, SpawnOptions } from '../../../../client/common/process/types'; +import { VENV_CREATED_MARKER } from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; +import * as venvUtils from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { + CreateEnvironmentProvider, + CreateEnvironmentResult, +} from '../../../../client/pythonEnvironments/creation/proposed.createEnvApis'; + +chaiUse(chaiAsPromised.default); + +suite('venv Creation provider tests', () => { + let venvProvider: CreateEnvironmentProvider; + let pickWorkspaceFolderStub: sinon.SinonStub; + let interpreterQuickPick: typemoq.IMock<IInterpreterQuickPick>; + let progressMock: typemoq.IMock<CreateEnvironmentProgress>; + let execObservableStub: sinon.SinonStub; + let withProgressStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let pickPackagesToInstallStub: sinon.SinonStub; + let pickExistingVenvActionStub: sinon.SinonStub; + let deleteEnvironmentStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pickExistingVenvActionStub = sinon.stub(venvUtils, 'pickExistingVenvAction'); + deleteEnvironmentStub = sinon.stub(venvUtils, 'deleteEnvironment'); + pickWorkspaceFolderStub = sinon.stub(wsSelect, 'pickWorkspaceFolder'); + execObservableStub = sinon.stub(rawProcessApis, 'execObservable'); + interpreterQuickPick = typemoq.Mock.ofType<IInterpreterQuickPick>(); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + pickPackagesToInstallStub = sinon.stub(venvUtils, 'pickPackagesToInstall'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + progressMock = typemoq.Mock.ofType<CreateEnvironmentProgress>(); + venvProvider = new VenvCreationProvider(interpreterQuickPick.object); + + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Create); + deleteEnvironmentStub.resolves(true); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No workspace selected', async () => { + pickWorkspaceFolderStub.resolves(undefined); + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny())) + .verifiable(typemoq.Times.never()); + + await assert.isRejected(venvProvider.createEnvironment()); + assert.isTrue(pickWorkspaceFolderStub.calledOnce); + interpreterQuickPick.verifyAll(); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(pickExistingVenvActionStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('No Python selected', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(typemoq.Times.once()); + + await assert.isRejected(venvProvider.createEnvironment()); + + assert.isTrue(pickWorkspaceFolderStub.calledOnce); + interpreterQuickPick.verifyAll(); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('User pressed Esc while selecting dependencies', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves(undefined); + + await assert.isRejected(venvProvider.createEnvironment()); + assert.isTrue(pickPackagesToInstallStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with python selected by user no packages selected', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv failed', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _error: undefined | ((error: unknown) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + _next?: (value: Output<string>) => void, + // eslint-disable-next-line no-shadow + error?: (error: unknown) => void, + complete?: () => void, + ) => { + _error = error; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_error); + _error!('bad arguments'); + _complete!(); + const result = await promise; + assert.ok(result?.error); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv failed (non-zero exit code)', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 1, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + const result = await promise; + assert.ok(result?.error); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.calledOnce); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with pre-existing .venv, user selects re-create', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + execObservableStub.callsFake(() => { + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects re-create, delete env failed', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.Recreate); + pickWorkspaceFolderStub.resolves(workspace1); + deleteEnvironmentStub.resolves(false); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + pickPackagesToInstallStub.resolves([]); + + await assert.isRejected(venvProvider.createEnvironment()); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.calledOnce); + }); + + test('Create venv with pre-existing .venv, user selects use existing', async () => { + pickExistingVenvActionStub.resolves(venvUtils.ExistingVenvAction.UseExisting); + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.never()); + + pickPackagesToInstallStub.resolves([]); + + interpreterQuickPick.verifyAll(); + assert.isTrue(withProgressStub.notCalled); + assert.isTrue(pickPackagesToInstallStub.notCalled); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + }); + + test('Create venv with 1000 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 1000 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expected = JSON.stringify({ requirements: requirements.map((r) => r.installItem) }); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + execObservableStub.callsFake((_c, argv: string[], options) => { + stdin = options?.stdinStr; + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.strictEqual(stdin, expected); + assert.isTrue(hasStdinArg); + }); + + test('Create venv with 5 requirement files', async () => { + pickWorkspaceFolderStub.resolves(workspace1); + + interpreterQuickPick + .setup((i) => i.getInterpreterViaQuickPick(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => Promise.resolve('/usr/bin/python')) + .verifiable(typemoq.Times.once()); + + const requirements = Array.from({ length: 5 }, (_, i) => ({ + installType: 'requirements', + installItem: `requirements${i}.txt`, + })); + pickPackagesToInstallStub.resolves(requirements); + const expectedRequirements = requirements.map((r) => r.installItem).sort(); + + const deferred = createDeferred(); + let _next: undefined | ((value: Output<string>) => void); + let _complete: undefined | (() => void); + let stdin: undefined | string; + let hasStdinArg = false; + let actualRequirements: string[] = []; + execObservableStub.callsFake((_c, argv: string[], options: SpawnOptions) => { + stdin = options?.stdinStr; + actualRequirements = argv.filter((arg) => arg.startsWith('requirements')).sort(); + hasStdinArg = argv.includes('--stdin'); + deferred.resolve(); + return { + proc: { + exitCode: 0, + }, + out: { + subscribe: ( + next?: (value: Output<string>) => void, + _error?: (error: unknown) => void, + complete?: () => void, + ) => { + _next = next; + _complete = complete; + }, + }, + dispose: () => undefined, + }; + }); + + progressMock.setup((p) => p.report({ message: CreateEnv.statusStarting })).verifiable(typemoq.Times.once()); + + withProgressStub.callsFake( + ( + _options: ProgressOptions, + task: ( + progress: CreateEnvironmentProgress, + token?: CancellationToken, + ) => Thenable<CreateEnvironmentResult>, + ) => task(progressMock.object), + ); + + const promise = venvProvider.createEnvironment(); + await deferred.promise; + assert.isDefined(_next); + assert.isDefined(_complete); + + _next!({ out: `${VENV_CREATED_MARKER}new_environment`, source: 'stdout' }); + _complete!(); + + const actual = await promise; + assert.deepStrictEqual(actual, { + path: 'new_environment', + workspaceFolder: workspace1, + }); + interpreterQuickPick.verifyAll(); + progressMock.verifyAll(); + assert.isTrue(showErrorMessageWithLogsStub.notCalled); + assert.isTrue(deleteEnvironmentStub.notCalled); + assert.isUndefined(stdin); + assert.deepStrictEqual(actualRequirements, expectedRequirements); + assert.isFalse(hasStdinArg); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts new file mode 100644 index 000000000000..231222acbaec --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvDeleteUtils.unit.test.ts @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as sinon from 'sinon'; +import { Uri, WorkspaceFolder } from 'vscode'; +import { assert } from 'chai'; +import * as path from 'path'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import * as commonUtils from '../../../../client/pythonEnvironments/creation/common/commonUtils'; +import { + deleteEnvironmentNonWindows, + deleteEnvironmentWindows, +} from '../../../../client/pythonEnvironments/creation/provider/venvDeleteUtils'; +import * as switchPython from '../../../../client/pythonEnvironments/creation/provider/venvSwitchPython'; +import * as asyncApi from '../../../../client/common/utils/async'; + +suite('Test Delete environments (windows)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let unlinkStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + let switchPythonStub: sinon.SinonStub; + let sleepStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + pathExistsStub.resolves(true); + + rmdirStub = sinon.stub(fs, 'rmdir'); + unlinkStub = sinon.stub(fs, 'unlink'); + + sleepStub = sinon.stub(asyncApi, 'sleep'); + sleepStub.resolves(); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + + switchPythonStub = sinon.stub(switchPython, 'switchSelectedPython'); + switchPythonStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + rmdirStub.resolves(); + unlinkStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe succeeded but venv dir failed', async () => { + rmdirStub.rejects(); + unlinkStub.resolves(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(unlinkStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed first attempt', async () => { + unlinkStub.rejects(); + rmdirStub.resolves(); + assert.ok(await deleteEnvironmentWindows(workspace1, 'python.exe')); + + assert.ok(rmdirStub.calledOnce); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete python.exe failed all attempts', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, 'python.exe')); + assert.ok(switchPythonStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); + + test('Delete python.exe failed no interpreter', async () => { + unlinkStub.rejects(); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentWindows(workspace1, undefined)); + assert.ok(switchPythonStub.notCalled); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); + +suite('Test Delete environments (linux/mac)', () => { + let pathExistsStub: sinon.SinonStub; + let rmdirStub: sinon.SinonStub; + let showErrorMessageWithLogsStub: sinon.SinonStub; + + const workspace1: WorkspaceFolder = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + rmdirStub = sinon.stub(fs, 'rmdir'); + + showErrorMessageWithLogsStub = sinon.stub(commonUtils, 'showErrorMessageWithLogs'); + showErrorMessageWithLogsStub.resolves(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Delete venv folder succeeded', async () => { + pathExistsStub.resolves(true); + rmdirStub.resolves(); + + assert.ok(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.notCalled); + }); + + test('Delete venv folder failed', async () => { + pathExistsStub.resolves(true); + rmdirStub.rejects(); + assert.notOk(await deleteEnvironmentNonWindows(workspace1)); + + assert.ok(pathExistsStub.calledOnce); + assert.ok(rmdirStub.calledOnce); + assert.ok(showErrorMessageWithLogsStub.calledOnce); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts new file mode 100644 index 000000000000..ecb7d1434ada --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvProgressAndTelemetry.unit.test.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + VENV_CREATED_MARKER, + VenvProgressAndTelemetry, +} from '../../../../client/pythonEnvironments/creation/provider/venvProgressAndTelemetry'; +import { CreateEnvironmentProgress } from '../../../../client/pythonEnvironments/creation/types'; +import * as telemetry from '../../../../client/telemetry'; +import { CreateEnv } from '../../../../client/common/utils/localize'; + +suite('Venv Progress and Telemetry', () => { + let sendTelemetryEventStub: sinon.SinonStub; + let progressReporterMock: typemoq.IMock<CreateEnvironmentProgress>; + + setup(() => { + sendTelemetryEventStub = sinon.stub(telemetry, 'sendTelemetryEvent'); + progressReporterMock = typemoq.Mock.ofType<CreateEnvironmentProgress>(); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Ensure telemetry event and progress are sent', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); + + test('Do not trigger telemetry event the second time', async () => { + const progressReporter = progressReporterMock.object; + progressReporterMock + .setup((p) => p.report({ message: CreateEnv.Venv.created })) + .returns(() => undefined) + .verifiable(typemoq.Times.once()); + + const progressAndTelemetry = new VenvProgressAndTelemetry(progressReporter); + progressAndTelemetry.process(VENV_CREATED_MARKER); + progressAndTelemetry.process(VENV_CREATED_MARKER); + assert.isTrue(sendTelemetryEventStub.calledOnce); + progressReporterMock.verifyAll(); + }); +}); diff --git a/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts new file mode 100644 index 000000000000..2c8ec2ebce87 --- /dev/null +++ b/src/test/pythonEnvironments/creation/provider/venvUtils.unit.test.ts @@ -0,0 +1,489 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { assert, use as chaiUse } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as path from 'path'; +import * as fs from '../../../../client/common/platform/fs-paths'; +import * as windowApis from '../../../../client/common/vscodeApis/windowApis'; +import * as workspaceApis from '../../../../client/common/vscodeApis/workspaceApis'; +import { + ExistingVenvAction, + OPEN_REQUIREMENTS_BUTTON, + pickExistingVenvAction, + pickPackagesToInstall, +} from '../../../../client/pythonEnvironments/creation/provider/venvUtils'; +import { EXTENSION_ROOT_DIR_FOR_TESTS } from '../../../constants'; +import { CreateEnv } from '../../../../client/common/utils/localize'; +import { createDeferred } from '../../../../client/common/utils/async'; + +chaiUse(chaiAsPromised.default); + +suite('Venv Utils test', () => { + let findFilesStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + let showTextDocumentStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + findFilesStub = sinon.stub(workspaceApis, 'findFiles'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + pathExistsStub = sinon.stub(fs, 'pathExists'); + readFileStub = sinon.stub(fs, 'readFile'); + showTextDocumentStub = sinon.stub(windowApis, 'showTextDocument'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('No requirements or toml found', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(false); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + + test('Toml found with no build system', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves('[project]\nname = "spam"\nversion = "2020.0.0"\n'); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + + test('Toml found with no project table', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[tool.poetry]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]', + ); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, []); + }); + + test('Toml found with no optional deps', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]', + ); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue(showQuickPickWithBackStub.notCalled); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Toml found with deps, but user presses escape', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickWithBackStub.resolves(undefined); + + await assert.isRejected(pickPackagesToInstall(workspace1)); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + }); + + test('Toml found with dependencies and user selects None', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickWithBackStub.resolves([]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Toml found with dependencies and user selects One', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]', + ); + + showQuickPickWithBackStub.resolves([{ label: 'doc' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + installItem: 'doc', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Toml found with dependencies and user selects Few', async () => { + findFilesStub.resolves([]); + pathExistsStub.resolves(true); + readFileStub.resolves( + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[project.optional-dependencies]\ntest = ["pytest"]\ndoc = ["sphinx", "furo"]\ncov = ["pytest-cov"]', + ); + + showQuickPickWithBackStub.resolves([{ label: 'test' }, { label: 'cov' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [{ label: 'test' }, { label: 'doc' }, { label: 'cov' }], + { + placeHolder: CreateEnv.Venv.tomlExtrasQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'toml', + installItem: 'test', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + { + installType: 'toml', + installItem: 'cov', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + { + installType: 'toml', + source: path.join(workspace1.uri.fsPath, 'pyproject.toml'), + }, + ]); + }); + + test('Requirements found, but user presses escape', async () => { + pathExistsStub.resolves(true); + readFileStub.resolves('[project]\nname = "spam"\nversion = "2020.0.0"\n'); + + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + + showQuickPickWithBackStub.resolves(undefined); + + await assert.isRejected(pickPackagesToInstall(workspace1)); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.isTrue(readFileStub.calledOnce); + assert.isTrue(pathExistsStub.calledOnce); + }); + + test('Requirements found and user selects None', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickWithBackStub.resolves([]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.deepStrictEqual(actual, []); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects One', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickWithBackStub.resolves([{ label: 'requirements.txt' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'requirements', + installItem: path.join(workspace1.uri.fsPath, 'requirements.txt'), + }, + ]); + assert.isTrue(readFileStub.notCalled); + }); + + test('Requirements found and user selects Few', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + showQuickPickWithBackStub.resolves([{ label: 'dev-requirements.txt' }, { label: 'test-requirements.txt' }]); + + const actual = await pickPackagesToInstall(workspace1); + assert.isTrue( + showQuickPickWithBackStub.calledWithExactly( + [ + { label: 'requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'dev-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + { label: 'test-requirements.txt', buttons: [OPEN_REQUIREMENTS_BUTTON] }, + ], + { + placeHolder: CreateEnv.Venv.requirementsQuickPickTitle, + ignoreFocusOut: true, + canPickMany: true, + }, + undefined, + sinon.match.func, + ), + ); + assert.deepStrictEqual(actual, [ + { + installType: 'requirements', + installItem: path.join(workspace1.uri.fsPath, 'dev-requirements.txt'), + }, + { + installType: 'requirements', + installItem: path.join(workspace1.uri.fsPath, 'test-requirements.txt'), + }, + ]); + assert.isTrue(readFileStub.notCalled); + }); + + test('User clicks button to open requirements.txt', async () => { + let allow = true; + findFilesStub.callsFake(() => { + if (allow) { + allow = false; + return Promise.resolve([ + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'dev-requirements.txt')), + Uri.file(path.join(workspace1.uri.fsPath, 'test-requirements.txt')), + ]); + } + return Promise.resolve([]); + }); + pathExistsStub.resolves(false); + + const deferred = createDeferred(); + showQuickPickWithBackStub.callsFake(async (_items, _options, _token, callback) => { + callback({ + button: OPEN_REQUIREMENTS_BUTTON, + item: { label: 'requirements.txt' }, + }); + await deferred.promise; + return [{ label: 'requirements.txt' }]; + }); + + let uri: Uri | undefined; + showTextDocumentStub.callsFake((arg: Uri) => { + uri = arg; + deferred.resolve(); + return Promise.resolve(); + }); + + await pickPackagesToInstall(workspace1); + assert.deepStrictEqual( + uri?.toString(), + Uri.file(path.join(workspace1.uri.fsPath, 'requirements.txt')).toString(), + ); + }); +}); + +suite('Test pick existing venv action', () => { + let withProgressStub: sinon.SinonStub; + let showQuickPickWithBackStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + + const workspace1 = { + uri: Uri.file(path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'workspace1')), + name: 'workspace1', + index: 0, + }; + + setup(() => { + pathExistsStub = sinon.stub(fs, 'pathExists'); + withProgressStub = sinon.stub(windowApis, 'withProgress'); + showQuickPickWithBackStub = sinon.stub(windowApis, 'showQuickPickWithBack'); + }); + teardown(() => { + sinon.restore(); + }); + + test('User selects existing venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.useExisting, + description: CreateEnv.Venv.useExistingDescription, + }); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.UseExisting); + }); + + test('User presses escape', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves(undefined); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('User selects delete venv', async () => { + pathExistsStub.resolves(true); + showQuickPickWithBackStub.resolves({ + label: CreateEnv.Venv.recreate, + description: CreateEnv.Venv.recreateDescription, + }); + withProgressStub.resolves(true); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Recreate); + }); + + test('User clicks on back', async () => { + pathExistsStub.resolves(true); + // We use reject with "Back" to simulate the user clicking on back. + showQuickPickWithBackStub.rejects(windowApis.MultiStepAction.Back); + withProgressStub.resolves(false); + await assert.isRejected(pickExistingVenvAction(workspace1)); + }); + + test('No venv found', async () => { + pathExistsStub.resolves(false); + const actual = await pickExistingVenvAction(workspace1); + assert.deepStrictEqual(actual, ExistingVenvAction.Create); + }); +}); diff --git a/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts new file mode 100644 index 000000000000..3e787570304a --- /dev/null +++ b/src/test/pythonEnvironments/creation/pyProjectTomlContext.unit.test.ts @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable @typescript-eslint/no-explicit-any */ + +import * as chaiAsPromised from 'chai-as-promised'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { assert, use as chaiUse } from 'chai'; +import { TextDocument } from 'vscode'; +import * as cmdApis from '../../../client/common/vscodeApis/commandApis'; +import * as workspaceApis from '../../../client/common/vscodeApis/workspaceApis'; +import { IDisposableRegistry } from '../../../client/common/types'; +import { registerPyProjectTomlFeatures } from '../../../client/pythonEnvironments/creation/pyProjectTomlContext'; + +chaiUse(chaiAsPromised.default); + +class FakeDisposable { + public dispose() { + // Do nothing + } +} + +function getInstallableToml(): typemoq.IMock<TextDocument> { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType<TextDocument>(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns( + () => + '[project]\nname = "spam"\nversion = "2020.0.0"\n[build-system]\nrequires = ["setuptools ~= 58.0", "cython ~= 0.29.0"]\n[dependency-groups]\ndev = ["ruff", { include-group = "test" }]\ntest = ["pytest"]', + ); + return pyprojectToml; +} + +function getNonInstallableToml(): typemoq.IMock<TextDocument> { + const pyprojectTomlPath = 'pyproject.toml'; + const pyprojectToml = typemoq.Mock.ofType<TextDocument>(); + pyprojectToml.setup((p) => p.fileName).returns(() => pyprojectTomlPath); + pyprojectToml + .setup((p) => p.getText(typemoq.It.isAny())) + .returns(() => '[project]\nname = "spam"\nversion = "2020.0.0"\n'); + return pyprojectToml; +} + +function getSomeFile(): typemoq.IMock<TextDocument> { + const someFilePath = 'something.py'; + const someFile = typemoq.Mock.ofType<TextDocument>(); + someFile.setup((p) => p.fileName).returns(() => someFilePath); + someFile.setup((p) => p.getText(typemoq.It.isAny())).returns(() => 'print("Hello World")'); + return someFile; +} + +suite('PyProject.toml Create Env Features', () => { + let executeCommandStub: sinon.SinonStub; + const disposables: IDisposableRegistry = []; + let getOpenTextDocumentsStub: sinon.SinonStub; + let onDidOpenTextDocumentStub: sinon.SinonStub; + let onDidSaveTextDocumentStub: sinon.SinonStub; + + setup(() => { + executeCommandStub = sinon.stub(cmdApis, 'executeCommand'); + getOpenTextDocumentsStub = sinon.stub(workspaceApis, 'getOpenTextDocuments'); + onDidOpenTextDocumentStub = sinon.stub(workspaceApis, 'onDidOpenTextDocument'); + onDidSaveTextDocumentStub = sinon.stub(workspaceApis, 'onDidSaveTextDocument'); + + onDidOpenTextDocumentStub.returns(new FakeDisposable()); + onDidSaveTextDocumentStub.returns(new FakeDisposable()); + }); + + teardown(() => { + sinon.restore(); + disposables.forEach((d) => d.dispose()); + }); + + test('Installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non installable pyproject.toml is already open in the editor on extension activate', async () => { + const pyprojectToml = getNonInstallableToml(); + getOpenTextDocumentsStub.returns([pyprojectToml.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file open in the editor on extension activate', async () => { + const someFile = getSomeFile(); + getOpenTextDocumentsStub.returns([someFile.object]); + + registerPyProjectTomlFeatures(disposables); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', true)); + + handler(pyprojectToml.object); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Some random file is opened in the editor', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.neverCalledWith('setContext', 'pipInstallableToml', false)); + }); + + test('Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Non Installable pyproject.toml is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const pyprojectToml = getNonInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(pyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + }); + + test('Non Installable pyproject.toml is changed to Installable', async () => { + getOpenTextDocumentsStub.returns([]); + + let openHandler: (doc: TextDocument) => void = () => { + /* do nothing */ + }; + onDidOpenTextDocumentStub.callsFake((callback) => { + openHandler = callback; + return new FakeDisposable(); + }); + + let changeHandler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + changeHandler = callback; + return new FakeDisposable(); + }); + + const nonInatallablePyprojectToml = getNonInstallableToml(); + const installablePyprojectToml = getInstallableToml(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + openHandler(nonInatallablePyprojectToml.object); + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + changeHandler(installablePyprojectToml.object); + + assert.ok(executeCommandStub.calledOnceWithExactly('setContext', 'pipInstallableToml', true)); + }); + + test('Some random file is changed', async () => { + getOpenTextDocumentsStub.returns([]); + + let handler: (d: TextDocument) => void = () => { + /* do nothing */ + }; + onDidSaveTextDocumentStub.callsFake((callback) => { + handler = callback; + return new FakeDisposable(); + }); + + const someFile = getSomeFile(); + + registerPyProjectTomlFeatures(disposables); + assert.ok(executeCommandStub.calledWithExactly('setContext', 'pipInstallableToml', false)); + executeCommandStub.reset(); + + handler(someFile.object); + + assert.ok(executeCommandStub.notCalled); + }); +}); diff --git a/src/test/pythonEnvironments/discovery/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<IPlatformService>; + let condaService: CondaService; + let fileSystem: TypeMoq.IMock<IFileSystem>; + setup(async () => { + platformService = TypeMoq.Mock.ofType<IPlatformService>(); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + + 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) + <python.exe> + <python2.7.exe> + <python3.exe> + <python3.8.exe> + <python3.8> + <python3.8.1rc1.10213.exe> # should match but doesn't + #<python27.exe> + #<python38.exe> + <python.3.8.exe> # should match but doesn't + python.txt + <my-python.exe> # should match but doesn't + <spam.exe> + spam.txt + parent/ + root2/ + <python2.exe> + <python2> + root3/ # empty + root4/ # no executables + subdir/ + spam.txt + python2 + #python.exe # matches on Windows (not actually executable though) + root5/ # executables only in subdir + subdir/ + <python2.exe> + <python2> + python2 + #python2.exe # matches on Windows (not actually executable though) + root6/ # no matching executables + <spam.exe> + spam.txt + <py> + <py.exe> + `.trimEnd(); + + suiteSetup(async function () { + if (!IS_WINDOWS) { + if (!process.env.PVSC_TEST_FORCE) { + this.skip(); + } + } + await ensureFSTree(dataTree, __dirname); + }); + setup(async () => { + if (!IS_WINDOWS) { + // eslint-disable-next-line global-require + const platformAPI = require('../../../../../client/common/utils/platform'); + const stub = sinon.stub(platformAPI, 'getOSType'); + stub.returns(OSType.Windows); + } + sinon.stub(externalDependencies, 'inExperiment').returns(true); + cleanUps = []; + + const oldSearchPath = process.env[ENV_VAR]; + cleanUps.push(() => { + process.env[ENV_VAR] = oldSearchPath; + }); + }); + teardown(() => { + cleanUps.forEach((run) => { + try { + run(); + } catch (err) { + console.log(err); + } + }); + sinon.restore(); + }); + + function getActiveLocator(...roots: string[]): WindowsPathEnvVarLocator { + process.env[ENV_VAR] = roots.join(path.delimiter); + const locator = new WindowsPathEnvVarLocator(); + cleanUps.push(() => locator.dispose()); + return locator; + } + + suite('iterEnvs()', () => { + test('no executables found', async () => { + const expected: BasicEnvInfo[] = []; + const locator = getActiveLocator(ROOT3, ROOT4, DOES_NOT_EXIST, ROOT5); + const query: PythonLocatorQuery | undefined = undefined; + + const iterator = locator.iterEnvs(query); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('no executables match', async () => { + const expected: BasicEnvInfo[] = []; + const locator = getActiveLocator(ROOT6, DOES_NOT_EXIST); + const query: PythonLocatorQuery | undefined = undefined; + + const iterator = locator.iterEnvs(query); + const envs = await getEnvs(iterator); + + assert.deepEqual(envs, expected); + }); + + test('some executables match', async () => { + const expected: BasicEnvInfo[] = [ + createBasicEnv(PythonEnvKind.System, path.join(ROOT1, 'python.exe'), [PythonEnvSource.PathEnvVar]), + + // We will expect the following once we switch + // to a better filter than isStandardPythonBinary(). + + // // On Windows we do not assume 2.7 for "python.exe". + // getEnv('', '2.7', path.join(ROOT2, 'python2.exe')), + // // This file isn't executable (but on Windows we can't tell that): + // getEnv('', '2.7', path.join(ROOT1, 'python2.exe')), + // getEnv('', '', path.join(ROOT1, 'python.exe')), + // getEnv('', '2.7', path.join(ROOT1, 'python2.7.exe')), + // getEnv('', '3.8', path.join(ROOT1, 'python3.8.exe')), + // getEnv('', '3', path.join(ROOT1, 'python3.exe')), + ]; + const locator = getActiveLocator(ROOT2, ROOT6, ROOT1); + const query: PythonLocatorQuery | undefined = undefined; + + const iterator = locator.iterEnvs(query); + const envs = await getEnvs(iterator); + + assertBasicEnvsEqual(envs, expected); + }); + }); +}); diff --git a/src/test/pythonEnvironments/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<ExecutionResult<string>>; +} + +suite('getExecutablePath()', () => { + let deps: IMock<IDeps>; + const python = buildPythonExecInfo('path/to/python'); + + setup(() => { + deps = Mock.ofType<IDeps>(undefined, MockBehavior.Strict); + }); + + test('should get the value by running python', async () => { + const expected = 'path/to/dummy/executable'; + 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<ShellExecResult>; +} + +suite('getInterpreterInfo()', () => { + let deps: IMock<IDeps>; + const python = buildPythonExecInfo('path/to/python'); + + setup(() => { + deps = Mock.ofType<IDeps>(undefined, MockBehavior.Strict); + }); + + test('should call exec() with the proper command and timeout', async () => { + const json = { + versionInfo: [3, 7, 5, 'candidate', 1], + sysPrefix: '/path/of/sysprefix/versions/3.7.5rc1', + version: '3.7.5rc1 (default, Oct 18 2019, 14:48:48) \n[Clang 11.0.0 (clang-1100.0.33.8)]', + is64Bit: true, + }; + const cmd = `"${python.command}" "${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<void> { + initializeExternalDependencies(serviceContainer); + // The old tests do not need real instances, directly pass in mocks. + registerNewDiscoveryForIOC(serviceManager, instance(mock<IDiscoveryAPI>())); +} diff --git a/src/test/pythonEnvironments/nativeAPI.unit.test.ts b/src/test/pythonEnvironments/nativeAPI.unit.test.ts new file mode 100644 index 000000000000..a3696b59c6ac --- /dev/null +++ b/src/test/pythonEnvironments/nativeAPI.unit.test.ts @@ -0,0 +1,338 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +/* eslint-disable class-methods-use-this */ + +import { assert } from 'chai'; +import * as path from 'path'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as nativeAPI from '../../client/pythonEnvironments/nativeAPI'; +import { IDiscoveryAPI } from '../../client/pythonEnvironments/base/locator'; +import { + NativeEnvInfo, + NativeEnvManagerInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import { Architecture, getPathEnvVariable, isWindows } from '../../client/common/utils/platform'; +import { PythonEnvInfo, PythonEnvKind, PythonEnvType } from '../../client/pythonEnvironments/base/info'; +import { NativePythonEnvironmentKind } from '../../client/pythonEnvironments/base/locators/common/nativePythonUtils'; +import * as condaApi from '../../client/pythonEnvironments/common/environmentManagers/conda'; +import * as pyenvApi from '../../client/pythonEnvironments/common/environmentManagers/pyenv'; +import * as pw from '../../client/pythonEnvironments/base/locators/common/pythonWatcher'; +import * as ws from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python API', () => { + let api: IDiscoveryAPI; + let mockFinder: typemoq.IMock<NativePythonFinder>; + let setCondaBinaryStub: sinon.SinonStub; + let getCondaPathSettingStub: sinon.SinonStub; + let getCondaEnvDirsStub: sinon.SinonStub; + let setPyEnvBinaryStub: sinon.SinonStub; + let createPythonWatcherStub: sinon.SinonStub; + let mockWatcher: typemoq.IMock<pw.PythonWatcher>; + let getWorkspaceFoldersStub: sinon.SinonStub; + + const basicEnv: NativeEnvInfo = { + displayName: 'Basic Python', + name: 'basic_python', + executable: '/usr/bin/python', + kind: NativePythonEnvironmentKind.LinuxGlobal, + version: `3.12.0`, + prefix: '/usr/bin', + }; + + const basicEnv2: NativeEnvInfo = { + displayName: 'Basic Python', + name: 'basic_python', + executable: '/usr/bin/python', + kind: NativePythonEnvironmentKind.LinuxGlobal, + version: undefined, // this is intentionally set to trigger resolve + prefix: '/usr/bin', + }; + + const expectedBasicEnv: PythonEnvInfo = { + arch: Architecture.Unknown, + id: '/usr/bin/python', + detailedDisplayName: 'Python 3.12.0 (basic_python)', + display: 'Python 3.12.0 (basic_python)', + distro: { org: '' }, + executable: { filename: '/usr/bin/python', sysPrefix: '/usr/bin', ctime: -1, mtime: -1 }, + kind: PythonEnvKind.System, + location: '/usr/bin/python', + source: [], + name: 'basic_python', + type: undefined, + version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 }, + }; + + const conda: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: '/home/user/.conda/envs/conda_python/python', + kind: NativePythonEnvironmentKind.Conda, + version: `3.12.0`, + prefix: '/home/user/.conda/envs/conda_python', + }; + + const conda1: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: '/home/user/.conda/envs/conda_python/python', + kind: NativePythonEnvironmentKind.Conda, + version: undefined, // this is intentionally set to test conda without python + prefix: '/home/user/.conda/envs/conda_python', + }; + + const conda2: NativeEnvInfo = { + displayName: 'Conda Python', + name: 'conda_python', + executable: undefined, // this is intentionally set to test env with no executable + kind: NativePythonEnvironmentKind.Conda, + version: undefined, // this is intentionally set to test conda without python + prefix: '/home/user/.conda/envs/conda_python', + }; + + const exePath = isWindows() + ? path.join('/home/user/.conda/envs/conda_python', 'python.exe') + : path.join('/home/user/.conda/envs/conda_python', 'python'); + + const expectedConda1: PythonEnvInfo = { + arch: Architecture.Unknown, + detailedDisplayName: 'Python 3.12.0 (conda_python)', + display: 'Python 3.12.0 (conda_python)', + distro: { org: '' }, + id: '/home/user/.conda/envs/conda_python/python', + executable: { + filename: '/home/user/.conda/envs/conda_python/python', + sysPrefix: '/home/user/.conda/envs/conda_python', + ctime: -1, + mtime: -1, + }, + kind: PythonEnvKind.Conda, + location: '/home/user/.conda/envs/conda_python', + source: [], + name: 'conda_python', + type: PythonEnvType.Conda, + version: { sysVersion: '3.12.0', major: 3, minor: 12, micro: 0 }, + }; + + const expectedConda2: PythonEnvInfo = { + arch: Architecture.Unknown, + detailedDisplayName: 'Conda Python', + display: 'Conda Python', + distro: { org: '' }, + id: exePath, + executable: { + filename: exePath, + sysPrefix: '/home/user/.conda/envs/conda_python', + ctime: -1, + mtime: -1, + }, + kind: PythonEnvKind.Conda, + location: '/home/user/.conda/envs/conda_python', + source: [], + name: 'conda_python', + type: PythonEnvType.Conda, + version: { sysVersion: undefined, major: -1, minor: -1, micro: -1 }, + }; + + setup(() => { + setCondaBinaryStub = sinon.stub(condaApi, 'setCondaBinary'); + getCondaEnvDirsStub = sinon.stub(condaApi, 'getCondaEnvDirs'); + getCondaPathSettingStub = sinon.stub(condaApi, 'getCondaPathSetting'); + setPyEnvBinaryStub = sinon.stub(pyenvApi, 'setPyEnvBinary'); + getWorkspaceFoldersStub = sinon.stub(ws, 'getWorkspaceFolders'); + getWorkspaceFoldersStub.returns([]); + + createPythonWatcherStub = sinon.stub(pw, 'createPythonWatcher'); + mockWatcher = typemoq.Mock.ofType<pw.PythonWatcher>(); + createPythonWatcherStub.returns(mockWatcher.object); + + mockWatcher.setup((w) => w.watchWorkspace(typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.watchPath(typemoq.It.isAny(), typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.unwatchWorkspace(typemoq.It.isAny())).returns(() => undefined); + mockWatcher.setup((w) => w.unwatchPath(typemoq.It.isAny())).returns(() => undefined); + + mockFinder = typemoq.Mock.ofType<NativePythonFinder>(); + api = nativeAPI.createNativeEnvironmentsApi(mockFinder.object); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Trigger refresh without resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Trigger refresh with resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv2]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(basicEnv)) + .verifiable(typemoq.Times.once()); + + api.triggerRefresh(); + await api.getRefreshPromise(); + + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Trigger refresh and use refresh promise API', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + api.triggerRefresh(); + await api.getRefreshPromise(); + + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedBasicEnv]); + }); + + test('Conda environment with resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda1]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(conda)) + .verifiable(typemoq.Times.once()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda1]); + }); + + test('Ensure no duplication on resolve', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda1]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder + .setup((f) => f.resolve(typemoq.It.isAny())) + .returns(() => Promise.resolve(conda)) + .verifiable(typemoq.Times.once()); + + await api.triggerRefresh(); + await api.resolveEnv('/home/user/.conda/envs/conda_python/python'); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda1]); + }); + + test('Conda environment with no python', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [conda2]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + const actual = api.getEnvs(); + assert.deepEqual(actual, [expectedConda2]); + }); + + test('Refresh promise undefined after refresh', async () => { + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [basicEnv]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + + mockFinder.setup((f) => f.resolve(typemoq.It.isAny())).verifiable(typemoq.Times.never()); + + await api.triggerRefresh(); + assert.isUndefined(api.getRefreshPromise()); + }); + + test('Setting conda binary', async () => { + getCondaPathSettingStub.returns(undefined); + getCondaEnvDirsStub.resolves(undefined); + const condaFakeDir = getPathEnvVariable()[0]; + const condaMgr: NativeEnvManagerInfo = { + tool: 'Conda', + executable: path.join(condaFakeDir, 'conda'), + }; + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [condaMgr]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + await api.triggerRefresh(); + assert.isTrue(setCondaBinaryStub.calledOnceWith(condaMgr.executable)); + }); + + test('Setting pyenv binary', async () => { + const pyenvMgr: NativeEnvManagerInfo = { + tool: 'PyEnv', + executable: '/usr/bin/pyenv', + }; + mockFinder + .setup((f) => f.refresh()) + .returns(() => { + async function* generator() { + yield* [pyenvMgr]; + } + return generator(); + }) + .verifiable(typemoq.Times.once()); + await api.triggerRefresh(); + assert.isTrue(setPyEnvBinaryStub.calledOnceWith(pyenvMgr.executable)); + }); +}); diff --git a/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts new file mode 100644 index 000000000000..b6182da8111f --- /dev/null +++ b/src/test/pythonEnvironments/nativePythonFinder.unit.test.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { WorkspaceConfiguration } from 'vscode'; +import { + getNativePythonFinder, + isNativeEnvInfo, + NativeEnvInfo, + NativePythonFinder, +} from '../../client/pythonEnvironments/base/locators/common/nativePythonFinder'; +import * as windowsApis from '../../client/common/vscodeApis/windowApis'; +import { MockOutputChannel } from '../mockClasses'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('Native Python Finder', () => { + let finder: NativePythonFinder; + let createLogOutputChannelStub: sinon.SinonStub; + let getConfigurationStub: sinon.SinonStub; + let configMock: typemoq.IMock<WorkspaceConfiguration>; + let getWorkspaceFolderPathsStub: sinon.SinonStub; + + setup(() => { + createLogOutputChannelStub = sinon.stub(windowsApis, 'createLogOutputChannel'); + createLogOutputChannelStub.returns(new MockOutputChannel('locator')); + + getWorkspaceFolderPathsStub = sinon.stub(workspaceApis, 'getWorkspaceFolderPaths'); + getWorkspaceFolderPathsStub.returns([]); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + configMock = typemoq.Mock.ofType<WorkspaceConfiguration>(); + configMock.setup((c) => c.get<string>('venvPath')).returns(() => undefined); + configMock.setup((c) => c.get<string[]>('venvFolders')).returns(() => []); + configMock.setup((c) => c.get<string>('condaPath')).returns(() => ''); + configMock.setup((c) => c.get<string>('poetryPath')).returns(() => ''); + getConfigurationStub.returns(configMock.object); + + finder = getNativePythonFinder(); + }); + + teardown(() => { + sinon.restore(); + }); + + suiteTeardown(() => { + finder.dispose(); + }); + + test('Refresh should return python environments', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + }); + + test('Resolve should return python environments with version', async () => { + const envs = []; + for await (const env of finder.refresh()) { + envs.push(env); + } + + // typically all test envs should have at least one environment + assert.isNotEmpty(envs); + + // pick and env without version + const env: NativeEnvInfo | undefined = envs + .filter((e) => isNativeEnvInfo(e)) + .find((e) => e.version && e.version.length > 0 && (e.executable || (e as NativeEnvInfo).prefix)); + + if (env) { + env.version = undefined; + } else { + assert.fail('Expected at least one env with valid version'); + } + + const envPath = env.executable ?? env.prefix; + if (envPath) { + const resolved = await finder.resolve(envPath); + assert.isString(resolved.version, 'Version must be a string'); + assert.isTrue((resolved?.version?.length ?? 0) > 0, 'Version must not be empty'); + } else { + assert.fail('Expected either executable or prefix to be defined'); + } + }); +}); diff --git a/src/test/pythonFiles/autocomp/deco.py b/src/test/pythonFiles/autocomp/deco.py deleted file mode 100644 index b843741ef647..000000000000 --- a/src/test/pythonFiles/autocomp/deco.py +++ /dev/null @@ -1,6 +0,0 @@ - -import abc -class Decorator(metaclass=abc.ABCMeta): - @abc.-# no abstract class - @abc.abstractclassmethod - \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/doc.py b/src/test/pythonFiles/autocomp/doc.py deleted file mode 100644 index a0d62874538f..000000000000 --- a/src/test/pythonFiles/autocomp/doc.py +++ /dev/null @@ -1,11 +0,0 @@ -import os - -if os.path.exists(("/etc/hosts")): - with open("/etc/hosts", "a") as f: - for line in f.readlines(): - content = line.upper() - - - -import time -time.slee \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/five.py b/src/test/pythonFiles/autocomp/five.py deleted file mode 100644 index 507c5fed967c..000000000000 --- a/src/test/pythonFiles/autocomp/five.py +++ /dev/null @@ -1,2 +0,0 @@ -import four -four.showMessage() diff --git a/src/test/pythonFiles/autocomp/four.py b/src/test/pythonFiles/autocomp/four.py deleted file mode 100644 index 470338f71157..000000000000 --- a/src/test/pythonFiles/autocomp/four.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=E0401, W0512 - -import os - - -class Foo(object): - '''说明''' - - @staticmethod - def bar(): - """ - 说明 - keep this line, it works - delete following line, it works - 如果存在需要等待审批或正在执行的任务,将不刷新页面 - """ - return os.path.exists('c:/') - -def showMessage(): - """ - Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. - Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку. - """ - print('1234') - -Foo.bar() -showMessage() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/hoverTest.py b/src/test/pythonFiles/autocomp/hoverTest.py deleted file mode 100644 index 0ff88d80dffc..000000000000 --- a/src/test/pythonFiles/autocomp/hoverTest.py +++ /dev/null @@ -1,16 +0,0 @@ -import random -import math - -for x in range(0, 10): - print(x) - -rnd = random.Random() -print(rnd.randint(0, 5)) -print(math.acos(90)) - -import misc -rnd2 = misc.Random() -rnd2.randint() - -t = misc.Thread() -t.__init__() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/imp.py b/src/test/pythonFiles/autocomp/imp.py deleted file mode 100644 index 0d0c98ed1cde..000000000000 --- a/src/test/pythonFiles/autocomp/imp.py +++ /dev/null @@ -1,2 +0,0 @@ -from os import * -fsta \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/lamb.py b/src/test/pythonFiles/autocomp/lamb.py deleted file mode 100644 index 05b92f5cd581..000000000000 --- a/src/test/pythonFiles/autocomp/lamb.py +++ /dev/null @@ -1,2 +0,0 @@ -instant_print = lambda x: [print(x), sys.stdout.flush(), sys.stderr.flush()] -instant_print("X"). \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/misc.py b/src/test/pythonFiles/autocomp/misc.py deleted file mode 100644 index 3d4a54cbc145..000000000000 --- a/src/test/pythonFiles/autocomp/misc.py +++ /dev/null @@ -1,1905 +0,0 @@ -"""Thread module emulating a subset of Java's threading model.""" - -import sys as _sys - -try: - import thread -except ImportError: - del _sys.modules[__name__] - raise - -import warnings - -from collections import deque as _deque -from itertools import count as _count -from time import time as _time, sleep as _sleep -from traceback import format_exc as _format_exc - -# Note regarding PEP 8 compliant aliases -# This threading model was originally inspired by Java, and inherited -# the convention of camelCase function and method names from that -# language. While those names are not in any imminent danger of being -# deprecated, starting with Python 2.6, the module now provides a -# PEP 8 compliant alias for any such method name. -# Using the new PEP 8 compliant names also facilitates substitution -# with the multiprocessing module, which doesn't provide the old -# Java inspired names. - - -# Rename some stuff so "from threading import *" is safe -__all__ = ['activeCount', 'active_count', 'Condition', 'currentThread', - 'current_thread', 'enumerate', 'Event', - 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Thread', - 'Timer', 'setprofile', 'settrace', 'local', 'stack_size'] - -_start_new_thread = thread.start_new_thread -_allocate_lock = thread.allocate_lock -_get_ident = thread.get_ident -ThreadError = thread.error -del thread - - -# sys.exc_clear is used to work around the fact that except blocks -# don't fully clear the exception until 3.0. -warnings.filterwarnings('ignore', category=DeprecationWarning, - module='threading', message='sys.exc_clear') - -# Debug support (adapted from ihooks.py). -# All the major classes here derive from _Verbose. We force that to -# be a new-style class so that all the major classes here are new-style. -# This helps debugging (type(instance) is more revealing for instances -# of new-style classes). - -_VERBOSE = False - -if __debug__: - - class _Verbose(object): - - def __init__(self, verbose=None): - if verbose is None: - verbose = _VERBOSE - self.__verbose = verbose - - def _note(self, format, *args): - if self.__verbose: - format = format % args - # Issue #4188: calling current_thread() can incur an infinite - # recursion if it has to create a DummyThread on the fly. - ident = _get_ident() - try: - name = _active[ident].name - except KeyError: - name = "<OS thread %d>" % ident - format = "%s: %s\n" % (name, format) - _sys.stderr.write(format) - -else: - # Disable this when using "python -O" - class _Verbose(object): - def __init__(self, verbose=None): - pass - def _note(self, *args): - pass - -# Support for profile and trace hooks - -_profile_hook = None -_trace_hook = None - -def setprofile(func): - """Set a profile function for all threads started from the threading module. - - The func will be passed to sys.setprofile() for each thread, before its - run() method is called. - - """ - global _profile_hook - _profile_hook = func - -def settrace(func): - """Set a trace function for all threads started from the threading module. - - The func will be passed to sys.settrace() for each thread, before its run() - method is called. - - """ - global _trace_hook - _trace_hook = func - -# Synchronization classes - -Lock = _allocate_lock - -def RLock(*args, **kwargs): - """Factory function that returns a new reentrant lock. - - A reentrant lock must be released by the thread that acquired it. Once a - thread has acquired a reentrant lock, the same thread may acquire it again - without blocking; the thread must release it once for each time it has - acquired it. - - """ - return _RLock(*args, **kwargs) - -class _RLock(_Verbose): - """A reentrant lock must be released by the thread that acquired it. Once a - thread has acquired a reentrant lock, the same thread may acquire it - again without blocking; the thread must release it once for each time it - has acquired it. - """ - - def __init__(self, verbose=None): - _Verbose.__init__(self, verbose) - self.__block = _allocate_lock() - self.__owner = None - self.__count = 0 - - def __repr__(self): - owner = self.__owner - try: - owner = _active[owner].name - except KeyError: - pass - return "<%s owner=%r count=%d>" % ( - self.__class__.__name__, owner, self.__count) - - def acquire(self, blocking=1): - """Acquire a lock, blocking or non-blocking. - - When invoked without arguments: if this thread already owns the lock, - increment the recursion level by one, and return immediately. Otherwise, - if another thread owns the lock, block until the lock is unlocked. Once - the lock is unlocked (not owned by any thread), then grab ownership, set - the recursion level to one, and return. If more than one thread is - blocked waiting until the lock is unlocked, only one at a time will be - able to grab ownership of the lock. There is no return value in this - case. - - When invoked with the blocking argument set to true, do the same thing - as when called without arguments, and return true. - - When invoked with the blocking argument set to false, do not block. If a - call without an argument would block, return false immediately; - otherwise, do the same thing as when called without arguments, and - return true. - - """ - me = _get_ident() - if self.__owner == me: - self.__count = self.__count + 1 - if __debug__: - self._note("%s.acquire(%s): recursive success", self, blocking) - return 1 - rc = self.__block.acquire(blocking) - if rc: - self.__owner = me - self.__count = 1 - if __debug__: - self._note("%s.acquire(%s): initial success", self, blocking) - else: - if __debug__: - self._note("%s.acquire(%s): failure", self, blocking) - return rc - - __enter__ = acquire - - def release(self): - """Release a lock, decrementing the recursion level. - - If after the decrement it is zero, reset the lock to unlocked (not owned - by any thread), and if any other threads are blocked waiting for the - lock to become unlocked, allow exactly one of them to proceed. If after - the decrement the recursion level is still nonzero, the lock remains - locked and owned by the calling thread. - - Only call this method when the calling thread owns the lock. A - RuntimeError is raised if this method is called when the lock is - unlocked. - - There is no return value. - - """ - if self.__owner != _get_ident(): - raise RuntimeError("cannot release un-acquired lock") - self.__count = count = self.__count - 1 - if not count: - self.__owner = None - self.__block.release() - if __debug__: - self._note("%s.release(): final release", self) - else: - if __debug__: - self._note("%s.release(): non-final release", self) - - def __exit__(self, t, v, tb): - self.release() - - # Internal methods used by condition variables - - def _acquire_restore(self, count_owner): - count, owner = count_owner - self.__block.acquire() - self.__count = count - self.__owner = owner - if __debug__: - self._note("%s._acquire_restore()", self) - - def _release_save(self): - if __debug__: - self._note("%s._release_save()", self) - count = self.__count - self.__count = 0 - owner = self.__owner - self.__owner = None - self.__block.release() - return (count, owner) - - def _is_owned(self): - return self.__owner == _get_ident() - - -def Condition(*args, **kwargs): - """Factory function that returns a new condition variable object. - - A condition variable allows one or more threads to wait until they are - notified by another thread. - - If the lock argument is given and not None, it must be a Lock or RLock - object, and it is used as the underlying lock. Otherwise, a new RLock object - is created and used as the underlying lock. - - """ - return _Condition(*args, **kwargs) - -class _Condition(_Verbose): - """Condition variables allow one or more threads to wait until they are - notified by another thread. - """ - - def __init__(self, lock=None, verbose=None): - _Verbose.__init__(self, verbose) - if lock is None: - lock = RLock() - self.__lock = lock - # Export the lock's acquire() and release() methods - self.acquire = lock.acquire - self.release = lock.release - # If the lock defines _release_save() and/or _acquire_restore(), - # these override the default implementations (which just call - # release() and acquire() on the lock). Ditto for _is_owned(). - try: - self._release_save = lock._release_save - except AttributeError: - pass - try: - self._acquire_restore = lock._acquire_restore - except AttributeError: - pass - try: - self._is_owned = lock._is_owned - except AttributeError: - pass - self.__waiters = [] - - def __enter__(self): - return self.__lock.__enter__() - - def __exit__(self, *args): - return self.__lock.__exit__(*args) - - def __repr__(self): - return "<Condition(%s, %d)>" % (self.__lock, len(self.__waiters)) - - def _release_save(self): - self.__lock.release() # No state to save - - def _acquire_restore(self, x): - self.__lock.acquire() # Ignore saved state - - def _is_owned(self): - # Return True if lock is owned by current_thread. - # This method is called only if __lock doesn't have _is_owned(). - if self.__lock.acquire(0): - self.__lock.release() - return False - else: - return True - - def wait(self, timeout=None): - """Wait until notified or until a timeout occurs. - - If the calling thread has not acquired the lock when this method is - called, a RuntimeError is raised. - - This method releases the underlying lock, and then blocks until it is - awakened by a notify() or notifyAll() call for the same condition - variable in another thread, or until the optional timeout occurs. Once - awakened or timed out, it re-acquires the lock and returns. - - When the timeout argument is present and not None, it should be a - floating point number specifying a timeout for the operation in seconds - (or fractions thereof). - - When the underlying lock is an RLock, it is not released using its - release() method, since this may not actually unlock the lock when it - was acquired multiple times recursively. Instead, an internal interface - of the RLock class is used, which really unlocks it even when it has - been recursively acquired several times. Another internal interface is - then used to restore the recursion level when the lock is reacquired. - - """ - if not self._is_owned(): - raise RuntimeError("cannot wait on un-acquired lock") - waiter = _allocate_lock() - waiter.acquire() - self.__waiters.append(waiter) - saved_state = self._release_save() - try: # restore state no matter what (e.g., KeyboardInterrupt) - if timeout is None: - waiter.acquire() - if __debug__: - self._note("%s.wait(): got it", self) - else: - # Balancing act: We can't afford a pure busy loop, so we - # have to sleep; but if we sleep the whole timeout time, - # we'll be unresponsive. The scheme here sleeps very - # little at first, longer as time goes on, but never longer - # than 20 times per second (or the timeout time remaining). - endtime = _time() + timeout - delay = 0.0005 # 500 us -> initial delay of 1 ms - while True: - gotit = waiter.acquire(0) - if gotit: - break - remaining = endtime - _time() - if remaining <= 0: - break - delay = min(delay * 2, remaining, .05) - _sleep(delay) - if not gotit: - if __debug__: - self._note("%s.wait(%s): timed out", self, timeout) - try: - self.__waiters.remove(waiter) - except ValueError: - pass - else: - if __debug__: - self._note("%s.wait(%s): got it", self, timeout) - finally: - self._acquire_restore(saved_state) - - def notify(self, n=1): - """Wake up one or more threads waiting on this condition, if any. - - If the calling thread has not acquired the lock when this method is - called, a RuntimeError is raised. - - This method wakes up at most n of the threads waiting for the condition - variable; it is a no-op if no threads are waiting. - - """ - if not self._is_owned(): - raise RuntimeError("cannot notify on un-acquired lock") - __waiters = self.__waiters - waiters = __waiters[:n] - if not waiters: - if __debug__: - self._note("%s.notify(): no waiters", self) - return - self._note("%s.notify(): notifying %d waiter%s", self, n, - n!=1 and "s" or "") - for waiter in waiters: - waiter.release() - try: - __waiters.remove(waiter) - except ValueError: - pass - - def notifyAll(self): - """Wake up all threads waiting on this condition. - - If the calling thread has not acquired the lock when this method - is called, a RuntimeError is raised. - - """ - self.notify(len(self.__waiters)) - - notify_all = notifyAll - - -def Semaphore(*args, **kwargs): - """A factory function that returns a new semaphore. - - Semaphores manage a counter representing the number of release() calls minus - the number of acquire() calls, plus an initial value. The acquire() method - blocks if necessary until it can return without making the counter - negative. If not given, value defaults to 1. - - """ - return _Semaphore(*args, **kwargs) - -class _Semaphore(_Verbose): - """Semaphores manage a counter representing the number of release() calls - minus the number of acquire() calls, plus an initial value. The acquire() - method blocks if necessary until it can return without making the counter - negative. If not given, value defaults to 1. - - """ - - # After Tim Peters' semaphore class, but not quite the same (no maximum) - - def __init__(self, value=1, verbose=None): - if value < 0: - raise ValueError("semaphore initial value must be >= 0") - _Verbose.__init__(self, verbose) - self.__cond = Condition(Lock()) - self.__value = value - - def acquire(self, blocking=1): - """Acquire a semaphore, decrementing the internal counter by one. - - When invoked without arguments: if the internal counter is larger than - zero on entry, decrement it by one and return immediately. If it is zero - on entry, block, waiting until some other thread has called release() to - make it larger than zero. This is done with proper interlocking so that - if multiple acquire() calls are blocked, release() will wake exactly one - of them up. The implementation may pick one at random, so the order in - which blocked threads are awakened should not be relied on. There is no - return value in this case. - - When invoked with blocking set to true, do the same thing as when called - without arguments, and return true. - - When invoked with blocking set to false, do not block. If a call without - an argument would block, return false immediately; otherwise, do the - same thing as when called without arguments, and return true. - - """ - rc = False - with self.__cond: - while self.__value == 0: - if not blocking: - break - if __debug__: - self._note("%s.acquire(%s): blocked waiting, value=%s", - self, blocking, self.__value) - self.__cond.wait() - else: - self.__value = self.__value - 1 - if __debug__: - self._note("%s.acquire: success, value=%s", - self, self.__value) - rc = True - return rc - - __enter__ = acquire - - def release(self): - """Release a semaphore, incrementing the internal counter by one. - - When the counter is zero on entry and another thread is waiting for it - to become larger than zero again, wake up that thread. - - """ - with self.__cond: - self.__value = self.__value + 1 - if __debug__: - self._note("%s.release: success, value=%s", - self, self.__value) - self.__cond.notify() - - def __exit__(self, t, v, tb): - self.release() - - -def BoundedSemaphore(*args, **kwargs): - """A factory function that returns a new bounded semaphore. - - A bounded semaphore checks to make sure its current value doesn't exceed its - initial value. If it does, ValueError is raised. In most situations - semaphores are used to guard resources with limited capacity. - - If the semaphore is released too many times it's a sign of a bug. If not - given, value defaults to 1. - - Like regular semaphores, bounded semaphores manage a counter representing - the number of release() calls minus the number of acquire() calls, plus an - initial value. The acquire() method blocks if necessary until it can return - without making the counter negative. If not given, value defaults to 1. - - """ - return _BoundedSemaphore(*args, **kwargs) - -class _BoundedSemaphore(_Semaphore): - """A bounded semaphore checks to make sure its current value doesn't exceed - its initial value. If it does, ValueError is raised. In most situations - semaphores are used to guard resources with limited capacity. - """ - - def __init__(self, value=1, verbose=None): - _Semaphore.__init__(self, value, verbose) - self._initial_value = value - - def release(self): - """Release a semaphore, incrementing the internal counter by one. - - When the counter is zero on entry and another thread is waiting for it - to become larger than zero again, wake up that thread. - - If the number of releases exceeds the number of acquires, - raise a ValueError. - - """ - with self._Semaphore__cond: - if self._Semaphore__value >= self._initial_value: - raise ValueError("Semaphore released too many times") - self._Semaphore__value += 1 - self._Semaphore__cond.notify() - - -def Event(*args, **kwargs): - """A factory function that returns a new event. - - Events manage a flag that can be set to true with the set() method and reset - to false with the clear() method. The wait() method blocks until the flag is - true. - - """ - return _Event(*args, **kwargs) - -class _Event(_Verbose): - """A factory function that returns a new event object. An event manages a - flag that can be set to true with the set() method and reset to false - with the clear() method. The wait() method blocks until the flag is true. - - """ - - # After Tim Peters' event class (without is_posted()) - - def __init__(self, verbose=None): - _Verbose.__init__(self, verbose) - self.__cond = Condition(Lock()) - self.__flag = False - - def _reset_internal_locks(self): - # private! called by Thread._reset_internal_locks by _after_fork() - self.__cond.__init__() - - def isSet(self): - 'Return true if and only if the internal flag is true.' - return self.__flag - - is_set = isSet - - def set(self): - """Set the internal flag to true. - - All threads waiting for the flag to become true are awakened. Threads - that call wait() once the flag is true will not block at all. - - """ - self.__cond.acquire() - try: - self.__flag = True - self.__cond.notify_all() - finally: - self.__cond.release() - - def clear(self): - """Reset the internal flag to false. - - Subsequently, threads calling wait() will block until set() is called to - set the internal flag to true again. - - """ - self.__cond.acquire() - try: - self.__flag = False - finally: - self.__cond.release() - - def wait(self, timeout=None): - """Block until the internal flag is true. - - If the internal flag is true on entry, return immediately. Otherwise, - block until another thread calls set() to set the flag to true, or until - the optional timeout occurs. - - When the timeout argument is present and not None, it should be a - floating point number specifying a timeout for the operation in seconds - (or fractions thereof). - - This method returns the internal flag on exit, so it will always return - True except if a timeout is given and the operation times out. - - """ - self.__cond.acquire() - try: - if not self.__flag: - self.__cond.wait(timeout) - return self.__flag - finally: - self.__cond.release() - -# Helper to generate new thread names -_counter = _count().next -_counter() # Consume 0 so first non-main thread has id 1. -def _newname(template="Thread-%d"): - return template % _counter() - -# Active thread administration -_active_limbo_lock = _allocate_lock() -_active = {} # maps thread id to Thread object -_limbo = {} - - -# Main class for threads - -class Thread(_Verbose): - """A class that represents a thread of control. - - This class can be safely subclassed in a limited fashion. - - """ - __initialized = False - # Need to store a reference to sys.exc_info for printing - # out exceptions when a thread tries to use a global var. during interp. - # shutdown and thus raises an exception about trying to perform some - # operation on/with a NoneType - __exc_info = _sys.exc_info - # Keep sys.exc_clear too to clear the exception just before - # allowing .join() to return. - __exc_clear = _sys.exc_clear - - def __init__(self, group=None, target=None, name=None, - args=(), kwargs=None, verbose=None): - """This constructor should always be called with keyword arguments. Arguments are: - - *group* should be None; reserved for future extension when a ThreadGroup - class is implemented. - - *target* is the callable object to be invoked by the run() - method. Defaults to None, meaning nothing is called. - - *name* is the thread name. By default, a unique name is constructed of - the form "Thread-N" where N is a small decimal number. - - *args* is the argument tuple for the target invocation. Defaults to (). - - *kwargs* is a dictionary of keyword arguments for the target - invocation. Defaults to {}. - - If a subclass overrides the constructor, it must make sure to invoke - the base class constructor (Thread.__init__()) before doing anything - else to the thread. - -""" - assert group is None, "group argument must be None for now" - _Verbose.__init__(self, verbose) - if kwargs is None: - kwargs = {} - self.__target = target - self.__name = str(name or _newname()) - self.__args = args - self.__kwargs = kwargs - self.__daemonic = self._set_daemon() - self.__ident = None - self.__started = Event() - self.__stopped = False - self.__block = Condition(Lock()) - self.__initialized = True - # sys.stderr is not stored in the class like - # sys.exc_info since it can be changed between instances - self.__stderr = _sys.stderr - - def _reset_internal_locks(self): - # private! Called by _after_fork() to reset our internal locks as - # they may be in an invalid state leading to a deadlock or crash. - if hasattr(self, '_Thread__block'): # DummyThread deletes self.__block - self.__block.__init__() - self.__started._reset_internal_locks() - - @property - def _block(self): - # used by a unittest - return self.__block - - def _set_daemon(self): - # Overridden in _MainThread and _DummyThread - return current_thread().daemon - - def __repr__(self): - assert self.__initialized, "Thread.__init__() was not called" - status = "initial" - if self.__started.is_set(): - status = "started" - if self.__stopped: - status = "stopped" - if self.__daemonic: - status += " daemon" - if self.__ident is not None: - status += " %s" % self.__ident - return "<%s(%s, %s)>" % (self.__class__.__name__, self.__name, status) - - def start(self): - """Start the thread's activity. - - It must be called at most once per thread object. It arranges for the - object's run() method to be invoked in a separate thread of control. - - This method will raise a RuntimeError if called more than once on the - same thread object. - - """ - if not self.__initialized: - raise RuntimeError("thread.__init__() not called") - if self.__started.is_set(): - raise RuntimeError("threads can only be started once") - if __debug__: - self._note("%s.start(): starting thread", self) - with _active_limbo_lock: - _limbo[self] = self - try: - _start_new_thread(self.__bootstrap, ()) - except Exception: - with _active_limbo_lock: - del _limbo[self] - raise - self.__started.wait() - - def run(self): - """Method representing the thread's activity. - - You may override this method in a subclass. The standard run() method - invokes the callable object passed to the object's constructor as the - target argument, if any, with sequential and keyword arguments taken - from the args and kwargs arguments, respectively. - - """ - try: - if self.__target: - self.__target(*self.__args, **self.__kwargs) - finally: - # Avoid a refcycle if the thread is running a function with - # an argument that has a member that points to the thread. - del self.__target, self.__args, self.__kwargs - - def __bootstrap(self): - # Wrapper around the real bootstrap code that ignores - # exceptions during interpreter cleanup. Those typically - # happen when a daemon thread wakes up at an unfortunate - # moment, finds the world around it destroyed, and raises some - # random exception *** while trying to report the exception in - # __bootstrap_inner() below ***. Those random exceptions - # don't help anybody, and they confuse users, so we suppress - # them. We suppress them only when it appears that the world - # indeed has already been destroyed, so that exceptions in - # __bootstrap_inner() during normal business hours are properly - # reported. Also, we only suppress them for daemonic threads; - # if a non-daemonic encounters this, something else is wrong. - try: - self.__bootstrap_inner() - except: - if self.__daemonic and _sys is None: - return - raise - - def _set_ident(self): - self.__ident = _get_ident() - - def __bootstrap_inner(self): - try: - self._set_ident() - self.__started.set() - with _active_limbo_lock: - _active[self.__ident] = self - del _limbo[self] - if __debug__: - self._note("%s.__bootstrap(): thread started", self) - - if _trace_hook: - self._note("%s.__bootstrap(): registering trace hook", self) - _sys.settrace(_trace_hook) - if _profile_hook: - self._note("%s.__bootstrap(): registering profile hook", self) - _sys.setprofile(_profile_hook) - - try: - self.run() - except SystemExit: - if __debug__: - self._note("%s.__bootstrap(): raised SystemExit", self) - except: - if __debug__: - self._note("%s.__bootstrap(): unhandled exception", self) - # If sys.stderr is no more (most likely from interpreter - # shutdown) use self.__stderr. Otherwise still use sys (as in - # _sys) in case sys.stderr was redefined since the creation of - # self. - if _sys and _sys.stderr is not None: - print>>_sys.stderr, ("Exception in thread %s:\n%s" % - (self.name, _format_exc())) - elif self.__stderr is not None: - # Do the best job possible w/o a huge amt. of code to - # approximate a traceback (code ideas from - # Lib/traceback.py) - exc_type, exc_value, exc_tb = self.__exc_info() - try: - print>>self.__stderr, ( - "Exception in thread " + self.name + - " (most likely raised during interpreter shutdown):") - print>>self.__stderr, ( - "Traceback (most recent call last):") - while exc_tb: - print>>self.__stderr, ( - ' File "%s", line %s, in %s' % - (exc_tb.tb_frame.f_code.co_filename, - exc_tb.tb_lineno, - exc_tb.tb_frame.f_code.co_name)) - exc_tb = exc_tb.tb_next - print>>self.__stderr, ("%s: %s" % (exc_type, exc_value)) - # Make sure that exc_tb gets deleted since it is a memory - # hog; deleting everything else is just for thoroughness - finally: - del exc_type, exc_value, exc_tb - else: - if __debug__: - self._note("%s.__bootstrap(): normal return", self) - finally: - # Prevent a race in - # test_threading.test_no_refcycle_through_target when - # the exception keeps the target alive past when we - # assert that it's dead. - self.__exc_clear() - finally: - with _active_limbo_lock: - self.__stop() - try: - # We don't call self.__delete() because it also - # grabs _active_limbo_lock. - del _active[_get_ident()] - except: - pass - - def __stop(self): - # DummyThreads delete self.__block, but they have no waiters to - # notify anyway (join() is forbidden on them). - if not hasattr(self, '_Thread__block'): - return - self.__block.acquire() - self.__stopped = True - self.__block.notify_all() - self.__block.release() - - def __delete(self): - "Remove current thread from the dict of currently running threads." - - # Notes about running with dummy_thread: - # - # Must take care to not raise an exception if dummy_thread is being - # used (and thus this module is being used as an instance of - # dummy_threading). dummy_thread.get_ident() always returns -1 since - # there is only one thread if dummy_thread is being used. Thus - # len(_active) is always <= 1 here, and any Thread instance created - # overwrites the (if any) thread currently registered in _active. - # - # An instance of _MainThread is always created by 'threading'. This - # gets overwritten the instant an instance of Thread is created; both - # threads return -1 from dummy_thread.get_ident() and thus have the - # same key in the dict. So when the _MainThread instance created by - # 'threading' tries to clean itself up when atexit calls this method - # it gets a KeyError if another Thread instance was created. - # - # This all means that KeyError from trying to delete something from - # _active if dummy_threading is being used is a red herring. But - # since it isn't if dummy_threading is *not* being used then don't - # hide the exception. - - try: - with _active_limbo_lock: - del _active[_get_ident()] - # There must not be any python code between the previous line - # and after the lock is released. Otherwise a tracing function - # could try to acquire the lock again in the same thread, (in - # current_thread()), and would block. - except KeyError: - if 'dummy_threading' not in _sys.modules: - raise - - def join(self, timeout=None): - """Wait until the thread terminates. - - This blocks the calling thread until the thread whose join() method is - called terminates -- either normally or through an unhandled exception - or until the optional timeout occurs. - - When the timeout argument is present and not None, it should be a - floating point number specifying a timeout for the operation in seconds - (or fractions thereof). As join() always returns None, you must call - isAlive() after join() to decide whether a timeout happened -- if the - thread is still alive, the join() call timed out. - - When the timeout argument is not present or None, the operation will - block until the thread terminates. - - A thread can be join()ed many times. - - join() raises a RuntimeError if an attempt is made to join the current - thread as that would cause a deadlock. It is also an error to join() a - thread before it has been started and attempts to do so raises the same - exception. - - """ - if not self.__initialized: - raise RuntimeError("Thread.__init__() not called") - if not self.__started.is_set(): - raise RuntimeError("cannot join thread before it is started") - if self is current_thread(): - raise RuntimeError("cannot join current thread") - - if __debug__: - if not self.__stopped: - self._note("%s.join(): waiting until thread stops", self) - self.__block.acquire() - try: - if timeout is None: - while not self.__stopped: - self.__block.wait() - if __debug__: - self._note("%s.join(): thread stopped", self) - else: - deadline = _time() + timeout - while not self.__stopped: - delay = deadline - _time() - if delay <= 0: - if __debug__: - self._note("%s.join(): timed out", self) - break - self.__block.wait(delay) - else: - if __debug__: - self._note("%s.join(): thread stopped", self) - finally: - self.__block.release() - - @property - def name(self): - """A string used for identification purposes only. - - It has no semantics. Multiple threads may be given the same name. The - initial name is set by the constructor. - - """ - assert self.__initialized, "Thread.__init__() not called" - return self.__name - - @name.setter - def name(self, name): - assert self.__initialized, "Thread.__init__() not called" - self.__name = str(name) - - @property - def ident(self): - """Thread identifier of this thread or None if it has not been started. - - This is a nonzero integer. See the thread.get_ident() function. Thread - identifiers may be recycled when a thread exits and another thread is - created. The identifier is available even after the thread has exited. - - """ - assert self.__initialized, "Thread.__init__() not called" - return self.__ident - - def isAlive(self): - """Return whether the thread is alive. - - This method returns True just before the run() method starts until just - after the run() method terminates. The module function enumerate() - returns a list of all alive threads. - - """ - assert self.__initialized, "Thread.__init__() not called" - return self.__started.is_set() and not self.__stopped - - is_alive = isAlive - - @property - def daemon(self): - """A boolean value indicating whether this thread is a daemon thread (True) or not (False). - - This must be set before start() is called, otherwise RuntimeError is - raised. Its initial value is inherited from the creating thread; the - main thread is not a daemon thread and therefore all threads created in - the main thread default to daemon = False. - - The entire Python program exits when no alive non-daemon threads are - left. - - """ - assert self.__initialized, "Thread.__init__() not called" - return self.__daemonic - - @daemon.setter - def daemon(self, daemonic): - if not self.__initialized: - raise RuntimeError("Thread.__init__() not called") - if self.__started.is_set(): - raise RuntimeError("cannot set daemon status of active thread"); - self.__daemonic = daemonic - - def isDaemon(self): - return self.daemon - - def setDaemon(self, daemonic): - self.daemon = daemonic - - def getName(self): - return self.name - - def setName(self, name): - self.name = name - -# The timer class was contributed by Itamar Shtull-Trauring - -def Timer(*args, **kwargs): - """Factory function to create a Timer object. - - Timers call a function after a specified number of seconds: - - t = Timer(30.0, f, args=[], kwargs={}) - t.start() - t.cancel() # stop the timer's action if it's still waiting - - """ - return _Timer(*args, **kwargs) - -class _Timer(Thread): - """Call a function after a specified number of seconds: - - t = Timer(30.0, f, args=[], kwargs={}) - t.start() - t.cancel() # stop the timer's action if it's still waiting - - """ - - def __init__(self, interval, function, args=[], kwargs={}): - Thread.__init__(self) - self.interval = interval - self.function = function - self.args = args - self.kwargs = kwargs - self.finished = Event() - - def cancel(self): - """Stop the timer if it hasn't finished yet""" - self.finished.set() - - def run(self): - self.finished.wait(self.interval) - if not self.finished.is_set(): - self.function(*self.args, **self.kwargs) - self.finished.set() - -# Special thread class to represent the main thread -# This is garbage collected through an exit handler - -class _MainThread(Thread): - - def __init__(self): - Thread.__init__(self, name="MainThread") - self._Thread__started.set() - self._set_ident() - with _active_limbo_lock: - _active[_get_ident()] = self - - def _set_daemon(self): - return False - - def _exitfunc(self): - self._Thread__stop() - t = _pickSomeNonDaemonThread() - if t: - if __debug__: - self._note("%s: waiting for other threads", self) - while t: - t.join() - t = _pickSomeNonDaemonThread() - if __debug__: - self._note("%s: exiting", self) - self._Thread__delete() - -def _pickSomeNonDaemonThread(): - for t in enumerate(): - if not t.daemon and t.is_alive(): - return t - return None - - -# Dummy thread class to represent threads not started here. -# These aren't garbage collected when they die, nor can they be waited for. -# If they invoke anything in threading.py that calls current_thread(), they -# leave an entry in the _active dict forever after. -# Their purpose is to return *something* from current_thread(). -# They are marked as daemon threads so we won't wait for them -# when we exit (conform previous semantics). - -class _DummyThread(Thread): - - def __init__(self): - Thread.__init__(self, name=_newname("Dummy-%d")) - - # Thread.__block consumes an OS-level locking primitive, which - # can never be used by a _DummyThread. Since a _DummyThread - # instance is immortal, that's bad, so release this resource. - del self._Thread__block - - self._Thread__started.set() - self._set_ident() - with _active_limbo_lock: - _active[_get_ident()] = self - - def _set_daemon(self): - return True - - def join(self, timeout=None): - assert False, "cannot join a dummy thread" - - -# Global API functions - -def currentThread(): - """Return the current Thread object, corresponding to the caller's thread of control. - - If the caller's thread of control was not created through the threading - module, a dummy thread object with limited functionality is returned. - - """ - try: - return _active[_get_ident()] - except KeyError: - ##print "current_thread(): no current thread for", _get_ident() - return _DummyThread() - -current_thread = currentThread - -def activeCount(): - """Return the number of Thread objects currently alive. - - The returned count is equal to the length of the list returned by - enumerate(). - - """ - with _active_limbo_lock: - return len(_active) + len(_limbo) - -active_count = activeCount - -def _enumerate(): - # Same as enumerate(), but without the lock. Internal use only. - return _active.values() + _limbo.values() - -def enumerate(): - """Return a list of all Thread objects currently alive. - - The list includes daemonic threads, dummy thread objects created by - current_thread(), and the main thread. It excludes terminated threads and - threads that have not yet been started. - - """ - with _active_limbo_lock: - return _active.values() + _limbo.values() - -from thread import stack_size - -# Create the main thread object, -# and make it available for the interpreter -# (Py_Main) as threading._shutdown. - -_shutdown = _MainThread()._exitfunc - -# get thread-local implementation, either from the thread -# module, or from the python fallback - -try: - from thread import _local as local -except ImportError: - from _threading_local import local - - -def _after_fork(): - # This function is called by Python/ceval.c:PyEval_ReInitThreads which - # is called from PyOS_AfterFork. Here we cleanup threading module state - # that should not exist after a fork. - - # Reset _active_limbo_lock, in case we forked while the lock was held - # by another (non-forked) thread. http://bugs.python.org/issue874900 - global _active_limbo_lock - _active_limbo_lock = _allocate_lock() - - # fork() only copied the current thread; clear references to others. - new_active = {} - current = current_thread() - with _active_limbo_lock: - for thread in _enumerate(): - # Any lock/condition variable may be currently locked or in an - # invalid state, so we reinitialize them. - if hasattr(thread, '_reset_internal_locks'): - thread._reset_internal_locks() - if thread is current: - # There is only one active thread. We reset the ident to - # its new value since it can have changed. - ident = _get_ident() - thread._Thread__ident = ident - new_active[ident] = thread - else: - # All the others are already stopped. - thread._Thread__stop() - - _limbo.clear() - _active.clear() - _active.update(new_active) - assert len(_active) == 1 - - -# Self-test code - -def _test(): - - class BoundedQueue(_Verbose): - - def __init__(self, limit): - _Verbose.__init__(self) - self.mon = RLock() - self.rc = Condition(self.mon) - self.wc = Condition(self.mon) - self.limit = limit - self.queue = _deque() - - def put(self, item): - self.mon.acquire() - while len(self.queue) >= self.limit: - self._note("put(%s): queue full", item) - self.wc.wait() - self.queue.append(item) - self._note("put(%s): appended, length now %d", - item, len(self.queue)) - self.rc.notify() - self.mon.release() - - def get(self): - self.mon.acquire() - while not self.queue: - self._note("get(): queue empty") - self.rc.wait() - item = self.queue.popleft() - self._note("get(): got %s, %d left", item, len(self.queue)) - self.wc.notify() - self.mon.release() - return item - - class ProducerThread(Thread): - - def __init__(self, queue, quota): - Thread.__init__(self, name="Producer") - self.queue = queue - self.quota = quota - - def run(self): - from random import random - counter = 0 - while counter < self.quota: - counter = counter + 1 - self.queue.put("%s.%d" % (self.name, counter)) - _sleep(random() * 0.00001) - - - class ConsumerThread(Thread): - - def __init__(self, queue, count): - Thread.__init__(self, name="Consumer") - self.queue = queue - self.count = count - - def run(self): - while self.count > 0: - item = self.queue.get() - print item - self.count = self.count - 1 - - NP = 3 - QL = 4 - NI = 5 - - Q = BoundedQueue(QL) - P = [] - for i in range(NP): - t = ProducerThread(Q, NI) - t.name = ("Producer-%d" % (i+1)) - P.append(t) - C = ConsumerThread(Q, NI*NP) - for t in P: - t.start() - _sleep(0.000001) - C.start() - for t in P: - t.join() - C.join() - - -class Random(_random.Random): - """Random number generator base class used by bound module functions. - - Used to instantiate instances of Random to get generators that don't - share state. - - Class Random can also be subclassed if you want to use a different basic - generator of your own devising: in that case, override the following - methods: random(), seed(), getstate(), and setstate(). - Optionally, implement a getrandbits() method so that randrange() - can cover arbitrarily large ranges. - - """ - - VERSION = 3 # used by getstate/setstate - - def __init__(self, x=None): - """Initialize an instance. - - Optional argument x controls seeding, as for Random.seed(). - """ - - self.seed(x) - self.gauss_next = None - - def seed(self, a=None, version=2): - """Initialize internal state from hashable object. - - None or no argument seeds from current time or from an operating - system specific randomness source if available. - - For version 2 (the default), all of the bits are used if *a* is a str, - bytes, or bytearray. For version 1, the hash() of *a* is used instead. - - If *a* is an int, all bits are used. - - """ - - if a is None: - try: - # Seed with enough bytes to span the 19937 bit - # state space for the Mersenne Twister - a = int.from_bytes(_urandom(2500), 'big') - except NotImplementedError: - import time - a = int(time.time() * 256) # use fractional seconds - - if version == 2: - if isinstance(a, (str, bytes, bytearray)): - if isinstance(a, str): - a = a.encode() - a += _sha512(a).digest() - a = int.from_bytes(a, 'big') - - super().seed(a) - self.gauss_next = None - - def getstate(self): - """Return internal state; can be passed to setstate() later.""" - return self.VERSION, super().getstate(), self.gauss_next - - def setstate(self, state): - """Restore internal state from object returned by getstate().""" - version = state[0] - if version == 3: - version, internalstate, self.gauss_next = state - super().setstate(internalstate) - elif version == 2: - version, internalstate, self.gauss_next = state - # In version 2, the state was saved as signed ints, which causes - # inconsistencies between 32/64-bit systems. The state is - # really unsigned 32-bit ints, so we convert negative ints from - # version 2 to positive longs for version 3. - try: - internalstate = tuple(x % (2**32) for x in internalstate) - except ValueError as e: - raise TypeError from e - super().setstate(internalstate) - else: - raise ValueError("state with version %s passed to " - "Random.setstate() of version %s" % - (version, self.VERSION)) - -## ---- Methods below this point do not need to be overridden when -## ---- subclassing for the purpose of using a different core generator. - -## -------------------- pickle support ------------------- - - # Issue 17489: Since __reduce__ was defined to fix #759889 this is no - # longer called; we leave it here because it has been here since random was - # rewritten back in 2001 and why risk breaking something. - def __getstate__(self): # for pickle - return self.getstate() - - def __setstate__(self, state): # for pickle - self.setstate(state) - - def __reduce__(self): - return self.__class__, (), self.getstate() - -## -------------------- integer methods ------------------- - - def randrange(self, start, stop=None, step=1, _int=int): - """Choose a random item from range(start, stop[, step]). - - This fixes the problem with randint() which includes the - endpoint; in Python this is usually not what you want. - - """ - - # This code is a bit messy to make it fast for the - # common case while still doing adequate error checking. - istart = _int(start) - if istart != start: - raise ValueError("non-integer arg 1 for randrange()") - if stop is None: - if istart > 0: - return self._randbelow(istart) - raise ValueError("empty range for randrange()") - - # stop argument supplied. - istop = _int(stop) - if istop != stop: - raise ValueError("non-integer stop for randrange()") - width = istop - istart - if step == 1 and width > 0: - return istart + self._randbelow(width) - if step == 1: - raise ValueError("empty range for randrange() (%d,%d, %d)" % (istart, istop, width)) - - # Non-unit step argument supplied. - istep = _int(step) - if istep != step: - raise ValueError("non-integer step for randrange()") - if istep > 0: - n = (width + istep - 1) // istep - elif istep < 0: - n = (width + istep + 1) // istep - else: - raise ValueError("zero step for randrange()") - - if n <= 0: - raise ValueError("empty range for randrange()") - - return istart + istep*self._randbelow(n) - - def randint(self, a, b): - """Return random integer in range [a, b], including both end points. - """ - - return self.randrange(a, b+1) - - def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type, - Method=_MethodType, BuiltinMethod=_BuiltinMethodType): - "Return a random int in the range [0,n). Raises ValueError if n==0." - - random = self.random - getrandbits = self.getrandbits - # Only call self.getrandbits if the original random() builtin method - # has not been overridden or if a new getrandbits() was supplied. - if type(random) is BuiltinMethod or type(getrandbits) is Method: - k = n.bit_length() # don't use (n-1) here because n can be 1 - r = getrandbits(k) # 0 <= r < 2**k - while r >= n: - r = getrandbits(k) - return r - # There's an overridden random() method but no new getrandbits() method, - # so we can only use random() from here. - if n >= maxsize: - _warn("Underlying random() generator does not supply \n" - "enough bits to choose from a population range this large.\n" - "To remove the range limitation, add a getrandbits() method.") - return int(random() * n) - rem = maxsize % n - limit = (maxsize - rem) / maxsize # int(limit * maxsize) % n == 0 - r = random() - while r >= limit: - r = random() - return int(r*maxsize) % n - -## -------------------- sequence methods ------------------- - - def choice(self, seq): - """Choose a random element from a non-empty sequence.""" - try: - i = self._randbelow(len(seq)) - except ValueError: - raise IndexError('Cannot choose from an empty sequence') - return seq[i] - - def shuffle(self, x, random=None): - """Shuffle list x in place, and return None. - - Optional argument random is a 0-argument function returning a - random float in [0.0, 1.0); if it is the default None, the - standard random.random will be used. - - """ - - if random is None: - randbelow = self._randbelow - for i in reversed(range(1, len(x))): - # pick an element in x[:i+1] with which to exchange x[i] - j = randbelow(i+1) - x[i], x[j] = x[j], x[i] - else: - _int = int - for i in reversed(range(1, len(x))): - # pick an element in x[:i+1] with which to exchange x[i] - j = _int(random() * (i+1)) - x[i], x[j] = x[j], x[i] - - def sample(self, population, k): - """Chooses k unique random elements from a population sequence or set. - - Returns a new list containing elements from the population while - leaving the original population unchanged. The resulting list is - in selection order so that all sub-slices will also be valid random - samples. This allows raffle winners (the sample) to be partitioned - into grand prize and second place winners (the subslices). - - Members of the population need not be hashable or unique. If the - population contains repeats, then each occurrence is a possible - selection in the sample. - - To choose a sample in a range of integers, use range as an argument. - This is especially fast and space efficient for sampling from a - large population: sample(range(10000000), 60) - """ - - # Sampling without replacement entails tracking either potential - # selections (the pool) in a list or previous selections in a set. - - # When the number of selections is small compared to the - # population, then tracking selections is efficient, requiring - # only a small set and an occasional reselection. For - # a larger number of selections, the pool tracking method is - # preferred since the list takes less space than the - # set and it doesn't suffer from frequent reselections. - - if isinstance(population, _Set): - population = tuple(population) - if not isinstance(population, _Sequence): - raise TypeError("Population must be a sequence or set. For dicts, use list(d).") - randbelow = self._randbelow - n = len(population) - if not 0 <= k <= n: - raise ValueError("Sample larger than population") - result = [None] * k - setsize = 21 # size of a small set minus size of an empty list - if k > 5: - setsize += 4 ** _ceil(_log(k * 3, 4)) # table size for big sets - if n <= setsize: - # An n-length list is smaller than a k-length set - pool = list(population) - for i in range(k): # invariant: non-selected at [0,n-i) - j = randbelow(n-i) - result[i] = pool[j] - pool[j] = pool[n-i-1] # move non-selected item into vacancy - else: - selected = set() - selected_add = selected.add - for i in range(k): - j = randbelow(n) - while j in selected: - j = randbelow(n) - selected_add(j) - result[i] = population[j] - return result - -## -------------------- real-valued distributions ------------------- - -## -------------------- uniform distribution ------------------- - - def uniform(self, a, b): - "Get a random number in the range [a, b) or [a, b] depending on rounding." - return a + (b-a) * self.random() - -## -------------------- triangular -------------------- - - def triangular(self, low=0.0, high=1.0, mode=None): - """Triangular distribution. - - Continuous distribution bounded by given lower and upper limits, - and having a given mode value in-between. - - http://en.wikipedia.org/wiki/Triangular_distribution - - """ - u = self.random() - try: - c = 0.5 if mode is None else (mode - low) / (high - low) - except ZeroDivisionError: - return low - if u > c: - u = 1.0 - u - c = 1.0 - c - low, high = high, low - return low + (high - low) * (u * c) ** 0.5 - -## -------------------- normal distribution -------------------- - - def normalvariate(self, mu, sigma): - """Normal distribution. - - mu is the mean, and sigma is the standard deviation. - - """ - # mu = mean, sigma = standard deviation - - # Uses Kinderman and Monahan method. Reference: Kinderman, - # A.J. and Monahan, J.F., "Computer generation of random - # variables using the ratio of uniform deviates", ACM Trans - # Math Software, 3, (1977), pp257-260. - - random = self.random - while 1: - u1 = random() - u2 = 1.0 - random() - z = NV_MAGICCONST*(u1-0.5)/u2 - zz = z*z/4.0 - if zz <= -_log(u2): - break - return mu + z*sigma - -## -------------------- lognormal distribution -------------------- - - def lognormvariate(self, mu, sigma): - """Log normal distribution. - - If you take the natural logarithm of this distribution, you'll get a - normal distribution with mean mu and standard deviation sigma. - mu can have any value, and sigma must be greater than zero. - - """ - return _exp(self.normalvariate(mu, sigma)) - -## -------------------- exponential distribution -------------------- - - def expovariate(self, lambd): - """Exponential distribution. - - lambd is 1.0 divided by the desired mean. It should be - nonzero. (The parameter would be called "lambda", but that is - a reserved word in Python.) Returned values range from 0 to - positive infinity if lambd is positive, and from negative - infinity to 0 if lambd is negative. - - """ - # lambd: rate lambd = 1/mean - # ('lambda' is a Python reserved word) - - # we use 1-random() instead of random() to preclude the - # possibility of taking the log of zero. - return -_log(1.0 - self.random())/lambd - -## -------------------- von Mises distribution -------------------- - - def vonmisesvariate(self, mu, kappa): - """Circular data distribution. - - mu is the mean angle, expressed in radians between 0 and 2*pi, and - kappa is the concentration parameter, which must be greater than or - equal to zero. If kappa is equal to zero, this distribution reduces - to a uniform random angle over the range 0 to 2*pi. - - """ - # mu: mean angle (in radians between 0 and 2*pi) - # kappa: concentration parameter kappa (>= 0) - # if kappa = 0 generate uniform random angle - - # Based upon an algorithm published in: Fisher, N.I., - # "Statistical Analysis of Circular Data", Cambridge - # University Press, 1993. - - # Thanks to Magnus Kessler for a correction to the - # implementation of step 4. - - random = self.random - if kappa <= 1e-6: - return TWOPI * random() - - s = 0.5 / kappa - r = s + _sqrt(1.0 + s * s) - - while 1: - u1 = random() - z = _cos(_pi * u1) - - d = z / (r + z) - u2 = random() - if u2 < 1.0 - d * d or u2 <= (1.0 - d) * _exp(d): - break - - q = 1.0 / r - f = (q + z) / (1.0 + q * z) - u3 = random() - if u3 > 0.5: - theta = (mu + _acos(f)) % TWOPI - else: - theta = (mu - _acos(f)) % TWOPI - - return theta - -## -------------------- gamma distribution -------------------- - - def gammavariate(self, alpha, beta): - """Gamma distribution. Not the gamma function! - - Conditions on the parameters are alpha > 0 and beta > 0. - - The probability distribution function is: - - x ** (alpha - 1) * math.exp(-x / beta) - pdf(x) = -------------------------------------- - math.gamma(alpha) * beta ** alpha - - """ - - # alpha > 0, beta > 0, mean is alpha*beta, variance is alpha*beta**2 - - # Warning: a few older sources define the gamma distribution in terms - # of alpha > -1.0 - if alpha <= 0.0 or beta <= 0.0: - raise ValueError('gammavariate: alpha and beta must be > 0.0') - - random = self.random - if alpha > 1.0: - - # Uses R.C.H. Cheng, "The generation of Gamma - # variables with non-integral shape parameters", - # Applied Statistics, (1977), 26, No. 1, p71-74 - - ainv = _sqrt(2.0 * alpha - 1.0) - bbb = alpha - LOG4 - ccc = alpha + ainv - - while 1: - u1 = random() - if not 1e-7 < u1 < .9999999: - continue - u2 = 1.0 - random() - v = _log(u1/(1.0-u1))/ainv - x = alpha*_exp(v) - z = u1*u1*u2 - r = bbb+ccc*v-x - if r + SG_MAGICCONST - 4.5*z >= 0.0 or r >= _log(z): - return x * beta - - elif alpha == 1.0: - # expovariate(1) - u = random() - while u <= 1e-7: - u = random() - return -_log(u) * beta - - else: # alpha is between 0 and 1 (exclusive) - - # Uses ALGORITHM GS of Statistical Computing - Kennedy & Gentle - - while 1: - u = random() - b = (_e + alpha)/_e - p = b*u - if p <= 1.0: - x = p ** (1.0/alpha) - else: - x = -_log((b-p)/alpha) - u1 = random() - if p > 1.0: - if u1 <= x ** (alpha - 1.0): - break - elif u1 <= _exp(-x): - break - return x * beta - -## -------------------- Gauss (faster alternative) -------------------- - - def gauss(self, mu, sigma): - """Gaussian distribution. - - mu is the mean, and sigma is the standard deviation. This is - slightly faster than the normalvariate() function. - - Not thread-safe without a lock around calls. - - """ - - # When x and y are two variables from [0, 1), uniformly - # distributed, then - # - # cos(2*pi*x)*sqrt(-2*log(1-y)) - # sin(2*pi*x)*sqrt(-2*log(1-y)) - # - # are two *independent* variables with normal distribution - # (mu = 0, sigma = 1). - # (Lambert Meertens) - # (corrected version; bug discovered by Mike Miller, fixed by LM) - - # Multithreading note: When two threads call this function - # simultaneously, it is possible that they will receive the - # same return value. The window is very small though. To - # avoid this, you have to use a lock around all calls. (I - # didn't want to slow this down in the serial case by using a - # lock here.) - - random = self.random - z = self.gauss_next - self.gauss_next = None - if z is None: - x2pi = random() * TWOPI - g2rad = _sqrt(-2.0 * _log(1.0 - random())) - z = _cos(x2pi) * g2rad - self.gauss_next = _sin(x2pi) * g2rad - - return mu + z*sigma - -## -------------------- beta -------------------- -## See -## http://mail.python.org/pipermail/python-bugs-list/2001-January/003752.html -## for Ivan Frohne's insightful analysis of why the original implementation: -## -## def betavariate(self, alpha, beta): -## # Discrete Event Simulation in C, pp 87-88. -## -## y = self.expovariate(alpha) -## z = self.expovariate(1.0/beta) -## return z/(y+z) -## -## was dead wrong, and how it probably got that way. - - def betavariate(self, alpha, beta): - """Beta distribution. - - Conditions on the parameters are alpha > 0 and beta > 0. - Returned values range between 0 and 1. - - """ - - # This version due to Janne Sinkkonen, and matches all the std - # texts (e.g., Knuth Vol 2 Ed 3 pg 134 "the beta distribution"). - y = self.gammavariate(alpha, 1.) - if y == 0: - return 0.0 - else: - return y / (y + self.gammavariate(beta, 1.)) - -## -------------------- Pareto -------------------- - - def paretovariate(self, alpha): - """Pareto distribution. alpha is the shape parameter.""" - # Jain, pg. 495 - - u = 1.0 - self.random() - return 1.0 / u ** (1.0/alpha) - -## -------------------- Weibull -------------------- - - def weibullvariate(self, alpha, beta): - """Weibull distribution. - - alpha is the scale parameter and beta is the shape parameter. - - """ - # Jain, pg. 499; bug fix courtesy Bill Arms - - u = 1.0 - self.random() - return alpha * (-_log(u)) ** (1.0/beta) - -## --------------- Operating System Random Source ------------------ - - -if __name__ == '__main__': - _test() - diff --git a/src/test/pythonFiles/autocomp/one.py b/src/test/pythonFiles/autocomp/one.py deleted file mode 100644 index 5e5708fd92f0..000000000000 --- a/src/test/pythonFiles/autocomp/one.py +++ /dev/null @@ -1,31 +0,0 @@ - -import sys - -print(sys.api_version) - -class Class1(object): - """Some class - And the second line - """ - - description = "Run isort on modules registered in setuptools" - user_options = [] - - def __init__(self, file_path=None, file_contents=None): - self.prop1 = '' - self.prop2 = 1 - - def method1(self): - """ - This is method1 - """ - pass - - def method2(self): - """ - This is method2 - """ - pass - -obj = Class1() -obj.method1() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/pep484.py b/src/test/pythonFiles/autocomp/pep484.py deleted file mode 100644 index 79edec69ae1a..000000000000 --- a/src/test/pythonFiles/autocomp/pep484.py +++ /dev/null @@ -1,12 +0,0 @@ - -def greeting(name: str) -> str: - return 'Hello ' + name.upper() - - -def add(num1, num2) -> int: - return num1 + num2 - -add().bit_length() - - - diff --git a/src/test/pythonFiles/autocomp/pep526.py b/src/test/pythonFiles/autocomp/pep526.py deleted file mode 100644 index d8cd0300ed0d..000000000000 --- a/src/test/pythonFiles/autocomp/pep526.py +++ /dev/null @@ -1,22 +0,0 @@ - - -PEP_526_style: str = "hello world" -captain: str # Note: no initial value! -PEP_484_style = SOMETHING # type: str - - -PEP_484_style.upper() -PEP_526_style.upper() -captain.upper() - -# https://github.com/DonJayamanne/pythonVSCode/issues/918 -class A: - a = 0 - - -class B: - b: int = 0 - - -A().a # -> Autocomplete works -B().b.bit_length() # -> Autocomplete doesn't work \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/suppress.py b/src/test/pythonFiles/autocomp/suppress.py deleted file mode 100644 index 9f74959ef14b..000000000000 --- a/src/test/pythonFiles/autocomp/suppress.py +++ /dev/null @@ -1,6 +0,0 @@ -"string" #comment -""" -content -""" -#comment -'un#closed diff --git a/src/test/pythonFiles/autocomp/three.py b/src/test/pythonFiles/autocomp/three.py deleted file mode 100644 index 35ad7f399172..000000000000 --- a/src/test/pythonFiles/autocomp/three.py +++ /dev/null @@ -1,2 +0,0 @@ -import two -two.ct().fun() \ No newline at end of file diff --git a/src/test/pythonFiles/autocomp/two.py b/src/test/pythonFiles/autocomp/two.py deleted file mode 100644 index 99a6e3c4bdf1..000000000000 --- a/src/test/pythonFiles/autocomp/two.py +++ /dev/null @@ -1,6 +0,0 @@ -class ct: - def fun(): - """ - This is fun - """ - pass \ No newline at end of file diff --git a/src/test/pythonFiles/definition/await.test.py b/src/test/pythonFiles/definition/await.test.py deleted file mode 100644 index 7b4acd876c27..000000000000 --- a/src/test/pythonFiles/definition/await.test.py +++ /dev/null @@ -1,19 +0,0 @@ -# https://github.com/DonJayamanne/pythonVSCode/issues/962 - -class A: - def __init__(self): - self.test_value = 0 - - async def test(self): - pass - - async def test2(self): - await self.test() - -async def testthis(): - """ - Wow - """ - pass - -await testthis() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/five.py b/src/test/pythonFiles/definition/five.py deleted file mode 100644 index 507c5fed967c..000000000000 --- a/src/test/pythonFiles/definition/five.py +++ /dev/null @@ -1,2 +0,0 @@ -import four -four.showMessage() diff --git a/src/test/pythonFiles/definition/four.py b/src/test/pythonFiles/definition/four.py deleted file mode 100644 index 470338f71157..000000000000 --- a/src/test/pythonFiles/definition/four.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# pylint: disable=E0401, W0512 - -import os - - -class Foo(object): - '''说明''' - - @staticmethod - def bar(): - """ - 说明 - keep this line, it works - delete following line, it works - 如果存在需要等待审批或正在执行的任务,将不刷新页面 - """ - return os.path.exists('c:/') - -def showMessage(): - """ - Кюм ут жэмпэр пошжим льаборэж, коммюны янтэрэсщэт нам ед, декта игнота ныморэ жят эи. - Шэа декам экшырки эи, эи зыд эррэм докэндё, векж факэтэ пэрчыквюэрёж ку. - """ - print('1234') - -Foo.bar() -showMessage() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/navigation/definitions.py b/src/test/pythonFiles/definition/navigation/definitions.py deleted file mode 100644 index a8379a49f960..000000000000 --- a/src/test/pythonFiles/definition/navigation/definitions.py +++ /dev/null @@ -1,31 +0,0 @@ -from contextlib import contextmanager - -def my_decorator(fn): - """ - This is my decorator. - """ - def wrapper(*args, **kwargs): - """ - This is the wrapper. - """ - return 42 - return wrapper - -@my_decorator -def thing(arg): - """ - Thing which is decorated. - """ - pass - -@contextmanager -def my_context_manager(): - """ - This is my context manager. - """ - print("before") - yield - print("after") - -with my_context_manager(): - thing(19) diff --git a/src/test/pythonFiles/definition/navigation/usages.py b/src/test/pythonFiles/definition/navigation/usages.py deleted file mode 100644 index deb6d78edc15..000000000000 --- a/src/test/pythonFiles/definition/navigation/usages.py +++ /dev/null @@ -1,16 +0,0 @@ -import definitions -from .definitions import my_context_manager, my_decorator, thing - -@definitions.my_decorator -def one(): - pass - -@my_decorator -def two(): - pass - -with definitions.my_context_manager(): - definitions.thing(19) - -with my_context_manager(): - thing(19) diff --git a/src/test/pythonFiles/definition/one.py b/src/test/pythonFiles/definition/one.py deleted file mode 100644 index f1e3d75ffcbc..000000000000 --- a/src/test/pythonFiles/definition/one.py +++ /dev/null @@ -1,46 +0,0 @@ - -import sys - -print(sys.api_version) - -class Class1(object): - """Some class - And the second line - """ - - description = "Run isort on modules registered in setuptools" - user_options = [] - - def __init__(self, file_path=None, file_contents=None): - self.prop1 = '' - self.prop2 = 1 - - def method1(self): - """ - This is method1 - """ - pass - - def method2(self): - """ - This is method2 - """ - pass - -obj = Class1() -obj.method1() - -def function1(): - print("SOMETHING") - - -def function2(): - print("SOMETHING") - -def function3(): - print("SOMETHING") - -def function4(): - print("SOMETHING") - -function1() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/three.py b/src/test/pythonFiles/definition/three.py deleted file mode 100644 index 35ad7f399172..000000000000 --- a/src/test/pythonFiles/definition/three.py +++ /dev/null @@ -1,2 +0,0 @@ -import two -two.ct().fun() \ No newline at end of file diff --git a/src/test/pythonFiles/definition/two.py b/src/test/pythonFiles/definition/two.py deleted file mode 100644 index 99a6e3c4bdf1..000000000000 --- a/src/test/pythonFiles/definition/two.py +++ /dev/null @@ -1,6 +0,0 @@ -class ct: - def fun(): - """ - This is fun - """ - pass \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/Lib/fileLib.py b/src/test/pythonFiles/exclusions/Lib/fileLib.py deleted file mode 100644 index 50000adeda40..000000000000 --- a/src/test/pythonFiles/exclusions/Lib/fileLib.py +++ /dev/null @@ -1 +0,0 @@ - a \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py b/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py deleted file mode 100644 index dad1af98c7f5..000000000000 --- a/src/test/pythonFiles/exclusions/Lib/site-packages/sitePackages.py +++ /dev/null @@ -1 +0,0 @@ - b \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/dir1/dir1file.py b/src/test/pythonFiles/exclusions/dir1/dir1file.py deleted file mode 100644 index fe453b3fcc6a..000000000000 --- a/src/test/pythonFiles/exclusions/dir1/dir1file.py +++ /dev/null @@ -1 +0,0 @@ - for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py b/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py deleted file mode 100644 index fe453b3fcc6a..000000000000 --- a/src/test/pythonFiles/exclusions/dir1/dir2/dir2file.py +++ /dev/null @@ -1 +0,0 @@ - for \ No newline at end of file diff --git a/src/test/pythonFiles/exclusions/one.py b/src/test/pythonFiles/exclusions/one.py deleted file mode 100644 index 8c68a1c1fee2..000000000000 --- a/src/test/pythonFiles/exclusions/one.py +++ /dev/null @@ -1 +0,0 @@ - if \ No newline at end of file diff --git a/src/test/pythonFiles/folding/attach_server.py b/src/test/pythonFiles/folding/attach_server.py deleted file mode 100644 index 9c331d6c49e1..000000000000 --- a/src/test/pythonFiles/folding/attach_server.py +++ /dev/null @@ -1,337 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] - -import atexit -import getpass -import os -import os.path -import platform -import socket -import struct -import sys -import threading -try: - import thread -except ImportError: - import _thread as thread -try: - import ssl -except ImportError: - ssl = None - -import ptvsd.visualstudio_py_debugger as vspd -import ptvsd.visualstudio_py_repl as vspr -from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string - - -# The server (i.e. the Python app) waits on a TCP port provided. Whenever anything connects to that port, -# it immediately sends the octet sequence 'PTVSDBG', followed by version number represented as int64, -# and then waits for the client to respond with the same exact byte sequence. After signatures are thereby -# exchanged and found to match, the client is expected to provide a string secret (in the usual debugger -# string format, None/ACII/Unicode prefix + length + data), which can be an empty string to designate the -# lack of a specified secret. -# -# If the secret does not match the one expected by the server, it responds with 'RJCT', and then closes -# the connection. Otherwise, the server responds with 'ACPT', and awaits a 4-octet command. The following -# commands are recognized: -# -# 'INFO' -# Report information about the process. The server responds with the following information, in order: -# - Process ID (int64) -# - Executable name (string) -# - User name (string) -# - Implementation name (string) -# and then immediately closes connection. Note, all string fields can be empty or null strings. -# -# 'ATCH' -# Attach debugger to the process. If successful, the server responds with 'ACPT', followed by process ID -# (int64), and then the Python language version that the server is running represented by three int64s - -# major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. -# If attaching was not successful (which can happen if some other debugger is already attached), the server -# responds with 'RJCT' and closes the connection. -# -# 'REPL' -# Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket -# is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is -# no debugger attached), the server responds with 'RJCT' and closes the connection. - -PTVS_VER = '2.2' -DEFAULT_PORT = 5678 -PTVSDBG_VER = 6 # must be kept in sync with DebuggerProtocolVersion in PythonRemoteProcess.cs -PTVSDBG = to_bytes('PTVSDBG') -ACPT = to_bytes('ACPT') -RJCT = to_bytes('RJCT') -INFO = to_bytes('INFO') -ATCH = to_bytes('ATCH') -REPL = to_bytes('REPL') - -PY_ROOT = os.path.normcase(__file__) -while os.path.basename(PY_ROOT) != 'pythonFiles': - PY_ROOT = os.path.dirname(PY_ROOT) - -_attach_enabled = False -_attached = threading.Event() - - -class AttachAlreadyEnabledError(Exception): - """`ptvsd.enable_attach` has already been called in this process.""" - - -def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): - """Enables Python Tools for Visual Studio to attach to this process remotely - to debug Python code. - - Parameters - ---------- - secret : str - Used to validate the clients - only those clients providing the valid - secret will be allowed to connect to this server. On client side, the - secret is prepended to the Qualifier string, separated from the - hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If - secret is ``None``, there's no validation, and any client can connect - freely. - address : (str, int), optional - Specifies the interface and port on which the debugging server should - listen for TCP connections. It is in the same format as used for - regular sockets of the `socket.AF_INET` family, i.e. a tuple of - ``(hostname, port)``. On client side, the server is identified by the - Qualifier string in the usual ``'hostname:port'`` format, e.g.: - ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. - certfile : str, optional - Used to enable SSL. If not specified, or if set to ``None``, the - connection between this program and the debugger will be unsecure, - and can be intercepted on the wire. If specified, the meaning of this - parameter is the same as for `ssl.wrap_socket`. - keyfile : str, optional - Used together with `certfile` when SSL is enabled. Its meaning is the - same as for ``ssl.wrap_socket``. - redirect_output : bool, optional - Specifies whether any output (on both `stdout` and `stderr`) produced - by this program should be sent to the debugger. Default is ``True``. - - Notes - ----- - This function returns immediately after setting up the debugging server, - and does not block program execution. If you need to block until debugger - is attached, call `ptvsd.wait_for_attach`. The debugger can be detached - and re-attached multiple times after `enable_attach` is called. - - This function can only be called once during the lifetime of the process. - On a second call, `AttachAlreadyEnabledError` is raised. In circumstances - where the caller does not control how many times the function will be - called (e.g. when a script with a single call is run more than once by - a hosting app or framework), the call should be wrapped in ``try..except``. - - Only the thread on which this function is called, and any threads that are - created after it returns, will be visible in the debugger once it is - attached. Any threads that are already running before this function is - called will not be visible. - """ - - if not ssl and (certfile or keyfile): - raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') - - if sys.platform == 'cli': - # Check that IronPython was launched with -X:Frames and -X:Tracing, since we can't register our trace - # func on the thread that calls enable_attach otherwise - import clr - x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing - x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames - if not x_tracing or not x_frames: - raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') - - global _attach_enabled - if _attach_enabled: - raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') - _attach_enabled = True - - atexit.register(vspd.detach_process_and_notify_debugger) - - server = socket.socket(proto=socket.IPPROTO_TCP) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(address) - server.listen(1) - def server_thread_func(): - while True: - client = None - raw_client = None - try: - client, addr = server.accept() - if certfile: - client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) - write_bytes(client, PTVSDBG) - write_int(client, PTVSDBG_VER) - - response = read_bytes(client, 7) - if response != PTVSDBG: - continue - dbg_ver = read_int(client) - if dbg_ver != PTVSDBG_VER: - continue - - client_secret = read_string(client) - if secret is None or secret == client_secret: - write_bytes(client, ACPT) - else: - write_bytes(client, RJCT) - continue - - response = read_bytes(client, 4) - - if response == INFO: - try: - pid = os.getpid() - except AttributeError: - pid = 0 - write_int(client, pid) - - exe = sys.executable or '' - write_string(client, exe) - - try: - username = getpass.getuser() - except AttributeError: - username = '' - write_string(client, username) - - try: - impl = platform.python_implementation() - except AttributeError: - try: - impl = sys.implementation.name - except AttributeError: - impl = 'Python' - - major, minor, micro, release_level, serial = sys.version_info - - os_and_arch = platform.system() - if os_and_arch == "": - os_and_arch = sys.platform - try: - if sys.maxsize > 2**32: - os_and_arch += ' 64-bit' - else: - os_and_arch += ' 32-bit' - except AttributeError: - pass - - version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) - write_string(client, version) - - # Don't just drop the connection - let the debugger close it after it finishes reading. - client.recv(1) - - elif response == ATCH: - debug_options = vspd.parse_debug_options(read_string(client)) - debug_options.setdefault('rules', []).append({ - 'path': PY_ROOT, - 'include': False, - }) - if redirect_output: - debug_options.add('RedirectOutput') - - if vspd.DETACHED: - write_bytes(client, ACPT) - try: - pid = os.getpid() - except AttributeError: - pid = 0 - write_int(client, pid) - - major, minor, micro, release_level, serial = sys.version_info - write_int(client, major) - write_int(client, minor) - write_int(client, micro) - - vspd.attach_process_from_socket(client, debug_options, report = True) - vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) - _attached.set() - client = None - else: - write_bytes(client, RJCT) - - elif response == REPL: - if not vspd.DETACHED: - write_bytes(client, ACPT) - vspd.connect_repl_using_socket(client) - client = None - else: - write_bytes(client, RJCT) - - except (socket.error, OSError): - pass - finally: - if client is not None: - client.close() - - server_thread = threading.Thread(target = server_thread_func) - server_thread.setDaemon(True) - server_thread.start() - - frames = [] - f = sys._getframe() - while True: - f = f.f_back - if f is None: - break - frames.append(f) - frames.reverse() - cur_thread = vspd.new_thread() - for f in frames: - cur_thread.push_frame(f) - def replace_trace_func(): - for f in frames: - f.f_trace = cur_thread.trace_func - replace_trace_func() - sys.settrace(cur_thread.trace_func) - vspd.intercept_threads(for_attach = True) - - -# Alias for convenience of users of pydevd -settrace = enable_attach - - -def wait_for_attach(timeout = None): - """If a PTVS remote debugger is attached, returns immediately. Otherwise, - blocks until a remote debugger attaches to this process, or until the - optional timeout occurs. - - Parameters - ---------- - timeout : float, optional - The timeout for the operation in seconds (or fractions thereof). - """ - if vspd.DETACHED: - _attached.clear() - _attached.wait(timeout) - - -def break_into_debugger(): - """If a PTVS remote debugger is attached, pauses execution of all threads, - and breaks into the debugger with current thread as active. - """ - if not vspd.DETACHED: - vspd.SEND_BREAK_COMPLETE = thread.get_ident() - vspd.mark_all_threads_for_break() - -def is_attached(): - """Returns ``True`` if debugger is attached, ``False`` otherwise.""" - return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/miscSamples.py b/src/test/pythonFiles/folding/miscSamples.py deleted file mode 100644 index 01495fb0ee9c..000000000000 --- a/src/test/pythonFiles/folding/miscSamples.py +++ /dev/null @@ -1,40 +0,0 @@ - -def one(): - """comment""" - pass - -def two(): - value = """a doc string with single and double quotes "This is how it's done" """ - pass - -def three(): - """a doc string with single and double quotes "This is how it's done" - Another line - """ - pass - -def four(): - '''a doc string with single and double quotes "This is how it's done" ''' - pass - -def five(): - '''a doc string with single and double quotes "This is how it's done" - Another line - ''' - pass - -def six(): - """ s1 """ """ s2 """ - pass - -def seven(): - value = """ s1 """ """ s2 """ - pass - -def eight(): - ''' s1 ''' ''' s2 ''' - pass - -def nine(): - value = ''' s1 ''' ''' s2 ''' - pass diff --git a/src/test/pythonFiles/folding/noComments.py b/src/test/pythonFiles/folding/noComments.py deleted file mode 100644 index 4f0f7c5ec235..000000000000 --- a/src/test/pythonFiles/folding/noComments.py +++ /dev/null @@ -1,285 +0,0 @@ -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] - -import atexit -import getpass -import os -import os.path -import platform -import socket -import struct -import sys -import threading -try: - import thread -except ImportError: - import _thread as thread -try: - import ssl -except ImportError: - ssl = None - -import ptvsd.visualstudio_py_debugger as vspd -import ptvsd.visualstudio_py_repl as vspr -from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string - -PTVS_VER = '2.2' -DEFAULT_PORT = 5678 -PTVSDBG_VER = 6 -PTVSDBG = to_bytes('PTVSDBG') -ACPT = to_bytes('ACPT') -RJCT = to_bytes('RJCT') -INFO = to_bytes('INFO') -ATCH = to_bytes('ATCH') -REPL = to_bytes('REPL') - -PY_ROOT = os.path.normcase(__file__) -while os.path.basename(PY_ROOT) != 'pythonFiles': - PY_ROOT = os.path.dirname(PY_ROOT) - -_attach_enabled = False -_attached = threading.Event() - - -class AttachAlreadyEnabledError(Exception): - """`ptvsd.enable_attach` has already been called in this process.""" - - -def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): - """Enables Python Tools for Visual Studio to attach to this process remotely - to debug Python code. - - Parameters - ---------- - secret : str - Used to validate the clients - only those clients providing the valid - secret will be allowed to connect to this server. On client side, the - secret is prepended to the Qualifier string, separated from the - hostname by ``'@'``, e.g.: ``'secret@myhost.cloudapp.net:5678'``. If - secret is ``None``, there's no validation, and any client can connect - freely. - address : (str, int), optional - Specifies the interface and port on which the debugging server should - listen for TCP connections. It is in the same format as used for - regular sockets of the `socket.AF_INET` family, i.e. a tuple of - ``(hostname, port)``. On client side, the server is identified by the - Qualifier string in the usual ``'hostname:port'`` format, e.g.: - ``'myhost.cloudapp.net:5678'``. Default is ``('0.0.0.0', 5678)``. - certfile : str, optional - Used to enable SSL. If not specified, or if set to ``None``, the - connection between this program and the debugger will be unsecure, - and can be intercepted on the wire. If specified, the meaning of this - parameter is the same as for `ssl.wrap_socket`. - keyfile : str, optional - Used together with `certfile` when SSL is enabled. Its meaning is the - same as for ``ssl.wrap_socket``. - redirect_output : bool, optional - Specifies whether any output (on both `stdout` and `stderr`) produced - by this program should be sent to the debugger. Default is ``True``. - - Notes - ----- - This function returns immediately after setting up the debugging server, - and does not block program execution. If you need to block until debugger - is attached, call `ptvsd.wait_for_attach`. The debugger can be detached - and re-attached multiple times after `enable_attach` is called. - - This function can only be called once during the lifetime of the process. - On a second call, `AttachAlreadyEnabledError` is raised. In circumstances - where the caller does not control how many times the function will be - called (e.g. when a script with a single call is run more than once by - a hosting app or framework), the call should be wrapped in ``try..except``. - - Only the thread on which this function is called, and any threads that are - created after it returns, will be visible in the debugger once it is - attached. Any threads that are already running before this function is - called will not be visible. - """ - - if not ssl and (certfile or keyfile): - raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') - - if sys.platform == 'cli': - import clr - x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing - x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames - if not x_tracing or not x_frames: - raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') - - global _attach_enabled - if _attach_enabled: - raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') - _attach_enabled = True - - atexit.register(vspd.detach_process_and_notify_debugger) - - server = socket.socket(proto=socket.IPPROTO_TCP) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(address) - server.listen(1) - def server_thread_func(): - while True: - client = None - raw_client = None - try: - client, addr = server.accept() - if certfile: - client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) - write_bytes(client, PTVSDBG) - write_int(client, PTVSDBG_VER) - - response = read_bytes(client, 7) - if response != PTVSDBG: - continue - dbg_ver = read_int(client) - if dbg_ver != PTVSDBG_VER: - continue - - client_secret = read_string(client) - if secret is None or secret == client_secret: - write_bytes(client, ACPT) - else: - write_bytes(client, RJCT) - continue - - response = read_bytes(client, 4) - - if response == INFO: - try: - pid = os.getpid() - except AttributeError: - pid = 0 - write_int(client, pid) - - exe = sys.executable or '' - write_string(client, exe) - - try: - username = getpass.getuser() - except AttributeError: - username = '' - write_string(client, username) - - try: - impl = platform.python_implementation() - except AttributeError: - try: - impl = sys.implementation.name - except AttributeError: - impl = 'Python' - - major, minor, micro, release_level, serial = sys.version_info - - os_and_arch = platform.system() - if os_and_arch == "": - os_and_arch = sys.platform - try: - if sys.maxsize > 2**32: - os_and_arch += ' 64-bit' - else: - os_and_arch += ' 32-bit' - except AttributeError: - pass - - version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) - write_string(client, version) - - client.recv(1) - - elif response == ATCH: - debug_options = vspd.parse_debug_options(read_string(client)) - debug_options.setdefault('rules', []).append({ - 'path': PY_ROOT, - 'include': False, - }) - if redirect_output: - debug_options.add('RedirectOutput') - - if vspd.DETACHED: - write_bytes(client, ACPT) - try: - pid = os.getpid() - except AttributeError: - pid = 0 - write_int(client, pid) - - major, minor, micro, release_level, serial = sys.version_info - write_int(client, major) - write_int(client, minor) - write_int(client, micro) - - vspd.attach_process_from_socket(client, debug_options, report = True) - vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) - _attached.set() - client = None - else: - write_bytes(client, RJCT) - - elif response == REPL: - if not vspd.DETACHED: - write_bytes(client, ACPT) - vspd.connect_repl_using_socket(client) - client = None - else: - write_bytes(client, RJCT) - - except (socket.error, OSError): - pass - finally: - if client is not None: - client.close() - - server_thread = threading.Thread(target = server_thread_func) - server_thread.setDaemon(True) - server_thread.start() - - frames = [] - f = sys._getframe() - while True: - f = f.f_back - if f is None: - break - frames.append(f) - frames.reverse() - cur_thread = vspd.new_thread() - for f in frames: - cur_thread.push_frame(f) - def replace_trace_func(): - for f in frames: - f.f_trace = cur_thread.trace_func - replace_trace_func() - sys.settrace(cur_thread.trace_func) - vspd.intercept_threads(for_attach = True) - - -settrace = enable_attach - - -def wait_for_attach(timeout = None): - """If a PTVS remote debugger is attached, returns immediately. Otherwise, - blocks until a remote debugger attaches to this process, or until the - optional timeout occurs. - - Parameters - ---------- - timeout : float, optional - The timeout for the operation in seconds (or fractions thereof). - """ - if vspd.DETACHED: - _attached.clear() - _attached.wait(timeout) - - -def break_into_debugger(): - """If a PTVS remote debugger is attached, pauses execution of all threads, - and breaks into the debugger with current thread as active. - """ - if not vspd.DETACHED: - vspd.SEND_BREAK_COMPLETE = thread.get_ident() - vspd.mark_all_threads_for_break() - -def is_attached(): - """Returns ``True`` if debugger is attached, ``False`` otherwise.""" - return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/noDocStrings.py b/src/test/pythonFiles/folding/noDocStrings.py deleted file mode 100644 index f5750dbfde78..000000000000 --- a/src/test/pythonFiles/folding/noDocStrings.py +++ /dev/null @@ -1,273 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -__all__ = ['enable_attach', 'wait_for_attach', 'break_into_debugger', 'settrace', 'is_attached', 'AttachAlreadyEnabledError'] - -import atexit -import getpass -import os -import os.path -import platform -import socket -import struct -import sys -import threading -try: - import thread -except ImportError: - import _thread as thread -try: - import ssl -except ImportError: - ssl = None - -import ptvsd.visualstudio_py_debugger as vspd -import ptvsd.visualstudio_py_repl as vspr -from ptvsd.visualstudio_py_util import to_bytes, read_bytes, read_int, read_string, write_bytes, write_int, write_string - - -# The server (i.e. the Python app) waits on a TCP port provided. Whenever anything connects to that port, -# it immediately sends the octet sequence 'PTVSDBG', followed by version number represented as int64, -# and then waits for the client to respond with the same exact byte sequence. After signatures are thereby -# exchanged and found to match, the client is expected to provide a string secret (in the usual debugger -# string format, None/ACII/Unicode prefix + length + data), which can be an empty string to designate the -# lack of a specified secret. -# -# If the secret does not match the one expected by the server, it responds with 'RJCT', and then closes -# the connection. Otherwise, the server responds with 'ACPT', and awaits a 4-octet command. The following -# commands are recognized: -# -# 'INFO' -# Report information about the process. The server responds with the following information, in order: -# - Process ID (int64) -# - Executable name (string) -# - User name (string) -# - Implementation name (string) -# and then immediately closes connection. Note, all string fields can be empty or null strings. -# -# 'ATCH' -# Attach debugger to the process. If successful, the server responds with 'ACPT', followed by process ID -# (int64), and then the Python language version that the server is running represented by three int64s - -# major, minor, micro; From there on the socket is assumed to be using the normal PTVS debugging protocol. -# If attaching was not successful (which can happen if some other debugger is already attached), the server -# responds with 'RJCT' and closes the connection. -# -# 'REPL' -# Attach REPL to the process. If successful, the server responds with 'ACPT', and from there on the socket -# is assumed to be using the normal PTVS REPL protocol. If not successful (which can happen if there is -# no debugger attached), the server responds with 'RJCT' and closes the connection. - -PTVS_VER = '2.2' -DEFAULT_PORT = 5678 -PTVSDBG_VER = 6 # must be kept in sync with DebuggerProtocolVersion in PythonRemoteProcess.cs -PTVSDBG = to_bytes('PTVSDBG') -ACPT = to_bytes('ACPT') -RJCT = to_bytes('RJCT') -INFO = to_bytes('INFO') -ATCH = to_bytes('ATCH') -REPL = to_bytes('REPL') - -PY_ROOT = os.path.normcase(__file__) -while os.path.basename(PY_ROOT) != 'pythonFiles': - PY_ROOT = os.path.dirname(PY_ROOT) - -_attach_enabled = False -_attached = threading.Event() - - -class AttachAlreadyEnabledError(Exception): - - -def enable_attach(secret, address = ('0.0.0.0', DEFAULT_PORT), certfile = None, keyfile = None, redirect_output = True): - if not ssl and (certfile or keyfile): - raise ValueError('could not import the ssl module - SSL is not supported on this version of Python') - - if sys.platform == 'cli': - # Check that IronPython was launched with -X:Frames and -X:Tracing, since we can't register our trace - # func on the thread that calls enable_attach otherwise - import clr - x_tracing = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Tracing - x_frames = clr.GetCurrentRuntime().GetLanguageByExtension('py').Options.Frames - if not x_tracing or not x_frames: - raise RuntimeError('IronPython must be started with -X:Tracing and -X:Frames options to support PTVS remote debugging.') - - global _attach_enabled - if _attach_enabled: - raise AttachAlreadyEnabledError('ptvsd.enable_attach() has already been called in this process.') - _attach_enabled = True - - atexit.register(vspd.detach_process_and_notify_debugger) - - server = socket.socket(proto=socket.IPPROTO_TCP) - server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - server.bind(address) - server.listen(1) - def server_thread_func(): - while True: - client = None - raw_client = None - try: - client, addr = server.accept() - if certfile: - client = ssl.wrap_socket(client, server_side = True, ssl_version = ssl.PROTOCOL_TLSv1, certfile = certfile, keyfile = keyfile) - write_bytes(client, PTVSDBG) - write_int(client, PTVSDBG_VER) - - response = read_bytes(client, 7) - if response != PTVSDBG: - continue - dbg_ver = read_int(client) - if dbg_ver != PTVSDBG_VER: - continue - - client_secret = read_string(client) - if secret is None or secret == client_secret: - write_bytes(client, ACPT) - else: - write_bytes(client, RJCT) - continue - - response = read_bytes(client, 4) - - if response == INFO: - try: - pid = os.getpid() - except AttributeError: - pid = 0 - write_int(client, pid) - - exe = sys.executable or '' - write_string(client, exe) - - try: - username = getpass.getuser() - except AttributeError: - username = '' - write_string(client, username) - - try: - impl = platform.python_implementation() - except AttributeError: - try: - impl = sys.implementation.name - except AttributeError: - impl = 'Python' - - major, minor, micro, release_level, serial = sys.version_info - - os_and_arch = platform.system() - if os_and_arch == "": - os_and_arch = sys.platform - try: - if sys.maxsize > 2**32: - os_and_arch += ' 64-bit' - else: - os_and_arch += ' 32-bit' - except AttributeError: - pass - - version = '%s %s.%s.%s (%s)' % (impl, major, minor, micro, os_and_arch) - write_string(client, version) - - # Don't just drop the connection - let the debugger close it after it finishes reading. - client.recv(1) - - elif response == ATCH: - debug_options = vspd.parse_debug_options(read_string(client)) - debug_options.setdefault('rules', []).append({ - 'path': PY_ROOT, - 'include': False, - }) - if redirect_output: - debug_options.add('RedirectOutput') - - if vspd.DETACHED: - write_bytes(client, ACPT) - try: - pid = os.getpid() - except AttributeError: - pid = 0 - write_int(client, pid) - - major, minor, micro, release_level, serial = sys.version_info - write_int(client, major) - write_int(client, minor) - write_int(client, micro) - - vspd.attach_process_from_socket(client, debug_options, report = True) - vspd.mark_all_threads_for_break(vspd.STEPPING_ATTACH_BREAK) - _attached.set() - client = None - else: - write_bytes(client, RJCT) - - elif response == REPL: - if not vspd.DETACHED: - write_bytes(client, ACPT) - vspd.connect_repl_using_socket(client) - client = None - else: - write_bytes(client, RJCT) - - except (socket.error, OSError): - pass - finally: - if client is not None: - client.close() - - server_thread = threading.Thread(target = server_thread_func) - server_thread.setDaemon(True) - server_thread.start() - - frames = [] - f = sys._getframe() - while True: - f = f.f_back - if f is None: - break - frames.append(f) - frames.reverse() - cur_thread = vspd.new_thread() - for f in frames: - cur_thread.push_frame(f) - def replace_trace_func(): - for f in frames: - f.f_trace = cur_thread.trace_func - replace_trace_func() - sys.settrace(cur_thread.trace_func) - vspd.intercept_threads(for_attach = True) - - -# Alias for convenience of users of pydevd -settrace = enable_attach - - -def wait_for_attach(timeout = None): - if vspd.DETACHED: - _attached.clear() - _attached.wait(timeout) - - -def break_into_debugger(): - if not vspd.DETACHED: - vspd.SEND_BREAK_COMPLETE = thread.get_ident() - vspd.mark_all_threads_for_break() - -def is_attached(): - return not vspd.DETACHED diff --git a/src/test/pythonFiles/folding/visualstudio_ipython_repl.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl.py deleted file mode 100644 index 33aa109de971..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_ipython_repl.py +++ /dev/null @@ -1,430 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -"""Implements REPL support over IPython/ZMQ for VisualStudio""" - -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -import re -import sys -from visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list -from visualstudio_py_util import to_bytes -try: - import thread -except: - import _thread as thread # Renamed as Py3k - -from base64 import decodestring - -try: - import IPython -except ImportError: - exc_value = sys.exc_info()[1] - raise UnsupportedReplException('IPython mode requires IPython 0.11 or later: ' + str(exc_value)) - -def is_ipython_versionorgreater(major, minor): - """checks if we are at least a specific IPython version""" - match = re.match('(\d+).(\d+)', IPython.__version__) - if match: - groups = match.groups() - if int(groups[0]) > major: - return True - elif int(groups[0]) == major: - return int(groups[1]) >= minor - - return False - -remove_escapes = re.compile(r'\x1b[^m]*m') - -try: - if is_ipython_versionorgreater(3, 0): - from IPython.kernel import KernelManager - from IPython.kernel.channels import HBChannel - from IPython.kernel.threaded import (ThreadedZMQSocketChannel, ThreadedKernelClient as KernelClient) - ShellChannel = StdInChannel = IOPubChannel = ThreadedZMQSocketChannel - elif is_ipython_versionorgreater(1, 0): - from IPython.kernel import KernelManager, KernelClient - from IPython.kernel.channels import ShellChannel, HBChannel, StdInChannel, IOPubChannel - else: - import IPython.zmq - KernelClient = object # was split out from KernelManager in 1.0 - from IPython.zmq.kernelmanager import (KernelManager, - ShellSocketChannel as ShellChannel, - SubSocketChannel as IOPubChannel, - StdInSocketChannel as StdInChannel, - HBSocketChannel as HBChannel) - - from IPython.utils.traitlets import Type -except ImportError: - exc_value = sys.exc_info()[1] - raise UnsupportedReplException(str(exc_value)) - - -# TODO: SystemExit exceptions come back to us as strings, can we automatically exit when ones raised somehow? - -##### -# Channels which forward events - -# Description of the messaging protocol -# http://ipython.scipy.org/doc/manual/html/development/messaging.html - - -class DefaultHandler(object): - def unknown_command(self, content): - import pprint - print('unknown command ' + str(type(self))) - pprint.pprint(content) - - def call_handlers(self, msg): - # msg_type: - # execute_reply - msg_type = 'handle_' + msg['msg_type'] - - getattr(self, msg_type, self.unknown_command)(msg['content']) - -class VsShellChannel(DefaultHandler, ShellChannel): - - def handle_execute_reply(self, content): - # we could have a payload here... - payload = content['payload'] - - for item in payload: - data = item.get('data') - if data is not None: - try: - # Could be named km.sub_channel for very old IPython, but - # those versions should not put 'data' in this payload - write_data = self._vs_backend.km.iopub_channel.write_data - except AttributeError: - pass - else: - write_data(data) - continue - - output = item.get('text', None) - if output is not None: - self._vs_backend.write_stdout(output) - self._vs_backend.send_command_executed() - - def handle_inspect_reply(self, content): - self.handle_object_info_reply(content) - - def handle_object_info_reply(self, content): - self._vs_backend.object_info_reply = content - self._vs_backend.members_lock.release() - - def handle_complete_reply(self, content): - self._vs_backend.complete_reply = content - self._vs_backend.members_lock.release() - - def handle_kernel_info_reply(self, content): - self._vs_backend.write_stdout(content['banner']) - - -class VsIOPubChannel(DefaultHandler, IOPubChannel): - def call_handlers(self, msg): - # only output events from our session or no sessions - # https://pytools.codeplex.com/workitem/1622 - parent = msg.get('parent_header') - if not parent or parent.get('session') == self.session.session: - msg_type = 'handle_' + msg['msg_type'] - getattr(self, msg_type, self.unknown_command)(msg['content']) - - def handle_display_data(self, content): - # called when user calls display() - data = content.get('data', None) - - if data is not None: - self.write_data(data) - - def handle_stream(self, content): - stream_name = content['name'] - if is_ipython_versionorgreater(3, 0): - output = content['text'] - else: - output = content['data'] - if stream_name == 'stdout': - self._vs_backend.write_stdout(output) - elif stream_name == 'stderr': - self._vs_backend.write_stderr(output) - # TODO: stdin can show up here, do we echo that? - - def handle_execute_result(self, content): - self.handle_execute_output(content) - - def handle_execute_output(self, content): - # called when an expression statement is printed, we treat - # identical to stream output but it always goes to stdout - output = content['data'] - execution_count = content['execution_count'] - self._vs_backend.execution_count = execution_count + 1 - self._vs_backend.send_prompt( - '\r\nIn [%d]: ' % (execution_count + 1), - ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', - allow_multiple_statements=True - ) - self.write_data(output, execution_count) - - def write_data(self, data, execution_count = None): - output_xaml = data.get('application/xaml+xml', None) - if output_xaml is not None: - try: - if isinstance(output_xaml, str) and sys.version_info[0] >= 3: - output_xaml = output_xaml.encode('ascii') - self._vs_backend.write_xaml(decodestring(output_xaml)) - self._vs_backend.write_stdout('\n') - return - except: - pass - - output_png = data.get('image/png', None) - if output_png is not None: - try: - if isinstance(output_png, str) and sys.version_info[0] >= 3: - output_png = output_png.encode('ascii') - self._vs_backend.write_png(decodestring(output_png)) - self._vs_backend.write_stdout('\n') - return - except: - pass - - output_str = data.get('text/plain', None) - if output_str is not None: - if execution_count is not None: - if '\n' in output_str: - output_str = '\n' + output_str - output_str = 'Out[' + str(execution_count) + ']: ' + output_str - - self._vs_backend.write_stdout(output_str) - self._vs_backend.write_stdout('\n') - return - - def handle_error(self, content): - # TODO: this includes escape sequences w/ color, we need to unescape that - ename = content['ename'] - evalue = content['evalue'] - tb = content['traceback'] - self._vs_backend.write_stderr('\n'.join(tb)) - self._vs_backend.write_stdout('\n') - - def handle_execute_input(self, content): - # just a rebroadcast of the command to be executed, can be ignored - self._vs_backend.execution_count += 1 - self._vs_backend.send_prompt( - '\r\nIn [%d]: ' % (self._vs_backend.execution_count), - ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', - allow_multiple_statements=True - ) - pass - - def handle_status(self, content): - pass - - # Backwards compat w/ 0.13 - handle_pyin = handle_execute_input - handle_pyout = handle_execute_output - handle_pyerr = handle_error - - -class VsStdInChannel(DefaultHandler, StdInChannel): - def handle_input_request(self, content): - # queue this to another thread so we don't block the channel - def read_and_respond(): - value = self._vs_backend.read_line() - - self.input(value) - - thread.start_new_thread(read_and_respond, ()) - - -class VsHBChannel(DefaultHandler, HBChannel): - pass - - -class VsKernelManager(KernelManager, KernelClient): - shell_channel_class = Type(VsShellChannel) - if is_ipython_versionorgreater(1, 0): - iopub_channel_class = Type(VsIOPubChannel) - else: - sub_channel_class = Type(VsIOPubChannel) - stdin_channel_class = Type(VsStdInChannel) - hb_channel_class = Type(VsHBChannel) - - -class IPythonBackend(ReplBackend): - def __init__(self, mod_name = '__main__', launch_file = None): - ReplBackend.__init__(self) - self.launch_file = launch_file - self.mod_name = mod_name - self.km = VsKernelManager() - - if is_ipython_versionorgreater(0, 13): - # http://pytools.codeplex.com/workitem/759 - # IPython stopped accepting the ipython flag and switched to launcher, the new - # default is what we want though. - self.km.start_kernel(**{'extra_arguments': self.get_extra_arguments()}) - else: - self.km.start_kernel(**{'ipython': True, 'extra_arguments': self.get_extra_arguments()}) - self.km.start_channels() - self.exit_lock = thread.allocate_lock() - self.exit_lock.acquire() # used as an event - self.members_lock = thread.allocate_lock() - self.members_lock.acquire() - - self.km.shell_channel._vs_backend = self - self.km.stdin_channel._vs_backend = self - if is_ipython_versionorgreater(1, 0): - self.km.iopub_channel._vs_backend = self - else: - self.km.sub_channel._vs_backend = self - self.km.hb_channel._vs_backend = self - self.execution_count = 1 - - def get_extra_arguments(self): - if sys.version <= '2.': - return [unicode('--pylab=inline')] - return ['--pylab=inline'] - - def execute_file_as_main(self, filename, arg_string): - f = open(filename, 'rb') - try: - contents = f.read().replace(to_bytes("\r\n"), to_bytes("\n")) - finally: - f.close() - args = [filename] + _command_line_to_args_list(arg_string) - code = ''' -import sys -sys.argv = %(args)r -__file__ = %(filename)r -del sys -exec(compile(%(contents)r, %(filename)r, 'exec')) -''' % {'filename' : filename, 'contents':contents, 'args': args} - - self.run_command(code, True) - - def execution_loop(self): - # we've got a bunch of threads setup for communication, we just block - # here until we're requested to exit. - self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) - self.exit_lock.acquire() - - def run_command(self, command, silent = False): - if is_ipython_versionorgreater(3, 0): - self.km.execute(command, silent) - else: - self.km.shell_channel.execute(command, silent) - - def execute_file_ex(self, filetype, filename, args): - if filetype == 'script': - self.execute_file_as_main(filename, args) - else: - raise NotImplementedError("Cannot execute %s file" % filetype) - - def exit_process(self): - self.exit_lock.release() - - def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" - text = expression + '.' - if is_ipython_versionorgreater(3, 0): - self.km.complete(text) - else: - self.km.shell_channel.complete(text, text, 1) - - self.members_lock.acquire() - - reply = self.complete_reply - - res = {} - text_len = len(text) - for member in reply['matches']: - res[member[text_len:]] = 'object' - - return ('unknown', res, {}) - - def get_signatures(self, expression): - """returns doc, args, vargs, varkw, defaults.""" - - if is_ipython_versionorgreater(3, 0): - self.km.inspect(expression, None, 2) - else: - self.km.shell_channel.object_info(expression) - - self.members_lock.acquire() - - reply = self.object_info_reply - if is_ipython_versionorgreater(3, 0): - data = reply['data'] - text = data['text/plain'] - text = remove_escapes.sub('', text) - return [(text, (), None, None, [])] - else: - argspec = reply['argspec'] - defaults = argspec['defaults'] - if defaults is not None: - defaults = [repr(default) for default in defaults] - else: - defaults = [] - return [(reply['docstring'], argspec['args'], argspec['varargs'], argspec['varkw'], defaults)] - - def interrupt_main(self): - """aborts the current running command""" - self.km.interrupt_kernel() - - def set_current_module(self, module): - pass - - def get_module_names(self): - """returns a list of module names""" - return [] - - def flush(self): - pass - - def init_debugger(self): - from os import path - self.run_command(''' -def __visualstudio_debugger_init(): - import sys - sys.path.append(''' + repr(path.dirname(__file__)) + ''') - import visualstudio_py_debugger - new_thread = visualstudio_py_debugger.new_thread() - sys.settrace(new_thread.trace_func) - visualstudio_py_debugger.intercept_threads(True) - -__visualstudio_debugger_init() -del __visualstudio_debugger_init -''', True) - - def attach_process(self, port, debugger_id): - self.run_command(''' -def __visualstudio_debugger_attach(): - import visualstudio_py_debugger - - def do_detach(): - visualstudio_py_debugger.DETACH_CALLBACKS.remove(do_detach) - - visualstudio_py_debugger.DETACH_CALLBACKS.append(do_detach) - visualstudio_py_debugger.attach_process(''' + str(port) + ''', ''' + repr(debugger_id) + ''', report = True, block = True) - -__visualstudio_debugger_attach() -del __visualstudio_debugger_attach -''', True) - -class IPythonBackendWithoutPyLab(IPythonBackend): - def get_extra_arguments(self): - return [] \ No newline at end of file diff --git a/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py b/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py deleted file mode 100644 index 473046639147..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_ipython_repl_double_quotes.py +++ /dev/null @@ -1,430 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. - -"""Implements REPL support over IPython/ZMQ for VisualStudio""" - -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -import re -import sys -from visualstudio_py_repl import BasicReplBackend, ReplBackend, UnsupportedReplException, _command_line_to_args_list -from visualstudio_py_util import to_bytes -try: - import thread -except: - import _thread as thread # Renamed as Py3k - -from base64 import decodestring - -try: - import IPython -except ImportError: - exc_value = sys.exc_info()[1] - raise UnsupportedReplException('IPython mode requires IPython 0.11 or later: ' + str(exc_value)) - -def is_ipython_versionorgreater(major, minor): - """checks if we are at least a specific IPython version""" - match = re.match('(\d+).(\d+)', IPython.__version__) - if match: - groups = match.groups() - if int(groups[0]) > major: - return True - elif int(groups[0]) == major: - return int(groups[1]) >= minor - - return False - -remove_escapes = re.compile(r'\x1b[^m]*m') - -try: - if is_ipython_versionorgreater(3, 0): - from IPython.kernel import KernelManager - from IPython.kernel.channels import HBChannel - from IPython.kernel.threaded import (ThreadedZMQSocketChannel, ThreadedKernelClient as KernelClient) - ShellChannel = StdInChannel = IOPubChannel = ThreadedZMQSocketChannel - elif is_ipython_versionorgreater(1, 0): - from IPython.kernel import KernelManager, KernelClient - from IPython.kernel.channels import ShellChannel, HBChannel, StdInChannel, IOPubChannel - else: - import IPython.zmq - KernelClient = object # was split out from KernelManager in 1.0 - from IPython.zmq.kernelmanager import (KernelManager, - ShellSocketChannel as ShellChannel, - SubSocketChannel as IOPubChannel, - StdInSocketChannel as StdInChannel, - HBSocketChannel as HBChannel) - - from IPython.utils.traitlets import Type -except ImportError: - exc_value = sys.exc_info()[1] - raise UnsupportedReplException(str(exc_value)) - - -# TODO: SystemExit exceptions come back to us as strings, can we automatically exit when ones raised somehow? - -##### -# Channels which forward events - -# Description of the messaging protocol -# http://ipython.scipy.org/doc/manual/html/development/messaging.html - - -class DefaultHandler(object): - def unknown_command(self, content): - import pprint - print('unknown command ' + str(type(self))) - pprint.pprint(content) - - def call_handlers(self, msg): - # msg_type: - # execute_reply - msg_type = 'handle_' + msg['msg_type'] - - getattr(self, msg_type, self.unknown_command)(msg['content']) - -class VsShellChannel(DefaultHandler, ShellChannel): - - def handle_execute_reply(self, content): - # we could have a payload here... - payload = content['payload'] - - for item in payload: - data = item.get('data') - if data is not None: - try: - # Could be named km.sub_channel for very old IPython, but - # those versions should not put 'data' in this payload - write_data = self._vs_backend.km.iopub_channel.write_data - except AttributeError: - pass - else: - write_data(data) - continue - - output = item.get('text', None) - if output is not None: - self._vs_backend.write_stdout(output) - self._vs_backend.send_command_executed() - - def handle_inspect_reply(self, content): - self.handle_object_info_reply(content) - - def handle_object_info_reply(self, content): - self._vs_backend.object_info_reply = content - self._vs_backend.members_lock.release() - - def handle_complete_reply(self, content): - self._vs_backend.complete_reply = content - self._vs_backend.members_lock.release() - - def handle_kernel_info_reply(self, content): - self._vs_backend.write_stdout(content['banner']) - - -class VsIOPubChannel(DefaultHandler, IOPubChannel): - def call_handlers(self, msg): - # only output events from our session or no sessions - # https://pytools.codeplex.com/workitem/1622 - parent = msg.get('parent_header') - if not parent or parent.get('session') == self.session.session: - msg_type = 'handle_' + msg['msg_type'] - getattr(self, msg_type, self.unknown_command)(msg['content']) - - def handle_display_data(self, content): - # called when user calls display() - data = content.get('data', None) - - if data is not None: - self.write_data(data) - - def handle_stream(self, content): - stream_name = content['name'] - if is_ipython_versionorgreater(3, 0): - output = content['text'] - else: - output = content['data'] - if stream_name == 'stdout': - self._vs_backend.write_stdout(output) - elif stream_name == 'stderr': - self._vs_backend.write_stderr(output) - # TODO: stdin can show up here, do we echo that? - - def handle_execute_result(self, content): - self.handle_execute_output(content) - - def handle_execute_output(self, content): - # called when an expression statement is printed, we treat - # identical to stream output but it always goes to stdout - output = content['data'] - execution_count = content['execution_count'] - self._vs_backend.execution_count = execution_count + 1 - self._vs_backend.send_prompt( - '\r\nIn [%d]: ' % (execution_count + 1), - ' ' + ('.' * (len(str(execution_count + 1)) + 2)) + ': ', - allow_multiple_statements=True - ) - self.write_data(output, execution_count) - - def write_data(self, data, execution_count = None): - output_xaml = data.get('application/xaml+xml', None) - if output_xaml is not None: - try: - if isinstance(output_xaml, str) and sys.version_info[0] >= 3: - output_xaml = output_xaml.encode('ascii') - self._vs_backend.write_xaml(decodestring(output_xaml)) - self._vs_backend.write_stdout('\n') - return - except: - pass - - output_png = data.get('image/png', None) - if output_png is not None: - try: - if isinstance(output_png, str) and sys.version_info[0] >= 3: - output_png = output_png.encode('ascii') - self._vs_backend.write_png(decodestring(output_png)) - self._vs_backend.write_stdout('\n') - return - except: - pass - - output_str = data.get('text/plain', None) - if output_str is not None: - if execution_count is not None: - if '\n' in output_str: - output_str = '\n' + output_str - output_str = 'Out[' + str(execution_count) + ']: ' + output_str - - self._vs_backend.write_stdout(output_str) - self._vs_backend.write_stdout('\n') - return - - def handle_error(self, content): - # TODO: this includes escape sequences w/ color, we need to unescape that - ename = content['ename'] - evalue = content['evalue'] - tb = content['traceback'] - self._vs_backend.write_stderr('\n'.join(tb)) - self._vs_backend.write_stdout('\n') - - def handle_execute_input(self, content): - # just a rebroadcast of the command to be executed, can be ignored - self._vs_backend.execution_count += 1 - self._vs_backend.send_prompt( - '\r\nIn [%d]: ' % (self._vs_backend.execution_count), - ' ' + ('.' * (len(str(self._vs_backend.execution_count)) + 2)) + ': ', - allow_multiple_statements=True - ) - pass - - def handle_status(self, content): - pass - - # Backwards compat w/ 0.13 - handle_pyin = handle_execute_input - handle_pyout = handle_execute_output - handle_pyerr = handle_error - - -class VsStdInChannel(DefaultHandler, StdInChannel): - def handle_input_request(self, content): - # queue this to another thread so we don't block the channel - def read_and_respond(): - value = self._vs_backend.read_line() - - self.input(value) - - thread.start_new_thread(read_and_respond, ()) - - -class VsHBChannel(DefaultHandler, HBChannel): - pass - - -class VsKernelManager(KernelManager, KernelClient): - shell_channel_class = Type(VsShellChannel) - if is_ipython_versionorgreater(1, 0): - iopub_channel_class = Type(VsIOPubChannel) - else: - sub_channel_class = Type(VsIOPubChannel) - stdin_channel_class = Type(VsStdInChannel) - hb_channel_class = Type(VsHBChannel) - - -class IPythonBackend(ReplBackend): - def __init__(self, mod_name = '__main__', launch_file = None): - ReplBackend.__init__(self) - self.launch_file = launch_file - self.mod_name = mod_name - self.km = VsKernelManager() - - if is_ipython_versionorgreater(0, 13): - # http://pytools.codeplex.com/workitem/759 - # IPython stopped accepting the ipython flag and switched to launcher, the new - # default is what we want though. - self.km.start_kernel(**{'extra_arguments': self.get_extra_arguments()}) - else: - self.km.start_kernel(**{'ipython': True, 'extra_arguments': self.get_extra_arguments()}) - self.km.start_channels() - self.exit_lock = thread.allocate_lock() - self.exit_lock.acquire() # used as an event - self.members_lock = thread.allocate_lock() - self.members_lock.acquire() - - self.km.shell_channel._vs_backend = self - self.km.stdin_channel._vs_backend = self - if is_ipython_versionorgreater(1, 0): - self.km.iopub_channel._vs_backend = self - else: - self.km.sub_channel._vs_backend = self - self.km.hb_channel._vs_backend = self - self.execution_count = 1 - - def get_extra_arguments(self): - if sys.version <= '2.': - return [unicode('--pylab=inline')] - return ['--pylab=inline'] - - def execute_file_as_main(self, filename, arg_string): - f = open(filename, 'rb') - try: - contents = f.read().replace(to_bytes("\r\n"), to_bytes("\n")) - finally: - f.close() - args = [filename] + _command_line_to_args_list(arg_string) - code = """ -import sys -sys.argv = %(args)r -__file__ = %(filename)r -del sys -exec(compile(%(contents)r, %(filename)r, 'exec')) -""" % {'filename' : filename, 'contents':contents, 'args': args} - - self.run_command(code, True) - - def execution_loop(self): - # we've got a bunch of threads setup for communication, we just block - # here until we're requested to exit. - self.send_prompt('\r\nIn [1]: ', ' ...: ', allow_multiple_statements=True) - self.exit_lock.acquire() - - def run_command(self, command, silent = False): - if is_ipython_versionorgreater(3, 0): - self.km.execute(command, silent) - else: - self.km.shell_channel.execute(command, silent) - - def execute_file_ex(self, filetype, filename, args): - if filetype == 'script': - self.execute_file_as_main(filename, args) - else: - raise NotImplementedError("Cannot execute %s file" % filetype) - - def exit_process(self): - self.exit_lock.release() - - def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" - text = expression + '.' - if is_ipython_versionorgreater(3, 0): - self.km.complete(text) - else: - self.km.shell_channel.complete(text, text, 1) - - self.members_lock.acquire() - - reply = self.complete_reply - - res = {} - text_len = len(text) - for member in reply['matches']: - res[member[text_len:]] = 'object' - - return ('unknown', res, {}) - - def get_signatures(self, expression): - """returns doc, args, vargs, varkw, defaults.""" - - if is_ipython_versionorgreater(3, 0): - self.km.inspect(expression, None, 2) - else: - self.km.shell_channel.object_info(expression) - - self.members_lock.acquire() - - reply = self.object_info_reply - if is_ipython_versionorgreater(3, 0): - data = reply['data'] - text = data['text/plain'] - text = remove_escapes.sub('', text) - return [(text, (), None, None, [])] - else: - argspec = reply['argspec'] - defaults = argspec['defaults'] - if defaults is not None: - defaults = [repr(default) for default in defaults] - else: - defaults = [] - return [(reply['docstring'], argspec['args'], argspec['varargs'], argspec['varkw'], defaults)] - - def interrupt_main(self): - """aborts the current running command""" - self.km.interrupt_kernel() - - def set_current_module(self, module): - pass - - def get_module_names(self): - """returns a list of module names""" - return [] - - def flush(self): - pass - - def init_debugger(self): - from os import path - self.run_command(""" -def __visualstudio_debugger_init(): - import sys - sys.path.append(""" + repr(path.dirname(__file__)) + """) - import visualstudio_py_debugger - new_thread = visualstudio_py_debugger.new_thread() - sys.settrace(new_thread.trace_func) - visualstudio_py_debugger.intercept_threads(True) - -__visualstudio_debugger_init() -del __visualstudio_debugger_init -""", True) - - def attach_process(self, port, debugger_id): - self.run_command(""" -def __visualstudio_debugger_attach(): - import visualstudio_py_debugger - - def do_detach(): - visualstudio_py_debugger.DETACH_CALLBACKS.remove(do_detach) - - visualstudio_py_debugger.DETACH_CALLBACKS.append(do_detach) - visualstudio_py_debugger.attach_process(""" + str(port) + """, """ + repr(debugger_id) + """, report = True, block = True) - -__visualstudio_debugger_attach() -del __visualstudio_debugger_attach -""", True) - -class IPythonBackendWithoutPyLab(IPythonBackend): - def get_extra_arguments(self): - return [] diff --git a/src/test/pythonFiles/folding/visualstudio_py_debugger.py b/src/test/pythonFiles/folding/visualstudio_py_debugger.py deleted file mode 100644 index ec18ff8c63b0..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_py_debugger.py +++ /dev/null @@ -1,644 +0,0 @@ -# Python Tools for Visual Studio -# Copyright(c) Microsoft Corporation -# All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the License); you may not use -# this file except in compliance with the License. You may obtain a copy of the -# License at http://www.apache.org/licenses/LICENSE-2.0 -# -# THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS -# OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY -# IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, -# MERCHANTABLITY OR NON-INFRINGEMENT. -# -# See the Apache Version 2.0 License for specific language governing -# permissions and limitations under the License. -# With number of modifications by Don Jayamanne - -from __future__ import with_statement - -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) -# attach scenario, it is loaded on the injected debugger attach thread, and if threading module -# hasn't been loaded already, it will assume that the thread on which it is being loaded is the -# main thread. This will cause issues when the thread goes away after attach completes. -_threading = None - -import sys -import ctypes -try: - import thread -except ImportError: - import _thread as thread -import socket -import struct -import weakref -import traceback -import types -import bisect -from os import path -import ntpath -import runpy -import datetime -from codecs import BOM_UTF8 - -try: - # In the local attach scenario, visualstudio_py_util is injected into globals() - # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util -except: - try: - import visualstudio_py_util as _vspu - except ImportError: - import ptvsd.visualstudio_py_util as _vspu - -to_bytes = _vspu.to_bytes -exec_file = _vspu.exec_file -exec_module = _vspu.exec_module -exec_code = _vspu.exec_code -read_bytes = _vspu.read_bytes -read_int = _vspu.read_int -read_string = _vspu.read_string -write_bytes = _vspu.write_bytes -write_int = _vspu.write_int -write_string = _vspu.write_string -safe_repr = _vspu.SafeRepr() - -try: - # In the local attach scenario, visualstudio_py_repl is injected into globals() - # by PyDebugAttach before loading this module, and cannot be imported. - _vspr = visualstudio_py_repl -except: - try: - import visualstudio_py_repl as _vspr - except ImportError: - import ptvsd.visualstudio_py_repl as _vspr - -try: - import stackless -except ImportError: - stackless = None - -try: - xrange -except: - xrange = range - -if sys.platform == 'cli': - import clr - from System.Runtime.CompilerServices import ConditionalWeakTable - IPY_SEEN_MODULES = ConditionalWeakTable[object, object]() - -# Import encodings early to avoid import on the debugger thread, which may cause deadlock -from encodings import utf_8 - -# WARNING: Avoid imports beyond this point, specifically on the debugger thread, as this may cause -# deadlock where the debugger thread performs an import while a user thread has the import lock - -# save start_new_thread so we can call it later, we'll intercept others calls to it. - -debugger_dll_handle = None -DETACHED = True -def thread_creator(func, args, kwargs = {}, *extra_args): - if not isinstance(args, tuple): - # args is not a tuple. This may be because we have become bound to a - # class, which has offset our arguments by one. - if isinstance(kwargs, tuple): - func, args = args, kwargs - kwargs = extra_args[0] if len(extra_args) > 0 else {} - - return _start_new_thread(new_thread_wrapper, (func, args, kwargs)) - -_start_new_thread = thread.start_new_thread -THREADS = {} -THREADS_LOCK = thread.allocate_lock() -MODULES = [] - -BREAK_ON_SYSTEMEXIT_ZERO = False -DEBUG_STDLIB = False -DJANGO_DEBUG = False - -RICH_EXCEPTIONS = False -IGNORE_DJANGO_TEMPLATE_WARNINGS = False - -# Py3k compat - alias unicode to str -try: - unicode -except: - unicode = str - -# A value of a synthesized child. The string is passed through to the variable list, and type is not displayed at all. -class SynthesizedValue(object): - def __init__(self, repr_value='', len_value=None): - self.repr_value = repr_value - self.len_value = len_value - def __repr__(self): - return self.repr_value - def __len__(self): - return self.len_value - -# Specifies list of files not to debug. Can be extended by other modules -# (the REPL does this for $attach support and not stepping into the REPL). -DONT_DEBUG = [path.normcase(__file__), path.normcase(_vspu.__file__)] -if sys.version_info >= (3, 3): - DONT_DEBUG.append(path.normcase('<frozen importlib._bootstrap>')) -if sys.version_info >= (3, 5): - DONT_DEBUG.append(path.normcase('<frozen importlib._bootstrap_external>')) - -# Contains information about all breakpoints in the process. Keys are line numbers on which -# there are breakpoints in any file, and values are dicts. For every line number, the -# corresponding dict contains all the breakpoints that fall on that line. The keys in that -# dict are tuples of the form (filename, breakpoint_id), each entry representing a single -# breakpoint, and values are BreakpointInfo objects. -# -# For example, given the following breakpoints: -# -# 1. In 'main.py' at line 10. -# 2. In 'main.py' at line 20. -# 3. In 'module.py' at line 10. -# -# the contents of BREAKPOINTS would be: -# {10: {('main.py', 1): ..., ('module.py', 3): ...}, 20: {('main.py', 2): ... }} -BREAKPOINTS = {} - -# Contains information about all pending (i.e. not yet bound) breakpoints in the process. -# Elements are BreakpointInfo objects. -PENDING_BREAKPOINTS = set() - -# Must be in sync with enum PythonBreakpointConditionKind in PythonBreakpoint.cs -BREAKPOINT_CONDITION_ALWAYS = 0 -BREAKPOINT_CONDITION_WHEN_TRUE = 1 -BREAKPOINT_CONDITION_WHEN_CHANGED = 2 - -# Must be in sync with enum PythonBreakpointPassCountKind in PythonBreakpoint.cs -BREAKPOINT_PASS_COUNT_ALWAYS = 0 -BREAKPOINT_PASS_COUNT_EVERY = 1 -BREAKPOINT_PASS_COUNT_WHEN_EQUAL = 2 -BREAKPOINT_PASS_COUNT_WHEN_EQUAL_OR_GREATER = 3 - -## Begin modification by Don Jayamanne -DJANGO_VERSIONS_IDENTIFIED = False -IS_DJANGO18 = False -IS_DJANGO19 = False -IS_DJANGO19_OR_HIGHER = False - -try: - dict_contains = dict.has_key -except: - try: - #Py3k does not have has_key anymore, and older versions don't have __contains__ - dict_contains = dict.__contains__ - except: - try: - dict_contains = dict.has_key - except NameError: - def dict_contains(d, key): - return d.has_key(key) -## End modification by Don Jayamanne - -class BreakpointInfo(object): - __slots__ = [ - 'breakpoint_id', 'filename', 'lineno', 'condition_kind', 'condition', - 'pass_count_kind', 'pass_count', 'is_bound', 'last_condition_value', - 'hit_count' - ] - - # For "when changed" breakpoints, this is used as the initial value of last_condition_value, - # such that it is guaranteed to not compare equal to any other value that it will get later. - _DUMMY_LAST_VALUE = object() - - def __init__(self, breakpoint_id, filename, lineno, condition_kind, condition, pass_count_kind, pass_count): - self.breakpoint_id = breakpoint_id - self.filename = filename - self.lineno = lineno - self.condition_kind = condition_kind - self.condition = condition - self.pass_count_kind = pass_count_kind - self.pass_count = pass_count - self.is_bound = False - self.last_condition_value = BreakpointInfo._DUMMY_LAST_VALUE - self.hit_count = 0 - - @staticmethod - def find_by_id(breakpoint_id): - for line, bp_dict in BREAKPOINTS.items(): - for (filename, bp_id), bp in bp_dict.items(): - if bp_id == breakpoint_id: - return bp - return None - -# lock for calling .send on the socket -send_lock = thread.allocate_lock() - -class _SendLockContextManager(object): - """context manager for send lock. Handles both acquiring/releasing the - send lock as well as detaching the debugger if the remote process - is disconnected""" - - def __enter__(self): - # mark that we're about to do socket I/O so we won't deliver - # debug events when we're debugging the standard library - cur_thread = get_thread_from_id(thread.get_ident()) - if cur_thread is not None: - cur_thread.is_sending = True - - send_lock.acquire() - - def __exit__(self, exc_type, exc_value, tb): - send_lock.release() - - # start sending debug events again - cur_thread = get_thread_from_id(thread.get_ident()) - if cur_thread is not None: - cur_thread.is_sending = False - - if exc_type is not None: - detach_threads() - detach_process() - # swallow the exception, we're no longer debugging - return True - -_SendLockCtx = _SendLockContextManager() - -SEND_BREAK_COMPLETE = False - -STEPPING_OUT = -1 # first value, we decrement below this -STEPPING_NONE = 0 -STEPPING_BREAK = 1 -STEPPING_LAUNCH_BREAK = 2 -STEPPING_ATTACH_BREAK = 3 -STEPPING_INTO = 4 -STEPPING_OVER = 5 # last value, we increment past this. - -USER_STEPPING = (STEPPING_OUT, STEPPING_INTO, STEPPING_OVER) - -FRAME_KIND_NONE = 0 -FRAME_KIND_PYTHON = 1 -FRAME_KIND_DJANGO = 2 - -DJANGO_BUILTINS = {'True': True, 'False': False, 'None': None} - -PYTHON_EVALUATION_RESULT_REPR_KIND_NORMAL = 0 # regular repr and hex repr (if applicable) for the evaluation result; length is len(result) -PYTHON_EVALUATION_RESULT_REPR_KIND_RAW = 1 # repr is raw representation of the value - see TYPES_WITH_RAW_REPR; length is len(repr) -PYTHON_EVALUATION_RESULT_REPR_KIND_RAWLEN = 2 # same as above, but only the length is reported, not the actual value - -PYTHON_EVALUATION_RESULT_EXPANDABLE = 1 -PYTHON_EVALUATION_RESULT_METHOD_CALL = 2 -PYTHON_EVALUATION_RESULT_SIDE_EFFECTS = 4 -PYTHON_EVALUATION_RESULT_RAW = 8 -PYTHON_EVALUATION_RESULT_HAS_RAW_REPR = 16 - -# Don't show attributes of these types if they come from the class (assume they are methods). -METHOD_TYPES = ( - types.FunctionType, - types.MethodType, - types.BuiltinFunctionType, - type("".__repr__), # method-wrapper -) - -# repr() for these types can be used as input for eval() to get the original value. -# float is intentionally not included because it is not always round-trippable (e.g inf, nan). -TYPES_WITH_ROUND_TRIPPING_REPR = set((type(None), int, bool, str, unicode)) -if sys.version[0] == '3': - TYPES_WITH_ROUND_TRIPPING_REPR.add(bytes) -else: - TYPES_WITH_ROUND_TRIPPING_REPR.add(long) - -# repr() for these types can be used as input for eval() to get the original value, provided that the same is true for all their elements. -COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR = set((tuple, list, set, frozenset)) - -# eval(repr(x)), but optimized for common types for which it is known that result == x. -def eval_repr(x): - def is_repr_round_tripping(x): - # Do exact type checks here - subclasses can override __repr__. - if type(x) in TYPES_WITH_ROUND_TRIPPING_REPR: - return True - elif type(x) in COLLECTION_TYPES_WITH_ROUND_TRIPPING_REPR: - # All standard sequence types are round-trippable if their elements are. - return all((is_repr_round_tripping(item) for item in x)) - else: - return False - if is_repr_round_tripping(x): - return x - else: - return eval(repr(x), {}) - -# key is type, value is function producing the raw repr -TYPES_WITH_RAW_REPR = { - unicode: (lambda s: s) -} - -# bytearray is 2.6+ -try: - # getfilesystemencoding is used here because it effectively corresponds to the notion of "locale encoding": - # current ANSI codepage on Windows, LC_CTYPE on Linux, UTF-8 on OS X - which is exactly what we want. - TYPES_WITH_RAW_REPR[bytearray] = lambda b: b.decode(sys.getfilesystemencoding(), 'ignore') -except: - pass - -if sys.version[0] == '3': - TYPES_WITH_RAW_REPR[bytes] = TYPES_WITH_RAW_REPR[bytearray] -else: - TYPES_WITH_RAW_REPR[str] = TYPES_WITH_RAW_REPR[unicode] - -if sys.version[0] == '3': - # work around a crashing bug on CPython 3.x where they take a hard stack overflow - # we'll never see this exception but it'll allow us to keep our try/except handler - # the same across all versions of Python - class StackOverflowException(Exception): pass -else: - StackOverflowException = RuntimeError - -ASBR = to_bytes('ASBR') -SETL = to_bytes('SETL') -THRF = to_bytes('THRF') -DETC = to_bytes('DETC') -NEWT = to_bytes('NEWT') -EXTT = to_bytes('EXTT') -EXIT = to_bytes('EXIT') -EXCP = to_bytes('EXCP') -EXC2 = to_bytes('EXC2') -MODL = to_bytes('MODL') -STPD = to_bytes('STPD') -BRKS = to_bytes('BRKS') -BRKF = to_bytes('BRKF') -BRKH = to_bytes('BRKH') -BRKC = to_bytes('BRKC') -BKHC = to_bytes('BKHC') -LOAD = to_bytes('LOAD') -EXCE = to_bytes('EXCE') -EXCR = to_bytes('EXCR') -CHLD = to_bytes('CHLD') -OUTP = to_bytes('OUTP') -REQH = to_bytes('REQH') -LAST = to_bytes('LAST') - -def get_thread_from_id(id): - THREADS_LOCK.acquire() - try: - return THREADS.get(id) - finally: - THREADS_LOCK.release() - -def should_send_frame(frame): - return (frame is not None and - frame.f_code not in DEBUG_ENTRYPOINTS and - path.normcase(frame.f_code.co_filename) not in DONT_DEBUG) - -KNOWN_DIRECTORIES = set((None, '')) -KNOWN_ZIPS = set() - -def is_file_in_zip(filename): - parent, name = path.split(path.abspath(filename)) - if parent in KNOWN_DIRECTORIES: - return False - elif parent in KNOWN_ZIPS: - return True - elif path.isdir(parent): - KNOWN_DIRECTORIES.add(parent) - return False - else: - KNOWN_ZIPS.add(parent) - return True - -def lookup_builtin(name, frame): - try: - return frame.f_builtins.get(bits) - except: - # http://ironpython.codeplex.com/workitem/30908 - builtins = frame.f_globals['__builtins__'] - if not isinstance(builtins, dict): - builtins = builtins.__dict__ - return builtins.get(name) - -def lookup_local(frame, name): - bits = name.split('.') - obj = frame.f_locals.get(bits[0]) or frame.f_globals.get(bits[0]) or lookup_builtin(bits[0], frame) - bits.pop(0) - while bits and obj is not None and type(obj) is types.ModuleType: - obj = getattr(obj, bits.pop(0), None) - return obj - -if sys.version_info[0] >= 3: - _EXCEPTIONS_MODULE = 'builtins' -else: - _EXCEPTIONS_MODULE = 'exceptions' - -def get_exception_name(exc_type): - if exc_type.__module__ == _EXCEPTIONS_MODULE: - return exc_type.__name__ - else: - return exc_type.__module__ + '.' + exc_type.__name__ - -# These constants come from Visual Studio - enum_EXCEPTION_STATE -BREAK_MODE_NEVER = 0 -BREAK_MODE_ALWAYS = 1 -BREAK_MODE_UNHANDLED = 32 - -BREAK_TYPE_NONE = 0 -BREAK_TYPE_UNHANDLED = 1 -BREAK_TYPE_HANDLED = 2 - -class ExceptionBreakInfo(object): - BUILT_IN_HANDLERS = { - path.normcase('<frozen importlib._bootstrap>'): ((None, None, '*'),), - path.normcase('build\\bdist.win32\\egg\\pkg_resources.py'): ((None, None, '*'),), - path.normcase('build\\bdist.win-amd64\\egg\\pkg_resources.py'): ((None, None, '*'),), - } - - def __init__(self): - self.default_mode = BREAK_MODE_UNHANDLED - self.break_on = { } - self.handler_cache = dict(self.BUILT_IN_HANDLERS) - self.handler_lock = thread.allocate_lock() - self.add_exception('exceptions.IndexError', BREAK_MODE_NEVER) - self.add_exception('builtins.IndexError', BREAK_MODE_NEVER) - self.add_exception('exceptions.KeyError', BREAK_MODE_NEVER) - self.add_exception('builtins.KeyError', BREAK_MODE_NEVER) - self.add_exception('exceptions.AttributeError', BREAK_MODE_NEVER) - self.add_exception('builtins.AttributeError', BREAK_MODE_NEVER) - self.add_exception('exceptions.StopIteration', BREAK_MODE_NEVER) - self.add_exception('builtins.StopIteration', BREAK_MODE_NEVER) - self.add_exception('exceptions.GeneratorExit', BREAK_MODE_NEVER) - self.add_exception('builtins.GeneratorExit', BREAK_MODE_NEVER) - - def clear(self): - self.default_mode = BREAK_MODE_UNHANDLED - self.break_on.clear() - self.handler_cache = dict(self.BUILT_IN_HANDLERS) - - def should_break(self, thread, ex_type, ex_value, trace): - probe_stack() - name = get_exception_name(ex_type) - mode = self.break_on.get(name, self.default_mode) - break_type = BREAK_TYPE_NONE - if mode & BREAK_MODE_ALWAYS: - if self.is_handled(thread, ex_type, ex_value, trace): - break_type = BREAK_TYPE_HANDLED - else: - break_type = BREAK_TYPE_UNHANDLED - elif (mode & BREAK_MODE_UNHANDLED) and not self.is_handled(thread, ex_type, ex_value, trace): - break_type = BREAK_TYPE_UNHANDLED - - if break_type: - if issubclass(ex_type, SystemExit): - if not BREAK_ON_SYSTEMEXIT_ZERO: - if not ex_value or (isinstance(ex_value, SystemExit) and not ex_value.code): - break_type = BREAK_TYPE_NONE - - return break_type - - def is_handled(self, thread, ex_type, ex_value, trace): - if trace is None: - # get out if we didn't get a traceback - return False - - if trace.tb_next is not None: - if should_send_frame(trace.tb_next.tb_frame) and should_debug_code(trace.tb_next.tb_frame.f_code): - # don't break if this is not the top of the traceback, - # unless the previous frame was not debuggable - return True - - cur_frame = trace.tb_frame - - while should_send_frame(cur_frame) and cur_frame.f_code is not None and cur_frame.f_code.co_filename is not None: - filename = path.normcase(cur_frame.f_code.co_filename) - if is_file_in_zip(filename): - # File is in a zip, so assume it handles exceptions - return True - - if not is_same_py_file(filename, __file__): - handlers = self.handler_cache.get(filename) - - if handlers is None: - # req handlers for this file from the debug engine - self.handler_lock.acquire() - - with _SendLockCtx: - write_bytes(conn, REQH) - write_string(conn, filename) - - # wait for the handler data to be received - self.handler_lock.acquire() - self.handler_lock.release() - - handlers = self.handler_cache.get(filename) - - if handlers is None: - # no code available, so assume unhandled - return False - - line = cur_frame.f_lineno - for line_start, line_end, expressions in handlers: - if line_start is None or line_start <= line < line_end: - if '*' in expressions: - return True - - for text in expressions: - try: - res = lookup_local(cur_frame, text) - if res is not None and issubclass(ex_type, res): - return True - except: - pass - - cur_frame = cur_frame.f_back - - return False - - def add_exception(self, name, mode=BREAK_MODE_UNHANDLED): - if name.startswith(_EXCEPTIONS_MODULE + '.'): - name = name[len(_EXCEPTIONS_MODULE) + 1:] - self.break_on[name] = mode - -BREAK_ON = ExceptionBreakInfo() - -def probe_stack(depth = 10): - """helper to make sure we have enough stack space to proceed w/o corrupting - debugger state.""" - if depth == 0: - return - probe_stack(depth - 1) - -PREFIXES = [path.normcase(sys.prefix)] -# If we're running in a virtual env, DEBUG_STDLIB should respect this too. -if hasattr(sys, 'base_prefix'): - PREFIXES.append(path.normcase(sys.base_prefix)) -if hasattr(sys, 'real_prefix'): - PREFIXES.append(path.normcase(sys.real_prefix)) - -def should_debug_code(code): - if not code or not code.co_filename: - return False - - filename = path.normcase(code.co_filename) - if not DEBUG_STDLIB: - for prefix in PREFIXES: - if prefix != '' and filename.startswith(prefix): - return False - - for dont_debug_file in DONT_DEBUG: - if is_same_py_file(filename, dont_debug_file): - return False - - if is_file_in_zip(filename): - # file in inside an egg or zip, so we can't debug it - return False - - return True - -attach_lock = thread.allocate() -attach_sent_break = False - -local_path_to_vs_path = {} - -def breakpoint_path_match(vs_path, local_path): - vs_path_norm = path.normcase(vs_path) - local_path_norm = path.normcase(local_path) - if local_path_to_vs_path.get(local_path_norm) == vs_path_norm: - return True - - # Walk the local filesystem from local_path up, matching agains win_path component by component, - # and stop when we no longer see an __init__.py. This should give a reasonably close approximation - # of matching the package name. - while True: - local_path, local_name = path.split(local_path) - vs_path, vs_name = ntpath.split(vs_path) - # Match the last component in the path. If one or both components are unavailable, then - # we have reached the root on the corresponding path without successfully matching. - if not local_name or not vs_name or path.normcase(local_name) != path.normcase(vs_name): - return False - # If we have an __init__.py, this module was inside the package, and we still need to match - # thatpackage, so walk up one level and keep matching. Otherwise, we've walked as far as we - # needed to, and matched all names on our way, so this is a match. - if not path.exists(path.join(local_path, '__init__.py')): - break - - local_path_to_vs_path[local_path_norm] = vs_path_norm - return True - -def update_all_thread_stacks(blocking_thread = None, check_is_blocked = True): - THREADS_LOCK.acquire() - all_threads = list(THREADS.values()) - THREADS_LOCK.release() - - for cur_thread in all_threads: - if cur_thread is blocking_thread: - continue - - cur_thread._block_starting_lock.acquire() - if not check_is_blocked or not cur_thread._is_blocked: - # release the lock, we're going to run user code to evaluate the frames - cur_thread._block_starting_lock.release() - - frames = cur_thread.get_frame_list() - - # re-acquire the lock and make sure we're still not blocked. If so send - # the frame list. - cur_thread._block_starting_lock.acquire() - if not check_is_blocked or not cur_thread._is_blocked: - cur_thread.send_frame_list(frames) - - cur_thread._block_starting_lock.release() diff --git a/src/test/pythonFiles/folding/visualstudio_py_repl.py b/src/test/pythonFiles/folding/visualstudio_py_repl.py deleted file mode 100644 index 9c5127d66537..000000000000 --- a/src/test/pythonFiles/folding/visualstudio_py_repl.py +++ /dev/null @@ -1,520 +0,0 @@ -# Python Tools for Visual Studio - -# Copyright(c) Microsoft Corporation - -# All rights reserved. - -from __future__ import with_statement - -__author__ = "Microsoft Corporation <ptvshelp@microsoft.com>" -__version__ = "3.0.0.0" - -# This module MUST NOT import threading in global scope. This is because in a direct (non-ptvsd) - -# attach scenario, it is loaded on the injected debugger attach thread, and if threading module - -# hasn't been loaded already, it will assume that the thread on which it is being loaded is the - -# main thread. This will cause issues when the thread goes away after attach completes. - -try: - import thread -except ImportError: - # Renamed in Python3k - import _thread as thread -try: - from ssl import SSLError -except: - SSLError = None - -import sys -import socket -import select -import time -import struct -import imp -import traceback -import random -import os -import inspect -import types -from collections import deque - -try: - # In the local attach scenario, visualstudio_py_util is injected into globals() - - # by PyDebugAttach before loading this module, and cannot be imported. - _vspu = visualstudio_py_util -except: - try: - import visualstudio_py_util as _vspu - except ImportError: - import ptvsd.visualstudio_py_util as _vspu -to_bytes = _vspu.to_bytes -read_bytes = _vspu.read_bytes -read_int = _vspu.read_int -read_string = _vspu.read_string -write_bytes = _vspu.write_bytes -write_int = _vspu.write_int -write_string = _vspu.write_string - -try: - unicode -except NameError: - unicode = str - -try: - BaseException -except NameError: - # BaseException not defined until Python 2.5 - BaseException = Exception - -DEBUG = os.environ.get('DEBUG_REPL') is not None - -PY_ROOT = os.path.normcase(__file__) -while os.path.basename(PY_ROOT) != 'pythonFiles': - PY_ROOT = os.path.dirname(PY_ROOT) - -__all__ = ['ReplBackend', 'BasicReplBackend', 'BACKEND'] - -def _debug_write(out): - if DEBUG: - sys.__stdout__.write(out) - sys.__stdout__.flush() - - -class SafeSendLock(object): - """a lock which ensures we're released if we take a KeyboardInterrupt exception acquiring it""" - def __init__(self): - self.lock = thread.allocate_lock() - - def __enter__(self): - self.acquire() - - def __exit__(self, exc_type, exc_value, tb): - self.release() - - def acquire(self): - try: - self.lock.acquire() - except KeyboardInterrupt: - try: - self.lock.release() - except: - pass - raise - - def release(self): - self.lock.release() - -def _command_line_to_args_list(cmdline): - """splits a string into a list using Windows command line syntax.""" - args_list = [] - - if cmdline and cmdline.strip(): - from ctypes import c_int, c_voidp, c_wchar_p - from ctypes import byref, POINTER, WinDLL - - clta = WinDLL('shell32').CommandLineToArgvW - clta.argtypes = [c_wchar_p, POINTER(c_int)] - clta.restype = POINTER(c_wchar_p) - - lf = WinDLL('kernel32').LocalFree - lf.argtypes = [c_voidp] - - pNumArgs = c_int() - r = clta(cmdline, byref(pNumArgs)) - if r: - for index in range(0, pNumArgs.value): - if sys.hexversion >= 0x030000F0: - argval = r[index] - else: - argval = r[index].encode('ascii', 'replace') - args_list.append(argval) - lf(r) - else: - sys.stderr.write('Error parsing script arguments:\n') - sys.stderr.write(cmdline + '\n') - - return args_list - - -class UnsupportedReplException(Exception): - def __init__(self, reason): - self.reason = reason - -# save the start_new_thread so we won't debug/break into the REPL comm thread. -start_new_thread = thread.start_new_thread -class ReplBackend(object): - """back end for executing REPL code. This base class handles all of the communication with the remote process while derived classes implement the actual inspection and introspection.""" - _MRES = to_bytes('MRES') - _SRES = to_bytes('SRES') - _MODS = to_bytes('MODS') - _IMGD = to_bytes('IMGD') - _PRPC = to_bytes('PRPC') - _RDLN = to_bytes('RDLN') - _STDO = to_bytes('STDO') - _STDE = to_bytes('STDE') - _DBGA = to_bytes('DBGA') - _DETC = to_bytes('DETC') - _DPNG = to_bytes('DPNG') - _DXAM = to_bytes('DXAM') - _CHWD = to_bytes('CHWD') - - _MERR = to_bytes('MERR') - _SERR = to_bytes('SERR') - _ERRE = to_bytes('ERRE') - _EXIT = to_bytes('EXIT') - _DONE = to_bytes('DONE') - _MODC = to_bytes('MODC') - - def __init__(self, *args, **kwargs): - import threading - self.conn = None - self.send_lock = SafeSendLock() - self.input_event = threading.Lock() - self.input_event.acquire() # lock starts acquired (we use it like a manual reset event) - self.input_string = None - self.exit_requested = False - - def connect(self, port): - self.conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.conn.connect(('127.0.0.1', port)) - - # start a new thread for communicating w/ the remote process - start_new_thread(self._repl_loop, ()) - - def connect_using_socket(self, socket): - self.conn = socket - start_new_thread(self._repl_loop, ()) - - def _repl_loop(self): - """loop on created thread which processes communicates with the REPL window""" - try: - while True: - if self.check_for_exit_repl_loop(): - break - - # we receive a series of 4 byte commands. Each command then - - # has it's own format which we must parse before continuing to - - # the next command. - self.flush() - self.conn.settimeout(10) - - # 2.x raises SSLError in case of timeout (http://bugs.python.org/issue10272) - if SSLError: - timeout_exc_types = (socket.timeout, SSLError) - else: - timeout_exc_types = socket.timeout - try: - inp = read_bytes(self.conn, 4) - except timeout_exc_types: - r, w, x = select.select([], [], [self.conn], 0) - if x: - # an exception event has 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)) - debug_options.setdefault('rules', []).append({ - 'path': PY_ROOT, - 'include': False, - }) - self.attach_process(port, id, debug_options) - - _COMMANDS = { - to_bytes('run '): _cmd_run, - to_bytes('abrt'): _cmd_abrt, - to_bytes('exit'): _cmd_exit, - to_bytes('mems'): _cmd_mems, - to_bytes('sigs'): _cmd_sigs, - to_bytes('mods'): _cmd_mods, - to_bytes('setm'): _cmd_setm, - to_bytes('sett'): _cmd_sett, - to_bytes('inpl'): _cmd_inpl, - to_bytes('excf'): _cmd_excf, - to_bytes('excx'): _cmd_excx, - to_bytes('dbga'): _cmd_debug_attach, - } - - def _write_member_dict(self, mem_dict): - write_int(self.conn, len(mem_dict)) - for name, type_name in mem_dict.items(): - write_string(self.conn, name) - write_string(self.conn, type_name) - - def on_debugger_detach(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DETC) - - def init_debugger(self): - from os import path - sys.path.append(path.dirname(__file__)) - import visualstudio_py_debugger - new_thread = visualstudio_py_debugger.new_thread() - sys.settrace(new_thread.trace_func) - visualstudio_py_debugger.intercept_threads(True) - - def send_image(self, filename): - with self.send_lock: - write_bytes(self.conn, ReplBackend._IMGD) - write_string(self.conn, filename) - - def write_png(self, image_bytes): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DPNG) - write_int(self.conn, len(image_bytes)) - write_bytes(self.conn, image_bytes) - - def write_xaml(self, xaml_bytes): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DXAM) - write_int(self.conn, len(xaml_bytes)) - write_bytes(self.conn, xaml_bytes) - - def send_prompt(self, ps1, ps2, allow_multiple_statements): - """sends the current prompt to the interactive window""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._PRPC) - write_string(self.conn, ps1) - write_string(self.conn, ps2) - write_int(self.conn, 1 if allow_multiple_statements else 0) - - def send_cwd(self): - """sends the current working directory""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._CHWD) - write_string(self.conn, os.getcwd()) - - def send_error(self): - """reports that an error occured to the interactive window""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._ERRE) - - def send_exit(self): - """reports the that the REPL process has exited to the interactive window""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._EXIT) - - def send_command_executed(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._DONE) - - def send_modules_changed(self): - with self.send_lock: - write_bytes(self.conn, ReplBackend._MODC) - - def read_line(self): - """reads a line of input from standard input""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._RDLN) - self.input_event.acquire() - return self.input_string - - def write_stdout(self, value): - """writes a string to standard output in the remote console""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._STDO) - write_string(self.conn, value) - - def write_stderr(self, value): - """writes a string to standard input in the remote console""" - with self.send_lock: - write_bytes(self.conn, ReplBackend._STDE) - write_string(self.conn, value) - - ################################################################ - - # Implementation of execution, etc... - - def execution_loop(self): - """starts processing execution requests""" - raise NotImplementedError - - def run_command(self, command): - """runs the specified command which is a string containing code""" - raise NotImplementedError - - def execute_file(self, filename, args): - """executes the given filename as the main module""" - return self.execute_file_ex('script', filename, args) - - def execute_file_ex(self, filetype, filename, args): - """executes the given filename as a 'script', 'module' or 'process'.""" - raise NotImplementedError - - def interrupt_main(self): - """aborts the current running command""" - raise NotImplementedError - - def exit_process(self): - """exits the REPL process""" - raise NotImplementedError - - def get_members(self, expression): - """returns a tuple of the type name, instance members, and type members""" - raise NotImplementedError - - def get_signatures(self, expression): - """returns doc, args, vargs, varkw, defaults.""" - raise NotImplementedError - - def set_current_module(self, module): - """sets the module which code executes against""" - raise NotImplementedError - - def set_current_thread_and_frame(self, thread_id, frame_id, frame_kind): - """sets the current thread and frame which code will execute against""" - raise NotImplementedError - - def get_module_names(self): - """returns a list of module names""" - raise NotImplementedError - - def flush(self): - """flushes the stdout/stderr buffers""" - raise NotImplementedError - - def attach_process(self, port, debugger_id, debug_options): - """starts processing execution requests""" - raise NotImplementedError - -def exit_work_item(): - sys.exit(0) diff --git a/src/test/pythonFiles/formatting/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, "<test>", "exec") - self.assertIn("unexpected EOF", str(cm.exception)) - -var_annot_global: int # a global annotated is necessary for test_var_annot - -# custom namespace for testing __annotations__ - -class CNS: - def __init__(self): - self._dct = {} - def __setitem__(self, item, value): - self._dct[item.lower()] = value - def __getitem__(self, item): - return self._dct[item] - - -class GrammarTests(unittest.TestCase): - - check_syntax_error = check_syntax_error - - # single_input: NEWLINE | simple_stmt | compound_stmt NEWLINE - # XXX can't test in a script -- this rule is only used when interactive - - # file_input: (NEWLINE | stmt)* ENDMARKER - # Being tested as this very moment this very module - - # expr_input: testlist NEWLINE - # XXX Hard to test -- used only in calls to input() - - def test_eval_input(self): - # testlist ENDMARKER - x = eval('1, 0 or 1') - - def test_var_annot_basics(self): - # all these should be allowed - var1: int = 5 - var2: [int, str] - my_lst = [42] - def one(): - return 1 - int.new_attr: int - [list][0]: type - my_lst[one() - 1]: int = 5 - self.assertEqual(my_lst, [5]) - - def test_var_annot_syntax_errors(self): - # parser pass - check_syntax_error(self, "def f: int") - check_syntax_error(self, "x: int: str") - check_syntax_error(self, "def f():\n" - " nonlocal x: int\n") - # AST pass - check_syntax_error(self, "[x, 0]: int\n") - check_syntax_error(self, "f(): int\n") - check_syntax_error(self, "(x,): int") - check_syntax_error(self, "def f():\n" - " (x, y): int = (1, 2)\n") - # symtable pass - check_syntax_error(self, "def f():\n" - " x: int\n" - " global x\n") - check_syntax_error(self, "def f():\n" - " global x\n" - " x: int\n") - - def test_var_annot_basic_semantics(self): - # execution order - with self.assertRaises(ZeroDivisionError): - no_name[does_not_exist]: no_name_again = 1 / 0 - with self.assertRaises(NameError): - no_name[does_not_exist]: 1 / 0 = 0 - global var_annot_global - - # function semantics - def f(): - st: str = "Hello" - a.b: int = (1, 2) - return st - self.assertEqual(f.__annotations__, {}) - def f_OK(): - x: 1 / 0 - f_OK() - def fbad(): - x: int - print(x) - with self.assertRaises(UnboundLocalError): - fbad() - def f2bad(): - (no_such_global): int - print(no_such_global) - try: - f2bad() - except Exception as e: - self.assertIs(type(e), NameError) - - # class semantics - class C: - __foo: int - s: str = "attr" - z = 2 - def __init__(self, x): - self.x: int = x - self.assertEqual(C.__annotations__, {'_C__foo': int, 's': str}) - with self.assertRaises(NameError): - class CBad: - no_such_name_defined.attr: int = 0 - with self.assertRaises(NameError): - class Cbad2(C): - x: int - x.y: list = [] - - def test_var_annot_metaclass_semantics(self): - class CMeta(type): - @classmethod - def __prepare__(metacls, name, bases, **kwds): - return {'__annotations__': CNS()} - class CC(metaclass=CMeta): - XX: 'ANNOT' - self.assertEqual(CC.__annotations__['xx'], 'ANNOT') - - def test_var_annot_module_semantics(self): - with self.assertRaises(AttributeError): - print(test.__annotations__) - self.assertEqual(ann_module.__annotations__, - {1: 2, 'x': int, 'y': str, 'f': typing.Tuple[int, int]}) - self.assertEqual(ann_module.M.__annotations__, - {'123': 123, 'o': type}) - self.assertEqual(ann_module2.__annotations__, {}) - - def test_var_annot_in_module(self): - # check that functions fail the same way when executed - # outside of module where they were defined - from test.ann_module3 import f_bad_ann, g_bad_ann, D_bad_ann - with self.assertRaises(NameError): - f_bad_ann() - with self.assertRaises(NameError): - g_bad_ann() - with self.assertRaises(NameError): - D_bad_ann(5) - - def test_var_annot_simple_exec(self): - gns = {}; lns = {} - exec("'docstring'\n" - "__annotations__[1] = 2\n" - "x: int = 5\n", gns, lns) - self.assertEqual(lns["__annotations__"], {1: 2, 'x': int}) - with self.assertRaises(KeyError): - gns['__annotations__'] - - def test_var_annot_custom_maps(self): - # tests with custom locals() and __annotations__ - ns = {'__annotations__': CNS()} - exec('X: int; Z: str = "Z"; (w): complex = 1j', ns) - self.assertEqual(ns['__annotations__']['x'], int) - self.assertEqual(ns['__annotations__']['z'], str) - with self.assertRaises(KeyError): - ns['__annotations__']['w'] - nonloc_ns = {} - class CNS2: - def __init__(self): - self._dct = {} - def __setitem__(self, item, value): - nonlocal nonloc_ns - self._dct[item] = value - nonloc_ns[item] = value - def __getitem__(self, item): - return self._dct[item] - exec('x: int = 1', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], int) - - def test_var_annot_refleak(self): - # complex case: custom locals plus custom __annotations__ - # this was causing refleak - cns = CNS() - nonloc_ns = {'__annotations__': cns} - class CNS2: - def __init__(self): - self._dct = {'__annotations__': cns} - def __setitem__(self, item, value): - nonlocal nonloc_ns - self._dct[item] = value - nonloc_ns[item] = value - def __getitem__(self, item): - return self._dct[item] - exec('X: str', {}, CNS2()) - self.assertEqual(nonloc_ns['__annotations__']['x'], str) - - def test_funcdef(self): - ### [decorators] 'def' NAME parameters ['->' test] ':' suite - ### decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE - ### decorators: decorator+ - ### parameters: '(' [typedargslist] ')' - ### typedargslist: ((tfpdef ['=' test] ',')* - ### ('*' [tfpdef] (',' tfpdef ['=' test])* [',' '**' tfpdef] | '**' tfpdef) - ### | tfpdef ['=' test] (',' tfpdef ['=' test])* [',']) - ### tfpdef: NAME [':' test] - ### varargslist: ((vfpdef ['=' test] ',')* - ### ('*' [vfpdef] (',' vfpdef ['=' test])* [',' '**' vfpdef] | '**' vfpdef) - ### | vfpdef ['=' test] (',' vfpdef ['=' test])* [',']) - ### vfpdef: NAME - def f1(): pass - f1() - f1(*()) - f1(*(), **{}) - def f2(one_argument): pass - def f3(two, arguments): pass - self.assertEqual(f2.__code__.co_varnames, ('one_argument',)) - self.assertEqual(f3.__code__.co_varnames, ('two', 'arguments')) - def a1(one_arg,): pass - def a2(two, args,): pass - def v0(*rest): pass - def v1(a, *rest): pass - def v2(a, b, *rest): pass - - f1() - f2(1) - f2(1,) - f3(1, 2) - f3(1, 2,) - v0() - v0(1) - v0(1,) - v0(1, 2) - v0(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - v1(1) - v1(1,) - v1(1, 2) - v1(1, 2, 3) - v1(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - v2(1, 2) - v2(1, 2, 3) - v2(1, 2, 3, 4) - v2(1, 2, 3, 4, 5, 6, 7, 8, 9, 0) - - def d01(a=1): pass - d01() - d01(1) - d01(*(1,)) - d01(*[] or [2]) - d01(*() or (), *{} and (), **() or {}) - d01(**{'a': 2}) - d01(**{'a': 2} or {}) - def d11(a, b=1): pass - d11(1) - d11(1, 2) - d11(1, **{'b': 2}) - def d21(a, b, c=1): pass - d21(1, 2) - d21(1, 2, 3) - d21(*(1, 2, 3)) - d21(1, *(2, 3)) - d21(1, 2, *(3,)) - d21(1, 2, **{'c': 3}) - def d02(a=1, b=2): pass - d02() - d02(1) - d02(1, 2) - d02(*(1, 2)) - d02(1, *(2,)) - d02(1, **{'b': 2}) - d02(**{'a': 1, 'b': 2}) - def d12(a, b=1, c=2): pass - d12(1) - d12(1, 2) - d12(1, 2, 3) - def d22(a, b, c=1, d=2): pass - d22(1, 2) - d22(1, 2, 3) - d22(1, 2, 3, 4) - def d01v(a=1, *rest): pass - d01v() - d01v(1) - d01v(1, 2) - d01v(*(1, 2, 3, 4)) - d01v(*(1,)) - d01v(**{'a': 2}) - def d11v(a, b=1, *rest): pass - d11v(1) - d11v(1, 2) - d11v(1, 2, 3) - def d21v(a, b, c=1, *rest): pass - d21v(1, 2) - d21v(1, 2, 3) - d21v(1, 2, 3, 4) - d21v(*(1, 2, 3, 4)) - d21v(1, 2, **{'c': 3}) - def d02v(a=1, b=2, *rest): pass - d02v() - d02v(1) - d02v(1, 2) - d02v(1, 2, 3) - d02v(1, *(2, 3, 4)) - d02v(**{'a': 1, 'b': 2}) - def d12v(a, b=1, c=2, *rest): pass - d12v(1) - d12v(1, 2) - d12v(1, 2, 3) - d12v(1, 2, 3, 4) - d12v(*(1, 2, 3, 4)) - d12v(1, 2, *(3, 4, 5)) - d12v(1, *(2,), **{'c': 3}) - def d22v(a, b, c=1, d=2, *rest): pass - d22v(1, 2) - d22v(1, 2, 3) - d22v(1, 2, 3, 4) - d22v(1, 2, 3, 4, 5) - d22v(*(1, 2, 3, 4)) - d22v(1, 2, *(3, 4, 5)) - d22v(1, *(2, 3), **{'d': 4}) - - # keyword argument type tests - try: - str('x', **{b'foo': 1}) - except TypeError: - pass - else: - self.fail('Bytes should not work as keyword argument names') - # keyword only argument tests - def pos0key1(*, key): return key - pos0key1(key=100) - def pos2key2(p1, p2, *, k1, k2=100): return p1, p2, k1, k2 - pos2key2(1, 2, k1=100) - pos2key2(1, 2, k1=100, k2=200) - pos2key2(1, 2, k2=100, k1=200) - def pos2key2dict(p1, p2, *, k1=100, k2, **kwarg): return p1, p2, k1, k2, kwarg - pos2key2dict(1, 2, k2=100, tokwarg1=100, tokwarg2=200) - pos2key2dict(1, 2, tokwarg1=100, tokwarg2=200, k2=100) - - self.assertRaises(SyntaxError, eval, "def f(*): pass") - self.assertRaises(SyntaxError, eval, "def f(*,): pass") - self.assertRaises(SyntaxError, eval, "def f(*, **kwds): pass") - - # keyword arguments after *arglist - def f(*args, **kwargs): - return args, kwargs - self.assertEqual(f(1, x=2, *[3, 4], y=5), ((1, 3, 4), - {'x': 2, 'y': 5})) - self.assertEqual(f(1, *(2, 3), 4), ((1, 2, 3, 4), {})) - self.assertRaises(SyntaxError, eval, "f(1, x=2, *(3,4), x=5)") - self.assertEqual(f(**{'eggs': 'scrambled', 'spam': 'fried'}), - ((), {'eggs': 'scrambled', 'spam': 'fried'})) - self.assertEqual(f(spam='fried', **{'eggs': 'scrambled'}), - ((), {'eggs': 'scrambled', 'spam': 'fried'})) - - # Check ast errors in *args and *kwargs - check_syntax_error(self, "f(*g(1=2))") - check_syntax_error(self, "f(**g(1=2))") - - # argument annotation tests - def f(x) -> list: pass - self.assertEqual(f.__annotations__, {'return': list}) - def f(x: int): pass - self.assertEqual(f.__annotations__, {'x': int}) - def f(*x: str): pass - self.assertEqual(f.__annotations__, {'x': str}) - def f(**x: float): pass - self.assertEqual(f.__annotations__, {'x': float}) - def f(x, y: 1 + 2): pass - self.assertEqual(f.__annotations__, {'y': 3}) - def f(a, b: 1, c: 2, d): pass - self.assertEqual(f.__annotations__, {'b': 1, 'c': 2}) - def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6): pass - self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'g': 6}) - def f(a, b: 1, c: 2, d, e: 3 = 4, f=5, *g: 6, h: 7, i=8, j: 9 = 10, - **k: 11) -> 12: pass - self.assertEqual(f.__annotations__, - {'b': 1, 'c': 2, 'e': 3, 'g': 6, 'h': 7, 'j': 9, - 'k': 11, 'return': 12}) - # Check for issue #20625 -- annotations mangling - class Spam: - def f(self, *, __kw: 1): - pass - class Ham(Spam): pass - self.assertEqual(Spam.f.__annotations__, {'_Spam__kw': 1}) - self.assertEqual(Ham.f.__annotations__, {'_Spam__kw': 1}) - # Check for SF Bug #1697248 - mixing decorators and a return annotation - def null(x): return x - @null - def f(x) -> list: pass - self.assertEqual(f.__annotations__, {'return': list}) - - # test closures with a variety of opargs - closure = 1 - def f(): return closure - def f(x=1): return closure - def f(*, k=1): return closure - def f() -> int: return closure - - # Check trailing commas are permitted in funcdef argument list - def f(a,): pass - def f(*args,): pass - def f(**kwds,): pass - def f(a, *args,): pass - def f(a, **kwds,): pass - def f(*args, b,): pass - def f(*, b,): pass - def f(*args, **kwds,): pass - def f(a, *args, b,): pass - def f(a, *, b,): pass - def f(a, *args, **kwds,): pass - def f(*args, b, **kwds,): pass - def f(*, b, **kwds,): pass - def f(a, *args, b, **kwds,): pass - def f(a, *, b, **kwds,): pass - - def test_lambdef(self): - ### lambdef: 'lambda' [varargslist] ':' test - l1 = lambda: 0 - self.assertEqual(l1(), 0) - l2 = lambda: a[d] # XXX just testing the expression - l3 = lambda: [2 < x for x in [-1, 3, 0]] - self.assertEqual(l3(), [0, 1, 0]) - l4 = lambda x=lambda y=lambda z=1: z: y(): x() - self.assertEqual(l4(), 1) - l5 = lambda x, y, z=2: x + y + z - self.assertEqual(l5(1, 2), 5) - self.assertEqual(l5(1, 2, 3), 6) - check_syntax_error(self, "lambda x: x = 2") - check_syntax_error(self, "lambda (None,): None") - l6 = lambda x, y, *, k=20: x + y + k - self.assertEqual(l6(1, 2), 1 + 2 + 20) - self.assertEqual(l6(1, 2, k=10), 1 + 2 + 10) - - # check that trailing commas are permitted - l10 = lambda a,: 0 - l11 = lambda *args,: 0 - l12 = lambda **kwds,: 0 - l13 = lambda a, *args,: 0 - l14 = lambda a, **kwds,: 0 - l15 = lambda *args, b,: 0 - l16 = lambda *, b,: 0 - l17 = lambda *args, **kwds,: 0 - l18 = lambda a, *args, b,: 0 - l19 = lambda a, *, b,: 0 - l20 = lambda a, *args, **kwds,: 0 - l21 = lambda *args, b, **kwds,: 0 - l22 = lambda *, b, **kwds,: 0 - l23 = lambda a, *args, b, **kwds,: 0 - l24 = lambda a, *, b, **kwds,: 0 - - - ### stmt: simple_stmt | compound_stmt - # Tested below - - def test_simple_stmt(self): - ### simple_stmt: small_stmt (';' small_stmt)* [';'] - x = 1; pass; del x - def foo(): - # verify statements that end with semi-colons - x = 1; pass; del x; - foo() - - ### small_stmt: expr_stmt | pass_stmt | del_stmt | flow_stmt | import_stmt | global_stmt | access_stmt - # Tested below - - def test_expr_stmt(self): - # (exprlist '=')* exprlist - 1 - 1, 2, 3 - x = 1 - x = 1, 2, 3 - x = y = z = 1, 2, 3 - x, y, z = 1, 2, 3 - abc = a, b, c = x, y, z = xyz = 1, 2, (3, 4) - - check_syntax_error(self, "x + 1 = 1") - check_syntax_error(self, "a + 1 = b + 2") - - # Check the heuristic for print & exec covers significant cases - # As well as placing some limits on false positives - def test_former_statements_refer_to_builtins(self): - keywords = "print", "exec" - # Cases where we want the custom error - cases = [ - "{} foo", - "{} {{1:foo}}", - "if 1: {} foo", - "if 1: {} {{1:foo}}", - "if 1:\n {} foo", - "if 1:\n {} {{1:foo}}", - ] - for keyword in keywords: - custom_msg = "call to '{}'".format(keyword) - for case in cases: - source = case.format(keyword) - with self.subTest(source=source): - with self.assertRaisesRegex(SyntaxError, custom_msg): - exec(source) - source = source.replace("foo", "(foo.)") - with self.subTest(source=source): - with self.assertRaisesRegex(SyntaxError, "invalid syntax"): - exec(source) - - def test_del_stmt(self): - # 'del' exprlist - abc = [1, 2, 3] - x, y, z = abc - xyz = x, y, z - - del abc - del x, y, (z, xyz) - - def test_pass_stmt(self): - # 'pass' - pass - - # flow_stmt: break_stmt | continue_stmt | return_stmt | raise_stmt - # Tested below - - def test_break_stmt(self): - # 'break' - while 1: break - - def test_continue_stmt(self): - # 'continue' - i = 1 - while i: i = 0; continue - - msg = "" - while not msg: - msg = "ok" - try: - continue - msg = "continue failed to continue inside try" - except: - msg = "continue inside try called except block" - if msg != "ok": - self.fail(msg) - - msg = "" - while not msg: - msg = "finally block not called" - try: - continue - finally: - msg = "ok" - if msg != "ok": - self.fail(msg) - - def test_break_continue_loop(self): - # This test warrants an explanation. It is a test specifically for SF bugs - # #463359 and #462937. The bug is that a 'break' statement executed or - # exception raised inside a try/except inside a loop, *after* a continue - # statement has been executed in that loop, will cause the wrong number of - # arguments to be popped off the stack and the instruction pointer reset to - # a very small number (usually 0.) Because of this, the following test - # *must* written as a function, and the tracking vars *must* be function - # arguments with default values. Otherwise, the test will loop and loop. - - def test_inner(extra_burning_oil=1, count=0): - big_hippo = 2 - while big_hippo: - count += 1 - try: - if extra_burning_oil and big_hippo == 1: - extra_burning_oil -= 1 - break - big_hippo -= 1 - continue - except: - raise - if count > 2 or big_hippo != 1: - self.fail("continue then break in try/except in loop broken!") - test_inner() - - def test_return(self): - # 'return' [testlist] - def g1(): return - def g2(): return 1 - g1() - x = g2() - check_syntax_error(self, "class foo:return 1") - - def test_break_in_finally(self): - count = 0 - while count < 2: - count += 1 - try: - pass - finally: - break - self.assertEqual(count, 1) - - count = 0 - while count < 2: - count += 1 - try: - continue - finally: - break - self.assertEqual(count, 1) - - count = 0 - while count < 2: - count += 1 - try: - 1 / 0 - finally: - break - self.assertEqual(count, 1) - - for count in [0, 1]: - self.assertEqual(count, 0) - try: - pass - finally: - break - self.assertEqual(count, 0) - - for count in [0, 1]: - self.assertEqual(count, 0) - try: - continue - finally: - break - self.assertEqual(count, 0) - - for count in [0, 1]: - self.assertEqual(count, 0) - try: - 1 / 0 - finally: - break - self.assertEqual(count, 0) - - def test_continue_in_finally(self): - count = 0 - while count < 2: - count += 1 - try: - pass - finally: - continue - break - self.assertEqual(count, 2) - - count = 0 - while count < 2: - count += 1 - try: - break - finally: - continue - self.assertEqual(count, 2) - - count = 0 - while count < 2: - count += 1 - try: - 1 / 0 - finally: - continue - break - self.assertEqual(count, 2) - - for count in [0, 1]: - try: - pass - finally: - continue - break - self.assertEqual(count, 1) - - for count in [0, 1]: - try: - break - finally: - continue - self.assertEqual(count, 1) - - for count in [0, 1]: - try: - 1 / 0 - finally: - continue - break - self.assertEqual(count, 1) - - def test_return_in_finally(self): - def g1(): - try: - pass - finally: - return 1 - self.assertEqual(g1(), 1) - - def g2(): - try: - return 2 - finally: - return 3 - self.assertEqual(g2(), 3) - - def g3(): - try: - 1 / 0 - finally: - return 4 - self.assertEqual(g3(), 4) - - def test_yield(self): - # Allowed as standalone statement - def g(): yield 1 - def g(): yield from () - # Allowed as RHS of assignment - def g(): x = yield 1 - def g(): x = yield from () - # Ordinary yield accepts implicit tuples - def g(): yield 1, 1 - def g(): x = yield 1, 1 - # 'yield from' does not - check_syntax_error(self, "def g(): yield from (), 1") - check_syntax_error(self, "def g(): x = yield from (), 1") - # Requires parentheses as subexpression - def g(): 1, (yield 1) - def g(): 1, (yield from ()) - check_syntax_error(self, "def g(): 1, yield 1") - check_syntax_error(self, "def g(): 1, yield from ()") - # Requires parentheses as call argument - def g(): f((yield 1)) - def g(): f((yield 1), 1) - def g(): f((yield from ())) - def g(): f((yield from ()), 1) - check_syntax_error(self, "def g(): f(yield 1)") - check_syntax_error(self, "def g(): f(yield 1, 1)") - check_syntax_error(self, "def g(): f(yield from ())") - check_syntax_error(self, "def g(): f(yield from (), 1)") - # Not allowed at top level - check_syntax_error(self, "yield") - check_syntax_error(self, "yield from") - # Not allowed at class scope - check_syntax_error(self, "class foo:yield 1") - check_syntax_error(self, "class foo:yield from ()") - # Check annotation refleak on SyntaxError - check_syntax_error(self, "def g(a:(yield)): pass") - - def test_yield_in_comprehensions(self): - # Check yield in comprehensions - def g(): [x for x in [(yield 1)]] - def g(): [x for x in [(yield from ())]] - - check = self.check_syntax_error - check("def g(): [(yield x) for x in ()]", - "'yield' inside list comprehension") - check("def g(): [x for x in () if not (yield x)]", - "'yield' inside list comprehension") - check("def g(): [y for x in () for y in [(yield x)]]", - "'yield' inside list comprehension") - check("def g(): {(yield x) for x in ()}", - "'yield' inside set comprehension") - check("def g(): {(yield x): x for x in ()}", - "'yield' inside dict comprehension") - check("def g(): {x: (yield x) for x in ()}", - "'yield' inside dict comprehension") - check("def g(): ((yield x) for x in ())", - "'yield' inside generator expression") - check("def g(): [(yield from x) for x in ()]", - "'yield' inside list comprehension") - check("class C: [(yield x) for x in ()]", - "'yield' inside list comprehension") - check("[(yield x) for x in ()]", - "'yield' inside list comprehension") - - def test_raise(self): - # 'raise' test [',' test] - try: raise RuntimeError('just testing') - except RuntimeError: pass - try: raise KeyboardInterrupt - except KeyboardInterrupt: pass - - def test_import(self): - # 'import' dotted_as_names - import sys - import time, sys - # 'from' dotted_name 'import' ('*' | '(' import_as_names ')' | import_as_names) - from time import time - from time import (time) - # not testable inside a function, but already done at top of the module - # from sys import * - from sys import path, argv - from sys import (path, argv) - from sys import (path, argv,) - - def test_global(self): - # 'global' NAME (',' NAME)* - global a - global a, b - global one, two, three, four, five, six, seven, eight, nine, ten - - def test_nonlocal(self): - # 'nonlocal' NAME (',' NAME)* - x = 0 - y = 0 - def f(): - nonlocal x - nonlocal x, y - - def test_assert(self): - # assertTruestmt: 'assert' test [',' test] - assert 1 - assert 1, 1 - assert lambda x: x - assert 1, lambda x: x + 1 - - try: - assert True - except AssertionError as e: - self.fail("'assert True' should not have raised an AssertionError") - - try: - assert True, 'this should always pass' - except AssertionError as e: - self.fail("'assert True, msg' should not have " - "raised an AssertionError") - - # these tests fail if python is run with -O, so check __debug__ - @unittest.skipUnless(__debug__, "Won't work if __debug__ is False") - def testAssert2(self): - try: - assert 0, "msg" - except AssertionError as e: - self.assertEqual(e.args[0], "msg") - else: - self.fail("AssertionError not raised by assert 0") - - try: - assert False - except AssertionError as e: - self.assertEqual(len(e.args), 0) - else: - self.fail("AssertionError not raised by 'assert False'") - - - ### compound_stmt: if_stmt | while_stmt | for_stmt | try_stmt | funcdef | classdef - # Tested below - - def test_if(self): - # 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite] - if 1: pass - if 1: pass - else: pass - if 0: pass - elif 0: pass - if 0: pass - elif 0: pass - elif 0: pass - elif 0: pass - else: pass - - def test_while(self): - # 'while' test ':' suite ['else' ':' suite] - while 0: pass - while 0: pass - else: pass - - # Issue1920: "while 0" is optimized away, - # ensure that the "else" clause is still present. - x = 0 - while 0: - x = 1 - else: - x = 2 - self.assertEqual(x, 2) - - def test_for(self): - # 'for' exprlist 'in' exprlist ':' suite ['else' ':' suite] - for i in 1, 2, 3: pass - for i, j, k in (): pass - else: pass - class Squares: - def __init__(self, max): - self.max = max - self.sofar = [] - def __len__(self): return len(self.sofar) - def __getitem__(self, i): - if not 0 <= i < self.max: raise IndexError - n = len(self.sofar) - while n <= i: - self.sofar.append(n * n) - n = n + 1 - return self.sofar[i] - n = 0 - for x in Squares(10): n = n + x - if n != 285: - self.fail('for over growing sequence') - - result = [] - for x, in [(1,), (2,), (3,)]: - result.append(x) - self.assertEqual(result, [1, 2, 3]) - - def test_try(self): - ### try_stmt: 'try' ':' suite (except_clause ':' suite)+ ['else' ':' suite] - ### | 'try' ':' suite 'finally' ':' suite - ### except_clause: 'except' [expr ['as' expr]] - try: - 1 / 0 - except ZeroDivisionError: - pass - else: - pass - try: 1 / 0 - except EOFError: pass - except TypeError as msg: pass - except: pass - else: pass - try: 1 / 0 - except (EOFError, TypeError, ZeroDivisionError): pass - try: 1 / 0 - except (EOFError, TypeError, ZeroDivisionError) as msg: pass - try: pass - finally: pass - - def test_suite(self): - # simple_stmt | NEWLINE INDENT NEWLINE* (stmt NEWLINE*)+ DEDENT - if 1: pass - if 1: - pass - if 1: - # - # - # - pass - pass - # - pass - # - - def test_test(self): - ### and_test ('or' and_test)* - ### and_test: not_test ('and' not_test)* - ### not_test: 'not' not_test | comparison - if not 1: pass - if 1 and 1: pass - if 1 or 1: pass - if not not not 1: pass - if not 1 and 1 and 1: pass - if 1 and 1 or 1 and 1 and 1 or not 1 and 1: pass - - def test_comparison(self): - ### comparison: expr (comp_op expr)* - ### comp_op: '<'|'>'|'=='|'>='|'<='|'!='|'in'|'not' 'in'|'is'|'is' 'not' - if 1: pass - x = (1 == 1) - if 1 == 1: pass - if 1 != 1: pass - if 1 < 1: pass - if 1 > 1: pass - if 1 <= 1: pass - if 1 >= 1: pass - if 1 is 1: pass - if 1 is not 1: pass - if 1 in (): pass - if 1 not in (): pass - if 1 < 1 > 1 == 1 >= 1 <= 1 != 1 in 1 not in 1 is 1 is not 1: pass - - def test_binary_mask_ops(self): - x = 1 & 1 - x = 1 ^ 1 - x = 1 | 1 - - def test_shift_ops(self): - x = 1 << 1 - x = 1 >> 1 - x = 1 << 1 >> 1 - - def test_additive_ops(self): - x = 1 - x = 1 + 1 - x = 1 - 1 - 1 - x = 1 - 1 + 1 - 1 + 1 - - def test_multiplicative_ops(self): - x = 1 * 1 - x = 1 / 1 - x = 1 % 1 - x = 1 / 1 * 1 % 1 - - def test_unary_ops(self): - x = +1 - x = -1 - x = ~1 - x = ~1 ^ 1 & 1 | 1 & 1 ^ -1 - x = -1 * 1 / 1 + 1 * 1 - -1 * 1 - - def test_selectors(self): - ### trailer: '(' [testlist] ')' | '[' subscript ']' | '.' NAME - ### subscript: expr | [expr] ':' [expr] - - import sys, time - c = sys.path[0] - x = time.time() - x = sys.modules['time'].time() - a = '01234' - c = a[0] - c = a[-1] - s = a[0:5] - s = a[:5] - s = a[0:] - s = a[:] - s = a[-5:] - s = a[:-1] - s = a[-4:-3] - # A rough test of SF bug 1333982. http://python.org/sf/1333982 - # The testing here is fairly incomplete. - # Test cases should include: commas with 1 and 2 colons - d = {} - d[1] = 1 - d[1,] = 2 - d[1, 2] = 3 - d[1, 2, 3] = 4 - L = list(d) - L.sort(key=lambda x: (type(x).__name__, x)) - self.assertEqual(str(L), '[1, (1,), (1, 2), (1, 2, 3)]') - - def test_atoms(self): - ### atom: '(' [testlist] ')' | '[' [testlist] ']' | '{' [dictsetmaker] '}' | NAME | NUMBER | STRING - ### dictsetmaker: (test ':' test (',' test ':' test)* [',']) | (test (',' test)* [',']) - - x = (1) - x = (1 or 2 or 3) - x = (1 or 2 or 3, 2, 3) - - x = [] - x = [1] - x = [1 or 2 or 3] - x = [1 or 2 or 3, 2, 3] - x = [] - - x = {} - x = {'one': 1} - x = {'one': 1,} - x = {'one' or 'two': 1 or 2} - x = {'one': 1, 'two': 2} - x = {'one': 1, 'two': 2,} - x = {'one': 1, 'two': 2, 'three': 3, 'four': 4, 'five': 5, 'six': 6} - - x = {'one'} - x = {'one', 1,} - x = {'one', 'two', 'three'} - x = {2, 3, 4,} - - x = x - x = 'x' - x = 123 - - ### exprlist: expr (',' expr)* [','] - ### testlist: test (',' test)* [','] - # These have been exercised enough above - - def test_classdef(self): - # 'class' NAME ['(' [testlist] ')'] ':' suite - class B: pass - class B2(): pass - class C1(B): pass - class C2(B): pass - class D(C1, C2, B): pass - class C: - def meth1(self): pass - def meth2(self, arg): pass - def meth3(self, a1, a2): pass - - # decorator: '@' dotted_name [ '(' [arglist] ')' ] NEWLINE - # decorators: decorator+ - # decorated: decorators (classdef | funcdef) - def class_decorator(x): return x - @class_decorator - class G: pass - - def test_dictcomps(self): - # dictorsetmaker: ( (test ':' test (comp_for | - # (',' test ':' test)* [','])) | - # (test (comp_for | (',' test)* [','])) ) - nums = [1, 2, 3] - self.assertEqual({i: i + 1 for i in nums}, {1: 2, 2: 3, 3: 4}) - - def test_listcomps(self): - # list comprehension tests - nums = [1, 2, 3, 4, 5] - strs = ["Apple", "Banana", "Coconut"] - spcs = [" Apple", " Banana ", "Coco nut "] - - self.assertEqual([s.strip() for s in spcs], ['Apple', 'Banana', 'Coco nut']) - self.assertEqual([3 * x for x in nums], [3, 6, 9, 12, 15]) - self.assertEqual([x for x in nums if x > 2], [3, 4, 5]) - self.assertEqual([(i, s) for i in nums for s in strs], - [(1, 'Apple'), (1, 'Banana'), (1, 'Coconut'), - (2, 'Apple'), (2, 'Banana'), (2, 'Coconut'), - (3, 'Apple'), (3, 'Banana'), (3, 'Coconut'), - (4, 'Apple'), (4, 'Banana'), (4, 'Coconut'), - (5, 'Apple'), (5, 'Banana'), (5, 'Coconut')]) - self.assertEqual([(i, s) for i in nums for s in [f for f in strs if "n" in f]], - [(1, 'Banana'), (1, 'Coconut'), (2, 'Banana'), (2, 'Coconut'), - (3, 'Banana'), (3, 'Coconut'), (4, 'Banana'), (4, 'Coconut'), - (5, 'Banana'), (5, 'Coconut')]) - self.assertEqual([(lambda a:[a ** i for i in range(a + 1)])(j) for j in range(5)], - [[1], [1, 1], [1, 2, 4], [1, 3, 9, 27], [1, 4, 16, 64, 256]]) - - def test_in_func(l): - return [0 < x < 3 for x in l if x > 2] - - self.assertEqual(test_in_func(nums), [False, False, False]) - - def test_nested_front(): - self.assertEqual([[y for y in [x, x + 1]] for x in [1, 3, 5]], - [[1, 2], [3, 4], [5, 6]]) - - test_nested_front() - - check_syntax_error(self, "[i, s for i in nums for s in strs]") - check_syntax_error(self, "[x if y]") - - suppliers = [ - (1, "Boeing"), - (2, "Ford"), - (3, "Macdonalds") - ] - - parts = [ - (10, "Airliner"), - (20, "Engine"), - (30, "Cheeseburger") - ] - - suppart = [ - (1, 10), (1, 20), (2, 20), (3, 30) - ] - - x = [ - (sname, pname) - for (sno, sname) in suppliers - for (pno, pname) in parts - for (sp_sno, sp_pno) in suppart - if sno == sp_sno and pno == sp_pno - ] - - self.assertEqual(x, [('Boeing', 'Airliner'), ('Boeing', 'Engine'), ('Ford', 'Engine'), - ('Macdonalds', 'Cheeseburger')]) - - def test_genexps(self): - # generator expression tests - g = ([x for x in range(10)] for x in range(1)) - self.assertEqual(next(g), [x for x in range(10)]) - try: - next(g) - self.fail('should produce StopIteration exception') - except StopIteration: - pass - - a = 1 - try: - g = (a for d in a) - next(g) - self.fail('should produce TypeError') - except TypeError: - pass - - self.assertEqual(list((x, y) for x in 'abcd' for y in 'abcd'), [(x, y) for x in 'abcd' for y in 'abcd']) - self.assertEqual(list((x, y) for x in 'ab' for y in 'xy'), [(x, y) for x in 'ab' for y in 'xy']) - - a = [x for x in range(10)] - b = (x for x in (y for y in a)) - self.assertEqual(sum(b), sum([x for x in range(10)])) - - self.assertEqual(sum(x ** 2 for x in range(10)), sum([x ** 2 for x in range(10)])) - self.assertEqual(sum(x * x for x in range(10) if x % 2), sum([x * x for x in range(10) if x % 2])) - self.assertEqual(sum(x for x in (y for y in range(10))), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in (y for y in (z for z in range(10)))), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in [y for y in (z for z in range(10))]), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True)) if True), sum([x for x in range(10)])) - self.assertEqual(sum(x for x in (y for y in (z for z in range(10) if True) if False) if True), 0) - check_syntax_error(self, "foo(x for x in range(10), 100)") - check_syntax_error(self, "foo(100, x for x in range(10))") - - def test_comprehension_specials(self): - # test for outmost iterable precomputation - x = 10; g = (i for i in range(x)); x = 5 - self.assertEqual(len(list(g)), 10) - - # This should hold, since we're only precomputing outmost iterable. - x = 10; t = False; g = ((i, j) for i in range(x) if t for j in range(x)) - x = 5; t = True; - self.assertEqual([(i, j) for i in range(10) for j in range(5)], list(g)) - - # Grammar allows multiple adjacent 'if's in listcomps and genexps, - # even though it's silly. Make sure it works (ifelse broke this.) - self.assertEqual([x for x in range(10) if x % 2 if x % 3], [1, 5, 7]) - self.assertEqual(list(x for x in range(10) if x % 2 if x % 3), [1, 5, 7]) - - # verify unpacking single element tuples in listcomp/genexp. - self.assertEqual([x for x, in [(4,), (5,), (6,)]], [4, 5, 6]) - self.assertEqual(list(x for x, in [(7,), (8,), (9,)]), [7, 8, 9]) - - def test_with_statement(self): - class manager(object): - def __enter__(self): - return (1, 2) - def __exit__(self, *args): - pass - - with manager(): - pass - with manager() as x: - pass - with manager() as (x, y): - pass - with manager(), manager(): - pass - with manager() as x, manager() as y: - pass - with manager() as x, manager(): - pass - - def test_if_else_expr(self): - # Test ifelse expressions in various cases - def _checkeval(msg, ret): - "helper to check that evaluation of expressions is done correctly" - print(msg) - return ret - - # the next line is not allowed anymore - #self.assertEqual([ x() for x in lambda: True, lambda: False if x() ], [True]) - self.assertEqual([x() for x in (lambda:True, lambda:False) if x()], [True]) - self.assertEqual([x(False) for x in (lambda x:False if x else True, lambda x:True if x else False) if x(False)], [True]) - self.assertEqual((5 if 1 else _checkeval("check 1", 0)), 5) - self.assertEqual((_checkeval("check 2", 0) if 0 else 5), 5) - self.assertEqual((5 and 6 if 0 else 1), 1) - self.assertEqual(((5 and 6) if 0 else 1), 1) - self.assertEqual((5 and (6 if 1 else 1)), 6) - self.assertEqual((0 or _checkeval("check 3", 2) if 0 else 3), 3) - self.assertEqual((1 or _checkeval("check 4", 2) if 1 else _checkeval("check 5", 3)), 1) - self.assertEqual((0 or 5 if 1 else _checkeval("check 6", 3)), 5) - self.assertEqual((not 5 if 1 else 1), False) - self.assertEqual((not 5 if 0 else 1), 1) - self.assertEqual((6 + 1 if 1 else 2), 7) - self.assertEqual((6 - 1 if 1 else 2), 5) - self.assertEqual((6 * 2 if 1 else 4), 12) - self.assertEqual((6 / 2 if 1 else 3), 3) - self.assertEqual((6 < 4 if 0 else 2), 2) - - def test_paren_evaluation(self): - self.assertEqual(16 // (4 // 2), 8) - self.assertEqual((16 // 4) // 2, 2) - self.assertEqual(16 // 4 // 2, 2) - self.assertTrue(False is (2 is 3)) - self.assertFalse((False is 2) is 3) - self.assertFalse(False is 2 is 3) - - def test_matrix_mul(self): - # This is not intended to be a comprehensive test, rather just to be few - # samples of the @ operator in test_grammar.py. - class M: - def __matmul__(self, o): - return 4 - def __imatmul__(self, o): - self.other = o - return self - m = M() - self.assertEqual(m @ m, 4) - m @= 42 - self.assertEqual(m.other, 42) - - def test_async_await(self): - async def test(): - def sum(): - pass - if 1: - await someobj() - - self.assertEqual(test.__name__, 'test') - self.assertTrue(bool(test.__code__.co_flags & inspect.CO_COROUTINE)) - - def decorator(func): - setattr(func, '_marked', True) - return func - - @decorator - async def test2(): - return 22 - self.assertTrue(test2._marked) - self.assertEqual(test2.__name__, 'test2') - self.assertTrue(bool(test2.__code__.co_flags & inspect.CO_COROUTINE)) - - def test_async_for(self): - class Done(Exception): pass - - class AIter: - def __aiter__(self): - return self - async def __anext__(self): - raise StopAsyncIteration - - async def foo(): - async for i in AIter(): - pass - async for i, j in AIter(): - pass - async for i in AIter(): - pass - else: - pass - raise Done - - with self.assertRaises(Done): - foo().send(None) - - def test_async_with(self): - class Done(Exception): pass - - class manager: - async def __aenter__(self): - return (1, 2) - async def __aexit__(self, *exc): - return False - - async def foo(): - async with manager(): - pass - async with manager() as x: - pass - async with manager() as (x, y): - pass - async with manager(), manager(): - pass - async with manager() as x, manager() as y: - pass - async with manager() as x, manager(): - pass - raise Done - - with self.assertRaises(Done): - foo().send(None) - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/formatting/yapf.output b/src/test/pythonFiles/formatting/yapf.output deleted file mode 100644 index 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 | - +-----------------+ - | <size> | - +----+------------+ - | | AIFC | - | +------------+ - | | <chunks> | - | | . | - | | . | - | | . | - +----+------------+ - -An AIFF file has the string "AIFF" instead of "AIFC". - -A chunk consists of an identifier (4 bytes) followed by a size (4 bytes, -big endian order), followed by the data. The size field does not include -the size of the 8 byte header. - -The following chunk types are recognized. - - FVER - <version number of AIFF-C defining document> (AIFF-C only). - MARK - <# of markers> (2 bytes) - list of markers: - <marker ID> (2 bytes, must be > 0) - <position> (4 bytes) - <marker name> ("pstring") - COMM - <# of channels> (2 bytes) - <# of sound frames> (4 bytes) - <size of the samples> (2 bytes) - <sampling frequency> (10 bytes, IEEE 80-bit extended - floating point) - in AIFF-C files only: - <compression type> (4 bytes) - <human-readable version of compression type> ("pstring") - SSND - <offset> (4 bytes, not used by this program) - <blocksize> (4 bytes, not used by this program) - <sound data> - -A pstring consists of 1 byte length, a string of characters, and 0 or 1 -byte pad to make the total length even. - -Usage. - -Reading AIFF files: - f = aifc.open(file, 'r') -where file is either the name of a file or an open file pointer. -The open file pointer must have methods read(), seek(), and close(). -In some types of audio files, if the setpos() method is not used, -the seek() method is not necessary. - -This returns an instance of a class with the following public methods: - getnchannels() -- returns number of audio channels (1 for - mono, 2 for stereo) - getsampwidth() -- returns sample width in bytes - getframerate() -- returns sampling frequency - getnframes() -- returns number of audio frames - getcomptype() -- returns compression type ('NONE' for AIFF files) - getcompname() -- returns human-readable version of - compression type ('not compressed' for AIFF files) - getparams() -- returns a tuple consisting of all of the - above in the above order - getmarkers() -- get the list of marks in the audio file or None - if there are no marks - getmark(id) -- get mark with the specified id (raises an error - if the mark does not exist) - readframes(n) -- returns at most n frames of audio - rewind() -- rewind to the beginning of the audio stream - setpos(pos) -- seek to the specified position - tell() -- return the current position - close() -- close the instance (make it unusable) -The position returned by tell(), the position given to setpos() and -the position of marks are all compatible and have nothing to do with -the actual position in the file. -The close() method is called automatically when the class instance -is destroyed. - -Writing AIFF files: - f = aifc.open(file, 'w') -where file is either the name of a file or an open file pointer. -The open file pointer must have methods write(), tell(), seek(), and -close(). - -This returns an instance of a class with the following public methods: - aiff() -- create an AIFF file (AIFF-C default) - aifc() -- create an AIFF-C file - setnchannels(n) -- set the number of channels - setsampwidth(n) -- set the sample width - setframerate(n) -- set the frame rate - setnframes(n) -- set the number of frames - setcomptype(type, name) - -- set the compression type and the - human-readable compression type - setparams(tuple) - -- set all parameters at once - setmark(id, pos, name) - -- add specified mark to the list of marks - tell() -- return current position in output file (useful - in combination with setmark()) - writeframesraw(data) - -- write audio frames without pathing up the - file header - writeframes(data) - -- write audio frames and patch up the file header - close() -- patch up the file header and close the - output file -You should set the parameters before the first writeframesraw or -writeframes. The total number of frames does not need to be set, -but when it is set to the correct value, the header does not have to -be patched up. -It is best to first set all parameters, perhaps possibly the -compression type, and then write audio frames using writeframesraw. -When all frames have been written, either call writeframes('') or -close() to patch up the sizes in the header. -Marks can be added anytime. If there are any marks, you must call -close() after all frames have been written. -The close() method is called automatically when the class instance -is destroyed. - -When a file is opened with the extension '.aiff', an AIFF file is -written, otherwise an AIFF-C file is written. This default can be -changed by calling aiff() or aifc() before the first writeframes or -writeframesraw. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/anydbm.md b/src/test/pythonFiles/markdown/anydbm.md deleted file mode 100644 index e5914dcbadde..000000000000 --- a/src/test/pythonFiles/markdown/anydbm.md +++ /dev/null @@ -1,33 +0,0 @@ -Generic interface to all dbm clones. - -Instead of -```html - import dbm - d = dbm.open(file, 'w', 0666) -``` -use -```html - import anydbm - d = anydbm.open(file, 'w') -``` -The returned object is a dbhash, gdbm, dbm or dumbdbm object, -dependent on the type of database being opened (determined by whichdb -module) in the case of an existing dbm. If the dbm does not exist and -the create or new flag ('c' or 'n') was specified, the dbm type will -be determined by the availability of the modules (tested in the above -order). - -It has the following interface (key and data are strings): -```html - d[key] = data # store data at key (may override data at - # existing key) - data = d[key] # retrieve data at key (raise KeyError if no - # such key) - del d[key] # delete data stored at key (raises KeyError - # if no such key) - flag = key in d # true if the key exists - list = d.keys() # return a list of all existing keys (slow!) -``` -Future versions may change the order in which implementations are -tested for existence, and add interfaces to other dbm-like -implementations. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/anydbm.pydoc b/src/test/pythonFiles/markdown/anydbm.pydoc deleted file mode 100644 index 2d46b5881789..000000000000 --- a/src/test/pythonFiles/markdown/anydbm.pydoc +++ /dev/null @@ -1,33 +0,0 @@ -Generic interface to all dbm clones. - -Instead of - - import dbm - d = dbm.open(file, 'w', 0666) - -use - - import anydbm - d = anydbm.open(file, 'w') - -The returned object is a dbhash, gdbm, dbm or dumbdbm object, -dependent on the type of database being opened (determined by whichdb -module) in the case of an existing dbm. If the dbm does not exist and -the create or new flag ('c' or 'n') was specified, the dbm type will -be determined by the availability of the modules (tested in the above -order). - -It has the following interface (key and data are strings): - - d[key] = data # store data at key (may override data at - # existing key) - data = d[key] # retrieve data at key (raise KeyError if no - # such key) - del d[key] # delete data stored at key (raises KeyError - # if no such key) - flag = key in d # true if the key exists - list = d.keys() # return a list of all existing keys (slow!) - -Future versions may change the order in which implementations are -tested for existence, and add interfaces to other dbm-like -implementations. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/astroid.md b/src/test/pythonFiles/markdown/astroid.md deleted file mode 100644 index b5ece21c1faf..000000000000 --- a/src/test/pythonFiles/markdown/astroid.md +++ /dev/null @@ -1,24 +0,0 @@ -Python Abstract Syntax Tree New Generation - -The aim of this module is to provide a common base representation of -python source code for projects such as pychecker, pyreverse, -pylint... Well, actually the development of this library is essentially -governed by pylint's needs. - -It extends class defined in the python's \_ast module with some -additional methods and attributes. Instance attributes are added by a -builder object, which can either generate extended ast (let's call -them astroid ;) by visiting an existent ast tree or by inspecting living -object. Methods are added by monkey patching ast classes. - -Main modules are: -```html -* nodes and scoped_nodes for more information about methods and - attributes added to different node classes - -* the manager contains a high level object to get astroid trees from - source files and living objects. It maintains a cache of previously - constructed tree for quick access - -* builder contains the class responsible to build astroid trees -``` \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/astroid.pydoc b/src/test/pythonFiles/markdown/astroid.pydoc deleted file mode 100644 index 84d58487ead5..000000000000 --- a/src/test/pythonFiles/markdown/astroid.pydoc +++ /dev/null @@ -1,23 +0,0 @@ -Python Abstract Syntax Tree New Generation - -The aim of this module is to provide a common base representation of -python source code for projects such as pychecker, pyreverse, -pylint... Well, actually the development of this library is essentially -governed by pylint's needs. - -It extends class defined in the python's _ast module with some -additional methods and attributes. Instance attributes are added by a -builder object, which can either generate extended ast (let's call -them astroid ;) by visiting an existent ast tree or by inspecting living -object. Methods are added by monkey patching ast classes. - -Main modules are: - -* nodes and scoped_nodes for more information about methods and - attributes added to different node classes - -* the manager contains a high level object to get astroid trees from - source files and living objects. It maintains a cache of previously - constructed tree for quick access - -* builder contains the class responsible to build astroid trees \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.md b/src/test/pythonFiles/markdown/scipy.md deleted file mode 100644 index d28c1e290abe..000000000000 --- a/src/test/pythonFiles/markdown/scipy.md +++ /dev/null @@ -1,47 +0,0 @@ -### SciPy: A scientific computing package for Python - -Documentation is available in the docstrings and -online at https://docs.scipy.org. - -#### Contents -SciPy imports all the functions from the NumPy namespace, and in -addition provides: - -#### Subpackages -Using any of these subpackages requires an explicit import. For example, -`import scipy.cluster`. -```html - cluster --- Vector Quantization / Kmeans - fftpack --- Discrete Fourier Transform algorithms - integrate --- Integration routines - interpolate --- Interpolation Tools - io --- Data input and output - linalg --- Linear algebra routines - linalg.blas --- Wrappers to BLAS library - linalg.lapack --- Wrappers to LAPACK library - misc --- Various utilities that don't have - another home. - ndimage --- n-dimensional image package - odr --- Orthogonal Distance Regression - optimize --- Optimization Tools - signal --- Signal Processing Tools - sparse --- Sparse Matrices - sparse.linalg --- Sparse Linear Algebra - sparse.linalg.dsolve --- Linear Solvers - sparse.linalg.dsolve.umfpack --- :Interface to the UMFPACK library: - Conjugate Gradient Method (LOBPCG) - sparse.linalg.eigen --- Sparse Eigenvalue Solvers - sparse.linalg.eigen.lobpcg --- Locally Optimal Block Preconditioned - Conjugate Gradient Method (LOBPCG) - spatial --- Spatial data structures and algorithms - special --- Special functions - stats --- Statistical Functions -``` -#### Utility tools -```html - test --- Run scipy unittests - show_config --- Show scipy build configuration - show_numpy_config --- Show numpy build configuration - __version__ --- Scipy version string - __numpy_version__ --- Numpy version string -``` \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.pydoc b/src/test/pythonFiles/markdown/scipy.pydoc deleted file mode 100644 index 293445fbea5b..000000000000 --- a/src/test/pythonFiles/markdown/scipy.pydoc +++ /dev/null @@ -1,53 +0,0 @@ -SciPy: A scientific computing package for Python -================================================ - -Documentation is available in the docstrings and -online at https://docs.scipy.org. - -Contents --------- -SciPy imports all the functions from the NumPy namespace, and in -addition provides: - -Subpackages ------------ -Using any of these subpackages requires an explicit import. For example, -``import scipy.cluster``. - -:: - - cluster --- Vector Quantization / Kmeans - fftpack --- Discrete Fourier Transform algorithms - integrate --- Integration routines - interpolate --- Interpolation Tools - io --- Data input and output - linalg --- Linear algebra routines - linalg.blas --- Wrappers to BLAS library - linalg.lapack --- Wrappers to LAPACK library - misc --- Various utilities that don't have - another home. - ndimage --- n-dimensional image package - odr --- Orthogonal Distance Regression - optimize --- Optimization Tools - signal --- Signal Processing Tools - sparse --- Sparse Matrices - sparse.linalg --- Sparse Linear Algebra - sparse.linalg.dsolve --- Linear Solvers - sparse.linalg.dsolve.umfpack --- :Interface to the UMFPACK library: - Conjugate Gradient Method (LOBPCG) - sparse.linalg.eigen --- Sparse Eigenvalue Solvers - sparse.linalg.eigen.lobpcg --- Locally Optimal Block Preconditioned - Conjugate Gradient Method (LOBPCG) - spatial --- Spatial data structures and algorithms - special --- Special functions - stats --- Statistical Functions - -Utility tools -------------- -:: - - test --- Run scipy unittests - show_config --- Show scipy build configuration - show_numpy_config --- Show numpy build configuration - __version__ --- Scipy version string - __numpy_version__ --- Numpy version string \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.spatial.distance.md b/src/test/pythonFiles/markdown/scipy.spatial.distance.md deleted file mode 100644 index 276acddef787..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.distance.md +++ /dev/null @@ -1,54 +0,0 @@ -### Distance computations (module:`scipy.spatial.distance`) - - -#### Function Reference - -Distance matrix computation from a collection of raw observation vectors -stored in a rectangular array. -```html - pdist -- pairwise distances between observation vectors. - cdist -- distances between two collections of observation vectors - squareform -- convert distance matrix to a condensed one and vice versa - directed_hausdorff -- directed Hausdorff distance between arrays -``` -Predicates for checking the validity of distance matrices, both -condensed and redundant. Also contained in this module are functions -for computing the number of observations in a distance matrix. -```html - is_valid_dm -- checks for a valid distance matrix - is_valid_y -- checks for a valid condensed distance matrix - num_obs_dm -- # of observations in a distance matrix - num_obs_y -- # of observations in a condensed distance matrix -``` -Distance functions between two numeric vectors `u` and `v`. Computing -distances over a large collection of vectors is inefficient for these -functions. Use `pdist` for this purpose. -```html - braycurtis -- the Bray-Curtis distance. - canberra -- the Canberra distance. - chebyshev -- the Chebyshev distance. - cityblock -- the Manhattan distance. - correlation -- the Correlation distance. - cosine -- the Cosine distance. - euclidean -- the Euclidean distance. - mahalanobis -- the Mahalanobis distance. - minkowski -- the Minkowski distance. - seuclidean -- the normalized Euclidean distance. - sqeuclidean -- the squared Euclidean distance. - wminkowski -- (deprecated) alias of `minkowski`. -``` -Distance functions between two boolean vectors (representing sets) `u` and -`v`. As in the case of numerical vectors, `pdist` is more efficient for -computing the distances between all pairs. -```html - dice -- the Dice dissimilarity. - hamming -- the Hamming distance. - jaccard -- the Jaccard distance. - kulsinski -- the Kulsinski distance. - rogerstanimoto -- the Rogers-Tanimoto dissimilarity. - russellrao -- the Russell-Rao dissimilarity. - sokalmichener -- the Sokal-Michener dissimilarity. - sokalsneath -- the Sokal-Sneath dissimilarity. - yule -- the Yule dissimilarity. -``` -:func:`hamming` also operates over discrete numerical vectors. \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc b/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc deleted file mode 100644 index cfc9b7008b99..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.distance.pydoc +++ /dev/null @@ -1,71 +0,0 @@ - -===================================================== -Distance computations (:mod:`scipy.spatial.distance`) -===================================================== - -.. sectionauthor:: Damian Eads - -Function Reference ------------------- - -Distance matrix computation from a collection of raw observation vectors -stored in a rectangular array. - -.. autosummary:: - :toctree: generated/ - - pdist -- pairwise distances between observation vectors. - cdist -- distances between two collections of observation vectors - squareform -- convert distance matrix to a condensed one and vice versa - directed_hausdorff -- directed Hausdorff distance between arrays - -Predicates for checking the validity of distance matrices, both -condensed and redundant. Also contained in this module are functions -for computing the number of observations in a distance matrix. - -.. autosummary:: - :toctree: generated/ - - is_valid_dm -- checks for a valid distance matrix - is_valid_y -- checks for a valid condensed distance matrix - num_obs_dm -- # of observations in a distance matrix - num_obs_y -- # of observations in a condensed distance matrix - -Distance functions between two numeric vectors ``u`` and ``v``. Computing -distances over a large collection of vectors is inefficient for these -functions. Use ``pdist`` for this purpose. - -.. autosummary:: - :toctree: generated/ - - braycurtis -- the Bray-Curtis distance. - canberra -- the Canberra distance. - chebyshev -- the Chebyshev distance. - cityblock -- the Manhattan distance. - correlation -- the Correlation distance. - cosine -- the Cosine distance. - euclidean -- the Euclidean distance. - mahalanobis -- the Mahalanobis distance. - minkowski -- the Minkowski distance. - seuclidean -- the normalized Euclidean distance. - sqeuclidean -- the squared Euclidean distance. - wminkowski -- (deprecated) alias of `minkowski`. - -Distance functions between two boolean vectors (representing sets) ``u`` and -``v``. As in the case of numerical vectors, ``pdist`` is more efficient for -computing the distances between all pairs. - -.. autosummary:: - :toctree: generated/ - - dice -- the Dice dissimilarity. - hamming -- the Hamming distance. - jaccard -- the Jaccard distance. - kulsinski -- the Kulsinski distance. - rogerstanimoto -- the Rogers-Tanimoto dissimilarity. - russellrao -- the Russell-Rao dissimilarity. - sokalmichener -- the Sokal-Michener dissimilarity. - sokalsneath -- the Sokal-Sneath dissimilarity. - yule -- the Yule dissimilarity. - -:func:`hamming` also operates over discrete numerical vectors. diff --git a/src/test/pythonFiles/markdown/scipy.spatial.md b/src/test/pythonFiles/markdown/scipy.spatial.md deleted file mode 100644 index 2d5e891db625..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.md +++ /dev/null @@ -1,65 +0,0 @@ -### Spatial algorithms and data structures (module:`scipy.spatial`) - - -### Nearest-neighbor Queries -```html - KDTree -- class for efficient nearest-neighbor queries - cKDTree -- class for efficient nearest-neighbor queries (faster impl.) - distance -- module containing many different distance measures - Rectangle -``` -### Delaunay Triangulation, Convex Hulls and Voronoi Diagrams -```html - Delaunay -- compute Delaunay triangulation of input points - ConvexHull -- compute a convex hull for input points - Voronoi -- compute a Voronoi diagram hull from input points - SphericalVoronoi -- compute a Voronoi diagram from input points on the surface of a sphere - HalfspaceIntersection -- compute the intersection points of input halfspaces -``` -### Plotting Helpers -```html - delaunay_plot_2d -- plot 2-D triangulation - convex_hull_plot_2d -- plot 2-D convex hull - voronoi_plot_2d -- plot 2-D voronoi diagram -``` -### Simplex representation -The simplices (triangles, tetrahedra, ...) appearing in the Delaunay -tesselation (N-dim simplices), convex hull facets, and Voronoi ridges -(N-1 dim simplices) are represented in the following scheme: -```html - tess = Delaunay(points) - hull = ConvexHull(points) - voro = Voronoi(points) - - # coordinates of the j-th vertex of the i-th simplex - tess.points[tess.simplices[i, j], :] # tesselation element - hull.points[hull.simplices[i, j], :] # convex hull facet - voro.vertices[voro.ridge_vertices[i, j], :] # ridge between Voronoi cells -``` -For Delaunay triangulations and convex hulls, the neighborhood -structure of the simplices satisfies the condition: -```html - `tess.neighbors[i,j]` is the neighboring simplex of the i-th - simplex, opposite to the j-vertex. It is -1 in case of no - neighbor. -``` -Convex hull facets also define a hyperplane equation: -```html - (hull.equations[i,:-1] * coord).sum() + hull.equations[i,-1] == 0 -``` -Similar hyperplane equations for the Delaunay triangulation correspond -to the convex hull facets on the corresponding N+1 dimensional -paraboloid. - -The Delaunay triangulation objects offer a method for locating the -simplex containing a given point, and barycentric coordinate -computations. - -#### Functions -```html - tsearch - distance_matrix - minkowski_distance - minkowski_distance_p - procrustes -``` \ No newline at end of file diff --git a/src/test/pythonFiles/markdown/scipy.spatial.pydoc b/src/test/pythonFiles/markdown/scipy.spatial.pydoc deleted file mode 100644 index 1613b94384b7..000000000000 --- a/src/test/pythonFiles/markdown/scipy.spatial.pydoc +++ /dev/null @@ -1,86 +0,0 @@ -============================================================= -Spatial algorithms and data structures (:mod:`scipy.spatial`) -============================================================= - -.. currentmodule:: scipy.spatial - -Nearest-neighbor Queries -======================== -.. autosummary:: - :toctree: generated/ - - KDTree -- class for efficient nearest-neighbor queries - cKDTree -- class for efficient nearest-neighbor queries (faster impl.) - distance -- module containing many different distance measures - Rectangle - -Delaunay Triangulation, Convex Hulls and Voronoi Diagrams -========================================================= - -.. autosummary:: - :toctree: generated/ - - Delaunay -- compute Delaunay triangulation of input points - ConvexHull -- compute a convex hull for input points - Voronoi -- compute a Voronoi diagram hull from input points - SphericalVoronoi -- compute a Voronoi diagram from input points on the surface of a sphere - HalfspaceIntersection -- compute the intersection points of input halfspaces - -Plotting Helpers -================ - -.. autosummary:: - :toctree: generated/ - - delaunay_plot_2d -- plot 2-D triangulation - convex_hull_plot_2d -- plot 2-D convex hull - voronoi_plot_2d -- plot 2-D voronoi diagram - -.. seealso:: :ref:`Tutorial <qhulltutorial>` - - -Simplex representation -====================== -The simplices (triangles, tetrahedra, ...) appearing in the Delaunay -tesselation (N-dim simplices), convex hull facets, and Voronoi ridges -(N-1 dim simplices) are represented in the following scheme:: - - tess = Delaunay(points) - hull = ConvexHull(points) - voro = Voronoi(points) - - # coordinates of the j-th vertex of the i-th simplex - tess.points[tess.simplices[i, j], :] # tesselation element - hull.points[hull.simplices[i, j], :] # convex hull facet - voro.vertices[voro.ridge_vertices[i, j], :] # ridge between Voronoi cells - -For Delaunay triangulations and convex hulls, the neighborhood -structure of the simplices satisfies the condition: - - ``tess.neighbors[i,j]`` is the neighboring simplex of the i-th - simplex, opposite to the j-vertex. It is -1 in case of no - neighbor. - -Convex hull facets also define a hyperplane equation:: - - (hull.equations[i,:-1] * coord).sum() + hull.equations[i,-1] == 0 - -Similar hyperplane equations for the Delaunay triangulation correspond -to the convex hull facets on the corresponding N+1 dimensional -paraboloid. - -The Delaunay triangulation objects offer a method for locating the -simplex containing a given point, and barycentric coordinate -computations. - -Functions ---------- - -.. autosummary:: - :toctree: generated/ - - tsearch - distance_matrix - minkowski_distance - minkowski_distance_p - procrustes \ No newline at end of file diff --git a/src/test/pythonFiles/refactoring/source folder/with empty line.py b/src/test/pythonFiles/refactoring/source folder/with empty line.py deleted file mode 100644 index 01ed75727900..000000000000 --- a/src/test/pythonFiles/refactoring/source folder/with empty line.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - -def one(): - return True - -def two(): - if one(): - print("A" + one()) diff --git a/src/test/pythonFiles/refactoring/source folder/without empty line.py b/src/test/pythonFiles/refactoring/source folder/without empty line.py deleted file mode 100644 index a449eb106f5c..000000000000 --- a/src/test/pythonFiles/refactoring/source folder/without empty line.py +++ /dev/null @@ -1,8 +0,0 @@ -import os - -def one(): - return True - -def two(): - if one(): - print("A" + one()) \ No newline at end of file diff --git a/src/test/pythonFiles/refactoring/standAlone/refactor.py b/src/test/pythonFiles/refactoring/standAlone/refactor.py deleted file mode 100644 index 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.plugins.capture.Capture object at 0x10de9cf98>, <nose.plugins.logcapture.LogCapture object at 0x10dd8a3c8>, <nose.plugins.deprecated.Deprecated object at 0x10df5beb8>, <nose.plugins.skip.Skip object at 0x10dfa6908>, <nose.plugins.collect.CollectOnly object at 0x10e0731d0>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10e073cc0: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test_', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10cf0b470>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10cee6518>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test_'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10d556780> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4530410048)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4530410048)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e089898>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e089588>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 3 tests in 0.022s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/four.output b/src/test/pythonFiles/testFiles/noseFiles/four.output deleted file mode 100644 index 511f0b1c863c..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/four.output +++ /dev/null @@ -1,205 +0,0 @@ -nose.config: INFO: Set working dir to /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x105fd4048>, <nose.plugins.logcapture.LogCapture object at 0x105ebd470>, <nose.plugins.deprecated.Deprecated object at 0x106077a58>, <nose.plugins.skip.Skip object at 0x1060d9828>, <nose.plugins.collect.CollectOnly object at 0x1061a6208>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1061a6cf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': ['specific'], 'py3where': None, 'testMatch': 'tst', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x1054dc4a8>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x1054dc4e0>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('tst'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1056897f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4397453944)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4397453944)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py module tst_unittest_one call None -nose.importer: DEBUG: Import tst_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.importer: DEBUG: find module part tst_unittest_one (tst_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific'] -nose.loader: DEBUG: Load from module <module 'tst_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'tst_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'tst_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'tst_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'tst_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tst_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tst_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1061bd978>) -nose.plugins.collect: DEBUG: Add test tst_A (tst_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test tst_B (tst_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1061bd9b0>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<tst_unittest_one.Test_test1 testMethod=tst_A>), Test(<tst_unittest_one.Test_test1 testMethod=tst_B>)]> -nose.plugins.collect: DEBUG: Preparing test case tst_A (tst_unittest_one.Test_test1) -tst_A (tst_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case tst_B (tst_unittest_one.Test_test1) -tst_B (tst_unittest_one.Test_test1) ... ok -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py module tst_unittest_two call None -nose.importer: DEBUG: Import tst_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific -nose.importer: DEBUG: find module part tst_unittest_two (tst_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific'] -nose.loader: DEBUG: Load from module <module 'tst_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'tst_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'tst_unittest_two.Tst_test2'>? True -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'tst_unittest_two.Tst_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'tst_unittest_two.Tst_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Tst_test2.tst_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1061bdfd0>) -nose.plugins.collect: DEBUG: Add test tst_A2 (tst_unittest_two.Tst_test2) -nose.plugins.collect: DEBUG: Add test tst_B2 (tst_unittest_two.Tst_test2) -nose.plugins.collect: DEBUG: Add test tst_C2 (tst_unittest_two.Tst_test2) -nose.plugins.collect: DEBUG: Add test tst_D2 (tst_unittest_two.Tst_test2) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1061bd518>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<tst_unittest_two.Tst_test2 testMethod=tst_A2>), Test(<tst_unittest_two.Tst_test2 testMethod=tst_B2>), Test(<tst_unittest_two.Tst_test2 testMethod=tst_C2>), Test(<tst_unittest_two.Tst_test2 testMethod=tst_D2>)]> -nose.plugins.collect: DEBUG: Preparing test case tst_A2 (tst_unittest_two.Tst_test2) -tst_A2 (tst_unittest_two.Tst_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case tst_B2 (tst_unittest_two.Tst_test2) -tst_B2 (tst_unittest_two.Tst_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case tst_C2 (tst_unittest_two.Tst_test2) -tst_C2 (tst_unittest_two.Tst_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case tst_D2 (tst_unittest_two.Tst_test2) -tst_D2 (tst_unittest_two.Tst_test2) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 6 tests in 0.033s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/one.output b/src/test/pythonFiles/testFiles/noseFiles/one.output deleted file mode 100644 index cafdaf5b906a..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/one.output +++ /dev/null @@ -1,211 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x108249f98>, <nose.plugins.logcapture.LogCapture object at 0x108139390>, <nose.plugins.deprecated.Deprecated object at 0x108307e80>, <nose.plugins.skip.Skip object at 0x108354908>, <nose.plugins.collect.CollectOnly object at 0x108423198>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x108423c88: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': '(?:^|[\\b_\\./-])[Tt]est', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x107758438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x107296390>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('(?:^|[\\b_\\./-])[Tt]est'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x107905780> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4433617416)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4433617416)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10843a748>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10843a898>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x1083f38e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py module test_one call None -nose.importer: DEBUG: Import test_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: find module part test_one (test_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests'] -nose.loader: DEBUG: Load from module <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'> -nose.selector: DEBUG: wantModule <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10843a898>) -nose.plugins.collect: DEBUG: Add test test_A (test_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10843a898>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.plugins.collect: DEBUG: Preparing test case test_A (test_one.Test_test1) -test_A (test_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_one.Test_test1) -test_B (test_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_one.Test_test1) -test_c (test_one.Test_test1) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 6 tests in 0.023s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.five.output b/src/test/pythonFiles/testFiles/noseFiles/run.five.output deleted file mode 100644 index 640132ffe72e..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.five.output +++ /dev/null @@ -1,567 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10b97a048>, <nose.plugins.logcapture.LogCapture object at 0x10b863400>, <nose.plugins.deprecated.Deprecated object at 0x10ba32f28>, <nose.plugins.skip.Skip object at 0x10ba7f978>, <nose.plugins.collect.CollectOnly object at 0x10bb4d240>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10bb4dd30: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10ae81470>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10ae81518>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10b02f7f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4491453048)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4491453048)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.four.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.result? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb628d0>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb62630>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10bb218e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None -nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> -nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True -nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb62e10>) -nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) -nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb62c50>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None -nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb7e4e0>) -nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb62d30>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None -nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb7eba8>) -nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb7eba8>) -nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb7eb38>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None -nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> -nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True -nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10bb89588>) -nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10bb7eac8>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) -test4A (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) -test4B (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) -test_A (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) -test_B (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) -test_c (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) -test_A2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) -test_B2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) -test_C2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) -test_D2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) -test_222A2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) -test_222B2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) -test_A (unittest_three_test.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) -test_B (unittest_three_test.Test_test3) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 16 tests in 0.048s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.five.result b/src/test/pythonFiles/testFiles/noseFiles/run.five.result deleted file mode 100644 index 97c7e0e0216f..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.five.result +++ /dev/null @@ -1,11 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="1" errors="0" failures="1" skip="0"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.four.output b/src/test/pythonFiles/testFiles/noseFiles/run.four.output deleted file mode 100644 index aa01067d7925..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.four.output +++ /dev/null @@ -1,565 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10e107048>, <nose.plugins.logcapture.LogCapture object at 0x10dfef400>, <nose.plugins.deprecated.Deprecated object at 0x10e1bef98>, <nose.plugins.skip.Skip object at 0x10e20b860>, <nose.plugins.collect.CollectOnly object at 0x10e2d9208>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10e2d9cf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10d60d470>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10d60d518>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10d7bb7f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4532920952)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4532920952)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.three.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.result? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e2ee8d0>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e2ee630>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10e2ad8e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None -nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> -nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True -nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e2eee10>) -nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) -nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e2eec50>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None -nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e30a4e0>) -nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e2eed30>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None -nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e30aba8>) -nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e30aba8>) -nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e30ab38>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None -nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> -nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True -nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10e315588>) -nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10e30aac8>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) -test4A (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) -test4B (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) -test_A (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) -test_B (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) -test_c (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) -test_A2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) -test_B2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) -test_C2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) -test_D2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) -test_222A2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) -test_222B2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) -test_A (unittest_three_test.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) -test_B (unittest_three_test.Test_test3) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 16 tests in 0.061s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.four.result b/src/test/pythonFiles/testFiles/noseFiles/run.four.result deleted file mode 100644 index 828e4a74b06a..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.four.result +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="3" errors="0" failures="1" skip="1"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping -]]></skipped></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.one.output b/src/test/pythonFiles/testFiles/noseFiles/run.one.output deleted file mode 100644 index 475ac92d3bb4..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.one.output +++ /dev/null @@ -1,558 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x105a14048>, <nose.plugins.logcapture.LogCapture object at 0x1058fb400>, <nose.plugins.deprecated.Deprecated object at 0x105acaf98>, <nose.plugins.skip.Skip object at 0x105b179e8>, <nose.plugins.collect.CollectOnly object at 0x105be4208>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x105be4cf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x104f1a128>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x104a7c438>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1050c77f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4391412344)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4391412344)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105bfa8d0>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105bfa630>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x105bb68e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None -nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> -nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True -nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105bfae10>) -nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) -nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105bfac50>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None -nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c164e0>) -nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105bfad30>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None -nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c16ba8>) -nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c16ba8>) -nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105c16b38>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None -nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> -nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True -nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x105c21588>) -nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x105c16ac8>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) -test4A (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) -test4B (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) -test_A (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) -test_B (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) -test_c (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) -test_A2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) -test_B2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) -test_C2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) -test_D2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) -test_222A2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) -test_222B2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) -test_A (unittest_three_test.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) -test_B (unittest_three_test.Test_test3) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 16 tests in 0.048s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.one.result b/src/test/pythonFiles/testFiles/noseFiles/run.one.result deleted file mode 100644 index 59de2cfcdcc8..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.one.result +++ /dev/null @@ -1,83 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="16" errors="1" failures="7" skip="2"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping -]]></skipped></testcase><testcase classname="test4.Test_test3" name="test4A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py", line 6, in test4A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test4.Test_test3" name="test4B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py", line 8, in test_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_one.Test_test1" name="test_B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping -]]></skipped></testcase><testcase classname="test_unittest_two.Test_test2" name="test_A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 5, in test_A2 - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_B2" time="0.000"></testcase><testcase classname="test_unittest_two.Test_test2" name="test_C2" time="0.000"><failure type="builtins.AssertionError" message="1 != 2 : Not equal"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 11, in test_C2 - self.assertEqual(1,2,'Not equal') - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 829, in assertEqual - assertion_func(first, second, msg=msg) - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 822, in _baseAssertEqual - raise self.failureException(msg) -AssertionError: 1 != 2 : Not equal -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_D2" time="0.000"><error type="builtins.ArithmeticError" message=""><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 14, in test_D2 - raise ArithmeticError() -ArithmeticError -]]></error></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 19, in test_222A2 - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222B2" time="0.000"></testcase><testcase classname="unittest_three_test.Test_test3" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py", line 6, in test_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="unittest_three_test.Test_test3" name="test_B" time="0.000"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.three.output b/src/test/pythonFiles/testFiles/noseFiles/run.three.output deleted file mode 100644 index da1ec6bc25c9..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.three.output +++ /dev/null @@ -1,563 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x1065d3048>, <nose.plugins.logcapture.LogCapture object at 0x1064ba438>, <nose.plugins.deprecated.Deprecated object at 0x106635fd0>, <nose.plugins.skip.Skip object at 0x1066d7860>, <nose.plugins.collect.CollectOnly object at 0x1066d79b0>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1067a3d30: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x105ad9438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x105ad94e0>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x105c877f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4403733168)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4403733168)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.two.result? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067ba908>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067ba668>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x1067798e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None -nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> -nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True -nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067bae48>) -nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) -nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067bac88>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None -nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067d6518>) -nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067bad68>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None -nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067d6be0>) -nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067d6be0>) -nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067d6b70>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None -nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> -nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True -nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1067e15c0>) -nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1067d6b00>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) -test4A (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) -test4B (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) -test_A (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) -test_B (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) -test_c (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) -test_A2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) -test_B2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) -test_C2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) -test_D2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) -test_222A2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) -test_222B2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) -test_A (unittest_three_test.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) -test_B (unittest_three_test.Test_test3) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 16 tests in 0.047s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.three.result b/src/test/pythonFiles/testFiles/noseFiles/run.three.result deleted file mode 100644 index 828e4a74b06a..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.three.result +++ /dev/null @@ -1,12 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="3" errors="0" failures="1" skip="1"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping -]]></skipped></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result b/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result deleted file mode 100644 index b60e8229c55d..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.two.again.result +++ /dev/null @@ -1,81 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="8" errors="1" failures="7" skip="0"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test4.Test_test3" name="test4A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py", line 6, in test4A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_one.Test_test1" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py", line 8, in test_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 5, in test_A2 - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_C2" time="0.000"><failure type="builtins.AssertionError" message="1 != 2 : Not equal"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 11, in test_C2 - self.assertEqual(1,2,'Not equal') - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 829, in assertEqual - assertion_func(first, second, msg=msg) - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 822, in _baseAssertEqual - raise self.failureException(msg) -AssertionError: 1 != 2 : Not equal -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_D2" time="0.000"><error type="builtins.ArithmeticError" message=""><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 14, in test_D2 - raise ArithmeticError() -ArithmeticError -]]></error></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 19, in test_222A2 - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="unittest_three_test.Test_test3" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py", line 6, in test_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.two.output b/src/test/pythonFiles/testFiles/noseFiles/run.two.output deleted file mode 100644 index 31a5a5e9c34b..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.two.output +++ /dev/null @@ -1,560 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x1041fe048>, <nose.plugins.logcapture.LogCapture object at 0x1040e5438>, <nose.plugins.deprecated.Deprecated object at 0x1042b5fd0>, <nose.plugins.skip.Skip object at 0x104301a58>, <nose.plugins.collect.CollectOnly object at 0x1043d0278>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1043d0d68: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x103704160>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x1031996d8>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1038b17f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4366152424)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4366152424)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/five.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/four.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/run.one.result? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/three.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1043e3940>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1043e3ef0>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x1043a48e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None -nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> -nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True -nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1043e3e48>) -nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) -nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1043e3c88>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None -nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x104401518>) -nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1043e3d68>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None -nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x104401be0>) -nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x104401be0>) -nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x104401b70>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None -nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> -nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True -nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10440c588>) -nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x104401b00>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) -test4A (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) -test4B (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) -test_A (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) -test_B (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) -test_c (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) -test_A2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) -test_B2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) -test_C2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) -test_D2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) -test_222A2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) -test_222B2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) -test_A (unittest_three_test.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) -test_B (unittest_three_test.Test_test3) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 16 tests in 0.137s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/run.two.result b/src/test/pythonFiles/testFiles/noseFiles/run.two.result deleted file mode 100644 index 59de2cfcdcc8..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/run.two.result +++ /dev/null @@ -1,83 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?><testsuite name="nosetests" tests="16" errors="1" failures="7" skip="2"><testcase classname="test_root.Test_Root_test1" name="test_Root_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py", line 8, in test_Root_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_B" time="0.000"></testcase><testcase classname="test_root.Test_Root_test1" name="test_Root_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping -]]></skipped></testcase><testcase classname="test4.Test_test3" name="test4A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py", line 6, in test4A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test4.Test_test3" name="test4B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py", line 8, in test_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_one.Test_test1" name="test_B" time="0.000"></testcase><testcase classname="test_unittest_one.Test_test1" name="test_c" time="0.000"><skipped type="unittest.case.SkipTest" message="demonstrating skipping"><![CDATA[Exception: demonstrating skipping -]]></skipped></testcase><testcase classname="test_unittest_two.Test_test2" name="test_A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 5, in test_A2 - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_B2" time="0.000"></testcase><testcase classname="test_unittest_two.Test_test2" name="test_C2" time="0.000"><failure type="builtins.AssertionError" message="1 != 2 : Not equal"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 11, in test_C2 - self.assertEqual(1,2,'Not equal') - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 829, in assertEqual - assertion_func(first, second, msg=msg) - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 822, in _baseAssertEqual - raise self.failureException(msg) -AssertionError: 1 != 2 : Not equal -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2" name="test_D2" time="0.000"><error type="builtins.ArithmeticError" message=""><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 14, in test_D2 - raise ArithmeticError() -ArithmeticError -]]></error></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222A2" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py", line 19, in test_222A2 - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="test_unittest_two.Test_test2a" name="test_222B2" time="0.000"></testcase><testcase classname="unittest_three_test.Test_test3" name="test_A" time="0.000"><failure type="builtins.AssertionError" message="Not implemented"><![CDATA[Traceback (most recent call last): - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 59, in testPartExecutor - yield - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 605, in run - testMethod() - File "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py", line 6, in test_A - self.fail("Not implemented") - File "/Users/donjayamanne/anaconda3/lib/python3.6/unittest/case.py", line 670, in fail - raise self.failureException(msg) -AssertionError: Not implemented -]]></failure></testcase><testcase classname="unittest_three_test.Test_test3" name="test_B" time="0.000"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py deleted file mode 100644 index 4825f3a4db3b..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_one.py +++ /dev/null @@ -1,15 +0,0 @@ -import sys -import os - -import unittest - -class Test_test1(unittest.TestCase): - def tst_A(self): - self.fail("Not implemented") - - def tst_B(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py deleted file mode 100644 index c9a76c07f933..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/specific/tst_unittest_two.py +++ /dev/null @@ -1,18 +0,0 @@ -import unittest - -class Tst_test2(unittest.TestCase): - def tst_A2(self): - self.fail("Not implemented") - - def tst_B2(self): - self.assertEqual(1,1,'Not equal') - - def tst_C2(self): - self.assertEqual(1,2,'Not equal') - - def tst_D2(self): - raise ArithmeticError() - pass - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/test_root.py b/src/test/pythonFiles/testFiles/noseFiles/test_root.py deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os - -import unittest - -class Test_Root_test1(unittest.TestCase): - def test_Root_A(self): - self.fail("Not implemented") - - def test_Root_B(self): - self.assertEqual(1, 1, 'Not equal') - - @unittest.skip("demonstrating skipping") - def test_Root_c(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py deleted file mode 100644 index 734b84cd342e..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest - - -class Test_test3(unittest.TestCase): - def test4A(self): - self.fail("Not implemented") - - def test4B(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os - -import unittest - -class Test_test1(unittest.TestCase): - def test_A(self): - self.fail("Not implemented") - - def test_B(self): - self.assertEqual(1, 1, 'Not equal') - - @unittest.skip("demonstrating skipping") - def test_c(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py deleted file mode 100644 index ad89d873e879..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest - -class Test_test2(unittest.TestCase): - def test_A2(self): - self.fail("Not implemented") - - def test_B2(self): - self.assertEqual(1,1,'Not equal') - - def test_C2(self): - self.assertEqual(1,2,'Not equal') - - def test_D2(self): - raise ArithmeticError() - pass - -class Test_test2a(unittest.TestCase): - def test_222A2(self): - self.fail("Not implemented") - - def test_222B2(self): - self.assertEqual(1,1,'Not equal') - - class Test_test2a1(unittest.TestCase): - def test_222A2wow(self): - self.fail("Not implemented") - - def test_222B2wow(self): - self.assertEqual(1,1,'Not equal') - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py deleted file mode 100644 index 507e6af02063..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest - - -class Test_test3(unittest.TestCase): - def test_A(self): - self.fail("Not implemented") - - def test_B(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/noseFiles/three.output b/src/test/pythonFiles/testFiles/noseFiles/three.output deleted file mode 100644 index a57dae74d180..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/three.output +++ /dev/null @@ -1,555 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x1091f9048>, <nose.plugins.logcapture.LogCapture object at 0x1090e2400>, <nose.plugins.deprecated.Deprecated object at 0x10929dac8>, <nose.plugins.skip.Skip object at 0x1092fe8d0>, <nose.plugins.collect.CollectOnly object at 0x1093cb208>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x1093cbcf8: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': 'test', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x108701438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x1087014e0>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('test'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x1088ae7f0> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4450030200)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4450030200)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/one.output? False -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/specific? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/two.output? False -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093e18d0>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093e1630>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10939d8e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py module test4 call None -nose.importer: DEBUG: Import test4 from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test4 (test4) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'> -nose.selector: DEBUG: wantModule <module 'test4' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test4.py'>? True -nose.selector: DEBUG: wantClass <class 'test4.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test4.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test4B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093e1e10>) -nose.plugins.collect: DEBUG: Add test test4A (test4.Test_test3) -nose.plugins.collect: DEBUG: Add test test4B (test4.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093e1c50>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test4.Test_test3 testMethod=test4A>), Test(<test4.Test_test3 testMethod=test4B>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py module test_unittest_one call None -nose.importer: DEBUG: Import test_unittest_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_one (test_unittest_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093fd4e0>) -nose.plugins.collect: DEBUG: Add test test_A (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_unittest_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093e1d30>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_one.Test_test1 testMethod=test_A>), Test(<test_unittest_one.Test_test1 testMethod=test_B>), Test(<test_unittest_one.Test_test1 testMethod=test_c>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py module test_unittest_two call None -nose.importer: DEBUG: Import test_unittest_two from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part test_unittest_two (test_unittest_two) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'> -nose.selector: DEBUG: wantModule <module 'test_unittest_two' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/test_unittest_two.py'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2'>? True -nose.selector: DEBUG: wantClass <class 'test_unittest_two.Test_test2a'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_B2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_C2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2.test_D2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093fdba8>) -nose.plugins.collect: DEBUG: Add test test_A2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_B2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_C2 (test_unittest_two.Test_test2) -nose.plugins.collect: DEBUG: Add test test_D2 (test_unittest_two.Test_test2) -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_unittest_two.Test_test2a'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222A2>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test2a.test_222B2>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x1093fdba8>) -nose.plugins.collect: DEBUG: Add test test_222A2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: Add test test_222B2 (test_unittest_two.Test_test2a) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093fdb38>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2 testMethod=test_A2>), Test(<test_unittest_two.Test_test2 testMethod=test_B2>), Test(<test_unittest_two.Test_test2 testMethod=test_C2>), Test(<test_unittest_two.Test_test2 testMethod=test_D2>)]>, <nose.plugins.collect.TestSuite tests=[Test(<test_unittest_two.Test_test2a testMethod=test_222A2>), Test(<test_unittest_two.Test_test2a testMethod=test_222B2>)]>]> -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py module unittest_three_test call None -nose.importer: DEBUG: Import unittest_three_test from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.importer: DEBUG: find module part unittest_three_test (unittest_three_test) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests'] -nose.loader: DEBUG: Load from module <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'> -nose.selector: DEBUG: wantModule <module 'unittest_three_test' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests/unittest_three_test.py'>? True -nose.selector: DEBUG: wantClass <class 'unittest_three_test.Test_test3'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'unittest_three_test.Test_test3'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test3.test_B>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x109408588>) -nose.plugins.collect: DEBUG: Add test test_A (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: Add test test_B (unittest_three_test.Test_test3) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x1093fdac8>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<unittest_three_test.Test_test3 testMethod=test_A>), Test(<unittest_three_test.Test_test3 testMethod=test_B>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/noseFiles/tests -nose.plugins.collect: DEBUG: Preparing test case test4A (test4.Test_test3) -test4A (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test4B (test4.Test_test3) -test4B (test4.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (test_unittest_one.Test_test1) -test_A (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_unittest_one.Test_test1) -test_B (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_unittest_one.Test_test1) -test_c (test_unittest_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A2 (test_unittest_two.Test_test2) -test_A2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B2 (test_unittest_two.Test_test2) -test_B2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_C2 (test_unittest_two.Test_test2) -test_C2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_D2 (test_unittest_two.Test_test2) -test_D2 (test_unittest_two.Test_test2) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222A2 (test_unittest_two.Test_test2a) -test_222A2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_222B2 (test_unittest_two.Test_test2a) -test_222B2 (test_unittest_two.Test_test2a) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_A (unittest_three_test.Test_test3) -test_A (unittest_three_test.Test_test3) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (unittest_three_test.Test_test3) -test_B (unittest_three_test.Test_test3) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 16 tests in 0.052s - -OK diff --git a/src/test/pythonFiles/testFiles/noseFiles/two.output b/src/test/pythonFiles/testFiles/noseFiles/two.output deleted file mode 100644 index 25fcf10c93d5..000000000000 --- a/src/test/pythonFiles/testFiles/noseFiles/two.output +++ /dev/null @@ -1,211 +0,0 @@ -nose.config: INFO: Ignoring files matching ['^\\.', '^_', '^setup\\.py$'] -nose.plugins.manager: DEBUG: Configuring plugins -nose.plugins.manager: DEBUG: Plugins enabled: [<nose.plugins.capture.Capture object at 0x10bf02fd0>, <nose.plugins.logcapture.LogCapture object at 0x10bdf2390>, <nose.plugins.deprecated.Deprecated object at 0x10bfc0ef0>, <nose.plugins.skip.Skip object at 0x10c00d978>, <nose.plugins.collect.CollectOnly object at 0x10c0da1d0>] -nose.core: DEBUG: configured Config(addPaths=True, args=(), configSection='nosetests', debug=None, debugLog=None, env={}, exclude=None, files=[], firstPackageWins=False, getTestCaseNamesCompat=False, ignoreFiles=[re.compile('^\\.'), re.compile('^_'), re.compile('^setup\\.py$')], ignoreFilesDefaultStrings=['^\\.', '^_', '^setup\\.py$'], include=None, includeExe=False, logStream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, loggingConfig=None, options=<Values at 0x10c0dacc0: {'version': False, 'showPlugins': False, 'verbosity': 4, 'files': None, 'where': None, 'py3where': None, 'testMatch': '(?:^|[\\b_\\./-])[Tt]est', 'testNames': None, 'debug': None, 'debugLog': None, 'loggingConfig': None, 'ignoreFiles': [], 'exclude': [], 'include': [], 'stopOnError': False, 'addPaths': True, 'includeExe': False, 'traverseNamespace': False, 'firstPackageWins': False, 'byteCompile': True, 'attr': None, 'eval_attr': None, 'capture': True, 'logcapture': True, 'logcapture_format': '%(name)s: %(levelname)s: %(message)s', 'logcapture_datefmt': None, 'logcapture_filters': None, 'logcapture_clear': False, 'logcapture_level': 'NOTSET', 'enable_plugin_coverage': None, 'cover_packages': None, 'cover_erase': None, 'cover_tests': None, 'cover_min_percentage': None, 'cover_inclusive': None, 'cover_html': None, 'cover_html_dir': 'cover', 'cover_branches': None, 'cover_xml': None, 'cover_xml_file': 'coverage.xml', 'debugBoth': False, 'debugFailures': False, 'debugErrors': False, 'noDeprecated': False, 'enable_plugin_doctest': None, 'doctest_tests': None, 'doctestExtension': None, 'doctest_result_var': None, 'doctestFixtures': None, 'doctestOptions': None, 'enable_plugin_isolation': None, 'detailedErrors': None, 'noSkip': False, 'enable_plugin_id': None, 'testIdFile': '.noseids', 'failed': False, 'multiprocess_workers': 0, 'multiprocess_timeout': 10, 'multiprocess_restartworker': False, 'enable_plugin_xunit': None, 'xunit_file': 'nosetests.xml', 'xunit_testsuite_name': 'nosetests', 'enable_plugin_allmodules': None, 'collect_only': True}>, parser=<optparse.OptionParser object at 0x10b411438>, parserClass=<class 'optparse.OptionParser'>, plugins=<nose.plugins.manager.DefaultPluginManager object at 0x10af4f320>, py3where=(), runOnInit=True, srcDirs=('lib', 'src'), stopOnError=False, stream=<_io.TextIOWrapper name='<stderr>' mode='w' encoding='UTF-8'>, testMatch=re.compile('(?:^|[\\b_\\./-])[Tt]est'), testMatchPat='(?:^|[\\b_\\./-])[Tt]est', testNames=[], traverseNamespace=False, verbosity=4, where=(), worker=False, workingDir='/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single') -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single into sys.path -nose.plugins.collect: DEBUG: Preparing test loader -nose.core: DEBUG: test loader is <nose.loader.TestLoader object at 0x10b5be780> -nose.core: DEBUG: defaultTest . -nose.core: DEBUG: Test names are ['.'] -nose.core: DEBUG: createTests called with None -nose.loader: DEBUG: load from . (None) -nose.selector: DEBUG: Test name . resolved to file ., module None, call None -nose.selector: DEBUG: Final resolution of test name .: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single module None call None -nose.plugins.collect: DEBUG: TestSuite([<nose.suite.LazySuite tests=generator (4497277504)>]) -nose.plugins.collect: DEBUG: Add test <nose.suite.LazySuite tests=generator (4497277504)> -nose.core: DEBUG: runTests called -nose.suite: DEBUG: precache is [] -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py module test_root call None -nose.importer: DEBUG: Import test_root from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single -nose.importer: DEBUG: find module part test_root (test_root) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single'] -nose.loader: DEBUG: Load from module <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'> -nose.selector: DEBUG: wantModule <module 'test_root' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/test_root.py'>? True -nose.selector: DEBUG: wantClass <class 'test_root.Test_Root_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_root.Test_Root_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_Root_test1.test_Root_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10c0f0780>) -nose.plugins.collect: DEBUG: Add test test_Root_A (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_B (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: Add test test_Root_c (test_root.Test_Root_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10c0f08d0>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_root.Test_Root_test1 testMethod=test_Root_A>), Test(<test_root.Test_Root_test1 testMethod=test_Root_B>), Test(<test_root.Test_Root_test1 testMethod=test_Root_c>)]> -nose.plugins.collect: DEBUG: Preparing test case test_Root_A (test_root.Test_Root_test1) -test_Root_A (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_B (test_root.Test_Root_test1) -test_Root_B (test_root.Test_Root_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_Root_c (test_root.Test_Root_test1) -test_Root_c (test_root.Test_Root_test1) ... ok -nose.selector: DEBUG: wantDirectory /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests? True -nose.plugins.collect: DEBUG: TestSuite(<generator object TestLoader.loadTestsFromDir at 0x10c0ac8e0>) -nose.loader: DEBUG: load from dir /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: insert /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests into sys.path -nose.selector: DEBUG: wantFile /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py? True -nose.loader: DEBUG: load from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py (None) -nose.selector: DEBUG: Test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py resolved to file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py, module None, call None -nose.selector: DEBUG: Final resolution of test name /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py: file /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py module test_one call None -nose.importer: DEBUG: Import test_one from /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: Add path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.importer: DEBUG: find module part test_one (test_one) in ['/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests'] -nose.loader: DEBUG: Load from module <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'> -nose.selector: DEBUG: wantModule <module 'test_one' from '/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests/test_one.py'>? True -nose.selector: DEBUG: wantClass <class 'test_one.Test_test1'>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addCleanup>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.addTypeEqualityFunc>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertCountEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictContainsSubset>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertDictEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertFalse>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreater>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertGreaterEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNot>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertIsNotNone>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLess>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLessEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertListEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertLogs>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertMultiLineEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotAlmostEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIn>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotIsInstance>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertNotRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaises>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRaisesRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSequenceEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertSetEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTrue>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertTupleEqual>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarns>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.assertWarnsRegex>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.countTestCases>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.debug>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.defaultTestResult>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.doCleanups>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.fail>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.deprecated_func>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.id>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.run>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.setUp>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.setUpClass of <class 'test_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.shortDescription>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.skipTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.subTest>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.tearDown>? None -nose.selector: DEBUG: wantMethod <bound method TestCase.tearDownClass of <class 'test_one.Test_test1'>>? None -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_A>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_B>? True -nose.selector: DEBUG: wantMethod <unbound method Test_test1.test_c>? True -nose.plugins.collect: DEBUG: TestSuite(<map object at 0x10c0f08d0>) -nose.plugins.collect: DEBUG: Add test test_A (test_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_B (test_one.Test_test1) -nose.plugins.collect: DEBUG: Add test test_c (test_one.Test_test1) -nose.plugins.collect: DEBUG: TestSuite(<nose.suite.ContextList object at 0x10c0f08d0>) -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]> -nose.plugins.collect: DEBUG: Add test <nose.plugins.collect.TestSuite tests=[<nose.plugins.collect.TestSuite tests=[Test(<test_one.Test_test1 testMethod=test_A>), Test(<test_one.Test_test1 testMethod=test_B>), Test(<test_one.Test_test1 testMethod=test_c>)]>]> -nose.importer: DEBUG: Remove path /Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single/tests -nose.plugins.collect: DEBUG: Preparing test case test_A (test_one.Test_test1) -test_A (test_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_B (test_one.Test_test1) -test_B (test_one.Test_test1) ... ok -nose.plugins.collect: DEBUG: Preparing test case test_c (test_one.Test_test1) -test_c (test_one.Test_test1) ... ok -nose.suite: DEBUG: precache is [] - ----------------------------------------------------------------------- -Ran 6 tests in 0.188s - -OK diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/five.output b/src/test/pythonFiles/testFiles/pytestFiles/results/five.output deleted file mode 100644 index 125a1d107372..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/five.output +++ /dev/null @@ -1,367 +0,0 @@ -[ - { - "rootid": ".", - "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", - "parents": [ - { - "id": "./test_root.py", - "kind": "file", - "name": "test_root.py", - "parentid": "." - }, - { - "id": "./test_root.py::Test_Root_test1", - "kind": "suite", - "name": "Test_Root_test1", - "parentid": "./test_root.py" - }, - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "parentid": "." - }, - { - "id": "./tests/test_another_pytest.py", - "kind": "file", - "name": "test_another_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py", - "kind": "file", - "name": "test_foreign_nested_tests.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", - "kind": "suite", - "name": "TestNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", - "kind": "suite", - "name": "TestInheritingHere", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", - "kind": "suite", - "name": "TestExtraNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_pytest.py", - "kind": "file", - "name": "test_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp", - "kind": "suite", - "name": "Test_CheckMyApp", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", - "kind": "suite", - "name": "Test_NestedClassA", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", - "kind": "suite", - "name": "Test_nested_classB_Of_A", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_unittest_one.py", - "kind": "file", - "name": "test_unittest_one.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1", - "kind": "suite", - "name": "Test_test1", - "parentid": "./tests/test_unittest_one.py" - }, - { - "id": "./tests/test_unittest_two.py", - "kind": "file", - "name": "test_unittest_two.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2", - "kind": "suite", - "name": "Test_test2", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a", - "kind": "suite", - "name": "Test_test2a", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/unittest_three_test.py", - "kind": "file", - "name": "unittest_three_test.py", - "parentid": "./tests" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3", - "kind": "suite", - "name": "Test_test3", - "parentid": "./tests/unittest_three_test.py" - } - ], - "tests": [ - { - "id": "./test_root.py::Test_Root_test1::test_Root_A", - "name": "test_Root_A", - "source": "./test_root.py:6", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_B", - "name": "test_Root_B", - "source": "./test_root.py:9", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_c", - "name": "test_Root_c", - "source": "./test_root.py:12", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./tests/test_another_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_another_pytest.py:12", - "markers": [], - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", - "name": "test_super_deep_foreign", - "source": "tests/external.py:2", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", - "name": "test_foreign_test", - "source": "tests/external.py:4", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", - "name": "test_nested_normal", - "source": "tests/test_foreign_nested_tests.py:5", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", - "name": "test_normal", - "source": "tests/test_foreign_nested_tests.py:7", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", - "name": "test_simple_check", - "source": "tests/test_pytest.py:6", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", - "name": "test_complex_check", - "source": "tests/test_pytest.py:9", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", - "name": "test_nested_class_methodB", - "source": "tests/test_pytest.py:13", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", - "name": "test_d", - "source": "tests/test_pytest.py:16", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", - "name": "test_nested_class_methodC", - "source": "tests/test_pytest.py:18", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", - "name": "test_simple_check2", - "source": "tests/test_pytest.py:21", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", - "name": "test_complex_check2", - "source": "tests/test_pytest.py:23", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_pytest.py:35", - "markers": [], - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_A", - "name": "test_A", - "source": "tests/test_unittest_one.py:6", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_B", - "name": "test_B", - "source": "tests/test_unittest_one.py:9", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_c", - "name": "test_c", - "source": "tests/test_unittest_one.py:12", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_A2", - "name": "test_A2", - "source": "tests/test_unittest_two.py:3", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_B2", - "name": "test_B2", - "source": "tests/test_unittest_two.py:6", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_C2", - "name": "test_C2", - "source": "tests/test_unittest_two.py:9", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_D2", - "name": "test_D2", - "source": "tests/test_unittest_two.py:12", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", - "name": "test_222A2", - "source": "tests/test_unittest_two.py:17", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", - "name": "test_222B2", - "source": "tests/test_unittest_two.py:20", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_A", - "name": "test_A", - "source": "tests/unittest_three_test.py:4", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_B", - "name": "test_B", - "source": "tests/unittest_three_test.py:7", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - } - ] - } -] diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml deleted file mode 100644 index 87d7abeb58ce..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/five.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="1" name="pytest" skips="0" tests="1" time="0.050"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.0016579627990722656"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; - - def test_Root_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -test_root.py:8: AssertionError</failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/four.output b/src/test/pythonFiles/testFiles/pytestFiles/results/four.output deleted file mode 100644 index 125a1d107372..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/four.output +++ /dev/null @@ -1,367 +0,0 @@ -[ - { - "rootid": ".", - "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", - "parents": [ - { - "id": "./test_root.py", - "kind": "file", - "name": "test_root.py", - "parentid": "." - }, - { - "id": "./test_root.py::Test_Root_test1", - "kind": "suite", - "name": "Test_Root_test1", - "parentid": "./test_root.py" - }, - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "parentid": "." - }, - { - "id": "./tests/test_another_pytest.py", - "kind": "file", - "name": "test_another_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py", - "kind": "file", - "name": "test_foreign_nested_tests.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", - "kind": "suite", - "name": "TestNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", - "kind": "suite", - "name": "TestInheritingHere", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", - "kind": "suite", - "name": "TestExtraNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_pytest.py", - "kind": "file", - "name": "test_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp", - "kind": "suite", - "name": "Test_CheckMyApp", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", - "kind": "suite", - "name": "Test_NestedClassA", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", - "kind": "suite", - "name": "Test_nested_classB_Of_A", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_unittest_one.py", - "kind": "file", - "name": "test_unittest_one.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1", - "kind": "suite", - "name": "Test_test1", - "parentid": "./tests/test_unittest_one.py" - }, - { - "id": "./tests/test_unittest_two.py", - "kind": "file", - "name": "test_unittest_two.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2", - "kind": "suite", - "name": "Test_test2", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a", - "kind": "suite", - "name": "Test_test2a", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/unittest_three_test.py", - "kind": "file", - "name": "unittest_three_test.py", - "parentid": "./tests" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3", - "kind": "suite", - "name": "Test_test3", - "parentid": "./tests/unittest_three_test.py" - } - ], - "tests": [ - { - "id": "./test_root.py::Test_Root_test1::test_Root_A", - "name": "test_Root_A", - "source": "./test_root.py:6", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_B", - "name": "test_Root_B", - "source": "./test_root.py:9", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_c", - "name": "test_Root_c", - "source": "./test_root.py:12", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./tests/test_another_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_another_pytest.py:12", - "markers": [], - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", - "name": "test_super_deep_foreign", - "source": "tests/external.py:2", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", - "name": "test_foreign_test", - "source": "tests/external.py:4", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", - "name": "test_nested_normal", - "source": "tests/test_foreign_nested_tests.py:5", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", - "name": "test_normal", - "source": "tests/test_foreign_nested_tests.py:7", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", - "name": "test_simple_check", - "source": "tests/test_pytest.py:6", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", - "name": "test_complex_check", - "source": "tests/test_pytest.py:9", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", - "name": "test_nested_class_methodB", - "source": "tests/test_pytest.py:13", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", - "name": "test_d", - "source": "tests/test_pytest.py:16", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", - "name": "test_nested_class_methodC", - "source": "tests/test_pytest.py:18", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", - "name": "test_simple_check2", - "source": "tests/test_pytest.py:21", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", - "name": "test_complex_check2", - "source": "tests/test_pytest.py:23", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_pytest.py:35", - "markers": [], - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_A", - "name": "test_A", - "source": "tests/test_unittest_one.py:6", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_B", - "name": "test_B", - "source": "tests/test_unittest_one.py:9", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_c", - "name": "test_c", - "source": "tests/test_unittest_one.py:12", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_A2", - "name": "test_A2", - "source": "tests/test_unittest_two.py:3", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_B2", - "name": "test_B2", - "source": "tests/test_unittest_two.py:6", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_C2", - "name": "test_C2", - "source": "tests/test_unittest_two.py:9", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_D2", - "name": "test_D2", - "source": "tests/test_unittest_two.py:12", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", - "name": "test_222A2", - "source": "tests/test_unittest_two.py:17", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", - "name": "test_222B2", - "source": "tests/test_unittest_two.py:20", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_A", - "name": "test_A", - "source": "tests/unittest_three_test.py:4", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_B", - "name": "test_B", - "source": "tests/unittest_three_test.py:7", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - } - ] - } -] diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml deleted file mode 100644 index b13d0a4c1fc3..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/four.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="1" name="pytest" skips="1" tests="3" time="0.052"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.0012121200561523438"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; - - def test_Root_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -test_root.py:8: AssertionError</failure></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="9" name="test_Root_B" time="0.0005743503570556641"></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="12" name="test_Root_c" time="0.0008814334869384766"><skipped message="demonstrating skipping" type="pytest.skip">test_root.py:12: &lt;py._xmlgen.raw object at 0x10a139048&gt;</skipped></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/one.output b/src/test/pythonFiles/testFiles/pytestFiles/results/one.output deleted file mode 100644 index 125a1d107372..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/one.output +++ /dev/null @@ -1,367 +0,0 @@ -[ - { - "rootid": ".", - "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", - "parents": [ - { - "id": "./test_root.py", - "kind": "file", - "name": "test_root.py", - "parentid": "." - }, - { - "id": "./test_root.py::Test_Root_test1", - "kind": "suite", - "name": "Test_Root_test1", - "parentid": "./test_root.py" - }, - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "parentid": "." - }, - { - "id": "./tests/test_another_pytest.py", - "kind": "file", - "name": "test_another_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py", - "kind": "file", - "name": "test_foreign_nested_tests.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", - "kind": "suite", - "name": "TestNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", - "kind": "suite", - "name": "TestInheritingHere", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", - "kind": "suite", - "name": "TestExtraNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_pytest.py", - "kind": "file", - "name": "test_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp", - "kind": "suite", - "name": "Test_CheckMyApp", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", - "kind": "suite", - "name": "Test_NestedClassA", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", - "kind": "suite", - "name": "Test_nested_classB_Of_A", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_unittest_one.py", - "kind": "file", - "name": "test_unittest_one.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1", - "kind": "suite", - "name": "Test_test1", - "parentid": "./tests/test_unittest_one.py" - }, - { - "id": "./tests/test_unittest_two.py", - "kind": "file", - "name": "test_unittest_two.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2", - "kind": "suite", - "name": "Test_test2", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a", - "kind": "suite", - "name": "Test_test2a", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/unittest_three_test.py", - "kind": "file", - "name": "unittest_three_test.py", - "parentid": "./tests" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3", - "kind": "suite", - "name": "Test_test3", - "parentid": "./tests/unittest_three_test.py" - } - ], - "tests": [ - { - "id": "./test_root.py::Test_Root_test1::test_Root_A", - "name": "test_Root_A", - "source": "./test_root.py:6", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_B", - "name": "test_Root_B", - "source": "./test_root.py:9", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_c", - "name": "test_Root_c", - "source": "./test_root.py:12", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./tests/test_another_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_another_pytest.py:12", - "markers": [], - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", - "name": "test_super_deep_foreign", - "source": "tests/external.py:2", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", - "name": "test_foreign_test", - "source": "tests/external.py:4", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", - "name": "test_nested_normal", - "source": "tests/test_foreign_nested_tests.py:5", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", - "name": "test_normal", - "source": "tests/test_foreign_nested_tests.py:7", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", - "name": "test_simple_check", - "source": "tests/test_pytest.py:6", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", - "name": "test_complex_check", - "source": "tests/test_pytest.py:9", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", - "name": "test_nested_class_methodB", - "source": "tests/test_pytest.py:13", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", - "name": "test_d", - "source": "tests/test_pytest.py:16", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", - "name": "test_nested_class_methodC", - "source": "tests/test_pytest.py:18", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", - "name": "test_simple_check2", - "source": "tests/test_pytest.py:21", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", - "name": "test_complex_check2", - "source": "tests/test_pytest.py:23", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_pytest.py:35", - "markers": [], - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_A", - "name": "test_A", - "source": "tests/test_unittest_one.py:6", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_B", - "name": "test_B", - "source": "tests/test_unittest_one.py:9", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_c", - "name": "test_c", - "source": "tests/test_unittest_one.py:12", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_A2", - "name": "test_A2", - "source": "tests/test_unittest_two.py:3", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_B2", - "name": "test_B2", - "source": "tests/test_unittest_two.py:6", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_C2", - "name": "test_C2", - "source": "tests/test_unittest_two.py:9", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_D2", - "name": "test_D2", - "source": "tests/test_unittest_two.py:12", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", - "name": "test_222A2", - "source": "tests/test_unittest_two.py:17", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", - "name": "test_222B2", - "source": "tests/test_unittest_two.py:20", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_A", - "name": "test_A", - "source": "tests/unittest_three_test.py:4", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_B", - "name": "test_B", - "source": "tests/unittest_three_test.py:7", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - } - ] - } -] diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml deleted file mode 100644 index e4d7a513e119..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/one.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="11" name="pytest" skips="3" tests="33" time="0.210"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.001688241958618164"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; - - def test_Root_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -test_root.py:8: AssertionError</failure></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="9" name="test_Root_B" time="0.0007982254028320312"></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="12" name="test_Root_c" time="0.0004982948303222656"><skipped message="demonstrating skipping" type="pytest.skip">test_root.py:12: &lt;py._xmlgen.raw object at 0x1024cf048&gt;</skipped></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="12" name="test_username" time="0.0006861686706542969"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[one]" time="0.0006616115570068359"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[two]" time="0.0005772113800048828"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0009157657623291016"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_another_pytest.py:17: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()" file="tests/external.py" line="2" name="test_super_deep_foreign" time="0.0021979808807373047"><failure message="AssertionError">self = &lt;tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0&gt; - - def test_super_deep_foreign(self): -&gt; assert False -E AssertionError - -tests/external.py:4: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/external.py" line="4" name="test_foreign_test" time="0.0007357597351074219"><failure message="AssertionError">self = &lt;tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898&gt; - - def test_foreign_test(self): -&gt; assert False -E AssertionError - -tests/external.py:6: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/test_foreign_nested_tests.py" line="5" name="test_nested_normal" time="0.0006644725799560547"></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests" file="tests/test_foreign_nested_tests.py" line="7" name="test_normal" time="0.0007319450378417969"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="6" name="test_simple_check" time="0.0006330013275146484"><skipped message="demonstrating skipping" type="pytest.skip">/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: &lt;py._xmlgen.raw object at 0x1024fb518&gt;</skipped></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="9" name="test_complex_check" time="0.0006620883941650391"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="13" name="test_nested_class_methodB" time="0.0004994869232177734"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()" file="tests/test_pytest.py" line="16" name="test_d" time="0.0006279945373535156"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="18" name="test_nested_class_methodC" time="0.0005779266357421875"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="21" name="test_simple_check2" time="0.000728607177734375"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="23" name="test_complex_check2" time="0.0005090236663818359"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="35" name="test_username" time="0.0008552074432373047"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[one]" time="0.0010302066802978516"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[two]" time="0.0009279251098632812"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.001287698745727539"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="6" name="test_A" time="0.0006275177001953125"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_one.Test_test1 testMethod=test_A&gt; - - def test_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_one.py:8: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="9" name="test_B" time="0.00047135353088378906"></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="12" name="test_c" time="0.0005207061767578125"><skipped message="demonstrating skipping" type="pytest.skip">tests/test_unittest_one.py:12: &lt;py._xmlgen.raw object at 0x102504cc0&gt;</skipped></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="3" name="test_A2" time="0.0006039142608642578"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2 testMethod=test_A2&gt; - - def test_A2(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_two.py:5: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="6" name="test_B2" time="0.0007021427154541016"></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="9" name="test_C2" time="0.0008001327514648438"><failure message="AssertionError: 1 != 2 : Not equal">self = &lt;test_unittest_two.Test_test2 testMethod=test_C2&gt; - - def test_C2(self): -&gt; self.assertEqual(1,2,&apos;Not equal&apos;) -E AssertionError: 1 != 2 : Not equal - -tests/test_unittest_two.py:11: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="12" name="test_D2" time="0.0005772113800048828"><failure message="ArithmeticError">self = &lt;test_unittest_two.Test_test2 testMethod=test_D2&gt; - - def test_D2(self): -&gt; raise ArithmeticError() -E ArithmeticError - -tests/test_unittest_two.py:14: ArithmeticError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="17" name="test_222A2" time="0.0005698204040527344"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2a testMethod=test_222A2&gt; - - def test_222A2(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_two.py:19: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="20" name="test_222B2" time="0.0004627704620361328"></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="4" name="test_A" time="0.0006659030914306641"><failure message="AssertionError: Not implemented">self = &lt;unittest_three_test.Test_test3 testMethod=test_A&gt; - - def test_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/unittest_three_test.py:6: AssertionError</failure></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="7" name="test_B" time="0.0006167888641357422"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/three.output b/src/test/pythonFiles/testFiles/pytestFiles/results/three.output deleted file mode 100644 index 125a1d107372..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/three.output +++ /dev/null @@ -1,367 +0,0 @@ -[ - { - "rootid": ".", - "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", - "parents": [ - { - "id": "./test_root.py", - "kind": "file", - "name": "test_root.py", - "parentid": "." - }, - { - "id": "./test_root.py::Test_Root_test1", - "kind": "suite", - "name": "Test_Root_test1", - "parentid": "./test_root.py" - }, - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "parentid": "." - }, - { - "id": "./tests/test_another_pytest.py", - "kind": "file", - "name": "test_another_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py", - "kind": "file", - "name": "test_foreign_nested_tests.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", - "kind": "suite", - "name": "TestNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", - "kind": "suite", - "name": "TestInheritingHere", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", - "kind": "suite", - "name": "TestExtraNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_pytest.py", - "kind": "file", - "name": "test_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp", - "kind": "suite", - "name": "Test_CheckMyApp", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", - "kind": "suite", - "name": "Test_NestedClassA", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", - "kind": "suite", - "name": "Test_nested_classB_Of_A", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_unittest_one.py", - "kind": "file", - "name": "test_unittest_one.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1", - "kind": "suite", - "name": "Test_test1", - "parentid": "./tests/test_unittest_one.py" - }, - { - "id": "./tests/test_unittest_two.py", - "kind": "file", - "name": "test_unittest_two.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2", - "kind": "suite", - "name": "Test_test2", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a", - "kind": "suite", - "name": "Test_test2a", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/unittest_three_test.py", - "kind": "file", - "name": "unittest_three_test.py", - "parentid": "./tests" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3", - "kind": "suite", - "name": "Test_test3", - "parentid": "./tests/unittest_three_test.py" - } - ], - "tests": [ - { - "id": "./test_root.py::Test_Root_test1::test_Root_A", - "name": "test_Root_A", - "source": "./test_root.py:6", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_B", - "name": "test_Root_B", - "source": "./test_root.py:9", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_c", - "name": "test_Root_c", - "source": "./test_root.py:12", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./tests/test_another_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_another_pytest.py:12", - "markers": [], - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", - "name": "test_super_deep_foreign", - "source": "tests/external.py:2", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", - "name": "test_foreign_test", - "source": "tests/external.py:4", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", - "name": "test_nested_normal", - "source": "tests/test_foreign_nested_tests.py:5", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", - "name": "test_normal", - "source": "tests/test_foreign_nested_tests.py:7", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", - "name": "test_simple_check", - "source": "tests/test_pytest.py:6", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", - "name": "test_complex_check", - "source": "tests/test_pytest.py:9", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", - "name": "test_nested_class_methodB", - "source": "tests/test_pytest.py:13", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", - "name": "test_d", - "source": "tests/test_pytest.py:16", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", - "name": "test_nested_class_methodC", - "source": "tests/test_pytest.py:18", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", - "name": "test_simple_check2", - "source": "tests/test_pytest.py:21", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", - "name": "test_complex_check2", - "source": "tests/test_pytest.py:23", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_pytest.py:35", - "markers": [], - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_A", - "name": "test_A", - "source": "tests/test_unittest_one.py:6", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_B", - "name": "test_B", - "source": "tests/test_unittest_one.py:9", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_c", - "name": "test_c", - "source": "tests/test_unittest_one.py:12", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_A2", - "name": "test_A2", - "source": "tests/test_unittest_two.py:3", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_B2", - "name": "test_B2", - "source": "tests/test_unittest_two.py:6", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_C2", - "name": "test_C2", - "source": "tests/test_unittest_two.py:9", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_D2", - "name": "test_D2", - "source": "tests/test_unittest_two.py:12", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", - "name": "test_222A2", - "source": "tests/test_unittest_two.py:17", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", - "name": "test_222B2", - "source": "tests/test_unittest_two.py:20", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_A", - "name": "test_A", - "source": "tests/unittest_three_test.py:4", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_B", - "name": "test_B", - "source": "tests/unittest_three_test.py:7", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - } - ] - } -] diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml deleted file mode 100644 index 0d1e912f656c..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/three.xml +++ /dev/null @@ -1,7 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="1" name="pytest" skips="0" tests="4" time="0.048"><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="12" name="test_username" time="0.001523733139038086"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[one]" time="0.0007066726684570312"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[two]" time="0.0009090900421142578"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0011417865753173828"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_another_pytest.py:17: AssertionError</failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml deleted file mode 100644 index af1ee36ca7b7..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.again.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="10" name="pytest" skips="0" tests="11" time="0.080"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.0010533332824707031"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; - - def test_Root_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -test_root.py:8: AssertionError</failure></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0008268356323242188"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_another_pytest.py:17: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()" file="tests/external.py" line="2" name="test_super_deep_foreign" time="0.0021979808807373047"><failure message="AssertionError">self = &lt;tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0&gt; - - def test_super_deep_foreign(self): -&gt; assert False -E AssertionError - -tests/external.py:4: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/external.py" line="4" name="test_foreign_test" time="0.0007357597351074219"><failure message="AssertionError">self = &lt;tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898&gt; - - def test_foreign_test(self): -&gt; assert False -E AssertionError - -tests/external.py:6: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/test_foreign_nested_tests.py" line="5" name="test_nested_normal" time="0.0006644725799560547"></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests" file="tests/test_foreign_nested_tests.py" line="7" name="test_normal" time="0.0007319450378417969"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="6" name="test_simple_check" time="0.0006330013275146484"><skipped message="demonstrating skipping" type="pytest.skip">/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: &lt;py._xmlgen.raw object at 0x1024fb518&gt;</skipped></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="9" name="test_complex_check" time="0.0006620883941650391"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="13" name="test_nested_class_methodB" time="0.0004994869232177734"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()" file="tests/test_pytest.py" line="16" name="test_d" time="0.0006279945373535156"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="18" name="test_nested_class_methodC" time="0.0005779266357421875"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="21" name="test_simple_check2" time="0.000728607177734375"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="23" name="test_complex_check2" time="0.0005090236663818359"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="35" name="test_username" time="0.0008552074432373047"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[one]" time="0.0010302066802978516"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[two]" time="0.0009279251098632812"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.001287698745727539"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.0009989738464355469"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="6" name="test_A" time="0.00090789794921875"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_one.Test_test1 testMethod=test_A&gt; - - def test_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_one.py:8: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="3" name="test_A2" time="0.0007557868957519531"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2 testMethod=test_A2&gt; - - def test_A2(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_two.py:5: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="9" name="test_C2" time="0.0006463527679443359"><failure message="AssertionError: 1 != 2 : Not equal">self = &lt;test_unittest_two.Test_test2 testMethod=test_C2&gt; - - def test_C2(self): -&gt; self.assertEqual(1,2,&apos;Not equal&apos;) -E AssertionError: 1 != 2 : Not equal - -tests/test_unittest_two.py:11: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="12" name="test_D2" time="0.00047707557678222656"><failure message="ArithmeticError">self = &lt;test_unittest_two.Test_test2 testMethod=test_D2&gt; - - def test_D2(self): -&gt; raise ArithmeticError() -E ArithmeticError - -tests/test_unittest_two.py:14: ArithmeticError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="17" name="test_222A2" time="0.00048279762268066406"></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="4" name="test_A" time="0.000579833984375"><failure message="AssertionError: Not implemented">self = &lt;unittest_three_test.Test_test3 testMethod=test_A&gt; - - def test_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/unittest_three_test.py:6: AssertionError</failure></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/two.output b/src/test/pythonFiles/testFiles/pytestFiles/results/two.output deleted file mode 100644 index 125a1d107372..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.output +++ /dev/null @@ -1,367 +0,0 @@ -[ - { - "rootid": ".", - "root": "/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard", - "parents": [ - { - "id": "./test_root.py", - "kind": "file", - "name": "test_root.py", - "parentid": "." - }, - { - "id": "./test_root.py::Test_Root_test1", - "kind": "suite", - "name": "Test_Root_test1", - "parentid": "./test_root.py" - }, - { - "id": "./tests", - "kind": "folder", - "name": "tests", - "parentid": "." - }, - { - "id": "./tests/test_another_pytest.py", - "kind": "file", - "name": "test_another_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py", - "kind": "file", - "name": "test_foreign_nested_tests.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests", - "kind": "suite", - "name": "TestNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere", - "kind": "suite", - "name": "TestInheritingHere", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests", - "kind": "suite", - "name": "TestExtraNestedForeignTests", - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_pytest.py", - "kind": "file", - "name": "test_pytest.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp", - "kind": "suite", - "name": "Test_CheckMyApp", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA", - "kind": "suite", - "name": "Test_NestedClassA", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A", - "kind": "suite", - "name": "Test_nested_classB_Of_A", - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username", - "kind": "function", - "name": "test_parametrized_username", - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_unittest_one.py", - "kind": "file", - "name": "test_unittest_one.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1", - "kind": "suite", - "name": "Test_test1", - "parentid": "./tests/test_unittest_one.py" - }, - { - "id": "./tests/test_unittest_two.py", - "kind": "file", - "name": "test_unittest_two.py", - "parentid": "./tests" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2", - "kind": "suite", - "name": "Test_test2", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a", - "kind": "suite", - "name": "Test_test2a", - "parentid": "./tests/test_unittest_two.py" - }, - { - "id": "./tests/unittest_three_test.py", - "kind": "file", - "name": "unittest_three_test.py", - "parentid": "./tests" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3", - "kind": "suite", - "name": "Test_test3", - "parentid": "./tests/unittest_three_test.py" - } - ], - "tests": [ - { - "id": "./test_root.py::Test_Root_test1::test_Root_A", - "name": "test_Root_A", - "source": "./test_root.py:6", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_B", - "name": "test_Root_B", - "source": "./test_root.py:9", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./test_root.py::Test_Root_test1::test_Root_c", - "name": "test_Root_c", - "source": "./test_root.py:12", - "markers": [], - "parentid": "./test_root.py::Test_Root_test1" - }, - { - "id": "./tests/test_another_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_another_pytest.py:12", - "markers": [], - "parentid": "./tests/test_another_pytest.py" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_another_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_another_pytest.py:15", - "markers": [], - "parentid": "./tests/test_another_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign", - "name": "test_super_deep_foreign", - "source": "tests/external.py:2", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test", - "name": "test_foreign_test", - "source": "tests/external.py:4", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal", - "name": "test_nested_normal", - "source": "tests/test_foreign_nested_tests.py:5", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere" - }, - { - "id": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal", - "name": "test_normal", - "source": "tests/test_foreign_nested_tests.py:7", - "markers": [], - "parentid": "./tests/test_foreign_nested_tests.py::TestNestedForeignTests" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check", - "name": "test_simple_check", - "source": "tests/test_pytest.py:6", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check", - "name": "test_complex_check", - "source": "tests/test_pytest.py:9", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB", - "name": "test_nested_class_methodB", - "source": "tests/test_pytest.py:13", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d", - "name": "test_d", - "source": "tests/test_pytest.py:16", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC", - "name": "test_nested_class_methodC", - "source": "tests/test_pytest.py:18", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_simple_check2", - "name": "test_simple_check2", - "source": "tests/test_pytest.py:21", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::Test_CheckMyApp::test_complex_check2", - "name": "test_complex_check2", - "source": "tests/test_pytest.py:23", - "markers": [], - "parentid": "./tests/test_pytest.py::Test_CheckMyApp" - }, - { - "id": "./tests/test_pytest.py::test_username", - "name": "test_username", - "source": "tests/test_pytest.py:35", - "markers": [], - "parentid": "./tests/test_pytest.py" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[one]", - "name": "test_parametrized_username[one]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[two]", - "name": "test_parametrized_username[two]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_pytest.py::test_parametrized_username[three]", - "name": "test_parametrized_username[three]", - "source": "tests/test_pytest.py:38", - "markers": [], - "parentid": "./tests/test_pytest.py::test_parametrized_username" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_A", - "name": "test_A", - "source": "tests/test_unittest_one.py:6", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_B", - "name": "test_B", - "source": "tests/test_unittest_one.py:9", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_one.py::Test_test1::test_c", - "name": "test_c", - "source": "tests/test_unittest_one.py:12", - "markers": [], - "parentid": "./tests/test_unittest_one.py::Test_test1" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_A2", - "name": "test_A2", - "source": "tests/test_unittest_two.py:3", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_B2", - "name": "test_B2", - "source": "tests/test_unittest_two.py:6", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_C2", - "name": "test_C2", - "source": "tests/test_unittest_two.py:9", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2::test_D2", - "name": "test_D2", - "source": "tests/test_unittest_two.py:12", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222A2", - "name": "test_222A2", - "source": "tests/test_unittest_two.py:17", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/test_unittest_two.py::Test_test2a::test_222B2", - "name": "test_222B2", - "source": "tests/test_unittest_two.py:20", - "markers": [], - "parentid": "./tests/test_unittest_two.py::Test_test2a" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_A", - "name": "test_A", - "source": "tests/unittest_three_test.py:4", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - }, - { - "id": "./tests/unittest_three_test.py::Test_test3::test_B", - "name": "test_B", - "source": "tests/unittest_three_test.py:7", - "markers": [], - "parentid": "./tests/unittest_three_test.py::Test_test3" - } - ] - } -] diff --git a/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml b/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml deleted file mode 100644 index e4d7a513e119..000000000000 --- a/src/test/pythonFiles/testFiles/pytestFiles/results/two.xml +++ /dev/null @@ -1,67 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?><testsuite errors="0" failures="11" name="pytest" skips="3" tests="33" time="0.210"><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="6" name="test_Root_A" time="0.001688241958618164"><failure message="AssertionError: Not implemented">self = &lt;test_root.Test_Root_test1 testMethod=test_Root_A&gt; - - def test_Root_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -test_root.py:8: AssertionError</failure></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="9" name="test_Root_B" time="0.0007982254028320312"></testcase><testcase classname="test_root.Test_Root_test1" file="test_root.py" line="12" name="test_Root_c" time="0.0004982948303222656"><skipped message="demonstrating skipping" type="pytest.skip">test_root.py:12: &lt;py._xmlgen.raw object at 0x1024cf048&gt;</skipped></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="12" name="test_username" time="0.0006861686706542969"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[one]" time="0.0006616115570068359"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[two]" time="0.0005772113800048828"></testcase><testcase classname="tests.test_another_pytest" file="tests/test_another_pytest.py" line="15" name="test_parametrized_username[three]" time="0.0009157657623291016"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_another_pytest.py:17: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()" file="tests/external.py" line="2" name="test_super_deep_foreign" time="0.0021979808807373047"><failure message="AssertionError">self = &lt;tests.external.ForeignTests.TestExtraNestedForeignTests object at 0x10fb685c0&gt; - - def test_super_deep_foreign(self): -&gt; assert False -E AssertionError - -tests/external.py:4: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/external.py" line="4" name="test_foreign_test" time="0.0007357597351074219"><failure message="AssertionError">self = &lt;tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere object at 0x10fb74898&gt; - - def test_foreign_test(self): -&gt; assert False -E AssertionError - -tests/external.py:6: AssertionError</failure></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()" file="tests/test_foreign_nested_tests.py" line="5" name="test_nested_normal" time="0.0006644725799560547"></testcase><testcase classname="tests.test_foreign_nested_tests.TestNestedForeignTests" file="tests/test_foreign_nested_tests.py" line="7" name="test_normal" time="0.0007319450378417969"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="6" name="test_simple_check" time="0.0006330013275146484"><skipped message="demonstrating skipping" type="pytest.skip">/Users/donjayamanne/anaconda3/lib/python3.6/site-packages/_pytest/nose.py:23: &lt;py._xmlgen.raw object at 0x1024fb518&gt;</skipped></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="9" name="test_complex_check" time="0.0006620883941650391"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="13" name="test_nested_class_methodB" time="0.0004994869232177734"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()" file="tests/test_pytest.py" line="16" name="test_d" time="0.0006279945373535156"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()" file="tests/test_pytest.py" line="18" name="test_nested_class_methodC" time="0.0005779266357421875"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="21" name="test_simple_check2" time="0.000728607177734375"></testcase><testcase classname="tests.test_pytest.Test_CheckMyApp" file="tests/test_pytest.py" line="23" name="test_complex_check2" time="0.0005090236663818359"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="35" name="test_username" time="0.0008552074432373047"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[one]" time="0.0010302066802978516"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[two]" time="0.0009279251098632812"></testcase><testcase classname="tests.test_pytest" file="tests/test_pytest.py" line="38" name="test_parametrized_username[three]" time="0.001287698745727539"><failure message="AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;]">non_parametrized_username = &apos;three&apos; - - def test_parametrized_username(non_parametrized_username): -&gt; assert non_parametrized_username in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] -E AssertionError: assert &apos;three&apos; in [&apos;one&apos;, &apos;two&apos;, &apos;threes&apos;] - -tests/test_pytest.py:40: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="6" name="test_A" time="0.0006275177001953125"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_one.Test_test1 testMethod=test_A&gt; - - def test_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_one.py:8: AssertionError</failure></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="9" name="test_B" time="0.00047135353088378906"></testcase><testcase classname="tests.test_unittest_one.Test_test1" file="tests/test_unittest_one.py" line="12" name="test_c" time="0.0005207061767578125"><skipped message="demonstrating skipping" type="pytest.skip">tests/test_unittest_one.py:12: &lt;py._xmlgen.raw object at 0x102504cc0&gt;</skipped></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="3" name="test_A2" time="0.0006039142608642578"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2 testMethod=test_A2&gt; - - def test_A2(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_two.py:5: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="6" name="test_B2" time="0.0007021427154541016"></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="9" name="test_C2" time="0.0008001327514648438"><failure message="AssertionError: 1 != 2 : Not equal">self = &lt;test_unittest_two.Test_test2 testMethod=test_C2&gt; - - def test_C2(self): -&gt; self.assertEqual(1,2,&apos;Not equal&apos;) -E AssertionError: 1 != 2 : Not equal - -tests/test_unittest_two.py:11: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2" file="tests/test_unittest_two.py" line="12" name="test_D2" time="0.0005772113800048828"><failure message="ArithmeticError">self = &lt;test_unittest_two.Test_test2 testMethod=test_D2&gt; - - def test_D2(self): -&gt; raise ArithmeticError() -E ArithmeticError - -tests/test_unittest_two.py:14: ArithmeticError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="17" name="test_222A2" time="0.0005698204040527344"><failure message="AssertionError: Not implemented">self = &lt;test_unittest_two.Test_test2a testMethod=test_222A2&gt; - - def test_222A2(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/test_unittest_two.py:19: AssertionError</failure></testcase><testcase classname="tests.test_unittest_two.Test_test2a" file="tests/test_unittest_two.py" line="20" name="test_222B2" time="0.0004627704620361328"></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="4" name="test_A" time="0.0006659030914306641"><failure message="AssertionError: Not implemented">self = &lt;unittest_three_test.Test_test3 testMethod=test_A&gt; - - def test_A(self): -&gt; self.fail(&quot;Not implemented&quot;) -E AssertionError: Not implemented - -tests/unittest_three_test.py:6: AssertionError</failure></testcase><testcase classname="tests.unittest_three_test.Test_test3" file="tests/unittest_three_test.py" line="7" name="test_B" time="0.0006167888641357422"></testcase></testsuite> diff --git a/src/test/pythonFiles/testFiles/single/test_root.py b/src/test/pythonFiles/testFiles/single/test_root.py deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/single/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os - -import unittest - -class Test_Root_test1(unittest.TestCase): - def test_Root_A(self): - self.fail("Not implemented") - - def test_Root_B(self): - self.assertEqual(1, 1, 'Not equal') - - @unittest.skip("demonstrating skipping") - def test_Root_c(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/single/tests/test_one.py b/src/test/pythonFiles/testFiles/single/tests/test_one.py deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/single/tests/test_one.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os - -import unittest - -class Test_test1(unittest.TestCase): - def test_A(self): - self.fail("Not implemented") - - def test_B(self): - self.assertEqual(1, 1, 'Not equal') - - @unittest.skip("demonstrating skipping") - def test_c(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py deleted file mode 100644 index 72db843aa2af..000000000000 --- a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest - -class Test_test_one_1(unittest.TestCase): - def test_1_1_1(self): - self.assertEqual(1,1,'Not equal') - - def test_1_1_2(self): - self.assertEqual(1,2,'Not equal') - - @unittest.skip("demonstrating skipping") - def test_1_1_3(self): - self.assertEqual(1,2,'Not equal') - -class Test_test_one_2(unittest.TestCase): - def test_1_2_1(self): - self.assertEqual(1,1,'Not equal') - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py deleted file mode 100644 index abac1b49023f..000000000000 --- a/src/test/pythonFiles/testFiles/specificTest/tests/test_unittest_two.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest - -class Test_test_two_1(unittest.TestCase): - def test_1_1_1(self): - self.assertEqual(1,1,'Not equal') - - def test_1_1_2(self): - self.assertEqual(1,2,'Not equal') - - @unittest.skip("demonstrating skipping") - def test_1_1_3(self): - self.assertEqual(1,2,'Not equal') - -class Test_test_two_2(unittest.TestCase): - def test_2_1_1(self): - self.assertEqual(1,1,'Not equal') - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/test_root.py b/src/test/pythonFiles/testFiles/standard/test_root.py deleted file mode 100644 index 452813e9a079..000000000000 --- a/src/test/pythonFiles/testFiles/standard/test_root.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os - -import unittest - -class Test_Root_test1(unittest.TestCase): - def test_Root_A(self): - self.fail("Not implemented") - - def test_Root_B(self): - self.assertEqual(1, 1, 'Not equal') - - @unittest.skip("demonstrating skipping") - def test_Root_c(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/tests/external.py b/src/test/pythonFiles/testFiles/standard/tests/external.py deleted file mode 100644 index e7446cadb184..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/external.py +++ /dev/null @@ -1,6 +0,0 @@ -class ForeignTests: - class TestExtraNestedForeignTests: - def test_super_deep_foreign(self): - assert False - def test_foreign_test(self): - assert False diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py b/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py deleted file mode 100644 index 129bc168f0d5..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_another_pytest.py +++ /dev/null @@ -1,18 +0,0 @@ -# content of tests/test_something.py -import pytest -import unittest - -@pytest.fixture -def parametrized_username(): - return 'overridden-username' - -@pytest.fixture(params=['one', 'two', 'three']) -def non_parametrized_username(request): - return request.param - -def test_username(parametrized_username): - assert parametrized_username == 'overridden-username' - -def test_parametrized_username(non_parametrized_username): - assert non_parametrized_username in ['one', 'two', 'threes'] - diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py b/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py deleted file mode 100644 index 60df159b4c6d..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_foreign_nested_tests.py +++ /dev/null @@ -1,9 +0,0 @@ -from .external import ForeignTests - - -class TestNestedForeignTests: - class TestInheritingHere(ForeignTests): - def test_nested_normal(self): - assert True - def test_normal(self): - assert True diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py b/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py deleted file mode 100644 index dc5798306bb6..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_pytest.py +++ /dev/null @@ -1,41 +0,0 @@ -# content of tests/test_something.py -import pytest -import unittest - -# content of check_myapp.py -class Test_CheckMyApp: - @unittest.skip("demonstrating skipping") - def test_simple_check(self): - pass - def test_complex_check(self): - pass - - class Test_NestedClassA: - def test_nested_class_methodB(self): - assert True - class Test_nested_classB_Of_A: - def test_d(self): - assert True - def test_nested_class_methodC(self): - assert True - - def test_simple_check2(self): - pass - def test_complex_check2(self): - pass - - -@pytest.fixture -def parametrized_username(): - return 'overridden-username' - -@pytest.fixture(params=['one', 'two', 'three']) -def non_parametrized_username(request): - return request.param - -def test_username(parametrized_username): - assert parametrized_username == 'overridden-username' - -def test_parametrized_username(non_parametrized_username): - assert non_parametrized_username in ['one', 'two', 'threes'] - diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py b/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py deleted file mode 100644 index e869986b6ead..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_one.py +++ /dev/null @@ -1,19 +0,0 @@ -import sys -import os - -import unittest - -class Test_test1(unittest.TestCase): - def test_A(self): - self.fail("Not implemented") - - def test_B(self): - self.assertEqual(1, 1, 'Not equal') - - @unittest.skip("demonstrating skipping") - def test_c(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py b/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py deleted file mode 100644 index ad89d873e879..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/test_unittest_two.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest - -class Test_test2(unittest.TestCase): - def test_A2(self): - self.fail("Not implemented") - - def test_B2(self): - self.assertEqual(1,1,'Not equal') - - def test_C2(self): - self.assertEqual(1,2,'Not equal') - - def test_D2(self): - raise ArithmeticError() - pass - -class Test_test2a(unittest.TestCase): - def test_222A2(self): - self.fail("Not implemented") - - def test_222B2(self): - self.assertEqual(1,1,'Not equal') - - class Test_test2a1(unittest.TestCase): - def test_222A2wow(self): - self.fail("Not implemented") - - def test_222B2wow(self): - self.assertEqual(1,1,'Not equal') - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py b/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py deleted file mode 100644 index 507e6af02063..000000000000 --- a/src/test/pythonFiles/testFiles/standard/tests/unittest_three_test.py +++ /dev/null @@ -1,13 +0,0 @@ -import unittest - - -class Test_test3(unittest.TestCase): - def test_A(self): - self.fail("Not implemented") - - def test_B(self): - self.assertEqual(1, 1, 'Not equal') - - -if __name__ == '__main__': - unittest.main() diff --git a/src/test/pythonFiles/testFiles/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/pythonFiles/terminalExec/sample8_normalized.py b/src/test/python_files/terminalExec/sample7_normalized_selection.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_normalized.py rename to src/test/python_files/terminalExec/sample7_normalized_selection.py diff --git a/src/test/pythonFiles/terminalExec/sample7_raw.py b/src/test/python_files/terminalExec/sample7_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample7_raw.py rename to src/test/python_files/terminalExec/sample7_raw.py diff --git a/src/test/python_files/terminalExec/sample8_normalized.py b/src/test/python_files/terminalExec/sample8_normalized.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/python_files/terminalExec/sample8_normalized.py @@ -0,0 +1,8 @@ +if True: + print(1) + print(1) +else: + print(2) + print(2) + +print(3) diff --git a/src/test/python_files/terminalExec/sample8_normalized_selection.py b/src/test/python_files/terminalExec/sample8_normalized_selection.py new file mode 100644 index 000000000000..2288800fc985 --- /dev/null +++ b/src/test/python_files/terminalExec/sample8_normalized_selection.py @@ -0,0 +1,8 @@ +if True: + print(1) + print(1) +else: + print(2) + print(2) + +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample8_raw.py b/src/test/python_files/terminalExec/sample8_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample8_raw.py rename to src/test/python_files/terminalExec/sample8_raw.py diff --git a/src/test/python_files/terminalExec/sample_invalid_smart_selection.py b/src/test/python_files/terminalExec/sample_invalid_smart_selection.py new file mode 100644 index 000000000000..73d9e0fba066 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_invalid_smart_selection.py @@ -0,0 +1,10 @@ +def beliebig(x, y, *mehr): + print "x=", x, ", x=", y + print "mehr: ", mehr + +list = [ +1, +2, +3, +] +print("Above is invalid");print("deprecated");print("show warning") diff --git a/src/test/pythonFiles/terminalExec/sample_normalized.py b/src/test/python_files/terminalExec/sample_normalized.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_normalized.py rename to src/test/python_files/terminalExec/sample_normalized.py diff --git a/src/test/python_files/terminalExec/sample_normalized_selection.py b/src/test/python_files/terminalExec/sample_normalized_selection.py new file mode 100644 index 000000000000..8ee9b90cdd27 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_normalized_selection.py @@ -0,0 +1,5 @@ +import sys +print(sys.executable) +print("1234") +print(1) +print(2) diff --git a/src/test/pythonFiles/terminalExec/sample_raw.py b/src/test/python_files/terminalExec/sample_raw.py similarity index 100% rename from src/test/pythonFiles/terminalExec/sample_raw.py rename to src/test/python_files/terminalExec/sample_raw.py diff --git a/src/test/python_files/terminalExec/sample_smart_selection.py b/src/test/python_files/terminalExec/sample_smart_selection.py new file mode 100644 index 000000000000..3933f06b5d65 --- /dev/null +++ b/src/test/python_files/terminalExec/sample_smart_selection.py @@ -0,0 +1,21 @@ +my_dict = { + "key1": "value1", + "key2": "value2" +} +#Sample + +print("Audi");print("BMW");print("Mercedes") + +# print("dont print me") + +def my_dogs(): + print("Corgi") + print("Husky") + print("Corgi2") + print("Husky2") + print("no dogs") + +# Skip me to prove that you did a good job +def next_func(): + print("You") + diff --git a/src/test/refactor/extension.refactor.extract.method.test.ts b/src/test/refactor/extension.refactor.extract.method.test.ts deleted file mode 100644 index a1919718a7c0..000000000000 --- a/src/test/refactor/extension.refactor.extract.method.test.ts +++ /dev/null @@ -1,141 +0,0 @@ -// tslint:disable:interface-name no-any max-func-body-length estrict-plus-operands no-empty - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import { commands, Position, Range, Selection, TextEditorCursorStyle, TextEditorLineNumbersStyle, TextEditorOptions, Uri, window, workspace } from 'vscode'; -import { getTextEditsFromPatch } from '../../client/common/editor'; -import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { CondaService } from '../../client/interpreter/locators/services/condaService'; -import { extractMethod } from '../../client/providers/simpleRefactorProvider'; -import { RefactorProxy } from '../../client/refactor/proxy'; -import { getExtensionSettings } from '../common'; -import { UnitTestIocContainer } from '../testing/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: any) => Promise.resolve(); - }); - teardown(async () => { - commands.executeCommand = oldExecuteCommand; - try { - await fs.unlink(refactorTargetFile); - } catch { } - await closeActiveWindows(); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - - async function testingMethodExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise<void> { - 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<RenameResponse>(mockTextDoc, 'myNewMethod', refactorTargetFile, rangeOfTextToExtract, options); - if (shouldError) { - assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); - } - const textEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); - assert.equal(response.results.length, 1, 'Invalid number of items in response'); - assert.equal(textEdits.length, expectedTextEdits.length, 'Invalid number of Text Edits'); - textEdits.forEach(edit => { - const foundEdit = expectedTextEdits.filter(item => item.newText === edit.newText && item.range.isEqual(edit.range)); - assert.equal(foundEdit.length, 1, 'Edit not found'); - }); - } catch (error) { - if (!shouldError) { - // Wait a minute this shouldn't work, what's going on - assert.equal('Error', 'No error', `${error}`); - } - } - } - - test('Extract Method', async () => { - const startPos = new Position(239, 0); - const endPos = new Position(241, 35); - await testingMethodExtraction(false, startPos, endPos); - }); - - test('Extract Method will fail if complete statements are not selected', async () => { - const startPos = new Position(239, 30); - const endPos = new Position(241, 35); - await testingMethodExtraction(true, startPos, endPos); - }); - - async function testingMethodExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position): Promise<void> { - const ch = new MockOutputChannel('Python'); - const rangeOfTextToExtract = new Range(startPos, endPos); - - const textDocument = await workspace.openTextDocument(refactorTargetFile); - const editor = await window.showTextDocument(textDocument); - - editor.selections = [new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end)]; - editor.selection = new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end); - - try { - await extractMethod(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 7652d5398326..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 '../testing/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(); - (<any>commands).executeCommand = (_cmd: any) => Promise.resolve(); - }); - teardown(async () => { - commands.executeCommand = oldExecuteCommand; - try { - await fs.unlink(refactorTargetFile); - } catch { } - await closeActiveWindows(); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - } - - async function testingVariableExtraction(shouldError: boolean, startPos: Position, endPos: Position): Promise<void> { - 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<RenameResponse>(mockTextDoc, 'myNewVariable', refactorTargetFile, rangeOfTextToExtract, options); - if (shouldError) { - assert.fail('No error', 'Error', 'Extraction should fail with an error', ''); - } - const textEdits = getTextEditsFromPatch(mockTextDoc.getText(), DIFF); - assert.equal(response.results.length, 1, 'Invalid number of items in response'); - assert.equal(textEdits.length, expectedTextEdits.length, 'Invalid number of Text Edits'); - textEdits.forEach(edit => { - const foundEdit = expectedTextEdits.filter(item => item.newText === edit.newText && item.range.isEqual(edit.range)); - assert.equal(foundEdit.length, 1, 'Edit not found'); - }); - } catch (error) { - if (!shouldError) { - assert.equal('Error', 'No error', `${error}`); - } - } - } - - // tslint:disable-next-line:no-function-expression - test('Extract Variable', async function () { - if (isPythonVersion('3.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } else { - const startPos = new Position(234, 29); - const endPos = new Position(234, 38); - await testingVariableExtraction(false, startPos, endPos); - } - }); - - test('Extract Variable fails if whole string not selected', async () => { - const startPos = new Position(234, 20); - const endPos = new Position(234, 38); - await testingVariableExtraction(true, startPos, endPos); - }); - - async function testingVariableExtractionEndToEnd(shouldError: boolean, startPos: Position, endPos: Position): Promise<void> { - const ch = new MockOutputChannel('Python'); - const rangeOfTextToExtract = new Range(startPos, endPos); - - const textDocument = await workspace.openTextDocument(refactorTargetFile); - const editor = await window.showTextDocument(textDocument); - - editor.selections = [new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end)]; - editor.selection = new Selection(rangeOfTextToExtract.start, rangeOfTextToExtract.end); - try { - await extractVariable(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 6459701abc8e..000000000000 --- a/src/test/refactor/rename.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 { 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 { IEnvironmentActivationService } from '../../client/interpreter/activation/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { RefactorProxy } from '../../client/refactor/proxy'; -import { PYTHON_PATH } from '../common'; -import { closeActiveWindows, initialize, initializeTest } from './../initialize'; - -// tslint:disable:no-any - -type RenameResponse = { - results: [{ diff: string }]; -}; - -suite('Refactor Rename', () => { - const options: TextEditorOptions = { cursorStyle: TextEditorCursorStyle.Line, insertSpaces: true, lineNumbers: TextEditorLineNumbersStyle.Off, tabSize: 4 }; - let pythonSettings: typeMoq.IMock<IPythonSettings>; - let serviceContainer: typeMoq.IMock<IServiceContainer>; - suiteSetup(initialize); - setup(async () => { - pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); - const configService = typeMoq.Mock.ofType<IConfigurationService>(); - configService.setup(c => c.getSettings(typeMoq.It.isAny())).returns(() => pythonSettings.object); - const processServiceFactory = typeMoq.Mock.ofType<IProcessServiceFactory>(); - processServiceFactory.setup(p => p.create(typeMoq.It.isAny())).returns(() => Promise.resolve(new ProcessService(new BufferDecoder()))); - const envActivationService = typeMoq.Mock.ofType<IEnvironmentActivationService>(); - envActivationService.setup(e => e.getActivatedEnvironmentVariables(typeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(IConfigurationService), typeMoq.It.isAny())).returns(() => configService.object); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(IProcessServiceFactory), typeMoq.It.isAny())).returns(() => processServiceFactory.object); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(IEnvironmentActivationService), typeMoq.It.isAny())) - .returns(() => envActivationService.object); - serviceContainer - .setup(s => s.get(typeMoq.It.isValue(IPythonExecutionFactory), typeMoq.It.isAny())) - .returns(() => new PythonExecutionFactory(serviceContainer.object, - undefined as any, processServiceFactory.object, - configService.object, undefined as any)); - 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<RenameResponse>(textDocument, 'three', sourceFile, new Range(7, 20, 7, 23), options); - expect(response.results).to.be.lengthOf(1); - expect(response.results[0].diff.splitLines({ removeEmptyEntries: false, trim: false })).to.be.deep.equal(expectedDiff); - }); - test('Rename function in source with a trailing empty line', async () => { - const sourceFile = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'refactoring', 'source folder', 'with empty line.py'); - const expectedDiff = `--- a/${path.basename(sourceFile)}${EOL}+++ b/${path.basename(sourceFile)}${EOL}@@ -1,8 +1,8 @@${EOL} import os${EOL} ${EOL}-def one():${EOL}+def three():${EOL} return True${EOL} ${EOL} def two():${EOL}- if one():${EOL}- print(\"A\" + one())${EOL}+ if three():${EOL}+ print(\"A\" + three())${EOL}` - .splitLines({ removeEmptyEntries: false, trim: false }); - - const 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<RenameResponse>(textDocument, 'three', sourceFile, new Range(7, 20, 7, 23), options); - expect(response.results).to.be.lengthOf(1); - expect(response.results[0].diff.splitLines({ removeEmptyEntries: false, trim: false })).to.be.deep.equal(expectedDiff); - }); -}); diff --git a/src/test/repl/nativeRepl.test.ts b/src/test/repl/nativeRepl.test.ts new file mode 100644 index 000000000000..2cf18cefe1f7 --- /dev/null +++ b/src/test/repl/nativeRepl.test.ts @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable no-unused-expressions */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import { Disposable, EventEmitter, NotebookDocument, Uri } from 'vscode'; +import { expect } from 'chai'; + +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; +import * as NativeReplModule from '../../client/repl/nativeRepl'; +import * as persistentState from '../../client/common/persistentState'; +import * as PythonServer from '../../client/repl/pythonServer'; +import * as vscodeWorkspaceApis from '../../client/common/vscodeApis/workspaceApis'; +import * as replController from '../../client/repl/replController'; +import { executeCommand } from '../../client/common/vscodeApis/commandApis'; + +suite('REPL - Native REPL', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + + let disposable: TypeMoq.IMock<Disposable>; + let disposableArray: Disposable[] = []; + let setReplDirectoryStub: sinon.SinonStub; + let setReplControllerSpy: sinon.SinonSpy; + let getWorkspaceStateValueStub: sinon.SinonStub; + let updateWorkspaceStateValueStub: sinon.SinonStub; + let createReplControllerStub: sinon.SinonStub; + let mockNotebookController: any; + + setup(() => { + (NativeReplModule as any).nativeRepl = undefined; + + mockNotebookController = { + id: 'mockController', + dispose: sinon.stub(), + updateNotebookAffinity: sinon.stub(), + createNotebookCellExecution: sinon.stub(), + variableProvider: null, + }; + + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + disposable = TypeMoq.Mock.ofType<Disposable>(); + disposableArray = [disposable.object]; + + createReplControllerStub = sinon.stub(replController, 'createReplController').returns(mockNotebookController); + setReplDirectoryStub = sinon.stub(NativeReplModule.NativeRepl.prototype as any, 'setReplDirectory').resolves(); + setReplControllerSpy = sinon.spy(NativeReplModule.NativeRepl.prototype, 'setReplController'); + updateWorkspaceStateValueStub = sinon.stub(persistentState, 'updateWorkspaceStateValue').resolves(); + }); + + teardown(async () => { + disposableArray.forEach((d) => { + if (d) { + d.dispose(); + } + }); + disposableArray = []; + sinon.restore(); + executeCommand('workbench.action.closeActiveEditor'); + }); + + test('getNativeRepl should call create constructor', async () => { + const createMethodStub = sinon.stub(NativeReplModule.NativeRepl, 'create'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + expect(createMethodStub.calledOnce).to.be.true; + }); + + test('sendToNativeRepl should look for memento URI if notebook document is undefined', async () => { + getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns(undefined); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + nativeRepl.sendToNativeRepl(undefined, false); + + expect(getWorkspaceStateValueStub.calledOnce).to.be.true; + }); + + test('sendToNativeRepl should call updateWorkspaceStateValue', async () => { + getWorkspaceStateValueStub = sinon.stub(persistentState, 'getWorkspaceStateValue').returns('myNameIsMemento'); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + const interpreter = await interpreterService.object.getActiveInterpreter(); + const nativeRepl = await NativeReplModule.getNativeRepl(interpreter as PythonEnvironment, disposableArray); + + nativeRepl.sendToNativeRepl(undefined, false); + + expect(updateWorkspaceStateValueStub.calledOnce).to.be.true; + }); + + test('create should call setReplDirectory, setReplController', async () => { + const interpreter = await interpreterService.object.getActiveInterpreter(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + await NativeReplModule.NativeRepl.create(interpreter as PythonEnvironment); + + expect(setReplDirectoryStub.calledOnce).to.be.true; + expect(setReplControllerSpy.calledOnce).to.be.true; + expect(createReplControllerStub.calledOnce).to.be.true; + }); + + test('watchNotebookClosed should clean up resources when notebook is closed', async () => { + const notebookCloseEmitter = new EventEmitter<NotebookDocument>(); + sinon.stub(vscodeWorkspaceApis, 'onDidCloseNotebookDocument').callsFake((handler) => { + const disposable = notebookCloseEmitter.event(handler); + return disposable; + }); + + const mockPythonServer = { + onCodeExecuted: new EventEmitter<void>().event, + execute: sinon.stub().resolves({ status: true, output: 'test output' }), + executeSilently: sinon.stub().resolves({ status: true, output: 'test output' }), + interrupt: sinon.stub(), + input: sinon.stub(), + checkValidCommand: sinon.stub().resolves(true), + dispose: sinon.stub(), + isExecuting: false, + isDisposed: false, + }; + + // Track the number of times createPythonServer was called + let createPythonServerCallCount = 0; + sinon.stub(PythonServer, 'createPythonServer').callsFake(() => { + // eslint-disable-next-line no-plusplus + createPythonServerCallCount++; + return mockPythonServer; + }); + + const interpreter = await interpreterService.object.getActiveInterpreter(); + + // Create NativeRepl directly to have more control over its state, go around private constructor. + const nativeRepl = new (NativeReplModule.NativeRepl as any)(); + nativeRepl.interpreter = interpreter as PythonEnvironment; + nativeRepl.cwd = '/helloJustMockedCwd/cwd'; + nativeRepl.pythonServer = mockPythonServer; + nativeRepl.replController = mockNotebookController; + nativeRepl.disposables = []; + + // Make the singleton point to our instance for testing + // Otherwise, it gets mixed with Native Repl from .create from test above. + (NativeReplModule as any).nativeRepl = nativeRepl; + + // Reset call count after initial setup + createPythonServerCallCount = 0; + + // Set notebookDocument to a mock document + const mockReplUri = Uri.parse('untitled:Untitled-999.ipynb?jupyter-notebook'); + const mockNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + nativeRepl.notebookDocument = mockNotebookDocument; + + // Create a mock notebook document for closing event with same URI + const closingNotebookDocument = ({ + uri: mockReplUri, + toString: () => mockReplUri.toString(), + } as unknown) as NotebookDocument; + + notebookCloseEmitter.fire(closingNotebookDocument); + await new Promise((resolve) => setTimeout(resolve, 50)); + + expect( + updateWorkspaceStateValueStub.calledWith(NativeReplModule.NATIVE_REPL_URI_MEMENTO, undefined), + 'updateWorkspaceStateValue should be called with NATIVE_REPL_URI_MEMENTO and undefined', + ).to.be.true; + expect(mockPythonServer.dispose.calledOnce, 'pythonServer.dispose() should be called once').to.be.true; + expect(createPythonServerCallCount, 'createPythonServer should be called to create a new server').to.equal(1); + expect(nativeRepl.notebookDocument, 'notebookDocument should be undefined after closing').to.be.undefined; + expect(nativeRepl.newReplSession, 'newReplSession should be set to true after closing').to.be.true; + expect(mockNotebookController.dispose.calledOnce, 'replController.dispose() should be called once').to.be.true; + }); +}); diff --git a/src/test/repl/replCommand.test.ts b/src/test/repl/replCommand.test.ts new file mode 100644 index 000000000000..0b5edda863f9 --- /dev/null +++ b/src/test/repl/replCommand.test.ts @@ -0,0 +1,250 @@ +// Create test suite and test cases for the `replUtils` module +import * as TypeMoq from 'typemoq'; +import { commands, Disposable, Uri } from 'vscode'; +import * as sinon from 'sinon'; +import { expect } from 'chai'; +import { IInterpreterService } from '../../client/interpreter/contracts'; +import { ICommandManager } from '../../client/common/application/types'; +import { ICodeExecutionHelper } from '../../client/terminals/types'; +import * as replCommands from '../../client/repl/replCommands'; +import * as replUtils from '../../client/repl/replUtils'; +import * as nativeRepl from '../../client/repl/nativeRepl'; +import * as windowApis from '../../client/common/vscodeApis/windowApis'; +import { Commands } from '../../client/common/constants'; +import { PythonEnvironment } from '../../client/pythonEnvironments/info'; + +suite('REPL - register native repl command', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let executionHelper: TypeMoq.IMock<ICodeExecutionHelper>; + let getSendToNativeREPLSettingStub: sinon.SinonStub; + // @ts-ignore: TS6133 + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let registerCommandSpy: sinon.SinonSpy; + let executeInTerminalStub: sinon.SinonStub; + let getNativeReplStub: sinon.SinonStub; + let disposable: TypeMoq.IMock<Disposable>; + let disposableArray: Disposable[] = []; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + executionHelper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); + commandManager + .setup((cm) => cm.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => TypeMoq.Mock.ofType<Disposable>().object); + + getSendToNativeREPLSettingStub = sinon.stub(replUtils, 'getSendToNativeREPLSetting'); + getSendToNativeREPLSettingStub.returns(false); + executeInTerminalStub = sinon.stub(replUtils, 'executeInTerminal'); + executeInTerminalStub.returns(Promise.resolve()); + registerCommandSpy = sinon.spy(commandManager.object, 'registerCommand'); + disposable = TypeMoq.Mock.ofType<Disposable>(); + disposableArray = [disposable.object]; + }); + + teardown(() => { + sinon.restore(); + disposableArray.forEach((d) => { + if (d) { + d.dispose(); + } + }); + + disposableArray = []; + }); + + test('Ensure repl command is registered', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + await replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + commandManager.verify( + (c) => c.registerCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.atLeastOnce(), + ); + }); + + test('Ensure getSendToNativeREPLSetting is called', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + + let commandHandler: undefined | (() => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!(); + + sinon.assert.calledOnce(getSendToNativeREPLSettingStub); + }); + + test('Ensure executeInTerminal is called when getSendToNativeREPLSetting returns false', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + getSendToNativeREPLSettingStub.returns(false); + + let commandHandler: undefined | (() => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!(); + + sinon.assert.calledOnce(executeInTerminalStub); + }); + + test('Ensure we call getNativeREPL() when interpreter exist', async () => { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + getSendToNativeREPLSettingStub.returns(true); + getNativeReplStub = sinon.stub(nativeRepl, 'getNativeRepl'); + + let commandHandler: undefined | ((uri: string) => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!('uri'); + sinon.assert.calledOnce(getNativeReplStub); + }); + + test('Ensure we do not call getNativeREPL() when interpreter does not exist', async () => { + getNativeReplStub = sinon.stub(nativeRepl, 'getNativeRepl'); + getSendToNativeREPLSettingStub.returns(true); + + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + let commandHandler: undefined | ((uri: string) => Promise<void>); + commandManager + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .setup((c) => c.registerCommand as any) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .returns(() => (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_REPL) { + commandHandler = callback; + } + // eslint-disable-next-line no-void + return { dispose: () => void 0 }; + }); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)); + + replCommands.registerReplCommands( + disposableArray, + interpreterService.object, + executionHelper.object, + commandManager.object, + ); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + await commandHandler!('uri'); + sinon.assert.notCalled(getNativeReplStub); + }); +}); + +suite('Native REPL getActiveInterpreter', () => { + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let executeCommandStub: sinon.SinonStub; + let getActiveResourceStub: sinon.SinonStub; + + setup(() => { + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + executeCommandStub = sinon.stub(commands, 'executeCommand').resolves(undefined); + getActiveResourceStub = sinon.stub(windowApis, 'getActiveResource'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Uses active resource when uri is undefined', async () => { + const resource = Uri.file('/workspace/app.py'); + const expected = ({ path: 'ps' } as unknown) as PythonEnvironment; + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(expected)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(expected); + interpreterService.verify((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource)), TypeMoq.Times.once()); + sinon.assert.notCalled(executeCommandStub); + }); + + test('Triggers environment selection using active resource when interpreter is missing', async () => { + const resource = Uri.file('/workspace/app.py'); + getActiveResourceStub.returns(resource); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isValue(resource))) + .returns(() => Promise.resolve(undefined)); + + const result = await replUtils.getActiveInterpreter(undefined, interpreterService.object); + + expect(result).to.equal(undefined); + sinon.assert.calledWith(executeCommandStub, Commands.TriggerEnvironmentSelection, resource); + }); +}); diff --git a/src/test/repl/variableProvider.test.ts b/src/test/repl/variableProvider.test.ts new file mode 100644 index 000000000000..e401041e17d9 --- /dev/null +++ b/src/test/repl/variableProvider.test.ts @@ -0,0 +1,299 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { assert } from 'chai'; +import sinon from 'sinon'; +import { + NotebookDocument, + CancellationTokenSource, + VariablesResult, + Variable, + EventEmitter, + ConfigurationScope, + WorkspaceConfiguration, +} from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IVariableDescription } from '../../client/repl/variables/types'; +import { VariablesProvider } from '../../client/repl/variables/variablesProvider'; +import { VariableRequester } from '../../client/repl/variables/variableRequester'; +import * as workspaceApis from '../../client/common/vscodeApis/workspaceApis'; + +suite('ReplVariablesProvider', () => { + let provider: VariablesProvider; + let varRequester: TypeMoq.IMock<VariableRequester>; + let notebook: TypeMoq.IMock<NotebookDocument>; + let getConfigurationStub: sinon.SinonStub; + let configMock: TypeMoq.IMock<WorkspaceConfiguration>; + let enabled: boolean; + const executionEventEmitter = new EventEmitter<void>(); + const cancellationToken = new CancellationTokenSource().token; + + const objectVariable: IVariableDescription = { + name: 'myObject', + value: '...', + root: 'myObject', + hasNamedChildren: true, + propertyChain: [], + }; + + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 3, + root: 'myObject', + propertyChain: ['myList'], + }; + + function createListItem(index: number): IVariableDescription { + return { + name: index.toString(), + value: `value${index}`, + count: index, + root: 'myObject', + propertyChain: ['myList', index], + }; + } + + function setVariablesForParent( + parent: IVariableDescription | undefined, + result: IVariableDescription[], + updated?: IVariableDescription[], + startIndex?: number, + ) { + let returnedOnce = false; + varRequester + .setup((v) => v.getAllVariableDescriptions(parent, startIndex ?? TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + if (updated && returnedOnce) { + return Promise.resolve(updated); + } + returnedOnce = true; + return Promise.resolve(result); + }); + } + + async function provideVariables(parent: Variable | undefined, kind = 1) { + const results: VariablesResult[] = []; + for await (const result of provider.provideVariables(notebook.object, parent, kind, 0, cancellationToken)) { + results.push(result); + } + return results; + } + + setup(() => { + enabled = true; + varRequester = TypeMoq.Mock.ofType<VariableRequester>(); + notebook = TypeMoq.Mock.ofType<NotebookDocument>(); + provider = new VariablesProvider(varRequester.object, () => notebook.object, executionEventEmitter.event); + configMock = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + configMock.setup((c) => c.get<boolean>('REPL.provideVariables')).returns(() => enabled); + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + getConfigurationStub.callsFake((section?: string, _scope?: ConfigurationScope | null) => { + if (section === 'python') { + return configMock.object; + } + return undefined; + }); + }); + + teardown(() => { + sinon.restore(); + }); + + test('provideVariables without parent should yield variables', async () => { + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isNotEmpty(results); + assert.equal(results.length, 1); + assert.equal(results[0].variable.name, 'myObject'); + assert.equal(results[0].variable.expression, 'myObject'); + }); + + test('No variables are returned when variable provider is disabled', async () => { + enabled = false; + setVariablesForParent(undefined, [objectVariable]); + + const results = await provideVariables(undefined); + + assert.isEmpty(results); + }); + + test('No change event from provider when disabled', async () => { + enabled = false; + let eventFired = false; + provider.onDidChangeVariables(() => { + eventFired = true; + }); + + executionEventEmitter.fire(); + + assert.isFalse(eventFired, 'event should not have fired'); + }); + + test('Variables change event from provider should fire when execution happens', async () => { + let eventFired = false; + provider.onDidChangeVariables(() => { + eventFired = true; + }); + + executionEventEmitter.fire(); + + assert.isTrue(eventFired, 'event should have fired'); + }); + + test('provideVariables with a parent should call get children correctly', async () => { + const listVariableItems = [0, 1, 2].map(createListItem); + setVariablesForParent(undefined, [objectVariable]); + + // pass each the result as the parent in the next call + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, [listVariable]); + const listResult = (await provideVariables(rootVariable!.variable))[0]; + setVariablesForParent(listResult.variable as IVariableDescription, listVariableItems); + const listItems = await provideVariables(listResult!.variable, 2); + + assert.equal(listResult.variable.name, 'myList'); + assert.equal(listResult.variable.expression, 'myObject.myList'); + assert.isNotEmpty(listItems); + assert.equal(listItems.length, 3); + listItems.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + assert.equal(item.variable.expression, `myObject.myList[${index}]`); + }); + }); + + test('All indexed variables should be returned when requested', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting less indexed items than the specified count is handled', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + setVariablesForParent(rootVariable.variable as IVariableDescription, [], undefined, 5); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 5); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + }); + + test('Getting variables again with new execution count should get updated variables', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + setVariablesForParent(undefined, [intVariable], [{ ...intVariable, value: '2' }]); + + const first = await provideVariables(undefined); + executionEventEmitter.fire(); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + assert.equal(second[0].variable.value, '2'); + }); + + test('Getting variables again with same execution count should not make another call', async () => { + const intVariable: IVariableDescription = { + name: 'myInt', + value: '1', + root: '', + propertyChain: [], + }; + + setVariablesForParent(undefined, [intVariable]); + + const first = await provideVariables(undefined); + const second = await provideVariables(undefined); + + assert.equal(first.length, 1); + assert.equal(second.length, 1); + assert.equal(first[0].variable.value, '1'); + + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('Cache pages of indexed children correctly', async () => { + const listVariable: IVariableDescription = { + name: 'myList', + value: '[...]', + count: 6, + root: 'myList', + propertyChain: [], + }; + + const firstPage = [0, 1, 2].map(createListItem); + const secondPage = [3, 4, 5].map(createListItem); + setVariablesForParent(undefined, [listVariable]); + const rootVariable = (await provideVariables(undefined))[0]; + setVariablesForParent(rootVariable.variable as IVariableDescription, firstPage, undefined, 0); + setVariablesForParent(rootVariable.variable as IVariableDescription, secondPage, undefined, firstPage.length); + + await provideVariables(rootVariable!.variable, 2); + + // once for the parent and once for each of the two pages of list items + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); + + const listItemResult = await provideVariables(rootVariable!.variable, 2); + + assert.equal(listItemResult.length, 6, 'full list of items should be returned'); + listItemResult.forEach((item, index) => { + assert.equal(item.variable.name, index.toString()); + assert.equal(item.variable.value, `value${index}`); + }); + + // no extra calls for getting the children again + varRequester.verify( + (x) => x.getAllVariableDescriptions(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.exactly(3), + ); + }); +}); diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index c689e1c3d086..382659b3f838 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -2,51 +2,74 @@ // Licensed under the MIT License. import { Container } from 'inversify'; -import { anything, instance, mock, when } from 'ts-mockito'; +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 { EnvironmentActivationService } from '../client/interpreter/activation/service'; import { IEnvironmentActivationService } from '../client/interpreter/activation/types'; -import { IInterpreterAutoSelectionService, IInterpreterAutoSeletionProxyService } from '../client/interpreter/autoSelection/types'; -import { registerTypes as interpretersRegisterTypes } from '../client/interpreter/serviceRegistry'; +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/testing/common/constants'; import { registerTypes as unittestsRegisterTypes } from '../client/testing/serviceRegistry'; +import { LegacyFileSystem } from './legacyFileSystem'; import { MockOutputChannel } from './mockClasses'; import { MockAutoSelectionService } from './mocks/autoSelector'; import { MockMemento } from './mocks/mementos'; import { MockProcessService } from './mocks/proc'; import { MockProcess } from './mocks/process'; +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); @@ -57,80 +80,113 @@ export class IocContainer { const stdOutputChannel = new MockOutputChannel('Python'); this.disposables.push(stdOutputChannel); - this.serviceManager.addSingletonInstance<OutputChannel>(IOutputChannel, stdOutputChannel, STANDARD_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance<ILogOutputChannel>(ILogOutputChannel, stdOutputChannel); const testOutputChannel = new MockOutputChannel('Python Test - UnitTests'); this.disposables.push(testOutputChannel); - this.serviceManager.addSingletonInstance<OutputChannel>(IOutputChannel, testOutputChannel, TEST_OUTPUT_CHANNEL); + this.serviceManager.addSingletonInstance<ILogOutputChannel>(ILogOutputChannel, testOutputChannel); - this.serviceManager.addSingleton<IInterpreterAutoSelectionService>(IInterpreterAutoSelectionService, MockAutoSelectionService); - this.serviceManager.addSingleton<IInterpreterAutoSeletionProxyService>(IInterpreterAutoSeletionProxyService, MockAutoSelectionService); + this.serviceManager.addSingleton<IInterpreterAutoSelectionService>( + IInterpreterAutoSelectionService, + MockAutoSelectionService, + ); + this.serviceManager.addSingleton<IInterpreterAutoSelectionProxyService>( + IInterpreterAutoSelectionProxyService, + MockAutoSelectionService, + ); } - public async dispose() : Promise<void> { + + public async dispose(): Promise<void> { for (const disposable of this.disposables) { - if (!disposable) { - continue; - } - // tslint:disable-next-line:no-any - const promise = disposable.dispose() as Promise<any>; - if (promise) { - await promise; + if (disposable) { + const promise = disposable.dispose() as Promise<unknown>; + if (promise) { + await promise; + } } } + this.disposables = []; + this.serviceManager.dispose(); } - public registerCommonTypes(registerFileSystem: boolean = true) { + public registerCommonTypes(registerFileSystem = true): void { commonRegisterTypes(this.serviceManager); if (registerFileSystem) { this.registerFileSystemTypes(); } } - public registerFileSystemTypes() { + + public registerFileSystemTypes(): void { this.serviceManager.addSingleton<IPlatformService>(IPlatformService, PlatformService); - this.serviceManager.addSingleton<IFileSystem>(IFileSystem, FileSystem); + this.serviceManager.addSingleton<IFileSystem>( + IFileSystem, + // Maybe use fake vscode.workspace.filesystem API: + this.useVSCodeAPI ? FileSystem : LegacyFileSystem, + ); } - public registerProcessTypes() { + + public registerProcessTypes(): void { processRegisterTypes(this.serviceManager); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - this.serviceManager.addSingletonInstance<IEnvironmentActivationService>(IEnvironmentActivationService, instance(mockEnvironmentActivationService)); + const mockEnvironmentActivationService = createTypeMoq<IEnvironmentActivationService>(); + mockEnvironmentActivationService + .setup((f) => f.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); } - public registerVariableTypes() { + + public registerVariableTypes(): void { variableRegisterTypes(this.serviceManager); } - public registerUnitTestTypes() { + + public registerUnitTestTypes(): void { unittestsRegisterTypes(this.serviceManager); } - public registerLinterTypes() { - lintersRegisterTypes(this.serviceManager); - } - public registerFormatterTypes() { - formattersRegisterTypes(this.serviceManager); - } - public registerPlatformTypes() { + + public registerPlatformTypes(): void { platformRegisterTypes(this.serviceManager); } - public registerInterpreterTypes() { - 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>(IBufferDecoder, BufferDecoder); - const processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); - // tslint:disable-next-line:no-any - const processService = new MockProcessService(new ProcessService(new BufferDecoder(), process.env as any)); - processServiceFactory.setup(f => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); - this.serviceManager.addSingletonInstance<IProcessServiceFactory>(IProcessServiceFactory, processServiceFactory.object); + + public registerMockProcessTypes(): void { + const processServiceFactory = createTypeMoq<IProcessServiceFactory>(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processService = new MockProcessService(new ProcessService(process.env as any)); + processServiceFactory.setup((f) => f.create(TypeMoq.It.isAny())).returns(() => Promise.resolve(processService)); + this.serviceManager.addSingletonInstance<IProcessServiceFactory>( + IProcessServiceFactory, + processServiceFactory.object, + ); this.serviceManager.addSingleton<IPythonExecutionFactory>(IPythonExecutionFactory, PythonExecutionFactory); - this.serviceManager.addSingleton<IPythonToolExecutionService>(IPythonToolExecutionService, PythonToolExecutionService); - this.serviceManager.addSingleton<IEnvironmentActivationService>(IEnvironmentActivationService, EnvironmentActivationService); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - this.serviceManager.rebindInstance<IEnvironmentActivationService>(IEnvironmentActivationService, instance(mockEnvironmentActivationService)); + this.serviceManager.addSingleton<IPythonToolExecutionService>( + IPythonToolExecutionService, + PythonToolExecutionService, + ); + this.serviceManager.addSingleton<IEnvironmentActivationService>( + IEnvironmentActivationService, + EnvironmentActivationService, + ); + const mockEnvironmentActivationService = createTypeMoq<IEnvironmentActivationService>(); + mockEnvironmentActivationService + .setup((m) => m.getActivatedEnvironmentVariables(anything())) + .returns(() => Promise.resolve(undefined)); + this.serviceManager.rebindInstance<IEnvironmentActivationService>( + IEnvironmentActivationService, + mockEnvironmentActivationService.object, + ); + } + + public async registerMockInterpreterTypes(): Promise<void> { + this.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); + this.serviceManager.addSingleton<IRegistry>(IRegistry, RegistryImplementation); + await registerForIOC(this.serviceManager, this.serviceContainer); } - public registerMockProcess() { - this.serviceManager.addSingletonInstance<boolean>(IsWindows, IS_WINDOWS); + public registerMockProcess(): void { + this.serviceManager.addSingletonInstance<boolean>(IsWindows, isWindows()); - this.serviceManager.addSingleton<ILogger>(ILogger, Logger); this.serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils); this.serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, MockProcess); } diff --git a/src/test/smoke/common.ts b/src/test/smoke/common.ts index 45b9e3a43992..5f5b691fb496 100644 --- a/src/test/smoke/common.ts +++ b/src/test/smoke/common.ts @@ -3,57 +3,106 @@ 'use strict'; -// tslint:disable:no-any no-invalid-this no-default-export no-console - import * as assert from 'assert'; -import * as fs from 'fs-extra'; import * as glob from 'glob'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; +import { JUPYTER_EXTENSION_ID } from '../../client/common/constants'; import { SMOKE_TEST_EXTENSIONS_DIR } from '../constants'; import { noop, sleep } from '../core'; -export async function updateSetting(setting: string, value: any) { +// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any +export async function updateSetting(setting: string, value: any): Promise<void> { const resource = vscode.workspace.workspaceFolders![0].uri; await vscode.workspace .getConfiguration('python', resource) .update(setting, value, vscode.ConfigurationTarget.WorkspaceFolder); } -export async function removeLanguageServerFiles() { - const folders = await getLanaguageServerFolders(); - await Promise.all(folders.map(item => fs.remove(item).catch(noop))); +export async function removeLanguageServerFiles(): Promise<void> { + const folders = await getLanguageServerFolders(); + await Promise.all(folders.map((item) => fs.remove(item).catch(noop))); } -async function getLanaguageServerFolders(): Promise<string[]> { +async function getLanguageServerFolders(): Promise<string[]> { return new Promise<string[]>((resolve, reject) => { - glob('languageServer.*', { cwd: SMOKE_TEST_EXTENSIONS_DIR }, (ex, matches) => { - ex ? reject(ex) : resolve(matches.map(item => path.join(SMOKE_TEST_EXTENSIONS_DIR, item))); + glob.default('languageServer.*', { cwd: SMOKE_TEST_EXTENSIONS_DIR }, (ex, matches) => { + if (ex) { + reject(ex); + } else { + resolve(matches.map((item) => path.join(SMOKE_TEST_EXTENSIONS_DIR, item))); + } }); }); } -export function isJediEnabled() { +export function isJediEnabled(): boolean { const resource = vscode.workspace.workspaceFolders![0].uri; const settings = vscode.workspace.getConfiguration('python', resource); - return settings.get<boolean>('jediEnabled') === true; + return settings.get<string>('languageServer') === 'Jedi'; } -export async function enableJedi(enable: boolean | undefined) { +export async function enableJedi(enable: boolean | undefined): Promise<void> { if (isJediEnabled() === enable) { return; } - await updateSetting('jediEnabled', enable); + await updateSetting('languageServer', 'Jedi'); } -export async function openFileAndWaitForLS(file: string): Promise<vscode.TextDocument> { - const textDocument = await vscode.workspace.openTextDocument(file); - await vscode.window.showTextDocument(textDocument); - assert(vscode.window.activeTextEditor, 'No active editor'); + +export async function openNotebook(file: string): Promise<vscode.NotebookDocument> { + await verifyExtensionIsAvailable(JUPYTER_EXTENSION_ID); + await vscode.commands.executeCommand('vscode.openWith', vscode.Uri.file(file), 'jupyter-notebook'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const notebook = (vscode.window.activeTextEditor!.document as any | undefined)?.notebook as vscode.NotebookDocument; + assert.ok(notebook, 'Notebook did not open'); + return notebook; +} + +export async function openNotebookAndWaitForLS(file: string): Promise<vscode.NotebookDocument> { + const notebook = await openNotebook(file); // Make sure LS completes file loading and analysis. // In test mode it awaits for the completion before trying // to fetch data for completion, hover.etc. await vscode.commands.executeCommand( 'vscode.executeCompletionItemProvider', - textDocument.uri, - new vscode.Position(0, 0) + notebook.cellAt(0).document.uri, + new vscode.Position(0, 0), ); // For for LS to get extracted. await sleep(10_000); + return notebook; +} + +export async function openFileAndWaitForLS(file: string): Promise<vscode.TextDocument> { + const textDocument = await vscode.workspace.openTextDocument(file).then( + (result) => result, + (err) => { + assert.fail(`Something went wrong opening the text document: ${err}`); + }, + ); + await vscode.window.showTextDocument(textDocument).then(undefined, (err) => { + assert.fail(`Something went wrong showing the text document: ${err}`); + }); + assert.ok(vscode.window.activeTextEditor, 'No active editor'); + // Make sure LS completes file loading and analysis. + // In test mode it awaits for the completion before trying + // to fetch data for completion, hover.etc. + await vscode.commands + .executeCommand<vscode.CompletionList>( + 'vscode.executeCompletionItemProvider', + textDocument.uri, + new vscode.Position(0, 0), + ) + .then(undefined, (err) => { + assert.fail(`Something went wrong opening the file: ${err}`); + }); + // For for LS to get extracted. + await sleep(10_000); return textDocument; } + +export async function verifyExtensionIsAvailable(extensionId: string): Promise<void> { + const extension = vscode.extensions.all.find((e) => e.id === extensionId); + assert.ok( + extension, + `Extension ${extensionId} not installed. ${JSON.stringify(vscode.extensions.all.map((e) => e.id))}`, + ); + await extension.activate(); +} diff --git a/src/test/smoke/datascience.smoke.test.ts b/src/test/smoke/datascience.smoke.test.ts 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<void>('jupyter.runallcells', textDocument.uri).then(undefined, (err) => { + assert.fail(`Something went wrong running all cells in the interactive window: ${err}`); + }); + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, timeoutForCellToRun, `"${outputFile}" file not created`); + }).timeout(timeoutForCellToRun); + + 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<void>('jupyter.notebookeditor.runallcells').then(undefined, (err) => { + assert.fail(`Something went wrong running all cells in the native editor: ${err}`); + }); + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, timeoutForCellToRun, `"${outputFile}" file not created`); + + // 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 4be62b9f7894..000000000000 --- a/src/test/smoke/debugger.smoke.test.ts +++ /dev/null @@ -1,60 +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, initialize, initializeTest } from '../initialize'; - -suite('Smoke Test: Debug file', () => { - suiteSetup(async function () { - if (!IS_SMOKE_TEST) { - return this.skip(); - } - await initialize(); - }); - 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, - args: [outputFile] - }; - - 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, `"${outputFile}" file not created`); - }); -}); diff --git a/src/test/smoke/jedilsp.smoke.test.ts b/src/test/smoke/jedilsp.smoke.test.ts new file mode 100644 index 000000000000..a2087ff42085 --- /dev/null +++ b/src/test/smoke/jedilsp.smoke.test.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import * as path from 'path'; +import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; +import { openFile, waitForCondition } from '../common'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; + +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; + +suite('Smoke Test: Jedi LSP', () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + test('Verify diagnostics on a python file', async () => { + const file = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'test', 'python_files', 'intellisense', 'test.py'); + const outputFile = path.join(path.dirname(file), 'ds.log'); + if (await fs.pathExists(outputFile)) { + await fs.unlink(outputFile); + } + const textDocument = await openFile(file); + + waitForCondition( + async () => { + const diagnostics = vscode.languages.getDiagnostics(textDocument.uri); + return diagnostics && diagnostics.length >= 1; + }, + 60_000, + `No diagnostics found in file with invalid syntax`, + ); + }); +}); diff --git a/src/test/smoke/languageServer.smoke.test.ts b/src/test/smoke/languageServer.smoke.test.ts deleted file mode 100644 index 72a072f78b31..000000000000 --- a/src/test/smoke/languageServer.smoke.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length no-invalid-this no-any - -import * as assert from 'assert'; -import { expect } from 'chai'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { updateSetting } from '../common'; -import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; -import { sleep } from '../core'; -import { closeActiveWindows, initialize, initializeTest } from '../initialize'; -import { openFileAndWaitForLS } from './common'; - -const fileDefinitions = path.join( - EXTENSION_ROOT_DIR_FOR_TESTS, - 'src', - 'testMultiRootWkspc', - 'smokeTests', - 'definitions.py' -); - -suite('Smoke Test: Language Server', () => { - suiteSetup(async function() { - if (!IS_SMOKE_TEST) { - return this.skip(); - } - await updateSetting( - 'linting.ignorePatterns', - ['**/dir1/**'], - vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder - ); - await initialize(); - }); - setup(async () => { - await initializeTest(); - await closeActiveWindows(); - }); - suiteTeardown(async () => { - await closeActiveWindows(); - await updateSetting( - 'linting.ignorePatterns', - undefined, - vscode.workspace.workspaceFolders![0].uri, - vscode.ConfigurationTarget.WorkspaceFolder - ); - }); - teardown(closeActiveWindows); - - test('Definitions', async () => { - const startPosition = new vscode.Position(13, 6); - const textDocument = await openFileAndWaitForLS(fileDefinitions); - let tested = false; - for (let i = 0; i < 5; i += 1) { - const locations = await vscode.commands.executeCommand<vscode.Location[]>( - 'vscode.executeDefinitionProvider', - textDocument.uri, - startPosition - ); - if (locations && locations.length > 0) { - expect(locations![0].uri.fsPath).to.contain(path.basename(fileDefinitions)); - tested = true; - break; - } else { - // Wait for LS to start. - await sleep(5_000); - } - } - if (!tested) { - assert.fail('Failled to test definitions'); - } - }); -}); diff --git a/src/test/smoke/runInTerminal.smoke.test.ts b/src/test/smoke/runInTerminal.smoke.test.ts index 013e6bff396d..4bdec0843862 100644 --- a/src/test/smoke/runInTerminal.smoke.test.ts +++ b/src/test/smoke/runInTerminal.smoke.test.ts @@ -3,11 +3,10 @@ 'use strict'; -// tslint:disable:max-func-body-length no-invalid-this no-any - -import * as fs from 'fs-extra'; +import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; +import * as fs from '../../client/common/platform/fs-paths'; import { openFile, waitForCondition } from '../common'; import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; import { closeActiveWindows, initialize, initializeTest } from '../initialize'; @@ -18,32 +17,44 @@ suite('Smoke Test: Run Python File In Terminal', () => { return this.skip(); } await initialize(); + // Ensure the environments extension is not used for this test + await vscode.workspace + .getConfiguration('python') + .update('useEnvironmentsExtension', false, vscode.ConfigurationTarget.Global); + return undefined; }); + setup(initializeTest); suiteTeardown(closeActiveWindows); teardown(closeActiveWindows); - test('Exec', async () => { + // TODO: Re-enable this test once the flakiness on Windows is resolved + test('Exec', async function () { + if (process.platform === 'win32') { + return this.skip(); + } const file = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', - 'testExecInTerminal.py' + 'testExecInTerminal.py', ); const outputFile = path.join( EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testMultiRootWkspc', 'smokeTests', - 'testExecInTerminal.log' + 'testExecInTerminal.log', ); if (await fs.pathExists(outputFile)) { await fs.unlink(outputFile); } const textDocument = await openFile(file); - await vscode.commands.executeCommand<void>('python.execInTerminal', textDocument.uri); + await vscode.commands.executeCommand<void>('python.execInTerminal', textDocument.uri).then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); await waitForCondition(checkIfFileHasBeenCreated, 30_000, `"${outputFile}" file not created`); }); diff --git a/src/test/smoke/smartSend.smoke.test.ts b/src/test/smoke/smartSend.smoke.test.ts new file mode 100644 index 000000000000..cae41cc094d5 --- /dev/null +++ b/src/test/smoke/smartSend.smoke.test.ts @@ -0,0 +1,84 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; +import { assert } from 'chai'; +import * as fs from '../../client/common/platform/fs-paths'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, IS_SMOKE_TEST } from '../constants'; +import { closeActiveWindows, initialize, initializeTest } from '../initialize'; +import { openFile, waitForCondition } from '../common'; + +suite('Smoke Test: Run Smart Selection and Advance Cursor', async () => { + suiteSetup(async function () { + if (!IS_SMOKE_TEST) { + return this.skip(); + } + await initialize(); + return undefined; + }); + + setup(initializeTest); + suiteTeardown(closeActiveWindows); + teardown(closeActiveWindows); + + // TODO: Re-enable this test once the flakiness on Windows, linux are resolved + test.skip('Smart Send', async function () { + const file = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'create_delete_file.py', + ); + const outputFile = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testMultiRootWkspc', + 'smokeTests', + 'smart_send_smoke.txt', + ); + + await fs.remove(outputFile); + + const textDocument = await openFile(file); + + if (vscode.window.activeTextEditor) { + const myPos = new vscode.Position(0, 0); + vscode.window.activeTextEditor!.selections = [new vscode.Selection(myPos, myPos)]; + } + await vscode.commands + .executeCommand<void>('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + const checkIfFileHasBeenCreated = () => fs.pathExists(outputFile); + await waitForCondition(checkIfFileHasBeenCreated, 20_000, `"${outputFile}" file not created`); + + await vscode.commands + .executeCommand<void>('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + await vscode.commands + .executeCommand<void>('python.execSelectionInTerminal', textDocument.uri) + .then(undefined, (err) => { + assert.fail(`Something went wrong running the Python file in the terminal: ${err}`); + }); + + async function wait() { + return new Promise<void>((resolve) => { + setTimeout(() => { + resolve(); + }, 10000); + }); + } + + await wait(); + + const deletedFile = !(await fs.pathExists(outputFile)); + if (deletedFile) { + assert.ok(true, `"${outputFile}" file has been deleted`); + } else { + assert.fail(`"${outputFile}" file still exists`); + } + }); +}); diff --git a/src/test/smokeTest.ts b/src/test/smokeTest.ts index 0e5b385c30fc..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() { + private async launchSmokeTests() { const env: Record<string, {}> = { VSC_PYTHON_SMOKE_TEST: '1', - CODE_EXTENSIONS_PATH: SMOKE_TEST_EXTENSIONS_DIR + CODE_EXTENSIONS_PATH: SMOKE_TEST_EXTENSIONS_DIR, }; await this.launchTest(env); } - private async enableLanguageServer(enable: boolean) { - 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: Record<string, {}>) { - await new Promise((resolve, reject) => { - const env: Record<string, {}> = { + private async launchTest(customEnvVars: Record<string, {}>) { + console.log('Launch tests in test runner'); + await new Promise<void>((resolve, reject) => { + const env: Record<string, string> = { 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,12 +79,14 @@ class TestRunner { } private async extractLatestExtension(targetDir: string): Promise<void> { - const extensionFile = await new Promise<string>((resolve, reject) => glob('*.vsix', (ex, files) => ex ? reject(ex) : resolve(files[0]))); + const extensionFile = await new Promise<string>((resolve, reject) => + glob.default('*.vsix', (ex, files) => (ex ? reject(ex) : resolve(files[0]))), + ); await unzip(extensionFile, targetDir); } } -new TestRunner().start().catch(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.test.ts b/src/test/sourceMapSupport.test.ts deleted file mode 100644 index 67295bd7157d..000000000000 --- a/src/test/sourceMapSupport.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any no-unused-expression chai-vague-errors no-unnecessary-override max-func-body-length max-classes-per-file - -import { expect } from 'chai'; -import * as fs from 'fs'; -import { ConfigurationTarget, Disposable } from 'vscode'; -import { FileSystem } from '../client/common/platform/fileSystem'; -import { PlatformService } from '../client/common/platform/platformService'; -import { Diagnostics } from '../client/common/utils/localize'; -import { SourceMapSupport } from '../client/sourceMapSupport'; -import { noop } from './core'; - -suite('Source Map Support', () => { - function createVSCStub(isEnabled: boolean = false, selectDisableButton: boolean = false) { - const stubInfo = { - configValueRetrieved: false, - configValueUpdated: false, - messageDisplayed: false - }; - const vscode = { - workspace: { - getConfiguration: (setting: string, _defaultValue: any) => { - if (setting !== 'python.diagnostics') { - return; - } - return { - get: (prop: string) => { - stubInfo.configValueRetrieved = prop === 'sourceMapsEnabled'; - return isEnabled; - }, - update: (prop: string, value: boolean, scope: ConfigurationTarget) => { - if ( - prop === 'sourceMapsEnabled' && - value === false && - scope === ConfigurationTarget.Global - ) { - stubInfo.configValueUpdated = true; - } - } - }; - } - }, - window: { - showWarningMessage: () => { - stubInfo.messageDisplayed = true; - return Promise.resolve(selectDisableButton ? Diagnostics.disableSourceMaps() : undefined); - } - }, - ConfigurationTarget: ConfigurationTarget - }; - return { stubInfo, vscode }; - } - - const disposables: Disposable[] = []; - teardown(() => { - disposables.forEach(disposable => { - try { - disposable.dispose(); - } catch { - noop(); - } - }); - }); - test('When disabling source maps, the map file is renamed and vice versa', async () => { - const fileSystem = new FileSystem(new PlatformService()); - const jsFile = await fileSystem.createTemporaryFile('.js'); - disposables.push(jsFile); - const mapFile = `${jsFile.filePath}.map`; - disposables.push({ - dispose: () => fs.unlinkSync(mapFile) - }); - await fileSystem.writeFile(mapFile, 'ABC'); - expect(await fileSystem.fileExists(mapFile)).to.be.true; - - const stub = createVSCStub(true, true); - const instance = new class extends SourceMapSupport { - public async enableSourceMap(enable: boolean, sourceFile: string) { - return super.enableSourceMap(enable, sourceFile); - } - }(stub.vscode as any); - - await instance.enableSourceMap(false, jsFile.filePath); - - expect(await fileSystem.fileExists(jsFile.filePath)).to.be.equal(true, 'Source file does not exist'); - expect(await fileSystem.fileExists(mapFile)).to.be.equal(false, 'Source map file not renamed'); - expect(await fileSystem.fileExists(`${mapFile}.disabled`)).to.be.equal(true, 'Expected renamed file not found'); - - await instance.enableSourceMap(true, jsFile.filePath); - - expect(await fileSystem.fileExists(jsFile.filePath)).to.be.equal(true, 'Source file does not exist'); - expect(await fileSystem.fileExists(mapFile)).to.be.equal(true, 'Source map file not found'); - expect(await fileSystem.fileExists(`${mapFile}.disabled`)).to.be.equal(false, 'Source map file not renamed'); - }); -}); diff --git a/src/test/sourceMapSupport.unit.test.ts b/src/test/sourceMapSupport.unit.test.ts deleted file mode 100644 index 3932a236a83f..000000000000 --- a/src/test/sourceMapSupport.unit.test.ts +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any no-unused-expression chai-vague-errors no-unnecessary-override max-func-body-length max-classes-per-file - -import { expect } from 'chai'; -import * as path from 'path'; -import { ConfigurationTarget, Disposable } from 'vscode'; -import { Diagnostics } from '../client/common/utils/localize'; -import { EXTENSION_ROOT_DIR } from '../client/constants'; -import { initialize, SourceMapSupport } from '../client/sourceMapSupport'; -import { noop, sleep } from './core'; - -suite('Source Map Support', () => { - function createVSCStub(isEnabled: boolean = false, selectDisableButton: boolean = false) { - const stubInfo = { - configValueRetrieved: false, - configValueUpdated: false, - messageDisplayed: false - }; - const vscode = { - workspace: { - getConfiguration: (setting: string, _defaultValue: any) => { - if (setting !== 'python.diagnostics') { - return; - } - return { - get: (prop: string) => { - stubInfo.configValueRetrieved = prop === 'sourceMapsEnabled'; - return isEnabled; - }, - update: (prop: string, value: boolean, scope: ConfigurationTarget) => { - if (prop === 'sourceMapsEnabled' && value === false && scope === ConfigurationTarget.Global) { - stubInfo.configValueUpdated = true; - } - } - }; - } - }, - window: { - showWarningMessage: () => { - stubInfo.messageDisplayed = true; - return Promise.resolve(selectDisableButton ? Diagnostics.disableSourceMaps() : undefined); - } - }, - ConfigurationTarget: ConfigurationTarget - }; - return { stubInfo, vscode }; - } - - const disposables: Disposable[] = []; - teardown(() => { - disposables.forEach(disposable => { - try { - disposable.dispose(); - } catch { noop(); } - }); - }); - test('Test message is not displayed when source maps are not enabled', async () => { - const stub = createVSCStub(false); - initialize(stub.vscode as any); - await sleep(100); - expect(stub.stubInfo.configValueRetrieved).to.be.equal(true, 'Config Value not retrieved'); - expect(stub.stubInfo.messageDisplayed).to.be.equal(false, 'Message displayed'); - }); - test('Test message is not displayed when source maps are not enabled', async () => { - const stub = createVSCStub(true); - const instance = new class extends SourceMapSupport { - protected async enableSourceMaps(_enable: boolean) { - 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 SourceMapSupport { - protected async enableSourceMaps(_enable: boolean) { - 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'); - }); - async function testRenamingFilesWhenEnablingDisablingSourceMaps(enableSourceMaps: boolean) { - const stub = createVSCStub(true, true); - const sourceFilesPassed: string[] = []; - const instance = new class extends SourceMapSupport { - public async enableSourceMaps(enable: boolean) { - return super.enableSourceMaps(enable); - } - public async enableSourceMap(enable: boolean, sourceFile: string) { - expect(enable).to.equal(enableSourceMaps); - sourceFilesPassed.push(sourceFile); - return Promise.resolve(); - } - }(stub.vscode as any); - - await instance.enableSourceMaps(enableSourceMaps); - const extensionSourceMap = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'extension.js'); - const debuggerSourceMap = path.join(EXTENSION_ROOT_DIR, 'out', 'client', 'debugger', 'debugAdapter', 'main.js'); - expect(sourceFilesPassed).to.deep.equal([extensionSourceMap, debuggerSourceMap]); - } - test('Rename extension and debugger source maps when enabling source maps', () => testRenamingFilesWhenEnablingDisablingSourceMaps(true)); - test('Rename extension and debugger source maps when disabling source maps', () => testRenamingFilesWhenEnablingDisablingSourceMaps(false)); -}); diff --git a/src/test/standardTest.ts b/src/test/standardTest.ts index 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<IServiceContainer>; + let experimentsManager: TypeMoq.IMock<IExperimentService>; + let interpreterPathService: TypeMoq.IMock<IInterpreterPathService>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + experimentsManager = TypeMoq.Mock.ofType<IExperimentService>(); + interpreterPathService = TypeMoq.Mock.ofType<IInterpreterPathService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); + 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/importTracker.unit.test.ts b/src/test/telemetry/importTracker.unit.test.ts deleted file mode 100644 index b074ebaca04d..000000000000 --- a/src/test/telemetry/importTracker.unit.test.ts +++ /dev/null @@ -1,262 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. -'use strict'; -//tslint:disable:max-func-body-length match-default-export-name no-any no-multiline-string no-trailing-whitespace -import { expect } from 'chai'; -import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; -import { EventEmitter, TextDocument } from 'vscode'; - -import { IDocumentManager } from '../../client/common/application/types'; -import { EventName } from '../../client/telemetry/constants'; -import { ImportTracker } from '../../client/telemetry/importTracker'; -import { createDocument } from '../datascience/editor-integration/helpers'; - -suite('Import Tracker', () => { - const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; - const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; - // tslint:disable-next-line:no-require-imports - const hashJs = require('hash.js'); - let importTracker: ImportTracker; - let documentManager: TypeMoq.IMock<IDocumentManager>; - let openedEventEmitter: EventEmitter<TextDocument>; - let savedEventEmitter: EventEmitter<TextDocument>; - const pandasHash = hashJs.sha256().update('pandas').digest('hex'); - const elephasHash = hashJs.sha256().update('elephas').digest('hex'); - const kerasHash = hashJs.sha256().update('keras').digest('hex'); - const pysparkHash = hashJs.sha256().update('pyspark').digest('hex'); - const sparkdlHash = hashJs.sha256().update('sparkdl').digest('hex'); - const numpyHash = hashJs.sha256().update('numpy').digest('hex'); - const scipyHash = hashJs.sha256().update('scipy').digest('hex'); - const sklearnHash = hashJs.sha256().update('sklearn').digest('hex'); - const randomHash = hashJs.sha256().update('random').digest('hex'); - - class Reporter { - public static eventNames: string[] = []; - public static properties: Record<string, string>[] = []; - public static measures: {}[] = []; - public sendTelemetryEvent(eventName: string, properties?: {}, measures?: {}) { - Reporter.eventNames.push(eventName); - Reporter.properties.push(properties!); - Reporter.measures.push(measures!); - } - } - - setup(() => { - process.env.VSC_PYTHON_UNIT_TEST = undefined; - process.env.VSC_PYTHON_CI_TEST = undefined; - - openedEventEmitter = new EventEmitter<TextDocument>(); - savedEventEmitter = new EventEmitter<TextDocument>(); - - documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - documentManager.setup(a => a.onDidOpenTextDocument).returns(() => openedEventEmitter.event); - documentManager.setup(a => a.onDidSaveTextDocument).returns(() => savedEventEmitter.event); - - rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); - - importTracker = new ImportTracker(documentManager.object); - }); - teardown(() => { - process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; - process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; - Reporter.properties = []; - Reporter.eventNames = []; - Reporter.measures = []; - rewiremock.disable(); - - }); - - function emitDocEvent(code: string, ev: EventEmitter<TextDocument>) { - const textDoc = createDocument(code, 'foo.py', 1, TypeMoq.Times.atMost(100), true); - ev.fire(textDoc.object); - } - - test('Open document', () => { - emitDocEvent('import pandas\r\n', openedEventEmitter); - - expect(Reporter.eventNames).to.deep.equal([EventName.HASHED_PACKAGE_NAME]); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('Already opened documents', async () => { - const doc = createDocument('import pandas\r\n', 'foo.py', 1, TypeMoq.Times.atMost(100), true); - documentManager.setup(d => d.textDocuments).returns(() => [doc.object]); - await importTracker.activate(); - - expect(Reporter.eventNames).to.deep.equal([EventName.HASHED_PACKAGE_NAME]); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('Save document', () => { - emitDocEvent('import pandas\r\n', savedEventEmitter); - - expect(Reporter.eventNames).to.deep.equal([EventName.HASHED_PACKAGE_NAME]); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('from <pkg>._ import _, _', () => { - const elephas = ` - from elephas.java import java_classes, adapter - from keras.models import Sequential - from keras.layers import Dense - - - model = Sequential() - model.add(Dense(units=64, activation='relu', input_dim=100)) - model.add(Dense(units=10, activation='softmax')) - model.compile(loss='categorical_crossentropy', optimizer='sgd', metrics=['accuracy']) - - model.save('test.h5') - - - kmi = java_classes.KerasModelImport - file = java_classes.File("test.h5") - - java_model = kmi.importKerasSequentialModelAndWeights(file.absolutePath) - - weights = adapter.retrieve_keras_weights(java_model) - model.set_weights(weights)`; - - emitDocEvent(elephas, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: elephasHash}, {hashedName: kerasHash}]); - }); - - test('from <pkg>._ import _', () => { - const pyspark = `from pyspark.ml.classification import LogisticRegression - from pyspark.ml.evaluation import MulticlassClassificationEvaluator - from pyspark.ml import Pipeline - from sparkdl import DeepImageFeaturizer - - featurizer = DeepImageFeaturizer(inputCol="image", outputCol="features", modelName="InceptionV3") - lr = LogisticRegression(maxIter=20, regParam=0.05, elasticNetParam=0.3, labelCol="label") - p = Pipeline(stages=[featurizer, lr]) - - model = p.fit(train_images_df) # train_images_df is a dataset of images and labels - - # Inspect training error - df = model.transform(train_images_df.limit(10)).select("image", "probability", "uri", "label") - predictionAndLabels = df.select("prediction", "label") - evaluator = MulticlassClassificationEvaluator(metricName="accuracy") - print("Training set accuracy = " + str(evaluator.evaluate(predictionAndLabels)))`; - - emitDocEvent(pyspark, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pysparkHash}, {hashedName: sparkdlHash}]); - }); - - test('import <pkg> as _', () => { - const code = `import pandas as pd -import numpy as np -import random as rnd - -def simplify_ages(df): - df.Age = df.Age.fillna(-0.5) - bins = (-1, 0, 5, 12, 18, 25, 35, 60, 120) - group_names = ['Unknown', 'Baby', 'Child', 'Teenager', 'Student', 'Young Adult', 'Adult', 'Senior'] - categories = pd.cut(df.Age, bins, labels=group_names) - df.Age = categories - return df`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}, {hashedName: numpyHash}, {hashedName: randomHash}]); - }); - - test('from <pkg> import _', () => { - const code = `from scipy import special -def drumhead_height(n, k, distance, angle, t): - kth_zero = special.jn_zeros(n, k)[-1] - return np.cos(t) * np.cos(n*angle) * special.jn(n, distance*kth_zero) -theta = np.r_[0:2*np.pi:50j] -radius = np.r_[0:1:50j] -x = np.array([r * np.cos(theta) for r in radius]) -y = np.array([r * np.sin(theta) for r in radius]) -z = np.array([drumhead_height(1, 1, r, theta, 0.5) for r in radius])`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: scipyHash}]); - }); - - - test('from <pkg> import _ as _', () => { - const code = `from pandas import DataFrame as df`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('import <pkg1>, <pkg2>', () => { - const code = ` -def drumhead_height(n, k, distance, angle, t): - import sklearn, pandas - return np.cos(t) * np.cos(n*angle) * special.jn(n, distance*kth_zero) -theta = np.r_[0:2*np.pi:50j] -radius = np.r_[0:1:50j] -x = np.array([r * np.cos(theta) for r in radius]) -y = np.array([r * np.sin(theta) for r in radius]) -z = np.array([drumhead_height(1, 1, r, theta, 0.5) for r in radius])`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: sklearnHash}, {hashedName: pandasHash}]); - }); - - /*test('from <pkg> import (_, _)', () => { - const code = `from pandas import (DataFrame, Series)`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('from <pkg> import (_,', () => { - const code = ` -from pandas import (DataFrame, -Series)`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - });*/ - - test('import pkg # Comment', () => { - const code = `import pandas # Because we wants it.`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('Import from within a function', () => { - const code = ` -def drumhead_height(n, k, distance, angle, t): - import sklearn as sk - return np.cos(t) * np.cos(n*angle) * special.jn(n, distance*kth_zero) -theta = np.r_[0:2*np.pi:50j] -radius = np.r_[0:1:50j] -x = np.array([r * np.cos(theta) for r in radius]) -y = np.array([r * np.sin(theta) for r in radius]) -z = np.array([drumhead_height(1, 1, r, theta, 0.5) for r in radius])`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: sklearnHash}]); - }); - - test('Do not send the same package twice', () => { - const code = ` -import pandas -import pandas`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([{hashedName: pandasHash}]); - }); - - test('Ignore relative imports', () => { - const code = 'from .pandas import not_real'; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([]); - }); - - test('Ignore docstring for `from` imports', () => { - const code = `""" -from numpy import the random function -"""`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([]); - }); - - test('Ignore docstring for `import` imports', () => { - const code = `""" -import numpy for all the things -"""`; - emitDocEvent(code, savedEventEmitter); - expect(Reporter.properties).to.deep.equal([]); - }); -}); diff --git a/src/test/telemetry/index.unit.test.ts b/src/test/telemetry/index.unit.test.ts index 0cbe9cd60a6b..d8a6b72eedc6 100644 --- a/src/test/telemetry/index.unit.test.ts +++ b/src/test/telemetry/index.unit.test.ts @@ -2,28 +2,29 @@ // Licensed under the MIT License. 'use strict'; -//tslint:disable:max-func-body-length match-default-export-name no-any import { expect } from 'chai'; import rewiremock from 'rewiremock'; -import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as fs from '../../client/common/platform/fs-paths'; -import { instance, mock, verify, when } from 'ts-mockito'; -import { WorkspaceConfiguration } from 'vscode'; -import { IWorkspaceService } from '../../client/common/application/types'; -import { WorkspaceService } from '../../client/common/application/workspace'; -import { EXTENSION_ROOT_DIR } from '../../client/constants'; -import { clearTelemetryReporter, isTelemetryDisabled, sendTelemetryEvent } from '../../client/telemetry'; -import { correctPathForOsType } from '../common'; +import { + _resetSharedProperties, + clearTelemetryReporter, + sendTelemetryEvent, + setSharedProperty, +} from '../../client/telemetry'; suite('Telemetry', () => { - let workspaceService: IWorkspaceService; const oldValueOfVSC_PYTHON_UNIT_TEST = process.env.VSC_PYTHON_UNIT_TEST; const oldValueOfVSC_PYTHON_CI_TEST = process.env.VSC_PYTHON_CI_TEST; + let readJSONSyncStub: sinon.SinonStub; class Reporter { public static eventName: string[] = []; public static properties: Record<string, string>[] = []; public static measures: {}[] = []; + public static exception: Error | undefined; + public static clear() { Reporter.eventName = []; Reporter.properties = []; @@ -34,12 +35,19 @@ suite('Telemetry', () => { 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(() => { - workspaceService = mock(WorkspaceService); process.env.VSC_PYTHON_UNIT_TEST = undefined; process.env.VSC_PYTHON_CI_TEST = undefined; + readJSONSyncStub = sinon.stub(fs, 'readJSONSync'); + readJSONSyncStub.returns({ enableTelemetry: true }); clearTelemetryReporter(); Reporter.clear(); }); @@ -47,57 +55,27 @@ suite('Telemetry', () => { process.env.VSC_PYTHON_UNIT_TEST = oldValueOfVSC_PYTHON_UNIT_TEST; process.env.VSC_PYTHON_CI_TEST = oldValueOfVSC_PYTHON_CI_TEST; rewiremock.disable(); - }); - - const testsForisTelemetryDisabled = - [ - { - testName: 'Returns true when globalValue is set to false', - settings: { globalValue: false }, - expectedResult: true - }, - { - testName: 'Returns false otherwise', - settings: {}, - expectedResult: false - } - ]; - - suite('Function isTelemetryDisabled()', () => { - testsForisTelemetryDisabled.forEach(testParams => { - test(testParams.testName, async () => { - const workspaceConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); - when(workspaceService.getConfiguration('telemetry')).thenReturn(workspaceConfig.object); - workspaceConfig.setup(c => c.inspect<string>('enableTelemetry')) - .returns(() => testParams.settings as any) - .verifiable(TypeMoq.Times.once()); - - expect(isTelemetryDisabled(instance(workspaceService))).to.equal(testParams.expectedResult); - - verify(workspaceService.getConfiguration('telemetry')).once(); - workspaceConfig.verifyAll(); - }); - }); + _resetSharedProperties(); + sinon.restore(); }); test('Send Telemetry', () => { rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; const properties = { hello: 'world', foo: 'bar' }; const measures = { start: 123, end: 987 }; - // tslint:disable-next-line:no-any sendTelemetryEvent(eventName as any, measures, properties as any); expect(Reporter.eventName).to.deep.equal([eventName]); expect(Reporter.measures).to.deep.equal([measures]); expect(Reporter.properties).to.deep.equal([properties]); }); - test('Send Telemetry', () => { + test('Send Telemetry with no properties', () => { rewiremock.enable(); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; @@ -107,163 +85,59 @@ suite('Telemetry', () => { expect(Reporter.measures).to.deep.equal([undefined], 'Measures should be empty'); expect(Reporter.properties).to.deep.equal([{}], 'Properties should be empty'); }); - test('Send Error Telemetry', () => { + test('Send Telemetry with shared properties', () => { rewiremock.enable(); - const error = new Error('Boo'); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; const properties = { hello: 'world', foo: 'bar' }; const measures = { start: 123, end: 987 }; + const expectedProperties = { ...properties, one: 'two' }; - // tslint:disable-next-line:no-any - sendTelemetryEvent(eventName as any, measures, properties as any, error); + setSharedProperty('one' as any, 'two' as any); - const expectedErrorProperties = { - originalEventName: eventName - }; + sendTelemetryEvent(eventName as any, measures, properties as any); - expect(Reporter.eventName).to.deep.equal(['ERROR', eventName]); - expect(Reporter.measures).to.deep.equal([measures, measures]); - expect(Reporter.properties[0].stackTrace).to.be.length.greaterThan(1); - delete Reporter.properties[0].stackTrace; - expect(Reporter.properties).to.deep.equal([expectedErrorProperties, properties]); + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); }); - test('Send Error Telemetry', () => { + test('Shared properties will replace existing ones', () => { rewiremock.enable(); - const error = new Error('Boo'); - error.stack = correctPathForOsType(['Error: Boo', - `at Context.test (${EXTENSION_ROOT_DIR}/src/test/telemetry/index.unit.test.ts:50:23)`, - `at callFn (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runnable.js:372:21)`, - `at Test.Runnable.run (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runnable.js:364:7)`, - `at Runner.runTest (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:455:10)`, - `at ${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:573:12`, - `at next (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:369:14)`, - `at ${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:379:7`, - `at next (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:303:14)`, - `at ${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:342:7`, - `at done (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runnable.js:319:5)`, - `at callFn (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runnable.js:395:7)`, - `at Hook.Runnable.run (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runnable.js:364:7)`, - `at next (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:317:10)`, - `at Immediate.<anonymous> (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:347:5)`, - 'at runCallback (timers.js:789:20)', - 'at tryOnImmediate (timers.js:751:5)', - 'at processImmediate [as _immediateCallback] (timers.js:722:5)'].join('\n\t')); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + 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' }; - // tslint:disable-next-line:no-any - sendTelemetryEvent(eventName as any, measures, properties as any, error); + setSharedProperty('foo' as any, 'baz' as any); - const expectedErrorProperties = { - originalEventName: eventName - }; - - const stackTrace = Reporter.properties[0].stackTrace; - delete Reporter.properties[0].stackTrace; - - expect(Reporter.eventName).to.deep.equal(['ERROR', eventName]); - expect(Reporter.measures).to.deep.equal([measures, measures]); - expect(Reporter.properties).to.deep.equal([expectedErrorProperties, properties]); - expect(stackTrace).to.be.length.greaterThan(1); - - const expectedStack = correctPathForOsType(['at Context.test <pvsc>/src/test/telemetry/index.unit.test.ts:50:23\n\tat callFn <pvsc>/node_modules/mocha/lib/runnable.js:372:21', - 'at Test.Runnable.run <pvsc>/node_modules/mocha/lib/runnable.js:364:7', - 'at Runner.runTest <pvsc>/node_modules/mocha/lib/runner.js:455:10', - 'at <pvsc>/node_modules/mocha/lib/runner.js:573:12', - 'at next <pvsc>/node_modules/mocha/lib/runner.js:369:14', - 'at <pvsc>/node_modules/mocha/lib/runner.js:379:7', - 'at next <pvsc>/node_modules/mocha/lib/runner.js:303:14', - 'at <pvsc>/node_modules/mocha/lib/runner.js:342:7', - 'at done <pvsc>/node_modules/mocha/lib/runnable.js:319:5', - 'at callFn <pvsc>/node_modules/mocha/lib/runnable.js:395:7', - 'at Hook.Runnable.run <pvsc>/node_modules/mocha/lib/runnable.js:364:7', - 'at next <pvsc>/node_modules/mocha/lib/runner.js:317:10', - 'at Immediate <pvsc>/node_modules/mocha/lib/runner.js:347:5', - 'at runCallback <hidden>/timers.js:789:20', - 'at tryOnImmediate <hidden>/timers.js:751:5', - 'at processImmediate [as _immediateCallback] <hidden>/timers.js:722:5'].join('\n\t')); + sendTelemetryEvent(eventName as any, measures, properties as any); - expect(stackTrace).to.be.equal(expectedStack); + expect(Reporter.eventName).to.deep.equal([eventName]); + expect(Reporter.measures).to.deep.equal([measures]); + expect(Reporter.properties).to.deep.equal([expectedProperties]); }); - test('Ensure non extension file paths are stripped from stack trace', () => { + test('Send Exception Telemetry', () => { rewiremock.enable(); const error = new Error('Boo'); - error.stack = correctPathForOsType(['Error: Boo', - `at Context.test (${EXTENSION_ROOT_DIR}/src/test/telemetry/index.unit.test.ts:50:23)`, - 'at callFn (c:/one/two/user/node_modules/mocha/lib/runnable.js:372:21)', - 'at Test.Runnable.run (/usr/Paul/Homer/desktop/node_modules/mocha/lib/runnable.js:364:7)', - 'at Runner.runTest (\\wow\wee/node_modules/mocha/lib/runner.js:455:10)', - `at Immediate.<anonymous> (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:347:5)`].join('\n\t')); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); + rewiremock('@vscode/extension-telemetry').with({ default: Reporter }); const eventName = 'Testing'; - const properties = { hello: 'world', foo: 'bar' }; const measures = { start: 123, end: 987 }; - - // tslint:disable-next-line:no-any - sendTelemetryEvent(eventName as any, measures, properties as any, error); - - const expectedErrorProperties = { - originalEventName: eventName - }; - - const stackTrace = Reporter.properties[0].stackTrace; - delete Reporter.properties[0].stackTrace; - - expect(Reporter.eventName).to.deep.equal(['ERROR', eventName]); - expect(Reporter.measures).to.deep.equal([measures, measures]); - expect(Reporter.properties).to.deep.equal([expectedErrorProperties, properties]); - expect(stackTrace).to.be.length.greaterThan(1); - - const expectedStack = correctPathForOsType(['at Context.test <pvsc>/src/test/telemetry/index.unit.test.ts:50:23', - 'at callFn <hidden>/runnable.js:372:21', - 'at Test.Runnable.run <hidden>/runnable.js:364:7', - 'at Runner.runTest <hidden>/runner.js:455:10', - 'at Immediate <pvsc>/node_modules/mocha/lib/runner.js:347:5'].join('\n\t')); - - expect(stackTrace).to.be.equal(expectedStack); - }); - test('Ensure non function names containing file names (unlikely, but for sake of completeness) are stripped from stack trace', () => { - rewiremock.enable(); - const error = new Error('Boo'); - error.stack = correctPathForOsType(['Error: Boo', - `at Context.test (${EXTENSION_ROOT_DIR}/src/test/telemetry/index.unit.test.ts:50:23)`, - 'at callFn (c:/one/two/user/node_modules/mocha/lib/runnable.js:372:21)', - 'at Test./usr/Paul/Homer/desktop/node_modules/mocha/lib/runnable.run (/usr/Paul/Homer/desktop/node_modules/mocha/lib/runnable.js:364:7)', - 'at Runner.runTest (\\wow\wee/node_modules/mocha/lib/runner.js:455:10)', - `at Immediate.<anonymous> (${EXTENSION_ROOT_DIR}/node_modules/mocha/lib/runner.js:347:5)`].join('\n\t')); - rewiremock('vscode-extension-telemetry').with({ default: Reporter }); - - const eventName = 'Testing'; const properties = { hello: 'world', foo: 'bar' }; - const measures = { start: 123, end: 987 }; - // tslint:disable-next-line:no-any sendTelemetryEvent(eventName as any, measures, properties as any, error); - const expectedErrorProperties = { - originalEventName: eventName + const expectedProperties = { + ...properties, + errorName: error.name, + errorStack: error.stack, }; - const stackTrace = Reporter.properties[0].stackTrace; - delete Reporter.properties[0].stackTrace; - - expect(Reporter.eventName).to.deep.equal(['ERROR', eventName]); - expect(Reporter.measures).to.deep.equal([measures, measures]); - expect(Reporter.properties).to.deep.equal([expectedErrorProperties, properties]); - expect(stackTrace).to.be.length.greaterThan(1); - - const expectedStack = correctPathForOsType(['at Context.test <pvsc>/src/test/telemetry/index.unit.test.ts:50:23', - 'at callFn <hidden>/runnable.js:372:21', - 'at <hidden>.run <hidden>/runnable.js:364:7', - 'at Runner.runTest <hidden>/runner.js:455:10', - 'at Immediate <pvsc>/node_modules/mocha/lib/runner.js:347:5'].join('\n\t')); - - expect(stackTrace).to.be.equal(expectedStack); + 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<Terminal>; + let terminal: Terminal; + let nonActivatedTerminal: Terminal; + let shouldEnvExtHandleActivationStub: sinon.SinonStub; + + setup(() => { + shouldEnvExtHandleActivationStub = sinon.stub(extapi, 'shouldEnvExtHandleActivation'); + shouldEnvExtHandleActivationStub.returns(false); + + manager = mock(TerminalManager); + activator = mock(TerminalActivator); + resourceService = mock(ActiveResourceService); + onDidOpenTerminalEventEmitter = new EventEmitter<Terminal>(); + when(manager.onDidOpenTerminal).thenReturn(onDidOpenTerminalEventEmitter.event); + when(activator.activateEnvironmentInTerminal(anything(), anything())).thenResolve(); + + autoActivation = new TerminalAutoActivation( + instance(manager), + [], + instance(activator), + instance(resourceService), + ); + + terminal = ({ + dispose: noop, + hide: noop, + name: 'Some Name', + creationOptions: {}, + processId: Promise.resolve(0), + sendText: noop, + show: noop, + exitStatus: { code: 0 }, + } 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<void>); + + // The terminal should get activated. + verify(activator.activateEnvironmentInTerminal(terminal, anything())).once(); + }); + test('Should not activate terminal if name starts with specific prefix', async () => { + // Trigger opening a terminal. + + await ((onDidOpenTerminalEventEmitter.fire(nonActivatedTerminal) as unknown) as Promise<void>); + + // 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<void>); + + 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 228c9974d94f..726b118ce180 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.unit.test.ts @@ -2,17 +2,20 @@ // Licensed under the MIT License. import { expect } from 'chai'; import * as TypeMoq from 'typemoq'; +import * as sinon from 'sinon'; import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; import { Commands } from '../../../client/common/constants'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { IPythonExtensionBanner } from '../../../client/common/types'; import { IServiceContainer } from '../../../client/ioc/types'; import { CodeExecutionManager } from '../../../client/terminals/codeExecution/codeExecutionManager'; import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../../client/terminals/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; +import * as triggerApis from '../../../client/pythonEnvironments/creation/createEnvironmentTrigger'; +import * as extapi from '../../../client/envExt/api.internal'; -// tslint:disable:no-multiline-string no-trailing-whitespace max-func-body-length no-any suite('Terminal - Code Execution Manager', () => { let executionManager: ICodeExecutionManager; let workspace: TypeMoq.IMock<IWorkspaceService>; @@ -20,28 +23,47 @@ suite('Terminal - Code Execution Manager', () => { let disposables: Disposable[] = []; let serviceContainer: TypeMoq.IMock<IServiceContainer>; let documentManager: TypeMoq.IMock<IDocumentManager>; - let shiftEnterBanner: TypeMoq.IMock<IPythonExtensionBanner>; - let fileSystem: TypeMoq.IMock<IFileSystem>; + let configService: TypeMoq.IMock<IConfigurationService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let triggerCreateEnvironmentCheckNonBlockingStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; setup(() => { - fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - fileSystem.setup(f => f.readFile(TypeMoq.It.isAny())).returns(() => Promise.resolve('')); + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - shiftEnterBanner = TypeMoq.Mock.ofType<IPythonExtensionBanner>(); - shiftEnterBanner.setup(b => b.showBanner()).returns(() => { - return Promise.resolve(); - }); - workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return { - dispose: () => void 0 - }; - }); + workspace + .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { + dispose: () => void 0, + }; + }); documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); - commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(undefined, TypeMoq.MockBehavior.Strict); serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - executionManager = new CodeExecutionManager(commandManager.object, documentManager.object, disposables, fileSystem.object, shiftEnterBanner.object, serviceContainer.object); + configService = TypeMoq.Mock.ofType<IConfigurationService>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'ps' } as unknown) as PythonEnvironment)); + serviceContainer.setup((c) => c.get(IInterpreterService)).returns(() => interpreterService.object); + executionManager = new CodeExecutionManager( + commandManager.object, + documentManager.object, + disposables, + 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(); } @@ -51,106 +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) as any, TypeMoq.It.isAny()), TypeMoq.Times.once()); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_Selection_In_Terminal) as any, TypeMoq.It.isAny()), TypeMoq.Times.once()); - commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_Selection_In_Django_Shell) as any, 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<void>); - commandManager.setup(c => c.registerCommand as any).returns(() => { - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === Commands.Exec_In_Terminal) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + 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<void>); - commandManager.setup(c => c.registerCommand as any).returns(() => { - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === Commands.Exec_In_Terminal) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); + 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<void>); - commandManager.setup(c => c.registerCommand as any).returns(() => { - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === Commands.Exec_In_Terminal) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const fileToExecute = Uri.file('x'); const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(async h => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup(async (h) => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); + 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<void>); - commandManager.setup(c => c.registerCommand as any).returns(() => { - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === commandId) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionSericeId))).returns(() => executionService.object); - documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + 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,27 +223,35 @@ suite('Terminal - Code Execution Manager', () => { async function testExecutionOfSlectionWithoutAnythingSelected(commandId: string, executionServiceId: string) { let commandHandler: undefined | (() => Promise<void>); - commandManager.setup(c => c.registerCommand as any).returns(() => { - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === commandId) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + commandManager + .setup((c) => c.registerCommand as any) + .returns(() => { + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); executionManager.registerCommands(); expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve('')); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup((h) => h.getSelectedTextToExecute).returns(() => () => Promise.resolve('')); const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); - documentManager.setup(d => d.activeTextEditor).returns(() => { return {} as any; }); + 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 () => { @@ -196,14 +264,16 @@ suite('Terminal - Code Execution Manager', () => { async function testExecutionOfSelectionIsSentToTerminal(commandId: string, executionServiceId: string) { let commandHandler: undefined | (() => Promise<void>); - commandManager.setup(c => c.registerCommand as any).returns(() => { - return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { - if (command === commandId) { - commandHandler = callback; - } - return { dispose: () => void 0 }; - }; - }); + 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'); @@ -211,19 +281,27 @@ suite('Terminal - Code Execution Manager', () => { const textSelected = 'abcd'; const activeDocumentUri = Uri.file('abc'); const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); - helper.setup(h => h.normalizeLines).returns(() => () => Promise.resolve(textSelected)).verifiable(TypeMoq.Times.once()); + serviceContainer.setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup((h) => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); + helper + .setup((h) => h.normalizeLines) + .returns(() => () => Promise.resolve(textSelected)) + .verifiable(TypeMoq.Times.once()); const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))) + .returns(() => executionService.object); const document = TypeMoq.Mock.ofType<TextDocument>(); - document.setup(d => d.uri).returns(() => activeDocumentUri); + document.setup((d) => d.uri).returns(() => activeDocumentUri); const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); - activeEditor.setup(e => e.document).returns(() => document.object); - documentManager.setup(d => d.activeTextEditor).returns(() => activeEditor.object); + 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<ITerminalSettings>; let terminalService: TypeMoq.IMock<ITerminalService>; let workspace: TypeMoq.IMock<IWorkspaceService>; let platform: TypeMoq.IMock<IPlatformService>; + let fileSystem: TypeMoq.IMock<IFileSystem>; let settings: TypeMoq.IMock<IPythonSettings>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let applicationShell: TypeMoq.IMock<IApplicationShell>; let disposables: Disposable[] = []; setup(() => { const terminalFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>(); terminalSettings = TypeMoq.Mock.ofType<ITerminalSettings>(); terminalService = TypeMoq.Mock.ofType<ITerminalService>(); const configService = TypeMoq.Mock.ofType<IConfigurationService>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); workspace = TypeMoq.Mock.ofType<IWorkspaceService>(); - workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { - return { - dispose: () => void 0 - }; - }); + workspace + .setup((c) => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { + return { + dispose: () => void 0, + }; + }); platform = TypeMoq.Mock.ofType<IPlatformService>(); const documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); const commandManager = TypeMoq.Mock.ofType<ICommandManager>(); - const fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, - workspace.object, documentManager.object, platform.object, commandManager.object, fileSystem.object, disposables); + fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); + pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + executor = new DjangoShellCodeExecutionProvider( + terminalFactory.object, + configService.object, + workspace.object, + documentManager.object, + platform.object, + commandManager.object, + fileSystem.object, + disposables, + 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<IPythonSettings>(); - 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<IProcessService>(); + 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 be8185f2b238..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<IActiveResourceService>; let documentManager: TypeMoq.IMock<IDocumentManager>; let applicationShell: TypeMoq.IMock<IApplicationShell>; let helper: ICodeExecutionHelper; let document: TypeMoq.IMock<TextDocument>; let editor: TypeMoq.IMock<TextEditor>; let processService: TypeMoq.IMock<IProcessService>; - let configService: TypeMoq.IMock<IConfigurationService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let commandManager: TypeMoq.IMock<ICommandManager>; + let workspaceService: TypeMoq.IMock<IWorkspaceService>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + let jsonParseStub: sinon.SinonStub; + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + setup(() => { const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(); documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); const envVariablesProvider = TypeMoq.Mock.ofType<IEnvironmentVariablesProvider>(); processService = TypeMoq.Mock.ofType<IProcessService>(); - configService = TypeMoq.Mock.ofType<IConfigurationService>(); - const pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); - pythonSettings.setup(p => p.pythonPath).returns(() => PYTHON_PATH); - // tslint:disable-next-line:no-any + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + const resource = Uri.parse('a'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any processService.setup((x: any) => x.then).returns(() => undefined); - 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<IProcessServiceFactory>(); - 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<TextDocument>(); editor = TypeMoq.Mock.ofType<TextEditor>(); - 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<string>; }); - const normalizedCode = await helper.normalizeLines(code.join(EOL)); - const doubleCrIndex = normalizedCode.indexOf('\r\r'); - expect(doubleCrIndex).to.be.equal(-1, 'Double CR (CRCRLF) line endings detected in normalized code snippet.'); + + await helper.normalizeLines('print("hello")', ReplType.terminal); + + expect(execArgs).to.contain('normalizeSelection.py'); }); - ['', '1', '2', '3', '4', '5', '6', '7', '8'].forEach(fileNameSuffix => { - test(`Ensure blank lines are removed (Sample${fileNameSuffix})`, async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); - await ensureBlankLinesAreRemoved(code, expectedCode); - }); - test(`Ensure last two blank lines are preserved (Sample${fileNameSuffix})`, async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); - await ensureBlankLinesAreRemoved(code + EOL, expectedCode + EOL); - }); - test(`Ensure last two blank lines are preserved even if we have more than 2 trailing blank lines (Sample${fileNameSuffix})`, async function () { - // This test has not been working for many months in Python 2.7 under - // Windows.Tracked by #2544. - if (isOs(OSType.Windows) && await isPythonVersion('2.7')) { - // tslint:disable-next-line:no-invalid-this - return this.skip(); - } - - const code = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_raw.py`), 'utf8'); - const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `sample${fileNameSuffix}_normalized.py`), 'utf8'); - 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>(); - 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>(); + textLine.setup((t) => t.text).returns(() => ' print(x)'); + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => 'print(x)'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Single line: partial text selection without whitespace ', async () => { + // This test verifies following case: + // 1: if (isPrime(x) || isFibonacci(x)): + // 2: ↑--------↑ <--- selection range + const expected = 'isPrime(x)'; + editor.setup((e) => e.selection).returns(() => new Selection(1, 4, 1, 14)); const textLine = TypeMoq.Mock.ofType<TextLine>(); - textLine.setup(t => t.text).returns(() => 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>(); + textLine.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType<TextLine>(); + textLine2.setup((t) => t.text).returns(() => ' print(m)'); + const textLine3 = TypeMoq.Mock.ofType<TextLine>(); + textLine3.setup((t) => t.text).returns(() => ' print(n)'); + const textLines = [textLine, textLine2, textLine3]; + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((r: number) => textLines[r - 1].object); + document + .setup((d) => d.getText(new Range(selection.start, selection.end))) + .returns(() => 'print(m)\n print(n)'); + document + .setup((d) => d.getText(new Range(new Position(selection.start.line, 0), selection.end))) + .returns(() => ' print(m)\n print(n)'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Multi-line: text selection without whitespace and partial last line ', async () => { + // This test verifies following case: + // 1: def calc(m, n): + // ↓<------------------------------ selection start + // 2: if (m == 0): + // 3: return n + 1 + // ↑<------------------- selection end (notice " + 1" is not selected) + const expected = ' if (m == 0):\n return n'; + const selection = new Selection(2, 4, 3, 16); + editor.setup((e) => e.selection).returns(() => selection); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType<TextLine>(); + textLine2.setup((t) => t.text).returns(() => ' if (m == 0):'); + const textLine3 = TypeMoq.Mock.ofType<TextLine>(); + textLine3.setup((t) => t.text).returns(() => ' return n + 1'); + const textLines = [textLine, textLine2, textLine3]; + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((r: number) => textLines[r - 1].object); + document + .setup((d) => d.getText(new Range(selection.start, selection.end))) + .returns(() => 'if (m == 0):\n return n'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 4), new Position(selection.start.line, 16))), + ) + .returns(() => 'if (m == 0):'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 0), new Position(selection.end.line, 20))), + ) + .returns(() => ' if (m == 0):\n return n + 1'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(expected); + }); + + test('Multi-line: partial first and last line', async () => { + // This test verifies following case: + // 1: def calc(m, n): + // ↓<------------------------------- selection start + // 2: if (m > 0 + // 3: and n == 0): + // ↑<-------------------- selection end + // 4: pass + const expected = '(m > 0\n and n == 0)'; + const selection = new Selection(2, 7, 3, 19); + editor.setup((e) => e.selection).returns(() => selection); + const textLine = TypeMoq.Mock.ofType<TextLine>(); + textLine.setup((t) => t.text).returns(() => 'def calc(m, n):'); + const textLine2 = TypeMoq.Mock.ofType<TextLine>(); + textLine2.setup((t) => t.text).returns(() => ' if (m > 0'); + const textLine3 = TypeMoq.Mock.ofType<TextLine>(); + textLine3.setup((t) => t.text).returns(() => ' and n == 0)'); + const textLines = [textLine, textLine2, textLine3]; + document.setup((d) => d.lineAt(TypeMoq.It.isAny())).returns((r: number) => textLines[r - 1].object); + document + .setup((d) => d.getText(new Range(selection.start, selection.end))) + .returns(() => '(m > 0\n and n == 0)'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 7), new Position(selection.start.line, 13))), + ) + .returns(() => '(m > 0'); + document + .setup((d) => + d.getText(new Range(new Position(selection.start.line, 0), new Position(selection.end.line, 19))), + ) + .returns(() => ' if (m > 0\n and n == 0)'); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(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<IDocumentManager>; + let applicationShell: TypeMoq.IMock<IApplicationShell>; + + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let commandManager: TypeMoq.IMock<ICommandManager>; + + let processServiceFactory: TypeMoq.IMock<IProcessServiceFactory>; + let configurationService: TypeMoq.IMock<IConfigurationService>; + + let serviceContainer: TypeMoq.IMock<IServiceContainer>; + let codeExecutionHelper: ICodeExecutionHelper; + let experimentService: TypeMoq.IMock<IExperimentService>; + + let processService: TypeMoq.IMock<IProcessService>; + let activeResourceService: TypeMoq.IMock<IActiveResourceService>; + + let document: TypeMoq.IMock<TextDocument>; + let pythonSettings: TypeMoq.IMock<IPythonSettings>; + + const workingPython: PythonEnvironment = { + path: PYTHON_PATH, + version: new SemVer('3.6.6-final'), + sysVersion: '1.0.0.0', + sysPrefix: 'Python', + displayName: 'Python', + envType: EnvironmentType.Unknown, + architecture: Architecture.x64, + }; + + // suite set up only run once for each suite. Very start + // set up --- before each test + // tests -- actual tests + // tear down -- run after each test + // suite tear down only run once at the very end. + + // all object that is common to every test. What each test needs + setup(() => { + documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); + processServiceFactory = TypeMoq.Mock.ofType<IProcessServiceFactory>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + commandManager = TypeMoq.Mock.ofType<ICommandManager>(); + configurationService = TypeMoq.Mock.ofType<IConfigurationService>(); + serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); + experimentService = TypeMoq.Mock.ofType<IExperimentService>(); + processService = TypeMoq.Mock.ofType<IProcessService>(); + activeResourceService = TypeMoq.Mock.ofType<IActiveResourceService>(); + pythonSettings = TypeMoq.Mock.ofType<IPythonSettings>(); + const resource = Uri.parse('a'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + processService.setup((x: any) => x.then).returns(() => undefined); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDocumentManager))) + .returns(() => documentManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))) + .returns(() => applicationShell.object); + processServiceFactory + .setup((p) => p.create(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(processService.object)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IProcessServiceFactory))) + .returns(() => processServiceFactory.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IInterpreterService))) + .returns(() => interpreterService.object); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(ICommandManager))).returns(() => commandManager.object); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) + .returns(() => configurationService.object); + serviceContainer + .setup((s) => s.get(TypeMoq.It.isValue(IExperimentService))) + .returns(() => experimentService.object); + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(workingPython)); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IActiveResourceService))) + .returns(() => activeResourceService.object); + activeResourceService.setup((a) => a.getActiveResource()).returns(() => resource); + + pythonSettings + .setup((s) => s.REPL) + .returns(() => ({ + enableREPLSmartSend: true, + REPLSmartSend: true, + sendToNativeREPL: false, + })); + + configurationService.setup((x) => x.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + codeExecutionHelper = new CodeExecutionHelper(serviceContainer.object); + document = TypeMoq.Mock.ofType<TextDocument>(); + }); + + test('Cursor is not moved when explicit selection is present', async () => { + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', ReplType.terminal, wholeFileContent); + + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.never()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.never()); + + commandManager.verifyAll(); + }); + + const pythonTestVersion = await getPythonSemVer(); + + if (pythonTestVersion && pythonTestVersion.minor < 13) { + test('Smart send should perform smart selection and move cursor - Python < 3.13', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualSmartOutput = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + + // my_dict = { <----- smart shift+enter here + // "key1": "value1", + // "key2": "value2" + // } <---- cursor should be here afterwards, hence offset 3 + commandManager + .setup((c) => c.executeCommand('cursorMove', TypeMoq.It.isAny())) + .callback((_, arg2) => { + assert.deepEqual(arg2, { + to: 'down', + by: 'line', + value: 3, + }); + return Promise.resolve(); + }) + .verifiable(TypeMoq.Times.once()); + + commandManager + .setup((c) => c.executeCommand('cursorEnd')) + .returns(() => Promise.resolve()) + .verifiable(TypeMoq.Times.once()); + + const expectedSmartOutput = 'my_dict = {\n "key1": "value1",\n "key2": "value2"\n}\n'; + expect(actualSmartOutput).to.be.equal(expectedSmartOutput); + commandManager.verifyAll(); + }); + } + + // Do not perform smart selection when there is explicit selection + test('Smart send should not perform smart selection when there is explicit selection', async () => { + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile(path.join(TEST_FILES_PATH, `sample_smart_selection.py`), 'utf8'); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => false); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + const actualNonSmartResult = await codeExecutionHelper.normalizeLines( + 'my_dict = {', + ReplType.terminal, + wholeFileContent, + ); + const expectedNonSmartResult = 'my_dict = {\n\n'; // Standard for previous normalization logic + expect(actualNonSmartResult).to.be.equal(expectedNonSmartResult); + }); + + test('Smart Send should provide warning when code is not valid', async () => { + configurationService + .setup((c) => c.getSettings(TypeMoq.It.isAny())) + .returns({ + REPL: { + REPLSmartSend: true, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } as any); + + const activeEditor = TypeMoq.Mock.ofType<TextEditor>(); + const firstIndexPosition = new Position(0, 0); + const selection = TypeMoq.Mock.ofType<Selection>(); + const wholeFileContent = await fs.readFile( + path.join(TEST_FILES_PATH, `sample_invalid_smart_selection.py`), + 'utf8', + ); + + selection.setup((s) => s.anchor).returns(() => firstIndexPosition); + selection.setup((s) => s.active).returns(() => firstIndexPosition); + selection.setup((s) => s.isEmpty).returns(() => true); + activeEditor.setup((e) => e.selection).returns(() => selection.object); + + documentManager.setup((d) => d.activeTextEditor).returns(() => activeEditor.object); + document.setup((d) => d.getText(TypeMoq.It.isAny())).returns(() => wholeFileContent); + const actualProcessService = new ProcessService(); + + const { execObservable } = actualProcessService; + + processService + .setup((p) => p.execObservable(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns((file, args, options) => execObservable.apply(actualProcessService, [file, args, options])); + + await codeExecutionHelper.normalizeLines('my_dict = {', ReplType.terminal, wholeFileContent); + + applicationShell + .setup((a) => + a.showWarningMessage( + l10n.t( + 'Python is unable to parse the code provided. Please turn off Smart Send if you wish to always run line by line or explicitly select code to force run. [logs](command:{0}) for more details.', + Commands.ViewOutput, + ), + 'Switch to line-by-line', + ), + ) + .verifiable(TypeMoq.Times.once()); + }); +}); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.unit.test.ts index 2c83c827d974..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<ITerminalSettings>; let terminalService: TypeMoq.IMock<ITerminalService>; let workspace: TypeMoq.IMock<IWorkspaceService>; @@ -33,15 +49,18 @@ suite('Terminal - Code Execution', () => { let documentManager: TypeMoq.IMock<IDocumentManager>; let commandManager: TypeMoq.IMock<ICommandManager>; let fileSystem: TypeMoq.IMock<IFileSystem>; + let pythonExecutionFactory: TypeMoq.IMock<IPythonExecutionFactory>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; let isDjangoRepl: boolean; + let applicationShell: TypeMoq.IMock<IApplicationShell>; teardown(() => { - disposables.forEach(disposable => { + disposables.forEach((disposable) => { if (disposable) { disposable.dispose(); } }); - + sinon.restore(); disposables = []; }); @@ -56,28 +75,62 @@ suite('Terminal - Code Execution', () => { documentManager = TypeMoq.Mock.ofType<IDocumentManager>(); commandManager = TypeMoq.Mock.ofType<ICommandManager>(); fileSystem = TypeMoq.Mock.ofType<IFileSystem>(); - + pythonExecutionFactory = TypeMoq.Mock.ofType<IPythonExecutionFactory>(); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); + applicationShell = TypeMoq.Mock.ofType<IApplicationShell>(); settings = TypeMoq.Mock.ofType<IPythonSettings>(); - settings.setup(s => s.terminal).returns(() => terminalSettings.object); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + 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<TerminalCreationOptions>((a) => a.title === expectedTerminalTitle), + ), + ) + .returns(() => terminalService.object); }); - async function ensureTerminalIsCreatedUponInvokingInitializeRepl(isWindows: boolean, isOsx: boolean, isLinux: boolean): Promise<void> { - platform.setup(p => p.isWindows).returns(() => isWindows); - platform.setup(p => p.isMac).returns(() => isOsx); - platform.setup(p => p.isLinux).returns(() => isLinux); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + async function ensureTerminalIsCreatedUponInvokingInitializeRepl( + isWindows: boolean, + isOsx: boolean, + isLinux: boolean, + ): Promise<void> { + platform.setup((p) => p.isWindows).returns(() => isWindows); + platform.setup((p) => p.isMac).returns(() => isOsx); + platform.setup((p) => p.isLinux).returns(() => isLinux); + 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<void> { + const file = Uri.file(path.join('d:', 'path', 'to', 'file', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.rootPath).returns(() => path.join('c:', 'path', 'to')); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c:', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => true); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue('d:')), TypeMoq.Times.once()); + } + test('Ensure we set current drive before changing directory on windows', async () => { + await ensureWeSetCurrentDriveBeforeChangingDirectory(true); + }); + + test('Ensure once set current drive before, we always send command to change the drive letter for subsequent executions', async () => { + await ensureWeSetCurrentDriveBeforeChangingDirectory(true); + const file = Uri.file(path.join('c:', 'path', 'to', 'file', 'one.py')); + await executor.executeFile(file); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue('c:')), TypeMoq.Times.once()); + }); + + async function ensureWeDoNotChangeDriveIfDriveLetterSameAsFileDriveLetter( + _isWindows: boolean, + ): Promise<void> { + const file = Uri.file(path.join('c:', 'path', 'to', 'file', 'one.py')); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => true); + workspace.setup((w) => w.rootPath).returns(() => path.join('c:', 'path', 'to')); + workspaceFolder.setup((w) => w.uri).returns(() => Uri.file(path.join('c:', 'path', 'to'))); + platform.setup((p) => p.isWindows).returns(() => true); + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: PYTHON_PATH } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => []); + + await executor.executeFile(file); + terminalService.verify(async (t) => t.sendText(TypeMoq.It.isValue('c:')), TypeMoq.Times.never()); + } + test('Ensure we do not change drive if current drive letter is same as the file drive letter on windows', async () => { + await ensureWeDoNotChangeDriveIfDriveLetterSameAsFileDriveLetter(true); }); async function ensureWeSetCurrentDirectoryBeforeExecutingAFile(_isWindows: boolean): Promise<void> { const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); - platform.setup(p => p.isWindows).returns(() => false); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - 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<void> { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); - workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + 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<void> { + async function ensureWeSetCurrentDirectoryBeforeExecutingFileInWorkspaceDirectory( + isWindows: boolean, + ): Promise<void> { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - 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<void> { + async function ensureWeSetCurrentDirectoryBeforeExecutingFileNotInSameDirectory( + isWindows: boolean, + ): Promise<void> { const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); - terminalSettings.setup(t => t.executeInFileDir).returns(() => true); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => PYTHON_PATH); - terminalSettings.setup(t => t.launchArgs).returns(() => []); + 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<void> { - platform.setup(p => p.isWindows).returns(() => isWindows); - settings.setup(s => s.pythonPath).returns(() => pythonPath); - terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); - terminalSettings.setup(t => t.executeInFileDir).returns(() => false); - workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + async function testFileExecution( + isWindows: boolean, + pythonPath: string, + terminalArgs: string[], + file: Uri, + ): Promise<void> { + 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<void> { + interpreterService + .setup((s) => s.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: pythonPath } as unknown) as PythonEnvironment)); + terminalSettings.setup((t) => t.launchArgs).returns(() => terminalArgs); + terminalSettings.setup((t) => t.executeInFileDir).returns(() => false); + workspace.setup((w) => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + const condaFile = 'conda'; + const procService = TypeMoq.Mock.ofType<IProcessService>(); + 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<IProcessService>(); + 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<IServiceManager>(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<WorkspaceConfiguration>; + let editorConfig: TypeMoq.IMock<WorkspaceConfiguration>; + let context: TypeMoq.IMock<IExtensionContext>; + let createDirectoryStub: sinon.SinonStub; + let copyStub: sinon.SinonStub; + let globalEnvironmentVariableCollection: TypeMoq.IMock<GlobalEnvironmentVariableCollection>; + + setup(() => { + context = TypeMoq.Mock.ofType<IExtensionContext>(); + globalEnvironmentVariableCollection = TypeMoq.Mock.ofType<GlobalEnvironmentVariableCollection>(); + context.setup((c) => c.environmentVariableCollection).returns(() => globalEnvironmentVariableCollection.object); + context.setup((c) => c.storageUri).returns(() => Uri.parse('a')); + context.setup((c) => c.subscriptions).returns(() => []); + + globalEnvironmentVariableCollection + .setup((c) => c.replace(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve()); + + globalEnvironmentVariableCollection.setup((c) => c.delete(TypeMoq.It.isAny())).returns(() => Promise.resolve()); + + getConfigurationStub = sinon.stub(workspaceApis, 'getConfiguration'); + createDirectoryStub = sinon.stub(workspaceApis, 'createDirectory'); + copyStub = sinon.stub(workspaceApis, 'copy'); + + pythonConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + editorConfig = TypeMoq.Mock.ofType<WorkspaceConfiguration>(); + getConfigurationStub.callsFake((section: string) => { + if (section === 'python') { + return pythonConfig.object; + } + return editorConfig.object; + }); + + createDirectoryStub.callsFake((_) => Promise.resolve()); + copyStub.callsFake((_, __, ___) => Promise.resolve()); + }); + + teardown(() => { + sinon.restore(); + }); + + test('Verify createDirectory is called when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + sinon.assert.calledOnce(createDirectoryStub); + }); + + test('Verify createDirectory is not called when shell integration is disabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + sinon.assert.notCalled(createDirectoryStub); + }); + + test('Verify copy is called when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + sinon.assert.calledOnce(copyStub); + }); + + test('Verify copy is not called when shell integration is disabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + sinon.assert.notCalled(copyStub); + }); + + test('PYTHONSTARTUP is set when enableShellIntegration setting is true', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHONSTARTUP', TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('environmentCollection should not remove PYTHONSTARTUP when enableShellIntegration setting is true', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.never()); + }); + + test('PYTHONSTARTUP is not set when enableShellIntegration setting is false', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHONSTARTUP', TypeMoq.It.isAny(), TypeMoq.It.isAny()), + TypeMoq.Times.never(), + ); + }); + + test('PYTHONSTARTUP is deleted when enableShellIntegration setting is false', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => false); + + await registerPythonStartup(context.object); + + globalEnvironmentVariableCollection.verify((c) => c.delete('PYTHONSTARTUP'), TypeMoq.Times.once()); + }); + + test('PYTHON_BASIC_REPL is set when shell integration is enabled', async () => { + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + await registerPythonStartup(context.object); + globalEnvironmentVariableCollection.verify( + (c) => c.replace('PYTHON_BASIC_REPL', '1', TypeMoq.It.isAny()), + TypeMoq.Times.once(), + ); + }); + + test('Ensure registering terminal link calls registerTerminalLinkProvider', async () => { + const registerTerminalLinkProviderStub = sinon.stub( + pythonStartupLinkProvider, + 'registerCustomTerminalLinkProvider', + ); + const disposableArray: Disposable[] = []; + pythonStartupLinkProvider.registerCustomTerminalLinkProvider(disposableArray); + + sinon.assert.calledOnce(registerTerminalLinkProviderStub); + sinon.assert.calledWith(registerTerminalLinkProviderStub, disposableArray); + + registerTerminalLinkProviderStub.restore(); + }); + + test('Verify onDidChangeConfiguration is called when configuration changes', async () => { + const onDidChangeConfigurationSpy = sinon.spy(workspace, 'onDidChangeConfiguration'); + pythonConfig.setup((p) => p.get('terminal.shellIntegration.enabled')).returns(() => true); + + await registerPythonStartup(context.object); + + assert.isTrue(onDidChangeConfigurationSpy.calledOnce); + onDidChangeConfigurationSpy.restore(); + }); + + if (process.platform === 'darwin') { + test('Mac - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Cmd click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter<unknown>().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Cmd click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Cmd click to launch VS Code Native REPL'.length, + 'Match expected length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + if (process.platform !== 'darwin') { + test('Windows/Linux - Verify provideTerminalLinks returns links when context.line contains expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string with Ctrl click to launch VS Code Native REPL', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter<unknown>().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isNotNull(links, 'Expected links to be not undefined'); + assert.isArray(links, 'Expected links to be an array'); + assert.isNotEmpty(links, 'Expected links to be not empty'); + + if (Array.isArray(links)) { + assert.equal( + links[0].command, + 'python.startNativeREPL', + 'Expected command to be python.startNativeREPL', + ); + assert.equal( + links[0].startIndex, + context.line.indexOf('Ctrl click to launch VS Code Native REPL'), + 'start index should match', + ); + assert.equal( + links[0].length, + 'Ctrl click to launch VS Code Native REPL'.length, + 'Match expected Length', + ); + assert.equal( + links[0].tooltip, + Repl.launchNativeRepl, + 'Expected tooltip to be Launch VS Code Native REPL', + ); + } + }); + } + + test('Verify provideTerminalLinks returns no links when context.line does not contain expectedNativeLink', () => { + const provider = new CustomTerminalLinkProvider(); + const context: TerminalLinkContext = { + line: 'Some random string without the expected link', + terminal: {} as Terminal, + }; + const token: CancellationToken = { + isCancellationRequested: false, + onCancellationRequested: new EventEmitter<unknown>().event, + }; + + const links = provider.provideTerminalLinks(context, token); + + assert.isArray(links, 'Expected links to be an array'); + assert.isEmpty(links, 'Expected links to be empty'); + }); +}); diff --git a/src/test/testBootstrap.ts b/src/test/testBootstrap.ts index 484303618789..ab902255203b 100644 --- a/src/test/testBootstrap.ts +++ b/src/test/testBootstrap.ts @@ -4,13 +4,14 @@ 'use strict'; import { ChildProcess, spawn, SpawnOptions } from 'child_process'; -import * as fs from 'fs-extra'; -import { createServer, Server } from 'net'; +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'; -// tslint:disable:no-console +initializeLogger(); /* This is a simple work around for tests tasks not completing on Azure Pipelines. @@ -73,28 +74,31 @@ async function end(exitCode: number) { } async function startSocketServer() { - return new Promise(resolve => { - server = createServer(socket => { - socket.on('data', buffer => { + return new Promise<void>((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 => { + socket.on('error', (ex) => { // Just log it, no need to do anything else. console.error(ex); }); }); - server.listen({ host: '127.0.0.1', port: 0 }, async () => { - const port = server!.address().port; - console.log(`Test server listening on port ${port}`); - await deletePortFile(); - await fs.writeFile(portFile, port.toString()); - resolve(); - }); - server.on('error', ex => { + server.listen( + { host: '127.0.0.1', port: 0 }, + async (): Promise<void> => { + const port = (server!.address() as AddressInfo).port; + console.log(`Test server listening on port ${port}`); + await deletePortFile(); + await fs.writeFile(portFile, port.toString()); + resolve(); + }, + ); + server.on('error', (ex) => { // Just log it, no need to do anything else. console.error(ex); }); @@ -108,4 +112,7 @@ async function start() { proc.once('close', end); } -start().catch(ex => console.error(ex)); +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 d4496eaab506..6187597a46a3 100644 --- a/src/test/testRunner.ts +++ b/src/test/testRunner.ts @@ -1,95 +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 glob from 'glob'; -import * as Mocha from 'mocha'; -import * as path from 'path'; -import { MochaSetupOptions } from 'vscode/lib/testrunner'; -import { IS_SMOKE_TEST } from './constants'; -import { initialize } from './initialize'; - -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(<any>{ - ui: 'tdd', - colors: true -}); - -export type SetupOptions = MochaSetupOptions & { - testFilesSuffix?: string; - reporter?: string; - reporterOptions?: { - mochaFile?: string; - properties?: string; - }; -}; - -let testFilesGlob = 'test'; - -export function configure(setupOptions: SetupOptions): void { - if (setupOptions.testFilesSuffix) { - testFilesGlob = setupOptions.testFilesSuffix; - } - // Force Mocha to exit. - (setupOptions as any).exit = true; - mocha = new Mocha(setupOptions); -} - -export 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. - if (!IS_SMOKE_TEST) { - const reactHelpers = require('./datascience/reactHelpers') as typeof import('./datascience/reactHelpers'); - reactHelpers.setUpDomEnvironment(); - } - - /** - * Waits until the Python Extension completes loading or a timeout. - * When running tests within VSC, we need to wait for the Python Extension to complete loading, - * this is where `initialize` comes in, we load the PVSC extension using VSC API, wait for it - * to complete. - * That's when we know out PVSC extension specific code is ready for testing. - * So, this code needs to run always for every test running in VS Code (what we call these `system test`) . - * @returns - */ - function initializationScript() { - const ex = new Error('Failed to initialize Python extension for tests after 2 minutes'); - let timer: NodeJS.Timer | undefined; - const failed = new Promise((_, reject) => { - timer = setTimeout(() => reject(ex), 120_000); - }); - const promise = Promise.race([initialize(), failed]); - promise.then(() => clearTimeout(timer!)).catch(() => clearTimeout(timer!)); - return promise; - } - // 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))); - initializationScript() - .then(() => mocha.run(failures => callback(undefined, failures))) - .catch(callback); - } catch (error) { - return callback(error); - } - }); -} +// 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(<any>{ + ui: 'tdd', + colors: true, +}); + +export type SetupOptions = Mocha.MochaOptions & { + testFilesSuffix?: string; + reporterOptions?: { + mochaFile?: string; + properties?: string; + }; +}; + +let testFilesGlob = 'test'; + +export function configure(setupOptions: SetupOptions): void { + if (setupOptions.testFilesSuffix) { + testFilesGlob = setupOptions.testFilesSuffix; + } + // Force Mocha to exit. + (setupOptions as any).exit = true; + mocha = new Mocha.default(setupOptions); +} + +export async function run(): Promise<void> { + 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<void>((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/argsService.test.ts b/src/test/testing/argsService.test.ts deleted file mode 100644 index 84cc4e7be4f0..000000000000 --- a/src/test/testing/argsService.test.ts +++ /dev/null @@ -1,178 +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/testing/common/argumentsHelper'; -import { UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; -import { ArgumentsService as NoseTestArgumentsService } from '../../client/testing/nosetest/services/argsService'; -import { ArgumentsService as PyTestArgumentsService } from '../../client/testing/pytest/services/argsService'; -import { IArgumentsHelper, IArgumentsService } from '../../client/testing/types'; -import { ArgumentsService as UnitTestArgumentsService } from '../../client/testing/unittest/services/argsService'; -import { PYTHON_PATH } from '../common'; - -suite('ArgsService: Common', () => { - UNIT_TEST_PRODUCTS.forEach(product => { - const productNames = getNamesAndValues(Product); - const productName = productNames.find(item => item.value === product)!.name; - suite(productName, () => { - let argumentsService: IArgumentsService; - let moduleName = ''; - let expectedWithArgs: string[] = []; - let expectedWithoutArgs: string[] = []; - - setup(function () { - // Take the spawning of process into account. - // tslint:disable-next-line:no-invalid-this - this.timeout(5000); - const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - const logger = typeMoq.Mock.ofType<ILogger>(); - - 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); -} - -// tslint:disable-next-line:no-any -function getMatches(pattern: any, str: string) { - const matches: string[] = []; - const regex = new RegExp(pattern, 'gm'); - let result: RegExpExecArray | null = regex.exec(str); - while (result !== null) { - if (result.index === regex.lastIndex) { - regex.lastIndex += 1; - } - matches.push(result[1].trim()); - result = regex.exec(str); - } - return matches - .sort() - .reduce<string[]>((items, item) => items.indexOf(item) === -1 ? items.concat([item]) : items, []); -} diff --git a/src/test/testing/banners/languageServerSurvey.unit.test.ts b/src/test/testing/banners/languageServerSurvey.unit.test.ts deleted file mode 100644 index 388178100291..000000000000 --- a/src/test/testing/banners/languageServerSurvey.unit.test.ts +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any 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, IPersistentState, IPersistentStateFactory } from '../../../client/common/types'; -import { LanguageServerSurveyBanner, LSSurveyStateKeys } from '../../../client/languageServices/languageServerSurveyBanner'; - -suite('Language Server Survey Banner', () => { - let appShell: typemoq.IMock<IApplicationShell>; - let browser: typemoq.IMock<IBrowserService>; - let lsService: typemoq.IMock<ILanguageServerFolderService>; - - 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(() => { - appShell = typemoq.Mock.ofType<IApplicationShell>(); - browser = typemoq.Mock.ofType<IBrowserService>(); - lsService = typemoq.Mock.ofType<ILanguageServerFolderService>(); - }); - 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<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>>(); - const completionCountState: 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(() => { - 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/testing/banners/proposeNewLanguageServerBanner.unit.test.ts b/src/test/testing/banners/proposeNewLanguageServerBanner.unit.test.ts deleted file mode 100644 index e11c7e75d637..000000000000 --- a/src/test/testing/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<IConfigurationService>; - let appShell: typemoq.IMock<IApplicationShell>; - 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<IConfigurationService>(); - appShell = typemoq.Mock.ofType<IApplicationShell>(); - }); - 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<IPersistentStateFactory> = typemoq.Mock.ofType<IPersistentStateFactory>(); - const val: typemoq.IMock<IPersistentState<boolean>> = typemoq.Mock.ofType<IPersistentState<boolean>>(); - val.setup(a => a.updateValue(typemoq.It.isValue(true))).returns(() => { - enabledValue = true; - return Promise.resolve(); - }); - val.setup(a => a.updateValue(typemoq.It.isValue(false))).returns(() => { - enabledValue = false; - return Promise.resolve(); - }); - val.setup(a => a.value).returns(() => { - return enabledValue; - }); - myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(ProposeLSStateKeys.ShowBanner), - typemoq.It.isValue(true))) - .returns(() => { - return val.object; - }); - myfactory.setup(a => a.createGlobalPersistentState(typemoq.It.isValue(ProposeLSStateKeys.ShowBanner), - typemoq.It.isValue(false))) - .returns(() => { - return val.object; - }); - return new ProposeLanguageServerBanner( - appShell, - myfactory.object, - config, - sampleValue); -} diff --git a/src/test/testing/common/argsHelper.unit.test.ts b/src/test/testing/common/argsHelper.unit.test.ts deleted file mode 100644 index 6a359d121c7b..000000000000 --- a/src/test/testing/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/testing/common/argumentsHelper'; -import { IArgumentsHelper } from '../../../client/testing/types'; -const assertArrays = require('chai-arrays'); -use(assertArrays); - -suite('Unit Tests - Arguments Helper', () => { - let argsHelper: IArgumentsHelper; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - const logger = typeMoq.Mock.ofType<ILogger>(); - - 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/testing/common/debugLauncher.unit.test.ts b/src/test/testing/common/debugLauncher.unit.test.ts index cee573109f4f..86e862103bf6 100644 --- a/src/test/testing/common/debugLauncher.unit.test.ts +++ b/src/test/testing/common/debugLauncher.unit.test.ts @@ -3,155 +3,197 @@ 'use strict'; -// tslint:disable:no-any - import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as path from 'path'; +import * as sinon from 'sinon'; import * as TypeMoq from 'typemoq'; -import { - CancellationTokenSource, DebugConfiguration, Uri, WorkspaceFolder -} from 'vscode'; -import { - IInvalidPythonPathInDebuggerService -} from '../../../client/application/diagnostics/types'; -import { - IApplicationShell, IDebugService, IDocumentManager, IWorkspaceService -} from '../../../client/common/application/types'; +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 { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; -import { IConfigurationService, IPythonSettings, ITestingSettings } from '../../../client/common/types'; -import { DebuggerTypeName } from '../../../client/debugger/constants'; -import { - LaunchConfigurationResolver -} from '../../../client/debugger/extension/configuration/resolvers/launch'; -import { - IConfigurationProviderUtils -} from '../../../client/debugger/extension/configuration/types'; +import { IConfigurationService, IPythonSettings } from '../../../client/common/types'; +import { PythonDebuggerTypeName } from '../../../client/debugger/constants'; +import { IDebugEnvironmentVariablesService } from '../../../client/debugger/extension/configuration/resolvers/helper'; +import { LaunchConfigurationResolver } from '../../../client/debugger/extension/configuration/resolvers/launch'; import { DebugOptions } from '../../../client/debugger/types'; +import { IInterpreterService } from '../../../client/interpreter/contracts'; import { IServiceContainer } from '../../../client/ioc/types'; +import { PythonEnvironment } from '../../../client/pythonEnvironments/info'; import { DebugLauncher } from '../../../client/testing/common/debugLauncher'; -import { LaunchOptions, TestProvider } from '../../../client/testing/common/types'; +import { LaunchOptions } from '../../../client/testing/common/types'; +import { ITestingSettings } from '../../../client/testing/configuration/types'; +import { TestProvider } from '../../../client/testing/types'; import { isOs, OSType } from '../../common'; +import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; +import { createDeferred } from '../../../client/common/utils/async'; +import * as envExtApi from '../../../client/envExt/api.internal'; -use(chaiAsPromised); +use(chaiAsPromised.default); -// tslint:disable-next-line:max-func-body-length no-any suite('Unit Tests - Debug Launcher', () => { let serviceContainer: TypeMoq.IMock<IServiceContainer>; let unitTestSettings: TypeMoq.IMock<ITestingSettings>; let debugLauncher: DebugLauncher; let debugService: TypeMoq.IMock<IDebugService>; - let workspaceService: TypeMoq.IMock<IWorkspaceService>; - let platformService: TypeMoq.IMock<IPlatformService>; - let filesystem: TypeMoq.IMock<IFileSystem>; let settings: TypeMoq.IMock<IPythonSettings>; - let hasWorkspaceFolders: boolean; + let debugEnvHelper: TypeMoq.IMock<IDebugEnvironmentVariablesService>; + let interpreterService: TypeMoq.IMock<IInterpreterService>; + let environmentActivationService: TypeMoq.IMock<IEnvironmentActivationService>; + let getWorkspaceFolderStub: sinon.SinonStub; + let getWorkspaceFoldersStub: sinon.SinonStub; + let pathExistsStub: sinon.SinonStub; + let readFileStub: sinon.SinonStub; + const envVars = { FOO: 'BAR' }; + setup(async () => { + environmentActivationService = TypeMoq.Mock.ofType<IEnvironmentActivationService>(); + environmentActivationService + .setup((e) => e.getActivatedEnvironmentVariables(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(envVars)); + interpreterService = TypeMoq.Mock.ofType<IInterpreterService>(); serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(undefined, TypeMoq.MockBehavior.Strict); const configService = TypeMoq.Mock.ofType<IConfigurationService>(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IConfigurationService))) + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IConfigurationService))) .returns(() => configService.object); debugService = TypeMoq.Mock.ofType<IDebugService>(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDebugService))) - .returns(() => debugService.object); - - hasWorkspaceFolders = true; - workspaceService = TypeMoq.Mock.ofType<IWorkspaceService>(undefined, TypeMoq.MockBehavior.Strict); - workspaceService.setup(u => u.hasWorkspaceFolders) - .returns(() => hasWorkspaceFolders); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IWorkspaceService))) - .returns(() => workspaceService.object); - - platformService = TypeMoq.Mock.ofType<IPlatformService>(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IPlatformService))) - .returns(() => platformService.object); - - filesystem = TypeMoq.Mock.ofType<IFileSystem>(undefined, TypeMoq.MockBehavior.Strict); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IFileSystem))) - .returns(() => filesystem.object); + 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<IApplicationShell>(undefined, TypeMoq.MockBehavior.Strict); - appShell.setup(a => a.showErrorMessage(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)); - serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell))) - .returns(() => appShell.object); + appShell.setup((a) => a.showErrorMessage(TypeMoq.It.isAny())).returns(() => Promise.resolve(undefined)); + serviceContainer.setup((c) => c.get(TypeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); settings = TypeMoq.Mock.ofType<IPythonSettings>(undefined, TypeMoq.MockBehavior.Strict); - configService.setup(c => c.getSettings(TypeMoq.It.isAny())) - .returns(() => settings.object); + configService.setup((c) => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); - unitTestSettings = TypeMoq.Mock.ofType<ITestingSettings>(undefined, TypeMoq.MockBehavior.Strict); - settings.setup(p => p.testing) - .returns(() => unitTestSettings.object); + unitTestSettings = TypeMoq.Mock.ofType<ITestingSettings>(); + settings.setup((p) => p.testing).returns(() => unitTestSettings.object); - debugLauncher = new DebugLauncher( - serviceContainer.object, - getNewResolver(configService.object) - ); + debugEnvHelper = TypeMoq.Mock.ofType<IDebugEnvironmentVariablesService>(undefined, TypeMoq.MockBehavior.Strict); + serviceContainer + .setup((c) => c.get(TypeMoq.It.isValue(IDebugEnvironmentVariablesService))) + .returns(() => debugEnvHelper.object); + + debugLauncher = new DebugLauncher(serviceContainer.object, getNewResolver(configService.object)); }); + + teardown(() => { + sinon.restore(); + }); + function getNewResolver(configService: IConfigurationService) { - const validator = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>(undefined, TypeMoq.MockBehavior.Strict); - validator.setup(v => v.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + const validator = TypeMoq.Mock.ofType<IInvalidPythonPathInDebuggerService>( + undefined, + TypeMoq.MockBehavior.Strict, + ); + validator + .setup((v) => v.validatePythonPath(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => Promise.resolve(true)); return new LaunchConfigurationResolver( - workspaceService.object, - TypeMoq.Mock.ofType<IDocumentManager>(undefined, TypeMoq.MockBehavior.Strict).object, - TypeMoq.Mock.ofType<IConfigurationProviderUtils>(undefined, TypeMoq.MockBehavior.Strict).object, validator.object, - platformService.object, - configService + configService, + debugEnvHelper.object, + interpreterService.object, + environmentActivationService.object, ); } function setupDebugManager( - workspaceFolder: WorkspaceFolder, + _workspaceFolder: WorkspaceFolder, expected: DebugConfiguration, - testProvider: TestProvider + testProvider: TestProvider, ) { - platformService.setup(p => p.isWindows) - .returns(() => /^win/.test(process.platform)); - settings.setup(p => p.pythonPath) - .returns(() => 'python'); - settings.setup(p => p.envFile) - .returns(() => __filename); + 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; - //debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) - debugService.setup(d => d.startDebugging(TypeMoq.It.isValue(workspaceFolder), TypeMoq.It.isValue(expected))) - .returns((_wspc: WorkspaceFolder, _expectedParam: DebugConfiguration) => { - return Promise.resolve(undefined as any); + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve(expected.env)); + + const deferred = createDeferred<void>(); + let capturedConfig: DebugConfiguration | undefined; + + // Use TypeMoq.It.isAny() because the implementation adds a session marker to the config + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_wspc: WorkspaceFolder, config: DebugConfiguration) => { + capturedConfig = config; + deferred.resolve(); }) - .verifiable(TypeMoq.Times.once()); + .returns(() => Promise.resolve(true)); + + // Setup onDidStartDebugSession - the new implementation uses this to capture the session + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .returns((callback) => { + deferred.promise.then(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }); + return { dispose: () => {} }; + }); + + // Setup onDidTerminateDebugSession - fires after the session starts + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .returns((callback) => { + deferred.promise.then(() => { + setTimeout(() => { + if (capturedConfig) { + callback(({ + id: 'test-session-id', + configuration: capturedConfig, + } as unknown) as DebugSession); + } + }, 10); + }); + return { dispose: () => {} }; + }); } function createWorkspaceFolder(folderPath: string): WorkspaceFolder { return { index: 0, name: path.basename(folderPath), - uri: Uri.file(folderPath) + uri: Uri.file(folderPath), }; } - function getTestLauncherScript(testProvider: TestProvider) { - switch (testProvider) { - case 'unittest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'visualstudio_py_testlauncher.py'); - } - case 'pytest': - case 'nosetest': { - return path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testlauncher.py'); - } - default: { - throw new Error(`Unknown test provider '${testProvider}'`); + function getTestLauncherScript(testProvider: TestProvider, pythonTestAdapterRewriteExperiment?: boolean) { + if (!pythonTestAdapterRewriteExperiment) { + switch (testProvider) { + case 'unittest': { + return path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'execution.py'); + } + case 'pytest': { + return path.join(EXTENSION_ROOT_DIR, 'python_files', 'vscode_pytest', 'run_pytest_script.py'); + } + default: { + throw new Error(`Unknown test provider '${testProvider}'`); + } } } } + function getDefaultDebugConfig(): DebugConfiguration { return { name: 'Debug Unit Test', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'launch', console: 'internalConsole', env: {}, @@ -160,67 +202,67 @@ suite('Unit Tests - Debug Launcher', () => { showReturnValue: true, redirectOutput: true, debugStdLib: false, - subProcess: true + subProcess: true, + purpose: [], }; } function setupSuccess( options: LaunchOptions, testProvider: TestProvider, expected?: DebugConfiguration, - debugConfigs?: string | DebugConfiguration[] + debugConfigs?: string | DebugConfiguration[], ) { - const testLaunchScript = getTestLauncherScript(testProvider); + const testLaunchScript = getTestLauncherScript(testProvider, false); - const workspaceFolders = [ - createWorkspaceFolder(options.cwd), - createWorkspaceFolder('five/six/seven') - ]; - workspaceService.setup(u => u.workspaceFolders) - .returns(() => workspaceFolders); - workspaceService.setup(u => u.getWorkspaceFolder(TypeMoq.It.isAny())) - .returns(() => workspaceFolders[0]); + const workspaceFolders = [createWorkspaceFolder(options.cwd), createWorkspaceFolder('five/six/seven')]; + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); if (!debugConfigs) { - filesystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(false)); + pathExistsStub.resolves(false); } else { - filesystem.setup(fs => fs.fileExists(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(true)); + pathExistsStub.resolves(true); + if (typeof debugConfigs !== 'string') { debugConfigs = JSON.stringify({ version: '0.1.0', - configurations: debugConfigs + configurations: debugConfigs, }); } - filesystem.setup(fs => fs.readFile(TypeMoq.It.isAny())) - .returns(() => Promise.resolve(debugConfigs as string)); + readFileStub.resolves(debugConfigs as string); } if (!expected) { expected = getDefaultDebugConfig(); } - expected.rules = [ - { path: path.join(EXTENSION_ROOT_DIR, 'pythonFiles'), include: false } - ]; + expected.rules = [{ path: path.join(EXTENSION_ROOT_DIR, 'python_files'), include: false }]; expected.program = testLaunchScript; expected.args = options.args; + if (!expected.cwd) { expected.cwd = workspaceFolders[0].uri.fsPath; } + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + expected.env.TEST_RUN_PIPE = 'pytestPort'; + expected.env.RUN_TEST_IDS_PIPE = 'runTestIdsPort'; // added by LaunchConfigurationResolver: - if (!expected.pythonPath) { - expected.pythonPath = 'python'; + if (!expected.python) { + expected.python = 'python'; } - expected.workspaceFolder = workspaceFolders[0].uri.fsPath; - expected.debugOptions = []; - if (expected.justMyCode === undefined) { - // Populate justMyCode using debugStdLib - expected.justMyCode = !expected.debugStdLib; + if (!expected.clientOS) { + expected.clientOS = isOs(OSType.Windows) ? 'windows' : 'unix'; } - if (!expected.justMyCode) { - expected.debugOptions.push(DebugOptions.DebugStdLib); + if (!expected.debugAdapterPython) { + expected.debugAdapterPython = 'python'; } + if (!expected.debugLauncherPython) { + expected.debugLauncherPython = 'python'; + } + expected.workspaceFolder = workspaceFolders[0].uri.fsPath; + expected.debugOptions = []; if (expected.stopOnEntry) { expected.debugOptions.push(DebugOptions.StopOnEntry); } @@ -237,35 +279,39 @@ suite('Unit Tests - Debug Launcher', () => { expected.debugOptions.push(DebugOptions.FixFilePathCase); } - setupDebugManager( - workspaceFolders[0], - expected, - testProvider - ); + setupDebugManager(workspaceFolders[0], expected, testProvider); } - const testProviders: TestProvider[] = ['nosetest', 'pytest', 'unittest']; - // tslint:disable-next-line:max-func-body-length - testProviders.forEach(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 + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; setupSuccess(options, testProvider); await debugLauncher.launchDebugger(options); - debugService.verifyAll(); + try { + debugService.verifyAll(); + } catch (ex) { + console.log(ex); + } }); test(`Must launch debugger with arguments ${testTitleSuffix}`, async () => { const options = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py', '--debug', '1'], - testProvider + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; setupSuccess(options, testProvider); @@ -274,7 +320,8 @@ suite('Unit Tests - Debug Launcher', () => { debugService.verifyAll(); }); test(`Must not launch debugger if cancelled ${testTitleSuffix}`, async () => { - debugService.setup(d => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) .returns(() => { return Promise.resolve(undefined as any); }) @@ -283,25 +330,38 @@ suite('Unit Tests - Debug Launcher', () => { const cancellationToken = new CancellationTokenSource(); cancellationToken.cancel(); const token = cancellationToken.token; - const options: LaunchOptions = { cwd: '', args: [], token, testProvider }; + const options: LaunchOptions = { + cwd: '', + args: [], + token, + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; - await expect( - debugLauncher.launchDebugger(options) - ).to.be.eventually.equal(undefined, 'not undefined'); + await expect(debugLauncher.launchDebugger(options)).to.be.eventually.equal(undefined, 'not undefined'); debugService.verifyAll(); }); test(`Must throw an exception if there are no workspaces ${testTitleSuffix}`, async () => { - hasWorkspaceFolders = false; - debugService.setup(d => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined as any)) + 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 }; + const options: LaunchOptions = { + cwd: '', + args: [], + testProvider, + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; - await expect( - debugLauncher.launchDebugger(options) - ).to.eventually.rejectedWith('Please open a workspace'); + await expect(debugLauncher.launchDebugger(options)).to.eventually.rejectedWith('Please open a workspace'); debugService.verifyAll(); }); @@ -311,14 +371,35 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam'; - setupSuccess(options, 'unittest', expected, [ - { name: 'spam', type: DebuggerTypeName, request: 'test' } - ]); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'test' }]); + + await debugLauncher.launchDebugger(options); + + debugService.verifyAll(); + }); + test('Use cwd value in settings if exist', async () => { + unitTestSettings.setup((p) => p.cwd).returns(() => 'path/to/settings/cwd'); + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + const expected = getDefaultDebugConfig(); + expected.cwd = 'path/to/settings/cwd'; + const pluginPath = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pythonPath = `${pluginPath}${path.delimiter}${expected.cwd}`; + expected.env.PYTHONPATH = pythonPath; + + setupSuccess(options, 'unittest', expected); await debugLauncher.launchDebugger(options); debugService.verifyAll(); @@ -328,34 +409,41 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = { name: 'my tests', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'launch', - pythonPath: 'some/dir/bin/py3', + python: 'some/dir/bin/py3', + debugAdapterPython: 'some/dir/bin/py3', + debugLauncherPython: 'some/dir/bin/py3', stopOnEntry: true, showReturnValue: true, console: 'integratedTerminal', cwd: 'some/dir', env: { - SPAM: 'EGGS' + PYTHONPATH: 'one/two/three', + SPAM: 'EGGS', + TEST_RUN_PIPE: 'pytestPort', + RUN_TEST_IDS_PIPE: 'runTestIdsPort', }, envFile: 'some/dir/.env', redirectOutput: false, debugStdLib: true, - justMyCode: false, // added by LaunchConfigurationResolver: internalConsoleOptions: 'neverOpen', - subProcess: true + subProcess: true, + purpose: [], }; setupSuccess(options, 'unittest', expected, [ { name: 'my tests', - type: DebuggerTypeName, + type: PythonDebuggerTypeName, request: 'test', - pythonPath: expected.pythonPath, + pythonPath: expected.python, stopOnEntry: expected.stopOnEntry, showReturnValue: expected.showReturnValue, console: expected.console, @@ -364,8 +452,7 @@ suite('Unit Tests - Debug Launcher', () => { envFile: expected.envFile, redirectOutput: expected.redirectOutput, debugStdLib: expected.debugStdLib, - justMyCode: undefined - } + }, ]); await debugLauncher.launchDebugger(options); @@ -377,14 +464,16 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam1'; setupSuccess(options, 'unittest', expected, [ - { name: 'spam1', type: DebuggerTypeName, request: 'test' }, - { name: 'spam2', type: DebuggerTypeName, request: 'test' }, - { name: 'spam3', type: DebuggerTypeName, request: 'test' } + { name: 'spam1', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'test' }, ]); await debugLauncher.launchDebugger(options); @@ -396,7 +485,9 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, ']'); @@ -411,7 +502,7 @@ suite('Unit Tests - Debug Launcher', () => { '// test 2 \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ', @@ -419,7 +510,7 @@ suite('Unit Tests - Debug Launcher', () => { [ \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ] \n\ @@ -429,12 +520,12 @@ suite('Unit Tests - Debug Launcher', () => { "configurations": [ \n\ { \n\ "name": "spam", \n\ - "type": "python", \n\ + "type": "debugpy", \n\ "request": "test" \n\ } \n\ ] \n\ } \n\ - ' + ', ]; for (const text of malformedFiles) { const testID = text.split('\n')[0].substring(3).trim(); @@ -442,7 +533,9 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, text); @@ -457,20 +550,21 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); - // tslint:disable:no-object-literal-type-assertion + setupSuccess(options, 'unittest', expected, [ {} as DebugConfiguration, { name: 'spam1' } as DebugConfiguration, - { name: 'spam2', type: DebuggerTypeName } as DebugConfiguration, + { name: 'spam2', type: PythonDebuggerTypeName } as DebugConfiguration, { name: 'spam3', request: 'test' } as DebugConfiguration, - { type: DebuggerTypeName } as DebugConfiguration, - { type: DebuggerTypeName, request: 'test' } as DebugConfiguration, - { request: 'test' } as DebugConfiguration + { type: PythonDebuggerTypeName } as DebugConfiguration, + { type: PythonDebuggerTypeName, request: 'test' } as DebugConfiguration, + { request: 'test' } as DebugConfiguration, ]); - // tslint:enable:no-object-literal-type-assertion await debugLauncher.launchDebugger(options); @@ -481,12 +575,12 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); - setupSuccess(options, 'unittest', expected, [ - { name: 'foo', type: 'other', request: 'bar' } - ]); + setupSuccess(options, 'unittest', expected, [{ name: 'foo', type: 'other', request: 'bar' }]); await debugLauncher.launchDebugger(options); @@ -497,12 +591,12 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); - setupSuccess(options, 'unittest', expected, [ - { name: 'spam', type: DebuggerTypeName, request: 'bogus' } - ]); + setupSuccess(options, 'unittest', expected, [{ name: 'spam', type: PythonDebuggerTypeName, request: 'bogus' }]); await debugLauncher.launchDebugger(options); @@ -513,12 +607,14 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); setupSuccess(options, 'unittest', expected, [ - { name: 'spam', type: DebuggerTypeName, request: 'launch' }, - { name: 'spam', type: DebuggerTypeName, request: 'attach' } + { name: 'spam', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam', type: PythonDebuggerTypeName, request: 'attach' }, ]); await debugLauncher.launchDebugger(options); @@ -530,17 +626,19 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam2'; setupSuccess(options, 'unittest', expected, [ { name: 'foo1', type: 'other', request: 'bar' }, { name: 'foo2', type: 'other', request: 'bar' }, - { name: 'spam1', type: DebuggerTypeName, request: 'launch' }, - { name: 'spam2', type: DebuggerTypeName, request: 'test' }, - { name: 'spam3', type: DebuggerTypeName, request: 'attach' }, - { name: 'xyz', type: 'another', request: 'abc' } + { name: 'spam1', type: PythonDebuggerTypeName, request: 'launch' }, + { name: 'spam2', type: PythonDebuggerTypeName, request: 'test' }, + { name: 'spam3', type: PythonDebuggerTypeName, request: 'attach' }, + { name: 'xyz', type: 'another', request: 'abc' }, ]); await debugLauncher.launchDebugger(options); @@ -552,12 +650,18 @@ suite('Unit Tests - Debug Launcher', () => { const options: LaunchOptions = { cwd: 'one/two/three', args: ['/one/two/three/testfile.py'], - testProvider: 'unittest' + testProvider: 'unittest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', }; const expected = getDefaultDebugConfig(); expected.name = 'spam'; expected.stopOnEntry = true; - setupSuccess(options, 'unittest', expected, ' \n\ + setupSuccess( + options, + 'unittest', + expected, + ' \n\ { \n\ "version": "0.1.0", \n\ "configurations": [ \n\ @@ -565,14 +669,15 @@ suite('Unit Tests - Debug Launcher', () => { { \n\ // "test" debug config \n\ "name": "spam", /* non-empty */ \n\ - "type": "python", /* must be "python" */ \n\ + "type": "debugpy", /* must be "python" */ \n\ "request": "test", /* must be "test" */ \n\ // extra stuff here: \n\ "stopOnEntry": true \n\ } \n\ ] \n\ } \n\ - '); + ', + ); await debugLauncher.launchDebugger(options); @@ -582,8 +687,8 @@ suite('Unit Tests - Debug Launcher', () => { const workspaceFolder = { name: 'abc', index: 0, uri: Uri.file(__filename) }; const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); const jsonc = '{"version":"1234", "configurations":[1,2,],}'; - filesystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(true)); - filesystem.setup(fs => fs.readFile(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(jsonc)); + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); @@ -594,11 +699,235 @@ suite('Unit Tests - Debug Launcher', () => { const filename = path.join(workspaceFolder.uri.fsPath, '.vscode', 'launch.json'); const jsonc = '{"version":"1234"'; - filesystem.setup(fs => fs.fileExists(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(true)); - filesystem.setup(fs => fs.readFile(TypeMoq.It.isValue(filename))).returns(() => Promise.resolve(jsonc)); + pathExistsStub.resolves(true); + readFileStub.withArgs(filename).resolves(jsonc); const configs = await debugLauncher.readAllDebugConfigs(workspaceFolder); expect(configs).to.be.deep.equal([]); }); + + // ===== PROJECT-BASED DEBUG SESSION TESTS ===== + + suite('Project-based debug sessions', () => { + function setupForProjectTests(options: LaunchOptions) { + interpreterService + .setup((i) => i.getActiveInterpreter(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(({ path: 'python' } as unknown) as PythonEnvironment)); + settings.setup((p) => p.envFile).returns(() => __filename); + + debugEnvHelper + .setup((x) => x.getEnvironmentVariables(TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => Promise.resolve({})); + + const workspaceFolders = [{ index: 0, name: 'test', uri: Uri.file(options.cwd) }]; + getWorkspaceFoldersStub.returns(workspaceFolders); + getWorkspaceFolderStub.returns(workspaceFolders[0]); + pathExistsStub.resolves(false); + + // Stub useEnvExtension to avoid null reference errors in tests + sinon.stub(envExtApi, 'useEnvExtension').returns(false); + } + + /** + * Helper to setup debug service mocks with proper session lifecycle simulation. + * The implementation uses onDidStartDebugSession to capture the session via marker, + * then onDidTerminateDebugSession to resolve when that session ends. + */ + function setupDebugServiceWithSessionLifecycle(): { + capturedConfigs: DebugConfiguration[]; + } { + const capturedConfigs: DebugConfiguration[] = []; + let startCallback: ((session: DebugSession) => void) | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfigs.push(config); + // Simulate the full session lifecycle after startDebugging resolves + setTimeout(() => { + const session = ({ + id: `session-${capturedConfigs.length}`, + configuration: config, + } as unknown) as DebugSession; + // Fire start first (so ourSession is captured) + startCallback?.(session); + // Then fire terminate (so the promise resolves) + setTimeout(() => terminateCallback?.(session), 5); + }, 5); + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + return { capturedConfigs }; + } + + test('should use project name in config name when provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + project: { name: 'myproject (Python 3.11)', uri: Uri.file('one/two/three') }, + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + expect(capturedConfigs[0].name).to.equal('Debug Tests: myproject (Python 3.11)'); + }); + + test('should use default python when no project provided', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should use the default 'python' from interpreterService mock + expect(capturedConfigs[0].python).to.equal('python'); + }); + + test('should add unique session marker to launch config', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(1); + // Should have a session marker of format 'test-{timestamp}-{random}' + const marker = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + expect(marker).to.be.a('string'); + expect(marker).to.match(/^test-\d+-[a-z0-9]+$/); + }); + + test('should generate unique markers for each launch', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + const { capturedConfigs } = setupDebugServiceWithSessionLifecycle(); + + // Launch twice + await debugLauncher.launchDebugger(options); + await debugLauncher.launchDebugger(options); + + expect(capturedConfigs).to.have.length(2); + const marker1 = (capturedConfigs[0] as any).__vscodeTestSessionMarker; + const marker2 = (capturedConfigs[1] as any).__vscodeTestSessionMarker; + expect(marker1).to.not.equal(marker2); + }); + + test('should only resolve when matching session terminates', async () => { + const options: LaunchOptions = { + cwd: 'one/two/three', + args: ['/one/two/three/testfile.py'], + testProvider: 'pytest', + runTestIdsPort: 'runTestIdsPort', + pytestPort: 'pytestPort', + }; + + setupForProjectTests(options); + + let capturedConfig: DebugConfiguration | undefined; + let terminateCallback: ((session: DebugSession) => void) | undefined; + let startCallback: ((session: DebugSession) => void) | undefined; + + debugService + .setup((d) => d.startDebugging(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .callback((_, config) => { + capturedConfig = config; + }) + .returns(() => Promise.resolve(true)); + + debugService + .setup((d) => d.onDidStartDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + startCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + debugService + .setup((d) => d.onDidTerminateDebugSession(TypeMoq.It.isAny())) + .callback((cb) => { + terminateCallback = cb; + }) + .returns(() => ({ dispose: () => {} })); + + const launchPromise = debugLauncher.launchDebugger(options); + + // Wait for config to be captured + await new Promise((r) => setTimeout(r, 10)); + + // Simulate our session starting + const ourSession = ({ + id: 'our-session-id', + configuration: capturedConfig!, + } as unknown) as DebugSession; + startCallback?.(ourSession); + + // Create a different session (like another project's debug) + const otherSession = ({ + id: 'other-session-id', + configuration: { __vscodeTestSessionMarker: 'different-marker' }, + } as unknown) as DebugSession; + + // Terminate the OTHER session first - should NOT resolve our promise + terminateCallback?.(otherSession); + + // Wait a bit to ensure it didn't resolve + let resolved = false; + const checkPromise = launchPromise.then(() => { + resolved = true; + }); + + await new Promise((r) => setTimeout(r, 20)); + expect(resolved).to.be.false; + + // Now terminate OUR session - should resolve + terminateCallback?.(ourSession); + + await checkPromise; + expect(resolved).to.be.true; + }); + }); }); diff --git a/src/test/testing/common/helpers.unit.test.ts b/src/test/testing/common/helpers.unit.test.ts new file mode 100644 index 000000000000..441b257d4d0e --- /dev/null +++ b/src/test/testing/common/helpers.unit.test.ts @@ -0,0 +1,48 @@ +import * as path from 'path'; +import * as assert from 'assert'; +import { addPathToPythonpath } from '../../../client/testing/common/helpers'; + +suite('Unit Tests - Test Helpers', () => { + const newPaths = [path.join('path', 'to', 'new')]; + test('addPathToPythonpath handles undefined path', async () => { + const launchPythonPath = undefined; + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + assert.equal(actualPath, path.join('path', 'to', 'new')); + }); + test('addPathToPythonpath adds path if it does not exist in the python path', async () => { + const launchPythonPath = path.join('random', 'existing', 'pythonpath'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('random', 'existing', 'pythonpath') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath does not add to python path if the given python path already contains the path', async () => { + const launchPythonPath = path.join('path', 'to', 'new'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath correctly normalizes both existing and new paths', async () => { + const newerPaths = [path.join('path', 'to', '/', 'new')]; + const launchPythonPath = path.join('path', 'to', '..', 'old'); + const actualPath = addPathToPythonpath(newerPaths, launchPythonPath); + const expectedPath = path.join('path', 'old') + path.delimiter + path.join('path', 'to', 'new'); + assert.equal(actualPath, expectedPath); + }); + test('addPathToPythonpath splits pythonpath then rejoins it', async () => { + const launchPythonPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + const actualPath = addPathToPythonpath(newPaths, launchPythonPath); + const expectedPath = + path.join('path', 'to', 'new') + + path.delimiter + + path.join('path', 'to', 'old') + + path.delimiter + + path.join('path', 'to', 'random'); + assert.equal(actualPath, expectedPath); + }); +}); diff --git a/src/test/testing/common/managers/baseTestManager.unit.test.ts b/src/test/testing/common/managers/baseTestManager.unit.test.ts deleted file mode 100644 index 4ad4ec3a351d..000000000000 --- a/src/test/testing/common/managers/baseTestManager.unit.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:no-any - -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Disposable, OutputChannel, Uri } from 'vscode'; -import { CommandManager } from '../../../../client/common/application/commandManager'; -import { ICommandManager, IWorkspaceService } from '../../../../client/common/application/types'; -import { WorkspaceService } from '../../../../client/common/application/workspace'; -import { PythonSettings } from '../../../../client/common/configSettings'; -import { ConfigurationService } from '../../../../client/common/configuration/service'; -import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../../../../client/common/types'; -import { ServiceContainer } from '../../../../client/ioc/container'; -import { IServiceContainer } from '../../../../client/ioc/types'; -import { CommandSource, TEST_OUTPUT_CHANNEL } from '../../../../client/testing/common/constants'; -import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; -import { TestResultsService } from '../../../../client/testing/common/services/testResultsService'; -import { TestsStatusUpdaterService } from '../../../../client/testing/common/services/testsStatusService'; -import { UnitTestDiagnosticService } from '../../../../client/testing/common/services/unitTestDiagnosticService'; -import { TestsHelper } from '../../../../client/testing/common/testUtils'; -import { ITestCollectionStorageService, ITestManager, ITestMessageService, ITestResultsService, ITestsHelper, ITestsStatusUpdaterService } from '../../../../client/testing/common/types'; -import { TestManager as NoseTestManager } from '../../../../client/testing/nosetest/main'; -import { TestManager as PyTestTestManager } from '../../../../client/testing/pytest/main'; -import { ArgumentsService } from '../../../../client/testing/pytest/services/argsService'; -import { TestMessageService } from '../../../../client/testing/pytest/services/testMessageService'; -import { IArgumentsService, ITestDiagnosticService, ITestManagerRunner } from '../../../../client/testing/types'; -import { TestManager as UnitTestTestManager } from '../../../../client/testing/unittest/main'; -import { TestManagerRunner } from '../../../../client/testing/unittest/runner'; -import { noop } from '../../../core'; -import { MockOutputChannel } from '../../../mockClasses'; - -suite('Unit Tests - Base Test Manager', () => { - [ - { name: 'nose', class: NoseTestManager }, - { name: 'pytest', class: PyTestTestManager }, - { name: 'unittest', class: UnitTestTestManager } - ].forEach(item => { - suite(item.name, () => { - let testManager: ITestManager; - const workspaceFolder = Uri.file(__dirname); - let serviceContainer: IServiceContainer; - let configService: IConfigurationService; - let settings: IPythonSettings; - let outputChannel: IOutputChannel; - let storageService: ITestCollectionStorageService; - let resultsService: ITestResultsService; - let workspaceService: IWorkspaceService; - let diagnosticService: ITestDiagnosticService; - let statusUpdater: ITestsStatusUpdaterService; - let commandManager: ICommandManager; - setup(() => { - serviceContainer = mock(ServiceContainer); - settings = mock(PythonSettings); - configService = mock(ConfigurationService); - outputChannel = mock(MockOutputChannel); - storageService = mock(TestCollectionStorageService); - resultsService = mock(TestResultsService); - workspaceService = mock(WorkspaceService); - diagnosticService = mock(UnitTestDiagnosticService); - statusUpdater = mock(TestsStatusUpdaterService); - commandManager = mock(CommandManager); - - const argsService = mock(ArgumentsService); - const testsHelper = mock(TestsHelper); - const runner = mock(TestManagerRunner); - const messageService = mock(TestMessageService); - - when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(configService)); - when(serviceContainer.get<Disposable[]>(IDisposableRegistry)).thenReturn([]); - when(serviceContainer.get<OutputChannel>(IOutputChannel, TEST_OUTPUT_CHANNEL)).thenReturn(instance(outputChannel)); - when(serviceContainer.get<ITestCollectionStorageService>(ITestCollectionStorageService)).thenReturn(instance(storageService)); - when(serviceContainer.get<ITestResultsService>(ITestResultsService)).thenReturn(instance(resultsService)); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get<ITestDiagnosticService>(ITestDiagnosticService)).thenReturn(instance(diagnosticService)); - when(serviceContainer.get<ITestsStatusUpdaterService>(ITestsStatusUpdaterService)).thenReturn(instance(statusUpdater)); - when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(commandManager)); - - when(serviceContainer.get<IArgumentsService>(IArgumentsService, anything())).thenReturn(instance(argsService)); - when(serviceContainer.get<ITestsHelper>(ITestsHelper)).thenReturn(instance(testsHelper)); - when(serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, anything())).thenReturn(instance(runner)); - when(serviceContainer.get<ITestMessageService>(ITestMessageService, anything())).thenReturn(instance(messageService)); - - when(configService.getSettings(anything())).thenReturn(instance(settings)); - - testManager = new item.class(workspaceFolder, workspaceFolder.fsPath, instance(serviceContainer)); - }); - - test('Discovering tests should display test manager', async () => { - when(commandManager.executeCommand(anything(), anything(), anything())).thenResolve(); - - try { - await testManager.discoverTests(CommandSource.auto, true, true, true); - } catch { - noop(); - } - - verify(commandManager.executeCommand('setContext', 'testsDiscovered', true)).once(); - }); - }); - }); -}); diff --git a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts index 253215b83616..1b049d4f3fbe 100644 --- a/src/test/testing/common/managers/testConfigurationManager.unit.test.ts +++ b/src/test/testing/common/managers/testConfigurationManager.unit.test.ts @@ -3,31 +3,33 @@ 'use strict'; -// tslint:disable:no-any - import * as TypeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../../../client/common/types'; +import { IInstaller, ILogOutputChannel, Product } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { IServiceContainer } from '../../../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; -import { TestConfigurationManager } from '../../../../client/testing/common/managers/testConfigurationManager'; -import { UnitTestProduct } from '../../../../client/testing/common/types'; -import { ITestConfigSettingsService } from '../../../../client/testing/types'; +import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; +import { TestConfigurationManager } from '../../../../client/testing/common/testConfigurationManager'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; class MockTestConfigurationManager extends TestConfigurationManager { - public requiresUserToConfigure(_wkspace: Uri): Promise<boolean> { + // The workspace arg is ignored. + // eslint-disable-next-line class-methods-use-this + public requiresUserToConfigure(): Promise<boolean> { throw new Error('Method not implemented.'); } - public configure(_wkspace: any): Promise<any> { + + // The workspace arg is ignored. + // eslint-disable-next-line class-methods-use-this + public configure(): Promise<void> { throw new Error('Method not implemented.'); } } suite('Unit Test Configuration Manager (unit)', () => { - UNIT_TEST_PRODUCTS.forEach(product => { + UNIT_TEST_PRODUCTS.forEach((product) => { const prods = getNamesAndValues(Product); - const productName = prods.filter(item => item.value === product)[0]; + const productName = prods.filter((item) => item.value === product)[0]; suite(productName.name, () => { const workspaceUri = Uri.file(__dirname); let manager: TestConfigurationManager; @@ -38,22 +40,29 @@ suite('Unit Test Configuration Manager (unit)', () => { const outputChannel = TypeMoq.Mock.ofType<OutputChannel>().object; const installer = TypeMoq.Mock.ofType<IInstaller>().object; const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>(); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IOutputChannel), TypeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ITestConfigSettingsService))).returns(() => configService.object); - serviceContainer.setup(s => s.get(TypeMoq.It.isValue(IInstaller))).returns(() => installer); - manager = new MockTestConfigurationManager(workspaceUri, product as UnitTestProduct, serviceContainer.object); + 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))) + 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()); diff --git a/src/test/testing/common/services/configSettingService.unit.test.ts b/src/test/testing/common/services/configSettingService.unit.test.ts index adf6ab4651f4..d369d7ead825 100644 --- a/src/test/testing/common/services/configSettingService.unit.test.ts +++ b/src/test/testing/common/services/configSettingService.unit.test.ts @@ -3,8 +3,6 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any - import { expect, use } from 'chai'; import * as chaiPromise from 'chai-as-promised'; import * as typeMoq from 'typemoq'; @@ -14,22 +12,24 @@ import { Product } from '../../../../client/common/types'; import { getNamesAndValues } from '../../../../client/common/utils/enum'; import { IServiceContainer } from '../../../../client/ioc/types'; import { UNIT_TEST_PRODUCTS } from '../../../../client/testing/common/constants'; -import { - BufferedTestConfigSettingsService, TestConfigSettingsService -} from '../../../../client/testing/common/services/configSettingService'; -import { UnitTestProduct } from '../../../../client/testing/common/types'; -import { ITestConfigSettingsService } from '../../../../client/testing/types'; +import { TestConfigSettingsService } from '../../../../client/testing/common/configSettingService'; +import { ITestConfigSettingsService, UnitTestProduct } from '../../../../client/testing/common/types'; +import { BufferedTestConfigSettingsService } from '../../../../client/testing/common/bufferedTestConfigSettingService'; -use(chaiPromise); +use(chaiPromise.default); -const updateMethods: (keyof ITestConfigSettingsService)[] = ['updateTestArgs', 'disable', 'enable']; +const updateMethods: (keyof Omit<ITestConfigSettingsService, 'getTestEnablingSetting'>)[] = [ + 'updateTestArgs', + 'disable', + 'enable', +]; suite('Unit Tests - ConfigSettingsService', () => { - UNIT_TEST_PRODUCTS.forEach(product => { + UNIT_TEST_PRODUCTS.forEach((product) => { const prods = getNamesAndValues(Product); - const productName = prods.filter(item => item.value === product)[0]; + const productName = prods.filter((item) => item.value === product)[0]; const workspaceUri = Uri.file(__filename); - updateMethods.forEach(updateMethod => { + updateMethods.forEach((updateMethod) => { suite(`Test '${updateMethod}' method with ${productName.name}`, () => { let testConfigSettingsService: ITestConfigSettingsService; let workspaceService: typeMoq.IMock<IWorkspaceService>; @@ -37,7 +37,9 @@ suite('Unit Tests - ConfigSettingsService', () => { const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); workspaceService = typeMoq.Mock.ofType<IWorkspaceService>(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(IWorkspaceService))) + .returns(() => workspaceService.object); testConfigSettingsService = new TestConfigSettingsService(serviceContainer.object); }); function getTestArgSetting(prod: UnitTestProduct) { @@ -46,8 +48,6 @@ suite('Unit Tests - ConfigSettingsService', () => { return 'testing.unittestArgs'; case Product.pytest: return 'testing.pytestArgs'; - case Product.nosetest: - return 'testing.nosetestArgs'; default: throw new Error('Invalid Test Product'); } @@ -58,8 +58,6 @@ suite('Unit Tests - ConfigSettingsService', () => { return 'testing.unittestEnabled'; case Product.pytest: return 'testing.pytestEnabled'; - case Product.nosetest: - return 'testing.nosetestsEnabled'; default: throw new Error('Invalid Test Product'); } @@ -81,18 +79,16 @@ suite('Unit Tests - ConfigSettingsService', () => { } } test('Update Test Arguments with workspace Uri without workspaces', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => false) - .verifiable(typeMoq.Times.atLeastOnce()); - const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'))) + 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))) + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); @@ -105,25 +101,27 @@ suite('Unit Tests - ConfigSettingsService', () => { pythonConfig.verifyAll(); }); test('Update Test Arguments with workspace Uri with one workspace', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - const workspaceFolder = typeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolder.setup(w => w.uri) + workspaceFolder + .setup((w) => w.uri) .returns(() => workspaceUri) .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) + workspaceService + .setup((w) => w.workspaceFolders) .returns(() => [workspaceFolder.object]) .verifiable(typeMoq.Times.atLeastOnce()); const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) + 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))) + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); @@ -137,28 +135,31 @@ suite('Unit Tests - ConfigSettingsService', () => { pythonConfig.verifyAll(); }); test('Update Test Arguments with workspace Uri with more than one workspace and uri belongs to a workspace', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - const workspaceFolder = typeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolder.setup(w => w.uri) + workspaceFolder + .setup((w) => w.uri) .returns(() => workspaceUri) .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) + workspaceService + .setup((w) => w.workspaceFolders) .returns(() => [workspaceFolder.object, workspaceFolder.object]) .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + workspaceService + .setup((w) => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) .returns(() => workspaceFolder.object) .verifiable(typeMoq.Times.once()); const pythonConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), typeMoq.It.isValue(workspaceUri))) + 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))) + pythonConfig + .setup((p) => p.update(typeMoq.It.isValue(configName), typeMoq.It.isValue(configValue))) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); @@ -172,18 +173,17 @@ suite('Unit Tests - ConfigSettingsService', () => { pythonConfig.verifyAll(); }); test('Expect an exception when updating Test Arguments with workspace Uri with more than one workspace and uri does not belong to a workspace', async () => { - workspaceService.setup(w => w.hasWorkspaceFolders) - .returns(() => true) - .verifiable(typeMoq.Times.atLeastOnce()); - const workspaceFolder = typeMoq.Mock.ofType<WorkspaceFolder>(); - workspaceFolder.setup(w => w.uri) + workspaceFolder + .setup((w) => w.uri) .returns(() => workspaceUri) .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.workspaceFolders) + workspaceService + .setup((w) => w.workspaceFolders) .returns(() => [workspaceFolder.object, workspaceFolder.object]) .verifiable(typeMoq.Times.atLeastOnce()); - workspaceService.setup(w => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) + workspaceService + .setup((w) => w.getWorkspaceFolder(typeMoq.It.isValue(workspaceUri))) .returns(() => undefined) .verifiable(typeMoq.Times.once()); @@ -203,23 +203,25 @@ suite('Unit Tests - BufferedTestConfigSettingsService', () => { const testDir = '/my/project'; const newArgs: string[] = ['-x', '--spam=42']; const cfg = typeMoq.Mock.ofType<ITestConfigSettingsService>(undefined, typeMoq.MockBehavior.Strict); - cfg.setup(c => c.updateTestArgs(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.pytest), typeMoq.It.isValue(newArgs))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - cfg.setup(c => c.disable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.unittest))) + 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.nosetest))) + 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))) + cfg.setup((c) => c.enable(typeMoq.It.isValue(testDir), typeMoq.It.isValue(Product.pytest))) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); const delayed = new BufferedTestConfigSettingsService(); await delayed.updateTestArgs(testDir, Product.pytest, newArgs); await delayed.disable(testDir, Product.unittest); - await delayed.disable(testDir, Product.nosetest); await delayed.enable(testDir, Product.pytest); await delayed.apply(cfg.object); @@ -231,7 +233,7 @@ suite('Unit Tests - BufferedTestConfigSettingsService', () => { test('applied changes are cleared', async () => { const cfg = typeMoq.Mock.ofType<ITestConfigSettingsService>(undefined, typeMoq.MockBehavior.Strict); - cfg.setup(c => c.enable(typeMoq.It.isAny(), typeMoq.It.isAny())) + cfg.setup((c) => c.enable(typeMoq.It.isAny(), typeMoq.It.isAny())) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); diff --git a/src/test/testing/common/services/contextService.unit.test.ts b/src/test/testing/common/services/contextService.unit.test.ts deleted file mode 100644 index 88b4150f6096..000000000000 --- a/src/test/testing/common/services/contextService.unit.test.ts +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { CommandManager } from '../../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../../client/common/application/types'; -import { TestContextService } from '../../../../client/testing/common/services/contextService'; -import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; -import { ITestCollectionStorageService, ITestContextService, TestStatus } from '../../../../client/testing/common/types'; -import { UnitTestManagementService } from '../../../../client/testing/main'; -import { ITestManagementService, WorkspaceTestStatus } from '../../../../client/testing/types'; - -// tslint:disable:no-any max-func-body-length -suite('Unit Tests - Context Service', () => { - let cmdManager: ICommandManager; - let contextService: ITestContextService; - let storage: ITestCollectionStorageService; - let mgr: ITestManagementService; - const workspaceUri = Uri.file(__filename); - type StatusChangeHandler = (status: WorkspaceTestStatus) => Promise<void>; - setup(() => { - cmdManager = mock(CommandManager); - storage = mock(TestCollectionStorageService); - mgr = mock(UnitTestManagementService); - contextService = new TestContextService(instance(storage), instance(mgr), instance(cmdManager)); - }); - - test('register will add event handler', () => { - let invoked = false; - const fn = () => invoked = true; - when(mgr.onDidStatusChange).thenReturn(fn as any); - - contextService.register(); - - assert.equal(invoked, true); - }); - test('Status change without tests does not update hasFailedTests', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(workspaceUri)).thenReturn(); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'hasFailedTests', anything())).never(); - }); - test('Status change without a summary does not update hasFailedTests', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(workspaceUri)).thenReturn({} as any); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'hasFailedTests', anything())).never(); - }); - test('Status change with a summary updates hasFailedTests to false ', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(anything())).thenReturn({ summary: { failures: 0 } } as any); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'hasFailedTests', false)).once(); - }); - test('Status change with a summary and failures updates hasFailedTests to false', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(anything())).thenReturn({ summary: { failures: 1 } } as any); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'hasFailedTests', true)).once(); - }); - test('Status change with status of running', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(anything())).thenReturn({} as any); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Running, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'runningTests', true)).once(); - verify(cmdManager.executeCommand('setContext', 'discoveringTests', false)).once(); - verify(cmdManager.executeCommand('setContext', 'busyTests', true)).once(); - }); - test('Status change with status of discovering', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(anything())).thenReturn({} as any); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Discovering, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'runningTests', false)).once(); - verify(cmdManager.executeCommand('setContext', 'discoveringTests', true)).once(); - verify(cmdManager.executeCommand('setContext', 'busyTests', true)).once(); - }); - test('Status change with status of others', async () => { - let handler!: StatusChangeHandler; - const fn = (cb: StatusChangeHandler) => handler = cb; - when(mgr.onDidStatusChange).thenReturn(fn as any); - when(storage.getTests(anything())).thenReturn({} as any); - contextService.register(); - - await handler.bind(contextService)({ status: TestStatus.Error, workspace: workspaceUri }); - await handler.bind(contextService)({ status: TestStatus.Fail, workspace: workspaceUri }); - await handler.bind(contextService)({ status: TestStatus.Idle, workspace: workspaceUri }); - await handler.bind(contextService)({ status: TestStatus.Pass, workspace: workspaceUri }); - await handler.bind(contextService)({ status: TestStatus.Skipped, workspace: workspaceUri }); - await handler.bind(contextService)({ status: TestStatus.Unknown, workspace: workspaceUri }); - - verify(cmdManager.executeCommand('setContext', 'runningTests', false)).once(); - verify(cmdManager.executeCommand('setContext', 'discoveringTests', false)).once(); - verify(cmdManager.executeCommand('setContext', 'busyTests', false)).once(); - - verify(cmdManager.executeCommand('setContext', 'runningTests', true)).never(); - verify(cmdManager.executeCommand('setContext', 'discoveringTests', true)).never(); - verify(cmdManager.executeCommand('setContext', 'busyTests', true)).never(); - }); -}); diff --git a/src/test/testing/common/services/discovery.unit.test.ts b/src/test/testing/common/services/discovery.unit.test.ts deleted file mode 100644 index 41f0f9cc8aa2..000000000000 --- a/src/test/testing/common/services/discovery.unit.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { deepEqual, instance, mock, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, OutputChannel, Uri, ViewColumn } from 'vscode'; -import { PythonExecutionFactory } from '../../../../client/common/process/pythonExecutionFactory'; -import { ExecutionFactoryCreateWithEnvironmentOptions, IPythonExecutionFactory, IPythonExecutionService, SpawnOptions } from '../../../../client/common/process/types'; -import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; -import { TestDiscoveredTestParser } from '../../../../client/testing/common/services/discoveredTestParser'; -import { TestsDiscoveryService } from '../../../../client/testing/common/services/discovery'; -import { DiscoveredTests, ITestDiscoveredTestParser } from '../../../../client/testing/common/services/types'; -import { TestDiscoveryOptions, Tests } from '../../../../client/testing/common/types'; -import { MockOutputChannel } from '../../../mockClasses'; - -// tslint:disable:no-unnecessary-override no-any -suite('Unit Tests - Common Discovery', () => { - let output: OutputChannel; - let discovery: TestsDiscoveryService; - let executionFactory: IPythonExecutionFactory; - let parser: ITestDiscoveredTestParser; - setup(() => { - // tslint:disable-next-line:no-use-before-declare - output = mock(StubOutput); - executionFactory = mock(PythonExecutionFactory); - parser = mock(TestDiscoveredTestParser); - discovery = new TestsDiscoveryService(instance(executionFactory), instance(parser), instance(output)); - }); - test('Use parser to parse results', async () => { - const options: TestDiscoveryOptions = { - args: [], cwd: __dirname, workspaceFolder: Uri.file(__dirname), - ignoreCache: false, token: new CancellationTokenSource().token, - outChannel: new MockOutputChannel('Test') - }; - const discoveredTests: DiscoveredTests[] = [{ hello: 1 } as any]; - const parsedResult = { done: true } as any as Tests; - const json = JSON.stringify(discoveredTests); - discovery.exec = () => Promise.resolve({ stdout: json }); - when(parser.parse(options.workspaceFolder, deepEqual(discoveredTests))).thenResolve(parsedResult as any); - - const tests = await discovery.discoverTests(options); - - assert.deepEqual(tests, parsedResult); - }); - test('Invoke Python Code to discover tests', async () => { - const options: TestDiscoveryOptions = { - args: ['1', '2', '3'], cwd: __dirname, workspaceFolder: Uri.file(__dirname), - ignoreCache: false, token: new CancellationTokenSource().token, - outChannel: new MockOutputChannel('Test') - }; - const discoveredTests = '[1]'; - const execService = typemoq.Mock.ofType<IPythonExecutionService>(); - execService.setup((e: any) => e.then).returns(() => undefined); - const creationOptions: ExecutionFactoryCreateWithEnvironmentOptions = { - allowEnvironmentFetchExceptions: false, - resource: options.workspaceFolder - }; - const pythonFile = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'testing_tools', 'run_adapter.py'); - const spawnOptions: SpawnOptions = { - token: options.token, - cwd: options.cwd, - throwOnStdErr: true - }; - - when(executionFactory.createActivatedEnvironment(deepEqual(creationOptions))).thenResolve(execService.object); - const executionResult = { stdout: discoveredTests }; - execService.setup(e => e.exec(typemoq.It.isValue([pythonFile, ...options.args]), typemoq.It.isValue(spawnOptions))).returns(() => Promise.resolve(executionResult)); - - const result = await discovery.exec(options); - - execService.verifyAll(); - assert.deepEqual(result, executionResult); - }); -}); - -// tslint:disable:no-empty - -//class StubOutput implements OutputChannel { -class StubOutput { - constructor(public name: string) {} - public append(_value: string) {} - public appendLine(_value: string) {} - public clear() {} - //public show(_preserveFocus?: boolean) {} - public show(_column?: ViewColumn | boolean, _preserveFocus?: boolean) {} - public hide() {} - public dispose() {} -} diff --git a/src/test/testing/common/services/storageService.unit.test.ts b/src/test/testing/common/services/storageService.unit.test.ts deleted file mode 100644 index 46869040520a..000000000000 --- a/src/test/testing/common/services/storageService.unit.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { copyDesiredTestResults } from '../../../../client/testing/common/testUtils'; -import { FlattenedTestFunction, FlattenedTestSuite, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../../client/testing/common/types'; -import { createMockTestDataItem } from '../testUtils.unit.test'; - -// tslint:disable:no-any max-func-body-length -suite('Unit Tests - Storage Service', () => { - let testData1: Tests; - let testData2: Tests; - setup(() => { - setupTestData1(); - setupTestData2(); - }); - - function setupTestData1() { - const folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder, '1'); - const file1 = createMockTestDataItem<TestFile>(TestType.testFile, '1'); - folder1.testFiles.push(file1); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite, '1'); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite, '2'); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction, '1'); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction, '2'); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction, '3'); - file1.suites.push(suite1); - file1.suites.push(suite2); - file1.functions.push(fn1); - suite1.functions.push(fn2); - suite2.functions.push(fn3); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - testData1 = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1], - testFolders: [folder1], - testFunctions: [flattendFn1, flattendFn2, flattendFn3], - testSuites: [flattendSuite1, flattendSuite2] - }; - } - - function setupTestData2() { - const folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder, '1'); - const file1 = createMockTestDataItem<TestFile>(TestType.testFile, '1'); - folder1.testFiles.push(file1); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite, '1'); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite, '2'); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction, '1'); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction, '2'); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction, '3'); - file1.suites.push(suite1); - file1.suites.push(suite2); - suite1.functions.push(fn1); - suite1.functions.push(fn2); - suite2.functions.push(fn3); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - testData2 = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1], - testFolders: [folder1], - testFunctions: [flattendFn1, flattendFn2, flattendFn3], - testSuites: [flattendSuite1, flattendSuite2] - }; - } - - test('Merge Status from existing tests', () => { - testData1.testFunctions[0].testFunction.passed = true; - testData1.testFunctions[1].testFunction.status = TestStatus.Fail; - testData1.testFunctions[2].testFunction.time = 1234; - - assert.notDeepEqual(testData1.testFunctions[0].testFunction, testData2.testFunctions[0].testFunction); - assert.notDeepEqual(testData1.testFunctions[1].testFunction, testData2.testFunctions[1].testFunction); - assert.notDeepEqual(testData1.testFunctions[2].testFunction, testData2.testFunctions[2].testFunction); - - copyDesiredTestResults(testData1, testData2); - - // Function 1 is in a different suite now, hence should not get updated. - assert.notDeepEqual(testData1.testFunctions[0].testFunction, testData2.testFunctions[0].testFunction); - assert.deepEqual(testData1.testFunctions[1].testFunction, testData2.testFunctions[1].testFunction); - assert.deepEqual(testData1.testFunctions[2].testFunction, testData2.testFunctions[2].testFunction); - }); -}); diff --git a/src/test/testing/common/services/testResultsService.unit.test.ts b/src/test/testing/common/services/testResultsService.unit.test.ts deleted file mode 100644 index 48a73ff09e7e..000000000000 --- a/src/test/testing/common/services/testResultsService.unit.test.ts +++ /dev/null @@ -1,203 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as typemoq from 'typemoq'; -import { TestResultsService } from '../../../../client/testing/common/services/testResultsService'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestVisitor, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../../client/testing/common/types'; -import { createMockTestDataItem } from '../testUtils.unit.test'; - -// tslint:disable:no-any max-func-body-length -suite('Unit Tests - Tests Results Service', () => { - let testResultsService: TestResultsService; - let resultResetVisitor: typemoq.IMock<ITestVisitor>; - let tests!: Tests; - // tslint:disable:one-variable-per-declaration - let folder1: TestFolder, folder2: TestFolder, folder3: TestFolder, folder4: TestFolder, folder5: TestFolder, suite1: TestSuite, suite2: TestSuite, suite3: TestSuite, suite4: TestSuite, suite5: TestSuite; - let file1: TestFile, file2: TestFile, file3: TestFile, file4: TestFile, file5: TestFile; - setup(() => { - resultResetVisitor = typemoq.Mock.ofType<ITestVisitor>(); - folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder2 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder3 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder4 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder5 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder1.folders.push(folder2); - folder1.folders.push(folder3); - folder2.folders.push(folder4); - folder3.folders.push(folder5); - - file1 = createMockTestDataItem<TestFile>(TestType.testFile); - file2 = createMockTestDataItem<TestFile>(TestType.testFile); - file3 = createMockTestDataItem<TestFile>(TestType.testFile); - file4 = createMockTestDataItem<TestFile>(TestType.testFile); - file5 = createMockTestDataItem<TestFile>(TestType.testFile); - folder1.testFiles.push(file1); - folder3.testFiles.push(file2); - folder3.testFiles.push(file3); - folder4.testFiles.push(file5); - folder5.testFiles.push(file4); - - suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn1.passed = true; - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn2.passed = undefined; - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn3.passed = true; - const fn4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn4.passed = false; - const fn5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn5.passed = undefined; - const fn6 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn6.passed = true; - const fn7 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn7.passed = undefined; - const fn8 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn8.passed = false; - const fn9 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn9.passed = true; - const fn10 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn10.passed = true; - const fn11 = createMockTestDataItem<TestFunction>(TestType.testFunction); - fn11.passed = true; - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - file1.functions.push(fn1); - file1.functions.push(fn2); - file2.functions.push(fn8); - file4.functions.push(fn9); - file4.functions.push(fn11); - file5.functions.push(fn10); - suite1.functions.push(fn3); - suite1.functions.push(fn4); - suite2.functions.push(fn6); - suite3.functions.push(fn5); - suite5.functions.push(fn7); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - const flattendFn4: FlattenedTestFunction = { - testFunction: fn4, - xmlClassName: fn4.name - } as any; - const flattendFn5: FlattenedTestFunction = { - testFunction: fn5, - xmlClassName: fn5.name - } as any; - const flattendFn6: FlattenedTestFunction = { - testFunction: fn6, - xmlClassName: fn6.name - } as any; - const flattendFn7: FlattenedTestFunction = { - testFunction: fn7, - xmlClassName: fn7.name - } as any; - const flattendFn8: FlattenedTestFunction = { - testFunction: fn8, - xmlClassName: fn8.name - } as any; - const flattendFn9: FlattenedTestFunction = { - testFunction: fn9, - xmlClassName: fn9.name - } as any; - const flattendFn10: FlattenedTestFunction = { - testFunction: fn10, - xmlClassName: fn10.name - } as any; - const flattendFn11: FlattenedTestFunction = { - testFunction: fn11, - xmlClassName: fn11.name - } as any; - tests = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4, file5], - testFolders: [folder1, folder2, folder3, folder4, folder5], - testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5, flattendFn6, flattendFn7, flattendFn8, flattendFn9, flattendFn10, flattendFn11], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - testResultsService = new TestResultsService(resultResetVisitor.object); - }); - - test('If any test fails, parent fails', () => { - testResultsService.updateResults(tests); - expect(suite1.status).to.equal(TestStatus.Fail); - expect(file1.status).to.equal(TestStatus.Fail); - expect(folder1.status).to.equal(TestStatus.Fail); - expect(file2.status).to.equal(TestStatus.Fail); - expect(folder3.status).to.equal(TestStatus.Fail); - }); - - test('If all tests pass, parent passes', () => { - testResultsService.updateResults(tests); - expect(file4.status).to.equal(TestStatus.Pass); - expect(folder5.status).to.equal(TestStatus.Pass); - expect(folder2.status).to.equal(TestStatus.Pass); - }); - - test('If no tests run, parent status is not run', () => { - testResultsService.updateResults(tests); - expect(suite3.status).to.equal(TestStatus.Unknown); - expect(suite4.status).to.equal(TestStatus.Unknown); - expect(suite5.status).to.equal(TestStatus.Unknown); - expect(file3.status).to.equal(TestStatus.Unknown); - }); - - test('Number of functions passed, not run and failed are correctly calculated', () => { - testResultsService.updateResults(tests); - - expect(file1.functionsPassed).to.equal(3); - expect(folder2.functionsPassed).to.equal(1); - expect(folder3.functionsPassed).to.equal(2); - expect(folder1.functionsPassed).to.equal(6); - - expect(file1.functionsFailed).to.equal(1); - expect(folder2.functionsFailed).to.equal(0); - expect(folder3.functionsFailed).to.equal(1); - expect(folder1.functionsFailed).to.equal(2); - - expect(file1.functionsDidNotRun).to.equal(1); - expect(suite4.functionsDidNotRun).to.equal(1); - expect(suite3.functionsDidNotRun).to.equal(2); - expect(folder1.functionsDidNotRun).to.equal(3); - }); -}); diff --git a/src/test/testing/common/services/testStatusService.unit.test.ts b/src/test/testing/common/services/testStatusService.unit.test.ts deleted file mode 100644 index 43f5e9880d7e..000000000000 --- a/src/test/testing/common/services/testStatusService.unit.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { TestCollectionStorageService } from '../../../../client/testing/common/services/storageService'; -import { TestsStatusUpdaterService } from '../../../../client/testing/common/services/testsStatusService'; -import { visitRecursive } from '../../../../client/testing/common/testVisitors/visitor'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestCollectionStorageService, ITestsStatusUpdaterService, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../../client/testing/common/types'; -import { TestDataItem } from '../../../../client/testing/types'; -import { createMockTestDataItem } from '../testUtils.unit.test'; - -// tslint:disable:no-any max-func-body-length -suite('Unit Tests - Tests Status Updater', () => { - let storage: ITestCollectionStorageService; - let updater: ITestsStatusUpdaterService; - const workspaceUri = Uri.file(__filename); - let tests!: Tests; - setup(() => { - storage = mock(TestCollectionStorageService); - updater = new TestsStatusUpdaterService(instance(storage)); - const folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder2 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder3 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder4 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder5 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder1.folders.push(folder2); - folder1.folders.push(folder3); - folder2.folders.push(folder4); - folder3.folders.push(folder5); - - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - folder1.testFiles.push(file1); - folder3.testFiles.push(file2); - folder3.testFiles.push(file3); - folder5.testFiles.push(file4); - - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - file1.functions.push(fn1); - file1.functions.push(fn2); - suite1.functions.push(fn3); - suite1.functions.push(fn4); - suite3.functions.push(fn5); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - const flattendFn4: FlattenedTestFunction = { - testFunction: fn4, - xmlClassName: fn4.name - } as any; - const flattendFn5: FlattenedTestFunction = { - testFunction: fn5, - xmlClassName: fn5.name - } as any; - tests = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [folder1, folder2, folder3, folder4, folder5], - testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - when(storage.getTests(workspaceUri)).thenReturn(tests); - }); - - test('Updating discovery status will recursively update all items and triggers an update for each', () => { - updater.updateStatusAsDiscovering(workspaceUri, tests); - - function validate(item: TestDataItem) { - assert.equal(item.status, TestStatus.Discovering); - verify(storage.update(workspaceUri, item)).once(); - } - tests.testFolders.forEach(validate); - tests.testFiles.forEach(validate); - tests.testFunctions.forEach(func => validate(func.testFunction)); - tests.testSuites.forEach(suite => validate(suite.testSuite)); - }); - test('Updating unknown status will recursively update all items and triggers an update for each', () => { - updater.updateStatusAsUnknown(workspaceUri, tests); - - function validate(item: TestDataItem) { - assert.equal(item.status, TestStatus.Unknown); - verify(storage.update(workspaceUri, item)).once(); - } - tests.testFolders.forEach(validate); - tests.testFiles.forEach(validate); - tests.testFunctions.forEach(func => validate(func.testFunction)); - tests.testSuites.forEach(suite => validate(suite.testSuite)); - }); - test('Updating running status will recursively update all items and triggers an update for each', () => { - updater.updateStatusAsRunning(workspaceUri, tests); - - function validate(item: TestDataItem) { - assert.equal(item.status, TestStatus.Running); - verify(storage.update(workspaceUri, item)).once(); - } - tests.testFolders.forEach(validate); - tests.testFiles.forEach(validate); - tests.testFunctions.forEach(func => validate(func.testFunction)); - tests.testSuites.forEach(suite => validate(suite.testSuite)); - }); - test('Updating running status for failed tests will recursively update all items and triggers an update for each', () => { - tests.testFolders[1].status = TestStatus.Fail; - tests.testFolders[2].status = TestStatus.Error; - tests.testFiles[2].status = TestStatus.Fail; - tests.testFiles[3].status = TestStatus.Error; - tests.testFunctions[2].testFunction.status = TestStatus.Fail; - tests.testFunctions[3].testFunction.status = TestStatus.Error; - tests.testFunctions[4].testFunction.status = TestStatus.Pass; - tests.testSuites[1].testSuite.status = TestStatus.Fail; - tests.testSuites[2].testSuite.status = TestStatus.Error; - - updater.updateStatusAsRunningFailedTests(workspaceUri, tests); - - // Do not update status of folders and files. - assert.equal(tests.testFolders[1].status, TestStatus.Fail); - assert.equal(tests.testFolders[2].status, TestStatus.Error); - assert.equal(tests.testFiles[2].status, TestStatus.Fail); - assert.equal(tests.testFiles[3].status, TestStatus.Error); - - // Update status of test functions and suites. - const updatedItems: TestDataItem[] = []; - const visitor = (item: TestDataItem) => { - if (item.status && item.status !== TestStatus.Pass) { - updatedItems.push(item); - } - }; - const failedItems = [ - tests.testFunctions[2].testFunction, - tests.testFunctions[3].testFunction, - tests.testSuites[1].testSuite, - tests.testSuites[2].testSuite - ]; - failedItems.forEach(failedItem => visitRecursive(tests, failedItem, visitor)); - - for (const item of updatedItems) { - assert.equal(item.status, TestStatus.Running); - verify(storage.update(workspaceUri, item)).once(); - } - - // Only items with status Fail & Error should be modified - assert.equal(tests.testFunctions[4].testFunction.status, TestStatus.Pass); - - // Should only be called for failed items. - verify(storage.update(workspaceUri, anything())).times(updatedItems.length); - }); - test('Updating idle status for runnings tests will recursively update all items and triggers an update for each', () => { - tests.testFolders[1].status = TestStatus.Running; - tests.testFolders[2].status = TestStatus.Running; - tests.testFiles[2].status = TestStatus.Running; - tests.testFiles[3].status = TestStatus.Running; - tests.testFunctions[2].testFunction.status = TestStatus.Running; - tests.testFunctions[3].testFunction.status = TestStatus.Running; - tests.testSuites[1].testSuite.status = TestStatus.Running; - tests.testSuites[2].testSuite.status = TestStatus.Running; - - updater.updateStatusOfRunningTestsAsIdle(workspaceUri, tests); - - const updatedItems: TestDataItem[] = []; - updatedItems.push(tests.testFolders[1]); - updatedItems.push(tests.testFolders[2]); - updatedItems.push(tests.testFiles[2]); - updatedItems.push(tests.testFiles[3]); - updatedItems.push(tests.testFunctions[2].testFunction); - updatedItems.push(tests.testFunctions[3].testFunction); - updatedItems.push(tests.testSuites[1].testSuite); - updatedItems.push(tests.testSuites[2].testSuite); - - for (const item of updatedItems) { - assert.equal(item.status, TestStatus.Idle); - verify(storage.update(workspaceUri, item)).once(); - } - - // Should only be called for failed items. - verify(storage.update(workspaceUri, anything())).times(updatedItems.length); - }); - test('Triggers an update for each', () => { - updater.triggerUpdatesToTests(workspaceUri, tests); - - const updatedItems: TestDataItem[] = [ - ...tests.testFolders, - ...tests.testFiles, - ...tests.testFunctions.map(item => item.testFunction), - ...tests.testSuites.map(item => item.testSuite) - ]; - - for (const item of updatedItems) { - verify(storage.update(workspaceUri, item)).once(); - } - - verify(storage.update(workspaceUri, anything())).times(updatedItems.length); - }); -}); diff --git a/src/test/testing/common/testUtils.unit.test.ts b/src/test/testing/common/testUtils.unit.test.ts deleted file mode 100644 index 2d907c796419..000000000000 --- a/src/test/testing/common/testUtils.unit.test.ts +++ /dev/null @@ -1,697 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import * as path from 'path'; -import { Uri } from 'vscode'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { - getChildren, getParent, getParentFile, getParentSuite, getTestFile, - getTestFolder, getTestFunction, getTestSuite, getTestType -} from '../../../client/testing/common/testUtils'; -import { - FlattenedTestFunction, FlattenedTestSuite, SubtestParent, TestFile, - TestFolder, TestFunction, Tests, TestSuite, TestType -} from '../../../client/testing/common/types'; -import { - TestDataItem, TestWorkspaceFolder -} from '../../../client/testing/types'; - -// tslint:disable:prefer-template - -function longestCommonSubstring(strings: string[]): string { - strings = strings.concat().sort(); - let substr = strings.shift() || ''; - strings.forEach(str => { - for (const [idx, ch] of [...substr].entries()) { - if (str[idx] !== ch) { - substr = substr.substring(0, idx); - break; - } - } - }); - return substr; -} - -export function createMockTestDataItem<T extends TestDataItem>( - type: TestType, - nameSuffix: string = '', - name?: string, - nameToRun?: string -) { - const folder: TestFolder = { - resource: Uri.file(__filename), - folders: [], - name: name || 'Some Folder' + nameSuffix, - nameToRun: nameToRun || name || ' Some Folder' + nameSuffix, - testFiles: [], - time: 0 - }; - const file: TestFile = { - resource: Uri.file(__filename), - name: name || 'Some File' + nameSuffix, - nameToRun: nameToRun || name || ' Some File' + nameSuffix, - fullPath: __filename, - xmlName: name || 'some xml name' + nameSuffix, - functions: [], - suites: [], - time: 0 - }; - const func: TestFunction = { - resource: Uri.file(__filename), - name: name || 'Some Function' + nameSuffix, - nameToRun: nameToRun || name || ' Some Function' + nameSuffix, - time: 0 - }; - const suite: TestSuite = { - resource: Uri.file(__filename), - name: name || 'Some Suite' + nameSuffix, - nameToRun: nameToRun || name || ' Some Suite' + nameSuffix, - functions: [], - isInstance: true, - isUnitTest: false, - suites: [], - xmlName: name || 'some name' + nameSuffix, - time: 0 - }; - - switch (type) { - case TestType.testFile: - return file as T; - case TestType.testFolder: - return folder as T; - case TestType.testFunction: - return func as T; - case TestType.testSuite: - return suite as T; - case TestType.testWorkspaceFolder: - return (new TestWorkspaceFolder({ uri: Uri.file(''), name: 'a', index: 0 })) as T; - default: - throw new Error('Unknown type'); - } -} - -export function createSubtestParent(funcs: TestFunction[]): SubtestParent { - const name = longestCommonSubstring(funcs.map(func => func.name)); - const nameToRun = longestCommonSubstring(funcs.map(func => func.nameToRun)); - const subtestParent: SubtestParent = { - name: name, - nameToRun: nameToRun, - asSuite: { - resource: Uri.file(__filename), - name: name, - nameToRun: nameToRun, - functions: funcs, - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: '', - time: 0 - }, - time: 0 - }; - funcs.forEach(func => { - func.subtestParent = subtestParent; - }); - return subtestParent; -} - -export function createTests( - folders: TestFolder[], - files: TestFile[], - suites: TestSuite[], - funcs: TestFunction[] -): Tests { - // tslint:disable:no-any - return { - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - rootTestFolders: folders.length > 0 ? [folders[0]] : [], - testFolders: folders, - testFiles: files, - testSuites: suites.map(suite => { - return { - testSuite: suite, - xmlClassName: suite.xmlName - } as any; - }), - testFunctions: funcs.map(func => { - return { - testFunction: func, - xmlClassName: func.name - } as any; - }) - }; -} - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - TestUtils', () => { - test('Get TestType for Folders', () => { - const item = createMockTestDataItem(TestType.testFolder); - assert.equal(getTestType(item), TestType.testFolder); - }); - test('Get TestType for Files', () => { - const item = createMockTestDataItem(TestType.testFile); - assert.equal(getTestType(item), TestType.testFile); - }); - test('Get TestType for Functions', () => { - const item = createMockTestDataItem(TestType.testFunction); - assert.equal(getTestType(item), TestType.testFunction); - }); - test('Get TestType for Suites', () => { - const item = createMockTestDataItem(TestType.testSuite); - assert.equal(getTestType(item), TestType.testSuite); - }); - test('Casting to a specific items', () => { - for (const typeName of getNamesAndValues<TestType>(TestType)) { - const item = createMockTestDataItem(typeName.value); - const file = getTestFile(item); - const folder = getTestFolder(item); - const suite = getTestSuite(item); - const func = getTestFunction(item); - - switch (typeName.value) { - case TestType.testFile: - { - assert.equal(file, item); - assert.equal(folder, undefined); - assert.equal(suite, undefined); - assert.equal(func, undefined); - break; - } - case TestType.testFolder: - { - assert.equal(file, undefined); - assert.equal(folder, item); - assert.equal(suite, undefined); - assert.equal(func, undefined); - break; - } - case TestType.testFunction: - { - assert.equal(file, undefined); - assert.equal(folder, undefined); - assert.equal(suite, undefined); - assert.equal(func, item); - break; - } - case TestType.testSuite: - { - assert.equal(file, undefined); - assert.equal(folder, undefined); - assert.equal(suite, item); - assert.equal(func, undefined); - break; - } - case TestType.testWorkspaceFolder: - { - assert.equal(file, undefined); - assert.equal(folder, undefined); - assert.equal(suite, undefined); - assert.equal(func, undefined); - break; - } - default: - throw new Error(`Unknown type ${typeName.name},${typeName.value}`); - } - } - }); - test('Get Parent of folder', () => { - const folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder2 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder3 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder4 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder5 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder1.folders.push(folder2); - folder1.folders.push(folder3); - folder2.folders.push(folder4); - folder3.folders.push(folder5); - const tests: Tests = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [], - testFolders: [folder1, folder2, folder3, folder4, folder5], - testFunctions: [], - testSuites: [] - }; - assert.equal(getParent(tests, folder1), undefined); - assert.equal(getParent(tests, folder2), folder1); - assert.equal(getParent(tests, folder3), folder1); - assert.equal(getParent(tests, folder4), folder2); - assert.equal(getParent(tests, folder5), folder3); - }); - test('Get Parent of file', () => { - const folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder2 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder3 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder4 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder5 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder1.folders.push(folder2); - folder1.folders.push(folder3); - folder2.folders.push(folder4); - folder3.folders.push(folder5); - - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - folder1.testFiles.push(file1); - folder3.testFiles.push(file2); - folder3.testFiles.push(file3); - folder5.testFiles.push(file4); - const tests: Tests = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [folder1, folder2, folder3, folder4, folder5], - testFunctions: [], - testSuites: [] - }; - assert.equal(getParent(tests, file1), folder1); - assert.equal(getParent(tests, file2), folder3); - assert.equal(getParent(tests, file3), folder3); - assert.equal(getParent(tests, file4), folder5); - }); - test('Get Parent File', () => { - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - file1.functions.push(fn1); - file1.functions.push(fn2); - suite1.functions.push(fn3); - suite1.functions.push(fn4); - suite3.functions.push(fn5); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - const flattendFn4: FlattenedTestFunction = { - testFunction: fn4, - xmlClassName: fn4.name - } as any; - const flattendFn5: FlattenedTestFunction = { - testFunction: fn5, - xmlClassName: fn5.name - } as any; - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [], - testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - // Test parent file of functions (standalone and those in suites). - assert.equal(getParentFile(tests, fn1), file1); - assert.equal(getParentFile(tests, fn2), file1); - assert.equal(getParentFile(tests, fn3), file1); - assert.equal(getParentFile(tests, fn4), file1); - assert.equal(getParentFile(tests, fn5), file3); - - // Test parent file of suites (standalone and nested suites). - assert.equal(getParentFile(tests, suite1), file1); - assert.equal(getParentFile(tests, suite2), file1); - assert.equal(getParentFile(tests, suite3), file3); - assert.equal(getParentFile(tests, suite4), file3); - assert.equal(getParentFile(tests, suite5), file3); - }); - test('Get Parent Suite', () => { - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - file1.functions.push(fn1); - file1.functions.push(fn2); - suite1.functions.push(fn3); - suite1.functions.push(fn4); - suite3.functions.push(fn5); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - const flattendFn4: FlattenedTestFunction = { - testFunction: fn4, - xmlClassName: fn4.name - } as any; - const flattendFn5: FlattenedTestFunction = { - testFunction: fn5, - xmlClassName: fn5.name - } as any; - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [], - testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - // Test parent file of functions (standalone and those in suites). - assert.equal(getParentSuite(tests, fn1), undefined); - assert.equal(getParentSuite(tests, fn2), undefined); - assert.equal(getParentSuite(tests, fn3), suite1); - assert.equal(getParentSuite(tests, fn4), suite1); - assert.equal(getParentSuite(tests, fn5), suite3); - - // Test parent file of suites (standalone and nested suites). - assert.equal(getParentSuite(tests, suite1), undefined); - assert.equal(getParentSuite(tests, suite2), undefined); - assert.equal(getParentSuite(tests, suite3), undefined); - assert.equal(getParentSuite(tests, suite4), suite3); - assert.equal(getParentSuite(tests, suite5), suite4); - }); - test('Get Parent file throws an exception', () => { - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1], - testFolders: [], - testFunctions: [flattendFn1], - testSuites: [flattendSuite1] - }; - assert.throws(() => getParentFile(tests, fn1), new RegExp('No parent file for provided test item')); - assert.throws(() => getParentFile(tests, suite1), new RegExp('No parent file for provided test item')); - }); - test('Get parent of orphaned items', () => { - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1], - testFolders: [], - testFunctions: [flattendFn1], - testSuites: [flattendSuite1] - }; - assert.equal(getParent(tests, fn1), undefined); - assert.equal(getParent(tests, suite1), undefined); - }); - test('Get Parent of suite', () => { - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [], - testFunctions: [], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - assert.equal(getParent(tests, suite1), file1); - assert.equal(getParent(tests, suite2), file1); - assert.equal(getParent(tests, suite3), file3); - assert.equal(getParent(tests, suite4), suite3); - assert.equal(getParent(tests, suite5), suite4); - }); - test('Get Parent of function', () => { - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - file1.functions.push(fn1); - file1.functions.push(fn2); - suite1.functions.push(fn3); - suite1.functions.push(fn4); - suite3.functions.push(fn5); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - const flattendFn4: FlattenedTestFunction = { - testFunction: fn4, - xmlClassName: fn4.name - } as any; - const flattendFn5: FlattenedTestFunction = { - testFunction: fn5, - xmlClassName: fn5.name - } as any; - const tests: Tests = { - rootTestFolders: [], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [], - testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - assert.equal(getParent(tests, fn1), file1); - assert.equal(getParent(tests, fn2), file1); - assert.equal(getParent(tests, fn3), suite1); - assert.equal(getParent(tests, fn4), suite1); - assert.equal(getParent(tests, fn5), suite3); - }); - test('Get parent of parameterized function', () => { - const folder = createMockTestDataItem<TestFolder>(TestType.testFolder); - const file = createMockTestDataItem<TestFile>(TestType.testFile); - const func1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const func2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const func3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const subParent1 = createSubtestParent([func2, func3]); - const suite = createMockTestDataItem<TestSuite>(TestType.testSuite); - const func4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const func5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const func6 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const subParent2 = createSubtestParent([func5, func6]); - folder.testFiles.push(file); - file.functions.push(func1); - file.functions.push(func2); - file.functions.push(func3); - file.suites.push(suite); - suite.functions.push(func4); - suite.functions.push(func5); - suite.functions.push(func6); - const tests = createTests( - [folder], - [file], - [suite], - [func1, func2, func3, func4, func5, func6] - ); - - assert.equal(getParent(tests, folder), undefined); - assert.equal(getParent(tests, file), folder); - assert.equal(getParent(tests, func1), file); - assert.equal(getParent(tests, subParent1.asSuite), file); - assert.equal(getParent(tests, func2), subParent1.asSuite); - assert.equal(getParent(tests, func3), subParent1.asSuite); - assert.equal(getParent(tests, suite), file); - assert.equal(getParent(tests, func4), suite); - assert.equal(getParent(tests, subParent2.asSuite), suite); - assert.equal(getParent(tests, func5), subParent2.asSuite); - assert.equal(getParent(tests, func6), subParent2.asSuite); - }); - test('Get children of parameterized function', () => { - const filename = path.join('tests', 'test_spam.py'); - const folder = createMockTestDataItem<TestFolder>(TestType.testFolder, 'tests'); - const file = createMockTestDataItem<TestFile>(TestType.testFile, filename); - const func1 = createMockTestDataItem<TestFunction>(TestType.testFunction, 'test_x'); - const func2 = createMockTestDataItem<TestFunction>(TestType.testFunction, 'test_y'); - const func3 = createMockTestDataItem<TestFunction>(TestType.testFunction, 'test_z'); - const subParent1 = createSubtestParent([func2, func3]); - const suite = createMockTestDataItem<TestSuite>(TestType.testSuite); - const func4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const func5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const func6 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const subParent2 = createSubtestParent([func5, func6]); - folder.testFiles.push(file); - file.functions.push(func1); - file.functions.push(func2); - file.functions.push(func3); - file.suites.push(suite); - suite.functions.push(func4); - suite.functions.push(func5); - suite.functions.push(func6); - - assert.deepEqual(getChildren(folder), [file]); - assert.deepEqual(getChildren(file), [func1, suite, subParent1.asSuite]); - assert.deepEqual(getChildren(func1), []); - assert.deepEqual(getChildren(subParent1.asSuite), [func2, func3]); - assert.deepEqual(getChildren(func2), []); - assert.deepEqual(getChildren(func3), []); - assert.deepEqual(getChildren(suite), [func4, subParent2.asSuite]); - assert.deepEqual(getChildren(func4), []); - assert.deepEqual(getChildren(subParent2.asSuite), [func5, func6]); - assert.deepEqual(getChildren(func5), []); - assert.deepEqual(getChildren(func6), []); - }); -}); diff --git a/src/test/testing/common/testingAdapter.test.ts b/src/test/testing/common/testingAdapter.test.ts new file mode 100644 index 000000000000..478e9dd85744 --- /dev/null +++ b/src/test/testing/common/testingAdapter.test.ts @@ -0,0 +1,1242 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { TestController, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as sinon from 'sinon'; +import { PytestTestDiscoveryAdapter } from '../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { + ITestController, + ITestResultResolver, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { EXTENSION_ROOT_DIR_FOR_TESTS, initialize } from '../../initialize'; +import { traceError, traceLog } from '../../../client/logging'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { PythonResultResolver } from '../../../client/testing/testController/common/resultResolver'; +import { TestProvider } from '../../../client/testing/types'; +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import { IEnvironmentVariablesProvider } from '../../../client/common/variables/types'; +import * as pixi from '../../../client/pythonEnvironments/common/environmentManagers/pixi'; + +suite('End to End Tests: test adapters', () => { + let resultResolver: ITestResultResolver; + let pythonExecFactory: IPythonExecutionFactory; + let configService: IConfigurationService; + let serviceContainer: IServiceContainer; + let envVarsService: IEnvironmentVariablesProvider; + let workspaceUri: Uri; + let testController: TestController; + let getPixiStub: sinon.SinonStub; + const unittestProvider: TestProvider = UNITTEST_PROVIDER; + const pytestProvider: TestProvider = PYTEST_PROVIDER; + const rootPathSmallWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'smallWorkspace', + ); + const rootPathLargeWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'largeWorkspace', + ); + const rootPathErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'errorWorkspace', + ); + const rootPathDiscoveryErrorWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'discoveryErrorWorkspace', + ); + const rootPathDiscoverySymlink = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'symlinkWorkspace', + ); + const nestedTarget = path.join(EXTENSION_ROOT_DIR_FOR_TESTS, 'src', 'testTestingRootWkspc', 'target workspace'); + const nestedSymlink = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'symlink_parent-folder', + ); + const rootPathCoverageWorkspace = path.join( + EXTENSION_ROOT_DIR_FOR_TESTS, + 'src', + 'testTestingRootWkspc', + 'coverageWorkspace', + ); + suiteSetup(async () => { + // create symlink for specific symlink test + const target = rootPathSmallWorkspace; + const dest = rootPathDiscoverySymlink; + try { + fs.symlink(target, dest, 'dir', (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink created successfully for regular symlink end to end tests.'); + } + }); + fs.symlink(nestedTarget, nestedSymlink, 'dir', (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink created successfully for nested symlink end to end tests.'); + } + }); + } catch (err) { + traceError(err); + } + }); + + setup(async () => { + serviceContainer = (await initialize()).serviceContainer; + getPixiStub = sinon.stub(pixi, 'getPixi'); + getPixiStub.resolves(undefined); + + // create objects that were injected + configService = serviceContainer.get<IConfigurationService>(IConfigurationService); + pythonExecFactory = serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory); + testController = serviceContainer.get<TestController>(ITestController); + envVarsService = serviceContainer.get<IEnvironmentVariablesProvider>(IEnvironmentVariablesProvider); + + // create objects that were not injected + }); + teardown(() => { + sinon.restore(); + }); + suiteTeardown(async () => { + // remove symlink + const dest = rootPathDiscoverySymlink; + if (fs.existsSync(dest)) { + fs.unlink(dest, (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink removed successfully after tests, rootPathDiscoverySymlink.'); + } + }); + } else { + traceLog('Symlink was not found to remove after tests, exiting successfully, rootPathDiscoverySymlink.'); + } + + if (fs.existsSync(nestedSymlink)) { + fs.unlink(nestedSymlink, (err) => { + if (err) { + traceError(err); + } else { + traceLog('Symlink removed successfully after tests, nestedSymlink.'); + } + }); + } else { + traceLog('Symlink was not found to remove after tests, exiting successfully, nestedSymlink.'); + } + }); + test('unittest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + // const deferredTillEOT = createTestingDeferred(); + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + + // set workspace to test workspace folder and set up settings + + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('unittest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + + // set settings to work for the given workspace + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run discovery + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); + // 2. Confirm no errors + assert.strictEqual(actualData.error, undefined, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter small workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter nested symlink', async () => { + if (os.platform() === 'win32') { + console.log('Skipping test for windows'); + return; + } + + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + const workspacePath = path.join(nestedSymlink, 'custom_sub_folder'); + const workspacePathParent = nestedSymlink; + workspaceUri = Uri.parse(workspacePath); + const filePath = path.join(workspacePath, 'test_simple.py'); + const stats = fs.lstatSync(workspacePathParent); + + // confirm that the path is a symbolic link + assert.ok(stats.isSymbolicLink(), 'The PARENT path is not a symbolic link but must be for this test.'); + + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + // 4. Confirm that the cwd returned is the symlink path and the test's path is also using the symlink as the root + if (process.platform === 'win32') { + // covert string to lowercase for windows as the path is case insensitive + traceLog('windows machine detected, converting path to lowercase for comparison'); + const a = actualData.cwd.toLowerCase(); + const b = filePath.toLowerCase(); + const testSimpleActual = (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path.toLowerCase(); + const testSimpleExpected = filePath.toLowerCase(); + assert.strictEqual(a, b, `Expected cwd to be the symlink path actual: ${a} expected: ${b}`); + assert.strictEqual( + testSimpleActual, + testSimpleExpected, + `Expected test path to be the symlink path actual: ${testSimpleActual} expected: ${testSimpleExpected}`, + ); + } else { + assert.strictEqual( + path.join(actualData.cwd), + path.join(workspacePath), + 'Expected cwd to be the symlink path, check for non-windows machines', + ); + assert.strictEqual( + (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path, + filePath, + 'Expected test path to be the symlink path, check for non windows machines', + ); + } + + // 5. Confirm that resolveDiscovery was called once + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter small workspace with symlink', async () => { + if (os.platform() === 'win32') { + console.log('Skipping test for windows'); + return; + } + + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + // set workspace to test workspace folder + const testSimpleSymlinkPath = path.join(rootPathDiscoverySymlink, 'test_simple.py'); + workspaceUri = Uri.parse(rootPathDiscoverySymlink); + const stats = fs.lstatSync(rootPathDiscoverySymlink); + + // confirm that the path is a symbolic link + assert.ok(stats.isSymbolicLink(), 'The path is not a symbolic link but must be for this test.'); + + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + // 4. Confirm that the cwd returned is the symlink path and the test's path is also using the symlink as the root + if (process.platform === 'win32') { + // covert string to lowercase for windows as the path is case insensitive + traceLog('windows machine detected, converting path to lowercase for comparison'); + const a = actualData.cwd.toLowerCase(); + const b = rootPathDiscoverySymlink.toLowerCase(); + const testSimpleActual = (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path.toLowerCase(); + const testSimpleExpected = testSimpleSymlinkPath.toLowerCase(); + assert.strictEqual(a, b, `Expected cwd to be the symlink path actual: ${a} expected: ${b}`); + assert.strictEqual( + testSimpleActual, + testSimpleExpected, + `Expected test path to be the symlink path actual: ${testSimpleActual} expected: ${testSimpleExpected}`, + ); + } else { + assert.strictEqual( + path.join(actualData.cwd), + path.join(rootPathDiscoverySymlink), + 'Expected cwd to be the symlink path, check for non-windows machines', + ); + assert.strictEqual( + (actualData.tests as { + children: { + path: string; + }[]; + }).children[0].path, + testSimpleSymlinkPath, + 'Expected test path to be the symlink path, check for non windows machines', + ); + } + + // 5. Confirm that resolveDiscovery was called once + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('pytest discovery adapter large workspace', async () => { + // result resolver and saved data for assertions + let actualData: { + cwd: string; + tests?: unknown; + status: 'success' | 'error'; + error?: string[]; + }; + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + resultResolver.resolveDiscovery = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + actualData = payload; + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + // 1. Check the status is "success" + assert.strictEqual( + actualData.status, + 'success', + `Expected status to be 'success' instead status is ${actualData.status}`, + ); // 2. Confirm no errors + assert.strictEqual(actualData.error?.length, 0, "Expected no errors in 'error' field"); + // 3. Confirm tests are found + assert.ok(actualData.tests, 'Expected tests to be present'); + + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + }); + }); + test('unittest execution adapter small workspace with correct output', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_simple.SimpleClass.test_simple_unit'], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as unittest output + assert.ok( + collectedOutput.includes('expected printed output, stdout'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('expected printed output, stderr'), + 'The test string does not contain the expected stderr output.', + ); + assert.ok( + collectedOutput.includes('Ran 1 test in'), + 'The test string does not contain the expected unittest output.', + ); + }); + }); + test('unittest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + const validStatuses = ['subtest-success', 'subtest-failure']; + assert.ok( + validStatuses.includes(payload.status), + `Expected status to be one of ${validStatuses.join(', ')}, but instead status is ${ + payload.status + }`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + // run unittest execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_parameterized_subtest.NumbersTest.test_even'], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output + assert.ok( + collectedOutput.includes('test_parameterized_subtest.py'), + 'The test string does not contain the correct test name which should be printed', + ); + assert.ok( + collectedOutput.includes('FAILED (failures=1000)'), + 'The test string does not contain the last of the unittest output', + ); + }); + }); + test('pytest execution adapter small workspace with correct output', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathSmallWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathSmallWorkspace}/test_simple.py::test_a`], + TestRunProfileKind.Run, + testRun.object, + pythonExecFactory, + ) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for stdout and stderr as well as pytest output + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output.', + ); + assert.ok( + collectedOutput.includes('Captured log call'), + 'The test string does not contain the expected log section.', + ); + const searchStrings = [ + 'This is a warning message.', + 'This is an error message.', + 'This is a critical message.', + ]; + let searchString: string; + for (searchString of searchStrings) { + const count: number = (collectedOutput.match(new RegExp(searchString, 'g')) || []).length; + assert.strictEqual( + count, + 2, + `The test string does not contain two instances of ${searchString}. Should appear twice from logging output and stack trace`, + ); + } + }); + }); + + test('Unittest execution with coverage, small workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + resultResolver._resolveCoverage = (payload, _token?) => { + assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); + assert.ok(payload.result, 'Expected results to be present'); + const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; + assert.ok(simpleFileCov, 'Expected test_simple.py coverage to be present'); + // since only one test was run, the other test in the same file will have missed coverage lines + assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); + assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathCoverageWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + // run execution + const executionAdapter = new UnittestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + ['test_even.TestNumbers.test_odd'], + TestRunProfileKind.Coverage, + testRun.object, + pythonExecFactory, + ) + .finally(() => { + assert.ok(collectedOutput, 'expect output to be collected'); + }); + }); + test('pytest coverage execution, small workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + resultResolver._resolveCoverage = (payload, _runInstance?) => { + assert.strictEqual(payload.cwd, rootPathCoverageWorkspace, 'Expected cwd to be the workspace folder'); + assert.ok(payload.result, 'Expected results to be present'); + const simpleFileCov = payload.result[`${rootPathCoverageWorkspace}/even.py`]; + assert.ok(simpleFileCov, 'Expected test_simple.py coverage to be present'); + // since only one test was run, the other test in the same file will have missed coverage lines + assert.strictEqual(simpleFileCov.lines_covered.length, 3, 'Expected 1 line to be covered in even.py'); + assert.strictEqual(simpleFileCov.lines_missed.length, 1, 'Expected 3 lines to be missed in even.py'); + assert.strictEqual(simpleFileCov.executed_branches, 1, 'Expected 1 branch to be executed in even.py'); + assert.strictEqual(simpleFileCov.total_branches, 2, 'Expected 2 branches in even.py'); + }; + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathCoverageWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests( + workspaceUri, + [`${rootPathCoverageWorkspace}/test_even.py::TestNumbers::test_odd`], + TestRunProfileKind.Coverage, + testRun.object, + pythonExecFactory, + ) + .then(() => { + assert.ok(collectedOutput, 'expect output to be collected'); + }); + }); + test('pytest execution adapter large workspace', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (payload, _token?) => { + traceLog(`resolveDiscovery ${payload}`); + callCount = callCount + 1; + // the payloads that get to the _resolveExecution are all data and should be successful. + try { + if ('status' in payload) { + assert.strictEqual( + payload.status, + 'success', + `Expected status to be 'success', instead status is ${payload.status}`, + ); + assert.ok(payload.result, 'Expected results to be present'); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathLargeWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // generate list of test_ids + const testIds: string[] = []; + for (let i = 0; i < 2000; i = i + 1) { + const testId = `${rootPathLargeWorkspace}/test_parameterized_subtest.py::test_odd_even[${i}]`; + testIds.push(testId); + } + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + let collectedOutput = ''; + testRun + .setup((t) => t.appendOutput(typeMoq.It.isAny())) + .callback((output: string) => { + collectedOutput += output; + traceLog('appendOutput was called with:', output); + }) + .returns(() => false); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .then(() => { + // verify that the _resolveExecution was called once per test + assert.strictEqual(callCount, 2000, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + + // verify output works for large repo + assert.ok( + collectedOutput.includes('test session starts'), + 'The test string does not contain the expected stdout output from pytest.', + ); + }); + }); + test('unittest discovery adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, unittestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveDiscovery = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`unittest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + assert.notDeepEqual(indexOfTest, -1, 'Expected test to have a null pointer'); + } else { + assert.ok(data.error, "Expected errors in 'error' field"); + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + configService.getSettings(workspaceUri).testing.unittestArgs = ['-s', '.', '-p', '*test*.py']; + + const discoveryAdapter = new UnittestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveDiscovery to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest discovery seg fault error handling', async () => { + // result resolver and saved data for assertions + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveDiscovery = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + callCount = callCount + 1; + traceLog(`add one to call count, is now ${callCount}`); + traceLog(`pytest discovery adapter seg fault error handling \n ${JSON.stringify(data)}`); + try { + if (data.status === 'error') { + if (data.error === undefined) { + // Dereference a NULL pointer + const indexOfTest = JSON.stringify(data).search('Dereference a NULL pointer'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = 'Expected test to have a null pointer'; + } + } else if (data.error.length === 0) { + failureOccurred = true; + failureMsg = "Expected errors in 'error' field"; + } + } else { + const indexOfTest = JSON.stringify(data.tests).search('error'); + if (indexOfTest === -1) { + failureOccurred = true; + failureMsg = + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.'; + } + } + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + // run pytest discovery + const discoveryAdapter = new PytestTestDiscoveryAdapter(configService, resultResolver, envVarsService); + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathDiscoveryErrorWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + await discoveryAdapter.discoverTests(workspaceUri, pythonExecFactory).finally(() => { + // verification after discovery is complete + assert.ok( + callCount >= 1, + `Expected _resolveDiscovery to be called at least once, call count was instead ${callCount}`, + ); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + test('pytest execution adapter seg fault error handling', async () => { + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + let callCount = 0; + let failureOccurred = false; + let failureMsg = ''; + resultResolver.resolveExecution = (data, _token?) => { + // do the following asserts for each time resolveExecution is called, should be called once per test. + console.log(`pytest execution adapter seg fault error handling \n ${JSON.stringify(data)}`); + callCount = callCount + 1; + try { + if ('status' in data) { + if (data.status === 'error') { + assert.ok(data.error, "Expected errors in 'error' field"); + } else { + const indexOfTest = JSON.stringify(data.result).search('error'); + assert.notDeepEqual( + indexOfTest, + -1, + 'If payload status is not error then the individual tests should be marked as errors. This should occur on windows machines.', + ); + } + assert.ok(data.result, 'Expected results to be present'); + } + // make sure the testID is found in the results + const indexOfTest = JSON.stringify(data).search( + 'test_seg_fault.py::TestSegmentationFault::test_segfault', + ); + assert.notDeepEqual(indexOfTest, -1, 'Expected testId to be present'); + } catch (err) { + failureMsg = err ? (err as Error).toString() : ''; + failureOccurred = true; + } + }; + + const testId = `${rootPathErrorWorkspace}/test_seg_fault.py::TestSegmentationFault::test_segfault`; + const testIds: string[] = [testId]; + + // set workspace to test workspace folder + workspaceUri = Uri.parse(rootPathErrorWorkspace); + configService.getSettings(workspaceUri).testing.pytestArgs = []; + + // run pytest execution + const executionAdapter = new PytestTestExecutionAdapter(configService, resultResolver, envVarsService); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + await executionAdapter + .runTests(workspaceUri, testIds, TestRunProfileKind.Run, testRun.object, pythonExecFactory) + .finally(() => { + assert.strictEqual(callCount, 1, 'Expected _resolveExecution to be called once'); + assert.strictEqual(failureOccurred, false, failureMsg); + }); + }); + + test('resolveExecution performance test: validates efficient test result processing', async () => { + // This test validates that resolveExecution processes test results efficiently + // without expensive tree rebuilding or linear searching operations. + // + // The test ensures that processing many test results (like parameterized tests) + // remains fast and doesn't cause performance issues or stack overflow. + + // ================================================================ + // SETUP: Initialize test environment and tracking variables + // ================================================================ + resultResolver = new PythonResultResolver(testController, pytestProvider, workspaceUri); + + // Performance tracking variables + let totalCallTime = 0; + let callCount = 0; + const callTimes: number[] = []; + let treeRebuildCount = 0; + let totalSearchOperations = 0; + + // Test configuration - Moderate scale to validate efficiency + const numTestFiles = 5; // Multiple test files + const testFunctionsPerFile = 10; // Test functions per file + const totalTestItems = numTestFiles * testFunctionsPerFile; // Total test items in mock tree + const numParameterizedResults = 15; // Number of parameterized test results to process + + // ================================================================ + // MOCK: Set up spies and function wrapping to track performance + // ================================================================ + + // Mock getTestCaseNodes to track expensive tree operations + const originalGetTestCaseNodes = require('../../../client/testing/testController/common/testItemUtilities') + .getTestCaseNodes; + const getTestCaseNodesSpy = sinon.stub().callsFake((item) => { + treeRebuildCount++; + const result = originalGetTestCaseNodes(item); + // Track search operations through tree items + // Safely handle undefined results + if (result && Array.isArray(result)) { + totalSearchOperations += result.length; + } + return result || []; // Return empty array if undefined + }); + + // Replace the real function with our spy + const testItemUtilities = require('../../../client/testing/testController/common/testItemUtilities'); + testItemUtilities.getTestCaseNodes = getTestCaseNodesSpy; + + // Stub isTestItemValid to always return true for performance test + // This prevents expensive tree searches during validation + const testItemIndexStub = sinon.stub((resultResolver as any).testItemIndex, 'isTestItemValid').returns(true); + + // Wrap the _resolveExecution function to measure performance + const original_resolveExecution = resultResolver.resolveExecution.bind(resultResolver); + resultResolver.resolveExecution = (payload, runInstance) => { + const startTime = performance.now(); + callCount++; + + // Call the actual implementation + original_resolveExecution(payload, runInstance); + + const endTime = performance.now(); + const callTime = endTime - startTime; + callTimes.push(callTime); + totalCallTime += callTime; + }; + + // ================================================================ + // SETUP: Create test data that simulates realistic test scenarios + // ================================================================ + + // Create a mock TestController with the methods we need + const mockTestController = { + items: new Map(), + createTestItem: (id: string, label: string, uri?: Uri) => { + const childrenMap = new Map(); + // Add forEach method to children map to simulate TestItemCollection + (childrenMap as any).forEach = function (callback: (item: any) => void) { + Map.prototype.forEach.call(this, callback); + }; + + const mockTestItem = { + id, + label, + uri, + children: childrenMap, + parent: undefined, + canResolveChildren: false, + tags: [{ id: 'python-run' }, { id: 'python-debug' }], + }; + return mockTestItem; + }, + // Add a forEach method to simulate the problematic iteration + forEach: function (callback: (item: any) => void) { + this.items.forEach(callback); + }, + }; // Replace the testController in our resolver + (resultResolver as any).testController = mockTestController; + + // Create test controller with many test items (simulates real workspace) + for (let i = 0; i < numTestFiles; i++) { + const testItem = mockTestController.createTestItem( + `test_file_${i}`, + `Test File ${i}`, + Uri.file(`/test_${i}.py`), + ); + mockTestController.items.set(`test_file_${i}`, testItem); + + // Add child test items to each file + for (let j = 0; j < testFunctionsPerFile; j++) { + const childItem = mockTestController.createTestItem( + `test_${i}_${j}`, + `test_method_${j}`, + Uri.file(`/test_${i}.py`), + ); + testItem.children.set(`test_${i}_${j}`, childItem); + + // Set up the ID mappings that the resolver uses + resultResolver.runIdToTestItem.set(`test_${i}_${j}`, childItem as any); + resultResolver.runIdToVSid.set(`test_${i}_${j}`, `test_${i}_${j}`); + resultResolver.vsIdToRunId.set(`test_${i}_${j}`, `test_${i}_${j}`); + } + } // Create payload with multiple test results (simulates real test execution) + const testResults: Record<string, any> = {}; + for (let i = 0; i < numParameterizedResults; i++) { + // Use test IDs that actually exist in our mock setup (test_0_0 through test_0_9) + testResults[`test_0_${i % testFunctionsPerFile}`] = { + test: `test_method[${i}]`, + outcome: 'success', + message: null, + traceback: null, + subtest: null, + }; + } + + const payload: ExecutionTestPayload = { + cwd: '/test', + status: 'success' as const, + error: '', + result: testResults, + }; + + const mockRunInstance = { + passed: sinon.stub(), + failed: sinon.stub(), + errored: sinon.stub(), + skipped: sinon.stub(), + }; + + // ================================================================ + // EXECUTION: Run the performance test + // ================================================================ + + const overallStartTime = performance.now(); + + // Run the resolveExecution function with test data + await resultResolver.resolveExecution(payload, mockRunInstance as any); + + const overallEndTime = performance.now(); + const totalTime = overallEndTime - overallStartTime; + + // ================================================================ + // CLEANUP: Restore original functions + // ================================================================ + testItemUtilities.getTestCaseNodes = originalGetTestCaseNodes; + testItemIndexStub.restore(); + + // ================================================================ + // ASSERT: Verify efficient performance characteristics + // ================================================================ + console.log(`\n=== PERFORMANCE RESULTS ===`); + console.log( + `Test setup: ${numTestFiles} files × ${testFunctionsPerFile} test functions = ${totalTestItems} total items`, + ); + console.log(`Total execution time: ${totalTime.toFixed(2)}ms`); + console.log(`Tree operations performed: ${treeRebuildCount}`); + console.log(`Search operations: ${totalSearchOperations}`); + console.log(`Average time per call: ${(totalCallTime / callCount).toFixed(2)}ms`); + console.log(`Results processed: ${numParameterizedResults}`); + + // Basic function call verification + assert.strictEqual(callCount, 1, 'Expected resolveExecution to be called once'); + + // EFFICIENCY VERIFICATION: Ensure minimal expensive operations + assert.strictEqual( + treeRebuildCount, + 0, + 'Expected ZERO tree rebuilds - efficient implementation should use cached lookups', + ); + + assert.strictEqual( + totalSearchOperations, + 0, + 'Expected ZERO linear search operations - efficient implementation should use direct lookups', + ); + + // Performance threshold verification - should be fast + assert.ok(totalTime < 100, `Function should complete quickly, took ${totalTime}ms (should be under 100ms)`); + + // Scalability check - time should not grow significantly with more results + const timePerResult = totalTime / numParameterizedResults; + assert.ok( + timePerResult < 10, + `Time per result should be minimal: ${timePerResult.toFixed(2)}ms per result (should be under 10ms)`, + ); + }); +}); diff --git a/src/test/testing/configuration.unit.test.ts b/src/test/testing/configuration.unit.test.ts index fd0166403cff..e259587ecccd 100644 --- a/src/test/testing/configuration.unit.test.ts +++ b/src/test/testing/configuration.unit.test.ts @@ -3,26 +3,34 @@ 'use strict'; -// tslint:disable:max-func-body-length no-any - import { expect } from 'chai'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri, WorkspaceConfiguration } from 'vscode'; import { IApplicationShell, ICommandManager, IWorkspaceService } from '../../client/common/application/types'; -import { IConfigurationService, IInstaller, IOutputChannel, IPythonSettings, ITestingSettings, Product } from '../../client/common/types'; +import { + IConfigurationService, + IInstaller, + ILogOutputChannel, + IPythonSettings, + Product, +} from '../../client/common/types'; import { getNamesAndValues } from '../../client/common/utils/enum'; import { IServiceContainer } from '../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL, UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; +import { UNIT_TEST_PRODUCTS } from '../../client/testing/common/constants'; import { TestsHelper } from '../../client/testing/common/testUtils'; -import { TestFlatteningVisitor } from '../../client/testing/common/testVisitors/flatteningVisitor'; -import { ITestsHelper } from '../../client/testing/common/types'; +import { + ITestConfigSettingsService, + ITestConfigurationManager, + ITestConfigurationManagerFactory, + ITestsHelper, +} from '../../client/testing/common/types'; +import { ITestingSettings } from '../../client/testing/configuration/types'; import { UnitTestConfigurationService } from '../../client/testing/configuration'; -import { ITestConfigSettingsService, ITestConfigurationManager, ITestConfigurationManagerFactory } from '../../client/testing/types'; suite('Unit Tests - ConfigurationService', () => { - UNIT_TEST_PRODUCTS.forEach(product => { + UNIT_TEST_PRODUCTS.forEach((product) => { const prods = getNamesAndValues(Product); - const productName = prods.filter(item => item.value === product)[0]; + const productName = prods.filter((item) => item.value === product)[0]; const workspaceUri = Uri.file(__filename); suite(productName.name, () => { let testConfigService: typeMoq.IMock<UnitTestConfigurationService>; @@ -33,50 +41,82 @@ suite('Unit Tests - ConfigurationService', () => { let unitTestSettings: typeMoq.IMock<ITestingSettings>; setup(() => { const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(undefined, typeMoq.MockBehavior.Strict); - const configurationService = typeMoq.Mock.ofType<IConfigurationService>(undefined, typeMoq.MockBehavior.Strict); + const configurationService = typeMoq.Mock.ofType<IConfigurationService>( + undefined, + typeMoq.MockBehavior.Strict, + ); appShell = typeMoq.Mock.ofType<IApplicationShell>(undefined, typeMoq.MockBehavior.Strict); const outputChannel = typeMoq.Mock.ofType<OutputChannel>(undefined, typeMoq.MockBehavior.Strict); const installer = typeMoq.Mock.ofType<IInstaller>(undefined, typeMoq.MockBehavior.Strict); workspaceService = typeMoq.Mock.ofType<IWorkspaceService>(undefined, typeMoq.MockBehavior.Strict); factory = typeMoq.Mock.ofType<ITestConfigurationManagerFactory>(undefined, typeMoq.MockBehavior.Strict); - testSettingsService = typeMoq.Mock.ofType<ITestConfigSettingsService>(undefined, typeMoq.MockBehavior.Strict); + testSettingsService = typeMoq.Mock.ofType<ITestConfigSettingsService>( + undefined, + typeMoq.MockBehavior.Strict, + ); unitTestSettings = typeMoq.Mock.ofType<ITestingSettings>(); const pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(undefined, typeMoq.MockBehavior.Strict); - pythonSettings.setup(p => p.testing).returns(() => unitTestSettings.object); - configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); - - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IWorkspaceService))).returns(() => workspaceService.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigurationManagerFactory))).returns(() => factory.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigSettingsService))).returns(() => testSettingsService.object); + 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<ICommandManager>(undefined, typeMoq.MockBehavior.Strict); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ICommandManager))) + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ICommandManager))) .returns(() => commands.object); - const flattener = typeMoq.Mock.ofType<TestFlatteningVisitor>(undefined, typeMoq.MockBehavior.Strict); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestsHelper))) - .returns(() => new TestsHelper(flattener.object, serviceContainer.object)); - testConfigService = typeMoq.Mock.ofType(UnitTestConfigurationService, typeMoq.MockBehavior.Loose, true, serviceContainer.object); + 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<ITestConfigurationManager>(undefined, typeMoq.MockBehavior.Strict); - configMgr.setup(c => c.enable()) + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict, + ); + configMgr + .setup((c) => c.enable()) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); - factory.setup(f => f.create(workspaceUri, product)) + factory + .setup((f) => f.create(workspaceUri, product)) .returns(() => configMgr.object) .verifiable(typeMoq.Times.once()); - const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) .returns(() => workspaceConfig.object) .verifiable(typeMoq.Times.once()); - workspaceConfig.setup(w => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + workspaceConfig + .setup((w) => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) .returns(() => true) .verifiable(typeMoq.Times.once()); @@ -88,25 +128,38 @@ suite('Unit Tests - ConfigurationService', () => { workspaceConfig.verifyAll(); }); test('Enable Test when setting testing.promptToConfigure is disabled', async () => { - const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(undefined, typeMoq.MockBehavior.Strict); - configMgr.setup(c => c.enable()) + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict, + ); + configMgr + .setup((c) => c.enable()) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); - factory.setup(f => f.create(workspaceUri, product)) + factory + .setup((f) => f.create(workspaceUri, product)) .returns(() => configMgr.object) .verifiable(typeMoq.Times.once()); - const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) .returns(() => workspaceConfig.object) .verifiable(typeMoq.Times.once()); - workspaceConfig.setup(w => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + 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))) + workspaceConfig + .setup((w) => + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)), + ) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); @@ -118,27 +171,40 @@ suite('Unit Tests - ConfigurationService', () => { workspaceConfig.verifyAll(); }); test('Enable Test when setting testing.promptToConfigure is disabled and fail to update the settings', async () => { - const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(undefined, typeMoq.MockBehavior.Strict); - configMgr.setup(c => c.enable()) + const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>( + undefined, + typeMoq.MockBehavior.Strict, + ); + configMgr + .setup((c) => c.enable()) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); - factory.setup(f => f.create(workspaceUri, product)) + factory + .setup((f) => f.create(workspaceUri, product)) .returns(() => configMgr.object) .verifiable(typeMoq.Times.once()); - const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceService + .setup((w) => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) .returns(() => workspaceConfig.object) .verifiable(typeMoq.Times.once()); - workspaceConfig.setup(w => w.get(typeMoq.It.isValue('testing.promptToConfigure'))) + 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))) + workspaceConfig + .setup((w) => + w.update(typeMoq.It.isValue('testing.promptToConfigure'), typeMoq.It.isValue(undefined)), + ) .returns(() => Promise.reject(updateFailError)) .verifiable(typeMoq.Times.once()); @@ -150,10 +216,11 @@ suite('Unit Tests - ConfigurationService', () => { workspaceService.verifyAll(); workspaceConfig.verifyAll(); }); - test('Select Test runner displays 3 items', async () => { + test('Select Test runner displays 2 items', async () => { const placeHolder = 'Some message'; - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) - .callback(items => expect(items).be.lengthOf(3)) + 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); @@ -161,9 +228,10 @@ suite('Unit Tests - ConfigurationService', () => { }); test('Ensure selected item is returned', async () => { const placeHolder = 'Some message'; - const indexes = [Product.unittest, Product.pytest, Product.nosetest]; - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) - .callback(items => expect(items).be.lengthOf(3)) + 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()); @@ -171,9 +239,10 @@ suite('Unit Tests - ConfigurationService', () => { expect(selectedItem).to.be.equal(product); appShell.verifyAll(); }); - test('Ensure undefined is returned when nothing is seleted', async () => { + test('Ensure undefined is returned when nothing is selected', async () => { const placeHolder = 'Some message'; - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) + appShell + .setup((s) => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isObjectWith({ placeHolder }))) .returns(() => Promise.resolve(undefined)) .verifiable(typeMoq.Times.once()); @@ -181,206 +250,60 @@ suite('Unit Tests - ConfigurationService', () => { expect(selectedItem).to.be.equal(undefined, 'invalid value'); appShell.verifyAll(); }); - test('Prompt to enable a test if a test framework is not enabled', async () => { - unitTestSettings.setup(u => u.pytestEnabled).returns(() => false); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== null) { - throw exc; - } - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Prompt to select a test if a test framework is not enabled', async () => { - unitTestSettings.setup(u => u.pytestEnabled).returns(() => false); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - let selectTestRunnerInvoked = false; - try { - testConfigService.callBase = false; - testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(undefined); - }); - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== null) { - throw exc; - } - exceptionThrown = true; - } - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Method not invoked'); - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('Configure selected test framework and disable others', async () => { - unitTestSettings.setup(u => u.pytestEnabled).returns(() => false); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => false); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); - - const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); - workspaceConfig.setup(w => w.get(typeMoq.It.isAny())) - .returns(() => true) - .verifiable(typeMoq.Times.once()); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) - .returns(() => workspaceConfig.object) - .verifiable(typeMoq.Times.once()); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.once()); - - let selectTestRunnerInvoked = false; - testConfigService.callBase = false; - testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(product as any); - }); - - const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(undefined, typeMoq.MockBehavior.Strict); - factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny())) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - configMgr.setup(c => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.displayTestFrameworkError(workspaceUri); - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); - appShell.verifyAll(); - factory.verifyAll(); - configMgr.verifyAll(); - workspaceConfig.verifyAll(); - }); - test('If more than one test framework is enabled, then prompt to select a test framework', async () => { - unitTestSettings.setup(u => u.pytestEnabled).returns(() => true); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => true); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => true); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.never()); - appShell.setup(s => s.showQuickPick(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - let exceptionThrown = false; - try { - await testConfigService.target.displayTestFrameworkError(workspaceUri); - } catch (exc) { - if (exc !== null) { - throw exc; - } - exceptionThrown = true; - } - - expect(exceptionThrown).to.be.equal(true, 'Exception not thrown'); - appShell.verifyAll(); - }); - test('If more than one test framework is enabled, then prompt to select a test framework and enable test, but do not configure', async () => { - unitTestSettings.setup(u => u.pytestEnabled).returns(() => true); - unitTestSettings.setup(u => u.unittestEnabled).returns(() => true); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => true); - - appShell.setup(s => s.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns((_msg, option) => Promise.resolve(option)) - .verifiable(typeMoq.Times.never()); - - let selectTestRunnerInvoked = false; - testConfigService.callBase = false; - testConfigService.setup(t => t.selectTestRunner(typeMoq.It.isAny())) - .returns(() => { - selectTestRunnerInvoked = true; - return Promise.resolve(product as any); - }); - - let enableTestInvoked = false; - testConfigService.setup(t => t.enableTest(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product))) - .returns(() => { - enableTestInvoked = true; - return Promise.resolve(); - }); - - const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(undefined, typeMoq.MockBehavior.Strict); - factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny())) - .returns(() => configMgr.object) - .verifiable(typeMoq.Times.once()); - - configMgr.setup(c => c.configure(typeMoq.It.isValue(workspaceUri))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.never()); - configMgr.setup(c => c.enable()) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - - await testConfigService.target.displayTestFrameworkError(workspaceUri); - - expect(selectTestRunnerInvoked).to.be.equal(true, 'Select Test Runner not invoked'); - expect(enableTestInvoked).to.be.equal(false, 'Enable Test is invoked'); - factory.verifyAll(); - appShell.verifyAll(); - configMgr.verifyAll(); + test('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); - unitTestSettings.setup(u => u.nosetestsEnabled).returns(() => false); - - const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>(undefined, typeMoq.MockBehavior.Strict); - workspaceConfig.setup(w => w.get(typeMoq.It.isAny())) + unitTestSettings.setup((u) => u.pytestEnabled).returns(() => false); + unitTestSettings.setup((u) => u.unittestEnabled).returns(() => false); + + const workspaceConfig = typeMoq.Mock.ofType<WorkspaceConfiguration>( + undefined, + typeMoq.MockBehavior.Strict, + ); + workspaceConfig + .setup((w) => w.get(typeMoq.It.isAny())) .returns(() => true) .verifiable(typeMoq.Times.once()); - workspaceService.setup(w => w.getConfiguration(typeMoq.It.isValue('python'), workspaceUri)) + 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())) + 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())) + testConfigService + .setup((t) => t.selectTestRunner(typeMoq.It.isAny())) .returns(() => { selectTestRunnerInvoked = true; - return Promise.resolve(product as any); + return Promise.resolve(product); }); const configMgr = typeMoq.Mock.ofType<ITestConfigurationManager>(); - factory.setup(f => f.create(typeMoq.It.isValue(workspaceUri), typeMoq.It.isValue(product), typeMoq.It.isAny())) + 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))) + configMgr + .setup((c) => c.configure(typeMoq.It.isValue(workspaceUri))) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); - configMgr.setup(c => c.enable()) + configMgr + .setup((c) => c.enable()) .returns(() => Promise.resolve()) .verifiable(typeMoq.Times.once()); const configManagersToVerify: typeof configMgr[] = [configMgr]; diff --git a/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts new file mode 100644 index 000000000000..d7a1313df591 --- /dev/null +++ b/src/test/testing/configuration/pytestInstallationHelper.unit.test.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { Uri } from 'vscode'; +import * as TypeMoq from 'typemoq'; +import { IApplicationShell } from '../../../client/common/application/types'; +import { PytestInstallationHelper } from '../../../client/testing/configuration/pytestInstallationHelper'; +import * as envExtApi from '../../../client/envExt/api.internal'; + +suite('PytestInstallationHelper', () => { + let appShell: TypeMoq.IMock<IApplicationShell>; + let helper: PytestInstallationHelper; + let useEnvExtensionStub: sinon.SinonStub; + let getEnvExtApiStub: sinon.SinonStub; + let getEnvironmentStub: sinon.SinonStub; + + const workspaceUri = Uri.file('/test/workspace'); + + setup(() => { + appShell = TypeMoq.Mock.ofType<IApplicationShell>(); + helper = new PytestInstallationHelper(appShell.object); + + useEnvExtensionStub = sinon.stub(envExtApi, 'useEnvExtension'); + getEnvExtApiStub = sinon.stub(envExtApi, 'getEnvExtApi'); + getEnvironmentStub = sinon.stub(envExtApi, 'getEnvironment'); + }); + + teardown(() => { + sinon.restore(); + }); + + test('promptToInstallPytest should return false if user selects ignore', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Ignore')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should return false if user cancels', async () => { + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('isEnvExtensionAvailable should return result from useEnvExtension', () => { + useEnvExtensionStub.returns(true); + + const result = helper.isEnvExtensionAvailable(); + + expect(result).to.be.true; + expect(useEnvExtensionStub.calledOnce).to.be.true; + }); + + test('promptToInstallPytest should return false if env extension not available', async () => { + useEnvExtensionStub.returns(false); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.false; + appShell.verifyAll(); + }); + + test('promptToInstallPytest should attempt installation when env extension is available', async () => { + useEnvExtensionStub.returns(true); + + const mockEnvironment = { envId: { id: 'test-env', managerId: 'test-manager' } }; + const mockEnvExtApi = { + managePackages: sinon.stub().resolves(), + }; + + getEnvExtApiStub.resolves(mockEnvExtApi); + getEnvironmentStub.resolves(mockEnvironment); + + appShell + .setup((a) => + a.showInformationMessage( + TypeMoq.It.is((msg: string) => msg.includes('pytest selected but not installed')), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + TypeMoq.It.isAny(), + ), + ) + .returns(() => Promise.resolve('Install pytest')) + .verifiable(TypeMoq.Times.once()); + + const result = await helper.promptToInstallPytest(workspaceUri); + + expect(result).to.be.true; + expect(mockEnvExtApi.managePackages.calledOnceWithExactly(mockEnvironment, { install: ['pytest'] })).to.be.true; + appShell.verifyAll(); + }); +}); diff --git a/src/test/testing/configurationFactory.unit.test.ts b/src/test/testing/configurationFactory.unit.test.ts index b2a0234e9c12..493dfcc00b95 100644 --- a/src/test/testing/configurationFactory.unit.test.ts +++ b/src/test/testing/configurationFactory.unit.test.ts @@ -7,18 +7,14 @@ import { expect, use } from 'chai'; import * as chaiAsPromised from 'chai-as-promised'; import * as typeMoq from 'typemoq'; import { OutputChannel, Uri } from 'vscode'; -import { IInstaller, IOutputChannel, Product } from '../../client/common/types'; +import { IInstaller, ILogOutputChannel, Product } from '../../client/common/types'; import { IServiceContainer } from '../../client/ioc/types'; -import { TEST_OUTPUT_CHANNEL } from '../../client/testing/common/constants'; +import { ITestConfigSettingsService, ITestConfigurationManagerFactory } from '../../client/testing/common/types'; import { TestConfigurationManagerFactory } from '../../client/testing/configurationFactory'; -import * as nose from '../../client/testing/nosetest/testConfigurationManager'; -import * as pytest from '../../client/testing/pytest/testConfigurationManager'; -import { - ITestConfigSettingsService, ITestConfigurationManagerFactory -} from '../../client/testing/types'; -import * as unittest from '../../client/testing/unittest/testConfigurationManager'; +import * as pytest from '../../client/testing/configuration/pytest/testConfigurationManager'; +import * as unittest from '../../client/testing/configuration/unittest/testConfigurationManager'; -use(chaiAsPromised); +use(chaiAsPromised.default); suite('Unit Tests - ConfigurationManagerFactory', () => { let factory: ITestConfigurationManagerFactory; @@ -28,9 +24,11 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { const installer = typeMoq.Mock.ofType<IInstaller>(); const testConfigService = typeMoq.Mock.ofType<ITestConfigSettingsService>(); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IOutputChannel), typeMoq.It.isValue(TEST_OUTPUT_CHANNEL))).returns(() => outputChannel.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestConfigSettingsService))).returns(() => testConfigService.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(ILogOutputChannel))).returns(() => outputChannel.object); + serviceContainer.setup((c) => c.get(typeMoq.It.isValue(IInstaller))).returns(() => installer.object); + serviceContainer + .setup((c) => c.get(typeMoq.It.isValue(ITestConfigSettingsService))) + .returns(() => testConfigService.object); factory = new TestConfigurationManagerFactory(serviceContainer.object); }); test('Create Unit Test Configuration', async () => { @@ -41,8 +39,4 @@ suite('Unit Tests - ConfigurationManagerFactory', () => { const configMgr = factory.create(Uri.file(__filename), Product.pytest); expect(configMgr).to.be.instanceOf(pytest.ConfigurationManager); }); - test('Create nose Configuration', async () => { - const configMgr = factory.create(Uri.file(__filename), Product.nosetest); - expect(configMgr).to.be.instanceOf(nose.ConfigurationManager); - }); }); diff --git a/src/test/testing/debugger.test.ts b/src/test/testing/debugger.test.ts deleted file mode 100644 index 4fd81d90f6ab..000000000000 --- a/src/test/testing/debugger.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { assert, expect, use } from 'chai'; -import * as chaiAsPromised from 'chai-as-promised'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import { ConfigurationTarget } from 'vscode'; -import { createDeferred } from '../../client/common/utils/async'; -import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { CondaService } from '../../client/interpreter/locators/services/condaService'; -import { TestManagerRunner as NoseTestManagerRunner } from '../../client/testing//nosetest/runner'; -import { TestManagerRunner as PytestManagerRunner } from '../../client/testing//pytest/runner'; -import { TestManagerRunner as UnitTestTestManagerRunner } from '../../client/testing//unittest/runner'; -import { ArgumentsHelper } from '../../client/testing/common/argumentsHelper'; -import { CANCELLATION_REASON, CommandSource, NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/testing/common/constants'; -import { TestRunner } from '../../client/testing/common/runner'; -import { ITestDebugLauncher, ITestManagerFactory, ITestMessageService, ITestRunner, IXUnitParser, TestProvider } from '../../client/testing/common/types'; -import { XUnitParser } from '../../client/testing/common/xUnitParser'; -import { ArgumentsService as NoseTestArgumentsService } from '../../client/testing/nosetest/services/argsService'; -import { ArgumentsService as PyTestArgumentsService } from '../../client/testing/pytest/services/argsService'; -import { TestMessageService } from '../../client/testing/pytest/services/testMessageService'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../client/testing/types'; -import { UnitTestHelper } from '../../client/testing/unittest/helper'; -import { ArgumentsService as UnitTestArgumentsService } from '../../client/testing/unittest/services/argsService'; -import { deleteDirectory, rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } 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('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - setup(async () => { - await deleteDirectory(path.join(testFilesPath, '.cache')); - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - - ioc.registerTestParsers(); - ioc.registerTestVisitors(); - ioc.registerTestDiscoveryServices(); - ioc.registerTestDiagnosticServices(); - ioc.registerTestResultsHelper(); - ioc.registerTestStorage(); - ioc.registerTestsHelper(); - ioc.registerTestManagers(); - ioc.registerMockUnitTestSocketServer(); - ioc.serviceManager.add<IArgumentsHelper>(IArgumentsHelper, ArgumentsHelper); - ioc.serviceManager.add<ITestRunner>(ITestRunner, TestRunner); - ioc.serviceManager.add<IXUnitParser>(IXUnitParser, XUnitParser); - ioc.serviceManager.add<IUnitTestHelper>(IUnitTestHelper, UnitTestHelper); - ioc.serviceManager.add<IArgumentsService>(IArgumentsService, NoseTestArgumentsService, NOSETEST_PROVIDER); - ioc.serviceManager.add<IArgumentsService>(IArgumentsService, PyTestArgumentsService, PYTEST_PROVIDER); - ioc.serviceManager.add<IArgumentsService>(IArgumentsService, UnitTestArgumentsService, UNITTEST_PROVIDER); - ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, PytestManagerRunner, PYTEST_PROVIDER); - ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, NoseTestManagerRunner, NOSETEST_PROVIDER); - ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, UnitTestTestManagerRunner, UNITTEST_PROVIDER); - ioc.serviceManager.addSingleton<ITestDebugLauncher>(ITestDebugLauncher, MockDebugLauncher); - ioc.serviceManager.addSingleton<ITestMessageService>(ITestMessageService, TestMessageService, PYTEST_PROVIDER); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - - async function testStartingDebugger(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - const mockDebugLauncher = ioc.serviceContainer.get<MockDebugLauncher>(ITestDebugLauncher); - const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - - const deferred = createDeferred<string>(); - const testFunction = [tests.testFunctions[0].testFunction]; - const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); - - // This promise should never resolve nor reject. - runningPromise - .then(() => deferred.reject('Debugger stopped when it shouldn\'t have')) - .catch(error => deferred.reject(error)); - - mockDebugLauncher.launched - .then((launched) => { - if (launched) { - deferred.resolve(''); - } else { - deferred.reject('Debugger not launched'); - } - }).catch(error => deferred.reject(error)); - - await deferred.promise; - } - - test('Debugger should start (unittest)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await testStartingDebugger('unittest'); - }); - - test('Debugger should start (pytest)', async () => { - await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await testStartingDebugger('pytest'); - }); - - test('Debugger should start (nosetest)', async () => { - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await testStartingDebugger('nosetest'); - }); - - async function testStoppingDebugger(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - const mockDebugLauncher = ioc.serviceContainer.get<MockDebugLauncher>(ITestDebugLauncher); - const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - - const testFunction = [tests.testFunctions[0].testFunction]; - const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); - const launched = await mockDebugLauncher.launched; - assert.isTrue(launched, 'Debugger not launched'); - - const discoveryPromise = testManager.discoverTests(CommandSource.commandPalette, true, true, true); - await expect(runningPromise).to.be.rejectedWith(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); - await ioc.dispose(); // will cancel test discovery - await expect(discoveryPromise).to.be.rejectedWith(CANCELLATION_REASON, 'Incorrect reason for ending the debugger'); - } - - test('Debugger should stop when user invokes a test discovery (unittest)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await testStoppingDebugger('unittest'); - }); - - test('Debugger should stop when user invokes a test discovery (pytest)', async () => { - await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await testStoppingDebugger('pytest'); - }); - - test('Debugger should stop when user invokes a test discovery (nosetest)', async () => { - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await testStoppingDebugger('nosetest'); - }); - - async function testDebuggerWhenRediscoveringTests(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - const mockDebugLauncher = ioc.serviceContainer.get<MockDebugLauncher>(ITestDebugLauncher); - const tests = await testManager.discoverTests(CommandSource.commandPalette, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - - const testFunction = [tests.testFunctions[0].testFunction]; - const runningPromise = testManager.runTest(CommandSource.commandPalette, { testFunction }, false, true); - const launched = await mockDebugLauncher.launched; - assert.isTrue(launched, 'Debugger not launched'); - - const discoveryPromise = testManager.discoverTests(CommandSource.commandPalette, false, true); - const deferred = createDeferred<string>(); - - discoveryPromise - // tslint:disable-next-line:no-unsafe-any - .then(() => deferred.resolve('')) - // tslint:disable-next-line:no-unsafe-any - .catch(ex => deferred.reject(ex)); - - // This promise should never resolve nor reject. - runningPromise - .then(() => 'Debugger stopped when it shouldn\'t have') - .catch(() => 'Debugger crashed when it shouldn\'t have') - // tslint:disable-next-line: no-floating-promises - .then(error => { - deferred.reject(error); - }); - - // Should complete without any errors - await deferred.promise; - } - - test('Debugger should not stop when test discovery is invoked automatically by extension (unittest)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await testDebuggerWhenRediscoveringTests('unittest'); - }); - - test('Debugger should not stop when test discovery is invoked automatically by extension (pytest)', async () => { - await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await testDebuggerWhenRediscoveringTests('pytest'); - }); - - test('Debugger should not stop when test discovery is invoked automatically by extension (nosetest)', async () => { - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await testDebuggerWhenRediscoveringTests('nosetest'); - }); -}); diff --git a/src/test/testing/display/main.unit.test.ts b/src/test/testing/display/main.unit.test.ts deleted file mode 100644 index 20a1448cf3f6..000000000000 --- a/src/test/testing/display/main.unit.test.ts +++ /dev/null @@ -1,408 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length no-any - -import { expect } from 'chai'; -import * as typeMoq from 'typemoq'; -import { StatusBarItem, Uri } from 'vscode'; -import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import '../../../client/common/extensions'; -import { IConfigurationService, IPythonSettings, ITestingSettings } from '../../../client/common/types'; -import { createDeferred } from '../../../client/common/utils/async'; -import { Testing } from '../../../client/common/utils/localize'; -import { noop } from '../../../client/common/utils/misc'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { CANCELLATION_REASON } from '../../../client/testing/common/constants'; -import { ITestsHelper, Tests } from '../../../client/testing/common/types'; -import { TestResultDisplay } from '../../../client/testing/display/main'; -import { sleep } from '../../core'; - -suite('Unit Tests - TestResultDisplay', () => { - const workspaceUri = Uri.file(__filename); - let appShell: typeMoq.IMock<IApplicationShell>; - let unitTestSettings: typeMoq.IMock<ITestingSettings>; - let serviceContainer: typeMoq.IMock<IServiceContainer>; - let display: TestResultDisplay; - let testsHelper: typeMoq.IMock<ITestsHelper>; - let configurationService: typeMoq.IMock<IConfigurationService>; - let cmdManager: typeMoq.IMock<ICommandManager>; - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - configurationService = typeMoq.Mock.ofType<IConfigurationService>(); - appShell = typeMoq.Mock.ofType<IApplicationShell>(); - unitTestSettings = typeMoq.Mock.ofType<ITestingSettings>(); - const pythonSettings = typeMoq.Mock.ofType<IPythonSettings>(); - testsHelper = typeMoq.Mock.ofType<ITestsHelper>(); - cmdManager = typeMoq.Mock.ofType<ICommandManager>(); - - pythonSettings.setup(p => p.testing).returns(() => unitTestSettings.object); - configurationService.setup(c => c.getSettings(workspaceUri)).returns(() => pythonSettings.object); - - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IConfigurationService))).returns(() => configurationService.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(IApplicationShell))).returns(() => appShell.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ITestsHelper))).returns(() => testsHelper.object); - serviceContainer.setup(c => c.get(typeMoq.It.isValue(ICommandManager))).returns(() => cmdManager.object); - }); - teardown(() => { - try { - display.dispose(); - } catch { noop(); } - }); - function createTestResultDisplay() { - display = new TestResultDisplay(serviceContainer.object); - } - test('Should create a status bar item upon instantiation', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - appShell.verifyAll(); - }); - test('Should be disabled upon instantiation', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - appShell.verifyAll(); - expect(display.enabled).to.be.equal(false, 'not disabled'); - }); - test('Enable display should show the statusbar', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - display.enabled = true; - statusBar.verifyAll(); - }); - test('Disable display should hide the statusbar', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.hide()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - display.enabled = false; - statusBar.verifyAll(); - }); - test('Ensure status bar is displayed and updated with progress with ability to stop tests', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - display.displayProgressStatus(createDeferred<Tests>().promise, false); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); - }); - test('Ensure status bar is updated with success with ability to view ui without any results', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayProgressStatus(def.promise, false); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); - - const tests = typeMoq.Mock.ofType<Tests>(); - tests.setup((t: any) => t.then).returns(() => undefined); - tests.setup(t => t.summary).returns(() => { - return { errors: 0, failures: 0, passed: 0, skipped: 0 }; - }).verifiable(typeMoq.Times.atLeastOnce()); - - appShell.setup(a => a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - def.resolve(tests.object); - await sleep(1); - - tests.verifyAll(); - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - }); - test('Ensure status bar is updated with success with ability to view ui with results', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayProgressStatus(def.promise, false); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); - - const tests = typeMoq.Mock.ofType<Tests>(); - tests.setup((t: any) => t.then).returns(() => undefined); - tests.setup(t => t.summary).returns(() => { - return { errors: 0, failures: 0, passed: 1, skipped: 0 }; - }).verifiable(typeMoq.Times.atLeastOnce()); - - appShell.setup(a => a.showWarningMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.never()); - - def.resolve(tests.object); - await sleep(1); - - tests.verifyAll(); - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - }); - test('Ensure status bar is updated with error when cancelled by user with ability to view ui with results', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayProgressStatus(def.promise, false); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); - - testsHelper.setup(t => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.never()); - - def.reject(CANCELLATION_REASON); - await sleep(1); - - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - testsHelper.verifyAll(); - }); - test('Ensure status bar is updated, and error message display with error in running tests, with ability to view ui with results', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayProgressStatus(def.promise, false); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Test), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Running Tests'), typeMoq.Times.atLeastOnce()); - - testsHelper.setup(t => t.displayTestErrorMessage(typeMoq.It.isAny())).verifiable(typeMoq.Times.once()); - - def.reject('Some other reason'); - await sleep(1); - - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - testsHelper.verifyAll(); - }); - - test('Ensure status bar is displayed and updated with progress with ability to stop test discovery', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - display.displayDiscoverStatus(createDeferred<Tests>().promise, false).ignoreErrors(); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); - }); - test('Ensure status bar is displayed and updated with success and no tests, with ability to view ui to view results of test discovery', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayDiscoverStatus(def.promise, false).ignoreErrors(); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); - - const tests = typeMoq.Mock.ofType<Tests>(); - appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(undefined)) - .verifiable(typeMoq.Times.once()); - - def.resolve(undefined as any); - await sleep(1); - - tests.verifyAll(); - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - }); - test('Ensure tests are disabled when there are errors and user choses to disable tests', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - cmdManager.setup(c => c.executeCommand(typeMoq.It.isValue('setContext'), typeMoq.It.isValue('testsDiscovered'), typeMoq.It.isValue(false))) - .verifiable(typeMoq.Times.once()); - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayDiscoverStatus(def.promise, false).ignoreErrors(); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); - - const tests = typeMoq.Mock.ofType<Tests>(); - appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(Testing.disableTests())) - .verifiable(typeMoq.Times.once()); - - for (const setting of ['testing.promptToConfigure', 'testing.pytestEnabled', - 'testing.unittestEnabled', 'testing.nosetestsEnabled']) { - configurationService.setup(c => c.updateSetting(typeMoq.It.isValue(setting), typeMoq.It.isValue(false))) - .returns(() => Promise.resolve()) - .verifiable(typeMoq.Times.once()); - } - def.resolve(undefined as any); - await sleep(1); - - tests.verifyAll(); - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - configurationService.verifyAll(); - cmdManager.verifyAll(); - }); - test('Ensure corresponding command is executed when there are errors and user choses to configure test framework', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayDiscoverStatus(def.promise, false).ignoreErrors(); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); - - const tests = typeMoq.Mock.ofType<Tests>(); - appShell.setup(a => a.showInformationMessage(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => Promise.resolve(Testing.configureTests())) - .verifiable(typeMoq.Times.once()); - - const undefinedArg = typeMoq.It.isValue(undefined); - cmdManager - .setup(c => c.executeCommand(typeMoq.It.isValue(Commands.Tests_Configure as any), undefinedArg, undefinedArg, undefinedArg)) - .returns(() => Promise.resolve() as any) - .verifiable(typeMoq.Times.once()); - def.resolve(undefined as any); - await sleep(1); - - tests.verifyAll(); - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_View_UI), typeMoq.Times.atLeastOnce()); - cmdManager.verifyAll(); - }); - test('Ensure status bar is displayed and updated with error info when test discovery is cancelled by the user', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayDiscoverStatus(def.promise, false).ignoreErrors(); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); - - appShell.setup(a => a.showErrorMessage(typeMoq.It.isAny())) - .verifiable(typeMoq.Times.never()); - - def.reject(CANCELLATION_REASON); - await sleep(1); - - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Discover), typeMoq.Times.atLeastOnce()); - configurationService.verifyAll(); - }); - test('Ensure status bar is displayed and updated with error info, and message is displayed when test discovery is fails due to errors', async () => { - const statusBar = typeMoq.Mock.ofType<StatusBarItem>(); - appShell.setup(a => a.createStatusBarItem(typeMoq.It.isAny())) - .returns(() => statusBar.object) - .verifiable(typeMoq.Times.once()); - - statusBar.setup(s => s.show()).verifiable(typeMoq.Times.once()); - - createTestResultDisplay(); - const def = createDeferred<Tests>(); - - display.displayDiscoverStatus(def.promise, false).ignoreErrors(); - - statusBar.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Ask_To_Stop_Discovery), typeMoq.Times.atLeastOnce()); - statusBar.verify(s => s.text = typeMoq.It.isValue('$(stop) Discovering Tests'), typeMoq.Times.atLeastOnce()); - - appShell.setup(a => a.showErrorMessage(typeMoq.It.isAny())) - .verifiable(typeMoq.Times.once()); - - def.reject('some weird error'); - await sleep(1); - - appShell.verifyAll(); - statusBar.verify(s => s.command = typeMoq.It.isValue(Commands.Tests_Discover), typeMoq.Times.atLeastOnce()); - configurationService.verifyAll(); - }); -}); diff --git a/src/test/testing/display/picker.unit.test.ts b/src/test/testing/display/picker.unit.test.ts deleted file mode 100644 index b59fe4a18bb6..000000000000 --- a/src/test/testing/display/picker.unit.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { anything, instance, mock, verify } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { Commands } from '../../../client/common/constants'; -import { getNamesAndValues } from '../../../client/common/utils/enum'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { TestsToRun } from '../../../client/testing/common/types'; -import { onItemSelected, Type } from '../../../client/testing/display/picker'; - -// tslint:disable:no-any - -suite('Unit Tests - Picker (execution of commands)', () => { - getNamesAndValues<Type>(Type).forEach(item => { - getNamesAndValues<CommandSource>(Type).forEach(commandSource => { - [true, false].forEach(debug => { - test(`Invoking command for selection ${item.name} from ${commandSource.name} (${debug ? 'Debug' : 'No debug'})`, async () => { - const commandManager = mock(CommandManager); - const workspaceUri = Uri.file(__filename); - - const testFunction = 'some test Function'; - const selection = { type: item.value, fn: { testFunction } }; - onItemSelected(instance(commandManager), commandSource.value, workspaceUri, selection as any, debug); - - switch (selection.type) { - case Type.Null: { - verify(commandManager.executeCommand(anything())).never(); - const args: any[] = []; - for (let i = 0; i <= 7; i += 1) { - args.push(anything()); - } - verify(commandManager.executeCommand(anything(), ...args)).never(); - return; - } - case Type.RunAll: { - verify(commandManager.executeCommand(Commands.Tests_Run, undefined, commandSource.value, workspaceUri, undefined)).once(); - return; - } - case Type.ReDiscover: { - verify(commandManager.executeCommand(Commands.Tests_Discover, undefined, commandSource.value, workspaceUri)).once(); - return; - } - case Type.ViewTestOutput: { - verify(commandManager.executeCommand(Commands.Tests_ViewOutput, undefined, commandSource.value)).once(); - return; - } - case Type.RunFailed: { - verify(commandManager.executeCommand(Commands.Tests_Run_Failed, undefined, commandSource.value, workspaceUri)).once(); - return; - } - case Type.SelectAndRunMethod: { - const cmd = debug ? Commands.Tests_Select_And_Debug_Method : Commands.Tests_Select_And_Run_Method; - verify(commandManager.executeCommand(cmd, undefined, commandSource.value, workspaceUri)).once(); - return; - } - case Type.RunMethod: { - const testsToRun: TestsToRun = { testFunction: ['something' as any] }; - verify(commandManager.executeCommand(Commands.Tests_Run, undefined, commandSource.value, workspaceUri, testsToRun)).never(); - return; - } - case Type.DebugMethod: { - const testsToRun: TestsToRun = { testFunction: ['something' as any] }; - verify(commandManager.executeCommand(Commands.Tests_Debug, undefined, commandSource.value, workspaceUri, testsToRun)).never(); - return; - } - case Type.Configure: { - verify(commandManager.executeCommand(Commands.Tests_Configure, undefined, commandSource.value, workspaceUri)).once(); - return; - } - default: { - return; - } - } - }); - }); - }); - }); -}); diff --git a/src/test/testing/explorer/explorerTestData.ts b/src/test/testing/explorer/explorerTestData.ts deleted file mode 100644 index 7811012fbd2d..000000000000 --- a/src/test/testing/explorer/explorerTestData.ts +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -/** - * Test utilities for testing the TestViewTreeProvider class. - */ - -import { join, parse as path_parse } from 'path'; -import * as tsmockito from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Uri, WorkspaceFolder } from 'vscode'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { - IApplicationShell, ICommandManager, IWorkspaceService -} from '../../../client/common/application/types'; -import { - IDisposable, IDisposableRegistry -} from '../../../client/common/types'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { TestsHelper } from '../../../client/testing/common/testUtils'; -import { - TestFlatteningVisitor -} from '../../../client/testing/common/testVisitors/flatteningVisitor'; -import { - ITestCollectionStorageService, TestFile, - TestFolder, TestFunction, Tests, TestSuite -} from '../../../client/testing/common/types'; -import { - TestTreeViewProvider -} from '../../../client/testing/explorer/testTreeViewProvider'; -import { ITestManagementService } from '../../../client/testing/types'; - -/** - * Disposable class that doesn't do anything, help for event-registration against - * ITestManagementService. - */ -export class ExplorerTestsDisposable implements IDisposable { - // tslint:disable-next-line:no-empty - public dispose() { } -} - -export function getMockTestFolder(folderPath: string, testFiles: TestFile[] = []): TestFolder { - - // tslint:disable-next-line:no-unnecessary-local-variable - const folder: TestFolder = { - resource: Uri.file(__filename), - folders: [], - name: folderPath, - nameToRun: folderPath, - testFiles: testFiles, - time: 0 - }; - - return folder; -} - -export function getMockTestFile(filePath: string, testSuites: TestSuite[] = [], testFunctions: TestFunction[] = []): TestFile { - - // tslint:disable-next-line:no-unnecessary-local-variable - const testFile: TestFile = { - resource: Uri.file(__filename), - name: (path_parse(filePath)).base, - nameToRun: filePath, - time: 0, - fullPath: join(__dirname, filePath), - functions: testFunctions, - suites: testSuites, - xmlName: filePath.replace(/\//g, '.') - }; - - return testFile; -} - -export function getMockTestSuite( - suiteNameToRun: string, - testFunctions: TestFunction[] = [], - subSuites: TestSuite[] = [], - instance: boolean = true, - unitTest: boolean = true -): TestSuite { - const suiteNameChunks = suiteNameToRun.split('::'); - const suiteName = suiteNameChunks[suiteNameChunks.length - 1]; - - // tslint:disable-next-line:no-unnecessary-local-variable - const testSuite: TestSuite = { - resource: Uri.file(__filename), - functions: testFunctions, - isInstance: instance, - isUnitTest: unitTest, - name: suiteName, - nameToRun: suiteNameToRun, - suites: subSuites, - time: 0, - xmlName: suiteNameToRun.replace(/\//g, '.').replace(/\:\:/g, ':') - }; - return testSuite; -} - -export function getMockTestFunction(fnNameToRun: string): TestFunction { - - const fnNameChunks = fnNameToRun.split('::'); - const fnName = fnNameChunks[fnNameChunks.length - 1]; - - // tslint:disable-next-line:no-unnecessary-local-variable - const fn: TestFunction = { - resource: Uri.file(__filename), - name: fnName, - nameToRun: fnNameToRun, - time: 0 - }; - - return fn; -} - -/** - * Return a basic hierarchy of test data items for use in testing. - * - * @returns Array containing the items broken out from the hierarchy (all items are linked to one another) - */ -export function getTestExplorerViewItemData(): [TestFolder, TestFile, TestFunction, TestSuite, TestFunction] { - - let testFolder: TestFolder; - let testFile: TestFile; - let testSuite: TestSuite; - let testFunction: TestFunction; - let testSuiteFunction: TestFunction; - - testSuiteFunction = getMockTestFunction('workspace/test_folder/test_file.py::test_suite::test_suite_function'); - testSuite = getMockTestSuite('workspace/test_folder/test_file.py::test_suite', [testSuiteFunction]); - testFunction = getMockTestFunction('workspace/test_folder/test_file.py::test_function'); - testFile = getMockTestFile('workspace/test_folder/test_file.py', [testSuite], [testFunction]); - testFolder = getMockTestFolder('workspace/test_folder', [testFile]); - - return [testFolder, testFile, testFunction, testSuite, testSuiteFunction]; -} - -/** - * Return an instance of `TestsHelper` that can be used in a unit test scenario. - * - * @returns An instance of `TestsHelper` class with mocked AppShell & ICommandManager members. - */ -export function getTestHelperInstance(): TestsHelper { - - const appShellMoq = typemoq.Mock.ofType<IApplicationShell>(); - const commMgrMoq = typemoq.Mock.ofType<ICommandManager>(); - const serviceContainerMoq = typemoq.Mock.ofType<IServiceContainer>(); - - serviceContainerMoq.setup(a => a.get(typemoq.It.isValue(IApplicationShell), typemoq.It.isAny())) - .returns(() => appShellMoq.object); - serviceContainerMoq.setup(a => a.get(typemoq.It.isValue(ICommandManager), typemoq.It.isAny())) - .returns(() => commMgrMoq.object); - - return new TestsHelper(new TestFlatteningVisitor(), serviceContainerMoq.object); -} - -/** - * Creates mock `Tests` data suitable for testing the TestTreeViewProvider with. - */ -export function createMockTestsData(testData?: TestFile[]): Tests { - if (testData === undefined) { - let testFile: TestFile; - - [, testFile] = getTestExplorerViewItemData(); - - testData = [testFile]; - } - - const testHelper = getTestHelperInstance(); - return testHelper.flattenTestFiles(testData, __dirname); -} - -export function createMockTestStorageService(testData?: Tests): typemoq.IMock<ITestCollectionStorageService> { - const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); - - if (!testData) { - testData = createMockTestsData(); - } - - testStoreMoq.setup(t => t.getTests(typemoq.It.isAny())).returns(() => testData); - - return testStoreMoq; -} - -/** - * Create an ITestManagementService that will work for the TeestTreeViewProvider in a unit test scenario. - * - * Provider an 'onDidStatusChange' hook that can be called, but that does nothing. - */ -export function createMockUnitTestMgmtService(): typemoq.IMock<ITestManagementService> { - const unitTestMgmtSrvMoq = typemoq.Mock.ofType<ITestManagementService>(); - unitTestMgmtSrvMoq.setup(u => u.onDidStatusChange(typemoq.It.isAny())) - .returns(() => new ExplorerTestsDisposable()); - return unitTestMgmtSrvMoq; -} - -/** - * Create an IWorkspaceService mock that will work with the TestTreeViewProvider class. - * - * @param workspaceFolderPath Optional, the path to use as the current Resource-path for - * the tests within the TestTree. - */ -export function createMockWorkspaceService(): typemoq.IMock<IWorkspaceService> { - const workspcSrvMoq = typemoq.Mock.ofType<IWorkspaceService>(); - class ExplorerTestsWorkspaceFolder implements WorkspaceFolder { - public get uri(): Uri { - return Uri.parse(''); - } - public get name(): string { - return (path_parse(this.uri.fsPath)).base; - } - public get index(): number { - return 0; - } - } - workspcSrvMoq.setup(w => w.workspaceFolders) - .returns(() => [new ExplorerTestsWorkspaceFolder()]); - return workspcSrvMoq; -} - -/** - * Create a testable mocked up version of the TestExplorerViewProvider. Creates any - * mocked dependencies not provided in the parameters. - * - * @param testStore Test storage service, provides access to the Tests structure that the view is built from. - * @param unitTestMgmtService Unit test management service that provides the 'onTestStatusUpdated' event. - * @param workspaceService Workspace service used to determine the current workspace that the test view is showing. - * @param disposableReg Disposable registry used to dispose of items in the view. - */ -export function createMockTestExplorer( - testStore?: ITestCollectionStorageService, - testsData?: Tests, - unitTestMgmtService?: ITestManagementService, - workspaceService?: IWorkspaceService, - commandManager?: ICommandManager -): TestTreeViewProvider { - - if (!testStore) { - testStore = createMockTestStorageService(testsData).object; - } - - if (!unitTestMgmtService) { - unitTestMgmtService = createMockUnitTestMgmtService().object; - } - - if (!workspaceService) { - workspaceService = createMockWorkspaceService().object; - } - if (!commandManager) { - commandManager = tsmockito.instance(tsmockito.mock(CommandManager)); - } - - const dispRegMoq = typemoq.Mock.ofType<IDisposableRegistry>(); - dispRegMoq.setup(d => d.push(typemoq.It.isAny())); - - return new TestTreeViewProvider( - testStore, unitTestMgmtService, workspaceService, commandManager, - dispRegMoq.object - ); -} diff --git a/src/test/testing/explorer/failedTestHandler.unit.test.ts b/src/test/testing/explorer/failedTestHandler.unit.test.ts deleted file mode 100644 index 23fdde2d0ec8..000000000000 --- a/src/test/testing/explorer/failedTestHandler.unit.test.ts +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; -import { ITestCollectionStorageService, TestFile, TestFolder, TestFunction, TestStatus, TestSuite } from '../../../client/testing/common/types'; -import { FailedTestHandler } from '../../../client/testing/explorer/failedTestHandler'; -import { noop, sleep } from '../../core'; - -// tslint:disable:no-any - -suite('Unit Tests Test Explorer View Items', () => { - let failedTestHandler: FailedTestHandler; - let commandManager: ICommandManager; - let testStorageService: ITestCollectionStorageService; - setup(() => { - commandManager = mock(CommandManager); - testStorageService = mock(TestCollectionStorageService); - failedTestHandler = new FailedTestHandler([], instance(commandManager), instance(testStorageService)); - }); - - test('Activation will add command handlers (without a resource)', async () => { - when(testStorageService.onDidChange).thenReturn(noop as any); - - await failedTestHandler.activate(undefined); - - verify(testStorageService.onDidChange).once(); - }); - test('Activation will add command handlers (with a resource)', async () => { - when(testStorageService.onDidChange).thenReturn(noop as any); - - await failedTestHandler.activate(Uri.file(__filename)); - - verify(testStorageService.onDidChange).once(); - }); - test('Change handler will invoke the command to reveal the nodes (for failed and errored items)', async () => { - const uri = Uri.file(__filename); - const failedFunc1: TestFunction = { name: 'fn1', time: 0, resource: uri, nameToRun: 'fn1', status: TestStatus.Error }; - const failedFunc2: TestFunction = { name: 'fn2', time: 0, resource: uri, nameToRun: 'fn2', status: TestStatus.Fail }; - when(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).thenResolve(); - - failedTestHandler.onDidChangeTestData({ uri, data: failedFunc1 }); - failedTestHandler.onDidChangeTestData({ uri, data: failedFunc2 }); - - // wait for debouncing to take effect. - await sleep(1); - - verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).times(2); - verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, failedFunc1)).once(); - verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, failedFunc2)).once(); - }); - test('Change handler will not invoke the command to reveal the nodes (for failed and errored suites, files & folders)', async () => { - const uri = Uri.file(__filename); - const failedSuite: TestSuite = { - name: 'suite1', time: 0, resource: uri, nameToRun: 'suite1', - functions: [], isInstance: false, isUnitTest: false, suites: [], xmlName: 'suite1', - status: TestStatus.Error - }; - const failedFile: TestFile = { - name: 'suite1', time: 0, resource: uri, nameToRun: 'file', - functions: [], suites: [], xmlName: 'file', status: TestStatus.Error, - fullPath: '' - }; - const failedFolder: TestFolder = { - name: 'suite1', time: 0, resource: uri, nameToRun: 'file', - testFiles: [], folders: [], status: TestStatus.Error - }; - when(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).thenResolve(); - - failedTestHandler.onDidChangeTestData({ uri, data: failedSuite }); - failedTestHandler.onDidChangeTestData({ uri, data: failedFile }); - failedTestHandler.onDidChangeTestData({ uri, data: failedFolder }); - - // wait for debouncing to take effect. - await sleep(1); - - verify(commandManager.executeCommand(Commands.Test_Reveal_Test_Item, anything())).never(); - }); -}); diff --git a/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts b/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts deleted file mode 100644 index b551e0cf4d64..000000000000 --- a/src/test/testing/explorer/testExplorerCommandHandler.unit.test.ts +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { IDisposable } from '@phosphor/disposable'; -import { - anything, capture, deepEqual, - instance, mock, verify, when -} from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Uri } from 'vscode'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { - TestFile, TestFunction, - TestsToRun, TestSuite -} from '../../../client/testing/common/types'; -import { TestExplorerCommandHandler } from '../../../client/testing/explorer/commandHandlers'; -import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; -import { ITestExplorerCommandHandler } from '../../../client/testing/navigation/types'; -import { ITestDataItemResource } from '../../../client/testing/types'; - -// tslint:disable:no-any max-func-body-length -suite('Unit Tests - Test Explorer Command Handler', () => { - let commandHandler: ITestExplorerCommandHandler; - let cmdManager: ICommandManager; - let testResourceMapper: ITestDataItemResource; - - setup(() => { - cmdManager = mock(CommandManager); - testResourceMapper = mock(TestTreeViewProvider); - commandHandler = new TestExplorerCommandHandler(instance(cmdManager), instance(testResourceMapper)); - }); - test('Commands are registered', () => { - commandHandler.register(); - - verify(cmdManager.registerCommand(Commands.runTestNode, anything(), commandHandler)).once(); - verify(cmdManager.registerCommand(Commands.debugTestNode, anything(), commandHandler)).once(); - verify(cmdManager.registerCommand(Commands.openTestNodeInEditor, anything(), commandHandler)).once(); - }); - test('Handlers are disposed', () => { - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - const disposable3 = typemoq.Mock.ofType<IDisposable>(); - - when(cmdManager.registerCommand(Commands.runTestNode, anything(), commandHandler)).thenReturn(disposable1.object); - when(cmdManager.registerCommand(Commands.debugTestNode, anything(), commandHandler)).thenReturn(disposable2.object); - when(cmdManager.registerCommand(Commands.openTestNodeInEditor, anything(), commandHandler)).thenReturn(disposable3.object); - - commandHandler.register(); - commandHandler.dispose(); - - disposable1.verify(d => d.dispose(), typemoq.Times.once()); - disposable2.verify(d => d.dispose(), typemoq.Times.once()); - disposable3.verify(d => d.dispose(), typemoq.Times.once()); - }); - async function testOpeningTestNode(data: TestFile | TestSuite | TestFunction, expectedCommand: 'navigateToTestFunction' | 'navigateToTestSuite' | 'navigateToTestFile') { - const resource = Uri.file(__filename); - when(testResourceMapper.getResource(data)).thenReturn(resource); - - commandHandler.register(); - - const handler = capture(cmdManager.registerCommand as any).last()[1] as any as Function; - await handler.bind(commandHandler)(data); - - verify(cmdManager.executeCommand(expectedCommand, resource, data, true)).once(); - } - test('Opening a file will invoke correct command', async () => { - const testFilePath = 'some file path'; - const data: TestFile = { fullPath: testFilePath } as any; - await testOpeningTestNode(data, Commands.navigateToTestFile); - }); - test('Opening a test suite will invoke correct command', async () => { - const data: TestSuite = { suites: [] } as any; - await testOpeningTestNode(data, Commands.navigateToTestSuite); - }); - test('Opening a test function will invoke correct command', async () => { - const data: TestFunction = { name: 'hello' } as any; - await testOpeningTestNode(data, Commands.navigateToTestFunction); - }); - async function testRunOrDebugTestNode(data: TestFile | TestSuite | TestFunction, - expectedTestRun: TestsToRun, runType: 'run' | 'debug') { - const resource = Uri.file(__filename); - when(testResourceMapper.getResource(data)).thenReturn(resource); - - commandHandler.register(); - - const capturedCommand = capture(cmdManager.registerCommand as any); - const handler = (runType === 'run' ? capturedCommand.first()[1] : capturedCommand.second()[1]) as any as Function; - await handler.bind(commandHandler)(data); - - const cmd = runType === 'run' ? Commands.Tests_Run : Commands.Tests_Debug; - verify(cmdManager.executeCommand(cmd, undefined, CommandSource.testExplorer, resource, deepEqual(expectedTestRun))).once(); - } - test('Running a file will invoke correct command', async () => { - const testFilePath = 'some file path'; - const data: TestFile = { fullPath: testFilePath } as any; - await testRunOrDebugTestNode(data, { testFile: [data] }, 'run'); - }); - test('Running a suite will invoke correct command', async () => { - const data: TestSuite = { suites: [] } as any; - await testRunOrDebugTestNode(data, { testSuite: [data] }, 'run'); - }); - test('Running a function will invoke correct command', async () => { - const data: TestSuite = { suites: [] } as any; - await testRunOrDebugTestNode(data, { testSuite: [data] }, 'run'); - }); - test('Debugging a file will invoke correct command', async () => { - const testFilePath = 'some file path'; - const data: TestFile = { fullPath: testFilePath } as any; - await testRunOrDebugTestNode(data, { testFile: [data] }, 'debug'); - }); - test('Debugging a suite will invoke correct command', async () => { - const data: TestSuite = { suites: [] } as any; - await testRunOrDebugTestNode(data, { testSuite: [data] }, 'debug'); - }); - test('Debugging a function will invoke correct command', async () => { - const data: TestSuite = { suites: [] } as any; - await testRunOrDebugTestNode(data, { testSuite: [data] }, 'debug'); - }); -}); diff --git a/src/test/testing/explorer/testTreeViewItem.unit.test.ts b/src/test/testing/explorer/testTreeViewItem.unit.test.ts deleted file mode 100644 index 569abf9e1f1b..000000000000 --- a/src/test/testing/explorer/testTreeViewItem.unit.test.ts +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import { Uri } from 'vscode'; -import { - Commands -} from '../../../client/common/constants'; -import { - TestFile, TestFolder, - TestFunction, TestSuite, TestType -} from '../../../client/testing/common/types'; -import { - TestTreeItem -} from '../../../client/testing/explorer/testTreeViewItem'; -import { - createMockTestDataItem, createSubtestParent -} from '../common/testUtils.unit.test'; -import { getTestExplorerViewItemData } from './explorerTestData'; - -suite('Unit Tests Test Explorer View Items', () => { - let testFolder: TestFolder; - let testFile: TestFile; - let testSuite: TestSuite; - let testFunction: TestFunction; - let testSuiteFunction: TestFunction; - const resource = Uri.file(__filename); - setup(() => { - [testFolder, testFile, testFunction, testSuite, testSuiteFunction] = getTestExplorerViewItemData(); - }); - - test('Test root folder created into test view item', () => { - const viewItem = new TestTreeItem(resource, testFolder); - expect(viewItem.contextValue).is.equal('testFolder'); - }); - - test('Test file created into test view item', () => { - const viewItem = new TestTreeItem(resource, testFile); - expect(viewItem.contextValue).is.equal('testFile'); - }); - - test('Test suite created into test view item', () => { - const viewItem = new TestTreeItem(resource, testSuite); - expect(viewItem.contextValue).is.equal('testSuite'); - }); - - test('Test function created into test view item', () => { - const viewItem = new TestTreeItem(resource, testFunction); - expect(viewItem.contextValue).is.equal('testFunction'); - }); - - test('Test suite function created into test view item', () => { - const viewItem = new TestTreeItem(resource, testSuiteFunction); - expect(viewItem.contextValue).is.equal('testFunction'); - }); - - test('Test subtest parent created into test view item', () => { - const subtestParent = createSubtestParent([ - createMockTestDataItem<TestFunction>(TestType.testFunction, 'test_x'), - createMockTestDataItem<TestFunction>(TestType.testFunction, 'test_y') - ]); - - const viewItem = new TestTreeItem(resource, subtestParent.asSuite); - - expect(viewItem.contextValue).is.equal('testSuite'); - expect(viewItem.command!.command).is.equal(Commands.navigateToTestFunction); - }); - - test('Test subtest created into test view item', () => { - createSubtestParent([testFunction]); // sets testFunction.subtestParent - - const viewItem = new TestTreeItem(resource, testFunction); - - expect(viewItem.contextValue).is.equal('testFunction'); - }); -}); diff --git a/src/test/testing/explorer/testTreeViewProvider.unit.test.ts b/src/test/testing/explorer/testTreeViewProvider.unit.test.ts deleted file mode 100644 index b797186c06d5..000000000000 --- a/src/test/testing/explorer/testTreeViewProvider.unit.test.ts +++ /dev/null @@ -1,890 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { TreeItemCollapsibleState, Uri } from 'vscode'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { Commands } from '../../../client/common/constants'; -import { IDisposable } from '../../../client/common/types'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; -import { getTestType } from '../../../client/testing/common/testUtils'; -import { - ITestCollectionStorageService, TestFile, TestFolder, Tests, TestStatus, TestType -} from '../../../client/testing/common/types'; -import { TestTreeItem } from '../../../client/testing/explorer/testTreeViewItem'; -import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; -import { UnitTestManagementService } from '../../../client/testing/main'; -import { TestDataItem, TestWorkspaceFolder } from '../../../client/testing/types'; -import { noop } from '../../core'; -import { - createMockTestExplorer as createMockTestTreeProvider, createMockTestsData, - getMockTestFile, getMockTestFolder, getMockTestFunction, getMockTestSuite -} from './explorerTestData'; - -// tslint:disable:no-any - -/** - * Class that is useful to track any Tree View update requests made by the view provider. - */ -class TestExplorerCaptureRefresh implements IDisposable { - public refreshCount: number = 0; // this counts the number of times 'onDidChangeTreeData' is emitted. - - private disposable: IDisposable; - - constructor(private testViewProvider: TestTreeViewProvider, disposableContainer: IDisposable[]) { - this.disposable = this.testViewProvider.onDidChangeTreeData(this.onRefreshOcured.bind(this)); - disposableContainer.push(this); - } - - public dispose() { - this.disposable.dispose(); - } - - private onRefreshOcured(_testDataItem?: TestDataItem): void { - this.refreshCount = this.refreshCount + 1; - } -} - -// tslint:disable:max-func-body-length -suite('Unit Tests Test Explorer TestTreeViewProvider', () => { - suite('Misc', () => { - const testResource: Uri = Uri.parse('anything'); - let disposables: IDisposable[] = []; - - teardown(() => { - disposables.forEach((disposableItem: IDisposable) => { - disposableItem.dispose(); - }); - disposables = []; - }); - - test('Create the initial view and ensure it provides a default view', async () => { - const testTreeProvider = createMockTestTreeProvider(); - expect(testTreeProvider).is.not.equal(undefined, 'Could not create a mock test explorer, check the parameters of the test setup.'); - const treeRoot = await testTreeProvider.getChildren(); - expect(treeRoot.length).to.be.greaterThan(0, 'No children returned from default view of the TreeViewProvider.'); - }); - - test('Ensure that updates from the test manager propagate to the TestExplorer', async () => { - const testsData = createMockTestsData(); - const workspaceService = mock(WorkspaceService); - const testStore = mock(TestCollectionStorageService); - const workspaceFolder = { uri: Uri.file(''), name: 'root', index: 0 }; - when(workspaceService.getWorkspaceFolder(testResource)).thenReturn(workspaceFolder); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(noop as any); - when(testStore.getTests(testResource)).thenReturn(testsData); - when(testStore.onDidChange).thenReturn(noop as any); - const changeItem = testsData.testFolders[1].testFiles[0].functions[0]; - const testTreeProvider = createMockTestTreeProvider(instance(testStore), testsData, undefined, instance(workspaceService)); - const refreshCap = new TestExplorerCaptureRefresh(testTreeProvider, disposables); - - testTreeProvider.refresh(testResource); - const originalTreeItem = await testTreeProvider.getTreeItem(changeItem) as TestTreeItem; - const origStatus = originalTreeItem.testStatus; - - changeItem.status = TestStatus.Fail; - testTreeProvider.refresh(testResource); - const changedTreeItem = await testTreeProvider.getTreeItem(changeItem) as TestTreeItem; - const updatedStatus = changedTreeItem.testStatus; - - expect(origStatus).to.not.equal(updatedStatus); - expect(refreshCap.refreshCount).to.equal(2); - }); - - test('When the test data is updated, the update event is emitted', () => { - const testsData = createMockTestsData(); - const workspaceService = mock(WorkspaceService); - const testStore = mock(TestCollectionStorageService); - const workspaceFolder = { uri: Uri.file(''), name: 'root', index: 0 }; - when(workspaceService.getWorkspaceFolder(testResource)).thenReturn(workspaceFolder); - when(workspaceService.onDidChangeWorkspaceFolders).thenReturn(noop as any); - when(testStore.getTests(testResource)).thenReturn(testsData); - when(testStore.onDidChange).thenReturn(noop as any); - const testView = createMockTestTreeProvider(instance(testStore), testsData, undefined, instance(workspaceService)); - - const refreshCap = new TestExplorerCaptureRefresh(testView, disposables); - testView.refresh(testResource); - - expect(refreshCap.refreshCount).to.be.equal(1); - }); - - test('A test file is added/removed/renamed', async () => { - // create an inital test tree with a single file. - const fn = getMockTestFunction('test/test_fl.py::test_fn1'); - const fl1 = getMockTestFile('test/test_fl.py', [], [fn]); - const originalTestData = createMockTestsData([fl1]); - - // create an updated test tree, similar to the first, but with a new file - const origName = 'test_fl2'; - const afn = getMockTestFunction(`test/${origName}.py::test_2fn1`); - const fl2 = getMockTestFile(`test/${origName}.py`, [], [afn]); - const updatedTestData = createMockTestsData([fl1, fl2]); - - let testData = originalTestData; - const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); - testStoreMoq.setup(a => a.getTests(typemoq.It.isAny())).returns(() => testData); - - const testTreeProvider = createMockTestTreeProvider(testStoreMoq.object); - - testTreeProvider.refresh(testResource); - let unchangedItem = await testTreeProvider.getTreeItem(fl1); - expect(unchangedItem).to.not.be.equal(undefined, 'The file that will always be present, is not present.'); - - testData = updatedTestData; - testTreeProvider.refresh(testResource); - - unchangedItem = await testTreeProvider.getTreeItem(fl1); - expect(unchangedItem).to.not.be.equal(undefined, 'The file that will always be present, is not present.'); - let addedTreeItem = await testTreeProvider.getTreeItem(fl2) as TestTreeItem; - expect(addedTreeItem).to.not.be.equal(undefined, 'The file has been added to the tests tree but not found?'); - expect(addedTreeItem.data.name).to.be.equal(`${origName}.py`); - - // change the name of the added file... - const newName = 'test_file_two'; - afn.name = afn.name.replace(origName, newName); - afn.nameToRun = afn.nameToRun.replace(origName, newName); - fl2.name = fl2.name.replace(origName, newName); - fl2.fullPath = fl2.fullPath.replace(origName, newName); - fl2.nameToRun = fl2.nameToRun.replace(origName, newName); - fl2.xmlName = fl2.xmlName.replace(origName, newName); - - testTreeProvider.refresh(testResource); - - unchangedItem = await testTreeProvider.getTreeItem(fl1); - expect(unchangedItem).to.not.be.equal(undefined, 'The file that will always be present, is not present.'); - addedTreeItem = await testTreeProvider.getTreeItem(fl2) as TestTreeItem; - expect(addedTreeItem).to.not.be.equal(undefined, 'The file has been updated in the tests tree but in tree view?'); - expect(addedTreeItem.data.name).to.be.equal(`${newName}.py`); - }); - - test('A test suite is added/removed/renamed', async () => { - // create an inital test tree with a single file containing a single suite. - const sfn = getMockTestFunction('test/test_fl.py::suite1::test_fn'); - const suite = getMockTestSuite('test/test_fl.py::suite1', [sfn]); - const fl1 = getMockTestFile('test/test_fl.py', [suite]); - const originalTestData = createMockTestsData([fl1]); - - // create an updated test tree, similar to the first, but with a new file - const origName = 'suite2'; - const sfn2 = getMockTestFunction(`test/test_fl.py::${origName}::test_fn`); - const suite2 = getMockTestSuite(`test/test_fl.py::${origName}`, [sfn2]); - const fl1_update = getMockTestFile('test/test_fl.py', [suite, suite2]); - const updatedTestData = createMockTestsData([fl1_update]); - - let testData = originalTestData; - const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); - testStoreMoq.setup(a => a.getTests(typemoq.It.isAny())).returns(() => testData); - - const testTreeProvider = createMockTestTreeProvider(testStoreMoq.object); - - testTreeProvider.refresh(testResource); - let unchangedItem = await testTreeProvider.getTreeItem(suite); - expect(unchangedItem).to.not.be.equal(undefined, 'The suite that will always be present, is not present.'); - - testData = updatedTestData; - testTreeProvider.refresh(testResource); - - unchangedItem = await testTreeProvider.getTreeItem(suite); - expect(unchangedItem).to.not.be.equal(undefined, 'The suite that will always be present, is not present.'); - let addedTreeItem = await testTreeProvider.getTreeItem(suite2) as TestTreeItem; - expect(addedTreeItem).to.not.be.equal(undefined, 'The suite has been added to the tests tree but not found?'); - - const newName = 'suite_two'; - suite2.name = suite2.name.replace(origName, newName); - suite2.nameToRun = suite2.nameToRun.replace(origName, newName); - suite2.xmlName = suite2.xmlName.replace(origName, newName); - - testTreeProvider.refresh(testResource); - - unchangedItem = await testTreeProvider.getTreeItem(suite); - expect(unchangedItem).to.not.be.equal(undefined, 'The suite that will always be present, is not present.'); - addedTreeItem = await testTreeProvider.getTreeItem(suite2) as TestTreeItem; - expect(addedTreeItem).to.not.be.equal(undefined, 'The suite has been updated in the tests tree but in tree view?'); - expect(addedTreeItem.data.name).to.be.equal(newName); - }); - - test('A test function is added/removed/renamed', async () => { - // create an inital test tree with a single file containing a single suite. - const fn = getMockTestFunction('test/test_fl.py::test_fn'); - const fl1 = getMockTestFile('test/test_fl.py', [], [fn]); - const originalTestData = createMockTestsData([fl1]); - - // create an updated test tree, similar to the first, but with a new function - const origName = 'test_fn2'; - const fn2 = getMockTestFunction(`test/test_fl.py::${origName}`); - const fl1_update = getMockTestFile('test/test_fl.py', [], [fn, fn2]); - const updatedTestData = createMockTestsData([fl1_update]); - - let testData = originalTestData; - const testStoreMoq = typemoq.Mock.ofType<ITestCollectionStorageService>(); - testStoreMoq.setup(a => a.getTests(typemoq.It.isAny())).returns(() => testData); - - const testTreeProvider = createMockTestTreeProvider(testStoreMoq.object); - - testTreeProvider.refresh(testResource); - let unchangedItem = await testTreeProvider.getTreeItem(fn); - expect(unchangedItem).to.not.be.equal(undefined, 'The function that will always be present, is not present.'); - - testData = updatedTestData; - testTreeProvider.refresh(testResource); - - unchangedItem = await testTreeProvider.getTreeItem(fn); - expect(unchangedItem).to.not.be.equal(undefined, 'The function that will always be present, is not present.'); - let addedTreeItem = await testTreeProvider.getTreeItem(fn2) as TestTreeItem; - expect(addedTreeItem).to.not.be.equal(undefined, 'The function has been added to the tests tree but not found?'); - expect(addedTreeItem.data.name).to.be.equal('test_fn2'); - - const newName = 'test_func_two'; - fn2.name = fn2.name.replace(origName, newName); - fn2.nameToRun = fn2.nameToRun.replace(origName, newName); - - testTreeProvider.refresh(testResource); - - unchangedItem = await testTreeProvider.getTreeItem(fn); - expect(unchangedItem).to.not.be.equal(undefined, 'The function that will always be present, is not present.'); - addedTreeItem = await testTreeProvider.getTreeItem(fn2) as TestTreeItem; - expect(addedTreeItem).to.not.be.equal(undefined, 'The function has been updated in the tests tree but in tree view?'); - expect(addedTreeItem.data.name).to.be.equal(newName); - }); - - test('A test status changes and is reflected in the tree view', async () => { - // create a single file with a single function - const testFunction = getMockTestFunction('test/test_file.py::test_fn'); - testFunction.status = TestStatus.Pass; - const testFile = getMockTestFile('test/test_file.py', [], [testFunction]); - const testData = createMockTestsData([testFile]); - - const testTreeProvider = createMockTestTreeProvider(undefined, testData); - - // test's initial state is success - testTreeProvider.refresh(testResource); - const treeItem = await testTreeProvider.getTreeItem(testFunction) as TestTreeItem; - expect(treeItem.testStatus).to.be.equal(TestStatus.Pass); - - // test's next state is fail - testFunction.status = TestStatus.Fail; - testTreeProvider.refresh(testResource); - let updatedTreeItem = await testTreeProvider.getTreeItem(testFunction) as TestTreeItem; - expect(updatedTreeItem.testStatus).to.be.equal(TestStatus.Fail); - - // test's next state is skip - testFunction.status = TestStatus.Skipped; - testTreeProvider.refresh(testResource); - updatedTreeItem = await testTreeProvider.getTreeItem(testFunction) as TestTreeItem; - expect(updatedTreeItem.testStatus).to.be.equal(TestStatus.Skipped); - }); - - test('Get parent is working for each item type', async () => { - // create a single folder/file/suite/test setup - const testFunction = getMockTestFunction('test/test_file.py::test_suite::test_fn'); - const testSuite = getMockTestSuite('test/test_file.py::test_suite', [testFunction]); - const outerTestFunction = getMockTestFunction('test/test_file.py::test_outer_fn'); - const testFile = getMockTestFile('test/test_file.py', [testSuite], [outerTestFunction]); - const testData = createMockTestsData([testFile]); - - const testTreeProvider = createMockTestTreeProvider(undefined, testData); - - // build up the view item tree - testTreeProvider.refresh(testResource); - - let parent = (await testTreeProvider.getParent(testFunction))!; - expect(parent.name).to.be.equal(testSuite.name, 'Function within a test suite not returning the suite as parent.'); - let parentType = getTestType(parent); - expect(parentType).to.be.equal(TestType.testSuite); - - parent = (await testTreeProvider.getParent(testSuite))!; - expect(parent.name).to.be.equal(testFile.name, 'Suite within a test file not returning the test file as parent.'); - parentType = getTestType(parent); - expect(parentType).to.be.equal(TestType.testFile); - - parent = (await testTreeProvider.getParent(outerTestFunction))!; - expect(parent.name).to.be.equal(testFile.name, 'Function within a test file not returning the test file as parent.'); - parentType = getTestType(parent); - expect(parentType).to.be.equal(TestType.testFile); - - parent = (await testTreeProvider.getParent(testFile))!; - parentType = getTestType(parent!); - expect(parentType).to.be.equal(TestType.testFolder); - }); - - test('Get children is working for each item type', async () => { - // create a single folder/file/suite/test setup - const testFunction = getMockTestFunction('test/test_file.py::test_suite::test_fn'); - const testSuite = getMockTestSuite('test/test_file.py::test_suite', [testFunction]); - const outerTestFunction = getMockTestFunction('test/test_file.py::test_outer_fn'); - const testFile = getMockTestFile('test/test_file.py', [testSuite], [outerTestFunction]); - const testData = createMockTestsData([testFile]); - - const testTreeProvider = createMockTestTreeProvider(undefined, testData); - - // build up the view item tree - testTreeProvider.refresh(testResource); - - let children = await testTreeProvider.getChildren(testFunction); - expect(children.length).to.be.equal(0, 'A function should never have children.'); - - children = await testTreeProvider.getChildren(testSuite); - expect(children.length).to.be.equal(1, 'Suite a single function should only return one child.'); - children.forEach((child: TestDataItem) => { - expect(child.name).oneOf(['test_fn']); - expect(getTestType(child)).to.be.equal(TestType.testFunction); - }); - - children = await testTreeProvider.getChildren(outerTestFunction); - expect(children.length).to.be.equal(0, 'A function should never have children.'); - - children = await testTreeProvider.getChildren(testFile); - expect(children.length).to.be.equal(2, 'A file with one suite and one function should have a total of 2 children.'); - children.forEach((child: TestDataItem) => { - expect(child.name).oneOf(['test_suite', 'test_outer_fn']); - }); - }); - - test('Tree items for subtests are correct', async () => { - const resource = Uri.file(__filename); - // Set up the folder & file. - const folder = getMockTestFolder('tests'); - const file = getMockTestFile(`${folder.name}/test_file.py`); - folder.testFiles.push(file); - // Set up the file-level tests. - const func1 = getMockTestFunction(`${file.name}::test_spam`); - file.functions.push(func1); - const func2 = getMockTestFunction(`${file.name}::test_ham[1-2]`); - func2.subtestParent = { - name: 'test_ham', - nameToRun: `${file.name}::test_ham`, - asSuite: { - resource: resource, - name: 'test_ham', - nameToRun: `${file.name}::test_ham`, - functions: [func2], - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: 'test_ham', - time: 0 - }, - time: 0 - }; - file.functions.push(func2); - const func3 = getMockTestFunction(`${file.name}::test_ham[3-4]`); - func3.subtestParent = func2.subtestParent; - func3.subtestParent.asSuite.functions.push(func3); - file.functions.push(func3); - // Set up the suite. - const suite = getMockTestSuite(`${file.name}::MyTests`); - file.suites.push(suite); - const func4 = getMockTestFunction('MyTests::test_foo'); - suite.functions.push(func4); - const func5 = getMockTestFunction('MyTests::test_bar[2-3]'); - func5.subtestParent = { - name: 'test_bar', - nameToRun: `${file.name}::MyTests::test_bar`, - asSuite: { - resource: resource, - name: 'test_bar', - nameToRun: `${file.name}::MyTests::test_bar`, - functions: [func5], - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: 'test_bar', - time: 0 - }, - time: 0 - }; - suite.functions.push(func5); - // Set up the tests data. - const testData = createMockTestsData([file]); - - const testExplorer = createMockTestTreeProvider(undefined, testData); - const items = [ - await testExplorer.getTreeItem(func1), - await testExplorer.getTreeItem(func2), - await testExplorer.getTreeItem(func3), - await testExplorer.getTreeItem(func4), - await testExplorer.getTreeItem(func5), - await testExplorer.getTreeItem(file), - await testExplorer.getTreeItem(suite), - await testExplorer.getTreeItem(func2.subtestParent.asSuite), - await testExplorer.getTreeItem(func5.subtestParent.asSuite) - ]; - - expect(items).to.deep.equal([ - new TestTreeItem(func1.resource, func1), - new TestTreeItem(func2.resource, func2), - new TestTreeItem(func3.resource, func3), - new TestTreeItem(func4.resource, func4), - new TestTreeItem(func5.resource, func5), - new TestTreeItem(file.resource, file), - new TestTreeItem(suite.resource, suite), - new TestTreeItem(resource, func2.subtestParent.asSuite), - new TestTreeItem(resource, func5.subtestParent.asSuite) - ]); - }); - - test('Parents for subtests are correct', async () => { - const resource = Uri.file(__filename); - // Set up the folder & file. - const folder = getMockTestFolder('tests'); - const file = getMockTestFile(`${folder.name}/test_file.py`); - folder.testFiles.push(file); - // Set up the file-level tests. - const func1 = getMockTestFunction(`${file.name}::test_spam`); - file.functions.push(func1); - const func2 = getMockTestFunction(`${file.name}::test_ham[1-2]`); - func2.subtestParent = { - name: 'test_ham', - nameToRun: `${file.name}::test_ham`, - asSuite: { - resource: resource, - name: 'test_ham', - nameToRun: `${file.name}::test_ham`, - functions: [func2], - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: 'test_ham', - time: 0 - }, - time: 0 - }; - file.functions.push(func2); - const func3 = getMockTestFunction(`${file.name}::test_ham[3-4]`); - func3.subtestParent = func2.subtestParent; - func3.subtestParent.asSuite.functions.push(func3); - file.functions.push(func3); - // Set up the suite. - const suite = getMockTestSuite(`${file.name}::MyTests`); - file.suites.push(suite); - const func4 = getMockTestFunction('MyTests::test_foo'); - suite.functions.push(func4); - const func5 = getMockTestFunction('MyTests::test_bar[2-3]'); - func5.subtestParent = { - name: 'test_bar', - nameToRun: `${file.name}::MyTests::test_bar`, - asSuite: { - resource: resource, - name: 'test_bar', - nameToRun: `${file.name}::MyTests::test_bar`, - functions: [func5], - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: 'test_bar', - time: 0 - }, - time: 0 - }; - suite.functions.push(func5); - // Set up the tests data. - const testData = createMockTestsData([file]); - - const testExplorer = createMockTestTreeProvider(undefined, testData); - const parents = [ - await testExplorer.getParent(func1), - await testExplorer.getParent(func2), - await testExplorer.getParent(func3), - await testExplorer.getParent(func4), - await testExplorer.getParent(func5), - await testExplorer.getParent(suite), - await testExplorer.getParent(func2.subtestParent.asSuite), - await testExplorer.getParent(func3.subtestParent.asSuite), - await testExplorer.getParent(func5.subtestParent.asSuite) - ]; - - expect(parents).to.deep.equal([ - file, - func2.subtestParent.asSuite, - func3.subtestParent.asSuite, - suite, - func5.subtestParent.asSuite, - file, - file, - file, - suite - ]); - }); - test('Children for subtests are correct', async () => { - const resource = Uri.file(__filename); - // Set up the folder & file. - const folder = getMockTestFolder('tests'); - const file = getMockTestFile(`${folder.name}/test_file.py`); - folder.testFiles.push(file); - // Set up the file-level tests. - const func1 = getMockTestFunction(`${file.name}::test_spam`); - file.functions.push(func1); - const func2 = getMockTestFunction(`${file.name}::test_ham[1-2]`); - func2.subtestParent = { - name: 'test_ham', - nameToRun: `${file.name}::test_ham`, - asSuite: { - resource: resource, - name: 'test_ham', - nameToRun: `${file.name}::test_ham`, - functions: [func2], - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: 'test_ham', - time: 0 - }, - time: 0 - }; - file.functions.push(func2); - const func3 = getMockTestFunction(`${file.name}::test_ham[3-4]`); - func3.subtestParent = func2.subtestParent; - func3.subtestParent.asSuite.functions.push(func3); - file.functions.push(func3); - // Set up the suite. - const suite = getMockTestSuite(`${file.name}::MyTests`); - file.suites.push(suite); - const func4 = getMockTestFunction('MyTests::test_foo'); - suite.functions.push(func4); - const func5 = getMockTestFunction('MyTests::test_bar[2-3]'); - func5.subtestParent = { - name: 'test_bar', - nameToRun: `${file.name}::MyTests::test_bar`, - asSuite: { - resource: resource, - name: 'test_bar', - nameToRun: `${file.name}::MyTests::test_bar`, - functions: [func5], - suites: [], - isUnitTest: false, - isInstance: false, - xmlName: 'test_bar', - time: 0 - }, - time: 0 - }; - suite.functions.push(func5); - // Set up the tests data. - const testData = createMockTestsData([file]); - - const testExplorer = createMockTestTreeProvider(undefined, testData); - const childrens = [ - await testExplorer.getChildren(func1), - await testExplorer.getChildren(func2), - await testExplorer.getChildren(func3), - await testExplorer.getChildren(func4), - await testExplorer.getChildren(func5), - await testExplorer.getChildren(file), - await testExplorer.getChildren(suite), - await testExplorer.getChildren(func2.subtestParent.asSuite), - await testExplorer.getChildren(func3.subtestParent.asSuite), - await testExplorer.getChildren(func5.subtestParent.asSuite) - ]; - - expect(childrens).to.deep.equal([ - [], - [], - [], - [], - [], - [func1, suite, func2.subtestParent.asSuite], - [func4, func5.subtestParent.asSuite], - [func2, func3], - [func2, func3], - [func5] - ]); - test('Get children will discover only once', async () => { - const commandManager = mock(CommandManager); - const testStore = mock(TestCollectionStorageService); - const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); - when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); - when(testStore.onDidChange).thenReturn(noop as any); - - const testTreeProvider = createMockTestTreeProvider(instance(testStore), undefined, undefined, undefined, instance(commandManager)); - - let tests = await testTreeProvider.getChildren(testWorkspaceFolder); - - expect(tests).to.be.lengthOf(0); - verify(commandManager.executeCommand(Commands.Tests_Discover, testWorkspaceFolder, CommandSource.testExplorer, undefined)).once(); - - tests = await testTreeProvider.getChildren(testWorkspaceFolder); - expect(tests).to.be.lengthOf(0); - verify(commandManager.executeCommand(Commands.Tests_Discover, testWorkspaceFolder, CommandSource.testExplorer, undefined)).once(); - }); - }); - test('Expand tree item if it does not have any parent', async () => { - const commandManager = mock(CommandManager); - const testStore = mock(TestCollectionStorageService); - const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); - when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); - when(testStore.onDidChange).thenReturn(noop as any); - const testTreeProvider = createMockTestTreeProvider(instance(testStore), undefined, undefined, undefined, instance(commandManager)); - - // No parent - testTreeProvider.getParent = () => Promise.resolve(undefined); - - const element: TestFile = { - fullPath: __filename, - functions: [], - suites: [], - name: 'name', - time: 0, - resource: Uri.file(__filename), - xmlName: '', - nameToRun: '' - }; - - const node = await testTreeProvider.getTreeItem(element); - - expect(node.collapsibleState).to.equal(TreeItemCollapsibleState.Expanded); - }); - test('Expand tree item if the parent is the Workspace Folder in a multiroot scenario', async () => { - const commandManager = mock(CommandManager); - const testStore = mock(TestCollectionStorageService); - const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); - when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); - when(testStore.onDidChange).thenReturn(noop as any); - const testTreeProvider = createMockTestTreeProvider(instance(testStore), undefined, undefined, undefined, instance(commandManager)); - - // Has a workspace folder as parent. - const parentFolder = new TestWorkspaceFolder({ name: '', index: 0, uri: Uri.file(__filename) }); - - testTreeProvider.getParent = () => Promise.resolve(parentFolder); - - const element: TestFile = { - fullPath: __filename, - functions: [], - suites: [], - name: 'name', - time: 0, - resource: Uri.file(__filename), - xmlName: '', - nameToRun: '' - }; - - const node = await testTreeProvider.getTreeItem(element); - - expect(node.collapsibleState).to.equal(TreeItemCollapsibleState.Expanded); - }); - test('Do not expand tree item if it does not have any parent', async () => { - const commandManager = mock(CommandManager); - const testStore = mock(TestCollectionStorageService); - const testWorkspaceFolder = new TestWorkspaceFolder({ uri: Uri.file(__filename), name: '', index: 0 }); - when(testStore.getTests(testWorkspaceFolder.workspaceFolder.uri)).thenReturn(); - when(testStore.onDidChange).thenReturn(noop as any); - const testTreeProvider = createMockTestTreeProvider(instance(testStore), undefined, undefined, undefined, instance(commandManager)); - - // Has a parent folder - const parentFolder: TestFolder = { - name: '', - nameToRun: '', - resource: Uri.file(__filename), - time: 0, - testFiles: [], - folders: [] - }; - - testTreeProvider.getParent = () => Promise.resolve(parentFolder); - - const element: TestFile = { - fullPath: __filename, - functions: [], - suites: [], - name: 'name', - time: 0, - resource: Uri.file(__filename), - xmlName: '', - nameToRun: '' - }; - - const node = await testTreeProvider.getTreeItem(element); - - expect(node.collapsibleState).to.not.equal(TreeItemCollapsibleState.Expanded); - }); - }); - suite('Root Nodes', () => { - let treeProvider: TestTreeViewProvider; - setup(() => { - const store = mock(TestCollectionStorageService); - const managementService = mock(UnitTestManagementService); - when(managementService.onDidStatusChange).thenReturn(noop as any); - when(store.onDidChange).thenReturn(noop as any); - const workspace = mock(WorkspaceService); - when(workspace.onDidChangeWorkspaceFolders).thenReturn(noop as any); - const commandManager = mock(CommandManager); - treeProvider = new TestTreeViewProvider(instance(store), instance(managementService), - instance(workspace), instance(commandManager), []); - - }); - test('The root folder will not be displayed if there are no tests', async () => { - const children = treeProvider.getRootNodes(); - - expect(children).to.deep.equal([]); - }); - test('The root folder will not be displayed if there are no test files directly under the root', async () => { - const folder1: TestFolder = { - folders: [], - name: 'child', - nameToRun: 'child', - testFiles: [], - time: 0, - resource: Uri.file(__filename) - }; - const tests: Tests = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [], - testFunctions: [], - testFolders: [], - testSuites: [] - }; - const children = treeProvider.getRootNodes(tests); - - expect(children).to.deep.equal([]); - }); - test('Files & folders under root folder are returned as children', async () => { - const rootFolderPath = path.join('a', 'b', 'root'); - const child1FolderPath = path.join('a', 'b', 'root', 'child1'); - const child2FolderPath = path.join('a', 'b', 'root', 'child2'); - const file1: TestFile = { - fullPath: path.join(rootFolderPath, 'file1'), - functions: [], - name: 'file', - nameToRun: 'file', - resource: Uri.file('file'), - suites: [], - time: 0, - xmlName: 'file' - }; - const file2: TestFile = { - fullPath: path.join(rootFolderPath, 'file2'), - functions: [], - name: 'file2', - nameToRun: 'file2', - resource: Uri.file('file2'), - suites: [], - time: 0, - xmlName: 'file2' - }; - const file3: TestFile = { - fullPath: path.join(child1FolderPath, 'file1'), - functions: [], - name: 'file3', - nameToRun: 'file3', - resource: Uri.file('file3'), - suites: [], - time: 0, - xmlName: 'file3' - }; - const child2Folder: TestFolder = { - folders: [], - name: child2FolderPath, - nameToRun: 'child3', - testFiles: [], - time: 0, - resource: Uri.file(__filename) - }; - const child1Folder: TestFolder = { - folders: [child2Folder], - name: child1FolderPath, - nameToRun: 'child2', - testFiles: [file3], - time: 0, - resource: Uri.file(__filename) - }; - const rootFolder: TestFolder = { - folders: [child1Folder], - name: rootFolderPath, - nameToRun: 'child', - testFiles: [file1, file2], - time: 0, - resource: Uri.file(__filename) - }; - const tests: Tests = { - rootTestFolders: [rootFolder], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3], - testFunctions: [], - testFolders: [rootFolder, child1Folder, child2Folder], - testSuites: [] - }; - const children = treeProvider.getRootNodes(tests); - - expect(children).to.be.lengthOf(3); - expect(children).to.deep.equal([file1, file2, child1Folder]); - }); - test('Root folders are returned as children', async () => { - const child1FolderPath = path.join('a', 'b', 'root1', 'child1'); - const child2FolderPath = path.join('a', 'b', 'root1', 'child1', 'child2'); - const child3FolderPath = path.join('a', 'b', 'root2', 'child3'); - const file1: TestFile = { - fullPath: path.join(child3FolderPath, 'file1'), - functions: [], - name: 'file', - nameToRun: 'file', - resource: Uri.file('file'), - suites: [], - time: 0, - xmlName: 'file' - }; - const file2: TestFile = { - fullPath: path.join(child3FolderPath, 'file2'), - functions: [], - name: 'file2', - nameToRun: 'file2', - resource: Uri.file('file2'), - suites: [], - time: 0, - xmlName: 'file2' - }; - const file3: TestFile = { - fullPath: path.join(child3FolderPath, 'file3'), - functions: [], - name: 'file3', - nameToRun: 'file3', - resource: Uri.file('file3'), - suites: [], - time: 0, - xmlName: 'file3' - }; - const child2Folder: TestFolder = { - folders: [], - name: child2FolderPath, - nameToRun: 'child3', - testFiles: [file2], - time: 0, - resource: Uri.file(__filename) - }; - const child1Folder: TestFolder = { - folders: [child2Folder], - name: child1FolderPath, - nameToRun: 'child2', - testFiles: [file1], - time: 0, - resource: Uri.file(__filename) - }; - const child3Folder: TestFolder = { - folders: [], - name: child3FolderPath, - nameToRun: 'child', - testFiles: [file3], - time: 0, - resource: Uri.file(__filename) - }; - const tests: Tests = { - rootTestFolders: [child1Folder, child3Folder], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3], - testFunctions: [], - testFolders: [child3Folder, child1Folder, child2Folder], - testSuites: [] - }; - const children = treeProvider.getRootNodes(tests); - - expect(children).to.be.lengthOf(2); - expect(children).to.deep.equal([child1Folder, child3Folder]); - }); - }); -}); diff --git a/src/test/testing/explorer/treeView.unit.test.ts b/src/test/testing/explorer/treeView.unit.test.ts deleted file mode 100644 index 65fc2d43d0f1..000000000000 --- a/src/test/testing/explorer/treeView.unit.test.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { TreeView, Uri } from 'vscode'; -import { ApplicationShell } from '../../../client/common/application/applicationShell'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { IApplicationShell, ICommandManager } from '../../../client/common/application/types'; -import { Commands } from '../../../client/common/constants'; -import { TestTreeViewProvider } from '../../../client/testing/explorer/testTreeViewProvider'; -import { TreeViewService } from '../../../client/testing/explorer/treeView'; -import { ITestTreeViewProvider, TestDataItem } from '../../../client/testing/types'; - -// tslint:disable:no-any - -suite('Unit Tests Test Explorer Tree View', () => { - let treeViewService: TreeViewService; - let treeView: typemoq.IMock<TreeView<TestDataItem>>; - let commandManager: ICommandManager; - let appShell: IApplicationShell; - let treeViewProvider: ITestTreeViewProvider; - setup(() => { - commandManager = mock(CommandManager); - treeViewProvider = mock(TestTreeViewProvider); - appShell = mock(ApplicationShell); - treeView = typemoq.Mock.ofType<TreeView<TestDataItem>>(); - treeViewService = new TreeViewService(instance(treeViewProvider), [], - instance(appShell), instance(commandManager)); - }); - - test('Activation will create the treeview (without a resource)', async () => { - await treeViewService.activate(undefined); - verify(appShell.createTreeView('python_tests', deepEqual({ showCollapseAll: true, treeDataProvider: instance(treeViewProvider) }))).once(); - }); - test('Activation will create the treeview (with a resource)', async () => { - await treeViewService.activate(Uri.file(__filename)); - verify(appShell.createTreeView('python_tests', deepEqual({ showCollapseAll: true, treeDataProvider: instance(treeViewProvider) }))).once(); - }); - test('Activation will add command handlers (without a resource)', async () => { - await treeViewService.activate(undefined); - verify(commandManager.registerCommand(Commands.Test_Reveal_Test_Item, treeViewService.onRevealTestItem, treeViewService)).once(); - }); - test('Activation will add command handlers (with a resource)', async () => { - await treeViewService.activate(Uri.file(__filename)); - verify(commandManager.registerCommand(Commands.Test_Reveal_Test_Item, treeViewService.onRevealTestItem, treeViewService)).once(); - }); - test('Invoking the command handler will reveal the node in the tree', async () => { - const data = {} as any; - treeView - .setup(t => t.reveal(typemoq.It.isAny())) - .returns(() => Promise.resolve()) - .verifiable(typemoq.Times.once()); - when(appShell.createTreeView('python_tests', anything())).thenReturn(treeView.object); - - await treeViewService.activate(undefined); - await treeViewService.onRevealTestItem(data); - - treeView.verifyAll(); - }); -}); diff --git a/src/test/testing/helper.ts b/src/test/testing/helper.ts deleted file mode 100644 index 846fa70dc386..000000000000 --- a/src/test/testing/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/testing/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/testing/main.unit.test.ts b/src/test/testing/main.unit.test.ts deleted file mode 100644 index 7f38e6ad6b72..000000000000 --- a/src/test/testing/main.unit.test.ts +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as sinon from 'sinon'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import { Disposable } from 'vscode'; -import { CommandManager } from '../../client/common/application/commandManager'; -import { ICommandManager } from '../../client/common/application/types'; -import { AlwaysDisplayTestExplorerGroups } from '../../client/common/experimentGroups'; -import { ExperimentsManager } from '../../client/common/experiments'; -import { IDisposableRegistry, IExperimentsManager } from '../../client/common/types'; -import { ServiceContainer } from '../../client/ioc/container'; -import { IServiceContainer } from '../../client/ioc/types'; -import { JediSymbolProvider } from '../../client/providers/symbolProvider'; -import { UnitTestManagementService } from '../../client/testing/main'; - -suite('Unit Tests - ManagementService', () => { - suite('Experiments', () => { - let serviceContainer: IServiceContainer; - let sandbox: sinon.SinonSandbox; - let experiment: IExperimentsManager; - let commandManager: ICommandManager; - let testManagementService: UnitTestManagementService; - setup(() => { - serviceContainer = mock(ServiceContainer); - sandbox = sinon.createSandbox(); - - sandbox.stub(UnitTestManagementService.prototype, 'registerSymbolProvider'); - sandbox.stub(UnitTestManagementService.prototype, 'registerCommands'); - sandbox.stub(UnitTestManagementService.prototype, 'registerHandlers'); - sandbox.stub(UnitTestManagementService.prototype, 'autoDiscoverTests').callsFake(() => Promise.resolve()); - - experiment = mock(ExperimentsManager); - commandManager = mock(CommandManager); - - when(serviceContainer.get<Disposable[]>(IDisposableRegistry)).thenReturn([]); - when(serviceContainer.get<IExperimentsManager>(IExperimentsManager)).thenReturn(instance(experiment)); - when(serviceContainer.get<ICommandManager>(ICommandManager)).thenReturn(instance(commandManager)); - when(commandManager.executeCommand(anything(), anything(), anything())).thenResolve(); - - testManagementService = new UnitTestManagementService(instance(serviceContainer)); - }); - teardown(() => { - sandbox.restore(); - }); - - test('Execute command if in experiment', async () => { - when(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).thenReturn(true); - - await testManagementService.activate(instance(mock(JediSymbolProvider))); - - verify(commandManager.executeCommand('setContext', 'testsDiscovered', true)).once(); - verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).once(); - verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.control)).never(); - verify(experiment.sendTelemetryIfInExperiment(anything())).never(); - }); - test('If not in experiment, check and send Telemetry for control group and do not execute command', async () => { - when(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).thenReturn(false); - - await testManagementService.activate(instance(mock(JediSymbolProvider))); - - verify(commandManager.executeCommand('setContext', 'testsDiscovered', anything())).never(); - verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.experiment)).once(); - verify(experiment.inExperiment(AlwaysDisplayTestExplorerGroups.control)).never(); - verify(experiment.sendTelemetryIfInExperiment(AlwaysDisplayTestExplorerGroups.control)).once(); - }); - }); -}); diff --git a/src/test/testing/mocks.ts b/src/test/testing/mocks.ts deleted file mode 100644 index 276961f558ca..000000000000 --- a/src/test/testing/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/testing/common/constants'; -import { BaseTestManager } from '../../client/testing/common/managers/baseTestManager'; -import { ITestDebugLauncher, ITestDiscoveryService, IUnitTestSocketServer, LaunchOptions, TestDiscoveryOptions, TestProvider, Tests, TestsToRun } from '../../client/testing/common/types'; - -@injectable() -export class MockDebugLauncher implements ITestDebugLauncher, Disposable { - public get launched(): Promise<boolean> { - return this._launched.promise; - } - public get debuggerPromise(): Deferred<Tests> { - // tslint:disable-next-line:no-non-null-assertion - return this._promise!; - } - public get cancellationToken(): CancellationToken { - if (this._token === undefined) { - throw Error('debugger not launched'); - } - return this._token; - } - // tslint:disable-next-line:variable-name - private _launched: Deferred<boolean>; - // tslint:disable-next-line:variable-name - private _promise?: Deferred<Tests>; - // tslint:disable-next-line:variable-name - private _token?: CancellationToken; - constructor() { - this._launched = createDeferred<boolean>(); - } - public async getLaunchOptions(_resource?: Uri): Promise<{ port: number; host: string }> { - return { port: 0, host: 'localhost' }; - } - public async launchDebugger(options: LaunchOptions): Promise<void> { - this._launched.resolve(true); - // tslint:disable-next-line:no-non-null-assertion - this._token = options.token!; - this._promise = createDeferred<Tests>(); - // tslint:disable-next-line:no-non-null-assertion - options.token!.onCancellationRequested(() => { - if (this._promise) { - this._promise.reject('Mock-User Cancelled'); - } - }); - return this._promise.promise as {} as Promise<void>; - } - public dispose() { - this._promise = undefined; - } -} - -@injectable() -export class MockTestManagerWithRunningTests extends BaseTestManager { - // tslint:disable-next-line:no-any - public readonly runnerDeferred = createDeferred<Tests>(); - public readonly enabled = true; - // tslint:disable-next-line:no-any - public readonly discoveryDeferred = createDeferred<Tests>(); - constructor(testProvider: TestProvider, product: Product, workspaceFolder: Uri, rootDirectory: string, - serviceContainer: IServiceContainer) { - super(testProvider, product, workspaceFolder, rootDirectory, serviceContainer); - } - protected getDiscoveryOptions(_ignoreCache: boolean) { - // tslint:disable-next-line:no-object-literal-type-assertion - return {} as TestDiscoveryOptions; - } - // tslint:disable-next-line:no-any - protected async runTestImpl(_tests: Tests, _testsToRun?: TestsToRun, _runFailedTests?: boolean, _debug?: boolean): Promise<Tests> { - // tslint:disable-next-line:no-non-null-assertion - this.testRunnerCancellationToken!.onCancellationRequested(() => { - this.runnerDeferred.reject(CANCELLATION_REASON); - }); - return this.runnerDeferred.promise; - } - protected async discoverTestsImpl(_ignoreCache: boolean, _debug?: boolean): Promise<Tests> { - // tslint:disable-next-line:no-non-null-assertion - this.testDiscoveryCancellationToken!.onCancellationRequested(() => { - this.discoveryDeferred.reject(CANCELLATION_REASON); - }); - return this.discoveryDeferred.promise; - } -} - -@injectable() -export class MockDiscoveryService implements ITestDiscoveryService { - constructor(private discoverPromise: Promise<Tests>) { } - public async discoverTests(_options: TestDiscoveryOptions): Promise<Tests> { - return this.discoverPromise; - } -} - -// tslint:disable-next-line:max-classes-per-file -@injectable() -export class MockUnitTestSocketServer extends EventEmitter implements IUnitTestSocketServer { - private results: {}[] = []; - public reset() { - this.removeAllListeners(); - } - public addResults(results: {}[]) { - this.results.push(...results); - } - public async start(options: { port: number; host: string } = { port: 0, host: 'localhost' }): Promise<number> { - this.results.forEach(result => { - this.emit('result', result); - }); - this.results = []; - return typeof options.port === 'number' ? options.port! : 0; - } - // tslint:disable-next-line:no-empty - public stop(): void { } - // tslint:disable-next-line:no-empty - public dispose() { } -} diff --git a/src/test/testing/navigation/commandHandlers.unit.test.ts b/src/test/testing/navigation/commandHandlers.unit.test.ts deleted file mode 100644 index 397cf7da5d2a..000000000000 --- a/src/test/testing/navigation/commandHandlers.unit.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CommandManager } from '../../../client/common/application/commandManager'; -import { ICommandManager } from '../../../client/common/application/types'; -import { AsyncDisposableRegistry } from '../../../client/common/asyncDisposableRegistry'; -import { Commands } from '../../../client/common/constants'; -import { IDisposable, IDisposableRegistry } from '../../../client/common/types'; -import { TestCodeNavigatorCommandHandler } from '../../../client/testing/navigation/commandHandler'; -import { TestFileCodeNavigator } from '../../../client/testing/navigation/fileNavigator'; -import { TestFunctionCodeNavigator } from '../../../client/testing/navigation/functionNavigator'; -import { TestSuiteCodeNavigator } from '../../../client/testing/navigation/suiteNavigator'; -import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler } from '../../../client/testing/navigation/types'; - -// tslint:disable:max-func-body-length -suite('Unit Tests - Navigation Command Handler', () => { - let commandHandler: ITestCodeNavigatorCommandHandler; - let cmdManager: ICommandManager; - let fileHandler: ITestCodeNavigator; - let functionHandler: ITestCodeNavigator; - let suiteHandler: ITestCodeNavigator; - let disposableRegistry: IDisposableRegistry; - setup(() => { - cmdManager = mock(CommandManager); - fileHandler = mock(TestFileCodeNavigator); - functionHandler = mock(TestFunctionCodeNavigator); - suiteHandler = mock(TestSuiteCodeNavigator); - disposableRegistry = mock(AsyncDisposableRegistry); - commandHandler = new TestCodeNavigatorCommandHandler( - instance(cmdManager), - instance(fileHandler), - instance(functionHandler), - instance(suiteHandler), - instance(disposableRegistry) - ); - }); - test('Ensure Navigation handlers are registered', async () => { - commandHandler.register(); - verify(cmdManager.registerCommand(Commands.navigateToTestFile, instance(fileHandler).navigateTo, instance(fileHandler))).once(); - verify(cmdManager.registerCommand(Commands.navigateToTestFunction, instance(functionHandler).navigateTo, instance(functionHandler))).once(); - verify(cmdManager.registerCommand(Commands.navigateToTestSuite, instance(suiteHandler).navigateTo, instance(suiteHandler))).once(); - }); - test('Ensure handlers are disposed', async () => { - const disposable1 = typemoq.Mock.ofType<IDisposable>(); - const disposable2 = typemoq.Mock.ofType<IDisposable>(); - const disposable3 = typemoq.Mock.ofType<IDisposable>(); - when(cmdManager.registerCommand(Commands.navigateToTestFile, instance(fileHandler).navigateTo, instance(fileHandler))).thenReturn(disposable1.object); - when(cmdManager.registerCommand(Commands.navigateToTestFunction, instance(functionHandler).navigateTo, instance(functionHandler))).thenReturn(disposable2.object); - when(cmdManager.registerCommand(Commands.navigateToTestSuite, instance(suiteHandler).navigateTo, instance(suiteHandler))).thenReturn(disposable3.object); - - commandHandler.register(); - commandHandler.dispose(); - - disposable1.verify(d => d.dispose(), typemoq.Times.once()); - disposable2.verify(d => d.dispose(), typemoq.Times.once()); - disposable3.verify(d => d.dispose(), typemoq.Times.once()); - }); - test('Ensure command handler is reigstered to be disposed', async () => { - commandHandler.register(); - verify(disposableRegistry.push(commandHandler)).once(); - }); -}); diff --git a/src/test/testing/navigation/fileNavigator.unit.test.ts b/src/test/testing/navigation/fileNavigator.unit.test.ts deleted file mode 100644 index 4147a2e3def8..000000000000 --- a/src/test/testing/navigation/fileNavigator.unit.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaisAsPromised from 'chai-as-promised'; -import { anything, capture, instance, mock, verify, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { TestFileCodeNavigator } from '../../../client/testing/navigation/fileNavigator'; -import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; -import { ITestNavigatorHelper } from '../../../client/testing/navigation/types'; - -use(chaisAsPromised); - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - Navigation File', () => { - let navigator: TestFileCodeNavigator; - let helper: ITestNavigatorHelper; - setup(() => { - helper = mock(TestNavigatorHelper); - navigator = new TestFileCodeNavigator(instance(helper)); - }); - test('Ensure file is opened', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenResolve(); - - await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any, false); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - }); - test('Ensure errors are swallowed', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenReject(new Error('kaboom')); - - await navigator.navigateTo(filePath, { fullPath: filePath.fsPath } as any, false); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - }); -}); diff --git a/src/test/testing/navigation/functionNavigator.unit.test.ts b/src/test/testing/navigation/functionNavigator.unit.test.ts deleted file mode 100644 index 91c0bc0aaf95..000000000000 --- a/src/test/testing/navigation/functionNavigator.unit.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaisAsPromised from 'chai-as-promised'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Location, Range, SymbolInformation, SymbolKind, TextDocument, TextEditor, TextEditorRevealType, Uri } from 'vscode'; -import { DocumentManager } from '../../../client/common/application/documentManager'; -import { IDocumentManager } from '../../../client/common/application/types'; -import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; -import { ITestCollectionStorageService } from '../../../client/testing/common/types'; -import { TestFunctionCodeNavigator } from '../../../client/testing/navigation/functionNavigator'; -import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; -import { ITestNavigatorHelper } from '../../../client/testing/navigation/types'; - -use(chaisAsPromised); - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - Navigation Function', () => { - let navigator: TestFunctionCodeNavigator; - let helper: ITestNavigatorHelper; - let docManager: IDocumentManager; - let doc: typemoq.IMock<TextDocument>; - let editor: typemoq.IMock<TextEditor>; - let storage: ITestCollectionStorageService; - setup(() => { - doc = typemoq.Mock.ofType<TextDocument>(); - editor = typemoq.Mock.ofType<TextEditor>(); - helper = mock(TestNavigatorHelper); - docManager = mock(DocumentManager); - storage = mock(TestCollectionStorageService); - navigator = new TestFunctionCodeNavigator(instance(helper), instance(docManager), instance(storage)); - }); - test('Ensure file is opened', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: {} }; - when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); - - await navigator.navigateTo(filePath, {} as any); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - }); - test('Ensure errors are swallowed', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenReject(new Error('kaboom')); - const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: {} }; - when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); - - await navigator.navigateTo(filePath, {} as any); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - }); - async function navigateToFunction(focusCode: boolean) { - const filePath = Uri.file('some file Path'); - const line = 999; - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: { name: 'function_name' } }; - when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); - const range = new Range(line, 0, line, 0); - const symbol: SymbolInformation = { - containerName: '', - kind: SymbolKind.Function, - name: 'function_name', - location: new Location(Uri.file(__filename), range) - }; - when(helper.findSymbol(doc.object, anything(), anything())).thenResolve(symbol); - - await navigator.navigateTo(filePath, { name: 'function_name' } as any, focusCode); - - verify(helper.openFile(anything())).once(); - verify(helper.findSymbol(doc.object, anything(), anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - if (focusCode) { - verify(docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range }))).once(); - } else { - editor.verify(e => e.revealRange(typemoq.It.isAny(), TextEditorRevealType.Default), typemoq.Times.once()); - } - } - test('Ensure we use line number from test function when navigating in file (without focusing code)', async () => { - await navigateToFunction(false); - }); - test('Ensure we use line number from test function when navigating in file (focusing code)', async () => { - await navigateToFunction(true); - }); - test('Ensure file is opened and range not revealed', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedFn = { parentTestFile: { fullPath: filePath.fsPath }, testFunction: {} }; - when(storage.findFlattendTestFunction(filePath, anything())).thenReturn(flattenedFn as any); - const search = (s: SymbolInformation) => s.kind === SymbolKind.Function && s.name === 'Hello'; - when(helper.findSymbol(doc.object, search, anything())).thenResolve(); - - await navigator.navigateTo(filePath, {} as any); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - editor.verify(e => e.revealRange(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - }); -}); diff --git a/src/test/testing/navigation/helper.unit.test.ts b/src/test/testing/navigation/helper.unit.test.ts deleted file mode 100644 index d13e09ce3c99..000000000000 --- a/src/test/testing/navigation/helper.unit.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaisAsPromised from 'chai-as-promised'; -import { anything, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationTokenSource, DocumentSymbolProvider, SymbolInformation, SymbolKind, TextDocument, TextEditor, Uri } from 'vscode'; -import { DocumentManager } from '../../../client/common/application/documentManager'; -import { IDocumentManager } from '../../../client/common/application/types'; -import { LanguageServerSymbolProvider } from '../../../client/providers/symbolProvider'; -import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; - -use(chaisAsPromised); - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - Navigation Helper', () => { - let helper: TestNavigatorHelper; - let docManager: IDocumentManager; - let doc: typemoq.IMock<TextDocument>; - let editor: typemoq.IMock<TextEditor>; - let symbolProvider: DocumentSymbolProvider; - setup(() => { - doc = typemoq.Mock.ofType<TextDocument>(); - editor = typemoq.Mock.ofType<TextEditor>(); - doc.setup((d: any) => d.then).returns(() => undefined); - editor.setup((e: any) => e.then).returns(() => undefined); - docManager = mock(DocumentManager); - symbolProvider = mock(LanguageServerSymbolProvider); - helper = new TestNavigatorHelper(instance(docManager), instance(symbolProvider)); - }); - test('Ensure file is opened', async () => { - const filePath = Uri.file('some file Path'); - when(docManager.openTextDocument(anything())).thenResolve(doc.object as any); - when(docManager.showTextDocument(doc.object)).thenResolve(editor.object as any); - - const [d, e] = await helper.openFile(filePath); - - verify(docManager.openTextDocument(filePath)).once(); - verify(docManager.showTextDocument(doc.object)).once(); - expect(d).to.deep.equal(doc.object); - expect(e).to.deep.equal(editor.object); - }); - test('No symbols if symbol provider is not registered', async () => { - const token = new CancellationTokenSource().token; - const predicate = (s: SymbolInformation) => s.kind === SymbolKind.Function && s.name === ''; - const symbol = await helper.findSymbol(doc.object, predicate, token); - expect(symbol).to.equal(undefined, 'Must be undefined'); - }); - test('No symbols if no symbols', async () => { - const token = new CancellationTokenSource().token; - when(symbolProvider.provideDocumentSymbols(doc.object, token)).thenResolve([] as any); - - const predicate = (s: SymbolInformation) => s.kind === SymbolKind.Function && s.name === ''; - const symbol = await helper.findSymbol(doc.object, predicate, token); - - expect(symbol).to.equal(undefined, 'Must be undefined'); - verify(symbolProvider.provideDocumentSymbols(doc.object, token)).once(); - }); - test('Returns matching symbol', async () => { - const symbols: SymbolInformation[] = [ - { containerName: '', kind: SymbolKind.Function, name: '1', location: undefined as any }, - { containerName: '', kind: SymbolKind.Class, name: '2', location: undefined as any }, - { containerName: '', kind: SymbolKind.File, name: '2', location: undefined as any } - ]; - const token = new CancellationTokenSource().token; - when(symbolProvider.provideDocumentSymbols(doc.object, token)).thenResolve(symbols as any); - - const predicate = (s: SymbolInformation) => s.kind === SymbolKind.Class && s.name === '2'; - const symbol = await helper.findSymbol(doc.object, predicate, token); - - expect(symbol).to.deep.equal(symbols[1]); - verify(symbolProvider.provideDocumentSymbols(doc.object, token)).once(); - }); -}); diff --git a/src/test/testing/navigation/serviceRegistry.unit.test.ts b/src/test/testing/navigation/serviceRegistry.unit.test.ts deleted file mode 100644 index 601daf645054..000000000000 --- a/src/test/testing/navigation/serviceRegistry.unit.test.ts +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { use } from 'chai'; -import * as chaisAsPromised from 'chai-as-promised'; -import { anything, instance, mock, verify } from 'ts-mockito'; -import { IDocumentSymbolProvider } from '../../../client/common/types'; -import { ServiceManager } from '../../../client/ioc/serviceManager'; -import { TestCodeNavigatorCommandHandler } from '../../../client/testing/navigation/commandHandler'; -import { TestFileCodeNavigator } from '../../../client/testing/navigation/fileNavigator'; -import { TestFunctionCodeNavigator } from '../../../client/testing/navigation/functionNavigator'; -import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; -import { registerTypes } from '../../../client/testing/navigation/serviceRegistry'; -import { TestSuiteCodeNavigator } from '../../../client/testing/navigation/suiteNavigator'; -import { TestFileSymbolProvider } from '../../../client/testing/navigation/symbolProvider'; -import { ITestCodeNavigator, ITestCodeNavigatorCommandHandler, ITestNavigatorHelper, NavigableItemType } from '../../../client/testing/navigation/types'; - -use(chaisAsPromised); - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - Navigation Service Registry', () => { - test('Ensure services are registered', async () => { - const serviceManager = mock(ServiceManager); - - registerTypes(instance(serviceManager)); - - verify(serviceManager.addSingleton<ITestNavigatorHelper>(ITestNavigatorHelper, TestNavigatorHelper)).once(); - verify(serviceManager.addSingleton<ITestCodeNavigatorCommandHandler>(ITestCodeNavigatorCommandHandler, TestCodeNavigatorCommandHandler)).once(); - verify(serviceManager.addSingleton<ITestCodeNavigator>(ITestCodeNavigator, TestFileCodeNavigator, NavigableItemType.testFile)).once(); - verify(serviceManager.addSingleton<ITestCodeNavigator>(ITestCodeNavigator, TestFunctionCodeNavigator, NavigableItemType.testFunction)).once(); - verify(serviceManager.addSingleton<ITestCodeNavigator>(ITestCodeNavigator, TestSuiteCodeNavigator, NavigableItemType.testSuite)).once(); - verify(serviceManager.addSingleton<IDocumentSymbolProvider>(anything(), TestFileSymbolProvider, 'test')).once(); - }); -}); diff --git a/src/test/testing/navigation/suiteNavigator.unit.test.ts b/src/test/testing/navigation/suiteNavigator.unit.test.ts deleted file mode 100644 index a0cb39a20d3d..000000000000 --- a/src/test/testing/navigation/suiteNavigator.unit.test.ts +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect, use } from 'chai'; -import * as chaisAsPromised from 'chai-as-promised'; -import { anything, capture, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { Location, Range, SymbolInformation, SymbolKind, TextDocument, TextEditor, TextEditorRevealType, Uri } from 'vscode'; -import { DocumentManager } from '../../../client/common/application/documentManager'; -import { IDocumentManager } from '../../../client/common/application/types'; -import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; -import { ITestCollectionStorageService } from '../../../client/testing/common/types'; -import { TestNavigatorHelper } from '../../../client/testing/navigation/helper'; -import { TestSuiteCodeNavigator } from '../../../client/testing/navigation/suiteNavigator'; -import { ITestNavigatorHelper } from '../../../client/testing/navigation/types'; - -use(chaisAsPromised); - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - Navigation Suite', () => { - let navigator: TestSuiteCodeNavigator; - let helper: ITestNavigatorHelper; - let docManager: IDocumentManager; - let doc: typemoq.IMock<TextDocument>; - let editor: typemoq.IMock<TextEditor>; - let storage: ITestCollectionStorageService; - setup(() => { - doc = typemoq.Mock.ofType<TextDocument>(); - editor = typemoq.Mock.ofType<TextEditor>(); - helper = mock(TestNavigatorHelper); - docManager = mock(DocumentManager); - storage = mock(TestCollectionStorageService); - navigator = new TestSuiteCodeNavigator(instance(helper), instance(docManager), instance(storage)); - }); - test('Ensure file is opened', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: {} }; - when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); - - await navigator.navigateTo(filePath, {} as any); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - }); - test('Ensure errors are swallowed', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenReject(new Error('kaboom')); - const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: {} }; - when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); - - await navigator.navigateTo(filePath, {} as any); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - }); - async function navigateUsingLineFromSuite(focusCode: boolean) { - const filePath = Uri.file('some file Path'); - const line = 999; - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: { name: 'suite_name' } }; - when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); - const range = new Range(line, 0, line, 0); - const symbol: SymbolInformation = { - containerName: '', - kind: SymbolKind.Class, - name: 'suite_name', - location: new Location(Uri.file(__filename), range) - }; - when(helper.findSymbol(doc.object, anything(), anything())).thenResolve(symbol); - - await navigator.navigateTo(filePath, { name: 'suite_name' } as any, focusCode); - - verify(helper.openFile(anything())).once(); - verify(helper.findSymbol(doc.object, anything(), anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - if (focusCode) { - verify(docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range }))).once(); - } else { - editor.verify(e => e.revealRange(range, TextEditorRevealType.Default), typemoq.Times.once()); - } - } - test('Ensure we use line number from test suite when navigating in file (without focusing code)', async () => { - await navigateUsingLineFromSuite(false); - }); - test('Ensure we use line number from test suite when navigating in file (focusing code)', async () => { - await navigateUsingLineFromSuite(true); - }); - async function navigateFromSuite(focusCode: boolean) { - const filePath = Uri.file('some file Path'); - const line = 999; - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: { line } }; - when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); - const range = new Range(line, 0, line, 0); - - await navigator.navigateTo(filePath, { line } as any, focusCode); - - verify(helper.openFile(anything())).once(); - verify(helper.findSymbol(anything(), anything(), anything())).never(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - if (focusCode) { - verify(docManager.showTextDocument(doc.object, deepEqual({ preserveFocus: false, selection: range }))).once(); - } else { - editor.verify(e => e.revealRange(range, TextEditorRevealType.Default), typemoq.Times.once()); - } - } - test('Navigating in file (without focusing code)', async () => { - await navigateFromSuite(false); - }); - test('Navigating in file (focusing code)', async () => { - await navigateFromSuite(true); - }); - test('Ensure file is opened and range not revealed', async () => { - const filePath = Uri.file('some file Path'); - when(helper.openFile(anything())).thenResolve([doc.object, editor.object]); - const flattenedSuite = { parentTestFile: { fullPath: filePath.fsPath }, testSuite: {} }; - when(storage.findFlattendTestSuite(filePath, anything())).thenReturn(flattenedSuite as any); - const search = (s: SymbolInformation) => s.kind === SymbolKind.Class && s.name === 'Hello'; - when(helper.findSymbol(doc.object, search, anything())).thenResolve(); - - await navigator.navigateTo(filePath, {} as any); - - verify(helper.openFile(anything())).once(); - expect(capture(helper.openFile).first()[0]!.fsPath).to.equal(filePath.fsPath); - editor.verify(e => e.revealRange(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); - }); -}); diff --git a/src/test/testing/navigation/symbolNavigator.unit.test.ts b/src/test/testing/navigation/symbolNavigator.unit.test.ts deleted file mode 100644 index 8b4e2cc2f656..000000000000 --- a/src/test/testing/navigation/symbolNavigator.unit.test.ts +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { expect } from 'chai'; -import * as path from 'path'; -import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import * as typemoq from 'typemoq'; -import { CancellationToken, CancellationTokenSource, Range, SymbolInformation, SymbolKind, TextDocument, Uri } from 'vscode'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { ProcessService } from '../../../client/common/process/proc'; -import { ProcessServiceFactory } from '../../../client/common/process/processFactory'; -import { ExecutionResult, IProcessService, IProcessServiceFactory } from '../../../client/common/process/types'; -import { IConfigurationService, IDocumentSymbolProvider } from '../../../client/common/types'; -import { EXTENSION_ROOT_DIR } from '../../../client/constants'; -import { TestFileSymbolProvider } from '../../../client/testing/navigation/symbolProvider'; - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - Navigation Command Handler', () => { - let symbolProvider: IDocumentSymbolProvider; - let configService: IConfigurationService; - let processFactory: IProcessServiceFactory; - let processService: IProcessService; - let doc: typemoq.IMock<TextDocument>; - let token: CancellationToken; - setup(() => { - configService = mock(ConfigurationService); - processFactory = mock(ProcessServiceFactory); - processService = mock(ProcessService); - doc = typemoq.Mock.ofType<TextDocument>(); - token = new CancellationTokenSource().token; - symbolProvider = new TestFileSymbolProvider(instance(configService), instance(processFactory)); - }); - test('Ensure no symbols are returned when file has not been saved', async () => { - doc.setup(d => d.isUntitled) - .returns(() => true) - .verifiable(typemoq.Times.once()); - - const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); - - expect(symbols).to.be.lengthOf(0); - doc.verifyAll(); - }); - test('Ensure no symbols are returned when there are errors in running the code', async () => { - when(configService.getSettings(anything())).thenThrow(new Error('Kaboom')); - doc.setup(d => d.isUntitled) - .returns(() => false) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.isDirty) - .returns(() => false) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.uri) - .returns(() => Uri.file(__filename)) - .verifiable(typemoq.Times.atLeastOnce()); - - const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); - - verify(configService.getSettings(anything())).once(); - expect(symbols).to.be.lengthOf(0); - doc.verifyAll(); - }); - test('Ensure no symbols are returned when there are no symbols to be returned', async () => { - const pythonPath = 'Hello There'; - const docUri = Uri.file(__filename); - const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), docUri.fsPath]; - const proc: ExecutionResult<string> = { - stdout: JSON.stringify({ classes: [], methods: [], functions: [] }) - }; - doc.setup(d => d.isUntitled) - .returns(() => false) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.isDirty) - .returns(() => false) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - when(configService.getSettings(anything())).thenReturn({ pythonPath } as any); - when(processFactory.create(anything())).thenResolve(instance(processService)); - when(processService.exec(pythonPath, anything(), anything())).thenResolve(proc); - doc.setup(d => d.isDirty).returns(() => false); - doc.setup(d => d.uri).returns(() => docUri); - - const symbols = await symbolProvider.provideDocumentSymbols(doc.object, token); - - verify(configService.getSettings(anything())).once(); - verify(processFactory.create(anything())).once(); - verify(processService.exec(pythonPath, deepEqual(args), deepEqual({ throwOnStdErr: true, token }))).once(); - expect(symbols).to.be.lengthOf(0); - doc.verifyAll(); - }); - test('Ensure symbols are returned', async () => { - const pythonPath = 'Hello There'; - const docUri = Uri.file(__filename); - const args = [path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'symbolProvider.py'), docUri.fsPath]; - const proc: ExecutionResult<string> = { - stdout: JSON.stringify({ - classes: [ - { - namespace: '1', - name: 'one', - kind: SymbolKind.Class, - range: { start: { line: 1, character: 2 }, end: { line: 3, character: 4 } } - } - ], - methods: [ - { - namespace: '2', - name: 'two', - kind: SymbolKind.Class, - range: { start: { line: 5, character: 6 }, end: { line: 7, character: 8 } } - } - ], - functions: [ - { - namespace: '3', - name: 'three', - kind: SymbolKind.Class, - range: { start: { line: 9, character: 10 }, end: { line: 11, character: 12 } } - } - ] - }) - }; - doc.setup(d => d.isUntitled) - .returns(() => false) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.isDirty) - .returns(() => false) - .verifiable(typemoq.Times.once()); - doc.setup(d => d.uri) - .returns(() => docUri) - .verifiable(typemoq.Times.atLeastOnce()); - when(configService.getSettings(anything())).thenReturn({ pythonPath } as any); - when(processFactory.create(anything())).thenResolve(instance(processService)); - when(processService.exec(pythonPath, anything(), anything())).thenResolve(proc); - doc.setup(d => d.isDirty).returns(() => false); - doc.setup(d => d.uri).returns(() => docUri); - - const symbols = (await symbolProvider.provideDocumentSymbols(doc.object, token)) as SymbolInformation[]; - - verify(configService.getSettings(anything())).once(); - verify(processFactory.create(anything())).once(); - verify(processService.exec(pythonPath, deepEqual(args), deepEqual({ throwOnStdErr: true, token }))).once(); - expect(symbols).to.be.lengthOf(3); - doc.verifyAll(); - expect(symbols[0].kind).to.be.equal(SymbolKind.Class); - expect(symbols[0].name).to.be.equal('one'); - expect(symbols[0].location.range).to.be.deep.equal(new Range(1, 2, 3, 4)); - - expect(symbols[1].kind).to.be.equal(SymbolKind.Method); - expect(symbols[1].name).to.be.equal('two'); - expect(symbols[1].location.range).to.be.deep.equal(new Range(5, 6, 7, 8)); - - expect(symbols[2].kind).to.be.equal(SymbolKind.Function); - expect(symbols[2].name).to.be.equal('three'); - expect(symbols[2].location.range).to.be.deep.equal(new Range(9, 10, 11, 12)); - }); -}); diff --git a/src/test/testing/nosetest/nosetest.argsService.unit.test.ts b/src/test/testing/nosetest/nosetest.argsService.unit.test.ts deleted file mode 100644 index 152d1dd70686..000000000000 --- a/src/test/testing/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/testing/common/argumentsHelper'; -import { ArgumentsService as NoseTestArgumentsService } from '../../../client/testing/nosetest/services/argsService'; -import { IArgumentsHelper } from '../../../client/testing/types'; - -suite('ArgsService: nosetest', () => { - let argumentsService: NoseTestArgumentsService; - - suiteSetup(() => { - const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - const logger = typeMoq.Mock.ofType<ILogger>(); - - 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/testing/nosetest/nosetest.discovery.unit.test.ts b/src/test/testing/nosetest/nosetest.discovery.unit.test.ts deleted file mode 100644 index 2e8bb8fda26b..000000000000 --- a/src/test/testing/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/testing/common/constants'; -import { ITestDiscoveryService, ITestRunner, ITestsParser, Options, TestDiscoveryOptions, Tests } from '../../../client/testing/common/types'; -import { TestDiscoveryService } from '../../../client/testing/nosetest/services/discoveryService'; -import { IArgumentsService, TestFilter } from '../../../client/testing/types'; - -use(chaipromise); - -suite('Unit Tests - nose - Discovery', () => { - let discoveryService: ITestDiscoveryService; - let argsService: typeMoq.IMock<IArgumentsService>; - let testParser: typeMoq.IMock<ITestsParser>; - let runner: typeMoq.IMock<ITestRunner>; - setup(() => { - const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - argsService = typeMoq.Mock.ofType<IArgumentsService>(); - testParser = typeMoq.Mock.ofType<ITestsParser>(); - runner = typeMoq.Mock.ofType<ITestRunner>(); - - serviceContainer.setup(s => s.get(typeMoq.It.isValue(IArgumentsService), typeMoq.It.isAny())) - .returns(() => argsService.object); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) - .returns(() => runner.object); - - discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); - }); - test('Ensure discovery is invoked with the right args', async () => { - const args: string[] = []; - const runOutput = 'xyz'; - const tests: Tests = { - summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, - testFiles: [], testFunctions: [], testSuites: [], - rootTestFolders: [], testFolders: [] - }; - argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) - .returns(() => []) - .verifiable(typeMoq.Times.once()); - runner.setup(r => r.run(typeMoq.It.isValue(NOSETEST_PROVIDER), typeMoq.It.isAny())) - .callback((_, opts: Options) => { - expect(opts.args).to.include('--collect-only'); - expect(opts.args).to.include('-vvv'); - }) - .returns(() => Promise.resolve(runOutput)) - .verifiable(typeMoq.Times.once()); - testParser.setup(t => t.parse(typeMoq.It.isValue(runOutput), typeMoq.It.isAny())) - .returns(() => tests) - .verifiable(typeMoq.Times.once()); - - const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - argsService.verifyAll(); - runner.verifyAll(); - testParser.verifyAll(); - }); - test('Ensure discovery is cancelled', async () => { - const args: string[] = []; - const runOutput = 'xyz'; - const tests: Tests = { - summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, - testFiles: [], testFunctions: [], testSuites: [], - rootTestFolders: [], testFolders: [] - }; - argsService.setup(a => a.filterArguments(typeMoq.It.isValue(args), typeMoq.It.isValue(TestFilter.discovery))) - .returns(() => []) - .verifiable(typeMoq.Times.once()); - runner.setup(r => r.run(typeMoq.It.isValue(NOSETEST_PROVIDER), typeMoq.It.isAny())) - .callback((_, opts: Options) => { - expect(opts.args).to.include('--collect-only'); - expect(opts.args).to.include('-vvv'); - }) - .returns(() => Promise.resolve(runOutput)) - .verifiable(typeMoq.Times.once()); - testParser.setup(t => t.parse(typeMoq.It.isAny(), typeMoq.It.isAny())) - .returns(() => tests) - .verifiable(typeMoq.Times.never()); - - const options = typeMoq.Mock.ofType<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - token.setup(t => t.isCancellationRequested) - .returns(() => true) - .verifiable(typeMoq.Times.once()); - - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - const promise = discoveryService.discoverTests(options.object); - - await expect(promise).to.eventually.be.rejectedWith('cancelled'); - argsService.verifyAll(); - runner.verifyAll(); - testParser.verifyAll(); - }); -}); diff --git a/src/test/testing/nosetest/nosetest.disovery.test.ts b/src/test/testing/nosetest/nosetest.disovery.test.ts deleted file mode 100644 index 3722113af351..000000000000 --- a/src/test/testing/nosetest/nosetest.disovery.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'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as vscode from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { ITestManagerFactory } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { MockProcessService } from '../../mocks/proc'; -import { lookForTestFile } from '../helper'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; - -const PYTHON_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles'); -const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const filesToDelete = [ - path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), - path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') -]; - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - nose - discovery with mocked process output', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - - suiteSetup(async () => { - filesToDelete.forEach(file => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - await initialize(); - }); - suiteTeardown(async () => { - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - filesToDelete.forEach(file => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - - async function injectTestDiscoveryOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--collect-only') >= 0) { - let out = fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8'); - // Value in the test files. - out = out.replace(/\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles/g, PYTHON_FILES_PATH); - callback({ - out, - source: 'stdout' - }); - } - }); - } - - test('Discover Tests (single test file)', async () => { - await injectTestDiscoveryOutput('one.output'); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - lookForTestFile(tests, path.join('tests', 'test_one.py')); - }); - - test('Check that nameToRun in testSuites has class name after : (single test file)', async () => { - await injectTestDiscoveryOutput('two.output'); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testSuites.every(t => t.testSuite.name === t.testSuite.nameToRun.split(':')[1]), true, 'Suite name does not match class name'); - }); - test('Discover Tests (-m=test)', async () => { - await injectTestDiscoveryOutput('three.output'); - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 5, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 16, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 6, 'Incorrect number of test suites'); - lookForTestFile(tests, path.join('tests', 'test_unittest_one.py')); - lookForTestFile(tests, path.join('tests', 'test_unittest_two.py')); - lookForTestFile(tests, path.join('tests', 'unittest_three_test.py')); - lookForTestFile(tests, path.join('tests', 'test4.py')); - lookForTestFile(tests, 'test_root.py'); - }); - - test('Discover Tests (-w=specific -m=tst)', async () => { - await injectTestDiscoveryOutput('four.output'); - await updateSetting('testing.nosetestArgs', ['-w', 'specific', '-m', 'tst'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - lookForTestFile(tests, path.join('specific', 'tst_unittest_one.py')); - lookForTestFile(tests, path.join('specific', 'tst_unittest_two.py')); - }); - - test('Discover Tests (-m=test_)', async () => { - await injectTestDiscoveryOutput('five.output'); - await updateSetting('testing.nosetestArgs', ['-m', 'test_'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - lookForTestFile(tests, 'test_root.py'); - }); -}); diff --git a/src/test/testing/nosetest/nosetest.run.test.ts b/src/test/testing/nosetest/nosetest.run.test.ts deleted file mode 100644 index 727821e10bc8..000000000000 --- a/src/test/testing/nosetest/nosetest.run.test.ts +++ /dev/null @@ -1,184 +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 { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { ITestManagerFactory, TestsToRun } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { MockProcessService } from '../../mocks/proc'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; - -const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const filesToDelete = [ - path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), - path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') -]; - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - nose - run against actual python process', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - - suiteSetup(async () => { - filesToDelete.forEach(file => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - await initialize(); - }); - suiteTeardown(async () => { - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - filesToDelete.forEach(file => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingleton<ICondaService>(ICondaService, CondaService); - ioc.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); - } - - async function injectTestDiscoveryOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.indexOf('--collect-only') >= 0) { - callback({ - out: fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8').replace(/\/Users\/donjayamanne\/.vscode\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles\/noseFiles/g, UNITTEST_TEST_FILES_PATH), - source: 'stdout' - }); - } - }); - } - - async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (failedOutput && args.indexOf('--failed') === -1) { - return; - } - - const index = args.findIndex(arg => arg.startsWith('--xunit-file=')); - if (index >= 0) { - const fileName = args[index].substr('--xunit-file='.length); - const contents = fs.readFileSync(path.join(UNITTEST_TEST_FILES_PATH, outputFileName), 'utf8'); - fs.writeFileSync(fileName, contents, 'utf8'); - callback({ out: '', source: 'stdout' }); - } - }); - } - - test('Run Tests', async () => { - await injectTestDiscoveryOutput('run.one.output'); - await injectTestRunOutput('run.one.result'); - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const results = await testManager.runTest(CommandSource.ui); - assert.equal(results.summary.errors, 1, 'Errors'); - assert.equal(results.summary.failures, 7, 'Failures'); - assert.equal(results.summary.passed, 6, 'Passed'); - assert.equal(results.summary.skipped, 2, 'skipped'); - }); - - test('Run Failed Tests', async () => { - await injectTestDiscoveryOutput('run.two.output'); - await injectTestRunOutput('run.two.result'); - await injectTestRunOutput('run.two.again.result', true); - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - let results = await testManager.runTest(CommandSource.ui); - assert.equal(results.summary.errors, 1, 'Errors'); - assert.equal(results.summary.failures, 7, 'Failures'); - assert.equal(results.summary.passed, 6, 'Passed'); - assert.equal(results.summary.skipped, 2, 'skipped'); - - results = await testManager.runTest(CommandSource.ui, undefined, true); - assert.equal(results.summary.errors, 1, 'Errors again'); - assert.equal(results.summary.failures, 7, 'Failures again'); - assert.equal(results.summary.passed, 0, 'Passed again'); - assert.equal(results.summary.skipped, 0, 'skipped again'); - }); - - test('Run Specific Test File', async () => { - await injectTestDiscoveryOutput('run.three.output'); - await injectTestRunOutput('run.three.result'); - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testFileToRun = tests.testFiles.find(t => t.fullPath.endsWith('test_root.py')); - assert.ok(testFileToRun, 'Test file not found'); - // tslint:disable-next-line:no-non-null-assertion - const testFile: TestsToRun = { testFile: [testFileToRun!], testFolder: [], testFunction: [], testSuite: [] }; - const results = await testManager.runTest(CommandSource.ui, testFile); - assert.equal(results.summary.errors, 0, 'Errors'); - assert.equal(results.summary.failures, 1, 'Failures'); - assert.equal(results.summary.passed, 1, 'Passed'); - assert.equal(results.summary.skipped, 1, 'skipped'); - }); - - test('Run Specific Test Suite', async () => { - await injectTestDiscoveryOutput('run.four.output'); - await injectTestRunOutput('run.four.result'); - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testSuiteToRun = tests.testSuites.find(s => s.xmlClassName === 'test_root.Test_Root_test1'); - assert.ok(testSuiteToRun, 'Test suite not found'); - // tslint:disable-next-line:no-non-null-assertion - const testSuite: TestsToRun = { testFile: [], testFolder: [], testFunction: [], testSuite: [testSuiteToRun!.testSuite] }; - const results = await testManager.runTest(CommandSource.ui, testSuite); - assert.equal(results.summary.errors, 0, 'Errors'); - assert.equal(results.summary.failures, 1, 'Failures'); - assert.equal(results.summary.passed, 1, 'Passed'); - assert.equal(results.summary.skipped, 1, 'skipped'); - }); - - test('Run Specific Test Function', async () => { - await injectTestDiscoveryOutput('run.five.output'); - await injectTestRunOutput('run.five.result'); - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testFnToRun = tests.testFunctions.find(f => f.xmlClassName === 'test_root.Test_Root_test1'); - assert.ok(testFnToRun, 'Test function not found'); - // tslint:disable-next-line:no-non-null-assertion - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [testFnToRun!.testFunction], testSuite: [] }; - const results = await testManager.runTest(CommandSource.ui, testFn); - assert.equal(results.summary.errors, 0, 'Errors'); - assert.equal(results.summary.failures, 1, 'Failures'); - assert.equal(results.summary.passed, 0, 'Passed'); - assert.equal(results.summary.skipped, 0, 'skipped'); - }); -}); diff --git a/src/test/testing/nosetest/nosetest.test.ts b/src/test/testing/nosetest/nosetest.test.ts deleted file mode 100644 index b601d8d5b81f..000000000000 --- a/src/test/testing/nosetest/nosetest.test.ts +++ /dev/null @@ -1,73 +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 { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { ITestManagerFactory } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { lookForTestFile } from '../helper'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; - -const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'noseFiles'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const filesToDelete = [ - path.join(UNITTEST_TEST_FILES_PATH, '.noseids'), - path.join(UNITTEST_SINGLE_TEST_FILE_PATH, '.noseids') -]; - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - nose - discovery against actual python process', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - - suiteSetup(async () => { - filesToDelete.forEach(file => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - await initialize(); - }); - suiteTeardown(async () => { - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - filesToDelete.forEach(file => { - if (fs.existsSync(file)) { - fs.unlinkSync(file); - } - }); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - ioc.serviceManager.addSingleton<ICondaService>(ICondaService, CondaService); - ioc.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); - } - - test('Discover Tests (single test file)', async () => { - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('nosetest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - lookForTestFile(tests, path.join('tests', 'test_one.py')); - }); -}); diff --git a/src/test/testing/pytest/pytest.argsService.unit.test.ts b/src/test/testing/pytest/pytest.argsService.unit.test.ts deleted file mode 100644 index 6f39bbeb2aa3..000000000000 --- a/src/test/testing/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/testing/common/argumentsHelper'; -import { ArgumentsService as PyTestArgumentsService } from '../../../client/testing/pytest/services/argsService'; -import { IArgumentsHelper } from '../../../client/testing/types'; - -suite('ArgsService: pytest', () => { - let argumentsService: PyTestArgumentsService; - - suiteSetup(() => { - const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - const logger = typeMoq.Mock.ofType<ILogger>(); - - 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/testing/pytest/pytest.discovery.test.ts b/src/test/testing/pytest/pytest.discovery.test.ts deleted file mode 100644 index 3f9e3dc021ad..000000000000 --- a/src/test/testing/pytest/pytest.discovery.test.ts +++ /dev/null @@ -1,800 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as vscode from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; -import { PythonExecutionService } from '../../../client/common/process/pythonProcess'; -import { ExecutionFactoryCreateWithEnvironmentOptions, IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -import { IConfigurationService } from '../../../client/common/types'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { ITestManagerFactory } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; -import { MockProcessService } from '../../mocks/proc'; -import { UnitTestIocContainer } from '../serviceRegistry'; - -const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); -const UNITTEST_TEST_FILES_PATH_WITH_CONFIGS = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'unitestsWithConfigs'); -const unitTestTestFilesCwdPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'cwd', 'src'); - -/* -These test results are from `/src/test/pythonFiles/testFiles/...` directories. -Run the command `python <ExtensionDir>/pythonFiles/testing_tools/run_adapter.py discover pytest -- -s --cache-clear` to get the JSON output. -*/ - -// tslint:disable:max-func-body-length -suite('Unit Tests - pytest - discovery with mocked process output', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - @injectable() - class ExecutionFactory extends PythonExecutionFactory { - constructor(@inject(IServiceContainer) private readonly _serviceContainer: IServiceContainer, - @inject(IEnvironmentActivationService) activationHelper: IEnvironmentActivationService, - @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, - @inject(IConfigurationService) private readonly _configService: IConfigurationService, - @inject(IBufferDecoder) decoder: IBufferDecoder) { - super(_serviceContainer, activationHelper, processServiceFactory, _configService, decoder); - } - public async createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise<IPythonExecutionService> { - const pythonPath = options.interpreter ? options.interpreter.path : this._configService.getSettings(options.resource).pythonPath; - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - return new PythonExecutionService(this._serviceContainer, procService, pythonPath); - } - } - suiteSetup(async () => { - await initialize(); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - ioc.serviceManager.rebind<IPythonExecutionFactory>(IPythonExecutionFactory, ExecutionFactory); - } - - async function injectTestDiscoveryOutput(output: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExec((_file, args, _options, callback) => { - if (args.indexOf('discover') >= 0 && args.indexOf('pytest') >= 0) { - callback({ - stdout: output - }); - } - }); - } - - test('Discover Tests (single test file)', async () => { - await injectTestDiscoveryOutput(JSON.stringify([ - { - rootid: '.', - root: '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/single', - parents: [ - { id: './test_root.py', kind: 'file', name: 'test_root.py', parentid: '.' }, - { id: './test_root.py::Test_Root_test1', kind: 'suite', name: 'Test_Root_test1', parentid: './test_root.py' }, - { id: './tests', kind: 'folder', name: 'tests', parentid: '.' }, - { id: './tests/test_one.py', kind: 'file', name: 'test_one.py', parentid: './tests' }, - { id: './tests/test_one.py::Test_test1', kind: 'suite', name: 'Test_test1', parentid: './tests/test_one.py' } - ], - tests: [ - { id: './test_root.py::Test_Root_test1::test_Root_A', name: 'test_Root_A', source: './test_root.py:6', markers: [], parentid: './test_root.py::Test_Root_test1' }, - { id: './test_root.py::Test_Root_test1::test_Root_B', name: 'test_Root_B', source: './test_root.py:9', markers: [], parentid: './test_root.py::Test_Root_test1' }, - { id: './test_root.py::Test_Root_test1::test_Root_c', name: 'test_Root_c', source: './test_root.py:12', markers: [], parentid: './test_root.py::Test_Root_test1' }, - { id: './tests/test_one.py::Test_test1::test_A', name: 'test_A', source: 'tests/test_one.py:6', markers: [], parentid: './tests/test_one.py::Test_test1' }, - { id: './tests/test_one.py::Test_test1::test_B', name: 'test_B', source: 'tests/test_one.py:9', markers: [], parentid: './tests/test_one.py::Test_test1' }, - { id: './tests/test_one.py::Test_test1::test_c', name: 'test_c', source: 'tests/test_one.py:12', markers: [], parentid: './tests/test_one.py::Test_test1' } - ] - }])); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const diagnosticCollectionUris: vscode.Uri[] = []; - testManager.diagnosticCollection.forEach(uri => { - diagnosticCollectionUris.push(uri); - }); - assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py'), true, 'Test File not found'); - }); - - test('Discover Tests (pattern = test_)', async () => { - await injectTestDiscoveryOutput(JSON.stringify([{ - rootid: '.', - root: '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard', - parents: [ - { - id: './test_root.py', - kind: 'file', - name: 'test_root.py', - parentid: '.' - }, - { - id: './test_root.py::Test_Root_test1', - kind: 'suite', - name: 'Test_Root_test1', - parentid: './test_root.py' - }, - { - id: './tests', - kind: 'folder', - name: 'tests', - parentid: '.' - }, - { - id: './tests/test_another_pytest.py', - kind: 'file', - name: 'test_another_pytest.py', - parentid: './tests' - }, - { - id: './tests/test_another_pytest.py::test_parametrized_username', - kind: 'function', - name: 'test_parametrized_username', - parentid: './tests/test_another_pytest.py' - }, - { - id: './tests/test_foreign_nested_tests.py', - kind: 'file', - name: 'test_foreign_nested_tests.py', - parentid: './tests' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests', - kind: 'suite', - name: 'TestNestedForeignTests', - parentid: './tests/test_foreign_nested_tests.py' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere', - kind: 'suite', - name: 'TestInheritingHere', - parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests', - kind: 'suite', - name: 'TestExtraNestedForeignTests', - parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere' - }, - { - id: './tests/test_pytest.py', - kind: 'file', - name: 'test_pytest.py', - parentid: './tests' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp', - kind: 'suite', - name: 'Test_CheckMyApp', - parentid: './tests/test_pytest.py' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA', - kind: 'suite', - name: 'Test_NestedClassA', - parentid: './tests/test_pytest.py::Test_CheckMyApp' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A', - kind: 'suite', - name: 'Test_nested_classB_Of_A', - parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' - }, - { - id: './tests/test_pytest.py::test_parametrized_username', - kind: 'function', - name: 'test_parametrized_username', - parentid: './tests/test_pytest.py' - }, - { - id: './tests/test_unittest_one.py', - kind: 'file', - name: 'test_unittest_one.py', - parentid: './tests' - }, - { - id: './tests/test_unittest_one.py::Test_test1', - kind: 'suite', - name: 'Test_test1', - parentid: './tests/test_unittest_one.py' - }, - { - id: './tests/test_unittest_two.py', - kind: 'file', - name: 'test_unittest_two.py', - parentid: './tests' - }, - { - id: './tests/test_unittest_two.py::Test_test2', - kind: 'suite', - name: 'Test_test2', - parentid: './tests/test_unittest_two.py' - }, - { - id: './tests/test_unittest_two.py::Test_test2a', - kind: 'suite', - name: 'Test_test2a', - parentid: './tests/test_unittest_two.py' - }, - { - id: './tests/unittest_three_test.py', - kind: 'file', - name: 'unittest_three_test.py', - parentid: './tests' - }, - { - id: './tests/unittest_three_test.py::Test_test3', - kind: 'suite', - name: 'Test_test3', - parentid: './tests/unittest_three_test.py' - } - ], - tests: [ - { - id: './test_root.py::Test_Root_test1::test_Root_A', - name: 'test_Root_A', - source: './test_root.py:6', - markers: [], - parentid: './test_root.py::Test_Root_test1' - }, - { - id: './test_root.py::Test_Root_test1::test_Root_B', - name: 'test_Root_B', - source: './test_root.py:9', - markers: [], - parentid: './test_root.py::Test_Root_test1' - }, - { - id: './test_root.py::Test_Root_test1::test_Root_c', - name: 'test_Root_c', - source: './test_root.py:12', - markers: [], - parentid: './test_root.py::Test_Root_test1' - }, - { - id: './tests/test_another_pytest.py::test_username', - name: 'test_username', - source: 'tests/test_another_pytest.py:12', - markers: [], - parentid: './tests/test_another_pytest.py' - }, - { - id: './tests/test_another_pytest.py::test_parametrized_username[one]', - name: 'test_parametrized_username[one]', - source: 'tests/test_another_pytest.py:15', - markers: [], - parentid: './tests/test_another_pytest.py::test_parametrized_username' - }, - { - id: './tests/test_another_pytest.py::test_parametrized_username[two]', - name: 'test_parametrized_username[two]', - source: 'tests/test_another_pytest.py:15', - markers: [], - parentid: './tests/test_another_pytest.py::test_parametrized_username' - }, - { - id: './tests/test_another_pytest.py::test_parametrized_username[three]', - name: 'test_parametrized_username[three]', - source: 'tests/test_another_pytest.py:15', - markers: [], - parentid: './tests/test_another_pytest.py::test_parametrized_username' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign', - name: 'test_super_deep_foreign', - source: 'tests/external.py:2', - markers: [], - parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test', - name: 'test_foreign_test', - source: 'tests/external.py:4', - markers: [], - parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal', - name: 'test_nested_normal', - source: 'tests/test_foreign_nested_tests.py:5', - markers: [], - parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere' - }, - { - id: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal', - name: 'test_normal', - source: 'tests/test_foreign_nested_tests.py:7', - markers: [], - parentid: './tests/test_foreign_nested_tests.py::TestNestedForeignTests' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check', - name: 'test_simple_check', - source: 'tests/test_pytest.py:6', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check', - name: 'test_complex_check', - source: 'tests/test_pytest.py:9', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB', - name: 'test_nested_class_methodB', - source: 'tests/test_pytest.py:13', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d', - name: 'test_d', - source: 'tests/test_pytest.py:16', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC', - name: 'test_nested_class_methodC', - source: 'tests/test_pytest.py:18', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check2', - name: 'test_simple_check2', - source: 'tests/test_pytest.py:21', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp' - }, - { - id: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check2', - name: 'test_complex_check2', - source: 'tests/test_pytest.py:23', - markers: [], - parentid: './tests/test_pytest.py::Test_CheckMyApp' - }, - { - id: './tests/test_pytest.py::test_username', - name: 'test_username', - source: 'tests/test_pytest.py:35', - markers: [], - parentid: './tests/test_pytest.py' - }, - { - id: './tests/test_pytest.py::test_parametrized_username[one]', - name: 'test_parametrized_username[one]', - source: 'tests/test_pytest.py:38', - markers: [], - parentid: './tests/test_pytest.py::test_parametrized_username' - }, - { - id: './tests/test_pytest.py::test_parametrized_username[two]', - name: 'test_parametrized_username[two]', - source: 'tests/test_pytest.py:38', - markers: [], - parentid: './tests/test_pytest.py::test_parametrized_username' - }, - { - id: './tests/test_pytest.py::test_parametrized_username[three]', - name: 'test_parametrized_username[three]', - source: 'tests/test_pytest.py:38', - markers: [], - parentid: './tests/test_pytest.py::test_parametrized_username' - }, - { - id: './tests/test_unittest_one.py::Test_test1::test_A', - name: 'test_A', - source: 'tests/test_unittest_one.py:6', - markers: [], - parentid: './tests/test_unittest_one.py::Test_test1' - }, - { - id: './tests/test_unittest_one.py::Test_test1::test_B', - name: 'test_B', - source: 'tests/test_unittest_one.py:9', - markers: [], - parentid: './tests/test_unittest_one.py::Test_test1' - }, - { - id: './tests/test_unittest_one.py::Test_test1::test_c', - name: 'test_c', - source: 'tests/test_unittest_one.py:12', - markers: [], - parentid: './tests/test_unittest_one.py::Test_test1' - }, - { - id: './tests/test_unittest_two.py::Test_test2::test_A2', - name: 'test_A2', - source: 'tests/test_unittest_two.py:3', - markers: [], - parentid: './tests/test_unittest_two.py::Test_test2' - }, - { - id: './tests/test_unittest_two.py::Test_test2::test_B2', - name: 'test_B2', - source: 'tests/test_unittest_two.py:6', - markers: [], - parentid: './tests/test_unittest_two.py::Test_test2' - }, - { - id: './tests/test_unittest_two.py::Test_test2::test_C2', - name: 'test_C2', - source: 'tests/test_unittest_two.py:9', - markers: [], - parentid: './tests/test_unittest_two.py::Test_test2' - }, - { - id: './tests/test_unittest_two.py::Test_test2::test_D2', - name: 'test_D2', - source: 'tests/test_unittest_two.py:12', - markers: [], - parentid: './tests/test_unittest_two.py::Test_test2' - }, - { - id: './tests/test_unittest_two.py::Test_test2a::test_222A2', - name: 'test_222A2', - source: 'tests/test_unittest_two.py:17', - markers: [], - parentid: './tests/test_unittest_two.py::Test_test2a' - }, - { - id: './tests/test_unittest_two.py::Test_test2a::test_222B2', - name: 'test_222B2', - source: 'tests/test_unittest_two.py:20', - markers: [], - parentid: './tests/test_unittest_two.py::Test_test2a' - }, - { - id: './tests/unittest_three_test.py::Test_test3::test_A', - name: 'test_A', - source: 'tests/unittest_three_test.py:4', - markers: [], - parentid: './tests/unittest_three_test.py::Test_test3' - }, - { - id: './tests/unittest_three_test.py::Test_test3::test_B', - name: 'test_B', - source: 'tests/unittest_three_test.py:7', - markers: [], - parentid: './tests/unittest_three_test.py::Test_test3' - } - ] - } - ])); - await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const diagnosticCollectionUris: vscode.Uri[] = []; - testManager.diagnosticCollection.forEach(uri => { - diagnosticCollectionUris.push(uri); - }); - assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); - assert.equal(tests.testFiles.length, 7, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 33, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 11, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_foreign_nested_tests.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_two.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_pytest.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_another_pytest.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py'), true, 'Test File not found'); - }); - - test('Discover Tests (pattern = _test)', async () => { - await injectTestDiscoveryOutput(JSON.stringify([ - { - rootid: '.', - root: '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/standard', - parents: [ - { - id: './tests', - kind: 'folder', - name: 'tests', - parentid: '.' - }, - { - id: './tests/unittest_three_test.py', - kind: 'file', - name: 'unittest_three_test.py', - parentid: './tests' - }, - { - id: './tests/unittest_three_test.py::Test_test3', - kind: 'suite', - name: 'Test_test3', - parentid: './tests/unittest_three_test.py' - } - ], - tests: [ - { - id: './tests/unittest_three_test.py::Test_test3::test_A', - name: 'test_A', - source: 'tests/unittest_three_test.py:4', - markers: [], - parentid: './tests/unittest_three_test.py::Test_test3' - }, - { - id: './tests/unittest_three_test.py::Test_test3::test_B', - name: 'test_B', - source: 'tests/unittest_three_test.py:7', - markers: [], - parentid: './tests/unittest_three_test.py::Test_test3' - } - ] - } - ] - )); - await updateSetting('testing.pytestArgs', ['-k=_test.py'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const diagnosticCollectionUris: vscode.Uri[] = []; - testManager.diagnosticCollection.forEach(uri => { - diagnosticCollectionUris.push(uri); - }); - assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py'), true, 'Test File not found'); - }); - - test('Discover Tests (with config)', async () => { - await injectTestDiscoveryOutput(JSON.stringify([ - { - rootid: '.', - root: '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/unitestsWithConfigs', - parents: [ - { - id: './other', - kind: 'folder', - name: 'other', - parentid: '.' - }, - { - id: './other/test_pytest.py', - kind: 'file', - name: 'test_pytest.py', - parentid: './other' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp', - kind: 'suite', - name: 'Test_CheckMyApp', - parentid: './other/test_pytest.py' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA', - kind: 'suite', - name: 'Test_NestedClassA', - parentid: './other/test_pytest.py::Test_CheckMyApp' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A', - kind: 'suite', - name: 'Test_nested_classB_Of_A', - parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' - }, - { - id: './other/test_pytest.py::test_parametrized_username', - kind: 'function', - name: 'test_parametrized_username', - parentid: './other/test_pytest.py' - }, - { - id: './other/test_unittest_one.py', - kind: 'file', - name: 'test_unittest_one.py', - parentid: './other' - }, - { - id: './other/test_unittest_one.py::Test_test1', - kind: 'suite', - name: 'Test_test1', - parentid: './other/test_unittest_one.py' - } - ], - tests: [ - { - id: './other/test_pytest.py::Test_CheckMyApp::test_simple_check', - name: 'test_simple_check', - source: 'other/test_pytest.py:6', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::test_complex_check', - name: 'test_complex_check', - source: 'other/test_pytest.py:9', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB', - name: 'test_nested_class_methodB', - source: 'other/test_pytest.py:13', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d', - name: 'test_d', - source: 'other/test_pytest.py:16', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC', - name: 'test_nested_class_methodC', - source: 'other/test_pytest.py:18', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp::Test_NestedClassA' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::test_simple_check2', - name: 'test_simple_check2', - source: 'other/test_pytest.py:21', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp' - }, - { - id: './other/test_pytest.py::Test_CheckMyApp::test_complex_check2', - name: 'test_complex_check2', - source: 'other/test_pytest.py:23', - markers: [], - parentid: './other/test_pytest.py::Test_CheckMyApp' - }, - { - id: './other/test_pytest.py::test_username', - name: 'test_username', - source: 'other/test_pytest.py:35', - markers: [], - parentid: './other/test_pytest.py' - }, - { - id: './other/test_pytest.py::test_parametrized_username[one]', - name: 'test_parametrized_username[one]', - source: 'other/test_pytest.py:38', - markers: [], - parentid: './other/test_pytest.py::test_parametrized_username' - }, - { - id: './other/test_pytest.py::test_parametrized_username[two]', - name: 'test_parametrized_username[two]', - source: 'other/test_pytest.py:38', - markers: [], - parentid: './other/test_pytest.py::test_parametrized_username' - }, - { - id: './other/test_pytest.py::test_parametrized_username[three]', - name: 'test_parametrized_username[three]', - source: 'other/test_pytest.py:38', - markers: [], - parentid: './other/test_pytest.py::test_parametrized_username' - }, - { - id: './other/test_unittest_one.py::Test_test1::test_A', - name: 'test_A', - source: 'other/test_unittest_one.py:6', - markers: [], - parentid: './other/test_unittest_one.py::Test_test1' - }, - { - id: './other/test_unittest_one.py::Test_test1::test_B', - name: 'test_B', - source: 'other/test_unittest_one.py:9', - markers: [], - parentid: './other/test_unittest_one.py::Test_test1' - }, - { - id: './other/test_unittest_one.py::Test_test1::test_c', - name: 'test_c', - source: 'other/test_unittest_one.py:12', - markers: [], - parentid: './other/test_unittest_one.py::Test_test1' - } - ] - } - ] - )); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH_WITH_CONFIGS); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const diagnosticCollectionUris: vscode.Uri[] = []; - testManager.diagnosticCollection.forEach(uri => { - diagnosticCollectionUris.push(uri); - }); - assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 14, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 4, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_pytest.py'), true, 'Test File not found'); - }); - - test('Setting cwd should return tests', async () => { - await injectTestDiscoveryOutput(JSON.stringify([ - { - rootid: '.', - root: '/Users/donjayamanne/.vscode-insiders/extensions/pythonVSCode/src/test/pythonFiles/testFiles/cwd/src', - parents: [ - { - id: './tests', - kind: 'folder', - name: 'tests', - parentid: '.' - }, - { - id: './tests/test_cwd.py', - kind: 'file', - name: 'test_cwd.py', - parentid: './tests' - }, - { - id: './tests/test_cwd.py::Test_Current_Working_Directory', - kind: 'suite', - name: 'Test_Current_Working_Directory', - parentid: './tests/test_cwd.py' - } - ], - tests: [ - { - id: './tests/test_cwd.py::Test_Current_Working_Directory::test_cwd', - name: 'test_cwd', - source: 'tests/test_cwd.py:6', - markers: [], - parentid: './tests/test_cwd.py::Test_Current_Working_Directory' - } - ] - } - ])); - await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('pytest', rootWorkspaceUri!, unitTestTestFilesCwdPath); - - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const diagnosticCollectionUris: vscode.Uri[] = []; - testManager.diagnosticCollection.forEach(uri => { - diagnosticCollectionUris.push(uri); - }); - assert.equal(diagnosticCollectionUris.length, 0, 'Should not have diagnostics yet'); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFolders.length, 2, 'Incorrect number of test folders'); - assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - }); -}); diff --git a/src/test/testing/pytest/pytest.run.test.ts b/src/test/testing/pytest/pytest.run.test.ts deleted file mode 100644 index 7240fba8b4e9..000000000000 --- a/src/test/testing/pytest/pytest.run.test.ts +++ /dev/null @@ -1,472 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as fs from 'fs'; -import { inject, injectable } from 'inversify'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as vscode from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IFileSystem } from '../../../client/common/platform/types'; -import { PythonExecutionFactory } from '../../../client/common/process/pythonExecutionFactory'; -import { PythonExecutionService } from '../../../client/common/process/pythonProcess'; -import { ExecutionFactoryCreateWithEnvironmentOptions, IBufferDecoder, IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from '../../../client/common/process/types'; -import { IConfigurationService } from '../../../client/common/types'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { UnitTestDiagnosticService } from '../../../client/testing/common/services/unitTestDiagnosticService'; -import { FlattenedTestFunction, ITestManager, ITestManagerFactory, Tests, TestStatus, TestsToRun } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { 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<TestsToRun> { - const generateTestsToRun = (scenario.testSuiteIndex || scenario.testFunctionIndex); - if (scenario.testsToRun === undefined && generateTestsToRun) { - scenario.testsToRun = { - testFolder: [], - testFile: [], - testSuite: [], - testFunction: [] - }; - if (scenario.testSuiteIndex) { - scenario.testsToRun.testSuite!.push(tests.testSuites[scenario.testSuiteIndex].testSuite); - } - if (scenario.testFunctionIndex) { - scenario.testsToRun.testFunction!.push(tests.testSuites[scenario.testFunctionIndex].testSuite); - } - } - return scenario.testsToRun!; -} - -/** - * Run the tests and return the results. - * - * In the case of a failed test run, some test details can be marked through the passOnFailedRun property to pass on a - * failed run. This is meant to simulate a test or the thing it's meant to test being fixed. - * - * @param testManager The test manager used to run the tests. - * @param testsToRun The tests that the test manager should run. - * @param failedRun Whether or not the current test run is for failed tests from a previous run. - */ -async function getResultsFromTestManagerRunTest(testManager: ITestManager, testsToRun: TestsToRun, failedRun: boolean = false): Promise<Tests> { - if (failedRun) { - return testManager.runTest(CommandSource.ui, undefined, true); - } else { - return testManager.runTest(CommandSource.ui, testsToRun); - } -} - -/** - * Get the number of passes/skips/failures/errors for a test run based on the test details for a scenario. - * - * In the case of a failed test run, some test details can be marked through the passOnFailedRun property to pass on a - * failed run. This is meant to simulate a test or the thing it's meant to test being fixed. - * - * @param testDetails All the test details for a scenario. - * @param failedRun Whether or not the current test run is for failed tests from a previous run. - */ -function getExpectedSummaryCount(testDetails: ITestDetails[], failedRun: boolean): IResultsSummaryCount { - const summaryCount: IResultsSummaryCount = { - passes: 0, - skips: 0, - failures: 0, - errors: 0 - }; - testDetails.forEach(td => { - let tStatus = td.status; - if (failedRun && td.passOnFailedRun) { - tStatus = TestStatus.Pass; - } - switch (tStatus) { - case TestStatus.Pass: { - summaryCount.passes += 1; - break; - } - case TestStatus.Skipped: { - summaryCount.skips += 1; - break; - } - case TestStatus.Fail: { - summaryCount.failures += 1; - break; - } - case TestStatus.Error: { - summaryCount.errors += 1; - break; - } - default: { - throw Error('Unsupported TestStatus'); - } - } - }); - return summaryCount; -} - -/** - * Get all the test details associated with a file. - * - * @param testDetails All the test details for a scenario. - * @param fileName The name of the file to find test details for. - */ -function getRelevantTestDetailsForFile(testDetails: ITestDetails[], fileName: string): ITestDetails[] { - return testDetails.filter(td => { - return td.fileName === fileName; - }); -} - -/** - * Every failed/skipped test in a file should should have an associated Diagnostic for it. This calculates and returns the - * expected number of Diagnostics based on the expected test details for that file. In the event of a normal test run, - * skipped tests will be included in the results, and thus will be included in the testDetails argument. But if it's a - * failed test run, skipped tests will not be attempted again, so they will not be included in the testDetails argument. - * - * In the case of a failed test run, some test details can be marked through the passOnFailedRun property to pass on a - * failed run. This is meant to simulate a test or the thing it's meant to test being fixed. - * - * @param testDetails All the test details for a file for the tests that were run. - * @param skippedTestDetails All the test details for skipped tests for a file. - * @param failedRun Whether or not the current test run is for failed tests from a previous run. - */ -function getIssueCountFromRelevantTestDetails(testDetails: ITestDetails[], skippedTestDetails: ITestDetails[], failedRun: boolean = false): number { - const relevantIssueDetails = testDetails.filter(td => { - return td.status !== TestStatus.Pass && !(failedRun && td.passOnFailedRun); - }); - // If it's a failed run, the skipped tests won't be included in testDetails, but should still be included as they still aren't passing. - return relevantIssueDetails.length + (failedRun ? skippedTestDetails.length : 0); -} - -/** - * Get the Diagnostic associated with the FlattenedTestFunction. - * - * @param diagnostics The array of Diagnostics for a file. - * @param testFunc The FlattenedTestFunction to find the Diagnostic for. - */ -function getDiagnosticForTestFunc(diagnostics: vscode.Diagnostic[], testFunc: FlattenedTestFunction): vscode.Diagnostic { - return diagnostics.find(diag => { - return testFunc.testFunction.nameToRun === diag.code; - })!; -} - -/** - * Get a list of all the unique files found in a given testDetails array. - * - * @param testDetails All the test details for a scenario. - */ -function getUniqueIssueFilesFromTestDetails(testDetails: ITestDetails[]): string[] { - return testDetails.reduce<string[]>((filtered, issue) => { - if (filtered.indexOf(issue.fileName) === -1 && issue.fileName !== undefined) { - filtered.push(issue.fileName); - } - return filtered; - }, []); -} - -/** - * Of all the test details that were run for a scenario, given a file location, get all those that were skipped. - * - * @param testDetails All test details that should have been run for the scenario. - * @param fileName The location of a file that had tests run. - */ -function getRelevantSkippedIssuesFromTestDetailsForFile(testDetails: ITestDetails[], fileName: string): ITestDetails[] { - return testDetails.filter(td => { - return td.fileName === fileName && td.status === TestStatus.Skipped; - }); -} - -/** - * Get the FlattenedTestFunction from the test results that's associated with the given testDetails object. - * - * @param results Results of the test run. - * @param testFileUri The Uri of the test file that was run. - * @param testDetails The details of a particular test. - */ -function getTestFuncFromResultsByTestFileAndName(ioc: UnitTestIocContainer, results: Tests, testFileUri: vscode.Uri, testDetails: ITestDetails): FlattenedTestFunction { - const fileSystem = ioc.serviceContainer.get<IFileSystem>(IFileSystem); - return results.testFunctions.find(test => { - return fileSystem.arePathsSame(vscode.Uri.file(test.parentTestFile.fullPath).fsPath, testFileUri.fsPath) && test.testFunction.name === testDetails.testName; - })!; -} - -/** - * Generate a Diagnostic object (including DiagnosticRelatedInformation) using the provided test details that reflects - * what the Diagnostic for the associated test should be in order for it to be compared to by the actual Diagnostic - * for the test. - * - * @param testDetails Test details for a specific test. - */ -async function getExpectedDiagnosticFromTestDetails(testDetails: ITestDetails): Promise<vscode.Diagnostic> { - const relatedInfo: vscode.DiagnosticRelatedInformation[] = []; - const testFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.fileName); - const testFileUri = vscode.Uri.file(testFilePath); - let expectedSourceTestFilePath = testFilePath; - if (testDetails.imported) { - expectedSourceTestFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.sourceFileName!); - } - const expectedSourceTestFileUri = vscode.Uri.file(expectedSourceTestFilePath); - const diagMsgPrefix = new UnitTestDiagnosticService().getMessagePrefix(testDetails.status); - const expectedDiagMsg = `${diagMsgPrefix ? `${diagMsgPrefix}: ` : ''}${testDetails.message}`; - let expectedDiagRange = testDetails.testDefRange; - let expectedSeverity = vscode.DiagnosticSeverity.Error; - if (testDetails.status === TestStatus.Skipped) { - // Stack should stop at the test definition line. - expectedSeverity = vscode.DiagnosticSeverity.Information; - } - if (testDetails.imported) { - // Stack should include the class furthest down the chain from the file that was executed. - relatedInfo.push( - new vscode.DiagnosticRelatedInformation( - new vscode.Location(testFileUri, testDetails.classDefRange!), - testDetails.simpleClassName! - ) - ); - expectedDiagRange = testDetails.classDefRange; - } - relatedInfo.push( - new vscode.DiagnosticRelatedInformation( - new vscode.Location(expectedSourceTestFileUri, testDetails.testDefRange!), - testDetails.sourceTestName - ) - ); - if (testDetails.status !== TestStatus.Skipped) { - relatedInfo.push( - new vscode.DiagnosticRelatedInformation( - new vscode.Location(expectedSourceTestFileUri, testDetails.issueRange!), - testDetails.issueLineText! - ) - ); - } else { - expectedSeverity = vscode.DiagnosticSeverity.Information; - } - - const expectedDiagnostic = new vscode.Diagnostic(expectedDiagRange!, expectedDiagMsg, expectedSeverity); - expectedDiagnostic.source = 'pytest'; - expectedDiagnostic.code = testDetails.nameToRun; - expectedDiagnostic.relatedInformation = relatedInfo; - return expectedDiagnostic; -} - -async function testResultsSummary(results: Tests, expectedSummaryCount: IResultsSummaryCount) { - const totalTests = results.summary.passed + results.summary.skipped + results.summary.failures + results.summary.errors; - assert.notEqual(totalTests, 0); - assert.equal(results.summary.passed, expectedSummaryCount.passes, 'Passed'); - assert.equal(results.summary.skipped, expectedSummaryCount.skips, 'Skipped'); - assert.equal(results.summary.failures, expectedSummaryCount.failures, 'Failures'); - assert.equal(results.summary.errors, expectedSummaryCount.errors, 'Errors'); -} - -async function testDiagnostic(diagnostic: vscode.Diagnostic, expectedDiagnostic: vscode.Diagnostic) { - assert.equal(diagnostic.code, expectedDiagnostic.code, 'Diagnostic code'); - assert.equal(diagnostic.message, expectedDiagnostic.message, 'Diagnostic message'); - assert.equal(diagnostic.severity, expectedDiagnostic.severity, 'Diagnostic severity'); - assert.equal(diagnostic.range.start.line, expectedDiagnostic.range.start.line, 'Diagnostic range start line'); - assert.equal(diagnostic.range.start.character, expectedDiagnostic.range.start.character, 'Diagnostic range start character'); - assert.equal(diagnostic.range.end.line, expectedDiagnostic.range.end.line, 'Diagnostic range end line'); - assert.equal(diagnostic.range.end.character, expectedDiagnostic.range.end.character, 'Diagnostic range end character'); - assert.equal(diagnostic.source, expectedDiagnostic.source, 'Diagnostic source'); - assert.equal(diagnostic.relatedInformation!.length, expectedDiagnostic.relatedInformation!.length, 'DiagnosticRelatedInformation count'); -} - -async function testDiagnosticRelatedInformation(relatedInfo: vscode.DiagnosticRelatedInformation, expectedRelatedInfo: vscode.DiagnosticRelatedInformation) { - assert.equal(relatedInfo.message, expectedRelatedInfo.message, 'DiagnosticRelatedInfo definition'); - assert.equal(relatedInfo.location.range.start.line, expectedRelatedInfo.location.range.start.line, 'DiagnosticRelatedInfo definition range start line'); - assert.equal(relatedInfo.location.range.start.character, expectedRelatedInfo.location.range.start.character, 'DiagnosticRelatedInfo definition range start character'); - assert.equal(relatedInfo.location.range.end.line, expectedRelatedInfo.location.range.end.line, 'DiagnosticRelatedInfo definition range end line'); - assert.equal(relatedInfo.location.range.end.character, expectedRelatedInfo.location.range.end.character, 'DiagnosticRelatedInfo definition range end character'); -} - -// 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; - @injectable() - class ExecutionFactory extends PythonExecutionFactory { - constructor(@inject(IServiceContainer) private readonly _serviceContainer: IServiceContainer, - @inject(IEnvironmentActivationService) activationHelper: IEnvironmentActivationService, - @inject(IProcessServiceFactory) processServiceFactory: IProcessServiceFactory, - @inject(IConfigurationService) private readonly _configService: IConfigurationService, - @inject(IBufferDecoder) decoder: IBufferDecoder) { - super(_serviceContainer, activationHelper, processServiceFactory, _configService, decoder); - } - public async createActivatedEnvironment(options: ExecutionFactoryCreateWithEnvironmentOptions): Promise<IPythonExecutionService> { - const pythonPath = options.interpreter ? options.interpreter.path : this._configService.getSettings(options.resource).pythonPath; - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - return new PythonExecutionService(this._serviceContainer, procService, pythonPath); - } - } - suiteSetup(async () => { - await initialize(); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - // Mocks. - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - ioc.serviceManager.rebind<IPythonExecutionFactory>(IPythonExecutionFactory, ExecutionFactory); - } - - async function injectTestDiscoveryOutput(outputFileName: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExec((_file, args, _options, callback) => { - if (args.indexOf('discover') >= 0 && args.indexOf('pytest') >= 0) { - let stdout = fs.readFileSync(path.join(PYTEST_RESULTS_PATH, outputFileName), 'utf8'); - stdout = stdout.replace(/\/Users\/donjayamanne\/.vscode-insiders\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles/g, path.dirname(UNITTEST_TEST_FILES_PATH)) - stdout = stdout.replace(/\\/g, '/'); - callback({ stdout }); - } - }); - } - async function injectTestRunOutput(outputFileName: string, failedOutput: boolean = false) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (failedOutput && args.indexOf('--last-failed') === -1) { - return; - } - const index = args.findIndex(arg => arg.startsWith('--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('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_TEST_FILES_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - scenario.testsToRun = await getScenarioTestsToRun(scenario, tests); - }); - suiteTeardown(async () => { - await ioc.dispose(); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - const shouldRunProperly = (suiteName: string, failedRun = false) => { - suite(suiteName, () => { - testDetails = getScenarioTestDetails(scenario, failedRun); - const uniqueIssueFiles = getUniqueIssueFilesFromTestDetails(testDetails); - let expectedSummaryCount: IResultsSummaryCount; - suiteSetup(async () => { - testDetails = getScenarioTestDetails(scenario, failedRun); - results = await getResultsFromTestManagerRunTest(testManager, scenario.testsToRun!, failedRun); - expectedSummaryCount = getExpectedSummaryCount(testDetails, failedRun); - }); - test('Test results summary', async () => { await testResultsSummary(results, expectedSummaryCount); }); - uniqueIssueFiles.forEach(fileName => { - suite(fileName, () => { - let testFileUri: vscode.Uri; - const relevantTestDetails = getRelevantTestDetailsForFile(testDetails, fileName); - const relevantSkippedIssues = getRelevantSkippedIssuesFromTestDetailsForFile(scenario.testDetails!, fileName); - suiteSetup(async () => { - testFileUri = vscode.Uri.file(path.join(UNITTEST_TEST_FILES_PATH, fileName)); - diagnostics = testManager.diagnosticCollection.get(testFileUri)!; - getIssueCountFromRelevantTestDetails(relevantTestDetails, relevantSkippedIssues, failedRun); - }); - // test('Test DiagnosticCollection', async () => { assert.equal(diagnostics.length, expectedDiagnosticCount, 'Diagnostics count'); }); - const validateTestFunctionAndDiagnostics = (td: ITestDetails) => { - suite(td.testName, () => { - let testFunc: FlattenedTestFunction; - let expectedStatus: TestStatus; - let diagnostic: vscode.Diagnostic; - let expectedDiagnostic: vscode.Diagnostic; - suiteSetup(async () => { - testFunc = getTestFuncFromResultsByTestFileAndName(ioc, results, testFileUri, td)!; - expectedStatus = (failedRun && td.passOnFailedRun) ? TestStatus.Pass : td.status; - }); - suite('TestFunction', async () => { - test('Status', async () => { - assert.equal(testFunc.testFunction.status, expectedStatus, 'Test status'); - }); - }); - if (td.status !== TestStatus.Pass && !(failedRun && td.passOnFailedRun)) { - suite('Diagnostic', async () => { - suiteSetup(async () => { - diagnostic = getDiagnosticForTestFunc(diagnostics, testFunc)!; - expectedDiagnostic = await getExpectedDiagnosticFromTestDetails(td); - }); - test('Test Diagnostic', async () => { await testDiagnostic(diagnostic, expectedDiagnostic); }); - suite('Test DiagnosticRelatedInformation', async () => { - if (td.imported) { - test('Class Definition', async () => { - await testDiagnosticRelatedInformation(diagnostic.relatedInformation![0], expectedDiagnostic.relatedInformation![0]); - }); - } - test('Test Function Definition', async () => { - await testDiagnosticRelatedInformation(diagnostic.relatedInformation![(td.imported ? 1 : 0)], expectedDiagnostic.relatedInformation![(td.imported ? 1 : 0)]); - }); - if (td.status !== TestStatus.Skipped) { - test('Failure Line', async () => { - await testDiagnosticRelatedInformation(diagnostic.relatedInformation![(td.imported ? 1 : 0) + 1], expectedDiagnostic.relatedInformation![(td.imported ? 1 : 0) + 1]); - }); - } - }); - }); - } - }); - }; - relevantTestDetails.forEach((td: ITestDetails) => { validateTestFunctionAndDiagnostics(td); }); - if (failedRun) { - relevantSkippedIssues.forEach((td: ITestDetails) => { - validateTestFunctionAndDiagnostics(td); - }); - } - }); - }); - }); - }; - shouldRunProperly('Run'); - if (scenario.shouldRunFailed) { shouldRunProperly('Run Failed', true); } - }); - }); -}); diff --git a/src/test/testing/pytest/pytest.test.ts b/src/test/testing/pytest/pytest.test.ts deleted file mode 100644 index e580d22b8824..000000000000 --- a/src/test/testing/pytest/pytest.test.ts +++ /dev/null @@ -1,53 +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 { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { ITestManagerFactory } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; - -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'single'); - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - pytest - discovery against actual python process', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? vscode.ConfigurationTarget.WorkspaceFolder : vscode.ConfigurationTarget.Workspace; - suiteSetup(async () => { - await initialize(); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - ioc.serviceManager.addSingleton<ICondaService>(ICondaService, CondaService); - ioc.serviceManager.addSingleton<IInterpreterService>(IInterpreterService, InterpreterService); - } - - test('Discover Tests (single test file)', async () => { - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('pytest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 6, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py'), true, 'Test File not found'); - assert.equal(tests.testFiles.some(t => t.name === 'test_root.py'), true, 'Test File not found'); - }); -}); diff --git a/src/test/testing/pytest/pytest.testMessageService.test.ts b/src/test/testing/pytest/pytest.testMessageService.test.ts deleted file mode 100644 index 69be5d7d624d..000000000000 --- a/src/test/testing/pytest/pytest.testMessageService.test.ts +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import { assert } from 'chai'; -import * as fs from 'fs'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import * as typeMoq from 'typemoq'; -import * as vscode from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { ProductNames } from '../../../client/common/installer/productNames'; -import { Product } from '../../../client/common/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { TestDiscoveredTestParser } from '../../../client/testing/common/services/discoveredTestParser'; -import { TestResultsService } from '../../../client/testing/common/services/testResultsService'; -import { DiscoveredTests } from '../../../client/testing/common/services/types'; -import { ITestVisitor, PassCalculationFormulae, TestDiscoveryOptions, Tests, TestStatus } from '../../../client/testing/common/types'; -import { XUnitParser } from '../../../client/testing/common/xUnitParser'; -import { TestMessageService } from '../../../client/testing/pytest/services/testMessageService'; -import { ILocationStackFrameDetails, IPythonTestMessage, PythonTestMessageSeverity } from '../../../client/testing/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../../initialize'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { ITestDetails, testScenarios } from './pytest_run_tests_data'; - -const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); -const PYTEST_RESULTS_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'pytestFiles', 'results'); - -const filterdTestScenarios = testScenarios.filter((ts) => { return !ts.shouldRunFailed; }); - -async function testMessageProperties(message: IPythonTestMessage, expectedMessage: IPythonTestMessage, imported: boolean = false, status: TestStatus) { - assert.equal(message.code, expectedMessage.code, 'IPythonTestMessage code'); - assert.equal(message.message, expectedMessage.message, 'IPythonTestMessage message'); - assert.equal(message.severity, expectedMessage.severity, 'IPythonTestMessage severity'); - assert.equal(message.provider, expectedMessage.provider, 'IPythonTestMessage provider'); - assert.isNumber(message.testTime, 'IPythonTestMessage testTime'); - assert.equal(message.status, expectedMessage.status, 'IPythonTestMessage status'); - assert.equal(message.testFilePath, expectedMessage.testFilePath, 'IPythonTestMessage testFilePath'); - if (status !== TestStatus.Pass) { - assert.equal(message.locationStack![0].lineText, expectedMessage.locationStack![0].lineText, 'IPythonTestMessage line text'); - assert.equal(message.locationStack![0].location.uri.fsPath, expectedMessage.locationStack![0].location.uri.fsPath, 'IPythonTestMessage locationStack fsPath'); - if (status !== TestStatus.Skipped) { - assert.equal(message.locationStack![1].lineText, expectedMessage.locationStack![1].lineText, 'IPythonTestMessage line text'); - assert.equal(message.locationStack![1].location.uri.fsPath, expectedMessage.locationStack![1].location.uri.fsPath, 'IPythonTestMessage locationStack fsPath'); - } - if (imported) { - assert.equal(message.locationStack![2].lineText, expectedMessage.locationStack![2].lineText, 'IPythonTestMessage imported line text'); - assert.equal(message.locationStack![2].location.uri.fsPath, expectedMessage.locationStack![2].location.uri.fsPath, 'IPythonTestMessage imported location fsPath'); - } - } -} - -/** - * Generate a Diagnostic object (including DiagnosticRelatedInformation) using the provided test details that reflects - * what the Diagnostic for the associated test should be in order for it to be compared to by the actual Diagnostic - * for the test. - * - * @param testDetails Test details for a specific test. - */ -async function getExpectedLocationStackFromTestDetails(testDetails: ITestDetails): Promise<ILocationStackFrameDetails[]> { - const locationStack: ILocationStackFrameDetails[] = []; - const testFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.fileName); - const testFileUri = vscode.Uri.file(testFilePath); - let expectedSourceTestFilePath = testFilePath; - if (testDetails.imported) { - expectedSourceTestFilePath = path.join(UNITTEST_TEST_FILES_PATH, testDetails.sourceFileName!); - } - const expectedSourceTestFileUri = vscode.Uri.file(expectedSourceTestFilePath); - if (testDetails.imported) { - // Stack should include the class furthest down the chain from the file that was executed. - locationStack.push( - { - location: new vscode.Location(testFileUri, testDetails.classDefRange!), - lineText: testDetails.simpleClassName! - } - ); - } - locationStack.push( - { - location: new vscode.Location(expectedSourceTestFileUri, testDetails.testDefRange!), - lineText: testDetails.sourceTestName - } - ); - if (testDetails.status !== TestStatus.Skipped) { - locationStack.push( - { - location: new vscode.Location(expectedSourceTestFileUri, testDetails.issueRange!), - lineText: testDetails.issueLineText! - } - ); - } - return locationStack; -} - -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('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerUnitTestTypes(); - ioc.registerVariableTypes(); - // Mocks. - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - // Build tests for the test data that is relevant for this platform. - filterdTestScenarios.forEach((scenario) => { - suite(scenario.scenarioName, async () => { - let testMessages: IPythonTestMessage[]; - suiteSetup(async () => { - await initializeTest(); - initializeDI(); - // Setup the service container for use by the parser. - const testVisitor = typeMoq.Mock.ofType<ITestVisitor>(); - const outChannel = typeMoq.Mock.ofType<vscode.OutputChannel>(); - const cancelToken = typeMoq.Mock.ofType<vscode.CancellationToken>(); - cancelToken.setup(c => c.isCancellationRequested).returns(() => false); - const options: TestDiscoveryOptions = { - args: [], - cwd: UNITTEST_TEST_FILES_PATH, - ignoreCache: true, - outChannel: outChannel.object, - token: cancelToken.object, - workspaceFolder: vscode.Uri.file(__dirname) - }; - // Setup the parser. - const workspaceService = ioc.serviceContainer.get<IWorkspaceService>(IWorkspaceService); - const parser = new TestDiscoveredTestParser(workspaceService); - const discoveryOutput = fs.readFileSync(path.join(PYTEST_RESULTS_PATH, scenario.discoveryOutput), 'utf8').replace(/\/Users\/donjayamanne\/.vscode-insiders\/extensions\/pythonVSCode\/src\/test\/pythonFiles\/testFiles/g, path.dirname(UNITTEST_TEST_FILES_PATH)).replace(/\\/g, '/'); - const discoveredTest: DiscoveredTests[] = JSON.parse(discoveryOutput); - options.workspaceFolder = vscode.Uri.file(discoveredTest[0].root); - const parsedTests: Tests = parser.parse(options.workspaceFolder, discoveredTest); - const xUnitParser = new XUnitParser(); - 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('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - }); - scenario.testDetails!.forEach((td) => { - suite(td.nameToRun, () => { - let testMessage: IPythonTestMessage; - let expectedMessage: IPythonTestMessage; - suiteSetup(async () => { - let expectedSeverity: PythonTestMessageSeverity; - if (td.status === TestStatus.Error || td.status === TestStatus.Fail) { - expectedSeverity = PythonTestMessageSeverity.Error; - } else if (td.status === TestStatus.Skipped) { - expectedSeverity = PythonTestMessageSeverity.Skip; - } else { - expectedSeverity = PythonTestMessageSeverity.Pass; - } - const expectedLocationStack = await getExpectedLocationStackFromTestDetails(td); - expectedMessage = { - code: td.nameToRun, - message: td.message, - severity: expectedSeverity, - provider: ProductNames.get(Product.pytest)!, - testTime: 0, - status: td.status, - locationStack: expectedLocationStack, - testFilePath: path.join(UNITTEST_TEST_FILES_PATH, td.fileName) - }; - testMessage = testMessages.find(tm => tm.code === td.nameToRun)!; - }); - test('Message', async () => { - await testMessageProperties(testMessage, expectedMessage, td.imported, td.status); - }); - }); - }); - }); - }); -}); diff --git a/src/test/testing/pytest/pytest_run_tests_data.ts b/src/test/testing/pytest/pytest_run_tests_data.ts deleted file mode 100644 index abebd0fe80a9..000000000000 --- a/src/test/testing/pytest/pytest_run_tests_data.ts +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as path from 'path'; -import * as vscode from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { TestStatus, TestsToRun } from '../../../client/testing/common/types'; - -// tslint:disable: no-any - -const UNITTEST_TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles', 'standard'); - -export interface ITestDetails { - className: string; - nameToRun: string; - fileName: string; - sourceFileName?: string; - testName: string; - simpleClassName?: string; - sourceTestName: string; - imported: boolean; - passOnFailedRun?: boolean; - status: TestStatus; - classDefRange?: vscode.Range; - testDefRange?: vscode.Range; - issueRange?: vscode.Range; - issueLineText?: string; - message?: string; - expectedDiagnostic?: vscode.Diagnostic; -} - -export const allTestDetails: ITestDetails[] = [ - { - className: 'test_root.Test_Root_test1', - nameToRun: './test_root.py::Test_Root_test1::test_Root_A', - fileName: 'test_root.py', - testName: 'test_Root_A', - sourceTestName: 'test_Root_A', - testDefRange: new vscode.Range(6, 8, 6, 19), - issueRange: new vscode.Range(7, 8, 7, 36), - issueLineText: 'self.fail("Not implemented")', - message: 'AssertionError: Not implemented', - imported: false, - status: TestStatus.Fail - }, - { - className: 'test_root.Test_Root_test1', - nameToRun: './test_root.py::Test_Root_test1::test_Root_B', - fileName: 'test_root.py', - testName: 'test_Root_B', - sourceTestName: 'test_Root_B', - imported: false, - status: TestStatus.Pass - }, - { - className: 'test_root.Test_Root_test1', - nameToRun: './test_root.py::Test_Root_test1::test_Root_c', - fileName: 'test_root.py', - testName: 'test_Root_c', - sourceTestName: 'test_Root_c', - testDefRange: new vscode.Range(13, 8, 13, 19), - message: 'demonstrating skipping', - imported: false, - status: TestStatus.Skipped - }, - { - className: 'tests.test_another_pytest', - nameToRun: './tests/test_another_pytest.py::test_username', - fileName: path.join(...'tests/test_another_pytest.py'.split('/')), - testName: 'test_username', - sourceTestName: 'test_username', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_another_pytest', - nameToRun: './tests/test_another_pytest.py::test_parametrized_username[one]', - fileName: path.join(...'tests/test_another_pytest.py'.split('/')), - testName: 'test_parametrized_username[one]', - sourceTestName: 'test_parametrized_username', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_another_pytest', - nameToRun: './tests/test_another_pytest.py::test_parametrized_username[two]', - fileName: path.join(...'tests/test_another_pytest.py'.split('/')), - testName: 'test_parametrized_username[two]', - sourceTestName: 'test_parametrized_username', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_another_pytest', - nameToRun: './tests/test_another_pytest.py::test_parametrized_username[three]', - fileName: path.join(...'tests/test_another_pytest.py'.split('/')), - testName: 'test_parametrized_username[three]', - sourceTestName: 'test_parametrized_username', - testDefRange: new vscode.Range(15, 4, 15, 30), - issueRange: new vscode.Range(16, 4, 16, 64), - issueLineText: 'assert non_parametrized_username in [\'one\', \'two\', \'threes\']', - message: 'AssertionError: assert \'three\' in [\'one\', \'two\', \'threes\']', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.().TestExtraNestedForeignTests.()', - nameToRun: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::TestExtraNestedForeignTests::test_super_deep_foreign', - simpleClassName: 'TestInheritingHere', - fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), - testName: 'test_super_deep_foreign', - sourceTestName: 'test_super_deep_foreign', - sourceFileName: path.join(...'tests/external.py'.split('/')), - classDefRange: new vscode.Range(4, 10, 4, 28), - testDefRange: new vscode.Range(2, 12, 2, 35), - issueRange: new vscode.Range(3, 12, 3, 24), - issueLineText: 'assert False', - message: 'AssertionError', - imported: true, - status: TestStatus.Fail - }, - { - className: 'tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()', - nameToRun: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_foreign_test', - simpleClassName: 'TestInheritingHere', - fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), - testName: 'test_foreign_test', - sourceTestName: 'test_foreign_test', - sourceFileName: path.join(...'tests/external.py'.split('/')), - classDefRange: new vscode.Range(4, 10, 4, 28), - testDefRange: new vscode.Range(4, 8, 4, 25), - issueRange: new vscode.Range(5, 8, 5, 20), - issueLineText: 'assert False', - message: 'AssertionError', - imported: true, - status: TestStatus.Fail - }, - { - className: 'tests.test_foreign_nested_tests.TestNestedForeignTests.TestInheritingHere.()', - nameToRun: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::TestInheritingHere::test_nested_normal', - fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), - testName: 'test_nested_normal', - sourceTestName: 'test_nested_normal', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_foreign_nested_tests.TestNestedForeignTests', - nameToRun: './tests/test_foreign_nested_tests.py::TestNestedForeignTests::test_normal', - fileName: path.join(...'tests/test_foreign_nested_tests.py'.split('/')), - testName: 'test_normal', - sourceTestName: 'test_normal', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest.Test_CheckMyApp', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_simple_check', - sourceTestName: 'test_simple_check', - testDefRange: new vscode.Range(7, 8, 7, 25), - message: 'demonstrating skipping', - imported: false, - status: TestStatus.Skipped - }, - { - className: 'tests.test_pytest.Test_CheckMyApp', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_complex_check', - sourceTestName: 'test_complex_check', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodB', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_nested_class_methodB', - sourceTestName: 'test_nested_class_methodB', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.().Test_nested_classB_Of_A.()', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::Test_nested_classB_Of_A::test_d', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_d', - sourceTestName: 'test_d', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest.Test_CheckMyApp.Test_NestedClassA.()', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::Test_NestedClassA::test_nested_class_methodC', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_nested_class_methodC', - sourceTestName: 'test_nested_class_methodC', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest.Test_CheckMyApp', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_simple_check2', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_simple_check2', - sourceTestName: 'test_simple_check2', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest.Test_CheckMyApp', - nameToRun: './tests/test_pytest.py::Test_CheckMyApp::test_complex_check2', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_complex_check2', - sourceTestName: 'test_complex_check2', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest', - nameToRun: './tests/test_pytest.py::test_username', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_username', - sourceTestName: 'test_username', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest', - nameToRun: './tests/test_pytest.py::test_parametrized_username[one]', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_parametrized_username[one]', - sourceTestName: 'test_parametrized_username', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest', - nameToRun: './tests/test_pytest.py::test_parametrized_username[two]', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_parametrized_username[two]', - sourceTestName: 'test_parametrized_username', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_pytest', - nameToRun: './tests/test_pytest.py::test_parametrized_username[three]', - fileName: path.join(...'tests/test_pytest.py'.split('/')), - testName: 'test_parametrized_username[three]', - sourceTestName: 'test_parametrized_username', - testDefRange: new vscode.Range(38, 4, 38, 30), - issueRange: new vscode.Range(39, 4, 39, 64), - issueLineText: 'assert non_parametrized_username in [\'one\', \'two\', \'threes\']', - message: 'AssertionError: assert \'three\' in [\'one\', \'two\', \'threes\']', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.test_unittest_one.Test_test1', - nameToRun: './tests/test_unittest_one.py::Test_test1::test_A', - fileName: path.join(...'tests/test_unittest_one.py'.split('/')), - testName: 'test_A', - sourceTestName: 'test_A', - testDefRange: new vscode.Range(6, 8, 6, 14), - issueRange: new vscode.Range(7, 8, 7, 36), - issueLineText: 'self.fail("Not implemented")', - message: 'AssertionError: Not implemented', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.test_unittest_one.Test_test1', - nameToRun: './tests/test_unittest_one.py::Test_test1::test_B', - fileName: path.join(...'tests/test_unittest_one.py'.split('/')), - testName: 'test_B', - sourceTestName: 'test_B', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_unittest_one.Test_test1', - nameToRun: './tests/test_unittest_one.py::Test_test1::test_c', - fileName: path.join(...'tests/test_unittest_one.py'.split('/')), - testName: 'test_c', - sourceTestName: 'test_c', - testDefRange: new vscode.Range(13, 8, 13, 14), - message: 'demonstrating skipping', - imported: false, - status: TestStatus.Skipped - }, - { - className: 'tests.test_unittest_two.Test_test2', - nameToRun: './tests/test_unittest_two.py::Test_test2::test_A2', - fileName: path.join(...'tests/test_unittest_two.py'.split('/')), - testName: 'test_A2', - sourceTestName: 'test_A2', - testDefRange: new vscode.Range(3, 8, 3, 15), - issueRange: new vscode.Range(4, 8, 4, 36), - issueLineText: 'self.fail("Not implemented")', - message: 'AssertionError: Not implemented', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.test_unittest_two.Test_test2', - nameToRun: './tests/test_unittest_two.py::Test_test2::test_B2', - fileName: path.join(...'tests/test_unittest_two.py'.split('/')), - testName: 'test_B2', - sourceTestName: 'test_B2', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.test_unittest_two.Test_test2', - nameToRun: './tests/test_unittest_two.py::Test_test2::test_C2', - fileName: path.join(...'tests/test_unittest_two.py'.split('/')), - testName: 'test_C2', - sourceTestName: 'test_C2', - testDefRange: new vscode.Range(9, 8, 9, 15), - issueRange: new vscode.Range(10, 8, 10, 41), - issueLineText: 'self.assertEqual(1,2,\'Not equal\')', - message: 'AssertionError: 1 != 2 : Not equal', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.test_unittest_two.Test_test2', - nameToRun: './tests/test_unittest_two.py::Test_test2::test_D2', - fileName: path.join(...'tests/test_unittest_two.py'.split('/')), - testName: 'test_D2', - sourceTestName: 'test_D2', - testDefRange: new vscode.Range(12, 8, 12, 15), - issueRange: new vscode.Range(13, 8, 13, 31), - issueLineText: 'raise ArithmeticError()', - message: 'ArithmeticError', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.test_unittest_two.Test_test2a', - nameToRun: './tests/test_unittest_two.py::Test_test2a::test_222A2', - fileName: path.join(...'tests/test_unittest_two.py'.split('/')), - testName: 'test_222A2', - sourceTestName: 'test_222A2', - testDefRange: new vscode.Range(17, 8, 17, 18), - issueRange: new vscode.Range(18, 8, 18, 36), - issueLineText: 'self.fail("Not implemented")', - message: 'AssertionError: Not implemented', - imported: false, - passOnFailedRun: true, - status: TestStatus.Fail - }, - { - className: 'tests.test_unittest_two.Test_test2a', - nameToRun: './tests/test_unittest_two.py::Test_test2a::test_222B2', - fileName: path.join(...'tests/test_unittest_two.py'.split('/')), - testName: 'test_222B2', - sourceTestName: 'test_222B2', - imported: false, - status: TestStatus.Pass - }, - { - className: 'tests.unittest_three_test.Test_test3', - nameToRun: './tests/unittest_three_test.py::Test_test3::test_A', - fileName: path.join(...'tests/unittest_three_test.py'.split('/')), - testName: 'test_A', - sourceTestName: 'test_A', - testDefRange: new vscode.Range(4, 8, 4, 14), - issueRange: new vscode.Range(5, 8, 5, 36), - issueLineText: 'self.fail("Not implemented")', - message: 'AssertionError: Not implemented', - imported: false, - status: TestStatus.Fail - }, - { - className: 'tests.unittest_three_test.Test_test3', - nameToRun: './tests/unittest_three_test.py::Test_test3::test_B', - fileName: path.join(...'tests/unittest_three_test.py'.split('/')), - testName: 'test_B', - sourceTestName: 'test_B', - imported: false, - status: TestStatus.Pass - } -]; - -export interface ITestScenarioDetails { - scenarioName: string; - discoveryOutput: string; - runOutput: string; - testsToRun?: TestsToRun; - testDetails?: ITestDetails[]; - testSuiteIndex?: number; - testFunctionIndex?: number; - shouldRunFailed?: boolean; - failedRunOutput?: string; -} - -export const testScenarios: ITestScenarioDetails[] = [ - { - scenarioName: 'Run Tests', - discoveryOutput: 'one.output', - runOutput: 'one.xml', - testsToRun: undefined as any, - testDetails: allTestDetails.filter(() => { return true; }) - }, - { - scenarioName: 'Run Specific Test File', - discoveryOutput: 'three.output', - runOutput: 'three.xml', - testsToRun: { - testFile: [{ - fullPath: path.join(UNITTEST_TEST_FILES_PATH, 'tests', 'test_another_pytest.py'), - name: 'tests/test_another_pytest.py', - nameToRun: 'tests/test_another_pytest.py', - xmlName: 'tests/test_another_pytest.py', - functions: [], - suites: [], - time: 0 - }], - testFolder: [], - testFunction: [], - testSuite: [] - }, - testDetails: allTestDetails.filter(td => { return td.fileName === path.join('tests', 'test_another_pytest.py'); }) - }, - { - scenarioName: 'Run Specific Test Suite', - discoveryOutput: 'four.output', - runOutput: 'four.xml', - testsToRun: undefined as any, - testSuiteIndex: 0, - testDetails: allTestDetails.filter(td => { return td.className === 'test_root.Test_Root_test1'; }) - }, - { - scenarioName: 'Run Specific Test Function', - discoveryOutput: 'five.output', - runOutput: 'five.xml', - testsToRun: undefined as any, - testFunctionIndex: 0, - testDetails: allTestDetails.filter(td => { return td.testName === 'test_Root_A'; }) - }, - { - scenarioName: 'Run Failed Tests', - discoveryOutput: 'two.output', - runOutput: 'two.xml', - testsToRun: undefined as any, - testDetails: allTestDetails.filter(_td => { return true; }), - shouldRunFailed: true, - failedRunOutput: 'two.again.xml' - } -]; diff --git a/src/test/testing/pytest/pytest_unittest_parser_data.ts b/src/test/testing/pytest/pytest_unittest_parser_data.ts deleted file mode 100644 index a01c7d7eb557..000000000000 --- a/src/test/testing/pytest/pytest_unittest_parser_data.ts +++ /dev/null @@ -1,2105 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// disable the ' quotemark, as we need to consume many strings from stdout that use that -// test delimiter exclusively. - -// tslint:disable:quotemark - -export enum PytestDataPlatformType { - NonWindows = 'non-windows', - Windows = 'windows' -} - -export type PytestDiscoveryScenario = { - pytest_version_spec: string; - platform: string; - description: string; - rootdir: string; - test_functions: string[]; - functionCount: number; - stdout: string[]; -}; - -// Data to test the pytest unit test parser with. See pytest.discovery.unit.test.ts. -export const pytestScenarioData: PytestDiscoveryScenario[] = - [ - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "src/test_things.py::test_things_major", - "test/this/is/deep/testing/test_very_deeply.py::test_math_works" - ], - functionCount: 9, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 9 items", - "<Module 'src/test_things.py'>", - " <Function 'test_things_major'>", - " <Function 'test_things_minor'>", - "<Module 'src/under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'src/under/test_stuff.py'>", - " <Function 'test_platform'>", - "<Module 'test/test_other_other_things.py'>", - " <Function 'test_sys_ver'>", - "<Module 'test/test_other_things.py'>", - " <Function 'test_sys_ver'>", - "<Module 'test/this/is/deep/testing/test_deeply.py'>", - " <Function 'test_json_works'>", - " <Function 'test_json_numbers_work'>", - "<Module 'test/this/is/deep/testing/test_very_deeply.py'>", - " <Function 'test_math_works'>", - "", - "========================= no tests ran in 0.02 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "src/test_things.py::test_things_major", - "test/this/is/deep/testing/test_very_deeply.py::test_math_works" - ], - functionCount: 9, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 9 items", - "<Module 'src/test_things.py'>", - " <Function 'test_things_major'>", - " <Function 'test_things_minor'>", - "<Module 'src/under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'src/under/test_stuff.py'>", - " <Function 'test_platform'>", - "<Module 'test/test_other_other_things.py'>", - " <Function 'test_sys_ver'>", - "<Module 'test/test_other_things.py'>", - " <Function 'test_sys_ver'>", - "<Module 'test/this/is/deep/testing/test_deeply.py'>", - " <Function 'test_json_works'>", - " <Function 'test_json_numbers_work'>", - "<Module 'test/this/is/deep/testing/test_very_deeply.py'>", - " <Function 'test_math_works'>", - "", - "========================= no tests ran in 0.18 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "src/test_things.py::test_things_major", - "test/this/is/deep/testing/test_very_deeply.py::test_math_works" - ], - functionCount: 9, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 9 items", - "<Module src/test_things.py>", - " <Function test_things_major>", - " <Function test_things_minor>", - "<Module src/under/test_other_stuff.py>", - " <Function test_machine_values>", - "<Module src/under/test_stuff.py>", - " <Function test_platform>", - "<Module test/test_other_other_things.py>", - " <Function test_sys_ver>", - "<Module test/test_other_things.py>", - " <Function test_sys_ver>", - "<Module test/this/is/deep/testing/test_deeply.py>", - " <Function test_json_works>", - " <Function test_json_numbers_work>", - "<Module test/this/is/deep/testing/test_very_deeply.py>", - " <Function test_math_works>", - "", - "========================= no tests ran in 0.18 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "src/test_things.py::test_things_major", - "src/under/test_stuff.py::test_platform" - ], - functionCount: 5, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "<Module 'src/test_things.py'>", - " <Function 'test_things_major'>", - " <Function 'test_things_minor'>", - "<Module 'src/test_things_again.py'>", - " <Function 'test_it_over_again'>", - "<Module 'src/under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'src/under/test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.05 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "src/test_things.py::test_things_major", - "src/under/test_stuff.py::test_platform" - ], - functionCount: 5, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "<Module 'src/test_things.py'>", - " <Function 'test_things_major'>", - " <Function 'test_things_minor'>", - "<Module 'src/test_things_again.py'>", - " <Function 'test_it_over_again'>", - "<Module 'src/under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'src/under/test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.03 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "src/test_things.py::test_things_major", - "src/under/test_stuff.py::test_platform" - ], - functionCount: 5, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "<Module src/test_things.py>", - " <Function test_things_major>", - " <Function test_things_minor>", - "<Module src/test_things_again.py>", - " <Function test_it_over_again>", - "<Module src/under/test_other_stuff.py>", - " <Function test_machine_values>", - "<Module src/under/test_stuff.py>", - " <Function test_platform>", - "", - "========================= no tests ran in 0.03 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in root folder and two more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_things.py::test_things_major", - "under/test_stuff.py::test_platform" - ], - functionCount: 5, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "<Module 'test_things.py'>", - " <Function 'test_things_major'>", - " <Function 'test_things_minor'>", - "<Module 'test_things_again.py'>", - " <Function 'test_it_over_again'>", - "<Module 'under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'under/test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.12 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in root folder and two more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_things.py::test_things_major", - "under/test_stuff.py::test_platform" - ], - functionCount: 5, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "<Module 'test_things.py'>", - " <Function 'test_things_major'>", - " <Function 'test_things_minor'>", - "<Module 'test_things_again.py'>", - " <Function 'test_it_over_again'>", - "<Module 'under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'under/test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.12 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in root folder and two more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_things.py::test_things_major", - "under/test_stuff.py::test_platform" - ], - functionCount: 5, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 5 items", - "<Module test_things.py>", - " <Function test_things_major>", - " <Function test_things_minor>", - "<Module test_things_again.py>", - " <Function test_it_over_again>", - "<Module under/test_other_stuff.py>", - " <Function test_machine_values>", - "<Module under/test_stuff.py>", - " <Function test_platform>", - "", - "========================= no tests ran in 0.12 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in a subfolder off the root.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "under/test_other_stuff.py::test_machine_values", - "under/test_stuff.py::test_platform" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Module 'under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'under/test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.06 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in a subfolder off the root.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "under/test_other_stuff.py::test_machine_values", - "under/test_stuff.py::test_platform" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Module 'under/test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'under/test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.05 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 test modules in a subfolder off the root.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "under/test_other_stuff.py::test_machine_values", - "under/test_stuff.py::test_platform" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Module under/test_other_stuff.py>", - " <Function test_machine_values>", - "<Module under/test_stuff.py>", - " <Function test_platform>", - "", - "========================= no tests ran in 0.05 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 modules at the topmost level.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_other_stuff.py::test_machine_values", - "test_stuff.py::test_platform" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Module 'test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.05 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 modules at the topmost level.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_other_stuff.py::test_machine_values", - "test_stuff.py::test_platform" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Module 'test_other_stuff.py'>", - " <Function 'test_machine_values'>", - "<Module 'test_stuff.py'>", - " <Function 'test_platform'>", - "", - "========================= no tests ran in 0.05 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Non-package source, 2 modules at the topmost level.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_other_stuff.py::test_machine_values", - "test_stuff.py::test_platform" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Module test_other_stuff.py>", - " <Function test_machine_values>", - "<Module test_stuff.py>", - " <Function test_platform>", - "", - "========================= no tests ran in 0.05 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_basic_root.py::test_basic_major", - "test/test_other_basic.py::test_basic_major_minor_internal", - "test/subdir/under/another/subdir/test_other_basic_sub.py::test_basic_major_minor" - ], - functionCount: 16, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 16 items", - "<Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/subdir/under/another/subdir/test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/subdir/under/another/subdir/test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/uneven/folders/test_basic_uneven.py'>", - " <Function 'test_basic_major_uneven'>", - " <Function 'test_basic_minor_uneven'>", - "<Module 'test/uneven/folders/test_other_basic_uneven.py'>", - " <Function 'test_basic_major_minor_uneven'>", - " <Function 'test_basic_major_minor_internal_uneven'>", - "", - "========================= no tests ran in 0.07 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_basic_root.py::test_basic_major", - "test/test_other_basic.py::test_basic_major_minor_internal", - "test/subdir/under/another/subdir/test_other_basic_sub.py::test_basic_major_minor", - "test/uneven/folders/test_other_basic_uneven.py::test_basic_major_minor_internal_uneven" - ], - functionCount: 16, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 16 items", - "<Package '/home/user/test/pytest_scenario'>", - " <Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Package '/home/user/test/pytest_scenario/test'>", - " <Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Package '/home/user/test/pytest_scenario/test/subdir'>", - " <Package '/home/user/test/pytest_scenario/test/subdir/under'>", - " <Package '/home/user/test/pytest_scenario/test/subdir/under/another'>", - " <Package '/home/user/test/pytest_scenario/test/subdir/under/another/subdir'>", - " <Module 'test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Package '/home/user/test/pytest_scenario/test/uneven'>", - " <Package '/home/user/test/pytest_scenario/test/uneven/folders'>", - " <Module 'test_basic_uneven.py'>", - " <Function 'test_basic_major_uneven'>", - " <Function 'test_basic_minor_uneven'>", - " <Module 'test_other_basic_uneven.py'>", - " <Function 'test_basic_major_minor_uneven'>", - " <Function 'test_basic_major_minor_internal_uneven'>", - "", - "========================= no tests ran in 0.13 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_basic_root.py::test_basic_major", - "test/test_other_basic.py::test_basic_major_minor_internal", - "test/subdir/under/another/subdir/test_other_basic_sub.py::test_basic_major_minor", - "test/uneven/folders/test_other_basic_uneven.py::test_basic_major_minor_internal_uneven" - ], - functionCount: 16, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 16 items", - "<Package /home/user/test/pytest_scenario>", - " <Module test_basic_root.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic_root.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Package /home/user/test/pytest_scenario/test>", - " <Module test_basic.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Package /home/user/test/pytest_scenario/test/subdir>", - " <Package /home/user/test/pytest_scenario/test/subdir/under>", - " <Package /home/user/test/pytest_scenario/test/subdir/under/another>", - " <Package /home/user/test/pytest_scenario/test/subdir/under/another/subdir>", - " <Module test_basic_sub.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic_sub.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Package /home/user/test/pytest_scenario/test/uneven>", - " <Package /home/user/test/pytest_scenario/test/uneven/folders>", - " <Module test_basic_uneven.py>", - " <Function test_basic_major_uneven>", - " <Function test_basic_minor_uneven>", - " <Module test_other_basic_uneven.py>", - " <Function test_basic_major_minor_uneven>", - " <Function test_basic_major_minor_internal_uneven>", - "", - "========================= no tests ran in 0.13 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test/test_other_basic.py::test_basic_major_minor_internal", - "test/subdir/test_other_basic_sub.py::test_basic_major_minor" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Module 'test/test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/subdir/test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/subdir/test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.18 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test/test_other_basic.py::test_basic_major_minor_internal", - "test/subdir/test_other_basic_sub.py::test_basic_major_minor" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package '/home/user/test/pytest_scenario'>", - " <Package '/home/user/test/pytest_scenario/test'>", - " <Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Package '/home/user/test/pytest_scenario/test/subdir'>", - " <Module 'test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.07 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test/test_other_basic.py::test_basic_major_minor_internal", - "test/subdir/test_other_basic_sub.py::test_basic_major_minor" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package /home/user/test/pytest_scenario>", - " <Package /home/user/test/pytest_scenario/test>", - " <Module test_basic.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_basic_root.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Module test_other_basic_root.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Package /home/user/test/pytest_scenario/test/subdir>", - " <Module test_basic_sub.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic_sub.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - "", - "========================= no tests ran in 0.07 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_other_basic_root.py::test_basic_major_minor_internal", - "test/test_other_basic_sub.py::test_basic_major_minor" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.18 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_other_basic_root.py::test_basic_major_minor_internal", - "test/test_basic_sub.py::test_basic_major", - "test/test_basic_sub.py::test_basic_minor" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package '/home/user/test/pytest_scenario'>", - " <Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Package '/home/user/test/pytest_scenario/test'>", - " <Module 'test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.22 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_other_basic_root.py::test_basic_major_minor_internal", - "test/test_basic_sub.py::test_basic_major", - "test/test_basic_sub.py::test_basic_minor" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package /home/user/test/pytest_scenario>", - " <Module test_basic.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_basic_root.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Module test_other_basic_root.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Package /home/user/test/pytest_scenario/test>", - " <Module test_basic_sub.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic_sub.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - "", - "========================= no tests ran in 0.22 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ test modules in a subfolder off the root.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test/test_basic.py::test_basic_minor", - "test/test_other_basic.py::test_basic_major_minor", - "test/test_other_basic_root.py::test_basic_major_minor", - "test/test_other_basic_sub.py::test_basic_major_minor_internal" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Module 'test/test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test/test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test/test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.15 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ test modules in a subfolder off the root.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test/test_basic.py::test_basic_minor", - "test/test_other_basic.py::test_basic_major_minor", - "test/test_other_basic_root.py::test_basic_major_minor", - "test/test_other_basic_sub.py::test_basic_major_minor_internal" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package '/home/user/test/pytest_scenario'>", - " <Package '/home/user/test/pytest_scenario/test'>", - " <Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Module 'test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.15 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ test modules in a subfolder off the root.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test/test_basic.py::test_basic_minor", - "test/test_other_basic.py::test_basic_major_minor", - "test/test_other_basic_root.py::test_basic_major_minor", - "test/test_other_basic_sub.py::test_basic_major_minor_internal" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package /home/user/test/pytest_scenario>", - " <Package /home/user/test/pytest_scenario/test>", - " <Module test_basic.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_basic_root.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_basic_sub.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Module test_other_basic_root.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Module test_other_basic_sub.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - "", - "========================= no tests ran in 0.15 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ modules at the topmost level.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_basic.py::test_basic_major", - "test_basic_root.py::test_basic_major", - "test_other_basic_root.py::test_basic_major_minor", - "test_other_basic_sub.py::test_basic_major_minor_internal" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - "<Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "<Module 'test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.23 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ modules at the topmost level.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_basic.py::test_basic_major", - "test_basic_root.py::test_basic_major", - "test_other_basic_root.py::test_basic_major_minor", - "test_other_basic_sub.py::test_basic_major_minor_internal" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package '/home/user/test/pytest_scenario'>", - " <Module 'test_basic.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_basic_root.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_basic_sub.py'>", - " <Function 'test_basic_major'>", - " <Function 'test_basic_minor'>", - " <Module 'test_other_basic.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Module 'test_other_basic_root.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - " <Module 'test_other_basic_sub.py'>", - " <Function 'test_basic_major_minor'>", - " <Function 'test_basic_major_minor_internal'>", - "", - "========================= no tests ran in 0.16 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Package-based source, 2+ modules at the topmost level.", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "test_basic.py::test_basic_major", - "test_basic_root.py::test_basic_major", - "test_other_basic_root.py::test_basic_major_minor", - "test_other_basic_sub.py::test_basic_major_minor_internal" - ], - functionCount: 12, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.0+, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 12 items", - "<Package /home/user/test/pytest_scenario>", - " <Module test_basic.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_basic_root.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_basic_sub.py>", - " <Function test_basic_major>", - " <Function test_basic_minor>", - " <Module test_other_basic.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Module test_other_basic_root.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - " <Module test_other_basic_sub.py>", - " <Function test_basic_major_minor>", - " <Function test_basic_major_minor_internal>", - "", - "========================= no tests ran in 0.16 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "other_tests/test_base_stuff.py::test_do_other_test", - "other_tests/test_base_stuff.py::test_do_test", - "tests/further_tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2", - "tests/further_tests/deeper/test_more_multiply.py::test_times_100", - "tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 8 items", - "<Module 'other_tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/further_tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/further_tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "<Module 'tests/further_tests/deeper/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "", - "======================== no tests ran in 0.30 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "other_tests/test_base_stuff.py::test_do_other_test", - "other_tests/test_base_stuff.py::test_do_test", - "tests/further_tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2", - "tests/further_tests/deeper/test_more_multiply.py::test_times_100", - "tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\other_tests'>", - " <Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests'>", - " <Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - " <Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests\\\\deeper'>", - " <Module 'test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "", - "======================== no tests ran in 0.42 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "other_tests/test_base_stuff.py::test_do_other_test", - "other_tests/test_base_stuff.py::test_do_test", - "tests/further_tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2", - "tests/further_tests/deeper/test_more_multiply.py::test_times_100", - "tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package e:\\\\user\\\\test\\\\pytest_scenario>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\other_tests>", - " <Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests>", - " <Module test_gimme_5.py>", - " <Function test_gimme_5>", - " <Module test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests\\\\deeper>", - " <Module test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - "", - "======================== no tests ran in 0.42 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "other_tests/test_base_stuff.py::test_do_other_test", - "other_tests/test_base_stuff.py::test_do_test", - "tests/further_tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2", - "tests/further_tests/deeper/test_more_multiply.py::test_times_100", - "tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'other_tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/further_tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/further_tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "<Module 'tests/further_tests/deeper/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "", - "======================== no tests ran in 0.11 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "other_tests/test_base_stuff.py::test_do_other_test", - "other_tests/test_base_stuff.py::test_do_test", - "tests/further_tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2", - "tests/further_tests/deeper/test_more_multiply.py::test_times_100", - "tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'other_tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/further_tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/further_tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "<Module 'tests/further_tests/deeper/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "", - "======================== no tests ran in 0.17 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, tests throughout a deeper tree, including 2 distinct folder paths at different levels.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "other_tests/test_base_stuff.py::test_do_other_test", - "other_tests/test_base_stuff.py::test_do_test", - "tests/further_tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2", - "tests/further_tests/deeper/test_more_multiply.py::test_times_100", - "tests/further_tests/deeper/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module other_tests/test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - "<Module tests/further_tests/test_gimme_5.py>", - " <Function test_gimme_5>", - "<Module tests/further_tests/test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "<Module tests/further_tests/deeper/test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - "", - "======================== no tests ran in 0.17 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_more_multiply.py::test_times_100", - "tests/further_tests/test_more_multiply.py::test_times_negative_1", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/further_tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/further_tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_more_multiply.py::test_times_100", - "tests/further_tests/test_more_multiply.py::test_times_negative_1", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", - " <Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - " <Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests'>", - " <Module 'test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - " <Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.38 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_more_multiply.py::test_times_100", - "tests/further_tests/test_more_multiply.py::test_times_negative_1", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package e:\\\\user\\\\test\\\\pytest_scenario>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>", - " <Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - " <Module test_gimme_5.py>", - " <Function test_gimme_5>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests\\\\further_tests>", - " <Module test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - " <Module test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.38 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_more_multiply.py::test_times_100", - "tests/further_tests/test_more_multiply.py::test_times_negative_1", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/further_tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/further_tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.17 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_more_multiply.py::test_times_100", - "tests/further_tests/test_more_multiply.py::test_times_negative_1", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/further_tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/further_tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.20 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2 test modules in subfolders of root, and 2 more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/further_tests/test_more_multiply.py::test_times_100", - "tests/further_tests/test_more_multiply.py::test_times_negative_1", - "tests/further_tests/test_multiply.py::test_times_10", - "tests/further_tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module tests/test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - "<Module tests/test_gimme_5.py>", - " <Function test_gimme_5>", - "<Module tests/further_tests/test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - "<Module tests/further_tests/test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.20 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", - " <Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - " <Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", - " <Module 'test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - " <Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.66 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package e:\\\\user\\\\test\\\\pytest_scenario>", - " <Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - " <Module test_gimme_5.py>", - " <Function test_gimme_5>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>", - " <Module test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - " <Module test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.66 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.11 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.41 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2+ test modules in root folder and two more in one (direct) subfolder.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - "<Module test_gimme_5.py>", - " <Function test_gimme_5>", - "<Module tests/test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - "<Module tests/test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.41 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ test modules in a subfolder off the root.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.20 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ test modules in a subfolder off the root.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", - " <Package 'e:\\\\user\\\\test\\\\pytest_scenario\\\\tests'>", - " <Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - " <Module 'test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - " <Module 'test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - " <Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ test modules in a subfolder off the root.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Package e:\\\\user\\\\test\\\\pytest_scenario>", - " <Package e:\\\\user\\\\test\\\\pytest_scenario\\\\tests>", - " <Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - " <Module test_gimme_5.py>", - " <Function test_gimme_5>", - " <Module test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - " <Module test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2+ test modules in a subfolder off the root.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2+ test modules in a subfolder off the root.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module 'tests/test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'tests/test_gimme_5.py'>", - " <Function 'test_gimme_5'>", - "<Module 'tests/test_more_multiply.py'>", - " <Function 'test_times_100'>", - " <Function 'test_times_negative_1'>", - "<Module 'tests/test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2+ test modules in a subfolder off the root.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "tests/test_base_stuff.py::test_do_test", - "tests/test_base_stuff.py::test_do_other_test", - "tests/test_gimme_5.py::test_gimme_5", - "tests/test_more_multiply.py::test_times_100", - "tests/test_more_multiply.py::test_times_negative_1", - "tests/test_multiply.py::test_times_10", - "tests/test_multiply.py::test_times_2" - ], - functionCount: 7, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 7 items", - "<Module tests/test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - "<Module tests/test_gimme_5.py>", - " <Function test_gimme_5>", - "<Module tests/test_more_multiply.py>", - " <Function test_times_100>", - " <Function test_times_negative_1>", - "<Module tests/test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.26 seconds =========================" - ] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ modules at the topmost level.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "test_multiply.py::test_times_10", - "test_multiply.py::test_times_2" - ], - functionCount: 4, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "<Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.17 seconds ========================="] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ modules at the topmost level.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "test_multiply.py::test_times_10", - "test_multiply.py::test_times_2" - ], - functionCount: 4, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "<Package 'e:\\\\user\\\\test\\\\pytest_scenario'>", - " <Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - " <Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.37 seconds ========================="] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Package-based source, 2+ modules at the topmost level.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "test_multiply.py::test_times_10", - "test_multiply.py::test_times_2" - ], - functionCount: 4, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "<Package e:\\\\user\\\\test\\\\pytest_scenario>", - " <Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - " <Module test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.37 seconds ========================="] - }, - { - pytest_version_spec: "< 3.7", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2 modules at the topmost level.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "test_multiply.py::test_times_10", - "test_multiply.py::test_times_2" - ], - functionCount: 4, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.6.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "<Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.18 seconds ========================="] - }, - { - pytest_version_spec: ">= 3.7 < 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2 modules at the topmost level.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "test_multiply.py::test_times_10", - "test_multiply.py::test_times_2" - ], - functionCount: 4, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-3.7.4, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "<Module 'test_base_stuff.py'>", - " <Function 'test_do_test'>", - " <Function 'test_do_other_test'>", - "<Module 'test_multiply.py'>", - " <Function 'test_times_10'>", - " <Function 'test_times_2'>", - "", - "======================== no tests ran in 0.36 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.Windows, - description: "Non-package source, 2 modules at the topmost level.", - rootdir: "e:\\user\\test\\pytest_scenario", - test_functions: [ - "test_base_stuff.py::test_do_test", - "test_base_stuff.py::test_do_other_test", - "test_multiply.py::test_times_10", - "test_multiply.py::test_times_2" - ], - functionCount: 4, - stdout: [ - "============================= test session starts =============================", - "platform win32 -- Python 3.7.0, pytest-4.1.0, py-1.6.0, pluggy-0.7.1", - "rootdir: e:\\user\\test\\pytest_scenario, inifile:", - "collected 4 items", - "<Module test_base_stuff.py>", - " <Function test_do_test>", - " <Function test_do_other_test>", - "<Module test_multiply.py>", - " <Function test_times_10>", - " <Function test_times_2>", - "", - "======================== no tests ran in 0.36 seconds =========================" - ] - }, - { - pytest_version_spec: ">= 4.1", - platform: PytestDataPlatformType.NonWindows, - description: "Parameterized tests", - rootdir: "/home/user/test/pytest_scenario", - test_functions: [ - "tests/test_spam.py::test_with_subtests[1-2]", - "tests/test_spam.py::test_with_subtests[3-4]" - ], - functionCount: 2, - stdout: [ - "============================= test session starts ==============================", - "platform linux -- Python 3.7.1, pytest-4.2.1, py-1.7.0, pluggy-0.8.1", - "rootdir: /home/user/test/pytest_scenario, inifile:", - "collected 2 items", - "<Package /home/user/test/pytest_scenario/tests>", - " <Module test_spam.py>", - " <Function test_with_subtests[1-2]>", - " <Function test_with_subtests[3-4]>", - "", - "========================= no tests ran in 0.02 seconds =========================" - ] - } - ]; diff --git a/src/test/testing/pytest/services/discoveryService.unit.test.ts b/src/test/testing/pytest/services/discoveryService.unit.test.ts deleted file mode 100644 index 15365a209a60..000000000000 --- a/src/test/testing/pytest/services/discoveryService.unit.test.ts +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -// tslint:disable:max-func-body-length - -import { expect } from 'chai'; -import { deepEqual, instance, mock, verify, when } from 'ts-mockito'; -import { CancellationTokenSource, Uri } from 'vscode'; -import { ServiceContainer } from '../../../../client/ioc/container'; -import { IServiceContainer } from '../../../../client/ioc/types'; -import { PYTEST_PROVIDER } from '../../../../client/testing/common/constants'; -import { TestsDiscoveryService } from '../../../../client/testing/common/services/discovery'; -import { TestsHelper } from '../../../../client/testing/common/testUtils'; -import { ITestDiscoveryService, ITestsHelper, TestDiscoveryOptions, Tests } from '../../../../client/testing/common/types'; -import { ArgumentsService } from '../../../../client/testing/pytest/services/argsService'; -import { TestDiscoveryService } from '../../../../client/testing/pytest/services/discoveryService'; -import { IArgumentsService, TestFilter } from '../../../../client/testing/types'; -import { MockOutputChannel } from '../../../mockClasses'; - -// tslint:disable: no-unnecessary-override no-any -suite('Unit Tests - PyTest - Discovery', () => { - class DiscoveryService extends TestDiscoveryService { - public buildTestCollectionArgs(options: TestDiscoveryOptions): string[] { - return super.buildTestCollectionArgs(options); - } - public discoverTestsInTestDirectory(options: TestDiscoveryOptions): Promise<Tests> { - return super.discoverTestsInTestDirectory(options); - } - } - let discoveryService: DiscoveryService; - let serviceContainer: IServiceContainer; - let argsService: IArgumentsService; - let helper: ITestsHelper; - setup(() => { - serviceContainer = mock(ServiceContainer); - helper = mock(TestsHelper); - argsService = mock(ArgumentsService); - - when(serviceContainer.get<IArgumentsService>(IArgumentsService, PYTEST_PROVIDER)).thenReturn(instance(argsService)); - when(serviceContainer.get<ITestsHelper>(ITestsHelper)).thenReturn(instance(helper)); - discoveryService = new DiscoveryService(instance(serviceContainer)); - }); - test('Ensure discovery is invoked when there are no test directories', async () => { - const options: TestDiscoveryOptions = { - args: ['some args'], - cwd: __dirname, - ignoreCache: true, - outChannel: new MockOutputChannel('Tests'), - token: new CancellationTokenSource().token, - workspaceFolder: Uri.file(__dirname) - }; - const args = ['1', '2', '3']; - const discoveredTests = 'Hello World' as any as Tests; - discoveryService.buildTestCollectionArgs = () => args; - discoveryService.discoverTestsInTestDirectory = () => Promise.resolve(discoveredTests); - when(argsService.getTestFolders(deepEqual(options.args))).thenReturn([]); - - const tests = await discoveryService.discoverTests(options); - - expect(tests).equal(discoveredTests); - }); - test('Ensure discovery is invoked when there are multiple test directories', async () => { - const options: TestDiscoveryOptions = { - args: ['some args'], - cwd: __dirname, - ignoreCache: true, - outChannel: new MockOutputChannel('Tests'), - token: new CancellationTokenSource().token, - workspaceFolder: Uri.file(__dirname) - }; - const args = ['1', '2', '3']; - discoveryService.buildTestCollectionArgs = () => args; - const directories = ['a', 'b']; - discoveryService.discoverTestsInTestDirectory = async (opts) => { - const dir = opts.args[opts.args.length - 1]; - if (dir === 'a') { - return 'Result A' as any as Tests; - } - if (dir === 'b') { - return 'Result B' as any as Tests; - } - throw new Error('Unrecognized directory'); - }; - when(argsService.getTestFolders(deepEqual(options.args))).thenReturn(directories); - when(helper.mergeTests(deepEqual(['Result A', 'Result B']))).thenReturn('mergedTests' as any); - - const tests = await discoveryService.discoverTests(options); - - verify(helper.mergeTests(deepEqual(['Result A', 'Result B']))).once(); - expect(tests).equal('mergedTests'); - }); - test('Build collection arguments', async () => { - const options: TestDiscoveryOptions = { - args: ['some args', 'and some more'], - cwd: __dirname, - ignoreCache: false, - outChannel: new MockOutputChannel('Tests'), - token: new CancellationTokenSource().token, - workspaceFolder: Uri.file(__dirname) - }; - - const filteredArgs = options.args; - const expectedArgs = ['-s', ...filteredArgs]; - when(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).thenReturn(filteredArgs); - - const args = discoveryService.buildTestCollectionArgs(options); - - expect(args).deep.equal(expectedArgs); - verify(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).once(); - }); - test('Build collection arguments with ignore in args', async () => { - const options: TestDiscoveryOptions = { - args: ['some args', 'and some more', '--cache-clear'], - cwd: __dirname, - ignoreCache: true, - outChannel: new MockOutputChannel('Tests'), - token: new CancellationTokenSource().token, - workspaceFolder: Uri.file(__dirname) - }; - - const filteredArgs = options.args; - const expectedArgs = ['-s', ...filteredArgs]; - when(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).thenReturn(filteredArgs); - - const args = discoveryService.buildTestCollectionArgs(options); - - expect(args).deep.equal(expectedArgs); - verify(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).once(); - }); - test('Build collection arguments (& ignore)', async () => { - const options: TestDiscoveryOptions = { - args: ['some args', 'and some more'], - cwd: __dirname, - ignoreCache: true, - outChannel: new MockOutputChannel('Tests'), - token: new CancellationTokenSource().token, - workspaceFolder: Uri.file(__dirname) - }; - - const filteredArgs = options.args; - const expectedArgs = ['-s', '--cache-clear', ...filteredArgs]; - when(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).thenReturn(filteredArgs); - - const args = discoveryService.buildTestCollectionArgs(options); - - expect(args).deep.equal(expectedArgs); - verify(argsService.filterArguments(deepEqual(options.args), TestFilter.discovery)).once(); - }); - test('Discover using common discovery', async () => { - const options: TestDiscoveryOptions = { - args: ['some args', 'and some more'], - cwd: __dirname, - ignoreCache: true, - outChannel: new MockOutputChannel('Tests'), - token: new CancellationTokenSource().token, - workspaceFolder: Uri.file(__dirname) - }; - const expectedDiscoveryArgs = ['discover', 'pytest', '--', ...options.args]; - const discoveryOptions = { ...options }; - discoveryOptions.args = expectedDiscoveryArgs; - - const commonDiscoveryService = mock(TestsDiscoveryService); - const discoveredTests = 'Hello' as any as Tests; - when(serviceContainer.get<ITestDiscoveryService>(ITestDiscoveryService, 'common')).thenReturn(instance(commonDiscoveryService)); - when(commonDiscoveryService.discoverTests(deepEqual(discoveryOptions))).thenResolve(discoveredTests); - - const tests = await discoveryService.discoverTestsInTestDirectory(options); - - verify(commonDiscoveryService.discoverTests(deepEqual(discoveryOptions))).once(); - expect(tests).equal(discoveredTests); - }); -}); diff --git a/src/test/testing/rediscover.test.ts b/src/test/testing/rediscover.test.ts deleted file mode 100644 index b368d35e4919..000000000000 --- a/src/test/testing/rediscover.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { assert } from 'chai'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import { ConfigurationTarget } from 'vscode'; -import { ICondaService, IInterpreterService } from '../../client/interpreter/contracts'; -import { InterpreterService } from '../../client/interpreter/interpreterService'; -import { CondaService } from '../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../client/testing/common/constants'; -import { ITestManagerFactory, TestProvider } from '../../client/testing/common/types'; -import { deleteDirectory, deleteFile, rootWorkspaceUri, updateSetting } from '../common'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } 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('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - await updateSetting('testing.nosetestArgs', [], rootWorkspaceUri, configTarget); - await updateSetting('testing.pytestArgs', [], rootWorkspaceUri, configTarget); - } - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - - async function discoverUnitTests(testProvider: TestProvider) { - const testManager = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory)(testProvider, rootWorkspaceUri!, testFilesPath); - let tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testSuites.length, 2, 'Incorrect number of test suites'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - await deleteFile(path.join(path.dirname(testFile), `${path.basename(testFile, '.py')}.pyc`)); - await fs.copy(testFileWithMoreTests, testFile, { overwrite: true }); - tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFunctions.length, 4, 'Incorrect number of updated test functions'); - } - - test('Re-discover tests (unittest)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - await discoverUnitTests('unittest'); - }); - - test('Re-discover tests (pytest)', async () => { - await updateSetting('testing.pytestArgs', ['-k=test_'], rootWorkspaceUri, configTarget); - await discoverUnitTests('pytest'); - }); - - test('Re-discover tests (nosetest)', async () => { - await updateSetting('testing.nosetestArgs', ['-m', 'test'], rootWorkspaceUri, configTarget); - await discoverUnitTests('nosetest'); - }); -}); diff --git a/src/test/testing/serviceRegistry.ts b/src/test/testing/serviceRegistry.ts index b8288a1ed2fb..231716b653ba 100644 --- a/src/test/testing/serviceRegistry.ts +++ b/src/test/testing/serviceRegistry.ts @@ -1,162 +1,34 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. + 'use strict'; + import { Uri } from 'vscode'; import { IProcessServiceFactory } from '../../client/common/process/types'; -import { CodeCssGenerator } from '../../client/datascience/codeCssGenerator'; -import { InteractiveWindow } from '../../client/datascience/interactive-window/interactiveWindow'; -import { InteractiveWindowProvider } from '../../client/datascience/interactive-window/interactiveWindowProvider'; -import { JupyterExecutionFactory } from '../../client/datascience/jupyter/jupyterExecutionFactory'; -import { JupyterImporter } from '../../client/datascience/jupyter/jupyterImporter'; -import { JupyterServerFactory } from '../../client/datascience/jupyter/jupyterServerFactory'; -import { - ICodeCssGenerator, - IInteractiveWindow, - IInteractiveWindowProvider, - IJupyterExecution, - INotebookImporter, - INotebookServer -} from '../../client/datascience/types'; -import { IServiceContainer } from '../../client/ioc/types'; -import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../client/testing/common/constants'; -import { TestContextService } from '../../client/testing/common/services/contextService'; -import { TestDiscoveredTestParser } from '../../client/testing/common/services/discoveredTestParser'; -import { TestsDiscoveryService } from '../../client/testing/common/services/discovery'; -import { TestCollectionStorageService } from '../../client/testing/common/services/storageService'; -import { TestManagerService } from '../../client/testing/common/services/testManagerService'; -import { TestResultsService } from '../../client/testing/common/services/testResultsService'; -import { TestsStatusUpdaterService } from '../../client/testing/common/services/testsStatusService'; -import { ITestDiscoveredTestParser } from '../../client/testing/common/services/types'; -import { UnitTestDiagnosticService } from '../../client/testing/common/services/unitTestDiagnosticService'; +import { IInterpreterHelper } from '../../client/interpreter/contracts'; +import { InterpreterHelper } from '../../client/interpreter/helpers'; import { TestsHelper } from '../../client/testing/common/testUtils'; -import { TestFlatteningVisitor } from '../../client/testing/common/testVisitors/flatteningVisitor'; -import { TestResultResetVisitor } from '../../client/testing/common/testVisitors/resultResetVisitor'; -import { - ITestCollectionStorageService, - ITestContextService, - ITestDiscoveryService, - ITestManager, - ITestManagerFactory, - ITestManagerService, - ITestManagerServiceFactory, - ITestResultsService, - ITestsHelper, - ITestsParser, - ITestsStatusUpdaterService, - ITestVisitor, - IUnitTestSocketServer, - TestProvider -} from '../../client/testing/common/types'; -import { TestManager as NoseTestManager } from '../../client/testing/nosetest/main'; -import { TestDiscoveryService as NoseTestDiscoveryService } from '../../client/testing/nosetest/services/discoveryService'; -import { TestsParser as NoseTestTestsParser } from '../../client/testing/nosetest/services/parserService'; -import { TestManager as PyTestTestManager } from '../../client/testing/pytest/main'; -import { TestDiscoveryService as PytestTestDiscoveryService } from '../../client/testing/pytest/services/discoveryService'; -import { ITestDiagnosticService } from '../../client/testing/types'; -import { TestManager as UnitTestTestManager } from '../../client/testing/unittest/main'; -import { - TestDiscoveryService as UnitTestTestDiscoveryService -} from '../../client/testing/unittest/services/discoveryService'; -import { TestsParser as UnitTestTestsParser } from '../../client/testing/unittest/services/parserService'; +import { ITestsHelper } from '../../client/testing/common/types'; import { getPythonSemVer } from '../common'; import { IocContainer } from '../serviceRegistry'; -import { MockUnitTestSocketServer } from './mocks'; export class UnitTestIocContainer extends IocContainer { - constructor() { - super(); - } public async getPythonMajorVersion(resource: Uri): Promise<number> { const procServiceFactory = this.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory); const procService = await procServiceFactory.create(resource); const pythonVersion = await getPythonSemVer(procService); if (pythonVersion) { return pythonVersion.major; - } else { - return -1; // log warning already issued by underlying functions... } + return -1; // log warning already issued by underlying functions... } - public registerTestVisitors() { - this.serviceManager.add<ITestVisitor>(ITestVisitor, TestFlatteningVisitor, 'TestFlatteningVisitor'); - this.serviceManager.add<ITestVisitor>(ITestVisitor, TestResultResetVisitor, 'TestResultResetVisitor'); - this.serviceManager.addSingleton<ITestsStatusUpdaterService>(ITestsStatusUpdaterService, TestsStatusUpdaterService); - this.serviceManager.addSingleton<ITestContextService>(ITestContextService, TestContextService); - } - - public registerTestStorage() { - this.serviceManager.addSingleton<ITestCollectionStorageService>(ITestCollectionStorageService, TestCollectionStorageService); - } - - public registerTestsHelper() { + public registerTestsHelper(): void { this.serviceManager.addSingleton<ITestsHelper>(ITestsHelper, TestsHelper); } - public registerTestResultsHelper() { - this.serviceManager.add<ITestResultsService>(ITestResultsService, TestResultsService); - } - - public registerTestParsers() { - this.serviceManager.add<ITestsParser>(ITestsParser, UnitTestTestsParser, UNITTEST_PROVIDER); - this.serviceManager.add<ITestsParser>(ITestsParser, NoseTestTestsParser, NOSETEST_PROVIDER); - } - - public registerTestDiscoveryServices() { - this.serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, UnitTestTestDiscoveryService, UNITTEST_PROVIDER); - this.serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, PytestTestDiscoveryService, PYTEST_PROVIDER); - this.serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, NoseTestDiscoveryService, NOSETEST_PROVIDER); - this.serviceManager.add<ITestDiscoveryService>(ITestDiscoveryService, TestsDiscoveryService, 'common'); - this.serviceManager.add<ITestDiscoveredTestParser>(ITestDiscoveredTestParser, TestDiscoveredTestParser); - } - - public registerTestDiagnosticServices() { - this.serviceManager.addSingleton<ITestDiagnosticService>(ITestDiagnosticService, UnitTestDiagnosticService); - } - - public registerTestManagers() { - this.serviceManager.addFactory<ITestManager>(ITestManagerFactory, (context) => { - return (testProvider: TestProvider, workspaceFolder: Uri, rootDirectory: string) => { - const serviceContainer = context.container.get<IServiceContainer>(IServiceContainer); - - switch (testProvider) { - case NOSETEST_PROVIDER: { - return new NoseTestManager(workspaceFolder, rootDirectory, serviceContainer); - } - case PYTEST_PROVIDER: { - return new PyTestTestManager(workspaceFolder, rootDirectory, serviceContainer); - } - case UNITTEST_PROVIDER: { - return new UnitTestTestManager(workspaceFolder, rootDirectory, serviceContainer); - } - default: { - throw new Error(`Unrecognized test provider '${testProvider}'`); - } - } - }; - }); - } - - public registerTestManagerService() { - this.serviceManager.addFactory<ITestManagerService>(ITestManagerServiceFactory, (context) => { - return (workspaceFolder: Uri) => { - const serviceContainer = context.container.get<IServiceContainer>(IServiceContainer); - const testsHelper = context.container.get<ITestsHelper>(ITestsHelper); - return new TestManagerService(workspaceFolder, testsHelper, serviceContainer); - }; - }); - } - - public registerMockUnitTestSocketServer() { - this.serviceManager.addSingleton<IUnitTestSocketServer>(IUnitTestSocketServer, MockUnitTestSocketServer); - } - - public registerDataScienceTypes() { - this.serviceManager.addSingleton<IJupyterExecution>(IJupyterExecution, JupyterExecutionFactory); - this.serviceManager.addSingleton<IInteractiveWindowProvider>(IInteractiveWindowProvider, InteractiveWindowProvider); - this.serviceManager.add<IInteractiveWindow>(IInteractiveWindow, InteractiveWindow); - this.serviceManager.add<INotebookImporter>(INotebookImporter, JupyterImporter); - this.serviceManager.add<INotebookServer>(INotebookServer, JupyterServerFactory); - this.serviceManager.addSingleton<ICodeCssGenerator>(ICodeCssGenerator, CodeCssGenerator); + public registerInterpreterStorageTypes(): void { + this.serviceManager.add<IInterpreterHelper>(IInterpreterHelper, InterpreterHelper); } } diff --git a/src/test/testing/stoppingDiscoverAndTest.test.ts b/src/test/testing/stoppingDiscoverAndTest.test.ts deleted file mode 100644 index e6b70f6c0870..000000000000 --- a/src/test/testing/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/testing/common/constants'; -import { ITestDiscoveryService } from '../../client/testing/common/types'; -import { initialize, initializeTest } from '../initialize'; -import { MockDiscoveryService, MockTestManagerWithRunningTests } from './mocks'; -import { UnitTestIocContainer } from './serviceRegistry'; - -use(chaiAsPromised); - -const testFilesPath = path.join(__dirname, '..', '..', '..', 'src', 'test', 'pythonFiles', 'testFiles', 'debuggerTest'); -// tslint:disable-next-line:variable-name -const EmptyTests = { - summary: { - passed: 0, - failures: 0, - errors: 0, - skipped: 0 - }, - testFiles: [], - testFunctions: [], - testSuites: [], - testFolders: [], - rootTestFolders: [] -}; - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests Stopping Discovery and Runner', () => { - let ioc: UnitTestIocContainer; - suiteSetup(initialize); - setup(async () => { - await initializeTest(); - initializeDI(); - }); - teardown(() => ioc.dispose()); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerProcessTypes(); - ioc.registerVariableTypes(); - - ioc.registerTestParsers(); - ioc.registerTestVisitors(); - ioc.registerTestResultsHelper(); - ioc.registerTestStorage(); - ioc.registerTestsHelper(); - ioc.registerTestDiagnosticServices(); - } - - test('Running tests should not stop existing discovery', async () => { - const mockTestManager = new MockTestManagerWithRunningTests(UNITTEST_PROVIDER, Product.unittest, Uri.file(testFilesPath), testFilesPath, ioc.serviceContainer); - ioc.serviceManager.addSingletonInstance<ITestDiscoveryService>(ITestDiscoveryService, new MockDiscoveryService(mockTestManager.discoveryDeferred.promise), UNITTEST_PROVIDER); - - const discoveryPromise = mockTestManager.discoverTests(CommandSource.auto); - mockTestManager.discoveryDeferred.resolve(EmptyTests); - const runningPromise = mockTestManager.runTest(CommandSource.ui); - const deferred = createDeferred<string>(); - - // This promise should never resolve nor reject. - runningPromise - .then(() => Promise.reject('Debugger stopped when it shouldn\'t have')) - .catch(error => deferred.reject(error)); - - discoveryPromise.then(result => { - if (result === EmptyTests) { - deferred.resolve(''); - } else { - deferred.reject('tests not empty'); - } - }).catch(error => deferred.reject(error)); - - await deferred.promise; - }); - - test('Discovering tests should stop running tests', async () => { - const mockTestManager = new MockTestManagerWithRunningTests(UNITTEST_PROVIDER, Product.unittest, Uri.file(testFilesPath), testFilesPath, ioc.serviceContainer); - ioc.serviceManager.addSingletonInstance<ITestDiscoveryService>(ITestDiscoveryService, new MockDiscoveryService(mockTestManager.discoveryDeferred.promise), UNITTEST_PROVIDER); - mockTestManager.discoveryDeferred.resolve(EmptyTests); - await mockTestManager.discoverTests(CommandSource.auto); - const runPromise = mockTestManager.runTest(CommandSource.ui); - // tslint:disable-next-line:no-string-based-set-timeout - await new Promise(resolve => setTimeout(resolve, 1000)); - - // User manually discovering tests will kill the existing test runner. - await mockTestManager.discoverTests(CommandSource.ui, true, false, true); - await expect(runPromise).to.eventually.be.rejectedWith(CANCELLATION_REASON); - }); -}); diff --git a/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts new file mode 100644 index 000000000000..643ea17903e6 --- /dev/null +++ b/src/test/testing/testController/common/buildErrorNodeOptions.unit.test.ts @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { buildErrorNodeOptions } from '../../../../client/testing/testController/common/utils'; + +suite('buildErrorNodeOptions - missing module detection', () => { + const workspaceUri = Uri.file('/test/workspace'); + + test('Should detect pytest ModuleNotFoundError and show missing module label', () => { + const errorMessage = + 'Traceback (most recent call last):\n File "<string>", line 1, in <module>\n import pytest\nModuleNotFoundError: No module named \'pytest\''; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: pytest [workspace]'); + expect(result.error).to.equal( + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect pytest ImportError and show missing module label', () => { + const errorMessage = 'ImportError: No module named pytest'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: pytest [workspace]'); + expect(result.error).to.equal( + "The module 'pytest' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect other missing modules and show module name in label', () => { + const errorMessage = + "bob\\test_bob.py:3: in <module>\n import requests\nE ModuleNotFoundError: No module named 'requests'\n=========================== short test summary info"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: requests [workspace]'); + expect(result.error).to.equal( + "The module 'requests' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should detect missing module with double quotes', () => { + const errorMessage = 'ModuleNotFoundError: No module named "numpy"'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('Missing Module: numpy [workspace]'); + expect(result.error).to.equal( + "The module 'numpy' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for non-module-related errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest'); + + expect(result.label).to.equal('pytest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should detect missing module for unittest errors', () => { + const errorMessage = "ModuleNotFoundError: No module named 'pandas'"; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Missing Module: pandas [workspace]'); + expect(result.error).to.equal( + "The module 'pandas' is not installed in the selected Python environment. Please install it to enable test discovery.", + ); + }); + + test('Should use generic error for unittest non-module errors', () => { + const errorMessage = 'Some other error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest'); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + expect(result.error).to.equal('Some other error occurred'); + }); + + test('Should use project name in label when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', 'my-project'); + + expect(result.label).to.equal('Unittest Discovery Error [my-project]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use project name in label for pytest when projectName is provided', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'pytest', 'ada'); + + expect(result.label).to.equal('pytest Discovery Error [ada]'); + expect(result.error).to.equal('Some error occurred'); + }); + + test('Should use folder name when projectName is undefined', () => { + const errorMessage = 'Some error occurred'; + + const result = buildErrorNodeOptions(workspaceUri, errorMessage, 'unittest', undefined); + + expect(result.label).to.equal('Unittest Discovery Error [workspace]'); + }); +}); diff --git a/src/test/testing/testController/common/projectTestExecution.unit.test.ts b/src/test/testing/testController/common/projectTestExecution.unit.test.ts new file mode 100644 index 000000000000..1cce2d1a8ce0 --- /dev/null +++ b/src/test/testing/testController/common/projectTestExecution.unit.test.ts @@ -0,0 +1,740 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { + CancellationToken, + CancellationTokenSource, + TestRun, + TestRunProfile, + TestRunProfileKind, + TestRunRequest, + Uri, +} from 'vscode'; +import { + createMockDependencies, + createMockProjectAdapter, + createMockTestItem, + createMockTestItemWithoutUri, + createMockTestRun, +} from '../testMocks'; +import { + executeTestsForProject, + executeTestsForProjects, + findProjectForTestItem, + getTestCaseNodesRecursive, + groupTestItemsByProject, + setupCoverageForProjects, +} from '../../../../client/testing/testController/common/projectTestExecution'; +import * as telemetry from '../../../../client/telemetry'; +import * as envExtApi from '../../../../client/envExt/api.internal'; + +suite('Project Test Execution', () => { + let sandbox: sinon.SinonSandbox; + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + // Default to disabled env extension for path-based fallback tests + useEnvExtensionStub = sandbox.stub(envExtApi, 'useEnvExtension').returns(false); + }); + + teardown(() => { + sandbox.restore(); + }); + + // ===== findProjectForTestItem Tests ===== + + suite('findProjectForTestItem', () => { + test('should return undefined when test item has no URI', async () => { + // Mock + const item = createMockTestItemWithoutUri('test1'); + const projects = [createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' })]; + + // Run + const result = await findProjectForTestItem(item, projects); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return matching project when item path is within project directory', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should return undefined when item path is outside all project directories', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.be.undefined; + }); + + test('should return most specific (deepest) project when nested projects exist', async () => { + // Mock - parent and child project with overlapping paths + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await findProjectForTestItem(item, [parentProject, childProject]); + + // Assert - should match child (longer path) not parent + expect(result).to.equal(childProject); + }); + + test('should return most specific project regardless of input order', async () => { + // Mock - same as above but different order + const item = createMockTestItem('test1', '/workspace/parent/child/tests/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run - pass child first, then parent + const result = await findProjectForTestItem(item, [childProject, parentProject]); + + // Assert - order shouldn't affect result + expect(result).to.equal(childProject); + }); + + test('should match item at project root level', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + }); + + test('should use env extension API when available', async () => { + // Enable env extension + useEnvExtensionStub.returns(true); + + // Mock the env extension API + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + const mockEnvApi = { + getPythonProject: sandbox.stub().returns({ uri: project.projectUri }), + }; + sandbox.stub(envExtApi, 'getEnvExtApi').resolves(mockEnvApi as any); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert + expect(result).to.equal(project); + expect(mockEnvApi.getPythonProject.calledOnceWith(item.uri)).to.be.true; + }); + + test('should fall back to path matching when env extension API is unavailable', async () => { + // Env extension enabled but throws + useEnvExtensionStub.returns(true); + sandbox.stub(envExtApi, 'getEnvExtApi').rejects(new Error('API unavailable')); + + // Mock + const item = createMockTestItem('test1', '/workspace/proj/tests/test_file.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await findProjectForTestItem(item, [project]); + + // Assert - should still work via fallback + expect(result).to.equal(project); + }); + }); + + // ===== groupTestItemsByProject Tests ===== + + suite('groupTestItemsByProject', () => { + test('should group single test item to its matching project', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.project).to.equal(project); + expect(entry.items).to.deep.equal([item]); + }); + + test('should aggregate multiple items belonging to same project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj/tests/test1.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/tests/test2.py'); + const item3 = createMockTestItem('test3', '/workspace/proj/test3.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [project]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(1); + const entry = Array.from(result.values())[0]; + expect(entry.items).to.have.length(3); + expect(new Set(entry.items)).to.deep.equal(new Set([item1, item2, item3])); + }); + + test('should separate items into groups by their owning project', async () => { + // Mock + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const item3 = createMockTestItem('test3', '/workspace/proj1/other_test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item1, item2, item3], [proj1, proj2]); + + // Assert - use Set for order-agnostic comparison + expect(result.size).to.equal(2); + const proj1Entry = result.get(proj1.projectUri.toString()); + const proj2Entry = result.get(proj2.projectUri.toString()); + expect(proj1Entry?.items).to.have.length(2); + expect(new Set(proj1Entry?.items)).to.deep.equal(new Set([item1, item3])); + expect(proj2Entry?.items).to.deep.equal([item2]); + }); + + test('should return empty map when no test items provided', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should exclude items that do not match any project path', async () => { + // Mock + const item = createMockTestItem('test1', '/other/path/test.py'); + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + + // Run + const result = await groupTestItemsByProject([item], [project]); + + // Assert + expect(result.size).to.equal(0); + }); + + test('should assign item to most specific (deepest) project for nested paths', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/parent/child/test.py'); + const parentProject = createMockProjectAdapter({ projectPath: '/workspace/parent', projectName: 'parent' }); + const childProject = createMockProjectAdapter({ + projectPath: '/workspace/parent/child', + projectName: 'child', + }); + + // Run + const result = await groupTestItemsByProject([item], [parentProject, childProject]); + + // Assert + expect(result.size).to.equal(1); + const entry = result.get(childProject.projectUri.toString()); + expect(entry?.project).to.equal(childProject); + expect(entry?.items).to.deep.equal([item]); + }); + + test('should omit projects that have no matching test items', async () => { + // Mock + const item = createMockTestItem('test1', '/workspace/proj1/test.py'); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + + // Run + const result = await groupTestItemsByProject([item], [proj1, proj2]); + + // Assert + expect(result.size).to.equal(1); + expect(result.has(proj1.projectUri.toString())).to.be.true; + expect(result.has(proj2.projectUri.toString())).to.be.false; + }); + }); + + // ===== getTestCaseNodesRecursive Tests ===== + + suite('getTestCaseNodesRecursive', () => { + test('should return single item when it is a leaf node with no children', () => { + // Mock + const item = createMockTestItem('test_func', '/test.py'); + + // Run + const result = getTestCaseNodesRecursive(item); + + // Assert + expect(result).to.deep.equal([item]); + }); + + test('should return all leaf nodes from single-level nested structure', () => { + // Mock + const leaf1 = createMockTestItem('test_method1', '/test.py'); + const leaf2 = createMockTestItem('test_method2', '/test.py'); + const classItem = createMockTestItem('TestClass', '/test.py', [leaf1, leaf2]); + + // Run + const result = getTestCaseNodesRecursive(classItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should traverse deeply nested structure to find all leaf nodes', () => { + // Mock - 3 levels deep: file → class → inner class → test + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const innerClass = createMockTestItem('InnerClass', '/test.py', [leaf2]); + const outerClass = createMockTestItem('OuterClass', '/test.py', [leaf1, innerClass]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [outerClass]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(2); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2])); + }); + + test('should collect leaves from multiple sibling branches', () => { + // Mock - multiple test classes at same level + const leaf1 = createMockTestItem('test1', '/test.py'); + const leaf2 = createMockTestItem('test2', '/test.py'); + const leaf3 = createMockTestItem('test3', '/test.py'); + const class1 = createMockTestItem('Class1', '/test.py', [leaf1]); + const class2 = createMockTestItem('Class2', '/test.py', [leaf2, leaf3]); + const fileItem = createMockTestItem('test_file.py', '/test.py', [class1, class2]); + + // Run + const result = getTestCaseNodesRecursive(fileItem); + + // Assert - use Set for order-agnostic comparison + expect(result).to.have.length(3); + expect(new Set(result)).to.deep.equal(new Set([leaf1, leaf2, leaf3])); + }); + }); + + // ===== executeTestsForProject Tests ===== + + suite('executeTestsForProject', () => { + test('should call executionAdapter.runTests with project URI and mapped test IDs', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'test_file.py::test1'); + const testItem = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [testItem], runMock.object, request, deps); + + // Assert + expect(project.executionAdapterStub.calledOnce).to.be.true; + const callArgs = project.executionAdapterStub.firstCall.args; + expect(callArgs[0].fsPath).to.equal(project.projectUri.fsPath); // uri + expect(callArgs[1]).to.deep.equal(['test_file.py::test1']); // testCaseIds + expect(callArgs[7]).to.equal(project); // project + }); + + test('should mark all leaf test items as started in the test run', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - both items marked as started + runMock.verify((r) => r.started(item1), typemoq.Times.once()); + runMock.verify((r) => r.started(item2), typemoq.Times.once()); + }); + + test('should resolve test IDs via resultResolver.vsIdToRunId mapping', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'path/to/test1'); + project.resultResolver.vsIdToRunId.set('test2', 'path/to/test2'); + const item1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item1, item2], runMock.object, request, deps); + + // Assert - use Set for order-agnostic comparison + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(new Set(passedTestIds)).to.deep.equal(new Set(['path/to/test1', 'path/to/test2'])); + }); + + test('should skip execution when no items have vsIdToRunId mappings', async () => { + // Mock - no mappings set, so lookups return undefined + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('unmapped_test', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [item], runMock.object, request, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should recursively expand nested test items to find leaf nodes', async () => { + // Mock - class containing two test methods + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const leaf1 = createMockTestItem('test1', '/workspace/proj/test.py'); + const leaf2 = createMockTestItem('test2', '/workspace/proj/test.py'); + const classItem = createMockTestItem('TestClass', '/workspace/proj/test.py', [leaf1, leaf2]); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + project.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProject(project, [classItem], runMock.object, request, deps); + + // Assert - leaf nodes marked as started, not the parent class + runMock.verify((r) => r.started(leaf1), typemoq.Times.once()); + runMock.verify((r) => r.started(leaf2), typemoq.Times.once()); + const passedTestIds = project.executionAdapterStub.firstCall.args[1] as string[]; + expect(passedTestIds).to.have.length(2); + }); + }); + + // ===== executeTestsForProjects Tests ===== + + suite('executeTestsForProjects', () => { + let telemetryStub: sinon.SinonStub; + + setup(() => { + telemetryStub = sandbox.stub(telemetry, 'sendTelemetryEvent'); + }); + + test('should return immediately when empty projects array provided', async () => { + // Mock + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([], [], runMock.object, request, token, deps); + + // Assert - no telemetry sent since no projects executed + expect(telemetryStub.called).to.be.false; + }); + + test('should skip execution when cancellation requested before start', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const tokenSource = new CancellationTokenSource(); + tokenSource.cancel(); // Pre-cancel + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, tokenSource.token, deps); + + // Assert - execution adapter never called + expect(project.executionAdapterStub.called).to.be.false; + }); + + test('should execute tests for each project when multiple projects provided', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - both projects had their execution adapters called + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should emit telemetry event for each project execution', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - telemetry sent twice (once per project) + expect(telemetryStub.callCount).to.equal(2); + }); + + test('should stop processing remaining projects when cancellation requested mid-execution', async () => { + // Mock + const tokenSource = new CancellationTokenSource(); + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + // First project triggers cancellation during its execution + proj1.executionAdapterStub.callsFake(async () => { + tokenSource.cancel(); + }); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects( + [proj1, proj2], + [item1, item2], + runMock.object, + request, + tokenSource.token, + deps, + ); + + // Assert - first project executed, second may be skipped due to cancellation check + expect(proj1.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should continue executing remaining projects when one project fails', async () => { + // Mock + const proj1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const proj2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + proj1.executionAdapterStub.rejects(new Error('Execution failed')); + proj1.resultResolver.vsIdToRunId.set('test1', 'runId1'); + proj2.resultResolver.vsIdToRunId.set('test2', 'runId2'); + const item1 = createMockTestItem('test1', '/workspace/proj1/test.py'); + const item2 = createMockTestItem('test2', '/workspace/proj2/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Run } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run - should not throw + await executeTestsForProjects([proj1, proj2], [item1, item2], runMock.object, request, token, deps); + + // Assert - second project still executed despite first failing + expect(proj2.executionAdapterStub.calledOnce).to.be.true; + }); + + test('should configure loadDetailedCoverage callback when run profile is Coverage', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - loadDetailedCoverage callback was configured + expect(profileMock.loadDetailedCoverage).to.not.be.undefined; + }); + + test('should include debugging=true in telemetry when run profile is Debug', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + project.resultResolver.vsIdToRunId.set('test1', 'runId1'); + const item = createMockTestItem('test1', '/workspace/proj/test.py'); + const runMock = createMockTestRun(); + const token = new CancellationTokenSource().token; + const request = { profile: { kind: TestRunProfileKind.Debug } } as TestRunRequest; + const deps = createMockDependencies(); + + // Run + await executeTestsForProjects([project], [item], runMock.object, request, token, deps); + + // Assert - telemetry contains debugging=true + expect(telemetryStub.calledOnce).to.be.true; + const telemetryProps = telemetryStub.firstCall.args[2]; + expect(telemetryProps.debugging).to.be.true; + }); + }); + + // ===== setupCoverageForProjects Tests ===== + + suite('setupCoverageForProjects', () => { + test('should configure loadDetailedCoverage callback when profile kind is Coverage', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.a('function'); + }); + + test('should leave loadDetailedCoverage undefined when profile kind is Run', () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Run, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run + setupCoverageForProjects(request, [project]); + + // Assert + expect(profileMock.loadDetailedCoverage).to.be.undefined; + }); + + test('should return coverage data from detailedCoverageMap when loadDetailedCoverage is called', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const mockCoverageDetails = [{ line: 1, executed: true }]; + // Use Uri.fsPath as the key to match the implementation's lookup + const fileUri = Uri.file('/workspace/proj/file.py'); + project.resultResolver.detailedCoverageMap.set(fileUri.fsPath, mockCoverageDetails as any); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call the configured callback + const fileCoverage = { uri: fileUri }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal(mockCoverageDetails); + }); + + test('should return empty array when file has no coverage data in map', async () => { + // Mock + const project = createMockProjectAdapter({ projectPath: '/workspace/proj', projectName: 'proj' }); + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage + setupCoverageForProjects(request, [project]); + + // Run - call callback for file not in map + const fileCoverage = { uri: Uri.file('/workspace/proj/uncovered_file.py') }; + const result = await profileMock.loadDetailedCoverage!( + {} as TestRun, + fileCoverage as any, + {} as CancellationToken, + ); + + // Assert + expect(result).to.deep.equal([]); + }); + + test('should route to correct project when multiple projects have coverage data', async () => { + // Mock - two projects with different coverage data + const project1 = createMockProjectAdapter({ projectPath: '/workspace/proj1', projectName: 'proj1' }); + const project2 = createMockProjectAdapter({ projectPath: '/workspace/proj2', projectName: 'proj2' }); + const coverage1 = [{ line: 1, executed: true }]; + const coverage2 = [{ line: 2, executed: false }]; + const file1Uri = Uri.file('/workspace/proj1/file1.py'); + const file2Uri = Uri.file('/workspace/proj2/file2.py'); + project1.resultResolver.detailedCoverageMap.set(file1Uri.fsPath, coverage1 as any); + project2.resultResolver.detailedCoverageMap.set(file2Uri.fsPath, coverage2 as any); + + const profileMock = ({ + kind: TestRunProfileKind.Coverage, + loadDetailedCoverage: undefined, + } as unknown) as TestRunProfile; + const request = { profile: profileMock } as TestRunRequest; + + // Run - configure coverage with both projects + setupCoverageForProjects(request, [project1, project2]); + + // Assert - can get coverage from both projects through single callback + const result1 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file1Uri } as any, + {} as CancellationToken, + ); + const result2 = await profileMock.loadDetailedCoverage!( + {} as TestRun, + { uri: file2Uri } as any, + {} as CancellationToken, + ); + + expect(result1).to.deep.equal(coverage1); + expect(result2).to.deep.equal(coverage2); + }); + }); +}); diff --git a/src/test/testing/testController/common/projectUtils.unit.test.ts b/src/test/testing/testController/common/projectUtils.unit.test.ts new file mode 100644 index 000000000000..75f399e89fc0 --- /dev/null +++ b/src/test/testing/testController/common/projectUtils.unit.test.ts @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { Uri } from 'vscode'; +import { + getProjectId, + createProjectDisplayName, + parseVsId, + PROJECT_ID_SEPARATOR, +} from '../../../../client/testing/testController/common/projectUtils'; + +suite('Project Utils Tests', () => { + suite('getProjectId', () => { + test('should return URI string representation', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + expect(id).to.equal(uri.toString()); + }); + + test('should be consistent for same URI', () => { + const uri = Uri.file('/workspace/project'); + + const id1 = getProjectId(uri); + const id2 = getProjectId(uri); + + expect(id1).to.equal(id2); + }); + + test('should be different for different URIs', () => { + const uri1 = Uri.file('/workspace/project1'); + const uri2 = Uri.file('/workspace/project2'); + + const id1 = getProjectId(uri1); + const id2 = getProjectId(uri2); + + expect(id1).to.not.equal(id2); + }); + + test('should handle Windows paths', () => { + const uri = Uri.file('C:\\workspace\\project'); + + const id = getProjectId(uri); + + expect(id).to.be.a('string'); + expect(id).to.have.length.greaterThan(0); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should match Python Environments extension format', () => { + const uri = Uri.file('/workspace/project'); + + const id = getProjectId(uri); + + // Should match how Python Environments extension keys projects + expect(id).to.equal(uri.toString()); + expect(typeof id).to.equal('string'); + }); + }); + + suite('createProjectDisplayName', () => { + test('should format name with major.minor version', () => { + const result = createProjectDisplayName('MyProject', '3.11.2'); + + expect(result).to.equal('MyProject (Python 3.11)'); + }); + + test('should handle version with patch and pre-release', () => { + const result = createProjectDisplayName('MyProject', '3.12.0rc1'); + + expect(result).to.equal('MyProject (Python 3.12)'); + }); + + test('should handle version with only major.minor', () => { + const result = createProjectDisplayName('MyProject', '3.10'); + + expect(result).to.equal('MyProject (Python 3.10)'); + }); + + test('should handle invalid version format gracefully', () => { + const result = createProjectDisplayName('MyProject', 'invalid-version'); + + expect(result).to.equal('MyProject (Python invalid-version)'); + }); + + test('should handle empty version string', () => { + const result = createProjectDisplayName('MyProject', ''); + + expect(result).to.equal('MyProject (Python )'); + }); + + test('should handle version with single digit', () => { + const result = createProjectDisplayName('MyProject', '3'); + + expect(result).to.equal('MyProject (Python 3)'); + }); + + test('should handle project name with special characters', () => { + const result = createProjectDisplayName('My-Project_123', '3.11.5'); + + expect(result).to.equal('My-Project_123 (Python 3.11)'); + }); + + test('should handle empty project name', () => { + const result = createProjectDisplayName('', '3.11.2'); + + expect(result).to.equal(' (Python 3.11)'); + }); + }); + + suite('parseVsId', () => { + test('should parse project-scoped ID correctly', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle legacy ID without project scope', () => { + const vsId = 'test_file.py'; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.be.undefined; + expect(runId).to.equal('test_file.py'); + }); + + test('should handle runId containing separator', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}test_file.py::test_class::test_method`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('test_file.py::test_class::test_method'); + }); + + test('should handle empty project ID', () => { + const vsId = `${PROJECT_ID_SEPARATOR}test_file.py::test_name`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal(''); + expect(runId).to.equal('test_file.py::test_name'); + }); + + test('should handle empty runId', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal(''); + }); + + test('should handle ID with file path', () => { + const vsId = `project-abc123def456${PROJECT_ID_SEPARATOR}/workspace/tests/test_file.py`; + + const [projectId, runId] = parseVsId(vsId); + + expect(projectId).to.equal('project-abc123def456'); + expect(runId).to.equal('/workspace/tests/test_file.py'); + }); + + test('should handle Windows file paths', () => { + const projectUri = Uri.file('/workspace/project'); + const projectId = getProjectId(projectUri); + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}C:\\workspace\\tests\\test_file.py`; + + const [parsedProjectId, runId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(runId).to.equal('C:\\workspace\\tests\\test_file.py'); + }); + }); + + suite('Integration Tests', () => { + test('should generate unique IDs for different URIs', () => { + const uris = [ + Uri.file('/workspace/a'), + Uri.file('/workspace/b'), + Uri.file('/workspace/c'), + Uri.file('/workspace/d'), + Uri.file('/workspace/e'), + ]; + + const ids = uris.map((uri) => getProjectId(uri)); + const uniqueIds = new Set(ids); + + expect(uniqueIds.size).to.equal(uris.length, 'All IDs should be unique'); + }); + + test('should handle nested project paths', () => { + const parentUri = Uri.file('/workspace/parent'); + const childUri = Uri.file('/workspace/parent/child'); + + const parentId = getProjectId(parentUri); + const childId = getProjectId(childUri); + + expect(parentId).to.not.equal(childId); + }); + + test('should create complete vsId and parse it back', () => { + const projectUri = Uri.file('/workspace/myproject'); + const projectId = getProjectId(projectUri); + const runId = 'tests/test_module.py::TestClass::test_method'; + const vsId = `${projectId}${PROJECT_ID_SEPARATOR}${runId}`; + + const [parsedProjectId, parsedRunId] = parseVsId(vsId); + + expect(parsedProjectId).to.equal(projectId); + expect(parsedRunId).to.equal(runId); + }); + + test('should match Python Environments extension URI format', () => { + const uri = Uri.file('/workspace/project'); + + const projectId = getProjectId(uri); + + // Should be string representation of URI + expect(projectId).to.equal(uri.toString()); + expect(typeof projectId).to.equal('string'); + }); + }); +}); diff --git a/src/test/testing/testController/common/testCoverageHandler.unit.test.ts b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts new file mode 100644 index 000000000000..a81aed591128 --- /dev/null +++ b/src/test/testing/testController/common/testCoverageHandler.unit.test.ts @@ -0,0 +1,502 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestRun, Uri, FileCoverage } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import { TestCoverageHandler } from '../../../../client/testing/testController/common/testCoverageHandler'; +import { CoveragePayload } from '../../../../client/testing/testController/common/types'; + +suite('TestCoverageHandler', () => { + let coverageHandler: TestCoverageHandler; + let runInstanceMock: typemoq.IMock<TestRun>; + + setup(() => { + coverageHandler = new TestCoverageHandler(); + runInstanceMock = typemoq.Mock.ofType<TestRun>(); + }); + + suite('processCoverage', () => { + test('should return empty map for undefined result', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: undefined, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.never()); + }); + + test('should create FileCoverage for each file', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + '/path/to/file2.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + }, + error: '', + }; + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + runInstanceMock.verify((r) => r.addCoverage(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test('should call runInstance.addCoverage with correct FileCoverage', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 5, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + assert.strictEqual(capturedCoverage!.uri.fsPath, Uri.file('/path/to/file.py').fsPath); + }); + + test('should return detailed coverage map with correct keys', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file1.py': { + lines_covered: [1, 2], + lines_missed: [3], + executed_branches: 2, + total_branches: 4, + }, + '/path/to/file2.py': { + lines_covered: [5, 6, 7], + lines_missed: [], + executed_branches: 3, + total_branches: 3, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 2); + assert.ok(result.has(Uri.file('/path/to/file1.py').fsPath)); + assert.ok(result.has(Uri.file('/path/to/file2.py').fsPath)); + }); + + test('should handle empty coverage data', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: {}, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.strictEqual(result.size, 0); + }); + + test('should handle file with no covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only missed lines + }); + + test('should handle file with no missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 5, + total_branches: 5, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); // Only covered lines + }); + + test('should handle undefined lines_covered', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: undefined as any, + lines_missed: [1, 2], + executed_branches: 0, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only missed lines + }); + + test('should handle undefined lines_missed', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2], + lines_missed: undefined as any, + executed_branches: 2, + total_branches: 2, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 2); // Only covered lines + }); + }); + + suite('createFileCoverage', () => { + test('should handle line coverage only when totalBranches is -1', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4, 5], + executed_branches: 0, + total_branches: -1, // Branch coverage disabled + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Branch coverage should not be included + assert.strictEqual((capturedCoverage as any).branchCoverage, undefined); + }); + + test('should include branch coverage when available', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [4], + executed_branches: 7, + total_branches: 10, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // Should have branch coverage + assert.ok((capturedCoverage as any).branchCoverage); + }); + + test('should calculate line coverage counts correctly', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3, 4, 5], + lines_missed: [6, 7], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + let capturedCoverage: FileCoverage | undefined; + runInstanceMock + .setup((r) => r.addCoverage(typemoq.It.isAny())) + .callback((coverage: FileCoverage) => { + capturedCoverage = coverage; + }); + + coverageHandler.processCoverage(payload, runInstanceMock.object); + + assert.ok(capturedCoverage); + // 5 covered out of 7 total (5 covered + 2 missed) + assert.strictEqual((capturedCoverage as any).statementCoverage.covered, 5); + assert.strictEqual((capturedCoverage as any).statementCoverage.total, 7); + }); + }); + + suite('createDetailedCoverage', () => { + test('should create StatementCoverage for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 2, 3], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be covered (true) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, true); + }); + }); + + test('should create StatementCoverage for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [1, 2, 3], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // All should be NOT covered (false) + detailedCoverage!.forEach((coverage) => { + assert.strictEqual((coverage as any).executed, false); + }); + }); + + test('should convert 1-indexed to 0-indexed line numbers for covered lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 5, 10], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 1 should map to range starting at line 0 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 0); + // Line 5 should map to range starting at line 4 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4); + // Line 10 should map to range starting at line 9 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9); + }); + + test('should convert 1-indexed to 0-indexed line numbers for missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [], + lines_missed: [3, 7, 12], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + // Line 3 should map to range starting at line 2 + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 2); + // Line 7 should map to range starting at line 6 + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 6); + // Line 12 should map to range starting at line 11 + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 11); + }); + + test('should handle large line numbers', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1000, 5000, 10000], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 3); + + // Verify conversion is correct for large numbers + assert.strictEqual((detailedCoverage![0] as any).location.start.line, 999); + assert.strictEqual((detailedCoverage![1] as any).location.start.line, 4999); + assert.strictEqual((detailedCoverage![2] as any).location.start.line, 9999); + }); + + test('should create detailed coverage with both covered and missed lines', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1, 3, 5], + lines_missed: [2, 4, 6], + executed_branches: 3, + total_branches: 6, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + assert.strictEqual(detailedCoverage!.length, 6); // 3 covered + 3 missed + + // Count covered vs not covered + const covered = detailedCoverage!.filter((c) => (c as any).executed === true); + const notCovered = detailedCoverage!.filter((c) => (c as any).executed === false); + + assert.strictEqual(covered.length, 3); + assert.strictEqual(notCovered.length, 3); + }); + + test('should set range to cover entire line', () => { + const payload: CoveragePayload = { + coverage: true, + cwd: '/foo/bar', + result: { + '/path/to/file.py': { + lines_covered: [1], + lines_missed: [], + executed_branches: 0, + total_branches: -1, + }, + }, + error: '', + }; + + const result = coverageHandler.processCoverage(payload, runInstanceMock.object); + + const detailedCoverage = result.get(Uri.file('/path/to/file.py').fsPath); + assert.ok(detailedCoverage); + + const coverage = detailedCoverage![0] as any; + // Start at column 0 + assert.strictEqual(coverage.location.start.character, 0); + // End at max safe integer (entire line) + assert.strictEqual(coverage.location.end.character, Number.MAX_SAFE_INTEGER); + }); + }); +}); diff --git a/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts new file mode 100644 index 000000000000..458e3d984405 --- /dev/null +++ b/src/test/testing/testController/common/testDiscoveryHandler.unit.test.ts @@ -0,0 +1,517 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, CancellationToken, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestDiscoveryHandler } from '../../../../client/testing/testController/common/testDiscoveryHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { DiscoveredTestPayload, DiscoveredTestNode } from '../../../../client/testing/testController/common/types'; +import { TestProvider } from '../../../../client/testing/types'; +import * as utils from '../../../../client/testing/testController/common/utils'; +import * as testItemUtilities from '../../../../client/testing/testController/common/testItemUtilities'; + +suite('TestDiscoveryHandler', () => { + let discoveryHandler: TestDiscoveryHandler; + let testControllerMock: typemoq.IMock<TestController>; + let testItemIndexMock: typemoq.IMock<TestItemIndex>; + let testItemCollectionMock: typemoq.IMock<TestItemCollection>; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + + setup(() => { + discoveryHandler = new TestDiscoveryHandler(); + testControllerMock = typemoq.Mock.ofType<TestController>(); + testItemIndexMock = typemoq.Mock.ofType<TestItemIndex>(); + testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + + // Setup default test controller items mock + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + testItemCollectionMock.setup((x) => x.delete(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + workspaceUri = Uri.file('/foo/bar'); + testProvider = 'pytest'; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processDiscovery', () => { + test('should handle null payload gracefully', () => { + discoveryHandler.processDiscovery( + null as any, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should not throw and should not call populateTestTree + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.never()); + }); + + test('should call populateTestTree with correct params on success', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + + // Setup map getters for populateTestTree + const mockRunIdMap = new Map(); + const mockVSidMap = new Map(); + const mockVStoRunMap = new Map(); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => mockRunIdMap); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => mockVSidMap); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => mockVStoRunMap); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + assert.ok(populateTestTreeStub.calledOnce); + sinon.assert.calledWith( + populateTestTreeStub, + testControllerMock.object, + tests, + undefined, + sinon.match.any, + cancelationToken, + ); + }); + + test('should clear index before populating', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + + const clearSpy = sinon.spy(); + testItemIndexMock.setup((x) => x.clear()).callback(clearSpy); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(clearSpy.calledOnce); + }); + + test('should handle error status and create error node', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Error message 1', 'Error message 2'], + }; + + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(createErrorNodeSpy.calledOnce); + assert.ok( + createErrorNodeSpy.calledWith(testControllerMock.object, workspaceUri, payload.error, testProvider), + ); + }); + + test('should handle both errors and tests in same payload', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: ['Partial error'], + tests, + }; + + sinon.stub(utils, 'populateTestTree'); + const createErrorNodeSpy = sinon.spy(discoveryHandler, 'createErrorNode'); + + // Mock createTestItem to return a proper TestItem + const mockErrorItem = ({ + id: 'error_id', + error: null, + canResolveChildren: false, + tags: [], + } as unknown) as TestItem; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockErrorItem); + + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should create error node AND populate test tree + assert.ok(createErrorNodeSpy.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + + test('should delete error node on successful discovery', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const deleteSpy = sinon.spy(); + // Reset and reconfigure the collection mock to capture delete call + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.delete(typemoq.It.isAny())) + .callback(deleteSpy) + .returns(() => undefined); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + assert.ok(deleteSpy.calledOnce); + assert.ok(deleteSpy.calledWith(`DiscoveryError:${workspaceUri.fsPath}`)); + }); + + test('should respect cancellation token', () => { + const tests: DiscoveredTestNode = { + path: '/foo/bar', + name: 'root', + type_: 'folder', + id_: 'root_id', + children: [], + }; + + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Verify token was passed to populateTestTree + assert.ok(populateTestTreeStub.calledOnce); + const lastArg = populateTestTreeStub.getCall(0).args[4]; + assert.strictEqual(lastArg, cancelationToken); + }); + + test('should handle null tests in payload', () => { + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests: null as any, + }; + + const populateTestTreeStub = sinon.stub(utils, 'populateTestTree'); + testItemIndexMock.setup((x) => x.clear()).returns(() => undefined); + testItemIndexMock.setup((x) => x.runIdToTestItemMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.runIdToVSidMap).returns(() => new Map()); + testItemIndexMock.setup((x) => x.vsIdToRunIdMap).returns(() => new Map()); + + discoveryHandler.processDiscovery( + payload, + testControllerMock.object, + testItemIndexMock.object, + workspaceUri, + testProvider, + cancelationToken, + ); + + // Should still call populateTestTree with null + assert.ok(populateTestTreeStub.calledOnce); + testItemIndexMock.verify((x) => x.clear(), typemoq.Times.once()); + }); + }); + + suite('createErrorNode', () => { + test('should create error with correct message for pytest', () => { + const error = ['Error line 1', 'Error line 2']; + testProvider = 'pytest'; + + const buildErrorNodeOptionsStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(buildErrorNodeOptionsStub.calledOnce); + assert.ok(createErrorTestItemStub.calledOnce); + assert.ok(mockErrorItem.error !== null); + }); + + test('should create error with correct message for unittest', () => { + const error = ['Unittest error']; + testProvider = 'unittest'; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error !== null); + }); + + test('should set markdown error label correctly', () => { + const error = ['Test error']; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + assert.ok(mockErrorItem.error); + assert.strictEqual( + (mockErrorItem.error as any).value, + '[Show output](command:python.viewOutput) to view error logs', + ); + assert.strictEqual((mockErrorItem.error as any).isTrusted, true); + }); + + test('should handle undefined error array', () => { + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, undefined, testProvider); + + // Should not throw + assert.ok(mockErrorItem.error !== null); + }); + + test('should reuse existing error node if present', () => { + const error = ['Error']; + + // Create a proper object with settable error property + const existingErrorItem: any = { + id: `DiscoveryError:${workspaceUri.fsPath}`, + error: null, + canResolveChildren: false, + tags: [], + }; + + sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: `DiscoveryError:${workspaceUri.fsPath}`, + label: 'Error Label', + error: 'Error Message', + }); + + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem'); + + // Reset and setup collection to return existing item + testItemCollectionMock.reset(); + testItemCollectionMock + .setup((x) => x.get(`DiscoveryError:${workspaceUri.fsPath}`)) + .returns(() => existingErrorItem); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Should not create a new error item + assert.ok(createErrorTestItemStub.notCalled); + // Should still update the error property + assert.ok(existingErrorItem.error !== null); + }); + + test('should handle multiple error messages', () => { + const error = ['Error 1', 'Error 2', 'Error 3']; + + const buildStub = sinon.stub(utils, 'buildErrorNodeOptions').returns({ + id: 'error_id', + label: 'Error Label', + error: 'Error Message', + }); + + const mockErrorItem = ({ + id: 'error_id', + error: null, + } as unknown) as TestItem; + + sinon.stub(testItemUtilities, 'createErrorTestItem').returns(mockErrorItem); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testItemCollectionMock.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + discoveryHandler.createErrorNode(testControllerMock.object, workspaceUri, error, testProvider); + + // Verify the error messages are joined + const expectedMessage = sinon.match((value: string) => { + return value.includes('Error 1') && value.includes('Error 2') && value.includes('Error 3'); + }); + sinon.assert.calledWith(buildStub, workspaceUri, expectedMessage, testProvider); + }); + }); +}); diff --git a/src/test/testing/testController/common/testExecutionHandler.unit.test.ts b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts new file mode 100644 index 000000000000..c6be4548c192 --- /dev/null +++ b/src/test/testing/testController/common/testExecutionHandler.unit.test.ts @@ -0,0 +1,922 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, TestRun, TestMessage, Uri, Range, TestItemCollection, MarkdownString } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestExecutionHandler } from '../../../../client/testing/testController/common/testExecutionHandler'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; +import { ExecutionTestPayload } from '../../../../client/testing/testController/common/types'; + +suite('TestExecutionHandler', () => { + let executionHandler: TestExecutionHandler; + let testControllerMock: typemoq.IMock<TestController>; + let testItemIndexMock: typemoq.IMock<TestItemIndex>; + let runInstanceMock: typemoq.IMock<TestRun>; + let mockTestItem: TestItem; + let mockParentItem: TestItem; + + setup(() => { + executionHandler = new TestExecutionHandler(); + testControllerMock = typemoq.Mock.ofType<TestController>(); + testItemIndexMock = typemoq.Mock.ofType<TestItemIndex>(); + runInstanceMock = typemoq.Mock.ofType<TestRun>(); + + mockTestItem = createMockTestItem('test1', 'Test 1'); + mockParentItem = createMockTestItem('parentTest', 'Parent Test'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('processExecution', () => { + test('should process empty payload without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: {}, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process undefined result without errors', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // No errors should be thrown + }); + + test('should process multiple test results', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { test: 'test1', outcome: 'success', message: '', traceback: '' }, + test2: { test: 'test2', outcome: 'failure', message: 'Failed', traceback: 'traceback' }, + }, + error: '', + }; + + const mockTestItem2 = createMockTestItem('test2', 'Test 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + testItemIndexMock + .setup((x) => x.getTestItem('test2', testControllerMock.object)) + .returns(() => mockTestItem2); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockTestItem2, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestError', () => { + test('should create error message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error occurred', + traceback: 'line1\nline2\nline3', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Error occurred')); + assert.ok(messageText.includes('line1')); + assert.ok(messageText.includes('line2')); + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should set location when test item has range', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.errored(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + assert.ok(capturedMessage!.location); + assert.strictEqual(capturedMessage!.location!.uri.fsPath, mockTestItem.uri!.fsPath); + }); + + test('should handle missing traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'error', + message: 'Error', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.errored(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestFailure', () => { + test('should create failure message with traceback', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'failure', + message: 'Assertion failed', + traceback: 'AssertionError\nline1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + let capturedMessage: TestMessage | undefined; + runInstanceMock + .setup((r) => r.failed(mockTestItem, typemoq.It.isAny())) + .callback((_, message: TestMessage) => { + capturedMessage = message; + }); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + assert.ok(capturedMessage); + const messageText = + capturedMessage!.message instanceof MarkdownString + ? capturedMessage!.message.value + : capturedMessage!.message; + assert.ok(messageText.includes('Assertion failed')); + assert.ok(messageText.includes('AssertionError')); + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should handle passed-unexpected outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'passed-unexpected', + message: 'Unexpected pass', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.failed(mockTestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + }); + + suite('handleTestSuccess', () => { + test('should mark test as passed', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should handle expected-failure outcome', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'expected-failure', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockTestItem), typemoq.Times.once()); + }); + + test('should not call passed when test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'success', + message: '', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock.setup((x) => x.getTestItem('test1', testControllerMock.object)).returns(() => undefined); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); + + suite('handleTestSkipped', () => { + test('should mark test as skipped', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + test1: { + test: 'test1', + outcome: 'skipped', + message: 'Test skipped', + traceback: '', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('test1', testControllerMock.object)) + .returns(() => mockTestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.skipped(mockTestItem), typemoq.Times.once()); + }); + }); + + suite('handleSubtestFailure', () => { + test('should create child test item for subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Subtest failed', + traceback: 'traceback', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.failed === 1 && stats.passed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtestItem, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should update stats correctly for multiple subtests', () => { + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + + // First subtest: no existing stats + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + // Return different items based on call order + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => { + callCount++; + return callCount === 1 ? mockSubtest1 : mockSubtest2; + }); + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Second subtest: should have existing stats from first + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ failed: 1, passed: 0 })); + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify the first subtest set initial stats + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + }); + + test('should throw error when parent test item not found', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => undefined); + + assert.throws(() => { + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + }, /Parent test item not found/); + }); + }); + + suite('handleSubtestSuccess', () => { + test('should create passing subtest', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify stats were set correctly + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'parentTest', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + runInstanceMock.verify((r) => r.started(mockSubtestItem), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + + test('should handle subtest with special characters in name', () => { + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest [subtest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest with spaces and [brackets]', + }, + }, + error: '', + }; + + const mockSubtestItem = createMockTestItem('[subtest with spaces and [brackets]]', 'Subtest'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtestItem); + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + runInstanceMock.verify((r) => r.passed(mockSubtestItem), typemoq.Times.once()); + }); + }); + + suite('Comprehensive Subtest Scenarios', () => { + test('should handle mixed passing and failing subtests in sequence', () => { + // Simulates unittest with subtests like: test_even with i=0,1,2,3,4,5 + const mockSubtest0 = createMockTestItem('(i=0)', '(i=0)'); + const mockSubtest1 = createMockTestItem('(i=1)', '(i=1)'); + const mockSubtest2 = createMockTestItem('(i=2)', '(i=2)'); + const mockSubtest3 = createMockTestItem('(i=3)', '(i=3)'); + const mockSubtest4 = createMockTestItem('(i=4)', '(i=4)'); + const mockSubtest5 = createMockTestItem('(i=5)', '(i=5)'); + + const subtestItems = [mockSubtest0, mockSubtest1, mockSubtest2, mockSubtest3, mockSubtest4, mockSubtest5]; + + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + + let subtestCallCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => subtestItems[subtestCallCount++]); + + // First subtest (i=0) - passes + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => undefined); + testItemIndexMock.setup((x) => x.setSubtestStats('test_even', typemoq.It.isAny())).returns(() => undefined); + + const payload0: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=0)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=0)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload0, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify first subtest created stats + testItemIndexMock.verify( + (x) => + x.setSubtestStats( + 'test_even', + typemoq.It.is((stats) => stats.passed === 1 && stats.failed === 0), + ), + typemoq.Times.once(), + ); + + // Second subtest (i=1) - fails + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 0 })); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=1)': { + test: 'test_even', + outcome: 'subtest-failure', + message: '1 is not even', + traceback: 'AssertionError', + subtest: '(i=1)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Third subtest (i=2) - passes + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('test_even', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('test_even')).returns(() => ({ passed: 1, failed: 1 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'test_even (i=2)': { + test: 'test_even', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: '(i=2)', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify all subtests were started and had outcomes + runInstanceMock.verify((r) => r.started(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest0), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest1, typemoq.It.isAny()), typemoq.Times.once()); + runInstanceMock.verify((r) => r.started(mockSubtest2), typemoq.Times.once()); + runInstanceMock.verify((r) => r.passed(mockSubtest2), typemoq.Times.once()); + }); + + test('should persist stats across multiple processExecution calls', () => { + // Test that stats persist in TestItemIndex across multiple processExecution calls + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + const mockSubtest2 = createMockTestItem('subtest2', 'Subtest 2'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + + let callCount = 0; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => (callCount++ === 0 ? mockSubtest1 : mockSubtest2)); + + const payload1: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + // First call - no existing stats + executionHandler.processExecution( + payload1, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Simulate stats being stored in TestItemIndex + testItemIndexMock.reset(); + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => ({ passed: 1, failed: 0 })); + + const payload2: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest2)': { + test: 'parentTest', + outcome: 'subtest-failure', + message: 'Failed', + traceback: '', + subtest: 'subtest2', + }, + }, + error: '', + }; + + // Second call - existing stats should be retrieved and updated + executionHandler.processExecution( + payload2, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify getSubtestStats was called to retrieve existing stats + testItemIndexMock.verify((x) => x.getSubtestStats('parentTest'), typemoq.Times.once()); + + // Verify both subtests were processed + runInstanceMock.verify((r) => r.passed(mockSubtest1), typemoq.Times.once()); + runInstanceMock.verify((r) => r.failed(mockSubtest2, typemoq.It.isAny()), typemoq.Times.once()); + }); + + test('should clear children only on first subtest when no existing stats', () => { + // When first subtest arrives, children should be cleared + // Subsequent subtests should NOT clear children + const mockSubtest1 = createMockTestItem('subtest1', 'Subtest 1'); + + testItemIndexMock + .setup((x) => x.getTestItem('parentTest', testControllerMock.object)) + .returns(() => mockParentItem); + testItemIndexMock.setup((x) => x.getSubtestStats('parentTest')).returns(() => undefined); + testItemIndexMock + .setup((x) => x.setSubtestStats('parentTest', typemoq.It.isAny())) + .returns(() => undefined); + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .returns(() => mockSubtest1); + + const payload: ExecutionTestPayload = { + cwd: '/foo/bar', + status: 'success', + result: { + 'parentTest (subtest1)': { + test: 'parentTest', + outcome: 'subtest-success', + message: '', + traceback: '', + subtest: 'subtest1', + }, + }, + error: '', + }; + + executionHandler.processExecution( + payload, + runInstanceMock.object, + testItemIndexMock.object, + testControllerMock.object, + ); + + // Verify setSubtestStats was called (which happens when creating new stats) + testItemIndexMock.verify((x) => x.setSubtestStats('parentTest', typemoq.It.isAny()), typemoq.Times.once()); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType<TestItemCollection>(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar/test.py'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testItemIndex.unit.test.ts b/src/test/testing/testController/common/testItemIndex.unit.test.ts new file mode 100644 index 000000000000..6712d90ff667 --- /dev/null +++ b/src/test/testing/testController/common/testItemIndex.unit.test.ts @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, TestItem, Uri, Range, TestItemCollection } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { TestItemIndex } from '../../../../client/testing/testController/common/testItemIndex'; + +suite('TestItemIndex', () => { + let testItemIndex: TestItemIndex; + let testControllerMock: typemoq.IMock<TestController>; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + let mockParentItem: TestItem; + + setup(() => { + testItemIndex = new TestItemIndex(); + testControllerMock = typemoq.Mock.ofType<TestController>(); + + // Create mock test items + mockTestItem1 = createMockTestItem('test1', 'Test 1'); + mockTestItem2 = createMockTestItem('test2', 'Test 2'); + mockParentItem = createMockTestItem('parent', 'Parent'); + }); + + teardown(() => { + sinon.restore(); + }); + + suite('registerTestItem', () => { + test('should store all three mappings correctly', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + + test('should overwrite existing mappings', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + testItemIndex.registerTestItem(runId, vsId, mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), mockTestItem2); + }); + + test('should handle different runId and vsId', () => { + const runId = 'test_file.py::TestClass::test_method'; + const vsId = 'different_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId), vsId); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId), runId); + }); + }); + + suite('getTestItem', () => { + test('should return item on direct lookup when valid', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Register the item + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock the validation to return true + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, mockTestItem1); + assert.ok(isValidStub.calledOnce); + }); + + test('should remove stale item and try vsId fallback', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + // Mock validation to fail on first call (stale item) + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to not find the item + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should have removed the stale item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), undefined); + assert.strictEqual(result, undefined); + assert.ok(isValidStub.calledOnce); + }); + + test('should perform vsId search when direct lookup is stale', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'test_file.py::test_example'; + + // Create test item with correct ID + const searchableTestItem = createMockTestItem(vsId, 'Test Example'); + + testItemIndex.registerTestItem(runId, vsId, searchableTestItem); + + // First validation fails (stale), need to search by vsId + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + // Setup controller to find item by vsId + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + callback(searchableTestItem); + }) + .returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + // Should recache the found item + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId), searchableTestItem); + assert.strictEqual(result, searchableTestItem); + }); + + test('should return undefined if not found anywhere', () => { + const runId = 'nonexistent'; + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.getTestItem(runId, testControllerMock.object); + + assert.strictEqual(result, undefined); + }); + }); + + suite('getRunId and getVSId', () => { + test('getRunId should convert VS Code ID to Python run ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getRunId(vsId), runId); + }); + + test('getRunId should return undefined for unknown vsId', () => { + assert.strictEqual(testItemIndex.getRunId('unknown'), undefined); + }); + + test('getVSId should convert Python run ID to VS Code ID', () => { + const runId = 'test_file.py::test_example'; + const vsId = 'vscode_id'; + + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + assert.strictEqual(testItemIndex.getVSId(runId), vsId); + }); + + test('getVSId should return undefined for unknown runId', () => { + assert.strictEqual(testItemIndex.getVSId('unknown'), undefined); + }); + }); + + suite('clear', () => { + test('should remove all mappings', () => { + testItemIndex.registerTestItem('runId1', 'vsId1', mockTestItem1); + testItemIndex.registerTestItem('runId2', 'vsId2', mockTestItem2); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 2); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 2); + + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + + test('should handle clearing empty index', () => { + testItemIndex.clear(); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('isTestItemValid', () => { + test('should return true for item with valid parent chain leading to controller', () => { + const childItem = createMockTestItem('child', 'Child'); + (childItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(mockParentItem.id)).returns(() => mockParentItem); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(childItem, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for orphaned item', () => { + const orphanedItem = createMockTestItem('orphaned', 'Orphaned'); + (orphanedItem as any).parent = mockParentItem; + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(orphanedItem, testControllerMock.object); + + assert.strictEqual(result, false); + }); + + test('should return true for root item in controller', () => { + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(mockTestItem1.id)).returns(() => mockTestItem1); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, true); + }); + + test('should return false for item not in controller and no parent', () => { + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock.setup((x) => x.get(typemoq.It.isAny())).returns(() => undefined); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + const result = testItemIndex.isTestItemValid(mockTestItem1, testControllerMock.object); + + assert.strictEqual(result, false); + }); + }); + + suite('cleanupStaleReferences', () => { + test('should remove items not in controller', () => { + const runId1 = 'test1'; + const runId2 = 'test2'; + const vsId1 = 'vs1'; + const vsId2 = 'vs2'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + testItemIndex.registerTestItem(runId2, vsId2, mockTestItem2); + + // Mock validation: first item invalid, second valid + const isValidStub = sinon.stub(testItemIndex, 'isTestItemValid'); + isValidStub.onFirstCall().returns(false); // mockTestItem1 is invalid + isValidStub.onSecondCall().returns(true); // mockTestItem2 is valid + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // First item should be removed + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), undefined); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), undefined); + + // Second item should remain + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId2), mockTestItem2); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId2), vsId2); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId2), runId2); + }); + + test('should keep all valid items', () => { + const runId1 = 'test1'; + const vsId1 = 'vs1'; + + testItemIndex.registerTestItem(runId1, vsId1, mockTestItem1); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(true); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + // Item should still be there + assert.strictEqual(testItemIndex.runIdToTestItemMap.get(runId1), mockTestItem1); + assert.strictEqual(testItemIndex.runIdToVSidMap.get(runId1), vsId1); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.get(vsId1), runId1); + }); + + test('should handle empty index', () => { + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + }); + + test('should remove all items when all are invalid', () => { + testItemIndex.registerTestItem('test1', 'vs1', mockTestItem1); + testItemIndex.registerTestItem('test2', 'vs2', mockTestItem2); + + sinon.stub(testItemIndex, 'isTestItemValid').returns(false); + + testItemIndex.cleanupStaleReferences(testControllerMock.object); + + assert.strictEqual(testItemIndex.runIdToTestItemMap.size, 0); + assert.strictEqual(testItemIndex.runIdToVSidMap.size, 0); + assert.strictEqual(testItemIndex.vsIdToRunIdMap.size, 0); + }); + }); + + suite('Backward compatibility getters', () => { + test('runIdToTestItemMap should return the internal map', () => { + const runId = 'test1'; + testItemIndex.registerTestItem(runId, 'vs1', mockTestItem1); + + const map = testItemIndex.runIdToTestItemMap; + + assert.strictEqual(map.get(runId), mockTestItem1); + }); + + test('runIdToVSidMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.runIdToVSidMap; + + assert.strictEqual(map.get(runId), vsId); + }); + + test('vsIdToRunIdMap should return the internal map', () => { + const runId = 'test1'; + const vsId = 'vs1'; + testItemIndex.registerTestItem(runId, vsId, mockTestItem1); + + const map = testItemIndex.vsIdToRunIdMap; + + assert.strictEqual(map.get(vsId), runId); + }); + }); +}); + +function createMockTestItem(id: string, label: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType<TestItemCollection>(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + label, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + parent: undefined, + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/common/testProjectRegistry.unit.test.ts b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts new file mode 100644 index 000000000000..5d04930d0e88 --- /dev/null +++ b/src/test/testing/testController/common/testProjectRegistry.unit.test.ts @@ -0,0 +1,440 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as path from 'path'; +import * as sinon from 'sinon'; +import { TestController, Uri } from 'vscode'; +import { IConfigurationService } from '../../../../client/common/types'; +import { IEnvironmentVariablesProvider } from '../../../../client/common/variables/types'; +import { IInterpreterService } from '../../../../client/interpreter/contracts'; +import { TestProjectRegistry } from '../../../../client/testing/testController/common/testProjectRegistry'; +import * as envExtApiInternal from '../../../../client/envExt/api.internal'; +import { PythonProject, PythonEnvironment } from '../../../../client/envExt/types'; + +suite('TestProjectRegistry', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let configSettings: IConfigurationService; + let interpreterService: IInterpreterService; + let envVarsService: IEnvironmentVariablesProvider; + let registry: TestProjectRegistry; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create mock test controller + testController = ({ + items: { + get: sandbox.stub(), + add: sandbox.stub(), + delete: sandbox.stub(), + forEach: sandbox.stub(), + }, + createTestItem: sandbox.stub(), + dispose: sandbox.stub(), + } as unknown) as TestController; + + // Create mock config settings + configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + pytestEnabled: true, + unittestEnabled: false, + }, + }), + } as unknown) as IConfigurationService; + + // Create mock interpreter service + interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as unknown) as IInterpreterService; + + // Create mock env vars service + envVarsService = ({ + getEnvironmentVariables: sandbox.stub().resolves({}), + } as unknown) as IEnvironmentVariablesProvider; + + registry = new TestProjectRegistry(testController, configSettings, interpreterService, envVarsService); + }); + + teardown(() => { + sandbox.restore(); + }); + + suite('hasProjects', () => { + test('should return false for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.false; + }); + + test('should return true after projects are registered', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.hasProjects(workspaceUri); + + expect(result).to.be.true; + }); + }); + + suite('getProjectsArray', () => { + test('should return empty array for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').that.is.empty; + }); + + test('should return projects after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + // Mock useEnvExtension to return false to use default project path + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getProjectsArray(workspaceUri); + + expect(result).to.be.an('array').with.length(1); + expect(result[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('discoverAndRegisterProjects', () => { + test('should create default project when env extension not available', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(projects[0].testProvider).to.equal('pytest'); + }); + + test('should use unittest when configured', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + (configSettings.getSettings as sinon.SinonStub).returns({ + testing: { + pytestEnabled: false, + unittestEnabled: true, + }, + }); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].testProvider).to.equal('unittest'); + }); + + test('should discover projects from Python Environments API', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectName).to.include('project1'); + expect(projects[0].pythonEnvironment).to.deep.equal(mockPythonEnv); + }); + + test('should filter projects to current workspace', async () => { + const workspaceUri = Uri.file('/workspace1'); + const projectInWorkspace = Uri.file('/workspace1/project1'); + const projectOutsideWorkspace = Uri.file('/workspace2/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: projectInWorkspace }, + { name: 'project2', uri: projectOutsideWorkspace }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(projectInWorkspace.fsPath); + }); + + test('should fallback to default project when no projects found', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [], + } as any); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + + test('should fallback to default project on API error', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').rejects(new Error('API error')); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + expect(projects).to.have.length(1); + expect(projects[0].projectUri.fsPath).to.equal(workspaceUri.fsPath); + }); + }); + + suite('configureNestedProjectIgnores', () => { + test('should not set ignores when no nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const projectUri = Uri.file('/workspace/project1'); + + const mockPythonProject: PythonProject = { + name: 'project1', + uri: projectUri, + }; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [mockPythonProject], + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + expect(projects[0].nestedProjectPathsToIgnore).to.be.undefined; + }); + + test('should configure ignore paths for nested projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const parentProjectUri = Uri.file('/workspace/parent'); + const childProjectUri = Uri.file(path.join('/workspace/parent', 'child')); + + const mockProjects: PythonProject[] = [ + { name: 'parent', uri: parentProjectUri }, + { name: 'child', uri: childProjectUri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + const parentProject = projects.find((p) => p.projectUri.fsPath === parentProjectUri.fsPath); + + expect(parentProject?.nestedProjectPathsToIgnore).to.include(childProjectUri.fsPath); + }); + + test('should not set child project as ignored for sibling projects', async () => { + const workspaceUri = Uri.file('/workspace'); + const project1Uri = Uri.file('/workspace/project1'); + const project2Uri = Uri.file('/workspace/project2'); + + const mockProjects: PythonProject[] = [ + { name: 'project1', uri: project1Uri }, + { name: 'project2', uri: project2Uri }, + ]; + + const mockPythonEnv: PythonEnvironment = { + name: 'env1', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'env1', managerId: 'manager1' }, + }; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => mockProjects, + getEnvironment: sandbox.stub().resolves(mockPythonEnv), + } as any); + + await registry.discoverAndRegisterProjects(workspaceUri); + registry.configureNestedProjectIgnores(workspaceUri); + + const projects = registry.getProjectsArray(workspaceUri); + projects.forEach((project) => { + expect(project.nestedProjectPathsToIgnore).to.be.undefined; + }); + }); + }); + + suite('clearWorkspace', () => { + test('should remove all projects for a workspace', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + expect(registry.hasProjects(workspaceUri)).to.be.true; + + registry.clearWorkspace(workspaceUri); + + expect(registry.hasProjects(workspaceUri)).to.be.false; + expect(registry.getProjectsArray(workspaceUri)).to.be.empty; + }); + + test('should not affect other workspaces', async () => { + const workspace1Uri = Uri.file('/workspace1'); + const workspace2Uri = Uri.file('/workspace2'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspace1Uri); + await registry.discoverAndRegisterProjects(workspace2Uri); + + registry.clearWorkspace(workspace1Uri); + + expect(registry.hasProjects(workspace1Uri)).to.be.false; + expect(registry.hasProjects(workspace2Uri)).to.be.true; + }); + }); + + suite('getWorkspaceProjects', () => { + test('should return undefined for uninitialized workspace', () => { + const workspaceUri = Uri.file('/workspace'); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.undefined; + }); + + test('should return map after registration', async () => { + const workspaceUri = Uri.file('/workspace'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + await registry.discoverAndRegisterProjects(workspaceUri); + + const result = registry.getWorkspaceProjects(workspaceUri); + + expect(result).to.be.instanceOf(Map); + expect(result?.size).to.equal(1); + }); + }); + + suite('ProjectAdapter properties', () => { + test('should create adapter with correct test infrastructure', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.projectName).to.be.a('string'); + expect(project.projectUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.workspaceUri.fsPath).to.equal(workspaceUri.fsPath); + expect(project.testProvider).to.equal('pytest'); + expect(project.discoveryAdapter).to.exist; + expect(project.executionAdapter).to.exist; + expect(project.resultResolver).to.exist; + expect(project.isDiscovering).to.be.false; + expect(project.isExecuting).to.be.false; + }); + + test('should include python environment details', async () => { + const workspaceUri = Uri.file('/workspace/myproject'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + expect(project.pythonEnvironment).to.exist; + expect(project.pythonProject).to.exist; + expect(project.pythonProject.name).to.equal('myproject'); + }); + }); +}); diff --git a/src/test/testing/testController/controller.unit.test.ts b/src/test/testing/testController/controller.unit.test.ts new file mode 100644 index 000000000000..feb5f36fc797 --- /dev/null +++ b/src/test/testing/testController/controller.unit.test.ts @@ -0,0 +1,344 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import { TestController, Uri } from 'vscode'; + +import { PYTEST_PROVIDER, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; +import * as envExtApiInternal from '../../../client/envExt/api.internal'; +import * as projectUtils from '../../../client/testing/testController/common/projectUtils'; +import { PythonTestController } from '../../../client/testing/testController/controller'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; + +function createStubTestController(): TestController { + const disposable = { dispose: () => undefined }; + + const controller = ({ + items: { + forEach: sinon.stub(), + get: sinon.stub(), + add: sinon.stub(), + replace: sinon.stub(), + delete: sinon.stub(), + size: 0, + [Symbol.iterator]: sinon.stub(), + }, + createRunProfile: sinon.stub().returns(disposable), + createTestItem: sinon.stub(), + dispose: sinon.stub(), + resolveHandler: undefined, + refreshHandler: undefined, + } as unknown) as TestController; + + return controller; +} + +suite('PythonTestController', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + function createController(options?: { unittestEnabled?: boolean; interpreter?: any }): any { + const unittestEnabled = options?.unittestEnabled ?? false; + const interpreter = + options?.interpreter ?? + ({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + } as any); + + const workspaceService = ({ workspaceFolders: [] } as unknown) as any; + const configSettings = ({ + getSettings: sandbox.stub().returns({ + testing: { + unittestEnabled, + autoTestDiscoverOnSaveEnabled: false, + }, + }), + } as unknown) as any; + + const pytest = ({} as unknown) as any; + const unittest = ({} as unknown) as any; + const disposables: any[] = []; + const interpreterService = ({ + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as unknown) as any; + + const commandManager = ({ + registerCommand: sandbox.stub().returns({ dispose: () => undefined }), + } as unknown) as any; + const pythonExecFactory = ({} as unknown) as any; + const debugLauncher = ({} as unknown) as any; + const envVarsService = ({} as unknown) as any; + + return new PythonTestController( + workspaceService, + configSettings, + pytest, + unittest, + disposables, + interpreterService, + commandManager, + pythonExecFactory, + debugLauncher, + envVarsService, + ); + } + + suite('getTestProvider', () => { + test('returns unittest when enabled', () => { + const controller = createController({ unittestEnabled: true }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, UNITTEST_PROVIDER); + }); + + test('returns pytest when unittest not enabled', () => { + const controller = createController({ unittestEnabled: false }); + const workspaceUri: Uri = vscode.Uri.file('/workspace'); + + const provider = (controller as any).getTestProvider(workspaceUri); + + assert.strictEqual(provider, PYTEST_PROVIDER); + }); + }); + + suite('createDefaultProject (via TestProjectRegistry)', () => { + test('creates a single default project using active interpreter', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/myws'); + const interpreter = { + displayName: 'My Python', + path: '/opt/py/bin/python', + version: { raw: '3.12.1' }, + sysPrefix: '/opt/py', + }; + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + // Stub useEnvExtension to return false so createDefaultProject is called + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + const project = projects[0]; + + assert.strictEqual(projects.length, 1); + assert.strictEqual(project.workspaceUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectUri.toString(), workspaceUri.toString()); + assert.strictEqual(project.projectName, 'myws'); + + assert.strictEqual(project.testProvider, PYTEST_PROVIDER); + assert.strictEqual(project.discoveryAdapter, fakeDiscoveryAdapter); + assert.strictEqual(project.executionAdapter, fakeExecutionAdapter); + + assert.strictEqual(project.pythonProject.uri.toString(), workspaceUri.toString()); + assert.strictEqual(project.pythonProject.name, 'myws'); + + assert.strictEqual(project.pythonEnvironment.displayName, 'My Python'); + assert.strictEqual(project.pythonEnvironment.version, '3.12.1'); + assert.strictEqual(project.pythonEnvironment.execInfo.run.executable, '/opt/py/bin/python'); + }); + }); + + suite('discoverWorkspaceProjects (via TestProjectRegistry)', () => { + test('respects useEnvExtension() == false and falls back to single default project', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/a'); + + const useEnvExtensionStub = sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(false); + const getEnvExtApiStub = sandbox.stub(envExtApiInternal, 'getEnvExtApi'); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves({ + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + assert.strictEqual(useEnvExtensionStub.called, true); + assert.strictEqual(getEnvExtApiStub.notCalled, true); + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + + test('filters Python projects to workspace and creates adapters for each', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + const pythonProjects = [ + { name: 'p1', uri: vscode.Uri.file('/workspace/root/p1') }, + { name: 'p2', uri: vscode.Uri.file('/workspace/root/nested/p2') }, + { name: 'other', uri: vscode.Uri.file('/other/root/p3') }, + ]; + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => pythonProjects, + getEnvironment: sandbox.stub().resolves({ + name: 'env', + displayName: 'Python 3.11', + shortDisplayName: 'Python 3.11', + displayPath: '/usr/bin/python3', + version: '3.11.8', + environmentPath: vscode.Uri.file('/usr/bin/python3'), + sysPrefix: '/usr', + execInfo: { run: { executable: '/usr/bin/python3' } }, + envId: { id: 'test', managerId: 'test' }, + }), + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(null), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should only create adapters for the 2 projects in the workspace (not 'other') + assert.strictEqual(projects.length, 2); + const projectUris = projects.map((p: { projectUri: { fsPath: string } }) => p.projectUri.fsPath); + const expectedInWorkspace = [ + vscode.Uri.file('/workspace/root/p1').fsPath, + vscode.Uri.file('/workspace/root/nested/p2').fsPath, + ]; + const expectedOutOfWorkspace = vscode.Uri.file('/other/root/p3').fsPath; + + expectedInWorkspace.forEach((expectedPath) => { + assert.ok(projectUris.includes(expectedPath)); + }); + assert.ok(!projectUris.includes(expectedOutOfWorkspace)); + }); + + test('falls back to default project when no projects are in the workspace', async () => { + const workspaceUri: Uri = vscode.Uri.file('/workspace/root'); + + sandbox.stub(envExtApiInternal, 'useEnvExtension').returns(true); + sandbox.stub(envExtApiInternal, 'getEnvExtApi').resolves({ + getPythonProjects: () => [{ name: 'other', uri: vscode.Uri.file('/other/root/p3') }], + } as any); + + const fakeDiscoveryAdapter = { kind: 'discovery' }; + const fakeExecutionAdapter = { kind: 'execution' }; + sandbox.stub(projectUtils, 'createTestAdapters').returns({ + discoveryAdapter: fakeDiscoveryAdapter, + executionAdapter: fakeExecutionAdapter, + } as any); + + const interpreter = { + displayName: 'Python 3.11', + path: '/usr/bin/python3', + version: { raw: '3.11.8' }, + sysPrefix: '/usr', + }; + + const interpreterService = { + getActiveInterpreter: sandbox.stub().resolves(interpreter), + } as any; + + const configSettings = { + getSettings: sandbox.stub().returns({ + testing: { unittestEnabled: false }, + }), + } as any; + + const testController = createStubTestController(); + const envVarsService = {} as any; + + const registry = new TestProjectRegistry( + testController, + configSettings, + interpreterService, + envVarsService, + ); + + const projects = await registry.discoverAndRegisterProjects(workspaceUri); + + // Should fall back to default project since no projects are in the workspace + assert.strictEqual(projects.length, 1); + assert.strictEqual(projects[0].projectUri.toString(), workspaceUri.toString()); + }); + }); +}); diff --git a/src/test/testing/testController/payloadTestCases.ts b/src/test/testing/testController/payloadTestCases.ts new file mode 100644 index 000000000000..7f2f5e23bfc3 --- /dev/null +++ b/src/test/testing/testController/payloadTestCases.ts @@ -0,0 +1,171 @@ +export interface DataWithPayloadChunks { + payloadArray: string[]; + data: string; +} + +const SINGLE_UNITTEST_SUBTEST = { + cwd: '/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace', + status: 'success', + result: { + 'test_parameterized_subtest.NumbersTest.test_even (i=0)': { + test: 'test_parameterized_subtest.NumbersTest.test_even', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'test_parameterized_subtest.NumbersTest.test_even (i=0)', + }, + }, +}; + +export const SINGLE_PYTEST_PAYLOAD = { + cwd: 'path/to', + status: 'success', + result: { + 'path/to/file.py::test_funct': { + test: 'path/to/file.py::test_funct', + outcome: 'success', + message: 'None', + traceback: null, + subtest: 'path/to/file.py::test_funct', + }, + }, +}; + +const SINGLE_PYTEST_PAYLOAD_TWO = { + cwd: 'path/to/second', + status: 'success', + result: { + 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]': { + test: 'path/to/workspace/parametrize_tests.py::test_adding[3+5-8]', + outcome: 'success', + message: 'None', + traceback: null, + }, + }, +}; + +function splitIntoRandomSubstrings(payload: string): string[] { + // split payload at random + const splitPayload = []; + const n = payload.length; + let remaining = n; + while (remaining > 0) { + // Randomly split what remains of the string + const randomSize = Math.floor(Math.random() * remaining) + 1; + splitPayload.push(payload.slice(n - remaining, n - remaining + randomSize)); + + remaining -= randomSize; + } + return splitPayload; +} + +export function createPayload(uuid: string, data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json +Request-uuid: ${uuid} + +${JSON.stringify(data)}`; +} + +export function createPayload2(data: unknown): string { + return `Content-Length: ${JSON.stringify(data).length} +Content-Type: application/json + +${JSON.stringify(data)}`; +} + +export function PAYLOAD_SINGLE_CHUNK(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + + return { + payloadArray: [payload], + data: JSON.stringify(SINGLE_UNITTEST_SUBTEST.result), + }; +} + +// more than one payload (item with header) per chunk sent +// payload has 3 SINGLE_UNITTEST_SUBTEST +export function PAYLOAD_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + let payload = ''; + let result = ''; + for (let i = 0; i < 3; i = i + 1) { + payload += createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + result += JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + } + return { + payloadArray: [payload], + data: result, + }; +} + +// more than one payload, split so the first one is only 'Content-Length' to confirm headers +// with null values are ignored +export function PAYLOAD_ONLY_HEADER_MULTI_CHUNK(uuid: string): DataWithPayloadChunks { + const payloadArray: string[] = []; + const result = JSON.stringify(SINGLE_UNITTEST_SUBTEST.result); + + const val = createPayload(uuid, SINGLE_UNITTEST_SUBTEST); + const firstSpaceIndex = val.indexOf(' '); + const payload1 = val.substring(0, firstSpaceIndex); + const payload2 = val.substring(firstSpaceIndex); + payloadArray.push(payload1); + payloadArray.push(payload2); + return { + payloadArray, + data: result, + }; +} + +// single payload divided by an arbitrary character and split across payloads +export function PAYLOAD_SPLIT_ACROSS_CHUNKS_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result); + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +// here a payload is split across the buffer chunks and there are multiple payloads in a single buffer chunk +export function PAYLOAD_SPLIT_MULTI_CHUNK_ARRAY(uuid: string): DataWithPayloadChunks { + const payload = createPayload(uuid, SINGLE_PYTEST_PAYLOAD).concat(createPayload(uuid, SINGLE_PYTEST_PAYLOAD_TWO)); + const splitPayload = splitIntoRandomSubstrings(payload); + const finalResult = JSON.stringify(SINGLE_PYTEST_PAYLOAD.result).concat( + JSON.stringify(SINGLE_PYTEST_PAYLOAD_TWO.result), + ); + + return { + payloadArray: splitPayload, + data: finalResult, + }; +} + +export function PAYLOAD_SPLIT_MULTI_CHUNK_RAN_ORDER_ARRAY(uuid: string): Array<string> { + return [ + `Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=0)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=0)"}}} + +Content-Length: 411 +Content-Type: application/json +Request-uuid: 9${uuid} + +{"cwd": "/home/runner/work/vscode-`, + `python/vscode-python/path with`, + ` spaces/src" + +Content-Length: 959 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-failure", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=1)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-failure", "message": "(<class 'AssertionError'>, AssertionError('1 != 0'), <traceback object at 0x7fd86fc47580>)", "traceback": " File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 57, in testPartExecutor\n yield\n File \"/opt/hostedtoolcache/Python/3.11.4/x64/lib/python3.11/unittest/case.py\", line 538, in subTest\n yield\n File \"/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace/test_parameterized_subtest.py\", line 16, in test_even\n self.assertEqual(i % 2, 0)\nAssertionError: 1 != 0\n", "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=1)"}}} +Content-Length: 411 +Content-Type: application/json +Request-uuid: ${uuid} + +{"cwd": "/home/runner/work/vscode-python/vscode-python/path with spaces/src/testTestingRootWkspc/largeWorkspace", "status": "subtest-success", "result": {"test_parameterized_subtest.NumbersTest.test_even (i=2)": {"test": "test_parameterized_subtest.NumbersTest.test_even", "outcome": "subtest-success", "message": "None", "traceback": null, "subtest": "test_parameterized_subtest.NumbersTest.test_even (i=2)"}}}`, + ]; +} diff --git a/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts new file mode 100644 index 000000000000..ec155ee3107d --- /dev/null +++ b/src/test/testing/testController/pytest/pytestDiscoveryAdapter.unit.test.ts @@ -0,0 +1,414 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { Uri, CancellationTokenSource } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import * as fs from 'fs'; +import * as sinon from 'sinon'; +import { IConfigurationService } from '../../../../client/common/types'; +import { PytestTestDiscoveryAdapter } from '../../../../client/testing/testController/pytest/pytestDiscoveryAdapter'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + SpawnOptions, + Output, +} from '../../../../client/common/process/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; +import * as util from '../../../../client/testing/testController/common/utils'; +import * as extapi from '../../../../client/envExt/api.internal'; + +suite('pytest test discovery adapter', () => { + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let adapter: PytestTestDiscoveryAdapter; + let execService: typeMoq.IMock<IPythonExecutionService>; + let deferred: Deferred<void>; + let expectedPath: string; + let uri: Uri; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + let expectedExtraVariables: Record<string, string>; + let mockProc: MockChildProcess; + let deferred2: Deferred<void>; + let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + const mockExtensionRootDir = typeMoq.Mock.ofType<string>(); + mockExtensionRootDir.setup((m) => m.toString()).returns(() => '/mocked/extension/root/dir'); + + utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + + // constants + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + const relativePathToPytest = 'python_files'; + const fullPluginPath = path.join(EXTENSION_ROOT_DIR, relativePathToPytest); + expectedExtraVariables = { + PYTHONPATH: fullPluginPath, + TEST_RUN_PIPE: 'discoveryResultPipe-mockName', + }; + + // set up config service + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + deferred2 = createDeferred(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + + cancellationTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancellationTokenSource.dispose(); + }); + test('Discovery should call exec with correct basic args', async () => { + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is<SpawnOptions>((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery correctly pulls pytest args from config service settings', async () => { + // set up a config service with different pytest args + const expectedPathNew = path.join('other', 'path'); + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPathNew, + }, + }), + } as unknown) as IConfigurationService; + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPathNew}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPathNew); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery adds cwd to pytest args when path is symlink', async () => { + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => true, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPath, + }, + }), + } as unknown) as IConfigurationService; + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPath}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery adds cwd to pytest args when path parent is symlink', async () => { + let counter = 0; + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => { + counter = counter + 1; + return counter > 2; + }, + } as fs.Stats), + ); + + sinon.stub(fs.promises, 'realpath').callsFake(async () => 'diff value'); + + // set up a config service with different pytest args + const configServiceNew: IConfigurationService = ({ + getSettings: () => ({ + testing: { + pytestArgs: ['.', 'abc', 'xyz'], + cwd: expectedPath, + }, + }), + } as unknown) as IConfigurationService; + + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService.object); + }); + + adapter = new PytestTestDiscoveryAdapter(configServiceNew); + adapter.discoverTests(uri, execFactory.object); + // add in await and trigger + await deferred.promise; + await deferred2.promise; + mockProc.trigger('close'); + + // verification + const expectedArgs = [ + '-m', + 'pytest', + '-p', + 'vscode_pytest', + '--collect-only', + '.', + 'abc', + 'xyz', + `--rootdir=${expectedPath}`, + ]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable<Output<string>>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + adapter = new PytestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); +}); diff --git a/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts new file mode 100644 index 000000000000..40c701b22641 --- /dev/null +++ b/src/test/testing/testController/pytest/pytestExecutionAdapter.unit.test.ts @@ -0,0 +1,535 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { TestRun, Uri, TestRunProfileKind, DebugSessionOptions } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { IConfigurationService } from '../../../../client/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { PytestTestExecutionAdapter } from '../../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { createMockProjectAdapter } from '../testMocks'; + +suite('pytest test execution adapter', () => { + let useEnvExtensionStub: sinon.SinonStub; + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let adapter: PytestTestExecutionAdapter; + let execService: typeMoq.IMock<IPythonExecutionService>; + let deferred: Deferred<void>; + let deferred4: Deferred<void>; + let debugLauncher: typeMoq.IMock<ITestDebugLauncher>; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipeStub: sinon.SinonStub; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType<ITestDebugLauncher>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + + utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + }); + teardown(() => { + sinon.restore(); + }); + test('WriteTestIdsFile called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve({ + name: 'mockName', + dispose: () => { + /* no-op */ + }, + }); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + const testIds = ['test1id', 'test2id']; + + adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); + + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsWriteTestIdsFileStub, testIds); + }); + test('pytest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const rootDirArg = `--rootdir=${myTestPath}`; + const expectedArgs = [pathToPythonScript, rootDirArg]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.env?.COVERAGE_ENABLED, undefined); // coverage not enabled + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('pytest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const expectedArgs = [pathToPythonScript, `--rootdir=${newCwd}`]; + const expectedExtraVariables = { + PYTHONPATH: pathToPythonFiles, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for pytest', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.deepEqual(launchOptions.args, [`--rootdir=${myTestPath}`, '--capture=no']); + assert.equal(launchOptions.testProvider, 'pytest'); + assert.equal(launchOptions.pytestPort, 'runResultPipe-mockName'); + assert.strictEqual(launchOptions.runTestIdsPort, 'testIdPipe-mockName'); + assert.notEqual(launchOptions.token, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.is<DebugSessionOptions>((sessionOptions) => { + assert.equal(sessionOptions.testRun, testRun.object); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('pytest execution with coverage turns on correctly', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToPythonScript = path.join(pathToPythonFiles, 'vscode_pytest', 'run_pytest_script.py'); + const rootDirArg = `--rootdir=${myTestPath}`; + const expectedArgs = [pathToPythonScript, rootDirArg]; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.COVERAGE_ENABLED, 'True'); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + // ===== PROJECT-BASED EXECUTION TESTS ===== + + suite('project-based execution', () => { + test('should set PROJECT_ROOT_PATH env var when project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, + undefined, + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, projectPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should pass debugSessionName in LaunchOptions for debug mode with project', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + }); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set PROJECT_ROOT_PATH when no project provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.isAny(), + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PROJECT_ROOT_PATH, undefined); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('should not set project in LaunchOptions when no project provided', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + + const uri = Uri.file(myTestPath); + adapter = new PytestTestExecutionAdapter(configService); + // Call without project parameter + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + assert.equal(launchOptions.project, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + }); +}); diff --git a/src/test/testing/testController/resultResolver.unit.test.ts b/src/test/testing/testController/resultResolver.unit.test.ts new file mode 100644 index 000000000000..e4b350a20750 --- /dev/null +++ b/src/test/testing/testController/resultResolver.unit.test.ts @@ -0,0 +1,613 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TestController, Uri, TestItem, CancellationToken, TestRun, TestItemCollection, Range } from 'vscode'; +import * as typemoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { TestProvider } from '../../../client/testing/types'; +import { + DiscoveredTestNode, + DiscoveredTestPayload, + ExecutionTestPayload, +} from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import * as util from '../../../client/testing/testController/common/utils'; +import { traceLog } from '../../../client/logging'; + +suite('Result Resolver tests', () => { + suite('Test discovery', () => { + let resultResolver: ResultResolver.PythonResultResolver; + let testController: TestController; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let defaultErrorMessage: string; + let blankTestItem: TestItem; + let cancelationToken: CancellationToken; + + setup(() => { + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + + dispose: () => { + // empty + }, + } as unknown) as TestController; + defaultErrorMessage = 'pytest test discovery error (see Output > Python)'; + blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + }); + teardown(() => { + sinon.restore(); + }); + + test('resolveDiscovery calls populate test tree correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + tests, + }; + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of populateTestTree is (testController: TestController, testTreeData: DiscoveredTestNode, testRoot: TestItem | undefined, resultResolver: ITestResultResolver, token?: CancellationToken) + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + sinon.match.has('runIdToTestItem'), // inline object with maps + cancelationToken, // token + ); + }); + test('resolveDiscovery should create error node on error with correct params and no root node with tests in payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // header of buildErrorNodeOptions is (uri: Uri, message: string, testType: string) + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // header of createErrorTestItem is (options: ErrorTestItemOptions, testController: TestController, uri: Uri) + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + }); + test('resolveDiscovery should create error and root node when error and tests exist on payload', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const payload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + const populateTestTreeStub = sinon.stub(util, 'populateTestTree').returns(); + // call resolve discovery + resultResolver.resolveDiscovery(payload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + // also calls populateTestTree with the discovery test results + // After refactor, an inline object with testItemIndex maps is passed instead of resultResolver + sinon.assert.calledWithMatch( + populateTestTreeStub, + testController, // testController + tests, // testTreeData + undefined, // testRoot + sinon.match.has('runIdToTestItem'), // inline object with maps + cancelationToken, // token + ); + }); + test('resolveDiscovery should create error and not clear test items to allow for error tolerant discovery', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + const errorMessage = 'error msg A'; + const expectedErrorMessage = `${defaultErrorMessage}\r\n ${errorMessage}`; + + // create test result node + const tests: DiscoveredTestNode = { + path: 'path', + name: 'name', + type_: 'folder', + id_: 'id', + children: [], + }; + // stub out return values of functions called in resolveDiscovery + const errorPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: [errorMessage], + }; + const regPayload: DiscoveredTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + error: [errorMessage], + tests, + }; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + + // stub out functionality of buildErrorNodeOptions and createErrorTestItem which are called in resolveDiscovery + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + + // stub out functionality of populateTestTreeStub which is called in resolveDiscovery + sinon.stub(util, 'populateTestTree').returns(); + // add spies to insure these aren't called + const deleteSpy = sinon.spy(testController.items, 'delete'); + const replaceSpy = sinon.spy(testController.items, 'replace'); + // call resolve discovery + resultResolver.resolveDiscovery(regPayload, cancelationToken); + resultResolver.resolveDiscovery(errorPayload, cancelationToken); + + // assert the stub functions were called with the correct parameters + + // builds an error node root + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, workspaceUri, expectedErrorMessage, testProvider); + // builds an error item + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + + if (!deleteSpy.calledOnce) { + throw new Error("The delete method was called, but it shouldn't have been."); + } + if (replaceSpy.called) { + throw new Error("The replace method was called, but it shouldn't have been."); + } + }); + }); + suite('Test execution result resolver', () => { + let resultResolver: ResultResolver.PythonResultResolver; + const log: string[] = []; + let workspaceUri: Uri; + let testProvider: TestProvider; + let cancelationToken: CancellationToken; + let runInstance: typemoq.IMock<TestRun>; + let testControllerMock: typemoq.IMock<TestController>; + let mockTestItem1: TestItem; + let mockTestItem2: TestItem; + + setup(() => { + // create mock test items + mockTestItem1 = createMockTestItem('mockTestItem1'); + mockTestItem2 = createMockTestItem('mockTestItem2'); + + // create mock testItems to pass into a iterable + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + // create mock testItemCollection + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + + // create mock testController + testControllerMock = typemoq.Mock.ofType<TestController>(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + cancelationToken = ({ + isCancellationRequested: false, + } as unknown) as CancellationToken; + + // define functions within runInstance + runInstance = typemoq.Mock.ofType<TestRun>(); + runInstance.setup((r) => r.name).returns(() => 'name'); + runInstance.setup((r) => r.token).returns(() => cancelationToken); + runInstance.setup((r) => r.isPersisted).returns(() => true); + runInstance + .setup((r) => r.enqueued(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('enqueue'); + return undefined; + }); + runInstance + .setup((r) => r.started(typemoq.It.isAny())) + .returns(() => { + // empty + log.push('start'); + }); + + // mock getTestCaseNodes to just return the given testNode added + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => [testNode]); + }); + teardown(() => { + sinon.restore(); + }); + test('resolveExecution create correct subtest item for unittest', async () => { + // test specific constants used expected values + sinon.stub(testItemUtilities, 'clearAllChildren').callsFake(() => undefined); + testProvider = 'unittest'; + workspaceUri = Uri.file('/foo/bar'); + + // Create parent test item with correct ID + const mockParentItem = createMockTestItem('parentTest'); + + // Update testControllerMock to include parent item in its collection + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + ['parentTest', mockParentItem], + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testItemCollectionMock.setup((x) => x.get('parentTest')).returns(() => mockParentItem); + + testControllerMock.reset(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + const subtestName = 'parentTest [subTest with spaces and [brackets]]'; + const mockSubtestItem = createMockTestItem(subtestName); + + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + // creates a mock test item with a space which will be used to split the runId + resultResolver.runIdToVSid.set(subtestName, subtestName); + // Register parent test in testItemIndex so it can be found by getTestItem + resultResolver.runIdToVSid.set('parentTest', 'parentTest'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('parentTest', mockParentItem); + resultResolver.runIdToTestItem.set(subtestName, mockSubtestItem); + + let generatedId: string | undefined; + let generatedUri: Uri | undefined; + testControllerMock + .setup((t) => t.createTestItem(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())) + .callback((id: string) => { + generatedId = id; + generatedUri = workspaceUri; + traceLog('createTestItem function called with id:', id); + }) + .returns(() => ({ id: 'id_this', label: 'label_this', uri: workspaceUri } as TestItem)); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + 'parentTest [subTest with spaces and [brackets]]': { + test: 'parentTest', + outcome: 'subtest-success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: subtestName, + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + assert.ok(generatedId); + assert.strictEqual(generatedUri, workspaceUri); + assert.strictEqual(generatedId, '[subTest with spaces and [brackets]]'); + }); + test('resolveExecution handles failed tests correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'failure', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles skipped correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'skipped', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly as test outcome', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'error', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.errored(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles success correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + // add a mock test item to the map of known VSCode ids to run ids + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + resultResolver.runIdToVSid.set('mockTestItem2', 'mockTestItem2'); + + // add this mock test to the map of known test items + resultResolver.runIdToTestItem.set('mockTestItem1', mockTestItem1); + resultResolver.runIdToTestItem.set('mockTestItem2', mockTestItem2); + + // create a successful payload with a single test called mockTestItem1 + const successPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'success', + result: { + mockTestItem1: { + test: 'test', + outcome: 'success', // failure, passed-unexpected, skipped, success, expected-failure, subtest-failure, subtest-succcess + message: 'message', + traceback: 'traceback', + subtest: 'subtest', + }, + }, + error: '', + }; + + // call resolveExecution + resultResolver.resolveExecution(successPayload, runInstance.object); + + // verify that the passed function was called for the single test item + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.once()); + }); + test('resolveExecution handles error correctly', async () => { + // test specific constants used expected values + testProvider = 'pytest'; + workspaceUri = Uri.file('/foo/bar'); + resultResolver = new ResultResolver.PythonResultResolver( + testControllerMock.object, + testProvider, + workspaceUri, + ); + + const errorPayload: ExecutionTestPayload = { + cwd: workspaceUri.fsPath, + status: 'error', + error: 'error', + }; + + resultResolver.resolveExecution(errorPayload, runInstance.object); + + // verify that none of these functions are called + + runInstance.verify((r) => r.passed(typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny()), typemoq.Times.never()); + runInstance.verify((r) => r.skipped(typemoq.It.isAny()), typemoq.Times.never()); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = new Range(0, 0, 0, 0); + const mockChildren = typemoq.Mock.ofType<TestItemCollection>(); + mockChildren.setup((x) => x.add(typemoq.It.isAny())).returns(() => undefined); + mockChildren.setup((x) => x.forEach(typemoq.It.isAny())).returns(() => undefined); + + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: mockChildren.object, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts new file mode 100644 index 000000000000..cdf0d00c5dc4 --- /dev/null +++ b/src/test/testing/testController/testCancellationRunAdapters.unit.test.ts @@ -0,0 +1,234 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import { CancellationTokenSource, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs'; +import { IPythonExecutionFactory, IPythonExecutionService, Output } from '../../../client/common/process/types'; +import { IConfigurationService } from '../../../client/common/types'; +import { Deferred, createDeferred } from '../../../client/common/utils/async'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { PytestTestExecutionAdapter } from '../../../client/testing/testController/pytest/pytestExecutionAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; +import { MockChildProcess } from '../../mocks/mockChildProcess'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as extapi from '../../../client/envExt/api.internal'; +import { noop } from '../../core'; + +const adapters: Array<string> = ['pytest', 'unittest']; + +suite('Execution Flow Run Adapters', () => { + // define suit level variables + let configService: IConfigurationService; + let execFactoryStub = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let execServiceStub: typeMoq.IMock<IPythonExecutionService>; + // let deferred: Deferred<void>; + let debugLauncher: typeMoq.IMock<ITestDebugLauncher>; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipe: sinon.SinonStub; + let serverDisposeStub: sinon.SinonStub; + + let useEnvExtensionStub: sinon.SinonStub; + + setup(() => { + const proc = typeMoq.Mock.ofType<MockChildProcess>(); + proc.setup((p) => p.on).returns(() => noop as any); + proc.setup((p) => p.stdout).returns(() => null); + proc.setup((p) => p.stderr).returns(() => null); + mockProc = proc.object; + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + // general vars + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + configService = ({ + getSettings: () => ({ + testing: { pytestArgs: ['.'], unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up execService and execFactory, all mocked + execServiceStub = typeMoq.Mock.ofType<IPythonExecutionService>(); + execFactoryStub = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + + // mocked utility functions that handle pipe related functions + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + utilsStartRunResultNamedPipe = sinon.stub(util, 'startRunResultNamedPipe'); + serverDisposeStub = sinon.stub(); + + // debug specific mocks + debugLauncher = typeMoq.Mock.ofType<ITestDebugLauncher>(); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + }); + teardown(() => { + sinon.restore(); + }); + adapters.forEach((adapter) => { + test(`Adapter ${adapter}: cancelation token called mid-run resolves correctly`, async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType<TestRun>(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred<void> | undefined; + + // // mock exec service and exec factory + execServiceStub + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc as any, + out: typeMoq.Mock.ofType<Observable<Output<string>>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactoryStub + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceStub.object)); + execFactoryStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // test ids named pipe mocking + const deferredStartTestIdsNamedPipe = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => { + deferredStartTestIdsNamedPipe.resolve(); + return Promise.resolve('named-pipe'); + }); + + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, token) => { + deferredTillServerCloseTester = deferredTillServerClose; + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + + return Promise.resolve('named-pipes-socket-name'); + }); + serverDisposeStub.callsFake(() => { + console.log('server disposed'); + if (deferredTillServerCloseTester) { + deferredTillServerCloseTester.resolve(); + } else { + console.log('deferredTillServerCloseTester is undefined'); + throw new Error( + 'deferredTillServerCloseTester is undefined, should be defined from startRunResultNamedPipe', + ); + } + }); + + // define adapter and run tests + const testAdapter = createAdapter(adapter, configService); + await testAdapter.runTests( + Uri.file(myTestPath), + [], + TestRunProfileKind.Run, + testRunMock.object, + execFactoryStub.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartTestIdsNamedPipe.promise; + }); + test(`Adapter ${adapter}: token called mid-debug resolves correctly`, async () => { + // mock test run and cancelation token + const testRunMock = typeMoq.Mock.ofType<TestRun>(); + const cancellationToken = new CancellationTokenSource(); + const { token } = cancellationToken; + testRunMock.setup((t) => t.token).returns(() => token); + + // run result pipe mocking and the related server close dispose + let deferredTillServerCloseTester: Deferred<void> | undefined; + + // // mock exec service and exec factory + execServiceStub + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + cancellationToken.cancel(); + return { + proc: mockProc as any, + out: typeMoq.Mock.ofType<Observable<Output<string>>>().object, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactoryStub + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execServiceStub.object)); + execFactoryStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execServiceStub.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // test ids named pipe mocking + const deferredStartTestIdsNamedPipe = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => { + deferredStartTestIdsNamedPipe.resolve(); + return Promise.resolve('named-pipe'); + }); + + utilsStartRunResultNamedPipe.callsFake((_callback, deferredTillServerClose, _token) => { + deferredTillServerCloseTester = deferredTillServerClose; + token?.onCancellationRequested(() => { + deferredTillServerCloseTester?.resolve(); + }); + return Promise.resolve('named-pipes-socket-name'); + }); + serverDisposeStub.callsFake(() => { + console.log('server disposed'); + if (deferredTillServerCloseTester) { + deferredTillServerCloseTester.resolve(); + } else { + console.log('deferredTillServerCloseTester is undefined'); + throw new Error( + 'deferredTillServerCloseTester is undefined, should be defined from startRunResultNamedPipe', + ); + } + }); + + // debugLauncher mocked + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .callback((_options, callback) => { + if (callback) { + callback(); + } + }) + .returns(async () => { + cancellationToken.cancel(); + return Promise.resolve(); + }); + + // define adapter and run tests + const testAdapter = createAdapter(adapter, configService); + await testAdapter.runTests( + Uri.file(myTestPath), + [], + TestRunProfileKind.Debug, + testRunMock.object, + execFactoryStub.object, + debugLauncher.object, + ); + // wait for server to start to keep test from failing + await deferredStartTestIdsNamedPipe.promise; + }); + }); +}); + +// Helper function to create an adapter based on the specified type +function createAdapter( + adapterType: string, + configService: IConfigurationService, +): PytestTestExecutionAdapter | UnittestTestExecutionAdapter { + if (adapterType === 'pytest') return new PytestTestExecutionAdapter(configService); + if (adapterType === 'unittest') return new UnittestTestExecutionAdapter(configService); + throw Error('un-compatible adapter type'); +} diff --git a/src/test/testing/testController/testMocks.ts b/src/test/testing/testController/testMocks.ts new file mode 100644 index 000000000000..eb37d492f1d9 --- /dev/null +++ b/src/test/testing/testController/testMocks.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Centralized mock utilities for testing testController components. + * Re-use these helpers across multiple test files for consistency. + */ + +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; +import { TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; +import { ITestDebugLauncher } from '../../../client/testing/common/types'; +import { ProjectAdapter } from '../../../client/testing/testController/common/projectAdapter'; +import { ProjectExecutionDependencies } from '../../../client/testing/testController/common/projectTestExecution'; +import { TestProjectRegistry } from '../../../client/testing/testController/common/testProjectRegistry'; +import { ITestExecutionAdapter, ITestResultResolver } from '../../../client/testing/testController/common/types'; + +/** + * Creates a mock TestItem with configurable properties. + * @param id - The unique ID of the test item + * @param uriPath - The file path for the test item's URI + * @param children - Optional array of child test items + */ +export function createMockTestItem(id: string, uriPath: string, children?: TestItem[]): TestItem { + const childMap = new Map<string, TestItem>(); + children?.forEach((c) => childMap.set(c.id, c)); + + const mockChildren: TestItemCollection = { + size: childMap.size, + forEach: (callback: (item: TestItem, collection: TestItemCollection) => void) => { + childMap.forEach((item) => callback(item, mockChildren)); + }, + get: (itemId: string) => childMap.get(itemId), + add: () => {}, + delete: () => {}, + replace: () => {}, + [Symbol.iterator]: function* () { + for (const [key, value] of childMap) { + yield [key, value] as [string, TestItem]; + } + }, + } as TestItemCollection; + + return ({ + id, + uri: Uri.file(uriPath), + children: mockChildren, + label: id, + canResolveChildren: false, + busy: false, + tags: [], + range: undefined, + error: undefined, + parent: undefined, + } as unknown) as TestItem; +} + +/** + * Creates a mock TestItem without a URI. + * Useful for testing edge cases where test items have no associated file. + * @param id - The unique ID of the test item + */ +export function createMockTestItemWithoutUri(id: string): TestItem { + return ({ + id, + uri: undefined, + children: ({ size: 0, forEach: () => {} } as unknown) as TestItemCollection, + label: id, + } as unknown) as TestItem; +} + +export interface MockProjectAdapterConfig { + projectPath: string; + projectName: string; + pythonPath?: string; + testProvider?: 'pytest' | 'unittest'; +} + +export type MockProjectAdapter = ProjectAdapter & { executionAdapterStub: sinon.SinonStub }; + +/** + * Creates a mock ProjectAdapter for testing project-based test execution. + * @param config - Configuration object with project details + * @returns A mock ProjectAdapter with an exposed executionAdapterStub for verification + */ +export function createMockProjectAdapter(config: MockProjectAdapterConfig): MockProjectAdapter { + const runTestsStub = sinon.stub().resolves(); + const executionAdapter: ITestExecutionAdapter = ({ + runTests: runTestsStub, + } as unknown) as ITestExecutionAdapter; + + const resultResolverMock: ITestResultResolver = ({ + vsIdToRunId: new Map<string, string>(), + runIdToVSid: new Map<string, string>(), + runIdToTestItem: new Map<string, TestItem>(), + detailedCoverageMap: new Map(), + resolveDiscovery: () => Promise.resolve(), + resolveExecution: () => {}, + } as unknown) as ITestResultResolver; + + const adapter = ({ + projectUri: Uri.file(config.projectPath), + projectName: config.projectName, + workspaceUri: Uri.file(config.projectPath), + testProvider: config.testProvider ?? 'pytest', + pythonEnvironment: config.pythonPath + ? { + execInfo: { run: { executable: config.pythonPath } }, + } + : undefined, + pythonProject: { + name: config.projectName, + uri: Uri.file(config.projectPath), + }, + executionAdapter, + discoveryAdapter: {} as any, + resultResolver: resultResolverMock, + isDiscovering: false, + isExecuting: false, + // Expose the stub for testing + executionAdapterStub: runTestsStub, + } as unknown) as MockProjectAdapter; + + return adapter; +} + +/** + * Creates mock dependencies for project test execution. + * @returns An object containing mocked ProjectExecutionDependencies + */ +export function createMockDependencies(): ProjectExecutionDependencies { + return { + projectRegistry: typemoq.Mock.ofType<TestProjectRegistry>().object, + pythonExecFactory: typemoq.Mock.ofType<IPythonExecutionFactory>().object, + debugLauncher: typemoq.Mock.ofType<ITestDebugLauncher>().object, + }; +} + +/** + * Creates a mock TestRun with common setup methods. + * @returns A TypeMoq mock of TestRun + */ +export function createMockTestRun(): typemoq.IMock<TestRun> { + const runMock = typemoq.Mock.ofType<TestRun>(); + runMock.setup((r) => r.started(typemoq.It.isAny())); + runMock.setup((r) => r.passed(typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.failed(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny())); + runMock.setup((r) => r.skipped(typemoq.It.isAny())); + runMock.setup((r) => r.end()); + return runMock; +} diff --git a/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts new file mode 100644 index 000000000000..031f30afba8a --- /dev/null +++ b/src/test/testing/testController/unittest/testDiscoveryAdapter.unit.test.ts @@ -0,0 +1,342 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as assert from 'assert'; +import * as path from 'path'; +import * as typeMoq from 'typemoq'; +import * as fs from 'fs'; +import { CancellationTokenSource, Uri } from 'vscode'; +import { Observable } from 'rxjs'; +import * as sinon from 'sinon'; +import { IConfigurationService } from '../../../../client/common/types'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { UnittestTestDiscoveryAdapter } from '../../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { Deferred, createDeferred } from '../../../../client/common/utils/async'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; + +suite('Unittest test discovery adapter', () => { + let configService: IConfigurationService; + let mockProc: MockChildProcess; + let execService: typeMoq.IMock<IPythonExecutionService>; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let deferred: Deferred<void>; + let expectedExtraVariables: Record<string, string>; + let expectedPath: string; + let uri: Uri; + let utilsStartDiscoveryNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + let cancellationTokenSource: CancellationTokenSource; + + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + + expectedPath = path.join('/', 'new', 'cwd'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'] }, + }), + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + console.log('execObservable is returning'); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + deferred = createDeferred(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + + // constants + expectedPath = path.join('/', 'my', 'test', 'path'); + uri = Uri.file(expectedPath); + expectedExtraVariables = { + TEST_RUN_PIPE: 'discoveryResultPipe-mockName', + }; + + utilsStartDiscoveryNamedPipeStub = sinon.stub(util, 'startDiscoveryNamedPipe'); + utilsStartDiscoveryNamedPipeStub.callsFake(() => Promise.resolve('discoveryResultPipe-mockName')); + cancellationTokenSource = new CancellationTokenSource(); + }); + teardown(() => { + sinon.restore(); + cancellationTokenSource.dispose(); + }); + + test('DiscoverTests should send the discovery command to the test server with the correct args', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('DiscoverTests should respect settings.testings.cwd when present', async () => { + const expectedNewPath = path.join('/', 'new', 'cwd'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['-v', '-s', '.', '-p', 'test*'], cwd: expectedNewPath.toString() }, + }), + } as unknown) as IConfigurationService; + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + assert.deepEqual(options.env, expectedExtraVariables); + assert.equal(options.cwd, expectedNewPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + test('Test discovery canceled before exec observable call finishes', async () => { + // set up exec mock + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // Trigger cancellation before exec observable call finishes + cancellationTokenSource.cancel(); + + await discoveryPromise; + + assert.ok( + true, + 'Test resolves correctly when triggering a cancellation token immediately after starting discovery.', + ); + }); + + test('Test discovery cancelled while exec observable is running and proc is closed', async () => { + // + const execService2 = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService2.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService2 + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + // Trigger cancellation while exec observable is running + cancellationTokenSource.cancel(); + return { + proc: mockProc as any, + out: new Observable<Output<string>>(), + dispose: () => { + /* no-body */ + }, + }; + }); + // set up exec mock + deferred = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve(execService2.object); + }); + + sinon.stub(fs.promises, 'lstat').callsFake( + async () => + ({ + isFile: () => true, + isSymbolicLink: () => false, + } as fs.Stats), + ); + sinon.stub(fs.promises, 'realpath').callsFake(async (pathEntered) => pathEntered.toString()); + + const adapter = new UnittestTestDiscoveryAdapter(configService); + const discoveryPromise = adapter.discoverTests(uri, execFactory.object, cancellationTokenSource.token); + + // add in await and trigger + await discoveryPromise; + assert.ok(true, 'Test resolves correctly when triggering a cancellation token in exec observable.'); + }); + + test('DiscoverTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object, undefined, undefined, mockProject); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); + + test('DiscoverTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const adapter = new UnittestTestDiscoveryAdapter(configService); + adapter.discoverTests(uri, execFactory.object); + const script = path.join(EXTENSION_ROOT_DIR, 'python_files', 'unittestadapter', 'discovery.py'); + const argsExpected = [script, '--udiscovery', '-v', '-s', '.', '-p', 'test*']; + + // must await until the execObservable is called in order to verify it + await deferred.promise; + + execService.verify( + (x) => + x.execObservable( + typeMoq.It.is<Array<string>>((argsActual) => { + try { + assert.equal(argsActual.length, argsExpected.length); + assert.deepEqual(argsActual, argsExpected); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + typeMoq.It.is<SpawnOptions>((options) => { + try { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + assert.equal(options.cwd, expectedPath); + assert.equal(options.throwOnStdErr, true); + return true; + } catch (e) { + console.error(e); + throw e; + } + }), + ), + typeMoq.Times.once(), + ); + }); +}); diff --git a/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts new file mode 100644 index 000000000000..8a86e9228567 --- /dev/null +++ b/src/test/testing/testController/unittest/testExecutionAdapter.unit.test.ts @@ -0,0 +1,581 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +import * as assert from 'assert'; +import { DebugSessionOptions, TestRun, TestRunProfileKind, Uri } from 'vscode'; +import * as typeMoq from 'typemoq'; +import * as sinon from 'sinon'; +import * as path from 'path'; +import { Observable } from 'rxjs/Observable'; +import { IConfigurationService } from '../../../../client/common/types'; +import { + IPythonExecutionFactory, + IPythonExecutionService, + Output, + SpawnOptions, +} from '../../../../client/common/process/types'; +import { createDeferred, Deferred } from '../../../../client/common/utils/async'; +import { ITestDebugLauncher, LaunchOptions } from '../../../../client/testing/common/types'; +import * as util from '../../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../../client/constants'; +import { MockChildProcess } from '../../../mocks/mockChildProcess'; +import { traceInfo } from '../../../../client/logging'; +import { UnittestTestExecutionAdapter } from '../../../../client/testing/testController/unittest/testExecutionAdapter'; +import * as extapi from '../../../../client/envExt/api.internal'; +import { ProjectAdapter } from '../../../../client/testing/testController/common/projectAdapter'; +import { createMockProjectAdapter } from '../testMocks'; + +suite('Unittest test execution adapter', () => { + let configService: IConfigurationService; + let execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + let adapter: UnittestTestExecutionAdapter; + let execService: typeMoq.IMock<IPythonExecutionService>; + let deferred: Deferred<void>; + let deferred4: Deferred<void>; + let debugLauncher: typeMoq.IMock<ITestDebugLauncher>; + (global as any).EXTENSION_ROOT_DIR = EXTENSION_ROOT_DIR; + let myTestPath: string; + let mockProc: MockChildProcess; + let utilsWriteTestIdsFileStub: sinon.SinonStub; + let utilsStartRunResultNamedPipeStub: sinon.SinonStub; + let useEnvExtensionStub: sinon.SinonStub; + setup(() => { + useEnvExtensionStub = sinon.stub(extapi, 'useEnvExtension'); + useEnvExtensionStub.returns(false); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['.'] }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + + // set up exec service with child process + mockProc = new MockChildProcess('', ['']); + const output = new Observable<Output<string>>(() => { + /* no op */ + }); + deferred4 = createDeferred(); + execService = typeMoq.Mock.ofType<IPythonExecutionService>(); + execService + .setup((x) => x.execObservable(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred4.resolve(); + return { + proc: mockProc as any, + out: output, + dispose: () => { + /* no-body */ + }, + }; + }); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + + // added + utilsWriteTestIdsFileStub = sinon.stub(util, 'writeTestIdsFile'); + debugLauncher = typeMoq.Mock.ofType<ITestDebugLauncher>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => Promise.resolve(execService.object)); + deferred = createDeferred(); + execService + .setup((x) => x.exec(typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(() => { + deferred.resolve(); + return Promise.resolve({ stdout: '{}' }); + }); + execFactory.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + execService.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + debugLauncher.setup((p) => ((p as unknown) as any).then).returns(() => undefined); + myTestPath = path.join('/', 'my', 'test', 'path', '/'); + + utilsStartRunResultNamedPipeStub = sinon.stub(util, 'startRunResultNamedPipe'); + utilsStartRunResultNamedPipeStub.callsFake(() => Promise.resolve('runResultPipe-mockName')); + + execService.setup((x) => x.getExecutablePath()).returns(() => Promise.resolve('/mock/path/to/python')); + }); + teardown(() => { + sinon.restore(); + }); + test('startTestIdServer called with correct testIds', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve({ + name: 'mockName', + dispose: () => { + /* no-op */ + }, + }); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const testIds = ['test1id', 'test2id']; + + adapter.runTests(uri, testIds, TestRunProfileKind.Run, testRun.object, execFactory.object); + + // add in await and trigger + await deferred2.promise; + await deferred3.promise; + mockProc.trigger('close'); + + // assert + sinon.assert.calledWithExactly(utilsWriteTestIdsFileStub, testIds); + }); + test('unittest execution called with correct args', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + const expectedExtraVariables = { + PYTHONPATH: myTestPath, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.env?.COVERAGE_ENABLED, undefined); // coverage not enabled + assert.equal(options.cwd, uri.fsPath); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('unittest execution respects settings.testing.cwd when present', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const newCwd = path.join('new', 'path'); + configService = ({ + getSettings: () => ({ + testing: { unittestArgs: ['.'], cwd: newCwd }, + }), + isTestExecution: () => false, + } as unknown) as IConfigurationService; + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + const expectedExtraVariables = { + PYTHONPATH: newCwd, + TEST_RUN_PIPE: 'runResultPipe-mockName', + RUN_TEST_IDS_PIPE: 'testIdPipe-mockName', + }; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.PYTHONPATH, expectedExtraVariables.PYTHONPATH); + assert.equal(options.env?.TEST_RUN_PIPE, expectedExtraVariables.TEST_RUN_PIPE); + assert.equal(options.env?.RUN_TEST_IDS_PIPE, expectedExtraVariables.RUN_TEST_IDS_PIPE); + assert.equal(options.cwd, newCwd); + assert.equal(options.throwOnStdErr, true); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('Debug launched correctly for unittest', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Debug, testRun.object, execFactory.object, debugLauncher.object); + await deferred3.promise; + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + assert.equal(launchOptions.cwd, uri.fsPath); + assert.equal(launchOptions.testProvider, 'unittest'); + assert.equal(launchOptions.pytestPort, 'runResultPipe-mockName'); + assert.strictEqual(launchOptions.runTestIdsPort, 'testIdPipe-mockName'); + assert.notEqual(launchOptions.token, undefined); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.is<DebugSessionOptions>((sessionOptions) => { + assert.equal(sessionOptions.testRun, testRun.object); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + test('unittest execution with coverage turned on correctly', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Coverage, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + assert.equal(options.env?.COVERAGE_ENABLED, uri.fsPath); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('RunTests should set PROJECT_ROOT_PATH when project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = ({ + projectId: 'file:///workspace/myproject', + projectUri: Uri.file(projectPath), + projectName: 'myproject', + workspaceUri: Uri.file('/workspace'), + } as unknown) as ProjectAdapter; + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + undefined, // debugLauncher + undefined, // interpreter + mockProject, + ); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + // Verify PROJECT_ROOT_PATH is set when project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + projectPath, + 'PROJECT_ROOT_PATH should be set to project URI path', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('RunTests should NOT set PROJECT_ROOT_PATH when no project is provided', async () => { + const deferred2 = createDeferred(); + const deferred3 = createDeferred(); + execFactory = typeMoq.Mock.ofType<IPythonExecutionFactory>(); + execFactory + .setup((x) => x.createActivatedEnvironment(typeMoq.It.isAny())) + .returns(() => { + deferred2.resolve(); + return Promise.resolve(execService.object); + }); + utilsWriteTestIdsFileStub.callsFake(() => { + deferred3.resolve(); + return Promise.resolve('testIdPipe-mockName'); + }); + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun.setup((t) => t.token).returns(() => ({ onCancellationRequested: () => undefined } as any)); + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests(uri, [], TestRunProfileKind.Run, testRun.object, execFactory.object); + + await deferred2.promise; + await deferred3.promise; + await deferred4.promise; + mockProc.trigger('close'); + + const pathToPythonFiles = path.join(EXTENSION_ROOT_DIR, 'python_files'); + const pathToExecutionScript = path.join(pathToPythonFiles, 'unittestadapter', 'execution.py'); + const expectedArgs = [pathToExecutionScript, '--udiscovery', '.']; + + execService.verify( + (x) => + x.execObservable( + expectedArgs, + typeMoq.It.is<SpawnOptions>((options) => { + // Verify PROJECT_ROOT_PATH is NOT set when no project is provided + assert.strictEqual( + options.env?.PROJECT_ROOT_PATH, + undefined, + 'PROJECT_ROOT_PATH should NOT be set when no project is provided', + ); + return true; + }), + ), + typeMoq.Times.once(), + ); + }); + + test('Debug mode with project should pass project.pythonProject to debug launcher', async () => { + const deferred3 = createDeferred(); + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + debugLauncher + .setup((dl) => dl.launchDebugger(typeMoq.It.isAny(), typeMoq.It.isAny(), typeMoq.It.isAny())) + .returns(async (_opts, callback) => { + traceInfo('stubs launch debugger'); + if (typeof callback === 'function') { + deferred3.resolve(); + callback(); + } + }); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + adapter.runTests( + uri, + [], + TestRunProfileKind.Debug, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + await deferred3.promise; + + debugLauncher.verify( + (x) => + x.launchDebugger( + typeMoq.It.is<LaunchOptions>((launchOptions) => { + // Project should be passed for project-based debugging + assert.ok(launchOptions.project, 'project should be defined'); + assert.equal(launchOptions.project?.name, 'myproject (Python 3.11)'); + assert.equal(launchOptions.project?.uri.fsPath, projectPath); + return true; + }), + typeMoq.It.isAny(), + typeMoq.It.isAny(), + ), + typeMoq.Times.once(), + ); + }); + + test('useEnvExtension mode with project should use project pythonEnvironment', async () => { + // Enable the useEnvExtension path + useEnvExtensionStub.returns(true); + + utilsWriteTestIdsFileStub.callsFake(() => Promise.resolve('testIdPipe-mockName')); + + // Store the deferredTillServerClose so we can resolve it + let serverCloseDeferred: Deferred<void> | undefined; + utilsStartRunResultNamedPipeStub.callsFake((_callback: unknown, deferred: Deferred<void>, _token: unknown) => { + serverCloseDeferred = deferred; + return Promise.resolve('runResultPipe-mockName'); + }); + + const projectPath = path.join('/', 'workspace', 'myproject'); + const mockProject = createMockProjectAdapter({ + projectPath, + projectName: 'myproject (Python 3.11)', + pythonPath: '/custom/python/path', + testProvider: 'unittest', + }); + + // Stub runInBackground to capture which environment was used + const runInBackgroundStub = sinon.stub(extapi, 'runInBackground'); + const exitCallbacks: ((code: number, signal: string | null) => void)[] = []; + // Promise that resolves when the production code registers its onExit handler + const onExitRegistered = createDeferred<void>(); + const mockProc2 = { + stdout: { on: sinon.stub() }, + stderr: { on: sinon.stub() }, + onExit: (cb: (code: number, signal: string | null) => void) => { + exitCallbacks.push(cb); + onExitRegistered.resolve(); + }, + kill: sinon.stub(), + }; + runInBackgroundStub.callsFake(() => Promise.resolve(mockProc2 as any)); + + const testRun = typeMoq.Mock.ofType<TestRun>(); + testRun + .setup((t) => t.token) + .returns( + () => + ({ + onCancellationRequested: () => undefined, + } as any), + ); + + const uri = Uri.file(myTestPath); + adapter = new UnittestTestExecutionAdapter(configService); + const runPromise = adapter.runTests( + uri, + [], + TestRunProfileKind.Run, + testRun.object, + execFactory.object, + debugLauncher.object, + undefined, + mockProject, + ); + + // Wait for production code to register its onExit handler + await onExitRegistered.promise; + + // Simulate process exit to complete the test + exitCallbacks.forEach((cb) => cb(0, null)); + + // Resolve the server close deferred to allow the runTests to complete + serverCloseDeferred?.resolve(); + + await runPromise; + + // Verify runInBackground was called with the project's Python environment + sinon.assert.calledOnce(runInBackgroundStub); + const envArg = runInBackgroundStub.firstCall.args[0]; + // The environment should be the project's pythonEnvironment + assert.ok(envArg, 'runInBackground should be called with an environment'); + assert.equal(envArg.execInfo?.run?.executable, '/custom/python/path'); + }); +}); diff --git a/src/test/testing/testController/utils.unit.test.ts b/src/test/testing/testController/utils.unit.test.ts new file mode 100644 index 000000000000..3cba6fb697a5 --- /dev/null +++ b/src/test/testing/testController/utils.unit.test.ts @@ -0,0 +1,754 @@ +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as fs from 'fs'; +import * as path from 'path'; +import { CancellationToken, TestController, TestItem, Uri, Range, Position } from 'vscode'; +import { writeTestIdsFile, populateTestTree } from '../../../client/testing/testController/common/utils'; +import { EXTENSION_ROOT_DIR } from '../../../client/constants'; +import { + DiscoveredTestNode, + DiscoveredTestItem, + ITestResultResolver, +} from '../../../client/testing/testController/common/types'; +import { RunTestTag, DebugTestTag } from '../../../client/testing/testController/common/testItemUtilities'; + +suite('writeTestIdsFile tests', () => { + let sandbox: sinon.SinonSandbox; + + setup(() => { + sandbox = sinon.createSandbox(); + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should write test IDs to a temporary file', async () => { + const testIds = ['test1', 'test2', 'test3']; + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + + // Set up XDG_RUNTIME_DIR + process.env = { + ...process.env, + XDG_RUNTIME_DIR: '/xdg/runtime/dir', + }; + + await writeTestIdsFile(testIds); + + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); + + test('should handle error when accessing temp directory', async () => { + const testIds = ['test1', 'test2', 'test3']; + const error = new Error('Access error'); + const accessStub = sandbox.stub(fs.promises, 'access').rejects(error); + const writeFileStub = sandbox.stub(fs.promises, 'writeFile').resolves(); + const mkdirStub = sandbox.stub(fs.promises, 'mkdir').resolves(); + + const result = await writeTestIdsFile(testIds); + + const tempFileFolder = path.join(EXTENSION_ROOT_DIR, '.temp'); + + assert.ok(result.startsWith(tempFileFolder)); + + assert.ok(accessStub.called); + assert.ok(mkdirStub.called); + assert.ok(writeFileStub.calledOnceWith(sinon.match.string, testIds.join('\n'))); + }); +}); + +suite('getTempDir tests', () => { + let sandbox: sinon.SinonSandbox; + let originalPlatform: NodeJS.Platform; + let originalEnv: NodeJS.ProcessEnv; + + setup(() => { + sandbox = sinon.createSandbox(); + originalPlatform = process.platform; + originalEnv = process.env; + }); + + teardown(() => { + sandbox.restore(); + Object.defineProperty(process, 'platform', { value: originalPlatform }); + process.env = originalEnv; + }); + + test('should use XDG_RUNTIME_DIR on non-Windows if available', async () => { + if (process.platform === 'win32') { + return; + } + // Force platform to be Linux + Object.defineProperty(process, 'platform', { value: 'linux' }); + + // Set up XDG_RUNTIME_DIR + process.env = { ...process.env, XDG_RUNTIME_DIR: '/xdg/runtime/dir' }; + + const testIds = ['test1', 'test2', 'test3']; + sandbox.stub(fs.promises, 'access').resolves(); + sandbox.stub(fs.promises, 'writeFile').resolves(); + + // This will use getTempDir internally + const result = await writeTestIdsFile(testIds); + + assert.ok(result.startsWith('/xdg/runtime/dir')); + }); +}); + +suite('populateTestTree tests', () => { + let sandbox: sinon.SinonSandbox; + let testController: TestController; + let resultResolver: ITestResultResolver; + let cancelationToken: CancellationToken; + let createTestItemStub: sinon.SinonStub; + let itemsAddStub: sinon.SinonStub; + let itemsGetStub: sinon.SinonStub; + + setup(() => { + sandbox = sinon.createSandbox(); + + // Create stubs for TestController methods + createTestItemStub = sandbox.stub(); + itemsAddStub = sandbox.stub(); + itemsGetStub = sandbox.stub(); + + // Create mock TestController + testController = { + createTestItem: createTestItemStub, + items: { + add: itemsAddStub, + get: itemsGetStub, + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + // Create mock result resolver + resultResolver = { + runIdToTestItem: new Map(), + runIdToVSid: new Map(), + vsIdToRunId: new Map(), + detailedCoverageMap: new Map(), + resolveDiscovery: sandbox.stub(), + resolveExecution: sandbox.stub(), + _resolveDiscovery: sandbox.stub(), + _resolveExecution: sandbox.stub(), + _resolveCoverage: sandbox.stub(), + }; + + // Mock cancellation token + cancelationToken = { + isCancellationRequested: false, + onCancellationRequested: sandbox.stub(), + } as any; + }); + + teardown(() => { + sandbox.restore(); + }); + + test('should create a root node if testRoot is undefined', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const mockRootItem: TestItem = { + id: '/test/path/root', + label: 'RootTest', + uri: Uri.file('/test/path/root'), + canResolveChildren: true, + tags: [RunTestTag, DebugTestTag], + children: { + add: sandbox.stub(), + get: sandbox.stub(), + delete: sandbox.stub(), + replace: sandbox.stub(), + forEach: sandbox.stub(), + size: 0, + [Symbol.iterator]: sandbox.stub(), + }, + } as any; + + createTestItemStub.returns(mockRootItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnce); + // Check the args manually - function uses testTreeData.path as id + const call = createTestItemStub.firstCall; + assert.strictEqual(call.args[0], '/test/path/root'); + assert.strictEqual(call.args[1], 'RootTest'); + // Don't check Uri.file since it's complex to compare + assert.ok(itemsAddStub.calledOnceWith(mockRootItem)); + assert.strictEqual(mockRootItem.canResolveChildren, true); + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should recursively add children as TestItems', () => { + // Arrange + // Tree structure: + // RootWorkspaceFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootWorkspaceFolder', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + label: 'test_example', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + assert.strictEqual(mockTestItem.canResolveChildren, false); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should create TestItem with correct range when lineno is provided', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 5, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + const expectedRange = new Range(new Position(4, 0), new Position(5, 0)); + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should handle lineno = 0 correctly', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: '0', + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert- if lineno is '0', range should be defined but at the top + const expectedRange = new Range(new Position(0, 0), new Position(0, 0)); + + assert.deepStrictEqual(mockTestItem.range, expectedRange); + }); + + test('should update resultResolver mappings correctly for test items', () => { + // Arrange + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const mockRootItem: TestItem = { + children: { add: sandbox.stub() }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.strictEqual(resultResolver.runIdToTestItem.get('run-id-123'), mockTestItem); + assert.strictEqual(resultResolver.runIdToVSid.get('run-id-123'), 'test-id'); + assert.strictEqual(resultResolver.vsIdToRunId.get('test-id'), 'run-id-123'); + }); + + test('should create nodes for non-leaf items and recurse', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── nested_test (test) + const nestedTestItem: DiscoveredTestItem = { + path: '/test/path/nested_test.py', + name: 'nested_test', + type_: 'test', + id_: 'nested-test-id', + lineno: 5, + runID: 'nested-run-id', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [nestedTestItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const rootChildrenGetStub = sandbox.stub().returns(undefined); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, + } as any; + + const nestedChildrenAddStub = sandbox.stub(); + const nestedChildrenGetStub = sandbox.stub().returns(undefined); + const mockNestedNode: TestItem = { + id: 'nested-id', + canResolveChildren: true, + tags: [], + children: { add: nestedChildrenAddStub, get: nestedChildrenGetStub }, + } as any; + + const mockNestedTestItem: TestItem = { + id: 'nested-test-id', + tags: [], + } as any; + + createTestItemStub.onFirstCall().returns(mockNestedNode); + createTestItemStub.onSecondCall().returns(mockNestedTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should create nested node - uses child.id_ for non-leaf nodes + assert.ok(createTestItemStub.calledWith('nested-id', 'NestedFolder', sinon.match.any)); + assert.ok(rootChildrenAddStub.calledWith(mockNestedNode)); + assert.strictEqual(mockNestedNode.canResolveChildren, true); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + + // Should create nested test item - uses child.id_ for test items too + assert.ok(createTestItemStub.calledWith('nested-test-id', 'nested_test', sinon.match.any)); + assert.ok(nestedChildrenAddStub.calledWith(mockNestedTestItem)); + }); + + test('should reuse existing nodes when they already exist', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── ExistingFolder (folder, already exists) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/existing', + name: 'ExistingFolder', + type_: 'folder', + id_: 'existing-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const rootChildrenAddStub = sandbox.stub(); + const existingChildrenAddStub = sandbox.stub(); + const existingChildrenGetStub = sandbox.stub().returns(undefined); + const existingNode: TestItem = { + id: 'existing-id', + children: { add: existingChildrenAddStub, get: existingChildrenGetStub }, + } as any; + const rootChildrenGetStub = sandbox.stub().withArgs('existing-id').returns(existingNode); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub, get: rootChildrenGetStub }, + } as any; + + const mockTestItem: TestItem = { + tags: [], + } as any; + + // Mock existing node in testController.items + itemsGetStub.withArgs('/test/path/existing').returns(existingNode); + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + // Should not create a new node, should reuse existing one + assert.ok(createTestItemStub.calledOnceWith('test-id', 'test_example', sinon.match.any)); + // Should not create a new node for the existing folder + assert.ok(createTestItemStub.neverCalledWith('existing-id', 'ExistingFolder', sinon.match.any)); + assert.ok(existingChildrenAddStub.calledWith(mockTestItem)); + // Should not add existing node to root children again + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should respect cancellation token and stop processing', () => { + // Arrange + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test1', + type_: 'test', + id_: 'test1-id', + lineno: 10, + runID: 'run-id-1', + }; + + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test2', + type_: 'test', + id_: 'test2-id', + lineno: 20, + runID: 'run-id-2', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Set cancellation token to be cancelled + const cancelledToken = { + isCancellationRequested: true, + onCancellationRequested: sandbox.stub(), + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelledToken); + + // Assert - no test items should be created when cancelled + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + assert.strictEqual(resultResolver.runIdToTestItem.size, 0); + }); + + test('should handle empty children array gracefully', () => { + // Arrange + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [], + }; + + const rootChildrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + children: { add: rootChildrenAddStub }, + } as any; + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert - should complete without errors + assert.ok(createTestItemStub.notCalled); + assert.ok(rootChildrenAddStub.notCalled); + }); + + test('should add correct tags to all created items', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── NestedFolder (folder) + // └── test_example (test) + const testItem: DiscoveredTestItem = { + path: '/test/path/test.py', + name: 'test_example', + type_: 'test', + id_: 'test-id', + lineno: 10, + runID: 'run-id-123', + }; + + const nestedNode: DiscoveredTestNode = { + path: '/test/path/nested', + name: 'NestedFolder', + type_: 'folder', + id_: 'nested-id', + children: [testItem], + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [nestedNode], + }; + + const mockRootItem: TestItem = { + id: 'root-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, + } as any; + + const mockNestedNode: TestItem = { + id: 'nested-id', + tags: [], + canResolveChildren: true, + children: { add: sandbox.stub(), get: sandbox.stub().returns(undefined) }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-id', + tags: [], + canResolveChildren: false, + } as any; + + createTestItemStub.onCall(0).returns(mockRootItem); + createTestItemStub.onCall(1).returns(mockNestedNode); + createTestItemStub.onCall(2).returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, undefined, resultResolver, cancelationToken); + + // Assert - All items should have RunTestTag and DebugTestTag + assert.deepStrictEqual(mockRootItem.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockNestedNode.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + test('should handle a test node with no lineno property', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // └── test_without_lineno (test, no lineno) + const testItem = { + path: '/test/path/test.py', + name: 'test_without_lineno', + type_: 'test', + id_: 'test-no-lineno-id', + runID: 'run-id-no-lineno', + } as DiscoveredTestItem; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem: TestItem = { + id: 'test-no-lineno-id', + label: 'test_without_lineno', + uri: Uri.file('/test/path/test.py'), + canResolveChildren: false, + tags: [], + range: undefined, + } as any; + + createTestItemStub.returns(mockTestItem); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledOnceWith('test-no-lineno-id', 'test_without_lineno', sinon.match.any)); + assert.ok(childrenAddStub.calledOnceWith(mockTestItem)); + // range is undefined since lineno is not provided + assert.strictEqual(mockTestItem.range, undefined); + assert.deepStrictEqual(mockTestItem.tags, [RunTestTag, DebugTestTag]); + }); + + test('should handle a node with multiple children', () => { + // Arrange + // Tree structure: + // RootTest (folder) + // ├── test_one (test) + // └── test_two (test) + const testItem1: DiscoveredTestItem = { + path: '/test/path/test1.py', + name: 'test_one', + type_: 'test', + id_: 'test-one-id', + lineno: 3, + runID: 'run-id-one', + }; + const testItem2: DiscoveredTestItem = { + path: '/test/path/test2.py', + name: 'test_two', + type_: 'test', + id_: 'test-two-id', + lineno: 7, + runID: 'run-id-two', + }; + + const testTreeData: DiscoveredTestNode = { + path: '/test/path/root', + name: 'RootTest', + type_: 'folder', + id_: 'root-id', + children: [testItem1, testItem2], + }; + + const childrenAddStub = sandbox.stub(); + const mockRootItem: TestItem = { + id: 'root-id', + children: { + add: childrenAddStub, + }, + } as any; + + const mockTestItem1: TestItem = { + id: 'test-one-id', + label: 'test_one', + uri: Uri.file('/test/path/test1.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(2, 0), new Position(3, 0)), + } as any; + const mockTestItem2: TestItem = { + id: 'test-two-id', + label: 'test_two', + uri: Uri.file('/test/path/test2.py'), + canResolveChildren: false, + tags: [], + range: new Range(new Position(6, 0), new Position(7, 0)), + } as any; + + createTestItemStub.onFirstCall().returns(mockTestItem1); + createTestItemStub.onSecondCall().returns(mockTestItem2); + + // Act + populateTestTree(testController, testTreeData, mockRootItem, resultResolver, cancelationToken); + + // Assert + assert.ok(createTestItemStub.calledWith('test-one-id', 'test_one', sinon.match.any)); + assert.ok(createTestItemStub.calledWith('test-two-id', 'test_two', sinon.match.any)); + // two test items called with mockRootItem's method childrenAddStub + assert.strictEqual(childrenAddStub.callCount, 2); + assert.deepStrictEqual(mockTestItem1.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem2.tags, [RunTestTag, DebugTestTag]); + assert.deepStrictEqual(mockTestItem1.range, new Range(new Position(2, 0), new Position(3, 0))); + assert.deepStrictEqual(mockTestItem2.range, new Range(new Position(6, 0), new Position(7, 0))); + }); +}); diff --git a/src/test/testing/testController/workspaceTestAdapter.unit.test.ts b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts new file mode 100644 index 000000000000..6d2895ca2979 --- /dev/null +++ b/src/test/testing/testController/workspaceTestAdapter.unit.test.ts @@ -0,0 +1,521 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import * as typemoq from 'typemoq'; + +import { TestController, TestItem, TestItemCollection, TestRun, Uri } from 'vscode'; +import { IConfigurationService } from '../../../client/common/types'; +import { UnittestTestDiscoveryAdapter } from '../../../client/testing/testController/unittest/testDiscoveryAdapter'; +import { UnittestTestExecutionAdapter } from '../../../client/testing/testController/unittest/testExecutionAdapter'; // 7/7 +import { WorkspaceTestAdapter } from '../../../client/testing/testController/workspaceTestAdapter'; +import * as Telemetry from '../../../client/telemetry'; +import { EventName } from '../../../client/telemetry/constants'; +import { ITestResultResolver } from '../../../client/testing/testController/common/types'; +import * as testItemUtilities from '../../../client/testing/testController/common/testItemUtilities'; +import * as util from '../../../client/testing/testController/common/utils'; +import * as ResultResolver from '../../../client/testing/testController/common/resultResolver'; +import { IPythonExecutionFactory } from '../../../client/common/process/types'; + +suite('Workspace test adapter', () => { + suite('Test discovery', () => { + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + + let discoverTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + + let telemetryEvent: { eventName: EventName; properties: Record<string, unknown> }[] = []; + let execFactory: typemoq.IMock<IPythonExecutionFactory>; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + } as unknown) as ITestResultResolver; + + // const vsIdToRunIdGetStub = sinon.stub(stubResultResolver.vsIdToRunId, 'get'); + // const expectedRunId = 'expectedRunId'; + // vsIdToRunIdGetStub.withArgs(sinon.match.any).returns(expectedRunId); + + // For some reason the 'tests' namespace in vscode returns undefined. + // While I figure out how to expose to the tests, they will run + // against a stub test controller and stub test items. + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; + + // testController = tests.createTestController('mock-python-tests', 'Mock Python Tests'); + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record<string, number> | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record<string, unknown>, + }); + }; + + discoverTestsStub = sinon.stub(UnittestTestDiscoveryAdapter.prototype, 'discoverTests'); + sendTelemetryStub = sinon.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + }); + + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sinon.restore(); + }); + + test('If discovery failed correctly create error node', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const uriFoo = Uri.parse('foo'); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + uriFoo, + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + execFactory = typemoq.Mock.ofType<IPythonExecutionFactory>(); + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, uriFoo, sinon.match.any, testProvider); + }); + + test("When discovering tests, the workspace test adapter should call the test discovery adapter's discoverTest method", async () => { + discoverTestsStub.resolves(); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledOnce(discoverTestsStub); + }); + + test('If discovery is already running, do not call discoveryAdapter.discoverTests again', async () => { + discoverTestsStub.callsFake( + async () => + new Promise<void>((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.discoverTests(testController, execFactory.object); + const two = workspaceTestAdapter.discoverTests(testController, execFactory.object); + + Promise.all([one, two]); + + sinon.assert.calledOnce(discoverTestsStub); + }); + + test('If discovery succeeds, send a telemetry event with the "failed" key set to false', async () => { + discoverTestsStub.resolves({ status: 'success' }); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); + assert.strictEqual(telemetryEvent.length, 2); + + const lastEvent = telemetryEvent[1]; + assert.strictEqual(lastEvent.properties.failed, false); + }); + + test('If discovery failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + discoverTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.discoverTests(testController, execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_DISCOVERY_DONE); + assert.strictEqual(telemetryEvent.length, 2); + + const lastEvent = telemetryEvent[1]; + assert.ok(lastEvent.properties.failed); + }); + }); + suite('Test execution workspace test adapter', () => { + let stubConfigSettings: IConfigurationService; + let stubResultResolver: ITestResultResolver; + let executionTestsStub: sinon.SinonStub; + let sendTelemetryStub: sinon.SinonStub; + let runInstance: typemoq.IMock<TestRun>; + let testControllerMock: typemoq.IMock<TestController>; + let telemetryEvent: { eventName: EventName; properties: Record<string, unknown> }[] = []; + let resultResolver: ResultResolver.PythonResultResolver; + let execFactory: typemoq.IMock<IPythonExecutionFactory>; + + // Stubbed test controller (see comment around L.40) + let testController: TestController; + let log: string[] = []; + + const sandbox = sinon.createSandbox(); + + setup(() => { + stubConfigSettings = ({ + getSettings: () => ({ + testing: { unittestArgs: ['--foo'] }, + }), + } as unknown) as IConfigurationService; + + stubResultResolver = ({ + resolveDiscovery: () => { + // no body + }, + resolveExecution: () => { + // no body + }, + vsIdToRunId: { + get: sinon.stub().returns('expectedRunId'), + }, + } as unknown) as ITestResultResolver; + const testItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + + testController = ({ + items: { + get: () => { + log.push('get'); + }, + add: () => { + log.push('add'); + }, + replace: () => { + log.push('replace'); + }, + delete: () => { + log.push('delete'); + }, + }, + createTestItem: () => { + log.push('createTestItem'); + return testItem; + }, + dispose: () => { + // empty + }, + } as unknown) as TestController; + + const mockSendTelemetryEvent = ( + eventName: EventName, + _: number | Record<string, number> | undefined, + properties: unknown, + ) => { + telemetryEvent.push({ + eventName, + properties: properties as Record<string, unknown>, + }); + }; + + executionTestsStub = sandbox.stub(UnittestTestExecutionAdapter.prototype, 'runTests'); + sendTelemetryStub = sandbox.stub(Telemetry, 'sendTelemetryEvent').callsFake(mockSendTelemetryEvent); + execFactory = typemoq.Mock.ofType<IPythonExecutionFactory>(); + runInstance = typemoq.Mock.ofType<TestRun>(); + + const testProvider = 'pytest'; + const workspaceUri = Uri.file('foo'); + resultResolver = new ResultResolver.PythonResultResolver(testController, testProvider, workspaceUri); + }); + + teardown(() => { + telemetryEvent = []; + log = []; + testController.dispose(); + sandbox.restore(); + }); + test('When executing tests, the right tests should be sent to be executed', async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + resultResolver, + ); + resultResolver.runIdToVSid.set('mockTestItem1', 'mockTestItem1'); + + sinon.stub(testItemUtilities, 'getTestCaseNodes').callsFake((testNode: TestItem) => + // Custom implementation logic here based on the provided testNode and collection + + // Example implementation: returning a predefined array of TestItem objects + [testNode], + ); + + const mockTestItem1 = createMockTestItem('mockTestItem1'); + const mockTestItem2 = createMockTestItem('mockTestItem2'); + const mockTestItems: [string, TestItem][] = [ + ['1', mockTestItem1], + ['2', mockTestItem2], + // Add as many mock TestItems as needed + ]; + const iterableMock = mockTestItems[Symbol.iterator](); + + const testItemCollectionMock = typemoq.Mock.ofType<TestItemCollection>(); + + testItemCollectionMock + .setup((x) => x.forEach(typemoq.It.isAny())) + .callback((callback) => { + let result = iterableMock.next(); + while (!result.done) { + callback(result.value[1]); + result = iterableMock.next(); + } + }) + .returns(() => mockTestItem1); + testControllerMock = typemoq.Mock.ofType<TestController>(); + testControllerMock.setup((t) => t.items).returns(() => testItemCollectionMock.object); + + await workspaceTestAdapter.executeTests( + testController, + runInstance.object, + [mockTestItem1, mockTestItem2], + execFactory.object, + ); + + runInstance.verify((r) => r.started(typemoq.It.isAny()), typemoq.Times.exactly(2)); + }); + + test("When executing tests, the workspace test adapter should call the test execute adapter's executionTest method", async () => { + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution is already running, do not call executionAdapter.runTests again', async () => { + executionTestsStub.callsFake( + async () => + new Promise<void>((resolve) => { + setTimeout(() => { + // Simulate time taken by discovery. + resolve(); + }, 2000); + }), + ); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + // Try running discovery twice + const one = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + const two = workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + Promise.all([one, two]); + + sinon.assert.calledOnce(executionTestsStub); + }); + + test('If execution failed correctly create error node', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + const blankTestItem = ({ + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + } as unknown) as TestItem; + const errorTestItemOptions: testItemUtilities.ErrorTestItemOptions = { + id: 'id', + label: 'label', + error: 'error', + }; + const createErrorTestItemStub = sinon.stub(testItemUtilities, 'createErrorTestItem').returns(blankTestItem); + const buildErrorNodeOptionsStub = sinon.stub(util, 'buildErrorNodeOptions').returns(errorTestItemOptions); + const testProvider = 'unittest'; + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledWithMatch(createErrorTestItemStub, sinon.match.any, sinon.match.any); + sinon.assert.calledWithMatch(buildErrorNodeOptionsStub, Uri.parse('foo'), sinon.match.any, testProvider); + }); + + test('If execution failed, send a telemetry event with the "failed" key set to true, and add an error node to the test controller', async () => { + executionTestsStub.rejects(new Error('foo')); + + const testDiscoveryAdapter = new UnittestTestDiscoveryAdapter(stubConfigSettings); + const testExecutionAdapter = new UnittestTestExecutionAdapter(stubConfigSettings); + + const workspaceTestAdapter = new WorkspaceTestAdapter( + 'unittest', + testDiscoveryAdapter, + testExecutionAdapter, + Uri.parse('foo'), + stubResultResolver, + ); + + await workspaceTestAdapter.executeTests(testController, runInstance.object, [], execFactory.object); + + sinon.assert.calledWith(sendTelemetryStub, EventName.UNITTEST_RUN_ALL_FAILED); + assert.strictEqual(telemetryEvent.length, 1); + }); + }); +}); + +function createMockTestItem(id: string): TestItem { + const range = typemoq.Mock.ofType<Range>(); + const mockTestItem = ({ + id, + canResolveChildren: false, + tags: [], + children: { + add: () => { + // empty + }, + }, + range, + uri: Uri.file('/foo/bar'), + } as unknown) as TestItem; + + return mockTestItem; +} diff --git a/src/test/testing/unittest/unittest.argsService.unit.test.ts b/src/test/testing/unittest/unittest.argsService.unit.test.ts deleted file mode 100644 index 5e28dbeb22b4..000000000000 --- a/src/test/testing/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/testing/common/argumentsHelper'; -import { IArgumentsHelper } from '../../../client/testing/types'; -import { ArgumentsService as UnittestArgumentsService } from '../../../client/testing/unittest/services/argsService'; - -suite('ArgsService: unittest', () => { - let argumentsService: UnittestArgumentsService; - - suiteSetup(() => { - const serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - const logger = typeMoq.Mock.ofType<ILogger>(); - - 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/testing/unittest/unittest.diagnosticService.unit.test.ts b/src/test/testing/unittest/unittest.diagnosticService.unit.test.ts deleted file mode 100644 index 7904441997d1..000000000000 --- a/src/test/testing/unittest/unittest.diagnosticService.unit.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { DiagnosticSeverity } from 'vscode'; -import * as localize from '../../../client/common/utils/localize'; -import { UnitTestDiagnosticService } from '../../../client/testing/common/services/unitTestDiagnosticService'; -import { TestStatus } from '../../../client/testing/common/types'; -import { PythonTestMessageSeverity } from '../../../client/testing/types'; - -suite('UnitTestDiagnosticService: unittest', () => { - let diagnosticService: UnitTestDiagnosticService; - - suiteSetup(() => { - diagnosticService = new UnitTestDiagnosticService(); - }); - suite('TestStatus: Error', () => { - let actualPrefix: string; - let actualSeverity: DiagnosticSeverity; - let expectedPrefix: string; - let expectedSeverity: DiagnosticSeverity; - suiteSetup(() => { - actualPrefix = diagnosticService.getMessagePrefix(TestStatus.Error)!; - actualSeverity = diagnosticService.getSeverity(PythonTestMessageSeverity.Error)!; - expectedPrefix = localize.Testing.testErrorDiagnosticMessage(); - expectedSeverity = DiagnosticSeverity.Error; - }); - test('Message Prefix', () => { - assert.equal(actualPrefix, expectedPrefix); - }); - test('Severity', () => { - assert.equal(actualSeverity, expectedSeverity); - }); - }); - suite('TestStatus: Fail', () => { - let actualPrefix: string; - let actualSeverity: DiagnosticSeverity; - let expectedPrefix: string; - let expectedSeverity: DiagnosticSeverity; - suiteSetup(() => { - actualPrefix = diagnosticService.getMessagePrefix(TestStatus.Fail)!; - actualSeverity = diagnosticService.getSeverity(PythonTestMessageSeverity.Failure)!; - expectedPrefix = localize.Testing.testFailDiagnosticMessage(); - expectedSeverity = DiagnosticSeverity.Error; - }); - test('Message Prefix', () => { - assert.equal(actualPrefix, expectedPrefix); - }); - test('Severity', () => { - assert.equal(actualSeverity, expectedSeverity); - }); - }); - suite('TestStatus: Skipped', () => { - let actualPrefix: string; - let actualSeverity: DiagnosticSeverity; - let expectedPrefix: string; - let expectedSeverity: DiagnosticSeverity; - suiteSetup(() => { - actualPrefix = diagnosticService.getMessagePrefix(TestStatus.Skipped)!; - actualSeverity = diagnosticService.getSeverity(PythonTestMessageSeverity.Skip)!; - expectedPrefix = localize.Testing.testSkippedDiagnosticMessage(); - expectedSeverity = DiagnosticSeverity.Information; - }); - test('Message Prefix', () => { - assert.equal(actualPrefix, expectedPrefix); - }); - test('Severity', () => { - assert.equal(actualSeverity, expectedSeverity); - }); - }); -}); diff --git a/src/test/testing/unittest/unittest.discovery.test.ts b/src/test/testing/unittest/unittest.discovery.test.ts deleted file mode 100644 index 5da7b0a4219e..000000000000 --- a/src/test/testing/unittest/unittest.discovery.test.ts +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import { EOL } from 'os'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import { ConfigurationTarget } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { ITestManagerFactory } from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { MockProcessService } from '../../mocks/proc'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; - -const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); -const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); -const unitTestTestFilesCwdPath = path.join(testFilesPath, 'cwd', 'src'); -const defaultUnitTestArgs = [ - '-v', - '-s', - '.', - '-p', - '*test*.py' -]; - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - unittest - discovery with mocked process output', () => { - let ioc: UnitTestIocContainer; - const rootDirectory = UNITTEST_TEST_FILES_PATH; - const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - suiteSetup(async () => { - await initialize(); - await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - }); - setup(async () => { - const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); - if (await fs.pathExists(cachePath)) { - await fs.remove(cachePath); - } - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - - // Mocks. - ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - - async function injectTestDiscoveryOutput(output: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.length > 1 && args[0] === '-c' && args[1].includes('import unittest') && args[1].includes('loader = unittest.TestLoader()')) { - callback({ - // Ensure any spaces added during code formatting or the like are removed. - out: output.split(/\r?\n/g).map(item => item.trim()).join(EOL), - source: 'stdout' - }); - } - }); - } - - test('Discover Tests (single test file)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(`start - test_one.Test_test1.test_A - test_one.Test_test1.test_B - test_one.Test_test1.test_c - `); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'test_one.Test_test1.test_A'), true, 'Test File not found'); - }); - - test('Discover Tests', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(`start - test_unittest_one.Test_test1.test_A - test_unittest_one.Test_test1.test_B - test_unittest_one.Test_test1.test_c - test_unittest_two.Test_test2.test_A2 - test_unittest_two.Test_test2.test_B2 - test_unittest_two.Test_test2.test_C2 - test_unittest_two.Test_test2.test_D2 - test_unittest_two.Test_test2a.test_222A2 - test_unittest_two.Test_test2a.test_222B2 - `); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 2, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_unittest_one.py' && t.nameToRun === 'test_unittest_one.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('testing.unittestArgs', ['-s=./tests', '-p=*_test*.py'], rootWorkspaceUri, configTarget); - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(`start - unittest_three_test.Test_test3.test_A - unittest_three_test.Test_test3.test_B - `); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 2, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'unittest_three_test.py' && t.nameToRun === 'unittest_three_test.Test_test3.test_A'), true, 'Test File not found'); - }); - - test('Setting cwd should return tests', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri, configTarget); - // tslint:disable-next-line:no-multiline-string - await injectTestDiscoveryOutput(`start - test_cwd.Test_Current_Working_Directory.test_cwd - `); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, unitTestTestFilesCwdPath); - - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFolders.length, 1, 'Incorrect number of test folders'); - assert.equal(tests.testFunctions.length, 1, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - }); -}); diff --git a/src/test/testing/unittest/unittest.discovery.unit.test.ts b/src/test/testing/unittest/unittest.discovery.unit.test.ts deleted file mode 100644 index 1b40aed67847..000000000000 --- a/src/test/testing/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/testing/common/constants'; -import { TestsHelper } from '../../../client/testing/common/testUtils'; -import { TestFlatteningVisitor } from '../../../client/testing/common/testVisitors/flatteningVisitor'; -import { ITestDiscoveryService, ITestRunner, ITestsParser, - Options, TestDiscoveryOptions, Tests, UnitTestParserOptions } from '../../../client/testing/common/types'; -import { IArgumentsHelper } from '../../../client/testing/types'; -import { TestDiscoveryService } from '../../../client/testing/unittest/services/discoveryService'; -import { TestsParser } from '../../../client/testing/unittest/services/parserService'; - -use(chaipromise); - -suite('Unit Tests - Unittest - Discovery', () => { - let discoveryService: ITestDiscoveryService; - let argsHelper: typeMoq.IMock<IArgumentsHelper>; - let testParser: typeMoq.IMock<ITestsParser>; - let runner: typeMoq.IMock<ITestRunner>; - let serviceContainer: typeMoq.IMock<IServiceContainer>; - const dir = path.join('a', 'b', 'c'); - const pattern = 'Pattern_To_Search_For'; - setup(() => { - serviceContainer = typeMoq.Mock.ofType<IServiceContainer>(); - argsHelper = typeMoq.Mock.ofType<IArgumentsHelper>(); - testParser = typeMoq.Mock.ofType<ITestsParser>(); - runner = typeMoq.Mock.ofType<ITestRunner>(); - - serviceContainer.setup(s => s.get(typeMoq.It.isValue(IArgumentsHelper), typeMoq.It.isAny())) - .returns(() => argsHelper.object); - serviceContainer.setup(s => s.get(typeMoq.It.isValue(ITestRunner), typeMoq.It.isAny())) - .returns(() => runner.object); - - discoveryService = new TestDiscoveryService(serviceContainer.object, testParser.object); - }); - test('Ensure discovery is invoked with the right args with start directory defined with -s', async () => { - const args: string[] = []; - const runOutput = 'xyz'; - const tests: Tests = { - summary: { errors: 1, failures: 0, passed: 0, skipped: 0 }, - testFiles: [], testFunctions: [], testSuites: [], - rootTestFolders: [], testFolders: [] - }; - argsHelper.setup(a => a.getOptionValues(typeMoq.It.isValue(args), typeMoq.It.isValue('-s'))) - .returns(() => dir) - .verifiable(typeMoq.Times.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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - 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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - 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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - 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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - 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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - 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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => false); - - const result = await discoveryService.discoverTests(options.object); - - expect(result).to.be.equal(tests); - 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<TestDiscoveryOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - options.setup(o => o.args).returns(() => args); - options.setup(o => o.token).returns(() => token.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - - const promise = discoveryService.discoverTests(options.object); - - await expect(promise).to.eventually.be.rejectedWith('cancelled'); - runner.verifyAll(); - testParser.verifyAll(); - }); - test('Ensure discovery resolves test suites in n-depth directories', async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => '/home/user/dev/tests'); - - const discoveryOutput: string = ['start', - 'apptests.debug.class_name.RootClassName.test_root', - 'apptests.debug.class_name.RootClassName.test_root_other', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', - ''].join('\n'); - - const tests: Tests = testsParser.parse(discoveryOutput, opts.object); - - expect(tests.testFiles.length).to.be.equal(3); - expect(tests.testFunctions.length).to.be.equal(6); - expect(tests.testSuites.length).to.be.equal(3); - expect(tests.testFolders.length).to.be.equal(5); - - // now ensure that each test function belongs within a single test suite... - tests.testFunctions.forEach(fn => { - if (fn.parentTestSuite) { - const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); - expect(testPrefix).to.equal(true, - [`function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, - `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, - `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); - } - }); - }); - test('Ensure discovery resolves test files in n-depth directories', async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => '/home/user/dev/tests'); - - const discoveryOutput: string = ['start', - 'apptests.debug.class_name.RootClassName.test_root', - 'apptests.debug.class_name.RootClassName.test_root_other', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', - ''].join('\n'); - - const tests: Tests = testsParser.parse(discoveryOutput, opts.object); - - expect(tests.testFiles.length).to.be.equal(3); - expect(tests.testFunctions.length).to.be.equal(6); - expect(tests.testSuites.length).to.be.equal(3); - expect(tests.testFolders.length).to.be.equal(5); - - // now ensure that the 'nameToRun' for each test function begins with its file's a single test suite... - tests.testFunctions.forEach(fn => { - if (fn.parentTestSuite) { - const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestFile.nameToRun); - expect(testPrefix).to.equal(true, - [`function ${fn.testFunction.name} was found in file ${fn.parentTestFile.name}, `, - `but the parent file 'nameToRun' (${fn.parentTestFile.nameToRun}) isn't the `, - `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); - } - }); - }); - test('Ensure discovery resolves test suites in n-depth directories when no start directory is given', async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => ''); - - const discoveryOutput: string = ['start', - 'apptests.debug.class_name.RootClassName.test_root', - 'apptests.debug.class_name.RootClassName.test_root_other', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', - ''].join('\n'); - - const tests: Tests = testsParser.parse(discoveryOutput, opts.object); - - expect(tests.testFiles.length).to.be.equal(3); - expect(tests.testFunctions.length).to.be.equal(6); - expect(tests.testSuites.length).to.be.equal(3); - expect(tests.testFolders.length).to.be.equal(4); - - // now ensure that each test function belongs within a single test suite... - tests.testFunctions.forEach(fn => { - if (fn.parentTestSuite) { - const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); - expect(testPrefix).to.equal(true, - [`function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, - `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, - `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); - } - }); - }); - test('Ensure discovery resolves test suites in n-depth directories when a relative start directory is given', async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => './tests'); - - const discoveryOutput: string = ['start', - 'apptests.debug.class_name.RootClassName.test_root', - 'apptests.debug.class_name.RootClassName.test_root_other', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first', - 'apptests.debug.first.class_name.FirstLevelClassName.test_first_other', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second', - 'apptests.debug.first.second.class_name.SecondLevelClassName.test_second_other', - ''].join('\n'); - - const tests: Tests = testsParser.parse(discoveryOutput, opts.object); - - expect(tests.testFiles.length).to.be.equal(3); - expect(tests.testFunctions.length).to.be.equal(6); - expect(tests.testSuites.length).to.be.equal(3); - expect(tests.testFolders.length).to.be.equal(5); - - // now ensure that each test function belongs within a single test suite... - tests.testFunctions.forEach(fn => { - if (fn.parentTestSuite) { - const testPrefix: boolean = fn.testFunction.nameToRun.startsWith(fn.parentTestSuite.nameToRun); - expect(testPrefix).to.equal(true, - [`function ${fn.testFunction.name} has a parent suite ${fn.parentTestSuite.name}, `, - `but the parent suite 'nameToRun' (${fn.parentTestSuite.nameToRun}) isn't the `, - `prefix to the functions 'nameToRun' (${fn.testFunction.nameToRun})`].join('')); - } - }); - }); - test('Ensure discovery will not fail with blank content' , async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => './tests'); - - const tests: Tests = testsParser.parse('', opts.object); - - expect(tests.testFiles.length).to.be.equal(0); - expect(tests.testFunctions.length).to.be.equal(0); - expect(tests.testSuites.length).to.be.equal(0); - expect(tests.testFolders.length).to.be.equal(0); - }); - test('Ensure discovery will not fail with corrupt content', async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => './tests'); - - const discoveryOutput: string = ['a;lskdjfa', - 'allikbrilkpdbfkdfbalk;nfm', - '', - ';;h,spmn,nlikmslkjls.bmnl;klkjna;jdfngad,lmvnjkldfhb', - ''].join('\n'); - - const tests: Tests = testsParser.parse(discoveryOutput, opts.object); - - expect(tests.testFiles.length).to.be.equal(0); - expect(tests.testFunctions.length).to.be.equal(0); - expect(tests.testSuites.length).to.be.equal(0); - expect(tests.testFolders.length).to.be.equal(0); - }); - test('Ensure discovery resolves when no tests are found in the given path', async () => { - const testHelper: TestsHelper = new TestsHelper(new TestFlatteningVisitor(), serviceContainer.object); - - const testsParser: TestsParser = new TestsParser(testHelper); - - const opts = typeMoq.Mock.ofType<UnitTestParserOptions>(); - const token = typeMoq.Mock.ofType<CancellationToken>(); - const wspace = typeMoq.Mock.ofType<Uri>(); - opts.setup(o => o.token).returns(() => token.object); - opts.setup(o => o.workspaceFolder).returns(() => wspace.object); - token.setup(t => t.isCancellationRequested) - .returns(() => true); - opts.setup(o => o.cwd).returns(() => '/home/user/dev'); - opts.setup(o => o.startDirectory).returns(() => './tests'); - - const discoveryOutput: string = 'start'; - - const tests: Tests = testsParser.parse(discoveryOutput, opts.object); - - expect(tests.testFiles.length).to.be.equal(0); - expect(tests.testFunctions.length).to.be.equal(0); - expect(tests.testSuites.length).to.be.equal(0); - expect(tests.testFolders.length).to.be.equal(0); - }); -}); diff --git a/src/test/testing/unittest/unittest.run.test.ts b/src/test/testing/unittest/unittest.run.test.ts deleted file mode 100644 index fc87fd8dfa8c..000000000000 --- a/src/test/testing/unittest/unittest.run.test.ts +++ /dev/null @@ -1,314 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import { EOL } from 'os'; -import * as path from 'path'; -import { instance, mock } from 'ts-mockito'; -import { ConfigurationTarget } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { IProcessServiceFactory } from '../../../client/common/process/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; -import { CommandSource, UNITTEST_PROVIDER } from '../../../client/testing/common/constants'; -import { TestRunner } from '../../../client/testing/common/runner'; -import { ITestManagerFactory, ITestRunner, IUnitTestSocketServer, TestsToRun } from '../../../client/testing/common/types'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner, IUnitTestHelper } from '../../../client/testing/types'; -import { UnitTestHelper } from '../../../client/testing/unittest/helper'; -import { TestManagerRunner } from '../../../client/testing/unittest/runner'; -import { ArgumentsService } from '../../../client/testing/unittest/services/argsService'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { MockProcessService } from '../../mocks/proc'; -import { MockUnitTestSocketServer } from '../mocks'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { initialize, initializeTest, IS_MULTI_ROOT_TEST } from './../../initialize'; - -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('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - }); - setup(async () => { - const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); - if (await fs.pathExists(cachePath)) { - await fs.remove(cachePath); - } - await initializeTest(); - initializeDI(); - await ignoreTestLauncher(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri, configTarget); - }); - - 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>(IArgumentsService, ArgumentsService, UNITTEST_PROVIDER); - ioc.serviceManager.add<IArgumentsHelper>(IArgumentsHelper, ArgumentsHelper); - ioc.serviceManager.add<ITestManagerRunner>(ITestManagerRunner, TestManagerRunner, UNITTEST_PROVIDER); - ioc.serviceManager.add<ITestRunner>(ITestRunner, TestRunner); - ioc.serviceManager.add<IUnitTestHelper>(IUnitTestHelper, UnitTestHelper); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - } - - async function ignoreTestLauncher() { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - // When running the python test launcher, just return. - procService.onExecObservable((_file, args, _options, callback) => { - if (args.length > 1 && args[0].endsWith('visualstudio_py_testlauncher.py')) { - callback({ out: '', source: 'stdout' }); - } - }); - } - async function injectTestDiscoveryOutput(output: string) { - const procService = await ioc.serviceContainer.get<IProcessServiceFactory>(IProcessServiceFactory).create() as MockProcessService; - procService.onExecObservable((_file, args, _options, callback) => { - if (args.length > 1 && args[0] === '-c' && args[1].includes('import unittest') && args[1].includes('loader = unittest.TestLoader()')) { - callback({ - // Ensure any spaces added during code formatting or the like are removed - out: output.split(/\r?\n/g).map(item => item.trim()).join(EOL), - source: 'stdout' - }); - } - }); - } - function injectTestSocketServerResults(results: {}[]) { - // Add results to be sent by unit test socket server. - const socketServer = ioc.serviceContainer.get<MockUnitTestSocketServer>(IUnitTestSocketServer); - socketServer.reset(); - socketServer.addResults(results); - } - - test('Run Tests', async () => { - await updateSetting('testing.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>(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('testing.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>(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('testing.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>(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('testing.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>(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('testing.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>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, rootDirectory); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testFn: TestsToRun = { testFile: [], testFolder: [], testFunction: [tests.testFunctions[0].testFunction], testSuite: [] }; - const results = await testManager.runTest(CommandSource.ui, testFn); - assert.equal(results.summary.errors, 0, 'Errors'); - assert.equal(results.summary.failures, 1, 'Failures'); - assert.equal(results.summary.passed, 0, 'Passed'); - assert.equal(results.summary.skipped, 0, 'skipped'); - }); -}); diff --git a/src/test/testing/unittest/unittest.test.ts b/src/test/testing/unittest/unittest.test.ts deleted file mode 100644 index 6f9ea25ea578..000000000000 --- a/src/test/testing/unittest/unittest.test.ts +++ /dev/null @@ -1,170 +0,0 @@ -'use strict'; - -import * as assert from 'assert'; -import * as fs from 'fs-extra'; -import * as path from 'path'; -import { anything, instance, mock, when } from 'ts-mockito'; -import { ConfigurationTarget } from 'vscode'; -import { EXTENSION_ROOT_DIR } from '../../../client/common/constants'; -import { EnvironmentActivationService } from '../../../client/interpreter/activation/service'; -import { IEnvironmentActivationService } from '../../../client/interpreter/activation/types'; -import { ICondaService, IInterpreterService } from '../../../client/interpreter/contracts'; -import { InterpreterService } from '../../../client/interpreter/interpreterService'; -import { CondaService } from '../../../client/interpreter/locators/services/condaService'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { - ITestManagerFactory, TestFile, - TestFunction, Tests, TestsToRun -} from '../../../client/testing/common/types'; -import { rootWorkspaceUri, updateSetting } from '../../common'; -import { UnitTestIocContainer } from '../serviceRegistry'; -import { - initialize, initializeTest, - IS_MULTI_ROOT_TEST -} from './../../initialize'; - -const testFilesPath = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'testFiles'); -const UNITTEST_TEST_FILES_PATH = path.join(testFilesPath, 'standard'); -const UNITTEST_SINGLE_TEST_FILE_PATH = path.join(testFilesPath, 'single'); -const UNITTEST_MULTI_TEST_FILE_PATH = path.join(testFilesPath, 'multi'); -const UNITTEST_COUNTS_TEST_FILE_PATH = path.join(testFilesPath, 'counter'); -const defaultUnitTestArgs = [ - '-v', - '-s', - '.', - '-p', - '*test*.py' -]; - -// tslint:disable-next-line:max-func-body-length -suite('Unit Tests - unittest - discovery against actual python process', () => { - let ioc: UnitTestIocContainer; - const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; - - suiteSetup(async () => { - - await initialize(); - await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); - }); - setup(async () => { - const cachePath = path.join(UNITTEST_TEST_FILES_PATH, '.cache'); - if (await fs.pathExists(cachePath)) { - await fs.remove(cachePath); - } - await initializeTest(); - initializeDI(); - }); - teardown(async () => { - await ioc.dispose(); - await updateSetting('testing.unittestArgs', defaultUnitTestArgs, rootWorkspaceUri!, configTarget); - }); - - function initializeDI() { - ioc = new UnitTestIocContainer(); - ioc.registerCommonTypes(); - ioc.registerVariableTypes(); - ioc.registerUnitTestTypes(); - ioc.registerProcessTypes(); - ioc.serviceManager.addSingletonInstance<ICondaService>(ICondaService, instance(mock(CondaService))); - ioc.serviceManager.addSingletonInstance<IInterpreterService>(IInterpreterService, instance(mock(InterpreterService))); - const mockEnvironmentActivationService = mock(EnvironmentActivationService); - when(mockEnvironmentActivationService.getActivatedEnvironmentVariables(anything())).thenResolve(); - ioc.serviceManager.rebindInstance<IEnvironmentActivationService>(IEnvironmentActivationService, instance(mockEnvironmentActivationService)); - } - - test('Discover Tests (single test file)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_SINGLE_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 1, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 3, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 1, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'test_one.Test_test1.test_A'), true, 'Test File not found'); - }); - - test('Discover Tests (many test files, subdir included)', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_MULTI_TEST_FILE_PATH); - const tests = await testManager.discoverTests(CommandSource.ui, true, true); - assert.equal(tests.testFiles.length, 3, 'Incorrect number of test files'); - assert.equal(tests.testFunctions.length, 9, 'Incorrect number of test functions'); - assert.equal(tests.testSuites.length, 3, 'Incorrect number of test suites'); - assert.equal(tests.testFiles.some(t => t.name === 'test_one.py' && t.nameToRun === 'test_one.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('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_MULTI_TEST_FILE_PATH); - const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testFile: TestFile | undefined = testsDiscovered.testFiles.find( - (value: TestFile) => value.nameToRun.endsWith('_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 () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_COUNTS_TEST_FILE_PATH); - const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testsFile: TestFile | undefined = testsDiscovered.testFiles.find( - (value: TestFile) => value.name.startsWith('test_unit_test_counter') - ); - assert.notEqual(testsFile, undefined, `No test file suffixed with _counter in test files. Looked in ${UNITTEST_COUNTS_TEST_FILE_PATH}.`); - assert.equal(testsFile!.suites.length, 1, 'Expected only 1 test suite in counter test file.'); - const testsToRun: TestsToRun = { - testFolder: [testsDiscovered.testFolders[0]] - }; - - // ensure that each re-run of the unit tests in question result in the same summary count information. - let testRunResult: Tests = await testManager.runTest(CommandSource.ui, testsToRun); - assert.equal(testRunResult.summary.failures, 2, 'This test was written assuming there was 2 tests run that would fail. (iteration 1)'); - assert.equal(testRunResult.summary.passed, 2, 'This test was written assuming there was 2 tests run that would succeed. (iteration 1)'); - - testRunResult = await testManager.runTest(CommandSource.ui, testsToRun); - assert.equal(testRunResult.summary.failures, 2, 'This test was written assuming there was 2 tests run that would fail. (iteration 2)'); - assert.equal(testRunResult.summary.passed, 2, 'This test was written assuming there was 2 tests run that would succeed. (iteration 2)'); - }); - - test('Re-run failed tests results in the correct number of tests counted', async () => { - await updateSetting('testing.unittestArgs', ['-s=./tests', '-p=test_*.py'], rootWorkspaceUri!, configTarget); - const factory = ioc.serviceContainer.get<ITestManagerFactory>(ITestManagerFactory); - const testManager = factory('unittest', rootWorkspaceUri!, UNITTEST_COUNTS_TEST_FILE_PATH); - const testsDiscovered: Tests = await testManager.discoverTests(CommandSource.ui, true, true); - const testsFile: TestFile | undefined = testsDiscovered.testFiles.find( - (value: TestFile) => value.name.startsWith('test_unit_test_counter') - ); - assert.notEqual(testsFile, undefined, `No test file suffixed with _counter in test files. Looked in ${UNITTEST_COUNTS_TEST_FILE_PATH}.`); - assert.equal(testsFile!.suites.length, 1, 'Expected only 1 test suite in counter test file.'); - const testsToRun: TestsToRun = { - testFolder: [testsDiscovered.testFolders[0]] - }; - - // ensure that each re-run of the unit tests in question result in the same summary count information. - let testRunResult: Tests = await testManager.runTest(CommandSource.ui, testsToRun); - assert.equal(testRunResult.summary.failures, 2, 'This test was written assuming there was 2 tests run that would fail. (iteration 1)'); - assert.equal(testRunResult.summary.passed, 2, 'This test was written assuming there was 2 tests run that would succeed. (iteration 1)'); - - testRunResult = await testManager.runTest(CommandSource.ui, testsToRun, true); - assert.equal(testRunResult.summary.failures, 2, 'This test was written assuming there was 2 tests run that would fail. (iteration 2)'); - }); -}); diff --git a/src/test/testing/unittest/unittest.unit.test.ts b/src/test/testing/unittest/unittest.unit.test.ts deleted file mode 100644 index 7cafcc6b493f..000000000000 --- a/src/test/testing/unittest/unittest.unit.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -import * as assert from 'assert'; -import { anything, capture, instance, mock, when } from 'ts-mockito'; -import { Uri } from 'vscode'; -import { IWorkspaceService } from '../../../client/common/application/types'; -import { WorkspaceService } from '../../../client/common/application/workspace'; -import { ConfigurationService } from '../../../client/common/configuration/service'; -import { IConfigurationService, IDisposableRegistry, IOutputChannel, IPythonSettings } from '../../../client/common/types'; -import { ServiceContainer } from '../../../client/ioc/container'; -import { IServiceContainer } from '../../../client/ioc/types'; -import { ArgumentsHelper } from '../../../client/testing/common/argumentsHelper'; -import { CommandSource } from '../../../client/testing/common/constants'; -import { TestCollectionStorageService } from '../../../client/testing/common/services/storageService'; -import { TestResultsService } from '../../../client/testing/common/services/testResultsService'; -import { TestsStatusUpdaterService } from '../../../client/testing/common/services/testsStatusService'; -import { TestsHelper } from '../../../client/testing/common/testUtils'; -import { TestResultResetVisitor } from '../../../client/testing/common/testVisitors/resultResetVisitor'; -import { FlattenedTestFunction, FlattenedTestSuite, ITestResultsService, ITestsHelper, ITestsStatusUpdaterService, TestFile, TestFolder, TestFunction, Tests, TestStatus, TestSuite, TestType } from '../../../client/testing/common/types'; -import { IArgumentsHelper, IArgumentsService, ITestManagerRunner } from '../../../client/testing/types'; -import { TestManager } from '../../../client/testing/unittest/main'; -import { TestManagerRunner } from '../../../client/testing/unittest/runner'; -import { ArgumentsService } from '../../../client/testing/unittest/services/argsService'; -import { MockOutputChannel } from '../../mockClasses'; -import { createMockTestDataItem } from '../common/testUtils.unit.test'; - -// tslint:disable:max-func-body-length no-any -suite('Unit Tests - unittest - run failed tests', () => { - let testManager: TestManager; - const workspaceFolder = Uri.file(__dirname); - let serviceContainer: IServiceContainer; - let testsHelper: ITestsHelper; - let testManagerRunner: ITestManagerRunner; - let tests: Tests; - function createTestData() { - - const folder1 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder2 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder3 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder4 = createMockTestDataItem<TestFolder>(TestType.testFolder); - const folder5 = createMockTestDataItem<TestFolder>(TestType.testFolder); - folder1.folders.push(folder2); - folder1.folders.push(folder3); - folder2.folders.push(folder4); - folder3.folders.push(folder5); - - const file1 = createMockTestDataItem<TestFile>(TestType.testFile); - const file2 = createMockTestDataItem<TestFile>(TestType.testFile); - const file3 = createMockTestDataItem<TestFile>(TestType.testFile); - const file4 = createMockTestDataItem<TestFile>(TestType.testFile); - folder1.testFiles.push(file1); - folder3.testFiles.push(file2); - folder3.testFiles.push(file3); - folder5.testFiles.push(file4); - - const suite1 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite2 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite3 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite4 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const suite5 = createMockTestDataItem<TestSuite>(TestType.testSuite); - const fn1 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn2 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn3 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn4 = createMockTestDataItem<TestFunction>(TestType.testFunction); - const fn5 = createMockTestDataItem<TestFunction>(TestType.testFunction); - file1.suites.push(suite1); - file1.suites.push(suite2); - file3.suites.push(suite3); - suite3.suites.push(suite4); - suite4.suites.push(suite5); - file1.functions.push(fn1); - file1.functions.push(fn2); - suite1.functions.push(fn3); - suite1.functions.push(fn4); - suite3.functions.push(fn5); - const flattendSuite1: FlattenedTestSuite = { - testSuite: suite1, - xmlClassName: suite1.xmlName - } as any; - const flattendSuite2: FlattenedTestSuite = { - testSuite: suite2, - xmlClassName: suite2.xmlName - } as any; - const flattendSuite3: FlattenedTestSuite = { - testSuite: suite3, - xmlClassName: suite3.xmlName - } as any; - const flattendSuite4: FlattenedTestSuite = { - testSuite: suite4, - xmlClassName: suite4.xmlName - } as any; - const flattendSuite5: FlattenedTestSuite = { - testSuite: suite5, - xmlClassName: suite5.xmlName - } as any; - const flattendFn1: FlattenedTestFunction = { - testFunction: fn1, - xmlClassName: fn1.name - } as any; - const flattendFn2: FlattenedTestFunction = { - testFunction: fn2, - xmlClassName: fn2.name - } as any; - const flattendFn3: FlattenedTestFunction = { - testFunction: fn3, - xmlClassName: fn3.name - } as any; - const flattendFn4: FlattenedTestFunction = { - testFunction: fn4, - xmlClassName: fn4.name - } as any; - const flattendFn5: FlattenedTestFunction = { - testFunction: fn5, - xmlClassName: fn5.name - } as any; - tests = { - rootTestFolders: [folder1], - summary: { errors: 0, skipped: 0, passed: 0, failures: 0 }, - testFiles: [file1, file2, file3, file4], - testFolders: [folder1, folder2, folder3, folder4, folder5], - testFunctions: [flattendFn1, flattendFn2, flattendFn3, flattendFn4, flattendFn5], - testSuites: [flattendSuite1, flattendSuite2, flattendSuite3, flattendSuite4, flattendSuite5] - }; - } - setup(() => { - createTestData(); - serviceContainer = mock(ServiceContainer); - testsHelper = mock(TestsHelper); - testManagerRunner = mock(TestManagerRunner); - const testStorage = mock(TestCollectionStorageService); - const workspaceService = mock(WorkspaceService); - const svcInstance = instance(serviceContainer); - when(testStorage.getTests(anything())).thenReturn(tests); - when(workspaceService.getWorkspaceFolder(anything())).thenReturn({ name: '', index: 0, uri: workspaceFolder }); - when(serviceContainer.get<IWorkspaceService>(IWorkspaceService)).thenReturn(instance(workspaceService)); - when(serviceContainer.get<IArgumentsHelper>(IArgumentsHelper)).thenReturn(new ArgumentsHelper(svcInstance)); - when(serviceContainer.get<IArgumentsService>(IArgumentsService, anything())).thenReturn(new ArgumentsService(svcInstance)); - when(serviceContainer.get<ITestsHelper>(ITestsHelper)).thenReturn(instance(testsHelper)); - when(serviceContainer.get<ITestManagerRunner>(ITestManagerRunner, anything())).thenReturn(instance(testManagerRunner)); - when(serviceContainer.get<ITestsStatusUpdaterService>(ITestsStatusUpdaterService)).thenReturn(new TestsStatusUpdaterService(instance(testStorage))); - when(serviceContainer.get<ITestResultsService>(ITestResultsService)).thenReturn(new TestResultsService(new TestResultResetVisitor())); - when(serviceContainer.get<IOutputChannel>(IOutputChannel)).thenReturn(instance(mock(MockOutputChannel))); - when(serviceContainer.get<IOutputChannel>(IOutputChannel)).thenReturn(instance(mock(MockOutputChannel))); - when(serviceContainer.get<IDisposableRegistry>(IDisposableRegistry)).thenReturn([]); - const settingsService = mock(ConfigurationService); - const settings: IPythonSettings = { - testing: { - unittestArgs: [] - } - } as any; - when(settingsService.getSettings(anything())).thenReturn(settings); - when(serviceContainer.get<IConfigurationService>(IConfigurationService)).thenReturn(instance(settingsService)); - - testManager = new TestManager(workspaceFolder, workspaceFolder.fsPath, svcInstance); - }); - - test('Run Failed tests', async () => { - testManager.discoverTests = () => Promise.resolve(tests); - when(testsHelper.shouldRunAllTests(anything())).thenReturn(false); - when(testManagerRunner.runTest(anything(), anything(), anything())).thenResolve(undefined as any); - (testManager as any).tests = tests; - tests.testFunctions[0].testFunction.status = TestStatus.Fail; - tests.testFunctions[2].testFunction.status = TestStatus.Fail; - - await testManager.runTest(CommandSource.testExplorer, undefined, true); - - const options = capture(testManagerRunner.runTest).last()[1]; - assert.deepEqual(options.tests, tests); - assert.equal(options.testsToRun!.testFile!.length, 0); - assert.equal(options.testsToRun!.testFolder!.length, 0); - assert.equal(options.testsToRun!.testSuite!.length, 0); - assert.equal(options.testsToRun!.testFunction!.length, 2); - assert.deepEqual(options.testsToRun!.testFunction![0], tests.testFunctions[0].testFunction); - assert.deepEqual(options.testsToRun!.testFunction![1], tests.testFunctions[2].testFunction); - }); - test('Run All tests', async () => { - testManager.discoverTests = () => Promise.resolve(tests); - when(testsHelper.shouldRunAllTests(anything())).thenReturn(false); - when(testManagerRunner.runTest(anything(), anything(), anything())).thenResolve(undefined as any); - (testManager as any).tests = tests; - - await testManager.runTest(CommandSource.testExplorer, undefined, true); - - const options = capture(testManagerRunner.runTest).last()[1]; - assert.deepEqual(options.tests, tests); - assert.equal(options.testsToRun!.testFile!.length, 0); - assert.equal(options.testsToRun!.testFolder!.length, 0); - assert.equal(options.testsToRun!.testSuite!.length, 0); - assert.equal(options.testsToRun!.testFunction!.length, 0); - }); -}); diff --git a/src/test/testing/utils.unit.test.ts b/src/test/testing/utils.unit.test.ts new file mode 100644 index 000000000000..8efa0cee0e65 --- /dev/null +++ b/src/test/testing/utils.unit.test.ts @@ -0,0 +1,51 @@ +import { expect, use } from 'chai'; +import * as chaiAsPromised from 'chai-as-promised'; +import * as utils from '../../client/testing/utils'; +import sinon from 'sinon'; +use(chaiAsPromised.default); + +function test_idToModuleClassMethod() { + try { + expect(utils.idToModuleClassMethod('foo')).to.equal('foo'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClass')).to.equal('c.MyClass'); + expect(utils.idToModuleClassMethod('a/b/c.pyMyClassmy_method')).to.equal('c.MyClass.my_method'); + expect(utils.idToModuleClassMethod('\\MyClass')).to.be.undefined; + console.log('test_idToModuleClassMethod passed'); + } catch (e) { + console.error('test_idToModuleClassMethod failed:', e); + } +} + +async function test_writeTestIdToClipboard() { + let clipboardStub = sinon.stub(utils, 'clipboardWriteText').resolves(); + const { writeTestIdToClipboard } = utils; + try { + // unittest id + const testItem = { id: 'a/b/c.pyMyClass\\my_method' }; + await writeTestIdToClipboard(testItem as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'c.MyClass.my_method'); + clipboardStub.resetHistory(); + + // pytest id + const testItem2 = { id: 'tests/test_foo.py::TestClass::test_method' }; + await writeTestIdToClipboard(testItem2 as any); + sinon.assert.calledOnceWithExactly(clipboardStub, 'tests/test_foo.py::TestClass::test_method'); + clipboardStub.resetHistory(); + + // undefined + await writeTestIdToClipboard(undefined as any); + sinon.assert.notCalled(clipboardStub); + + console.log('test_writeTestIdToClipboard passed'); + } catch (e) { + console.error('test_writeTestIdToClipboard failed:', e); + } finally { + sinon.restore(); + } +} + +// Run tests +(async () => { + test_idToModuleClassMethod(); + await test_writeTestIdToClipboard(); +})(); diff --git a/src/test/textUtils.ts b/src/test/textUtils.ts index 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 4d4e3e6a7eb3..dc4e79cbbff3 100644 --- a/src/test/unittests.ts +++ b/src/test/unittests.ts @@ -1,66 +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, setupTranspile } from './datascience/reactHelpers'; -import { initialize } from './vscode-mock'; - -// Custom module loader so we skip .css files that break non webpack wrapped compiles -// tslint:disable-next-line:no-var-requires no-require-imports -const Module = require('module'); - -// Required for DS functional tests. -// tslint:disable-next-line:no-function-expression -(function () { - const origRequire = Module.prototype.require; - const _require = (context: any, filepath: any) => { - return origRequire.call(context, filepath); - }; - Module.prototype.require = function (filepath: string) { - if (filepath.endsWith('.css') || filepath.endsWith('.svg')) { - return ''; - } - if (filepath.startsWith('expose-loader?')) { - // Pull out the thing to expose - const queryEnd = filepath.indexOf('!'); - if (queryEnd >= 0) { - const query = filepath.substring('expose-loader?'.length, queryEnd); - // tslint:disable-next-line:no-invalid-this - (global as any)[query] = _require(this, filepath.substring(queryEnd + 1)); - return ''; - } - } - if (filepath.startsWith('slickgrid/slick.core')) { - // Special case. This module sticks something into the global 'window' object. - // tslint:disable-next-line:no-invalid-this - const result = _require(this, filepath); - - // However it doesn't look in the 'window' object later. we have to move it to - // the globals when in node.js - if ((window as any).Slick) { - (global as any).Slick = (window as any).Slick; - } - - return result; - } - // tslint:disable-next-line:no-invalid-this - return _require(this, filepath); - }; -})(); - -// nteract/transforms-full expects to run in the browser so we have to fake -// parts of the browser here. -setUpDomEnvironment(); - -// Also have to setup babel to get the monaco editor to work. -setupTranspile(); -initialize(); +// 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/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>` `['spam', 'exe']` + * `<x/y/z/spam>` `['x/y/z/spam', 'exe']` + * `<spam.exe> `['spam.exe', 'exe']` + * + * Examples (valid but unlikely usage): + * + * `x/y/z/some_dir/` `['x/y/z/some_dir', 'dir']` # inline parents + * + * Examples (invalid): + * + * `/x/y/z/a_root/` # if not "topLevel" + * `./x/a_root/` ` # if not "topLevel" + * `../a_root/` # moving above CWD + * `x/y/../z/` # unnormalized + * `x/y/./z/` # unnormalized + * `<some_dir/>` # directories cannot be marked as executable + * `<some_dir>/` # directories cannot be marked as executable + * `<spam` # missing closing bracket + * `spam>` # missing opening bracket + */ +function parseFSEntry( + entry: string, + opts: { + topLevel?: boolean; + allowInlineParents?: boolean; + } = {}, +): [string | [string, string], FileKind] { + let text = entry; + let symlinkTarget = ''; + if (text.startsWith('|')) { + text = text.slice(1); + } else { + // Deal with executables. + if (text.match(/^<[^/<>]+>$/)) { + const name = text.slice(1, -1); + return [name, 'exe']; + } + // Deal with symlinks. + const parts = text.split(' -> ', 2); + if (parts.length == 2) { + [text, symlinkTarget] = parts; + if (text.endsWith('/')) { + throw Error(`bad symlink "${entry}"`); + } + if (symlinkTarget.includes('<') || symlinkTarget.includes('>')) { + throw Error(`bad entry "${entry}"`); + } + } + // It must be a regular file or directory. + if (text.includes('<') || text.includes('>')) { + throw Error(`bad entry "${entry}"`); + } + } + + // Make sure the entry is normalized. + const candidate = text.startsWith('./') ? text.slice(1) : text; + if (path.posix.normalize(candidate) !== candidate || text.startsWith('../')) { + throw Error(`expected normalized path, got "${entry}"`); + } + + // Handle "top-level" entries. + if (opts.topLevel) { + if (!text.endsWith('/')) { + throw Error(`expected directory at top level, got "${entry}"`); + } + if (!text.startsWith('/') && !text.startsWith('./')) { + throw Error(`expected prefix for top level, got "${entry}"`); + } + return [text, 'dir']; + } + + // Handle other entries. + let relname: string; + let reltext: string | [string, string]; + let kind: FileKind; + if (text.endsWith('/')) { + kind = 'dir'; + relname = text.slice(0, -1); + reltext = text; + } else if (symlinkTarget !== '') { + kind = 'symlink'; + relname = text; + reltext = [relname, symlinkTarget]; + } else { + kind = 'file'; + relname = text; + reltext = text; + } + if (relname.includes('/') && !opts.allowInlineParents) { + throw Error(`did not expect inline parents, got "${entry}"`); + } + if (relname.startsWith('/') || relname.startsWith('./')) { + throw Error(`expected relative path, got "${entry}"`); + } + return [reltext, kind]; +} + +/** + * Extract the directory tree represented by the given text.' + * + * "/" is the expected path separator, regardless of current OS. + * Directories always end with "/". Executables are surrounded + * by angle brackets "<>". See `parseFSEntry()` for more info. + * + * @returns - the flat list of (filename, parentdir, kind) for each + * node in the tree + * + * Example: + * + * parseFSTree(` + * ./x/y/z/root1/ + * dir1/ + * file1 + * subdir1_1/ + * # empty + * subdir1_2/ + * file2 + * <file3> + * <file4> + * file5 + * dir2/ + * file6 + * <file7> + * ./x/y/z/root2/ + * dir3/ + * subdir3_1/ + * file8 + * ./a/b/root3/ + * <file9> + * `.trim()) + * + * would produce the following: + * + * [ + * ['CWD/x/y/z/root1', '', 'dir'], + * ['CWD/x/y/z/root1/dir1', 'CWD/x/y/z/root1', 'dir'], + * ['CWD/x/y/z/root1/dir1/file1', 'CWD/x/y/z/root1/dir1', 'file'], + * ['CWD/x/y/z/root1/dir1/subdir1_1', 'CWD/x/y/z/root1/dir1', 'dir'], + * ['CWD/x/y/z/root1/dir1/subdir1_2', 'CWD/x/y/z/root1/dir1', 'dir'], + * ['CWD/x/y/z/root1/dir1/subdir1_2/file2', 'CWD/x/y/z/root1/dir1/subdir1_2', 'file'], + * ['CWD/x/y/z/root1/dir1/subdir1_2/file3', 'CWD/x/y/z/root1/dir1/subdir1_2', 'exe'], + * ['CWD/x/y/z/root1/dir1/file4', 'CWD/x/y/z/root1/dir1', 'exe'], + * ['CWD/x/y/z/root1/dir1/file5', 'CWD/x/y/z/root1/dir1', 'file'], + * ['CWD/x/y/z/root1/dir2', 'CWD/x/y/z/root1', 'dir'], + * ['CWD/x/y/z/root1/dir2/file6', 'CWD/x/y/z/root1/dir2', 'file'], + * ['CWD/x/y/z/root1/dir2/file7', 'CWD/x/y/z/root1/dir2', 'exe'], + * + * ['CWD/x/y/z/root2', '', 'dir'], + * ['CWD/x/y/z/root2/dir3', 'CWD/x/y/z/root2', 'dir'], + * ['CWD/x/y/z/root2/dir3/subdir3_1', 'CWD/x/y/z/root2/dir3', 'dir'], + * ['CWD/x/y/z/root2/dir3/subdir3_1/file8', 'CWD/x/y/z/root2/dir3/subdir3_1', 'file'], + * + * ['CWD/a/b/root3', '', 'dir'], + * ['CWD/a/b/root3/file9', 'CWD/a/b/root3', 'exe'], + * ] + */ +export function parseFSTree( + text: string, + // Use process.cwd() by default. + cwd?: string, +): [string | [string, string], string, FileKind][] { + const curDir = cwd ?? process.cwd(); + const parsed: [string | [string, string], string, FileKind][] = []; + + const entries = parseTree(text); + entries.forEach((data) => { + const [entry, parentIndex] = data; + const opts = { + topLevel: parentIndex === -1, + allowInlineParents: false, + }; + const [relname, kind] = parseFSEntry(entry, opts); + let fullname: string | [string, string]; + let parentFilename: string; + if (parentIndex === -1) { + parentFilename = ''; + fullname = path.resolve(curDir, relname as string); + } else { + if (typeof parsed[parentIndex][0] !== 'string') { + throw Error(`parent can't be a symlink, got ${parsed[parentIndex]} (for ${kind} ${relname})`); + } + parentFilename = parsed[parentIndex][0] as string; + if (kind === 'symlink') { + let [target, symlink] = relname as [string, string]; + target = path.join(parentFilename, target); + symlink = path.join(parentFilename, symlink); + fullname = [target, symlink]; + } else { + fullname = path.join(parentFilename, relname as string); + } + } + parsed.push([fullname, parentFilename, kind]); + }); + + return parsed; +} + +/** + * Mirror the directory tree (represented by the given text) on disk. + * + * See `parseFSTree()` for the "spec" format. + */ +export async function ensureFSTree( + spec: string, + // Use process.cwd() by default. + cwd?: string, +): Promise<string[]> { + const roots: string[] = []; + const promises = parseFSTree(spec, cwd) + // Now ensure each entry exists. + .map(async (data) => { + const [filename, parentFilename, kind] = data; + + try { + if (kind === 'dir') { + await fsapi.ensureDir(filename as string); + } else if (kind === 'exe') { + await ensureExecutable(filename as string); + } else if (kind === 'file') { + // "touch" the file. + await fsapi.ensureFile(filename as string); + } else if (kind === 'symlink') { + const [symlink, target] = filename as [string, string]; + await ensureSymlink(target, symlink); + } else { + throw Error(`unsupported file kind ${kind}`); + } + } catch (err) { + console.log('FAILED:', err); + throw err; + } + + if (parentFilename === '') { + roots.push(filename as string); + } + }); + await Promise.all(promises); + return roots; +} + +async function ensureExecutable(filename: string): Promise<void> { + // "touch" the file. + await fsapi.ensureFile(filename as string); + await fsapi.chmod(filename as string, 0o755); +} + +async function ensureSymlink(target: string, filename: string): Promise<void> { + try { + await fsapi.ensureSymlink(target, filename); + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code === 'ENOENT') { + // The target doesn't exist. Make the symlink anyway. + try { + await fsapi.symlink(target, filename); + } catch (err) { + const symlinkError = err as NodeJS.ErrnoException; + if (symlinkError.code !== 'EEXIST') { + throw err; // re-throw + } + } + } else { + throw err; // re-throw + } + } +} diff --git a/src/test/utils/interpreters.ts b/src/test/utils/interpreters.ts 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>): 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 cd99771644b4..b7ea2bc549a0 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -3,42 +3,67 @@ 'use strict'; -// tslint:disable:no-invalid-this no-require-imports no-var-requires no-any - -import * as TypeMoq from 'typemoq'; import * as vscode from 'vscode'; import * as vscodeMocks from './mocks/vsc'; import { vscMockTelemetryReporter } from './mocks/vsc/telemetryReporter'; +import { anything, instance, mock, when } from 'ts-mockito'; +import { TestItem } from 'vscode'; const Module = require('module'); type VSCode = typeof vscode; const mockedVSCode: Partial<VSCode> = {}; -export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: TypeMoq.IMock<VSCode[P]> } = {}; +export const mockedVSCodeNamespaces: { [P in keyof VSCode]?: VSCode[P] } = {}; const originalLoad = Module._load; function generateMock<K extends keyof VSCode>(name: K): void { - const mockedObj = TypeMoq.Mock.ofType<VSCode[K]>(); - (mockedVSCode as any)[name] = mockedObj.object; + const mockedObj = mock<VSCode[K]>(); + (mockedVSCode as any)[name] = instance(mockedObj); mockedVSCodeNamespaces[name] = mockedObj as any; } +class MockClipboard { + private text: string = ''; + public readText(): Promise<string> { + return Promise.resolve(this.text); + } + public async writeText(value: string): Promise<void> { + this.text = value; + } +} export function initialize() { generateMock('workspace'); generateMock('window'); generateMock('commands'); generateMock('languages'); + generateMock('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<vscode.Extension<any>>(); + const packageJson = mock<any>(); + const contributes = mock<any>(); + when(extension.packageJSON).thenReturn(instance(packageJson)); + when(packageJson.contributes).thenReturn(instance(contributes)); + when(contributes.debuggers).thenReturn([{ aiKey: '' }]); + when(mockedVSCodeNamespaces.extensions!.getExtension(anything())).thenReturn(instance(extension)); + when(mockedVSCodeNamespaces.extensions!.all).thenReturn([]); // When upgrading to npm 9-10, this might have to change, as we could have explicit imports (named imports). Module._load = function (request: any, _parent: any) { if (request === 'vscode') { return mockedVSCode; } - if (request === 'vscode-extension-telemetry') { - 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 @@ -49,20 +74,32 @@ export function initialize() { }; } -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.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.ConfigurationTarget; @@ -77,16 +114,64 @@ mockedVSCode.ViewColumn = vscodeMocks.vscMockExtHostedTypes.ViewColumn; mockedVSCode.TextEditorRevealType = vscodeMocks.vscMockExtHostedTypes.TextEditorRevealType; mockedVSCode.TreeItem = vscodeMocks.vscMockExtHostedTypes.TreeItem; mockedVSCode.TreeItemCollapsibleState = vscodeMocks.vscMockExtHostedTypes.TreeItemCollapsibleState; -mockedVSCode.CodeActionKind = vscodeMocks.vscMock.CodeActionKind; +(mockedVSCode 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<typeof vscode.extensions>(); -extensions.setup(e => e.all).returns(() => []); -const extension = TypeMoq.Mock.ofType<vscode.Extension<any>>(); -const packageJson = TypeMoq.Mock.ofType<any>(); -const contributes = TypeMoq.Mock.ofType<any>(); -extension.setup(e => e.packageJSON).returns(() => packageJson.object); -packageJson.setup(p => p.contributes).returns(() => contributes.object); -contributes.setup(p => p.debuggers).returns(() => [{ aiKey: '' }]); -extensions.setup(e => e.getExtension(TypeMoq.It.isAny())).returns(() => extension.object); -mockedVSCode.extensions = extensions.object; +// 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 4abb354fde53..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<IPythonSettings>; - let generator: Generator; - let factory: typemoq.IMock<IProcessServiceFactory>; - let shell: IApplicationShell; - let processService: IProcessService; - let fs: IFileSystem; - const folderUri = Uri.parse(path.join('a', 'b', 'c')); - setup(() => { - pythonSettings = typemoq.Mock.ofType<IPythonSettings>(); - configurationService = mock(ConfigurationService); - factory = typemoq.Mock.ofType<IProcessServiceFactory>(); - shell = mock(ApplicationShell); - fs = mock(FileSystem); - processService = mock(ProcessService); - factory.setup(f => f.create(typemoq.It.isAny())).returns(() => Promise.resolve(instance(processService))); - when(configurationService.getSettings(anything())).thenReturn(pythonSettings.object); - const outputChannel = typemoq.Mock.ofType<IOutputChannel>(); - generator = new Generator(folderUri, outputChannel.object, instance(shell), - instance(fs), factory.object, instance(configurationService)); - }); - test('should be disabled', () => { - const workspaceSymbols = { enabled: false } as any; - pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymbols); - - expect(generator.enabled).to.be.equal(false, 'not disabled'); - }); - test('should be enabled', () => { - const workspaceSymbols = { enabled: true } as any; - pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymbols); - - expect(generator.enabled).to.be.equal(true, 'not enabled'); - }); - test('Check tagFilePath', () => { - const workspaceSymbols = { tagFilePath: '1234' } as any; - pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymbols); - - expect(generator.tagFilePath).to.be.equal('1234'); - }); - test('Throw error when generating tags', async () => { - const ctagsPath = 'CTAG_PATH'; - const workspaceSymbols = { - enabled: true, tagFilePath: '1234', - exclusionPatterns: [], ctagsPath - } as any; - pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymbols); - when(fs.directoryExists(anything())).thenResolve(true); - const observable = { - out: { - subscribe: (cb: (out: Output<string>) => void, _errorCb: any, done: Function) => { - cb({ source: 'stderr', out: 'KABOOM' }); - done(); - } - } - }; - when(processService.execObservable(ctagsPath, anything(), anything())) - .thenReturn(observable as any); - - const promise = generator.generateWorkspaceTags(); - await expect(promise).to.eventually.be.rejectedWith('KABOOM'); - verify(shell.setStatusBarMessage(anything(), anything())).once(); - }); - test('Does not throw error when generating tags', async () => { - const ctagsPath = 'CTAG_PATH'; - const workspaceSymbols = { - enabled: true, tagFilePath: '1234', - exclusionPatterns: [], ctagsPath - } as any; - pythonSettings.setup(p => p.workspaceSymbols).returns(() => workspaceSymbols); - when(fs.directoryExists(anything())).thenResolve(true); - const observable = { - out: { - subscribe: (cb: (out: Output<string>) => void, _errorCb: any, done: Function) => { - cb({ source: 'stdout', out: '' }); - done(); - } - } - }; - when(processService.execObservable(ctagsPath, anything(), anything())) - .thenReturn(observable as any); - - await generator.generateWorkspaceTags(); - verify(shell.setStatusBarMessage(anything(), anything())).once(); - }); -}); diff --git a/src/test/workspaceSymbols/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/workspace1/.vscode/.ropeproject/config.py b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/config.py new file mode 100644 index 000000000000..dee2d1ae9a6b --- /dev/null +++ b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/config.py @@ -0,0 +1,114 @@ +# The default ``config.py`` +# flake8: noqa + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git', '.tox'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + # prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + # prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + # prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + # If `True`, rope will insert new module imports as + # `from <package> import <module>` by default. + prefs['prefer_module_from_imports'] = False + + # If `True`, rope will transform a comma list of imports into + # multiple separate import statements when organizing + # imports. + prefs['split_imports'] = False + + # If `True`, rope will remove all top-level import statements and + # reinsert them at the top of the module when making changes. + prefs['pull_imports_to_top'] = True + + # If `True`, rope will sort imports alphabetically by module name instead + # of alphabetically by import statement, with from imports after normal + # imports. + prefs['sort_imports_alphabetically'] = False + + # Location of implementation of + # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general + # case, you don't have to change this value, unless you're an rope expert. + # Change this value to inject you own implementations of interfaces + # listed in module rope.base.oi.type_hinting.providers.interfaces + # For example, you can add you own providers for Django Models, or disable + # the search type-hinting in a class hierarchy, etc. + prefs['type_hinting_factory'] = ( + 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/objectdb b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/objectdb new file mode 100644 index 000000000000..0a47446c0ad2 Binary files /dev/null and b/src/testMultiRootWkspc/workspace1/.vscode/.ropeproject/objectdb differ diff --git a/src/testMultiRootWkspc/workspace1/.vscode/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/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 babbea3fc8c2..000000000000 --- a/tpn/distribution.toml +++ /dev/null @@ -1,1949 +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.2.1" -url = "https://registry.npmjs.org/@jupyterlab/coreutils/-/coreutils-2.2.1.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.1.1" -url = "https://registry.npmjs.org/@jupyterlab/observables/-/observables-2.1.1.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.2.1" -url = "https://registry.npmjs.org/@jupyterlab/services/-/services-3.2.1.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 = "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. <fernando.perez@colorado.edu> -Copyright (c) 2001, Janko Hauser <jhauser@zscout.de> -Copyright (c) 2001, Nathaniel Gray <n8gray@caltech.edu> - -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 = "Jedi" -version = "0.13.3" -url = "https://github.com/davidhalter/jedi/tree/v0.13.3" -purpose = "PyPI" -license = """ -All contributions towards Jedi are MIT licensed. - -------------------------------------------------------------------------------- -The MIT License (MIT) - -Copyright (c) <2013> <David Halter and others, see AUTHORS.txt> - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 = "_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-code-frame" -version = "6.26.0" -url = "https://github.com/babel/babel/tree/v6.26.0/packages/babel-code-frame" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2014-2017 Sebastian McKenzie <sebmck@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 = "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 = "bootstrap-less" -version = "3.3.8" -url = "https://github.com/distros/bootstrap-less" -purpose = "npm" -license = """ -The MIT License (MIT) - -Copyright (c) 2011-2019 Twitter, Inc. -Copyright (c) 2011-2019 The Bootstrap Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -[[project]] -name = "brotli" -version = "1.3.2" -url = "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz" -purpose = "npm" -license = """ -Copyright 2019 brotli 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. -""" - -[[project]] -name = "browserify-optional" -version = "1.0.1" -url = "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz" -purpose = "npm" -license = """ -Copyright 2019 browserify-optional 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. -""" - -[[project]] -name = "dfa" -version = "1.2.0" -url = "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz" -purpose = "npm" -license = """ -Copyright 2019 dfa 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. -""" - -[[project]] -name = "falafel" -version = "2.1.0" -url = "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz" -purpose = "npm" -license = """ -Copyright 2019 falafel 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. -""" - -[[project]] -name = "fontkit" -version = "1.8.0" -url = "https://registry.npmjs.org/fontkit/-/fontkit-1.8.0.tgz" -purpose = "npm" -license = """ -Copyright 2019 fontkit 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. -""" - -[[project]] -name = "hash.js" -version = "1.1.7" -url = "https://github.com/indutny/hash.js/tree/v1.1.7" -purpose = "npm" -license = """ -This software is licensed under the MIT License. - -Copyright Fedor Indutny, 2014. - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to permit -persons to whom the Software is furnished to do so, subject to the -following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN -NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR -OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE -USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -[[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 &lt;julian@juliangruber.com&gt; - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 = "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 = "linear-layout-vector" -version = "0.0.1" -url = "https://registry.npmjs.org/linear-layout-vector/-/linear-layout-vector-0.0.1.tgz" -purpose = "npm" -license = """ -Copyright 2019 linear-layout-vector 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 = "linebreak" -version = "0.3.0" -url = "https://registry.npmjs.org/linebreak/-/linebreak-0.3.0.tgz" -purpose = "npm" -license = """ -Copyright 2019 linebreak 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. -""" - -[[project]] -name = "magic-string" -version = "0.22.5" -url = "https://registry.npmjs.org/magic-string/-/magic-string-0.22.5.tgz" -purpose = "npm" -license = """ -Copyright 2018 Rich Harris - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 = "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 = "parso" -version = "0.5.0" -url = "https://github.com/davidhalter/parso/tree/v0.5.0" -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> <David Halter and others, see AUTHORS.txt> - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION 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 = "path-parse" -version = "1.0.5" -url = "https://github.com/jbgutierrez/path-parse" -purpose = "npm" -license = """ -The MIT License (MIT) - -Copyright (c) 2015 Javier Blanco - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -""" - -[[project]] -name = "png-js" -version = "0.1.1" -url = "https://registry.npmjs.org/png-js/-/png-js-0.1.1.tgz" -purpose = "npm" -license = """ -MIT License - -Copyright (c) 2017 Devon Govett - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. -""" - -[[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 <lupomontero@gmail.com> - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -[[project]] -name = "ptvsd" -version = "4.2.4" -url = "https://github.com/Microsoft/ptvsd/tree/v4.2.4" -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 = "raf" -version = "3.4.0" -url = "https://registry.npmjs.org/raf/-/raf-3.4.0.tgz" -purpose = "npm" -license = """ -Copyright 2013 Chris Dickinson <chris@neversaw.us> - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 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 = "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 <tituswormer@gmail.com> -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 = "restructure" -version = "0.5.4" -url = "https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz" -purpose = "npm" -license = """ -Copyright 2019 restructure 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. -""" - -[[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 = "slickgrid" -version = "2.4.7" -url = "https://registry.npmjs.org/slickgrid/-/slickgrid-2.4.7.tgz" -purpose = "npm" -license = """ -Copyright (c) 2009-2019 Michael Leibman and Ben McIntyre, http://github.com/6pac/slickgrid - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[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 = "svg-to-pdfkit" -version = "0.1.7" -url = "https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.7.tgz" -purpose = "npm" -license = """ -Copyright 2019 svg-to-pdfkit 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. -""" - -[[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 = "tiny-inflate" -version = "1.0.2" -url = "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.2.tgz" -purpose = "npm" -license = """ -Copyright 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[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 <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.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 = "unicode-properties" -version = "1.1.0" -url = "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.1.0.tgz" -purpose = "npm" -license = """ -Copyright 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[project]] -name = "unicode-trie" -version = "0.3.1" -url = "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz" -purpose = "npm" -license = """ -Copyright 2018 - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" - -[[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 <mail@adamhalasz.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 = "untangle" -version = "(for ptvsd 4)" -url = "https://pypi.org/project/untangle/" -purpose = "explicit" -license = """ -# Author: Christian Stefanescu <chris@0chris.com> - -# Contributions from: - -Florian Idelberger <flo@terrorpop.de> -Apalala <apalala@gmail.com> - -// 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 = "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 = "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. -""" 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 09fe26159195..000000000000 --- a/tpn/tpn/__main__.py +++ /dev/null @@ -1,167 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -"""Third-party notices generation. - -Usage: tpn [--npm=<package-lock.json>] [--npm-overrides=<webpack-overrides.json>] --config=<TPN.toml> <tpn_path> - -Options: - --npm=<package-lock.json> Path to a package-lock.json for npm. - --npm-overrides=<webpack-overrides.json> Path to a JSON file containing an array of names to override "dev" in <package-lock.json>. - --config=<TPN.toml> Path to the configuration file. - -""" -import asyncio -import json -import os -import pathlib -import re -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) - # Check if a project which is stale by version is actually unneeded. - for stale_project in stale.keys(): - if stale_project in projects: - stale[stale_project].error = config.UnneededEntry(stale_project) - return projects, stale, failures - - -def _fix_toml(text, comments): - lines = text.split(os.linesep) - for i, line in enumerate(lines): - for orig in comments: - if line != orig: - continue - line += comments[orig] - if "\\n" in line: - line = line.replace('\\"', '"') - line = line.replace('"', '"""\n', 1) - line = line[::-1].replace('"', '"""', 1)[::-1] - line = line.replace("\\n", "\n") - lines[i] = line - return os.linesep.join(lines) - - -def _find_trailing_comments(text): - for line in text.splitlines(): - m = re.match(r".*?( +#[^#]*)$", line) - if not m: - continue - line, _, _ = line.rpartition('#') - comment, = m.groups() - yield line.rstrip(), comment - - -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) - if stale: - print("STALE ", end="") - print("*" * 20) - for name, details in stale.items(): - print(details.error) - if failures: - print("FAILURES ", end="") - print("*" * 20) # Make failure stand out more. - for name, details in failures.items(): - print(f"{name!r} {details.version} @ {details.url}: {details.error}") - print(f"NPM URL: {details.npm}") - print(textwrap.dedent(f""" - [[project]] - name = "{name}" - version = "{details.version}" - url = "{details.url}" - purpose = "{details.purpose or "XXX"}" - license = \"\"\" - (TODO) - \"\"\" - """)) - config_data["project"].append({ - 'name': name, - 'version': details.version, - 'url': details.url, - 'purpose': details.purpose or "(TODO)", - 'license': "(TODO)\n", - }) - print() - print(f"Could not find a license for {len(failures)} projects") - print(f"Update {config_path} by filling in the license there for each (look for TODO)") - - comments = dict(_find_trailing_comments( - config_path.read_text(encoding="utf-8"))) - - # Normalize the format and sort. - config_data["project"] = sorted(config_data["project"], key=lambda p: p["name"]) - text = _fix_toml( - toml.dumps(config_data), - comments, - ) - config_path.write_text(text, encoding="utf-8") - - if stale or failures: - sys.exit(1) - else: - 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["<tpn_path>"], - 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 3e93580b3657..000000000000 --- a/tpn/tpn/config.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from __future__ import annotations - -import dataclasses -import enum -from typing import Optional - -from . import data - - -class StaleVersion(Exception): - - """For when the license for a project is a version mismatch.""" - - def __init__(self, project_name, expected, given): - super().__init__( - f"{project_name!r} has a license in the configuration file for {expected} but need {given}" - ) - - -class UnneededEntry(Exception): - def __init__(self, project_name): - super().__init__( - f"{project_name!r} no longer needs to be specified in the configuration file" - ) - - -@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", "<unknown>") - 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 - else: - details.error = StaleVersion(name, config_version, requested_version) - else: - details.error = UnneededEntry(name) - 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 ec49648a70f8..000000000000 --- a/tpn/tpn/data.py +++ /dev/null @@ -1,22 +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 - - @property - def npm(self): - return f"https://www.npmjs.com/package/{self.name}" diff --git a/tpn/tpn/npm.py b/tpn/tpn/npm.py deleted file mode 100644 index dc6305c8ec59..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(connector=aiohttp.TCPConnector(verify_ssl=False)) 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 83991fea6957..000000000000 --- a/tpn/tpn/tests/test_config.py +++ /dev/null @@ -1,97 +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 isinstance(stale["Arch"].error, config.StaleVersion) - 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 isinstance(stale["Arch"].error, config.UnneededEntry) - 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<name>.+?) (?P<version>\S+) NOTICES AND INFORMATION BEGIN HERE \((?P<url>http.+?)\)\n=========================================\n(?P<license>.+?)\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 679a9b3c8564..000000000000 --- a/tsconfig.datascience-ui.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "compilerOptions": { - "baseUrl": ".", - "module": "commonjs", - "target": "es5", - "outDir": "out", - "lib": [ - "es6", - "dom" - ], - "jsx": "react", - "sourceMap": true, - "rootDirs": [ - "node_modules/vsls", - "src", - "types" - ], - "paths": { "*": ["types/*"] }, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "noImplicitThis": false, - "noUnusedLocals": true, - "noUnusedParameters": false, - "strict": true - }, - "exclude": [ - ".vscode-test", - ".vscode test", - "src/test", - "src/server", - "src/client", - "build" - ] -} diff --git a/tsconfig.extension.json b/tsconfig.extension.json index 9409a457a0b6..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, @@ -18,7 +20,6 @@ "node_modules", ".vscode-test", ".vscode test", - "src/datascience-ui", "build" ] } diff --git a/tsconfig.json b/tsconfig.json index d84df6db4064..718d4ab4aad1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,20 @@ { "compilerOptions": { "baseUrl": ".", - "paths": { "*": ["types/*"] }, - "module": "commonjs", + "paths": { + "*": ["types/*"] + }, + "module": "NodeNext", + "moduleResolution": "NodeNext", "target": "es2018", "outDir": "out", "lib": [ "es6", "es2018", - "dom" + "dom", + "ES2019", + "ES2020" ], - "jsx": "react", "sourceMap": true, "rootDir": "src", "experimentalDecorators": true, @@ -20,9 +24,9 @@ "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - // We don't worry about this one: - //"noImplicitReturns": true, - "noFallthroughCasesInSwitch": true + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "removeComments": true }, "exclude": [ "node_modules", @@ -32,6 +36,10 @@ "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 3560f7758468..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, - "no-unused-expression": true, - "no-duplicate-variable": true, - "curly": true, - "non-literal-fs-path": false, - "newline-per-chained-call": false, - "class-name": true, - "semicolon": [ - true - ], - "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/@nteract/transform-dataresource.d.ts b/types/@nteract/transform-dataresource.d.ts deleted file mode 100644 index ac38b46b19d4..000000000000 --- a/types/@nteract/transform-dataresource.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@nteract/transform-dataresource' { - export = index; - const index: any; -} diff --git a/types/@nteract/transform-geojson.d.ts b/types/@nteract/transform-geojson.d.ts deleted file mode 100644 index 6993907366e2..000000000000 --- a/types/@nteract/transform-geojson.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '@nteract/transform-geojson' { - export = index; - const index: any; -} diff --git a/types/@nteract/transform-model-debug.d.ts b/types/@nteract/transform-model-debug.d.ts deleted file mode 100644 index c173dbbd65ab..000000000000 --- a/types/@nteract/transform-model-debug.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module '@nteract/transform-model-debug' { - export default class _default { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(): any; - } -} diff --git a/types/@nteract/transform-plotly.d.ts b/types/@nteract/transform-plotly.d.ts deleted file mode 100644 index d647dcd7775c..000000000000 --- a/types/@nteract/transform-plotly.d.ts +++ /dev/null @@ -1,26 +0,0 @@ -declare module '@nteract/transform-plotly' { - export function PlotlyNullTransform(): any; - export namespace PlotlyNullTransform { - const MIMETYPE: string; - } - export class PlotlyTransform { - static MIMETYPE: string; - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export default class _default { - static MIMETYPE: string; - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } -} diff --git a/types/@nteract/transforms.d.ts b/types/@nteract/transforms.d.ts deleted file mode 100644 index b414442e1a42..000000000000 --- a/types/@nteract/transforms.d.ts +++ /dev/null @@ -1,114 +0,0 @@ -declare module '@nteract/transforms' { - export class GIFTransform { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - } - export class HTMLTransform { - static MIMETYPE: string; - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export class JPEGTransform { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - } - export class JSONTransform { - static MIMETYPE: string; - static defaultProps: { - data: {}; - metadata: {}; - theme: string; - }; - static handles(mimetype: any): any; - constructor(props: any); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - shouldExpandNode(): any; - } - export class JavaScriptTransform { - static MIMETYPE: string; - static handles(mimetype: any): any; - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export function LaTeXTransform(props: any, context: any): any; - export namespace LaTeXTransform { - const MIMETYPE: string; - namespace contextTypes { - function MathJax(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; - namespace MathJax { - function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; - } - function MathJaxContext(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; - namespace MathJaxContext { - function isRequired(p0: any, p1: any, p2: any, p3: any, p4: any, p5: any): any; - } - } - } - export class MarkdownTransform { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export class PNGTransform { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - } - export class SVGTransform { - static MIMETYPE: string; - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export class TextTransform { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export class VDOMTransform { - static MIMETYPE: string; - constructor(...args: any[]); - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - } - export const displayOrder: string[]; - export function registerTransform(_ref: any, transform: any): any; - export function richestMimetype(bundle: any, ...args: any[]): any; - export const standardDisplayOrder: string[]; - - export let standardTransforms: {}; - export namespace transforms { } -} diff --git a/types/ansi-to-html.d.ts b/types/ansi-to-html.d.ts deleted file mode 100644 index c35b0f3c8439..000000000000 --- a/types/ansi-to-html.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -declare module 'ansi-to-html' { - export = ansiToHtml; - class ansiToHtml { - constructor(options?: any); - opts: any; - stack: any; - stickyStack: any; - toHtml(input: any): any; - } -} diff --git a/types/react-data-grid.d.ts b/types/react-data-grid.d.ts deleted file mode 100644 index 9f9c9fe87481..000000000000 --- a/types/react-data-grid.d.ts +++ /dev/null @@ -1,709 +0,0 @@ -// Type definitions for react-data-grid 4.0 -// Project: https://github.com/adazzle/react-data-grid.git -// Definitions by: Simon Gellis <https://github.com/SupernaviX>, Kieran Peat <https://github.com/KieranPeat>, Martin Novak <https://github.com/martinnov92>, Sebastijan Grabar <https://github.com/baso53> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 - -// Copied here so that could fix this to work with an older version of React. - -/// <reference types="react" /> - -declare namespace AdazzleReactDataGrid { - interface ExcelColumn { - editable: boolean; - name: any; - key: string; - width: number; - resizeable: boolean; - filterable: boolean; - } - - interface EditorBaseProps { - value: any; - column: ExcelColumn; - height: number; - onBlur: () => void; - onCommit: () => void; - onCommitCancel: () => void; - rowData: any; - rowMetaData: any; - } - - interface SelectionParams<T> { - rowIdx: number; - row: T; - } - - interface GridProps<T> { - /** - * Gets the data to render in each row. Required. - * Can be an array or a function that takes an index and returns an object. - */ - rowGetter: Array<T> | ((rowIdx: number) => T) - /** - * The total number of rows to render. Required. - */ - rowsCount: number - /** - * The columns to render. - */ - columns?: Array<Column<T>> - - /** - * Invoked when the user changes the value of a single cell. - * Should update that cell's value. - * @param e Information about the event - */ - onRowUpdated?: (e: RowUpdateEvent<T>) => void - /** - * Invoked when the user pulls down the drag handle of an editable cell. - * Should update the values of the selected cells. - * @param e Information about the event - */ - onCellsDragged?: (e: CellDragEvent) => void - /** - * Invoked when the user double clicks on the drag handle of an editable cell. - * Should update the values of the cells beneath the selected cell. - * @param e Information about the event - */ - onDragHandleDoubleClick?: (e: DragHandleDoubleClickEvent<T>) => void - /** - * Invoked when the user copies a value from one cell and pastes it into another (in the same column). - * Should update the value of the cell in row e.toRow. - * @param e Information about the event - */ - onCellCopyPaste?: (e: CellCopyPasteEvent) => void - /** - * Invoked after the user updates the grid rows in any way. - * @param e Information about the event - */ - onGridRowsUpdated?: (e: GridRowsUpdatedEvent<T>) => void - - /** - * A toolbar to display above the grid. - * Consider using the toolbar included in "react-data-grid/addons". - */ - toolbar?: React.ReactElement<any> - /** - * A context menu to disiplay when the user right-clicks a cell. - * Consider using "react-contextmenu", included in "react-data-grid/addons". - */ - contextMenu?: React.ReactElement<any> - /** - * A react component to customize how rows are rendered. - * If you want to define your own, consider extending ReactDataGrid.Row. - */ - rowRenderer?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any> - /** - * A component to display when there are no rows to render. - */ - emptyRowsView?: React.ComponentClass<any> | React.StatelessComponent<any> - - /** - * The minimum width of the entire grid in pixels. - */ - minWidth?: number - /** - * The minimum height of the entire grid in pixels. - * @default 350 - */ - minHeight?: number - /** - * The height of each individual row in pixels. - * @default 35 - */ - rowHeight?: number - /** - * The height of the header row in pixels. - * @default rowHeight - */ - headerRowHeight?: number - /** - * The height of the header filter row in pixels. - * @default 45 - */ - headerFiltersHeight?: number - /** - * The minimum width of each column in pixels. - * @default 80 - */ - minColumnWidth?: number - /** - * Invoked when a column has been resized. - * @param index The index of the column - * @param width The new width of the column - */ - onColumnResize?: (index: number, width: number) => void - - /** - * Controls what happens when the user navigates beyond the first or last cells. - * 'loopOverRow' will navigate to the beginning/end of the current row. - * 'changeRow' will navigate to the beginning of the next row or the end of the last. - * 'none' will do nothing. - * @default none - */ - cellNavigationMode?: 'none' | 'loopOverRow' | 'changeRow' - - /** - * Called when the user sorts the grid by some column. - * Should update the order of the rows returned by rowGetter. - * @param sortColumn The name of the column being sorted by - * @param sortDirection The direction to sort ('ASC'/'DESC'/'NONE') - */ - onGridSort?: (sortColumn: string, sortDirection: 'ASC' | 'DESC' | 'NONE') => void - - /** - * Initial sorting direction - */ - sortDirection?: 'ASC' | 'DESC' | 'NONE' - - /** - * key of the initial sorted column - */ - sortColumn?: string - - /** - * Called when the user filters a column by some value. - * Should restrict the rows in rowGetter to only things that match the filter. - * @param filter The filter being added - */ - onAddFilter?: (filter: Filter) => void - /** - * Called when the user clears all filters. - * Should restore the rows in rowGetter to their original state. - */ - onClearFilters?: () => void - - /** - * When set to true or 'multi', enables multiple row select. - * When set to 'single', enables single row select. - * When set to false or not set, disables row select. - * @default false - */ - enableRowSelect?: boolean | 'single' | 'multi' - /** - * Called when a row is selected. - * @param rows The (complete) current selection of rows. - */ - onRowSelect?: (rows: Array<T>) => void - /** - * A property that's unique to every row. - * This property is required to enable row selection. - * @default 'id' - */ - rowKey?: string - - /** - * Enables cells to be selected when clicked. - * @default false - */ - enableCellSelect?: boolean - - /** - * Enables cells to be dragged and dropped - * @default false - */ - enableDragAndDrop?: boolean - - /** - * Called when a cell is selected. - * @param coordinates The row and column indices of the selected cell. - */ - onCellSelected?: (coordinates: {rowIdx: number, idx: number}) => void - /** - * Called when a cell is deselected. - * @param coordinates The row and column indices of the deselected cell. - */ - onCellDeSelected?: (coordinates: {rowIdx: number, idx: number}) => void - - /** - * How long to wait before rendering a new row while scrolling in milliseconds. - * @default 0 - */ - rowScrollTimeout?: number - /** - * Options object for selecting rows - */ - rowSelection?: { - showCheckbox?: boolean - enableShiftSelect?: boolean - onRowsSelected?: (rows: Array<SelectionParams<T>>) => void, - onRowsDeselected?: (rows: Array<SelectionParams<T>>) => void, - selectBy?: { - indexes?: Array<number>; - keys?: { rowKey: string, values: Array<any> }; - isSelectedKey?: string; - } - } - /** - * A custom formatter for the select all checkbox cell - * @default react-data-grid/src/formatters/SelectAll.js - */ - selectAllRenderer?: React.ComponentClass<any> | React.StatelessComponent<any>; - /** - * A custom formatter for select row column - * @default AdazzleReactDataGridPlugins.Editors.CheckboxEditor - */ - rowActionsCell?: React.ComponentClass<any> | React.StatelessComponent<any>; - /** - * An event function called when a row is clicked. - * Clicking the header row will trigger a call with -1 for the rowIdx. - * @param rowIdx zero index number of row clicked - * @param row object behind the row - */ - onRowClick?: (rowIdx: number, row: T) => void - onRowDoubleClick?: (rowIdx: number, row: T) => void - - /** - * An event function called when a row is expanded with the toggle - * @param props OnRowExpandToggle object - */ - onRowExpandToggle?: (props: OnRowExpandToggle ) => void - - /** - * Responsible for returning an Array of values that can be used for filtering - * a column that is column.filterable and using a column.filterRenderer that - * displays a list of options. - * @param columnKey the column key that we are looking to pull values from - */ - getValidFilterValues?: (columnKey: string) => Array<any> - - getCellActions?: (column: Column<T>, row: T) => (ActionButton | ActionMenu)[] - } - - type ActionButton = { - icon: string; - callback: () => void; - } - - type ActionMenu = { - icon: string; - actions: { - icon: string; - text: string; - callback: () => void; - }[]; - } - - /** - * Information about a specific column to be rendered. - */ - interface Column<T> { - /** - * A unique key for this column. Required. - * Each row should have a property with this name, which contains this column's value. - */ - key: string - /** - * This column's display name. Required. - */ - name: string - /** - * A custom width for this specific column. - * @default minColumnWidth from the ReactDataGrid - */ - width?: number - /** - * Whether this column can be resized by the user. - * @default false - */ - resizable?: boolean - /** - * Whether this column should stay fixed on the left as the user scrolls horizontally. - * @default false - */ - locked?: boolean - /** - * Whether this column can be edited. - * @default false - */ - editable?: boolean - /** - * Whether the rows in the grid can be sorted by this column. - * @default false - */ - sortable?: boolean - /** - * Whether the rows in the grid can be filtered by this column. - * @default false - */ - filterable?: boolean; - /** - * A custom formatter for this column's filter. - */ - filterRenderer?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any>; - /** - * The editor for this column. Several editors are available in "react-data-grid/addons". - * @default A simple text editor - */ - editor?: - | React.ReactElement<EditorBaseProps> - | React.ComponentClass<EditorBaseProps> - | React.StatelessComponent<EditorBaseProps>; - /** - * A custom read-only formatter for this column. An image formatter is available in "react-data-grid/addons". - */ - formatter?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any> - /** - * A custom formatter for this column's header. - */ - headerRenderer?: React.ReactElement<any> | React.ComponentClass<any> | React.StatelessComponent<any> - /** - * Events to be bound to the cells in this specific column. - * Each event must respect this standard in order to work correctly: - * @example - * function onXxx(ev :SyntheticEvent, (rowIdx, idx, name): args) - */ - events?: { - [name: string]: ColumnEventCallback - }; - /** - * Retrieve meta data about the row, optionally provide column as a second argument - */ - getRowMetaData?: (rowdata: T, column?: Column<T>) => any; - /** - * A class name to be applied to the cells in the column - */ - cellClass?: string; - /** - * Whether this column can be dragged (re-arranged). - * @default false - */ - draggable?: boolean; - } - - interface ColumnEventCallback { - /** - * A callback for a native react event on a specific cell. - * @param ev The react event - * @param args The row and column coordinates of the cell, and the name of the event. - */ - (ev: React.SyntheticEvent<any>, args: {rowIdx: number, idx: number, name: string}): void - } - - /** - * Information about a row update. Generic event type returns untyped row, use parameterized type with the row type as the parameter - * @default T = any - */ - interface RowUpdateEvent<T = any> { - /** - * The index of the updated row. - */ - rowIdx: number - /** - * The columns that were updated and their values. - */ - updated: T - /** - * The name of the column that was updated. - */ - cellKey: string - /** - * The name of the key pressed to trigger the event ('Tab', 'Enter', etc.). - */ - key: string - } - - /** - * Information about a cell drag - */ - interface CellDragEvent { - /** - * The name of the column that was dragged. - */ - cellKey: string - /** - * The row where the drag began. - */ - fromRow: number - /** - * The row where the drag ended. - */ - toRow: number - /** - * The value of the cell that was dragged. - */ - value: any - } - - /** - * Information about a drag handle double click. Generic event type returns untyped row, use parameterized type with the row type as the parameter - * @default T = any - */ - interface DragHandleDoubleClickEvent<T = any> { - /** - * The row where the double click occurred. - */ - rowIdx: number - /** - * The column where the double click occurred. - */ - idx: number - /** - * The values of the row. - */ - rowData: T - /** - * The double click event. - */ - e: React.SyntheticEvent<any> - } - - /** - * Information about a copy paste - */ - interface CellCopyPasteEvent { - /** - * The row that was pasted to. - */ - rowIdx: number - /** - * The value that was pasted. - */ - value: any - /** - * The row that was copied from. - */ - fromRow: number - /** - * The row that was pasted to. - */ - toRow: number - /** - * The key of the column where the copy paste occurred. - */ - cellKey: string - } - - /** - * Information about some update to the grid's contents. Generic event type returns untyped row, use parameterized type with the row type as the parameter - * @default T = any - */ - interface GridRowsUpdatedEvent<T = any> { - /** - * The key of the column where the event occurred. - */ - cellKey: string - /** - * The top row affected by the event. - */ - fromRow: number - /** - * The bottom row affected by the event. - */ - toRow: number - /** - * The columns that were updated and their values. - */ - updated: T - /** - * The action that occurred to trigger this event. - * One of 'cellUpdate', 'cellDrag', 'columnFill', or 'copyPaste'. - */ - action: 'cellUpdate' | 'cellDrag' | 'columnFill' | 'copyPaste' - } - - /** - * Information about the row toggler - */ - interface OnRowExpandToggle { - /** - * The name of the column group the row is in - */ - columnGroupName: string - /** - * The name of the expanded row - */ - name: string - /** - * If it should expand or not - */ - shouldExpand: boolean - } - - /** - * Some filter to be applied to the grid's contents - */ - interface Filter { - /** - * The key of the column being filtered. - */ - columnKey: string - /** - * The term to filter by. - */ - filterTerm: string - } - - /** - * Excel-like grid component built with React, with editors, keyboard navigation, copy & paste, and the like - * http://adazzle.github.io/react-data-grid/ - */ - export class ReactDataGrid<T> extends React.Component<GridProps<T>> { - /** - * Opens the editor for the cell (idx) in the given row (rowIdx). If the column is not editable then nothing will happen. - */ - openCellEditor(rowIdx: number, idx: number): void; - } - export namespace ReactDataGrid { - // Useful types - export import Column = AdazzleReactDataGrid.Column; - export import Filter = AdazzleReactDataGrid.Filter; - - // Various events - export import RowUpdateEvent = AdazzleReactDataGrid.RowUpdateEvent; - export import SelectionParams = AdazzleReactDataGrid.SelectionParams; - export import CellDragEvent = AdazzleReactDataGrid.CellDragEvent; - export import DragHandleDoubleClickEvent = AdazzleReactDataGrid.DragHandleDoubleClickEvent; - export import CellCopyPasteEvent = AdazzleReactDataGrid.CellCopyPasteEvent; - export import GridRowsUpdatedEvent = AdazzleReactDataGrid.GridRowsUpdatedEvent; - export import OnRowExpandToggle = AdazzleReactDataGrid.OnRowExpandToggle; - - export namespace editors { - class EditorBase<P = {}, S = {}> extends React.Component<P & EditorBaseProps, S> { - getStyle(): { width: string }; - - getValue(): any; - - getInputNode(): Element | null | Text; - - inheritContainerStyles(): boolean; - } - } - - // Actual classes exposed on module.exports - /** - * A react component that renders a row of the grid - */ - export class Row extends React.Component<any> { } - /** - * A react coponent that renders a cell of the grid - */ - export class Cell extends React.Component<any> { } - } - } - - declare namespace AdazzleReactDataGridPlugins { - interface AutoCompleteEditorProps { - onCommit?: () => void; - options?: Array<{ id: any; title: string }>; - label?: any; - value?: any; - height?: number; - valueParams?: string[]; - column?: AdazzleReactDataGrid.ExcelColumn; - resultIdentifier?: string; - search?: string; - onKeyDown?: () => void; - onFocus?: () => void; - editorDisplayValue?: (column: AdazzleReactDataGrid.ExcelColumn, value: any) => string; - } - - interface AutoCompleteTokensEditorProps { - options: Array<string | { id: number; caption: string }>; - column?: AdazzleReactDataGrid.ExcelColumn; - value?: any[]; - } - - interface DropDownEditorProps { - options: - Array<string | { - id: string; - title: string; - value: string; - text: string; - }>; - } - - export namespace Editors { - export class AutoComplete extends React.Component<AutoCompleteEditorProps> {} - export class AutoCompleteTokensEditor extends React.Component<AutoCompleteTokensEditorProps> {} - export class DropDownEditor extends React.Component<DropDownEditorProps> {} - - // TODO: refine types for these addons - export class SimpleTextEditor extends React.Component<any> {} - export class CheckboxEditor extends React.Component<any> {} - } - export namespace Filters { - export class NumericFilter extends React.Component<any> { } - export class AutoCompleteFilter extends React.Component<any> { } - export class MultiSelectFilter extends React.Component<any> { } - export class SingleSelectFilter extends React.Component<any> { } - } - export namespace Formatters { - export class ImageFormatter extends React.Component<any> { } - export class DropDownFormatter extends React.Component<any> { } - } - export class Toolbar extends React.Component<any> {} - export namespace DraggableHeader { - export class DraggableContainer extends React.Component<any>{ } - } - export namespace Data { - export const Selectors: { - getRows: (state: object) => object[]; - getSelectedRowsByKey: (state: object) => object[]; - } - } - // TODO: re-export the react-contextmenu typings once those exist - // https://github.com/vkbansal/react-contextmenu/issues/10 - export namespace Menu { - export class ContextMenu extends React.Component<any> { } - export class MenuHeader extends React.Component<any> { } - export class MenuItem extends React.Component<any> { } - export class SubMenu extends React.Component<any> { } - export const monitor: { - getItem(): any - getPosition(): any - hideMenu(): void - }; - export function connect(Menu: any): any; - export function ContextMenuLayer( - identifier: any, - configure?: (props: any) => any - ): (Component: any) => any - } - } - - declare module "react-data-grid" { - import ReactDataGrid = AdazzleReactDataGrid.ReactDataGrid; - - // commonjs export - export = ReactDataGrid; - } - - declare module "react-data-grid-addons" { - import Plugins = AdazzleReactDataGridPlugins; - import Editors = Plugins.Editors; - import Filters = Plugins.Filters; - import Formatters = Plugins.Formatters; - import Toolbar = Plugins.Toolbar; - import Menu = Plugins.Menu; - import Data = Plugins.Data; - import DraggableHeader = Plugins.DraggableHeader; - - // ES6 named exports - export { - Editors, - Filters, - Formatters, - Toolbar, - Menu, - Data, - DraggableHeader - } - - // attach to window - global { - interface Window { - ReactDataGridPlugins: { - Editors: typeof Editors, - Filters: typeof Filters, - Formatters: typeof Formatters, - Toolbar: typeof Toolbar, - Menu: typeof Menu, - Data: typeof Data, - DraggableHeader: typeof DraggableHeader - } - } - } - } diff --git a/types/react-svg-pan-zoom.d.ts b/types/react-svg-pan-zoom.d.ts deleted file mode 100644 index 7d65bb5b3027..000000000000 --- a/types/react-svg-pan-zoom.d.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Type definitions for react-svg-pan-zoom 2.5 -// Project: https://github.com/chrvadala/react-svg-pan-zoom#readme, https://chrvadala.github.io/react-svg-pan-zoom -// Definitions by: Huy Nguyen <https://github.com/huy-nguyen> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 - -// Copied here so could add UncontrolledReactSVGPanZoom -declare module 'react-svg-pan-zoom'; - -/// <reference types="react" /> - -import * as React from 'react'; - -// String constants: -export const MODE_IDLE = 'idle'; -export const MODE_PANNING = 'panning'; -export const MODE_ZOOMING = 'zooming'; - -export const TOOL_AUTO = 'auto'; -export const TOOL_NONE = 'none'; -export const TOOL_PAN = 'pan'; -export const TOOL_ZOOM_IN = 'zoom-in'; -export const TOOL_ZOOM_OUT = 'zoom-out'; - -export const POSITION_NONE = 'none'; -export const POSITION_TOP = 'top'; -export const POSITION_RIGHT = 'right'; -export const POSITION_BOTTOM = 'bottom'; -export const POSITION_LEFT = 'left'; - -export type Mode = typeof MODE_IDLE | typeof MODE_PANNING | typeof MODE_ZOOMING; - -export interface Value { - version: 2; - mode: Mode; - focus: boolean; - a: number; - b: number; - c: number; - d: number; - e: number; - f: number; - viewerWidth: number; - viewerHeight: number; - SVGWidth: number; - SVGHeight: number; - startX?: number | null; - startY?: number | null; - endX?: number | null; - endY?: number | null; -} - -export type Tool = typeof TOOL_AUTO | typeof TOOL_NONE | typeof TOOL_PAN | - typeof TOOL_ZOOM_IN | typeof TOOL_ZOOM_OUT; -export type ToolbarPosition = typeof POSITION_NONE | typeof POSITION_TOP | typeof POSITION_RIGHT | - typeof POSITION_BOTTOM | typeof POSITION_LEFT; - -export interface OptionalProps { - // background of the viewer - background: string; - - // background of the svg - SVGBackground: string; - - // value of the viewer (current point of view) - value: Value | null; - - // default value of the viewer - defaultValue?: Value; - - // default tool to start with - defaultTool?: Tool; - - // CSS style of the Viewer - style: object; - - // className of the Viewer - className: string; - - // detect zoom operation performed trough pinch gesture or mouse scroll - detectWheel: boolean; - - // perform PAN if the mouse is on viewer border - detectAutoPan: boolean; - - // toolbar props - toolbarProps: { position: ToolbarPosition }; - - // handler something changed - onChangeValue(value: Value): void; - - // handler tool changed - onChangeTool(tool: Tool): void; - - // Note: The `T` type parameter is the type of the `target` of the event: - // handler click - onClick<T>(event: ViewerMouseEvent<T>): void; - - // handler double click - onDoubleClick<T>(event: ViewerMouseEvent<T>): void; - - // handler mouseup - onMouseUp<T>(event: ViewerMouseEvent<T>): void; - - // handler mousemove - onMouseMove<T>(event: ViewerMouseEvent<T>): void; - - // handler mousedown - onMouseDown<T>(event: ViewerMouseEvent<T>): void; - - // if disabled the user can move the image outside the viewer - preventPanOutside: boolean; - - // how much scale in or out - scaleFactor: number; - - // current active tool (TOOL_NONE, TOOL_PAN, TOOL_ZOOM_IN, TOOL_ZOOM_OUT) - tool: Tool; - - // modifier keys //https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState - modifierKeys: string[]; - - // override default toolbar component - // TODO: specify function type more clearly - customToolbar: React.Component<any> | React.StatelessComponent<any>; - customMiniature: React.Component<any> | React.StatelessComponent<any>; - - // How about touch events? They are in README but not in `propTypes`. -} - -export interface RequiredProps { - // width of the viewer displayed on screen - width: number; - // height of the viewer displayed on screen - height: number; - - // accept only one node SVG - // TODO: Figure out how to constrain `children` or maybe just leave it commented out - // because `children` is already implicit props - // children: () => any; -} - -export type Props = RequiredProps & Partial<OptionalProps>; - -export class ReactSVGPanZoom extends React.Component<Props> { - pan(SVGDeltaX: number, SVGDeltaY: number): void; - zoom(SVGPointX: number, SVGPointY: number, scaleFactor: number): void; - fitSelection(selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): void; - fitToViewer(): void; - setPointOnViewerCenter(SVGPointX: number, SVGPointY: number, zoomLevel: number): void; - reset(): void; - zoomOnViewerCenter(scaleFactor: number): void; - getValue(): Value; - setValue(value: Value): void; - getTool(): Tool; - setTool(tool: Tool): void; -} - -export class UncontrolledReactSVGPanZoom extends React.Component<Props> { - pan(SVGDeltaX: number, SVGDeltaY: number): void; - zoom(SVGPointX: number, SVGPointY: number, scaleFactor: number): void; - fitSelection(selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): void; - fitToViewer(): void; - setPointOnViewerCenter(SVGPointX: number, SVGPointY: number, zoomLevel: number): void; - reset(): void; - zoomOnViewerCenter(scaleFactor: number): void; - changeValue(value: Value): void; - changeTool(tool: Tool): void; -} - -export interface Point { - x: number; - y: number; -} - -export interface ViewerMouseEvent<T> { - originalEvent: React.MouseEvent<T>; - SVGViewer: SVGSVGElement; - point: Point; - x: number; - y: number; - scaleFactor: number; - translationX: number; - translationY: number; - preventDefault(): void; - stopPropagation(): void; -} - -export interface ViewerTouchEvent<T> { - originalEvent: React.TouchEvent<T>; - SVGViewer: SVGSVGElement; - points: Point[]; - changedPoints: Point[]; - scaleFactor: number; - translationX: number; - translationY: number; - preventDefault(): void; - stopPropagation(): void; -} - -// Utility functions exposed: -export function pan(value: Value, SVGDeltaX: number, SVGDeltaY: number, panLimit?: number): Value; - -export function zoom(value: Value, SVGPointX: number, SVGPointY: number, scaleFactor: number): Value; - -export function fitSelection( - value: Value, selectionSVGPointX: number, selectionSVGPointY: number, selectionWidth: number, selectionHeight: number): Value; - -export function fitToViewer(value: Value): Value; - -export function zoomOnViewerCenter(value: Value, scaleFactor: number): Value; - -export function setPointOnViewerCenter(value: Value, SVGPointX: number, SVGPointY: number, zoomLevel: number): Value; - -export function reset(value: Value): Value; diff --git a/types/react-svgmt.d.ts b/types/react-svgmt.d.ts deleted file mode 100644 index 4cee2e8d6dfa..000000000000 --- a/types/react-svgmt.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -declare module 'react-svgmt' { - export class SvgLoader { - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - props: any; - state: any; - context: any; - refs: any; - } - export class SvgProxy { - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - props: any; - state: any; - context: any; - refs: any; - } -} \ No newline at end of file diff --git a/types/react-tabulator.d.ts b/types/react-tabulator.d.ts deleted file mode 100644 index a0a0c35a36a0..000000000000 --- a/types/react-tabulator.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -declare module 'react-tabulator' { - export class React15Tabulator { - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - props: any; - state: any; - context: any; - refs: any; - } - export default class _default { - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - props: any; - state: any; - context: any; - refs: any; - } -} \ No newline at end of file diff --git a/types/slickgrid/plugins/slick.autotooltips.d.ts b/types/slickgrid/plugins/slick.autotooltips.d.ts deleted file mode 100644 index 2c33e524311a..000000000000 --- a/types/slickgrid/plugins/slick.autotooltips.d.ts +++ /dev/null @@ -1,35 +0,0 @@ -// Type definitions for SlickGrid AutoToolTips Plugin 2.1.0 -// Project: https://github.com/mleibman/SlickGrid -// Definitions by: Ryo Iwamoto <https://github.com/ryiwamoto> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - - - -declare namespace Slick { - export interface SlickGridAutoTooltipsOption extends PluginOptions { - /** - * Enable tooltip for grid cells - * @default true - */ - enableForCells?: boolean; - - /** - * Enable tooltip for header cells - * @default false - */ - enableForHeaderCells?: boolean; - - /** - * The maximum length for a tooltip - * @default null - */ - maxToolTipLength?: number; - } - - /** - * AutoTooltips plugin to show/hide tooltips when columns are too narrow to fit content. - */ - export class AutoTooltips extends Plugin<Slick.SlickData> { - constructor(option?: SlickGridAutoTooltipsOption); - } -} diff --git a/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts b/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts deleted file mode 100644 index ab97e4612389..000000000000 --- a/types/slickgrid/plugins/slick.checkboxselectcolumn.d.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Type definitions for SlickGrid CheckboxSelectColumn Plugin 2.1.0 -// Project: https://github.com/mleibman/SlickGrid -// Definitions by: berwyn <https://github.com/berwyn> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -declare namespace Slick { - export interface SlickGridCheckBoxSelectColumnOptions extends PluginOptions { - /** - * Column to add the checkbox to - * @default "_checkbox_selector" - */ - columnId?: string; - - /** - * CSS class to be added to cells in this column - * @default null - */ - cssClass?: string; - - /** - * Tooltip text to display for this column - * @default "Select/Deselect All" - */ - toolTip?: string; - - /** - * Width of the column - * @default 30 - */ - width?: number; - } - - export class CheckboxSelectColumn<T extends Slick.SlickData> extends Plugin<T> { - constructor(options?: SlickGridCheckBoxSelectColumnOptions); - init(grid: Slick.Grid<T>): void; - destroy(): void; - getColumnDefinition(): Slick.ColumnMetadata<T>; - } -} diff --git a/types/slickgrid/plugins/slick.columnpicker.d.ts b/types/slickgrid/plugins/slick.columnpicker.d.ts deleted file mode 100644 index e0a0269282de..000000000000 --- a/types/slickgrid/plugins/slick.columnpicker.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -// Type definitions for SlickGrid ColumnPicker Control 2.1.0 -// Project: https://github.com/mleibman/SlickGrid -// Definitions by: berwyn <https://github.com/berwyn> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - -declare namespace Slick { - export namespace Controls { - export interface SlickColumnPickerOptions { - fadeSpeed?: number; - } - - export class ColumnPicker<T extends Slick.SlickData> { - constructor(columns: Slick.Column<T>[], grid: Slick.Grid<T>, options: SlickColumnPickerOptions); - getAllColumns(): Slick.Column<T>[]; - destroy(): void; - } - } -} diff --git a/types/slickgrid/plugins/slick.headerbuttons.d.ts b/types/slickgrid/plugins/slick.headerbuttons.d.ts deleted file mode 100644 index a4191a79f36a..000000000000 --- a/types/slickgrid/plugins/slick.headerbuttons.d.ts +++ /dev/null @@ -1,41 +0,0 @@ -// Type definitions for SlickGrid HeaderButtons Plugin 2.1.0 -// Project: https://github.com/mleibman/SlickGrid -// Definitions by: Derek Cicerone <https://github.com/derekcicerone/> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - - - -declare namespace Slick { - - export interface Column<T extends SlickData> { - header?: Header; - } - - export interface Header { - buttons: HeaderButton[]; - } - - export interface HeaderButton { - command?: string; - cssClass?: string; - handler?: Function; - image?: string; - showOnHover?: boolean; - tooltip?: string; - } - - export interface OnCommandEventArgs<T extends SlickData> { - grid: Grid<T>; - column: Column<T>; - command: string; - button: HeaderButton; - } - - export module Plugins { - - export class HeaderButtons<T extends SlickData> extends Plugin<T> { - constructor(); - public onCommand: Event<OnCommandEventArgs<T>>; - } - } -} diff --git a/types/slickgrid/plugins/slick.rowselectionmodel.d.ts b/types/slickgrid/plugins/slick.rowselectionmodel.d.ts deleted file mode 100644 index 2c06f6359133..000000000000 --- a/types/slickgrid/plugins/slick.rowselectionmodel.d.ts +++ /dev/null @@ -1,20 +0,0 @@ -// Type definitions for SlickGrid RowSelectionModel Plugin 2.1.0 -// Project: https://github.com/mleibman/SlickGrid -// Definitions by: Derek Cicerone <https://github.com/derekcicerone/> -// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped - - - -declare namespace Slick { - class RowSelectionModel<T extends SlickData, E> extends SelectionModel<T, E> { - constructor(options?:{selectActiveRow:boolean;}); - - getSelectedRows():number[]; - - setSelectedRows(rows:number[]):void; - - getSelectedRanges():number[]; - - setSelectedRanges(ranges:number[]):void; - } -} diff --git a/types/svg-inline-react.d.ts b/types/svg-inline-react.d.ts deleted file mode 100644 index 46e748682709..000000000000 --- a/types/svg-inline-react.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -declare module 'svg-inline-react' { - export class InlineSVG { - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - props: any; - state: any; - context: any; - refs: any; - } - export default class _default { - constructor(...args: any[]); - componentDidMount(): void; - componentDidUpdate(): void; - forceUpdate(callback: any): void; - render(): any; - setState(partialState: any, callback: any): void; - shouldComponentUpdate(nextProps: any): any; - props: any; - state: any; - context: any; - refs: any; - } -} diff --git a/types/vscode.proposed.envCollectionOptions.d.ts b/types/vscode.proposed.envCollectionOptions.d.ts new file mode 100644 index 000000000000..d25a92725a4d --- /dev/null +++ b/types/vscode.proposed.envCollectionOptions.d.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/179476 + + /** + * Options applied to the mutator. + */ + export interface EnvironmentVariableMutatorOptions { + /** + * Apply to the environment just before the process is created. + * + * Defaults to true. + */ + applyAtProcessCreation?: boolean; + + /** + * Apply to the environment in the shell integration script. Note that this _will not_ apply + * the mutator if shell integration is disabled or not working for some reason. + * + * Defaults to false. + */ + applyAtShellIntegration?: boolean; + } + + /** + * A type of mutation and its value to be applied to an environment variable. + */ + export interface EnvironmentVariableMutator { + /** + * Options applied to the mutator. + */ + readonly options: EnvironmentVariableMutatorOptions; + } + + export interface EnvironmentVariableCollection extends Iterable<[variable: string, mutator: EnvironmentVariableMutator]> { + /** + * @param options Options applied to the mutator. + */ + replace(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + append(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + + /** + * @param options Options applied to the mutator. + */ + prepend(variable: string, value: string, options?: EnvironmentVariableMutatorOptions): void; + } +} diff --git a/types/vscode.proposed.envCollectionWorkspace.d.ts b/types/vscode.proposed.envCollectionWorkspace.d.ts new file mode 100644 index 000000000000..a03a639b5ee2 --- /dev/null +++ b/types/vscode.proposed.envCollectionWorkspace.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/171173 + + // export interface ExtensionContext { + // /** + // * Gets the extension's global environment variable collection for this workspace, enabling changes to be + // * applied to terminal environment variables. + // */ + // readonly environmentVariableCollection: GlobalEnvironmentVariableCollection; + // } + + export interface GlobalEnvironmentVariableCollection extends EnvironmentVariableCollection { + /** + * Gets scope-specific environment variable collection for the extension. This enables alterations to + * terminal environment variables solely within the designated scope, and is applied in addition to (and + * after) the global collection. + * + * Each object obtained through this method is isolated and does not impact objects for other scopes, + * including the global collection. + * + * @param scope The scope to which the environment variable collection applies to. + */ + getScoped(scope: EnvironmentVariableScope): EnvironmentVariableCollection; + } +} diff --git a/types/vscode.proposed.notebookReplDocument.d.ts b/types/vscode.proposed.notebookReplDocument.d.ts new file mode 100644 index 000000000000..d78450e944a8 --- /dev/null +++ b/types/vscode.proposed.notebookReplDocument.d.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface NotebookDocumentShowOptions { + /** + * The notebook should be opened in a REPL editor, + * where the last cell of the notebook is an input box and the other cells are the read-only history. + * When the value is a string, it will be used as the label for the editor tab. + */ + readonly asRepl?: boolean | string | { + /** + * The label to be used for the editor tab. + */ + readonly label: string; + }; + } + + export interface NotebookEditor { + /** + * Information about the REPL editor if the notebook was opened as a repl. + */ + replOptions?: { + /** + * The index where new cells should be appended. + */ + appendIndex: number; + }; + } +} diff --git a/types/vscode.proposed.notebookVariableProvider.d.ts b/types/vscode.proposed.notebookVariableProvider.d.ts new file mode 100644 index 000000000000..4fac96c45f0a --- /dev/null +++ b/types/vscode.proposed.notebookVariableProvider.d.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +declare module 'vscode' { + + export interface NotebookController { + /** Set this to attach a variable provider to this controller. */ + variableProvider?: NotebookVariableProvider; + } + + export enum NotebookVariablesRequestKind { + Named = 1, + Indexed = 2 + } + + interface VariablesResult { + variable: Variable; + hasNamedChildren: boolean; + indexedChildrenCount: number; + } + + interface NotebookVariableProvider { + onDidChangeVariables: Event<NotebookDocument>; + + /** When parent is undefined, this is requesting global Variables. When a variable is passed, it's requesting child props of that Variable. */ + provideVariables(notebook: NotebookDocument, parent: Variable | undefined, kind: NotebookVariablesRequestKind, start: number, token: CancellationToken): AsyncIterable<VariablesResult>; + } + + interface Variable { + /** The variable's name. */ + name: string; + + /** The variable's value. + This can be a multi-line text, e.g. for a function the body of a function. + For structured variables (which do not have a simple value), it is recommended to provide a one-line representation of the structured object. + This helps to identify the structured object in the collapsed state when its children are not yet visible. + An empty string can be used if no value should be shown in the UI. + */ + value: string; + + /** The code that represents how the variable would be accessed in the runtime environment */ + expression?: string; + + /** The type of the variable's value */ + type?: string; + + /** The interfaces or contracts that the type satisfies */ + interfaces?: string[]; + + /** The language of the variable's value */ + language?: string; + } + +} diff --git a/types/vscode.proposed.quickPickSortByLabel.d.ts b/types/vscode.proposed.quickPickSortByLabel.d.ts new file mode 100644 index 000000000000..405d67671d78 --- /dev/null +++ b/types/vscode.proposed.quickPickSortByLabel.d.ts @@ -0,0 +1,16 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/73904 + + export interface QuickPick<T extends QuickPickItem> extends QuickInput { + /** + * An optional flag to sort the final results by index of first query match in label. Defaults to true. + */ + sortByLabel: boolean; + } +} diff --git a/types/vscode.proposed.testObserver.d.ts b/types/vscode.proposed.testObserver.d.ts new file mode 100644 index 000000000000..2bdb21d74732 --- /dev/null +++ b/types/vscode.proposed.testObserver.d.ts @@ -0,0 +1,202 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/107467 + + export namespace tests { + /** + * Requests that tests be run by their controller. + * @param run Run options to use. + * @param token Cancellation token for the test run + */ + export function runTests(run: TestRunRequest, token?: CancellationToken): Thenable<void>; + + /** + * Returns an observer that watches and can request tests. + */ + export function createTestObserver(): TestObserver; + /** + * List of test results stored by the editor, sorted in descending + * order by their `completedAt` time. + */ + export const testResults: ReadonlyArray<TestRunResult>; + + /** + * Event that fires when the {@link testResults} array is updated. + */ + export const onDidChangeTestResults: Event<void>; + } + + export interface TestObserver { + /** + * List of tests returned by test provider for files in the workspace. + */ + readonly tests: ReadonlyArray<TestItem>; + + /** + * An event that fires when an existing test in the collection changes, or + * null if a top-level test was added or removed. When fired, the consumer + * should check the test item and all its children for changes. + */ + readonly onDidChangeTest: Event<TestsChangeEvent>; + + /** + * Dispose of the observer, allowing the editor to eventually tell test + * providers that they no longer need to update tests. + */ + dispose(): void; + } + + export interface TestsChangeEvent { + /** + * List of all tests that are newly added. + */ + readonly added: ReadonlyArray<TestItem>; + + /** + * List of existing tests that have updated. + */ + readonly updated: ReadonlyArray<TestItem>; + + /** + * List of existing tests that have been removed. + */ + readonly removed: ReadonlyArray<TestItem>; + } + + /** + * A test item is an item shown in the "test explorer" view. It encompasses + * both a suite and a test, since they have almost or identical capabilities. + */ + export interface TestItem { + /** + * Marks the test as outdated. This can happen as a result of file changes, + * for example. In "auto run" mode, tests that are outdated will be + * automatically rerun after a short delay. Invoking this on a + * test with children will mark the entire subtree as outdated. + * + * Extensions should generally not override this method. + */ + // todo@api still unsure about this + invalidateResults(): void; + } + + + /** + * TestResults can be provided to the editor in {@link tests.publishTestResult}, + * or read from it in {@link tests.testResults}. + * + * The results contain a 'snapshot' of the tests at the point when the test + * run is complete. Therefore, information such as its {@link Range} may be + * out of date. If the test still exists in the workspace, consumers can use + * its `id` to correlate the result instance with the living test. + */ + export interface TestRunResult { + /** + * Unix milliseconds timestamp at which the test run was completed. + */ + readonly completedAt: number; + + /** + * Optional raw output from the test run. + */ + readonly output?: string; + + /** + * List of test results. The items in this array are the items that + * were passed in the {@link tests.runTests} method. + */ + readonly results: ReadonlyArray<Readonly<TestResultSnapshot>>; + } + + /** + * A {@link TestItem}-like interface with an associated result, which appear + * or can be provided in {@link TestResult} interfaces. + */ + export interface TestResultSnapshot { + /** + * Unique identifier that matches that of the associated TestItem. + * This is used to correlate test results and tests in the document with + * those in the workspace (test explorer). + */ + readonly id: string; + + /** + * Parent of this item. + */ + readonly parent?: TestResultSnapshot; + + /** + * URI this TestItem is associated with. May be a file or file. + */ + readonly uri?: Uri; + + /** + * Display name describing the test case. + */ + readonly label: string; + + /** + * Optional description that appears next to the label. + */ + readonly description?: string; + + /** + * Location of the test item in its `uri`. This is only meaningful if the + * `uri` points to a file. + */ + readonly range?: Range; + + /** + * State of the test in each task. In the common case, a test will only + * be executed in a single task and the length of this array will be 1. + */ + readonly taskStates: ReadonlyArray<TestSnapshotTaskState>; + + /** + * Optional list of nested tests for this item. + */ + readonly children: Readonly<TestResultSnapshot>[]; + } + + export interface TestSnapshotTaskState { + /** + * Current result of the test. + */ + readonly state: TestResultState; + + /** + * The number of milliseconds the test took to run. This is set once the + * `state` is `Passed`, `Failed`, or `Errored`. + */ + readonly duration?: number; + + /** + * Associated test run message. Can, for example, contain assertion + * failure information if the test fails. + */ + readonly messages: ReadonlyArray<TestMessage>; + } + + /** + * Possible states of tests in a test run. + */ + export enum TestResultState { + // Test will be run, but is not currently running. + Queued = 1, + // Test is currently running + Running = 2, + // Test run has passed + Passed = 3, + // Test run has failed (on an assertion) + Failed = 4, + // Test run has been skipped + Skipped = 5, + // Test run failed for some other reason (compilation error, timeout, etc) + Errored = 6 + } +} diff --git a/typings/dom.fix.rx.compiler.d.ts b/typings/dom.fix.rx.compiler.d.ts index 64ced3161585..b6779426a7ac 100644 --- a/typings/dom.fix.rx.compiler.d.ts +++ b/typings/dom.fix.rx.compiler.d.ts @@ -6,7 +6,7 @@ * Another solution is to add the 'dom' lib to tsconfig, but that's even worse. * We don't need dom, as the extension does nothing with the dom (dom = HTML entities and the like). */ -// tslint:disable: interface-name + interface EventTarget { } interface NodeList { } interface HTMLCollection { } diff --git a/typings/extensions.d.ts b/typings/extensions.d.ts index 4a423f329d57..f12b718c4b10 100644 --- a/typings/extensions.d.ts +++ b/typings/extensions.d.ts @@ -2,30 +2,30 @@ // Licensed under the MIT License. /** -* @typedef {Object} SplitLinesOptions -* @property {boolean} [trim=true] - Whether to trim the lines. -* @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. -*/ + * @typedef {Object} SplitLinesOptions + * @property {boolean} [trim=true] - Whether to trim the lines. + * @property {boolean} [removeEmptyEntries=true] - Whether to remove empty entries. + */ // https://stackoverflow.com/questions/39877156/how-to-extend-string-prototype-and-use-it-next-in-typescript -// tslint:disable-next-line:interface-name + declare interface String { /** * Split a string using the cr and lf characters and return them as an array. * By default lines are trimmed and empty lines are removed. * @param {SplitLinesOptions=} splitOptions - Options used for splitting the string. */ - splitLines(splitOptions?: { trim: boolean, removeEmptyEntries?: boolean }): string[]; + splitLines(splitOptions?: { trim: boolean; removeEmptyEntries?: boolean }): string[]; /** * Appropriately formats a string so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ - toCommandArgument(): string; + toCommandArgumentForPythonExt(): string; /** * Appropriately formats a a file path so it can be used as an argument for a command in a shell. * E.g. if an argument contains a space, then it will be enclosed within double quotes. */ - fileToCommandArgument(): string; + fileToCommandArgumentForPythonExt(): string; /** * String.format() implementation. * Tokens such as {0}, {1} will be replaced with corresponding positional arguments. @@ -38,10 +38,9 @@ declare interface String { trimQuotes(): string; } -// tslint:disable-next-line:interface-name declare interface Promise<T> { /** * Catches task errors and ignores them. */ - ignoreErrors(): void; + ignoreErrors(): Promise<void>; } diff --git a/typings/index.d.ts b/typings/index.d.ts index 9ecb31003376..7003ea5043b7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,111 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -declare module '@phosphor/coreutils' { - /** - * A type alias for a JSON primitive. - */ - export type JSONPrimitive = boolean | number | string | null | undefined; - /** - * A type alias for a JSON value. - */ - export type JSONValue = JSONPrimitive | JSONObject | JSONArray; - /** - * A type definition for a JSON object. - */ - export interface JSONObject { - [key: string]: JSONValue; - } - /** - * A type definition for a JSON array. - */ - export interface JSONArray extends Array<JSONValue> {} - /** - * A type definition for a readonly JSON object. - */ - export interface ReadonlyJSONObject { - readonly [key: string]: ReadonlyJSONValue; - } - /** - * A type definition for a readonly JSON array. - */ - export interface ReadonlyJSONArray extends ReadonlyArray<ReadonlyJSONValue> {} - /** - * A type alias for a readonly JSON value. - */ - export type ReadonlyJSONValue = JSONPrimitive | ReadonlyJSONObject | ReadonlyJSONArray; - /** - * The namespace for JSON-specific functions. - */ - export namespace JSONExt { - /** - * A shared frozen empty JSONObject - */ - const emptyObject: ReadonlyJSONObject; - /** - * A shared frozen empty JSONArray - */ - const emptyArray: ReadonlyJSONArray; - /** - * Test whether a JSON value is a primitive. - * - * @param value - The JSON value of interest. - * - * @returns `true` if the value is a primitive,`false` otherwise. - */ - function isPrimitive(value: ReadonlyJSONValue): value is JSONPrimitive; - /** - * Test whether a JSON value is an array. - * - * @param value - The JSON value of interest. - * - * @returns `true` if the value is a an array, `false` otherwise. - */ - function isArray(value: JSONValue): value is JSONArray; - function isArray(value: ReadonlyJSONValue): value is ReadonlyJSONArray; - /** - * Test whether a JSON value is an object. - * - * @param value - The JSON value of interest. - * - * @returns `true` if the value is a an object, `false` otherwise. - */ - function isObject(value: JSONValue): value is JSONObject; - function isObject(value: ReadonlyJSONValue): value is ReadonlyJSONObject; - /** - * Compare two JSON values for deep equality. - * - * @param first - The first JSON value of interest. - * - * @param second - The second JSON value of interest. - * - * @returns `true` if the values are equivalent, `false` otherwise. - */ - function deepEqual(first: ReadonlyJSONValue, second: ReadonlyJSONValue): boolean; - /** - * Create a deep copy of a JSON value. - * - * @param value - The JSON value to copy. - * - * @returns A deep copy of the given JSON value. - */ - function deepCopy<T extends ReadonlyJSONValue>(value: T): T; - } - - export class Token<T> { - /** - * Construct a new token. - * - * @param name - A human readable name for the token. - */ - constructor(name: string); - /** - * The human readable name for the token. - * - * #### Notes - * This can be useful for debugging and logging. - */ - readonly name: string; - private _tokenStructuralPropertyT; - } +// 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<Uri | undefined>; + + /** + * Saves the editor identified by the given resource to a new file name as provided by the user and + * returns the resulting resource or `undefined` if save was not successful or cancelled. + * + * **Note** that an editor with the provided resource must be opened in order to be saved as. + * + * @param uri the associated uri for the opened editor to save as. + * @return A thenable that resolves when the save-as operation has finished. + */ + export function saveAs(uri: Uri): Thenable<Uri | undefined>; + } +} diff --git a/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts new file mode 100644 index 000000000000..6913b862c70f --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalDataWriteEvent.d.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/78502 + // + // This API is still proposed but we don't intent on promoting it to stable due to problems + // around performance. See #145234 for a more likely API to get stabilized. + + export interface TerminalDataWriteEvent { + /** + * The {@link Terminal} for which the data was written. + */ + readonly terminal: Terminal; + /** + * The data being written. + */ + readonly data: string; + } + + namespace window { + /** + * An event which fires when the terminal's child pseudo-device is written to (the shell). + * In other words, this provides access to the raw data stream from the process running + * within the terminal, including VT sequences. + */ + export const onDidWriteTerminalData: Event<TerminalDataWriteEvent>; + } +} diff --git a/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts b/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts new file mode 100644 index 000000000000..7f503f1aa6da --- /dev/null +++ b/typings/vscode-proposed/vscode.proposed.terminalExecuteCommandEvent.d.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + // https://github.com/microsoft/vscode/issues/145234 + + export interface TerminalExecutedCommand { + /** + * The {@link Terminal} the command was executed in. + */ + terminal: Terminal; + /** + * The full command line that was executed, including both the command and the arguments. + */ + commandLine: string | undefined; + /** + * The current working directory that was reported by the shell. This will be a {@link Uri} + * if the string reported by the shell can reliably be mapped to the connected machine. + */ + cwd: Uri | string | undefined; + /** + * The exit code reported by the shell. + */ + exitCode: number | undefined; + /** + * The output of the command when it has finished executing. This is the plain text shown in + * the terminal buffer and does not include raw escape sequences. Depending on the shell + * setup, this may include the command line as part of the output. + */ + output: string | undefined; + } + + export namespace window { + /** + * An event that is emitted when a terminal with shell integration activated has completed + * executing a command. + * + * Note that this event will not fire if the executed command exits the shell, listen to + * {@link onDidCloseTerminal} to handle that case. + */ + export const onDidExecuteTerminalCommand: Event<TerminalExecutedCommand>; + } +} diff --git a/typings/webworker.fix.d.ts b/typings/webworker.fix.d.ts new file mode 100644 index 000000000000..80b53fb5b2e3 --- /dev/null +++ b/typings/webworker.fix.d.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// Fake interfaces that are required for web workers to work around +// tsconfig's DOM and WebWorker lib options being mutally exclusive. +// https://github.com/microsoft/TypeScript/issues/20595 + +interface DedicatedWorkerGlobalScope {} diff --git a/uitests/README.md b/uitests/README.md deleted file mode 100644 index d96e2e899fbe..000000000000 --- a/uitests/README.md +++ /dev/null @@ -1,155 +0,0 @@ -# UI driven BDD Tests for Python Extension. - -## Usage - -Assuming you have created a virtual environment (for Python 3.7), -installed the `uitests/requirements.txt` dependencies, and activated the virtual environment: - -```shell -$ # This step `npm run package` is required to ensure the 'ms-python-insiders.vsix' is available locally. -$ # You could instead just download this and dump into the working directory (much faster). -$ npm run package # see notes above. - - -$ python uitests download -$ python uitests install -$ python uitests test # Use the `-- --tags=@xyz` argument to run specific tests. -$ python uitests --help # for more information. -``` - -## Overview - -- These are a set of UI tests for the Python Extension in VSC. -- The UI is driven using the [selenium webdriver](https://selenium-python.readthedocs.io/). -- [BDD](https://docs.cucumber.io/bdd/overview/) is used to create the tests, and executed using [Behave](https://behave.readthedocs.io/en/latest/). - -## How does it work? - -Here are the steps involved in running the tests: - -* Setup environment: - - Download a completely fresh version of VS Code (`stable/insiders`. Defaults to `stable`). - - Download [ChromeDriver](http://chromedriver.chromium.org/) corresponding to the version of [Electron](https://electronjs.org/) upon which VS Code is built. - - WARNING: When testing against VSC Insiders, it was found that chromedriver for electron 4.2.3 didn't work, and we had to revert to the version used in electron found in stable VSC. - - Currently when testing against VSC insiders, we use the same version of chromedriver used for VSC Stable. (due to a known issue in `ChromeDriver`) - - Use [selenium webdriver](https://selenium-python.readthedocs.io/) to drive the VSC UI. - - Create a folder named `.vsccode test` where test specific files will be created (reports, logs, VS Code, etc). - -* When launching VSC, we will launch it as a completely stand alone version of VSC. - - I.e. even if it is installed on the current machine, we'll download and launch a new instance. - - This new instance will not interfere with currently installed version of VSC. - - All user settings, etc will be in a separate directory (see `user` folder). - - VSC will not have any extensions (see `extensions` folder). -* Automate VSC UI - - Launch VSC using the [ChromeDriver](http://chromedriver.chromium.org/) - - Use [selenium webdriver](https://selenium-python.readthedocs.io/) to drive the VSC UI. - - The [BDD](https://docs.cucumber.io/bdd/overview/) tests are written and executed using [Behave](https://behave.readthedocs.io/en/latest/). -* Workspace folder/files - - Each [feature](https://docs.cucumber.io/gherkin/reference/#feature) can have its own set of files in the form of a github repo. - - Just add a tag with the path of the github repo url to the `feature`. - - When starting the tests for a feature, the repo is downloaded into a new random directory `.vscode test/temp/workspace folder xyz` - - At the begining of every scenario, we repeate the previous step. - - This ensures each scenario starts with a clean workspace folder. -* Reports - - Test results are stored in the `reports` directory - - These `json` (`cucumber format`) report files are converted into HTML using an `npm` script [cucumber-html-reporter](https://www.npmjs.com/package/cucumber-html-reporter). - - For each `scenario` that's executed, we create a corresponding directory in `reports` directory. - - This will contain all screenshots realted to that scenario. - - If the scenario fails, all logs, workspace folder are copied into this directory. - - Thus, when ever a test fails, we have everything related to that test. - - If the scenario passes, this directory is deleted (we don't need them on CI server). - -## Technology - -* 99% of the code is written in `Python`. -* Downloading of `chrome driver` and generating `html reports` is done in `node.js` (using pre-existing `npm` packages). -* The tests are written using [Behave](https://behave.readthedocs.io/en/latest/) in `Python`. -* `GitHub` repos are used to provide the files to be used for testing in a workspace folder. -* The reports (`cucumber format`) are converted into HTML using an `npm` script [cucumber-html-reporter](https://www.npmjs.com/package/cucumber-html-reporter). -* Test result reports are generated using `junit` format, for Azure Devops. - -## Caveats - -* VSC UI needs be a top level window for elements to receive focus. Hence when running tests, try not do anything else. -* For each test we create a whole new folder and open that in VS Code: - - We could use `git reset`, however on Windows, this is flaky if VSC is open. - - Deleting files on `Windows` is flaky due to files being in use, etc. - - Majority of the issues are around `fs` on `windows` - - The easies fix for all of this is simple - - create new folders for every test. -* `chromedriver` only supports arguments that begin with `--`. Hence arguments passed to VSC are limited to those that start with `--`. -* `Terminal` output cannot be retrieved using the `driver`. Hence output from terminal cannot be inspected. - - Perhaps thi sis possible, but at the time of writinng this I couldn't find a solution. - - I believe the `Terminal` in VSC is `SVG` based, hence reading text is out of the question. - - (This is made possible by writing the command to be executed into `commands.txt`, and letting the bootstrap extension read that file and run the command in the terminal using the VSC API). -* Sending characters to an input is slow, the `selenium` send text one character at a time. Hence tests are slow. -* Sending text to an editor can be flaky. - - Assume we would like to `type` some code into a VSC editor. - - As `selenium` sends a character at a time, VSC kicks in and attempts to format/autocomplete code and the like. This interferes with the code being typed out. - - Solution: Copy code into clipboard, then pase into editor. -* `Behave` does not generate any HTML reports - - Solution, we generate `cucumber` compliant `json` report. Hence the custom formatter in `report.py`. - - Using a `cucumber json` report format allows us to use existing tools to generate other HTML reports out of the raw `json` files. -* Sending keyboard commands to VSC (such as `ctrl+p`) is currently not possible (**not known how to**). - - `Selenium driver` can only send keyboard commands to a specific `html element`. - - But kyeboard commands such as `ctrl+p` are to be sent to the main window, and this isn't possible/not known. - - Solution: We need to find the `html element` in VSC that will accept keys such as `ctrl+p` and the like. - - Fortunately almost everything in VSC can be driven through commands in the `command palette`. - - Hence, we have an extension that opens the `command palette`, from there, we use `selenium driver` to select commands. - - This same extension is used to `activate` the `Python extension`. - - This extension is referred to as the `bootstrap extension`. -* When updating settings in VSC, do not alter the settings files directly. VSC could take a while to detect file changes and load the settings. - - An even better way, is to use the VSC api to update the settings (via the bootstrap API) or edit the settings file directly through the UI. - - Updating settings through the editor (by editing the `settings.json` file directly is not easy, as its not easy to update/remove settings). - - Using the API we can easily determine when VSC is aware of the changes (basically when API completes, VSC is aware of the new settings). - - (This is made possible by writing the settings to be updated into `settingsToUpdate.txt`, and letting the bootstrap extension read that file and update the VSC settings using the VSC API). - -## Files & Folders - -* The folder `.vsccode-test` in the root directory is where VSC is downloaded, workspace files created, etc. - - `stable` This is VS Code stable is downloaded (corresponding version of `chromedriver` is also downloaded and stored in this same place). - - `insider` This is VS Code insider is downloaded (corresponding version of `chromedriver` is also downloaded and stored in this same place). - - `user` Directory VS Code uses to store user information (settings, etc) - - `extensions` This is where the extensions get installed for the instance of VSC used for testing. - - `workspace folder` Folder opened in VS Code for testing - - `temp` Temporary directory for testing. (sometimes tests will create folders named `workspace folder xyz` to be used as workspace folders used for testing) - - `reports` Location where generated reports are stored. - - `logs` Logs for tests - - `screenshots` Screen shots captured during tests -- `uitests/tests/bootstrap` This is where the source for the bootstrap extension is stored. -- `uitests/tests/features` Location where all `BDD features` are stored. -- `uitests/tests/steps` Location where all `BDD steps` are defined. -- `uitests/tests/js` Location with helper `js` files (download chrome driver and generate html reports). -- `uitests/tests/vscode` Contains all modules related to `vscode` (driving the UI, downloading, starting, etc). -- `environment.py` `enviroyment` file for `Behave`. - -## CI Integration - -* For more details please check `build/ci`. -* We generally try to run all tests against all permutations of OS + Python Version + VSC - - I.e. we run tests across permutations of the follows: - - OS: Windows, Mac, Linux - - Python: 2.7, 3.5, 3.6, 3.7 - - VSC: Stable, Insiders -* Each scenario is treated as a test - - These results are published on Azure Devops - - Artifacts are published containing a folder named `.vscode test/reports/<scenario name>` - - This folder contains all information related to that test run: - - Screenshots (including the point in time the test failed) for every step in the scenario (sequentially named files) - - VS Code logs (including output from the output panels) - - The workspace folder that was opened in VSC code (we have the exact files used by VSC) - - Our logs (Extension logs, debugger logs) - - Basically we have everything we'd need to diagnoze the failure. -* The report for the entire run is uploaded as part of the artifact for the test job. - - The HTML report contains test results (screenshots & all the steps). -* The same ui tests are run as smoke tests as part of a PR. - - -## Miscellaneous - -* Use the debug configuration `Behave Smoke Tests` for debugging. -* In order to pass custom arguments to `Behave`, refer to the `CLI` (pass `behave` specific args after `--` in `python uitests test`). - - E.g. `python uitests test -- --tags=@wip --more-behave-args` -* Remember, the automated UI interactions can be faster than normal user interactions. - - E.g. just because we started debugging (using command `Debug: Start Debugging`), that doesn't mean the debug panel will open immediately. User interactions are slower compared to code execution. - - Solution, always wait for the UI elements to be available/active. E.g. when you open a file, check whether the corresponding elements are visible. diff --git a/uitests/TODO.md b/uitests/TODO.md deleted file mode 100644 index 47620abedcff..000000000000 --- a/uitests/TODO.md +++ /dev/null @@ -1,18 +0,0 @@ -- [ ] Dynamic detection of where `pyenv` environments are created and stored - - uitests/uitests/vscode/startup.py -- [ ] CRC compare files unzipped using python module and general unzipping. - - [ ] Identify whats wrong and file an issue upstream on Python if required. - - [ ] Use node.js as alternative -- [ ] Unzip using Python code - - Unzip tar files. -- [ ] Conda on Azure Pipelines don't work as the `environments.txt` file is not available/not updated. - - Is this the case in realworld? - - We need a fix/work around. -- [ ] Ensure we use spaces in path to the extension - - We have had bugs where extension fails due to spaces in paths (user name) - - Debugger fails -- [ ] When testing VS Code insiders, use the same chrome driver used in stable. - - Just hardcode the version of the chrome driver for now. -- [ ] Fail CI if a file is not created or vice versa. - Or run another script that'll check the existence and fail on stderr. - We don't want behave to monitor stderr, as we can ignore many errors. diff --git a/uitests/__main__.py b/uitests/__main__.py deleted file mode 100644 index 76b4f5720536..000000000000 --- a/uitests/__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("uitests", run_name="__main__", alter_sys=True) diff --git a/uitests/requirements.txt b/uitests/requirements.txt deleted file mode 100644 index dc8820deab0c..000000000000 --- a/uitests/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -docopt~=0.6.2 -behave~=1.2.6 -requests~=2.21.0 -selenium~=3.141.0 -progress~=1.5 -junitparser~=1.3.2 -pillow~=5.4.1 -psutil~=5.6.2 -pyperclip~=1.7.0 \ No newline at end of file diff --git a/uitests/uitests/__init__.py b/uitests/uitests/__init__.py deleted file mode 100644 index 6c532b962985..000000000000 --- a/uitests/uitests/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import vscode # noqa diff --git a/uitests/uitests/__main__.py b/uitests/uitests/__main__.py deleted file mode 100644 index 872f01f5fa9e..000000000000 --- a/uitests/uitests/__main__.py +++ /dev/null @@ -1,227 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -"""PVSC Smoke Tests. - -Usage: - uitests download [--channel=<stable_or_insider>] [--destination=<path>] - uitests install [--channel=<stable_or_insider>] [--destination=<path>] - uitests launch [--channel=<stable_or_insider>] [--destination=<path>] [--timeout=<seconds>] [--vsix=<vsix>] - uitests test [--channel=<stable_or_insider>] [--destination=<path>] [--timeout=<seconds>] [--vsix=<vsix>] [--] [<behave-options> ...] - uitests report [--destination=<path>] [--show] - uitests behave -- [<behave-options> ...] - -Options: - -h --help Show this screen. - --channel=<stable_or_insider> Defines the channel for VSC (stable or insider) [default: stable]. - --destination=<path> Path for smoke tests [default: .vscode test]. - --vsix=VSIX Path to VSIX [default: ms-python-insiders.vsix]. - --timeout=TIMEOUT Timeout for closing instance of VSC when Launched to validate instance of VSC [default: 30] - --show Whether to display the report or not. - --log=LEVEL Log Level [default: INFO]. - - Commands: - download Downloads chromedriver and VS Code (stable/insider based on --channel) - E.g. `python uitests download`, `python uitests download --channel=insider` - install Installs the extensions in VS Code. - E.g. `python uitests install`, `python uitests install --channel=insider --vsix=hello.vsix` - launch Launches VS Code (stable/insider based on --channel) with a default timeout of 30s. - Used for development purposes (e.g. check if VS loads, etc). - E.g. `python uitests launch`, `python uitests launch --channel=insider --timeout=60` - E.g. `python uitests install`, `python uitests install --channel=insider --vsix=hello.vsix` - test Launches the BDD tests using behave - E.g. `python uitests test`, `python uitests test --channel=insider -- --custom-behave=arguments --tags=@wip` - report Generates the BDD test reports (html report) - E.g. `python uitests report`, `python uitests report --show` - behave Run behave manually passing in the arguments after `--` - Used for development purposes. - E.g. `python uitests behave --- --dry-run` - -""" -import glob -import logging -import os -import os.path -import pathlib -import sys -import time - -from behave import __main__ -from docopt import docopt -from junitparser import JUnitXml - -from . import tools, vscode - - -def download(destination, channel, **kwargs): - """Download VS Code (stable/insiders) and chrome driver. - - The channel defines the channel for VSC (stable or insiders). - """ - destination = os.path.abspath(destination) - destination = os.path.join(destination, channel) - vscode.download.download_vscode(destination, channel) - vscode.download.download_chrome_driver(destination, channel) - - -def install(destination, channel, vsix, **kwargs): - """Installs the Python Extension into VS Code in preparation for the smoke tests.""" - destination = os.path.abspath(destination) - vsix = os.path.abspath(vsix) - options = vscode.application.get_options(destination, vsix=vsix, channel=channel) - vscode.application.install_extension(options) - - # Launch extension and exit (we want to ensure folders are created & extensions work). - vscode.application.setup_environment(options) - driver = vscode.application.launch_vscode(options) - context = vscode.application.Context(options, driver) - vscode.application.exit(context) - - -def launch(destination, channel, vsix, timeout=30, **kwargs): - """Launches VS Code (the same instance used for smoke tests).""" - destination = os.path.abspath(destination) - vsix = os.path.abspath(vsix) - options = vscode.application.get_options(destination, vsix=vsix, channel=channel) - logging.info(f"Launched VSC ({channel}) will exit in {timeout}s") - context = vscode.application.start(options) - logging.info(f"Activating Python Extension (assuming it is installed") - vscode.extension.activate_python_extension(context) - time.sleep(int(timeout)) - vscode.application.exit(context) - - -def report(destination, show=False, **kwargs): - """Generates an HTML report and optionally displays it.""" - _update_junit_report(destination, **kwargs) - destination = os.path.abspath(destination) - report_dir = os.path.join(destination, "reports") - tools.run_command( - [ - "node", - os.path.join("uitests", "uitests", "js", "report.js"), - report_dir, - str(show), - ] - ) - - -def _update_junit_report(destination, **kwargs): - """Updates the junit reports to contain the names of the current Azdo Job.""" - destination = os.path.abspath(destination) - report_dir = os.path.join(destination, "reports") - report_name = os.getenv("AgentJobName", "") - for name in glob.glob(os.path.join(report_dir, "*.xml")): - xml = JUnitXml.fromfile(name) - xml.name = f"({report_name}): {xml.name}" - for suite in xml: - suite.classname = f"({report_name}): {suite.classname}" - xml.write() - - -def test(destination, channel, vsix, behave_options, **kwargs): - """Start the bdd tests.""" - destination = os.path.abspath(destination) - - vsix = os.path.abspath(vsix) - args = ( - [ - "-f", - "uitests.report:PrettyCucumberJSONFormatter", - "-o", - os.path.join(destination, "reports", "report.json"), - "--junit", - "--junit-directory", - os.path.join(destination, "reports"), - ] - + [ - "--define", - f"destination={destination}", - "--define", - f"channel={channel}", - "--define", - f"vsix={vsix}", - os.path.abspath("uitests/uitests"), - ] - # Custom arguments provided via command line or on CI. - + behave_options - ) - - # Change directory for behave to work correctly. - curdir = os.path.dirname(os.path.realpath(__file__)) - os.chdir(pathlib.Path(__file__).parent) - - # Selenium and other packages write to stderr & so does default logging output. - # Confused how this can be configured with behave and other libs. - # Hence just capture exit code from behave and throw error to signal failure to CI. - exit_code = __main__.main(args) - # Write exit code to a text file, so we can read it and fail CI in a separate task (fail if file exists). - # CI doesn't seem to fail based on exit codes. - # We can't fail on writing to stderr either as python logs stuff there & other errors that can be ignored are written there. - failure_file = os.path.join(curdir, "uitest_failed.txt") - - if exit_code > 0: - with open(failure_file, "w") as fp: - fp.write(str(exit_code)) - sys.stderr.write("Behave tests failed") - sys.stderr.flush() - else: - try: - os.unlink(failure_file) - except Exception: - pass - return exit_code - - -def run_behave(destination, *args): - """Start the smoke tests.""" - destination = os.path.abspath(destination) - - # Change directory for behave to work correctly. - os.chdir(pathlib.Path(__file__).parent) - - return __main__.main([*args]) - - -def main(): - arguments = docopt(__doc__, version="1.0") - behave_options = arguments.get("<behave-options>") - options = { - **{ - key[2:]: value for (key, value) in arguments.items() if key.startswith("--") - }, - **{ - key: value for (key, value) in arguments.items() if not key.startswith("--") - }, - } - log = arguments.get("--log", "INFO") - log_level = getattr(logging, log.upper()) - - if log_level == logging.INFO: - logging.basicConfig( - level=log_level, format="%(asctime)s %(message)s", stream=sys.stdout - ) - else: - logging.basicConfig(level=log_level) - - options.setdefault("behave_options", behave_options) - handler = lambda **kwargs: 0 # noqa - if arguments.get("download"): - handler = download - if arguments.get("install"): - handler = install - if arguments.get("launch"): - handler = launch - if arguments.get("test"): - handler = test - if arguments.get("report"): - handler = report - if arguments.get("behave"): - options = behave_options - return run_behave(arguments.get("--destination"), *behave_options) - return handler(**options) - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/uitests/uitests/bootstrap/README.md b/uitests/uitests/bootstrap/README.md deleted file mode 100644 index 49c6cb511e9b..000000000000 --- a/uitests/uitests/bootstrap/README.md +++ /dev/null @@ -1,9 +0,0 @@ -# Purpose of the bootstrap extension -* Haven't found a way to pass command line arguments to VSC when using selenium. -* We need to open a workspace folder when launching VSC. -* As we cannot (don't yet know) do this via CLI, the approach is simple: - * Create a simple extension that will activate when VSC loads - * Look for a file that contains the path to the workspace folder that needs to be opened. - * Next use VSC API to re-load VSC by opening that folder. - -* Hacky, but it works, at least untill we know how to pass CLI args when using `selenium` diff --git a/uitests/uitests/bootstrap/__init__.py b/uitests/uitests/bootstrap/__init__.py deleted file mode 100644 index 378218f602fc..000000000000 --- a/uitests/uitests/bootstrap/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import main # noqa diff --git a/uitests/uitests/bootstrap/extension/.vscodeignore b/uitests/uitests/bootstrap/extension/.vscodeignore deleted file mode 100644 index cb57e71dec6b..000000000000 --- a/uitests/uitests/bootstrap/extension/.vscodeignore +++ /dev/null @@ -1,11 +0,0 @@ -.vscode/** -.vscode-test/** -.vscode test/** -out/test/** -out/**/*.map -src/** -.gitignore -tsconfig.json -vsc-extension-quickstart.md -tslint.json -*.vsix diff --git a/uitests/uitests/bootstrap/extension/extension.js b/uitests/uitests/bootstrap/extension/extension.js deleted file mode 100644 index 786301203d1e..000000000000 --- a/uitests/uitests/bootstrap/extension/extension.js +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -Object.defineProperty(exports, "__esModule", { value: true }); -const vscode = require("vscode"); -const fs = require('fs'); -const path = require('path'); -const util = require('util'); - -let activated = false; -async function sleep(timeout) { - return new Promise(resolve => setTimeout(resolve, timeout)); -} -function activate(context) { - const statusBarItemActivated = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10000000); - const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10000000); - statusBarItem.command = 'workbench.action.quickOpen'; - statusBarItem.text = 'Py'; - statusBarItem.tooltip = 'Py'; - statusBarItem.show(); - - context.subscriptions.push(statusBarItem); - // Always display editor line, column in this statusbar. - // Sometimes we cannot detect the line,column of editor (because that item in statubar is not visbible due to lack of realestate). - // This will get around that problem. - vscode.window.onDidChangeTextEditorSelection(e => { - try { - statusBarItemActivated.text = `${e.textEditor.selection.start.line + 1},${e.textEditor.selection.start.character + 1}`; - } catch { } - }); - vscode.commands.registerCommand('smoketest.activatePython', async () => { - if (activated) { - return; - } - const ext = vscode.extensions.getExtension('ms-python.python'); - if (!ext.isActive) { - await ext.activate(); - } - statusBarItemActivated.text = 'Py2'; - statusBarItemActivated.tooltip = 'Py2'; - // Don't remove this command, else the CSS selector for this will be different. - // VSC will render a span if there's no span. - statusBarItemActivated.command = 'workbench.action.quickOpen'; - statusBarItemActivated.show(); - - activated = true; - context.subscriptions.push(statusBarItemActivated); - }); - vscode.commands.registerCommand('smoketest.runInTerminal', async () => { - const filePath = path.join(__dirname, '..', 'commands.txt'); - const command = fs.readFileSync(filePath).toString().trim(); - for (let counter = 0; counter < 5; counter++) { - if (!vscode.window.activeTerminal) { - await sleep(5000); - } - } - if (!vscode.window.activeTerminal) { - vscode.window.createTerminal('Manual'); - await sleep(5000); - } - if (!vscode.window.activeTerminal) { - vscode.window.showErrorMessage('No Terminal in Bootstrap Extension'); - } - await vscode.window.activeTerminal.sendText(command, true); - fs.unlinkSync(filePath); - }); - vscode.commands.registerCommand('smoketest.updateSettings', async () => { - const filePath = path.join(__dirname, '..', 'settingsToUpdate.txt'); - try { - const setting = getSettingsToUpdateRemove(filePath); - const configTarget = setting.type === 'user' ? vscode.ConfigurationTarget.Global : - (setting.type === 'workspace' ? vscode.ConfigurationTarget.Workspace : vscode.ConfigurationTarget.WorkspaceFolder); - - if (configTarget === vscode.ConfigurationTarget.WorkspaceFolder && !setting.workspaceFolder) { - vscode.window.showErrorMessage('Workspace Folder not defined for udpate/remove of settings'); - throw new Error('Workspace Folder not defined'); - } - - const resource = setting.workspaceFolder ? vscode.Uri.file(setting.workspaceFolder) : undefined; - - for (let settingToRemove in (setting.remove || [])) { - const parentSection = settingToRemove.split('.')[0]; - const childSection = settingToRemove.split('.').filter((_, i) => i > 0).join('.'); - const settings = vscode.workspace.getConfiguration(parentSection, resource); - await settings.update(childSection, undefined, configTarget); - } - for (let settingToAddUpdate in (setting.update || [])) { - const parentSection = settingToAddUpdate.split('.')[0]; - const childSection = settingToAddUpdate.split('.').filter((_, i) => i > 0).join('.'); - const settings = vscode.workspace.getConfiguration(parentSection, resource); - await settings.update(childSection, setting.update[settingToAddUpdate], configTarget); - } - fs.unlinkSync(filePath); - } catch (ex) { - fs.appendFileSync(path.join(__dirname, '..', 'settingsToUpdate_error.txt'), util.format(ex)); - } - }); - vscode.commands.registerCommand('smoketest.openFile', async () => { - const file = fs.readFileSync(path.join(__dirname, '..', 'commands.txt')).toString().trim(); - const doc = await vscode.workspace.openTextDocument(file); - await vscode.window.showTextDocument(doc) - }); -} - -/** -* @typedef {Object} SettingsToUpdate - creates a new type named 'SpecialType' -* @property {'user' | 'workspace' | 'workspaceFolder'} [type] - Type. -* @property {?string} workspaceFolder - Workspace Folder -* @property {Object.<string, object>} update - Settings to update. -* @property {Array<string>} remove - Skip format checks. -*/ - -/** - * - * - * @param {*} filePath - * @return {SettingsToUpdate} Settings to update/remove. - */ -function getSettingsToUpdateRemove(filePath) { - return JSON.parse(fs.readFileSync(filePath).toString().trim()); -} -exports.activate = activate; -function deactivate() { - // Do nothing. -} -exports.deactivate = deactivate; diff --git a/uitests/uitests/bootstrap/extension/package.json b/uitests/uitests/bootstrap/extension/package.json deleted file mode 100644 index 0d8258357a1c..000000000000 --- a/uitests/uitests/bootstrap/extension/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "smoketest", - "publisher": "ms-python", - "displayName": "smokeTestPython", - "description": "Bootstrap for Python Smoke Tests", - "version": "0.0.1", - "license": "MIT", - "homepage": "https://github.com/Microsoft/vscode-python", - "repository": { - "type": "git", - "url": "https://github.com/Microsoft/vscode-python" - }, - "bugs": { - "url": "https://github.com/Microsoft/vscode-python/issues" - }, - "qna": "https://stackoverflow.com/questions/tagged/visual-studio-code+python", - "engines": { - "vscode": "^1.32.0" - }, - "categories": [ - "Other" - ], - "activationEvents": [ - "*" - ], - "main": "./extension", - "contributes": { - "commands": [ - { - "command": "smoketest.openworkspace", - "title": "Open Smoke Test Workspace" - }, - { - "command": "smoketest.activatePython", - "title": "Activate Python Extension" - }, - { - "command": "smoketest.runInTerminal", - "title": "Smoke: Run Command In Terminal" - }, - { - "command": "smoketest.updateSettings", - "title": "Smoke: Update Settings" - }, - { - "command": "smoketest.openFile", - "title": "Smoke: Open File" - } - ] - } -} diff --git a/uitests/uitests/bootstrap/main.py b/uitests/uitests/bootstrap/main.py deleted file mode 100644 index 4f7d33dd232b..000000000000 --- a/uitests/uitests/bootstrap/main.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import os.path - -import uitests.tools - -_current_dir = os.path.dirname(os.path.realpath(__file__)) -EXTENSION_DIR = os.path.abspath(os.path.join(_current_dir, "extension")) # noqa -EXTENSION_FILE = os.path.join(EXTENSION_DIR, "smoketest-0.0.1.vsix") - - -def build_extension(): - """Build the bootstrap extension.""" - command = ["vsce", "package"] - uitests.tools.run_command( - command, cwd=EXTENSION_DIR, progress_message="Build Bootstrap Extension" # noqa - ) - - -def get_extension_path(): - """Get the path to the VSIX of the bootstrap extension.""" - if not os.path.isfile(EXTENSION_FILE): - build_extension() - return EXTENSION_FILE diff --git a/uitests/uitests/config.json b/uitests/uitests/config.json deleted file mode 100644 index 7b959282a406..000000000000 --- a/uitests/uitests/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "--python-path": "/Library/Frameworks/Python.framework/Versions/3.7/bin/python3", - "--python-type": "", - "--python-version": "3.7", - "--python3-path": "/Library/Frameworks/Python.framework/Versions/3.7/bin/python3", - "--pipenv-path": "" -} diff --git a/uitests/uitests/environment.py b/uitests/uitests/environment.py deleted file mode 100644 index e2ddb8ada41c..000000000000 --- a/uitests/uitests/environment.py +++ /dev/null @@ -1,316 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import os -import os.path -import re -import shutil -import time -from functools import wraps - -import behave -import behave.model_core -import parse -from behave.contrib.scenario_autoretry import patch_scenario_with_autoretry -from selenium.common.exceptions import WebDriverException - -import uitests.tools -import uitests.vscode -import uitests.vscode.core -import uitests.vscode.extension -import uitests.vscode.settings - - -@parse.with_pattern(r"\d+") -def parse_number(text): - return int(text) - - -behave.register_type(Number=parse_number) - - -def restore_context(): - """The context object gets created a new for every test. - We need to ensure we keep adding the required items. - We need to update the `driver` and `options` property of the context. - Note, its possible we have a new driver instance due to reloading of VSC. - This needs to be done for every hook. - - """ - - def deco_context(f): - @wraps(f) - def f_restore_context(*args, **kwargs): - context = args[0] - context.driver = uitests.vscode.application.CONTEXT["driver"] - context.options = uitests.vscode.application.CONTEXT["options"] - return f(*args, **kwargs) - - return f_restore_context - - return deco_context - - -@uitests.tools.retry((TimeoutError, WebDriverException), tries=5, delay=5) -@uitests.tools.log_exceptions() -def before_all(context): - options = uitests.vscode.application.get_options(**context.config.userdata) - # Exit before retrying. - _exit(context) - _start_and_clear(context, options) - - -def after_all(context): - _exit(context) - - -def before_feature(context, feature): - for scenario in feature.scenarios: - # If we're working on a scenario, then don't retry. - if "wip" in scenario.effective_tags: - continue - elif "autoretry" in scenario.effective_tags: - patch_scenario_with_autoretry(scenario, max_attempts=3) - else: - # Try at least once. - # We might want to remove this, but leave it for now. - # VSC can be flaky at times, here are a few examples: - # 1. Line number isn't displayed in statusbar of VSC. - # 2. Invoking `Close All Editors`, doesn't necessarily close everything. - # 3. Other flaky issues. - # 4. Download speeds are slow and LS doesn't get downloaded. - # 5. Starting LS/Jedi is slow for some reason, and go-to-definition is slow/doesn't work. - # 6. Similar intellisense is slow/doesn't work on both Jedi & LS. - # We might want to log these as well, so we're aware of the flaky tests. - patch_scenario_with_autoretry(scenario, max_attempts=2) - - -@uitests.tools.retry((PermissionError, FileNotFoundError), tries=2) -@uitests.tools.log_exceptions() -@restore_context() -def before_scenario(context, scenario): - """Note: - - Create new workspace folders for each test. - - Shutdown and start vscode for every test. - - Reasons: - - Its alsmost impossible to use the same folder in Windows, as we cannot delete files. - If VSC is open, Windows won't let us delete files... etc. - - More VSC issue is `recent files`. - Assume we open a workspace folder with a file named `hello.py`. - We open the file for a test. - Next we open another workspace folder for another test with a file named `some folder/hello.py`. - Now VSC remembers the files it opened previously (`recent files`). - So, when we attempt to open files using just file name, then VSC attempts to open `hello.py - instead of `some folder/hello.py`. At this point, VSC displays an error message to the user. - However this is not desired in our tests. Hence just create a new folder. - - As we need to create new folders, and sometimes we have a few flaky issues with selenium, - its easier to just start vs code evertime. - - """ - _exit(context) - - repo = [ - tag - for tag in scenario.effective_tags - if tag.lower().startswith("https://github.com/") - ] - uitests.vscode.application.setup_workspace(context, repo[0] if repo else None) - - # Create directory for scenario specific logs. - context.scenario_log_dir = os.path.join( - context.options.reports_dir, - scenario.filename, - re.sub("[^-a-zA-Z0-9_. ]+", "", scenario.name).strip(), - ).replace("/", os.path.sep) - os.makedirs(context.scenario_log_dir, exist_ok=True) - # Ensure screenshots go here. - context.options.screenshots_dir = os.path.join( - context.scenario_log_dir, "screenshots" - ) - os.makedirs(context.options.screenshots_dir, exist_ok=True) - - # Restore user settings (could have been changed for tests) - uitests.vscode.application.setup_user_settings(context.options) - - # Always reload, as we create a new workspace folder. - uitests.vscode.application.reload(context) - - # Possible we restarted VSC, so ensure we clear the onetime messages. - _dismiss_one_time_messages(context, retry_count=2) - - -@uitests.tools.log_exceptions() -@restore_context() -def after_scenario(context, scenario): - try: - # Clear before the next test. - uitests.vscode.application.clear_everything(context) - except Exception: - pass - _exit(context) - - if scenario.status == behave.model_core.Status.passed: - try: - # If passed successfully, then delete screenshots of each step. - # Save space in logs captured (else logs/artifacts would be too large on CI). - # By default the screenshots directory is included into reports - # If tests pass, then no need of the screenshots. - uitests.tools.empty_directory( - os.path.join(context.options.screenshots_dir, "steps") - ) - except Exception: - pass - else: - # Copy all logs & current workspace into scenario specific directory. - copy_folders = [ - (context.options.logfiles_dir, "logs"), - (os.path.join(context.options.user_dir, "logs"), "user_logs"), - (context.options.workspace_folder, "workspace"), - ] - for source, target in copy_folders: - try: - shutil.copytree(source, os.path.join(context.scenario_log_dir, target)) - except Exception: - pass - # We need user settings as well for logs. - # When running a test that requires us to test loading VSC for the first time, - # (the step is `I open VS Code for the first time`) - # then we delete this user settings file (after all this shouldn't exist when loading VSC for first time). - # However, if the test fails half way through, then the settings.json will not exist. - # Hence check if the file exists. - if os.path.exists( - os.path.join(context.options.user_dir, "User", "settings.json") - ): - os.makedirs(os.path.join(context.scenario_log_dir, "User"), exist_ok=True) - shutil.copyfile( - os.path.join(context.options.user_dir, "User", "settings.json"), - os.path.join(context.scenario_log_dir, "User", "settings.json"), - ) - # We don't need these logs anymore. - _exit(context) - uitests.tools.empty_directory(context.options.logfiles_dir) - uitests.tools.empty_directory(os.path.join(context.options.user_dir, "logs")) - os.makedirs(context.options.logfiles_dir, exist_ok=True) - - -@uitests.tools.log_exceptions() -@restore_context() -def before_step(context, step): - logging.info("Before step") - - -@uitests.tools.log_exceptions() -@restore_context() -def after_step(context, step): - logging.info("After step") - - # Lets take screenshots after every step for logging purposes. - # If the scenario passes, then delete all screenshots. - # These screenshots are captured into a special directory. - try: - # This is a hack, hence handle any error. - step_index = context._stack[0]["scenario"].steps.index(step) - screenshot_file_name = os.path.join( - context.options.screenshots_dir, "steps", f"step_{step_index}.png" - ) - os.makedirs( - os.path.join(context.options.screenshots_dir, "steps"), exist_ok=True - ) - uitests.vscode.application.capture_screen_to_file(context, screenshot_file_name) - except Exception: - pass - - # If this is the last step, then add a screenshot. - # This is just for reporting purposes. I.e. take screenshots after every test. - add_screenshot = False - try: - # This is a hack, hence handle any error. - add_screenshot = context._stack[0]["scenario"].steps[-1:][0] == step - except Exception: - pass - - if add_screenshot or step.exception is not None: - try: - uitests.vscode.application.capture_screen(context) - # # We might want folder view in screenshoits as well. - # uitests.vscode.quick_open.select_command(context, "View: Show Explorer") - # uitests.vscode.application.capture_screen(context) - # # We might want panels without - # uitests.vscode.notifications.clear() - # uitests.vscode.application.capture_screen(context) - except Exception: - # Possible vsc has died as part of the exceptiion. - # Or we closed it as part of a step. - pass - - # Attach the traceback (behave doesn't add tb if assertions have an error message) - if step.exception is not None: - try: - uitests.vscode.application.capture_exception(context, step) - except Exception: - pass - - -@restore_context() -def _exit(context): - uitests.vscode.application.exit(context) - uitests.vscode.application.CONTEXT["driver"] = None - - -def _start_and_clear(context, options): - # Clear VS Code folders (do not let VSC save state). - # During tests, this can be done as a step `When I load VSC for the first time`. - # But when starting tests from scratch, always start fresh. - uitests.vscode.application.clear_vscode(options) - - app_context = uitests.vscode.application.start(options) - context.driver = app_context.driver - context.options = app_context.options - - try: - # For VS Code to start displaying messages, we need to perform some UI operations. - # Right now, loading extension does that. - # Ensure extension loads - # Also loading extensions will display extension messages which we can close. - uitests.vscode.extension.activate_python_extension(context) - - _dismiss_one_time_messages(context) - except Exception: - try: - uitests.vscode.application.capture_screen_to_file( - context, os.path.join(options.reports_dir, "Start_Clear_Failed.png") - ) - except Exception: - pass - raise - - -def _dismiss_one_time_messages(context, retry_count=100, retry_interval=0.1): - # Dismiss one time VSC messages. - # Dismiss one time extension messages. - # Append to previous messages, possibly they weren't dimissed as they timed out. - messages_to_dismiss = [ - ("Help improve VS Code by allowing",), - ("Tip: you can change the Python interpreter", "Got it!"), - ] - - # Using the step `I open VS Code for the first time` will ensure these messages - # get displayed again. Check out application.clear_vscode() - # We don't care if we are unable to dismiss these messages. - for i in range(retry_count): - message = messages_to_dismiss.pop(0) - - try: - uitests.vscode.notifications.dismiss_message( - context, *message, retry_count=1, retry_interval=0.1 - ) - except Exception: - # Re-queue to try and dismiss it again. - messages_to_dismiss.append(message) - # Wait for message to appear. - time.sleep(0.5) - - if len(messages_to_dismiss) == 0: - break diff --git a/uitests/uitests/features/README.md b/uitests/uitests/features/README.md deleted file mode 100644 index 49cb58a13b7e..000000000000 --- a/uitests/uitests/features/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Tags - -* @wip - - Used only for debugging purposes. - - When debugging in VSC, only features/scenarios with @wip tag will be executed. -* @skip - - Used to skip a feature/scenario. -* @https://github.com/xxx/yyy.git - - Can only be used at a feature level. - - The conents of the above repo will be used as the contents of the workspace folder. - - Note: assume the tag is `@https://github.com/DonJayamanne/pyvscSmokeTesting.git` - - The above repo is cloned directly into the workspace. - - If however the tag is `@https://github.com/DonJayamanne/pyvscSmokeTesting/testing` - - Now, the contents of the workspace is the `tests` directory in the above repo. - - This allows us to have a single repo with files/tests for more than just one feature/scenario. - - Else we'd need to have multiple repos for each feature/scenario. -* @mac, @win, @linux - - Used to ensure a particular feature/scenario runs only on mac, win or linux respectively. -* @python2, @python3, @python3.5, , @python3.6, , @python3.7 - - Used to ensure a particular feature/scenario runs only on specific version of Python, respectively. -* @smoke - - All smoke test related functionality. -* @testing - - All testing related functionality. -* @debugging - - All debugger related functionality. -* @ls - - Language Server (Jedi + MS LS) related functionality. -* @terminal - - All terminal related functionality. - - @terminal.venv - - Related to virtual environments (`python -m venv`) - - @terminal.pipenv - - Related to pipenv environments (`pipenv shell`) diff --git a/uitests/uitests/features/datascience/basic.feature b/uitests/uitests/features/datascience/basic.feature deleted file mode 100644 index 93428b20ae43..000000000000 --- a/uitests/uitests/features/datascience/basic.feature +++ /dev/null @@ -1,39 +0,0 @@ -# @ds @smoke -# @https://github.com/DonJayamanne/vscode-python-uitests/datascience -# Feature: Data Science -# Scenario: Can display an image and print text into the interactive window -# Given the package "jupyter" is installed -# And a file named "log.log" does not exist -# # Increase font size for text detection. -# And the workspace setting "editor.fontSize" has the value 15 -# And the file "smoke.py" is open -# When I wait for the Python extension to activate -# # Code will display an image and print stuff into interactive window. -# When I select the command "Python: Run All Cells" -# # Wait for Interactive Window to open -# And I wait for 10 seconds -# # Close the file, to close it, first set focus to it by opening it again. -# And I open the file "smoke.py" -# And I select the command "View: Revert and Close Editor" -# And I select the command "View: Close Panel" -# # Wait for 2 minutes for Jupyter to start -# Then a file named "log.log" will be created within 120 seconds -# # This is the content of the image rendered in the interactive window. -# # And the text "VSCODEROCKS" is displayed in the Interactive Window -# # # This is the content printed by a python script. -# # And the text "DATASCIENCEROCKS" is displayed in the Interactive Window - -# Scenario: Workspace directory is used as cwd for untitled python files -# Given the package "jupyter" is installed -# And a file named "log.log" does not exist -# When I wait for the Python extension to activate -# When I create an untitled Python file with the following contents -# """ -# open("log.log", "w").write("Hello") -# """ -# # Code will display an image and print stuff into interactive window. -# When I select the command "Python: Run All Cells" -# # Wait for Interactive Window to open -# And I wait for 10 seconds -# # Wait for 2 minutes for Jupyter to start -# Then a file named "log.log" will be created within 120 seconds diff --git a/uitests/uitests/features/debugging/basic.feature b/uitests/uitests/features/debugging/basic.feature deleted file mode 100644 index cd3dd54f5011..000000000000 --- a/uitests/uitests/features/debugging/basic.feature +++ /dev/null @@ -1,37 +0,0 @@ -@debugging -Feature: Debugging - Scenario: Debugging a python file without creating a launch configuration (with delays) - Given the file ".vscode/launch.json" does not exist - And a file named "simple sample.py" is created with the following contents - """ - # Add a minor delay for tests to confirm debugger has started - import time - - - time.sleep(2) - print("Hello World") - open("log.log", "w").write("Hello") - """ - When I wait for the Python extension to activate - And I open the file "simple sample.py" - And I select the command "Debug: Start Debugging" - Then the Python Debug Configuration picker is displayed - When I select the debug configuration "Python File" - Then the debugger starts - And the debugger will stop within 5 seconds - And a file named "log.log" will be created - - Scenario: Debugging a python file without creating a launch configuration (hello world) - Given the file ".vscode/launch.json" does not exist - And a file named "simple sample.py" is created with the following contents - """ - print("Hello World") - open("log.log", "w").write("Hello") - """ - When I wait for the Python extension to activate - And I open the file "simple sample.py" - And I select the command "Debug: Start Debugging" - Then the Python Debug Configuration picker is displayed - When I select the debug configuration "Python File" - Then the debugger will stop within 5 seconds - And a file named "log.log" will be created within 5 seconds diff --git a/uitests/uitests/features/debugging/breakpoints.feature b/uitests/uitests/features/debugging/breakpoints.feature deleted file mode 100644 index ae8f508be6df..000000000000 --- a/uitests/uitests/features/debugging/breakpoints.feature +++ /dev/null @@ -1,66 +0,0 @@ -@debugging -Feature: Debugging - @smoke - Scenario: Debugging a python file with breakpoints - Given a file named ".vscode/launch.json" is created with the following contents - """ - { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/simple sample.py", - "console": "integratedTerminal" - } - ] - } - """ - And a file named "simple sample.py" is created with the following contents - """ - open("log.log", "w").write("Hello") - """ - When I wait for the Python extension to activate - And I open the file "simple sample.py" - And I add a breakpoint to line 1 in "simple sample.py" - And I select the command "View: Close All Editors" - And I select the command "Debug: Start Debugging" - Then the debugger starts - And the debugger pauses - And the file "simple sample.py" is opened - And the cursor is on line 1 - And the current stack frame is at line 1 in "simple sample.py" - When I select the command "Debug: Continue" - Then the debugger stops - - Scenario: Debugging a python file without breakpoints - Given a file named ".vscode/launch.json" is created with the following contents - """ - { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/simple sample.py", - "console": "integratedTerminal", - "stopOnEntry": true - } - ] - } - """ - And a file named "simple sample.py" is created with the following contents - """ - open("log.log", "w").write("Hello") - """ - When I wait for the Python extension to activate - And I select the command "Debug: Start Debugging" - Then the debugger starts - And the debugger pauses - And the file "simple sample.py" is opened - And the cursor is on line 1 - And the current stack frame is at line 1 in "simple sample.py" - When I select the command "Debug: Continue" - Then the debugger stops diff --git a/uitests/uitests/features/environmentFiles/terminal.feature b/uitests/uitests/features/environmentFiles/terminal.feature deleted file mode 100644 index 1daec45e9a82..000000000000 --- a/uitests/uitests/features/environmentFiles/terminal.feature +++ /dev/null @@ -1,89 +0,0 @@ -@terminal -Feature: Environment Files - Background: Activted Extension - Given the python extension has been activated - Given a file named ".env" is created with the following contents - """ - MY_FILE_NAME=log1.log - """ - Given a file named ".env2" is created with the following contents - """ - MY_FILE_NAME=log2.log - """ - Given a file named "simple sample.py" is created with the following contents - """ - import os - file_name = os.environ.get("MY_FILE_NAME", "other.log") - with open(file_name, "w") as fp: - fp.write("Hello") - """ - And a file named "log1.log" does not exist - And a file named "log2.log" does not exist - - Scenario: Environment variable defined in default environment file is used by debugger - Given a file named ".vscode/launch.json" is created with the following contents - """ - { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/simple sample.py", - "console": "integratedTerminal" - } - ] - } - """ - When I open the file "simple sample.py" - And I select the command "Debug: Start Debugging" - Then the debugger starts - And the debugger stops - And a file named "log1.log" will be created - - Scenario: Environment variable defined in envFile of launch.json is used by debugger - Given a file named ".vscode/launch.json" is created with the following contents - """ - { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/simple sample.py", - "console": "integratedTerminal", - "envFile": "${workspaceFolder}/.env2" - } - ] - } - """ - When I open the file "simple sample.py" - And I select the command "Debug: Start Debugging" - Then the debugger starts - And the debugger stops - And a file named "log2.log" will be created - - Scenario: Environment variable defined in envFile of settings.json is used by debugger - Given the workspace setting "python.envFile" has the value "${workspaceFolder}/.env2" - Given a file named ".vscode/launch.json" is created with the following contents - """ - { - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "program": "${workspaceFolder}/simple sample.py", - "console": "integratedTerminal" - } - ] - } - """ - When I open the file "simple sample.py" - And I select the command "Debug: Start Debugging" - Then the debugger starts - And the debugger stops - And a file named "log2.log" will be created diff --git a/uitests/uitests/features/interpreter/basic.feature b/uitests/uitests/features/interpreter/basic.feature deleted file mode 100644 index 18e46a656c40..000000000000 --- a/uitests/uitests/features/interpreter/basic.feature +++ /dev/null @@ -1,9 +0,0 @@ -@terminal -Feature: Interpreter - Background: Activted Extension - Given the python extension has been activated - - @mac - Scenario: Select default Mac 2.7 Interpreter - When I select the default mac Interpreter - Then a message with the text "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." is displayed diff --git a/uitests/uitests/features/interpreter/conda.feature b/uitests/uitests/features/interpreter/conda.feature deleted file mode 100644 index f16eb11886f3..000000000000 --- a/uitests/uitests/features/interpreter/conda.feature +++ /dev/null @@ -1,88 +0,0 @@ -# @terminal @terminal.conda -# @skip -# @https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution -# Feature: Terminal (conda) -# Scenario: Interpreter display name contains the name of the environment and conda -# Given the user setting "python.pythonPath" does not exist -# And a conda environment is created with the name "helloworld" -# Then take a screenshot -# When I wait for 20 seconds -# Then take a screenshot -# # Wait for some time for the new conda environment to get discovered. -# When I reload VSC -# Then take a screenshot -# When I send the command "conda env list" to the terminal -# When I wait for 5 seconds -# Then take a screenshot -# When I wait for 20 seconds -# Then take a screenshot -# When I select the command "Python: Select Interpreter" -# When I wait for 3 seconds -# Then take a screenshot -# When I reload VSC -# And I wait for 30 seconds -# And I select the Python Interpreter containing the name "helloworld" -# Then take a screenshot -# Then the python interpreter displayed in the the status bar contains the value "conda" in the display name -# And the python interpreter displayed in the the status bar contains the value "helloworld" in the display name -# And the workspace setting "python.pythonPath" exists - -# @preserve.workspace -# Scenario: Pipenv is auto selected -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# And the workspace setting "python.pythonPath" exists - -# @preserve.workspace -# Scenario: Pipenv is not auto selected (if we already have a local interpreter selected) -# Given a generic Python Interpreter is selected -# When I reload VSC -# Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name -# And the workspace setting "python.pythonPath" exists - -# @preserve.workspace -# Scenario: Pipenv is not auto selected (if we have a global interpreter selected) -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" exists -# When I reload VSC -# Then open the file "settings.json" -# Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name - -# @preserve.workspace -# Scenario: Environment is not activated in the Terminal -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# Given the file "write_pyPath_in_log.py" is open -# And a file named "log.log" does not exist -# And the workspace setting "python.terminal.activateEnvironment" is disabled -# And a terminal is opened -# When I send the command "python run_in_terminal.py" to the terminal -# Then a file named "log.log" will be created -# And open the file "log.log" -# And the file "log.log" does not contain the value "workspace_folder" -# And take a screenshot - -# @preserve.workspace -# Scenario: Environment is activated in the Terminal -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# Given the file "run_in_terminal.py" is open -# And a file named "log.log" does not exist -# And the workspace setting "python.terminal.activateEnvironment" is enabled -# And a terminal is opened -# When I send the command "python run_in_terminal.py" to the terminal -# Then a file named "log.log" will be created -# And open the file "log.log" -# And the file "log.log" contains the value "workspace_folder" -# And take a screenshot diff --git a/uitests/uitests/features/interpreter/pipenv.feature b/uitests/uitests/features/interpreter/pipenv.feature deleted file mode 100644 index a0444baa8113..000000000000 --- a/uitests/uitests/features/interpreter/pipenv.feature +++ /dev/null @@ -1,71 +0,0 @@ -# @terminal @terminal.pipenv -# @https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution -# Feature: Terminal (pipenv) -# Scenario: Interpreter display name contains the name of the current workspace folder and pipenv -# Given the user setting "python.pythonPath" does not exist -# And a pipenv environment is created -# When I reload VSC -# And I select the Python Interpreter containing the name "workspace folder pipenv" -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# And the workspace setting "python.pythonPath" exists - -# @preserve.workspace -# Scenario: Pipenv is auto selected -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# And the workspace setting "python.pythonPath" exists - -# @preserve.workspace -# Scenario: Pipenv is not auto selected (if we already have a local interpreter selected) -# Given a generic Python Interpreter is selected -# When I reload VSC -# Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name -# And the workspace setting "python.pythonPath" exists - -# @preserve.workspace -# Scenario: Pipenv is not auto selected (if we have a global interpreter selected) -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" exists -# When I reload VSC -# Then open the file "settings.json" -# Then the python interpreter displayed in the the status bar does not contain the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar does not contain the value "workspace folder" in the display name - -# @preserve.workspace -# Scenario: Environment is not activated in the Terminal -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# Given the file "write_pyPath_in_log.py" is open -# And a file named "log.log" does not exist -# And the workspace setting "python.terminal.activateEnvironment" is disabled -# And a terminal is opened -# When I send the command "python run_in_terminal.py" to the terminal -# Then a file named "log.log" will be created -# And open the file "log.log" -# And the file "log.log" does not contain the value "workspace_folder" -# And take a screenshot - -# @preserve.workspace -# Scenario: Environment is activated in the Terminal -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "pipenv" in the display name -# And the python interpreter displayed in the the status bar contains the value "workspace folder" in the display name -# Given the file "run_in_terminal.py" is open -# And a file named "log.log" does not exist -# And the workspace setting "python.terminal.activateEnvironment" is enabled -# And a terminal is opened -# When I send the command "python run_in_terminal.py" to the terminal -# Then a file named "log.log" will be created -# And open the file "log.log" -# And the file "log.log" contains the value "workspace_folder" -# And take a screenshot diff --git a/uitests/uitests/features/interpreter/statusbar.feature b/uitests/uitests/features/interpreter/statusbar.feature deleted file mode 100644 index f81b97f0ab97..000000000000 --- a/uitests/uitests/features/interpreter/statusbar.feature +++ /dev/null @@ -1,18 +0,0 @@ -@terminal -Feature: Statusbar - Background: Activted Extension - Given the python extension has been activated - - @smoke - Scenario: Interpreter is displayed in the statusbar - Then the python interpreter displayed in the the status bar contains the value "Python" in the display name - - @python2 - Scenario: Can select a Python 2.7 interpreter - When I select the Python Interpreter containing the name "2.7" - Then the python interpreter displayed in the the status bar contains the value "2.7" in the display name - - @python3 - Scenario: Can select a Python 3 interpreter - When I select the Python Interpreter containing the name "3." - Then the python interpreter displayed in the the status bar contains the value "3." in the display name diff --git a/uitests/uitests/features/interpreter/terminal.feature b/uitests/uitests/features/interpreter/terminal.feature deleted file mode 100644 index 7955f37ba033..000000000000 --- a/uitests/uitests/features/interpreter/terminal.feature +++ /dev/null @@ -1,34 +0,0 @@ -@terminal -@https://github.com/DonJayamanne/pyvscSmokeTesting/terminal -Feature: Terminal - Background: Activted Extension - Given the python extension has been activated - - @smoke - Scenario: Execute File in Terminal - Given the file "run in terminal.py" is open - And a file named "log.log" does not exist - When I select the command "Python: Run Python File in Terminal" - Then a file named "log.log" will be created - - Scenario: Execute Selection in Terminal - Given the file "run selection in terminal.py" is open - And a file named "log1.log" does not exist - And a file named "log2.log" does not exist - When I go to line 1 - And I select the command "Python: Run Selection/Line in Python Terminal" - Then a file named "log1.log" will be created - When I go to line 2 - And I select the command "Python: Run Selection/Line in Python Terminal" - Then a file named "log2.log" will be created - - Scenario: Execute Selection in Terminal using shift+enter - Given the file "run selection in terminal.py" is open - And a file named "log1.log" does not exist - And a file named "log2.log" does not exist - When I go to line 1 - And I press shift+enter - Then a file named "log1.log" will be created - When I go to line 2 - And I press shift+enter - Then a file named "log2.log" will be created diff --git a/uitests/uitests/features/interpreter/venv.feature b/uitests/uitests/features/interpreter/venv.feature deleted file mode 100644 index d984595e5143..000000000000 --- a/uitests/uitests/features/interpreter/venv.feature +++ /dev/null @@ -1,63 +0,0 @@ -# @terminal @terminal.venv @python3 -# @https://github.com/DonJayamanne/vscode-python-uitests/terminal/execution -# Feature: Terminal (venv) -# Scenario: Interpreter display name contains the name of the venv folder -# Given a venv with the name "venv 1" is created -# When In Mac, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" -# When In Linux, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" -# When In Windows, I update the workspace setting "python.pythonPath" with the value "venv 1/Scripts/python.exe" -# Then the python interpreter displayed in the the status bar contains the value "venv 1" in the display name - -# @preserve.workspace -# Scenario: Venv is auto selected -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" does not exist -# Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name -# When I reload VSC -# Then the python interpreter displayed in the the status bar contains the value "venv 1" in the display name - -# @preserve.workspace -# Scenario: Venv is not auto selected (if we already have a local interpreter selected) -# Given a generic Python Interpreter is selected -# And the user setting "python.pythonPath" does not exist -# Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name -# When I reload VSC -# Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name - -# @preserve.workspace -# Scenario: Venv is not auto selected (if we have a global interpreter selected) -# Given the workspace setting "python.pythonPath" does not exist -# And the user setting "python.pythonPath" exists -# Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name -# When I reload VSC -# Then the python interpreter displayed in the the status bar does not contain the value "venv 1" in the display name - -# @preserve.workspace -# Scenario: Environment is not activated in the Terminal -# When In Mac, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" -# When In Linux, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" -# When In Windows, I update the workspace setting "python.pythonPath" with the value "venv 1/Scripts/python.exe" -# Given the file "write_pyPath_in_log.py" is open -# And a file named "log.log" does not exist -# And the workspace setting "python.terminal.activateEnvironment" is disabled -# And a terminal is opened -# When I send the command "python write_pyPath_in_log.py" to the terminal -# Then a file named "log.log" will be created -# And open the file "log.log" -# And the file "log.log" does not contain the value "env 1" -# And take a screenshot - -# @preserve.workspace -# Scenario: Environment is activated in the Terminal -# When In Mac, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" -# When In Linux, I update the workspace setting "python.pythonPath" with the value "venv 1/bin/python" -# When In Windows, I update the workspace setting "python.pythonPath" with the value "venv 1/Scripts/python.exe" -# Given the file "write_pyPath_in_log.py" is open -# And a file named "log.log" does not exist -# And the workspace setting "python.terminal.activateEnvironment" is enabled -# And a terminal is opened -# When I send the command "python write_pyPath_in_log.py" to the terminal -# Then a file named "log.log" will be created -# And open the file "log.log" -# And the file "log.log" contains the value "env 1" -# And take a screenshot diff --git a/uitests/uitests/features/languageServer/basic.feature b/uitests/uitests/features/languageServer/basic.feature deleted file mode 100644 index 82b16156f887..000000000000 --- a/uitests/uitests/features/languageServer/basic.feature +++ /dev/null @@ -1,68 +0,0 @@ -@ls -@https://github.com/DonJayamanne/pvscSmokeLS.git -Feature: Language Server - Scenario Outline: Check output of 'Python' output panel when starting VS Code with Jedi <jedi_enabled> - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When I reload VS Code - And I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - - Examples: - | jedi_enabled | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | - | enabled | 5 | Jedi Python language engine | Jedi Python language engine | - | disabled | 120 | Microsoft Python language server | Initializing for | - - Scenario Outline: Language Server is downloaded with http.proxyStrictSSL set to true and false - When I open VS Code for the first time - Given the workspace setting "python.jediEnabled" is disabled - And the user setting "http.proxyStrictSSL" is <enabled_disabled> - When I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "Microsoft Python language server" will be displayed in the output panel within 120 seconds - Then the text "<protocol_to_look_for>" will be displayed in the output panel within 120 seconds - And the text "Initializing for" will be displayed in the output panel within 120 seconds - - Examples: - | enabled_disabled | protocol_to_look_for | - | enabled | https:// | - | disabled | http:// | - - @autoretry @smoke - Scenario Outline: Navigate to definition of a variable when extension has already been activated with Jedi <jedi_enabled> - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When I reload VS Code - And I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - When I open the file "my_sample.py" - And I go to line 3, column 10 - # Wait for intellisense to kick in (sometimes slow in jedi & ls) - And I wait for 5 seconds - When I select the command "Go to Definition" - Then the cursor is on line 1 - - Examples: - | jedi_enabled | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | - | enabled | 5 | Jedi Python language engine | Jedi Python language engine | - | disabled | 120 | Microsoft Python language server | Initializing for | - - @autoretry - Scenario Outline: Navigate to definition of a variable after opening a file with Jedi <jedi_enabled> - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When I open the file "my_sample.py" - And I select the command "Python: Show Output" - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - When I go to line 3, column 10 - # Wait for intellisense to kick in (sometimes slow in jedi & ls) - And I wait for 5 seconds - And I select the command "Go to Definition" - Then the cursor is on line 1 - - Examples: - | jedi_enabled | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | - | enabled | 5 | Jedi Python language engine | Jedi Python language engine | - | disabled | 120 | Microsoft Python language server | Initializing for | diff --git a/uitests/uitests/features/languageServer/goToDefinition.feature b/uitests/uitests/features/languageServer/goToDefinition.feature deleted file mode 100644 index ec93f36fe709..000000000000 --- a/uitests/uitests/features/languageServer/goToDefinition.feature +++ /dev/null @@ -1,62 +0,0 @@ -@ls -@https://github.com/DonJayamanne/pvscSmokeLS.git -Feature: Language Server - Scenario Outline: When <reload_or_start_vs_for_first_time> with Jedi <jedi_enabled> then output contains <first_text_in_ooutput_panel> - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When <reload_or_start_vs_for_first_time> - And I select the command "Python: Show Output" - And I wait for the Python extension to activate - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - - Examples: - | jedi_enabled | reload_or_start_vs_for_first_time | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | - | enabled | I open VS Code for the first time | 5 | Jedi Python language engine | Jedi Python language engine | - | enabled | I reload VS Code | 5 | Jedi Python language engine | Jedi Python language engine | - | disabled | I open VS Code for the first time | 120 | Microsoft Python language server | Initializing for | - | disabled | I open VS Code for the first time | 120 | Downloading | Initializing for | - | disabled | I reload VS Code | 120 | Microsoft Python language server | Initializing for | - - @autoretry - Scenario Outline: When <reload_or_start_vs_for_first_time> with Jedi <jedi_enabled> then navigate to definition of a variable - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When <reload_or_start_vs_for_first_time> - And I select the command "Python: Show Output" - And I wait for the Python extension to activate - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - When I open the file "my_sample.py" - And I go to line 3, column 10 - # Wait for intellisense to kick in (sometimes slow in jedi & ls) - And I wait for 5 seconds - And I select the command "Go to Definition" - Then the cursor is on line 1 - - Examples: - | jedi_enabled | reload_or_start_vs_for_first_time | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | - | enabled | I open VS Code for the first time | 5 | Jedi Python language engine | Jedi Python language engine | - | enabled | I reload VS Code | 5 | Jedi Python language engine | Jedi Python language engine | - | disabled | I open VS Code for the first time | 120 | Microsoft Python language server | Initializing for | - | disabled | I open VS Code for the first time | 120 | Downloading | Initializing for | - | disabled | I reload VS Code | 120 | Microsoft Python language server | Initializing for | - - @autoretry - Scenario Outline: When I open VS Code for the first time with Jedi <jedi_enabled>, open a file then navigate to definition of a variable - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When I open VS Code for the first time - And I select the command "Python: Show Output" - And I wait for the Python extension to activate - And I open the file "my_sample.py" - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - When I go to line 3, column 10 - # Wait for intellisense to kick in (sometimes slow in jedi & ls) - And I wait for 5 seconds - And I select the command "Go to Definition" - Then the cursor is on line 1 - - Examples: - | jedi_enabled | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | - | enabled | 5 | Jedi Python language engine | Jedi Python language engine | - | disabled | 120 | Microsoft Python language server | Initializing for | - | disabled | 120 | Downloading | Initializing for | diff --git a/uitests/uitests/features/languageServer/intellisense.feature b/uitests/uitests/features/languageServer/intellisense.feature deleted file mode 100644 index ee3fe00d2ab1..000000000000 --- a/uitests/uitests/features/languageServer/intellisense.feature +++ /dev/null @@ -1,75 +0,0 @@ -@ls -@https://github.com/DonJayamanne/pvscSmokeLS.git -Feature: Language Server - @autoretry - Scenario Outline: When <reload_or_start_vs_for_first_time> with Jedi <jedi_enabled> then intellisense works - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When <reload_or_start_vs_for_first_time> - And I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - When I open the file "intelli_sample.py" - # Wait for intellisense to kick in (sometimes slow in jedi & ls) - And I wait for <wait_time> seconds - And I go to line 3, column 13 - And I press ctrl+space - Then auto completion list will contain the item "excepthook" - And auto completion list will contain the item "exec_prefix" - And auto completion list will contain the item "executable" - When I go to line 11, column 21 - And I press ctrl+space - Then auto completion list will contain the item "age" - When I go to line 12, column 21 - And I press ctrl+space - Then auto completion list will contain the item "name" - When I go to line 17, column 10 - And I press ctrl+space - Then auto completion list will contain the item "say_something" - When I go to line 18, column 10 - And I press ctrl+space - Then auto completion list will contain the item "age" - When I go to line 19, column 10 - And I press ctrl+space - Then auto completion list will contain the item "name" - When I go to line 17, column 24 - And I press . - Then auto completion list will contain the item "capitalize" - And auto completion list will contain the item "count" - - Examples: - | jedi_enabled | reload_or_start_vs_for_first_time | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | wait_time | - | enabled | I open VS Code for the first time | 5 | Jedi Python language engine | Jedi Python language engine | 5 | - | enabled | I reload VS Code | 5 | Jedi Python language engine | Jedi Python language engine | 5 | - | disabled | I open VS Code for the first time | 120 | Microsoft Python language server | Initializing for | 5 | - | disabled | I reload VS Code | 120 | Microsoft Python language server | Initializing for | 5 | - - @autoretry - Scenario Outline: When <reload_or_start_vs_for_first_time> with Jedi <jedi_enabled> then intellisense works for untitled files - Given the workspace setting "python.jediEnabled" is <jedi_enabled> - When <reload_or_start_vs_for_first_time> - And I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "<first_text_in_ooutput_panel>" will be displayed in the output panel within <time_to_activate> seconds - And the text "<second_text_in_output_panel>" will be displayed in the output panel within <time_to_activate> seconds - When I create a new file with the following contents - """ - import sys - - print(sys.executable) - """ - And I change the language of the file to "Python" - # Wait for intellisense to kick in (sometimes slow in jedi & ls) - And I wait for <wait_time> seconds - And I go to line 3, column 13 - And I press ctrl+space - Then auto completion list will contain the item "excepthook" - And auto completion list will contain the item "exec_prefix" - And auto completion list will contain the item "executable" - - Examples: - | jedi_enabled | reload_or_start_vs_for_first_time | time_to_activate | first_text_in_ooutput_panel | second_text_in_output_panel | wait_time | - | enabled | I open VS Code for the first time | 5 | Jedi Python language engine | Jedi Python language engine | 5 | - | enabled | I reload VS Code | 5 | Jedi Python language engine | Jedi Python language engine | 5 | - | disabled | I open VS Code for the first time | 120 | Microsoft Python language server | Initializing for | 5 | - | disabled | I reload VS Code | 120 | Microsoft Python language server | Initializing for | 5 | diff --git a/uitests/uitests/features/languageServer/unresolvedImport.feature b/uitests/uitests/features/languageServer/unresolvedImport.feature deleted file mode 100644 index fe36db868562..000000000000 --- a/uitests/uitests/features/languageServer/unresolvedImport.feature +++ /dev/null @@ -1,42 +0,0 @@ -@ls -Feature: Language Server - Background: Unresolved imports - Given a file named "sample.py" is created with the following contents - """ - import requests - """ - Given the workspace setting "python.jediEnabled" is disabled - Given the package "requests" is not installed - When I reload VS Code - And I open the file "sample.py" - And I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "Initializing for" will be displayed in the output panel within 120 seconds - When I select the command "View: Focus Problems (Errors, Warnings, Infos)" - Then there is at least one problem in the problems panel - And there is a problem with the file named "sample.py" - And there is a problem with the message "unresolved import 'requests'" - - Scenario: Display problem about unresolved imports - Then do nothing - - Scenario: There should be no problem related to unresolved imports when reloading VSC - When I install the package "requests" - When I reload VS Code - # # Wait for some time for LS to detect this. - # And I wait for 5 seconds - And I open the file "sample.py" - And I wait for the Python extension to activate - And I select the command "Python: Show Output" - Then the text "Initializing for" will be displayed in the output panel within 120 seconds - When I select the command "View: Focus Problems (Errors, Warnings, Infos)" - # Ensure we are not too eager, possible LS hasn't analyzed yet. - And I wait for 10 seconds - Then there are no problems in the problems panel - - @skip - Scenario: Unresolved import message should go away when package is installed - When I install the package "requests" - # Wait for some time for LS to detect this new package. - And I wait for 10 seconds - Then there are no problems in the problems panel diff --git a/uitests/uitests/features/testing/discover.feature b/uitests/uitests/features/testing/discover.feature deleted file mode 100644 index 4f4e39ed0ded..000000000000 --- a/uitests/uitests/features/testing/discover.feature +++ /dev/null @@ -1,49 +0,0 @@ -@testing -Feature: Testing - Scenario Outline: Prompt to configure tests when not configured and attempting to discover tests - If user has not configured the extension for testing, then prompt to configure. - This should happen when selecting test specific commands. - Given the file ".vscode/settings.json" does not exist - When I wait for the Python extension to activate - And I select the command "<command>" - Then a message containing the text "No test framework configured" is displayed - - Examples: - | command | - | Python: Discover Tests | - | Python: Run All Tests | - | Python: Debug All Tests | - | Python: Debug Test Method ... | - | Python: Run Test Method ... | - | Python: Run Failed Tests | - - Scenario Outline: Prompt to install <package> when discovering tests - Given the package "<package>" is not installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - And I select the command "Python: Discover Tests" - Then a message containing the text "<message>" is displayed - - Examples: - | package | setting_to_enable | message | - | pytest | pytestEnabled | pytest is not installed | - | nose | nosetestsEnabled | nosetest is not installed | - - Scenario Outline: Display message if there are no tests (<package>) - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.<args_setting>": <args>, - "python.testing.<setting_to_enable>": true - } - """ - And the package "<package>" is installed - When I wait for the Python extension to activate - And I select the command "Python: Discover Tests" - Then a message containing the text "No tests discovered" is displayed - - Examples: - | package | setting_to_enable | args_setting | args | - | unittest | unittestEnabled | unittestArgs | ["-v","-s",".","-p","*test*.py"] | - | pytest | pytestEnabled | pytestArgs | ["."] | - | nose | nosetestsEnabled | nosetestArgs | ["."] | diff --git a/uitests/uitests/features/testing/explorer/basic.feature b/uitests/uitests/features/testing/explorer/basic.feature deleted file mode 100644 index a95e16aba384..000000000000 --- a/uitests/uitests/features/testing/explorer/basic.feature +++ /dev/null @@ -1,79 +0,0 @@ -@testing -@https://github.com/DonJayamanne/pyvscSmokeTesting/testing -Feature: Test Explorer - Background: Activted Extension - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.unittestEnabled": true, - "python.testing.pytestArgs": ["."], - "python.testing.pytestEnabled": false, - "python.testing.nosetestArgs": ["."], - "python.testing.nosetestsEnabled": false - } - """ - - Scenario Outline: Explorer will be displayed when tests are discovered (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - And I select the command "Python: Discover Tests" - Then the test explorer icon will be visible - - Examples: - | package | setting_to_enable | - | unittest | unittestEnabled | - | pytest | pytestEnabled | - | nose | nosetestsEnabled | - - Scenario Outline: All expected items are displayed in the test explorer (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - - Examples: - | package | setting_to_enable | node_count | - | unittest | unittestEnabled | 14 | - | pytest | pytestEnabled | 15 | - | nose | nosetestsEnabled | 14 | - - Scenario Outline: When discovering tests, the nodes will have the progress icon and clicking stop will stop discovery (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - # Now, add a delay for the discovery of the tests - Given a file named "tests/test_discovery_delay" is created with the following contents - """ - 10 - """ - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then all of the test tree nodes have a progress icon - And the stop icon is visible in the toolbar - When I stop discovering tests - Then the stop icon is not visible in the toolbar - - Examples: - | package | setting_to_enable | node_count | - | unittest | unittestEnabled | 14 | - | pytest | pytestEnabled | 15 | - | nose | nosetestsEnabled | 14 | diff --git a/uitests/uitests/features/testing/explorer/code.navigation.feature b/uitests/uitests/features/testing/explorer/code.navigation.feature deleted file mode 100644 index acf20b87fa74..000000000000 --- a/uitests/uitests/features/testing/explorer/code.navigation.feature +++ /dev/null @@ -1,74 +0,0 @@ -@testing -@https://github.com/DonJayamanne/pyvscSmokeTesting/testing -Feature: Test Explorer (code nav) - Background: Activted Extension - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.unittestEnabled": true, - "python.testing.pytestArgs": ["."], - "python.testing.pytestEnabled": false, - "python.testing.nosetestArgs": ["."], - "python.testing.nosetestsEnabled": false - } - """ - - Scenario Outline: When navigating to a test file, suite & test, then open the file and set the cursor at the right line (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - And I select the command "Python: Discover Tests" - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - And I navigate to the code associated with test node "<node_label>" - Then the file "<file>" is opened - And <optionally_check_line> - - Examples: - | package | setting_to_enable | node_label | file | optionally_check_line | - | unittest | unittestEnabled | test_one.py | test_one.py | nothing | - | unittest | unittestEnabled | test_one_first_suite | test_one.py | the cursor is on line 20 | - | unittest | unittestEnabled | test_three_first_suite | test_one.py | the cursor is on line 30 | - | unittest | unittestEnabled | test_two_first_suite | test_one.py | the cursor is on line 25 | - | pytest | pytestEnabled | test_one.py | test_one.py | nothing | - | pytest | pytestEnabled | test_one_first_suite | test_one.py | the cursor is on line 20 | - | pytest | pytestEnabled | test_three_first_suite | test_one.py | the cursor is on line 30 | - | pytest | pytestEnabled | test_two_first_suite | test_one.py | the cursor is on line 25 | - | nose | nosetestsEnabled | tests/test_one.py | test_one.py | nothing | - | nose | nosetestsEnabled | test_one_first_suite | test_one.py | the cursor is on line 20 | - | nose | nosetestsEnabled | test_three_first_suite | test_one.py | the cursor is on line 30 | - | nose | nosetestsEnabled | test_two_first_suite | test_one.py | the cursor is on line 25 | - - Scenario Outline: When selecting a node, then open the file (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - And I select the command "Python: Discover Tests" - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - When I click node "<node_label>" - Then the file "<file>" is opened - - Examples: - | package | setting_to_enable | node_label | file | - | unittest | unittestEnabled | TestFirstSuite | test_one.py | - | unittest | unittestEnabled | test_one_first_suite | test_one.py | - | unittest | unittestEnabled | test_three_first_suite | test_one.py | - | unittest | unittestEnabled | test_two_third_suite | test_two.py | - | pytest | pytestEnabled | TestFirstSuite | test_one.py | - | pytest | pytestEnabled | test_one_first_suite | test_one.py | - | pytest | pytestEnabled | test_three_first_suite | test_one.py | - | pytest | pytestEnabled | test_two_third_suite | test_two.py | - | nose | nosetestsEnabled | TestFirstSuite | test_one.py | - | nose | nosetestsEnabled | test_one_first_suite | test_one.py | - | nose | nosetestsEnabled | test_three_first_suite | test_one.py | - | nose | nosetestsEnabled | test_two_third_suite | test_two.py | diff --git a/uitests/uitests/features/testing/explorer/debug.feature b/uitests/uitests/features/testing/explorer/debug.feature deleted file mode 100644 index 27739d08590d..000000000000 --- a/uitests/uitests/features/testing/explorer/debug.feature +++ /dev/null @@ -1,137 +0,0 @@ -@testing -@https://github.com/DonJayamanne/pyvscSmokeTesting/testing -Feature: Test Explorer (debugging) - Background: Activted Extension - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.unittestEnabled": true, - "python.testing.pytestArgs": ["."], - "python.testing.pytestEnabled": false, - "python.testing.nosetestArgs": ["."], - "python.testing.nosetestsEnabled": false - } - """ - - Scenario Outline: When debugging tests, the nodes will have the progress icon and clicking stop will stop the debugger (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - # The number entered in this file will be used in a `time.sleep(?)` statement. - # Resulting in delays in running the tests (delay is in the python code in the above repo). - And a file named "tests/test_running_delay" is created with the following contents - """ - 5 - """ - When I wait for the Python extension to activate - And I select the command "Python: Discover Tests" - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - And <node_count> nodes have a status of "Unknown" - When I debug the test node "test_three_first_suite" - Then the debugger starts - When I select the command "Debug: Stop" - Then the debugger stops - - Examples: - | package | setting_to_enable | node_count | - | unittest | unittestEnabled | 14 | - | pytest | pytestEnabled | 15 | - | nose | nosetestsEnabled | 14 | - - - Scenario Outline: When debugging tests, only the specific function will be debugged (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - When I add a breakpoint to line 33 in "test_one.py" - And I add a breakpoint to line 23 in "test_one.py" - And I debug the test node "test_three_first_suite" - Then the debugger starts - And the debugger pauses - And the current stack frame is at line 33 in "test_one.py" - When I select the command "Debug: Continue" - Then the debugger stops - - Examples: - | package | setting_to_enable | - | unittest | unittestEnabled | - | pytest | pytestEnabled | - | nose | nosetestsEnabled | - - - Scenario Outline: When debugging tests, only the specific suite will be debugged (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - When I add a breakpoint to line 33 in "test_one.py" - And I add a breakpoint to line 28 in "test_one.py" - And I add a breakpoint to line 23 in "test_one.py" - And I debug the test node "TestFirstSuite" - Then the debugger starts - And the debugger pauses - And the current stack frame is at line 23 in "test_one.py" - When I select the command "Debug: Continue" - Then the debugger pauses - And the current stack frame is at line 33 in "test_one.py" - When I select the command "Debug: Continue" - Then the debugger pauses - And the current stack frame is at line 28 in "test_one.py" - When I select the command "Debug: Continue" - Then the debugger stops - - Examples: - | package | setting_to_enable | - | unittest | unittestEnabled | - | pytest | pytestEnabled | - | nose | nosetestsEnabled | - - - Scenario Outline: When debugging tests, everything will be debugged (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - When I add a breakpoint to line 23 in "test_one.py" - And I add a breakpoint to line 38 in "test_one.py" - And I add a breakpoint to line 23 in "test_two.py" - And I select the command "Python: Debug All Tests" - Then the debugger starts - And the debugger pauses - And the current stack frame is at line 23 in "test_one.py" - When I select the command "Debug: Continue" - Then the debugger pauses - And the current stack frame is at line 38 in "test_one.py" - When I select the command "Debug: Continue" - Then the debugger pauses - And the current stack frame is at line 23 in "test_two.py" - When I select the command "Debug: Continue" - Then the debugger stops - - Examples: - | package | setting_to_enable | - | unittest | unittestEnabled | - | pytest | pytestEnabled | - | nose | nosetestsEnabled | diff --git a/uitests/uitests/features/testing/explorer/run.failed.feature b/uitests/uitests/features/testing/explorer/run.failed.feature deleted file mode 100644 index 7406eaa08872..000000000000 --- a/uitests/uitests/features/testing/explorer/run.failed.feature +++ /dev/null @@ -1,132 +0,0 @@ -@testing -@https://github.com/DonJayamanne/pyvscSmokeTesting/testing -Feature: Test Explorer - Re-run Failed Tests - Background: Activted Extension - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.unittestEnabled": true, - "python.testing.pytestArgs": ["."], - "python.testing.pytestEnabled": false, - "python.testing.nosetestArgs": ["."], - "python.testing.nosetestsEnabled": false - } - """ - - Scenario Outline: We are able to re-run a failed tests (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - And a file named "tests/test_running_delay" is created with the following contents - """ - 0 - """ - And a file named "tests/data.json" is created with the following contents - """ - [1,-1,-1,4,5,6] - """ - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - And <node_count> nodes have a status of "Unknown" - When I select the command "Python: Run All Tests" - And I wait for tests to complete running - Then the node "<test_one_file_label>" has a status of "Fail" - And the node "TestFirstSuite" has a status of "Fail" - And the node "test_three_first_suite" has a status of "Fail" - And the node "test_two_first_suite" has a status of "Fail" - And the node "<test_two_file_label>" has a status of "Fail" - And the node "TestThirdSuite" has a status of "Fail" - And the node "test_three_third_suite" has a status of "Fail" - And the node "test_two_third_suite" has a status of "Fail" - And 6 nodes have a status of "Success" - And the run failed tests icon is visible in the toolbar - Given a file named "tests/test_running_delay" is created with the following contents - """ - 1 - """ - And a file named "tests/data.json" is created with the following contents - """ - [1,2,3,4,5,6] - """ - When I run failed tests - And I wait for tests to complete running - Then <node_count> nodes have a status of "Success" - - Examples: - | package | setting_to_enable | node_count | test_one_file_label | test_two_file_label | - | unittest | unittestEnabled | 14 | test_one.py | test_two.py | - | pytest | pytestEnabled | 15 | test_one.py | test_two.py | - | nose | nosetestsEnabled | 14 | tests/test_one.py | tests/test_two.py | - - Scenario Outline: We are able to stop tests after re-running failed tests (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - And a file named "tests/test_running_delay" is created with the following contents - """ - 0 - """ - And a file named "tests/data.json" is created with the following contents - """ - [1,-1,-1,4,5,6] - """ - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - And <node_count> nodes have a status of "Unknown" - When I select the command "Python: Run All Tests" - And I wait for tests to complete running - Then the node "<test_one_file_label>" has a status of "Fail" - And the node "TestFirstSuite" has a status of "Fail" - And the node "test_three_first_suite" has a status of "Fail" - And the node "test_two_first_suite" has a status of "Fail" - And the node "<test_two_file_label>" has a status of "Fail" - And the node "TestThirdSuite" has a status of "Fail" - And the node "test_three_third_suite" has a status of "Fail" - And the node "test_two_third_suite" has a status of "Fail" - And <failed_node_count> nodes have a status of "Success" - And the run failed tests icon is visible in the toolbar - Given a file named "tests/test_running_delay" is created with the following contents - """ - 100 - """ - And a file named "tests/data.json" is created with the following contents - """ - [1,2,3,4,5,6] - """ - When I run failed tests - Then the stop icon is visible in the toolbar - Then the node "TestFirstSuite" has a status of "Progress" - And the node "test_three_first_suite" has a status of "Progress" - And the node "test_two_first_suite" has a status of "Progress" - And the node "TestThirdSuite" has a status of "Progress" - And the node "test_three_third_suite" has a status of "Progress" - And the node "test_two_third_suite" has a status of "Progress" - And <failed_node_count> nodes have a status of "Progress" - When I stop running tests - And I wait for tests to complete running - Then the stop icon is not visible in the toolbar - And the node "test_three_first_suite" has a status of "Unknown" - And the node "test_two_first_suite" has a status of "Unknown" - And the node "test_three_third_suite" has a status of "Unknown" - And the node "test_two_third_suite" has a status of "Unknown" - - Examples: - | package | setting_to_enable | node_count | failed_node_count | test_one_file_label | test_two_file_label | - | unittest | unittestEnabled | 14 | 6 | test_one.py | test_two.py | - | pytest | pytestEnabled | 15 | 6 | test_one.py | test_two.py | - | nose | nosetestsEnabled | 14 | 6 | tests/test_one.py | tests/test_two.py | diff --git a/uitests/uitests/features/testing/explorer/run.progress.feature b/uitests/uitests/features/testing/explorer/run.progress.feature deleted file mode 100644 index be8fa42ac5e0..000000000000 --- a/uitests/uitests/features/testing/explorer/run.progress.feature +++ /dev/null @@ -1,46 +0,0 @@ -@testing -@https://github.com/DonJayamanne/pyvscSmokeTesting/testing -Feature: Test Explorer - Background: Activted Extension - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.unittestEnabled": true, - "python.testing.pytestArgs": ["."], - "python.testing.pytestEnabled": false, - "python.testing.nosetestArgs": ["."], - "python.testing.nosetestsEnabled": false - } - """ - - Scenario Outline: When running tests, the nodes will have the progress icon and clicking stop will stop running (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - And the file "tests/test_running_delay" has the following content - """ - 10 - """ - When I select the command "Python: Run All Tests" - Then all of the test tree nodes have a progress icon - And the stop icon is visible in the toolbar - When I stop running tests - Then the stop icon is not visible in the toolbar - - Examples: - | package | setting_to_enable | - | unittest | unittestEnabled | - | pytest | pytestEnabled | - | nose | nosetestsEnabled | diff --git a/uitests/uitests/features/testing/explorer/run.success.feature b/uitests/uitests/features/testing/explorer/run.success.feature deleted file mode 100644 index 3a7acc2f2925..000000000000 --- a/uitests/uitests/features/testing/explorer/run.success.feature +++ /dev/null @@ -1,88 +0,0 @@ -@testing -@https://github.com/DonJayamanne/pyvscSmokeTesting/testing -Feature: Test Explorer - Background: Activted Extension - Given a file named ".vscode/settings.json" is created with the following contents - """ - { - "python.testing.unittestArgs": [ - "-v", - "-s", - "./tests", - "-p", - "test_*.py" - ], - "python.testing.unittestEnabled": true, - "python.testing.pytestArgs": ["."], - "python.testing.pytestEnabled": false, - "python.testing.nosetestArgs": ["."], - "python.testing.nosetestsEnabled": false - } - """ - - Scenario Outline: When running tests, the nodes will have the progress icon and when completed will have a success status (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - And a file named "tests/test_running_delay" is created with the following contents - """ - 5 - """ - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - And <node_count> nodes have a status of "Unknown" - When I run the test node "test_two_first_suite" - Then the stop icon is visible in the toolbar - And 1 node has a status of "Progress" - And the node "test_two_first_suite" has a status of "Progress" - When I wait for tests to complete running - Then the node "<test_one_file_label>" has a status of "Success" - And the node "TestFirstSuite" has a status of "Success" - And the node "test_two_first_suite" has a status of "Success" - And 11 nodes have a status of "Unknown" - - Examples: - | package | setting_to_enable | node_count | test_one_file_label | - | unittest | unittestEnabled | 14 | test_one.py | - | pytest | pytestEnabled | 15 | test_one.py | - | nose | nosetestsEnabled | 14 | tests/test_one.py | - - - Scenario Outline: When running tests, the nodes will have the progress icon and when completed will have a error status (<package>) - Given the package "<package>" is installed - And the workspace setting "python.testing.<setting_to_enable>" is enabled - And a file named "tests/test_running_delay" is created with the following contents - """ - 5 - """ - And a file named "tests/data.json" is created with the following contents - """ - [1,2,-1,4,5,6] - """ - When I wait for the Python extension to activate - When I select the command "Python: Discover Tests" - And I wait for tests discovery to complete - Then the test explorer icon will be visible - When I select the command "View: Show Test" - And I expand all of the test tree nodes - Then there are <node_count> nodes in the tree - And <node_count> nodes have a status of "Unknown" - When I run the test node "test_three_first_suite" - Then the stop icon is visible in the toolbar - And 1 node has a status of "Progress" - And the node "test_three_first_suite" has a status of "Progress" - When I wait for tests to complete running - Then the node "<test_one_file_label>" has a status of "Fail" - And the node "TestFirstSuite" has a status of "Fail" - And the node "test_three_first_suite" has a status of "Fail" - And 11 nodes have a status of "Unknown" - - Examples: - | package | setting_to_enable | node_count | test_one_file_label | - | unittest | unittestEnabled | 14 | test_one.py | - | pytest | pytestEnabled | 15 | test_one.py | - | nose | nosetestsEnabled | 14 | tests/test_one.py | diff --git a/uitests/uitests/js/chromeDownloader.js b/uitests/uitests/js/chromeDownloader.js deleted file mode 100644 index 4d9acad1ba78..000000000000 --- a/uitests/uitests/js/chromeDownloader.js +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -const fs = require('fs') -const path = require('path') -const electronDownload = require('electron-download') -const extractZip = require('extract-zip') -const versionToDownload = process.argv.length > 2 ? process.argv[2] : '3.1.3'; -const downloadDir = process.argv.length > 3 ? process.argv[3] : path.join(__dirname, 'bin'); - -function download(version, callback) { - electronDownload({ - version, - chromedriver: true, - platform: process.env.npm_config_platform, - arch: process.env.npm_config_arch, - strictSSL: process.env.npm_config_strict_ssl === 'true', - quiet: ['info', 'verbose', 'silly', 'http'].indexOf(process.env.npm_config_loglevel) === -1 - }, callback) -} - -function processDownload(err, zipPath) { - if (err != null) throw err - extractZip(zipPath, { dir: downloadDir }, error => { - if (error != null) throw error - if (process.platform !== 'win32') { - fs.chmod(path.join(downloadDir, 'chromedriver'), '755', error => { - if (error != null) throw error - }) - } - }) -} - -download(versionToDownload, (err, zipPath) => { - if (err) { - const parts = versionToDownload.split('.') - const baseVersion = `${parts[0]}.${parts[1]}.0` - download(baseVersion, processDownload) - } else { - processDownload(err, zipPath) - } -}) diff --git a/uitests/uitests/js/report.js b/uitests/uitests/js/report.js deleted file mode 100644 index bc0824b3de83..000000000000 --- a/uitests/uitests/js/report.js +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -const reporter = require('cucumber-html-reporter'); -const path = require('path'); -const os = require('os'); -const reportsDir = process.argv[2]; -const launchReport = process.argv[3].toUpperCase() === 'TRUE'; - -const options = { - theme: 'bootstrap', - jsonFile: path.join(reportsDir, 'report.json'), - output: path.join(reportsDir, 'report.html'), - reportSuiteAsScenarios: true, - launchReport, - metadata: { - "Platform": os.platform() - } -}; - -reporter.generate(options, () => process.exit(0)); diff --git a/uitests/uitests/js/unzip.js b/uitests/uitests/js/unzip.js deleted file mode 100644 index 5b25fe3ba023..000000000000 --- a/uitests/uitests/js/unzip.js +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -'use strict'; - -const gulp = require('gulp'); -const fs = require("fs-extra"); -const vzip = require('gulp-vinyl-zip'); -const vfs = require('vinyl-fs'); -const untar = require('gulp-untar'); -const gunzip = require('gulp-gunzip'); -const chmod = require('gulp-chmod'); -const filter = require('gulp-filter'); - -const zipFile = process.argv[2]; -const targetDir = process.argv[3]; - -const unzipFn = (zipFile.indexOf('.gz') > 0 || zipFile.indexOf('.tag') > 0) ? unzipTarGz : unzip; - -unzipFn(zipFile, targetDir).catch(ex => { - console.error(ex); - return Promise.reject(ex); -}); - -async function unzip(zipFile, targetFolder) { - await fs.ensureDir(targetFolder); - return new Promise((resolve, reject) => { - gulp.src(zipFile) - .pipe(vzip.src()) - .pipe(vfs.dest(targetFolder)) - .on('end', resolve) - .on('error', reject); - }); -} - -async function unzipTarGz(zipFile, targetFolder) { - await fs.ensureDir(targetFolder); - return new Promise((resolve, reject) => { - var gulpFilter = filter(['VSCode-linux-x64/code', 'VSCode-linux-x64/code-insiders', 'VSCode-linux-x64/resources/app/node_modules*/vscode-ripgrep/**/rg'], { restore: true }); - gulp.src(zipFile) - .pipe(gunzip()) - .pipe(untar()) - .pipe(gulpFilter) - .pipe(chmod(493)) // 0o755 - .pipe(gulpFilter.restore) - .pipe(vfs.dest(targetFolder)) - .on('end', resolve) - .on('error', reject); - }); -} diff --git a/uitests/uitests/report.py b/uitests/uitests/report.py deleted file mode 100644 index 296ed1271739..000000000000 --- a/uitests/uitests/report.py +++ /dev/null @@ -1,262 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -# Original source can be found here: -# https://gist.github.com/fredizzimo/b92adf1d4596c0c1da1b05cc9899574b -# Code has been adopted to allow for adding attachments to cucumber reports. - -import base64 -import copy -import json - -import behave.formatter.base -import behave.model_core - - -class CucumberJSONFormatter(behave.formatter.base.Formatter): - instance = None - name = "json" - description = "JSON dump of test run" - dumps_kwargs = {} - - json_number_types = (int, float) - json_scalar_types = (str, bool, type(None)) - - def __new__(cls, stream_opener, config): - if cls.instance is None: - cls.instance = object.__new__(cls) - return cls.instance - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.stream = self.open() - self.feature_count = 0 - self.attachments = [] - self.reset() - - @property - def current_feature_element(self): - assert self.current_feature_data is not None - return self.current_feature_data["elements"][-1] - - @property - def current_step(self): - step_index = self._step_index - if self.current_feature.background is not None: - element = self.current_feature_data["elements"][-2] - if step_index >= len(self.current_feature.background.steps): - step_index -= len(self.current_feature.background.steps) - element = self.current_feature_element - else: - element = self.current_feature_element - - return element["steps"][step_index] - - def reset(self): - self.current_feature = None - self.current_feature_data = None - self._step_index = 0 - self.current_background = None - - def uri(self, uri): - pass - - def status(self, status_obj): - if status_obj == behave.model_core.Status.passed: - return "passed" - elif status_obj == behave.model_core.Status.failed: - return "failed" - else: - return "skipped" - - def feature(self, feature): - self.reset() - self.current_feature = feature - self.current_feature_data = { - "id": self.generate_id(feature), - "uri": feature.location.filename, - "line": feature.location.line, - "description": "", - "keyword": feature.keyword, - "name": feature.name, - "tags": self.write_tags(feature.tags), - "status": self.status(feature.status), - } - element = self.current_feature_data - if feature.description: - element["description"] = self.format_description(feature.description) - - def background(self, background): - element = { - "type": "background", - "keyword": background.keyword, - "name": background.name, - "location": str(background.location), - "steps": [], - } - self._step_index = 0 - self.current_background = element - - def scenario(self, scenario): - if self.current_background is not None: - self.add_feature_element(copy.deepcopy(self.current_background)) - element = self.add_feature_element( - { - "type": "scenario", - "id": self.generate_id(self.current_feature, scenario), - "line": scenario.location.line, - "description": "", - "keyword": scenario.keyword, - "name": scenario.name, - "tags": self.write_tags(scenario.tags), - "location": str(scenario.location), - "steps": [], - } - ) - if scenario.description: - element["description"] = self.format_description(scenario.description) - self._step_index = 0 - - def step(self, step): - self.attachments.clear() - step_info = { - "keyword": step.keyword, - "step_type": step.step_type, - "name": step.name, - "line": step.location.line, - "result": {"status": "skipped", "duration": 0}, - "embeddings": [], - "text": "", # This is required by the cucumber js reporter. - # We need to make it non-empty when attaching stuff. - } - - if step.text: - step_info["doc_string"] = {"value": step.text, "line": step.text.line} - if step.table: - step_info["rows"] = [ - {"cells": [heading for heading in step.table.headings]} - ] - step_info["rows"] += [ - {"cells": [cell for cell in row.cells]} for row in step.table - ] - - if self.current_feature.background is not None: - element = self.current_feature_data["elements"][-2] - if len(element["steps"]) >= len(self.current_feature.background.steps): - element = self.current_feature_element - else: - element = self.current_feature_element - element["steps"].append(step_info) - - def match(self, match): - if match.location: - match_data = {"location": str(match.location) or ""} - self.current_step["match"] = match_data - - def attach_image(self, base64): - self.attachments.append({"mime_type": "image/png", "data": base64}) - - def attach_html(self, html): - self.attachments.append({"mime_type": "text/html", "data": html}) - - def result(self, result): - self.current_step["embeddings"] = self.attachments.copy() - - # Ensure step.text is non-empty, else cucumber js reporter won't embed them correctly. - if any(self.current_step["embeddings"]): - self.current_step["text"] = "More:" - - self.attachments.clear() - self.current_step["result"] = { - "status": self.status(result.status), - "duration": int(round(result.duration * 1000.0 * 1000.0 * 1000.0)), - } - if result.error_message and result.status == "failed": - error_message = result.error_message - result_element = self.current_step["result"] - result_element["error_message"] = error_message - self._step_index += 1 - - def embedding(self, mime_type, data): - step = self.current_feature_element["steps"][-1] - step["embeddings"].append( - {"mime_type": mime_type, "data": base64.b64encode(data).replace("\n", "")} - ) - - def eof(self): - """ - End of feature - """ - if not self.current_feature_data: - return - - self.update_status_data() - - if self.feature_count == 0: - self.write_json_header() - else: - self.write_json_feature_separator() - - self.write_json_feature(self.current_feature_data) - self.current_feature_data = None - self.feature_count += 1 - - def close(self): - self.write_json_footer() - self.close_stream() - - def add_feature_element(self, element): - assert self.current_feature_data is not None - if "elements" not in self.current_feature_data: - self.current_feature_data["elements"] = [] - self.current_feature_data["elements"].append(element) - return element - - def update_status_data(self): - assert self.current_feature - assert self.current_feature_data - self.current_feature_data["status"] = self.status(self.current_feature.status) - - def write_tags(self, tags): - return [ - {"name": tag, "line": tag.line if hasattr(tag, "line") else 1} - for tag in tags - ] - - def generate_id(self, feature, scenario=None): - def convert(name): - return name.lower().replace(" ", "-") - - id = convert(feature.name) - if scenario is not None: - id += ";" - id += convert(scenario.name) - return id - - def format_description(self, lines): - description = "\n".join(lines) - description = "<pre>%s</pre>" % description - return description - - def write_json_header(self): - self.stream.write("[\n") - - def write_json_footer(self): - self.stream.write("\n]\n") - - def write_json_feature(self, feature_data): - self.stream.write(json.dumps(feature_data, **self.dumps_kwargs)) - self.stream.flush() - - def write_json_feature_separator(self): - self.stream.write(",\n\n") - - -class PrettyCucumberJSONFormatter(CucumberJSONFormatter): - """ - Provides readable/comparable textual JSON output. - """ - - name = "json.pretty" - description = "JSON dump of test run (human readable)" - dumps_kwargs = {"indent": 4, "sort_keys": True} diff --git a/uitests/uitests/steps/commands.py b/uitests/uitests/steps/commands.py deleted file mode 100644 index 878a8f5d7bcd..000000000000 --- a/uitests/uitests/steps/commands.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import behave - -import uitests.vscode.quick_open - - -@behave.given('the command "{command}" is selected') -def given_command_selected(context, command): - """Select a command from the command palette. - - Parameters: - command (string): Command to be selected - - """ - uitests.vscode.quick_open.select_command(context, command) - - -@behave.when('I select the command "{command}"') -def when_select_command(context, command): - """Select a command from the command palette. - - Parameters: - command (string): Command to be selected - - """ - uitests.vscode.quick_open.select_command(context, command) - - -@behave.then('select the command "{command}"') -def then_select_command(context, command): - """Select a command from the command palette. - - Parameters: - command (string): Command to be selected - - """ - uitests.vscode.quick_open.select_command(context, command) diff --git a/uitests/uitests/steps/core.py b/uitests/uitests/steps/core.py deleted file mode 100644 index d7356e2e85cd..000000000000 --- a/uitests/uitests/steps/core.py +++ /dev/null @@ -1,289 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import sys -import time - -import behave -from selenium.webdriver.common.keys import Keys - -import uitests.tools -import uitests.vscode.application -import uitests.vscode.core -import uitests.vscode.extension -import uitests.vscode.quick_open - - -@behave.given("In Windows,{step}") -def given_on_windows(context, step): - """Executes a `Give` step when on Windows. - - Parameters: - step (string): `Given` Step to be executed. - - """ - if not sys.platform.startswith("win"): - return - context.execute_steps(f"Given {step.strip()}") - - -@behave.given("In Mac,{step}") -def given_on_mac(context, step): - """Executes a `Give` step when on Mac. - - Parameters: - step (string): `Given` Step to be executed. - - """ - if not sys.platform.startswith("darwin"): - return - context.execute_steps(f"Given {step.strip()}") - - -@behave.given("In Linux,{step}") -def given_on_linux(context, step): - """Executes a `Give` step when on Linux. - - Parameters: - step (string): `Given` Step to be executed. - - """ - if not sys.platform.startswith("linux"): - return - context.execute_steps(f"When {step.strip()}") - - -@behave.when("In Windows,{step}") -def when_on_widows(context, step): - """Executes a `When` step when on Windows. - - Parameters: - step (string): `When` Step to be executed. - - """ - if not sys.platform.startswith("win"): - return - context.execute_steps(f"When {step.strip()}") - - -@behave.when("In Mac,{step}") -def when_on_mac(context, step): - """Executes a `When` step when on Mac. - - Parameters: - step (string): `When` Step to be executed. - - """ - if not sys.platform.startswith("darwin"): - return - context.execute_steps(f"When {step.strip()}") - - -@behave.when("In Linux,{step}") -def when_on_linux(context, step): - """Executes a `When` step when on Linux. - - Parameters: - step (string): `When` Step to be executed. - - """ - if not sys.platform.startswith("linux"): - return - context.execute_steps(f"When {step.strip()}") - - -@behave.then("In Windows,{step}") -def then_on_windows(context, step): - """Executes a `Then` step when on Widows. - - Parameters: - step (string): `Then` Step to be executed. - - """ - if not sys.platform.startswith("win"): - return - context.execute_steps(f"Then {step.strip()}") - - -@behave.then("In Mac,{step}") -def then_on_mac(context, step): - """Executes a `Then` step when on Mac. - - Parameters: - step (string): `Then` Step to be executed. - - """ - if not sys.platform.startswith("darwin"): - return - context.execute_steps(f"Then {step.strip()}") - - -@behave.then("In Linux,{step}") -def then_on_linux(context, step): - """Executes a `Then` step when on Linux. - - Parameters: - step (string): `Then` Step to be executed. - - """ - if not sys.platform.startswith("linux"): - return - context.execute_steps(f"Then {step.strip()}") - - -@behave.when("I wait for {seconds:g} seconds") -def when_sleep(context, seconds): - """Wait for n seconds. - - Parameters: - seconds (int): Time in seconds to wait. - - """ - time.sleep(seconds) - - -@behave.when("I wait for 1 second") -def when_sleep1(context): - """Wait for n seconds. - - Parameters: - seconds (int): Time in seconds to wait. - - """ - time.sleep(1) - - -@behave.then("nothing") -def then_nothing(context): - """Do nothing.""" - pass - - -@behave.then("do nothing") -def then_do_nothing(context): - """Do nothing.""" - pass - - -@behave.when("I reload VSC") -def when_reload_vsc(context): - """Reload VS Code.""" - uitests.vscode.application.reload(context) - - -@behave.when("I open VS Code for the first time") -def when_open_vscode_first_time(context): - """Delete the user folder. - Delete the language server folder - (that's pretty much same as Opening VSC from scratch). - - """ - uitests.vscode.application.exit(context) - uitests.vscode.application.clear_vscode(context.options) - uitests.vscode.application.reload(context) - - -@behave.when("I reload VS Code") -def when_reload_vscode(context): - """Reload VS Code.""" - uitests.vscode.application.reload(context) - - -@behave.then("reload VSC") -def then_reload_vsc(context): - """Reload VS Code.""" - uitests.vscode.application.reload(context) - - -@behave.then("reload VS Code") -def then_reload_vscode(context): - """Reload VS Code.""" - uitests.vscode.application.reload(context) - - -@behave.then("wait for {seconds:g} seconds") -def then_sleep(context, seconds): - """Wait for n seconds. - - Parameters: - seconds (int): Time in seconds to wait. - - """ - time.sleep(1) - - time.sleep(seconds) - - -@behave.then("wait for 1 second") -def then_sleep1(context, seconds): - """Wait for n seconds. - - Parameters: - seconds (int): Time in seconds to wait. - - """ - time.sleep(1) - - time.sleep(seconds) - - -@behave.then('log the message "{message}"') -def log_message(context, message): - """Logs a message to stdout. - - Parameters: - message (string): Message to be logged. - - """ - time.sleep(1) - - logging.info(message) - - -@behave.then("take a screenshot") -def capture_screen(context): - """Caprtures a screenshot.""" - uitests.vscode.application.capture_screen(context) - - -@behave.when("I wait for the Python extension to activate") -def when_extension_has_loaded(context): - """Activate the Python extension and wait for it to complete activating.""" - uitests.vscode.extension.activate_python_extension(context) - - -def _get_key(key): - if key.lower() == "ctrl": - return Keys.CONTROL - if key.lower() == "cmd": - return Keys.COMMAND - return getattr(Keys, key.upper(), key) - - -@behave.when("I press {key_combination}") -def when_I_press(context, key_combination): - """Press a Key. - Supports one key or a combination of keys. - E.g. I press A, I press ctrl, I press space, I press ctrl+space - - Parameters: - key_combination (string): Key or a combination of keys. - - """ - keys = map(_get_key, key_combination.split("+")) - uitests.vscode.core.dispatch_keys(context.driver, *list(keys)) - - -@behave.given("the Python extension has been activated") -def given_extension_has_loaded(context): - """Activate the Python extension and wait for it to complete activating.""" - uitests.vscode.extension.activate_python_extension(context) - - -@behave.then('the text "{text}" is displayed in the Interactive Window') -def text_on_screen(context, text): - """Checks whether some text is displayed in the Interactive Window.""" - text_on_screen = uitests.vscode.screen.get_screen_text(context) - if text not in text_on_screen: - raise SystemError(f"{text} not found in {text_on_screen}") diff --git a/uitests/uitests/steps/debugger.py b/uitests/uitests/steps/debugger.py deleted file mode 100644 index 093b16135d94..000000000000 --- a/uitests/uitests/steps/debugger.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import behave - -import uitests.tools -import uitests.vscode.debugger - - -@behave.then("the debugger starts") -def then_starts(context): - uitests.vscode.debugger.wait_for_debugger_to_start(context) - - -@behave.then("the debugger stops") -def then_stops(context): - uitests.vscode.debugger.wait_for_debugger_to_stop(context) - - -@behave.then("the debugger will stop within {seconds:d} seconds") -def then_stops_in_seconds(context, seconds): - uitests.vscode.debugger.wait_for_debugger_to_stop( - context, retry_count=seconds * 1000 / 100, retry_interval=0.1 - ) - - -@behave.then("the debugger pauses") -def then_pauses(context): - uitests.vscode.debugger.wait_for_debugger_to_pause(context) - - -@behave.when('I add a breakpoint to line {line:Number} in "{file}"') -def add_breakpoint(context, line, file): - uitests.vscode.debugger.add_breakpoint(context, file, line) - - -@behave.then('the current stack frame is at line {line_number:Number} in "{file_name}"') -@uitests.tools.retry(AssertionError) -def current_stack_is(context, line_number, file_name): - uitests.vscode.documents.is_file_open(context, file_name) - line = uitests.vscode.documents.get_current_line(context) - assert line == line_number, f"{line} != {line_number}" - - -@behave.then("the Python Debug Configuration picker is displayed") -@uitests.tools.retry(AssertionError, tries=5, delay=1) -def python_debug_picker(context): - uitests.vscode.debugger.wait_for_python_debug_config_picker(context) - - -@behave.when('I select the debug configuration "{label}"') -def select_debug_config(context, label): - uitests.vscode.quick_input.select_value(context, label) - - -# @behave.then( -# 'the current stack frame is not at line {line_number:Number} in "{file_name}"' -# ) -# def current_stack_is_not(context, line_number, file_name): -# try: -# current_frame = uitests.vscode.debugger.get_current_frame_position(context) -# assert current_frame[0] != file_name -# assert current_frame[1] != line_number -# except Exception: -# pass diff --git a/uitests/uitests/steps/documents.py b/uitests/uitests/steps/documents.py deleted file mode 100644 index 58caeb8d5ecb..000000000000 --- a/uitests/uitests/steps/documents.py +++ /dev/null @@ -1,157 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os.path -import time - -import behave -import uitests.tools -import uitests.vscode.documents - - -@behave.given('a file named "{name}" is created with the following contents') -def given_file_create(context, name): - uitests.vscode.documents.create_file_with_contents(context, name, context.text) - - -@behave.when('the file "{name}" has the following content') -def when_file_with_content(context, name): - uitests.vscode.documents.create_file_with_contents(context, name, context.text) - - -@behave.when("I create a new file with the following contents") -def when_new_file_with_content(context): - uitests.vscode.documents.create_new_untitled_file_with_contents( - context, context.text - ) - - -@behave.given('a file named "{name}" does not exist') -def given_file_no_exist(context, name): - try: - os.unlink(os.path.join(context.options.workspace_folder, name)) - except Exception: - pass - - -@behave.given('the file "{name}" does not exist') -def given_the_file_no_exist(context, name): - try: - os.unlink(os.path.join(context.options.workspace_folder, name)) - except Exception: - pass - - -@behave.then('a file named "{name}" will be created') -@uitests.tools.retry(AssertionError) -def then_file_exists(context, name): - assert os.path.exists( - os.path.join(context.options.workspace_folder, name) - ), os.path.join(context.options.workspace_folder, name) - - -@behave.then('a file named "{name}" will be created within {time:Number} seconds') -def then_file_exists_retry(context, name, time): - @uitests.tools.retry(AssertionError, tries=time, delay=1) - def check(context, name): - assert os.path.exists( - os.path.join(context.options.workspace_folder, name) - ), os.path.join(context.options.workspace_folder, name) - - check(context, name) - - -@behave.given('the file "{name}" is open') -def given_file_opened(context, name): - uitests.vscode.documents.open_file(context, name) - - -@behave.then('the file "{name}" is opened') -def then_file_opened(context, name): - uitests.vscode.documents.is_file_open(context, name) - - -@behave.when("I go to line {line_number:d}") -def when_go_to_line(context, line_number): - # Wait for 1/2 second, else things happen too quickly. - time.sleep(0.5) - uitests.vscode.documents.go_to_line(context, line_number) - - -@behave.when("I go to line {line_number:d}, column {column:d}") -def when_go_to_line_column(context, line_number, column): - # Wait for 1/2 second, else things happen too quickly. - time.sleep(0.5) - uitests.vscode.documents.go_to_line_column(context, line_number, column) - - -@behave.then("the cursor is on line {line_number:d}") -@uitests.tools.retry(AssertionError, tries=30, delay=1) -def then_line(context, line_number): - line = uitests.vscode.documents.get_current_line(context) - assert line == line_number, AssertionError(f"{line} != {line_number}") - - -@behave.then("the cursor is on line {line_number:Number} and column {column:Number}") -@uitests.tools.retry(AssertionError, tries=30, delay=1) -def then_line_and_column(context, line_number, column): - value = uitests.vscode.documents.get_current_position(context) - assert line_number == value[0], f"{line_number} != {value[0]}" - assert column == value[0], f"{column} != {value[0]}" - - -@behave.then('the file "{name}" contains the value "{value}"') -@uitests.tools.retry(AssertionError) -def file_contains(context, name, value): - file_name = os.path.join(context.options.workspace_folder, name) - with open(file_name, "r") as file: - contents = file.read() - assert value in contents, f"{value} not in {contents}" - - -@behave.then('the file "{name}" does not contain the value "{value}"') -@uitests.tools.retry(AssertionError) -def file_not_contains(context, name, value): - file_name = os.path.join(context.options.workspace_folder, name) - with open(file_name, "r") as file: - contents = file.read() - assert value not in contents, f"{value} not in {contents}" - - -@behave.when('I open the file "{name}"') -def when_file_opened(context, name): - uitests.vscode.documents.open_file(context, name) - - -@behave.then('open the file "{name}"') -def then_open_file(context, name): - uitests.vscode.documents.open_file(context, name) - - -@behave.when("I create an untitled Python file with the following contents") -def create_untitled_python_file(context): - create_new_python_file(context) - - -@behave.when("I change the language of the file to {language}") -def change_language(context, language): - """You could either quote the language within " or not.""" - uitests.vscode.documents.change_document_language( - context, language=language.strip('"') - ) - - -@behave.when("I create an new Python file with the following contents") -def create_new_python_file(context): - uitests.vscode.documents.create_new_untitled_file(context) - uitests.vscode.documents.send_text_to_editor(context, "Untitled-1", context.text) - - -@behave.then("auto completion list will contain the item {label}") -@uitests.tools.retry(AssertionError) -def auto_complete_list_contains(context, label): - """You could either quote the label within " or not.""" - items = uitests.vscode.documents.get_completion_list(context) - label = label.strip('"').lower() - contents = "".join(items).lower() - assert label in contents, f"{label} not in {contents}" diff --git a/uitests/uitests/steps/interpreter.py b/uitests/uitests/steps/interpreter.py deleted file mode 100644 index dcd9a108c69d..000000000000 --- a/uitests/uitests/steps/interpreter.py +++ /dev/null @@ -1,87 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import os.path -import sys - -import behave - -import uitests.tools -import uitests.vscode.quick_input -import uitests.vscode.quick_open -import uitests.vscode.settings - - -@behave.given('a Python Interpreter containing the name "{name}" is selected') -def given_select_interpreter_with_name(context, name): - uitests.vscode.quick_open.select_command(context, "Python: Select Interpreter") - uitests.vscode.quick_input.select_value(context, name) - - -@behave.given('a venv with the name "{name}" is created') -def given_venv_created(context, name): - context.execute_steps("Given a terminal is opened") - context.execute_steps( - f'When I send the command ""{context.options.python3_path}" -m venv "{name}"" to the terminal' - ) - uitests.tools.wait_for_python_env(context.options.workspace_folder, name) - - -@behave.given("a pipenv environment is created") -def given_pipenv_created(context): - context.execute_steps("Given a terminal is opened") - context.execute_steps( - f'When I send the command "pipenv shell --anyway" to the terminal' - ) - uitests.tools.wait_for_pipenv(context.options.workspace_folder) - - -@behave.given('a conda environment is created with the name "{name}"') -def given_conda_env_created(context, name): - context.execute_steps("Given a terminal is opened") - context.execute_steps( - f'When I send the command ""{context.options.conda_path}" create --yes --name "{name}"" to the terminal' - ) - uitests.tools.wait_for_conda_env(context.options.conda_path, name) - - -@behave.given("a generic Python Interpreter is selected") -def given_select_generic_interpreter(context): - uitests.vscode.settings.update_workspace_settings( - context, {"python.pythonPath": sys.executable} - ) - - -@behave.when('I select the Python Interpreter containing the name "{name}"') -def when_select_interpreter_with_name(context, name): - uitests.vscode.quick_open.select_command(context, "Python: Select Interpreter") - uitests.vscode.quick_input.select_value(context, name) - - -@behave.when("I select the default mac Interpreter") -def select_interpreter(context): - uitests.vscode.quick_open.select_command(context, "Python: Select Interpreter") - uitests.vscode.quick_input.select_value(context, "/usr/bin/python") - - -@behave.then( - 'the contents of the file "{name}" does not contain the current python interpreter' -) -def file_not_contains_interpreter(context, name): - with open(os.path.join(context.options.workspace_folder, name), "r") as file: - contents = file.read() - assert ( - context.options.python_path not in contents - ), f"{context.options.python_path} in {contents}" - - -@behave.then( - 'the contents of the file "{name}" contains the current python interpreter' -) -def file_contains_interpreter(context, name): - with open(os.path.join(context.options.workspace_folder, name), "r") as file: - contents = file.read() - assert ( - context.options.python_path in contents - ), f"{context.options.python_path} not in {contents}" diff --git a/uitests/uitests/steps/notifications.py b/uitests/uitests/steps/notifications.py deleted file mode 100644 index ce695ef2b078..000000000000 --- a/uitests/uitests/steps/notifications.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import behave -import uitests.vscode.notifications - - -@behave.then('a message with the text "{message}" is displayed') -def show_message(context, message): - uitests.vscode.notifications.wait_for_message(context, message) - - -@behave.then('a message containing the text "{message}" is displayed') -def show_message_containing(context, message): - uitests.vscode.notifications.wait_for_message_containing(context, message) - - -@behave.then('dismiss the message containing the text "{message}"') -def dismiss_message(context, message): - uitests.vscode.notifications.dismiss_message(context, message) - - -@behave.then('click "{button}" button on the message containing the text "{message}"') -def dismiss_message_with_button(context, button, message): - uitests.vscode.notifications.dismiss_message(context, message, button) diff --git a/uitests/uitests/steps/output_panel.py b/uitests/uitests/steps/output_panel.py deleted file mode 100644 index fba8a0d884d0..000000000000 --- a/uitests/uitests/steps/output_panel.py +++ /dev/null @@ -1,43 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import behave - -import uitests.tools -import uitests.vscode.output_panel - - -@behave.then('the output panel contains the text "{text}"') -@uitests.tools.retry(AssertionError) -def then_output_contains(context, text): - """Add retries, e.g. download LS can be slow on CI""" - - then_output_contains_within(context, text, seconds=100, delay=0.1) - - -@behave.then( - 'the text "{text}" will be displayed in the output panel within {seconds:n} seconds' -) -def then_output_contains_within(context, text, seconds=1000, delay=0.1): - """Add retries, e.g. download LS can be slow on CI""" - - # Append messages, in case the list is large. - messages_seen_thus_far = "" - - @uitests.tools.retry(AssertionError, tries=seconds, delay=delay) - def check_output(context, text): - lines = uitests.vscode.output_panel.get_output_panel_lines(context) - text = text.strip('"').lower() - nonlocal messages_seen_thus_far - messages_seen_thus_far = messages_seen_thus_far + "".join(lines).lower() - assert text in messages_seen_thus_far, f"{text} not in {messages_seen_thus_far}" - - try: - # Maximize the panel so we can see everything in the panel. - # Lines in output panels are virtualized. - uitests.vscode.output_panel.maximize_bottom_panel(context) - check_output(context, text) - except Exception: - raise - finally: - uitests.vscode.output_panel.minimize_bottom_panel(context) diff --git a/uitests/uitests/steps/packages.py b/uitests/uitests/steps/packages.py deleted file mode 100644 index eeb6af057216..000000000000 --- a/uitests/uitests/steps/packages.py +++ /dev/null @@ -1,56 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import behave -import uitests.tools - - -@behave.given('the package "{name}" is not installed') -def given_no_package(context, name): - _uninstall_module(context, name) - - -@behave.when('I uninstall the package "{name}"') -def when_no_package(context, name): - _uninstall_module(context, name) - - -@behave.then('uninstall the package "{name}"') -def then_no_package(context, name): - _uninstall_module(context, name) - - -@behave.given('the package "{name}" is installed') -def given_package_installed(context, name): - _install_module(context, name) - - -@behave.when('I install the package "{name}"') -def when_package_installed(context, name): - _install_module(context, name) - - -@behave.then('install the package "{name}"') -def then_package_installed(context, name): - _install_module(context, name) - - -def _uninstall_module(context, name): - python_path = context.options.python_path - try: - uitests.tools.run_command( - [python_path, "-m", "pip", "uninstall", name, "-y", "-q"], silent=True - ) - except Exception: - pass - - -def _install_module(context, name): - python_path = context.options.python_path - try: - uitests.tools.run_command( - [python_path, "-m", "pip", "install", name, "-q"], silent=True - ) - except Exception: - pass diff --git a/uitests/uitests/steps/problems.py b/uitests/uitests/steps/problems.py deleted file mode 100644 index 99084dd61bf4..000000000000 --- a/uitests/uitests/steps/problems.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import behave -from selenium.common.exceptions import StaleElementReferenceException - -import uitests.vscode.status_bar -import uitests.tools -import uitests.vscode.problems - - -@behave.then("there are no problems in the problems panel") -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_no_problems(context): - count = uitests.vscode.problems.get_problem_count(context) - assert count == 0, f"Number of problems is {count}" - - -@behave.then("there is at least one problem in the problems panel") -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_atleast_one_problem(context): - count = uitests.vscode.problems.get_problem_count(context) - assert count > 0 - - -@behave.then("there are at least {problem_count:Number} problems in the problems panel") -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_atleast_n_problems(context, problem_count): - count = uitests.vscode.problems.get_problem_count(context) - assert problem_count >= count, f"{problem_count} should be >= {count}" - - -@behave.then('there is a problem with the message "{message}"') -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_has_problem_with_message(context, message): - messages = uitests.vscode.problems.get_problems(context) - all_problems = "".join(messages) - assert message in all_problems, f"{message} not in {all_problems}" - - -@behave.then('there is a problem with the file named "{name}"') -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_has_poroblem_in_filename(context, name): - files = uitests.vscode.problems.get_problem_files(context) - all_files = "".join(files) - assert name in all_files, f"{all_files} not in {all_files}" diff --git a/uitests/uitests/steps/sample.py b/uitests/uitests/steps/sample.py deleted file mode 100644 index bd5cacead899..000000000000 --- a/uitests/uitests/steps/sample.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import behave -import uitests.vscode.application - - -@behave.given("we have behave installed") -def step_impl(context): - uitests.vscode.application.capture_screen(context) - pass - - -@behave.when("we implement a test") -def implement_test(context): - assert True is not False - - -@behave.then("behave will test it for us!") -def test_it(context): - uitests.vscode.application.capture_screen(context) - assert True - - -@behave.then("Another one!") -def another(context): - assert True diff --git a/uitests/uitests/steps/settings.py b/uitests/uitests/steps/settings.py deleted file mode 100644 index 091c93c8e3e5..000000000000 --- a/uitests/uitests/steps/settings.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import behave - -import uitests.vscode.settings - - -@behave.given('the workspace setting "{name}" has the value "{value}"') -def given_workspace_setting(context, name, value): - uitests.vscode.settings.update_workspace_settings(context, {name: value}) - - -@behave.given('the workspace setting "{name}" has the value {value:Number}') -def given_workspace_setting_is_value(context, name, value): - uitests.vscode.settings.update_workspace_settings(context, {name: value}) - - -@behave.given('the workspace setting "{name}" is enabled') -def given_workspace_setting_enabled(context, name): - uitests.vscode.settings.update_workspace_settings(context, {name: True}) - - -@behave.given('the workspace setting "{name}" is disabled') -def given_workspace_setting_disabled(context, name): - uitests.vscode.settings.update_workspace_settings(context, {name: False}) - - -@behave.given('the user setting "{name}" is disabled') -def given_user_setting_disabled(context, name): - uitests.vscode.settings.update_user_settings(context, {name: False}) - - -@behave.given('the user setting "{name}" is enabled') -def given_user_setting_enabled(context, name): - uitests.vscode.settings.update_user_settings(context, {name: True}) - - -@behave.when('I update the workspace setting "{name}" with the value "{value}"') -def given_workspace_setting_value(context, name, value): - uitests.vscode.settings.update_workspace_settings(context, {name: value}) - - -@behave.when('I enable the workspace setting "{name}"') -def when_workspace_setting_enable(context, name): - uitests.vscode.settings.update_workspace_settings(context, {name: True}) - - -@behave.when('I disable the workspace setting "{name}"') -def when_workspace_setting_disable(context, name): - uitests.vscode.settings.update_workspace_settings(context, {name: False}) - - -@behave.given('the workspace setting "{name}" does not exist') -def given_workspace_setting_is_removed(context, name): - uitests.vscode.settings.remove_workspace_setting(context, name) - - -@behave.given('the user setting "{name}" does not exist') -def given_user_setting_is_removed(context, name): - uitests.vscode.settings.remove_user_setting(context, name) - - -@behave.given('the user setting "{name}" exists') -def given_user_setting_is_not_empty(context, name): - current_value = uitests.vscode.settings.get_user_setting(context, name) - assert current_value is not None - - -@behave.when('I remove the workspace setting "{name}"') -def when_workspace_setting_is_removed(context, name): - uitests.vscode.settings.remove_workspace_setting(context, name) - - -@behave.then('the workspace setting "{name}" is enabled') -def then_workspace_setting_enabled(context, name): - assert uitests.vscode.settings.get_workspace_setting(context, name) is True - - -@behave.then('the workspace setting "{name}" is disabled') -def then_workspace_setting_disabled(context, name): - assert uitests.vscode.settings.get_workspace_setting(context, name) is False - - -@behave.then('the workspace setting "{name}" is "{value}"') -def then_workspace_setting_value(context, name, value): - assert uitests.vscode.settings.get_workspace_setting(context, name) == value - - -@behave.then('the workspace setting "{name}" contains the value "{value}"') -def then_workspace_setting_contains_value(context, name, value): - assert value in uitests.vscode.settings.get_workspace_setting( - context, name - ), f"{value} not in {uitests.vscode.settings.get_workspace_setting(context, name)}" - - -@behave.then('the workspace setting "{name}" exists') -def then_workspace_setting_is_defined(context, name): - assert uitests.vscode.settings.get_workspace_setting(context, name) is not None diff --git a/uitests/uitests/steps/status_bar.py b/uitests/uitests/steps/status_bar.py deleted file mode 100644 index ddfb95e2825c..000000000000 --- a/uitests/uitests/steps/status_bar.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import behave -from selenium.common.exceptions import StaleElementReferenceException - -import uitests.vscode.status_bar -import uitests.tools - - -@behave.then( - 'the python interpreter displayed in the the status bar contains the value "{name}" in the tooltip' -) -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_selected_interpreter_has_tooltip(context, name): - element = uitests.vscode.status_bar.wait_for_python_statusbar(context) - title = element.get_attribute("title") - assert name in title, f"{name} in {title}" - - -@behave.then( - 'the python interpreter displayed in the the status bar contains the value "{name}" in the display name' -) -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_selected_interpreter_has_text(context, name): - element = uitests.vscode.status_bar.wait_for_python_statusbar(context) - assert name in element.text, f"{name} not in {element.text}" - - -@behave.then( - 'the python interpreter displayed in the the status bar does not contain the value "{name}" in the display name' -) -@uitests.tools.retry((AssertionError, StaleElementReferenceException)) -def then_selected_interpreter_does_not_have_text(context, name): - element = uitests.vscode.status_bar.wait_for_python_statusbar(context) - assert name not in element.text, f"{name} in {element.text}" diff --git a/uitests/uitests/steps/terminal.py b/uitests/uitests/steps/terminal.py deleted file mode 100644 index 3cd0e7402d05..000000000000 --- a/uitests/uitests/steps/terminal.py +++ /dev/null @@ -1,99 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import os.path -import sys -import time - -import behave - -import uitests.vscode.application -import uitests.vscode.notifications -import uitests.vscode.quick_open - - -@behave.given("a terminal is opened") -def terminal_opened(context): - uitests.vscode.quick_open.select_command( - context, "Terminal: Create New Integrated Terminal" - ) - # Take a couple of screen shots (for logging purposes, in case things don't work out). - # Sending commands to terminals is flaky, hence logs just take screenshots - just in case. - uitests.vscode.application.capture_screen(context) - time.sleep(10) # wait for terminal to open and wait for activation. - uitests.vscode.application.capture_screen(context) - - -@behave.when('I send the command "{command}" to the terminal') -def send_command_to_terminal(context, command): - """We're unable send text directly to the terminal (no idea how to do this). - Can't find the exact element to send text to. - Easy work around, use the bootstrap extension to send text to the terminal.""" - with open( - os.path.join(context.options.extensions_dir, "commands.txt"), "w" - ) as file: - file.write(command) - - # Ensure the shell is Command Prompt for Windows & bash for Linux - _ensure_shell_is_cmd(context) - _ensure_shell_is_bash(context) - - # Take a couple of screen shots (for logging purposes, in case things don't work out). - # Sending commands to terminals is flaky, hence logs just take screenshots - just in case. - uitests.vscode.application.capture_screen(context) - uitests.vscode.quick_open.select_command(context, "Smoke: Run Command In Terminal") - uitests.vscode.application.capture_screen(context) - # wait for command to be sent to the terminal by the bootstrap extension. - time.sleep(5) - uitests.vscode.application.capture_screen(context) - - -@behave.when("I change the terminal shell to Command Prompt") -def change_shell_to_cmd(context): - uitests.vscode.quick_open.select_command(context, "Terminal: Select Default Shell") - uitests.vscode.quick_input.select_value(context, "Command Prompt") - # Wait for changes to take affect before opening a new terminal. - time.sleep(1) - - -@behave.when("I change the terminal shell to bash") -def change_shell_to_bash(context): - command = 'I update the workspace setting "terminal.integrated.shell.linux" with the value "/bin/bash"' - context.execute_steps(f"When {command.strip()}") - # Wait for changes to take affect before opening a new terminal. - # Take screenshots, as VSC seems to display prompts asking using to allow this change. - # This has been observed to happen once while testing. - uitests.vscode.application.capture_screen(context) - time.sleep(1) - uitests.vscode.application.capture_screen(context) - - -def _ensure_shell_is_cmd(context): - if not sys.platform.startswith("win"): - return - try: - current_value = uitests.vscode.settings.get_user_setting( - context, "terminal.integrated.shell.windows" - ) - except Exception: - current_value = "" - - if current_value is not None and "cmd.exe" in current_value: - return - change_shell_to_cmd(context) - - -def _ensure_shell_is_bash(context): - if not sys.platform.startswith("linux"): - return - try: - current_value = uitests.vscode.settings.get_user_setting( - context, "terminal.integrated.shell.linux" - ) - except Exception: - current_value = "" - - if current_value is not None and "bash" in current_value: - return - change_shell_to_bash(context) diff --git a/uitests/uitests/steps/testing.py b/uitests/uitests/steps/testing.py deleted file mode 100644 index ee57f643db04..000000000000 --- a/uitests/uitests/steps/testing.py +++ /dev/null @@ -1,203 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import time - -import behave - -import uitests.vscode.testing -import uitests.tools - -node_status_icon_mapping = { - "UNKNOWN": "status-unknown.svg", - "SKIP": "status-unknown.svg", - "PROGRESS": "discovering-tests.svg", - "OK": "status-ok.svg", - "PASS": "status-ok.svg", - "SUCCESS": "status-ok.svg", - "FAIL": "status-error.svg", - "ERROR": "status-error.svg", -} - - -@behave.then("the test explorer icon will be visible") -@uitests.tools.retry(AssertionError) -def icon_visible(context): - uitests.vscode.testing.wait_for_explorer_icon(context) - - -@behave.when("I run the test node number {number:Number}") -def run_node(context, number): - uitests.vscode.testing.click_node_action_item(context, number, "Run") - - -@behave.when('I run the test node "{label}"') -def run_node_by_name(context, label): - number = uitests.vscode.testing.get_node_number(context, label) - run_node(context, number) - - -@behave.when("I debug the test node number {number:Number}") -def debug_node(context, number): - uitests.vscode.testing.click_node_action_item(context, number, "Debug") - - -@behave.when('I debug the test node "{label}"') -def debug_node_by_name(context, label): - number = uitests.vscode.testing.get_node_number(context, label) - debug_node(context, number) - - -@behave.when("I navigate to the code associated with test node number {number:Number}") -def navigate_node(context, number): - uitests.vscode.testing.click_node_action_item(context, number, "Open") - - -@behave.when('I navigate to the code associated with test node "{label}"') -def navigate_node_by_name(context, label): - number = uitests.vscode.testing.get_node_number(context, label) - navigate_node(context, number) - - -@behave.then("there are {count:Number} nodes in the tree") -def explorer_node_count(context, count): - total_count = uitests.vscode.testing.get_node_count(context) - assert total_count == count, f"{total_count} != {count}" - - -@behave.when("I expand all of the test tree nodes") -def explorer_expand_nodes(context): - try: - uitests.vscode.testing.expand_nodes(context) - return - except TimeoutError: - # Rediscover tests. - uitests.vscode.quick_open.select_command(context, "Python: Discover Tests") - # As this is a flaky scenario, lets wait for 5s. - # Enough time for tests to start & perhaps complete. - time.sleep(5) - # If tests discovery has not completed, then lets wait. - wait_for_discovery_to_complete(context) - # try again. - uitests.vscode.testing.expand_nodes(context) - - -@behave.when("I click node number {number:Number}") -def click_node(context, number): - uitests.vscode.testing.click_node(context, number) - - -@behave.when('I click node "{label}"') -def click_node_by_name(context, label): - number = uitests.vscode.testing.get_node_number(context, label) - click_node(context, number) - - -@behave.then("all of the test tree nodes have an unknown icon") -def all_unknown(context): - icons = uitests.vscode.testing.get_node_icons(context) - assert all("status-unknown.svg" in icon.get_attribute("style") for icon in icons) - - -@behave.then('the node number {number:Number} has a status of "{status}"') -@uitests.tools.retry(AssertionError) -def node_status(context, number, status): - icon = uitests.vscode.testing.get_node_icon(context, number) - assert node_status_icon_mapping.get(status.upper(), "") in icon.get_attribute( - "style" - ) - - -@behave.then('the node "{label}" has a status of "{status}"') -@uitests.tools.retry(AssertionError) -def node_status_by_name(context, label, status): - number = uitests.vscode.testing.get_node_number(context, label) - node_status(context, number, status) - - -@behave.then('{number:Number} nodes have a status of "{status}"') -@uitests.tools.retry(AssertionError) -def node_count_status(context, number, status): - check_node_count_status(context, number, status) - - -@behave.then('1 node has a status of "{status}"') -@uitests.tools.retry(AssertionError) -def node_one_status(context, status): - check_node_count_status(context, 1, status) - - -@behave.then("all of the test tree nodes have a progress icon") -@uitests.tools.retry(AssertionError, tries=20, delay=0.5) -def all_progress(context): - """Retry, & wait for 0.5 seconds (longer than default 0.1). - Wait for long enough for tests to start and UI get updated.""" - icons = uitests.vscode.testing.get_node_icons(context) - assert all("discovering-tests.svg" in icon.get_attribute("style") for icon in icons) - - -@behave.then("the stop icon is visible in the toolbar") -@uitests.tools.retry(AssertionError, tries=20, delay=0.5) -def stop_icon_visible(context): - """Retry, & wait for 0.5 seconds (longer than default 0.1). - Wait for long enough for tests to start and UI get updated.""" - uitests.vscode.testing.wait_for_stop_icon(context) - - -@behave.then("the stop icon is not visible in the toolbar") -@uitests.tools.retry(AssertionError) -def stop_icon_not_visible(context): - uitests.vscode.testing.wait_for_stop_hidden(context) - - -@behave.then("the run failed tests icon is visible in the toolbar") -@uitests.tools.retry(AssertionError) -def fun_failed_icon_visible(context): - uitests.vscode.testing.wait_for_run_failed_icon(context) - - -@behave.then("the run failed tests icon is not visible in the toolbar") -@uitests.tools.retry(AssertionError) -def fun_failed_icon_not_visible(context): - uitests.vscode.testing.wait_for_run_failed_hidden(context) - - -@behave.when("I wait for tests to complete running") -@uitests.tools.retry(AssertionError) -def wait_for_run_to_complete(context): - uitests.vscode.testing.wait_for_stop_hidden(context) - - -@behave.when("I wait for tests discovery to complete") -@uitests.tools.retry(AssertionError) -def wait_for_discovery_to_complete(context): - uitests.vscode.testing.wait_for_stop_hidden(context) - - -@behave.when("I stop discovering tests") -def when_stop_discovering(context): - uitests.vscode.testing.stop(context) - - -@behave.when("I run failed tests") -def when_run_failed_tests(context): - uitests.vscode.testing.run_failed_tests(context) - - -@behave.when("I stop running tests") -def when_stop_running(context): - uitests.vscode.testing.stop(context) - - -@behave.then("stop discovering tests") -def then_stop_discovering(context): - uitests.vscode.testing.stop(context) - - -def check_node_count_status(context, number, status): - icon_name = node_status_icon_mapping.get(status.upper(), "") - icons = uitests.vscode.testing.get_node_icons(context) - assert ( - len(list(icon for icon in icons if icon_name in icon.get_attribute("style"))) - == number - ) diff --git a/uitests/uitests/tools.py b/uitests/uitests/tools.py deleted file mode 100644 index 5710d7f5f5c2..000000000000 --- a/uitests/uitests/tools.py +++ /dev/null @@ -1,223 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import logging -import os -import os.path -import shutil -import subprocess -import sys -import time -import traceback -from functools import wraps - -import progress.bar -import pyperclip -import requests - - -def retry(exceptions, tries=100, delay=0.1, backoff=1): - """Retry calling the decorated function using an exponential backoff. - Original source from https://www.calazan.com/retry-decorator-for-python-3/ - Args: - exceptions: The exception to check. may be a tuple of - exceptions to check. - tries: Number of times to try (not retry) before giving up. - delay: Initial delay between retries in seconds. - backoff: Backoff multiplier (e.g. value of 2 will double the delay - each retry). - - """ - - def deco_retry(f): - @wraps(f) - def f_retry(*args, **kwargs): - mtries, mdelay = tries, delay - timeout = tries * delay - start = time.time() - # The code could take a few milli seconds to run, - # Hence the timeout could be too high. - # Instead lets wait for loop to run at least 20 times, - # Before checking if we have exceeded timeout. - # Else for instance if we have a timeout of 120 seconds (tries = 1200), - # This could be too high, as code can take > 0.5 seconds to execute or longer. - while mtries > 1: - try: - return f(*args, **kwargs) - except exceptions: - # except exceptions as e: - # msg = "{}, Retrying in {} seconds...".format(e, mdelay) - # logging.info(msg) - time.sleep(mdelay) - mtries -= 1 - mdelay *= backoff - - if tries - mtries > 20 and (time.time() - start) > timeout: - msg = f"Timeout: After {timeout} seconds." - raise TimeoutError(msg) - return f(*args, **kwargs) - - return f_retry # true decorator - - return deco_retry - - -def log_exceptions(): - """Decorator to just log exceptions and re-raise them. - For some reason behave doesn't print the entire stack trace when handling exceptions. - - """ - - def deco_log_exceptions(f): - @wraps(f) - def wrapper(*args, **kwargs): - try: - return f(*args, **kwargs) - except Exception: - logging.info(traceback.format_exc()) - raise - - return wrapper - - return deco_log_exceptions - - -def run_command(command, *, cwd=None, silent=False, progress_message=None, env=None): - """Run the specified command in a subprocess shell with the following options: - - Pipe output from subprocess into current console. - - Display a progress message. - - """ - - if progress_message is not None: - logging.info(progress_message) - is_git = command[0] == "git" - shell = is_git - command[0] = shutil.which(command[0]) - out = subprocess.PIPE if silent else None - # Else Python throws crazy errors (socket (10106) The requested service provider could not be loaded or initialized.) - # The solution is to pass the complete list of variables. - if sys.platform.startswith("win"): - new_env = {} if env is None else env - env = os.environ.copy() - env.update(new_env) - if not is_git: - proc = subprocess.run( - command, cwd=cwd, shell=shell, env=env, stdout=out, stderr=out - ) - proc.check_returncode() - return - p = subprocess.Popen(command, cwd=cwd, stdout=out, stderr=out, shell=False) - _, err = p.communicate() - - if p.returncode != 0: - raise SystemError( - f"Exit code is not 0, {p.returncode} for command {command}, with an error: {err}" - ) - - -def unzip_file(zip_file, destination, progress_message="Unzip"): - """Unzip a file.""" - - # For now now using zipfile module, - # as the unzippig didn't work for executables. - os.makedirs(destination, exist_ok=True) - dir = os.path.dirname(os.path.realpath(__file__)) - js_file = os.path.join(dir, "js", "unzip.js") - run_command( - ["node", js_file, zip_file, destination], progress_message=progress_message - ) - - -def download_file(url, download_file, progress_message="Downloading"): # noqa - """Download a file and optionally displays a progress indicator.""" - - try: - os.remove(download_file) - except FileNotFoundError: - pass - progress_bar = progress.bar.Bar(progress_message, max=100) - response = requests.get(url, stream=True) - total = response.headers.get("content-length") - - try: - with open(download_file, "wb") as fs: - if total is None: - fs.write(response.content) - else: - downloaded = 0 - total = int(total) - chunk_size = 1024 * 1024 - percent = 0 - for data in response.iter_content(chunk_size=chunk_size): - downloaded += len(data) - fs.write(data) - change_in_percent = (downloaded * 100 // total) - percent - percent = downloaded * 100 // total - for i in range(change_in_percent): - progress_bar.next() - except Exception: - os.remove(download_file) - raise - finally: - progress_bar.finish() - - -def empty_directory(dir): - # Ignore errors on windows. - for root, dirs, files in os.walk(dir): - for f in files: - try: - os.unlink(os.path.join(root, f)) - except Exception: - pass - for d in dirs: - try: - shutil.rmtree(os.path.join(root, d)) - except Exception: - pass - - -@retry(Exception, tries=30) -def wait_for_python_env(cwd, path): - python_exec = _get_python_executable(path) - subprocess.run( - [python_exec, "--version"], check=True, stdout=subprocess.PIPE, cwd=cwd - ).stdout - - -@retry(Exception, tries=30) -def wait_for_pipenv(cwd): - subprocess.run( - ["pipenv", "--py"], - check=True, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - cwd=cwd, - ) - - -@retry(Exception, tries=30, delay=5) -def wait_for_conda_env(conda_path, env_name): - proc = subprocess.run( - [conda_path, "env", "list"], - check=False, - env=os.environ.copy(), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - output = proc.stdout.decode("utf-8") + proc.stderr.decode("utf-8") - assert env_name in output, f"{env_name} not in {output}" - - -def copy_to_clipboard(text): - """Copies text to the clipboard.""" - pyperclip.copy(text) - - -def _get_python_executable(path): - if sys.platform.startswith("win"): - return os.path.join(path, "Scripts", "python.exe") - else: - return os.path.join(path, "bin", "python") diff --git a/uitests/uitests/vscode/__init__.py b/uitests/uitests/vscode/__init__.py deleted file mode 100644 index 7b4d9f8f21e6..000000000000 --- a/uitests/uitests/vscode/__init__.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -from . import ( # noqa - application, - constants, - core, - debugger, - documents, - download, - extension, - notifications, - output_panel, - problems, - quick_input, - quick_open, - screen, - settings, - status_bar, - testing, -) diff --git a/uitests/uitests/vscode/application.py b/uitests/uitests/vscode/application.py deleted file mode 100644 index a2ae47bbb516..000000000000 --- a/uitests/uitests/vscode/application.py +++ /dev/null @@ -1,518 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import contextlib -import glob -import html -import io -import json -import logging -import os -import os.path -import shutil -import stat -import sys -import tempfile -import time -import traceback -from dataclasses import dataclass - -import psutil -from selenium import webdriver - -import uitests.bootstrap -import uitests.report -import uitests.tools - -from . import application, quick_open - -CONTEXT = {"driver": None, "options": None} - - -@dataclass -class Options: - """Options used to configure tests. - E.g. version of VSC, where are tests located, - where are extensions installed. etc. - Some of these can change during the course of the tests. - - Attrs: - channel: Are we using stable or insiders version of VSC. - executable_dir: Directory where VSC executable is located. - user_dir: Directory where VSC will store user related information (user settings, logs). - extensions:dir: Directory where VSC extensions are installed. - extension_path: Path to Python Extension VSIX. - workspace_folder: Directory opened in VSC (during tests). - temp_folder: Temp directory. - screenshots_dir: Directory where screenshots are stored. - python_path: Path to python executable. - logfiles_dir: Directory where logs are stored. - conda_path: Path to conda - python_extension_dir: Directory where Python Extension is installed. - reports_dir: Directory where reports are stored. - """ - - channel: str - executable_dir: str - user_dir: str - extensions_dir: str - extension_path: str - workspace_folder: str - temp_folder: str - screenshots_dir: str - python_path: str - logfiles_dir: str - conda_path: str - python_extension_dir: str - reports_dir: str - - -@dataclass -class Context: - """Context object available in all parts of the test lifecycle. - The behave hooks, steps will have access to this objet. - - Attrs: - options: Instance of options. - driver: Instance of webdriver.Chrome - """ - - options: application.Options - driver: webdriver.Chrome - - -def start(options): - """Starts VS Code and returns a context object""" - logging.debug("Starting VS Code") - uitests.tools.empty_directory(options.workspace_folder) - setup_user_settings(options) - return _launch(options) - - -def get_options( - destination=".vscode test", - vsix="ms-python-insiders.vsix", - channel="stable", - python_path=sys.executable, - conda_path="conda", -): - """Gets the options used for smoke tests.""" - destination = os.path.abspath(destination) - options = Options( - channel, - os.path.join(destination, channel), - os.path.join(destination, "user"), - os.path.join(destination, "extensions"), - vsix, - os.path.join(destination, "workspace folder"), - os.path.join(destination, "temp"), - os.path.join(destination, "screenshots"), - python_path, - os.path.join(destination, "logs"), - conda_path, - os.path.join(destination, "extensions", "pythonExtension"), - os.path.join(destination, "reports"), - ) - os.makedirs(options.extensions_dir, exist_ok=True) - os.makedirs(options.user_dir, exist_ok=True) - os.makedirs(options.workspace_folder, exist_ok=True) - os.makedirs(options.temp_folder, exist_ok=True) - os.makedirs(options.screenshots_dir, exist_ok=True) - os.makedirs(options.logfiles_dir, exist_ok=True) - os.makedirs(options.reports_dir, exist_ok=True) - return options - - -def setup_environment(options): - """Setup environment for smoke tests.""" - # Ensure PTVSD logs are in the reports directory, - # This way they are available for analyzing. - os.environ["PTVSD_LOG_DIR"] = options.logfiles_dir - # Log extension stuff into vsc.log file. - os.environ["VSC_PYTHON_LOG_FILE"] = os.path.join(options.logfiles_dir, "pvsc.log") - - -def setup_user_settings(options): - """Set up user settings for VS Code. - E.g. we want to ensure VS Code uses a specific version of Python as the default. - Or we want to ensure VS Code starts maximized, etc. - - """ - settings_to_add = { - "python.pythonPath": options.python_path, - # Log everything in LS server, to ensure they are captured in reports. - # Found under .vscode test/reports/user/logs/xxx/exthostx/output_logging_xxx/x-Python.log - # These are logs created by VSC. - # Enabling this makes it difficult to look for text in the panel (there's too much content). - # "python.analysis.logLevel": "Trace", - "python.venvFolders": ["envs", ".pyenv", ".direnv", ".local/share/virtualenvs"], - # Disable pylint (we don't want this message) - "python.linting.pylintEnabled": False, - # We dont need these (avoid VSC from displaying prompts). - "telemetry.enableTelemetry": False, - "telemetry.enableCrashReporter": False, - # Start VS Code maximized (good for screenshots and the like). - # At the same time reduce font size, so we can fit more in statusbar. - # If there isn't much room, then Line/Column info isn't displayed in statusbar. - # This could also impact Python Interpreter info, hence reduce font size. - # Also more realestate (capturing logs, etc). - # "window.zoomLevel": -1, # Disable, clicking elements doesn't work with selenium. - "debug.showInStatusBar": "never", # Save some more room in statusbar. - "window.newWindowDimensions": "maximized", - # We don't want VSC to complete the brackets. - # When sending text to editors, such as json files, VSC will automatically complete brackets. - # And that messes up with the text thats being sent to the editor. - "editor.autoClosingBrackets": "never", - } - - folder = os.path.join(options.user_dir, "User") - os.makedirs(folder, exist_ok=True) - settings_file = os.path.join(folder, "settings.json") - if os.path.exists(settings_file): - os.remove(settings_file) - with open(settings_file, "w") as fp: - json.dump(settings_to_add, fp, indent=4) - - -def uninstall_extension(options): - """Uninstalls extensions from smoke tests copy of VSC.""" - shutil.rmtree(options.extensions_dir, ignore_errors=True) - - -def install_extension(options): - """Installs extensions into smoke tests copy of VSC.""" - _set_permissions(options) - uninstall_extension(options) - bootstrap_extension = uitests.bootstrap.main.get_extension_path() - _install_extension(options.extensions_dir, "bootstrap", bootstrap_extension) - _install_extension( - options.extensions_dir, "pythonExtension", options.extension_path - ) - - -def clear_logs(options): - """Clears logs created between tests""" - uitests.tools.empty_directory(options.logfiles_dir) - os.makedirs(options.logfiles_dir, exist_ok=True) - - -def clear_vscode(options): - # Delete the directories. - uitests.tools.empty_directory(options.user_dir) - for folder in glob.glob( - os.path.join(options.python_extension_dir, "languageServer*") - ): - uitests.tools.empty_directory(folder) - - -def reload(context): - """Reloads VS Code.""" - logging.debug("Reloading VS Code") - # Ignore all messages written to console. - with contextlib.redirect_stdout(io.StringIO()): - with contextlib.redirect_stderr(io.StringIO()): - application.exit(context) - app_context = _launch(context.options) - context.driver = app_context.driver - CONTEXT["driver"] = context.driver - return app_context - - -def clear_everything(context): - """Clears everyting within VS Code, that could interfer with tests. - E.g. close opened editors, dismiss all messages.. - - """ - commands = [ - "View: Revert and Close Editor", - "Terminal: Kill the Active Terminal Instance", - "Debug: Remove All Breakpoints", - "File: Clear Recently Opened", - "Clear Editor History", - "Clear Command History", - "View: Close All Editors", - "View: Close Panel", - "Notifications: Clear All Notifications", - ] - for command in commands: - quick_open.select_command(context, command) - - -def setup_workspace(context, source_repo=None): - """Set the workspace for a feature/scenario. - source_repo is either the github url of the repo to be used as the workspace folder. - Or it is None. - - """ - logging.debug(f"Setting up workspace folder from {source_repo}") - - # On windows, create a new folder everytime. - # Deleting/reverting changes doesn't work too well. - # We get a number of access denied errors (files are in use). - try: - uitests.tools.empty_directory(context.options.temp_folder) - except (PermissionError, FileNotFoundError, OSError): - pass - try: - uitests.tools.empty_directory(context.options.workspace_folder) - except (PermissionError, FileNotFoundError, OSError): - pass - workspace_folder_name = os.path.basename( - tempfile.NamedTemporaryFile(prefix="workspace folder ").name - ) - context.options.workspace_folder = os.path.join( - context.options.temp_folder, workspace_folder_name - ) - os.makedirs(context.options.workspace_folder, exist_ok=True) - - if source_repo is None: - return - - # Just delete the files in current workspace. - uitests.tools.empty_directory(context.options.workspace_folder) - target = context.options.workspace_folder - repo_url = _get_repo_url(source_repo) - uitests.tools.run_command(["git", "clone", repo_url, "."], cwd=target, silent=True) - - # Its possible source_repo is https://github.com/Microsoft/vscode-python/tree/master/build - # Meaning, we want to glon https://github.com/Microsoft/vscode-python - # and want the workspace folder to be tree/master/build when cloned. - if len(source_repo) > len(repo_url): - # Exclude trailing `.git` and take everthying after. - sub_directory = source_repo[len(repo_url[:-4]) + 1 :] - context.options.workspace_folder = os.path.join( - context.options.workspace_folder, os.path.sep.join(sub_directory.split("/")) - ) - - -def launch_vscode(options): - """Launches the smoke tests copy of VSC.""" - chrome_options = webdriver.ChromeOptions() - # Remember to remove the leading `--`. - # Chromedriver will add `--` for ALL arguments. - # I.e. arguments without a leading `--` are not supported. - for arg in [ - f"user-data-dir={options.user_dir}", - f"extensions-dir={options.extensions_dir}", - f"folder-uri=file:{options.workspace_folder}", - "skip-getting-started", - "skip-release-notes", - "sticky-quickopen", - "disable-telemetry", - "disable-updates", - "disable-crash-reporter", - # TODO: No sure whether these are required - # Was trying to get VSC Insiders working - "no-sandbox", - "--no-sandbox", - # "no-first-run", - # "--no-first-run", - "--disable-dev-shm-usage", - "disable-dev-shm-usage", - "--disable-setuid-sandbox", - "disable-setuid-sandbox", - ]: - chrome_options.add_argument(arg) - - chrome_options.binary_location = _get_binary_location( - options.executable_dir, options.channel - ) - - chrome_driver_path = os.path.join(options.executable_dir, "chromedriver") - - # TODO: No sure whether chrome_options is required - driver = webdriver.Chrome( - options=chrome_options, - chrome_options=chrome_options, - executable_path=chrome_driver_path, - ) - return driver - - -def exit(context): - """Exits VS Code""" - # Ignore all messages written to console. - with contextlib.redirect_stdout(io.StringIO()): - with contextlib.redirect_stderr(io.StringIO()): - pid = 0 - try: - pid = context.driver.service.process.id - except Exception: - pass - try: - context.driver.close() - except Exception: - pass - try: - context.driver.quit() - except Exception: - pass - try: - if pid != 0: - psutil.Process(pid).terminate() - except Exception: - pass - try: - # Clear reference. - context.driver = None - except Exception: - pass - - -def capture_screen(context): - """Capture screenshots and attach to the report. - Also save to screenshots directory. - - """ - # So its easy to tell the order of screenshots taken. - counter = getattr(context, "screenshot_counter", 1) - context.screenshot_counter = counter + 1 - - screenshot = context.driver.get_screenshot_as_base64() - uitests.report.PrettyCucumberJSONFormatter.instance.attach_image(screenshot) - - capture_screen_to_file(context) - # # Also save for logging purposes (easier to look at images). - # filename = tempfile.NamedTemporaryFile(prefix=f"screen_capture_{counter}_") - # filename = f"{os.path.basename(filename.name)}.png" - # filename = os.path.join(context.options.screenshots_dir, filename) - # context.driver.save_screenshot(filename) - # relative_path = os.path.relpath(filename, context.options.reports_dir) - # html_content = f'<a href="{relative_path}" target="_blank">More screen shots</a>' - - # uitests.report.PrettyCucumberJSONFormatter.instance.attach_html(html_content) - - -def capture_exception(context, info): - """Capture exception infor and attach to the report.""" - formatted_ex = "<br>".join( - map( - html.escape, - traceback.format_exception( - type(info.exception), info.exception, info.exc_traceback - ), - ) - ) - uitests.report.PrettyCucumberJSONFormatter.instance.attach_html(formatted_ex) - - -def capture_screen_to_file(context, file_path=None, prefix=""): - """Capture screenshots to a file""" - if file_path is None: - with tempfile.NamedTemporaryFile(prefix=f"{prefix}screen_capture_") as fp: - filename = f"{os.path.basename(fp.name)}.png" - filename = os.path.join(context.options.screenshots_dir, filename) - else: - filename = file_path - - context.driver.save_screenshot(filename) - return filename - - -def _set_permissions(options): - """Set necessary permissions on Linux to be able to start VSC. - Else selenium throws errors. - & so does VSC, when accessing vscode-ripgrep/bin/rg. - - """ - if sys.platform.startswith("linux"): - binary_location = _get_binary_location(options.executable_dir, options.channel) - file_stat = os.stat(binary_location) - os.chmod(binary_location, file_stat.st_mode | stat.S_IEXEC) - - rg_path = os.path.join( - os.path.dirname(binary_location), - "resources", - "app", - "node_modules.asar.unpacked", - "vscode-ripgrep", - "bin", - "rg", - ) - file_stat = os.stat(rg_path) - os.chmod(rg_path, file_stat.st_mode | stat.S_IEXEC) - - -def _install_extension(extensions_dir, extension_name, vsix): - """Installs an extensions into smoke tests copy of VSC.""" - temp_dir = os.path.join(tempfile.gettempdir(), extension_name) - uitests.tools.unzip_file(vsix, temp_dir) - shutil.copytree( - os.path.join(temp_dir, "extension"), - os.path.join(extensions_dir, extension_name), - ) - shutil.rmtree(temp_dir, ignore_errors=True) - - -def _get_binary_location(executable_directory, channel): - """Returns the path to the VSC executable""" - if sys.platform.startswith("darwin"): - return os.path.join( - executable_directory, - "Visual Studio Code.app" - if channel == "stable" - else "Visual Studio Code - Insiders.app", - "Contents", - "MacOS", - "Electron", - ) - - if sys.platform.startswith("win"): - return os.path.join( - executable_directory, - "Code.exe" if channel == "stable" else "Code - Insiders.exe", - ) - - return os.path.join( - executable_directory, - "VSCode-linux-x64", - "code" if channel == "stable" else "code-insiders", - ) - - -def _launch(options): - app_context = _start_vscode(options) - CONTEXT["driver"] = app_context.driver - if CONTEXT["options"] is None: - CONTEXT["options"] = options - return app_context - - -def _start_vscode(options): - application.setup_environment(options) - driver = application.launch_vscode(options) - context = Context(options, driver) - # Wait for VSC to startup. - time.sleep(2) - return context - - -def _get_cli_location(executable_directory): - if sys.platform.startswith("darwin"): - return os.path.join( - executable_directory, - "Visual Studio Code.app", - "Contents", - "Resources", - "app", - "out", - "cli.js", - ) - - if sys.platform.startswith("win"): - return os.path.join(executable_directory, "resources", "app", "out", "cli.js") - - return os.path.join( - executable_directory, "VSCode-linux-x64", "resources", "app", "out", "cli.js" - ) - - -def _get_repo_url(source_repo): - """Will return the repo url ignoring any sub directories.""" - - repo_parts = source_repo[len("https://github.com/") :].split("/") - repo_name = ( - repo_parts[1] if repo_parts[1].endswith(".git") else f"{repo_parts[1]}.git" - ) - return f"https://github.com/{repo_parts[0]}/{repo_name}" diff --git a/uitests/uitests/vscode/code_lenses.py b/uitests/uitests/vscode/code_lenses.py deleted file mode 100644 index 51e4956d4425..000000000000 --- a/uitests/uitests/vscode/code_lenses.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -from . import core - - -def get_code_lenses(context, **kwargs): - selector = ".editor-container .monaco-editor .lines-content .codelens-decoration a" - - return core.wait_for_elements(context.driver, selector, **kwargs) diff --git a/uitests/uitests/vscode/constants.py b/uitests/uitests/vscode/constants.py deleted file mode 100644 index dbcadcf0eee8..000000000000 --- a/uitests/uitests/vscode/constants.py +++ /dev/null @@ -1,7 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -PYTHON_STATUS_BAR_PRIORITY = 100 -PYTHON_STATUS_BAR_PREFIX = "Python" -MAC_INTERPRETER_SELECTED_AND_HAVE_OTEHR_INTERPRETER_MESSAGE = "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." diff --git a/uitests/uitests/vscode/core.py b/uitests/uitests/vscode/core.py deleted file mode 100644 index d7012a55d8aa..000000000000 --- a/uitests/uitests/vscode/core.py +++ /dev/null @@ -1,127 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import time - -from selenium.common import exceptions - - -class ElementVisibleException(exceptions.InvalidElementStateException): - """Thrown when an element is present/visible on the DOM, when it should not be. - """ - - pass - - -def _try_and_find( - fn, - timeout_messge="Timeout", - retry_count=100, - retry_interval=0.1, - timeout=None, - **kwargs, -): - """Try and find a DOM element in VSC based on a predicate within a given time period.""" - if timeout is not None: - retry_count = timeout / retry_interval - else: - timeout = retry_count * retry_interval - - trial_counter = 0 - start = time.time() - while trial_counter <= retry_count: - if time.time() - start > timeout: - trial_counter = retry_count + 1 - try: - return fn.__call__() - except ( - exceptions.NoSuchElementException, - exceptions.StaleElementReferenceException, - ElementVisibleException, - ): - trial_counter += 1 - time.sleep(retry_interval) - else: - msg = f"Timeout: {timeout_messge} after {retry_count * retry_interval} seconds." - raise TimeoutError(msg) - - -def dispatch_keys(driver, *keys, **kwargs): - """Sends key stokes to a DOM element.""" - element = kwargs.pop("element", driver.switch_to.active_element) - element.send_keys(*keys) - - -def wait_for_element(driver, css_selector, predicate=lambda ele: True, **kwargs): - """Wait till a DOM element in VSC is found.""" - - def find(): - element = driver.find_element_by_css_selector(css_selector) - if not element.is_displayed(): - raise exceptions.NoSuchElementException( - "Element not yet visible, so lets wait again" - ) - if element is None: - raise exceptions.NoSuchElementException( - "Predicate returned False in wait_for_element" - ) - return element - - return _try_and_find(find, **kwargs) - - -def wait_for_element_to_be_hidden(driver, css_selector, **kwargs): - """Wait till a DOM element in VSC is found.""" - - def find(): - try: - element = driver.find_element_by_css_selector(css_selector) - except (TimeoutError, exceptions.NoSuchElementException): - return - if not element.is_displayed(): - return - raise ElementVisibleException("Element is visible when it should not be") - - try: - return _try_and_find(find, **kwargs) - except TimeoutError: - pass - - -def wait_for_elements( - driver, css_selector, predicate=lambda elements: elements, **kwargs -): - """Wait till DOM elements in VSC is found.""" - - def find(): - elements = driver.find_elements_by_css_selector(css_selector) - filtered = predicate(elements) - if filtered: - # Ensure all items returned are visible. - for element in filtered: - if not element.is_displayed(): - raise exceptions.NoSuchElementException( - "Element not yet visible, so lets wait again" - ) - - return filtered - raise exceptions.NoSuchElementException( - "Predicate returned False in wait_for_elements" - ) - - return _try_and_find(find, **kwargs) - - -def wait_for_active_element(driver, css_selector, **kwargs): - """Wait till a DOM element with a given css selector is the active element.""" - - def is_active(): - element = driver.find_element_by_css_selector(css_selector) - assert element == driver.switch_to.active_element - if not element.is_displayed(): - raise exceptions.NoSuchElementException( - "Element not yet visible, so lets wait again" - ) - - return _try_and_find(is_active, **kwargs) diff --git a/uitests/uitests/vscode/debugger.py b/uitests/uitests/vscode/debugger.py deleted file mode 100644 index 18cc030b026e..000000000000 --- a/uitests/uitests/vscode/debugger.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import time - -import uitests.vscode.core -import uitests.vscode.extension - - -def is_debugg_sidebar_visible(context): - try: - uitests.vscode.core.wait_for_element( - context.driver, ".composite.viewlet.debug-viewlet", retry_count=2 - ) - return True - except TimeoutError: - return False - - -def wait_for_debugger_to_start(context): - uitests.vscode.core.wait_for_element(context.driver, "div.debug-toolbar") - - -def wait_for_debugger_to_pause(context): - # Wait before checking, wait for debug toolbar to get displayed - time.sleep(1.5) - - find = lambda ele: "Continue" in ele.get_attribute("title") # noqa - uitests.vscode.core.wait_for_element( - context.driver, "div.debug-toolbar .action-item .action-label.icon", find - ) - - -def wait_for_debugger_to_stop(context, **kwargs): - # Wait before checking, wait for debug toolbar to get displayed - time.sleep(1.5) - - uitests.vscode.core.wait_for_element_to_be_hidden( - context.driver, "div.debug-toolbar", **kwargs - ) - - -def add_breakpoint(context, file_name, line): - uitests.vscode.documents.open_file(context, file_name) - uitests.vscode.documents.go_to_line(context, line) - uitests.vscode.quick_open.select_command(context, "Debug: Toggle Breakpoint") - - -def wait_for_python_debug_config_picker(context): - selector = ".quick-input-widget .quick-input-title" - - debug_label = uitests.vscode.extension.get_localized_string( - "debug.selectConfigurationTitle" - ) - - def find(elements): - return [element for element in elements if element.text == debug_label] - - return uitests.vscode.core.wait_for_elements(context.driver, selector, find) - - -# def get_current_frame_position(context): -# selector = ".panel-body.debug-call-stack .monaco-list-row.selected" -# stack_trace = uitests.vscode.core.wait_for_element(context.driver, selector) -# file_name = stack_trace.find_element_by_css_selector(".file-name").text -# position = stack_trace.find_element_by_css_selector(".line-number").text.split(":") -# return file_name, int(position[0]), int(position[1]) diff --git a/uitests/uitests/vscode/documents.py b/uitests/uitests/vscode/documents.py deleted file mode 100644 index 09f006a6bad6..000000000000 --- a/uitests/uitests/vscode/documents.py +++ /dev/null @@ -1,284 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import logging -import os.path -import pathlib -import re -import time -import traceback -from urllib.parse import quote - -from selenium.webdriver.common.keys import Keys - -import uitests.tools -import uitests.vscode.application -import uitests.vscode.core -import uitests.vscode.debugger -import uitests.vscode.quick_open -import uitests.vscode.testing - -from . import core, quick_input, quick_open -from .selectors import get_selector - -LINE_COLUMN_REGEX = re.compile("Ln (?P<line>\d+), Col (?P<col>\d+)") -LINE_COLUMN_REGEX_FROM_PY_STATUS_BAR = re.compile("(?P<line>\d+),(?P<col>\d+)") -LINE_REGEX_FROM_GOTO_LABEL = re.compile("Current Line: (?P<line>\d+). Type a .*") - - -def is_explorer_sidebar_visible(context): - try: - uitests.vscode.core.wait_for_element( - context.driver, ".composite.viewlet.explorer-viewlet", retry_count=2 - ) - return True - except TimeoutError: - return False - - -def _refresh_file_explorer(context): - # Check what explorer is currently visible - is_debug_explorer_visbile = uitests.vscode.debugger.is_debugg_sidebar_visible( - context - ) - if not is_debug_explorer_visbile: - is_test_explorer_visbile = uitests.vscode.testing.is_explorer_sidebar_visible( - context - ) - - # Refresh the explorer, its possible a new file was created, we need to ensure - # VSC is aware of this. Else opening files in vsc fails. - # Note: This will cause explorer to be displayed. - uitests.vscode.quick_open.select_command(context, "File: Refresh Explorer") - # Wait for explorer to get refreshed. - time.sleep(0.5) - - if is_debug_explorer_visbile: - uitests.vscode.quick_open.select_command(context, "View: Show Debug") - if is_test_explorer_visbile: - uitests.vscode.quick_open.select_command(context, "View: Show Test") - - -@uitests.tools.retry(TimeoutError, tries=5) -def open_file(context, filename): - _refresh_file_explorer(context) - quick_open.select_command(context, "Go to File...") - quick_open.select_value(context, filename) - _wait_for_editor_focus(context, filename) - - -def is_file_open(context, filename, **kwargs): - _wait_for_active_tab(context, filename, **kwargs) - _wait_for_editor_focus(context, filename) - - -def create_file_with_contents(context, filename, text): - os.makedirs( - pathlib.Path(os.path.join(context.options.workspace_folder, filename)).parent, - exist_ok=True, - ) - with open(os.path.join(context.options.workspace_folder, filename), "w") as file: - file.write("") - - try: - # Using `core.dispatch_keys(context.driver, text)` will not always work, as its the same as typing in editor. - # Sometimes VSC takes over and completes text, such as brackets (auto completion items). - # Hence the solution is to open the file and paste the text into the editor (without typing it out). - # This could bomb out, in case we're unable to copy to the clipboard. - uitests.tools.copy_to_clipboard(text) - open_file(context, filename) - _wait_for_editor_focus(context, filename) - quick_open.select_command(context, "Paste") - except Exception: - open_file(context, filename) - _wait_for_editor_focus(context, filename) - # Just update the file manually. - with open( - os.path.join(context.options.workspace_folder, filename), "w" - ) as file: - file.write(text) - # Let VSC see the changes (dirty hack, but this is a fallback). - time.sleep(1) - - quick_open.select_command(context, "File: Save") - quick_open.select_command(context, "View: Close Editor") - - -def create_new_untitled_file_with_contents(context, text): - quick_open.select_command(context, "File: New Untitled File") - _wait_for_editor_focus(context, "Untitled-1") - core.dispatch_keys(context.driver, text) - - -def create_new_untitled_file(context, language="Python"): - quick_open.select_command(context, "File: New Untitled File") - _wait_for_editor_focus(context, "Untitled-1") - quick_open.select_command(context, "Change Language Mode") - quick_input.select_value(context, language) - - -def change_document_language(context, language="Python"): - quick_open.select_command(context, "Change Language Mode") - quick_input.select_value(context, language) - - -def scroll_to_top(context): - go_to_line(context, 1) - - -def go_to_line(context, line_number): - quick_open.select_command(context, "Go to Line...") - quick_open.select_value(context, str(line_number)) - _wait_for_line(context, line_number) - - -@uitests.tools.retry(AssertionError, tries=5) -def go_to_line_column(context, line_number, column): - go_to_line(context, line_number) - for i in range(column - 1): - core.dispatch_keys(context.driver, Keys.RIGHT) - time.sleep(0.1) - - try: - position = get_current_position(context) - assert position == ( - line_number, - column, - ), f"{position} != ({line_number}, {column})" - except Exception: - logging.info( - f"Failed to get position using get_current_position, assuming column is as expected, {traceback.format_exc()}" # noqa - ) - # Some times VSC does not display the line numbers in the status bar. - # Got some screenshots from CI where this has happened (for over 10 seconds no line line in statusbar!!!). - # As a fallback, use another CSS query. - # If the line number is equal, assume the column number is what's expected as well. - line = get_current_line(context) - assert line == line_number - - -@uitests.tools.retry((AssertionError, ValueError), tries=2) -def get_current_line(context): - try: - position = get_current_position(context, retry_count=30, retry_interval=0.1) - return position[0] - except Exception: - uitests.vscode.application.capture_screen_to_file( - context, prefix="get_position_failed_1" - ) - logging.info( - f"Failed to get position using get_current_position, {traceback.format_exc()}" - ) - - try: - # Some times VSC does not display the line numbers in the status bar. - # Got some screenshots from CI where this has happened (for over 10 seconds no line line in statusbar!!!). - # As a fallback, use another CSS query. - selector = get_selector("STATUS_BAR_SELECTOR", context.options.channel).format( - "Py2" - ) - element = core.wait_for_element( - context.driver, selector, retry_count=30, retry_interval=0.1 - ) - match = LINE_COLUMN_REGEX_FROM_PY_STATUS_BAR.match(element.text) - if match is None: - raise ValueError(f"Unable to detemrine line & column") - return int(match.group("line")), int(match.group("col")) - except Exception: - uitests.vscode.application.capture_screen_to_file( - context, prefix="get_line_failed_2" - ) - logging.info( - f"Failed to get position using Bootstrap extension, {traceback.format_exc()}" - ) - - try: - # Some times VSC does not display the line numbers in the status bar. - # Got some screenshots from CI where this has happened (for over 10 seconds no line line in statusbar!!!). - # As a fallback, use another CSS query. - selector = ".quick-open-entry .quick-open-row a.label-name span span" - element = core.wait_for_element( - context.driver, selector, retry_count=30, retry_interval=0.1 - ) - return int(element.text.strip()) - except Exception: - uitests.vscode.application.capture_screen_to_file( - context, prefix="get_line_failed_3" - ) - logging.info( - f"Failed to get position using editor highlighted line, {traceback.format_exc()}" - ) - - # Try to go to a line, the popup that appears contains the current line number. - element = uitests.vscode.quick_open._open(context, "Go to Line...") - try: - selector = ".margin .margin-view-overlays .current-line + .line-numbers" - element = core.wait_for_element( - context.driver, selector, retry_count=30, retry_interval=0.1 - ) - match = LINE_REGEX_FROM_GOTO_LABEL.match(element.text) - except Exception: - uitests.vscode.application.capture_screen_to_file( - context, prefix="get_line_goto_failed_4" - ) - logging.info( - f"Failed to get position using Go to line, {traceback.format_exc()}" - ) - finally: - # Close the go to line, prompt - core.dispatch_keys(context.driver, Keys.ESCAPE, element=element) - - if match is None: - raise ValueError(f"Unable to detemrine line from Go to label") - return int(match.group("line")) - - -def get_current_position(context, **kwargs): - selector = get_selector("GOTO_STATUS_BAR_SELECTOR", context.options.channel) - element = core.wait_for_element(context.driver, selector, **kwargs) - match = LINE_COLUMN_REGEX.match(element.text) - if match is None: - raise ValueError(f"Unable to detemrine line & column") - return int(match.group("line")), int(match.group("col")) - - -def send_text_to_editor(context, filename, text): - """Send text to the editor.""" - selector = f'.monaco-editor[data-uri$="{quote(filename)}"] textarea' - element = core.wait_for_element(context.driver, selector) - core.dispatch_keys(context.driver, text, element=element) - - -def get_completion_list(context): - selector = ".editor-widget.suggest-widget.visible .monaco-list-row a.label-name .monaco-highlighted-label" - elements = core.wait_for_elements(context.driver, selector) - return [element.text for element in elements] - - -@uitests.tools.retry(AssertionError, tries=15, delay=1) -def _wait_for_line(context, line_number): - line = get_current_position(context) - assert line[0] == line_number, f"{line[0]} != {line_number}" - - -def _wait_for_active_tab(context, filename, is_dirty=False): - """Wait till a tab is active with the given file name.""" - dirty_class = ".dirty" if is_dirty else "" - dirty_class = "" - filename = os.path.basename(filename) - selector = f'.tabs-container div.tab.active{dirty_class}[aria-selected="true"][aria-label="{filename}, tab"]' - core.wait_for_element(context.driver, selector) - - -def _wait_for_active_editor(context, filename, is_dirty=False): - """Wait till an editor with the given file name is active.""" - selector = ( - f'.editor-instance .monaco-editor[data-uri$="{quote(filename)}"] textarea' - ) - core.wait_for_element(context.driver, selector) - - -def _wait_for_editor_focus(context, filename, is_dirty=False, **kwargs): - """Wait till an editor with the given file name receives focus.""" - _wait_for_active_tab(context, filename, is_dirty, **kwargs) - _wait_for_active_editor(context, filename, is_dirty, **kwargs) diff --git a/uitests/uitests/vscode/download.py b/uitests/uitests/vscode/download.py deleted file mode 100644 index 7d0dbd4062be..000000000000 --- a/uitests/uitests/vscode/download.py +++ /dev/null @@ -1,95 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import os -import os.path -import re -import shutil -import sys -import tempfile - -import requests - -import uitests.tools - - -def _get_download_platform(): - platform = sys.platform - if platform.startswith('linux'): - return "linux-x64" - if platform.startswith('darwin'): - return "darwin" - if platform.startswith('win'): - return "win32-archive" - - -def _get_latest_version(channel="stable"): - """Get the latest version of VS Code - The channel defines the channel for VSC (stable or insiders).""" - - download_platform = _get_download_platform() - url = f"https://update.code.visualstudio.com/api/releases/{channel}/{download_platform}" # noqa - versions = requests.get(url) - return versions.json()[0] - - -def _get_download_url( - version, download_platform, channel="stable" -) -> str: - """Get the download url for vs code.""" - return f"https://vscode-update.azurewebsites.net/{version}/{download_platform}/{channel}" # noqa - - -def _get_electron_version(channel="stable"): - if channel == "stable": - version = _get_latest_version() - # Assume that VSC tags based on major and minor numbers. - # E.g. 1.32 and not 1.32.1 - version_parts = version.split(".") - tag = f"{version_parts[0]}.{version_parts[1]}" - url = ( - f"https://raw.githubusercontent.com/Microsoft/vscode/release/{tag}/.yarnrc" # noqa - ) - else: - url = "https://raw.githubusercontent.com/Microsoft/vscode/master/.yarnrc" # noqa - - response = requests.get(url) - matches = re.finditer(r'target\s"(\d+.\d+.\d+)"', response.text, re.MULTILINE) - for _, match in enumerate(matches, start=1): - return match.groups()[0] - - -def download_chrome_driver(download_path, channel="stable"): - """Download chrome driver corresponding to the version of electron. - Basically check version of chrome released with the version of Electron.""" - - # Note: When VSC Insiders uses Electron 4.2.3, but the version of chromedriver for that doesn't work. - # Known issue with chromedriver and selenium (no idea what's going on). Others have reported the same issue. - # Solution, keep using the same version of chrome driver used by the stable version of VSC. - - os.makedirs(download_path, exist_ok=True) - electron_version = _get_electron_version("stable") - dir = os.path.dirname(os.path.realpath(__file__)) - js_file = os.path.join(dir, "..", "js", "chromeDownloader.js") - # Use an exising npm package. - uitests.tools.run_command( - ["node", js_file, electron_version, download_path], - progress_message="Downloading chrome driver", - ) - - -def download_vscode(download_path, channel="stable"): - """Download VS Code.""" - - shutil.rmtree(download_path, ignore_errors=True) - os.makedirs(download_path, exist_ok=True) - - download_platform = _get_download_platform() - version = _get_latest_version(channel) - url = _get_download_url(version, download_platform, channel) - - file_name = "vscode.tar.gz" if sys.platform.startswith("linux") else "vscode.zip" - zip_file = os.path.join(tempfile.mkdtemp(), file_name) - uitests.tools.download_file(url, zip_file, f"Downloading VS Code {channel}") - uitests.tools.unzip_file(zip_file, download_path) diff --git a/uitests/uitests/vscode/extension.py b/uitests/uitests/vscode/extension.py deleted file mode 100644 index cfc74ae42aec..000000000000 --- a/uitests/uitests/vscode/extension.py +++ /dev/null @@ -1,52 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import json -import os.path - -from . import core, quick_open, status_bar -from .selectors import get_selector - -_localized_strings = {} - - -def activate_python_extension(context): - last_error = None - for _ in range(5): - quick_open.select_command(context, "Activate Python Extension") - try: - # Sometimes it takes a while, specially on Windows. - # So lets wait for 30 seconds. - core.wait_for_element( - context.driver, - get_selector("STATUS_BAR_SELECTOR", context.options.channel).format( - "Py2" - ), - timeout=30, - ) - break - except Exception as ex: - last_error = ex - continue - else: - raise SystemError("Failed to activate extension") from last_error - status_bar.wait_for_python_statusbar(context) - _initialize_localized_strings(context) - - -def get_localized_string(key): - """ - Gets a localized string from the `package.nls.json` file of the Python Extension. - This is used to ensure we do not hardcord labels in our tests. - """ - return _localized_strings[key] - - -def _initialize_localized_strings(context): - """Load the localized strings.""" - with open( - os.path.join(context.options.python_extension_dir, "package.nls.json"), "r" - ) as fp: - global _localized_strings - _localized_strings = json.load(fp) diff --git a/uitests/uitests/vscode/notifications.py b/uitests/uitests/vscode/notifications.py deleted file mode 100644 index 5745974c3ce0..000000000000 --- a/uitests/uitests/vscode/notifications.py +++ /dev/null @@ -1,102 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import time - -from uitests.tools import retry - -from . import core, quick_open - - -def clear(context, **kwargs): - quick_open.select_command(context, "Notifications: Clear All Notifications") - - -def wait_for_message(context, value, **kwargs): - selector = ".notifications-toasts.visible .notifications-list-container .notification-list-item-message" - - def find(elements): - return [element for element in elements if element.text == value] - - return core.wait_for_elements(context.driver, selector, find, **kwargs) - - -def dismiss_message( - context, message, button_text=None, retry_count=100, retry_interval=0.1 -): - @retry(AssertionError, tries=retry_count, delay=retry_interval) - def dismiss(): - # Get a list of all notifications with the above message - elements = _get_messages_containing_text(context, message) - - if button_text is None: - # For each of these click the `X` box - for close_icon in map(_get_close_button, elements): - close_icon.click() - # Wait for click to take affect. - time.sleep(0.5) - else: - # For each of these click the `<button_text>` button - for button in map( - lambda element: _get_button(element, button_text), elements - ): - button.click() - # Wait for click to take affect. - time.sleep(0.5) - - dismiss() - - -def wait_for_message_containing(context, value, **kwargs): - selector = ".notifications-toasts.visible .notifications-list-container .notification-list-item-message" - - def find(elements): - return [element for element in elements if value in element.text] - - return core.wait_for_elements(context.driver, selector, find, **kwargs) - - -def _does_notification_contain_message(element, message): - return any( - [ - child - for child in element.find_elements_by_css_selector( - ".notification-list-item-message" - ) - if message.lower() in child.text.lower() - ] - ) - - -def _get_close_button(element): - return element.find_element_by_css_selector( - ".action-label.icon.clear-notification-action" - ) - - -def _get_button(element, button_text): - buttons = element.find_elements_by_css_selector(".monaco-button.monaco-text-button") - valid_buttons = [ - button for button in buttons if button_text.lower() in button.text.lower() - ] - return valid_buttons[0] if any(valid_buttons) else None - - -def _get_messages_containing_text(context, message): - selector = ".notifications-toasts.visible .notifications-list-container" - - def find(elements): - return [ - element - for element in elements - if _does_notification_contain_message(element, message) - ] - - # Get a list of all notifications with the above message - # If the message isn't visisble yet, then no need to retry, we'll do that in dismiss. - elements = core.wait_for_elements(context.driver, selector, find, retry_count=2) - if any(elements): - return elements - else: - raise AssertionError(f"No notification with the provided message '{message}'") diff --git a/uitests/uitests/vscode/output_panel.py b/uitests/uitests/vscode/output_panel.py deleted file mode 100644 index 3a9d5f312dbe..000000000000 --- a/uitests/uitests/vscode/output_panel.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import time - -from selenium.common.exceptions import StaleElementReferenceException - -import uitests.tools - -from . import core - - -# The ui can get updated, hence retry at least 10 times. -@uitests.tools.retry(StaleElementReferenceException) -def get_output_panel_lines(context, **kwargs): - selector = ".part.panel.bottom .view-lines .view-line span span" - elements = core.wait_for_elements(context.driver, selector, **kwargs) - return [element.text for element in elements] - - -def maximize_bottom_panel(context): - try: - selector = ".part.panel.bottom a.icon.maximize-panel-action" - element = core.wait_for_element(context.driver, selector) - element.click() - # Wait for some time for click to take affect. - time.sleep(0.5) - except Exception: - pass - - -def minimize_bottom_panel(context): - try: - selector = ".part.panel.bottom a.icon.minimize-panel-action" - element = core.wait_for_element(context.driver, selector) - element.click() - # Wait for some time for click to take affect. - time.sleep(0.5) - except Exception: - pass diff --git a/uitests/uitests/vscode/problems.py b/uitests/uitests/vscode/problems.py deleted file mode 100644 index 569c2a68f3f3..000000000000 --- a/uitests/uitests/vscode/problems.py +++ /dev/null @@ -1,54 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -from enum import Enum - -from . import core - - -class ProblemType(Enum): - All = 0 - Error = 1 - Warning = 2 - Info = 3 - - -def get_problem_count(context, problem_type=ProblemType.All, **kwargs): - if problem_type == ProblemType.All: - selector = ".part.panel.bottom .action-item.checked .badge-content" - try: - element = context.driver.find_element_by_css_selector(selector) - if element is None or not element.is_displayed(): - return 0 - except Exception: - pass - - element = core.wait_for_element(context.driver, selector, **kwargs) - if element.text == "": - return 0 - else: - return int(element.text) - - if problem_type == ProblemType.Errors: - selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-icon.error" - elif problem_type == ProblemType.Warning: - selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-icon.warning" - elif problem_type == ProblemType.Info: - selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-icon.info" - - return len(core.wait_for_elements(context.driver, selector, **kwargs)) - - -def get_problem_files(context, **kwargs): - selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .file-icon .label-name span span" - - elements = core.wait_for_elements(context.driver, selector, **kwargs) - return [element.text for element in elements] - - -def get_problems(context, **kwargs): - selector = ".part.panel.bottom .content .tree-container .monaco-tl-row .marker-message-details" - - elements = core.wait_for_elements(context.driver, selector, **kwargs) - return [element.text for element in elements] diff --git a/uitests/uitests/vscode/quick_input.py b/uitests/uitests/vscode/quick_input.py deleted file mode 100644 index b1c1897620aa..000000000000 --- a/uitests/uitests/vscode/quick_input.py +++ /dev/null @@ -1,16 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -from selenium.webdriver.common.keys import Keys - -from . import core - -QUICK_OPEN_GENERIC = f".quick-input-widget" -QUICK_OPEN_GENERIC_INPUT = f"{QUICK_OPEN_GENERIC} .quick-input-box input" - - -def select_value(context, value, **kwargs): - element = core.wait_for_element(context.driver, QUICK_OPEN_GENERIC_INPUT) - core.dispatch_keys(context.driver, value, element=element) - core.dispatch_keys(context.driver, Keys.ENTER, element=element) diff --git a/uitests/uitests/vscode/quick_open.py b/uitests/uitests/vscode/quick_open.py deleted file mode 100644 index b6b380c857e8..000000000000 --- a/uitests/uitests/vscode/quick_open.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import time - -from selenium.webdriver.common.keys import Keys - -import uitests.tools - -from . import core -from .selectors import get_selector - -QUICK_OPEN = "div.monaco-quick-open-widget" -QUICK_OPEN_HIDDEN = 'div.monaco-quick-open-widget[aria-hidden="true"]' -QUICK_OPEN_INPUT = f"{QUICK_OPEN} .quick-open-input input" -QUICK_OPEN_FOCUSED_ELEMENT = ( - f"{QUICK_OPEN} .quick-open-tree .monaco-tree-row.focused .monaco-highlighted-label" -) -QUICK_OPEN_ENTRY_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry' # noqa -QUICK_OPEN_ENTRY_LABEL_SELECTOR = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row .quick-open-entry .label-name' # noqa -QUICK_OPEN_ENTRY_LABEL_SELECTOR_FOCUSED = 'div[aria-label="Quick Picker"] .monaco-tree-rows.show-twisties .monaco-tree-row.focused .quick-open-entry .label-name' # noqa - - -def select_command(context, command, **kwargs): - if command == "View: Close All Editors": - try: - close_all_editors(context) - except Exception: - pass - return - if command == "Debug: Continue": - # When debugging, add a delay of 0.5s before continuing. - time.sleep(0.5) - element = _open(context, command, **kwargs) - core.dispatch_keys(context.driver, Keys.ENTER, element=element) - - -@uitests.tools.retry(AssertionError) -@uitests.tools.log_exceptions() -def close_all_editors(context): - element = _open(context, "View: Close All Editors") - core.dispatch_keys(context.driver, Keys.ENTER, element=element) - # Wait for tabs to close - time.sleep(0.5) - # If we have any editors, close them one by one. - selector = ( - 'div[id="workbench.parts.editor"] .title.tabs .tab-close a.close-editor-action' - ) - elements = context.driver.find_elements_by_css_selector(selector) - if not elements: - return - for button in elements: - button.click() - time.sleep(0.5) - elements = context.driver.find_elements_by_css_selector(selector) - if elements: - raise AssertionError("Tabs not closed") - - -def select_value(context, value): - element = core.wait_for_element(context.driver, QUICK_OPEN_INPUT) - core.dispatch_keys(context.driver, value, element=element) - core.dispatch_keys(context.driver, Keys.ENTER, element=element) - - -def wait_until_selected(context, value, **kwargs): - def find(eles): - try: - if eles[0].text == value: - return [eles[0]] - if any([ele for ele in eles if ele.text == value]): - # Check if the item that matches exactly is highlighted, - # If it is, then select that and return it - highlighted_element = core.wait_for_element( - context.driver, QUICK_OPEN_ENTRY_LABEL_SELECTOR_FOCUSED - ) - if highlighted_element.text == value: - return [highlighted_element] - return [] - - return [eles[0]] if eles[0].text == value else [] - except Exception: - return [] - - return core.wait_for_elements( - context.driver, QUICK_OPEN_ENTRY_LABEL_SELECTOR, find, **kwargs - ) - - -def _open(context, value, **kwargs): - retry = kwargs.get("retry", 30) - timeout = kwargs.get("timeout", 10) - # This is a hack, we cannot send key strokes to the electron app using selenium. - # So, lets bring up the `Go to line` input window - # then type in the character '>' to turn it into a quick input window 😊 - last_ex = None - for _ in range(retry, -1, -1): - element = core.wait_for_element( - context.driver, - get_selector("STATUS_BAR_SELECTOR", context.options.channel).format("Py"), - timeout=timeout, - ) - element.click() - try: - element = core.wait_for_element(context.driver, QUICK_OPEN_INPUT) - core.dispatch_keys(context.driver, f"> {value}", element=element) - wait_until_selected(context, value, timeout=timeout) - return element - except Exception as ex: - last_ex = ex - continue - else: - raise SystemError("Failed to open quick open") from last_ex diff --git a/uitests/uitests/vscode/screen.py b/uitests/uitests/vscode/screen.py deleted file mode 100644 index 6af2a51ad4e0..000000000000 --- a/uitests/uitests/vscode/screen.py +++ /dev/null @@ -1,51 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os - -import requests - -import uitests.vscode.application -from uitests.tools import retry - - -def get_screen_text(context): - """Gets the text from the current VSC screen.""" - - image_file = uitests.vscode.application.capture_screen_to_file(context) - - # Get endpoint and key from environment variables - endpoint = os.getenv("AZURE_COGNITIVE_ENDPOINT") - subscription_key = os.getenv("AZURE_COGNITIVE_KEY") - - if endpoint is None or subscription_key is None: - raise EnvironmentError( - "Variables AZURE_COGNITIVE_ENDPOINT, AZURE_COGNITIVE_KEY not defined" - ) - - ocr_url = f"{endpoint}vision/v2.0/ocr" - - @retry(ConnectionError, tries=10, backoff=2) - def get_result(): - headers = { - "Ocp-Apim-Subscription-Key": subscription_key, - "Content-Type": "application/octet-stream", - } - - with open(image_file, "rb") as fp: - response = requests.post(ocr_url, headers=headers, data=fp.read()) - - response.raise_for_status() - return response.json() - - result = get_result() - - # Extract the text. - line_infos = [region["lines"] for region in result["regions"]] - word_infos = [] - for line in line_infos: - for word_metadata in line: - for word_info in word_metadata["words"]: - word_infos.append(word_info.get("text")) - - return " ".join(word_infos) diff --git a/uitests/uitests/vscode/selectors.py b/uitests/uitests/vscode/selectors.py deleted file mode 100644 index f2855098dc44..000000000000 --- a/uitests/uitests/vscode/selectors.py +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -SELECTORS = { - "STATUS_BAR_SELECTOR": { - "stable": ".part.statusbar *[title='{}']", - "insider": ".part.statusbar *[title='{}'] a", - }, - "GOTO_STATUS_BAR_SELECTOR": { - "stable": 'div.statusbar-item a[title="Go to Line"]', - "insider": 'div.statusbar-item[title="Go to Line"] a', - }, -} - - -def get_selector(selector, channel="stable"): - return SELECTORS[selector][channel] or SELECTORS[selector]["stable"] diff --git a/uitests/uitests/vscode/settings.py b/uitests/uitests/vscode/settings.py deleted file mode 100644 index 1250c3ef09a4..000000000000 --- a/uitests/uitests/vscode/settings.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -import enum -import json -import os -import pathlib -import time - -import uitests.tools - - -class ConfigurationTarget(enum.Enum): - user = 0 - workspace = 1 - workspace_folder = 2 - - -def _get_workspace_file_path(context): - file_path = os.path.join( - context.options.workspace_folder, ".vscode", "settings.json" - ) - _ensure_setttings_json(file_path) - return file_path - - -def _get_user_file_path(context): - file_path = os.path.join(context.options.user_dir, "User", "settings.json") - _ensure_setttings_json(file_path) - return file_path - - -def update_workspace_settings(context, settings={}): - crud_settings = { - "type": "workspaceFolder", - "update": settings, - "workspaceFolder": context.options.workspace_folder, - } - _send_command_to_bootstrap(context, crud_settings) - - -def update_user_settings(context, settings={}): - crud_settings = {"type": "user", "update": settings} - _send_command_to_bootstrap(context, crud_settings) - - -def remove_workspace_setting(context, setting): - crud_settings = { - "type": "workspaceFolder", - "remove": [setting], - "workspaceFolder": context.options.workspace_folder, - } - _send_command_to_bootstrap(context, crud_settings) - - -def remove_user_setting(context, setting): - crud_settings = {"type": "user", "remove": [setting]} - _send_command_to_bootstrap(context, crud_settings) - - -def get_user_setting(context, setting): - return _get_setting(_get_user_file_path(context), setting) - - -# For some reason this throws an error on Widows. -@uitests.tools.retry(AssertionError) -def _ensure_setttings_json(settings_json): - os.makedirs(pathlib.Path(settings_json).parent, exist_ok=True) - if os.path.exists(settings_json): - return - with open(settings_json, "w") as file: - file.write("{}") - - -def get_workspace_setting(context, setting): - return _get_setting(_get_workspace_file_path(context), setting) - - -def _get_setting(settings_json, setting): - _ensure_setttings_json(settings_json) - existing_settings = {} - with open(settings_json, "r") as file: - existing_settings = json.loads(file.read()) - - return existing_settings.get(setting) - - -def _send_command_to_bootstrap(context, crud_settings): - """Let the bootstrap extension update the settings. This way VSC will be aware of it and extensions - will get the right values. If we update the file directly then VSC might not get notified immediately. - We'll let the bootstrap extension update the settings and delete the original file. - When the file has been deleted we know the settings have been updated and VSC is aware of the updates. - - """ - instructions_file = os.path.join( - context.options.extensions_dir, "settingsToUpdate.txt" - ) - error_file = os.path.join( - context.options.extensions_dir, "settingsToUpdate_error.txt" - ) - if os.path.exists(error_file): - os.remove(error_file) - with open(instructions_file, "w") as fp: - json.dump(crud_settings, fp, indent=4) - - uitests.vscode.quick_open.select_command(context, "Smoke: Update Settings") - uitests.vscode.application.capture_screen(context) - # Wait for 5 seconds for settings to get updated. - # If file has been deleted then yes it has been udpated, else error - for i in range(10): - if not os.path.exists(instructions_file): - return - time.sleep(0.5) - uitests.vscode.application.capture_screen(context) - - error_message = "" - if os.path.exists(error_file): - with open(error_file, "r") as fp: - error_message += fp.read() - with open(instructions_file, "r") as fp: - error_message += fp.read() - raise SystemError(f"Settings not updated by Bootstrap\n {error_message}") diff --git a/uitests/uitests/vscode/status_bar.py b/uitests/uitests/vscode/status_bar.py deleted file mode 100644 index f76cba7b68e8..000000000000 --- a/uitests/uitests/vscode/status_bar.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - - -from typing import List - -from . import constants, core - -STATUS_BAR_SELECTOR = 'div[id="workbench.parts.statusbar"]' - - -def wait_for_item_with_tooltip(context, value): - selector = f'{STATUS_BAR_SELECTOR} span[title="${value}"]' - core.wait_for_element(context.driver, selector) - - -def wait_for_python_statusbar(context, parts: List[str] = []): - selector = "div.statusbar-item.left.statusbar-entry a" - - def find(elements): - for element in elements: - if constants.PYTHON_STATUS_BAR_PREFIX not in element.text: - continue - if not parts: - return [element] - text_parts = element.text.split(" ") - if all(map(text_parts.index, parts)): - return [element] - return [] - - return core.wait_for_elements(context.driver, selector, find)[0] diff --git a/uitests/uitests/vscode/testing.py b/uitests/uitests/vscode/testing.py deleted file mode 100644 index cbee8ffd1076..000000000000 --- a/uitests/uitests/vscode/testing.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -import os.path -import time - -import uitests.tools -import uitests.vscode.core -from selenium.common.exceptions import StaleElementReferenceException -from selenium.webdriver.common.action_chains import ActionChains -from selenium.webdriver.common.keys import Keys - - -def is_explorer_sidebar_visible(context): - try: - uitests.vscode.core.wait_for_element( - context.driver, - '.composite.viewlet[id="workbench.view.extension.test"]', - retry_count=2, - ) - return True - except TimeoutError: - return False - - -def wait_for_explorer_icon(context): - selector = ".activitybar.left .actions-container a[title='Test']" - uitests.vscode.core.wait_for_element(context.driver, selector) - - -def wait_for_stop_icon(context): - selector = "div[id='workbench.parts.sidebar'] .action-item a[title='Stop']" - uitests.vscode.core.wait_for_element(context.driver, selector) - - -def wait_for_stop_hidden(context): - # Wait for tests to start and UI to get updated. - time.sleep(2) - selector = "div[id='workbench.parts.sidebar'] .action-item a[title='Stop']" - uitests.vscode.core.wait_for_element_to_be_hidden(context.driver, selector) - - -def wait_for_run_failed_icon(context): - selector = ( - "div[id='workbench.parts.sidebar'] .action-item a[title='Run Failed Tests']" - ) - uitests.vscode.core.wait_for_element(context.driver, selector) - - -def wait_for_run_failed_hidden(context): - # Wait for tests to start and UI to get updated. - time.sleep(2) - selector = ( - "div[id='workbench.parts.sidebar'] .action-item a[title='Run Failed Tests']" - ) - uitests.vscode.core.wait_for_element_to_be_hidden(context.driver, selector) - - -def stop(context): - selector = "div[id='workbench.parts.sidebar'] .action-item a[title='Stop']" - element = uitests.vscode.core.wait_for_element(context.driver, selector) - element.click() - - -def run_failed_tests(context): - selector = ( - "div[id='workbench.parts.sidebar'] .action-item a[title='Run Failed Tests']" - ) - element = uitests.vscode.core.wait_for_element(context.driver, selector) - element.click() - - -def get_node_count(context): - selector = "div[id='workbench.view.extension.test'] .monaco-tree-row" - return len(list(uitests.vscode.core.wait_for_elements(context.driver, selector))) - - -def get_node_icons(context): - selector = "div[id='workbench.view.extension.test'] .monaco-tree-row .custom-view-tree-node-item-icon" - return uitests.vscode.core.wait_for_elements(context.driver, selector) - - -def get_node_icon(context, number): - selector = f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number}) .custom-view-tree-node-item-icon" - return uitests.vscode.core.wait_for_element(context.driver, selector) - - -def get_node(context, number): - selector = ( - f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number})" - ) - return uitests.vscode.core.wait_for_elements(context.driver, selector) - - -def get_node_number(context, text): - # Node names can contain folder & file names delimited by path separator. - # Ensure we use the OS specific path separator. - text = text.replace("/", os.path.sep) - nodes = _get_node_labels(context) - return nodes.index(text) + 1 - - -@uitests.tools.retry(StaleElementReferenceException) -def _get_node_labels(context): - selector = "div[id='workbench.view.extension.test'] .monaco-tree-row .monaco-icon-label .label-name span span" - elements = context.driver.find_elements_by_css_selector(selector) - return [element.text for element in elements] - - -def _select_node(context, number): - tree = uitests.vscode.core.wait_for_element( - context.driver, ".monaco-tree.monaco-tree-instance-2" - ) - tree.click() - selector = ( - f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number})" - ) - element = context.driver.find_element_by_css_selector(selector) - action = ActionChains(context.driver) - action.context_click(element) - action.perform() - find = lambda ele: "focused" in ele.get_attribute("class") - uitests.vscode.core.wait_for_element(context.driver, selector, find) - return element - - -def click_node(context, number): - element = _select_node(context, number) - element.click() - - -def click_node_action_item(context, number, tooltip): - expand_nodes(context) - _select_node(context, number) - action = _get_action_item(context, number, tooltip) - action.click() - - -def _get_action_item(context, number, tooltip): - selector = f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({number}) .actions .action-item a.action-label.icon[title='{tooltip}']" - return context.driver.find_element_by_css_selector(selector) - - -def expand_nodes(context): - time.sleep(0.1) - start_time = time.time() - while time.time() - start_time < 5: - _expand_nodes(context) - if get_node_count(context) > 1: - return - time.sleep(0.1) - else: - raise TimeoutError("Timeout waiting to expand all nodes") - - -def _expand_nodes(context): - tree = uitests.vscode.core.wait_for_element( - context.driver, ".monaco-tree.monaco-tree-instance-2" - ) - tree.click() - for i in range(1, 5000): - selector = ( - f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({i})" - ) - element = context.driver.find_element_by_css_selector(selector) - action = ActionChains(context.driver) - action.context_click(element) - action.perform() - find = lambda ele: "focused" in ele.get_attribute("class") # noqa - uitests.vscode.core.wait_for_element(context.driver, selector, find) - css_class = element.get_attribute("class") - - if "has-children" in css_class and "expanded" not in css_class: - tree.send_keys(Keys.RIGHT) - find = lambda ele: "expanded" in ele.get_attribute("class") # noqa - uitests.vscode.core.wait_for_element(context.driver, selector, find) - - try: - selector = f"div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child({i+1})" - element = context.driver.find_element_by_css_selector(selector) - except Exception: - return - - -def get_root_node(context): - selector = "div[id='workbench.view.extension.test'] .monaco-tree-row:nth-child(1)" - return uitests.vscode.core.wait_for_element(context.driver, selector) diff --git a/webpack.config.js b/webpack.config.js deleted file mode 100644 index b65c3765b5e8..000000000000 --- a/webpack.config.js +++ /dev/null @@ -1,27 +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 = [ - // history-react - merge(datascience[0], { - devtool: 'eval' - }), - // data-explorer - merge(datascience[1], { - devtool: 'eval' - }), - // plot - merge(datascience[2], { - 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 df238e656a39..000000000000 --- a/webpack.datascience-ui.config.js +++ /dev/null @@ -1,297 +0,0 @@ - -// Note to editors, if you change this file you have to restart compile-webviews. -// It doesn't reload the config otherwise. -const webpack = require('webpack'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const FixDefaultImportPlugin = require('webpack-fix-default-import-plugin'); -const path = require('path'); -const CopyWebpackPlugin = require('copy-webpack-plugin'); -const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); -const TerserPlugin = require('terser-webpack-plugin') - -const 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', - optimization: { - minimizer: [new TerserPlugin()] - }, - 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' }), - new MonacoWebpackPlugin({ - languages: [] // force to empty so onigasm will be used - }) - ], - 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: {} - } - ] - }, - { test: /\.(png|woff|woff2|eot|ttf)$/, loader: 'url-loader?limit=100000' }, - { - test: /\.less$/, - use: [ - 'style-loader', - 'css-loader', - 'less-loader' - ] - } - ] - } - }, - { - entry: ['babel-polyfill', './src/datascience-ui/data-explorer/index.tsx'], - output: { - path: path.join(__dirname, 'out'), - filename: 'datascience-ui/data-explorer/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', - optimization: { - minimizer: [new TerserPlugin()] - }, - node: { - fs: 'empty' - }, - plugins: [ - new HtmlWebpackPlugin({ template: 'src/datascience-ui/data-explorer/index.html', imageBaseUrl: `${__dirname.replace(/\\/g, '/')}/out/datascience-ui/data-explorer`, indexUrl: `${__dirname}/out/1`, filename: './datascience-ui/data-explorer/index.html' }), - new FixDefaultImportPlugin(), - new CopyWebpackPlugin([ - { from: './**/*.png', to: '.' }, - { from: './**/*.svg', to: '.' }, - { from: './**/*.css', to: '.' }, - { from: './**/*theme*.json', to: '.' } - ], { context: 'src' }), - new webpack.DefinePlugin({ - "process.env": { - NODE_ENV: JSON.stringify("production") - } - }) - ], - 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: /\.(png|woff|woff2|eot|gif|ttf)$/, loader: 'url-loader?limit=100000' }, - { - test: /\.json$/, - type: 'javascript/auto', - include: /node_modules.*remark.*/, - use: [ - { - loader: path.resolve('./build/webpack/loaders/jsonloader.js'), - options: {} - } - ] - } - ] - } - }, - { - entry: ['babel-polyfill', './src/datascience-ui/plot/index.tsx'], - output: { - path: path.join(__dirname, 'out'), - filename: 'datascience-ui/plot/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', - optimization: { - minimizer: [new TerserPlugin()] - }, - node: { - fs: 'empty' - }, - plugins: [ - new HtmlWebpackPlugin({ template: 'src/datascience-ui/plot/index.html', imageBaseUrl: `${__dirname.replace(/\\/g, '/')}/out/datascience-ui/plot`, indexUrl: `${__dirname}/out/1`, filename: './datascience-ui/plot/index.html' }), - new FixDefaultImportPlugin(), - new CopyWebpackPlugin([ - { from: './**/*.png', to: '.' }, - { from: './**/*.svg', to: '.' }, - { from: './**/*.css', to: '.' }, - { from: './**/*theme*.json', to: '.' } - ], { context: 'src' }), - new webpack.DefinePlugin({ - "process.env": { - NODE_ENV: JSON.stringify("production") - } - }) - ], - 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: /\.(png|woff|woff2|eot|gif|ttf)$/, loader: 'url-loader?limit=100000' }, - { - test: /\.json$/, - type: 'javascript/auto', - include: /node_modules.*remark.*/, - use: [ - { - loader: path.resolve('./build/webpack/loaders/jsonloader.js'), - options: {} - } - ] - } - ] - } - } -];